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 3d41f224..0b3c9f52 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 @@ -818,6 +818,8 @@ function elementById(id) { return needed <= bar.clientWidth; }; const reflowOverflow = () => { + const nav = document.querySelector(".tab-bar-nav"); + nav?.classList.remove("nav-icons-only"); overflowOrder.forEach((selector) => { const element = menu.querySelector(selector); if (element) actions.insertBefore(element, wrap); @@ -830,6 +832,7 @@ function elementById(id) { wrap.hidden = false; menu.appendChild(element); } + if (nav && !barFits()) nav.classList.add("nav-icons-only"); wrap.hidden = menu.children.length === 0; if (wrap.hidden) closeMenu(); }; 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 c74ed041..3ae4a766 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 @@ -1562,11 +1562,17 @@ small { color: var(--text-muted); } } .tab-bar-nav .tab { flex: 0 0 auto; } /* Icons before scrolling: every tab already carries one, and four icons plus - * More always fit, so the strip never has to hide a destination. */ -@media (max-width: 1360px) and (min-width: 701px) { - .tab-bar-nav .tab .tab-label { display: none; } - .tab-bar-nav .tab .tab-icon, .tab-bar-nav .tab .tab-more-icon { display: block; } - .tab-bar-nav .tab { padding: 0.5rem 0.6rem; } + * More always fit, so the strip never has to hide a destination. Applied by + * measurement (ui-core's reflowOverflow) rather than at a viewport width: how + * much fits depends on the rig name and on how wide the platform renders the + * labels, so the same width fits on one machine and clips on another. + * Bounded below 761px, where the strip becomes the bottom nav and keeps its + * labels under the icons. */ +@media (min-width: 761px) { + .tab-bar-nav.nav-icons-only .tab .tab-label { display: none; } + .tab-bar-nav.nav-icons-only .tab .tab-icon, + .tab-bar-nav.nav-icons-only .tab .tab-more-icon { display: block; } + .tab-bar-nav.nav-icons-only .tab { padding: 0.45rem 0.55rem; } } /* The selected destination is boxed, not underlined — the same treatment the mobile bottom nav already used, so one navigation model reads the same at 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 52c7ac59..a67dc939 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 @@ -422,6 +422,10 @@ function elementById(id: string): T { return needed <= bar.clientWidth; }; const reflowOverflow = () => { + const nav = document.querySelector(".tab-bar-nav"); + // Measure from the roomiest state every time, so the decision is a + // function of the current widths alone and cannot ratchet. + nav?.classList.remove("nav-icons-only"); overflowOrder.forEach((selector) => { const element = menu.querySelector(selector); if (element) actions.insertBefore(element, wrap); @@ -434,6 +438,12 @@ function elementById(id: string): T { wrap.hidden = false; menu.appendChild(element); } + // Last resort, once every movable control is already in the menu: drop + // the tabs to their icons. Without it the nav — which may shrink below + // its content — keeps its tabs at full width and runs them under the + // controls, so the destinations nearest the controls become unclickable. + // Icon widths are fixed, so this always buys back the labels' width. + if (nav && !barFits()) nav.classList.add("nav-icons-only"); wrap.hidden = menu.children.length === 0; if (wrap.hidden) closeMenu(); }; diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/browser-smoke.mjs b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/browser-smoke.mjs index 95a55a65..c9e0bcd3 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/browser-smoke.mjs +++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/browser-smoke.mjs @@ -169,6 +169,34 @@ try { assert.ok(menu.height > 40 && menu.width > 80, `menu rendered ${menu.width}x${menu.height}`); assert.ok(menu.onTop, "menu is painted underneath the page"); + // Wider text than this machine renders. The checks above passed on macOS + // while CI, whose system font is wider, put the tab strip 9px into the + // controls: the bar had run out of controls to move into the overflow menu + // and the tabs kept their full width anyway. Scaling the bar's own text + // reproduces that on any machine. The tabs carry their font size themselves, + // so the parent size alone does not cascade to them. + await page.addStyleTag({ content: ` + .tab-bar { font-size: 160%; } + .tab-bar .tab, .tab-bar select, .tab-bar button { font-size: 1.52rem; } + ` }); + for (const width of [1440, 1280, 1100]) { + await page.setViewportSize({ width, height: 900 }); + await page.waitForTimeout(250); + const crowded = await page.evaluate(() => { + const nav = document.querySelector(".tab-bar-nav"); + const actions = document.querySelector(".top-bar-actions"); + const tabs = [...nav.querySelectorAll(".tab")].filter((tab) => tab.offsetParent !== null); + return { + overlap: Math.round(Math.max(...tabs.map((tab) => tab.getBoundingClientRect().right)) + - actions.getBoundingClientRect().left), + iconsOnly: nav.classList.contains("nav-icons-only"), + }; + }); + assert.ok(crowded.overlap <= 0, + `with wider text the tab strip overlaps the controls by ${crowded.overlap}px at ${width}px`); + assert.equal(crowded.iconsOnly, true, `the strip kept its labels at ${width}px with no room for them`); + } + } finally { await browser.close(); await fixture.close();