@mdxui/terminal 2.0.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 +571 -0
- package/dist/ansi-css-Sk5mWtdK.d.ts +119 -0
- package/dist/ansi-css-V6JIHGsM.d.ts +119 -0
- package/dist/ansi-css-_3eSEU9d.d.ts +119 -0
- package/dist/chunk-3EFDH7PK.js +5235 -0
- package/dist/chunk-3RG5ZIWI.js +10 -0
- package/dist/chunk-3X5IR6WE.js +884 -0
- package/dist/chunk-4FV5ZDCE.js +5236 -0
- package/dist/chunk-4OVMSF2J.js +243 -0
- package/dist/chunk-63FEETIS.js +4048 -0
- package/dist/chunk-B43KP7XJ.js +884 -0
- package/dist/chunk-BMTJXWUV.js +655 -0
- package/dist/chunk-C3SVH4N7.js +882 -0
- package/dist/chunk-EVWR7Y47.js +874 -0
- package/dist/chunk-F6A5VWUC.js +1285 -0
- package/dist/chunk-FD7KW7GE.js +882 -0
- package/dist/chunk-GBQ6UD6I.js +655 -0
- package/dist/chunk-GMDD3M6U.js +5227 -0
- package/dist/chunk-JBHRXOXM.js +1058 -0
- package/dist/chunk-JFOO3EYO.js +1182 -0
- package/dist/chunk-JQ5H3WXL.js +1291 -0
- package/dist/chunk-JQD5NASE.js +234 -0
- package/dist/chunk-KRHJP5R7.js +592 -0
- package/dist/chunk-KWF6WVJE.js +962 -0
- package/dist/chunk-LHYQVN3H.js +1038 -0
- package/dist/chunk-M3TLQLGC.js +1032 -0
- package/dist/chunk-MVW4Q5OP.js +240 -0
- package/dist/chunk-NXCZSWLU.js +1294 -0
- package/dist/chunk-O25TNRO6.js +607 -0
- package/dist/chunk-PNECDA2I.js +884 -0
- package/dist/chunk-QIHWRLJR.js +962 -0
- package/dist/chunk-QW5YMQ7K.js +882 -0
- package/dist/chunk-R5U7XKVJ.js +16 -0
- package/dist/chunk-RP2MVQLR.js +962 -0
- package/dist/chunk-TP6RXGXA.js +1087 -0
- package/dist/chunk-TQQSTITZ.js +655 -0
- package/dist/chunk-X24GWXQV.js +1281 -0
- package/dist/components/index.d.ts +802 -0
- package/dist/components/index.js +149 -0
- package/dist/data/index.d.ts +2554 -0
- package/dist/data/index.js +51 -0
- package/dist/forms/index.d.ts +1596 -0
- package/dist/forms/index.js +464 -0
- package/dist/index-CQRFZntR.d.ts +867 -0
- package/dist/index.d.ts +579 -0
- package/dist/index.js +786 -0
- package/dist/interactive-D0JkWosD.d.ts +217 -0
- package/dist/keyboard/index.d.ts +2 -0
- package/dist/keyboard/index.js +43 -0
- package/dist/renderers/index.d.ts +546 -0
- package/dist/renderers/index.js +2157 -0
- package/dist/storybook/index.d.ts +396 -0
- package/dist/storybook/index.js +641 -0
- package/dist/theme/index.d.ts +1339 -0
- package/dist/theme/index.js +123 -0
- package/dist/types-Bxu5PAgA.d.ts +710 -0
- package/dist/types-CIlop5Ji.d.ts +701 -0
- package/dist/types-Ca8p_p5X.d.ts +710 -0
- package/package.json +90 -0
- package/src/__tests__/components/data/card.test.ts +458 -0
- package/src/__tests__/components/data/list.test.ts +473 -0
- package/src/__tests__/components/data/metrics.test.ts +541 -0
- package/src/__tests__/components/data/table.test.ts +448 -0
- package/src/__tests__/components/input/field.test.ts +555 -0
- package/src/__tests__/components/input/form.test.ts +870 -0
- package/src/__tests__/components/input/search.test.ts +1238 -0
- package/src/__tests__/components/input/select.test.ts +658 -0
- package/src/__tests__/components/navigation/breadcrumb.test.ts +923 -0
- package/src/__tests__/components/navigation/command-palette.test.ts +1095 -0
- package/src/__tests__/components/navigation/sidebar.test.ts +1018 -0
- package/src/__tests__/components/navigation/tabs.test.ts +995 -0
- package/src/__tests__/components.test.tsx +1197 -0
- package/src/__tests__/core/compiler.test.ts +986 -0
- package/src/__tests__/core/parser.test.ts +785 -0
- package/src/__tests__/core/tier-switcher.test.ts +1103 -0
- package/src/__tests__/core/types.test.ts +1398 -0
- package/src/__tests__/data/collections.test.ts +1337 -0
- package/src/__tests__/data/db.test.ts +1265 -0
- package/src/__tests__/data/reactive.test.ts +1010 -0
- package/src/__tests__/data/sync.test.ts +1614 -0
- package/src/__tests__/errors.test.ts +660 -0
- package/src/__tests__/forms/integration.test.ts +444 -0
- package/src/__tests__/integration.test.ts +905 -0
- package/src/__tests__/keyboard.test.ts +1791 -0
- package/src/__tests__/renderer.test.ts +489 -0
- package/src/__tests__/renderers/ansi-css.test.ts +948 -0
- package/src/__tests__/renderers/ansi.test.ts +1366 -0
- package/src/__tests__/renderers/ascii.test.ts +1360 -0
- package/src/__tests__/renderers/interactive.test.ts +2353 -0
- package/src/__tests__/renderers/markdown.test.ts +1483 -0
- package/src/__tests__/renderers/text.test.ts +1369 -0
- package/src/__tests__/renderers/unicode.test.ts +1307 -0
- package/src/__tests__/theme.test.ts +639 -0
- package/src/__tests__/utils/assertions.ts +685 -0
- package/src/__tests__/utils/index.ts +115 -0
- package/src/__tests__/utils/test-renderer.ts +381 -0
- package/src/__tests__/utils/utils.test.ts +560 -0
- package/src/components/containers/card.ts +56 -0
- package/src/components/containers/dialog.ts +53 -0
- package/src/components/containers/index.ts +9 -0
- package/src/components/containers/panel.ts +59 -0
- package/src/components/feedback/badge.ts +40 -0
- package/src/components/feedback/index.ts +8 -0
- package/src/components/feedback/spinner.ts +23 -0
- package/src/components/helpers.ts +81 -0
- package/src/components/index.ts +153 -0
- package/src/components/layout/breadcrumb.ts +31 -0
- package/src/components/layout/index.ts +10 -0
- package/src/components/layout/list.ts +29 -0
- package/src/components/layout/sidebar.ts +79 -0
- package/src/components/layout/table.ts +62 -0
- package/src/components/primitives/box.ts +95 -0
- package/src/components/primitives/button.ts +54 -0
- package/src/components/primitives/index.ts +11 -0
- package/src/components/primitives/input.ts +88 -0
- package/src/components/primitives/select.ts +97 -0
- package/src/components/primitives/text.ts +60 -0
- package/src/components/render.ts +155 -0
- package/src/components/templates/app.ts +43 -0
- package/src/components/templates/index.ts +8 -0
- package/src/components/templates/site.ts +54 -0
- package/src/components/types.ts +777 -0
- package/src/core/compiler.ts +718 -0
- package/src/core/parser.ts +127 -0
- package/src/core/tier-switcher.ts +607 -0
- package/src/core/types.ts +672 -0
- package/src/data/collection.ts +316 -0
- package/src/data/collections.ts +50 -0
- package/src/data/context.tsx +174 -0
- package/src/data/db.ts +127 -0
- package/src/data/hooks.ts +532 -0
- package/src/data/index.ts +138 -0
- package/src/data/reactive.ts +1225 -0
- package/src/data/saas-collections.ts +375 -0
- package/src/data/sync.ts +1213 -0
- package/src/data/types.ts +660 -0
- package/src/forms/converters.ts +512 -0
- package/src/forms/index.ts +133 -0
- package/src/forms/schemas.ts +403 -0
- package/src/forms/types.ts +476 -0
- package/src/index.ts +542 -0
- package/src/keyboard/focus.ts +748 -0
- package/src/keyboard/index.ts +96 -0
- package/src/keyboard/integration.ts +371 -0
- package/src/keyboard/manager.ts +377 -0
- package/src/keyboard/presets.ts +90 -0
- package/src/renderers/ansi-css.ts +576 -0
- package/src/renderers/ansi.ts +802 -0
- package/src/renderers/ascii.ts +680 -0
- package/src/renderers/breadcrumb.ts +480 -0
- package/src/renderers/command-palette.ts +802 -0
- package/src/renderers/components/field.ts +210 -0
- package/src/renderers/components/form.ts +327 -0
- package/src/renderers/components/index.ts +21 -0
- package/src/renderers/components/search.ts +449 -0
- package/src/renderers/components/select.ts +222 -0
- package/src/renderers/index.ts +101 -0
- package/src/renderers/interactive/component-handlers.ts +622 -0
- package/src/renderers/interactive/cursor-manager.ts +147 -0
- package/src/renderers/interactive/focus-manager.ts +279 -0
- package/src/renderers/interactive/index.ts +661 -0
- package/src/renderers/interactive/input-handler.ts +164 -0
- package/src/renderers/interactive/keyboard-handler.ts +212 -0
- package/src/renderers/interactive/mouse-handler.ts +167 -0
- package/src/renderers/interactive/state-manager.ts +109 -0
- package/src/renderers/interactive/types.ts +338 -0
- package/src/renderers/interactive-string.ts +299 -0
- package/src/renderers/interactive.ts +59 -0
- package/src/renderers/markdown.ts +950 -0
- package/src/renderers/sidebar.ts +549 -0
- package/src/renderers/tabs.ts +682 -0
- package/src/renderers/text.ts +791 -0
- package/src/renderers/unicode.ts +917 -0
- package/src/renderers/utils.ts +942 -0
- package/src/router/adapters.ts +383 -0
- package/src/router/types.ts +140 -0
- package/src/router/utils.ts +452 -0
- package/src/schemas.ts +205 -0
- package/src/storybook/index.ts +91 -0
- package/src/storybook/interactive-decorator.tsx +659 -0
- package/src/storybook/keyboard-simulator.ts +501 -0
- package/src/theme/ansi-codes.ts +80 -0
- package/src/theme/box-drawing.ts +132 -0
- package/src/theme/color-convert.ts +254 -0
- package/src/theme/color-support.ts +321 -0
- package/src/theme/index.ts +134 -0
- package/src/theme/strip-ansi.ts +50 -0
- package/src/theme/tailwind-map.ts +469 -0
- package/src/theme/text-styles.ts +206 -0
- package/src/theme/theme-system.ts +568 -0
- package/src/types.ts +103 -0
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ASCII Renderer - TDD GREEN Phase Implementation
|
|
3
|
+
*
|
|
4
|
+
* This file implements the ASCII renderer for the 6-tier Universal Terminal UI.
|
|
5
|
+
* All output uses pure ASCII characters (0x00-0x7F) for maximum compatibility.
|
|
6
|
+
*
|
|
7
|
+
* ASCII Tier Constraints:
|
|
8
|
+
* - Output must contain ONLY ASCII characters (0x00-0x7F)
|
|
9
|
+
* - No unicode box drawing characters (U+2500-U+257F)
|
|
10
|
+
* - No ANSI escape sequences
|
|
11
|
+
* - No emoji or special symbols
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { UINode, RenderContext, ThemeTokens } from '../core/types'
|
|
15
|
+
import {
|
|
16
|
+
sanitizeToASCII,
|
|
17
|
+
boxDrawingToASCII,
|
|
18
|
+
wrapText,
|
|
19
|
+
padText,
|
|
20
|
+
DEFAULT_THEME_TOKENS,
|
|
21
|
+
DEFAULT_RENDER_CONTEXT,
|
|
22
|
+
getASCIIBoxChars,
|
|
23
|
+
calculateColumnWidths,
|
|
24
|
+
buildTableSeparator,
|
|
25
|
+
buildTableRow,
|
|
26
|
+
} from './utils'
|
|
27
|
+
|
|
28
|
+
// Default context values
|
|
29
|
+
const DEFAULT_WIDTH = DEFAULT_RENDER_CONTEXT.width
|
|
30
|
+
const DEFAULT_HEIGHT = DEFAULT_RENDER_CONTEXT.height
|
|
31
|
+
|
|
32
|
+
const DEFAULT_THEME: ThemeTokens = { ...DEFAULT_THEME_TOKENS }
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Creates a default render context
|
|
36
|
+
*/
|
|
37
|
+
function createDefaultContext(): RenderContext {
|
|
38
|
+
return {
|
|
39
|
+
tier: 'ascii',
|
|
40
|
+
width: DEFAULT_WIDTH,
|
|
41
|
+
height: DEFAULT_HEIGHT,
|
|
42
|
+
depth: 0,
|
|
43
|
+
theme: DEFAULT_THEME,
|
|
44
|
+
interactive: false,
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// sanitizeToASCII, boxDrawingToASCII, and wrapText are now imported from ./utils
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Renders a UINode tree to pure ASCII output for low-capability terminals.
|
|
52
|
+
*/
|
|
53
|
+
export function renderASCII(node: UINode, context?: RenderContext): string {
|
|
54
|
+
const ctx = context ?? createDefaultContext()
|
|
55
|
+
return renderNode(node, ctx)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Main node renderer - dispatches to type-specific renderers
|
|
60
|
+
*/
|
|
61
|
+
function renderNode(node: UINode, ctx: RenderContext): string {
|
|
62
|
+
switch (node.type) {
|
|
63
|
+
case 'text':
|
|
64
|
+
return renderText(node, ctx)
|
|
65
|
+
case 'box':
|
|
66
|
+
return renderBox(node, ctx)
|
|
67
|
+
case 'table':
|
|
68
|
+
return renderTable(node, ctx)
|
|
69
|
+
case 'list':
|
|
70
|
+
return renderList(node, ctx)
|
|
71
|
+
case 'progress':
|
|
72
|
+
return renderProgress(node, ctx)
|
|
73
|
+
case 'spinner':
|
|
74
|
+
return renderSpinner(node, ctx)
|
|
75
|
+
case 'panel':
|
|
76
|
+
return renderPanel(node, ctx)
|
|
77
|
+
case 'card':
|
|
78
|
+
return renderCard(node, ctx)
|
|
79
|
+
case 'badge':
|
|
80
|
+
return renderBadge(node, ctx)
|
|
81
|
+
case 'button':
|
|
82
|
+
return renderButton(node, ctx)
|
|
83
|
+
case 'input':
|
|
84
|
+
return renderInput(node, ctx)
|
|
85
|
+
case 'select':
|
|
86
|
+
return renderSelect(node, ctx)
|
|
87
|
+
case 'dialog':
|
|
88
|
+
return renderDialog(node, ctx)
|
|
89
|
+
case 'breadcrumb':
|
|
90
|
+
return renderBreadcrumb(node, ctx)
|
|
91
|
+
case 'sidebar':
|
|
92
|
+
return renderSidebar(node, ctx)
|
|
93
|
+
case 'metrics':
|
|
94
|
+
return renderMetrics(node, ctx)
|
|
95
|
+
case 'metric':
|
|
96
|
+
return renderSingleMetric(node, ctx)
|
|
97
|
+
default:
|
|
98
|
+
// Unknown type - render children if present
|
|
99
|
+
return renderChildren(node, ctx)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Renders children of a node
|
|
105
|
+
*/
|
|
106
|
+
function renderChildren(node: UINode, ctx: RenderContext): string {
|
|
107
|
+
if (!node.children || node.children.length === 0) {
|
|
108
|
+
return ''
|
|
109
|
+
}
|
|
110
|
+
if (typeof node.children === 'string') {
|
|
111
|
+
return node.children
|
|
112
|
+
}
|
|
113
|
+
return node.children.map((child) => renderNode(child, ctx)).join('\n')
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Renders a text node
|
|
118
|
+
*/
|
|
119
|
+
function renderText(node: UINode, ctx: RenderContext): string {
|
|
120
|
+
const content = String(node.props?.content ?? '')
|
|
121
|
+
const sanitized = sanitizeToASCII(content)
|
|
122
|
+
const width = ctx.width > 0 ? ctx.width : DEFAULT_WIDTH
|
|
123
|
+
const lines = wrapText(sanitized, width)
|
|
124
|
+
return lines.join('\n')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Renders a box with ASCII borders
|
|
129
|
+
*/
|
|
130
|
+
function renderBox(node: UINode, ctx: RenderContext): string {
|
|
131
|
+
const border = node.props?.border as string
|
|
132
|
+
const explicitWidth = node.props?.width as number | undefined
|
|
133
|
+
const padding = (node.props?.padding as number) ?? 0
|
|
134
|
+
|
|
135
|
+
// If border is 'none', just render content
|
|
136
|
+
if (border === 'none') {
|
|
137
|
+
return renderChildren(node, ctx)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Get border characters from shared utils
|
|
141
|
+
const chars = getASCIIBoxChars(border)
|
|
142
|
+
const { horizontal, vertical } = chars
|
|
143
|
+
const corner = chars.topLeft // All corners are '+' in ASCII
|
|
144
|
+
|
|
145
|
+
// Calculate available inner width for children
|
|
146
|
+
// We need at least some space for the content
|
|
147
|
+
const availableWidth = explicitWidth ?? ctx.width
|
|
148
|
+
const neededForBorders = 2 + padding * 2 // | on each side + padding
|
|
149
|
+
const childWidth = Math.max(1, availableWidth - neededForBorders)
|
|
150
|
+
|
|
151
|
+
// Render children first to determine content
|
|
152
|
+
const childCtx: RenderContext = {
|
|
153
|
+
...ctx,
|
|
154
|
+
depth: ctx.depth + 1,
|
|
155
|
+
width: childWidth,
|
|
156
|
+
}
|
|
157
|
+
const childContent = renderChildren(node, childCtx)
|
|
158
|
+
const contentLines = childContent.split('\n').filter((line, idx, arr) => {
|
|
159
|
+
// Keep all lines, including empty ones in the middle
|
|
160
|
+
return line.length > 0 || (idx > 0 && idx < arr.length - 1)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
// Calculate box width based on actual content (not wrapped to child width)
|
|
164
|
+
let contentWidth = 0
|
|
165
|
+
for (const line of contentLines) {
|
|
166
|
+
contentWidth = Math.max(contentWidth, line.length)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// If explicit width is set, use it (minus borders)
|
|
170
|
+
let innerWidth: number
|
|
171
|
+
if (explicitWidth !== undefined) {
|
|
172
|
+
innerWidth = explicitWidth - 2 // Account for | on each side
|
|
173
|
+
} else {
|
|
174
|
+
// Use content width, but ensure enough space for content + padding
|
|
175
|
+
innerWidth = Math.max(contentWidth + padding * 2, 1)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Respect context width, but ensure minimum space for content
|
|
179
|
+
if (ctx.width > 0 && innerWidth > ctx.width - 2) {
|
|
180
|
+
innerWidth = ctx.width - 2
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Build the box
|
|
184
|
+
const lines: string[] = []
|
|
185
|
+
const topBottom = corner + horizontal.repeat(Math.max(innerWidth, 1)) + corner
|
|
186
|
+
lines.push(topBottom)
|
|
187
|
+
|
|
188
|
+
// Add padding lines at top
|
|
189
|
+
for (let i = 0; i < padding; i++) {
|
|
190
|
+
lines.push(vertical + ' '.repeat(innerWidth) + vertical)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Add content lines (or at least one empty line)
|
|
194
|
+
const linesToRender = contentLines.length > 0 ? contentLines : ['']
|
|
195
|
+
for (const line of linesToRender) {
|
|
196
|
+
const paddedLine = ' '.repeat(padding) + line
|
|
197
|
+
// Ensure the line fits within innerWidth
|
|
198
|
+
const truncated = paddedLine.length > innerWidth ? paddedLine.slice(0, innerWidth) : paddedLine
|
|
199
|
+
const padded = truncated.padEnd(innerWidth)
|
|
200
|
+
lines.push(vertical + padded + vertical)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Add padding lines at bottom
|
|
204
|
+
for (let i = 0; i < padding; i++) {
|
|
205
|
+
lines.push(vertical + ' '.repeat(innerWidth) + vertical)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
lines.push(topBottom)
|
|
209
|
+
|
|
210
|
+
return lines.join('\n')
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Renders a table with ASCII borders
|
|
215
|
+
*/
|
|
216
|
+
function renderTable(node: UINode, ctx: RenderContext): string {
|
|
217
|
+
// Support new TDD format (columns + node.data) and legacy format (headers + rows)
|
|
218
|
+
const columns = node.props?.columns as Array<{ key: string; header: string }> | undefined
|
|
219
|
+
const nodeData = (node.data as Array<Record<string, unknown>> | undefined) ??
|
|
220
|
+
(node.props?.data as Array<Record<string, unknown>> | undefined)
|
|
221
|
+
|
|
222
|
+
let headers: string[] = []
|
|
223
|
+
let rows: string[][] = []
|
|
224
|
+
|
|
225
|
+
if (columns && columns.length > 0) {
|
|
226
|
+
// New TDD format
|
|
227
|
+
headers = columns.map((col) => col.header)
|
|
228
|
+
if (nodeData && nodeData.length > 0) {
|
|
229
|
+
rows = nodeData.map((row) =>
|
|
230
|
+
columns.map((col) => {
|
|
231
|
+
const val = row[col.key]
|
|
232
|
+
return val != null ? String(val) : ''
|
|
233
|
+
})
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
} else {
|
|
237
|
+
// Legacy format
|
|
238
|
+
headers = (node.props?.headers as string[]) ?? []
|
|
239
|
+
rows = (node.props?.rows as string[][]) ?? []
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Handle empty table
|
|
243
|
+
if (headers.length === 0 && rows.length === 0) {
|
|
244
|
+
return ''
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Calculate column widths using shared utility
|
|
248
|
+
const maxTableWidth = ctx.width > 0 ? ctx.width : DEFAULT_WIDTH
|
|
249
|
+
const colWidths = calculateColumnWidths(headers, rows, maxTableWidth)
|
|
250
|
+
|
|
251
|
+
// Table separator characters (ASCII)
|
|
252
|
+
const separatorChars = { left: '+', middle: '+', right: '+', horizontal: '-' }
|
|
253
|
+
|
|
254
|
+
const lines: string[] = []
|
|
255
|
+
lines.push(buildTableSeparator(colWidths, separatorChars))
|
|
256
|
+
|
|
257
|
+
// Headers
|
|
258
|
+
if (headers.length > 0) {
|
|
259
|
+
lines.push(buildTableRow(headers, colWidths, '|'))
|
|
260
|
+
lines.push(buildTableSeparator(colWidths, separatorChars))
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Data rows
|
|
264
|
+
for (const row of rows) {
|
|
265
|
+
// Handle multi-line cells - for now just take first line
|
|
266
|
+
const processedRow = row.map((cell) => {
|
|
267
|
+
const firstLine = cell.split('\n')[0] ?? ''
|
|
268
|
+
return sanitizeToASCII(firstLine)
|
|
269
|
+
})
|
|
270
|
+
lines.push(buildTableRow(processedRow, colWidths, '|'))
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (rows.length > 0 || headers.length > 0) {
|
|
274
|
+
lines.push(buildTableSeparator(colWidths, separatorChars))
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return lines.join('\n')
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Renders a list with ASCII bullets
|
|
282
|
+
*/
|
|
283
|
+
function renderList(node: UINode, ctx: RenderContext): string {
|
|
284
|
+
const items = (node.props?.items as Array<string | { text: string; checked?: boolean }>) ?? []
|
|
285
|
+
const ordered = (node.props?.ordered as boolean) ?? false
|
|
286
|
+
const numbered = (node.props?.numbered as boolean) ?? false
|
|
287
|
+
const taskList = (node.props?.taskList as boolean) ?? false
|
|
288
|
+
const bullet = (node.props?.bullet as string) ?? '*'
|
|
289
|
+
|
|
290
|
+
const lines: string[] = []
|
|
291
|
+
|
|
292
|
+
// Determine indent based on depth
|
|
293
|
+
const baseIndent = ctx.depth * 2
|
|
294
|
+
const indent = ' '.repeat(baseIndent)
|
|
295
|
+
|
|
296
|
+
// Handle items prop
|
|
297
|
+
if (items.length > 0) {
|
|
298
|
+
const maxNum = items.length
|
|
299
|
+
const numWidth = String(maxNum).length
|
|
300
|
+
|
|
301
|
+
for (let i = 0; i < items.length; i++) {
|
|
302
|
+
const item = items[i]
|
|
303
|
+
let text: string
|
|
304
|
+
let prefix: string
|
|
305
|
+
|
|
306
|
+
if (typeof item === 'string') {
|
|
307
|
+
text = sanitizeToASCII(item)
|
|
308
|
+
if (ordered || numbered) {
|
|
309
|
+
const num = String(i + 1).padStart(numWidth)
|
|
310
|
+
prefix = `${num}. `
|
|
311
|
+
} else {
|
|
312
|
+
prefix = `${bullet} `
|
|
313
|
+
}
|
|
314
|
+
} else {
|
|
315
|
+
text = sanitizeToASCII(item.text ?? '')
|
|
316
|
+
if (taskList && 'checked' in item) {
|
|
317
|
+
prefix = item.checked ? '[x] ' : '[ ] '
|
|
318
|
+
} else if (ordered || numbered) {
|
|
319
|
+
const num = String(i + 1).padStart(numWidth)
|
|
320
|
+
prefix = `${num}. `
|
|
321
|
+
} else {
|
|
322
|
+
prefix = `${bullet} `
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
lines.push(`${indent}${prefix}${text}`)
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Handle list-item children (TDD test format)
|
|
331
|
+
if (node.children && Array.isArray(node.children) && node.children.length > 0) {
|
|
332
|
+
const nestedCtx: RenderContext = {
|
|
333
|
+
...ctx,
|
|
334
|
+
depth: ctx.depth + 1,
|
|
335
|
+
}
|
|
336
|
+
node.children.forEach((child: UINode, index: number) => {
|
|
337
|
+
if (child.type === 'list-item') {
|
|
338
|
+
const content = sanitizeToASCII(String(child.props?.content ?? ''))
|
|
339
|
+
const prefix = (ordered || numbered) ? `${index + 1}. ` : `${bullet} `
|
|
340
|
+
lines.push(`${indent}${prefix}${content}`)
|
|
341
|
+
|
|
342
|
+
// Handle nested children of list-item
|
|
343
|
+
if (child.children && Array.isArray(child.children) && child.children.length > 0) {
|
|
344
|
+
child.children.forEach((nestedChild: UINode) => {
|
|
345
|
+
const nestedOutput = renderNode(nestedChild, nestedCtx)
|
|
346
|
+
if (nestedOutput) {
|
|
347
|
+
lines.push(nestedOutput)
|
|
348
|
+
}
|
|
349
|
+
})
|
|
350
|
+
}
|
|
351
|
+
} else {
|
|
352
|
+
const nestedContent = renderNode(child, nestedCtx)
|
|
353
|
+
if (nestedContent.length > 0) {
|
|
354
|
+
lines.push(nestedContent)
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
})
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return lines.join('\n')
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Renders a progress bar
|
|
365
|
+
*/
|
|
366
|
+
function renderProgress(node: UINode, ctx: RenderContext): string {
|
|
367
|
+
const value = Math.max(0, Math.min(100, Number(node.props?.value) ?? 0))
|
|
368
|
+
const barWidth = (node.props?.width as number) ?? 20
|
|
369
|
+
const showLabel = node.props?.showLabel as boolean
|
|
370
|
+
const label = node.props?.label as string | undefined
|
|
371
|
+
|
|
372
|
+
// Inner width (excluding brackets)
|
|
373
|
+
const innerWidth = Math.max(1, barWidth - 2)
|
|
374
|
+
const filledCount = Math.round((value / 100) * innerWidth)
|
|
375
|
+
const emptyCount = innerWidth - filledCount
|
|
376
|
+
|
|
377
|
+
const bar = '[' + '='.repeat(filledCount) + ' '.repeat(emptyCount) + ']'
|
|
378
|
+
|
|
379
|
+
let result = bar
|
|
380
|
+
if (showLabel) {
|
|
381
|
+
result += ` ${Math.round(value)}%`
|
|
382
|
+
}
|
|
383
|
+
if (label) {
|
|
384
|
+
result += ` ${label}`
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return result
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Renders a spinner (static frame for ASCII output)
|
|
392
|
+
*/
|
|
393
|
+
function renderSpinner(node: UINode, ctx: RenderContext): string {
|
|
394
|
+
const label = node.props?.label as string | undefined
|
|
395
|
+
// ASCII spinner frames: | / - \
|
|
396
|
+
const frame = '-' // Static frame for non-animated output
|
|
397
|
+
|
|
398
|
+
if (label) {
|
|
399
|
+
return `${frame} ${label}`
|
|
400
|
+
}
|
|
401
|
+
return frame
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Renders a panel with title
|
|
406
|
+
*/
|
|
407
|
+
function renderPanel(node: UINode, ctx: RenderContext): string {
|
|
408
|
+
const title = String(node.props?.title ?? '')
|
|
409
|
+
|
|
410
|
+
// Render children
|
|
411
|
+
const childCtx: RenderContext = {
|
|
412
|
+
...ctx,
|
|
413
|
+
depth: ctx.depth + 1,
|
|
414
|
+
width: Math.max(1, ctx.width - 4),
|
|
415
|
+
}
|
|
416
|
+
const childContent = renderChildren(node, childCtx)
|
|
417
|
+
const contentLines = childContent.split('\n')
|
|
418
|
+
|
|
419
|
+
// Calculate width
|
|
420
|
+
let contentWidth = title.length
|
|
421
|
+
for (const line of contentLines) {
|
|
422
|
+
contentWidth = Math.max(contentWidth, line.length)
|
|
423
|
+
}
|
|
424
|
+
const innerWidth = Math.min(contentWidth + 2, ctx.width > 0 ? ctx.width - 2 : DEFAULT_WIDTH - 2)
|
|
425
|
+
|
|
426
|
+
// Build panel
|
|
427
|
+
const lines: string[] = []
|
|
428
|
+
const topLine = '+' + '-'.repeat(innerWidth) + '+'
|
|
429
|
+
lines.push(topLine)
|
|
430
|
+
|
|
431
|
+
// Title line
|
|
432
|
+
if (title) {
|
|
433
|
+
const titlePadded = (' ' + title).padEnd(innerWidth)
|
|
434
|
+
lines.push('|' + titlePadded + '|')
|
|
435
|
+
lines.push('+' + '-'.repeat(innerWidth) + '+')
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Content
|
|
439
|
+
for (const line of contentLines) {
|
|
440
|
+
const padded = (' ' + line).padEnd(innerWidth)
|
|
441
|
+
lines.push('|' + padded + '|')
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
lines.push(topLine)
|
|
445
|
+
|
|
446
|
+
return lines.join('\n')
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Renders a card
|
|
451
|
+
*/
|
|
452
|
+
function renderCard(node: UINode, ctx: RenderContext): string {
|
|
453
|
+
const title = node.props?.title as string | undefined
|
|
454
|
+
const subtitle = node.props?.subtitle as string | undefined
|
|
455
|
+
const badge = node.props?.badge as { content: string; variant?: string } | undefined
|
|
456
|
+
const titleAction = node.props?.titleAction as { label: string; action?: string } | undefined
|
|
457
|
+
const pairs = node.props?.pairs as Array<{ key: string; value: unknown }> | undefined
|
|
458
|
+
const actions = node.props?.actions as Array<{ label: string; action?: string }> | undefined
|
|
459
|
+
const border = node.props?.border as boolean | undefined
|
|
460
|
+
|
|
461
|
+
const contentLines: string[] = []
|
|
462
|
+
|
|
463
|
+
// Title section
|
|
464
|
+
if (title) {
|
|
465
|
+
let titleLine = title
|
|
466
|
+
if (badge) {
|
|
467
|
+
titleLine += ` [${badge.content}]`
|
|
468
|
+
}
|
|
469
|
+
if (titleAction) {
|
|
470
|
+
titleLine += ` | ${titleAction.label}`
|
|
471
|
+
}
|
|
472
|
+
contentLines.push(titleLine)
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (subtitle) {
|
|
476
|
+
contentLines.push(subtitle)
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Key-value pairs
|
|
480
|
+
if (pairs && pairs.length > 0) {
|
|
481
|
+
if (contentLines.length > 0) contentLines.push('')
|
|
482
|
+
for (const pair of pairs) {
|
|
483
|
+
const val = pair.value != null ? String(pair.value) : ''
|
|
484
|
+
contentLines.push(`${pair.key}: ${val}`)
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Children content
|
|
489
|
+
if (node.children && (typeof node.children === 'string' || node.children.length > 0)) {
|
|
490
|
+
if (contentLines.length > 0) contentLines.push('')
|
|
491
|
+
const childCtx: RenderContext = { ...ctx, depth: ctx.depth + 1 }
|
|
492
|
+
const childContent = renderChildren(node, childCtx)
|
|
493
|
+
if (childContent) {
|
|
494
|
+
contentLines.push(childContent)
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Actions
|
|
499
|
+
if (actions && actions.length > 0) {
|
|
500
|
+
if (contentLines.length > 0) contentLines.push('')
|
|
501
|
+
const actionLabels = actions.map((a) => `[ ${a.label} ]`).join(' ')
|
|
502
|
+
contentLines.push(actionLabels)
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// If border is true or not specified with title, wrap in box
|
|
506
|
+
if (border === true) {
|
|
507
|
+
const boxNode: UINode = {
|
|
508
|
+
type: 'box',
|
|
509
|
+
props: { border: 'single' },
|
|
510
|
+
children: [{ type: 'text', props: { content: contentLines.join('\n') } }],
|
|
511
|
+
}
|
|
512
|
+
return renderBox(boxNode, ctx)
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return contentLines.join('\n')
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Renders a badge
|
|
520
|
+
*/
|
|
521
|
+
function renderBadge(node: UINode, ctx: RenderContext): string {
|
|
522
|
+
const label = String(node.props?.label ?? '')
|
|
523
|
+
return `[${label}]`
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Renders a button
|
|
528
|
+
*/
|
|
529
|
+
function renderButton(node: UINode, ctx: RenderContext): string {
|
|
530
|
+
const label = String(node.props?.label ?? '')
|
|
531
|
+
return `[ ${label} ]`
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Renders an input field
|
|
536
|
+
*/
|
|
537
|
+
function renderInput(node: UINode, ctx: RenderContext): string {
|
|
538
|
+
const value = String(node.props?.value ?? '')
|
|
539
|
+
const placeholder = String(node.props?.placeholder ?? '')
|
|
540
|
+
const display = value || placeholder
|
|
541
|
+
|
|
542
|
+
return `[${display}]`
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Renders a select dropdown
|
|
547
|
+
*/
|
|
548
|
+
function renderSelect(node: UINode, ctx: RenderContext): string {
|
|
549
|
+
const options = (node.props?.options as Array<{ label: string; value: string }>) ?? []
|
|
550
|
+
const selectedValue = node.props?.value as string
|
|
551
|
+
|
|
552
|
+
const lines: string[] = []
|
|
553
|
+
for (const opt of options) {
|
|
554
|
+
const marker = opt.value === selectedValue ? '>' : ' '
|
|
555
|
+
lines.push(`${marker} ${opt.label}`)
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return lines.join('\n')
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Renders a dialog
|
|
563
|
+
*/
|
|
564
|
+
function renderDialog(node: UINode, ctx: RenderContext): string {
|
|
565
|
+
const title = String(node.props?.title ?? '')
|
|
566
|
+
|
|
567
|
+
// Render children
|
|
568
|
+
const childCtx: RenderContext = {
|
|
569
|
+
...ctx,
|
|
570
|
+
depth: ctx.depth + 1,
|
|
571
|
+
width: Math.max(1, ctx.width - 4),
|
|
572
|
+
}
|
|
573
|
+
const childContent = renderChildren(node, childCtx)
|
|
574
|
+
const contentLines = childContent.split('\n')
|
|
575
|
+
|
|
576
|
+
// Calculate width
|
|
577
|
+
let contentWidth = title.length
|
|
578
|
+
for (const line of contentLines) {
|
|
579
|
+
contentWidth = Math.max(contentWidth, line.length)
|
|
580
|
+
}
|
|
581
|
+
const innerWidth = Math.min(contentWidth + 2, ctx.width > 0 ? ctx.width - 2 : DEFAULT_WIDTH - 2)
|
|
582
|
+
|
|
583
|
+
// Build dialog (similar to panel)
|
|
584
|
+
const lines: string[] = []
|
|
585
|
+
const topLine = '+' + '-'.repeat(innerWidth) + '+'
|
|
586
|
+
lines.push(topLine)
|
|
587
|
+
|
|
588
|
+
// Title line
|
|
589
|
+
if (title) {
|
|
590
|
+
const titlePadded = (' ' + title).padEnd(innerWidth)
|
|
591
|
+
lines.push('|' + titlePadded + '|')
|
|
592
|
+
lines.push('+' + '-'.repeat(innerWidth) + '+')
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Content
|
|
596
|
+
for (const line of contentLines) {
|
|
597
|
+
const padded = (' ' + line).padEnd(innerWidth)
|
|
598
|
+
lines.push('|' + padded + '|')
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
lines.push(topLine)
|
|
602
|
+
|
|
603
|
+
return lines.join('\n')
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Renders breadcrumbs
|
|
608
|
+
*/
|
|
609
|
+
function renderBreadcrumb(node: UINode, ctx: RenderContext): string {
|
|
610
|
+
const items = (node.props?.items as Array<{ label: string; href?: string }>) ?? []
|
|
611
|
+
const labels = items.map((item) => item.label)
|
|
612
|
+
return labels.join(' > ')
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Renders a sidebar
|
|
617
|
+
*/
|
|
618
|
+
function renderSidebar(node: UINode, ctx: RenderContext): string {
|
|
619
|
+
const items = (node.props?.items as Array<{ label: string; icon?: string }>) ?? []
|
|
620
|
+
const lines: string[] = []
|
|
621
|
+
|
|
622
|
+
for (const item of items) {
|
|
623
|
+
lines.push(`* ${item.label}`)
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
return lines.join('\n')
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Renders metrics (multiple metrics)
|
|
631
|
+
*/
|
|
632
|
+
function renderMetrics(node: UINode, ctx: RenderContext): string {
|
|
633
|
+
const metrics = node.props?.metrics as Array<{
|
|
634
|
+
label: string
|
|
635
|
+
value: unknown
|
|
636
|
+
format?: string
|
|
637
|
+
unit?: string
|
|
638
|
+
trend?: string
|
|
639
|
+
}> | undefined
|
|
640
|
+
|
|
641
|
+
if (!metrics || metrics.length === 0) return ''
|
|
642
|
+
|
|
643
|
+
const lines: string[] = []
|
|
644
|
+
for (const m of metrics) {
|
|
645
|
+
const val = m.value != null ? String(m.value) : ''
|
|
646
|
+
let formatted = val
|
|
647
|
+
if (m.format === 'percentage' && !val.includes('%')) {
|
|
648
|
+
formatted = `${val}%`
|
|
649
|
+
}
|
|
650
|
+
if (m.unit) {
|
|
651
|
+
formatted = `${formatted} ${m.unit}`
|
|
652
|
+
}
|
|
653
|
+
lines.push(`${m.label}: ${formatted}`)
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
return lines.join('\n')
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Renders a single metric
|
|
661
|
+
*/
|
|
662
|
+
function renderSingleMetric(node: UINode, ctx: RenderContext): string {
|
|
663
|
+
const label = node.props?.label as string | undefined
|
|
664
|
+
const value = node.props?.value
|
|
665
|
+
const format = node.props?.format as string | undefined
|
|
666
|
+
const unit = node.props?.unit as string | undefined
|
|
667
|
+
|
|
668
|
+
if (!label) return ''
|
|
669
|
+
|
|
670
|
+
const val = value != null ? String(value) : ''
|
|
671
|
+
let formatted = val
|
|
672
|
+
if (format === 'percentage' && !val.includes('%')) {
|
|
673
|
+
formatted = `${val}%`
|
|
674
|
+
}
|
|
675
|
+
if (unit) {
|
|
676
|
+
formatted = `${formatted} ${unit}`
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
return `${label}: ${formatted}`
|
|
680
|
+
}
|