@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.
@@ -0,0 +1,155 @@
1
+ import { type IProjectionNode, type ResolvedValues, type Transition, type VisualElement } from 'motion-dom';
2
+ type ProjectionVisualElement = VisualElement & {
3
+ latestValues: ResolvedValues;
4
+ projection?: IProjectionNode<HTMLElement>;
5
+ };
6
+ type LayoutOption = boolean | string | undefined;
7
+ export interface MotionDomProjectionOptions {
8
+ /** Parent adapter used to connect this node into the upstream projection tree. */
9
+ parent?: MotionDomProjectionAdapter | null;
10
+ }
11
+ /**
12
+ * Latest layout-related motion props applied to an upstream projection node.
13
+ */
14
+ export interface MotionDomProjectionUpdateOptions {
15
+ /** Enables layout projection and selects the upstream animation type. */
16
+ layout?: LayoutOption;
17
+ /** Shared layout id used by upstream projection matching. */
18
+ layoutId?: string;
19
+ /** Tracks scroll on this element for descendant layout projection. */
20
+ layoutScroll?: boolean;
21
+ /** Transition passed to the upstream layout animation builder. */
22
+ transition?: Transition;
23
+ /** Inline style props passed through to the visual element. */
24
+ style?: unknown;
25
+ }
26
+ /**
27
+ * Svelte lifecycle adapter for motion-dom's upstream projection node system.
28
+ *
29
+ * The public Svelte API stays unchanged (`layout`, `layoutId`, `transition`).
30
+ * This adapter only translates those props into the same `HTMLProjectionNode`
31
+ * and `HTMLVisualElement` internals Framer Motion uses.
32
+ */
33
+ export declare class MotionDomProjectionAdapter {
34
+ private static adapters;
35
+ readonly visualElement: ProjectionVisualElement;
36
+ readonly projection: IProjectionNode<HTMLElement>;
37
+ private element;
38
+ private layout;
39
+ private layoutId;
40
+ private transition;
41
+ private lastLayout;
42
+ constructor(options?: MotionDomProjectionOptions);
43
+ /**
44
+ * Update projection options from current Svelte props.
45
+ *
46
+ * @param options Current layout-related motion props.
47
+ * @returns Nothing.
48
+ *
49
+ * @example
50
+ * ```ts
51
+ * adapter.updateOptions({ layout, layoutId, transition, style })
52
+ * ```
53
+ */
54
+ updateOptions(options: MotionDomProjectionUpdateOptions): void;
55
+ /**
56
+ * Mount the upstream projection node to an element and seed its layout.
57
+ *
58
+ * @param element Element represented by the current motion component.
59
+ * @returns Nothing.
60
+ *
61
+ * @example
62
+ * ```ts
63
+ * adapter.mount(element)
64
+ * ```
65
+ */
66
+ mount(element: HTMLElement): void;
67
+ /**
68
+ * Unmount the upstream projection node and clear its visual-element store.
69
+ *
70
+ * @returns Nothing.
71
+ *
72
+ * @example
73
+ * ```ts
74
+ * adapter.unmount()
75
+ * ```
76
+ */
77
+ unmount(): void;
78
+ /**
79
+ * Capture the upstream "before" snapshot.
80
+ *
81
+ * @returns Nothing.
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * adapter.willUpdate()
86
+ * ```
87
+ */
88
+ willUpdate(): void;
89
+ /**
90
+ * Commit an upstream layout update after Svelte has patched the DOM.
91
+ *
92
+ * @returns Nothing.
93
+ *
94
+ * @example
95
+ * ```ts
96
+ * adapter.didUpdate()
97
+ * ```
98
+ */
99
+ didUpdate(): void;
100
+ /**
101
+ * Seed the current layout without animating.
102
+ *
103
+ * @returns Nothing.
104
+ *
105
+ * @example
106
+ * ```ts
107
+ * adapter.seedLayout()
108
+ * ```
109
+ */
110
+ seedLayout(): void;
111
+ /**
112
+ * Animate from the last cached layout to the current observed layout.
113
+ *
114
+ * This covers layout changes discovered after the mutation by observers.
115
+ * Svelte runes mode doesn't expose the same component pre/post-update
116
+ * hooks Framer Motion uses in React, so this adapter reuses upstream
117
+ * projection while the Svelte component controls the snapshot timing.
118
+ *
119
+ * @returns Nothing.
120
+ *
121
+ * @example
122
+ * ```ts
123
+ * adapter.commitObservedLayoutChange()
124
+ * ```
125
+ */
126
+ commitObservedLayoutChange(): void;
127
+ /**
128
+ * Finish any active upstream layout animation in this subtree.
129
+ *
130
+ * @returns Nothing.
131
+ *
132
+ * @example
133
+ * ```ts
134
+ * adapter.finishAnimation()
135
+ * ```
136
+ */
137
+ finishAnimation(): void;
138
+ /**
139
+ * Check whether this projection subtree has an active layout animation.
140
+ *
141
+ * @returns `true` when this projection subtree is currently animating.
142
+ *
143
+ * @example
144
+ * ```ts
145
+ * if (adapter.isAnimating()) adapter.finishAnimation()
146
+ * ```
147
+ */
148
+ isAnimating(): boolean;
149
+ private seedCachedSnapshotsForSubtree;
150
+ private finishAnimationForSubtree;
151
+ private isAnimatingSubtree;
152
+ private updatePathScroll;
153
+ private refreshCachedLayout;
154
+ }
155
+ export {};
@@ -0,0 +1,279 @@
1
+ import { HTMLProjectionNode, HTMLVisualElement, copyBoxInto, createBox, visualElementStore } from 'motion-dom';
2
+ const createVisualState = () => ({
3
+ latestValues: {},
4
+ renderState: {
5
+ transform: {},
6
+ transformOrigin: {},
7
+ style: {},
8
+ vars: {}
9
+ }
10
+ });
11
+ const cloneMeasurements = (measurements) => {
12
+ if (!measurements)
13
+ return undefined;
14
+ const measuredBox = createBox();
15
+ const layoutBox = createBox();
16
+ copyBoxInto(measuredBox, measurements.measuredBox);
17
+ copyBoxInto(layoutBox, measurements.layoutBox);
18
+ return {
19
+ animationId: measurements.animationId,
20
+ measuredBox,
21
+ layoutBox,
22
+ latestValues: { ...measurements.latestValues },
23
+ source: measurements.source
24
+ };
25
+ };
26
+ const animationTypes = new Set([
27
+ 'position',
28
+ 'x',
29
+ 'y',
30
+ 'size',
31
+ 'both',
32
+ 'preserve-aspect'
33
+ ]);
34
+ const animationTypeForLayout = (layout) => typeof layout === 'string' && animationTypes.has(layout)
35
+ ? layout
36
+ : 'both';
37
+ /**
38
+ * Svelte lifecycle adapter for motion-dom's upstream projection node system.
39
+ *
40
+ * The public Svelte API stays unchanged (`layout`, `layoutId`, `transition`).
41
+ * This adapter only translates those props into the same `HTMLProjectionNode`
42
+ * and `HTMLVisualElement` internals Framer Motion uses.
43
+ */
44
+ export class MotionDomProjectionAdapter {
45
+ static adapters = new WeakMap();
46
+ visualElement;
47
+ projection;
48
+ element = null;
49
+ layout;
50
+ layoutId;
51
+ transition;
52
+ lastLayout;
53
+ constructor(options = {}) {
54
+ const parent = options.parent ?? null;
55
+ this.visualElement = new HTMLVisualElement({
56
+ parent: parent?.visualElement,
57
+ props: {},
58
+ presenceContext: null,
59
+ visualState: createVisualState()
60
+ }, { allowProjection: true });
61
+ this.projection = new HTMLProjectionNode(this.visualElement.latestValues, parent?.projection);
62
+ this.visualElement.projection = this.projection;
63
+ MotionDomProjectionAdapter.adapters.set(this.projection, this);
64
+ }
65
+ /**
66
+ * Update projection options from current Svelte props.
67
+ *
68
+ * @param options Current layout-related motion props.
69
+ * @returns Nothing.
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * adapter.updateOptions({ layout, layoutId, transition, style })
74
+ * ```
75
+ */
76
+ updateOptions(options) {
77
+ this.layout = options.layout;
78
+ this.layoutId = options.layoutId;
79
+ this.transition = options.transition;
80
+ this.visualElement.update({
81
+ transition: options.transition,
82
+ style: options.style
83
+ }, null);
84
+ this.projection.setOptions({
85
+ layout: options.layout,
86
+ layoutId: options.layoutId,
87
+ layoutScroll: options.layoutScroll,
88
+ animationType: animationTypeForLayout(options.layout),
89
+ transition: options.transition,
90
+ visualElement: this.visualElement
91
+ });
92
+ }
93
+ /**
94
+ * Mount the upstream projection node to an element and seed its layout.
95
+ *
96
+ * @param element Element represented by the current motion component.
97
+ * @returns Nothing.
98
+ *
99
+ * @example
100
+ * ```ts
101
+ * adapter.mount(element)
102
+ * ```
103
+ */
104
+ mount(element) {
105
+ if (this.element === element)
106
+ return;
107
+ if (this.element)
108
+ this.unmount();
109
+ this.element = element;
110
+ MotionDomProjectionAdapter.adapters.set(this.projection, this);
111
+ this.visualElement.mount(element);
112
+ this.seedLayout();
113
+ }
114
+ /**
115
+ * Unmount the upstream projection node and clear its visual-element store.
116
+ *
117
+ * @returns Nothing.
118
+ *
119
+ * @example
120
+ * ```ts
121
+ * adapter.unmount()
122
+ * ```
123
+ */
124
+ unmount() {
125
+ if (!this.element)
126
+ return;
127
+ const element = this.element;
128
+ this.projection.scheduleCheckAfterUnmount();
129
+ this.visualElement.unmount();
130
+ visualElementStore.delete(element);
131
+ MotionDomProjectionAdapter.adapters.delete(this.projection);
132
+ this.element = null;
133
+ this.lastLayout = undefined;
134
+ }
135
+ /**
136
+ * Capture the upstream "before" snapshot.
137
+ *
138
+ * @returns Nothing.
139
+ *
140
+ * @example
141
+ * ```ts
142
+ * adapter.willUpdate()
143
+ * ```
144
+ */
145
+ willUpdate() {
146
+ if (!this.element || !this.layout)
147
+ return;
148
+ this.projection.willUpdate();
149
+ }
150
+ /**
151
+ * Commit an upstream layout update after Svelte has patched the DOM.
152
+ *
153
+ * @returns Nothing.
154
+ *
155
+ * @example
156
+ * ```ts
157
+ * adapter.didUpdate()
158
+ * ```
159
+ */
160
+ didUpdate() {
161
+ if (!this.element || !this.layout)
162
+ return;
163
+ this.projection.root?.didUpdate();
164
+ this.refreshCachedLayout();
165
+ }
166
+ /**
167
+ * Seed the current layout without animating.
168
+ *
169
+ * @returns Nothing.
170
+ *
171
+ * @example
172
+ * ```ts
173
+ * adapter.seedLayout()
174
+ * ```
175
+ */
176
+ seedLayout() {
177
+ if (!this.element)
178
+ return;
179
+ this.updatePathScroll();
180
+ this.projection.isLayoutDirty = true;
181
+ this.projection.updateLayout();
182
+ this.lastLayout = cloneMeasurements(this.projection.layout);
183
+ }
184
+ /**
185
+ * Animate from the last cached layout to the current observed layout.
186
+ *
187
+ * This covers layout changes discovered after the mutation by observers.
188
+ * Svelte runes mode doesn't expose the same component pre/post-update
189
+ * hooks Framer Motion uses in React, so this adapter reuses upstream
190
+ * projection while the Svelte component controls the snapshot timing.
191
+ *
192
+ * @returns Nothing.
193
+ *
194
+ * @example
195
+ * ```ts
196
+ * adapter.commitObservedLayoutChange()
197
+ * ```
198
+ */
199
+ commitObservedLayoutChange() {
200
+ if (!this.element || !this.layout)
201
+ return;
202
+ if (!this.lastLayout) {
203
+ this.seedLayout();
204
+ return;
205
+ }
206
+ this.projection.root?.startUpdate();
207
+ this.seedCachedSnapshotsForSubtree(this.projection);
208
+ this.projection.root?.didUpdate();
209
+ this.refreshCachedLayout();
210
+ }
211
+ /**
212
+ * Finish any active upstream layout animation in this subtree.
213
+ *
214
+ * @returns Nothing.
215
+ *
216
+ * @example
217
+ * ```ts
218
+ * adapter.finishAnimation()
219
+ * ```
220
+ */
221
+ finishAnimation() {
222
+ if (!this.element || !this.layout)
223
+ return;
224
+ this.finishAnimationForSubtree(this.projection);
225
+ this.seedLayout();
226
+ }
227
+ /**
228
+ * Check whether this projection subtree has an active layout animation.
229
+ *
230
+ * @returns `true` when this projection subtree is currently animating.
231
+ *
232
+ * @example
233
+ * ```ts
234
+ * if (adapter.isAnimating()) adapter.finishAnimation()
235
+ * ```
236
+ */
237
+ isAnimating() {
238
+ return this.isAnimatingSubtree(this.projection);
239
+ }
240
+ seedCachedSnapshotsForSubtree(projection) {
241
+ const adapter = MotionDomProjectionAdapter.adapters.get(projection);
242
+ const snapshot = cloneMeasurements(adapter?.lastLayout);
243
+ if (snapshot && projection.options.layout) {
244
+ projection.snapshot = snapshot;
245
+ projection.isLayoutDirty = true;
246
+ }
247
+ for (const child of projection.children) {
248
+ this.seedCachedSnapshotsForSubtree(child);
249
+ }
250
+ }
251
+ finishAnimationForSubtree(projection) {
252
+ projection.finishAnimation();
253
+ projection.targetDelta = projection.relativeTarget = projection.target = undefined;
254
+ projection.isProjectionDirty = true;
255
+ projection.scheduleRender();
256
+ for (const child of projection.children) {
257
+ this.finishAnimationForSubtree(child);
258
+ }
259
+ }
260
+ isAnimatingSubtree(projection) {
261
+ if (projection.currentAnimation)
262
+ return true;
263
+ for (const child of projection.children) {
264
+ if (this.isAnimatingSubtree(child))
265
+ return true;
266
+ }
267
+ return false;
268
+ }
269
+ updatePathScroll() {
270
+ for (const node of this.projection.path) {
271
+ node.updateScroll();
272
+ }
273
+ }
274
+ refreshCachedLayout() {
275
+ requestAnimationFrame(() => {
276
+ this.lastLayout = cloneMeasurements(this.projection.layout);
277
+ });
278
+ }
279
+ }
@@ -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 };