@humanspeak/svelte-motion 0.5.4 → 0.6.1
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/motionDomProjection.context.d.ts +13 -0
- package/dist/components/motionDomProjection.context.js +18 -0
- package/dist/html/_MotionContainer.svelte +831 -81
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/types.d.ts +87 -3
- package/dist/utils/animationControls.svelte.d.ts +63 -0
- package/dist/utils/animationControls.svelte.js +111 -0
- package/dist/utils/layout.d.ts +9 -6
- package/dist/utils/layout.js +148 -14
- package/dist/utils/motionDomProjection.d.ts +155 -0
- package/dist/utils/motionDomProjection.js +279 -0
- package/dist/utils/optimizedAppear.d.ts +141 -0
- package/dist/utils/optimizedAppear.js +311 -0
- package/dist/utils/presence.d.ts +3 -2
- package/dist/utils/presence.js +49 -12
- package/dist/utils/projection.d.ts +3 -3
- package/dist/utils/projection.js +1 -1
- package/dist/utils/svg.d.ts +4 -4
- package/dist/utils/svg.js +44 -25
- package/package.json +1 -1
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { mergeInlineStyles } from './style';
|
|
2
|
+
import { resolveRestingValues } from './variants';
|
|
3
|
+
import { startWaapiAnimation } from 'motion';
|
|
4
|
+
import { mapEasingToNativeEasing, optimizedAppearDataAttribute, optimizedAppearDataId, transformProps } from 'motion-dom';
|
|
5
|
+
const appearStoreId = (elementId, valueName) => {
|
|
6
|
+
const key = transformProps.has(valueName) ? 'transform' : valueName;
|
|
7
|
+
return `${elementId}: ${key}`;
|
|
8
|
+
};
|
|
9
|
+
const getAppearStore = () => {
|
|
10
|
+
if (typeof window === 'undefined')
|
|
11
|
+
return undefined;
|
|
12
|
+
window.__SvelteMotionAppear ??= {
|
|
13
|
+
animations: new Map(),
|
|
14
|
+
complete: new Map(),
|
|
15
|
+
started: []
|
|
16
|
+
};
|
|
17
|
+
return window.__SvelteMotionAppear;
|
|
18
|
+
};
|
|
19
|
+
const installAppearGlobals = () => {
|
|
20
|
+
if (typeof window === 'undefined')
|
|
21
|
+
return;
|
|
22
|
+
const store = getAppearStore();
|
|
23
|
+
if (!store)
|
|
24
|
+
return;
|
|
25
|
+
window.MotionHasOptimisedAnimation ??= (elementId, valueName) => {
|
|
26
|
+
if (!elementId)
|
|
27
|
+
return false;
|
|
28
|
+
if (!valueName)
|
|
29
|
+
return store.complete.has(elementId);
|
|
30
|
+
return store.animations.has(appearStoreId(elementId, valueName));
|
|
31
|
+
};
|
|
32
|
+
window.MotionHandoffMarkAsComplete ??= (elementId) => {
|
|
33
|
+
if (store.complete.has(elementId)) {
|
|
34
|
+
store.complete.set(elementId, true);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
window.MotionHandoffIsComplete ??= (elementId) => {
|
|
38
|
+
return store.complete.get(elementId) === true;
|
|
39
|
+
};
|
|
40
|
+
window.MotionCancelOptimisedAnimation ??= (elementId, valueName) => {
|
|
41
|
+
if (!elementId || !valueName)
|
|
42
|
+
return;
|
|
43
|
+
const animationId = appearStoreId(elementId, valueName);
|
|
44
|
+
const data = store.animations.get(animationId);
|
|
45
|
+
if (!data)
|
|
46
|
+
return;
|
|
47
|
+
data.animation.cancel();
|
|
48
|
+
store.animations.delete(animationId);
|
|
49
|
+
if (!store.animations.size) {
|
|
50
|
+
window.MotionCancelOptimisedAnimation = undefined;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
const readStyleProp = (style, prop) => {
|
|
55
|
+
return style
|
|
56
|
+
.split(';')
|
|
57
|
+
.map((part) => part.trim())
|
|
58
|
+
.find((part) => part.startsWith(`${prop}:`))
|
|
59
|
+
?.slice(prop.length + 1)
|
|
60
|
+
.trim();
|
|
61
|
+
};
|
|
62
|
+
const toNativeOptions = (transition) => {
|
|
63
|
+
const duration = typeof transition?.duration === 'number' ? transition.duration : 0.3;
|
|
64
|
+
const delay = typeof transition?.delay === 'number' ? transition.delay : 0;
|
|
65
|
+
const durationMs = duration * 1000;
|
|
66
|
+
const options = {
|
|
67
|
+
duration: durationMs,
|
|
68
|
+
delay: delay * 1000,
|
|
69
|
+
fill: 'both'
|
|
70
|
+
};
|
|
71
|
+
const easing = mapEasingToNativeEasing(transition?.ease, durationMs);
|
|
72
|
+
if (Array.isArray(easing)) {
|
|
73
|
+
options.easing = easing[0] ?? 'linear';
|
|
74
|
+
}
|
|
75
|
+
else if (easing) {
|
|
76
|
+
options.easing = easing;
|
|
77
|
+
}
|
|
78
|
+
return options;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Build serialisable optimized-appear animation entries from an initial and
|
|
82
|
+
* animate pair.
|
|
83
|
+
*
|
|
84
|
+
* @param initial Initial keyframes reflected into SSR markup.
|
|
85
|
+
* @param animate Target keyframes for the enter animation.
|
|
86
|
+
* @param transition Motion transition options.
|
|
87
|
+
* @returns Appear entries for WAAPI-supported opacity and transform values.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```ts
|
|
91
|
+
* const entries = createOptimizedAppearData(
|
|
92
|
+
* { opacity: 0, scale: 0.8 },
|
|
93
|
+
* { opacity: 1, scale: 1 },
|
|
94
|
+
* { duration: 0.6, ease: [0.16, 1, 0.3, 1] }
|
|
95
|
+
* )
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
export const createOptimizedAppearData = (initial, animate, transition) => {
|
|
99
|
+
if (!initial || !animate)
|
|
100
|
+
return [];
|
|
101
|
+
const target = resolveRestingValues(animate);
|
|
102
|
+
if (!target)
|
|
103
|
+
return [];
|
|
104
|
+
const options = toNativeOptions(transition);
|
|
105
|
+
const entries = [];
|
|
106
|
+
if (initial.opacity != null && target.opacity != null) {
|
|
107
|
+
entries.push({
|
|
108
|
+
name: 'opacity',
|
|
109
|
+
keyframes: [
|
|
110
|
+
Array.isArray(initial.opacity) ? initial.opacity[0] : initial.opacity,
|
|
111
|
+
Array.isArray(target.opacity) ? target.opacity[0] : target.opacity
|
|
112
|
+
],
|
|
113
|
+
options
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
const initialTransform = readStyleProp(mergeInlineStyles('', initial), 'transform');
|
|
117
|
+
const targetTransform = readStyleProp(mergeInlineStyles('', target), 'transform');
|
|
118
|
+
if (initialTransform && targetTransform && initialTransform !== targetTransform) {
|
|
119
|
+
entries.push({
|
|
120
|
+
name: 'transform',
|
|
121
|
+
keyframes: [initialTransform, targetTransform],
|
|
122
|
+
options
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return entries;
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Create the inline SSR bootstrap that starts appear animations before Svelte
|
|
129
|
+
* hydrates the component tree.
|
|
130
|
+
*
|
|
131
|
+
* @param appearId Stable optimized-appear id attached to the motion element.
|
|
132
|
+
* @param entries WAAPI animation entries to start.
|
|
133
|
+
* @returns A script tag string, or an empty string when no entries exist.
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* ```ts
|
|
137
|
+
* const script = createOptimizedAppearScript('appear-1', [
|
|
138
|
+
* { name: 'opacity', keyframes: [0, 1], options: { duration: 300, fill: 'both' } }
|
|
139
|
+
* ])
|
|
140
|
+
* ```
|
|
141
|
+
*/
|
|
142
|
+
export const createOptimizedAppearScript = (appearId, entries) => {
|
|
143
|
+
if (!appearId || entries.length === 0)
|
|
144
|
+
return '';
|
|
145
|
+
const payload = JSON.stringify({ id: appearId, entries }).replace(/</g, '\\u003c');
|
|
146
|
+
return `<script>(()=>{const p=${payload},w=window;if(w.MotionIsMounted)return;const q=String(p.id).replace(/["\\\\]/g,"\\\\$&");const e=document.querySelector('[${optimizedAppearDataAttribute}="'+q+'"]');if(!e||!e.animate)return;const s=w.__SvelteMotionAppear||(w.__SvelteMotionAppear={animations:new Map,complete:new Map,started:[]});const k=(id,n)=>id+": "+(n==="transform"?"transform":n);w.MotionHasOptimisedAnimation=w.MotionHasOptimisedAnimation||((id,n)=>id?n?s.animations.has(k(id,n)):s.complete.has(id):false);w.MotionHandoffMarkAsComplete=w.MotionHandoffMarkAsComplete||((id)=>{if(s.complete.has(id))s.complete.set(id,true)});w.MotionHandoffIsComplete=w.MotionHandoffIsComplete||((id)=>s.complete.get(id)===true);w.MotionCancelOptimisedAnimation=w.MotionCancelOptimisedAnimation||((id,n)=>{const key=k(id,n),d=s.animations.get(key);if(!d)return;d.animation.cancel();s.animations.delete(key);if(!s.animations.size)w.MotionCancelOptimisedAnimation=undefined});s.complete.set(p.id,false);for(const a of p.entries){const key=k(p.id,a.name);if(!s.readyAnimation){s.readyAnimation=e.animate({[a.name]:[a.keyframes[0],a.keyframes[0]]},{duration:1e4,easing:"linear",fill:"both"});s.animations.set(key,{animation:s.readyAnimation,startTime:null})}const start=()=>{s.readyAnimation.cancel();let t=s.startFrameTime;if(t===undefined){t=performance.now();s.startFrameTime=t}const anim=e.animate({[a.name]:a.keyframes},a.options);anim.startTime=t;s.animations.set(key,{animation:anim,startTime:t});s.started.push({id:p.id,name:a.name})};const r=s.readyAnimation;r.ready?r.ready.then(start).catch(()=>{}):start()}})();</script>`;
|
|
147
|
+
};
|
|
148
|
+
/**
|
|
149
|
+
* Start an optimized appear animation imperatively.
|
|
150
|
+
*
|
|
151
|
+
* Mirrors Framer Motion's `startOptimizedAppearAnimation`: if Motion has
|
|
152
|
+
* already mounted, this intentionally does nothing.
|
|
153
|
+
*
|
|
154
|
+
* @param element Element carrying `data-framer-appear-id`.
|
|
155
|
+
* @param name CSS property to animate.
|
|
156
|
+
* @param keyframes WAAPI keyframes for the property.
|
|
157
|
+
* @param options Motion animation options.
|
|
158
|
+
* @param onReady Optional callback receiving the started animation.
|
|
159
|
+
* @returns Nothing.
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```ts
|
|
163
|
+
* const element = document.querySelector('[data-framer-appear-id]')
|
|
164
|
+
* if (element instanceof HTMLElement) {
|
|
165
|
+
* startOptimizedAppearAnimation(element, 'opacity', [0, 1], { duration: 0.3 })
|
|
166
|
+
* }
|
|
167
|
+
* ```
|
|
168
|
+
*/
|
|
169
|
+
export const startOptimizedAppearAnimation = (element, name, keyframes, options, onReady) => {
|
|
170
|
+
if (typeof window === 'undefined' || window.MotionIsMounted)
|
|
171
|
+
return;
|
|
172
|
+
const id = element.dataset[optimizedAppearDataId];
|
|
173
|
+
if (!id)
|
|
174
|
+
return;
|
|
175
|
+
installAppearGlobals();
|
|
176
|
+
const store = getAppearStore();
|
|
177
|
+
if (!store)
|
|
178
|
+
return;
|
|
179
|
+
const storeId = appearStoreId(id, name);
|
|
180
|
+
if (!store.readyAnimation) {
|
|
181
|
+
store.readyAnimation = startWaapiAnimation(element, name, [keyframes[0], keyframes[0]], {
|
|
182
|
+
duration: 10000,
|
|
183
|
+
ease: 'linear'
|
|
184
|
+
});
|
|
185
|
+
store.animations.set(storeId, { animation: store.readyAnimation, startTime: null });
|
|
186
|
+
}
|
|
187
|
+
const startAnimation = () => {
|
|
188
|
+
store.readyAnimation?.cancel();
|
|
189
|
+
store.startFrameTime ??= performance.now();
|
|
190
|
+
const animation = startWaapiAnimation(element, name, keyframes, options);
|
|
191
|
+
animation.startTime = store.startFrameTime;
|
|
192
|
+
store.animations.set(storeId, { animation, startTime: store.startFrameTime });
|
|
193
|
+
store.started.push({ id, name });
|
|
194
|
+
onReady?.(animation);
|
|
195
|
+
};
|
|
196
|
+
store.complete.set(id, false);
|
|
197
|
+
const readyAnimation = store.readyAnimation;
|
|
198
|
+
if (readyAnimation.ready) {
|
|
199
|
+
readyAnimation.ready.then(startAnimation).catch(() => { });
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
startAnimation();
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
/**
|
|
206
|
+
* Commit and cancel optimized appear animations for an element.
|
|
207
|
+
*
|
|
208
|
+
* @param elementId Optimized appear id.
|
|
209
|
+
* @returns `true` when at least one optimized animation was handed off.
|
|
210
|
+
*
|
|
211
|
+
* @example
|
|
212
|
+
* ```ts
|
|
213
|
+
* const wasHandedOff = handoffOptimizedAppearAnimation('appear-1')
|
|
214
|
+
* if (wasHandedOff) {
|
|
215
|
+
* console.log('Animation handed off to runtime')
|
|
216
|
+
* }
|
|
217
|
+
* ```
|
|
218
|
+
*/
|
|
219
|
+
export const handoffOptimizedAppearAnimation = (elementId) => {
|
|
220
|
+
if (!elementId || typeof window === 'undefined')
|
|
221
|
+
return false;
|
|
222
|
+
const store = getAppearStore();
|
|
223
|
+
if (!store)
|
|
224
|
+
return false;
|
|
225
|
+
let handedOff = false;
|
|
226
|
+
for (const [key, data] of [...store.animations]) {
|
|
227
|
+
if (!key.startsWith(`${elementId}: `))
|
|
228
|
+
continue;
|
|
229
|
+
data.animation.commitStyles?.();
|
|
230
|
+
data.animation.cancel();
|
|
231
|
+
store.animations.delete(key);
|
|
232
|
+
handedOff = true;
|
|
233
|
+
}
|
|
234
|
+
if (store.complete.has(elementId)) {
|
|
235
|
+
store.complete.set(elementId, true);
|
|
236
|
+
}
|
|
237
|
+
return handedOff;
|
|
238
|
+
};
|
|
239
|
+
/**
|
|
240
|
+
* Let active optimized appear animations finish before handing their final
|
|
241
|
+
* styles back to Svelte Motion.
|
|
242
|
+
*
|
|
243
|
+
* @param elementId Optimized appear id.
|
|
244
|
+
* @returns Whether at least one optimized animation was adopted.
|
|
245
|
+
*
|
|
246
|
+
* @example
|
|
247
|
+
* ```ts
|
|
248
|
+
* const wasAdopted = await finishOptimizedAppearAnimation('appear-1')
|
|
249
|
+
* if (wasAdopted) {
|
|
250
|
+
* console.log('Animation finished and adopted')
|
|
251
|
+
* }
|
|
252
|
+
* ```
|
|
253
|
+
*/
|
|
254
|
+
export const finishOptimizedAppearAnimation = async (elementId) => {
|
|
255
|
+
if (!elementId || typeof window === 'undefined')
|
|
256
|
+
return false;
|
|
257
|
+
const store = getAppearStore();
|
|
258
|
+
if (!store)
|
|
259
|
+
return false;
|
|
260
|
+
let entries = [...store.animations].filter(([key]) => key.startsWith(`${elementId}: `));
|
|
261
|
+
if (!entries.length)
|
|
262
|
+
return false;
|
|
263
|
+
await Promise.all(entries.map(([, data]) => data.startTime === null ? data.animation.ready?.catch(() => undefined) : undefined));
|
|
264
|
+
entries = [...store.animations].filter(([key]) => key.startsWith(`${elementId}: `));
|
|
265
|
+
await Promise.all(entries.map(([, data]) => data.animation.finished.catch(() => undefined)));
|
|
266
|
+
for (const [key, data] of entries) {
|
|
267
|
+
if (!store.animations.has(key))
|
|
268
|
+
continue;
|
|
269
|
+
data.animation.commitStyles?.();
|
|
270
|
+
data.animation.cancel();
|
|
271
|
+
store.animations.delete(key);
|
|
272
|
+
}
|
|
273
|
+
if (store.complete.has(elementId)) {
|
|
274
|
+
store.complete.set(elementId, true);
|
|
275
|
+
}
|
|
276
|
+
return true;
|
|
277
|
+
};
|
|
278
|
+
/**
|
|
279
|
+
* Check whether an optimized appear animation is active for an element.
|
|
280
|
+
*
|
|
281
|
+
* @param elementId Optimized appear id.
|
|
282
|
+
* @returns Whether any optimized appear animation is currently registered.
|
|
283
|
+
*
|
|
284
|
+
* @example
|
|
285
|
+
* ```ts
|
|
286
|
+
* if (hasOptimizedAppearAnimation('appear-1')) {
|
|
287
|
+
* console.log('Animation is active')
|
|
288
|
+
* }
|
|
289
|
+
* ```
|
|
290
|
+
*/
|
|
291
|
+
export const hasOptimizedAppearAnimation = (elementId) => {
|
|
292
|
+
if (!elementId || typeof window === 'undefined')
|
|
293
|
+
return false;
|
|
294
|
+
return window.MotionHasOptimisedAnimation?.(elementId) ?? false;
|
|
295
|
+
};
|
|
296
|
+
/**
|
|
297
|
+
* Mark Motion as mounted so late optimized-appear starters no-op.
|
|
298
|
+
*
|
|
299
|
+
* @returns Nothing.
|
|
300
|
+
*
|
|
301
|
+
* @example
|
|
302
|
+
* ```ts
|
|
303
|
+
* markMotionMounted()
|
|
304
|
+
* ```
|
|
305
|
+
*/
|
|
306
|
+
export const markMotionMounted = () => {
|
|
307
|
+
if (typeof window !== 'undefined') {
|
|
308
|
+
window.MotionIsMounted = true;
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
export { optimizedAppearDataAttribute };
|
package/dist/utils/presence.d.ts
CHANGED
|
@@ -16,10 +16,11 @@ export type AnimatePresenceContext = {
|
|
|
16
16
|
*/
|
|
17
17
|
shouldAnimateEnter: (key: string) => boolean;
|
|
18
18
|
/**
|
|
19
|
-
* For mode='wait': Returns true if enters should be blocked
|
|
19
|
+
* For mode='wait': Returns true if enters should be blocked by an exiting
|
|
20
|
+
* or currently-present sibling.
|
|
20
21
|
* Motion elements should delay their enter animation until this returns false.
|
|
21
22
|
*/
|
|
22
|
-
isEnterBlocked: () => boolean;
|
|
23
|
+
isEnterBlocked: (key?: string) => boolean;
|
|
23
24
|
/**
|
|
24
25
|
* For mode='wait': Register a callback to be invoked when enters are unblocked.
|
|
25
26
|
* Returns an unsubscribe function.
|
package/dist/utils/presence.js
CHANGED
|
@@ -31,6 +31,15 @@ const resetTransforms = (element) => {
|
|
|
31
31
|
s.MozTransform = 'none';
|
|
32
32
|
s.OTransform = 'none';
|
|
33
33
|
};
|
|
34
|
+
const findLayoutInsertionParent = (element) => {
|
|
35
|
+
let before = element;
|
|
36
|
+
let parent = element.parentElement;
|
|
37
|
+
while (parent && getComputedStyle(parent).display === 'contents') {
|
|
38
|
+
before = parent;
|
|
39
|
+
parent = parent.parentElement;
|
|
40
|
+
}
|
|
41
|
+
return parent ? { parent, before } : null;
|
|
42
|
+
};
|
|
34
43
|
/**
|
|
35
44
|
* Create a new `AnimatePresence` context instance.
|
|
36
45
|
*
|
|
@@ -130,10 +139,20 @@ export const createAnimatePresenceContext = (context) => {
|
|
|
130
139
|
* }
|
|
131
140
|
* ```
|
|
132
141
|
*/
|
|
133
|
-
const isEnterBlocked = () => {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
142
|
+
const isEnterBlocked = (key) => {
|
|
143
|
+
if (mode !== 'wait') {
|
|
144
|
+
pwLog('[presence] isEnterBlocked', { blocked: false, mode, inFlightExits, key });
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
const hasBlockingSibling = key !== undefined && Array.from(children.keys()).some((childKey) => childKey !== key);
|
|
148
|
+
const blocked = inFlightExits > 0 || hasBlockingSibling;
|
|
149
|
+
pwLog('[presence] isEnterBlocked', {
|
|
150
|
+
blocked,
|
|
151
|
+
mode,
|
|
152
|
+
inFlightExits,
|
|
153
|
+
key,
|
|
154
|
+
hasBlockingSibling
|
|
155
|
+
});
|
|
137
156
|
return blocked;
|
|
138
157
|
};
|
|
139
158
|
/**
|
|
@@ -286,7 +305,8 @@ export const createAnimatePresenceContext = (context) => {
|
|
|
286
305
|
exit,
|
|
287
306
|
mergedTransition,
|
|
288
307
|
lastRect: initialRect,
|
|
289
|
-
lastComputedStyle: initialStyle
|
|
308
|
+
lastComputedStyle: initialStyle,
|
|
309
|
+
layoutInsertion: findLayoutInsertionParent(element) ?? undefined
|
|
290
310
|
});
|
|
291
311
|
};
|
|
292
312
|
/**
|
|
@@ -337,12 +357,14 @@ export const createAnimatePresenceContext = (context) => {
|
|
|
337
357
|
}
|
|
338
358
|
const rect = child.lastRect;
|
|
339
359
|
const computed = child.lastComputedStyle;
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
const shouldPreserveLayout = mode
|
|
360
|
+
// Wait mode holds the exiting layout slot while the entering child is hidden.
|
|
361
|
+
// Sync and popLayout must not add an extra layout participant.
|
|
362
|
+
const shouldPreserveLayout = mode === 'wait';
|
|
343
363
|
let placeholder = null;
|
|
344
|
-
const
|
|
345
|
-
|
|
364
|
+
const liveLayoutInsertion = findLayoutInsertionParent(child.element);
|
|
365
|
+
const layoutInsertion = liveLayoutInsertion ??
|
|
366
|
+
(child.layoutInsertion?.parent.isConnected ? child.layoutInsertion : null);
|
|
367
|
+
if (shouldPreserveLayout && layoutInsertion) {
|
|
346
368
|
placeholder = document.createElement(child.element.tagName.toLowerCase());
|
|
347
369
|
placeholder.setAttribute('data-presence-placeholder', 'true');
|
|
348
370
|
placeholder.style.display = computed.display === 'contents' ? 'block' : computed.display;
|
|
@@ -359,7 +381,22 @@ export const createAnimatePresenceContext = (context) => {
|
|
|
359
381
|
if (computed.alignSelf) {
|
|
360
382
|
placeholder.style.alignSelf = computed.alignSelf;
|
|
361
383
|
}
|
|
362
|
-
|
|
384
|
+
if (computed.gridColumnStart) {
|
|
385
|
+
placeholder.style.gridColumnStart = computed.gridColumnStart;
|
|
386
|
+
}
|
|
387
|
+
if (computed.gridColumnEnd) {
|
|
388
|
+
placeholder.style.gridColumnEnd = computed.gridColumnEnd;
|
|
389
|
+
}
|
|
390
|
+
if (computed.gridRowStart) {
|
|
391
|
+
placeholder.style.gridRowStart = computed.gridRowStart;
|
|
392
|
+
}
|
|
393
|
+
if (computed.gridRowEnd) {
|
|
394
|
+
placeholder.style.gridRowEnd = computed.gridRowEnd;
|
|
395
|
+
}
|
|
396
|
+
const before = layoutInsertion.before?.parentElement === layoutInsertion.parent
|
|
397
|
+
? layoutInsertion.before
|
|
398
|
+
: null;
|
|
399
|
+
layoutInsertion.parent.insertBefore(placeholder, before);
|
|
363
400
|
}
|
|
364
401
|
// Clone original node to preserve structure/classes, then inline computed styles to freeze look
|
|
365
402
|
const clone = child.element.cloneNode(true);
|
|
@@ -485,7 +522,6 @@ export const createAnimatePresenceContext = (context) => {
|
|
|
485
522
|
// ignore
|
|
486
523
|
}
|
|
487
524
|
clone.remove();
|
|
488
|
-
placeholder?.remove();
|
|
489
525
|
// Log clone removal and element counts for debugging rapid toggle
|
|
490
526
|
pwLog('[presence] clone REMOVED from DOM', {
|
|
491
527
|
key,
|
|
@@ -514,6 +550,7 @@ export const createAnimatePresenceContext = (context) => {
|
|
|
514
550
|
inFlightExits: inFlightExits - 1,
|
|
515
551
|
clonesInDOM: document.querySelectorAll('[data-clone="true"]').length
|
|
516
552
|
});
|
|
553
|
+
placeholder?.remove();
|
|
517
554
|
finishExit();
|
|
518
555
|
});
|
|
519
556
|
});
|
|
@@ -78,9 +78,9 @@ export interface ProjectionNodeOptions {
|
|
|
78
78
|
/**
|
|
79
79
|
* Thunk returning the `layoutScroll` ancestor chain at measure
|
|
80
80
|
* time. Used as the second argument to `measureRect`, which
|
|
81
|
-
* shifts the returned rect by the sum of
|
|
82
|
-
* `scrollLeft`/`scrollTop` so FLIP deltas stay correct
|
|
83
|
-
* scrollable ancestors scroll between two measurements.
|
|
81
|
+
* shifts the returned rect by the viewport scroll plus the sum of
|
|
82
|
+
* ancestor `scrollLeft`/`scrollTop` so FLIP deltas stay correct
|
|
83
|
+
* when the page or scrollable ancestors scroll between two measurements.
|
|
84
84
|
*
|
|
85
85
|
* Returning `[]` (or omitting the option entirely) gives
|
|
86
86
|
* viewport-relative measurements — fine for the common case.
|
package/dist/utils/projection.js
CHANGED
|
@@ -236,7 +236,7 @@ export class ProjectionNode {
|
|
|
236
236
|
for (const { el, base } of restoreList)
|
|
237
237
|
el.style.transform = base;
|
|
238
238
|
// measureRect applies self's base transform + scroll-container offset.
|
|
239
|
-
const rect = measureRect(this.element, this.getScrollContainers?.() ?? [], this.resolveBaseTransform());
|
|
239
|
+
const rect = measureRect(this.element, this.getScrollContainers?.() ?? [], this.resolveBaseTransform(), true);
|
|
240
240
|
const box = rectToBox(rect);
|
|
241
241
|
this.latestLayout = box;
|
|
242
242
|
// Clone for the event: `box` aliases `this.latestLayout`, so a
|
package/dist/utils/svg.d.ts
CHANGED
|
@@ -69,8 +69,8 @@ export declare const isSVGElement: (element: Element) => element is SVGElement;
|
|
|
69
69
|
*
|
|
70
70
|
* Normalized behavior (React/Framer Motion parity):
|
|
71
71
|
* - Ensures `pathLength="1"` is set when any path prop is present
|
|
72
|
-
* - Maps `pathLength`/`pathSpacing` → `stroke-dasharray`
|
|
73
|
-
* - Maps `pathOffset` → `stroke-dashoffset`
|
|
72
|
+
* - Maps `pathLength`/`pathSpacing` → unitless `stroke-dasharray`
|
|
73
|
+
* - Maps `pathOffset` → unitless negative `stroke-dashoffset`
|
|
74
74
|
*
|
|
75
75
|
* @param {Element} element The element being animated (must be an SVG path).
|
|
76
76
|
* @param {Record<string, unknown>} keyframes The input keyframes possibly containing path props.
|
|
@@ -105,8 +105,8 @@ export declare const transformInitialSVGPathProperties: (element: Element, initi
|
|
|
105
105
|
*
|
|
106
106
|
* Behavior matches React/Framer Motion parity:
|
|
107
107
|
* - Always sets pathLength="1" whenever any of path props are present
|
|
108
|
-
* - stroke-dasharray =
|
|
109
|
-
* - stroke-dashoffset =
|
|
108
|
+
* - stroke-dasharray = pathLength + ' ' + (pathSpacing ?? 1)
|
|
109
|
+
* - stroke-dashoffset = -(pathOffset ?? 0)
|
|
110
110
|
*
|
|
111
111
|
* The returned object is suitable for direct DOM attribute assignment (dash-cased keys).
|
|
112
112
|
*
|
package/dist/utils/svg.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildSVGPath } from 'motion-dom';
|
|
1
2
|
/**
|
|
2
3
|
* SVG-specific properties that need special handling during animation.
|
|
3
4
|
* These properties are not standard CSS properties and need to be transformed.
|
|
@@ -145,8 +146,8 @@ export const isSVGElement = (element) => {
|
|
|
145
146
|
*
|
|
146
147
|
* Normalized behavior (React/Framer Motion parity):
|
|
147
148
|
* - Ensures `pathLength="1"` is set when any path prop is present
|
|
148
|
-
* - Maps `pathLength`/`pathSpacing` → `stroke-dasharray`
|
|
149
|
-
* - Maps `pathOffset` → `stroke-dashoffset`
|
|
149
|
+
* - Maps `pathLength`/`pathSpacing` → unitless `stroke-dasharray`
|
|
150
|
+
* - Maps `pathOffset` → unitless negative `stroke-dashoffset`
|
|
150
151
|
*
|
|
151
152
|
* @param {Element} element The element being animated (must be an SVG path).
|
|
152
153
|
* @param {Record<string, unknown>} keyframes The input keyframes possibly containing path props.
|
|
@@ -189,46 +190,65 @@ export const transformSVGPathProperties = (element, keyframes) => {
|
|
|
189
190
|
const n = toNum(v);
|
|
190
191
|
return n ?? v;
|
|
191
192
|
})();
|
|
192
|
-
const
|
|
193
|
-
const buildDashArray = (len, spa) => `${
|
|
194
|
-
// stroke-dasharray from pathLength/pathSpacing with default spacing = 1
|
|
193
|
+
const toUnitless = (v) => (typeof v === 'number' ? `${v}` : String(v));
|
|
194
|
+
const buildDashArray = (len, spa) => `${toUnitless(len)} ${toUnitless(spa)}`;
|
|
195
|
+
// stroke-dasharray from pathLength/pathSpacing with upstream's default spacing = 1
|
|
195
196
|
if (Array.isArray(length)) {
|
|
196
197
|
const lenArr = length;
|
|
197
198
|
const spaArr = Array.isArray(spacing)
|
|
198
199
|
? spacing
|
|
199
|
-
: lenArr.map((
|
|
200
|
+
: lenArr.map(() => (spacing !== undefined ? spacing : 1));
|
|
200
201
|
const dashArray = lenArr.map((lv, i) => buildDashArray(lv, spaArr[i]));
|
|
201
202
|
transformed.strokeDasharray = dashArray;
|
|
202
203
|
transformed['stroke-dasharray'] = dashArray;
|
|
203
204
|
}
|
|
204
205
|
else if (length !== undefined) {
|
|
205
|
-
const
|
|
206
|
-
const
|
|
207
|
-
const
|
|
208
|
-
|
|
206
|
+
const lenNum = toNum(length);
|
|
207
|
+
const spaNum = spacing === undefined ? undefined : toNum(spacing);
|
|
208
|
+
const offsetNum = offset === undefined ? undefined : toNum(offset);
|
|
209
|
+
let dashArray;
|
|
210
|
+
let dashOffset;
|
|
211
|
+
if (lenNum !== undefined &&
|
|
212
|
+
(spacing === undefined || spaNum !== undefined) &&
|
|
213
|
+
(offset === undefined || offsetNum !== undefined)) {
|
|
214
|
+
const attrs = {};
|
|
215
|
+
buildSVGPath(attrs, lenNum, spacing === undefined ? 1 : spaNum, offsetNum, true);
|
|
216
|
+
dashArray = String(attrs['stroke-dasharray']);
|
|
217
|
+
dashOffset = String(attrs['stroke-dashoffset']);
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
const spa = spacing !== undefined ? spacing : 1;
|
|
221
|
+
dashArray = buildDashArray(length, spa);
|
|
222
|
+
}
|
|
223
|
+
;
|
|
209
224
|
transformed.strokeDasharray = dashArray;
|
|
210
225
|
transformed['stroke-dasharray'] = dashArray;
|
|
226
|
+
if (dashOffset !== undefined) {
|
|
227
|
+
;
|
|
228
|
+
transformed.strokeDashoffset = dashOffset;
|
|
229
|
+
transformed['stroke-dashoffset'] = dashOffset;
|
|
230
|
+
}
|
|
211
231
|
}
|
|
212
232
|
// stroke-dashoffset from -pathOffset
|
|
213
233
|
if (Array.isArray(offset)) {
|
|
214
234
|
const offs = offset.map((ov) => {
|
|
215
235
|
const n = toNum(ov);
|
|
216
|
-
return n !== undefined ? `${-n}
|
|
236
|
+
return n !== undefined ? `${-n}` : String(ov);
|
|
217
237
|
});
|
|
218
238
|
transformed.strokeDashoffset = offs;
|
|
219
239
|
transformed['stroke-dashoffset'] = offs;
|
|
220
240
|
}
|
|
221
241
|
else if (offset !== undefined) {
|
|
222
242
|
const n = toNum(offset);
|
|
223
|
-
const off = n !== undefined ? `${-n}
|
|
243
|
+
const off = n !== undefined ? `${-n}` : String(offset);
|
|
224
244
|
transformed.strokeDashoffset = off;
|
|
225
245
|
transformed['stroke-dashoffset'] = off;
|
|
226
246
|
}
|
|
227
|
-
else {
|
|
247
|
+
else if (!('stroke-dashoffset' in transformed)) {
|
|
228
248
|
// default 0
|
|
229
249
|
;
|
|
230
|
-
transformed.strokeDashoffset = '
|
|
231
|
-
transformed['stroke-dashoffset'] = '
|
|
250
|
+
transformed.strokeDashoffset = '0';
|
|
251
|
+
transformed['stroke-dashoffset'] = '0';
|
|
232
252
|
}
|
|
233
253
|
delete transformed.pathLength;
|
|
234
254
|
delete transformed.pathSpacing;
|
|
@@ -273,8 +293,8 @@ export const transformInitialSVGPathProperties = (element, initial) => {
|
|
|
273
293
|
*
|
|
274
294
|
* Behavior matches React/Framer Motion parity:
|
|
275
295
|
* - Always sets pathLength="1" whenever any of path props are present
|
|
276
|
-
* - stroke-dasharray =
|
|
277
|
-
* - stroke-dashoffset =
|
|
296
|
+
* - stroke-dasharray = pathLength + ' ' + (pathSpacing ?? 1)
|
|
297
|
+
* - stroke-dashoffset = -(pathOffset ?? 0)
|
|
278
298
|
*
|
|
279
299
|
* The returned object is suitable for direct DOM attribute assignment (dash-cased keys).
|
|
280
300
|
*
|
|
@@ -287,19 +307,18 @@ export const computeNormalizedSVGInitialAttrs = (initial) => {
|
|
|
287
307
|
const hasAny = 'pathLength' in initial || 'pathSpacing' in initial || 'pathOffset' in initial;
|
|
288
308
|
if (!hasAny)
|
|
289
309
|
return null;
|
|
290
|
-
const
|
|
291
|
-
const
|
|
310
|
+
const toUnitless = (v) => (typeof v === 'number' ? `${v}` : String(v));
|
|
311
|
+
const negate = (v) => {
|
|
292
312
|
if (typeof v === 'number')
|
|
293
|
-
return `${-v}
|
|
313
|
+
return `${-v}`;
|
|
294
314
|
const s = String(v);
|
|
295
|
-
return s.startsWith('-') ? s : /^[\d.]+(px)?$/i.test(s) ? `-${s}` : s;
|
|
315
|
+
return s.startsWith('-') ? s : /^[\d.]+(px)?$/i.test(s) ? `-${s.replace(/px$/i, '')}` : s;
|
|
296
316
|
};
|
|
297
317
|
const len = initial.pathLength ?? 0;
|
|
298
|
-
const spa = initial.pathSpacing ??
|
|
299
|
-
(typeof len === 'number' ? 1 - len : 1);
|
|
318
|
+
const spa = initial.pathSpacing ?? 1;
|
|
300
319
|
const off = initial.pathOffset ?? 0;
|
|
301
|
-
const dashArray = `${
|
|
302
|
-
const dashOffset =
|
|
320
|
+
const dashArray = `${toUnitless(len)} ${toUnitless(spa)}`;
|
|
321
|
+
const dashOffset = negate(off);
|
|
303
322
|
// logging removed
|
|
304
323
|
return {
|
|
305
324
|
pathLength: '1',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@humanspeak/svelte-motion",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
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",
|