Repository navigation
Feel: motion measured against the spec, one set of motion tokens, reduced motion (feel lane: FL, AP-17) - #25
Conversation
…-29) The shared sampler (tests/e2e/frames.ts) records chosen elements every frame; feel-rows, feel-sidebar, feel-spaces, feel-commandbar and feel-motion turn feel.md's P0 observables into tests. Several fail on purpose: they measure gaps the next commits fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Medians of three runs of the feel specs at 4a193cd (load 11-14). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sampler read the DOM in its own requestAnimationFrame callback, so whatever ran in a later rAF callback that frame (motion's frame loop, rAF-batched updates) showed one frame late. It now reads in a ResizeObserver callback, which runs after all rAF callbacks and layout and just before paint. The baseline in docs/FEEL.md is re-measured with it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… FL-28, AP-17) FL-01: the springs (response / damping ratio), curves, durations and the 0.7x exit factor live in src/shared/motion.ts, which main uses too; the renderer imports them from renderer/src/motion, whose motion.css has them as --motion-* / --ease-* variables. The sidebar's motion-tokens.ts, springFrom in spaces/physics.ts and split/spring.ts now come from it, and hard-coded curves and durations across the renderer use the tokens. Folders and the pinned section open on the snappy spring and close on its exit spring, so a click mid-way turns them round from where they are. FL-28 / AP-17: reduced motion follows the OS (prefers-reduced-motion in renderers; Electron's per-platform animation setting in main) and settings.reduceMotion forces it on (default off, normalised, a command and a Settings > Appearance switch). Every view keys its reduced rules off a reduce-motion class, so the setting reaches CSS, motion's components (OrbitMotionConfig) and hand-run animations alike. Per surface: the Space switch cross-fades in 120 ms, drags and swipes settle with a 120 ms ease-out, rows fade instead of sliding, the command bar fades without scaling, and the rest as FL-28 lists. Command bar (FL-16): 150 ms entrance on the quick curve that starts a frame in, 105 ms exit. Spaces (FL-13, FL-29): the settle spring is 0.30 s / 0.90 and the strip comes to rest within half a pixel, so both settle inside 350 ms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While the card slid away its view still covered the window's left edge, so the shell's edge zone never heard a pointer coming back and the card slid all the way out first. The card now listens for the edge itself while it leaves. Its slide is a motion value: from rest it keeps FL-03's timing curves (in on snappy, out on quick in 160 ms), and turned round mid-way it takes the snappy spring from the card's position and speed. The slide starts in a layout effect, before the frame is drawn. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…osed row takes its gap (FL-04, FL-05, FL-09) - FL-05: the pressed row shows as selected in the frame of the press, ahead of main's answer, and any hover fade on it stops so the highlight is not held back. - PR #10 review F1: a row stays drawn (content-visibility: visible) from the moment it grows in until it first leaves the screen, and a press on its slot goes to the row, so a press just after the growth ends selects it. - FL-04: a hover fade that is turned round starts from the colour it shows rather than from full, and starts in the frame it is made. - FL-09: a single close collapses at once, with no stagger delay, and the 1 px list gap goes with the row as it collapses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-09, FL-29) Selecting a sleeping tab (⌘W onto it, a click) woke its page inside the state change's listeners, which held the change back from the sidebar for 12-15 ms. The page now wakes one task later, so the sidebar draws the new selection in the next frame. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ghost sat in the Space strip, whose track is a containing block for fixed elements, so it was drawn lower than the pointer by the strip's offset (82 px) and would slide with the Space pages. It is now drawn in the shell's root. Escape now brings it back into its slot in 270 ms (was 355-370 ms, measured until the ghost was removed). The FL-10 test checks the row is held where it was pressed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Long lists kept every row drawn, each in a clipped slot, so layout, paint, hit testing and layerization grew with the number of tabs. A row window per sidebar scroller now draws the rows within 1.5 screens of the visible band; the others keep a plain slot of a row's height and draw nothing, so drag and drop, keys and scrolling a row into view still see every row. Rows coming on screen are drawn before the frame that shows them (synchronously from a scroll or resize); the rest of the margin renders in the background, and rows left behind are dropped in a task after the frame. A Space shown for the first time draws the rows its scroll position shows in its first frame. Tests: 300 tabs scrolled by wheel, keys, a tab found elsewhere, a jump and a drag-scroll show no blank row in any frame (measured just before paint, with a self-check that the watcher does see a frame drawn blank); the 200-tab fling and the 500-tab + 50-folder scroll budgets; the first showing of a 200-tab Space. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…that draws ⌘S already moves (FL-02, FL-29) A CSS transition's first frame shows where it starts, so the sidebar column and the card's margins only moved in the frame after the one that drew ⌘S. They now start with a delay of minus one 60 Hz frame (--motion-lead, ONE_FRAME_S): whenever a loaded machine draws ⌘S, that frame already moves. The split engine follows the column's slide from its transitionrun event, which now comes after the first frame has moved the panes' frame. It catches the slide when it measures (catchSlide), from the frame as it was, so SB-11's panes keep pace from the first frame. Tests: feel-sidebar FL-02 checks that the first frame drawn hidden already moves the edge; the motion-token test checks the CSS value against ONE_FRAME_S. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…D51) Timing budgets that one sample judged now take the best of up to three rounds, as the command bar's CB-22 test already did (D46): other work on this shared machine can only add delay. Each round is logged, so a slow machine shows in the output rather than in a failure. - archive: the Library's first open, and its open and search at 10,000 entries; media: the floating player's open, and with reduced motion its open, glide and return; Peek's open and close; Settings' appearance switch; SV-13's long tasks while dragging a divider. - CB-22's first open after a launch is judged on its own, and the rest by their upper median (PR #17 review F2); the typing test judges the median keystroke, leaving the worst to the budget tests. - SV-26's growth also needs its settle within budget for a round to count. - SV-17 waits until the shell has drawn the close before comparing views and panes; SV-13's mid-drag wait says, when it stalls, whether the pane card stopped too and whether the divider held the pointer. - The archive search unit test times each search in thread CPU time (D51). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… task of its own (PR #19 review F7) - The test hook's flush() writes the state file from a task of its own (later), as the other hooks do, so a flush asked through the inspector no longer runs part-way through other main-process code. flushNow() writes in the calling task, for an evaluate that checks the state first and must write in the same turn (downloads' saveStateWhen). - scripts/perf/run.mjs and scripts/capture/record.mjs quit Orbit as the e2e harness does (askQuit, PR #16): app.quit() from a task of its own, then Playwright's close only cleans up. The capture script's setBounds runs from a task of its own too. - run.mjs takes --cold-only, Orbit's cold start alone, for the loading view's cold-start rounds in PERF.md (PR #18 fix check 3, F5). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hat is no expression (PR #19 review F6) A getter, a computed method name or a bound function turned into broken source. The converted source is now checked with new Function too, and when neither parses, functionSource throws Playwright's "Passed function is not well-serializable!" before anything runs in main. core.spec's TB-22 harness test checks all three. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A scale noted in × read 0.94 as 0.9; ratios with no unit or × now keep three places. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s re-flowing; out of sight it leaves in place ⌘D, ⌘Z and Move to… on a row whose new place is in sight: the row is drawn whole from where it was and moves with the standard spring while the room it leaves and the room it takes close and open with that same spring, so the rows between re-flow and the rows below both places stay put. Before, its new room opened in one frame (the divider and rows above jumped a whole row) and the room it left closed on the snappy spring, so the rows below dipped by up to 31 px. When the new place is scrolled out of sight or in another Space, the row collapses out in place with the close motion and arrives there at once; the list's scroll anchoring keeps the rows in sight still. Auto-reveal's settle no longer scrolls after a row that has since moved to another section (it scrolled the list 420 px up to the pinned section). Tests: the travel (no frame with the row gone or drawn twice, no jump, rows below stay) and ⌘Z back; the out-of-sight and other-Space moves (no scroll, no fly-through). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ph back The ×, the audio button and a folder's search button: on hover the button's own circle fades in with the instant timing (it switched on in one frame); on press the glyph alone is drawn at 0.9 at once and springs back with the pop curve on release (the whole button snapped to 0.9 and back). The click still fires on release; nothing waits for the glyph. The reset's circle fades in the same way (it acts on press, PN-04). With reduced motion the press shows with no spring. Test: the circle fades in (frames part-way, about 90 ms), the press shows in the next frame, and the row starts leaving within two frames of the release (main closes the tab in between), best of three. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…0 ms; ⌘Z brings them back together An audit of the clear with the sampler: 8 Today rows are gone in about 430 ms, each starting 20–30 ms after the one above and sliding left about 22 px; the divider and New Tab stay; ⌘Z grows them back in one group with the insertion motion. It already met the spec; no change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… its rows in one after another While the box grows with the snappy spring, its rows fade in 12 ms apart (10–15 ms; from the thirteenth on they come together); before, they all faded in at once. Only a box the user opens staggers, not one drawn open with the sidebar; with reduced motion the box only fades. The folder's children and the pinned section share the box (RevealBox). Test: opening a folder of 6, the chevron turns, the content below makes room within 250 ms with no jump while the children fade in one after another; closing is quicker. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, where it was The grid's AnimatePresence (popLayout) needs each tile's element to take a leaving tile out of the flow at its size; the tiles took no ref, so a removed tile followed the grid's re-flow and was drawn growing wider instead of shrinking (smallest scale drawn 0.99). FavoriteTile and SplitFavoriteTile now pass the ref to their element (React 19: ref is a prop); the smallest scale drawn is 0.85. Test: a new Favorite pops in from about 0.85 with the pop spring while the other tiles re-flow with no jump; a removed one shrinks and fades. Waits poll for the tiles to stand still instead of sleeping. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ones up and fade out after 3 s In, a toast rises about 12 px, fades in and grows from 0.96 with the pop spring (it rose 6 px from 0.98 on a CSS keyframe); a new one pushes the older ones up with the snappy spring (they jumped). Out, it fades and drops about 8 px over about 160 ms (it vanished). A plain toast stays 3 s (it stayed 2.2 s), one with a button 4 s; a hovered one waits and leaves about 1 s after the pointer leaves it. At most three stack, 8 px apart. Plain toasts stay pointer-transparent, so only one with a button holds on hover. With reduced motion the moves drop and the fades stay. Test: three toasts, each rising in with the pop spring and pushing the older ones up, 8 px apart; each leaves 3–4 s after it came, fading and dropping. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… quick fade A folder's search and the icon picker: the open starts a frame along (POPOVER.in, -ONE_FRAME_S), so the first frame after the click already draws them (it drew nothing until the second, the fade starting at 0); they close with the quick timing, fading back to 0.97 (they were cut in one frame). Keys go back to the sidebar as the close starts, and a closing popover takes no clicks. Test: click → drawn in the next frame from about 0.97, open in about 150 ms; Escape → a fade of a few frames, gone in about 90–130 ms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every icon button already pressed to 0.94 at once; on release it snapped back in one frame. It now springs back with the pop curve (about 240 ms); the press stays immediate (60 ms) and a disabled one stays still. With reduced motion the press shows with no spring either way. Test: press → 0.94 in the next frame, release → settled at full size with the pop timing, and a disabled button does not change size. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
feel.md FL-18: while a tab is dragged over the page, the highlight "moves between halves instantly" (SV-26 defers its timings to feel.md). It glided there with the snappy spring and was in its new place 325 ms after the move (load 5.6); now it is there in the frame after the move (2 frames, 25 ms). Its entrance and the panes making room are unchanged. feel-split.spec.ts measures the rest of FL-18 with the sampler: the zone fades in over 100 ms and shows the half; on release, the add, a flip and a close, the panes are within 2 px of their place in 270-300 ms and within 0.5 px by about 450 ms (FL-01's standard spring, 0.32 s / 0.88, overshoots by 0.3 % of the travel, 1.4 px over half a page card, before it comes within 0.5 px); the divider follows the pointer in every frame. The drag test keeps the X server's own pointer over the sidebar: it gets a move with no button down whenever the view under it changes, and that ends a tab drag in the test (a real drag holds the button). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
feel-window.spec.ts measures, frame by frame in the views that draw them and against main's clock: - FL-23: a slow page's pill is drawn 141 ms after the load starts (main shows its view at 150 ms), sweeps on a 1.2 s turn at 60 fps, and is gone 179 ms after the load ends after a 9-frame fade, the sweep filling the pill first; a cached reload (41 ms) never shows it. - FL-27: blur dims the sidebar over 117 ms with 7 frames in between; focus brings it back in 10 ms with no fade. A window resized a step every frame keeps the card's margin in every frame, main gives the page view each width 7 ms (worst 16 ms) after the card draws it, and the card under any gap is the page's own colour. All within the spec's numbers: no product change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
X hands the window 8 to 10 of the 16 widths at a load of 3 to 4 (three runs), so "more than 10" failed every run while the checks that matter (the card's margin, the page colour, the page view following) passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the layer leaves once the card can't be seen Re-audit of FL-19 with the shared sampler (tests/e2e/feel-peek.spec.ts): open, dim, close and expand against feel.md's numbers, and the observable: after Escape, the page behind takes clicks within 200 ms. Measured at 1920500 it sat at the edge (187-201 ms): the close's tween drew its start state again in its first frame, and main kept the overlay up through the curve's last tenth, which is under 0.1 %. - The spring and the tween start their clocks when they are started, so the first frame after a phase change already moves. - The close tells main it has settled once the card and the dim are under 0.1 %; main then removes the layer as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…move that finds it Seven runs gave 2 to 4 frames, 3 in five: a fade from 0 shows from its second frame, and React renders a pointer move's state in a task of its own, which can miss the next frame. The bound still catches a zone that eases in late; docs/FEEL.md keeps the 3 frames against the 2 targeted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Playwright's default poll steps (100, 250, 500 ms) only told whether the card had gone before 350 or before 850 ms, so the leave read 885–896 ms. Polled every 16 ms it reads 625–632 ms: the 400 ms grace and the 200 ms exit, within FL-03's 300–500 ms plus a quick slide. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gap, the FL-30 checklist docs/FEEL.md: how the numbers are taken; medians of three runs for each row, before (main plus the specs for P0, the lane after P0 for P1 and FL-19) and after; gaps marked, with why each after-gap stays; what is not measured or not built (FL-24, FL-26, FL-31, FL-18's bubble and wobble); FL-30's nine points with a verdict and the specs that show it. Feature status: FL-21 partial (frames of two intervals under load), FL-29's note (the Space switch now settles in ~290 ms; what still misses), FL-19's open and close timings, FL-27's resize worst case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SidebarItems.tsx: both imports kept (the import lane's sidebar helpers and the feel lane's motion config). The import lane's reveal finds a row by its slot, and slots stay for every row in the windowed list (FL-21), so it reaches rows not drawn yet. FEATURES.md regenerated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… that shows the input handled (review M4) interruption() compared raw steps, so a late frame (one step over two intervals) read as a jump, and it started at main's wall-clock time for the input, when the overlay hadn't handled Escape yet: the 'after' steps were the entrance still accelerating. It failed 2 in 11 runs alone at load 7. Steps are now speeds (each over its own frame time, per 60 Hz frame), and each test starts 'after' at the first frame that shows the input handled: the closing bar, the Space dot of the new target, the folder closed again, the hover sidebar's turn. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ms ease-out, or at once from more than two screens away (review M5) FL-05 asks for a 200-250 ms ease-out scroll, instant past two screens. Chromium's smooth scroll eased in and out (2 % of the way in its first frame) and took 417-467 ms for a row a screen or three away. The reveal now runs its own frame loop on the instant token's curve (CSS ease-out) for 225 ms, starting one frame along so the highlight's frame moves, and jumps when the row is more than two screens away or with reduced motion. Each step draws the rows it brings on screen before the frame. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… in up to three rounds (review M4) The bar fades in fast, so comparing speeds alone let a jump to the end through. The exit now has to fade over at least 3 frames from the frame that shows Escape handled. A late frame under load can show a whole exit in two, so the test takes up to three rounds (D46). With the exit eased as a step, 4 runs of 4 failed and all 12 rounds faded over 0 to 2 frames. With the real exit, 20 runs of 20 passed in their first round, fading over 3 to 6 frames (median 5) at load 3.9 to 8.4. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts, with no frame of its move (review M1) Under OrbitMotionConfig, motion jumps a reduced move to its end in a frame of its own. So a toast could still show 12.6 px low at 0.96 in its first frame, and it dropped 8 px as it started to leave. Under reduced motion the toast now starts and ends where it rests: only its opacity changes, and the push has no layout animation. It no longer needs the config to reduce, so the guard's self-test takes out both the wrap and the read. feel-reduced's toast tests now follow the first toast out as well. Before this change they failed 10 of 10 (5 per setting): the toast dropped 8 px as it left in every run, and 2 runs also drew it 12.6 px low in its first frame. The earlier test, which stopped before the leave, failed 3 of 10 under the system setting on that first frame. With it, 40 of 40 passed (10 per setting, twice): 0 px rise, 0 px drop, a 117 ms fade in, a fade out over 7-8 frames, the push in 1 frame. FL-22's own test passed 3 of 3 with its numbers unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… in rounds of five closes (review M2) The FL-09 test only noted the frames from ⌘W to the selection. A new test closes five tabs a round, each with a sleeping tab below it, and asserts the round's median is 1 frame (FRAME_SLACK: 2), in up to three rounds (D46). Every close must also select the next row no later than the closed one starts to leave. It shows windows.ts's deferral is needed: main wakes a sleeping tab's page only once the change has gone out to the shell. Waking it first takes main about 13 ms and holds the patch back a frame. - With the deferral: 5 runs of 5 passed in their first round at load 4.5-6.2 (22 of 25 closes in 1 frame, 8-11 ms), and 10 of 10 with 28 busy threads beside them at load 16-32 (7 in round 1, 2 in round 2, 1 in round 3). - With lines 1317-1329 taken out: 5 runs of 5 failed (Expected <= 1, Received 2). All 15 rounds had a median of 2 frames, 24-27 ms after the key, and 71 of 75 closes took 2 frames. The first close after a launch still takes 2 frames in 10 runs of 15: main sends that change 7-12 ms after the key, against 2-3.5 ms later on, as the key's route, the close and the state update run cold. FEEL.md lists it as a gap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ettle at 0.1 % comes out (review follow-up 3) The close told main it was done once the card and the dim were under 0.1 %, so the page behind took clicks sooner (FL-19: within 200 ms of Escape). No test fails without it: FL-19's close test passed 6 of 6 at load 15-19 without it, the layer gone 173-194 ms after Escape (best of up to three rounds) and the fade 133-150 ms. It comes out; the spring's and the tween's clocks still start at the call. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… has stopped drawing Since 8443086 the spring's clock starts at the call, so where its frames fall on the spring-back varies from run to run. The test read the centres as soon as main's whole-pixel bounds were within 1 px of rest, which can be mid-settle, 1.1-1.4 px away: unchanged, it failed 4 of 8 and then 1 of 8 on the lane's build. It passed on main only because main's frames always stopped the poll at 0.99 px. It now also waits until no frame has come for 200 ms: 16 of 16 pass on the lane's build, 6 of 6 on main's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…frame, not rounds (review follow-up 8) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… (review follow-up 9) One build runs both sides of the A/B: ORBIT_LOADING_AT=ready makes the loading indicator's view at ready-to-show (A, as Orbit does), anything else once the shell has drawn its first frame (B). It applies to this head with `git apply`; the header says how to run the rounds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…17 entry (review M1-M5, follow-ups 3 and 5) - FEEL.md: the rows the round touched, measured again on the lane: FL-01 as speeds, FL-05's scroll, FL-09 (the median of five closes in 1 frame; the first close after a launch is a gap at 2 frames), FL-19's close without the early settle, FL-21's place and list size, and the toasts under reduced motion. FL-30's points 2, 3 and 8 follow. - D70: FL-08's exit starts once main has closed the tab; FL-19's open uses a spring tuned to its 280-320 ms (the comment in peek-geometry.ts says so); FL-13's rubber band keeps its own spring, as FL-29's 350 ms is for swipes that land on a Space. - feature-status.json: AP-17 (partial: no shimmer at start), and the notes for FL-01, 05, 08, 09, 13, 19, 21, 22, 28 and 29. FEATURES.md regenerated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: 0e29b59f-baad-42ba-a966-fc8b9645e39a) |
… (D46) The typing test judged its worst keystroke from a single round, so one slow frame on a busy machine failed gate r4 (it passed 5 of 5 alone). Each round now types the query into a fresh bar and closes it; the echo and local-result budgets take the best of up to three rounds, and the checks that must always hold (no late highlight move, suggestions shown, no height bounce, every key seen) are asserted in every round. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: 46d36efb-ffef-4354-857d-52bd505a5d0b) |
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: f0cd3baf-a3fd-4e2d-9097-b02fe23e2677) |
…already on screen (FL-28, fix check N1) motion reads OrbitMotionConfig once, as an element mounts, so a Favorites tile, a folder or the pinned section that was on screen when the setting turned on kept gliding, shrinking and turning until it mounted again. Each long-lived motion element now asks useReducedMotion() and chooses its reduced motion: no layout animation, no scale on the way in or out, moves and turns at once, 120 ms fades, and no press nudge (FL-28: decorative motion is off). - Favorites grid, its tiles and the split tile; the folder and pinned box; the folder chevron and the pinned caret; the "+"; the drag ghost; the icon picker, folder search and downloads flyout. - AT_ONCE and REDUCED_FADE join the motion tokens. - feel-reduced turns the setting on after these show: the Favorites under Orbit's switch and the system setting, a folder and the pinned section, and the presses. All four fail without the fix. - The unit guard now requires useReducedMotion() of every element with layout, an entrance, an exit, a gesture or a moving animate (rows whose variants ask as they run excepted), and Orbit's hook rather than motion's. On the code before the fix it names 11 components in 9 files. - The rotation() helper moves from feel-motion into frames.ts for both specs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: abad473a-97b5-4c03-84ac-d7e4749d9b2f) |
…, fix check N2) The drop-zone test judged its budgets on a single drag, so one slow frame failed it in 2 of 7 runs (133.4 ms against 133.33 ms; 5 frames against 4). Each round now drags a fresh tab onto a fresh page through the right half, the bottom band and back, and drops it. The timing budgets take the best of up to three rounds; the checks that must always hold (the zone fades in, shows the half, moves to the bottom half, and both tabs end as panes) are asserted in every round. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…median (fix check N3) The test judged a round of five closes on their median alone, so a change that slowed two closes in five would pass. A round now also needs every close within one frame more than the median's budget (2 frames on Linux), which keeps the known first-close-after-launch gap and catches the rest. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…350 + 90 ms (D70, fix check N4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: be22b64b-7b9b-4ff4-8c9e-3f26045f3a96) |
Gate r6 failed FL-11 once: Today emptied in 452.1 ms against its 450 ms budget at load 5.9, and the test passed 3 of 3 alone. It was judged on one round. Each round now clears Today with ⌘⇧K and brings the same eight rows back with ⌘Z. The budgets (Today empty within 450 ms, the rows growing back in) are judged on the best of up to three rounds. The order, the stagger, the slide, the divider staying put and the rows coming back in one group are checked in every round. With all three rounds forced: 426, 422 and 433 ms, every check holding in each round. Also: FEEL.md's FL-28 note says the guard named 11 components in 8 files on the code before N1's fix, not 9 (the builder's count). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: 7a6f3076-83ff-470e-b167-33b80bb118f2) |
…ft is gone Gate r7 failed "[FL-07][PN-02] a row pinned with Cmd+D travels to the pinned section": right after the row stopped travelling, its unscoped locator found two rows, the pinned one and the copy left in Today. That copy is never drawn (visibility: hidden) and goes once its own spring settles, which can be a frame after the row lands. The test failed 1 of 3 alone at load 2. It now waits for the Today copy to go, which it must, and reads the row in the pinned list: 6 of 6. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: 9ddb74cc-4b9b-480a-81f7-9d7aadbef7e0) |
The feel lane measures every motion in
spec/feel.mdagainst its number and observable, puts all motion on one set of tokens, and fixes the gaps it measured. It's built fromlanes/feel.md, withmainmerged in twice: import (fbb9103), then keys and PR #24 (592096b). After its review came two fix rounds (to28ba347, then9fc8bff) and four test commits from the lead (3a5f9ed,2a7391e,9bef710and1fdee1e).docs/FEEL.mdholds the measured-against-target table, before and after (medians of three runs, with the load average). It also has a note on every gap that's left, and FL-30's checklist.What it builds
tests/e2e/frames.ts). Once per rendered frame, it records the boxes, opacity and chosen styles of the elements under test, and the frame times. Each P0 observable infeel.mdis now a test on it, in 10feel-*.spec.tsfiles.src/shared/motion.tsandsrc/renderer/src/motion/.settings.reduceMotion, off by default, with a "Toggle Reduce Motion" command and a switch in Settings → Appearance.motion/reactuse or a style rule ignores Orbit's own switch.archive-storeunit bench is timed in thread CPU time (D51).scripts/perf.main;Changes to what a feature does
Each came after a feel test failed on the old build:
sheet.cssandimport.cssfollow Orbit's switch, not only the OS setting.Decisions where the spec was unclear
feel.md.feel.mdconflicts with itself:Fix round 1 (after the review)
Each must-fix has a test that fails without its fix (D18):
1af4432,e60e252feel-reduced's toast tests (Orbit's switch and the OS setting); unit guardreduced-motion-guard.test.tssheet.cssandimport.cssdd89018feel-rows[FL-09][FL-29]: rounds of 5 closes, the median against the budgetf2bddfdfeel-sidebar: 200 tabs, the 151st selecteda4adf9b,b2410f7feel-motion's interruption tests compare speeds; the bar's exit fades over at least 3 framesd74dd06feel-sidebarFL-05 scroll: near, one screen away, past two screens, reduced motion567c6bd), 5 (AP-17's entry), 6 (the unit guard), 8 (4b69a76), 9 (c917900, the cold-start patch). D70's notes are in FEEL.md, the notes and a code comment.tests/e2e/peek.spec.ts(689221d): PK-06's swipe test waits until no frame has come for 200 ms before it reads the card's last position. Since the springs' clocks start when they're called, it could read the card mid-settle.3a5f9edruns FL-17's typing test in up to three rounds (D46). The echo and result budgets are judged on the best round, and the checks that must always hold on every round. It failed gate r4 on a single slow frame, and passed 5 of 5 alone.2a7391eupdates the comment incommandbar.spec.tsthat says so.Fix round 2 (after the fix check)
The fix check confirmed the five earlier fixes and found two more must-fixes. Each has a test that fails without its fix (D18):
ef0eafffeel-reducedtests turn it on after mount (Orbit's switch and the OS setting); unit guardreduced-motion-guard.test.tscda6ec6a9b0ad2): every close in FL-09's rounds takes at most 2 frames, beside the median. N4 (9fc8bff): the rubber band's log line names FL-13's own spring (D70).9bef710: gate r6 failed FL-11 once (Today emptied in 452.1 ms against 450 at load 5.9; it passed 3 of 3 alone). FL-11 now takes D46's rounds. With all three forced, the rounds took 426, 422 and 433 ms, and every always-hold check held in each round. The same commit corrects FEEL.md's file count.1fdee1e: gate r7 failed FL-07's pin test. Right after the row landed in the pinned section, the test's locator also found the copy left in Today, which is never drawn but stays until its room closes. It failed 1 of 3 alone at load 2. It now waits for that copy to go, and reads the row in the pinned list: 6 of 6.Merge gate (D42), run locally on Linux on
1fdee1enpm run build,npm run typecheck,npm run lintand./node_modules/.bin/vitest runall exit 0. Unit: 865 passed of 865, in 86 files.xvfb-run -a -s "-screen 0 1440x900x24" ./node_modules/.bin/playwright test --workers 1 --retries 0ran 648 tests in 33.3 min: 644 passed, 4 skipped, 0 failed, and it exited 0.1fdee1eitself, withmain(592096b) in it. The load average was 3–9, with other lanes' test runs beside it.test.fail()) counts as passed.7b6179c(before keys and PR test: one temp folder per test run, removed when the run ends #24): 589 passed, 0 failed.0f9fbfb(9 of 9 alone).0f9fbfb(the reviewed head): 635 passed, 4 skipped, 0 failed, at load 2–5.28ba347(fix round 1): 639 passed, 1 failed, 4 skipped. The failure was FL-17's echo, judged on a single round, which passed 5 of 5 alone;3a5f9edfixes that.2a7391e: 640 passed, 4 skipped, 0 failed, at load 7–12; unit 863/863.9fc8bff(fix round 2): 643 passed, 1 failed, 4 skipped, at load 4–6; unit 865/865. The failure was FL-11's sweep, judged on a single round (452.1 ms against 450), which passed 3 of 3 alone;9bef710fixes that.9bef710: 642 passed, 2 failed, 4 skipped, at load 3–6; unit 865/865.split.spec.ts:493) is a known flake, and passed 3 of 3 alone.1fdee1efixes.Review
reviews/lane-feel.md, on0f9fbfb): REQUEST CHANGES, 5 must-fix and 12 follow-ups.reviews/lane-feel-fixcheck.md, on2a7391e): APPROVE WITH FIXES, 2 must-fix. It confirmed all five earlier fixes. N1 (reduced motion turned on while Orbit runs) and N2 (FL-18 on one round) are fixed in fix round 2.reviews/lane-feel-fixcheck2.md, on9bef710): APPROVE, no must-fix.1fdee1ecame after this check. It changes only one test's locator.🤖 Generated with Claude Code
Note
Medium Risk
Wide renderer and main-process timing changes (tab switch, overlays, PiP) affect core UX; mitigated by extensive feel e2e coverage and mostly visual behavior.
Overview
This PR lands the feel lane: motion is measured against
spec/feel.md, documented in newdocs/FEEL.md(before/after table, gaps, FL-30 checklist), and reflected inFEATURES.md/feature-status.json(many FL-* rows marked built or partial).Shared motion (FL-01) centralizes springs and curves in
src/shared/motion.tsandmotion.css, with rendererOrbitMotionConfigand sidebar-local tokens removed in favor ofsrc/renderer/src/motion/.Reduced motion (FL-28, AP-17) adds
settings.reduceMotion, mainregisterMotion/reducedMotion(ctx)(Mini Window, PiP), rendererinstallReducedMotion+useReducedMotion(), and replaces@media (prefers-reduced-motion)with areduce-motionclass on<html>. Settings → Appearance gets a Reduce motion switch.Interaction polish includes a motion-based toast stack (FL-22); floating sidebar slide via Motion with interruptible springs; Peek springs/tweens that start on the first frame; command bar entrance with a one-frame lead; ⌘W deferring pane apply until after state is sent when waking a sleeping tab; drag ghost portaled to
.shellso it tracks the pointer during Space swipes; and Favorites / folder search / icon picker motion aligned to the tokens.Tooling: perf/capture scripts use
quitOrbit(like e2e) and--cold-only; optional loading-view A/B patch for cold start.Reviewed by Cursor Bugbot for commit 1fdee1e. Bugbot is set up for automated code reviews on this repo. Configure here.