From c087d0ae4d5557c6f521f1433b89a9812b79b536 Mon Sep 17 00:00:00 2001 From: JD Date: Mon, 7 Sep 2026 22:36:50 -0400 Subject: [PATCH] fix(mobile): raise the phone breakpoint from 430px to 600px The phone tier stopped at innerWidth < 430 and @media (max-width: 430px), so every current large phone landed in the tablet layout: the 430pt iPhone 14 Pro Max, 15 Plus, 15 Pro Max and 16 Plus, the 440pt iPhone 16 Pro Max and 17 Pro Max, Pixel 6 Pro, 7 Pro and OnePlus 12 Pro, the 448pt Pixel 8 Pro and 9 Pro XL, and the Galaxy Z Fold 5 cover screen at 460. On those devices the header icon row replaced the session pill, the toolbar kept the desktop Run Shell button instead of Enter and the mic, the keyboard accessory bar could never become visible because its .visible rule lives inside the phone block, and the toolbar jumped to the top of the page when the keyboard opened. The new cutoff is 600, the line test/mobile/devices.ts already draws between large phones (430-599) and small tablets (600-767). No physical device sits between 480 and 600, but a phone zoomed out one or two steps in Safari does: a 440pt iPhone at 85% or 75% page zoom reports 518px or 587px and still needs the phone controls, which a 480 cutoff would have taken away. The phone block is max-width: 599px and the tablet block starts at min-width: 600px, so a 600px device is a tablet in CSS and in getDeviceType() alike instead of straddling the boundary the way 430pt phones did. The number changes everywhere it is encoded: JS, CSS, comments, CLAUDE.md, the CI tests that pin the phone block, and the test:mobile helpers. Measurement history that names 430px stays as written. --- CLAUDE.md | 6 +++--- src/web/public/index.html | 2 +- src/web/public/mobile-handlers.js | 10 +++++----- src/web/public/mobile-overview.js | 4 ++-- src/web/public/mobile.css | 24 +++++++++++------------ src/web/public/session-ui.js | 2 +- src/web/public/settings-ui.js | 2 +- src/web/public/styles.css | 6 +++--- src/web/public/terminal-ui.js | 2 +- test/home-sessions.test.ts | 2 +- test/mobile-header-buttons-policy.test.ts | 6 +++--- test/mobile-overview.test.ts | 2 +- test/mobile-tab-tap-zones.test.ts | 2 +- test/mobile/README.md | 6 +++--- test/mobile/device-matrix.test.ts | 4 ++-- test/mobile/devices.ts | 2 +- test/mobile/helpers/constants.ts | 2 +- test/mobile/layout.test.ts | 6 +++--- test/mobile/visual-regression.test.ts | 8 ++++---- test/readmymind-phone-key.test.ts | 4 ++-- 20 files changed, 51 insertions(+), 51 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index ed423971a..655e64f94 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -294,11 +294,11 @@ Frontend JS modules have `@fileoverview` with `@dependency`/`@loadorder` tags. L **Entrance animations** (`entrance-animations.js`, all OFF by default): opt-in animations for the four things that appear when work starts, chosen per surface via `data-tab-anim` / `data-term-anim` / `data-win-anim` / `data-line-anim` on ``. Defaults are the `legacy` theme, so an untouched install behaves exactly as before and every hook short-circuits on its first line. ⚠️ Tabs and connection lines are **destroyed mid-animation** on every re-render (`_fullRenderSessionTabs()` replaces the strip's innerHTML; `_updateConnectionLinesImmediate()` does `svg.innerHTML = ''`), so both are tracked by id and re-applied to the fresh element with a **negative `animation-delay`** to resume rather than restart. ⚠️ The terminal-pane styles may animate **transform / opacity / clip-path only**, xterm's FitAddon derives rows+cols from `getComputedStyle(parent).width/height`, so animating width/height/padding there would resize the PTY. ⚠️ Window styles other than `beam` transform the window, which moves the rect its connection line is aimed at; `beam` deliberately animates opacity/filter only so its line can draw toward a stable target. Persisted to its own `codeman:*Anim` localStorage keys (per-device, deliberately NOT in the `.strict()` `SettingsUpdateSchema`); picker in App Settings → Appearance, full per-surface lab at `?animlab=1`. -**Mobile tab strip scrolling** (issue #257): under 768px the tab strip is a horizontal scroller (desktop wraps to a second row instead), so the active tab can sit off-screen. Three rules keep it reachable and they only work together: `_updateActiveTabImmediate()` scrolls the selected tab into view via `computeTabScrollLeft()` (pure, in constants.js) using **rect math on the strip's own `scrollLeft`**, never `scrollIntoView()`, which would also scroll the document under a fixed header; `_fullRenderSessionTabs()` **restores `scrollLeft`** across the `innerHTML` rebuild, since ambient rebuilds (a task badge appearing, a session created elsewhere) otherwise snap a mid-swipe strip back to 0; and it re-reveals the active tab **only when it changed** (`_lastRenderedActiveTabId`), so browsing the far end of the strip is not undone by background renders. ⚠️ **The ACTIVE tab is the only one with action icons, and on a phone they can eat it**: `.session-tab.active .tab-name` reserves `min-width: 44px` in the ≤430px block, because a short session name rendered a 13px label against a 50px gear+close cluster, putting the tab's geometric CENTRE on the gear, so a thumb aiming at the tab opened Session Options instead of switching (measured at 360/393/430px; only long names cleared it). ⚠️ **The floor is set by the 10th tab onward, not by the tabs you can see**: `.tab-number` renders only for `_tabIdx < 9`, so tab 10 loses 16px + a gap off its left and its centre sits 10px further right. The centre clears the icons when `reserved > icons + rightEdge - leftRunUp - gap` (= 50 + 9 - 17 - 4 = **38px**), hit-testing snaps to whole pixels so 39px still lands on the gear, and the practical floor is 40px — a NUMBERED tab clears it at 20px, which is exactly why reasoning from the tabs on screen would put the centre back on the gear. `test/mobile-tab-tap-zones.test.ts` recomputes that inequality from the stylesheet, so widening the gear or the padding fails there rather than on a phone. The guarantee is centre-off-the-ICONS, not centre-inside-the-label (on a numberless tab it lands in the gap between them, which still switches). Non-active tabs keep their icons hidden and stay tappable end to end. ⚠️ 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. Scroll-into-view replaces it; do not reintroduce it. +**Mobile tab strip scrolling** (issue #257): under 768px the tab strip is a horizontal scroller (desktop wraps to a second row instead), so the active tab can sit off-screen. Three rules keep it reachable and they only work together: `_updateActiveTabImmediate()` scrolls the selected tab into view via `computeTabScrollLeft()` (pure, in constants.js) using **rect math on the strip's own `scrollLeft`**, never `scrollIntoView()`, which would also scroll the document under a fixed header; `_fullRenderSessionTabs()` **restores `scrollLeft`** across the `innerHTML` rebuild, since ambient rebuilds (a task badge appearing, a session created elsewhere) otherwise snap a mid-swipe strip back to 0; and it re-reveals the active tab **only when it changed** (`_lastRenderedActiveTabId`), so browsing the far end of the strip is not undone by background renders. ⚠️ **The ACTIVE tab is the only one with action icons, and on a phone they can eat it**: `.session-tab.active .tab-name` reserves `min-width: 44px` in the phone block (under 600px), because a short session name rendered a 13px label against a 50px gear+close cluster, putting the tab's geometric CENTRE on the gear, so a thumb aiming at the tab opened Session Options instead of switching (measured at 360/393/430px; only long names cleared it). ⚠️ **The floor is set by the 10th tab onward, not by the tabs you can see**: `.tab-number` renders only for `_tabIdx < 9`, so tab 10 loses 16px + a gap off its left and its centre sits 10px further right. The centre clears the icons when `reserved > icons + rightEdge - leftRunUp - gap` (= 50 + 9 - 17 - 4 = **38px**), hit-testing snaps to whole pixels so 39px still lands on the gear, and the practical floor is 40px — a NUMBERED tab clears it at 20px, which is exactly why reasoning from the tabs on screen would put the centre back on the gear. `test/mobile-tab-tap-zones.test.ts` recomputes that inequality from the stylesheet, so widening the gear or the padding fails there rather than on a phone. The guarantee is centre-off-the-ICONS, not centre-inside-the-label (on a numberless tab it lands in the gap between them, which still switches). Non-active tabs keep their icons hidden and stay tappable end to end. ⚠️ 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. Scroll-into-view replaces it; do not reintroduce it. **Session list layout: header strip or left sidebar** (`sessionListLayout`, App Settings → Appearance → Tabs, default `header`; per-device policy — it IS in `SettingsUpdateSchema` and persists server-side, but `displayKeys` makes a device keep its own value): with many sessions the horizontal strip stops being scannable, so the list can move into a vertical `