@humanspeak/svelte-motion 0.5.2 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/projection.context.d.ts +22 -0
- package/dist/components/projection.context.js +56 -0
- package/dist/html/_MotionContainer.svelte +148 -21
- package/dist/index.d.ts +1 -1
- package/dist/types.d.ts +59 -0
- package/dist/utils/drag.d.ts +42 -11
- package/dist/utils/drag.js +103 -12
- package/dist/utils/layout.d.ts +26 -2
- package/dist/utils/layout.js +13 -2
- package/dist/utils/projection.d.ts +287 -0
- package/dist/utils/projection.js +392 -0
- package/dist/utils/style.d.ts +23 -0
- package/dist/utils/style.js +27 -0
- package/dist/utils/variants.d.ts +26 -0
- package/dist/utils/variants.js +42 -0
- package/package.json +2 -2
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Projection layout system — minimal foundation for cross-element layout
|
|
3
|
+
* coordination during gestures (drag, layoutId, future shared-element).
|
|
4
|
+
*
|
|
5
|
+
* Direct port of the surface area framer-motion's projection system
|
|
6
|
+
* exposes to consumers, slimmed down to what we actually need for the
|
|
7
|
+
* acceptance criteria of #379:
|
|
8
|
+
*
|
|
9
|
+
* - Per-element `ProjectionNode`s with parent/child wiring through the
|
|
10
|
+
* Svelte component tree (via `projection.context.ts`).
|
|
11
|
+
* - `willUpdate()` / `didUpdate()` lifecycle around layout-mutating
|
|
12
|
+
* operations: caller snapshots before the mutation, re-measures
|
|
13
|
+
* after, fan-outs a `didUpdate` event with the computed delta.
|
|
14
|
+
* - Transform-stripping `measure()` — the load-bearing read primitive.
|
|
15
|
+
* A child's `getBoundingClientRect()` is contaminated by every
|
|
16
|
+
* ancestor's transform, so while measuring we temporarily reset the
|
|
17
|
+
* whole ancestor chain to each node's mount-time `baseTransform`,
|
|
18
|
+
* then restore in reverse order. Resetting to the captured base
|
|
19
|
+
* (rather than `'none'`) strips the motion-applied portion of each
|
|
20
|
+
* transform while preserving the user-authored one — mirroring
|
|
21
|
+
* framer-motion's `removeBoxTransforms`, which only subtracts
|
|
22
|
+
* motion-tracked `latestValues`. Generalises what
|
|
23
|
+
* `layout.ts:measureRect` does for the single-element case across
|
|
24
|
+
* the projection chain.
|
|
25
|
+
*
|
|
26
|
+
* Math helpers (`createBox`, `createDelta`, `calcBoxDelta`,
|
|
27
|
+
* `isDeltaZero`) are imported directly from `motion-dom` — no need to
|
|
28
|
+
* re-port what upstream already re-exports.
|
|
29
|
+
*
|
|
30
|
+
* KNOWN_LIMITATIONS (deferred to follow-up PRs, see #379):
|
|
31
|
+
* - No depth-sorted FlatTree / `path` array. We walk via `parent`
|
|
32
|
+
* pointers from leaves; siblings under one parent is sufficient for
|
|
33
|
+
* the Reorder use case and the rest of the projection tree workflows
|
|
34
|
+
* this PR enables.
|
|
35
|
+
* - No 4-phase tree walk (propagateDirty → resolveTarget →
|
|
36
|
+
* calcProjection → cleanDirty). Projection-transform *inheritance*
|
|
37
|
+
* (parent target deltas affecting child positioning) is not implemented
|
|
38
|
+
* yet. The ancestor-zeroing measure is independent of this — it's a
|
|
39
|
+
* read-time concern, not a projection-compose concern.
|
|
40
|
+
* - No scale-correction utilities (border-radius / box-shadow). Visual
|
|
41
|
+
* polish; defer.
|
|
42
|
+
* - No `relativeTarget` / `projectionDelta` with transform inheritance.
|
|
43
|
+
* Only matters for full shared-element morphing through nested
|
|
44
|
+
* transforms; current `layoutId.ts` registry handles the simple case.
|
|
45
|
+
* - No `layoutId` registry migration onto projection nodes. The
|
|
46
|
+
* one-shot pattern in `layoutId.ts` keeps working; future PR can
|
|
47
|
+
* route through projection nodes for richer coordination.
|
|
48
|
+
*/
|
|
49
|
+
import { measureRect } from './layout';
|
|
50
|
+
import { calcBoxDelta, createDelta, isDeltaZero } from 'motion-dom';
|
|
51
|
+
/**
|
|
52
|
+
* Convert a `DOMRect` to our `Box` shape. Inline because `motion-dom`'s
|
|
53
|
+
* `convertBoundingBoxToBox` works on a BoundingBox (already-derived
|
|
54
|
+
* `top`/`bottom`/`left`/`right`) rather than the DOMRect we get from
|
|
55
|
+
* `getBoundingClientRect`. Same math, just one less indirection.
|
|
56
|
+
*/
|
|
57
|
+
const rectToBox = (rect) => ({
|
|
58
|
+
x: { min: rect.left, max: rect.right },
|
|
59
|
+
y: { min: rect.top, max: rect.bottom }
|
|
60
|
+
});
|
|
61
|
+
/** Deep-copy a Box so subsequent measurements don't mutate snapshots. */
|
|
62
|
+
const cloneBox = (box) => ({
|
|
63
|
+
x: { min: box.x.min, max: box.x.max },
|
|
64
|
+
y: { min: box.y.min, max: box.y.max }
|
|
65
|
+
});
|
|
66
|
+
/**
|
|
67
|
+
* Per-element node in the projection tree. Created at component setup
|
|
68
|
+
* time in `_MotionContainer.svelte`, mounted when the element ref
|
|
69
|
+
* binds, unmounted on cleanup.
|
|
70
|
+
*
|
|
71
|
+
* Lifecycle:
|
|
72
|
+
* 1. `new ProjectionNode({ parent, getScrollContainers })` at setup.
|
|
73
|
+
* 2. `node.mount(element)` once the element ref binds.
|
|
74
|
+
* 3. `node.willUpdate()` before any layout-mutating state change (e.g.
|
|
75
|
+
* a `values` reassign that reorders DOM children).
|
|
76
|
+
* 4. State mutates → Svelte commits the DOM update.
|
|
77
|
+
* 5. `node.didUpdate()` after the DOM update is flushed — fires
|
|
78
|
+
* `didUpdate` listeners with the snapshot→current delta.
|
|
79
|
+
* 6. `node.unmount()` on cleanup.
|
|
80
|
+
*/
|
|
81
|
+
export class ProjectionNode {
|
|
82
|
+
/** The mounted element. `null` until `mount()` runs. */
|
|
83
|
+
element = null;
|
|
84
|
+
/**
|
|
85
|
+
* Parent node in the projection tree. Captured at construction
|
|
86
|
+
* from the Svelte context. Set to `null` for root-level motion
|
|
87
|
+
* elements that have no motion ancestor.
|
|
88
|
+
*/
|
|
89
|
+
parent = null;
|
|
90
|
+
/**
|
|
91
|
+
* Descendant nodes registered via `mount()`. Iterated when we
|
|
92
|
+
* need to broadcast to the subtree (none in this PR; reserved
|
|
93
|
+
* for follow-up work).
|
|
94
|
+
*/
|
|
95
|
+
children = new Set();
|
|
96
|
+
/** Most-recent post-mutation measurement, or `null` before first measure. */
|
|
97
|
+
latestLayout = null;
|
|
98
|
+
/**
|
|
99
|
+
* Pre-mutation snapshot captured by `willUpdate`. Cleared by
|
|
100
|
+
* `didUpdate` after the delta has been computed. Idempotent for
|
|
101
|
+
* repeat `willUpdate` calls in the same frame — only the first
|
|
102
|
+
* snapshots; subsequent calls no-op so a parent broadcasting
|
|
103
|
+
* `willUpdate` to its children doesn't clobber a child's own
|
|
104
|
+
* earlier snapshot.
|
|
105
|
+
*/
|
|
106
|
+
snapshot = null;
|
|
107
|
+
/** Whether `mount()` has been called and `unmount()` has not. */
|
|
108
|
+
isMounted = false;
|
|
109
|
+
/**
|
|
110
|
+
* Fallback user-authored base transform, captured from
|
|
111
|
+
* `element.style.transform` at `mount()` time. Used only when no
|
|
112
|
+
* `getBaseTransform` thunk was provided (e.g. unit tests that
|
|
113
|
+
* construct a bare node and set the transform before mounting).
|
|
114
|
+
*
|
|
115
|
+
* For real `motion.*` elements the `getBaseTransform` thunk is
|
|
116
|
+
* preferred: capturing at mount is unsafe because a transform-type
|
|
117
|
+
* `initial` keyframe is serialized into the inline `style.transform`
|
|
118
|
+
* BEFORE effects run, so the mount-time value can be a motion
|
|
119
|
+
* transform rather than the user's. `resolveBaseTransform()` picks
|
|
120
|
+
* the thunk first.
|
|
121
|
+
*
|
|
122
|
+
* `measure()` resets ancestors (and self, via `measureRect`) to this
|
|
123
|
+
* base rather than to `'none'`, removing the motion-applied portion
|
|
124
|
+
* while leaving the user-authored part intact — the same distinction
|
|
125
|
+
* framer-motion draws by only subtracting motion-tracked
|
|
126
|
+
* `latestValues` in `removeBoxTransforms`.
|
|
127
|
+
*/
|
|
128
|
+
baseTransform = '';
|
|
129
|
+
listeners = new Map();
|
|
130
|
+
getScrollContainers;
|
|
131
|
+
getBaseTransform;
|
|
132
|
+
constructor(options = {}) {
|
|
133
|
+
this.parent = options.parent ?? null;
|
|
134
|
+
this.getScrollContainers = options.getScrollContainers;
|
|
135
|
+
this.getBaseTransform = options.getBaseTransform;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The transform `measure()` resets this node's element to while
|
|
139
|
+
* reading. Prefers the `getBaseTransform` thunk (the user's authored
|
|
140
|
+
* `style` transform, motion-independent); falls back to the
|
|
141
|
+
* mount-captured `baseTransform`. See `getBaseTransform` /
|
|
142
|
+
* `baseTransform` for why the thunk is the safe source.
|
|
143
|
+
*/
|
|
144
|
+
resolveBaseTransform() {
|
|
145
|
+
return this.getBaseTransform?.() ?? this.baseTransform;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Register this node with its parent + bind to a DOM element.
|
|
149
|
+
* Idempotent — calling `mount()` twice on the same element is a
|
|
150
|
+
* no-op for the registration steps.
|
|
151
|
+
*
|
|
152
|
+
* Re-mounting onto a DIFFERENT element swaps the element in place
|
|
153
|
+
* WITHOUT a full `unmount()`. A full unmount would `children.clear()`,
|
|
154
|
+
* orphaning still-mounted descendants that registered themselves in
|
|
155
|
+
* this node's `children` (they keep their `parent` pointer but the
|
|
156
|
+
* set would never be repopulated). Listeners and children are
|
|
157
|
+
* therefore preserved across an element swap.
|
|
158
|
+
*
|
|
159
|
+
* @param element The DOM element this node represents.
|
|
160
|
+
*/
|
|
161
|
+
mount(element) {
|
|
162
|
+
if (this.isMounted && this.element === element)
|
|
163
|
+
return;
|
|
164
|
+
this.element = element;
|
|
165
|
+
// Fallback base capture (the consumer's getBaseTransform thunk is
|
|
166
|
+
// preferred — see `baseTransform`).
|
|
167
|
+
this.baseTransform = element.style.transform;
|
|
168
|
+
// Stale measurement from a previous element; refreshed on next measure.
|
|
169
|
+
this.latestLayout = null;
|
|
170
|
+
if (!this.isMounted) {
|
|
171
|
+
this.isMounted = true;
|
|
172
|
+
this.parent?.children.add(this);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Tear down. Detaches from parent, clears children references,
|
|
177
|
+
* drops all listeners. Safe to call on a never-mounted node and
|
|
178
|
+
* safe to call twice.
|
|
179
|
+
*/
|
|
180
|
+
unmount() {
|
|
181
|
+
if (!this.isMounted)
|
|
182
|
+
return;
|
|
183
|
+
this.parent?.children.delete(this);
|
|
184
|
+
this.children.clear();
|
|
185
|
+
this.listeners.clear();
|
|
186
|
+
this.element = null;
|
|
187
|
+
this.latestLayout = null;
|
|
188
|
+
this.snapshot = null;
|
|
189
|
+
this.baseTransform = '';
|
|
190
|
+
this.isMounted = false;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Read the element's layout box with every ancestor's
|
|
194
|
+
* motion-applied transform temporarily removed, while preserving
|
|
195
|
+
* each ancestor's user-authored base transform.
|
|
196
|
+
*
|
|
197
|
+
* Mechanism:
|
|
198
|
+
* 1. Walk `this.parent` chain bottom-up, collecting every
|
|
199
|
+
* mounted ancestor node (excludes self — `measureRect`
|
|
200
|
+
* handles self's transform internally).
|
|
201
|
+
* 2. Snapshot each ancestor's current `el.style.transform`.
|
|
202
|
+
* 3. Set each to its node's resolved base transform (the
|
|
203
|
+
* user-authored value, via `resolveBaseTransform`). This strips
|
|
204
|
+
* any FLIP/drag/initial transform while keeping the user's static
|
|
205
|
+
* one — see `getBaseTransform` / `baseTransform`.
|
|
206
|
+
* 4. Delegate to `measureRect(self.element, scrollContainers,
|
|
207
|
+
* self base)`, which applies self's base transform inside its own
|
|
208
|
+
* try/finally and returns the scroll-compensated DOMRect.
|
|
209
|
+
* 5. Restore ancestor transforms in reverse order inside a
|
|
210
|
+
* `finally` block — guarantees restoration even if measure
|
|
211
|
+
* throws.
|
|
212
|
+
* 6. Convert DOMRect → Box and cache as `latestLayout`.
|
|
213
|
+
*
|
|
214
|
+
* Returns `null` when `element` is not mounted.
|
|
215
|
+
*/
|
|
216
|
+
measure() {
|
|
217
|
+
if (!this.element)
|
|
218
|
+
return null;
|
|
219
|
+
// Collect ancestor nodes bottom-up. Skips ancestors that
|
|
220
|
+
// haven't bound yet (`element === null`).
|
|
221
|
+
const ancestors = [];
|
|
222
|
+
let cursor = this.parent;
|
|
223
|
+
while (cursor) {
|
|
224
|
+
if (cursor.element)
|
|
225
|
+
ancestors.push(cursor);
|
|
226
|
+
cursor = cursor.parent;
|
|
227
|
+
}
|
|
228
|
+
// Snapshot current transform; reset each ancestor to its
|
|
229
|
+
// user-authored base for the duration of the measure.
|
|
230
|
+
const restoreList = ancestors.map((node) => ({
|
|
231
|
+
el: node.element,
|
|
232
|
+
prev: node.element.style.transform,
|
|
233
|
+
base: node.resolveBaseTransform()
|
|
234
|
+
}));
|
|
235
|
+
try {
|
|
236
|
+
for (const { el, base } of restoreList)
|
|
237
|
+
el.style.transform = base;
|
|
238
|
+
// measureRect applies self's base transform + scroll-container offset.
|
|
239
|
+
const rect = measureRect(this.element, this.getScrollContainers?.() ?? [], this.resolveBaseTransform());
|
|
240
|
+
const box = rectToBox(rect);
|
|
241
|
+
this.latestLayout = box;
|
|
242
|
+
// Clone for the event: `box` aliases `this.latestLayout`, so a
|
|
243
|
+
// listener mutating it would corrupt the cached layout.
|
|
244
|
+
this.notify('measure', cloneBox(box));
|
|
245
|
+
return box;
|
|
246
|
+
}
|
|
247
|
+
finally {
|
|
248
|
+
// Reverse-order restore — important because ancestor
|
|
249
|
+
// composition cascades from outer-most down; restoring
|
|
250
|
+
// bottom-up matches the snapshot order.
|
|
251
|
+
for (let i = restoreList.length - 1; i >= 0; i--) {
|
|
252
|
+
restoreList[i].el.style.transform = restoreList[i].prev;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Snapshot the current layout box for use by the next
|
|
258
|
+
* `didUpdate()`. Caller's contract: invoke this BEFORE the
|
|
259
|
+
* layout-mutating state change so the snapshot reflects the
|
|
260
|
+
* pre-mutation position.
|
|
261
|
+
*
|
|
262
|
+
* Idempotent within a frame — once a snapshot exists, subsequent
|
|
263
|
+
* `willUpdate()` calls are no-ops until `didUpdate()` consumes it.
|
|
264
|
+
* This means a parent that broadcasts `willUpdate` to its
|
|
265
|
+
* children before its own snapshot is fine: children snapshot
|
|
266
|
+
* themselves first via their own willUpdate, parent's broadcast
|
|
267
|
+
* is a no-op.
|
|
268
|
+
*/
|
|
269
|
+
willUpdate() {
|
|
270
|
+
if (!this.element || this.snapshot)
|
|
271
|
+
return;
|
|
272
|
+
const measured = this.measure();
|
|
273
|
+
if (!measured)
|
|
274
|
+
return;
|
|
275
|
+
this.snapshot = cloneBox(measured);
|
|
276
|
+
// Clone for the event: emitting `this.snapshot` by reference would
|
|
277
|
+
// let a listener mutate the stored snapshot.
|
|
278
|
+
this.notify('willUpdate', cloneBox(this.snapshot));
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Re-measure post-mutation, compute the delta against the
|
|
282
|
+
* snapshot, fire `didUpdate` listeners. No-op when there's no
|
|
283
|
+
* snapshot (matches upstream — `willUpdate` MUST precede
|
|
284
|
+
* `didUpdate` for the cycle to fire).
|
|
285
|
+
*
|
|
286
|
+
* Always clears the snapshot at the end so the next gesture's
|
|
287
|
+
* `willUpdate`/`didUpdate` cycle starts fresh.
|
|
288
|
+
*/
|
|
289
|
+
didUpdate() {
|
|
290
|
+
if (!this.element || !this.snapshot) {
|
|
291
|
+
this.snapshot = null;
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const layout = this.measure();
|
|
295
|
+
if (!layout) {
|
|
296
|
+
this.snapshot = null;
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const delta = createDelta();
|
|
300
|
+
calcBoxDelta(delta, this.snapshot, layout);
|
|
301
|
+
const hasLayoutChanged = !isDeltaZero(delta);
|
|
302
|
+
const payload = {
|
|
303
|
+
// Clone the layout box: it aliases `this.latestLayout`, so
|
|
304
|
+
// handing the live reference out would let a listener mutate
|
|
305
|
+
// the node's cached layout and poison the next diff.
|
|
306
|
+
layout: cloneBox(layout),
|
|
307
|
+
snapshot: this.snapshot,
|
|
308
|
+
delta,
|
|
309
|
+
hasLayoutChanged
|
|
310
|
+
};
|
|
311
|
+
this.snapshot = null;
|
|
312
|
+
this.notify('didUpdate', payload);
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Observer-driven layout-change commit. Unlike the explicit
|
|
316
|
+
* `willUpdate()` → mutate → `didUpdate()` cycle (used when a
|
|
317
|
+
* consumer controls the exact mutation moment, e.g. Reorder.Group
|
|
318
|
+
* before a `values` reassign), this is for the reactive path where
|
|
319
|
+
* a layout change has ALREADY happened and we only learn about it
|
|
320
|
+
* after the fact (the existing `observeLayoutChanges` FLIP loop in
|
|
321
|
+
* `_MotionContainer`).
|
|
322
|
+
*
|
|
323
|
+
* Uses the cached `latestLayout` (the pre-change position from
|
|
324
|
+
* mount or the previous commit) as the snapshot, re-measures the
|
|
325
|
+
* post-change position, and fires `didUpdate` with the delta.
|
|
326
|
+
*
|
|
327
|
+
* First call after mount just seeds `latestLayout` (no prior
|
|
328
|
+
* position to diff against) and fires nothing.
|
|
329
|
+
*
|
|
330
|
+
* Returns the freshly-measured `Box` (or `null` when unmounted) so
|
|
331
|
+
* the caller can reuse it — the FLIP loop in `_MotionContainer` uses
|
|
332
|
+
* this as its `next` rect instead of measuring a second time per
|
|
333
|
+
* frame.
|
|
334
|
+
*
|
|
335
|
+
* The `didUpdate` fan-out is gated on a non-zero delta. The FLIP
|
|
336
|
+
* animation this commit runs alongside writes its own inverse
|
|
337
|
+
* `transform` to the element every frame, and those writes re-trigger
|
|
338
|
+
* the same `observeLayoutChanges` signal that drives this method.
|
|
339
|
+
* Those re-fires carry no real layout change (the ancestor-stripped
|
|
340
|
+
* measure is identical to the previous one), so without the
|
|
341
|
+
* `isDeltaZero` gate every animation frame would fan out a `delta: 0`
|
|
342
|
+
* event and clobber the genuine delta from the originating change.
|
|
343
|
+
*/
|
|
344
|
+
commitLayoutChange() {
|
|
345
|
+
if (!this.element)
|
|
346
|
+
return null;
|
|
347
|
+
const previous = this.latestLayout;
|
|
348
|
+
const layout = this.measure();
|
|
349
|
+
if (!layout)
|
|
350
|
+
return null;
|
|
351
|
+
if (previous) {
|
|
352
|
+
const delta = createDelta();
|
|
353
|
+
calcBoxDelta(delta, previous, layout);
|
|
354
|
+
// Skip the no-op re-fires from our own FLIP transform writes.
|
|
355
|
+
if (!isDeltaZero(delta)) {
|
|
356
|
+
this.notify('didUpdate', {
|
|
357
|
+
// Clone: `layout` aliases `this.latestLayout`.
|
|
358
|
+
layout: cloneBox(layout),
|
|
359
|
+
snapshot: previous,
|
|
360
|
+
delta,
|
|
361
|
+
hasLayoutChanged: true
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return layout;
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Subscribe to a projection event. Returns an unsubscribe
|
|
369
|
+
* function. Safe to call after `unmount()` (becomes a no-op).
|
|
370
|
+
*/
|
|
371
|
+
addEventListener(name, cb) {
|
|
372
|
+
let bucket = this.listeners.get(name);
|
|
373
|
+
if (!bucket) {
|
|
374
|
+
bucket = new Set();
|
|
375
|
+
this.listeners.set(name, bucket);
|
|
376
|
+
}
|
|
377
|
+
const wrapped = cb;
|
|
378
|
+
bucket.add(wrapped);
|
|
379
|
+
return () => {
|
|
380
|
+
this.listeners.get(name)?.delete(wrapped);
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
notify(name, payload) {
|
|
384
|
+
const bucket = this.listeners.get(name);
|
|
385
|
+
if (!bucket || bucket.size === 0)
|
|
386
|
+
return;
|
|
387
|
+
// Snapshot the bucket so unsubscribes inside a listener don't
|
|
388
|
+
// skip the next listener in the iteration.
|
|
389
|
+
for (const cb of [...bucket])
|
|
390
|
+
cb(payload);
|
|
391
|
+
}
|
|
392
|
+
}
|
package/dist/utils/style.d.ts
CHANGED
|
@@ -3,3 +3,26 @@
|
|
|
3
3
|
* This is used during SSR to reflect the initial state in server-rendered markup.
|
|
4
4
|
*/
|
|
5
5
|
export declare const mergeInlineStyles: (existingStyle: unknown, initial: Record<string, unknown> | null | undefined, animateFallback?: Record<string, unknown> | null | undefined) => string;
|
|
6
|
+
/**
|
|
7
|
+
* Extract the user-authored `transform` declaration from a `style` prop.
|
|
8
|
+
*
|
|
9
|
+
* Used by the projection system as the "base" transform a node resets to
|
|
10
|
+
* while measuring — the value the user wrote, independent of any
|
|
11
|
+
* motion-applied transform (`initial`/`animate`/FLIP/drag) that lands on
|
|
12
|
+
* `element.style.transform` after mount. Reading it from the `style` prop
|
|
13
|
+
* rather than the live inline style is what keeps an `initial={{ x }}` (or
|
|
14
|
+
* any transform-type initial/animate) from being mistaken for the base.
|
|
15
|
+
*
|
|
16
|
+
* Returns `''` when the prop is not a string or carries no transform.
|
|
17
|
+
*
|
|
18
|
+
* @param style The component's `style` prop.
|
|
19
|
+
* @returns The user's `transform` value, or `''`.
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* extractTransform('opacity: 0.5; transform: translateX(10px) scale(2)')
|
|
23
|
+
* // => 'translateX(10px) scale(2)'
|
|
24
|
+
* extractTransform('color: red') // => ''
|
|
25
|
+
* extractTransform({ color: 'red' }) // => '' (non-string)
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export declare const extractTransform: (style: unknown) => string;
|
package/dist/utils/style.js
CHANGED
|
@@ -117,6 +117,33 @@ export const mergeInlineStyles = (existingStyle, initial, animateFallback) => {
|
|
|
117
117
|
}
|
|
118
118
|
return stringifyStyleObject(base);
|
|
119
119
|
};
|
|
120
|
+
/**
|
|
121
|
+
* Extract the user-authored `transform` declaration from a `style` prop.
|
|
122
|
+
*
|
|
123
|
+
* Used by the projection system as the "base" transform a node resets to
|
|
124
|
+
* while measuring — the value the user wrote, independent of any
|
|
125
|
+
* motion-applied transform (`initial`/`animate`/FLIP/drag) that lands on
|
|
126
|
+
* `element.style.transform` after mount. Reading it from the `style` prop
|
|
127
|
+
* rather than the live inline style is what keeps an `initial={{ x }}` (or
|
|
128
|
+
* any transform-type initial/animate) from being mistaken for the base.
|
|
129
|
+
*
|
|
130
|
+
* Returns `''` when the prop is not a string or carries no transform.
|
|
131
|
+
*
|
|
132
|
+
* @param style The component's `style` prop.
|
|
133
|
+
* @returns The user's `transform` value, or `''`.
|
|
134
|
+
* @example
|
|
135
|
+
* ```ts
|
|
136
|
+
* extractTransform('opacity: 0.5; transform: translateX(10px) scale(2)')
|
|
137
|
+
* // => 'translateX(10px) scale(2)'
|
|
138
|
+
* extractTransform('color: red') // => ''
|
|
139
|
+
* extractTransform({ color: 'red' }) // => '' (non-string)
|
|
140
|
+
* ```
|
|
141
|
+
*/
|
|
142
|
+
export const extractTransform = (style) => {
|
|
143
|
+
if (typeof style !== 'string')
|
|
144
|
+
return '';
|
|
145
|
+
return parseStyleString(style).transform ?? '';
|
|
146
|
+
};
|
|
120
147
|
const parseStyleString = (style) => {
|
|
121
148
|
const out = {};
|
|
122
149
|
style
|
package/dist/utils/variants.d.ts
CHANGED
|
@@ -148,3 +148,29 @@ export declare const resolveExit: (exit: MotionExit, variants: Variants | undefi
|
|
|
148
148
|
* ```
|
|
149
149
|
*/
|
|
150
150
|
export declare const resolveWhile: (value: MotionWhileTap | MotionWhileHover | MotionWhileFocus | MotionWhileDrag | MotionWhileInView, variants: Variants | undefined, custom?: unknown) => DOMKeyframesDefinition | undefined;
|
|
151
|
+
/**
|
|
152
|
+
* Collapse each keyframe value to the value the element comes to REST at
|
|
153
|
+
* — the last element of a keyframe array, or the value itself otherwise.
|
|
154
|
+
*
|
|
155
|
+
* Used when deriving the post-animation inline style baseline: an
|
|
156
|
+
* `animate={{ x: [0, 100, 50] }}` settles at `50`, so the resting inline
|
|
157
|
+
* transform must reflect `50`, not the first keyframe. Mirrors
|
|
158
|
+
* framer-motion, whose `buildTransform` reads the motion value as a
|
|
159
|
+
* scalar that has already settled at the final keyframe
|
|
160
|
+
* (`motion-dom/.../build-transform.ts`).
|
|
161
|
+
*
|
|
162
|
+
* @param keyframes - Resolved animate keyframes (scalars and/or arrays),
|
|
163
|
+
* or `undefined`.
|
|
164
|
+
* @returns A new object with each value collapsed to its resting scalar,
|
|
165
|
+
* or `undefined` when given `undefined`. Keys whose value is an empty
|
|
166
|
+
* array are omitted (no resting value).
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* ```ts
|
|
170
|
+
* resolveRestingValues({ x: [0, 100, 50], scaleX: 1 }) // { x: 50, scaleX: 1 }
|
|
171
|
+
* resolveRestingValues({ opacity: 0.5 }) // { opacity: 0.5 }
|
|
172
|
+
* resolveRestingValues({ x: [], y: 5 }) // { y: 5 } (empty array dropped)
|
|
173
|
+
* resolveRestingValues(undefined) // undefined
|
|
174
|
+
* ```
|
|
175
|
+
*/
|
|
176
|
+
export declare const resolveRestingValues: (keyframes: DOMKeyframesDefinition | undefined) => DOMKeyframesDefinition | undefined;
|
package/dist/utils/variants.js
CHANGED
|
@@ -210,3 +210,45 @@ export const resolveWhile = (value, variants, custom) => {
|
|
|
210
210
|
return resolveVariantList(variants, value, custom);
|
|
211
211
|
return value;
|
|
212
212
|
};
|
|
213
|
+
/**
|
|
214
|
+
* Collapse each keyframe value to the value the element comes to REST at
|
|
215
|
+
* — the last element of a keyframe array, or the value itself otherwise.
|
|
216
|
+
*
|
|
217
|
+
* Used when deriving the post-animation inline style baseline: an
|
|
218
|
+
* `animate={{ x: [0, 100, 50] }}` settles at `50`, so the resting inline
|
|
219
|
+
* transform must reflect `50`, not the first keyframe. Mirrors
|
|
220
|
+
* framer-motion, whose `buildTransform` reads the motion value as a
|
|
221
|
+
* scalar that has already settled at the final keyframe
|
|
222
|
+
* (`motion-dom/.../build-transform.ts`).
|
|
223
|
+
*
|
|
224
|
+
* @param keyframes - Resolved animate keyframes (scalars and/or arrays),
|
|
225
|
+
* or `undefined`.
|
|
226
|
+
* @returns A new object with each value collapsed to its resting scalar,
|
|
227
|
+
* or `undefined` when given `undefined`. Keys whose value is an empty
|
|
228
|
+
* array are omitted (no resting value).
|
|
229
|
+
*
|
|
230
|
+
* @example
|
|
231
|
+
* ```ts
|
|
232
|
+
* resolveRestingValues({ x: [0, 100, 50], scaleX: 1 }) // { x: 50, scaleX: 1 }
|
|
233
|
+
* resolveRestingValues({ opacity: 0.5 }) // { opacity: 0.5 }
|
|
234
|
+
* resolveRestingValues({ x: [], y: 5 }) // { y: 5 } (empty array dropped)
|
|
235
|
+
* resolveRestingValues(undefined) // undefined
|
|
236
|
+
* ```
|
|
237
|
+
*/
|
|
238
|
+
export const resolveRestingValues = (keyframes) => {
|
|
239
|
+
if (keyframes === undefined)
|
|
240
|
+
return undefined;
|
|
241
|
+
const out = {};
|
|
242
|
+
for (const [key, value] of Object.entries(keyframes)) {
|
|
243
|
+
if (Array.isArray(value)) {
|
|
244
|
+
// An empty array has no resting value — omit the key rather than
|
|
245
|
+
// emitting `value[-1]` (undefined).
|
|
246
|
+
if (value.length > 0)
|
|
247
|
+
out[key] = value[value.length - 1];
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
out[key] = value;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return out;
|
|
254
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@humanspeak/svelte-motion",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"description": "Framer Motion for Svelte 5. Declarative motion.<tag> components with AnimatePresence exit animations, gestures (hover, tap, drag, focus, in-view), variants, FLIP layout animations, shared-layout transitions, spring physics, and scroll-linked motion values. The drop-in Framer Motion alternative for Svelte and SvelteKit.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"svelte",
|
|
@@ -119,7 +119,7 @@
|
|
|
119
119
|
"eslint": "^10.4.0",
|
|
120
120
|
"eslint-config-prettier": "10.1.8",
|
|
121
121
|
"eslint-plugin-import": "2.32.0",
|
|
122
|
-
"eslint-plugin-svelte": "3.
|
|
122
|
+
"eslint-plugin-svelte": "3.18.0",
|
|
123
123
|
"eslint-plugin-unused-imports": "4.4.1",
|
|
124
124
|
"esm-env": "^1.2.2",
|
|
125
125
|
"globals": "^17.6.0",
|