@wireweave/core 1.0.0-beta.20260107130839 → 1.0.0-beta.20260107133417
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/dist/index.cjs +0 -1
- package/dist/index.js +0 -1
- package/dist/parser.cjs +0 -1
- package/dist/parser.js +0 -1
- package/dist/renderer.cjs +0 -1
- package/dist/renderer.js +0 -1
- package/package.json +7 -4
- package/dist/index.cjs.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/parser.cjs.map +0 -1
- package/dist/parser.js.map +0 -1
- package/dist/renderer.cjs.map +0 -1
- package/dist/renderer.js.map +0 -1
- package/src/ast/guards.ts +0 -361
- package/src/ast/index.ts +0 -9
- package/src/ast/types.ts +0 -661
- package/src/ast/utils.ts +0 -238
- package/src/grammar/wireframe.peggy +0 -677
- package/src/icons/lucide-icons.ts +0 -46422
- package/src/index.ts +0 -20
- package/src/parser/generated-parser.js +0 -5199
- package/src/parser/index.ts +0 -214
- package/src/renderer/html/base.ts +0 -186
- package/src/renderer/html/components.ts +0 -1092
- package/src/renderer/html/index.ts +0 -1608
- package/src/renderer/html/layout.ts +0 -392
- package/src/renderer/index.ts +0 -143
- package/src/renderer/styles-components.ts +0 -1232
- package/src/renderer/styles.ts +0 -382
- package/src/renderer/svg/index.ts +0 -1050
- package/src/renderer/types.ts +0 -173
- package/src/types/index.ts +0 -138
- package/src/viewport/index.ts +0 -17
- package/src/viewport/presets.ts +0 -181
|
@@ -1,1608 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HTML Renderer for wireweave
|
|
3
|
-
*
|
|
4
|
-
* Converts AST nodes to HTML output
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type {
|
|
8
|
-
AnyNode,
|
|
9
|
-
PageNode,
|
|
10
|
-
HeaderNode,
|
|
11
|
-
MainNode,
|
|
12
|
-
FooterNode,
|
|
13
|
-
SidebarNode,
|
|
14
|
-
SectionNode,
|
|
15
|
-
RowNode,
|
|
16
|
-
ColNode,
|
|
17
|
-
CardNode,
|
|
18
|
-
ModalNode,
|
|
19
|
-
DrawerNode,
|
|
20
|
-
AccordionNode,
|
|
21
|
-
TextNode,
|
|
22
|
-
TitleNode,
|
|
23
|
-
LinkNode,
|
|
24
|
-
InputNode,
|
|
25
|
-
TextareaNode,
|
|
26
|
-
SelectNode,
|
|
27
|
-
CheckboxNode,
|
|
28
|
-
RadioNode,
|
|
29
|
-
SwitchNode,
|
|
30
|
-
SliderNode,
|
|
31
|
-
ButtonNode,
|
|
32
|
-
ImageNode,
|
|
33
|
-
PlaceholderNode,
|
|
34
|
-
AvatarNode,
|
|
35
|
-
BadgeNode,
|
|
36
|
-
IconNode,
|
|
37
|
-
TableNode,
|
|
38
|
-
ListNode,
|
|
39
|
-
AlertNode,
|
|
40
|
-
ToastNode,
|
|
41
|
-
ProgressNode,
|
|
42
|
-
SpinnerNode,
|
|
43
|
-
TooltipNode,
|
|
44
|
-
PopoverNode,
|
|
45
|
-
DropdownNode,
|
|
46
|
-
NavNode,
|
|
47
|
-
TabsNode,
|
|
48
|
-
BreadcrumbNode,
|
|
49
|
-
DividerComponentNode,
|
|
50
|
-
CommonProps,
|
|
51
|
-
ValueWithUnit,
|
|
52
|
-
SpacingValue,
|
|
53
|
-
} from '../../ast/types';
|
|
54
|
-
// hasChildren guard available from ast/guards if needed
|
|
55
|
-
import { BaseRenderer } from './base';
|
|
56
|
-
import type { RenderOptions } from '../types';
|
|
57
|
-
import { resolveViewport } from '../../viewport';
|
|
58
|
-
import { getIconData, renderIconSvg } from '../../icons/lucide-icons';
|
|
59
|
-
|
|
60
|
-
// Re-export layout and component utilities
|
|
61
|
-
export * from './layout';
|
|
62
|
-
export * from './components';
|
|
63
|
-
|
|
64
|
-
// Import size resolution helper for use in HTMLRenderer
|
|
65
|
-
import { resolveSizeValue, buildClassString as _buildClassString } from './components';
|
|
66
|
-
|
|
67
|
-
// Spacing token table: number -> CSS value
|
|
68
|
-
// Token values (0-20) map to px values
|
|
69
|
-
const SPACING_TOKENS: Record<number, string> = {
|
|
70
|
-
0: '0px',
|
|
71
|
-
1: '4px',
|
|
72
|
-
2: '8px',
|
|
73
|
-
3: '12px',
|
|
74
|
-
4: '16px',
|
|
75
|
-
5: '20px',
|
|
76
|
-
6: '24px',
|
|
77
|
-
8: '32px',
|
|
78
|
-
10: '40px',
|
|
79
|
-
12: '48px',
|
|
80
|
-
16: '64px',
|
|
81
|
-
20: '80px',
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Resolve a spacing value to CSS string
|
|
86
|
-
* - number: spacing token (e.g., 4 → '16px')
|
|
87
|
-
* - ValueWithUnit: direct CSS value (e.g., { value: 16, unit: 'px' } → '16px')
|
|
88
|
-
*/
|
|
89
|
-
function resolveSpacingValue(value: SpacingValue | undefined): string | undefined {
|
|
90
|
-
if (value === undefined) return undefined;
|
|
91
|
-
|
|
92
|
-
// ValueWithUnit object: direct CSS value
|
|
93
|
-
if (typeof value === 'object' && 'value' in value && 'unit' in value) {
|
|
94
|
-
return `${value.value}${value.unit}`;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Number: spacing token
|
|
98
|
-
if (typeof value === 'number') {
|
|
99
|
-
// Look up token value, fallback to direct px if not in token table
|
|
100
|
-
return SPACING_TOKENS[value] ?? `${value}px`;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return undefined;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Type guard to check if a value is a ValueWithUnit object
|
|
108
|
-
*/
|
|
109
|
-
function isValueWithUnit(value: unknown): value is ValueWithUnit {
|
|
110
|
-
return typeof value === 'object' && value !== null && 'value' in value && 'unit' in value;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Resolve a size value (width/height) to CSS string
|
|
115
|
-
* - number: direct px value (dimensions are not tokenized)
|
|
116
|
-
* - ValueWithUnit: direct CSS value with unit
|
|
117
|
-
*/
|
|
118
|
-
function resolveSizeValueToCss(value: number | ValueWithUnit | undefined): string | undefined {
|
|
119
|
-
if (value === undefined) return undefined;
|
|
120
|
-
|
|
121
|
-
// ValueWithUnit object: direct CSS value
|
|
122
|
-
if (isValueWithUnit(value)) {
|
|
123
|
-
return `${value.value}${value.unit}`;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Number: direct px value (for width/height)
|
|
127
|
-
if (typeof value === 'number') {
|
|
128
|
-
return `${value}px`;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return undefined;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* HTML Renderer class
|
|
136
|
-
*
|
|
137
|
-
* Renders wireframe AST to semantic HTML with utility classes
|
|
138
|
-
*/
|
|
139
|
-
export class HtmlRenderer extends BaseRenderer {
|
|
140
|
-
constructor(options: RenderOptions = {}) {
|
|
141
|
-
super(options);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Render a page node
|
|
146
|
-
*/
|
|
147
|
-
protected renderPage(node: PageNode): string {
|
|
148
|
-
// Resolve viewport size - use explicit w/h if provided, otherwise use viewport/device
|
|
149
|
-
let viewport = resolveViewport(node.viewport, node.device);
|
|
150
|
-
|
|
151
|
-
// Override with explicit width/height if provided (for playground-style sizing)
|
|
152
|
-
// Support both short form (w/h) and long form (width/height)
|
|
153
|
-
const nodeAny = node as PageNode & { width?: number; height?: number };
|
|
154
|
-
const explicitW = node.w ?? nodeAny.width;
|
|
155
|
-
const explicitH = node.h ?? nodeAny.height;
|
|
156
|
-
|
|
157
|
-
if (explicitW !== undefined || explicitH !== undefined) {
|
|
158
|
-
const explicitWidth = typeof explicitW === 'number' ? explicitW :
|
|
159
|
-
(typeof explicitW === 'string' && /^\d+$/.test(explicitW) ? parseInt(explicitW) : viewport.width);
|
|
160
|
-
const explicitHeight = typeof explicitH === 'number' ? explicitH :
|
|
161
|
-
(typeof explicitH === 'string' && /^\d+$/.test(explicitH) ? parseInt(explicitH) : viewport.height);
|
|
162
|
-
viewport = {
|
|
163
|
-
width: explicitWidth,
|
|
164
|
-
height: explicitHeight,
|
|
165
|
-
label: `${explicitWidth}x${explicitHeight}`,
|
|
166
|
-
category: explicitWidth <= 430 ? 'mobile' : explicitWidth <= 1024 ? 'tablet' : 'desktop',
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const classes = this.buildClassString([
|
|
171
|
-
`${this.prefix}-page`,
|
|
172
|
-
node.centered ? `${this.prefix}-page-centered` : undefined,
|
|
173
|
-
...this.getCommonClasses(node),
|
|
174
|
-
]);
|
|
175
|
-
|
|
176
|
-
const children = this.renderChildren(node.children);
|
|
177
|
-
const title = node.title ? `<title>${this.escapeHtml(node.title)}</title>\n` : '';
|
|
178
|
-
|
|
179
|
-
// Build common styles (padding, margin, etc.) and combine with viewport dimensions
|
|
180
|
-
const commonStyles = this.buildCommonStyles(node);
|
|
181
|
-
const viewportStyle = `width: ${viewport.width}px; height: ${viewport.height}px`;
|
|
182
|
-
const combinedStyle = commonStyles ? `${viewportStyle}; ${commonStyles}` : viewportStyle;
|
|
183
|
-
|
|
184
|
-
// Add data attributes for viewport info
|
|
185
|
-
const dataAttrs = `data-viewport-width="${viewport.width}" data-viewport-height="${viewport.height}" data-viewport-label="${viewport.label}"`;
|
|
186
|
-
|
|
187
|
-
return `<div class="${classes}" style="${combinedStyle}" ${dataAttrs}>\n${title}${children}\n</div>`;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Render any AST node
|
|
192
|
-
*/
|
|
193
|
-
protected renderNode(node: AnyNode): string {
|
|
194
|
-
switch (node.type) {
|
|
195
|
-
// Layout nodes
|
|
196
|
-
case 'Page':
|
|
197
|
-
return this.renderPage(node as PageNode);
|
|
198
|
-
case 'Header':
|
|
199
|
-
return this.renderHeader(node as HeaderNode);
|
|
200
|
-
case 'Main':
|
|
201
|
-
return this.renderMain(node as MainNode);
|
|
202
|
-
case 'Footer':
|
|
203
|
-
return this.renderFooter(node as FooterNode);
|
|
204
|
-
case 'Sidebar':
|
|
205
|
-
return this.renderSidebar(node as SidebarNode);
|
|
206
|
-
case 'Section':
|
|
207
|
-
return this.renderSection(node as SectionNode);
|
|
208
|
-
|
|
209
|
-
// Grid nodes
|
|
210
|
-
case 'Row':
|
|
211
|
-
return this.renderRow(node as RowNode);
|
|
212
|
-
case 'Col':
|
|
213
|
-
return this.renderCol(node as ColNode);
|
|
214
|
-
|
|
215
|
-
// Container nodes
|
|
216
|
-
case 'Card':
|
|
217
|
-
return this.renderCard(node as CardNode);
|
|
218
|
-
case 'Modal':
|
|
219
|
-
return this.renderModal(node as ModalNode);
|
|
220
|
-
case 'Drawer':
|
|
221
|
-
return this.renderDrawer(node as DrawerNode);
|
|
222
|
-
case 'Accordion':
|
|
223
|
-
return this.renderAccordion(node as AccordionNode);
|
|
224
|
-
|
|
225
|
-
// Text nodes
|
|
226
|
-
case 'Text':
|
|
227
|
-
return this.renderText(node as TextNode);
|
|
228
|
-
case 'Title':
|
|
229
|
-
return this.renderTitle(node as TitleNode);
|
|
230
|
-
case 'Link':
|
|
231
|
-
return this.renderLink(node as LinkNode);
|
|
232
|
-
|
|
233
|
-
// Input nodes
|
|
234
|
-
case 'Input':
|
|
235
|
-
return this.renderInput(node as InputNode);
|
|
236
|
-
case 'Textarea':
|
|
237
|
-
return this.renderTextarea(node as TextareaNode);
|
|
238
|
-
case 'Select':
|
|
239
|
-
return this.renderSelect(node as SelectNode);
|
|
240
|
-
case 'Checkbox':
|
|
241
|
-
return this.renderCheckbox(node as CheckboxNode);
|
|
242
|
-
case 'Radio':
|
|
243
|
-
return this.renderRadio(node as RadioNode);
|
|
244
|
-
case 'Switch':
|
|
245
|
-
return this.renderSwitch(node as SwitchNode);
|
|
246
|
-
case 'Slider':
|
|
247
|
-
return this.renderSlider(node as SliderNode);
|
|
248
|
-
|
|
249
|
-
// Button
|
|
250
|
-
case 'Button':
|
|
251
|
-
return this.renderButton(node as ButtonNode);
|
|
252
|
-
|
|
253
|
-
// Display nodes
|
|
254
|
-
case 'Image':
|
|
255
|
-
return this.renderImage(node as ImageNode);
|
|
256
|
-
case 'Placeholder':
|
|
257
|
-
return this.renderPlaceholder(node as PlaceholderNode);
|
|
258
|
-
case 'Avatar':
|
|
259
|
-
return this.renderAvatar(node as AvatarNode);
|
|
260
|
-
case 'Badge':
|
|
261
|
-
return this.renderBadge(node as BadgeNode);
|
|
262
|
-
case 'Icon':
|
|
263
|
-
return this.renderIcon(node as IconNode);
|
|
264
|
-
|
|
265
|
-
// Data nodes
|
|
266
|
-
case 'Table':
|
|
267
|
-
return this.renderTable(node as TableNode);
|
|
268
|
-
case 'List':
|
|
269
|
-
return this.renderList(node as ListNode);
|
|
270
|
-
|
|
271
|
-
// Feedback nodes
|
|
272
|
-
case 'Alert':
|
|
273
|
-
return this.renderAlert(node as AlertNode);
|
|
274
|
-
case 'Toast':
|
|
275
|
-
return this.renderToast(node as ToastNode);
|
|
276
|
-
case 'Progress':
|
|
277
|
-
return this.renderProgress(node as ProgressNode);
|
|
278
|
-
case 'Spinner':
|
|
279
|
-
return this.renderSpinner(node as SpinnerNode);
|
|
280
|
-
|
|
281
|
-
// Overlay nodes
|
|
282
|
-
case 'Tooltip':
|
|
283
|
-
return this.renderTooltip(node as TooltipNode);
|
|
284
|
-
case 'Popover':
|
|
285
|
-
return this.renderPopover(node as PopoverNode);
|
|
286
|
-
case 'Dropdown':
|
|
287
|
-
return this.renderDropdown(node as DropdownNode);
|
|
288
|
-
|
|
289
|
-
// Navigation nodes
|
|
290
|
-
case 'Nav':
|
|
291
|
-
return this.renderNav(node as NavNode);
|
|
292
|
-
case 'Tabs':
|
|
293
|
-
return this.renderTabs(node as TabsNode);
|
|
294
|
-
case 'Breadcrumb':
|
|
295
|
-
return this.renderBreadcrumb(node as BreadcrumbNode);
|
|
296
|
-
|
|
297
|
-
// Other
|
|
298
|
-
case 'Divider':
|
|
299
|
-
return this.renderDivider(node as DividerComponentNode);
|
|
300
|
-
|
|
301
|
-
default:
|
|
302
|
-
return `<!-- Unknown node type: ${(node as AnyNode).type} -->`;
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* Render children nodes
|
|
308
|
-
*/
|
|
309
|
-
protected renderChildren(children: AnyNode[]): string {
|
|
310
|
-
return this.withDepth(() => {
|
|
311
|
-
return children
|
|
312
|
-
.map((child) => this.indent(this.renderNode(child)))
|
|
313
|
-
.join('\n');
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* Get common CSS classes from props
|
|
319
|
-
* Uses Omit to exclude 'align' since TextNode/TitleNode have incompatible align types
|
|
320
|
-
*
|
|
321
|
-
* All numeric values are handled by buildCommonStyles as inline px values.
|
|
322
|
-
* CSS classes are only used for keyword values (full, auto, screen, fit, etc.)
|
|
323
|
-
*/
|
|
324
|
-
private getCommonClasses(props: Omit<Partial<CommonProps>, 'align'> & { align?: string }): string[] {
|
|
325
|
-
const classes: string[] = [];
|
|
326
|
-
const p = this.prefix;
|
|
327
|
-
|
|
328
|
-
// Spacing - only 'auto' uses class, all numbers use inline px styles
|
|
329
|
-
if (props.mx === 'auto') classes.push(`${p}-mx-auto`);
|
|
330
|
-
|
|
331
|
-
// Size - only keyword values use classes
|
|
332
|
-
if (props.w === 'full') classes.push(`${p}-w-full`);
|
|
333
|
-
if (props.w === 'auto') classes.push(`${p}-w-auto`);
|
|
334
|
-
if (props.w === 'fit') classes.push(`${p}-w-fit`);
|
|
335
|
-
if (props.h === 'full') classes.push(`${p}-h-full`);
|
|
336
|
-
if (props.h === 'auto') classes.push(`${p}-h-auto`);
|
|
337
|
-
if (props.h === 'screen') classes.push(`${p}-h-screen`);
|
|
338
|
-
|
|
339
|
-
// Flex
|
|
340
|
-
if (props.flex === true) classes.push(`${p}-flex`);
|
|
341
|
-
if (typeof props.flex === 'number') classes.push(`${p}-flex-${props.flex}`);
|
|
342
|
-
if (props.direction === 'row') classes.push(`${p}-flex-row`);
|
|
343
|
-
if (props.direction === 'column') classes.push(`${p}-flex-col`);
|
|
344
|
-
if (props.direction === 'row-reverse') classes.push(`${p}-flex-row-reverse`);
|
|
345
|
-
if (props.direction === 'column-reverse') classes.push(`${p}-flex-col-reverse`);
|
|
346
|
-
if (props.justify) classes.push(`${p}-justify-${props.justify}`);
|
|
347
|
-
if (props.align) classes.push(`${p}-align-${props.align}`);
|
|
348
|
-
if (props.wrap === true) classes.push(`${p}-flex-wrap`);
|
|
349
|
-
if (props.wrap === 'nowrap') classes.push(`${p}-flex-nowrap`);
|
|
350
|
-
// gap is handled by inline styles for numeric values
|
|
351
|
-
|
|
352
|
-
return classes;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// ===========================================
|
|
356
|
-
// Layout Node Renderers
|
|
357
|
-
// ===========================================
|
|
358
|
-
|
|
359
|
-
private renderHeader(node: HeaderNode): string {
|
|
360
|
-
const classes = this.buildClassString([
|
|
361
|
-
`${this.prefix}-header`,
|
|
362
|
-
node.border === false ? `${this.prefix}-no-border` : undefined,
|
|
363
|
-
...this.getCommonClasses(node),
|
|
364
|
-
]);
|
|
365
|
-
|
|
366
|
-
const styles = this.buildCommonStyles(node);
|
|
367
|
-
const styleAttr = styles ? ` style="${styles}"` : '';
|
|
368
|
-
|
|
369
|
-
const children = this.renderChildren(node.children);
|
|
370
|
-
return `<header class="${classes}"${styleAttr}>\n${children}\n</header>`;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
private renderMain(node: MainNode): string {
|
|
374
|
-
const classes = this.buildClassString([
|
|
375
|
-
`${this.prefix}-main`,
|
|
376
|
-
...this.getCommonClasses(node),
|
|
377
|
-
]);
|
|
378
|
-
|
|
379
|
-
const styles = this.buildCommonStyles(node);
|
|
380
|
-
const styleAttr = styles ? ` style="${styles}"` : '';
|
|
381
|
-
|
|
382
|
-
const children = this.renderChildren(node.children);
|
|
383
|
-
return `<main class="${classes}"${styleAttr}>\n${children}\n</main>`;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
private renderFooter(node: FooterNode): string {
|
|
387
|
-
const classes = this.buildClassString([
|
|
388
|
-
`${this.prefix}-footer`,
|
|
389
|
-
node.border === false ? `${this.prefix}-no-border` : undefined,
|
|
390
|
-
...this.getCommonClasses(node),
|
|
391
|
-
]);
|
|
392
|
-
|
|
393
|
-
const styles = this.buildCommonStyles(node);
|
|
394
|
-
const styleAttr = styles ? ` style="${styles}"` : '';
|
|
395
|
-
|
|
396
|
-
const children = this.renderChildren(node.children);
|
|
397
|
-
return `<footer class="${classes}"${styleAttr}>\n${children}\n</footer>`;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
private renderSidebar(node: SidebarNode): string {
|
|
401
|
-
const classes = this.buildClassString([
|
|
402
|
-
`${this.prefix}-sidebar`,
|
|
403
|
-
node.position === 'right' ? `${this.prefix}-sidebar-right` : undefined,
|
|
404
|
-
...this.getCommonClasses(node),
|
|
405
|
-
]);
|
|
406
|
-
|
|
407
|
-
const styles = this.buildCommonStyles(node);
|
|
408
|
-
const styleAttr = styles ? ` style="${styles}"` : '';
|
|
409
|
-
|
|
410
|
-
const children = this.renderChildren(node.children);
|
|
411
|
-
return `<aside class="${classes}"${styleAttr}>\n${children}\n</aside>`;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
private renderSection(node: SectionNode): string {
|
|
415
|
-
const classes = this.buildClassString([
|
|
416
|
-
`${this.prefix}-section`,
|
|
417
|
-
...this.getCommonClasses(node),
|
|
418
|
-
]);
|
|
419
|
-
|
|
420
|
-
const styles = this.buildCommonStyles(node);
|
|
421
|
-
const styleAttr = styles ? ` style="${styles}"` : '';
|
|
422
|
-
|
|
423
|
-
const title = node.title
|
|
424
|
-
? `<h2 class="${this.prefix}-title">${this.escapeHtml(node.title)}</h2>\n`
|
|
425
|
-
: '';
|
|
426
|
-
const children = this.renderChildren(node.children);
|
|
427
|
-
return `<section class="${classes}"${styleAttr}>\n${title}${children}\n</section>`;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
// ===========================================
|
|
431
|
-
// Grid Node Renderers
|
|
432
|
-
// ===========================================
|
|
433
|
-
|
|
434
|
-
private renderRow(node: RowNode): string {
|
|
435
|
-
const classes = this.buildClassString([
|
|
436
|
-
`${this.prefix}-row`,
|
|
437
|
-
...this.getCommonClasses(node),
|
|
438
|
-
]);
|
|
439
|
-
|
|
440
|
-
const styles = this.buildCommonStyles(node);
|
|
441
|
-
const styleAttr = styles ? ` style="${styles}"` : '';
|
|
442
|
-
|
|
443
|
-
const children = this.renderChildren(node.children);
|
|
444
|
-
return `<div class="${classes}"${styleAttr}>\n${children}\n</div>`;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
private renderCol(node: ColNode): string {
|
|
448
|
-
const classes = this.buildClassString([
|
|
449
|
-
`${this.prefix}-col`,
|
|
450
|
-
node.span ? `${this.prefix}-col-${node.span}` : undefined,
|
|
451
|
-
// Responsive breakpoint classes
|
|
452
|
-
node.sm ? `${this.prefix}-col-sm-${node.sm}` : undefined,
|
|
453
|
-
node.md ? `${this.prefix}-col-md-${node.md}` : undefined,
|
|
454
|
-
node.lg ? `${this.prefix}-col-lg-${node.lg}` : undefined,
|
|
455
|
-
node.xl ? `${this.prefix}-col-xl-${node.xl}` : undefined,
|
|
456
|
-
...this.getCommonClasses(node),
|
|
457
|
-
]);
|
|
458
|
-
|
|
459
|
-
// Build inline styles for numeric width/height and order
|
|
460
|
-
const styles = this.buildColStyles(node);
|
|
461
|
-
const styleAttr = styles ? ` style="${styles}"` : '';
|
|
462
|
-
|
|
463
|
-
const children = this.renderChildren(node.children);
|
|
464
|
-
return `<div class="${classes}"${styleAttr}>\n${children}\n</div>`;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
/**
|
|
468
|
-
* Build common inline styles for all values
|
|
469
|
-
*
|
|
470
|
-
* Spacing values (p, m, gap) use token system:
|
|
471
|
-
* - number: spacing token (e.g., p=4 → padding: 16px from token table)
|
|
472
|
-
* - ValueWithUnit: direct CSS value (e.g., p=16px → padding: 16px)
|
|
473
|
-
*
|
|
474
|
-
* Size values (w, h, minW, maxW, minH, maxH) use direct px:
|
|
475
|
-
* - number: direct px value (e.g., w=400 → width: 400px)
|
|
476
|
-
* - ValueWithUnit: direct CSS value (e.g., w=50% → width: 50%)
|
|
477
|
-
*
|
|
478
|
-
* Token table: 0=0px, 1=4px, 2=8px, 3=12px, 4=16px, 5=20px, 6=24px, 8=32px, etc.
|
|
479
|
-
*
|
|
480
|
-
* Uses Omit to exclude 'align' since TextNode/TitleNode have incompatible align types
|
|
481
|
-
*/
|
|
482
|
-
private buildCommonStyles(props: Omit<Partial<CommonProps>, 'align'> & { align?: string }): string {
|
|
483
|
-
const styles: string[] = [];
|
|
484
|
-
|
|
485
|
-
// Width (direct px or ValueWithUnit)
|
|
486
|
-
const wValue = resolveSizeValueToCss(props.w as number | ValueWithUnit | undefined);
|
|
487
|
-
if (wValue) {
|
|
488
|
-
styles.push(`width: ${wValue}`);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
// Height (direct px or ValueWithUnit)
|
|
492
|
-
// Also set min-height to override CSS defaults (e.g., placeholder min-height: 100px)
|
|
493
|
-
const hValue = resolveSizeValueToCss(props.h as number | ValueWithUnit | undefined);
|
|
494
|
-
if (hValue) {
|
|
495
|
-
styles.push(`height: ${hValue}`);
|
|
496
|
-
styles.push(`min-height: ${hValue}`);
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
// Min/Max Width
|
|
500
|
-
const minWValue = resolveSizeValueToCss(props.minW);
|
|
501
|
-
if (minWValue) {
|
|
502
|
-
styles.push(`min-width: ${minWValue}`);
|
|
503
|
-
}
|
|
504
|
-
const maxWValue = resolveSizeValueToCss(props.maxW);
|
|
505
|
-
if (maxWValue) {
|
|
506
|
-
styles.push(`max-width: ${maxWValue}`);
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
// Min/Max Height
|
|
510
|
-
const minHValue = resolveSizeValueToCss(props.minH);
|
|
511
|
-
if (minHValue) {
|
|
512
|
-
styles.push(`min-height: ${minHValue}`);
|
|
513
|
-
}
|
|
514
|
-
const maxHValue = resolveSizeValueToCss(props.maxH);
|
|
515
|
-
if (maxHValue) {
|
|
516
|
-
styles.push(`max-height: ${maxHValue}`);
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
// Padding - uses spacing tokens
|
|
520
|
-
const pValue = resolveSpacingValue(props.p);
|
|
521
|
-
if (pValue) {
|
|
522
|
-
styles.push(`padding: ${pValue}`);
|
|
523
|
-
}
|
|
524
|
-
const pxValue = resolveSpacingValue(props.px);
|
|
525
|
-
if (pxValue) {
|
|
526
|
-
styles.push(`padding-left: ${pxValue}`);
|
|
527
|
-
styles.push(`padding-right: ${pxValue}`);
|
|
528
|
-
}
|
|
529
|
-
const pyValue = resolveSpacingValue(props.py);
|
|
530
|
-
if (pyValue) {
|
|
531
|
-
styles.push(`padding-top: ${pyValue}`);
|
|
532
|
-
styles.push(`padding-bottom: ${pyValue}`);
|
|
533
|
-
}
|
|
534
|
-
const ptValue = resolveSpacingValue(props.pt);
|
|
535
|
-
if (ptValue) {
|
|
536
|
-
styles.push(`padding-top: ${ptValue}`);
|
|
537
|
-
}
|
|
538
|
-
const prValue = resolveSpacingValue(props.pr);
|
|
539
|
-
if (prValue) {
|
|
540
|
-
styles.push(`padding-right: ${prValue}`);
|
|
541
|
-
}
|
|
542
|
-
const pbValue = resolveSpacingValue(props.pb);
|
|
543
|
-
if (pbValue) {
|
|
544
|
-
styles.push(`padding-bottom: ${pbValue}`);
|
|
545
|
-
}
|
|
546
|
-
const plValue = resolveSpacingValue(props.pl);
|
|
547
|
-
if (plValue) {
|
|
548
|
-
styles.push(`padding-left: ${plValue}`);
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
// Margin - uses spacing tokens
|
|
552
|
-
const mValue = resolveSpacingValue(props.m);
|
|
553
|
-
if (mValue) {
|
|
554
|
-
styles.push(`margin: ${mValue}`);
|
|
555
|
-
}
|
|
556
|
-
const mxValue = props.mx !== 'auto' ? resolveSpacingValue(props.mx as SpacingValue | undefined) : undefined;
|
|
557
|
-
if (mxValue) {
|
|
558
|
-
styles.push(`margin-left: ${mxValue}`);
|
|
559
|
-
styles.push(`margin-right: ${mxValue}`);
|
|
560
|
-
}
|
|
561
|
-
const myValue = resolveSpacingValue(props.my);
|
|
562
|
-
if (myValue) {
|
|
563
|
-
styles.push(`margin-top: ${myValue}`);
|
|
564
|
-
styles.push(`margin-bottom: ${myValue}`);
|
|
565
|
-
}
|
|
566
|
-
const mtValue = resolveSpacingValue(props.mt);
|
|
567
|
-
if (mtValue) {
|
|
568
|
-
styles.push(`margin-top: ${mtValue}`);
|
|
569
|
-
}
|
|
570
|
-
const mrValue = resolveSpacingValue(props.mr);
|
|
571
|
-
if (mrValue) {
|
|
572
|
-
styles.push(`margin-right: ${mrValue}`);
|
|
573
|
-
}
|
|
574
|
-
const mbValue = resolveSpacingValue(props.mb);
|
|
575
|
-
if (mbValue) {
|
|
576
|
-
styles.push(`margin-bottom: ${mbValue}`);
|
|
577
|
-
}
|
|
578
|
-
const mlValue = resolveSpacingValue(props.ml);
|
|
579
|
-
if (mlValue) {
|
|
580
|
-
styles.push(`margin-left: ${mlValue}`);
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
// Gap - uses spacing tokens
|
|
584
|
-
const gapValue = resolveSpacingValue(props.gap);
|
|
585
|
-
if (gapValue) {
|
|
586
|
-
styles.push(`gap: ${gapValue}`);
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
return styles.join('; ');
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
/**
|
|
593
|
-
* Build inline styles for Col node (extends common styles with order)
|
|
594
|
-
*/
|
|
595
|
-
private buildColStyles(node: ColNode): string {
|
|
596
|
-
const styles: string[] = [];
|
|
597
|
-
|
|
598
|
-
// Get common styles first
|
|
599
|
-
const commonStyles = this.buildCommonStyles(node);
|
|
600
|
-
if (commonStyles) {
|
|
601
|
-
styles.push(commonStyles);
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
// Order (Col-specific)
|
|
605
|
-
if (node.order !== undefined) {
|
|
606
|
-
styles.push(`order: ${node.order}`);
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
return styles.join('; ');
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
// ===========================================
|
|
613
|
-
// Container Node Renderers
|
|
614
|
-
// ===========================================
|
|
615
|
-
|
|
616
|
-
private renderCard(node: CardNode): string {
|
|
617
|
-
const classes = this.buildClassString([
|
|
618
|
-
`${this.prefix}-card`,
|
|
619
|
-
node.shadow ? `${this.prefix}-card-shadow-${node.shadow}` : undefined,
|
|
620
|
-
...this.getCommonClasses(node),
|
|
621
|
-
]);
|
|
622
|
-
|
|
623
|
-
const styles = this.buildCommonStyles(node);
|
|
624
|
-
const styleAttr = styles ? ` style="${styles}"` : '';
|
|
625
|
-
|
|
626
|
-
const title = node.title
|
|
627
|
-
? `<h3 class="${this.prefix}-title">${this.escapeHtml(node.title)}</h3>\n`
|
|
628
|
-
: '';
|
|
629
|
-
const children = this.renderChildren(node.children);
|
|
630
|
-
return `<div class="${classes}"${styleAttr}>\n${title}${children}\n</div>`;
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
private renderModal(node: ModalNode): string {
|
|
634
|
-
const classes = this.buildClassString([
|
|
635
|
-
`${this.prefix}-modal`,
|
|
636
|
-
...this.getCommonClasses(node),
|
|
637
|
-
]);
|
|
638
|
-
|
|
639
|
-
const styles = this.buildCommonStyles(node);
|
|
640
|
-
const styleAttr = styles ? ` style="${styles}"` : '';
|
|
641
|
-
|
|
642
|
-
const title = node.title
|
|
643
|
-
? `<h2 class="${this.prefix}-title">${this.escapeHtml(node.title)}</h2>\n`
|
|
644
|
-
: '';
|
|
645
|
-
const children = this.renderChildren(node.children);
|
|
646
|
-
return `<div class="${this.prefix}-modal-backdrop">
|
|
647
|
-
<div class="${classes}"${styleAttr} role="dialog" aria-modal="true">
|
|
648
|
-
${title}${children}
|
|
649
|
-
</div>
|
|
650
|
-
</div>`;
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
private renderDrawer(node: DrawerNode): string {
|
|
654
|
-
const position = node.position || 'left';
|
|
655
|
-
const classes = this.buildClassString([
|
|
656
|
-
`${this.prefix}-drawer`,
|
|
657
|
-
`${this.prefix}-drawer-${position}`,
|
|
658
|
-
...this.getCommonClasses(node),
|
|
659
|
-
]);
|
|
660
|
-
|
|
661
|
-
const styles = this.buildCommonStyles(node);
|
|
662
|
-
const styleAttr = styles ? ` style="${styles}"` : '';
|
|
663
|
-
|
|
664
|
-
const title = node.title
|
|
665
|
-
? `<h2 class="${this.prefix}-title">${this.escapeHtml(node.title)}</h2>\n`
|
|
666
|
-
: '';
|
|
667
|
-
const children = this.renderChildren(node.children);
|
|
668
|
-
return `<aside class="${classes}"${styleAttr}>\n${title}${children}\n</aside>`;
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
private renderAccordion(node: AccordionNode): string {
|
|
672
|
-
const classes = this.buildClassString([
|
|
673
|
-
`${this.prefix}-accordion`,
|
|
674
|
-
...this.getCommonClasses(node),
|
|
675
|
-
]);
|
|
676
|
-
|
|
677
|
-
const styles = this.buildCommonStyles(node);
|
|
678
|
-
const styleAttr = styles ? ` style="${styles}"` : '';
|
|
679
|
-
|
|
680
|
-
const title = node.title
|
|
681
|
-
? `<button class="${this.prefix}-accordion-header">${this.escapeHtml(node.title)}</button>\n`
|
|
682
|
-
: '';
|
|
683
|
-
const children = this.renderChildren(node.children);
|
|
684
|
-
return `<div class="${classes}"${styleAttr}>\n${title}<div class="${this.prefix}-accordion-content">\n${children}\n</div>\n</div>`;
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
// ===========================================
|
|
688
|
-
// Text Node Renderers
|
|
689
|
-
// ===========================================
|
|
690
|
-
|
|
691
|
-
private renderText(node: TextNode): string {
|
|
692
|
-
const classes = this.buildClassString([
|
|
693
|
-
`${this.prefix}-text`,
|
|
694
|
-
node.size ? `${this.prefix}-text-${node.size}` : undefined,
|
|
695
|
-
node.weight ? `${this.prefix}-text-${node.weight}` : undefined,
|
|
696
|
-
node.align ? `${this.prefix}-text-${node.align}` : undefined,
|
|
697
|
-
node.muted ? `${this.prefix}-text-muted` : undefined,
|
|
698
|
-
...this.getCommonClasses(node),
|
|
699
|
-
]);
|
|
700
|
-
|
|
701
|
-
const styles = this.buildCommonStyles(node);
|
|
702
|
-
const styleAttr = styles ? ` style="${styles}"` : '';
|
|
703
|
-
|
|
704
|
-
return `<p class="${classes}"${styleAttr}>${this.escapeHtml(node.content)}</p>`;
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
private renderTitle(node: TitleNode): string {
|
|
708
|
-
const level = node.level || 1;
|
|
709
|
-
const tag = `h${level}`;
|
|
710
|
-
const classes = this.buildClassString([
|
|
711
|
-
`${this.prefix}-title`,
|
|
712
|
-
node.size ? `${this.prefix}-text-${node.size}` : undefined,
|
|
713
|
-
node.align ? `${this.prefix}-text-${node.align}` : undefined,
|
|
714
|
-
...this.getCommonClasses(node),
|
|
715
|
-
]);
|
|
716
|
-
|
|
717
|
-
const styles = this.buildCommonStyles(node);
|
|
718
|
-
const styleAttr = styles ? ` style="${styles}"` : '';
|
|
719
|
-
|
|
720
|
-
return `<${tag} class="${classes}"${styleAttr}>${this.escapeHtml(node.content)}</${tag}>`;
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
private renderLink(node: LinkNode): string {
|
|
724
|
-
const classes = this.buildClassString([
|
|
725
|
-
`${this.prefix}-link`,
|
|
726
|
-
...this.getCommonClasses(node),
|
|
727
|
-
]);
|
|
728
|
-
|
|
729
|
-
const styles = this.buildCommonStyles(node);
|
|
730
|
-
const styleAttr = styles ? ` style="${styles}"` : '';
|
|
731
|
-
|
|
732
|
-
const attrs: Record<string, string | boolean | undefined> = {
|
|
733
|
-
class: classes,
|
|
734
|
-
href: node.href || '#',
|
|
735
|
-
};
|
|
736
|
-
|
|
737
|
-
if (node.external) {
|
|
738
|
-
attrs.target = '_blank';
|
|
739
|
-
attrs.rel = 'noopener noreferrer';
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
return `<a${this.buildAttrsString(attrs)}${styleAttr}>${this.escapeHtml(node.content)}</a>`;
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
// ===========================================
|
|
746
|
-
// Input Node Renderers
|
|
747
|
-
// ===========================================
|
|
748
|
-
|
|
749
|
-
private renderInput(node: InputNode): string {
|
|
750
|
-
const inputClasses = this.buildClassString([
|
|
751
|
-
`${this.prefix}-input`,
|
|
752
|
-
node.icon ? `${this.prefix}-input-with-icon` : undefined,
|
|
753
|
-
...this.getCommonClasses(node),
|
|
754
|
-
]);
|
|
755
|
-
|
|
756
|
-
const styles = this.buildCommonStyles(node);
|
|
757
|
-
const styleAttr = styles ? ` style="${styles}"` : '';
|
|
758
|
-
|
|
759
|
-
const attrs: Record<string, string | boolean | undefined> = {
|
|
760
|
-
class: inputClasses,
|
|
761
|
-
type: node.inputType || 'text',
|
|
762
|
-
placeholder: node.placeholder,
|
|
763
|
-
value: node.value,
|
|
764
|
-
disabled: node.disabled,
|
|
765
|
-
required: node.required,
|
|
766
|
-
readonly: node.readonly,
|
|
767
|
-
};
|
|
768
|
-
|
|
769
|
-
const inputElement = `<input${this.buildAttrsString(attrs)} />`;
|
|
770
|
-
|
|
771
|
-
// Wrap with icon if specified
|
|
772
|
-
if (node.icon) {
|
|
773
|
-
const iconData = getIconData(node.icon);
|
|
774
|
-
let iconHtml: string;
|
|
775
|
-
if (iconData) {
|
|
776
|
-
iconHtml = renderIconSvg(iconData, 16, 2, `${this.prefix}-input-icon`);
|
|
777
|
-
} else {
|
|
778
|
-
iconHtml = `<span class="${this.prefix}-input-icon">[${this.escapeHtml(node.icon)}]</span>`;
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
const wrapperClasses = this.buildClassString([`${this.prefix}-input-wrapper`]);
|
|
782
|
-
const wrapper = `<div class="${wrapperClasses}"${styleAttr}>${iconHtml}${inputElement}</div>`;
|
|
783
|
-
|
|
784
|
-
if (node.label) {
|
|
785
|
-
return `<label class="${this.prefix}-input-label">${this.escapeHtml(node.label)}</label>\n${wrapper}`;
|
|
786
|
-
}
|
|
787
|
-
return wrapper;
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
const input = `<input${this.buildAttrsString(attrs)}${styleAttr} />`;
|
|
791
|
-
|
|
792
|
-
if (node.label) {
|
|
793
|
-
return `<label class="${this.prefix}-input-label">${this.escapeHtml(node.label)}</label>\n${input}`;
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
return input;
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
private renderTextarea(node: TextareaNode): string {
|
|
800
|
-
const classes = this.buildClassString([
|
|
801
|
-
`${this.prefix}-input`,
|
|
802
|
-
...this.getCommonClasses(node),
|
|
803
|
-
]);
|
|
804
|
-
|
|
805
|
-
const styles = this.buildCommonStyles(node);
|
|
806
|
-
const styleAttr = styles ? ` style="${styles}"` : '';
|
|
807
|
-
|
|
808
|
-
const attrs: Record<string, string | boolean | undefined> = {
|
|
809
|
-
class: classes,
|
|
810
|
-
placeholder: node.placeholder,
|
|
811
|
-
disabled: node.disabled,
|
|
812
|
-
required: node.required,
|
|
813
|
-
rows: node.rows?.toString(),
|
|
814
|
-
};
|
|
815
|
-
|
|
816
|
-
const textarea = `<textarea${this.buildAttrsString(attrs)}${styleAttr}>${this.escapeHtml(node.value || '')}</textarea>`;
|
|
817
|
-
|
|
818
|
-
if (node.label) {
|
|
819
|
-
return `<label class="${this.prefix}-input-label">${this.escapeHtml(node.label)}</label>\n${textarea}`;
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
return textarea;
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
private renderSelect(node: SelectNode): string {
|
|
826
|
-
const classes = this.buildClassString([
|
|
827
|
-
`${this.prefix}-input`,
|
|
828
|
-
...this.getCommonClasses(node),
|
|
829
|
-
]);
|
|
830
|
-
|
|
831
|
-
const styles = this.buildCommonStyles(node);
|
|
832
|
-
const styleAttr = styles ? ` style="${styles}"` : '';
|
|
833
|
-
|
|
834
|
-
const attrs: Record<string, string | boolean | undefined> = {
|
|
835
|
-
class: classes,
|
|
836
|
-
disabled: node.disabled,
|
|
837
|
-
required: node.required,
|
|
838
|
-
};
|
|
839
|
-
|
|
840
|
-
const options = node.options
|
|
841
|
-
.map((opt) => {
|
|
842
|
-
if (typeof opt === 'string') {
|
|
843
|
-
const selected = opt === node.value ? ' selected' : '';
|
|
844
|
-
return `<option value="${this.escapeHtml(opt)}"${selected}>${this.escapeHtml(opt)}</option>`;
|
|
845
|
-
}
|
|
846
|
-
const selected = opt.value === node.value ? ' selected' : '';
|
|
847
|
-
return `<option value="${this.escapeHtml(opt.value)}"${selected}>${this.escapeHtml(opt.label)}</option>`;
|
|
848
|
-
})
|
|
849
|
-
.join('\n');
|
|
850
|
-
|
|
851
|
-
const placeholder = node.placeholder
|
|
852
|
-
? `<option value="" disabled selected>${this.escapeHtml(node.placeholder)}</option>\n`
|
|
853
|
-
: '';
|
|
854
|
-
|
|
855
|
-
const select = `<select${this.buildAttrsString(attrs)}${styleAttr}>\n${placeholder}${options}\n</select>`;
|
|
856
|
-
|
|
857
|
-
if (node.label) {
|
|
858
|
-
return `<label class="${this.prefix}-input-label">${this.escapeHtml(node.label)}</label>\n${select}`;
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
return select;
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
private renderCheckbox(node: CheckboxNode): string {
|
|
865
|
-
const styles = this.buildCommonStyles(node);
|
|
866
|
-
const styleAttr = styles ? ` style="${styles}"` : '';
|
|
867
|
-
|
|
868
|
-
const attrs: Record<string, string | boolean | undefined> = {
|
|
869
|
-
type: 'checkbox',
|
|
870
|
-
checked: node.checked,
|
|
871
|
-
disabled: node.disabled,
|
|
872
|
-
};
|
|
873
|
-
|
|
874
|
-
const checkbox = `<input${this.buildAttrsString(attrs)} />`;
|
|
875
|
-
|
|
876
|
-
if (node.label) {
|
|
877
|
-
return `<label class="${this.prefix}-checkbox"${styleAttr}>${checkbox}<span>${this.escapeHtml(node.label)}</span></label>`;
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
return checkbox;
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
private renderRadio(node: RadioNode): string {
|
|
884
|
-
const styles = this.buildCommonStyles(node);
|
|
885
|
-
const styleAttr = styles ? ` style="${styles}"` : '';
|
|
886
|
-
|
|
887
|
-
const attrs: Record<string, string | boolean | undefined> = {
|
|
888
|
-
type: 'radio',
|
|
889
|
-
name: node.name,
|
|
890
|
-
checked: node.checked,
|
|
891
|
-
disabled: node.disabled,
|
|
892
|
-
};
|
|
893
|
-
|
|
894
|
-
const radio = `<input${this.buildAttrsString(attrs)} />`;
|
|
895
|
-
|
|
896
|
-
if (node.label) {
|
|
897
|
-
return `<label class="${this.prefix}-radio"${styleAttr}>${radio}<span>${this.escapeHtml(node.label)}</span></label>`;
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
return radio;
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
private renderSwitch(node: SwitchNode): string {
|
|
904
|
-
const classes = this.buildClassString([
|
|
905
|
-
`${this.prefix}-switch`,
|
|
906
|
-
...this.getCommonClasses(node),
|
|
907
|
-
]);
|
|
908
|
-
|
|
909
|
-
const styles = this.buildCommonStyles(node);
|
|
910
|
-
const styleAttr = styles ? ` style="${styles}"` : '';
|
|
911
|
-
|
|
912
|
-
const attrs: Record<string, string | boolean | undefined> = {
|
|
913
|
-
type: 'checkbox',
|
|
914
|
-
role: 'switch',
|
|
915
|
-
checked: node.checked,
|
|
916
|
-
disabled: node.disabled,
|
|
917
|
-
};
|
|
918
|
-
|
|
919
|
-
const switchEl = `<input${this.buildAttrsString(attrs)} />`;
|
|
920
|
-
|
|
921
|
-
if (node.label) {
|
|
922
|
-
return `<label class="${classes}"${styleAttr}>${switchEl} ${this.escapeHtml(node.label)}</label>`;
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
// Always wrap in label with .wf-switch class for proper styling
|
|
926
|
-
return `<label class="${classes}"${styleAttr}>${switchEl}</label>`;
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
private renderSlider(node: SliderNode): string {
|
|
930
|
-
const classes = this.buildClassString([
|
|
931
|
-
`${this.prefix}-slider`,
|
|
932
|
-
...this.getCommonClasses(node),
|
|
933
|
-
]);
|
|
934
|
-
|
|
935
|
-
const styles = this.buildCommonStyles(node);
|
|
936
|
-
const styleAttr = styles ? ` style="${styles}"` : '';
|
|
937
|
-
|
|
938
|
-
const attrs: Record<string, string | boolean | undefined> = {
|
|
939
|
-
class: classes,
|
|
940
|
-
type: 'range',
|
|
941
|
-
min: node.min?.toString(),
|
|
942
|
-
max: node.max?.toString(),
|
|
943
|
-
step: node.step?.toString(),
|
|
944
|
-
value: node.value?.toString(),
|
|
945
|
-
disabled: node.disabled,
|
|
946
|
-
};
|
|
947
|
-
|
|
948
|
-
const slider = `<input${this.buildAttrsString(attrs)}${styleAttr} />`;
|
|
949
|
-
|
|
950
|
-
if (node.label) {
|
|
951
|
-
return `<label class="${this.prefix}-input-label">${this.escapeHtml(node.label)}</label>\n${slider}`;
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
return slider;
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
// ===========================================
|
|
958
|
-
// Button Renderer
|
|
959
|
-
// ===========================================
|
|
960
|
-
|
|
961
|
-
private renderButton(node: ButtonNode): string {
|
|
962
|
-
// Icon-only button: has icon but no text content
|
|
963
|
-
const isIconOnly = node.icon && !node.content.trim();
|
|
964
|
-
const classes = this.buildClassString([
|
|
965
|
-
`${this.prefix}-button`,
|
|
966
|
-
node.primary ? `${this.prefix}-button-primary` : undefined,
|
|
967
|
-
node.secondary ? `${this.prefix}-button-secondary` : undefined,
|
|
968
|
-
node.outline ? `${this.prefix}-button-outline` : undefined,
|
|
969
|
-
node.ghost ? `${this.prefix}-button-ghost` : undefined,
|
|
970
|
-
node.danger ? `${this.prefix}-button-danger` : undefined,
|
|
971
|
-
node.size ? `${this.prefix}-button-${node.size}` : undefined,
|
|
972
|
-
node.disabled ? `${this.prefix}-button-disabled` : undefined,
|
|
973
|
-
node.loading ? `${this.prefix}-button-loading` : undefined,
|
|
974
|
-
isIconOnly ? `${this.prefix}-button-icon-only` : undefined,
|
|
975
|
-
...this.getCommonClasses(node),
|
|
976
|
-
]);
|
|
977
|
-
|
|
978
|
-
const styles = this.buildCommonStyles(node);
|
|
979
|
-
const styleAttr = styles ? ` style="${styles}"` : '';
|
|
980
|
-
|
|
981
|
-
const attrs: Record<string, string | boolean | undefined> = {
|
|
982
|
-
class: classes,
|
|
983
|
-
disabled: node.disabled,
|
|
984
|
-
};
|
|
985
|
-
|
|
986
|
-
let icon = '';
|
|
987
|
-
if (node.icon) {
|
|
988
|
-
const iconData = getIconData(node.icon);
|
|
989
|
-
if (iconData) {
|
|
990
|
-
icon = renderIconSvg(iconData, 16, 2, `${this.prefix}-icon`);
|
|
991
|
-
} else {
|
|
992
|
-
icon = `<span class="${this.prefix}-icon">[${this.escapeHtml(node.icon)}]</span>`;
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
const loading = node.loading ? `<span class="${this.prefix}-spinner ${this.prefix}-spinner-sm"></span>` : '';
|
|
996
|
-
const content = this.escapeHtml(node.content);
|
|
997
|
-
|
|
998
|
-
return `<button${this.buildAttrsString(attrs)}${styleAttr}>${loading}${icon}${content}</button>`;
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
// ===========================================
|
|
1002
|
-
// Display Node Renderers
|
|
1003
|
-
// ===========================================
|
|
1004
|
-
|
|
1005
|
-
private renderImage(node: ImageNode): string {
|
|
1006
|
-
const classes = this.buildClassString([
|
|
1007
|
-
`${this.prefix}-image`,
|
|
1008
|
-
...this.getCommonClasses(node),
|
|
1009
|
-
]);
|
|
1010
|
-
|
|
1011
|
-
const styles = this.buildCommonStyles(node);
|
|
1012
|
-
const styleAttr = styles ? ` style="${styles}"` : '';
|
|
1013
|
-
|
|
1014
|
-
// If src is provided, render as actual img tag
|
|
1015
|
-
if (node.src) {
|
|
1016
|
-
const attrs: Record<string, string | boolean | undefined> = {
|
|
1017
|
-
class: classes,
|
|
1018
|
-
src: node.src,
|
|
1019
|
-
alt: node.alt || 'Image',
|
|
1020
|
-
};
|
|
1021
|
-
// Add style attribute for img tag
|
|
1022
|
-
const imgStyleAttr = styles ? `; ${styles}` : '';
|
|
1023
|
-
return `<img${this.buildAttrsString(attrs)}${imgStyleAttr ? ` style="${imgStyleAttr.slice(2)}"` : ''} />`;
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
// Otherwise render as placeholder with image icon
|
|
1027
|
-
const label = node.alt || 'Image';
|
|
1028
|
-
const icon = `<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>`;
|
|
1029
|
-
return `<div class="${classes}"${styleAttr} role="img" aria-label="${this.escapeHtml(label)}">${icon}<span>${this.escapeHtml(label)}</span></div>`;
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
private renderPlaceholder(node: PlaceholderNode): string {
|
|
1033
|
-
const classes = this.buildClassString([
|
|
1034
|
-
`${this.prefix}-placeholder`,
|
|
1035
|
-
...this.getCommonClasses(node),
|
|
1036
|
-
]);
|
|
1037
|
-
|
|
1038
|
-
const styles = this.buildCommonStyles(node);
|
|
1039
|
-
const styleAttr = styles ? ` style="${styles}"` : '';
|
|
1040
|
-
|
|
1041
|
-
const label = node.label ? this.escapeHtml(node.label) : 'Placeholder';
|
|
1042
|
-
return `<div class="${classes}"${styleAttr}>${label}</div>`;
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
private renderAvatar(node: AvatarNode): string {
|
|
1046
|
-
// Resolve size: token string (xs, sm, md, lg, xl) or custom px number
|
|
1047
|
-
const sizeResolved = resolveSizeValue(node.size, 'avatar', this.prefix);
|
|
1048
|
-
|
|
1049
|
-
const classes = this.buildClassString([
|
|
1050
|
-
`${this.prefix}-avatar`,
|
|
1051
|
-
sizeResolved.className,
|
|
1052
|
-
...this.getCommonClasses(node),
|
|
1053
|
-
]);
|
|
1054
|
-
|
|
1055
|
-
const baseStyles = this.buildCommonStyles(node);
|
|
1056
|
-
const sizeStyle = sizeResolved.style || '';
|
|
1057
|
-
const combinedStyles = baseStyles && sizeStyle
|
|
1058
|
-
? `${baseStyles}; ${sizeStyle}`
|
|
1059
|
-
: baseStyles || sizeStyle;
|
|
1060
|
-
const styleAttr = combinedStyles ? ` style="${combinedStyles}"` : '';
|
|
1061
|
-
|
|
1062
|
-
const initials = node.name
|
|
1063
|
-
? node.name
|
|
1064
|
-
.split(' ')
|
|
1065
|
-
.map((n) => n[0])
|
|
1066
|
-
.join('')
|
|
1067
|
-
.toUpperCase()
|
|
1068
|
-
.slice(0, 2)
|
|
1069
|
-
: '?';
|
|
1070
|
-
|
|
1071
|
-
return `<div class="${classes}"${styleAttr} role="img" aria-label="${this.escapeHtml(node.name || 'Avatar')}">${initials}</div>`;
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
private renderBadge(node: BadgeNode): string {
|
|
1075
|
-
// If icon is provided, render as icon badge (circular background with icon)
|
|
1076
|
-
if (node.icon) {
|
|
1077
|
-
const iconData = getIconData(node.icon);
|
|
1078
|
-
const classes = this.buildClassString([
|
|
1079
|
-
`${this.prefix}-badge-icon`,
|
|
1080
|
-
node.size ? `${this.prefix}-badge-icon-${node.size}` : undefined,
|
|
1081
|
-
node.variant ? `${this.prefix}-badge-icon-${node.variant}` : undefined,
|
|
1082
|
-
...this.getCommonClasses(node),
|
|
1083
|
-
]);
|
|
1084
|
-
|
|
1085
|
-
const styles = this.buildCommonStyles(node);
|
|
1086
|
-
const styleAttr = styles ? ` style="${styles}"` : '';
|
|
1087
|
-
|
|
1088
|
-
if (iconData) {
|
|
1089
|
-
const svg = renderIconSvg(iconData, 24, 2, `${this.prefix}-icon`);
|
|
1090
|
-
return `<span class="${classes}"${styleAttr} aria-label="${this.escapeHtml(node.icon)}">${svg}</span>`;
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
// Fallback for unknown icon
|
|
1094
|
-
return `<span class="${classes}"${styleAttr} aria-label="unknown icon">?</span>`;
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
// Default text badge (empty content = dot indicator)
|
|
1098
|
-
const isDot = !node.content || node.content.trim() === '';
|
|
1099
|
-
const classes = this.buildClassString([
|
|
1100
|
-
`${this.prefix}-badge`,
|
|
1101
|
-
isDot ? `${this.prefix}-badge-dot` : undefined,
|
|
1102
|
-
node.variant ? `${this.prefix}-badge-${node.variant}` : undefined,
|
|
1103
|
-
node.pill ? `${this.prefix}-badge-pill` : undefined,
|
|
1104
|
-
...this.getCommonClasses(node),
|
|
1105
|
-
]);
|
|
1106
|
-
|
|
1107
|
-
const styles = this.buildCommonStyles(node);
|
|
1108
|
-
const styleAttr = styles ? ` style="${styles}"` : '';
|
|
1109
|
-
|
|
1110
|
-
return `<span class="${classes}"${styleAttr}>${this.escapeHtml(node.content)}</span>`;
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
private renderIcon(node: IconNode): string {
|
|
1114
|
-
const iconData = getIconData(node.name);
|
|
1115
|
-
|
|
1116
|
-
// Resolve size: token string (xs, sm, md, lg, xl) or custom px number
|
|
1117
|
-
const sizeResolved = resolveSizeValue(node.size, 'icon', this.prefix);
|
|
1118
|
-
|
|
1119
|
-
const wrapperClasses = this.buildClassString([
|
|
1120
|
-
`${this.prefix}-icon-wrapper`,
|
|
1121
|
-
node.muted ? `${this.prefix}-text-muted` : undefined,
|
|
1122
|
-
...this.getCommonClasses(node),
|
|
1123
|
-
]);
|
|
1124
|
-
|
|
1125
|
-
const baseStyles = this.buildCommonStyles(node);
|
|
1126
|
-
|
|
1127
|
-
if (iconData) {
|
|
1128
|
-
// Build icon class with optional size class
|
|
1129
|
-
const iconClasses = _buildClassString([
|
|
1130
|
-
`${this.prefix}-icon`,
|
|
1131
|
-
sizeResolved.className,
|
|
1132
|
-
]);
|
|
1133
|
-
const svgStyleAttr = sizeResolved.style ? ` style="${sizeResolved.style}"` : '';
|
|
1134
|
-
const svg = renderIconSvg(iconData, 24, 2, iconClasses, svgStyleAttr);
|
|
1135
|
-
const wrapperStyleAttr = baseStyles ? ` style="${baseStyles}"` : '';
|
|
1136
|
-
return `<span class="${wrapperClasses}"${wrapperStyleAttr} aria-hidden="true">${svg}</span>`;
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
// Fallback for unknown icons - render a placeholder circle
|
|
1140
|
-
const size = sizeResolved.style?.match(/(\d+)px/)?.[1] || '24';
|
|
1141
|
-
const sizeNum = parseInt(size, 10);
|
|
1142
|
-
const placeholderSvg = `<svg class="${this.prefix}-icon ${sizeResolved.className || ''}" width="${sizeNum}" height="${sizeNum}" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
1143
|
-
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" stroke-dasharray="4 2" fill="none" opacity="0.5"/>
|
|
1144
|
-
<text x="12" y="16" text-anchor="middle" font-size="10" fill="currentColor" opacity="0.7">?</text>
|
|
1145
|
-
</svg>`;
|
|
1146
|
-
const wrapperStyleAttr = baseStyles ? ` style="${baseStyles}"` : '';
|
|
1147
|
-
return `<span class="${wrapperClasses}"${wrapperStyleAttr} aria-hidden="true" title="Unknown icon: ${this.escapeHtml(node.name)}">${placeholderSvg}</span>`;
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
// ===========================================
|
|
1151
|
-
// Data Node Renderers
|
|
1152
|
-
// ===========================================
|
|
1153
|
-
|
|
1154
|
-
private renderTable(node: TableNode): string {
|
|
1155
|
-
const classes = this.buildClassString([
|
|
1156
|
-
`${this.prefix}-table`,
|
|
1157
|
-
node.striped ? `${this.prefix}-table-striped` : undefined,
|
|
1158
|
-
node.bordered ? `${this.prefix}-table-bordered` : undefined,
|
|
1159
|
-
node.hover ? `${this.prefix}-table-hover` : undefined,
|
|
1160
|
-
...this.getCommonClasses(node),
|
|
1161
|
-
]);
|
|
1162
|
-
|
|
1163
|
-
const styles = this.buildCommonStyles(node);
|
|
1164
|
-
const styleAttr = styles ? ` style="${styles}"` : '';
|
|
1165
|
-
|
|
1166
|
-
const thead = `<thead><tr>${node.columns
|
|
1167
|
-
.map((col) => `<th>${this.escapeHtml(col)}</th>`)
|
|
1168
|
-
.join('')}</tr></thead>`;
|
|
1169
|
-
|
|
1170
|
-
const tbody = `<tbody>${node.rows
|
|
1171
|
-
.map(
|
|
1172
|
-
(row) =>
|
|
1173
|
-
`<tr>${row
|
|
1174
|
-
.map((cell) => {
|
|
1175
|
-
if (typeof cell === 'string') {
|
|
1176
|
-
// Support semantic markers and newlines in table cells
|
|
1177
|
-
return `<td>${this.renderTableCellContent(cell)}</td>`;
|
|
1178
|
-
}
|
|
1179
|
-
return `<td>${this.renderNode(cell)}</td>`;
|
|
1180
|
-
})
|
|
1181
|
-
.join('')}</tr>`
|
|
1182
|
-
)
|
|
1183
|
-
.join('')}</tbody>`;
|
|
1184
|
-
|
|
1185
|
-
return `<table class="${classes}"${styleAttr}>\n${thead}\n${tbody}\n</table>`;
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
private renderList(node: ListNode): string {
|
|
1189
|
-
const tag = node.ordered ? 'ol' : 'ul';
|
|
1190
|
-
const classes = this.buildClassString([
|
|
1191
|
-
`${this.prefix}-list`,
|
|
1192
|
-
node.ordered ? `${this.prefix}-list-ordered` : undefined,
|
|
1193
|
-
node.none ? `${this.prefix}-list-none` : undefined,
|
|
1194
|
-
...this.getCommonClasses(node),
|
|
1195
|
-
]);
|
|
1196
|
-
|
|
1197
|
-
const styles = this.buildCommonStyles(node);
|
|
1198
|
-
const styleAttr = styles ? ` style="${styles}"` : '';
|
|
1199
|
-
|
|
1200
|
-
const items = node.items
|
|
1201
|
-
.map((item) => {
|
|
1202
|
-
if (typeof item === 'string') {
|
|
1203
|
-
return `<li class="${this.prefix}-list-item">${this.escapeHtml(item)}</li>`;
|
|
1204
|
-
}
|
|
1205
|
-
return `<li class="${this.prefix}-list-item">${this.escapeHtml(item.content)}</li>`;
|
|
1206
|
-
})
|
|
1207
|
-
.join('\n');
|
|
1208
|
-
|
|
1209
|
-
return `<${tag} class="${classes}"${styleAttr}>\n${items}\n</${tag}>`;
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
// ===========================================
|
|
1213
|
-
// Feedback Node Renderers
|
|
1214
|
-
// ===========================================
|
|
1215
|
-
|
|
1216
|
-
private renderAlert(node: AlertNode): string {
|
|
1217
|
-
const classes = this.buildClassString([
|
|
1218
|
-
`${this.prefix}-alert`,
|
|
1219
|
-
node.variant ? `${this.prefix}-alert-${node.variant}` : undefined,
|
|
1220
|
-
...this.getCommonClasses(node),
|
|
1221
|
-
]);
|
|
1222
|
-
|
|
1223
|
-
const styles = this.buildCommonStyles(node);
|
|
1224
|
-
const styleAttr = styles ? ` style="${styles}"` : '';
|
|
1225
|
-
|
|
1226
|
-
const dismissBtn = node.dismissible
|
|
1227
|
-
? ` <button class="${this.prefix}-alert-close" aria-label="Close">×</button>`
|
|
1228
|
-
: '';
|
|
1229
|
-
|
|
1230
|
-
return `<div class="${classes}"${styleAttr} role="alert">${this.escapeHtml(node.content)}${dismissBtn}</div>`;
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
private renderToast(node: ToastNode): string {
|
|
1234
|
-
const classes = this.buildClassString([
|
|
1235
|
-
`${this.prefix}-toast`,
|
|
1236
|
-
node.position ? `${this.prefix}-toast-${node.position}` : undefined,
|
|
1237
|
-
node.variant ? `${this.prefix}-toast-${node.variant}` : undefined,
|
|
1238
|
-
...this.getCommonClasses(node),
|
|
1239
|
-
]);
|
|
1240
|
-
|
|
1241
|
-
const styles = this.buildCommonStyles(node);
|
|
1242
|
-
const styleAttr = styles ? ` style="${styles}"` : '';
|
|
1243
|
-
|
|
1244
|
-
return `<div class="${classes}"${styleAttr} role="status">${this.escapeHtml(node.content)}</div>`;
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
|
-
private renderProgress(node: ProgressNode): string {
|
|
1248
|
-
const classes = this.buildClassString([
|
|
1249
|
-
`${this.prefix}-progress`,
|
|
1250
|
-
...this.getCommonClasses(node),
|
|
1251
|
-
]);
|
|
1252
|
-
|
|
1253
|
-
const styles = this.buildCommonStyles(node);
|
|
1254
|
-
const styleAttr = styles ? ` style="${styles}"` : '';
|
|
1255
|
-
|
|
1256
|
-
const value = node.value || 0;
|
|
1257
|
-
const max = node.max || 100;
|
|
1258
|
-
const percentage = Math.round((value / max) * 100);
|
|
1259
|
-
|
|
1260
|
-
const label = node.label ? `<span class="${this.prefix}-progress-label">${this.escapeHtml(node.label)}</span>` : '';
|
|
1261
|
-
|
|
1262
|
-
if (node.indeterminate) {
|
|
1263
|
-
return `<div class="${classes} ${this.prefix}-progress-indeterminate"${styleAttr} role="progressbar">${label}</div>`;
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
return `<div class="${classes}"${styleAttr} role="progressbar" aria-valuenow="${value}" aria-valuemin="0" aria-valuemax="${max}">
|
|
1267
|
-
${label}
|
|
1268
|
-
<div class="${this.prefix}-progress-bar" style="width: ${percentage}%"></div>
|
|
1269
|
-
</div>`;
|
|
1270
|
-
}
|
|
1271
|
-
|
|
1272
|
-
private renderSpinner(node: SpinnerNode): string {
|
|
1273
|
-
// Resolve size: token string (xs, sm, md, lg, xl) or custom px number
|
|
1274
|
-
const sizeResolved = resolveSizeValue(node.size, 'spinner', this.prefix);
|
|
1275
|
-
|
|
1276
|
-
const classes = this.buildClassString([
|
|
1277
|
-
`${this.prefix}-spinner`,
|
|
1278
|
-
sizeResolved.className,
|
|
1279
|
-
...this.getCommonClasses(node),
|
|
1280
|
-
]);
|
|
1281
|
-
|
|
1282
|
-
const baseStyles = this.buildCommonStyles(node);
|
|
1283
|
-
const sizeStyle = sizeResolved.style || '';
|
|
1284
|
-
const combinedStyles = baseStyles && sizeStyle
|
|
1285
|
-
? `${baseStyles}; ${sizeStyle}`
|
|
1286
|
-
: baseStyles || sizeStyle;
|
|
1287
|
-
const styleAttr = combinedStyles ? ` style="${combinedStyles}"` : '';
|
|
1288
|
-
|
|
1289
|
-
const label = node.label || 'Loading...';
|
|
1290
|
-
return `<span class="${classes}"${styleAttr} role="status" aria-label="${this.escapeHtml(label)}"></span>`;
|
|
1291
|
-
}
|
|
1292
|
-
|
|
1293
|
-
// ===========================================
|
|
1294
|
-
// Overlay Node Renderers
|
|
1295
|
-
// ===========================================
|
|
1296
|
-
|
|
1297
|
-
private renderTooltip(node: TooltipNode): string {
|
|
1298
|
-
const classes = this.buildClassString([
|
|
1299
|
-
`${this.prefix}-tooltip-wrapper`,
|
|
1300
|
-
...this.getCommonClasses(node),
|
|
1301
|
-
]);
|
|
1302
|
-
|
|
1303
|
-
const styles = this.buildCommonStyles(node);
|
|
1304
|
-
const styleAttr = styles ? ` style="${styles}"` : '';
|
|
1305
|
-
|
|
1306
|
-
const position = node.position || 'top';
|
|
1307
|
-
const children = this.renderChildren(node.children);
|
|
1308
|
-
|
|
1309
|
-
return `<div class="${classes}"${styleAttr}>
|
|
1310
|
-
${children}
|
|
1311
|
-
<div class="${this.prefix}-tooltip ${this.prefix}-tooltip-${position}" role="tooltip">${this.escapeHtml(node.content)}</div>
|
|
1312
|
-
</div>`;
|
|
1313
|
-
}
|
|
1314
|
-
|
|
1315
|
-
private renderPopover(node: PopoverNode): string {
|
|
1316
|
-
const classes = this.buildClassString([
|
|
1317
|
-
`${this.prefix}-popover`,
|
|
1318
|
-
...this.getCommonClasses(node),
|
|
1319
|
-
]);
|
|
1320
|
-
|
|
1321
|
-
const styles = this.buildCommonStyles(node);
|
|
1322
|
-
const styleAttr = styles ? ` style="${styles}"` : '';
|
|
1323
|
-
|
|
1324
|
-
const title = node.title
|
|
1325
|
-
? `<div class="${this.prefix}-popover-header">${this.escapeHtml(node.title)}</div>\n`
|
|
1326
|
-
: '';
|
|
1327
|
-
const children = this.renderChildren(node.children);
|
|
1328
|
-
|
|
1329
|
-
return `<div class="${classes}"${styleAttr}>\n${title}<div class="${this.prefix}-popover-body">\n${children}\n</div>\n</div>`;
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
private renderDropdown(node: DropdownNode): string {
|
|
1333
|
-
const classes = this.buildClassString([
|
|
1334
|
-
`${this.prefix}-dropdown`,
|
|
1335
|
-
...this.getCommonClasses(node),
|
|
1336
|
-
]);
|
|
1337
|
-
|
|
1338
|
-
const styles = this.buildCommonStyles(node);
|
|
1339
|
-
const styleAttr = styles ? ` style="${styles}"` : '';
|
|
1340
|
-
|
|
1341
|
-
const items = node.items
|
|
1342
|
-
.map((item) => {
|
|
1343
|
-
if ('type' in item && item.type === 'divider') {
|
|
1344
|
-
return `<hr class="${this.prefix}-divider" />`;
|
|
1345
|
-
}
|
|
1346
|
-
// TypeScript narrowing: item is DropdownItemNode after the divider check
|
|
1347
|
-
const dropdownItem = item as { label: string; danger?: boolean; disabled?: boolean };
|
|
1348
|
-
const itemClasses = this.buildClassString([
|
|
1349
|
-
`${this.prefix}-dropdown-item`,
|
|
1350
|
-
dropdownItem.danger ? `${this.prefix}-dropdown-item-danger` : undefined,
|
|
1351
|
-
dropdownItem.disabled ? `${this.prefix}-dropdown-item-disabled` : undefined,
|
|
1352
|
-
]);
|
|
1353
|
-
return `<button class="${itemClasses}"${dropdownItem.disabled ? ' disabled' : ''}>${this.escapeHtml(dropdownItem.label)}</button>`;
|
|
1354
|
-
})
|
|
1355
|
-
.join('\n');
|
|
1356
|
-
|
|
1357
|
-
return `<div class="${classes}"${styleAttr}>\n${items}\n</div>`;
|
|
1358
|
-
}
|
|
1359
|
-
|
|
1360
|
-
// ===========================================
|
|
1361
|
-
// Navigation Node Renderers
|
|
1362
|
-
// ===========================================
|
|
1363
|
-
|
|
1364
|
-
private renderNav(node: NavNode): string {
|
|
1365
|
-
const classes = this.buildClassString([
|
|
1366
|
-
`${this.prefix}-nav`,
|
|
1367
|
-
node.vertical ? `${this.prefix}-nav-vertical` : undefined,
|
|
1368
|
-
...this.getCommonClasses(node),
|
|
1369
|
-
]);
|
|
1370
|
-
|
|
1371
|
-
const styles = this.buildCommonStyles(node);
|
|
1372
|
-
const styleAttr = styles ? ` style="${styles}"` : '';
|
|
1373
|
-
|
|
1374
|
-
const items = node.items
|
|
1375
|
-
.map((item) => {
|
|
1376
|
-
if (typeof item === 'string') {
|
|
1377
|
-
return `<a class="${this.prefix}-nav-link" href="#">${this.escapeHtml(item)}</a>`;
|
|
1378
|
-
}
|
|
1379
|
-
const linkClasses = this.buildClassString([
|
|
1380
|
-
`${this.prefix}-nav-link`,
|
|
1381
|
-
item.active ? `${this.prefix}-nav-link-active` : undefined,
|
|
1382
|
-
item.disabled ? `${this.prefix}-nav-link-disabled` : undefined,
|
|
1383
|
-
]);
|
|
1384
|
-
return `<a class="${linkClasses}" href="${item.href || '#'}">${this.escapeHtml(item.label)}</a>`;
|
|
1385
|
-
})
|
|
1386
|
-
.join('\n');
|
|
1387
|
-
|
|
1388
|
-
return `<nav class="${classes}"${styleAttr}>\n${items}\n</nav>`;
|
|
1389
|
-
}
|
|
1390
|
-
|
|
1391
|
-
private renderTabs(node: TabsNode): string {
|
|
1392
|
-
const classes = this.buildClassString([
|
|
1393
|
-
`${this.prefix}-tabs`,
|
|
1394
|
-
...this.getCommonClasses(node),
|
|
1395
|
-
]);
|
|
1396
|
-
|
|
1397
|
-
const styles = this.buildCommonStyles(node);
|
|
1398
|
-
const styleAttr = styles ? ` style="${styles}"` : '';
|
|
1399
|
-
|
|
1400
|
-
const tabList = node.items
|
|
1401
|
-
.map((label, idx) => {
|
|
1402
|
-
const isActive = idx === (node.active || 0);
|
|
1403
|
-
const tabClasses = `${this.prefix}-tab${isActive ? ` ${this.prefix}-tab-active` : ''}`;
|
|
1404
|
-
return `<button class="${tabClasses}" role="tab" aria-selected="${isActive}">${this.escapeHtml(label)}</button>`;
|
|
1405
|
-
})
|
|
1406
|
-
.join('\n');
|
|
1407
|
-
|
|
1408
|
-
return `<div class="${classes}"${styleAttr}>
|
|
1409
|
-
<div class="${this.prefix}-tab-list" role="tablist">
|
|
1410
|
-
${tabList}
|
|
1411
|
-
</div>
|
|
1412
|
-
</div>`;
|
|
1413
|
-
}
|
|
1414
|
-
|
|
1415
|
-
private renderBreadcrumb(node: BreadcrumbNode): string {
|
|
1416
|
-
const classes = this.buildClassString([
|
|
1417
|
-
`${this.prefix}-breadcrumb`,
|
|
1418
|
-
...this.getCommonClasses(node),
|
|
1419
|
-
]);
|
|
1420
|
-
|
|
1421
|
-
const styles = this.buildCommonStyles(node);
|
|
1422
|
-
const styleAttr = styles ? ` style="${styles}"` : '';
|
|
1423
|
-
|
|
1424
|
-
const items = node.items
|
|
1425
|
-
.map((item, idx) => {
|
|
1426
|
-
const isLast = idx === node.items.length - 1;
|
|
1427
|
-
if (typeof item === 'string') {
|
|
1428
|
-
return isLast
|
|
1429
|
-
? `<span class="${this.prefix}-breadcrumb-item" aria-current="page">${this.escapeHtml(item)}</span>`
|
|
1430
|
-
: `<a class="${this.prefix}-breadcrumb-item" href="#">${this.escapeHtml(item)}</a>`;
|
|
1431
|
-
}
|
|
1432
|
-
return isLast
|
|
1433
|
-
? `<span class="${this.prefix}-breadcrumb-item" aria-current="page">${this.escapeHtml(item.label)}</span>`
|
|
1434
|
-
: `<a class="${this.prefix}-breadcrumb-item" href="${item.href || '#'}">${this.escapeHtml(item.label)}</a>`;
|
|
1435
|
-
})
|
|
1436
|
-
.join(' / ');
|
|
1437
|
-
|
|
1438
|
-
return `<nav class="${classes}"${styleAttr} aria-label="Breadcrumb">${items}</nav>`;
|
|
1439
|
-
}
|
|
1440
|
-
|
|
1441
|
-
// ===========================================
|
|
1442
|
-
// Divider Renderer
|
|
1443
|
-
// ===========================================
|
|
1444
|
-
|
|
1445
|
-
private renderDivider(node: DividerComponentNode): string {
|
|
1446
|
-
const styles = this.buildCommonStyles(node);
|
|
1447
|
-
const styleAttr = styles ? ` style="${styles}"` : '';
|
|
1448
|
-
|
|
1449
|
-
return `<hr class="${this.prefix}-divider"${styleAttr} />`;
|
|
1450
|
-
}
|
|
1451
|
-
|
|
1452
|
-
// ===========================================
|
|
1453
|
-
// Semantic Marker Rendering
|
|
1454
|
-
// ===========================================
|
|
1455
|
-
|
|
1456
|
-
/**
|
|
1457
|
-
* Parse and render semantic markers in text content
|
|
1458
|
-
*
|
|
1459
|
-
* Semantic markers use the syntax [component:variant] to indicate
|
|
1460
|
-
* what a visual element represents. This helps LLMs understand
|
|
1461
|
-
* the meaning of placeholder content.
|
|
1462
|
-
*
|
|
1463
|
-
* Supported markers:
|
|
1464
|
-
* - [avatar] or [avatar:size] - User avatar (renders as circle placeholder)
|
|
1465
|
-
* - [badge:variant] TEXT - Status badge (TEXT is displayed inside the badge)
|
|
1466
|
-
* - [dot:variant] - Status dot (renders as small circle before text)
|
|
1467
|
-
* - [icon:name] - Icon placeholder
|
|
1468
|
-
*
|
|
1469
|
-
* Examples:
|
|
1470
|
-
* - "[avatar] John Doe" → renders avatar circle + "John Doe"
|
|
1471
|
-
* - "[badge:primary] PRO" → renders badge containing "PRO"
|
|
1472
|
-
* - "[dot:success] Active" → renders green dot + "Active"
|
|
1473
|
-
*/
|
|
1474
|
-
private renderSemanticMarkers(text: string): string {
|
|
1475
|
-
// Pattern: [component] or [component:variant] with optional following text for badge
|
|
1476
|
-
const markerPattern = /\[([a-z]+)(?::([a-z0-9-]+))?\](\s*)/gi;
|
|
1477
|
-
|
|
1478
|
-
let result = '';
|
|
1479
|
-
let lastIndex = 0;
|
|
1480
|
-
let match: RegExpExecArray | null;
|
|
1481
|
-
|
|
1482
|
-
while ((match = markerPattern.exec(text)) !== null) {
|
|
1483
|
-
// Add text before the marker
|
|
1484
|
-
if (match.index > lastIndex) {
|
|
1485
|
-
result += this.escapeHtml(text.substring(lastIndex, match.index));
|
|
1486
|
-
}
|
|
1487
|
-
|
|
1488
|
-
const [fullMatch, component, variant] = match;
|
|
1489
|
-
const comp = component.toLowerCase();
|
|
1490
|
-
const varnt = variant?.toLowerCase();
|
|
1491
|
-
|
|
1492
|
-
// For badge, consume the following word as the badge content
|
|
1493
|
-
if (comp === 'badge') {
|
|
1494
|
-
const afterMarker = text.substring(match.index + fullMatch.length);
|
|
1495
|
-
// Match until newline, next marker, or end
|
|
1496
|
-
const contentMatch = afterMarker.match(/^([^\n\[]+?)(?=\n|\[|$)/);
|
|
1497
|
-
const badgeContent = contentMatch ? contentMatch[1].trim() : '';
|
|
1498
|
-
|
|
1499
|
-
result += this.renderSemanticMarkerWithContent(comp, varnt, badgeContent);
|
|
1500
|
-
lastIndex = match.index + fullMatch.length + (contentMatch ? contentMatch[0].length : 0);
|
|
1501
|
-
markerPattern.lastIndex = lastIndex; // Update regex position
|
|
1502
|
-
} else {
|
|
1503
|
-
result += this.renderSemanticMarker(comp, varnt);
|
|
1504
|
-
lastIndex = match.index + fullMatch.length;
|
|
1505
|
-
}
|
|
1506
|
-
}
|
|
1507
|
-
|
|
1508
|
-
// Add remaining text after last marker
|
|
1509
|
-
if (lastIndex < text.length) {
|
|
1510
|
-
result += this.escapeHtml(text.substring(lastIndex));
|
|
1511
|
-
}
|
|
1512
|
-
|
|
1513
|
-
// If no markers found, just escape and return
|
|
1514
|
-
if (lastIndex === 0) {
|
|
1515
|
-
return this.escapeHtml(text);
|
|
1516
|
-
}
|
|
1517
|
-
|
|
1518
|
-
return result;
|
|
1519
|
-
}
|
|
1520
|
-
|
|
1521
|
-
/**
|
|
1522
|
-
* Render a single semantic marker to HTML (without content)
|
|
1523
|
-
*/
|
|
1524
|
-
private renderSemanticMarker(component: string, variant?: string): string {
|
|
1525
|
-
const prefix = this.prefix;
|
|
1526
|
-
|
|
1527
|
-
switch (component) {
|
|
1528
|
-
case 'avatar':
|
|
1529
|
-
// Render as small circle placeholder
|
|
1530
|
-
const avatarSize = variant || 'sm';
|
|
1531
|
-
return `<span class="${prefix}-semantic-avatar ${prefix}-semantic-avatar-${avatarSize}" data-semantic="avatar" data-variant="${avatarSize}" aria-hidden="true"></span>`;
|
|
1532
|
-
|
|
1533
|
-
case 'dot':
|
|
1534
|
-
// Render as status dot
|
|
1535
|
-
const dotVariant = variant || 'default';
|
|
1536
|
-
return `<span class="${prefix}-semantic-dot ${prefix}-semantic-dot-${dotVariant}" data-semantic="dot" data-variant="${dotVariant}" aria-hidden="true"></span>`;
|
|
1537
|
-
|
|
1538
|
-
case 'icon':
|
|
1539
|
-
// Render as icon placeholder
|
|
1540
|
-
const iconName = variant || 'default';
|
|
1541
|
-
return `<span class="${prefix}-semantic-icon" data-semantic="icon" data-variant="${iconName}" aria-hidden="true">[${iconName}]</span>`;
|
|
1542
|
-
|
|
1543
|
-
default:
|
|
1544
|
-
// Unknown marker - render as data attribute only
|
|
1545
|
-
return `<span class="${prefix}-semantic-unknown" data-semantic="${component}" data-variant="${variant || ''}">[${component}${variant ? ':' + variant : ''}]</span>`;
|
|
1546
|
-
}
|
|
1547
|
-
}
|
|
1548
|
-
|
|
1549
|
-
/**
|
|
1550
|
-
* Render a semantic marker with text content (for badge)
|
|
1551
|
-
*/
|
|
1552
|
-
private renderSemanticMarkerWithContent(component: string, variant: string | undefined, content: string): string {
|
|
1553
|
-
const prefix = this.prefix;
|
|
1554
|
-
|
|
1555
|
-
switch (component) {
|
|
1556
|
-
case 'badge':
|
|
1557
|
-
// Render as inline badge with content inside
|
|
1558
|
-
const badgeVariant = variant || 'default';
|
|
1559
|
-
const escapedContent = this.escapeHtml(content);
|
|
1560
|
-
return `<span class="${prefix}-semantic-badge ${prefix}-semantic-badge-${badgeVariant}" data-semantic="badge" data-variant="${badgeVariant}">${escapedContent}</span>`;
|
|
1561
|
-
|
|
1562
|
-
default:
|
|
1563
|
-
// Fallback: render marker then content
|
|
1564
|
-
return this.renderSemanticMarker(component, variant) + this.escapeHtml(content);
|
|
1565
|
-
}
|
|
1566
|
-
}
|
|
1567
|
-
|
|
1568
|
-
/**
|
|
1569
|
-
* Process table cell content with semantic markers and newlines
|
|
1570
|
-
*
|
|
1571
|
-
* Special handling for avatar + text layout:
|
|
1572
|
-
* When content starts with [avatar], wraps in flex container
|
|
1573
|
-
* so avatar and text align horizontally, with text stacking vertically
|
|
1574
|
-
*/
|
|
1575
|
-
private renderTableCellContent(content: string): string {
|
|
1576
|
-
// Check if content starts with [avatar] marker
|
|
1577
|
-
const avatarMatch = content.match(/^\[avatar(?::([a-z0-9-]+))?\]\s*/i);
|
|
1578
|
-
|
|
1579
|
-
if (avatarMatch) {
|
|
1580
|
-
// Avatar + text layout: flex container with avatar and text block
|
|
1581
|
-
const avatarVariant = avatarMatch[1]?.toLowerCase();
|
|
1582
|
-
const avatarHtml = this.renderSemanticMarker('avatar', avatarVariant);
|
|
1583
|
-
const restContent = content.slice(avatarMatch[0].length);
|
|
1584
|
-
|
|
1585
|
-
// Process remaining content for other markers
|
|
1586
|
-
const restHtml = this.renderSemanticMarkers(restContent);
|
|
1587
|
-
// Convert newlines to flex items for vertical stacking
|
|
1588
|
-
const lines = restHtml.split('\n');
|
|
1589
|
-
const textHtml =
|
|
1590
|
-
lines.length > 1
|
|
1591
|
-
? lines.map((line) => `<span>${line}</span>`).join('')
|
|
1592
|
-
: restHtml;
|
|
1593
|
-
|
|
1594
|
-
return `<div class="${this.prefix}-cell-avatar-layout">${avatarHtml}<div class="${this.prefix}-cell-avatar-text">${textHtml}</div></div>`;
|
|
1595
|
-
}
|
|
1596
|
-
|
|
1597
|
-
// Normal rendering: semantic markers then newlines to <br>
|
|
1598
|
-
const withMarkers = this.renderSemanticMarkers(content);
|
|
1599
|
-
return withMarkers.replace(/\n/g, '<br>');
|
|
1600
|
-
}
|
|
1601
|
-
}
|
|
1602
|
-
|
|
1603
|
-
/**
|
|
1604
|
-
* Create a new HTML renderer instance
|
|
1605
|
-
*/
|
|
1606
|
-
export function createHtmlRenderer(options?: RenderOptions): HtmlRenderer {
|
|
1607
|
-
return new HtmlRenderer(options);
|
|
1608
|
-
}
|