@humanspeak/svelte-motion 0.5.3 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/LazyMotion.svelte +70 -0
- package/dist/components/LazyMotion.svelte.d.ts +16 -0
- package/dist/components/lazyMotion.context.d.ts +25 -0
- package/dist/components/lazyMotion.context.js +19 -0
- package/dist/components/motionDomProjection.context.d.ts +13 -0
- package/dist/components/motionDomProjection.context.js +18 -0
- package/dist/features/domAnimation.d.ts +6 -0
- package/dist/features/domAnimation.js +8 -0
- package/dist/features/domMax.d.ts +5 -0
- package/dist/features/domMax.js +9 -0
- package/dist/features/domMin.d.ts +5 -0
- package/dist/features/domMin.js +6 -0
- package/dist/features/index.d.ts +39 -0
- package/dist/features/index.js +18 -0
- package/dist/html/_MotionContainer.svelte +602 -70
- package/dist/index.d.ts +8 -1
- package/dist/index.js +7 -1
- package/dist/m.d.ts +9 -0
- package/dist/m.js +9 -0
- package/dist/types.d.ts +2 -2
- package/dist/utils/layout.d.ts +9 -6
- package/dist/utils/layout.js +141 -14
- package/dist/utils/motionDomProjection.d.ts +126 -0
- package/dist/utils/motionDomProjection.js +212 -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,141 @@
|
|
|
1
|
+
import { type AnimationOptions } from 'motion';
|
|
2
|
+
import { optimizedAppearDataAttribute } from 'motion-dom';
|
|
3
|
+
type AppearValueName = 'opacity' | 'transform';
|
|
4
|
+
type OptimizedAppearEntry = {
|
|
5
|
+
name: AppearValueName;
|
|
6
|
+
keyframes: [string | number, string | number];
|
|
7
|
+
options: KeyframeAnimationOptions;
|
|
8
|
+
};
|
|
9
|
+
type AppearStoreEntry = {
|
|
10
|
+
animation: Animation;
|
|
11
|
+
startTime: number | null;
|
|
12
|
+
};
|
|
13
|
+
type SvelteMotionAppearStore = {
|
|
14
|
+
animations: Map<string, AppearStoreEntry>;
|
|
15
|
+
complete: Map<string, boolean>;
|
|
16
|
+
started: Array<{
|
|
17
|
+
id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
}>;
|
|
20
|
+
readyAnimation?: Animation;
|
|
21
|
+
startFrameTime?: number;
|
|
22
|
+
};
|
|
23
|
+
declare global {
|
|
24
|
+
interface Window {
|
|
25
|
+
__SvelteMotionAppear?: SvelteMotionAppearStore;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Build serialisable optimized-appear animation entries from an initial and
|
|
30
|
+
* animate pair.
|
|
31
|
+
*
|
|
32
|
+
* @param initial Initial keyframes reflected into SSR markup.
|
|
33
|
+
* @param animate Target keyframes for the enter animation.
|
|
34
|
+
* @param transition Motion transition options.
|
|
35
|
+
* @returns Appear entries for WAAPI-supported opacity and transform values.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts
|
|
39
|
+
* const entries = createOptimizedAppearData(
|
|
40
|
+
* { opacity: 0, scale: 0.8 },
|
|
41
|
+
* { opacity: 1, scale: 1 },
|
|
42
|
+
* { duration: 0.6, ease: [0.16, 1, 0.3, 1] }
|
|
43
|
+
* )
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export declare const createOptimizedAppearData: (initial: Record<string, unknown> | null | undefined, animate: Record<string, unknown> | null | undefined, transition?: AnimationOptions) => OptimizedAppearEntry[];
|
|
47
|
+
/**
|
|
48
|
+
* Create the inline SSR bootstrap that starts appear animations before Svelte
|
|
49
|
+
* hydrates the component tree.
|
|
50
|
+
*
|
|
51
|
+
* @param appearId Stable optimized-appear id attached to the motion element.
|
|
52
|
+
* @param entries WAAPI animation entries to start.
|
|
53
|
+
* @returns A script tag string, or an empty string when no entries exist.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts
|
|
57
|
+
* const script = createOptimizedAppearScript('appear-1', [
|
|
58
|
+
* { name: 'opacity', keyframes: [0, 1], options: { duration: 300, fill: 'both' } }
|
|
59
|
+
* ])
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export declare const createOptimizedAppearScript: (appearId: string | undefined, entries: OptimizedAppearEntry[]) => string;
|
|
63
|
+
/**
|
|
64
|
+
* Start an optimized appear animation imperatively.
|
|
65
|
+
*
|
|
66
|
+
* Mirrors Framer Motion's `startOptimizedAppearAnimation`: if Motion has
|
|
67
|
+
* already mounted, this intentionally does nothing.
|
|
68
|
+
*
|
|
69
|
+
* @param element Element carrying `data-framer-appear-id`.
|
|
70
|
+
* @param name CSS property to animate.
|
|
71
|
+
* @param keyframes WAAPI keyframes for the property.
|
|
72
|
+
* @param options Motion animation options.
|
|
73
|
+
* @param onReady Optional callback receiving the started animation.
|
|
74
|
+
* @returns Nothing.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```ts
|
|
78
|
+
* const element = document.querySelector('[data-framer-appear-id]')
|
|
79
|
+
* if (element instanceof HTMLElement) {
|
|
80
|
+
* startOptimizedAppearAnimation(element, 'opacity', [0, 1], { duration: 0.3 })
|
|
81
|
+
* }
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export declare const startOptimizedAppearAnimation: (element: HTMLElement, name: AppearValueName, keyframes: string[] | number[], options: AnimationOptions, onReady?: (animation: Animation) => void) => void;
|
|
85
|
+
/**
|
|
86
|
+
* Commit and cancel optimized appear animations for an element.
|
|
87
|
+
*
|
|
88
|
+
* @param elementId Optimized appear id.
|
|
89
|
+
* @returns `true` when at least one optimized animation was handed off.
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```ts
|
|
93
|
+
* const wasHandedOff = handoffOptimizedAppearAnimation('appear-1')
|
|
94
|
+
* if (wasHandedOff) {
|
|
95
|
+
* console.log('Animation handed off to runtime')
|
|
96
|
+
* }
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
export declare const handoffOptimizedAppearAnimation: (elementId: string | undefined) => boolean;
|
|
100
|
+
/**
|
|
101
|
+
* Let active optimized appear animations finish before handing their final
|
|
102
|
+
* styles back to Svelte Motion.
|
|
103
|
+
*
|
|
104
|
+
* @param elementId Optimized appear id.
|
|
105
|
+
* @returns Whether at least one optimized animation was adopted.
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```ts
|
|
109
|
+
* const wasAdopted = await finishOptimizedAppearAnimation('appear-1')
|
|
110
|
+
* if (wasAdopted) {
|
|
111
|
+
* console.log('Animation finished and adopted')
|
|
112
|
+
* }
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
export declare const finishOptimizedAppearAnimation: (elementId: string | undefined) => Promise<boolean>;
|
|
116
|
+
/**
|
|
117
|
+
* Check whether an optimized appear animation is active for an element.
|
|
118
|
+
*
|
|
119
|
+
* @param elementId Optimized appear id.
|
|
120
|
+
* @returns Whether any optimized appear animation is currently registered.
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* ```ts
|
|
124
|
+
* if (hasOptimizedAppearAnimation('appear-1')) {
|
|
125
|
+
* console.log('Animation is active')
|
|
126
|
+
* }
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
export declare const hasOptimizedAppearAnimation: (elementId: string | undefined) => boolean;
|
|
130
|
+
/**
|
|
131
|
+
* Mark Motion as mounted so late optimized-appear starters no-op.
|
|
132
|
+
*
|
|
133
|
+
* @returns Nothing.
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* ```ts
|
|
137
|
+
* markMotionMounted()
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
export declare const markMotionMounted: () => void;
|
|
141
|
+
export { optimizedAppearDataAttribute };
|
|
@@ -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
|
*
|