@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/index.d.ts CHANGED
@@ -1,7 +1,13 @@
1
1
  import AnimatePresence from './components/AnimatePresence.svelte';
2
2
  import LayoutGroup from './components/LayoutGroup.svelte';
3
+ import LazyMotion from './components/LazyMotion.svelte';
3
4
  import MotionConfig from './components/MotionConfig.svelte';
4
5
  import PresenceChild from './components/PresenceChild.svelte';
6
+ export type { FeatureBundle, LazyFeatureBundle } from './features';
7
+ export { domAnimation } from './features/domAnimation';
8
+ export { domMax } from './features/domMax';
9
+ export { domMin } from './features/domMin';
10
+ export { m } from './m';
5
11
  export { motion } from './motion';
6
12
  export { animate, delay, hover, inView, press, resize, scroll, stagger, transform } from 'motion';
7
13
  export { anticipate, backIn, backInOut, backOut, circIn, circInOut, circOut, cubicBezier, easeIn, easeInOut, easeOut } from 'motion';
@@ -23,6 +29,7 @@ export type { MotionTemplateInput } from './utils/motionTemplate.svelte';
23
29
  export { useMotionValue } from './utils/motionValue.svelte';
24
30
  export type { MotionValue, RawMotionValue } from './utils/motionValue.svelte';
25
31
  export { useMotionValueEvent } from './utils/motionValueEvent';
32
+ export { optimizedAppearDataAttribute, startOptimizedAppearAnimation } from './utils/optimizedAppear';
26
33
  export { useReducedMotion } from './utils/reducedMotion.svelte';
27
34
  export type { ReducedMotionState } from './utils/reducedMotion.svelte';
28
35
  export { useReducedMotionConfig } from './utils/reducedMotionConfig.svelte';
@@ -42,7 +49,7 @@ export { styleString } from './utils/styleObject.svelte';
42
49
  export { useTime } from './utils/time.svelte';
43
50
  export { useTransform } from './utils/transform.svelte';
44
51
  export type { MultiTransformer, SingleTransformer, TransformOptions, TransformOutputMap, TransformSource } from './utils/transform.svelte';
45
- export { AnimatePresence, LayoutGroup, MotionConfig, PresenceChild };
52
+ export { AnimatePresence, LayoutGroup, LazyMotion, MotionConfig, PresenceChild };
46
53
  export { default as MotionA } from './html/A.svelte';
47
54
  export { default as MotionAbbr } from './html/Abbr.svelte';
48
55
  export { default as MotionAddress } from './html/Address.svelte';
package/dist/index.js CHANGED
@@ -1,7 +1,12 @@
1
1
  import AnimatePresence from './components/AnimatePresence.svelte';
2
2
  import LayoutGroup from './components/LayoutGroup.svelte';
3
+ import LazyMotion from './components/LazyMotion.svelte';
3
4
  import MotionConfig from './components/MotionConfig.svelte';
4
5
  import PresenceChild from './components/PresenceChild.svelte';
6
+ export { domAnimation } from './features/domAnimation';
7
+ export { domMax } from './features/domMax';
8
+ export { domMin } from './features/domMin';
9
+ export { m } from './m';
5
10
  export { motion } from './motion';
6
11
  // Re-export core animation functions from motion
7
12
  export { animate, delay, hover, inView, press, resize, scroll, stagger, transform } from 'motion';
@@ -18,6 +23,7 @@ export { useInView } from './utils/inView.svelte';
18
23
  export { useMotionTemplate } from './utils/motionTemplate.svelte';
19
24
  export { useMotionValue } from './utils/motionValue.svelte';
20
25
  export { useMotionValueEvent } from './utils/motionValueEvent';
26
+ export { optimizedAppearDataAttribute, startOptimizedAppearAnimation } from './utils/optimizedAppear';
21
27
  export { useReducedMotion } from './utils/reducedMotion.svelte';
22
28
  export { useReducedMotionConfig } from './utils/reducedMotionConfig.svelte';
23
29
  export { useScroll } from './utils/scroll.svelte';
@@ -31,7 +37,7 @@ export { stringifyStyleObject } from './utils/styleObject';
31
37
  export { styleString } from './utils/styleObject.svelte';
32
38
  export { useTime } from './utils/time.svelte';
33
39
  export { useTransform } from './utils/transform.svelte';
34
- export { AnimatePresence, LayoutGroup, MotionConfig, PresenceChild };
40
+ export { AnimatePresence, LayoutGroup, LazyMotion, MotionConfig, PresenceChild };
35
41
  // Named component exports — tree-shakeable alternative to the `motion` object
36
42
  export { default as MotionA } from './html/A.svelte';
37
43
  export { default as MotionAbbr } from './html/Abbr.svelte';
package/dist/m.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ import type { MotionComponents } from './html/index';
2
+ /**
3
+ * Lazy motion component namespace used with `<LazyMotion>`.
4
+ *
5
+ * The namespace mirrors the default `motion` object API (`m.div`, `m.button`,
6
+ * `m.svg`, etc.) while reading feature availability from the nearest
7
+ * LazyMotion provider.
8
+ */
9
+ export declare const m: MotionComponents;
package/dist/m.js ADDED
@@ -0,0 +1,9 @@
1
+ import * as html from './html/index';
2
+ /**
3
+ * Lazy motion component namespace used with `<LazyMotion>`.
4
+ *
5
+ * The namespace mirrors the default `motion` object API (`m.div`, `m.button`,
6
+ * `m.svg`, etc.) while reading feature availability from the nearest
7
+ * LazyMotion provider.
8
+ */
9
+ export const m = Object.fromEntries(Object.entries(html).map(([key, component]) => [key.toLowerCase(), component]));
package/dist/types.d.ts CHANGED
@@ -431,8 +431,8 @@ export type MotionProps = {
431
431
  style?: string;
432
432
  /** CSS classes */
433
433
  class?: string;
434
- /** Enable FLIP layout animations; "position" limits to translation only */
435
- layout?: boolean | 'position';
434
+ /** Enable FLIP layout animations; string values select the upstream projection animation type. */
435
+ layout?: boolean | 'position' | 'size' | 'preserve-aspect';
436
436
  /**
437
437
  * Fires after each `layout`-driven change with the FLIP delta from
438
438
  * the element's internal projection node. Mirrors framer-motion's
@@ -6,11 +6,13 @@ import { type AnimationOptions } from 'motion';
6
6
  * immediately after reading the rect.
7
7
  *
8
8
  * When `scrollContainers` are provided, the returned rect is shifted by the
9
- * **sum** of each container's `scrollLeft` / `scrollTop`. FLIP deltas
10
- * computed from two such measures stay correct even when the user scrolls
11
- * any of the containers between measurements including a nested
12
- * `layoutScroll` inside another `layoutScroll`. Mirrors framer-motion's
13
- * `removeElementScroll`, which walks every ancestor in the path.
9
+ * **sum** of each container's `scrollLeft` / `scrollTop`. When
10
+ * `includeViewportScroll` is true, the viewport's `window.scrollX` /
11
+ * `window.scrollY` is included too. FLIP deltas computed from two such
12
+ * measures stay correct even when the user scrolls between measurements —
13
+ * including a nested `layoutScroll` inside another `layoutScroll`. Mirrors
14
+ * framer-motion's `removeElementScroll`, which walks every ancestor in the
15
+ * path, plus root scroll compensation from the projection tree.
14
16
  *
15
17
  * Pass an empty array (or omit) for viewport-relative behaviour.
16
18
  *
@@ -27,6 +29,7 @@ import { type AnimationOptions } from 'motion';
27
29
  * @param el Element to measure.
28
30
  * @param scrollContainers Optional ancestor chain with `layoutScroll` enabled.
29
31
  * @param baseTransform Transform string applied during measurement. Defaults to `'none'`.
32
+ * @param includeViewportScroll Whether to include `window.scrollX/Y` in the returned rect.
30
33
  * @returns DOMRect snapshot of the element.
31
34
  *
32
35
  * @example
@@ -41,7 +44,7 @@ import { type AnimationOptions } from 'motion';
41
44
  * const rect = measureRect(node, [innerScroll, outerScroll])
42
45
  * ```
43
46
  */
44
- export declare const measureRect: (el: HTMLElement, scrollContainers?: HTMLElement[], baseTransform?: string) => DOMRect;
47
+ export declare const measureRect: (el: HTMLElement, scrollContainers?: HTMLElement[], baseTransform?: string, includeViewportScroll?: boolean) => DOMRect;
45
48
  /**
46
49
  * Minimal rectangle shape `computeFlipTransforms` reads. A `DOMRect`
47
50
  * satisfies it structurally, and so does a projection `Box` converted to
@@ -1,4 +1,102 @@
1
1
  import { animate } from 'motion';
2
+ const layoutSizeAnimationAttribute = 'data-layout-size-animation';
3
+ const px = (value) => `${Math.max(0, value)}px`;
4
+ const isViewportOffscreen = (el) => {
5
+ if (typeof window === 'undefined')
6
+ return false;
7
+ const rect = el.getBoundingClientRect();
8
+ return (rect.bottom <= 0 ||
9
+ rect.right <= 0 ||
10
+ rect.top >= window.innerHeight ||
11
+ rect.left >= window.innerWidth);
12
+ };
13
+ const runBoxSizeAnimation = (el, transforms, transition) => {
14
+ const { dx, dy, sx, sy } = transforms;
15
+ const originalWidth = el.style.width;
16
+ const originalHeight = el.style.height;
17
+ const originalTransform = el.style.transform;
18
+ const originalTransformOrigin = el.style.transformOrigin;
19
+ const nextRect = el.getBoundingClientRect();
20
+ const prevWidth = nextRect.width * sx;
21
+ const prevHeight = nextRect.height * sy;
22
+ el.setAttribute(layoutSizeAnimationAttribute, 'true');
23
+ for (const child of el.querySelectorAll('[data-svelte-motion-layout]')) {
24
+ child.style.transform = '';
25
+ child.style.transformOrigin = '';
26
+ if (child.style.willChange === 'transform')
27
+ child.style.willChange = '';
28
+ }
29
+ el.style.width = px(prevWidth);
30
+ el.style.height = px(prevHeight);
31
+ const sizedRect = el.getBoundingClientRect();
32
+ const residualDx = nextRect.left + dx - sizedRect.left;
33
+ const residualDy = nextRect.top + dy - sizedRect.top;
34
+ const shouldTranslate = Math.abs(residualDx) > 0.5 || Math.abs(residualDy) > 0.5;
35
+ const keyframes = {
36
+ width: [px(prevWidth), px(nextRect.width)],
37
+ height: [px(prevHeight), px(nextRect.height)]
38
+ };
39
+ if (shouldTranslate) {
40
+ keyframes.x = [residualDx, 0];
41
+ keyframes.y = [residualDy, 0];
42
+ el.style.transformOrigin = '0 0';
43
+ el.style.transform = `translate(${residualDx}px, ${residualDy}px)`;
44
+ }
45
+ const animation = animate(el, keyframes, transition);
46
+ let removeScrollListener;
47
+ let offscreenRaf = null;
48
+ let cleanupRan = false;
49
+ const cleanup = () => {
50
+ if (cleanupRan)
51
+ return;
52
+ cleanupRan = true;
53
+ removeScrollListener?.();
54
+ if (offscreenRaf !== null &&
55
+ typeof window !== 'undefined' &&
56
+ typeof window.cancelAnimationFrame === 'function') {
57
+ window.cancelAnimationFrame(offscreenRaf);
58
+ offscreenRaf = null;
59
+ }
60
+ el.style.width = originalWidth;
61
+ el.style.height = originalHeight;
62
+ el.style.transformOrigin = originalTransformOrigin;
63
+ el.style.transform = originalTransform;
64
+ el.removeAttribute(layoutSizeAnimationAttribute);
65
+ };
66
+ if (typeof window !== 'undefined') {
67
+ const completeIfOffscreen = () => {
68
+ if (cleanupRan)
69
+ return;
70
+ if (isViewportOffscreen(el)) {
71
+ animation.complete();
72
+ cleanup();
73
+ }
74
+ };
75
+ const scheduleCompleteIfOffscreen = () => {
76
+ if (typeof window.requestAnimationFrame !== 'function') {
77
+ completeIfOffscreen();
78
+ return;
79
+ }
80
+ if (offscreenRaf !== null)
81
+ return;
82
+ offscreenRaf = window.requestAnimationFrame(() => {
83
+ offscreenRaf = null;
84
+ completeIfOffscreen();
85
+ });
86
+ };
87
+ const handleScroll = () => {
88
+ completeIfOffscreen();
89
+ scheduleCompleteIfOffscreen();
90
+ };
91
+ window.addEventListener('scroll', handleScroll, { passive: true });
92
+ removeScrollListener = () => {
93
+ window.removeEventListener('scroll', handleScroll);
94
+ };
95
+ completeIfOffscreen();
96
+ scheduleCompleteIfOffscreen();
97
+ }
98
+ animation.finished?.finally(cleanup);
99
+ };
2
100
  /**
3
101
  * Measure an element's bounding client rect without current transform.
4
102
  *
@@ -6,11 +104,13 @@ import { animate } from 'motion';
6
104
  * immediately after reading the rect.
7
105
  *
8
106
  * When `scrollContainers` are provided, the returned rect is shifted by the
9
- * **sum** of each container's `scrollLeft` / `scrollTop`. FLIP deltas
10
- * computed from two such measures stay correct even when the user scrolls
11
- * any of the containers between measurements including a nested
12
- * `layoutScroll` inside another `layoutScroll`. Mirrors framer-motion's
13
- * `removeElementScroll`, which walks every ancestor in the path.
107
+ * **sum** of each container's `scrollLeft` / `scrollTop`. When
108
+ * `includeViewportScroll` is true, the viewport's `window.scrollX` /
109
+ * `window.scrollY` is included too. FLIP deltas computed from two such
110
+ * measures stay correct even when the user scrolls between measurements —
111
+ * including a nested `layoutScroll` inside another `layoutScroll`. Mirrors
112
+ * framer-motion's `removeElementScroll`, which walks every ancestor in the
113
+ * path, plus root scroll compensation from the projection tree.
14
114
  *
15
115
  * Pass an empty array (or omit) for viewport-relative behaviour.
16
116
  *
@@ -27,6 +127,7 @@ import { animate } from 'motion';
27
127
  * @param el Element to measure.
28
128
  * @param scrollContainers Optional ancestor chain with `layoutScroll` enabled.
29
129
  * @param baseTransform Transform string applied during measurement. Defaults to `'none'`.
130
+ * @param includeViewportScroll Whether to include `window.scrollX/Y` in the returned rect.
30
131
  * @returns DOMRect snapshot of the element.
31
132
  *
32
133
  * @example
@@ -41,19 +142,22 @@ import { animate } from 'motion';
41
142
  * const rect = measureRect(node, [innerScroll, outerScroll])
42
143
  * ```
43
144
  */
44
- export const measureRect = (el, scrollContainers, baseTransform = 'none') => {
145
+ export const measureRect = (el, scrollContainers, baseTransform = 'none', includeViewportScroll = false) => {
45
146
  const prev = el.style.transform;
46
147
  try {
47
148
  el.style.transform = baseTransform;
48
149
  const rect = el.getBoundingClientRect();
49
- if (!scrollContainers || scrollContainers.length === 0)
50
- return rect;
150
+ let offsetLeft = includeViewportScroll && typeof window !== 'undefined' ? window.scrollX : 0;
151
+ let offsetTop = includeViewportScroll && typeof window !== 'undefined' ? window.scrollY : 0;
152
+ if (!scrollContainers || scrollContainers.length === 0) {
153
+ if (offsetLeft === 0 && offsetTop === 0)
154
+ return rect;
155
+ return new DOMRect(rect.left + offsetLeft, rect.top + offsetTop, rect.width, rect.height);
156
+ }
51
157
  // Re-express the rect in the *combined* scroll-container coordinate
52
158
  // space so a subsequent scroll on any of them doesn't show up as
53
159
  // movement. DOMRect's left/top are read-only, so allocate a fresh
54
160
  // one with the summed offsets applied.
55
- let offsetLeft = 0;
56
- let offsetTop = 0;
57
161
  for (const container of scrollContainers) {
58
162
  offsetLeft += container.scrollLeft;
59
163
  offsetTop += container.scrollTop;
@@ -95,6 +199,13 @@ export const runFlipAnimation = (el, transforms, transition) => {
95
199
  const { dx, dy, sx, sy, shouldTranslate, shouldScale } = transforms;
96
200
  if (!(shouldTranslate || shouldScale))
97
201
  return;
202
+ const correctionTargets = shouldScale
203
+ ? Array.from(el.querySelectorAll('[data-svelte-motion-layout]'))
204
+ : [];
205
+ if (shouldScale && correctionTargets.length > 0) {
206
+ runBoxSizeAnimation(el, { dx, dy, sx, sy }, transition);
207
+ return;
208
+ }
98
209
  const keyframes = {};
99
210
  if (shouldTranslate) {
100
211
  keyframes.x = [dx, 0];
@@ -140,6 +251,13 @@ export const observeLayoutChanges = (el, onChange) => {
140
251
  let pendingRaf = null;
141
252
  let releaseTimeout = null;
142
253
  const schedule = () => {
254
+ if (el.closest(`[${layoutSizeAnimationAttribute}]`)) {
255
+ el.style.transform = '';
256
+ el.style.transformOrigin = '';
257
+ if (el.style.willChange === 'transform')
258
+ el.style.willChange = '';
259
+ return;
260
+ }
143
261
  if (pendingRaf !== null || releaseTimeout !== null)
144
262
  return;
145
263
  // Leading-edge: call immediately, then throttle further calls until next frame (or 50ms)
@@ -157,14 +275,23 @@ export const observeLayoutChanges = (el, onChange) => {
157
275
  };
158
276
  const ro = new ResizeObserver(() => schedule());
159
277
  ro.observe(el);
160
- const mo = new MutationObserver(() => schedule());
161
- mo.observe(el, { attributes: true, attributeFilter: ['class', 'style'] });
278
+ const attributeObserver = new MutationObserver(() => schedule());
279
+ attributeObserver.observe(el, {
280
+ attributes: true,
281
+ attributeFilter: ['class', 'style', 'data-presence-layout-hold']
282
+ });
283
+ const childListObserver = new MutationObserver(() => schedule());
284
+ childListObserver.observe(el, {
285
+ childList: true,
286
+ subtree: true
287
+ });
162
288
  if (el.parentElement) {
163
- mo.observe(el.parentElement, { childList: true, subtree: false, attributes: true });
289
+ childListObserver.observe(el.parentElement, { childList: true, subtree: false });
164
290
  }
165
291
  return () => {
166
292
  ro.disconnect();
167
- mo.disconnect();
293
+ attributeObserver.disconnect();
294
+ childListObserver.disconnect();
168
295
  if (pendingRaf !== null && typeof cancelAnimationFrame === 'function') {
169
296
  cancelAnimationFrame(pendingRaf);
170
297
  pendingRaf = null;
@@ -0,0 +1,126 @@
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
+ /** Transition passed to the upstream layout animation builder. */
20
+ transition?: Transition;
21
+ /** Inline style props passed through to the visual element. */
22
+ style?: unknown;
23
+ }
24
+ /**
25
+ * Svelte lifecycle adapter for motion-dom's upstream projection node system.
26
+ *
27
+ * The public Svelte API stays unchanged (`layout`, `layoutId`, `transition`).
28
+ * This adapter only translates those props into the same `HTMLProjectionNode`
29
+ * and `HTMLVisualElement` internals Framer Motion uses.
30
+ */
31
+ export declare class MotionDomProjectionAdapter {
32
+ readonly visualElement: ProjectionVisualElement;
33
+ readonly projection: IProjectionNode<HTMLElement>;
34
+ private element;
35
+ private layout;
36
+ private layoutId;
37
+ private transition;
38
+ private lastLayout;
39
+ constructor(options?: MotionDomProjectionOptions);
40
+ /**
41
+ * Update projection options from current Svelte props.
42
+ *
43
+ * @param options Current layout-related motion props.
44
+ * @returns Nothing.
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * adapter.updateOptions({ layout, layoutId, transition, style })
49
+ * ```
50
+ */
51
+ updateOptions(options: MotionDomProjectionUpdateOptions): void;
52
+ /**
53
+ * Mount the upstream projection node to an element and seed its layout.
54
+ *
55
+ * @param element Element represented by the current motion component.
56
+ * @returns Nothing.
57
+ *
58
+ * @example
59
+ * ```ts
60
+ * adapter.mount(element)
61
+ * ```
62
+ */
63
+ mount(element: HTMLElement): void;
64
+ /**
65
+ * Unmount the upstream projection node and clear its visual-element store.
66
+ *
67
+ * @returns Nothing.
68
+ *
69
+ * @example
70
+ * ```ts
71
+ * adapter.unmount()
72
+ * ```
73
+ */
74
+ unmount(): void;
75
+ /**
76
+ * Capture the upstream "before" snapshot.
77
+ *
78
+ * @returns Nothing.
79
+ *
80
+ * @example
81
+ * ```ts
82
+ * adapter.willUpdate()
83
+ * ```
84
+ */
85
+ willUpdate(): void;
86
+ /**
87
+ * Commit an upstream layout update after Svelte has patched the DOM.
88
+ *
89
+ * @returns Nothing.
90
+ *
91
+ * @example
92
+ * ```ts
93
+ * adapter.didUpdate()
94
+ * ```
95
+ */
96
+ didUpdate(): void;
97
+ /**
98
+ * Seed the current layout without animating.
99
+ *
100
+ * @returns Nothing.
101
+ *
102
+ * @example
103
+ * ```ts
104
+ * adapter.seedLayout()
105
+ * ```
106
+ */
107
+ seedLayout(): void;
108
+ /**
109
+ * Animate from the last cached layout to the current observed layout.
110
+ *
111
+ * This covers layout changes discovered after the mutation by observers.
112
+ * Svelte runes mode doesn't expose the same component pre/post-update
113
+ * hooks Framer Motion uses in React, so this adapter reuses upstream
114
+ * projection while the Svelte component controls the snapshot timing.
115
+ *
116
+ * @returns Nothing.
117
+ *
118
+ * @example
119
+ * ```ts
120
+ * adapter.commitObservedLayoutChange()
121
+ * ```
122
+ */
123
+ commitObservedLayoutChange(): void;
124
+ private refreshCachedLayout;
125
+ }
126
+ export {};
@@ -0,0 +1,212 @@
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
+ visualElement;
46
+ projection;
47
+ element = null;
48
+ layout;
49
+ layoutId;
50
+ transition;
51
+ lastLayout;
52
+ constructor(options = {}) {
53
+ const parent = options.parent ?? null;
54
+ this.visualElement = new HTMLVisualElement({
55
+ parent: parent?.visualElement,
56
+ props: {},
57
+ presenceContext: null,
58
+ visualState: createVisualState()
59
+ }, { allowProjection: true });
60
+ this.projection = new HTMLProjectionNode(this.visualElement.latestValues, parent?.projection);
61
+ this.visualElement.projection = this.projection;
62
+ }
63
+ /**
64
+ * Update projection options from current Svelte props.
65
+ *
66
+ * @param options Current layout-related motion props.
67
+ * @returns Nothing.
68
+ *
69
+ * @example
70
+ * ```ts
71
+ * adapter.updateOptions({ layout, layoutId, transition, style })
72
+ * ```
73
+ */
74
+ updateOptions(options) {
75
+ this.layout = options.layout;
76
+ this.layoutId = options.layoutId;
77
+ this.transition = options.transition;
78
+ this.visualElement.update({
79
+ transition: options.transition,
80
+ style: options.style
81
+ }, null);
82
+ this.projection.setOptions({
83
+ layout: options.layout,
84
+ layoutId: options.layoutId,
85
+ animationType: animationTypeForLayout(options.layout),
86
+ transition: options.transition,
87
+ visualElement: this.visualElement
88
+ });
89
+ }
90
+ /**
91
+ * Mount the upstream projection node to an element and seed its layout.
92
+ *
93
+ * @param element Element represented by the current motion component.
94
+ * @returns Nothing.
95
+ *
96
+ * @example
97
+ * ```ts
98
+ * adapter.mount(element)
99
+ * ```
100
+ */
101
+ mount(element) {
102
+ if (this.element === element)
103
+ return;
104
+ if (this.element)
105
+ this.unmount();
106
+ this.element = element;
107
+ this.visualElement.mount(element);
108
+ this.seedLayout();
109
+ }
110
+ /**
111
+ * Unmount the upstream projection node and clear its visual-element store.
112
+ *
113
+ * @returns Nothing.
114
+ *
115
+ * @example
116
+ * ```ts
117
+ * adapter.unmount()
118
+ * ```
119
+ */
120
+ unmount() {
121
+ if (!this.element)
122
+ return;
123
+ const element = this.element;
124
+ this.projection.scheduleCheckAfterUnmount();
125
+ this.visualElement.unmount();
126
+ visualElementStore.delete(element);
127
+ this.element = null;
128
+ this.lastLayout = undefined;
129
+ }
130
+ /**
131
+ * Capture the upstream "before" snapshot.
132
+ *
133
+ * @returns Nothing.
134
+ *
135
+ * @example
136
+ * ```ts
137
+ * adapter.willUpdate()
138
+ * ```
139
+ */
140
+ willUpdate() {
141
+ if (!this.element || !this.layoutId)
142
+ return;
143
+ this.projection.willUpdate();
144
+ }
145
+ /**
146
+ * Commit an upstream layout update after Svelte has patched the DOM.
147
+ *
148
+ * @returns Nothing.
149
+ *
150
+ * @example
151
+ * ```ts
152
+ * adapter.didUpdate()
153
+ * ```
154
+ */
155
+ didUpdate() {
156
+ if (!this.element || !this.layoutId)
157
+ return;
158
+ this.projection.root?.didUpdate();
159
+ this.refreshCachedLayout();
160
+ }
161
+ /**
162
+ * Seed the current layout without animating.
163
+ *
164
+ * @returns Nothing.
165
+ *
166
+ * @example
167
+ * ```ts
168
+ * adapter.seedLayout()
169
+ * ```
170
+ */
171
+ seedLayout() {
172
+ if (!this.element)
173
+ return;
174
+ this.projection.isLayoutDirty = true;
175
+ this.projection.updateLayout();
176
+ this.lastLayout = cloneMeasurements(this.projection.layout);
177
+ }
178
+ /**
179
+ * Animate from the last cached layout to the current observed layout.
180
+ *
181
+ * This covers layout changes discovered after the mutation by observers.
182
+ * Svelte runes mode doesn't expose the same component pre/post-update
183
+ * hooks Framer Motion uses in React, so this adapter reuses upstream
184
+ * projection while the Svelte component controls the snapshot timing.
185
+ *
186
+ * @returns Nothing.
187
+ *
188
+ * @example
189
+ * ```ts
190
+ * adapter.commitObservedLayoutChange()
191
+ * ```
192
+ */
193
+ commitObservedLayoutChange() {
194
+ if (!this.element || !this.layoutId)
195
+ return;
196
+ const snapshot = cloneMeasurements(this.lastLayout);
197
+ if (!snapshot) {
198
+ this.seedLayout();
199
+ return;
200
+ }
201
+ this.projection.root?.startUpdate();
202
+ this.projection.snapshot = snapshot;
203
+ this.projection.isLayoutDirty = true;
204
+ this.projection.root?.didUpdate();
205
+ this.refreshCachedLayout();
206
+ }
207
+ refreshCachedLayout() {
208
+ requestAnimationFrame(() => {
209
+ this.lastLayout = cloneMeasurements(this.projection.layout);
210
+ });
211
+ }
212
+ }