@humanspeak/svelte-motion 0.3.1 → 0.3.3
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 +4 -0
- package/dist/index.js +2 -0
- package/dist/utils/attachable.d.ts +74 -0
- package/dist/utils/attachable.js +104 -0
- package/dist/utils/cycle.d.ts +35 -0
- package/dist/utils/cycle.js +48 -0
- package/dist/utils/dom.d.ts +27 -0
- package/dist/utils/dom.js +23 -0
- package/dist/utils/inView.d.ts +62 -8
- package/dist/utils/inView.js +138 -71
- package/dist/utils/scroll.d.ts +1 -6
- package/dist/utils/scroll.js +9 -49
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -6,7 +6,11 @@ export { anticipate, backIn, backInOut, backOut, circIn, circInOut, circOut, cub
|
|
|
6
6
|
export { clamp, distance, distance2D, interpolate, mix, pipe, progress, wrap } from 'motion';
|
|
7
7
|
export type { DragAxis, DragConstraints, DragControls, DragInfo, DragTransition, MotionAnimate, MotionInitial, MotionOnDirectionLock, MotionOnDragTransitionEnd, MotionOnInViewEnd, MotionOnInViewStart, MotionTransition, MotionWhileDrag, MotionWhileFocus, MotionWhileHover, MotionWhileInView, MotionWhileTap, ReducedMotionConfig, Variants } from './types';
|
|
8
8
|
export { useAnimationFrame } from './utils/animationFrame';
|
|
9
|
+
export { useCycle } from './utils/cycle';
|
|
10
|
+
export type { Cycle, CycleState } from './utils/cycle';
|
|
9
11
|
export { createDragControls } from './utils/dragControls';
|
|
12
|
+
export { useInView } from './utils/inView';
|
|
13
|
+
export type { UseInViewOptions } from './utils/inView';
|
|
10
14
|
export { useMotionTemplate } from './utils/motionTemplate';
|
|
11
15
|
export { useMotionValue } from './utils/motionValue';
|
|
12
16
|
export type { MotionValue } from './utils/motionValue';
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,9 @@ export { anticipate, backIn, backInOut, backOut, circIn, circInOut, circOut, cub
|
|
|
8
8
|
// Re-export utility functions
|
|
9
9
|
export { clamp, distance, distance2D, interpolate, mix, pipe, progress, wrap } from 'motion';
|
|
10
10
|
export { useAnimationFrame } from './utils/animationFrame';
|
|
11
|
+
export { useCycle } from './utils/cycle';
|
|
11
12
|
export { createDragControls } from './utils/dragControls';
|
|
13
|
+
export { useInView } from './utils/inView';
|
|
12
14
|
export { useMotionTemplate } from './utils/motionTemplate';
|
|
13
15
|
export { useMotionValue } from './utils/motionValue';
|
|
14
16
|
export { useMotionValueEvent } from './utils/motionValueEvent';
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { type ElementOrGetter } from './dom.js';
|
|
2
|
+
/**
|
|
3
|
+
* Reference set of DOM `ElementOrGetter` inputs keyed by name.
|
|
4
|
+
*
|
|
5
|
+
* Each value is either a resolved element, a getter that returns one (used
|
|
6
|
+
* with Svelte's `bind:this` post-mount timing), or `undefined` when the
|
|
7
|
+
* caller does not need this slot.
|
|
8
|
+
*/
|
|
9
|
+
export type AttachableRefs = Record<string, ElementOrGetter | undefined>;
|
|
10
|
+
/**
|
|
11
|
+
* Map of resolved elements supplied to `onAttach`. Slots whose ref was
|
|
12
|
+
* `undefined` resolve to `undefined`.
|
|
13
|
+
*/
|
|
14
|
+
export type ResolvedRefs<R extends AttachableRefs> = {
|
|
15
|
+
[K in keyof R]: HTMLElement | undefined;
|
|
16
|
+
};
|
|
17
|
+
export type AttachableConfig<R extends AttachableRefs> = {
|
|
18
|
+
/** DOM refs that must resolve before `onAttach` runs. */
|
|
19
|
+
refs: R;
|
|
20
|
+
/**
|
|
21
|
+
* Called when every supplied ref has resolved. Receives a `stop` function
|
|
22
|
+
* that synchronously tears down the attachment - useful for one-shot
|
|
23
|
+
* latches that need to detach inside their own callback.
|
|
24
|
+
*
|
|
25
|
+
* Return a cleanup function for the standard "tear down on last
|
|
26
|
+
* unsubscribe" path.
|
|
27
|
+
*/
|
|
28
|
+
onAttach: (els: ResolvedRefs<R>, stop: VoidFunction) => VoidFunction | void;
|
|
29
|
+
/**
|
|
30
|
+
* When this returns `true`, `subscribe` short-circuits without attaching.
|
|
31
|
+
* Used by hooks that latch a value and stop observing (e.g. `once`).
|
|
32
|
+
* Subscribers added after the latch increment the refcount but never
|
|
33
|
+
* trigger `onAttach`; their unsubscribe is a clean no-op.
|
|
34
|
+
*/
|
|
35
|
+
isLatched?: () => boolean;
|
|
36
|
+
};
|
|
37
|
+
export type Attachable = {
|
|
38
|
+
/**
|
|
39
|
+
* Register a subscriber. Triggers `onAttach` on the first subscriber,
|
|
40
|
+
* polls on `requestAnimationFrame` until refs resolve. Returns a release
|
|
41
|
+
* function that cleans up when the last subscriber leaves.
|
|
42
|
+
*
|
|
43
|
+
* Callers must eventually unsubscribe; the rAF poll loop continues until
|
|
44
|
+
* either every ref resolves or the last subscriber releases.
|
|
45
|
+
*/
|
|
46
|
+
subscribe: () => () => void;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Builds a subscriber-refcounted DOM-attachment primitive. Both `useScroll`
|
|
50
|
+
* and `useInView` use this to defer observer setup until a subscriber arrives,
|
|
51
|
+
* poll for `bind:this` element resolution, and tear down on the last
|
|
52
|
+
* unsubscribe.
|
|
53
|
+
*
|
|
54
|
+
* @param config Attachment configuration: refs to resolve, an `onAttach`
|
|
55
|
+
* callback that returns a cleanup function, and an optional `isLatched`
|
|
56
|
+
* short-circuit.
|
|
57
|
+
* @returns An `Attachable` exposing `subscribe()` for use inside Svelte
|
|
58
|
+
* `readable(..., start)` callbacks.
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* const attachable = createAttachable({
|
|
62
|
+
* refs: { target },
|
|
63
|
+
* onAttach: ({ target }) => {
|
|
64
|
+
* const stopObserving = startObserver(target!, ...)
|
|
65
|
+
* return stopObserving
|
|
66
|
+
* }
|
|
67
|
+
* })
|
|
68
|
+
* return readable(initial, (set) => {
|
|
69
|
+
* const release = attachable.subscribe()
|
|
70
|
+
* return release
|
|
71
|
+
* })
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export declare const createAttachable: <R extends AttachableRefs>(config: AttachableConfig<R>) => Attachable;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { resolveElement } from './dom.js';
|
|
2
|
+
/**
|
|
3
|
+
* Builds a subscriber-refcounted DOM-attachment primitive. Both `useScroll`
|
|
4
|
+
* and `useInView` use this to defer observer setup until a subscriber arrives,
|
|
5
|
+
* poll for `bind:this` element resolution, and tear down on the last
|
|
6
|
+
* unsubscribe.
|
|
7
|
+
*
|
|
8
|
+
* @param config Attachment configuration: refs to resolve, an `onAttach`
|
|
9
|
+
* callback that returns a cleanup function, and an optional `isLatched`
|
|
10
|
+
* short-circuit.
|
|
11
|
+
* @returns An `Attachable` exposing `subscribe()` for use inside Svelte
|
|
12
|
+
* `readable(..., start)` callbacks.
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* const attachable = createAttachable({
|
|
16
|
+
* refs: { target },
|
|
17
|
+
* onAttach: ({ target }) => {
|
|
18
|
+
* const stopObserving = startObserver(target!, ...)
|
|
19
|
+
* return stopObserving
|
|
20
|
+
* }
|
|
21
|
+
* })
|
|
22
|
+
* return readable(initial, (set) => {
|
|
23
|
+
* const release = attachable.subscribe()
|
|
24
|
+
* return release
|
|
25
|
+
* })
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export const createAttachable = (config) => {
|
|
29
|
+
let cleanup;
|
|
30
|
+
let pollRaf = 0;
|
|
31
|
+
let subscriberCount = 0;
|
|
32
|
+
// When stop() runs synchronously inside onAttach, cleanup hasn't been
|
|
33
|
+
// assigned yet. Defer the teardown so the just-returned disposer still
|
|
34
|
+
// gets invoked.
|
|
35
|
+
let attaching = false;
|
|
36
|
+
let stopRequestedDuringAttach = false;
|
|
37
|
+
const cancelPoll = () => {
|
|
38
|
+
if (pollRaf) {
|
|
39
|
+
cancelAnimationFrame(pollRaf);
|
|
40
|
+
pollRaf = 0;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const stop = () => {
|
|
44
|
+
cancelPoll();
|
|
45
|
+
if (attaching) {
|
|
46
|
+
stopRequestedDuringAttach = true;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (cleanup) {
|
|
50
|
+
const fn = cleanup;
|
|
51
|
+
cleanup = undefined;
|
|
52
|
+
fn();
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const tryAttach = () => {
|
|
56
|
+
if (cleanup || config.isLatched?.())
|
|
57
|
+
return;
|
|
58
|
+
const els = {};
|
|
59
|
+
let needsPoll = false;
|
|
60
|
+
for (const key of Object.keys(config.refs)) {
|
|
61
|
+
const ref = config.refs[key];
|
|
62
|
+
const el = resolveElement(ref);
|
|
63
|
+
if (ref && !el)
|
|
64
|
+
needsPoll = true;
|
|
65
|
+
els[key] = el;
|
|
66
|
+
}
|
|
67
|
+
if (needsPoll) {
|
|
68
|
+
if (!pollRaf) {
|
|
69
|
+
pollRaf = requestAnimationFrame(() => {
|
|
70
|
+
pollRaf = 0;
|
|
71
|
+
tryAttach();
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
attaching = true;
|
|
77
|
+
let result;
|
|
78
|
+
try {
|
|
79
|
+
result = config.onAttach(els, stop);
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
attaching = false;
|
|
83
|
+
}
|
|
84
|
+
if (typeof result === 'function')
|
|
85
|
+
cleanup = result;
|
|
86
|
+
if (stopRequestedDuringAttach) {
|
|
87
|
+
stopRequestedDuringAttach = false;
|
|
88
|
+
stop();
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
return {
|
|
92
|
+
subscribe: () => {
|
|
93
|
+
subscriberCount++;
|
|
94
|
+
tryAttach();
|
|
95
|
+
return () => {
|
|
96
|
+
if (subscriberCount > 0)
|
|
97
|
+
subscriberCount--;
|
|
98
|
+
if (subscriberCount > 0)
|
|
99
|
+
return;
|
|
100
|
+
stop();
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
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>;
|
|
@@ -0,0 +1,48 @@
|
|
|
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/dom.d.ts
CHANGED
|
@@ -14,3 +14,30 @@
|
|
|
14
14
|
* @return Whether the value is a DOM `Element`.
|
|
15
15
|
*/
|
|
16
16
|
export declare const isDomElement: (v: unknown) => v is Element;
|
|
17
|
+
/**
|
|
18
|
+
* An element reference - either an element directly or a getter function
|
|
19
|
+
* that returns one. Getters defer resolution past mount, which is useful
|
|
20
|
+
* with Svelte's `bind:this` where the element isn't available synchronously.
|
|
21
|
+
*
|
|
22
|
+
* Both `null` and `undefined` returns from the getter are normalised to
|
|
23
|
+
* `undefined` by `resolveElement`, so either nullable shape works.
|
|
24
|
+
*/
|
|
25
|
+
export type ElementOrGetter = HTMLElement | (() => HTMLElement | null | undefined);
|
|
26
|
+
/**
|
|
27
|
+
* Resolves an `ElementOrGetter` to an `HTMLElement`, or `undefined` if not
|
|
28
|
+
* yet available (e.g. a getter is supplied but the bound element hasn't
|
|
29
|
+
* mounted).
|
|
30
|
+
*
|
|
31
|
+
* Coerces a `null` getter result to `undefined` so the common
|
|
32
|
+
* `let el: HTMLElement | null = null` `bind:this` pattern lines up with the
|
|
33
|
+
* declared return type.
|
|
34
|
+
*
|
|
35
|
+
* @param ref Element or getter to resolve.
|
|
36
|
+
* @returns The resolved element, or `undefined` when not yet available.
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts
|
|
39
|
+
* let node: HTMLElement | null = null
|
|
40
|
+
* resolveElement(() => node) // => undefined until bind:this fires
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export declare const resolveElement: (ref?: ElementOrGetter) => HTMLElement | undefined;
|
package/dist/utils/dom.js
CHANGED
|
@@ -16,3 +16,26 @@
|
|
|
16
16
|
export const isDomElement = (v) => {
|
|
17
17
|
return !!v && typeof v.getBoundingClientRect === 'function';
|
|
18
18
|
};
|
|
19
|
+
/**
|
|
20
|
+
* Resolves an `ElementOrGetter` to an `HTMLElement`, or `undefined` if not
|
|
21
|
+
* yet available (e.g. a getter is supplied but the bound element hasn't
|
|
22
|
+
* mounted).
|
|
23
|
+
*
|
|
24
|
+
* Coerces a `null` getter result to `undefined` so the common
|
|
25
|
+
* `let el: HTMLElement | null = null` `bind:this` pattern lines up with the
|
|
26
|
+
* declared return type.
|
|
27
|
+
*
|
|
28
|
+
* @param ref Element or getter to resolve.
|
|
29
|
+
* @returns The resolved element, or `undefined` when not yet available.
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* let node: HTMLElement | null = null
|
|
33
|
+
* resolveElement(() => node) // => undefined until bind:this fires
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export const resolveElement = (ref) => {
|
|
37
|
+
if (!ref)
|
|
38
|
+
return undefined;
|
|
39
|
+
const value = typeof ref === 'function' ? ref() : ref;
|
|
40
|
+
return value ?? undefined;
|
|
41
|
+
};
|
package/dist/utils/inView.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { type AnimationOptions, type DOMKeyframesDefinition } from 'motion';
|
|
2
|
+
import { type Readable } from 'svelte/store';
|
|
3
|
+
import { type ElementOrGetter } from './dom.js';
|
|
2
4
|
/**
|
|
3
5
|
* Split a whileInView definition into keyframes and an optional nested transition.
|
|
4
6
|
*
|
|
@@ -41,21 +43,21 @@ export declare const computeInViewBaseline: (el: HTMLElement, opts: {
|
|
|
41
43
|
whileInView?: Record<string, unknown>;
|
|
42
44
|
}) => Record<string, unknown>;
|
|
43
45
|
/**
|
|
44
|
-
* Attach whileInView interactions to an element
|
|
46
|
+
* Attach whileInView interactions to an element via motion's `inView` primitive.
|
|
45
47
|
*
|
|
46
|
-
* On
|
|
47
|
-
*
|
|
48
|
-
*
|
|
48
|
+
* On entry, animates to `whileInView` state (using the nested `transition` if
|
|
49
|
+
* provided). On exit, restores the changed keys to a baseline computed from
|
|
50
|
+
* `initial` / `animate` / neutral transform defaults / inline styles.
|
|
49
51
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
+
* Delegates to motion's `inView` so the IntersectionObserver implementation
|
|
53
|
+
* is shared with the public `useInView` hook.
|
|
52
54
|
*
|
|
53
55
|
* @param el Target element.
|
|
54
56
|
* @param whileInView While-in-view definition.
|
|
55
57
|
* @param mergedTransition Root/component merged transition.
|
|
56
58
|
* @param callbacks Optional lifecycle callbacks for in-view start/end and animation completion.
|
|
57
59
|
* @param baselineSources Optional sources used to compute baseline.
|
|
58
|
-
* @returns Cleanup function to
|
|
60
|
+
* @returns Cleanup function to stop observing.
|
|
59
61
|
* @example
|
|
60
62
|
* const cleanup = attachWhileInView(
|
|
61
63
|
* element,
|
|
@@ -67,7 +69,7 @@ export declare const computeInViewBaseline: (el: HTMLElement, opts: {
|
|
|
67
69
|
* },
|
|
68
70
|
* { initial: { opacity: 0, y: 50 } }
|
|
69
71
|
* )
|
|
70
|
-
* // Later: cleanup() to
|
|
72
|
+
* // Later: cleanup() to stop observing
|
|
71
73
|
*/
|
|
72
74
|
export declare const attachWhileInView: (el: HTMLElement, whileInView: Record<string, unknown> | undefined, mergedTransition: AnimationOptions, callbacks?: {
|
|
73
75
|
onStart?: () => void;
|
|
@@ -77,3 +79,55 @@ export declare const attachWhileInView: (el: HTMLElement, whileInView: Record<st
|
|
|
77
79
|
initial?: Record<string, unknown>;
|
|
78
80
|
animate?: Record<string, unknown>;
|
|
79
81
|
}) => (() => void);
|
|
82
|
+
/**
|
|
83
|
+
* Options accepted by `useInView`.
|
|
84
|
+
*/
|
|
85
|
+
export type UseInViewOptions = {
|
|
86
|
+
/** Element to use as the IntersectionObserver root. Defaults to the viewport. */
|
|
87
|
+
root?: ElementOrGetter;
|
|
88
|
+
/** CSS margin string applied to the root bounding box (e.g. `"100px 0px"`). */
|
|
89
|
+
margin?: string;
|
|
90
|
+
/** Fraction (0-1) or `"some"` / `"all"` of the target that must be visible. */
|
|
91
|
+
amount?: 'some' | 'all' | number;
|
|
92
|
+
/** When `true`, the store latches to `true` on first entry and never flips back. */
|
|
93
|
+
once?: boolean;
|
|
94
|
+
/** Initial value emitted before the first IntersectionObserver callback. */
|
|
95
|
+
initial?: boolean;
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Returns a Svelte readable store that tracks whether `target` is in the
|
|
99
|
+
* viewport. Mirrors Framer Motion's `useInView` so the same options
|
|
100
|
+
* (`root`, `margin`, `amount`, `once`, `initial`) work as in React.
|
|
101
|
+
*
|
|
102
|
+
* `target` (and `options.root`) accept either an `HTMLElement` directly or a
|
|
103
|
+
* getter `() => HTMLElement | undefined`. With Svelte's `bind:this`, the
|
|
104
|
+
* element isn't available until after mount, so element resolution is
|
|
105
|
+
* deferred until the first subscriber arrives; if the element isn't ready,
|
|
106
|
+
* the hook polls on `requestAnimationFrame` until it is.
|
|
107
|
+
*
|
|
108
|
+
* SSR-safe: returns a static `readable(initial)` when `window` or
|
|
109
|
+
* `IntersectionObserver` is unavailable.
|
|
110
|
+
*
|
|
111
|
+
* @param target - Element (or getter) to observe.
|
|
112
|
+
* @param options - Optional `UseInViewOptions` (`root`, `margin`, `amount`,
|
|
113
|
+
* `once`, `initial`).
|
|
114
|
+
* @returns A `Readable<boolean>` that flips to `true` while `target` is in view.
|
|
115
|
+
* @see https://motion.dev/docs/react-use-in-view
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* ```svelte
|
|
119
|
+
* <script>
|
|
120
|
+
* import { useInView } from '@humanspeak/svelte-motion'
|
|
121
|
+
*
|
|
122
|
+
* let ref
|
|
123
|
+
* const inView = useInView(() => ref, { once: true })
|
|
124
|
+
*
|
|
125
|
+
* $effect(() => {
|
|
126
|
+
* if ($inView) trackImpression()
|
|
127
|
+
* })
|
|
128
|
+
* </script>
|
|
129
|
+
*
|
|
130
|
+
* <div bind:this={ref}>{$inView ? 'visible' : 'hidden'}</div>
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
export declare const useInView: (target: ElementOrGetter, options?: UseInViewOptions) => Readable<boolean>;
|
package/dist/utils/inView.js
CHANGED
|
@@ -1,4 +1,30 @@
|
|
|
1
|
-
import { animate } from 'motion';
|
|
1
|
+
import { animate, inView as motionInView } from 'motion';
|
|
2
|
+
import { readable, writable } from 'svelte/store';
|
|
3
|
+
import { createAttachable } from './attachable.js';
|
|
4
|
+
import {} from './dom.js';
|
|
5
|
+
const CSS_FUNCTION_RE = /\b(var|calc|min|max|clamp|rgb|rgba|hsl|hsla|url)\s*\(/i;
|
|
6
|
+
/**
|
|
7
|
+
* Read an inline CSS-function value (var/calc/url/etc.) for `propName`
|
|
8
|
+
* directly from the element's style declaration. Returns `null` for missing
|
|
9
|
+
* or non-function values so the caller can fall back to `getComputedStyle`.
|
|
10
|
+
*
|
|
11
|
+
* Uses `style.getPropertyValue` so values like `url(data:image/svg+xml;...)`
|
|
12
|
+
* with nested semicolons are preserved intact - the browser has already
|
|
13
|
+
* parsed the declaration, no string scraping required.
|
|
14
|
+
*
|
|
15
|
+
* @param el Element whose inline style is read.
|
|
16
|
+
* @param propName Camel-case JS property name (e.g. `borderColor`).
|
|
17
|
+
* @returns The inline CSS function value, or `null`.
|
|
18
|
+
* @example
|
|
19
|
+
* getInlineCssFunction(node, 'background') // => 'var(--brand)' | null
|
|
20
|
+
*/
|
|
21
|
+
const getInlineCssFunction = (el, propName) => {
|
|
22
|
+
const kebab = propName.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
23
|
+
const value = el.style.getPropertyValue(kebab).trim();
|
|
24
|
+
if (!value)
|
|
25
|
+
return null;
|
|
26
|
+
return CSS_FUNCTION_RE.test(value) ? value : null;
|
|
27
|
+
};
|
|
2
28
|
/**
|
|
3
29
|
* Split a whileInView definition into keyframes and an optional nested transition.
|
|
4
30
|
*
|
|
@@ -59,27 +85,6 @@ export const computeInViewBaseline = (el, opts) => {
|
|
|
59
85
|
opacity: 1
|
|
60
86
|
};
|
|
61
87
|
const cs = getComputedStyle(el);
|
|
62
|
-
const inlineStyle = el.getAttribute('style') || '';
|
|
63
|
-
// Helper to escape regex metacharacters to prevent ReDoS and ensure literal matching
|
|
64
|
-
const escapeRegExp = (str) => {
|
|
65
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
66
|
-
};
|
|
67
|
-
// Helper to extract CSS function (var, calc, min, max, etc.) from inline style if present
|
|
68
|
-
const getInlineStyleValue = (propName) => {
|
|
69
|
-
const kebabCase = propName.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
70
|
-
const escapedKebabCase = escapeRegExp(kebabCase);
|
|
71
|
-
// Match property name at start of string or after semicolon
|
|
72
|
-
const regex = new RegExp(`(?:^|;)\\s*${escapedKebabCase}\\s*:\\s*([^;]+)`, 'i');
|
|
73
|
-
const match = inlineStyle.match(regex);
|
|
74
|
-
if (match) {
|
|
75
|
-
const value = match[1].trim();
|
|
76
|
-
// Preserve CSS functions: var(), calc(), min(), max(), clamp(), rgb(), hsl(), url(), etc.
|
|
77
|
-
if (/\b(var|calc|min|max|clamp|rgb|rgba|hsl|hsla|url)\s*\(/.test(value)) {
|
|
78
|
-
return value;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
return null;
|
|
82
|
-
};
|
|
83
88
|
for (const key of Object.keys(whileInViewRecord)) {
|
|
84
89
|
if (Object.prototype.hasOwnProperty.call(animateRecord, key)) {
|
|
85
90
|
baseline[key] = animateRecord[key];
|
|
@@ -91,8 +96,7 @@ export const computeInViewBaseline = (el, opts) => {
|
|
|
91
96
|
baseline[key] = neutralTransformDefaults[key];
|
|
92
97
|
}
|
|
93
98
|
else {
|
|
94
|
-
|
|
95
|
-
const inlineValue = getInlineStyleValue(key);
|
|
99
|
+
const inlineValue = getInlineCssFunction(el, key);
|
|
96
100
|
if (inlineValue) {
|
|
97
101
|
baseline[key] = inlineValue;
|
|
98
102
|
}
|
|
@@ -104,21 +108,21 @@ export const computeInViewBaseline = (el, opts) => {
|
|
|
104
108
|
return baseline;
|
|
105
109
|
};
|
|
106
110
|
/**
|
|
107
|
-
* Attach whileInView interactions to an element
|
|
111
|
+
* Attach whileInView interactions to an element via motion's `inView` primitive.
|
|
108
112
|
*
|
|
109
|
-
* On
|
|
110
|
-
*
|
|
111
|
-
*
|
|
113
|
+
* On entry, animates to `whileInView` state (using the nested `transition` if
|
|
114
|
+
* provided). On exit, restores the changed keys to a baseline computed from
|
|
115
|
+
* `initial` / `animate` / neutral transform defaults / inline styles.
|
|
112
116
|
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
117
|
+
* Delegates to motion's `inView` so the IntersectionObserver implementation
|
|
118
|
+
* is shared with the public `useInView` hook.
|
|
115
119
|
*
|
|
116
120
|
* @param el Target element.
|
|
117
121
|
* @param whileInView While-in-view definition.
|
|
118
122
|
* @param mergedTransition Root/component merged transition.
|
|
119
123
|
* @param callbacks Optional lifecycle callbacks for in-view start/end and animation completion.
|
|
120
124
|
* @param baselineSources Optional sources used to compute baseline.
|
|
121
|
-
* @returns Cleanup function to
|
|
125
|
+
* @returns Cleanup function to stop observing.
|
|
122
126
|
* @example
|
|
123
127
|
* const cleanup = attachWhileInView(
|
|
124
128
|
* element,
|
|
@@ -130,50 +134,113 @@ export const computeInViewBaseline = (el, opts) => {
|
|
|
130
134
|
* },
|
|
131
135
|
* { initial: { opacity: 0, y: 50 } }
|
|
132
136
|
* )
|
|
133
|
-
* // Later: cleanup() to
|
|
137
|
+
* // Later: cleanup() to stop observing
|
|
134
138
|
*/
|
|
135
139
|
export const attachWhileInView = (el, whileInView, mergedTransition, callbacks, baselineSources) => {
|
|
136
140
|
if (!whileInView)
|
|
137
141
|
return () => { };
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
})
|
|
158
|
-
.catch(() => {
|
|
159
|
-
// Animation was cancelled, don't call completion callback
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
else if (!entry.isIntersecting && hasAnimated) {
|
|
163
|
-
// Element left viewport: animate back to baseline
|
|
164
|
-
if (inViewBaseline && Object.keys(inViewBaseline).length > 0) {
|
|
165
|
-
animate(el, inViewBaseline, mergedTransition);
|
|
166
|
-
}
|
|
167
|
-
callbacks?.onEnd?.();
|
|
168
|
-
hasAnimated = false;
|
|
142
|
+
return motionInView(el, () => {
|
|
143
|
+
const inViewBaseline = computeInViewBaseline(el, {
|
|
144
|
+
initial: baselineSources?.initial,
|
|
145
|
+
animate: baselineSources?.animate,
|
|
146
|
+
whileInView
|
|
147
|
+
});
|
|
148
|
+
callbacks?.onStart?.();
|
|
149
|
+
const { keyframes, transition } = splitInViewDefinition(whileInView);
|
|
150
|
+
const animation = animate(el, keyframes, (transition ?? mergedTransition));
|
|
151
|
+
animation.finished
|
|
152
|
+
.then(() => {
|
|
153
|
+
callbacks?.onAnimationComplete?.(keyframes);
|
|
154
|
+
})
|
|
155
|
+
.catch(() => {
|
|
156
|
+
/* animation cancelled — skip completion callback */
|
|
157
|
+
});
|
|
158
|
+
return () => {
|
|
159
|
+
if (Object.keys(inViewBaseline).length > 0) {
|
|
160
|
+
animate(el, inViewBaseline, mergedTransition);
|
|
169
161
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
);
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
162
|
+
callbacks?.onEnd?.();
|
|
163
|
+
};
|
|
164
|
+
}, { amount: 0 });
|
|
165
|
+
};
|
|
166
|
+
/**
|
|
167
|
+
* Returns a Svelte readable store that tracks whether `target` is in the
|
|
168
|
+
* viewport. Mirrors Framer Motion's `useInView` so the same options
|
|
169
|
+
* (`root`, `margin`, `amount`, `once`, `initial`) work as in React.
|
|
170
|
+
*
|
|
171
|
+
* `target` (and `options.root`) accept either an `HTMLElement` directly or a
|
|
172
|
+
* getter `() => HTMLElement | undefined`. With Svelte's `bind:this`, the
|
|
173
|
+
* element isn't available until after mount, so element resolution is
|
|
174
|
+
* deferred until the first subscriber arrives; if the element isn't ready,
|
|
175
|
+
* the hook polls on `requestAnimationFrame` until it is.
|
|
176
|
+
*
|
|
177
|
+
* SSR-safe: returns a static `readable(initial)` when `window` or
|
|
178
|
+
* `IntersectionObserver` is unavailable.
|
|
179
|
+
*
|
|
180
|
+
* @param target - Element (or getter) to observe.
|
|
181
|
+
* @param options - Optional `UseInViewOptions` (`root`, `margin`, `amount`,
|
|
182
|
+
* `once`, `initial`).
|
|
183
|
+
* @returns A `Readable<boolean>` that flips to `true` while `target` is in view.
|
|
184
|
+
* @see https://motion.dev/docs/react-use-in-view
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* ```svelte
|
|
188
|
+
* <script>
|
|
189
|
+
* import { useInView } from '@humanspeak/svelte-motion'
|
|
190
|
+
*
|
|
191
|
+
* let ref
|
|
192
|
+
* const inView = useInView(() => ref, { once: true })
|
|
193
|
+
*
|
|
194
|
+
* $effect(() => {
|
|
195
|
+
* if ($inView) trackImpression()
|
|
196
|
+
* })
|
|
197
|
+
* </script>
|
|
198
|
+
*
|
|
199
|
+
* <div bind:this={ref}>{$inView ? 'visible' : 'hidden'}</div>
|
|
200
|
+
* ```
|
|
201
|
+
*/
|
|
202
|
+
export const useInView = (target, options = {}) => {
|
|
203
|
+
const initial = options.initial ?? false;
|
|
204
|
+
if (typeof window === 'undefined' || typeof IntersectionObserver === 'undefined') {
|
|
205
|
+
return readable(initial);
|
|
206
|
+
}
|
|
207
|
+
const store = writable(initial);
|
|
208
|
+
let current = initial;
|
|
209
|
+
const update = (next) => {
|
|
210
|
+
if (next === current)
|
|
211
|
+
return;
|
|
212
|
+
current = next;
|
|
213
|
+
store.set(next);
|
|
178
214
|
};
|
|
215
|
+
let latched = false;
|
|
216
|
+
const attachable = createAttachable({
|
|
217
|
+
refs: { target, root: options.root },
|
|
218
|
+
isLatched: () => latched,
|
|
219
|
+
onAttach: ({ target: el, root }, stop) => motionInView(el, () => {
|
|
220
|
+
update(true);
|
|
221
|
+
if (options.once) {
|
|
222
|
+
// Detach inside the entry callback; motion's inView
|
|
223
|
+
// handles re-entry safely via observer.unobserve.
|
|
224
|
+
latched = true;
|
|
225
|
+
stop();
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
return () => update(false);
|
|
229
|
+
}, {
|
|
230
|
+
root: root,
|
|
231
|
+
// framer-motion types `margin` as a CSS-shorthand template
|
|
232
|
+
// literal; we expose plain `string` so the public API is
|
|
233
|
+
// ergonomic and forward-compat with future motion changes.
|
|
234
|
+
margin: options.margin,
|
|
235
|
+
amount: options.amount
|
|
236
|
+
})
|
|
237
|
+
});
|
|
238
|
+
return readable(initial, (set) => {
|
|
239
|
+
const release = attachable.subscribe();
|
|
240
|
+
const unsub = store.subscribe(set);
|
|
241
|
+
return () => {
|
|
242
|
+
unsub();
|
|
243
|
+
release();
|
|
244
|
+
};
|
|
245
|
+
});
|
|
179
246
|
};
|
package/dist/utils/scroll.d.ts
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
import { type Readable } from 'svelte/store';
|
|
2
|
+
import { type ElementOrGetter } from './dom.js';
|
|
2
3
|
/**
|
|
3
4
|
* A scroll offset edge defined as a string (e.g. `"start"`, `"end"`, `"center"`)
|
|
4
5
|
* or a number (0–1 progress). Each offset entry is a pair of `[target, container]`.
|
|
5
6
|
*/
|
|
6
7
|
type ScrollOffset = Array<[number | string, number | string]> | string[];
|
|
7
|
-
/**
|
|
8
|
-
* An element reference — either an element directly or a getter function
|
|
9
|
-
* that returns one (useful with Svelte's `bind:this` where the element
|
|
10
|
-
* isn't available until after mount).
|
|
11
|
-
*/
|
|
12
|
-
type ElementOrGetter = HTMLElement | (() => HTMLElement | undefined);
|
|
13
8
|
/**
|
|
14
9
|
* Options accepted by `useScroll`.
|
|
15
10
|
*/
|
package/dist/utils/scroll.js
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
import { scroll } from 'motion';
|
|
2
2
|
import { readable, writable } from 'svelte/store';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
*/
|
|
6
|
-
const resolveElement = (ref) => {
|
|
7
|
-
if (!ref)
|
|
8
|
-
return undefined;
|
|
9
|
-
return typeof ref === 'function' ? ref() : ref;
|
|
10
|
-
};
|
|
3
|
+
import { createAttachable } from './attachable.js';
|
|
4
|
+
import {} from './dom.js';
|
|
11
5
|
/**
|
|
12
6
|
* Creates scroll-linked Svelte stores for building scroll-driven animations
|
|
13
7
|
* such as progress indicators and parallax effects.
|
|
@@ -54,29 +48,9 @@ export const useScroll = (options) => {
|
|
|
54
48
|
scrollXProgress: writable(0),
|
|
55
49
|
scrollYProgress: writable(0)
|
|
56
50
|
};
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const attach = () => {
|
|
61
|
-
if (cleanup)
|
|
62
|
-
return;
|
|
63
|
-
// Resolve elements — they may not be available yet when using getters
|
|
64
|
-
const container = resolveElement(options?.container);
|
|
65
|
-
const target = resolveElement(options?.target);
|
|
66
|
-
// If a getter was provided but returned undefined, the element isn't
|
|
67
|
-
// mounted yet. Poll on the next frame until it appears.
|
|
68
|
-
const needsContainer = options?.container && !container;
|
|
69
|
-
const needsTarget = options?.target && !target;
|
|
70
|
-
if (needsContainer || needsTarget) {
|
|
71
|
-
if (!pollRaf) {
|
|
72
|
-
pollRaf = requestAnimationFrame(() => {
|
|
73
|
-
pollRaf = 0;
|
|
74
|
-
attach();
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
cleanup = scroll((_progress, info) => {
|
|
51
|
+
const attachable = createAttachable({
|
|
52
|
+
refs: { container: options?.container, target: options?.target },
|
|
53
|
+
onAttach: ({ container, target }) => scroll((_progress, info) => {
|
|
80
54
|
stores.scrollX.set(info.x.current);
|
|
81
55
|
stores.scrollY.set(info.y.current);
|
|
82
56
|
stores.scrollXProgress.set(info.x.progress);
|
|
@@ -86,28 +60,14 @@ export const useScroll = (options) => {
|
|
|
86
60
|
target,
|
|
87
61
|
offset: options?.offset,
|
|
88
62
|
axis: options?.axis
|
|
89
|
-
})
|
|
90
|
-
};
|
|
91
|
-
const detach = () => {
|
|
92
|
-
if (subscriberCount <= 0) {
|
|
93
|
-
if (pollRaf) {
|
|
94
|
-
cancelAnimationFrame(pollRaf);
|
|
95
|
-
pollRaf = 0;
|
|
96
|
-
}
|
|
97
|
-
if (cleanup) {
|
|
98
|
-
cleanup();
|
|
99
|
-
cleanup = undefined;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
};
|
|
63
|
+
})
|
|
64
|
+
});
|
|
103
65
|
const make = (key) => readable(0, (set) => {
|
|
104
|
-
|
|
66
|
+
const release = attachable.subscribe();
|
|
105
67
|
const unsub = stores[key].subscribe(set);
|
|
106
|
-
attach();
|
|
107
68
|
return () => {
|
|
108
69
|
unsub();
|
|
109
|
-
|
|
110
|
-
detach();
|
|
70
|
+
release();
|
|
111
71
|
};
|
|
112
72
|
});
|
|
113
73
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@humanspeak/svelte-motion",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "A Framer Motion-compatible animation library for Svelte 5 that provides smooth, hardware-accelerated animations. Features include spring physics, custom easing, and fluid transitions. Built on top of the motion library, it offers a simple API for creating complex animations with minimal code. Perfect for interactive UIs, micro-interactions, and engaging user experiences.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"svelte",
|