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
package/dist/index.js
ADDED
|
@@ -0,0 +1,2627 @@
|
|
|
1
|
+
import { animate, animate as animate$1, cancelFrame, frame, inView, isMotionValue, isMotionValue as isMotionValue$1, motionValue, motionValue as motionValue$1, scroll, scroll as scroll$1, spring, springValue, transform } from "motion";
|
|
2
|
+
import { Dynamic, createComponent, mergeProps } from "solid-js/web";
|
|
3
|
+
import { Match, Switch, createComputed, createContext, createEffect, createMemo, createSignal, from, mergeProps as mergeProps$1, onCleanup, onMount, splitProps, untrack, useContext } from "solid-js";
|
|
4
|
+
import { mergeRefs, resolveElements } from "@solid-primitives/refs";
|
|
5
|
+
import { createStore } from "solid-js/store";
|
|
6
|
+
import { HTMLVisualElement, addDomEvent, hover, isPrimaryPointer, press, time, visualElementStore } from "motion-dom";
|
|
7
|
+
import { createListTransition, createSwitchTransition } from "@solid-primitives/transition-group";
|
|
8
|
+
var MotionConfigContext = createContext({
|
|
9
|
+
reducedMotion: () => "never",
|
|
10
|
+
transition: () => void 0,
|
|
11
|
+
nonce: () => void 0
|
|
12
|
+
});
|
|
13
|
+
function useMotionConfig() {
|
|
14
|
+
return useContext(MotionConfigContext);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Provider that flows reduced-motion mode, default transition, and CSP nonce
|
|
18
|
+
* to every descendant motion element.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* <MotionConfig reducedMotion="user" transition={{ duration: 0.4 }}>
|
|
22
|
+
* <App />
|
|
23
|
+
* </MotionConfig>
|
|
24
|
+
*/
|
|
25
|
+
function MotionConfig(props) {
|
|
26
|
+
const value = {
|
|
27
|
+
reducedMotion: createMemo(() => props.reducedMotion ?? "never"),
|
|
28
|
+
transition: createMemo(() => props.transition),
|
|
29
|
+
nonce: createMemo(() => props.nonce)
|
|
30
|
+
};
|
|
31
|
+
return createComponent(MotionConfigContext.Provider, {
|
|
32
|
+
value,
|
|
33
|
+
get children() {
|
|
34
|
+
return props.children;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
var PresenceContext = createContext({
|
|
39
|
+
register: () => {},
|
|
40
|
+
unregister: () => {},
|
|
41
|
+
beforeUnmount: () => Promise.resolve()
|
|
42
|
+
});
|
|
43
|
+
function usePresenceContext() {
|
|
44
|
+
return useContext(PresenceContext);
|
|
45
|
+
}
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/reduced-motion.ts
|
|
48
|
+
/**
|
|
49
|
+
* A reactive `Accessor<boolean>` tracking the user's
|
|
50
|
+
* `prefers-reduced-motion: reduce` media query.
|
|
51
|
+
*
|
|
52
|
+
* Returns `false` server-side (no `window.matchMedia`). On the client, it seeds
|
|
53
|
+
* with the current match state and updates as the system preference toggles.
|
|
54
|
+
* The matchMedia listener is removed automatically on owner disposal via
|
|
55
|
+
* `from`'s teardown callback.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* const reduced = createReducedMotion()
|
|
59
|
+
* createEffect(() => {
|
|
60
|
+
* if (reduced()) console.log("user prefers reduced motion")
|
|
61
|
+
* })
|
|
62
|
+
*/
|
|
63
|
+
function createReducedMotion() {
|
|
64
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return () => false;
|
|
65
|
+
const mql = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
66
|
+
return from((set) => {
|
|
67
|
+
set(mql.matches);
|
|
68
|
+
const handler = (e) => set(e.matches);
|
|
69
|
+
mql.addEventListener("change", handler);
|
|
70
|
+
return () => mql.removeEventListener("change", handler);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Compute the effective reduced-motion state by combining a {@link MotionConfig}
|
|
75
|
+
* `reducedMotion` setting with the system preference.
|
|
76
|
+
*
|
|
77
|
+
* - `"always"` — forced reduced, regardless of system pref
|
|
78
|
+
* - `"never"` — never reduced, regardless of system pref
|
|
79
|
+
* - `"user"` — respect system pref
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* const reduced = shouldReduceMotion("user", createReducedMotion()())
|
|
83
|
+
*/
|
|
84
|
+
function shouldReduceMotion(configValue, systemReduced) {
|
|
85
|
+
if (configValue === "always") return true;
|
|
86
|
+
if (configValue === "never") return false;
|
|
87
|
+
return systemReduced;
|
|
88
|
+
}
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region src/style.ts
|
|
91
|
+
/**
|
|
92
|
+
* Set of CSS shortcut keys motion treats as transform components. Re-used by
|
|
93
|
+
* `createMotion`'s Stage 3 animate bridge to decide whether an animate-target
|
|
94
|
+
* key should be routed through the value registry (composed via the writer's
|
|
95
|
+
* `el.style.transform =`) or sent down the existing WAA path.
|
|
96
|
+
*/
|
|
97
|
+
var TRANSFORM_KEYS = /* @__PURE__ */ new Set([
|
|
98
|
+
"x",
|
|
99
|
+
"y",
|
|
100
|
+
"z",
|
|
101
|
+
"scale",
|
|
102
|
+
"scaleX",
|
|
103
|
+
"scaleY",
|
|
104
|
+
"scaleZ",
|
|
105
|
+
"rotate",
|
|
106
|
+
"rotateX",
|
|
107
|
+
"rotateY",
|
|
108
|
+
"rotateZ",
|
|
109
|
+
"skew",
|
|
110
|
+
"skewX",
|
|
111
|
+
"skewY",
|
|
112
|
+
"transformPerspective"
|
|
113
|
+
]);
|
|
114
|
+
/** Order matters — motion composes transforms in this sequence (Q5 sub-1). */
|
|
115
|
+
var TRANSFORM_ORDER = [
|
|
116
|
+
"x",
|
|
117
|
+
"y",
|
|
118
|
+
"z",
|
|
119
|
+
"scale",
|
|
120
|
+
"scaleX",
|
|
121
|
+
"scaleY",
|
|
122
|
+
"scaleZ",
|
|
123
|
+
"rotate",
|
|
124
|
+
"rotateX",
|
|
125
|
+
"rotateY",
|
|
126
|
+
"rotateZ",
|
|
127
|
+
"skew",
|
|
128
|
+
"skewX",
|
|
129
|
+
"skewY",
|
|
130
|
+
"transformPerspective"
|
|
131
|
+
];
|
|
132
|
+
var PX_PROPERTIES = /* @__PURE__ */ new Set([
|
|
133
|
+
"width",
|
|
134
|
+
"minWidth",
|
|
135
|
+
"maxWidth",
|
|
136
|
+
"height",
|
|
137
|
+
"minHeight",
|
|
138
|
+
"maxHeight",
|
|
139
|
+
"top",
|
|
140
|
+
"right",
|
|
141
|
+
"bottom",
|
|
142
|
+
"left",
|
|
143
|
+
"padding",
|
|
144
|
+
"paddingTop",
|
|
145
|
+
"paddingRight",
|
|
146
|
+
"paddingBottom",
|
|
147
|
+
"paddingLeft",
|
|
148
|
+
"paddingInline",
|
|
149
|
+
"paddingBlock",
|
|
150
|
+
"paddingInlineStart",
|
|
151
|
+
"paddingInlineEnd",
|
|
152
|
+
"paddingBlockStart",
|
|
153
|
+
"paddingBlockEnd",
|
|
154
|
+
"margin",
|
|
155
|
+
"marginTop",
|
|
156
|
+
"marginRight",
|
|
157
|
+
"marginBottom",
|
|
158
|
+
"marginLeft",
|
|
159
|
+
"marginInline",
|
|
160
|
+
"marginBlock",
|
|
161
|
+
"marginInlineStart",
|
|
162
|
+
"marginInlineEnd",
|
|
163
|
+
"marginBlockStart",
|
|
164
|
+
"marginBlockEnd",
|
|
165
|
+
"borderWidth",
|
|
166
|
+
"borderTopWidth",
|
|
167
|
+
"borderRightWidth",
|
|
168
|
+
"borderBottomWidth",
|
|
169
|
+
"borderLeftWidth",
|
|
170
|
+
"borderRadius",
|
|
171
|
+
"borderTopLeftRadius",
|
|
172
|
+
"borderTopRightRadius",
|
|
173
|
+
"borderBottomLeftRadius",
|
|
174
|
+
"borderBottomRightRadius",
|
|
175
|
+
"gap",
|
|
176
|
+
"rowGap",
|
|
177
|
+
"columnGap",
|
|
178
|
+
"fontSize",
|
|
179
|
+
"outlineWidth",
|
|
180
|
+
"outlineOffset"
|
|
181
|
+
]);
|
|
182
|
+
/**
|
|
183
|
+
* Reduce a target-value (which may be raw, a MotionValue, an Accessor, or a
|
|
184
|
+
* keyframe array) to a concrete leaf value the writer can apply to the DOM
|
|
185
|
+
* or use to initialize a transient MotionValue. The cascade follows motion's
|
|
186
|
+
* own semantics:
|
|
187
|
+
*
|
|
188
|
+
* - `null` / `undefined` → `undefined` (caller drops the key)
|
|
189
|
+
* - keyframe array → first frame (consistent with motion-vanilla's
|
|
190
|
+
* initial-style snapshot)
|
|
191
|
+
* - `MotionValue` → its current `.get()`
|
|
192
|
+
* - `Accessor` (a bare zero-arg function) → its invocation result
|
|
193
|
+
* - primitive (string / number) → returned as-is
|
|
194
|
+
*
|
|
195
|
+
* Exported for the MV-in-style Stage 4 work: createMotion uses it to
|
|
196
|
+
* snapshot initial-target entries when registering them into the value
|
|
197
|
+
* registry as transient MVs.
|
|
198
|
+
*/
|
|
199
|
+
function snapshotValue(value) {
|
|
200
|
+
if (value === null || value === void 0) return void 0;
|
|
201
|
+
if (Array.isArray(value)) return snapshotValue(value[0]);
|
|
202
|
+
if (isMotionValue$1(value)) return snapshotValue(value.get());
|
|
203
|
+
if (typeof value === "function") return snapshotValue(value());
|
|
204
|
+
if (typeof value === "string" || typeof value === "number") return value;
|
|
205
|
+
}
|
|
206
|
+
var TRANSFORM_FORMATTERS = {
|
|
207
|
+
x: (v) => `translateX(${typeof v === "string" ? v : `${v}px`})`,
|
|
208
|
+
y: (v) => `translateY(${typeof v === "string" ? v : `${v}px`})`,
|
|
209
|
+
z: (v) => `translateZ(${typeof v === "string" ? v : `${v}px`})`,
|
|
210
|
+
scale: (v) => `scale(${v})`,
|
|
211
|
+
scaleX: (v) => `scaleX(${v})`,
|
|
212
|
+
scaleY: (v) => `scaleY(${v})`,
|
|
213
|
+
scaleZ: (v) => `scaleZ(${v})`,
|
|
214
|
+
rotate: (v) => `rotate(${typeof v === "string" ? v : `${v}deg`})`,
|
|
215
|
+
rotateX: (v) => `rotateX(${typeof v === "string" ? v : `${v}deg`})`,
|
|
216
|
+
rotateY: (v) => `rotateY(${typeof v === "string" ? v : `${v}deg`})`,
|
|
217
|
+
rotateZ: (v) => `rotateZ(${typeof v === "string" ? v : `${v}deg`})`,
|
|
218
|
+
skew: (v) => `skew(${typeof v === "string" ? v : `${v}deg`})`,
|
|
219
|
+
skewX: (v) => `skewX(${typeof v === "string" ? v : `${v}deg`})`,
|
|
220
|
+
skewY: (v) => `skewY(${typeof v === "string" ? v : `${v}deg`})`,
|
|
221
|
+
transformPerspective: (v) => `perspective(${typeof v === "string" ? v : `${v}px`})`
|
|
222
|
+
};
|
|
223
|
+
/**
|
|
224
|
+
* Look up the formatter for a transform-shortcut key. Returns `undefined`
|
|
225
|
+
* for non-transform keys; callers should check `TRANSFORM_KEYS.has(key)`
|
|
226
|
+
* before assuming a formatter exists.
|
|
227
|
+
*/
|
|
228
|
+
function pickTransformFormatter(key) {
|
|
229
|
+
return TRANSFORM_FORMATTERS[key];
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Format a motion transform-shortcut key + value as the corresponding CSS
|
|
233
|
+
* transform function string (e.g. `transformFunctionFor("scale", 1.05)`
|
|
234
|
+
* → `"scale(1.05)"`). One-shot variant — for hot paths, use
|
|
235
|
+
* `pickTransformFormatter(key)` once at compile time and reuse.
|
|
236
|
+
*/
|
|
237
|
+
function transformFunctionFor(key, value) {
|
|
238
|
+
return TRANSFORM_FORMATTERS[key]?.(value) ?? "";
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Format a non-transform CSS property's value (e.g. `formatProperty("width", 100)`
|
|
242
|
+
* → `"100px"`, `formatProperty("opacity", 0.5)` → `0.5`). Applies motion's
|
|
243
|
+
* default-unit table (PX for dimensional CSS, dimensionless otherwise);
|
|
244
|
+
* leaves CSS variables alone. Exported for `createMotion`'s writer fast path.
|
|
245
|
+
*/
|
|
246
|
+
function formatProperty(key, value) {
|
|
247
|
+
if (typeof value === "string") return value;
|
|
248
|
+
if (key.startsWith("--")) return String(value);
|
|
249
|
+
if (PX_PROPERTIES.has(key)) return `${value}px`;
|
|
250
|
+
return value;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Convert a {@link Target} to a Solid {@link JSX.CSSProperties} object.
|
|
254
|
+
*
|
|
255
|
+
* - Composes transform shorthand (`x`, `y`, `scale`, `rotate`, etc.) into a
|
|
256
|
+
* single `transform: "..."` string in motion's canonical order.
|
|
257
|
+
* - Applies the default-unit table (px for dimensional CSS, deg for rotate/
|
|
258
|
+
* skew, dimensionless for scale/opacity/etc.).
|
|
259
|
+
* - For keyframe arrays, uses the first frame; a leading `null`/`undefined`
|
|
260
|
+
* keyframe omits the property entirely.
|
|
261
|
+
* - MotionValues and Solid Accessors are snapshotted at call time. Callers
|
|
262
|
+
* wrap in `untrack` if they don't want the read to subscribe.
|
|
263
|
+
* - Skips the `transition` key (animation config, not style).
|
|
264
|
+
* - CSS variables (`--foo`) emit raw values, no unit guess.
|
|
265
|
+
*
|
|
266
|
+
* @example
|
|
267
|
+
* targetToStyle({ x: 100, scale: 0.9, opacity: 0.5 })
|
|
268
|
+
* // { transform: "translateX(100px) scale(0.9)", opacity: 0.5 }
|
|
269
|
+
*/
|
|
270
|
+
function targetToStyle(target) {
|
|
271
|
+
const out = {};
|
|
272
|
+
const transforms = {};
|
|
273
|
+
for (const key in target) {
|
|
274
|
+
if (key === "transition") continue;
|
|
275
|
+
const raw = target[key];
|
|
276
|
+
const snapshot = snapshotValue(raw);
|
|
277
|
+
if (snapshot === void 0) continue;
|
|
278
|
+
if (TRANSFORM_KEYS.has(key)) transforms[key] = snapshot;
|
|
279
|
+
else out[key] = formatProperty(key, snapshot);
|
|
280
|
+
}
|
|
281
|
+
const parts = [];
|
|
282
|
+
for (const key of TRANSFORM_ORDER) if (key in transforms) parts.push(transformFunctionFor(key, transforms[key]));
|
|
283
|
+
if (parts.length > 0) out.transform = parts.join(" ");
|
|
284
|
+
return out;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Solid context propagating variant state from a motion ancestor to its
|
|
288
|
+
* descendants. Only the wrapper components (`<motion.div>`, `motion(...)`)
|
|
289
|
+
* provide a value. Bare `useMotion` consumers can opt in via the `Provider`
|
|
290
|
+
* returned alongside the getter.
|
|
291
|
+
*/
|
|
292
|
+
var VariantContext = createContext({});
|
|
293
|
+
function useVariantContext() {
|
|
294
|
+
return useContext(VariantContext);
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Resolve a variant label (or array of labels) against a variants map and the
|
|
298
|
+
* current `custom` value. Returns a {@link Target} object, or `null` if nothing
|
|
299
|
+
* resolves.
|
|
300
|
+
*
|
|
301
|
+
* Resolution rules locked in Phase 1 Q4:
|
|
302
|
+
*
|
|
303
|
+
* - Child's own `variants` always wins for a given name (sub-1A).
|
|
304
|
+
* - No cascade: if a child has no `variants` of its own, parent's are NOT
|
|
305
|
+
* consulted (sub-1B / Pattern X). Callers pass `variants = undefined` in
|
|
306
|
+
* that case and this returns `null`.
|
|
307
|
+
* - String + array forms both supported; array variants merge in order
|
|
308
|
+
* (later wins on conflicting keys).
|
|
309
|
+
* - Function variants are invoked with `custom`; the value can be any type.
|
|
310
|
+
*
|
|
311
|
+
* @example
|
|
312
|
+
* const variants = { visible: { opacity: 1 }, hidden: { opacity: 0 } }
|
|
313
|
+
* resolveVariant("visible", variants, undefined)
|
|
314
|
+
* // { opacity: 1 }
|
|
315
|
+
*
|
|
316
|
+
* resolveVariant(["visible", "highlighted"], variants, undefined)
|
|
317
|
+
* // merged in order, last variant's keys override
|
|
318
|
+
*
|
|
319
|
+
* resolveVariant("visible", { visible: (i: number) => ({ x: i * 10 }) }, 3)
|
|
320
|
+
* // { x: 30 }
|
|
321
|
+
*/
|
|
322
|
+
function resolveVariant(names, variants, custom) {
|
|
323
|
+
if (!names || !variants) return null;
|
|
324
|
+
const list = Array.isArray(names) ? names : [names];
|
|
325
|
+
let merged = null;
|
|
326
|
+
for (const name of list) {
|
|
327
|
+
const variant = variants[name];
|
|
328
|
+
if (!variant) continue;
|
|
329
|
+
const resolved = typeof variant === "function" ? variant(custom) : variant;
|
|
330
|
+
merged = merged ? Object.assign({}, merged, resolved) : Object.assign({}, resolved);
|
|
331
|
+
}
|
|
332
|
+
return merged;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Determine the effective variant name for a given motion state. If the caller
|
|
336
|
+
* provided an explicit value (string, array, or Target object), that wins.
|
|
337
|
+
* Otherwise the parent context's value (per gesture/state) is used as a fall-
|
|
338
|
+
* back.
|
|
339
|
+
*
|
|
340
|
+
* Returns the explicit value as-is when it's a Target object (used by callers
|
|
341
|
+
* to detect "explicit target — skip variant lookup entirely").
|
|
342
|
+
*/
|
|
343
|
+
function effectiveLabels(own, parent) {
|
|
344
|
+
if (own !== void 0) return own;
|
|
345
|
+
return parent;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Determine whether an `AnimateValue` is a variant *label* (string or array
|
|
349
|
+
* of strings) — as opposed to a `Target` object or `false` / `undefined`.
|
|
350
|
+
*
|
|
351
|
+
* Mirrors motion-dom's `isVariantLabel`. Used by `isControllingVariants`
|
|
352
|
+
* to decide whether a prop opts the node into the "controlling" role.
|
|
353
|
+
*/
|
|
354
|
+
function isVariantLabelValue(v) {
|
|
355
|
+
if (typeof v === "string") return true;
|
|
356
|
+
if (Array.isArray(v)) return true;
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* A motion node is "controlling variants" when any of its variant slots
|
|
361
|
+
* (`initial`, `animate`, `hover`, `press`, `focus`, `inView`, `exit`) carries
|
|
362
|
+
* a variant *label* (string or array of strings).
|
|
363
|
+
*
|
|
364
|
+
* Mirrors motion-dom's same-named check. A controlling node opts OUT of
|
|
365
|
+
* inheriting its parent's variant cascade — it provides its own. Descendants
|
|
366
|
+
* with no controlling props of their own DO inherit from the nearest
|
|
367
|
+
* controlling ancestor.
|
|
368
|
+
*
|
|
369
|
+
* Behavior is binary (any single controlling-slot label flips the node into
|
|
370
|
+
* controlling mode); not per-slot.
|
|
371
|
+
*
|
|
372
|
+
* Object-shaped values (`animate: \{ x: 100 \}`) do NOT make a node
|
|
373
|
+
* controlling — they're treated as targets, not as variant references.
|
|
374
|
+
*/
|
|
375
|
+
function isControllingVariants(opts) {
|
|
376
|
+
return isVariantLabelValue(opts.initial) || isVariantLabelValue(opts.animate) || isVariantLabelValue(opts.hover) || isVariantLabelValue(opts.press) || isVariantLabelValue(opts.focus) || isVariantLabelValue(opts.inView) || isVariantLabelValue(opts.exit);
|
|
377
|
+
}
|
|
378
|
+
//#endregion
|
|
379
|
+
//#region src/primitives/createDragControls.ts
|
|
380
|
+
/**
|
|
381
|
+
* Symbol used to attach the registration method on the controls object.
|
|
382
|
+
* `createDrag` imports this symbol to find/register its handler. Userland
|
|
383
|
+
* code never touches it — exported only for library-internal use.
|
|
384
|
+
*/
|
|
385
|
+
var DRAG_CONTROLS_REGISTER = Symbol("solidjs-motion.dragControls.register");
|
|
386
|
+
/**
|
|
387
|
+
* Create a controls instance for imperatively starting a drag on a motion
|
|
388
|
+
* element from a different element (e.g., a drag-handle button).
|
|
389
|
+
*
|
|
390
|
+
* Pattern (Q9):
|
|
391
|
+
*
|
|
392
|
+
* @example
|
|
393
|
+
* function Card() {
|
|
394
|
+
* const controls = createDragControls()
|
|
395
|
+
* const m = useMotion({ drag: "y", dragControls: controls })
|
|
396
|
+
* return (
|
|
397
|
+
* <div {...m()}>
|
|
398
|
+
* <button onPointerDown={(e) => controls.start(e)}>handle</button>
|
|
399
|
+
* Card body
|
|
400
|
+
* </div>
|
|
401
|
+
* )
|
|
402
|
+
* }
|
|
403
|
+
*
|
|
404
|
+
* The handle's pointerdown fires `controls.start(event)`. createDrag is
|
|
405
|
+
* registered with the controls and translates the call into a pan-session
|
|
406
|
+
* synthesized from the handle's event, bypassing the threshold gate.
|
|
407
|
+
*/
|
|
408
|
+
function createDragControls() {
|
|
409
|
+
let handler = null;
|
|
410
|
+
const controls = {
|
|
411
|
+
start(event, options = {}) {
|
|
412
|
+
handler?.(event, options);
|
|
413
|
+
},
|
|
414
|
+
[DRAG_CONTROLS_REGISTER]: void 0
|
|
415
|
+
};
|
|
416
|
+
Object.defineProperty(controls, DRAG_CONTROLS_REGISTER, {
|
|
417
|
+
value: ((newHandler) => {
|
|
418
|
+
handler = newHandler;
|
|
419
|
+
return () => {
|
|
420
|
+
if (handler === newHandler) handler = null;
|
|
421
|
+
};
|
|
422
|
+
}),
|
|
423
|
+
enumerable: false,
|
|
424
|
+
writable: false,
|
|
425
|
+
configurable: false
|
|
426
|
+
});
|
|
427
|
+
return controls;
|
|
428
|
+
}
|
|
429
|
+
//#endregion
|
|
430
|
+
//#region src/primitives/motion-value.ts
|
|
431
|
+
function makeAccessor(mv) {
|
|
432
|
+
const [signal, setSignal] = createSignal(mv.get());
|
|
433
|
+
onCleanup(mv.on("change", (v) => setSignal(() => v)));
|
|
434
|
+
const fn = (() => signal());
|
|
435
|
+
return new Proxy(fn, {
|
|
436
|
+
get(target, prop, receiver) {
|
|
437
|
+
if (prop === "call" || prop === "apply" || prop === "bind") return Reflect.get(target, prop, receiver);
|
|
438
|
+
if (Reflect.has(target, prop)) return Reflect.get(target, prop, receiver);
|
|
439
|
+
const value = Reflect.get(mv, prop, mv);
|
|
440
|
+
return typeof value === "function" ? value.bind(mv) : value;
|
|
441
|
+
},
|
|
442
|
+
has(target, prop) {
|
|
443
|
+
return Reflect.has(target, prop) || prop in mv;
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Create a {@link MotionValueAccessor} bound to the current reactive scope.
|
|
449
|
+
*
|
|
450
|
+
* The returned value has two access patterns:
|
|
451
|
+
*
|
|
452
|
+
* - `mv()` — invoke as a Solid Accessor. Tracks in JSX, `createEffect`,
|
|
453
|
+
* `createMemo`, etc.
|
|
454
|
+
* - `mv.get()` / `mv.set(v)` / `mv.jump(v)` / `mv.on(...)` — the full upstream
|
|
455
|
+
* {@link MotionValue} surface. Matches motion/react idioms.
|
|
456
|
+
*
|
|
457
|
+
* The same value can be passed as a target in
|
|
458
|
+
* `useMotion({ animate: { x: mv } })` (motion engine sees `.getVelocity` via
|
|
459
|
+
* the Proxy and treats it as a motion value) or directly as the target of
|
|
460
|
+
* `animate(mv, 100)`.
|
|
461
|
+
*
|
|
462
|
+
* Auto-destroyed via `onCleanup` when the owner is disposed.
|
|
463
|
+
*
|
|
464
|
+
* @example
|
|
465
|
+
* const x = createMotionValue(0)
|
|
466
|
+
* x.set(100)
|
|
467
|
+
* animate(x, 200, { duration: 0.5 })
|
|
468
|
+
* <p>{x()}</p> // reactive read in JSX
|
|
469
|
+
*/
|
|
470
|
+
function createMotionValue(initial) {
|
|
471
|
+
const accessor = makeAccessor(motionValue$1(initial));
|
|
472
|
+
onCleanup(() => accessor.destroy());
|
|
473
|
+
return accessor;
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Bridge a raw {@link MotionValue} (from motion's `motionValue()` factory or
|
|
477
|
+
* any other motion API that doesn't return our hybrid) to a Solid
|
|
478
|
+
* {@link Accessor}. Seeds with the current value and updates on every
|
|
479
|
+
* `change` event.
|
|
480
|
+
*
|
|
481
|
+
* **You usually don't need this.** Values returned by `createMotionValue`,
|
|
482
|
+
* `createTransform`, `createSpring`, `createTime`, `createVelocity`, and
|
|
483
|
+
* `createTemplate` are already callable — you can do `mv()` directly. Reach
|
|
484
|
+
* for `toSignal` only when you receive a raw MotionValue from an external API.
|
|
485
|
+
*
|
|
486
|
+
* @example
|
|
487
|
+
* import { motionValue } from "motion"
|
|
488
|
+
* const rawMv = motionValue(0)
|
|
489
|
+
* const xSignal = toSignal(rawMv)
|
|
490
|
+
*/
|
|
491
|
+
function toSignal(mv) {
|
|
492
|
+
const [value, setValue] = createSignal(mv.get());
|
|
493
|
+
onCleanup(mv.on("change", (v) => setValue(() => v)));
|
|
494
|
+
return value;
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Subscribe to a {@link MotionValue} event with automatic cleanup. Convenience
|
|
498
|
+
* wrapper around `mv.on(event, cb)` for parity with motion/react's
|
|
499
|
+
* `useMotionValueEvent`. For per-change reactivity, prefer
|
|
500
|
+
* `createComputed(() => fn(mv()))` since hybrids are directly callable.
|
|
501
|
+
*
|
|
502
|
+
* @example
|
|
503
|
+
* const x = createMotionValue(0)
|
|
504
|
+
* createMotionValueEvent(x, "animationComplete", () => console.log("done"))
|
|
505
|
+
*/
|
|
506
|
+
function createMotionValueEvent(mv, event, callback) {
|
|
507
|
+
onCleanup(mv.on(event, callback));
|
|
508
|
+
}
|
|
509
|
+
function readInputValue(input) {
|
|
510
|
+
if (isMotionValue$1(input)) return input.get();
|
|
511
|
+
return input();
|
|
512
|
+
}
|
|
513
|
+
function subscribeInput(input, onChange) {
|
|
514
|
+
if (isMotionValue$1(input)) onCleanup(input.on("change", onChange));
|
|
515
|
+
else createComputed(() => onChange(input()));
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Create a {@link MotionValueAccessor} that maps an input through a range/
|
|
519
|
+
* output pair. Mirrors motion/react's `useTransform`. The input can be a
|
|
520
|
+
* MotionValue, our hybrid, or any Solid Accessor; the output composes with
|
|
521
|
+
* `animate()`, `useMotion`'s targets, and JSX reactivity.
|
|
522
|
+
*
|
|
523
|
+
* @example
|
|
524
|
+
* const { scrollY } = createScroll()
|
|
525
|
+
* const opacity = createTransform(scrollY, [0, 200], [1, 0])
|
|
526
|
+
* <div style={{ opacity: opacity() }}>...</div>
|
|
527
|
+
*/
|
|
528
|
+
function createTransform(input, inputRange, outputRange, options) {
|
|
529
|
+
const mapper = transform(inputRange, outputRange, options);
|
|
530
|
+
const mv = motionValue$1(mapper(readInputValue(input)));
|
|
531
|
+
onCleanup(() => mv.destroy());
|
|
532
|
+
subscribeInput(input, (v) => mv.set(mapper(v)));
|
|
533
|
+
return makeAccessor(mv);
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Spring-smoothed mirror of a numeric input. Returns a
|
|
537
|
+
* {@link MotionValueAccessor} that tracks the source with physics-based easing.
|
|
538
|
+
*
|
|
539
|
+
* @example
|
|
540
|
+
* const x = createMotionValue(0)
|
|
541
|
+
* const smoothX = createSpring(x, { stiffness: 100, damping: 20 })
|
|
542
|
+
*/
|
|
543
|
+
function createSpring(source, options) {
|
|
544
|
+
if (isMotionValue$1(source)) {
|
|
545
|
+
const mv = springValue(source, options);
|
|
546
|
+
onCleanup(() => mv.destroy());
|
|
547
|
+
return makeAccessor(mv);
|
|
548
|
+
}
|
|
549
|
+
const bridge = motionValue$1(source());
|
|
550
|
+
onCleanup(() => bridge.destroy());
|
|
551
|
+
createComputed(() => bridge.set(source()));
|
|
552
|
+
const mv = springValue(bridge, options);
|
|
553
|
+
onCleanup(() => mv.destroy());
|
|
554
|
+
return makeAccessor(mv);
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* {@link MotionValueAccessor} that advances every animation frame, holding
|
|
558
|
+
* the milliseconds elapsed since this primitive was called. Driver for
|
|
559
|
+
* time-based animations and {@link createTransform}-derived values.
|
|
560
|
+
*
|
|
561
|
+
* @example
|
|
562
|
+
* const t = createTime()
|
|
563
|
+
* const wobble = createTransform(t, [0, 1000, 2000], [0, 10, 0])
|
|
564
|
+
*/
|
|
565
|
+
function createTime() {
|
|
566
|
+
const mv = motionValue$1(0);
|
|
567
|
+
onCleanup(() => mv.destroy());
|
|
568
|
+
const startedAt = performance.now();
|
|
569
|
+
const tick = () => mv.set(performance.now() - startedAt);
|
|
570
|
+
frame.update(tick, true);
|
|
571
|
+
onCleanup(() => cancelFrame(tick));
|
|
572
|
+
return makeAccessor(mv);
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* {@link MotionValueAccessor} reporting the velocity of a source motion value.
|
|
576
|
+
* Updated whenever the source changes.
|
|
577
|
+
*
|
|
578
|
+
* @example
|
|
579
|
+
* const x = createMotionValue(0)
|
|
580
|
+
* const xVelocity = createVelocity(x)
|
|
581
|
+
*/
|
|
582
|
+
function createVelocity(source) {
|
|
583
|
+
const mv = motionValue$1(source.getVelocity());
|
|
584
|
+
onCleanup(() => mv.destroy());
|
|
585
|
+
onCleanup(source.on("change", () => mv.set(source.getVelocity())));
|
|
586
|
+
return makeAccessor(mv);
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Tagged template producing a {@link MotionValueAccessor}\<string\>.
|
|
590
|
+
* Interpolated {@link MotionValue}s, hybrids, and Solid Accessors recompute
|
|
591
|
+
* the output string on change; primitives and static strings are baked in.
|
|
592
|
+
*
|
|
593
|
+
* @example
|
|
594
|
+
* const x = createMotionValue(0)
|
|
595
|
+
* const y = createMotionValue(0)
|
|
596
|
+
* const transform = createTemplate`translate(${x}px, ${y}px) scale(1.1)`
|
|
597
|
+
* <div style={{ transform: transform() }} />
|
|
598
|
+
*/
|
|
599
|
+
function createTemplate(strings, ...values) {
|
|
600
|
+
const compute = () => {
|
|
601
|
+
let out = "";
|
|
602
|
+
for (let i = 0; i < strings.length; i++) {
|
|
603
|
+
out += strings[i];
|
|
604
|
+
if (i < values.length) {
|
|
605
|
+
const v = values[i];
|
|
606
|
+
if (isMotionValue$1(v)) out += String(v.get());
|
|
607
|
+
else if (typeof v === "function") out += String(v());
|
|
608
|
+
else out += String(v);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return out;
|
|
612
|
+
};
|
|
613
|
+
const mv = motionValue$1(compute());
|
|
614
|
+
onCleanup(() => mv.destroy());
|
|
615
|
+
for (const v of values) if (isMotionValue$1(v)) onCleanup(v.on("change", () => mv.set(compute())));
|
|
616
|
+
if (values.some((v) => typeof v === "function" && !isMotionValue$1(v))) createComputed(() => {
|
|
617
|
+
for (const v of values) if (typeof v === "function" && !isMotionValue$1(v)) v();
|
|
618
|
+
mv.set(compute());
|
|
619
|
+
});
|
|
620
|
+
return makeAccessor(mv);
|
|
621
|
+
}
|
|
622
|
+
//#endregion
|
|
623
|
+
//#region src/primitives/createPan.ts
|
|
624
|
+
/** Sliding-window width for velocity computation (Q15a). */
|
|
625
|
+
var VELOCITY_WINDOW_MS = 200;
|
|
626
|
+
/** Default movement threshold before onPanStart fires (Q11a, matches motion). */
|
|
627
|
+
var DEFAULT_THRESHOLD = 3;
|
|
628
|
+
/**
|
|
629
|
+
* Observe pointer-driven pan gestures on an element.
|
|
630
|
+
*
|
|
631
|
+
* Returns `{ isPanning, point, delta, offset, velocity }`:
|
|
632
|
+
*
|
|
633
|
+
* - `pan.isPanning()` — Solid Accessor; `true` between onPanStart and onPanEnd.
|
|
634
|
+
* - `pan.point.x`, `pan.point.y` — current pointer position in client coords.
|
|
635
|
+
* Each is a {@link MotionValueAccessor}: call `pan.point.x()` for a tracked
|
|
636
|
+
* read, `pan.point.x.get()` for an untracked snapshot, and pass it directly
|
|
637
|
+
* to `animate()`, `createTransform`, or `useMotion` targets.
|
|
638
|
+
* - `pan.delta.x/y` — delta since last pointermove.
|
|
639
|
+
* - `pan.offset.x/y` — cumulative offset since the current pointerdown.
|
|
640
|
+
* - `pan.velocity.x/y` — sliding-window velocity in px/sec.
|
|
641
|
+
*
|
|
642
|
+
* Fields update from `pointerdown` forward (including pre-threshold moves)
|
|
643
|
+
* — gate reads on `pan.isPanning()` if you only care about real pans.
|
|
644
|
+
*
|
|
645
|
+
* The `options` argument accepts either a static object or a function form
|
|
646
|
+
* (matching `useMotion`'s convention). The function form is read INSIDE
|
|
647
|
+
* each pointer-event handler, so reactive option changes apply on the next
|
|
648
|
+
* relevant event without re-attaching listeners.
|
|
649
|
+
*
|
|
650
|
+
* @example Static options
|
|
651
|
+
* const pan = createPan(el, {
|
|
652
|
+
* onPanStart: (e, info) => console.log("start", info.point),
|
|
653
|
+
* threshold: 3,
|
|
654
|
+
* })
|
|
655
|
+
*
|
|
656
|
+
* @example Reactive options (function form — signals tracked)
|
|
657
|
+
* const [threshold, setThreshold] = createSignal(3)
|
|
658
|
+
* const pan = createPan(el, () => ({
|
|
659
|
+
* threshold: threshold(),
|
|
660
|
+
* onPanStart: (e, info) => console.log(info),
|
|
661
|
+
* }))
|
|
662
|
+
*
|
|
663
|
+
* @example Composing pan.point.x with createTransform
|
|
664
|
+
* const pan = createPan(el)
|
|
665
|
+
* const rotation = createTransform(pan.point.x, [0, 300], [0, 90])
|
|
666
|
+
* <div ref={setEl} style={{ transform: `rotate(${rotation()}deg)` }} />
|
|
667
|
+
*
|
|
668
|
+
* @example Reading reactively in JSX
|
|
669
|
+
* const pan = createPan(el)
|
|
670
|
+
* <Show when={pan.isPanning()}>
|
|
671
|
+
* Position: {pan.point.x()}, {pan.point.y()}
|
|
672
|
+
* </Show>
|
|
673
|
+
*/
|
|
674
|
+
function createPan(ref, options = {}) {
|
|
675
|
+
const getOpts = typeof options === "function" ? options : () => options;
|
|
676
|
+
const [isPanning, setIsPanning] = createSignal(false);
|
|
677
|
+
const pointX = createMotionValue(0);
|
|
678
|
+
const pointY = createMotionValue(0);
|
|
679
|
+
const deltaX = createMotionValue(0);
|
|
680
|
+
const deltaY = createMotionValue(0);
|
|
681
|
+
const offsetX = createMotionValue(0);
|
|
682
|
+
const offsetY = createMotionValue(0);
|
|
683
|
+
const velocityX = createMotionValue(0);
|
|
684
|
+
const velocityY = createMotionValue(0);
|
|
685
|
+
createEffect(() => {
|
|
686
|
+
const el = ref();
|
|
687
|
+
if (!el) return;
|
|
688
|
+
let startPoint = null;
|
|
689
|
+
let lastPoint = null;
|
|
690
|
+
let pointerId = null;
|
|
691
|
+
let panning = false;
|
|
692
|
+
let samples = [];
|
|
693
|
+
function pointOf(event) {
|
|
694
|
+
return {
|
|
695
|
+
x: event.clientX,
|
|
696
|
+
y: event.clientY
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
function computeVelocity() {
|
|
700
|
+
if (samples.length < 2) return {
|
|
701
|
+
x: 0,
|
|
702
|
+
y: 0
|
|
703
|
+
};
|
|
704
|
+
const first = samples[0];
|
|
705
|
+
const last = samples[samples.length - 1];
|
|
706
|
+
const dt = last.t - first.t;
|
|
707
|
+
if (dt <= 0) return {
|
|
708
|
+
x: 0,
|
|
709
|
+
y: 0
|
|
710
|
+
};
|
|
711
|
+
return {
|
|
712
|
+
x: (last.point.x - first.point.x) / dt * 1e3,
|
|
713
|
+
y: (last.point.y - first.point.y) / dt * 1e3
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
function buildInfo(event) {
|
|
717
|
+
const point = pointOf(event);
|
|
718
|
+
return {
|
|
719
|
+
point,
|
|
720
|
+
delta: lastPoint ? {
|
|
721
|
+
x: point.x - lastPoint.x,
|
|
722
|
+
y: point.y - lastPoint.y
|
|
723
|
+
} : {
|
|
724
|
+
x: 0,
|
|
725
|
+
y: 0
|
|
726
|
+
},
|
|
727
|
+
offset: startPoint ? {
|
|
728
|
+
x: point.x - startPoint.x,
|
|
729
|
+
y: point.y - startPoint.y
|
|
730
|
+
} : {
|
|
731
|
+
x: 0,
|
|
732
|
+
y: 0
|
|
733
|
+
},
|
|
734
|
+
velocity: computeVelocity()
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
/** Push a freshly-computed info snapshot into the MVs. Each `.set` fires
|
|
738
|
+
* the MV's change subscription, which the callable-hybrid bridge
|
|
739
|
+
* forwards to Solid; consumers reading e.g. only `pan.velocity.x()` only
|
|
740
|
+
* re-run when velocity.x actually changes — pre-existing MotionValue
|
|
741
|
+
* granularity, not Store path-tracking. */
|
|
742
|
+
function writeInfo(info) {
|
|
743
|
+
pointX.set(info.point.x);
|
|
744
|
+
pointY.set(info.point.y);
|
|
745
|
+
deltaX.set(info.delta.x);
|
|
746
|
+
deltaY.set(info.delta.y);
|
|
747
|
+
offsetX.set(info.offset.x);
|
|
748
|
+
offsetY.set(info.offset.y);
|
|
749
|
+
velocityX.set(info.velocity.x);
|
|
750
|
+
velocityY.set(info.velocity.y);
|
|
751
|
+
}
|
|
752
|
+
function onPointerDown(event) {
|
|
753
|
+
if (!isPrimaryPointer(event)) return;
|
|
754
|
+
startPoint = pointOf(event);
|
|
755
|
+
lastPoint = startPoint;
|
|
756
|
+
pointerId = event.pointerId;
|
|
757
|
+
panning = false;
|
|
758
|
+
samples = [{
|
|
759
|
+
t: time.now(),
|
|
760
|
+
point: startPoint
|
|
761
|
+
}];
|
|
762
|
+
setIsPanning(false);
|
|
763
|
+
pointX.set(startPoint.x);
|
|
764
|
+
pointY.set(startPoint.y);
|
|
765
|
+
deltaX.set(0);
|
|
766
|
+
deltaY.set(0);
|
|
767
|
+
offsetX.set(0);
|
|
768
|
+
offsetY.set(0);
|
|
769
|
+
velocityX.set(0);
|
|
770
|
+
velocityY.set(0);
|
|
771
|
+
window.addEventListener("pointermove", onPointerMove);
|
|
772
|
+
window.addEventListener("pointerup", onPointerEnd);
|
|
773
|
+
window.addEventListener("pointercancel", onPointerEnd);
|
|
774
|
+
}
|
|
775
|
+
function onPointerMove(event) {
|
|
776
|
+
if (event.pointerId !== pointerId) return;
|
|
777
|
+
const point = pointOf(event);
|
|
778
|
+
const now = time.now();
|
|
779
|
+
samples.push({
|
|
780
|
+
t: now,
|
|
781
|
+
point
|
|
782
|
+
});
|
|
783
|
+
const cutoff = now - VELOCITY_WINDOW_MS;
|
|
784
|
+
while (samples.length > 1 && (samples[0]?.t ?? 0) < cutoff) samples.shift();
|
|
785
|
+
const info = buildInfo(event);
|
|
786
|
+
writeInfo(info);
|
|
787
|
+
if (!panning) {
|
|
788
|
+
const threshold = getOpts().threshold ?? DEFAULT_THRESHOLD;
|
|
789
|
+
if (Math.hypot(info.offset.x, info.offset.y) >= threshold) {
|
|
790
|
+
panning = true;
|
|
791
|
+
setIsPanning(true);
|
|
792
|
+
getOpts().onPanStart?.(event, info);
|
|
793
|
+
}
|
|
794
|
+
} else getOpts().onPan?.(event, info);
|
|
795
|
+
lastPoint = point;
|
|
796
|
+
}
|
|
797
|
+
function onPointerEnd(event) {
|
|
798
|
+
if (event.pointerId !== pointerId) return;
|
|
799
|
+
if (panning) getOpts().onPanEnd?.(event, buildInfo(event));
|
|
800
|
+
panning = false;
|
|
801
|
+
setIsPanning(false);
|
|
802
|
+
startPoint = null;
|
|
803
|
+
lastPoint = null;
|
|
804
|
+
pointerId = null;
|
|
805
|
+
samples = [];
|
|
806
|
+
window.removeEventListener("pointermove", onPointerMove);
|
|
807
|
+
window.removeEventListener("pointerup", onPointerEnd);
|
|
808
|
+
window.removeEventListener("pointercancel", onPointerEnd);
|
|
809
|
+
}
|
|
810
|
+
el.addEventListener("pointerdown", onPointerDown);
|
|
811
|
+
onCleanup(() => {
|
|
812
|
+
el.removeEventListener("pointerdown", onPointerDown);
|
|
813
|
+
window.removeEventListener("pointermove", onPointerMove);
|
|
814
|
+
window.removeEventListener("pointerup", onPointerEnd);
|
|
815
|
+
window.removeEventListener("pointercancel", onPointerEnd);
|
|
816
|
+
});
|
|
817
|
+
});
|
|
818
|
+
return {
|
|
819
|
+
isPanning,
|
|
820
|
+
point: {
|
|
821
|
+
x: pointX,
|
|
822
|
+
y: pointY
|
|
823
|
+
},
|
|
824
|
+
delta: {
|
|
825
|
+
x: deltaX,
|
|
826
|
+
y: deltaY
|
|
827
|
+
},
|
|
828
|
+
offset: {
|
|
829
|
+
x: offsetX,
|
|
830
|
+
y: offsetY
|
|
831
|
+
},
|
|
832
|
+
velocity: {
|
|
833
|
+
x: velocityX,
|
|
834
|
+
y: velocityY
|
|
835
|
+
}
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
//#endregion
|
|
839
|
+
//#region src/primitives/createDrag.ts
|
|
840
|
+
/**
|
|
841
|
+
* Get or create a motion-dom VisualElement for an HTMLElement. Required
|
|
842
|
+
* because we write to the VE's `x`/`y` MotionValues during drag, and motion
|
|
843
|
+
* only auto-creates the VE inside `animate(el, target)` calls — if a user
|
|
844
|
+
* configures drag without any animate target, no VE would exist.
|
|
845
|
+
*
|
|
846
|
+
* Mirrors framer-motion's `createDOMVisualElement` (which isn't reachable
|
|
847
|
+
* from a non-React context — framer-motion's main entry requires React).
|
|
848
|
+
* The options shape and `mount` + `visualElementStore.set` calls match the
|
|
849
|
+
* upstream implementation. SVG support is omitted for v0.1 — drag on SVG
|
|
850
|
+
* is an unusual case.
|
|
851
|
+
*/
|
|
852
|
+
function ensureVisualElement(el) {
|
|
853
|
+
const existing = visualElementStore.get(el);
|
|
854
|
+
if (existing) return existing;
|
|
855
|
+
const ve = new HTMLVisualElement({
|
|
856
|
+
presenceContext: null,
|
|
857
|
+
props: {},
|
|
858
|
+
visualState: {
|
|
859
|
+
renderState: {
|
|
860
|
+
transform: {},
|
|
861
|
+
transformOrigin: {},
|
|
862
|
+
style: {},
|
|
863
|
+
vars: {},
|
|
864
|
+
attrs: {}
|
|
865
|
+
},
|
|
866
|
+
latestValues: {}
|
|
867
|
+
}
|
|
868
|
+
});
|
|
869
|
+
ve.mount(el);
|
|
870
|
+
visualElementStore.set(el, ve);
|
|
871
|
+
return ve;
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* Compute the `touch-action` CSS value for an element being dragged.
|
|
875
|
+
* Disabling touch-action prevents the browser from interpreting the gesture
|
|
876
|
+
* as a scroll. Axis-locked drags leave the unused axis available for scroll
|
|
877
|
+
* (so a horizontally-draggable card can still be scrolled vertically by the
|
|
878
|
+
* surrounding page).
|
|
879
|
+
*/
|
|
880
|
+
function touchActionFor(drag) {
|
|
881
|
+
if (drag === "x") return "pan-y";
|
|
882
|
+
if (drag === "y") return "pan-x";
|
|
883
|
+
return "none";
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* Resolve a {@link DragConstraints} value into absolute MotionValue bounds at
|
|
887
|
+
* drag-start. Two input shapes (Q8):
|
|
888
|
+
*
|
|
889
|
+
* - **Numeric** (`{ top, left, right, bottom }`): bounds are absolute MV
|
|
890
|
+
* values. `left: -100` means x cannot go below -100.
|
|
891
|
+
* - **HTMLElement or `() => HTMLElement | null`**: container that the
|
|
892
|
+
* dragged element must stay inside. Bounds are computed from the
|
|
893
|
+
* container's bounding rect vs the dragged element's current rect, then
|
|
894
|
+
* re-centered around the current MV values.
|
|
895
|
+
*
|
|
896
|
+
* The element form is resolved ONCE at drag-start (current viewport rects).
|
|
897
|
+
* Reactive constraint changes mid-drag aren't honored in v0.1 — they'd
|
|
898
|
+
* require re-measuring on each pointermove. Acceptable corner case.
|
|
899
|
+
*
|
|
900
|
+
* Returns `null` when constraints are unset or the accessor returns null —
|
|
901
|
+
* caller treats as "no clamping, no elastic resistance."
|
|
902
|
+
*/
|
|
903
|
+
function resolveConstraints(constraints, el, dragStartX, dragStartY) {
|
|
904
|
+
if (!constraints) return null;
|
|
905
|
+
let container = null;
|
|
906
|
+
if (constraints instanceof HTMLElement) container = constraints;
|
|
907
|
+
else if (typeof constraints === "function") container = constraints();
|
|
908
|
+
if (container) {
|
|
909
|
+
const containerRect = container.getBoundingClientRect();
|
|
910
|
+
const elementRect = el.getBoundingClientRect();
|
|
911
|
+
return {
|
|
912
|
+
minX: dragStartX + (containerRect.left - elementRect.left),
|
|
913
|
+
maxX: dragStartX + (containerRect.right - elementRect.right),
|
|
914
|
+
minY: dragStartY + (containerRect.top - elementRect.top),
|
|
915
|
+
maxY: dragStartY + (containerRect.bottom - elementRect.bottom)
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
const numeric = constraints;
|
|
919
|
+
return {
|
|
920
|
+
minX: numeric.left ?? -Infinity,
|
|
921
|
+
maxX: numeric.right ?? Infinity,
|
|
922
|
+
minY: numeric.top ?? -Infinity,
|
|
923
|
+
maxY: numeric.bottom ?? Infinity
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* Apply elastic resistance past a boundary (Q15c — linear).
|
|
928
|
+
*
|
|
929
|
+
* Within bounds: `value` passes through unchanged. Past a bound by `Δ`: the
|
|
930
|
+
* displayed value is `boundary + elastic × Δ`. With `elastic: 0` the value
|
|
931
|
+
* clamps hard at the boundary; with `elastic: 1` resistance vanishes
|
|
932
|
+
* (motion's default is `0.5`, halving the overflow).
|
|
933
|
+
*
|
|
934
|
+
* The function is symmetric — overflow on either side resists with the
|
|
935
|
+
* same coefficient.
|
|
936
|
+
*/
|
|
937
|
+
function applyElastic(value, min, max, elastic) {
|
|
938
|
+
if (value < min) return min + (value - min) * elastic;
|
|
939
|
+
if (value > max) return max + (value - max) * elastic;
|
|
940
|
+
return value;
|
|
941
|
+
}
|
|
942
|
+
var DEFAULT_ELASTIC = .5;
|
|
943
|
+
/**
|
|
944
|
+
* Default `dragTransition` (Q15d — matches motion's inertia preset).
|
|
945
|
+
*
|
|
946
|
+
* `type: "inertia"` decays from the release point using `velocity`, with
|
|
947
|
+
* spring physics at `min`/`max` boundaries. The defaults are the values
|
|
948
|
+
* the user signed off on during Phase 2 grilling; passing a custom
|
|
949
|
+
* `dragTransition` shallow-merges over these.
|
|
950
|
+
*/
|
|
951
|
+
var DEFAULT_DRAG_TRANSITION = {
|
|
952
|
+
type: "inertia",
|
|
953
|
+
power: .8,
|
|
954
|
+
timeConstant: 750,
|
|
955
|
+
bounceStiffness: 500,
|
|
956
|
+
bounceDamping: 10,
|
|
957
|
+
restDelta: 1,
|
|
958
|
+
restSpeed: 10
|
|
959
|
+
};
|
|
960
|
+
/**
|
|
961
|
+
* Bind pointer-driven drag to an element. Layers on top of createPan for the
|
|
962
|
+
* pointer session; adds transform writes, body styles, pointer capture, and
|
|
963
|
+
* state-machine activation.
|
|
964
|
+
*
|
|
965
|
+
* Drag is enabled when `opts.drag` is truthy (`true`, `"x"`, or `"y"`).
|
|
966
|
+
* createDrag always wires the pointer session — the enable check is per-
|
|
967
|
+
* gesture-start, so toggling `opts.drag` on/off doesn't churn listeners.
|
|
968
|
+
*
|
|
969
|
+
* Phase 2 Commit 6 — Stage 2 scope: VE bootstrap, translation, axis lock,
|
|
970
|
+
* body/pointer styles, callbacks, cleanup. Constraints + elastic resistance
|
|
971
|
+
* land in Stage 3; momentum + dragSnapToOrigin in Stage 4.
|
|
972
|
+
*/
|
|
973
|
+
function createDrag(el, getOpts, setActive) {
|
|
974
|
+
let xMV = null;
|
|
975
|
+
let yMV = null;
|
|
976
|
+
/** Drag-start position of the x/y MotionValues (the values that existed
|
|
977
|
+
* before the user grabbed). offsets from PanInfo accumulate from this. */
|
|
978
|
+
let dragStartX = 0;
|
|
979
|
+
let dragStartY = 0;
|
|
980
|
+
/** Resolved bounds for this session — computed once at drag-start and
|
|
981
|
+
* reused across all pointermoves to avoid repeat layout reads. `null`
|
|
982
|
+
* means no constraints. */
|
|
983
|
+
let sessionBounds = null;
|
|
984
|
+
/** Saved before applying drag's `user-select` / `touch-action` overrides
|
|
985
|
+
* so we can restore them exactly on session end. */
|
|
986
|
+
let savedUserSelect = "";
|
|
987
|
+
let savedTouchAction = "";
|
|
988
|
+
let capturedPointerId = null;
|
|
989
|
+
/** In-flight momentum animations (one per axis when active). Stopped on
|
|
990
|
+
* owner disposal AND on a fresh pointerdown (to interrupt a settling
|
|
991
|
+
* momentum if the user grabs again mid-decay). */
|
|
992
|
+
let momentumControls = [];
|
|
993
|
+
function isDragEnabled() {
|
|
994
|
+
return Boolean(getOpts().drag);
|
|
995
|
+
}
|
|
996
|
+
function restoreBodyAndElementStyles() {
|
|
997
|
+
document.body.style.userSelect = savedUserSelect;
|
|
998
|
+
el.style.touchAction = savedTouchAction;
|
|
999
|
+
}
|
|
1000
|
+
function releasePointerCaptureSafely() {
|
|
1001
|
+
if (capturedPointerId === null) return;
|
|
1002
|
+
try {
|
|
1003
|
+
el.releasePointerCapture(capturedPointerId);
|
|
1004
|
+
} catch {}
|
|
1005
|
+
capturedPointerId = null;
|
|
1006
|
+
}
|
|
1007
|
+
function stopMomentum() {
|
|
1008
|
+
for (const ctrl of momentumControls) ctrl.stop();
|
|
1009
|
+
momentumControls = [];
|
|
1010
|
+
}
|
|
1011
|
+
const handlePanStart = (event, info) => {
|
|
1012
|
+
if (!isDragEnabled()) return;
|
|
1013
|
+
stopMomentum();
|
|
1014
|
+
const ve = ensureVisualElement(el);
|
|
1015
|
+
xMV = ve.getValue("x", 0);
|
|
1016
|
+
yMV = ve.getValue("y", 0);
|
|
1017
|
+
dragStartX = xMV.get();
|
|
1018
|
+
dragStartY = yMV.get();
|
|
1019
|
+
sessionBounds = resolveConstraints(getOpts().dragConstraints, el, dragStartX, dragStartY);
|
|
1020
|
+
savedUserSelect = document.body.style.userSelect;
|
|
1021
|
+
savedTouchAction = el.style.touchAction;
|
|
1022
|
+
document.body.style.userSelect = "none";
|
|
1023
|
+
el.style.touchAction = touchActionFor(getOpts().drag);
|
|
1024
|
+
try {
|
|
1025
|
+
el.setPointerCapture(event.pointerId);
|
|
1026
|
+
capturedPointerId = event.pointerId;
|
|
1027
|
+
} catch {}
|
|
1028
|
+
setActive("whileDrag", true);
|
|
1029
|
+
getOpts().onDragStart?.(event, info);
|
|
1030
|
+
};
|
|
1031
|
+
const handlePan = (event, info) => {
|
|
1032
|
+
if (!isDragEnabled() || !xMV || !yMV) return;
|
|
1033
|
+
const axis = getOpts().drag;
|
|
1034
|
+
const writeX = axis !== "y";
|
|
1035
|
+
const writeY = axis !== "x";
|
|
1036
|
+
const elastic = getOpts().dragElastic ?? DEFAULT_ELASTIC;
|
|
1037
|
+
if (writeX) {
|
|
1038
|
+
const candidateX = dragStartX + info.offset.x;
|
|
1039
|
+
const finalX = sessionBounds ? applyElastic(candidateX, sessionBounds.minX, sessionBounds.maxX, elastic) : candidateX;
|
|
1040
|
+
xMV.set(finalX);
|
|
1041
|
+
}
|
|
1042
|
+
if (writeY) {
|
|
1043
|
+
const candidateY = dragStartY + info.offset.y;
|
|
1044
|
+
const finalY = sessionBounds ? applyElastic(candidateY, sessionBounds.minY, sessionBounds.maxY, elastic) : candidateY;
|
|
1045
|
+
yMV.set(finalY);
|
|
1046
|
+
}
|
|
1047
|
+
getOpts().onDrag?.(event, info);
|
|
1048
|
+
};
|
|
1049
|
+
const handlePanEnd = (event, info) => {
|
|
1050
|
+
if (!isDragEnabled() || !xMV || !yMV) return;
|
|
1051
|
+
setActive("whileDrag", false);
|
|
1052
|
+
restoreBodyAndElementStyles();
|
|
1053
|
+
releasePointerCaptureSafely();
|
|
1054
|
+
getOpts().onDragEnd?.(event, info);
|
|
1055
|
+
const xRef = xMV;
|
|
1056
|
+
const yRef = yMV;
|
|
1057
|
+
const boundsRef = sessionBounds;
|
|
1058
|
+
const opts = getOpts();
|
|
1059
|
+
const snapToOrigin = opts.dragSnapToOrigin ?? false;
|
|
1060
|
+
const momentum = opts.dragMomentum ?? true;
|
|
1061
|
+
const userTransition = opts.dragTransition ?? {};
|
|
1062
|
+
const dragAxis = opts.drag;
|
|
1063
|
+
const releaseX = dragAxis !== "y";
|
|
1064
|
+
const releaseY = dragAxis !== "x";
|
|
1065
|
+
momentumControls = [];
|
|
1066
|
+
const elastic = opts.dragElastic ?? DEFAULT_ELASTIC;
|
|
1067
|
+
const bounceParams = elastic ? {
|
|
1068
|
+
bounceStiffness: 200,
|
|
1069
|
+
bounceDamping: 40
|
|
1070
|
+
} : {
|
|
1071
|
+
bounceStiffness: 1e6,
|
|
1072
|
+
bounceDamping: 1e7
|
|
1073
|
+
};
|
|
1074
|
+
const xAtMax = boundsRef !== null && boundsRef.maxX !== Infinity && xRef.get() >= boundsRef.maxX;
|
|
1075
|
+
const xAtMin = boundsRef !== null && boundsRef.minX !== -Infinity && xRef.get() <= boundsRef.minX;
|
|
1076
|
+
const yAtMax = boundsRef !== null && boundsRef.maxY !== Infinity && yRef.get() >= boundsRef.maxY;
|
|
1077
|
+
const yAtMin = boundsRef !== null && boundsRef.minY !== -Infinity && yRef.get() <= boundsRef.minY;
|
|
1078
|
+
const xVelocity = !elastic && (xAtMax && info.velocity.x > 0 || xAtMin && info.velocity.x < 0) ? 0 : info.velocity.x;
|
|
1079
|
+
const yVelocity = !elastic && (yAtMax && info.velocity.y > 0 || yAtMin && info.velocity.y < 0) ? 0 : info.velocity.y;
|
|
1080
|
+
/** Fire onDragTransitionEnd via getOpts so reactive callback swaps see
|
|
1081
|
+
* the latest value (the user may have swapped handlers between pan-end
|
|
1082
|
+
* and momentum-settle). */
|
|
1083
|
+
const fireTransitionEnd = () => getOpts().onDragTransitionEnd?.();
|
|
1084
|
+
if (snapToOrigin) {
|
|
1085
|
+
const transitionX = {
|
|
1086
|
+
...DEFAULT_DRAG_TRANSITION,
|
|
1087
|
+
...bounceParams,
|
|
1088
|
+
...userTransition,
|
|
1089
|
+
velocity: xVelocity,
|
|
1090
|
+
min: 0,
|
|
1091
|
+
max: 0
|
|
1092
|
+
};
|
|
1093
|
+
const transitionY = {
|
|
1094
|
+
...DEFAULT_DRAG_TRANSITION,
|
|
1095
|
+
...bounceParams,
|
|
1096
|
+
...userTransition,
|
|
1097
|
+
velocity: yVelocity,
|
|
1098
|
+
min: 0,
|
|
1099
|
+
max: 0
|
|
1100
|
+
};
|
|
1101
|
+
const settles = [];
|
|
1102
|
+
if (releaseX) {
|
|
1103
|
+
const ctrlX = animate$1(xRef, 0, transitionX);
|
|
1104
|
+
momentumControls.push(ctrlX);
|
|
1105
|
+
settles.push(ctrlX);
|
|
1106
|
+
}
|
|
1107
|
+
if (releaseY) {
|
|
1108
|
+
const ctrlY = animate$1(yRef, 0, transitionY);
|
|
1109
|
+
momentumControls.push(ctrlY);
|
|
1110
|
+
settles.push(ctrlY);
|
|
1111
|
+
}
|
|
1112
|
+
if (settles.length > 0) Promise.all(settles).then(fireTransitionEnd);
|
|
1113
|
+
else fireTransitionEnd();
|
|
1114
|
+
} else {
|
|
1115
|
+
const releaseVelocityX = momentum ? xVelocity : 0;
|
|
1116
|
+
const releaseVelocityY = momentum ? yVelocity : 0;
|
|
1117
|
+
const transitionX = {
|
|
1118
|
+
...DEFAULT_DRAG_TRANSITION,
|
|
1119
|
+
...bounceParams,
|
|
1120
|
+
...userTransition,
|
|
1121
|
+
velocity: releaseVelocityX,
|
|
1122
|
+
min: boundsRef?.minX,
|
|
1123
|
+
max: boundsRef?.maxX
|
|
1124
|
+
};
|
|
1125
|
+
const transitionY = {
|
|
1126
|
+
...DEFAULT_DRAG_TRANSITION,
|
|
1127
|
+
...bounceParams,
|
|
1128
|
+
...userTransition,
|
|
1129
|
+
velocity: releaseVelocityY,
|
|
1130
|
+
min: boundsRef?.minY,
|
|
1131
|
+
max: boundsRef?.maxY
|
|
1132
|
+
};
|
|
1133
|
+
const settles = [];
|
|
1134
|
+
if (releaseX) {
|
|
1135
|
+
const ctrlX = animate$1(xRef, 0, transitionX);
|
|
1136
|
+
momentumControls.push(ctrlX);
|
|
1137
|
+
settles.push(ctrlX);
|
|
1138
|
+
}
|
|
1139
|
+
if (releaseY) {
|
|
1140
|
+
const ctrlY = animate$1(yRef, 0, transitionY);
|
|
1141
|
+
momentumControls.push(ctrlY);
|
|
1142
|
+
settles.push(ctrlY);
|
|
1143
|
+
}
|
|
1144
|
+
if (settles.length > 0) Promise.all(settles).then(fireTransitionEnd);
|
|
1145
|
+
else fireTransitionEnd();
|
|
1146
|
+
}
|
|
1147
|
+
xMV = null;
|
|
1148
|
+
yMV = null;
|
|
1149
|
+
sessionBounds = null;
|
|
1150
|
+
};
|
|
1151
|
+
createPan(() => el, () => ({
|
|
1152
|
+
threshold: getOpts().panThreshold,
|
|
1153
|
+
onPanStart: handlePanStart,
|
|
1154
|
+
onPan: handlePan,
|
|
1155
|
+
onPanEnd: handlePanEnd
|
|
1156
|
+
}));
|
|
1157
|
+
function startExternalDrag(event, options) {
|
|
1158
|
+
if (!isDragEnabled()) return;
|
|
1159
|
+
if (options.snapToCursor) {
|
|
1160
|
+
const ve = ensureVisualElement(el);
|
|
1161
|
+
const snapXMV = ve.getValue("x", 0);
|
|
1162
|
+
const snapYMV = ve.getValue("y", 0);
|
|
1163
|
+
const elRect = el.getBoundingClientRect();
|
|
1164
|
+
const centerX = elRect.left + elRect.width / 2;
|
|
1165
|
+
const centerY = elRect.top + elRect.height / 2;
|
|
1166
|
+
const axis = getOpts().drag;
|
|
1167
|
+
if (axis !== "y") snapXMV.set(snapXMV.get() + (event.clientX - centerX));
|
|
1168
|
+
if (axis !== "x") snapYMV.set(snapYMV.get() + (event.clientY - centerY));
|
|
1169
|
+
}
|
|
1170
|
+
handlePanStart(event, {
|
|
1171
|
+
point: {
|
|
1172
|
+
x: event.clientX,
|
|
1173
|
+
y: event.clientY
|
|
1174
|
+
},
|
|
1175
|
+
delta: {
|
|
1176
|
+
x: 0,
|
|
1177
|
+
y: 0
|
|
1178
|
+
},
|
|
1179
|
+
offset: {
|
|
1180
|
+
x: 0,
|
|
1181
|
+
y: 0
|
|
1182
|
+
},
|
|
1183
|
+
velocity: {
|
|
1184
|
+
x: 0,
|
|
1185
|
+
y: 0
|
|
1186
|
+
}
|
|
1187
|
+
});
|
|
1188
|
+
const sessionStartPoint = {
|
|
1189
|
+
x: event.clientX,
|
|
1190
|
+
y: event.clientY
|
|
1191
|
+
};
|
|
1192
|
+
let sessionLastPoint = { ...sessionStartPoint };
|
|
1193
|
+
const sessionPointerId = event.pointerId;
|
|
1194
|
+
const sessionSamples = [{
|
|
1195
|
+
t: time.now(),
|
|
1196
|
+
point: { ...sessionStartPoint }
|
|
1197
|
+
}];
|
|
1198
|
+
function computeSessionVelocity() {
|
|
1199
|
+
if (sessionSamples.length < 2) return {
|
|
1200
|
+
x: 0,
|
|
1201
|
+
y: 0
|
|
1202
|
+
};
|
|
1203
|
+
const first = sessionSamples[0];
|
|
1204
|
+
const last = sessionSamples[sessionSamples.length - 1];
|
|
1205
|
+
if (!first || !last) return {
|
|
1206
|
+
x: 0,
|
|
1207
|
+
y: 0
|
|
1208
|
+
};
|
|
1209
|
+
const dt = last.t - first.t;
|
|
1210
|
+
if (dt <= 0) return {
|
|
1211
|
+
x: 0,
|
|
1212
|
+
y: 0
|
|
1213
|
+
};
|
|
1214
|
+
return {
|
|
1215
|
+
x: (last.point.x - first.point.x) / dt * 1e3,
|
|
1216
|
+
y: (last.point.y - first.point.y) / dt * 1e3
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
function buildSessionInfo(e) {
|
|
1220
|
+
const point = {
|
|
1221
|
+
x: e.clientX,
|
|
1222
|
+
y: e.clientY
|
|
1223
|
+
};
|
|
1224
|
+
return {
|
|
1225
|
+
point,
|
|
1226
|
+
delta: {
|
|
1227
|
+
x: point.x - sessionLastPoint.x,
|
|
1228
|
+
y: point.y - sessionLastPoint.y
|
|
1229
|
+
},
|
|
1230
|
+
offset: {
|
|
1231
|
+
x: point.x - sessionStartPoint.x,
|
|
1232
|
+
y: point.y - sessionStartPoint.y
|
|
1233
|
+
},
|
|
1234
|
+
velocity: computeSessionVelocity()
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
function onSessionMove(e) {
|
|
1238
|
+
if (e.pointerId !== sessionPointerId) return;
|
|
1239
|
+
const point = {
|
|
1240
|
+
x: e.clientX,
|
|
1241
|
+
y: e.clientY
|
|
1242
|
+
};
|
|
1243
|
+
const now = time.now();
|
|
1244
|
+
sessionSamples.push({
|
|
1245
|
+
t: now,
|
|
1246
|
+
point
|
|
1247
|
+
});
|
|
1248
|
+
const cutoff = now - 200;
|
|
1249
|
+
while (sessionSamples.length > 1 && (sessionSamples[0]?.t ?? 0) < cutoff) sessionSamples.shift();
|
|
1250
|
+
const info = buildSessionInfo(e);
|
|
1251
|
+
sessionLastPoint = point;
|
|
1252
|
+
handlePan(e, info);
|
|
1253
|
+
}
|
|
1254
|
+
function onSessionEnd(e) {
|
|
1255
|
+
if (e.pointerId !== sessionPointerId) return;
|
|
1256
|
+
handlePanEnd(e, buildSessionInfo(e));
|
|
1257
|
+
window.removeEventListener("pointermove", onSessionMove);
|
|
1258
|
+
window.removeEventListener("pointerup", onSessionEnd);
|
|
1259
|
+
window.removeEventListener("pointercancel", onSessionEnd);
|
|
1260
|
+
}
|
|
1261
|
+
window.addEventListener("pointermove", onSessionMove);
|
|
1262
|
+
window.addEventListener("pointerup", onSessionEnd);
|
|
1263
|
+
window.addEventListener("pointercancel", onSessionEnd);
|
|
1264
|
+
}
|
|
1265
|
+
createEffect(() => {
|
|
1266
|
+
const controls = getOpts().dragControls;
|
|
1267
|
+
if (!controls) return;
|
|
1268
|
+
const register = controls[DRAG_CONTROLS_REGISTER];
|
|
1269
|
+
if (!register) return;
|
|
1270
|
+
onCleanup(register(startExternalDrag));
|
|
1271
|
+
});
|
|
1272
|
+
onCleanup(() => {
|
|
1273
|
+
stopMomentum();
|
|
1274
|
+
if (xMV || yMV) {
|
|
1275
|
+
restoreBodyAndElementStyles();
|
|
1276
|
+
releasePointerCaptureSafely();
|
|
1277
|
+
}
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
//#endregion
|
|
1281
|
+
//#region src/primitives/createInView.ts
|
|
1282
|
+
/**
|
|
1283
|
+
* Observe an element via {@link IntersectionObserver} and expose its
|
|
1284
|
+
* in-view state as a pair of Solid Accessors.
|
|
1285
|
+
*
|
|
1286
|
+
* Pass a ref-style accessor that returns the element. The observer
|
|
1287
|
+
* attaches once the accessor returns a non-null element and re-attaches
|
|
1288
|
+
* if it changes. The observer is disconnected on owner disposal.
|
|
1289
|
+
*
|
|
1290
|
+
* Options can be a static object or a function form (matching `useMotion`
|
|
1291
|
+
* and `createPan`'s convention). The function form is tracked inside the
|
|
1292
|
+
* effect — option changes (e.g., switching `root`) re-attach the observer.
|
|
1293
|
+
*
|
|
1294
|
+
* @example Static options
|
|
1295
|
+
* const [el, setEl] = createSignal<HTMLElement>()
|
|
1296
|
+
* const view = createInView(el, { once: true })
|
|
1297
|
+
* createEffect(() => {
|
|
1298
|
+
* if (view.isInView()) console.log("now in view")
|
|
1299
|
+
* })
|
|
1300
|
+
*
|
|
1301
|
+
* @example Function-form options (reactive)
|
|
1302
|
+
* const [root, setRoot] = createSignal<HTMLElement>()
|
|
1303
|
+
* const view = createInView(el, () => ({ root, margin: "100px" }))
|
|
1304
|
+
*
|
|
1305
|
+
* @example Reading the raw entry reactively
|
|
1306
|
+
* const view = createInView(el)
|
|
1307
|
+
* createEffect(() => {
|
|
1308
|
+
* const e = view.entry()
|
|
1309
|
+
* if (e) console.log("ratio:", e.intersectionRatio)
|
|
1310
|
+
* })
|
|
1311
|
+
*
|
|
1312
|
+
* <div ref={setEl}>watch me</div>
|
|
1313
|
+
*/
|
|
1314
|
+
function createInView(ref, options = {}) {
|
|
1315
|
+
const [isInView, setIsInView] = createSignal(false);
|
|
1316
|
+
const [entry, setEntry] = createSignal(null);
|
|
1317
|
+
createEffect(() => {
|
|
1318
|
+
const el = ref();
|
|
1319
|
+
if (!el) return;
|
|
1320
|
+
const opts = typeof options === "function" ? options() : options;
|
|
1321
|
+
const threshold = resolveThreshold(opts.amount);
|
|
1322
|
+
const observer = new IntersectionObserver((entries) => {
|
|
1323
|
+
for (const e of entries) {
|
|
1324
|
+
opts.onChange?.(e);
|
|
1325
|
+
setEntry(e);
|
|
1326
|
+
if (e.isIntersecting) {
|
|
1327
|
+
setIsInView(true);
|
|
1328
|
+
if (opts.once) observer.disconnect();
|
|
1329
|
+
} else if (!opts.once) setIsInView(false);
|
|
1330
|
+
}
|
|
1331
|
+
}, {
|
|
1332
|
+
root: opts.root?.() ?? null,
|
|
1333
|
+
rootMargin: opts.margin ?? "0px",
|
|
1334
|
+
threshold
|
|
1335
|
+
});
|
|
1336
|
+
observer.observe(el);
|
|
1337
|
+
onCleanup(() => observer.disconnect());
|
|
1338
|
+
});
|
|
1339
|
+
return {
|
|
1340
|
+
isInView,
|
|
1341
|
+
entry
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
function resolveThreshold(amount) {
|
|
1345
|
+
if (Array.isArray(amount)) return amount;
|
|
1346
|
+
if (typeof amount === "number") return amount;
|
|
1347
|
+
if (amount === "all") return 1;
|
|
1348
|
+
return 0;
|
|
1349
|
+
}
|
|
1350
|
+
//#endregion
|
|
1351
|
+
//#region src/primitives/createGestures.ts
|
|
1352
|
+
/**
|
|
1353
|
+
* Bind pointer-event-driven gestures (hover, press) to the motion element.
|
|
1354
|
+
* Toggles the state machine's `whileHover` / `whilePress` flags and forwards
|
|
1355
|
+
* events to the user's `MotionCallbacks`.
|
|
1356
|
+
*/
|
|
1357
|
+
function createGestures(el, getOpts, setActive) {
|
|
1358
|
+
onCleanup(hover(el, (_element, event) => {
|
|
1359
|
+
setActive("whileHover", true);
|
|
1360
|
+
getOpts().onHoverStart?.(event);
|
|
1361
|
+
return (event) => {
|
|
1362
|
+
setActive("whileHover", false);
|
|
1363
|
+
getOpts().onHoverEnd?.(event);
|
|
1364
|
+
};
|
|
1365
|
+
}));
|
|
1366
|
+
onCleanup(press(el, (_element, event) => {
|
|
1367
|
+
setActive("whilePress", true);
|
|
1368
|
+
getOpts().onPressStart?.(event);
|
|
1369
|
+
return (event, info) => {
|
|
1370
|
+
setActive("whilePress", false);
|
|
1371
|
+
if (info.success) getOpts().onPress?.(event, info);
|
|
1372
|
+
else getOpts().onPressCancel?.(event, info);
|
|
1373
|
+
};
|
|
1374
|
+
}));
|
|
1375
|
+
let focusActiveByVisible = false;
|
|
1376
|
+
const stopFocus = addDomEvent(el, "focus", (event) => {
|
|
1377
|
+
let isFocusVisible = false;
|
|
1378
|
+
try {
|
|
1379
|
+
isFocusVisible = el.matches(":focus-visible");
|
|
1380
|
+
} catch {
|
|
1381
|
+
isFocusVisible = true;
|
|
1382
|
+
}
|
|
1383
|
+
if (isFocusVisible) {
|
|
1384
|
+
setActive("whileFocus", true);
|
|
1385
|
+
focusActiveByVisible = true;
|
|
1386
|
+
}
|
|
1387
|
+
getOpts().onFocus?.(event);
|
|
1388
|
+
});
|
|
1389
|
+
const stopBlur = addDomEvent(el, "blur", (event) => {
|
|
1390
|
+
if (focusActiveByVisible) {
|
|
1391
|
+
setActive("whileFocus", false);
|
|
1392
|
+
focusActiveByVisible = false;
|
|
1393
|
+
}
|
|
1394
|
+
getOpts().onBlur?.(event);
|
|
1395
|
+
});
|
|
1396
|
+
onCleanup(stopFocus);
|
|
1397
|
+
onCleanup(stopBlur);
|
|
1398
|
+
const view = createInView(() => el, () => ({
|
|
1399
|
+
...getOpts().inViewOptions,
|
|
1400
|
+
onChange: (entry) => {
|
|
1401
|
+
if (entry.isIntersecting) getOpts().onViewportEnter?.(entry);
|
|
1402
|
+
else getOpts().onViewportLeave?.(entry);
|
|
1403
|
+
}
|
|
1404
|
+
}));
|
|
1405
|
+
createEffect(() => {
|
|
1406
|
+
setActive("whileInView", view.isInView());
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
//#endregion
|
|
1410
|
+
//#region src/default-values.ts
|
|
1411
|
+
var TRANSFORM_DEFAULTS = {
|
|
1412
|
+
x: 0,
|
|
1413
|
+
y: 0,
|
|
1414
|
+
z: 0,
|
|
1415
|
+
translateX: 0,
|
|
1416
|
+
translateY: 0,
|
|
1417
|
+
translateZ: 0,
|
|
1418
|
+
scale: 1,
|
|
1419
|
+
scaleX: 1,
|
|
1420
|
+
scaleY: 1,
|
|
1421
|
+
scaleZ: 1,
|
|
1422
|
+
rotate: 0,
|
|
1423
|
+
rotateX: 0,
|
|
1424
|
+
rotateY: 0,
|
|
1425
|
+
rotateZ: 0,
|
|
1426
|
+
skew: 0,
|
|
1427
|
+
skewX: 0,
|
|
1428
|
+
skewY: 0,
|
|
1429
|
+
perspective: 0,
|
|
1430
|
+
transformPerspective: 0,
|
|
1431
|
+
opacity: 1
|
|
1432
|
+
};
|
|
1433
|
+
/**
|
|
1434
|
+
* Look up the canonical fallback value for a property key. Returns the table
|
|
1435
|
+
* value if known, else `null` — which motion's `animate()` interprets as
|
|
1436
|
+
* "read from computed style at animation start."
|
|
1437
|
+
*/
|
|
1438
|
+
function getMotionDefault(key) {
|
|
1439
|
+
return TRANSFORM_DEFAULTS[key] ?? null;
|
|
1440
|
+
}
|
|
1441
|
+
//#endregion
|
|
1442
|
+
//#region src/primitives/gesture-state.ts
|
|
1443
|
+
/** High → low priority for the winners walk. Materialized once. */
|
|
1444
|
+
var PRIORITY_HIGH_TO_LOW = [...[
|
|
1445
|
+
"animate",
|
|
1446
|
+
"whileInView",
|
|
1447
|
+
"whileHover",
|
|
1448
|
+
"whilePress",
|
|
1449
|
+
"whileFocus",
|
|
1450
|
+
"whileDrag",
|
|
1451
|
+
"exit"
|
|
1452
|
+
]].reverse();
|
|
1453
|
+
/**
|
|
1454
|
+
* Construct the per-element gesture state machine.
|
|
1455
|
+
*
|
|
1456
|
+
* Wired primitives:
|
|
1457
|
+
* - `createStore` for the seven active flags — Solid tracks per-path, so
|
|
1458
|
+
* toggling `whileHover` doesn't dirty memos reading `whilePress`.
|
|
1459
|
+
* - `createMemo` for `stateTargets` — cached, re-runs only when opts/parent
|
|
1460
|
+
* context change.
|
|
1461
|
+
* - `createMemo` for `winners` — same caching, re-runs when `active` flags or
|
|
1462
|
+
* `stateTargets` change.
|
|
1463
|
+
* - `createEffect` for the diff-and-animate loop — fires on `winners` change;
|
|
1464
|
+
* compares against `lastApplied` to compute changed/removed keys.
|
|
1465
|
+
* - `onCleanup` inside the effect for per-iteration MV subscriptions — scoped
|
|
1466
|
+
* to each effect run (fires on re-run AND owner disposal). Same iteration-
|
|
1467
|
+
* scoped cleanup pattern Phase 1 established.
|
|
1468
|
+
*/
|
|
1469
|
+
function createGestureStateMachine(deps) {
|
|
1470
|
+
const { el, getOpts, parentVariantCtx, motionConfig, systemReducedMotion, initialTarget, externalActiveStore, suppressFirstMount, enterReady, getValueForAnimate } = deps;
|
|
1471
|
+
const [active, setActiveStore] = externalActiveStore ?? createStore({
|
|
1472
|
+
animate: true,
|
|
1473
|
+
whileInView: false,
|
|
1474
|
+
whileHover: false,
|
|
1475
|
+
whilePress: false,
|
|
1476
|
+
whileFocus: false,
|
|
1477
|
+
whileDrag: false,
|
|
1478
|
+
exit: false
|
|
1479
|
+
});
|
|
1480
|
+
const stateTargets = createMemo(() => {
|
|
1481
|
+
const opts = getOpts();
|
|
1482
|
+
const variants = opts.variants;
|
|
1483
|
+
const custom = opts.custom ?? parentVariantCtx.custom?.();
|
|
1484
|
+
return {
|
|
1485
|
+
animate: resolveTarget(opts.animate, variants, asVariantLabels(parentVariantCtx.animate?.()), custom),
|
|
1486
|
+
whileInView: resolveTarget(opts.inView, variants, asVariantLabels(parentVariantCtx.inView?.()), custom),
|
|
1487
|
+
whileHover: resolveTarget(opts.hover, variants, asVariantLabels(parentVariantCtx.hover?.()), custom),
|
|
1488
|
+
whilePress: resolveTarget(opts.press, variants, asVariantLabels(parentVariantCtx.press?.()), custom),
|
|
1489
|
+
whileFocus: resolveTarget(opts.focus, variants, asVariantLabels(parentVariantCtx.focus?.()), custom),
|
|
1490
|
+
whileDrag: resolveTarget(opts.whileDrag, variants, void 0, custom),
|
|
1491
|
+
exit: resolveTarget(opts.exit, variants, asVariantLabels(parentVariantCtx.exit?.()), custom)
|
|
1492
|
+
};
|
|
1493
|
+
});
|
|
1494
|
+
const winners = createMemo(() => {
|
|
1495
|
+
const targets = stateTargets();
|
|
1496
|
+
const dragEnabled = Boolean(getOpts().drag);
|
|
1497
|
+
const out = {};
|
|
1498
|
+
for (const stateName of PRIORITY_HIGH_TO_LOW) {
|
|
1499
|
+
if (!isStateActive(stateName, active, parentVariantCtx)) continue;
|
|
1500
|
+
const target = targets[stateName];
|
|
1501
|
+
if (!target) continue;
|
|
1502
|
+
for (const key in target) {
|
|
1503
|
+
if (key === "transition") continue;
|
|
1504
|
+
if (key in out) continue;
|
|
1505
|
+
if (!active.exit && dragEnabled && (key === "x" || key === "y")) continue;
|
|
1506
|
+
out[key] = {
|
|
1507
|
+
value: target[key],
|
|
1508
|
+
transition: target.transition,
|
|
1509
|
+
stateName
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
return out;
|
|
1514
|
+
});
|
|
1515
|
+
let prevControls = null;
|
|
1516
|
+
let lastApplied = {};
|
|
1517
|
+
let isFirstRun = true;
|
|
1518
|
+
let pendingExitResolvers = [];
|
|
1519
|
+
function drainPendingExitResolvers() {
|
|
1520
|
+
const resolvers = pendingExitResolvers;
|
|
1521
|
+
pendingExitResolvers = [];
|
|
1522
|
+
for (const r of resolvers) r();
|
|
1523
|
+
}
|
|
1524
|
+
createEffect(() => {
|
|
1525
|
+
const next = winners();
|
|
1526
|
+
const opts = getOpts();
|
|
1527
|
+
if (isFirstRun && enterReady && !enterReady()) return;
|
|
1528
|
+
let skipAnimate = false;
|
|
1529
|
+
if (isFirstRun && (untrack(() => opts.initial) === false || suppressFirstMount)) {
|
|
1530
|
+
lastApplied = snapshotValues(next);
|
|
1531
|
+
skipAnimate = true;
|
|
1532
|
+
}
|
|
1533
|
+
isFirstRun = false;
|
|
1534
|
+
if (!skipAnimate && Object.keys(next).length === 0 && Object.keys(lastApplied).length === 0 && opts.animate === void 0 && parentVariantCtx.animate?.() === void 0) return;
|
|
1535
|
+
const changes = {};
|
|
1536
|
+
let mergedPerTargetTransition;
|
|
1537
|
+
if (!skipAnimate) {
|
|
1538
|
+
for (const key in next) {
|
|
1539
|
+
const entry = next[key];
|
|
1540
|
+
if (!entry) continue;
|
|
1541
|
+
if (lastApplied[key] !== entry.value) {
|
|
1542
|
+
changes[key] = entry.value;
|
|
1543
|
+
mergedPerTargetTransition ??= entry.transition;
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
for (const key in lastApplied) {
|
|
1547
|
+
if (key in next) continue;
|
|
1548
|
+
const initialValue = initialTarget && key in initialTarget ? initialTarget[key] : void 0;
|
|
1549
|
+
changes[key] = initialValue !== void 0 ? initialValue : getMotionDefault(key);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
const reduced = shouldReduceMotion(motionConfig.reducedMotion(), systemReducedMotion());
|
|
1553
|
+
const transition = mergeTransition(motionConfig.transition(), opts.transition, mergedPerTargetTransition, reduced);
|
|
1554
|
+
const driverState = highestActiveDriverState(next);
|
|
1555
|
+
const effectiveAnimateValue = animateValueForState(driverState, opts, parentVariantCtx);
|
|
1556
|
+
const buildAnimateOptions = () => ({
|
|
1557
|
+
...transition,
|
|
1558
|
+
onPlay: opts.onAnimationStart ? () => untrack(() => opts.onAnimationStart?.()) : void 0,
|
|
1559
|
+
onComplete: opts.onAnimationComplete ? () => untrack(() => {
|
|
1560
|
+
if (effectiveAnimateValue != null) opts.onAnimationComplete?.(effectiveAnimateValue);
|
|
1561
|
+
}) : void 0,
|
|
1562
|
+
onStop: opts.onAnimationCancel ? () => untrack(() => opts.onAnimationCancel?.()) : void 0,
|
|
1563
|
+
onUpdate: opts.onUpdate ? (latest) => untrack(() => opts.onUpdate?.(latest)) : void 0
|
|
1564
|
+
});
|
|
1565
|
+
if (!skipAnimate && Object.keys(changes).length > 0) {
|
|
1566
|
+
lastApplied = {
|
|
1567
|
+
...lastApplied,
|
|
1568
|
+
...changes
|
|
1569
|
+
};
|
|
1570
|
+
for (const key in lastApplied) if (!(key in next) && !(key in changes)) delete lastApplied[key];
|
|
1571
|
+
const { plain } = splitTarget(changes);
|
|
1572
|
+
const routed = [];
|
|
1573
|
+
const waaPlain = {};
|
|
1574
|
+
for (const key in plain) {
|
|
1575
|
+
const value = plain[key];
|
|
1576
|
+
const fallback = initialTarget && key in initialTarget ? initialTarget[key] : getMotionDefault(key);
|
|
1577
|
+
const routedMV = getValueForAnimate?.(key, fallback);
|
|
1578
|
+
if (routedMV) routed.push({
|
|
1579
|
+
mv: routedMV,
|
|
1580
|
+
value
|
|
1581
|
+
});
|
|
1582
|
+
else waaPlain[key] = value;
|
|
1583
|
+
}
|
|
1584
|
+
prevControls?.stop();
|
|
1585
|
+
const animOpts = buildAnimateOptions();
|
|
1586
|
+
if (routed.length === 0) prevControls = animate$1(el, waaPlain, animOpts);
|
|
1587
|
+
else {
|
|
1588
|
+
const controls = [];
|
|
1589
|
+
for (const { mv, value } of routed) controls.push(animate$1(mv, value, animOpts));
|
|
1590
|
+
if (Object.keys(waaPlain).length > 0) controls.push(animate$1(el, waaPlain, animOpts));
|
|
1591
|
+
prevControls = aggregateControls(controls);
|
|
1592
|
+
}
|
|
1593
|
+
if (driverState === "exit") {
|
|
1594
|
+
const dispatched = prevControls;
|
|
1595
|
+
dispatched.then(() => {
|
|
1596
|
+
if (prevControls === dispatched) drainPendingExitResolvers();
|
|
1597
|
+
});
|
|
1598
|
+
}
|
|
1599
|
+
} else if (driverState === "exit") drainPendingExitResolvers();
|
|
1600
|
+
for (const key in next) {
|
|
1601
|
+
const entry = next[key];
|
|
1602
|
+
if (!entry) continue;
|
|
1603
|
+
if (isMotionValue$1(entry.value)) {
|
|
1604
|
+
const targetMV = entry.value;
|
|
1605
|
+
onCleanup(targetMV.on("change", (v) => {
|
|
1606
|
+
const fallback = initialTarget && key in initialTarget ? initialTarget[key] : getMotionDefault(key);
|
|
1607
|
+
const routedMV = getValueForAnimate?.(key, fallback);
|
|
1608
|
+
if (routedMV && routedMV !== targetMV) animate$1(routedMV, v, buildAnimateOptions());
|
|
1609
|
+
else animate$1(el, { [key]: v }, buildAnimateOptions());
|
|
1610
|
+
}));
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
});
|
|
1614
|
+
onCleanup(() => prevControls?.stop());
|
|
1615
|
+
function setActive(state, isActive) {
|
|
1616
|
+
setActiveStore(state, isActive);
|
|
1617
|
+
}
|
|
1618
|
+
/**
|
|
1619
|
+
* Phase 3 — Presence integration. Returns a Promise that resolves when the
|
|
1620
|
+
* NEXT exit-driven animate dispatched by the diff effect completes, OR
|
|
1621
|
+
* immediately if no exit target is configured (nothing to wait for).
|
|
1622
|
+
*
|
|
1623
|
+
* The typical caller is `createMotion`'s presence-registered `runExit`:
|
|
1624
|
+
* it flips `setActive("exit", true)` then awaits this. The diff effect
|
|
1625
|
+
* runs in the next microtask, dispatches the exit animation, and on its
|
|
1626
|
+
* completion drains the pending resolvers.
|
|
1627
|
+
*
|
|
1628
|
+
* Multiple concurrent waiters are supported — they all resolve from the
|
|
1629
|
+
* same animation's completion.
|
|
1630
|
+
*
|
|
1631
|
+
* Edge case: if the user reactively removes `opts.exit` AFTER this call
|
|
1632
|
+
* but before the effect runs, the resolver will still be drained the
|
|
1633
|
+
* next time exit drives a dispatch (or by the "no-animate but exit-
|
|
1634
|
+
* driven" branch in the effect).
|
|
1635
|
+
*/
|
|
1636
|
+
function onceExitComplete() {
|
|
1637
|
+
if (untrack(() => stateTargets().exit) === null) return Promise.resolve();
|
|
1638
|
+
return new Promise((resolve) => {
|
|
1639
|
+
pendingExitResolvers.push(resolve);
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
return {
|
|
1643
|
+
setActive,
|
|
1644
|
+
onceExitComplete
|
|
1645
|
+
};
|
|
1646
|
+
}
|
|
1647
|
+
/**
|
|
1648
|
+
* Q4 — a state is considered active if EITHER its own flag is set OR the
|
|
1649
|
+
* parent's VariantContext carries a label for it (the parent's gesture is
|
|
1650
|
+
* active and propagating). The parent slots are themselves active-gated in
|
|
1651
|
+
* `useMotion`'s `myVariantCtx`, so a defined return value here means the
|
|
1652
|
+
* parent's gesture really is firing right now.
|
|
1653
|
+
*
|
|
1654
|
+
* `animate` and `exit` are special — their inheritance happens through the
|
|
1655
|
+
* normal label-resolution path in `resolveTarget`, not through the active
|
|
1656
|
+
* flag. We treat `animate` as always-active (matches motion's
|
|
1657
|
+
* createTypeState(true)). `exit` is driven by the Presence context; the
|
|
1658
|
+
* flag-based check is fine.
|
|
1659
|
+
*/
|
|
1660
|
+
function isStateActive(state, active, parent) {
|
|
1661
|
+
if (active[state]) return true;
|
|
1662
|
+
switch (state) {
|
|
1663
|
+
case "whileHover": return parent.hover?.() !== void 0;
|
|
1664
|
+
case "whilePress": return parent.press?.() !== void 0;
|
|
1665
|
+
case "whileFocus": return parent.focus?.() !== void 0;
|
|
1666
|
+
case "whileInView": return parent.inView?.() !== void 0;
|
|
1667
|
+
case "whileDrag": return false;
|
|
1668
|
+
case "animate":
|
|
1669
|
+
case "exit": return false;
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
/** Convert a winners map into the flat value snapshot used by `lastApplied`. */
|
|
1673
|
+
function snapshotValues(winners) {
|
|
1674
|
+
const out = {};
|
|
1675
|
+
for (const key in winners) {
|
|
1676
|
+
const entry = winners[key];
|
|
1677
|
+
if (entry) out[key] = entry.value;
|
|
1678
|
+
}
|
|
1679
|
+
return out;
|
|
1680
|
+
}
|
|
1681
|
+
/**
|
|
1682
|
+
* Phase 1's splitTarget: separate MotionValue refs in a target from plain
|
|
1683
|
+
* values. Motion-vanilla `animate(el, target)` doesn't subscribe to MV refs
|
|
1684
|
+
* passed in target — we handle that bridge ourselves.
|
|
1685
|
+
*/
|
|
1686
|
+
function splitTarget(target) {
|
|
1687
|
+
const plain = {};
|
|
1688
|
+
const motionValues = [];
|
|
1689
|
+
for (const key in target) {
|
|
1690
|
+
const value = target[key];
|
|
1691
|
+
if (value === void 0 || value === null) {
|
|
1692
|
+
plain[key] = value;
|
|
1693
|
+
continue;
|
|
1694
|
+
}
|
|
1695
|
+
if (isMotionValue$1(value)) {
|
|
1696
|
+
motionValues.push({
|
|
1697
|
+
key,
|
|
1698
|
+
mv: value
|
|
1699
|
+
});
|
|
1700
|
+
plain[key] = value.get();
|
|
1701
|
+
} else if (typeof value === "function") plain[key] = value();
|
|
1702
|
+
else if (Array.isArray(value)) plain[key] = value.map((v) => {
|
|
1703
|
+
if (isMotionValue$1(v)) return v.get();
|
|
1704
|
+
if (typeof v === "function") return v();
|
|
1705
|
+
return v;
|
|
1706
|
+
});
|
|
1707
|
+
else plain[key] = value;
|
|
1708
|
+
}
|
|
1709
|
+
return {
|
|
1710
|
+
plain,
|
|
1711
|
+
motionValues
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
/**
|
|
1715
|
+
* Return the highest-priority active state that contributed any key in the
|
|
1716
|
+
* current winners map. Used to identify the "driver" for onAnimationComplete
|
|
1717
|
+
* (which receives the AnimateValue that drove the animation).
|
|
1718
|
+
*
|
|
1719
|
+
* `animate` is the fallback when no gesture is contributing — matches Phase 1's
|
|
1720
|
+
* effectiveAnimateValue semantic.
|
|
1721
|
+
*/
|
|
1722
|
+
function highestActiveDriverState(winners) {
|
|
1723
|
+
for (const stateName of PRIORITY_HIGH_TO_LOW) for (const key in winners) {
|
|
1724
|
+
const entry = winners[key];
|
|
1725
|
+
if (entry && entry.stateName === stateName) return stateName;
|
|
1726
|
+
}
|
|
1727
|
+
return "animate";
|
|
1728
|
+
}
|
|
1729
|
+
/**
|
|
1730
|
+
* Look up the AnimateValue (Target | string | string[]) that corresponds to a
|
|
1731
|
+
* given state — for onAnimationComplete's argument.
|
|
1732
|
+
*/
|
|
1733
|
+
function animateValueForState(state, opts, parentVariantCtx) {
|
|
1734
|
+
switch (state) {
|
|
1735
|
+
case "animate": return opts.animate ?? parentVariantCtx.animate?.();
|
|
1736
|
+
case "whileHover": return opts.hover ?? parentVariantCtx.hover?.();
|
|
1737
|
+
case "whilePress": return opts.press ?? parentVariantCtx.press?.();
|
|
1738
|
+
case "whileFocus": return opts.focus ?? parentVariantCtx.focus?.();
|
|
1739
|
+
case "whileInView": return opts.inView ?? parentVariantCtx.inView?.();
|
|
1740
|
+
case "exit": return opts.exit ?? parentVariantCtx.exit?.();
|
|
1741
|
+
case "whileDrag": return opts.whileDrag;
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
/**
|
|
1745
|
+
* Combine N AnimationPlaybackControls into a single Thenable+stoppable handle.
|
|
1746
|
+
*
|
|
1747
|
+
* Used by the Stage 3 bridge when an animate dispatch fans out across per-MV
|
|
1748
|
+
* `animate(mv, value, opts)` calls (one per routed key) plus an optional
|
|
1749
|
+
* single `animate(el, target, opts)` for keys still on the WAA path. The
|
|
1750
|
+
* gesture state machine treats `prevControls` as one handle: subsequent diff
|
|
1751
|
+
* runs call `.stop()` on it to cancel the in-flight animation, and the exit
|
|
1752
|
+
* drain awaits `.then(...)` to settle Presence's `onceExitComplete()` waiters.
|
|
1753
|
+
* Aggregating lets both code paths stay uniform whether bridging fired one
|
|
1754
|
+
* underlying motion call or six.
|
|
1755
|
+
*
|
|
1756
|
+
* The other AnimationPlaybackControls methods (pause/play/cancel/complete)
|
|
1757
|
+
* fan out unchanged. `time`/`speed`/`duration` aren't aggregated — they're
|
|
1758
|
+
* read-rare in our codebase and a meaningful aggregate isn't well-defined
|
|
1759
|
+
* across heterogeneous animations.
|
|
1760
|
+
*/
|
|
1761
|
+
function aggregateControls(controls) {
|
|
1762
|
+
let settled = null;
|
|
1763
|
+
const settle = () => {
|
|
1764
|
+
if (!settled) settled = Promise.all(controls.map((c) => c));
|
|
1765
|
+
return settled;
|
|
1766
|
+
};
|
|
1767
|
+
const forAll = (fn) => {
|
|
1768
|
+
for (const c of controls) fn(c);
|
|
1769
|
+
};
|
|
1770
|
+
return {
|
|
1771
|
+
stop: () => {
|
|
1772
|
+
forAll((c) => c.stop());
|
|
1773
|
+
},
|
|
1774
|
+
pause: () => {
|
|
1775
|
+
forAll((c) => c.pause());
|
|
1776
|
+
},
|
|
1777
|
+
play: () => {
|
|
1778
|
+
forAll((c) => c.play());
|
|
1779
|
+
},
|
|
1780
|
+
cancel: () => {
|
|
1781
|
+
forAll((c) => c.cancel());
|
|
1782
|
+
},
|
|
1783
|
+
complete: () => {
|
|
1784
|
+
forAll((c) => c.complete());
|
|
1785
|
+
},
|
|
1786
|
+
speed: 1,
|
|
1787
|
+
time: 0,
|
|
1788
|
+
duration: controls.reduce((acc, c) => Math.max(acc, c.duration ?? 0), 0),
|
|
1789
|
+
then: (onFulfilled, onRejected) => settle().then(onFulfilled, onRejected)
|
|
1790
|
+
};
|
|
1791
|
+
}
|
|
1792
|
+
//#endregion
|
|
1793
|
+
//#region src/primitives/value-registry.ts
|
|
1794
|
+
function createValueRegistry() {
|
|
1795
|
+
const values = /* @__PURE__ */ new Map();
|
|
1796
|
+
const transient = /* @__PURE__ */ new Set();
|
|
1797
|
+
return {
|
|
1798
|
+
get(key) {
|
|
1799
|
+
return values.get(key);
|
|
1800
|
+
},
|
|
1801
|
+
has(key) {
|
|
1802
|
+
return values.has(key);
|
|
1803
|
+
},
|
|
1804
|
+
setExternal(key, mv) {
|
|
1805
|
+
const existing = values.get(key);
|
|
1806
|
+
if (existing && transient.has(existing)) transient.delete(existing);
|
|
1807
|
+
values.set(key, mv);
|
|
1808
|
+
},
|
|
1809
|
+
getOrCreateTransient(key, fallback) {
|
|
1810
|
+
const existing = values.get(key);
|
|
1811
|
+
if (existing) return existing;
|
|
1812
|
+
const mv = motionValue$1(fallback);
|
|
1813
|
+
values.set(key, mv);
|
|
1814
|
+
transient.add(mv);
|
|
1815
|
+
return mv;
|
|
1816
|
+
},
|
|
1817
|
+
entries() {
|
|
1818
|
+
return values.entries();
|
|
1819
|
+
},
|
|
1820
|
+
get size() {
|
|
1821
|
+
return values.size;
|
|
1822
|
+
},
|
|
1823
|
+
dispose() {
|
|
1824
|
+
transient.clear();
|
|
1825
|
+
values.clear();
|
|
1826
|
+
}
|
|
1827
|
+
};
|
|
1828
|
+
}
|
|
1829
|
+
//#endregion
|
|
1830
|
+
//#region src/primitives/createMotion.ts
|
|
1831
|
+
/**
|
|
1832
|
+
* Detect whether an animate-value is a variant name (string or string[]) vs.
|
|
1833
|
+
* an explicit target object. Returns the labels or undefined.
|
|
1834
|
+
*/
|
|
1835
|
+
function asVariantLabels(value) {
|
|
1836
|
+
if (value === void 0) return void 0;
|
|
1837
|
+
if (typeof value === "string") return value;
|
|
1838
|
+
if (Array.isArray(value)) return value;
|
|
1839
|
+
}
|
|
1840
|
+
/**
|
|
1841
|
+
* Resolve a per-state animate value into a {@link Target}. Implements the
|
|
1842
|
+
* Q4 sub-2 priority table:
|
|
1843
|
+
*
|
|
1844
|
+
* - explicit Target object → use as-is (parent context ignored)
|
|
1845
|
+
* - variant name → look up in own variants only (no cascade)
|
|
1846
|
+
* - undefined → fall back to parent context's variant name, then look up in
|
|
1847
|
+
* own variants
|
|
1848
|
+
*/
|
|
1849
|
+
function resolveTarget(ownValue, ownVariants, parentLabel, custom) {
|
|
1850
|
+
if (ownValue !== void 0 && typeof ownValue !== "string" && !Array.isArray(ownValue)) return ownValue;
|
|
1851
|
+
const labels = effectiveLabels(ownValue, parentLabel);
|
|
1852
|
+
if (labels === void 0) return null;
|
|
1853
|
+
return resolveVariant(labels, ownVariants, custom);
|
|
1854
|
+
}
|
|
1855
|
+
/**
|
|
1856
|
+
* Merge transition specs in priority order: MotionConfig default <
|
|
1857
|
+
* user's `transition` < per-target `transition`. When reduced motion is
|
|
1858
|
+
* active, returns `{ duration: 0 }` and drops everything else (Q11 sub-4).
|
|
1859
|
+
*/
|
|
1860
|
+
function mergeTransition(configDefault, ownTransition, perTargetTransition, reduced) {
|
|
1861
|
+
if (reduced) return { duration: 0 };
|
|
1862
|
+
return {
|
|
1863
|
+
...configDefault ?? {},
|
|
1864
|
+
...ownTransition ?? {},
|
|
1865
|
+
...perTargetTransition ?? {}
|
|
1866
|
+
};
|
|
1867
|
+
}
|
|
1868
|
+
/**
|
|
1869
|
+
* Apply a static target to an element's inline style before paint. Used on
|
|
1870
|
+
* mount when no SSR style was emitted. The ref callback fires before the
|
|
1871
|
+
* browser yields, so this avoids a frame of flicker.
|
|
1872
|
+
*/
|
|
1873
|
+
function applyStaticStyle(el, target) {
|
|
1874
|
+
const style = targetToStyle(target);
|
|
1875
|
+
for (const key in style) {
|
|
1876
|
+
const value = style[key];
|
|
1877
|
+
if (value === void 0) continue;
|
|
1878
|
+
if (key.startsWith("--")) el.style.setProperty(key, String(value));
|
|
1879
|
+
else el.style[key] = value;
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
/**
|
|
1883
|
+
* The imperative primitive: bind an element to a reactive motion-options
|
|
1884
|
+
* source. Caller is responsible for keeping the element alive (refs in a
|
|
1885
|
+
* component, drag controls, etc.).
|
|
1886
|
+
*
|
|
1887
|
+
* Phase 1 scope: animate + initial + transition + lifecycle hooks +
|
|
1888
|
+
* reduced-motion override + presence registration. Phase 2 layers gesture
|
|
1889
|
+
* states (hover/press/focus/inView) and drag on top.
|
|
1890
|
+
*/
|
|
1891
|
+
function createMotion(el, getOpts, config) {
|
|
1892
|
+
const parentVariantCtx = config?.parentContext ?? useVariantContext();
|
|
1893
|
+
const presence = usePresenceContext();
|
|
1894
|
+
const motionConfig = useMotionConfig();
|
|
1895
|
+
const systemReducedMotion = createReducedMotion();
|
|
1896
|
+
let valueRegistry;
|
|
1897
|
+
const ensureRegistry = () => {
|
|
1898
|
+
if (!valueRegistry) valueRegistry = createValueRegistry();
|
|
1899
|
+
return valueRegistry;
|
|
1900
|
+
};
|
|
1901
|
+
const initialOpts = untrack(getOpts);
|
|
1902
|
+
let capturedInitialTarget = null;
|
|
1903
|
+
if (initialOpts.initial !== false) {
|
|
1904
|
+
const inheritedInitial = parentVariantCtx.initial?.();
|
|
1905
|
+
const inheritedAnimate = parentVariantCtx.animate?.();
|
|
1906
|
+
const effective = initialOpts.initial !== void 0 ? initialOpts.initial : inheritedInitial !== void 0 ? inheritedInitial : initialOpts.animate !== void 0 ? initialOpts.animate : inheritedAnimate;
|
|
1907
|
+
if (effective !== void 0) capturedInitialTarget = resolveTarget(effective, initialOpts.variants, void 0, initialOpts.custom ?? parentVariantCtx.custom?.());
|
|
1908
|
+
}
|
|
1909
|
+
const suppressFirstMount = untrack(() => presence.initial?.()) === false;
|
|
1910
|
+
if (!config?.initialAppliedBySSR && !suppressFirstMount && capturedInitialTarget) applyStaticStyle(el, capturedInitialTarget);
|
|
1911
|
+
const noop = () => {};
|
|
1912
|
+
let writer = noop;
|
|
1913
|
+
const writeFromRegistry = () => writer();
|
|
1914
|
+
const multiKeyWriter = () => {
|
|
1915
|
+
if (!valueRegistry) return;
|
|
1916
|
+
const target = {};
|
|
1917
|
+
for (const [k, mv] of valueRegistry.entries()) target[k] = mv.get();
|
|
1918
|
+
if (Object.keys(target).length === 0) return;
|
|
1919
|
+
applyStaticStyle(el, target);
|
|
1920
|
+
};
|
|
1921
|
+
const compileSingleKeyWriter = () => {
|
|
1922
|
+
const [key, mv] = valueRegistry.entries().next().value;
|
|
1923
|
+
if (TRANSFORM_KEYS.has(key)) {
|
|
1924
|
+
const formatter = pickTransformFormatter(key);
|
|
1925
|
+
if (formatter !== void 0) return () => {
|
|
1926
|
+
const v = snapshotValue(mv.get());
|
|
1927
|
+
if (v !== void 0) el.style.transform = formatter(v);
|
|
1928
|
+
};
|
|
1929
|
+
}
|
|
1930
|
+
if (key.startsWith("--")) return () => {
|
|
1931
|
+
const v = snapshotValue(mv.get());
|
|
1932
|
+
if (v !== void 0) el.style.setProperty(key, String(v));
|
|
1933
|
+
};
|
|
1934
|
+
return () => {
|
|
1935
|
+
const v = snapshotValue(mv.get());
|
|
1936
|
+
if (v === void 0) return;
|
|
1937
|
+
const formatted = formatProperty(key, v);
|
|
1938
|
+
el.style[key] = formatted;
|
|
1939
|
+
};
|
|
1940
|
+
};
|
|
1941
|
+
const refreshWriter = () => {
|
|
1942
|
+
const size = valueRegistry?.size ?? 0;
|
|
1943
|
+
if (size === 0) {
|
|
1944
|
+
writer = noop;
|
|
1945
|
+
return;
|
|
1946
|
+
}
|
|
1947
|
+
if (size === 1) {
|
|
1948
|
+
writer = compileSingleKeyWriter();
|
|
1949
|
+
return;
|
|
1950
|
+
}
|
|
1951
|
+
writer = multiKeyWriter;
|
|
1952
|
+
};
|
|
1953
|
+
let bridgeActive = false;
|
|
1954
|
+
if (config?.styleMotionValues && config.styleMotionValues.size > 0) {
|
|
1955
|
+
const registry = ensureRegistry();
|
|
1956
|
+
for (const [key, mv] of config.styleMotionValues) {
|
|
1957
|
+
registry.setExternal(key, mv);
|
|
1958
|
+
onCleanup(mv.on("change", writeFromRegistry));
|
|
1959
|
+
}
|
|
1960
|
+
bridgeActive = true;
|
|
1961
|
+
}
|
|
1962
|
+
if (config?.styleStaticTransforms && config.styleStaticTransforms.size > 0) bridgeActive = true;
|
|
1963
|
+
if (bridgeActive && capturedInitialTarget) {
|
|
1964
|
+
const registry = ensureRegistry();
|
|
1965
|
+
for (const key in capturedInitialTarget) {
|
|
1966
|
+
if (key === "transition") continue;
|
|
1967
|
+
if (!TRANSFORM_KEYS.has(key)) continue;
|
|
1968
|
+
if (registry.has(key)) continue;
|
|
1969
|
+
const raw = capturedInitialTarget[key];
|
|
1970
|
+
const snapshot = snapshotValue(raw);
|
|
1971
|
+
if (snapshot === void 0) continue;
|
|
1972
|
+
onCleanup(registry.getOrCreateTransient(key, snapshot).on("change", writeFromRegistry));
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
if (bridgeActive && config?.styleStaticTransforms) {
|
|
1976
|
+
const registry = ensureRegistry();
|
|
1977
|
+
for (const [key, value] of config.styleStaticTransforms) {
|
|
1978
|
+
const existing = registry.get(key);
|
|
1979
|
+
if (existing) existing.set(value);
|
|
1980
|
+
else onCleanup(registry.getOrCreateTransient(key, value).on("change", writeFromRegistry));
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
if (bridgeActive) {
|
|
1984
|
+
refreshWriter();
|
|
1985
|
+
writeFromRegistry();
|
|
1986
|
+
}
|
|
1987
|
+
const getValueForAnimate = (key, fallback) => {
|
|
1988
|
+
if (!bridgeActive) return void 0;
|
|
1989
|
+
const registry = ensureRegistry();
|
|
1990
|
+
const existing = registry.get(key);
|
|
1991
|
+
if (existing) return existing;
|
|
1992
|
+
if (!TRANSFORM_KEYS.has(key)) return void 0;
|
|
1993
|
+
const mv = registry.getOrCreateTransient(key, fallback);
|
|
1994
|
+
onCleanup(mv.on("change", writeFromRegistry));
|
|
1995
|
+
refreshWriter();
|
|
1996
|
+
return mv;
|
|
1997
|
+
};
|
|
1998
|
+
const inPresence = presence.registerEnter !== void 0;
|
|
1999
|
+
const [enterReady, setEnterReady] = createSignal(!inPresence);
|
|
2000
|
+
if (inPresence && presence.registerEnter) {
|
|
2001
|
+
presence.registerEnter(el, () => setEnterReady(true));
|
|
2002
|
+
queueMicrotask(() => {
|
|
2003
|
+
if (el.isConnected) setEnterReady(true);
|
|
2004
|
+
});
|
|
2005
|
+
}
|
|
2006
|
+
const { setActive, onceExitComplete } = createGestureStateMachine({
|
|
2007
|
+
el,
|
|
2008
|
+
getOpts,
|
|
2009
|
+
parentVariantCtx,
|
|
2010
|
+
motionConfig,
|
|
2011
|
+
systemReducedMotion,
|
|
2012
|
+
initialTarget: capturedInitialTarget,
|
|
2013
|
+
externalActiveStore: config?.activeStore,
|
|
2014
|
+
suppressFirstMount,
|
|
2015
|
+
enterReady,
|
|
2016
|
+
getValueForAnimate
|
|
2017
|
+
});
|
|
2018
|
+
const inheritedExitLabel = untrack(() => parentVariantCtx.exit?.());
|
|
2019
|
+
const hasOwnExit = initialOpts.exit !== void 0;
|
|
2020
|
+
const hasCascadedExit = inheritedExitLabel !== void 0 && initialOpts.variants !== void 0;
|
|
2021
|
+
if (hasOwnExit || hasCascadedExit) {
|
|
2022
|
+
const runExit = async () => {
|
|
2023
|
+
const opts = untrack(getOpts);
|
|
2024
|
+
const exitTarget = resolveTarget(opts.exit, opts.variants, asVariantLabels(untrack(() => parentVariantCtx.exit?.())), opts.custom ?? parentVariantCtx.custom?.());
|
|
2025
|
+
if (!exitTarget) {
|
|
2026
|
+
setActive("exit", true);
|
|
2027
|
+
await onceExitComplete();
|
|
2028
|
+
return;
|
|
2029
|
+
}
|
|
2030
|
+
const reduced = shouldReduceMotion(motionConfig.reducedMotion(), systemReducedMotion());
|
|
2031
|
+
const transition = mergeTransition(motionConfig.transition(), opts.transition, exitTarget.transition, reduced);
|
|
2032
|
+
const animTarget = {};
|
|
2033
|
+
for (const k in exitTarget) if (k !== "transition") animTarget[k] = exitTarget[k];
|
|
2034
|
+
await animate$1(el, animTarget, transition);
|
|
2035
|
+
};
|
|
2036
|
+
presence.register(el, runExit);
|
|
2037
|
+
}
|
|
2038
|
+
createGestures(el, getOpts, setActive);
|
|
2039
|
+
if (el instanceof HTMLElement) createDrag(el, getOpts, setActive);
|
|
2040
|
+
}
|
|
2041
|
+
//#endregion
|
|
2042
|
+
//#region src/use-motion.tsx
|
|
2043
|
+
/**
|
|
2044
|
+
* Wire motion to an element via a getter function.
|
|
2045
|
+
*
|
|
2046
|
+
* ```tsx
|
|
2047
|
+
* const motion = useMotion({
|
|
2048
|
+
* initial: { opacity: 0, y: 20 },
|
|
2049
|
+
* animate: { opacity: 1, y: 0 },
|
|
2050
|
+
* transition: { duration: 0.6 },
|
|
2051
|
+
* })
|
|
2052
|
+
*
|
|
2053
|
+
* <div {...motion({ class: "card" })}>Hello</div>
|
|
2054
|
+
* ```
|
|
2055
|
+
*
|
|
2056
|
+
* **Reactive form**: pass a function to track signals.
|
|
2057
|
+
* ```tsx
|
|
2058
|
+
* useMotion(() => ({ animate: { x: x() } }))
|
|
2059
|
+
* ```
|
|
2060
|
+
*
|
|
2061
|
+
* **Variant context propagation**: `useMotion` only *consumes* the parent
|
|
2062
|
+
* variant context. To propagate to descendants, wrap them in `motion.Provider`:
|
|
2063
|
+
* ```tsx
|
|
2064
|
+
* const m = useMotion({ animate: "visible", variants })
|
|
2065
|
+
* <div {...m()}>
|
|
2066
|
+
* <m.Provider>
|
|
2067
|
+
* <ChildMotion />
|
|
2068
|
+
* </m.Provider>
|
|
2069
|
+
* </div>
|
|
2070
|
+
* ```
|
|
2071
|
+
*
|
|
2072
|
+
* For the common "JSX wrapper does propagation automatically" pattern, use
|
|
2073
|
+
* `<motion.div>` (Phase 4).
|
|
2074
|
+
*/
|
|
2075
|
+
function useMotion(opts) {
|
|
2076
|
+
const getOpts = typeof opts === "function" ? opts : () => opts;
|
|
2077
|
+
const actualParentCtx = useVariantContext();
|
|
2078
|
+
const isControlling = () => isControllingVariants(getOpts());
|
|
2079
|
+
const parentVariantCtx = {
|
|
2080
|
+
variants: () => isControlling() ? void 0 : actualParentCtx.variants?.(),
|
|
2081
|
+
initial: () => isControlling() ? void 0 : actualParentCtx.initial?.(),
|
|
2082
|
+
animate: () => isControlling() ? void 0 : actualParentCtx.animate?.(),
|
|
2083
|
+
hover: () => isControlling() ? void 0 : actualParentCtx.hover?.(),
|
|
2084
|
+
press: () => isControlling() ? void 0 : actualParentCtx.press?.(),
|
|
2085
|
+
focus: () => isControlling() ? void 0 : actualParentCtx.focus?.(),
|
|
2086
|
+
inView: () => isControlling() ? void 0 : actualParentCtx.inView?.(),
|
|
2087
|
+
exit: () => isControlling() ? void 0 : actualParentCtx.exit?.(),
|
|
2088
|
+
custom: () => isControlling() ? void 0 : actualParentCtx.custom?.(),
|
|
2089
|
+
transition: () => isControlling() ? void 0 : actualParentCtx.transition?.()
|
|
2090
|
+
};
|
|
2091
|
+
const presenceCtx = usePresenceContext();
|
|
2092
|
+
const initialTarget = computeInitialTarget(untrack(getOpts), parentVariantCtx, presenceCtx.initial);
|
|
2093
|
+
const activeStore = createStore({
|
|
2094
|
+
animate: true,
|
|
2095
|
+
whileInView: false,
|
|
2096
|
+
whileHover: false,
|
|
2097
|
+
whilePress: false,
|
|
2098
|
+
whileFocus: false,
|
|
2099
|
+
whileDrag: false,
|
|
2100
|
+
exit: false
|
|
2101
|
+
});
|
|
2102
|
+
const [active] = activeStore;
|
|
2103
|
+
let styleMotionValues;
|
|
2104
|
+
let styleStaticTransforms;
|
|
2105
|
+
let styleCaptured = false;
|
|
2106
|
+
const motionRef = (el) => {
|
|
2107
|
+
createMotion(el, getOpts, {
|
|
2108
|
+
initialAppliedBySSR: initialTarget !== null || styleMotionValues !== void 0 || styleStaticTransforms !== void 0,
|
|
2109
|
+
activeStore,
|
|
2110
|
+
parentContext: parentVariantCtx,
|
|
2111
|
+
styleMotionValues,
|
|
2112
|
+
styleStaticTransforms
|
|
2113
|
+
});
|
|
2114
|
+
};
|
|
2115
|
+
let renderedOnce = false;
|
|
2116
|
+
onMount(() => {
|
|
2117
|
+
renderedOnce = true;
|
|
2118
|
+
});
|
|
2119
|
+
/**
|
|
2120
|
+
* Walk `style` once and pull `MotionValue` refs into `styleMotionValues`.
|
|
2121
|
+
* Idempotent across re-renders — Stage 2's contract is "MV refs in style
|
|
2122
|
+
* are captured on first call and never re-scraped." Subsequent m()
|
|
2123
|
+
* invocations that pass a different style with new MVs won't pick them
|
|
2124
|
+
* up; that pattern wasn't in scope for v0.1.
|
|
2125
|
+
*
|
|
2126
|
+
* The read is `untrack`ed because m() is typically called from inside a
|
|
2127
|
+
* JSX spread, which Solid evaluates within a tracked owner. Without
|
|
2128
|
+
* untrack we'd subscribe to whatever signals the user's `style` object
|
|
2129
|
+
* references and re-fire this useMotion's owner-level effects on every
|
|
2130
|
+
* change.
|
|
2131
|
+
*/
|
|
2132
|
+
const captureStyleEntries = (style) => {
|
|
2133
|
+
if (styleCaptured) return;
|
|
2134
|
+
styleCaptured = true;
|
|
2135
|
+
if (!style || typeof style !== "object") return;
|
|
2136
|
+
for (const key in style) {
|
|
2137
|
+
const value = style[key];
|
|
2138
|
+
if (isMotionValue$1(value)) {
|
|
2139
|
+
if (!styleMotionValues) styleMotionValues = /* @__PURE__ */ new Map();
|
|
2140
|
+
styleMotionValues.set(key, value);
|
|
2141
|
+
} else if (TRANSFORM_KEYS.has(key)) {
|
|
2142
|
+
const snap = snapshotValue(value);
|
|
2143
|
+
if (snap !== void 0) {
|
|
2144
|
+
if (!styleStaticTransforms) styleStaticTransforms = /* @__PURE__ */ new Map();
|
|
2145
|
+
styleStaticTransforms.set(key, snap);
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
};
|
|
2150
|
+
/**
|
|
2151
|
+
* Produce a style object with MV-valued keys (and transform-shortcut keys —
|
|
2152
|
+
* see below) removed. Solid's style binding would otherwise either write the
|
|
2153
|
+
* MotionValue instance as a literal (coercing it via String() to
|
|
2154
|
+
* "[object Object]") for MV-valued entries, or apply transform shortcuts
|
|
2155
|
+
* directly as bogus CSS properties for static-shortcut entries. createMotion
|
|
2156
|
+
* handles both via the registry-write path; we strip them here so the
|
|
2157
|
+
* Solid-bound `cleaned` style only contains regular CSS keys.
|
|
2158
|
+
*/
|
|
2159
|
+
const stripStyleEntriesOwnedByRegistry = (style) => {
|
|
2160
|
+
if (!style) return {};
|
|
2161
|
+
const out = {};
|
|
2162
|
+
for (const key in style) {
|
|
2163
|
+
if (styleMotionValues?.has(key)) continue;
|
|
2164
|
+
if (TRANSFORM_KEYS.has(key)) continue;
|
|
2165
|
+
out[key] = style[key];
|
|
2166
|
+
}
|
|
2167
|
+
return out;
|
|
2168
|
+
};
|
|
2169
|
+
/**
|
|
2170
|
+
* Stage 4 — compose the first-paint inline style from:
|
|
2171
|
+
* 1. `initialTarget` (resolved via the priority chain at construction)
|
|
2172
|
+
* 2. MotionValue snapshots from `style: { key: mv }`
|
|
2173
|
+
* 3. Static transform shortcuts in `style: { x: 10, scale: 0.5 }`
|
|
2174
|
+
*
|
|
2175
|
+
* Style entries (2, 3) override `initialTarget` (1) on the same key because
|
|
2176
|
+
* `style` is the runtime source-of-truth for those keys. Returns the composed
|
|
2177
|
+
* `JSX.CSSProperties` or null when nothing applies (no initial + no style
|
|
2178
|
+
* registry contributions).
|
|
2179
|
+
*
|
|
2180
|
+
* Called only before `onMount` flips `renderedOnce`. After mount, the
|
|
2181
|
+
* registry's writer (in createMotion) owns el.style directly and this
|
|
2182
|
+
* function isn't consulted.
|
|
2183
|
+
*/
|
|
2184
|
+
const composeFirstPaintStyle = (userStyle) => {
|
|
2185
|
+
const merged = {};
|
|
2186
|
+
let hasAny = false;
|
|
2187
|
+
if (initialTarget) {
|
|
2188
|
+
Object.assign(merged, initialTarget);
|
|
2189
|
+
hasAny = true;
|
|
2190
|
+
}
|
|
2191
|
+
if (styleMotionValues) for (const [key, mv] of styleMotionValues) {
|
|
2192
|
+
merged[key] = mv.get();
|
|
2193
|
+
hasAny = true;
|
|
2194
|
+
}
|
|
2195
|
+
if (userStyle) for (const key in userStyle) {
|
|
2196
|
+
if (styleMotionValues?.has(key)) continue;
|
|
2197
|
+
if (!TRANSFORM_KEYS.has(key)) continue;
|
|
2198
|
+
const v = userStyle[key];
|
|
2199
|
+
if (typeof v === "number" || typeof v === "string") {
|
|
2200
|
+
merged[key] = v;
|
|
2201
|
+
hasAny = true;
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
return hasAny ? targetToStyle(merged) : null;
|
|
2205
|
+
};
|
|
2206
|
+
function getProps(userProps) {
|
|
2207
|
+
untrack(() => captureStyleEntries(userProps?.style));
|
|
2208
|
+
const wroteFirstPaintStyle = initialTarget !== null || styleMotionValues !== void 0 || styleStaticTransforms !== void 0;
|
|
2209
|
+
return mergeProps$1(userProps ?? {}, {
|
|
2210
|
+
get style() {
|
|
2211
|
+
const cleaned = stripStyleEntriesOwnedByRegistry(userProps?.style);
|
|
2212
|
+
if (renderedOnce) return cleaned;
|
|
2213
|
+
const composed = composeFirstPaintStyle(userProps?.style);
|
|
2214
|
+
return composed ? {
|
|
2215
|
+
...cleaned,
|
|
2216
|
+
...composed
|
|
2217
|
+
} : cleaned;
|
|
2218
|
+
},
|
|
2219
|
+
ref: mergeRefs(userProps?.ref, motionRef),
|
|
2220
|
+
...wroteFirstPaintStyle ? { "data-motion-hydrated": "" } : {}
|
|
2221
|
+
});
|
|
2222
|
+
}
|
|
2223
|
+
const myVariantCtx = {
|
|
2224
|
+
variants: () => getOpts().variants,
|
|
2225
|
+
initial: () => {
|
|
2226
|
+
const v = getOpts().initial;
|
|
2227
|
+
return v === false ? void 0 : asVariantLabels(v);
|
|
2228
|
+
},
|
|
2229
|
+
animate: () => asVariantLabels(getOpts().animate),
|
|
2230
|
+
hover: () => active.whileHover ? asVariantLabels(getOpts().hover) : void 0,
|
|
2231
|
+
press: () => active.whilePress ? asVariantLabels(getOpts().press) : void 0,
|
|
2232
|
+
focus: () => active.whileFocus ? asVariantLabels(getOpts().focus) : void 0,
|
|
2233
|
+
inView: () => active.whileInView ? asVariantLabels(getOpts().inView) : void 0,
|
|
2234
|
+
exit: () => asVariantLabels(getOpts().exit),
|
|
2235
|
+
custom: () => getOpts().custom,
|
|
2236
|
+
transition: () => getOpts().transition
|
|
2237
|
+
};
|
|
2238
|
+
const Provider = (props) => createComponent(VariantContext.Provider, {
|
|
2239
|
+
value: myVariantCtx,
|
|
2240
|
+
get children() {
|
|
2241
|
+
return props.children;
|
|
2242
|
+
}
|
|
2243
|
+
});
|
|
2244
|
+
return Object.assign(getProps, { Provider });
|
|
2245
|
+
}
|
|
2246
|
+
function computeInitialTarget(opts, parentVariantCtx, presenceInitial) {
|
|
2247
|
+
if (presenceInitial?.() === false) {
|
|
2248
|
+
const animateValue = opts.animate !== void 0 ? opts.animate : parentVariantCtx.animate?.();
|
|
2249
|
+
if (animateValue === void 0) return null;
|
|
2250
|
+
return resolveTarget(animateValue, opts.variants, void 0, opts.custom ?? parentVariantCtx.custom?.());
|
|
2251
|
+
}
|
|
2252
|
+
if (opts.initial === false) return null;
|
|
2253
|
+
const inheritedInitial = parentVariantCtx.initial?.();
|
|
2254
|
+
const inheritedAnimate = parentVariantCtx.animate?.();
|
|
2255
|
+
const effective = opts.initial !== void 0 ? opts.initial : inheritedInitial !== void 0 ? inheritedInitial : opts.animate !== void 0 ? opts.animate : inheritedAnimate;
|
|
2256
|
+
if (effective === void 0) return null;
|
|
2257
|
+
return resolveTarget(effective, opts.variants, void 0, opts.custom ?? parentVariantCtx.custom?.());
|
|
2258
|
+
}
|
|
2259
|
+
//#endregion
|
|
2260
|
+
//#region src/motion-proxy.tsx
|
|
2261
|
+
/**
|
|
2262
|
+
* Frozen list of `MotionOptKey`s — fed to `splitProps` at every
|
|
2263
|
+
* tag-component render to separate motion options from element
|
|
2264
|
+
* attributes. The `satisfies` clause checks every entry against
|
|
2265
|
+
* `MotionOptKey` at compile time so typos / drift between the union
|
|
2266
|
+
* and the array surface as errors.
|
|
2267
|
+
*/
|
|
2268
|
+
var MOTION_OPT_KEYS = [
|
|
2269
|
+
"initial",
|
|
2270
|
+
"animate",
|
|
2271
|
+
"exit",
|
|
2272
|
+
"hover",
|
|
2273
|
+
"press",
|
|
2274
|
+
"focus",
|
|
2275
|
+
"inView",
|
|
2276
|
+
"inViewOptions",
|
|
2277
|
+
"drag",
|
|
2278
|
+
"dragConstraints",
|
|
2279
|
+
"dragElastic",
|
|
2280
|
+
"dragMomentum",
|
|
2281
|
+
"dragTransition",
|
|
2282
|
+
"dragSnapToOrigin",
|
|
2283
|
+
"dragControls",
|
|
2284
|
+
"whileDrag",
|
|
2285
|
+
"panThreshold",
|
|
2286
|
+
"variants",
|
|
2287
|
+
"custom",
|
|
2288
|
+
"transition",
|
|
2289
|
+
"onAnimationStart",
|
|
2290
|
+
"onAnimationComplete",
|
|
2291
|
+
"onAnimationCancel",
|
|
2292
|
+
"onUpdate",
|
|
2293
|
+
"onHoverStart",
|
|
2294
|
+
"onHoverEnd",
|
|
2295
|
+
"onPressStart",
|
|
2296
|
+
"onPress",
|
|
2297
|
+
"onPressCancel",
|
|
2298
|
+
"onFocus",
|
|
2299
|
+
"onBlur",
|
|
2300
|
+
"onPanStart",
|
|
2301
|
+
"onPan",
|
|
2302
|
+
"onPanEnd",
|
|
2303
|
+
"onViewportEnter",
|
|
2304
|
+
"onViewportLeave",
|
|
2305
|
+
"onDragStart",
|
|
2306
|
+
"onDrag",
|
|
2307
|
+
"onDragEnd",
|
|
2308
|
+
"onDragTransitionEnd"
|
|
2309
|
+
];
|
|
2310
|
+
var tagComponentCache = /* @__PURE__ */ new Map();
|
|
2311
|
+
var motionComponents = /* @__PURE__ */ new WeakSet();
|
|
2312
|
+
function makeMotionTag(tag) {
|
|
2313
|
+
const cached = tagComponentCache.get(tag);
|
|
2314
|
+
if (cached) return cached;
|
|
2315
|
+
const Tag = (props) => {
|
|
2316
|
+
const [motionOpts, rest] = splitProps(props, MOTION_OPT_KEYS);
|
|
2317
|
+
const m = useMotion(() => motionOpts);
|
|
2318
|
+
return createComponent(m.Provider, { get children() {
|
|
2319
|
+
return createComponent(Dynamic, mergeProps({ component: tag }, () => m(rest)));
|
|
2320
|
+
} });
|
|
2321
|
+
};
|
|
2322
|
+
const stored = Tag;
|
|
2323
|
+
tagComponentCache.set(tag, stored);
|
|
2324
|
+
motionComponents.add(stored);
|
|
2325
|
+
return stored;
|
|
2326
|
+
}
|
|
2327
|
+
/**
|
|
2328
|
+
* `motion.create(Component)` — wraps a custom Component with motion's
|
|
2329
|
+
* behavior. The wrapped Component must forward props to a single DOM
|
|
2330
|
+
* element root; the contract is documented in the {@link Motion.create}
|
|
2331
|
+
* JSDoc above and enforced at runtime (in dev mode) by detecting whether
|
|
2332
|
+
* motion's ref ever reaches the DOM after mount.
|
|
2333
|
+
*/
|
|
2334
|
+
function motionCreate(Component) {
|
|
2335
|
+
if (process.env.NODE_ENV !== "production" && motionComponents.has(Component)) console.warn("[solidjs-motion] motion.create(motion.X) double-wraps the same element with two motion state machines. Compose options on a single layer instead.");
|
|
2336
|
+
const Wrapped = (props) => {
|
|
2337
|
+
const [motionOpts, rest] = splitProps(props, MOTION_OPT_KEYS);
|
|
2338
|
+
const m = useMotion(() => motionOpts);
|
|
2339
|
+
let refFired = false;
|
|
2340
|
+
const detector = (_el) => {
|
|
2341
|
+
refFired = true;
|
|
2342
|
+
};
|
|
2343
|
+
const userRef = rest.ref;
|
|
2344
|
+
const mergedUserRef = process.env.NODE_ENV !== "production" ? mergeRefs(userRef, detector) : userRef;
|
|
2345
|
+
const restWithDetector = process.env.NODE_ENV !== "production" ? mergeProps$1(rest, { ref: mergedUserRef }) : rest;
|
|
2346
|
+
if (process.env.NODE_ENV !== "production") onMount(() => {
|
|
2347
|
+
queueMicrotask(() => {
|
|
2348
|
+
if (!refFired) console.warn("[solidjs-motion] motion.create wrapped a Component whose root didn't receive motion's ref. The wrapped Component must either spread {...props} on a single DOM element OR explicitly forward `props.ref` to its root. Motion's animations and exit registration won't run until this is fixed.");
|
|
2349
|
+
});
|
|
2350
|
+
});
|
|
2351
|
+
return createComponent(m.Provider, { get children() {
|
|
2352
|
+
return createComponent(Component, mergeProps(() => m(restWithDetector)));
|
|
2353
|
+
} });
|
|
2354
|
+
};
|
|
2355
|
+
motionComponents.add(Wrapped);
|
|
2356
|
+
return Wrapped;
|
|
2357
|
+
}
|
|
2358
|
+
/**
|
|
2359
|
+
* `motion` — the indexable proxy. Every property access returns a cached
|
|
2360
|
+
* motion-aware component for the given HTML/SVG tag. The reserved
|
|
2361
|
+
* `motion.create` key returns the HOC entry point.
|
|
2362
|
+
*
|
|
2363
|
+
* @example HTML element
|
|
2364
|
+
* ```tsx
|
|
2365
|
+
* <motion.div animate={{ x: 100 }} hover={{ scale: 1.05 }}>
|
|
2366
|
+
* draggable card
|
|
2367
|
+
* </motion.div>
|
|
2368
|
+
* ```
|
|
2369
|
+
*
|
|
2370
|
+
* @example SVG element (handled transparently via <Dynamic>)
|
|
2371
|
+
* ```tsx
|
|
2372
|
+
* <motion.path d="M0 0 L100 100" animate={{ pathLength: 1 }} />
|
|
2373
|
+
* ```
|
|
2374
|
+
*
|
|
2375
|
+
* @example Wrapping a custom Component via the HOC
|
|
2376
|
+
* ```tsx
|
|
2377
|
+
* const Animated = motion.create(MyCard)
|
|
2378
|
+
* <Animated animate={{ scale: 1.05 }} class="my-card" />
|
|
2379
|
+
* ```
|
|
2380
|
+
*
|
|
2381
|
+
* Non-string keys (Symbols, well-known properties) return `undefined` so
|
|
2382
|
+
* debugging tools and `typeof` checks see a sane shape.
|
|
2383
|
+
*/
|
|
2384
|
+
var motion = new Proxy({}, { get(_target, key) {
|
|
2385
|
+
if (typeof key !== "string") return void 0;
|
|
2386
|
+
if (key === "create") return motionCreate;
|
|
2387
|
+
return makeMotionTag(key);
|
|
2388
|
+
} });
|
|
2389
|
+
//#endregion
|
|
2390
|
+
//#region src/presence.tsx
|
|
2391
|
+
/**
|
|
2392
|
+
* Internal helper: maps the public `mode` prop to the value
|
|
2393
|
+
* `createSwitchTransition` expects. `popLayout` is intentionally deferred
|
|
2394
|
+
* (layout animations are v0.2+); `wait` maps to `out-in`; `sync` is the
|
|
2395
|
+
* default and corresponds to transition-group's `parallel`.
|
|
2396
|
+
*/
|
|
2397
|
+
function switchMode(mode) {
|
|
2398
|
+
return mode === "wait" ? "out-in" : "parallel";
|
|
2399
|
+
}
|
|
2400
|
+
/**
|
|
2401
|
+
* Find every motion child registered under `root` (or root itself) and
|
|
2402
|
+
* return their [element, runExit] pairs. Walks the `runExits` map and
|
|
2403
|
+
* tests containment via `Node.contains`, which is O(depth) per check —
|
|
2404
|
+
* cheap because n is bounded by the number of motion children Presence
|
|
2405
|
+
* is tracking. Order isn't load-bearing; callers Promise.all the runExits.
|
|
2406
|
+
*/
|
|
2407
|
+
function collectSubtreeExits(root, runExits) {
|
|
2408
|
+
const out = [];
|
|
2409
|
+
for (const [el, fn] of runExits) if (el === root || root.contains(el)) out.push([el, fn]);
|
|
2410
|
+
return out;
|
|
2411
|
+
}
|
|
2412
|
+
/**
|
|
2413
|
+
* Wraps a conditional or iterated JSX subtree and runs the descendants'
|
|
2414
|
+
* `exit` targets before they unmount. Matches motion-react's
|
|
2415
|
+
* `<AnimatePresence>` shape but with Solid's `<Show>` / `<For>` /
|
|
2416
|
+
* `<Index>` patterns instead of conditional children.
|
|
2417
|
+
*
|
|
2418
|
+
* Nested motion children are first-class: when an ancestor unmounts,
|
|
2419
|
+
* Presence walks the subtree from each resolved child and fires every
|
|
2420
|
+
* registered `runExit` it finds in parallel — including motion children
|
|
2421
|
+
* nested inside plain wrappers, or descendants whose `exit` label was
|
|
2422
|
+
* cascaded down via `m.Provider`. Each motion descendant animates with
|
|
2423
|
+
* its own variant/target; transition-group only releases the DOM once
|
|
2424
|
+
* the combined `Promise.all` settles. Mirrors motion-react's behavior
|
|
2425
|
+
* where a `<motion.div exit={...}>` inside an `<AnimatePresence>` boundary
|
|
2426
|
+
* animates correctly regardless of depth.
|
|
2427
|
+
*
|
|
2428
|
+
* @example Single (conditional unmount)
|
|
2429
|
+
* <Presence>
|
|
2430
|
+
* <Show when={open()}>
|
|
2431
|
+
* <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}>
|
|
2432
|
+
* saved
|
|
2433
|
+
* </motion.div>
|
|
2434
|
+
* </Show>
|
|
2435
|
+
* </Presence>
|
|
2436
|
+
*
|
|
2437
|
+
* @example List (items entering and exiting independently)
|
|
2438
|
+
* <Presence>
|
|
2439
|
+
* <For each={items()}>
|
|
2440
|
+
* {(item) => (
|
|
2441
|
+
* <motion.li exit={{ opacity: 0, x: 20 }}>{item.text}</motion.li>
|
|
2442
|
+
* )}
|
|
2443
|
+
* </For>
|
|
2444
|
+
* </Presence>
|
|
2445
|
+
*/
|
|
2446
|
+
var Presence = (props) => {
|
|
2447
|
+
const runExits = /* @__PURE__ */ new Map();
|
|
2448
|
+
const runEnters = /* @__PURE__ */ new Map();
|
|
2449
|
+
const [presenceInitial, setPresenceInitial] = createSignal(props.initial ?? true);
|
|
2450
|
+
queueMicrotask(() => setPresenceInitial(true));
|
|
2451
|
+
const ctx = {
|
|
2452
|
+
register: (el, runExit) => {
|
|
2453
|
+
runExits.set(el, runExit);
|
|
2454
|
+
},
|
|
2455
|
+
unregister: (el) => {
|
|
2456
|
+
runExits.delete(el);
|
|
2457
|
+
},
|
|
2458
|
+
beforeUnmount: (el) => {
|
|
2459
|
+
const exiting = collectSubtreeExits(el, runExits);
|
|
2460
|
+
if (exiting.length === 0) return Promise.resolve();
|
|
2461
|
+
return Promise.all(exiting.map((pair) => pair[1]())).then(() => {
|
|
2462
|
+
for (const [exitedEl] of exiting) runExits.delete(exitedEl);
|
|
2463
|
+
});
|
|
2464
|
+
},
|
|
2465
|
+
registerEnter: (el, runEnter) => {
|
|
2466
|
+
runEnters.set(el, runEnter);
|
|
2467
|
+
},
|
|
2468
|
+
beforeMount: (el) => {
|
|
2469
|
+
const fn = runEnters.get(el);
|
|
2470
|
+
runEnters.delete(el);
|
|
2471
|
+
fn?.();
|
|
2472
|
+
},
|
|
2473
|
+
initial: presenceInitial
|
|
2474
|
+
};
|
|
2475
|
+
return createComponent(PresenceContext.Provider, {
|
|
2476
|
+
value: ctx,
|
|
2477
|
+
get children() {
|
|
2478
|
+
return createComponent(PresenceCore, {
|
|
2479
|
+
source: () => props.children,
|
|
2480
|
+
get mode() {
|
|
2481
|
+
return props.mode;
|
|
2482
|
+
},
|
|
2483
|
+
get exitMethod() {
|
|
2484
|
+
return props.exitMethod;
|
|
2485
|
+
},
|
|
2486
|
+
appear: presenceInitial,
|
|
2487
|
+
ctx
|
|
2488
|
+
});
|
|
2489
|
+
}
|
|
2490
|
+
});
|
|
2491
|
+
};
|
|
2492
|
+
var PresenceCore = (p) => {
|
|
2493
|
+
const resolved = resolveElements(p.source);
|
|
2494
|
+
const path = createMemo((prev) => {
|
|
2495
|
+
if (prev) return prev;
|
|
2496
|
+
const v = resolved();
|
|
2497
|
+
if (v == null) return null;
|
|
2498
|
+
return Array.isArray(v) ? "list" : "switch";
|
|
2499
|
+
});
|
|
2500
|
+
if (process.env.NODE_ENV !== "production") createEffect(() => {
|
|
2501
|
+
if (path() === "list" && p.mode === "wait") console.warn("[solidjs-motion] <Presence mode=\"wait\"> has no meaningful effect with a list of children — \"wait\" sequences a single exiting element before a single entering one. Use it with `<Show>`-style conditional rendering.");
|
|
2502
|
+
});
|
|
2503
|
+
return createComponent(Switch, { get children() {
|
|
2504
|
+
return [createComponent(Match, {
|
|
2505
|
+
get when() {
|
|
2506
|
+
return path() === "switch";
|
|
2507
|
+
},
|
|
2508
|
+
keyed: true,
|
|
2509
|
+
children: (_v) => createSwitchTransition(() => {
|
|
2510
|
+
const v = resolved();
|
|
2511
|
+
return Array.isArray(v) ? v[0] ?? null : v;
|
|
2512
|
+
}, {
|
|
2513
|
+
appear: p.appear(),
|
|
2514
|
+
mode: switchMode(p.mode),
|
|
2515
|
+
onExit(el, done) {
|
|
2516
|
+
const motionEl = el;
|
|
2517
|
+
if (motionEl instanceof HTMLElement || motionEl instanceof SVGElement) motionEl.style.pointerEvents = "none";
|
|
2518
|
+
p.ctx.beforeUnmount(motionEl).then(done);
|
|
2519
|
+
},
|
|
2520
|
+
onEnter(el, done) {
|
|
2521
|
+
p.ctx.beforeMount?.(el);
|
|
2522
|
+
done();
|
|
2523
|
+
}
|
|
2524
|
+
})
|
|
2525
|
+
}), createComponent(Match, {
|
|
2526
|
+
get when() {
|
|
2527
|
+
return path() === "list";
|
|
2528
|
+
},
|
|
2529
|
+
keyed: true,
|
|
2530
|
+
children: (_v) => createListTransition(() => resolved.toArray(), {
|
|
2531
|
+
appear: p.appear(),
|
|
2532
|
+
exitMethod: p.exitMethod,
|
|
2533
|
+
onChange({ added, removed, finishRemoved }) {
|
|
2534
|
+
for (const el of added) p.ctx.beforeMount?.(el);
|
|
2535
|
+
if (removed.length === 0) return;
|
|
2536
|
+
for (const el of removed) if (el instanceof HTMLElement || el instanceof SVGElement) el.style.pointerEvents = "none";
|
|
2537
|
+
Promise.all(removed.map((el) => p.ctx.beforeUnmount(el))).then(() => finishRemoved(removed));
|
|
2538
|
+
}
|
|
2539
|
+
})
|
|
2540
|
+
})];
|
|
2541
|
+
} });
|
|
2542
|
+
};
|
|
2543
|
+
function useAnimatePresence(options) {
|
|
2544
|
+
const runExits = /* @__PURE__ */ new Map();
|
|
2545
|
+
const [presenceInitial, setPresenceInitial] = createSignal(options?.initial ?? true);
|
|
2546
|
+
queueMicrotask(() => setPresenceInitial(true));
|
|
2547
|
+
const ctx = {
|
|
2548
|
+
register: (el, runExit) => {
|
|
2549
|
+
runExits.set(el, runExit);
|
|
2550
|
+
},
|
|
2551
|
+
unregister: (el) => {
|
|
2552
|
+
runExits.delete(el);
|
|
2553
|
+
},
|
|
2554
|
+
beforeUnmount: (el) => {
|
|
2555
|
+
const exiting = collectSubtreeExits(el, runExits);
|
|
2556
|
+
if (exiting.length === 0) return Promise.resolve();
|
|
2557
|
+
return Promise.all(exiting.map((pair) => pair[1]())).then(() => {
|
|
2558
|
+
for (const [exitedEl] of exiting) runExits.delete(exitedEl);
|
|
2559
|
+
});
|
|
2560
|
+
},
|
|
2561
|
+
initial: presenceInitial
|
|
2562
|
+
};
|
|
2563
|
+
onCleanup(() => runExits.clear());
|
|
2564
|
+
const Provider = (p) => createComponent(PresenceContext.Provider, {
|
|
2565
|
+
value: ctx,
|
|
2566
|
+
get children() {
|
|
2567
|
+
return p.children;
|
|
2568
|
+
}
|
|
2569
|
+
});
|
|
2570
|
+
const exit = async () => {
|
|
2571
|
+
const snapshot = [...runExits.values()];
|
|
2572
|
+
await Promise.all(snapshot.map((fn) => fn()));
|
|
2573
|
+
};
|
|
2574
|
+
return {
|
|
2575
|
+
Provider,
|
|
2576
|
+
exit
|
|
2577
|
+
};
|
|
2578
|
+
}
|
|
2579
|
+
//#endregion
|
|
2580
|
+
//#region src/primitives/createScroll.ts
|
|
2581
|
+
/**
|
|
2582
|
+
* Bind four {@link MotionValueAccessor}s to a scroll source. Mirrors
|
|
2583
|
+
* motion/react's `useScroll`; defaults to the window when no container is
|
|
2584
|
+
* supplied. Each returned value is callable as a Solid Accessor AND has the
|
|
2585
|
+
* full MotionValue surface, so it composes with `useMotion`'s target,
|
|
2586
|
+
* `animate()`, `createTransform`, and direct JSX reactivity.
|
|
2587
|
+
*
|
|
2588
|
+
* @example
|
|
2589
|
+
* const { scrollY, scrollYProgress } = createScroll()
|
|
2590
|
+
* const opacity = createTransform(scrollYProgress, [0, 1], [1, 0])
|
|
2591
|
+
*
|
|
2592
|
+
* @example
|
|
2593
|
+
* const [el, setEl] = createSignal<HTMLElement>()
|
|
2594
|
+
* const { scrollY } = createScroll({ container: el })
|
|
2595
|
+
* <div ref={setEl} style={{ overflow: "auto" }}>...</div>
|
|
2596
|
+
*/
|
|
2597
|
+
function createScroll(options) {
|
|
2598
|
+
const scrollX = createMotionValue(0);
|
|
2599
|
+
const scrollY = createMotionValue(0);
|
|
2600
|
+
const scrollXProgress = createMotionValue(0);
|
|
2601
|
+
const scrollYProgress = createMotionValue(0);
|
|
2602
|
+
const handler = (_progress, info) => {
|
|
2603
|
+
if (!info) return;
|
|
2604
|
+
scrollX.set(info.x.current);
|
|
2605
|
+
scrollY.set(info.y.current);
|
|
2606
|
+
scrollXProgress.set(info.x.progress);
|
|
2607
|
+
scrollYProgress.set(info.y.progress);
|
|
2608
|
+
};
|
|
2609
|
+
createEffect(() => {
|
|
2610
|
+
onCleanup(scroll$1(handler, {
|
|
2611
|
+
container: options?.container?.() ?? void 0,
|
|
2612
|
+
target: options?.target?.() ?? void 0,
|
|
2613
|
+
axis: options?.axis,
|
|
2614
|
+
offset: options?.offset
|
|
2615
|
+
}));
|
|
2616
|
+
});
|
|
2617
|
+
return {
|
|
2618
|
+
scrollX,
|
|
2619
|
+
scrollY,
|
|
2620
|
+
scrollXProgress,
|
|
2621
|
+
scrollYProgress
|
|
2622
|
+
};
|
|
2623
|
+
}
|
|
2624
|
+
//#endregion
|
|
2625
|
+
export { MOTION_OPT_KEYS, MotionConfig, MotionConfigContext, Presence, PresenceContext, VariantContext, animate, createDragControls, createInView, createMotion, createMotionValue, createMotionValueEvent, createPan, createReducedMotion, createScroll, createSpring, createTemplate, createTime, createTransform, createVelocity, effectiveLabels, inView, isMotionValue, motion, motionValue, resolveVariant, scroll, shouldReduceMotion, spring, targetToStyle, toSignal, useAnimatePresence, useMotion, useMotionConfig, usePresenceContext, useVariantContext };
|
|
2626
|
+
|
|
2627
|
+
//# sourceMappingURL=index.js.map
|