fix(theseus): tabs never run under the window controls; the strip scrolls

With many tabs open, the last ones slid under the minimise / maximise /
close overlay: the strip reserved that space as padding, and padding does
not stop overflowing flex items. The tabs now live in their own row that
clips at its own edge, shrink down to icon + close before anything is
hidden, and the row scrolls sideways with the wheel once they hit that
minimum, keeping the selected tab in view. The + button sits outside the
row so it stays reachable no matter how many tabs are open.
This commit is contained in:
Local Dev 2026-09-20 16:36:35 +02:00
parent 5dc90f20b4
commit f5877bc890

View file

@ -41,13 +41,24 @@
opts out, and --wco-reserve keeps tabs clear of the native minimise /
maximise / close overlay (set from navigator.windowControlsOverlay, or a
fixed 140px when that API isn't exposed). */
.tabs { display: flex; align-items: flex-end; gap: 4px; padding: 6px 8px 0; padding-right: calc(8px + var(--wco-reserve, 0px));
.tabs { display: flex; align-items: flex-end; padding: 6px 8px 0; padding-right: calc(8px + var(--wco-reserve, 0px));
height: 34px; overflow: hidden; -webkit-app-region: drag; }
.tabs > *, .grouppop, .ctxmenu { -webkit-app-region: no-drag; }
/* The tabs sit in their own row INSIDE the reserved-for-window-controls
padding. Padding does not stop overflowing flex items, so with many
tabs the last ones used to run under the minimise / close buttons;
the row is a sized box (min-width: 0) that clips at its own edge. */
.tabrow { display: flex; align-items: flex-end; gap: 4px; flex: 1 1 auto; min-width: 0; height: 100%;
overflow-x: auto; overflow-y: hidden; scrollbar-width: none; -webkit-app-region: drag; }
.tabrow::-webkit-scrollbar { display: none; }
.tabrow > *, .tabs > .newtab, .grouppop, .ctxmenu { -webkit-app-region: no-drag; }
/* The + button sits OUTSIDE the scrolling row, so it stays reachable no
matter how many tabs are open; the row scrolls (wheel) once tabs hit
their minimum width, like Chrome. */
.tabs > .newtab { flex: none; margin-left: 2px; }
/* Same-size tabs: each tab claims an equal share of the row, capped at
200px so 2 tabs don't stretch across the whole window. min-width lets
many tabs shrink cleanly while still showing a couple letters. */
.tab { display: flex; align-items: center; gap: 8px; flex: 1 1 0; max-width: 200px; min-width: 60px; padding: 6px 10px;
200px so 2 tabs don't stretch across the whole window. With many tabs
they shrink down to icon + close (34px) before anything is clipped. */
.tab { display: flex; align-items: center; gap: 8px; flex: 1 1 0; max-width: 200px; min-width: 34px; padding: 6px 10px;
background: var(--surface); border: 1px solid var(--line2); border-bottom: none; border-radius: 8px 8px 0 0;
font-size: 12.5px; cursor: default; color: var(--mut); user-select: none; }
.tab.dragging { opacity: .45; cursor: grabbing; }
@ -59,7 +70,7 @@
.tab.active { background: var(--active); color: var(--ink); border-color: rgb(from var(--acid) r g b / .7);
box-shadow: inset 0 2px 0 var(--acid); }
.tab.active.grp { box-shadow: inset 0 2px 0 var(--gc, var(--acid)); }
.tab .t { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
.tab .t { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; min-width: 0; }
.tab .ico { display: flex; align-items: center; flex: none; }
.tab .ico:empty { display: none; }
.tab .fav { width: 14px; height: 14px; flex: none; border-radius: 2px; object-fit: contain; }
@ -410,7 +421,7 @@
.bcnrbar .bdismiss { color: var(--dim); text-decoration: none; } .bcnrbar .bdismiss:hover { color: var(--ink); }
</style></head>
<body>
<div class="tabs" id="tabs"></div>
<div class="tabs" id="tabstrip"><div class="tabrow" id="tabs"></div></div>
<div class="bar" id="bar">
<div class="nav">
<button class="ic" id="back" title="Back"><svg viewBox="0 0 16 16" stroke-linecap="round" stroke-linejoin="round"><path d="M13 8H3M6 5L3 8l3 3"/></svg></button>
@ -1318,13 +1329,32 @@
updateTab(el, t);
order.push(el);
}
let plus = document.getElementById("newtab");
if (!plus) { plus = document.createElement("span"); plus.className = "newtab"; plus.id = "newtab"; plus.textContent = "+"; plus.onclick = () => T.newTab(); }
order.push(plus);
// "+" lives next to the row, not in it (see CSS) — created once.
if (!document.getElementById("newtab")) {
const plus = document.createElement("span"); plus.className = "newtab"; plus.id = "newtab"; plus.textContent = "+"; plus.title = "New tab (Ctrl+T)";
plus.onclick = () => T.newTab();
box.parentElement.appendChild(plus);
}
const keep = new Set(order);
for (const el of [...box.children]) if (!keep.has(el)) el.remove();
order.forEach((el, i) => { if (box.children[i] !== el) box.insertBefore(el, box.children[i] || null); });
// Keep the selected tab in view when the row has scrolled.
const act = box.querySelector(".tab.active");
if (act && box.scrollWidth > box.clientWidth) {
const l = act.offsetLeft, r = l + act.offsetWidth;
if (l < box.scrollLeft) box.scrollLeft = l - 4;
else if (r > box.scrollLeft + box.clientWidth) box.scrollLeft = r - box.clientWidth + 4;
}
}
// Wheel over the row scrolls it sideways (there is no visible scrollbar).
$("tabs").addEventListener("wheel", (e) => {
const box = $("tabs");
if (box.scrollWidth <= box.clientWidth) return;
const d = Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : e.deltaY;
if (!d) return;
box.scrollLeft += d;
e.preventDefault();
}, { passive: false });
// ---- Tab context menu (right-click a tab) ----
const TAB_GROUP_COLORS = [