@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,791 @@
1
+ /**
2
+ * Text Renderer - Plain text output without formatting
3
+ *
4
+ * The TEXT renderer is the first tier in the Universal Terminal UI's
5
+ * multi-tier rendering architecture. It outputs plain text without any
6
+ * formatting markers, colors, or special characters.
7
+ *
8
+ * Text Tier (tier 1 of 6):
9
+ * - Simple content only, no bold/italic/code markers
10
+ * - Tables rendered as key=value lines (one per line)
11
+ * - Lists rendered as bullet points (- item)
12
+ * - Metrics rendered as "Label: value" format
13
+ * - Nested structures indented (2 spaces per level)
14
+ * - Dashboard layout as labeled sections with separators
15
+ * - All special characters and formatting stripped
16
+ */
17
+
18
+ import type { UINode } from '../core/types'
19
+ import { getIndentStr, extractStringArray, extractHeaders, extractRowValues, joinParts, getProp } from './utils'
20
+
21
+ /**
22
+ * Text render options
23
+ */
24
+ export interface TextRenderOptions {
25
+ /** Current indentation level */
26
+ indent?: number
27
+ }
28
+
29
+ /**
30
+ * Renders a UINode tree to a plain text string.
31
+ *
32
+ * @param node - The UINode tree to render
33
+ * @param options - Optional rendering configuration
34
+ * @returns Plain text string
35
+ */
36
+ export function renderText(node: UINode, options: TextRenderOptions = {}): string {
37
+ const indent = options.indent ?? 0
38
+ const indentStr = getIndentStr(indent)
39
+
40
+ const type = node.type
41
+ const props = node.props || {}
42
+ // Normalize children: string becomes a text node array
43
+ const rawChildren = node.children
44
+ const children: UINode[] = typeof rawChildren === 'string'
45
+ ? [{ type: 'text', props: { content: rawChildren } }]
46
+ : rawChildren || []
47
+
48
+ switch (type) {
49
+ case 'text':
50
+ return renderTextNode(props)
51
+
52
+ case 'header':
53
+ return renderHeader(props)
54
+
55
+ case 'list':
56
+ return renderList(props, children, indent)
57
+
58
+ case 'table':
59
+ return renderTable(props, node.data)
60
+
61
+ case 'code':
62
+ return renderCode(props)
63
+
64
+ case 'link':
65
+ return renderLink(props)
66
+
67
+ case 'button':
68
+ return renderButton(props)
69
+
70
+ case 'box':
71
+ return renderBox(props, children, indent)
72
+
73
+ case 'panel':
74
+ return renderPanel(props, children, indent)
75
+
76
+ case 'card':
77
+ return renderCard(props, children, indent)
78
+
79
+ case 'metrics':
80
+ return renderMetrics(props)
81
+
82
+ case 'metric':
83
+ return renderSingleMetric(props)
84
+
85
+ case 'sidebar':
86
+ return renderSidebar(props, indent)
87
+
88
+ case 'breadcrumb':
89
+ return renderBreadcrumb(props)
90
+
91
+ case 'badge':
92
+ return renderBadge(props)
93
+
94
+ case 'dialog':
95
+ return renderDialog(props, children, indent)
96
+
97
+ case 'spinner':
98
+ return renderSpinner(props)
99
+
100
+ case 'dashboard':
101
+ return renderDashboard(props, children, indent)
102
+
103
+ case 'settings':
104
+ return renderSettings(props, children, indent)
105
+
106
+ case 'input':
107
+ return renderInput(props)
108
+
109
+ case 'select':
110
+ return renderSelect(props)
111
+
112
+ case 'hero':
113
+ return renderHero(props)
114
+
115
+ case 'features':
116
+ return renderFeatures(props)
117
+
118
+ case 'pricing':
119
+ return renderPricing(props)
120
+
121
+ case 'faq':
122
+ return renderFAQ(props)
123
+
124
+ case 'footer':
125
+ return renderFooter(props)
126
+
127
+ default:
128
+ // Unknown types: render children only
129
+ return renderChildren(children, indent)
130
+ }
131
+ }
132
+
133
+ function renderTextNode(props: Record<string, unknown>): string {
134
+ const content = props.content
135
+ if (content == null) return ''
136
+ return String(content)
137
+ }
138
+
139
+ function renderHeader(props: Record<string, unknown>): string {
140
+ const content = props.content
141
+ const nav = props.nav as Array<{ label: string; href?: string }> | undefined
142
+ const callToAction = props.callToAction as string | undefined
143
+ const breadcrumbs = props.breadcrumbs as Array<{ label: string; href?: string }> | undefined
144
+
145
+ // If it has content (simple heading), return it
146
+ if (content != null) {
147
+ return String(content)
148
+ }
149
+
150
+ // Otherwise handle as site header with nav/cta/breadcrumbs
151
+ const parts: string[] = []
152
+
153
+ if (nav && Array.isArray(nav) && nav.length > 0) {
154
+ const navLabels = nav.map((item) => item.label).join(' | ')
155
+ parts.push(navLabels)
156
+ }
157
+
158
+ if (breadcrumbs && Array.isArray(breadcrumbs) && breadcrumbs.length > 0) {
159
+ const crumbLabels = breadcrumbs.map((item) => item.label).join(' > ')
160
+ parts.push(crumbLabels)
161
+ }
162
+
163
+ if (callToAction) {
164
+ parts.push(callToAction)
165
+ }
166
+
167
+ return parts.join('\n')
168
+ }
169
+
170
+ function renderList(
171
+ props: Record<string, unknown>,
172
+ children: UINode[],
173
+ indent: number
174
+ ): string {
175
+ const items = props.items as unknown[] | undefined
176
+ const numbered = getProp(props, 'numbered', false)
177
+ const taskList = getProp(props, 'taskList', false)
178
+ const indentStr = getIndentStr(indent)
179
+ const lines: string[] = []
180
+
181
+ // Handle items prop (simple items or task items)
182
+ if (items && Array.isArray(items)) {
183
+ items.forEach((item, index) => {
184
+ let text: string
185
+ let prefix: string
186
+
187
+ if (typeof item === 'string') {
188
+ text = item
189
+ prefix = numbered ? `${index + 1}. ` : '- '
190
+ } else if (typeof item === 'object' && item !== null) {
191
+ const itemObj = item as { text?: string; checked?: boolean }
192
+ text = itemObj.text ?? String(item)
193
+ if (taskList && 'checked' in itemObj) {
194
+ prefix = itemObj.checked ? '[x] ' : '[ ] '
195
+ } else {
196
+ prefix = numbered ? `${index + 1}. ` : '- '
197
+ }
198
+ } else {
199
+ text = String(item)
200
+ prefix = numbered ? `${index + 1}. ` : '- '
201
+ }
202
+
203
+ lines.push(`${indentStr}${prefix}${text}`)
204
+ })
205
+ }
206
+
207
+ // Handle list-item children (TDD test format)
208
+ const nestedChildren = (children && children.length > 0)
209
+ ? children
210
+ : (props.children as UINode[] | undefined)
211
+
212
+ if (nestedChildren && nestedChildren.length > 0) {
213
+ nestedChildren.forEach((child, index) => {
214
+ if (child.type === 'list-item') {
215
+ const content = child.props?.content as string ?? ''
216
+ const prefix = numbered ? `${index + 1}. ` : '- '
217
+ lines.push(`${indentStr}${prefix}${content}`)
218
+
219
+ // Handle nested children of list-item
220
+ const childChildren = child.children
221
+ if (childChildren && Array.isArray(childChildren) && childChildren.length > 0) {
222
+ childChildren.forEach((nestedChild: UINode) => {
223
+ const nestedOutput = renderText(nestedChild, { indent: indent + 1 })
224
+ if (nestedOutput) {
225
+ lines.push(nestedOutput)
226
+ }
227
+ })
228
+ }
229
+ } else {
230
+ const childOutput = renderText(child, { indent: indent + 1 })
231
+ if (childOutput) {
232
+ lines.push(childOutput)
233
+ }
234
+ }
235
+ })
236
+ }
237
+
238
+ return lines.join('\n')
239
+ }
240
+
241
+ function renderTable(props: Record<string, unknown>, nodeData?: unknown): string {
242
+ const columns = props.columns as Array<{ key: string; header: string }> | undefined
243
+ // Support data from node.data (TDD tests) or props.data (legacy)
244
+ const data = (nodeData as Array<Record<string, unknown>> | undefined) ??
245
+ (props.data as Array<Record<string, unknown>> | undefined)
246
+ const lines: string[] = []
247
+
248
+ if (!columns || !Array.isArray(columns)) return ''
249
+
250
+ // Add headers using shared utility
251
+ const headers = extractHeaders(columns)
252
+ lines.push(headers.join(', '))
253
+
254
+ // Add data rows using shared utility
255
+ if (data && Array.isArray(data)) {
256
+ data.forEach((row) => {
257
+ const values = extractRowValues(row, columns)
258
+ lines.push(values.join(', '))
259
+ })
260
+ }
261
+
262
+ return lines.join('\n')
263
+ }
264
+
265
+ function renderCode(props: Record<string, unknown>): string {
266
+ const code = props.code
267
+ if (code == null) return ''
268
+ return String(code)
269
+ }
270
+
271
+ function renderLink(props: Record<string, unknown>): string {
272
+ const text = props.text
273
+ if (text == null) return ''
274
+ return String(text)
275
+ }
276
+
277
+ function renderButton(props: Record<string, unknown>): string {
278
+ const label = props.label as string | undefined
279
+ const hotkey = props.hotkey as string | undefined
280
+
281
+ if (!label) return ''
282
+
283
+ if (hotkey) {
284
+ return `${label} (${hotkey})`
285
+ }
286
+ return label
287
+ }
288
+
289
+ function renderBox(
290
+ props: Record<string, unknown>,
291
+ children: UINode[],
292
+ indent: number
293
+ ): string {
294
+ const title = props.title as string | undefined
295
+ const parts: string[] = []
296
+
297
+ if (title) {
298
+ parts.push(title)
299
+ }
300
+
301
+ const childOutput = renderChildren(children, indent)
302
+ if (childOutput) {
303
+ parts.push(childOutput)
304
+ }
305
+
306
+ return parts.join('\n\n')
307
+ }
308
+
309
+ function renderPanel(
310
+ props: Record<string, unknown>,
311
+ children: UINode[],
312
+ indent: number
313
+ ): string {
314
+ const title = props.title as string | undefined
315
+ const collapsed = props.collapsed as boolean | undefined
316
+ const parts: string[] = []
317
+
318
+ if (title) {
319
+ parts.push(title)
320
+ }
321
+
322
+ // If collapsed, still show title but skip content for now
323
+ // Based on tests, collapsed panels still show title
324
+ const childOutput = renderChildren(children, indent)
325
+ if (childOutput) {
326
+ parts.push(childOutput)
327
+ }
328
+
329
+ return parts.join('\n\n')
330
+ }
331
+
332
+ function renderCard(
333
+ props: Record<string, unknown>,
334
+ children: UINode[],
335
+ indent: number
336
+ ): string {
337
+ const title = props.title as string | undefined
338
+ const subtitle = props.subtitle as string | undefined
339
+ const badge = props.badge as { content: string; variant?: string } | undefined
340
+ const titleAction = props.titleAction as { label: string; action?: string } | undefined
341
+ const pairs = props.pairs as Array<{ key: string; value: unknown }> | undefined
342
+ const actions = props.actions as Array<{ label: string; action?: string }> | undefined
343
+ const parts: string[] = []
344
+
345
+ // Title section
346
+ if (title) {
347
+ let titleLine = title
348
+ if (badge) {
349
+ titleLine += ` ${badge.content}`
350
+ }
351
+ if (titleAction) {
352
+ titleLine += ` ${titleAction.label}`
353
+ }
354
+ parts.push(titleLine)
355
+ }
356
+
357
+ if (subtitle) {
358
+ parts.push(subtitle)
359
+ }
360
+
361
+ // Key-value pairs
362
+ if (pairs && Array.isArray(pairs)) {
363
+ const pairLines = pairs.map((p) => {
364
+ const val = p.value != null ? String(p.value) : ''
365
+ return `${p.key}: ${val}`
366
+ })
367
+ if (pairLines.length > 0) {
368
+ parts.push(pairLines.join('\n'))
369
+ }
370
+ }
371
+
372
+ // Children content
373
+ const childOutput = renderChildren(children, indent)
374
+ if (childOutput) {
375
+ parts.push(childOutput)
376
+ }
377
+
378
+ // Actions
379
+ if (actions && Array.isArray(actions)) {
380
+ const actionLabels = actions.map((a) => a.label).join(' | ')
381
+ if (actionLabels) {
382
+ parts.push(actionLabels)
383
+ }
384
+ }
385
+
386
+ return parts.join('\n\n')
387
+ }
388
+
389
+ function renderMetrics(props: Record<string, unknown>): string {
390
+ const metrics = props.metrics as Array<{
391
+ label: string
392
+ value: unknown
393
+ format?: string
394
+ unit?: string
395
+ trend?: string
396
+ }> | undefined
397
+
398
+ if (!metrics || !Array.isArray(metrics) || metrics.length === 0) {
399
+ return ''
400
+ }
401
+
402
+ return metrics.map((m) => {
403
+ const val = m.value != null ? String(m.value) : ''
404
+ let formatted = val
405
+ if (m.format === 'percentage' && !val.includes('%')) {
406
+ formatted = `${val}%`
407
+ }
408
+ if (m.unit) {
409
+ formatted = `${formatted} ${m.unit}`
410
+ }
411
+ return `${m.label}: ${formatted}`
412
+ }).join('\n')
413
+ }
414
+
415
+ function renderSingleMetric(props: Record<string, unknown>): string {
416
+ const label = props.label as string | undefined
417
+ const value = props.value
418
+ const format = props.format as string | undefined
419
+ const unit = props.unit as string | undefined
420
+
421
+ if (!label) return ''
422
+
423
+ const val = value != null ? String(value) : ''
424
+ let formatted = val
425
+ if (format === 'percentage' && !val.includes('%')) {
426
+ formatted = `${val}%`
427
+ }
428
+ if (unit) {
429
+ formatted = `${formatted} ${unit}`
430
+ }
431
+
432
+ return `${label}: ${formatted}`
433
+ }
434
+
435
+ function renderSidebar(props: Record<string, unknown>, indent: number): string {
436
+ const nav = props.nav as Array<{
437
+ label: string
438
+ href?: string
439
+ active?: boolean
440
+ children?: Array<{ label: string; href?: string }>
441
+ }> | undefined
442
+ const sections = props.sections as Array<{
443
+ title: string
444
+ items: Array<{ label: string; href?: string }>
445
+ }> | undefined
446
+
447
+ const lines: string[] = []
448
+ const indentStr = getIndentStr(indent)
449
+
450
+ if (nav && Array.isArray(nav)) {
451
+ nav.forEach((item) => {
452
+ lines.push(`${indentStr}- ${item.label}`)
453
+ if (item.children && Array.isArray(item.children)) {
454
+ item.children.forEach((child) => {
455
+ lines.push(`${indentStr} - ${child.label}`)
456
+ })
457
+ }
458
+ })
459
+ }
460
+
461
+ if (sections && Array.isArray(sections)) {
462
+ sections.forEach((section) => {
463
+ lines.push(`${indentStr}${section.title}`)
464
+ if (section.items && Array.isArray(section.items)) {
465
+ section.items.forEach((item) => {
466
+ lines.push(`${indentStr} - ${item.label}`)
467
+ })
468
+ }
469
+ })
470
+ }
471
+
472
+ return lines.join('\n')
473
+ }
474
+
475
+ function renderBreadcrumb(props: Record<string, unknown>): string {
476
+ const items = props.items as Array<{ label: string; path?: string }> | undefined
477
+ const separator = (props.separator as string) || '>'
478
+
479
+ if (!items || !Array.isArray(items)) return ''
480
+
481
+ return items.map((item) => item.label).join(` ${separator} `)
482
+ }
483
+
484
+ function renderBadge(props: Record<string, unknown>): string {
485
+ const children = props.children
486
+ if (children == null) return ''
487
+ return String(children)
488
+ }
489
+
490
+ function renderDialog(
491
+ props: Record<string, unknown>,
492
+ children: UINode[],
493
+ indent: number
494
+ ): string {
495
+ const open = props.open as boolean | undefined
496
+ const title = props.title as string | undefined
497
+ const actions = props.actions as Array<{ label: string; action: string }> | undefined
498
+
499
+ // Skip closed dialogs
500
+ if (open === false) return ''
501
+
502
+ const parts: string[] = []
503
+
504
+ if (title) {
505
+ parts.push(title)
506
+ }
507
+
508
+ const childOutput = renderChildren(children, indent)
509
+ if (childOutput) {
510
+ parts.push(childOutput)
511
+ }
512
+
513
+ if (actions && Array.isArray(actions)) {
514
+ const actionLabels = actions.map((a) => a.label).join(' | ')
515
+ parts.push(actionLabels)
516
+ }
517
+
518
+ return parts.join('\n\n')
519
+ }
520
+
521
+ function renderSpinner(props: Record<string, unknown>): string {
522
+ const label = props.label as string | undefined
523
+ return label || 'Loading...'
524
+ }
525
+
526
+ function renderDashboard(
527
+ props: Record<string, unknown>,
528
+ children: UINode[],
529
+ indent: number
530
+ ): string {
531
+ const title = props.title as string | undefined
532
+ const metrics = props.metrics as Array<{
533
+ label: string
534
+ value: string | number
535
+ trend?: string
536
+ }> | undefined
537
+
538
+ const parts: string[] = []
539
+
540
+ if (title) {
541
+ parts.push(title)
542
+ }
543
+
544
+ if (metrics && Array.isArray(metrics) && metrics.length > 0) {
545
+ const metricsOutput = metrics.map((m) => `${m.label}: ${m.value}`).join('\n')
546
+ parts.push(metricsOutput)
547
+ }
548
+
549
+ const childOutput = renderChildren(children, indent)
550
+ if (childOutput) {
551
+ parts.push(childOutput)
552
+ }
553
+
554
+ return parts.join('\n\n')
555
+ }
556
+
557
+ function renderSettings(
558
+ props: Record<string, unknown>,
559
+ children: UINode[],
560
+ indent: number
561
+ ): string {
562
+ const sections = props.sections as string[] | undefined
563
+ const parts: string[] = []
564
+
565
+ if (sections && Array.isArray(sections)) {
566
+ sections.forEach((section) => {
567
+ parts.push(section)
568
+ })
569
+ }
570
+
571
+ const childOutput = renderChildren(children, indent)
572
+ if (childOutput) {
573
+ parts.push(childOutput)
574
+ }
575
+
576
+ return parts.join('\n')
577
+ }
578
+
579
+ function renderInput(props: Record<string, unknown>): string {
580
+ const label = props.label as string | undefined
581
+ const value = props.value as string | undefined
582
+ const placeholder = props.placeholder as string | undefined
583
+
584
+ const parts: string[] = []
585
+
586
+ if (label) {
587
+ parts.push(label)
588
+ }
589
+
590
+ if (value != null) {
591
+ parts.push(String(value))
592
+ } else if (placeholder) {
593
+ parts.push(placeholder)
594
+ }
595
+
596
+ return parts.join(': ')
597
+ }
598
+
599
+ function renderSelect(props: Record<string, unknown>): string {
600
+ const label = props.label as string | undefined
601
+ const value = props.value as string | undefined
602
+ const options = props.options as Array<{ label: string; value: string }> | undefined
603
+
604
+ const parts: string[] = []
605
+
606
+ if (label) {
607
+ parts.push(label)
608
+ }
609
+
610
+ // Show current selection if there is one
611
+ if (value && options) {
612
+ const selected = options.find((o) => o.value === value)
613
+ if (selected) {
614
+ parts.push(`Selected: ${selected.label}`)
615
+ }
616
+ }
617
+
618
+ // List all options
619
+ if (options && Array.isArray(options)) {
620
+ options.forEach((opt) => {
621
+ parts.push(`- ${opt.label}`)
622
+ })
623
+ }
624
+
625
+ return parts.join('\n')
626
+ }
627
+
628
+ function renderHero(props: Record<string, unknown>): string {
629
+ const title = props.title as string | undefined
630
+ const subtitle = props.subtitle as string | undefined
631
+ const callToAction = props.callToAction as string | undefined
632
+ const secondaryCallToAction = props.secondaryCallToAction as string | undefined
633
+ const badge = props.badge as string | undefined
634
+
635
+ const parts: string[] = []
636
+
637
+ if (badge) {
638
+ parts.push(badge)
639
+ }
640
+
641
+ if (title) {
642
+ parts.push(title)
643
+ }
644
+
645
+ if (subtitle) {
646
+ parts.push(subtitle)
647
+ }
648
+
649
+ const ctas: string[] = []
650
+ if (callToAction) {
651
+ ctas.push(callToAction)
652
+ }
653
+ if (secondaryCallToAction) {
654
+ ctas.push(secondaryCallToAction)
655
+ }
656
+ if (ctas.length > 0) {
657
+ parts.push(ctas.join(' | '))
658
+ }
659
+
660
+ return parts.join('\n\n')
661
+ }
662
+
663
+ function renderFeatures(props: Record<string, unknown>): string {
664
+ const title = props.title as string | undefined
665
+ const features = props.features as Array<{
666
+ title: string
667
+ description?: string
668
+ icon?: string
669
+ }> | undefined
670
+
671
+ const parts: string[] = []
672
+
673
+ if (title) {
674
+ parts.push(title)
675
+ }
676
+
677
+ if (features && Array.isArray(features)) {
678
+ features.forEach((feature) => {
679
+ if (feature.description) {
680
+ parts.push(`${feature.title}: ${feature.description}`)
681
+ } else {
682
+ parts.push(feature.title)
683
+ }
684
+ })
685
+ }
686
+
687
+ return parts.join('\n\n')
688
+ }
689
+
690
+ function renderPricing(props: Record<string, unknown>): string {
691
+ const tiers = props.tiers as Array<{
692
+ name: string
693
+ price: string
694
+ features: string[]
695
+ callToAction?: string
696
+ }> | undefined
697
+
698
+ if (!tiers || !Array.isArray(tiers)) return ''
699
+
700
+ const parts: string[] = []
701
+
702
+ tiers.forEach((tier) => {
703
+ const tierParts: string[] = []
704
+ tierParts.push(`${tier.name} - ${tier.price}`)
705
+
706
+ if (tier.features && Array.isArray(tier.features)) {
707
+ tier.features.forEach((feature) => {
708
+ tierParts.push(` - ${feature}`)
709
+ })
710
+ }
711
+
712
+ if (tier.callToAction) {
713
+ tierParts.push(` ${tier.callToAction}`)
714
+ }
715
+
716
+ parts.push(tierParts.join('\n'))
717
+ })
718
+
719
+ return parts.join('\n\n')
720
+ }
721
+
722
+ function renderFAQ(props: Record<string, unknown>): string {
723
+ const title = props.title as string | undefined
724
+ const items = props.items as Array<{
725
+ question: string
726
+ answer: string
727
+ }> | undefined
728
+
729
+ const parts: string[] = []
730
+
731
+ if (title) {
732
+ parts.push(title)
733
+ }
734
+
735
+ if (items && Array.isArray(items)) {
736
+ items.forEach((item) => {
737
+ parts.push(`Q: ${item.question}`)
738
+ parts.push(`A: ${item.answer}`)
739
+ })
740
+ }
741
+
742
+ return parts.join('\n\n')
743
+ }
744
+
745
+ function renderFooter(props: Record<string, unknown>): string {
746
+ const links = props.links as Array<{
747
+ title: string
748
+ links: Array<{ label: string; href: string }>
749
+ }> | undefined
750
+ const copyright = props.copyright as string | undefined
751
+ const social = props.social as Array<{ platform: string; href: string }> | undefined
752
+
753
+ const parts: string[] = []
754
+
755
+ if (links && Array.isArray(links)) {
756
+ links.forEach((section) => {
757
+ parts.push(section.title)
758
+ if (section.links && Array.isArray(section.links)) {
759
+ section.links.forEach((link) => {
760
+ parts.push(` - ${link.label}`)
761
+ })
762
+ }
763
+ })
764
+ }
765
+
766
+ if (social && Array.isArray(social)) {
767
+ const socialParts = social.map((s) => s.platform).join(' | ')
768
+ parts.push(socialParts)
769
+ }
770
+
771
+ if (copyright) {
772
+ parts.push(copyright)
773
+ }
774
+
775
+ return parts.join('\n')
776
+ }
777
+
778
+ function renderChildren(children: UINode[], indent: number): string {
779
+ if (!children || children.length === 0) return ''
780
+
781
+ const parts: string[] = []
782
+
783
+ children.forEach((child) => {
784
+ const output = renderText(child, { indent })
785
+ if (output) {
786
+ parts.push(output)
787
+ }
788
+ })
789
+
790
+ return parts.join('\n\n')
791
+ }