Motion: pill press bounce is effectively gone (toned to 1.035); make it subtle but felt, verify it fires on every pill #425

Closed
opened 2026-09-29 09:36:01 +00:00 by kayg · 6 comments
Owner

Bug (owner, 2026-09-29)

"The click-on-a-pill bounce effect is completely gone? calternal.js had it. Please investigate. It shouldn't be too dramatic, but subtle."

Findings so far (orchestrator)

  • The machinery exists:
    • the pillFeedback action, apps/web/src/lib/actions/pillFeedback.ts, used by packages/ui/src/components/Pill.svelte:104;
    • .pill-bounce in apps/web/src/calternal-app.css:~873;
    • --pill-bounce and @keyframes pill-bounce in packages/ui/src/tokens.css.
  • calternal.js peaks at scale(1.1), then 0.97 (calternal.js/packages/web/src/app.css:1125). Commit dd215053 (2026-09-25, "tone down the pill bounce") changed ours to 1.035 / 0.992 over 0.32 s. On a 30 px pill that is about 1 px: invisible, which reads as "gone".

Work

  1. Verify that it fires everywhere. In a production build, press every pill-like control: tray modes, header islands, PillGroup segments, Settings pills, composer mode pills, tag chips, toast actions, and the selection-count pill. Record for each whether a pill-bounce animation actually runs (getAnimations()), and whether something cancels or overrides it: a scoped animation: or transform rule, a transition: transform, the magnetic-hover inline transform, will-change, or a parent overflow: hidden clipping it. Fix every control where it does not run, at the shared primitive (one owner; no per-component copies).
  2. Retune to "subtle but clearly felt", between the two extremes. Reuse the app's spring from #291 (the capsule morph the owner loves; see packages/ui/src/motion.ts) if it can drive a press, so the feel matches the rest of the app. Starting point: press-in to about 0.96 on pointerdown, then a spring release overshooting to about 1.04 and settling. Or a single keyframe peaking about 1.05 with a 0.985 undershoot over about 300 ms. Scale by size, so large pills do not move more pixels than small ones (cap the pixel displacement at about 2 px).
  3. Keep the reduced-motion rule (no bounce), keyboard activation parity, and no layout shift (transform only).
  4. Record the chosen values and why in the token's doc comment and in DESIGN §34 motion.

Proof

  • A frame strip (every frame over 400 ms) of a tray mode pill and a small tag chip being pressed, from a production build at 1440 and 390 px, light and dark, attached to this issue for the orchestrator's visual review.
  • An e2e that asserts the animation runs on each pill type listed above.
    Web gates: bun run check, bun run test.
## Bug (owner, 2026-09-29) "The click-on-a-pill bounce effect is completely gone? calternal.js had it. Please investigate. It shouldn't be too dramatic, but subtle." ## Findings so far (orchestrator) - The machinery exists: - the `pillFeedback` action, `apps/web/src/lib/actions/pillFeedback.ts`, used by `packages/ui/src/components/Pill.svelte:104`; - `.pill-bounce` in `apps/web/src/calternal-app.css:~873`; - `--pill-bounce` and `@keyframes pill-bounce` in `packages/ui/src/tokens.css`. - calternal.js peaks at `scale(1.1)`, then `0.97` (`calternal.js/packages/web/src/app.css:1125`). Commit dd215053 (2026-09-25, "tone down the pill bounce") changed ours to `1.035` / `0.992` over 0.32 s. On a 30 px pill that is about 1 px: invisible, which reads as "gone". ## Work 1. **Verify that it fires everywhere.** In a production build, press every pill-like control: tray modes, header islands, PillGroup segments, Settings pills, composer mode pills, tag chips, toast actions, and the selection-count pill. Record for each whether a `pill-bounce` animation actually runs (`getAnimations()`), and whether something cancels or overrides it: a scoped `animation:` or `transform` rule, a `transition: transform`, the magnetic-hover inline transform, `will-change`, or a parent `overflow: hidden` clipping it. Fix every control where it does not run, at the shared primitive (one owner; no per-component copies). 2. **Retune to "subtle but clearly felt"**, between the two extremes. Reuse the app's spring from #291 (the capsule morph the owner loves; see `packages/ui/src/motion.ts`) if it can drive a press, so the feel matches the rest of the app. Starting point: press-in to about 0.96 on pointerdown, then a spring release overshooting to about 1.04 and settling. Or a single keyframe peaking about 1.05 with a 0.985 undershoot over about 300 ms. Scale by size, so large pills do not move more pixels than small ones (cap the pixel displacement at about 2 px). 3. Keep the reduced-motion rule (no bounce), keyboard activation parity, and no layout shift (transform only). 4. Record the chosen values and why in the token's doc comment and in DESIGN §34 motion. ## Proof - A frame strip (every frame over 400 ms) of a tray mode pill and a small tag chip being pressed, from a production build at 1440 and 390 px, light and dark, attached to this issue for the orchestrator's visual review. - An e2e that asserts the animation runs on each pill type listed above. Web gates: `bun run check`, `bun run test`.
Author
Owner

Started investigation on branch job/bounce-425, based on dev at 191b179baac3ef4f5bebfe07ce91c4b7a887ace2 (same as starting HEAD). I will trace each requested control in a real production build, make the shared motion change, add production-build e2e coverage and visual evidence, then run the web gates.

Started investigation on branch `job/bounce-425`, based on `dev` at `191b179baac3ef4f5bebfe07ce91c4b7a887ace2` (same as starting HEAD). I will trace each requested control in a real production build, make the shared motion change, add production-build e2e coverage and visual evidence, then run the web gates.
Author
Owner

Source trace before the production interaction pass: TabBar.svelte renders the mode tray's SegmentedControl without bounce, while SegmentedControl.svelte only adds its separate .bounce animation from activate() when that optional prop is true; the tray does not use pillFeedback. Clickable TagPill buttons also do not use pillFeedback; their only press rule is transform: scale(0.94). Header PillGroup, generic Pill (including toast actions), and the selection-count Pill already use the shared action. I am checking these paths in a production build before changing them.

Source trace before the production interaction pass: `TabBar.svelte` renders the mode tray's `SegmentedControl` without `bounce`, while `SegmentedControl.svelte` only adds its separate `.bounce` animation from `activate()` when that optional prop is true; the tray does not use `pillFeedback`. Clickable `TagPill` buttons also do not use `pillFeedback`; their only press rule is `transform: scale(0.94)`. Header `PillGroup`, generic `Pill` (including toast actions), and the selection-count `Pill` already use the shared action. I am checking these paths in a production build before changing them.
Author
Owner

Production E2E found a tray clipping case. With a 135 px scroll window, the pulse peak extended to x=759.97 while the clip ended at x=755.5. The segmented control's border-box max-width omitted the two 3 px pulse gutters, and reveal geometry snapped the pill to the clip edge. I added both gutters to the shared window sizing and snap reveal, with regression coverage for the 3 px inset. The rebuilt production E2E is checking the fix and the remaining pill families now.

Production E2E found a tray clipping case. With a 135 px scroll window, the pulse peak extended to x=759.97 while the clip ended at x=755.5. The segmented control's border-box max-width omitted the two 3 px pulse gutters, and reveal geometry snapped the pill to the clip edge. I added both gutters to the shared window sizing and snap reveal, with regression coverage for the 3 px inset. The rebuilt production E2E is checking the fix and the remaining pill families now.
Author
Owner

A keyboard activation probe found a second cap issue during a mode label morph. The segment grew from its 45 px icon width to 112 px while its pulse still used a 1.05 peak, so the measured edge move was 2.8 px and crossed the scroll clip by 0.06 px. The shared action now sizes from scrollWidth as well as the rendered box, and a regression test covers overflowing content during label growth.

A keyboard activation probe found a second cap issue during a mode label morph. The segment grew from its 45 px icon width to 112 px while its pulse still used a 1.05 peak, so the measured edge move was 2.8 px and crossed the scroll clip by 0.06 px. The shared action now sizes from `scrollWidth` as well as the rendered box, and a regression test covers overflowing content during label growth.
Author
Owner

Production trace for #425 found an additional shared tray issue. On a real pointer press of Files, the settled track was 209 px, then it contracted to 145 px during the selected-label transition. The predicted pulse ended at x=775.88 while the scroll clip ended at x=760.50, so the pill was clipped by 15.38 px. The action now caps the morphing 87 px pill at a peak scale of 1.04598. I am adding a layout guard for active label transitions and will rerun the production trace.

Production trace for #425 found an additional shared tray issue. On a real pointer press of Files, the settled track was 209 px, then it contracted to 145 px during the selected-label transition. The predicted pulse ended at x=775.88 while the scroll clip ended at x=760.50, so the pill was clipped by 15.38 px. The action now caps the morphing 87 px pill at a peak scale of 1.04598. I am adding a layout guard for active label transitions and will rerun the production trace.
Author
Owner

#425 complete — shared pill press feedback

What changed

Restored a clearly perceptible press pulse through the shared pillFeedback action. The production E2E pressed tray modes, header islands, PillGroup date segments, Settings segments, composer modes, a tag chip, a toast action and the selection-count pill. getAnimations() reported pill-bounce running for each enabled control. The disabled composer Event mode and reduced-motion case correctly had no pulse. Enter and Space activation both ran the pulse.

The probe also recorded computed animation, transition, transform, magnetic inline translation and ancestor clipping. No tested control had a clipped pulse or a competing animation override. Magnetic translation continued independently of the CSS scale pulse.

The E2E used a production build and real local server. It passed with PILL_FEEDBACK_PRODUCTION_CHECKS_PASSED.

Decisions

  • Use a 300 ms shared keyframe: scale to 0.96 at 12%, overshoot to 1.05 at 38%, dip to 0.985 at 68%, then rest at 1.0. The CSS scale property leaves layout unchanged. The action reduces each stop for larger pills so edge displacement stays at or below 2 px.
  • Use the keyframe instead of #291’s capsule spring. That spring is overdamped and cannot produce the requested release overshoot.
  • Give scrollable segmented controls a 6 px inner gutter. The previous 5 px gutter left the snapped pill 0.34 px inside the clip; the extra pixel clears fractional snap rounding.
  • When Chromium downscales CDP screencast frames, capture clipped screenshots from the same production page to retain the pill’s pixel bounds.

Production evidence

Frame strips cover the tray mode pill and tag chip at 390 px and 1440 px in light and dark themes. Viewport matrices cover shell, Settings, composer, toast, tag and selection screens at 390 px, 820 px and 1440 px in both themes.

Frame strips (each capture spans 400 ms; host compositor delivery yielded 2–9 captured frames):

Viewport matrices:

Files

apps/web/src/lib/actions/pillFeedback.ts, apps/web/src/lib/actions/pillFeedback.test.ts, apps/web/src/calternal-app.css, apps/web/src/lib/tray.svelte.test.ts, apps/web/e2e/pill-feedback.mjs, apps/web/package.json, packages/ui/src/components/Pill.svelte, PillGroup.svelte, SegmentedControl.svelte, TagPill.svelte, segmentScroll.ts, packages/ui/src/tokens.css, docs/DESIGN.md.

Head: b83caf9871

Gates

Command: bun run check

$ node scripts/check-type-tokens.mjs && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json
Text sizes use shared role tokens.
Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/bounce-425/apps/web
Getting Svelte diagnostics...

svelte-check found 0 errors and 0 warnings

Command: bun run test

$ vitest run

 RUN  v5.0.1 /home/kayg/Developer/calternal-wt/bounce-425/apps/web

Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Could not parse CSS stylesheet
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method

 Test Files  125 passed (125)
      Tests  804 passed (804)
   Start at  18:30:33
   Duration  103.90s (transform 56%, import 16%, environment 15%, tests 10%, setup 3%)

Environment  |component| jsdom was created 39 times · 77.81s total, 25% of tracked time
             create it once per worker with pool: 'vmThreads' (keeps per-file isolation) or isolate: false (shares it across files)
             learn more: https://vitest.dev/guide/improving-performance#test-environments

Rust gates were not run because this change does not touch Rust code. cargo clean removed 6940 files (4.3 GiB).

Known gaps

No product behavior gap found. The shared host delivered 2–9 captured frames across each 400 ms press sequence. The production proof falls back to cropped browser screenshots for downscaled screencast frames; the strips record the available frames and are attached above.

#425 complete — shared pill press feedback ## What changed Restored a clearly perceptible press pulse through the shared `pillFeedback` action. The production E2E pressed tray modes, header islands, PillGroup date segments, Settings segments, composer modes, a tag chip, a toast action and the selection-count pill. `getAnimations()` reported `pill-bounce` running for each enabled control. The disabled composer Event mode and reduced-motion case correctly had no pulse. Enter and Space activation both ran the pulse. The probe also recorded computed animation, transition, transform, magnetic inline translation and ancestor clipping. No tested control had a clipped pulse or a competing animation override. Magnetic translation continued independently of the CSS `scale` pulse. The E2E used a production build and real local server. It passed with `PILL_FEEDBACK_PRODUCTION_CHECKS_PASSED`. ## Decisions - Use a 300 ms shared keyframe: scale to 0.96 at 12%, overshoot to 1.05 at 38%, dip to 0.985 at 68%, then rest at 1.0. The CSS `scale` property leaves layout unchanged. The action reduces each stop for larger pills so edge displacement stays at or below 2 px. - Use the keyframe instead of #291’s capsule spring. That spring is overdamped and cannot produce the requested release overshoot. - Give scrollable segmented controls a 6 px inner gutter. The previous 5 px gutter left the snapped pill 0.34 px inside the clip; the extra pixel clears fractional snap rounding. - When Chromium downscales CDP screencast frames, capture clipped screenshots from the same production page to retain the pill’s pixel bounds. ## Production evidence Frame strips cover the tray mode pill and tag chip at 390 px and 1440 px in light and dark themes. Viewport matrices cover shell, Settings, composer, toast, tag and selection screens at 390 px, 820 px and 1440 px in both themes. Frame strips (each capture spans 400 ms; host compositor delivery yielded 2–9 captured frames): - 390 px: [tray, light](https://git.kayg.org/attachments/6c8e9433-d8fa-4d05-81de-6331c7f47779), [tag, light](https://git.kayg.org/attachments/1c38228b-6564-424b-8fcb-282d9d4ea1cf), [tray, dark](https://git.kayg.org/attachments/6fe3702b-84f7-48e8-ac61-4f6135d67c5a), [tag, dark](https://git.kayg.org/attachments/ba99fc85-67b8-4457-bff7-3588e4c24b7c) - 1440 px: [tray, light](https://git.kayg.org/attachments/2b6b6a01-773f-40cd-b8ac-dc4fabdd67ba), [tag, light](https://git.kayg.org/attachments/f95f36f3-b24a-46b4-bd61-ef94903b16aa), [tray, dark](https://git.kayg.org/attachments/c96349c6-2552-4c2c-b9aa-36ae8b8cf068), [tag, dark](https://git.kayg.org/attachments/1e838855-76b7-4267-b34e-d6b9f94a83a5) Viewport matrices: - [Shell](https://git.kayg.org/attachments/39482d82-b811-433d-9e9a-67647ba07049) - [Settings](https://git.kayg.org/attachments/ade73b6e-79a6-4f32-8900-8b65babc9638) - [Composer](https://git.kayg.org/attachments/68f452c1-21d5-40b3-8aa1-26d173ae1309) - [Toast](https://git.kayg.org/attachments/eac9a50e-19b3-4cd0-894c-f4d68621fc43) - [Tag](https://git.kayg.org/attachments/d2191f61-279f-49e4-990b-a43ef8974494) - [Selection](https://git.kayg.org/attachments/c79d3f2c-838c-4712-bdcb-641b77308167) ## Files `apps/web/src/lib/actions/pillFeedback.ts`, `apps/web/src/lib/actions/pillFeedback.test.ts`, `apps/web/src/calternal-app.css`, `apps/web/src/lib/tray.svelte.test.ts`, `apps/web/e2e/pill-feedback.mjs`, `apps/web/package.json`, `packages/ui/src/components/Pill.svelte`, `PillGroup.svelte`, `SegmentedControl.svelte`, `TagPill.svelte`, `segmentScroll.ts`, `packages/ui/src/tokens.css`, `docs/DESIGN.md`. Head: b83caf9871d46d4858840787ab2b7285332ce1a4 ## Gates Command: `bun run check` ```text $ node scripts/check-type-tokens.mjs && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json Text sizes use shared role tokens. Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/bounce-425/apps/web Getting Svelte diagnostics... svelte-check found 0 errors and 0 warnings ``` Command: `bun run test` ```text $ vitest run RUN v5.0.1 /home/kayg/Developer/calternal-wt/bounce-425/apps/web Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Could not parse CSS stylesheet Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Test Files 125 passed (125) Tests 804 passed (804) Start at 18:30:33 Duration 103.90s (transform 56%, import 16%, environment 15%, tests 10%, setup 3%) Environment |component| jsdom was created 39 times · 77.81s total, 25% of tracked time create it once per worker with pool: 'vmThreads' (keeps per-file isolation) or isolate: false (shares it across files) learn more: https://vitest.dev/guide/improving-performance#test-environments ``` Rust gates were not run because this change does not touch Rust code. `cargo clean` removed 6940 files (4.3 GiB). ## Known gaps No product behavior gap found. The shared host delivered 2–9 captured frames across each 400 ms press sequence. The production proof falls back to cropped browser screenshots for downscaled screencast frames; the strips record the available frames and are attached above.
kayg closed this issue 2026-09-29 16:42:48 +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#425
No description provided.