fix: motion audit #859 #863

Open
kayg wants to merge 12 commits from fix/motion-audit-859 into dev
Owner

Fixes the motion audit in #859 (items 1–7 of the task). Token-level and small, to keep conflicts with the open port PRs (#845, #846, #850–#853) easy.

Per item

  1. Springs settle inside their tokens. installSprings() now solves each house spring's response so it rests (0.1 % of the move) inside its token; the declared --spring-*-ms can no longer exceed it. Tokens: snap 260, morph/sheet/card/style/bounce 300 (were 320/380/520/340; tokens.css fallbacks match). The JS springs the primitives use (check press, selector edges/slide, switch glide/squash, toast capsule) are retuned to rest within 300 ms (1 % band; the check press is a CSS linear() so it keeps 0.1 %). Selector lead/trail ratio is kept (0.17/0.27) so the stretch-and-catch still reads. Tests: lib/spring.test.ts (every house spring ≤ token, tokens.css matches) and primitives/spring/house.test.ts.
  2. Keyboard no-motion in WebKit: not a product bug. I reproduced the failure on WebKit (dev-02) and traced it: the animation seen after Ctrl+\/Ctrl+. was the click's own text-bar width spring (102→250 px, started ~290 ms before the key, still alive when still() first read empty), not something the key started; the key's transition-duration: 0s rule applies (computed 0s, no new animation, data-last-input="keyboard" set before the state change). The audit's Esc finding was the same kind of race (the sheet's open spring from the pointer click). No product change. Fixed the spec instead: no priming Esc (the last input before the key under test is now the pointer), waits for the text bar and for longer than the longest spring, plus a new Esc closing a sheet moves nothing test. Passes 3/3 on webkit-1440 and on chromium-1440.
  3. Tooltips. Warm neighbours (pointer or keyboard focus) are always instant; the travel glide, its CSS block and the word-slide animation are deleted.
  4. Press scale 0.97 everywhere. Changed (was → 0.97): .ib 0.96, .seg button 0.96 (chrome.css); .page .check 0.92 (editor.css); .page .tag-add 0.96 (cm.css); .go 0.96 (SignIn); .sw 0.9 (Inspector, ColorPicker); .accent .dot 0.92 (Settings); .ra-act 0.94 (RowActions); .act 0.96 (EmptyState, ToastRow); .opt 0.96 (Selector); task checkboxes 0.86 (cm.css, SpringCheckBox). Judgement call: the checkboxes were a deliberate squeeze; I followed the rule, so the box now presses 0.97. Easy to revert for those two if the squish matters more. Check fills no longer start at scale(0): they grow from 0.6 with an opacity fade (cm.css .cm-task::before, SpringCheckBox .sc-fill).
  5. Easing. Plain ease/ease-out/ease-in/linear on UI motion replaced by --ease-out (CSS) or its literal (script) in: cm.css, SpringCheckBox, strike.ts (the strike untick used ease-in), ToastRow/ToastCapsule, SquishSwitch, Selector, tree.css, motion.ts, Palette, TickSlider, TopPills (the title exit was an ease-in), editing.svelte.ts, picked.ts, copied.ts (per-segment ease-out on the moving parts, holds stay flat), and the drawer exit in Shell (was t*t; drawer open 320 → 280). Left on purpose: progress/ambient loops (spin, drift, twinkle, pulse, squiggle, counter-scroll). Sonner's 400 ms ease is NOT touched: #836 (own toast stack) is still open.
  6. Gaps. Tags rows now unfold/fold with the chevron (unfold transition: height + opacity from the heading, 220 ms, instant for keys, fade-only for reduced motion). The view-transition CSS in editor.css is deleted: nothing calls startViewTransition; a row → title flight would be a new feature with a WebKit support risk, and the note open already has its own fade. Blur crossfade (2 px, explicit blur(0) end so filter-motion lint is happy) added to swapTabs (list → note via fadeIn, Notes ⇄ Settings). Backdrop picture change: skipped. The ArtStack layer already carries the frost filter on the same element (a second animated filter would clash with it, #697) and a full-screen blur pass is the most expensive thing here; it needs a wrapper layer and its own frame measurement. Open for a follow-up.
  7. Date label. Not a loop. The 3 morph spans stay in document.getAnimations() as finished fill: forwards animations after one click (probe: running at +500 ms, finished at +1000 ms, nothing new for 6 s of idle, mouse moves and a row click). So the "kept firing" reading was finished animations lingering in the list. Duration 420 → 280 ms (torph scales its sub-animations).

Gates

  • svelte-check 0 errors (14 warnings of the existing a11y/CSS kinds; not compared against base), bun run lint (filter-motion) ok, unit tests: web 78/78, ui 62/62 (includes the new spring tests) on dev-01 and dev-02.
  • Playwright (CI=1 --retries=0, --workers=1): keys-no-motion, tooltips, task-check, tags, switching, tag-picker, select-rows, bar-stable, sheet-geometry, tree, toast-bar, info on chromium-1440/390 (dev-01) and webkit-1440/390 (dev-02): all pass after updating switching.spec.ts (its "only opacity moves" check now expects opacity + filter).
  • Frame numbers: comment below and on #859 (not in the repo).

🤖 Generated with Claude Code

Fixes the motion audit in #859 (items 1–7 of the task). Token-level and small, to keep conflicts with the open port PRs (#845, #846, #850–#853) easy. ## Per item 1. **Springs settle inside their tokens.** `installSprings()` now solves each house spring's response so it rests (0.1 % of the move) inside its token; the declared `--spring-*-ms` can no longer exceed it. Tokens: snap 260, morph/sheet/card/style/bounce 300 (were 320/380/520/340; `tokens.css` fallbacks match). The JS springs the primitives use (check press, selector edges/slide, switch glide/squash, toast capsule) are retuned to rest within 300 ms (1 % band; the check press is a CSS `linear()` so it keeps 0.1 %). Selector lead/trail ratio is kept (0.17/0.27) so the stretch-and-catch still reads. Tests: `lib/spring.test.ts` (every house spring ≤ token, tokens.css matches) and `primitives/spring/house.test.ts`. 2. **Keyboard no-motion in WebKit: not a product bug.** I reproduced the failure on WebKit (dev-02) and traced it: the animation seen after `Ctrl+\`/`Ctrl+.` was the *click's own* text-bar width spring (102→250 px, started ~290 ms before the key, still alive when `still()` first read empty), not something the key started; the key's `transition-duration: 0s` rule applies (computed `0s`, no new animation, `data-last-input="keyboard"` set before the state change). The audit's `Esc` finding was the same kind of race (the sheet's open spring from the pointer click). No product change. Fixed the spec instead: no priming `Esc` (the last input before the key under test is now the pointer), waits for the text bar and for longer than the longest spring, plus a new `Esc closing a sheet moves nothing` test. Passes 3/3 on webkit-1440 and on chromium-1440. 3. **Tooltips.** Warm neighbours (pointer or keyboard focus) are always `instant`; the `travel` glide, its CSS block and the word-slide animation are deleted. 4. **Press scale 0.97 everywhere.** Changed (was → 0.97): `.ib` 0.96, `.seg button` 0.96 (`chrome.css`); `.page .check` 0.92 (`editor.css`); `.page .tag-add` 0.96 (`cm.css`); `.go` 0.96 (`SignIn`); `.sw` 0.9 (`Inspector`, `ColorPicker`); `.accent .dot` 0.92 (`Settings`); `.ra-act` 0.94 (`RowActions`); `.act` 0.96 (`EmptyState`, `ToastRow`); `.opt` 0.96 (`Selector`); task checkboxes 0.86 (`cm.css`, `SpringCheckBox`). Judgement call: the checkboxes were a deliberate squeeze; I followed the rule, so the box now presses 0.97. Easy to revert for those two if the squish matters more. Check fills no longer start at `scale(0)`: they grow from 0.6 with an opacity fade (`cm.css` `.cm-task::before`, `SpringCheckBox` `.sc-fill`). 5. **Easing.** Plain `ease`/`ease-out`/`ease-in`/`linear` on UI motion replaced by `--ease-out` (CSS) or its literal (script) in: cm.css, SpringCheckBox, strike.ts (the strike untick used `ease-in`), ToastRow/ToastCapsule, SquishSwitch, Selector, tree.css, motion.ts, Palette, TickSlider, TopPills (the title exit was an ease-in), editing.svelte.ts, picked.ts, copied.ts (per-segment ease-out on the moving parts, holds stay flat), and the drawer exit in Shell (was `t*t`; drawer open 320 → 280). Left on purpose: progress/ambient loops (`spin`, `drift`, `twinkle`, `pulse`, `squiggle`, `counter-scroll`). **Sonner's 400 ms `ease` is NOT touched: #836 (own toast stack) is still open.** 6. **Gaps.** Tags rows now unfold/fold with the chevron (`unfold` transition: height + opacity from the heading, 220 ms, instant for keys, fade-only for reduced motion). The view-transition CSS in `editor.css` is **deleted**: nothing calls `startViewTransition`; a row → title flight would be a new feature with a WebKit support risk, and the note open already has its own fade. Blur crossfade (2 px, explicit `blur(0)` end so `filter-motion` lint is happy) added to `swapTabs` (list → note via `fadeIn`, Notes ⇄ Settings). **Backdrop picture change: skipped.** The `ArtStack` layer already carries the frost filter on the same element (a second animated filter would clash with it, #697) and a full-screen blur pass is the most expensive thing here; it needs a wrapper layer and its own frame measurement. Open for a follow-up. 7. **Date label.** Not a loop. The 3 morph spans stay in `document.getAnimations()` as *finished* `fill: forwards` animations after one click (probe: running at +500 ms, finished at +1000 ms, nothing new for 6 s of idle, mouse moves and a row click). So the "kept firing" reading was finished animations lingering in the list. Duration 420 → 280 ms (torph scales its sub-animations). ## Gates - `svelte-check` 0 errors (14 warnings of the existing a11y/CSS kinds; not compared against base), `bun run lint` (filter-motion) ok, unit tests: web 78/78, ui 62/62 (includes the new spring tests) on dev-01 and dev-02. - Playwright (CI=1 --retries=0, --workers=1): keys-no-motion, tooltips, task-check, tags, switching, tag-picker, select-rows, bar-stable, sheet-geometry, tree, toast-bar, info on chromium-1440/390 (dev-01) and webkit-1440/390 (dev-02): all pass after updating `switching.spec.ts` (its "only opacity moves" check now expects opacity + filter). - Frame numbers: comment below and on #859 (not in the repo). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
kayg added 12 commits 2026-11-02 13:47:00 +00:00
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
switching spec: the note fade carries a blur now (#859)
Some checks failed
CI / server (pull_request) Has been cancelled
CI / rust (pull_request) Has been cancelled
CI / web (pull_request) Has been cancelled
Markdown extraction contract / markdown-native-contract (pull_request) Has been cancelled
Markdown extraction contract / markdown-editor-contract (pull_request) Has been cancelled
f2c3a44cda
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Author
Owner

Frame measurements, before vs after (Mac VM macos-01-std, real GPU, lock /tmp/mac-bench.lock held only while it ran)

Before = origin/dev 55f35fcd; after = PR head f2c3a44c (same tree as the tested one). Throwaway spec (not in the repo): real server, 3 seeded notes, 8 runs per gesture; each run records requestAnimationFrame deltas from just before the pointer click to 700 ms after it. "drops" = frames over 20 ms per run in that 700 ms window. Backend binary: older macOS debug build (fix759b), the same for both sides.

WebKit 1440 (the meaningful one: 60 Hz, p50 17 ms)

gesture frames in window (before → after) p95 ms worst frame ms drops per run
sidebar hide (click) 21 → 35 109 → 47 252 → 104 10.6 → 11.0
sidebar show (click) 19 → 36 132 → 45 224 → 76 10.1 → 11.5
sheet open (Insert) 40 → 43 31 → 32 1046 → 87 3.4 → 4.1
sheet close (outside click) 44 → 42 32 → 29 116 → 301 3.4 → 3.3
list → note 45 → 43 23 → 28 186 → 282 3.4 → 3.5
Tags fold (no motion before → new unfold) 45 → 44 25 → 29 77 → 163 3.0 → 3.6
Tags unfold 44 → 45 25 → 30 313 → 216 2.8 → 3.8

Reading: the layout-heavy gestures (sidebar) were the ones the 556 ms spring hurt: the whole window now runs at p50 17 ms with p95 halved (109 → 47, 132 → 45). Sheet, list → note and Tags are within noise of before (one stray 100–300 ms frame per eight runs on either side; these are single-frame outliers, not sustained drops; the new Tags unfold adds a height tween and costs ~0.6–1 extra slow frame per run, which is the expected price of a height animation; I did not tune it further). The 1046 ms "before" outlier is a first-run compile hiccup.

Chromium 1440 on the VM

Chromium on this VM runs at a 50 ms p50 frame for both sides (software compositing path, ~20 fps floor), so its numbers prove only that the gestures ran: before p95 66–117 ms, after p95 96–100 ms; do not read them as performance.

Linux VMs (dev-01, dev-02) have no GPU; they ran the functional specs only.

## Frame measurements, before vs after (Mac VM `macos-01-std`, real GPU, lock `/tmp/mac-bench.lock` held only while it ran) Before = origin/dev `55f35fcd`; after = PR head `f2c3a44c` (same tree as the tested one). Throwaway spec (not in the repo): real server, 3 seeded notes, 8 runs per gesture; each run records `requestAnimationFrame` deltas from just before the pointer click to 700 ms after it. "drops" = frames over 20 ms per run in that 700 ms window. Backend binary: older macOS debug build (`fix759b`), the same for both sides. ### WebKit 1440 (the meaningful one: 60 Hz, p50 17 ms) | gesture | frames in window (before → after) | p95 ms | worst frame ms | drops per run | |---|---|---|---|---| | sidebar hide (click) | 21 → 35 | 109 → 47 | 252 → 104 | 10.6 → 11.0 | | sidebar show (click) | 19 → 36 | 132 → 45 | 224 → 76 | 10.1 → 11.5 | | sheet open (Insert) | 40 → 43 | 31 → 32 | 1046 → 87 | 3.4 → 4.1 | | sheet close (outside click) | 44 → 42 | 32 → 29 | 116 → 301 | 3.4 → 3.3 | | list → note | 45 → 43 | 23 → 28 | 186 → 282 | 3.4 → 3.5 | | Tags fold (no motion before → new unfold) | 45 → 44 | 25 → 29 | 77 → 163 | 3.0 → 3.6 | | Tags unfold | 44 → 45 | 25 → 30 | 313 → 216 | 2.8 → 3.8 | Reading: the layout-heavy gestures (sidebar) were the ones the 556 ms spring hurt: the whole window now runs at p50 17 ms with p95 halved (109 → 47, 132 → 45). Sheet, list → note and Tags are within noise of before (one stray 100–300 ms frame per eight runs on either side; these are single-frame outliers, not sustained drops; the new Tags unfold adds a height tween and costs ~0.6–1 extra slow frame per run, which is the expected price of a height animation; I did not tune it further). The 1046 ms "before" outlier is a first-run compile hiccup. ### Chromium 1440 on the VM Chromium on this VM runs at a 50 ms p50 frame for both sides (software compositing path, ~20 fps floor), so its numbers prove only that the gestures ran: before p95 66–117 ms, after p95 96–100 ms; do not read them as performance. Linux VMs (dev-01, dev-02) have no GPU; they ran the functional specs only.
Author
Owner

housekeeping #1009: needs-decision.

Motion audit partly targets the deleted Inspector and an older spring solver, since changed by #976/current Float work. Retain remaining Tag-fold/transition ideas for a fresh Mac measurement against current dev; old VM frame numbers are not current qualification.

original head: f2c3a44cda090d763540366d03478ce7922a4cdf. against dev cd7f84432febfc63b788fbb9b9f9950452982d74, git cherry reports 12 unmatched and 0 equivalent patches. the landing audit lists each candidate missing commit and stable patch match in pr-863.md; unmatched patches can include superseded behavior.

left open, source branch unchanged. no rebase or new test claim for this decision lane.

housekeeping #1009: needs-decision. Motion audit partly targets the deleted Inspector and an older spring solver, since changed by #976/current Float work. Retain remaining Tag-fold/transition ideas for a fresh Mac measurement against current dev; old VM frame numbers are not current qualification. original head: `f2c3a44cda090d763540366d03478ce7922a4cdf`. against dev `cd7f84432febfc63b788fbb9b9f9950452982d74`, git cherry reports 12 unmatched and 0 equivalent patches. the [landing audit](https://git.kayg.org/attachments/fe3c09dc-dea3-47a0-92e2-b44a777083e5) lists each candidate missing commit and stable patch match in `pr-863.md`; unmatched patches can include superseded behavior. left open, source branch unchanged. no rebase or new test claim for this decision lane.
Some checks failed
CI / server (pull_request) Has been cancelled
CI / rust (pull_request) Has been cancelled
CI / web (pull_request) Has been cancelled
Markdown extraction contract / markdown-native-contract (pull_request) Has been cancelled
Markdown extraction contract / markdown-editor-contract (pull_request) Has been cancelled
This pull request has changes conflicting with the target branch.
  • apps/web/src/components/Palette.svelte
View command line instructions

Manual merge helper

Use this merge commit message when completing the merge manually.

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin fix/motion-audit-859:fix/motion-audit-859
git switch fix/motion-audit-859

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch dev
git merge --no-ff fix/motion-audit-859
git switch fix/motion-audit-859
git rebase dev
git switch dev
git merge --ff-only fix/motion-audit-859
git switch fix/motion-audit-859
git rebase dev
git switch dev
git merge --no-ff fix/motion-audit-859
git switch dev
git merge --squash fix/motion-audit-859
git switch dev
git merge --ff-only fix/motion-audit-859
git switch dev
git merge fix/motion-audit-859
git push origin dev
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal!863
No description provided.