@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.
- package/dist/src/components/ListView.d.ts.map +1 -1
- package/dist/src/components/ListView.js +7 -2
- package/dist/src/components/ListView.js.map +1 -1
- package/dist/src/dev/Toast.d.ts.map +1 -1
- package/dist/src/dev/Toast.js +24 -15
- package/dist/src/dev/Toast.js.map +1 -1
- package/dist/src/dev.d.ts +1 -1
- package/dist/src/dev.d.ts.map +1 -1
- package/dist/src/dev.js.map +1 -1
- package/dist/src/hooks/index.d.ts +2 -0
- package/dist/src/hooks/index.d.ts.map +1 -1
- package/dist/src/hooks/index.js +1 -0
- package/dist/src/hooks/index.js.map +1 -1
- package/dist/src/hooks/use-scroll.js +12 -12
- package/dist/src/hooks/use-scroll.js.map +1 -1
- package/dist/src/hooks/use-timer.d.ts +57 -0
- package/dist/src/hooks/use-timer.d.ts.map +1 -0
- package/dist/src/hooks/use-timer.js +94 -0
- package/dist/src/hooks/use-timer.js.map +1 -0
- package/dist/src/hosts/base.d.ts +28 -0
- package/dist/src/hosts/base.d.ts.map +1 -1
- package/dist/src/hosts/base.js +118 -0
- package/dist/src/hosts/base.js.map +1 -1
- package/dist/src/hosts/box.d.ts +6 -3
- package/dist/src/hosts/box.d.ts.map +1 -1
- package/dist/src/hosts/box.js +10 -3
- package/dist/src/hosts/box.js.map +1 -1
- package/dist/src/hosts/scroll.d.ts +0 -2
- package/dist/src/hosts/scroll.d.ts.map +1 -1
- package/dist/src/hosts/scroll.js +3 -7
- package/dist/src/hosts/scroll.js.map +1 -1
- package/dist/src/hosts/text.d.ts +5 -2
- package/dist/src/hosts/text.d.ts.map +1 -1
- package/dist/src/hosts/text.js +7 -3
- package/dist/src/hosts/text.js.map +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/motion/brands.d.ts +9 -0
- package/dist/src/motion/brands.d.ts.map +1 -0
- package/dist/src/motion/brands.js +9 -0
- package/dist/src/motion/brands.js.map +1 -0
- package/dist/src/motion/color-motion-value.d.ts +4 -0
- package/dist/src/motion/color-motion-value.d.ts.map +1 -1
- package/dist/src/motion/color-motion-value.js +4 -0
- package/dist/src/motion/color-motion-value.js.map +1 -1
- package/dist/src/motion/hooks.d.ts +29 -3
- package/dist/src/motion/hooks.d.ts.map +1 -1
- package/dist/src/motion/hooks.js +40 -4
- package/dist/src/motion/hooks.js.map +1 -1
- package/dist/src/motion/index.d.ts +1 -1
- package/dist/src/motion/index.d.ts.map +1 -1
- package/dist/src/motion/index.js +1 -1
- package/dist/src/motion/index.js.map +1 -1
- package/dist/src/motion/motion-value.d.ts +6 -2
- package/dist/src/motion/motion-value.d.ts.map +1 -1
- package/dist/src/motion/motion-value.js +6 -2
- package/dist/src/motion/motion-value.js.map +1 -1
- package/dist/src/motion/use-sequence.d.ts +10 -2
- package/dist/src/motion/use-sequence.d.ts.map +1 -1
- package/dist/src/motion/use-sequence.js +101 -11
- package/dist/src/motion/use-sequence.js.map +1 -1
- package/dist/src/reconciler/types.d.ts +24 -1
- package/dist/src/reconciler/types.d.ts.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/components/ListView.tsx +8 -2
- package/src/dev/Toast.tsx +27 -17
- package/src/dev.tsx +4 -4
- package/src/hooks/index.ts +2 -0
- package/src/hooks/use-scroll.ts +12 -12
- package/src/hooks/use-timer.ts +155 -0
- package/src/hosts/base.ts +137 -0
- package/src/hosts/box.ts +17 -6
- package/src/hosts/scroll.ts +3 -7
- package/src/hosts/text.ts +13 -5
- package/src/index.ts +5 -1
- package/src/motion/brands.ts +10 -0
- package/src/motion/color-motion-value.ts +6 -0
- package/src/motion/hooks.ts +50 -4
- package/src/motion/index.ts +3 -0
- package/src/motion/motion-value.ts +8 -1
- package/src/motion/use-sequence.ts +113 -13
- 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>
|
package/src/motion/hooks.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
+
}
|
package/src/motion/index.ts
CHANGED
|
@@ -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 (
|
|
10
|
-
times
|
|
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,
|
|
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
|
-
|
|
37
|
-
|
|
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
|
|
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
|
|
44
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/reconciler/types.ts
CHANGED
|
@@ -132,6 +132,26 @@ export interface CommonProps {
|
|
|
132
132
|
/** Maximum height - natural size won't exceed this */
|
|
133
133
|
maxHeight?: number
|
|
134
134
|
|
|
135
|
-
/**
|
|
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
|
}
|