@effect-tui/react 0.9.3 → 0.10.1

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 (85) hide show
  1. package/dist/src/components/ListView.d.ts.map +1 -1
  2. package/dist/src/components/ListView.js +7 -2
  3. package/dist/src/components/ListView.js.map +1 -1
  4. package/dist/src/dev/Toast.d.ts.map +1 -1
  5. package/dist/src/dev/Toast.js +24 -15
  6. package/dist/src/dev/Toast.js.map +1 -1
  7. package/dist/src/dev.d.ts +1 -1
  8. package/dist/src/dev.d.ts.map +1 -1
  9. package/dist/src/dev.js.map +1 -1
  10. package/dist/src/hooks/index.d.ts +2 -0
  11. package/dist/src/hooks/index.d.ts.map +1 -1
  12. package/dist/src/hooks/index.js +1 -0
  13. package/dist/src/hooks/index.js.map +1 -1
  14. package/dist/src/hooks/use-scroll.js +12 -12
  15. package/dist/src/hooks/use-scroll.js.map +1 -1
  16. package/dist/src/hooks/use-timer.d.ts +57 -0
  17. package/dist/src/hooks/use-timer.d.ts.map +1 -0
  18. package/dist/src/hooks/use-timer.js +94 -0
  19. package/dist/src/hooks/use-timer.js.map +1 -0
  20. package/dist/src/hosts/base.d.ts +28 -0
  21. package/dist/src/hosts/base.d.ts.map +1 -1
  22. package/dist/src/hosts/base.js +118 -0
  23. package/dist/src/hosts/base.js.map +1 -1
  24. package/dist/src/hosts/box.d.ts +6 -3
  25. package/dist/src/hosts/box.d.ts.map +1 -1
  26. package/dist/src/hosts/box.js +10 -3
  27. package/dist/src/hosts/box.js.map +1 -1
  28. package/dist/src/hosts/scroll.d.ts +0 -2
  29. package/dist/src/hosts/scroll.d.ts.map +1 -1
  30. package/dist/src/hosts/scroll.js +3 -7
  31. package/dist/src/hosts/scroll.js.map +1 -1
  32. package/dist/src/hosts/text.d.ts +5 -2
  33. package/dist/src/hosts/text.d.ts.map +1 -1
  34. package/dist/src/hosts/text.js +7 -3
  35. package/dist/src/hosts/text.js.map +1 -1
  36. package/dist/src/index.d.ts +2 -2
  37. package/dist/src/index.d.ts.map +1 -1
  38. package/dist/src/index.js +1 -1
  39. package/dist/src/index.js.map +1 -1
  40. package/dist/src/motion/brands.d.ts +9 -0
  41. package/dist/src/motion/brands.d.ts.map +1 -0
  42. package/dist/src/motion/brands.js +9 -0
  43. package/dist/src/motion/brands.js.map +1 -0
  44. package/dist/src/motion/color-motion-value.d.ts +4 -0
  45. package/dist/src/motion/color-motion-value.d.ts.map +1 -1
  46. package/dist/src/motion/color-motion-value.js +4 -0
  47. package/dist/src/motion/color-motion-value.js.map +1 -1
  48. package/dist/src/motion/hooks.d.ts +29 -3
  49. package/dist/src/motion/hooks.d.ts.map +1 -1
  50. package/dist/src/motion/hooks.js +40 -4
  51. package/dist/src/motion/hooks.js.map +1 -1
  52. package/dist/src/motion/index.d.ts +1 -1
  53. package/dist/src/motion/index.d.ts.map +1 -1
  54. package/dist/src/motion/index.js +1 -1
  55. package/dist/src/motion/index.js.map +1 -1
  56. package/dist/src/motion/motion-value.d.ts +6 -2
  57. package/dist/src/motion/motion-value.d.ts.map +1 -1
  58. package/dist/src/motion/motion-value.js +6 -2
  59. package/dist/src/motion/motion-value.js.map +1 -1
  60. package/dist/src/motion/use-sequence.d.ts +10 -2
  61. package/dist/src/motion/use-sequence.d.ts.map +1 -1
  62. package/dist/src/motion/use-sequence.js +101 -11
  63. package/dist/src/motion/use-sequence.js.map +1 -1
  64. package/dist/src/reconciler/types.d.ts +24 -1
  65. package/dist/src/reconciler/types.d.ts.map +1 -1
  66. package/dist/tsconfig.tsbuildinfo +1 -1
  67. package/package.json +2 -2
  68. package/src/components/ListView.tsx +8 -2
  69. package/src/dev/Toast.tsx +27 -17
  70. package/src/dev.tsx +4 -4
  71. package/src/hooks/index.ts +2 -0
  72. package/src/hooks/use-scroll.ts +12 -12
  73. package/src/hooks/use-timer.ts +155 -0
  74. package/src/hosts/base.ts +137 -0
  75. package/src/hosts/box.ts +17 -6
  76. package/src/hosts/scroll.ts +3 -7
  77. package/src/hosts/text.ts +13 -5
  78. package/src/index.ts +5 -1
  79. package/src/motion/brands.ts +10 -0
  80. package/src/motion/color-motion-value.ts +6 -0
  81. package/src/motion/hooks.ts +50 -4
  82. package/src/motion/index.ts +3 -0
  83. package/src/motion/motion-value.ts +8 -1
  84. package/src/motion/use-sequence.ts +113 -13
  85. package/src/reconciler/types.ts +21 -1
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Brand Symbols for MotionValue type detection.
3
+ * Separated to avoid circular dependency issues.
4
+ */
5
+
6
+ /** Brand Symbol for safer MotionValue detection */
7
+ export const MOTION_VALUE_BRAND = Symbol.for("effect-tui/MotionValue")
8
+
9
+ /** Brand Symbol for safer ColorMotionValue detection */
10
+ export const COLOR_MOTION_VALUE_BRAND = Symbol.for("effect-tui/ColorMotionValue")
@@ -2,16 +2,22 @@
2
2
  * ColorMotionValue - animates RGBA colors using 4 internal numeric springs.
3
3
  */
4
4
 
5
+ import { COLOR_MOTION_VALUE_BRAND } from "./brands.js"
5
6
  import { type ColorInput, parseColor, type RGBA } from "./color.js"
6
7
  import { EventEmitter } from "./event-emitter.js"
7
8
  import { MotionValue } from "./motion-value.js"
8
9
  import type { SpringOptions } from "./types.js"
9
10
 
11
+ export { COLOR_MOTION_VALUE_BRAND } from "./brands.js"
12
+
10
13
  /**
11
14
  * ColorMotionValue animates RGBA colors using 4 internal numeric springs.
12
15
  * Each channel (r, g, b, a) is animated independently with the same spring config.
13
16
  */
14
17
  export class ColorMotionValue extends EventEmitter<RGBA> {
18
+ /** @internal Brand for safe type detection */
19
+ readonly [COLOR_MOTION_VALUE_BRAND] = true
20
+
15
21
  private rMv: MotionValue<number>
16
22
  private gMv: MotionValue<number>
17
23
  private bMv: MotionValue<number>
@@ -2,7 +2,7 @@
2
2
  * React hooks for spring animations.
3
3
  */
4
4
 
5
- import { useCallback, useEffect, useRef } from "react"
5
+ import { useCallback, useEffect, useRef, useState } from "react"
6
6
  import { useRenderer } from "../renderer.js"
7
7
  import type { ColorInput } from "./color.js"
8
8
  import { ColorMotionValue } from "./color-motion-value.js"
@@ -211,13 +211,18 @@ export function useColorMotionValue(initial: ColorInput): ColorMotionValue {
211
211
  /**
212
212
  * Create a spring-animated color. Accepts hex, rgb(), hsl(), or {r,g,b} object.
213
213
  *
214
+ * Returns a MotionValue - best for <canvas draw={...}> where you read in callbacks.
215
+ * For JSX props, use useColorSpringState instead.
216
+ *
214
217
  * @example
215
218
  * const [colorMv, setColor] = useColorSpring("#ff0000", { visualDuration: 0.5 })
216
219
  * setColor("#00ff00") // Spring to green
217
- * setColor("hsl(240, 100%, 50%)") // Spring to blue
218
220
  *
219
- * // In draw callback
220
- * const { r, g, b } = colorMv.get()
221
+ * // In canvas draw callback
222
+ * <canvas draw={(ctx) => {
223
+ * const { r, g, b } = colorMv.get()
224
+ * ctx.fill(0, 0, 10, 5, "█", { fg: Colors.rgb(r, g, b) })
225
+ * }} />
221
226
  */
222
227
  export function useColorSpring(
223
228
  initial: ColorInput,
@@ -243,3 +248,44 @@ export function useColorSpring(
243
248
 
244
249
  return [mv, set]
245
250
  }
251
+
252
+ /**
253
+ * Like useColorSpring, but returns React state that updates during animation.
254
+ * Use this when you need spring values in JSX props.
255
+ *
256
+ * For <canvas draw={...}> callbacks, use useColorSpring instead (better perf).
257
+ *
258
+ * @example
259
+ * const [color, setColor] = useColorSpringState("#ff0000", { visualDuration: 0.5 })
260
+ * setColor("#00ff00") // Spring to green
261
+ *
262
+ * // In JSX - React re-renders as the spring animates
263
+ * <box bg={Colors.rgb(color.r, color.g, color.b)}>
264
+ * <text>Hello</text>
265
+ * </box>
266
+ */
267
+ export function useColorSpringState(
268
+ initial: ColorInput,
269
+ options?: SpringOptions,
270
+ ): [{ r: number; g: number; b: number; a: number }, (color: ColorInput) => void] {
271
+ const mv = useColorMotionValue(initial)
272
+ const [value, setValue] = useState(() => mv.get())
273
+ const optionsRef = useRef(options)
274
+ optionsRef.current = options
275
+
276
+ // Subscribe to spring changes and update React state
277
+ useEffect(() => {
278
+ return mv._subscribeChannels(() => {
279
+ setValue(mv.get())
280
+ })
281
+ }, [mv])
282
+
283
+ const set = useCallback(
284
+ (color: ColorInput) => {
285
+ mv.set(color, optionsRef.current)
286
+ },
287
+ [mv],
288
+ )
289
+
290
+ return [value, set]
291
+ }
@@ -5,10 +5,13 @@ export { useAnimationFrame } from "./frame.js"
5
5
  export type { ColorInput, MotionValue, RGBA, SpringOptions } from "./motion-value.js"
6
6
  export {
7
7
  // Color springs
8
+ COLOR_MOTION_VALUE_BRAND,
8
9
  ColorMotionValue,
10
+ MOTION_VALUE_BRAND,
9
11
  motionValue,
10
12
  useColorMotionValue,
11
13
  useColorSpring,
14
+ useColorSpringState,
12
15
  useMotionValue,
13
16
  useMotionValueEvent,
14
17
  useSpring,
@@ -3,11 +3,14 @@
3
3
  * Inspired by framer-motion's MotionValue.
4
4
  */
5
5
 
6
+ import { MOTION_VALUE_BRAND } from "./brands.js"
6
7
  import { EventEmitter } from "./event-emitter.js"
7
8
  import { subscribeFrame } from "./frame.js"
8
9
  import { createSpringResolver, springFromVisualDuration } from "./spring-math.js"
9
10
  import { DEFAULT_SPRING_OPTIONS, type SpringOptions } from "./types.js"
10
11
 
12
+ export { MOTION_VALUE_BRAND } from "./brands.js"
13
+
11
14
  /** Create a MotionValue. Factory function like Motion's motionValue(). */
12
15
  export function motionValue<T>(initial: T): MotionValue<T> {
13
16
  return new MotionValue(initial)
@@ -17,6 +20,9 @@ export function motionValue<T>(initial: T): MotionValue<T> {
17
20
  * MotionValue tracks a value and its velocity, supporting spring animations.
18
21
  */
19
22
  export class MotionValue<T = number> extends EventEmitter<T> {
23
+ /** @internal Brand for safe type detection */
24
+ readonly [MOTION_VALUE_BRAND] = true
25
+
20
26
  private current: T
21
27
  private target: T
22
28
  private velocity = 0
@@ -124,11 +130,12 @@ export class MotionValue<T = number> extends EventEmitter<T> {
124
130
  }
125
131
 
126
132
  export type { ColorInput, RGBA } from "./color.js"
127
- export { ColorMotionValue } from "./color-motion-value.js"
133
+ export { COLOR_MOTION_VALUE_BRAND, ColorMotionValue } from "./color-motion-value.js"
128
134
  export type { EventName, Subscriber } from "./event-emitter.js"
129
135
  export {
130
136
  useColorMotionValue,
131
137
  useColorSpring,
138
+ useColorSpringState,
132
139
  useMotionValue,
133
140
  useMotionValueEvent,
134
141
  useSpring,
@@ -1,17 +1,51 @@
1
1
  // useSequence - Timed discrete state sequences (motion.dev-style keyframes)
2
2
  // For animating through discrete values like emojis, strings, or any non-numeric state
3
3
 
4
- import { useEffect, useState } from "react"
4
+ import { useCallback, useEffect, useRef, useState } from "react"
5
5
 
6
6
  export interface SequenceOptions<T> {
7
7
  /** Array of values to animate through */
8
8
  keyframes: T[]
9
- /** Position of each keyframe as 0-1 fraction (must match keyframes length) */
10
- times: number[]
9
+ /** Position of each keyframe as 0-1 fraction (defaults to evenly spaced) */
10
+ times?: number[]
11
11
  /** Total duration in milliseconds */
12
12
  duration: number
13
+ /** Initial delay before starting (ms) */
14
+ delay?: number
13
15
  /** Start automatically (default: true) */
14
16
  autoPlay?: boolean
17
+ /** Change this value to replay the sequence */
18
+ playKey?: unknown
19
+ /** Optional callback when the sequence finishes */
20
+ onComplete?: () => void
21
+ /** Optional callback for each keyframe change */
22
+ onUpdate?: (value: T, index: number) => void
23
+ }
24
+
25
+ function clamp01(value: number) {
26
+ if (!Number.isFinite(value)) return 0
27
+ if (value < 0) return 0
28
+ if (value > 1) return 1
29
+ return value
30
+ }
31
+
32
+ function resolveTimes(count: number, times?: number[]): number[] {
33
+ if (count <= 0) return []
34
+ if (!times || times.length !== count) {
35
+ if (count === 1) return [0]
36
+ const step = 1 / (count - 1)
37
+ return Array.from({ length: count }, (_, i) => i * step)
38
+ }
39
+
40
+ const resolved = new Array<number>(count)
41
+ let prev = 0
42
+ resolved[0] = 0
43
+ for (let i = 1; i < count; i++) {
44
+ const next = Math.max(prev, clamp01(times[i] ?? 0))
45
+ resolved[i] = next
46
+ prev = next
47
+ }
48
+ return resolved
15
49
  }
16
50
 
17
51
  /**
@@ -30,22 +64,88 @@ export interface SequenceOptions<T> {
30
64
  * ```
31
65
  */
32
66
  export function useSequence<T>(options: SequenceOptions<T>): T {
33
- const { keyframes, times, duration, autoPlay = true } = options
67
+ const { keyframes, autoPlay = true, playKey } = options
34
68
  const [index, setIndex] = useState(0)
69
+ const optionsRef = useRef(options)
70
+ optionsRef.current = options
71
+ const timersRef = useRef<NodeJS.Timeout[]>([])
72
+ const runIdRef = useRef(0)
73
+ const prevPlayKeyRef = useRef<unknown>(playKey)
74
+ const hasInitializedRef = useRef(false)
35
75
 
36
- useEffect(() => {
37
- if (!autoPlay || keyframes.length <= 1) return
76
+ const clearTimers = useCallback(() => {
77
+ for (const timer of timersRef.current) {
78
+ clearTimeout(timer)
79
+ }
80
+ timersRef.current = []
81
+ }, [])
82
+
83
+ const play = useCallback(() => {
84
+ const { keyframes, times, duration, delay = 0, onComplete, onUpdate } = optionsRef.current
85
+ clearTimers()
86
+ runIdRef.current += 1
87
+ const runId = runIdRef.current
88
+
89
+ if (keyframes.length === 0) return
90
+
91
+ setIndex((prev) => (prev === 0 ? prev : 0))
92
+ onUpdate?.(keyframes[0], 0)
93
+
94
+ if (keyframes.length === 1) {
95
+ onComplete?.()
96
+ return
97
+ }
38
98
 
39
- const timers: NodeJS.Timeout[] = []
99
+ const resolvedTimes = resolveTimes(keyframes.length, times)
100
+ const durationMs = Number.isFinite(duration) ? Math.max(0, duration) : 0
101
+ const delayMs = Number.isFinite(delay) ? Math.max(0, delay) : 0
40
102
 
41
- // Schedule each keyframe transition
42
103
  for (let i = 1; i < keyframes.length; i++) {
43
- const delayMs = times[i]! * duration
44
- timers.push(setTimeout(() => setIndex(i), delayMs))
104
+ const targetDelay = delayMs + resolvedTimes[i]! * durationMs
105
+ const timer = setTimeout(() => {
106
+ if (runId !== runIdRef.current) return
107
+ setIndex(i)
108
+ onUpdate?.(keyframes[i]!, i)
109
+ if (i === keyframes.length - 1) {
110
+ onComplete?.()
111
+ }
112
+ }, targetDelay)
113
+ timersRef.current.push(timer)
114
+ }
115
+ }, [clearTimers])
116
+
117
+ // Handle autoPlay on initial mount
118
+ useEffect(() => {
119
+ if (hasInitializedRef.current) return
120
+ hasInitializedRef.current = true
121
+ if (autoPlay) {
122
+ play()
45
123
  }
124
+ return () => clearTimers()
125
+ }, [autoPlay, play, clearTimers])
46
126
 
47
- return () => timers.forEach(clearTimeout)
48
- }, [keyframes, times, duration, autoPlay])
127
+ // Handle playKey changes - ALWAYS replay regardless of autoPlay
128
+ useEffect(() => {
129
+ if (playKey !== prevPlayKeyRef.current) {
130
+ prevPlayKeyRef.current = playKey
131
+ play()
132
+ }
133
+ }, [playKey, play])
134
+
135
+ useEffect(() => {
136
+ return () => clearTimers()
137
+ }, [clearTimers])
138
+
139
+ useEffect(() => {
140
+ if (keyframes.length === 0) return
141
+ if (index >= keyframes.length) {
142
+ setIndex(keyframes.length - 1)
143
+ }
144
+ }, [index, keyframes.length])
49
145
 
50
- return keyframes[index]!
146
+ if (keyframes.length === 0) {
147
+ return undefined as T
148
+ }
149
+ const safeIndex = Math.min(index, keyframes.length - 1)
150
+ return keyframes[safeIndex]!
51
151
  }
@@ -132,6 +132,26 @@ export interface CommonProps {
132
132
  /** Maximum height - natural size won't exceed this */
133
133
  maxHeight?: number
134
134
 
135
- /** Index signature for Record<string, unknown> compatibility */
135
+ /**
136
+ * Called after layout with the element's final dimensions.
137
+ * Fires only when size changes (deduplicated).
138
+ *
139
+ * @example
140
+ * ```tsx
141
+ * const [size, setSize] = useState({ width: 0, height: 0 })
142
+ * <vstack greedy onLayout={setSize}>
143
+ * <text>Width: {size.width}</text>
144
+ * </vstack>
145
+ * ```
146
+ */
147
+ onLayout?: (size: { width: number; height: number; x: number; y: number }) => void
148
+
149
+ /** React key prop for list reconciliation */
150
+ key?: string | number
151
+
152
+ /**
153
+ * Index signature for Record<string, unknown> compatibility.
154
+ * TODO: Remove this and properly type all props to catch typos like "grow" instead of "greedy"
155
+ */
136
156
  [key: string]: unknown
137
157
  }