-
Notifications
You must be signed in to change notification settings - Fork 0
feat(roots): expressive motion tokens (spring easings + pop/shake/fade presets) #45
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
37 changes: 37 additions & 0 deletions
37
docs/feedback/0015-assert-every-shipped-variant-and-acceptance-property.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| # 0015 - Tests must pin every shipped variant and every acceptance property | ||
|
|
||
| ## Symptom | ||
|
|
||
| Spec 0033 shipped five new `animate-*` presets (`pop-in/out`, `shake`, `fade-in/out`) but the | ||
| first test pass only asserted the `-in` halves and `shake`, leaving `pop-out` / `fade-out` | ||
| unverified - a regression dropping either would have stayed green. Separately, the spec's own | ||
| Acceptance listed "the fold stays idempotent (no double-append)" as a deliverable, yet no test | ||
| enforced it: the idempotency lived only in `build.mjs`'s prose, so a regression to the old | ||
| append-in-place bug (feedback 0003) would have doubled every keyframe while every `toContain` | ||
| assertion still passed. Two personas (tester, engineer) independently flagged both gaps on PR #45. | ||
|
|
||
| ## Root cause | ||
|
|
||
| The tests asserted the *representative* case, not the *contract*. When a change ships a set of | ||
| variants, testing one member reads as "covered" but only pins that member. And an acceptance | ||
| bullet that names a property (idempotency) is a promise; if no test encodes it, the promise is | ||
| enforced by comment, which rots silently - the exact failure mode of a `toContain` assertion that | ||
| can't see a duplicate. | ||
|
|
||
| ## Fix | ||
|
|
||
| - Assert BOTH halves of every preset pair and that each `--animate-*` value *composes* its tokens | ||
| (`var(--duration-*)` / `var(--ease-*)`), not merely that it is present - looping over the full | ||
| variant list rather than hand-picking one. | ||
| - Add an occurrence-count guard (`split(needle).length - 1 === 1`) for a keyframe and an | ||
| `--animate-*` in the folded preset, turning the idempotency acceptance item into a real test. | ||
|
|
||
| ## Learning | ||
|
|
||
| **Test at the granularity the spec promises.** For a set of shipped variants, assert *every* | ||
| member, not a representative one - a per-variant loop beats a hand-picked sample. For any property | ||
| an acceptance item names (idempotency, ordering, single-occurrence), write the test that *fails* | ||
| if it regresses; a `toContain` that passes on both the correct and the doubled output is not | ||
| coverage. This is the second coverage-gap review in a row (see | ||
| [`0014-interactive-component-test-coverage.md`](0014-interactive-component-test-coverage.md)): | ||
| the recurring root is testing the happy representative instead of the whole contract. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| # 0033 - Expressive motion tokens (spring easings + motion presets) | ||
|
|
||
| ## Problem | ||
|
|
||
| Canopy's motion scale is deliberately small but currently one-note: three durations | ||
| (`fast/base/slow`) and three easings (`standard/emphasized/decelerate`) that all **decelerate**. | ||
| There is no overshoot/spring curve and no ready-made motion for the small, expressive | ||
| interactions components keep re-inventing inline - a badge popping in, a form field shaking on | ||
| an invalid submit, a toast fading up. Consumers either hardcode `cubic-bezier`/`ms` values (off | ||
| the token seam) or skip the motion entirely. | ||
|
|
||
| transitions.dev (https://github.com/Jakubantalik/transitions.dev) is a well-tuned catalogue of | ||
| exactly these micro-interactions. We want its best, broadly-reusable motion **as Canopy tokens + | ||
| preset utilities**, curated to fit Canopy's minimal, opinionated scale - not a wholesale import. | ||
|
|
||
| Who it is for: component authors (Branches) and app consumers who want tasteful spring/pop/shake | ||
| motion that already composes Canopy's `--duration-*` / `--ease-*` tokens, works out of the box | ||
| from the preset every consumer imports, and honours `motion-reduce`. | ||
|
|
||
| ## Outcome | ||
|
|
||
| - **Two new easing tokens** in `tokens/motion.json` (adapted from transitions.dev's bounce pair): | ||
| - `ease.spring` = `cubic-bezier(0.34, 1.36, 0.64, 1)` - gentle overshoot for playful entrances. | ||
| - `ease.spring-strong` = `cubic-bezier(0.34, 2.2, 0.64, 1)` - pronounced bounce, celebratory | ||
| accents, use sparingly. | ||
| - **Two new duration tokens** extending the scale at both ends (existing `fast/base/slow` | ||
| unchanged, so no collision and no consumer breakage): | ||
| - `duration.micro` = `80ms` - tiny state changes, staggered children, badge/icon pops. | ||
| - `duration.slower` = `480ms` - expressive / spring-driven motion that needs room to settle. | ||
| - **Three motion presets** hand-authored in `preset-motion.css` as `@keyframes` + a `@theme` | ||
| block of `--animate-*` vars that **compose the tokens** (never hardcoded ms/easing), matching | ||
| the existing `animate-dialog-*` / `animate-drawer-*` / `animate-bottom-sheet-*` pattern exactly: | ||
| - `animate-pop-in` / `animate-pop-out` - scale + fade; `-in` springs, `-out` settles. | ||
| - `animate-shake` - short horizontal shake for error/invalid states. | ||
| - `animate-fade-in` / `animate-fade-out` - pure opacity for toasts/content reveal. | ||
| - All five outputs stay in lockstep: the new tokens appear in `dist/tokens.css` (`:root` vars), | ||
| `dist/tokens.ts` (typed export), and `dist/tailwind-preset.css` (`@theme inline`), and the new | ||
| `animate-*` utilities ship folded into `dist/tailwind-preset.css` from `preset-motion.css`. | ||
| - `pnpm test` + `pnpm lint` + `pnpm build` stay green in `packages/roots`; README motion section | ||
| and `docs/overview/` (`features`, `architecture`) updated to match reality. | ||
|
|
||
| ## Scope | ||
|
|
||
| In: | ||
|
|
||
| - `tokens/motion.json` - add `duration.micro`, `duration.slower`, `ease.spring`, | ||
| `ease.spring-strong` with `$description`s. | ||
| - `preset-motion.css` - add the `pop-*` / `shake` / `fade-*` keyframes and their token-composed | ||
| `--animate-*` theme vars; extend the file's header comment to cover the new presets. | ||
| - `tokens.test.ts` - assert the new tokens land in each output and the new `animate-*` utilities | ||
| ship token-composed (grep the built preset, per the existing motion test). | ||
| - README motion section + `docs/overview/features.md` / `architecture.md`. | ||
|
|
||
| Out: | ||
|
|
||
| - `success-check` and other **multi-element composites** from transitions.dev (fade+rotate+blur+ | ||
| bob+SVG-path-draw). A single `--animate-*` utility is one keyframe timeline on one element; the | ||
| check's path-draw needs `stroke-dashoffset` on a nested SVG path. That is component motion, not | ||
| a token - a future Branches component can compose these tokens for it. Noted as future work. | ||
| - Distance / scale / blur helper tokens from transitions.dev. Canopy authors those inline per | ||
| component today; adding a parallel token family is a separate decision, not this spec. | ||
| - Changing or renaming any existing token (`fast/base/slow`, `standard/emphasized/decelerate`) | ||
| or existing `animate-dialog-*` / `-drawer-*` / `-bottom-sheet-*` utility. Purely additive. | ||
| - Retrofitting existing components to use the new presets (each is its own small follow-up). | ||
|
|
||
| ## Approach | ||
|
|
||
| **Tokens.** Append to the existing DTCG groups in `motion.json`. Durations keep file order | ||
| low-to-high (`micro`, `fast`, `base`, `slow`, `slower`); easings append after `decelerate`. The | ||
| Style Dictionary pipeline already runs `cubicBezier/css` + duration transforms, so no config or | ||
| transform change is needed - the new tokens flow through the same three web outputs automatically. | ||
|
|
||
| **Naming.** Canopy easings are named by **intent** (`standard`/`emphasized`/`decelerate`), so | ||
| transitions.dev's `bounce` becomes `spring` (subtle) / `spring-strong` (pronounced). Durations | ||
| keep Canopy's t-shirt scale; transitions.dev's own `fast`(250)/`slow`(400) values are **not** | ||
| reused - they would collide with Canopy's `fast`(120)/`slow`(320). We map intent, not literals. | ||
|
|
||
| **Presets.** Mirror the established partial exactly - a pair of `@keyframes` (`-in`/`-out`) plus | ||
| `@theme { --animate-<name>: <keyframes> var(--duration-*) var(--ease-*); }`. Choices: | ||
|
|
||
| - `pop-in`: `scale(0.9)`+`opacity:0` -> `scale(1)`+`opacity:1`, `var(--duration-base)` | ||
| `var(--ease-spring)` - the flagship showcase of the new spring curve (badges, popovers, menus, | ||
| tooltips). | ||
| - `pop-out`: reverse to `scale(0.96)`+`opacity:0`, `var(--duration-fast)` `var(--ease-standard)` - | ||
| exits settle rather than bounce, matching Canopy's existing `*-out` convention (dialog/drawer | ||
| exits use `--ease-standard`). | ||
| - `shake`: 3 short `translateX` cycles (`+/- 4px`), `var(--duration-slow)` `var(--ease-standard)` - | ||
| for `aria-invalid` form fields; the amplitude stays small and finite so it reads as feedback, | ||
| not decoration. | ||
| - `fade-in` / `fade-out`: pure `opacity`, `var(--duration-base)`/`var(--duration-fast)` | ||
| `var(--ease-standard)` - the most reusable primitive (toasts, content). | ||
|
|
||
| Every consumer gates these with `motion-reduce:animate-none` at the call site (documented in the | ||
| README), same as the dialog/drawer motion - the preset ships the motion; the component opts in. | ||
|
|
||
| **Testing.** Follow the existing motion test: build the preset in-test and grep the **built rule** | ||
| (the literal-class learning - keyframes/`@theme --animate-*` can never come from `@source`), and | ||
| assert each new `--animate-*` value contains the expected `var(--duration-*)`/`var(--ease-*)` so | ||
| motion stays token-driven, not hardcoded. Assert the four new tokens appear in `tokens.css`, | ||
| `tokens.ts`, and the preset's `@theme inline`. | ||
|
|
||
| ## Acceptance | ||
|
|
||
| - [ ] `motion.json` defines `duration.micro` (80ms), `duration.slower` (480ms), `ease.spring` | ||
| (`cubic-bezier(0.34, 1.36, 0.64, 1)`), `ease.spring-strong` | ||
| (`cubic-bezier(0.34, 2.2, 0.64, 1)`) with `$description`s; existing tokens unchanged. | ||
| - [ ] `pnpm build` emits the four new tokens into `dist/tokens.css`, `dist/tokens.ts`, and | ||
| `dist/tailwind-preset.css` (`@theme inline`). | ||
| - [ ] `preset-motion.css` adds `@keyframes` for pop-in/out, shake, fade-in/out and `--animate-*` | ||
| theme vars that COMPOSE `--duration-*` / `--ease-*` (no hardcoded ms/easing); they ship | ||
| folded into `dist/tailwind-preset.css`, and existing `animate-dialog-*`/`-drawer-*`/ | ||
| `-bottom-sheet-*` utilities are untouched. | ||
| - [ ] `tokens.test.ts` asserts: the four new tokens in each output; `animate-pop-in` composes | ||
| `var(--duration-base)` + `var(--ease-spring)`; `animate-shake` and `animate-fade-in` ship | ||
| token-composed; the fold stays idempotent (no double-append). | ||
| - [ ] `pnpm test` + `pnpm lint` green in `packages/roots`. | ||
| - [ ] README motion section documents the new easings, durations, and `animate-*` utilities | ||
| (with the `motion-reduce` gating note); `docs/overview/features.md` + `architecture.md` | ||
| updated to match. `success-check` listed as future component-level work. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.