transitions-refine 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/.agents/skills/refine-live/SKILL.md +205 -0
  2. package/.agents/skills/transitions-dev/01-card-resize.md +53 -0
  3. package/.agents/skills/transitions-dev/02-number-pop-in.md +119 -0
  4. package/.agents/skills/transitions-dev/03-notification-badge.md +110 -0
  5. package/.agents/skills/transitions-dev/04-text-states-swap.md +97 -0
  6. package/.agents/skills/transitions-dev/05-menu-dropdown.md +105 -0
  7. package/.agents/skills/transitions-dev/06-modal.md +94 -0
  8. package/.agents/skills/transitions-dev/07-panel-reveal.md +81 -0
  9. package/.agents/skills/transitions-dev/08-page-side-by-side.md +100 -0
  10. package/.agents/skills/transitions-dev/09-icon-swap.md +78 -0
  11. package/.agents/skills/transitions-dev/10-success-check.md +169 -0
  12. package/.agents/skills/transitions-dev/11-avatar-group-hover.md +200 -0
  13. package/.agents/skills/transitions-dev/12-error-state-shake.md +202 -0
  14. package/.agents/skills/transitions-dev/13-input-clear-dissolve.md +276 -0
  15. package/.agents/skills/transitions-dev/14-skeleton-reveal.md +149 -0
  16. package/.agents/skills/transitions-dev/15-shimmer-text.md +95 -0
  17. package/.agents/skills/transitions-dev/16-tabs-sliding.md +146 -0
  18. package/.agents/skills/transitions-dev/17-tooltip.md +103 -0
  19. package/.agents/skills/transitions-dev/18-texts-reveal.md +110 -0
  20. package/.agents/skills/transitions-dev/19-card-tilt.md +170 -0
  21. package/.agents/skills/transitions-dev/20-plus-menu-morph.md +167 -0
  22. package/.agents/skills/transitions-dev/21-accordion.md +124 -0
  23. package/.agents/skills/transitions-dev/SKILL.md +225 -0
  24. package/.agents/skills/transitions-dev/_root.css +204 -0
  25. package/README.md +89 -0
  26. package/bin/cli.mjs +264 -0
  27. package/demo.html +2531 -0
  28. package/package.json +37 -0
  29. package/server/inject.mjs +116 -0
  30. package/server/motion-tokens.mjs +106 -0
  31. package/server/refine-agent.mjs +86 -0
  32. package/server/relay.mjs +421 -0
@@ -0,0 +1,205 @@
1
+ ---
2
+ name: refine-live
3
+ description: Become the live "Refine" agent for the Timeline Inspector. Use when the user runs `/refine live`, asks to "refine live", "go live", "answer refine jobs", or wants the timeline panel's Refine button (LLM mode) to be backed by a real agent. Long-polls the local refine relay, reasons about each CSS transition with the transitions-dev skill, and posts suggestions back to the browser panel.
4
+ ---
5
+
6
+ # Refine Live
7
+
8
+ Turn yourself into the LLM behind the Timeline Inspector's **Refine** button. While
9
+ this loop runs, the panel's **LLM** tab is "available": each click sends one
10
+ transition here, you reason about it, and your suggestions appear in the panel.
11
+
12
+ You are the poller. Nothing is installed — you just talk to a small local relay
13
+ (default `http://localhost:7331`) that the `npx` injector already started.
14
+
15
+ ## How it works
16
+
17
+ ```
18
+ Browser (Refine, LLM tab) ──POST /jobs──► relay ──GET /jobs/next──► YOU
19
+ ◄──GET /jobs/:id── relay ◄──POST /jobs/:id/result── YOU
20
+ ```
21
+
22
+ ## The loop — stay live for the whole session
23
+
24
+ **Keep polling continuously until the user explicitly stops you.** This is the
25
+ only thing that keeps the panel's LLM tab "available", so do not give up on idle.
26
+ A long stretch of `204` responses is *normal and expected* — it just means no one
27
+ has clicked Refine yet. Re-poll immediately every time; never treat repeated
28
+ `204`s as a reason to stop. The relay reports the agent as "available" for ~120s
29
+ after your last poll, so as long as you keep looping you stay live and the user
30
+ never has to re-run `/refine live`.
31
+
32
+ 1. **Claim the next job (long-poll).** This call blocks up to ~25s, then returns.
33
+
34
+ ```bash
35
+ curl -s http://localhost:7331/jobs/next
36
+ ```
37
+
38
+ - HTTP `204` / empty body → no work yet. Immediately call it again.
39
+ - HTTP `200` with JSON → a job. Shape:
40
+
41
+ ```json
42
+ {
43
+ "id": "uuid",
44
+ "request": {
45
+ "label": "Resize + Color",
46
+ "selector": ".box-resize",
47
+ "mode": "llm",
48
+ "refineType": "small",
49
+ "timings": [
50
+ { "property": "width", "durationMs": 400, "delayMs": 0, "easing": "ease-out" },
51
+ { "property": "background", "durationMs": 400, "delayMs": 0, "easing": "ease-out" }
52
+ ]
53
+ }
54
+ }
55
+ ```
56
+
57
+ - `refineType` chooses what kinds of suggestions to make (it mirrors the
58
+ panel's two tabs):
59
+ - `"small"` (or missing) → **Small refinements**: nudge the existing
60
+ declarations toward the motion tokens (step 3a) **and**, when it's possible
61
+ and sensible, *also* suggest swapping the whole transition for a
62
+ transitions.dev recipe (step 3b).
63
+ - `"replace"` → **Replace transition**: suggest a whole-transition recipe
64
+ swap **only** (step 3b). Do **not** propose motion-token tweaks — skip
65
+ step 3a entirely.
66
+
67
+ 2. **(Optional) post progress** so the panel shows what you're doing:
68
+
69
+ ```bash
70
+ curl -s -X POST http://localhost:7331/jobs/<id>/status \
71
+ -H 'Content-Type: application/json' \
72
+ -d '{"message":"Matching to transitions.dev motion tokens…"}'
73
+ ```
74
+
75
+ 3. **Reason about the transition.** Read `transitions-dev` and apply its
76
+ `transitions refine` behaviour. Which steps you run depends on `refineType`:
77
+ - `refineType === "small"` → do step 3a **and** step 3b.
78
+ - `refineType === "replace"` → do step 3b **only** (skip 3a — no token tweaks).
79
+
80
+ First, infer each declaration's **usage** from `label` + `selector` (modal
81
+ close, dropdown open, tooltip, badge, resize, color/theme change…). Every
82
+ decision below keys off that usage — match on **intent, not the nearest
83
+ number**.
84
+
85
+ **3a. Motion-token tweaks (only for `refineType === "small"`).** Using
86
+ `## Motion tokens`:
87
+ - Pick the motion token that fits the usage — a modal close wants a fast exit;
88
+ a color/theme change can be slower; spring/back easings suit playful slides,
89
+ not opacity.
90
+ - Only propose a change where the current value actually differs.
91
+
92
+ **3b. Replace the whole transition (for `small` when sensible, always
93
+ considered for `replace`).** Judge whether this transition would be better off
94
+ re-built as one of the twenty-one transitions.dev recipes:
95
+ - The `transitions-dev` skill stays in the loop — run its `## Decision rules`
96
+ against the inferred usage to pick the **single** best-fit recipe, then open
97
+ that recipe's reference file (e.g. `06-modal.md`, `05-menu-dropdown.md`) to
98
+ read its real timings, easing, distance, scale, and blur.
99
+ - Only propose a replacement when it is **possible and sensible**: the current
100
+ declarations are clearly a hand-rolled version of a catalogued recipe, or are
101
+ missing the structure the usage calls for (e.g. an opacity-only "modal" that
102
+ should scale, a width tween that should be the card-resize recipe). If the
103
+ transition already *is* the right recipe, or no recipe genuinely fits, **do
104
+ not** force one.
105
+ - For `refineType === "small"`: skip the replace suggestion and let the 3a
106
+ token tweaks stand alone.
107
+ - For `refineType === "replace"`: there are no token tweaks to fall back on,
108
+ so return an **empty** `suggestions` array with a short `summary` saying the
109
+ transition already fits / no recipe applies.
110
+ - Emit at most **one** `kind: "replace"` suggestion per job. For `small` it sits
111
+ alongside the token tweaks (don't drop those); for `replace` it is the only
112
+ suggestion.
113
+ - Make its `patch` apply what the panel *can* apply live — the recipe's
114
+ recommended duration/easing for the property that already transitions (or
115
+ `"all"`). Name the recipe and its reference file in `title` + `reason` so the
116
+ user knows the structural parts (keyframes, extra properties, JS hooks) come
117
+ from running `transitions apply <recipe>` / pasting that reference file. Never
118
+ invent timings — quote the ones from the reference file.
119
+
120
+ 4. **Post the result** (this completes the job and renders cards in the panel):
121
+
122
+ ```bash
123
+ curl -s -X POST http://localhost:7331/jobs/<id>/result \
124
+ -H 'Content-Type: application/json' \
125
+ -d '{
126
+ "summary": "Tightened the resize and softened the color fade.",
127
+ "suggestions": [
128
+ {
129
+ "id": "width-duration",
130
+ "kind": "duration",
131
+ "property": "width",
132
+ "title": "Duration → Snappy (250ms)",
133
+ "from": "400ms",
134
+ "to": "250ms",
135
+ "patch": { "property": "width", "durationMs": 250 },
136
+ "reason": "A size change reads as direct manipulation — snappy is more responsive than 400ms."
137
+ }
138
+ ]
139
+ }'
140
+ ```
141
+
142
+ The example above is a `small` job (token tweaks). When a recipe genuinely
143
+ fits, include a `kind: "replace"` card — alongside the token tweaks for
144
+ `small`, or as the **only** suggestion for `replace`:
145
+
146
+ ```json
147
+ {
148
+ "id": "replace-card-resize",
149
+ "kind": "replace",
150
+ "property": "width",
151
+ "title": "Replace with Card resize",
152
+ "from": "hand-rolled width tween",
153
+ "to": "transitions.dev · Card resize",
154
+ "patch": { "property": "width", "durationMs": 250, "easing": "cubic-bezier(0.22, 1, 0.36, 1)" },
155
+ "reference": "transitions-dev/01-card-resize.md",
156
+ "reason": "This is a width tween on layout change — the Card resize recipe handles it properly. Apply nudges the live timing; paste 01-card-resize.md (run `transitions apply card-resize`) for the full recipe."
157
+ }
158
+ ```
159
+
160
+ If nothing should change, post `"suggestions": []` with a short `summary`.
161
+ If something goes wrong, report it instead:
162
+
163
+ ```bash
164
+ curl -s -X POST http://localhost:7331/jobs/<id>/error \
165
+ -H 'Content-Type: application/json' -d '{"message":"…"}'
166
+ ```
167
+
168
+ 5. **Go back to step 1.** Keep looping indefinitely. **Only stop when the user
169
+ explicitly tells you to** (e.g. "stop refine", "exit live"). Do not stop just
170
+ because it's been quiet — idle is the normal state between clicks. If you do
171
+ stop, tell them the LLM tab will go unavailable and how to restart
172
+ (`/refine live`).
173
+
174
+ ## Suggestion shape (must match the panel)
175
+
176
+ Each suggestion object:
177
+
178
+ | field | meaning |
179
+ | --- | --- |
180
+ | `id` | unique within the job (e.g. `"width-duration"`) — used to track "Applied" |
181
+ | `kind` | `"duration"` \| `"delay"` \| `"easing"` for token tweaks, or `"replace"` for a whole-transition swap (drives the card label) |
182
+ | `property` | the CSS property this targets, or `"all"` |
183
+ | `title` | short label shown on the card |
184
+ | `from` / `to` | human-readable before → after |
185
+ | `patch` | **what actually gets applied** — `{ "property", "durationMs"?, "delayMs"?, "easing"? }`. Include only changed fields; `property` must match an input property (or `"all"`). For a `replace`, use the chosen recipe's recommended timing here so Apply still does something live. |
186
+ | `reference` | *(replace only, optional)* the transitions.dev reference file the user should paste for the full recipe, e.g. `"transitions-dev/06-modal.md"`. |
187
+ | `reason` | one sentence of *why*, in usage terms |
188
+
189
+ The panel applies `patch` live in the browser via the property override. Values
190
+ are not written to source files — the user copies the ones they keep.
191
+
192
+ ## Notes
193
+
194
+ - Relay port: `http://localhost:7331` unless `REFINE_RELAY_PORT` was changed.
195
+ - Only **LLM**-mode jobs reach you; **Deterministic**-mode jobs are answered by
196
+ the relay itself (nearest-token snapping) and never appear here. Whole-transition
197
+ **replace** suggestions are therefore LLM-only — the deterministic path can't
198
+ infer usage well enough to pick a recipe, so a Deterministic + "Replace
199
+ transition" job just returns an empty result pointing the user back to the Agent
200
+ tab.
201
+ - A `replace` card's Apply only changes the live timing in the patch. The recipe's
202
+ structural parts (keyframes, extra properties, JS hooks) aren't applied in the
203
+ browser — that's why the card points the user at the reference file to paste.
204
+ - The relay errors a waiting job after ~120s, so answer promptly once you claim
205
+ one. The long-poll itself returning `204` is normal — just poll again.
@@ -0,0 +1,53 @@
1
+ # Card resize
2
+
3
+ ## When to use
4
+
5
+ Tweening a container's width or height when its layout state changes (compact ↔ expanded card, collapsing panel, list row toggling extra detail). Pure CSS — no JS required beyond the class toggle that drives the size change.
6
+
7
+ ## HTML usage
8
+
9
+ ```html
10
+ <div class="t-resize">…</div>
11
+ ```
12
+
13
+ Put `.t-resize` on any element and change its width/height
14
+ (directly, or via a state class such as `.is-small`). The
15
+ transition will tween the two sizes.
16
+
17
+ ## Tunable variables
18
+
19
+ | Variable | Default | Notes |
20
+ | --- | --- | --- |
21
+ | `--resize-dur` | `300ms` | sourced from `--p4-dur` |
22
+ | `--resize-ease` | `cubic-bezier(0.22, 1, 0.36, 1)` | sourced from `--p4-ease` |
23
+
24
+ The `:root` defaults below match the live tuning on [transitions.dev](https://transitions.dev). Drop them into your global stylesheet once — every transition in this skill reads from semantic names like these, so multiple transitions can share a single `:root` block.
25
+
26
+ ```css
27
+ :root {
28
+ --resize-dur: 300ms;
29
+ --resize-ease: cubic-bezier(0.22, 1, 0.36, 1);
30
+ }
31
+ ```
32
+
33
+ ## CSS
34
+
35
+ ```css
36
+ .t-resize {
37
+ transition:
38
+ width var(--resize-dur) var(--resize-ease),
39
+ height var(--resize-dur) var(--resize-ease);
40
+ will-change: width, height;
41
+ }
42
+
43
+ @media (prefers-reduced-motion: reduce) {
44
+ .t-resize { transition: none !important; }
45
+ }
46
+ ```
47
+
48
+ The `@media (prefers-reduced-motion: reduce)` guard at the bottom of the snippet is required — keep it. It zeroes the transition for users who have asked for less motion at the OS level.
49
+
50
+ ## JavaScript orchestration
51
+
52
+ None — pure CSS. Toggle the documented HTML attributes or class names from whatever already drives state in your app.
53
+
@@ -0,0 +1,119 @@
1
+ # Number pop-in
2
+
3
+ ## When to use
4
+
5
+ Counters, prices, balances, or any number that updates and should re-enter from a direction with blur. Each character animates independently and the last two digits stagger so decimals feel alive without looking chaotic.
6
+
7
+ ## HTML usage
8
+
9
+ ```html
10
+ <span class="t-digit-group is-animating">
11
+ <span class="t-digit">1</span>
12
+ <span class="t-digit">2</span>
13
+ <span class="t-digit" data-stagger="1">.</span>
14
+ <span class="t-digit" data-stagger="2">3</span>
15
+ </span>
16
+ ```
17
+
18
+ Replay:
19
+ - Remove `.is-animating`, re-render digits (or swap text),
20
+ force a reflow, then re-add `.is-animating`.
21
+ - Use data-stagger="1", "2", … to delay individual
22
+ digits by `n * var(--digit-stagger)`.
23
+
24
+ Direction:
25
+ --digit-dir-x / --digit-dir-y are unit-less multipliers
26
+ (e.g. 1, -1, 0) applied to --digit-distance.
27
+
28
+ ## Tunable variables
29
+
30
+ | Variable | Default | Notes |
31
+ | --- | --- | --- |
32
+ | `--digit-dur` | `500ms` | sourced from `--p9-dur` |
33
+ | `--digit-distance` | `8px` | sourced from `--p9-distance` |
34
+ | `--digit-stagger` | `70ms` | sourced from `--p9-stagger` |
35
+ | `--digit-blur` | `2px` | sourced from `--p9-blur` |
36
+ | `--digit-ease` | `cubic-bezier(0.34, 1.45, 0.64, 1)` | sourced from `--p9-ease` |
37
+ | `--digit-dir-x` | `0` | sourced from `--p9-dir-x` |
38
+ | `--digit-dir-y` | `1` | sourced from `--p9-dir-y` |
39
+
40
+ The `:root` defaults below match the live tuning on [transitions.dev](https://transitions.dev). Drop them into your global stylesheet once — every transition in this skill reads from semantic names like these, so multiple transitions can share a single `:root` block.
41
+
42
+ ```css
43
+ :root {
44
+ --digit-dur: 500ms;
45
+ --digit-distance: 8px;
46
+ --digit-stagger: 70ms;
47
+ --digit-blur: 2px;
48
+ --digit-ease: cubic-bezier(0.34, 1.45, 0.64, 1);
49
+ --digit-dir-x: 0;
50
+ --digit-dir-y: 1;
51
+ }
52
+ ```
53
+
54
+ ## CSS
55
+
56
+ ```css
57
+ @keyframes t-digit-pop-in {
58
+ 0% {
59
+ transform: translate(
60
+ calc(var(--digit-distance) * var(--digit-dir-x)),
61
+ calc(var(--digit-distance) * var(--digit-dir-y))
62
+ );
63
+ opacity: 0;
64
+ filter: blur(var(--digit-blur));
65
+ }
66
+ 100% { transform: translate(0, 0); opacity: 1; filter: blur(0); }
67
+ }
68
+
69
+ .t-digit-group {
70
+ display: inline-flex;
71
+ align-items: baseline;
72
+ }
73
+ .t-digit {
74
+ display: inline-block;
75
+ will-change: transform, opacity, filter;
76
+ }
77
+ .t-digit-group.is-animating .t-digit {
78
+ animation: t-digit-pop-in var(--digit-dur) var(--digit-ease) both;
79
+ }
80
+ .t-digit-group.is-animating .t-digit[data-stagger="1"] {
81
+ animation-delay: var(--digit-stagger);
82
+ }
83
+ .t-digit-group.is-animating .t-digit[data-stagger="2"] {
84
+ animation-delay: calc(var(--digit-stagger) * 2);
85
+ }
86
+
87
+ @media (prefers-reduced-motion: reduce) {
88
+ .t-digit-group .t-digit { animation: none !important; }
89
+ }
90
+ ```
91
+
92
+ The `@media (prefers-reduced-motion: reduce)` guard at the bottom of the snippet is required — keep it. It zeroes the transition for users who have asked for less motion at the OS level.
93
+
94
+ ## JavaScript orchestration
95
+
96
+ ```js
97
+ // Replay the digit pop-in: remove .is-animating, swap the digit spans,
98
+ // force a reflow, then re-add .is-animating. Mark the last two digits
99
+ // with data-stagger="1" / "2" so they ride in 1× / 2× --digit-stagger
100
+ // behind the leading digits.
101
+ const group = document.querySelector(".t-digit-group");
102
+
103
+ function setDigits(str) {
104
+ group.classList.remove("is-animating");
105
+ group.replaceChildren();
106
+ const chars = str.split("");
107
+ chars.forEach((ch, i) => {
108
+ const span = document.createElement("span");
109
+ span.className = "t-digit";
110
+ span.textContent = ch;
111
+ if (i === chars.length - 2) span.dataset.stagger = "1";
112
+ else if (i === chars.length - 1) span.dataset.stagger = "2";
113
+ group.appendChild(span);
114
+ });
115
+ void group.offsetHeight; // force reflow
116
+ group.classList.add("is-animating");
117
+ }
118
+ ```
119
+
@@ -0,0 +1,110 @@
1
+ # Notification badge
2
+
3
+ ## When to use
4
+
5
+ A small badge appearing on top of a trigger (bell, inbox, button). Slides in diagonally and pops the dot independently of the trigger so the trigger itself never moves.
6
+
7
+ ## HTML usage
8
+
9
+ ```html
10
+ <!-- Place .t-badge inside your trigger (bell icon, button, etc.). -->
11
+ <!-- The trigger must be position: relative so .t-badge can anchor to it. -->
12
+ <button class="your-trigger" style="position: relative">
13
+ <!-- your icon / trigger contents -->
14
+ <span class="t-badge" data-open="false">
15
+ <span class="t-badge-dot">1</span>
16
+ </span>
17
+ </button>
18
+ ```
19
+
20
+ State: toggle data-open="true" / "false" on .t-badge.
21
+ Only the badge slides + pops — the trigger itself stays put.
22
+
23
+ ## Tunable variables
24
+
25
+ | Variable | Default | Notes |
26
+ | --- | --- | --- |
27
+ | `--badge-slide-dur` | `260ms` | sourced from `--p1-pos-open-dur` |
28
+ | `--badge-pop-dur` | `500ms` | sourced from `--p1-scale-open-dur` |
29
+ | `--badge-pop-close-dur` | `180ms` | sourced from `--p1-scale-close-dur` |
30
+ | `--badge-fade-dur` | `400ms` | sourced from `--p1-opacity-open-dur` |
31
+ | `--badge-fade-close-dur` | `180ms` | sourced from `--p1-opacity-close-dur` |
32
+ | `--badge-blur` | `2px` | sourced from `--p1-blur` |
33
+ | `--badge-offset-x` | `-8.2px` | sourced from `--p1-distance-x` |
34
+ | `--badge-offset-y` | `12.4px` | sourced from `--p1-distance-y` |
35
+ | `--badge-slide-ease` | `cubic-bezier(0.22, 1, 0.36, 1)` | sourced from `--p1-ease-pos-open` |
36
+ | `--badge-pop-ease` | `cubic-bezier(0.34, 1.36, 0.64, 1)` | sourced from `--p1-ease-scale-open` |
37
+ | `--badge-close-ease` | `cubic-bezier(0.4, 0, 0.2, 1)` | sourced from `--p1-ease-close` |
38
+
39
+ The `:root` defaults below match the live tuning on [transitions.dev](https://transitions.dev). Drop them into your global stylesheet once — every transition in this skill reads from semantic names like these, so multiple transitions can share a single `:root` block.
40
+
41
+ ```css
42
+ :root {
43
+ --badge-slide-dur: 260ms;
44
+ --badge-pop-dur: 500ms;
45
+ --badge-pop-close-dur: 180ms;
46
+ --badge-fade-dur: 400ms;
47
+ --badge-fade-close-dur: 180ms;
48
+ --badge-blur: 2px;
49
+ --badge-offset-x: -8.2px;
50
+ --badge-offset-y: 12.4px;
51
+ --badge-slide-ease: cubic-bezier(0.22, 1, 0.36, 1);
52
+ --badge-pop-ease: cubic-bezier(0.34, 1.36, 0.64, 1);
53
+ --badge-close-ease: cubic-bezier(0.4, 0, 0.2, 1);
54
+ }
55
+ ```
56
+
57
+ ## CSS
58
+
59
+ ```css
60
+ @keyframes t-badge-slide-in {
61
+ from { transform: translate(var(--badge-offset-x), var(--badge-offset-y)); }
62
+ to { transform: translate(0, 0); }
63
+ }
64
+
65
+ /* .t-badge is the absolutely-positioned wrapper for the dot.
66
+ Adjust top/right (or left/bottom) to anchor it on your trigger. */
67
+ .t-badge {
68
+ position: absolute;
69
+ top: -6px;
70
+ right: -8px;
71
+ pointer-events: none;
72
+ will-change: transform;
73
+ }
74
+ .t-badge[data-open="true"] {
75
+ animation: t-badge-slide-in var(--badge-slide-dur) var(--badge-slide-ease);
76
+ }
77
+
78
+ .t-badge-dot {
79
+ display: block;
80
+ transform-origin: center;
81
+ transform: scale(1);
82
+ opacity: 1;
83
+ filter: blur(0);
84
+ transition:
85
+ transform var(--badge-pop-dur) var(--badge-pop-ease),
86
+ opacity var(--badge-fade-dur) var(--badge-pop-ease),
87
+ filter var(--badge-pop-dur) var(--badge-pop-ease);
88
+ will-change: transform, opacity, filter;
89
+ }
90
+ .t-badge[data-open="false"] .t-badge-dot {
91
+ transform: scale(0);
92
+ opacity: 0;
93
+ filter: blur(var(--badge-blur));
94
+ transition:
95
+ transform var(--badge-pop-close-dur) var(--badge-close-ease),
96
+ opacity var(--badge-fade-close-dur) var(--badge-close-ease),
97
+ filter var(--badge-pop-close-dur) var(--badge-close-ease);
98
+ }
99
+
100
+ @media (prefers-reduced-motion: reduce) {
101
+ .t-badge, .t-badge-dot { animation: none !important; transition: none !important; }
102
+ }
103
+ ```
104
+
105
+ The `@media (prefers-reduced-motion: reduce)` guard at the bottom of the snippet is required — keep it. It zeroes the transition for users who have asked for less motion at the OS level.
106
+
107
+ ## JavaScript orchestration
108
+
109
+ None — pure CSS. Toggle the documented HTML attributes or class names from whatever already drives state in your app.
110
+
@@ -0,0 +1,97 @@
1
+ # Text states swap
2
+
3
+ ## When to use
4
+
5
+ Swapping the text of a status indicator in place — "Processing…" → "Done", "Save" → "Saved". The old text exits up with blur, the new text enters from below.
6
+
7
+ ## HTML usage
8
+
9
+ ```html
10
+ <span class="t-text-swap">Processing…</span>
11
+ ```
12
+
13
+ Driven by JS (three-phase sequence):
14
+ 1. Add `.is-exit` -> old text slides up + blurs + fades.
15
+ 2. After --text-swap-dur: change textContent, then add
16
+ `.is-enter-start` (jumps to below, no transition).
17
+ 3. Force reflow, remove `.is-enter-start` so the new text
18
+ animates back to 0 with the default transition.
19
+
20
+ ## Tunable variables
21
+
22
+ | Variable | Default | Notes |
23
+ | --- | --- | --- |
24
+ | `--text-swap-dur` | `150ms` | sourced from `--p6-dur` |
25
+ | `--text-swap-translate-y` | `4px` | sourced from `--p6-translate-y` |
26
+ | `--text-swap-blur` | `2px` | sourced from `--p6-blur` |
27
+ | `--text-swap-ease` | `ease-in-out` | sourced from `--p6-ease` |
28
+
29
+ The `:root` defaults below match the live tuning on [transitions.dev](https://transitions.dev). Drop them into your global stylesheet once — every transition in this skill reads from semantic names like these, so multiple transitions can share a single `:root` block.
30
+
31
+ ```css
32
+ :root {
33
+ --text-swap-dur: 150ms;
34
+ --text-swap-translate-y: 4px;
35
+ --text-swap-blur: 2px;
36
+ --text-swap-ease: ease-in-out;
37
+ }
38
+ ```
39
+
40
+ ## CSS
41
+
42
+ ```css
43
+ .t-text-swap {
44
+ display: inline-block;
45
+ transform: translateY(0);
46
+ filter: blur(0);
47
+ opacity: 1;
48
+ transition:
49
+ transform var(--text-swap-dur) var(--text-swap-ease),
50
+ filter var(--text-swap-dur) var(--text-swap-ease),
51
+ opacity var(--text-swap-dur) var(--text-swap-ease);
52
+ will-change: transform, filter, opacity;
53
+ }
54
+ .t-text-swap.is-exit {
55
+ transform: translateY(calc(var(--text-swap-translate-y) * -1));
56
+ filter: blur(var(--text-swap-blur));
57
+ opacity: 0;
58
+ }
59
+ .t-text-swap.is-enter-start {
60
+ transform: translateY(var(--text-swap-translate-y));
61
+ filter: blur(var(--text-swap-blur));
62
+ opacity: 0;
63
+ transition: none;
64
+ }
65
+
66
+ @media (prefers-reduced-motion: reduce) {
67
+ .t-text-swap { transition: none !important; }
68
+ }
69
+ ```
70
+
71
+ The `@media (prefers-reduced-motion: reduce)` guard at the bottom of the snippet is required — keep it. It zeroes the transition for users who have asked for less motion at the OS level.
72
+
73
+ ## JavaScript orchestration
74
+
75
+ ```js
76
+ // Three-phase text swap:
77
+ // 1. Add .is-exit — old text exits up with blur.
78
+ // 2. After --text-swap-dur, swap textContent and add .is-enter-start
79
+ // (jumps to "below, no transition"), force a reflow.
80
+ // 3. Remove .is-enter-start — new text animates back to rest.
81
+ const el = document.querySelector(".t-text-swap");
82
+ const dur = parseFloat(
83
+ getComputedStyle(document.documentElement).getPropertyValue("--text-swap-dur")
84
+ ) || 200;
85
+
86
+ function swapText(next) {
87
+ el.classList.add("is-exit");
88
+ setTimeout(() => {
89
+ el.textContent = next;
90
+ el.classList.remove("is-exit");
91
+ el.classList.add("is-enter-start");
92
+ void el.offsetHeight; // force reflow so the next change transitions
93
+ el.classList.remove("is-enter-start");
94
+ }, dur);
95
+ }
96
+ ```
97
+
@@ -0,0 +1,105 @@
1
+ # Menu dropdown
2
+
3
+ ## When to use
4
+
5
+ Contextual menus, dropdowns, popovers — anything that opens from a trigger and should visually grow from that trigger's position. Origin-aware via `data-origin` (top-left, top-center, top-right, bottom-*).
6
+
7
+ ## HTML usage
8
+
9
+ ```html
10
+ <div class="t-dropdown" data-origin="top-center">
11
+ <!-- your menu contents -->
12
+ </div>
13
+ ```
14
+
15
+ State:
16
+ - Add `.is-open` to show.
17
+ - On close, swap `.is-open` for `.is-closing`, then remove
18
+ `.is-closing` after --dropdown-close-dur.
19
+
20
+ data-origin values: top-left | top-center | top-right |
21
+ bottom-left | bottom-center | bottom-right.
22
+
23
+ ## Tunable variables
24
+
25
+ | Variable | Default | Notes |
26
+ | --- | --- | --- |
27
+ | `--dropdown-open-dur` | `250ms` | sourced from `--p2-open-dur` |
28
+ | `--dropdown-close-dur` | `150ms` | sourced from `--p2-close-dur` |
29
+ | `--dropdown-pre-scale` | `0.97` | sourced from `--p2-pre-scale` |
30
+ | `--dropdown-closing-scale` | `0.99` | sourced from `--p2-closing-scale` |
31
+ | `--dropdown-ease` | `cubic-bezier(0.22, 1, 0.36, 1)` | sourced from `--p2-ease` |
32
+
33
+ The `:root` defaults below match the live tuning on [transitions.dev](https://transitions.dev). Drop them into your global stylesheet once — every transition in this skill reads from semantic names like these, so multiple transitions can share a single `:root` block.
34
+
35
+ ```css
36
+ :root {
37
+ --dropdown-open-dur: 250ms;
38
+ --dropdown-close-dur: 150ms;
39
+ --dropdown-pre-scale: 0.97;
40
+ --dropdown-closing-scale: 0.99;
41
+ --dropdown-ease: cubic-bezier(0.22, 1, 0.36, 1);
42
+ }
43
+ ```
44
+
45
+ ## CSS
46
+
47
+ ```css
48
+ .t-dropdown {
49
+ transform-origin: top left;
50
+ transform: scale(var(--dropdown-pre-scale));
51
+ opacity: 0;
52
+ pointer-events: none;
53
+ transition:
54
+ transform var(--dropdown-open-dur) var(--dropdown-ease),
55
+ opacity var(--dropdown-open-dur) var(--dropdown-ease);
56
+ will-change: transform, opacity;
57
+ }
58
+ .t-dropdown[data-origin="top-right"] { transform-origin: top right; }
59
+ .t-dropdown[data-origin="top-center"] { transform-origin: top center; }
60
+ .t-dropdown[data-origin="bottom-left"] { transform-origin: bottom left; }
61
+ .t-dropdown[data-origin="bottom-center"] { transform-origin: bottom center; }
62
+ .t-dropdown[data-origin="bottom-right"] { transform-origin: bottom right; }
63
+
64
+ .t-dropdown.is-open {
65
+ transform: scale(1);
66
+ opacity: 1;
67
+ pointer-events: auto;
68
+ }
69
+ .t-dropdown.is-closing {
70
+ transform: scale(var(--dropdown-closing-scale));
71
+ opacity: 0;
72
+ pointer-events: none;
73
+ transition:
74
+ transform var(--dropdown-close-dur) var(--dropdown-ease),
75
+ opacity var(--dropdown-close-dur) var(--dropdown-ease);
76
+ }
77
+
78
+ @media (prefers-reduced-motion: reduce) {
79
+ .t-dropdown { transition: none !important; }
80
+ }
81
+ ```
82
+
83
+ The `@media (prefers-reduced-motion: reduce)` guard at the bottom of the snippet is required — keep it. It zeroes the transition for users who have asked for less motion at the OS level.
84
+
85
+ ## JavaScript orchestration
86
+
87
+ ```js
88
+ // Toggle .is-open / .is-closing with a setTimeout cleanup so the closing
89
+ // scale animates before the element resets to its pre-open rest state.
90
+ const dropdown = document.querySelector(".t-dropdown");
91
+ const closeMs = parseFloat(
92
+ getComputedStyle(document.documentElement).getPropertyValue("--dropdown-close-dur")
93
+ ) || 150;
94
+
95
+ function openDropdown() {
96
+ dropdown.classList.remove("is-closing");
97
+ dropdown.classList.add("is-open");
98
+ }
99
+ function closeDropdown() {
100
+ dropdown.classList.remove("is-open");
101
+ dropdown.classList.add("is-closing");
102
+ setTimeout(() => dropdown.classList.remove("is-closing"), closeMs);
103
+ }
104
+ ```
105
+