@effect-tui/react 0.9.2 → 0.10.0

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 (80) hide show
  1. package/dist/src/dev/Toast.d.ts.map +1 -1
  2. package/dist/src/dev/Toast.js +24 -15
  3. package/dist/src/dev/Toast.js.map +1 -1
  4. package/dist/src/dev.d.ts +20 -1
  5. package/dist/src/dev.d.ts.map +1 -1
  6. package/dist/src/dev.js +6 -3
  7. package/dist/src/dev.js.map +1 -1
  8. package/dist/src/hooks/index.d.ts +2 -0
  9. package/dist/src/hooks/index.d.ts.map +1 -1
  10. package/dist/src/hooks/index.js +1 -0
  11. package/dist/src/hooks/index.js.map +1 -1
  12. package/dist/src/hooks/use-scroll.d.ts.map +1 -1
  13. package/dist/src/hooks/use-scroll.js +4 -2
  14. package/dist/src/hooks/use-scroll.js.map +1 -1
  15. package/dist/src/hooks/use-timer.d.ts +57 -0
  16. package/dist/src/hooks/use-timer.d.ts.map +1 -0
  17. package/dist/src/hooks/use-timer.js +94 -0
  18. package/dist/src/hooks/use-timer.js.map +1 -0
  19. package/dist/src/hosts/base.d.ts +20 -0
  20. package/dist/src/hosts/base.d.ts.map +1 -1
  21. package/dist/src/hosts/base.js +104 -0
  22. package/dist/src/hosts/base.js.map +1 -1
  23. package/dist/src/hosts/box.d.ts +6 -3
  24. package/dist/src/hosts/box.d.ts.map +1 -1
  25. package/dist/src/hosts/box.js +10 -3
  26. package/dist/src/hosts/box.js.map +1 -1
  27. package/dist/src/hosts/scroll.d.ts +0 -2
  28. package/dist/src/hosts/scroll.d.ts.map +1 -1
  29. package/dist/src/hosts/scroll.js +3 -6
  30. package/dist/src/hosts/scroll.js.map +1 -1
  31. package/dist/src/hosts/text.d.ts +5 -2
  32. package/dist/src/hosts/text.d.ts.map +1 -1
  33. package/dist/src/hosts/text.js +7 -3
  34. package/dist/src/hosts/text.js.map +1 -1
  35. package/dist/src/index.d.ts +1 -1
  36. package/dist/src/index.d.ts.map +1 -1
  37. package/dist/src/index.js +1 -1
  38. package/dist/src/index.js.map +1 -1
  39. package/dist/src/motion/brands.d.ts +9 -0
  40. package/dist/src/motion/brands.d.ts.map +1 -0
  41. package/dist/src/motion/brands.js +9 -0
  42. package/dist/src/motion/brands.js.map +1 -0
  43. package/dist/src/motion/color-motion-value.d.ts +4 -0
  44. package/dist/src/motion/color-motion-value.d.ts.map +1 -1
  45. package/dist/src/motion/color-motion-value.js +4 -0
  46. package/dist/src/motion/color-motion-value.js.map +1 -1
  47. package/dist/src/motion/hooks.d.ts +29 -3
  48. package/dist/src/motion/hooks.d.ts.map +1 -1
  49. package/dist/src/motion/hooks.js +40 -4
  50. package/dist/src/motion/hooks.js.map +1 -1
  51. package/dist/src/motion/index.d.ts +1 -1
  52. package/dist/src/motion/index.d.ts.map +1 -1
  53. package/dist/src/motion/index.js +1 -1
  54. package/dist/src/motion/index.js.map +1 -1
  55. package/dist/src/motion/motion-value.d.ts +6 -2
  56. package/dist/src/motion/motion-value.d.ts.map +1 -1
  57. package/dist/src/motion/motion-value.js +6 -2
  58. package/dist/src/motion/motion-value.js.map +1 -1
  59. package/dist/src/motion/use-sequence.d.ts +10 -2
  60. package/dist/src/motion/use-sequence.d.ts.map +1 -1
  61. package/dist/src/motion/use-sequence.js +101 -11
  62. package/dist/src/motion/use-sequence.js.map +1 -1
  63. package/dist/tsconfig.tsbuildinfo +1 -1
  64. package/package.json +2 -2
  65. package/src/dev/Toast.tsx +27 -17
  66. package/src/dev.tsx +19 -14
  67. package/src/hooks/index.ts +2 -0
  68. package/src/hooks/use-scroll.ts +4 -2
  69. package/src/hooks/use-timer.ts +155 -0
  70. package/src/hosts/base.ts +120 -0
  71. package/src/hosts/box.ts +17 -6
  72. package/src/hosts/scroll.ts +3 -6
  73. package/src/hosts/text.ts +13 -5
  74. package/src/index.ts +2 -0
  75. package/src/motion/brands.ts +10 -0
  76. package/src/motion/color-motion-value.ts +6 -0
  77. package/src/motion/hooks.ts +50 -4
  78. package/src/motion/index.ts +3 -0
  79. package/src/motion/motion-value.ts +8 -1
  80. package/src/motion/use-sequence.ts +113 -13
@@ -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
  }