From b4912f5879d9231ccb5a523ee4c347a2128c3b87 Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Sun, 2 Aug 2026 19:18:47 +0200 Subject: [PATCH] [fix](trx-frontend-http): render the header menus above the page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both header dropdowns were laid out inside the bar rather than over the page. The navigation menu opened as an 18px sliver positioned above its own button, and the overflow menu did not appear at all. Two causes. The tab strip kept `overflow-x: auto` from when it scrolled, which clips an absolutely positioned descendant — and the strip is what the navigation menu anchors to. The strip no longer scrolls, since the occasional destinations moved behind More, so the property and the edge fade that went with it are both gone. Anchoring in fixed coordinates at open time addresses the general case: an absolutely positioned menu is clipped by any scrolling ancestor and trapped inside whatever stacking context its ancestors create, so it can be squashed inside the bar or painted underneath page content. Fixed coordinates answer to the viewport, and the menu flips above its button near the bottom edge. Clearing `right` when setting `left` keeps the menus at their natural width: the stylesheet pins them to the right of their anchor, and leaving that in place stretched them across the bar — 845px for a four-item list. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01GdyUjuXejCEfiub675z6cz Signed-off-by: Stan Grams --- .../assets/web/generated/app.js | 24 +++++++++++++- .../trx-frontend-http/assets/web/style.css | 21 ++---------- .../trx-frontend-http/frontend/src/ui-core.ts | 33 ++++++++++++++++++- 3 files changed, 58 insertions(+), 20 deletions(-) diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js index f469756a..30341776 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js @@ -748,6 +748,24 @@ function elementById(id) { } } const overflowOrder = [".operator-layout-picker", ".header-style-pick", "#theme-toggle"]; + function anchorMenu(button, menu) { + const rect = button.getBoundingClientRect(); + menu.style.position = "fixed"; + menu.style.zIndex = "200"; + menu.style.top = "0px"; + menu.style.bottom = "auto"; + menu.style.right = "auto"; + const width = menu.offsetWidth || 200; + const height = menu.offsetHeight || 0; + menu.style.left = `${String(Math.round(Math.min(Math.max(8, rect.right - width), window.innerWidth - width - 8)))}px`; + const below = rect.bottom + 8; + if (below + height <= window.innerHeight - 8) { + menu.style.top = `${String(Math.round(below))}px`; + } else { + menu.style.top = "auto"; + menu.style.bottom = `${String(Math.round(window.innerHeight - rect.top + 8))}px`; + } + } function installTopBarOverflow() { const actions = document.querySelector(".top-bar-actions"); if (!actions || document.getElementById("top-bar-more")) return; @@ -777,6 +795,7 @@ function elementById(id) { button.addEventListener("click", () => { const open = menu.classList.toggle("is-open"); button.setAttribute("aria-expanded", String(open)); + if (open) anchorMenu(button, menu); }); document.addEventListener("click", (event) => { if (!(event.target instanceof Node) || !wrap.contains(event.target)) closeMenu(); @@ -851,7 +870,10 @@ function elementById(id) { more.addEventListener("click", () => { const open = menu.classList.toggle("is-open"); more.setAttribute("aria-expanded", String(open)); - if (open) menu.querySelector('[role="menuitem"]')?.focus(); + if (open) { + anchorMenu(more, menu); + menu.querySelector('[role="menuitem"]')?.focus(); + } }); document.addEventListener("click", (event) => { if (!(event.target instanceof Node) || !menu.contains(event.target) && !more.contains(event.target)) closeMore(); diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css index ce261f02..b75b9a3b 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css @@ -1531,25 +1531,10 @@ small { color: var(--text-muted); } align-items: center; gap: 0.2rem; min-width: 0; - /* 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. */ + /* Never wraps and never scrolls: the occasional destinations live behind + * More, so four tabs plus More always fit. overflow-x here would clip the + * dropdown this element anchors. */ flex-wrap: nowrap; - overflow-x: auto; - scrollbar-width: thin; -} -/* Fade the trailing edge so a tab scrolled out of view reads as "there is more" - * rather than as a label cut mid-word. A mask rather than a colour-matched - * cover gradient: the card is transparent, so a cover would have to track the - * page background across both themes and all nine styles. Only the trailing - * edge is faded, so the first tab stays crisp when the strip is at rest. */ -.tab-bar-nav { - -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 2rem), transparent 100%); - mask-image: linear-gradient(to right, #000 calc(100% - 2rem), transparent 100%); -} -.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; } /* Icons before scrolling: every tab already carries one, and four icons plus diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/ui-core.ts b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/ui-core.ts index c1da0358..45285c14 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/ui-core.ts +++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/ui-core.ts @@ -326,6 +326,33 @@ function elementById(id: string): T { // Audio, record and the rig picker are the operating controls and stay. const overflowOrder = [".operator-layout-picker", ".header-style-pick", "#theme-toggle"]; + // Anchored at paint time in fixed coordinates. An absolutely positioned + // dropdown is clipped by any scrolling ancestor and trapped inside the + // stacking context its ancestors create, so it can end up squashed inside + // the bar or painted underneath page content. Fixed positioning answers to + // the viewport instead, and the menu flips above its button near the bottom + // edge. + function anchorMenu(button: HTMLElement, menu: HTMLElement) { + const rect = button.getBoundingClientRect(); + menu.style.position = "fixed"; + menu.style.zIndex = "200"; + menu.style.top = "0px"; + menu.style.bottom = "auto"; + // The stylesheet pins these menus to the right of their anchor; leaving + // that in place while setting a left would stretch them across the bar. + menu.style.right = "auto"; + const width = menu.offsetWidth || 200; + const height = menu.offsetHeight || 0; + menu.style.left = `${String(Math.round(Math.min(Math.max(8, rect.right - width), window.innerWidth - width - 8)))}px`; + const below = rect.bottom + 8; + if (below + height <= window.innerHeight - 8) { + menu.style.top = `${String(Math.round(below))}px`; + } else { + menu.style.top = "auto"; + menu.style.bottom = `${String(Math.round(window.innerHeight - rect.top + 8))}px`; + } + } + function installTopBarOverflow() { const actions = document.querySelector(".top-bar-actions"); if (!actions || document.getElementById("top-bar-more")) return; @@ -356,6 +383,7 @@ function elementById(id: string): T { button.addEventListener("click", () => { const open = menu.classList.toggle("is-open"); button.setAttribute("aria-expanded", String(open)); + if (open) anchorMenu(button, menu); }); document.addEventListener("click", (event) => { if (!(event.target instanceof Node) || !wrap.contains(event.target)) closeMenu(); @@ -445,7 +473,10 @@ function elementById(id: string): T { more.addEventListener("click", () => { const open = menu.classList.toggle("is-open"); more.setAttribute("aria-expanded", String(open)); - if (open) menu.querySelector('[role="menuitem"]')?.focus(); + if (open) { + anchorMenu(more, menu); + menu.querySelector('[role="menuitem"]')?.focus(); + } }); document.addEventListener("click", (event) => { if (!(event.target instanceof Node) || (!menu.contains(event.target) && !more.contains(event.target))) closeMore(); -- 2.55.0