Skip to content

Polish phones: child reading mode, a 16px field floor on touch and shared sheet tokens - #484

Open
MaggieAppleton wants to merge 3 commits into
mainfrom
mobile/phone-polish
Open

MaggieAppleton wants to merge 3 commits into
mainfrom
mobile/phone-polish

Conversation

@MaggieAppleton

@MaggieAppleton MaggieAppleton commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator

Final follow-up to the mobile redesign (#470–#478). Covers the "phone polish" notes left by #477, #478 and #473. Desktop is unchanged.

Why

  1. Text fields under 16px on touch. Make Chopin installable and fix the phone platform baseline #470 set a 16px floor in @layer base, but unlayered field rules beat it: .question-field { font: inherit } (the "Add an option" field, 13.4px) and .title-control-label { font-size: inherit } (the header rename on a touch tablet, 12.2px). On iOS, focusing either one zooms the page.
  2. Child (nested) documents on a phone. These were always editable. The tab-bar pencil toggled the parent, which is hidden under the child, so the top bar said "Editing" while the child stayed unchanged. Decision cards inside a child showed the desktop Discard/Save buttons.
  3. Sheets didn't match. The comment sheet (Dock phone selection actions and give the comment sheet detents #474) and the documents and action sheets (Give phones a documents sheet and action sheets #478) each had their own scrim, grabber, radius and timing. The comment sheet's scrim was also navy: it passed an oklch triplet to rgb().
  4. Wide content used its own copy of the phone media query.

What changed

  • 16px floor, fixed in the cascade. The coarse-pointer floor in apps/web/src/theme.css is now font-size: max(var(--field-text-min), 1em) !important. Only !important beats an unlayered inherit. 1em is the inherited size, so a field that inherits larger type keeps it. The small --text-* tokens are still raised on the field, so placeholders follow. Fine pointers are untouched.
  • Child documents on a phone (room-workspace.tsx and the new child-phone-chrome.ts):
    • Deviation from the brief: the brief planned a separate floating pencil because it assumed a child shows no tab bar. In practice the parent's tab bar stays visible over an open child. While a child is open, that bar's 60px glass action edits the child instead. It sits in the same bottom-right thumb-zone spot, and there is still only one pencil.
    • The child opens for reading by default. The pencil puts the caret where the reader tapped. Tapping it from Chat brings the Document tab back first.
    • The docked formatting toolbar takes the parent bar's place above the keyboard.
    • The top bar's "Editing" label follows the child.
    • Viewers get no pencil.
    • New DecisionSaveHost (editor package): when there is no Decisions pane, as in a child, phone cards save from the same SaveBar as the pane, resting above the tab bar. The save-bar CSS now applies to both hosts.
  • Shared sheet tokens (packages/visuals/theme.css):
    • --color-grabber, --sheet-grabber-width/height/inset, --sheet-ease and --sheet-scrim-medium are new. Both sheet families also use the existing --color-scrim, --sheet-rise-dur, --sheet-fall-dur and --radius-sheet.
    • The comment sheet now uses the shared scrim: 75% of it at the medium detent and the full scrim at the large detent.
    • Its shadow is now --shadow-overlay.
    • It leaves faster than it arrives, like the other sheets. Reduced motion is unchanged.
  • Cleanup: wide-content rules in packages/editor/src/styles.css and phone Chat markdown now key off :root[data-phone]. usePointerCapabilities now runs for every route, including the development audit and gallery pages, so the attribute matches isPhone everywhere.

Review follow-ups (second commit)

  • While the formatting dock is up, the document scroller pads its foot by the dock's row. --app-height already ends the workspace at the keyboard, so the keyboard inset is not added again. When the dock appears, the caret's block scrolls back above it, in root and child documents.
  • A child opening over a document that is being edited puts that document back into reading.
  • A flicked comment sheet still sets the pace of its dismissal.
  • The phone Chat composer now keys off :root[data-phone].
  • Removed the unreachable childRoom && !childChrome fallback: a child only ever renders inside its parent's workspace.
  • Added a one-line comment on the tradeoff of the !important floor.

Screenshots

Child document on a phone, before (always editable; the pencil toggles the hidden parent into "Editing"):
Child before

After (opens for reading; the pencil edits the child):
Child after

Decision inside a child, before → after (card Discard/Save → phone save bar):
Child decision save bar

Header rename on a touch tablet (1194×834), before (12.2px) → after (16px):
Tablet rename font size

Sheets, before (left pair) → after (right pair). The comment sheet's navy scrim now matches the documents sheet:
Sheets side by side

Testing

  • New e2e/responsive-child-phone.e2e.ts covers:
    • a child opens for reading
    • one pencil, at least 44px wide
    • tapping prose raises no keyboard
    • the pencil edits the child while the parent keeps reading
    • the pencil works from Chat
    • formatting docks above a simulated keyboard
    • the save bar counts, clears and saves above the tab bar
    • a viewer gets no pencil
    • a desktop child keeps its card's own Save
  • New e2e/responsive-touch-fields.e2e.ts scans every visible input, textarea, select and editable root for at least 16px. It runs on a phone (document while editing, comment, Chat, Decisions "Add an option", rename, documents search) and on a touch tablet (document, comment, Decisions, header rename, search palette). Desktop rename stays below 16px.
  • Ran locally with bun run e2e against my own databases, because another worktree held the Docker e2e ports:
    • the 7 new tests pass
    • 198 tests pass across phone-read-mode, decisions-phone, comment-sheet, documents-sheet, phone-workspace, wide-content, research-child-surface, research-child-motion, chat-phone, focus-ring, discarded-mobile-geometry, document-navigation, pwa, interface-quality and responsive*
  • The second commit adds e2e tests with the fake keyboard: the dock keeps the caret's block clear in a root document and in a child document, and opening a child from the documents sheet ends the parent's editing. I checked that the child dock test fails without the scroll effect. 94 tests pass across the child, touch-fields, phone read mode, chat phone and composer, comment sheet, research child, decisions phone, phone workspace and documents sheet specs.
  • bun run types passes.
  • bun test packages/editor apps/web packages/visuals passes: 1455 tests, with the ignored local design-jig folder moved aside.
  • dprint, oxlint, check-tokens and check-design-record pass. check-design-contract passes after the review commit.

Design-contract review

Reviewed and applied in Renew design-contract review exceptions for phone polish:

  • preserved-values.json:
    • Approved the floor font-size: max(var(--field-text-min), 1em), the token-based sheet rise transition, and the swipe-scaled dismissal transition-duration.
    • Removed the four stale entries: the rgb(var(--shadow-color)) backdrop, the raw upward shadow, the old popup transition, and var(--field-text-min).
  • dynamic-editor.json: renewed the plan-editor.tsx hash. The only change is the effect that scrolls the caret's block above the dock.

Needs a real device

  • Check that iOS Safari no longer zooms when focusing the iPad header rename or the "Add an option" field.
  • In a child on an iPhone: tap the pencil and check the keyboard rises inside the tap. Toggling goes through the parent's bar via flushSync, as on root documents.

🤖 Generated with Claude Code

@MaggieAppleton

Copy link
Copy Markdown
Collaborator Author

Review: Looks good

I built this branch (8839) and origin/main (8840) side by side, both on the fake GitHub server with NO_PLANNER=1. I checked them with my own headless Playwright on iPhone 15 Pro, iPhone SE, a touch tablet (1194×834) and desktop (1440×900).

Verified

  • 16px floor. I compared the computed font size of every visible field on main and this branch. That covered the root document, the stress fixture, a child, comment draft, Chat, Decisions "Add an option", phone rename, documents sheet search, tablet header rename and the search palette.
    • Only two values change: .question-field goes from 13.4px to 16px on a phone, and .title-control-label goes from 12.2px to 16px on the tablet.
    • The document body, headings, callout title, code and diff views, and the Chat composer are unchanged.
    • Desktop is identical for every field.
  • Child on a phone.
    • The bug is real on main: the pencil sets the hidden parent to contenteditable=true, while the child stays always-editable.
    • On this branch, the child opens with contenteditable=false. The pencil edits only the child, and the parent stays false. "Editing" follows the child.
    • Pressing the pencil from Chat brings the Document tab back with the child editing. Leaving editing from Chat works.
    • Formatting docks above a simulated keyboard, and the tab bar hides.
    • A viewer gets no pencil.
    • Back to the parent resets cleanly: the parent pencil reads "Edit document" and toggles the parent.
    • Opening a child while the parent is editing shows the child reading. On close the parent is still editing.
    • A desktop child is pixel-identical to main.
  • DecisionSaveHost in a child.
    • The card loses its Save, and the bar shows "1 unsaved answer". Clear and Save work, and the saved card resolves.
    • The bar reopens after a reload in the same session.
    • The Answer decisions on phones from one pane, with a save bar and a closure #473 rule holds. A second writer sees no bar for ana's draft. A fresh session for the same user, where sessionStorage has no touched record, falls back to the card's own Save, as the pane does.
  • Sheets.
    • The comment sheet scrim was rgb(0,0,95) at 0.24 on main. It is now --color-scrim at 0.75 (medium detent) and 1 (large detent).
    • Grabber (36×4px, 6px inset), 14px radius, shadow, 420ms rise and easing now match the documents sheet exactly.
  • :root[data-phone]. Table fade, code wrap and diagram actions match main on phone, tablet and desktop. Per-route mounting looks right.
  • Design-contract exceptions. check-design-contract reports exactly the 2 new declarations and 4 stale entries listed in the PR body. The list is accurate and minimal, and the --text-* entries stay valid. bun run ci fails only on these.

Nice-to-have

  1. packages/editor/src/styles.css:1831-1834: the [data-ending-style] transition-duration: var(--sheet-fall-dur) overrides the popup's calc(var(--drawer-swipe-strength, 1) * …). A fast flick-to-dismiss now always takes 260ms instead of scaling with velocity. Consider transition-duration: calc(var(--drawer-swipe-strength, 1) * var(--sheet-fall-dur)) on the popup. This would need one more contract entry.
  2. apps/web/src/theme.css:1266: the floor is a layered !important, so it beats every unlayered rule, including a field's own explicit size. A field that sets a size larger than its parent (for example a text-lg input in a 16px container) would be clamped to max(16px, parent) on touch. No current field does this; I checked them all. Worth adding one line to the comment so a future large field isn't silently shrunk.
  3. apps/web/src/chat/composer.css:291 still has its own copy of the phone media query. Its comment even says :root[data-phone] can replace it. It is the last copy, if the cleanup is meant to be complete.
  4. apps/web/src/room-workspace.tsx:1140: childRoom && !childChrome ? undefined : editMode. A child always renders inside its parent's provider, so the fallback looks unreachable.
  5. When a child opens over a parent that is editing, the hidden parent stays contenteditable=true until the child closes. This is harmless today, but ending the parent's edit when a child opens would match "one pencil, one editing document".

Real device: the caret's paragraph can sit under the docked toolbar in the simulated keyboard. This happens on root documents too, so it is not caused by this PR. Please confirm on iOS that the browser scrolls the caret clear, along with the zoom checks listed in the PR body.

@coolify-githubnext-app

coolify-githubnext-app Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

The preview deployment for chopin is in progress. 🟡

Open Build Logs | Open Application Logs

Last updated at: 2026-10-11 13:29:43 CET

MaggieAppleton and others added 3 commits October 11, 2026 17:54
…nd shared sheet tokens

- Child documents on a phone open for reading; the parent's tab-bar pencil
  and docked formatting toolbar edit the open child instead of the hidden
  parent, and the child's decision cards save from the phone save bar.
- The coarse-pointer 16px text-field floor is important, so unlayered
  `font: inherit` / `font-size: inherit` rules (question fields, the header
  rename) no longer drop fields below 16px and trigger iOS zoom.
- Comment, documents and action sheets share one scrim, grabber and set
  of sheet durations and easing from the theme. The comment sheet's scrim
  was navy because it read an oklch triplet as rgb.
- Wide-content and phone Chat markdown rules use :root[data-phone], which
  every route now sets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- While formatting docks above the keyboard, the document scroller pads its
  foot by the dock's row and brings the caret's block back above it, in root
  and child documents.
- A child opening over a document being edited returns that document to
  reading.
- A flicked comment sheet still sets the pace of its dismissal.
- The phone Chat composer keys off :root[data-phone].
- Drop an unreachable child fallback and note the 16px floor's tradeoff.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
preserved-values.json:
- apps/web/src/theme.css: the coarse-pointer field floor is now
  max(var(--field-text-min), 1em) (with !important), so an unlayered
  font: inherit cannot drop a touch field below 16px; 1em keeps a larger
  inherited size. Replaces the var(--field-text-min) entry.
- packages/editor/src/styles.css .plan-comment-sheet-popup: the approved
  swipe-scaled rise now uses the shared --sheet-rise-dur and --sheet-ease.
- .plan-comment-sheet-popup[data-ending-style]: the dismissal duration is
  scaled by Base UI's runtime --drawer-swipe-strength the same way, using
  --sheet-fall-dur.
- Removed the stale backdrop rgb(var(--shadow-color)) and the popup's raw
  upward box-shadow; both are tokens now (--color-scrim, --shadow-overlay).

dynamic-editor.json:
- packages/editor/src/plan-editor.tsx: the only change is an effect that
  scrolls the caret's block above the formatting dock; no class or style
  logic, and the reviewed className composition is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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