@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.
@@ -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