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();