Repository navigation
Let OpenDuet's sheets, swipe and swaps move instead of popping - #84
Merged
Merged
Conversation
Every always-open sheet (new task, new channel, a task, a question) was mounted open and closed by its parent unmounting it, so Base UI never played its entrance or its exit. useSheetPresence opens them a frame after mount and keeps them mounted through the exit before telling the parent. The hand-off swipe fades the card's content on the blur's ramp, so the dark Start pill no longer smears against the clipped edge; it is thrown at a least speed and fades as it reaches the edge of what shows it; and Up next closes a swiped card's place as soon as it lands. The drawer slides in from fully off-screen with no panel fade. Make's fan folds back into the + instead of vanishing. The tab capsule slides and the Tasks badge pops in with its number turning over. Start becomes Started, an approval becomes its decided line, and the composer's glyph swaps, each with a short blur. Menus, tooltips and dialogs move on the tokens, a bare transition utility now takes the tap duration and the strong ease-out, hover states that snapped transition, and the one transition-all is gone.
Collaborator
Author
|
Security review: no actionable findings. What was checked: the whole diff (53 files, all under
Limits: this was a read of the diff plus the existing unit tests, with no dynamic testing against a running server or auth stack. The recordings ran on a stubbed in-memory API. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
Most of what a person does in OpenDuet is answered by something arriving or leaving: a sheet, the side panel, the + fan, a card handed off, a word that changes. Too many of them did it on a single frame. The sheets were the worst case. New task, New channel, a task's page and a question's page were all mounted already open and closed by their parent unmounting them. Base UI plays a popup's entrance only when
openturns true after it has mounted, and its exit only while it is still mounted. Everytransitionanddata-starting-styleclass on those sheets was dead code. Each sheet appeared, and vanished, between one frame and the next.The hand-off swipe was the other one people see first. Its content blurred to 14px under a clip at the card's edge, so the black Start pill smeared into a hard-cut dark band down the side of the card. That was the "blurs badly near the edge" Jerel reported. A card let go near the edge then set off from rest over a fixed 280ms. Measured in headless Chrome, it covered the last 90px in that time, still blurred. On a wide screen it was sliced off by the edge of the phone-wide column, an edge nobody can see. Then its empty slot waited on the server before the list jumped up in one frame.
What the audit found, against main:
hiddenon the press, so the fold never playedease. Tooltips on Tailwind's defaultstransition-allapp/srcThe approach
Sheets.
hooks/use-sheet-presence.tsgives these sheets the lifecycle Base UI expects. The sheet opens on the frame after it mounts, so its entrance has a closed state to start from.close(then)runs the exit and calls the parent'sonCloseoronSavedonly once the exit has ended. Parents keep their contract of mounting a sheet to open it and dropping it when told. The cost is thatonClose,onAddedandonSavednow arrive one exit (about 250ms) after the press. A navigation after Create channel or Start now waits for the sheet to leave. An exit that never reports its end still closes after the longest exit.Tokens.
lib/motion.tsnow carries the four durations and both curves next to the CSS tokens.tests/motion-tokens.test.tsfails if the two disagree. Tailwind's--default-transition-durationand--default-transition-timing-functionnow point at the tap token and the ease-out curve. Every baretransition-colorsin the app learns them in one place. A class that names its own still wins.Swaps play only on a change. Each swap uses
AnimatePresence initial={false}. The brief row, the approval line, the composer glyph and the badge number all draw still on first paint and animate only when their state changes on screen. A decided approval is a new, folded card.InlineApprovaltells it whether it replaced a waiting card, so one read back from history never replays.Up next closes over a swiped card at once. The card has already flown off the screen, so its id is left out of the list while the request is in flight. If the request fails, the card comes back. A card answered by a button still waits for the server, as before. This is the one change that is more than timing. It removes the gap, and it keeps "the card comes back with the error visible".
Reduced motion keeps the fade and drops the movement throughout. The drawer and sheets cross-fade in place. The swipe has no blur, tilt or flight. Swaps fade without blur or rise. Menus lose their zoom and slide through one unlayered rule in
styles.css. Everything animates transform, opacity and, for the swaps, a 2px blur. Nothing animates layout.What is not covered
task-report.tsx's chevron keeps itsduration-200, to stay out of Make Start hand an email off, and the brief and Tasks say what is true #73's file.Verification
bun test app/tests: 2446 pass, 0 fail across 269 files. Main had 2431 across 266.bun run typecheck: every workspace exits 0.bunx biome lint --error-on-warnings app/src app/tests: 781 files, nothing reported.bunx biome format app/src app/tests: clean.New tests:
motion-tokens.test.ts: JS and CSS tokens agree, the Tailwind defaults point at them, and notransition-allis left.sheet-presence.test.tsx: the sheet opens a frame after mount, stays drawn through its exit, tells its parent once, and still closes when the exit never reports.motion-swaps.test.tsx: Start → Started and the approval's decided line animate only when the change happens on screen, and only fade under reduced motion.tasks-screen: a swiped card's place closes before the request returns, and the card comes back with the reason. Without the change this test fails.app-shell: one capsule, and the badge pops in.swipe-to-answer: the content fades, and the card fades by the end of its flight.make-screen: the fan stays drawn while it folds.Recordings were made through the app's own Vite config. A temporary page, deleted before commit, rendered the real
AppShell, Tasks, Make, the drawer, the sheets, the brief, an inline approval, chips, the composer, a dropdown and a page, over an in-memory API. Before is a cleanorigin/mainworktree on :3094; after is this branch on :3093. Both were run in headless Chrome at 390×844, and at 1280×800 for the swipe, drawer and fan. Paths are on Jerel's machine under/private/tmp/claude-501/-Users-jerel-cpk-OpenDuet/8d570fdc-9cf4-4d73-a2c3-5a0db6c25efb/scratchpad/recordings/:side-by-side/*.mp4: before on the left, after on the right. There are 16 clips:swipe-phone,swipe-desktop,drawer-phone,drawer-desktop,newtask-phone,newchannel-phone,tasksheet-phone,fan-phone,fan-desktop,tabs-phone,brief-phone,approval-phone,send-phone,chips-phone,menu-phoneandpageactions-phone.pairs/*.png: frame strips, before on the top row and after below. These areswipe-drag-edge,swipe-release-collapse,swipe-desktop-release,drawer-open,drawer-close,newtask-open,newtask-close,fan-open,fan-close,tabs-capsule,brief-started,approval-allowedandsend-to-stop.after-reduced/*.mp4: the same interactions with reduced motion on.Merge notes
This branch and #73 (
jerel/openduet-fix-brief-tasks) both touchmorning-brief-card.tsx. Here, only the imports andRowActionat the foot of the file change. #73 changesMorningBriefCardand addssectionsOfandbriefPieceCountabove it. A trial merge of #73's head (ef9dfb1) into this branch merges that file cleanly. Its only conflict isREADME.md, which is between #73 and main. This branch does not touchmorning-brief-tool.tsxortask-report.tsx.