@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,802 @@
1
+ /**
2
+ * ANSI Renderer
3
+ *
4
+ * Converts UINode trees to ANSI-escaped terminal strings with support for:
5
+ * - 16 color, 256 color, and true color modes
6
+ * - Text formatting (bold, italic, underline, etc.)
7
+ * - Theme colors (dark/light mode with semantic colors)
8
+ * - Graceful color degradation
9
+ */
10
+
11
+ import { ANSI } from '../theme/ansi-codes'
12
+ import { hexToRgb } from '../theme/color-convert'
13
+ import type { UINode } from '../core/types'
14
+
15
+ // ============================================================================
16
+ // Types
17
+ // ============================================================================
18
+
19
+ /**
20
+ * ANSI render options
21
+ */
22
+ interface ANSIRenderOptions {
23
+ colorSupport?: 'none' | '16' | '256' | 'truecolor'
24
+ theme?: 'dark' | 'light'
25
+ width?: number
26
+ resetBetweenElements?: boolean
27
+ }
28
+
29
+ /**
30
+ * RGB color object
31
+ */
32
+ interface RgbColor {
33
+ r: number
34
+ g: number
35
+ b: number
36
+ }
37
+
38
+ // ============================================================================
39
+ // Color Maps
40
+ // ============================================================================
41
+
42
+ /** Map of basic 16 color names to foreground codes */
43
+ const FG_COLOR_MAP: Record<string, string> = {
44
+ black: '\x1b[30m',
45
+ red: '\x1b[31m',
46
+ green: '\x1b[32m',
47
+ yellow: '\x1b[33m',
48
+ blue: '\x1b[34m',
49
+ magenta: '\x1b[35m',
50
+ cyan: '\x1b[36m',
51
+ white: '\x1b[37m',
52
+ brightBlack: '\x1b[90m',
53
+ brightRed: '\x1b[91m',
54
+ brightGreen: '\x1b[92m',
55
+ brightYellow: '\x1b[93m',
56
+ brightBlue: '\x1b[94m',
57
+ brightMagenta: '\x1b[95m',
58
+ brightCyan: '\x1b[96m',
59
+ brightWhite: '\x1b[97m',
60
+ }
61
+
62
+ /** Map of basic 16 color names to background codes */
63
+ const BG_COLOR_MAP: Record<string, string> = {
64
+ black: '\x1b[40m',
65
+ red: '\x1b[41m',
66
+ green: '\x1b[42m',
67
+ yellow: '\x1b[43m',
68
+ blue: '\x1b[44m',
69
+ magenta: '\x1b[45m',
70
+ cyan: '\x1b[46m',
71
+ white: '\x1b[47m',
72
+ }
73
+
74
+ /** Semantic color mappings for dark theme */
75
+ const DARK_THEME_COLORS: Record<string, string> = {
76
+ primary: '\x1b[36m', // cyan
77
+ secondary: '\x1b[34m', // blue
78
+ accent: '\x1b[35m', // magenta
79
+ muted: '\x1b[90m', // brightBlack (gray)
80
+ success: '\x1b[32m', // green
81
+ warning: '\x1b[33m', // yellow
82
+ error: '\x1b[31m', // red
83
+ info: '\x1b[34m', // blue
84
+ foreground: '\x1b[37m', // white
85
+ background: '\x1b[40m', // black bg
86
+ }
87
+
88
+ /** Semantic color mappings for light theme */
89
+ const LIGHT_THEME_COLORS: Record<string, string> = {
90
+ primary: '\x1b[34m', // blue (darker for light bg)
91
+ secondary: '\x1b[36m', // cyan
92
+ accent: '\x1b[35m', // magenta
93
+ muted: '\x1b[90m', // brightBlack (gray)
94
+ success: '\x1b[32m', // green
95
+ warning: '\x1b[33m', // yellow
96
+ error: '\x1b[31m', // red
97
+ info: '\x1b[34m', // blue
98
+ foreground: '\x1b[30m', // black
99
+ background: '\x1b[47m', // white bg
100
+ }
101
+
102
+ // ============================================================================
103
+ // Color Utilities
104
+ // ============================================================================
105
+
106
+ /**
107
+ * Convert RGB to nearest ANSI 256 color code
108
+ */
109
+ function rgbToAnsi256(r: number, g: number, b: number): number {
110
+ // Check for grayscale
111
+ if (Math.abs(r - g) < 10 && Math.abs(g - b) < 10 && Math.abs(r - b) < 10) {
112
+ const avg = (r + g + b) / 3
113
+ if (avg < 8) return 0
114
+ if (avg > 248) return 15
115
+ const grayIndex = Math.round((avg - 8) / 10)
116
+ return Math.min(255, Math.max(232, 232 + grayIndex))
117
+ }
118
+
119
+ // Color cube
120
+ const toColorCubeIndex = (v: number): number => {
121
+ if (v < 48) return 0
122
+ if (v < 115) return 1
123
+ return Math.min(5, Math.floor((v - 35) / 40))
124
+ }
125
+
126
+ const ri = toColorCubeIndex(r)
127
+ const gi = toColorCubeIndex(g)
128
+ const bi = toColorCubeIndex(b)
129
+
130
+ return 16 + 36 * ri + 6 * gi + bi
131
+ }
132
+
133
+ /**
134
+ * Convert ANSI 256 color to nearest basic 16 color
135
+ */
136
+ function ansi256To16(code: number): number {
137
+ if (code < 8) return 30 + code
138
+ if (code < 16) return 90 + (code - 8)
139
+
140
+ let r: number, g: number, b: number
141
+
142
+ if (code >= 232) {
143
+ const gray = (code - 232) * 10 + 8
144
+ r = g = b = gray
145
+ } else {
146
+ const cubeIndex = code - 16
147
+ r = Math.floor(cubeIndex / 36) * 51
148
+ g = Math.floor((cubeIndex % 36) / 6) * 51
149
+ b = (cubeIndex % 6) * 51
150
+ }
151
+
152
+ const basicColors = [
153
+ { r: 0, g: 0, b: 0, code: 30 },
154
+ { r: 170, g: 0, b: 0, code: 31 },
155
+ { r: 0, g: 170, b: 0, code: 32 },
156
+ { r: 170, g: 170, b: 0, code: 33 },
157
+ { r: 0, g: 0, b: 170, code: 34 },
158
+ { r: 170, g: 0, b: 170, code: 35 },
159
+ { r: 0, g: 170, b: 170, code: 36 },
160
+ { r: 170, g: 170, b: 170, code: 37 },
161
+ { r: 85, g: 85, b: 85, code: 90 },
162
+ { r: 255, g: 85, b: 85, code: 91 },
163
+ { r: 85, g: 255, b: 85, code: 92 },
164
+ { r: 255, g: 255, b: 85, code: 93 },
165
+ { r: 85, g: 85, b: 255, code: 94 },
166
+ { r: 255, g: 85, b: 255, code: 95 },
167
+ { r: 85, g: 255, b: 255, code: 96 },
168
+ { r: 255, g: 255, b: 255, code: 97 },
169
+ ]
170
+
171
+ let minDist = Infinity
172
+ let closestCode = 37
173
+
174
+ for (const bc of basicColors) {
175
+ const dist = Math.sqrt(
176
+ Math.pow(r - bc.r, 2) + Math.pow(g - bc.g, 2) + Math.pow(b - bc.b, 2)
177
+ )
178
+ if (dist < minDist) {
179
+ minDist = dist
180
+ closestCode = bc.code
181
+ }
182
+ }
183
+
184
+ return closestCode
185
+ }
186
+
187
+ /**
188
+ * Resolve a color value to an ANSI escape code
189
+ */
190
+ function resolveColor(
191
+ color: unknown,
192
+ isBackground: boolean,
193
+ options: ANSIRenderOptions
194
+ ): string {
195
+ const colorSupport = options.colorSupport || 'truecolor'
196
+ const theme = options.theme || 'dark'
197
+ const themeColors = theme === 'dark' ? DARK_THEME_COLORS : LIGHT_THEME_COLORS
198
+
199
+ // No colors when support is none
200
+ if (colorSupport === 'none') {
201
+ return ''
202
+ }
203
+
204
+ // Handle semantic colors
205
+ if (typeof color === 'string' && themeColors[color]) {
206
+ const ansi = themeColors[color]
207
+ // For background semantic colors, we need to convert fg code to bg
208
+ if (isBackground && ansi.startsWith('\x1b[3')) {
209
+ return ansi.replace('\x1b[3', '\x1b[4')
210
+ }
211
+ return ansi
212
+ }
213
+
214
+ // Handle named 16 colors
215
+ if (typeof color === 'string') {
216
+ const colorMap = isBackground ? BG_COLOR_MAP : FG_COLOR_MAP
217
+ if (colorMap[color]) {
218
+ return colorMap[color]
219
+ }
220
+
221
+ // Handle hex colors
222
+ if (color.startsWith('#')) {
223
+ const rgb = hexToRgb(color)
224
+ return formatRgbColor(rgb.r, rgb.g, rgb.b, isBackground, colorSupport)
225
+ }
226
+
227
+ // Unknown string color - return empty
228
+ return ''
229
+ }
230
+
231
+ // Handle numeric 256 colors
232
+ if (typeof color === 'number') {
233
+ const code = Math.max(0, Math.min(255, Math.floor(color)))
234
+
235
+ if (colorSupport === '16') {
236
+ const code16 = ansi256To16(code)
237
+ return `\x1b[${code16}m`
238
+ }
239
+
240
+ const prefix = isBackground ? '48' : '38'
241
+ return `\x1b[${prefix};5;${code}m`
242
+ }
243
+
244
+ // Handle RGB object
245
+ if (
246
+ typeof color === 'object' &&
247
+ color !== null &&
248
+ 'r' in color &&
249
+ 'g' in color &&
250
+ 'b' in color
251
+ ) {
252
+ const rgb = color as RgbColor
253
+ return formatRgbColor(rgb.r, rgb.g, rgb.b, isBackground, colorSupport)
254
+ }
255
+
256
+ return ''
257
+ }
258
+
259
+ /**
260
+ * Format RGB color based on color support level
261
+ */
262
+ function formatRgbColor(
263
+ r: number,
264
+ g: number,
265
+ b: number,
266
+ isBackground: boolean,
267
+ colorSupport: string
268
+ ): string {
269
+ const prefix = isBackground ? '48' : '38'
270
+
271
+ if (colorSupport === 'truecolor') {
272
+ return `\x1b[${prefix};2;${r};${g};${b}m`
273
+ }
274
+
275
+ const code256 = rgbToAnsi256(r, g, b)
276
+
277
+ if (colorSupport === '256') {
278
+ return `\x1b[${prefix};5;${code256}m`
279
+ }
280
+
281
+ if (colorSupport === '16') {
282
+ const code16 = ansi256To16(code256)
283
+ return `\x1b[${code16}m`
284
+ }
285
+
286
+ return ''
287
+ }
288
+
289
+ // ============================================================================
290
+ // Style Building
291
+ // ============================================================================
292
+
293
+ /**
294
+ * Build ANSI escape codes from props
295
+ */
296
+ function buildStyleCodes(
297
+ props: Record<string, unknown> | undefined,
298
+ options: ANSIRenderOptions
299
+ ): string[] {
300
+ if (!props) return []
301
+
302
+ const codes: string[] = []
303
+ const colorSupport = options.colorSupport || 'truecolor'
304
+
305
+ // Text formatting (preserved even when colors are stripped)
306
+ if (props.bold) codes.push('\x1b[1m')
307
+ if (props.dim) codes.push('\x1b[2m')
308
+ if (props.italic) codes.push('\x1b[3m')
309
+ if (props.underline) codes.push('\x1b[4m')
310
+ if (props.inverse) codes.push('\x1b[7m')
311
+ if (props.strikethrough) codes.push('\x1b[9m')
312
+
313
+ // Colors (only when not 'none')
314
+ if (colorSupport !== 'none') {
315
+ if (props.color !== undefined) {
316
+ const fg = resolveColor(props.color, false, options)
317
+ if (fg) codes.push(fg)
318
+ }
319
+
320
+ if (props.backgroundColor !== undefined) {
321
+ const bg = resolveColor(props.backgroundColor, true, options)
322
+ if (bg) codes.push(bg)
323
+ }
324
+ }
325
+
326
+ return codes
327
+ }
328
+
329
+ /**
330
+ * Check if any styling is applied
331
+ */
332
+ function hasStyles(props: Record<string, unknown> | undefined): boolean {
333
+ if (!props) return false
334
+ return !!(
335
+ props.bold ||
336
+ props.dim ||
337
+ props.italic ||
338
+ props.underline ||
339
+ props.inverse ||
340
+ props.strikethrough ||
341
+ props.color !== undefined ||
342
+ props.backgroundColor !== undefined
343
+ )
344
+ }
345
+
346
+ // ============================================================================
347
+ // Component Renderers
348
+ // ============================================================================
349
+
350
+ /**
351
+ * Render a badge component
352
+ */
353
+ function renderBadge(node: UINode, options: ANSIRenderOptions): string {
354
+ const text = node.text || ''
355
+ const variant = (node.props?.variant as string) || 'default'
356
+
357
+ // Map variants to colors
358
+ const variantColors: Record<string, string> = {
359
+ success: 'success',
360
+ error: 'error',
361
+ warning: 'warning',
362
+ info: 'info',
363
+ default: 'primary',
364
+ }
365
+
366
+ const colorName = variantColors[variant] || 'primary'
367
+ const colorCode = resolveColor(colorName, false, options)
368
+
369
+ if (colorCode) {
370
+ return `${colorCode}${text}${ANSI.reset}`
371
+ }
372
+ return text
373
+ }
374
+
375
+ /**
376
+ * Render a button component
377
+ */
378
+ function renderButton(node: UINode, options: ANSIRenderOptions): string {
379
+ const text = node.text || ''
380
+ const focused = node.props?.focused as boolean
381
+ const variant = (node.props?.variant as string) || 'default'
382
+
383
+ const codes: string[] = []
384
+
385
+ if (focused) {
386
+ codes.push(ANSI.inverse)
387
+ }
388
+
389
+ if (variant === 'primary') {
390
+ const colorCode = resolveColor('primary', false, options)
391
+ if (colorCode) codes.push(colorCode)
392
+ }
393
+
394
+ if (codes.length > 0) {
395
+ return `${codes.join('')}${text}${ANSI.reset}`
396
+ }
397
+ return text
398
+ }
399
+
400
+ /**
401
+ * Render a spinner component
402
+ */
403
+ function renderSpinner(node: UINode): string {
404
+ const label = (node.props?.label as string) || ''
405
+ return label
406
+ }
407
+
408
+ /**
409
+ * Render a panel component
410
+ */
411
+ function renderPanel(node: UINode, options: ANSIRenderOptions): string {
412
+ const title = (node.props?.title as string) || ''
413
+ const content = renderChildren(node.children, options)
414
+ return `${title}${content ? '\n' + content : ''}`
415
+ }
416
+
417
+ /**
418
+ * Render a card component
419
+ */
420
+ function renderCard(node: UINode, options: ANSIRenderOptions): string {
421
+ const title = node.props?.title as string | undefined
422
+ const subtitle = node.props?.subtitle as string | undefined
423
+ const badge = node.props?.badge as { content: string; variant?: string } | undefined
424
+ const titleAction = node.props?.titleAction as { label: string; action?: string } | undefined
425
+ const pairs = node.props?.pairs as Array<{ key: string; value: unknown }> | undefined
426
+ const actions = node.props?.actions as Array<{ label: string; action?: string }> | undefined
427
+ const variant = node.props?.variant as string | undefined
428
+
429
+ const lines: string[] = []
430
+
431
+ // Get variant color if specified
432
+ const variantColor = variant ? resolveColor(variant, false, options) : null
433
+
434
+ // Title section - apply bold styling and optional variant color
435
+ if (title) {
436
+ let titleLine = title
437
+ if (badge) {
438
+ const badgeColor = badge.variant ? resolveColor(badge.variant, false, options) : null
439
+ const badgeText = badgeColor
440
+ ? `${badgeColor}[${badge.content}]${ANSI.reset}`
441
+ : `[${badge.content}]`
442
+ titleLine += ` ${badgeText}`
443
+ }
444
+ if (titleAction) {
445
+ titleLine += ` | ${titleAction.label}`
446
+ }
447
+ // Apply bold to title, and variant color if specified
448
+ const titleCodes: string[] = [ANSI.bold]
449
+ if (variantColor) {
450
+ titleCodes.push(variantColor)
451
+ }
452
+ lines.push(`${titleCodes.join('')}${titleLine}${ANSI.reset}`)
453
+ }
454
+
455
+ if (subtitle) {
456
+ // Apply muted color to subtitle
457
+ const mutedColor = resolveColor('muted', false, options)
458
+ if (mutedColor) {
459
+ lines.push(`${mutedColor}${subtitle}${ANSI.reset}`)
460
+ } else {
461
+ lines.push(subtitle)
462
+ }
463
+ }
464
+
465
+ // Key-value pairs
466
+ if (pairs && pairs.length > 0) {
467
+ for (const pair of pairs) {
468
+ const val = pair.value != null ? String(pair.value) : ''
469
+ // Apply variant color to value if pair has variant
470
+ const pairVariant = (pair as { variant?: string }).variant
471
+ if (pairVariant) {
472
+ const pairColor = resolveColor(pairVariant, false, options)
473
+ if (pairColor) {
474
+ lines.push(`${pair.key}: ${pairColor}${val}${ANSI.reset}`)
475
+ } else {
476
+ lines.push(`${pair.key}: ${val}`)
477
+ }
478
+ } else {
479
+ lines.push(`${pair.key}: ${val}`)
480
+ }
481
+ }
482
+ }
483
+
484
+ // Children content
485
+ const content = renderChildren(node.children, options)
486
+ if (content) {
487
+ lines.push(content)
488
+ }
489
+
490
+ // Actions
491
+ if (actions && actions.length > 0) {
492
+ const actionLabels = actions.map((a) => {
493
+ const actionVariant = (a as { variant?: string }).variant
494
+ if (actionVariant) {
495
+ const actionColor = resolveColor(actionVariant, false, options)
496
+ if (actionColor) {
497
+ return `${actionColor}[ ${a.label} ]${ANSI.reset}`
498
+ }
499
+ }
500
+ return `[ ${a.label} ]`
501
+ }).join(' ')
502
+ lines.push(actionLabels)
503
+ }
504
+
505
+ return lines.join('\n')
506
+ }
507
+
508
+ /**
509
+ * Render an input component
510
+ */
511
+ function renderInput(node: UINode): string {
512
+ const value = (node.props?.value as string) || ''
513
+ return value
514
+ }
515
+
516
+ /**
517
+ * Render a select component
518
+ */
519
+ function renderSelect(node: UINode): string {
520
+ const opts = (node.props?.options as Array<{ label: string; value: unknown }>) || []
521
+ return opts.map((o) => o.label).join('\n')
522
+ }
523
+
524
+ /**
525
+ * Render a table component with ANSI styling
526
+ */
527
+ function renderTable(node: UINode, options: ANSIRenderOptions): string {
528
+ const columns = (node.props?.columns as Array<{ key: string; header: string }>) || []
529
+ // Support data from node.data (TDD tests) or props.data (legacy)
530
+ const nodeData = (node as { data?: unknown }).data as Array<Record<string, unknown>> | undefined
531
+ const data = nodeData ?? (node.props?.data as Array<Record<string, unknown>>) ?? []
532
+ const headerStyle = node.props?.headerStyle as Record<string, unknown> | undefined
533
+
534
+ // Apply header styling (bold by default, or from props)
535
+ const headerCodes = buildStyleCodes(headerStyle ?? { bold: true }, options)
536
+ const headerPrefix = headerCodes.length > 0 ? headerCodes.join('') : ''
537
+ const headerSuffix = headerCodes.length > 0 ? ANSI.reset : ''
538
+
539
+ const headers = columns
540
+ .map((c) => `${headerPrefix}${c.header}${headerSuffix}`)
541
+ .join('\t')
542
+ const rows = data.map((row) => columns.map((c) => String(row[c.key] || '')).join('\t'))
543
+
544
+ return [headers, ...rows].join('\n')
545
+ }
546
+
547
+ /**
548
+ * Render a list component
549
+ */
550
+ function renderList(node: UINode, options: ANSIRenderOptions, depth = 0): string {
551
+ const items = (node.props?.items as Array<string | { text: string; checked?: boolean }>) || []
552
+ const bullet = (node.props?.bullet as string) || '-'
553
+ const numbered = (node.props?.numbered as boolean) ?? false
554
+ const taskList = (node.props?.taskList as boolean) ?? false
555
+ const children = node.children as UINode[] | undefined
556
+ const indent = ' '.repeat(depth)
557
+
558
+ const lines: string[] = []
559
+
560
+ // Get color for list markers
561
+ const markerColor = resolveColor('muted', false, options)
562
+ const resetCode = ANSI.reset
563
+
564
+ // Handle items prop
565
+ if (items.length > 0) {
566
+ items.forEach((item, index) => {
567
+ let text: string
568
+ let prefix: string
569
+
570
+ if (typeof item === 'string') {
571
+ text = item
572
+ prefix = numbered ? `${index + 1}. ` : `${bullet} `
573
+ } else {
574
+ text = item.text ?? ''
575
+ if (taskList && 'checked' in item) {
576
+ prefix = item.checked ? '[x] ' : '[ ] '
577
+ } else {
578
+ prefix = numbered ? `${index + 1}. ` : `${bullet} `
579
+ }
580
+ }
581
+
582
+ // Apply marker color
583
+ const styledPrefix = markerColor ? `${markerColor}${prefix}${resetCode}` : prefix
584
+ lines.push(`${indent}${styledPrefix}${text}`)
585
+ })
586
+ }
587
+
588
+ // Handle list-item children (with nested list support)
589
+ if (children && Array.isArray(children)) {
590
+ children.forEach((child, index) => {
591
+ if (child.type === 'list-item') {
592
+ const content = (child.props?.content as string) ?? ''
593
+ const variant = child.props?.variant as string | undefined
594
+ const prefix = numbered ? `${index + 1}. ` : `${bullet} `
595
+
596
+ // Apply marker color
597
+ const styledPrefix = markerColor ? `${markerColor}${prefix}${resetCode}` : prefix
598
+
599
+ // Apply variant color to content if specified
600
+ let styledContent = content
601
+ if (variant) {
602
+ const variantColor = resolveColor(variant, false, options)
603
+ if (variantColor) {
604
+ styledContent = `${variantColor}${content}${resetCode}`
605
+ }
606
+ }
607
+
608
+ lines.push(`${indent}${styledPrefix}${styledContent}`)
609
+
610
+ // Handle nested lists
611
+ if (child.children && Array.isArray(child.children)) {
612
+ for (const nestedChild of child.children) {
613
+ if (nestedChild.type === 'list') {
614
+ const nestedLines = renderList(nestedChild, options, depth + 1)
615
+ if (nestedLines) {
616
+ lines.push(nestedLines)
617
+ }
618
+ }
619
+ }
620
+ }
621
+ }
622
+ })
623
+ }
624
+
625
+ return lines.join('\n')
626
+ }
627
+
628
+ /**
629
+ * Render a breadcrumb component
630
+ */
631
+ function renderBreadcrumb(node: UINode): string {
632
+ const items = (node.props?.items as Array<{ label: string; path: string }>) || []
633
+ const separator = (node.props?.separator as string) || '/'
634
+ return items.map((item) => item.label).join(` ${separator} `)
635
+ }
636
+
637
+ /**
638
+ * Render metrics (multiple metrics)
639
+ */
640
+ function renderMetrics(node: UINode, options: ANSIRenderOptions): string {
641
+ const metrics = node.props?.metrics as Array<{
642
+ label: string
643
+ value: unknown
644
+ format?: string
645
+ unit?: string
646
+ trend?: string
647
+ variant?: string
648
+ }> | undefined
649
+
650
+ if (!metrics || metrics.length === 0) return ''
651
+
652
+ const labelColor = resolveColor('muted', false, options)
653
+ const resetCode = ANSI.reset
654
+
655
+ const lines: string[] = []
656
+ for (const m of metrics) {
657
+ const val = m.value != null ? String(m.value) : ''
658
+ let formatted = val
659
+ if (m.format === 'percentage' && !val.includes('%')) {
660
+ formatted = `${val}%`
661
+ }
662
+ if (m.unit) {
663
+ formatted = `${formatted} ${m.unit}`
664
+ }
665
+
666
+ // Apply variant color if specified
667
+ const variantColor = m.variant ? resolveColor(m.variant, false, options) : null
668
+ const styledLabel = labelColor ? `${labelColor}${m.label}:${resetCode}` : `${m.label}:`
669
+ const styledValue = variantColor ? `${variantColor}${formatted}${resetCode}` : formatted
670
+
671
+ lines.push(`${styledLabel} ${styledValue}`)
672
+ }
673
+
674
+ return lines.join('\n')
675
+ }
676
+
677
+ /**
678
+ * Render a single metric
679
+ */
680
+ function renderSingleMetric(node: UINode): string {
681
+ const label = node.props?.label as string | undefined
682
+ const value = node.props?.value
683
+ const format = node.props?.format as string | undefined
684
+ const unit = node.props?.unit as string | undefined
685
+
686
+ if (!label) return ''
687
+
688
+ const val = value != null ? String(value) : ''
689
+ let formatted = val
690
+ if (format === 'percentage' && !val.includes('%')) {
691
+ formatted = `${val}%`
692
+ }
693
+ if (unit) {
694
+ formatted = `${formatted} ${unit}`
695
+ }
696
+
697
+ return `${label}: ${formatted}`
698
+ }
699
+
700
+ // ============================================================================
701
+ // Main Rendering
702
+ // ============================================================================
703
+
704
+ /**
705
+ * Render children nodes
706
+ */
707
+ function renderChildren(
708
+ children: UINode[] | string | undefined,
709
+ options: ANSIRenderOptions
710
+ ): string {
711
+ if (!children) return ''
712
+
713
+ if (typeof children === 'string') {
714
+ return children
715
+ }
716
+
717
+ if (Array.isArray(children) && children.length === 0) {
718
+ return ''
719
+ }
720
+
721
+ return children.map((child) => renderNode(child, options)).join('')
722
+ }
723
+
724
+ /**
725
+ * Render a single node
726
+ */
727
+ function renderNode(node: UINode, options: ANSIRenderOptions): string {
728
+ // Get text content
729
+ let text = ''
730
+
731
+ switch (node.type) {
732
+ case 'badge':
733
+ return renderBadge(node, options)
734
+ case 'button':
735
+ return renderButton(node, options)
736
+ case 'spinner':
737
+ return renderSpinner(node)
738
+ case 'panel':
739
+ return renderPanel(node, options)
740
+ case 'card':
741
+ return renderCard(node, options)
742
+ case 'input':
743
+ return renderInput(node)
744
+ case 'select':
745
+ return renderSelect(node)
746
+ case 'table':
747
+ return renderTable(node, options)
748
+ case 'list':
749
+ return renderList(node, options)
750
+ case 'breadcrumb':
751
+ return renderBreadcrumb(node)
752
+ case 'metrics':
753
+ return renderMetrics(node, options)
754
+ case 'metric':
755
+ return renderSingleMetric(node)
756
+ case 'text':
757
+ case 'box':
758
+ default:
759
+ // Handle text content - check props.content, node.text, then children
760
+ if (node.props?.content !== undefined) {
761
+ text = String(node.props.content)
762
+ } else if (node.text !== undefined) {
763
+ text = node.text
764
+ } else if (typeof node.children === 'string') {
765
+ text = node.children
766
+ } else if (Array.isArray(node.children)) {
767
+ text = renderChildren(node.children, options)
768
+ }
769
+ }
770
+
771
+ // Apply styles if present
772
+ if (hasStyles(node.props)) {
773
+ const codes = buildStyleCodes(node.props, options)
774
+ if (codes.length > 0) {
775
+ return `${codes.join('')}${text}${ANSI.reset}`
776
+ }
777
+ }
778
+
779
+ return text
780
+ }
781
+
782
+ // ============================================================================
783
+ // Public API
784
+ // ============================================================================
785
+
786
+ /**
787
+ * Renders a UINode tree to an ANSI-escaped string for terminal output.
788
+ *
789
+ * @param node - The UINode tree to render
790
+ * @param options - Rendering options (color support, theme, width)
791
+ * @returns ANSI-escaped string ready for terminal output
792
+ *
793
+ * @example
794
+ * ```tsx
795
+ * const node = { type: 'text', text: 'Hello', props: { color: 'cyan', bold: true } }
796
+ * const output = renderANSI(node)
797
+ * console.log(output) // Outputs bold cyan text
798
+ * ```
799
+ */
800
+ export function renderANSI(node: UINode, options?: ANSIRenderOptions): string {
801
+ return renderNode(node, options || {})
802
+ }