@effect-tui/react 0.10.0 → 0.10.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/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/hooks/use-scroll.d.ts.map +1 -1
- package/dist/src/hooks/use-scroll.js +12 -4
- package/dist/src/hooks/use-scroll.js.map +1 -1
- package/dist/src/hooks/use-timer.d.ts +2 -2
- package/dist/src/hooks/use-timer.d.ts.map +1 -1
- package/dist/src/hooks/use-timer.js +2 -2
- package/dist/src/hooks/use-timer.js.map +1 -1
- package/dist/src/hosts/base.d.ts +8 -0
- package/dist/src/hosts/base.d.ts.map +1 -1
- package/dist/src/hosts/base.js +14 -0
- package/dist/src/hosts/base.js.map +1 -1
- package/dist/src/hosts/box.js.map +1 -1
- package/dist/src/hosts/canvas.js.map +1 -1
- package/dist/src/hosts/codeblock.js.map +1 -1
- package/dist/src/hosts/flex-container.js.map +1 -1
- package/dist/src/hosts/hstack.d.ts +3 -5
- package/dist/src/hosts/hstack.d.ts.map +1 -1
- package/dist/src/hosts/hstack.js.map +1 -1
- package/dist/src/hosts/overlay-item.js.map +1 -1
- package/dist/src/hosts/scroll.js.map +1 -1
- package/dist/src/hosts/spacer.js.map +1 -1
- package/dist/src/hosts/text.js.map +1 -1
- package/dist/src/hosts/vstack.d.ts +3 -5
- package/dist/src/hosts/vstack.d.ts.map +1 -1
- package/dist/src/hosts/vstack.js.map +1 -1
- package/dist/src/hosts/zstack.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/reconciler/types.d.ts +24 -1
- package/dist/src/reconciler/types.d.ts.map +1 -1
- package/dist/src/test-grow.d.ts +3 -0
- package/dist/src/test-grow.d.ts.map +1 -0
- package/dist/src/test-grow.js +8 -0
- package/dist/src/test-grow.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/components/ListView.tsx +8 -2
- package/src/hooks/use-scroll.ts +12 -4
- package/src/hooks/use-timer.ts +9 -6
- package/src/hosts/base.ts +17 -0
- package/src/hosts/box.ts +1 -1
- package/src/hosts/canvas.ts +1 -1
- package/src/hosts/codeblock.ts +1 -1
- package/src/hosts/flex-container.ts +1 -1
- package/src/hosts/hstack.ts +2 -5
- package/src/hosts/overlay-item.ts +1 -1
- package/src/hosts/scroll.ts +1 -1
- package/src/hosts/spacer.ts +1 -1
- package/src/hosts/text.ts +3 -3
- package/src/hosts/vstack.ts +2 -5
- package/src/hosts/zstack.ts +1 -1
- package/src/index.ts +5 -1
- package/src/reconciler/types.ts +21 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effect-tui/react",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.2",
|
|
4
4
|
"description": "React bindings for @effect-tui/core",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"prepublishOnly": "bun run typecheck && bun run build"
|
|
84
84
|
},
|
|
85
85
|
"dependencies": {
|
|
86
|
-
"@effect-tui/core": "^0.10.
|
|
86
|
+
"@effect-tui/core": "^0.10.2",
|
|
87
87
|
"@effect/platform": "^0.94.0",
|
|
88
88
|
"@effect/platform-bun": "^0.87.0",
|
|
89
89
|
"@effect/rpc": "^0.73.0",
|
|
@@ -123,12 +123,18 @@ export function ListView<T>({
|
|
|
123
123
|
|
|
124
124
|
// Track previous selection to detect changes
|
|
125
125
|
const prevSelectedRef = useRef(selectedIndex)
|
|
126
|
+
// Track previous scrollToVisible reference to detect viewport changes
|
|
127
|
+
const prevScrollToVisibleRef = useRef(scrollToVisible)
|
|
126
128
|
|
|
127
|
-
// Scroll to keep selection visible when it changes
|
|
129
|
+
// Scroll to keep selection visible when it changes OR when viewport size changes
|
|
128
130
|
// useLayoutEffect runs before paint - prevents visible "jump" when navigating
|
|
129
131
|
useLayoutEffect(() => {
|
|
130
|
-
|
|
132
|
+
const selectionChanged = selectedIndex !== prevSelectedRef.current
|
|
133
|
+
const viewportChanged = scrollToVisible !== prevScrollToVisibleRef.current
|
|
134
|
+
|
|
135
|
+
if (selectionChanged || viewportChanged) {
|
|
131
136
|
prevSelectedRef.current = selectedIndex
|
|
137
|
+
prevScrollToVisibleRef.current = scrollToVisible
|
|
132
138
|
// Pass totalHeight to avoid stale contentSize issues when jumping to end
|
|
133
139
|
scrollToVisible(selectedIndex, itemHeight, scrollPadding, totalHeight)
|
|
134
140
|
}
|
package/src/hooks/use-scroll.ts
CHANGED
|
@@ -353,7 +353,9 @@ export function useScroll(options: UseScrollOptions = {}): UseScrollReturn {
|
|
|
353
353
|
useKeyboard(handleKey)
|
|
354
354
|
|
|
355
355
|
// Scroll to make a position visible (for keeping selection in view)
|
|
356
|
-
// Uses refs to avoid stale closures -
|
|
356
|
+
// Uses refs to avoid stale closures, but re-creates when viewport size changes
|
|
357
|
+
// so selection effects can re-run after measurement updates.
|
|
358
|
+
// Bypasses clampOffset because it uses totalSize for accurate clamping
|
|
357
359
|
const scrollToVisible = useCallback(
|
|
358
360
|
(position: number, itemSize = 1, padding = 0, totalSize?: number) => {
|
|
359
361
|
const currentOffset = offsetRef.current
|
|
@@ -367,15 +369,21 @@ export function useScroll(options: UseScrollOptions = {}): UseScrollReturn {
|
|
|
367
369
|
|
|
368
370
|
// If item is above viewport, scroll up to show it
|
|
369
371
|
if (isAbove) {
|
|
370
|
-
|
|
372
|
+
const newOffset = Math.max(0, itemStart - padding)
|
|
373
|
+
offsetRef.current = newOffset
|
|
374
|
+
wasAtEndRef.current = false
|
|
375
|
+
setOffsetRaw(newOffset)
|
|
371
376
|
}
|
|
372
377
|
// If item is below viewport, scroll down to show it
|
|
373
378
|
else if (isBelow) {
|
|
374
379
|
const currentMaxOffset = Math.max(0, effectiveContentSize - currentViewportSize)
|
|
375
|
-
|
|
380
|
+
const newOffset = Math.min(currentMaxOffset, itemEnd - currentViewportSize + padding)
|
|
381
|
+
offsetRef.current = newOffset
|
|
382
|
+
wasAtEndRef.current = newOffset >= currentMaxOffset - 1
|
|
383
|
+
setOffsetRaw(newOffset)
|
|
376
384
|
}
|
|
377
385
|
},
|
|
378
|
-
[contentSize,
|
|
386
|
+
[contentSize, viewportSize],
|
|
379
387
|
)
|
|
380
388
|
|
|
381
389
|
const state: ScrollState = {
|
package/src/hooks/use-timer.ts
CHANGED
|
@@ -33,8 +33,8 @@ export interface UseTimerReturn {
|
|
|
33
33
|
start: () => void
|
|
34
34
|
/** Pause the timer */
|
|
35
35
|
pause: () => void
|
|
36
|
-
/** Reset timer to initialTime and stop */
|
|
37
|
-
reset: () => void
|
|
36
|
+
/** Reset timer to initialTime (or provided time) and stop */
|
|
37
|
+
reset: (newTime?: number) => void
|
|
38
38
|
/** Current timer status */
|
|
39
39
|
status: TimerStatus
|
|
40
40
|
}
|
|
@@ -101,10 +101,13 @@ export function useTimer(config: UseTimerConfig): UseTimerReturn {
|
|
|
101
101
|
setStatus((prev) => (prev === "RUNNING" ? "PAUSED" : prev))
|
|
102
102
|
}, [])
|
|
103
103
|
|
|
104
|
-
const reset = useCallback(
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
104
|
+
const reset = useCallback(
|
|
105
|
+
(newTime?: number) => {
|
|
106
|
+
setTime(newTime ?? initialTime)
|
|
107
|
+
setStatus("STOPPED")
|
|
108
|
+
},
|
|
109
|
+
[initialTime],
|
|
110
|
+
)
|
|
108
111
|
|
|
109
112
|
// Timer effect
|
|
110
113
|
useEffect(() => {
|
package/src/hosts/base.ts
CHANGED
|
@@ -88,6 +88,13 @@ export abstract class BaseHost implements HostInstance {
|
|
|
88
88
|
/** @internal Marks this node as static content (for Static component) */
|
|
89
89
|
__static?: boolean
|
|
90
90
|
|
|
91
|
+
// ─────────────────────────────────────────────────────────────
|
|
92
|
+
// onLayout callback - fires when layout size changes
|
|
93
|
+
// ─────────────────────────────────────────────────────────────
|
|
94
|
+
onLayout?: (size: { width: number; height: number; x: number; y: number }) => void
|
|
95
|
+
private _lastLayoutW = -1
|
|
96
|
+
private _lastLayoutH = -1
|
|
97
|
+
|
|
91
98
|
protected ctx: HostContext
|
|
92
99
|
|
|
93
100
|
// ─────────────────────────────────────────────────────────────
|
|
@@ -200,6 +207,13 @@ export abstract class BaseHost implements HostInstance {
|
|
|
200
207
|
if (this.frameMaxHeight !== undefined) h = Math.min(this.frameMaxHeight, h)
|
|
201
208
|
|
|
202
209
|
this.rect = { x: rect.x, y: rect.y, w, h }
|
|
210
|
+
|
|
211
|
+
// Fire onLayout callback if size changed (deduplicated)
|
|
212
|
+
if (this.onLayout && (w !== this._lastLayoutW || h !== this._lastLayoutH)) {
|
|
213
|
+
this._lastLayoutW = w
|
|
214
|
+
this._lastLayoutH = h
|
|
215
|
+
this.onLayout({ width: w, height: h, x: rect.x, y: rect.y })
|
|
216
|
+
}
|
|
203
217
|
}
|
|
204
218
|
|
|
205
219
|
/**
|
|
@@ -303,6 +317,9 @@ export abstract class BaseHost implements HostInstance {
|
|
|
303
317
|
this.frameMaxWidth = typeof props.maxWidth === "number" ? props.maxWidth : undefined
|
|
304
318
|
this.frameMinHeight = typeof props.minHeight === "number" ? props.minHeight : undefined
|
|
305
319
|
this.frameMaxHeight = typeof props.maxHeight === "number" ? props.maxHeight : undefined
|
|
320
|
+
|
|
321
|
+
// onLayout callback
|
|
322
|
+
this.onLayout = typeof props.onLayout === "function" ? (props.onLayout as typeof this.onLayout) : undefined
|
|
306
323
|
}
|
|
307
324
|
|
|
308
325
|
destroy(): void {
|
package/src/hosts/box.ts
CHANGED
|
@@ -40,7 +40,7 @@ export class BoxHost extends SingleChildHost {
|
|
|
40
40
|
|
|
41
41
|
constructor(props: BoxProps, ctx: HostContext) {
|
|
42
42
|
super("box", props, ctx)
|
|
43
|
-
this.updateProps(props)
|
|
43
|
+
this.updateProps(props as Record<string, unknown>)
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
private get borderThickness(): number {
|
package/src/hosts/canvas.ts
CHANGED
|
@@ -75,7 +75,7 @@ export class CanvasHost extends BaseHost {
|
|
|
75
75
|
|
|
76
76
|
constructor(props: CanvasProps, ctx: HostContext) {
|
|
77
77
|
super("canvas", props, ctx)
|
|
78
|
-
this.updateProps(props)
|
|
78
|
+
this.updateProps(props as Record<string, unknown>)
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
measure(maxW: number, maxH: number): Size {
|
package/src/hosts/codeblock.ts
CHANGED
|
@@ -30,7 +30,7 @@ export class CodeBlockHost extends BaseHost {
|
|
|
30
30
|
|
|
31
31
|
constructor(props: CodeBlockProps, ctx: HostContext) {
|
|
32
32
|
super("codeblock", props, ctx)
|
|
33
|
-
this.updateProps(props)
|
|
33
|
+
this.updateProps(props as Record<string, unknown>)
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
private computeGutterWidth(): number {
|
|
@@ -46,7 +46,7 @@ export class FlexContainerHost<A extends FlexAxis> extends BaseHost {
|
|
|
46
46
|
) {
|
|
47
47
|
super(elementType, props, ctx)
|
|
48
48
|
this.alignment = defaultAlignment
|
|
49
|
-
this.updateProps(props)
|
|
49
|
+
this.updateProps(props as Record<string, unknown>)
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
/** Get children excluding __static nodes (which are rendered separately) */
|
package/src/hosts/hstack.ts
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { HostContext } from "../reconciler/types.js"
|
|
2
2
|
import { FlexContainerHost, type FlexContainerProps } from "./flex-container.js"
|
|
3
3
|
|
|
4
|
-
export interface HStackProps extends
|
|
5
|
-
spacing?: number
|
|
6
|
-
alignment?: "top" | "center" | "bottom"
|
|
7
|
-
}
|
|
4
|
+
export interface HStackProps extends FlexContainerProps<"horizontal"> {}
|
|
8
5
|
|
|
9
6
|
/**
|
|
10
7
|
* HStackHost lays out children horizontally with optional spacing and cross-axis alignment.
|
|
@@ -23,7 +23,7 @@ export class OverlayItemHost extends SingleChildHost {
|
|
|
23
23
|
|
|
24
24
|
constructor(props: OverlayItemProps, ctx: HostContext) {
|
|
25
25
|
super("overlayItem", props, ctx)
|
|
26
|
-
this.updateProps(props)
|
|
26
|
+
this.updateProps(props as Record<string, unknown>)
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
override measure(maxW: number, maxH: number): Size {
|
package/src/hosts/scroll.ts
CHANGED
|
@@ -64,7 +64,7 @@ export class ScrollHost extends SingleChildHost {
|
|
|
64
64
|
|
|
65
65
|
constructor(props: ScrollProps, ctx: HostContext) {
|
|
66
66
|
super("scroll", props, ctx)
|
|
67
|
-
this.updateProps(props)
|
|
67
|
+
this.updateProps(props as Record<string, unknown>)
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
measure(maxW: number, maxH: number): Size {
|
package/src/hosts/spacer.ts
CHANGED
|
@@ -18,7 +18,7 @@ export class SpacerHost extends BaseHost {
|
|
|
18
18
|
|
|
19
19
|
constructor(props: SpacerProps, ctx: HostContext) {
|
|
20
20
|
super("spacer", props, ctx)
|
|
21
|
-
this.updateProps(props)
|
|
21
|
+
this.updateProps(props as Record<string, unknown>)
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
measure(_maxW: number, _maxH: number): Size {
|
package/src/hosts/text.ts
CHANGED
|
@@ -38,7 +38,7 @@ export class TextHost extends BaseHost {
|
|
|
38
38
|
|
|
39
39
|
constructor(props: TextProps, ctx: HostContext) {
|
|
40
40
|
super("text", props, ctx)
|
|
41
|
-
this.updateProps(props)
|
|
41
|
+
this.updateProps(props as Record<string, unknown>)
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/** Check if we have SpanHost children (requires styled rendering) */
|
|
@@ -447,7 +447,7 @@ export class SpanHost extends BaseHost {
|
|
|
447
447
|
|
|
448
448
|
constructor(props: SpanProps, ctx: HostContext) {
|
|
449
449
|
super("span", props, ctx)
|
|
450
|
-
this.updateProps(props)
|
|
450
|
+
this.updateProps(props as Record<string, unknown>)
|
|
451
451
|
}
|
|
452
452
|
|
|
453
453
|
/** Get text content from RawTextHost children */
|
|
@@ -523,7 +523,7 @@ export class StyledTextHost extends BaseHost {
|
|
|
523
523
|
|
|
524
524
|
constructor(props: StyledTextProps, ctx: HostContext) {
|
|
525
525
|
super("styledtext", props, ctx)
|
|
526
|
-
this.updateProps(props)
|
|
526
|
+
this.updateProps(props as Record<string, unknown>)
|
|
527
527
|
}
|
|
528
528
|
|
|
529
529
|
measure(maxW: number, maxH: number): Size {
|
package/src/hosts/vstack.ts
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { HostContext } from "../reconciler/types.js"
|
|
2
2
|
import { FlexContainerHost, type FlexContainerProps } from "./flex-container.js"
|
|
3
3
|
|
|
4
|
-
export interface VStackProps extends
|
|
5
|
-
spacing?: number
|
|
6
|
-
alignment?: "leading" | "center" | "trailing"
|
|
7
|
-
}
|
|
4
|
+
export interface VStackProps extends FlexContainerProps<"vertical"> {}
|
|
8
5
|
|
|
9
6
|
/**
|
|
10
7
|
* VStackHost lays out children vertically with optional spacing and cross-axis alignment.
|
package/src/hosts/zstack.ts
CHANGED
|
@@ -15,7 +15,7 @@ export class ZStackHost extends BaseHost {
|
|
|
15
15
|
|
|
16
16
|
constructor(props: ZStackProps, ctx: HostContext) {
|
|
17
17
|
super("zstack", props, ctx)
|
|
18
|
-
this.updateProps(props)
|
|
18
|
+
this.updateProps(props as Record<string, unknown>)
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
measure(maxW: number, maxH: number): Size {
|
package/src/index.ts
CHANGED
|
@@ -48,13 +48,17 @@ export {
|
|
|
48
48
|
} from "./highlight.js"
|
|
49
49
|
export type {
|
|
50
50
|
ScrollState,
|
|
51
|
+
TimerStatus,
|
|
52
|
+
TimerType,
|
|
51
53
|
UseKeyboardOptions,
|
|
52
54
|
UseMouseOptions,
|
|
53
55
|
UseScrollOptions,
|
|
54
56
|
UseScrollReturn,
|
|
57
|
+
UseTimerConfig,
|
|
58
|
+
UseTimerReturn,
|
|
55
59
|
} from "./hooks/index.js"
|
|
56
60
|
// Hooks
|
|
57
|
-
export { useKeyboard, useMouse, usePaste, useQuit, useScroll } from "./hooks/index.js"
|
|
61
|
+
export { useKeyboard, useMouse, usePaste, useQuit, useScroll, useTimer } from "./hooks/index.js"
|
|
58
62
|
export { useFrameStats } from "./hooks/useFrameStats.js"
|
|
59
63
|
export type { BorderKind, BoxProps } from "./hosts/box.js"
|
|
60
64
|
export type { CanvasProps, DrawContext } from "./hosts/canvas.js"
|
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
|
}
|