MOTION: keyboard actions never animate; loading never waits on animation (one modality flag in the motion tokens) #527

Closed
opened 2026-09-30 15:07:57 +00:00 by kayg · 7 comments
Owner

Owner principle (2026-09-30): "loading should be instantaneous. animations are what should take time. keyboard actions = no animations!"

  1. Keyboard actions never animate: any change triggered by a keyboard shortcut or key navigation (switching Tabs with shortcuts, moving the selection with arrows, opening or closing panels and overlays with keys, Enter/Escape, Cmd+K) applies instantly: no transition, no spring, no fade. Pointer and touch interactions keep their motion.
    • Implement once: an input-modality flag (the last input was keyboard) that the shared motion tokens (#477) read, so every transition that uses the tokens resolves to 0 ms for keyboard-initiated changes. No per-component special cases.
    • Keep focus rings (they are state, not animation).
  2. Loading never waits on animation: content appears as soon as data exists; no animation may delay content or input (for example, do not await a transition before rendering the list).
  3. Record both rules in DESIGN §34 (motion) and CLAUDE-level docs, and add a test: a keyboard-driven Tab switch, selection move and overlay open complete in one frame (no running animations via document.getAnimations()), while the pointer-driven versions animate.
    Depends on #477 (motion tokens), which is merging now.
## Owner principle (2026-09-30): "loading should be instantaneous. animations are what should take time. keyboard actions = no animations!" 1. **Keyboard actions never animate:** any change triggered by a keyboard shortcut or key navigation (switching Tabs with shortcuts, moving the selection with arrows, opening or closing panels and overlays with keys, Enter/Escape, Cmd+K) applies instantly: no transition, no spring, no fade. Pointer and touch interactions keep their motion. - Implement once: an input-modality flag (the last input was keyboard) that the shared motion tokens (#477) read, so every transition that uses the tokens resolves to 0 ms for keyboard-initiated changes. No per-component special cases. - Keep focus rings (they are state, not animation). 2. **Loading never waits on animation:** content appears as soon as data exists; no animation may delay content or input (for example, do not await a transition before rendering the list). 3. Record both rules in DESIGN §34 (motion) and CLAUDE-level docs, and add a test: a keyboard-driven Tab switch, selection move and overlay open complete in one frame (no running animations via `document.getAnimations()`), while the pointer-driven versions animate. Depends on #477 (motion tokens), which is merging now.
Author
Owner

Starting implementation on job/kbd-motion-527, based on origin/dev at 9ecde429a7e6f8b846211287ecd6b1475394db94 (includes #477 motion tokens). I found an existing root data-input modality flag, so this job will extend it and connect it to the shared CSS and JavaScript motion helpers. I am mapping the browser interaction test and CSS/JS animation paths before the first code change.

Starting implementation on `job/kbd-motion-527`, based on `origin/dev` at `9ecde429a7e6f8b846211287ecd6b1475394db94` (includes #477 motion tokens). I found an existing root `data-input` modality flag, so this job will extend it and connect it to the shared CSS and JavaScript motion helpers. I am mapping the browser interaction test and CSS/JS animation paths before the first code change.
Author
Owner

Finding (reproduced before the fix): the input-modality unit test returned false for keydownSignalsKeyboardModality({ key: 'k', target: button }), so Ctrl/Cmd+K did not switch the shared data-input flag to keyboard. The shared cascade transition also returned { delay: 190, duration: 310 } with data-input="keyboard". The root key classifier and shared motion helpers now handle those cases; the focused suite passes (2 files, 11 tests).

Finding (reproduced before the fix): the input-modality unit test returned `false` for `keydownSignalsKeyboardModality({ key: 'k', target: button })`, so Ctrl/Cmd+K did not switch the shared `data-input` flag to keyboard. The shared cascade transition also returned `{ delay: 190, duration: 310 }` with `data-input="keyboard"`. The root key classifier and shared motion helpers now handle those cases; the focused suite passes (2 files, 11 tests).
Author
Owner

Finding (reproduced and covered by a unit test): Ctrl/Cmd+K opened Search from an editable target, but the root input-modality predicate treated the key as typing and left data-input="pointer". That allowed the overlay motion to run for a keyboard action. Before the fix, the new test failed with expected false to be true; after classifying Ctrl/Cmd+K as keyboard input even when an editor has focus, the focused suite passes (3 tests).

Finding (reproduced and covered by a unit test): Ctrl/Cmd+K opened Search from an editable target, but the root input-modality predicate treated the key as typing and left `data-input="pointer"`. That allowed the overlay motion to run for a keyboard action. Before the fix, the new test failed with `expected false to be true`; after classifying Ctrl/Cmd+K as keyboard input even when an editor has focus, the focused suite passes (3 tests).
Author
Owner

Finding (reproduced on the production SPA): after a pointer-opened Search overlay received keyboard Escape, document.getAnimations() still showed a live CSSTransition on button.pg-btn.chrome-action with duration 420 ms and current time 133 ms. Setting transition duration to 0 did not cancel an already-running transition. The shared keyboard CSS now uses transition: none and animation: none, which cancels active CSS motion. The shared pill feedback action also returns through motionIsImmediate() so Enter/Space does not create a keyboard pulse. The focused pulse suite passes (21 tests), and the production E2E passes with the pointer motion and keyboard no-motion checks.

Finding (reproduced on the production SPA): after a pointer-opened Search overlay received keyboard Escape, `document.getAnimations()` still showed a live `CSSTransition` on `button.pg-btn.chrome-action` with duration 420 ms and current time 133 ms. Setting transition duration to 0 did not cancel an already-running transition. The shared keyboard CSS now uses `transition: none` and `animation: none`, which cancels active CSS motion. The shared pill feedback action also returns through `motionIsImmediate()` so Enter/Space does not create a keyboard pulse. The focused pulse suite passes (21 tests), and the production E2E passes with the pointer motion and keyboard no-motion checks.
Author
Owner

Completed: #527 keyboard motion and loading

Branch head: d4120e1d0c2e8b861b60970e9581258fbcb66c7a

Keyboard-driven state changes now use the shared motion contract at 0 ms. This covers Tab changes, selection movement, overlays and panels, Escape, Enter, and Cmd/Ctrl+K. Pointer and touch motion remains enabled. Root modality is set in capture phase; shared CSS cancels both new and already-running transitions/animations. Overlay content does not wait for an animation frame before becoming available. Focus rings remain enabled for keyboard input.

A production E2E run passed: PASS keyboard Tab switch, selection move and overlay open are immediate; pointer actions animate. The run checks document.getAnimations() after a frame for keyboard actions and confirms pointer transitions still run. Eighteen production screenshots are attached below.

Decisions

  • Any key on a non-editable target sets keyboard modality. Editable typing keeps the current modality, except Cmd/Ctrl+K because the global Search shortcut works from editors too.
  • When keyboard input starts during a pointer animation, cancel the active CSS animation and transition as well as setting future durations to zero. The E2E run found a live 420 ms CSS transition after Escape; duration tokens alone did not cancel it.
  • Keyboard overlay open skips the pointer-only staging frame. Pointer overlays keep their existing staged opening motion.

Files

CLAUDE.md; docs/DESIGN.md; apps/web/package.json; apps/web/e2e/kbd-motion-527.mjs; apps/web/src/lib/a11y/inputModality.ts and tests; apps/web/src/lib/actions/pillFeedback.ts and tests; apps/web/src/lib/capsule-motion.test.ts; apps/web/src/lib/components/SidebarLinks.svelte; apps/web/src/lib/files/FilesSidebar.svelte; apps/web/src/lib/notifications/InboxPanel.svelte; apps/web/src/routes/+layout.svelte; bench/kbd-motion-527.mjs; packages/ui/package.json; packages/ui/src/index.ts; packages/ui/src/motion.ts; packages/ui/src/tokens.css; packages/ui/src/components/OverlaySurface.svelte; packages/ui/src/components/SegmentedControl.svelte; packages/ui/src/components/TabBar.svelte; packages/ui/src/components/menu/FloatingSurface.svelte.

Performance profile

Local production profile, 1,000 real Files, 20 average moves and a 100-move burst. Local load average before the run was [39.78, 35.71, 35.59].

  • Average: p50 23.54 ms, p95 71.79 ms, max 99.24 ms; browser CPU 0.42 s; RSS 629,579,776 bytes before and 628,645,888 bytes after.
  • Burst: p50 16.43 ms, p95 38.65 ms, max 73.32 ms; browser CPU 1.31 s; peak RSS 634,281,984 bytes.
  • docs/perf/baseline.json has no keyboard-motion profile to compare against. These are local measurements on a heavily loaded host.

Visual evidence

Files, Today and Search at 390, 820 and 1440 px in light and dark modes:

View 390 px 820 px 1440 px
Files light · dark light · dark light · dark
Today light · dark light · dark light · dark
Search light · dark light · dark light · dark

Gates

bun run check:

$ node scripts/check-type-tokens.mjs && node scripts/check-motion-tokens.mjs && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json
Text sizes and UI shape values use shared role tokens.
UI transitions and animation options use shared motion tokens or documented exceptions.
Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/kbd-motion-527/apps/web
Getting Svelte diagnostics...

svelte-check found 0 errors and 0 warnings

bun run test final output:

 Test Files  141 passed (141)
      Tests  920 passed (920)
   Start at  19:25:16
   Duration  124.31s (transform 58%, environment 16%, import 14%, tests 9%, setup 3%)

The test run also printed jsdom notices for unimplemented Window.scrollTo() and CSS parsing; it exited 0. No Rust source or route/API changed, so Rust gates and the API adversarial round did not apply. Cargo cleanup removed 7,067 files / 4.4 GiB; web build output was removed.

The full gates ran on merge head 8d1f427142981c1a01450769229de6ee3c74f6a2. Final commit d4120e1d0c2e8b861b60970e9581258fbcb66c7a is a documentation-only predicate comment correction made after the gates.

## Completed: #527 keyboard motion and loading **Branch head:** `d4120e1d0c2e8b861b60970e9581258fbcb66c7a` Keyboard-driven state changes now use the shared motion contract at 0 ms. This covers Tab changes, selection movement, overlays and panels, Escape, Enter, and Cmd/Ctrl+K. Pointer and touch motion remains enabled. Root modality is set in capture phase; shared CSS cancels both new and already-running transitions/animations. Overlay content does not wait for an animation frame before becoming available. Focus rings remain enabled for keyboard input. A production E2E run passed: `PASS keyboard Tab switch, selection move and overlay open are immediate; pointer actions animate`. The run checks `document.getAnimations()` after a frame for keyboard actions and confirms pointer transitions still run. Eighteen production screenshots are attached below. ### Decisions - Any key on a non-editable target sets keyboard modality. Editable typing keeps the current modality, except Cmd/Ctrl+K because the global Search shortcut works from editors too. - When keyboard input starts during a pointer animation, cancel the active CSS animation and transition as well as setting future durations to zero. The E2E run found a live 420 ms CSS transition after Escape; duration tokens alone did not cancel it. - Keyboard overlay open skips the pointer-only staging frame. Pointer overlays keep their existing staged opening motion. ### Files `CLAUDE.md`; `docs/DESIGN.md`; `apps/web/package.json`; `apps/web/e2e/kbd-motion-527.mjs`; `apps/web/src/lib/a11y/inputModality.ts` and tests; `apps/web/src/lib/actions/pillFeedback.ts` and tests; `apps/web/src/lib/capsule-motion.test.ts`; `apps/web/src/lib/components/SidebarLinks.svelte`; `apps/web/src/lib/files/FilesSidebar.svelte`; `apps/web/src/lib/notifications/InboxPanel.svelte`; `apps/web/src/routes/+layout.svelte`; `bench/kbd-motion-527.mjs`; `packages/ui/package.json`; `packages/ui/src/index.ts`; `packages/ui/src/motion.ts`; `packages/ui/src/tokens.css`; `packages/ui/src/components/OverlaySurface.svelte`; `packages/ui/src/components/SegmentedControl.svelte`; `packages/ui/src/components/TabBar.svelte`; `packages/ui/src/components/menu/FloatingSurface.svelte`. ### Performance profile Local production profile, 1,000 real Files, 20 average moves and a 100-move burst. Local load average before the run was `[39.78, 35.71, 35.59]`. - Average: p50 23.54 ms, p95 71.79 ms, max 99.24 ms; browser CPU 0.42 s; RSS 629,579,776 bytes before and 628,645,888 bytes after. - Burst: p50 16.43 ms, p95 38.65 ms, max 73.32 ms; browser CPU 1.31 s; peak RSS 634,281,984 bytes. - `docs/perf/baseline.json` has no keyboard-motion profile to compare against. These are local measurements on a heavily loaded host. ### Visual evidence Files, Today and Search at 390, 820 and 1440 px in light and dark modes: | View | 390 px | 820 px | 1440 px | |---|---|---|---| | Files | [light](https://git.kayg.org/attachments/5cdd10b0-c7b0-4123-8289-7fe7e171c77f) · [dark](https://git.kayg.org/attachments/dee7e6a7-c8ec-49a5-b7a3-781777844554) | [light](https://git.kayg.org/attachments/d9ff2935-e464-4a73-bd09-dc62dbe7c843) · [dark](https://git.kayg.org/attachments/69e5a6f0-78f8-4fa6-8eb6-e87154f920cb) | [light](https://git.kayg.org/attachments/desktop-light-files.png) · [dark](https://git.kayg.org/attachments/d49bdeac-00d6-4810-94bf-4c63d0637fad) | | Today | [light](https://git.kayg.org/attachments/b838f72e-ec59-4eb8-b5c5-4389a3cbdd46) · [dark](https://git.kayg.org/attachments/793d67fb-269d-4e16-a79c-1e363fc3a40e) | [light](https://git.kayg.org/attachments/d3536be4-a5cf-4864-824f-94e7de263763) · [dark](https://git.kayg.org/attachments/ae8265ce-9a13-4ef0-a5e1-9c5608ffb04b) | [light](https://git.kayg.org/attachments/f3e73a0e-65ff-48a3-a06e-e0ace9b0e72f) · [dark](https://git.kayg.org/attachments/b400bc18-65b0-4b45-b046-4be008e18e80) | | Search | [light](https://git.kayg.org/attachments/b7025635-f1a4-4f1f-9590-eb92bd4b6cfe) · [dark](https://git.kayg.org/attachments/7572240b-eb93-44ba-8e57-5b7820749695) | [light](https://git.kayg.org/attachments/74e1705a-2f6a-4498-9a12-2fb7eca862c6) · [dark](https://git.kayg.org/attachments/d9be3a5d-d687-4124-8707-7cb5dc66bdf6) | [light](https://git.kayg.org/attachments/e9c89d89-cc35-425a-bef3-70df3a65a93e) · [dark](https://git.kayg.org/attachments/5fa04704-2811-492d-be2f-89851ce1928f) | ### Gates `bun run check`: ```text $ node scripts/check-type-tokens.mjs && node scripts/check-motion-tokens.mjs && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json Text sizes and UI shape values use shared role tokens. UI transitions and animation options use shared motion tokens or documented exceptions. Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/kbd-motion-527/apps/web Getting Svelte diagnostics... svelte-check found 0 errors and 0 warnings ``` `bun run test` final output: ```text Test Files 141 passed (141) Tests 920 passed (920) Start at 19:25:16 Duration 124.31s (transform 58%, environment 16%, import 14%, tests 9%, setup 3%) ``` The test run also printed jsdom notices for unimplemented `Window.scrollTo()` and CSS parsing; it exited 0. No Rust source or route/API changed, so Rust gates and the API adversarial round did not apply. Cargo cleanup removed 7,067 files / 4.4 GiB; web build output was removed. The full gates ran on merge head `8d1f427142981c1a01450769229de6ee3c74f6a2`. Final commit `d4120e1d0c2e8b861b60970e9581258fbcb66c7a` is a documentation-only predicate comment correction made after the gates.
Author
Owner

Correction to the visual evidence table in my report: the 1440 px light Files screenshot link is desktop-light-files.png. The image was uploaded successfully; the original table had a filename in place of its attachment URL.

Correction to the visual evidence table in my report: the 1440 px light Files screenshot link is [desktop-light-files.png](https://git.kayg.org/attachments/6890434b-d7cd-4d7f-9e06-2bbf5890836d). The image was uploaded successfully; the original table had a filename in place of its attachment URL.
Author
Owner

Shipped in merge round 4, deployed to calternal.cloud in 1af8ead26 (healthy).

Shipped in merge round 4, deployed to calternal.cloud in 1af8ead26 (healthy).
kayg closed this issue 2026-10-01 09:17:57 +00:00
Sign in to join this conversation.
No labels
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#527
No description provided.