@pyreon/kinetic 0.11.1 → 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/src/types.ts ADDED
@@ -0,0 +1,118 @@
1
+ import type { Ref, VNode } from "@pyreon/core"
2
+ import type { Signal } from "@pyreon/reactivity"
3
+
4
+ export type CSSProperties = Record<string, string | number | undefined>
5
+
6
+ /** Internal lifecycle stages of a transition. */
7
+ export type TransitionStage = "hidden" | "entering" | "entered" | "leaving"
8
+
9
+ /** Class-based transition definition. */
10
+ export type ClassTransitionProps = {
11
+ /** Classes applied during the entire enter phase */
12
+ enter?: string | undefined
13
+ /** Classes applied on first frame of enter, removed on next frame */
14
+ enterFrom?: string | undefined
15
+ /** Classes applied on second frame of enter, kept until complete */
16
+ enterTo?: string | undefined
17
+ /** Classes applied during the entire leave phase */
18
+ leave?: string | undefined
19
+ /** Classes applied on first frame of leave */
20
+ leaveFrom?: string | undefined
21
+ /** Classes applied on second frame of leave, kept until complete */
22
+ leaveTo?: string | undefined
23
+ }
24
+
25
+ /** Style-object transition definition (zero-CSS option). */
26
+ export type StyleTransitionProps = {
27
+ /** Inline styles for the start of enter */
28
+ enterStyle?: CSSProperties | undefined
29
+ /** Inline styles for the end of enter */
30
+ enterToStyle?: CSSProperties | undefined
31
+ /** CSS transition shorthand applied during enter */
32
+ enterTransition?: string | undefined
33
+ /** Inline styles for the start of leave */
34
+ leaveStyle?: CSSProperties | undefined
35
+ /** Inline styles for the end of leave */
36
+ leaveToStyle?: CSSProperties | undefined
37
+ /** CSS transition shorthand applied during leave */
38
+ leaveTransition?: string | undefined
39
+ }
40
+
41
+ /** Lifecycle callbacks. */
42
+ export type TransitionCallbacks = {
43
+ /** Called immediately when entering begins */
44
+ onEnter?: (() => void) | undefined
45
+ /** Called when enter animation completes */
46
+ onAfterEnter?: (() => void) | undefined
47
+ /** Called immediately when leaving begins */
48
+ onLeave?: (() => void) | undefined
49
+ /** Called when leave animation completes */
50
+ onAfterLeave?: (() => void) | undefined
51
+ }
52
+
53
+ export type TransitionProps = ClassTransitionProps &
54
+ StyleTransitionProps &
55
+ TransitionCallbacks & {
56
+ /** Reactive accessor controlling visibility. true = enter, false = leave + unmount. */
57
+ show: () => boolean
58
+ /** If true, runs enter animation on initial mount. Default: false. */
59
+ appear?: boolean | undefined
60
+ /** If true (default), unmounts when hidden. If false, keeps with display:none. */
61
+ unmount?: boolean | undefined
62
+ /** Safety timeout in ms. Default: 5000. */
63
+ timeout?: number | undefined
64
+ /** Single child element. Must accept ref. */
65
+ children: VNode
66
+ }
67
+
68
+ export type TransitionGroupProps = ClassTransitionProps &
69
+ StyleTransitionProps &
70
+ TransitionCallbacks & {
71
+ /** If true, animates initial children on mount. Default: false. */
72
+ appear?: boolean | undefined
73
+ /** Safety timeout in ms. Default: 5000. */
74
+ timeout?: number | undefined
75
+ /** Children with unique keys. */
76
+ children: VNode[]
77
+ }
78
+
79
+ export type StaggerProps = ClassTransitionProps &
80
+ StyleTransitionProps &
81
+ TransitionCallbacks & {
82
+ /** Reactive accessor controlling visibility of all children. */
83
+ show: () => boolean
84
+ /** Delay between each child's animation start in ms. Default: 50. */
85
+ interval?: number | undefined
86
+ /** If true, reverses stagger order on leave. Default: false. */
87
+ reverseLeave?: boolean | undefined
88
+ /** If true, animates on initial mount. Default: false. */
89
+ appear?: boolean | undefined
90
+ /** Safety timeout in ms. Default: 5000. */
91
+ timeout?: number | undefined
92
+ /** Children to stagger. */
93
+ children: VNode[]
94
+ }
95
+
96
+ export type CollapseProps = TransitionCallbacks & {
97
+ /** Reactive accessor controlling expanded/collapsed state. */
98
+ show: () => boolean
99
+ /** CSS transition for height. Default: "height 300ms ease". */
100
+ transition?: string | undefined
101
+ /** If true, animates on initial mount. Default: false. */
102
+ appear?: boolean | undefined
103
+ /** Safety timeout in ms. Default: 5000. */
104
+ timeout?: number | undefined
105
+ /** The content to collapse. */
106
+ children: VNode
107
+ }
108
+
109
+ export type TransitionStateResult = {
110
+ /** Current lifecycle stage (signal) */
111
+ stage: Signal<TransitionStage>
112
+ /** Ref callback to attach to the transitioning element */
113
+ ref: Ref<HTMLElement> | ((node: HTMLElement | null) => void)
114
+ /** Reactive accessor: whether the element should be rendered */
115
+ shouldMount: () => boolean
116
+ /** Call when the current animation finishes */
117
+ complete: () => void
118
+ }
@@ -0,0 +1,59 @@
1
+ import type { Ref } from "@pyreon/core"
2
+ import { watch } from "@pyreon/reactivity"
3
+
4
+ const DEFAULT_TIMEOUT = 5000
5
+
6
+ export type UseAnimationEnd = (options: {
7
+ ref: Ref<HTMLElement>
8
+ onEnd: () => void
9
+ active: () => boolean
10
+ timeout?: number | undefined
11
+ }) => void
12
+
13
+ const useAnimationEnd: UseAnimationEnd = ({ ref, onEnd, active, timeout = DEFAULT_TIMEOUT }) => {
14
+ let called = false
15
+
16
+ watch(
17
+ active,
18
+ (isActive) => {
19
+ if (!isActive) {
20
+ called = false
21
+ return
22
+ }
23
+
24
+ const el = ref.current
25
+ if (!el) return
26
+
27
+ called = false
28
+
29
+ const done = () => {
30
+ if (called) return
31
+ called = true
32
+ el.removeEventListener("transitionend", handleEnd)
33
+ el.removeEventListener("animationend", handleEnd)
34
+ clearTimeout(timer)
35
+ onEnd()
36
+ }
37
+
38
+ const handleEnd = (e: Event) => {
39
+ // Ignore bubbled events from children
40
+ if (e.target !== el) return
41
+ done()
42
+ }
43
+
44
+ el.addEventListener("transitionend", handleEnd)
45
+ el.addEventListener("animationend", handleEnd)
46
+
47
+ const timer = setTimeout(done, timeout)
48
+
49
+ return () => {
50
+ el.removeEventListener("transitionend", handleEnd)
51
+ el.removeEventListener("animationend", handleEnd)
52
+ clearTimeout(timer)
53
+ }
54
+ },
55
+ { immediate: true },
56
+ )
57
+ }
58
+
59
+ export default useAnimationEnd
@@ -0,0 +1,28 @@
1
+ import { onMount, onUnmount } from "@pyreon/core"
2
+ import { signal } from "@pyreon/reactivity"
3
+
4
+ /**
5
+ * Inline reduced-motion check for kinetic package.
6
+ * Avoids depending on @pyreon/hooks for a single media query.
7
+ */
8
+ export function useReducedMotion(): () => boolean {
9
+ const matches = signal(false)
10
+ let mql: MediaQueryList | undefined
11
+
12
+ const onChange = (e: MediaQueryListEvent) => {
13
+ matches.set(e.matches)
14
+ }
15
+
16
+ onMount(() => {
17
+ mql = window.matchMedia("(prefers-reduced-motion: reduce)")
18
+ matches.set(mql.matches)
19
+ mql.addEventListener("change", onChange)
20
+ return undefined
21
+ })
22
+
23
+ onUnmount(() => {
24
+ mql?.removeEventListener("change", onChange)
25
+ })
26
+
27
+ return matches
28
+ }
@@ -0,0 +1,62 @@
1
+ import { createRef } from "@pyreon/core"
2
+ import { runUntracked, signal, watch } from "@pyreon/reactivity"
3
+ import type { TransitionStage, TransitionStateResult } from "./types"
4
+
5
+ export type UseTransitionState = (options: {
6
+ show: () => boolean
7
+ appear?: boolean | undefined
8
+ }) => TransitionStateResult
9
+
10
+ const useTransitionState: UseTransitionState = ({ show, appear = false }) => {
11
+ const initialShow = show()
12
+ // When appear=true and show starts true, mount the element (stage='entered')
13
+ // but defer the enter animation until the ref is connected.
14
+ const needsAppear = appear && initialShow
15
+ const stage = signal<TransitionStage>(initialShow ? "entered" : "hidden")
16
+ const elementRef = createRef<HTMLElement>()
17
+ let isInitialMount = true
18
+ let appearTriggered = false
19
+
20
+ // Ref callback that triggers the appear animation once the element is wired
21
+ const refCallback = (node: HTMLElement | null) => {
22
+ elementRef.current = node
23
+ if (node && needsAppear && !appearTriggered) {
24
+ appearTriggered = true
25
+ stage.set("entering")
26
+ }
27
+ }
28
+
29
+ watch(
30
+ show,
31
+ (showVal) => {
32
+ if (isInitialMount) {
33
+ isInitialMount = false
34
+ // appear case is handled by refCallback above
35
+ return
36
+ }
37
+
38
+ const currentStage = runUntracked(() => stage())
39
+ if (showVal && (currentStage === "hidden" || currentStage === "leaving")) {
40
+ stage.set("entering")
41
+ } else if (!showVal && (currentStage === "entered" || currentStage === "entering")) {
42
+ stage.set("leaving")
43
+ }
44
+ },
45
+ { immediate: true },
46
+ )
47
+
48
+ const complete = () => {
49
+ const current = stage()
50
+ if (current === "entering") stage.set("entered")
51
+ if (current === "leaving") stage.set("hidden")
52
+ }
53
+
54
+ return {
55
+ stage,
56
+ ref: refCallback,
57
+ shouldMount: () => stage() !== "hidden",
58
+ complete,
59
+ }
60
+ }
61
+
62
+ export default useTransitionState
package/src/utils.ts ADDED
@@ -0,0 +1,81 @@
1
+ import type { Ref, VNode } from "@pyreon/core"
2
+ import type { CSSProperties } from "./types"
3
+
4
+ const splitCache = new Map<string, string[]>()
5
+ const splitClasses = (classes: string): string[] => {
6
+ let cached = splitCache.get(classes)
7
+ if (!cached) {
8
+ cached = classes.split(/\s+/).filter(Boolean)
9
+ splitCache.set(classes, cached)
10
+ }
11
+ return cached
12
+ }
13
+
14
+ /** Adds space-separated CSS classes to an element. */
15
+ export const addClasses = (el: HTMLElement, classes: string | undefined) => {
16
+ if (!classes) return
17
+ const list = splitClasses(classes)
18
+ if (list.length > 0) el.classList.add(...list)
19
+ }
20
+
21
+ /** Removes space-separated CSS classes from an element. */
22
+ export const removeClasses = (el: HTMLElement, classes: string | undefined) => {
23
+ if (!classes) return
24
+ const list = splitClasses(classes)
25
+ if (list.length > 0) el.classList.remove(...list)
26
+ }
27
+
28
+ /**
29
+ * Executes callback after two animation frames (double-rAF).
30
+ * Ensures the browser paints the current state before applying changes,
31
+ * which is required for CSS transitions to trigger.
32
+ */
33
+ export const nextFrame = (callback: () => void): number =>
34
+ requestAnimationFrame(() => {
35
+ requestAnimationFrame(callback)
36
+ })
37
+
38
+ /** Merges two className strings, filtering undefined/empty. */
39
+ export const mergeClassNames = (
40
+ existing: string | undefined,
41
+ additional: string | undefined,
42
+ ): string | undefined => {
43
+ const parts = [existing, additional].filter(Boolean)
44
+ return parts.length > 0 ? parts.join(" ") : undefined
45
+ }
46
+
47
+ /** Merges two CSSProperties objects, with `b` taking precedence. */
48
+ export const mergeStyles = (
49
+ a: CSSProperties | undefined,
50
+ b: CSSProperties | undefined,
51
+ ): CSSProperties | undefined => {
52
+ if (!a && !b) return undefined
53
+ if (!a) return b
54
+ if (!b) return a
55
+ return { ...a, ...b }
56
+ }
57
+
58
+ // ─── Ref & Motion Utilities ─────────────────────────────────
59
+
60
+ type RefCallback<T> = (node: T | null) => void
61
+ type RefLike<T> = RefCallback<T> | Ref<T>
62
+
63
+ /** Merges multiple refs (callback or object) into a single callback ref. */
64
+ export const mergeRefs = <T>(...refs: (RefLike<T> | undefined)[]): ((node: T | null) => void) => {
65
+ return (node: T | null) => {
66
+ for (const ref of refs) {
67
+ if (!ref) continue
68
+ if (typeof ref === "function") {
69
+ ref(node)
70
+ } else {
71
+ ;(ref as { current: unknown }).current = node
72
+ }
73
+ }
74
+ }
75
+ }
76
+
77
+ /** Clones a VNode with merged props. */
78
+ export const cloneVNode = (vnode: VNode, extraProps: Record<string, unknown>): VNode => ({
79
+ ...vnode,
80
+ props: { ...vnode.props, ...extraProps },
81
+ })