@effect-tui/react 0.2.4 → 0.4.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/components/Markdown.d.ts +3 -3
- package/dist/src/components/Markdown.d.ts.map +1 -1
- package/dist/src/components/Markdown.js +16 -15
- package/dist/src/components/Markdown.js.map +1 -1
- package/dist/src/components/Table.d.ts +58 -0
- package/dist/src/components/Table.d.ts.map +1 -0
- package/dist/src/components/Table.js +185 -0
- package/dist/src/components/Table.js.map +1 -0
- package/dist/src/components/TextInput.js +1 -1
- package/dist/src/components/TextInput.js.map +1 -1
- package/dist/src/components/index.d.ts +1 -0
- package/dist/src/components/index.d.ts.map +1 -1
- package/dist/src/components/index.js +1 -0
- package/dist/src/components/index.js.map +1 -1
- package/dist/src/hosts/canvas.d.ts.map +1 -1
- package/dist/src/hosts/canvas.js +12 -5
- package/dist/src/hosts/canvas.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- 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/utils/border.d.ts +14 -0
- package/dist/src/utils/border.d.ts.map +1 -1
- package/dist/src/utils/border.js +21 -0
- package/dist/src/utils/border.js.map +1 -1
- 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/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/components/Markdown.tsx +20 -17
- package/src/components/Table.tsx +326 -0
- package/src/components/TextInput.tsx +1 -1
- package/src/components/index.ts +1 -0
- package/src/hosts/canvas.ts +13 -5
- package/src/index.ts +4 -0
- package/src/utils/border.ts +33 -0
- package/src/utils/index.ts +9 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effect-tui/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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.4.0",
|
|
87
87
|
"@effect/platform": "^0.94.0",
|
|
88
88
|
"@effect/platform-bun": "^0.87.0",
|
|
89
89
|
"@effect/rpc": "^0.73.0",
|
|
@@ -52,8 +52,8 @@ export interface MarkdownProps {
|
|
|
52
52
|
theme?: MarkdownTheme
|
|
53
53
|
/** Code block theme for syntax highlighting */
|
|
54
54
|
codeTheme?: BundledTheme
|
|
55
|
-
/**
|
|
56
|
-
|
|
55
|
+
/** Enable text wrapping (default: false, text is truncated) */
|
|
56
|
+
wrap?: boolean
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
// Parsed markdown elements
|
|
@@ -226,39 +226,42 @@ function parseMarkdown(content: string): MdElement[] {
|
|
|
226
226
|
}
|
|
227
227
|
|
|
228
228
|
/**
|
|
229
|
-
* Render inline spans as text elements
|
|
229
|
+
* Render inline spans as text elements.
|
|
230
|
+
* Uses <text wrap> for host-level wrapping when wrap=true.
|
|
230
231
|
*/
|
|
231
|
-
function renderSpans(spans: MdSpan[], theme: Required<MarkdownTheme
|
|
232
|
+
function renderSpans(spans: MdSpan[], theme: Required<MarkdownTheme>, wrap = false) {
|
|
232
233
|
return spans.map((span, i) => {
|
|
233
234
|
switch (span.type) {
|
|
234
235
|
case "text":
|
|
235
236
|
return (
|
|
236
|
-
<text key={i} fg={theme.text}>
|
|
237
|
+
<text key={i} fg={theme.text} wrap={wrap}>
|
|
237
238
|
{span.text}
|
|
238
239
|
</text>
|
|
239
240
|
)
|
|
240
241
|
case "bold":
|
|
241
242
|
return (
|
|
242
|
-
<text key={i} fg={theme.bold} bold>
|
|
243
|
+
<text key={i} fg={theme.bold} bold wrap={wrap}>
|
|
243
244
|
{span.text}
|
|
244
245
|
</text>
|
|
245
246
|
)
|
|
246
247
|
case "italic":
|
|
247
248
|
return (
|
|
248
|
-
<text key={i} fg={theme.italic} italic>
|
|
249
|
+
<text key={i} fg={theme.italic} italic wrap={wrap}>
|
|
249
250
|
{span.text}
|
|
250
251
|
</text>
|
|
251
252
|
)
|
|
252
253
|
case "code":
|
|
253
254
|
return (
|
|
254
|
-
<text key={i} fg={theme.code} bg={theme.codeBg}>
|
|
255
|
+
<text key={i} fg={theme.code} bg={theme.codeBg} wrap={wrap}>
|
|
255
256
|
{span.text}
|
|
256
257
|
</text>
|
|
257
258
|
)
|
|
258
259
|
case "link":
|
|
259
260
|
return (
|
|
260
261
|
<hstack key={i}>
|
|
261
|
-
<text fg={theme.link}
|
|
262
|
+
<text fg={theme.link} wrap={wrap}>
|
|
263
|
+
{span.text}
|
|
264
|
+
</text>
|
|
262
265
|
<text fg={theme.linkUrl}>{" ("}</text>
|
|
263
266
|
<text fg={theme.linkUrl}>{span.url}</text>
|
|
264
267
|
<text fg={theme.linkUrl}>{")"}</text>
|
|
@@ -294,7 +297,7 @@ function renderSpans(spans: MdSpan[], theme: Required<MarkdownTheme>) {
|
|
|
294
297
|
* `} />
|
|
295
298
|
* ```
|
|
296
299
|
*/
|
|
297
|
-
export function Markdown({ content, theme: themeOverrides, codeTheme = "nord" }: MarkdownProps) {
|
|
300
|
+
export function Markdown({ content, theme: themeOverrides, codeTheme = "nord", wrap = false }: MarkdownProps) {
|
|
298
301
|
const theme = { ...defaultTheme, ...themeOverrides }
|
|
299
302
|
const elements = parseMarkdown(content)
|
|
300
303
|
|
|
@@ -304,27 +307,27 @@ export function Markdown({ content, theme: themeOverrides, codeTheme = "nord" }:
|
|
|
304
307
|
switch (el.type) {
|
|
305
308
|
case "h1":
|
|
306
309
|
return (
|
|
307
|
-
<text key={i} fg={theme.h1} bold>
|
|
310
|
+
<text key={i} fg={theme.h1} bold wrap={wrap}>
|
|
308
311
|
{"# "}
|
|
309
312
|
{el.text}
|
|
310
313
|
</text>
|
|
311
314
|
)
|
|
312
315
|
case "h2":
|
|
313
316
|
return (
|
|
314
|
-
<text key={i} fg={theme.h2} bold>
|
|
317
|
+
<text key={i} fg={theme.h2} bold wrap={wrap}>
|
|
315
318
|
{"## "}
|
|
316
319
|
{el.text}
|
|
317
320
|
</text>
|
|
318
321
|
)
|
|
319
322
|
case "h3":
|
|
320
323
|
return (
|
|
321
|
-
<text key={i} fg={theme.h3} bold>
|
|
324
|
+
<text key={i} fg={theme.h3} bold wrap={wrap}>
|
|
322
325
|
{"### "}
|
|
323
326
|
{el.text}
|
|
324
327
|
</text>
|
|
325
328
|
)
|
|
326
329
|
case "paragraph":
|
|
327
|
-
return <hstack key={i}>{renderSpans(el.spans, theme)}</hstack>
|
|
330
|
+
return <hstack key={i}>{renderSpans(el.spans, theme, wrap)}</hstack>
|
|
328
331
|
case "code":
|
|
329
332
|
return (
|
|
330
333
|
<CodeBlock
|
|
@@ -340,7 +343,7 @@ export function Markdown({ content, theme: themeOverrides, codeTheme = "nord" }:
|
|
|
340
343
|
return (
|
|
341
344
|
<hstack key={i}>
|
|
342
345
|
<text fg={theme.quoteBorder}>{"│ "}</text>
|
|
343
|
-
{renderSpans(el.spans, theme)}
|
|
346
|
+
{renderSpans(el.spans, theme, wrap)}
|
|
344
347
|
</hstack>
|
|
345
348
|
)
|
|
346
349
|
case "ul":
|
|
@@ -349,7 +352,7 @@ export function Markdown({ content, theme: themeOverrides, codeTheme = "nord" }:
|
|
|
349
352
|
{el.items.map((item, j) => (
|
|
350
353
|
<hstack key={j}>
|
|
351
354
|
<text fg={theme.listMarker}>{" • "}</text>
|
|
352
|
-
{renderSpans(item, theme)}
|
|
355
|
+
{renderSpans(item, theme, wrap)}
|
|
353
356
|
</hstack>
|
|
354
357
|
))}
|
|
355
358
|
</vstack>
|
|
@@ -360,7 +363,7 @@ export function Markdown({ content, theme: themeOverrides, codeTheme = "nord" }:
|
|
|
360
363
|
{el.items.map((item, j) => (
|
|
361
364
|
<hstack key={j}>
|
|
362
365
|
<text fg={theme.listMarker}>{` ${el.start + j}. `}</text>
|
|
363
|
-
{renderSpans(item, theme)}
|
|
366
|
+
{renderSpans(item, theme, wrap)}
|
|
364
367
|
</hstack>
|
|
365
368
|
))}
|
|
366
369
|
</vstack>
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Table component for rendering tabular data with box-drawing borders.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```tsx
|
|
6
|
+
* <Table data={users} border="square" headerSeparator>
|
|
7
|
+
* <Table.Column header="Name" width={20}>
|
|
8
|
+
* {(user) => <text fg={WHITE}>{user.name}</text>}
|
|
9
|
+
* </Table.Column>
|
|
10
|
+
* <Table.Column header="Score" width={5} align="right">
|
|
11
|
+
* {(user) => <text fg={user.score > 80 ? GREEN : YELLOW}>{user.score}</text>}
|
|
12
|
+
* </Table.Column>
|
|
13
|
+
* </Table>
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { type Color, Colors } from "@effect-tui/core"
|
|
18
|
+
import { Children, isValidElement, type ReactElement, type ReactNode } from "react"
|
|
19
|
+
import { type BorderKind, tableBorderChars } from "../utils/border.js"
|
|
20
|
+
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// Types
|
|
23
|
+
// ============================================================================
|
|
24
|
+
|
|
25
|
+
type Align = "left" | "center" | "right"
|
|
26
|
+
|
|
27
|
+
export interface ColumnProps<T = unknown> {
|
|
28
|
+
/** Column header text or element */
|
|
29
|
+
header?: ReactNode
|
|
30
|
+
/** Fixed column width in characters */
|
|
31
|
+
width?: number
|
|
32
|
+
/** Minimum width when auto-sizing (default: header length) */
|
|
33
|
+
minWidth?: number
|
|
34
|
+
/** Maximum width when auto-sizing */
|
|
35
|
+
maxWidth?: number
|
|
36
|
+
/** Text alignment within cell (default: "left") */
|
|
37
|
+
align?: Align
|
|
38
|
+
/** Render function for cell content */
|
|
39
|
+
children: (item: T, index: number) => ReactNode
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface TableProps<T> {
|
|
43
|
+
/** Data array to render rows from */
|
|
44
|
+
data: readonly T[]
|
|
45
|
+
/** Border style (default: "square") */
|
|
46
|
+
border?: BorderKind
|
|
47
|
+
/** Show separator line between header and body */
|
|
48
|
+
headerSeparator?: boolean
|
|
49
|
+
/** Show separator lines between all rows */
|
|
50
|
+
rowSeparator?: boolean
|
|
51
|
+
/** Cell padding in characters (default: 1) */
|
|
52
|
+
padding?: number
|
|
53
|
+
/** Border color (default: dim gray) */
|
|
54
|
+
borderColor?: Color
|
|
55
|
+
/** Header text color (default: dim gray) */
|
|
56
|
+
headerColor?: Color
|
|
57
|
+
/** Column definitions */
|
|
58
|
+
children: ReactNode
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ============================================================================
|
|
62
|
+
// Column (data holder, not rendered directly)
|
|
63
|
+
// ============================================================================
|
|
64
|
+
|
|
65
|
+
function Column<T>(_props: ColumnProps<T>): ReactElement | null {
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ============================================================================
|
|
70
|
+
// Internal: Extract column configs from children
|
|
71
|
+
// ============================================================================
|
|
72
|
+
|
|
73
|
+
interface ColumnConfig<T> {
|
|
74
|
+
header?: ReactNode
|
|
75
|
+
width: number
|
|
76
|
+
align: Align
|
|
77
|
+
render: (item: T, index: number) => ReactNode
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function extractColumns<T>(children: ReactNode, data: readonly T[]): ColumnConfig<T>[] {
|
|
81
|
+
const columns: ColumnConfig<T>[] = []
|
|
82
|
+
|
|
83
|
+
Children.forEach(children, (child) => {
|
|
84
|
+
if (!isValidElement(child)) return
|
|
85
|
+
// Check if it's a Column element by checking the function name
|
|
86
|
+
if (typeof child.type === "function" && (child.type as { name?: string }).name === "Column") {
|
|
87
|
+
const props = child.props as ColumnProps<T>
|
|
88
|
+
|
|
89
|
+
// Calculate width
|
|
90
|
+
let width = props.width ?? 0
|
|
91
|
+
if (width === 0) {
|
|
92
|
+
// Auto-size: measure header + content
|
|
93
|
+
const headerLen = typeof props.header === "string" ? props.header.length : 0
|
|
94
|
+
const minFromHeader = Math.max(headerLen, props.minWidth ?? 0)
|
|
95
|
+
|
|
96
|
+
// Measure content from data (sample first few rows)
|
|
97
|
+
let maxContentLen = 0
|
|
98
|
+
for (let i = 0; i < Math.min(data.length, 10); i++) {
|
|
99
|
+
const rendered = props.children(data[i]!, i)
|
|
100
|
+
// Rough estimate - extract text content length
|
|
101
|
+
const textLen = estimateTextLength(rendered)
|
|
102
|
+
maxContentLen = Math.max(maxContentLen, textLen)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
width = Math.max(minFromHeader, maxContentLen, 1)
|
|
106
|
+
if (props.maxWidth !== undefined) {
|
|
107
|
+
width = Math.min(width, props.maxWidth)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
columns.push({
|
|
112
|
+
header: props.header,
|
|
113
|
+
width,
|
|
114
|
+
align: props.align ?? "left",
|
|
115
|
+
render: props.children,
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
return columns
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function estimateTextLength(node: ReactNode): number {
|
|
124
|
+
if (node == null) return 0
|
|
125
|
+
if (typeof node === "string") return node.length
|
|
126
|
+
if (typeof node === "number") return String(node).length
|
|
127
|
+
if (Array.isArray(node)) return node.reduce((sum, n) => sum + estimateTextLength(n), 0)
|
|
128
|
+
if (isValidElement(node)) {
|
|
129
|
+
const children = (node.props as { children?: ReactNode }).children
|
|
130
|
+
return estimateTextLength(children)
|
|
131
|
+
}
|
|
132
|
+
return 0
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ============================================================================
|
|
136
|
+
// Internal: Helpers
|
|
137
|
+
// ============================================================================
|
|
138
|
+
|
|
139
|
+
function alignText(text: string, width: number, align: Align): string {
|
|
140
|
+
if (text.length >= width) return text.slice(0, width)
|
|
141
|
+
const padding = width - text.length
|
|
142
|
+
switch (align) {
|
|
143
|
+
case "right":
|
|
144
|
+
return " ".repeat(padding) + text
|
|
145
|
+
case "center": {
|
|
146
|
+
const left = Math.floor(padding / 2)
|
|
147
|
+
const right = padding - left
|
|
148
|
+
return " ".repeat(left) + text + " ".repeat(right)
|
|
149
|
+
}
|
|
150
|
+
default:
|
|
151
|
+
return text + " ".repeat(padding)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function buildBorderLine(
|
|
156
|
+
widths: number[],
|
|
157
|
+
left: string,
|
|
158
|
+
mid: string,
|
|
159
|
+
right: string,
|
|
160
|
+
fill: string,
|
|
161
|
+
padding: number,
|
|
162
|
+
): string {
|
|
163
|
+
return (
|
|
164
|
+
left + widths.map((w) => fill.repeat(w + padding * 2)).join(mid) + right
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ============================================================================
|
|
169
|
+
// Table Component
|
|
170
|
+
// ============================================================================
|
|
171
|
+
|
|
172
|
+
function TableRoot<T>({
|
|
173
|
+
data,
|
|
174
|
+
border = "square",
|
|
175
|
+
headerSeparator = false,
|
|
176
|
+
rowSeparator = false,
|
|
177
|
+
padding = 1,
|
|
178
|
+
borderColor = Colors.gray(12),
|
|
179
|
+
headerColor = Colors.gray(12),
|
|
180
|
+
children,
|
|
181
|
+
}: TableProps<T>) {
|
|
182
|
+
const columns = extractColumns<T>(children, data)
|
|
183
|
+
if (columns.length === 0) return null
|
|
184
|
+
|
|
185
|
+
const chars = tableBorderChars(border)
|
|
186
|
+
const widths = columns.map((c) => c.width)
|
|
187
|
+
const pad = " ".repeat(padding)
|
|
188
|
+
const noBorder = border === "none"
|
|
189
|
+
|
|
190
|
+
// Build border lines
|
|
191
|
+
const topLine = buildBorderLine(widths, chars.tl, chars.tt, chars.tr, chars.h, padding)
|
|
192
|
+
const midLine = buildBorderLine(widths, chars.lt, chars.cross, chars.rt, chars.h, padding)
|
|
193
|
+
const bottomLine = buildBorderLine(widths, chars.bl, chars.bt, chars.br, chars.h, padding)
|
|
194
|
+
|
|
195
|
+
// Check if any column has a header
|
|
196
|
+
const hasHeader = columns.some((c) => c.header !== undefined)
|
|
197
|
+
|
|
198
|
+
const rows: ReactNode[] = []
|
|
199
|
+
|
|
200
|
+
// Top border
|
|
201
|
+
if (!noBorder) {
|
|
202
|
+
rows.push(
|
|
203
|
+
<text key="border-top" fg={borderColor}>
|
|
204
|
+
{topLine}
|
|
205
|
+
</text>,
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Header row
|
|
210
|
+
if (hasHeader) {
|
|
211
|
+
const headerCells: ReactNode[] = []
|
|
212
|
+
columns.forEach((col, i) => {
|
|
213
|
+
// Left border or column separator
|
|
214
|
+
if (!noBorder) {
|
|
215
|
+
headerCells.push(
|
|
216
|
+
<text key={`h-sep-${i}`} fg={borderColor}>
|
|
217
|
+
{chars.v + pad}
|
|
218
|
+
</text>,
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
const headerText = typeof col.header === "string" ? col.header : ""
|
|
222
|
+
headerCells.push(
|
|
223
|
+
<text key={`h-${i}`} fg={headerColor}>
|
|
224
|
+
{alignText(headerText, col.width, col.align)}
|
|
225
|
+
</text>,
|
|
226
|
+
)
|
|
227
|
+
// Right padding (border comes from next column's left, or final border)
|
|
228
|
+
if (!noBorder) {
|
|
229
|
+
headerCells.push(
|
|
230
|
+
<text key={`h-pad-${i}`} fg={borderColor}>
|
|
231
|
+
{pad}
|
|
232
|
+
</text>,
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
})
|
|
236
|
+
// Final right border
|
|
237
|
+
if (!noBorder) {
|
|
238
|
+
headerCells.push(
|
|
239
|
+
<text key="h-sep-end" fg={borderColor}>
|
|
240
|
+
{chars.v}
|
|
241
|
+
</text>,
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
rows.push(<hstack key="header" spacing={0}>{headerCells}</hstack>)
|
|
245
|
+
|
|
246
|
+
// Header separator
|
|
247
|
+
if (headerSeparator && !noBorder) {
|
|
248
|
+
rows.push(
|
|
249
|
+
<text key="border-mid-header" fg={borderColor}>
|
|
250
|
+
{midLine}
|
|
251
|
+
</text>,
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Data rows
|
|
257
|
+
data.forEach((item, rowIdx) => {
|
|
258
|
+
// Row separator (between rows)
|
|
259
|
+
if (rowSeparator && rowIdx > 0 && !noBorder) {
|
|
260
|
+
rows.push(
|
|
261
|
+
<text key={`border-mid-${rowIdx}`} fg={borderColor}>
|
|
262
|
+
{midLine}
|
|
263
|
+
</text>,
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const rowCells: ReactNode[] = []
|
|
268
|
+
columns.forEach((col, colIdx) => {
|
|
269
|
+
// Left border or column separator
|
|
270
|
+
if (!noBorder) {
|
|
271
|
+
rowCells.push(
|
|
272
|
+
<text key={`r${rowIdx}-sep-${colIdx}`} fg={borderColor}>
|
|
273
|
+
{chars.v + pad}
|
|
274
|
+
</text>,
|
|
275
|
+
)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Cell content
|
|
279
|
+
const content = col.render(item, rowIdx)
|
|
280
|
+
rowCells.push(
|
|
281
|
+
<hstack key={`r${rowIdx}-c${colIdx}`} spacing={0} width={col.width}>
|
|
282
|
+
{content}
|
|
283
|
+
</hstack>,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
// Right padding
|
|
287
|
+
if (!noBorder) {
|
|
288
|
+
rowCells.push(
|
|
289
|
+
<text key={`r${rowIdx}-pad-${colIdx}`} fg={borderColor}>
|
|
290
|
+
{pad}
|
|
291
|
+
</text>,
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
})
|
|
295
|
+
// Final right border
|
|
296
|
+
if (!noBorder) {
|
|
297
|
+
rowCells.push(
|
|
298
|
+
<text key={`r${rowIdx}-sep-end`} fg={borderColor}>
|
|
299
|
+
{chars.v}
|
|
300
|
+
</text>,
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
rows.push(<hstack key={`row-${rowIdx}`} spacing={0}>{rowCells}</hstack>)
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
// Bottom border
|
|
307
|
+
if (!noBorder) {
|
|
308
|
+
rows.push(
|
|
309
|
+
<text key="border-bottom" fg={borderColor}>
|
|
310
|
+
{bottomLine}
|
|
311
|
+
</text>,
|
|
312
|
+
)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return <vstack spacing={0}>{rows}</vstack>
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ============================================================================
|
|
319
|
+
// Exports
|
|
320
|
+
// ============================================================================
|
|
321
|
+
|
|
322
|
+
export const Table = Object.assign(TableRoot, {
|
|
323
|
+
Column,
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
export type { Align as TableAlign }
|
package/src/components/index.ts
CHANGED
|
@@ -3,4 +3,5 @@ export { Markdown, type MarkdownProps, type MarkdownTheme } from "./Markdown.js"
|
|
|
3
3
|
export { MultilineTextInput, type MultilineTextInputProps } from "./MultilineTextInput.js"
|
|
4
4
|
export { Overlay, type OverlayItemProps, type OverlayProps } from "./Overlay.js"
|
|
5
5
|
export { Static, type StaticProps } from "./Static.js"
|
|
6
|
+
export { Table, type TableAlign, type ColumnProps, type TableProps } from "./Table.js"
|
|
6
7
|
export { TextInput, type TextInputProps } from "./TextInput.js"
|
package/src/hosts/canvas.ts
CHANGED
|
@@ -89,10 +89,14 @@ export class CanvasHost extends BaseHost {
|
|
|
89
89
|
if (!this.rect) return
|
|
90
90
|
const { x: ox, y: oy, w, h } = this.rect
|
|
91
91
|
|
|
92
|
+
// Get inherited background for use in drawing functions
|
|
93
|
+
const { value: inheritedBgValue, styleId: inheritedBgStyleId } = this.inheritBg
|
|
94
|
+
? resolveInheritedBgStyle(palette, undefined, this.parent)
|
|
95
|
+
: { value: undefined, styleId: 0 }
|
|
96
|
+
|
|
92
97
|
// Pre-fill with inherited background if requested
|
|
93
|
-
if (this.inheritBg) {
|
|
94
|
-
|
|
95
|
-
buffer.fillRect(ox, oy, w, h, " ".codePointAt(0)!, styleId)
|
|
98
|
+
if (this.inheritBg && inheritedBgValue !== undefined) {
|
|
99
|
+
buffer.fillRect(ox, oy, w, h, " ".codePointAt(0)!, inheritedBgStyleId)
|
|
96
100
|
}
|
|
97
101
|
|
|
98
102
|
// Create draw context
|
|
@@ -103,9 +107,11 @@ export class CanvasHost extends BaseHost {
|
|
|
103
107
|
text: (x, y, str, opts) => {
|
|
104
108
|
const px = Math.round(ox + x)
|
|
105
109
|
const py = Math.round(oy + y)
|
|
110
|
+
// Use inherited bg when inheritBg is enabled and no explicit bg provided
|
|
111
|
+
const effectiveBg = opts?.bg ?? (this.inheritBg ? inheritedBgValue : undefined)
|
|
106
112
|
const style = styleIdFromProps(palette, {
|
|
107
113
|
fg: opts?.fg,
|
|
108
|
-
bg:
|
|
114
|
+
bg: effectiveBg,
|
|
109
115
|
bold: opts?.bold,
|
|
110
116
|
italic: opts?.italic,
|
|
111
117
|
underline: opts?.underline,
|
|
@@ -123,9 +129,11 @@ export class CanvasHost extends BaseHost {
|
|
|
123
129
|
const px = Math.round(ox + x)
|
|
124
130
|
const py = Math.round(oy + y)
|
|
125
131
|
const cp = char.codePointAt(0)!
|
|
132
|
+
// Use inherited bg when inheritBg is enabled and no explicit bg provided
|
|
133
|
+
const effectiveBg = opts?.bg ?? (this.inheritBg ? inheritedBgValue : undefined)
|
|
126
134
|
const style = styleIdFromProps(palette, {
|
|
127
135
|
fg: opts?.fg,
|
|
128
|
-
bg:
|
|
136
|
+
bg: effectiveBg,
|
|
129
137
|
bold: opts?.bold,
|
|
130
138
|
italic: opts?.italic,
|
|
131
139
|
underline: opts?.underline,
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ export type { CodeBlockProps } from "./codeblock.js"
|
|
|
5
5
|
export { CodeBlock } from "./codeblock.js"
|
|
6
6
|
// Input components
|
|
7
7
|
export {
|
|
8
|
+
type ColumnProps,
|
|
8
9
|
Markdown,
|
|
9
10
|
type MarkdownProps,
|
|
10
11
|
type MarkdownTheme,
|
|
@@ -15,6 +16,9 @@ export {
|
|
|
15
16
|
type OverlayProps,
|
|
16
17
|
Static,
|
|
17
18
|
type StaticProps,
|
|
19
|
+
Table,
|
|
20
|
+
type TableAlign,
|
|
21
|
+
type TableProps,
|
|
18
22
|
TextInput,
|
|
19
23
|
type TextInputProps,
|
|
20
24
|
} from "./components/index.js"
|
package/src/utils/border.ts
CHANGED
|
@@ -15,6 +15,17 @@ export interface BorderChars {
|
|
|
15
15
|
v: string // vertical
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Extended border chars for tables with T-connectors.
|
|
20
|
+
*/
|
|
21
|
+
export interface TableBorderChars extends BorderChars {
|
|
22
|
+
tt: string // top T (┬)
|
|
23
|
+
bt: string // bottom T (┴)
|
|
24
|
+
lt: string // left T (├)
|
|
25
|
+
rt: string // right T (┤)
|
|
26
|
+
cross: string // cross (┼)
|
|
27
|
+
}
|
|
28
|
+
|
|
18
29
|
/**
|
|
19
30
|
* Get border characters for a given border style.
|
|
20
31
|
* Returns empty strings for "none" (drawing code should skip when border is "none").
|
|
@@ -38,6 +49,28 @@ export function borderChars(kind: BorderKind): BorderChars {
|
|
|
38
49
|
}
|
|
39
50
|
}
|
|
40
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Get table border characters (includes T-connectors and cross).
|
|
54
|
+
*/
|
|
55
|
+
export function tableBorderChars(kind: BorderKind): TableBorderChars {
|
|
56
|
+
switch (kind) {
|
|
57
|
+
case "rounded":
|
|
58
|
+
return { tl: "╭", tr: "╮", bl: "╰", br: "╯", h: "─", v: "│", tt: "┬", bt: "┴", lt: "├", rt: "┤", cross: "┼" }
|
|
59
|
+
case "square":
|
|
60
|
+
return { tl: "┌", tr: "┐", bl: "└", br: "┘", h: "─", v: "│", tt: "┬", bt: "┴", lt: "├", rt: "┤", cross: "┼" }
|
|
61
|
+
case "double":
|
|
62
|
+
return { tl: "╔", tr: "╗", bl: "╚", br: "╝", h: "═", v: "║", tt: "╦", bt: "╩", lt: "╠", rt: "╣", cross: "╬" }
|
|
63
|
+
case "heavy":
|
|
64
|
+
return { tl: "┏", tr: "┓", bl: "┗", br: "┛", h: "━", v: "┃", tt: "┳", bt: "┻", lt: "┣", rt: "┫", cross: "╋" }
|
|
65
|
+
case "dashed":
|
|
66
|
+
return { tl: "┌", tr: "┐", bl: "└", br: "┘", h: "┄", v: "┆", tt: "┬", bt: "┴", lt: "├", rt: "┤", cross: "┼" }
|
|
67
|
+
case "ascii":
|
|
68
|
+
return { tl: "+", tr: "+", bl: "+", br: "+", h: "-", v: "|", tt: "+", bt: "+", lt: "+", rt: "+", cross: "+" }
|
|
69
|
+
default:
|
|
70
|
+
return { tl: "", tr: "", bl: "", br: "", h: "", v: "", tt: "", bt: "", lt: "", rt: "", cross: "" }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
41
74
|
export interface ClipRect {
|
|
42
75
|
ox: number
|
|
43
76
|
oy: number
|
package/src/utils/index.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
export { alignInRect, type HAlign, type VAlign } from "./alignment.js"
|
|
2
|
-
export {
|
|
2
|
+
export {
|
|
3
|
+
type BorderChars,
|
|
4
|
+
type BorderKind,
|
|
5
|
+
borderChars,
|
|
6
|
+
type ClipRect,
|
|
7
|
+
drawBorder,
|
|
8
|
+
type TableBorderChars,
|
|
9
|
+
tableBorderChars,
|
|
10
|
+
} from "./border.js"
|
|
3
11
|
export { type FlexAlignment, type FlexAxis, type FlexMeasureResult, layoutFlex, measureFlex } from "./flex-layout.js"
|
|
4
12
|
export { type Padding, type PaddingInput, resolvePadding } from "./padding.js"
|
|
5
13
|
export {
|