@effect-tui/react 0.15.2 → 0.16.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.
- package/README.md +2 -2
- package/dist/src/components/ListView.d.ts +4 -4
- package/dist/src/components/ListView.d.ts.map +1 -1
- package/dist/src/components/ListView.js +16 -17
- package/dist/src/components/ListView.js.map +1 -1
- package/dist/src/console/ConsolePopover.d.ts +7 -1
- package/dist/src/console/ConsolePopover.d.ts.map +1 -1
- package/dist/src/console/ConsolePopover.js +55 -74
- package/dist/src/console/ConsolePopover.js.map +1 -1
- package/dist/src/debug/DebugOverlay.d.ts.map +1 -1
- package/dist/src/debug/DebugOverlay.js +3 -57
- package/dist/src/debug/DebugOverlay.js.map +1 -1
- package/dist/src/debug/DiagnosticsPanel.js +1 -1
- package/dist/src/debug/DiagnosticsPanel.js.map +1 -1
- package/dist/src/dev.d.ts +5 -117
- package/dist/src/dev.d.ts.map +1 -1
- package/dist/src/dev.js +3 -333
- package/dist/src/dev.js.map +1 -1
- package/dist/src/hooks/use-scroll.d.ts +31 -35
- package/dist/src/hooks/use-scroll.d.ts.map +1 -1
- package/dist/src/hooks/use-scroll.js +51 -90
- package/dist/src/hooks/use-scroll.js.map +1 -1
- package/dist/src/hosts/canvas.d.ts +2 -2
- package/dist/src/hosts/canvas.d.ts.map +1 -1
- package/dist/src/hosts/canvas.js +8 -10
- package/dist/src/hosts/canvas.js.map +1 -1
- package/dist/src/hosts/codeblock.d.ts +2 -2
- package/dist/src/hosts/codeblock.js +2 -2
- package/dist/src/hosts/flex-container.d.ts +1 -1
- package/dist/src/hosts/flex-container.d.ts.map +1 -1
- package/dist/src/hosts/flex-container.js +3 -3
- package/dist/src/hosts/flex-container.js.map +1 -1
- package/dist/src/hosts/index.d.ts +2 -1
- package/dist/src/hosts/index.d.ts.map +1 -1
- package/dist/src/hosts/index.js +2 -1
- package/dist/src/hosts/index.js.map +1 -1
- package/dist/src/hosts/layout-helpers.d.ts +10 -0
- package/dist/src/hosts/layout-helpers.d.ts.map +1 -0
- package/dist/src/hosts/layout-helpers.js +10 -0
- package/dist/src/hosts/layout-helpers.js.map +1 -0
- package/dist/src/hosts/leaf.d.ts +14 -0
- package/dist/src/hosts/leaf.d.ts.map +1 -0
- package/dist/src/hosts/leaf.js +31 -0
- package/dist/src/hosts/leaf.js.map +1 -0
- package/dist/src/hosts/overlay.d.ts.map +1 -1
- package/dist/src/hosts/overlay.js +4 -7
- package/dist/src/hosts/overlay.js.map +1 -1
- package/dist/src/hosts/scroll.d.ts +47 -24
- package/dist/src/hosts/scroll.d.ts.map +1 -1
- package/dist/src/hosts/scroll.js +68 -51
- package/dist/src/hosts/scroll.js.map +1 -1
- package/dist/src/hosts/spacer.d.ts +2 -2
- package/dist/src/hosts/spacer.js +2 -2
- package/dist/src/hosts/text.d.ts +2 -3
- package/dist/src/hosts/text.d.ts.map +1 -1
- package/dist/src/hosts/text.js +5 -61
- package/dist/src/hosts/text.js.map +1 -1
- package/dist/src/hosts/vstack.js +1 -1
- package/dist/src/hosts/vstack.js.map +1 -1
- package/dist/src/hosts/zstack.d.ts +1 -1
- package/dist/src/hosts/zstack.d.ts.map +1 -1
- package/dist/src/hosts/zstack.js +6 -6
- package/dist/src/hosts/zstack.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/internal/dev/hmr.d.ts +20 -0
- package/dist/src/internal/dev/hmr.d.ts.map +1 -0
- package/dist/src/internal/dev/hmr.js +93 -0
- package/dist/src/internal/dev/hmr.js.map +1 -0
- package/dist/src/internal/dev/runtime.d.ts +24 -0
- package/dist/src/internal/dev/runtime.d.ts.map +1 -0
- package/dist/src/internal/dev/runtime.js +135 -0
- package/dist/src/internal/dev/runtime.js.map +1 -0
- package/dist/src/internal/dev/ui.d.ts +13 -0
- package/dist/src/internal/dev/ui.d.ts.map +1 -0
- package/dist/src/internal/dev/ui.js +51 -0
- package/dist/src/internal/dev/ui.js.map +1 -0
- package/dist/src/internal/renderer/context.d.ts +9 -0
- package/dist/src/internal/renderer/context.d.ts.map +1 -0
- package/dist/src/internal/renderer/context.js +22 -0
- package/dist/src/internal/renderer/context.js.map +1 -0
- package/dist/src/internal/renderer/core/FrameBuilder.d.ts +18 -0
- package/dist/src/internal/renderer/core/FrameBuilder.d.ts.map +1 -0
- package/dist/src/internal/renderer/core/FrameBuilder.js +40 -0
- package/dist/src/internal/renderer/core/FrameBuilder.js.map +1 -0
- package/dist/src/internal/renderer/core/RendererState.d.ts +41 -0
- package/dist/src/internal/renderer/core/RendererState.d.ts.map +1 -0
- package/dist/src/internal/renderer/core/RendererState.js +70 -0
- package/dist/src/internal/renderer/core/RendererState.js.map +1 -0
- package/dist/src/internal/renderer/core/index.d.ts +3 -0
- package/dist/src/internal/renderer/core/index.d.ts.map +1 -0
- package/dist/src/internal/renderer/core/index.js +3 -0
- package/dist/src/internal/renderer/core/index.js.map +1 -0
- package/dist/src/internal/renderer/index.d.ts +40 -0
- package/dist/src/internal/renderer/index.d.ts.map +1 -0
- package/dist/src/internal/renderer/index.js +518 -0
- package/dist/src/internal/renderer/index.js.map +1 -0
- package/dist/src/internal/renderer/input/InputProcessor.d.ts +30 -0
- package/dist/src/internal/renderer/input/InputProcessor.d.ts.map +1 -0
- package/dist/src/internal/renderer/input/InputProcessor.js +122 -0
- package/dist/src/internal/renderer/input/InputProcessor.js.map +1 -0
- package/dist/src/internal/renderer/input/index.d.ts +2 -0
- package/dist/src/internal/renderer/input/index.d.ts.map +1 -0
- package/dist/src/internal/renderer/input/index.js +2 -0
- package/dist/src/internal/renderer/input/index.js.map +1 -0
- package/dist/src/internal/renderer/lifecycle/EventBus.d.ts +42 -0
- package/dist/src/internal/renderer/lifecycle/EventBus.d.ts.map +1 -0
- package/dist/src/internal/renderer/lifecycle/EventBus.js +97 -0
- package/dist/src/internal/renderer/lifecycle/EventBus.js.map +1 -0
- package/dist/src/internal/renderer/lifecycle/ProcessLifecycle.d.ts +13 -0
- package/dist/src/internal/renderer/lifecycle/ProcessLifecycle.d.ts.map +1 -0
- package/dist/src/internal/renderer/lifecycle/ProcessLifecycle.js +111 -0
- package/dist/src/internal/renderer/lifecycle/ProcessLifecycle.js.map +1 -0
- package/dist/src/internal/renderer/lifecycle/RenderCache.d.ts +3 -0
- package/dist/src/internal/renderer/lifecycle/RenderCache.d.ts.map +1 -0
- package/dist/src/internal/renderer/lifecycle/RenderCache.js +9 -0
- package/dist/src/internal/renderer/lifecycle/RenderCache.js.map +1 -0
- package/dist/src/internal/renderer/lifecycle/index.d.ts +4 -0
- package/dist/src/internal/renderer/lifecycle/index.d.ts.map +1 -0
- package/dist/src/internal/renderer/lifecycle/index.js +4 -0
- package/dist/src/internal/renderer/lifecycle/index.js.map +1 -0
- package/dist/src/internal/renderer/modes/FullscreenRenderer.d.ts +12 -0
- package/dist/src/internal/renderer/modes/FullscreenRenderer.d.ts.map +1 -0
- package/dist/src/internal/renderer/modes/FullscreenRenderer.js +54 -0
- package/dist/src/internal/renderer/modes/FullscreenRenderer.js.map +1 -0
- package/dist/src/internal/renderer/modes/InlineRenderer.d.ts +25 -0
- package/dist/src/internal/renderer/modes/InlineRenderer.d.ts.map +1 -0
- package/dist/src/internal/renderer/modes/InlineRenderer.js +166 -0
- package/dist/src/internal/renderer/modes/InlineRenderer.js.map +1 -0
- package/dist/src/internal/renderer/modes/RendererMode.d.ts +42 -0
- package/dist/src/internal/renderer/modes/RendererMode.d.ts.map +1 -0
- package/dist/src/internal/renderer/modes/RendererMode.js +2 -0
- package/dist/src/internal/renderer/modes/RendererMode.js.map +1 -0
- package/dist/src/internal/renderer/modes/StaticContentRenderer.d.ts +25 -0
- package/dist/src/internal/renderer/modes/StaticContentRenderer.d.ts.map +1 -0
- package/dist/src/internal/renderer/modes/StaticContentRenderer.js +49 -0
- package/dist/src/internal/renderer/modes/StaticContentRenderer.js.map +1 -0
- package/dist/src/internal/renderer/modes/index.d.ts +5 -0
- package/dist/src/internal/renderer/modes/index.d.ts.map +1 -0
- package/dist/src/internal/renderer/modes/index.js +4 -0
- package/dist/src/internal/renderer/modes/index.js.map +1 -0
- package/dist/src/internal/renderer/terminal/KeyboardCapabilityProbe.d.ts +13 -0
- package/dist/src/internal/renderer/terminal/KeyboardCapabilityProbe.d.ts.map +1 -0
- package/dist/src/internal/renderer/terminal/KeyboardCapabilityProbe.js +75 -0
- package/dist/src/internal/renderer/terminal/KeyboardCapabilityProbe.js.map +1 -0
- package/dist/src/internal/renderer/terminal/TerminalSetup.d.ts +29 -0
- package/dist/src/internal/renderer/terminal/TerminalSetup.d.ts.map +1 -0
- package/dist/src/internal/renderer/terminal/TerminalSetup.js +82 -0
- package/dist/src/internal/renderer/terminal/TerminalSetup.js.map +1 -0
- package/dist/src/internal/renderer/terminal/index.d.ts +3 -0
- package/dist/src/internal/renderer/terminal/index.d.ts.map +1 -0
- package/dist/src/internal/renderer/terminal/index.js +3 -0
- package/dist/src/internal/renderer/terminal/index.js.map +1 -0
- package/dist/src/internal/renderer/types.d.ts +118 -0
- package/dist/src/internal/renderer/types.d.ts.map +1 -0
- package/dist/src/internal/renderer/types.js +2 -0
- package/dist/src/internal/renderer/types.js.map +1 -0
- package/dist/src/renderer-context.d.ts +1 -8
- package/dist/src/renderer-context.d.ts.map +1 -1
- package/dist/src/renderer-context.js +1 -21
- package/dist/src/renderer-context.js.map +1 -1
- package/dist/src/renderer-types.d.ts +1 -115
- package/dist/src/renderer-types.d.ts.map +1 -1
- package/dist/src/renderer.d.ts +1 -31
- package/dist/src/renderer.d.ts.map +1 -1
- package/dist/src/renderer.js +1 -495
- package/dist/src/renderer.js.map +1 -1
- package/dist/src/test/render-tui.d.ts +3 -3
- package/dist/src/test/render-tui.d.ts.map +1 -1
- package/dist/src/test/render-tui.js +16 -9
- package/dist/src/test/render-tui.js.map +1 -1
- package/dist/src/utils/alignment.d.ts +1 -1
- package/dist/src/utils/alignment.d.ts.map +1 -1
- package/dist/src/utils/alignment.js +0 -2
- package/dist/src/utils/alignment.js.map +1 -1
- package/dist/src/utils/console-helpers.d.ts +19 -0
- package/dist/src/utils/console-helpers.d.ts.map +1 -0
- package/dist/src/utils/console-helpers.js +61 -0
- package/dist/src/utils/console-helpers.js.map +1 -0
- package/dist/src/utils/index.d.ts +1 -1
- package/dist/src/utils/index.d.ts.map +1 -1
- package/dist/src/utils/index.js +1 -1
- package/dist/src/utils/index.js.map +1 -1
- package/dist/src/utils/styles.d.ts +8 -1
- package/dist/src/utils/styles.d.ts.map +1 -1
- package/dist/src/utils/styles.js +10 -8
- package/dist/src/utils/styles.js.map +1 -1
- package/dist/src/utils/text-wrap.d.ts +5 -0
- package/dist/src/utils/text-wrap.d.ts.map +1 -1
- package/dist/src/utils/text-wrap.js +110 -48
- package/dist/src/utils/text-wrap.js.map +1 -1
- package/dist/src/visualize/index.js +1 -1
- package/dist/src/visualize/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/components/ListView.tsx +21 -23
- package/src/console/ConsolePopover.tsx +124 -107
- package/src/debug/DebugOverlay.ts +15 -74
- package/src/debug/DiagnosticsPanel.tsx +1 -1
- package/src/dev.tsx +5 -458
- package/src/hooks/use-scroll.ts +85 -145
- package/src/hosts/canvas.ts +8 -11
- package/src/hosts/codeblock.ts +2 -2
- package/src/hosts/flex-container.ts +4 -4
- package/src/hosts/index.ts +10 -1
- package/src/hosts/layout-helpers.ts +20 -0
- package/src/hosts/leaf.ts +36 -0
- package/src/hosts/overlay.ts +11 -9
- package/src/hosts/scroll.ts +94 -69
- package/src/hosts/spacer.ts +2 -2
- package/src/hosts/text.ts +5 -58
- package/src/hosts/vstack.ts +1 -1
- package/src/hosts/zstack.ts +7 -7
- package/src/index.ts +1 -1
- package/src/internal/dev/hmr.ts +101 -0
- package/src/internal/dev/runtime.ts +170 -0
- package/src/internal/dev/ui.tsx +87 -0
- package/src/internal/renderer/context.ts +27 -0
- package/src/{renderer → internal/renderer}/core/FrameBuilder.ts +2 -2
- package/src/internal/renderer/index.ts +656 -0
- package/src/{renderer → internal/renderer}/input/InputProcessor.ts +10 -1
- package/src/{renderer → internal/renderer}/lifecycle/EventBus.ts +9 -1
- package/src/internal/renderer/lifecycle/ProcessLifecycle.ts +125 -0
- package/src/internal/renderer/lifecycle/index.ts +3 -0
- package/src/{renderer → internal/renderer}/modes/InlineRenderer.ts +5 -2
- package/src/{renderer → internal/renderer}/modes/RendererMode.ts +1 -1
- package/src/{renderer → internal/renderer}/modes/StaticContentRenderer.ts +5 -2
- package/src/internal/renderer/terminal/KeyboardCapabilityProbe.ts +91 -0
- package/src/{renderer/lifecycle → internal/renderer/terminal}/TerminalSetup.ts +4 -22
- package/src/internal/renderer/terminal/index.ts +2 -0
- package/src/internal/renderer/types.ts +125 -0
- package/src/renderer-context.ts +1 -27
- package/src/renderer-types.ts +10 -123
- package/src/renderer.ts +1 -619
- package/src/test/render-tui.ts +16 -10
- package/src/utils/alignment.ts +1 -3
- package/src/utils/console-helpers.ts +86 -0
- package/src/utils/index.ts +1 -1
- package/src/utils/styles.ts +16 -4
- package/src/utils/text-wrap.ts +139 -48
- package/src/visualize/index.tsx +1 -1
- package/src/renderer/lifecycle/ResizeManager.ts +0 -65
- package/src/renderer/lifecycle/index.ts +0 -4
- /package/src/{renderer → internal/renderer}/core/RendererState.ts +0 -0
- /package/src/{renderer → internal/renderer}/core/index.ts +0 -0
- /package/src/{renderer → internal/renderer}/input/index.ts +0 -0
- /package/src/{renderer → internal/renderer}/lifecycle/RenderCache.ts +0 -0
- /package/src/{renderer → internal/renderer}/modes/FullscreenRenderer.ts +0 -0
- /package/src/{renderer → internal/renderer}/modes/index.ts +0 -0
package/src/hosts/scroll.ts
CHANGED
|
@@ -4,15 +4,32 @@ import type { CommonProps, HostContext, Rect, Size } from "../reconciler/types.j
|
|
|
4
4
|
import { fillRectWithInheritedBg } from "../utils/index.js"
|
|
5
5
|
import { SingleChildHost } from "./single-child.js"
|
|
6
6
|
|
|
7
|
+
export type ScrollAxis = "vertical" | "horizontal" | "both"
|
|
8
|
+
export type ScrollAlignX = "left" | "center" | "right"
|
|
9
|
+
export type ScrollAlignY = "top" | "center" | "bottom"
|
|
10
|
+
export type ScrollAlign = ScrollAlignX | ScrollAlignY | { x?: ScrollAlignX; y?: ScrollAlignY }
|
|
11
|
+
export interface ScrollLayoutChange {
|
|
12
|
+
content: { width: number; height: number }
|
|
13
|
+
viewport: { width: number; height: number }
|
|
14
|
+
offset: { x: number; y: number }
|
|
15
|
+
rect: { x: number; y: number; w: number; h: number }
|
|
16
|
+
axis: ScrollAxis
|
|
17
|
+
}
|
|
18
|
+
|
|
7
19
|
export interface ScrollProps extends CommonProps {
|
|
8
20
|
/** Scroll axis: "vertical" (default), "horizontal", or "both" */
|
|
9
|
-
axis?:
|
|
10
|
-
/**
|
|
11
|
-
|
|
12
|
-
/** Horizontal offset
|
|
21
|
+
axis?: ScrollAxis
|
|
22
|
+
/** Vertical scroll offset in pixels (0 = top) */
|
|
23
|
+
offsetY?: number
|
|
24
|
+
/** Horizontal scroll offset in pixels (0 = left) */
|
|
13
25
|
offsetX?: number
|
|
14
|
-
/**
|
|
15
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Alignment when content is smaller than viewport.
|
|
28
|
+
* - axis="vertical": "top" | "center" | "bottom"
|
|
29
|
+
* - axis="horizontal": "left" | "center" | "right"
|
|
30
|
+
* - axis="both": { x, y }
|
|
31
|
+
*/
|
|
32
|
+
align?: ScrollAlign
|
|
16
33
|
/** Background color for the scroll viewport */
|
|
17
34
|
bg?: Color
|
|
18
35
|
/** Whether to show scrollbar indicators */
|
|
@@ -22,34 +39,43 @@ export interface ScrollProps extends CommonProps {
|
|
|
22
39
|
* This is handled in the host itself for instant updates (no React roundtrip).
|
|
23
40
|
*/
|
|
24
41
|
sticky?: boolean
|
|
25
|
-
/** Called when
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
42
|
+
/** Called when scroll layout changes (content/viewport/offset/rect). */
|
|
43
|
+
onScrollLayoutChange?: (event: ScrollLayoutChange) => void
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const isAlignX = (value: string): value is ScrollAlignX => value === "left" || value === "center" || value === "right"
|
|
47
|
+
const isAlignY = (value: string): value is ScrollAlignY => value === "top" || value === "center" || value === "bottom"
|
|
48
|
+
|
|
49
|
+
const resolveAlign = (align: ScrollAlign | undefined): { x: ScrollAlignX; y: ScrollAlignY } => {
|
|
50
|
+
let x: ScrollAlignX | undefined
|
|
51
|
+
let y: ScrollAlignY | undefined
|
|
52
|
+
|
|
53
|
+
if (typeof align === "string") {
|
|
54
|
+
if (isAlignX(align)) x = align
|
|
55
|
+
if (isAlignY(align)) y = align
|
|
56
|
+
} else if (align) {
|
|
57
|
+
x = align.x
|
|
58
|
+
y = align.y
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
x: x ?? "left",
|
|
63
|
+
y: y ?? "top",
|
|
64
|
+
}
|
|
35
65
|
}
|
|
36
66
|
|
|
37
67
|
export class ScrollHost extends SingleChildHost {
|
|
38
|
-
axis:
|
|
39
|
-
|
|
68
|
+
axis: ScrollAxis = "vertical"
|
|
69
|
+
offsetY = 0
|
|
40
70
|
offsetX = 0
|
|
41
|
-
align
|
|
71
|
+
align?: ScrollAlign
|
|
42
72
|
bg?: Color
|
|
43
73
|
showScrollbar = true
|
|
44
74
|
sticky = false
|
|
45
75
|
|
|
46
76
|
// Scroll is greedy by default - expands to fill available space
|
|
47
77
|
override greedy: boolean | number | undefined = 1
|
|
48
|
-
|
|
49
|
-
onViewportSize?: (width: number, height: number) => void
|
|
50
|
-
onEffectiveOffset?: (offset: number) => void
|
|
51
|
-
onEffectiveOffsetX?: (offsetX: number) => void
|
|
52
|
-
onRect?: (x: number, y: number, w: number, h: number) => void
|
|
78
|
+
onScrollLayoutChange?: (event: ScrollLayoutChange) => void
|
|
53
79
|
|
|
54
80
|
// Measured content dimensions (full size before clipping)
|
|
55
81
|
private contentWidth = 0
|
|
@@ -63,6 +89,8 @@ export class ScrollHost extends SingleChildHost {
|
|
|
63
89
|
private lastRectY = -1
|
|
64
90
|
private lastRectW = -1
|
|
65
91
|
private lastRectH = -1
|
|
92
|
+
private lastOffsetY = -1
|
|
93
|
+
private lastOffsetX = -1
|
|
66
94
|
// Track if we were at end (for sticky behavior)
|
|
67
95
|
private wasAtEnd = true
|
|
68
96
|
private wasAtEndX = true
|
|
@@ -96,7 +124,7 @@ export class ScrollHost extends SingleChildHost {
|
|
|
96
124
|
const childSize = child.measure(childMaxW, childMaxH)
|
|
97
125
|
this.contentWidth = childSize.w
|
|
98
126
|
this.contentHeight = childSize.h
|
|
99
|
-
// Note:
|
|
127
|
+
// Note: onScrollLayoutChange callback is deferred to layout() to keep measure() pure
|
|
100
128
|
}
|
|
101
129
|
|
|
102
130
|
// Report natural content size (clamped to constrained bounds)
|
|
@@ -109,34 +137,16 @@ export class ScrollHost extends SingleChildHost {
|
|
|
109
137
|
|
|
110
138
|
override layout(rect: Rect): void {
|
|
111
139
|
const layoutRect = this.layoutWithConstraints(rect)
|
|
140
|
+
const { x: alignX, y: alignY } = resolveAlign(this.align)
|
|
112
141
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
this.onContentSize?.(this.contentWidth, this.contentHeight)
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Report viewport size if changed (for useScroll hook)
|
|
121
|
-
if (this.onViewportSize && (layoutRect.w !== this.lastViewportW || layoutRect.h !== this.lastViewportH)) {
|
|
122
|
-
this.lastViewportW = layoutRect.w
|
|
123
|
-
this.lastViewportH = layoutRect.h
|
|
124
|
-
this.onViewportSize(layoutRect.w, layoutRect.h)
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Report rect if position changed (for hit testing)
|
|
128
|
-
if (
|
|
142
|
+
const contentChanged =
|
|
143
|
+
this.contentWidth !== this.lastReportedContentW || this.contentHeight !== this.lastReportedContentH
|
|
144
|
+
const viewportChanged = layoutRect.w !== this.lastViewportW || layoutRect.h !== this.lastViewportH
|
|
145
|
+
const rectChanged =
|
|
129
146
|
layoutRect.x !== this.lastRectX ||
|
|
130
147
|
layoutRect.y !== this.lastRectY ||
|
|
131
148
|
layoutRect.w !== this.lastRectW ||
|
|
132
149
|
layoutRect.h !== this.lastRectH
|
|
133
|
-
) {
|
|
134
|
-
this.lastRectX = layoutRect.x
|
|
135
|
-
this.lastRectY = layoutRect.y
|
|
136
|
-
this.lastRectW = layoutRect.w
|
|
137
|
-
this.lastRectH = layoutRect.h
|
|
138
|
-
this.onRect?.(layoutRect.x, layoutRect.y, layoutRect.w, layoutRect.h)
|
|
139
|
-
}
|
|
140
150
|
|
|
141
151
|
const child = this.child
|
|
142
152
|
if (!child) return
|
|
@@ -146,7 +156,7 @@ export class ScrollHost extends SingleChildHost {
|
|
|
146
156
|
const maxScrollX = Math.max(0, this.contentWidth - layoutRect.w)
|
|
147
157
|
|
|
148
158
|
// Start with the offset from props (controlled by useScroll)
|
|
149
|
-
let scrollY = this.
|
|
159
|
+
let scrollY = this.offsetY
|
|
150
160
|
let scrollX = this.offsetX
|
|
151
161
|
|
|
152
162
|
// Sticky scroll logic (vertical):
|
|
@@ -154,7 +164,7 @@ export class ScrollHost extends SingleChildHost {
|
|
|
154
164
|
// - If at end (or was at end and content grew), stay stuck
|
|
155
165
|
if (this.sticky && (this.axis === "vertical" || this.axis === "both")) {
|
|
156
166
|
// Detect if user scrolled away (offset prop is less than where we rendered)
|
|
157
|
-
const userScrolledAway = this.
|
|
167
|
+
const userScrolledAway = this.offsetY < this.effectiveOffset - 1
|
|
158
168
|
|
|
159
169
|
if (userScrolledAway) {
|
|
160
170
|
// User scrolled up - unstick
|
|
@@ -185,25 +195,23 @@ export class ScrollHost extends SingleChildHost {
|
|
|
185
195
|
scrollX = Math.max(0, Math.min(maxScrollX, scrollX))
|
|
186
196
|
|
|
187
197
|
// Store effective offsets for rendering (scrollbar position)
|
|
188
|
-
// Report back if clamped or changed (keep controller in sync)
|
|
189
|
-
if (scrollY !== this.effectiveOffset || scrollY !== this.offset) {
|
|
190
|
-
this.onEffectiveOffset?.(scrollY)
|
|
191
|
-
}
|
|
192
|
-
if (scrollX !== this.effectiveOffsetX || scrollX !== this.offsetX) {
|
|
193
|
-
this.onEffectiveOffsetX?.(scrollX)
|
|
194
|
-
}
|
|
195
198
|
this.effectiveOffset = scrollY
|
|
196
199
|
this.effectiveOffsetX = scrollX
|
|
200
|
+
const offsetChanged = this.effectiveOffset !== this.lastOffsetY || this.effectiveOffsetX !== this.lastOffsetX
|
|
197
201
|
|
|
198
202
|
// Handle alignment when content is smaller than viewport
|
|
199
|
-
if (this.
|
|
200
|
-
if (
|
|
201
|
-
// Align to bottom
|
|
203
|
+
if (this.contentHeight < layoutRect.h && (this.axis === "vertical" || this.axis === "both")) {
|
|
204
|
+
if (alignY === "bottom") {
|
|
202
205
|
scrollY = -(layoutRect.h - this.contentHeight)
|
|
206
|
+
} else if (alignY === "center") {
|
|
207
|
+
scrollY = -Math.floor((layoutRect.h - this.contentHeight) / 2)
|
|
203
208
|
}
|
|
204
|
-
|
|
205
|
-
|
|
209
|
+
}
|
|
210
|
+
if (this.contentWidth < layoutRect.w && (this.axis === "horizontal" || this.axis === "both")) {
|
|
211
|
+
if (alignX === "right") {
|
|
206
212
|
scrollX = -(layoutRect.w - this.contentWidth)
|
|
213
|
+
} else if (alignX === "center") {
|
|
214
|
+
scrollX = -Math.floor((layoutRect.w - this.contentWidth) / 2)
|
|
207
215
|
}
|
|
208
216
|
}
|
|
209
217
|
|
|
@@ -215,6 +223,27 @@ export class ScrollHost extends SingleChildHost {
|
|
|
215
223
|
h: this.contentHeight,
|
|
216
224
|
}
|
|
217
225
|
child.layout(childRect)
|
|
226
|
+
|
|
227
|
+
if (this.onScrollLayoutChange && (contentChanged || viewportChanged || rectChanged || offsetChanged)) {
|
|
228
|
+
this.lastReportedContentW = this.contentWidth
|
|
229
|
+
this.lastReportedContentH = this.contentHeight
|
|
230
|
+
this.lastViewportW = layoutRect.w
|
|
231
|
+
this.lastViewportH = layoutRect.h
|
|
232
|
+
this.lastRectX = layoutRect.x
|
|
233
|
+
this.lastRectY = layoutRect.y
|
|
234
|
+
this.lastRectW = layoutRect.w
|
|
235
|
+
this.lastRectH = layoutRect.h
|
|
236
|
+
this.lastOffsetY = this.effectiveOffset
|
|
237
|
+
this.lastOffsetX = this.effectiveOffsetX
|
|
238
|
+
|
|
239
|
+
this.onScrollLayoutChange({
|
|
240
|
+
content: { width: this.contentWidth, height: this.contentHeight },
|
|
241
|
+
viewport: { width: layoutRect.w, height: layoutRect.h },
|
|
242
|
+
offset: { x: this.effectiveOffsetX, y: this.effectiveOffset },
|
|
243
|
+
rect: { x: layoutRect.x, y: layoutRect.y, w: layoutRect.w, h: layoutRect.h },
|
|
244
|
+
axis: this.axis,
|
|
245
|
+
})
|
|
246
|
+
}
|
|
218
247
|
}
|
|
219
248
|
|
|
220
249
|
render(buffer: CellBuffer, palette: Palette): void {
|
|
@@ -286,16 +315,12 @@ export class ScrollHost extends SingleChildHost {
|
|
|
286
315
|
// Scroll is greedy by default unless explicitly set to false
|
|
287
316
|
this.applyGreedyDefault(props, 1)
|
|
288
317
|
if (props.axis !== undefined) this.axis = (props.axis as ScrollProps["axis"]) ?? "vertical"
|
|
289
|
-
if (props.
|
|
318
|
+
if (props.offsetY !== undefined) this.offsetY = props.offsetY as number
|
|
290
319
|
if (props.offsetX !== undefined) this.offsetX = props.offsetX as number
|
|
291
|
-
if (props.align !== undefined) this.align =
|
|
320
|
+
if (props.align !== undefined) this.align = props.align as ScrollProps["align"]
|
|
292
321
|
this.bg = props.bg as Color | undefined
|
|
293
322
|
if (props.showScrollbar !== undefined) this.showScrollbar = props.showScrollbar as boolean
|
|
294
323
|
if (props.sticky !== undefined) this.sticky = props.sticky as boolean
|
|
295
|
-
this.
|
|
296
|
-
this.onViewportSize = props.onViewportSize as ScrollProps["onViewportSize"]
|
|
297
|
-
this.onEffectiveOffset = props.onEffectiveOffset as ScrollProps["onEffectiveOffset"]
|
|
298
|
-
this.onEffectiveOffsetX = props.onEffectiveOffsetX as ScrollProps["onEffectiveOffsetX"]
|
|
299
|
-
this.onRect = props.onRect as ScrollProps["onRect"]
|
|
324
|
+
this.onScrollLayoutChange = props.onScrollLayoutChange as ScrollProps["onScrollLayoutChange"]
|
|
300
325
|
}
|
|
301
326
|
}
|
package/src/hosts/spacer.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { CellBuffer, Palette } from "@effect-tui/core"
|
|
2
2
|
import type { CommonProps, HostContext, Size } from "../reconciler/types.js"
|
|
3
|
-
import {
|
|
3
|
+
import { LeafHost } from "./leaf.js"
|
|
4
4
|
|
|
5
5
|
export interface SpacerProps extends CommonProps {
|
|
6
6
|
/** Minimum width (default 0) */
|
|
@@ -9,7 +9,7 @@ export interface SpacerProps extends CommonProps {
|
|
|
9
9
|
minHeight?: number
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export class SpacerHost extends
|
|
12
|
+
export class SpacerHost extends LeafHost {
|
|
13
13
|
minWidth = 0
|
|
14
14
|
minHeight = 0
|
|
15
15
|
|
package/src/hosts/text.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { type CellBuffer, type Color, displayWidth, type Palette } from "@effect-tui/core"
|
|
2
2
|
import type { ColorMotionValue } from "../motion/color-motion-value.js"
|
|
3
3
|
import type { CommonProps, HostContext, HostInstance, Rect, Size } from "../reconciler/types.js"
|
|
4
|
-
import {
|
|
4
|
+
import { resolveInheritedBgStyle, styleIdFromProps, wrapSpans, wrapText } from "../utils/index.js"
|
|
5
5
|
import { BaseHost, getInheritedBg } from "./base.js"
|
|
6
|
+
import { LeafHost } from "./leaf.js"
|
|
6
7
|
|
|
7
8
|
/** Color prop that can be a static Color or a spring-animated ColorMotionValue */
|
|
8
9
|
export type ColorProp = Color | ColorMotionValue
|
|
@@ -205,7 +206,7 @@ export class TextHost extends BaseHost {
|
|
|
205
206
|
if (this.wrap) {
|
|
206
207
|
// Wrap mode: may span multiple lines. Cache result for render()
|
|
207
208
|
this.cachedLines = rawLines.flatMap((line, idx) =>
|
|
208
|
-
idx < rawLines.length - 1 ? [...
|
|
209
|
+
idx < rawLines.length - 1 ? [...wrapText(line, constrained.w), ""] : wrapText(line, constrained.w),
|
|
209
210
|
)
|
|
210
211
|
this.cachedWidth = constrained.w
|
|
211
212
|
const w = this.cachedLines.reduce((max, line) => Math.max(max, displayWidth(line)), 0)
|
|
@@ -220,60 +221,6 @@ export class TextHost extends BaseHost {
|
|
|
220
221
|
return this.constrainResult({ w, h })
|
|
221
222
|
}
|
|
222
223
|
|
|
223
|
-
/** Wrap text to fit within maxWidth, preferring word boundaries */
|
|
224
|
-
private wrapText(text: string, maxWidth: number): string[] {
|
|
225
|
-
const result: string[] = []
|
|
226
|
-
for (const rawLine of text.split("\n")) {
|
|
227
|
-
if (rawLine === "") {
|
|
228
|
-
result.push("")
|
|
229
|
-
continue
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// Split into words (keeping whitespace as separate tokens)
|
|
233
|
-
const tokens = splitWords(rawLine)
|
|
234
|
-
let line = ""
|
|
235
|
-
let lineW = 0
|
|
236
|
-
|
|
237
|
-
for (const token of tokens) {
|
|
238
|
-
const tokenW = displayWidth(token)
|
|
239
|
-
const isWs = isWhitespace(token)
|
|
240
|
-
|
|
241
|
-
if (lineW + tokenW <= maxWidth) {
|
|
242
|
-
// Token fits on current line
|
|
243
|
-
line += token
|
|
244
|
-
lineW += tokenW
|
|
245
|
-
} else if (isWs) {
|
|
246
|
-
// Whitespace doesn't fit - just skip it (don't start new line with space)
|
|
247
|
-
continue
|
|
248
|
-
} else if (tokenW <= maxWidth) {
|
|
249
|
-
// Word doesn't fit but is smaller than maxWidth - start new line
|
|
250
|
-
if (line.trimEnd()) result.push(line.trimEnd())
|
|
251
|
-
line = token
|
|
252
|
-
lineW = tokenW
|
|
253
|
-
} else {
|
|
254
|
-
// Word is longer than maxWidth - break it character by character
|
|
255
|
-
if (line.trimEnd()) result.push(line.trimEnd())
|
|
256
|
-
line = ""
|
|
257
|
-
lineW = 0
|
|
258
|
-
for (const ch of token) {
|
|
259
|
-
const chW = displayWidth(ch)
|
|
260
|
-
if (lineW + chW > maxWidth && line.length > 0) {
|
|
261
|
-
result.push(line)
|
|
262
|
-
line = ch
|
|
263
|
-
lineW = chW
|
|
264
|
-
} else {
|
|
265
|
-
line += ch
|
|
266
|
-
lineW += chW
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
// Preserve trailing space for inline flow - only trimEnd when breaking mid-line (done above)
|
|
272
|
-
if (line) result.push(line)
|
|
273
|
-
}
|
|
274
|
-
return result.length > 0 ? result : [""]
|
|
275
|
-
}
|
|
276
|
-
|
|
277
224
|
override layout(rect: Rect): void {
|
|
278
225
|
const layoutRect = this.layoutWithConstraints(rect)
|
|
279
226
|
// Layout children (RawTextHost nodes) at same position
|
|
@@ -349,7 +296,7 @@ export class TextHost extends BaseHost {
|
|
|
349
296
|
this.cachedLines && this.cachedWidth === rectW
|
|
350
297
|
? this.cachedLines
|
|
351
298
|
: rawLines.flatMap((line, idx) =>
|
|
352
|
-
idx < rawLines.length - 1 ? [...
|
|
299
|
+
idx < rawLines.length - 1 ? [...wrapText(line, rectW), ""] : wrapText(line, rectW),
|
|
353
300
|
)
|
|
354
301
|
const visibleLines = Math.min(lines.length, this.rect.h)
|
|
355
302
|
// Explicitly paint background under text lines to clear stale styles (e.g., selection highlights).
|
|
@@ -399,7 +346,7 @@ export class TextHost extends BaseHost {
|
|
|
399
346
|
}
|
|
400
347
|
|
|
401
348
|
/** Special host for raw text nodes (React text children) */
|
|
402
|
-
export class RawTextHost extends
|
|
349
|
+
export class RawTextHost extends LeafHost {
|
|
403
350
|
content = ""
|
|
404
351
|
|
|
405
352
|
constructor(text: string, ctx: HostContext) {
|
package/src/hosts/vstack.ts
CHANGED
|
@@ -8,6 +8,6 @@ export interface VStackProps extends FlexContainerProps<"vertical"> {}
|
|
|
8
8
|
*/
|
|
9
9
|
export class VStackHost extends FlexContainerHost<"vertical"> {
|
|
10
10
|
constructor(props: VStackProps, ctx: HostContext) {
|
|
11
|
-
super("vertical", "vstack", props as FlexContainerProps<"vertical">, ctx, "
|
|
11
|
+
super("vertical", "vstack", props as FlexContainerProps<"vertical">, ctx, "left")
|
|
12
12
|
}
|
|
13
13
|
}
|
package/src/hosts/zstack.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { CellBuffer, Palette } from "@effect-tui/core"
|
|
2
2
|
import type { CommonProps, HostContext, Rect, Size } from "../reconciler/types.js"
|
|
3
|
-
import {
|
|
3
|
+
import { type HAlign, type VAlign } from "../utils/index.js"
|
|
4
4
|
import { BaseHost } from "./base.js"
|
|
5
|
+
import { layoutAlignedChildren } from "./layout-helpers.js"
|
|
5
6
|
|
|
6
7
|
export interface ZStackProps extends CommonProps {
|
|
7
|
-
alignment?: { h?: "
|
|
8
|
+
alignment?: { h?: "left" | "center" | "right"; v?: "top" | "center" | "bottom" }
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
// Overlay children in the same rect, honoring alignment for each child.
|
|
@@ -45,11 +46,10 @@ export class ZStackHost extends BaseHost {
|
|
|
45
46
|
override layout(rect: Rect): void {
|
|
46
47
|
const layoutRect = this.layoutWithConstraints(rect)
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
49
|
+
layoutAlignedChildren(layoutRect, this.children, this.cachedSizes, () => ({
|
|
50
|
+
h: this.alignmentH,
|
|
51
|
+
v: this.alignmentV,
|
|
52
|
+
}))
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
render(buffer: CellBuffer, palette: Palette): void {
|
package/src/index.ts
CHANGED
|
@@ -67,7 +67,7 @@ export { useFrameStats } from "./hooks/useFrameStats.js"
|
|
|
67
67
|
export type { BorderKind, BoxProps } from "./hosts/box.js"
|
|
68
68
|
export type { CanvasProps, DrawContext } from "./hosts/canvas.js"
|
|
69
69
|
export type { HStackProps } from "./hosts/hstack.js"
|
|
70
|
-
export type { ScrollProps } from "./hosts/scroll.js"
|
|
70
|
+
export type { ScrollAlign, ScrollAlignX, ScrollAlignY, ScrollAxis, ScrollLayoutChange, ScrollProps } from "./hosts/scroll.js"
|
|
71
71
|
export type { SpacerProps } from "./hosts/spacer.js"
|
|
72
72
|
export type { SpanProps, SpanStyle, TextProps } from "./hosts/text.js"
|
|
73
73
|
export type { VStackProps } from "./hosts/vstack.js"
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs"
|
|
2
|
+
import { globalValue } from "effect/GlobalValue"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Pipeable combinator that makes an atom persist across hot reloads.
|
|
6
|
+
*
|
|
7
|
+
* This is the cleanest way to define HMR-persistent atoms - just pipe it!
|
|
8
|
+
* Combines globalValue (for persistence) + keepAlive (prevents registry cleanup).
|
|
9
|
+
*/
|
|
10
|
+
export function hmr(key: string): <A,>(self: A) => A {
|
|
11
|
+
return <A,>(self: A): A => {
|
|
12
|
+
return globalValue(Symbol.for(`hmr/${key}`), () => {
|
|
13
|
+
// Apply keepAlive if it's an atom (just sets keepAlive: true)
|
|
14
|
+
if (typeof self === "object" && self !== null && "keepAlive" in self) {
|
|
15
|
+
return Object.assign(Object.create(Object.getPrototypeOf(self)), {
|
|
16
|
+
...self,
|
|
17
|
+
keepAlive: true,
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
return self
|
|
21
|
+
}) as A
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Wrap any value creation in HMR persistence.
|
|
27
|
+
*/
|
|
28
|
+
export function hmrState<T>(key: string, create: () => T): T {
|
|
29
|
+
return globalValue(Symbol.for(`hmr/${key}`), create)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Cache for runtime key extraction
|
|
33
|
+
const keyCache = new Map<string, string>()
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Parse stack trace to get file path and line number.
|
|
37
|
+
* Returns null if parsing fails.
|
|
38
|
+
*/
|
|
39
|
+
function parseStack(stack: string): { file: string; line: number; col: number } | null {
|
|
40
|
+
// Bun stack format: " at functionName (file:line:col)" or " at file:line:col"
|
|
41
|
+
const lines = stack.split("\n")
|
|
42
|
+
// Skip first line (Error message) and find caller (skip autoHmr itself)
|
|
43
|
+
for (let i = 2; i < lines.length; i++) {
|
|
44
|
+
const match = lines[i].match(/\((.+?):(\d+):(\d+)\)/) || lines[i].match(/at\s+(.+?):(\d+):(\d+)/)
|
|
45
|
+
if (match) {
|
|
46
|
+
return { file: match[1], line: parseInt(match[2], 10), col: parseInt(match[3], 10) }
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Extract variable name from source line using regex.
|
|
54
|
+
* Returns null if extraction fails.
|
|
55
|
+
*/
|
|
56
|
+
function extractVarName(source: string, line: number): string | null {
|
|
57
|
+
const lines = source.split("\n")
|
|
58
|
+
if (line < 1 || line > lines.length) return null
|
|
59
|
+
|
|
60
|
+
const sourceLine = lines[line - 1]
|
|
61
|
+
const match = sourceLine.match(/(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=/)
|
|
62
|
+
return match?.[1] ?? null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Auto-keyed HMR persistence for atoms.
|
|
67
|
+
*
|
|
68
|
+
* When used with the HMR plugin (recommended), keys are injected at load time
|
|
69
|
+
* based on the variable name. Without the plugin, falls back to runtime
|
|
70
|
+
* stack trace parsing.
|
|
71
|
+
*/
|
|
72
|
+
export function autoHmr<A>(self: A): A {
|
|
73
|
+
// Get stack trace for caller location
|
|
74
|
+
const stack = new Error().stack ?? ""
|
|
75
|
+
const location = parseStack(stack)
|
|
76
|
+
|
|
77
|
+
if (!location) {
|
|
78
|
+
// Fallback: use a hash of the stack trace
|
|
79
|
+
const fallbackKey = `unknown:${stack.slice(0, 100)}`
|
|
80
|
+
return hmr(fallbackKey)(self)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const cacheKey = `${location.file}:${location.line}:${location.col}`
|
|
84
|
+
|
|
85
|
+
// Check cache first
|
|
86
|
+
let key = keyCache.get(cacheKey)
|
|
87
|
+
if (!key) {
|
|
88
|
+
try {
|
|
89
|
+
// Read source file and extract variable name
|
|
90
|
+
const source = readFileSync(location.file, "utf-8")
|
|
91
|
+
const varName = extractVarName(source, location.line)
|
|
92
|
+
key = `${location.file}:${varName ?? `line${location.line}`}`
|
|
93
|
+
} catch {
|
|
94
|
+
// File read failed, use line-based key
|
|
95
|
+
key = `${location.file}:line${location.line}`
|
|
96
|
+
}
|
|
97
|
+
keyCache.set(cacheKey, key)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return hmr(key)(self)
|
|
101
|
+
}
|