[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:
@@ -747,6 +747,71 @@ function elementById(id) {
|
||||
api.applyLayout(savedLayoutName(), { persist: false });
|
||||
}
|
||||
}
|
||||
const overflowOrder = [".operator-layout-picker", ".header-style-pick", "#theme-toggle"];
|
||||
function installTopBarOverflow() {
|
||||
const actions = document.querySelector(".top-bar-actions");
|
||||
if (!actions || document.getElementById("top-bar-more")) return;
|
||||
const wrap = document.createElement("div");
|
||||
wrap.id = "top-bar-more";
|
||||
wrap.className = "top-bar-more";
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.id = "top-bar-more-btn";
|
||||
button.className = "header-bar-btn top-bar-more-btn";
|
||||
button.textContent = "⋯";
|
||||
button.setAttribute("aria-haspopup", "menu");
|
||||
button.setAttribute("aria-expanded", "false");
|
||||
button.setAttribute("aria-label", "More controls");
|
||||
button.title = "More controls";
|
||||
const menu = document.createElement("div");
|
||||
menu.id = "top-bar-more-menu";
|
||||
menu.className = "top-bar-more-menu";
|
||||
menu.setAttribute("role", "menu");
|
||||
button.setAttribute("aria-controls", menu.id);
|
||||
wrap.append(button, menu);
|
||||
actions.appendChild(wrap);
|
||||
const closeMenu = () => {
|
||||
menu.classList.remove("is-open");
|
||||
button.setAttribute("aria-expanded", "false");
|
||||
};
|
||||
button.addEventListener("click", () => {
|
||||
const open = menu.classList.toggle("is-open");
|
||||
button.setAttribute("aria-expanded", String(open));
|
||||
});
|
||||
document.addEventListener("click", (event) => {
|
||||
if (!(event.target instanceof Node) || !wrap.contains(event.target)) closeMenu();
|
||||
});
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Escape") closeMenu();
|
||||
});
|
||||
const barFits = () => {
|
||||
const bar = actions.closest(".tab-bar");
|
||||
if (!bar) return true;
|
||||
const identity = bar.querySelector(".header-main");
|
||||
const nav = bar.querySelector(".tab-bar-nav");
|
||||
const gutters = 48;
|
||||
const needed = (identity?.offsetWidth ?? 0) + (nav?.scrollWidth ?? 0) + actions.scrollWidth + gutters;
|
||||
return needed <= bar.clientWidth;
|
||||
};
|
||||
const reflowOverflow = () => {
|
||||
overflowOrder.forEach((selector) => {
|
||||
const element = menu.querySelector(selector);
|
||||
if (element) actions.insertBefore(element, wrap);
|
||||
});
|
||||
wrap.hidden = true;
|
||||
for (const selector of overflowOrder) {
|
||||
if (barFits()) break;
|
||||
const element = actions.querySelector(selector);
|
||||
if (!element) continue;
|
||||
wrap.hidden = false;
|
||||
menu.appendChild(element);
|
||||
}
|
||||
wrap.hidden = menu.children.length === 0;
|
||||
if (wrap.hidden) closeMenu();
|
||||
};
|
||||
reflowOverflow();
|
||||
window.addEventListener("resize", reflowOverflow);
|
||||
}
|
||||
function installMobileMore() {
|
||||
const nav = document.querySelector(".tab-bar-nav");
|
||||
if (!nav || document.getElementById("mobile-more-btn")) return;
|
||||
@@ -853,6 +918,7 @@ function elementById(id) {
|
||||
api.init = function init() {
|
||||
ensureLiveRegions();
|
||||
installLayoutControls();
|
||||
installTopBarOverflow();
|
||||
installMobileMore();
|
||||
installDecoderPicker();
|
||||
installDecoderBadges();
|
||||
|
||||
Reference in New Issue
Block a user