@humanspeak/svelte-motion 0.3.5 → 0.3.6
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/utils/drag.js +155 -48
- package/dist/utils/dragMath.d.ts +18 -0
- package/dist/utils/dragMath.js +21 -0
- package/dist/utils/inertia.js +4 -3
- package/dist/vite.js +176 -7
- package/package.json +58 -45
package/dist/utils/drag.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { pwLog, pwWarn } from './log';
|
|
1
|
+
import { isPlaywrightEnv, pwLog, pwWarn } from './log';
|
|
2
2
|
/**
|
|
3
3
|
* Drag utilities
|
|
4
4
|
*
|
|
@@ -16,7 +16,7 @@ import { pwLog, pwWarn } from './log';
|
|
|
16
16
|
* - For nested drags, set `propagation` as needed to avoid parent-child contention.
|
|
17
17
|
*/
|
|
18
18
|
import { isDomElement } from './dom';
|
|
19
|
-
import { applyConstraints as applyFloatConstraints } from './dragMath';
|
|
19
|
+
import { applyConstraints as applyFloatConstraints, parseMatrixTranslate } from './dragMath';
|
|
20
20
|
import { deriveBoundaryPhysics } from './dragParams';
|
|
21
21
|
import { computeHoverBaseline, splitHoverDefinition } from './hover';
|
|
22
22
|
import { createInertiaToBoundary } from './inertia';
|
|
@@ -87,6 +87,53 @@ export const applyElastic = (value, min, max, elastic) => {
|
|
|
87
87
|
};
|
|
88
88
|
/** Prefer high-resolution time in browser; fall back for SSR/tests. */
|
|
89
89
|
const now = () => (typeof performance !== 'undefined' ? performance.now() : Date.now());
|
|
90
|
+
/** Sample windows for release-velocity inference (matches motion-dom values). */
|
|
91
|
+
const MAX_VELOCITY_DELTA_MS = 30;
|
|
92
|
+
const MIN_VELOCITY_INTERVAL_MS = 5;
|
|
93
|
+
/**
|
|
94
|
+
* Compute the release velocity for momentum from a pointer-history window.
|
|
95
|
+
*
|
|
96
|
+
* Mirrors motion-dom: walks back from the newest sample, including only
|
|
97
|
+
* samples within `MAX_VELOCITY_DELTA_MS` (30 ms) of newest, then divides
|
|
98
|
+
* the displacement by the elapsed time. Returns 0 if the newest sample is
|
|
99
|
+
* stale, the window has fewer than two samples, or the oldest-newest span
|
|
100
|
+
* is shorter than `MIN_VELOCITY_INTERVAL_MS` (5 ms — sub-frame).
|
|
101
|
+
*
|
|
102
|
+
* @param history Recent pointer samples ordered oldest → newest. Each
|
|
103
|
+
* sample is `{ x, y, t }` where `t` is `performance.now()` ms.
|
|
104
|
+
* @param nowMs Current `performance.now()` ms — used to discard a stale
|
|
105
|
+
* newest sample (finger lifted after a pause).
|
|
106
|
+
* @returns Inferred release velocity in pixels per second on each axis.
|
|
107
|
+
* @example
|
|
108
|
+
* const v = computeReleaseVelocity(
|
|
109
|
+
* [{ x: 0, y: 0, t: 1000 }, { x: 20, y: 0, t: 1020 }],
|
|
110
|
+
* 1020
|
|
111
|
+
* )
|
|
112
|
+
* // v ≈ { x: 1000, y: 0 } — 20 px over 20 ms → 1000 px/s
|
|
113
|
+
*/
|
|
114
|
+
const computeReleaseVelocity = (history, nowMs) => {
|
|
115
|
+
if (history.length < 2)
|
|
116
|
+
return { x: 0, y: 0 };
|
|
117
|
+
const newest = history[history.length - 1];
|
|
118
|
+
if (nowMs - newest.t > MAX_VELOCITY_DELTA_MS)
|
|
119
|
+
return { x: 0, y: 0 };
|
|
120
|
+
let oldestIdx = history.length - 1;
|
|
121
|
+
for (let i = history.length - 2; i >= 0; i--) {
|
|
122
|
+
if (newest.t - history[i].t > MAX_VELOCITY_DELTA_MS)
|
|
123
|
+
break;
|
|
124
|
+
oldestIdx = i;
|
|
125
|
+
}
|
|
126
|
+
if (oldestIdx === history.length - 1)
|
|
127
|
+
return { x: 0, y: 0 };
|
|
128
|
+
const oldest = history[oldestIdx];
|
|
129
|
+
const dtMs = newest.t - oldest.t;
|
|
130
|
+
if (dtMs < MIN_VELOCITY_INTERVAL_MS)
|
|
131
|
+
return { x: 0, y: 0 };
|
|
132
|
+
return {
|
|
133
|
+
x: ((newest.x - oldest.x) / dtMs) * 1000,
|
|
134
|
+
y: ((newest.y - oldest.y) / dtMs) * 1000
|
|
135
|
+
};
|
|
136
|
+
};
|
|
90
137
|
/**
|
|
91
138
|
* Attach a drag gesture to an element.
|
|
92
139
|
*
|
|
@@ -184,25 +231,21 @@ export const attachDrag = (el, opts) => {
|
|
|
184
231
|
applied.x = x;
|
|
185
232
|
if ('y' in payload)
|
|
186
233
|
applied.y = y;
|
|
187
|
-
//
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
y,
|
|
194
|
-
transform: actualTransform
|
|
195
|
-
});
|
|
196
|
-
animate(el, ('x' in payload || 'y' in payload ? payload : { x, y }), {
|
|
197
|
-
duration: 0
|
|
198
|
-
});
|
|
199
|
-
actualTransform = getComputedStyle(el).transform;
|
|
234
|
+
// Playwright-only sanity check: confirm the transform actually
|
|
235
|
+
// landed on the element and retry once if not. Forces a style
|
|
236
|
+
// recalc via getComputedStyle, so we gate it behind the same
|
|
237
|
+
// playwright env flag pwLog uses so it never fires in prod.
|
|
238
|
+
if (isPlaywrightEnv()) {
|
|
239
|
+
let actualTransform = getComputedStyle(el).transform;
|
|
200
240
|
if (actualTransform === 'none' || !actualTransform.includes('matrix')) {
|
|
201
|
-
pwWarn('⚠️ setXY
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
241
|
+
pwWarn('⚠️ setXY transform missing; retrying write', { x, y });
|
|
242
|
+
animate(el, ('x' in payload || 'y' in payload
|
|
243
|
+
? payload
|
|
244
|
+
: { x, y }), { duration: 0 });
|
|
245
|
+
actualTransform = getComputedStyle(el).transform;
|
|
246
|
+
if (actualTransform === 'none' || !actualTransform.includes('matrix')) {
|
|
247
|
+
pwWarn('⚠️ setXY second attempt still missing transform', { x, y });
|
|
248
|
+
}
|
|
206
249
|
}
|
|
207
250
|
}
|
|
208
251
|
};
|
|
@@ -404,22 +447,28 @@ export const attachDrag = (el, opts) => {
|
|
|
404
447
|
});
|
|
405
448
|
if (!dragging)
|
|
406
449
|
return;
|
|
407
|
-
|
|
450
|
+
// Pointer was preempted (gesture-nav, palm rejection, scroll
|
|
451
|
+
// takeover). User did not release intentionally — skip the
|
|
452
|
+
// inertia/momentum path and force a no-momentum settle so the
|
|
453
|
+
// card clamps back into constraints without flinging.
|
|
454
|
+
finishDrag(e, true);
|
|
408
455
|
};
|
|
409
456
|
/**
|
|
410
457
|
* Finish a drag:
|
|
411
458
|
* - If momentum is enabled, decay towards a clamped target with exponential easing
|
|
412
459
|
* - Otherwise, animate back to a clamped position (or origin), then sync `applied`
|
|
413
460
|
*/
|
|
414
|
-
const finishDrag = (e) => {
|
|
461
|
+
const finishDrag = (e, cancelled = false) => {
|
|
415
462
|
dragging = false;
|
|
463
|
+
velocity = computeReleaseVelocity(history, now());
|
|
416
464
|
pwLog('[drag] finish', {
|
|
417
465
|
el: EL_ID,
|
|
418
466
|
lastPoint,
|
|
419
467
|
startPoint,
|
|
420
468
|
origin,
|
|
421
469
|
applied,
|
|
422
|
-
momentum
|
|
470
|
+
momentum,
|
|
471
|
+
velocity
|
|
423
472
|
});
|
|
424
473
|
try {
|
|
425
474
|
if ('releasePointerCapture' in el && typeof e.pointerId === 'number')
|
|
@@ -434,8 +483,10 @@ export const attachDrag = (el, opts) => {
|
|
|
434
483
|
window.removeEventListener('pointermove', onPointerMove);
|
|
435
484
|
window.removeEventListener('pointerup', onPointerUp);
|
|
436
485
|
window.removeEventListener('pointercancel', onPointerCancel);
|
|
437
|
-
// Momentum/inertia with boundary handoff: inertia until crossing, then spring to boundary
|
|
438
|
-
|
|
486
|
+
// Momentum/inertia with boundary handoff: inertia until crossing, then spring to boundary.
|
|
487
|
+
// Pointer-cancel forces a no-momentum settle (clamp into constraints, no fling) since the
|
|
488
|
+
// gesture was preempted rather than intentionally released.
|
|
489
|
+
if (momentum && !cancelled) {
|
|
439
490
|
pwLog('🚀 STARTING MOMENTUM', {
|
|
440
491
|
velocityX: velocity.x,
|
|
441
492
|
velocityY: velocity.y,
|
|
@@ -454,6 +505,18 @@ export const attachDrag = (el, opts) => {
|
|
|
454
505
|
...(applyX ? { x: 0 } : {}),
|
|
455
506
|
...(applyY ? { y: 0 } : {})
|
|
456
507
|
}, (mergedTransition ?? {}));
|
|
508
|
+
// Cancel hook so re-grab / controls.stop() interrupts the snap.
|
|
509
|
+
stopInertia = () => {
|
|
510
|
+
pwLog('❌ snapToOrigin cancelled');
|
|
511
|
+
controls.stop?.();
|
|
512
|
+
// Sync applied to wherever the snap reached so the next drag origin is correct.
|
|
513
|
+
const { tx, ty } = parseMatrixTranslate(getComputedStyle(el).transform);
|
|
514
|
+
if (applyX)
|
|
515
|
+
applied.x = tx;
|
|
516
|
+
if (applyY)
|
|
517
|
+
applied.y = ty;
|
|
518
|
+
stopInertia = null;
|
|
519
|
+
};
|
|
457
520
|
Promise.resolve(controls.finished)
|
|
458
521
|
.then(() => {
|
|
459
522
|
// Sync internal applied transform so next drag uses the correct origin
|
|
@@ -465,26 +528,31 @@ export const attachDrag = (el, opts) => {
|
|
|
465
528
|
el: EL_ID,
|
|
466
529
|
applied
|
|
467
530
|
});
|
|
531
|
+
if (stopInertia)
|
|
532
|
+
stopInertia = null;
|
|
468
533
|
})
|
|
469
534
|
.catch(() => { })
|
|
470
535
|
.finally(() => opts.callbacks?.onTransitionEnd?.());
|
|
471
536
|
return;
|
|
472
537
|
}
|
|
473
|
-
//
|
|
538
|
+
// Boundary min/max anchor to `constraintsBase` (the absolute
|
|
539
|
+
// pixel-constraint origin) so the inertia handoff snaps to the
|
|
540
|
+
// same edge pointermove's elastic clamping uses — not to a
|
|
541
|
+
// per-drag-shifted `origin` that would drift across drags.
|
|
474
542
|
const noConstraints = !constraints;
|
|
475
543
|
const huge = 1e6;
|
|
476
544
|
const minX = noConstraints
|
|
477
545
|
? applied.x - huge
|
|
478
|
-
:
|
|
546
|
+
: constraintsBase.x + (constraints?.left ?? -Infinity);
|
|
479
547
|
const maxX = noConstraints
|
|
480
548
|
? applied.x + huge
|
|
481
|
-
:
|
|
549
|
+
: constraintsBase.x + (constraints?.right ?? Infinity);
|
|
482
550
|
const minY = noConstraints
|
|
483
551
|
? applied.y - huge
|
|
484
|
-
:
|
|
552
|
+
: constraintsBase.y + (constraints?.top ?? -Infinity);
|
|
485
553
|
const maxY = noConstraints
|
|
486
554
|
? applied.y + huge
|
|
487
|
-
:
|
|
555
|
+
: constraintsBase.y + (constraints?.bottom ?? Infinity);
|
|
488
556
|
const { timeConstantMs, restDelta, restSpeed, bounceStiffness, bounceDamping } = deriveBoundaryPhysics(elastic, opts.transition);
|
|
489
557
|
pwLog('⚙️ boundary-physics', {
|
|
490
558
|
timeConstantMs,
|
|
@@ -500,6 +568,10 @@ export const attachDrag = (el, opts) => {
|
|
|
500
568
|
// Respect direction lock on release: only animate the locked axis
|
|
501
569
|
const applyX = (axis === true || axis === 'x') && lockAxis !== 'y';
|
|
502
570
|
const applyY = (axis === true || axis === 'y') && lockAxis !== 'x';
|
|
571
|
+
// Element-ref constraints can resize / reflow during inertia.
|
|
572
|
+
// Pixel constraints never change once set. We re-resolve only
|
|
573
|
+
// for element-ref each frame in the rAF loop below.
|
|
574
|
+
const isElementRefConstraint = isDomElement(opts.constraints);
|
|
503
575
|
const stepX = applyX
|
|
504
576
|
? createInertiaToBoundary({ value: applied.x, velocity: velocity.x }, { min: minX, max: maxX }, { timeConstantMs, restDelta, restSpeed, bounceStiffness, bounceDamping })
|
|
505
577
|
: null;
|
|
@@ -519,9 +591,27 @@ export const attachDrag = (el, opts) => {
|
|
|
519
591
|
// Use the precomputed step functions (built once per release)
|
|
520
592
|
const rx = stepX ? stepX(t) : { value: applied.x, done: true };
|
|
521
593
|
const ry = stepY ? stepY(t) : { value: applied.y, done: true };
|
|
522
|
-
//
|
|
523
|
-
|
|
524
|
-
|
|
594
|
+
// Element-ref constraints may have resized / reflowed since
|
|
595
|
+
// the steppers were built. Re-resolve and clamp the output
|
|
596
|
+
// so the card never lands outside the now-current container
|
|
597
|
+
// even if its boundary moved mid-spring. Pixel constraints
|
|
598
|
+
// don't move so we skip the work.
|
|
599
|
+
let nextX = rx.value;
|
|
600
|
+
let nextY = (axis === true || axis === 'y') && lockAxis !== 'x' ? ry.value : applied.y;
|
|
601
|
+
if (isElementRefConstraint) {
|
|
602
|
+
const freshConstraints = resolveConstraints(el, opts.constraints);
|
|
603
|
+
if (freshConstraints) {
|
|
604
|
+
constraintsBase = { x: applied.x, y: applied.y };
|
|
605
|
+
const freshMinX = constraintsBase.x + (freshConstraints.left ?? -Infinity);
|
|
606
|
+
const freshMaxX = constraintsBase.x + (freshConstraints.right ?? Infinity);
|
|
607
|
+
const freshMinY = constraintsBase.y + (freshConstraints.top ?? -Infinity);
|
|
608
|
+
const freshMaxY = constraintsBase.y + (freshConstraints.bottom ?? Infinity);
|
|
609
|
+
if (applyX)
|
|
610
|
+
nextX = Math.max(freshMinX, Math.min(freshMaxX, nextX));
|
|
611
|
+
if (applyY)
|
|
612
|
+
nextY = Math.max(freshMinY, Math.min(freshMaxY, nextY));
|
|
613
|
+
}
|
|
614
|
+
}
|
|
525
615
|
setXY(nextX, nextY);
|
|
526
616
|
if (frameCount <= 3 || frameCount % 10 === 0) {
|
|
527
617
|
pwLog(`🔄 FRAME ${frameCount}`, {
|
|
@@ -537,15 +627,19 @@ export const attachDrag = (el, opts) => {
|
|
|
537
627
|
if ((rx.done || !stepX) && (ry.done || !stepY)) {
|
|
538
628
|
pwLog('✅ REST REACHED', {
|
|
539
629
|
frameCount,
|
|
540
|
-
finalX:
|
|
541
|
-
finalY:
|
|
630
|
+
finalX: nextX,
|
|
631
|
+
finalY: nextY,
|
|
542
632
|
timeConstantMs,
|
|
543
633
|
restDelta,
|
|
544
634
|
restSpeed
|
|
545
635
|
});
|
|
546
|
-
//
|
|
547
|
-
|
|
548
|
-
|
|
636
|
+
// Sync `applied` from the post-clamp frame values
|
|
637
|
+
// (nextX/nextY), not the raw stepper output. When
|
|
638
|
+
// element-ref constraints clamped this frame, raw
|
|
639
|
+
// rx.value sits outside the visible bounds and would
|
|
640
|
+
// desync the next-drag origin from the rendered transform.
|
|
641
|
+
const finalX = stepX ? nextX : applied.x;
|
|
642
|
+
const finalY = stepY ? nextY : applied.y;
|
|
549
643
|
if (axis === true || axis === 'x')
|
|
550
644
|
applied.x = finalX;
|
|
551
645
|
if (axis === true || axis === 'y')
|
|
@@ -560,14 +654,12 @@ export const attachDrag = (el, opts) => {
|
|
|
560
654
|
stopInertia = () => {
|
|
561
655
|
pwLog('❌ MOMENTUM CANCELLED');
|
|
562
656
|
running = false;
|
|
563
|
-
//
|
|
564
|
-
//
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
if (axis === true || axis === 'y')
|
|
570
|
-
applied.y = finalY;
|
|
657
|
+
// `applied` is already in sync with the last frame rendered
|
|
658
|
+
// by the rAF loop (setXY updates it on every frame). We
|
|
659
|
+
// intentionally do NOT call stepX/stepY again here —
|
|
660
|
+
// they're stateful (mutate lastT/springX/springV) and an
|
|
661
|
+
// extra call advances them past the visible state, leaving
|
|
662
|
+
// applied slightly out of sync with what the user sees.
|
|
571
663
|
pwLog('[drag] inertia cancelled → sync applied', {
|
|
572
664
|
el: EL_ID,
|
|
573
665
|
applied: { x: applied.x, y: applied.y }
|
|
@@ -624,6 +716,17 @@ export const attachDrag = (el, opts) => {
|
|
|
624
716
|
const settleTransition = elastic === 0 ? { duration: 0 } : (mergedTransition ?? {});
|
|
625
717
|
// Animate with the merged transition so the settle feels consistent with other motion
|
|
626
718
|
const controls = animate(el, { ...(applyX ? { x } : {}), ...(applyY ? { y } : {}) }, settleTransition);
|
|
719
|
+
// Cancel hook so re-grab interrupts the settle animation cleanly.
|
|
720
|
+
stopInertia = () => {
|
|
721
|
+
pwLog('❌ settle (no momentum) cancelled');
|
|
722
|
+
controls.stop?.();
|
|
723
|
+
const { tx, ty } = parseMatrixTranslate(getComputedStyle(el).transform);
|
|
724
|
+
if (applyX)
|
|
725
|
+
applied.x = tx;
|
|
726
|
+
if (applyY)
|
|
727
|
+
applied.y = ty;
|
|
728
|
+
stopInertia = null;
|
|
729
|
+
};
|
|
627
730
|
// Fire transition end once settled
|
|
628
731
|
Promise.resolve(controls.finished)
|
|
629
732
|
.then(() => {
|
|
@@ -636,6 +739,8 @@ export const attachDrag = (el, opts) => {
|
|
|
636
739
|
el: EL_ID,
|
|
637
740
|
applied
|
|
638
741
|
});
|
|
742
|
+
if (stopInertia)
|
|
743
|
+
stopInertia = null;
|
|
639
744
|
})
|
|
640
745
|
.catch(() => { })
|
|
641
746
|
.finally(() => opts.callbacks?.onTransitionEnd?.());
|
|
@@ -643,10 +748,12 @@ export const attachDrag = (el, opts) => {
|
|
|
643
748
|
opts.callbacks?.onEnd?.(e, computeInfo());
|
|
644
749
|
endWhileDrag();
|
|
645
750
|
};
|
|
646
|
-
// Wire dragControls
|
|
751
|
+
// Wire dragControls. The cancelInertia thunk reads the *current*
|
|
752
|
+
// stopInertia at call time so the latest in-flight animation is
|
|
753
|
+
// targeted (stopInertia is re-assigned per finishDrag).
|
|
647
754
|
if (opts.controls) {
|
|
648
755
|
const internal = opts.controls;
|
|
649
|
-
internal._bind?.(el, beginDrag);
|
|
756
|
+
internal._bind?.(el, beginDrag, () => stopInertia?.());
|
|
650
757
|
pwLog('[drag] controls bound', { el: EL_ID });
|
|
651
758
|
}
|
|
652
759
|
el.addEventListener('pointerdown', onPointerDown);
|
package/dist/utils/dragMath.d.ts
CHANGED
|
@@ -34,3 +34,21 @@ export type ConstraintElastic = {
|
|
|
34
34
|
* ```
|
|
35
35
|
*/
|
|
36
36
|
export declare const applyConstraints: (point: number, range: ConstraintRange, elastic?: ConstraintElastic) => number;
|
|
37
|
+
/**
|
|
38
|
+
* Parse a CSS `matrix(a, b, c, d, tx, ty)` string into translate components.
|
|
39
|
+
*
|
|
40
|
+
* Used to read the rendered translate after Motion writes inline transforms
|
|
41
|
+
* during drag-cancel hooks.
|
|
42
|
+
*
|
|
43
|
+
* @param transform The computed `transform` style — typically `'none'` or
|
|
44
|
+
* `'matrix(1, 0, 0, 1, 10, 20)'`. `matrix3d(...)` is not supported.
|
|
45
|
+
* @returns The 2D translate components `{ tx, ty }`. Defaults to
|
|
46
|
+
* `{ tx: 0, ty: 0 }` for `'none'` or any non-matching input.
|
|
47
|
+
* @example
|
|
48
|
+
* parseMatrixTranslate('matrix(1, 0, 0, 1, 10, 20)') // → { tx: 10, ty: 20 }
|
|
49
|
+
* parseMatrixTranslate('none') // → { tx: 0, ty: 0 }
|
|
50
|
+
*/
|
|
51
|
+
export declare const parseMatrixTranslate: (transform: string) => {
|
|
52
|
+
tx: number;
|
|
53
|
+
ty: number;
|
|
54
|
+
};
|
package/dist/utils/dragMath.js
CHANGED
|
@@ -42,3 +42,24 @@ export const applyConstraints = (point, range, elastic) => {
|
|
|
42
42
|
}
|
|
43
43
|
return point;
|
|
44
44
|
};
|
|
45
|
+
/**
|
|
46
|
+
* Parse a CSS `matrix(a, b, c, d, tx, ty)` string into translate components.
|
|
47
|
+
*
|
|
48
|
+
* Used to read the rendered translate after Motion writes inline transforms
|
|
49
|
+
* during drag-cancel hooks.
|
|
50
|
+
*
|
|
51
|
+
* @param transform The computed `transform` style — typically `'none'` or
|
|
52
|
+
* `'matrix(1, 0, 0, 1, 10, 20)'`. `matrix3d(...)` is not supported.
|
|
53
|
+
* @returns The 2D translate components `{ tx, ty }`. Defaults to
|
|
54
|
+
* `{ tx: 0, ty: 0 }` for `'none'` or any non-matching input.
|
|
55
|
+
* @example
|
|
56
|
+
* parseMatrixTranslate('matrix(1, 0, 0, 1, 10, 20)') // → { tx: 10, ty: 20 }
|
|
57
|
+
* parseMatrixTranslate('none') // → { tx: 0, ty: 0 }
|
|
58
|
+
*/
|
|
59
|
+
export const parseMatrixTranslate = (transform) => {
|
|
60
|
+
const m = transform.match(/matrix\(([^)]+)\)/);
|
|
61
|
+
if (!m)
|
|
62
|
+
return { tx: 0, ty: 0 };
|
|
63
|
+
const parts = m[1].split(',').map((s) => Number.parseFloat(s.trim()));
|
|
64
|
+
return { tx: parts[4] ?? 0, ty: parts[5] ?? 0 };
|
|
65
|
+
};
|
package/dist/utils/inertia.js
CHANGED
|
@@ -80,12 +80,13 @@ export const createInertiaToBoundary = (initial, bounds, opts) => {
|
|
|
80
80
|
let springX = x;
|
|
81
81
|
let springV = 0;
|
|
82
82
|
let boundaryTarget = null;
|
|
83
|
-
//
|
|
83
|
+
// Starting OOB: skip inertia and engage the spring. Drop the
|
|
84
|
+
// away-from-boundary velocity component so the spring's first frames
|
|
85
|
+
// don't continue moving the value outward.
|
|
84
86
|
if (x < min || x > max) {
|
|
85
87
|
mode = 'spring';
|
|
86
|
-
// Set spring start at current value and carry current velocity
|
|
87
88
|
springX = x;
|
|
88
|
-
springV = v;
|
|
89
|
+
springV = x < min ? Math.max(0, v) : Math.min(0, v);
|
|
89
90
|
boundaryTarget = x < min ? min : max;
|
|
90
91
|
}
|
|
91
92
|
// Precompute first crossing (if any) from initial state
|
package/dist/vite.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Parser } from 'acorn';
|
|
1
2
|
/**
|
|
2
3
|
* Tag-to-component name mapping. Each key is the lowercase HTML/SVG tag,
|
|
3
4
|
* and the value is the PascalCase component filename (without .svelte).
|
|
@@ -318,16 +319,18 @@ export const svelteMotionOptimize = () => ({
|
|
|
318
319
|
for (const [tag, localName] of tagToLocal) {
|
|
319
320
|
const openRe = new RegExp(`<motion\\.${escapeRegExp(tag)}(?=[\\s/>])`, 'g');
|
|
320
321
|
const closeRe = new RegExp(`</motion\\.${escapeRegExp(tag)}\\s*>`, 'g');
|
|
321
|
-
const scriptRe = new RegExp(`\\bmotion\\.${escapeRegExp(tag)}\\b`, 'g');
|
|
322
322
|
transformed = transformed.replace(openRe, `<${localName}`);
|
|
323
323
|
transformed = transformed.replace(closeRe, `</${localName}>`);
|
|
324
|
-
// Also replace script-block references (e.g., const Component = motion.div)
|
|
325
|
-
// But only outside of the import statement we already handled
|
|
326
|
-
const importEndIdx = transformed.indexOf(localName) + localName.length;
|
|
327
|
-
const beforeImport = transformed.slice(0, importEndIdx);
|
|
328
|
-
const afterImport = transformed.slice(importEndIdx);
|
|
329
|
-
transformed = beforeImport + afterImport.replace(scriptRe, localName);
|
|
330
324
|
}
|
|
325
|
+
// Rewrite `motion.TAG` JS references (e.g. `const Component = motion.div`)
|
|
326
|
+
// inside <script> blocks only. A naive regex over the script body would
|
|
327
|
+
// also clobber the same substring in string literals (`"motion.div"`)
|
|
328
|
+
// and comments (`// motion.div`). Parse the script as JS instead and
|
|
329
|
+
// only rewrite real `motion.<tag>` MemberExpressions. For scripts that
|
|
330
|
+
// fail to parse as plain JS (e.g. `<script lang="ts">`), fall back to a
|
|
331
|
+
// string/comment-aware lexer that achieves the same correctness without
|
|
332
|
+
// needing a TS parser.
|
|
333
|
+
transformed = transformed.replace(/(<script\b[^>]*>)([\s\S]*?)(<\/script>)/g, (_full, open, content, close) => open + rewriteMotionRefsInScript(content, tagToLocal) + close);
|
|
331
334
|
return {
|
|
332
335
|
code: transformed,
|
|
333
336
|
map: null
|
|
@@ -341,3 +344,169 @@ export const svelteMotionOptimize = () => ({
|
|
|
341
344
|
* @returns The escaped string safe for use in a RegExp.
|
|
342
345
|
*/
|
|
343
346
|
const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
347
|
+
/**
|
|
348
|
+
* Rewrite `motion.<tag>` member-expression references inside a `<script>`
|
|
349
|
+
* body to the matching `SvelteMotionTag` local. Preserves string literals
|
|
350
|
+
* and comments — they look like `motion.div` to a regex but must not be
|
|
351
|
+
* rewritten.
|
|
352
|
+
*
|
|
353
|
+
* Strategy: parse the body as JS with acorn and splice only real
|
|
354
|
+
* MemberExpression matches. If parsing fails (TypeScript, JSX, etc.) fall
|
|
355
|
+
* back to a string/comment-aware lexer that skips literals and comments.
|
|
356
|
+
*
|
|
357
|
+
* @param content - Raw script body (between `<script ...>` and `</script>`).
|
|
358
|
+
* @param tagToLocal - Map of lowercase tag → local component identifier.
|
|
359
|
+
* @returns The rewritten body, ready to splice back into the source.
|
|
360
|
+
*/
|
|
361
|
+
const rewriteMotionRefsInScript = (content, tagToLocal) => {
|
|
362
|
+
try {
|
|
363
|
+
return rewriteViaAst(content, tagToLocal);
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
return rewriteViaLexer(content, tagToLocal);
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
const isIdentifier = (n) => !!n && n.type === 'Identifier';
|
|
370
|
+
const isMemberExpression = (n) => !!n && n.type === 'MemberExpression';
|
|
371
|
+
/**
|
|
372
|
+
* Walk an acorn AST and collect every `motion.<tag>` MemberExpression range
|
|
373
|
+
* we should rewrite. Splice from end to start so earlier indices stay valid.
|
|
374
|
+
*/
|
|
375
|
+
const rewriteViaAst = (content, tagToLocal) => {
|
|
376
|
+
const ast = Parser.parse(content, {
|
|
377
|
+
ecmaVersion: 'latest',
|
|
378
|
+
sourceType: 'module',
|
|
379
|
+
allowImportExportEverywhere: true,
|
|
380
|
+
allowReturnOutsideFunction: true,
|
|
381
|
+
allowAwaitOutsideFunction: true,
|
|
382
|
+
allowHashBang: true
|
|
383
|
+
});
|
|
384
|
+
const edits = [];
|
|
385
|
+
const visit = (node) => {
|
|
386
|
+
if (!node || typeof node !== 'object' || typeof node.type !== 'string')
|
|
387
|
+
return;
|
|
388
|
+
if (isMemberExpression(node) &&
|
|
389
|
+
!node.computed &&
|
|
390
|
+
isIdentifier(node.object) &&
|
|
391
|
+
node.object.name === 'motion' &&
|
|
392
|
+
isIdentifier(node.property)) {
|
|
393
|
+
const localName = tagToLocal.get(node.property.name);
|
|
394
|
+
if (localName) {
|
|
395
|
+
edits.push({ start: node.start, end: node.end, replacement: localName });
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
for (const key of Object.keys(node)) {
|
|
400
|
+
if (key === 'type' || key === 'start' || key === 'end' || key === 'loc')
|
|
401
|
+
continue;
|
|
402
|
+
const value = node[key];
|
|
403
|
+
if (Array.isArray(value))
|
|
404
|
+
value.forEach((v) => visit(v));
|
|
405
|
+
else if (value && typeof value === 'object')
|
|
406
|
+
visit(value);
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
visit(ast);
|
|
410
|
+
if (edits.length === 0)
|
|
411
|
+
return content;
|
|
412
|
+
edits.sort((a, b) => b.start - a.start);
|
|
413
|
+
let out = content;
|
|
414
|
+
for (const edit of edits) {
|
|
415
|
+
out = out.slice(0, edit.start) + edit.replacement + out.slice(edit.end);
|
|
416
|
+
}
|
|
417
|
+
return out;
|
|
418
|
+
};
|
|
419
|
+
/**
|
|
420
|
+
* Fallback for scripts acorn can't parse (TS, JSX). Walks the source
|
|
421
|
+
* character-by-character, skipping string literals (`'`, `"`, backtick incl.
|
|
422
|
+
* `${…}` substitutions) and line/block comments, then applies a `motion.<tag>`
|
|
423
|
+
* regex to the remaining "code" regions. Less precise than AST but covers
|
|
424
|
+
* the same correctness contract for literal/comment preservation.
|
|
425
|
+
*/
|
|
426
|
+
const rewriteViaLexer = (content, tagToLocal) => {
|
|
427
|
+
const len = content.length;
|
|
428
|
+
const out = [];
|
|
429
|
+
let i = 0;
|
|
430
|
+
const isIdStart = (ch) => /[A-Za-z_$]/.test(ch);
|
|
431
|
+
const isIdPart = (ch) => /[A-Za-z0-9_$-]/.test(ch);
|
|
432
|
+
while (i < len) {
|
|
433
|
+
const ch = content[i];
|
|
434
|
+
const next = content[i + 1];
|
|
435
|
+
// Line comment
|
|
436
|
+
if (ch === '/' && next === '/') {
|
|
437
|
+
const end = content.indexOf('\n', i);
|
|
438
|
+
const stop = end === -1 ? len : end;
|
|
439
|
+
out.push(content.slice(i, stop));
|
|
440
|
+
i = stop;
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
// Block comment
|
|
444
|
+
if (ch === '/' && next === '*') {
|
|
445
|
+
const end = content.indexOf('*/', i + 2);
|
|
446
|
+
const stop = end === -1 ? len : end + 2;
|
|
447
|
+
out.push(content.slice(i, stop));
|
|
448
|
+
i = stop;
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
// String literals (single/double)
|
|
452
|
+
if (ch === '"' || ch === "'") {
|
|
453
|
+
const quote = ch;
|
|
454
|
+
let j = i + 1;
|
|
455
|
+
while (j < len) {
|
|
456
|
+
if (content[j] === '\\') {
|
|
457
|
+
j += 2;
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
if (content[j] === quote) {
|
|
461
|
+
j++;
|
|
462
|
+
break;
|
|
463
|
+
}
|
|
464
|
+
j++;
|
|
465
|
+
}
|
|
466
|
+
out.push(content.slice(i, j));
|
|
467
|
+
i = j;
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
// Template literal — naive: skip to matching backtick, no `${…}` parsing
|
|
471
|
+
// is needed for our use case (we only need to NOT rewrite the literal
|
|
472
|
+
// text; substitutions still look like code but `motion.<tag>` inside
|
|
473
|
+
// a template substitution is vanishingly rare and acorn would normally
|
|
474
|
+
// handle it).
|
|
475
|
+
if (ch === '`') {
|
|
476
|
+
let j = i + 1;
|
|
477
|
+
while (j < len) {
|
|
478
|
+
if (content[j] === '\\') {
|
|
479
|
+
j += 2;
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
if (content[j] === '`') {
|
|
483
|
+
j++;
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
486
|
+
j++;
|
|
487
|
+
}
|
|
488
|
+
out.push(content.slice(i, j));
|
|
489
|
+
i = j;
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
// Possible `motion.<tag>` identifier — require word boundary on left
|
|
493
|
+
if ((i === 0 || !isIdPart(content[i - 1])) &&
|
|
494
|
+
isIdStart(ch) &&
|
|
495
|
+
content.slice(i, i + 7) === 'motion.') {
|
|
496
|
+
let j = i + 7;
|
|
497
|
+
const tagStart = j;
|
|
498
|
+
while (j < len && isIdPart(content[j]))
|
|
499
|
+
j++;
|
|
500
|
+
const tag = content.slice(tagStart, j);
|
|
501
|
+
const localName = tagToLocal.get(tag);
|
|
502
|
+
if (localName && (j === len || !isIdPart(content[j]))) {
|
|
503
|
+
out.push(localName);
|
|
504
|
+
i = j;
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
out.push(ch);
|
|
509
|
+
i++;
|
|
510
|
+
}
|
|
511
|
+
return out.join('');
|
|
512
|
+
};
|
package/package.json
CHANGED
|
@@ -1,29 +1,52 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@humanspeak/svelte-motion",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.6",
|
|
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",
|
|
7
|
+
"svelte-5",
|
|
8
|
+
"svelte5",
|
|
9
|
+
"sveltekit",
|
|
10
|
+
"svelte-motion",
|
|
11
|
+
"motion-svelte",
|
|
12
|
+
"svelte-animation",
|
|
13
|
+
"svelte-animations",
|
|
14
|
+
"svelte-animation-library",
|
|
7
15
|
"animation",
|
|
16
|
+
"animations",
|
|
8
17
|
"motion",
|
|
9
|
-
"transitions",
|
|
10
|
-
"spring-physics",
|
|
11
|
-
"ui-animation",
|
|
12
|
-
"svelte5",
|
|
13
|
-
"hardware-accelerated",
|
|
14
|
-
"micro-interactions",
|
|
15
|
-
"performance",
|
|
16
18
|
"framer-motion",
|
|
17
19
|
"framer-motion-svelte",
|
|
20
|
+
"framer-motion-for-svelte",
|
|
21
|
+
"framer-motion-alternative",
|
|
22
|
+
"transitions",
|
|
18
23
|
"gestures",
|
|
19
|
-
"exit-animation",
|
|
20
|
-
"animate-presence",
|
|
21
|
-
"layout-animation",
|
|
22
24
|
"drag",
|
|
25
|
+
"drag-and-drop",
|
|
26
|
+
"hover",
|
|
27
|
+
"tap",
|
|
28
|
+
"while-in-view",
|
|
23
29
|
"variants",
|
|
24
|
-
"
|
|
30
|
+
"animate-presence",
|
|
31
|
+
"exit-animation",
|
|
32
|
+
"layout-animation",
|
|
33
|
+
"shared-layout",
|
|
34
|
+
"flip-animation",
|
|
35
|
+
"spring",
|
|
36
|
+
"spring-physics",
|
|
37
|
+
"scroll-animation",
|
|
38
|
+
"scroll-linked",
|
|
39
|
+
"use-spring",
|
|
40
|
+
"use-scroll",
|
|
41
|
+
"use-animate",
|
|
42
|
+
"use-transform",
|
|
43
|
+
"use-in-view",
|
|
44
|
+
"use-presence",
|
|
45
|
+
"motion-values",
|
|
46
|
+
"ssr",
|
|
25
47
|
"typescript",
|
|
26
|
-
"
|
|
48
|
+
"ui-animation",
|
|
49
|
+
"micro-interactions"
|
|
27
50
|
],
|
|
28
51
|
"homepage": "https://motion.svelte.page",
|
|
29
52
|
"bugs": {
|
|
@@ -71,59 +94,60 @@
|
|
|
71
94
|
}
|
|
72
95
|
},
|
|
73
96
|
"dependencies": {
|
|
97
|
+
"acorn": "^8.16.0",
|
|
74
98
|
"motion": "^12.38.0",
|
|
75
99
|
"motion-dom": "^12.38.0"
|
|
76
100
|
},
|
|
77
101
|
"devDependencies": {
|
|
78
102
|
"@changesets/cli": "^2.31.0",
|
|
79
|
-
"@eslint/compat": "^2.0
|
|
103
|
+
"@eslint/compat": "^2.1.0",
|
|
80
104
|
"@eslint/js": "^10.0.1",
|
|
81
|
-
"@playwright/test": "^1.
|
|
105
|
+
"@playwright/test": "^1.60.0",
|
|
82
106
|
"@sveltejs/adapter-auto": "^7.0.1",
|
|
83
|
-
"@sveltejs/kit": "^2.
|
|
107
|
+
"@sveltejs/kit": "^2.59.1",
|
|
84
108
|
"@sveltejs/package": "^2.5.7",
|
|
85
|
-
"@sveltejs/vite-plugin-svelte": "^7.
|
|
109
|
+
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
|
86
110
|
"@tailwindcss/aspect-ratio": "^0.4.2",
|
|
87
111
|
"@tailwindcss/container-queries": "^0.1.1",
|
|
88
112
|
"@tailwindcss/forms": "^0.5.11",
|
|
89
113
|
"@tailwindcss/typography": "^0.5.19",
|
|
90
|
-
"@tailwindcss/vite": "^4.
|
|
114
|
+
"@tailwindcss/vite": "^4.3.0",
|
|
91
115
|
"@testing-library/jest-dom": "^6.9.1",
|
|
92
116
|
"@testing-library/svelte": "^5.3.1",
|
|
93
|
-
"@types/node": "^25.
|
|
94
|
-
"@vitest/coverage-v8": "^4.1.
|
|
95
|
-
"eslint": "^10.
|
|
117
|
+
"@types/node": "^25.7.0",
|
|
118
|
+
"@vitest/coverage-v8": "^4.1.6",
|
|
119
|
+
"eslint": "^10.3.0",
|
|
96
120
|
"eslint-config-prettier": "10.1.8",
|
|
97
121
|
"eslint-plugin-import": "2.32.0",
|
|
98
122
|
"eslint-plugin-svelte": "3.17.1",
|
|
99
123
|
"eslint-plugin-unused-imports": "4.4.1",
|
|
100
124
|
"esm-env": "^1.2.2",
|
|
101
|
-
"globals": "^17.
|
|
125
|
+
"globals": "^17.6.0",
|
|
102
126
|
"html-tags": "^5.1.0",
|
|
103
127
|
"html-void-elements": "^3.0.0",
|
|
104
128
|
"husky": "^9.1.7",
|
|
105
|
-
"jsdom": "^29.
|
|
129
|
+
"jsdom": "^29.1.1",
|
|
106
130
|
"mprocs": "^0.9.2",
|
|
107
131
|
"prettier": "^3.8.3",
|
|
108
132
|
"prettier-plugin-organize-imports": "^4.3.0",
|
|
109
133
|
"prettier-plugin-sort-json": "^4.2.0",
|
|
110
|
-
"prettier-plugin-svelte": "^3.5.
|
|
111
|
-
"prettier-plugin-tailwindcss": "^0.
|
|
112
|
-
"publint": "^0.3.
|
|
134
|
+
"prettier-plugin-svelte": "^3.5.2",
|
|
135
|
+
"prettier-plugin-tailwindcss": "^0.8.0",
|
|
136
|
+
"publint": "^0.3.21",
|
|
113
137
|
"runed": "0.37.1",
|
|
114
138
|
"svelte": "^5.55.5",
|
|
115
|
-
"svelte-check": "^4.4.
|
|
139
|
+
"svelte-check": "^4.4.8",
|
|
116
140
|
"svg-tags": "^1.0.0",
|
|
117
|
-
"tailwind-merge": "^3.
|
|
141
|
+
"tailwind-merge": "^3.6.0",
|
|
118
142
|
"tailwind-variants": "^3.2.2",
|
|
119
|
-
"tailwindcss": "^4.
|
|
143
|
+
"tailwindcss": "^4.3.0",
|
|
120
144
|
"tailwindcss-animate": "^1.0.7",
|
|
121
|
-
"tsx": "^4.
|
|
145
|
+
"tsx": "^4.22.0",
|
|
122
146
|
"typescript": "^6.0.3",
|
|
123
|
-
"typescript-eslint": "^8.59.
|
|
124
|
-
"vite": "^8.0.
|
|
147
|
+
"typescript-eslint": "^8.59.3",
|
|
148
|
+
"vite": "^8.0.13",
|
|
125
149
|
"vite-tsconfig-paths": "^6.1.1",
|
|
126
|
-
"vitest": "^4.1.
|
|
150
|
+
"vitest": "^4.1.6"
|
|
127
151
|
},
|
|
128
152
|
"peerDependencies": {
|
|
129
153
|
"svelte": "^5.0.0"
|
|
@@ -134,17 +158,6 @@
|
|
|
134
158
|
"publishConfig": {
|
|
135
159
|
"access": "public"
|
|
136
160
|
},
|
|
137
|
-
"tags": [
|
|
138
|
-
"svelte",
|
|
139
|
-
"animation",
|
|
140
|
-
"motion",
|
|
141
|
-
"transitions",
|
|
142
|
-
"spring-physics",
|
|
143
|
-
"performance",
|
|
144
|
-
"ui-animation",
|
|
145
|
-
"micro-interactions",
|
|
146
|
-
"svelte5"
|
|
147
|
-
],
|
|
148
161
|
"scripts": {
|
|
149
162
|
"build": "vite build && npm run package",
|
|
150
163
|
"cf-typegen": "pnpm --filter docs cf-typegen",
|