@hanzogui/animations-css 2.0.0-rc.41-hanzoai.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,702 @@
1
+ import {
2
+ normalizeTransition,
3
+ getAnimatedProperties,
4
+ hasAnimation as hasNormalizedAnimation,
5
+ getEffectiveAnimation,
6
+ getAnimationConfigsForKeys,
7
+ } from '@hanzogui/animation-helpers'
8
+ import { useIsomorphicLayoutEffect } from '@hanzogui/constants'
9
+ import { ResetPresence, usePresence } from '@hanzogui/use-presence'
10
+ import type { AnimationDriver, UniversalAnimatedNumber } from '@hanzogui/web'
11
+ import { transformsToString } from '@hanzogui/web'
12
+ import React, { useState } from 'react' // import { animate } from '@hanzogui/cubic-bezier-animator'
13
+
14
+ const EXTRACT_MS_REGEX = /(\d+(?:\.\d+)?)\s*ms/
15
+ const EXTRACT_S_REGEX = /(\d+(?:\.\d+)?)\s*s/
16
+
17
+ /**
18
+ * Helper function to extract duration from CSS animation string
19
+ * Examples: "ease-in 200ms" -> 200, "cubic-bezier(0.215, 0.610, 0.355, 1.000) 400ms" -> 400
20
+ * "ease-in 0.5s" -> 500, "slow 2s" -> 2000
21
+ */
22
+ function extractDuration(animation: string): number {
23
+ // Try to match milliseconds first
24
+ const msMatch = animation.match(EXTRACT_MS_REGEX)
25
+ if (msMatch) {
26
+ return Number.parseInt(msMatch[1], 10)
27
+ }
28
+
29
+ // Try to match seconds and convert to milliseconds
30
+ const sMatch = animation.match(EXTRACT_S_REGEX)
31
+ if (sMatch) {
32
+ return Math.round(Number.parseFloat(sMatch[1]) * 1000)
33
+ }
34
+
35
+ // Default to 300ms if no duration found
36
+ return 300
37
+ }
38
+
39
+ const MS_DURATION_REGEX = /(\d+(?:\.\d+)?)\s*ms/
40
+ const S_DURATION_REGEX = /(\d+(?:\.\d+)?)\s*s(?!tiffness)/
41
+
42
+ /**
43
+ * Apply duration override to a CSS animation string
44
+ * Replaces the existing duration with the override value
45
+ */
46
+ function applyDurationOverride(animation: string, durationMs: number): string {
47
+ // Replace ms duration
48
+ const msReplaced = animation.replace(MS_DURATION_REGEX, `${durationMs}ms`)
49
+ if (msReplaced !== animation) {
50
+ return msReplaced
51
+ }
52
+
53
+ // Replace seconds duration
54
+ const sReplaced = animation.replace(S_DURATION_REGEX, `${durationMs}ms`)
55
+ if (sReplaced !== animation) {
56
+ return sReplaced
57
+ }
58
+
59
+ // No duration found, prepend the duration
60
+ return `${durationMs}ms ${animation}`
61
+ }
62
+
63
+ // transform keys that need special handling
64
+ const TRANSFORM_KEYS = [
65
+ 'x',
66
+ 'y',
67
+ 'scale',
68
+ 'scaleX',
69
+ 'scaleY',
70
+ 'rotate',
71
+ 'rotateX',
72
+ 'rotateY',
73
+ 'rotateZ',
74
+ 'skewX',
75
+ 'skewY',
76
+ ] as const
77
+
78
+ /**
79
+ * Build a CSS transform string from a style object containing transform properties
80
+ */
81
+ function buildTransformString(style: Record<string, unknown> | undefined): string {
82
+ if (!style) return ''
83
+
84
+ const parts: string[] = []
85
+
86
+ if (style.x !== undefined || style.y !== undefined) {
87
+ const x = style.x ?? 0
88
+ const y = style.y ?? 0
89
+ parts.push(`translate(${x}px, ${y}px)`)
90
+ }
91
+ if (style.scale !== undefined) {
92
+ parts.push(`scale(${style.scale})`)
93
+ }
94
+ if (style.scaleX !== undefined) {
95
+ parts.push(`scaleX(${style.scaleX})`)
96
+ }
97
+ if (style.scaleY !== undefined) {
98
+ parts.push(`scaleY(${style.scaleY})`)
99
+ }
100
+ if (style.rotate !== undefined) {
101
+ const val = style.rotate
102
+ const unit = typeof val === 'string' && val.includes('deg') ? '' : 'deg'
103
+ parts.push(`rotate(${val}${unit})`)
104
+ }
105
+ if (style.rotateX !== undefined) {
106
+ parts.push(`rotateX(${style.rotateX}deg)`)
107
+ }
108
+ if (style.rotateY !== undefined) {
109
+ parts.push(`rotateY(${style.rotateY}deg)`)
110
+ }
111
+ if (style.rotateZ !== undefined) {
112
+ parts.push(`rotateZ(${style.rotateZ}deg)`)
113
+ }
114
+ if (style.skewX !== undefined) {
115
+ parts.push(`skewX(${style.skewX}deg)`)
116
+ }
117
+ if (style.skewY !== undefined) {
118
+ parts.push(`skewY(${style.skewY}deg)`)
119
+ }
120
+
121
+ return parts.join(' ')
122
+ }
123
+
124
+ /**
125
+ * Apply a style object to a DOM node, handling transform keys specially
126
+ */
127
+ function applyStylesToNode(
128
+ node: HTMLElement,
129
+ style: Record<string, unknown> | undefined
130
+ ): void {
131
+ if (!style) return
132
+
133
+ // collect transform values
134
+ const transformStr = buildTransformString(style)
135
+ if (transformStr) {
136
+ node.style.transform = transformStr
137
+ }
138
+
139
+ // apply non-transform properties
140
+ for (const [key, value] of Object.entries(style)) {
141
+ if (TRANSFORM_KEYS.includes(key as any)) continue
142
+ if (value === undefined) continue
143
+
144
+ if (key === 'opacity') {
145
+ node.style.opacity = String(value)
146
+ } else if (key === 'backgroundColor') {
147
+ node.style.backgroundColor = String(value)
148
+ } else if (key === 'color') {
149
+ node.style.color = String(value)
150
+ } else {
151
+ // generic fallback
152
+ node.style[key as any] = typeof value === 'number' ? `${value}px` : String(value)
153
+ }
154
+ }
155
+ }
156
+
157
+ export function createAnimations<A extends object>(animations: A): AnimationDriver<A> {
158
+ const reactionListeners = new WeakMap<any, Set<Function>>()
159
+
160
+ return {
161
+ animations,
162
+ usePresence,
163
+ ResetPresence,
164
+ inputStyle: 'css',
165
+ outputStyle: 'css',
166
+
167
+ useAnimatedNumber(initial): UniversalAnimatedNumber<Function> {
168
+ const [val, setVal] = React.useState(initial)
169
+ const finishTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)
170
+
171
+ return {
172
+ getInstance() {
173
+ return setVal
174
+ },
175
+ getValue() {
176
+ return val
177
+ },
178
+ setValue(next, config, onFinish) {
179
+ setVal(next)
180
+
181
+ // clear any pending finish callback from a previous setValue
182
+ if (finishTimerRef.current) {
183
+ clearTimeout(finishTimerRef.current)
184
+ finishTimerRef.current = null
185
+ }
186
+
187
+ if (onFinish) {
188
+ if (
189
+ !config ||
190
+ config.type === 'direct' ||
191
+ (config.type === 'timing' && config.duration === 0)
192
+ ) {
193
+ onFinish()
194
+ } else {
195
+ // estimate duration: use explicit duration, or fall back to
196
+ // default CSS transition duration for spring-type configs
197
+ const duration = config.type === 'timing' ? config.duration : 300
198
+ finishTimerRef.current = setTimeout(onFinish, duration)
199
+ }
200
+ }
201
+
202
+ // call reaction listeners with the new value
203
+ const listeners = reactionListeners.get(setVal)
204
+ if (listeners) {
205
+ listeners.forEach((listener) => listener(next))
206
+ }
207
+ },
208
+ stop() {
209
+ if (finishTimerRef.current) {
210
+ clearTimeout(finishTimerRef.current)
211
+ finishTimerRef.current = null
212
+ }
213
+ },
214
+ }
215
+ },
216
+
217
+ useAnimatedNumberReaction({ value }, onValue) {
218
+ React.useEffect(() => {
219
+ const instance = value.getInstance()
220
+ let queue = reactionListeners.get(instance)
221
+ if (!queue) {
222
+ const next = new Set<Function>()
223
+ reactionListeners.set(instance, next)
224
+ queue = next!
225
+ }
226
+ queue.add(onValue)
227
+ return () => {
228
+ queue?.delete(onValue)
229
+ }
230
+ }, [])
231
+ },
232
+
233
+ useAnimatedNumberStyle(val, getStyle) {
234
+ return getStyle(val.getValue())
235
+ },
236
+
237
+ useAnimatedNumbersStyle(vals, getStyle) {
238
+ return getStyle(...vals.map((v) => v.getValue()))
239
+ },
240
+
241
+ // @ts-ignore - styleState is added by createComponent
242
+ useAnimations: ({
243
+ props,
244
+ presence,
245
+ style,
246
+ componentState,
247
+ stateRef,
248
+ styleState,
249
+ }: any) => {
250
+ const isHydrating = componentState.unmounted === true
251
+ const isEntering = !!componentState.unmounted
252
+ const isExiting = presence?.[0] === false
253
+ const sendExitComplete = presence?.[1]
254
+
255
+ // Track if we just finished entering (transition from entering to not entering)
256
+ // This is needed because the CSS transition happens on the render AFTER t_unmounted is removed
257
+ const wasEnteringRef = React.useRef(isEntering)
258
+ const justFinishedEntering = wasEnteringRef.current && !isEntering
259
+ React.useEffect(() => {
260
+ wasEnteringRef.current = isEntering
261
+ })
262
+
263
+ // exit cycle guards to prevent stale/duplicate completion
264
+ const exitCycleIdRef = React.useRef(0)
265
+ const exitCompletedRef = React.useRef(false)
266
+ const wasExitingRef = React.useRef(false)
267
+ const exitInterruptedRef = React.useRef(false)
268
+
269
+ // detect transition into/out of exiting state
270
+ const justStartedExiting = isExiting && !wasExitingRef.current
271
+ const justStoppedExiting = !isExiting && wasExitingRef.current
272
+
273
+ // start new exit cycle only on transition INTO exiting
274
+ if (justStartedExiting) {
275
+ exitCycleIdRef.current++
276
+ exitCompletedRef.current = false
277
+ }
278
+ // track interruptions so we know to force-restart transitions
279
+ if (justStoppedExiting) {
280
+ exitCycleIdRef.current++
281
+ exitInterruptedRef.current = true
282
+ }
283
+
284
+ // track previous exiting state
285
+ React.useEffect(() => {
286
+ wasExitingRef.current = isExiting
287
+ })
288
+
289
+ // use effectiveTransition computed by createComponent (single source of truth)
290
+ const effectiveTransition = styleState?.effectiveTransition ?? props.transition
291
+
292
+ // Normalize the transition prop to a consistent format
293
+ const normalized = normalizeTransition(effectiveTransition)
294
+
295
+ // Determine animation state and get effective animation
296
+ // Use 'enter' if we're entering OR if we just finished entering (transition is happening)
297
+ const animationState = isExiting
298
+ ? 'exit'
299
+ : isEntering || justFinishedEntering
300
+ ? 'enter'
301
+ : 'default'
302
+ const effectiveAnimationKey = getEffectiveAnimation(normalized, animationState)
303
+ const defaultAnimation = effectiveAnimationKey
304
+ ? animations[effectiveAnimationKey]
305
+ : null
306
+ const animatedProperties = getAnimatedProperties(normalized)
307
+
308
+ // Determine which properties to animate
309
+ // - animateOnly prop is an exclusive filter (only animate those properties)
310
+ // - per-property configs WITHOUT a default = only animate those specific properties
311
+ // - per-property configs WITH a default = per-property overrides + default for rest
312
+ const hasDefault =
313
+ normalized.default !== null ||
314
+ normalized.enter !== null ||
315
+ normalized.exit !== null
316
+ const hasPerPropertyConfigs = animatedProperties.length > 0
317
+
318
+ let keys: string[]
319
+ if (props.animateOnly) {
320
+ // animateOnly is explicit filter
321
+ keys = props.animateOnly
322
+ } else if (hasPerPropertyConfigs && !hasDefault) {
323
+ // object format without default: { opacity: '200ms' } = only animate opacity
324
+ keys = animatedProperties
325
+ } else if (hasPerPropertyConfigs && hasDefault) {
326
+ // array format or object with default: 'all' first, then per-property overrides
327
+ // CSS transition specificity: later declarations override earlier ones for the same property
328
+ keys = ['all', ...animatedProperties]
329
+ } else {
330
+ // simple string format: 'quick' = animate all
331
+ keys = ['all']
332
+ }
333
+
334
+ useIsomorphicLayoutEffect(() => {
335
+ const host = stateRef.current.host
336
+ if (!sendExitComplete || !isExiting || !host) return
337
+ const node = host as HTMLElement
338
+
339
+ // capture current cycle id for this effect
340
+ const cycleId = exitCycleIdRef.current
341
+
342
+ // helper to complete exit with guards
343
+ const completeExit = () => {
344
+ if (cycleId !== exitCycleIdRef.current) return
345
+ if (exitCompletedRef.current) return
346
+ exitCompletedRef.current = true
347
+ sendExitComplete()
348
+ }
349
+
350
+ // if no properties to animate (animateOnly=[]), complete immediately
351
+ if (keys.length === 0) {
352
+ completeExit()
353
+ return
354
+ }
355
+
356
+ // Force transition restart for interrupted exits
357
+ // When an exit is interrupted and restarted, the element may already be at
358
+ // the exit style, so no CSS transition fires. We need to:
359
+ // 1. Reset to non-exit state
360
+ // 2. Force reflow
361
+ // 3. Re-apply exit state to trigger transition
362
+ let rafId: number | undefined
363
+ const wasInterrupted = exitInterruptedRef.current
364
+ // flag to ignore transitioncancel during reset (we intentionally cancel the old transition)
365
+ let ignoreCancelEvents = wasInterrupted
366
+ // get enter/exit styles for potential restart
367
+ const enterStyle = props.enterStyle as Record<string, unknown> | undefined
368
+ const exitStyle = props.exitStyle as Record<string, unknown> | undefined
369
+
370
+ // Build the exit transition string - needed for both normal and interrupted exits
371
+ const delayStr = normalized.delay ? ` ${normalized.delay}ms` : ''
372
+ const durationOverride = normalized.config?.duration
373
+ const exitTransitionString = keys
374
+ .map((key) => {
375
+ const propAnimation = normalized.properties[key]
376
+ let animationValue: string | null = null
377
+ if (typeof propAnimation === 'string') {
378
+ animationValue = animations[propAnimation]
379
+ } else if (
380
+ propAnimation &&
381
+ typeof propAnimation === 'object' &&
382
+ propAnimation.type
383
+ ) {
384
+ animationValue = animations[propAnimation.type]
385
+ } else if (defaultAnimation) {
386
+ animationValue = defaultAnimation
387
+ }
388
+ if (animationValue && durationOverride) {
389
+ animationValue = applyDurationOverride(animationValue, durationOverride)
390
+ }
391
+ return animationValue ? `${key} ${animationValue}${delayStr}` : null
392
+ })
393
+ .filter(Boolean)
394
+ .join(', ')
395
+
396
+ if (wasInterrupted) {
397
+ exitInterruptedRef.current = false
398
+ // disable transition, reset to enter state
399
+ node.style.transition = 'none'
400
+
401
+ // reset: apply active/open state for each exit property (not enterStyle,
402
+ // which may equal exitStyle — see comment in the normal exit path below)
403
+ if (exitStyle) {
404
+ const resetStyle: Record<string, unknown> = {}
405
+ for (const key of Object.keys(exitStyle)) {
406
+ if (key === 'opacity') {
407
+ resetStyle[key] = 1
408
+ } else if (TRANSFORM_KEYS.includes(key as any)) {
409
+ resetStyle[key] =
410
+ key === 'scale' || key === 'scaleX' || key === 'scaleY' ? 1 : 0
411
+ } else if (enterStyle?.[key] !== undefined) {
412
+ resetStyle[key] = enterStyle[key]
413
+ }
414
+ }
415
+ applyStylesToNode(node, resetStyle)
416
+ } else {
417
+ // fallback if no exitStyle defined
418
+ node.style.opacity = '1'
419
+ node.style.transform = 'none'
420
+ }
421
+
422
+ // force reflow
423
+ void node.offsetHeight
424
+ } else if (exitStyle) {
425
+ // For normal (non-interrupted) exits, we need to ensure the CSS transition is
426
+ // processed by the browser BEFORE the exitStyle takes effect. The issue is that
427
+ // React may have already applied exitStyle in the same render batch. To fix this:
428
+ // 1. Disable transition and reset to non-exit state
429
+ // 2. Force reflow so browser processes the reset
430
+ // 3. Use RAF to ensure we're in a new frame
431
+ // 4. Re-enable transition and apply exitStyle
432
+ // This mirrors the interrupted exit handling approach (which also uses RAF).
433
+ ignoreCancelEvents = true
434
+ node.style.transition = 'none'
435
+
436
+ // Reset to the active/open state (not enterStyle, which may equal exitStyle).
437
+ // enterStyle is the "unmounted" initial state and can share values with exitStyle
438
+ // (e.g., both have opacity: 0). resetting to enterStyle would mean no value change
439
+ // when exitStyle is applied, so the CSS transition wouldn't fire.
440
+ const resetStyle: Record<string, unknown> = {}
441
+ for (const key of Object.keys(exitStyle)) {
442
+ if (key === 'opacity') {
443
+ resetStyle[key] = 1
444
+ } else if (TRANSFORM_KEYS.includes(key as any)) {
445
+ resetStyle[key] =
446
+ key === 'scale' || key === 'scaleX' || key === 'scaleY' ? 1 : 0
447
+ } else if (enterStyle?.[key] !== undefined) {
448
+ resetStyle[key] = enterStyle[key]
449
+ }
450
+ }
451
+ applyStylesToNode(node, resetStyle)
452
+
453
+ // Force reflow
454
+ void node.offsetHeight
455
+
456
+ // Use RAF to ensure transition is applied in a new frame
457
+ rafId = requestAnimationFrame(() => {
458
+ if (cycleId !== exitCycleIdRef.current) return
459
+ // Re-enable transition
460
+ node.style.transition = exitTransitionString
461
+ // Force reflow to ensure transition is active
462
+ void node.offsetHeight
463
+ // Apply exit styles - this triggers the animation
464
+ applyStylesToNode(node, exitStyle)
465
+ // Re-enable cancel event handling
466
+ ignoreCancelEvents = false
467
+ })
468
+ }
469
+
470
+ /**
471
+ * Exit animation handling for Dialog/Modal components
472
+ *
473
+ * The Challenge: When users close dialogs (via Escape key or clicking outside),
474
+ * the element can disappear from the DOM before CSS transitions finish, which causes:
475
+ * 1. Dialogs to stick around on screen
476
+ * 2. Event handlers to stop working
477
+ *
478
+ * Fix: Calculate the MAXIMUM duration across all animated properties, not just
479
+ * the default. With animateOnly and per-property configs, different properties
480
+ * can have different durations, and we need to wait for the LONGEST one.
481
+ */
482
+
483
+ // calculate max duration across all animated properties
484
+ let maxDuration = defaultAnimation ? extractDuration(defaultAnimation) : 200
485
+
486
+ // check per-property animation durations using shared helper
487
+ const animationConfigs = getAnimationConfigsForKeys(
488
+ normalized,
489
+ animations as Record<string, string>,
490
+ keys,
491
+ defaultAnimation
492
+ )
493
+ for (const animationValue of animationConfigs.values()) {
494
+ if (animationValue) {
495
+ const duration = extractDuration(animationValue)
496
+ if (duration > maxDuration) {
497
+ maxDuration = duration
498
+ }
499
+ }
500
+ }
501
+
502
+ const delay = normalized.delay ?? 0
503
+ const fallbackTimeout = maxDuration + delay
504
+
505
+ const timeoutId = setTimeout(() => {
506
+ completeExit()
507
+ }, fallbackTimeout)
508
+
509
+ // track number of transitioning properties to wait for all to finish
510
+ // (each property fires its own transitionend event)
511
+ const transitioningProps = new Set(keys)
512
+ let completedCount = 0
513
+
514
+ const onFinishAnimation = (event: TransitionEvent) => {
515
+ // only count transitions on THIS element, not bubbled from children
516
+ if (event.target !== node) return
517
+
518
+ // map CSS property names to our key names
519
+ // e.g., transitionend fires with propertyName 'transform' for scale/x/y
520
+ const eventProp = event.propertyName
521
+ if (transitioningProps.has(eventProp) || eventProp === 'all') {
522
+ completedCount++
523
+ // wait for all properties to finish
524
+ if (completedCount >= transitioningProps.size) {
525
+ clearTimeout(timeoutId)
526
+ completeExit()
527
+ }
528
+ }
529
+ }
530
+
531
+ // on cancel, still complete (element is exiting and animation was interrupted)
532
+ // the guards prevent duplicate completion if this is a stale cycle
533
+ const onCancelAnimation = () => {
534
+ // ignore cancel events during reset phase (we intentionally cancel the old transition)
535
+ if (ignoreCancelEvents) return
536
+ clearTimeout(timeoutId)
537
+ completeExit()
538
+ }
539
+
540
+ node.addEventListener('transitionend', onFinishAnimation)
541
+ node.addEventListener('transitioncancel', onCancelAnimation)
542
+
543
+ // For interrupted exits, re-enable transition and re-apply exit styles
544
+ // This must happen AFTER listeners are set up so we catch the transitionend
545
+ if (wasInterrupted) {
546
+ rafId = requestAnimationFrame(() => {
547
+ if (cycleId !== exitCycleIdRef.current) return
548
+ // re-enable transition using the pre-built string
549
+ node.style.transition = exitTransitionString
550
+ // force reflow again
551
+ void node.offsetHeight
552
+ // now apply exit styles - this triggers the transition
553
+ applyStylesToNode(node, exitStyle)
554
+ // re-enable cancel event handling now that reset is complete
555
+ ignoreCancelEvents = false
556
+ })
557
+ }
558
+
559
+ return () => {
560
+ clearTimeout(timeoutId)
561
+ if (rafId !== undefined) cancelAnimationFrame(rafId)
562
+ node.removeEventListener('transitionend', onFinishAnimation)
563
+ node.removeEventListener('transitioncancel', onCancelAnimation)
564
+ // restore transition: the exit handling sets node.style.transition='none'
565
+ // directly on the DOM (bypassing React). if exit is interrupted (e.g. same-key
566
+ // re-entry in AnimatePresence), React won't re-apply its managed transition
567
+ // value because it hasn't changed in the virtual DOM. clearing the inline
568
+ // override lets React's value take effect again.
569
+ node.style.transition = ''
570
+ }
571
+ }, [
572
+ sendExitComplete,
573
+ isExiting,
574
+ stateRef,
575
+ keys,
576
+ normalized,
577
+ defaultAnimation,
578
+ props.enterStyle,
579
+ props.exitStyle,
580
+ ])
581
+
582
+ // hanzogui doesnt even use animation output during hydration
583
+ if (isHydrating) {
584
+ return null
585
+ }
586
+
587
+ // Check if we have any animation to apply
588
+ if (!hasNormalizedAnimation(normalized)) {
589
+ return null
590
+ }
591
+
592
+ if (Array.isArray(style.transform)) {
593
+ style.transform = transformsToString(style.transform)
594
+ }
595
+
596
+ // Build CSS transition string
597
+ // TODO: we disabled the transform transition, because it will create issue for inverse function and animate function
598
+ // for non layout transform properties either use animate function or find a workaround to do it with css
599
+ const delayStr = normalized.delay ? ` ${normalized.delay}ms` : ''
600
+ const durationOverride = normalized.config?.duration
601
+ style.transition = keys
602
+ .map((key) => {
603
+ // Check for property-specific animation, fall back to default
604
+ const propAnimation = normalized.properties[key]
605
+ let animationValue: string | null = null
606
+
607
+ if (typeof propAnimation === 'string') {
608
+ animationValue = animations[propAnimation]
609
+ } else if (
610
+ propAnimation &&
611
+ typeof propAnimation === 'object' &&
612
+ propAnimation.type
613
+ ) {
614
+ animationValue = animations[propAnimation.type]
615
+ } else if (defaultAnimation) {
616
+ animationValue = defaultAnimation
617
+ }
618
+
619
+ // Apply global duration override if specified
620
+ if (animationValue && durationOverride) {
621
+ animationValue = applyDurationOverride(animationValue, durationOverride)
622
+ }
623
+
624
+ return animationValue ? `${key} ${animationValue}${delayStr}` : null
625
+ })
626
+ .filter(Boolean)
627
+ .join(', ')
628
+
629
+ if (process.env.NODE_ENV === 'development' && props['debug'] === 'verbose') {
630
+ console.info('CSS animation', {
631
+ props,
632
+ animations,
633
+ normalized,
634
+ defaultAnimation,
635
+ style,
636
+ isEntering,
637
+ isExiting,
638
+ })
639
+ }
640
+
641
+ return { style, className: isEntering ? 't_unmounted' : '' }
642
+ },
643
+ }
644
+ }
645
+
646
+ // layout animations
647
+ // useIsomorphicLayoutEffect(() => {
648
+ // if (!host || !props.layout) {
649
+ // return
650
+ // }
651
+ // // @ts-ignore
652
+ // const boundingBox = host?.getBoundingClientRect()
653
+ // if (isChanged(initialPositionRef.current, boundingBox)) {
654
+ // const transform = invert(
655
+ // host,
656
+ // boundingBox,
657
+ // initialPositionRef.current
658
+ // )
659
+
660
+ // animate({
661
+ // from: transform,
662
+ // to: { x: 0, y: 0, scaleX: 1, scaleY: 1 },
663
+ // duration: 1000,
664
+ // onUpdate: ({ x, y, scaleX, scaleY }) => {
665
+ // // @ts-ignore
666
+ // host.style.transform = `translate(${x}px, ${y}px) scaleX(${scaleX}) scaleY(${scaleY})`
667
+ // // TODO: handle childRef inverse scale
668
+ // // childRef.current.style.transform = `scaleX(${1 / scaleX}) scaleY(${
669
+ // // 1 / scaleY
670
+ // // })`
671
+ // },
672
+ // // TODO: extract ease-in from string and convert/map it to a cubicBezier array
673
+ // cubicBezier: [0, 1.38, 1, -0.41],
674
+ // })
675
+ // }
676
+ // initialPositionRef.current = boundingBox
677
+ // })
678
+
679
+ // style.transition = `${keys} ${animation}${
680
+ // props.layout ? ',width 0s, height 0s, margin 0s, padding 0s, transform' : ''
681
+ // }`
682
+
683
+ // const isChanged = (initialBox: any, finalBox: any) => {
684
+ // // we just mounted, so we don't have complete data yet
685
+ // if (!initialBox || !finalBox) return false
686
+
687
+ // // deep compare the two boxes
688
+ // return JSON.stringify(initialBox) !== JSON.stringify(finalBox)
689
+ // }
690
+
691
+ // const invert = (el, from, to) => {
692
+ // const { x: fromX, y: fromY, width: fromWidth, height: fromHeight } = from
693
+ // const { x, y, width, height } = to
694
+
695
+ // const transform = {
696
+ // x: x - fromX - (fromWidth - width) / 2,
697
+ // y: y - fromY - (fromHeight - height) / 2,
698
+ // scaleX: width / fromWidth,
699
+ // scaleY: height / fromHeight,
700
+ // }
701
+
702
+ // el.style.transform = `
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './createAnimations'