Skip to content

fix(mobile): make the phone header tab strip read as live tabs - #504

Merged
Ark0N merged 1 commit into
masterfrom
fix/phone-tab-strip
Sep 28, 2026
Merged

Ark0N merged 1 commit into
masterfrom
fix/phone-tab-strip

Conversation

@Ark0N

@Ark0N Ark0N commented Sep 28, 2026

Copy link
Copy Markdown
Owner

What was wrong

On a phone, the session tab strip in the header looked switched off. Every inactive tab was transparent, so each one was grey 11px text floating in an unmarked gap, with nothing that reads as tappable. Each tab also carried a boxed Alt+N digit, a keyboard shortcut hint a phone can't use. Names were capped at 50px, so tabs sharing a w1- prefix mostly showed the prefix (w1-inge…, or just w1- on the tab at the edge). And the tab that didn't fit was cut off mid-word, right against the connection dot.

Phone header tab strip, before and after

Captured at a 402px iPhone viewport with invented session names. The off-screen drawer shadow described under "Not in this PR" is hidden in these captures so it doesn't muddy the comparison.

What changed

Everything is in the phone block (max-width: 599px) of src/web/public/mobile.css. Tablets and desktop are unchanged.

  • Tabs are chips. Every header tab gets a fill and border from the skin's --control-bg-hover / --control-border-hover tokens, and the name is --text at weight 500. The light skins already repaint the header with --glass-bg and define their own control tokens, so they get matching chips without an override block. The active tab keeps its accent fill and border (those are !important in styles.css).
  • The chip selector stays at (0,1,0). It's written :where(.header) .session-tab. As .header .session-tab it would tie with .session-tab[data-color="red"] in styles.css and win on load order, which would wipe the colour stripe off colour-tagged tabs. Scoping to .header also leaves sidebar layout alone, because there the list moves out of the header.
  • No Alt+N digit on phones. Inactive tabs also drop their empty .tab-actions container, which was adding 4px to the right side of every chip.
  • More name, bigger dot. The name cap goes from 50px to 80px (the hidden digit gives back 20px of that), the idle status dot from 4px to 6px, and the gap between tabs from 2px to 6px.
  • Edge fade, no JS. The strip gets a mask whose two fade widths follow its own horizontal scroll through a scroll-driven animation (animation-timeline: scroll(self inline), with the widths registered via @property). At the start only the right edge fades, at the end only the left, and in between both. A strip that fits has an inactive timeline, so it gets no mask. Browsers without scroll timelines (Firefox today) skip the @supports block and keep the old hard edge.

Tap targets

The active tab keeps its min-width: 44px label reserve, which is what stops a thumb aimed at the middle of the tab from landing on the gear icon. With every phone tab now numberless and the dot 2px wider, the reserve it actually needs drops from 38px to 36px, and the arithmetic in the comment is updated to match. Checked in Chromium at 360, 393, 402 and 430px: tapping the centre of an active tab named w9 hits the tab, not the gear or the close button.

Testing

  • New test/mobile-tab-strip-chips.test.ts (postcss, runs in the CI gate). It pins three ways this breaks with no visible error: the chip selector gaining specificity; @property moving inside the @media block, where it's invalid, so the fade stops interpolating; and animation-timeline being declared before the animation shorthand, which resets it. I broke each one on purpose and the test failed each time.
  • test/mobile/tabs.test.ts now expects the 80px cap. That suite isn't in the CI gate. npm run test:mobile -- test/mobile/tabs.test.ts gives 30/31 on this branch and the same 30/31 on master. The one failure ("switching tabs with the keyboard closed does not leave the terminal textarea focused") fails the same way on master.
  • npm test passes: 433 files, 8307 tests.
  • Browser checks (Playwright at 402x874 with an iPhone user agent and --safe-area-top forced to 62px): the dark, og and four light skins; the home screen, an active session, and the strip scrolled to the middle and to the end. The per-colour left border still renders red, and the computed styles of sidebar-layout rows and of the 700px tablet strip are unchanged.
  • Not yet checked on a physical iPhone. Safari 26 and later support scroll-driven animations, so the fade should work there, and older iOS keeps the hard edge.

Not in this PR

On the light skins a dark shadow runs down the right edge of the screen. It comes from the closed notification drawer (#notifDrawer) and approvals drawer: both are parked just past the right edge of the viewport, and their leftward box-shadow bleeds into view. It has nothing to do with the tab strip and shows up on master too, so it's left for a separate fix.

On a phone every inactive tab rendered transparent: grey 11px text
floating in unmarked gaps, a boxed Alt+N digit in each tab (a phone has
no Alt key), names capped at 50px so a shared `w1-` prefix was most of
what showed, and the tab that did not fit was chopped mid-word against
the connection dot. The strip looked like a row of disabled labels.

Phone block of mobile.css only:
- Every header tab is a chip, filled and bordered from the skin's
  --control-* tokens, name in --text at weight 500. Written
  `:where(.header) .session-tab` so it stays at (0,1,0): the per-colour
  left border still wins, and sidebar layout (where the list leaves the
  header) is untouched.
- The Alt+N digit is hidden in the header; inactive tabs drop their
  empty .tab-actions container, which padded the chip's right side.
- Name cap 50px -> 80px, status dot 4px -> 6px, strip gap 2px -> 6px.
- Scroll-driven edge fade: a mask on the strip whose widths follow its
  own inline scroll timeline (registered @Property lengths), so the
  clipped tab dissolves into the edge. No JS; a strip that does not
  overflow gets no mask, and browsers without scroll timelines keep the
  old hard edge.

The tap-zone arithmetic comment is updated for the numberless phone
tabs and the bigger dot (the required reserve drops from 38px to 36px;
the 44px min-width stays). test/mobile-tab-strip-chips.test.ts pins the
(0,1,0) selector, the top-level @Property registration and the
timeline-after-shorthand order, each of which fails silently otherwise.
test/mobile/tabs.test.ts follows the new name cap.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Ark0N
Ark0N merged commit 83e39c4 into master Sep 28, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants