@pyreon/kinetic 0.24.4 → 0.24.6

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.
Files changed (37) hide show
  1. package/package.json +10 -12
  2. package/src/Collapse.tsx +0 -166
  3. package/src/Stagger.tsx +0 -63
  4. package/src/Transition.tsx +0 -280
  5. package/src/TransitionGroup.tsx +0 -139
  6. package/src/__tests__/Collapse.test.tsx +0 -803
  7. package/src/__tests__/GroupRenderer.test.tsx +0 -434
  8. package/src/__tests__/StaggerRenderer.test.tsx +0 -523
  9. package/src/__tests__/Transition.ssr.test.tsx +0 -183
  10. package/src/__tests__/Transition.test.tsx +0 -403
  11. package/src/__tests__/TransitionItem.test.tsx +0 -514
  12. package/src/__tests__/kinetic-modes.ssr.test.tsx +0 -214
  13. package/src/__tests__/kinetic.browser.test.tsx +0 -327
  14. package/src/__tests__/kinetic.test.tsx +0 -565
  15. package/src/__tests__/presets.test.ts +0 -46
  16. package/src/__tests__/stagger-component-children-hydration.test.tsx +0 -191
  17. package/src/__tests__/top-level-transition-stagger-function-children.test.tsx +0 -141
  18. package/src/__tests__/useAnimationEnd.test.ts +0 -194
  19. package/src/__tests__/useReducedMotion.test.ts +0 -160
  20. package/src/__tests__/useTransitionState.test.ts +0 -132
  21. package/src/__tests__/utils.test.ts +0 -139
  22. package/src/index.ts +0 -15
  23. package/src/jsx-augment.d.ts +0 -12
  24. package/src/kinetic/CollapseRenderer.tsx +0 -216
  25. package/src/kinetic/GroupRenderer.tsx +0 -149
  26. package/src/kinetic/StaggerRenderer.tsx +0 -94
  27. package/src/kinetic/TransitionItem.tsx +0 -250
  28. package/src/kinetic/TransitionRenderer.tsx +0 -230
  29. package/src/kinetic/createKineticComponent.tsx +0 -224
  30. package/src/kinetic/types.ts +0 -149
  31. package/src/kinetic.ts +0 -25
  32. package/src/presets.ts +0 -66
  33. package/src/types.ts +0 -118
  34. package/src/useAnimationEnd.ts +0 -59
  35. package/src/useReducedMotion.ts +0 -28
  36. package/src/useTransitionState.ts +0 -62
  37. package/src/utils.ts +0 -113
@@ -1,94 +0,0 @@
1
- import type { VNode } from '@pyreon/core'
2
- import { h } from '@pyreon/core'
3
- import type { CSSProperties, TransitionCallbacks } from '../types'
4
- import { cloneVNode, resolveChildren } from '../utils'
5
- import TransitionItem from './TransitionItem'
6
- import type { KineticConfig } from './types'
7
-
8
- type StaggerRendererProps = {
9
- config: KineticConfig
10
- htmlProps: Record<string, unknown>
11
- show: () => boolean
12
- appear?: boolean | undefined
13
- timeout?: number | undefined
14
- interval?: number | undefined
15
- reverseLeave?: boolean | undefined
16
- callbacks: Partial<TransitionCallbacks>
17
- children: VNode[]
18
- }
19
-
20
- const isVNode = (child: unknown): child is VNode =>
21
- child != null && typeof child === 'object' && 'type' in (child as object)
22
-
23
- /**
24
- * Renders children with staggered enter/exit animation.
25
- * config.tag wraps the staggered children as a container element.
26
- * Each child is individually animated via TransitionItem.
27
- */
28
- const StaggerRenderer = ({
29
- config,
30
- htmlProps,
31
- show,
32
- appear,
33
- timeout,
34
- interval,
35
- reverseLeave,
36
- callbacks,
37
- children,
38
- }: StaggerRendererProps): VNode | null => {
39
- const effectiveAppear = appear ?? config.appear ?? false
40
- const effectiveTimeout = timeout ?? config.timeout ?? 5000
41
- const effectiveInterval = interval ?? config.interval ?? 50
42
- const effectiveReverseLeave = reverseLeave ?? config.reverseLeave ?? false
43
-
44
- // Unwrap compiler-emitted accessor wrap — see `resolveChildren` jsdoc.
45
- const resolved = resolveChildren(children)
46
- const childArray = (Array.isArray(resolved) ? resolved : [resolved]).filter(isVNode)
47
- const count = childArray.length
48
-
49
- const staggeredChildren = childArray.map((child, index) => {
50
- const staggerIndex = !show() && effectiveReverseLeave ? count - 1 - index : index
51
- const delay = staggerIndex * effectiveInterval
52
-
53
- return (
54
- <TransitionItem
55
- key={(child as VNode & { key?: string | number }).key ?? index}
56
- show={show}
57
- appear={effectiveAppear}
58
- timeout={effectiveTimeout + delay}
59
- enterStyle={config.enterStyle}
60
- enterToStyle={config.enterToStyle}
61
- enterTransition={config.enterTransition}
62
- leaveStyle={config.leaveStyle}
63
- leaveToStyle={config.leaveToStyle}
64
- leaveTransition={config.leaveTransition}
65
- enter={config.enter}
66
- enterFrom={config.enterFrom}
67
- enterTo={config.enterTo}
68
- leave={config.leave}
69
- leaveFrom={config.leaveFrom}
70
- leaveTo={config.leaveTo}
71
- onAfterLeave={
72
- index === (effectiveReverseLeave ? 0 : count - 1) ? callbacks.onAfterLeave : undefined
73
- }
74
- >
75
- {cloneVNode(child, {
76
- style: {
77
- ...((child.props as Record<string, unknown>)?.style as CSSProperties | undefined),
78
- '--stagger-index': staggerIndex,
79
- '--stagger-interval': `${effectiveInterval}ms`,
80
- transitionDelay: `${delay}ms`,
81
- } as CSSProperties,
82
- })}
83
- </TransitionItem>
84
- )
85
- })
86
-
87
- // Pass htmlProps by reference — `{ ...htmlProps }` value-copies, firing
88
- // any reactive getter the kinetic split preserved (frozen attr forever).
89
- // runtime-dom's applyProps detects the getter descriptor on the live
90
- // object and wraps it in renderEffect.
91
- return h(config.tag, htmlProps, ...staggeredChildren)
92
- }
93
-
94
- export default StaggerRenderer
@@ -1,250 +0,0 @@
1
- import type { VNode } from '@pyreon/core'
2
- import { createRef, cx, Show } from '@pyreon/core'
3
- import { watch } from '@pyreon/reactivity'
4
- import type { ClassTransitionProps, StyleTransitionProps, TransitionCallbacks } from '../types'
5
- import useAnimationEnd from '../useAnimationEnd'
6
- import { useReducedMotion } from '../useReducedMotion'
7
- import useTransitionState from '../useTransitionState'
8
- import {
9
- addClasses,
10
- cloneVNode,
11
- mergeRefs,
12
- mergeStyles,
13
- nextFrame,
14
- removeClasses,
15
- resolveChildren,
16
- } from '../utils'
17
-
18
- type TransitionItemProps = ClassTransitionProps &
19
- StyleTransitionProps &
20
- TransitionCallbacks & {
21
- show: () => boolean
22
- appear?: boolean | undefined
23
- unmount?: boolean | undefined
24
- timeout?: number | undefined
25
- delay?: number | undefined
26
- children: VNode
27
- }
28
-
29
- const applyEnter = (el: HTMLElement, config: ClassTransitionProps & StyleTransitionProps) => {
30
- // Symmetric to applyLeave: clear residual leave-cycle classes — including
31
- // the `leaveTo`/`enterFrom` class the SSR/initially-hidden render path
32
- // inlines (see the `wasInitiallyShown` branch below). Without this, the
33
- // SSR-baked hidden class would compete with `enterTo`'s CSS rules.
34
- removeClasses(el, config.leave)
35
- removeClasses(el, config.leaveFrom)
36
- removeClasses(el, config.leaveTo)
37
-
38
- addClasses(el, config.enter)
39
- addClasses(el, config.enterFrom)
40
- if (config.enterStyle) Object.assign(el.style, config.enterStyle)
41
- if (config.enterTransition) el.style.transition = config.enterTransition
42
-
43
- return nextFrame(() => {
44
- removeClasses(el, config.enterFrom)
45
- addClasses(el, config.enterTo)
46
- if (config.enterToStyle) Object.assign(el.style, config.enterToStyle)
47
- })
48
- }
49
-
50
- const applyLeave = (el: HTMLElement, config: ClassTransitionProps & StyleTransitionProps) => {
51
- removeClasses(el, config.enter)
52
- removeClasses(el, config.enterTo)
53
-
54
- addClasses(el, config.leave)
55
- addClasses(el, config.leaveFrom)
56
- if (config.leaveStyle) Object.assign(el.style, config.leaveStyle)
57
- if (config.leaveTransition) el.style.transition = config.leaveTransition
58
-
59
- return nextFrame(() => {
60
- removeClasses(el, config.leaveFrom)
61
- addClasses(el, config.leaveTo)
62
- if (config.leaveToStyle) Object.assign(el.style, config.leaveToStyle)
63
- })
64
- }
65
-
66
- const applyReducedMotion = (
67
- stage: string,
68
- callbacks: Partial<TransitionCallbacks>,
69
- complete: () => void,
70
- ) => {
71
- if (stage === 'entering') {
72
- callbacks.onEnter?.()
73
- callbacks.onAfterEnter?.()
74
- complete()
75
- } else if (stage === 'leaving') {
76
- callbacks.onLeave?.()
77
- callbacks.onAfterLeave?.()
78
- complete()
79
- }
80
- }
81
-
82
- /**
83
- * Internal per-child transition component. Used by StaggerRenderer and
84
- * GroupRenderer to give each child its own animation state.
85
- *
86
- * Uses cloneVNode to inject ref onto the child — the child must accept ref.
87
- */
88
- const TransitionItem = (props: TransitionItemProps): VNode | null => {
89
- // The Pyreon compiler wraps `{cloneVNode(child, {...})}` JSX child
90
- // expressions in StaggerRenderer/GroupRenderer as `() => cloneVNode(...)`
91
- // (prop-inlining for reactivity — see `resolveChildren` jsdoc). At this
92
- // boundary `props.children` therefore arrives as a FUNCTION instead of a
93
- // VNode. cloneVNode-on-a-function silently produces `{type: undefined,
94
- // props: {ref: ...}}` (spreading a function yields no own properties
95
- // because functions have none enumerable), which mountElement renders
96
- // as a literal `<undefined>` tag in the DOM — the SSG'd `<h1>Hello</h1>`
97
- // becomes an empty `<undefined></undefined>` post-hydrate (reproducer:
98
- // bokisch.com Intro section). Resolve eagerly so the entire body below
99
- // can treat `child` as a static VNode.
100
- const child = resolveChildren(props.children) as VNode
101
- const appear = props.appear ?? false
102
- const unmount = props.unmount ?? true
103
- const timeout = props.timeout ?? 5000
104
- const reducedMotion = useReducedMotion()
105
- const { stage, ref: stateRef, shouldMount, complete } = useTransitionState({
106
- show: props.show,
107
- appear,
108
- })
109
-
110
- const elementRef = createRef<HTMLElement>()
111
- const mergedRef = mergeRefs(
112
- elementRef,
113
- stateRef,
114
- (child?.props as Record<string, unknown>)?.ref as
115
- | ((el: HTMLElement | null) => void)
116
- | undefined,
117
- )
118
-
119
- const callbacks = {
120
- onEnter: props.onEnter,
121
- onAfterEnter: props.onAfterEnter,
122
- onLeave: props.onLeave,
123
- onAfterLeave: props.onAfterLeave,
124
- }
125
-
126
- const transitionConfig = {
127
- enter: props.enter,
128
- enterFrom: props.enterFrom,
129
- enterTo: props.enterTo,
130
- leave: props.leave,
131
- leaveFrom: props.leaveFrom,
132
- leaveTo: props.leaveTo,
133
- enterStyle: props.enterStyle,
134
- enterToStyle: props.enterToStyle,
135
- enterTransition: props.enterTransition,
136
- leaveStyle: props.leaveStyle,
137
- leaveToStyle: props.leaveToStyle,
138
- leaveTransition: props.leaveTransition,
139
- }
140
-
141
- useAnimationEnd({
142
- ref: elementRef,
143
- active: () => (stage() === 'entering' || stage() === 'leaving') && !reducedMotion(),
144
- timeout,
145
- onEnd: () => {
146
- if (stage() === 'entering') {
147
- callbacks.onAfterEnter?.()
148
- } else if (stage() === 'leaving') {
149
- callbacks.onAfterLeave?.()
150
- }
151
- complete()
152
- },
153
- })
154
-
155
- watch(
156
- () => stage(),
157
- (currentStage) => {
158
- const el = elementRef.current
159
- if (!el) return
160
-
161
- if (reducedMotion()) {
162
- applyReducedMotion(currentStage, callbacks, complete)
163
- return
164
- }
165
-
166
- if (currentStage === 'entering') {
167
- callbacks.onEnter?.()
168
- const frameId = applyEnter(el, transitionConfig)
169
- return () => cancelAnimationFrame(frameId)
170
- }
171
-
172
- if (currentStage === 'leaving') {
173
- callbacks.onLeave?.()
174
- const frameId = applyLeave(el, transitionConfig)
175
- return () => cancelAnimationFrame(frameId)
176
- }
177
-
178
- if (currentStage === 'entered') {
179
- removeClasses(el, props.enter)
180
- el.style.transition = ''
181
- }
182
- },
183
- { immediate: true },
184
- )
185
-
186
- // Initially-visible items keep the original Show-gated mount, preserving
187
- // the documented runtime-unmount semantic for visible→hidden. The SSR
188
- // bug (children dropped from prerendered HTML) only fires for the
189
- // initially-HIDDEN case below, where `<Show when={false}>` renders `null`
190
- // on the server. For Stagger/Group usage at SSR (when the parent's
191
- // `show: () => false`), each per-item TransitionItem hit this and
192
- // dropped its child — full list missing from prerendered HTML.
193
- //
194
- // Mirrors the fix in `<Transition>` (PR #717) and `TransitionRenderer`
195
- // (same PR as this). Ecosystem norm: content is structural, animation
196
- // is visual.
197
- const wasInitiallyShown = props.show()
198
- if (wasInitiallyShown) {
199
- return (
200
- <Show
201
- when={shouldMount}
202
- fallback={
203
- unmount
204
- ? null
205
- : cloneVNode(child, {
206
- ref: mergedRef,
207
- style: mergeStyles(
208
- (child?.props as Record<string, unknown>)?.style as
209
- | Record<string, string | number | undefined>
210
- | undefined,
211
- { display: 'none' },
212
- ),
213
- })
214
- }
215
- >
216
- {cloneVNode(child, { ref: mergedRef })}
217
- </Show>
218
- )
219
- }
220
-
221
- // Initially-hidden path — always emit the child with hidden-state class +
222
- // style inlined. `leaveTo`/`leaveToStyle` (explicit hidden-end state)
223
- // wins; falls back to `enterFrom`/`enterStyle` (pre-enter state — covers
224
- // both class-based scroll-reveal AND the preset path, where
225
- // `@pyreon/kinetic-presets` factories populate `enterStyle` as the
226
- // hidden state but may not set `leaveToStyle`).
227
- //
228
- // Trade-off: for an initially-hidden item, `unmount: true` no longer
229
- // triggers a true DOM removal after a later leave animation completes.
230
- // Initially-visible items keep the unmount semantic.
231
- const hiddenClass = props.leaveTo ?? props.enterFrom
232
- const hiddenStyle = props.leaveToStyle ?? props.enterStyle
233
- const childProps = (child?.props ?? {}) as Record<string, unknown>
234
- const childClass = childProps.class
235
- const mergedClass = hiddenClass
236
- ? cx([childClass as Parameters<typeof cx>[0], hiddenClass])
237
- : undefined
238
- const mergedStyle = mergeStyles(
239
- childProps.style as Record<string, string | number | undefined> | undefined,
240
- hiddenStyle,
241
- )
242
-
243
- const extra: Record<string, unknown> = { ref: mergedRef }
244
- if (mergedClass !== undefined) extra.class = mergedClass
245
- if (mergedStyle !== undefined) extra.style = mergedStyle
246
-
247
- return cloneVNode(child, extra)
248
- }
249
-
250
- export default TransitionItem
@@ -1,230 +0,0 @@
1
- import type { VNode } from '@pyreon/core'
2
- import { createRef, cx, h, mergeProps, Show } from '@pyreon/core'
3
- import { watch } from '@pyreon/reactivity'
4
- import type { CSSProperties, TransitionCallbacks } from '../types'
5
- import useAnimationEnd from '../useAnimationEnd'
6
- import { useReducedMotion } from '../useReducedMotion'
7
- import useTransitionState from '../useTransitionState'
8
- import { addClasses, mergeRefs, nextFrame, removeClasses } from '../utils'
9
- import type { KineticConfig } from './types'
10
-
11
- type TransitionRendererProps = {
12
- config: KineticConfig
13
- htmlProps: Record<string, unknown>
14
- show: () => boolean
15
- appear?: boolean | undefined
16
- unmount?: boolean | undefined
17
- timeout?: number | undefined
18
- callbacks: Partial<TransitionCallbacks>
19
- children: VNode | VNode[]
20
- }
21
-
22
- const applyEnter = (el: HTMLElement, config: KineticConfig) => {
23
- // Symmetric to applyLeave's `removeClasses(enter)` / `removeClasses(enterTo)`:
24
- // clear residual leave-cycle classes — including the `leaveTo` / `enterFrom`
25
- // class the SSR / initially-hidden render path inlines for structural
26
- // content (see the `wasInitiallyShown` branch below). Without this, the
27
- // SSR-baked hidden-state class would compete with `enterTo`'s CSS rules.
28
- removeClasses(el, config.leave)
29
- removeClasses(el, config.leaveFrom)
30
- removeClasses(el, config.leaveTo)
31
-
32
- addClasses(el, config.enter)
33
- addClasses(el, config.enterFrom)
34
- if (config.enterStyle) Object.assign(el.style, config.enterStyle)
35
- if (config.enterTransition) el.style.transition = config.enterTransition
36
-
37
- return nextFrame(() => {
38
- removeClasses(el, config.enterFrom)
39
- addClasses(el, config.enterTo)
40
- if (config.enterToStyle) Object.assign(el.style, config.enterToStyle)
41
- })
42
- }
43
-
44
- const applyLeave = (el: HTMLElement, config: KineticConfig) => {
45
- removeClasses(el, config.enter)
46
- removeClasses(el, config.enterTo)
47
-
48
- addClasses(el, config.leave)
49
- addClasses(el, config.leaveFrom)
50
- if (config.leaveStyle) Object.assign(el.style, config.leaveStyle)
51
- if (config.leaveTransition) el.style.transition = config.leaveTransition
52
-
53
- return nextFrame(() => {
54
- removeClasses(el, config.leaveFrom)
55
- addClasses(el, config.leaveTo)
56
- if (config.leaveToStyle) Object.assign(el.style, config.leaveToStyle)
57
- })
58
- }
59
-
60
- const applyReducedMotion = (
61
- stage: string,
62
- cbs: Partial<TransitionCallbacks>,
63
- complete: () => void,
64
- ) => {
65
- if (stage === 'entering') {
66
- cbs.onEnter?.()
67
- cbs.onAfterEnter?.()
68
- complete()
69
- } else if (stage === 'leaving') {
70
- cbs.onLeave?.()
71
- cbs.onAfterLeave?.()
72
- complete()
73
- }
74
- }
75
-
76
- /**
77
- * Renders a single element with CSS transition enter/exit animation.
78
- * Uses h(config.tag) — no cloneElement needed.
79
- */
80
- const TransitionRenderer = (props: TransitionRendererProps): VNode | null => {
81
- const reducedMotion = useReducedMotion()
82
- const {
83
- stage,
84
- ref: stateRef,
85
- shouldMount,
86
- complete,
87
- } = useTransitionState({
88
- show: props.show,
89
- appear: props.appear ?? props.config.appear ?? false,
90
- })
91
-
92
- const elementRef = createRef<HTMLElement>()
93
- const mergedRef = mergeRefs(elementRef, stateRef)
94
-
95
- const effectiveUnmount = props.unmount ?? props.config.unmount ?? true
96
- const effectiveTimeout = props.timeout ?? props.config.timeout ?? 5000
97
-
98
- useAnimationEnd({
99
- ref: elementRef,
100
- active: () => (stage() === 'entering' || stage() === 'leaving') && !reducedMotion(),
101
- timeout: effectiveTimeout,
102
- onEnd: () => {
103
- if (stage() === 'entering') {
104
- props.callbacks.onAfterEnter?.()
105
- } else if (stage() === 'leaving') {
106
- props.callbacks.onAfterLeave?.()
107
- }
108
- complete()
109
- },
110
- })
111
-
112
- watch(
113
- () => stage(),
114
- (currentStage) => {
115
- const el = elementRef.current
116
- if (!el) return
117
-
118
- if (reducedMotion()) {
119
- applyReducedMotion(currentStage, props.callbacks, complete)
120
- return
121
- }
122
-
123
- if (currentStage === 'entering') {
124
- props.callbacks.onEnter?.()
125
- const frameId = applyEnter(el, props.config)
126
- return () => cancelAnimationFrame(frameId)
127
- }
128
-
129
- if (currentStage === 'leaving') {
130
- props.callbacks.onLeave?.()
131
- const frameId = applyLeave(el, props.config)
132
- return () => cancelAnimationFrame(frameId)
133
- }
134
-
135
- if (currentStage === 'entered') {
136
- removeClasses(el, props.config.enter)
137
- el.style.transition = ''
138
- }
139
- },
140
- { immediate: true },
141
- )
142
-
143
- // Initially-visible kinetic-mode Transitions keep the original Show-gated
144
- // mount, preserving the documented runtime-unmount semantic for the
145
- // visible→hidden transition. The SSR bug (children dropped from prerendered
146
- // HTML) only fires for the initially-HIDDEN case below, where
147
- // `<Show when={false}>` renders `null` on the server — leaving SSG sites
148
- // using kinetic-mode transitions (e.g. `kinetic('div').preset(fadeUp)` with
149
- // `show: () => false` at SSR, the scroll-reveal pattern via
150
- // `useIntersection`) without structural content for SEO / social scrapers
151
- // / accessibility tools / no-JS users.
152
- //
153
- // Mirrors the same fix shape applied to the top-level `<Transition>` in
154
- // PR #717. Ecosystem norm (Framer Motion / react-transition-group / react-
155
- // spring): content is structural, animation is visual.
156
- const wasInitiallyShown = props.show()
157
- if (wasInitiallyShown) {
158
- return (
159
- <Show
160
- when={shouldMount}
161
- fallback={
162
- effectiveUnmount
163
- ? null
164
- : h(
165
- props.config.tag,
166
- // mergeProps keeps every reactive HTML-attr getter; ref + the
167
- // hidden-state `display:none` style come last and win. The
168
- // one-time `props.htmlProps.style` read seeds the hidden
169
- // style — display:none must compose over the user's style.
170
- mergeProps(props.htmlProps, {
171
- ref: mergedRef,
172
- style: {
173
- ...((props.htmlProps.style as CSSProperties) ?? {}),
174
- display: 'none',
175
- },
176
- }),
177
- props.children,
178
- )
179
- }
180
- >
181
- {h(
182
- props.config.tag,
183
- // Descriptor-preserving merge — reactive HTML attrs keep their
184
- // getters; ref wins last. `{ ...props.htmlProps }` would freeze them.
185
- mergeProps(props.htmlProps, { ref: mergedRef }),
186
- props.children,
187
- )}
188
- </Show>
189
- )
190
- }
191
-
192
- // Initially-hidden path — ecosystem-correct: always emit children with
193
- // hidden-state class/style inlined so SSG / SEO / social scrapers / no-JS
194
- // users see structural content. `leaveTo` (explicit hidden-end state)
195
- // wins; falls back to `enterFrom` (pre-enter state) for scroll-reveal
196
- // patterns that only configure the enter side. The existing
197
- // `watch(stage)` effect drives the enter animation when `show` flips
198
- // true; the symmetric `applyEnter` above clears these residual classes.
199
- //
200
- // Trade-off: for initially-hidden kinetic-mode Transitions, `unmount: true`
201
- // no longer triggers a true DOM removal after a later leave animation
202
- // completes — element stays in DOM with the leave-to class applied.
203
- // Initially-visible Transitions (the branch above) keep the unmount
204
- // semantic. Matches Framer Motion / react-transition-group conventions
205
- // and is the price of SSR correctness.
206
- // Mirrors the class picker: prefer `leaveTo`/`leaveToStyle` (explicit
207
- // leave-end / hidden state) and fall back to `enterFrom`/`enterStyle`
208
- // (pre-enter state). The fallback covers the preset path —
209
- // `@pyreon/kinetic-presets` factories (fadeUp, slideLeft, blurInUp, …)
210
- // populate `enterStyle` as the hidden state and may not set
211
- // `leaveToStyle` at all; without this fallback, presets would SSR-render
212
- // VISIBLE → flash-on-hydration.
213
- const hiddenClass = props.config.leaveTo ?? props.config.enterFrom
214
- const hiddenStyle = props.config.leaveToStyle ?? props.config.enterStyle
215
- const childClass = props.htmlProps.class
216
- const mergedClass = hiddenClass
217
- ? cx([childClass as Parameters<typeof cx>[0], hiddenClass])
218
- : undefined
219
- const mergedStyle = hiddenStyle
220
- ? { ...((props.htmlProps.style as CSSProperties) ?? {}), ...hiddenStyle }
221
- : undefined
222
-
223
- const extra: Record<string, unknown> = { ref: mergedRef }
224
- if (mergedClass !== undefined) extra.class = mergedClass
225
- if (mergedStyle !== undefined) extra.style = mergedStyle
226
-
227
- return h(props.config.tag, mergeProps(props.htmlProps, extra), props.children)
228
- }
229
-
230
- export default TransitionRenderer