@hanzogui/animations-css 2.0.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.
@@ -0,0 +1,698 @@
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
+ // @ts-ignore - styleState is added by createComponent
238
+ useAnimations: ({
239
+ props,
240
+ presence,
241
+ style,
242
+ componentState,
243
+ stateRef,
244
+ styleState,
245
+ }: any) => {
246
+ const isHydrating = componentState.unmounted === true
247
+ const isEntering = !!componentState.unmounted
248
+ const isExiting = presence?.[0] === false
249
+ const sendExitComplete = presence?.[1]
250
+
251
+ // Track if we just finished entering (transition from entering to not entering)
252
+ // This is needed because the CSS transition happens on the render AFTER t_unmounted is removed
253
+ const wasEnteringRef = React.useRef(isEntering)
254
+ const justFinishedEntering = wasEnteringRef.current && !isEntering
255
+ React.useEffect(() => {
256
+ wasEnteringRef.current = isEntering
257
+ })
258
+
259
+ // exit cycle guards to prevent stale/duplicate completion
260
+ const exitCycleIdRef = React.useRef(0)
261
+ const exitCompletedRef = React.useRef(false)
262
+ const wasExitingRef = React.useRef(false)
263
+ const exitInterruptedRef = React.useRef(false)
264
+
265
+ // detect transition into/out of exiting state
266
+ const justStartedExiting = isExiting && !wasExitingRef.current
267
+ const justStoppedExiting = !isExiting && wasExitingRef.current
268
+
269
+ // start new exit cycle only on transition INTO exiting
270
+ if (justStartedExiting) {
271
+ exitCycleIdRef.current++
272
+ exitCompletedRef.current = false
273
+ }
274
+ // track interruptions so we know to force-restart transitions
275
+ if (justStoppedExiting) {
276
+ exitCycleIdRef.current++
277
+ exitInterruptedRef.current = true
278
+ }
279
+
280
+ // track previous exiting state
281
+ React.useEffect(() => {
282
+ wasExitingRef.current = isExiting
283
+ })
284
+
285
+ // use effectiveTransition computed by createComponent (single source of truth)
286
+ const effectiveTransition = styleState?.effectiveTransition ?? props.transition
287
+
288
+ // Normalize the transition prop to a consistent format
289
+ const normalized = normalizeTransition(effectiveTransition)
290
+
291
+ // Determine animation state and get effective animation
292
+ // Use 'enter' if we're entering OR if we just finished entering (transition is happening)
293
+ const animationState = isExiting
294
+ ? 'exit'
295
+ : isEntering || justFinishedEntering
296
+ ? 'enter'
297
+ : 'default'
298
+ const effectiveAnimationKey = getEffectiveAnimation(normalized, animationState)
299
+ const defaultAnimation = effectiveAnimationKey
300
+ ? animations[effectiveAnimationKey]
301
+ : null
302
+ const animatedProperties = getAnimatedProperties(normalized)
303
+
304
+ // Determine which properties to animate
305
+ // - animateOnly prop is an exclusive filter (only animate those properties)
306
+ // - per-property configs WITHOUT a default = only animate those specific properties
307
+ // - per-property configs WITH a default = per-property overrides + default for rest
308
+ const hasDefault =
309
+ normalized.default !== null ||
310
+ normalized.enter !== null ||
311
+ normalized.exit !== null
312
+ const hasPerPropertyConfigs = animatedProperties.length > 0
313
+
314
+ let keys: string[]
315
+ if (props.animateOnly) {
316
+ // animateOnly is explicit filter
317
+ keys = props.animateOnly
318
+ } else if (hasPerPropertyConfigs && !hasDefault) {
319
+ // object format without default: { opacity: '200ms' } = only animate opacity
320
+ keys = animatedProperties
321
+ } else if (hasPerPropertyConfigs && hasDefault) {
322
+ // array format or object with default: 'all' first, then per-property overrides
323
+ // CSS transition specificity: later declarations override earlier ones for the same property
324
+ keys = ['all', ...animatedProperties]
325
+ } else {
326
+ // simple string format: 'quick' = animate all
327
+ keys = ['all']
328
+ }
329
+
330
+ useIsomorphicLayoutEffect(() => {
331
+ const host = stateRef.current.host
332
+ if (!sendExitComplete || !isExiting || !host) return
333
+ const node = host as HTMLElement
334
+
335
+ // capture current cycle id for this effect
336
+ const cycleId = exitCycleIdRef.current
337
+
338
+ // helper to complete exit with guards
339
+ const completeExit = () => {
340
+ if (cycleId !== exitCycleIdRef.current) return
341
+ if (exitCompletedRef.current) return
342
+ exitCompletedRef.current = true
343
+ sendExitComplete()
344
+ }
345
+
346
+ // if no properties to animate (animateOnly=[]), complete immediately
347
+ if (keys.length === 0) {
348
+ completeExit()
349
+ return
350
+ }
351
+
352
+ // Force transition restart for interrupted exits
353
+ // When an exit is interrupted and restarted, the element may already be at
354
+ // the exit style, so no CSS transition fires. We need to:
355
+ // 1. Reset to non-exit state
356
+ // 2. Force reflow
357
+ // 3. Re-apply exit state to trigger transition
358
+ let rafId: number | undefined
359
+ const wasInterrupted = exitInterruptedRef.current
360
+ // flag to ignore transitioncancel during reset (we intentionally cancel the old transition)
361
+ let ignoreCancelEvents = wasInterrupted
362
+ // get enter/exit styles for potential restart
363
+ const enterStyle = props.enterStyle as Record<string, unknown> | undefined
364
+ const exitStyle = props.exitStyle as Record<string, unknown> | undefined
365
+
366
+ // Build the exit transition string - needed for both normal and interrupted exits
367
+ const delayStr = normalized.delay ? ` ${normalized.delay}ms` : ''
368
+ const durationOverride = normalized.config?.duration
369
+ const exitTransitionString = keys
370
+ .map((key) => {
371
+ const propAnimation = normalized.properties[key]
372
+ let animationValue: string | null = null
373
+ if (typeof propAnimation === 'string') {
374
+ animationValue = animations[propAnimation]
375
+ } else if (
376
+ propAnimation &&
377
+ typeof propAnimation === 'object' &&
378
+ propAnimation.type
379
+ ) {
380
+ animationValue = animations[propAnimation.type]
381
+ } else if (defaultAnimation) {
382
+ animationValue = defaultAnimation
383
+ }
384
+ if (animationValue && durationOverride) {
385
+ animationValue = applyDurationOverride(animationValue, durationOverride)
386
+ }
387
+ return animationValue ? `${key} ${animationValue}${delayStr}` : null
388
+ })
389
+ .filter(Boolean)
390
+ .join(', ')
391
+
392
+ if (wasInterrupted) {
393
+ exitInterruptedRef.current = false
394
+ // disable transition, reset to enter state
395
+ node.style.transition = 'none'
396
+
397
+ // reset: apply active/open state for each exit property (not enterStyle,
398
+ // which may equal exitStyle — see comment in the normal exit path below)
399
+ if (exitStyle) {
400
+ const resetStyle: Record<string, unknown> = {}
401
+ for (const key of Object.keys(exitStyle)) {
402
+ if (key === 'opacity') {
403
+ resetStyle[key] = 1
404
+ } else if (TRANSFORM_KEYS.includes(key as any)) {
405
+ resetStyle[key] =
406
+ key === 'scale' || key === 'scaleX' || key === 'scaleY' ? 1 : 0
407
+ } else if (enterStyle?.[key] !== undefined) {
408
+ resetStyle[key] = enterStyle[key]
409
+ }
410
+ }
411
+ applyStylesToNode(node, resetStyle)
412
+ } else {
413
+ // fallback if no exitStyle defined
414
+ node.style.opacity = '1'
415
+ node.style.transform = 'none'
416
+ }
417
+
418
+ // force reflow
419
+ void node.offsetHeight
420
+ } else if (exitStyle) {
421
+ // For normal (non-interrupted) exits, we need to ensure the CSS transition is
422
+ // processed by the browser BEFORE the exitStyle takes effect. The issue is that
423
+ // React may have already applied exitStyle in the same render batch. To fix this:
424
+ // 1. Disable transition and reset to non-exit state
425
+ // 2. Force reflow so browser processes the reset
426
+ // 3. Use RAF to ensure we're in a new frame
427
+ // 4. Re-enable transition and apply exitStyle
428
+ // This mirrors the interrupted exit handling approach (which also uses RAF).
429
+ ignoreCancelEvents = true
430
+ node.style.transition = 'none'
431
+
432
+ // Reset to the active/open state (not enterStyle, which may equal exitStyle).
433
+ // enterStyle is the "unmounted" initial state and can share values with exitStyle
434
+ // (e.g., both have opacity: 0). resetting to enterStyle would mean no value change
435
+ // when exitStyle is applied, so the CSS transition wouldn't fire.
436
+ const resetStyle: Record<string, unknown> = {}
437
+ for (const key of Object.keys(exitStyle)) {
438
+ if (key === 'opacity') {
439
+ resetStyle[key] = 1
440
+ } else if (TRANSFORM_KEYS.includes(key as any)) {
441
+ resetStyle[key] =
442
+ key === 'scale' || key === 'scaleX' || key === 'scaleY' ? 1 : 0
443
+ } else if (enterStyle?.[key] !== undefined) {
444
+ resetStyle[key] = enterStyle[key]
445
+ }
446
+ }
447
+ applyStylesToNode(node, resetStyle)
448
+
449
+ // Force reflow
450
+ void node.offsetHeight
451
+
452
+ // Use RAF to ensure transition is applied in a new frame
453
+ rafId = requestAnimationFrame(() => {
454
+ if (cycleId !== exitCycleIdRef.current) return
455
+ // Re-enable transition
456
+ node.style.transition = exitTransitionString
457
+ // Force reflow to ensure transition is active
458
+ void node.offsetHeight
459
+ // Apply exit styles - this triggers the animation
460
+ applyStylesToNode(node, exitStyle)
461
+ // Re-enable cancel event handling
462
+ ignoreCancelEvents = false
463
+ })
464
+ }
465
+
466
+ /**
467
+ * Exit animation handling for Dialog/Modal components
468
+ *
469
+ * The Challenge: When users close dialogs (via Escape key or clicking outside),
470
+ * the element can disappear from the DOM before CSS transitions finish, which causes:
471
+ * 1. Dialogs to stick around on screen
472
+ * 2. Event handlers to stop working
473
+ *
474
+ * Fix: Calculate the MAXIMUM duration across all animated properties, not just
475
+ * the default. With animateOnly and per-property configs, different properties
476
+ * can have different durations, and we need to wait for the LONGEST one.
477
+ */
478
+
479
+ // calculate max duration across all animated properties
480
+ let maxDuration = defaultAnimation ? extractDuration(defaultAnimation) : 200
481
+
482
+ // check per-property animation durations using shared helper
483
+ const animationConfigs = getAnimationConfigsForKeys(
484
+ normalized,
485
+ animations as Record<string, string>,
486
+ keys,
487
+ defaultAnimation
488
+ )
489
+ for (const animationValue of animationConfigs.values()) {
490
+ if (animationValue) {
491
+ const duration = extractDuration(animationValue)
492
+ if (duration > maxDuration) {
493
+ maxDuration = duration
494
+ }
495
+ }
496
+ }
497
+
498
+ const delay = normalized.delay ?? 0
499
+ const fallbackTimeout = maxDuration + delay
500
+
501
+ const timeoutId = setTimeout(() => {
502
+ completeExit()
503
+ }, fallbackTimeout)
504
+
505
+ // track number of transitioning properties to wait for all to finish
506
+ // (each property fires its own transitionend event)
507
+ const transitioningProps = new Set(keys)
508
+ let completedCount = 0
509
+
510
+ const onFinishAnimation = (event: TransitionEvent) => {
511
+ // only count transitions on THIS element, not bubbled from children
512
+ if (event.target !== node) return
513
+
514
+ // map CSS property names to our key names
515
+ // e.g., transitionend fires with propertyName 'transform' for scale/x/y
516
+ const eventProp = event.propertyName
517
+ if (transitioningProps.has(eventProp) || eventProp === 'all') {
518
+ completedCount++
519
+ // wait for all properties to finish
520
+ if (completedCount >= transitioningProps.size) {
521
+ clearTimeout(timeoutId)
522
+ completeExit()
523
+ }
524
+ }
525
+ }
526
+
527
+ // on cancel, still complete (element is exiting and animation was interrupted)
528
+ // the guards prevent duplicate completion if this is a stale cycle
529
+ const onCancelAnimation = () => {
530
+ // ignore cancel events during reset phase (we intentionally cancel the old transition)
531
+ if (ignoreCancelEvents) return
532
+ clearTimeout(timeoutId)
533
+ completeExit()
534
+ }
535
+
536
+ node.addEventListener('transitionend', onFinishAnimation)
537
+ node.addEventListener('transitioncancel', onCancelAnimation)
538
+
539
+ // For interrupted exits, re-enable transition and re-apply exit styles
540
+ // This must happen AFTER listeners are set up so we catch the transitionend
541
+ if (wasInterrupted) {
542
+ rafId = requestAnimationFrame(() => {
543
+ if (cycleId !== exitCycleIdRef.current) return
544
+ // re-enable transition using the pre-built string
545
+ node.style.transition = exitTransitionString
546
+ // force reflow again
547
+ void node.offsetHeight
548
+ // now apply exit styles - this triggers the transition
549
+ applyStylesToNode(node, exitStyle)
550
+ // re-enable cancel event handling now that reset is complete
551
+ ignoreCancelEvents = false
552
+ })
553
+ }
554
+
555
+ return () => {
556
+ clearTimeout(timeoutId)
557
+ if (rafId !== undefined) cancelAnimationFrame(rafId)
558
+ node.removeEventListener('transitionend', onFinishAnimation)
559
+ node.removeEventListener('transitioncancel', onCancelAnimation)
560
+ // restore transition: the exit handling sets node.style.transition='none'
561
+ // directly on the DOM (bypassing React). if exit is interrupted (e.g. same-key
562
+ // re-entry in AnimatePresence), React won't re-apply its managed transition
563
+ // value because it hasn't changed in the virtual DOM. clearing the inline
564
+ // override lets React's value take effect again.
565
+ node.style.transition = ''
566
+ }
567
+ }, [
568
+ sendExitComplete,
569
+ isExiting,
570
+ stateRef,
571
+ keys,
572
+ normalized,
573
+ defaultAnimation,
574
+ props.enterStyle,
575
+ props.exitStyle,
576
+ ])
577
+
578
+ // hanzo-gui doesnt even use animation output during hydration
579
+ if (isHydrating) {
580
+ return null
581
+ }
582
+
583
+ // Check if we have any animation to apply
584
+ if (!hasNormalizedAnimation(normalized)) {
585
+ return null
586
+ }
587
+
588
+ if (Array.isArray(style.transform)) {
589
+ style.transform = transformsToString(style.transform)
590
+ }
591
+
592
+ // Build CSS transition string
593
+ // TODO: we disabled the transform transition, because it will create issue for inverse function and animate function
594
+ // for non layout transform properties either use animate function or find a workaround to do it with css
595
+ const delayStr = normalized.delay ? ` ${normalized.delay}ms` : ''
596
+ const durationOverride = normalized.config?.duration
597
+ style.transition = keys
598
+ .map((key) => {
599
+ // Check for property-specific animation, fall back to default
600
+ const propAnimation = normalized.properties[key]
601
+ let animationValue: string | null = null
602
+
603
+ if (typeof propAnimation === 'string') {
604
+ animationValue = animations[propAnimation]
605
+ } else if (
606
+ propAnimation &&
607
+ typeof propAnimation === 'object' &&
608
+ propAnimation.type
609
+ ) {
610
+ animationValue = animations[propAnimation.type]
611
+ } else if (defaultAnimation) {
612
+ animationValue = defaultAnimation
613
+ }
614
+
615
+ // Apply global duration override if specified
616
+ if (animationValue && durationOverride) {
617
+ animationValue = applyDurationOverride(animationValue, durationOverride)
618
+ }
619
+
620
+ return animationValue ? `${key} ${animationValue}${delayStr}` : null
621
+ })
622
+ .filter(Boolean)
623
+ .join(', ')
624
+
625
+ if (process.env.NODE_ENV === 'development' && props['debug'] === 'verbose') {
626
+ console.info('CSS animation', {
627
+ props,
628
+ animations,
629
+ normalized,
630
+ defaultAnimation,
631
+ style,
632
+ isEntering,
633
+ isExiting,
634
+ })
635
+ }
636
+
637
+ return { style, className: isEntering ? 't_unmounted' : '' }
638
+ },
639
+ }
640
+ }
641
+
642
+ // layout animations
643
+ // useIsomorphicLayoutEffect(() => {
644
+ // if (!host || !props.layout) {
645
+ // return
646
+ // }
647
+ // // @ts-ignore
648
+ // const boundingBox = host?.getBoundingClientRect()
649
+ // if (isChanged(initialPositionRef.current, boundingBox)) {
650
+ // const transform = invert(
651
+ // host,
652
+ // boundingBox,
653
+ // initialPositionRef.current
654
+ // )
655
+
656
+ // animate({
657
+ // from: transform,
658
+ // to: { x: 0, y: 0, scaleX: 1, scaleY: 1 },
659
+ // duration: 1000,
660
+ // onUpdate: ({ x, y, scaleX, scaleY }) => {
661
+ // // @ts-ignore
662
+ // host.style.transform = `translate(${x}px, ${y}px) scaleX(${scaleX}) scaleY(${scaleY})`
663
+ // // TODO: handle childRef inverse scale
664
+ // // childRef.current.style.transform = `scaleX(${1 / scaleX}) scaleY(${
665
+ // // 1 / scaleY
666
+ // // })`
667
+ // },
668
+ // // TODO: extract ease-in from string and convert/map it to a cubicBezier array
669
+ // cubicBezier: [0, 1.38, 1, -0.41],
670
+ // })
671
+ // }
672
+ // initialPositionRef.current = boundingBox
673
+ // })
674
+
675
+ // style.transition = `${keys} ${animation}${
676
+ // props.layout ? ',width 0s, height 0s, margin 0s, padding 0s, transform' : ''
677
+ // }`
678
+
679
+ // const isChanged = (initialBox: any, finalBox: any) => {
680
+ // // we just mounted, so we don't have complete data yet
681
+ // if (!initialBox || !finalBox) return false
682
+
683
+ // // deep compare the two boxes
684
+ // return JSON.stringify(initialBox) !== JSON.stringify(finalBox)
685
+ // }
686
+
687
+ // const invert = (el, from, to) => {
688
+ // const { x: fromX, y: fromY, width: fromWidth, height: fromHeight } = from
689
+ // const { x, y, width, height } = to
690
+
691
+ // const transform = {
692
+ // x: x - fromX - (fromWidth - width) / 2,
693
+ // y: y - fromY - (fromHeight - height) / 2,
694
+ // scaleX: width / fromWidth,
695
+ // scaleY: height / fromHeight,
696
+ // }
697
+
698
+ // el.style.transform = `
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './createAnimations'
@@ -0,0 +1,4 @@
1
+ import type { AnimationDriver } from "@hanzogui/web";
2
+ export declare function createAnimations<A extends object>(animations: A): AnimationDriver<A>;
3
+
4
+ //# sourceMappingURL=createAnimations.d.ts.map