Mobile: site header overflows the viewport on every page (nav + tray push past 390px; page scrolls horizontally) #273

Closed
opened 2026-09-10 11:17:33 +00:00 by crueber · 3 comments
Owner

What's wrong

At a phone viewport (390×844, mobile emulation), every page of the app horizontally overflows: document.documentElement.scrollWidth = 439px vs 390px client width (measured on /, /explore, /new, /import, /setup). The header's right cluster extends past the right edge — the theme-toggle button's right edge lands at x=439, and the notification bell is clipped at the screen edge. The whole page can be panned sideways, which on mobile feels broken.

Measured evidence (Playwright, 390×844, is_mobile, DPR 2)

docW: 439  clientW: 390  overflowX: 49     (every non-repo page)
offenders:
  DIV.ml-auto.flex.items-center.gap-2   right:439 (the tray+theme cluster)
  BUTTON.btn.px-2.py-1                  right:439 (theme toggle)
  DIV.tray                              w:384 (notification tray popover)

Root cause (code evidence)

  • The header row is flex … items-center gap-6 px-4 py-2.5 with no wrapping or scroll container (web/src/App.jsx:47), and flex-wrap: nowrap confirmed computed. Content: brand + site-nav (5 links, ~208px) + the ml-auto right cluster. At 390px the fixed gap-6 (24px × 2) plus nav + right cluster exceeds the viewport; nothing yields, so the row (and the page) grows.
  • The notification tray popover is a fixed-width .tray panel (384px — measured) that opens right-aligned from a clipped trigger, compounding the overflow when opened.
  • This worsened with #238 (API link moved into the already-crowded right cluster).

Fix direction

  • Allow the header to degrade at small widths: either (a) let site-nav scroll horizontally within itself (overflow-x: auto, no wrap) while the right cluster stays pinned — the common git-host pattern; or (b) collapse nav links into a hamburger below a breakpoint; or (c) reduce gaps/padding at sm: and allow wrap. (a) is the smallest change and keeps everything reachable.
  • Constrain .tray to min(384px, calc(100vw - 16px)) so the open popover never exceeds the viewport.
  • Acceptance: scrollWidth === clientWidth at 390px on /, /explore, /new, /import, /setup; bell + theme toggle fully visible and tappable; tray popover fits when opened.
## What's wrong At a phone viewport (390×844, mobile emulation), **every page** of the app horizontally overflows: `document.documentElement.scrollWidth` = **439px vs 390px client width** (measured on `/`, `/explore`, `/new`, `/import`, `/setup`). The header's right cluster extends past the right edge — the theme-toggle button's right edge lands at x=439, and the notification bell is clipped at the screen edge. The whole page can be panned sideways, which on mobile feels broken. ## Measured evidence (Playwright, 390×844, is_mobile, DPR 2) ``` docW: 439 clientW: 390 overflowX: 49 (every non-repo page) offenders: DIV.ml-auto.flex.items-center.gap-2 right:439 (the tray+theme cluster) BUTTON.btn.px-2.py-1 right:439 (theme toggle) DIV.tray w:384 (notification tray popover) ``` ## Root cause (code evidence) - The header row is `flex … items-center gap-6 px-4 py-2.5` with **no wrapping or scroll container** (`web/src/App.jsx:47`), and `flex-wrap: nowrap` confirmed computed. Content: brand + `site-nav` (5 links, ~208px) + the `ml-auto` right cluster. At 390px the fixed `gap-6` (24px × 2) plus nav + right cluster exceeds the viewport; nothing yields, so the row (and the page) grows. - The notification tray popover is a fixed-width `.tray` panel (384px — measured) that opens right-aligned from a clipped trigger, compounding the overflow when opened. - This worsened with #238 (API link moved into the already-crowded right cluster). ## Fix direction - Allow the header to degrade at small widths: either (a) let `site-nav` scroll horizontally within itself (`overflow-x: auto`, no wrap) while the right cluster stays pinned — the common git-host pattern; or (b) collapse nav links into a hamburger below a breakpoint; or (c) reduce gaps/padding at `sm:` and allow wrap. (a) is the smallest change and keeps everything reachable. - Constrain `.tray` to `min(384px, calc(100vw - 16px))` so the open popover never exceeds the viewport. - Acceptance: `scrollWidth === clientWidth` at 390px on `/`, `/explore`, `/new`, `/import`, `/setup`; bell + theme toggle fully visible and tappable; tray popover fits when opened.
Author
Owner

Fix ready for review: PR #296 (fix/issue-273) — header pins brand + right cluster and lets site-nav scroll internally at narrow widths; node --test 564/564 green, vite build clean, no new deps. Browser verification left open (shared daemon blocks loopback).

Fix ready for review: PR #296 (fix/issue-273) — header pins brand + right cluster and lets site-nav scroll internally at narrow widths; node --test 564/564 green, vite build clean, no new deps. Browser verification left open (shared daemon blocks loopback).
Author
Owner

Review of PR #296 (fix/issue-273, site header narrow widths) — verified in scratch worktree /tmp/pr296 (removed afterward); main worktree untouched. No browser used (node tests + flex-math reasoning + vite build, per task instructions).

Findings (all checks passed, no fixes needed):

  • 390px flex math (web/src/App.jsx:47-66): row = px-4 (32px) + 2x gap-3 (24px) + brand shrink-0 (~70px upper bound) + right cluster shrink-0 (API ~30 + bell ~40 + toggle ~40 + internal gaps ~16 = ~130px upper bound). Pinned total ~256px < 390px, so the flex-1/min-w-0 nav always gets a non-negative share and any excess scrolls INSIDE the strip (overflow-x-auto) — no page-level horizontal scroll. No bare gap-6 remains below sm: (gap-3 + sm:gap-6, desktop unchanged).
  • Hidden-scrollbar strip keeps keyboard scroll (web/src/ui.css #273 block): scrollbar-width:none + ::-webkit-scrollbar{display:none} hide only the visual scrollbar; overflow-x-auto is retained so touch scroll works and Tab focus moves along the native links with the strip following focus (asserted in header-narrow.test.js). Note: the nav itself is not focusable (no tabindex), so arrow-key scrolling requires focus inside the strip — deliberate, matches the common git-host pattern; adding a tabindex would cost a redundant tab stop.
  • #238 placement preserved: API link still class=nav-link inside the ml-auto cluster, left of the tray (App.jsx:63); nav-api-right.test.js diff is shape-only (block selector gains aria-label="Site"), all assertions intact and passing.
  • No logic change: diff is class/attribute/CSS/test-only; aria-label="Site" on the nav is additive a11y (labelled landmark for the scroll strip). No new deps (4 files changed; package.json/pnpm-lock untouched — Law 1 holds, Tailwind utilities + plain CSS only). Laws 7/8 N/A; Law 12: bugfix under 12_web_ui with in-code comments, no doc amendment needed. Dark+light share the treatment (theme-independent rules; link colors untouched). #278 tray/popover viewport bound asserted intact.

Tests: full node suite 564/564 pass (incl. 7 new header-narrow + 3 nav-api-right); vite build:ui exit 0 (~2s, only pre-existing >500kB chunk warning); compiled CSS confirmed to contain scrollbar-width:none + site-nav rules.

MERGE RECOMMENDATION: ready to merge (not merging per instructions).

Review of PR #296 (fix/issue-273, site header narrow widths) — verified in scratch worktree /tmp/pr296 (removed afterward); main worktree untouched. No browser used (node tests + flex-math reasoning + vite build, per task instructions). Findings (all checks passed, no fixes needed): - 390px flex math (web/src/App.jsx:47-66): row = px-4 (32px) + 2x gap-3 (24px) + brand shrink-0 (~70px upper bound) + right cluster shrink-0 (API ~30 + bell ~40 + toggle ~40 + internal gaps ~16 = ~130px upper bound). Pinned total ~256px < 390px, so the flex-1/min-w-0 nav always gets a non-negative share and any excess scrolls INSIDE the strip (overflow-x-auto) — no page-level horizontal scroll. No bare gap-6 remains below sm: (gap-3 + sm:gap-6, desktop unchanged). - Hidden-scrollbar strip keeps keyboard scroll (web/src/ui.css #273 block): scrollbar-width:none + ::-webkit-scrollbar{display:none} hide only the visual scrollbar; overflow-x-auto is retained so touch scroll works and Tab focus moves along the native links with the strip following focus (asserted in header-narrow.test.js). Note: the nav itself is not focusable (no tabindex), so arrow-key scrolling requires focus inside the strip — deliberate, matches the common git-host pattern; adding a tabindex would cost a redundant tab stop. - #238 placement preserved: API link still class=nav-link inside the ml-auto cluster, left of the tray (App.jsx:63); nav-api-right.test.js diff is shape-only (block selector gains aria-label="Site"), all assertions intact and passing. - No logic change: diff is class/attribute/CSS/test-only; aria-label="Site" on the nav is additive a11y (labelled landmark for the scroll strip). No new deps (4 files changed; package.json/pnpm-lock untouched — Law 1 holds, Tailwind utilities + plain CSS only). Laws 7/8 N/A; Law 12: bugfix under 12_web_ui with in-code comments, no doc amendment needed. Dark+light share the treatment (theme-independent rules; link colors untouched). #278 tray/popover viewport bound asserted intact. Tests: full node suite 564/564 pass (incl. 7 new header-narrow + 3 nav-api-right); vite build:ui exit 0 (~2s, only pre-existing >500kB chunk warning); compiled CSS confirmed to contain scrollbar-width:none + site-nav rules. MERGE RECOMMENDATION: ready to merge (not merging per instructions).
Author
Owner

Fixed by PR #296 (review: flex math + keyboard scroll verified; 564/564), merged. Closing.

Fixed by PR #296 (review: flex math + keyboard scroll verified; 564/564), merged. Closing.
crueber added this to the v1 milestone 2026-09-10 22:27:08 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
crueber/walhub#273
No description provided.