@effect-tui/react 0.2.3 → 0.3.1

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.
Files changed (46) hide show
  1. package/dist/src/components/Table.d.ts +58 -0
  2. package/dist/src/components/Table.d.ts.map +1 -0
  3. package/dist/src/components/Table.js +185 -0
  4. package/dist/src/components/Table.js.map +1 -0
  5. package/dist/src/components/TextInput.js +1 -1
  6. package/dist/src/components/TextInput.js.map +1 -1
  7. package/dist/src/components/index.d.ts +1 -0
  8. package/dist/src/components/index.d.ts.map +1 -1
  9. package/dist/src/components/index.js +1 -0
  10. package/dist/src/components/index.js.map +1 -1
  11. package/dist/src/dev.d.ts.map +1 -1
  12. package/dist/src/dev.js +19 -3
  13. package/dist/src/dev.js.map +1 -1
  14. package/dist/src/hosts/canvas.d.ts.map +1 -1
  15. package/dist/src/hosts/canvas.js +12 -5
  16. package/dist/src/hosts/canvas.js.map +1 -1
  17. package/dist/src/index.d.ts +1 -1
  18. package/dist/src/index.d.ts.map +1 -1
  19. package/dist/src/index.js +1 -1
  20. package/dist/src/index.js.map +1 -1
  21. package/dist/src/renderer.d.ts.map +1 -1
  22. package/dist/src/renderer.js +3 -1
  23. package/dist/src/renderer.js.map +1 -1
  24. package/dist/src/test/mock-streams.d.ts.map +1 -1
  25. package/dist/src/test/mock-streams.js +2 -1
  26. package/dist/src/test/mock-streams.js.map +1 -1
  27. package/dist/src/utils/border.d.ts +14 -0
  28. package/dist/src/utils/border.d.ts.map +1 -1
  29. package/dist/src/utils/border.js +21 -0
  30. package/dist/src/utils/border.js.map +1 -1
  31. package/dist/src/utils/index.d.ts +1 -1
  32. package/dist/src/utils/index.d.ts.map +1 -1
  33. package/dist/src/utils/index.js +1 -1
  34. package/dist/src/utils/index.js.map +1 -1
  35. package/dist/tsconfig.tsbuildinfo +1 -1
  36. package/package.json +2 -2
  37. package/src/components/Table.tsx +326 -0
  38. package/src/components/TextInput.tsx +1 -1
  39. package/src/components/index.ts +1 -0
  40. package/src/dev.tsx +22 -4
  41. package/src/hosts/canvas.ts +13 -5
  42. package/src/index.ts +4 -0
  43. package/src/renderer.ts +3 -1
  44. package/src/test/mock-streams.ts +2 -1
  45. package/src/utils/border.ts +33 -0
  46. 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.2.3",
3
+ "version": "0.3.1",
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.2.3",
86
+ "@effect-tui/core": "^0.3.1",
87
87
  "@effect/platform": "^0.94.0",
88
88
  "@effect/platform-bun": "^0.87.0",
89
89
  "@effect/rpc": "^0.73.0",
@@ -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 }
@@ -352,5 +352,5 @@ export function TextInput({
352
352
  ],
353
353
  )
354
354
 
355
- return <canvas draw={draw} width={width} height={1} />
355
+ return <canvas draw={draw} width={width} height={1} inheritBg={bg === undefined} />
356
356
  }
@@ -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/dev.tsx CHANGED
@@ -222,6 +222,20 @@ export interface DevRenderResult {
222
222
  stop: () => Promise<void>
223
223
  }
224
224
 
225
+ /**
226
+ * Clear require.cache for project files to ensure fresh imports.
227
+ * Bun uses require.cache internally even for ESM modules.
228
+ * This is necessary because query-string cache-busting only affects
229
+ * the entry point, not transitive dependencies.
230
+ */
231
+ function clearProjectCache(projectRoot: string): void {
232
+ for (const file of Object.keys(require.cache)) {
233
+ if (file.startsWith(projectRoot) && !file.includes("node_modules")) {
234
+ delete require.cache[file]
235
+ }
236
+ }
237
+ }
238
+
225
239
  /**
226
240
  * Wait for a file to stabilize (no size changes) before proceeding.
227
241
  * This avoids importing partially-written files.
@@ -401,11 +415,17 @@ export async function devRender(entryPath: string, options?: DevRenderOptions):
401
415
  let version = 0
402
416
  let subscription: watcher.AsyncSubscription | null = null
403
417
 
418
+ // Determine project root for cache clearing
419
+ const projectRoot = options?.watchDirs?.[0] ?? dirname(entryPath)
420
+
404
421
  // Import and render the module
405
422
  const render = async () => {
406
423
  const thisVersion = ++version
407
424
 
408
425
  try {
426
+ // Clear module cache for project files before re-importing
427
+ clearProjectCache(projectRoot)
428
+
409
429
  // Cache-bust by adding query string with version
410
430
  const mod = await import(`${entryPath}?v=${thisVersion}`)
411
431
 
@@ -445,10 +465,8 @@ export async function devRender(entryPath: string, options?: DevRenderOptions):
445
465
  await render()
446
466
 
447
467
  // Watch for changes using @parcel/watcher
448
- const watchDir = options?.watchDirs?.[0] ?? dirname(entryPath)
449
-
450
468
  try {
451
- subscription = await watcher.subscribe(watchDir, async (err, events) => {
469
+ subscription = await watcher.subscribe(projectRoot, async (err, events) => {
452
470
  if (err) {
453
471
  options?.onError?.(err)
454
472
  console.error("[devRender] Watcher error:", err)
@@ -472,7 +490,7 @@ export async function devRender(entryPath: string, options?: DevRenderOptions):
472
490
  }, debounceMs)
473
491
  })
474
492
  } catch (err) {
475
- console.error(`[devRender] Failed to watch ${watchDir}:`, err)
493
+ console.error(`[devRender] Failed to watch ${projectRoot}:`, err)
476
494
  }
477
495
 
478
496
  const stop = async () => {
@@ -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
- const { styleId } = resolveInheritedBgStyle(palette, undefined, this.parent)
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: opts?.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: opts?.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/renderer.ts CHANGED
@@ -176,7 +176,9 @@ export function createRenderer(options?: RendererOptions): TuiRenderer {
176
176
  })
177
177
 
178
178
  // Combine static + dynamic output for atomic write
179
- const output = staticOutput + modeOutput + state.palette.sgr(0)
179
+ // Wrap in synchronized output mode (DECSET 2026) to prevent tearing
180
+ // Terminals that don't support it will safely ignore these sequences
181
+ const output = ANSI.sync.begin + staticOutput + modeOutput + state.palette.sgr(0) + ANSI.sync.end
180
182
  contentH = contentHeight
181
183
  const diffAnsiMs = performance.now() - diffStartMs
182
184
  Prof.endPhase("diff+ansi", t)
@@ -134,7 +134,8 @@ export function encodeKey(key: KeyMsg): Buffer {
134
134
  */
135
135
  export function stripAnsi(str: string): string {
136
136
  // eslint-disable-next-line no-control-regex
137
- return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").replace(/\x1b\][^\x07]*\x07/g, "")
137
+ // Pattern handles: CSI sequences (including DEC private modes like ?2026h), OSC sequences
138
+ return str.replace(/\x1b\[[\?0-9;]*[a-zA-Z]/g, "").replace(/\x1b\][^\x07]*\x07/g, "")
138
139
  }
139
140
 
140
141
  /**
@@ -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
@@ -1,5 +1,13 @@
1
1
  export { alignInRect, type HAlign, type VAlign } from "./alignment.js"
2
- export { type BorderChars, type BorderKind, borderChars, type ClipRect, drawBorder } from "./border.js"
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 {