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 { 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 }