@humanspeak/svelte-motion 0.5.3 → 0.5.4

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,70 @@
1
+ <script lang="ts">
2
+ import { setLazyMotionContext } from './lazyMotion.context'
3
+ import { domMin } from '../features/domMin'
4
+ import {
5
+ isLazyFeatureBundle,
6
+ normalizeLazyFeatureBundle,
7
+ type FeatureBundle,
8
+ type LazyFeatureBundle
9
+ } from '../features'
10
+ import { untrack, type Snippet } from 'svelte'
11
+
12
+ /**
13
+ * Props accepted by the LazyMotion component.
14
+ */
15
+ type Props = {
16
+ /** Child content rendered inside the active LazyMotion context. */
17
+ children?: Snippet
18
+ /** Eager or async feature bundle used by descendant `m.*` components. */
19
+ features: FeatureBundle | LazyFeatureBundle
20
+ /** Enables strict LazyMotion usage checks. Defaults to false. */
21
+ strict?: boolean
22
+ }
23
+
24
+ let { children, features, strict = false }: Props = $props()
25
+
26
+ let loadedFeatures = $state<FeatureBundle>(
27
+ untrack(() => (isLazyFeatureBundle(features) ? domMin : features))
28
+ )
29
+ let isLoaded = $state(untrack(() => !isLazyFeatureBundle(features)))
30
+
31
+ setLazyMotionContext({
32
+ getFeatures: () => loadedFeatures,
33
+ getIsLoaded: () => isLoaded,
34
+ get strict() {
35
+ return strict
36
+ }
37
+ })
38
+
39
+ let loadId = 0
40
+
41
+ $effect(() => {
42
+ const currentLoadId = ++loadId
43
+
44
+ if (!isLazyFeatureBundle(features)) {
45
+ loadedFeatures = features
46
+ isLoaded = true
47
+ return
48
+ }
49
+
50
+ loadedFeatures = domMin
51
+ isLoaded = false
52
+ features()
53
+ .then((bundle) => {
54
+ if (currentLoadId !== loadId) return
55
+ loadedFeatures = normalizeLazyFeatureBundle(bundle)
56
+ isLoaded = true
57
+ })
58
+ .catch(() => {
59
+ if (currentLoadId !== loadId) return
60
+ loadedFeatures = domMin
61
+ isLoaded = false
62
+ })
63
+
64
+ return () => {
65
+ loadId += 1
66
+ }
67
+ })
68
+ </script>
69
+
70
+ {@render children?.()}
@@ -0,0 +1,16 @@
1
+ import { type FeatureBundle, type LazyFeatureBundle } from '../features';
2
+ import { type Snippet } from 'svelte';
3
+ /**
4
+ * Props accepted by the LazyMotion component.
5
+ */
6
+ type Props = {
7
+ /** Child content rendered inside the active LazyMotion context. */
8
+ children?: Snippet;
9
+ /** Eager or async feature bundle used by descendant `m.*` components. */
10
+ features: FeatureBundle | LazyFeatureBundle;
11
+ /** Enables strict LazyMotion usage checks. Defaults to false. */
12
+ strict?: boolean;
13
+ };
14
+ declare const LazyMotion: import("svelte").Component<Props, {}, "">;
15
+ type LazyMotion = ReturnType<typeof LazyMotion>;
16
+ export default LazyMotion;
@@ -0,0 +1,25 @@
1
+ import type { FeatureBundle } from '../features';
2
+ /**
3
+ * Context value published by `<LazyMotion>` for descendant motion elements.
4
+ */
5
+ export type LazyMotionContext = {
6
+ /** Returns the currently active feature bundle. */
7
+ getFeatures: () => FeatureBundle;
8
+ /** Returns whether an async bundle has finished loading. */
9
+ getIsLoaded: () => boolean;
10
+ /** Enables Framer Motion-style strict lazy usage checks. */
11
+ strict: boolean;
12
+ };
13
+ /**
14
+ * Reads the nearest LazyMotion context.
15
+ *
16
+ * @returns The active LazyMotion context, or undefined outside LazyMotion.
17
+ */
18
+ export declare const getLazyMotionContext: () => LazyMotionContext | undefined;
19
+ /**
20
+ * Publishes a LazyMotion context for descendant motion elements.
21
+ *
22
+ * @param context - LazyMotion context to publish.
23
+ * @returns The same context value returned by Svelte's setContext.
24
+ */
25
+ export declare const setLazyMotionContext: (context: LazyMotionContext) => LazyMotionContext;
@@ -0,0 +1,19 @@
1
+ import { getContext, setContext } from 'svelte';
2
+ const key = Symbol('lazyMotion');
3
+ /**
4
+ * Reads the nearest LazyMotion context.
5
+ *
6
+ * @returns The active LazyMotion context, or undefined outside LazyMotion.
7
+ */
8
+ export const getLazyMotionContext = () => {
9
+ return getContext(key);
10
+ };
11
+ /**
12
+ * Publishes a LazyMotion context for descendant motion elements.
13
+ *
14
+ * @param context - LazyMotion context to publish.
15
+ * @returns The same context value returned by Svelte's setContext.
16
+ */
17
+ export const setLazyMotionContext = (context) => {
18
+ return setContext(key, context);
19
+ };
@@ -0,0 +1,6 @@
1
+ import type { FeatureBundle } from './index';
2
+ /**
3
+ * DOM animation feature bundle: animations plus hover, tap, focus, pan,
4
+ * and in-view gestures.
5
+ */
6
+ export declare const domAnimation: FeatureBundle;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * DOM animation feature bundle: animations plus hover, tap, focus, pan,
3
+ * and in-view gestures.
4
+ */
5
+ export const domAnimation = {
6
+ animations: true,
7
+ gestures: true
8
+ };
@@ -0,0 +1,5 @@
1
+ import type { FeatureBundle } from './index';
2
+ /**
3
+ * Full DOM feature bundle: animations, gestures, drag, and layout features.
4
+ */
5
+ export declare const domMax: FeatureBundle;
@@ -0,0 +1,9 @@
1
+ import { domAnimation } from './domAnimation';
2
+ /**
3
+ * Full DOM feature bundle: animations, gestures, drag, and layout features.
4
+ */
5
+ export const domMax = {
6
+ ...domAnimation,
7
+ drag: true,
8
+ layout: true
9
+ };
@@ -0,0 +1,5 @@
1
+ import type { FeatureBundle } from './index';
2
+ /**
3
+ * Minimal DOM feature bundle: mount/update animations only.
4
+ */
5
+ export declare const domMin: FeatureBundle;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Minimal DOM feature bundle: mount/update animations only.
3
+ */
4
+ export const domMin = {
5
+ animations: true
6
+ };
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Runtime capabilities made available to motion components inside a
3
+ * `<LazyMotion>` subtree.
4
+ */
5
+ export type FeatureBundle = {
6
+ /** Enables initial/animate/exit animation behavior. */
7
+ animations: true;
8
+ /** Enables hover, tap, focus, pan, and in-view gesture behavior. */
9
+ gestures?: true;
10
+ /** Enables drag gesture behavior. */
11
+ drag?: true;
12
+ /** Enables layout and shared-layout animation behavior. */
13
+ layout?: true;
14
+ };
15
+ /**
16
+ * Function form accepted by `<LazyMotion features>`.
17
+ *
18
+ * The function resolves to a feature bundle directly or to a module-like
19
+ * object with the bundle as its default export.
20
+ */
21
+ export type LazyFeatureBundle = () => Promise<FeatureBundle | {
22
+ default: FeatureBundle;
23
+ }>;
24
+ /**
25
+ * Returns whether a LazyMotion `features` value is an async loader.
26
+ *
27
+ * @param features - Feature bundle or loader passed to `<LazyMotion>`.
28
+ * @returns True when the features value should be invoked asynchronously.
29
+ */
30
+ export declare const isLazyFeatureBundle: (features: FeatureBundle | LazyFeatureBundle) => features is LazyFeatureBundle;
31
+ /**
32
+ * Normalizes an asynchronously loaded feature bundle.
33
+ *
34
+ * @param loaded - Resolved bundle or default-export module wrapper.
35
+ * @returns The concrete feature bundle.
36
+ */
37
+ export declare const normalizeLazyFeatureBundle: (loaded: FeatureBundle | {
38
+ default: FeatureBundle;
39
+ }) => FeatureBundle;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Returns whether a LazyMotion `features` value is an async loader.
3
+ *
4
+ * @param features - Feature bundle or loader passed to `<LazyMotion>`.
5
+ * @returns True when the features value should be invoked asynchronously.
6
+ */
7
+ export const isLazyFeatureBundle = (features) => typeof features === 'function';
8
+ /**
9
+ * Normalizes an asynchronously loaded feature bundle.
10
+ *
11
+ * @param loaded - Resolved bundle or default-export module wrapper.
12
+ * @returns The concrete feature bundle.
13
+ */
14
+ export const normalizeLazyFeatureBundle = (loaded) => {
15
+ if ('default' in loaded)
16
+ return loaded.default;
17
+ return loaded;
18
+ };
@@ -5,6 +5,8 @@
5
5
 
6
6
  <script lang="ts">
7
7
  import { getMotionConfig } from '../components/motionConfig.context'
8
+ import { getLazyMotionContext } from '../components/lazyMotion.context'
9
+ import { domMax } from '../features/domMax'
8
10
  import {
9
11
  filterReducedMotionKeyframes,
10
12
  useReducedMotionConfig
@@ -156,6 +158,11 @@
156
158
  let enterAnimationSettled = $state(false)
157
159
  let dataPath = $state<number>(-1)
158
160
  const motionConfig = $derived(getMotionConfig())
161
+ const lazyMotion = getLazyMotionContext()
162
+ const activeFeatures = $derived(lazyMotion?.getFeatures() ?? domMax)
163
+ const hasGestureFeatures = $derived(!!activeFeatures.gestures)
164
+ const hasDragFeatures = $derived(!!activeFeatures.drag)
165
+ const hasLayoutFeatures = $derived(!!activeFeatures.layout)
159
166
  const reducedMotionState = useReducedMotionConfig()
160
167
  // `.current` is $state-backed inside reducedMotionState; tracking it via
161
168
  // $derived makes `reducedMotion` re-evaluate whenever the OS preference
@@ -553,7 +560,8 @@
553
560
  // key, empty array) would otherwise add `tabindex=0` for an
554
561
  // element that never actually receives a tap gesture — an
555
562
  // unintended tab stop. (#349 CR feedback)
556
- ...(isNotEmpty(resolvedWhileTap) &&
563
+ ...(hasGestureFeatures &&
564
+ isNotEmpty(resolvedWhileTap) &&
557
565
  !isNativelyFocusable(tag, rest as Record<string, unknown>) &&
558
566
  ((rest as Record<string, unknown>)?.tabindex ??
559
567
  (rest as Record<string, unknown>)?.tabIndex ??
@@ -626,7 +634,7 @@
626
634
  // after any non-zero duration settle animation.
627
635
  let teardownDrag: (() => void) | null = null
628
636
  $effect(() => {
629
- if (!(element && isLoaded === 'ready')) return
637
+ if (!(element && isLoaded === 'ready' && hasDragFeatures)) return
630
638
  // Only attach if drag enabled
631
639
  if (!dragProp) return
632
640
  // Clean up previous
@@ -787,7 +795,7 @@
787
795
  isLoaded
788
796
  })
789
797
  }
790
- if (!element) return
798
+ if (!element || !hasGestureFeatures) return
791
799
  // Defer attachment until the element has settled out of the enter
792
800
  // animation phase — matches the gate every other gesture effect
793
801
  // in this file uses (drag, whileTap, whileHover, whileFocus,
@@ -1050,7 +1058,7 @@
1050
1058
  // When layout === true we also scale to smoothly interpolate size changes.
1051
1059
  let lastRect: RectLike | null = null
1052
1060
  $effect(() => {
1053
- if (!(element && layoutProp && isLoaded === 'ready')) return
1061
+ if (!(element && layoutProp && isLoaded === 'ready' && hasLayoutFeatures)) return
1054
1062
 
1055
1063
  // Initialize last rect on first ready frame. We measure through the
1056
1064
  // projection node rather than `measureRect` directly so the rect is
@@ -1105,7 +1113,16 @@
1105
1113
  // Shared layout animation via layoutId.
1106
1114
  // On mount, consume the previous snapshot and FLIP from its position.
1107
1115
  $effect(() => {
1108
- if (!(element && scopedLayoutId && layoutIdRegistry && isLoaded === 'ready')) return
1116
+ if (
1117
+ !(
1118
+ element &&
1119
+ scopedLayoutId &&
1120
+ layoutIdRegistry &&
1121
+ isLoaded === 'ready' &&
1122
+ hasLayoutFeatures
1123
+ )
1124
+ )
1125
+ return
1109
1126
 
1110
1127
  const prev = layoutIdRegistry.consume(scopedLayoutId)
1111
1128
  if (!prev) return // First appearance, no animation needed
@@ -1123,7 +1140,10 @@
1123
1140
 
1124
1141
  // whileTap handling via motion-dom's press()
1125
1142
  $effect(() => {
1126
- if (!(element && isLoaded === 'ready' && isNotEmpty(resolvedWhileTap))) return
1143
+ if (
1144
+ !(element && isLoaded === 'ready' && hasGestureFeatures && isNotEmpty(resolvedWhileTap))
1145
+ )
1146
+ return
1127
1147
  return attachWhileTap(
1128
1148
  element!,
1129
1149
  (resolvedWhileTap ?? {}) as Record<string, unknown>,
@@ -1143,7 +1163,15 @@
1143
1163
 
1144
1164
  // whileHover handling, gated to true-hover devices to avoid sticky states on touch
1145
1165
  $effect(() => {
1146
- if (!(element && isLoaded === 'ready' && isNotEmpty(resolvedWhileHover))) return
1166
+ if (
1167
+ !(
1168
+ element &&
1169
+ isLoaded === 'ready' &&
1170
+ hasGestureFeatures &&
1171
+ isNotEmpty(resolvedWhileHover)
1172
+ )
1173
+ )
1174
+ return
1147
1175
  return attachWhileHover(
1148
1176
  element!,
1149
1177
  (resolvedWhileHover ?? {}) as Record<string, unknown>,
@@ -1158,7 +1186,15 @@
1158
1186
 
1159
1187
  // whileFocus handling for keyboard focus interactions
1160
1188
  $effect(() => {
1161
- if (!(element && isLoaded === 'ready' && isNotEmpty(resolvedWhileFocus))) return
1189
+ if (
1190
+ !(
1191
+ element &&
1192
+ isLoaded === 'ready' &&
1193
+ hasGestureFeatures &&
1194
+ isNotEmpty(resolvedWhileFocus)
1195
+ )
1196
+ )
1197
+ return
1162
1198
  return attachWhileFocus(
1163
1199
  element!,
1164
1200
  (resolvedWhileFocus ?? {}) as Record<string, unknown>,
@@ -1173,7 +1209,15 @@
1173
1209
 
1174
1210
  // whileInView handling for viewport intersection
1175
1211
  $effect(() => {
1176
- if (!(element && isLoaded === 'ready' && isNotEmpty(resolvedWhileInView))) return
1212
+ if (
1213
+ !(
1214
+ element &&
1215
+ isLoaded === 'ready' &&
1216
+ hasGestureFeatures &&
1217
+ isNotEmpty(resolvedWhileInView)
1218
+ )
1219
+ )
1220
+ return
1177
1221
  return attachWhileInView(
1178
1222
  element!,
1179
1223
  (resolvedWhileInView ?? {}) as Record<string, unknown>,
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';
@@ -42,7 +48,7 @@ export { styleString } from './utils/styleObject.svelte';
42
48
  export { useTime } from './utils/time.svelte';
43
49
  export { useTransform } from './utils/transform.svelte';
44
50
  export type { MultiTransformer, SingleTransformer, TransformOptions, TransformOutputMap, TransformSource } from './utils/transform.svelte';
45
- export { AnimatePresence, LayoutGroup, MotionConfig, PresenceChild };
51
+ export { AnimatePresence, LayoutGroup, LazyMotion, MotionConfig, PresenceChild };
46
52
  export { default as MotionA } from './html/A.svelte';
47
53
  export { default as MotionAbbr } from './html/Abbr.svelte';
48
54
  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';
@@ -31,7 +36,7 @@ export { stringifyStyleObject } from './utils/styleObject';
31
36
  export { styleString } from './utils/styleObject.svelte';
32
37
  export { useTime } from './utils/time.svelte';
33
38
  export { useTransform } from './utils/transform.svelte';
34
- export { AnimatePresence, LayoutGroup, MotionConfig, PresenceChild };
39
+ export { AnimatePresence, LayoutGroup, LazyMotion, MotionConfig, PresenceChild };
35
40
  // Named component exports — tree-shakeable alternative to the `motion` object
36
41
  export { default as MotionA } from './html/A.svelte';
37
42
  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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@humanspeak/svelte-motion",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
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",