@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,917 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unicode Renderer - Renders UINode trees using Unicode box-drawing characters
|
|
3
|
+
*
|
|
4
|
+
* This renderer is part of the 6-tier Universal Terminal UI system.
|
|
5
|
+
* It uses Unicode characters for beautiful terminal output without ANSI colors.
|
|
6
|
+
*
|
|
7
|
+
* Unicode box drawing characters:
|
|
8
|
+
* - Single: ┌ ─ ┐ │ └ ┘ ├ ┤ ┬ ┴ ┼
|
|
9
|
+
* - Double: ╔ ═ ╗ ║ ╚ ╝
|
|
10
|
+
* - Rounded: ╭ ╮ ╰ ╯
|
|
11
|
+
*
|
|
12
|
+
* Bullets: • ◦ ▪ ▸ ▾ ✓ ✗
|
|
13
|
+
* Progress: ▓ ░ ▒
|
|
14
|
+
* Spinners: ⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { UINode, RenderContext, ThemeTokens } from '../core/types'
|
|
18
|
+
import {
|
|
19
|
+
DEFAULT_THEME_TOKENS,
|
|
20
|
+
DEFAULT_RENDER_CONTEXT,
|
|
21
|
+
type BoxChars,
|
|
22
|
+
getUnicodeBoxChars,
|
|
23
|
+
UNICODE_SYMBOLS,
|
|
24
|
+
SPINNER_FRAMES,
|
|
25
|
+
} from './utils'
|
|
26
|
+
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// Unicode Characters
|
|
29
|
+
// ============================================================================
|
|
30
|
+
|
|
31
|
+
const UNICODE = {
|
|
32
|
+
// Single box drawing
|
|
33
|
+
topLeft: '┌',
|
|
34
|
+
topRight: '┐',
|
|
35
|
+
bottomLeft: '└',
|
|
36
|
+
bottomRight: '┘',
|
|
37
|
+
horizontal: '─',
|
|
38
|
+
vertical: '│',
|
|
39
|
+
|
|
40
|
+
// T-junctions
|
|
41
|
+
teeLeft: '├',
|
|
42
|
+
teeRight: '┤',
|
|
43
|
+
teeTop: '┬',
|
|
44
|
+
teeBottom: '┴',
|
|
45
|
+
crossJunction: '┼',
|
|
46
|
+
|
|
47
|
+
// Double box drawing
|
|
48
|
+
doubleTopLeft: '╔',
|
|
49
|
+
doubleTopRight: '╗',
|
|
50
|
+
doubleBottomLeft: '╚',
|
|
51
|
+
doubleBottomRight: '╝',
|
|
52
|
+
doubleHorizontal: '═',
|
|
53
|
+
doubleVertical: '║',
|
|
54
|
+
|
|
55
|
+
// Rounded corners
|
|
56
|
+
roundedTopLeft: '╭',
|
|
57
|
+
roundedTopRight: '╮',
|
|
58
|
+
roundedBottomLeft: '╰',
|
|
59
|
+
roundedBottomRight: '╯',
|
|
60
|
+
|
|
61
|
+
// Bullets and list markers
|
|
62
|
+
bullet: '•',
|
|
63
|
+
hollowBullet: '◦',
|
|
64
|
+
squareBullet: '▪',
|
|
65
|
+
triangleRight: '▸',
|
|
66
|
+
triangleDown: '▾',
|
|
67
|
+
checkmark: '✓',
|
|
68
|
+
crossMark: '✗',
|
|
69
|
+
unchecked: '☐',
|
|
70
|
+
arrowRight: '→',
|
|
71
|
+
arrowDown: '↓',
|
|
72
|
+
|
|
73
|
+
// Progress bar characters
|
|
74
|
+
progressFull: '▓',
|
|
75
|
+
progressEmpty: '░',
|
|
76
|
+
progressHalf: '▒',
|
|
77
|
+
|
|
78
|
+
// Dividers
|
|
79
|
+
ellipsis: '…',
|
|
80
|
+
middleDot: '·',
|
|
81
|
+
} as const
|
|
82
|
+
|
|
83
|
+
// Note: SPINNER_FRAMES is now imported from utils
|
|
84
|
+
|
|
85
|
+
// ============================================================================
|
|
86
|
+
// Default Context
|
|
87
|
+
// ============================================================================
|
|
88
|
+
|
|
89
|
+
function createDefaultTheme(): ThemeTokens {
|
|
90
|
+
return { ...DEFAULT_THEME_TOKENS }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function createDefaultContext(): RenderContext {
|
|
94
|
+
return {
|
|
95
|
+
tier: 'unicode',
|
|
96
|
+
width: DEFAULT_RENDER_CONTEXT.width,
|
|
97
|
+
height: DEFAULT_RENDER_CONTEXT.height,
|
|
98
|
+
depth: DEFAULT_RENDER_CONTEXT.depth,
|
|
99
|
+
theme: createDefaultTheme(),
|
|
100
|
+
interactive: false,
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ============================================================================
|
|
105
|
+
// Box Rendering
|
|
106
|
+
// ============================================================================
|
|
107
|
+
|
|
108
|
+
// Note: BoxChars interface and getUnicodeBoxChars are now imported from utils
|
|
109
|
+
|
|
110
|
+
function renderBox(node: UINode, context: RenderContext): string {
|
|
111
|
+
const props = node.props || {}
|
|
112
|
+
const border = (props.border as string) || 'single'
|
|
113
|
+
const chars = getUnicodeBoxChars(border)
|
|
114
|
+
|
|
115
|
+
// Render children first to determine content
|
|
116
|
+
let content = ''
|
|
117
|
+
if (node.children && (typeof node.children === 'string' || node.children.length > 0)) {
|
|
118
|
+
if (typeof node.children === 'string') {
|
|
119
|
+
content = node.children
|
|
120
|
+
} else {
|
|
121
|
+
content = node.children
|
|
122
|
+
.map((child: UINode) => renderUnicode(child, { ...context, depth: context.depth + 1 }))
|
|
123
|
+
.join('\n')
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const contentLines = content ? content.split('\n') : []
|
|
128
|
+
const contentWidth = Math.max(
|
|
129
|
+
...contentLines.map((line) => line.length),
|
|
130
|
+
0
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
// Determine box dimensions
|
|
134
|
+
let width = (props.width as number) || contentWidth + 2 || 6
|
|
135
|
+
let height = (props.height as number) || contentLines.length + 2 || 3
|
|
136
|
+
|
|
137
|
+
// Handle zero dimensions
|
|
138
|
+
if (width <= 0) width = 2
|
|
139
|
+
if (height <= 0) return ''
|
|
140
|
+
|
|
141
|
+
const innerWidth = Math.max(width - 2, 0)
|
|
142
|
+
|
|
143
|
+
// Build box
|
|
144
|
+
const lines: string[] = []
|
|
145
|
+
|
|
146
|
+
// Top border
|
|
147
|
+
lines.push(chars.topLeft + chars.horizontal.repeat(innerWidth) + chars.topRight)
|
|
148
|
+
|
|
149
|
+
// Content lines
|
|
150
|
+
const numContentLines = height - 2
|
|
151
|
+
for (let i = 0; i < numContentLines; i++) {
|
|
152
|
+
const lineContent = contentLines[i] || ''
|
|
153
|
+
const paddedContent = lineContent.padEnd(innerWidth, ' ')
|
|
154
|
+
lines.push(chars.vertical + paddedContent.slice(0, innerWidth) + chars.vertical)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Bottom border
|
|
158
|
+
lines.push(chars.bottomLeft + chars.horizontal.repeat(innerWidth) + chars.bottomRight)
|
|
159
|
+
|
|
160
|
+
return lines.join('\n')
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ============================================================================
|
|
164
|
+
// Text Rendering
|
|
165
|
+
// ============================================================================
|
|
166
|
+
|
|
167
|
+
function renderText(node: UINode): string {
|
|
168
|
+
const content = (node.props?.content as string) || ''
|
|
169
|
+
// Unicode tier ignores style props (bold, color, etc.) - no ANSI codes
|
|
170
|
+
return content
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ============================================================================
|
|
174
|
+
// List Rendering
|
|
175
|
+
// ============================================================================
|
|
176
|
+
|
|
177
|
+
function renderList(node: UINode, context: RenderContext): string {
|
|
178
|
+
const style = (node.props?.style as string) || 'unordered'
|
|
179
|
+
const items = node.props?.items as Array<string | { text: string; checked?: boolean }> | undefined
|
|
180
|
+
const numbered = (node.props?.numbered as boolean) ?? false
|
|
181
|
+
const taskList = (node.props?.taskList as boolean) ?? false
|
|
182
|
+
const children = Array.isArray(node.children) ? node.children : []
|
|
183
|
+
const lines: string[] = []
|
|
184
|
+
|
|
185
|
+
// Handle items prop
|
|
186
|
+
if (items && items.length > 0) {
|
|
187
|
+
items.forEach((item, index) => {
|
|
188
|
+
let text: string
|
|
189
|
+
let marker: string
|
|
190
|
+
|
|
191
|
+
if (typeof item === 'string') {
|
|
192
|
+
text = item
|
|
193
|
+
if (style === 'ordered' || numbered) {
|
|
194
|
+
marker = `${index + 1}. `
|
|
195
|
+
} else {
|
|
196
|
+
marker = `${UNICODE.bullet} `
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
text = item.text ?? ''
|
|
200
|
+
if (taskList && 'checked' in item) {
|
|
201
|
+
marker = item.checked ? `${UNICODE.checkmark} ` : `${UNICODE.unchecked} `
|
|
202
|
+
} else if (style === 'ordered' || numbered) {
|
|
203
|
+
marker = `${index + 1}. `
|
|
204
|
+
} else {
|
|
205
|
+
marker = `${UNICODE.bullet} `
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
lines.push(marker + text)
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Handle list-item children
|
|
214
|
+
children.forEach((child: UINode, index: number) => {
|
|
215
|
+
if (child.type === 'list-item') {
|
|
216
|
+
const content = (child.props?.content as string) || ''
|
|
217
|
+
let marker: string
|
|
218
|
+
|
|
219
|
+
if (style === 'ordered' || numbered) {
|
|
220
|
+
marker = `${index + 1}. `
|
|
221
|
+
} else if (style === 'checklist' || taskList) {
|
|
222
|
+
const checked = child.props?.checked as boolean
|
|
223
|
+
marker = checked ? `${UNICODE.checkmark} ` : `${UNICODE.unchecked} `
|
|
224
|
+
} else {
|
|
225
|
+
// unordered
|
|
226
|
+
marker = `${UNICODE.bullet} `
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
lines.push(marker + content)
|
|
230
|
+
|
|
231
|
+
// Handle nested lists
|
|
232
|
+
if (child.children && Array.isArray(child.children) && child.children.length > 0) {
|
|
233
|
+
child.children.forEach((nestedChild: UINode) => {
|
|
234
|
+
if (nestedChild.type === 'list') {
|
|
235
|
+
const nestedLines = renderNestedList(nestedChild, context)
|
|
236
|
+
lines.push(...nestedLines.split('\n').map((line) => ' ' + line))
|
|
237
|
+
}
|
|
238
|
+
})
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
return lines.join('\n')
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function renderNestedList(node: UINode, context: RenderContext, depth = 1): string {
|
|
247
|
+
const style = (node.props?.style as string) || 'unordered'
|
|
248
|
+
const children = Array.isArray(node.children) ? node.children : []
|
|
249
|
+
const lines: string[] = []
|
|
250
|
+
|
|
251
|
+
// Different bullet styles for different nesting levels
|
|
252
|
+
const bulletStyles = [UNICODE.bullet, UNICODE.hollowBullet, '▪', '▫', '▸']
|
|
253
|
+
const bullet = bulletStyles[Math.min(depth, bulletStyles.length - 1)]
|
|
254
|
+
|
|
255
|
+
children.forEach((child: UINode, index: number) => {
|
|
256
|
+
if (child.type === 'list-item') {
|
|
257
|
+
const content = (child.props?.content as string) || ''
|
|
258
|
+
let marker: string
|
|
259
|
+
|
|
260
|
+
if (style === 'ordered') {
|
|
261
|
+
marker = `${index + 1}. `
|
|
262
|
+
} else {
|
|
263
|
+
// Use different bullet for nested unordered lists
|
|
264
|
+
marker = `${bullet} `
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
lines.push(marker + content)
|
|
268
|
+
|
|
269
|
+
// Handle deeper nested lists
|
|
270
|
+
if (child.children && Array.isArray(child.children) && child.children.length > 0) {
|
|
271
|
+
child.children.forEach((nestedChild: UINode) => {
|
|
272
|
+
if (nestedChild.type === 'list') {
|
|
273
|
+
const nestedLines = renderNestedList(nestedChild, context, depth + 1)
|
|
274
|
+
lines.push(...nestedLines.split('\n').map((line) => ' ' + line))
|
|
275
|
+
}
|
|
276
|
+
})
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
return lines.join('\n')
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ============================================================================
|
|
285
|
+
// Progress Bar Rendering
|
|
286
|
+
// ============================================================================
|
|
287
|
+
|
|
288
|
+
function renderProgress(node: UINode): string {
|
|
289
|
+
const value = (node.props?.value as number) || 0
|
|
290
|
+
const max = (node.props?.max as number) || 100
|
|
291
|
+
const width = (node.props?.width as number) || 10
|
|
292
|
+
|
|
293
|
+
const percentage = Math.min(Math.max(value / max, 0), 1)
|
|
294
|
+
const filledBlocks = percentage * width
|
|
295
|
+
|
|
296
|
+
const fullBlocks = Math.floor(filledBlocks)
|
|
297
|
+
const hasHalf = filledBlocks - fullBlocks >= 0.5
|
|
298
|
+
const emptyBlocks = width - fullBlocks - (hasHalf ? 1 : 0)
|
|
299
|
+
|
|
300
|
+
let bar = UNICODE.progressFull.repeat(fullBlocks)
|
|
301
|
+
if (hasHalf) {
|
|
302
|
+
bar += UNICODE.progressHalf
|
|
303
|
+
}
|
|
304
|
+
bar += UNICODE.progressEmpty.repeat(Math.max(emptyBlocks, 0))
|
|
305
|
+
|
|
306
|
+
return bar
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ============================================================================
|
|
310
|
+
// Table Rendering
|
|
311
|
+
// ============================================================================
|
|
312
|
+
|
|
313
|
+
interface TableColumn {
|
|
314
|
+
key: string
|
|
315
|
+
header: string
|
|
316
|
+
width: number
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function renderTable(node: UINode, context: RenderContext): string {
|
|
320
|
+
const props = node.props || {}
|
|
321
|
+
const columns = (props.columns as TableColumn[]) || []
|
|
322
|
+
// Support data from node.data (TDD tests) or props.data (legacy)
|
|
323
|
+
const nodeData = node.data as Record<string, unknown>[] | undefined
|
|
324
|
+
const propsData = props.data as Record<string, unknown>[] | undefined
|
|
325
|
+
const data = nodeData ?? propsData ?? []
|
|
326
|
+
const rowSeparators = props.rowSeparators as boolean
|
|
327
|
+
|
|
328
|
+
if (columns.length === 0) return ''
|
|
329
|
+
|
|
330
|
+
const lines: string[] = []
|
|
331
|
+
|
|
332
|
+
// Build widths array - calculate from content if not specified
|
|
333
|
+
const widths = columns.map((col) => {
|
|
334
|
+
if (col.width && col.width > 0) return col.width
|
|
335
|
+
|
|
336
|
+
// Calculate based on header and data
|
|
337
|
+
let maxWidth = col.header.length
|
|
338
|
+
for (const row of data) {
|
|
339
|
+
const val = row[col.key]
|
|
340
|
+
const str = val != null ? String(val) : ''
|
|
341
|
+
maxWidth = Math.max(maxWidth, str.length)
|
|
342
|
+
}
|
|
343
|
+
return maxWidth
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
// Top border
|
|
347
|
+
const topBorder =
|
|
348
|
+
UNICODE.topLeft +
|
|
349
|
+
widths.map((w) => UNICODE.horizontal.repeat(w)).join(UNICODE.teeTop) +
|
|
350
|
+
UNICODE.topRight
|
|
351
|
+
lines.push(topBorder)
|
|
352
|
+
|
|
353
|
+
// Header row
|
|
354
|
+
const headerRow =
|
|
355
|
+
UNICODE.vertical +
|
|
356
|
+
columns.map((col, i) => col.header.padEnd(widths[i]).slice(0, widths[i])).join(UNICODE.vertical) +
|
|
357
|
+
UNICODE.vertical
|
|
358
|
+
lines.push(headerRow)
|
|
359
|
+
|
|
360
|
+
// Header separator
|
|
361
|
+
const headerSep =
|
|
362
|
+
UNICODE.teeLeft +
|
|
363
|
+
widths.map((w) => UNICODE.horizontal.repeat(w)).join(UNICODE.crossJunction) +
|
|
364
|
+
UNICODE.teeRight
|
|
365
|
+
lines.push(headerSep)
|
|
366
|
+
|
|
367
|
+
// Data rows
|
|
368
|
+
data.forEach((row, rowIndex) => {
|
|
369
|
+
const dataRow =
|
|
370
|
+
UNICODE.vertical +
|
|
371
|
+
columns
|
|
372
|
+
.map((col, i) => {
|
|
373
|
+
const value = String(row[col.key] ?? '')
|
|
374
|
+
return value.padEnd(widths[i]).slice(0, widths[i])
|
|
375
|
+
})
|
|
376
|
+
.join(UNICODE.vertical) +
|
|
377
|
+
UNICODE.vertical
|
|
378
|
+
lines.push(dataRow)
|
|
379
|
+
|
|
380
|
+
// Row separator if enabled and not last row
|
|
381
|
+
if (rowSeparators && rowIndex < data.length - 1) {
|
|
382
|
+
const rowSep =
|
|
383
|
+
UNICODE.teeLeft +
|
|
384
|
+
widths.map((w) => UNICODE.horizontal.repeat(w)).join(UNICODE.crossJunction) +
|
|
385
|
+
UNICODE.teeRight
|
|
386
|
+
lines.push(rowSep)
|
|
387
|
+
}
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
// Bottom border
|
|
391
|
+
const bottomBorder =
|
|
392
|
+
UNICODE.bottomLeft +
|
|
393
|
+
widths.map((w) => UNICODE.horizontal.repeat(w)).join(UNICODE.teeBottom) +
|
|
394
|
+
UNICODE.bottomRight
|
|
395
|
+
lines.push(bottomBorder)
|
|
396
|
+
|
|
397
|
+
return lines.join('\n')
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ============================================================================
|
|
401
|
+
// Panel Rendering
|
|
402
|
+
// ============================================================================
|
|
403
|
+
|
|
404
|
+
function renderPanel(node: UINode, context: RenderContext): string {
|
|
405
|
+
const props = node.props || {}
|
|
406
|
+
const title = (props.title as string) || ''
|
|
407
|
+
const width = (props.width as number) || 40
|
|
408
|
+
|
|
409
|
+
// Render children
|
|
410
|
+
let content = ''
|
|
411
|
+
if (node.children && (typeof node.children === 'string' || node.children.length > 0)) {
|
|
412
|
+
if (typeof node.children === 'string') {
|
|
413
|
+
content = node.children
|
|
414
|
+
} else {
|
|
415
|
+
content = node.children
|
|
416
|
+
.map((child: UINode) => renderUnicode(child, { ...context, depth: context.depth + 1 }))
|
|
417
|
+
.join('\n')
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const innerWidth = width - 2
|
|
422
|
+
const lines: string[] = []
|
|
423
|
+
|
|
424
|
+
// Top border
|
|
425
|
+
lines.push(UNICODE.topLeft + UNICODE.horizontal.repeat(innerWidth) + UNICODE.topRight)
|
|
426
|
+
|
|
427
|
+
// Title row
|
|
428
|
+
const paddedTitle = (' ' + title + ' ').padEnd(innerWidth).slice(0, innerWidth)
|
|
429
|
+
lines.push(UNICODE.vertical + paddedTitle + UNICODE.vertical)
|
|
430
|
+
|
|
431
|
+
// Title separator
|
|
432
|
+
lines.push(UNICODE.teeLeft + UNICODE.horizontal.repeat(innerWidth) + UNICODE.teeRight)
|
|
433
|
+
|
|
434
|
+
// Content rows
|
|
435
|
+
const contentLines = content ? content.split('\n') : []
|
|
436
|
+
contentLines.forEach((line) => {
|
|
437
|
+
const paddedLine = (' ' + line).padEnd(innerWidth).slice(0, innerWidth)
|
|
438
|
+
lines.push(UNICODE.vertical + paddedLine + UNICODE.vertical)
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
// Bottom border
|
|
442
|
+
lines.push(UNICODE.bottomLeft + UNICODE.horizontal.repeat(innerWidth) + UNICODE.bottomRight)
|
|
443
|
+
|
|
444
|
+
return lines.join('\n')
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// ============================================================================
|
|
448
|
+
// Divider Rendering
|
|
449
|
+
// ============================================================================
|
|
450
|
+
|
|
451
|
+
function renderDivider(node: UINode, context: RenderContext): string {
|
|
452
|
+
const props = node.props || {}
|
|
453
|
+
const label = props.label as string | undefined
|
|
454
|
+
const width = (props.width as number) || context.width || 40
|
|
455
|
+
|
|
456
|
+
if (label) {
|
|
457
|
+
const labelWithSpaces = ` ${label} `
|
|
458
|
+
const remaining = width - labelWithSpaces.length
|
|
459
|
+
const leftSide = Math.floor(remaining / 2)
|
|
460
|
+
const rightSide = remaining - leftSide
|
|
461
|
+
return (
|
|
462
|
+
UNICODE.horizontal.repeat(Math.max(leftSide, 0)) +
|
|
463
|
+
labelWithSpaces +
|
|
464
|
+
UNICODE.horizontal.repeat(Math.max(rightSide, 0))
|
|
465
|
+
)
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return UNICODE.horizontal.repeat(width)
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// ============================================================================
|
|
472
|
+
// Spinner Rendering
|
|
473
|
+
// ============================================================================
|
|
474
|
+
|
|
475
|
+
function renderSpinner(node: UINode): string {
|
|
476
|
+
const props = node.props || {}
|
|
477
|
+
const frame = (props.frame as number) || 0
|
|
478
|
+
const label = props.label as string | undefined
|
|
479
|
+
|
|
480
|
+
const spinnerChar = SPINNER_FRAMES[frame % SPINNER_FRAMES.length]
|
|
481
|
+
|
|
482
|
+
if (label) {
|
|
483
|
+
return `${spinnerChar} ${label}`
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return spinnerChar
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// ============================================================================
|
|
490
|
+
// Badge Rendering
|
|
491
|
+
// ============================================================================
|
|
492
|
+
|
|
493
|
+
function renderBadge(node: UINode): string {
|
|
494
|
+
const props = node.props || {}
|
|
495
|
+
const content = (props.content as string) || ''
|
|
496
|
+
// Use unicode brackets
|
|
497
|
+
return `【${content}】`
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// ============================================================================
|
|
501
|
+
// Tree Rendering
|
|
502
|
+
// ============================================================================
|
|
503
|
+
|
|
504
|
+
function renderTree(node: UINode, context: RenderContext): string {
|
|
505
|
+
const children = Array.isArray(node.children) ? node.children : []
|
|
506
|
+
const lines: string[] = []
|
|
507
|
+
|
|
508
|
+
children.forEach((child: UINode) => {
|
|
509
|
+
if (child.type === 'tree-item') {
|
|
510
|
+
const treeLines = renderTreeItem(child, '', true, context, true)
|
|
511
|
+
lines.push(...treeLines)
|
|
512
|
+
}
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
return lines.join('\n')
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function renderTreeItem(
|
|
519
|
+
node: UINode,
|
|
520
|
+
prefix: string,
|
|
521
|
+
isLast: boolean,
|
|
522
|
+
context: RenderContext,
|
|
523
|
+
isRoot: boolean = false
|
|
524
|
+
): string[] {
|
|
525
|
+
const label = (node.props?.label as string) || ''
|
|
526
|
+
const children = Array.isArray(node.children) ? node.children : []
|
|
527
|
+
const lines: string[] = []
|
|
528
|
+
|
|
529
|
+
// Root items have no connector prefix
|
|
530
|
+
if (isRoot) {
|
|
531
|
+
lines.push(label)
|
|
532
|
+
} else {
|
|
533
|
+
const connector = isLast ? UNICODE.bottomLeft : UNICODE.teeLeft
|
|
534
|
+
lines.push(prefix + connector + UNICODE.horizontal + UNICODE.horizontal + ' ' + label)
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Determine prefix for children
|
|
538
|
+
// For root items, children start with empty prefix (just the connector)
|
|
539
|
+
// For non-root items, extend the prefix with continuation lines
|
|
540
|
+
const childPrefix = isRoot ? '' : prefix + (isLast ? ' ' : UNICODE.vertical + ' ')
|
|
541
|
+
|
|
542
|
+
// Render children
|
|
543
|
+
children.forEach((child: UINode, index: number) => {
|
|
544
|
+
if (child.type === 'tree-item') {
|
|
545
|
+
const isChildLast = index === children.length - 1
|
|
546
|
+
const childLines = renderTreeItem(child, childPrefix, isChildLast, context, false)
|
|
547
|
+
lines.push(...childLines)
|
|
548
|
+
}
|
|
549
|
+
})
|
|
550
|
+
|
|
551
|
+
return lines
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// ============================================================================
|
|
555
|
+
// Breadcrumb Rendering
|
|
556
|
+
// ============================================================================
|
|
557
|
+
|
|
558
|
+
function renderBreadcrumb(node: UINode): string {
|
|
559
|
+
const children = Array.isArray(node.children) ? node.children : []
|
|
560
|
+
const items: string[] = []
|
|
561
|
+
|
|
562
|
+
children.forEach((child: UINode) => {
|
|
563
|
+
if (child.type === 'breadcrumb-item') {
|
|
564
|
+
const label = (child.props?.label as string) || ''
|
|
565
|
+
items.push(label)
|
|
566
|
+
}
|
|
567
|
+
})
|
|
568
|
+
|
|
569
|
+
return items.join(` ${UNICODE.arrowRight} `)
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// ============================================================================
|
|
573
|
+
// Tooltip Rendering
|
|
574
|
+
// ============================================================================
|
|
575
|
+
|
|
576
|
+
function renderTooltip(node: UINode): string {
|
|
577
|
+
const props = node.props || {}
|
|
578
|
+
const content = (props.content as string) || ''
|
|
579
|
+
const position = (props.position as string) || 'top'
|
|
580
|
+
|
|
581
|
+
let pointer = '▲' // default top
|
|
582
|
+
switch (position) {
|
|
583
|
+
case 'bottom':
|
|
584
|
+
pointer = '▼'
|
|
585
|
+
break
|
|
586
|
+
case 'left':
|
|
587
|
+
pointer = '◀'
|
|
588
|
+
break
|
|
589
|
+
case 'right':
|
|
590
|
+
pointer = '▶'
|
|
591
|
+
break
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
return `${pointer} ${content}`
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// ============================================================================
|
|
598
|
+
// Input Rendering
|
|
599
|
+
// ============================================================================
|
|
600
|
+
|
|
601
|
+
function renderInput(node: UINode, context: RenderContext): string {
|
|
602
|
+
const props = node.props || {}
|
|
603
|
+
const label = (props.label as string) || ''
|
|
604
|
+
const value = (props.value as string) || ''
|
|
605
|
+
|
|
606
|
+
return `${label}: [${value}]`
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// ============================================================================
|
|
610
|
+
// Button Rendering
|
|
611
|
+
// ============================================================================
|
|
612
|
+
|
|
613
|
+
function renderButton(node: UINode): string {
|
|
614
|
+
const props = node.props || {}
|
|
615
|
+
const label = (props.label as string) || ''
|
|
616
|
+
|
|
617
|
+
return `[ ${label} ]`
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// ============================================================================
|
|
621
|
+
// Card Rendering
|
|
622
|
+
// ============================================================================
|
|
623
|
+
|
|
624
|
+
function renderCard(node: UINode, context: RenderContext): string {
|
|
625
|
+
const title = node.props?.title as string | undefined
|
|
626
|
+
const subtitle = node.props?.subtitle as string | undefined
|
|
627
|
+
const badge = node.props?.badge as { content: string; variant?: string } | undefined
|
|
628
|
+
const titleAction = node.props?.titleAction as { label: string; action?: string } | undefined
|
|
629
|
+
const pairs = node.props?.pairs as Array<{ key: string; value: unknown }> | undefined
|
|
630
|
+
const actions = node.props?.actions as Array<{ label: string; action?: string }> | undefined
|
|
631
|
+
const border = node.props?.border as string | undefined
|
|
632
|
+
|
|
633
|
+
const contentLines: string[] = []
|
|
634
|
+
|
|
635
|
+
// Title section
|
|
636
|
+
if (title) {
|
|
637
|
+
let titleLine = title
|
|
638
|
+
if (badge) {
|
|
639
|
+
titleLine += ` 【${badge.content}】`
|
|
640
|
+
}
|
|
641
|
+
if (titleAction) {
|
|
642
|
+
titleLine += ` ${UNICODE.arrowRight} ${titleAction.label}`
|
|
643
|
+
}
|
|
644
|
+
contentLines.push(titleLine)
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (subtitle) {
|
|
648
|
+
contentLines.push(subtitle)
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Key-value pairs
|
|
652
|
+
if (pairs && pairs.length > 0) {
|
|
653
|
+
if (contentLines.length > 0) contentLines.push('')
|
|
654
|
+
for (const pair of pairs) {
|
|
655
|
+
const val = pair.value != null ? String(pair.value) : ''
|
|
656
|
+
contentLines.push(`${pair.key}: ${val}`)
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Children content
|
|
661
|
+
if (node.children && (typeof node.children === 'string' || node.children.length > 0)) {
|
|
662
|
+
if (contentLines.length > 0) contentLines.push('')
|
|
663
|
+
let childContent: string
|
|
664
|
+
if (typeof node.children === 'string') {
|
|
665
|
+
childContent = node.children
|
|
666
|
+
} else {
|
|
667
|
+
childContent = node.children
|
|
668
|
+
.map((child: UINode) => renderUnicode(child, { ...context, depth: context.depth + 1 }))
|
|
669
|
+
.join('\n')
|
|
670
|
+
}
|
|
671
|
+
if (childContent) {
|
|
672
|
+
contentLines.push(childContent)
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Actions
|
|
677
|
+
if (actions && actions.length > 0) {
|
|
678
|
+
if (contentLines.length > 0) contentLines.push('')
|
|
679
|
+
const actionLabels = actions.map((a) => `[ ${a.label} ]`).join(' ')
|
|
680
|
+
contentLines.push(actionLabels)
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// If border is specified, wrap content in a box
|
|
684
|
+
if (border) {
|
|
685
|
+
const chars = getUnicodeBoxChars(border)
|
|
686
|
+
// Calculate the width based on content
|
|
687
|
+
const contentWidth = Math.max(
|
|
688
|
+
...contentLines.map((line) => line.length),
|
|
689
|
+
0
|
|
690
|
+
)
|
|
691
|
+
const boxWidth = contentWidth + 4 // 2 for border chars, 2 for padding
|
|
692
|
+
|
|
693
|
+
const lines: string[] = []
|
|
694
|
+
const innerWidth = boxWidth - 2
|
|
695
|
+
|
|
696
|
+
// Top border
|
|
697
|
+
lines.push(chars.topLeft + chars.horizontal.repeat(innerWidth) + chars.topRight)
|
|
698
|
+
|
|
699
|
+
// Content lines with padding
|
|
700
|
+
for (const line of contentLines) {
|
|
701
|
+
const paddedLine = ' ' + line.padEnd(innerWidth - 2) + ' '
|
|
702
|
+
lines.push(chars.vertical + paddedLine.slice(0, innerWidth) + chars.vertical)
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// If no content, add an empty line
|
|
706
|
+
if (contentLines.length === 0) {
|
|
707
|
+
lines.push(chars.vertical + ' '.repeat(innerWidth) + chars.vertical)
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// Bottom border
|
|
711
|
+
lines.push(chars.bottomLeft + chars.horizontal.repeat(innerWidth) + chars.bottomRight)
|
|
712
|
+
|
|
713
|
+
return lines.join('\n')
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
return contentLines.join('\n')
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// ============================================================================
|
|
720
|
+
// Metrics Rendering
|
|
721
|
+
// ============================================================================
|
|
722
|
+
|
|
723
|
+
// Sparkline characters (8 levels of height)
|
|
724
|
+
const SPARKLINE_CHARS = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█']
|
|
725
|
+
|
|
726
|
+
// Trend arrow characters
|
|
727
|
+
const TREND_ARROWS = {
|
|
728
|
+
up: '↑',
|
|
729
|
+
down: '↓',
|
|
730
|
+
neutral: '→',
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function renderSparkline(data: number[]): string {
|
|
734
|
+
if (!data || data.length === 0) return ''
|
|
735
|
+
const min = Math.min(...data)
|
|
736
|
+
const max = Math.max(...data)
|
|
737
|
+
const range = max - min || 1 // Avoid division by zero
|
|
738
|
+
return data
|
|
739
|
+
.map((v) => {
|
|
740
|
+
const normalized = (v - min) / range
|
|
741
|
+
const charIndex = Math.min(Math.floor(normalized * 8), 7)
|
|
742
|
+
return SPARKLINE_CHARS[charIndex]
|
|
743
|
+
})
|
|
744
|
+
.join('')
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function renderMetrics(node: UINode): string {
|
|
748
|
+
const metrics = node.props?.metrics as Array<{
|
|
749
|
+
label: string
|
|
750
|
+
value: unknown
|
|
751
|
+
format?: string
|
|
752
|
+
unit?: string
|
|
753
|
+
trend?: 'up' | 'down' | 'neutral'
|
|
754
|
+
trendValue?: number
|
|
755
|
+
sparkline?: number[]
|
|
756
|
+
}> | undefined
|
|
757
|
+
|
|
758
|
+
if (!metrics || metrics.length === 0) return ''
|
|
759
|
+
|
|
760
|
+
const lines: string[] = []
|
|
761
|
+
for (const m of metrics) {
|
|
762
|
+
const val = m.value != null ? String(m.value) : ''
|
|
763
|
+
let formatted = val
|
|
764
|
+
if (m.format === 'percentage' && !val.includes('%')) {
|
|
765
|
+
formatted = `${val}%`
|
|
766
|
+
}
|
|
767
|
+
if (m.unit) {
|
|
768
|
+
formatted = `${formatted} ${m.unit}`
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Add trend arrow if specified
|
|
772
|
+
if (m.trend && TREND_ARROWS[m.trend]) {
|
|
773
|
+
formatted = `${formatted} ${TREND_ARROWS[m.trend]}`
|
|
774
|
+
if (m.trendValue !== undefined) {
|
|
775
|
+
formatted = `${formatted} ${m.trendValue}%`
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// Add sparkline if specified
|
|
780
|
+
if (m.sparkline && m.sparkline.length > 0) {
|
|
781
|
+
const sparkline = renderSparkline(m.sparkline)
|
|
782
|
+
formatted = `${formatted} ${sparkline}`
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
lines.push(`${m.label}: ${formatted}`)
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
return lines.join('\n')
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function renderSingleMetric(node: UINode): string {
|
|
792
|
+
const label = node.props?.label as string | undefined
|
|
793
|
+
const value = node.props?.value
|
|
794
|
+
const format = node.props?.format as string | undefined
|
|
795
|
+
const unit = node.props?.unit as string | undefined
|
|
796
|
+
|
|
797
|
+
if (!label) return ''
|
|
798
|
+
|
|
799
|
+
const val = value != null ? String(value) : ''
|
|
800
|
+
let formatted = val
|
|
801
|
+
if (format === 'percentage' && !val.includes('%')) {
|
|
802
|
+
formatted = `${val}%`
|
|
803
|
+
}
|
|
804
|
+
if (unit) {
|
|
805
|
+
formatted = `${formatted} ${unit}`
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
return `${label}: ${formatted}`
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// ============================================================================
|
|
812
|
+
// Main Render Function
|
|
813
|
+
// ============================================================================
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Renders a UINode tree to Unicode box-drawing output.
|
|
817
|
+
*
|
|
818
|
+
* This renderer is part of the 6-tier Universal Terminal UI system.
|
|
819
|
+
* It outputs Unicode characters for beautiful terminal borders and
|
|
820
|
+
* symbols without using ANSI escape sequences for colors.
|
|
821
|
+
*
|
|
822
|
+
* @param node - The UINode tree to render
|
|
823
|
+
* @param context - Optional render context with width, height, theme
|
|
824
|
+
* @returns Unicode string ready for terminal output
|
|
825
|
+
*
|
|
826
|
+
* @example
|
|
827
|
+
* ```ts
|
|
828
|
+
* const node: UINode = {
|
|
829
|
+
* type: 'box',
|
|
830
|
+
* props: { border: 'single' },
|
|
831
|
+
* children: [{ type: 'text', props: { content: 'Hello' } }]
|
|
832
|
+
* }
|
|
833
|
+
*
|
|
834
|
+
* const output = renderUnicode(node)
|
|
835
|
+
* // ┌───────┐
|
|
836
|
+
* // │ Hello │
|
|
837
|
+
* // └───────┘
|
|
838
|
+
* ```
|
|
839
|
+
*/
|
|
840
|
+
export function renderUnicode(node: UINode, context?: RenderContext): string {
|
|
841
|
+
const ctx = context || createDefaultContext()
|
|
842
|
+
|
|
843
|
+
switch (node.type) {
|
|
844
|
+
case 'text':
|
|
845
|
+
return renderText(node)
|
|
846
|
+
|
|
847
|
+
case 'box':
|
|
848
|
+
return renderBox(node, ctx)
|
|
849
|
+
|
|
850
|
+
case 'list':
|
|
851
|
+
return renderList(node, ctx)
|
|
852
|
+
|
|
853
|
+
case 'list-item':
|
|
854
|
+
// List items are handled by list
|
|
855
|
+
return (node.props?.content as string) || ''
|
|
856
|
+
|
|
857
|
+
case 'progress':
|
|
858
|
+
return renderProgress(node)
|
|
859
|
+
|
|
860
|
+
case 'table':
|
|
861
|
+
return renderTable(node, ctx)
|
|
862
|
+
|
|
863
|
+
case 'panel':
|
|
864
|
+
return renderPanel(node, ctx)
|
|
865
|
+
|
|
866
|
+
case 'divider':
|
|
867
|
+
return renderDivider(node, ctx)
|
|
868
|
+
|
|
869
|
+
case 'spinner':
|
|
870
|
+
return renderSpinner(node)
|
|
871
|
+
|
|
872
|
+
case 'badge':
|
|
873
|
+
return renderBadge(node)
|
|
874
|
+
|
|
875
|
+
case 'tree':
|
|
876
|
+
return renderTree(node, ctx)
|
|
877
|
+
|
|
878
|
+
case 'tree-item':
|
|
879
|
+
return (node.props?.label as string) || ''
|
|
880
|
+
|
|
881
|
+
case 'breadcrumb':
|
|
882
|
+
return renderBreadcrumb(node)
|
|
883
|
+
|
|
884
|
+
case 'breadcrumb-item':
|
|
885
|
+
return (node.props?.label as string) || ''
|
|
886
|
+
|
|
887
|
+
case 'tooltip':
|
|
888
|
+
return renderTooltip(node)
|
|
889
|
+
|
|
890
|
+
case 'input':
|
|
891
|
+
return renderInput(node, ctx)
|
|
892
|
+
|
|
893
|
+
case 'button':
|
|
894
|
+
return renderButton(node)
|
|
895
|
+
|
|
896
|
+
case 'card':
|
|
897
|
+
return renderCard(node, ctx)
|
|
898
|
+
|
|
899
|
+
case 'metrics':
|
|
900
|
+
return renderMetrics(node)
|
|
901
|
+
|
|
902
|
+
case 'metric':
|
|
903
|
+
return renderSingleMetric(node)
|
|
904
|
+
|
|
905
|
+
default:
|
|
906
|
+
// Unknown node type - return empty or render children
|
|
907
|
+
if (node.children && (typeof node.children === 'string' || node.children.length > 0)) {
|
|
908
|
+
if (typeof node.children === 'string') {
|
|
909
|
+
return node.children
|
|
910
|
+
}
|
|
911
|
+
return node.children
|
|
912
|
+
.map((child: UINode) => renderUnicode(child, { ...ctx, depth: ctx.depth + 1 }))
|
|
913
|
+
.join('\n')
|
|
914
|
+
}
|
|
915
|
+
return ''
|
|
916
|
+
}
|
|
917
|
+
}
|