solidjs-motion 0.1.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/CHANGELOG.md +117 -0
- package/LICENSE +21 -0
- package/README.md +140 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2627 -0
- package/dist/index.js.map +1 -0
- package/dist/src/default-values.d.ts +6 -0
- package/dist/src/index.d.ts +16 -0
- package/dist/src/motion-config.d.ts +14 -0
- package/dist/src/motion-proxy.d.ts +103 -0
- package/dist/src/presence-context.d.ts +4 -0
- package/dist/src/presence.d.ts +95 -0
- package/dist/src/primitives/createDrag.d.ts +16 -0
- package/dist/src/primitives/createDragControls.d.ts +30 -0
- package/dist/src/primitives/createGestures.d.ts +8 -0
- package/dist/src/primitives/createInView.d.ts +51 -0
- package/dist/src/primitives/createMotion.d.ts +82 -0
- package/dist/src/primitives/createPan.d.ts +83 -0
- package/dist/src/primitives/createScroll.d.ts +40 -0
- package/dist/src/primitives/gesture-state.d.ts +108 -0
- package/dist/src/primitives/motion-value.d.ts +111 -0
- package/dist/src/primitives/value-registry.d.ts +34 -0
- package/dist/src/reduced-motion.d.ts +29 -0
- package/dist/src/style.d.ts +79 -0
- package/dist/src/types.d.ts +374 -0
- package/dist/src/use-motion.d.ts +35 -0
- package/dist/src/variants.d.ts +64 -0
- package/package.json +78 -0
- package/src/default-values.ts +52 -0
- package/src/index.ts +60 -0
- package/src/motion-config.tsx +37 -0
- package/src/motion-proxy.tsx +377 -0
- package/src/presence-context.ts +32 -0
- package/src/presence.tsx +466 -0
- package/src/primitives/createDrag.ts +670 -0
- package/src/primitives/createDragControls.ts +101 -0
- package/src/primitives/createGestures.ts +145 -0
- package/src/primitives/createInView.ts +124 -0
- package/src/primitives/createMotion.ts +638 -0
- package/src/primitives/createPan.ts +338 -0
- package/src/primitives/createScroll.ts +101 -0
- package/src/primitives/gesture-state.ts +772 -0
- package/src/primitives/motion-value.ts +328 -0
- package/src/primitives/value-registry.ts +114 -0
- package/src/reduced-motion.ts +51 -0
- package/src/style.ts +266 -0
- package/src/types.ts +538 -0
- package/src/use-motion.tsx +412 -0
- package/src/variants.ts +134 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cancelFrame,
|
|
3
|
+
frame,
|
|
4
|
+
isMotionValue,
|
|
5
|
+
type MotionValue,
|
|
6
|
+
transform as motionTransform,
|
|
7
|
+
motionValue,
|
|
8
|
+
type SpringOptions,
|
|
9
|
+
springValue,
|
|
10
|
+
} from "motion"
|
|
11
|
+
import { type Accessor, createComputed, createSignal, onCleanup } from "solid-js"
|
|
12
|
+
import type { MotionValueAccessor } from "../types"
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// MotionValue events the engine can fire — kept narrow so TypeScript autocomplete
|
|
16
|
+
// surfaces only the documented surface.
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
type MotionValueEvent = "change" | "animationStart" | "animationComplete" | "animationCancel"
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// makeAccessor — wrap a raw motion.MotionValue as a callable hybrid. Invoking
|
|
23
|
+
// `mv()` returns a Solid-tracked read; every MotionValue method (.get, .set,
|
|
24
|
+
// .jump, .on, .getVelocity, etc.) forwards to the underlying value. Both
|
|
25
|
+
// `isMotionValue(mv)` (duck-typed on .getVelocity) and `typeof mv === "function"`
|
|
26
|
+
// are true; createMotion's splitTarget checks isMotionValue first, so the engine
|
|
27
|
+
// treats hybrids as MotionValues.
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
function makeAccessor<T>(mv: MotionValue<T>): MotionValueAccessor<T> {
|
|
31
|
+
// Solid signal bridge — kept in sync via `mv.on("change", ...)`.
|
|
32
|
+
const [signal, setSignal] = createSignal<T>(mv.get())
|
|
33
|
+
// Wrap in updater form so Setter accepts T regardless of its shape (T could
|
|
34
|
+
// include Function for callback-like motion values).
|
|
35
|
+
onCleanup(mv.on("change", (v) => setSignal(() => v)))
|
|
36
|
+
|
|
37
|
+
// The callable: invoking returns the tracked signal value.
|
|
38
|
+
const fn = (() => signal()) as MotionValueAccessor<T>
|
|
39
|
+
|
|
40
|
+
return new Proxy(fn, {
|
|
41
|
+
get(target, prop, receiver) {
|
|
42
|
+
// Function intrinsics (call/apply/bind) stay on the function itself so
|
|
43
|
+
// `fn.call(...)` etc. behave normally.
|
|
44
|
+
if (prop === "call" || prop === "apply" || prop === "bind") {
|
|
45
|
+
return Reflect.get(target, prop, receiver)
|
|
46
|
+
}
|
|
47
|
+
// If we ever attach our own properties to `fn`, prefer those.
|
|
48
|
+
if (Reflect.has(target, prop)) return Reflect.get(target, prop, receiver)
|
|
49
|
+
// Forward to the MotionValue. Methods are bound so `this` is the MV.
|
|
50
|
+
const value = Reflect.get(mv as object, prop, mv)
|
|
51
|
+
return typeof value === "function" ? value.bind(mv) : value
|
|
52
|
+
},
|
|
53
|
+
has(target, prop) {
|
|
54
|
+
return Reflect.has(target, prop) || prop in (mv as object)
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// createMotionValue — callable-hybrid MotionValue auto-disposed on cleanup.
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Create a {@link MotionValueAccessor} bound to the current reactive scope.
|
|
65
|
+
*
|
|
66
|
+
* The returned value has two access patterns:
|
|
67
|
+
*
|
|
68
|
+
* - `mv()` — invoke as a Solid Accessor. Tracks in JSX, `createEffect`,
|
|
69
|
+
* `createMemo`, etc.
|
|
70
|
+
* - `mv.get()` / `mv.set(v)` / `mv.jump(v)` / `mv.on(...)` — the full upstream
|
|
71
|
+
* {@link MotionValue} surface. Matches motion/react idioms.
|
|
72
|
+
*
|
|
73
|
+
* The same value can be passed as a target in
|
|
74
|
+
* `useMotion({ animate: { x: mv } })` (motion engine sees `.getVelocity` via
|
|
75
|
+
* the Proxy and treats it as a motion value) or directly as the target of
|
|
76
|
+
* `animate(mv, 100)`.
|
|
77
|
+
*
|
|
78
|
+
* Auto-destroyed via `onCleanup` when the owner is disposed.
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* const x = createMotionValue(0)
|
|
82
|
+
* x.set(100)
|
|
83
|
+
* animate(x, 200, { duration: 0.5 })
|
|
84
|
+
* <p>{x()}</p> // reactive read in JSX
|
|
85
|
+
*/
|
|
86
|
+
export function createMotionValue<T>(initial: T): MotionValueAccessor<T> {
|
|
87
|
+
const mv = motionValue(initial)
|
|
88
|
+
const accessor = makeAccessor(mv)
|
|
89
|
+
// Route the cleanup call through the accessor so test-time spies on
|
|
90
|
+
// `accessor.destroy` are invoked (the Proxy forwards to the underlying mv).
|
|
91
|
+
onCleanup(() => accessor.destroy())
|
|
92
|
+
return accessor
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// toSignal — adapt any raw MotionValue (e.g. from motion's `motionValue()`
|
|
97
|
+
// factory) to a Solid Accessor. Useful when interoperating with motion APIs
|
|
98
|
+
// that return raw MotionValues outside our hybrid factories.
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Bridge a raw {@link MotionValue} (from motion's `motionValue()` factory or
|
|
103
|
+
* any other motion API that doesn't return our hybrid) to a Solid
|
|
104
|
+
* {@link Accessor}. Seeds with the current value and updates on every
|
|
105
|
+
* `change` event.
|
|
106
|
+
*
|
|
107
|
+
* **You usually don't need this.** Values returned by `createMotionValue`,
|
|
108
|
+
* `createTransform`, `createSpring`, `createTime`, `createVelocity`, and
|
|
109
|
+
* `createTemplate` are already callable — you can do `mv()` directly. Reach
|
|
110
|
+
* for `toSignal` only when you receive a raw MotionValue from an external API.
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* import { motionValue } from "motion"
|
|
114
|
+
* const rawMv = motionValue(0)
|
|
115
|
+
* const xSignal = toSignal(rawMv)
|
|
116
|
+
*/
|
|
117
|
+
export function toSignal<T>(mv: MotionValue<T>): Accessor<T> {
|
|
118
|
+
const [value, setValue] = createSignal<T>(mv.get())
|
|
119
|
+
onCleanup(mv.on("change", (v) => setValue(() => v)))
|
|
120
|
+
return value
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// createMotionValueEvent — register a listener with automatic cleanup.
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Subscribe to a {@link MotionValue} event with automatic cleanup. Convenience
|
|
129
|
+
* wrapper around `mv.on(event, cb)` for parity with motion/react's
|
|
130
|
+
* `useMotionValueEvent`. For per-change reactivity, prefer
|
|
131
|
+
* `createComputed(() => fn(mv()))` since hybrids are directly callable.
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* const x = createMotionValue(0)
|
|
135
|
+
* createMotionValueEvent(x, "animationComplete", () => console.log("done"))
|
|
136
|
+
*/
|
|
137
|
+
export function createMotionValueEvent<T>(
|
|
138
|
+
mv: MotionValue<T>,
|
|
139
|
+
event: MotionValueEvent,
|
|
140
|
+
callback: (latest: T) => void,
|
|
141
|
+
): void {
|
|
142
|
+
onCleanup(mv.on(event, callback))
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// Shared helpers
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
function readInputValue<T>(input: MotionValue<T> | Accessor<T>): T {
|
|
150
|
+
if (isMotionValue(input)) return (input as MotionValue<T>).get()
|
|
151
|
+
return (input as Accessor<T>)()
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function subscribeInput<T>(
|
|
155
|
+
input: MotionValue<T> | Accessor<T>,
|
|
156
|
+
onChange: (value: T) => void,
|
|
157
|
+
): void {
|
|
158
|
+
if (isMotionValue(input)) {
|
|
159
|
+
onCleanup((input as MotionValue<T>).on("change", onChange))
|
|
160
|
+
} else {
|
|
161
|
+
createComputed(() => onChange((input as Accessor<T>)()))
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// createTransform — interpolate one MotionValue/Accessor through a range.
|
|
167
|
+
// Returns a MotionValueAccessor so callable behavior is preserved end-to-end.
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
type TransformOptions = NonNullable<Parameters<typeof motionTransform>[2]>
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Create a {@link MotionValueAccessor} that maps an input through a range/
|
|
174
|
+
* output pair. Mirrors motion/react's `useTransform`. The input can be a
|
|
175
|
+
* MotionValue, our hybrid, or any Solid Accessor; the output composes with
|
|
176
|
+
* `animate()`, `useMotion`'s targets, and JSX reactivity.
|
|
177
|
+
*
|
|
178
|
+
* @example
|
|
179
|
+
* const { scrollY } = createScroll()
|
|
180
|
+
* const opacity = createTransform(scrollY, [0, 200], [1, 0])
|
|
181
|
+
* <div style={{ opacity: opacity() }}>...</div>
|
|
182
|
+
*/
|
|
183
|
+
export function createTransform<I extends number, O>(
|
|
184
|
+
input: MotionValue<I> | Accessor<I>,
|
|
185
|
+
inputRange: I[],
|
|
186
|
+
outputRange: O[],
|
|
187
|
+
options?: TransformOptions,
|
|
188
|
+
): MotionValueAccessor<O> {
|
|
189
|
+
const mapper = motionTransform(inputRange, outputRange, options)
|
|
190
|
+
const mv = motionValue(mapper(readInputValue(input)))
|
|
191
|
+
onCleanup(() => mv.destroy())
|
|
192
|
+
subscribeInput(input, (v) => mv.set(mapper(v)))
|
|
193
|
+
return makeAccessor(mv)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
// createSpring — produce a MotionValueAccessor that spring-tracks an input.
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Spring-smoothed mirror of a numeric input. Returns a
|
|
202
|
+
* {@link MotionValueAccessor} that tracks the source with physics-based easing.
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* const x = createMotionValue(0)
|
|
206
|
+
* const smoothX = createSpring(x, { stiffness: 100, damping: 20 })
|
|
207
|
+
*/
|
|
208
|
+
export function createSpring(
|
|
209
|
+
source: MotionValue<number> | Accessor<number>,
|
|
210
|
+
options?: SpringOptions,
|
|
211
|
+
): MotionValueAccessor<number> {
|
|
212
|
+
if (isMotionValue(source)) {
|
|
213
|
+
const mv = springValue(source as MotionValue<number>, options)
|
|
214
|
+
onCleanup(() => mv.destroy())
|
|
215
|
+
return makeAccessor(mv)
|
|
216
|
+
}
|
|
217
|
+
// Accessor input — bridge through an intermediate MotionValue that mirrors it.
|
|
218
|
+
const bridge = motionValue((source as Accessor<number>)())
|
|
219
|
+
onCleanup(() => bridge.destroy())
|
|
220
|
+
createComputed(() => bridge.set((source as Accessor<number>)()))
|
|
221
|
+
const mv = springValue(bridge, options)
|
|
222
|
+
onCleanup(() => mv.destroy())
|
|
223
|
+
return makeAccessor(mv)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
// createTime — MotionValueAccessor that advances each frame with elapsed ms.
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* {@link MotionValueAccessor} that advances every animation frame, holding
|
|
232
|
+
* the milliseconds elapsed since this primitive was called. Driver for
|
|
233
|
+
* time-based animations and {@link createTransform}-derived values.
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* const t = createTime()
|
|
237
|
+
* const wobble = createTransform(t, [0, 1000, 2000], [0, 10, 0])
|
|
238
|
+
*/
|
|
239
|
+
export function createTime(): MotionValueAccessor<number> {
|
|
240
|
+
const mv = motionValue(0)
|
|
241
|
+
onCleanup(() => mv.destroy())
|
|
242
|
+
const startedAt = performance.now()
|
|
243
|
+
const tick = () => mv.set(performance.now() - startedAt)
|
|
244
|
+
frame.update(tick, true)
|
|
245
|
+
onCleanup(() => cancelFrame(tick))
|
|
246
|
+
return makeAccessor(mv)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
// createVelocity — MotionValueAccessor mirroring an input's instantaneous velocity.
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* {@link MotionValueAccessor} reporting the velocity of a source motion value.
|
|
255
|
+
* Updated whenever the source changes.
|
|
256
|
+
*
|
|
257
|
+
* @example
|
|
258
|
+
* const x = createMotionValue(0)
|
|
259
|
+
* const xVelocity = createVelocity(x)
|
|
260
|
+
*/
|
|
261
|
+
export function createVelocity(source: MotionValue<number>): MotionValueAccessor<number> {
|
|
262
|
+
const mv = motionValue(source.getVelocity())
|
|
263
|
+
onCleanup(() => mv.destroy())
|
|
264
|
+
onCleanup(source.on("change", () => mv.set(source.getVelocity())))
|
|
265
|
+
return makeAccessor(mv)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
// createTemplate — tagged template producing a MotionValueAccessor<string>.
|
|
270
|
+
// ---------------------------------------------------------------------------
|
|
271
|
+
|
|
272
|
+
// biome-ignore lint/suspicious/noExplicitAny: MotionValue is invariant in T; `any` lets the template accept MotionValues of any value type.
|
|
273
|
+
type TemplateInput = MotionValue<any> | Accessor<unknown> | string | number
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Tagged template producing a {@link MotionValueAccessor}\<string\>.
|
|
277
|
+
* Interpolated {@link MotionValue}s, hybrids, and Solid Accessors recompute
|
|
278
|
+
* the output string on change; primitives and static strings are baked in.
|
|
279
|
+
*
|
|
280
|
+
* @example
|
|
281
|
+
* const x = createMotionValue(0)
|
|
282
|
+
* const y = createMotionValue(0)
|
|
283
|
+
* const transform = createTemplate`translate(${x}px, ${y}px) scale(1.1)`
|
|
284
|
+
* <div style={{ transform: transform() }} />
|
|
285
|
+
*/
|
|
286
|
+
export function createTemplate(
|
|
287
|
+
strings: TemplateStringsArray,
|
|
288
|
+
...values: TemplateInput[]
|
|
289
|
+
): MotionValueAccessor<string> {
|
|
290
|
+
const compute = (): string => {
|
|
291
|
+
let out = ""
|
|
292
|
+
for (let i = 0; i < strings.length; i++) {
|
|
293
|
+
out += strings[i]
|
|
294
|
+
if (i < values.length) {
|
|
295
|
+
const v = values[i]
|
|
296
|
+
if (isMotionValue(v)) {
|
|
297
|
+
out += String((v as MotionValue<unknown>).get())
|
|
298
|
+
} else if (typeof v === "function") {
|
|
299
|
+
out += String((v as Accessor<unknown>)())
|
|
300
|
+
} else {
|
|
301
|
+
out += String(v)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return out
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const mv = motionValue(compute())
|
|
309
|
+
onCleanup(() => mv.destroy())
|
|
310
|
+
|
|
311
|
+
for (const v of values) {
|
|
312
|
+
if (isMotionValue(v)) {
|
|
313
|
+
onCleanup((v as MotionValue<unknown>).on("change", () => mv.set(compute())))
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const hasAccessor = values.some((v) => typeof v === "function" && !isMotionValue(v))
|
|
318
|
+
if (hasAccessor) {
|
|
319
|
+
createComputed(() => {
|
|
320
|
+
for (const v of values) {
|
|
321
|
+
if (typeof v === "function" && !isMotionValue(v)) (v as Accessor<unknown>)()
|
|
322
|
+
}
|
|
323
|
+
mv.set(compute())
|
|
324
|
+
})
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return makeAccessor(mv)
|
|
328
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { type MotionValue, motionValue } from "motion"
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Per-element value registry.
|
|
5
|
+
//
|
|
6
|
+
// One `ValueRegistry` per element managed by `createMotion`. It maps style /
|
|
7
|
+
// transform-shortcut keys (`scale`, `y`, `opacity`, etc.) to the
|
|
8
|
+
// `MotionValue` that authoritatively drives the corresponding CSS or
|
|
9
|
+
// transform component for that element.
|
|
10
|
+
//
|
|
11
|
+
// This is the motion-react `visualElement.values` shape, slimmed down. It
|
|
12
|
+
// exists to unify two write paths that currently disagree about who owns a
|
|
13
|
+
// CSS key on the element:
|
|
14
|
+
//
|
|
15
|
+
// 1. **User-provided MVs in `style`** — `<motion.div style={{ scale: mv }}>`.
|
|
16
|
+
// mv is the source of truth for `scale`; subscribing to it and writing
|
|
17
|
+
// `el.style.transform` is the only way the new value reaches the DOM.
|
|
18
|
+
// 2. **animate-target writes** — `useMotion({ animate: { scale: 1.5 } })`.
|
|
19
|
+
// Today these go directly via WAA (`animate(el, target, opts)`). After
|
|
20
|
+
// Stage 3 they will be routed through the registry: if a key has a
|
|
21
|
+
// registered MV, the animation tweens that MV (which writes the DOM
|
|
22
|
+
// via its subscription) instead of writing the element directly.
|
|
23
|
+
//
|
|
24
|
+
// Stage 1 introduces only the data structure. No code reads from or writes
|
|
25
|
+
// to it yet — `createMotion` instantiates an empty registry, attaches a
|
|
26
|
+
// disposal hook, and stops there. Subsequent stages wire it up.
|
|
27
|
+
//
|
|
28
|
+
// Two ownership classes:
|
|
29
|
+
//
|
|
30
|
+
// - **External** — MVs the user created via `createMotionValue` / motion's
|
|
31
|
+
// `motionValue()` and handed us via `style`. The registry tracks them so
|
|
32
|
+
// we know "this key is MV-backed" but does NOT dispose them. The user
|
|
33
|
+
// owns their MV's lifetime.
|
|
34
|
+
//
|
|
35
|
+
// - **Transient** — MVs the registry creates internally (Stage 3) because
|
|
36
|
+
// an animate target referenced a key with no existing MV. The registry
|
|
37
|
+
// owns these and clears them on `dispose()`. motion's MotionValue has no
|
|
38
|
+
// imperative teardown method; releasing references is what lets GC
|
|
39
|
+
// collect them once all subscribers have cleaned up.
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
export type ValueRegistry = {
|
|
43
|
+
/** Returns the MV registered for `key`, or `undefined` if none. */
|
|
44
|
+
get(key: string): MotionValue<unknown> | undefined
|
|
45
|
+
/** Has any MV been registered for `key`? */
|
|
46
|
+
has(key: string): boolean
|
|
47
|
+
/**
|
|
48
|
+
* Number of entries currently registered. Used by `createMotion` to decide
|
|
49
|
+
* whether the per-element writer can take a specialized single-key path
|
|
50
|
+
* (size === 1) or needs the general-purpose `applyStaticStyle` walk.
|
|
51
|
+
*/
|
|
52
|
+
readonly size: number
|
|
53
|
+
/**
|
|
54
|
+
* Register a user-provided MV. The registry will NOT dispose it on
|
|
55
|
+
* teardown. If a transient MV exists for the key, it is replaced (the
|
|
56
|
+
* external MV becomes the new source of truth).
|
|
57
|
+
*/
|
|
58
|
+
setExternal(key: string, mv: MotionValue<unknown>): void
|
|
59
|
+
/**
|
|
60
|
+
* Get the MV for `key`, creating a transient one initialized to
|
|
61
|
+
* `fallback` if absent. Transient MVs are disposed on `dispose()`.
|
|
62
|
+
*/
|
|
63
|
+
getOrCreateTransient(key: string, fallback: unknown): MotionValue<unknown>
|
|
64
|
+
/** Iterate every (key, MV) pair currently registered. */
|
|
65
|
+
entries(): IterableIterator<[string, MotionValue<unknown>]>
|
|
66
|
+
/**
|
|
67
|
+
* Drop registry-owned (transient) MVs. External MVs are untouched.
|
|
68
|
+
* Subscription cleanups are owned by whoever called `mv.on(...)`; they
|
|
69
|
+
* tie to the surrounding Solid owner via `onCleanup` in Stage 2+ so we
|
|
70
|
+
* don't unsubscribe imperatively here.
|
|
71
|
+
*/
|
|
72
|
+
dispose(): void
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function createValueRegistry(): ValueRegistry {
|
|
76
|
+
const values = new Map<string, MotionValue<unknown>>()
|
|
77
|
+
const transient = new Set<MotionValue<unknown>>()
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
get(key) {
|
|
81
|
+
return values.get(key)
|
|
82
|
+
},
|
|
83
|
+
has(key) {
|
|
84
|
+
return values.has(key)
|
|
85
|
+
},
|
|
86
|
+
setExternal(key, mv) {
|
|
87
|
+
const existing = values.get(key)
|
|
88
|
+
if (existing && transient.has(existing)) {
|
|
89
|
+
// Replacing a transient with an external takes the transient out
|
|
90
|
+
// of the owned-set so dispose() doesn't pretend to manage it.
|
|
91
|
+
transient.delete(existing)
|
|
92
|
+
}
|
|
93
|
+
values.set(key, mv)
|
|
94
|
+
},
|
|
95
|
+
getOrCreateTransient(key, fallback) {
|
|
96
|
+
const existing = values.get(key)
|
|
97
|
+
if (existing) return existing
|
|
98
|
+
const mv = motionValue(fallback) as MotionValue<unknown>
|
|
99
|
+
values.set(key, mv)
|
|
100
|
+
transient.add(mv)
|
|
101
|
+
return mv
|
|
102
|
+
},
|
|
103
|
+
entries() {
|
|
104
|
+
return values.entries()
|
|
105
|
+
},
|
|
106
|
+
get size() {
|
|
107
|
+
return values.size
|
|
108
|
+
},
|
|
109
|
+
dispose() {
|
|
110
|
+
transient.clear()
|
|
111
|
+
values.clear()
|
|
112
|
+
},
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type Accessor, from } from "solid-js"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A reactive `Accessor<boolean>` tracking the user's
|
|
5
|
+
* `prefers-reduced-motion: reduce` media query.
|
|
6
|
+
*
|
|
7
|
+
* Returns `false` server-side (no `window.matchMedia`). On the client, it seeds
|
|
8
|
+
* with the current match state and updates as the system preference toggles.
|
|
9
|
+
* The matchMedia listener is removed automatically on owner disposal via
|
|
10
|
+
* `from`'s teardown callback.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* const reduced = createReducedMotion()
|
|
14
|
+
* createEffect(() => {
|
|
15
|
+
* if (reduced()) console.log("user prefers reduced motion")
|
|
16
|
+
* })
|
|
17
|
+
*/
|
|
18
|
+
export function createReducedMotion(): Accessor<boolean> {
|
|
19
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
20
|
+
return () => false
|
|
21
|
+
}
|
|
22
|
+
const mql = window.matchMedia("(prefers-reduced-motion: reduce)")
|
|
23
|
+
// `from` produces an Accessor<T | undefined>; we seed synchronously inside the
|
|
24
|
+
// producer so the cast to Accessor<boolean> is sound.
|
|
25
|
+
return from<boolean>((set) => {
|
|
26
|
+
set(mql.matches)
|
|
27
|
+
const handler = (e: MediaQueryListEvent) => set(e.matches)
|
|
28
|
+
mql.addEventListener("change", handler)
|
|
29
|
+
return () => mql.removeEventListener("change", handler)
|
|
30
|
+
}) as Accessor<boolean>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Compute the effective reduced-motion state by combining a {@link MotionConfig}
|
|
35
|
+
* `reducedMotion` setting with the system preference.
|
|
36
|
+
*
|
|
37
|
+
* - `"always"` — forced reduced, regardless of system pref
|
|
38
|
+
* - `"never"` — never reduced, regardless of system pref
|
|
39
|
+
* - `"user"` — respect system pref
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* const reduced = shouldReduceMotion("user", createReducedMotion()())
|
|
43
|
+
*/
|
|
44
|
+
export function shouldReduceMotion(
|
|
45
|
+
configValue: "always" | "never" | "user",
|
|
46
|
+
systemReduced: boolean,
|
|
47
|
+
): boolean {
|
|
48
|
+
if (configValue === "always") return true
|
|
49
|
+
if (configValue === "never") return false
|
|
50
|
+
return systemReduced
|
|
51
|
+
}
|