mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-08 00:19:42 +02:00
fix(mobile): make every session tab reachable in the tab strip (#257)
With five tabs open on a phone, the right-hand tabs were effectively unreachable. Selecting a tab only toggled the .active class, so the strip never moved, and every full rebuild (a task badge appearing, a session created elsewhere) replaced the strip's innerHTML, which resets scrollLeft to 0 and yanked a mid-swipe strip back to the first tab. Three changes, which only work together: * computeTabScrollLeft() (pure, constants.js) decides the scroll target from measured rects, and _scrollActiveTabIntoView() applies it on selection. Rect math on the strip's own scrollLeft rather than scrollIntoView(), which also scrolls ancestors: on a phone that is the document, under a fixed header and possibly an open keyboard. * _fullRenderSessionTabs() saves and restores scrollLeft across the rebuild, and re-reveals the active tab only when it actually changed (_lastRenderedActiveTabId), so a background render never undoes a manual swipe. * Mobile no longer hoists the active session to the front of the strip. That reordering ran on full renders only, so tab order flipped depending on which render path fired, and it renumbered the Alt+N badges. Scrolling the active tab into view replaces it. Also sets overscroll-behavior-x: contain on the strip so a swipe that runs past the last tab stays in the strip instead of becoming the browser's back gesture. Tests: scroll-target math in test/tab-overflow.test.ts (runs in CI), plus five browser regressions in test/mobile/tabs.test.ts covering reveal-on- select in both directions, scroll preservation across an ambient rebuild, sessionOrder rendering on phones, and a real touch drag reaching the last tab. Closes #257 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -156,6 +156,47 @@ function shouldAutoWrapTabs(input) {
|
||||
return scrollWidth > clientWidth + 1;
|
||||
}
|
||||
|
||||
// Sliver of the neighbouring tab left visible when the strip scrolls a tab into
|
||||
// view. Landing a tab flush against the edge reads as "this is the last one";
|
||||
// the gap is what tells the user there is more strip to swipe to.
|
||||
const TAB_SCROLL_REVEAL_PX = 16;
|
||||
|
||||
// Phone/tablet tab-strip scroll policy (issue #257). Those breakpoints scroll
|
||||
// the strip horizontally (desktop wraps to a second row instead and never
|
||||
// scrolls), so the active tab can sit entirely outside the visible slice with
|
||||
// no way back except a swipe the user may not know is possible.
|
||||
//
|
||||
// Returns the scrollLeft that puts the tab inside the window, clamped to the
|
||||
// scrollable range, and returns the CURRENT scrollLeft when the tab is already
|
||||
// visible: callers compare and skip the write, so an already-correct strip is
|
||||
// never nudged. Pure: the caller measures, this decides.
|
||||
function computeTabScrollLeft(input) {
|
||||
const scrollWidth = Number(input?.scrollWidth) || 0;
|
||||
const clientWidth = Number(input?.clientWidth) || 0;
|
||||
const maxScroll = Math.max(0, scrollWidth - clientWidth);
|
||||
if (maxScroll === 0 || clientWidth <= 0) return 0;
|
||||
|
||||
const pad = input?.padding == null ? TAB_SCROLL_REVEAL_PX : Number(input.padding) || 0;
|
||||
const tabLeft = Number(input?.tabLeft) || 0;
|
||||
const tabWidth = Number(input?.tabWidth) || 0;
|
||||
const tabRight = tabLeft + tabWidth;
|
||||
const viewLeft = Math.min(Math.max(Number(input?.scrollLeft) || 0, 0), maxScroll);
|
||||
const viewRight = viewLeft + clientWidth;
|
||||
|
||||
let target = viewLeft;
|
||||
if (tabWidth + pad >= clientWidth) {
|
||||
// Tab is as wide as the window (long session name on a narrow phone):
|
||||
// there is no position that shows all of it plus padding, so align its
|
||||
// start, since the name matters more than the trailing badges.
|
||||
target = tabLeft;
|
||||
} else if (tabLeft - pad < viewLeft) {
|
||||
target = tabLeft - pad;
|
||||
} else if (tabRight + pad > viewRight) {
|
||||
target = tabRight + pad - clientWidth;
|
||||
}
|
||||
return Math.min(Math.max(Math.round(target), 0), maxScroll);
|
||||
}
|
||||
|
||||
// COD-134 — Terminal WebSocket reconnect policy.
|
||||
//
|
||||
// Decide what to do after a terminal WebSocket closes, given the close `code`
|
||||
@@ -261,6 +302,8 @@ if (typeof window !== 'undefined') {
|
||||
window.shouldSkipWebGL = shouldSkipWebGL;
|
||||
window.CodemanTabOverflow = {
|
||||
shouldAutoWrapTabs,
|
||||
computeTabScrollLeft,
|
||||
TAB_SCROLL_REVEAL_PX,
|
||||
};
|
||||
window.CodemanWsReconnect = {
|
||||
plan: planWsReconnect,
|
||||
|
||||
Reference in New Issue
Block a user