Repository navigation
Dock phone formatting to the keyboard as one contextual bar - #498
Conversation
|
Review: Changes needed I built this PR and
One blocker: Must fix
Nice to have
Checked and fine: the toolbar role and name, |
|
Review: Looks good I re-checked 2e3c82a in a fresh worktree with the fake keyboard (port 8847). During every open and close of Insert and Aa I sampled the bar's position on every animation frame: on iPhone 15 Pro, iPhone SE and 15 Pro landscape, through choose → return, prose tap → return, and Escape. The jump is gone on iOS. I first replayed the keyboard the way iOS Safari reports it: about 250ms with no viewport events, then one resize. In that case the bar never leaves its resting position on any phone or path (15 Pro y=275, SE 260, landscape 95), across 28–53 frames per path. The panel lands exactly where the keyboard was (15 Pro 323/336, SE 308/260, landscape 143/200). After a choice the editor has focus again and the panel is gone. The other six points are fixed too:
CI: Not blocking
|
495d2bc to
f78d49c
Compare
f78d49c to
28b59d0
Compare
On a phone the formatting dock was a floating pill beside a floating Done button, with its last commands scrolled out of sight and Insert opening a small popover far from the thumb. It now runs edge to edge across the top of the keyboard, with Done at its trailing end, and follows what the writer is doing: a caret gets Insert, Text style, Undo and Redo; a selection gets Bold, Italic, Strikethrough, Inline code, Link and Comment. Hide keyboard stays pinned. Insert and Text style let the keyboard go on purpose and show their choices at its height, then give the caret back where it was, so the keyboard returns. Undo and Redo follow the shared history's stacks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Opening Insert or Text style put the panel into the bar while the keyboard was still up, so the bar grew up the screen and fell back once the keyboard went down. The panel now takes no space until the viewport reports the keyboard gone, mirroring the return path, and a tap in the prose holds the space until the keyboard is back instead of flashing the tab bar. Review follow-ups: Comment leads the selection row so the narrowest phone keeps it in reach; the commands keep an even gap from the start; undo and redo start disabled and follow a history that registers later; a table's actions sit above the bar; and Task list is offered only as a text style. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Browsers that report the keyboard's travel every frame, Android Chrome among them, showed the whole panel while the keyboard still covered part of it, so the bar overshot for a few frames on open and close. The panel now takes only the space the keyboard has uncovered, measured each frame, and gives its space back only once the keyboard is fully up. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- packages/icons/src/undo.tsx, packages/icons/src/text-style.tsx: new icons forward IconProps to LineIcon exactly like list.tsx; static SVG paths only, so they get the same reviewed props-forwarding exception. - packages/editor/src/plan-editor.tsx: only adds an optional onFormatPanel callback prop and passes it through to the toolbar options and memo deps; the reviewed className composition is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
28b59d0 to
16fb436
Compare
Follow-up to #477 and #484 from the Mobbin comparison (gaps 1–3): the phone editing toolbar now behaves like a native keyboard bar.
Verification against current main
I checked each gap on current
main(9459feb) with iPhone 15 Pro (390×844) and iPhone SE (375×667), using the fake keyboard frome2e/visual-viewport.ts. All three were still present.ListIcon, Hide keyboard and Comment on a selection, but the row is still B · I · H · List · Link · (Comment) · + regardless of selection. There is no Undo or Redo. On both phones the row overflows, so Insert (and Comment when shown) sits off-screen behind the fade./and opens the slash listbox above the dock: a 5-row popover at the top left, far from the thumb. Dismissing it leaves the/paragraph in the document (visible in the BEFORE shots).What changed
--phone-dock-height, 48px) so the caret stays above it. The commands sit at the start with an even 2px gap between 44px targets; Hide keyboard and Done stay at the end at any width, including landscape.packages/editor/src/toolbar/dock.tsx)UNDO_COMMAND/REDO_COMMAND. They are disabled when the Yjs history stack is empty, using a newlistenToHistory()inhistory.ts. A listener added before the history registers reports both disabled and picks the history up when it arrives.dock-panel.tsx) stand in for the keyboard at its measured height. The fallback height is the--phone-format-panel-heighttoken. Each panel is a 2-column grid of 48px tiles.UndoIconandRedoIcon(undo.tsx) andTextStyleIcon(text-style.tsx), all on the 18-grid in line style.Why the panel works this way
A web page cannot replace the iOS keyboard. Instead, tapping Insert or Aa moves focus from the editor to the panel, inside the tap. iOS lowers the keyboard and the panel appears at the height the keyboard covered, measured from
visualViewportjust before.While the keyboard moves, the panel is only as tall as the space the keyboard has uncovered, measured on every viewport change (
uncovered(panel, cover)). The bar therefore never leaves its resting position, whether the browser reports the keyboard once (iOS) or every frame (Android Chrome). On the way back the panel gives up its space only once the keyboard is fully up, or after 1s.The workspace keeps the dock up while the panel is open (
onFormatPanel→docked).Choosing a tile works like this:
The panel keeps its space until the keyboard is back (or 800ms) so the bar doesn't drop and jump. Tapping the same button again or pressing Escape returns to the keyboard unchanged. A tap in the prose takes the same return path, so there is no tab-bar flash while the keyboard comes back. Tapping elsewhere or Hide keyboard closes it.
A table's action sheet now places itself above the bar (
aboveDock), so Undo stays reachable straight after inserting a table.I chose this over a sheet floating above the dock with the keyboard still up because, on an SE, that would leave about 200px for both the sheet and the caret line.
Screenshots
BEFORE: iPhone 15 Pro. Caret, selection, Insert.

AFTER: iPhone 15 Pro. Caret, selection, Insert just tapped (keyboard still up: the bar stays put), Insert panel (keyboard lowered), Text style panel.

BEFORE: iPhone SE (375×667)

AFTER: iPhone SE (375×667)

Landscape, before (top) and after (bottom)

The grey block is the emulated keyboard area.
Testing
bun run e2e e2e/phone-read-mode.e2e.ts e2e/responsive-child-phone.e2e.ts e2e/toolbar.e2e.ts e2e/comment-sheet.e2e.ts: all passed after the review fixes. I ran these locally with a config that skipped the harness server, because another process held port 8797.phone-read-modecoverage:responsive-touch-fields,editinganddiscarded-mobile-geometry. The only failure wasediting.e2e.ts › Tab on a first list item, which pressesEndand fails locally on macOS. It's unrelated to this change; CI runs on Linux.bun testinpackages/editorpasses, including the newdock-panel.test.tsand alistenToHistorytest for late registration; so doespackages/icons.bun run typesandoxlintare clean.bun run cifails locally only on an ignored local design-jig file that is not in this branch, plus the design-contract items below.Needs a design-contract review
bun scripts/check-design-contract.tsreports these. I did not edit any hashes.packages/icons/src/undo.tsx(new)scripts/design-contract/exceptions/dynamic-packages.jsondynamic * props×2 (UndoIcon,RedoIcon><LineIcon>)d3011674899a2058c523a11a3846c8cb3dce65be5cf972c9cd75a2ef95ae088bpackages/icons/src/text-style.tsx(new)dynamic * props×1 (TextStyleIcon><LineIcon>)399f3d192bc61cae7904baf1d5d814ce711651efec54e691f73e85e72fe4cb5cpackages/editor/src/plan-editor.tsxec32a97839334779a7f7303aafd7248a5c15b5d4ce0d8f0070c14c08dfce7e8dThe icon files forward
IconPropsexactly asline.tsxandlist.tsxalready do.plan-editor.tsxonly threads anonFormatPanelcallback into the widget options; there is no new style or class logic.Needs a real device
🤖 Generated with Claude Code