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