@humanspeak/svelte-motion 0.4.8 → 0.5.0
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 +17 -11
- package/dist/index.js +9 -9
- 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/booleanSnapshot.svelte.d.ts +37 -0
- package/dist/utils/booleanSnapshot.svelte.js +48 -0
- package/dist/utils/dom.d.ts +13 -0
- package/dist/utils/dom.js +19 -0
- package/dist/utils/inView.svelte.d.ts +209 -0
- package/dist/utils/inView.svelte.js +323 -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/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/scroll.svelte.d.ts +91 -0
- package/dist/utils/scroll.svelte.js +259 -0
- package/dist/utils/spring.svelte.d.ts +2 -6
- package/dist/utils/spring.svelte.js +6 -70
- 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/inView.d.ts +0 -136
- package/dist/utils/inView.js +0 -266
- 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/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/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
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type MotionValue as MotionDomMotionValue } from 'motion-dom';
|
|
2
|
+
import { type AugmentedMotionValue } from './augmentMotionValue.svelte.js';
|
|
3
|
+
/**
|
|
4
|
+
* The shape returned by {@link useMotionValue}: a real motion-dom
|
|
5
|
+
* `MotionValue<T>` (so it composes with `animate()`, `useTransform`,
|
|
6
|
+
* `useSpring`, etc. and passes `isMotionValue`) plus a Svelte 5 reactive
|
|
7
|
+
* `.current` getter and a Svelte readable store `.subscribe` shim.
|
|
8
|
+
*
|
|
9
|
+
* @template T The value type — typically `number` or `string`.
|
|
10
|
+
* @see {@link AugmentedMotionValue}
|
|
11
|
+
*/
|
|
12
|
+
export type MotionValue<T = number> = AugmentedMotionValue<T>;
|
|
13
|
+
/**
|
|
14
|
+
* Re-export of motion-dom's raw `MotionValue` type for cases where the
|
|
15
|
+
* un-augmented shape is needed (e.g. typing follow sources, function
|
|
16
|
+
* dependencies). Most call sites should prefer {@link MotionValue}.
|
|
17
|
+
*/
|
|
18
|
+
export type RawMotionValue<T = number> = MotionDomMotionValue<T>;
|
|
19
|
+
/**
|
|
20
|
+
* Creates a tracked, mutable value backed by motion-dom's `MotionValue`.
|
|
21
|
+
*
|
|
22
|
+
* Use `.set(v)` to write the value imperatively, `.get()` to sample it
|
|
23
|
+
* imperatively, and `.current` to read it inside Svelte 5 reactive scopes
|
|
24
|
+
* (templates, `$derived`, `$effect`). The same value also implements the
|
|
25
|
+
* Svelte readable store contract via `.subscribe(run)`, so legacy `$mv`
|
|
26
|
+
* template syntax and `svelte/store`'s `get()` keep working.
|
|
27
|
+
*
|
|
28
|
+
* Returned object is a real motion-dom `MotionValue` — it composes with
|
|
29
|
+
* `useTransform`, `useSpring`, `useVelocity`, the `animate()` driver, and
|
|
30
|
+
* passes `isMotionValue`. Unlike `useSpring`, writes are immediate — there
|
|
31
|
+
* is no follow source and no animation.
|
|
32
|
+
*
|
|
33
|
+
* Lifecycle: must be called during component initialization. Cleanup is
|
|
34
|
+
* registered via `$effect`; motion-dom's internal listeners and animation
|
|
35
|
+
* subscriptions are released when the surrounding component / effect tears
|
|
36
|
+
* down. Call `.destroy()` to clean up early.
|
|
37
|
+
*
|
|
38
|
+
* SSR-safe: motion-dom's `motionValue` runs without DOM access; on the
|
|
39
|
+
* server reads return the initial value and writes still work, with no
|
|
40
|
+
* timers or listeners attached.
|
|
41
|
+
*
|
|
42
|
+
* @template T The value type — typically `number` or `string`.
|
|
43
|
+
* @param initial The starting value.
|
|
44
|
+
* @returns A `MotionValue<T>` augmented with `.current` and `.subscribe`.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```svelte
|
|
48
|
+
* <script lang="ts">
|
|
49
|
+
* import { useMotionValue, useTransform } from '@humanspeak/svelte-motion'
|
|
50
|
+
*
|
|
51
|
+
* const x = useMotionValue(0)
|
|
52
|
+
* const opacity = useTransform(x, [0, 200], [0, 1])
|
|
53
|
+
* </script>
|
|
54
|
+
*
|
|
55
|
+
* <input type="range" min="0" max="200" oninput={(e) => x.set(+e.currentTarget.value)} />
|
|
56
|
+
* <div style="opacity: {opacity.current}">x = {x.current}</div>
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* @see https://motion.dev/docs/react-motion-value
|
|
60
|
+
*/
|
|
61
|
+
export declare const useMotionValue: <T = number>(initial: T) => MotionValue<T>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { motionValue } from 'motion-dom';
|
|
2
|
+
import { augmentMotionValue } from './augmentMotionValue.svelte.js';
|
|
3
|
+
/**
|
|
4
|
+
* Creates a tracked, mutable value backed by motion-dom's `MotionValue`.
|
|
5
|
+
*
|
|
6
|
+
* Use `.set(v)` to write the value imperatively, `.get()` to sample it
|
|
7
|
+
* imperatively, and `.current` to read it inside Svelte 5 reactive scopes
|
|
8
|
+
* (templates, `$derived`, `$effect`). The same value also implements the
|
|
9
|
+
* Svelte readable store contract via `.subscribe(run)`, so legacy `$mv`
|
|
10
|
+
* template syntax and `svelte/store`'s `get()` keep working.
|
|
11
|
+
*
|
|
12
|
+
* Returned object is a real motion-dom `MotionValue` — it composes with
|
|
13
|
+
* `useTransform`, `useSpring`, `useVelocity`, the `animate()` driver, and
|
|
14
|
+
* passes `isMotionValue`. Unlike `useSpring`, writes are immediate — there
|
|
15
|
+
* is no follow source and no animation.
|
|
16
|
+
*
|
|
17
|
+
* Lifecycle: must be called during component initialization. Cleanup is
|
|
18
|
+
* registered via `$effect`; motion-dom's internal listeners and animation
|
|
19
|
+
* subscriptions are released when the surrounding component / effect tears
|
|
20
|
+
* down. Call `.destroy()` to clean up early.
|
|
21
|
+
*
|
|
22
|
+
* SSR-safe: motion-dom's `motionValue` runs without DOM access; on the
|
|
23
|
+
* server reads return the initial value and writes still work, with no
|
|
24
|
+
* timers or listeners attached.
|
|
25
|
+
*
|
|
26
|
+
* @template T The value type — typically `number` or `string`.
|
|
27
|
+
* @param initial The starting value.
|
|
28
|
+
* @returns A `MotionValue<T>` augmented with `.current` and `.subscribe`.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```svelte
|
|
32
|
+
* <script lang="ts">
|
|
33
|
+
* import { useMotionValue, useTransform } from '@humanspeak/svelte-motion'
|
|
34
|
+
*
|
|
35
|
+
* const x = useMotionValue(0)
|
|
36
|
+
* const opacity = useTransform(x, [0, 200], [0, 1])
|
|
37
|
+
* </script>
|
|
38
|
+
*
|
|
39
|
+
* <input type="range" min="0" max="200" oninput={(e) => x.set(+e.currentTarget.value)} />
|
|
40
|
+
* <div style="opacity: {opacity.current}">x = {x.current}</div>
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* @see https://motion.dev/docs/react-motion-value
|
|
44
|
+
*/
|
|
45
|
+
export const useMotionValue = (initial) => {
|
|
46
|
+
const value = motionValue(initial);
|
|
47
|
+
$effect(() => () => value.destroy());
|
|
48
|
+
return augmentMotionValue(value);
|
|
49
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type BooleanSnapshot } from './booleanSnapshot.svelte.js';
|
|
2
|
+
/**
|
|
3
|
+
* State returned by {@link useReducedMotion}.
|
|
4
|
+
*/
|
|
5
|
+
export type ReducedMotionState = BooleanSnapshot;
|
|
6
|
+
/**
|
|
7
|
+
* Returns a `{ current, subscribe }` object that reflects the user's
|
|
8
|
+
* `prefers-reduced-motion` setting. Mirrors framer-motion's
|
|
9
|
+
* `useReducedMotion` adapted for Svelte 5 runes.
|
|
10
|
+
*
|
|
11
|
+
* - `state.current` is reactive — read it in templates / `$derived` /
|
|
12
|
+
* `$effect` and it tracks the underlying `matchMedia` listener.
|
|
13
|
+
* - `state.subscribe(run)` is the Svelte readable store contract:
|
|
14
|
+
* synchronously emits the current value, then re-emits on every change.
|
|
15
|
+
* Kept for compat with downstream hooks that still consume Svelte
|
|
16
|
+
* readables until the Tier 2 wave lands.
|
|
17
|
+
*
|
|
18
|
+
* Diverges from React framer-motion's plain `boolean | null` return for
|
|
19
|
+
* the same reason as `useCycle`: a `$state`-backed value must live on an
|
|
20
|
+
* object so reads inside getters preserve tracking under runes.
|
|
21
|
+
*
|
|
22
|
+
* SSR-safe: returns a static `{ current: false }` when `window` /
|
|
23
|
+
* `matchMedia` is unavailable, including when `matchMedia` throws.
|
|
24
|
+
*
|
|
25
|
+
* The media listener is bound to the surrounding reactive scope via
|
|
26
|
+
* `$effect` — call this from a component `<script>` block (the standard
|
|
27
|
+
* hook contract). On unmount the listener is detached automatically.
|
|
28
|
+
*
|
|
29
|
+
* @returns A `ReducedMotionState` reflecting the OS reduced-motion setting.
|
|
30
|
+
* @see https://motion.dev/docs/react-reduced-motion
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```svelte
|
|
34
|
+
* <script lang="ts">
|
|
35
|
+
* import { useReducedMotion } from '@humanspeak/svelte-motion'
|
|
36
|
+
*
|
|
37
|
+
* const reduced = useReducedMotion()
|
|
38
|
+
* </script>
|
|
39
|
+
*
|
|
40
|
+
* <div style:transform={reduced.current ? 'none' : 'rotate(45deg)'} />
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export declare const useReducedMotion: () => ReducedMotionState;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { createBooleanSnapshot } from './booleanSnapshot.svelte.js';
|
|
2
|
+
const REDUCED_MOTION_QUERY = '(prefers-reduced-motion: reduce)';
|
|
3
|
+
/**
|
|
4
|
+
* Returns a `{ current, subscribe }` object that reflects the user's
|
|
5
|
+
* `prefers-reduced-motion` setting. Mirrors framer-motion's
|
|
6
|
+
* `useReducedMotion` adapted for Svelte 5 runes.
|
|
7
|
+
*
|
|
8
|
+
* - `state.current` is reactive — read it in templates / `$derived` /
|
|
9
|
+
* `$effect` and it tracks the underlying `matchMedia` listener.
|
|
10
|
+
* - `state.subscribe(run)` is the Svelte readable store contract:
|
|
11
|
+
* synchronously emits the current value, then re-emits on every change.
|
|
12
|
+
* Kept for compat with downstream hooks that still consume Svelte
|
|
13
|
+
* readables until the Tier 2 wave lands.
|
|
14
|
+
*
|
|
15
|
+
* Diverges from React framer-motion's plain `boolean | null` return for
|
|
16
|
+
* the same reason as `useCycle`: a `$state`-backed value must live on an
|
|
17
|
+
* object so reads inside getters preserve tracking under runes.
|
|
18
|
+
*
|
|
19
|
+
* SSR-safe: returns a static `{ current: false }` when `window` /
|
|
20
|
+
* `matchMedia` is unavailable, including when `matchMedia` throws.
|
|
21
|
+
*
|
|
22
|
+
* The media listener is bound to the surrounding reactive scope via
|
|
23
|
+
* `$effect` — call this from a component `<script>` block (the standard
|
|
24
|
+
* hook contract). On unmount the listener is detached automatically.
|
|
25
|
+
*
|
|
26
|
+
* @returns A `ReducedMotionState` reflecting the OS reduced-motion setting.
|
|
27
|
+
* @see https://motion.dev/docs/react-reduced-motion
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```svelte
|
|
31
|
+
* <script lang="ts">
|
|
32
|
+
* import { useReducedMotion } from '@humanspeak/svelte-motion'
|
|
33
|
+
*
|
|
34
|
+
* const reduced = useReducedMotion()
|
|
35
|
+
* </script>
|
|
36
|
+
*
|
|
37
|
+
* <div style:transform={reduced.current ? 'none' : 'rotate(45deg)'} />
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export const useReducedMotion = () => {
|
|
41
|
+
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
|
|
42
|
+
return staticState(false);
|
|
43
|
+
}
|
|
44
|
+
let media;
|
|
45
|
+
try {
|
|
46
|
+
media = window.matchMedia(REDUCED_MOTION_QUERY);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return staticState(false);
|
|
50
|
+
}
|
|
51
|
+
const [state, set] = createBooleanSnapshot(media.matches);
|
|
52
|
+
const handler = (event) => set(event.matches);
|
|
53
|
+
$effect(() => {
|
|
54
|
+
// Resync on (re-)mount in case the OS preference changed while
|
|
55
|
+
// the component was torn down between effect runs.
|
|
56
|
+
set(media.matches);
|
|
57
|
+
if (typeof media.addEventListener === 'function') {
|
|
58
|
+
media.addEventListener('change', handler);
|
|
59
|
+
return () => media.removeEventListener('change', handler);
|
|
60
|
+
}
|
|
61
|
+
// Safari < 14 / older WebKit — addListener/removeListener fallback.
|
|
62
|
+
media.addListener(handler);
|
|
63
|
+
return () => media.removeListener(handler);
|
|
64
|
+
});
|
|
65
|
+
return state;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* SSR / no-matchMedia fallback. Static value, no listeners; `subscribe`
|
|
69
|
+
* is a one-shot sync emit + no-op unsubscribe so consumers can wire it
|
|
70
|
+
* the same way they do the live state.
|
|
71
|
+
*/
|
|
72
|
+
const staticState = (value) => ({
|
|
73
|
+
get current() {
|
|
74
|
+
return value;
|
|
75
|
+
},
|
|
76
|
+
subscribe(run) {
|
|
77
|
+
run(value);
|
|
78
|
+
return () => undefined;
|
|
79
|
+
}
|
|
80
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { type ReducedMotionState } from './reducedMotion.svelte.js';
|
|
2
|
+
/**
|
|
3
|
+
* Returns a copy of `keyframes` with transform-related keys
|
|
4
|
+
* (`x`, `y`, `scale`, `rotate`, `skew`, `translate*`, etc.) removed
|
|
5
|
+
* when `reduced` is `true`. Returns `keyframes` unchanged otherwise.
|
|
6
|
+
*
|
|
7
|
+
* The `transition` key is preserved so per-key transitions still flow
|
|
8
|
+
* through to the animation engine; only the visual *targets* are
|
|
9
|
+
* stripped, not the timing config.
|
|
10
|
+
*
|
|
11
|
+
* @template T Keyframes record (or `undefined`).
|
|
12
|
+
* @param keyframes Source keyframes record — may be `undefined`, in
|
|
13
|
+
* which case the same `undefined` is returned regardless of
|
|
14
|
+
* `reduced`.
|
|
15
|
+
* @param reduced When `true`, strip transform keys; when `false`,
|
|
16
|
+
* return `keyframes` unchanged (by reference).
|
|
17
|
+
* @returns The original `keyframes` when `reduced` is `false` (same
|
|
18
|
+
* reference); otherwise a new record with transform keys filtered
|
|
19
|
+
* out and other keys preserved.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* filterReducedMotionKeyframes(
|
|
24
|
+
* { x: 100, scale: 1.2, opacity: 0.5 },
|
|
25
|
+
* true
|
|
26
|
+
* )
|
|
27
|
+
* // → { opacity: 0.5 }
|
|
28
|
+
*
|
|
29
|
+
* filterReducedMotionKeyframes(
|
|
30
|
+
* { x: 100, opacity: 0.5, transition: { duration: 0.3 } },
|
|
31
|
+
* true
|
|
32
|
+
* )
|
|
33
|
+
* // → { opacity: 0.5, transition: { duration: 0.3 } }
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export declare const filterReducedMotionKeyframes: <T extends Record<string, unknown> | undefined>(keyframes: T, reduced: boolean) => T;
|
|
37
|
+
/**
|
|
38
|
+
* Returns a `{ current, subscribe }` object reflecting the resolved
|
|
39
|
+
* reduced-motion policy for the current component subtree.
|
|
40
|
+
*
|
|
41
|
+
* Resolution combines the nearest `<MotionConfig reducedMotion>` ancestor
|
|
42
|
+
* with the OS-level `prefers-reduced-motion` setting:
|
|
43
|
+
*
|
|
44
|
+
* - `'always'` → always `true`
|
|
45
|
+
* - `'never'` (or no `<MotionConfig>` ancestor) → always `false`
|
|
46
|
+
* - `'user'` → mirrors the OS preference, defaulting to `false` in SSR
|
|
47
|
+
*
|
|
48
|
+
* Both reactive read paths fire on **both** sources changing:
|
|
49
|
+
*
|
|
50
|
+
* - `.current` re-evaluates inside any reactive scope that reads it
|
|
51
|
+
* (templates, `$derived`, `$effect`) when either the OS preference *or*
|
|
52
|
+
* a parent `<MotionConfig reducedMotion={...}>` policy reassigns.
|
|
53
|
+
* - `.subscribe(run)` callbacks are driven by both the OS path
|
|
54
|
+
* (sync via `osReduced.subscribe`) and a `$effect` tracking the
|
|
55
|
+
* `motionConfig.reducedMotion` prop. Legacy store consumers see policy
|
|
56
|
+
* changes too — a fix vs. the prior `derived()`-based impl, which only
|
|
57
|
+
* re-fired on OS changes.
|
|
58
|
+
*
|
|
59
|
+
* @returns A `ReducedMotionState` reflecting the merged policy + OS setting.
|
|
60
|
+
* @see https://motion.dev/docs/react-reduced-motion
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```svelte
|
|
64
|
+
* <script>
|
|
65
|
+
* import { useReducedMotionConfig } from '@humanspeak/svelte-motion'
|
|
66
|
+
* const reduced = useReducedMotionConfig()
|
|
67
|
+
* </script>
|
|
68
|
+
*
|
|
69
|
+
* {#if !reduced.current}
|
|
70
|
+
* <motion.div animate={{ x: 100 }} />
|
|
71
|
+
* {/if}
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export declare const useReducedMotionConfig: () => ReducedMotionState;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { getMotionConfig } from '../components/motionConfig.context.js';
|
|
2
|
+
import { createBooleanSnapshot } from './booleanSnapshot.svelte.js';
|
|
3
|
+
import { useReducedMotion } from './reducedMotion.svelte.js';
|
|
4
|
+
/**
|
|
5
|
+
* CSS / motion property keys that move or rotate an element via `transform`.
|
|
6
|
+
* When reduced motion is active these keys are stripped from animate keyframes
|
|
7
|
+
* so the element stays in place while non-transform properties (opacity, color,
|
|
8
|
+
* etc.) continue to animate.
|
|
9
|
+
*/
|
|
10
|
+
const TRANSFORM_KEYS = new Set([
|
|
11
|
+
'x',
|
|
12
|
+
'y',
|
|
13
|
+
'z',
|
|
14
|
+
'translate',
|
|
15
|
+
'translateX',
|
|
16
|
+
'translateY',
|
|
17
|
+
'translateZ',
|
|
18
|
+
'scale',
|
|
19
|
+
'scaleX',
|
|
20
|
+
'scaleY',
|
|
21
|
+
'scaleZ',
|
|
22
|
+
'rotate',
|
|
23
|
+
'rotateX',
|
|
24
|
+
'rotateY',
|
|
25
|
+
'rotateZ',
|
|
26
|
+
'skew',
|
|
27
|
+
'skewX',
|
|
28
|
+
'skewY',
|
|
29
|
+
'transform',
|
|
30
|
+
'transformPerspective',
|
|
31
|
+
'perspective'
|
|
32
|
+
]);
|
|
33
|
+
/**
|
|
34
|
+
* Returns a copy of `keyframes` with transform-related keys
|
|
35
|
+
* (`x`, `y`, `scale`, `rotate`, `skew`, `translate*`, etc.) removed
|
|
36
|
+
* when `reduced` is `true`. Returns `keyframes` unchanged otherwise.
|
|
37
|
+
*
|
|
38
|
+
* The `transition` key is preserved so per-key transitions still flow
|
|
39
|
+
* through to the animation engine; only the visual *targets* are
|
|
40
|
+
* stripped, not the timing config.
|
|
41
|
+
*
|
|
42
|
+
* @template T Keyframes record (or `undefined`).
|
|
43
|
+
* @param keyframes Source keyframes record — may be `undefined`, in
|
|
44
|
+
* which case the same `undefined` is returned regardless of
|
|
45
|
+
* `reduced`.
|
|
46
|
+
* @param reduced When `true`, strip transform keys; when `false`,
|
|
47
|
+
* return `keyframes` unchanged (by reference).
|
|
48
|
+
* @returns The original `keyframes` when `reduced` is `false` (same
|
|
49
|
+
* reference); otherwise a new record with transform keys filtered
|
|
50
|
+
* out and other keys preserved.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts
|
|
54
|
+
* filterReducedMotionKeyframes(
|
|
55
|
+
* { x: 100, scale: 1.2, opacity: 0.5 },
|
|
56
|
+
* true
|
|
57
|
+
* )
|
|
58
|
+
* // → { opacity: 0.5 }
|
|
59
|
+
*
|
|
60
|
+
* filterReducedMotionKeyframes(
|
|
61
|
+
* { x: 100, opacity: 0.5, transition: { duration: 0.3 } },
|
|
62
|
+
* true
|
|
63
|
+
* )
|
|
64
|
+
* // → { opacity: 0.5, transition: { duration: 0.3 } }
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export const filterReducedMotionKeyframes = (keyframes, reduced) => {
|
|
68
|
+
if (!reduced || !keyframes)
|
|
69
|
+
return keyframes;
|
|
70
|
+
const out = {};
|
|
71
|
+
for (const key of Object.keys(keyframes)) {
|
|
72
|
+
if (!TRANSFORM_KEYS.has(key))
|
|
73
|
+
out[key] = keyframes[key];
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Returns a `{ current, subscribe }` object reflecting the resolved
|
|
79
|
+
* reduced-motion policy for the current component subtree.
|
|
80
|
+
*
|
|
81
|
+
* Resolution combines the nearest `<MotionConfig reducedMotion>` ancestor
|
|
82
|
+
* with the OS-level `prefers-reduced-motion` setting:
|
|
83
|
+
*
|
|
84
|
+
* - `'always'` → always `true`
|
|
85
|
+
* - `'never'` (or no `<MotionConfig>` ancestor) → always `false`
|
|
86
|
+
* - `'user'` → mirrors the OS preference, defaulting to `false` in SSR
|
|
87
|
+
*
|
|
88
|
+
* Both reactive read paths fire on **both** sources changing:
|
|
89
|
+
*
|
|
90
|
+
* - `.current` re-evaluates inside any reactive scope that reads it
|
|
91
|
+
* (templates, `$derived`, `$effect`) when either the OS preference *or*
|
|
92
|
+
* a parent `<MotionConfig reducedMotion={...}>` policy reassigns.
|
|
93
|
+
* - `.subscribe(run)` callbacks are driven by both the OS path
|
|
94
|
+
* (sync via `osReduced.subscribe`) and a `$effect` tracking the
|
|
95
|
+
* `motionConfig.reducedMotion` prop. Legacy store consumers see policy
|
|
96
|
+
* changes too — a fix vs. the prior `derived()`-based impl, which only
|
|
97
|
+
* re-fired on OS changes.
|
|
98
|
+
*
|
|
99
|
+
* @returns A `ReducedMotionState` reflecting the merged policy + OS setting.
|
|
100
|
+
* @see https://motion.dev/docs/react-reduced-motion
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```svelte
|
|
104
|
+
* <script>
|
|
105
|
+
* import { useReducedMotionConfig } from '@humanspeak/svelte-motion'
|
|
106
|
+
* const reduced = useReducedMotionConfig()
|
|
107
|
+
* </script>
|
|
108
|
+
*
|
|
109
|
+
* {#if !reduced.current}
|
|
110
|
+
* <motion.div animate={{ x: 100 }} />
|
|
111
|
+
* {/if}
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
export const useReducedMotionConfig = () => {
|
|
115
|
+
const motionConfig = getMotionConfig();
|
|
116
|
+
const osReduced = useReducedMotion();
|
|
117
|
+
const resolve = () => {
|
|
118
|
+
const policy = motionConfig?.reducedMotion ?? 'never';
|
|
119
|
+
if (policy === 'always')
|
|
120
|
+
return true;
|
|
121
|
+
if (policy === 'never')
|
|
122
|
+
return false;
|
|
123
|
+
return osReduced.current;
|
|
124
|
+
};
|
|
125
|
+
const [state, set] = createBooleanSnapshot(resolve());
|
|
126
|
+
// Sync path: `osReduced.subscribe` fires the run callback on every OS
|
|
127
|
+
// preference change (and once synchronously on subscribe). The
|
|
128
|
+
// snapshot's same-value dedupe absorbs that initial duplicate emit.
|
|
129
|
+
const osUnsub = osReduced.subscribe(() => set(resolve()));
|
|
130
|
+
// Async path: `<MotionConfig reducedMotion>` is exposed via a
|
|
131
|
+
// property getter over the config component's prop, so reading it
|
|
132
|
+
// inside `$effect` tracks reassignments and fires the same `set` —
|
|
133
|
+
// closing the gap the legacy `derived()`-based impl had. Returning
|
|
134
|
+
// `osUnsub` installs it as the effect's cleanup, so the OS
|
|
135
|
+
// subscription is released on unmount.
|
|
136
|
+
$effect(() => {
|
|
137
|
+
// Void the read so the lint unused-expression rule doesn't fire
|
|
138
|
+
// on a deliberate dependency touch.
|
|
139
|
+
void motionConfig?.reducedMotion;
|
|
140
|
+
set(resolve());
|
|
141
|
+
return osUnsub;
|
|
142
|
+
});
|
|
143
|
+
return state;
|
|
144
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { type AugmentedMotionValue } from './augmentMotionValue.svelte.js';
|
|
2
|
+
import { type ElementOrGetter } from './dom.js';
|
|
3
|
+
/**
|
|
4
|
+
* A scroll offset edge defined as a string (e.g. `"start"`, `"end"`,
|
|
5
|
+
* `"center"`) or a number (0–1 progress). Each offset entry is a pair of
|
|
6
|
+
* `[target, container]`.
|
|
7
|
+
*
|
|
8
|
+
* Intentionally looser than motion's internal `ScrollOffset` type — that
|
|
9
|
+
* uses template-literal types (`` `${Edge} ${Edge}` ``) we don't want to
|
|
10
|
+
* surface to consumers. The runtime call paths through `scroll(...)` cast
|
|
11
|
+
* `options.offset as never` to bridge the two; safe because every shape our
|
|
12
|
+
* type allows is structurally a member of motion's union.
|
|
13
|
+
*/
|
|
14
|
+
type ScrollOffset = Array<[number | string, number | string]> | string[];
|
|
15
|
+
/**
|
|
16
|
+
* Options accepted by {@link useScroll}.
|
|
17
|
+
*/
|
|
18
|
+
export type UseScrollOptions = {
|
|
19
|
+
/** Scrollable container to track. Defaults to the page. Accepts an element or a getter function. */
|
|
20
|
+
container?: ElementOrGetter;
|
|
21
|
+
/** Target element to track position of within the container. Accepts an element or a getter function. */
|
|
22
|
+
target?: ElementOrGetter;
|
|
23
|
+
/** Scroll offset configuration for element position tracking. */
|
|
24
|
+
offset?: ScrollOffset;
|
|
25
|
+
/** Which axis to use for the single-axis `progress` value supplied to `scroll()`. */
|
|
26
|
+
axis?: 'x' | 'y';
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Return type of {@link useScroll} — four motion-dom `MotionValue<number>`s
|
|
30
|
+
* representing scroll position and normalised progress for both axes, each
|
|
31
|
+
* augmented with `.current` + `.subscribe`.
|
|
32
|
+
*/
|
|
33
|
+
export type UseScrollReturn = {
|
|
34
|
+
scrollX: AugmentedMotionValue<number>;
|
|
35
|
+
scrollY: AugmentedMotionValue<number>;
|
|
36
|
+
scrollXProgress: AugmentedMotionValue<number>;
|
|
37
|
+
scrollYProgress: AugmentedMotionValue<number>;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Creates scroll-linked motion values for building scroll-driven animations
|
|
41
|
+
* such as progress indicators and parallax effects.
|
|
42
|
+
*
|
|
43
|
+
* Returns four `MotionValue<number>`s: `scrollX` / `scrollY` (current
|
|
44
|
+
* position in px) and `scrollXProgress` / `scrollYProgress` (0–1
|
|
45
|
+
* normalised). Each is a real motion-dom `MotionValue` augmented with a
|
|
46
|
+
* `$state`-backed `.current` getter and a `.subscribe` shim, so they
|
|
47
|
+
* compose with `useTransform`, `useSpring`, and the rest of the Tier 2
|
|
48
|
+
* surface, and they read reactively in Svelte 5 templates.
|
|
49
|
+
*
|
|
50
|
+
* **GPU-accelerated when supported.** On browsers that implement CSS
|
|
51
|
+
* scroll-timeline (no `target`) or view-timeline (with a `target` and a
|
|
52
|
+
* preset `offset`), the *Progress motion values run on the compositor
|
|
53
|
+
* thread via WAAPI — no per-frame JS callback. The non-progress
|
|
54
|
+
* `scrollX` / `scrollY` motion values always use the JS-driven `scroll()`
|
|
55
|
+
* primitive since the absolute pixel offset isn't directly available from
|
|
56
|
+
* native timelines.
|
|
57
|
+
*
|
|
58
|
+
* `container` and `target` accept either an `HTMLElement` directly or a
|
|
59
|
+
* getter `() => HTMLElement | undefined`. The getter form is the right
|
|
60
|
+
* choice with `bind:this`. Element resolution is deferred to a microtask
|
|
61
|
+
* (matches React framer-motion 1:1, faster than rAF polling), so a getter
|
|
62
|
+
* that hasn't hydrated yet is retried as soon as Svelte's mount tick
|
|
63
|
+
* settles it.
|
|
64
|
+
*
|
|
65
|
+
* Lifecycle: the underlying `scroll()` observer and any accelerate factory
|
|
66
|
+
* attach at mount via `$effect` and detach at unmount, regardless of how
|
|
67
|
+
* many consumers are reading the values. The four motion values are torn
|
|
68
|
+
* down at the same time.
|
|
69
|
+
*
|
|
70
|
+
* SSR-safe: returns four static `motionValue(0)`s with no scroll observer
|
|
71
|
+
* on the server.
|
|
72
|
+
*
|
|
73
|
+
* @param options Optional scroll tracking configuration.
|
|
74
|
+
* @returns Four `MotionValue<number>`s — `scrollX`, `scrollY`, `scrollXProgress`, `scrollYProgress`.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```svelte
|
|
78
|
+
* <script>
|
|
79
|
+
* import { useScroll, useSpring } from '@humanspeak/svelte-motion'
|
|
80
|
+
*
|
|
81
|
+
* const { scrollYProgress } = useScroll()
|
|
82
|
+
* const scaleX = useSpring(scrollYProgress)
|
|
83
|
+
* </script>
|
|
84
|
+
*
|
|
85
|
+
* <div style="transform: scaleX({scaleX.current}); transform-origin: left;" />
|
|
86
|
+
* ```
|
|
87
|
+
*
|
|
88
|
+
* @see https://motion.dev/docs/react-use-scroll
|
|
89
|
+
*/
|
|
90
|
+
export declare const useScroll: (options?: UseScrollOptions) => UseScrollReturn;
|
|
91
|
+
export {};
|