@humanspeak/svelte-motion 0.4.7 → 0.4.9
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/html/_MotionContainer.svelte +8 -8
- package/dist/index.d.ts +9 -7
- package/dist/index.js +5 -5
- package/dist/utils/booleanSnapshot.svelte.d.ts +37 -0
- package/dist/utils/booleanSnapshot.svelte.js +48 -0
- package/dist/utils/cycle.svelte.d.ts +98 -0
- package/dist/utils/cycle.svelte.js +44 -0
- package/dist/utils/inView.svelte.d.ts +209 -0
- package/dist/utils/inView.svelte.js +323 -0
- package/dist/utils/reducedMotion.svelte.d.ts +43 -0
- package/dist/utils/reducedMotion.svelte.js +80 -0
- package/dist/utils/reducedMotionConfig.svelte.d.ts +74 -0
- package/dist/utils/reducedMotionConfig.svelte.js +144 -0
- package/dist/utils/spring.svelte.d.ts +83 -0
- package/dist/utils/spring.svelte.js +118 -0
- package/package.json +8 -8
- package/dist/utils/cycle.d.ts +0 -35
- package/dist/utils/cycle.js +0 -48
- package/dist/utils/inView.d.ts +0 -136
- package/dist/utils/inView.js +0 -266
- package/dist/utils/reducedMotion.d.ts +0 -20
- package/dist/utils/reducedMotion.js +0 -42
- package/dist/utils/reducedMotionConfig.d.ts +0 -39
- package/dist/utils/reducedMotionConfig.js +0 -92
- package/dist/utils/spring.d.ts +0 -51
- package/dist/utils/spring.js +0 -157
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import {
|
|
9
9
|
filterReducedMotionKeyframes,
|
|
10
10
|
useReducedMotionConfig
|
|
11
|
-
} from '../utils/reducedMotionConfig'
|
|
11
|
+
} from '../utils/reducedMotionConfig.svelte'
|
|
12
12
|
import type {
|
|
13
13
|
MotionProps,
|
|
14
14
|
MotionTransition,
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
import { attachWhileTap } from '../utils/interaction'
|
|
30
30
|
import { attachWhileHover } from '../utils/hover'
|
|
31
31
|
import { attachWhileFocus } from '../utils/focus'
|
|
32
|
-
import { attachWhileInView } from '../utils/inView'
|
|
32
|
+
import { attachWhileInView } from '../utils/inView.svelte'
|
|
33
33
|
import {
|
|
34
34
|
measureRect,
|
|
35
35
|
computeFlipTransforms,
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
setCustomContext,
|
|
58
58
|
getCustomContext
|
|
59
59
|
} from '../components/variantContext.context'
|
|
60
|
-
import {
|
|
60
|
+
import { writable } from 'svelte/store'
|
|
61
61
|
import {
|
|
62
62
|
transformSVGPathProperties,
|
|
63
63
|
computeNormalizedSVGInitialAttrs,
|
|
@@ -130,11 +130,11 @@
|
|
|
130
130
|
let isLoaded = $state<'mounting' | 'initial' | 'ready' | 'animated'>('mounting')
|
|
131
131
|
let dataPath = $state<number>(-1)
|
|
132
132
|
const motionConfig = $derived(getMotionConfig())
|
|
133
|
-
const
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
|
|
133
|
+
const reducedMotionState = useReducedMotionConfig()
|
|
134
|
+
// `.current` is $state-backed inside reducedMotionState; tracking it via
|
|
135
|
+
// $derived makes `reducedMotion` re-evaluate whenever the OS preference
|
|
136
|
+
// or `<MotionConfig reducedMotion>` policy changes.
|
|
137
|
+
const reducedMotion = $derived(reducedMotionState.current)
|
|
138
138
|
|
|
139
139
|
// Get presence context to check if we're inside AnimatePresence
|
|
140
140
|
const context = getAnimatePresenceContext()
|
package/dist/index.d.ts
CHANGED
|
@@ -10,19 +10,21 @@ 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
|
-
export { useInView } from './utils/inView';
|
|
17
|
-
export type { UseInViewOptions } from './utils/inView';
|
|
16
|
+
export { useInView } from './utils/inView.svelte';
|
|
17
|
+
export type { InViewState, UseInViewOptions } from './utils/inView.svelte';
|
|
18
18
|
export { useMotionTemplate } from './utils/motionTemplate';
|
|
19
19
|
export { useMotionValue } from './utils/motionValue';
|
|
20
20
|
export type { MotionValue } from './utils/motionValue';
|
|
21
21
|
export { useMotionValueEvent } from './utils/motionValueEvent';
|
|
22
|
-
export { useReducedMotion } from './utils/reducedMotion';
|
|
23
|
-
export {
|
|
22
|
+
export { useReducedMotion } from './utils/reducedMotion.svelte';
|
|
23
|
+
export type { ReducedMotionState } from './utils/reducedMotion.svelte';
|
|
24
|
+
export { useReducedMotionConfig } from './utils/reducedMotionConfig.svelte';
|
|
24
25
|
export { useScroll } from './utils/scroll';
|
|
25
|
-
export { useSpring } from './utils/spring';
|
|
26
|
+
export { useSpring } from './utils/spring.svelte';
|
|
27
|
+
export type { SpringMotionValue, UseSpringOptions } from './utils/spring.svelte';
|
|
26
28
|
export { useIsPresent, usePresence } from './utils/usePresence';
|
|
27
29
|
export type { UsePresenceState } from './utils/usePresence';
|
|
28
30
|
export { useVelocity } from './utils/velocity';
|
package/dist/index.js
CHANGED
|
@@ -11,16 +11,16 @@ 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
|
-
export { useInView } from './utils/inView';
|
|
16
|
+
export { useInView } from './utils/inView.svelte';
|
|
17
17
|
export { useMotionTemplate } from './utils/motionTemplate';
|
|
18
18
|
export { useMotionValue } from './utils/motionValue';
|
|
19
19
|
export { useMotionValueEvent } from './utils/motionValueEvent';
|
|
20
|
-
export { useReducedMotion } from './utils/reducedMotion';
|
|
21
|
-
export { useReducedMotionConfig } from './utils/reducedMotionConfig';
|
|
20
|
+
export { useReducedMotion } from './utils/reducedMotion.svelte';
|
|
21
|
+
export { useReducedMotionConfig } from './utils/reducedMotionConfig.svelte';
|
|
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
|
/**
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared `{ current, subscribe }` shape returned by the Wave 2 boolean
|
|
3
|
+
* snapshot hooks — `useReducedMotion`, `useReducedMotionConfig`,
|
|
4
|
+
* `useInView`. `.current` is `$state`-backed; `.subscribe(run)` is the
|
|
5
|
+
* Svelte readable store contract preserved for legacy consumers during
|
|
6
|
+
* the Tier 2 migration.
|
|
7
|
+
*/
|
|
8
|
+
export type BooleanSnapshot = {
|
|
9
|
+
/** Reactive read in Svelte 5 templates / `$derived` / `$effect`. */
|
|
10
|
+
readonly current: boolean;
|
|
11
|
+
/** Svelte readable store contract — emits synchronously on subscribe. */
|
|
12
|
+
subscribe: (run: (value: boolean) => void) => () => void;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Build a `{ current, subscribe }` snapshot + an internal `set`
|
|
16
|
+
* function. Centralises the dedupe + subscriber-fanout that all three
|
|
17
|
+
* boolean-snapshot hooks need.
|
|
18
|
+
*
|
|
19
|
+
* Returns a tuple so consumers can hand the snapshot to callers while
|
|
20
|
+
* keeping `set` internal (it's not on the returned state object).
|
|
21
|
+
*
|
|
22
|
+
* Same-value writes via `set` are no-ops — saves a fanout call and
|
|
23
|
+
* means callers don't need their own change-detection guard.
|
|
24
|
+
*
|
|
25
|
+
* @param initial Starting value for the `current` cell.
|
|
26
|
+
* @returns A `[state, set]` tuple where `state` is the publicly-shared
|
|
27
|
+
* `{ current, subscribe }` and `set` is the internal updater the hook
|
|
28
|
+
* uses to push values from its event source.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* const [state, set] = createBooleanSnapshot(media.matches)
|
|
33
|
+
* media.addEventListener('change', (e) => set(e.matches))
|
|
34
|
+
* return state // { current, subscribe }
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare const createBooleanSnapshot: (initial: boolean) => [BooleanSnapshot, (value: boolean) => void];
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { SvelteSet } from 'svelte/reactivity';
|
|
2
|
+
/**
|
|
3
|
+
* Build a `{ current, subscribe }` snapshot + an internal `set`
|
|
4
|
+
* function. Centralises the dedupe + subscriber-fanout that all three
|
|
5
|
+
* boolean-snapshot hooks need.
|
|
6
|
+
*
|
|
7
|
+
* Returns a tuple so consumers can hand the snapshot to callers while
|
|
8
|
+
* keeping `set` internal (it's not on the returned state object).
|
|
9
|
+
*
|
|
10
|
+
* Same-value writes via `set` are no-ops — saves a fanout call and
|
|
11
|
+
* means callers don't need their own change-detection guard.
|
|
12
|
+
*
|
|
13
|
+
* @param initial Starting value for the `current` cell.
|
|
14
|
+
* @returns A `[state, set]` tuple where `state` is the publicly-shared
|
|
15
|
+
* `{ current, subscribe }` and `set` is the internal updater the hook
|
|
16
|
+
* uses to push values from its event source.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* const [state, set] = createBooleanSnapshot(media.matches)
|
|
21
|
+
* media.addEventListener('change', (e) => set(e.matches))
|
|
22
|
+
* return state // { current, subscribe }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export const createBooleanSnapshot = (initial) => {
|
|
26
|
+
let current = $state(initial);
|
|
27
|
+
const subscribers = new SvelteSet();
|
|
28
|
+
const state = {
|
|
29
|
+
get current() {
|
|
30
|
+
return current;
|
|
31
|
+
},
|
|
32
|
+
subscribe(run) {
|
|
33
|
+
subscribers.add(run);
|
|
34
|
+
run(current);
|
|
35
|
+
return () => {
|
|
36
|
+
subscribers.delete(run);
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const set = (value) => {
|
|
41
|
+
if (value === current)
|
|
42
|
+
return;
|
|
43
|
+
current = value;
|
|
44
|
+
for (const sub of subscribers)
|
|
45
|
+
sub(value);
|
|
46
|
+
};
|
|
47
|
+
return [state, set];
|
|
48
|
+
};
|
|
@@ -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,209 @@
|
|
|
1
|
+
import { type AnimationOptions, type DOMKeyframesDefinition } from 'motion';
|
|
2
|
+
import type { MotionViewport } from '../types.js';
|
|
3
|
+
import { type BooleanSnapshot } from './booleanSnapshot.svelte.js';
|
|
4
|
+
import { type ElementOrGetter } from './dom.js';
|
|
5
|
+
/**
|
|
6
|
+
* Split a `whileInView` definition into the visual keyframes and an
|
|
7
|
+
* optional nested `transition`. Mirrors the shape framer-motion uses
|
|
8
|
+
* where a single object carries both the target values and their
|
|
9
|
+
* timing config.
|
|
10
|
+
*
|
|
11
|
+
* Defensive against `undefined` / `null` input: `def ?? {}` ensures
|
|
12
|
+
* destructuring never throws, and the returned `keyframes` is then an
|
|
13
|
+
* empty record.
|
|
14
|
+
*
|
|
15
|
+
* @param def `whileInView` record possibly carrying a nested
|
|
16
|
+
* `transition` config. May be `null` / `undefined` defensively (the
|
|
17
|
+
* spread normalises to `{}`).
|
|
18
|
+
* @returns Object with the keyframes (everything *except* `transition`)
|
|
19
|
+
* and the extracted `transition` (or `undefined` if none was nested).
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* splitInViewDefinition({ opacity: 1, y: 0, transition: { duration: 0.5 } })
|
|
24
|
+
* // → { keyframes: { opacity: 1, y: 0 }, transition: { duration: 0.5 } }
|
|
25
|
+
*
|
|
26
|
+
* splitInViewDefinition({ opacity: 1 })
|
|
27
|
+
* // → { keyframes: { opacity: 1 }, transition: undefined }
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export declare const splitInViewDefinition: (def: Record<string, unknown>) => {
|
|
31
|
+
keyframes: Record<string, unknown>;
|
|
32
|
+
transition?: AnimationOptions;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Compute the baseline values to restore to when an element leaves the
|
|
36
|
+
* viewport — only for the keys named in `whileInView`. Any key the
|
|
37
|
+
* element is not animating into stays as it was.
|
|
38
|
+
*
|
|
39
|
+
* For each key in `whileInView`, resolve a baseline by walking sources
|
|
40
|
+
* in this preference order:
|
|
41
|
+
*
|
|
42
|
+
* 1. `animate[key]` — the user's declared resting state
|
|
43
|
+
* 2. `initial[key]` — the pre-animation state
|
|
44
|
+
* 3. Neutral transform defaults (e.g. `x: 0`, `scale: 1`, `opacity: 1`)
|
|
45
|
+
* when the key is a known transform property
|
|
46
|
+
* 4. Inline CSS function value (`var(...)`, `calc(...)`, `url(...)`)
|
|
47
|
+
* read off `style.getPropertyValue` — handles cases where nested
|
|
48
|
+
* semicolons (e.g. `url(data:...;base64,...)`) would break a
|
|
49
|
+
* string-scrape
|
|
50
|
+
* 5. `getComputedStyle(el)[key]` — last resort
|
|
51
|
+
*
|
|
52
|
+
* The walk is per-key, so different baseline keys may be sourced from
|
|
53
|
+
* different layers.
|
|
54
|
+
*
|
|
55
|
+
* @param el Element whose computed style is read as the final fallback.
|
|
56
|
+
* Must be a real DOM node (the function reads inline style and
|
|
57
|
+
* `getComputedStyle`).
|
|
58
|
+
* @param opts Layered animation definitions:
|
|
59
|
+
* @param opts.initial Optional `initial` record from the component.
|
|
60
|
+
* @param opts.animate Optional `animate` record from the component.
|
|
61
|
+
* @param opts.whileInView The `whileInView` record — its keys drive
|
|
62
|
+
* which baseline entries get computed. Nested `transition` is
|
|
63
|
+
* stripped before walking.
|
|
64
|
+
* @returns A new record containing one entry per key found in
|
|
65
|
+
* `opts.whileInView`. May be empty if `whileInView` is empty.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```ts
|
|
69
|
+
* computeInViewBaseline(element, {
|
|
70
|
+
* initial: { opacity: 0, y: 50 },
|
|
71
|
+
* animate: { opacity: 1, y: 0 },
|
|
72
|
+
* whileInView: { opacity: 1, scale: 1.1 }
|
|
73
|
+
* })
|
|
74
|
+
* // → { opacity: 1, scale: 1 }
|
|
75
|
+
* // opacity sourced from animate; scale falls to the neutral default.
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export declare const computeInViewBaseline: (el: HTMLElement, opts: {
|
|
79
|
+
initial?: Record<string, unknown>;
|
|
80
|
+
animate?: Record<string, unknown>;
|
|
81
|
+
whileInView?: Record<string, unknown>;
|
|
82
|
+
}) => Record<string, unknown>;
|
|
83
|
+
/**
|
|
84
|
+
* Wire a `whileInView` interaction onto an element using motion's
|
|
85
|
+
* `inView` primitive. On viewport entry the element animates to the
|
|
86
|
+
* supplied keyframes; on exit it animates back to a baseline computed
|
|
87
|
+
* via {@link computeInViewBaseline}.
|
|
88
|
+
*
|
|
89
|
+
* Used internally by `motion.<tag>` components to power the
|
|
90
|
+
* `whileInView` prop, and exposed for callers that want the same
|
|
91
|
+
* declarative behavior without going through a motion component.
|
|
92
|
+
*
|
|
93
|
+
* When `viewport.once` is `true`, the element latches on first entry
|
|
94
|
+
* — no exit animation runs, and the IntersectionObserver is detached
|
|
95
|
+
* via a `queueMicrotask(stop)` after the entry handler returns.
|
|
96
|
+
*
|
|
97
|
+
* @param el Target element to observe and animate.
|
|
98
|
+
* @param whileInView Keyframes to apply on entry. May carry a nested
|
|
99
|
+
* `transition` config (extracted via {@link splitInViewDefinition}).
|
|
100
|
+
* If `undefined`, the function returns a no-op cleanup without
|
|
101
|
+
* creating an observer.
|
|
102
|
+
* @param mergedTransition Default transition used both when
|
|
103
|
+
* `whileInView` has no nested `transition` and for the exit
|
|
104
|
+
* animation back to baseline.
|
|
105
|
+
* @param callbacks Optional lifecycle hooks:
|
|
106
|
+
* - `onStart` — fires on viewport entry, before the entry animation.
|
|
107
|
+
* - `onEnd` — fires on viewport exit, after the baseline restore
|
|
108
|
+
* animation kicks off. Not called when `viewport.once` is `true`.
|
|
109
|
+
* - `onAnimationComplete` — fires when the entry animation
|
|
110
|
+
* resolves; passed the keyframes that ran.
|
|
111
|
+
* @param baselineSources Sources for {@link computeInViewBaseline}'s
|
|
112
|
+
* per-key walk:
|
|
113
|
+
* - `initial` — the component's `initial` record.
|
|
114
|
+
* - `animate` — the component's `animate` record.
|
|
115
|
+
* @param viewport IntersectionObserver options:
|
|
116
|
+
* - `root` — scroll container (default page).
|
|
117
|
+
* - `margin` — `rootMargin` string.
|
|
118
|
+
* - `amount` — fraction visible required (defaults to `0` here so
|
|
119
|
+
* any pixel counts).
|
|
120
|
+
* - `once` — latch on first entry; skip exit animation.
|
|
121
|
+
* @returns A cleanup function that detaches the IntersectionObserver
|
|
122
|
+
* on call. Safe to invoke after a `once` latch has already fired.
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```ts
|
|
126
|
+
* const cleanup = attachWhileInView(
|
|
127
|
+
* element,
|
|
128
|
+
* { opacity: 1, y: 0, transition: { duration: 0.5 } },
|
|
129
|
+
* { duration: 0.3 },
|
|
130
|
+
* {
|
|
131
|
+
* onStart: () => trackImpression(),
|
|
132
|
+
* onEnd: () => console.log('left viewport')
|
|
133
|
+
* },
|
|
134
|
+
* { initial: { opacity: 0, y: 50 } },
|
|
135
|
+
* { once: true, amount: 0.5 }
|
|
136
|
+
* )
|
|
137
|
+
* // Later — typically component teardown:
|
|
138
|
+
* cleanup()
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
141
|
+
export declare const attachWhileInView: (el: HTMLElement, whileInView: Record<string, unknown> | undefined, mergedTransition: AnimationOptions, callbacks?: {
|
|
142
|
+
onStart?: () => void;
|
|
143
|
+
onEnd?: () => void;
|
|
144
|
+
onAnimationComplete?: (definition: DOMKeyframesDefinition | undefined) => void;
|
|
145
|
+
}, baselineSources?: {
|
|
146
|
+
initial?: Record<string, unknown>;
|
|
147
|
+
animate?: Record<string, unknown>;
|
|
148
|
+
}, viewport?: MotionViewport) => (() => void);
|
|
149
|
+
/**
|
|
150
|
+
* Options accepted by `useInView`.
|
|
151
|
+
*/
|
|
152
|
+
export type UseInViewOptions = {
|
|
153
|
+
/** Element to use as the IntersectionObserver root. Defaults to the viewport. */
|
|
154
|
+
root?: ElementOrGetter;
|
|
155
|
+
/** CSS margin string applied to the root bounding box (e.g. `"100px 0px"`). */
|
|
156
|
+
margin?: string;
|
|
157
|
+
/** Fraction (0-1) or `"some"` / `"all"` of the target that must be visible. */
|
|
158
|
+
amount?: 'some' | 'all' | number;
|
|
159
|
+
/** When `true`, the state latches to `true` on first entry and never flips back. */
|
|
160
|
+
once?: boolean;
|
|
161
|
+
/** Initial value emitted before the first IntersectionObserver callback. */
|
|
162
|
+
initial?: boolean;
|
|
163
|
+
};
|
|
164
|
+
/**
|
|
165
|
+
* State returned by {@link useInView}.
|
|
166
|
+
*/
|
|
167
|
+
export type InViewState = BooleanSnapshot;
|
|
168
|
+
/**
|
|
169
|
+
* Returns an `InViewState` that tracks whether `target` is in the viewport.
|
|
170
|
+
* Mirrors framer-motion's `useInView` adapted for Svelte 5 runes.
|
|
171
|
+
*
|
|
172
|
+
* `target` (and `options.root`) accept either an `HTMLElement` directly or
|
|
173
|
+
* a getter `() => HTMLElement | undefined`. With Svelte's `bind:this` the
|
|
174
|
+
* element isn't available until after mount, so element resolution is
|
|
175
|
+
* deferred — if the element isn't ready, the hook polls on
|
|
176
|
+
* `requestAnimationFrame` until it is.
|
|
177
|
+
*
|
|
178
|
+
* Lifecycle: the IntersectionObserver is bound to the surrounding reactive
|
|
179
|
+
* scope via `$effect`. The observer attaches at mount and detaches at
|
|
180
|
+
* unmount, regardless of how many consumers are reading `.current` or
|
|
181
|
+
* `.subscribe()`. This is a deliberate divergence from the previous
|
|
182
|
+
* store-based impl, which attached lazily on first subscribe.
|
|
183
|
+
*
|
|
184
|
+
* SSR-safe: returns a static `{ current: options.initial ?? false }` when
|
|
185
|
+
* `window` or `IntersectionObserver` is unavailable.
|
|
186
|
+
*
|
|
187
|
+
* @param target - Element (or getter) to observe.
|
|
188
|
+
* @param options - Optional `UseInViewOptions` (`root`, `margin`, `amount`,
|
|
189
|
+
* `once`, `initial`).
|
|
190
|
+
* @returns A `InViewState` reflecting the target's viewport intersection.
|
|
191
|
+
* @see https://motion.dev/docs/react-use-in-view
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* ```svelte
|
|
195
|
+
* <script>
|
|
196
|
+
* import { useInView } from '@humanspeak/svelte-motion'
|
|
197
|
+
*
|
|
198
|
+
* let ref
|
|
199
|
+
* const inView = useInView(() => ref, { once: true })
|
|
200
|
+
*
|
|
201
|
+
* $effect(() => {
|
|
202
|
+
* if (inView.current) trackImpression()
|
|
203
|
+
* })
|
|
204
|
+
* </script>
|
|
205
|
+
*
|
|
206
|
+
* <div bind:this={ref}>{inView.current ? 'visible' : 'hidden'}</div>
|
|
207
|
+
* ```
|
|
208
|
+
*/
|
|
209
|
+
export declare const useInView: (target: ElementOrGetter, options?: UseInViewOptions) => InViewState;
|