Repository navigation
Open documents for reading on phones, with an edit toggle - #477
Conversation
|
Review: Changes needed The core model is right. Reading is local state kept apart from permission Must fix
Should fix
Nice to have
#474 integration (
|
c99e0c9 to
f8f32f6
Compare
|
Review: Looks good I re-checked the latest head (b3c5375) on :8838 with a fresh worktree and headless Playwright: iPhone 15 Pro, iPhone SE, desktop 1440/1280, an 820px tablet and iPad Mini. Every must-fix and should-fix from my first review is resolved. CI: e2e and container pass. "format, lint, types, tests" fails only on the design contract: the 35 "reviewed dynamic owner changed" pins plus the new Verified fixed
Nice to have (non-blocking)
|
1bae625 to
a96ebe6
Compare
b8cbf08 to
2cac8ca
Compare
Phones now open a document in a local reading mode: the editor is not editable, so tapping prose places no caret and raises no keyboard, links follow on tap, and editing-only chrome (Show source, code language menus, image editing, tab add/remove, table rails, slash menu) stays hidden. Selection and commenting keep working, because reading is separate from the permission-based readOnly lock, which still wins. A pencil in the phone tab bar switches to editing inside the tap, placing the caret where the reader last tapped or at the first block in view. While the keyboard is up, a docked formatting toolbar (bold, italic, heading, list, link, insert, hide keyboard) replaces the tabs and the selection bubble. Empty and newly created documents open ready to write, and a new document's title hands focus to the body as its rename starts, so keys typed meanwhile are kept. The mode lasts across tab changes and reconnects and resets per document. Desktop and viewers are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y layout Reopen and discard on a resolved comment are sidecar actions, so reading mode keeps them; only other locks hide them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…w details - The dock needs focus in the visible document's editor, so Chat, a comment reply or the title rename never get it. - The pencil opens the document from Chat, which overlays it without changing the view. - Entering edit mode blurs a root that a reader's tap already focused before focusing it, so a real focus event fires (iOS keyboard, presence). - Hide keyboard is pinned outside a scrolling, edge-faded command row; a Comment button joins it while text is selected. - Insert block starts a new paragraph instead of splitting a word, and the slash menu keeps above the dock. - ListIcon in @chopin/icons for the dock and the block menu. - Title focus returns to the field when a rename fails; an "Editing" cue in the phone top bar; inert task boxes look inert while reading; the comment-only bubble is labelled "Selection". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s split Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The top bar's small title waits for the heading to scroll away, which hid the Editing subtitle with it; a standalone Editing pill now stands in while the title is hidden. The dock's command row fades whichever edge has buttons scrolled past it. Insert block keeps placing its paragraph after the whole list or table, and says why. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The selection bar from #474 now takes its gate from selectionOffer(), so a reader's lock no longer hides it; the reading spec asserts the bar itself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e content The interim phone checks from the mobile slices (coarse and at most 430px for the comment sheet and selection bar, and a copy of the phone media query for wide content and decisions) now all use usePhone() from @chopin/editor/pointer, the class the shell mirrors as :root[data-phone]. The comment sheet and docked selection bar therefore apply to every phone, landscape and phones up to 600px wide included. The toolbar spec that needed a non-phone touch screen moves from 600px to 601px. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dd ListIcon - apps/web/src/workspace.tsx: passes a phoneToolbar node through to the tab bar; no class or style change. - packages/editor/src/toolbar/bubble.tsx (20 pinned cases, one file): extracts convertBlock and adds a comment-only variant with a literal aria-label; the reviewed placement and class sites are unchanged. - packages/editor/src/toolbar/slash.tsx: an open-slash command and a viewport clipped above the dock, numeric geometry only. - packages/editor/src/plan-editor.tsx: reading mode, caret placement and a data-plan-reading flag; no new style expressions. - packages/editor/src/resolved-layer.tsx and comment-layer.tsx: reading keeps sidecar actions, and the sheet follows the shared phone class. - packages/editor/src/widgets/questionnaire.tsx: imports the shared phone hook; nothing else changed. - packages/icons/src/list.tsx: new icon forwarding IconProps to LineIcon, like the reviewed sibling icons. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…a heading - wide-content and pwa: phones open documents for reading, so the specs that type into a table cell, show source, or measure the callout title field tap Edit document first. - phone-read-mode: the Editing pill stands in for the bar's title only while the document's heading is in view; seed one so the test exercises it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
eb5a32c to
20a0dd8
Compare
- phone-read-mode: phones open document actions as a sheet (#478), so Rename is a button in that dialog, not a menu item. - wide-content: entering editing puts the caret in the first block in view, which can already open the code's source; accept either state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Was stacked on #475 (phone shell), which has merged along with #474 and #478; this is now based on
main.Why
On a phone the document was always contenteditable (critique M-19, P0): tapping prose placed a caret and raised the keyboard, links didn't follow and opened an overflowing "url · Open · Edit" popover (M-20), and readers saw editing chrome such as Show source, code language menus, image handles, and Add/remove tab (M-25).
What changed
PlanEditortakes a neweditMode?: "reading" | "editing"prop. This is a local, per-client mode and is never synced. Reading locks Lexical the same way any lock does (MDXEditorreadOnly, thensetEditable(false)), but it stays separate from the permission-basedreadOnly, so:BLUR_COMMAND. A contenteditable that stops being editable while focused fires no blur, so without this, peers would keep seeing this client's caret through awareness.phoneActionslot gets a glass pencil, "Edit document". In edit mode it becomes a filled brand check, "Done editing". It changes mode inside the tap withflushSync, so focus lands within the user gesture and iOS raises the keyboard. The caret goes where the reader last tapped (caretPositionFromPoint/caretRangeFromPoint), or else to the first block in view, so starting to edit never scrolls the reader away. Writers only. The pencil fits the compact landscape bar (48px there).titleEdit === "new"), or a document that is empty when it first arrives, opens ready to write.toolbar/dock.tsx). While editing with the software keyboard up (:root[data-keyboard-open]), the toolbar replaces the tab bar in the same row and rides above the keyboard: Bold, Italic, Heading (H2 ↔ text), Bulleted list, Link, Insert block (types a/and arms the slash menu through a newOPEN_SLASH_MENU_COMMAND), and Hide keyboard. It uses 44px buttons and scrolls horizontally on narrow phones. Presses never take focus. It reuses the bubble's commands (block conversion is extracted asconvertBlock). While the dock shows, the selection bubble is suppressed, so the two never appear together. With the keyboard down, the tabs and the check come back, and a selection gets the normal bubble.readOnly$lock: code language menus (shown as a static label), Edit image and resize handles, Add/remove tab, table rails and toolbar, and the slash menu.resolved-layer.tsxused to gate them on Lexical editability.RoomWorkspacestate, so it survives tab changes and reconnects.RoomWorkspaceis keyed by channel id, so opening another document starts in reading mode again.packages/editor/src/edit-mode.ts:documentEditMode,opensEditing, andselectionOffer, with unit tests.#474's selection bar (done in this PR)
SelectionBarfrom #474 now takes its gate fromselectionOffer()intoolbar/index.tsx. A phone shows it unless the offer is"none": the keyboard is up and the dock owns formatting, or a real lock applies. A reader's selection therefore keeps the docked Comment bar.phone-read-mode.e2e.tsasserts the "Selection actions" bar with Comment and no formatting.One phone class
The interim phone checks are gone:
packages/editor/src/phone.ts(coarse pointer and ≤430px) andwidgets/phone.ts. The comment sheet (in bothcomment-layer.tsxandresolved-layer.tsx), the selection bar, decisions, the questionnaire chip and wide diagrams all useusePhone()from@chopin/editor/pointer, which is the class the shell mirrors as:root[data-phone]. The comment sheet and docked bar therefore cover landscape phones and phones up to 600px wide. The toolbar spec that needed a non-phone touch screen moves from 600px to 601px.Rebase notes
planelement. Reading mode applies to it, but not to a child room rendered on its own, which has no tab bar and so no pencil. On phones a child document stays editable as before (follow-up).PhoneHeaderfollows Give phones a bottom tab bar and a collapsing top bar #475's root-title model, with no child title and no Return button.Screenshots
BEFORE (main, 8811) on the left, AFTER on the right. iPhone 15 Pro emulation.
Tapping prose: before, a focused editor and caret; after, nothing is focused.

Tapping a link: before, the overflowing preview popover; after, the link opens in a new tab and no popover appears.

Editing chrome: before, Show source and language menus for a reader; after, plain language labels.

AFTER, reading compared with editing on the same block. The chrome returns in edit mode.

AFTER: a reader's selection offers Comment (left); editing with the keyboard up shows the docked toolbar (middle, fake keyboard from

e2e/visual-viewport.ts); editing with the keyboard down shows the tabs and the check (right).Compact landscape bar, reading and editing:

Dock at 320px and on iPhone SE with the keyboard up. It scrolls when narrower than seven buttons.

Desktop at 1440×900 is unchanged: always editable, no pencil, and an e2e test covers it.
Testing
bun test packages/editor apps/web: all pass, including the newedit-mode.test.ts.bun run types: pass.bun run ci: dprint, oxlint, tokens and design record pass. The design contract fails only on reviewed-hash pins (see below, plus the ones Give phones a bottom tab bar and a collapsing top bar #475 already lists).e2e/phone-read-mode.e2e.tscovers:e2e/room.ts:ready()accepts a phone that opened for reading, and a newedit(page)helper taps the pencil.Text formattingtoolbar.Needs a real device:
flushSyncpath).pointerdown/mousedownpreventDefaulton dock buttons keeps the keyboard up on iOS Safari and Android Chrome.caretRangeFromPointaccuracy for the tapped caret position.Follow-ups:
@chopin/icons, which needs a design-contract exception.Design-contract review
Reviewed and renewed after the rebase, in
Renew design-contract review hashes for the reading-mode owners and add ListIcon. CI reported 35 entries, but they come from 6 files:bubble.tsxalone pins 20 cases under one file hash. Renewed:workspace.tsx,toolbar/bubble.tsx,toolbar/slash.tsx,plan-editor.tsx,resolved-layer.tsx,comment-layer.tsxandwidgets/questionnaire.tsx. None of them adds dynamic style logic. Added adynamic-packages.jsonentry for the newpackages/icons/src/list.tsx.Review fixes (after the first review)
Dock scope. The dock now needs four things at once: edit mode, the keyboard up, the Document destination visible, and focus inside this workspace's
.plan-content. Chat, a comment reply and the title rename never get it. There are e2e tests for the chat composer and for rename.Pencil from Chat. It now switches to the Document. The check uses
planVisible, because Chat overlays the document without changingview. The e2e test covers both Chat and Decisions.iOS focus. When a reader's tap already focused the locked root, entering edit mode blurs the root and then focuses it again, both inside the tap. This way a real
focusevent fires, which raises the iOS keyboard and announces the caret to peers. A comment in the code explains why. An e2e test clicks the pencil without moving focus and asserts that the root gets a focus event.Dock layout. Hide keyboard is pinned outside a scrolling command row: Bold, Italic, Heading, List, Link, Comment (while text is selected), and Insert. The row's trailing edge fades while more buttons wait past it. Nothing is cut off on iPhone SE.
Insert block. When the caret's block has text, Insert starts a new paragraph after that block, so it never splits a word. The slash menu is clamped above the dock (
toolbar/dock-clearance.ts).List icon.
ListIconis a new file in@chopin/icons, used by the dock and the bubble's block menu.Smaller fixes:
Final touches.
Dock v2: keyboard up (left), Insert's menu above the dock (middle), iPhone SE with Hide keyboard pinned (right).

🤖 Generated with Claude Code