Repository navigation
Give phones a documents sheet and action sheets - #478
Conversation
|
Review: Changes needed Checked the diff, built it, and drove it with headless Playwright on 8837 (iPhone 15 Pro, iPhone SE, 15 Pro landscape, 1440, 1280 and 1024 touch). It's a solid slice overall. Base UI drag, Escape and backdrop dismissal, the inert page, the Archived push, the account sheet, search, focus return to Documents, no header jump, and desktop/tablet are all unchanged and work. Two things need fixing first. Must fix
Should fix
Nice to have / design polish (judged as a demanding iOS designer)
Verified OKThe trigger focus-ref fix works: Escape and dismissal return focus to Documents (React 19 callback-ref cleanup is fine). Escape, backdrop tap and drag all dismiss. The search field is 16px, and filtering and the empty state work. Archived pushes with focus moving to Back, and Back returns focus to the Archived row. The account sheet opens Manage access on the tap, so no popup block. A row's ⋯ dims the documents sheet. Add project closes the sheet and the dialog isn't inert. Row Rename navigates and focuses (once #1 is fixed). Reduced motion swaps to fades. 1440 and 1280 keep the sidebar and menu popover; 1024 touch keeps "Show sidebar" with the drawer and popover. |
ce1d4e1 to
1afd26b
Compare
|
Review: Looks good Re-verified head Fixed and confirmed
One remaining should-fix (not blocking)
Nits
CI: |
1bae625 to
a96ebe6
Compare
9e8395c to
51b3f88
Compare
On a phone the top bar's Documents button opens a bottom sheet instead of the left Projects drawer: a project switcher, an inline search, recents derived from edit times, the document tree with 44px rows, Archived as a page pushed inside the sheet, the account row, and a pinned New document button. Base UI's drawer owns dragging, focus and dismissal, and the page beneath is inert. Document menus, in the top bar and on each row, render the same item list as an iOS-style action sheet on phones. Desktop and tablets are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Renaming forces the phone top bar's title visible. Recent lists the documents this person opened in the project, kept per user in local storage, without the one on screen, and only from two. The documents sheet is prefetched on phones, copying starts its clipboard write inside the tap, and the sidebar and sheet share one catalogue props object to recover initial-bundle bytes. Archive uses normal ink; action sheet groups are opaque; sheets cap their width in landscape. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…m Recent lazy() suspends a component's first render even when its module is already loaded, and React holds the reveal, so the first tap took about 300ms. The shell and document menus keep the module their prefetch resolves and render the sheet directly, choosing the component once per open so a late load never remounts it. Phones prefetch the action sheet on idle too. Recent is ordered by when you opened a document, so its cards no longer claim an edit time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-actions-menu.tsx and navigation-shell.tsx - apps/web/src/project-sidebar.tsx: documentGroups moved to the documents sheet model; the reviewed class and style sites are unchanged. - apps/web/src/document-actions-menu.tsx: phones open a lazily loaded action sheet instead of the menu; no new class or style expressions. - apps/web/src/navigation-shell.tsx: phones render the documents sheet in place of the drawer; the drawer keeps the same presence motion. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The phone top bar's rename field takes the base text step, so the touch field floor (now in the base layer, from #470) reaches it and iOS does not zoom on focus; the bar's inherited small step had overridden it. - pwa: phones now reach Documents from the top bar rather than a floating sidebar button, so the safe-area check follows that button. - document-activity: the Document tab's name no longer matches the top bar's Documents button. - documents-sheet: wait for the outgoing document's bar to leave after a route swap, and measure action-sheet rows once the entrance settles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
945f1f3 to
bea1294
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>
- 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>
Why
On a phone, documents lived in a 280px left drawer built for a mouse: 12px rows, a ⌘K search palette, a "Keyboard shortcuts" item, desktop
···popovers, no recents, Archived replacing the whole tree, and a drawer that ignored the top safe area (M-07, M-08, M-10, M-12). The page under it wasn't marked inert (M-11). The document⋯menu was a small desktop popover (M-13).Was stacked on #475, which has merged; this is now based on
main.What changed
Documents sheet (
documents-sheet.tsx). On a phone, the top bar's button (now a documents icon labelled "Documents") opens a bottom sheet at a ~92% detent instead of the drawer. It's built on Base UI'sDrawer, the same primitive as the comment sheet, which handles drag and flick to dismiss, Escape, focus trapping and scroll-vs-swipe. It has:owner/repo ⌄). Tapping it lists your projects with a checkmark and an "Add project" row.localStorage(wrapped in try/catch) when a document workspace opens, and the section only shows with at least two entries. The cards show titles only: they're ordered by when you opened them, so an edit time would contradict the order. It's hidden on landscape phones.--text-base, research children indented, decision counts, and a trailing 44px···on each row.Picking a document closes the sheet and navigates. The page beneath is
inertwhile the sheet is open, the scrim fades with drag progress, and the sheet's height respects the top safe area. Under reduced motion it fades instead of sliding. The data comes from the same catalogue as the sidebar (useProjectDocuments,documentGroups). The pure helpers (documentGroups,filterDocumentGroups,readRecentIds,rememberOpened,recentDocuments) live indocuments-sheet-model.ts, so they stay out of the initial bundle, and they have unit tests.Action sheet (
action-sheet.tsx). A small reusable iOS-style sheet: an opaque grouped list, 56px rows, the document title as its header, danger ink only for Delete (Archive has Undo, so it uses normal ink, as on desktop), a separate Cancel, safe-area padding, and a spring-like rise. On phones,DocumentActionsMenurenders the samedocumentMenuItems()list into it instead of the popover, so there are no duplicate item definitions. Copy link startsnavigator.clipboard.writeTextsynchronously inside the tap.copy-link.tsnow only reports the result, so iOS Safari keeps the gesture even when the module isn't cached yet. Other actions run once the sheet has gone, so Rename's inline field keeps focus. When a row's action sheet opens over the documents sheet, the documents sheet dims.Rename (M-13). Tapping the top-bar title does nothing. Rename lives in the action sheet and edits inline in the top bar at 16px. The title is forced visible while renaming (
:has(input)overrides Give phones a bottom tab bar and a collapsing top bar #475's scroll-hidden title). The e2e asserts its computed opacity.No dead first tap. On phones the top bar prefetches the documents-sheet and action-sheet chunks when idle and on
pointerdownof Documents. Row and top-bar⋯buttons prefetch the action sheet andcopy-linkonpointerdown.lazy()suspends a component's first render even when its module is already loaded, and React's Suspense throttle then holds the reveal. To avoid that, the shell and the document menu keep the module the prefetch resolves and render the sheet directly. The component is chosen once per open, so a load that finishes late never remounts an open sheet;lazy()is only the fallback for a tap before the prefetch lands. Measured from tap to sheet in the DOM (headless iPhone 15 Pro, localhost, 3 runs): documents sheet 313ms → 25–29ms, action sheet 306ms → 10ms.Polish.
DocumentsIcon(a page with another behind it) for the trigger, while the project row keeps the book glyph:hoverNo header jump (M-09). The trigger stays in the bar while the sheet is open, so the bar's box doesn't move (asserted in the e2e).
Trigger focus fix. The phone bar and the shell shared one
RefObjectfor the sidebar trigger, so when the shell's own trigger unmounted it nulled the ref the bar had just set. Both now share a guarded callback ref, so focus returns to the Documents button after a dismissal.New tokens:
--color-scrim,--radius-sheet,--sheet-large-detent,--sheet-gap,--sheet-max-width,--action-sheet-row-height,--sheet-rise-dur,--sheet-fall-dur,--sheet-page-dur.@base-ui/reactis now a direct dependency ofapps/web, at the same version as the editor's. All phone-only code is lazy. The shell only passes the props object it already shares with the Projects sidebar, and the drawer and sheet share one lazy boundary. Initial JS is 255,063 B raw against a 256,000 B budget: +677 B over Give phones a bottom tab bar and a collapsing top bar #475 (254,386), leaving 937 B of headroom.Desktop and tablets are unchanged. They still get the inline sidebar or the drawer and popover menus.
Screenshots
BEFORE is
mainon 8811 (critique shots). AFTER is this branch, emulating an iPhone 15 Pro.Drawer → documents sheet

⌘K palette → inline search

Archived replaced the tree → Archived page inside the sheet

Top-bar ⋯ popover → action sheet

Row ··· popover → action sheet over the dimmed documents sheet

Rename: inline in the top bar at 16px, opened from the action sheet

Project switcher

iPhone SE 375×667, and landscape 844×390 (Recent hidden)


Desktop 1440×900 still uses the sidebar and popover

Testing
bun test apps/web: 634 pass, 0 fail. Newdocuments-sheet-model.test.tscovers filtering, recently opened storage (per user and project, broken storage) and recents.bun run types: pass.bun run ci: dprint, oxlint, tokens and the design-policy rules pass. The design contract fails only on the hash pins listed below.check-design-recordanddesign:checkpass.e2e/documents-sheet.e2e.ts:menu, and a 900px window keeps the Projects drawerphone-workspace.e2e.tsandresponsive-workspace.e2e.ts, because the phone trigger is now "Documents" and opens the sheet. The two drawer-exit tests now run without touch at 390px, so drawer coverage is kept.Needs a real device: drag/flick feel and rubber-band; scroll vs. swipe inside the list; the iOS keyboard with the search field; safe areas on a notched phone in both orientations; Copy link's clipboard gesture in Safari.
Design-contract review
Reviewed and renewed in
Renew design-contract review hashes for project-sidebar.tsx, document-actions-menu.tsx and navigation-shell.tsx(dynamic-web.json):apps/web/src/project-sidebar.tsx:documentGroupsmoved to the documents sheet model. The reviewed class and style sites are unchanged.apps/web/src/document-actions-menu.tsx: phones open a lazily loaded action sheet instead of the menu. No new class or style expressions.apps/web/src/navigation-shell.tsx: phones render the documents sheet in place of the drawer, which keeps the same presence motion.Rebase notes
#475 changed
phone-header.tsxon its way in: the root's title and actions stay while a child is open, and the child's "Return to" button is gone, because the stack bar handles the way back. This PR's Documents button andDocumentsIconsit on top of that.🤖 Generated with Claude Code