@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.
- package/dist/html/_MotionContainer.svelte +8 -8
- package/dist/index.d.ts +5 -4
- package/dist/index.js +3 -3
- package/dist/utils/booleanSnapshot.svelte.d.ts +37 -0
- package/dist/utils/booleanSnapshot.svelte.js +48 -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/package.json +1 -1
- 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/inView.d.ts
DELETED
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import { type AnimationOptions, type DOMKeyframesDefinition } from 'motion';
|
|
2
|
-
import { type Readable } from 'svelte/store';
|
|
3
|
-
import type { MotionViewport } from '../types.js';
|
|
4
|
-
import { type ElementOrGetter } from './dom.js';
|
|
5
|
-
/**
|
|
6
|
-
* Split a whileInView definition into keyframes and an optional nested transition.
|
|
7
|
-
*
|
|
8
|
-
* @param def While-in-view record that may include a nested `transition`.
|
|
9
|
-
* @returns Object with `keyframes` (no `transition`) and optional `transition`.
|
|
10
|
-
* @example
|
|
11
|
-
* // With transition
|
|
12
|
-
* splitInViewDefinition({ opacity: 1, y: 0, transition: { duration: 0.5 } })
|
|
13
|
-
* // => { keyframes: { opacity: 1, y: 0 }, transition: { duration: 0.5 } }
|
|
14
|
-
*
|
|
15
|
-
* @example
|
|
16
|
-
* // Without transition
|
|
17
|
-
* splitInViewDefinition({ opacity: 1, scale: 1 })
|
|
18
|
-
* // => { keyframes: { opacity: 1, scale: 1 }, transition: undefined }
|
|
19
|
-
*/
|
|
20
|
-
export declare const splitInViewDefinition: (def: Record<string, unknown>) => {
|
|
21
|
-
keyframes: Record<string, unknown>;
|
|
22
|
-
transition?: AnimationOptions;
|
|
23
|
-
};
|
|
24
|
-
/**
|
|
25
|
-
* Compute the baseline values to restore to when element leaves viewport.
|
|
26
|
-
*
|
|
27
|
-
* Preference order per key: `animate` → `initial` → neutral transform defaults
|
|
28
|
-
* → computed style value if present.
|
|
29
|
-
*
|
|
30
|
-
* @param el Target element.
|
|
31
|
-
* @param opts Source records for baseline computation.
|
|
32
|
-
* @returns Minimal baseline record to restore when element leaves viewport.
|
|
33
|
-
* @example
|
|
34
|
-
* computeInViewBaseline(element, {
|
|
35
|
-
* initial: { opacity: 0, y: 50 },
|
|
36
|
-
* animate: { opacity: 1, y: 0 },
|
|
37
|
-
* whileInView: { opacity: 1, scale: 1.1 }
|
|
38
|
-
* })
|
|
39
|
-
* // => { opacity: 1, scale: 1 } (scale defaults to 1, opacity from animate)
|
|
40
|
-
*/
|
|
41
|
-
export declare const computeInViewBaseline: (el: HTMLElement, opts: {
|
|
42
|
-
initial?: Record<string, unknown>;
|
|
43
|
-
animate?: Record<string, unknown>;
|
|
44
|
-
whileInView?: Record<string, unknown>;
|
|
45
|
-
}) => Record<string, unknown>;
|
|
46
|
-
/**
|
|
47
|
-
* Attach whileInView interactions to an element via motion's `inView` primitive.
|
|
48
|
-
*
|
|
49
|
-
* On entry, animates to `whileInView` state (using the nested `transition` if
|
|
50
|
-
* provided). On exit, restores the changed keys to a baseline computed from
|
|
51
|
-
* `initial` / `animate` / neutral transform defaults / inline styles.
|
|
52
|
-
*
|
|
53
|
-
* Delegates to motion's `inView` so the IntersectionObserver implementation
|
|
54
|
-
* is shared with the public `useInView` hook.
|
|
55
|
-
*
|
|
56
|
-
* @param el Target element.
|
|
57
|
-
* @param whileInView While-in-view definition.
|
|
58
|
-
* @param mergedTransition Root/component merged transition.
|
|
59
|
-
* @param callbacks Optional lifecycle callbacks for in-view start/end and animation completion.
|
|
60
|
-
* @param baselineSources Optional sources used to compute baseline.
|
|
61
|
-
* @param viewport Optional IntersectionObserver options. `amount` defaults to `0` (any pixel visible).
|
|
62
|
-
* @returns Cleanup function to stop observing.
|
|
63
|
-
* @example
|
|
64
|
-
* const cleanup = attachWhileInView(
|
|
65
|
-
* element,
|
|
66
|
-
* { opacity: 1, y: 0, transition: { duration: 0.5 } },
|
|
67
|
-
* { duration: 0.3 },
|
|
68
|
-
* {
|
|
69
|
-
* onStart: () => console.log('Entered viewport'),
|
|
70
|
-
* onEnd: () => console.log('Left viewport')
|
|
71
|
-
* },
|
|
72
|
-
* { initial: { opacity: 0, y: 50 } },
|
|
73
|
-
* { once: true, amount: 0.5 }
|
|
74
|
-
* )
|
|
75
|
-
* // Later: cleanup() to stop observing
|
|
76
|
-
*/
|
|
77
|
-
export declare const attachWhileInView: (el: HTMLElement, whileInView: Record<string, unknown> | undefined, mergedTransition: AnimationOptions, callbacks?: {
|
|
78
|
-
onStart?: () => void;
|
|
79
|
-
onEnd?: () => void;
|
|
80
|
-
onAnimationComplete?: (definition: DOMKeyframesDefinition | undefined) => void;
|
|
81
|
-
}, baselineSources?: {
|
|
82
|
-
initial?: Record<string, unknown>;
|
|
83
|
-
animate?: Record<string, unknown>;
|
|
84
|
-
}, viewport?: MotionViewport) => (() => void);
|
|
85
|
-
/**
|
|
86
|
-
* Options accepted by `useInView`.
|
|
87
|
-
*/
|
|
88
|
-
export type UseInViewOptions = {
|
|
89
|
-
/** Element to use as the IntersectionObserver root. Defaults to the viewport. */
|
|
90
|
-
root?: ElementOrGetter;
|
|
91
|
-
/** CSS margin string applied to the root bounding box (e.g. `"100px 0px"`). */
|
|
92
|
-
margin?: string;
|
|
93
|
-
/** Fraction (0-1) or `"some"` / `"all"` of the target that must be visible. */
|
|
94
|
-
amount?: 'some' | 'all' | number;
|
|
95
|
-
/** When `true`, the store latches to `true` on first entry and never flips back. */
|
|
96
|
-
once?: boolean;
|
|
97
|
-
/** Initial value emitted before the first IntersectionObserver callback. */
|
|
98
|
-
initial?: boolean;
|
|
99
|
-
};
|
|
100
|
-
/**
|
|
101
|
-
* Returns a Svelte readable store that tracks whether `target` is in the
|
|
102
|
-
* viewport. Mirrors Framer Motion's `useInView` so the same options
|
|
103
|
-
* (`root`, `margin`, `amount`, `once`, `initial`) work as in React.
|
|
104
|
-
*
|
|
105
|
-
* `target` (and `options.root`) accept either an `HTMLElement` directly or a
|
|
106
|
-
* getter `() => HTMLElement | undefined`. With Svelte's `bind:this`, the
|
|
107
|
-
* element isn't available until after mount, so element resolution is
|
|
108
|
-
* deferred until the first subscriber arrives; if the element isn't ready,
|
|
109
|
-
* the hook polls on `requestAnimationFrame` until it is.
|
|
110
|
-
*
|
|
111
|
-
* SSR-safe: returns a static `readable(initial)` when `window` or
|
|
112
|
-
* `IntersectionObserver` is unavailable.
|
|
113
|
-
*
|
|
114
|
-
* @param target - Element (or getter) to observe.
|
|
115
|
-
* @param options - Optional `UseInViewOptions` (`root`, `margin`, `amount`,
|
|
116
|
-
* `once`, `initial`).
|
|
117
|
-
* @returns A `Readable<boolean>` that flips to `true` while `target` is in view.
|
|
118
|
-
* @see https://motion.dev/docs/react-use-in-view
|
|
119
|
-
*
|
|
120
|
-
* @example
|
|
121
|
-
* ```svelte
|
|
122
|
-
* <script>
|
|
123
|
-
* import { useInView } from '@humanspeak/svelte-motion'
|
|
124
|
-
*
|
|
125
|
-
* let ref
|
|
126
|
-
* const inView = useInView(() => ref, { once: true })
|
|
127
|
-
*
|
|
128
|
-
* $effect(() => {
|
|
129
|
-
* if ($inView) trackImpression()
|
|
130
|
-
* })
|
|
131
|
-
* </script>
|
|
132
|
-
*
|
|
133
|
-
* <div bind:this={ref}>{$inView ? 'visible' : 'hidden'}</div>
|
|
134
|
-
* ```
|
|
135
|
-
*/
|
|
136
|
-
export declare const useInView: (target: ElementOrGetter, options?: UseInViewOptions) => Readable<boolean>;
|
package/dist/utils/inView.js
DELETED
|
@@ -1,266 +0,0 @@
|
|
|
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
|
-
};
|
|
28
|
-
/**
|
|
29
|
-
* Split a whileInView definition into keyframes and an optional nested transition.
|
|
30
|
-
*
|
|
31
|
-
* @param def While-in-view record that may include a nested `transition`.
|
|
32
|
-
* @returns Object with `keyframes` (no `transition`) and optional `transition`.
|
|
33
|
-
* @example
|
|
34
|
-
* // With transition
|
|
35
|
-
* splitInViewDefinition({ opacity: 1, y: 0, transition: { duration: 0.5 } })
|
|
36
|
-
* // => { keyframes: { opacity: 1, y: 0 }, transition: { duration: 0.5 } }
|
|
37
|
-
*
|
|
38
|
-
* @example
|
|
39
|
-
* // Without transition
|
|
40
|
-
* splitInViewDefinition({ opacity: 1, scale: 1 })
|
|
41
|
-
* // => { keyframes: { opacity: 1, scale: 1 }, transition: undefined }
|
|
42
|
-
*/
|
|
43
|
-
export const splitInViewDefinition = (def) => {
|
|
44
|
-
const { transition, ...rest } = (def ?? {});
|
|
45
|
-
return { keyframes: rest, transition };
|
|
46
|
-
};
|
|
47
|
-
/**
|
|
48
|
-
* Compute the baseline values to restore to when element leaves viewport.
|
|
49
|
-
*
|
|
50
|
-
* Preference order per key: `animate` → `initial` → neutral transform defaults
|
|
51
|
-
* → computed style value if present.
|
|
52
|
-
*
|
|
53
|
-
* @param el Target element.
|
|
54
|
-
* @param opts Source records for baseline computation.
|
|
55
|
-
* @returns Minimal baseline record to restore when element leaves viewport.
|
|
56
|
-
* @example
|
|
57
|
-
* computeInViewBaseline(element, {
|
|
58
|
-
* initial: { opacity: 0, y: 50 },
|
|
59
|
-
* animate: { opacity: 1, y: 0 },
|
|
60
|
-
* whileInView: { opacity: 1, scale: 1.1 }
|
|
61
|
-
* })
|
|
62
|
-
* // => { opacity: 1, scale: 1 } (scale defaults to 1, opacity from animate)
|
|
63
|
-
*/
|
|
64
|
-
export const computeInViewBaseline = (el, opts) => {
|
|
65
|
-
const baseline = {};
|
|
66
|
-
const initialRecord = (opts.initial ?? {});
|
|
67
|
-
const animateRecord = (opts.animate ?? {});
|
|
68
|
-
const whileInViewRecordRaw = (opts.whileInView ?? {});
|
|
69
|
-
const whileInViewRecord = { ...whileInViewRecordRaw };
|
|
70
|
-
delete whileInViewRecord.transition;
|
|
71
|
-
const neutralTransformDefaults = {
|
|
72
|
-
x: 0,
|
|
73
|
-
y: 0,
|
|
74
|
-
translateX: 0,
|
|
75
|
-
translateY: 0,
|
|
76
|
-
scale: 1,
|
|
77
|
-
scaleX: 1,
|
|
78
|
-
scaleY: 1,
|
|
79
|
-
rotate: 0,
|
|
80
|
-
rotateX: 0,
|
|
81
|
-
rotateY: 0,
|
|
82
|
-
rotateZ: 0,
|
|
83
|
-
skewX: 0,
|
|
84
|
-
skewY: 0,
|
|
85
|
-
opacity: 1
|
|
86
|
-
};
|
|
87
|
-
const cs = getComputedStyle(el);
|
|
88
|
-
for (const key of Object.keys(whileInViewRecord)) {
|
|
89
|
-
if (Object.prototype.hasOwnProperty.call(animateRecord, key)) {
|
|
90
|
-
baseline[key] = animateRecord[key];
|
|
91
|
-
}
|
|
92
|
-
else if (Object.prototype.hasOwnProperty.call(initialRecord, key)) {
|
|
93
|
-
baseline[key] = initialRecord[key];
|
|
94
|
-
}
|
|
95
|
-
else if (key in neutralTransformDefaults) {
|
|
96
|
-
baseline[key] = neutralTransformDefaults[key];
|
|
97
|
-
}
|
|
98
|
-
else {
|
|
99
|
-
const inlineValue = getInlineCssFunction(el, key);
|
|
100
|
-
if (inlineValue) {
|
|
101
|
-
baseline[key] = inlineValue;
|
|
102
|
-
}
|
|
103
|
-
else if (key in cs) {
|
|
104
|
-
baseline[key] = cs[key];
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
return baseline;
|
|
109
|
-
};
|
|
110
|
-
/**
|
|
111
|
-
* Attach whileInView interactions to an element via motion's `inView` primitive.
|
|
112
|
-
*
|
|
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.
|
|
116
|
-
*
|
|
117
|
-
* Delegates to motion's `inView` so the IntersectionObserver implementation
|
|
118
|
-
* is shared with the public `useInView` hook.
|
|
119
|
-
*
|
|
120
|
-
* @param el Target element.
|
|
121
|
-
* @param whileInView While-in-view definition.
|
|
122
|
-
* @param mergedTransition Root/component merged transition.
|
|
123
|
-
* @param callbacks Optional lifecycle callbacks for in-view start/end and animation completion.
|
|
124
|
-
* @param baselineSources Optional sources used to compute baseline.
|
|
125
|
-
* @param viewport Optional IntersectionObserver options. `amount` defaults to `0` (any pixel visible).
|
|
126
|
-
* @returns Cleanup function to stop observing.
|
|
127
|
-
* @example
|
|
128
|
-
* const cleanup = attachWhileInView(
|
|
129
|
-
* element,
|
|
130
|
-
* { opacity: 1, y: 0, transition: { duration: 0.5 } },
|
|
131
|
-
* { duration: 0.3 },
|
|
132
|
-
* {
|
|
133
|
-
* onStart: () => console.log('Entered viewport'),
|
|
134
|
-
* onEnd: () => console.log('Left viewport')
|
|
135
|
-
* },
|
|
136
|
-
* { initial: { opacity: 0, y: 50 } },
|
|
137
|
-
* { once: true, amount: 0.5 }
|
|
138
|
-
* )
|
|
139
|
-
* // Later: cleanup() to stop observing
|
|
140
|
-
*/
|
|
141
|
-
export const attachWhileInView = (el, whileInView, mergedTransition, callbacks, baselineSources, viewport) => {
|
|
142
|
-
if (!whileInView)
|
|
143
|
-
return () => { };
|
|
144
|
-
let latched = false;
|
|
145
|
-
const stop = motionInView(el, () => {
|
|
146
|
-
if (latched)
|
|
147
|
-
return;
|
|
148
|
-
const inViewBaseline = computeInViewBaseline(el, {
|
|
149
|
-
initial: baselineSources?.initial,
|
|
150
|
-
animate: baselineSources?.animate,
|
|
151
|
-
whileInView
|
|
152
|
-
});
|
|
153
|
-
callbacks?.onStart?.();
|
|
154
|
-
const { keyframes, transition } = splitInViewDefinition(whileInView);
|
|
155
|
-
const animation = animate(el, keyframes, (transition ?? mergedTransition));
|
|
156
|
-
animation.finished
|
|
157
|
-
.then(() => {
|
|
158
|
-
callbacks?.onAnimationComplete?.(keyframes);
|
|
159
|
-
})
|
|
160
|
-
.catch(() => {
|
|
161
|
-
/* animation cancelled — skip completion callback */
|
|
162
|
-
});
|
|
163
|
-
if (viewport?.once) {
|
|
164
|
-
// Latch on first entry. Don't return an exit handler so the
|
|
165
|
-
// element holds its in-view state and we never animate back.
|
|
166
|
-
// Stop observing entirely after the entry handler returns.
|
|
167
|
-
latched = true;
|
|
168
|
-
queueMicrotask(stop);
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
return () => {
|
|
172
|
-
if (Object.keys(inViewBaseline).length > 0) {
|
|
173
|
-
animate(el, inViewBaseline, mergedTransition);
|
|
174
|
-
}
|
|
175
|
-
callbacks?.onEnd?.();
|
|
176
|
-
};
|
|
177
|
-
}, {
|
|
178
|
-
root: viewport?.root,
|
|
179
|
-
// framer-motion types `margin` as a CSS-shorthand template literal;
|
|
180
|
-
// we expose plain `string` so consumers can pass any computed value.
|
|
181
|
-
margin: viewport?.margin,
|
|
182
|
-
amount: viewport?.amount ?? 0
|
|
183
|
-
});
|
|
184
|
-
return stop;
|
|
185
|
-
};
|
|
186
|
-
/**
|
|
187
|
-
* Returns a Svelte readable store that tracks whether `target` is in the
|
|
188
|
-
* viewport. Mirrors Framer Motion's `useInView` so the same options
|
|
189
|
-
* (`root`, `margin`, `amount`, `once`, `initial`) work as in React.
|
|
190
|
-
*
|
|
191
|
-
* `target` (and `options.root`) accept either an `HTMLElement` directly or a
|
|
192
|
-
* getter `() => HTMLElement | undefined`. With Svelte's `bind:this`, the
|
|
193
|
-
* element isn't available until after mount, so element resolution is
|
|
194
|
-
* deferred until the first subscriber arrives; if the element isn't ready,
|
|
195
|
-
* the hook polls on `requestAnimationFrame` until it is.
|
|
196
|
-
*
|
|
197
|
-
* SSR-safe: returns a static `readable(initial)` when `window` or
|
|
198
|
-
* `IntersectionObserver` is unavailable.
|
|
199
|
-
*
|
|
200
|
-
* @param target - Element (or getter) to observe.
|
|
201
|
-
* @param options - Optional `UseInViewOptions` (`root`, `margin`, `amount`,
|
|
202
|
-
* `once`, `initial`).
|
|
203
|
-
* @returns A `Readable<boolean>` that flips to `true` while `target` is in view.
|
|
204
|
-
* @see https://motion.dev/docs/react-use-in-view
|
|
205
|
-
*
|
|
206
|
-
* @example
|
|
207
|
-
* ```svelte
|
|
208
|
-
* <script>
|
|
209
|
-
* import { useInView } from '@humanspeak/svelte-motion'
|
|
210
|
-
*
|
|
211
|
-
* let ref
|
|
212
|
-
* const inView = useInView(() => ref, { once: true })
|
|
213
|
-
*
|
|
214
|
-
* $effect(() => {
|
|
215
|
-
* if ($inView) trackImpression()
|
|
216
|
-
* })
|
|
217
|
-
* </script>
|
|
218
|
-
*
|
|
219
|
-
* <div bind:this={ref}>{$inView ? 'visible' : 'hidden'}</div>
|
|
220
|
-
* ```
|
|
221
|
-
*/
|
|
222
|
-
export const useInView = (target, options = {}) => {
|
|
223
|
-
const initial = options.initial ?? false;
|
|
224
|
-
if (typeof window === 'undefined' || typeof IntersectionObserver === 'undefined') {
|
|
225
|
-
return readable(initial);
|
|
226
|
-
}
|
|
227
|
-
const store = writable(initial);
|
|
228
|
-
let current = initial;
|
|
229
|
-
const update = (next) => {
|
|
230
|
-
if (next === current)
|
|
231
|
-
return;
|
|
232
|
-
current = next;
|
|
233
|
-
store.set(next);
|
|
234
|
-
};
|
|
235
|
-
let latched = false;
|
|
236
|
-
const attachable = createAttachable({
|
|
237
|
-
refs: { target, root: options.root },
|
|
238
|
-
isLatched: () => latched,
|
|
239
|
-
onAttach: ({ target: el, root }, stop) => motionInView(el, () => {
|
|
240
|
-
update(true);
|
|
241
|
-
if (options.once) {
|
|
242
|
-
// Detach inside the entry callback; motion's inView
|
|
243
|
-
// handles re-entry safely via observer.unobserve.
|
|
244
|
-
latched = true;
|
|
245
|
-
stop();
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
return () => update(false);
|
|
249
|
-
}, {
|
|
250
|
-
root: root,
|
|
251
|
-
// framer-motion types `margin` as a CSS-shorthand template
|
|
252
|
-
// literal; we expose plain `string` so the public API is
|
|
253
|
-
// ergonomic and forward-compat with future motion changes.
|
|
254
|
-
margin: options.margin,
|
|
255
|
-
amount: options.amount
|
|
256
|
-
})
|
|
257
|
-
});
|
|
258
|
-
return readable(initial, (set) => {
|
|
259
|
-
const release = attachable.subscribe();
|
|
260
|
-
const unsub = store.subscribe(set);
|
|
261
|
-
return () => {
|
|
262
|
-
unsub();
|
|
263
|
-
release();
|
|
264
|
-
};
|
|
265
|
-
});
|
|
266
|
-
};
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { type Readable } from 'svelte/store';
|
|
2
|
-
/**
|
|
3
|
-
* Returns a readable store that reflects the user's `prefers-reduced-motion` setting.
|
|
4
|
-
*
|
|
5
|
-
* Defaults to `false` in SSR or when `matchMedia` is unavailable/throws.
|
|
6
|
-
*
|
|
7
|
-
* @returns {Readable<boolean>} `true` when the user prefers reduced motion.
|
|
8
|
-
* @see https://motion.dev/docs/react-reduced-motion
|
|
9
|
-
*
|
|
10
|
-
* @example
|
|
11
|
-
* ```svelte
|
|
12
|
-
* <script>
|
|
13
|
-
* import { useReducedMotion } from '@humanspeak/svelte-motion'
|
|
14
|
-
* const reduced = useReducedMotion()
|
|
15
|
-
* </script>
|
|
16
|
-
*
|
|
17
|
-
* <div style:transform={$reduced ? 'none' : 'rotate(45deg)'}>...</div>
|
|
18
|
-
* ```
|
|
19
|
-
*/
|
|
20
|
-
export declare const useReducedMotion: () => Readable<boolean>;
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { readable } from 'svelte/store';
|
|
2
|
-
const REDUCED_MOTION_QUERY = '(prefers-reduced-motion: reduce)';
|
|
3
|
-
const SSR_FALLBACK = readable(false, () => { });
|
|
4
|
-
/**
|
|
5
|
-
* Returns a readable store that reflects the user's `prefers-reduced-motion` setting.
|
|
6
|
-
*
|
|
7
|
-
* Defaults to `false` in SSR or when `matchMedia` is unavailable/throws.
|
|
8
|
-
*
|
|
9
|
-
* @returns {Readable<boolean>} `true` when the user prefers reduced motion.
|
|
10
|
-
* @see https://motion.dev/docs/react-reduced-motion
|
|
11
|
-
*
|
|
12
|
-
* @example
|
|
13
|
-
* ```svelte
|
|
14
|
-
* <script>
|
|
15
|
-
* import { useReducedMotion } from '@humanspeak/svelte-motion'
|
|
16
|
-
* const reduced = useReducedMotion()
|
|
17
|
-
* </script>
|
|
18
|
-
*
|
|
19
|
-
* <div style:transform={$reduced ? 'none' : 'rotate(45deg)'}>...</div>
|
|
20
|
-
* ```
|
|
21
|
-
*/
|
|
22
|
-
export const useReducedMotion = () => {
|
|
23
|
-
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
|
|
24
|
-
return SSR_FALLBACK;
|
|
25
|
-
}
|
|
26
|
-
let media;
|
|
27
|
-
try {
|
|
28
|
-
media = window.matchMedia(REDUCED_MOTION_QUERY);
|
|
29
|
-
}
|
|
30
|
-
catch {
|
|
31
|
-
return SSR_FALLBACK;
|
|
32
|
-
}
|
|
33
|
-
return readable(media.matches, (set) => {
|
|
34
|
-
const handler = (event) => set(event.matches);
|
|
35
|
-
if (typeof media.addEventListener === 'function') {
|
|
36
|
-
media.addEventListener('change', handler);
|
|
37
|
-
return () => media.removeEventListener('change', handler);
|
|
38
|
-
}
|
|
39
|
-
media.addListener(handler);
|
|
40
|
-
return () => media.removeListener(handler);
|
|
41
|
-
});
|
|
42
|
-
};
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { type Readable } from 'svelte/store';
|
|
2
|
-
/**
|
|
3
|
-
* Returns a copy of `keyframes` with transform-related keys removed when
|
|
4
|
-
* `reduced` is `true`. Returns `keyframes` unchanged otherwise.
|
|
5
|
-
*
|
|
6
|
-
* The `transition` key is preserved so per-key transitions still flow through
|
|
7
|
-
* to the animation engine.
|
|
8
|
-
*/
|
|
9
|
-
export declare function filterReducedMotionKeyframes<T extends Record<string, unknown> | undefined>(keyframes: T, reduced: boolean): T;
|
|
10
|
-
/**
|
|
11
|
-
* Returns a readable store that reflects the resolved reduced-motion policy
|
|
12
|
-
* for the current component subtree.
|
|
13
|
-
*
|
|
14
|
-
* Resolution combines the nearest `<MotionConfig reducedMotion>` ancestor with
|
|
15
|
-
* the OS-level `prefers-reduced-motion` setting:
|
|
16
|
-
*
|
|
17
|
-
* - `'always'` → always `true`
|
|
18
|
-
* - `'never'` (or no `<MotionConfig>` ancestor) → always `false`
|
|
19
|
-
* - `'user'` → mirrors the OS preference, defaulting to `false` in SSR
|
|
20
|
-
*
|
|
21
|
-
* Use this from inside motion-aware components to decide whether to skip
|
|
22
|
-
* transform animations.
|
|
23
|
-
*
|
|
24
|
-
* @returns {Readable<boolean>} `true` when descendant motion should be reduced.
|
|
25
|
-
* @see https://motion.dev/docs/react-reduced-motion
|
|
26
|
-
*
|
|
27
|
-
* @example
|
|
28
|
-
* ```svelte
|
|
29
|
-
* <script>
|
|
30
|
-
* import { useReducedMotionConfig } from '@humanspeak/svelte-motion'
|
|
31
|
-
* const reduced = useReducedMotionConfig()
|
|
32
|
-
* </script>
|
|
33
|
-
*
|
|
34
|
-
* {#if !$reduced}
|
|
35
|
-
* <motion.div animate={{ x: 100 }} />
|
|
36
|
-
* {/if}
|
|
37
|
-
* ```
|
|
38
|
-
*/
|
|
39
|
-
export declare const useReducedMotionConfig: () => Readable<boolean>;
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { getMotionConfig } from '../components/motionConfig.context.js';
|
|
2
|
-
import { useReducedMotion } from './reducedMotion.js';
|
|
3
|
-
import { derived } from 'svelte/store';
|
|
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 removed when
|
|
35
|
-
* `reduced` is `true`. Returns `keyframes` unchanged otherwise.
|
|
36
|
-
*
|
|
37
|
-
* The `transition` key is preserved so per-key transitions still flow through
|
|
38
|
-
* to the animation engine.
|
|
39
|
-
*/
|
|
40
|
-
export function filterReducedMotionKeyframes(keyframes, reduced) {
|
|
41
|
-
if (!reduced || !keyframes)
|
|
42
|
-
return keyframes;
|
|
43
|
-
const out = {};
|
|
44
|
-
for (const key of Object.keys(keyframes)) {
|
|
45
|
-
if (!TRANSFORM_KEYS.has(key))
|
|
46
|
-
out[key] = keyframes[key];
|
|
47
|
-
}
|
|
48
|
-
return out;
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Returns a readable store that reflects the resolved reduced-motion policy
|
|
52
|
-
* for the current component subtree.
|
|
53
|
-
*
|
|
54
|
-
* Resolution combines the nearest `<MotionConfig reducedMotion>` ancestor with
|
|
55
|
-
* the OS-level `prefers-reduced-motion` setting:
|
|
56
|
-
*
|
|
57
|
-
* - `'always'` → always `true`
|
|
58
|
-
* - `'never'` (or no `<MotionConfig>` ancestor) → always `false`
|
|
59
|
-
* - `'user'` → mirrors the OS preference, defaulting to `false` in SSR
|
|
60
|
-
*
|
|
61
|
-
* Use this from inside motion-aware components to decide whether to skip
|
|
62
|
-
* transform animations.
|
|
63
|
-
*
|
|
64
|
-
* @returns {Readable<boolean>} `true` when descendant motion should be reduced.
|
|
65
|
-
* @see https://motion.dev/docs/react-reduced-motion
|
|
66
|
-
*
|
|
67
|
-
* @example
|
|
68
|
-
* ```svelte
|
|
69
|
-
* <script>
|
|
70
|
-
* import { useReducedMotionConfig } from '@humanspeak/svelte-motion'
|
|
71
|
-
* const reduced = useReducedMotionConfig()
|
|
72
|
-
* </script>
|
|
73
|
-
*
|
|
74
|
-
* {#if !$reduced}
|
|
75
|
-
* <motion.div animate={{ x: 100 }} />
|
|
76
|
-
* {/if}
|
|
77
|
-
* ```
|
|
78
|
-
*/
|
|
79
|
-
export const useReducedMotionConfig = () => {
|
|
80
|
-
const motionConfig = getMotionConfig();
|
|
81
|
-
// Read motionConfig?.reducedMotion *inside* the derived so dynamic
|
|
82
|
-
// `<MotionConfig reducedMotion={...}>` updates surface to subscribers —
|
|
83
|
-
// motionConfig uses property getters, so the value is always fresh.
|
|
84
|
-
return derived(useReducedMotion(), ($osReduced) => {
|
|
85
|
-
const policy = motionConfig?.reducedMotion ?? 'never';
|
|
86
|
-
if (policy === 'always')
|
|
87
|
-
return true;
|
|
88
|
-
if (policy === 'never')
|
|
89
|
-
return false;
|
|
90
|
-
return $osReduced;
|
|
91
|
-
});
|
|
92
|
-
};
|