@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,950 @@
1
+ /**
2
+ * Markdown Renderer
3
+ *
4
+ * Converts UINode trees to Markdown strings for AI agent consumption via MCP.
5
+ *
6
+ * The Markdown renderer outputs structured markdown that is:
7
+ * - Easily parseable by AI agents
8
+ * - Human-readable in raw form
9
+ * - Compatible with MCP (Model Context Protocol) text transport
10
+ *
11
+ * Markdown Tier (tier 2 of 6):
12
+ * Designed for AI agents that consume structured text via MCP protocols.
13
+ * More structured than TEXT tier, less visual than ASCII/UNICODE/ANSI.
14
+ */
15
+
16
+ import type { UINode } from '../core/types'
17
+
18
+ /**
19
+ * Markdown render options
20
+ */
21
+ export interface MarkdownRenderOptions {
22
+ /** Maximum line width for wrapping (optional) */
23
+ width?: number
24
+ /** Whether to include navigation footer hints */
25
+ includeNavFooter?: boolean
26
+ /** Header level offset (e.g., 1 to start at H2 instead of H1) */
27
+ headerOffset?: number
28
+ /** Whether to escape special markdown characters in content */
29
+ escapeContent?: boolean
30
+ }
31
+
32
+ interface RenderState {
33
+ indent: number
34
+ }
35
+
36
+ /**
37
+ * Escape pipe characters in table cell content
38
+ */
39
+ function escapePipes(content: string): string {
40
+ return content.replace(/\|/g, '\\|')
41
+ }
42
+
43
+ /**
44
+ * Escape brackets in link text
45
+ */
46
+ function escapeBrackets(text: string): string {
47
+ return text.replace(/\[/g, '\\[').replace(/\]/g, '\\]')
48
+ }
49
+
50
+ /**
51
+ * Clamp header level to valid range (1-6)
52
+ */
53
+ function clampHeaderLevel(level: number): number {
54
+ if (level < 1) return 1
55
+ if (level > 6) return 6
56
+ return level
57
+ }
58
+
59
+ /**
60
+ * Render text node with formatting
61
+ */
62
+ function renderText(node: UINode): string {
63
+ const { content, bold, italic, code, strikethrough } = node.props as {
64
+ content?: string | null
65
+ bold?: boolean
66
+ italic?: boolean
67
+ code?: boolean
68
+ strikethrough?: boolean
69
+ }
70
+
71
+ if (content == null) return ''
72
+
73
+ let text = String(content)
74
+
75
+ // Apply formatting in order
76
+ if (code) {
77
+ text = `\`${text}\``
78
+ } else {
79
+ if (bold && italic) {
80
+ text = `***${text}***`
81
+ } else if (bold) {
82
+ text = `**${text}**`
83
+ } else if (italic) {
84
+ text = `*${text}*`
85
+ }
86
+
87
+ if (strikethrough) {
88
+ text = `~~${text}~~`
89
+ }
90
+ }
91
+
92
+ return text
93
+ }
94
+
95
+ /**
96
+ * Render header with # prefix
97
+ */
98
+ function renderHeader(node: UINode): string {
99
+ const { level, content } = node.props as { level?: number; content?: string }
100
+ const headerLevel = clampHeaderLevel(level ?? 2)
101
+ const prefix = '#'.repeat(headerLevel)
102
+ return `${prefix} ${content ?? ''}\n`
103
+ }
104
+
105
+ /**
106
+ * Render list (unordered, ordered, or task list)
107
+ */
108
+ function renderList(node: UINode, state: RenderState): string {
109
+ const { items, numbered, taskList, children: propsChildren } = node.props as {
110
+ items?: (string | { text: string; checked?: boolean })[]
111
+ numbered?: boolean
112
+ taskList?: boolean
113
+ children?: UINode[]
114
+ }
115
+
116
+ // Use children from props or from node.children
117
+ const nestedChildren = node.children || propsChildren
118
+ const indent = ' '.repeat(state.indent)
119
+ const lines: string[] = []
120
+
121
+ // Handle items prop
122
+ if (items && items.length > 0) {
123
+ items.forEach((item, index) => {
124
+ if (taskList && typeof item === 'object' && 'checked' in item) {
125
+ const checkbox = item.checked ? '[x]' : '[ ]'
126
+ lines.push(`${indent}- ${checkbox} ${item.text}`)
127
+ } else if (numbered) {
128
+ lines.push(`${indent}${index + 1}. ${typeof item === 'string' ? item : item.text}`)
129
+ } else {
130
+ lines.push(`${indent}- ${typeof item === 'string' ? item : item.text}`)
131
+ }
132
+ })
133
+ }
134
+
135
+ // Handle list-item children (TDD test format)
136
+ if (nestedChildren && Array.isArray(nestedChildren) && nestedChildren.length > 0) {
137
+ nestedChildren.forEach((child: UINode, index: number) => {
138
+ if (child.type === 'list-item') {
139
+ const content = child.props?.content as string ?? ''
140
+ const prefix = numbered ? `${index + 1}. ` : '- '
141
+ lines.push(`${indent}${prefix}${content}`)
142
+
143
+ // Handle nested children of list-item
144
+ if (child.children && Array.isArray(child.children) && child.children.length > 0) {
145
+ child.children.forEach((nestedChild: UINode) => {
146
+ const nestedOutput = renderNode(nestedChild, { indent: state.indent + 2 })
147
+ if (nestedOutput) {
148
+ lines.push(nestedOutput.trimEnd())
149
+ }
150
+ })
151
+ }
152
+ } else {
153
+ const childOutput = renderNode(child, { indent: state.indent + 2 })
154
+ if (childOutput) {
155
+ lines.push(childOutput.trimEnd())
156
+ }
157
+ }
158
+ })
159
+ }
160
+
161
+ if (lines.length === 0) return ''
162
+ return lines.join('\n') + '\n'
163
+ }
164
+
165
+ /**
166
+ * Render table with pipe-separated columns
167
+ */
168
+ function renderTable(node: UINode): string {
169
+ const { columns } = node.props as {
170
+ columns?: { key: string; header: string; align?: 'left' | 'center' | 'right' }[]
171
+ data?: Record<string, unknown>[]
172
+ }
173
+ // Support data from node.data (TDD tests) or props.data (legacy)
174
+ const data = (node.data as Record<string, unknown>[] | undefined) ??
175
+ (node.props?.data as Record<string, unknown>[] | undefined)
176
+
177
+ if (!columns || columns.length === 0) return ''
178
+
179
+ const lines: string[] = []
180
+
181
+ // Header row
182
+ const headerCells = columns.map((col) => ` ${col.header} `)
183
+ lines.push(`|${headerCells.join('|')}|`)
184
+
185
+ // Separator row with alignment
186
+ const separatorCells = columns.map((col) => {
187
+ const base = '---'
188
+ if (col.align === 'center') return `:${base}:`
189
+ if (col.align === 'right') return `${base}:`
190
+ return `${base}` // left is default (no colon)
191
+ })
192
+ lines.push(`| ${separatorCells.join(' | ')} |`)
193
+
194
+ // Data rows
195
+ if (data && data.length > 0) {
196
+ for (const row of data) {
197
+ const cells = columns.map((col) => {
198
+ const value = row[col.key]
199
+ const cellContent = value != null ? String(value) : ''
200
+ return ` ${escapePipes(cellContent)} `
201
+ })
202
+ lines.push(`|${cells.join('|')}|`)
203
+ }
204
+ }
205
+
206
+ return lines.join('\n') + '\n'
207
+ }
208
+
209
+ /**
210
+ * Render code block with fences
211
+ */
212
+ function renderCode(node: UINode): string {
213
+ const { code, language } = node.props as { code?: string; language?: string }
214
+
215
+ if (code == null) return ''
216
+
217
+ const lang = language ?? ''
218
+ return `\`\`\`${lang}\n${code}\n\`\`\`\n`
219
+ }
220
+
221
+ /**
222
+ * Render link
223
+ */
224
+ function renderLink(node: UINode): string {
225
+ const { text, href } = node.props as { text?: string; href?: string }
226
+ return `[${escapeBrackets(text ?? '')}](${href ?? ''})`
227
+ }
228
+
229
+ /**
230
+ * Render button as link
231
+ */
232
+ function renderButton(node: UINode): string {
233
+ const { label, action, hotkey } = node.props as {
234
+ label?: string
235
+ action?: string
236
+ hotkey?: string
237
+ }
238
+
239
+ const labelText = hotkey ? `${label} (${hotkey})` : (label ?? '')
240
+ return `[${labelText}](${action ?? ''})`
241
+ }
242
+
243
+ /**
244
+ * Render box/container
245
+ */
246
+ function renderBox(node: UINode, state: RenderState): string {
247
+ const { title, border } = node.props as { title?: string; border?: string }
248
+ const lines: string[] = []
249
+
250
+ if (title) {
251
+ lines.push(`### ${title}\n`)
252
+ }
253
+
254
+ if (border) {
255
+ lines.push('---\n')
256
+ }
257
+
258
+ if (node.children && Array.isArray(node.children) && node.children.length > 0) {
259
+ const childContent = node.children.map((child: UINode) => renderNode(child, state)).join('\n')
260
+ lines.push(childContent)
261
+ }
262
+
263
+ if (border) {
264
+ lines.push('\n---')
265
+ }
266
+
267
+ return lines.join('') + '\n'
268
+ }
269
+
270
+ /**
271
+ * Render panel
272
+ */
273
+ function renderPanel(node: UINode, state: RenderState): string {
274
+ const { title, collapsible, collapsed } = node.props as {
275
+ title?: string
276
+ collapsible?: boolean
277
+ collapsed?: boolean
278
+ }
279
+
280
+ const lines: string[] = []
281
+
282
+ if (title) {
283
+ lines.push(`### ${title}\n`)
284
+ }
285
+
286
+ if (node.children && Array.isArray(node.children) && node.children.length > 0) {
287
+ const childContent = node.children.map((child: UINode) => renderNode(child, state)).join('\n')
288
+ lines.push(childContent)
289
+ }
290
+
291
+ return lines.join('') + '\n'
292
+ }
293
+
294
+ /**
295
+ * Render card
296
+ */
297
+ function renderCard(node: UINode, state: RenderState): string {
298
+ const { title, subtitle, badge, titleAction, pairs, actions } = node.props as {
299
+ title?: string
300
+ subtitle?: string
301
+ badge?: { content: string; variant?: string }
302
+ titleAction?: { label: string; action?: string }
303
+ pairs?: Array<{ key: string; value: unknown }>
304
+ actions?: Array<{ label: string; action?: string }>
305
+ }
306
+ const lines: string[] = []
307
+
308
+ // Title section
309
+ if (title) {
310
+ let titleLine = `### ${title}`
311
+ if (badge) {
312
+ titleLine += ` [${badge.content}]`
313
+ }
314
+ lines.push(titleLine)
315
+ if (titleAction) {
316
+ lines.push(`[${titleAction.label}](${titleAction.action ?? ''})`)
317
+ }
318
+ lines.push('')
319
+ }
320
+
321
+ if (subtitle) {
322
+ lines.push(`*${subtitle}*\n`)
323
+ }
324
+
325
+ // Key-value pairs
326
+ if (pairs && pairs.length > 0) {
327
+ for (const pair of pairs) {
328
+ const val = pair.value != null ? String(pair.value) : ''
329
+ lines.push(`**${pair.key}:** ${val}`)
330
+ }
331
+ lines.push('')
332
+ }
333
+
334
+ // Children content
335
+ if (node.children && Array.isArray(node.children) && node.children.length > 0) {
336
+ const childContent = node.children.map((child: UINode) => renderNode(child, state)).join('\n')
337
+ lines.push(childContent)
338
+ }
339
+
340
+ // Actions
341
+ if (actions && actions.length > 0) {
342
+ for (const action of actions) {
343
+ lines.push(`[${action.label}](${action.action ?? ''})`)
344
+ }
345
+ lines.push('')
346
+ }
347
+
348
+ return lines.join('\n')
349
+ }
350
+
351
+ /**
352
+ * Render sidebar
353
+ */
354
+ function renderSidebar(node: UINode): string {
355
+ const { nav, sections } = node.props as {
356
+ nav?: { label: string; href?: string; active?: boolean; children?: { label: string; href?: string }[] }[]
357
+ sections?: { title: string; items: { label: string; href?: string }[] }[]
358
+ }
359
+
360
+ const lines: string[] = []
361
+
362
+ if (sections) {
363
+ for (const section of sections) {
364
+ lines.push(`### ${section.title}\n`)
365
+ for (const item of section.items) {
366
+ lines.push(`- [${item.label}](${item.href ?? ''})`)
367
+ }
368
+ lines.push('')
369
+ }
370
+ }
371
+
372
+ if (nav) {
373
+ for (const item of nav) {
374
+ if (item.active) {
375
+ lines.push(`- **[${item.label}](${item.href ?? ''})**`)
376
+ } else {
377
+ lines.push(`- [${item.label}](${item.href ?? ''})`)
378
+ }
379
+
380
+ if (item.children) {
381
+ for (const child of item.children) {
382
+ lines.push(` - [${child.label}](${child.href ?? ''})`)
383
+ }
384
+ }
385
+ }
386
+ lines.push('')
387
+ }
388
+
389
+ return lines.join('\n')
390
+ }
391
+
392
+ /**
393
+ * Render breadcrumb
394
+ */
395
+ function renderBreadcrumb(node: UINode): string {
396
+ const { items, separator } = node.props as {
397
+ items?: { label: string; path?: string }[]
398
+ separator?: string
399
+ }
400
+
401
+ if (!items || items.length === 0) return ''
402
+
403
+ const sep = separator ?? ' > '
404
+ const parts = items.map((item, index) => {
405
+ const isLast = index === items.length - 1
406
+ if (isLast || !item.path) {
407
+ return item.label
408
+ }
409
+ return `[${item.label}](${item.path})`
410
+ })
411
+
412
+ return parts.join(sep) + '\n'
413
+ }
414
+
415
+ /**
416
+ * Render badge
417
+ */
418
+ function renderBadge(node: UINode): string {
419
+ const { children, variant } = node.props as { children?: string; variant?: string }
420
+ return `[${children ?? ''}]`
421
+ }
422
+
423
+ /**
424
+ * Render dialog
425
+ */
426
+ function renderDialog(node: UINode, state: RenderState): string {
427
+ const { title, open, actions } = node.props as {
428
+ title?: string
429
+ open?: boolean
430
+ actions?: { label: string; action: string }[]
431
+ }
432
+
433
+ // Don't render closed dialogs
434
+ if (open === false) return ''
435
+
436
+ const lines: string[] = []
437
+
438
+ if (title) {
439
+ lines.push(`### ${title}\n`)
440
+ }
441
+
442
+ if (node.children && Array.isArray(node.children) && node.children.length > 0) {
443
+ const childContent = node.children.map((child: UINode) => renderNode(child, state)).join('\n')
444
+ lines.push(childContent)
445
+ }
446
+
447
+ if (actions && actions.length > 0) {
448
+ lines.push('')
449
+ for (const action of actions) {
450
+ lines.push(`[${action.label}](${action.action})`)
451
+ }
452
+ }
453
+
454
+ return lines.join('') + '\n'
455
+ }
456
+
457
+ /**
458
+ * Render spinner
459
+ */
460
+ function renderSpinner(node: UINode): string {
461
+ const { label } = node.props as { label?: string }
462
+ return label ?? 'Loading...'
463
+ }
464
+
465
+ /**
466
+ * Render metrics (multiple metrics)
467
+ */
468
+ function renderMetrics(node: UINode): string {
469
+ const { metrics } = node.props as {
470
+ metrics?: Array<{
471
+ label: string
472
+ value: unknown
473
+ format?: string
474
+ unit?: string
475
+ trend?: string
476
+ }>
477
+ }
478
+
479
+ if (!metrics || metrics.length === 0) return ''
480
+
481
+ const lines: string[] = []
482
+ for (const m of metrics) {
483
+ const val = m.value != null ? String(m.value) : ''
484
+ let formatted = val
485
+ if (m.format === 'percentage' && !val.includes('%')) {
486
+ formatted = `${val}%`
487
+ }
488
+ if (m.unit) {
489
+ formatted = `${formatted} ${m.unit}`
490
+ }
491
+ lines.push(`**${m.label}:** ${formatted}`)
492
+ }
493
+
494
+ return lines.join('\n') + '\n'
495
+ }
496
+
497
+ /**
498
+ * Render single metric
499
+ */
500
+ function renderSingleMetric(node: UINode): string {
501
+ const { label, value, format, unit } = node.props as {
502
+ label?: string
503
+ value?: unknown
504
+ format?: string
505
+ unit?: string
506
+ }
507
+
508
+ if (!label) return ''
509
+
510
+ const val = value != null ? String(value) : ''
511
+ let formatted = val
512
+ if (format === 'percentage' && !val.includes('%')) {
513
+ formatted = `${val}%`
514
+ }
515
+ if (unit) {
516
+ formatted = `${formatted} ${unit}`
517
+ }
518
+
519
+ return `**${label}:** ${formatted}\n`
520
+ }
521
+
522
+ /**
523
+ * Render dashboard
524
+ */
525
+ function renderDashboard(node: UINode, state: RenderState): string {
526
+ const { title, metrics } = node.props as {
527
+ title?: string
528
+ metrics?: { label: string; value: string | number; trend?: 'up' | 'down' }[]
529
+ }
530
+
531
+ const lines: string[] = []
532
+
533
+ if (title) {
534
+ lines.push(`# ${title}\n`)
535
+ }
536
+
537
+ if (metrics && metrics.length > 0) {
538
+ // Render metrics as a table
539
+ lines.push('| Metric | Value | Trend |')
540
+ lines.push('| :--- | :--- | :--- |')
541
+ for (const metric of metrics) {
542
+ const trend = metric.trend === 'up' ? 'Up' : metric.trend === 'down' ? 'Down' : ''
543
+ lines.push(`| ${metric.label} | ${metric.value} | ${trend} |`)
544
+ }
545
+ lines.push('')
546
+ }
547
+
548
+ if (node.children && Array.isArray(node.children) && node.children.length > 0) {
549
+ const childContent = node.children.map((child: UINode) => renderNode(child, state)).join('\n')
550
+ lines.push(childContent)
551
+ }
552
+
553
+ return lines.join('\n')
554
+ }
555
+
556
+ /**
557
+ * Render settings
558
+ */
559
+ function renderSettings(node: UINode, state: RenderState): string {
560
+ const { sections } = node.props as { sections?: string[] }
561
+
562
+ const lines: string[] = []
563
+
564
+ if (sections && sections.length > 0) {
565
+ for (const section of sections) {
566
+ lines.push(`### ${section.charAt(0).toUpperCase() + section.slice(1)}\n`)
567
+ }
568
+ }
569
+
570
+ if (node.children && Array.isArray(node.children) && node.children.length > 0) {
571
+ const childContent = node.children.map((child: UINode) => renderNode(child, state)).join('\n')
572
+ lines.push(childContent)
573
+ }
574
+
575
+ return lines.join('\n')
576
+ }
577
+
578
+ /**
579
+ * Render input
580
+ */
581
+ function renderInput(node: UINode): string {
582
+ const { label, value, placeholder, disabled } = node.props as {
583
+ label?: string
584
+ value?: string
585
+ placeholder?: string
586
+ disabled?: boolean
587
+ }
588
+
589
+ const displayValue = value ?? placeholder ?? ''
590
+ return `**${label ?? 'Input'}:** ${displayValue}`
591
+ }
592
+
593
+ /**
594
+ * Render select
595
+ */
596
+ function renderSelect(node: UINode): string {
597
+ const { label, value, options } = node.props as {
598
+ label?: string
599
+ value?: string
600
+ options?: { label: string; value: string }[]
601
+ }
602
+
603
+ const lines: string[] = []
604
+
605
+ if (label) {
606
+ lines.push(`**${label}:**`)
607
+ }
608
+
609
+ if (options && options.length > 0) {
610
+ const selectedOption = options.find((opt) => opt.value === value)
611
+ if (selectedOption) {
612
+ lines.push(`Selected: ${selectedOption.label}`)
613
+ }
614
+ lines.push('Options:')
615
+ for (const opt of options) {
616
+ lines.push(`- ${opt.label}`)
617
+ }
618
+ }
619
+
620
+ return lines.join('\n') + '\n'
621
+ }
622
+
623
+ /**
624
+ * Render nav-footer
625
+ */
626
+ function renderNavFooter(node: UINode): string {
627
+ const { actions } = node.props as {
628
+ actions?: { key: string; label: string }[]
629
+ }
630
+
631
+ if (!actions || actions.length === 0) return ''
632
+
633
+ const parts = actions.map((action) => `\`${action.key}\` ${action.label}`)
634
+ return parts.join(' | ') + '\n'
635
+ }
636
+
637
+ /**
638
+ * Render hero
639
+ */
640
+ function renderHero(node: UINode): string {
641
+ const { title, subtitle, badge, callToAction, secondaryCallToAction, actions } = node.props as {
642
+ title?: string
643
+ subtitle?: string
644
+ badge?: string
645
+ callToAction?: string
646
+ secondaryCallToAction?: string
647
+ actions?: { primary?: string; secondary?: string }
648
+ }
649
+
650
+ const lines: string[] = []
651
+
652
+ if (badge) {
653
+ lines.push(`[${badge}]\n`)
654
+ }
655
+
656
+ if (title) {
657
+ lines.push(`# ${title}\n`)
658
+ }
659
+
660
+ if (subtitle) {
661
+ lines.push(`${subtitle}\n`)
662
+ }
663
+
664
+ if (callToAction) {
665
+ lines.push(`[${callToAction}](${actions?.primary ?? ''})`)
666
+ }
667
+
668
+ if (secondaryCallToAction) {
669
+ lines.push(`[${secondaryCallToAction}](${actions?.secondary ?? ''})`)
670
+ }
671
+
672
+ return lines.join('\n') + '\n'
673
+ }
674
+
675
+ /**
676
+ * Render features
677
+ */
678
+ function renderFeatures(node: UINode): string {
679
+ const { title, features } = node.props as {
680
+ title?: string
681
+ features?: { title: string; description: string; icon?: string }[]
682
+ }
683
+
684
+ const lines: string[] = []
685
+
686
+ if (title) {
687
+ lines.push(`## ${title}\n`)
688
+ }
689
+
690
+ if (features && features.length > 0) {
691
+ for (const feature of features) {
692
+ lines.push(`### ${feature.title}`)
693
+ lines.push(feature.description)
694
+ lines.push('')
695
+ }
696
+ }
697
+
698
+ return lines.join('\n')
699
+ }
700
+
701
+ /**
702
+ * Render pricing
703
+ */
704
+ function renderPricing(node: UINode): string {
705
+ const { tiers } = node.props as {
706
+ tiers?: {
707
+ name: string
708
+ price: string
709
+ features: string[]
710
+ highlighted?: boolean
711
+ callToAction?: string
712
+ }[]
713
+ }
714
+
715
+ if (!tiers || tiers.length === 0) return ''
716
+
717
+ const lines: string[] = []
718
+
719
+ for (const tier of tiers) {
720
+ lines.push(`### ${tier.name}`)
721
+ lines.push(`**${tier.price}**`)
722
+ lines.push('')
723
+ if (tier.features && tier.features.length > 0) {
724
+ for (const feature of tier.features) {
725
+ lines.push(`- ${feature}`)
726
+ }
727
+ lines.push('')
728
+ }
729
+ if (tier.callToAction) {
730
+ lines.push(`[${tier.callToAction}]()\n`)
731
+ }
732
+ }
733
+
734
+ return lines.join('\n')
735
+ }
736
+
737
+ /**
738
+ * Render FAQ
739
+ */
740
+ function renderFAQ(node: UINode): string {
741
+ const { title, items } = node.props as {
742
+ title?: string
743
+ items?: { question: string; answer: string }[]
744
+ }
745
+
746
+ const lines: string[] = []
747
+
748
+ if (title) {
749
+ lines.push(`## ${title}\n`)
750
+ }
751
+
752
+ if (items && items.length > 0) {
753
+ for (const item of items) {
754
+ lines.push(`**${item.question}**`)
755
+ lines.push(item.answer)
756
+ lines.push('')
757
+ }
758
+ }
759
+
760
+ return lines.join('\n')
761
+ }
762
+
763
+ /**
764
+ * Render footer
765
+ */
766
+ function renderFooter(node: UINode): string {
767
+ const { links, copyright, social } = node.props as {
768
+ links?: { title: string; links: { label: string; href: string }[] }[]
769
+ copyright?: string
770
+ social?: { platform: string; href: string }[]
771
+ }
772
+
773
+ const lines: string[] = []
774
+ lines.push('---\n')
775
+
776
+ if (links && links.length > 0) {
777
+ for (const section of links) {
778
+ lines.push(`### ${section.title}`)
779
+ for (const link of section.links) {
780
+ lines.push(`- [${link.label}](${link.href})`)
781
+ }
782
+ lines.push('')
783
+ }
784
+ }
785
+
786
+ if (social && social.length > 0) {
787
+ const socialLinks = social.map((s) => `[${s.platform}](${s.href})`).join(' | ')
788
+ lines.push(socialLinks)
789
+ lines.push('')
790
+ }
791
+
792
+ if (copyright) {
793
+ lines.push(`${copyright}\n`)
794
+ }
795
+
796
+ return lines.join('\n')
797
+ }
798
+
799
+ /**
800
+ * Render header (site header, not heading)
801
+ */
802
+ function renderSiteHeader(node: UINode): string {
803
+ const { nav, callToAction, actions, breadcrumbs } = node.props as {
804
+ nav?: { label: string; href: string }[]
805
+ callToAction?: string
806
+ actions?: { primary?: string }
807
+ breadcrumbs?: { label: string; href?: string }[]
808
+ }
809
+
810
+ const lines: string[] = []
811
+
812
+ if (nav && nav.length > 0) {
813
+ const navLinks = nav.map((item) => `[${item.label}](${item.href})`).join(' | ')
814
+ lines.push(navLinks)
815
+ }
816
+
817
+ if (callToAction) {
818
+ lines.push(`[${callToAction}](${actions?.primary ?? ''})`)
819
+ }
820
+
821
+ if (breadcrumbs && breadcrumbs.length > 0) {
822
+ const crumbs = breadcrumbs.map((b, i) => {
823
+ if (b.href && i < breadcrumbs.length - 1) {
824
+ return `[${b.label}](${b.href})`
825
+ }
826
+ return b.label
827
+ }).join(' > ')
828
+ lines.push(crumbs)
829
+ }
830
+
831
+ return lines.join('\n') + '\n'
832
+ }
833
+
834
+ /**
835
+ * Main render function for a UINode
836
+ */
837
+ function renderNode(node: UINode, state: RenderState = { indent: 0 }): string {
838
+ // Check for site header first (header with nav or breadcrumbs props)
839
+ if (node.type === 'header' && (node.props?.nav || node.props?.breadcrumbs || node.props?.callToAction)) {
840
+ return renderSiteHeader(node)
841
+ }
842
+
843
+ switch (node.type) {
844
+ case 'text':
845
+ return renderText(node)
846
+
847
+ case 'header':
848
+ return renderHeader(node)
849
+
850
+ case 'list':
851
+ return renderList(node, state)
852
+
853
+ case 'table':
854
+ return renderTable(node)
855
+
856
+ case 'code':
857
+ return renderCode(node)
858
+
859
+ case 'link':
860
+ return renderLink(node)
861
+
862
+ case 'button':
863
+ return renderButton(node)
864
+
865
+ case 'box':
866
+ return renderBox(node, state)
867
+
868
+ case 'panel':
869
+ return renderPanel(node, state)
870
+
871
+ case 'card':
872
+ return renderCard(node, state)
873
+
874
+ case 'sidebar':
875
+ return renderSidebar(node)
876
+
877
+ case 'breadcrumb':
878
+ return renderBreadcrumb(node)
879
+
880
+ case 'badge':
881
+ return renderBadge(node)
882
+
883
+ case 'dialog':
884
+ return renderDialog(node, state)
885
+
886
+ case 'spinner':
887
+ return renderSpinner(node)
888
+
889
+ case 'metrics':
890
+ return renderMetrics(node)
891
+
892
+ case 'metric':
893
+ return renderSingleMetric(node)
894
+
895
+ case 'dashboard':
896
+ return renderDashboard(node, state)
897
+
898
+ case 'settings':
899
+ return renderSettings(node, state)
900
+
901
+ case 'input':
902
+ return renderInput(node)
903
+
904
+ case 'select':
905
+ return renderSelect(node)
906
+
907
+ case 'nav-footer':
908
+ return renderNavFooter(node)
909
+
910
+ case 'hero':
911
+ return renderHero(node)
912
+
913
+ case 'features':
914
+ return renderFeatures(node)
915
+
916
+ case 'pricing':
917
+ return renderPricing(node)
918
+
919
+ case 'faq':
920
+ return renderFAQ(node)
921
+
922
+ case 'footer':
923
+ return renderFooter(node)
924
+
925
+ default: {
926
+ // Unknown type - render children if present
927
+ const defaultChildren: UINode[] = Array.isArray(node.children) ? node.children : []
928
+ if (defaultChildren.length > 0) {
929
+ return defaultChildren.map((child: UINode) => renderNode(child, state)).join('\n')
930
+ }
931
+ return ''
932
+ }
933
+ }
934
+ }
935
+
936
+ /**
937
+ * Renders a UINode tree to a Markdown string for AI agent consumption.
938
+ *
939
+ * @param node - The UINode tree to render
940
+ * @param options - Optional rendering configuration
941
+ * @returns Markdown string
942
+ */
943
+ export function renderMarkdown(node: UINode, options?: MarkdownRenderOptions): string {
944
+ const result = renderNode(node, { indent: 0 })
945
+
946
+ // Clean up: remove trailing whitespace from each line
947
+ const lines = result.split('\n').map((line) => line.trimEnd())
948
+
949
+ return lines.join('\n')
950
+ }