Skip to content

fix(viewer): keep the UI on-screen at every window width (responsive audit)#183

Merged
ivanmkc merged 1 commit into
masterfrom
fix/responsive-ui
Jun 11, 2026
Merged

fix(viewer): keep the UI on-screen at every window width (responsive audit)#183
ivanmkc merged 1 commit into
masterfrom
fix/responsive-ui

Conversation

@ivanmkc

@ivanmkc ivanmkc commented Jun 11, 2026

Copy link
Copy Markdown
Owner

What & why

Audited the viewer for responsiveness at runtime (Playwright against a real dist/server.js, width matrix 320 · 360 · 375 · 414 · 480 · 511 · 600 · 768 · 834 · 1024 · 1280 · 1440 · 1920 plus short/narrow windows and a live resize) — not by eyeballing the CSS.

Good news first: most surfaces are already responsive (the prior @media work, panes-grid auto-fit, viewport-capped popover widths, and the SimpleGrid cols 1→2→N normalization all hold up). The demo boards and welcome gallery had zero page horizontal overflow across the whole matrix. So this PR is deliberately tight — it fixes the breaks that actually remained and locks them in with a regression test.

Findings → fixes

F1 — page scrolled sideways at ≤320px on a writable workspace (worst offender)

A writable board shows the full header action cluster — Follow · Console · History · Share · Export · Templates (six ≥44px icon buttons) + the theme select — in a flex-wrap: nowrap row. At 320px those six buttons + gaps exceed the ~300px available, so the page overflowed.

  • Measured before: documentElement.scrollWidth 336 > clientWidth 320+16px at 320px.
  • It only bit real boards: the /w/demo/ workspace hides Share/Export/Templates, so it fit there (3 buttons) and was easy to miss.
  • Fix (CSS): a @media (max-width: 380px) rule lets .header-actions wrap onto a second strip (buttons stay ≥44px tap targets; the header just grows a row, which beats horizontal overflow). At ≥381px the existing nowrap 2-row header is unchanged.
  • Measured after: overflow none at 320 / 340 / 360 / 375 / 414 / 480.

F2 — popovers only clamped their left edge

openExportMenu / openSharePopover / openTemplatesPopover each did top = r.bottom + 6; left = max(8, r.right - width) — clamping the left edge only, with nothing catching the right/top/bottom. They stayed on-screen today by luck (top-anchored triggers + CSS width caps), not by design.

  • Fix (JS): one shared positionPopover(el, btn) that clamps left into [margin, vw - width - margin] (both edges), opens below the trigger, flips above if that would overflow the bottom, and otherwise pins to the top margin and relies on the element's max-height + scroll. Replaces the three duplicated blocks.

F3 — .export-menu / .share-popover had no max-height

Only .templates-popover had one, so a short window could clip the other two off the bottom.

  • Fix (CSS): cap both at max-height: min(70vh, 520px); overflow: auto, mirroring .templates-popover. This also makes F2's "pin + scroll" branch correct for all three.

After (F2/F3), measured: all three popovers open fully inside the viewport on narrow (320×900, 360×740) and short (768×380, 900×360) windows — right ≤ iw+1, bottom ≤ ih+1, left ≥ -1, top ≥ -1.

Before / after — F1 (the 320px header)

320px writable header
Before page scrolls sideways, scrollWidth 336 > clientWidth 320 (+16px); 6 buttons + theme crammed into one nowrap row
After no horizontal scroll (overflow: none); action cluster wraps to a tidy second strip, every control reachable at ≥44px

(Verified with a Playwright screenshot at 320×560 — header wraps to two rows, no scrollbar.)

Regression test

packages/viewer/e2e/responsive.e2e.mjs (modeled on board-sort / template): boots the server, then asserts —

  • no page h-overflow across the width matrix on a representative demo board per renderer (flow · panes · component · datatable · calltree) + the welcome gallery,
  • no page h-overflow with the full writable toolbar at 320–480px (F1 guard),
  • each popover opens within the viewport on narrow + short windows (F2/F3 guard),
  • no overflow through a live small→large→small resize.

Chained into test:e2e and test:e2e:nobuild. 20/20 pass.

Verification

  • npm run build → exit 0
  • npx vitest run455 passed (30 files)
  • npm run test:e2e:nobuild → rich 62/62, board-sort 12/12, template 15/15, responsive 20/20
  • NUL-byte check (grep -lP '\x00') on every edited .ts/.css → clean

Scope / tradeoffs

  • Changes are presentation-only (CSS media rule, JS popover clamping, e2e). No server/CLI/schema changes.
  • Mantine Grid/SimpleGrid with a responsive-object cols whose base > 1 is not auto-normalized (only numeric cols is, via the existing normalizeLayoutProps). Not observed breaking in the demo corpus; left as content-author guidance rather than forcing global responsiveness, per the conservative option.

…audit)

Audited the viewer at runtime (Playwright, real server, widths 320-1920 +
short/narrow windows + a live resize). Most surfaces were already responsive;
this fixes the breaks that remained:

- Header action bar overflowed the page at <=320px on a writable workspace: the
  full cluster (Follow / Console / History / Share / Export / Templates + theme)
  can't fit one nowrap row that narrow, so the page scrolled sideways (+16px at
  320px). Let the cluster wrap onto a second strip below 380px (still >=44px tap
  targets). The demo hides Share/Export/Templates, so this only bit real boards.

- Popover positioning (Share / Export / Templates) only clamped the LEFT edge.
  Add a shared positionPopover() that clamps into the viewport on BOTH axes --
  flips above the trigger if it would overflow the bottom, pins to the margin and
  relies on max-height otherwise -- so a popover never spills past any edge at any
  window size.

- .export-menu and .share-popover had no max-height (only .templates-popover
  did), so a short window could clip them. Cap both at min(70vh, 520px) with
  internal scroll.

Adds e2e/responsive.e2e.mjs (no page h-overflow across the width matrix on the
demo boards + welcome gallery; popovers on-screen on narrow/short windows; a live
small->large->small resize) and chains it into test:e2e / test:e2e:nobuild.
@ivanmkc ivanmkc merged commit f23f18d into master Jun 11, 2026
5 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