@humanspeak/svelte-motion 0.1.14 → 0.1.16

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
@@ -117,7 +117,7 @@ This package carefully selects its dependencies to provide a robust and maintain
117
117
  | [Fancy Like Button](https://github.com/DRlFTER/fancyLikeButton) | `/tests/random/fancy-like-button` | [View Example](https://svelte.dev/playground/c34b7e53d41c48b0ab1eaf21ca120c6e?version=5.38.10) |
118
118
  | [Keyframes (square → circle → square; scale 1→2→1)](https://motion.dev/docs/react-animation#keyframes) | `/tests/motion/keyframes` | [View Example](https://svelte.dev/playground/05595ce0db124c1cbbe4e74fda68d717?version=5.38.10) |
119
119
  | [Animated Border Gradient (conic-gradient rotate)](https://www.youtube.com/watch?v=OgQI1-9T6ZA) | `/tests/random/animated-border-gradient` | [View Example](https://svelte.dev/playground/6983a61b4c35441b8aa72a971de01a23?version=5.38.10) |
120
- | [Exit Animation](https://motion.dev/docs/react#exit-animations) | `/tests/motion/animate-presence` | [View Example](https://svelte.dev/playground/ef277e283d864653ace54e7453801601?version=5.38.10) |
120
+ | [Exit Animation](https://motion.dev/docs/react#exit-animations) | `/tests/animate-presence/basic` | [View Example](https://svelte.dev/playground/ef277e283d864653ace54e7453801601?version=5.38.10) |
121
121
 
122
122
  ## Interactions
123
123
 
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte'
3
3
  import { createAnimatePresenceContext, setAnimatePresenceContext } from '../utils/presence'
4
+ import { pwLog } from '../utils/log'
4
5
 
5
6
  /**
6
7
  * Provide `AnimatePresence` context to descendants.
@@ -10,14 +11,21 @@
10
11
  * styled clone is animated out before being removed from the DOM.
11
12
  *
12
13
  * @prop children Slotted content participating in presence.
14
+ * @prop initial When false, children skip their enter animation on initial mount.
13
15
  * @prop onExitComplete Optional callback invoked once all exits complete.
14
16
  */
15
- const { children, onExitComplete } = $props<{
17
+ const {
18
+ children,
19
+ initial = true,
20
+ onExitComplete
21
+ } = $props<{
16
22
  children?: Snippet
23
+ initial?: boolean
17
24
  onExitComplete?: () => void
18
25
  }>()
19
26
 
20
- const context = createAnimatePresenceContext({ onExitComplete })
27
+ pwLog('[AnimatePresence] mounting', { initial, hasOnExitComplete: !!onExitComplete })
28
+ const context = createAnimatePresenceContext({ initial, onExitComplete })
21
29
  setAnimatePresenceContext(context)
22
30
  </script>
23
31
 
@@ -1,6 +1,7 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  type $$ComponentProps = {
3
3
  children?: Snippet;
4
+ initial?: boolean;
4
5
  onExitComplete?: () => void;
5
6
  };
6
7
  declare const AnimatePresence: import("svelte").Component<$$ComponentProps, {}, "">;
@@ -1,3 +1,8 @@
1
+ <script lang="ts" module>
2
+ // Module-level counter for deterministic key generation (avoids SSR hydration mismatch)
3
+ let keyCounter = 0
4
+ </script>
5
+
1
6
  <script lang="ts">
2
7
  import { getMotionConfig } from '../components/motionConfig.context'
3
8
  import type {
@@ -20,6 +25,7 @@
20
25
  import { attachWhileTap } from '../utils/interaction'
21
26
  import { attachWhileHover } from '../utils/hover'
22
27
  import { attachWhileFocus } from '../utils/focus'
28
+ import { attachWhileInView } from '../utils/inView'
23
29
  import {
24
30
  measureRect,
25
31
  computeFlipTransforms,
@@ -52,6 +58,7 @@
52
58
  let {
53
59
  children,
54
60
  tag = 'div',
61
+ key: keyProp,
55
62
  variants: variantsProp,
56
63
  initial: initialProp,
57
64
  animate: animateProp,
@@ -64,11 +71,14 @@
64
71
  whileTap: whileTapProp,
65
72
  whileHover: whileHoverProp,
66
73
  whileFocus: whileFocusProp,
74
+ whileInView: whileInViewProp,
67
75
  whileDrag: whileDragProp,
68
76
  onHoverStart: onHoverStartProp,
69
77
  onHoverEnd: onHoverEndProp,
70
78
  onFocusStart: onFocusStartProp,
71
79
  onFocusEnd: onFocusEndProp,
80
+ onInViewStart: onInViewStartProp,
81
+ onInViewEnd: onInViewEndProp,
72
82
  onTapStart: onTapStartProp,
73
83
  onTap: onTapProp,
74
84
  onTapCancel: onTapCancelProp,
@@ -95,8 +105,20 @@
95
105
  let dataPath = $state<number>(-1)
96
106
  const motionConfig = $derived(getMotionConfig())
97
107
 
98
- // Generate unique key for presence tracking
99
- const presenceKey = `motion-${Math.random().toString(36).slice(2)}`
108
+ // Get presence context to check if we're inside AnimatePresence
109
+ const context = getAnimatePresenceContext()
110
+
111
+ // Validate key prop when inside AnimatePresence
112
+ if (context && !keyProp) {
113
+ throw new Error(
114
+ 'motion elements inside AnimatePresence must have a `key` prop. ' +
115
+ 'Example: <motion.div key="unique-id" />'
116
+ )
117
+ }
118
+
119
+ // Use the provided key for presence tracking
120
+ // When not inside AnimatePresence, use a stable identifier based on component instance
121
+ const presenceKey = keyProp ?? `motion-${++keyCounter}`
100
122
 
101
123
  // Compute merged transition without mutating props to avoid effect write loops
102
124
  const mergedTransition = $derived<AnimationOptions>(
@@ -118,12 +140,10 @@
118
140
  }
119
141
  })
120
142
 
121
- const context = getAnimatePresenceContext()
122
143
  // Update presence context with current state when element is ready and has size
123
144
  $effect(() => {
124
145
  if (!(context && element && isLoaded === 'ready')) return
125
146
 
126
- let rafId: number | null = null
127
147
  let lastWidth = 0
128
148
  let lastHeight = 0
129
149
  let stopped = false
@@ -132,11 +152,6 @@
132
152
  if (stopped || !element || !element.isConnected) return
133
153
  const rect = element.getBoundingClientRect()
134
154
  const style = getComputedStyle(element)
135
- pwLog('[motion][measure]', {
136
- w: rect.width,
137
- h: rect.height,
138
- transform: style.transform
139
- })
140
155
  if (
141
156
  Math.abs(rect.width - lastWidth) > 0.5 ||
142
157
  Math.abs(rect.height - lastHeight) > 0.5
@@ -149,6 +164,7 @@
149
164
 
150
165
  // Observe size changes
151
166
  const resizeObserver = new ResizeObserver(() => {
167
+ pwLog('[motion][resize]', { key: presenceKey })
152
168
  measureAndUpdate()
153
169
  })
154
170
  try {
@@ -157,15 +173,8 @@
157
173
  // Ignore
158
174
  }
159
175
 
160
- // Also poll on RAF to catch transform/layout-driven changes
161
- const tick = () => {
162
- if (stopped) return
163
- measureAndUpdate()
164
- rafId = requestAnimationFrame(tick)
165
- }
166
- rafId = requestAnimationFrame(tick)
167
-
168
176
  // Initial measure once
177
+ pwLog('[motion][initial-measure]', { key: presenceKey })
169
178
  measureAndUpdate()
170
179
 
171
180
  return () => {
@@ -175,7 +184,6 @@
175
184
  } catch {
176
185
  // Ignore
177
186
  }
178
- if (rafId) cancelAnimationFrame(rafId)
179
187
  }
180
188
  })
181
189
 
@@ -217,13 +225,23 @@
217
225
  )
218
226
 
219
227
  // Propagate initial={false} to children BEFORE setting variant context
228
+ // AnimatePresence initial={false} only applies on first render - check shouldAnimateEnter(key)
220
229
  const parentInitialFalse = getInitialFalseContext()
221
- const effectiveInitialProp =
222
- initialProp !== undefined
223
- ? initialProp
224
- : parentInitialFalse && variantsProp
225
- ? false
226
- : undefined
230
+ const presenceSkipEnter = context ? !context.shouldAnimateEnter(presenceKey) : false
231
+ const effectiveInitialProp = presenceSkipEnter
232
+ ? false
233
+ : initialProp !== undefined
234
+ ? initialProp
235
+ : parentInitialFalse && variantsProp
236
+ ? false
237
+ : undefined
238
+
239
+ pwLog('[motion] mount', {
240
+ presenceSkipEnter,
241
+ effectiveInitialProp,
242
+ initialProp,
243
+ animateProp
244
+ })
227
245
 
228
246
  if (initialProp === false) {
229
247
  setInitialFalseContext(true)
@@ -279,8 +297,20 @@
279
297
  initialKeyframes && 'pathLength' in initialKeyframes && isLoaded === 'mounting'
280
298
  ? `${styleProp || ''};visibility:hidden`
281
299
  : styleProp,
282
- initialKeyframes as unknown as Record<string, unknown>,
283
- resolvedAnimate as unknown as Record<string, unknown>
300
+ // Apply initialKeyframes as inline styles during mounting and initial phases
301
+ // The animation starts in RAF after 'initial' phase, so we need styles until then
302
+ // When ready AND we have initialKeyframes: DON'T set any animated properties!
303
+ // WAAPI is controlling them and inline styles can override the animation
304
+ isLoaded === 'mounting' || isLoaded === 'initial'
305
+ ? (initialKeyframes as unknown as Record<string, unknown>)
306
+ : undefined,
307
+ // Only use resolvedAnimate as fallback when we DON'T have initialKeyframes
308
+ // If we have initialKeyframes, the enter animation is running - setting
309
+ // inline styles to the target values will override the WAAPI animation
310
+ // Use isNotEmpty to handle empty initial objects (initial: {}) which should fallback
311
+ isNotEmpty(initialKeyframes)
312
+ ? undefined
313
+ : (resolvedAnimate as unknown as Record<string, unknown>)
284
314
  ),
285
315
  class: classProp
286
316
  })
@@ -357,7 +387,16 @@
357
387
  })
358
388
 
359
389
  const runAnimation = () => {
360
- if (!element || !resolvedAnimate) return
390
+ pwLog('[motion] runAnimation called', {
391
+ hasElement: !!element,
392
+ resolvedAnimate,
393
+ mergedTransition
394
+ })
395
+ if (!element || !resolvedAnimate) {
396
+ pwLog('[motion] runAnimation bailing - no element or resolvedAnimate')
397
+ return
398
+ }
399
+
361
400
  const transitionAnimate: MotionTransition = mergedTransition ?? {}
362
401
  let payload = $state.snapshot(resolvedAnimate)
363
402
 
@@ -373,6 +412,11 @@
373
412
  ;(element as HTMLElement).style.removeProperty('stroke-dashoffset')
374
413
  }
375
414
 
415
+ pwLog('[motion] runAnimation animating', {
416
+ payload,
417
+ transitionAnimate
418
+ })
419
+
376
420
  animateWithLifecycle(
377
421
  element,
378
422
  payload as unknown as DOMKeyframesDefinition,
@@ -385,6 +429,12 @@
385
429
  // Track the last variant key we ran to avoid re-running on mount
386
430
  let lastRanVariantKey = $state<string | undefined>(undefined)
387
431
  let mountedWithInitialFalse = $state(false)
432
+ // Track if the initial->animate transition has already been triggered by main effect
433
+ let initialAnimationTriggered = $state(false)
434
+ // Track if we've run the animation for object animateProp on this mount
435
+ let objectAnimateRanOnMount = $state(false)
436
+ // Track the serialized animateProp to detect changes for object animate props
437
+ let lastAnimatePropJson = $state<string | undefined>(undefined)
388
438
  const currentAnimateKey = $derived(
389
439
  typeof animateProp === 'string'
390
440
  ? animateProp
@@ -487,6 +537,25 @@
487
537
  )
488
538
  })
489
539
 
540
+ // whileInView handling for viewport intersection
541
+ $effect(() => {
542
+ if (!(element && isLoaded === 'ready' && isNotEmpty(whileInViewProp))) return
543
+ return attachWhileInView(
544
+ element!,
545
+ (whileInViewProp ?? {}) as Record<string, unknown>,
546
+ (mergedTransition ?? {}) as AnimationOptions,
547
+ {
548
+ onStart: onInViewStartProp,
549
+ onEnd: onInViewEndProp,
550
+ onAnimationComplete: onAnimationCompleteProp
551
+ },
552
+ {
553
+ initial: (resolvedInitial ?? {}) as Record<string, unknown>,
554
+ animate: (resolvedAnimate ?? {}) as Record<string, unknown>
555
+ }
556
+ )
557
+ })
558
+
490
559
  // Re-run animate when animateProp changes while ready
491
560
  $effect(() => {
492
561
  if (!(element && isLoaded === 'ready')) return
@@ -500,15 +569,39 @@
500
569
  // Variant has changed, so we should animate
501
570
  mountedWithInitialFalse = false
502
571
  }
572
+ // Skip if the initial animation was already triggered by the main effect
573
+ if (initialAnimationTriggered) {
574
+ pwLog('[motion] effect: skipping, initial animation already triggered')
575
+ initialAnimationTriggered = false
576
+ // Also mark object animate as ran to prevent duplicate runs from effect re-triggers
577
+ if (animateProp && typeof animateProp !== 'string') {
578
+ objectAnimateRanOnMount = true
579
+ }
580
+ return
581
+ }
503
582
  if (typeof animateProp === 'string') {
504
583
  if (lastRanVariantKey !== animateProp) {
505
584
  lastRanVariantKey = animateProp
506
585
  runAnimation()
507
586
  }
508
587
  } else if (animateProp) {
509
- // Object animate props - always run
510
- lastRanVariantKey = undefined
511
- runAnimation()
588
+ // Object animate props - detect if the prop actually changed
589
+ const currentJson = JSON.stringify(animateProp)
590
+ const propChanged = lastAnimatePropJson !== currentJson
591
+
592
+ // Reset flag if animate prop changed
593
+ if (propChanged) {
594
+ objectAnimateRanOnMount = false
595
+ lastAnimatePropJson = currentJson
596
+ }
597
+
598
+ // Only run if we haven't already animated on this mount (or prop changed)
599
+ // This prevents duplicate animations when Svelte re-triggers the effect
600
+ if (!objectAnimateRanOnMount) {
601
+ objectAnimateRanOnMount = true
602
+ lastRanVariantKey = undefined
603
+ runAnimation()
604
+ }
512
605
  }
513
606
  })
514
607
 
@@ -539,9 +632,18 @@
539
632
  $effect(() => {
540
633
  if (!(element && isLoaded === 'mounting')) return
541
634
 
635
+ pwLog('[motion] main effect running', {
636
+ effectiveAnimate: !!effectiveAnimate,
637
+ effectiveInitialProp,
638
+ resolvedAnimate,
639
+ initialKeyframes,
640
+ hasInitialKeyframes: isNotEmpty(initialKeyframes)
641
+ })
642
+
542
643
  if (effectiveAnimate) {
543
644
  // If initial={false}, render at animate state immediately with no transition
544
645
  if (effectiveInitialProp === false && resolvedAnimate) {
646
+ pwLog('[motion] path: initial=false, skip to animate')
545
647
  // Use Motion's animate() with duration:0 so it takes control of these properties
546
648
  // This prevents inline styles from pinning the properties during future animations
547
649
  let snapshot = $state.snapshot(resolvedAnimate) as Record<string, unknown>
@@ -555,6 +657,7 @@
555
657
  dataPath = 5
556
658
  isLoaded = 'ready'
557
659
  } else if (isNotEmpty(initialKeyframes)) {
660
+ pwLog('[motion] path: has initialKeyframes, will animate to target')
558
661
  // Apply initial instantly BEFORE exposing 'initial' state
559
662
  const transformedInitial = transformSVGPathProperties(
560
663
  element!,
@@ -598,11 +701,29 @@
598
701
  if (isPlaywright) {
599
702
  await sleep(10)
600
703
  }
601
- isLoaded = 'ready'
704
+ pwLog('[motion] RAF: promoting to ready and running animation')
705
+
706
+ // Mark that we're triggering the initial animation to prevent duplicate runs
707
+ initialAnimationTriggered = true
602
708
 
709
+ // IMPORTANT: Start the animation BEFORE changing isLoaded.
710
+ // When isLoaded changes to 'ready', Svelte will reactively remove the
711
+ // initial inline styles. We need the animation to capture the current
712
+ // state (from inline styles) before they're removed.
603
713
  runAnimation()
714
+
715
+ // CRITICAL: Wait for the next animation frame before changing isLoaded.
716
+ // This gives WAAPI time to:
717
+ // 1. Parse and create the animation
718
+ // 2. Start the animation layer
719
+ // 3. Lock in the "from" values from current computed style
720
+ // Only THEN can we safely clear inline styles without killing the animation
721
+ requestAnimationFrame(() => {
722
+ isLoaded = 'ready'
723
+ })
604
724
  })
605
725
  } else {
726
+ pwLog('[motion] path: no initialKeyframes, skip to ready')
606
727
  dataPath = 2
607
728
  isLoaded = 'ready'
608
729
  // If we're inheriting a variant and parent had initial={false}, apply the variant instantly
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@ import MotionConfig from './components/MotionConfig.svelte';
3
3
  import type { MotionComponents } from './html/index';
4
4
  export declare const motion: MotionComponents;
5
5
  export { animate, hover } from 'motion';
6
- export type { DragAxis, DragConstraints, DragControls, DragInfo, DragTransition, MotionAnimate, MotionInitial, MotionOnDirectionLock, MotionOnDragTransitionEnd, MotionTransition, MotionWhileDrag, MotionWhileFocus, MotionWhileHover, MotionWhileTap, Variants } from './types';
6
+ export type { DragAxis, DragConstraints, DragControls, DragInfo, DragTransition, MotionAnimate, MotionInitial, MotionOnDirectionLock, MotionOnDragTransitionEnd, MotionOnInViewEnd, MotionOnInViewStart, MotionTransition, MotionWhileDrag, MotionWhileFocus, MotionWhileHover, MotionWhileInView, MotionWhileTap, Variants } from './types';
7
7
  export { useAnimationFrame } from './utils/animationFrame';
8
8
  export { createDragControls } from './utils/dragControls';
9
9
  export { useSpring } from './utils/spring';
package/dist/types.d.ts CHANGED
@@ -121,6 +121,18 @@ export type MotionWhileFocus = (Record<string, unknown> & {
121
121
  export type MotionWhileDrag = (Record<string, unknown> & {
122
122
  transition?: AnimationOptions;
123
123
  }) | DOMKeyframesDefinition | undefined;
124
+ /**
125
+ * Animation properties for in-view interactions.
126
+ * When the element enters the viewport, it animates to this state; when it leaves,
127
+ * it animates back to its baseline (from animate/initial), restoring only the changed keys.
128
+ * @example
129
+ * ```svelte
130
+ * <motion.div whileInView={{ opacity: 1, y: 0 }} />
131
+ * ```
132
+ */
133
+ export type MotionWhileInView = (Record<string, unknown> & {
134
+ transition?: AnimationOptions;
135
+ }) | DOMKeyframesDefinition | undefined;
124
136
  /**
125
137
  * Animation transition configuration for hover interactions.
126
138
  * Overrides the global transition when provided.
@@ -136,6 +148,9 @@ export type MotionOnHoverEnd = (() => void) | undefined;
136
148
  /** Focus lifecycle callbacks */
137
149
  export type MotionOnFocusStart = (() => void) | undefined;
138
150
  export type MotionOnFocusEnd = (() => void) | undefined;
151
+ /** InView lifecycle callbacks */
152
+ export type MotionOnInViewStart = (() => void) | undefined;
153
+ export type MotionOnInViewEnd = (() => void) | undefined;
139
154
  /** Tap lifecycle callbacks */
140
155
  export type MotionOnTapStart = (() => void) | undefined;
141
156
  export type MotionOnTap = (() => void) | undefined;
@@ -190,6 +205,21 @@ export type DragControls = {
190
205
  * Base motion props shared by all motion components.
191
206
  */
192
207
  export type MotionProps = {
208
+ /**
209
+ * Unique key for AnimatePresence tracking.
210
+ * Required when inside an AnimatePresence component.
211
+ * Used to track enter/exit state and determine whether to animate.
212
+ *
213
+ * @example
214
+ * ```svelte
215
+ * <AnimatePresence>
216
+ * {#if isVisible}
217
+ * <motion.div key="box" exit={{ opacity: 0 }} />
218
+ * {/if}
219
+ * </AnimatePresence>
220
+ * ```
221
+ */
222
+ key?: string;
193
223
  /** Variants define named animation states */
194
224
  variants?: Variants;
195
225
  /** Initial state of the animation (object or variant key) */
@@ -208,6 +238,8 @@ export type MotionProps = {
208
238
  whileFocus?: MotionWhileFocus;
209
239
  /** Drag interaction animation */
210
240
  whileDrag?: MotionWhileDrag;
241
+ /** In-view interaction animation - animates when element enters viewport */
242
+ whileInView?: MotionWhileInView;
211
243
  /** Called right before a main animate transition starts */
212
244
  onAnimationStart?: MotionAnimationStart;
213
245
  /** Called after a main animate transition completes */
@@ -220,6 +252,10 @@ export type MotionProps = {
220
252
  onFocusStart?: MotionOnFocusStart;
221
253
  /** Called when element loses keyboard focus */
222
254
  onFocusEnd?: MotionOnFocusEnd;
255
+ /** Called when element enters viewport */
256
+ onInViewStart?: MotionOnInViewStart;
257
+ /** Called when element leaves viewport */
258
+ onInViewEnd?: MotionOnInViewEnd;
223
259
  /** Called when a tap gesture starts (pointerdown recognized) */
224
260
  onTapStart?: MotionOnTapStart;
225
261
  /** Called when a tap gesture ends successfully (pointerup) */
@@ -1,3 +1,4 @@
1
+ import { pwLog } from './log';
1
2
  import { hasFinishedPromise, isPromiseLike } from './promise';
2
3
  import { animate } from 'motion';
3
4
  /**
@@ -32,12 +33,17 @@ export const mergeTransitions = (...args) => {
32
33
  * @param onStart Optional lifecycle fired before animation starts.
33
34
  * @param onComplete Optional lifecycle fired after animation completes.
34
35
  */
35
- export const animateWithLifecycle = (el, keyframes, transition,
36
- /* trunk-ignore(eslint/no-unused-vars) */
37
- onStart,
38
- /* trunk-ignore(eslint/no-unused-vars) */
39
- onComplete) => {
36
+ export const animateWithLifecycle = (el, keyframes, transition, onStart, onComplete) => {
40
37
  const payload = keyframes;
38
+ const computed = getComputedStyle(el);
39
+ pwLog('[animateWithLifecycle] starting', {
40
+ keyframes: payload,
41
+ transition,
42
+ currentOpacity: el.style.opacity,
43
+ currentTransform: el.style.transform,
44
+ computedOpacity: computed.opacity,
45
+ computedTransform: computed.transform
46
+ });
41
47
  onStart?.(payload);
42
48
  const controls = animate(el, payload, transition);
43
49
  if (hasFinishedPromise(controls)) {
@@ -0,0 +1,79 @@
1
+ import { type AnimationOptions, type DOMKeyframesDefinition } from 'motion';
2
+ /**
3
+ * Split a whileInView definition into keyframes and an optional nested transition.
4
+ *
5
+ * @param def While-in-view record that may include a nested `transition`.
6
+ * @returns Object with `keyframes` (no `transition`) and optional `transition`.
7
+ * @example
8
+ * // With transition
9
+ * splitInViewDefinition({ opacity: 1, y: 0, transition: { duration: 0.5 } })
10
+ * // => { keyframes: { opacity: 1, y: 0 }, transition: { duration: 0.5 } }
11
+ *
12
+ * @example
13
+ * // Without transition
14
+ * splitInViewDefinition({ opacity: 1, scale: 1 })
15
+ * // => { keyframes: { opacity: 1, scale: 1 }, transition: undefined }
16
+ */
17
+ export declare const splitInViewDefinition: (def: Record<string, unknown>) => {
18
+ keyframes: Record<string, unknown>;
19
+ transition?: AnimationOptions;
20
+ };
21
+ /**
22
+ * Compute the baseline values to restore to when element leaves viewport.
23
+ *
24
+ * Preference order per key: `animate` → `initial` → neutral transform defaults
25
+ * → computed style value if present.
26
+ *
27
+ * @param el Target element.
28
+ * @param opts Source records for baseline computation.
29
+ * @returns Minimal baseline record to restore when element leaves viewport.
30
+ * @example
31
+ * computeInViewBaseline(element, {
32
+ * initial: { opacity: 0, y: 50 },
33
+ * animate: { opacity: 1, y: 0 },
34
+ * whileInView: { opacity: 1, scale: 1.1 }
35
+ * })
36
+ * // => { opacity: 1, scale: 1 } (scale defaults to 1, opacity from animate)
37
+ */
38
+ export declare const computeInViewBaseline: (el: HTMLElement, opts: {
39
+ initial?: Record<string, unknown>;
40
+ animate?: Record<string, unknown>;
41
+ whileInView?: Record<string, unknown>;
42
+ }) => Record<string, unknown>;
43
+ /**
44
+ * Attach whileInView interactions to an element using IntersectionObserver.
45
+ *
46
+ * On intersection (element enters viewport), animates to `whileInView` state
47
+ * (using nested `transition` if provided). On un-intersection, restores changed
48
+ * keys to the baseline using the merged root/component transition.
49
+ *
50
+ * Critical fix for issue #230: Checks `entry.isIntersecting` immediately on
51
+ * first callback to handle elements already in viewport on mount.
52
+ *
53
+ * @param el Target element.
54
+ * @param whileInView While-in-view definition.
55
+ * @param mergedTransition Root/component merged transition.
56
+ * @param callbacks Optional lifecycle callbacks for in-view start/end and animation completion.
57
+ * @param baselineSources Optional sources used to compute baseline.
58
+ * @returns Cleanup function to disconnect the IntersectionObserver.
59
+ * @example
60
+ * const cleanup = attachWhileInView(
61
+ * element,
62
+ * { opacity: 1, y: 0, transition: { duration: 0.5 } },
63
+ * { duration: 0.3 },
64
+ * {
65
+ * onStart: () => console.log('Entered viewport'),
66
+ * onEnd: () => console.log('Left viewport')
67
+ * },
68
+ * { initial: { opacity: 0, y: 50 } }
69
+ * )
70
+ * // Later: cleanup() to disconnect observer
71
+ */
72
+ export declare const attachWhileInView: (el: HTMLElement, whileInView: Record<string, unknown> | undefined, mergedTransition: AnimationOptions, callbacks?: {
73
+ onStart?: () => void;
74
+ onEnd?: () => void;
75
+ onAnimationComplete?: (definition: DOMKeyframesDefinition | undefined) => void;
76
+ }, baselineSources?: {
77
+ initial?: Record<string, unknown>;
78
+ animate?: Record<string, unknown>;
79
+ }) => (() => void);
@@ -0,0 +1,179 @@
1
+ import { animate } from 'motion';
2
+ /**
3
+ * Split a whileInView definition into keyframes and an optional nested transition.
4
+ *
5
+ * @param def While-in-view record that may include a nested `transition`.
6
+ * @returns Object with `keyframes` (no `transition`) and optional `transition`.
7
+ * @example
8
+ * // With transition
9
+ * splitInViewDefinition({ opacity: 1, y: 0, transition: { duration: 0.5 } })
10
+ * // => { keyframes: { opacity: 1, y: 0 }, transition: { duration: 0.5 } }
11
+ *
12
+ * @example
13
+ * // Without transition
14
+ * splitInViewDefinition({ opacity: 1, scale: 1 })
15
+ * // => { keyframes: { opacity: 1, scale: 1 }, transition: undefined }
16
+ */
17
+ export const splitInViewDefinition = (def) => {
18
+ const { transition, ...rest } = (def ?? {});
19
+ return { keyframes: rest, transition };
20
+ };
21
+ /**
22
+ * Compute the baseline values to restore to when element leaves viewport.
23
+ *
24
+ * Preference order per key: `animate` → `initial` → neutral transform defaults
25
+ * → computed style value if present.
26
+ *
27
+ * @param el Target element.
28
+ * @param opts Source records for baseline computation.
29
+ * @returns Minimal baseline record to restore when element leaves viewport.
30
+ * @example
31
+ * computeInViewBaseline(element, {
32
+ * initial: { opacity: 0, y: 50 },
33
+ * animate: { opacity: 1, y: 0 },
34
+ * whileInView: { opacity: 1, scale: 1.1 }
35
+ * })
36
+ * // => { opacity: 1, scale: 1 } (scale defaults to 1, opacity from animate)
37
+ */
38
+ export const computeInViewBaseline = (el, opts) => {
39
+ const baseline = {};
40
+ const initialRecord = (opts.initial ?? {});
41
+ const animateRecord = (opts.animate ?? {});
42
+ const whileInViewRecordRaw = (opts.whileInView ?? {});
43
+ const whileInViewRecord = { ...whileInViewRecordRaw };
44
+ delete whileInViewRecord.transition;
45
+ const neutralTransformDefaults = {
46
+ x: 0,
47
+ y: 0,
48
+ translateX: 0,
49
+ translateY: 0,
50
+ scale: 1,
51
+ scaleX: 1,
52
+ scaleY: 1,
53
+ rotate: 0,
54
+ rotateX: 0,
55
+ rotateY: 0,
56
+ rotateZ: 0,
57
+ skewX: 0,
58
+ skewY: 0,
59
+ opacity: 1
60
+ };
61
+ const cs = getComputedStyle(el);
62
+ const inlineStyle = el.getAttribute('style') || '';
63
+ // Helper to escape regex metacharacters to prevent ReDoS and ensure literal matching
64
+ const escapeRegExp = (str) => {
65
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
66
+ };
67
+ // Helper to extract CSS function (var, calc, min, max, etc.) from inline style if present
68
+ const getInlineStyleValue = (propName) => {
69
+ const kebabCase = propName.replace(/([A-Z])/g, '-$1').toLowerCase();
70
+ const escapedKebabCase = escapeRegExp(kebabCase);
71
+ // Match property name at start of string or after semicolon
72
+ const regex = new RegExp(`(?:^|;)\\s*${escapedKebabCase}\\s*:\\s*([^;]+)`, 'i');
73
+ const match = inlineStyle.match(regex);
74
+ if (match) {
75
+ const value = match[1].trim();
76
+ // Preserve CSS functions: var(), calc(), min(), max(), clamp(), rgb(), hsl(), url(), etc.
77
+ if (/\b(var|calc|min|max|clamp|rgb|rgba|hsl|hsla|url)\s*\(/.test(value)) {
78
+ return value;
79
+ }
80
+ }
81
+ return null;
82
+ };
83
+ for (const key of Object.keys(whileInViewRecord)) {
84
+ if (Object.prototype.hasOwnProperty.call(animateRecord, key)) {
85
+ baseline[key] = animateRecord[key];
86
+ }
87
+ else if (Object.prototype.hasOwnProperty.call(initialRecord, key)) {
88
+ baseline[key] = initialRecord[key];
89
+ }
90
+ else if (key in neutralTransformDefaults) {
91
+ baseline[key] = neutralTransformDefaults[key];
92
+ }
93
+ else {
94
+ // Check if inline style has a CSS variable for this property
95
+ const inlineValue = getInlineStyleValue(key);
96
+ if (inlineValue) {
97
+ baseline[key] = inlineValue;
98
+ }
99
+ else if (key in cs) {
100
+ baseline[key] = cs[key];
101
+ }
102
+ }
103
+ }
104
+ return baseline;
105
+ };
106
+ /**
107
+ * Attach whileInView interactions to an element using IntersectionObserver.
108
+ *
109
+ * On intersection (element enters viewport), animates to `whileInView` state
110
+ * (using nested `transition` if provided). On un-intersection, restores changed
111
+ * keys to the baseline using the merged root/component transition.
112
+ *
113
+ * Critical fix for issue #230: Checks `entry.isIntersecting` immediately on
114
+ * first callback to handle elements already in viewport on mount.
115
+ *
116
+ * @param el Target element.
117
+ * @param whileInView While-in-view definition.
118
+ * @param mergedTransition Root/component merged transition.
119
+ * @param callbacks Optional lifecycle callbacks for in-view start/end and animation completion.
120
+ * @param baselineSources Optional sources used to compute baseline.
121
+ * @returns Cleanup function to disconnect the IntersectionObserver.
122
+ * @example
123
+ * const cleanup = attachWhileInView(
124
+ * element,
125
+ * { opacity: 1, y: 0, transition: { duration: 0.5 } },
126
+ * { duration: 0.3 },
127
+ * {
128
+ * onStart: () => console.log('Entered viewport'),
129
+ * onEnd: () => console.log('Left viewport')
130
+ * },
131
+ * { initial: { opacity: 0, y: 50 } }
132
+ * )
133
+ * // Later: cleanup() to disconnect observer
134
+ */
135
+ export const attachWhileInView = (el, whileInView, mergedTransition, callbacks, baselineSources) => {
136
+ if (!whileInView)
137
+ return () => { };
138
+ let hasAnimated = false;
139
+ let inViewBaseline = null;
140
+ const observer = new IntersectionObserver((entries) => {
141
+ for (const entry of entries) {
142
+ if (entry.isIntersecting && !hasAnimated) {
143
+ // Element entered viewport: animate to whileInView state
144
+ hasAnimated = true;
145
+ inViewBaseline = computeInViewBaseline(el, {
146
+ initial: baselineSources?.initial,
147
+ animate: baselineSources?.animate,
148
+ whileInView
149
+ });
150
+ callbacks?.onStart?.();
151
+ const { keyframes, transition } = splitInViewDefinition(whileInView);
152
+ const animation = animate(el, keyframes, (transition ?? mergedTransition));
153
+ // Call onAnimationComplete when animation finishes
154
+ animation.finished
155
+ .then(() => {
156
+ callbacks?.onAnimationComplete?.(keyframes);
157
+ })
158
+ .catch(() => {
159
+ // Animation was cancelled, don't call completion callback
160
+ });
161
+ }
162
+ else if (!entry.isIntersecting && hasAnimated) {
163
+ // Element left viewport: animate back to baseline
164
+ if (inViewBaseline && Object.keys(inViewBaseline).length > 0) {
165
+ animate(el, inViewBaseline, mergedTransition);
166
+ }
167
+ callbacks?.onEnd?.();
168
+ hasAnimated = false;
169
+ }
170
+ }
171
+ }, { threshold: 0 } // Fire as soon as any part is visible
172
+ );
173
+ // Start observing - IntersectionObserver fires immediately for already-visible elements
174
+ observer.observe(el);
175
+ // Return cleanup function
176
+ return () => {
177
+ observer.disconnect();
178
+ };
179
+ };
@@ -5,6 +5,14 @@ import type { MotionExit, MotionTransition } from '../types';
5
5
  * so we can clone and animate them out after removal.
6
6
  */
7
7
  export type AnimatePresenceContext = {
8
+ /** When false, children skip their enter animation on initial mount. */
9
+ initial: boolean;
10
+ /**
11
+ * Returns true if a child with the given key should animate its enter.
12
+ * Returns false only during first render when initial={false} AND the key has never been seen.
13
+ * Re-entries (after exit) always animate.
14
+ */
15
+ shouldAnimateEnter: (key: string) => boolean;
8
16
  /** Called when all exit animations complete (optional). */
9
17
  onExitComplete?: () => void;
10
18
  /** Register a child element and its exit definition. */
@@ -35,6 +43,7 @@ export type AnimatePresenceContext = {
35
43
  * @returns A presence context with register/update/unregister APIs.
36
44
  */
37
45
  export declare function createAnimatePresenceContext(context: {
46
+ initial?: boolean;
38
47
  onExitComplete?: () => void;
39
48
  }): AnimatePresenceContext;
40
49
  /**
@@ -1,4 +1,5 @@
1
1
  import { mergeTransitions } from './animation';
2
+ import { pwLog } from './log';
2
3
  import { animate } from 'motion';
3
4
  import { getContext, onDestroy, setContext } from 'svelte';
4
5
  /**
@@ -44,6 +45,67 @@ const resetTransforms = (element) => {
44
45
  * @returns A presence context with register/update/unregister APIs.
45
46
  */
46
47
  export function createAnimatePresenceContext(context) {
48
+ // Default initial to true (animate on first mount) unless explicitly false
49
+ const initial = context.initial !== false;
50
+ // Track whether we're still in the initial render phase
51
+ // This is true only when initial={false} and we haven't completed the first frame
52
+ let isInitialRenderPhase = context.initial === false;
53
+ // Track keys that have been seen (registered at least once)
54
+ const seenKeys = new Set();
55
+ // Track keys that have exited (unregistered after being registered)
56
+ const exitedKeys = new Set();
57
+ // After first frame, mark initial render phase as complete
58
+ // Guard for SSR - requestAnimationFrame only exists in browser
59
+ if (isInitialRenderPhase && typeof window !== 'undefined') {
60
+ requestAnimationFrame(() => {
61
+ requestAnimationFrame(() => {
62
+ pwLog('[presence] initial render phase complete, enabling animations for new keys');
63
+ isInitialRenderPhase = false;
64
+ });
65
+ });
66
+ }
67
+ /**
68
+ * Determine if a child with the given key should animate its enter.
69
+ *
70
+ * - If we're past the initial render phase → always animate
71
+ * - If key has previously exited → animate (re-entry)
72
+ * - If key has never been seen AND we're in initial render phase → skip animation
73
+ */
74
+ const shouldAnimateEnter = (key) => {
75
+ // If the key has previously exited, it's a re-entry - always animate
76
+ if (exitedKeys.has(key)) {
77
+ pwLog('[presence] shouldAnimateEnter', {
78
+ key,
79
+ result: true,
80
+ reason: 're-entry after exit'
81
+ });
82
+ return true;
83
+ }
84
+ // If we're past the initial render phase, all new entries animate
85
+ if (!isInitialRenderPhase) {
86
+ pwLog('[presence] shouldAnimateEnter', {
87
+ key,
88
+ result: true,
89
+ reason: 'past initial render phase'
90
+ });
91
+ return true;
92
+ }
93
+ // We're in initial render phase and key hasn't exited before
94
+ // Check if key has been seen - if not, skip animation (initial={false} behavior)
95
+ const hasBeenSeen = seenKeys.has(key);
96
+ const shouldAnimate = hasBeenSeen; // Only animate if we've seen it before (shouldn't happen in initial phase)
97
+ pwLog('[presence] shouldAnimateEnter', {
98
+ key,
99
+ result: shouldAnimate,
100
+ reason: shouldAnimate ? 'previously seen' : 'first appearance during initial render'
101
+ });
102
+ return shouldAnimate;
103
+ };
104
+ pwLog('[presence] createContext', {
105
+ initial,
106
+ isInitialRenderPhase,
107
+ onExitComplete: !!context.onExitComplete
108
+ });
47
109
  const children = new Map();
48
110
  // Track number of in-flight exit animations to invoke onExitComplete once
49
111
  let inFlightExits = 0;
@@ -53,6 +115,20 @@ export function createAnimatePresenceContext(context) {
53
115
  const registerChild = (key, element, exit, mergedTransition) => {
54
116
  const initialRect = element.getBoundingClientRect();
55
117
  const initialStyle = getComputedStyle(element);
118
+ // Mark this key as seen
119
+ const wasExited = exitedKeys.has(key);
120
+ seenKeys.add(key);
121
+ // If this key was previously exited, remove it from exitedKeys (it's re-entering)
122
+ if (wasExited) {
123
+ exitedKeys.delete(key);
124
+ }
125
+ pwLog('[presence] registerChild', {
126
+ key,
127
+ hasExit: !!exit,
128
+ exit,
129
+ wasExited,
130
+ rect: { w: initialRect.width, h: initialRect.height }
131
+ });
56
132
  children.set(key, {
57
133
  element,
58
134
  exit,
@@ -77,7 +153,21 @@ export function createAnimatePresenceContext(context) {
77
153
  */
78
154
  const unregisterChild = (key) => {
79
155
  const child = children.get(key);
80
- if (!child || !child.exit) {
156
+ pwLog('[presence] unregisterChild', {
157
+ key,
158
+ found: !!child,
159
+ hasExit: !!child?.exit,
160
+ exit: child?.exit
161
+ });
162
+ // Only process if child was actually registered
163
+ if (!child) {
164
+ pwLog('[presence] unregisterChild - child not found, ignoring');
165
+ return;
166
+ }
167
+ // Mark this key as exited so re-entry will animate
168
+ exitedKeys.add(key);
169
+ if (!child.exit) {
170
+ pwLog('[presence] unregisterChild - no exit animation, removing immediately');
81
171
  children.delete(key);
82
172
  return;
83
173
  }
@@ -143,6 +233,10 @@ export function createAnimatePresenceContext(context) {
143
233
  }
144
234
  clone.setAttribute('data-clone', 'true');
145
235
  clone.setAttribute('data-exiting', 'true');
236
+ pwLog('[presence] clone created', {
237
+ key,
238
+ rect: { w: rect.width, h: rect.height, top: rect.top, left: rect.left }
239
+ });
146
240
  parent.appendChild(clone);
147
241
  // Merge transitions: default < mergedTransition < exit.transition (last wins)
148
242
  const exitObj = (child.exit ?? {});
@@ -151,12 +245,22 @@ export function createAnimatePresenceContext(context) {
151
245
  const rawExit = (child.exit ?? {});
152
246
  /* trunk-ignore(eslint/@typescript-eslint/no-unused-vars) */
153
247
  const { transition: _ignoredTransition, ...exitKeyframes } = rawExit;
248
+ pwLog('[presence] starting exit animation', {
249
+ key,
250
+ exitKeyframes,
251
+ finalTransition
252
+ });
253
+ // Capture the element reference for this specific exit animation
254
+ // This prevents race conditions where re-entry registers a new element with the same key
255
+ // before this exit animation completes
256
+ const exitingElement = child.element;
154
257
  // Start exit and track in-flight count
155
258
  inFlightExits += 1;
156
259
  requestAnimationFrame(() => {
157
260
  animate(clone, exitKeyframes, finalTransition)
158
261
  .finished.catch(() => { })
159
262
  .finally(() => {
263
+ pwLog('[presence] exit animation complete', { key });
160
264
  // Reset elevated styles then remove
161
265
  try {
162
266
  clone.style.zIndex = '';
@@ -165,16 +269,45 @@ export function createAnimatePresenceContext(context) {
165
269
  // ignore
166
270
  }
167
271
  clone.remove();
168
- children.delete(key);
272
+ // Log clone removal and element counts for debugging rapid toggle
273
+ pwLog('[presence] clone REMOVED from DOM', {
274
+ key,
275
+ clonesInDOM: document.querySelectorAll('[data-clone="true"]').length,
276
+ boxesInDOM: document.querySelectorAll('[data-testid="box"]').length
277
+ });
278
+ // Only delete from children map if the current registration is for the SAME element
279
+ // If a re-entry happened while we were animating, a new element is registered
280
+ // and we should NOT delete it
281
+ const currentChild = children.get(key);
282
+ if (currentChild && currentChild.element === exitingElement) {
283
+ children.delete(key);
284
+ pwLog('[presence] child deleted from map (same element)', { key });
285
+ }
286
+ else {
287
+ pwLog('[presence] child NOT deleted (re-entry registered new element)', {
288
+ key,
289
+ hasCurrentChild: !!currentChild,
290
+ isSameElement: currentChild?.element === exitingElement
291
+ });
292
+ }
293
+ // Log final state
294
+ pwLog('[presence] element count after exit', {
295
+ childrenMapSize: children.size,
296
+ inFlightExits: inFlightExits - 1,
297
+ clonesInDOM: document.querySelectorAll('[data-clone="true"]').length
298
+ });
169
299
  inFlightExits -= 1;
170
300
  if (inFlightExits === 0) {
301
+ pwLog('[presence] all exits complete, calling onExitComplete');
171
302
  context.onExitComplete?.();
172
303
  }
173
304
  });
174
305
  });
175
306
  };
176
307
  return {
177
- ...context,
308
+ initial,
309
+ shouldAnimateEnter,
310
+ onExitComplete: context.onExitComplete,
178
311
  registerChild,
179
312
  updateChildState,
180
313
  unregisterChild
@@ -218,11 +351,24 @@ export function setAnimatePresenceContext(context) {
218
351
  */
219
352
  export function usePresence(key, element, exit, mergedTransition) {
220
353
  const context = getAnimatePresenceContext();
354
+ pwLog('[presence] usePresence called', {
355
+ key,
356
+ hasElement: !!element,
357
+ hasContext: !!context,
358
+ hasExit: !!exit,
359
+ exit
360
+ });
221
361
  if (element && context && exit) {
222
362
  context.registerChild(key, element, exit, mergedTransition);
223
363
  onDestroy(() => {
364
+ pwLog('[presence] onDestroy triggered', { key });
224
365
  context.unregisterChild(key);
225
366
  });
226
367
  }
368
+ else {
369
+ pwLog('[presence] usePresence - skipping registration', {
370
+ reason: !element ? 'no element' : !context ? 'no context' : 'no exit'
371
+ });
372
+ }
227
373
  }
228
374
  /* c8 ignore end */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@humanspeak/svelte-motion",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "description": "A lightweight animation library for Svelte 5 that provides smooth, hardware-accelerated animations. Features include spring physics, custom easing, and fluid transitions. Built on top of the motion library, it offers a simple API for creating complex animations with minimal code. Perfect for interactive UIs, micro-interactions, and engaging user experiences.",
5
5
  "keywords": [
6
6
  "svelte",
@@ -53,18 +53,18 @@
53
53
  }
54
54
  },
55
55
  "dependencies": {
56
- "motion": "^12.24.7",
57
- "motion-dom": "^12.24.3"
56
+ "motion": "^12.29.2",
57
+ "motion-dom": "^12.29.2"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@changesets/cli": "^2.29.8",
61
- "@eslint/compat": "^2.0.0",
61
+ "@eslint/compat": "^2.0.2",
62
62
  "@eslint/js": "^9.39.2",
63
- "@playwright/test": "^1.57.0",
63
+ "@playwright/test": "^1.58.1",
64
64
  "@sveltejs/adapter-auto": "^7.0.0",
65
- "@sveltejs/kit": "^2.49.3",
65
+ "@sveltejs/kit": "^2.50.1",
66
66
  "@sveltejs/package": "^2.5.7",
67
- "@sveltejs/vite-plugin-svelte": "^6.2.2",
67
+ "@sveltejs/vite-plugin-svelte": "^6.2.4",
68
68
  "@tailwindcss/aspect-ratio": "^0.4.2",
69
69
  "@tailwindcss/container-queries": "^0.1.1",
70
70
  "@tailwindcss/forms": "^0.5.11",
@@ -72,29 +72,29 @@
72
72
  "@tailwindcss/typography": "^0.5.19",
73
73
  "@testing-library/jest-dom": "^6.9.1",
74
74
  "@testing-library/svelte": "^5.3.1",
75
- "@types/node": "^25.0.3",
76
- "@vitest/coverage-v8": "^4.0.16",
75
+ "@types/node": "^25.1.0",
76
+ "@vitest/coverage-v8": "^4.0.18",
77
77
  "concurrently": "^9.2.1",
78
78
  "eslint": "^9.39.2",
79
79
  "eslint-config-prettier": "10.1.8",
80
80
  "eslint-plugin-import": "2.32.0",
81
- "eslint-plugin-svelte": "3.13.1",
81
+ "eslint-plugin-svelte": "3.14.0",
82
82
  "eslint-plugin-unused-imports": "4.3.0",
83
83
  "esm-env": "^1.2.2",
84
- "globals": "^17.0.0",
84
+ "globals": "^17.2.0",
85
85
  "html-tags": "^5.1.0",
86
86
  "html-void-elements": "^3.0.0",
87
87
  "husky": "^9.1.7",
88
88
  "jsdom": "^27.4.0",
89
- "prettier": "^3.7.4",
89
+ "prettier": "^3.8.1",
90
90
  "prettier-plugin-organize-imports": "^4.3.0",
91
- "prettier-plugin-sort-json": "^4.1.1",
91
+ "prettier-plugin-sort-json": "^4.2.0",
92
92
  "prettier-plugin-svelte": "^3.4.1",
93
93
  "prettier-plugin-tailwindcss": "^0.7.2",
94
- "publint": "^0.3.16",
94
+ "publint": "^0.3.17",
95
95
  "runed": "0.37.1",
96
- "svelte": "^5.46.1",
97
- "svelte-check": "^4.3.5",
96
+ "svelte": "^5.49.1",
97
+ "svelte-check": "^4.3.6",
98
98
  "svg-tags": "^1.0.0",
99
99
  "tailwind-merge": "^3.4.0",
100
100
  "tailwind-variants": "^3.2.2",
@@ -102,16 +102,16 @@
102
102
  "tailwindcss-animate": "^1.0.7",
103
103
  "tsx": "^4.21.0",
104
104
  "typescript": "^5.9.3",
105
- "typescript-eslint": "^8.52.0",
106
- "vite": "^7.3.0",
107
- "vite-tsconfig-paths": "^6.0.3",
108
- "vitest": "^4.0.16"
105
+ "typescript-eslint": "^8.54.0",
106
+ "vite": "^7.3.1",
107
+ "vite-tsconfig-paths": "^6.0.5",
108
+ "vitest": "^4.0.18"
109
109
  },
110
110
  "peerDependencies": {
111
111
  "svelte": "^5.0.0"
112
112
  },
113
113
  "volta": {
114
- "node": "22.21.1"
114
+ "node": "24.12.0"
115
115
  },
116
116
  "publishConfig": {
117
117
  "access": "public"