@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.
Files changed (191) hide show
  1. package/README.md +571 -0
  2. package/dist/ansi-css-Sk5mWtdK.d.ts +119 -0
  3. package/dist/ansi-css-V6JIHGsM.d.ts +119 -0
  4. package/dist/ansi-css-_3eSEU9d.d.ts +119 -0
  5. package/dist/chunk-3EFDH7PK.js +5235 -0
  6. package/dist/chunk-3RG5ZIWI.js +10 -0
  7. package/dist/chunk-3X5IR6WE.js +884 -0
  8. package/dist/chunk-4FV5ZDCE.js +5236 -0
  9. package/dist/chunk-4OVMSF2J.js +243 -0
  10. package/dist/chunk-63FEETIS.js +4048 -0
  11. package/dist/chunk-B43KP7XJ.js +884 -0
  12. package/dist/chunk-BMTJXWUV.js +655 -0
  13. package/dist/chunk-C3SVH4N7.js +882 -0
  14. package/dist/chunk-EVWR7Y47.js +874 -0
  15. package/dist/chunk-F6A5VWUC.js +1285 -0
  16. package/dist/chunk-FD7KW7GE.js +882 -0
  17. package/dist/chunk-GBQ6UD6I.js +655 -0
  18. package/dist/chunk-GMDD3M6U.js +5227 -0
  19. package/dist/chunk-JBHRXOXM.js +1058 -0
  20. package/dist/chunk-JFOO3EYO.js +1182 -0
  21. package/dist/chunk-JQ5H3WXL.js +1291 -0
  22. package/dist/chunk-JQD5NASE.js +234 -0
  23. package/dist/chunk-KRHJP5R7.js +592 -0
  24. package/dist/chunk-KWF6WVJE.js +962 -0
  25. package/dist/chunk-LHYQVN3H.js +1038 -0
  26. package/dist/chunk-M3TLQLGC.js +1032 -0
  27. package/dist/chunk-MVW4Q5OP.js +240 -0
  28. package/dist/chunk-NXCZSWLU.js +1294 -0
  29. package/dist/chunk-O25TNRO6.js +607 -0
  30. package/dist/chunk-PNECDA2I.js +884 -0
  31. package/dist/chunk-QIHWRLJR.js +962 -0
  32. package/dist/chunk-QW5YMQ7K.js +882 -0
  33. package/dist/chunk-R5U7XKVJ.js +16 -0
  34. package/dist/chunk-RP2MVQLR.js +962 -0
  35. package/dist/chunk-TP6RXGXA.js +1087 -0
  36. package/dist/chunk-TQQSTITZ.js +655 -0
  37. package/dist/chunk-X24GWXQV.js +1281 -0
  38. package/dist/components/index.d.ts +802 -0
  39. package/dist/components/index.js +149 -0
  40. package/dist/data/index.d.ts +2554 -0
  41. package/dist/data/index.js +51 -0
  42. package/dist/forms/index.d.ts +1596 -0
  43. package/dist/forms/index.js +464 -0
  44. package/dist/index-CQRFZntR.d.ts +867 -0
  45. package/dist/index.d.ts +579 -0
  46. package/dist/index.js +786 -0
  47. package/dist/interactive-D0JkWosD.d.ts +217 -0
  48. package/dist/keyboard/index.d.ts +2 -0
  49. package/dist/keyboard/index.js +43 -0
  50. package/dist/renderers/index.d.ts +546 -0
  51. package/dist/renderers/index.js +2157 -0
  52. package/dist/storybook/index.d.ts +396 -0
  53. package/dist/storybook/index.js +641 -0
  54. package/dist/theme/index.d.ts +1339 -0
  55. package/dist/theme/index.js +123 -0
  56. package/dist/types-Bxu5PAgA.d.ts +710 -0
  57. package/dist/types-CIlop5Ji.d.ts +701 -0
  58. package/dist/types-Ca8p_p5X.d.ts +710 -0
  59. package/package.json +90 -0
  60. package/src/__tests__/components/data/card.test.ts +458 -0
  61. package/src/__tests__/components/data/list.test.ts +473 -0
  62. package/src/__tests__/components/data/metrics.test.ts +541 -0
  63. package/src/__tests__/components/data/table.test.ts +448 -0
  64. package/src/__tests__/components/input/field.test.ts +555 -0
  65. package/src/__tests__/components/input/form.test.ts +870 -0
  66. package/src/__tests__/components/input/search.test.ts +1238 -0
  67. package/src/__tests__/components/input/select.test.ts +658 -0
  68. package/src/__tests__/components/navigation/breadcrumb.test.ts +923 -0
  69. package/src/__tests__/components/navigation/command-palette.test.ts +1095 -0
  70. package/src/__tests__/components/navigation/sidebar.test.ts +1018 -0
  71. package/src/__tests__/components/navigation/tabs.test.ts +995 -0
  72. package/src/__tests__/components.test.tsx +1197 -0
  73. package/src/__tests__/core/compiler.test.ts +986 -0
  74. package/src/__tests__/core/parser.test.ts +785 -0
  75. package/src/__tests__/core/tier-switcher.test.ts +1103 -0
  76. package/src/__tests__/core/types.test.ts +1398 -0
  77. package/src/__tests__/data/collections.test.ts +1337 -0
  78. package/src/__tests__/data/db.test.ts +1265 -0
  79. package/src/__tests__/data/reactive.test.ts +1010 -0
  80. package/src/__tests__/data/sync.test.ts +1614 -0
  81. package/src/__tests__/errors.test.ts +660 -0
  82. package/src/__tests__/forms/integration.test.ts +444 -0
  83. package/src/__tests__/integration.test.ts +905 -0
  84. package/src/__tests__/keyboard.test.ts +1791 -0
  85. package/src/__tests__/renderer.test.ts +489 -0
  86. package/src/__tests__/renderers/ansi-css.test.ts +948 -0
  87. package/src/__tests__/renderers/ansi.test.ts +1366 -0
  88. package/src/__tests__/renderers/ascii.test.ts +1360 -0
  89. package/src/__tests__/renderers/interactive.test.ts +2353 -0
  90. package/src/__tests__/renderers/markdown.test.ts +1483 -0
  91. package/src/__tests__/renderers/text.test.ts +1369 -0
  92. package/src/__tests__/renderers/unicode.test.ts +1307 -0
  93. package/src/__tests__/theme.test.ts +639 -0
  94. package/src/__tests__/utils/assertions.ts +685 -0
  95. package/src/__tests__/utils/index.ts +115 -0
  96. package/src/__tests__/utils/test-renderer.ts +381 -0
  97. package/src/__tests__/utils/utils.test.ts +560 -0
  98. package/src/components/containers/card.ts +56 -0
  99. package/src/components/containers/dialog.ts +53 -0
  100. package/src/components/containers/index.ts +9 -0
  101. package/src/components/containers/panel.ts +59 -0
  102. package/src/components/feedback/badge.ts +40 -0
  103. package/src/components/feedback/index.ts +8 -0
  104. package/src/components/feedback/spinner.ts +23 -0
  105. package/src/components/helpers.ts +81 -0
  106. package/src/components/index.ts +153 -0
  107. package/src/components/layout/breadcrumb.ts +31 -0
  108. package/src/components/layout/index.ts +10 -0
  109. package/src/components/layout/list.ts +29 -0
  110. package/src/components/layout/sidebar.ts +79 -0
  111. package/src/components/layout/table.ts +62 -0
  112. package/src/components/primitives/box.ts +95 -0
  113. package/src/components/primitives/button.ts +54 -0
  114. package/src/components/primitives/index.ts +11 -0
  115. package/src/components/primitives/input.ts +88 -0
  116. package/src/components/primitives/select.ts +97 -0
  117. package/src/components/primitives/text.ts +60 -0
  118. package/src/components/render.ts +155 -0
  119. package/src/components/templates/app.ts +43 -0
  120. package/src/components/templates/index.ts +8 -0
  121. package/src/components/templates/site.ts +54 -0
  122. package/src/components/types.ts +777 -0
  123. package/src/core/compiler.ts +718 -0
  124. package/src/core/parser.ts +127 -0
  125. package/src/core/tier-switcher.ts +607 -0
  126. package/src/core/types.ts +672 -0
  127. package/src/data/collection.ts +316 -0
  128. package/src/data/collections.ts +50 -0
  129. package/src/data/context.tsx +174 -0
  130. package/src/data/db.ts +127 -0
  131. package/src/data/hooks.ts +532 -0
  132. package/src/data/index.ts +138 -0
  133. package/src/data/reactive.ts +1225 -0
  134. package/src/data/saas-collections.ts +375 -0
  135. package/src/data/sync.ts +1213 -0
  136. package/src/data/types.ts +660 -0
  137. package/src/forms/converters.ts +512 -0
  138. package/src/forms/index.ts +133 -0
  139. package/src/forms/schemas.ts +403 -0
  140. package/src/forms/types.ts +476 -0
  141. package/src/index.ts +542 -0
  142. package/src/keyboard/focus.ts +748 -0
  143. package/src/keyboard/index.ts +96 -0
  144. package/src/keyboard/integration.ts +371 -0
  145. package/src/keyboard/manager.ts +377 -0
  146. package/src/keyboard/presets.ts +90 -0
  147. package/src/renderers/ansi-css.ts +576 -0
  148. package/src/renderers/ansi.ts +802 -0
  149. package/src/renderers/ascii.ts +680 -0
  150. package/src/renderers/breadcrumb.ts +480 -0
  151. package/src/renderers/command-palette.ts +802 -0
  152. package/src/renderers/components/field.ts +210 -0
  153. package/src/renderers/components/form.ts +327 -0
  154. package/src/renderers/components/index.ts +21 -0
  155. package/src/renderers/components/search.ts +449 -0
  156. package/src/renderers/components/select.ts +222 -0
  157. package/src/renderers/index.ts +101 -0
  158. package/src/renderers/interactive/component-handlers.ts +622 -0
  159. package/src/renderers/interactive/cursor-manager.ts +147 -0
  160. package/src/renderers/interactive/focus-manager.ts +279 -0
  161. package/src/renderers/interactive/index.ts +661 -0
  162. package/src/renderers/interactive/input-handler.ts +164 -0
  163. package/src/renderers/interactive/keyboard-handler.ts +212 -0
  164. package/src/renderers/interactive/mouse-handler.ts +167 -0
  165. package/src/renderers/interactive/state-manager.ts +109 -0
  166. package/src/renderers/interactive/types.ts +338 -0
  167. package/src/renderers/interactive-string.ts +299 -0
  168. package/src/renderers/interactive.ts +59 -0
  169. package/src/renderers/markdown.ts +950 -0
  170. package/src/renderers/sidebar.ts +549 -0
  171. package/src/renderers/tabs.ts +682 -0
  172. package/src/renderers/text.ts +791 -0
  173. package/src/renderers/unicode.ts +917 -0
  174. package/src/renderers/utils.ts +942 -0
  175. package/src/router/adapters.ts +383 -0
  176. package/src/router/types.ts +140 -0
  177. package/src/router/utils.ts +452 -0
  178. package/src/schemas.ts +205 -0
  179. package/src/storybook/index.ts +91 -0
  180. package/src/storybook/interactive-decorator.tsx +659 -0
  181. package/src/storybook/keyboard-simulator.ts +501 -0
  182. package/src/theme/ansi-codes.ts +80 -0
  183. package/src/theme/box-drawing.ts +132 -0
  184. package/src/theme/color-convert.ts +254 -0
  185. package/src/theme/color-support.ts +321 -0
  186. package/src/theme/index.ts +134 -0
  187. package/src/theme/strip-ansi.ts +50 -0
  188. package/src/theme/tailwind-map.ts +469 -0
  189. package/src/theme/text-styles.ts +206 -0
  190. package/src/theme/theme-system.ts +568 -0
  191. 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
+ }