From 696ce65be4d812ac9a62e29c72e71b46fe895c9e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 18 Sep 2026 08:52:42 +0000 Subject: [PATCH 1/2] Make frequency the one lit reading, with time beneath it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In use the frequency at the cursor is by far the most important number in the panel, but the readout column set time above it at the same size, so the two competed for the eye and neither was the first read. Frequency is now the only large, lit reading. Time sits immediately beneath it at half the size, captioned, in the same grey as the panel's labels, and the doppler speed at the foot takes that same second rank rather than staying in ink at 19px — two secondary rows in different colours would have read as a third rank. The hairline between the two large readings goes with the second large reading. The LEDs keep their accessible names and classes, so no locator changes. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01CrHtVussSR8ZxsxisaepJM --- CLAUDE.md | 6 +++-- docs/Control-Panel.md | 12 +++++++--- src/components/CursorReadout.js | 35 +++++++++++++++-------------- src/gramframe.css | 39 ++++++++++++++++++++------------- 4 files changed, 56 insertions(+), 36 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 97c8a1d..b066906 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -181,8 +181,10 @@ Every path below exists; keep this list in step with `src/` when adding modules. - `GuidancePanel.js` - The armed mode's guidance column, and the 40px rail it collapses to. Collapse is remembered per user; with no stored choice the column decides by the panel's own width - - `CursorReadout.js` - The instrument face: time, frequency and doppler speed. - Reads the pointer, or the selected feature when there is one + - `CursorReadout.js` - The instrument face: frequency as the one large, lit + reading, with time immediately beneath it and doppler speed at the foot, + both at half the size in the label grey. Reads the pointer, or the selected + feature when there is one - `AnnotationTables.js` - The three table columns, their headers, count chips and the "Clear all annotations" footer - `icons.js` - The button glyphs, drawn as inline SVG in `currentColor` so diff --git a/docs/Control-Panel.md b/docs/Control-Panel.md index 8bf704c..c2c1edb 100644 --- a/docs/Control-Panel.md +++ b/docs/Control-Panel.md @@ -88,9 +88,15 @@ The stored preference distinguishes all three (`true` / `false` / absent). Were ### 3. The cursor readouts The panel's anchor, and the only part styled as an instrument: recessed ground, -large monospaced tabular numerals, and a faint bloom on the two readings the -pointer produces. Doppler speed is fenced off at the foot between two rules at -half the size, because it is a derived quantity rather than a coordinate. +monospaced tabular numerals, and a faint bloom on the one reading that is lit. + +That reading is the **frequency**. In use it is by far the most important +number in the panel, and the first pass set time at the same size directly +above it, so the two competed for the eye and neither was the first read. Now +frequency is the one large reading, and **time** sits immediately beneath it at +half the size, captioned, in the same grey as the panel's labels rather than +in ink. Doppler speed is fenced off at the foot between two rules at that same +second rank, because it is a derived quantity rather than a coordinate. The column has two targets, named by its kicker: diff --git a/src/components/CursorReadout.js b/src/components/CursorReadout.js index 752eb03..4d19afb 100644 --- a/src/components/CursorReadout.js +++ b/src/components/CursorReadout.js @@ -1,10 +1,14 @@ /** * The instrument face: the numbers the panel is anchored on. * - * Three readouts in a recessed column — time, frequency, and the doppler speed - * pinned to the foot — set large, monospaced and tabular so a digit never - * shifts sideways as it changes. Everything else in the control row is a - * control; this is the measurement, and it is styled to look like one. + * Three readouts in a recessed column, in two ranks. Frequency is the one + * large, lit reading: in use it is by far the most important number in the + * panel, and giving time the same size made the two compete for the eye. + * Time sits immediately beneath it at half the size in the label grey, and + * the doppler speed is pinned to the foot at the same rank. All three are + * monospaced and tabular so a digit never shifts sideways as it changes. + * Everything else in the control row is a control; this is the measurement, + * and it is styled to look like one. * * The column has two targets, named by its kicker. With nothing selected it * follows the pointer over the gram ("CURSOR"). With a feature selected it @@ -42,26 +46,25 @@ export function createCursorReadout() { kicker.textContent = 'Cursor' column.appendChild(kicker) - const timeLED = createLEDDisplay('Time (mm:ss)', formatTime(0), 'MM:SS') - column.appendChild(timeLED) - - // A hairline fading out to the right, rather than a full rule: it separates - // the two readings without drawing a box around either. - const separator = document.createElement('div') - separator.className = 'gram-frame-readout-separator' - column.appendChild(separator) - const freqLED = createLEDDisplay('Frequency (Hz)', '0.0', 'HZ') freqLED.classList.add('gram-frame-led-accent') column.appendChild(freqLED) + // Time is the second-rank reading: directly under the frequency, at half its + // size and in the label grey, with its own caption since it no longer has + // the size to name itself. It is not fenced like doppler — it belongs with + // the frequency, as the other half of the pointer's coordinate. + const timeLED = createLEDDisplay('Time (mm:ss)', formatTime(0), 'MM:SS', 'Time') + timeLED.classList.add('gram-frame-led-secondary') + column.appendChild(timeLED) + const spacer = document.createElement('div') spacer.className = 'gram-frame-readout-spacer' column.appendChild(spacer) // Doppler is a derived quantity rather than a coordinate, so it is fenced off - // at the foot between two rules and set at half the size — present, but not - // competing with the two readings the pointer actually produces. + // at the foot between two rules, at the same second rank as time — present, + // but not competing with the reading the column exists for. // // Its accessible name stays the full "Doppler Speed (kts)" while the caption // beside the number is the one word there is room for. The gap in that name @@ -69,7 +72,7 @@ export function createCursorReadout() { // the source; Playwright normalises it to a plain space, so the // `:text-is("Doppler Speed (kts)")` locators in tests/helpers still match. const speedLED = createLEDDisplay('Doppler\u00a0Speed (kts)', '0.0', 'KTS', 'Doppler') - speedLED.classList.add('gram-frame-led-inline') + speedLED.classList.add('gram-frame-led-secondary', 'gram-frame-led-inline') column.appendChild(speedLED) return { column, timeLED, freqLED, speedLED, kicker } diff --git a/src/gramframe.css b/src/gramframe.css index 5e2d405..51bb8b4 100644 --- a/src/gramframe.css +++ b/src/gramframe.css @@ -1168,12 +1168,6 @@ table.gram-config.gram-frame-config-error tr:first-child td::after { white-space: nowrap; } -.gram-frame-readout-separator { - height: 1px; - margin: 9px 0; - background: linear-gradient(to right, var(--gf-divider), transparent); -} - .gram-frame-readout-spacer { flex: 1; } @@ -1192,7 +1186,8 @@ table.gram-config.gram-frame-config-error tr:first-child td::after { font-variant-numeric: tabular-nums; letter-spacing: -0.01em; /* The faint bloom is what makes it read as a lit instrument rather than as - large text. It is the only glow in the panel, and only on these two. */ + large text. It is the only glow in the panel, and only the frequency + keeps it: the two secondary readings switch it off below. */ text-shadow: 0 0 18px rgba(247, 247, 250, 0.2); } @@ -1214,22 +1209,36 @@ table.gram-config.gram-frame-config-error tr:first-child td::after { text-transform: uppercase; } -/* The doppler speed: a derived quantity, so it is fenced off at the foot - between two rules and set at half the size of the two readings the pointer - itself produces. */ -.gram-frame-led-inline { +/* The second rank: time and the doppler speed. Half the frequency's size, no + bloom, and set in the same grey as the panel's labels rather than in ink, + so the frequency is the one reading that is lit. The caption on the left + names the row, since at this size the number cannot name itself. */ +.gram-frame-led-secondary { flex: none; - padding: 9px 0; - border-top: 1px solid var(--gf-divider); - border-bottom: 1px solid var(--gf-divider); } -.gram-frame-led-inline .gram-frame-led-value { +.gram-frame-led-secondary .gram-frame-led-value { font-size: 19px; + color: var(--gf-muted); text-shadow: none; margin-left: auto; } +/* Time sits directly under the frequency, as the other half of the pointer's + coordinate; a small gap rather than a rule, so they read as one reading and + its qualifier. */ +.gram-frame-led-secondary:not(.gram-frame-led-inline) { + margin-top: 6px; +} + +/* The doppler speed is a derived quantity rather than a coordinate, so it is + also fenced off at the foot between two rules. */ +.gram-frame-led-inline { + padding: 9px 0; + border-top: 1px solid var(--gf-divider); + border-bottom: 1px solid var(--gf-divider); +} + /* --- Column 4: the style panel ------------------------------------------- */ .gram-frame-color-picker { From 032b2bb5eeead646f1a68764955101700c632e89 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 18 Sep 2026 08:54:25 +0000 Subject: [PATCH 2/2] Port the multi-tab merge test clock pin from main (#333) The fast lane on this PR fails on four cases in tests/unit/multi-tab-merge.test.js, exactly as the base branch does: they left `now` to the wall clock while asserting on tombstones dated 2026-09-05, so they have failed on every run since the seven-day TTL passed. Not this PR's change, and not the design branch's either; main already carries the fix in 9147c49 (PR #333), which this commit ports verbatim. It no-ops once the design branch takes main. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01CrHtVussSR8ZxsxisaepJM --- tests/unit/multi-tab-merge.test.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/tests/unit/multi-tab-merge.test.js b/tests/unit/multi-tab-merge.test.js index 934d134..dfe4b49 100644 --- a/tests/unit/multi-tab-merge.test.js +++ b/tests/unit/multi-tab-merge.test.js @@ -22,6 +22,15 @@ import { mergeStoredAnnotations } from '../../src/core/storage.js' /** No deletions. */ const noTombstones = () => ({ markers: {}, harmonicSets: {}, sidebandSets: {}, doppler: null }) +/** + * The clock every case runs under unless it says otherwise: midday on the day + * the fixtures are dated. `mergeStoredAnnotations` defaults `now` to the wall + * clock, and a tombstone older than the TTL is forgotten -- so a case dated + * 2026-09-05 that left `now` to the wall clock passed until 2026-09-12 and + * failed every day after. Pinned here, once, so the fixtures never age. + */ +const NOW = Date.parse('2026-09-05T12:00:00.000Z') + /** * The ids in a list of features, sorted -- what most of these cases assert. * @param {any[]} features - Markers or pin sets @@ -38,13 +47,11 @@ const ids = features => features.map((/** @type {any} */ f) => f.id).sort() * guarded at each of the fifty-odd reads below (R9-10, issue #262). * @param {any} mine - What this tab would write * @param {any} theirs - What is already stored - * @param {number} [now] - Current epoch milliseconds, for tombstone pruning + * @param {number} [now=NOW] - Current epoch milliseconds, for tombstone pruning * @returns {any} The merged record */ -function merge(mine, theirs, now = undefined) { - const merged = now === undefined - ? mergeStoredAnnotations(mine, theirs) - : mergeStoredAnnotations(mine, theirs, now) +function merge(mine, theirs, now = NOW) { + const merged = mergeStoredAnnotations(mine, theirs, now) expect(merged).not.toBeNull() return merged }