@humanspeak/svelte-motion 0.4.4 → 0.4.6
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/README.md +1 -2
- package/dist/components/LayoutGroup.svelte +68 -0
- package/dist/components/LayoutGroup.svelte.d.ts +9 -0
- package/dist/components/layoutGroup.context.d.ts +37 -0
- package/dist/components/layoutGroup.context.js +41 -0
- package/dist/components/layoutScroll.context.d.ts +36 -0
- package/dist/components/layoutScroll.context.js +32 -0
- package/dist/html/_MotionContainer.svelte +53 -10
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/types.d.ts +16 -0
- package/dist/utils/layout.d.ts +24 -2
- package/dist/utils/layout.js +38 -3
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ Goal: Framer Motion API parity for Svelte where common React examples can be tra
|
|
|
44
44
|
| Drag (`drag`, constraints, momentum, controls, callbacks) | Supported |
|
|
45
45
|
| `AnimatePresence` (`initial`, `mode`, `onExitComplete`) | Supported |
|
|
46
46
|
| Layout (`layout`, `layout="position"`) | Supported (single-element FLIP) |
|
|
47
|
-
| Shared layout (`layoutId`)
|
|
47
|
+
| Shared layout (`layoutId`, `LayoutGroup`, `layoutScroll`) | Supported |
|
|
48
48
|
| Pan gesture API (`whilePan`, `onPan*`) | Not yet supported |
|
|
49
49
|
| `MotionConfig` parity beyond `transition` | Partial |
|
|
50
50
|
| `reducedMotion`, `features`, `transformPagePoint` | Not yet supported |
|
|
@@ -280,7 +280,6 @@ Validated against current source and test suite (local run):
|
|
|
280
280
|
|
|
281
281
|
## Known gaps vs Framer Motion
|
|
282
282
|
|
|
283
|
-
- No shared layout API (`layoutId`, `LayoutGroup`).
|
|
284
283
|
- No pan gesture API (`whilePan`, `onPan*`).
|
|
285
284
|
- `whileInView` does not yet expose Framer-style viewport options.
|
|
286
285
|
- `MotionConfig` currently only provides `transition` defaults.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte'
|
|
3
|
+
import {
|
|
4
|
+
chainLayoutGroupId,
|
|
5
|
+
getLayoutGroupContext,
|
|
6
|
+
setLayoutGroupContext
|
|
7
|
+
} from './layoutGroup.context'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Scope `layoutId` shared-layout animations to a subtree.
|
|
11
|
+
*
|
|
12
|
+
* Wrap a region in `<LayoutGroup id="…">` so descendants' `layoutId`
|
|
13
|
+
* snapshots / consumes are prefixed with the group's id. Two groups
|
|
14
|
+
* containing the same `layoutId` values won't cross-animate — useful
|
|
15
|
+
* for repeated UI patterns (multiple tab indicators, kanban columns,
|
|
16
|
+
* sibling carousels) where each instance should animate independently.
|
|
17
|
+
*
|
|
18
|
+
* Mirrors framer-motion's `<LayoutGroup>` (`inherit` defaults to true
|
|
19
|
+
* — descendants chain onto the parent group's id, so nested groups
|
|
20
|
+
* yield `"parent-child"`).
|
|
21
|
+
*
|
|
22
|
+
* @prop id Stable identifier for this group's scope. When omitted,
|
|
23
|
+
* the LayoutGroup is a transparent grouping with no own id
|
|
24
|
+
* (still useful for `inherit={false}` to break out of an outer
|
|
25
|
+
* group's scope, e.g. an embedded widget).
|
|
26
|
+
* @prop inherit `true` (default) — chain onto the parent group's id.
|
|
27
|
+
* `'id'` — same as `true` in this implementation; accepted for
|
|
28
|
+
* drop-in compatibility with framer-motion examples. In
|
|
29
|
+
* framer-motion, `'id'` inherits the id but breaks the internal
|
|
30
|
+
* projection-tree group. We don't have a projection-tree group
|
|
31
|
+
* (our snapshot/consume registry doesn't need sibling
|
|
32
|
+
* coordination), so `'id'` and `true` behave identically.
|
|
33
|
+
* `false` — start a fresh scope, ignoring any outer LayoutGroup.
|
|
34
|
+
* @prop children Slot rendered inside the group context.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```svelte
|
|
38
|
+
* <LayoutGroup id="tabs-a">
|
|
39
|
+
* <Tabs />
|
|
40
|
+
* </LayoutGroup>
|
|
41
|
+
* <LayoutGroup id="tabs-b">
|
|
42
|
+
* <Tabs /> <!-- same layoutId values, independent animations -->
|
|
43
|
+
* </LayoutGroup>
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* @see https://motion.dev/docs/react-layout-animations#scoped-layout-animations
|
|
47
|
+
*/
|
|
48
|
+
const {
|
|
49
|
+
id,
|
|
50
|
+
inherit = true,
|
|
51
|
+
children
|
|
52
|
+
}: {
|
|
53
|
+
id?: string
|
|
54
|
+
inherit?: boolean | 'id'
|
|
55
|
+
children?: Snippet
|
|
56
|
+
} = $props()
|
|
57
|
+
|
|
58
|
+
// setContext is one-shot at component init, so reading `id` and `inherit`
|
|
59
|
+
// here captures their initial values intentionally — the scope id is
|
|
60
|
+
// fixed for this subtree's lifetime. The warning would only matter if we
|
|
61
|
+
// wanted descendants to react to prop changes, which we explicitly don't.
|
|
62
|
+
// svelte-ignore state_referenced_locally
|
|
63
|
+
const shouldInheritId = inherit === true || inherit === 'id'
|
|
64
|
+
const effectiveId = shouldInheritId ? chainLayoutGroupId(getLayoutGroupContext(), id) : id
|
|
65
|
+
setLayoutGroupContext(effectiveId)
|
|
66
|
+
</script>
|
|
67
|
+
|
|
68
|
+
{@render children?.()}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
id?: string;
|
|
4
|
+
inherit?: boolean | 'id';
|
|
5
|
+
children?: Snippet;
|
|
6
|
+
};
|
|
7
|
+
declare const LayoutGroup: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
8
|
+
type LayoutGroup = ReturnType<typeof LayoutGroup>;
|
|
9
|
+
export default LayoutGroup;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identifier for a `<LayoutGroup>` subtree. Descendants prefix their
|
|
3
|
+
* `layoutId` lookups with this so two `<LayoutGroup>`s containing the
|
|
4
|
+
* same `layoutId` values don't cross-animate.
|
|
5
|
+
*
|
|
6
|
+
* `undefined` means "no enclosing LayoutGroup" — the descendant uses
|
|
7
|
+
* `layoutId` verbatim against the global registry, preserving the
|
|
8
|
+
* existing un-grouped behaviour.
|
|
9
|
+
*/
|
|
10
|
+
export type LayoutGroupContext = string | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* Publish a LayoutGroup id for descendants. Called by `<LayoutGroup>`
|
|
13
|
+
* after computing its own (possibly inherited and chained) id.
|
|
14
|
+
*/
|
|
15
|
+
export declare const setLayoutGroupContext: (id: LayoutGroupContext) => void;
|
|
16
|
+
/**
|
|
17
|
+
* Read the nearest LayoutGroup id, or `undefined` if not inside one.
|
|
18
|
+
*
|
|
19
|
+
* `_MotionContainer.svelte` reads this to prefix `layoutId` when
|
|
20
|
+
* snapshotting and consuming against the registry, so shared-layout
|
|
21
|
+
* animations stay scoped to the surrounding group.
|
|
22
|
+
*/
|
|
23
|
+
export declare const getLayoutGroupContext: () => LayoutGroupContext;
|
|
24
|
+
/**
|
|
25
|
+
* Combine a parent group's id with a descendant LayoutGroup's own id
|
|
26
|
+
* to produce the effective scope id. Mirrors framer-motion's chaining
|
|
27
|
+
* (`"parent-id"` + `"-"` + `"own-id"`).
|
|
28
|
+
*
|
|
29
|
+
* Either side can be `undefined`; the result is the other one, or
|
|
30
|
+
* `undefined` if both are absent.
|
|
31
|
+
*/
|
|
32
|
+
export declare const chainLayoutGroupId: (parent: LayoutGroupContext, own: string | undefined) => LayoutGroupContext;
|
|
33
|
+
/**
|
|
34
|
+
* Apply a LayoutGroup scope to a raw `layoutId` for registry lookups.
|
|
35
|
+
* Returns the un-prefixed id when no group is in scope.
|
|
36
|
+
*/
|
|
37
|
+
export declare const scopeLayoutId: (groupId: LayoutGroupContext, layoutId: string) => string;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { getContext, setContext } from 'svelte';
|
|
2
|
+
const LAYOUT_GROUP_CONTEXT_KEY = Symbol('layout-group');
|
|
3
|
+
/**
|
|
4
|
+
* Publish a LayoutGroup id for descendants. Called by `<LayoutGroup>`
|
|
5
|
+
* after computing its own (possibly inherited and chained) id.
|
|
6
|
+
*/
|
|
7
|
+
export const setLayoutGroupContext = (id) => {
|
|
8
|
+
setContext(LAYOUT_GROUP_CONTEXT_KEY, id);
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Read the nearest LayoutGroup id, or `undefined` if not inside one.
|
|
12
|
+
*
|
|
13
|
+
* `_MotionContainer.svelte` reads this to prefix `layoutId` when
|
|
14
|
+
* snapshotting and consuming against the registry, so shared-layout
|
|
15
|
+
* animations stay scoped to the surrounding group.
|
|
16
|
+
*/
|
|
17
|
+
export const getLayoutGroupContext = () => {
|
|
18
|
+
return getContext(LAYOUT_GROUP_CONTEXT_KEY);
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Combine a parent group's id with a descendant LayoutGroup's own id
|
|
22
|
+
* to produce the effective scope id. Mirrors framer-motion's chaining
|
|
23
|
+
* (`"parent-id"` + `"-"` + `"own-id"`).
|
|
24
|
+
*
|
|
25
|
+
* Either side can be `undefined`; the result is the other one, or
|
|
26
|
+
* `undefined` if both are absent.
|
|
27
|
+
*/
|
|
28
|
+
export const chainLayoutGroupId = (parent, own) => {
|
|
29
|
+
if (!parent)
|
|
30
|
+
return own;
|
|
31
|
+
if (!own)
|
|
32
|
+
return parent;
|
|
33
|
+
return `${parent}-${own}`;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Apply a LayoutGroup scope to a raw `layoutId` for registry lookups.
|
|
37
|
+
* Returns the un-prefixed id when no group is in scope.
|
|
38
|
+
*/
|
|
39
|
+
export const scopeLayoutId = (groupId, layoutId) => {
|
|
40
|
+
return groupId ? `${groupId}::${layoutId}` : layoutId;
|
|
41
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deferred references to the chain of `layoutScroll` ancestors for the
|
|
3
|
+
* current subtree. Returned as a thunk because element refs are bound
|
|
4
|
+
* after mount; consumers invoke at measurement time.
|
|
5
|
+
*
|
|
6
|
+
* Order is closest-first. Order doesn't matter for the current scroll-
|
|
7
|
+
* offset sum, but is preserved so future per-container semantics (e.g.
|
|
8
|
+
* a `scroll.wasRoot` marker like framer-motion) can iterate deterministically.
|
|
9
|
+
*/
|
|
10
|
+
export type LayoutScrollContainerRef = () => Array<HTMLElement | null | undefined>;
|
|
11
|
+
/**
|
|
12
|
+
* Publish the scroll-container chain for descendant motion components.
|
|
13
|
+
*
|
|
14
|
+
* Called on a `motion.*` component with `layoutScroll` enabled during
|
|
15
|
+
* its init phase. The provided thunk should resolve to `[...ancestorChain,
|
|
16
|
+
* ownElement]` — descendants get the full chain in one call.
|
|
17
|
+
*
|
|
18
|
+
* Mirrors framer-motion's `removeElementScroll`, which walks the path
|
|
19
|
+
* and sums every `layoutScroll` ancestor's offset.
|
|
20
|
+
*
|
|
21
|
+
* @param ref Thunk returning the full ancestor chain (closest first).
|
|
22
|
+
*/
|
|
23
|
+
export declare const setLayoutScrollContainer: (ref: LayoutScrollContainerRef) => void;
|
|
24
|
+
/**
|
|
25
|
+
* Capture the ancestor chain thunk at component init.
|
|
26
|
+
*
|
|
27
|
+
* Important: call this **before** the same component calls
|
|
28
|
+
* `setLayoutScrollContainer(...)`. Otherwise the lookup returns the
|
|
29
|
+
* component's own thunk (Svelte `setContext` shadows from the call site
|
|
30
|
+
* down) and the chain collapses.
|
|
31
|
+
*
|
|
32
|
+
* Returns `undefined` when no ancestor has `layoutScroll`.
|
|
33
|
+
*
|
|
34
|
+
* @returns Ancestor chain thunk, or `undefined`.
|
|
35
|
+
*/
|
|
36
|
+
export declare const getLayoutScrollContainerRef: () => LayoutScrollContainerRef | undefined;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { getContext, setContext } from 'svelte';
|
|
2
|
+
const LAYOUT_SCROLL_CONTEXT_KEY = Symbol('layout-scroll-container');
|
|
3
|
+
/**
|
|
4
|
+
* Publish the scroll-container chain for descendant motion components.
|
|
5
|
+
*
|
|
6
|
+
* Called on a `motion.*` component with `layoutScroll` enabled during
|
|
7
|
+
* its init phase. The provided thunk should resolve to `[...ancestorChain,
|
|
8
|
+
* ownElement]` — descendants get the full chain in one call.
|
|
9
|
+
*
|
|
10
|
+
* Mirrors framer-motion's `removeElementScroll`, which walks the path
|
|
11
|
+
* and sums every `layoutScroll` ancestor's offset.
|
|
12
|
+
*
|
|
13
|
+
* @param ref Thunk returning the full ancestor chain (closest first).
|
|
14
|
+
*/
|
|
15
|
+
export const setLayoutScrollContainer = (ref) => {
|
|
16
|
+
setContext(LAYOUT_SCROLL_CONTEXT_KEY, ref);
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Capture the ancestor chain thunk at component init.
|
|
20
|
+
*
|
|
21
|
+
* Important: call this **before** the same component calls
|
|
22
|
+
* `setLayoutScrollContainer(...)`. Otherwise the lookup returns the
|
|
23
|
+
* component's own thunk (Svelte `setContext` shadows from the call site
|
|
24
|
+
* down) and the chain collapses.
|
|
25
|
+
*
|
|
26
|
+
* Returns `undefined` when no ancestor has `layoutScroll`.
|
|
27
|
+
*
|
|
28
|
+
* @returns Ancestor chain thunk, or `undefined`.
|
|
29
|
+
*/
|
|
30
|
+
export const getLayoutScrollContainerRef = () => {
|
|
31
|
+
return getContext(LAYOUT_SCROLL_CONTEXT_KEY);
|
|
32
|
+
};
|
|
@@ -65,6 +65,11 @@
|
|
|
65
65
|
SVG_NAMESPACE
|
|
66
66
|
} from '../utils/svg'
|
|
67
67
|
import { getLayoutIdRegistry } from '../utils/layoutId'
|
|
68
|
+
import {
|
|
69
|
+
getLayoutScrollContainerRef,
|
|
70
|
+
setLayoutScrollContainer
|
|
71
|
+
} from '../components/layoutScroll.context'
|
|
72
|
+
import { getLayoutGroupContext, scopeLayoutId } from '../components/layoutGroup.context'
|
|
68
73
|
|
|
69
74
|
type Props = MotionProps & {
|
|
70
75
|
children?: Snippet
|
|
@@ -118,6 +123,7 @@
|
|
|
118
123
|
dragControls: dragControlsProp,
|
|
119
124
|
layout: layoutProp,
|
|
120
125
|
layoutId: layoutIdProp,
|
|
126
|
+
layoutScroll: layoutScrollProp,
|
|
121
127
|
ref: element = $bindable(null),
|
|
122
128
|
...rest
|
|
123
129
|
}: Props = $props()
|
|
@@ -142,6 +148,39 @@
|
|
|
142
148
|
// Get layoutId registry (provided by AnimatePresence or a parent LayoutGroup)
|
|
143
149
|
const layoutIdRegistry = getLayoutIdRegistry()
|
|
144
150
|
|
|
151
|
+
// Scope layoutId by the surrounding <LayoutGroup>, so identical
|
|
152
|
+
// layoutId values in two sibling groups don't cross-animate (#311).
|
|
153
|
+
// Undefined when no group is in scope — descendants behave exactly
|
|
154
|
+
// as before relative to the global registry.
|
|
155
|
+
const layoutGroupId = getLayoutGroupContext()
|
|
156
|
+
const scopedLayoutId = $derived(
|
|
157
|
+
layoutIdProp ? scopeLayoutId(layoutGroupId, layoutIdProp) : undefined
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
// Capture the ancestor `layoutScroll` chain BEFORE we potentially shadow
|
|
161
|
+
// the context with ourselves below — this element's own FLIP measurements
|
|
162
|
+
// must resolve against the *ancestors*' scroll containers, not against
|
|
163
|
+
// itself.
|
|
164
|
+
//
|
|
165
|
+
// We walk the full chain (not just the nearest) so a `layoutScroll`
|
|
166
|
+
// outside another `layoutScroll` still contributes to descendant
|
|
167
|
+
// measurements — matches framer-motion's `removeElementScroll` walking
|
|
168
|
+
// `this.path`.
|
|
169
|
+
const ancestorScrollContainerRef = getLayoutScrollContainerRef()
|
|
170
|
+
if (layoutScrollProp) {
|
|
171
|
+
// Publish [...ancestorChain, ownElement]. The chain is collected
|
|
172
|
+
// lazily because element refs bind after mount.
|
|
173
|
+
setLayoutScrollContainer(() => {
|
|
174
|
+
const inherited = ancestorScrollContainerRef?.() ?? []
|
|
175
|
+
return element ? [...inherited, element] : inherited
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
const resolveLayoutScrollAncestors = (): HTMLElement[] => {
|
|
179
|
+
const refs = ancestorScrollContainerRef?.() ?? []
|
|
180
|
+
// Filter out unbound refs (HTMLElement | null | undefined → HTMLElement[]).
|
|
181
|
+
return refs.filter((el): el is HTMLElement => Boolean(el))
|
|
182
|
+
}
|
|
183
|
+
|
|
145
184
|
// Get current presence depth (0 = direct child of AnimatePresence, undefined = not in AnimatePresence)
|
|
146
185
|
const presenceDepth = getPresenceDepth()
|
|
147
186
|
|
|
@@ -213,11 +252,14 @@
|
|
|
213
252
|
$effect(() => {
|
|
214
253
|
if (!(element && layoutIdProp && layoutIdRegistry)) return
|
|
215
254
|
|
|
216
|
-
// Capture rect on every frame while mounted
|
|
255
|
+
// Capture rect on every frame while mounted. Re-express in the
|
|
256
|
+
// nearest layoutScroll ancestor's coordinate space so the FLIP-from
|
|
257
|
+
// rect stored at unmount stays correct even if the scroll container
|
|
258
|
+
// moved between the snapshot and the next element's mount.
|
|
217
259
|
let rafId: number
|
|
218
260
|
const captureRect = () => {
|
|
219
261
|
if (element) {
|
|
220
|
-
layoutIdLastRect = element
|
|
262
|
+
layoutIdLastRect = measureRect(element, resolveLayoutScrollAncestors())
|
|
221
263
|
}
|
|
222
264
|
rafId = requestAnimationFrame(captureRect)
|
|
223
265
|
}
|
|
@@ -226,9 +268,9 @@
|
|
|
226
268
|
// On cleanup (before DOM removal), push last-known rect to registry
|
|
227
269
|
return () => {
|
|
228
270
|
cancelAnimationFrame(rafId)
|
|
229
|
-
if (layoutIdLastRect &&
|
|
271
|
+
if (layoutIdLastRect && scopedLayoutId) {
|
|
230
272
|
layoutIdRegistry.snapshot(
|
|
231
|
-
|
|
273
|
+
scopedLayoutId,
|
|
232
274
|
layoutIdLastRect,
|
|
233
275
|
(mergedTransition ?? {}) as AnimationOptions
|
|
234
276
|
)
|
|
@@ -701,17 +743,18 @@
|
|
|
701
743
|
if (!(element && layoutProp && isLoaded === 'ready')) return
|
|
702
744
|
|
|
703
745
|
// Initialize last rect on first ready frame
|
|
704
|
-
lastRect = measureRect(element
|
|
746
|
+
lastRect = measureRect(element!, resolveLayoutScrollAncestors())
|
|
705
747
|
// Hint compositor for smoother FLIP transforms
|
|
706
748
|
setCompositorHints(element!, true)
|
|
707
749
|
|
|
708
750
|
let rafId: number | null = null
|
|
709
751
|
const runFlip = () => {
|
|
752
|
+
const scrollContainers = resolveLayoutScrollAncestors()
|
|
710
753
|
if (!lastRect) {
|
|
711
|
-
lastRect = measureRect(element
|
|
754
|
+
lastRect = measureRect(element!, scrollContainers)
|
|
712
755
|
return
|
|
713
756
|
}
|
|
714
|
-
const next = measureRect(element
|
|
757
|
+
const next = measureRect(element!, scrollContainers)
|
|
715
758
|
const transforms = computeFlipTransforms(lastRect, next, layoutProp ?? false)
|
|
716
759
|
runFlipAnimation(element!, transforms, (mergedTransition ?? {}) as AnimationOptions)
|
|
717
760
|
lastRect = next
|
|
@@ -740,12 +783,12 @@
|
|
|
740
783
|
// Shared layout animation via layoutId.
|
|
741
784
|
// On mount, consume the previous snapshot and FLIP from its position.
|
|
742
785
|
$effect(() => {
|
|
743
|
-
if (!(element &&
|
|
786
|
+
if (!(element && scopedLayoutId && layoutIdRegistry && isLoaded === 'ready')) return
|
|
744
787
|
|
|
745
|
-
const prev = layoutIdRegistry.consume(
|
|
788
|
+
const prev = layoutIdRegistry.consume(scopedLayoutId)
|
|
746
789
|
if (!prev) return // First appearance, no animation needed
|
|
747
790
|
|
|
748
|
-
const next = measureRect(element)
|
|
791
|
+
const next = measureRect(element, resolveLayoutScrollAncestors())
|
|
749
792
|
const transforms = computeFlipTransforms(prev.rect, next, true)
|
|
750
793
|
|
|
751
794
|
setCompositorHints(element, true)
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import AnimatePresence from './components/AnimatePresence.svelte';
|
|
2
|
+
import LayoutGroup from './components/LayoutGroup.svelte';
|
|
2
3
|
import MotionConfig from './components/MotionConfig.svelte';
|
|
3
4
|
import PresenceChild from './components/PresenceChild.svelte';
|
|
4
5
|
export { motion } from './motion';
|
|
@@ -32,7 +33,7 @@ export { stringifyStyleObject } from './utils/styleObject';
|
|
|
32
33
|
export { styleString } from './utils/styleObject.svelte';
|
|
33
34
|
export { useTime } from './utils/time';
|
|
34
35
|
export { useTransform } from './utils/transform';
|
|
35
|
-
export { AnimatePresence, MotionConfig, PresenceChild };
|
|
36
|
+
export { AnimatePresence, LayoutGroup, MotionConfig, PresenceChild };
|
|
36
37
|
export { default as MotionA } from './html/A.svelte';
|
|
37
38
|
export { default as MotionAbbr } from './html/Abbr.svelte';
|
|
38
39
|
export { default as MotionAddress } from './html/Address.svelte';
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import AnimatePresence from './components/AnimatePresence.svelte';
|
|
2
|
+
import LayoutGroup from './components/LayoutGroup.svelte';
|
|
2
3
|
import MotionConfig from './components/MotionConfig.svelte';
|
|
3
4
|
import PresenceChild from './components/PresenceChild.svelte';
|
|
4
5
|
export { motion } from './motion';
|
|
@@ -29,7 +30,7 @@ export { stringifyStyleObject } from './utils/styleObject';
|
|
|
29
30
|
export { styleString } from './utils/styleObject.svelte';
|
|
30
31
|
export { useTime } from './utils/time';
|
|
31
32
|
export { useTransform } from './utils/transform';
|
|
32
|
-
export { AnimatePresence, MotionConfig, PresenceChild };
|
|
33
|
+
export { AnimatePresence, LayoutGroup, MotionConfig, PresenceChild };
|
|
33
34
|
// Named component exports — tree-shakeable alternative to the `motion` object
|
|
34
35
|
export { default as MotionA } from './html/A.svelte';
|
|
35
36
|
export { default as MotionAbbr } from './html/Abbr.svelte';
|
package/dist/types.d.ts
CHANGED
|
@@ -335,6 +335,22 @@ export type MotionProps = {
|
|
|
335
335
|
layout?: boolean | 'position';
|
|
336
336
|
/** Shared layout animation identifier. Elements with matching layoutId animate between positions. */
|
|
337
337
|
layoutId?: string;
|
|
338
|
+
/**
|
|
339
|
+
* Mark this element as a scroll container so descendant `layout` animations
|
|
340
|
+
* measure rects in this container's coordinate space. Without it, scrolling
|
|
341
|
+
* mid-animation makes the FLIP transform fight the scroll and the layout
|
|
342
|
+
* animation drifts.
|
|
343
|
+
*
|
|
344
|
+
* Apply on the same element as `overflow: scroll` / `overflow: auto`.
|
|
345
|
+
*
|
|
346
|
+
* @example
|
|
347
|
+
* ```svelte
|
|
348
|
+
* <motion.div layoutScroll style="overflow: auto">
|
|
349
|
+
* <motion.div layout />
|
|
350
|
+
* </motion.div>
|
|
351
|
+
* ```
|
|
352
|
+
*/
|
|
353
|
+
layoutScroll?: boolean;
|
|
338
354
|
/** Ref to the element */
|
|
339
355
|
ref?: HTMLElement | null;
|
|
340
356
|
/** Enable drag gestures. true for both axes, or lock to 'x'/'y'. */
|
package/dist/utils/layout.d.ts
CHANGED
|
@@ -5,10 +5,32 @@ import { type AnimationOptions } from 'motion';
|
|
|
5
5
|
* Temporarily clears `transform` to avoid skewing measurements, restoring it
|
|
6
6
|
* immediately after reading the rect.
|
|
7
7
|
*
|
|
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.
|
|
14
|
+
*
|
|
15
|
+
* Pass an empty array (or omit) for viewport-relative behaviour.
|
|
16
|
+
*
|
|
8
17
|
* @param el Element to measure.
|
|
9
|
-
* @
|
|
18
|
+
* @param scrollContainers Optional ancestor chain with `layoutScroll` enabled.
|
|
19
|
+
* @returns DOMRect snapshot of the element.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* // No scroll containers — viewport-relative rect.
|
|
24
|
+
* const rect = measureRect(node)
|
|
25
|
+
*
|
|
26
|
+
* // Single ancestor scroll container (one `layoutScroll`).
|
|
27
|
+
* const rect = measureRect(node, [scrollPanel])
|
|
28
|
+
*
|
|
29
|
+
* // Nested `layoutScroll` ancestors — sums offsets from every container.
|
|
30
|
+
* const rect = measureRect(node, [innerScroll, outerScroll])
|
|
31
|
+
* ```
|
|
10
32
|
*/
|
|
11
|
-
export declare const measureRect: (el: HTMLElement) => DOMRect;
|
|
33
|
+
export declare const measureRect: (el: HTMLElement, scrollContainers?: HTMLElement[]) => DOMRect;
|
|
12
34
|
/**
|
|
13
35
|
* Compute FLIP transform deltas between two rects.
|
|
14
36
|
*
|
package/dist/utils/layout.js
CHANGED
|
@@ -5,14 +5,49 @@ import { animate } from 'motion';
|
|
|
5
5
|
* Temporarily clears `transform` to avoid skewing measurements, restoring it
|
|
6
6
|
* immediately after reading the rect.
|
|
7
7
|
*
|
|
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.
|
|
14
|
+
*
|
|
15
|
+
* Pass an empty array (or omit) for viewport-relative behaviour.
|
|
16
|
+
*
|
|
8
17
|
* @param el Element to measure.
|
|
9
|
-
* @
|
|
18
|
+
* @param scrollContainers Optional ancestor chain with `layoutScroll` enabled.
|
|
19
|
+
* @returns DOMRect snapshot of the element.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* // No scroll containers — viewport-relative rect.
|
|
24
|
+
* const rect = measureRect(node)
|
|
25
|
+
*
|
|
26
|
+
* // Single ancestor scroll container (one `layoutScroll`).
|
|
27
|
+
* const rect = measureRect(node, [scrollPanel])
|
|
28
|
+
*
|
|
29
|
+
* // Nested `layoutScroll` ancestors — sums offsets from every container.
|
|
30
|
+
* const rect = measureRect(node, [innerScroll, outerScroll])
|
|
31
|
+
* ```
|
|
10
32
|
*/
|
|
11
|
-
export const measureRect = (el) => {
|
|
33
|
+
export const measureRect = (el, scrollContainers) => {
|
|
12
34
|
const prev = el.style.transform;
|
|
13
35
|
try {
|
|
14
36
|
el.style.transform = 'none';
|
|
15
|
-
|
|
37
|
+
const rect = el.getBoundingClientRect();
|
|
38
|
+
if (!scrollContainers || scrollContainers.length === 0)
|
|
39
|
+
return rect;
|
|
40
|
+
// Re-express the rect in the *combined* scroll-container coordinate
|
|
41
|
+
// space so a subsequent scroll on any of them doesn't show up as
|
|
42
|
+
// movement. DOMRect's left/top are read-only, so allocate a fresh
|
|
43
|
+
// one with the summed offsets applied.
|
|
44
|
+
let offsetLeft = 0;
|
|
45
|
+
let offsetTop = 0;
|
|
46
|
+
for (const container of scrollContainers) {
|
|
47
|
+
offsetLeft += container.scrollLeft;
|
|
48
|
+
offsetTop += container.scrollTop;
|
|
49
|
+
}
|
|
50
|
+
return new DOMRect(rect.left + offsetLeft, rect.top + offsetTop, rect.width, rect.height);
|
|
16
51
|
}
|
|
17
52
|
finally {
|
|
18
53
|
el.style.transform = prev;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@humanspeak/svelte-motion",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
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",
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
"@tailwindcss/vite": "^4.3.0",
|
|
115
115
|
"@testing-library/jest-dom": "^6.9.1",
|
|
116
116
|
"@testing-library/svelte": "^5.3.1",
|
|
117
|
-
"@types/node": "^25.9.
|
|
117
|
+
"@types/node": "^25.9.1",
|
|
118
118
|
"@vitest/coverage-v8": "^4.1.6",
|
|
119
119
|
"eslint": "^10.4.0",
|
|
120
120
|
"eslint-config-prettier": "10.1.8",
|
|
@@ -135,16 +135,16 @@
|
|
|
135
135
|
"prettier-plugin-tailwindcss": "^0.8.0",
|
|
136
136
|
"publint": "^0.3.21",
|
|
137
137
|
"runed": "0.37.1",
|
|
138
|
-
"svelte": "^5.55.
|
|
138
|
+
"svelte": "^5.55.8",
|
|
139
139
|
"svelte-check": "^4.4.8",
|
|
140
140
|
"svg-tags": "^1.0.0",
|
|
141
141
|
"tailwind-merge": "^3.6.0",
|
|
142
142
|
"tailwind-variants": "^3.2.2",
|
|
143
143
|
"tailwindcss": "^4.3.0",
|
|
144
144
|
"tailwindcss-animate": "^1.0.7",
|
|
145
|
-
"tsx": "^4.22.
|
|
145
|
+
"tsx": "^4.22.3",
|
|
146
146
|
"typescript": "^6.0.3",
|
|
147
|
-
"typescript-eslint": "^8.59.
|
|
147
|
+
"typescript-eslint": "^8.59.4",
|
|
148
148
|
"vite": "^8.0.13",
|
|
149
149
|
"vite-tsconfig-paths": "^6.1.1",
|
|
150
150
|
"vitest": "^4.1.6"
|