fix: cap the first tab on every pass, before a selection widens it (#748)

selectSession() toggles .active at once while _syncTabStripCorner() only runs in
the debounced render, so a cap set after measuring left a newly selected first
tab wider than the room and dropped the first row below the toolbar until then.
max-width changes nothing on a narrower tab, so the cap is simply always on, and
the clear-before-measure and finish-the-animation loops go. The no-usable-room
test uses 8 tabs: at 40 the scroll fallback fires first and it passed at any
threshold.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-11-04 16:48:57 +01:00
1 parent 823013226d
commit d9d6fad698
4 files changed
+24 -19

No files matched your search

+7 -12
View File
@@ -7073,8 +7073,6 @@ class CodemanApp {
const contentTop = strip.top + parseFloat(style.paddingTop);
const contentLeft = strip.left + container.clientLeft + parseFloat(style.paddingLeft);
const contentRight = strip.left + container.clientLeft + container.clientWidth - parseFloat(style.paddingRight);
// A first tab that cannot fit beside the toolbar would drop the whole first
// line below it and leave an empty band: keep the plain strip instead.
// The first tab must fit beside the toolbar, or the whole first line drops
// below it and leaves an empty band. A selected tab is wider (gear and
// close show), so falling back to the plain strip here made the layout
@@ -7090,17 +7088,14 @@ class CodemanApp {
return;
}
const firstTab = container.querySelector(':scope > .session-tab');
// Cleared first, the first tab's included: the last pass's trim would
// otherwise be measured as its width, and a room that grew would untrim a
// tab still too wide for it.
for (const tab of container.querySelectorAll(':scope > .tab-corner-fit')) tab.classList.remove('tab-corner-fit');
// A selection change slides the gear and close open or shut (a width
// transition), and this runs in the same render: finish the first tab's
// slide, or it is judged by the width it is leaving.
for (const anim of firstTab?.getAnimations?.({ subtree: true }) ?? []) {
if (/^(width|padding)/.test(anim.transitionProperty ?? '')) anim.finish();
// Capped every time, not only once it measures too wide: selectSession()
// widens a newly selected tab at once and this pass is debounced, so a cap
// set only here afterwards would leave the first row below the toolbar until
// it runs. The cap changes nothing on a tab narrower than the room.
for (const tab of container.querySelectorAll(':scope > .tab-corner-fit')) {
if (tab !== firstTab) tab.classList.remove('tab-corner-fit');
}
if (firstTab && firstTab.offsetWidth > room) firstTab.classList.add('tab-corner-fit');
firstTab?.classList.add('tab-corner-fit');
if (container.style.getPropertyValue('--tab-corner-first-max') !== `${room}px`) {
container.style.setProperty('--tab-corner-first-max', `${room}px`);
}
+3 -3
View File
@@ -651,9 +651,9 @@ body {
margin: 0 2px var(--lineage-row-gap, 4px) 0;
}
/* A first tab wider than the room beside the toolbar (a selected one shows its
gear and close) has its name trimmed to fit, so the layout never depends on
which tab is selected. */
/* The first tab is capped to the room beside the toolbar: one wider than that (a
selected one shows its gear and close) has its name trimmed to fit, so the
layout never depends on which tab is selected. */
.session-tabs-host > .session-tabs.tabs-corner > .session-tab.tab-corner-fit {
max-width: var(--tab-corner-first-max, none);
}