@effect-tui/react 0.14.3 → 0.15.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/dist/src/hosts/box.d.ts +6 -0
- package/dist/src/hosts/box.d.ts.map +1 -1
- package/dist/src/hosts/box.js +21 -4
- package/dist/src/hosts/box.js.map +1 -1
- package/dist/src/reconciler/host-config.d.ts.map +1 -1
- package/dist/src/reconciler/host-config.js +7 -0
- package/dist/src/reconciler/host-config.js.map +1 -1
- package/dist/src/renderer.d.ts.map +1 -1
- package/dist/src/renderer.js +45 -33
- package/dist/src/renderer.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/hosts/box.ts +26 -3
- package/src/reconciler/host-config.ts +7 -0
- package/src/renderer.ts +55 -36
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effect-tui/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
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.
|
|
86
|
+
"@effect-tui/core": "^0.15.0",
|
|
87
87
|
"@effect/platform": "^0.94.1",
|
|
88
88
|
"@effect/platform-bun": "^0.87.0",
|
|
89
89
|
"@effect/rpc": "^0.73.0",
|
package/src/hosts/box.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type Padding,
|
|
11
11
|
type PaddingInput,
|
|
12
12
|
resolvePadding,
|
|
13
|
+
tableBorderChars,
|
|
13
14
|
toColorValue,
|
|
14
15
|
} from "../utils/index.js"
|
|
15
16
|
import { SingleChildHost } from "./single-child.js"
|
|
@@ -28,6 +29,10 @@ export interface BoxProps extends CommonProps {
|
|
|
28
29
|
title?: string
|
|
29
30
|
/** Title text color (defaults to borderColor) */
|
|
30
31
|
titleColor?: ColorProp
|
|
32
|
+
/** Make title bold */
|
|
33
|
+
titleBold?: boolean
|
|
34
|
+
/** Show a divider line below the title (├──────┤) */
|
|
35
|
+
titleDivider?: boolean
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
export class BoxHost extends SingleChildHost {
|
|
@@ -37,6 +42,8 @@ export class BoxHost extends SingleChildHost {
|
|
|
37
42
|
bg?: Color
|
|
38
43
|
title?: string
|
|
39
44
|
titleColor?: Color
|
|
45
|
+
titleBold = false
|
|
46
|
+
titleDivider = false
|
|
40
47
|
|
|
41
48
|
constructor(props: BoxProps, ctx: HostContext) {
|
|
42
49
|
super("box", props, ctx)
|
|
@@ -52,7 +59,8 @@ export class BoxHost extends SingleChildHost {
|
|
|
52
59
|
}
|
|
53
60
|
|
|
54
61
|
private get insetY(): number {
|
|
55
|
-
|
|
62
|
+
const dividerHeight = this.titleDivider && this.title && this.border !== "none" ? 1 : 0
|
|
63
|
+
return this.borderThickness + dividerHeight + this.padding.top + this.padding.bottom + this.borderThickness
|
|
56
64
|
}
|
|
57
65
|
|
|
58
66
|
measure(maxW: number, maxH: number): Size {
|
|
@@ -85,9 +93,10 @@ export class BoxHost extends SingleChildHost {
|
|
|
85
93
|
const layoutRect = this.layoutWithConstraints(rect)
|
|
86
94
|
|
|
87
95
|
const t = this.borderThickness
|
|
96
|
+
const dividerHeight = this.titleDivider && this.title && this.border !== "none" ? 1 : 0
|
|
88
97
|
const innerRect: Rect = {
|
|
89
98
|
x: layoutRect.x + t + this.padding.left,
|
|
90
|
-
y: layoutRect.y + t + this.padding.top,
|
|
99
|
+
y: layoutRect.y + t + dividerHeight + this.padding.top,
|
|
91
100
|
w: Math.max(0, layoutRect.w - this.insetX),
|
|
92
101
|
h: Math.max(0, layoutRect.h - this.insetY),
|
|
93
102
|
}
|
|
@@ -117,7 +126,7 @@ export class BoxHost extends SingleChildHost {
|
|
|
117
126
|
// Draw title on top border if present
|
|
118
127
|
if (this.title && w >= 7) {
|
|
119
128
|
const titleFg = toColorValue(this.titleColor) ?? borderFg
|
|
120
|
-
const titleStyle = palette.id({ fg: titleFg })
|
|
129
|
+
const titleStyle = palette.id({ fg: titleFg, bold: this.titleBold })
|
|
121
130
|
// Reserve: ┌─ (2) + space before (1) + space after (1) + ─┐ (2) = 6 chars
|
|
122
131
|
const maxTitleLen = w - 6
|
|
123
132
|
const displayTitle =
|
|
@@ -126,6 +135,18 @@ export class BoxHost extends SingleChildHost {
|
|
|
126
135
|
const titleX = x + 2
|
|
127
136
|
const titleText = ` ${displayTitle} `
|
|
128
137
|
buffer.drawText(titleX, y, titleText, titleStyle, titleText.length)
|
|
138
|
+
|
|
139
|
+
// Draw divider line below title if enabled
|
|
140
|
+
if (this.titleDivider && h >= 3) {
|
|
141
|
+
const dividerY = y + 1
|
|
142
|
+
const tableChars = tableBorderChars(this.border)
|
|
143
|
+
// Draw ├ at left, ─ across, ┤ at right
|
|
144
|
+
buffer.drawText(x, dividerY, tableChars.lt, borderStyle, 1)
|
|
145
|
+
for (let dx = 1; dx < w - 1; dx++) {
|
|
146
|
+
buffer.drawText(x + dx, dividerY, tableChars.h, borderStyle, 1)
|
|
147
|
+
}
|
|
148
|
+
buffer.drawText(x + w - 1, dividerY, tableChars.rt, borderStyle, 1)
|
|
149
|
+
}
|
|
129
150
|
}
|
|
130
151
|
}
|
|
131
152
|
|
|
@@ -151,5 +172,7 @@ export class BoxHost extends SingleChildHost {
|
|
|
151
172
|
this.titleColor = this.resolveSpringProp("titleColor", props.titleColor, (v) => {
|
|
152
173
|
this.titleColor = v as Color
|
|
153
174
|
}) as Color | undefined
|
|
175
|
+
this.titleBold = Boolean(props.titleBold)
|
|
176
|
+
this.titleDivider = Boolean(props.titleDivider)
|
|
154
177
|
}
|
|
155
178
|
}
|
|
@@ -57,6 +57,9 @@ const hostConfig = {
|
|
|
57
57
|
supportsHydration: false,
|
|
58
58
|
|
|
59
59
|
createInstance(type: Type, props: Props, rootContainer: Container) {
|
|
60
|
+
if (type === "vstack") {
|
|
61
|
+
console.log("createInstance vstack props", props)
|
|
62
|
+
}
|
|
60
63
|
const instance = createHostInstance(type, props, rootContainer.ctx)
|
|
61
64
|
|
|
62
65
|
// Track static content (from <Static> component)
|
|
@@ -64,6 +67,7 @@ const hostConfig = {
|
|
|
64
67
|
instance.__static = true
|
|
65
68
|
rootContainer.staticRoot = instance
|
|
66
69
|
rootContainer.staticDirty = true
|
|
70
|
+
console.log("set staticRoot", Boolean(rootContainer.staticRoot))
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
return instance
|
|
@@ -94,6 +98,9 @@ const hostConfig = {
|
|
|
94
98
|
},
|
|
95
99
|
|
|
96
100
|
removeChildFromContainer(container: Container, child: Instance) {
|
|
101
|
+
if (child.__static) {
|
|
102
|
+
console.log("removeChildFromContainer static")
|
|
103
|
+
}
|
|
97
104
|
if (container.root) {
|
|
98
105
|
container.root.removeChild(child)
|
|
99
106
|
child.destroy()
|
package/src/renderer.ts
CHANGED
|
@@ -120,10 +120,57 @@ export function createRenderer(options?: RendererOptions): TuiRenderer {
|
|
|
120
120
|
},
|
|
121
121
|
})
|
|
122
122
|
|
|
123
|
+
const handleInlineFullRerender = () => {
|
|
124
|
+
if (mode !== "inline" || !staticRenderer) return
|
|
125
|
+
const inlineMode = renderMode as InlineRenderer
|
|
126
|
+
if (!inlineMode.needsFullRerender()) return
|
|
127
|
+
|
|
128
|
+
// Clear screen + scrollback + cursor home
|
|
129
|
+
stdout.write(ANSI.screen.clear + ANSI.screen.clearScrollback + ANSI.cursor.home)
|
|
130
|
+
// Replay all cached static content
|
|
131
|
+
const cachedStatic = staticRenderer.getCachedOutput()
|
|
132
|
+
if (cachedStatic) {
|
|
133
|
+
stdout.write(cachedStatic)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Reset state
|
|
137
|
+
inlineMode.clearFullRerenderFlag()
|
|
138
|
+
state.invalidateBuffers()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const flushInlineStatic = (container: Container | null, frameWidth: number) => {
|
|
142
|
+
if (mode !== "inline" || !container?.staticDirty || !container.staticRoot || !staticRenderer) {
|
|
143
|
+
return ""
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const inlineMode = renderMode as InlineRenderer
|
|
147
|
+
const prevHeight = inlineMode.getPreviousHeight()
|
|
148
|
+
let output = ""
|
|
149
|
+
|
|
150
|
+
// Step 1: Clear the dynamic area (move up + clear to end of screen)
|
|
151
|
+
if (prevHeight > 0) {
|
|
152
|
+
output += ANSI.cursor.up(prevHeight) + ANSI.cursor.startOfLine + ANSI.screen.clearToEnd
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Step 2: Append static content (cursor ends at bottom of static)
|
|
156
|
+
output += staticRenderer.render(container.staticRoot, frameWidth)
|
|
157
|
+
|
|
158
|
+
// Step 3: Reset previousHeight to 0 (we cleared dynamic, starting fresh)
|
|
159
|
+
inlineMode.reset()
|
|
160
|
+
inlineMode.forceFullOutputOnce() // Force full output to resync cursor tracking after static
|
|
161
|
+
state.invalidateBuffers()
|
|
162
|
+
container.staticDirty = false
|
|
163
|
+
|
|
164
|
+
return output
|
|
165
|
+
}
|
|
166
|
+
|
|
123
167
|
// The render frame logic
|
|
124
168
|
const renderFrame = () => {
|
|
125
169
|
const frameStart = Prof.startFrame()
|
|
126
170
|
const frameStartMs = performance.now()
|
|
171
|
+
// Flush any pending React updates before measuring/layout.
|
|
172
|
+
// Ensures resize-driven state (useTerminalSize) is reflected in the host tree.
|
|
173
|
+
flushSync(() => {})
|
|
127
174
|
const frameWidth = state.width
|
|
128
175
|
const frameHeight = state.height
|
|
129
176
|
let contentH = frameHeight
|
|
@@ -137,44 +184,12 @@ export function createRenderer(options?: RendererOptions): TuiRenderer {
|
|
|
137
184
|
|
|
138
185
|
try {
|
|
139
186
|
// Handle full rerender on resize (Ink-style: clear everything + replay static)
|
|
140
|
-
|
|
141
|
-
const inlineMode = renderMode as InlineRenderer
|
|
142
|
-
if (inlineMode.needsFullRerender()) {
|
|
143
|
-
// Clear screen + scrollback + cursor home
|
|
144
|
-
stdout.write(ANSI.screen.clear + ANSI.screen.clearScrollback + ANSI.cursor.home)
|
|
145
|
-
// Replay all cached static content
|
|
146
|
-
const cachedStatic = staticRenderer.getCachedOutput()
|
|
147
|
-
if (cachedStatic) {
|
|
148
|
-
stdout.write(cachedStatic)
|
|
149
|
-
}
|
|
150
|
-
// Reset state
|
|
151
|
-
inlineMode.clearFullRerenderFlag()
|
|
152
|
-
state.invalidateBuffers()
|
|
153
|
-
}
|
|
154
|
-
}
|
|
187
|
+
handleInlineFullRerender()
|
|
155
188
|
|
|
156
189
|
// Handle static content: clear dynamic area, append static, then fresh dynamic render
|
|
157
190
|
// Note: IL (insert lines) won't work here because inline mode uses relative positioning
|
|
158
191
|
// and IL would desync the screen state from our buffer tracking.
|
|
159
|
-
|
|
160
|
-
if (mode === "inline" && container?.staticDirty && container?.staticRoot && staticRenderer) {
|
|
161
|
-
const inlineMode = renderMode as InlineRenderer
|
|
162
|
-
const prevHeight = inlineMode.getPreviousHeight()
|
|
163
|
-
|
|
164
|
-
// Step 1: Clear the dynamic area (move up + clear to end of screen)
|
|
165
|
-
if (prevHeight > 0) {
|
|
166
|
-
staticOutput += ANSI.cursor.up(prevHeight) + ANSI.cursor.startOfLine + ANSI.screen.clearToEnd
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Step 2: Append static content (cursor ends at bottom of static)
|
|
170
|
-
staticOutput += staticRenderer.render(container.staticRoot, frameWidth)
|
|
171
|
-
|
|
172
|
-
// Step 3: Reset previousHeight to 0 (we cleared dynamic, starting fresh)
|
|
173
|
-
inlineMode.reset()
|
|
174
|
-
inlineMode.forceFullOutputOnce() // Force full output to resync cursor tracking after static
|
|
175
|
-
state.invalidateBuffers()
|
|
176
|
-
container.staticDirty = false
|
|
177
|
-
}
|
|
192
|
+
const staticOutput = flushInlineStatic(container ?? null, frameWidth)
|
|
178
193
|
|
|
179
194
|
// For inline mode, measure content unconstrained to handle overflow
|
|
180
195
|
let actualContentHeight = frameHeight
|
|
@@ -321,7 +336,9 @@ export function createRenderer(options?: RendererOptions): TuiRenderer {
|
|
|
321
336
|
state.updateDimensions(width, height)
|
|
322
337
|
state.invalidateBuffers()
|
|
323
338
|
state.markDirty()
|
|
324
|
-
|
|
339
|
+
flushSync(() => {
|
|
340
|
+
events.dispatchResize(width, height)
|
|
341
|
+
})
|
|
325
342
|
if (!manualMode) renderFrame()
|
|
326
343
|
},
|
|
327
344
|
}
|
|
@@ -344,7 +361,9 @@ export function createRenderer(options?: RendererOptions): TuiRenderer {
|
|
|
344
361
|
state.invalidateBuffers()
|
|
345
362
|
state.markDirty()
|
|
346
363
|
|
|
347
|
-
|
|
364
|
+
flushSync(() => {
|
|
365
|
+
events.dispatchResize(newWidth, newHeight)
|
|
366
|
+
})
|
|
348
367
|
}
|
|
349
368
|
stdout.on("resize", state.resizeHandler)
|
|
350
369
|
|