@humanspeak/svelte-motion 0.1.21 → 0.1.23
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/LICENSE +1 -1
- package/dist/html/_MotionContainer.svelte +35 -10
- package/dist/utils/interaction.d.ts +4 -3
- package/dist/utils/interaction.js +121 -263
- package/package.json +15 -15
package/LICENSE
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
import { sleep } from '../utils/testing'
|
|
20
20
|
import { animate, type AnimationOptions, type DOMKeyframesDefinition } from 'motion'
|
|
21
21
|
import { isPlaywrightEnv, pwLog } from '../utils/log'
|
|
22
|
-
import { type Snippet } from 'svelte'
|
|
22
|
+
import { onDestroy, type Snippet } from 'svelte'
|
|
23
23
|
import { VOID_TAGS } from '../utils/constants'
|
|
24
24
|
import { mergeTransitions, animateWithLifecycle } from '../utils/animation'
|
|
25
25
|
import { attachWhileTap } from '../utils/interaction'
|
|
@@ -37,7 +37,6 @@
|
|
|
37
37
|
import { mergeInlineStyles } from '../utils/style'
|
|
38
38
|
import { isNativelyFocusable } from '../utils/a11y'
|
|
39
39
|
import {
|
|
40
|
-
usePresence,
|
|
41
40
|
getAnimatePresenceContext,
|
|
42
41
|
getPresenceDepth,
|
|
43
42
|
setPresenceDepth
|
|
@@ -141,10 +140,10 @@
|
|
|
141
140
|
const presenceKey = keyProp ?? `motion-${++keyCounter}`
|
|
142
141
|
|
|
143
142
|
// Track previous key for key-change detection (simulates React's key-based remounting)
|
|
144
|
-
//
|
|
145
|
-
let keyTrackerPrev =
|
|
146
|
-
let keyTrackerIsTransitioning =
|
|
147
|
-
let keyTransitionStopped =
|
|
143
|
+
// Plain variables (not $state) to avoid self-triggering the key-change $effect
|
|
144
|
+
let keyTrackerPrev = keyProp
|
|
145
|
+
let keyTrackerIsTransitioning = false
|
|
146
|
+
let keyTransitionStopped = false
|
|
148
147
|
|
|
149
148
|
// Compute merged transition without mutating props to avoid effect write loops
|
|
150
149
|
const mergedTransition = $derived<AnimationOptions>(
|
|
@@ -154,10 +153,20 @@
|
|
|
154
153
|
)
|
|
155
154
|
)
|
|
156
155
|
|
|
157
|
-
// Register
|
|
156
|
+
// Register onDestroy at component level (guaranteed to work in Svelte 5)
|
|
157
|
+
// usePresence() cannot be called inside $effect because it uses getContext() and onDestroy(),
|
|
158
|
+
// which must be called during component initialization.
|
|
159
|
+
if (context) {
|
|
160
|
+
onDestroy(() => {
|
|
161
|
+
pwLog('[presence] onDestroy triggered', { key: presenceKey })
|
|
162
|
+
context.unregisterChild(presenceKey)
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Reactively update registration when element/exit/transition props change
|
|
158
167
|
$effect(() => {
|
|
159
|
-
if (element) {
|
|
160
|
-
|
|
168
|
+
if (element && context && resolvedExit) {
|
|
169
|
+
context.registerChild(
|
|
161
170
|
presenceKey,
|
|
162
171
|
element,
|
|
163
172
|
resolvedExit,
|
|
@@ -602,7 +611,7 @@
|
|
|
602
611
|
}
|
|
603
612
|
})
|
|
604
613
|
|
|
605
|
-
// whileTap handling
|
|
614
|
+
// whileTap handling via motion-dom's press()
|
|
606
615
|
$effect(() => {
|
|
607
616
|
if (!(element && isLoaded === 'ready' && isNotEmpty(whileTapProp))) return
|
|
608
617
|
return attachWhileTap(
|
|
@@ -690,6 +699,14 @@
|
|
|
690
699
|
currentKey === keyTrackerPrev ||
|
|
691
700
|
keyTrackerPrev === undefined
|
|
692
701
|
) {
|
|
702
|
+
pwLog('[motion] key effect: early return', {
|
|
703
|
+
currentKey,
|
|
704
|
+
keyTrackerPrev,
|
|
705
|
+
isLoaded,
|
|
706
|
+
hasElement: !!element,
|
|
707
|
+
hasContext: !!context,
|
|
708
|
+
keyTrackerIsTransitioning
|
|
709
|
+
})
|
|
693
710
|
// Update prev for next comparison
|
|
694
711
|
if (currentKey !== keyTrackerPrev) {
|
|
695
712
|
keyTrackerPrev = currentKey
|
|
@@ -704,6 +721,7 @@
|
|
|
704
721
|
|
|
705
722
|
// Mark as transitioning to prevent re-entry
|
|
706
723
|
keyTrackerIsTransitioning = true
|
|
724
|
+
keyTransitionStopped = false
|
|
707
725
|
keyTrackerPrev = currentKey
|
|
708
726
|
|
|
709
727
|
// Run the key transition sequence
|
|
@@ -723,6 +741,11 @@
|
|
|
723
741
|
).finished
|
|
724
742
|
}
|
|
725
743
|
|
|
744
|
+
pwLog('[motion] key transition: exit done', {
|
|
745
|
+
keyTransitionStopped,
|
|
746
|
+
hasElement: !!element
|
|
747
|
+
})
|
|
748
|
+
|
|
726
749
|
// Check if component was unmounted during exit animation
|
|
727
750
|
if (keyTransitionStopped || !element) return
|
|
728
751
|
|
|
@@ -743,6 +766,7 @@
|
|
|
743
766
|
pwLog('[motion] key transition: running enter animation')
|
|
744
767
|
runAnimation()
|
|
745
768
|
} finally {
|
|
769
|
+
pwLog('[motion] key transition: finally', { keyTransitionStopped })
|
|
746
770
|
if (!keyTransitionStopped) {
|
|
747
771
|
keyTrackerIsTransitioning = false
|
|
748
772
|
}
|
|
@@ -753,6 +777,7 @@
|
|
|
753
777
|
|
|
754
778
|
// Cleanup on unmount
|
|
755
779
|
return () => {
|
|
780
|
+
pwLog('[motion] key effect: cleanup, stopping transition')
|
|
756
781
|
keyTransitionStopped = true
|
|
757
782
|
}
|
|
758
783
|
})
|
|
@@ -15,14 +15,15 @@ export declare const buildTapResetRecord: (initial: Record<string, unknown>, ani
|
|
|
15
15
|
/**
|
|
16
16
|
* Attach whileTap interactions to an element.
|
|
17
17
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* Uses motion-dom's `press()` for pointer and Enter-key handling (with
|
|
19
|
+
* primary-pointer filtering, drag interop, and global release listeners).
|
|
20
|
+
* Space-key support is added manually since `press()` only handles Enter.
|
|
21
21
|
*
|
|
22
22
|
* @param el Element to attach listeners to.
|
|
23
23
|
* @param whileTap While-tap keyframe record.
|
|
24
24
|
* @param initial Initial keyframe record.
|
|
25
25
|
* @param animateDef Animate keyframe record.
|
|
26
|
+
* @param callbacks Optional lifecycle callbacks.
|
|
26
27
|
* @return Cleanup function to remove listeners.
|
|
27
28
|
*/
|
|
28
29
|
export declare const attachWhileTap: (el: HTMLElement, whileTap: Record<string, unknown> | undefined, initial?: Record<string, unknown>, animateDef?: Record<string, unknown>, callbacks?: {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isHoverCapable, splitHoverDefinition } from './hover';
|
|
2
2
|
import { pwLog } from './log';
|
|
3
|
-
import { parseMatrixScale } from './transform';
|
|
4
3
|
import { animate } from 'motion';
|
|
4
|
+
import { press } from 'motion-dom';
|
|
5
5
|
/**
|
|
6
6
|
* Build a reset record for whileTap on pointerup.
|
|
7
7
|
*
|
|
@@ -81,122 +81,60 @@ export const buildTapResetRecord = (initial, animateDef, whileTap) => {
|
|
|
81
81
|
/**
|
|
82
82
|
* Attach whileTap interactions to an element.
|
|
83
83
|
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
84
|
+
* Uses motion-dom's `press()` for pointer and Enter-key handling (with
|
|
85
|
+
* primary-pointer filtering, drag interop, and global release listeners).
|
|
86
|
+
* Space-key support is added manually since `press()` only handles Enter.
|
|
87
87
|
*
|
|
88
88
|
* @param el Element to attach listeners to.
|
|
89
89
|
* @param whileTap While-tap keyframe record.
|
|
90
90
|
* @param initial Initial keyframe record.
|
|
91
91
|
* @param animateDef Animate keyframe record.
|
|
92
|
+
* @param callbacks Optional lifecycle callbacks.
|
|
92
93
|
* @return Cleanup function to remove listeners.
|
|
93
94
|
*/
|
|
94
95
|
export const attachWhileTap = (el, whileTap, initial, animateDef, callbacks) => {
|
|
95
96
|
if (!whileTap)
|
|
96
97
|
return () => { };
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
98
|
+
pwLog('[tap] attached', { whileTap, initial, animateDef, hasHoverDef: !!callbacks?.hoverDef });
|
|
99
|
+
// Tween transitions prevent spring velocity accumulation during rapid
|
|
100
|
+
// press/release cycles. Cubic-bezier with slight overshoot mimics the
|
|
101
|
+
// reference spring feel (~275ms settle, ~7% overshoot).
|
|
102
|
+
const pressTransition = { duration: 0.25, ease: [0.22, 1.1, 0.36, 1] };
|
|
103
|
+
const releaseTransition = { duration: 0.3, ease: [0.22, 1.1, 0.36, 1] };
|
|
104
|
+
// Single control tracking whatever gesture animation is in-flight
|
|
105
|
+
// (tap, reset, or hover reapply). Every new gesture cancels the previous.
|
|
106
|
+
let gestureCtl = null;
|
|
107
|
+
const cancelGesture = () => {
|
|
108
|
+
if (gestureCtl) {
|
|
109
|
+
pwLog('[tap] cancel-gesture', {
|
|
110
|
+
currentTime: gestureCtl.currentTime,
|
|
111
|
+
transform: getComputedStyle(el).transform
|
|
112
|
+
});
|
|
112
113
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
try {
|
|
115
|
+
// Use stop() instead of cancel(). cancel() reverts to the
|
|
116
|
+
// pre-animation state (causing a visual snap), while stop()
|
|
117
|
+
// holds the element at its current interpolated position.
|
|
118
|
+
gestureCtl?.stop();
|
|
116
119
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
};
|
|
120
|
-
const handlePointerDown = (event) => {
|
|
121
|
-
// Capture pointer so we receive up/cancel even if pointer leaves the element
|
|
122
|
-
if (typeof event.pointerId === 'number') {
|
|
123
|
-
try {
|
|
124
|
-
if ('setPointerCapture' in el) {
|
|
125
|
-
el.setPointerCapture(event.pointerId);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
catch {
|
|
129
|
-
// noop if not supported
|
|
130
|
-
}
|
|
131
|
-
activePointerId = event.pointerId;
|
|
132
|
-
// Attach global listeners to catch off-element releases (even if capture unsupported)
|
|
133
|
-
window.addEventListener('pointerup', handlePointerUp);
|
|
134
|
-
window.addEventListener('pointercancel', handlePointerCancel);
|
|
135
|
-
document.addEventListener('pointerup', handlePointerUp);
|
|
136
|
-
document.addEventListener('pointercancel', handlePointerCancel);
|
|
120
|
+
catch {
|
|
121
|
+
// ignore
|
|
137
122
|
}
|
|
138
|
-
|
|
139
|
-
|
|
123
|
+
gestureCtl = null;
|
|
124
|
+
};
|
|
125
|
+
const animateTap = () => {
|
|
126
|
+
pwLog('[tap] animate-tap', {
|
|
140
127
|
w: el.getBoundingClientRect().width,
|
|
141
128
|
h: el.getBoundingClientRect().height,
|
|
142
129
|
transform: getComputedStyle(el).transform,
|
|
143
|
-
|
|
144
|
-
|
|
130
|
+
whileTap,
|
|
131
|
+
gestureActive: gestureCtl !== null
|
|
145
132
|
});
|
|
146
|
-
|
|
147
|
-
try {
|
|
148
|
-
resetCtl?.cancel();
|
|
149
|
-
}
|
|
150
|
-
catch {
|
|
151
|
-
// ignore
|
|
152
|
-
}
|
|
153
|
-
resetCtl = null;
|
|
154
|
-
if (!baselineTransform)
|
|
155
|
-
baselineTransform = computeCanonicalBaseline();
|
|
156
|
-
// Apply baseline immediately before whileTap
|
|
157
|
-
if (baselineTransform && baselineTransform !== 'none')
|
|
158
|
-
el.style.transform = baselineTransform;
|
|
159
|
-
else
|
|
160
|
-
el.style.removeProperty('transform');
|
|
161
|
-
// Software limit: if incoming scale is runaway, skip whileTap this cycle and force settle
|
|
162
|
-
const baselineScale = parseMatrixScale(baselineTransform) ?? 1;
|
|
163
|
-
const scaleDiff = Math.abs(incomingScale - baselineScale);
|
|
164
|
-
const tolerance = 0.02; // 2% threshold
|
|
165
|
-
pwLog('[tap] scale-check', { incomingScale, baselineScale, scaleDiff, tolerance });
|
|
166
|
-
if (scaleDiff > tolerance) {
|
|
167
|
-
pwLog('[tap] runaway-detected-skip-whileTap', {
|
|
168
|
-
incomingScale,
|
|
169
|
-
baselineScale,
|
|
170
|
-
scaleDiff
|
|
171
|
-
});
|
|
172
|
-
callbacks?.onTapStart?.();
|
|
173
|
-
tapCtl = null;
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
pwLog('[tap] whileTap-def', whileTap);
|
|
133
|
+
cancelGesture();
|
|
177
134
|
callbacks?.onTapStart?.();
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
182
|
-
catch {
|
|
183
|
-
// ignore cancellation errors
|
|
184
|
-
}
|
|
185
|
-
tapCtl = animate(el, whileTap);
|
|
186
|
-
// Safety clamp for runaway scale
|
|
187
|
-
if (safetyGuards) {
|
|
188
|
-
const a = parseMatrixScale(getComputedStyle(el).transform) ??
|
|
189
|
-
parseMatrixScale(el.style.transform);
|
|
190
|
-
if (a !== null && Math.abs(a) > 8) {
|
|
191
|
-
if (baselineTransform && baselineTransform !== 'none')
|
|
192
|
-
el.style.transform = baselineTransform;
|
|
193
|
-
else
|
|
194
|
-
el.style.removeProperty('transform');
|
|
195
|
-
pwLog('[tap] clamp-on-down', { a, restored: getComputedStyle(el).transform });
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
Promise.resolve(tapCtl.finished)
|
|
199
|
-
.then(() => pwLog('[tap] applied', {
|
|
135
|
+
gestureCtl = animate(el, whileTap, pressTransition);
|
|
136
|
+
Promise.resolve(gestureCtl?.finished)
|
|
137
|
+
.then(() => pwLog('[tap] tap-applied', {
|
|
200
138
|
w: el.getBoundingClientRect().width,
|
|
201
139
|
h: el.getBoundingClientRect().height,
|
|
202
140
|
transform: getComputedStyle(el).transform
|
|
@@ -204,210 +142,130 @@ export const attachWhileTap = (el, whileTap, initial, animateDef, callbacks) =>
|
|
|
204
142
|
.catch(() => { });
|
|
205
143
|
};
|
|
206
144
|
const reapplyHoverIfActive = () => {
|
|
207
|
-
if (!callbacks?.hoverDef)
|
|
145
|
+
if (!callbacks?.hoverDef) {
|
|
146
|
+
pwLog('[tap] hover-reapply-skip', { reason: 'no hoverDef' });
|
|
208
147
|
return false;
|
|
209
|
-
|
|
148
|
+
}
|
|
149
|
+
if (!isHoverCapable()) {
|
|
150
|
+
pwLog('[tap] hover-reapply-skip', { reason: 'not hover-capable' });
|
|
210
151
|
return false;
|
|
152
|
+
}
|
|
211
153
|
try {
|
|
212
|
-
if (!el.matches(':hover'))
|
|
154
|
+
if (!el.matches(':hover')) {
|
|
155
|
+
pwLog('[tap] hover-reapply-skip', { reason: 'not :hover' });
|
|
213
156
|
return false;
|
|
157
|
+
}
|
|
214
158
|
}
|
|
215
159
|
catch {
|
|
160
|
+
pwLog('[tap] hover-reapply-skip', { reason: 'matches threw' });
|
|
216
161
|
return false;
|
|
217
162
|
}
|
|
218
|
-
const { keyframes
|
|
219
|
-
|
|
163
|
+
const { keyframes } = splitHoverDefinition(callbacks.hoverDef);
|
|
164
|
+
pwLog('[tap] hover-reapply', {
|
|
165
|
+
keyframes,
|
|
166
|
+
transform: getComputedStyle(el).transform,
|
|
167
|
+
w: el.getBoundingClientRect().width
|
|
168
|
+
});
|
|
169
|
+
gestureCtl = animate(el, keyframes, releaseTransition);
|
|
170
|
+
Promise.resolve(gestureCtl?.finished)
|
|
171
|
+
.then(() => pwLog('[tap] hover-reapply-done', {
|
|
172
|
+
w: el.getBoundingClientRect().width,
|
|
173
|
+
transform: getComputedStyle(el).transform
|
|
174
|
+
}))
|
|
175
|
+
.catch(() => { });
|
|
220
176
|
return true;
|
|
221
177
|
};
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
return;
|
|
226
|
-
try {
|
|
227
|
-
if ('releasePointerCapture' in el)
|
|
228
|
-
el.releasePointerCapture(event.pointerId);
|
|
229
|
-
}
|
|
230
|
-
catch {
|
|
231
|
-
// noop
|
|
232
|
-
}
|
|
233
|
-
activePointerId = null;
|
|
234
|
-
window.removeEventListener('pointerup', handlePointerUp);
|
|
235
|
-
window.removeEventListener('pointercancel', handlePointerCancel);
|
|
236
|
-
document.removeEventListener('pointerup', handlePointerUp);
|
|
237
|
-
document.removeEventListener('pointercancel', handlePointerCancel);
|
|
238
|
-
}
|
|
239
|
-
callbacks?.onTap?.();
|
|
240
|
-
pwLog('[tap] pointerup', {
|
|
178
|
+
const animateReset = (success) => {
|
|
179
|
+
pwLog('[tap] animate-reset', {
|
|
180
|
+
success,
|
|
241
181
|
w: el.getBoundingClientRect().width,
|
|
242
182
|
h: el.getBoundingClientRect().height,
|
|
243
|
-
transform: getComputedStyle(el).transform
|
|
183
|
+
transform: getComputedStyle(el).transform,
|
|
184
|
+
gestureActive: gestureCtl !== null
|
|
244
185
|
});
|
|
245
|
-
if (
|
|
246
|
-
|
|
247
|
-
// Ensure the whileTap animation can't finish and overwrite our reset
|
|
248
|
-
try {
|
|
249
|
-
tapCtl?.cancel();
|
|
250
|
-
}
|
|
251
|
-
catch {
|
|
252
|
-
// ignore
|
|
253
|
-
}
|
|
254
|
-
tapCtl = null;
|
|
255
|
-
const style = el.style;
|
|
256
|
-
if (baselineTransform && baselineTransform !== 'none')
|
|
257
|
-
style.transform = baselineTransform;
|
|
186
|
+
if (success)
|
|
187
|
+
callbacks?.onTap?.();
|
|
258
188
|
else
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
189
|
+
callbacks?.onTapCancel?.();
|
|
190
|
+
cancelGesture();
|
|
191
|
+
// If still hovering after a successful tap, animate to hover state
|
|
192
|
+
if (success && reapplyHoverIfActive())
|
|
193
|
+
return;
|
|
264
194
|
const resetRecord = buildTapResetRecord(initial ?? {}, animateDef ?? {}, whileTap ?? {});
|
|
265
195
|
pwLog('[tap] reset-record', resetRecord);
|
|
266
196
|
if (Object.keys(resetRecord).length > 0) {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
};
|
|
271
|
-
resetCtl = animate(el, resetRecord, resetTransition);
|
|
272
|
-
Promise.resolve(resetCtl.finished)
|
|
273
|
-
.then(() => pwLog('[tap] reset-finished', {
|
|
197
|
+
gestureCtl = animate(el, resetRecord, releaseTransition);
|
|
198
|
+
Promise.resolve(gestureCtl?.finished)
|
|
199
|
+
.then(() => pwLog('[tap] reset-done', {
|
|
274
200
|
w: el.getBoundingClientRect().width,
|
|
275
201
|
h: el.getBoundingClientRect().height,
|
|
276
202
|
transform: getComputedStyle(el).transform
|
|
277
203
|
}))
|
|
278
|
-
.catch(() => { })
|
|
279
|
-
.finally(() => {
|
|
280
|
-
resetCtl = null;
|
|
281
|
-
});
|
|
204
|
+
.catch(() => { });
|
|
282
205
|
}
|
|
283
|
-
// After baseline and reset committed, optionally reapply hover on next frame
|
|
284
|
-
queueMicrotask(() => {
|
|
285
|
-
reapplyHoverIfActive();
|
|
286
|
-
});
|
|
287
206
|
};
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
return;
|
|
292
|
-
try {
|
|
293
|
-
if ('releasePointerCapture' in el)
|
|
294
|
-
el.releasePointerCapture(event.pointerId);
|
|
295
|
-
}
|
|
296
|
-
catch {
|
|
297
|
-
// noop
|
|
298
|
-
}
|
|
299
|
-
activePointerId = null;
|
|
300
|
-
window.removeEventListener('pointerup', handlePointerUp);
|
|
301
|
-
window.removeEventListener('pointercancel', handlePointerCancel);
|
|
302
|
-
document.removeEventListener('pointerup', handlePointerUp);
|
|
303
|
-
document.removeEventListener('pointercancel', handlePointerCancel);
|
|
304
|
-
}
|
|
305
|
-
callbacks?.onTapCancel?.();
|
|
306
|
-
pwLog('[tap] cancel', {
|
|
207
|
+
// Use press() for pointer + Enter key handling
|
|
208
|
+
const cancelPress = press(el, () => {
|
|
209
|
+
pwLog('[tap] press-start', {
|
|
307
210
|
w: el.getBoundingClientRect().width,
|
|
308
|
-
|
|
211
|
+
transform: getComputedStyle(el).transform
|
|
309
212
|
});
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
};
|
|
325
|
-
const handleKeyDown = (e) => {
|
|
326
|
-
if (!(e.key === 'Enter' || e.key === ' ' || e.key === 'Space'))
|
|
213
|
+
animateTap();
|
|
214
|
+
return (_endEvent, { success }) => {
|
|
215
|
+
pwLog('[tap] press-end', {
|
|
216
|
+
success,
|
|
217
|
+
w: el.getBoundingClientRect().width,
|
|
218
|
+
transform: getComputedStyle(el).transform
|
|
219
|
+
});
|
|
220
|
+
animateReset(success);
|
|
221
|
+
};
|
|
222
|
+
});
|
|
223
|
+
// Add Space key support (press() only handles Enter)
|
|
224
|
+
let spaceActive = false;
|
|
225
|
+
const onKeyDown = (e) => {
|
|
226
|
+
if (e.key !== ' ' && e.key !== 'Space')
|
|
327
227
|
return;
|
|
328
|
-
|
|
329
|
-
if (
|
|
330
|
-
e.preventDefault?.();
|
|
331
|
-
if (keyboardActive)
|
|
228
|
+
e.preventDefault();
|
|
229
|
+
if (spaceActive)
|
|
332
230
|
return;
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
pwLog('[tap] keydown', {
|
|
336
|
-
key: e.key,
|
|
231
|
+
spaceActive = true;
|
|
232
|
+
pwLog('[tap] space-down', {
|
|
337
233
|
w: el.getBoundingClientRect().width,
|
|
338
|
-
|
|
234
|
+
transform: getComputedStyle(el).transform
|
|
339
235
|
});
|
|
340
|
-
|
|
341
|
-
tapCtl?.cancel();
|
|
342
|
-
}
|
|
343
|
-
catch {
|
|
344
|
-
// ignore
|
|
345
|
-
}
|
|
346
|
-
tapCtl = animate(el, whileTap);
|
|
236
|
+
animateTap();
|
|
347
237
|
};
|
|
348
|
-
const
|
|
349
|
-
if (
|
|
238
|
+
const onKeyUp = (e) => {
|
|
239
|
+
if (e.key !== ' ' && e.key !== 'Space')
|
|
350
240
|
return;
|
|
351
|
-
|
|
352
|
-
if (
|
|
353
|
-
e.preventDefault?.();
|
|
354
|
-
if (!keyboardActive)
|
|
241
|
+
e.preventDefault();
|
|
242
|
+
if (!spaceActive)
|
|
355
243
|
return;
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
pwLog('[tap] keyup', {
|
|
359
|
-
key: e.key,
|
|
244
|
+
spaceActive = false;
|
|
245
|
+
pwLog('[tap] space-up', {
|
|
360
246
|
w: el.getBoundingClientRect().width,
|
|
361
|
-
|
|
247
|
+
transform: getComputedStyle(el).transform
|
|
362
248
|
});
|
|
363
|
-
|
|
364
|
-
return;
|
|
365
|
-
if (initial || animateDef) {
|
|
366
|
-
try {
|
|
367
|
-
tapCtl?.cancel();
|
|
368
|
-
}
|
|
369
|
-
catch {
|
|
370
|
-
// ignore
|
|
371
|
-
}
|
|
372
|
-
tapCtl = null;
|
|
373
|
-
const resetRecord = buildTapResetRecord(initial ?? {}, animateDef ?? {}, whileTap ?? {});
|
|
374
|
-
if (Object.keys(resetRecord).length > 0) {
|
|
375
|
-
animate(el, resetRecord);
|
|
376
|
-
}
|
|
377
|
-
}
|
|
249
|
+
animateReset(true);
|
|
378
250
|
};
|
|
379
|
-
const
|
|
380
|
-
if (!
|
|
251
|
+
const onBlur = () => {
|
|
252
|
+
if (!spaceActive)
|
|
381
253
|
return;
|
|
382
|
-
|
|
383
|
-
callbacks?.onTapCancel?.();
|
|
254
|
+
spaceActive = false;
|
|
384
255
|
pwLog('[tap] blur', {
|
|
385
256
|
w: el.getBoundingClientRect().width,
|
|
386
|
-
|
|
257
|
+
transform: getComputedStyle(el).transform
|
|
387
258
|
});
|
|
388
|
-
|
|
389
|
-
const resetRecord = buildTapResetRecord(initial ?? {}, animateDef ?? {}, whileTap ?? {});
|
|
390
|
-
if (Object.keys(resetRecord).length > 0) {
|
|
391
|
-
animate(el, resetRecord);
|
|
392
|
-
}
|
|
393
|
-
}
|
|
259
|
+
animateReset(false);
|
|
394
260
|
};
|
|
395
|
-
el.addEventListener('
|
|
396
|
-
el.addEventListener('
|
|
397
|
-
el.addEventListener('
|
|
398
|
-
el.addEventListener('keydown', handleKeyDown);
|
|
399
|
-
el.addEventListener('keyup', handleKeyUp);
|
|
400
|
-
el.addEventListener('blur', handleBlur);
|
|
261
|
+
el.addEventListener('keydown', onKeyDown);
|
|
262
|
+
el.addEventListener('keyup', onKeyUp);
|
|
263
|
+
el.addEventListener('blur', onBlur);
|
|
401
264
|
return () => {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
el.removeEventListener('
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
document.removeEventListener('pointerup', handlePointerUp);
|
|
408
|
-
document.removeEventListener('pointercancel', handlePointerCancel);
|
|
409
|
-
el.removeEventListener('keydown', handleKeyDown);
|
|
410
|
-
el.removeEventListener('keyup', handleKeyUp);
|
|
411
|
-
el.removeEventListener('blur', handleBlur);
|
|
265
|
+
pwLog('[tap] cleanup');
|
|
266
|
+
cancelPress();
|
|
267
|
+
el.removeEventListener('keydown', onKeyDown);
|
|
268
|
+
el.removeEventListener('keyup', onKeyUp);
|
|
269
|
+
el.removeEventListener('blur', onBlur);
|
|
412
270
|
};
|
|
413
271
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@humanspeak/svelte-motion",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.23",
|
|
4
4
|
"description": "A lightweight animation library for Svelte 5 that provides smooth, hardware-accelerated animations. Features include spring physics, custom easing, and fluid transitions. Built on top of the motion library, it offers a simple API for creating complex animations with minimal code. Perfect for interactive UIs, micro-interactions, and engaging user experiences.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"svelte",
|
|
@@ -53,29 +53,29 @@
|
|
|
53
53
|
}
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"motion": "^12.34.
|
|
57
|
-
"motion-dom": "^12.34.
|
|
56
|
+
"motion": "^12.34.2",
|
|
57
|
+
"motion-dom": "^12.34.2"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@changesets/cli": "^2.29.8",
|
|
61
61
|
"@eslint/compat": "^2.0.2",
|
|
62
|
-
"@eslint/js": "^
|
|
62
|
+
"@eslint/js": "^10.0.1",
|
|
63
63
|
"@playwright/test": "^1.58.2",
|
|
64
64
|
"@sveltejs/adapter-auto": "^7.0.1",
|
|
65
|
-
"@sveltejs/kit": "^2.
|
|
65
|
+
"@sveltejs/kit": "^2.52.2",
|
|
66
66
|
"@sveltejs/package": "^2.5.7",
|
|
67
67
|
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
|
68
68
|
"@tailwindcss/aspect-ratio": "^0.4.2",
|
|
69
69
|
"@tailwindcss/container-queries": "^0.1.1",
|
|
70
70
|
"@tailwindcss/forms": "^0.5.11",
|
|
71
|
-
"@tailwindcss/postcss": "^4.
|
|
71
|
+
"@tailwindcss/postcss": "^4.2.0",
|
|
72
72
|
"@tailwindcss/typography": "^0.5.19",
|
|
73
73
|
"@testing-library/jest-dom": "^6.9.1",
|
|
74
74
|
"@testing-library/svelte": "^5.3.1",
|
|
75
|
-
"@types/node": "^25.
|
|
75
|
+
"@types/node": "^25.3.0",
|
|
76
76
|
"@vitest/coverage-v8": "^4.0.18",
|
|
77
77
|
"concurrently": "^9.2.1",
|
|
78
|
-
"eslint": "^
|
|
78
|
+
"eslint": "^10.0.0",
|
|
79
79
|
"eslint-config-prettier": "10.1.8",
|
|
80
80
|
"eslint-plugin-import": "2.32.0",
|
|
81
81
|
"eslint-plugin-svelte": "3.15.0",
|
|
@@ -85,24 +85,24 @@
|
|
|
85
85
|
"html-tags": "^5.1.0",
|
|
86
86
|
"html-void-elements": "^3.0.0",
|
|
87
87
|
"husky": "^9.1.7",
|
|
88
|
-
"jsdom": "^28.
|
|
88
|
+
"jsdom": "^28.1.0",
|
|
89
89
|
"prettier": "^3.8.1",
|
|
90
90
|
"prettier-plugin-organize-imports": "^4.3.0",
|
|
91
91
|
"prettier-plugin-sort-json": "^4.2.0",
|
|
92
|
-
"prettier-plugin-svelte": "^3.
|
|
92
|
+
"prettier-plugin-svelte": "^3.5.0",
|
|
93
93
|
"prettier-plugin-tailwindcss": "^0.7.2",
|
|
94
94
|
"publint": "^0.3.17",
|
|
95
95
|
"runed": "0.37.1",
|
|
96
|
-
"svelte": "^5.
|
|
97
|
-
"svelte-check": "^4.4.
|
|
96
|
+
"svelte": "^5.53.0",
|
|
97
|
+
"svelte-check": "^4.4.1",
|
|
98
98
|
"svg-tags": "^1.0.0",
|
|
99
|
-
"tailwind-merge": "^3.
|
|
99
|
+
"tailwind-merge": "^3.5.0",
|
|
100
100
|
"tailwind-variants": "^3.2.2",
|
|
101
|
-
"tailwindcss": "^4.
|
|
101
|
+
"tailwindcss": "^4.2.0",
|
|
102
102
|
"tailwindcss-animate": "^1.0.7",
|
|
103
103
|
"tsx": "^4.21.0",
|
|
104
104
|
"typescript": "^5.9.3",
|
|
105
|
-
"typescript-eslint": "^8.
|
|
105
|
+
"typescript-eslint": "^8.56.0",
|
|
106
106
|
"vite": "^7.3.1",
|
|
107
107
|
"vite-tsconfig-paths": "^6.1.1",
|
|
108
108
|
"vitest": "^4.0.18"
|