@humanspeak/svelte-motion 0.4.8 → 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.
@@ -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 { get, writable } from 'svelte/store'
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 reducedMotionStore = useReducedMotionConfig()
134
- // Seed synchronously so the first render filters keyframes correctly —
135
- // otherwise transforms could flash before the subscribe effect runs.
136
- let reducedMotion = $state(get(reducedMotionStore))
137
- $effect(() => reducedMotionStore.subscribe((value) => (reducedMotion = value)))
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
@@ -13,14 +13,15 @@ export { useAnimationFrame } from './utils/animationFrame';
13
13
  export { useCycle } from './utils/cycle.svelte';
14
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 { useReducedMotionConfig } from './utils/reducedMotionConfig';
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
26
  export { useSpring } from './utils/spring.svelte';
26
27
  export type { SpringMotionValue, UseSpringOptions } from './utils/spring.svelte';
package/dist/index.js CHANGED
@@ -13,12 +13,12 @@ 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 { 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
23
  export { useSpring } from './utils/spring.svelte';
24
24
  export { useIsPresent, usePresence } from './utils/usePresence';
@@ -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,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;