[feat](trx-frontend-http): compact single-row top bar #25

Merged
sjg merged 2 commits from feat/compact-top-bar into main 2026-08-02 18:50:53 +02:00
Owner

What was wrong

The header's height depended on the viewport width, and not even monotonically:

width before after
1440 112px, controls in 2 rows, tabs in 2 72px, one row
1280 72px
1100 169px, tabs across four ragged rows 72px
900 131px 72px
720 246px 72px
480 191px 76px

Both control groups wrapped, so every width produced a different ragged block. Four different control heights (32, 34, 45 and 54px) sat in the same row — the 54px one being the rig picker with its summary stacked underneath — and on narrow viewports the icon buttons stretched to fill half the row, rendering a play triangle centred in a 249px box.

The change

Both groups are now one row that never wraps. Controls are a uniform 2rem and no longer stretch, the rig summary sits inline beside its select, and the page tabs scroll instead of wrapping with the trailing edge faded so a partly-scrolled tab reads as "there is more" rather than as a label cut mid-word.

Secondary controls — layout, style and theme — move into a overflow menu when the bar cannot hold them, leaving audio, record and the rig picker inline:

▶  REC  [Primary fixture ▾]  Smoke Fixture · RX · FM  ⋯

Deciding when things fit

This is the part worth reviewing. Two obvious tests are both wrong here, and only screenshots caught it:

  • A viewport-width threshold left the theme toggle clipped at the card edge at 1440px. How much fits depends on the rig name and the label text, not the window.
  • bar.scrollWidth > bar.clientWidth reports overflow at 1600px when nothing is clipped, so every control got banished to the menu.
  • "the nav isn't scrolling" is unsatisfiable: with min-width: 0 the nav always shrinks to the leftover space, so it always reports scrolling.

What works is comparing natural widths against the space available — scrollWidth on a scroll container is its unconstrained content width:

const needed = identity.offsetWidth + nav.scrollWidth + actions.scrollWidth + gutters;
return needed <= bar.clientWidth;

Verification

Measured in real Chromium at 1600, 1440, 1280, 1100, 900, 720 and 480px: header 72px throughout (76px at 480), every control 32px, nothing clipped at any width. Full gate green — typecheck, lint, 34/34 test, build, test:browser.

Notes for review

  • The menu is in use at every width. The card's max-width (~1100px) means eight tabs plus the identity block never leave room for the secondary controls inline. If they should appear inline on wide screens, the card max-width is the thing to change, not the bar.
  • The tab-strip fade is not scroll-aware. Pure CSS cannot detect scroll position, so the last tab keeps a slight fade even when scrolled fully right. The background-attachment: local alternative is scroll-aware but needs a colour-matched cover gradient, and .card is transparent — it would have to track the page background across both themes and all nine styles. A mask is colour-agnostic; making it scroll-aware would need a scroll listener.
  • No automated regression test for the geometry. The DOM fixture used by the unit tests cannot measure layout. The numbers above come from ad-hoc Chromium measurement. Folding a "header stays one row" assertion into browser-smoke.mjs would guard it if that is wanted.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GdyUjuXejCEfiub675z6cz

## What was wrong The header's height depended on the viewport width, and not even monotonically: | width | before | after | | --- | --- | --- | | 1440 | 112px, controls in 2 rows, tabs in 2 | **72px**, one row | | 1280 | — | **72px** | | 1100 | 169px, tabs across **four ragged rows** | **72px** | | 900 | 131px | **72px** | | 720 | 246px | **72px** | | 480 | 191px | **76px** | Both control groups wrapped, so every width produced a different ragged block. Four different control heights (32, 34, 45 and 54px) sat in the same row — the 54px one being the rig picker with its summary stacked underneath — and on narrow viewports the icon buttons stretched to fill half the row, rendering a play triangle centred in a 249px box. ## The change Both groups are now one row that never wraps. Controls are a uniform 2rem and no longer stretch, the rig summary sits inline beside its select, and the page tabs scroll instead of wrapping with the trailing edge faded so a partly-scrolled tab reads as "there is more" rather than as a label cut mid-word. Secondary controls — layout, style and theme — move into a `⋯` overflow menu when the bar cannot hold them, leaving audio, record and the rig picker inline: ``` ▶ REC [Primary fixture ▾] Smoke Fixture · RX · FM ⋯ ``` ## Deciding when things fit This is the part worth reviewing. Two obvious tests are both wrong here, and only screenshots caught it: - **A viewport-width threshold** left the theme toggle clipped at the card edge at 1440px. How much fits depends on the rig name and the label text, not the window. - **`bar.scrollWidth > bar.clientWidth`** reports overflow at 1600px when nothing is clipped, so every control got banished to the menu. - **"the nav isn't scrolling"** is unsatisfiable: with `min-width: 0` the nav always shrinks to the leftover space, so it always reports scrolling. What works is comparing *natural* widths against the space available — `scrollWidth` on a scroll container is its unconstrained content width: ```ts const needed = identity.offsetWidth + nav.scrollWidth + actions.scrollWidth + gutters; return needed <= bar.clientWidth; ``` ## Verification Measured in real Chromium at 1600, 1440, 1280, 1100, 900, 720 and 480px: header 72px throughout (76px at 480), every control 32px, nothing clipped at any width. Full gate green — `typecheck`, `lint`, 34/34 `test`, `build`, `test:browser`. ## Notes for review - **The `⋯` menu is in use at every width.** The card's max-width (~1100px) means eight tabs plus the identity block never leave room for the secondary controls inline. If they should appear inline on wide screens, the card max-width is the thing to change, not the bar. - **The tab-strip fade is not scroll-aware.** Pure CSS cannot detect scroll position, so the last tab keeps a slight fade even when scrolled fully right. The `background-attachment: local` alternative *is* scroll-aware but needs a colour-matched cover gradient, and `.card` is transparent — it would have to track the page background across both themes and all nine styles. A mask is colour-agnostic; making it scroll-aware would need a scroll listener. - **No automated regression test for the geometry.** The DOM fixture used by the unit tests cannot measure layout. The numbers above come from ad-hoc Chromium measurement. Folding a "header stays one row" assertion into `browser-smoke.mjs` would guard it if that is wanted. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01GdyUjuXejCEfiub675z6cz
sjg added 2 commits 2026-08-02 18:50:35 +02:00
The header's height depended on the viewport width, and not even
monotonically: 112px at 1440, 169px at 1100, 131px at 900, 246px at 720.
Both control groups wrapped, so every width produced a different ragged
block — eight page tabs across four rows at 1100px, and action controls
across three.  Four different control heights (32, 34, 45 and 54px) sat
in the same row, the 54px one being the rig picker with its summary
stacked underneath, and on narrow viewports the icon buttons stretched to
fill half the row, rendering a play triangle centred in a 249px box.

Lay both groups out as one row that never wraps.  Controls are a uniform
2rem and no longer stretch, the rig summary sits inline beside its select,
and the page tabs scroll instead of wrapping.  Secondary controls —
layout, style and theme — move into an overflow menu when the bar cannot
hold them, leaving audio, record and the rig picker inline.

Deciding when they no longer fit needs natural widths, not rendered ones:
the nav has min-width 0 and scrolls, so it always shrinks to the leftover
space and always reports scrolling, and the bar reports overflow even when
nothing is clipped.  scrollWidth on the scroll container is its
unconstrained content width, which is what the fit test compares against
the space available.

Measured after the change: 72px at 1440, 1280, 1100, 900 and 480, every
control 32px, nothing clipped at any width.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdyUjuXejCEfiub675z6cz
Signed-off-by: Stan Grams <sjg@haxx.space>
[style](trx-frontend-http): fade the scrolled tab strip edge
CI / lint (pull_request) Successful in 2m19s
CI / test (pull_request) Successful in 8m17s
CI / frontend (pull_request) Successful in 3m0s
CI / reuse (pull_request) Successful in 3s
CI / lint (push) Successful in 2m17s
CI / test (push) Successful in 7m33s
CI / frontend (push) Successful in 2m9s
CI / reuse (push) Successful in 3s
dd5760c436
The page tabs scroll rather than wrap, so the last visible tab was sliced
mid-word ("Se…" for Settings), which reads as a rendering fault instead of
as an invitation to scroll.

Fade the trailing edge with a mask.  A colour-matched cover gradient is
the usual trick, but the card is transparent, so a cover would have to
track the page background across both themes and all nine styles; a mask
is colour-agnostic.  Only the trailing edge is faded, leaving the first
tab crisp while the strip sits at rest.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdyUjuXejCEfiub675z6cz
Signed-off-by: Stan Grams <sjg@haxx.space>
sjg merged commit dd5760c436 into main 2026-08-02 18:50:53 +02:00
sjg deleted branch feat/compact-top-bar 2026-08-02 18:50:53 +02:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sjg/trx-rs#25