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.
- package/.agents/skills/refine-live/SKILL.md +205 -0
- package/.agents/skills/transitions-dev/01-card-resize.md +53 -0
- package/.agents/skills/transitions-dev/02-number-pop-in.md +119 -0
- package/.agents/skills/transitions-dev/03-notification-badge.md +110 -0
- package/.agents/skills/transitions-dev/04-text-states-swap.md +97 -0
- package/.agents/skills/transitions-dev/05-menu-dropdown.md +105 -0
- package/.agents/skills/transitions-dev/06-modal.md +94 -0
- package/.agents/skills/transitions-dev/07-panel-reveal.md +81 -0
- package/.agents/skills/transitions-dev/08-page-side-by-side.md +100 -0
- package/.agents/skills/transitions-dev/09-icon-swap.md +78 -0
- package/.agents/skills/transitions-dev/10-success-check.md +169 -0
- package/.agents/skills/transitions-dev/11-avatar-group-hover.md +200 -0
- package/.agents/skills/transitions-dev/12-error-state-shake.md +202 -0
- package/.agents/skills/transitions-dev/13-input-clear-dissolve.md +276 -0
- package/.agents/skills/transitions-dev/14-skeleton-reveal.md +149 -0
- package/.agents/skills/transitions-dev/15-shimmer-text.md +95 -0
- package/.agents/skills/transitions-dev/16-tabs-sliding.md +146 -0
- package/.agents/skills/transitions-dev/17-tooltip.md +103 -0
- package/.agents/skills/transitions-dev/18-texts-reveal.md +110 -0
- package/.agents/skills/transitions-dev/19-card-tilt.md +170 -0
- package/.agents/skills/transitions-dev/20-plus-menu-morph.md +167 -0
- package/.agents/skills/transitions-dev/21-accordion.md +124 -0
- package/.agents/skills/transitions-dev/SKILL.md +225 -0
- package/.agents/skills/transitions-dev/_root.css +204 -0
- package/README.md +89 -0
- package/bin/cli.mjs +264 -0
- package/demo.html +2531 -0
- package/package.json +37 -0
- package/server/inject.mjs +116 -0
- package/server/motion-tokens.mjs +106 -0
- package/server/refine-agent.mjs +86 -0
- 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
|
+
|