Skip to content

Feel: motion measured against the spec, one set of motion tokens, reduced motion (feel lane: FL, AP-17) - #25

Merged
siyabendoezdemir merged 54 commits into
mainfrom
lane/feel
Oct 11, 2026
Merged

siyabendoezdemir merged 54 commits into
mainfrom
lane/feel

Conversation

@siyabendoezdemir

@siyabendoezdemir siyabendoezdemir commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

The feel lane measures every motion in spec/feel.md against its number and observable, puts all motion on one set of tokens, and fixes the gaps it measured. It's built from lanes/feel.md, with main merged in twice: import (fbb9103), then keys and PR #24 (592096b). After its review came two fix rounds (to 28ba347, then 9fc8bff) and four test commits from the lead (3a5f9ed, 2a7391e, 9bef710 and 1fdee1e).

docs/FEEL.md holds 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

  • One frame sampler for e2e (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 in feel.md is now a test on it, in 10 feel-*.spec.ts files.
  • One set of motion tokens (FL-01), in src/shared/motion.ts and src/renderer/src/motion/.
    • Springs are given as a response and a damping ratio, with CSS fallbacks and the exit factor.
    • The sidebar's and the Spaces' token files now use it.
    • Timings the spec sets on their own keep their own values: FL-12's chevron, FL-16's bar, FL-19's springs, FL-23's sweep and FL-28's cross-fade.
  • Reduced motion everywhere (FL-28, AP-17).
    • Orbit follows the OS, and a new setting forces reduced motion on: settings.reduceMotion, off by default, with a "Toggle Reduce Motion" command and a switch in Settings → Appearance.
    • Toasts follow it too, and a unit guard fails if a motion/react use or a style rule ignores Orbit's own switch.
    • Turning it on while Orbit runs reaches what's already on screen: Favorites, folders, the Pinned section, the "+", the drag ghost, the popovers and the downloads flyout. The guard fails if a long-lived motion element doesn't read the setting itself.
    • Main's own checks (Mini Window, media player) include it.
  • Tab rows (FL-04 to FL-10).
    • The pressed row shows as selected in the frame of the press (it took 92 ms).
    • A press on a row that has just grown in now reaches the row (PR Lead polish 2: full-split drop target, Favorite splits past 12, e2e evaluate race #10 review F1).
    • A selected row out of sight scrolls in on its own 225 ms ease-out, or at once from more than two screens away or under reduced motion.
    • After ⌘W, the next tab is selected in the first frame (the median of five closes). The first close after a launch can take 2 frames: a gap, in FEEL.md.
  • Long sidebars (FL-21). Only rows within 1.5 screens of the visible area are drawn, and every other row keeps an empty slot of its height.
    • Every row tells screen readers its place and the list's size, drawn or not.
    • A 500-tab scroll went from 102 slow frames to 12.
    • A 200-tab Space's first showing has a 57 ms task (it was 258 ms).
  • Timing tests that hold under load (FL-29).
  • Built:
    • yes: FL-01 to FL-17, FL-19, FL-20, FL-25 and FL-28;
    • audits only: FL-11, FL-23 and FL-27.
  • Partial:
    • AP-17: no shimmer when the app starts.
    • FL-18: no "bubble" while a tab hovers a row, and no wobble on drop.
    • FL-21: p99 is still two frame intervals under load.
    • FL-22: toasts don't take the Space theme yet (AP-14, the look lane).
    • FL-29: a few p99s under load, the hover sidebar's and a Peek close's start, and the first ⌘T or ⌘W after a launch.
    • FL-30: 3 pass, 3 fixed, 2 partial, and 1 can't be measured under Xvfb.
  • FL-26, audited only: the recent-tab switcher has no on-screen list yet (TB-05), so there's no overlay to animate.
  • Not measured by this lane (P2):
    • FL-24, already built on main;
    • FL-31: Linux has no trackpad haptics.

Changes to what a feature does

Each came after a feel test failed on the old build:

  • FL-05: a row shows as selected as soon as it's pressed, before main answers. A selected row out of sight scrolls in on FL-05's own curve.
  • FL-09: main sends the new selection before it wakes a sleeping tab.
  • FL-07: a row pinned or moved to a place out of sight collapses where it is, and the sidebar no longer scrolls after it.
  • FL-21: every row tells screen readers its place, the list's size and its depth.
  • FL-22: a plain toast stays 3 s (it was 2.2 s). A hovered toast with a button leaves 1 s after the pointer does. Under reduced motion, a toast fades in and out where it rests.
  • FL-28 and AP-17: the new setting, command and switch. sheet.css and import.css follow Orbit's switch, not only the OS setting.
  • FL-28, fix round 2: under reduced motion, pressing a Favorite or the "+" no longer scales it. A tile fades over 120 ms, and the popovers and the downloads flyout only fade. Known limit: turning reduced motion off doesn't bring motion back to an element until it's drawn again.
  • Taken back in the fix round: the Peek close settles when its curve ends again. The early settle at 0.1 % had no test that failed without it.

Decisions where the spec was unclear

  • "In place" means within 0.5 px. The standard spring is within 2 px at 270–300 ms, which is the spec's "260–300 ms", and within 0.5 px only near 450 ms. Rows on that spring give both times.
  • FL-18 moves the split drop zone between halves at once, since SV-26 defers its timings to feel.md.
  • AP-17's setting can only force reduced motion on. When it's off, Orbit follows the OS.
  • D70, three places where feel.md conflicts with itself:
    • FL-08's exit starts when main has closed the tab, about 2 frames after the release;
    • FL-19's open spring is tuned to its 280–320 ms, not the standard token;
    • FL-13's rubber band keeps its own spring (about 480 ms); FL-29's 350 ms is for swipes that land on a Space.

Fix round 1 (after the review)

Each must-fix has a test that fails without its fix (D18):

Item Commits Test Without the fix
M1: toasts and two style rules ignored reduced motion 1af4432, e60e252 feel-reduced's toast tests (Orbit's switch and the OS setting); unit guard reduced-motion-guard.test.ts Toasts moved 12 px at scale 0.96; the guard names Toasts, sheet.css and import.css
M2: FL-09's change had no failing test, and its 1 frame didn't hold dd89018 feel-rows [FL-09][FL-29]: rounds of 5 closes, the median against the budget Deferral taken out: 5 of 5 failed (2 frames)
M3: rows not drawn left holes for screen readers f2bddfd feel-sidebar: 200 tabs, the 151st selected No list size or place
M4: an interruption check failed on a dropped frame a4adf9b, b2410f7 feel-motion's interruption tests compare speeds; the bar's exit fades over at least 3 frames An exit that jumps: 4 of 4 failed
M5: a selected row out of sight didn't scroll as FL-05 says d74dd06 feel-sidebar FL-05 scroll: near, one screen away, past two screens, reduced motion Failed: 2 frames to start, 417–433 ms one screen away
  • Follow-ups done: 3 (567c6bd), 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.
  • Left: 7 (tighter asserts need rounds and runs under load). 10 goes to pages, 11 to finish-tabs, and 12 is resolved at merge time.
  • 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.
  • The lead's commits:
    • 3a5f9ed runs 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.
    • 2a7391e updates the comment in commandbar.spec.ts that 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):

Item Commit Test Without the fix
N1: reduced motion turned on while Orbit runs didn't reach Favorites, folders or the Pinned section until they were drawn again ef0eaff Four feel-reduced tests turn it on after mount (Orbit's switch and the OS setting); unit guard reduced-motion-guard.test.ts Tiles moved over 11–12 frames and a removed one shrank to 0.85; folder content took 11 frames and the chevron 9; a pressed tile scaled to 0.94. The guard named 11 components in 8 files
N2: FL-18's drop-zone test was judged on one round cda6ec6 FL-18 in D46 rounds, a fresh tab and page each round 2 failures in 7 single-round runs
  • Also done: N3 (a9b0ad2): 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).
  • Left: N5 goes to finish-views. The split row becomes a tree item with its place in the list, which makes the count right.
  • The lead's commits:
    • 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 1fdee1e

  • Static checks, in the lane's worktree: npm run build, npm run typecheck, npm run lint and ./node_modules/.bin/vitest run all exit 0. Unit: 865 passed of 865, in 86 files.
  • E2E: xvfb-run -a -s "-screen 0 1440x900x24" ./node_modules/.bin/playwright test --workers 1 --retries 0 ran 648 tests in 33.3 min: 644 passed, 4 skipped, 0 failed, and it exited 0.
    • The run was on 1fdee1e itself, with main (592096b) in it. The load average was 3–9, with other lanes' test runs beside it.
    • TB-22's expected failure (test.fail()) counts as passed.
  • Earlier runs:
    • r1, on 7b6179c (before keys and PR test: one temp folder per test run, removed when the run ends #24): 589 passed, 0 failed.
    • r2, on the merge: stopped at test 226 after the new FL-26 test failed once. That was a race in the test, fixed in 0f9fbfb (9 of 9 alone).
    • r3, on 0f9fbfb (the reviewed head): 635 passed, 4 skipped, 0 failed, at load 2–5.
    • r4, on 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; 3a5f9ed fixes that.
    • r5, on 2a7391e: 640 passed, 4 skipped, 0 failed, at load 7–12; unit 863/863.
    • r6, on 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; 9bef710 fixes that.
    • r7, on 9bef710: 642 passed, 2 failed, 4 skipped, at load 3–6; unit 865/865.
      • SV-17/SV-19's reopen test (split.spec.ts:493) is a known flake, and passed 3 of 3 alone.
      • FL-07's pin test failed 1 of 3 alone: a race in the test, which 1fdee1e fixes.
  • macOS and Windows checks, not run here (unverified):
    • macOS's own reduced-motion setting reaching main;
    • frame times on a 2-core machine at the macOS window size;
    • FL-31's haptics.

Review

  • The review (reviews/lane-feel.md, on 0f9fbfb): REQUEST CHANGES, 5 must-fix and 12 follow-ups.
    • It reproduced 9 of FEEL.md's numbers.
    • 5 of 5 P0 tests failed on a slower spring.
    • Long sidebars held at 500 tabs for drag, ⇧-click, keyboard, folder search and an import.
  • The fix check (reviews/lane-feel-fixcheck.md, on 2a7391e): 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.
  • Fix check 2 (reviews/lane-feel-fixcheck2.md, on 9bef710): APPROVE, no must-fix.
    • N1 is fixed: every element follows a switch made after mount. The folder and Pinned test fails without the fix, and the guard catches the class.
    • FL-18, FL-09 and FL-11 in rounds can still catch a slow build.
    • Its five small follow-ups go to the look lane: the turn-off limit in FL-28's note, two false passes in the guard, FL-09's wording and the Library's close. FL-21's note waits for N5 in finish-views.
    • 1fdee1e came 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 new docs/FEEL.md (before/after table, gaps, FL-30 checklist), and reflected in FEATURES.md / feature-status.json (many FL-* rows marked built or partial).

Shared motion (FL-01) centralizes springs and curves in src/shared/motion.ts and motion.css, with renderer OrbitMotionConfig and sidebar-local tokens removed in favor of src/renderer/src/motion/.

Reduced motion (FL-28, AP-17) adds settings.reduceMotion, main registerMotion / reducedMotion(ctx) (Mini Window, PiP), renderer installReducedMotion + useReducedMotion(), and replaces @media (prefers-reduced-motion) with a reduce-motion class 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 .shell so 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.

siyabendoezdemir and others added 30 commits October 10, 2026 20:25
…-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>
siyabendoezdemir and others added 10 commits October 11, 2026 05:11
… 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>
@cursor

cursor Bot commented Oct 11, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot 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>
@cursor

cursor Bot commented Oct 11, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot 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)

3a5f9ed runs feel-commandbar's FL-17 typing test in up to three rounds (D46),
so the comment that 4b69a76 corrected is out of date again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cursor

cursor Bot commented Oct 11, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot 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>
@cursor

cursor Bot commented Oct 11, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot 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)

siyabendoezdemir and others added 3 commits October 11, 2026 08:37
…, 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>
@cursor

cursor Bot commented Oct 11, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot 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>
@cursor

cursor Bot commented Oct 11, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot 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>
@cursor

cursor Bot commented Oct 11, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot 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)

@siyabendoezdemir
siyabendoezdemir merged commit 3bcb521 into main Oct 11, 2026
1 check passed
siyabendoezdemir added a commit that referenced this pull request Oct 11, 2026
main is feel's merge (PR #25), whose tree equals 1fdee1e, already merged
here: the tree is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant