feat(theseus): Firefox-style tab overflow — scroll arrows, earliest tabs slide out
Tabs keep a readable minimum width (76px). Once they overflow, the row scrolls: the earliest tabs slide out on the left and an arrow at each end moves the row by 60% of its width, disabled at its end of travel and hidden while everything fits. The wheel scrolls the row too, the selected tab is brought into view when the selection changes (never while the user is scrolling), and the + button stays outside the row. Moves are immediate rather than animated: the chrome view has no smooth scrolling, and frame callbacks stop while the window is occluded, which stranded a frame-driven slide at its start.
This commit is contained in:
parent
f5877bc890
commit
b7b705827a
1 changed files with 54 additions and 7 deletions
61
chrome.html
61
chrome.html
|
|
@ -55,10 +55,18 @@
|
|||
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; }
|
||||
/* Firefox-style overflow: once tabs hit their minimum width the row
|
||||
scrolls, the earliest tabs slide out on the left, and an arrow at each
|
||||
end moves the row (they only appear when there is something to scroll). */
|
||||
.tabscroll { flex: none; width: 24px; height: 28px; margin: 0 2px; padding: 0; border: 1px solid var(--line2); border-bottom: none;
|
||||
border-radius: 8px 8px 0 0; background: var(--surface); color: var(--mut); cursor: pointer; font: 16px/1 system-ui, sans-serif;
|
||||
-webkit-app-region: no-drag; }
|
||||
.tabscroll:hover { color: var(--ink); background: var(--hover); }
|
||||
.tabscroll:disabled { opacity: .3; cursor: default; }
|
||||
/* 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. 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;
|
||||
.tab { display: flex; align-items: center; gap: 8px; flex: 1 1 0; max-width: 200px; min-width: 76px; 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; }
|
||||
|
|
@ -421,7 +429,7 @@
|
|||
.bcnrbar .bdismiss { color: var(--dim); text-decoration: none; } .bcnrbar .bdismiss:hover { color: var(--ink); }
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="tabs" id="tabstrip"><div class="tabrow" id="tabs"></div></div>
|
||||
<div class="tabs" id="tabstrip"><button class="tabscroll" id="tabsLeft" title="Scroll tabs left" hidden>‹</button><div class="tabrow" id="tabs"></div><button class="tabscroll" id="tabsRight" title="Scroll tabs right" hidden>›</button></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>
|
||||
|
|
@ -1338,21 +1346,60 @@
|
|||
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.
|
||||
// Bring the selected tab into view when the SELECTION changes (or a tab
|
||||
// appears). Not on every tabs event — those fire on every title/favicon/
|
||||
// loading change, and re-snapping then would undo the user's own
|
||||
// scrolling with the arrows or the wheel.
|
||||
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;
|
||||
if (act) {
|
||||
const key = act.dataset.id;
|
||||
if (key !== lastScrolledKey && performance.now() > userScrollUntil && 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;
|
||||
}
|
||||
lastScrolledKey = key;
|
||||
}
|
||||
updateTabScroll();
|
||||
}
|
||||
let lastScrolledKey = null;
|
||||
// While the user is scrolling the row (arrows / wheel), renders must not
|
||||
// snap it back to the selected tab.
|
||||
let userScrollUntil = 0;
|
||||
// Arrows at both ends of the row: shown only while the row overflows,
|
||||
// each disabled at its end of travel. A click moves by ~60% of the
|
||||
// visible width; the wheel scrolls too.
|
||||
function updateTabScroll() {
|
||||
const box = $("tabs"), left = $("tabsLeft"), right = $("tabsRight");
|
||||
const over = box.scrollWidth > box.clientWidth + 1;
|
||||
left.hidden = right.hidden = !over;
|
||||
if (!over) return;
|
||||
left.disabled = box.scrollLeft <= 0;
|
||||
right.disabled = box.scrollLeft >= box.scrollWidth - box.clientWidth - 1;
|
||||
}
|
||||
// Immediate, not animated: the chrome view has no smooth scrolling
|
||||
// (scrollBy({behavior:"smooth"}) moves nothing) and animation frames
|
||||
// stop whenever the window is occluded, which left a frame-driven slide
|
||||
// stuck at its start.
|
||||
function slideTabs(dir) {
|
||||
const box = $("tabs");
|
||||
userScrollUntil = performance.now() + 1500;
|
||||
box.scrollLeft = Math.max(0, Math.min(box.scrollWidth - box.clientWidth, box.scrollLeft + dir * Math.round(box.clientWidth * 0.6)));
|
||||
updateTabScroll(); // the scroll event itself waits for a frame
|
||||
}
|
||||
$("tabsLeft").onclick = () => slideTabs(-1);
|
||||
$("tabsRight").onclick = () => slideTabs(1);
|
||||
$("tabs").addEventListener("scroll", updateTabScroll, { passive: true });
|
||||
window.addEventListener("resize", () => requestAnimationFrame(updateTabScroll));
|
||||
// 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;
|
||||
userScrollUntil = performance.now() + 1500;
|
||||
box.scrollLeft += d;
|
||||
updateTabScroll();
|
||||
e.preventDefault();
|
||||
}, { passive: false });
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue