@effect-tui/react 0.13.0 → 0.14.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 +11 -7
- package/dist/jsx-runtime.d.ts +1 -2
- package/dist/jsx-runtime.d.ts.map +1 -1
- package/dist/src/components/Markdown.js +7 -7
- package/dist/src/components/Markdown.js.map +1 -1
- package/dist/src/components/MultilineTextInput.d.ts.map +1 -1
- package/dist/src/components/MultilineTextInput.js +11 -0
- package/dist/src/components/MultilineTextInput.js.map +1 -1
- package/dist/src/components/TextInput.d.ts.map +1 -1
- package/dist/src/components/TextInput.js +15 -0
- package/dist/src/components/TextInput.js.map +1 -1
- package/dist/src/hosts/base.d.ts +16 -0
- package/dist/src/hosts/base.d.ts.map +1 -1
- package/dist/src/hosts/base.js +30 -0
- package/dist/src/hosts/base.js.map +1 -1
- package/dist/src/hosts/box.d.ts.map +1 -1
- package/dist/src/hosts/box.js +7 -8
- package/dist/src/hosts/box.js.map +1 -1
- package/dist/src/hosts/canvas.d.ts.map +1 -1
- package/dist/src/hosts/canvas.js +5 -3
- package/dist/src/hosts/canvas.js.map +1 -1
- package/dist/src/hosts/codeblock.d.ts.map +1 -1
- package/dist/src/hosts/codeblock.js +5 -4
- package/dist/src/hosts/codeblock.js.map +1 -1
- package/dist/src/hosts/flex-container.d.ts.map +1 -1
- package/dist/src/hosts/flex-container.js +5 -8
- package/dist/src/hosts/flex-container.js.map +1 -1
- package/dist/src/hosts/index.d.ts +1 -1
- package/dist/src/hosts/index.d.ts.map +1 -1
- package/dist/src/hosts/index.js +2 -3
- package/dist/src/hosts/index.js.map +1 -1
- package/dist/src/hosts/overlay-item.js +2 -2
- package/dist/src/hosts/overlay-item.js.map +1 -1
- package/dist/src/hosts/overlay.d.ts.map +1 -1
- package/dist/src/hosts/overlay.js +6 -11
- package/dist/src/hosts/overlay.js.map +1 -1
- package/dist/src/hosts/scroll.d.ts +4 -0
- package/dist/src/hosts/scroll.d.ts.map +1 -1
- package/dist/src/hosts/scroll.js +32 -24
- package/dist/src/hosts/scroll.js.map +1 -1
- package/dist/src/hosts/spacer.d.ts.map +1 -1
- package/dist/src/hosts/spacer.js +1 -3
- package/dist/src/hosts/spacer.js.map +1 -1
- package/dist/src/hosts/text.d.ts +24 -45
- package/dist/src/hosts/text.d.ts.map +1 -1
- package/dist/src/hosts/text.js +69 -215
- package/dist/src/hosts/text.js.map +1 -1
- package/dist/src/hosts/zstack.d.ts.map +1 -1
- package/dist/src/hosts/zstack.js +4 -10
- package/dist/src/hosts/zstack.js.map +1 -1
- package/dist/src/reconciler/types.d.ts +2 -0
- package/dist/src/reconciler/types.d.ts.map +1 -1
- package/dist/src/renderer/core/FrameBuilder.d.ts.map +1 -1
- package/dist/src/renderer/core/FrameBuilder.js +2 -0
- package/dist/src/renderer/core/FrameBuilder.js.map +1 -1
- package/dist/src/renderer/input/InputProcessor.d.ts.map +1 -1
- package/dist/src/renderer/input/InputProcessor.js +5 -2
- package/dist/src/renderer/input/InputProcessor.js.map +1 -1
- package/dist/src/renderer/lifecycle/RenderCache.d.ts +3 -0
- package/dist/src/renderer/lifecycle/RenderCache.d.ts.map +1 -0
- package/dist/src/renderer/lifecycle/RenderCache.js +9 -0
- package/dist/src/renderer/lifecycle/RenderCache.js.map +1 -0
- package/dist/src/renderer/lifecycle/index.d.ts +1 -0
- package/dist/src/renderer/lifecycle/index.d.ts.map +1 -1
- package/dist/src/renderer/lifecycle/index.js +1 -0
- package/dist/src/renderer/lifecycle/index.js.map +1 -1
- package/dist/src/renderer-types.d.ts +1 -1
- package/dist/src/renderer-types.d.ts.map +1 -1
- package/dist/src/renderer.d.ts.map +1 -1
- package/dist/src/renderer.js +4 -14
- package/dist/src/renderer.js.map +1 -1
- package/dist/src/test/render-tui.d.ts.map +1 -1
- package/dist/src/test/render-tui.js +1 -0
- package/dist/src/test/render-tui.js.map +1 -1
- package/dist/src/utils/alignment.d.ts +4 -0
- package/dist/src/utils/alignment.d.ts.map +1 -1
- package/dist/src/utils/alignment.js +12 -0
- package/dist/src/utils/alignment.js.map +1 -1
- package/dist/src/utils/index.d.ts +3 -2
- package/dist/src/utils/index.d.ts.map +1 -1
- package/dist/src/utils/index.js +3 -2
- package/dist/src/utils/index.js.map +1 -1
- package/dist/src/utils/styles.d.ts +6 -1
- package/dist/src/utils/styles.d.ts.map +1 -1
- package/dist/src/utils/styles.js +9 -0
- package/dist/src/utils/styles.js.map +1 -1
- package/dist/src/utils/text-wrap.d.ts +10 -0
- package/dist/src/utils/text-wrap.d.ts.map +1 -0
- package/dist/src/utils/text-wrap.js +64 -0
- package/dist/src/utils/text-wrap.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/jsx-runtime.ts +1 -2
- package/package.json +2 -2
- package/src/components/Markdown.tsx +7 -7
- package/src/components/MultilineTextInput.tsx +14 -0
- package/src/components/TextInput.tsx +18 -0
- package/src/hosts/base.ts +35 -0
- package/src/hosts/box.ts +7 -8
- package/src/hosts/canvas.ts +5 -3
- package/src/hosts/codeblock.ts +5 -4
- package/src/hosts/flex-container.ts +5 -7
- package/src/hosts/index.ts +1 -4
- package/src/hosts/overlay-item.ts +2 -2
- package/src/hosts/overlay.ts +6 -12
- package/src/hosts/scroll.ts +34 -24
- package/src/hosts/spacer.ts +1 -3
- package/src/hosts/text.ts +89 -256
- package/src/hosts/zstack.ts +4 -11
- package/src/reconciler/types.ts +3 -0
- package/src/renderer/core/FrameBuilder.ts +3 -0
- package/src/renderer/input/InputProcessor.ts +5 -2
- package/src/renderer/lifecycle/RenderCache.ts +13 -0
- package/src/renderer/lifecycle/index.ts +1 -0
- package/src/renderer-types.ts +1 -1
- package/src/renderer.ts +6 -22
- package/src/test/render-tui.ts +1 -0
- package/src/utils/alignment.ts +18 -0
- package/src/utils/index.ts +3 -1
- package/src/utils/styles.ts +18 -1
- package/src/utils/text-wrap.ts +66 -0
package/jsx-runtime.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type { OverlayProps } from "./src/hosts/overlay.js"
|
|
|
11
11
|
import type { OverlayItemProps } from "./src/hosts/overlay-item.js"
|
|
12
12
|
import type { ScrollProps } from "./src/hosts/scroll.js"
|
|
13
13
|
import type { SpacerProps } from "./src/hosts/spacer.js"
|
|
14
|
-
import type { SpanProps,
|
|
14
|
+
import type { SpanProps, TextProps } from "./src/hosts/text.js"
|
|
15
15
|
import type { VStackProps } from "./src/hosts/vstack.js"
|
|
16
16
|
import type { ZStackProps } from "./src/hosts/zstack.js"
|
|
17
17
|
|
|
@@ -40,7 +40,6 @@ export declare namespace JSX {
|
|
|
40
40
|
export interface IntrinsicElements {
|
|
41
41
|
text: TextProps & { children?: React.ReactNode }
|
|
42
42
|
span: SpanProps & { children?: React.ReactNode }
|
|
43
|
-
styledtext: StyledTextProps
|
|
44
43
|
spacer: SpacerProps
|
|
45
44
|
vstack: VStackProps & { children?: React.ReactNode; __static?: boolean }
|
|
46
45
|
hstack: HStackProps & { children?: React.ReactNode }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effect-tui/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.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.14.0",
|
|
87
87
|
"@effect/platform": "^0.94.0",
|
|
88
88
|
"@effect/platform-bun": "^0.87.0",
|
|
89
89
|
"@effect/rpc": "^0.73.0",
|
|
@@ -229,7 +229,7 @@ function parseMarkdown(content: string): MdElement[] {
|
|
|
229
229
|
}
|
|
230
230
|
|
|
231
231
|
/**
|
|
232
|
-
* Convert markdown spans to styled spans for
|
|
232
|
+
* Convert markdown spans to styled spans for TextHost spans prop.
|
|
233
233
|
*/
|
|
234
234
|
function toStyledSpans(spans: MdSpan[], theme: Required<MarkdownTheme>): StyledSpan[] {
|
|
235
235
|
const result: StyledSpan[] = []
|
|
@@ -258,7 +258,7 @@ function toStyledSpans(spans: MdSpan[], theme: Required<MarkdownTheme>): StyledS
|
|
|
258
258
|
|
|
259
259
|
/**
|
|
260
260
|
* Render inline spans as text elements (used for non-wrapping contexts like lists).
|
|
261
|
-
* For wrapping paragraphs, use <
|
|
261
|
+
* For wrapping paragraphs, use <text spans={...} wrap /> instead.
|
|
262
262
|
*/
|
|
263
263
|
function renderSpans(spans: MdSpan[], theme: Required<MarkdownTheme>) {
|
|
264
264
|
return spans.map((span, i) => {
|
|
@@ -356,9 +356,9 @@ export function Markdown({ content, theme: themeOverrides, codeTheme = "nord", w
|
|
|
356
356
|
</text>
|
|
357
357
|
)
|
|
358
358
|
case "paragraph":
|
|
359
|
-
// Use
|
|
359
|
+
// Use spans for proper wrapping of styled inline text
|
|
360
360
|
if (wrap) {
|
|
361
|
-
return <
|
|
361
|
+
return <text key={i} spans={toStyledSpans(el.spans, theme)} wrap />
|
|
362
362
|
}
|
|
363
363
|
return <hstack key={i}>{renderSpans(el.spans, theme)}</hstack>
|
|
364
364
|
case "code":
|
|
@@ -377,7 +377,7 @@ export function Markdown({ content, theme: themeOverrides, codeTheme = "nord", w
|
|
|
377
377
|
<hstack key={i}>
|
|
378
378
|
<text fg={theme.quoteBorder}>{"│ "}</text>
|
|
379
379
|
{wrap ? (
|
|
380
|
-
<
|
|
380
|
+
<text spans={toStyledSpans(el.spans, theme)} wrap />
|
|
381
381
|
) : (
|
|
382
382
|
renderSpans(el.spans, theme)
|
|
383
383
|
)}
|
|
@@ -390,7 +390,7 @@ export function Markdown({ content, theme: themeOverrides, codeTheme = "nord", w
|
|
|
390
390
|
<hstack key={j}>
|
|
391
391
|
<text fg={theme.listMarker}>{" • "}</text>
|
|
392
392
|
{wrap ? (
|
|
393
|
-
<
|
|
393
|
+
<text spans={toStyledSpans(item, theme)} wrap />
|
|
394
394
|
) : (
|
|
395
395
|
renderSpans(item, theme)
|
|
396
396
|
)}
|
|
@@ -405,7 +405,7 @@ export function Markdown({ content, theme: themeOverrides, codeTheme = "nord", w
|
|
|
405
405
|
<hstack key={j}>
|
|
406
406
|
<text fg={theme.listMarker}>{` ${el.start + j}. `}</text>
|
|
407
407
|
{wrap ? (
|
|
408
|
-
<
|
|
408
|
+
<text spans={toStyledSpans(item, theme)} wrap />
|
|
409
409
|
) : (
|
|
410
410
|
renderSpans(item, theme)
|
|
411
411
|
)}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type Color, Colors, displayWidth, graphemes } from "@effect-tui/core"
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
|
3
3
|
import { useKeyboard } from "../hooks/use-keyboard.js"
|
|
4
|
+
import { usePaste } from "../hooks/use-paste.js"
|
|
4
5
|
import type { DrawContext } from "../hosts/canvas.js"
|
|
5
6
|
import {
|
|
6
7
|
deleteCharBackwardMultiline,
|
|
@@ -654,6 +655,19 @@ export function MultilineTextInput({
|
|
|
654
655
|
|
|
655
656
|
useKeyboard(handleKey, { phase: "any" })
|
|
656
657
|
|
|
658
|
+
usePaste(
|
|
659
|
+
(paste) => {
|
|
660
|
+
if (!focused) return
|
|
661
|
+
const state: MultilineState = { lines: logicalLines, cursor, killRing }
|
|
662
|
+
const result = insertTextMultiline(state, paste.text)
|
|
663
|
+
if (result.changed) {
|
|
664
|
+
onChange(result.state.lines.join("\n"))
|
|
665
|
+
setCursor(result.state.cursor)
|
|
666
|
+
}
|
|
667
|
+
},
|
|
668
|
+
{ stopPropagation: true },
|
|
669
|
+
)
|
|
670
|
+
|
|
657
671
|
const draw = useCallback(
|
|
658
672
|
(ctx: DrawContext) => {
|
|
659
673
|
// Update content width for layout calculations
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type Color, Colors, displayWidth } from "@effect-tui/core"
|
|
2
2
|
import { useCallback, useEffect, useState } from "react"
|
|
3
3
|
import { useKeyboard } from "../hooks/use-keyboard.js"
|
|
4
|
+
import { usePaste } from "../hooks/use-paste.js"
|
|
4
5
|
import type { DrawContext } from "../hosts/canvas.js"
|
|
5
6
|
import {
|
|
6
7
|
deleteCharBackward,
|
|
@@ -241,6 +242,23 @@ export function TextInput({
|
|
|
241
242
|
|
|
242
243
|
useKeyboard(handleKey, { phase: "any" })
|
|
243
244
|
|
|
245
|
+
usePaste(
|
|
246
|
+
(paste) => {
|
|
247
|
+
if (!focused) return
|
|
248
|
+
const normalized = paste.text.replace(/[\r\n]+/g, " ")
|
|
249
|
+
const state: TextState = { text: value, cursor: cursorPos, killRing }
|
|
250
|
+
const result = insertText(state, normalized)
|
|
251
|
+
if (result.changed) {
|
|
252
|
+
onChange(result.state.text)
|
|
253
|
+
setCursorPos(result.state.cursor)
|
|
254
|
+
if (result.state.killRing !== killRing) {
|
|
255
|
+
setKillRing(result.state.killRing)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
{ stopPropagation: true },
|
|
260
|
+
)
|
|
261
|
+
|
|
244
262
|
const draw = useCallback(
|
|
245
263
|
(ctx: DrawContext) => {
|
|
246
264
|
const displayText = value || placeholder
|
package/src/hosts/base.ts
CHANGED
|
@@ -113,6 +113,21 @@ export abstract class BaseHost implements HostInstance {
|
|
|
113
113
|
// which would overwrite any values set here.
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Optional pre-frame hook. BaseHost will call prepareSelf() and then recurse into children.
|
|
118
|
+
*/
|
|
119
|
+
prepareFrame(): void {
|
|
120
|
+
this.prepareSelf()
|
|
121
|
+
for (const child of this.children) {
|
|
122
|
+
child.prepareFrame?.()
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Override in subclasses to precompute caches once per frame. */
|
|
127
|
+
protected prepareSelf(): void {
|
|
128
|
+
// Default no-op
|
|
129
|
+
}
|
|
130
|
+
|
|
116
131
|
/**
|
|
117
132
|
* Resolve a prop that may be a MotionValue/ColorMotionValue.
|
|
118
133
|
* If it's a spring, subscribes to changes and returns current value.
|
|
@@ -193,6 +208,14 @@ export abstract class BaseHost implements HostInstance {
|
|
|
193
208
|
abstract render(buffer: CellBuffer, palette: Palette): void
|
|
194
209
|
|
|
195
210
|
layout(rect: Rect): void {
|
|
211
|
+
this.layoutWithConstraints(rect)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Apply frame constraints and update this.rect.
|
|
216
|
+
* Returns the constrained rect for convenience in subclasses.
|
|
217
|
+
*/
|
|
218
|
+
protected layoutWithConstraints(rect: Rect): Rect {
|
|
196
219
|
// Apply frame constraints to the assigned rect
|
|
197
220
|
let { w, h } = rect
|
|
198
221
|
|
|
@@ -214,6 +237,8 @@ export abstract class BaseHost implements HostInstance {
|
|
|
214
237
|
this._lastLayoutH = h
|
|
215
238
|
this.onLayout({ width: w, height: h, x: rect.x, y: rect.y })
|
|
216
239
|
}
|
|
240
|
+
|
|
241
|
+
return this.rect
|
|
217
242
|
}
|
|
218
243
|
|
|
219
244
|
/**
|
|
@@ -322,6 +347,16 @@ export abstract class BaseHost implements HostInstance {
|
|
|
322
347
|
this.onLayout = typeof props.onLayout === "function" ? (props.onLayout as typeof this.onLayout) : undefined
|
|
323
348
|
}
|
|
324
349
|
|
|
350
|
+
/**
|
|
351
|
+
* Apply a default greedy value when the prop is omitted.
|
|
352
|
+
* Subclasses with greedy defaults should call this after super.updateProps().
|
|
353
|
+
*/
|
|
354
|
+
protected applyGreedyDefault(props: Record<string, unknown>, fallback: number): void {
|
|
355
|
+
if (!("greedy" in props)) {
|
|
356
|
+
this.greedy = fallback
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
325
360
|
destroy(): void {
|
|
326
361
|
this.clearSpringSubscriptions()
|
|
327
362
|
// Override in subclasses if cleanup needed
|
package/src/hosts/box.ts
CHANGED
|
@@ -6,9 +6,9 @@ import {
|
|
|
6
6
|
type BorderKind,
|
|
7
7
|
borderChars,
|
|
8
8
|
drawBorder,
|
|
9
|
+
fillRectWithInheritedBg,
|
|
9
10
|
type Padding,
|
|
10
11
|
type PaddingInput,
|
|
11
|
-
resolveInheritedBgStyle,
|
|
12
12
|
resolvePadding,
|
|
13
13
|
toColorValue,
|
|
14
14
|
} from "../utils/index.js"
|
|
@@ -82,14 +82,14 @@ export class BoxHost extends SingleChildHost {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
override layout(rect: Rect): void {
|
|
85
|
-
|
|
85
|
+
const layoutRect = this.layoutWithConstraints(rect)
|
|
86
86
|
|
|
87
87
|
const t = this.borderThickness
|
|
88
88
|
const innerRect: Rect = {
|
|
89
|
-
x:
|
|
90
|
-
y:
|
|
91
|
-
w: Math.max(0,
|
|
92
|
-
h: Math.max(0,
|
|
89
|
+
x: layoutRect.x + t + this.padding.left,
|
|
90
|
+
y: layoutRect.y + t + this.padding.top,
|
|
91
|
+
w: Math.max(0, layoutRect.w - this.insetX),
|
|
92
|
+
h: Math.max(0, layoutRect.h - this.insetY),
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
// Layout single child
|
|
@@ -105,8 +105,7 @@ export class BoxHost extends SingleChildHost {
|
|
|
105
105
|
|
|
106
106
|
// Always clear our rect to ensure stale backgrounds are removed.
|
|
107
107
|
// If no explicit bg, inherit from parent (so child boxes are transparent over parents)
|
|
108
|
-
|
|
109
|
-
buffer.fillRect(x, y, w, h, " ".codePointAt(0)!, bgStyleId)
|
|
108
|
+
fillRectWithInheritedBg(buffer, palette, { x, y, w, h }, this.bg, this.parent)
|
|
110
109
|
|
|
111
110
|
// Draw border
|
|
112
111
|
if (this.border !== "none" && w >= 2 && h >= 2) {
|
package/src/hosts/canvas.ts
CHANGED
|
@@ -79,10 +79,12 @@ export class CanvasHost extends BaseHost {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
measure(maxW: number, maxH: number): Size {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
const constrained = this.constrainProposal(maxW, maxH)
|
|
83
|
+
const size = {
|
|
84
|
+
w: this.fixedWidth ?? constrained.w,
|
|
85
|
+
h: this.fixedHeight ?? constrained.h,
|
|
85
86
|
}
|
|
87
|
+
return this.constrainResult(size)
|
|
86
88
|
}
|
|
87
89
|
|
|
88
90
|
render(buffer: CellBuffer, palette: Palette): void {
|
package/src/hosts/codeblock.ts
CHANGED
|
@@ -50,6 +50,7 @@ export class CodeBlockHost extends BaseHost {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
measure(maxW: number, maxH: number): Size {
|
|
53
|
+
const constrained = this.constrainProposal(maxW, maxH)
|
|
53
54
|
this.cachedLineWidths = this.lines.map((l) => lineDisplayWidth(l))
|
|
54
55
|
this.gutterWidth = this.computeGutterWidth()
|
|
55
56
|
|
|
@@ -58,10 +59,10 @@ export class CodeBlockHost extends BaseHost {
|
|
|
58
59
|
|
|
59
60
|
const innerHeight = Math.max(1, this.lines.length) + this.insetY
|
|
60
61
|
|
|
61
|
-
return {
|
|
62
|
-
w: Math.min(
|
|
63
|
-
h: Math.min(
|
|
64
|
-
}
|
|
62
|
+
return this.constrainResult({
|
|
63
|
+
w: Math.min(constrained.w, contentW),
|
|
64
|
+
h: Math.min(constrained.h, innerHeight),
|
|
65
|
+
})
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
override layout(rect: Rect): void {
|
|
@@ -103,17 +103,15 @@ export class FlexContainerHost<A extends FlexAxis> extends BaseHost {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
override layout(rect: Rect): void {
|
|
106
|
-
|
|
107
|
-
// Use this.rect (constrained) not rect (raw input)
|
|
108
|
-
if (!this.rect) return
|
|
106
|
+
const layoutRect = this.layoutWithConstraints(rect)
|
|
109
107
|
const stretchCross = this.axis === "vertical" ? this.alignment === "leading" : this.alignment === "top"
|
|
110
108
|
const insetX = this.padding.left + this.padding.right
|
|
111
109
|
const insetY = this.padding.top + this.padding.bottom
|
|
112
110
|
const innerRect: Rect = {
|
|
113
|
-
x:
|
|
114
|
-
y:
|
|
115
|
-
w: Math.max(0,
|
|
116
|
-
h: Math.max(0,
|
|
111
|
+
x: layoutRect.x + this.padding.left,
|
|
112
|
+
y: layoutRect.y + this.padding.top,
|
|
113
|
+
w: Math.max(0, layoutRect.w - insetX),
|
|
114
|
+
h: Math.max(0, layoutRect.h - insetY),
|
|
117
115
|
}
|
|
118
116
|
layoutFlex(
|
|
119
117
|
this.axis,
|
package/src/hosts/index.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { OverlayHost } from "./overlay.js"
|
|
|
8
8
|
import { OverlayItemHost } from "./overlay-item.js"
|
|
9
9
|
import { ScrollHost } from "./scroll.js"
|
|
10
10
|
import { SpacerHost } from "./spacer.js"
|
|
11
|
-
import { RawTextHost, SpanHost,
|
|
11
|
+
import { RawTextHost, SpanHost, TextHost } from "./text.js"
|
|
12
12
|
import { VStackHost } from "./vstack.js"
|
|
13
13
|
import { ZStackHost } from "./zstack.js"
|
|
14
14
|
|
|
@@ -25,12 +25,10 @@ export { SpacerHost, type SpacerProps } from "./spacer.js"
|
|
|
25
25
|
export {
|
|
26
26
|
RawTextHost,
|
|
27
27
|
SpanHost,
|
|
28
|
-
StyledTextHost,
|
|
29
28
|
TextHost,
|
|
30
29
|
type SpanProps,
|
|
31
30
|
type SpanStyle,
|
|
32
31
|
type StyledSpan,
|
|
33
|
-
type StyledTextProps,
|
|
34
32
|
type TextProps,
|
|
35
33
|
} from "./text.js"
|
|
36
34
|
export { VStackHost, type VStackProps } from "./vstack.js"
|
|
@@ -41,7 +39,6 @@ export { ZStackHost, type ZStackProps } from "./zstack.js"
|
|
|
41
39
|
export const hostRegistry: Record<string, new (props: any, ctx: HostContext) => BaseHost> = {
|
|
42
40
|
text: TextHost,
|
|
43
41
|
span: SpanHost,
|
|
44
|
-
styledtext: StyledTextHost,
|
|
45
42
|
spacer: SpacerHost,
|
|
46
43
|
vstack: VStackHost,
|
|
47
44
|
hstack: HStackHost,
|
|
@@ -38,8 +38,8 @@ export class OverlayItemHost extends SingleChildHost {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
override layout(rect: Rect): void {
|
|
41
|
-
|
|
42
|
-
this.child?.layout(
|
|
41
|
+
const layoutRect = this.layoutWithConstraints(rect)
|
|
42
|
+
this.child?.layout(layoutRect)
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
override render(buffer: CellBuffer, palette: Palette): void {
|
package/src/hosts/overlay.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
import type { CellBuffer, Palette, Rect, Size } from "@effect-tui/core"
|
|
20
20
|
import type { CommonProps, HostContext } from "../reconciler/types.js"
|
|
21
|
-
import {
|
|
21
|
+
import { alignedChildRect } from "../utils/index.js"
|
|
22
22
|
import { BaseHost } from "./base.js"
|
|
23
23
|
import type { OverlayItemHost } from "./overlay-item.js"
|
|
24
24
|
|
|
@@ -29,6 +29,7 @@ export class OverlayHost extends BaseHost {
|
|
|
29
29
|
|
|
30
30
|
constructor(props: OverlayProps, ctx: HostContext) {
|
|
31
31
|
super("overlay", props, ctx)
|
|
32
|
+
this.updateProps(props as unknown as Record<string, unknown>)
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
override measure(maxW: number, maxH: number): Size {
|
|
@@ -56,29 +57,22 @@ export class OverlayHost extends BaseHost {
|
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
override layout(rect: Rect): void {
|
|
59
|
-
|
|
60
|
+
const layoutRect = this.layoutWithConstraints(rect)
|
|
60
61
|
|
|
61
62
|
// Layout base child to fill our rect
|
|
62
63
|
const baseChild = this.children[0]
|
|
63
64
|
if (baseChild) {
|
|
64
|
-
baseChild.layout(
|
|
65
|
+
baseChild.layout(layoutRect)
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
// Layout overlay children with their alignment
|
|
68
69
|
for (let i = 1; i < this.children.length; i++) {
|
|
69
70
|
const child = this.children[i]
|
|
70
|
-
const size = this.cachedSizes[i] ?? child.measure(
|
|
71
|
+
const size = this.cachedSizes[i] ?? child.measure(layoutRect.w, layoutRect.h)
|
|
71
72
|
|
|
72
73
|
// Read alignment from OverlayItemHost (use type check, not instanceof, for bundler compatibility)
|
|
73
74
|
const alignment = child.type === "overlayItem" ? (child as OverlayItemHost).alignment : {}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
child.layout({
|
|
77
|
-
x,
|
|
78
|
-
y,
|
|
79
|
-
w: Math.min(rect.w, size.w),
|
|
80
|
-
h: Math.min(rect.h, size.h),
|
|
81
|
-
})
|
|
75
|
+
child.layout(alignedChildRect(layoutRect, size, alignment.h ?? "center", alignment.v ?? "center"))
|
|
82
76
|
}
|
|
83
77
|
}
|
|
84
78
|
|
package/src/hosts/scroll.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// scroll.ts — Scrollable container host
|
|
2
2
|
import type { CellBuffer, Color, Palette } from "@effect-tui/core"
|
|
3
3
|
import type { CommonProps, HostContext, Rect, Size } from "../reconciler/types.js"
|
|
4
|
-
import {
|
|
4
|
+
import { fillRectWithInheritedBg } from "../utils/index.js"
|
|
5
5
|
import { SingleChildHost } from "./single-child.js"
|
|
6
6
|
|
|
7
7
|
export interface ScrollProps extends CommonProps {
|
|
@@ -55,10 +55,14 @@ export class ScrollHost extends SingleChildHost {
|
|
|
55
55
|
private contentWidth = 0
|
|
56
56
|
private contentHeight = 0
|
|
57
57
|
// Track last reported sizes to avoid redundant callbacks
|
|
58
|
-
private lastReportedContentW =
|
|
59
|
-
private lastReportedContentH =
|
|
58
|
+
private lastReportedContentW = -1
|
|
59
|
+
private lastReportedContentH = -1
|
|
60
|
+
private lastViewportW = -1
|
|
61
|
+
private lastViewportH = -1
|
|
60
62
|
private lastRectX = -1
|
|
61
63
|
private lastRectY = -1
|
|
64
|
+
private lastRectW = -1
|
|
65
|
+
private lastRectH = -1
|
|
62
66
|
// Track if we were at end (for sticky behavior)
|
|
63
67
|
private wasAtEnd = true
|
|
64
68
|
private wasAtEndX = true
|
|
@@ -104,7 +108,7 @@ export class ScrollHost extends SingleChildHost {
|
|
|
104
108
|
}
|
|
105
109
|
|
|
106
110
|
override layout(rect: Rect): void {
|
|
107
|
-
|
|
111
|
+
const layoutRect = this.layoutWithConstraints(rect)
|
|
108
112
|
|
|
109
113
|
// Report content size if changed (deferred from measure() to keep it pure)
|
|
110
114
|
if (this.contentWidth !== this.lastReportedContentW || this.contentHeight !== this.lastReportedContentH) {
|
|
@@ -114,24 +118,32 @@ export class ScrollHost extends SingleChildHost {
|
|
|
114
118
|
}
|
|
115
119
|
|
|
116
120
|
// Report viewport size if changed (for useScroll hook)
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
this.
|
|
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)
|
|
120
125
|
}
|
|
121
126
|
|
|
122
127
|
// Report rect if position changed (for hit testing)
|
|
123
|
-
if (
|
|
124
|
-
this.lastRectX
|
|
125
|
-
this.lastRectY
|
|
126
|
-
|
|
128
|
+
if (
|
|
129
|
+
layoutRect.x !== this.lastRectX ||
|
|
130
|
+
layoutRect.y !== this.lastRectY ||
|
|
131
|
+
layoutRect.w !== this.lastRectW ||
|
|
132
|
+
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)
|
|
127
139
|
}
|
|
128
140
|
|
|
129
141
|
const child = this.child
|
|
130
142
|
if (!child) return
|
|
131
143
|
|
|
132
144
|
// Calculate max scroll offsets
|
|
133
|
-
const maxScrollY = Math.max(0, this.contentHeight -
|
|
134
|
-
const maxScrollX = Math.max(0, this.contentWidth -
|
|
145
|
+
const maxScrollY = Math.max(0, this.contentHeight - layoutRect.h)
|
|
146
|
+
const maxScrollX = Math.max(0, this.contentWidth - layoutRect.w)
|
|
135
147
|
|
|
136
148
|
// Start with the offset from props (controlled by useScroll)
|
|
137
149
|
let scrollY = this.offset
|
|
@@ -185,20 +197,20 @@ export class ScrollHost extends SingleChildHost {
|
|
|
185
197
|
|
|
186
198
|
// Handle alignment when content is smaller than viewport
|
|
187
199
|
if (this.align === "end") {
|
|
188
|
-
if (this.contentHeight <
|
|
200
|
+
if (this.contentHeight < layoutRect.h && (this.axis === "vertical" || this.axis === "both")) {
|
|
189
201
|
// Align to bottom
|
|
190
|
-
scrollY = -(
|
|
202
|
+
scrollY = -(layoutRect.h - this.contentHeight)
|
|
191
203
|
}
|
|
192
|
-
if (this.contentWidth <
|
|
204
|
+
if (this.contentWidth < layoutRect.w && (this.axis === "horizontal" || this.axis === "both")) {
|
|
193
205
|
// Align to right
|
|
194
|
-
scrollX = -(
|
|
206
|
+
scrollX = -(layoutRect.w - this.contentWidth)
|
|
195
207
|
}
|
|
196
208
|
}
|
|
197
209
|
|
|
198
210
|
// Layout child at offset position
|
|
199
211
|
const childRect: Rect = {
|
|
200
|
-
x:
|
|
201
|
-
y:
|
|
212
|
+
x: layoutRect.x - scrollX,
|
|
213
|
+
y: layoutRect.y - scrollY,
|
|
202
214
|
w: this.contentWidth,
|
|
203
215
|
h: this.contentHeight,
|
|
204
216
|
}
|
|
@@ -210,8 +222,7 @@ export class ScrollHost extends SingleChildHost {
|
|
|
210
222
|
const { x, y, w, h } = this.rect
|
|
211
223
|
|
|
212
224
|
// Fill background (inherit from parent if not explicitly set)
|
|
213
|
-
|
|
214
|
-
buffer.fillRect(x, y, w, h, " ".codePointAt(0)!, bgStyleId)
|
|
225
|
+
fillRectWithInheritedBg(buffer, palette, { x, y, w, h }, this.bg, this.parent)
|
|
215
226
|
|
|
216
227
|
// Render children with clipping
|
|
217
228
|
buffer.withClip(x, y, w, h, () => {
|
|
@@ -273,9 +284,7 @@ export class ScrollHost extends SingleChildHost {
|
|
|
273
284
|
override updateProps(props: Record<string, unknown>): void {
|
|
274
285
|
super.updateProps(props)
|
|
275
286
|
// Scroll is greedy by default unless explicitly set to false
|
|
276
|
-
|
|
277
|
-
this.greedy = 1
|
|
278
|
-
}
|
|
287
|
+
this.applyGreedyDefault(props, 1)
|
|
279
288
|
if (props.axis !== undefined) this.axis = (props.axis as ScrollProps["axis"]) ?? "vertical"
|
|
280
289
|
if (props.offset !== undefined) this.offset = props.offset as number
|
|
281
290
|
if (props.offsetX !== undefined) this.offsetX = props.offsetX as number
|
|
@@ -286,6 +295,7 @@ export class ScrollHost extends SingleChildHost {
|
|
|
286
295
|
this.onContentSize = props.onContentSize as ScrollProps["onContentSize"]
|
|
287
296
|
this.onViewportSize = props.onViewportSize as ScrollProps["onViewportSize"]
|
|
288
297
|
this.onEffectiveOffset = props.onEffectiveOffset as ScrollProps["onEffectiveOffset"]
|
|
298
|
+
this.onEffectiveOffsetX = props.onEffectiveOffsetX as ScrollProps["onEffectiveOffsetX"]
|
|
289
299
|
this.onRect = props.onRect as ScrollProps["onRect"]
|
|
290
300
|
}
|
|
291
301
|
}
|
package/src/hosts/spacer.ts
CHANGED
|
@@ -37,9 +37,7 @@ export class SpacerHost extends BaseHost {
|
|
|
37
37
|
override updateProps(props: Record<string, unknown>): void {
|
|
38
38
|
super.updateProps(props)
|
|
39
39
|
// Spacer is greedy by default unless explicitly set to false
|
|
40
|
-
|
|
41
|
-
this.greedy = 1
|
|
42
|
-
}
|
|
40
|
+
this.applyGreedyDefault(props, 1)
|
|
43
41
|
if (props.minWidth !== undefined) this.minWidth = props.minWidth as number
|
|
44
42
|
if (props.minHeight !== undefined) this.minHeight = props.minHeight as number
|
|
45
43
|
}
|