From 0723737d89948bc2bcd08638c80aaf6d763133d1 Mon Sep 17 00:00:00 2001 From: iris Date: Thu, 4 Jun 2026 19:56:34 +0200 Subject: [PATCH] fix(#1029): use getBoundingClientRect for tab overflow cutoff MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The old measurement used tabbar.clientWidth which includes horizontal padding (~2em / ~30px total), causing an over-allocation of the same amount. Flex gap between tabs (0.2em) was also not counted in the accumulated offsetWidth, adding another ~20px of error. Together these caused the algorithm to leave too many tabs visible, overflowing the bar and pushing the ⋮ button off the right edge of the screen. Replace the clientWidth + cumulative-offsetWidth approach with getBoundingClientRect: the cutoff is the tabbar's actual right edge minus its right padding, minus a reserve for the overflow wrapper. Individual tabs are overflowed once their right edge exceeds the cutoff; subsequent tabs are overflowed unconditionally to keep the visible set contiguous and left-anchored. --- frontend/packages/dashboard/src/tabs.js | 34 +++++++++++++++++-------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/frontend/packages/dashboard/src/tabs.js b/frontend/packages/dashboard/src/tabs.js index ff499f42..5802dcda 100644 --- a/frontend/packages/dashboard/src/tabs.js +++ b/frontend/packages/dashboard/src/tabs.js @@ -3730,20 +3730,32 @@ window.marked = marked; // Step 1: un-overflow all dynamic tabs so we can measure natural widths. dynamic.forEach(t => t.classList.remove('tab-overflowed')); - // Step 2: measure available width (bar width minus overflow-btn width). - // We must read layout AFTER restoring all dynamic tabs. - // overflowBtn may be hidden (offsetWidth=0) on the very first call; fall - // back to a 32px estimate so the first-pass measurement isn't off. - const btnW = overflowBtn.offsetWidth || 32; - const availWidth = tabbar.clientWidth - btnW - 8; + // Step 2: compute the right boundary where visible tabs must end. + // getBoundingClientRect is used instead of clientWidth because + // clientWidth includes the tabbar's horizontal padding (~2em total), + // causing an over-allocation of ~2em; flex gap between tabs is also + // not captured by offsetWidth accumulation. Together these pushed + // the ⋮ button off the right edge of the screen. + const tabbarRect = tabbar.getBoundingClientRect(); + const padR = parseFloat(getComputedStyle(tabbar).paddingRight) || 0; + // Right edge of the flex content area (inside right padding). + // Fall back to clientWidth-based estimate when the rect is zero + // (element not in layout, e.g. display:none ancestor). + const contentRight = tabbarRect.width > 0 + ? (tabbarRect.right - padR) + : (tabbar.clientWidth - padR); + // Space to reserve for the overflow wrapper. Use its actual offsetWidth + // when available; fall back to 40px on the first call (button hidden). + const btnReserve = (overflowWrap.offsetWidth || 40) + 4; + const cutoffRight = contentRight - btnReserve; - // Step 3: walk dynamic tabs left-to-right, accumulating widths. - // Any that go over the available width get overflowed. - let cumWidth = 0; + // Step 3: any tab whose right edge exceeds the cutoff is overflowed, + // along with all subsequent tabs (keeps the visible set contiguous + // and left-anchored). Once the first offending tab is found, all + // following tabs are also overflowed without re-measuring. const dynamicOverflow = []; for (const tab of dynamic) { - cumWidth += tab.offsetWidth; - if (cumWidth > availWidth) { + if (dynamicOverflow.length > 0 || tab.getBoundingClientRect().right > cutoffRight) { dynamicOverflow.push(tab); } }