@humanspeak/svelte-motion 0.5.3 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/LazyMotion.svelte +70 -0
- package/dist/components/LazyMotion.svelte.d.ts +16 -0
- package/dist/components/lazyMotion.context.d.ts +25 -0
- package/dist/components/lazyMotion.context.js +19 -0
- package/dist/components/motionDomProjection.context.d.ts +13 -0
- package/dist/components/motionDomProjection.context.js +18 -0
- package/dist/features/domAnimation.d.ts +6 -0
- package/dist/features/domAnimation.js +8 -0
- package/dist/features/domMax.d.ts +5 -0
- package/dist/features/domMax.js +9 -0
- package/dist/features/domMin.d.ts +5 -0
- package/dist/features/domMin.js +6 -0
- package/dist/features/index.d.ts +39 -0
- package/dist/features/index.js +18 -0
- package/dist/html/_MotionContainer.svelte +602 -70
- package/dist/index.d.ts +8 -1
- package/dist/index.js +7 -1
- package/dist/m.d.ts +9 -0
- package/dist/m.js +9 -0
- package/dist/types.d.ts +2 -2
- package/dist/utils/layout.d.ts +9 -6
- package/dist/utils/layout.js +141 -14
- package/dist/utils/motionDomProjection.d.ts +126 -0
- package/dist/utils/motionDomProjection.js +212 -0
- package/dist/utils/optimizedAppear.d.ts +141 -0
- package/dist/utils/optimizedAppear.js +311 -0
- package/dist/utils/presence.d.ts +3 -2
- package/dist/utils/presence.js +49 -12
- package/dist/utils/projection.d.ts +3 -3
- package/dist/utils/projection.js +1 -1
- package/dist/utils/svg.d.ts +4 -4
- package/dist/utils/svg.js +44 -25
- package/package.json +1 -1
|
@@ -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
|
|
@@ -26,6 +28,7 @@
|
|
|
26
28
|
import { isNotEmpty } from '../utils/objects'
|
|
27
29
|
import { sleep } from '../utils/testing'
|
|
28
30
|
import { animate, type AnimationOptions, type DOMKeyframesDefinition } from 'motion'
|
|
31
|
+
import { motionValue, svgEffect, type MotionValue } from 'motion-dom'
|
|
29
32
|
import { isPlaywrightEnv, pwLog } from '../utils/log'
|
|
30
33
|
import { onDestroy, untrack, type Snippet } from 'svelte'
|
|
31
34
|
import { VOID_TAGS } from '../utils/constants'
|
|
@@ -56,6 +59,11 @@
|
|
|
56
59
|
import { attachPan, type AttachPanCleanup } from '../utils/pan'
|
|
57
60
|
import { ProjectionNode } from '../utils/projection'
|
|
58
61
|
import { getProjectionParent, setProjectionParent } from '../components/projection.context'
|
|
62
|
+
import { MotionDomProjectionAdapter } from '../utils/motionDomProjection'
|
|
63
|
+
import {
|
|
64
|
+
getMotionDomProjectionParent,
|
|
65
|
+
setMotionDomProjectionParent
|
|
66
|
+
} from '../components/motionDomProjection.context'
|
|
59
67
|
import {
|
|
60
68
|
resolveInitial,
|
|
61
69
|
resolveAnimate,
|
|
@@ -75,9 +83,19 @@
|
|
|
75
83
|
import {
|
|
76
84
|
transformSVGPathProperties,
|
|
77
85
|
computeNormalizedSVGInitialAttrs,
|
|
86
|
+
hasSVGPathProperties,
|
|
87
|
+
isSVGPathElement,
|
|
78
88
|
isSVGTag,
|
|
79
89
|
SVG_NAMESPACE
|
|
80
90
|
} from '../utils/svg'
|
|
91
|
+
import {
|
|
92
|
+
createOptimizedAppearData,
|
|
93
|
+
createOptimizedAppearScript,
|
|
94
|
+
finishOptimizedAppearAnimation,
|
|
95
|
+
hasOptimizedAppearAnimation,
|
|
96
|
+
markMotionMounted,
|
|
97
|
+
optimizedAppearDataAttribute
|
|
98
|
+
} from '../utils/optimizedAppear'
|
|
81
99
|
import { getLayoutIdRegistry } from '../utils/layoutId'
|
|
82
100
|
import {
|
|
83
101
|
getLayoutScrollContainerRef,
|
|
@@ -91,6 +109,8 @@
|
|
|
91
109
|
[key: string]: unknown
|
|
92
110
|
}
|
|
93
111
|
|
|
112
|
+
const componentHydrationId = $props.id()
|
|
113
|
+
|
|
94
114
|
let {
|
|
95
115
|
children,
|
|
96
116
|
tag = 'div',
|
|
@@ -156,6 +176,11 @@
|
|
|
156
176
|
let enterAnimationSettled = $state(false)
|
|
157
177
|
let dataPath = $state<number>(-1)
|
|
158
178
|
const motionConfig = $derived(getMotionConfig())
|
|
179
|
+
const lazyMotion = getLazyMotionContext()
|
|
180
|
+
const activeFeatures = $derived(lazyMotion?.getFeatures() ?? domMax)
|
|
181
|
+
const hasGestureFeatures = $derived(!!activeFeatures.gestures)
|
|
182
|
+
const hasDragFeatures = $derived(!!activeFeatures.drag)
|
|
183
|
+
const hasLayoutFeatures = $derived(!!activeFeatures.layout)
|
|
159
184
|
const reducedMotionState = useReducedMotionConfig()
|
|
160
185
|
// `.current` is $state-backed inside reducedMotionState; tracking it via
|
|
161
186
|
// $derived makes `reducedMotion` re-evaluate whenever the OS preference
|
|
@@ -227,6 +252,18 @@
|
|
|
227
252
|
})
|
|
228
253
|
setProjectionParent(projection)
|
|
229
254
|
|
|
255
|
+
const motionDomProjectionParent =
|
|
256
|
+
typeof window !== 'undefined' ? getMotionDomProjectionParent() : null
|
|
257
|
+
const motionDomProjection =
|
|
258
|
+
typeof window !== 'undefined'
|
|
259
|
+
? new MotionDomProjectionAdapter({
|
|
260
|
+
parent: motionDomProjectionParent
|
|
261
|
+
})
|
|
262
|
+
: null
|
|
263
|
+
if (motionDomProjection) {
|
|
264
|
+
setMotionDomProjectionParent(motionDomProjection)
|
|
265
|
+
}
|
|
266
|
+
|
|
230
267
|
// Convert a projection `Box` (ancestor chain reset to base, self
|
|
231
268
|
// transform stripped, scroll containers compensated) to the
|
|
232
269
|
// `RectLike` shape `computeFlipTransforms` consumes.
|
|
@@ -239,9 +276,20 @@
|
|
|
239
276
|
width: box.x.max - box.x.min,
|
|
240
277
|
height: box.y.max - box.y.min
|
|
241
278
|
})
|
|
279
|
+
const domRectToRectLike = (rect: DOMRect): RectLike => ({
|
|
280
|
+
left: rect.left,
|
|
281
|
+
top: rect.top,
|
|
282
|
+
width: rect.width,
|
|
283
|
+
height: rect.height
|
|
284
|
+
})
|
|
285
|
+
const isViewportOffscreen = (rect: DOMRect): boolean =>
|
|
286
|
+
rect.bottom <= 0 ||
|
|
287
|
+
rect.right <= 0 ||
|
|
288
|
+
rect.top >= window.innerHeight ||
|
|
289
|
+
rect.left >= window.innerWidth
|
|
242
290
|
|
|
243
291
|
// Ancestor-transform-invariant layout measurement for seeding the
|
|
244
|
-
// FLIP effect's first rect.
|
|
292
|
+
// fallback FLIP effect's first rect.
|
|
245
293
|
const measureLayoutRect = (): RectLike | null => {
|
|
246
294
|
const box = projection.measure()
|
|
247
295
|
return box ? boxToRectLike(box) : null
|
|
@@ -544,6 +592,173 @@
|
|
|
544
592
|
reducedMotion
|
|
545
593
|
)
|
|
546
594
|
)
|
|
595
|
+
const optimizedAppearId = $derived(
|
|
596
|
+
effectiveInitialProp !== false &&
|
|
597
|
+
isNotEmpty(initialKeyframes) &&
|
|
598
|
+
isNotEmpty(animateKeyframes)
|
|
599
|
+
? `svelte-motion-${componentHydrationId}`
|
|
600
|
+
: undefined
|
|
601
|
+
)
|
|
602
|
+
const optimizedAppearEntries = $derived(
|
|
603
|
+
createOptimizedAppearData(
|
|
604
|
+
initialKeyframes as Record<string, unknown> | undefined,
|
|
605
|
+
animateKeyframes as Record<string, unknown> | undefined,
|
|
606
|
+
mergedTransition
|
|
607
|
+
)
|
|
608
|
+
)
|
|
609
|
+
const optimizedAppearScript = $derived(
|
|
610
|
+
createOptimizedAppearScript(optimizedAppearId, optimizedAppearEntries)
|
|
611
|
+
)
|
|
612
|
+
const renderedOptimizedAppearScript = $derived(
|
|
613
|
+
optimizedAppearScript && (typeof window === 'undefined' || !window.MotionIsMounted)
|
|
614
|
+
? optimizedAppearScript
|
|
615
|
+
: ''
|
|
616
|
+
)
|
|
617
|
+
const applyAnimateRestingStyle = () => {
|
|
618
|
+
if (!element) return
|
|
619
|
+
const restingValues = resolveRestingValues(
|
|
620
|
+
animateKeyframes as DOMKeyframesDefinition | undefined
|
|
621
|
+
) as Record<string, unknown> | undefined
|
|
622
|
+
if (!restingValues) return
|
|
623
|
+
element.setAttribute(
|
|
624
|
+
'style',
|
|
625
|
+
mergeInlineStyles(element.getAttribute('style') ?? '', undefined, restingValues)
|
|
626
|
+
)
|
|
627
|
+
}
|
|
628
|
+
const isJsdomRuntime = (): boolean =>
|
|
629
|
+
typeof navigator !== 'undefined' && /jsdom/i.test(navigator.userAgent)
|
|
630
|
+
const getTransitionFallbackMs = (transition: AnimationOptions | undefined): number => {
|
|
631
|
+
const duration = typeof transition?.duration === 'number' ? transition.duration : 0
|
|
632
|
+
const delay = typeof transition?.delay === 'number' ? transition.delay : 0
|
|
633
|
+
return Math.max(0, (duration + delay) * 1000)
|
|
634
|
+
}
|
|
635
|
+
let cleanupSVGPathAttributeEffect: (() => void) | null = null
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Reads the current normalized SVG path drawing state from DOM
|
|
639
|
+
* attributes. `motion-dom`'s svgEffect owns future writes; this only
|
|
640
|
+
* seeds its MotionValues from the currently rendered frame.
|
|
641
|
+
*
|
|
642
|
+
* @param {SVGPathElement} path The SVG path element to inspect.
|
|
643
|
+
* @returns {{ pathLength: number; pathSpacing: number; pathOffset: number }} The normalized drawing state.
|
|
644
|
+
*/
|
|
645
|
+
const readSVGPathDrawingState = (
|
|
646
|
+
path: SVGPathElement
|
|
647
|
+
): { pathLength: number; pathSpacing: number; pathOffset: number } => {
|
|
648
|
+
const dashArray =
|
|
649
|
+
path.getAttribute('stroke-dasharray') || path.style.strokeDasharray || '1 0'
|
|
650
|
+
const [rawLength, rawSpacing] = dashArray
|
|
651
|
+
.split(/[,\s]+/)
|
|
652
|
+
.filter(Boolean)
|
|
653
|
+
.map((part) => Number.parseFloat(part))
|
|
654
|
+
const rawOffset = Number.parseFloat(
|
|
655
|
+
path.getAttribute('stroke-dashoffset') || path.style.strokeDashoffset || '0'
|
|
656
|
+
)
|
|
657
|
+
|
|
658
|
+
return {
|
|
659
|
+
pathLength: Number.isFinite(rawLength) ? rawLength : 1,
|
|
660
|
+
pathSpacing: Number.isFinite(rawSpacing) ? rawSpacing : 1,
|
|
661
|
+
pathOffset: Number.isFinite(rawOffset) ? -rawOffset : 0
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Removes custom SVG path props from keyframes after `svgEffect` has
|
|
667
|
+
* taken ownership of them.
|
|
668
|
+
*
|
|
669
|
+
* @param {Record<string, unknown>} keyframes Keyframes to copy.
|
|
670
|
+
* @returns {Record<string, unknown>} Keyframes without SVG path-only props.
|
|
671
|
+
*/
|
|
672
|
+
const stripSVGPathKeyframes = (keyframes: Record<string, unknown>): Record<string, unknown> => {
|
|
673
|
+
const stripped = { ...keyframes }
|
|
674
|
+
delete stripped.pathLength
|
|
675
|
+
delete stripped.pathSpacing
|
|
676
|
+
delete stripped.pathOffset
|
|
677
|
+
return stripped
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Extracts an animation completion promise from a Motion control when
|
|
682
|
+
* one is available.
|
|
683
|
+
*
|
|
684
|
+
* @param {unknown} control The return value from `animate`.
|
|
685
|
+
* @returns {Promise<unknown> | null} The finished promise, or null.
|
|
686
|
+
*/
|
|
687
|
+
const getFinishedPromise = (control: unknown): Promise<unknown> | null => {
|
|
688
|
+
if (!control || typeof control !== 'object') return null
|
|
689
|
+
const finished = (control as { finished?: unknown }).finished
|
|
690
|
+
return finished && typeof (finished as Promise<unknown>).then === 'function'
|
|
691
|
+
? (finished as Promise<unknown>)
|
|
692
|
+
: null
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Animates SVG path drawing props via motion-dom's `svgEffect`, matching
|
|
697
|
+
* upstream's attribute-based pathLength/pathSpacing/pathOffset behavior.
|
|
698
|
+
*
|
|
699
|
+
* @param {SVGPathElement} path The path element to animate.
|
|
700
|
+
* @param {Record<string, unknown>} keyframes Keyframes containing SVG path props.
|
|
701
|
+
* @param {MotionTransition} transition The transition to apply to generated MotionValues.
|
|
702
|
+
* @returns {Promise<unknown>[]} Promises for generated path animations.
|
|
703
|
+
*/
|
|
704
|
+
const animateSVGPathAttributes = (
|
|
705
|
+
path: SVGPathElement,
|
|
706
|
+
keyframes: Record<string, unknown>,
|
|
707
|
+
transition: MotionTransition
|
|
708
|
+
): Promise<unknown>[] => {
|
|
709
|
+
if (!hasSVGPathProperties(keyframes)) return []
|
|
710
|
+
|
|
711
|
+
cleanupSVGPathAttributeEffect?.()
|
|
712
|
+
const current = readSVGPathDrawingState(path)
|
|
713
|
+
const values: Record<string, MotionValue<number>> = {}
|
|
714
|
+
|
|
715
|
+
if ('pathLength' in keyframes) {
|
|
716
|
+
values.pathLength = motionValue(current.pathLength)
|
|
717
|
+
}
|
|
718
|
+
if ('pathLength' in keyframes || 'pathSpacing' in keyframes) {
|
|
719
|
+
values.pathSpacing = motionValue(current.pathSpacing)
|
|
720
|
+
}
|
|
721
|
+
if ('pathOffset' in keyframes) {
|
|
722
|
+
values.pathOffset = motionValue(current.pathOffset)
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
cleanupSVGPathAttributeEffect = svgEffect(path, values)
|
|
726
|
+
|
|
727
|
+
return Object.entries(values)
|
|
728
|
+
.map(([key, value]) => {
|
|
729
|
+
const control = animate(
|
|
730
|
+
value as never,
|
|
731
|
+
(key === 'pathSpacing' && !('pathSpacing' in keyframes)
|
|
732
|
+
? 1
|
|
733
|
+
: keyframes[key]) as never,
|
|
734
|
+
transition as unknown as AnimationOptions
|
|
735
|
+
)
|
|
736
|
+
return getFinishedPromise(control)
|
|
737
|
+
})
|
|
738
|
+
.filter((promise): promise is Promise<unknown> => promise !== null)
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
onDestroy(() => {
|
|
742
|
+
cleanupSVGPathAttributeEffect?.()
|
|
743
|
+
cleanupSVGPathAttributeEffect = null
|
|
744
|
+
})
|
|
745
|
+
|
|
746
|
+
// Wait-mode enter coordination needs to affect the first rendered attrs,
|
|
747
|
+
// before the blocked entrant can participate in layout.
|
|
748
|
+
let waitCallbackRegistered = $state(false)
|
|
749
|
+
let waitUnsubscribe: (() => void) | null = null
|
|
750
|
+
let waitHiddenDisplay: string | null = null
|
|
751
|
+
let waitEnterReleased = $state(false)
|
|
752
|
+
let waitLayoutParent: HTMLElement | null = null
|
|
753
|
+
let waitLayoutParentWidth = ''
|
|
754
|
+
let waitLayoutParentHeight = ''
|
|
755
|
+
let waitLayoutViewportScrollX = 0
|
|
756
|
+
let waitLayoutViewportScrollY = 0
|
|
757
|
+
const presenceLayoutHoldAttribute = 'data-presence-layout-hold'
|
|
758
|
+
const presenceLayoutReleaseEvent = 'svelte-motion:presence-layout-release'
|
|
759
|
+
const waitEnterBlockedBeforeMount = $derived(
|
|
760
|
+
context?.mode === 'wait' && !waitEnterReleased && context.isEnterBlocked(presenceKey)
|
|
761
|
+
)
|
|
547
762
|
|
|
548
763
|
// Derived attributes to keep both branches in sync (focusability, data flags, style, class)
|
|
549
764
|
const derivedAttrs = $derived<Record<string, unknown>>({
|
|
@@ -553,7 +768,8 @@
|
|
|
553
768
|
// key, empty array) would otherwise add `tabindex=0` for an
|
|
554
769
|
// element that never actually receives a tap gesture — an
|
|
555
770
|
// unintended tab stop. (#349 CR feedback)
|
|
556
|
-
...(
|
|
771
|
+
...(hasGestureFeatures &&
|
|
772
|
+
isNotEmpty(resolvedWhileTap) &&
|
|
557
773
|
!isNativelyFocusable(tag, rest as Record<string, unknown>) &&
|
|
558
774
|
((rest as Record<string, unknown>)?.tabindex ??
|
|
559
775
|
(rest as Record<string, unknown>)?.tabIndex ??
|
|
@@ -567,6 +783,16 @@
|
|
|
567
783
|
'data-path': dataPath
|
|
568
784
|
}
|
|
569
785
|
: {}),
|
|
786
|
+
...(renderedOptimizedAppearScript
|
|
787
|
+
? { [optimizedAppearDataAttribute]: optimizedAppearId }
|
|
788
|
+
: {}),
|
|
789
|
+
...(layoutProp
|
|
790
|
+
? { 'data-layout': String(layoutProp), 'data-svelte-motion-layout': '' }
|
|
791
|
+
: {}),
|
|
792
|
+
...(scopedLayoutId ? { 'data-layout-id': scopedLayoutId } : {}),
|
|
793
|
+
...(waitEnterBlockedBeforeMount || waitHiddenDisplay !== null
|
|
794
|
+
? { 'data-presence-wait-hidden': 'true' }
|
|
795
|
+
: {}),
|
|
570
796
|
// Apply normalized SVG path attributes synchronously on first render to avoid flash
|
|
571
797
|
// Compute via svg utils (no dynamic import in SSR/derived expressions)
|
|
572
798
|
...(() => {
|
|
@@ -580,9 +806,7 @@
|
|
|
580
806
|
return {}
|
|
581
807
|
})(),
|
|
582
808
|
style: mergeInlineStyles(
|
|
583
|
-
initialKeyframes && 'pathLength' in initialKeyframes && isLoaded === 'mounting'
|
|
584
|
-
? `${styleProp || ''};visibility:hidden`
|
|
585
|
-
: styleProp,
|
|
809
|
+
`${initialKeyframes && 'pathLength' in initialKeyframes && isLoaded === 'mounting' ? `${styleProp || ''};visibility:hidden` : (styleProp ?? '')}${waitEnterBlockedBeforeMount || waitHiddenDisplay !== null ? ';display:none' : ''}`,
|
|
586
810
|
// The "from" slot: apply initialKeyframes as inline styles during
|
|
587
811
|
// the mounting/initial phases (before the WAAPI animation locks
|
|
588
812
|
// its from-value and we promote to 'ready' — see the lifecycle
|
|
@@ -626,7 +850,7 @@
|
|
|
626
850
|
// after any non-zero duration settle animation.
|
|
627
851
|
let teardownDrag: (() => void) | null = null
|
|
628
852
|
$effect(() => {
|
|
629
|
-
if (!(element && isLoaded === 'ready')) return
|
|
853
|
+
if (!(element && isLoaded === 'ready' && hasDragFeatures)) return
|
|
630
854
|
// Only attach if drag enabled
|
|
631
855
|
if (!dragProp) return
|
|
632
856
|
// Clean up previous
|
|
@@ -787,7 +1011,7 @@
|
|
|
787
1011
|
isLoaded
|
|
788
1012
|
})
|
|
789
1013
|
}
|
|
790
|
-
if (!element) return
|
|
1014
|
+
if (!element || !hasGestureFeatures) return
|
|
791
1015
|
// Defer attachment until the element has settled out of the enter
|
|
792
1016
|
// animation phase — matches the gate every other gesture effect
|
|
793
1017
|
// in this file uses (drag, whileTap, whileHover, whileFocus,
|
|
@@ -861,7 +1085,17 @@
|
|
|
861
1085
|
}
|
|
862
1086
|
|
|
863
1087
|
const transitionAnimate: MotionTransition = mergedTransition ?? {}
|
|
864
|
-
|
|
1088
|
+
const rawPayload = filterReducedMotionKeyframes(
|
|
1089
|
+
$state.snapshot(resolvedAnimate) as Record<string, unknown>,
|
|
1090
|
+
reducedMotion
|
|
1091
|
+
) as Record<string, unknown>
|
|
1092
|
+
const svgPathFinished =
|
|
1093
|
+
isSVGPathElement(element) && hasSVGPathProperties(rawPayload)
|
|
1094
|
+
? animateSVGPathAttributes(element, rawPayload, transitionAnimate)
|
|
1095
|
+
: []
|
|
1096
|
+
let payload = (
|
|
1097
|
+
svgPathFinished.length > 0 ? stripSVGPathKeyframes(rawPayload) : rawPayload
|
|
1098
|
+
) as typeof rawPayload
|
|
865
1099
|
|
|
866
1100
|
// Transform SVG path properties (pathLength, pathOffset) to their CSS equivalents
|
|
867
1101
|
payload = transformSVGPathProperties(
|
|
@@ -869,13 +1103,6 @@
|
|
|
869
1103
|
payload as Record<string, unknown>
|
|
870
1104
|
) as typeof payload
|
|
871
1105
|
|
|
872
|
-
// Strip transform keys when reduced-motion is active so the element
|
|
873
|
-
// stays in place while opacity / color etc. still animate.
|
|
874
|
-
payload = filterReducedMotionKeyframes(
|
|
875
|
-
payload as Record<string, unknown>,
|
|
876
|
-
reducedMotion
|
|
877
|
-
) as typeof payload
|
|
878
|
-
|
|
879
1106
|
// Ensure dash properties aren't pinned as inline styles
|
|
880
1107
|
if (element && (element as HTMLElement).style) {
|
|
881
1108
|
;(element as HTMLElement).style.removeProperty('stroke-dasharray')
|
|
@@ -889,33 +1116,134 @@
|
|
|
889
1116
|
|
|
890
1117
|
// A fresh run owns the transform again until it completes.
|
|
891
1118
|
enterAnimationSettled = false
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
1119
|
+
const completeEnterAnimation = (
|
|
1120
|
+
def: DOMKeyframesDefinition | undefined = payload as unknown as DOMKeyframesDefinition
|
|
1121
|
+
) => {
|
|
1122
|
+
if (enterAnimationSettled) return
|
|
1123
|
+
// Now the target is the resting state — promote it to the
|
|
1124
|
+
// inline baseline so it persists after WAAPI surrenders the
|
|
1125
|
+
// property (default fill:'none'). (#377)
|
|
1126
|
+
applyAnimateRestingStyle()
|
|
1127
|
+
enterAnimationSettled = true
|
|
1128
|
+
onAnimationCompleteProp?.(def)
|
|
1129
|
+
}
|
|
1130
|
+
if (isNotEmpty(payload)) {
|
|
1131
|
+
animateWithLifecycle(
|
|
1132
|
+
element,
|
|
1133
|
+
payload as unknown as DOMKeyframesDefinition,
|
|
1134
|
+
transitionAnimate as unknown as AnimationOptions,
|
|
1135
|
+
(def) =>
|
|
1136
|
+
onAnimationStartProp?.(def as unknown as DOMKeyframesDefinition | undefined),
|
|
1137
|
+
(def) =>
|
|
1138
|
+
completeEnterAnimation(def as unknown as DOMKeyframesDefinition | undefined)
|
|
1139
|
+
)
|
|
1140
|
+
} else if (svgPathFinished.length > 0) {
|
|
1141
|
+
onAnimationStartProp?.(rawPayload as unknown as DOMKeyframesDefinition)
|
|
1142
|
+
Promise.all(svgPathFinished)
|
|
1143
|
+
.then(() => completeEnterAnimation(rawPayload as unknown as DOMKeyframesDefinition))
|
|
1144
|
+
.catch(() =>
|
|
1145
|
+
completeEnterAnimation(rawPayload as unknown as DOMKeyframesDefinition)
|
|
1146
|
+
)
|
|
1147
|
+
}
|
|
1148
|
+
if (isJsdomRuntime()) {
|
|
1149
|
+
window.setTimeout(
|
|
1150
|
+
() => completeEnterAnimation(),
|
|
1151
|
+
getTransitionFallbackMs(transitionAnimate as AnimationOptions)
|
|
1152
|
+
)
|
|
1153
|
+
}
|
|
905
1154
|
}
|
|
906
1155
|
|
|
907
|
-
// Track if we've already registered a wait callback to prevent duplicates
|
|
908
|
-
let waitCallbackRegistered = $state(false)
|
|
909
|
-
let waitUnsubscribe: (() => void) | null = null
|
|
910
|
-
|
|
911
1156
|
// Cleanup wait callback on component unmount to prevent memory leaks
|
|
912
1157
|
$effect(() => {
|
|
913
1158
|
return () => {
|
|
1159
|
+
if (element && waitHiddenDisplay !== null) {
|
|
1160
|
+
element.style.display = waitHiddenDisplay
|
|
1161
|
+
element.removeAttribute('data-presence-wait-hidden')
|
|
1162
|
+
waitHiddenDisplay = null
|
|
1163
|
+
}
|
|
1164
|
+
releaseWaitLayoutHold()
|
|
914
1165
|
waitUnsubscribe?.()
|
|
915
1166
|
waitUnsubscribe = null
|
|
916
1167
|
}
|
|
917
1168
|
})
|
|
918
1169
|
|
|
1170
|
+
const getPresenceLayoutParent = (): HTMLElement | null => {
|
|
1171
|
+
let parent = element?.parentElement ?? null
|
|
1172
|
+
const layoutParent = element?.parentElement?.closest<HTMLElement>(
|
|
1173
|
+
'[data-svelte-motion-layout]'
|
|
1174
|
+
)
|
|
1175
|
+
if (layoutParent) return layoutParent
|
|
1176
|
+
|
|
1177
|
+
while (parent && getComputedStyle(parent).display === 'contents') {
|
|
1178
|
+
parent = parent.parentElement
|
|
1179
|
+
}
|
|
1180
|
+
return parent
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
const holdWaitLayout = () => {
|
|
1184
|
+
if (!element || waitLayoutParent) return
|
|
1185
|
+
const parent = getPresenceLayoutParent()
|
|
1186
|
+
if (!parent) return
|
|
1187
|
+
|
|
1188
|
+
const rect = parent.getBoundingClientRect()
|
|
1189
|
+
waitLayoutParent = parent
|
|
1190
|
+
waitLayoutParentWidth = parent.style.width
|
|
1191
|
+
waitLayoutParentHeight = parent.style.height
|
|
1192
|
+
waitLayoutViewportScrollX = typeof window !== 'undefined' ? window.scrollX : 0
|
|
1193
|
+
waitLayoutViewportScrollY = typeof window !== 'undefined' ? window.scrollY : 0
|
|
1194
|
+
parent.setAttribute(presenceLayoutHoldAttribute, 'true')
|
|
1195
|
+
parent.style.width = `${rect.width}px`
|
|
1196
|
+
parent.style.height = `${rect.height}px`
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
function releaseWaitLayoutHold() {
|
|
1200
|
+
if (!waitLayoutParent) return
|
|
1201
|
+
const parent = waitLayoutParent
|
|
1202
|
+
const previousRect = parent.getBoundingClientRect()
|
|
1203
|
+
parent.removeAttribute(presenceLayoutHoldAttribute)
|
|
1204
|
+
if (waitLayoutParentWidth) {
|
|
1205
|
+
parent.style.width = waitLayoutParentWidth
|
|
1206
|
+
} else {
|
|
1207
|
+
parent.style.removeProperty('width')
|
|
1208
|
+
}
|
|
1209
|
+
if (waitLayoutParentHeight) {
|
|
1210
|
+
parent.style.height = waitLayoutParentHeight
|
|
1211
|
+
} else {
|
|
1212
|
+
parent.style.removeProperty('height')
|
|
1213
|
+
}
|
|
1214
|
+
const viewportScrolledDuringHold =
|
|
1215
|
+
typeof window !== 'undefined' &&
|
|
1216
|
+
(window.scrollX !== waitLayoutViewportScrollX ||
|
|
1217
|
+
window.scrollY !== waitLayoutViewportScrollY)
|
|
1218
|
+
parent.dispatchEvent(
|
|
1219
|
+
new CustomEvent(presenceLayoutReleaseEvent, {
|
|
1220
|
+
detail: {
|
|
1221
|
+
previousRect: domRectToRectLike(previousRect),
|
|
1222
|
+
viewportScrolledDuringHold
|
|
1223
|
+
}
|
|
1224
|
+
})
|
|
1225
|
+
)
|
|
1226
|
+
waitLayoutParent = null
|
|
1227
|
+
waitLayoutParentWidth = ''
|
|
1228
|
+
waitLayoutParentHeight = ''
|
|
1229
|
+
waitLayoutViewportScrollX = 0
|
|
1230
|
+
waitLayoutViewportScrollY = 0
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
const revealWaitHiddenElement = () => {
|
|
1234
|
+
waitEnterReleased = true
|
|
1235
|
+
if (waitHiddenDisplay !== null && element) {
|
|
1236
|
+
if (waitHiddenDisplay) {
|
|
1237
|
+
element.style.display = waitHiddenDisplay
|
|
1238
|
+
} else {
|
|
1239
|
+
element.style.removeProperty('display')
|
|
1240
|
+
}
|
|
1241
|
+
element.removeAttribute('data-presence-wait-hidden')
|
|
1242
|
+
waitHiddenDisplay = null
|
|
1243
|
+
}
|
|
1244
|
+
releaseWaitLayoutHold()
|
|
1245
|
+
}
|
|
1246
|
+
|
|
919
1247
|
/**
|
|
920
1248
|
* Run the enter animation, respecting wait mode if inside AnimatePresence.
|
|
921
1249
|
* Returns true if animation was deferred (wait mode with blocked enters).
|
|
@@ -941,12 +1269,21 @@
|
|
|
941
1269
|
return true // Still deferred
|
|
942
1270
|
}
|
|
943
1271
|
|
|
944
|
-
const blocked = context.isEnterBlocked?.()
|
|
1272
|
+
const blocked = context.isEnterBlocked?.(presenceKey)
|
|
945
1273
|
pwLog('[motion] runAnimation: wait mode', { blocked })
|
|
946
1274
|
|
|
947
1275
|
if (blocked) {
|
|
948
1276
|
pwLog('[motion] runAnimation: enters blocked, deferring')
|
|
949
1277
|
|
|
1278
|
+
waitEnterReleased = false
|
|
1279
|
+
if (waitHiddenDisplay === null) {
|
|
1280
|
+
waitHiddenDisplay =
|
|
1281
|
+
element.style.display === 'none' ? '' : element.style.display
|
|
1282
|
+
element.style.display = 'none'
|
|
1283
|
+
element.setAttribute('data-presence-wait-hidden', 'true')
|
|
1284
|
+
holdWaitLayout()
|
|
1285
|
+
}
|
|
1286
|
+
|
|
950
1287
|
waitCallbackRegistered = true
|
|
951
1288
|
|
|
952
1289
|
// Register callback to run animation when unblocked
|
|
@@ -956,6 +1293,12 @@
|
|
|
956
1293
|
waitUnsubscribe = null
|
|
957
1294
|
waitCallbackRegistered = false
|
|
958
1295
|
|
|
1296
|
+
// Reveal synchronously after the exiting placeholder has
|
|
1297
|
+
// been removed. The parent is fixed-size until the next
|
|
1298
|
+
// frame, so it measures the final entrant instead of an
|
|
1299
|
+
// overlap between exiting and entering content.
|
|
1300
|
+
revealWaitHiddenElement()
|
|
1301
|
+
|
|
959
1302
|
// Snap to initial state first (in case inline styles were removed)
|
|
960
1303
|
if (initialKeyframes && element) {
|
|
961
1304
|
const transformedInitial = transformSVGPathProperties(
|
|
@@ -987,6 +1330,11 @@
|
|
|
987
1330
|
})
|
|
988
1331
|
return true // Animation was deferred
|
|
989
1332
|
}
|
|
1333
|
+
|
|
1334
|
+
if (waitHiddenDisplay !== null || waitEnterBlockedBeforeMount) {
|
|
1335
|
+
pwLog('[motion] runAnimation: wait mode no longer blocked, revealing')
|
|
1336
|
+
revealWaitHiddenElement()
|
|
1337
|
+
}
|
|
990
1338
|
}
|
|
991
1339
|
|
|
992
1340
|
// Not blocked - run animation immediately
|
|
@@ -1010,6 +1358,7 @@
|
|
|
1010
1358
|
let objectAnimateRanOnMount = $state(false)
|
|
1011
1359
|
// Track the serialized animateProp to detect changes for object animate props
|
|
1012
1360
|
let lastAnimatePropJson = $state<string | undefined>(undefined)
|
|
1361
|
+
let motionDomProjectionUpdatePending = false
|
|
1013
1362
|
const currentAnimateKey = $derived(
|
|
1014
1363
|
typeof animateProp === 'string'
|
|
1015
1364
|
? animateProp
|
|
@@ -1018,6 +1367,72 @@
|
|
|
1018
1367
|
: undefined
|
|
1019
1368
|
)
|
|
1020
1369
|
|
|
1370
|
+
$effect(() => {
|
|
1371
|
+
if (!motionDomProjection) return
|
|
1372
|
+
motionDomProjection.updateOptions({
|
|
1373
|
+
layout: layoutProp,
|
|
1374
|
+
layoutId: scopedLayoutId,
|
|
1375
|
+
transition: mergedTransition as never,
|
|
1376
|
+
style: styleProp
|
|
1377
|
+
})
|
|
1378
|
+
})
|
|
1379
|
+
|
|
1380
|
+
$effect(() => {
|
|
1381
|
+
if (!motionDomProjection) return
|
|
1382
|
+
if (!element) return
|
|
1383
|
+
motionDomProjection.mount(element)
|
|
1384
|
+
return () => {
|
|
1385
|
+
motionDomProjection.unmount()
|
|
1386
|
+
}
|
|
1387
|
+
})
|
|
1388
|
+
|
|
1389
|
+
let explicitLayoutSnapshot: RectLike | null = null
|
|
1390
|
+
let lastRect: RectLike | null = null
|
|
1391
|
+
const trackLayoutProjectionDependencies = () => [
|
|
1392
|
+
classProp,
|
|
1393
|
+
styleProp,
|
|
1394
|
+
scopedLayoutId,
|
|
1395
|
+
mergedTransition
|
|
1396
|
+
]
|
|
1397
|
+
|
|
1398
|
+
$effect.pre(() => {
|
|
1399
|
+
const shouldProject = element && layoutProp && isLoaded === 'ready' && hasLayoutFeatures
|
|
1400
|
+
// Track common layout-affecting props so Svelte-owned updates can
|
|
1401
|
+
// snapshot before the DOM patch, matching upstream MeasureLayout.
|
|
1402
|
+
trackLayoutProjectionDependencies()
|
|
1403
|
+
|
|
1404
|
+
if (!shouldProject) {
|
|
1405
|
+
explicitLayoutSnapshot = null
|
|
1406
|
+
return
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
explicitLayoutSnapshot = measureLayoutRect()
|
|
1410
|
+
projection.willUpdate()
|
|
1411
|
+
motionDomProjection?.willUpdate()
|
|
1412
|
+
motionDomProjectionUpdatePending = true
|
|
1413
|
+
})
|
|
1414
|
+
|
|
1415
|
+
$effect(() => {
|
|
1416
|
+
const shouldProject = element && layoutProp && isLoaded === 'ready' && hasLayoutFeatures
|
|
1417
|
+
trackLayoutProjectionDependencies()
|
|
1418
|
+
|
|
1419
|
+
if (!shouldProject || !motionDomProjectionUpdatePending) return
|
|
1420
|
+
motionDomProjectionUpdatePending = false
|
|
1421
|
+
const previous = explicitLayoutSnapshot
|
|
1422
|
+
explicitLayoutSnapshot = null
|
|
1423
|
+
if (previous) {
|
|
1424
|
+
const next = measureLayoutRect()
|
|
1425
|
+
if (next) {
|
|
1426
|
+
const flipLayoutMode = layoutProp === 'position' ? 'position' : true
|
|
1427
|
+
const transforms = computeFlipTransforms(previous, next, flipLayoutMode)
|
|
1428
|
+
runFlipAnimation(element!, transforms, (mergedTransition ?? {}) as AnimationOptions)
|
|
1429
|
+
lastRect = next
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
projection.didUpdate()
|
|
1433
|
+
motionDomProjection?.didUpdate()
|
|
1434
|
+
})
|
|
1435
|
+
|
|
1021
1436
|
// Projection node lifecycle + the `onProjectionUpdate` listener.
|
|
1022
1437
|
// Mount once the element binds; seed the baseline layout; unmount on
|
|
1023
1438
|
// cleanup. Depends ONLY on `element` — the `onProjectionUpdate`
|
|
@@ -1045,56 +1460,104 @@
|
|
|
1045
1460
|
}
|
|
1046
1461
|
})
|
|
1047
1462
|
|
|
1048
|
-
//
|
|
1049
|
-
//
|
|
1050
|
-
//
|
|
1051
|
-
|
|
1463
|
+
// Upstream layout projection via motion-dom. Svelte runes mode doesn't
|
|
1464
|
+
// expose the React-style pre/post render hook pair used upstream, so the
|
|
1465
|
+
// component snapshots committed layout changes through DOM observers while
|
|
1466
|
+
// keeping the existing local ProjectionNode event fan-out alive.
|
|
1052
1467
|
$effect(() => {
|
|
1053
|
-
if (!(element && layoutProp && isLoaded === 'ready')) return
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
// motion-applied (FLIP/drag) transform up the tree. Without this a
|
|
1060
|
-
// child re-measures and FLIPs whenever an ancestor's transform
|
|
1061
|
-
// animates, even though the child's own layout never moved. (#379)
|
|
1468
|
+
if (!(element && layoutProp && isLoaded === 'ready' && hasLayoutFeatures)) return
|
|
1469
|
+
|
|
1470
|
+
let rafId: number | null = null
|
|
1471
|
+
let wasViewportOffscreenSinceLastLayout = false
|
|
1472
|
+
const flipLayoutMode = layoutProp === 'position' ? 'position' : true
|
|
1473
|
+
motionDomProjection?.seedLayout()
|
|
1062
1474
|
lastRect = measureLayoutRect()
|
|
1063
|
-
// Hint compositor for smoother FLIP transforms
|
|
1064
1475
|
setCompositorHints(element!, true)
|
|
1065
1476
|
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1477
|
+
const rememberOffscreenScroll = () => {
|
|
1478
|
+
if (isViewportOffscreen(element!.getBoundingClientRect())) {
|
|
1479
|
+
wasViewportOffscreenSinceLastLayout = true
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
const commitObservedLayout = () => {
|
|
1484
|
+
if (element!.hasAttribute('data-layout-size-animation')) {
|
|
1485
|
+
return
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
const hasPresenceHold = element!.hasAttribute(presenceLayoutHoldAttribute)
|
|
1489
|
+
const hasHiddenWaitEnter = !!element!.querySelector(
|
|
1490
|
+
'[data-presence-wait-hidden="true"]'
|
|
1491
|
+
)
|
|
1492
|
+
const hasPresencePlaceholder = !!element!.querySelector(
|
|
1493
|
+
'[data-presence-placeholder="true"]'
|
|
1494
|
+
)
|
|
1495
|
+
|
|
1496
|
+
if (hasPresenceHold || hasHiddenWaitEnter) {
|
|
1497
|
+
return
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
if (hasPresencePlaceholder) {
|
|
1501
|
+
lastRect = measureLayoutRect()
|
|
1502
|
+
motionDomProjection?.seedLayout()
|
|
1503
|
+
return
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1073
1506
|
const nextBox = projection.commitLayoutChange()
|
|
1074
|
-
if (
|
|
1075
|
-
|
|
1076
|
-
|
|
1507
|
+
if (nextBox && lastRect) {
|
|
1508
|
+
const next = boxToRectLike(nextBox)
|
|
1509
|
+
const transforms = computeFlipTransforms(lastRect, next, flipLayoutMode)
|
|
1510
|
+
runFlipAnimation(element!, transforms, (mergedTransition ?? {}) as AnimationOptions)
|
|
1077
1511
|
lastRect = next
|
|
1078
|
-
|
|
1512
|
+
wasViewportOffscreenSinceLastLayout = false
|
|
1513
|
+
} else if (nextBox) {
|
|
1514
|
+
lastRect = boxToRectLike(nextBox)
|
|
1515
|
+
wasViewportOffscreenSinceLastLayout = false
|
|
1079
1516
|
}
|
|
1080
|
-
|
|
1081
|
-
|
|
1517
|
+
motionDomProjection?.commitObservedLayoutChange()
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
const commitPresenceLayoutRelease = (event: Event) => {
|
|
1521
|
+
const detail = (
|
|
1522
|
+
event as CustomEvent<{
|
|
1523
|
+
previousRect?: RectLike
|
|
1524
|
+
viewportScrolledDuringHold?: boolean
|
|
1525
|
+
}>
|
|
1526
|
+
).detail
|
|
1527
|
+
const previous = detail?.previousRect
|
|
1528
|
+
const viewportRect = element!.getBoundingClientRect()
|
|
1529
|
+
const next = measureLayoutRect()
|
|
1530
|
+
if (!(previous && next)) return
|
|
1531
|
+
|
|
1082
1532
|
lastRect = next
|
|
1533
|
+
const shouldSkipLayoutAnimation =
|
|
1534
|
+
detail?.viewportScrolledDuringHold ||
|
|
1535
|
+
wasViewportOffscreenSinceLastLayout ||
|
|
1536
|
+
isViewportOffscreen(viewportRect)
|
|
1537
|
+
if (!shouldSkipLayoutAnimation) {
|
|
1538
|
+
const transforms = computeFlipTransforms(previous, next, flipLayoutMode)
|
|
1539
|
+
runFlipAnimation(element!, transforms, (mergedTransition ?? {}) as AnimationOptions)
|
|
1540
|
+
}
|
|
1541
|
+
wasViewportOffscreenSinceLastLayout = false
|
|
1542
|
+
motionDomProjection?.commitObservedLayoutChange()
|
|
1083
1543
|
}
|
|
1084
1544
|
|
|
1085
|
-
const
|
|
1086
|
-
if (rafId)
|
|
1545
|
+
const scheduleProjectionCommit = () => {
|
|
1546
|
+
if (rafId) return
|
|
1547
|
+
commitObservedLayout()
|
|
1087
1548
|
rafId = requestAnimationFrame(() => {
|
|
1088
1549
|
rafId = null
|
|
1089
|
-
runFlip()
|
|
1090
1550
|
})
|
|
1091
1551
|
}
|
|
1092
|
-
const disconnectObservers = observeLayoutChanges(element!, () =>
|
|
1552
|
+
const disconnectObservers = observeLayoutChanges(element!, () => scheduleProjectionCommit())
|
|
1553
|
+
window.addEventListener('scroll', rememberOffscreenScroll, { passive: true })
|
|
1554
|
+
element!.addEventListener(presenceLayoutReleaseEvent, commitPresenceLayoutRelease)
|
|
1093
1555
|
|
|
1094
1556
|
return () => {
|
|
1095
1557
|
disconnectObservers()
|
|
1558
|
+
window.removeEventListener('scroll', rememberOffscreenScroll)
|
|
1559
|
+
element?.removeEventListener(presenceLayoutReleaseEvent, commitPresenceLayoutRelease)
|
|
1096
1560
|
lastRect = null
|
|
1097
|
-
// Reset compositor hints on teardown
|
|
1098
1561
|
if (element) {
|
|
1099
1562
|
setCompositorHints(element, false)
|
|
1100
1563
|
}
|
|
@@ -1105,7 +1568,16 @@
|
|
|
1105
1568
|
// Shared layout animation via layoutId.
|
|
1106
1569
|
// On mount, consume the previous snapshot and FLIP from its position.
|
|
1107
1570
|
$effect(() => {
|
|
1108
|
-
if (
|
|
1571
|
+
if (
|
|
1572
|
+
!(
|
|
1573
|
+
element &&
|
|
1574
|
+
scopedLayoutId &&
|
|
1575
|
+
layoutIdRegistry &&
|
|
1576
|
+
isLoaded === 'ready' &&
|
|
1577
|
+
hasLayoutFeatures
|
|
1578
|
+
)
|
|
1579
|
+
)
|
|
1580
|
+
return
|
|
1109
1581
|
|
|
1110
1582
|
const prev = layoutIdRegistry.consume(scopedLayoutId)
|
|
1111
1583
|
if (!prev) return // First appearance, no animation needed
|
|
@@ -1123,7 +1595,10 @@
|
|
|
1123
1595
|
|
|
1124
1596
|
// whileTap handling via motion-dom's press()
|
|
1125
1597
|
$effect(() => {
|
|
1126
|
-
if (
|
|
1598
|
+
if (
|
|
1599
|
+
!(element && isLoaded === 'ready' && hasGestureFeatures && isNotEmpty(resolvedWhileTap))
|
|
1600
|
+
)
|
|
1601
|
+
return
|
|
1127
1602
|
return attachWhileTap(
|
|
1128
1603
|
element!,
|
|
1129
1604
|
(resolvedWhileTap ?? {}) as Record<string, unknown>,
|
|
@@ -1143,7 +1618,15 @@
|
|
|
1143
1618
|
|
|
1144
1619
|
// whileHover handling, gated to true-hover devices to avoid sticky states on touch
|
|
1145
1620
|
$effect(() => {
|
|
1146
|
-
if (
|
|
1621
|
+
if (
|
|
1622
|
+
!(
|
|
1623
|
+
element &&
|
|
1624
|
+
isLoaded === 'ready' &&
|
|
1625
|
+
hasGestureFeatures &&
|
|
1626
|
+
isNotEmpty(resolvedWhileHover)
|
|
1627
|
+
)
|
|
1628
|
+
)
|
|
1629
|
+
return
|
|
1147
1630
|
return attachWhileHover(
|
|
1148
1631
|
element!,
|
|
1149
1632
|
(resolvedWhileHover ?? {}) as Record<string, unknown>,
|
|
@@ -1158,7 +1641,15 @@
|
|
|
1158
1641
|
|
|
1159
1642
|
// whileFocus handling for keyboard focus interactions
|
|
1160
1643
|
$effect(() => {
|
|
1161
|
-
if (
|
|
1644
|
+
if (
|
|
1645
|
+
!(
|
|
1646
|
+
element &&
|
|
1647
|
+
isLoaded === 'ready' &&
|
|
1648
|
+
hasGestureFeatures &&
|
|
1649
|
+
isNotEmpty(resolvedWhileFocus)
|
|
1650
|
+
)
|
|
1651
|
+
)
|
|
1652
|
+
return
|
|
1162
1653
|
return attachWhileFocus(
|
|
1163
1654
|
element!,
|
|
1164
1655
|
(resolvedWhileFocus ?? {}) as Record<string, unknown>,
|
|
@@ -1173,7 +1664,15 @@
|
|
|
1173
1664
|
|
|
1174
1665
|
// whileInView handling for viewport intersection
|
|
1175
1666
|
$effect(() => {
|
|
1176
|
-
if (
|
|
1667
|
+
if (
|
|
1668
|
+
!(
|
|
1669
|
+
element &&
|
|
1670
|
+
isLoaded === 'ready' &&
|
|
1671
|
+
hasGestureFeatures &&
|
|
1672
|
+
isNotEmpty(resolvedWhileInView)
|
|
1673
|
+
)
|
|
1674
|
+
)
|
|
1675
|
+
return
|
|
1177
1676
|
return attachWhileInView(
|
|
1178
1677
|
element!,
|
|
1179
1678
|
(resolvedWhileInView ?? {}) as Record<string, unknown>,
|
|
@@ -1379,6 +1878,7 @@
|
|
|
1379
1878
|
|
|
1380
1879
|
$effect(() => {
|
|
1381
1880
|
if (!(element && isLoaded === 'mounting')) return
|
|
1881
|
+
markMotionMounted()
|
|
1382
1882
|
|
|
1383
1883
|
pwLog('[motion] main effect running', {
|
|
1384
1884
|
effectiveAnimate: !!effectiveAnimate,
|
|
@@ -1408,6 +1908,30 @@
|
|
|
1408
1908
|
dataPath = 5
|
|
1409
1909
|
isLoaded = 'ready'
|
|
1410
1910
|
} else if (isNotEmpty(initialKeyframes)) {
|
|
1911
|
+
const canHandoffOptimizedAppear = hasOptimizedAppearAnimation(optimizedAppearId)
|
|
1912
|
+
if (canHandoffOptimizedAppear) {
|
|
1913
|
+
pwLog('[motion] path: optimized appear handoff')
|
|
1914
|
+
dataPath = 6
|
|
1915
|
+
isLoaded = 'initial'
|
|
1916
|
+
initialAnimationTriggered = true
|
|
1917
|
+
if (animateProp && typeof animateProp !== 'string') {
|
|
1918
|
+
objectAnimateRanOnMount = true
|
|
1919
|
+
lastAnimatePropJson = JSON.stringify(animateProp)
|
|
1920
|
+
}
|
|
1921
|
+
finishOptimizedAppearAnimation(optimizedAppearId)
|
|
1922
|
+
.then(() => {
|
|
1923
|
+
applyAnimateRestingStyle()
|
|
1924
|
+
enterAnimationSettled = true
|
|
1925
|
+
isLoaded = 'ready'
|
|
1926
|
+
onAnimationCompleteProp?.(
|
|
1927
|
+
resolvedAnimate as DOMKeyframesDefinition | undefined
|
|
1928
|
+
)
|
|
1929
|
+
})
|
|
1930
|
+
.catch(() => {
|
|
1931
|
+
isLoaded = 'ready'
|
|
1932
|
+
})
|
|
1933
|
+
return
|
|
1934
|
+
}
|
|
1411
1935
|
pwLog('[motion] path: has initialKeyframes, will animate to target')
|
|
1412
1936
|
// Apply initial instantly BEFORE exposing 'initial' state
|
|
1413
1937
|
const transformedInitial = transformSVGPathProperties(
|
|
@@ -1528,15 +2052,23 @@
|
|
|
1528
2052
|
{#if isVoidTag}
|
|
1529
2053
|
{#if isSVGTag(String(tag))}
|
|
1530
2054
|
<svelte:element this={tag} bind:this={element} xmlns={SVG_NAMESPACE} {...derivedAttrs} />
|
|
2055
|
+
<!-- trunk-ignore(eslint/svelte/no-at-html-tags): optimized appear emits a JSON-escaped SSR bootstrap script, not user-authored HTML. -->
|
|
2056
|
+
{@html renderedOptimizedAppearScript}
|
|
1531
2057
|
{:else}
|
|
1532
2058
|
<svelte:element this={tag} bind:this={element} {...derivedAttrs} />
|
|
2059
|
+
<!-- trunk-ignore(eslint/svelte/no-at-html-tags): optimized appear emits a JSON-escaped SSR bootstrap script, not user-authored HTML. -->
|
|
2060
|
+
{@html renderedOptimizedAppearScript}
|
|
1533
2061
|
{/if}
|
|
1534
2062
|
{:else if isSVGTag(String(tag))}
|
|
1535
2063
|
<svelte:element this={tag} bind:this={element} xmlns={SVG_NAMESPACE} {...derivedAttrs}>
|
|
1536
2064
|
{@render children?.()}
|
|
1537
2065
|
</svelte:element>
|
|
2066
|
+
<!-- trunk-ignore(eslint/svelte/no-at-html-tags): optimized appear emits a JSON-escaped SSR bootstrap script, not user-authored HTML. -->
|
|
2067
|
+
{@html renderedOptimizedAppearScript}
|
|
1538
2068
|
{:else}
|
|
1539
2069
|
<svelte:element this={tag} bind:this={element} {...derivedAttrs}>
|
|
1540
2070
|
{@render children?.()}
|
|
1541
2071
|
</svelte:element>
|
|
2072
|
+
<!-- trunk-ignore(eslint/svelte/no-at-html-tags): optimized appear emits a JSON-escaped SSR bootstrap script, not user-authored HTML. -->
|
|
2073
|
+
{@html renderedOptimizedAppearScript}
|
|
1542
2074
|
{/if}
|