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

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>
This commit is contained in:
sjg
2026-08-02 18:45:43 +02:00
co-authored by Claude Opus 5
parent b31790ff48
commit eee3630f04
3 changed files with 225 additions and 16 deletions
@@ -1229,12 +1229,20 @@ small { color: var(--text-muted); }
gap: 0;
flex-shrink: 0;
}
/* One row, always. Controls that do not fit are moved into the overflow menu
* by ui-core rather than wrapping: wrapping made the header's height depend on
* the viewport width in a way that was not even monotonic (112px at 1440,
* 169px at 1100, 131px at 900), which is what made the bar feel unstable. */
.top-bar-actions {
display: flex;
align-items: flex-start;
gap: 0.45rem 0.6rem;
align-items: center;
gap: 0.4rem;
min-width: 0;
flex-wrap: wrap;
flex-wrap: nowrap;
}
/* Every control in the bar is the same height and none of them stretch. */
.top-bar-actions > * {
flex: 0 0 auto;
}
.header-bar-btn.header-audio-btn {
width: 2rem;
@@ -1403,15 +1411,19 @@ small { color: var(--text-muted); }
gap: 0.8rem;
padding: 0.9rem 0.2rem 0;
}
/* Inline, not stacked: the summary used to sit under the select, making this
* the only 54px control in a bar of 32px ones. */
.header-rig-switch {
display: grid;
grid-template-columns: minmax(0, 1fr);
align-items: start;
gap: 0.35rem;
display: flex;
align-items: center;
gap: 0.4rem;
min-width: 0;
}
.header-rig-summary {
display: block;
max-width: 20rem;
max-width: 12rem;
flex: 0 1 auto;
min-width: 0;
color: var(--text-muted);
font-size: var(--fs-xs);
line-height: 1.35;
@@ -1531,8 +1543,18 @@ small { color: var(--text-muted); }
align-items: center;
gap: 0.2rem;
min-width: 0;
flex-wrap: wrap;
/* Scroll rather than wrap: eight labels wrapped into a ragged block (four
* rows at 1100px) and made the header's height depend on the viewport. */
flex-wrap: nowrap;
overflow-x: auto;
scrollbar-width: thin;
}
.tab-bar-nav::-webkit-scrollbar { height: 3px; }
.tab-bar-nav::-webkit-scrollbar-thumb {
border-radius: 3px;
background: color-mix(in srgb, var(--border-light) 70%, transparent);
}
.tab-bar-nav .tab { flex: 0 0 auto; }
.tab {
background: transparent;
border: none;
@@ -2964,7 +2986,10 @@ button.is-active {
text-transform: uppercase;
overflow: visible;
}
.operator-layout-picker::before { content: "Layout"; }
/* The prefix made the least-used control the widest in the bar; the
* select carries its own aria-label and title instead. */
.operator-layout-picker { padding-left: 0; }
.operator-layout-picker select { max-width: 8rem; }
.operator-layout-picker select {
width: auto;
min-height: 2rem;
@@ -3282,24 +3307,26 @@ body[data-operator-layout="broadcast"] #cw-bar-overlay {
.toast-region { bottom: calc(5.7rem + env(safe-area-inset-bottom)); }
.top-bar-actions {
width: 100%;
justify-content: flex-start;
flex-wrap: wrap;
gap: 0.45rem;
justify-content: flex-end;
flex-wrap: nowrap;
gap: 0.4rem;
}
.operator-layout-picker { max-width: 100%; }
.operator-layout-picker select { max-width: 10rem; }
.header-rig-switch,
.header-style-pick {
flex: 1 1 12rem;
flex: 0 0 auto;
min-width: 0;
}
.header-rig-switch select,
.header-style-pick select {
width: 100%;
width: auto;
min-width: 0;
}
/* Rig hardware/feature detail is on the left subtitle and the About tab. */
.header-rig-summary { display: none; }
.header-bar-btn {
flex: 1 1 calc(50% - 0.3rem);
flex: 0 0 auto;
min-width: 0;
}
#tab-main,
@@ -5379,3 +5406,34 @@ body[data-operator-layout="broadcast"] #cw-bar-overlay {
font-size: max(0.75rem, 12px);
}
}
/* --- Top bar overflow menu ------------------------------------------------ */
.top-bar-more { position: relative; }
.top-bar-more[hidden] { display: none; }
.top-bar-more-btn {
width: 2rem;
height: 2rem;
min-height: 2rem;
padding: 0;
font-size: 1rem;
line-height: 1;
}
.top-bar-more-menu {
position: absolute;
top: calc(100% + 0.35rem);
right: 0;
z-index: 40;
display: none;
flex-direction: column;
gap: 0.4rem;
min-width: 12rem;
padding: 0.5rem;
border: 1px solid var(--border-light);
border-radius: var(--radius-md);
background: var(--surface);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.28);
}
.top-bar-more-menu.is-open { display: flex; }
.top-bar-more-menu > * { width: 100%; }
.top-bar-more-menu select { width: 100%; max-width: none; }