@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,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
|
+
}
|