@humanspeak/svelte-motion 0.1.8 → 0.1.10
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/README.md +113 -1
- package/dist/components/variantContext.context.d.ts +19 -0
- package/dist/components/variantContext.context.js +29 -0
- package/dist/html/_MotionContainer.svelte +180 -14
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/types.d.ts +63 -8
- package/dist/utils/a11y.d.ts +20 -0
- package/dist/utils/a11y.js +20 -0
- package/dist/utils/animationFrame.d.ts +35 -0
- package/dist/utils/animationFrame.js +54 -0
- package/dist/utils/focus.d.ts +45 -0
- package/dist/utils/focus.js +102 -0
- package/dist/utils/hover.js +30 -2
- package/dist/utils/initial.d.ts +3 -3
- package/dist/utils/initial.js +4 -4
- package/dist/utils/style.js +35 -0
- package/dist/utils/styleObject.d.ts +1 -0
- package/dist/utils/styleObject.js +24 -0
- package/dist/utils/variants.d.ts +82 -0
- package/dist/utils/variants.js +104 -0
- package/package.json +13 -13
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ All standard HTML and SVG elements are supported as motion components (e.g., `mo
|
|
|
30
30
|
|
|
31
31
|
### MotionConfig
|
|
32
32
|
|
|
33
|
-
This package includes support for `MotionConfig`, which allows you to set default motion settings for all child components. See the [
|
|
33
|
+
This package includes support for `MotionConfig`, which allows you to set default motion settings for all child components. See the [React - Motion Config](https://motion.dev/docs/react-motion-config) for more details.
|
|
34
34
|
|
|
35
35
|
```svelte
|
|
36
36
|
<MotionConfig transition={{ duration: 0.5 }}>
|
|
@@ -111,6 +111,7 @@ This package carefully selects its dependencies to provide a robust and maintain
|
|
|
111
111
|
| [HTML Content (0→100 counter)](https://examples.motion.dev/react/html-content) | `/tests/motion/html-content` | [View Example](https://motion.svelte.page/examples/html-content) |
|
|
112
112
|
| [Aspect Ratio](https://examples.motion.dev/react/aspect-ratio) | `/tests/motion/aspect-ratio` | [View Example](https://svelte.dev/playground/1bf60e745fae44f5becb4c830fde9b6e?version=5.38.10) |
|
|
113
113
|
| [Hover + Tap (whileHover + whileTap)](https://examples.motion.dev/react/gestures) | `/tests/motion/hover-and-tap` | [View Example](https://motion.svelte.page/examples/hover-and-tap) |
|
|
114
|
+
| [Focus (whileFocus)](https://motion.dev/docs/react-motion-component#focus) | `/tests/motion/while-focus` | [View Example](https://motion.svelte.page/examples/while-focus) |
|
|
114
115
|
| [Rotate](https://examples.motion.dev/react/rotate) | `/tests/motion/rotate` | [View Example](https://motion.svelte.page/examples/rotate) |
|
|
115
116
|
| [Random - Shiny Button](https://www.youtube.com/watch?v=jcpLprT5F0I) by [@verse\_](https://x.com/verse_) | `/tests/random/shiny-button` | [View Example](https://svelte.dev/playground/96f9e0bf624f4396adaf06c519147450?version=5.38.10) |
|
|
116
117
|
| [Fancy Like Button](https://github.com/DRlFTER/fancyLikeButton) | `/tests/random/fancy-like-button` | [View Example](https://svelte.dev/playground/c34b7e53d41c48b0ab1eaf21ca120c6e?version=5.38.10) |
|
|
@@ -153,6 +154,16 @@ Svelte Motion now supports hover interactions via the `whileHover` prop, similar
|
|
|
153
154
|
- Blur while key is held → fires `onTapCancel`
|
|
154
155
|
- `MotionContainer` sets `tabindex="0"` automatically when `whileTap` is present and no `tabindex`/`tabIndex` is provided.
|
|
155
156
|
|
|
157
|
+
### Focus
|
|
158
|
+
|
|
159
|
+
```svelte
|
|
160
|
+
<motion.button whileFocus={{ scale: 1.05, outline: '2px solid blue' }} />
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
- Animates when the element receives keyboard focus and restores baseline on blur.
|
|
164
|
+
- Callbacks: `onFocusStart`, `onFocusEnd` are supported.
|
|
165
|
+
- Perfect for keyboard navigation and accessibility enhancements.
|
|
166
|
+
|
|
156
167
|
### Animation lifecycle
|
|
157
168
|
|
|
158
169
|
```svelte
|
|
@@ -166,6 +177,77 @@ Svelte Motion now supports hover interactions via the `whileHover` prop, similar
|
|
|
166
177
|
/>
|
|
167
178
|
```
|
|
168
179
|
|
|
180
|
+
## Variants
|
|
181
|
+
|
|
182
|
+
Variants allow you to define named animation states that can be referenced throughout your component tree. They're perfect for creating reusable animations and orchestrating complex sequences.
|
|
183
|
+
|
|
184
|
+
### Basic usage
|
|
185
|
+
|
|
186
|
+
Instead of defining animation objects inline, create a `Variants` object with named states:
|
|
187
|
+
|
|
188
|
+
```svelte
|
|
189
|
+
<script lang="ts">
|
|
190
|
+
import { motion, type Variants } from '@humanspeak/svelte-motion'
|
|
191
|
+
|
|
192
|
+
let isOpen = $state(false)
|
|
193
|
+
|
|
194
|
+
const variants: Variants = {
|
|
195
|
+
open: { opacity: 1, scale: 1 },
|
|
196
|
+
closed: { opacity: 0, scale: 0.8 }
|
|
197
|
+
}
|
|
198
|
+
</script>
|
|
199
|
+
|
|
200
|
+
<motion.div {variants} initial="closed" animate={isOpen ? 'open' : 'closed'}>Click me</motion.div>
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Variant propagation
|
|
204
|
+
|
|
205
|
+
One of the most powerful features is **automatic propagation** through component trees. When a parent changes its animation state, all children with `variants` defined automatically inherit that state:
|
|
206
|
+
|
|
207
|
+
```svelte
|
|
208
|
+
<script lang="ts">
|
|
209
|
+
let isVisible = $state(false)
|
|
210
|
+
|
|
211
|
+
const containerVariants: Variants = {
|
|
212
|
+
visible: { opacity: 1 },
|
|
213
|
+
hidden: { opacity: 0 }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const itemVariants: Variants = {
|
|
217
|
+
visible: { opacity: 1, x: 0 },
|
|
218
|
+
hidden: { opacity: 0, x: -20 }
|
|
219
|
+
}
|
|
220
|
+
</script>
|
|
221
|
+
|
|
222
|
+
<motion.ul variants={containerVariants} initial="hidden" animate={isVisible ? 'visible' : 'hidden'}>
|
|
223
|
+
<!-- Children automatically inherit parent's variant state -->
|
|
224
|
+
<motion.li variants={itemVariants}>Item 1</motion.li>
|
|
225
|
+
<motion.li variants={itemVariants}>Item 2</motion.li>
|
|
226
|
+
<motion.li variants={itemVariants}>Item 3</motion.li>
|
|
227
|
+
</motion.ul>
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
**How it works:**
|
|
231
|
+
|
|
232
|
+
- Parent sets `animate="visible"`
|
|
233
|
+
- Children with `variants` automatically inherit `"visible"` state
|
|
234
|
+
- Each child resolves its own variant definition
|
|
235
|
+
- No need to pass `animate` props to children!
|
|
236
|
+
|
|
237
|
+
### Staggered animations
|
|
238
|
+
|
|
239
|
+
Create staggered animations with transition delays:
|
|
240
|
+
|
|
241
|
+
```svelte
|
|
242
|
+
{#each items as item, i}
|
|
243
|
+
<motion.div variants={itemVariants} transition={{ delay: i * 0.1 }}>
|
|
244
|
+
{item}
|
|
245
|
+
</motion.div>
|
|
246
|
+
{/each}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
See the [Variants documentation](https://motion.svelte.page/docs/variants) for complete details and examples.
|
|
250
|
+
|
|
169
251
|
## Server-side rendering
|
|
170
252
|
|
|
171
253
|
Motion components render their initial state during SSR. The container merges inline `style` with the first values from `initial` (or the first keyframes from `animate` when `initial` is empty) so the server HTML matches the starting appearance. On hydration, components promote to a ready state and animate without flicker.
|
|
@@ -204,6 +286,36 @@ Notes:
|
|
|
204
286
|
<motion.div style={`rotate: ${$rotate}deg`} />
|
|
205
287
|
```
|
|
206
288
|
|
|
289
|
+
### useAnimationFrame(callback)
|
|
290
|
+
|
|
291
|
+
- Runs a callback on every animation frame with the current timestamp.
|
|
292
|
+
- The callback receives a `DOMHighResTimeStamp` representing the time elapsed since the time origin.
|
|
293
|
+
- Returns a cleanup function that stops the animation loop.
|
|
294
|
+
- Best used inside a `$effect` to ensure proper cleanup when the component unmounts.
|
|
295
|
+
- SSR-safe: Does nothing and returns a no-op cleanup function when `window` is unavailable.
|
|
296
|
+
|
|
297
|
+
```svelte
|
|
298
|
+
<script lang="ts">
|
|
299
|
+
import { useAnimationFrame } from '$lib'
|
|
300
|
+
|
|
301
|
+
let cubeRef: HTMLDivElement
|
|
302
|
+
|
|
303
|
+
$effect(() => {
|
|
304
|
+
return useAnimationFrame((t) => {
|
|
305
|
+
if (!cubeRef) return
|
|
306
|
+
|
|
307
|
+
const rotate = Math.sin(t / 10000) * 200
|
|
308
|
+
const y = (1 + Math.sin(t / 1000)) * -50
|
|
309
|
+
cubeRef.style.transform = `translateY(${y}px) rotateX(${rotate}deg) rotateY(${rotate}deg)`
|
|
310
|
+
})
|
|
311
|
+
})
|
|
312
|
+
</script>
|
|
313
|
+
|
|
314
|
+
<div bind:this={cubeRef}>Animated content</div>
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
- Reference: Motion useAnimationFrame docs [motion.dev](https://motion.dev/docs/react-use-animation-frame).
|
|
318
|
+
|
|
207
319
|
### useSpring
|
|
208
320
|
|
|
209
321
|
`useSpring` creates a readable store that animates to its latest target with a spring. You can either control it directly with `set`/`jump`, or have it follow another readable (like a time-derived value).
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Writable } from 'svelte/store';
|
|
2
|
+
/**
|
|
3
|
+
* Provide a writable store for the current variant key so children can
|
|
4
|
+
* react to changes over time (true inheritance like Framer Motion).
|
|
5
|
+
*/
|
|
6
|
+
export declare function setVariantContext(store: Writable<string | undefined>): void;
|
|
7
|
+
/**
|
|
8
|
+
* Read the parent's variant store (if any). Children subscribe to this store
|
|
9
|
+
* to inherit and react to parent `animate` changes.
|
|
10
|
+
*/
|
|
11
|
+
export declare function getVariantContext(): Writable<string | undefined> | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* Set initial={false} in context so children inherit it
|
|
14
|
+
*/
|
|
15
|
+
export declare function setInitialFalseContext(value: boolean): void;
|
|
16
|
+
/**
|
|
17
|
+
* Check if parent has initial={false}
|
|
18
|
+
*/
|
|
19
|
+
export declare function getInitialFalseContext(): boolean;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { getContext, setContext } from 'svelte';
|
|
2
|
+
const VARIANT_CONTEXT_KEY = Symbol('variant-context');
|
|
3
|
+
const INITIAL_FALSE_CONTEXT_KEY = Symbol('initial-false-context');
|
|
4
|
+
/**
|
|
5
|
+
* Provide a writable store for the current variant key so children can
|
|
6
|
+
* react to changes over time (true inheritance like Framer Motion).
|
|
7
|
+
*/
|
|
8
|
+
export function setVariantContext(store) {
|
|
9
|
+
setContext(VARIANT_CONTEXT_KEY, store);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Read the parent's variant store (if any). Children subscribe to this store
|
|
13
|
+
* to inherit and react to parent `animate` changes.
|
|
14
|
+
*/
|
|
15
|
+
export function getVariantContext() {
|
|
16
|
+
return getContext(VARIANT_CONTEXT_KEY);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Set initial={false} in context so children inherit it
|
|
20
|
+
*/
|
|
21
|
+
export function setInitialFalseContext(value) {
|
|
22
|
+
setContext(INITIAL_FALSE_CONTEXT_KEY, value);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Check if parent has initial={false}
|
|
26
|
+
*/
|
|
27
|
+
export function getInitialFalseContext() {
|
|
28
|
+
return getContext(INITIAL_FALSE_CONTEXT_KEY) ?? false;
|
|
29
|
+
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { mergeTransitions, animateWithLifecycle } from '../utils/animation'
|
|
10
10
|
import { attachWhileTap } from '../utils/interaction'
|
|
11
11
|
import { attachWhileHover } from '../utils/hover'
|
|
12
|
+
import { attachWhileFocus } from '../utils/focus'
|
|
12
13
|
import {
|
|
13
14
|
measureRect,
|
|
14
15
|
computeFlipTransforms,
|
|
@@ -21,6 +22,14 @@
|
|
|
21
22
|
import { isNativelyFocusable } from '../utils/a11y'
|
|
22
23
|
import { usePresence, getAnimatePresenceContext } from '../utils/presence'
|
|
23
24
|
import { getInitialKeyframes } from '../utils/initial'
|
|
25
|
+
import { resolveInitial, resolveAnimate, resolveExit } from '../utils/variants'
|
|
26
|
+
import {
|
|
27
|
+
setVariantContext,
|
|
28
|
+
getVariantContext,
|
|
29
|
+
setInitialFalseContext,
|
|
30
|
+
getInitialFalseContext
|
|
31
|
+
} from '../components/variantContext.context'
|
|
32
|
+
import { writable } from 'svelte/store'
|
|
24
33
|
|
|
25
34
|
type Props = MotionProps & {
|
|
26
35
|
children?: Snippet
|
|
@@ -31,6 +40,7 @@
|
|
|
31
40
|
let {
|
|
32
41
|
children,
|
|
33
42
|
tag = 'div',
|
|
43
|
+
variants: variantsProp,
|
|
34
44
|
initial: initialProp,
|
|
35
45
|
animate: animateProp,
|
|
36
46
|
exit: exitProp,
|
|
@@ -41,8 +51,11 @@
|
|
|
41
51
|
class: classProp,
|
|
42
52
|
whileTap: whileTapProp,
|
|
43
53
|
whileHover: whileHoverProp,
|
|
54
|
+
whileFocus: whileFocusProp,
|
|
44
55
|
onHoverStart: onHoverStartProp,
|
|
45
56
|
onHoverEnd: onHoverEndProp,
|
|
57
|
+
onFocusStart: onFocusStartProp,
|
|
58
|
+
onFocusEnd: onFocusEndProp,
|
|
46
59
|
onTapStart: onTapStartProp,
|
|
47
60
|
onTap: onTapProp,
|
|
48
61
|
onTapCancel: onTapCancelProp,
|
|
@@ -71,7 +84,7 @@
|
|
|
71
84
|
usePresence(
|
|
72
85
|
presenceKey,
|
|
73
86
|
element,
|
|
74
|
-
|
|
87
|
+
resolvedExit,
|
|
75
88
|
mergedTransition as unknown as MotionTransition
|
|
76
89
|
)
|
|
77
90
|
}
|
|
@@ -140,8 +153,67 @@
|
|
|
140
153
|
// Recognized HTML void elements that cannot contain children
|
|
141
154
|
const isVoidTag = $derived(VOID_TAGS.has(tag as string))
|
|
142
155
|
|
|
143
|
-
//
|
|
144
|
-
const
|
|
156
|
+
// Variant inheritance and resolution
|
|
157
|
+
const parentVariantStore = getVariantContext()
|
|
158
|
+
|
|
159
|
+
// Get initial inherited variant synchronously
|
|
160
|
+
let initialInheritedVariant: string | undefined = undefined
|
|
161
|
+
if (parentVariantStore) {
|
|
162
|
+
parentVariantStore.subscribe((v) => (initialInheritedVariant = v))()
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Create store with initial value so children can inherit immediately
|
|
166
|
+
const initialVariantValue =
|
|
167
|
+
typeof animateProp === 'string'
|
|
168
|
+
? animateProp
|
|
169
|
+
: (variantsProp && initialInheritedVariant) || undefined
|
|
170
|
+
const localVariantStore = writable<string | undefined>(initialVariantValue)
|
|
171
|
+
|
|
172
|
+
let inheritedVariant = $state<string | undefined>(initialInheritedVariant)
|
|
173
|
+
|
|
174
|
+
$effect(() => {
|
|
175
|
+
if (!parentVariantStore) {
|
|
176
|
+
inheritedVariant = undefined
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
const unsubscribe = parentVariantStore.subscribe((v) => (inheritedVariant = v))
|
|
180
|
+
return () => unsubscribe()
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
// Use the initial value first, then switch to reactive once mounted
|
|
184
|
+
const effectiveAnimate = $derived(
|
|
185
|
+
animateProp ?? (variantsProp ? (inheritedVariant ?? initialInheritedVariant) : undefined)
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
// Propagate initial={false} to children BEFORE setting variant context
|
|
189
|
+
const parentInitialFalse = getInitialFalseContext()
|
|
190
|
+
const effectiveInitialProp =
|
|
191
|
+
initialProp !== undefined
|
|
192
|
+
? initialProp
|
|
193
|
+
: parentInitialFalse && variantsProp
|
|
194
|
+
? false
|
|
195
|
+
: undefined
|
|
196
|
+
|
|
197
|
+
if (initialProp === false) {
|
|
198
|
+
setInitialFalseContext(true)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Provide context immediately during initialization so children can inherit
|
|
202
|
+
setVariantContext(localVariantStore)
|
|
203
|
+
|
|
204
|
+
$effect(() => {
|
|
205
|
+
if (!variantsProp) return localVariantStore.set(undefined)
|
|
206
|
+
if (typeof animateProp === 'string') return localVariantStore.set(animateProp)
|
|
207
|
+
if (typeof effectiveAnimate === 'string') return localVariantStore.set(effectiveAnimate)
|
|
208
|
+
localVariantStore.set(undefined)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
const resolvedInitial = $derived(resolveInitial(effectiveInitialProp, variantsProp))
|
|
212
|
+
const resolvedAnimate = $derived(resolveAnimate(effectiveAnimate, variantsProp))
|
|
213
|
+
const resolvedExit = $derived(resolveExit(exitProp, variantsProp))
|
|
214
|
+
|
|
215
|
+
// Extract keyframes from resolved initial, handling initial={false}
|
|
216
|
+
const initialKeyframes = $derived(getInitialKeyframes(resolvedInitial))
|
|
145
217
|
|
|
146
218
|
// Derived attributes to keep both branches in sync (focusability, data flags, style, class)
|
|
147
219
|
const derivedAttrs = $derived<Record<string, unknown>>({
|
|
@@ -163,15 +235,15 @@
|
|
|
163
235
|
style: mergeInlineStyles(
|
|
164
236
|
styleProp,
|
|
165
237
|
initialKeyframes as unknown as Record<string, unknown>,
|
|
166
|
-
|
|
238
|
+
resolvedAnimate as unknown as Record<string, unknown>
|
|
167
239
|
),
|
|
168
240
|
class: classProp
|
|
169
241
|
})
|
|
170
242
|
|
|
171
243
|
const runAnimation = () => {
|
|
172
|
-
if (!element || !
|
|
244
|
+
if (!element || !resolvedAnimate) return
|
|
173
245
|
const transitionAnimate: MotionTransition = mergedTransition ?? {}
|
|
174
|
-
const payload = $state.snapshot(
|
|
246
|
+
const payload = $state.snapshot(resolvedAnimate)
|
|
175
247
|
animateWithLifecycle(
|
|
176
248
|
element,
|
|
177
249
|
payload as unknown as DOMKeyframesDefinition,
|
|
@@ -181,6 +253,17 @@
|
|
|
181
253
|
)
|
|
182
254
|
}
|
|
183
255
|
|
|
256
|
+
// Track the last variant key we ran to avoid re-running on mount
|
|
257
|
+
let lastRanVariantKey = $state<string | undefined>(undefined)
|
|
258
|
+
let mountedWithInitialFalse = $state(false)
|
|
259
|
+
const currentAnimateKey = $derived(
|
|
260
|
+
typeof animateProp === 'string'
|
|
261
|
+
? animateProp
|
|
262
|
+
: typeof effectiveAnimate === 'string'
|
|
263
|
+
? effectiveAnimate
|
|
264
|
+
: undefined
|
|
265
|
+
)
|
|
266
|
+
|
|
184
267
|
// Minimal layout animation using FLIP when `layout` is enabled.
|
|
185
268
|
// When layout === 'position' we only translate.
|
|
186
269
|
// When layout === true we also scale to smoothly interpolate size changes.
|
|
@@ -231,8 +314,8 @@
|
|
|
231
314
|
return attachWhileTap(
|
|
232
315
|
element!,
|
|
233
316
|
(whileTapProp ?? {}) as Record<string, unknown>,
|
|
234
|
-
(
|
|
235
|
-
(
|
|
317
|
+
(resolvedInitial ?? {}) as Record<string, unknown>,
|
|
318
|
+
(resolvedAnimate ?? {}) as Record<string, unknown>,
|
|
236
319
|
{
|
|
237
320
|
onTapStart: onTapStartProp,
|
|
238
321
|
onTap: onTapProp,
|
|
@@ -253,23 +336,93 @@
|
|
|
253
336
|
{ onStart: onHoverStartProp, onEnd: onHoverEndProp },
|
|
254
337
|
undefined,
|
|
255
338
|
{
|
|
256
|
-
initial: (
|
|
257
|
-
animate: (
|
|
339
|
+
initial: (resolvedInitial ?? {}) as Record<string, unknown>,
|
|
340
|
+
animate: (resolvedAnimate ?? {}) as Record<string, unknown>
|
|
341
|
+
}
|
|
342
|
+
)
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
// whileFocus handling for keyboard focus interactions
|
|
346
|
+
$effect(() => {
|
|
347
|
+
if (!(element && isLoaded === 'ready' && isNotEmpty(whileFocusProp))) return
|
|
348
|
+
return attachWhileFocus(
|
|
349
|
+
element!,
|
|
350
|
+
(whileFocusProp ?? {}) as Record<string, unknown>,
|
|
351
|
+
(mergedTransition ?? {}) as AnimationOptions,
|
|
352
|
+
{ onStart: onFocusStartProp, onEnd: onFocusEndProp },
|
|
353
|
+
{
|
|
354
|
+
initial: (resolvedInitial ?? {}) as Record<string, unknown>,
|
|
355
|
+
animate: (resolvedAnimate ?? {}) as Record<string, unknown>
|
|
258
356
|
}
|
|
259
357
|
)
|
|
260
358
|
})
|
|
261
359
|
|
|
262
360
|
// Re-run animate when animateProp changes while ready
|
|
263
361
|
$effect(() => {
|
|
264
|
-
if (element && isLoaded === 'ready'
|
|
362
|
+
if (!(element && isLoaded === 'ready')) return
|
|
363
|
+
// Skip first run if we mounted with initial={false} AND the variant hasn't changed
|
|
364
|
+
if (mountedWithInitialFalse) {
|
|
365
|
+
// Only skip if the variant is the same as what we mounted with
|
|
366
|
+
if (typeof animateProp === 'string' && lastRanVariantKey === animateProp) {
|
|
367
|
+
mountedWithInitialFalse = false
|
|
368
|
+
return
|
|
369
|
+
}
|
|
370
|
+
// Variant has changed, so we should animate
|
|
371
|
+
mountedWithInitialFalse = false
|
|
372
|
+
}
|
|
373
|
+
if (typeof animateProp === 'string') {
|
|
374
|
+
if (lastRanVariantKey !== animateProp) {
|
|
375
|
+
lastRanVariantKey = animateProp
|
|
376
|
+
runAnimation()
|
|
377
|
+
}
|
|
378
|
+
} else if (animateProp) {
|
|
379
|
+
// Object animate props - always run
|
|
380
|
+
lastRanVariantKey = undefined
|
|
381
|
+
runAnimation()
|
|
382
|
+
}
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
// Also run when inherited/effective variant changes
|
|
386
|
+
$effect(() => {
|
|
387
|
+
void resolvedAnimate
|
|
388
|
+
if (!(element && isLoaded === 'ready' && !animateProp && resolvedAnimate)) return
|
|
389
|
+
// Skip first run if we mounted with initial={false} AND the variant hasn't changed
|
|
390
|
+
if (mountedWithInitialFalse) {
|
|
391
|
+
// Only skip if the variant is the same as what we mounted with
|
|
392
|
+
if (typeof currentAnimateKey === 'string' && lastRanVariantKey === currentAnimateKey) {
|
|
393
|
+
mountedWithInitialFalse = false
|
|
394
|
+
return
|
|
395
|
+
}
|
|
396
|
+
// Variant has changed, so we should animate
|
|
397
|
+
mountedWithInitialFalse = false
|
|
398
|
+
}
|
|
399
|
+
if (typeof currentAnimateKey === 'string') {
|
|
400
|
+
if (lastRanVariantKey !== currentAnimateKey) {
|
|
401
|
+
lastRanVariantKey = currentAnimateKey
|
|
402
|
+
runAnimation()
|
|
403
|
+
}
|
|
404
|
+
} else {
|
|
265
405
|
runAnimation()
|
|
266
406
|
}
|
|
267
407
|
})
|
|
268
408
|
|
|
269
409
|
$effect(() => {
|
|
270
410
|
if (!(element && isLoaded === 'mounting')) return
|
|
271
|
-
if (
|
|
272
|
-
|
|
411
|
+
if (effectiveAnimate) {
|
|
412
|
+
// If initial={false}, render at animate state immediately with no transition
|
|
413
|
+
if (effectiveInitialProp === false && resolvedAnimate) {
|
|
414
|
+
// Use Motion's animate() with duration:0 so it takes control of these properties
|
|
415
|
+
// This prevents inline styles from pinning the properties during future animations
|
|
416
|
+
const snapshot = $state.snapshot(resolvedAnimate) as Record<string, unknown>
|
|
417
|
+
animate(element!, snapshot as DOMKeyframesDefinition, { duration: 0 })
|
|
418
|
+
// Mark that we've already applied this variant to avoid a second animate pass
|
|
419
|
+
mountedWithInitialFalse = true
|
|
420
|
+
if (typeof currentAnimateKey === 'string') {
|
|
421
|
+
lastRanVariantKey = currentAnimateKey
|
|
422
|
+
}
|
|
423
|
+
dataPath = 5
|
|
424
|
+
isLoaded = 'ready'
|
|
425
|
+
} else if (isNotEmpty(initialKeyframes)) {
|
|
273
426
|
// Apply initial instantly BEFORE exposing 'initial' state
|
|
274
427
|
animate(element!, initialKeyframes!, { duration: 0 })
|
|
275
428
|
// Mark initial after styles are applied so tests read CSS=0 while state=initial
|
|
@@ -286,7 +439,20 @@
|
|
|
286
439
|
} else {
|
|
287
440
|
dataPath = 2
|
|
288
441
|
isLoaded = 'ready'
|
|
289
|
-
|
|
442
|
+
// If we're inheriting a variant and parent had initial={false}, apply the variant instantly
|
|
443
|
+
// without animation, then mark it as applied
|
|
444
|
+
if (
|
|
445
|
+
parentInitialFalse &&
|
|
446
|
+
typeof currentAnimateKey === 'string' &&
|
|
447
|
+
resolvedAnimate
|
|
448
|
+
) {
|
|
449
|
+
// Apply variant styles instantly with duration:0
|
|
450
|
+
const snapshot = $state.snapshot(resolvedAnimate) as Record<string, unknown>
|
|
451
|
+
animate(element!, snapshot as DOMKeyframesDefinition, { duration: 0 })
|
|
452
|
+
lastRanVariantKey = currentAnimateKey
|
|
453
|
+
} else {
|
|
454
|
+
runAnimation()
|
|
455
|
+
}
|
|
290
456
|
}
|
|
291
457
|
} else if (isNotEmpty(initialKeyframes)) {
|
|
292
458
|
// Apply initial instantly BEFORE exposing 'initial' state
|
package/dist/index.d.ts
CHANGED
|
@@ -3,8 +3,10 @@ import MotionConfig from './components/MotionConfig.svelte';
|
|
|
3
3
|
import type { MotionComponents } from './html/index';
|
|
4
4
|
export declare const motion: MotionComponents;
|
|
5
5
|
export { animate, hover } from 'motion';
|
|
6
|
-
export type { MotionAnimate, MotionInitial, MotionTransition, MotionWhileTap } from './types';
|
|
6
|
+
export type { MotionAnimate, MotionInitial, MotionTransition, MotionWhileFocus, MotionWhileHover, MotionWhileTap, Variants } from './types';
|
|
7
|
+
export { useAnimationFrame } from './utils/animationFrame';
|
|
7
8
|
export { useSpring } from './utils/spring';
|
|
9
|
+
export { stringifyStyleObject } from './utils/styleObject';
|
|
8
10
|
export { useTime } from './utils/time';
|
|
9
11
|
export { useTransform } from './utils/transform';
|
|
10
12
|
export { AnimatePresence, MotionConfig };
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,9 @@ import * as html from './html/index';
|
|
|
5
5
|
export const motion = Object.fromEntries(Object.entries(html).map(([key, component]) => [key.toLowerCase(), component]));
|
|
6
6
|
// Export all types
|
|
7
7
|
export { animate, hover } from 'motion';
|
|
8
|
+
export { useAnimationFrame } from './utils/animationFrame';
|
|
8
9
|
export { useSpring } from './utils/spring';
|
|
10
|
+
export { stringifyStyleObject } from './utils/styleObject';
|
|
9
11
|
export { useTime } from './utils/time';
|
|
10
12
|
export { useTransform } from './utils/transform';
|
|
11
13
|
export { AnimatePresence, MotionConfig };
|
package/dist/types.d.ts
CHANGED
|
@@ -1,10 +1,27 @@
|
|
|
1
1
|
import type { AnimationOptions, DOMKeyframesDefinition } from 'motion';
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
+
/**
|
|
4
|
+
* Variants define named animation states that can be referenced by string keys.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```svelte
|
|
8
|
+
* <script>
|
|
9
|
+
* const variants = {
|
|
10
|
+
* open: { opacity: 1, scale: 1 },
|
|
11
|
+
* closed: { opacity: 0, scale: 0.8 }
|
|
12
|
+
* }
|
|
13
|
+
* </script>
|
|
14
|
+
*
|
|
15
|
+
* <motion.div variants={variants} animate="open" />
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export type Variants = Record<string, DOMKeyframesDefinition | undefined>;
|
|
3
19
|
/**
|
|
4
20
|
* Initial animation properties for a motion component.
|
|
5
21
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
22
|
+
* - Can be an object with animation properties
|
|
23
|
+
* - Can be a string key referencing a variant
|
|
24
|
+
* - Set to `false` to skip the initial animation and render directly at the animated state
|
|
8
25
|
*
|
|
9
26
|
* @example
|
|
10
27
|
* ```svelte
|
|
@@ -13,27 +30,44 @@ import type { Snippet } from 'svelte';
|
|
|
13
30
|
*
|
|
14
31
|
* <!-- Skip initial animation, render at animate state -->
|
|
15
32
|
* <motion.div initial={false} animate={{ opacity: 1 }} />
|
|
33
|
+
*
|
|
34
|
+
* <!-- Use variant key -->
|
|
35
|
+
* <motion.div variants={myVariants} initial="hidden" animate="visible" />
|
|
16
36
|
* ```
|
|
17
37
|
*/
|
|
18
|
-
export type MotionInitial = DOMKeyframesDefinition | false | undefined;
|
|
38
|
+
export type MotionInitial = DOMKeyframesDefinition | string | false | undefined;
|
|
19
39
|
/**
|
|
20
40
|
* Target animation properties for a motion component.
|
|
41
|
+
*
|
|
42
|
+
* - Can be an object with animation properties
|
|
43
|
+
* - Can be a string key referencing a variant
|
|
44
|
+
*
|
|
21
45
|
* @example
|
|
22
46
|
* ```svelte
|
|
23
47
|
* <motion.div animate={{ opacity: 1, scale: 1 }} />
|
|
48
|
+
*
|
|
49
|
+
* <!-- With variants -->
|
|
50
|
+
* <motion.div variants={myVariants} animate="visible" />
|
|
24
51
|
* ```
|
|
25
52
|
*/
|
|
26
|
-
export type MotionAnimate = DOMKeyframesDefinition | undefined;
|
|
53
|
+
export type MotionAnimate = DOMKeyframesDefinition | string | undefined;
|
|
27
54
|
/**
|
|
28
55
|
* Exit animation properties for a motion component when unmounted.
|
|
56
|
+
*
|
|
57
|
+
* - Can be an object with animation properties
|
|
58
|
+
* - Can be a string key referencing a variant
|
|
59
|
+
*
|
|
29
60
|
* @example
|
|
30
61
|
* ```svelte
|
|
31
62
|
* <motion.div exit={{ opacity: 0, scale: 0 }} />
|
|
63
|
+
*
|
|
64
|
+
* <!-- With variants -->
|
|
65
|
+
* <motion.div variants={myVariants} exit="hidden" />
|
|
32
66
|
* ```
|
|
33
67
|
*/
|
|
34
68
|
export type MotionExit = (Record<string, unknown> & {
|
|
35
69
|
transition?: AnimationOptions;
|
|
36
|
-
}) | DOMKeyframesDefinition | undefined;
|
|
70
|
+
}) | DOMKeyframesDefinition | string | undefined;
|
|
37
71
|
/**
|
|
38
72
|
* Animation transition configuration.
|
|
39
73
|
* @example
|
|
@@ -69,6 +103,16 @@ export type MotionWhileTap = DOMKeyframesDefinition | undefined;
|
|
|
69
103
|
export type MotionWhileHover = (Record<string, unknown> & {
|
|
70
104
|
transition?: AnimationOptions;
|
|
71
105
|
}) | DOMKeyframesDefinition | undefined;
|
|
106
|
+
/**
|
|
107
|
+
* Animation properties for focus interactions.
|
|
108
|
+
* @example
|
|
109
|
+
* ```svelte
|
|
110
|
+
* <motion.button whileFocus={{ scale: 1.05 }} />
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
export type MotionWhileFocus = (Record<string, unknown> & {
|
|
114
|
+
transition?: AnimationOptions;
|
|
115
|
+
}) | DOMKeyframesDefinition | undefined;
|
|
72
116
|
/**
|
|
73
117
|
* Animation transition configuration for hover interactions.
|
|
74
118
|
* Overrides the global transition when provided.
|
|
@@ -81,6 +125,9 @@ export type MotionAnimationComplete = ((_definition: DOMKeyframesDefinition | un
|
|
|
81
125
|
/** Hover lifecycle callbacks */
|
|
82
126
|
export type MotionOnHoverStart = (() => void) | undefined;
|
|
83
127
|
export type MotionOnHoverEnd = (() => void) | undefined;
|
|
128
|
+
/** Focus lifecycle callbacks */
|
|
129
|
+
export type MotionOnFocusStart = (() => void) | undefined;
|
|
130
|
+
export type MotionOnFocusEnd = (() => void) | undefined;
|
|
84
131
|
/** Tap lifecycle callbacks */
|
|
85
132
|
export type MotionOnTapStart = (() => void) | undefined;
|
|
86
133
|
export type MotionOnTap = (() => void) | undefined;
|
|
@@ -89,11 +136,13 @@ export type MotionOnTapCancel = (() => void) | undefined;
|
|
|
89
136
|
* Base motion props shared by all motion components.
|
|
90
137
|
*/
|
|
91
138
|
export type MotionProps = {
|
|
92
|
-
/**
|
|
139
|
+
/** Variants define named animation states */
|
|
140
|
+
variants?: Variants;
|
|
141
|
+
/** Initial state of the animation (object or variant key) */
|
|
93
142
|
initial?: MotionInitial;
|
|
94
|
-
/** Target state of the animation */
|
|
143
|
+
/** Target state of the animation (object or variant key) */
|
|
95
144
|
animate?: MotionAnimate;
|
|
96
|
-
/** Exit animation state when component is removed */
|
|
145
|
+
/** Exit animation state when component is removed (object or variant key) */
|
|
97
146
|
exit?: MotionExit;
|
|
98
147
|
/** Animation configuration */
|
|
99
148
|
transition?: MotionTransition;
|
|
@@ -101,6 +150,8 @@ export type MotionProps = {
|
|
|
101
150
|
whileTap?: MotionWhileTap;
|
|
102
151
|
/** Hover interaction animation */
|
|
103
152
|
whileHover?: MotionWhileHover;
|
|
153
|
+
/** Focus interaction animation */
|
|
154
|
+
whileFocus?: MotionWhileFocus;
|
|
104
155
|
/** Called right before a main animate transition starts */
|
|
105
156
|
onAnimationStart?: MotionAnimationStart;
|
|
106
157
|
/** Called after a main animate transition completes */
|
|
@@ -109,6 +160,10 @@ export type MotionProps = {
|
|
|
109
160
|
onHoverStart?: MotionOnHoverStart;
|
|
110
161
|
/** Called when a true hover gesture ends */
|
|
111
162
|
onHoverEnd?: MotionOnHoverEnd;
|
|
163
|
+
/** Called when element receives keyboard focus */
|
|
164
|
+
onFocusStart?: MotionOnFocusStart;
|
|
165
|
+
/** Called when element loses keyboard focus */
|
|
166
|
+
onFocusEnd?: MotionOnFocusEnd;
|
|
112
167
|
/** Called when a tap gesture starts (pointerdown recognized) */
|
|
113
168
|
onTapStart?: MotionOnTapStart;
|
|
114
169
|
/** Called when a tap gesture ends successfully (pointerup) */
|
package/dist/utils/a11y.d.ts
CHANGED
|
@@ -1,2 +1,22 @@
|
|
|
1
1
|
import type { SvelteHTMLElements } from 'svelte/elements';
|
|
2
|
+
/**
|
|
3
|
+
* Determines if an HTML element is natively focusable.
|
|
4
|
+
*
|
|
5
|
+
* Checks whether a given tag with provided attributes can receive keyboard
|
|
6
|
+
* focus without needing an explicit `tabindex`. Elements are considered
|
|
7
|
+
* natively focusable if they have `tabindex`, `tabIndex`, `contenteditable`,
|
|
8
|
+
* or are inherently focusable tags like `button`, `input`, or anchors with `href`.
|
|
9
|
+
*
|
|
10
|
+
* @param tag - The HTML element tag name.
|
|
11
|
+
* @param attrs - Attributes object that may contain focusability hints.
|
|
12
|
+
* @returns `true` if the element is natively focusable, otherwise `false`.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* isNativelyFocusable('button', {}) // true
|
|
17
|
+
* isNativelyFocusable('div', { tabindex: '0' }) // true
|
|
18
|
+
* isNativelyFocusable('a', { href: '/home' }) // true
|
|
19
|
+
* isNativelyFocusable('div', {}) // false
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
2
22
|
export declare const isNativelyFocusable: (tag: keyof SvelteHTMLElements, attrs?: Record<string, unknown>) => boolean;
|