@humanspeak/svelte-motion 0.4.6 → 0.4.7

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 CHANGED
@@ -36,18 +36,18 @@ npm install @humanspeak/svelte-motion
36
36
 
37
37
  Goal: Framer Motion API parity for Svelte where common React examples can be translated with minimal changes.
38
38
 
39
- | Capability | Status |
40
- | --------------------------------------------------------- | ------------------------------- |
41
- | `initial` / `animate` / `transition` | Supported |
42
- | `variants` (string keys + inheritance) | Supported |
43
- | `whileHover` / `whileTap` / `whileFocus` / `whileInView` | Supported |
44
- | Drag (`drag`, constraints, momentum, controls, callbacks) | Supported |
45
- | `AnimatePresence` (`initial`, `mode`, `onExitComplete`) | Supported |
46
- | Layout (`layout`, `layout="position"`) | Supported (single-element FLIP) |
47
- | Shared layout (`layoutId`, `LayoutGroup`, `layoutScroll`) | Supported |
48
- | Pan gesture API (`whilePan`, `onPan*`) | Not yet supported |
49
- | `MotionConfig` parity beyond `transition` | Partial |
50
- | `reducedMotion`, `features`, `transformPagePoint` | Not yet supported |
39
+ | Capability | Status |
40
+ | ---------------------------------------------------------------------- | ------------------------------------------ |
41
+ | `initial` / `animate` / `transition` | Supported |
42
+ | `variants` (string keys + inheritance, function-form `custom`) | Supported |
43
+ | `whileHover` / `whileTap` / `whileFocus` / `whileDrag` / `whileInView` | Supported (inline + variant keys / arrays) |
44
+ | Drag (`drag`, constraints, momentum, controls, callbacks) | Supported |
45
+ | `AnimatePresence` (`initial`, `mode`, `onExitComplete`) | Supported |
46
+ | Layout (`layout`, `layout="position"`) | Supported (single-element FLIP) |
47
+ | Shared layout (`layoutId`, `LayoutGroup`, `layoutScroll`) | Supported |
48
+ | Pan gesture API (`whilePan`, `onPan*`) | Not yet supported |
49
+ | `MotionConfig` parity beyond `transition` | Partial |
50
+ | `reducedMotion`, `features`, `transformPagePoint` | Not yet supported |
51
51
 
52
52
  ## Supported elements
53
53
 
@@ -48,7 +48,7 @@
48
48
  } from '../utils/presence'
49
49
  import { getInitialKeyframes } from '../utils/initial'
50
50
  import { attachDrag } from '../utils/drag'
51
- import { resolveInitial, resolveAnimate, resolveExit } from '../utils/variants'
51
+ import { resolveInitial, resolveAnimate, resolveExit, resolveWhile } from '../utils/variants'
52
52
  import {
53
53
  setVariantContext,
54
54
  getVariantContext,
@@ -446,6 +446,19 @@
446
446
  )
447
447
  const resolvedExit = $derived(resolveExit(exitProp, variantsProp, effectiveCustom))
448
448
 
449
+ // Resolve `whileX` props against `variants` so each gesture's attach
450
+ // helper receives a plain keyframes object regardless of whether the
451
+ // consumer wrote inline keyframes, a variant key, or an array of
452
+ // variant keys. Mirrors framer-motion's `whileHover` etc. surface
453
+ // (#349).
454
+ const resolvedWhileTap = $derived(resolveWhile(whileTapProp, variantsProp, effectiveCustom))
455
+ const resolvedWhileHover = $derived(resolveWhile(whileHoverProp, variantsProp, effectiveCustom))
456
+ const resolvedWhileFocus = $derived(resolveWhile(whileFocusProp, variantsProp, effectiveCustom))
457
+ const resolvedWhileDrag = $derived(resolveWhile(whileDragProp, variantsProp, effectiveCustom))
458
+ const resolvedWhileInView = $derived(
459
+ resolveWhile(whileInViewProp, variantsProp, effectiveCustom)
460
+ )
461
+
449
462
  // Extract keyframes from resolved initial, handling initial={false}
450
463
  const initialKeyframes = $derived(
451
464
  filterReducedMotionKeyframes(
@@ -457,7 +470,12 @@
457
470
  // Derived attributes to keep both branches in sync (focusability, data flags, style, class)
458
471
  const derivedAttrs = $derived<Record<string, unknown>>({
459
472
  ...(rest as Record<string, unknown>),
460
- ...(whileTapProp &&
473
+ // Gate on the *resolved* whileTap, not the raw prop. With
474
+ // variant-label support a truthy-but-unresolved value (unknown
475
+ // key, empty array) would otherwise add `tabindex=0` for an
476
+ // element that never actually receives a tap gesture — an
477
+ // unintended tab stop. (#349 CR feedback)
478
+ ...(isNotEmpty(resolvedWhileTap) &&
461
479
  !isNativelyFocusable(tag, rest as Record<string, unknown>) &&
462
480
  ((rest as Record<string, unknown>)?.tabindex ??
463
481
  (rest as Record<string, unknown>)?.tabIndex ??
@@ -541,7 +559,7 @@
541
559
  directionLock: !!dragDirectionLockProp,
542
560
  listener: dragListenerProp !== false,
543
561
  controls,
544
- whileDrag: whileDragProp as MotionWhileDrag,
562
+ whileDrag: resolvedWhileDrag as MotionWhileDrag,
545
563
  mergedTransition: (mergedTransition ?? {}) as AnimationOptions,
546
564
  callbacks: {
547
565
  onStart: onDragStartProp as (e: PointerEvent, info: DragInfo) => void,
@@ -801,18 +819,18 @@
801
819
 
802
820
  // whileTap handling via motion-dom's press()
803
821
  $effect(() => {
804
- if (!(element && isLoaded === 'ready' && isNotEmpty(whileTapProp))) return
822
+ if (!(element && isLoaded === 'ready' && isNotEmpty(resolvedWhileTap))) return
805
823
  return attachWhileTap(
806
824
  element!,
807
- (whileTapProp ?? {}) as Record<string, unknown>,
825
+ (resolvedWhileTap ?? {}) as Record<string, unknown>,
808
826
  (resolvedInitial ?? {}) as Record<string, unknown>,
809
827
  (resolvedAnimate ?? {}) as Record<string, unknown>,
810
828
  {
811
829
  onTapStart: onTapStartProp,
812
830
  onTap: onTapProp,
813
831
  onTapCancel: onTapCancelProp,
814
- hoverDef: isNotEmpty(whileHoverProp ?? {})
815
- ? ((whileHoverProp ?? {}) as Record<string, unknown>)
832
+ hoverDef: isNotEmpty(resolvedWhileHover ?? {})
833
+ ? ((resolvedWhileHover ?? {}) as Record<string, unknown>)
816
834
  : undefined,
817
835
  hoverFallbackTransition: (mergedTransition ?? {}) as AnimationOptions
818
836
  }
@@ -821,10 +839,10 @@
821
839
 
822
840
  // whileHover handling, gated to true-hover devices to avoid sticky states on touch
823
841
  $effect(() => {
824
- if (!(element && isLoaded === 'ready' && isNotEmpty(whileHoverProp))) return
842
+ if (!(element && isLoaded === 'ready' && isNotEmpty(resolvedWhileHover))) return
825
843
  return attachWhileHover(
826
844
  element!,
827
- (whileHoverProp ?? {}) as Record<string, unknown>,
845
+ (resolvedWhileHover ?? {}) as Record<string, unknown>,
828
846
  (mergedTransition ?? {}) as AnimationOptions,
829
847
  { onStart: onHoverStartProp, onEnd: onHoverEndProp },
830
848
  {
@@ -836,10 +854,10 @@
836
854
 
837
855
  // whileFocus handling for keyboard focus interactions
838
856
  $effect(() => {
839
- if (!(element && isLoaded === 'ready' && isNotEmpty(whileFocusProp))) return
857
+ if (!(element && isLoaded === 'ready' && isNotEmpty(resolvedWhileFocus))) return
840
858
  return attachWhileFocus(
841
859
  element!,
842
- (whileFocusProp ?? {}) as Record<string, unknown>,
860
+ (resolvedWhileFocus ?? {}) as Record<string, unknown>,
843
861
  (mergedTransition ?? {}) as AnimationOptions,
844
862
  { onStart: onFocusStartProp, onEnd: onFocusEndProp },
845
863
  {
@@ -851,10 +869,10 @@
851
869
 
852
870
  // whileInView handling for viewport intersection
853
871
  $effect(() => {
854
- if (!(element && isLoaded === 'ready' && isNotEmpty(whileInViewProp))) return
872
+ if (!(element && isLoaded === 'ready' && isNotEmpty(resolvedWhileInView))) return
855
873
  return attachWhileInView(
856
874
  element!,
857
- (whileInViewProp ?? {}) as Record<string, unknown>,
875
+ (resolvedWhileInView ?? {}) as Record<string, unknown>,
858
876
  (mergedTransition ?? {}) as AnimationOptions,
859
877
  {
860
878
  onStart: onInViewStartProp,
package/dist/types.d.ts CHANGED
@@ -59,7 +59,7 @@ export type Variants = Record<string, Variant>;
59
59
  * <motion.div variants={myVariants} initial="hidden" animate="visible" />
60
60
  * ```
61
61
  */
62
- export type MotionInitial = DOMKeyframesDefinition | string | false | undefined;
62
+ export type MotionInitial = DOMKeyframesDefinition | string | string[] | false | undefined;
63
63
  /**
64
64
  * Target animation properties for a motion component.
65
65
  *
@@ -74,7 +74,7 @@ export type MotionInitial = DOMKeyframesDefinition | string | false | undefined;
74
74
  * <motion.div variants={myVariants} animate="visible" />
75
75
  * ```
76
76
  */
77
- export type MotionAnimate = DOMKeyframesDefinition | string | undefined;
77
+ export type MotionAnimate = DOMKeyframesDefinition | string | string[] | undefined;
78
78
  /**
79
79
  * Exit animation properties for a motion component when unmounted.
80
80
  *
@@ -91,7 +91,7 @@ export type MotionAnimate = DOMKeyframesDefinition | string | undefined;
91
91
  */
92
92
  export type MotionExit = (Record<string, unknown> & {
93
93
  transition?: AnimationOptions;
94
- }) | DOMKeyframesDefinition | string | undefined;
94
+ }) | DOMKeyframesDefinition | string | string[] | undefined;
95
95
  /**
96
96
  * Animation transition configuration.
97
97
  * @example
@@ -111,52 +111,80 @@ export type MotionExit = (Record<string, unknown> & {
111
111
  export type MotionTransition = AnimationOptions | undefined;
112
112
  /**
113
113
  * Animation properties for tap/click interactions.
114
+ *
115
+ * Accepts inline keyframes, a variant key, or an array of variant keys
116
+ * (later entries override earlier ones on key collisions).
117
+ *
114
118
  * @example
115
119
  * ```svelte
116
120
  * <motion.button whileTap={{ scale: 0.95 }} />
121
+ *
122
+ * <!-- Variant key -->
123
+ * <motion.button variants={{ pressed: { scale: 0.95 } }} whileTap="pressed" />
124
+ *
125
+ * <!-- Array — later wins on conflicts -->
126
+ * <motion.button whileTap={["pressed", "muted"]} />
117
127
  * ```
118
128
  */
119
- export type MotionWhileTap = DOMKeyframesDefinition | undefined;
129
+ export type MotionWhileTap = (Record<string, unknown> & {
130
+ transition?: AnimationOptions;
131
+ }) | DOMKeyframesDefinition | string | string[] | undefined;
120
132
  /**
121
133
  * Animation properties for hover interactions.
134
+ *
135
+ * Accepts inline keyframes, a variant key, or an array of variant keys.
136
+ *
122
137
  * @example
123
138
  * ```svelte
124
139
  * <motion.div whileHover={{ scale: 1.05 }} />
140
+ *
141
+ * <!-- Variant key -->
142
+ * <motion.div variants={{ hover: { scale: 1.05 } }} whileHover="hover" />
125
143
  * ```
126
144
  */
127
145
  export type MotionWhileHover = (Record<string, unknown> & {
128
146
  transition?: AnimationOptions;
129
- }) | DOMKeyframesDefinition | undefined;
147
+ }) | DOMKeyframesDefinition | string | string[] | undefined;
130
148
  /**
131
149
  * Animation properties for focus interactions.
150
+ *
151
+ * Accepts inline keyframes, a variant key, or an array of variant keys.
152
+ *
132
153
  * @example
133
154
  * ```svelte
134
155
  * <motion.button whileFocus={{ scale: 1.05 }} />
156
+ * <motion.button variants={{ active: { outline: '2px solid blue' } }} whileFocus="active" />
135
157
  * ```
136
158
  */
137
159
  export type MotionWhileFocus = (Record<string, unknown> & {
138
160
  transition?: AnimationOptions;
139
- }) | DOMKeyframesDefinition | undefined;
161
+ }) | DOMKeyframesDefinition | string | string[] | undefined;
140
162
  /**
141
163
  * Animation properties for drag interactions.
142
164
  * When a drag gesture starts, the element animates to this state; when it ends,
143
165
  * it animates back to its baseline (from animate/initial), restoring only the changed keys.
166
+ *
167
+ * Accepts inline keyframes, a variant key, or an array of variant keys.
144
168
  */
145
169
  export type MotionWhileDrag = (Record<string, unknown> & {
146
170
  transition?: AnimationOptions;
147
- }) | DOMKeyframesDefinition | undefined;
171
+ }) | DOMKeyframesDefinition | string | string[] | undefined;
148
172
  /**
149
173
  * Animation properties for in-view interactions.
150
174
  * When the element enters the viewport, it animates to this state; when it leaves,
151
175
  * it animates back to its baseline (from animate/initial), restoring only the changed keys.
176
+ *
177
+ * Accepts inline keyframes, a variant key, or an array of variant keys.
178
+ *
152
179
  * @example
153
180
  * ```svelte
154
181
  * <motion.div whileInView={{ opacity: 1, y: 0 }} />
182
+ * <motion.div variants={{ inView: { opacity: 1 } }} whileInView="inView" />
155
183
  * ```
156
184
  */
157
185
  export type MotionWhileInView = (Record<string, unknown> & {
158
186
  transition?: AnimationOptions;
159
- }) | DOMKeyframesDefinition | undefined;
187
+ }) | DOMKeyframesDefinition | string | string[] | undefined;
160
188
  /**
161
189
  * IntersectionObserver configuration for `whileInView`. Mirrors framer-motion's
162
190
  * `viewport` prop. Same shape as `UseInViewOptions` minus `initial` (which is
@@ -1,4 +1,4 @@
1
- import type { MotionAnimate, MotionExit, MotionInitial, Variants } from '../types';
1
+ import type { MotionAnimate, MotionExit, MotionInitial, MotionWhileDrag, MotionWhileFocus, MotionWhileHover, MotionWhileInView, MotionWhileTap, Variants } from '../types';
2
2
  import type { DOMKeyframesDefinition } from 'motion';
3
3
  /**
4
4
  * Resolves a variant key to its keyframes definition.
@@ -27,6 +27,33 @@ import type { DOMKeyframesDefinition } from 'motion';
27
27
  * ```
28
28
  */
29
29
  export declare const resolveVariant: (variants: Variants | undefined, key: string | undefined, custom?: unknown) => DOMKeyframesDefinition | undefined;
30
+ /**
31
+ * Resolves a single variant key or an ordered list of keys to merged
32
+ * keyframes. Matches framer-motion's `VariantLabels = string | string[]`
33
+ * surface: later keys in the list override earlier ones on key
34
+ * collisions (`Object.assign` semantics).
35
+ *
36
+ * Missing keys are skipped. An empty list, an empty string, or an
37
+ * undefined argument all resolve to `undefined`.
38
+ *
39
+ * @param variants - The variants object containing named animation states.
40
+ * @param keys - A single variant key or an array of variant keys.
41
+ * @param custom - Forwarded to function-form variants (per-entry).
42
+ * @returns Merged keyframes definition, or `undefined` when nothing resolved.
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * const variants = {
47
+ * hover: { scale: 1.1 },
48
+ * active: { scale: 1.2, color: 'red' }
49
+ * }
50
+ * resolveVariantList(variants, 'hover') // { scale: 1.1 }
51
+ * resolveVariantList(variants, ['hover', 'active']) // { scale: 1.2, color: 'red' }
52
+ * resolveVariantList(variants, ['hover', 'missing']) // { scale: 1.1 }
53
+ * resolveVariantList(variants, []) // undefined
54
+ * ```
55
+ */
56
+ export declare const resolveVariantList: (variants: Variants | undefined, keys: string | string[] | undefined, custom?: unknown) => DOMKeyframesDefinition | undefined;
30
57
  /**
31
58
  * Resolves the initial prop to keyframes, handling variant keys and `initial={false}`.
32
59
  *
@@ -52,25 +79,72 @@ export declare const resolveInitial: (initial: MotionInitial, variants: Variants
52
79
  /**
53
80
  * Resolves the animate prop to keyframes, handling variant keys.
54
81
  *
55
- * When `animate` is a string, looks it up in the variants object (invoking
56
- * dynamic variants with `custom`). Otherwise returns the keyframes directly.
82
+ * When `animate` is a string (or array of strings), looks it up in the
83
+ * variants object (invoking dynamic variants with `custom`). Otherwise
84
+ * returns the keyframes directly.
57
85
  *
58
- * @param animate - The animate prop value (keyframes, variant key, or undefined).
86
+ * @param animate - The animate prop value (keyframes, variant key, array
87
+ * of variant keys, or undefined).
59
88
  * @param variants - The variants object for resolving string keys.
60
89
  * @param custom - Forwarded to function-form variants.
61
90
  * @returns Keyframes definition or undefined.
91
+ *
92
+ * @example
93
+ * ```ts
94
+ * const variants = { visible: { opacity: 1 }, shifted: { x: 100 } }
95
+ * resolveAnimate('visible', variants) // { opacity: 1 }
96
+ * resolveAnimate(['visible', 'shifted'], variants) // { opacity: 1, x: 100 }
97
+ * resolveAnimate({ scale: 1.2 }, variants) // { scale: 1.2 } (pass-through)
98
+ * resolveAnimate(undefined, variants) // undefined
99
+ * ```
62
100
  */
63
101
  export declare const resolveAnimate: (animate: MotionAnimate, variants: Variants | undefined, custom?: unknown) => DOMKeyframesDefinition | undefined;
64
102
  /**
65
103
  * Resolves the exit prop to keyframes, handling variant keys.
66
104
  *
67
- * When `exit` is a string, looks it up in the variants object (invoking
68
- * dynamic variants with `custom`). Otherwise returns the keyframes directly.
69
- * Used by AnimatePresence for exit animations.
105
+ * When `exit` is a string (or array of strings), looks it up in the
106
+ * variants object (invoking dynamic variants with `custom`). Otherwise
107
+ * returns the keyframes directly. Used by AnimatePresence for exit
108
+ * animations.
70
109
  *
71
- * @param exit - The exit prop value (keyframes, variant key, or undefined).
110
+ * @param exit - The exit prop value (keyframes, variant key, array of
111
+ * variant keys, or undefined).
72
112
  * @param variants - The variants object for resolving string keys.
73
113
  * @param custom - Forwarded to function-form variants.
74
114
  * @returns Keyframes definition or undefined.
115
+ *
116
+ * @example
117
+ * ```ts
118
+ * const variants = { hidden: { opacity: 0 }, small: { scale: 0.8 } }
119
+ * resolveExit('hidden', variants) // { opacity: 0 }
120
+ * resolveExit(['hidden', 'small'], variants) // { opacity: 0, scale: 0.8 }
121
+ * resolveExit({ y: -20 }, variants) // { y: -20 }
122
+ * resolveExit(undefined, variants) // undefined
123
+ * ```
75
124
  */
76
125
  export declare const resolveExit: (exit: MotionExit, variants: Variants | undefined, custom?: unknown) => DOMKeyframesDefinition | undefined;
126
+ /**
127
+ * Resolves a `whileX` prop (hover, tap, focus, drag, in-view) to
128
+ * keyframes. Mirrors `resolveAnimate` — pass-through for inline
129
+ * keyframes, look up variant keys via `resolveVariantList` (single
130
+ * string or array of strings, merged left-to-right).
131
+ *
132
+ * Used by `_MotionContainer.svelte` to feed the gesture attach helpers
133
+ * a consistent keyframes object regardless of whether the consumer
134
+ * wrote inline keyframes or a variant reference.
135
+ *
136
+ * @param value - The whileX prop value.
137
+ * @param variants - The variants object for resolving string keys.
138
+ * @param custom - Forwarded to function-form variants.
139
+ * @returns Keyframes definition or `undefined` when nothing applies.
140
+ *
141
+ * @example
142
+ * ```ts
143
+ * const variants = { hover: { scale: 1.1 }, active: { color: 'red' } }
144
+ * resolveWhile('hover', variants) // { scale: 1.1 }
145
+ * resolveWhile(['hover', 'active'], variants) // { scale: 1.1, color: 'red' }
146
+ * resolveWhile({ scale: 1.2 }, variants) // { scale: 1.2 } (pass-through)
147
+ * resolveWhile(undefined, variants) // undefined
148
+ * ```
149
+ */
150
+ export declare const resolveWhile: (value: MotionWhileTap | MotionWhileHover | MotionWhileFocus | MotionWhileDrag | MotionWhileInView, variants: Variants | undefined, custom?: unknown) => DOMKeyframesDefinition | undefined;
@@ -27,11 +27,69 @@
27
27
  export const resolveVariant = (variants, key, custom) => {
28
28
  if (!variants || !key)
29
29
  return undefined;
30
+ // Guard against built-in / inherited keys like 'toString' or
31
+ // 'constructor' — without this, `whileHover="toString"` would
32
+ // resolve to `Function.prototype.toString` and leak a function into
33
+ // the merge path.
34
+ if (!Object.prototype.hasOwnProperty.call(variants, key))
35
+ return undefined;
30
36
  const entry = variants[key];
31
37
  if (typeof entry === 'function')
32
38
  return entry(custom);
33
39
  return entry;
34
40
  };
41
+ /**
42
+ * Resolves a single variant key or an ordered list of keys to merged
43
+ * keyframes. Matches framer-motion's `VariantLabels = string | string[]`
44
+ * surface: later keys in the list override earlier ones on key
45
+ * collisions (`Object.assign` semantics).
46
+ *
47
+ * Missing keys are skipped. An empty list, an empty string, or an
48
+ * undefined argument all resolve to `undefined`.
49
+ *
50
+ * @param variants - The variants object containing named animation states.
51
+ * @param keys - A single variant key or an array of variant keys.
52
+ * @param custom - Forwarded to function-form variants (per-entry).
53
+ * @returns Merged keyframes definition, or `undefined` when nothing resolved.
54
+ *
55
+ * @example
56
+ * ```ts
57
+ * const variants = {
58
+ * hover: { scale: 1.1 },
59
+ * active: { scale: 1.2, color: 'red' }
60
+ * }
61
+ * resolveVariantList(variants, 'hover') // { scale: 1.1 }
62
+ * resolveVariantList(variants, ['hover', 'active']) // { scale: 1.2, color: 'red' }
63
+ * resolveVariantList(variants, ['hover', 'missing']) // { scale: 1.1 }
64
+ * resolveVariantList(variants, []) // undefined
65
+ * ```
66
+ */
67
+ export const resolveVariantList = (variants, keys, custom) => {
68
+ if (keys === undefined)
69
+ return undefined;
70
+ if (typeof keys === 'string')
71
+ return resolveVariant(variants, keys, custom);
72
+ if (keys.length === 0)
73
+ return undefined;
74
+ let merged;
75
+ for (const key of keys) {
76
+ const entry = resolveVariant(variants, key, custom);
77
+ // Defensive: only merge plain keyframe objects. A function-form
78
+ // variant could return something else (array, class instance,
79
+ // string) under a misuse, and spreading those would corrupt the
80
+ // merged result. Reject arrays explicitly and require the
81
+ // prototype to be `Object.prototype` (or `null` for objects
82
+ // created via `Object.create(null)`).
83
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry))
84
+ continue;
85
+ const proto = Object.getPrototypeOf(entry);
86
+ if (proto !== Object.prototype && proto !== null)
87
+ continue;
88
+ const obj = entry;
89
+ merged = merged ? { ...merged, ...obj } : { ...obj };
90
+ }
91
+ return merged;
92
+ };
35
93
  /**
36
94
  * Resolves the initial prop to keyframes, handling variant keys and `initial={false}`.
37
95
  *
@@ -58,44 +116,97 @@ export const resolveInitial = (initial, variants, custom) => {
58
116
  return false;
59
117
  if (initial === undefined)
60
118
  return undefined;
61
- if (typeof initial === 'string')
62
- return resolveVariant(variants, initial, custom);
119
+ if (typeof initial === 'string' || Array.isArray(initial))
120
+ return resolveVariantList(variants, initial, custom);
63
121
  return initial;
64
122
  };
65
123
  /**
66
124
  * Resolves the animate prop to keyframes, handling variant keys.
67
125
  *
68
- * When `animate` is a string, looks it up in the variants object (invoking
69
- * dynamic variants with `custom`). Otherwise returns the keyframes directly.
126
+ * When `animate` is a string (or array of strings), looks it up in the
127
+ * variants object (invoking dynamic variants with `custom`). Otherwise
128
+ * returns the keyframes directly.
70
129
  *
71
- * @param animate - The animate prop value (keyframes, variant key, or undefined).
130
+ * @param animate - The animate prop value (keyframes, variant key, array
131
+ * of variant keys, or undefined).
72
132
  * @param variants - The variants object for resolving string keys.
73
133
  * @param custom - Forwarded to function-form variants.
74
134
  * @returns Keyframes definition or undefined.
135
+ *
136
+ * @example
137
+ * ```ts
138
+ * const variants = { visible: { opacity: 1 }, shifted: { x: 100 } }
139
+ * resolveAnimate('visible', variants) // { opacity: 1 }
140
+ * resolveAnimate(['visible', 'shifted'], variants) // { opacity: 1, x: 100 }
141
+ * resolveAnimate({ scale: 1.2 }, variants) // { scale: 1.2 } (pass-through)
142
+ * resolveAnimate(undefined, variants) // undefined
143
+ * ```
75
144
  */
76
145
  export const resolveAnimate = (animate, variants, custom) => {
77
146
  if (animate === undefined)
78
147
  return undefined;
79
- if (typeof animate === 'string')
80
- return resolveVariant(variants, animate, custom);
148
+ if (typeof animate === 'string' || Array.isArray(animate))
149
+ return resolveVariantList(variants, animate, custom);
81
150
  return animate;
82
151
  };
83
152
  /**
84
153
  * Resolves the exit prop to keyframes, handling variant keys.
85
154
  *
86
- * When `exit` is a string, looks it up in the variants object (invoking
87
- * dynamic variants with `custom`). Otherwise returns the keyframes directly.
88
- * Used by AnimatePresence for exit animations.
155
+ * When `exit` is a string (or array of strings), looks it up in the
156
+ * variants object (invoking dynamic variants with `custom`). Otherwise
157
+ * returns the keyframes directly. Used by AnimatePresence for exit
158
+ * animations.
89
159
  *
90
- * @param exit - The exit prop value (keyframes, variant key, or undefined).
160
+ * @param exit - The exit prop value (keyframes, variant key, array of
161
+ * variant keys, or undefined).
91
162
  * @param variants - The variants object for resolving string keys.
92
163
  * @param custom - Forwarded to function-form variants.
93
164
  * @returns Keyframes definition or undefined.
165
+ *
166
+ * @example
167
+ * ```ts
168
+ * const variants = { hidden: { opacity: 0 }, small: { scale: 0.8 } }
169
+ * resolveExit('hidden', variants) // { opacity: 0 }
170
+ * resolveExit(['hidden', 'small'], variants) // { opacity: 0, scale: 0.8 }
171
+ * resolveExit({ y: -20 }, variants) // { y: -20 }
172
+ * resolveExit(undefined, variants) // undefined
173
+ * ```
94
174
  */
95
175
  export const resolveExit = (exit, variants, custom) => {
96
176
  if (exit === undefined)
97
177
  return undefined;
98
- if (typeof exit === 'string')
99
- return resolveVariant(variants, exit, custom);
178
+ if (typeof exit === 'string' || Array.isArray(exit))
179
+ return resolveVariantList(variants, exit, custom);
100
180
  return exit;
101
181
  };
182
+ /**
183
+ * Resolves a `whileX` prop (hover, tap, focus, drag, in-view) to
184
+ * keyframes. Mirrors `resolveAnimate` — pass-through for inline
185
+ * keyframes, look up variant keys via `resolveVariantList` (single
186
+ * string or array of strings, merged left-to-right).
187
+ *
188
+ * Used by `_MotionContainer.svelte` to feed the gesture attach helpers
189
+ * a consistent keyframes object regardless of whether the consumer
190
+ * wrote inline keyframes or a variant reference.
191
+ *
192
+ * @param value - The whileX prop value.
193
+ * @param variants - The variants object for resolving string keys.
194
+ * @param custom - Forwarded to function-form variants.
195
+ * @returns Keyframes definition or `undefined` when nothing applies.
196
+ *
197
+ * @example
198
+ * ```ts
199
+ * const variants = { hover: { scale: 1.1 }, active: { color: 'red' } }
200
+ * resolveWhile('hover', variants) // { scale: 1.1 }
201
+ * resolveWhile(['hover', 'active'], variants) // { scale: 1.1, color: 'red' }
202
+ * resolveWhile({ scale: 1.2 }, variants) // { scale: 1.2 } (pass-through)
203
+ * resolveWhile(undefined, variants) // undefined
204
+ * ```
205
+ */
206
+ export const resolveWhile = (value, variants, custom) => {
207
+ if (value === undefined)
208
+ return undefined;
209
+ if (typeof value === 'string' || Array.isArray(value))
210
+ return resolveVariantList(variants, value, custom);
211
+ return value;
212
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@humanspeak/svelte-motion",
3
- "version": "0.4.6",
3
+ "version": "0.4.7",
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",