[fix](trx-frontend-http): render the header menus above the page
CI / lint (push) Successful in 2m17s
CI / test (push) Successful in 7m23s
CI / lint (pull_request) Successful in 2m16s
CI / test (pull_request) Successful in 8m13s
CI / frontend (pull_request) Successful in 2m59s
CI / reuse (pull_request) Successful in 3s
CI / frontend (push) Successful in 2m10s
CI / reuse (push) Successful in 3s

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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdyUjuXejCEfiub675z6cz
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit was merged in pull request #27.
This commit is contained in:
sjg
2026-08-02 19:18:47 +02:00
co-authored by Claude Opus 5
parent 27f2558193
commit b4912f5879
3 changed files with 58 additions and 20 deletions
@@ -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();
@@ -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