@tamsensedev/runner 0.13.0 → 0.18.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/LICENSE +5 -5
- package/README.md +61 -57
- package/dist/cli.js +646 -315
- package/dist/cli.js.map +1 -1
- package/dist/recorder.cjs.map +1 -1
- package/library/.claude-plugin/plugin.json +5 -5
- package/library/THIRD_PARTY_NOTICES.md +123 -123
- package/library/agents/code-architect.md +67 -67
- package/library/agents/code-explorer.md +61 -61
- package/library/agents/code-reviewer.md +73 -73
- package/library/agents/code-simplifier.md +62 -62
- package/library/agents/design-critic.md +52 -52
- package/library/agents/second-opinion.md +64 -64
- package/library/agents/security-auditor.md +86 -86
- package/library/agents/silent-failure-hunter.md +70 -70
- package/library/agents/test-coverage-analyzer.md +78 -78
- package/library/agents/test-runner.md +48 -48
- package/library/agents/ui-tester.md +50 -50
- package/library/licenses/Apache-2.0.txt +202 -202
- package/library/skills/accessibility/SKILL.md +144 -144
- package/library/skills/accessibility/references/wcag-checks.md +168 -168
- package/library/skills/addressing-review-feedback/SKILL.md +109 -109
- package/library/skills/app-screens/SKILL.md +96 -96
- package/library/skills/app-screens/references/shell-kpis-charts.md +108 -108
- package/library/skills/app-screens/references/states-density-phone.md +54 -54
- package/library/skills/app-screens/references/tables-forms-settings.md +104 -104
- package/library/skills/avoiding-generic-ai-design/SKILL.md +92 -92
- package/library/skills/avoiding-generic-ai-design/references/tells.md +112 -112
- package/library/skills/clean-commits/SKILL.md +85 -85
- package/library/skills/design-review/SKILL.md +107 -107
- package/library/skills/design-review/references/critique-passes.md +101 -101
- package/library/skills/design-review/references/screenshot-procedure.md +121 -121
- package/library/skills/differential-security-review/LICENSE +427 -427
- package/library/skills/differential-security-review/SKILL.md +105 -105
- package/library/skills/differential-security-review/references/adversarial.md +115 -115
- package/library/skills/differential-security-review/references/methodology.md +145 -145
- package/library/skills/differential-security-review/references/patterns.md +108 -108
- package/library/skills/false-positive-check/LICENSE +427 -427
- package/library/skills/false-positive-check/SKILL.md +95 -95
- package/library/skills/false-positive-check/references/bug-class-verification.md +128 -128
- package/library/skills/false-positive-check/references/deep-verification.md +113 -113
- package/library/skills/false-positive-check/references/evidence-templates.md +93 -93
- package/library/skills/false-positive-check/references/false-positive-patterns.md +122 -122
- package/library/skills/false-positive-check/references/gate-reviews.md +36 -36
- package/library/skills/false-positive-check/references/standard-verification.md +80 -80
- package/library/skills/focused-implementation/SKILL.md +104 -104
- package/library/skills/implementation-plan/SKILL.md +66 -66
- package/library/skills/interface-design/SKILL.md +144 -144
- package/library/skills/interface-design/references/craft-defaults.md +130 -130
- package/library/skills/interface-design/references/new-direction.md +62 -62
- package/library/skills/interface-design/references/writing.md +52 -52
- package/library/skills/interface-polish/SKILL.md +103 -103
- package/library/skills/interface-polish/references/detail-rules.md +154 -154
- package/library/skills/interface-polish/references/worst-case-data.md +158 -158
- package/library/skills/motion-design/SKILL.md +106 -106
- package/library/skills/motion-design/references/recipes.md +243 -243
- package/library/skills/postgres-performance/SKILL.md +97 -97
- package/library/skills/postgres-performance/references/rules.md +265 -265
- package/library/skills/project-orientation/SKILL.md +77 -77
- package/library/skills/reviewing-changes/SKILL.md +183 -183
- package/library/skills/safe-database-changes/SKILL.md +111 -111
- package/library/skills/safe-database-changes/references/backfills.md +75 -75
- package/library/skills/safe-database-changes/references/migration-tools.md +69 -69
- package/library/skills/safe-database-changes/references/postgres-ddl.md +199 -199
- package/library/skills/security-review/SKILL.md +132 -132
- package/library/skills/security-review/references/checks-by-area.md +107 -107
- package/library/skills/security-review/references/do-not-report.md +86 -86
- package/library/skills/spec-writing/SKILL.md +144 -144
- package/library/skills/systematic-debugging/SKILL.md +130 -130
- package/library/skills/systematic-debugging/references/flaky-tests.md +51 -51
- package/library/skills/systematic-debugging/references/root-cause-tracing.md +55 -55
- package/library/skills/task-interview/SKILL.md +84 -84
- package/library/skills/test-driven-development/SKILL.md +116 -116
- package/library/skills/test-driven-development/references/writing-good-tests.md +85 -85
- package/library/skills/ui-testing/SKILL.md +102 -102
- package/library/skills/ui-testing/references/headless-check.md +295 -295
- package/library/skills/ui-testing/references/local-stand.md +51 -51
- package/library/skills/ui-testing/references/playwright-patterns.md +134 -134
- package/library/skills/ui-testing/references/recording.md +61 -61
- package/library/skills/verification-before-completion/SKILL.md +113 -113
- package/library/skills/web-interface-guidelines/SKILL.md +202 -202
- package/package.json +46 -46
|
@@ -1,243 +1,243 @@
|
|
|
1
|
-
# Motion recipes
|
|
2
|
-
|
|
3
|
-
Read this when implementing a specific transition. Use the project's existing motion tokens and library if it has them; these recipes are CSS first, then examples in common libraries. Do not add a library to use a recipe.
|
|
4
|
-
|
|
5
|
-
## Tokens
|
|
6
|
-
|
|
7
|
-
```css
|
|
8
|
-
:root {
|
|
9
|
-
--ease-out: cubic-bezier(0.23, 1, 0.32, 1);
|
|
10
|
-
--ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
|
|
11
|
-
--ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);
|
|
12
|
-
--duration-press: 150ms;
|
|
13
|
-
--duration-popover: 180ms;
|
|
14
|
-
--duration-dialog: 240ms;
|
|
15
|
-
}
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
## Reduced motion pattern
|
|
19
|
-
|
|
20
|
-
Define movement only for people who have not asked to reduce it; keep the opacity change for everyone.
|
|
21
|
-
|
|
22
|
-
```css
|
|
23
|
-
.toast { transition: opacity 200ms ease; }
|
|
24
|
-
|
|
25
|
-
@media (prefers-reduced-motion: no-preference) {
|
|
26
|
-
.toast {
|
|
27
|
-
transition:
|
|
28
|
-
opacity 200ms var(--ease-out),
|
|
29
|
-
transform 200ms var(--ease-out);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
## Press feedback
|
|
35
|
-
|
|
36
|
-
```css
|
|
37
|
-
.button {
|
|
38
|
-
transition: transform var(--duration-press) var(--ease-out);
|
|
39
|
-
}
|
|
40
|
-
.button:active:not(:disabled) { transform: scale(0.97); }
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
## Popover or menu (native popover, no JavaScript)
|
|
44
|
-
|
|
45
|
-
```css
|
|
46
|
-
.menu[popover] {
|
|
47
|
-
transform-origin: top left;
|
|
48
|
-
opacity: 0;
|
|
49
|
-
transform: scale(0.97);
|
|
50
|
-
transition:
|
|
51
|
-
opacity var(--duration-popover) var(--ease-out),
|
|
52
|
-
transform var(--duration-popover) var(--ease-out),
|
|
53
|
-
overlay var(--duration-popover) allow-discrete,
|
|
54
|
-
display var(--duration-popover) allow-discrete;
|
|
55
|
-
}
|
|
56
|
-
.menu[popover]:popover-open { opacity: 1; transform: scale(1); }
|
|
57
|
-
|
|
58
|
-
@starting-style {
|
|
59
|
-
.menu[popover]:popover-open { opacity: 0; transform: scale(0.97); }
|
|
60
|
-
}
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
Browsers without `@starting-style` simply show the menu without animation. Set `transform-origin` to the side of the trigger. With a headless library, use the origin variable it exposes (for example `var(--radix-popover-content-transform-origin)` or `var(--transform-origin)`).
|
|
64
|
-
|
|
65
|
-
## Dialog
|
|
66
|
-
|
|
67
|
-
Same pattern on `dialog[open]`, centred origin, `--duration-dialog`, plus a backdrop fade:
|
|
68
|
-
|
|
69
|
-
```css
|
|
70
|
-
dialog::backdrop { background: rgb(0 0 0 / 0.4); transition: opacity var(--duration-dialog) ease; }
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
Exit about 75% of the enter duration.
|
|
74
|
-
|
|
75
|
-
## Drawer or bottom sheet
|
|
76
|
-
|
|
77
|
-
```css
|
|
78
|
-
.sheet {
|
|
79
|
-
transform: translateY(100%);
|
|
80
|
-
transition: transform 300ms var(--ease-drawer);
|
|
81
|
-
}
|
|
82
|
-
.sheet[data-open="true"] { transform: translateY(0); }
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
Percent translations move by the element's own size, so they work at any height. Add `overscroll-behavior: contain` on the sheet's scroll area.
|
|
86
|
-
|
|
87
|
-
## Expand and collapse without animating height
|
|
88
|
-
|
|
89
|
-
```css
|
|
90
|
-
.collapsible {
|
|
91
|
-
display: grid;
|
|
92
|
-
grid-template-rows: 0fr;
|
|
93
|
-
transition: grid-template-rows 250ms var(--ease-out);
|
|
94
|
-
}
|
|
95
|
-
.collapsible[data-open="true"] { grid-template-rows: 1fr; }
|
|
96
|
-
.collapsible > .inner { overflow: hidden; }
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
## Toast
|
|
100
|
-
|
|
101
|
-
Enter from the edge it lives on, leave the same way, a little faster:
|
|
102
|
-
|
|
103
|
-
```css
|
|
104
|
-
.toast { transform: translateY(0); opacity: 1; }
|
|
105
|
-
.toast[data-state="entering"], .toast[data-state="leaving"] { transform: translateY(100%); opacity: 0; }
|
|
106
|
-
.toast[data-state="leaving"] { transition-duration: 150ms; }
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
Pause auto-dismiss timers while the toast is hovered or the tab is hidden.
|
|
110
|
-
|
|
111
|
-
## Icon swap (copy to copied, play to pause)
|
|
112
|
-
|
|
113
|
-
Keep both icons in the DOM, one positioned over the other, and cross-fade:
|
|
114
|
-
|
|
115
|
-
```css
|
|
116
|
-
.icon-swap { position: relative; display: inline-grid; }
|
|
117
|
-
.icon-swap > * {
|
|
118
|
-
grid-area: 1 / 1;
|
|
119
|
-
transition:
|
|
120
|
-
opacity 250ms cubic-bezier(0.2, 0, 0, 1),
|
|
121
|
-
transform 250ms cubic-bezier(0.2, 0, 0, 1),
|
|
122
|
-
filter 250ms cubic-bezier(0.2, 0, 0, 1);
|
|
123
|
-
}
|
|
124
|
-
.icon-swap > [data-hidden="true"] { opacity: 0; transform: scale(0.25); filter: blur(4px); }
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
## Skeleton
|
|
128
|
-
|
|
129
|
-
A flat muted block at the final size. If the project pulses skeletons, use a slow opacity pulse (1.5 to 2s) and stop it under reduced motion. No shimmer gradient sweeping across the screen.
|
|
130
|
-
|
|
131
|
-
## Web Animations API
|
|
132
|
-
|
|
133
|
-
```js
|
|
134
|
-
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
135
|
-
element.animate(
|
|
136
|
-
reduce
|
|
137
|
-
? [{ opacity: 0 }, { opacity: 1 }]
|
|
138
|
-
: [{ opacity: 0, transform: 'translateY(8px)' }, { opacity: 1, transform: 'none' }],
|
|
139
|
-
{ duration: 200, easing: 'cubic-bezier(0.23, 1, 0.32, 1)', fill: 'backwards' },
|
|
140
|
-
)
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
## View Transitions
|
|
144
|
-
|
|
145
|
-
```js
|
|
146
|
-
function update(change) {
|
|
147
|
-
if (!document.startViewTransition) return change()
|
|
148
|
-
document.startViewTransition(change)
|
|
149
|
-
}
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
```css
|
|
153
|
-
::view-transition-old(root),
|
|
154
|
-
::view-transition-new(root) { animation-duration: 200ms; }
|
|
155
|
-
|
|
156
|
-
@media (prefers-reduced-motion: reduce) {
|
|
157
|
-
::view-transition-group(*),
|
|
158
|
-
::view-transition-old(*),
|
|
159
|
-
::view-transition-new(*) { animation: none; }
|
|
160
|
-
}
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
Use them when continuity between two states is the point (a card expanding into its detail), not on every route change. Many routers (Astro, Nuxt, SvelteKit, Next.js) have their own switch for this; use it.
|
|
164
|
-
|
|
165
|
-
## Vue
|
|
166
|
-
|
|
167
|
-
```vue
|
|
168
|
-
<template>
|
|
169
|
-
<Transition name="pop">
|
|
170
|
-
<div v-if="open" class="menu">...</div>
|
|
171
|
-
</Transition>
|
|
172
|
-
</template>
|
|
173
|
-
|
|
174
|
-
<style>
|
|
175
|
-
.pop-enter-active { transition: opacity 180ms var(--ease-out), transform 180ms var(--ease-out); }
|
|
176
|
-
.pop-leave-active { transition: opacity 120ms var(--ease-out), transform 120ms var(--ease-out); }
|
|
177
|
-
.pop-enter-from,
|
|
178
|
-
.pop-leave-to { opacity: 0; transform: scale(0.97); }
|
|
179
|
-
</style>
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
For lists, `<TransitionGroup>` with a `.list-move { transition: transform 200ms var(--ease-out); }` class animates reordering.
|
|
183
|
-
|
|
184
|
-
## Svelte
|
|
185
|
-
|
|
186
|
-
```svelte
|
|
187
|
-
<script>
|
|
188
|
-
import { fly } from 'svelte/transition'
|
|
189
|
-
import { cubicOut } from 'svelte/easing'
|
|
190
|
-
</script>
|
|
191
|
-
|
|
192
|
-
{#if open}
|
|
193
|
-
<div class="menu" transition:fly={{ y: -4, duration: 180, easing: cubicOut }}>...</div>
|
|
194
|
-
{/if}
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
## Motion for React
|
|
198
|
-
|
|
199
|
-
Only if `motion` or `framer-motion` is already installed; import from whichever the project uses.
|
|
200
|
-
|
|
201
|
-
```jsx
|
|
202
|
-
import { AnimatePresence, motion } from 'motion/react'
|
|
203
|
-
|
|
204
|
-
<AnimatePresence initial={false}>
|
|
205
|
-
{open && (
|
|
206
|
-
<motion.div
|
|
207
|
-
key="panel"
|
|
208
|
-
initial={{ opacity: 0, transform: 'scale(0.97)' }}
|
|
209
|
-
animate={{ opacity: 1, transform: 'scale(1)' }}
|
|
210
|
-
exit={{ opacity: 0, transform: 'scale(0.97)', transition: { duration: 0.12 } }}
|
|
211
|
-
transition={{ duration: 0.18, ease: [0.23, 1, 0.32, 1] }}
|
|
212
|
-
/>
|
|
213
|
-
)}
|
|
214
|
-
</AnimatePresence>
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
`initial={false}` stops elements already present on first render from animating in. Full `transform` strings stay on the compositor when the main thread is busy; the `x` and `y` shorthands do not. For gestures and drags, springs (`{ type: 'spring', duration: 0.3, bounce: 0 }`) keep their velocity when interrupted; keep bounce at 0 in product UI. `useReducedMotion()` returns the user's preference.
|
|
218
|
-
|
|
219
|
-
## GSAP
|
|
220
|
-
|
|
221
|
-
Only if GSAP is already installed.
|
|
222
|
-
|
|
223
|
-
```js
|
|
224
|
-
import { gsap } from 'gsap'
|
|
225
|
-
|
|
226
|
-
const mm = gsap.matchMedia()
|
|
227
|
-
mm.add('(prefers-reduced-motion: no-preference)', () => {
|
|
228
|
-
gsap.from('.step', { opacity: 0, y: 12, duration: 0.4, ease: 'power3.out', stagger: 0.06 })
|
|
229
|
-
})
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
`gsap.matchMedia()` reverts the animation when the media query stops matching. Keep scroll-triggered work for marketing pages, and give it a version that leaves content visible.
|
|
233
|
-
|
|
234
|
-
## Drag and gestures
|
|
235
|
-
|
|
236
|
-
- Capture the pointer once dragging starts (`setPointerCapture`) so the drag continues outside the element.
|
|
237
|
-
- Ignore extra touch points after the drag begins.
|
|
238
|
-
- Dismiss on velocity as well as distance (a quick flick is enough).
|
|
239
|
-
- Past a boundary, apply friction instead of a hard stop.
|
|
240
|
-
- On `pointercancel`, `lostpointercapture` and window `blur`, clear the dragging state so the next drag works.
|
|
241
|
-
- Offer a click or keyboard alternative for every gesture.
|
|
242
|
-
|
|
243
|
-
Sources: adapted from emilkowalski/skills emil-design-eng (MIT, changed: examples made framework-neutral, Vue, Svelte and GSAP examples added), jakubkrehel/make-interfaces-feel-better animations reference (MIT, changed: icon swap duration aligned to 250ms, CSS grid variant) and pbakaus/impeccable animate and harden references (Apache-2.0, changed: rewritten).
|
|
1
|
+
# Motion recipes
|
|
2
|
+
|
|
3
|
+
Read this when implementing a specific transition. Use the project's existing motion tokens and library if it has them; these recipes are CSS first, then examples in common libraries. Do not add a library to use a recipe.
|
|
4
|
+
|
|
5
|
+
## Tokens
|
|
6
|
+
|
|
7
|
+
```css
|
|
8
|
+
:root {
|
|
9
|
+
--ease-out: cubic-bezier(0.23, 1, 0.32, 1);
|
|
10
|
+
--ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
|
|
11
|
+
--ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);
|
|
12
|
+
--duration-press: 150ms;
|
|
13
|
+
--duration-popover: 180ms;
|
|
14
|
+
--duration-dialog: 240ms;
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Reduced motion pattern
|
|
19
|
+
|
|
20
|
+
Define movement only for people who have not asked to reduce it; keep the opacity change for everyone.
|
|
21
|
+
|
|
22
|
+
```css
|
|
23
|
+
.toast { transition: opacity 200ms ease; }
|
|
24
|
+
|
|
25
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
26
|
+
.toast {
|
|
27
|
+
transition:
|
|
28
|
+
opacity 200ms var(--ease-out),
|
|
29
|
+
transform 200ms var(--ease-out);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Press feedback
|
|
35
|
+
|
|
36
|
+
```css
|
|
37
|
+
.button {
|
|
38
|
+
transition: transform var(--duration-press) var(--ease-out);
|
|
39
|
+
}
|
|
40
|
+
.button:active:not(:disabled) { transform: scale(0.97); }
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Popover or menu (native popover, no JavaScript)
|
|
44
|
+
|
|
45
|
+
```css
|
|
46
|
+
.menu[popover] {
|
|
47
|
+
transform-origin: top left;
|
|
48
|
+
opacity: 0;
|
|
49
|
+
transform: scale(0.97);
|
|
50
|
+
transition:
|
|
51
|
+
opacity var(--duration-popover) var(--ease-out),
|
|
52
|
+
transform var(--duration-popover) var(--ease-out),
|
|
53
|
+
overlay var(--duration-popover) allow-discrete,
|
|
54
|
+
display var(--duration-popover) allow-discrete;
|
|
55
|
+
}
|
|
56
|
+
.menu[popover]:popover-open { opacity: 1; transform: scale(1); }
|
|
57
|
+
|
|
58
|
+
@starting-style {
|
|
59
|
+
.menu[popover]:popover-open { opacity: 0; transform: scale(0.97); }
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Browsers without `@starting-style` simply show the menu without animation. Set `transform-origin` to the side of the trigger. With a headless library, use the origin variable it exposes (for example `var(--radix-popover-content-transform-origin)` or `var(--transform-origin)`).
|
|
64
|
+
|
|
65
|
+
## Dialog
|
|
66
|
+
|
|
67
|
+
Same pattern on `dialog[open]`, centred origin, `--duration-dialog`, plus a backdrop fade:
|
|
68
|
+
|
|
69
|
+
```css
|
|
70
|
+
dialog::backdrop { background: rgb(0 0 0 / 0.4); transition: opacity var(--duration-dialog) ease; }
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Exit about 75% of the enter duration.
|
|
74
|
+
|
|
75
|
+
## Drawer or bottom sheet
|
|
76
|
+
|
|
77
|
+
```css
|
|
78
|
+
.sheet {
|
|
79
|
+
transform: translateY(100%);
|
|
80
|
+
transition: transform 300ms var(--ease-drawer);
|
|
81
|
+
}
|
|
82
|
+
.sheet[data-open="true"] { transform: translateY(0); }
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Percent translations move by the element's own size, so they work at any height. Add `overscroll-behavior: contain` on the sheet's scroll area.
|
|
86
|
+
|
|
87
|
+
## Expand and collapse without animating height
|
|
88
|
+
|
|
89
|
+
```css
|
|
90
|
+
.collapsible {
|
|
91
|
+
display: grid;
|
|
92
|
+
grid-template-rows: 0fr;
|
|
93
|
+
transition: grid-template-rows 250ms var(--ease-out);
|
|
94
|
+
}
|
|
95
|
+
.collapsible[data-open="true"] { grid-template-rows: 1fr; }
|
|
96
|
+
.collapsible > .inner { overflow: hidden; }
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Toast
|
|
100
|
+
|
|
101
|
+
Enter from the edge it lives on, leave the same way, a little faster:
|
|
102
|
+
|
|
103
|
+
```css
|
|
104
|
+
.toast { transform: translateY(0); opacity: 1; }
|
|
105
|
+
.toast[data-state="entering"], .toast[data-state="leaving"] { transform: translateY(100%); opacity: 0; }
|
|
106
|
+
.toast[data-state="leaving"] { transition-duration: 150ms; }
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Pause auto-dismiss timers while the toast is hovered or the tab is hidden.
|
|
110
|
+
|
|
111
|
+
## Icon swap (copy to copied, play to pause)
|
|
112
|
+
|
|
113
|
+
Keep both icons in the DOM, one positioned over the other, and cross-fade:
|
|
114
|
+
|
|
115
|
+
```css
|
|
116
|
+
.icon-swap { position: relative; display: inline-grid; }
|
|
117
|
+
.icon-swap > * {
|
|
118
|
+
grid-area: 1 / 1;
|
|
119
|
+
transition:
|
|
120
|
+
opacity 250ms cubic-bezier(0.2, 0, 0, 1),
|
|
121
|
+
transform 250ms cubic-bezier(0.2, 0, 0, 1),
|
|
122
|
+
filter 250ms cubic-bezier(0.2, 0, 0, 1);
|
|
123
|
+
}
|
|
124
|
+
.icon-swap > [data-hidden="true"] { opacity: 0; transform: scale(0.25); filter: blur(4px); }
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Skeleton
|
|
128
|
+
|
|
129
|
+
A flat muted block at the final size. If the project pulses skeletons, use a slow opacity pulse (1.5 to 2s) and stop it under reduced motion. No shimmer gradient sweeping across the screen.
|
|
130
|
+
|
|
131
|
+
## Web Animations API
|
|
132
|
+
|
|
133
|
+
```js
|
|
134
|
+
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
135
|
+
element.animate(
|
|
136
|
+
reduce
|
|
137
|
+
? [{ opacity: 0 }, { opacity: 1 }]
|
|
138
|
+
: [{ opacity: 0, transform: 'translateY(8px)' }, { opacity: 1, transform: 'none' }],
|
|
139
|
+
{ duration: 200, easing: 'cubic-bezier(0.23, 1, 0.32, 1)', fill: 'backwards' },
|
|
140
|
+
)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## View Transitions
|
|
144
|
+
|
|
145
|
+
```js
|
|
146
|
+
function update(change) {
|
|
147
|
+
if (!document.startViewTransition) return change()
|
|
148
|
+
document.startViewTransition(change)
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
```css
|
|
153
|
+
::view-transition-old(root),
|
|
154
|
+
::view-transition-new(root) { animation-duration: 200ms; }
|
|
155
|
+
|
|
156
|
+
@media (prefers-reduced-motion: reduce) {
|
|
157
|
+
::view-transition-group(*),
|
|
158
|
+
::view-transition-old(*),
|
|
159
|
+
::view-transition-new(*) { animation: none; }
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Use them when continuity between two states is the point (a card expanding into its detail), not on every route change. Many routers (Astro, Nuxt, SvelteKit, Next.js) have their own switch for this; use it.
|
|
164
|
+
|
|
165
|
+
## Vue
|
|
166
|
+
|
|
167
|
+
```vue
|
|
168
|
+
<template>
|
|
169
|
+
<Transition name="pop">
|
|
170
|
+
<div v-if="open" class="menu">...</div>
|
|
171
|
+
</Transition>
|
|
172
|
+
</template>
|
|
173
|
+
|
|
174
|
+
<style>
|
|
175
|
+
.pop-enter-active { transition: opacity 180ms var(--ease-out), transform 180ms var(--ease-out); }
|
|
176
|
+
.pop-leave-active { transition: opacity 120ms var(--ease-out), transform 120ms var(--ease-out); }
|
|
177
|
+
.pop-enter-from,
|
|
178
|
+
.pop-leave-to { opacity: 0; transform: scale(0.97); }
|
|
179
|
+
</style>
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
For lists, `<TransitionGroup>` with a `.list-move { transition: transform 200ms var(--ease-out); }` class animates reordering.
|
|
183
|
+
|
|
184
|
+
## Svelte
|
|
185
|
+
|
|
186
|
+
```svelte
|
|
187
|
+
<script>
|
|
188
|
+
import { fly } from 'svelte/transition'
|
|
189
|
+
import { cubicOut } from 'svelte/easing'
|
|
190
|
+
</script>
|
|
191
|
+
|
|
192
|
+
{#if open}
|
|
193
|
+
<div class="menu" transition:fly={{ y: -4, duration: 180, easing: cubicOut }}>...</div>
|
|
194
|
+
{/if}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Motion for React
|
|
198
|
+
|
|
199
|
+
Only if `motion` or `framer-motion` is already installed; import from whichever the project uses.
|
|
200
|
+
|
|
201
|
+
```jsx
|
|
202
|
+
import { AnimatePresence, motion } from 'motion/react'
|
|
203
|
+
|
|
204
|
+
<AnimatePresence initial={false}>
|
|
205
|
+
{open && (
|
|
206
|
+
<motion.div
|
|
207
|
+
key="panel"
|
|
208
|
+
initial={{ opacity: 0, transform: 'scale(0.97)' }}
|
|
209
|
+
animate={{ opacity: 1, transform: 'scale(1)' }}
|
|
210
|
+
exit={{ opacity: 0, transform: 'scale(0.97)', transition: { duration: 0.12 } }}
|
|
211
|
+
transition={{ duration: 0.18, ease: [0.23, 1, 0.32, 1] }}
|
|
212
|
+
/>
|
|
213
|
+
)}
|
|
214
|
+
</AnimatePresence>
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
`initial={false}` stops elements already present on first render from animating in. Full `transform` strings stay on the compositor when the main thread is busy; the `x` and `y` shorthands do not. For gestures and drags, springs (`{ type: 'spring', duration: 0.3, bounce: 0 }`) keep their velocity when interrupted; keep bounce at 0 in product UI. `useReducedMotion()` returns the user's preference.
|
|
218
|
+
|
|
219
|
+
## GSAP
|
|
220
|
+
|
|
221
|
+
Only if GSAP is already installed.
|
|
222
|
+
|
|
223
|
+
```js
|
|
224
|
+
import { gsap } from 'gsap'
|
|
225
|
+
|
|
226
|
+
const mm = gsap.matchMedia()
|
|
227
|
+
mm.add('(prefers-reduced-motion: no-preference)', () => {
|
|
228
|
+
gsap.from('.step', { opacity: 0, y: 12, duration: 0.4, ease: 'power3.out', stagger: 0.06 })
|
|
229
|
+
})
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
`gsap.matchMedia()` reverts the animation when the media query stops matching. Keep scroll-triggered work for marketing pages, and give it a version that leaves content visible.
|
|
233
|
+
|
|
234
|
+
## Drag and gestures
|
|
235
|
+
|
|
236
|
+
- Capture the pointer once dragging starts (`setPointerCapture`) so the drag continues outside the element.
|
|
237
|
+
- Ignore extra touch points after the drag begins.
|
|
238
|
+
- Dismiss on velocity as well as distance (a quick flick is enough).
|
|
239
|
+
- Past a boundary, apply friction instead of a hard stop.
|
|
240
|
+
- On `pointercancel`, `lostpointercapture` and window `blur`, clear the dragging state so the next drag works.
|
|
241
|
+
- Offer a click or keyboard alternative for every gesture.
|
|
242
|
+
|
|
243
|
+
Sources: adapted from emilkowalski/skills emil-design-eng (MIT, changed: examples made framework-neutral, Vue, Svelte and GSAP examples added), jakubkrehel/make-interfaces-feel-better animations reference (MIT, changed: icon swap duration aligned to 250ms, CSS grid variant) and pbakaus/impeccable animate and harden references (Apache-2.0, changed: rewritten).
|