@humanspeak/svelte-motion 0.4.9 → 0.5.1
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/dist/index.d.ts +14 -7
- package/dist/index.js +7 -6
- package/dist/utils/attachable.js +14 -9
- package/dist/utils/augmentMotionValue.svelte.d.ts +156 -0
- package/dist/utils/augmentMotionValue.svelte.js +193 -0
- package/dist/utils/dom.d.ts +13 -0
- package/dist/utils/dom.js +19 -0
- package/dist/utils/followValue.svelte.d.ts +84 -0
- package/dist/utils/followValue.svelte.js +51 -0
- package/dist/utils/motionTemplate.svelte.d.ts +53 -0
- package/dist/utils/motionTemplate.svelte.js +78 -0
- package/dist/utils/motionValue.svelte.d.ts +61 -0
- package/dist/utils/motionValue.svelte.js +49 -0
- package/dist/utils/scroll.svelte.d.ts +91 -0
- package/dist/utils/scroll.svelte.js +259 -0
- package/dist/utils/spring.svelte.d.ts +26 -31
- package/dist/utils/spring.svelte.js +8 -116
- package/dist/utils/time.svelte.d.ts +47 -0
- package/dist/utils/time.svelte.js +128 -0
- package/dist/utils/transform.svelte.d.ts +170 -0
- package/dist/utils/transform.svelte.js +189 -0
- package/dist/utils/velocity.svelte.d.ts +61 -0
- package/dist/utils/velocity.svelte.js +132 -0
- package/package.json +1 -1
- package/dist/utils/motionTemplate.d.ts +0 -21
- package/dist/utils/motionTemplate.js +0 -33
- package/dist/utils/motionValue.d.ts +0 -6
- package/dist/utils/motionValue.js +0 -13
- package/dist/utils/scroll.d.ts +0 -63
- package/dist/utils/scroll.js +0 -79
- package/dist/utils/time.d.ts +0 -14
- package/dist/utils/time.js +0 -68
- package/dist/utils/transform.d.ts +0 -74
- package/dist/utils/transform.js +0 -211
- package/dist/utils/velocity.d.ts +0 -15
- package/dist/utils/velocity.js +0 -62
package/dist/index.d.ts
CHANGED
|
@@ -10,31 +10,38 @@ 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 type { AugmentedMotionValue } from './utils/augmentMotionValue.svelte';
|
|
13
14
|
export { useCycle } from './utils/cycle.svelte';
|
|
14
15
|
export type { Cycle, CycleState } from './utils/cycle.svelte';
|
|
15
16
|
export { createDragControls } from './utils/dragControls';
|
|
17
|
+
export { useFollowValue } from './utils/followValue.svelte';
|
|
18
|
+
export type { FollowMotionValue, UseFollowValueOptions } from './utils/followValue.svelte';
|
|
16
19
|
export { useInView } from './utils/inView.svelte';
|
|
17
20
|
export type { InViewState, UseInViewOptions } from './utils/inView.svelte';
|
|
18
|
-
export { useMotionTemplate } from './utils/motionTemplate';
|
|
19
|
-
export {
|
|
20
|
-
export
|
|
21
|
+
export { useMotionTemplate } from './utils/motionTemplate.svelte';
|
|
22
|
+
export type { MotionTemplateInput } from './utils/motionTemplate.svelte';
|
|
23
|
+
export { useMotionValue } from './utils/motionValue.svelte';
|
|
24
|
+
export type { MotionValue, RawMotionValue } from './utils/motionValue.svelte';
|
|
21
25
|
export { useMotionValueEvent } from './utils/motionValueEvent';
|
|
22
26
|
export { useReducedMotion } from './utils/reducedMotion.svelte';
|
|
23
27
|
export type { ReducedMotionState } from './utils/reducedMotion.svelte';
|
|
24
28
|
export { useReducedMotionConfig } from './utils/reducedMotionConfig.svelte';
|
|
25
|
-
export { useScroll } from './utils/scroll';
|
|
29
|
+
export { useScroll } from './utils/scroll.svelte';
|
|
30
|
+
export type { UseScrollOptions, UseScrollReturn } from './utils/scroll.svelte';
|
|
26
31
|
export { useSpring } from './utils/spring.svelte';
|
|
27
32
|
export type { SpringMotionValue, UseSpringOptions } from './utils/spring.svelte';
|
|
28
33
|
export { useIsPresent, usePresence } from './utils/usePresence';
|
|
29
34
|
export type { UsePresenceState } from './utils/usePresence';
|
|
30
|
-
export { useVelocity } from './utils/velocity';
|
|
35
|
+
export { useVelocity } from './utils/velocity.svelte';
|
|
36
|
+
export type { VelocitySource } from './utils/velocity.svelte';
|
|
31
37
|
/**
|
|
32
38
|
* @deprecated Use `styleString` instead for reactive styles with automatic unit handling.
|
|
33
39
|
*/
|
|
34
40
|
export { stringifyStyleObject } from './utils/styleObject';
|
|
35
41
|
export { styleString } from './utils/styleObject.svelte';
|
|
36
|
-
export { useTime } from './utils/time';
|
|
37
|
-
export { useTransform } from './utils/transform';
|
|
42
|
+
export { useTime } from './utils/time.svelte';
|
|
43
|
+
export { useTransform } from './utils/transform.svelte';
|
|
44
|
+
export type { MultiTransformer, SingleTransformer, TransformOptions, TransformOutputMap, TransformSource } from './utils/transform.svelte';
|
|
38
45
|
export { AnimatePresence, LayoutGroup, MotionConfig, PresenceChild };
|
|
39
46
|
export { default as MotionA } from './html/A.svelte';
|
|
40
47
|
export { default as MotionAbbr } from './html/Abbr.svelte';
|
package/dist/index.js
CHANGED
|
@@ -13,23 +13,24 @@ export { useAnimate } from './utils/animate.svelte';
|
|
|
13
13
|
export { useAnimationFrame } from './utils/animationFrame';
|
|
14
14
|
export { useCycle } from './utils/cycle.svelte';
|
|
15
15
|
export { createDragControls } from './utils/dragControls';
|
|
16
|
+
export { useFollowValue } from './utils/followValue.svelte';
|
|
16
17
|
export { useInView } from './utils/inView.svelte';
|
|
17
|
-
export { useMotionTemplate } from './utils/motionTemplate';
|
|
18
|
-
export { useMotionValue } from './utils/motionValue';
|
|
18
|
+
export { useMotionTemplate } from './utils/motionTemplate.svelte';
|
|
19
|
+
export { useMotionValue } from './utils/motionValue.svelte';
|
|
19
20
|
export { useMotionValueEvent } from './utils/motionValueEvent';
|
|
20
21
|
export { useReducedMotion } from './utils/reducedMotion.svelte';
|
|
21
22
|
export { useReducedMotionConfig } from './utils/reducedMotionConfig.svelte';
|
|
22
|
-
export { useScroll } from './utils/scroll';
|
|
23
|
+
export { useScroll } from './utils/scroll.svelte';
|
|
23
24
|
export { useSpring } from './utils/spring.svelte';
|
|
24
25
|
export { useIsPresent, usePresence } from './utils/usePresence';
|
|
25
|
-
export { useVelocity } from './utils/velocity';
|
|
26
|
+
export { useVelocity } from './utils/velocity.svelte';
|
|
26
27
|
/**
|
|
27
28
|
* @deprecated Use `styleString` instead for reactive styles with automatic unit handling.
|
|
28
29
|
*/
|
|
29
30
|
export { stringifyStyleObject } from './utils/styleObject';
|
|
30
31
|
export { styleString } from './utils/styleObject.svelte';
|
|
31
|
-
export { useTime } from './utils/time';
|
|
32
|
-
export { useTransform } from './utils/transform';
|
|
32
|
+
export { useTime } from './utils/time.svelte';
|
|
33
|
+
export { useTransform } from './utils/transform.svelte';
|
|
33
34
|
export { AnimatePresence, LayoutGroup, MotionConfig, PresenceChild };
|
|
34
35
|
// Named component exports — tree-shakeable alternative to the `motion` object
|
|
35
36
|
export { default as MotionA } from './html/A.svelte';
|
package/dist/utils/attachable.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { cancelMicrotask, microtask } from 'motion-dom';
|
|
1
2
|
import { resolveElement } from './dom.js';
|
|
2
3
|
/**
|
|
3
4
|
* Builds a subscriber-refcounted DOM-attachment primitive. Both `useScroll`
|
|
@@ -27,17 +28,21 @@ import { resolveElement } from './dom.js';
|
|
|
27
28
|
*/
|
|
28
29
|
export const createAttachable = (config) => {
|
|
29
30
|
let cleanup;
|
|
30
|
-
let
|
|
31
|
+
let pollScheduled = false;
|
|
31
32
|
let subscriberCount = 0;
|
|
32
33
|
// When stop() runs synchronously inside onAttach, cleanup hasn't been
|
|
33
34
|
// assigned yet. Defer the teardown so the just-returned disposer still
|
|
34
35
|
// gets invoked.
|
|
35
36
|
let attaching = false;
|
|
36
37
|
let stopRequestedDuringAttach = false;
|
|
38
|
+
const pollTick = () => {
|
|
39
|
+
pollScheduled = false;
|
|
40
|
+
tryAttach();
|
|
41
|
+
};
|
|
37
42
|
const cancelPoll = () => {
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
if (pollScheduled) {
|
|
44
|
+
cancelMicrotask(pollTick);
|
|
45
|
+
pollScheduled = false;
|
|
41
46
|
}
|
|
42
47
|
};
|
|
43
48
|
const stop = () => {
|
|
@@ -65,11 +70,11 @@ export const createAttachable = (config) => {
|
|
|
65
70
|
els[key] = el;
|
|
66
71
|
}
|
|
67
72
|
if (needsPoll) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
// Microtask schedule (matches useScroll's pattern) — one frame
|
|
74
|
+
// faster than rAF for refs that hydrate the same tick.
|
|
75
|
+
if (!pollScheduled) {
|
|
76
|
+
pollScheduled = true;
|
|
77
|
+
microtask.read(pollTick);
|
|
73
78
|
}
|
|
74
79
|
return;
|
|
75
80
|
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { type MotionValue } from 'motion-dom';
|
|
2
|
+
import { type Readable } from 'svelte/store';
|
|
3
|
+
/**
|
|
4
|
+
* A motion-dom `MotionValue<T>` augmented with the two affordances every
|
|
5
|
+
* Tier-2 hook in this library exposes for Svelte 5 consumers:
|
|
6
|
+
*
|
|
7
|
+
* - `current` — a `$state`-backed reactive getter. Reads inside templates,
|
|
8
|
+
* `$derived`, and `$effect` track changes automatically without going
|
|
9
|
+
* through `.subscribe()`.
|
|
10
|
+
* - `subscribe(run)` — Svelte readable store contract. Calls `run(value)`
|
|
11
|
+
* once synchronously on subscribe, then on every change. Lets the same
|
|
12
|
+
* value drive `$value` template syntax, `svelte/store`'s `get()`, and
|
|
13
|
+
* anything else that expects a readable.
|
|
14
|
+
*
|
|
15
|
+
* Every Wave 3 hook (`useMotionValue`, `useTransform`, `useScroll`,
|
|
16
|
+
* `useTime`, `useVelocity`, `useMotionTemplate`) and `useSpring` returns a
|
|
17
|
+
* value of this shape. The point is to keep one shared surface across all
|
|
18
|
+
* motion-value-producing hooks instead of repeating the wiring in each.
|
|
19
|
+
*/
|
|
20
|
+
export type AugmentedMotionValue<T> = Omit<MotionValue<T>, 'current'> & {
|
|
21
|
+
/** Reactive read in Svelte 5 templates / `$derived` / `$effect`. */
|
|
22
|
+
readonly current: T;
|
|
23
|
+
/** Svelte readable store compatibility. */
|
|
24
|
+
subscribe: (run: (value: T) => void) => () => void;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Detects a Svelte readable store, excluding motion-dom `MotionValue`
|
|
28
|
+
* instances (which also expose `subscribe`-shaped APIs in some versions).
|
|
29
|
+
* Used by hook factories that accept either a `MotionValue` or a readable
|
|
30
|
+
* store as a source.
|
|
31
|
+
*
|
|
32
|
+
* @template T The value type the readable emits.
|
|
33
|
+
* @param value Any value to test.
|
|
34
|
+
* @returns Whether the value is a Svelte readable (and not a `MotionValue`).
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* import { writable } from 'svelte/store'
|
|
39
|
+
* import { motionValue } from 'motion-dom'
|
|
40
|
+
*
|
|
41
|
+
* isSvelteReadable(writable(0)) // true
|
|
42
|
+
* isSvelteReadable(motionValue(0)) // false (a MotionValue, not a readable)
|
|
43
|
+
* isSvelteReadable({ subscribe: 'nope' }) // false (subscribe isn't callable)
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export declare const isSvelteReadable: <T = unknown>(value: unknown) => value is Readable<T>;
|
|
47
|
+
/**
|
|
48
|
+
* Synchronously samples a source: returns `T` directly, calls `.get()` on
|
|
49
|
+
* a `MotionValue`, or `svelte/store`'s `get()` on a readable. Used by hook
|
|
50
|
+
* factories to seed an initial value before any subscription is established.
|
|
51
|
+
*
|
|
52
|
+
* @template T The value type.
|
|
53
|
+
* @param source A plain value, a `MotionValue`, or a Svelte readable.
|
|
54
|
+
* @returns The current value of the source.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```ts
|
|
58
|
+
* sampleSource(42) // 42
|
|
59
|
+
* sampleSource(motionValue(7)) // 7
|
|
60
|
+
* sampleSource(writable(11)) // 11
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export declare const sampleSource: <T>(source: T | MotionValue<T> | Readable<T>) => T;
|
|
64
|
+
/**
|
|
65
|
+
* Layer Svelte 5 affordances onto a motion-dom `MotionValue`:
|
|
66
|
+
*
|
|
67
|
+
* 1. A `$state`-tracked `.current` accessor. motion-dom writes to its own
|
|
68
|
+
* `current` field on every frame via an internal setter; we redirect that
|
|
69
|
+
* setter through `$state` so templates and `$derived` / `$effect` re-run
|
|
70
|
+
* automatically. Same-value writes are skipped (motion-dom can call the
|
|
71
|
+
* setter at rest, and `$state` would itself dedupe, but the explicit
|
|
72
|
+
* check avoids the extra accessor work).
|
|
73
|
+
*
|
|
74
|
+
* 2. A `.subscribe(run)` shim implementing the Svelte readable store
|
|
75
|
+
* contract: synchronous initial emit, then re-emit on every change.
|
|
76
|
+
* Forwarded to motion-dom's `.on('change', …)` event bus.
|
|
77
|
+
*
|
|
78
|
+
* 3. `.destroy()` is wrapped so a caller-supplied `dispose` runs once before
|
|
79
|
+
* motion-dom's own teardown (and only once, guarded against re-entrant
|
|
80
|
+
* or duplicate destroy calls).
|
|
81
|
+
*
|
|
82
|
+
* The returned reference is the same `MotionValue` passed in, only retyped —
|
|
83
|
+
* so identity checks (`isMotionValue`, `===`) still work and motion-dom's
|
|
84
|
+
* own machinery (animation, follow, composition) is untouched.
|
|
85
|
+
*
|
|
86
|
+
* **Call once per MotionValue.** This function mutates the value (rewrites
|
|
87
|
+
* `current`, `subscribe`, `destroy`). Calling it twice would re-define the
|
|
88
|
+
* accessors and the first call's `dispose` would be discarded.
|
|
89
|
+
*
|
|
90
|
+
* @template T The value type — typically `number` or `string`.
|
|
91
|
+
* @param value The motion-dom `MotionValue` to augment.
|
|
92
|
+
* @param dispose Optional cleanup that runs once when `.destroy()` is first called (before motion-dom's internal teardown). Defaults to a no-op.
|
|
93
|
+
* @returns The same `MotionValue` typed as {@link AugmentedMotionValue}.
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* ```svelte
|
|
97
|
+
* <script lang="ts">
|
|
98
|
+
* import { motionValue } from 'motion-dom'
|
|
99
|
+
* import { augmentMotionValue } from './augmentMotionValue.svelte.js'
|
|
100
|
+
*
|
|
101
|
+
* const mv = motionValue(0)
|
|
102
|
+
* const aug = augmentMotionValue(mv, () => console.log('disposed'))
|
|
103
|
+
*
|
|
104
|
+
* $effect(() => () => aug.destroy())
|
|
105
|
+
* </script>
|
|
106
|
+
*
|
|
107
|
+
* <div style="transform: translateX({aug.current}px)">{aug.current}</div>
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
export declare const augmentMotionValue: <T>(value: MotionValue<T>, dispose?: VoidFunction) => AugmentedMotionValue<T>;
|
|
111
|
+
/**
|
|
112
|
+
* Bridges a Svelte `Readable<T>` into a motion-dom `MotionValue<T>` that
|
|
113
|
+
* mirrors the readable's emissions, so motion-dom primitives (`mapValue`,
|
|
114
|
+
* `transformValue`, `attachFollow`, `getVelocity`, etc.) that only accept
|
|
115
|
+
* `MotionValue` can track readable-shaped sources.
|
|
116
|
+
*
|
|
117
|
+
* The bridge:
|
|
118
|
+
* 1. Seeds via `get(source)` so the initial value is correct synchronously.
|
|
119
|
+
* 2. Subscribes to the readable, skipping the *synchronous initial emit*
|
|
120
|
+
* (Svelte readables always fire one on subscribe, but the seed already
|
|
121
|
+
* has it — without the skip the bridge would double-write on attach).
|
|
122
|
+
* 3. Optionally coerces each emit through `coerce` — useful for unit-string
|
|
123
|
+
* sources (e.g. `"100px"` → `100`).
|
|
124
|
+
*
|
|
125
|
+
* Returns the bridge value and a `dispose` that tears down the subscription
|
|
126
|
+
* and destroys the bridge MV. Callers register `dispose` with their lifecycle
|
|
127
|
+
* ($effect cleanup or the augmented `destroy`'s `dispose` slot).
|
|
128
|
+
*
|
|
129
|
+
* @template TIn The readable's emit type (often `number | string`).
|
|
130
|
+
* @template TOut The bridge MotionValue's value type (often `number`).
|
|
131
|
+
* @param source A Svelte readable store.
|
|
132
|
+
* @param coerce Optional transform applied to each emit (and the initial seed). Identity by default.
|
|
133
|
+
* @returns A `MotionValue<TOut>` mirroring the readable + a dispose function.
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* ```ts
|
|
137
|
+
* import { writable } from 'svelte/store'
|
|
138
|
+
*
|
|
139
|
+
* // Identity bridge — readable<number> → motionValue<number>
|
|
140
|
+
* const w = writable(0)
|
|
141
|
+
* const { value: mv, dispose } = bridgeReadableToMotionValue(w)
|
|
142
|
+
* w.set(50); mv.get() === 50
|
|
143
|
+
*
|
|
144
|
+
* // Coerce bridge — readable<string> → motionValue<number>
|
|
145
|
+
* const w2 = writable('100px')
|
|
146
|
+
* const bridge = bridgeReadableToMotionValue<string, number>(w2, parseFloat)
|
|
147
|
+
* bridge.value.get() === 100
|
|
148
|
+
*
|
|
149
|
+
* // Always pair with dispose() in your $effect cleanup.
|
|
150
|
+
* $effect(() => () => dispose())
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
export declare const bridgeReadableToMotionValue: <TIn, TOut = TIn>(source: Readable<TIn>, coerce?: (v: TIn) => TOut) => {
|
|
154
|
+
value: MotionValue<TOut>;
|
|
155
|
+
dispose: VoidFunction;
|
|
156
|
+
};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { isMotionValue, motionValue } from 'motion-dom';
|
|
2
|
+
import { get } from 'svelte/store';
|
|
3
|
+
/**
|
|
4
|
+
* Detects a Svelte readable store, excluding motion-dom `MotionValue`
|
|
5
|
+
* instances (which also expose `subscribe`-shaped APIs in some versions).
|
|
6
|
+
* Used by hook factories that accept either a `MotionValue` or a readable
|
|
7
|
+
* store as a source.
|
|
8
|
+
*
|
|
9
|
+
* @template T The value type the readable emits.
|
|
10
|
+
* @param value Any value to test.
|
|
11
|
+
* @returns Whether the value is a Svelte readable (and not a `MotionValue`).
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { writable } from 'svelte/store'
|
|
16
|
+
* import { motionValue } from 'motion-dom'
|
|
17
|
+
*
|
|
18
|
+
* isSvelteReadable(writable(0)) // true
|
|
19
|
+
* isSvelteReadable(motionValue(0)) // false (a MotionValue, not a readable)
|
|
20
|
+
* isSvelteReadable({ subscribe: 'nope' }) // false (subscribe isn't callable)
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export const isSvelteReadable = (value) => {
|
|
24
|
+
return (!!value &&
|
|
25
|
+
typeof value === 'object' &&
|
|
26
|
+
typeof value.subscribe === 'function' &&
|
|
27
|
+
!isMotionValue(value));
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Synchronously samples a source: returns `T` directly, calls `.get()` on
|
|
31
|
+
* a `MotionValue`, or `svelte/store`'s `get()` on a readable. Used by hook
|
|
32
|
+
* factories to seed an initial value before any subscription is established.
|
|
33
|
+
*
|
|
34
|
+
* @template T The value type.
|
|
35
|
+
* @param source A plain value, a `MotionValue`, or a Svelte readable.
|
|
36
|
+
* @returns The current value of the source.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```ts
|
|
40
|
+
* sampleSource(42) // 42
|
|
41
|
+
* sampleSource(motionValue(7)) // 7
|
|
42
|
+
* sampleSource(writable(11)) // 11
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export const sampleSource = (source) => {
|
|
46
|
+
if (isMotionValue(source))
|
|
47
|
+
return source.get();
|
|
48
|
+
if (isSvelteReadable(source))
|
|
49
|
+
return get(source);
|
|
50
|
+
return source;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Layer Svelte 5 affordances onto a motion-dom `MotionValue`:
|
|
54
|
+
*
|
|
55
|
+
* 1. A `$state`-tracked `.current` accessor. motion-dom writes to its own
|
|
56
|
+
* `current` field on every frame via an internal setter; we redirect that
|
|
57
|
+
* setter through `$state` so templates and `$derived` / `$effect` re-run
|
|
58
|
+
* automatically. Same-value writes are skipped (motion-dom can call the
|
|
59
|
+
* setter at rest, and `$state` would itself dedupe, but the explicit
|
|
60
|
+
* check avoids the extra accessor work).
|
|
61
|
+
*
|
|
62
|
+
* 2. A `.subscribe(run)` shim implementing the Svelte readable store
|
|
63
|
+
* contract: synchronous initial emit, then re-emit on every change.
|
|
64
|
+
* Forwarded to motion-dom's `.on('change', …)` event bus.
|
|
65
|
+
*
|
|
66
|
+
* 3. `.destroy()` is wrapped so a caller-supplied `dispose` runs once before
|
|
67
|
+
* motion-dom's own teardown (and only once, guarded against re-entrant
|
|
68
|
+
* or duplicate destroy calls).
|
|
69
|
+
*
|
|
70
|
+
* The returned reference is the same `MotionValue` passed in, only retyped —
|
|
71
|
+
* so identity checks (`isMotionValue`, `===`) still work and motion-dom's
|
|
72
|
+
* own machinery (animation, follow, composition) is untouched.
|
|
73
|
+
*
|
|
74
|
+
* **Call once per MotionValue.** This function mutates the value (rewrites
|
|
75
|
+
* `current`, `subscribe`, `destroy`). Calling it twice would re-define the
|
|
76
|
+
* accessors and the first call's `dispose` would be discarded.
|
|
77
|
+
*
|
|
78
|
+
* @template T The value type — typically `number` or `string`.
|
|
79
|
+
* @param value The motion-dom `MotionValue` to augment.
|
|
80
|
+
* @param dispose Optional cleanup that runs once when `.destroy()` is first called (before motion-dom's internal teardown). Defaults to a no-op.
|
|
81
|
+
* @returns The same `MotionValue` typed as {@link AugmentedMotionValue}.
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```svelte
|
|
85
|
+
* <script lang="ts">
|
|
86
|
+
* import { motionValue } from 'motion-dom'
|
|
87
|
+
* import { augmentMotionValue } from './augmentMotionValue.svelte.js'
|
|
88
|
+
*
|
|
89
|
+
* const mv = motionValue(0)
|
|
90
|
+
* const aug = augmentMotionValue(mv, () => console.log('disposed'))
|
|
91
|
+
*
|
|
92
|
+
* $effect(() => () => aug.destroy())
|
|
93
|
+
* </script>
|
|
94
|
+
*
|
|
95
|
+
* <div style="transform: translateX({aug.current}px)">{aug.current}</div>
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
export const augmentMotionValue = (value, dispose = () => undefined) => {
|
|
99
|
+
// motion-dom's `.get()` returns `NonNullable<V>`, which would otherwise
|
|
100
|
+
// narrow `$state` to that and reject nullable-T setter writes. Cast to T
|
|
101
|
+
// so the state slot matches the public augmented signature; motion-dom's
|
|
102
|
+
// own contract guarantees it never sets a null/undefined frame value.
|
|
103
|
+
let current = $state(value.get());
|
|
104
|
+
Object.defineProperty(value, 'current', {
|
|
105
|
+
get: () => current,
|
|
106
|
+
set: (v) => {
|
|
107
|
+
if (v !== current)
|
|
108
|
+
current = v;
|
|
109
|
+
},
|
|
110
|
+
enumerable: true,
|
|
111
|
+
configurable: true
|
|
112
|
+
});
|
|
113
|
+
const originalDestroy = value.destroy.bind(value);
|
|
114
|
+
let destroyed = false;
|
|
115
|
+
value.destroy = () => {
|
|
116
|
+
if (destroyed)
|
|
117
|
+
return;
|
|
118
|
+
destroyed = true;
|
|
119
|
+
dispose();
|
|
120
|
+
originalDestroy();
|
|
121
|
+
};
|
|
122
|
+
const subscribe = (run) => {
|
|
123
|
+
run(value.get());
|
|
124
|
+
return value.on('change', run);
|
|
125
|
+
};
|
|
126
|
+
Object.defineProperty(value, 'subscribe', {
|
|
127
|
+
value: subscribe,
|
|
128
|
+
writable: false,
|
|
129
|
+
enumerable: false,
|
|
130
|
+
configurable: true
|
|
131
|
+
});
|
|
132
|
+
return value;
|
|
133
|
+
};
|
|
134
|
+
/**
|
|
135
|
+
* Bridges a Svelte `Readable<T>` into a motion-dom `MotionValue<T>` that
|
|
136
|
+
* mirrors the readable's emissions, so motion-dom primitives (`mapValue`,
|
|
137
|
+
* `transformValue`, `attachFollow`, `getVelocity`, etc.) that only accept
|
|
138
|
+
* `MotionValue` can track readable-shaped sources.
|
|
139
|
+
*
|
|
140
|
+
* The bridge:
|
|
141
|
+
* 1. Seeds via `get(source)` so the initial value is correct synchronously.
|
|
142
|
+
* 2. Subscribes to the readable, skipping the *synchronous initial emit*
|
|
143
|
+
* (Svelte readables always fire one on subscribe, but the seed already
|
|
144
|
+
* has it — without the skip the bridge would double-write on attach).
|
|
145
|
+
* 3. Optionally coerces each emit through `coerce` — useful for unit-string
|
|
146
|
+
* sources (e.g. `"100px"` → `100`).
|
|
147
|
+
*
|
|
148
|
+
* Returns the bridge value and a `dispose` that tears down the subscription
|
|
149
|
+
* and destroys the bridge MV. Callers register `dispose` with their lifecycle
|
|
150
|
+
* ($effect cleanup or the augmented `destroy`'s `dispose` slot).
|
|
151
|
+
*
|
|
152
|
+
* @template TIn The readable's emit type (often `number | string`).
|
|
153
|
+
* @template TOut The bridge MotionValue's value type (often `number`).
|
|
154
|
+
* @param source A Svelte readable store.
|
|
155
|
+
* @param coerce Optional transform applied to each emit (and the initial seed). Identity by default.
|
|
156
|
+
* @returns A `MotionValue<TOut>` mirroring the readable + a dispose function.
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* ```ts
|
|
160
|
+
* import { writable } from 'svelte/store'
|
|
161
|
+
*
|
|
162
|
+
* // Identity bridge — readable<number> → motionValue<number>
|
|
163
|
+
* const w = writable(0)
|
|
164
|
+
* const { value: mv, dispose } = bridgeReadableToMotionValue(w)
|
|
165
|
+
* w.set(50); mv.get() === 50
|
|
166
|
+
*
|
|
167
|
+
* // Coerce bridge — readable<string> → motionValue<number>
|
|
168
|
+
* const w2 = writable('100px')
|
|
169
|
+
* const bridge = bridgeReadableToMotionValue<string, number>(w2, parseFloat)
|
|
170
|
+
* bridge.value.get() === 100
|
|
171
|
+
*
|
|
172
|
+
* // Always pair with dispose() in your $effect cleanup.
|
|
173
|
+
* $effect(() => () => dispose())
|
|
174
|
+
* ```
|
|
175
|
+
*/
|
|
176
|
+
export const bridgeReadableToMotionValue = (source, coerce = (v) => v) => {
|
|
177
|
+
const bridge = motionValue(coerce(get(source)));
|
|
178
|
+
let seenInitial = false;
|
|
179
|
+
const unsub = source.subscribe((v) => {
|
|
180
|
+
if (!seenInitial) {
|
|
181
|
+
seenInitial = true;
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
bridge.set(coerce(v));
|
|
185
|
+
});
|
|
186
|
+
return {
|
|
187
|
+
value: bridge,
|
|
188
|
+
dispose: () => {
|
|
189
|
+
unsub();
|
|
190
|
+
bridge.destroy();
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
};
|
package/dist/utils/dom.d.ts
CHANGED
|
@@ -41,3 +41,16 @@ export type ElementOrGetter = HTMLElement | (() => HTMLElement | null | undefine
|
|
|
41
41
|
* ```
|
|
42
42
|
*/
|
|
43
43
|
export declare const resolveElement: (ref?: ElementOrGetter) => HTMLElement | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* Tests whether an `ElementOrGetter` is currently unresolved — defined as a
|
|
46
|
+
* getter that returns falsy. Direct element refs are never "pending" (they
|
|
47
|
+
* were already resolved at call time) and absent refs are not "pending"
|
|
48
|
+
* either (they're just absent).
|
|
49
|
+
*
|
|
50
|
+
* Lets microtask-defer / rAF-defer loops wait for refs to hydrate after
|
|
51
|
+
* `bind:this` settles on mount.
|
|
52
|
+
*
|
|
53
|
+
* @param ref Element or getter to check.
|
|
54
|
+
* @returns `true` only when `ref` is a getter and the getter currently returns falsy.
|
|
55
|
+
*/
|
|
56
|
+
export declare const isRefPending: (ref?: ElementOrGetter) => boolean;
|
package/dist/utils/dom.js
CHANGED
|
@@ -39,3 +39,22 @@ export const resolveElement = (ref) => {
|
|
|
39
39
|
const value = typeof ref === 'function' ? ref() : ref;
|
|
40
40
|
return value ?? undefined;
|
|
41
41
|
};
|
|
42
|
+
/**
|
|
43
|
+
* Tests whether an `ElementOrGetter` is currently unresolved — defined as a
|
|
44
|
+
* getter that returns falsy. Direct element refs are never "pending" (they
|
|
45
|
+
* were already resolved at call time) and absent refs are not "pending"
|
|
46
|
+
* either (they're just absent).
|
|
47
|
+
*
|
|
48
|
+
* Lets microtask-defer / rAF-defer loops wait for refs to hydrate after
|
|
49
|
+
* `bind:this` settles on mount.
|
|
50
|
+
*
|
|
51
|
+
* @param ref Element or getter to check.
|
|
52
|
+
* @returns `true` only when `ref` is a getter and the getter currently returns falsy.
|
|
53
|
+
*/
|
|
54
|
+
export const isRefPending = (ref) => {
|
|
55
|
+
if (!ref)
|
|
56
|
+
return false;
|
|
57
|
+
if (typeof ref !== 'function')
|
|
58
|
+
return false;
|
|
59
|
+
return !ref();
|
|
60
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { type FollowValueOptions, type MotionValue } from 'motion-dom';
|
|
2
|
+
import { type Readable } from 'svelte/store';
|
|
3
|
+
import { type AugmentedMotionValue } from './augmentMotionValue.svelte.js';
|
|
4
|
+
/**
|
|
5
|
+
* Options accepted by {@link useFollowValue}. Mirrors framer-motion's
|
|
6
|
+
* `FollowValueOptions` 1:1 — any `ValueAnimationTransition` shape (spring /
|
|
7
|
+
* tween / inertia / keyframes) plus `skipInitialAnimation` for
|
|
8
|
+
* scroll-restoration scenarios.
|
|
9
|
+
*
|
|
10
|
+
* The shape is re-exported from motion-dom so consumers don't have to
|
|
11
|
+
* cross-import.
|
|
12
|
+
*
|
|
13
|
+
* @see https://motion.dev/docs/react-use-follow-value
|
|
14
|
+
*/
|
|
15
|
+
export type UseFollowValueOptions = FollowValueOptions;
|
|
16
|
+
/**
|
|
17
|
+
* The augmented `MotionValue` returned by {@link useFollowValue} (and, since
|
|
18
|
+
* `useSpring` now delegates here, by `useSpring` too).
|
|
19
|
+
*/
|
|
20
|
+
export type FollowMotionValue<T extends number | string> = AugmentedMotionValue<T>;
|
|
21
|
+
/**
|
|
22
|
+
* Creates an animated `MotionValue` that, when `.set(v)` is called, animates
|
|
23
|
+
* toward `v` using **any** transition type — spring, tween, inertia, or
|
|
24
|
+
* keyframes. Pass another `MotionValue` (or Svelte readable) as the source
|
|
25
|
+
* and the result follows it: every source emit kicks off a new animation
|
|
26
|
+
* toward the latest value.
|
|
27
|
+
*
|
|
28
|
+
* Mirrors React framer-motion's `useFollowValue` 1:1. `useSpring` in this
|
|
29
|
+
* library is now a thin wrapper that delegates here with the default
|
|
30
|
+
* `{ type: 'spring' }`.
|
|
31
|
+
*
|
|
32
|
+
* Returned object is a real motion-dom `MotionValue` (composes with
|
|
33
|
+
* `useTransform`, `useVelocity`, `animate()`, etc.) augmented with a
|
|
34
|
+
* `$state`-backed `.current` getter and a Svelte readable `.subscribe`
|
|
35
|
+
* shim.
|
|
36
|
+
*
|
|
37
|
+
* Lifecycle: must be called during component initialization. Cleanup is
|
|
38
|
+
* registered via `$effect`; the follow animation stops, the source bridge
|
|
39
|
+
* (if any) tears down, and `value.destroy()` runs when the surrounding
|
|
40
|
+
* `$effect` scope unmounts.
|
|
41
|
+
*
|
|
42
|
+
* SSR-safe: returns a static augmented `MotionValue` with no animation on
|
|
43
|
+
* the server. `.set` and `.jump` become no-ops to avoid drifting away from
|
|
44
|
+
* the server-rendered snapshot.
|
|
45
|
+
*
|
|
46
|
+
* @template T The value type — `number` or `string` (unit strings preserved).
|
|
47
|
+
* @param source Initial value, a `MotionValue` to follow, or a Svelte readable.
|
|
48
|
+
* @param options Transition + follow configuration (`type: 'spring' | 'tween' | 'inertia' | 'keyframes'`, plus the corresponding per-type options).
|
|
49
|
+
* @returns A `MotionValue<T>` with `.current` and `.subscribe`.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```svelte
|
|
53
|
+
* <script lang="ts">
|
|
54
|
+
* import { useFollowValue, useMotionValue } from '@humanspeak/svelte-motion'
|
|
55
|
+
*
|
|
56
|
+
* const target = useMotionValue(0)
|
|
57
|
+
*
|
|
58
|
+
* // Spring follow (default — same as `useSpring(target)`).
|
|
59
|
+
* const spring = useFollowValue(target, { stiffness: 300, damping: 30 })
|
|
60
|
+
*
|
|
61
|
+
* // Tween follow — eased linear interpolation.
|
|
62
|
+
* const eased = useFollowValue(target, {
|
|
63
|
+
* type: 'tween',
|
|
64
|
+
* duration: 0.4,
|
|
65
|
+
* ease: 'easeInOut'
|
|
66
|
+
* })
|
|
67
|
+
*
|
|
68
|
+
* // Inertia — decays from initial velocity.
|
|
69
|
+
* const drifting = useFollowValue(0, { type: 'inertia', velocity: 800, power: 0.6 })
|
|
70
|
+
* </script>
|
|
71
|
+
*
|
|
72
|
+
* <button onclick={() => target.set(target.get() === 0 ? 200 : 0)}>Toggle</button>
|
|
73
|
+
* <div style="transform: translateX({spring.current}px)">spring</div>
|
|
74
|
+
* <div style="transform: translateX({eased.current}px)">tween</div>
|
|
75
|
+
* <div style="transform: translateX({drifting.current}px)">inertia</div>
|
|
76
|
+
* ```
|
|
77
|
+
*
|
|
78
|
+
* @see https://motion.dev/docs/react-use-follow-value
|
|
79
|
+
*/
|
|
80
|
+
export declare function useFollowValue(source: number, options?: UseFollowValueOptions): FollowMotionValue<number>;
|
|
81
|
+
export declare function useFollowValue(source: string, options?: UseFollowValueOptions): FollowMotionValue<string>;
|
|
82
|
+
export declare function useFollowValue(source: MotionValue<number>, options?: UseFollowValueOptions): FollowMotionValue<number>;
|
|
83
|
+
export declare function useFollowValue(source: MotionValue<string>, options?: UseFollowValueOptions): FollowMotionValue<string>;
|
|
84
|
+
export declare function useFollowValue<T extends number | string>(source: Readable<T>, options?: UseFollowValueOptions): FollowMotionValue<T>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { attachFollow, isMotionValue, motionValue } from 'motion-dom';
|
|
2
|
+
import {} from 'svelte/store';
|
|
3
|
+
import { augmentMotionValue, isSvelteReadable, sampleSource } from './augmentMotionValue.svelte.js';
|
|
4
|
+
export function useFollowValue(source, options = {}) {
|
|
5
|
+
// SSR: return a static MotionValue with no animation. Reads return the
|
|
6
|
+
// best-effort initial value; .set / .jump become no-ops to avoid drifting
|
|
7
|
+
// away from the server-rendered snapshot.
|
|
8
|
+
if (typeof window === 'undefined') {
|
|
9
|
+
const initial = sampleSource(source);
|
|
10
|
+
const ssrValue = motionValue(initial);
|
|
11
|
+
ssrValue.set = () => undefined;
|
|
12
|
+
ssrValue.jump = () => undefined;
|
|
13
|
+
return augmentMotionValue(ssrValue);
|
|
14
|
+
}
|
|
15
|
+
// Resolve initial + follow source. Svelte readables get bridged into a
|
|
16
|
+
// motion-dom MotionValue so `attachFollow` can subscribe to their emits.
|
|
17
|
+
let followSource;
|
|
18
|
+
let cleanupReadableBridge;
|
|
19
|
+
let svelteBridge;
|
|
20
|
+
if (isMotionValue(source)) {
|
|
21
|
+
followSource = source;
|
|
22
|
+
}
|
|
23
|
+
else if (isSvelteReadable(source)) {
|
|
24
|
+
const initialFromReadable = sampleSource(source);
|
|
25
|
+
svelteBridge = motionValue(initialFromReadable);
|
|
26
|
+
cleanupReadableBridge = source.subscribe((v) => {
|
|
27
|
+
// Svelte readable contract emits synchronously on subscribe. Skip
|
|
28
|
+
// the initial emit (already seeded above) so attachFollow doesn't
|
|
29
|
+
// fire an animation on attach.
|
|
30
|
+
if (svelteBridge.get() === v)
|
|
31
|
+
return;
|
|
32
|
+
svelteBridge.set(v);
|
|
33
|
+
});
|
|
34
|
+
followSource = svelteBridge;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
followSource = source;
|
|
38
|
+
}
|
|
39
|
+
const initial = isMotionValue(followSource) ? followSource.get() : followSource;
|
|
40
|
+
const value = motionValue(initial);
|
|
41
|
+
// Default transition is `spring` — matches React framer-motion's
|
|
42
|
+
// `useFollowValue` default. Caller's `type` (if set) overrides.
|
|
43
|
+
const stopFollow = attachFollow(value, followSource, { type: 'spring', ...options });
|
|
44
|
+
const dispose = () => {
|
|
45
|
+
stopFollow?.();
|
|
46
|
+
cleanupReadableBridge?.();
|
|
47
|
+
svelteBridge?.destroy();
|
|
48
|
+
};
|
|
49
|
+
$effect(() => () => value.destroy());
|
|
50
|
+
return augmentMotionValue(value, dispose);
|
|
51
|
+
}
|