@humanspeak/svelte-motion 0.4.6 → 0.4.8
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 +12 -12
- package/dist/html/_MotionContainer.svelte +31 -13
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2 -2
- package/dist/types.d.ts +36 -8
- package/dist/utils/cycle.svelte.d.ts +98 -0
- package/dist/utils/cycle.svelte.js +44 -0
- package/dist/utils/spring.svelte.d.ts +83 -0
- package/dist/utils/spring.svelte.js +118 -0
- package/dist/utils/variants.d.ts +82 -8
- package/dist/utils/variants.js +124 -13
- package/package.json +8 -8
- package/dist/utils/cycle.d.ts +0 -35
- package/dist/utils/cycle.js +0 -48
- package/dist/utils/spring.d.ts +0 -51
- package/dist/utils/spring.js +0 -157
package/README.md
CHANGED
|
@@ -36,18 +36,18 @@ npm install @humanspeak/svelte-motion
|
|
|
36
36
|
|
|
37
37
|
Goal: Framer Motion API parity for Svelte where common React examples can be translated with minimal changes.
|
|
38
38
|
|
|
39
|
-
| Capability
|
|
40
|
-
|
|
|
41
|
-
| `initial` / `animate` / `transition`
|
|
42
|
-
| `variants` (string keys + inheritance)
|
|
43
|
-
| `whileHover` / `whileTap` / `whileFocus` / `whileInView`
|
|
44
|
-
| Drag (`drag`, constraints, momentum, controls, callbacks)
|
|
45
|
-
| `AnimatePresence` (`initial`, `mode`, `onExitComplete`)
|
|
46
|
-
| Layout (`layout`, `layout="position"`)
|
|
47
|
-
| Shared layout (`layoutId`, `LayoutGroup`, `layoutScroll`)
|
|
48
|
-
| Pan gesture API (`whilePan`, `onPan*`)
|
|
49
|
-
| `MotionConfig` parity beyond `transition`
|
|
50
|
-
| `reducedMotion`, `features`, `transformPagePoint`
|
|
39
|
+
| Capability | Status |
|
|
40
|
+
| ---------------------------------------------------------------------- | ------------------------------------------ |
|
|
41
|
+
| `initial` / `animate` / `transition` | Supported |
|
|
42
|
+
| `variants` (string keys + inheritance, function-form `custom`) | Supported |
|
|
43
|
+
| `whileHover` / `whileTap` / `whileFocus` / `whileDrag` / `whileInView` | Supported (inline + variant keys / arrays) |
|
|
44
|
+
| Drag (`drag`, constraints, momentum, controls, callbacks) | Supported |
|
|
45
|
+
| `AnimatePresence` (`initial`, `mode`, `onExitComplete`) | Supported |
|
|
46
|
+
| Layout (`layout`, `layout="position"`) | Supported (single-element FLIP) |
|
|
47
|
+
| Shared layout (`layoutId`, `LayoutGroup`, `layoutScroll`) | Supported |
|
|
48
|
+
| Pan gesture API (`whilePan`, `onPan*`) | Not yet supported |
|
|
49
|
+
| `MotionConfig` parity beyond `transition` | Partial |
|
|
50
|
+
| `reducedMotion`, `features`, `transformPagePoint` | Not yet supported |
|
|
51
51
|
|
|
52
52
|
## Supported elements
|
|
53
53
|
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
} from '../utils/presence'
|
|
49
49
|
import { getInitialKeyframes } from '../utils/initial'
|
|
50
50
|
import { attachDrag } from '../utils/drag'
|
|
51
|
-
import { resolveInitial, resolveAnimate, resolveExit } from '../utils/variants'
|
|
51
|
+
import { resolveInitial, resolveAnimate, resolveExit, resolveWhile } from '../utils/variants'
|
|
52
52
|
import {
|
|
53
53
|
setVariantContext,
|
|
54
54
|
getVariantContext,
|
|
@@ -446,6 +446,19 @@
|
|
|
446
446
|
)
|
|
447
447
|
const resolvedExit = $derived(resolveExit(exitProp, variantsProp, effectiveCustom))
|
|
448
448
|
|
|
449
|
+
// Resolve `whileX` props against `variants` so each gesture's attach
|
|
450
|
+
// helper receives a plain keyframes object regardless of whether the
|
|
451
|
+
// consumer wrote inline keyframes, a variant key, or an array of
|
|
452
|
+
// variant keys. Mirrors framer-motion's `whileHover` etc. surface
|
|
453
|
+
// (#349).
|
|
454
|
+
const resolvedWhileTap = $derived(resolveWhile(whileTapProp, variantsProp, effectiveCustom))
|
|
455
|
+
const resolvedWhileHover = $derived(resolveWhile(whileHoverProp, variantsProp, effectiveCustom))
|
|
456
|
+
const resolvedWhileFocus = $derived(resolveWhile(whileFocusProp, variantsProp, effectiveCustom))
|
|
457
|
+
const resolvedWhileDrag = $derived(resolveWhile(whileDragProp, variantsProp, effectiveCustom))
|
|
458
|
+
const resolvedWhileInView = $derived(
|
|
459
|
+
resolveWhile(whileInViewProp, variantsProp, effectiveCustom)
|
|
460
|
+
)
|
|
461
|
+
|
|
449
462
|
// Extract keyframes from resolved initial, handling initial={false}
|
|
450
463
|
const initialKeyframes = $derived(
|
|
451
464
|
filterReducedMotionKeyframes(
|
|
@@ -457,7 +470,12 @@
|
|
|
457
470
|
// Derived attributes to keep both branches in sync (focusability, data flags, style, class)
|
|
458
471
|
const derivedAttrs = $derived<Record<string, unknown>>({
|
|
459
472
|
...(rest as Record<string, unknown>),
|
|
460
|
-
|
|
473
|
+
// Gate on the *resolved* whileTap, not the raw prop. With
|
|
474
|
+
// variant-label support a truthy-but-unresolved value (unknown
|
|
475
|
+
// key, empty array) would otherwise add `tabindex=0` for an
|
|
476
|
+
// element that never actually receives a tap gesture — an
|
|
477
|
+
// unintended tab stop. (#349 CR feedback)
|
|
478
|
+
...(isNotEmpty(resolvedWhileTap) &&
|
|
461
479
|
!isNativelyFocusable(tag, rest as Record<string, unknown>) &&
|
|
462
480
|
((rest as Record<string, unknown>)?.tabindex ??
|
|
463
481
|
(rest as Record<string, unknown>)?.tabIndex ??
|
|
@@ -541,7 +559,7 @@
|
|
|
541
559
|
directionLock: !!dragDirectionLockProp,
|
|
542
560
|
listener: dragListenerProp !== false,
|
|
543
561
|
controls,
|
|
544
|
-
whileDrag:
|
|
562
|
+
whileDrag: resolvedWhileDrag as MotionWhileDrag,
|
|
545
563
|
mergedTransition: (mergedTransition ?? {}) as AnimationOptions,
|
|
546
564
|
callbacks: {
|
|
547
565
|
onStart: onDragStartProp as (e: PointerEvent, info: DragInfo) => void,
|
|
@@ -801,18 +819,18 @@
|
|
|
801
819
|
|
|
802
820
|
// whileTap handling via motion-dom's press()
|
|
803
821
|
$effect(() => {
|
|
804
|
-
if (!(element && isLoaded === 'ready' && isNotEmpty(
|
|
822
|
+
if (!(element && isLoaded === 'ready' && isNotEmpty(resolvedWhileTap))) return
|
|
805
823
|
return attachWhileTap(
|
|
806
824
|
element!,
|
|
807
|
-
(
|
|
825
|
+
(resolvedWhileTap ?? {}) as Record<string, unknown>,
|
|
808
826
|
(resolvedInitial ?? {}) as Record<string, unknown>,
|
|
809
827
|
(resolvedAnimate ?? {}) as Record<string, unknown>,
|
|
810
828
|
{
|
|
811
829
|
onTapStart: onTapStartProp,
|
|
812
830
|
onTap: onTapProp,
|
|
813
831
|
onTapCancel: onTapCancelProp,
|
|
814
|
-
hoverDef: isNotEmpty(
|
|
815
|
-
? ((
|
|
832
|
+
hoverDef: isNotEmpty(resolvedWhileHover ?? {})
|
|
833
|
+
? ((resolvedWhileHover ?? {}) as Record<string, unknown>)
|
|
816
834
|
: undefined,
|
|
817
835
|
hoverFallbackTransition: (mergedTransition ?? {}) as AnimationOptions
|
|
818
836
|
}
|
|
@@ -821,10 +839,10 @@
|
|
|
821
839
|
|
|
822
840
|
// whileHover handling, gated to true-hover devices to avoid sticky states on touch
|
|
823
841
|
$effect(() => {
|
|
824
|
-
if (!(element && isLoaded === 'ready' && isNotEmpty(
|
|
842
|
+
if (!(element && isLoaded === 'ready' && isNotEmpty(resolvedWhileHover))) return
|
|
825
843
|
return attachWhileHover(
|
|
826
844
|
element!,
|
|
827
|
-
(
|
|
845
|
+
(resolvedWhileHover ?? {}) as Record<string, unknown>,
|
|
828
846
|
(mergedTransition ?? {}) as AnimationOptions,
|
|
829
847
|
{ onStart: onHoverStartProp, onEnd: onHoverEndProp },
|
|
830
848
|
{
|
|
@@ -836,10 +854,10 @@
|
|
|
836
854
|
|
|
837
855
|
// whileFocus handling for keyboard focus interactions
|
|
838
856
|
$effect(() => {
|
|
839
|
-
if (!(element && isLoaded === 'ready' && isNotEmpty(
|
|
857
|
+
if (!(element && isLoaded === 'ready' && isNotEmpty(resolvedWhileFocus))) return
|
|
840
858
|
return attachWhileFocus(
|
|
841
859
|
element!,
|
|
842
|
-
(
|
|
860
|
+
(resolvedWhileFocus ?? {}) as Record<string, unknown>,
|
|
843
861
|
(mergedTransition ?? {}) as AnimationOptions,
|
|
844
862
|
{ onStart: onFocusStartProp, onEnd: onFocusEndProp },
|
|
845
863
|
{
|
|
@@ -851,10 +869,10 @@
|
|
|
851
869
|
|
|
852
870
|
// whileInView handling for viewport intersection
|
|
853
871
|
$effect(() => {
|
|
854
|
-
if (!(element && isLoaded === 'ready' && isNotEmpty(
|
|
872
|
+
if (!(element && isLoaded === 'ready' && isNotEmpty(resolvedWhileInView))) return
|
|
855
873
|
return attachWhileInView(
|
|
856
874
|
element!,
|
|
857
|
-
(
|
|
875
|
+
(resolvedWhileInView ?? {}) as Record<string, unknown>,
|
|
858
876
|
(mergedTransition ?? {}) as AnimationOptions,
|
|
859
877
|
{
|
|
860
878
|
onStart: onInViewStartProp,
|
package/dist/index.d.ts
CHANGED
|
@@ -10,8 +10,8 @@ export type { DragAxis, DragConstraints, DragControls, DragInfo, DragTransition,
|
|
|
10
10
|
export { useAnimate } from './utils/animate.svelte';
|
|
11
11
|
export type { AnimationScope } from './utils/animate.svelte';
|
|
12
12
|
export { useAnimationFrame } from './utils/animationFrame';
|
|
13
|
-
export { useCycle } from './utils/cycle';
|
|
14
|
-
export type { Cycle, CycleState } from './utils/cycle';
|
|
13
|
+
export { useCycle } from './utils/cycle.svelte';
|
|
14
|
+
export type { Cycle, CycleState } from './utils/cycle.svelte';
|
|
15
15
|
export { createDragControls } from './utils/dragControls';
|
|
16
16
|
export { useInView } from './utils/inView';
|
|
17
17
|
export type { UseInViewOptions } from './utils/inView';
|
|
@@ -22,7 +22,8 @@ export { useMotionValueEvent } from './utils/motionValueEvent';
|
|
|
22
22
|
export { useReducedMotion } from './utils/reducedMotion';
|
|
23
23
|
export { useReducedMotionConfig } from './utils/reducedMotionConfig';
|
|
24
24
|
export { useScroll } from './utils/scroll';
|
|
25
|
-
export { useSpring } from './utils/spring';
|
|
25
|
+
export { useSpring } from './utils/spring.svelte';
|
|
26
|
+
export type { SpringMotionValue, UseSpringOptions } from './utils/spring.svelte';
|
|
26
27
|
export { useIsPresent, usePresence } from './utils/usePresence';
|
|
27
28
|
export type { UsePresenceState } from './utils/usePresence';
|
|
28
29
|
export { useVelocity } from './utils/velocity';
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ export { anticipate, backIn, backInOut, backOut, circIn, circInOut, circOut, cub
|
|
|
11
11
|
export { clamp, distance, distance2D, interpolate, mix, pipe, progress, wrap } from 'motion';
|
|
12
12
|
export { useAnimate } from './utils/animate.svelte';
|
|
13
13
|
export { useAnimationFrame } from './utils/animationFrame';
|
|
14
|
-
export { useCycle } from './utils/cycle';
|
|
14
|
+
export { useCycle } from './utils/cycle.svelte';
|
|
15
15
|
export { createDragControls } from './utils/dragControls';
|
|
16
16
|
export { useInView } from './utils/inView';
|
|
17
17
|
export { useMotionTemplate } from './utils/motionTemplate';
|
|
@@ -20,7 +20,7 @@ export { useMotionValueEvent } from './utils/motionValueEvent';
|
|
|
20
20
|
export { useReducedMotion } from './utils/reducedMotion';
|
|
21
21
|
export { useReducedMotionConfig } from './utils/reducedMotionConfig';
|
|
22
22
|
export { useScroll } from './utils/scroll';
|
|
23
|
-
export { useSpring } from './utils/spring';
|
|
23
|
+
export { useSpring } from './utils/spring.svelte';
|
|
24
24
|
export { useIsPresent, usePresence } from './utils/usePresence';
|
|
25
25
|
export { useVelocity } from './utils/velocity';
|
|
26
26
|
/**
|
package/dist/types.d.ts
CHANGED
|
@@ -59,7 +59,7 @@ export type Variants = Record<string, Variant>;
|
|
|
59
59
|
* <motion.div variants={myVariants} initial="hidden" animate="visible" />
|
|
60
60
|
* ```
|
|
61
61
|
*/
|
|
62
|
-
export type MotionInitial = DOMKeyframesDefinition | string | false | undefined;
|
|
62
|
+
export type MotionInitial = DOMKeyframesDefinition | string | string[] | false | undefined;
|
|
63
63
|
/**
|
|
64
64
|
* Target animation properties for a motion component.
|
|
65
65
|
*
|
|
@@ -74,7 +74,7 @@ export type MotionInitial = DOMKeyframesDefinition | string | false | undefined;
|
|
|
74
74
|
* <motion.div variants={myVariants} animate="visible" />
|
|
75
75
|
* ```
|
|
76
76
|
*/
|
|
77
|
-
export type MotionAnimate = DOMKeyframesDefinition | string | undefined;
|
|
77
|
+
export type MotionAnimate = DOMKeyframesDefinition | string | string[] | undefined;
|
|
78
78
|
/**
|
|
79
79
|
* Exit animation properties for a motion component when unmounted.
|
|
80
80
|
*
|
|
@@ -91,7 +91,7 @@ export type MotionAnimate = DOMKeyframesDefinition | string | undefined;
|
|
|
91
91
|
*/
|
|
92
92
|
export type MotionExit = (Record<string, unknown> & {
|
|
93
93
|
transition?: AnimationOptions;
|
|
94
|
-
}) | DOMKeyframesDefinition | string | undefined;
|
|
94
|
+
}) | DOMKeyframesDefinition | string | string[] | undefined;
|
|
95
95
|
/**
|
|
96
96
|
* Animation transition configuration.
|
|
97
97
|
* @example
|
|
@@ -111,52 +111,80 @@ export type MotionExit = (Record<string, unknown> & {
|
|
|
111
111
|
export type MotionTransition = AnimationOptions | undefined;
|
|
112
112
|
/**
|
|
113
113
|
* Animation properties for tap/click interactions.
|
|
114
|
+
*
|
|
115
|
+
* Accepts inline keyframes, a variant key, or an array of variant keys
|
|
116
|
+
* (later entries override earlier ones on key collisions).
|
|
117
|
+
*
|
|
114
118
|
* @example
|
|
115
119
|
* ```svelte
|
|
116
120
|
* <motion.button whileTap={{ scale: 0.95 }} />
|
|
121
|
+
*
|
|
122
|
+
* <!-- Variant key -->
|
|
123
|
+
* <motion.button variants={{ pressed: { scale: 0.95 } }} whileTap="pressed" />
|
|
124
|
+
*
|
|
125
|
+
* <!-- Array — later wins on conflicts -->
|
|
126
|
+
* <motion.button whileTap={["pressed", "muted"]} />
|
|
117
127
|
* ```
|
|
118
128
|
*/
|
|
119
|
-
export type MotionWhileTap =
|
|
129
|
+
export type MotionWhileTap = (Record<string, unknown> & {
|
|
130
|
+
transition?: AnimationOptions;
|
|
131
|
+
}) | DOMKeyframesDefinition | string | string[] | undefined;
|
|
120
132
|
/**
|
|
121
133
|
* Animation properties for hover interactions.
|
|
134
|
+
*
|
|
135
|
+
* Accepts inline keyframes, a variant key, or an array of variant keys.
|
|
136
|
+
*
|
|
122
137
|
* @example
|
|
123
138
|
* ```svelte
|
|
124
139
|
* <motion.div whileHover={{ scale: 1.05 }} />
|
|
140
|
+
*
|
|
141
|
+
* <!-- Variant key -->
|
|
142
|
+
* <motion.div variants={{ hover: { scale: 1.05 } }} whileHover="hover" />
|
|
125
143
|
* ```
|
|
126
144
|
*/
|
|
127
145
|
export type MotionWhileHover = (Record<string, unknown> & {
|
|
128
146
|
transition?: AnimationOptions;
|
|
129
|
-
}) | DOMKeyframesDefinition | undefined;
|
|
147
|
+
}) | DOMKeyframesDefinition | string | string[] | undefined;
|
|
130
148
|
/**
|
|
131
149
|
* Animation properties for focus interactions.
|
|
150
|
+
*
|
|
151
|
+
* Accepts inline keyframes, a variant key, or an array of variant keys.
|
|
152
|
+
*
|
|
132
153
|
* @example
|
|
133
154
|
* ```svelte
|
|
134
155
|
* <motion.button whileFocus={{ scale: 1.05 }} />
|
|
156
|
+
* <motion.button variants={{ active: { outline: '2px solid blue' } }} whileFocus="active" />
|
|
135
157
|
* ```
|
|
136
158
|
*/
|
|
137
159
|
export type MotionWhileFocus = (Record<string, unknown> & {
|
|
138
160
|
transition?: AnimationOptions;
|
|
139
|
-
}) | DOMKeyframesDefinition | undefined;
|
|
161
|
+
}) | DOMKeyframesDefinition | string | string[] | undefined;
|
|
140
162
|
/**
|
|
141
163
|
* Animation properties for drag interactions.
|
|
142
164
|
* When a drag gesture starts, the element animates to this state; when it ends,
|
|
143
165
|
* it animates back to its baseline (from animate/initial), restoring only the changed keys.
|
|
166
|
+
*
|
|
167
|
+
* Accepts inline keyframes, a variant key, or an array of variant keys.
|
|
144
168
|
*/
|
|
145
169
|
export type MotionWhileDrag = (Record<string, unknown> & {
|
|
146
170
|
transition?: AnimationOptions;
|
|
147
|
-
}) | DOMKeyframesDefinition | undefined;
|
|
171
|
+
}) | DOMKeyframesDefinition | string | string[] | undefined;
|
|
148
172
|
/**
|
|
149
173
|
* Animation properties for in-view interactions.
|
|
150
174
|
* When the element enters the viewport, it animates to this state; when it leaves,
|
|
151
175
|
* it animates back to its baseline (from animate/initial), restoring only the changed keys.
|
|
176
|
+
*
|
|
177
|
+
* Accepts inline keyframes, a variant key, or an array of variant keys.
|
|
178
|
+
*
|
|
152
179
|
* @example
|
|
153
180
|
* ```svelte
|
|
154
181
|
* <motion.div whileInView={{ opacity: 1, y: 0 }} />
|
|
182
|
+
* <motion.div variants={{ inView: { opacity: 1 } }} whileInView="inView" />
|
|
155
183
|
* ```
|
|
156
184
|
*/
|
|
157
185
|
export type MotionWhileInView = (Record<string, unknown> & {
|
|
158
186
|
transition?: AnimationOptions;
|
|
159
|
-
}) | DOMKeyframesDefinition | undefined;
|
|
187
|
+
}) | DOMKeyframesDefinition | string | string[] | undefined;
|
|
160
188
|
/**
|
|
161
189
|
* IntersectionObserver configuration for `whileInView`. Mirrors framer-motion's
|
|
162
190
|
* `viewport` prop. Same shape as `UseInViewOptions` minus `initial` (which is
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/** Function returned by {@link useCycle} for advancing or jumping the index. */
|
|
2
|
+
export type Cycle = (next?: number) => void;
|
|
3
|
+
/**
|
|
4
|
+
* State returned by {@link useCycle}: an object with a reactive `.current`
|
|
5
|
+
* getter and a `cycle` function. Both reads and writes flow through the
|
|
6
|
+
* same object, so consumers don't need to destructure (which would
|
|
7
|
+
* snapshot `.current` and lose reactivity under runes).
|
|
8
|
+
*/
|
|
9
|
+
export type CycleState<T> = {
|
|
10
|
+
readonly current: T;
|
|
11
|
+
cycle: Cycle;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Function that returns the current items list, used by the reactive
|
|
15
|
+
* overload of {@link useCycle}. The function is re-invoked on every read
|
|
16
|
+
* so changes to the underlying reactive source propagate automatically.
|
|
17
|
+
*/
|
|
18
|
+
export type CycleItemsGetter<T> = () => readonly T[];
|
|
19
|
+
/**
|
|
20
|
+
* Cycles through a series of values. Mirrors framer-motion's `useCycle`.
|
|
21
|
+
*
|
|
22
|
+
* Two call forms:
|
|
23
|
+
*
|
|
24
|
+
* - **Varargs** — `useCycle(...items)` — items are captured once and stay
|
|
25
|
+
* fixed for the cycle's lifetime. Matches React framer-motion's signature.
|
|
26
|
+
* - **Reactive getter** — `useCycle(() => items)` — items are read on every
|
|
27
|
+
* access, so passing a `$state`/`$derived` source lets the cycle pick up
|
|
28
|
+
* list changes without recreating it.
|
|
29
|
+
*
|
|
30
|
+
* In both forms:
|
|
31
|
+
*
|
|
32
|
+
* - `state.current` is reactive — read it in templates / `$derived` / `$effect`
|
|
33
|
+
* and it tracks both index changes and (in the getter form) item changes.
|
|
34
|
+
* - `state.cycle()` advances to the next item (wrapping at the end).
|
|
35
|
+
* - `state.cycle(i)` jumps to index `i`. The index is stored as-given;
|
|
36
|
+
* `.current` then clamps on read so any out-of-range index — negative,
|
|
37
|
+
* overflow, or items shrinking underneath the reactive-getter form —
|
|
38
|
+
* resolves to the nearest valid edge (`items[0]` or `items[length - 1]`)
|
|
39
|
+
* instead of `undefined`. This is a defensive divergence from React
|
|
40
|
+
* framer-motion (which returns `items[i]`, possibly undefined) so the
|
|
41
|
+
* reactive form stays safe and `.current` always honors its `T` type.
|
|
42
|
+
* If the reactive getter ever returns an empty list, `.current` throws.
|
|
43
|
+
* - Calls that resolve to the current index are no-ops, matching React
|
|
44
|
+
* `useState`'s `Object.is` bail-out.
|
|
45
|
+
*
|
|
46
|
+
* Two deliberate divergences from React's `useCycle`:
|
|
47
|
+
*
|
|
48
|
+
* 1. Return shape — React's `[value, cycle]` tuple can't survive
|
|
49
|
+
* destructuring under Svelte 5 runes (snapshots the value, loses
|
|
50
|
+
* reactivity), so we return `{ current, cycle }`.
|
|
51
|
+
* 2. Out-of-range reads always clamp (see above) instead of returning
|
|
52
|
+
* `items[i]` undefined.
|
|
53
|
+
*
|
|
54
|
+
* Otherwise 1:1 with React, including same-index no-op bail-out and
|
|
55
|
+
* the `wrap(0, length, index + 1)` advance semantics.
|
|
56
|
+
*
|
|
57
|
+
* Ambiguity: `useCycle(fn)` with a single function value is treated as the
|
|
58
|
+
* reactive overload, not as a single-item cycle. To cycle through one
|
|
59
|
+
* function value, use `useCycle(() => [fn])` or just call it directly —
|
|
60
|
+
* a single-item cycle is a no-op anyway.
|
|
61
|
+
*
|
|
62
|
+
* @see https://motion.dev/docs/react-use-cycle
|
|
63
|
+
*
|
|
64
|
+
* @example Static varargs
|
|
65
|
+
* ```svelte
|
|
66
|
+
* <script lang="ts">
|
|
67
|
+
* import { motion, useCycle } from '@humanspeak/svelte-motion'
|
|
68
|
+
*
|
|
69
|
+
* const x = useCycle(0, 50, 100)
|
|
70
|
+
* </script>
|
|
71
|
+
*
|
|
72
|
+
* <motion.div animate={{ x: x.current }} onclick={() => x.cycle()} />
|
|
73
|
+
* ```
|
|
74
|
+
*
|
|
75
|
+
* @example Reactive items
|
|
76
|
+
* ```svelte
|
|
77
|
+
* <script lang="ts">
|
|
78
|
+
* let { labels }: { labels: string[] } = $props()
|
|
79
|
+
* const variant = useCycle(() => labels)
|
|
80
|
+
* </script>
|
|
81
|
+
*
|
|
82
|
+
* <motion.div animate={variant.current} onclick={() => variant.cycle()} />
|
|
83
|
+
* ```
|
|
84
|
+
*
|
|
85
|
+
* @param itemsGetter Function returning the current items list; re-invoked
|
|
86
|
+
* on every `.current` read so reactive sources propagate. Use this form
|
|
87
|
+
* when items can change between mount and unmount. (Reactive overload.)
|
|
88
|
+
* @param items One or more values to cycle through. Captured once at call
|
|
89
|
+
* time and fixed for the cycle's lifetime. (Varargs overload.)
|
|
90
|
+
* @returns A `CycleState<T>` with a reactive `.current` getter and a
|
|
91
|
+
* `cycle(next?: number)` advance/jump function. `.current` always
|
|
92
|
+
* honors its `T` type by clamping out-of-range indexes; it throws
|
|
93
|
+
* if a reactive getter empties the items list mid-cycle.
|
|
94
|
+
* `.cycle()` throws on non-integer (`NaN`, `1.5`, `Infinity`)
|
|
95
|
+
* indexes and returns early as a no-op on empty items.
|
|
96
|
+
*/
|
|
97
|
+
export declare function useCycle<T>(itemsGetter: CycleItemsGetter<T>): CycleState<T>;
|
|
98
|
+
export declare function useCycle<T>(...items: T[]): CycleState<T>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { wrap } from 'motion';
|
|
2
|
+
export function useCycle(...args) {
|
|
3
|
+
const getItems = args.length === 1 && typeof args[0] === 'function'
|
|
4
|
+
? args[0]
|
|
5
|
+
: () => args;
|
|
6
|
+
if (getItems().length === 0) {
|
|
7
|
+
throw new Error('useCycle requires at least one item');
|
|
8
|
+
}
|
|
9
|
+
let index = $state(0);
|
|
10
|
+
return {
|
|
11
|
+
get current() {
|
|
12
|
+
const items = getItems();
|
|
13
|
+
// Reactive-getter form: if the consumer's source emptied
|
|
14
|
+
// mid-cycle the public type can no longer be honored. Throw
|
|
15
|
+
// loudly so the bug surfaces immediately rather than leaking
|
|
16
|
+
// `undefined` through a `T`-typed read.
|
|
17
|
+
if (items.length === 0) {
|
|
18
|
+
throw new Error('useCycle items getter returned an empty list');
|
|
19
|
+
}
|
|
20
|
+
// Clamp on read so out-of-range indexes (from `cycle(-5)` or
|
|
21
|
+
// `cycle(99)`, or items shrinking under us in the getter form)
|
|
22
|
+
// resolve to the nearest valid edge instead of `undefined`.
|
|
23
|
+
const safeIndex = index < 0 ? 0 : index >= items.length ? items.length - 1 : index;
|
|
24
|
+
return items[safeIndex];
|
|
25
|
+
},
|
|
26
|
+
cycle: (next) => {
|
|
27
|
+
const items = getItems();
|
|
28
|
+
if (items.length === 0)
|
|
29
|
+
return;
|
|
30
|
+
// Reject non-finite / non-integer indexes up-front: `NaN` slips
|
|
31
|
+
// past the read-time clamp (NaN comparisons return false for
|
|
32
|
+
// both `< 0` and `>= length`) and would silently make `.current`
|
|
33
|
+
// resolve to `undefined`, breaking the `T` contract. Throw
|
|
34
|
+
// loudly to surface the consumer bug at write-time.
|
|
35
|
+
if (typeof next === 'number' && !Number.isInteger(next)) {
|
|
36
|
+
throw new Error('useCycle index must be a finite integer');
|
|
37
|
+
}
|
|
38
|
+
const target = typeof next === 'number' ? next : wrap(0, items.length, index + 1);
|
|
39
|
+
if (target === index)
|
|
40
|
+
return;
|
|
41
|
+
index = target;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { type FollowValueOptions, type MotionValue, type SpringOptions } from 'motion-dom';
|
|
2
|
+
import { type Readable } from 'svelte/store';
|
|
3
|
+
/**
|
|
4
|
+
* Spring + follow options for {@link useSpring}.
|
|
5
|
+
*
|
|
6
|
+
* Mirrors framer-motion's `useSpring` options 1:1: every `SpringOptions` key
|
|
7
|
+
* (`stiffness`, `damping`, `mass`, `duration`, `visualDuration`, `bounce`,
|
|
8
|
+
* `velocity`, `restDelta`, `restSpeed`) plus `skipInitialAnimation` for
|
|
9
|
+
* scroll-restoration scenarios.
|
|
10
|
+
*
|
|
11
|
+
* @see https://motion.dev/docs/react-use-spring
|
|
12
|
+
*/
|
|
13
|
+
export type UseSpringOptions = SpringOptions & Pick<FollowValueOptions, 'skipInitialAnimation'>;
|
|
14
|
+
/**
|
|
15
|
+
* The augmented `MotionValue` returned by {@link useSpring}.
|
|
16
|
+
*
|
|
17
|
+
* It IS a real `MotionValue<T>` (so it passes `isMotionValue`, composes with
|
|
18
|
+
* `animate()`, `useTransform`, and the rest of motion-dom). On top of that it
|
|
19
|
+
* adds two affordances:
|
|
20
|
+
*
|
|
21
|
+
* - `current` — a Svelte-5 reactive read backed by `$state`. Use in templates
|
|
22
|
+
* and `$derived` / `$effect` to track the spring value without subscribing.
|
|
23
|
+
* - `subscribe` — Svelte readable store contract. Calls the run function once
|
|
24
|
+
* synchronously with the current value, then on every change. Lets the
|
|
25
|
+
* spring be used with `$spring` template syntax, `get(spring)`, and as a
|
|
26
|
+
* dependency in `useTransform`'s function form.
|
|
27
|
+
*/
|
|
28
|
+
export type SpringMotionValue<T extends number | string> = Omit<MotionValue<T>, 'current'> & {
|
|
29
|
+
/** Reactive read in Svelte 5 templates / `$derived` / `$effect`. */
|
|
30
|
+
readonly current: T;
|
|
31
|
+
/** Svelte readable store compatibility. */
|
|
32
|
+
subscribe: (run: (value: T) => void) => () => void;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Creates a spring-animated `MotionValue`.
|
|
36
|
+
*
|
|
37
|
+
* Set a target with `.set(v)` to animate to it using spring physics, or
|
|
38
|
+
* `.jump(v)` to skip the animation. Pass another `MotionValue` (or, for
|
|
39
|
+
* backwards compatibility, a Svelte readable store like the ones from
|
|
40
|
+
* `useScroll` / `useTime`) as `source` and the spring will animate towards
|
|
41
|
+
* whatever that source emits.
|
|
42
|
+
*
|
|
43
|
+
* Returned object is a real motion-dom `MotionValue` — composes with
|
|
44
|
+
* `animate()`, `useTransform`, `useVelocity`, and motion-dom's animation
|
|
45
|
+
* engine. On top, it exposes:
|
|
46
|
+
*
|
|
47
|
+
* - `.current` — Svelte-5 reactive read for templates and `$derived` /
|
|
48
|
+
* `$effect`.
|
|
49
|
+
* - `.subscribe(run)` — Svelte readable store contract so `$spring` template
|
|
50
|
+
* syntax and `useTransform(() => …, [spring])` keep working during the
|
|
51
|
+
* Tier 2 migration window.
|
|
52
|
+
*
|
|
53
|
+
* Lifecycle: must be called during component initialization. Cleanup is
|
|
54
|
+
* registered via `$effect`; the spring stops animating and unsubscribes from
|
|
55
|
+
* its source when the surrounding component / effect tears down. Call
|
|
56
|
+
* `.destroy()` to clean up early.
|
|
57
|
+
*
|
|
58
|
+
* SSR-safe: returns a static `MotionValue` with no animation on the server.
|
|
59
|
+
*
|
|
60
|
+
* @template T
|
|
61
|
+
* @param {number|string|MotionValue<number>|MotionValue<string>|Readable<number|string>} source Initial value or a source to follow.
|
|
62
|
+
* @param {UseSpringOptions} [options] Spring + follow configuration.
|
|
63
|
+
* @returns {SpringMotionValue<T>} A `MotionValue` with `.current` and `.subscribe`.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```svelte
|
|
67
|
+
* <script lang="ts">
|
|
68
|
+
* import { useSpring } from '@humanspeak/svelte-motion'
|
|
69
|
+
*
|
|
70
|
+
* const x = useSpring(0, { stiffness: 300, damping: 30 })
|
|
71
|
+
* </script>
|
|
72
|
+
*
|
|
73
|
+
* <button onclick={() => x.set(100)}>Animate</button>
|
|
74
|
+
* <div>{x.current}</div>
|
|
75
|
+
* ```
|
|
76
|
+
*
|
|
77
|
+
* @see https://motion.dev/docs/react-use-spring
|
|
78
|
+
*/
|
|
79
|
+
export declare function useSpring(source: number, options?: UseSpringOptions): SpringMotionValue<number>;
|
|
80
|
+
export declare function useSpring(source: string, options?: UseSpringOptions): SpringMotionValue<string>;
|
|
81
|
+
export declare function useSpring(source: MotionValue<number>, options?: UseSpringOptions): SpringMotionValue<number>;
|
|
82
|
+
export declare function useSpring(source: MotionValue<string>, options?: UseSpringOptions): SpringMotionValue<string>;
|
|
83
|
+
export declare function useSpring<T extends number | string>(source: Readable<T>, options?: UseSpringOptions): SpringMotionValue<T>;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { attachFollow, isMotionValue, motionValue } from 'motion-dom';
|
|
2
|
+
import { get } from 'svelte/store';
|
|
3
|
+
/**
|
|
4
|
+
* Detects a Svelte readable store. Excludes motion-dom `MotionValue` instances
|
|
5
|
+
* (which also expose `subscribe`-shaped APIs in some versions) so the
|
|
6
|
+
* MotionValue path is preferred.
|
|
7
|
+
*/
|
|
8
|
+
const isSvelteReadable = (value) => {
|
|
9
|
+
return (!!value &&
|
|
10
|
+
typeof value === 'object' &&
|
|
11
|
+
typeof value.subscribe === 'function' &&
|
|
12
|
+
!isMotionValue(value));
|
|
13
|
+
};
|
|
14
|
+
export function useSpring(source, options = {}) {
|
|
15
|
+
// SSR: return a static MotionValue with no animation. Reads return the
|
|
16
|
+
// best-effort initial value; .set / .jump become no-ops to avoid drifting
|
|
17
|
+
// away from the server-rendered snapshot.
|
|
18
|
+
if (typeof window === 'undefined') {
|
|
19
|
+
const initial = readInitial(source);
|
|
20
|
+
const ssrValue = motionValue(initial);
|
|
21
|
+
ssrValue.set = () => undefined;
|
|
22
|
+
ssrValue.jump = () => undefined;
|
|
23
|
+
return augmentForSvelte(ssrValue, () => undefined);
|
|
24
|
+
}
|
|
25
|
+
// Resolve initial + follow source.
|
|
26
|
+
let followSource;
|
|
27
|
+
let cleanupReadableBridge;
|
|
28
|
+
let svelteBridge;
|
|
29
|
+
if (isMotionValue(source)) {
|
|
30
|
+
followSource = source;
|
|
31
|
+
}
|
|
32
|
+
else if (isSvelteReadable(source)) {
|
|
33
|
+
// Bridge a Svelte readable into a MotionValue so attachFollow can
|
|
34
|
+
// track it. Synchronous initial sample comes from svelte/store's get().
|
|
35
|
+
const initialFromReadable = get(source);
|
|
36
|
+
svelteBridge = motionValue(initialFromReadable);
|
|
37
|
+
cleanupReadableBridge = source.subscribe((v) => {
|
|
38
|
+
// The Svelte readable contract calls the subscriber synchronously
|
|
39
|
+
// with the current value on subscribe. Skip if it equals the
|
|
40
|
+
// already-seeded bridge value so attachFollow doesn't fire a
|
|
41
|
+
// spring on the initial emit. Subsequent emits go through set()
|
|
42
|
+
// and trigger animation.
|
|
43
|
+
if (svelteBridge.get() === v)
|
|
44
|
+
return;
|
|
45
|
+
svelteBridge.set(v);
|
|
46
|
+
});
|
|
47
|
+
followSource = svelteBridge;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
followSource = source;
|
|
51
|
+
}
|
|
52
|
+
const initial = isMotionValue(followSource) ? followSource.get() : followSource;
|
|
53
|
+
const value = motionValue(initial);
|
|
54
|
+
const stopFollow = attachFollow(value, followSource, { type: 'spring', ...options });
|
|
55
|
+
// Side-cleanup for our augmentations. Single-shot guard lives in the
|
|
56
|
+
// augmented `value.destroy` (the only caller), so no flag here.
|
|
57
|
+
const dispose = () => {
|
|
58
|
+
stopFollow?.();
|
|
59
|
+
cleanupReadableBridge?.();
|
|
60
|
+
svelteBridge?.destroy();
|
|
61
|
+
};
|
|
62
|
+
$effect(() => () => value.destroy());
|
|
63
|
+
return augmentForSvelte(value, dispose);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Pull the synchronous initial value out of any accepted source form.
|
|
67
|
+
*/
|
|
68
|
+
const readInitial = (source) => {
|
|
69
|
+
if (typeof source === 'number' || typeof source === 'string')
|
|
70
|
+
return source;
|
|
71
|
+
if (isMotionValue(source))
|
|
72
|
+
return source.get();
|
|
73
|
+
if (isSvelteReadable(source))
|
|
74
|
+
return get(source);
|
|
75
|
+
return 0;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Layer Svelte-friendly affordances onto a motion-dom MotionValue: a
|
|
79
|
+
* `$state`-tracked `.current` accessor (routing motion-dom's internal
|
|
80
|
+
* `this.current = v` writes through `$state` so templates and `$derived` /
|
|
81
|
+
* `$effect` re-run) and a Svelte readable store `.subscribe(run)` shim.
|
|
82
|
+
*/
|
|
83
|
+
const augmentForSvelte = (value, dispose) => {
|
|
84
|
+
let current = $state(value.get());
|
|
85
|
+
Object.defineProperty(value, 'current', {
|
|
86
|
+
get: () => current,
|
|
87
|
+
// Same-value writes are no-ops: motion-dom's `updateAndNotify` calls
|
|
88
|
+
// `setCurrent(v)` before its own change check, so spring frames at
|
|
89
|
+
// rest still hit this setter; skipping equal writes avoids gratuitous
|
|
90
|
+
// accessor work even though $state would itself dedupe.
|
|
91
|
+
set: (v) => {
|
|
92
|
+
if (v !== current)
|
|
93
|
+
current = v;
|
|
94
|
+
},
|
|
95
|
+
enumerable: true,
|
|
96
|
+
configurable: true
|
|
97
|
+
});
|
|
98
|
+
const originalDestroy = value.destroy.bind(value);
|
|
99
|
+
let destroyed = false;
|
|
100
|
+
value.destroy = () => {
|
|
101
|
+
if (destroyed)
|
|
102
|
+
return;
|
|
103
|
+
destroyed = true;
|
|
104
|
+
dispose();
|
|
105
|
+
originalDestroy();
|
|
106
|
+
};
|
|
107
|
+
const subscribe = (run) => {
|
|
108
|
+
run(value.get());
|
|
109
|
+
return value.on('change', run);
|
|
110
|
+
};
|
|
111
|
+
Object.defineProperty(value, 'subscribe', {
|
|
112
|
+
value: subscribe,
|
|
113
|
+
writable: false,
|
|
114
|
+
enumerable: false,
|
|
115
|
+
configurable: true
|
|
116
|
+
});
|
|
117
|
+
return value;
|
|
118
|
+
};
|
package/dist/utils/variants.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { MotionAnimate, MotionExit, MotionInitial, Variants } from '../types';
|
|
1
|
+
import type { MotionAnimate, MotionExit, MotionInitial, MotionWhileDrag, MotionWhileFocus, MotionWhileHover, MotionWhileInView, MotionWhileTap, Variants } from '../types';
|
|
2
2
|
import type { DOMKeyframesDefinition } from 'motion';
|
|
3
3
|
/**
|
|
4
4
|
* Resolves a variant key to its keyframes definition.
|
|
@@ -27,6 +27,33 @@ import type { DOMKeyframesDefinition } from 'motion';
|
|
|
27
27
|
* ```
|
|
28
28
|
*/
|
|
29
29
|
export declare const resolveVariant: (variants: Variants | undefined, key: string | undefined, custom?: unknown) => DOMKeyframesDefinition | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* Resolves a single variant key or an ordered list of keys to merged
|
|
32
|
+
* keyframes. Matches framer-motion's `VariantLabels = string | string[]`
|
|
33
|
+
* surface: later keys in the list override earlier ones on key
|
|
34
|
+
* collisions (`Object.assign` semantics).
|
|
35
|
+
*
|
|
36
|
+
* Missing keys are skipped. An empty list, an empty string, or an
|
|
37
|
+
* undefined argument all resolve to `undefined`.
|
|
38
|
+
*
|
|
39
|
+
* @param variants - The variants object containing named animation states.
|
|
40
|
+
* @param keys - A single variant key or an array of variant keys.
|
|
41
|
+
* @param custom - Forwarded to function-form variants (per-entry).
|
|
42
|
+
* @returns Merged keyframes definition, or `undefined` when nothing resolved.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* const variants = {
|
|
47
|
+
* hover: { scale: 1.1 },
|
|
48
|
+
* active: { scale: 1.2, color: 'red' }
|
|
49
|
+
* }
|
|
50
|
+
* resolveVariantList(variants, 'hover') // { scale: 1.1 }
|
|
51
|
+
* resolveVariantList(variants, ['hover', 'active']) // { scale: 1.2, color: 'red' }
|
|
52
|
+
* resolveVariantList(variants, ['hover', 'missing']) // { scale: 1.1 }
|
|
53
|
+
* resolveVariantList(variants, []) // undefined
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export declare const resolveVariantList: (variants: Variants | undefined, keys: string | string[] | undefined, custom?: unknown) => DOMKeyframesDefinition | undefined;
|
|
30
57
|
/**
|
|
31
58
|
* Resolves the initial prop to keyframes, handling variant keys and `initial={false}`.
|
|
32
59
|
*
|
|
@@ -52,25 +79,72 @@ export declare const resolveInitial: (initial: MotionInitial, variants: Variants
|
|
|
52
79
|
/**
|
|
53
80
|
* Resolves the animate prop to keyframes, handling variant keys.
|
|
54
81
|
*
|
|
55
|
-
* When `animate` is a string, looks it up in the
|
|
56
|
-
* dynamic variants with `custom`). Otherwise
|
|
82
|
+
* When `animate` is a string (or array of strings), looks it up in the
|
|
83
|
+
* variants object (invoking dynamic variants with `custom`). Otherwise
|
|
84
|
+
* returns the keyframes directly.
|
|
57
85
|
*
|
|
58
|
-
* @param animate - The animate prop value (keyframes, variant key,
|
|
86
|
+
* @param animate - The animate prop value (keyframes, variant key, array
|
|
87
|
+
* of variant keys, or undefined).
|
|
59
88
|
* @param variants - The variants object for resolving string keys.
|
|
60
89
|
* @param custom - Forwarded to function-form variants.
|
|
61
90
|
* @returns Keyframes definition or undefined.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```ts
|
|
94
|
+
* const variants = { visible: { opacity: 1 }, shifted: { x: 100 } }
|
|
95
|
+
* resolveAnimate('visible', variants) // { opacity: 1 }
|
|
96
|
+
* resolveAnimate(['visible', 'shifted'], variants) // { opacity: 1, x: 100 }
|
|
97
|
+
* resolveAnimate({ scale: 1.2 }, variants) // { scale: 1.2 } (pass-through)
|
|
98
|
+
* resolveAnimate(undefined, variants) // undefined
|
|
99
|
+
* ```
|
|
62
100
|
*/
|
|
63
101
|
export declare const resolveAnimate: (animate: MotionAnimate, variants: Variants | undefined, custom?: unknown) => DOMKeyframesDefinition | undefined;
|
|
64
102
|
/**
|
|
65
103
|
* Resolves the exit prop to keyframes, handling variant keys.
|
|
66
104
|
*
|
|
67
|
-
* When `exit` is a string, looks it up in the
|
|
68
|
-
* dynamic variants with `custom`). Otherwise
|
|
69
|
-
* Used by AnimatePresence for exit
|
|
105
|
+
* When `exit` is a string (or array of strings), looks it up in the
|
|
106
|
+
* variants object (invoking dynamic variants with `custom`). Otherwise
|
|
107
|
+
* returns the keyframes directly. Used by AnimatePresence for exit
|
|
108
|
+
* animations.
|
|
70
109
|
*
|
|
71
|
-
* @param exit - The exit prop value (keyframes, variant key,
|
|
110
|
+
* @param exit - The exit prop value (keyframes, variant key, array of
|
|
111
|
+
* variant keys, or undefined).
|
|
72
112
|
* @param variants - The variants object for resolving string keys.
|
|
73
113
|
* @param custom - Forwarded to function-form variants.
|
|
74
114
|
* @returns Keyframes definition or undefined.
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* ```ts
|
|
118
|
+
* const variants = { hidden: { opacity: 0 }, small: { scale: 0.8 } }
|
|
119
|
+
* resolveExit('hidden', variants) // { opacity: 0 }
|
|
120
|
+
* resolveExit(['hidden', 'small'], variants) // { opacity: 0, scale: 0.8 }
|
|
121
|
+
* resolveExit({ y: -20 }, variants) // { y: -20 }
|
|
122
|
+
* resolveExit(undefined, variants) // undefined
|
|
123
|
+
* ```
|
|
75
124
|
*/
|
|
76
125
|
export declare const resolveExit: (exit: MotionExit, variants: Variants | undefined, custom?: unknown) => DOMKeyframesDefinition | undefined;
|
|
126
|
+
/**
|
|
127
|
+
* Resolves a `whileX` prop (hover, tap, focus, drag, in-view) to
|
|
128
|
+
* keyframes. Mirrors `resolveAnimate` — pass-through for inline
|
|
129
|
+
* keyframes, look up variant keys via `resolveVariantList` (single
|
|
130
|
+
* string or array of strings, merged left-to-right).
|
|
131
|
+
*
|
|
132
|
+
* Used by `_MotionContainer.svelte` to feed the gesture attach helpers
|
|
133
|
+
* a consistent keyframes object regardless of whether the consumer
|
|
134
|
+
* wrote inline keyframes or a variant reference.
|
|
135
|
+
*
|
|
136
|
+
* @param value - The whileX prop value.
|
|
137
|
+
* @param variants - The variants object for resolving string keys.
|
|
138
|
+
* @param custom - Forwarded to function-form variants.
|
|
139
|
+
* @returns Keyframes definition or `undefined` when nothing applies.
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* ```ts
|
|
143
|
+
* const variants = { hover: { scale: 1.1 }, active: { color: 'red' } }
|
|
144
|
+
* resolveWhile('hover', variants) // { scale: 1.1 }
|
|
145
|
+
* resolveWhile(['hover', 'active'], variants) // { scale: 1.1, color: 'red' }
|
|
146
|
+
* resolveWhile({ scale: 1.2 }, variants) // { scale: 1.2 } (pass-through)
|
|
147
|
+
* resolveWhile(undefined, variants) // undefined
|
|
148
|
+
* ```
|
|
149
|
+
*/
|
|
150
|
+
export declare const resolveWhile: (value: MotionWhileTap | MotionWhileHover | MotionWhileFocus | MotionWhileDrag | MotionWhileInView, variants: Variants | undefined, custom?: unknown) => DOMKeyframesDefinition | undefined;
|
package/dist/utils/variants.js
CHANGED
|
@@ -27,11 +27,69 @@
|
|
|
27
27
|
export const resolveVariant = (variants, key, custom) => {
|
|
28
28
|
if (!variants || !key)
|
|
29
29
|
return undefined;
|
|
30
|
+
// Guard against built-in / inherited keys like 'toString' or
|
|
31
|
+
// 'constructor' — without this, `whileHover="toString"` would
|
|
32
|
+
// resolve to `Function.prototype.toString` and leak a function into
|
|
33
|
+
// the merge path.
|
|
34
|
+
if (!Object.prototype.hasOwnProperty.call(variants, key))
|
|
35
|
+
return undefined;
|
|
30
36
|
const entry = variants[key];
|
|
31
37
|
if (typeof entry === 'function')
|
|
32
38
|
return entry(custom);
|
|
33
39
|
return entry;
|
|
34
40
|
};
|
|
41
|
+
/**
|
|
42
|
+
* Resolves a single variant key or an ordered list of keys to merged
|
|
43
|
+
* keyframes. Matches framer-motion's `VariantLabels = string | string[]`
|
|
44
|
+
* surface: later keys in the list override earlier ones on key
|
|
45
|
+
* collisions (`Object.assign` semantics).
|
|
46
|
+
*
|
|
47
|
+
* Missing keys are skipped. An empty list, an empty string, or an
|
|
48
|
+
* undefined argument all resolve to `undefined`.
|
|
49
|
+
*
|
|
50
|
+
* @param variants - The variants object containing named animation states.
|
|
51
|
+
* @param keys - A single variant key or an array of variant keys.
|
|
52
|
+
* @param custom - Forwarded to function-form variants (per-entry).
|
|
53
|
+
* @returns Merged keyframes definition, or `undefined` when nothing resolved.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts
|
|
57
|
+
* const variants = {
|
|
58
|
+
* hover: { scale: 1.1 },
|
|
59
|
+
* active: { scale: 1.2, color: 'red' }
|
|
60
|
+
* }
|
|
61
|
+
* resolveVariantList(variants, 'hover') // { scale: 1.1 }
|
|
62
|
+
* resolveVariantList(variants, ['hover', 'active']) // { scale: 1.2, color: 'red' }
|
|
63
|
+
* resolveVariantList(variants, ['hover', 'missing']) // { scale: 1.1 }
|
|
64
|
+
* resolveVariantList(variants, []) // undefined
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export const resolveVariantList = (variants, keys, custom) => {
|
|
68
|
+
if (keys === undefined)
|
|
69
|
+
return undefined;
|
|
70
|
+
if (typeof keys === 'string')
|
|
71
|
+
return resolveVariant(variants, keys, custom);
|
|
72
|
+
if (keys.length === 0)
|
|
73
|
+
return undefined;
|
|
74
|
+
let merged;
|
|
75
|
+
for (const key of keys) {
|
|
76
|
+
const entry = resolveVariant(variants, key, custom);
|
|
77
|
+
// Defensive: only merge plain keyframe objects. A function-form
|
|
78
|
+
// variant could return something else (array, class instance,
|
|
79
|
+
// string) under a misuse, and spreading those would corrupt the
|
|
80
|
+
// merged result. Reject arrays explicitly and require the
|
|
81
|
+
// prototype to be `Object.prototype` (or `null` for objects
|
|
82
|
+
// created via `Object.create(null)`).
|
|
83
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry))
|
|
84
|
+
continue;
|
|
85
|
+
const proto = Object.getPrototypeOf(entry);
|
|
86
|
+
if (proto !== Object.prototype && proto !== null)
|
|
87
|
+
continue;
|
|
88
|
+
const obj = entry;
|
|
89
|
+
merged = merged ? { ...merged, ...obj } : { ...obj };
|
|
90
|
+
}
|
|
91
|
+
return merged;
|
|
92
|
+
};
|
|
35
93
|
/**
|
|
36
94
|
* Resolves the initial prop to keyframes, handling variant keys and `initial={false}`.
|
|
37
95
|
*
|
|
@@ -58,44 +116,97 @@ export const resolveInitial = (initial, variants, custom) => {
|
|
|
58
116
|
return false;
|
|
59
117
|
if (initial === undefined)
|
|
60
118
|
return undefined;
|
|
61
|
-
if (typeof initial === 'string')
|
|
62
|
-
return
|
|
119
|
+
if (typeof initial === 'string' || Array.isArray(initial))
|
|
120
|
+
return resolveVariantList(variants, initial, custom);
|
|
63
121
|
return initial;
|
|
64
122
|
};
|
|
65
123
|
/**
|
|
66
124
|
* Resolves the animate prop to keyframes, handling variant keys.
|
|
67
125
|
*
|
|
68
|
-
* When `animate` is a string, looks it up in the
|
|
69
|
-
* dynamic variants with `custom`). Otherwise
|
|
126
|
+
* When `animate` is a string (or array of strings), looks it up in the
|
|
127
|
+
* variants object (invoking dynamic variants with `custom`). Otherwise
|
|
128
|
+
* returns the keyframes directly.
|
|
70
129
|
*
|
|
71
|
-
* @param animate - The animate prop value (keyframes, variant key,
|
|
130
|
+
* @param animate - The animate prop value (keyframes, variant key, array
|
|
131
|
+
* of variant keys, or undefined).
|
|
72
132
|
* @param variants - The variants object for resolving string keys.
|
|
73
133
|
* @param custom - Forwarded to function-form variants.
|
|
74
134
|
* @returns Keyframes definition or undefined.
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* ```ts
|
|
138
|
+
* const variants = { visible: { opacity: 1 }, shifted: { x: 100 } }
|
|
139
|
+
* resolveAnimate('visible', variants) // { opacity: 1 }
|
|
140
|
+
* resolveAnimate(['visible', 'shifted'], variants) // { opacity: 1, x: 100 }
|
|
141
|
+
* resolveAnimate({ scale: 1.2 }, variants) // { scale: 1.2 } (pass-through)
|
|
142
|
+
* resolveAnimate(undefined, variants) // undefined
|
|
143
|
+
* ```
|
|
75
144
|
*/
|
|
76
145
|
export const resolveAnimate = (animate, variants, custom) => {
|
|
77
146
|
if (animate === undefined)
|
|
78
147
|
return undefined;
|
|
79
|
-
if (typeof animate === 'string')
|
|
80
|
-
return
|
|
148
|
+
if (typeof animate === 'string' || Array.isArray(animate))
|
|
149
|
+
return resolveVariantList(variants, animate, custom);
|
|
81
150
|
return animate;
|
|
82
151
|
};
|
|
83
152
|
/**
|
|
84
153
|
* Resolves the exit prop to keyframes, handling variant keys.
|
|
85
154
|
*
|
|
86
|
-
* When `exit` is a string, looks it up in the
|
|
87
|
-
* dynamic variants with `custom`). Otherwise
|
|
88
|
-
* Used by AnimatePresence for exit
|
|
155
|
+
* When `exit` is a string (or array of strings), looks it up in the
|
|
156
|
+
* variants object (invoking dynamic variants with `custom`). Otherwise
|
|
157
|
+
* returns the keyframes directly. Used by AnimatePresence for exit
|
|
158
|
+
* animations.
|
|
89
159
|
*
|
|
90
|
-
* @param exit - The exit prop value (keyframes, variant key,
|
|
160
|
+
* @param exit - The exit prop value (keyframes, variant key, array of
|
|
161
|
+
* variant keys, or undefined).
|
|
91
162
|
* @param variants - The variants object for resolving string keys.
|
|
92
163
|
* @param custom - Forwarded to function-form variants.
|
|
93
164
|
* @returns Keyframes definition or undefined.
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* ```ts
|
|
168
|
+
* const variants = { hidden: { opacity: 0 }, small: { scale: 0.8 } }
|
|
169
|
+
* resolveExit('hidden', variants) // { opacity: 0 }
|
|
170
|
+
* resolveExit(['hidden', 'small'], variants) // { opacity: 0, scale: 0.8 }
|
|
171
|
+
* resolveExit({ y: -20 }, variants) // { y: -20 }
|
|
172
|
+
* resolveExit(undefined, variants) // undefined
|
|
173
|
+
* ```
|
|
94
174
|
*/
|
|
95
175
|
export const resolveExit = (exit, variants, custom) => {
|
|
96
176
|
if (exit === undefined)
|
|
97
177
|
return undefined;
|
|
98
|
-
if (typeof exit === 'string')
|
|
99
|
-
return
|
|
178
|
+
if (typeof exit === 'string' || Array.isArray(exit))
|
|
179
|
+
return resolveVariantList(variants, exit, custom);
|
|
100
180
|
return exit;
|
|
101
181
|
};
|
|
182
|
+
/**
|
|
183
|
+
* Resolves a `whileX` prop (hover, tap, focus, drag, in-view) to
|
|
184
|
+
* keyframes. Mirrors `resolveAnimate` — pass-through for inline
|
|
185
|
+
* keyframes, look up variant keys via `resolveVariantList` (single
|
|
186
|
+
* string or array of strings, merged left-to-right).
|
|
187
|
+
*
|
|
188
|
+
* Used by `_MotionContainer.svelte` to feed the gesture attach helpers
|
|
189
|
+
* a consistent keyframes object regardless of whether the consumer
|
|
190
|
+
* wrote inline keyframes or a variant reference.
|
|
191
|
+
*
|
|
192
|
+
* @param value - The whileX prop value.
|
|
193
|
+
* @param variants - The variants object for resolving string keys.
|
|
194
|
+
* @param custom - Forwarded to function-form variants.
|
|
195
|
+
* @returns Keyframes definition or `undefined` when nothing applies.
|
|
196
|
+
*
|
|
197
|
+
* @example
|
|
198
|
+
* ```ts
|
|
199
|
+
* const variants = { hover: { scale: 1.1 }, active: { color: 'red' } }
|
|
200
|
+
* resolveWhile('hover', variants) // { scale: 1.1 }
|
|
201
|
+
* resolveWhile(['hover', 'active'], variants) // { scale: 1.1, color: 'red' }
|
|
202
|
+
* resolveWhile({ scale: 1.2 }, variants) // { scale: 1.2 } (pass-through)
|
|
203
|
+
* resolveWhile(undefined, variants) // undefined
|
|
204
|
+
* ```
|
|
205
|
+
*/
|
|
206
|
+
export const resolveWhile = (value, variants, custom) => {
|
|
207
|
+
if (value === undefined)
|
|
208
|
+
return undefined;
|
|
209
|
+
if (typeof value === 'string' || Array.isArray(value))
|
|
210
|
+
return resolveVariantList(variants, value, custom);
|
|
211
|
+
return value;
|
|
212
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@humanspeak/svelte-motion",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.8",
|
|
4
4
|
"description": "Framer Motion for Svelte 5. Declarative motion.<tag> components with AnimatePresence exit animations, gestures (hover, tap, drag, focus, in-view), variants, FLIP layout animations, shared-layout transitions, spring physics, and scroll-linked motion values. The drop-in Framer Motion alternative for Svelte and SvelteKit.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"svelte",
|
|
@@ -95,8 +95,8 @@
|
|
|
95
95
|
},
|
|
96
96
|
"dependencies": {
|
|
97
97
|
"acorn": "^8.16.0",
|
|
98
|
-
"motion": "^12.
|
|
99
|
-
"motion-dom": "^12.
|
|
98
|
+
"motion": "^12.40.0",
|
|
99
|
+
"motion-dom": "^12.40.0"
|
|
100
100
|
},
|
|
101
101
|
"devDependencies": {
|
|
102
102
|
"@changesets/cli": "^2.31.0",
|
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
"@testing-library/jest-dom": "^6.9.1",
|
|
116
116
|
"@testing-library/svelte": "^5.3.1",
|
|
117
117
|
"@types/node": "^25.9.1",
|
|
118
|
-
"@vitest/coverage-v8": "^4.1.
|
|
118
|
+
"@vitest/coverage-v8": "^4.1.7",
|
|
119
119
|
"eslint": "^10.4.0",
|
|
120
120
|
"eslint-config-prettier": "10.1.8",
|
|
121
121
|
"eslint-plugin-import": "2.32.0",
|
|
@@ -127,7 +127,7 @@
|
|
|
127
127
|
"html-void-elements": "^3.0.0",
|
|
128
128
|
"husky": "^9.1.7",
|
|
129
129
|
"jsdom": "^29.1.1",
|
|
130
|
-
"mprocs": "^0.9.
|
|
130
|
+
"mprocs": "^0.9.3",
|
|
131
131
|
"prettier": "^3.8.3",
|
|
132
132
|
"prettier-plugin-organize-imports": "^4.3.0",
|
|
133
133
|
"prettier-plugin-sort-json": "^4.2.0",
|
|
@@ -135,7 +135,7 @@
|
|
|
135
135
|
"prettier-plugin-tailwindcss": "^0.8.0",
|
|
136
136
|
"publint": "^0.3.21",
|
|
137
137
|
"runed": "0.37.1",
|
|
138
|
-
"svelte": "^5.55.
|
|
138
|
+
"svelte": "^5.55.9",
|
|
139
139
|
"svelte-check": "^4.4.8",
|
|
140
140
|
"svg-tags": "^1.0.0",
|
|
141
141
|
"tailwind-merge": "^3.6.0",
|
|
@@ -145,9 +145,9 @@
|
|
|
145
145
|
"tsx": "^4.22.3",
|
|
146
146
|
"typescript": "^6.0.3",
|
|
147
147
|
"typescript-eslint": "^8.59.4",
|
|
148
|
-
"vite": "^8.0.
|
|
148
|
+
"vite": "^8.0.14",
|
|
149
149
|
"vite-tsconfig-paths": "^6.1.1",
|
|
150
|
-
"vitest": "^4.1.
|
|
150
|
+
"vitest": "^4.1.7"
|
|
151
151
|
},
|
|
152
152
|
"peerDependencies": {
|
|
153
153
|
"svelte": "^5.0.0"
|
package/dist/utils/cycle.d.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { type Readable } from 'svelte/store';
|
|
2
|
-
export type Cycle = (next?: number) => void;
|
|
3
|
-
export type CycleState<T> = [Readable<T>, Cycle];
|
|
4
|
-
/**
|
|
5
|
-
* Cycles through a series of values. Mirrors Framer Motion's `useCycle`.
|
|
6
|
-
*
|
|
7
|
-
* Returns a tuple `[value, cycle]`:
|
|
8
|
-
*
|
|
9
|
-
* - `value` is a Svelte readable store of the current item; subscribe with
|
|
10
|
-
* `$value` in templates.
|
|
11
|
-
* - `cycle()` advances to the next item, wrapping back to index `0` when it
|
|
12
|
-
* passes the end.
|
|
13
|
-
* - `cycle(i)` jumps to the item at index `i`. The index is taken as-is to
|
|
14
|
-
* match `framer-motion` — out-of-range values yield `items[i]`, which
|
|
15
|
-
* may be `undefined`.
|
|
16
|
-
*
|
|
17
|
-
* Calls that resolve to the current index are no-ops and do not notify
|
|
18
|
-
* subscribers, matching React `useState`'s `Object.is` bail-out semantics.
|
|
19
|
-
*
|
|
20
|
-
* @param items - Items to cycle through. Must include at least one item.
|
|
21
|
-
* @returns A `[Readable<T>, Cycle]` tuple.
|
|
22
|
-
* @see https://motion.dev/docs/react-use-cycle
|
|
23
|
-
*
|
|
24
|
-
* @example
|
|
25
|
-
* ```svelte
|
|
26
|
-
* <script>
|
|
27
|
-
* import { motion, useCycle } from '@humanspeak/svelte-motion'
|
|
28
|
-
*
|
|
29
|
-
* const [x, cycleX] = useCycle(0, 50, 100)
|
|
30
|
-
* </script>
|
|
31
|
-
*
|
|
32
|
-
* <motion.div animate={{ x: $x }} onclick={() => cycleX()} />
|
|
33
|
-
* ```
|
|
34
|
-
*/
|
|
35
|
-
export declare const useCycle: <T>(...items: T[]) => CycleState<T>;
|
package/dist/utils/cycle.js
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { wrap } from 'motion';
|
|
2
|
-
import { writable } from 'svelte/store';
|
|
3
|
-
/**
|
|
4
|
-
* Cycles through a series of values. Mirrors Framer Motion's `useCycle`.
|
|
5
|
-
*
|
|
6
|
-
* Returns a tuple `[value, cycle]`:
|
|
7
|
-
*
|
|
8
|
-
* - `value` is a Svelte readable store of the current item; subscribe with
|
|
9
|
-
* `$value` in templates.
|
|
10
|
-
* - `cycle()` advances to the next item, wrapping back to index `0` when it
|
|
11
|
-
* passes the end.
|
|
12
|
-
* - `cycle(i)` jumps to the item at index `i`. The index is taken as-is to
|
|
13
|
-
* match `framer-motion` — out-of-range values yield `items[i]`, which
|
|
14
|
-
* may be `undefined`.
|
|
15
|
-
*
|
|
16
|
-
* Calls that resolve to the current index are no-ops and do not notify
|
|
17
|
-
* subscribers, matching React `useState`'s `Object.is` bail-out semantics.
|
|
18
|
-
*
|
|
19
|
-
* @param items - Items to cycle through. Must include at least one item.
|
|
20
|
-
* @returns A `[Readable<T>, Cycle]` tuple.
|
|
21
|
-
* @see https://motion.dev/docs/react-use-cycle
|
|
22
|
-
*
|
|
23
|
-
* @example
|
|
24
|
-
* ```svelte
|
|
25
|
-
* <script>
|
|
26
|
-
* import { motion, useCycle } from '@humanspeak/svelte-motion'
|
|
27
|
-
*
|
|
28
|
-
* const [x, cycleX] = useCycle(0, 50, 100)
|
|
29
|
-
* </script>
|
|
30
|
-
*
|
|
31
|
-
* <motion.div animate={{ x: $x }} onclick={() => cycleX()} />
|
|
32
|
-
* ```
|
|
33
|
-
*/
|
|
34
|
-
export const useCycle = (...items) => {
|
|
35
|
-
if (items.length === 0) {
|
|
36
|
-
throw new Error('useCycle requires at least one item');
|
|
37
|
-
}
|
|
38
|
-
let index = 0;
|
|
39
|
-
const store = writable(items[0]);
|
|
40
|
-
const cycle = (next) => {
|
|
41
|
-
const target = typeof next === 'number' ? next : wrap(0, items.length, index + 1);
|
|
42
|
-
if (target === index)
|
|
43
|
-
return;
|
|
44
|
-
index = target;
|
|
45
|
-
store.set(items[target]);
|
|
46
|
-
};
|
|
47
|
-
return [{ subscribe: store.subscribe }, cycle];
|
|
48
|
-
};
|
package/dist/utils/spring.d.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { type Readable } from 'svelte/store';
|
|
2
|
-
/**
|
|
3
|
-
* Spring configuration options.
|
|
4
|
-
*
|
|
5
|
-
* This is a minimal subset modeled after Motion's spring transition options.
|
|
6
|
-
* Values are tuned for sensible defaults, not parity.
|
|
7
|
-
*
|
|
8
|
-
* @typedef {Object} SpringOptions
|
|
9
|
-
* @property {number=} stiffness Spring stiffness (higher = snappier). Default 170.
|
|
10
|
-
* @property {number=} damping Spring damping (higher = less oscillation). Default 26.
|
|
11
|
-
* @property {number=} mass Mass of the object. Default 1.
|
|
12
|
-
* @property {number=} restDelta Threshold for absolute position delta to stop. Default 0.01.
|
|
13
|
-
* @property {number=} restSpeed Threshold for velocity magnitude to stop. Default 0.01.
|
|
14
|
-
*/
|
|
15
|
-
export type SpringOptions = {
|
|
16
|
-
stiffness?: number;
|
|
17
|
-
damping?: number;
|
|
18
|
-
mass?: number;
|
|
19
|
-
restDelta?: number;
|
|
20
|
-
restSpeed?: number;
|
|
21
|
-
};
|
|
22
|
-
/**
|
|
23
|
-
* Function type for updating the spring's target with animation.
|
|
24
|
-
*
|
|
25
|
-
* @param v New target value to animate towards (number or unit string).
|
|
26
|
-
*/
|
|
27
|
-
type SetType = (v: number | string) => void;
|
|
28
|
-
/**
|
|
29
|
-
* Function type for immediately setting the spring's value without animation.
|
|
30
|
-
*
|
|
31
|
-
* @param v New value to set instantly (number or unit string).
|
|
32
|
-
*/
|
|
33
|
-
type JumpType = (v: number | string) => void;
|
|
34
|
-
/**
|
|
35
|
-
* Creates a spring-animated readable store. The store exposes `set` to
|
|
36
|
-
* animate towards a target, or `jump` to immediately set the value without
|
|
37
|
-
* animation. When constructed with another readable store, the spring
|
|
38
|
-
* automatically follows it.
|
|
39
|
-
*
|
|
40
|
-
* This is SSR-safe: On the server it returns a static store and no timers run.
|
|
41
|
-
*
|
|
42
|
-
* @template T
|
|
43
|
-
* @param {number|string|Readable<number|string>} source Initial value or a source store to follow.
|
|
44
|
-
* @param {SpringOptions=} options Spring configuration.
|
|
45
|
-
* @returns {Readable<number|string> & { set: (v: number|string) => void; jump: (v: number|string) => void; }}
|
|
46
|
-
*/
|
|
47
|
-
export declare const useSpring: (source: number | string | Readable<number | string>, options?: SpringOptions) => Readable<number | string> & {
|
|
48
|
-
set: SetType;
|
|
49
|
-
jump: JumpType;
|
|
50
|
-
};
|
|
51
|
-
export {};
|
package/dist/utils/spring.js
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import { readable, writable } from 'svelte/store';
|
|
2
|
-
/**
|
|
3
|
-
* Parses a number or unit string into numeric value and unit.
|
|
4
|
-
* @param {number|string} v The input value.
|
|
5
|
-
* @returns {UnitValue} Parsed value and unit.
|
|
6
|
-
* @private
|
|
7
|
-
*/
|
|
8
|
-
const parseUnit = (v) => {
|
|
9
|
-
if (typeof v === 'number')
|
|
10
|
-
return { value: v, unit: '' };
|
|
11
|
-
const match = String(v).match(/^(-?\d*\.?\d+)(.*)$/);
|
|
12
|
-
if (!match || !match[1])
|
|
13
|
-
return { value: 0, unit: '' };
|
|
14
|
-
const parsed = Number.parseFloat(match[1]);
|
|
15
|
-
if (!Number.isFinite(parsed))
|
|
16
|
-
return { value: 0, unit: '' };
|
|
17
|
-
const unit = match[2] ?? '';
|
|
18
|
-
return { value: parsed, unit };
|
|
19
|
-
};
|
|
20
|
-
/**
|
|
21
|
-
* Formats a numeric value with a unit.
|
|
22
|
-
* @param {number} n Numeric value.
|
|
23
|
-
* @param {string} unit Unit suffix.
|
|
24
|
-
* @returns {number|string} Number or string with unit.
|
|
25
|
-
* @private
|
|
26
|
-
*/
|
|
27
|
-
const formatUnit = (n, unit) => (unit ? `${n}${unit}` : n);
|
|
28
|
-
/**
|
|
29
|
-
* Creates a spring-animated readable store. The store exposes `set` to
|
|
30
|
-
* animate towards a target, or `jump` to immediately set the value without
|
|
31
|
-
* animation. When constructed with another readable store, the spring
|
|
32
|
-
* automatically follows it.
|
|
33
|
-
*
|
|
34
|
-
* This is SSR-safe: On the server it returns a static store and no timers run.
|
|
35
|
-
*
|
|
36
|
-
* @template T
|
|
37
|
-
* @param {number|string|Readable<number|string>} source Initial value or a source store to follow.
|
|
38
|
-
* @param {SpringOptions=} options Spring configuration.
|
|
39
|
-
* @returns {Readable<number|string> & { set: (v: number|string) => void; jump: (v: number|string) => void; }}
|
|
40
|
-
*/
|
|
41
|
-
export const useSpring = (source, options = {}) => {
|
|
42
|
-
if (typeof window === 'undefined') {
|
|
43
|
-
// Derive best-effort initial value for SSR to avoid hydration mismatch
|
|
44
|
-
let initial = 0;
|
|
45
|
-
if (typeof source === 'number' || typeof source === 'string') {
|
|
46
|
-
initial = source;
|
|
47
|
-
}
|
|
48
|
-
else if (source && typeof source === 'object') {
|
|
49
|
-
const anySource = source;
|
|
50
|
-
if (typeof anySource.get === 'function') {
|
|
51
|
-
const v = anySource.get();
|
|
52
|
-
if (typeof v === 'number' || typeof v === 'string')
|
|
53
|
-
initial = v;
|
|
54
|
-
}
|
|
55
|
-
else if (typeof anySource.value === 'number' || typeof anySource.value === 'string') {
|
|
56
|
-
initial = anySource.value;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
const store = readable(initial, () => { });
|
|
60
|
-
store.set = () => { };
|
|
61
|
-
store.jump = () => { };
|
|
62
|
-
return store;
|
|
63
|
-
}
|
|
64
|
-
const { stiffness = 170, damping = 26, mass = 1, restDelta = 0.01, restSpeed = 0.01 } = options;
|
|
65
|
-
const state = {
|
|
66
|
-
current: parseUnit(typeof source === 'object' ? 0 : source),
|
|
67
|
-
target: parseUnit(typeof source === 'object' ? 0 : source)
|
|
68
|
-
};
|
|
69
|
-
const unit = state.current.unit || state.target.unit;
|
|
70
|
-
const store = writable(formatUnit(state.current.value, unit));
|
|
71
|
-
let raf = 0;
|
|
72
|
-
let lastTime = 0;
|
|
73
|
-
let velocity = 0;
|
|
74
|
-
const step = (t) => {
|
|
75
|
-
if (!lastTime)
|
|
76
|
-
lastTime = t;
|
|
77
|
-
// Clamp dt to a safe range to avoid instability across large time gaps
|
|
78
|
-
const dt = Math.min(0.1, Math.max(0.001, (t - lastTime) / 1000));
|
|
79
|
-
lastTime = t;
|
|
80
|
-
const displacement = state.current.value - state.target.value;
|
|
81
|
-
// Spring force based on Hooke's Law: F = -k x; damping force: -c v
|
|
82
|
-
const spring = -stiffness * displacement;
|
|
83
|
-
const damper = -damping * velocity;
|
|
84
|
-
const accel = (spring + damper) / mass;
|
|
85
|
-
velocity += accel * dt;
|
|
86
|
-
state.current.value += velocity * dt;
|
|
87
|
-
const isNoVelocity = Math.abs(velocity) <= restSpeed;
|
|
88
|
-
const isNoDisplacement = Math.abs(state.current.value - state.target.value) <= restDelta;
|
|
89
|
-
const done = isNoVelocity && isNoDisplacement;
|
|
90
|
-
if (done) {
|
|
91
|
-
state.current.value = state.target.value;
|
|
92
|
-
store.set(formatUnit(state.current.value, unit));
|
|
93
|
-
raf = 0;
|
|
94
|
-
lastTime = 0;
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
store.set(formatUnit(state.current.value, unit));
|
|
98
|
-
raf = requestAnimationFrame(step);
|
|
99
|
-
};
|
|
100
|
-
const start = () => {
|
|
101
|
-
if (raf)
|
|
102
|
-
return;
|
|
103
|
-
raf = requestAnimationFrame(step);
|
|
104
|
-
};
|
|
105
|
-
const api = {
|
|
106
|
-
set: (v) => {
|
|
107
|
-
state.target = parseUnit(v);
|
|
108
|
-
start();
|
|
109
|
-
},
|
|
110
|
-
jump: (v) => {
|
|
111
|
-
state.current = parseUnit(v);
|
|
112
|
-
state.target = parseUnit(v);
|
|
113
|
-
velocity = 0;
|
|
114
|
-
store.set(formatUnit(state.current.value, state.current.unit || state.target.unit));
|
|
115
|
-
}
|
|
116
|
-
};
|
|
117
|
-
// If following another store, subscribe and forward values to set()
|
|
118
|
-
if (typeof source === 'object' && 'subscribe' in source) {
|
|
119
|
-
let followSource = true;
|
|
120
|
-
const unsub = source.subscribe((v) => api.set(v));
|
|
121
|
-
const wrapped = readable(formatUnit(state.current.value, unit), (set) => {
|
|
122
|
-
const sub = store.subscribe(set);
|
|
123
|
-
return () => {
|
|
124
|
-
sub();
|
|
125
|
-
unsub();
|
|
126
|
-
followSource = false;
|
|
127
|
-
if (raf)
|
|
128
|
-
cancelAnimationFrame(raf);
|
|
129
|
-
};
|
|
130
|
-
});
|
|
131
|
-
wrapped.set = (v) => {
|
|
132
|
-
if (followSource)
|
|
133
|
-
unsub();
|
|
134
|
-
followSource = false;
|
|
135
|
-
api.set(v);
|
|
136
|
-
};
|
|
137
|
-
wrapped.jump = (v) => {
|
|
138
|
-
if (followSource)
|
|
139
|
-
unsub();
|
|
140
|
-
followSource = false;
|
|
141
|
-
api.jump(v);
|
|
142
|
-
};
|
|
143
|
-
return wrapped;
|
|
144
|
-
}
|
|
145
|
-
// Standard readable wrapping internal writable
|
|
146
|
-
const wrapped = readable(formatUnit(state.current.value, unit), (set) => {
|
|
147
|
-
const sub = store.subscribe(set);
|
|
148
|
-
return () => {
|
|
149
|
-
sub();
|
|
150
|
-
if (raf)
|
|
151
|
-
cancelAnimationFrame(raf);
|
|
152
|
-
};
|
|
153
|
-
});
|
|
154
|
-
wrapped.set = api.set;
|
|
155
|
-
wrapped.jump = api.jump;
|
|
156
|
-
return wrapped;
|
|
157
|
-
};
|