@pyreon/kinetic 0.11.0 → 0.11.2
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/package.json +12 -10
- package/src/Collapse.tsx +172 -0
- package/src/Stagger.tsx +52 -0
- package/src/Transition.tsx +214 -0
- package/src/TransitionGroup.tsx +108 -0
- package/src/__tests__/Collapse.test.tsx +782 -0
- package/src/__tests__/GroupRenderer.test.tsx +388 -0
- package/src/__tests__/StaggerRenderer.test.tsx +526 -0
- package/src/__tests__/Transition.test.tsx +406 -0
- package/src/__tests__/TransitionItem.test.tsx +522 -0
- package/src/__tests__/kinetic.test.tsx +562 -0
- package/src/__tests__/presets.test.ts +46 -0
- package/src/__tests__/useAnimationEnd.test.ts +194 -0
- package/src/__tests__/useReducedMotion.test.ts +157 -0
- package/src/__tests__/useTransitionState.test.ts +132 -0
- package/src/__tests__/utils.test.ts +139 -0
- package/src/index.ts +23 -0
- package/src/jsx-augment.d.ts +12 -0
- package/src/kinetic/CollapseRenderer.tsx +178 -0
- package/src/kinetic/GroupRenderer.tsx +124 -0
- package/src/kinetic/StaggerRenderer.tsx +88 -0
- package/src/kinetic/TransitionItem.tsx +196 -0
- package/src/kinetic/TransitionRenderer.tsx +168 -0
- package/src/kinetic/createKineticComponent.tsx +211 -0
- package/src/kinetic/types.ts +149 -0
- package/src/kinetic.ts +25 -0
- package/src/presets.ts +66 -0
- package/src/types.ts +118 -0
- package/src/useAnimationEnd.ts +59 -0
- package/src/useReducedMotion.ts +28 -0
- package/src/useTransitionState.ts +62 -0
- package/src/utils.ts +81 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import type { VNode } from "@pyreon/core"
|
|
2
|
+
import { createRef, h, 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
|
+
addClasses(el, config.enter)
|
|
24
|
+
addClasses(el, config.enterFrom)
|
|
25
|
+
if (config.enterStyle) Object.assign(el.style, config.enterStyle)
|
|
26
|
+
if (config.enterTransition) el.style.transition = config.enterTransition
|
|
27
|
+
|
|
28
|
+
return nextFrame(() => {
|
|
29
|
+
removeClasses(el, config.enterFrom)
|
|
30
|
+
addClasses(el, config.enterTo)
|
|
31
|
+
if (config.enterToStyle) Object.assign(el.style, config.enterToStyle)
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const applyLeave = (el: HTMLElement, config: KineticConfig) => {
|
|
36
|
+
removeClasses(el, config.enter)
|
|
37
|
+
removeClasses(el, config.enterTo)
|
|
38
|
+
|
|
39
|
+
addClasses(el, config.leave)
|
|
40
|
+
addClasses(el, config.leaveFrom)
|
|
41
|
+
if (config.leaveStyle) Object.assign(el.style, config.leaveStyle)
|
|
42
|
+
if (config.leaveTransition) el.style.transition = config.leaveTransition
|
|
43
|
+
|
|
44
|
+
return nextFrame(() => {
|
|
45
|
+
removeClasses(el, config.leaveFrom)
|
|
46
|
+
addClasses(el, config.leaveTo)
|
|
47
|
+
if (config.leaveToStyle) Object.assign(el.style, config.leaveToStyle)
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const applyReducedMotion = (
|
|
52
|
+
stage: string,
|
|
53
|
+
cbs: Partial<TransitionCallbacks>,
|
|
54
|
+
complete: () => void,
|
|
55
|
+
) => {
|
|
56
|
+
if (stage === "entering") {
|
|
57
|
+
cbs.onEnter?.()
|
|
58
|
+
cbs.onAfterEnter?.()
|
|
59
|
+
complete()
|
|
60
|
+
} else if (stage === "leaving") {
|
|
61
|
+
cbs.onLeave?.()
|
|
62
|
+
cbs.onAfterLeave?.()
|
|
63
|
+
complete()
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Renders a single element with CSS transition enter/exit animation.
|
|
69
|
+
* Uses h(config.tag) — no cloneElement needed.
|
|
70
|
+
*/
|
|
71
|
+
const TransitionRenderer = ({
|
|
72
|
+
config,
|
|
73
|
+
htmlProps,
|
|
74
|
+
show,
|
|
75
|
+
appear,
|
|
76
|
+
unmount,
|
|
77
|
+
timeout,
|
|
78
|
+
callbacks,
|
|
79
|
+
children,
|
|
80
|
+
}: TransitionRendererProps): VNode | null => {
|
|
81
|
+
const reducedMotion = useReducedMotion()
|
|
82
|
+
const {
|
|
83
|
+
stage,
|
|
84
|
+
ref: stateRef,
|
|
85
|
+
shouldMount,
|
|
86
|
+
complete,
|
|
87
|
+
} = useTransitionState({
|
|
88
|
+
show,
|
|
89
|
+
appear: appear ?? config.appear ?? false,
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const elementRef = createRef<HTMLElement>()
|
|
93
|
+
const mergedRef = mergeRefs(elementRef, stateRef)
|
|
94
|
+
|
|
95
|
+
const effectiveUnmount = unmount ?? config.unmount ?? true
|
|
96
|
+
const effectiveTimeout = timeout ?? 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
|
+
callbacks.onAfterEnter?.()
|
|
105
|
+
} else if (stage() === "leaving") {
|
|
106
|
+
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, callbacks, complete)
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (currentStage === "entering") {
|
|
124
|
+
callbacks.onEnter?.()
|
|
125
|
+
const frameId = applyEnter(el, config)
|
|
126
|
+
return () => cancelAnimationFrame(frameId)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (currentStage === "leaving") {
|
|
130
|
+
callbacks.onLeave?.()
|
|
131
|
+
const frameId = applyLeave(el, config)
|
|
132
|
+
return () => cancelAnimationFrame(frameId)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (currentStage === "entered") {
|
|
136
|
+
removeClasses(el, config.enter)
|
|
137
|
+
el.style.transition = ""
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
{ immediate: true },
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<Show
|
|
145
|
+
when={shouldMount}
|
|
146
|
+
fallback={
|
|
147
|
+
effectiveUnmount
|
|
148
|
+
? null
|
|
149
|
+
: h(
|
|
150
|
+
config.tag,
|
|
151
|
+
{
|
|
152
|
+
ref: mergedRef,
|
|
153
|
+
...htmlProps,
|
|
154
|
+
style: {
|
|
155
|
+
...((htmlProps.style as CSSProperties) ?? {}),
|
|
156
|
+
display: "none",
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
children,
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
>
|
|
163
|
+
{h(config.tag, { ref: mergedRef, ...htmlProps }, children)}
|
|
164
|
+
</Show>
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export default TransitionRenderer
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import type { VNode } from "@pyreon/core"
|
|
2
|
+
import type { CSSProperties, TransitionCallbacks } from "../types"
|
|
3
|
+
import CollapseRenderer from "./CollapseRenderer"
|
|
4
|
+
import GroupRenderer from "./GroupRenderer"
|
|
5
|
+
import StaggerRenderer from "./StaggerRenderer"
|
|
6
|
+
import TransitionRenderer from "./TransitionRenderer"
|
|
7
|
+
import type { ClassConfig, KineticComponent, KineticConfig, KineticMode } from "./types"
|
|
8
|
+
|
|
9
|
+
/** Keys that are kinetic-specific and should not be forwarded as HTML attrs. */
|
|
10
|
+
const KINETIC_KEYS = new Set([
|
|
11
|
+
"show",
|
|
12
|
+
"appear",
|
|
13
|
+
"unmount",
|
|
14
|
+
"timeout",
|
|
15
|
+
"transition",
|
|
16
|
+
"interval",
|
|
17
|
+
"reverseLeave",
|
|
18
|
+
"onEnter",
|
|
19
|
+
"onAfterEnter",
|
|
20
|
+
"onLeave",
|
|
21
|
+
"onAfterLeave",
|
|
22
|
+
])
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Core factory. Creates a component that delegates to the appropriate
|
|
26
|
+
* renderer based on config.mode, then attaches immutable chain methods
|
|
27
|
+
* via Object.assign.
|
|
28
|
+
*/
|
|
29
|
+
const createKineticComponent = <Tag extends string, Mode extends KineticMode = "transition">(
|
|
30
|
+
config: KineticConfig,
|
|
31
|
+
): KineticComponent<Tag, Mode> => {
|
|
32
|
+
const Component = (props: Record<string, unknown>): VNode | null => {
|
|
33
|
+
// Separate kinetic-specific props from HTML pass-through props
|
|
34
|
+
const htmlProps: Record<string, unknown> = {}
|
|
35
|
+
const kineticProps: Record<string, unknown> = {}
|
|
36
|
+
|
|
37
|
+
for (const key in props) {
|
|
38
|
+
if (KINETIC_KEYS.has(key)) {
|
|
39
|
+
kineticProps[key] = props[key]
|
|
40
|
+
} else {
|
|
41
|
+
htmlProps[key] = props[key]
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const {
|
|
46
|
+
show,
|
|
47
|
+
appear,
|
|
48
|
+
unmount,
|
|
49
|
+
timeout,
|
|
50
|
+
transition,
|
|
51
|
+
interval,
|
|
52
|
+
reverseLeave,
|
|
53
|
+
onEnter,
|
|
54
|
+
onAfterEnter,
|
|
55
|
+
onLeave,
|
|
56
|
+
onAfterLeave,
|
|
57
|
+
} = kineticProps as {
|
|
58
|
+
show?: () => boolean
|
|
59
|
+
appear?: boolean
|
|
60
|
+
unmount?: boolean
|
|
61
|
+
timeout?: number
|
|
62
|
+
transition?: string
|
|
63
|
+
interval?: number
|
|
64
|
+
reverseLeave?: boolean
|
|
65
|
+
} & Partial<TransitionCallbacks>
|
|
66
|
+
|
|
67
|
+
const callbacks: Partial<TransitionCallbacks> = {
|
|
68
|
+
onEnter: onEnter ?? config.onEnter,
|
|
69
|
+
onAfterEnter: onAfterEnter ?? config.onAfterEnter,
|
|
70
|
+
onLeave: onLeave ?? config.onLeave,
|
|
71
|
+
onAfterLeave: onAfterLeave ?? config.onAfterLeave,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Extract children from htmlProps (it's not an HTML attribute)
|
|
75
|
+
const { children, ...restHtml } = htmlProps
|
|
76
|
+
|
|
77
|
+
if (config.mode === "collapse") {
|
|
78
|
+
return (
|
|
79
|
+
<CollapseRenderer
|
|
80
|
+
config={config}
|
|
81
|
+
htmlProps={restHtml}
|
|
82
|
+
show={show as () => boolean}
|
|
83
|
+
appear={appear}
|
|
84
|
+
timeout={timeout}
|
|
85
|
+
transition={transition}
|
|
86
|
+
callbacks={callbacks}
|
|
87
|
+
>
|
|
88
|
+
{children as VNode | VNode[]}
|
|
89
|
+
</CollapseRenderer>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (config.mode === "stagger") {
|
|
94
|
+
return (
|
|
95
|
+
<StaggerRenderer
|
|
96
|
+
config={config}
|
|
97
|
+
htmlProps={restHtml}
|
|
98
|
+
show={show as () => boolean}
|
|
99
|
+
appear={appear}
|
|
100
|
+
timeout={timeout}
|
|
101
|
+
interval={interval}
|
|
102
|
+
reverseLeave={reverseLeave}
|
|
103
|
+
callbacks={callbacks}
|
|
104
|
+
>
|
|
105
|
+
{children as VNode[]}
|
|
106
|
+
</StaggerRenderer>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (config.mode === "group") {
|
|
111
|
+
return (
|
|
112
|
+
<GroupRenderer
|
|
113
|
+
config={config}
|
|
114
|
+
htmlProps={restHtml}
|
|
115
|
+
appear={appear}
|
|
116
|
+
timeout={timeout}
|
|
117
|
+
callbacks={callbacks}
|
|
118
|
+
>
|
|
119
|
+
{children as VNode[]}
|
|
120
|
+
</GroupRenderer>
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Default: transition mode
|
|
125
|
+
return (
|
|
126
|
+
<TransitionRenderer
|
|
127
|
+
config={config}
|
|
128
|
+
htmlProps={restHtml}
|
|
129
|
+
show={show as () => boolean}
|
|
130
|
+
appear={appear}
|
|
131
|
+
unmount={unmount}
|
|
132
|
+
timeout={timeout}
|
|
133
|
+
callbacks={callbacks}
|
|
134
|
+
>
|
|
135
|
+
{children as VNode | VNode[]}
|
|
136
|
+
</TransitionRenderer>
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
Component.displayName = `kinetic(${config.tag})`
|
|
141
|
+
|
|
142
|
+
// Immutable chain methods — each returns a new component with merged config.
|
|
143
|
+
return Object.assign(Component, {
|
|
144
|
+
preset: (preset: Record<string, unknown>) =>
|
|
145
|
+
createKineticComponent<Tag, Mode>({
|
|
146
|
+
...config,
|
|
147
|
+
...preset,
|
|
148
|
+
} as KineticConfig),
|
|
149
|
+
|
|
150
|
+
enter: (styles: CSSProperties) =>
|
|
151
|
+
createKineticComponent<Tag, Mode>({ ...config, enterStyle: styles }),
|
|
152
|
+
|
|
153
|
+
enterTo: (styles: CSSProperties) =>
|
|
154
|
+
createKineticComponent<Tag, Mode>({ ...config, enterToStyle: styles }),
|
|
155
|
+
|
|
156
|
+
enterTransition: (value: string) =>
|
|
157
|
+
createKineticComponent<Tag, Mode>({ ...config, enterTransition: value }),
|
|
158
|
+
|
|
159
|
+
leave: (styles: CSSProperties) =>
|
|
160
|
+
createKineticComponent<Tag, Mode>({ ...config, leaveStyle: styles }),
|
|
161
|
+
|
|
162
|
+
leaveTo: (styles: CSSProperties) =>
|
|
163
|
+
createKineticComponent<Tag, Mode>({ ...config, leaveToStyle: styles }),
|
|
164
|
+
|
|
165
|
+
leaveTransition: (value: string) =>
|
|
166
|
+
createKineticComponent<Tag, Mode>({ ...config, leaveTransition: value }),
|
|
167
|
+
|
|
168
|
+
enterClass: ({ active, from, to }: ClassConfig) =>
|
|
169
|
+
createKineticComponent<Tag, Mode>({
|
|
170
|
+
...config,
|
|
171
|
+
enter: active,
|
|
172
|
+
enterFrom: from,
|
|
173
|
+
enterTo: to,
|
|
174
|
+
}),
|
|
175
|
+
|
|
176
|
+
leaveClass: ({ active, from, to }: ClassConfig) =>
|
|
177
|
+
createKineticComponent<Tag, Mode>({
|
|
178
|
+
...config,
|
|
179
|
+
leave: active,
|
|
180
|
+
leaveFrom: from,
|
|
181
|
+
leaveTo: to,
|
|
182
|
+
}),
|
|
183
|
+
|
|
184
|
+
config: (opts: Record<string, unknown>) =>
|
|
185
|
+
createKineticComponent<Tag, Mode>({
|
|
186
|
+
...config,
|
|
187
|
+
...opts,
|
|
188
|
+
} as KineticConfig),
|
|
189
|
+
|
|
190
|
+
on: (cbs: Partial<TransitionCallbacks>) =>
|
|
191
|
+
createKineticComponent<Tag, Mode>({ ...config, ...cbs }),
|
|
192
|
+
|
|
193
|
+
collapse: (opts?: { transition?: string }) =>
|
|
194
|
+
createKineticComponent<Tag, "collapse">({
|
|
195
|
+
...config,
|
|
196
|
+
mode: "collapse",
|
|
197
|
+
...opts,
|
|
198
|
+
}),
|
|
199
|
+
|
|
200
|
+
stagger: (opts?: { interval?: number; reverseLeave?: boolean }) =>
|
|
201
|
+
createKineticComponent<Tag, "stagger">({
|
|
202
|
+
...config,
|
|
203
|
+
mode: "stagger",
|
|
204
|
+
...opts,
|
|
205
|
+
}),
|
|
206
|
+
|
|
207
|
+
group: () => createKineticComponent<Tag, "group">({ ...config, mode: "group" }),
|
|
208
|
+
}) as unknown as KineticComponent<Tag, Mode>
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export default createKineticComponent
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { ComponentFn, VNodeChild } from "@pyreon/core"
|
|
2
|
+
import type {
|
|
3
|
+
ClassTransitionProps,
|
|
4
|
+
CSSProperties,
|
|
5
|
+
StyleTransitionProps,
|
|
6
|
+
TransitionCallbacks,
|
|
7
|
+
} from "../types"
|
|
8
|
+
|
|
9
|
+
// ─── Kinetic Modes ────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
export type KineticMode = "transition" | "collapse" | "stagger" | "group"
|
|
12
|
+
|
|
13
|
+
// ─── Internal Config (accumulated through chaining) ──────
|
|
14
|
+
|
|
15
|
+
export type KineticConfig = StyleTransitionProps &
|
|
16
|
+
ClassTransitionProps &
|
|
17
|
+
TransitionCallbacks & {
|
|
18
|
+
tag: string
|
|
19
|
+
mode: KineticMode
|
|
20
|
+
appear?: boolean | undefined
|
|
21
|
+
unmount?: boolean | undefined
|
|
22
|
+
timeout?: number | undefined
|
|
23
|
+
/** Collapse: CSS transition for height. */
|
|
24
|
+
transition?: string | undefined
|
|
25
|
+
/** Stagger: delay between each child in ms. */
|
|
26
|
+
interval?: number | undefined
|
|
27
|
+
/** Stagger: reverse order on leave. */
|
|
28
|
+
reverseLeave?: boolean | undefined
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ─── Class Config (for .enterClass / .leaveClass) ────────
|
|
32
|
+
|
|
33
|
+
export type ClassConfig = {
|
|
34
|
+
active?: string | undefined
|
|
35
|
+
from?: string | undefined
|
|
36
|
+
to?: string | undefined
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ─── Mode-specific config options for .config() ──────────
|
|
40
|
+
|
|
41
|
+
export type TransitionConfigOpts = {
|
|
42
|
+
appear?: boolean | undefined
|
|
43
|
+
unmount?: boolean | undefined
|
|
44
|
+
timeout?: number | undefined
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type CollapseConfigOpts = {
|
|
48
|
+
appear?: boolean | undefined
|
|
49
|
+
timeout?: number | undefined
|
|
50
|
+
transition?: string | undefined
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type StaggerConfigOpts = {
|
|
54
|
+
appear?: boolean | undefined
|
|
55
|
+
timeout?: number | undefined
|
|
56
|
+
interval?: number | undefined
|
|
57
|
+
reverseLeave?: boolean | undefined
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type GroupConfigOpts = {
|
|
61
|
+
appear?: boolean | undefined
|
|
62
|
+
timeout?: number | undefined
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ─── Mode-specific component props ───────────────────────
|
|
66
|
+
|
|
67
|
+
export type KineticTransitionProps<_Tag extends string> = Record<string, unknown> & {
|
|
68
|
+
show: () => boolean
|
|
69
|
+
appear?: boolean | undefined
|
|
70
|
+
unmount?: boolean | undefined
|
|
71
|
+
timeout?: number | undefined
|
|
72
|
+
children?: VNodeChild | undefined
|
|
73
|
+
} & Partial<TransitionCallbacks>
|
|
74
|
+
|
|
75
|
+
export type KineticCollapseProps<_Tag extends string> = Record<string, unknown> & {
|
|
76
|
+
show: () => boolean
|
|
77
|
+
appear?: boolean | undefined
|
|
78
|
+
timeout?: number | undefined
|
|
79
|
+
transition?: string | undefined
|
|
80
|
+
children?: VNodeChild | undefined
|
|
81
|
+
} & Partial<TransitionCallbacks>
|
|
82
|
+
|
|
83
|
+
export type KineticStaggerProps<_Tag extends string> = Record<string, unknown> & {
|
|
84
|
+
show: () => boolean
|
|
85
|
+
appear?: boolean | undefined
|
|
86
|
+
timeout?: number | undefined
|
|
87
|
+
interval?: number | undefined
|
|
88
|
+
reverseLeave?: boolean | undefined
|
|
89
|
+
children: VNodeChild
|
|
90
|
+
} & Partial<TransitionCallbacks>
|
|
91
|
+
|
|
92
|
+
export type KineticGroupProps<_Tag extends string> = Record<string, unknown> & {
|
|
93
|
+
appear?: boolean | undefined
|
|
94
|
+
timeout?: number | undefined
|
|
95
|
+
children: VNodeChild
|
|
96
|
+
} & Partial<TransitionCallbacks>
|
|
97
|
+
|
|
98
|
+
// ─── Conditional props based on mode ─────────────────────
|
|
99
|
+
|
|
100
|
+
export type KineticComponentProps<
|
|
101
|
+
Tag extends string,
|
|
102
|
+
Mode extends KineticMode,
|
|
103
|
+
> = Mode extends "collapse"
|
|
104
|
+
? KineticCollapseProps<Tag>
|
|
105
|
+
: Mode extends "stagger"
|
|
106
|
+
? KineticStaggerProps<Tag>
|
|
107
|
+
: Mode extends "group"
|
|
108
|
+
? KineticGroupProps<Tag>
|
|
109
|
+
: KineticTransitionProps<Tag>
|
|
110
|
+
|
|
111
|
+
// ─── Conditional config opts based on mode ───────────────
|
|
112
|
+
|
|
113
|
+
type ConfigOpts<Mode extends KineticMode> = Mode extends "collapse"
|
|
114
|
+
? CollapseConfigOpts
|
|
115
|
+
: Mode extends "stagger"
|
|
116
|
+
? StaggerConfigOpts
|
|
117
|
+
: Mode extends "group"
|
|
118
|
+
? GroupConfigOpts
|
|
119
|
+
: TransitionConfigOpts
|
|
120
|
+
|
|
121
|
+
// ─── Chain methods ───────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
export type KineticChain<Tag extends string, Mode extends KineticMode> = {
|
|
124
|
+
displayName: string
|
|
125
|
+
preset: (preset: StyleTransitionProps & ClassTransitionProps) => KineticComponent<Tag, Mode>
|
|
126
|
+
enter: (styles: CSSProperties) => KineticComponent<Tag, Mode>
|
|
127
|
+
enterTo: (styles: CSSProperties) => KineticComponent<Tag, Mode>
|
|
128
|
+
enterTransition: (value: string) => KineticComponent<Tag, Mode>
|
|
129
|
+
leave: (styles: CSSProperties) => KineticComponent<Tag, Mode>
|
|
130
|
+
leaveTo: (styles: CSSProperties) => KineticComponent<Tag, Mode>
|
|
131
|
+
leaveTransition: (value: string) => KineticComponent<Tag, Mode>
|
|
132
|
+
enterClass: (opts: ClassConfig) => KineticComponent<Tag, Mode>
|
|
133
|
+
leaveClass: (opts: ClassConfig) => KineticComponent<Tag, Mode>
|
|
134
|
+
config: (opts: ConfigOpts<Mode>) => KineticComponent<Tag, Mode>
|
|
135
|
+
on: (callbacks: Partial<TransitionCallbacks>) => KineticComponent<Tag, Mode>
|
|
136
|
+
collapse: (opts?: CollapseConfigOpts) => KineticComponent<Tag, "collapse">
|
|
137
|
+
stagger: (opts?: {
|
|
138
|
+
interval?: number | undefined
|
|
139
|
+
reverseLeave?: boolean | undefined
|
|
140
|
+
}) => KineticComponent<Tag, "stagger">
|
|
141
|
+
group: () => KineticComponent<Tag, "group">
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ─── The full kinetic component: renderable + chainable ───
|
|
145
|
+
|
|
146
|
+
export type KineticComponent<
|
|
147
|
+
Tag extends string,
|
|
148
|
+
Mode extends KineticMode = "transition",
|
|
149
|
+
> = ComponentFn<KineticComponentProps<Tag, Mode>> & KineticChain<Tag, Mode>
|
package/src/kinetic.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import createKineticComponent from "./kinetic/createKineticComponent"
|
|
2
|
+
import type { KineticComponent } from "./kinetic/types"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates a reusable animated component via immutable chaining.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* // Transition (default)
|
|
10
|
+
* const FadeDiv = kinetic('div').preset(fade)
|
|
11
|
+
*
|
|
12
|
+
* // Collapse
|
|
13
|
+
* const Accordion = kinetic('div').collapse()
|
|
14
|
+
*
|
|
15
|
+
* // Stagger
|
|
16
|
+
* const StaggerList = kinetic('ul').preset(slideUp).stagger({ interval: 50 })
|
|
17
|
+
*
|
|
18
|
+
* // Group (key-based enter/exit)
|
|
19
|
+
* const AnimatedList = kinetic('ul').preset(fade).group()
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
const kinetic = <Tag extends string>(tag: Tag): KineticComponent<Tag, "transition"> =>
|
|
23
|
+
createKineticComponent<Tag, "transition">({ tag, mode: "transition" })
|
|
24
|
+
|
|
25
|
+
export default kinetic
|
package/src/presets.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { ClassTransitionProps, StyleTransitionProps } from "./types"
|
|
2
|
+
|
|
3
|
+
export type Preset = StyleTransitionProps & ClassTransitionProps
|
|
4
|
+
|
|
5
|
+
export const fade: Preset = {
|
|
6
|
+
enterStyle: { opacity: 0 },
|
|
7
|
+
enterToStyle: { opacity: 1 },
|
|
8
|
+
enterTransition: "opacity 300ms ease-out",
|
|
9
|
+
leaveStyle: { opacity: 1 },
|
|
10
|
+
leaveToStyle: { opacity: 0 },
|
|
11
|
+
leaveTransition: "opacity 200ms ease-in",
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const scaleIn: Preset = {
|
|
15
|
+
enterStyle: { opacity: 0, transform: "scale(0.95)" },
|
|
16
|
+
enterToStyle: { opacity: 1, transform: "scale(1)" },
|
|
17
|
+
enterTransition: "opacity 300ms ease-out, transform 300ms ease-out",
|
|
18
|
+
leaveStyle: { opacity: 1, transform: "scale(1)" },
|
|
19
|
+
leaveToStyle: { opacity: 0, transform: "scale(0.95)" },
|
|
20
|
+
leaveTransition: "opacity 200ms ease-in, transform 200ms ease-in",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const slideUp: Preset = {
|
|
24
|
+
enterStyle: { opacity: 0, transform: "translateY(16px)" },
|
|
25
|
+
enterToStyle: { opacity: 1, transform: "translateY(0)" },
|
|
26
|
+
enterTransition: "opacity 300ms ease-out, transform 300ms ease-out",
|
|
27
|
+
leaveStyle: { opacity: 1, transform: "translateY(0)" },
|
|
28
|
+
leaveToStyle: { opacity: 0, transform: "translateY(16px)" },
|
|
29
|
+
leaveTransition: "opacity 200ms ease-in, transform 200ms ease-in",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const slideDown: Preset = {
|
|
33
|
+
enterStyle: { opacity: 0, transform: "translateY(-16px)" },
|
|
34
|
+
enterToStyle: { opacity: 1, transform: "translateY(0)" },
|
|
35
|
+
enterTransition: "opacity 300ms ease-out, transform 300ms ease-out",
|
|
36
|
+
leaveStyle: { opacity: 1, transform: "translateY(0)" },
|
|
37
|
+
leaveToStyle: { opacity: 0, transform: "translateY(-16px)" },
|
|
38
|
+
leaveTransition: "opacity 200ms ease-in, transform 200ms ease-in",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const slideLeft: Preset = {
|
|
42
|
+
enterStyle: { opacity: 0, transform: "translateX(16px)" },
|
|
43
|
+
enterToStyle: { opacity: 1, transform: "translateX(0)" },
|
|
44
|
+
enterTransition: "opacity 300ms ease-out, transform 300ms ease-out",
|
|
45
|
+
leaveStyle: { opacity: 1, transform: "translateX(0)" },
|
|
46
|
+
leaveToStyle: { opacity: 0, transform: "translateX(16px)" },
|
|
47
|
+
leaveTransition: "opacity 200ms ease-in, transform 200ms ease-in",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const slideRight: Preset = {
|
|
51
|
+
enterStyle: { opacity: 0, transform: "translateX(-16px)" },
|
|
52
|
+
enterToStyle: { opacity: 1, transform: "translateX(0)" },
|
|
53
|
+
enterTransition: "opacity 300ms ease-out, transform 300ms ease-out",
|
|
54
|
+
leaveStyle: { opacity: 1, transform: "translateX(0)" },
|
|
55
|
+
leaveToStyle: { opacity: 0, transform: "translateX(-16px)" },
|
|
56
|
+
leaveTransition: "opacity 200ms ease-in, transform 200ms ease-in",
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const presets = {
|
|
60
|
+
fade,
|
|
61
|
+
scaleIn,
|
|
62
|
+
slideUp,
|
|
63
|
+
slideDown,
|
|
64
|
+
slideLeft,
|
|
65
|
+
slideRight,
|
|
66
|
+
} as const
|