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.
Files changed (49) hide show
  1. package/CHANGELOG.md +117 -0
  2. package/LICENSE +21 -0
  3. package/README.md +140 -0
  4. package/dist/index.d.ts +2 -0
  5. package/dist/index.js +2627 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/src/default-values.d.ts +6 -0
  8. package/dist/src/index.d.ts +16 -0
  9. package/dist/src/motion-config.d.ts +14 -0
  10. package/dist/src/motion-proxy.d.ts +103 -0
  11. package/dist/src/presence-context.d.ts +4 -0
  12. package/dist/src/presence.d.ts +95 -0
  13. package/dist/src/primitives/createDrag.d.ts +16 -0
  14. package/dist/src/primitives/createDragControls.d.ts +30 -0
  15. package/dist/src/primitives/createGestures.d.ts +8 -0
  16. package/dist/src/primitives/createInView.d.ts +51 -0
  17. package/dist/src/primitives/createMotion.d.ts +82 -0
  18. package/dist/src/primitives/createPan.d.ts +83 -0
  19. package/dist/src/primitives/createScroll.d.ts +40 -0
  20. package/dist/src/primitives/gesture-state.d.ts +108 -0
  21. package/dist/src/primitives/motion-value.d.ts +111 -0
  22. package/dist/src/primitives/value-registry.d.ts +34 -0
  23. package/dist/src/reduced-motion.d.ts +29 -0
  24. package/dist/src/style.d.ts +79 -0
  25. package/dist/src/types.d.ts +374 -0
  26. package/dist/src/use-motion.d.ts +35 -0
  27. package/dist/src/variants.d.ts +64 -0
  28. package/package.json +78 -0
  29. package/src/default-values.ts +52 -0
  30. package/src/index.ts +60 -0
  31. package/src/motion-config.tsx +37 -0
  32. package/src/motion-proxy.tsx +377 -0
  33. package/src/presence-context.ts +32 -0
  34. package/src/presence.tsx +466 -0
  35. package/src/primitives/createDrag.ts +670 -0
  36. package/src/primitives/createDragControls.ts +101 -0
  37. package/src/primitives/createGestures.ts +145 -0
  38. package/src/primitives/createInView.ts +124 -0
  39. package/src/primitives/createMotion.ts +638 -0
  40. package/src/primitives/createPan.ts +338 -0
  41. package/src/primitives/createScroll.ts +101 -0
  42. package/src/primitives/gesture-state.ts +772 -0
  43. package/src/primitives/motion-value.ts +328 -0
  44. package/src/primitives/value-registry.ts +114 -0
  45. package/src/reduced-motion.ts +51 -0
  46. package/src/style.ts +266 -0
  47. package/src/types.ts +538 -0
  48. package/src/use-motion.tsx +412 -0
  49. package/src/variants.ts +134 -0
package/src/style.ts ADDED
@@ -0,0 +1,266 @@
1
+ import { isMotionValue, type MotionValue } from "motion"
2
+ import type { JSX } from "solid-js"
3
+ import type { Target } from "./types"
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Tables (Q5 locked decisions)
7
+ // ---------------------------------------------------------------------------
8
+
9
+ /**
10
+ * Set of CSS shortcut keys motion treats as transform components. Re-used by
11
+ * `createMotion`'s Stage 3 animate bridge to decide whether an animate-target
12
+ * key should be routed through the value registry (composed via the writer's
13
+ * `el.style.transform =`) or sent down the existing WAA path.
14
+ */
15
+ export const TRANSFORM_KEYS = /* @__PURE__ */ new Set([
16
+ "x",
17
+ "y",
18
+ "z",
19
+ "scale",
20
+ "scaleX",
21
+ "scaleY",
22
+ "scaleZ",
23
+ "rotate",
24
+ "rotateX",
25
+ "rotateY",
26
+ "rotateZ",
27
+ "skew",
28
+ "skewX",
29
+ "skewY",
30
+ "transformPerspective",
31
+ ])
32
+
33
+ /** Order matters — motion composes transforms in this sequence (Q5 sub-1). */
34
+ const TRANSFORM_ORDER = [
35
+ "x",
36
+ "y",
37
+ "z",
38
+ "scale",
39
+ "scaleX",
40
+ "scaleY",
41
+ "scaleZ",
42
+ "rotate",
43
+ "rotateX",
44
+ "rotateY",
45
+ "rotateZ",
46
+ "skew",
47
+ "skewX",
48
+ "skewY",
49
+ "transformPerspective",
50
+ ] as const
51
+
52
+ const PX_PROPERTIES = /* @__PURE__ */ new Set([
53
+ "width",
54
+ "minWidth",
55
+ "maxWidth",
56
+ "height",
57
+ "minHeight",
58
+ "maxHeight",
59
+ "top",
60
+ "right",
61
+ "bottom",
62
+ "left",
63
+ "padding",
64
+ "paddingTop",
65
+ "paddingRight",
66
+ "paddingBottom",
67
+ "paddingLeft",
68
+ "paddingInline",
69
+ "paddingBlock",
70
+ "paddingInlineStart",
71
+ "paddingInlineEnd",
72
+ "paddingBlockStart",
73
+ "paddingBlockEnd",
74
+ "margin",
75
+ "marginTop",
76
+ "marginRight",
77
+ "marginBottom",
78
+ "marginLeft",
79
+ "marginInline",
80
+ "marginBlock",
81
+ "marginInlineStart",
82
+ "marginInlineEnd",
83
+ "marginBlockStart",
84
+ "marginBlockEnd",
85
+ "borderWidth",
86
+ "borderTopWidth",
87
+ "borderRightWidth",
88
+ "borderBottomWidth",
89
+ "borderLeftWidth",
90
+ "borderRadius",
91
+ "borderTopLeftRadius",
92
+ "borderTopRightRadius",
93
+ "borderBottomLeftRadius",
94
+ "borderBottomRightRadius",
95
+ "gap",
96
+ "rowGap",
97
+ "columnGap",
98
+ "fontSize",
99
+ "outlineWidth",
100
+ "outlineOffset",
101
+ ])
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // Snapshot — unwrap MotionValue / Accessor / keyframe-array to a leaf value.
105
+ // Returns `undefined` when the leaf is null or undefined; callers omit those.
106
+ // ---------------------------------------------------------------------------
107
+
108
+ type Leaf = string | number
109
+
110
+ /**
111
+ * Reduce a target-value (which may be raw, a MotionValue, an Accessor, or a
112
+ * keyframe array) to a concrete leaf value the writer can apply to the DOM
113
+ * or use to initialize a transient MotionValue. The cascade follows motion's
114
+ * own semantics:
115
+ *
116
+ * - `null` / `undefined` → `undefined` (caller drops the key)
117
+ * - keyframe array → first frame (consistent with motion-vanilla's
118
+ * initial-style snapshot)
119
+ * - `MotionValue` → its current `.get()`
120
+ * - `Accessor` (a bare zero-arg function) → its invocation result
121
+ * - primitive (string / number) → returned as-is
122
+ *
123
+ * Exported for the MV-in-style Stage 4 work: createMotion uses it to
124
+ * snapshot initial-target entries when registering them into the value
125
+ * registry as transient MVs.
126
+ */
127
+ export function snapshotValue(value: unknown): Leaf | undefined {
128
+ if (value === null || value === undefined) return undefined
129
+ if (Array.isArray(value)) return snapshotValue(value[0])
130
+ if (isMotionValue(value)) return snapshotValue((value as MotionValue<Leaf>).get())
131
+ if (typeof value === "function") return snapshotValue((value as () => unknown)())
132
+ if (typeof value === "string" || typeof value === "number") return value
133
+ // Anything else (objects, booleans) isn't a renderable CSS value — skip it.
134
+ return undefined
135
+ }
136
+
137
+ // ---------------------------------------------------------------------------
138
+ // Transform composition — produce a single `transform: ...` string from the
139
+ // shorthand keys present in the target.
140
+ // ---------------------------------------------------------------------------
141
+
142
+ /**
143
+ * Per-key transform formatter functions. Pre-built once at module load and
144
+ * shared across all elements. Used by `createMotion`'s specialized writer
145
+ * to avoid evaluating the transform-key switch on every single-key write —
146
+ * at Sierpinski-scale fan-out (thousands of writes per frame) the switch's
147
+ * 15 case-comparisons add up to non-trivial CPU.
148
+ *
149
+ * Pre-pick the formatter with `pickTransformFormatter(key)` ONCE at writer-
150
+ * compile time, then the per-call cost in the hot path is just `formatter(v)`.
151
+ */
152
+ type TransformFormatter = (value: Leaf) => string
153
+
154
+ const TRANSFORM_FORMATTERS: Readonly<Record<string, TransformFormatter>> = {
155
+ // Translate: number → "px", string passes through verbatim.
156
+ x: (v) => `translateX(${typeof v === "string" ? v : `${v}px`})`,
157
+ y: (v) => `translateY(${typeof v === "string" ? v : `${v}px`})`,
158
+ z: (v) => `translateZ(${typeof v === "string" ? v : `${v}px`})`,
159
+ // Scale: dimensionless. Skip the type check entirely.
160
+ scale: (v) => `scale(${v})`,
161
+ scaleX: (v) => `scaleX(${v})`,
162
+ scaleY: (v) => `scaleY(${v})`,
163
+ scaleZ: (v) => `scaleZ(${v})`,
164
+ // Rotate / skew: number → "deg", string passes through.
165
+ rotate: (v) => `rotate(${typeof v === "string" ? v : `${v}deg`})`,
166
+ rotateX: (v) => `rotateX(${typeof v === "string" ? v : `${v}deg`})`,
167
+ rotateY: (v) => `rotateY(${typeof v === "string" ? v : `${v}deg`})`,
168
+ rotateZ: (v) => `rotateZ(${typeof v === "string" ? v : `${v}deg`})`,
169
+ skew: (v) => `skew(${typeof v === "string" ? v : `${v}deg`})`,
170
+ skewX: (v) => `skewX(${typeof v === "string" ? v : `${v}deg`})`,
171
+ skewY: (v) => `skewY(${typeof v === "string" ? v : `${v}deg`})`,
172
+ transformPerspective: (v) => `perspective(${typeof v === "string" ? v : `${v}px`})`,
173
+ }
174
+
175
+ /**
176
+ * Look up the formatter for a transform-shortcut key. Returns `undefined`
177
+ * for non-transform keys; callers should check `TRANSFORM_KEYS.has(key)`
178
+ * before assuming a formatter exists.
179
+ */
180
+ export function pickTransformFormatter(key: string): TransformFormatter | undefined {
181
+ return TRANSFORM_FORMATTERS[key]
182
+ }
183
+
184
+ /**
185
+ * Format a motion transform-shortcut key + value as the corresponding CSS
186
+ * transform function string (e.g. `transformFunctionFor("scale", 1.05)`
187
+ * → `"scale(1.05)"`). One-shot variant — for hot paths, use
188
+ * `pickTransformFormatter(key)` once at compile time and reuse.
189
+ */
190
+ export function transformFunctionFor(key: string, value: Leaf): string {
191
+ return TRANSFORM_FORMATTERS[key]?.(value) ?? ""
192
+ }
193
+
194
+ // ---------------------------------------------------------------------------
195
+ // Property formatting — apply unit table for non-transform CSS properties.
196
+ // ---------------------------------------------------------------------------
197
+
198
+ /**
199
+ * Format a non-transform CSS property's value (e.g. `formatProperty("width", 100)`
200
+ * → `"100px"`, `formatProperty("opacity", 0.5)` → `0.5`). Applies motion's
201
+ * default-unit table (PX for dimensional CSS, dimensionless otherwise);
202
+ * leaves CSS variables alone. Exported for `createMotion`'s writer fast path.
203
+ */
204
+ export function formatProperty(key: string, value: Leaf): string | number {
205
+ if (typeof value === "string") return value
206
+ // CSS variables: stringify the number, never auto-unit (Q5 sub-4).
207
+ if (key.startsWith("--")) return String(value)
208
+ if (PX_PROPERTIES.has(key)) return `${value}px`
209
+ // Dimensionless or unknown — emit the bare number.
210
+ return value
211
+ }
212
+
213
+ // ---------------------------------------------------------------------------
214
+ // targetToStyle — the SSR/hydration linchpin. Pure, deterministic, no DOM
215
+ // reads, no time-dependent values, no input mutation. Same inputs → same
216
+ // outputs on server and client.
217
+ // ---------------------------------------------------------------------------
218
+
219
+ /**
220
+ * Convert a {@link Target} to a Solid {@link JSX.CSSProperties} object.
221
+ *
222
+ * - Composes transform shorthand (`x`, `y`, `scale`, `rotate`, etc.) into a
223
+ * single `transform: "..."` string in motion's canonical order.
224
+ * - Applies the default-unit table (px for dimensional CSS, deg for rotate/
225
+ * skew, dimensionless for scale/opacity/etc.).
226
+ * - For keyframe arrays, uses the first frame; a leading `null`/`undefined`
227
+ * keyframe omits the property entirely.
228
+ * - MotionValues and Solid Accessors are snapshotted at call time. Callers
229
+ * wrap in `untrack` if they don't want the read to subscribe.
230
+ * - Skips the `transition` key (animation config, not style).
231
+ * - CSS variables (`--foo`) emit raw values, no unit guess.
232
+ *
233
+ * @example
234
+ * targetToStyle({ x: 100, scale: 0.9, opacity: 0.5 })
235
+ * // { transform: "translateX(100px) scale(0.9)", opacity: 0.5 }
236
+ */
237
+ export function targetToStyle(target: Target): JSX.CSSProperties {
238
+ const out: Record<string, string | number> = {}
239
+ const transforms: Record<string, Leaf> = {}
240
+
241
+ for (const key in target) {
242
+ if (key === "transition") continue
243
+ const raw = target[key as keyof Target]
244
+ const snapshot = snapshotValue(raw)
245
+ if (snapshot === undefined) continue
246
+
247
+ if (TRANSFORM_KEYS.has(key)) {
248
+ transforms[key] = snapshot
249
+ } else {
250
+ out[key] = formatProperty(key, snapshot)
251
+ }
252
+ }
253
+
254
+ // Compose transform string in motion's canonical order.
255
+ const parts: string[] = []
256
+ for (const key of TRANSFORM_ORDER) {
257
+ if (key in transforms) {
258
+ parts.push(transformFunctionFor(key, transforms[key] as Leaf))
259
+ }
260
+ }
261
+ if (parts.length > 0) {
262
+ out.transform = parts.join(" ")
263
+ }
264
+
265
+ return out as JSX.CSSProperties
266
+ }