Skip to content

Keep wide tables, code and diagrams usable on phones - #471

Merged
MaggieAppleton merged 6 commits into
mainfrom
mobile/wide-content
Oct 11, 2026
Merged

MaggieAppleton merged 6 commits into
mainfrom
mobile/wide-content

Conversation

@MaggieAppleton

@MaggieAppleton MaggieAppleton commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator

Why

On a phone the document column is about 320px, and wide authored blocks fight it (mobile critique M-22, M-23, M-24, part of M-25):

  • A 561px table scrolls inside a 321px column, with nothing showing that more columns exist (M-22).
  • Code doesn't wrap. A 1174px line shows about 25 characters at a time (M-24).
  • Mermaid diagrams are cut off mid-node and can't be opened larger (M-23).
  • Tabs and the source toggle on these blocks have hit areas under 44px (M-25, editing chrome only; a separate read-mode slice will hide it).

What changed

Gate. The code wrap, table fade, Expand diagram button and sheet apply only on a phone with a narrow document. A phone means (pointer: coarse) and (max-width: 600px), (pointer: coarse) and (max-height: 500px), and narrow means @container (max-width: 40rem). The gate is one @media block in styles.css and one PHONE_QUERY/usePhone() in widgets/phone.ts. Both switch to :root[data-phone] / usePhone() from ../pointer once slice 2 lands. Narrow desktop windows (1280, 1024, 900, 768 with Chat open) keep their scroll lanes exactly as before (1280 screenshot below). The 44px hit areas apply to any coarse pointer.

  • Tables keep their own scroll lane and fade the side that still hides columns (data-more="start|end|both"). This is the same vocabulary and mask the Mermaid preview and tab strip already use. A small scroll and resize listener sets the attribute, writing it only when the value changes, so it works on iOS Safari 17 without scroll-driven animations. Tables are found again after every editor update, so the fade survives Lexical replacing a table's element. Native panning and the row and column rails are unchanged.
  • Code and diff previews wrap. The gated CSS sets a --plan-code-overflow switch. CodeView reads it in a layout effect before the renderer mounts, so a block is never highlighted twice, and passes overflow: "wrap" to @pierre/diffs. The renderer's shadow root can't see container queries, which is why it goes through a custom property. Wrapped lines get a 2ch hanging indent. The editable source wraps too, except plain-text fences. There is one owner and no remount: crossing the gate flips data-overflow on the same node.
  • Mermaid diagrams. A diagram still fits to width when it stays legible (0.75 floor, now in diagram-fit.ts with unit tests). Below that floor it keeps its readable size, scroll lane and fade, and gets an Expand diagram button in a row under the scroll lane, so it never covers the drawing. The button opens a full-screen modal sheet:
    • It is safe-area aware.
    • It rises from the bottom on --motion-smooth-out; with reduced motion it fades instead.
    • It opens at authored size unless the whole drawing fits legibly. One toggle switches between Fit and Actual size, using the existing FitWidthIcon and ActualSizeIcon.
    • Close is a 44px button. Escape closes the sheet. Tab cycles through links, fields, buttons and the scroller.
    • Focus returns to Expand after closing, including after a tap on Close.
    • The drawing pans and pinch-zooms natively.
  • SeeCode: no Expand in this PR. Its viewer re-derives a compact layout from the width it is given, so a sheet copy was no better than the inline view. Making it genuinely better needs a natural-width mode in @chopin/diagrams, which is better done as a follow-up. SeeCode keeps its existing scroll lane and zoom controls.
  • Hit areas (coarse pointer). Authored tabs get a 44px target from a pseudo-element; the tab list makes room for it so the scroller doesn't clip it. The source toggle's target now also grows sideways, away from the language menu. The language menu, Add tab and remove-tab buttons already had 44px targets.
  • New ExpandIcon in @chopin/icons.

Screenshots

iPhone SE, stress fixture. Table, then the same table scrolled (fades on both sides):

Table before and after
Table scrolled before and after

Code wraps:

Code before and after

Diagram, with Expand in its own row under the scroll lane:

Diagram before and after

iPhone 15 Pro: the inline diagram, the sheet at authored size (how it opens), and the sheet after Fit:

Expanded diagram sheet

iPhone 15 Pro, a realistic document (wrapped code, table scrolled):

Phone document

Narrow desktop, 1280×800 with Chat open (document 505px). Unchanged; the document region diffs at 0 pixels, as it does at 1440×900:

Desktop 1280 unchanged

Testing

  • e2e/wide-content.e2e.ts, phone cases using hasTouch at 390×844:
    • The table's data-more follows scrolling (end, both, start) and keeps updating after a cell edit.
    • Code wraps with no horizontal scroll in the preview or the source. Leaving the phone class flips the same node back to scroll without a remount.
    • The diagram's Expand sits below the drawing. The sheet opens at authored size for this fixture; Fit fits it, and the toggle then reads Actual size. Tab cycles inside the sheet.
    • Escape returns focus to Expand, and so does a tap on Close.
  • e2e/wide-content.e2e.ts, desktop case at 1280×800 with Chat open: code scrolls, the table has no mask, and there is no Expand button.
  • The earlier edits to responsive-content, code, syntax-contrast and e2e/design/responsive are reverted. Those tests use fine pointers, so their assertions still hold.
  • Unit tests:
    • diagram-fit.test.ts: the fit floor, which also covers the sheet's fitted-open path.
    • tab-edges.test.ts: hiddenSides.
    • bun test packages/editor packages/icons passes.
  • bun run types passes. dprint, oxlint, tokens, design record and impeccable pass.
  • Checked by hand against a fake-GitHub server with headless Chromium: iPhone 15 Pro, iPhone SE, phone landscape, 1280×800 and 1440×900.

Design-contract review

Reviewed and renewed in Renew design-contract review hashes for code-view.tsx:

  • packages/editor/src/widgets/code-view.tsx (dynamic-editor.json): renderer options gain overflow (one of two literals) and a constant unsafeCSS string. No new class or style logic.

On rebase, main had already added an ExpandIcon to packages/icons/src/line.tsx, so this PR's separate expand.tsx (and its exception entry) was dropped in favour of main's icon.

Gallery test

e2e/diagram-gallery/gallery.e2e.ts "a wide wireframe keeps its flow across" read bounding boxes once and failed in CI before the wireframe's container-query layout settled. It now polls the real layout condition (same assertions, no loosening).

Needs a real device

  • iOS Safari: the table mask fade, and horizontal swipe against vertical page scroll.
  • Pinch inside the sheet zooms the whole page; check that Close stays reachable afterwards.
  • Rubber-band scrolling through the sheet's bar.
  • Safe-area padding in landscape with the notch.

🤖 Generated with Claude Code

@MaggieAppleton

Copy link
Copy Markdown
Collaborator Author

Review: Changes needed

What works (checked on a fake-GitHub server with headless Chromium on iPhone 15 Pro, iPhone SE, 15 Pro landscape, 1440×900, 1280×800, 1024, 900 and 768): code wraps with the hanging indent; crossing 40rem switches data-overflow on the same preview element with no remount (one owner holds); table data-more follows scrolling; 1440 is unchanged; Escape, Close, the Tab cycle and the reduced-motion crossfade work; focus goes back to Expand after closing from the keyboard; all new targets are 44px.

Must fix

  1. Narrow desktop windows get the phone treatment, and the PR body doesn't say so. At 1280×800 with Chat open, the document is 505px, so code wraps, tables fade, and both diagrams show the 44px round Expand button and the phone sheet. The same happens at 1024, 900 and 768. Only 1440 (document 665px, just above 40rem) is unchanged. The brief says desktop must stay visually unchanged. Wrapping code in a 505px pane is a reasonable call, but it needs to be decided on purpose. Fix: either gate the Expand button and sheet (and possibly the fade) on the compact viewport or a coarse pointer, or state the desktop change in the PR body and add 1280 screenshots.
    1280 desktop: code wraps, table fades

  2. The Expand button covers the drawing (styles.css:574, inset-block-start: calc(var(--spacing) * 10)). On iPhone 15 Pro it sits on the first node's top-right corner, right where the fade and arrow say there's more to the right. Fix: move it out of the drawing, either to the bottom-right below the scroll lane or into a caption or chrome row.
    Expand overlapping a node

  3. The sheet opens less readable than the inline diagram (diagram-sheet.tsx:12, FIT_FLOOR = 0.2). A left-to-right flowchart in portrait opens fitted at about 0.3 scale, so labels are around 4px. That's smaller than inline, which was the problem the sheet was meant to solve. Fix: open at authored size whenever fitting would go below the inline floor (MIN_DIAGRAM_SCALE 0.75), and make Fit the toggle. Also use the existing FitWidthIcon / ActualSizeIcon instead of +/−, to match SeeCode and the image lightbox. Right now "+" reads as one zoom step, not "actual size".
    Sheet fitted on portrait phone

  4. The SeeCode Expand doesn't help on a phone yet.

    • Inline on iPhone SE, the new ch-diagram__expand group wraps onto its own third row under a stray divider (diagram.tsx:458).
    • In the sheet, SeeCode re-derives the same 320px narrow view: scrollWidth == clientWidth == 320, labels are still clipped ("REQUIRES AT", "COP"), and its controls start at y=600 in a 568px viewport, so they're below the fold.
    • Fix: give the sheet copy a wider stage (authored layout, pan natively) with its controls pinned to the bottom safe area, or leave SeeCode's Expand off phones for now and keep the group from wrapping alone.
      SeeCode inline on SE SeeCode sheet on SE

Nice to have

  • code-view.tsx:197: the overflow read runs in useEffect, so on a phone each code block first paints at scroll, then re-highlights and re-lays out at wrap. That's a visible jump below the fold and doubles the highlight work on load. Use useLayoutEffect.
  • code-view.tsx: diffOptions doesn't get unsafeCSS: HANGING_INDENT. Wrapped diff lines have no hanging indent; the gutter partly makes up for it.
  • edges.ts:12: mark() calls setAttribute on every scroll event even when the value hasn't changed. Compare it with getAttribute first to avoid style invalidation per frame.
  • diagram-sheet.tsx:9: STOPS leaves out a[href], select, input and textarea. Mermaid output can contain links, so the trap would let them escape.
  • Opening by tap: onMouseDown preventDefault means focus never reaches Expand, so after closing focus falls to <body>. Keyboard opening restores correctly. Consider focusing the button in onClose.
  • Plain-text fences still clip with no fade (co… at the right edge). This is out of scope, but it's now the only wide block without an edge cue.

Tests (e2e/wide-content.e2e.ts)

  • Line 107: if (await zoom.isVisible()) await zoom.click(); passes whichever state the sheet opens in. Assert the opening state you expect for this fixture, and add a fixture for the other path (at size, or the new floor).
  • After Escape, assert focus returns to Expand diagram.
  • Table: edit a cell (Lexical can replace the table element, as the lane note says), then scroll and assert data-more still updates. That's the listener-recovery path useTableEdges exists for.
  • Code: tag the .plan-code-view node before setViewportSize(DESKTOP) and assert the same node is still there afterwards, to lock in "one owner, no remount".
  • Add a 1280×800 case (with Chat open) that asserts whatever desktop behaviour you decide on in must-fix 1.

Needs a real device

  • iOS pinch inside the sheet zooms the whole page (no viewport restriction), not just the drawing. Check that Close stays reachable after a pinch.
  • Rubber-band scroll through the sheet's non-scrolling bar (Chromium shows the page is locked: html overflow: hidden, modal is inert).
  • Safe-area padding in landscape with the notch.

@MaggieAppleton

Copy link
Copy Markdown
Collaborator Author

Review: Looks good

I re-checked 0eead19 on a fresh worktree and server: headless Chromium on iPhone 15 Pro, iPhone SE, 15 Pro landscape, iPad Mini, 1440, 1280, 1024 and 768. All four must-fixes are resolved.

Earlier must-fix Now
Narrow desktop got the phone treatment Gated on the phone class (coarse pointer and ≤600px wide, or ≤500px tall). At 1440, 1280, 1024, 768 and on iPad Mini, code scrolls, tables have no mask and there's no Expand button; 1280 looks the same as main. iPhone 15 Pro, SE and landscape get all three.
Expand covered the drawing It now has its own labelled row under the scroll lane, with a 44px hit area through the coarse-pointer ::after (I probed it with elementFromPoint).
Sheet opened at about 0.3 scale Opens at authored size when fitting would go below 0.75 (fitsLegibly, unit-tested). The toggle uses the Fit width / 1:1 icons, with labels "Fit diagram to screen" / "Actual size".
SeeCode Expand was weak Removed; diagram.tsx is back to main and SeeCode on SE is back to its old layout.

All the earlier nice-to-haves are done too:

  • useLayoutEffect plus an undefined first state, so the renderer never mounts with a guessed overflow;
  • the hanging indent on diffs;
  • mark() skips writes when nothing changed;
  • STOPS now includes links and form fields;
  • after closing from a tap, focus goes back to Expand, with no :focus-visible ring.

The tests are now deterministic. They cover: a cell edit that replaces the table element; the preview node keeping its identity across the wrap/scroll switch; a 1280 fine-pointer case; the Expand row sitting below the drawing; focus returning after both Escape and a tap; and the at-size → Fit → Actual path. bun test packages/editor/src/widgets passes. bun run ci fails only on the two design-contract hashes listed in the PR body, and the body's code-view hash matches the file.

Expand in its own row (iPhone 15 Pro) Sheet opens at authored size 1280 desktop unchanged

Nice to have (not blocking)

  • apps/web/DESIGN.md still says the treatments apply "when the document is narrower than 40rem". It should say on a phone (coarse pointer, small screen), with a narrow document.
  • PHONE_QUERY in widgets/phone.ts and the @media in styles.css repeat the same query. The comment says they must match; when the shell's :root[data-phone] lands, switch both in the same change.
  • At authored size, the drawing sits at the top of the sheet with empty space below. Centring it vertically when it's shorter than the sheet (the same margin-block: auto the fitted mode uses) would feel more settled.

Still needs a real device: iOS pinch inside the sheet (page zoom; check Close stays reachable), the table mask while swiping, and safe areas in landscape.

Expand in its own row (iPhone 15 Pro)

Sheet opens at authored size

1280 desktop unchanged

MaggieAppleton and others added 6 commits October 11, 2026 09:44
When the document is narrower than 40rem: a wide table fades whichever
side still hides columns, code and diff previews wrap (with a hanging
indent) instead of scrolling a few characters at a time, and a Mermaid or
SeeCode diagram below its readability floor offers an Expand button that
opens it in a full-screen sheet. Coarse pointers get 44px hit areas on
authored tabs and the source toggle. Wide documents are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sheet

Code wrap, the table fade and Expand diagram now apply only to a coarse
pointer on a small screen with a document under 40rem, so narrow desktop
windows with Chat open keep their scroll lanes. Expand sits in a row under
the scroll lane, the sheet opens at authored size unless the whole drawing
fits legibly, and SeeCode keeps its own controls with no Expand for now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ESIGN.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- packages/editor/src/widgets/code-view.tsx: renderer options gain a finite
  scroll/wrap overflow read from a stylesheet custom property and a constant
  hanging-indent unsafeCSS string; no caller-supplied values reach styles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The test read bounding boxes once, before the wireframe's container query
had necessarily applied, and failed in CI with the flow still stacked.
Poll the real condition instead of sampling the first layout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaggieAppleton
MaggieAppleton merged commit c2c7a2e into main Oct 11, 2026
3 checks passed
@MaggieAppleton
MaggieAppleton deleted the mobile/wide-content branch October 11, 2026 08:59
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.

1 participant