@wireweave/core 1.0.0-beta.20260107130839 → 1.0.0-beta.20260107132939
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,1092 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Component Renderers for wireweave
|
|
3
|
-
*
|
|
4
|
-
* Dedicated renderers for all UI components with:
|
|
5
|
-
* - XSS prevention via HTML escaping
|
|
6
|
-
* - Accessibility attributes (role, aria-*)
|
|
7
|
-
* - Consistent class naming
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import type {
|
|
11
|
-
AnyNode,
|
|
12
|
-
CardNode,
|
|
13
|
-
ModalNode,
|
|
14
|
-
DrawerNode,
|
|
15
|
-
AccordionNode,
|
|
16
|
-
TextNode,
|
|
17
|
-
TitleNode,
|
|
18
|
-
LinkNode,
|
|
19
|
-
InputNode,
|
|
20
|
-
TextareaNode,
|
|
21
|
-
SelectNode,
|
|
22
|
-
CheckboxNode,
|
|
23
|
-
RadioNode,
|
|
24
|
-
SwitchNode,
|
|
25
|
-
SliderNode,
|
|
26
|
-
ButtonNode,
|
|
27
|
-
ImageNode,
|
|
28
|
-
PlaceholderNode,
|
|
29
|
-
AvatarNode,
|
|
30
|
-
BadgeNode,
|
|
31
|
-
IconNode,
|
|
32
|
-
TableNode,
|
|
33
|
-
ListNode,
|
|
34
|
-
AlertNode,
|
|
35
|
-
ToastNode,
|
|
36
|
-
ProgressNode,
|
|
37
|
-
SpinnerNode,
|
|
38
|
-
TooltipNode,
|
|
39
|
-
PopoverNode,
|
|
40
|
-
DropdownNode,
|
|
41
|
-
NavNode,
|
|
42
|
-
TabsNode,
|
|
43
|
-
BreadcrumbNode,
|
|
44
|
-
DividerComponentNode,
|
|
45
|
-
CommonProps,
|
|
46
|
-
} from '../../ast/types';
|
|
47
|
-
import type { RenderContext } from '../types';
|
|
48
|
-
import { getIconData, renderIconSvg } from '../../icons/lucide-icons';
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Type for children renderer callback (re-exported from layout)
|
|
52
|
-
*/
|
|
53
|
-
import type { ChildrenRenderer } from './layout';
|
|
54
|
-
export type { ChildrenRenderer };
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Component node types
|
|
58
|
-
*/
|
|
59
|
-
export type ComponentNodeType =
|
|
60
|
-
| 'Card' | 'Modal' | 'Drawer' | 'Accordion'
|
|
61
|
-
| 'Text' | 'Title' | 'Link'
|
|
62
|
-
| 'Input' | 'Textarea' | 'Select' | 'Checkbox' | 'Radio' | 'Switch' | 'Slider'
|
|
63
|
-
| 'Button'
|
|
64
|
-
| 'Image' | 'Placeholder' | 'Avatar' | 'Badge' | 'Icon'
|
|
65
|
-
| 'Table' | 'List'
|
|
66
|
-
| 'Alert' | 'Toast' | 'Progress' | 'Spinner'
|
|
67
|
-
| 'Tooltip' | 'Popover' | 'Dropdown'
|
|
68
|
-
| 'Nav' | 'Tabs' | 'Breadcrumb'
|
|
69
|
-
| 'Divider';
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Check if a node type is a component node
|
|
73
|
-
*/
|
|
74
|
-
export function isComponentNodeType(type: string): type is ComponentNodeType {
|
|
75
|
-
const componentTypes: string[] = [
|
|
76
|
-
'Card', 'Modal', 'Drawer', 'Accordion',
|
|
77
|
-
'Text', 'Title', 'Link',
|
|
78
|
-
'Input', 'Textarea', 'Select', 'Checkbox', 'Radio', 'Switch', 'Slider',
|
|
79
|
-
'Button',
|
|
80
|
-
'Image', 'Placeholder', 'Avatar', 'Badge', 'Icon',
|
|
81
|
-
'Table', 'List',
|
|
82
|
-
'Alert', 'Toast', 'Progress', 'Spinner',
|
|
83
|
-
'Tooltip', 'Popover', 'Dropdown',
|
|
84
|
-
'Nav', 'Tabs', 'Breadcrumb',
|
|
85
|
-
'Divider',
|
|
86
|
-
];
|
|
87
|
-
return componentTypes.includes(type);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// ===========================================
|
|
91
|
-
// Utility Functions
|
|
92
|
-
// ===========================================
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Escape HTML special characters to prevent XSS
|
|
96
|
-
*/
|
|
97
|
-
export function escapeHtml(text: string): string {
|
|
98
|
-
const escapeMap: Record<string, string> = {
|
|
99
|
-
'&': '&',
|
|
100
|
-
'<': '<',
|
|
101
|
-
'>': '>',
|
|
102
|
-
'"': '"',
|
|
103
|
-
"'": ''',
|
|
104
|
-
};
|
|
105
|
-
return text.replace(/[&<>"']/g, (char) => escapeMap[char] || char);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Build CSS class string from an array
|
|
110
|
-
*/
|
|
111
|
-
export function buildClassString(classes: (string | undefined | false)[]): string {
|
|
112
|
-
return classes.filter(Boolean).join(' ');
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Build HTML attributes string
|
|
117
|
-
*/
|
|
118
|
-
export function buildAttrsString(attrs: Record<string, string | undefined | boolean>): string {
|
|
119
|
-
const parts: string[] = [];
|
|
120
|
-
|
|
121
|
-
for (const [key, value] of Object.entries(attrs)) {
|
|
122
|
-
if (value === undefined || value === false) {
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (value === true) {
|
|
127
|
-
parts.push(key);
|
|
128
|
-
} else {
|
|
129
|
-
parts.push(`${key}="${escapeHtml(value)}"`);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return parts.length > 0 ? ' ' + parts.join(' ') : '';
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Get common CSS classes from props
|
|
138
|
-
* Uses Omit to exclude 'align' since TextNode/TitleNode have incompatible align types
|
|
139
|
-
*/
|
|
140
|
-
export function getCommonClasses(props: Omit<Partial<CommonProps>, 'align'> & { align?: string }, prefix: string): string[] {
|
|
141
|
-
const classes: string[] = [];
|
|
142
|
-
|
|
143
|
-
// Spacing
|
|
144
|
-
if (props.p !== undefined) classes.push(`${prefix}-p-${props.p}`);
|
|
145
|
-
if (props.px !== undefined) classes.push(`${prefix}-px-${props.px}`);
|
|
146
|
-
if (props.py !== undefined) classes.push(`${prefix}-py-${props.py}`);
|
|
147
|
-
if (props.pt !== undefined) classes.push(`${prefix}-pt-${props.pt}`);
|
|
148
|
-
if (props.pr !== undefined) classes.push(`${prefix}-pr-${props.pr}`);
|
|
149
|
-
if (props.pb !== undefined) classes.push(`${prefix}-pb-${props.pb}`);
|
|
150
|
-
if (props.pl !== undefined) classes.push(`${prefix}-pl-${props.pl}`);
|
|
151
|
-
if (props.m !== undefined) classes.push(`${prefix}-m-${props.m}`);
|
|
152
|
-
if (props.mx !== undefined) classes.push(`${prefix}-mx-${props.mx}`);
|
|
153
|
-
if (props.my !== undefined) classes.push(`${prefix}-my-${props.my}`);
|
|
154
|
-
if (props.mt !== undefined) classes.push(`${prefix}-mt-${props.mt}`);
|
|
155
|
-
if (props.mr !== undefined) classes.push(`${prefix}-mr-${props.mr}`);
|
|
156
|
-
if (props.mb !== undefined) classes.push(`${prefix}-mb-${props.mb}`);
|
|
157
|
-
if (props.ml !== undefined) classes.push(`${prefix}-ml-${props.ml}`);
|
|
158
|
-
|
|
159
|
-
// Size
|
|
160
|
-
if (props.w === 'full') classes.push(`${prefix}-w-full`);
|
|
161
|
-
if (props.w === 'auto') classes.push(`${prefix}-w-auto`);
|
|
162
|
-
if (props.w === 'fit') classes.push(`${prefix}-w-fit`);
|
|
163
|
-
if (props.h === 'full') classes.push(`${prefix}-h-full`);
|
|
164
|
-
if (props.h === 'auto') classes.push(`${prefix}-h-auto`);
|
|
165
|
-
if (props.h === 'screen') classes.push(`${prefix}-h-screen`);
|
|
166
|
-
|
|
167
|
-
// Flex
|
|
168
|
-
if (props.flex === true) classes.push(`${prefix}-flex`);
|
|
169
|
-
if (typeof props.flex === 'number') classes.push(`${prefix}-flex-${props.flex}`);
|
|
170
|
-
if (props.direction === 'row') classes.push(`${prefix}-flex-row`);
|
|
171
|
-
if (props.direction === 'column') classes.push(`${prefix}-flex-col`);
|
|
172
|
-
if (props.direction === 'row-reverse') classes.push(`${prefix}-flex-row-reverse`);
|
|
173
|
-
if (props.direction === 'column-reverse') classes.push(`${prefix}-flex-col-reverse`);
|
|
174
|
-
if (props.justify) classes.push(`${prefix}-justify-${props.justify}`);
|
|
175
|
-
if (props.align) classes.push(`${prefix}-align-${props.align}`);
|
|
176
|
-
if (props.wrap === true) classes.push(`${prefix}-flex-wrap`);
|
|
177
|
-
if (props.wrap === 'nowrap') classes.push(`${prefix}-flex-nowrap`);
|
|
178
|
-
if (props.gap !== undefined) classes.push(`${prefix}-gap-${props.gap}`);
|
|
179
|
-
|
|
180
|
-
return classes;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Size token definitions for each component type
|
|
185
|
-
* Maps token strings (xs, sm, md, lg, xl) to pixel values
|
|
186
|
-
*/
|
|
187
|
-
const SIZE_TOKENS = {
|
|
188
|
-
icon: { xs: 12, sm: 14, md: 16, lg: 20, xl: 24 },
|
|
189
|
-
avatar: { xs: 24, sm: 32, md: 40, lg: 48, xl: 64 },
|
|
190
|
-
spinner: { xs: 12, sm: 16, md: 24, lg: 32, xl: 48 },
|
|
191
|
-
} as const;
|
|
192
|
-
|
|
193
|
-
type SizeTokenType = keyof typeof SIZE_TOKENS;
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Resolve size value to either a CSS class name or inline style
|
|
197
|
-
* Supports both token strings (xs, sm, md, lg, xl) and custom px numbers
|
|
198
|
-
*
|
|
199
|
-
* @param size - Size value (token string or number in px)
|
|
200
|
-
* @param componentType - Component type for token lookup
|
|
201
|
-
* @param prefix - CSS class prefix
|
|
202
|
-
* @returns Object with className and style for the component
|
|
203
|
-
*/
|
|
204
|
-
export function resolveSizeValue(
|
|
205
|
-
size: string | number | undefined,
|
|
206
|
-
componentType: SizeTokenType,
|
|
207
|
-
prefix: string
|
|
208
|
-
): { className?: string; style?: string } {
|
|
209
|
-
if (size === undefined) {
|
|
210
|
-
return {};
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// If it's a known token, use CSS class
|
|
214
|
-
if (typeof size === 'string') {
|
|
215
|
-
const tokens = SIZE_TOKENS[componentType];
|
|
216
|
-
if (size in tokens) {
|
|
217
|
-
return { className: `${prefix}-${componentType}-${size}` };
|
|
218
|
-
}
|
|
219
|
-
// Unknown string, try to parse as number
|
|
220
|
-
const parsed = parseInt(size, 10);
|
|
221
|
-
if (!isNaN(parsed)) {
|
|
222
|
-
return { style: `width: ${parsed}px; height: ${parsed}px;` };
|
|
223
|
-
}
|
|
224
|
-
return {};
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// If it's a number, use inline style
|
|
228
|
-
if (typeof size === 'number') {
|
|
229
|
-
return { style: `width: ${size}px; height: ${size}px;` };
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
return {};
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Render an icon using Lucide icons
|
|
237
|
-
* Falls back to text placeholder if icon not found
|
|
238
|
-
*/
|
|
239
|
-
export function renderIconPlaceholder(name: string, prefix: string, size: number = 16): string {
|
|
240
|
-
const iconData = getIconData(name);
|
|
241
|
-
|
|
242
|
-
if (iconData) {
|
|
243
|
-
return renderIconSvg(iconData, size, 2, `${prefix}-icon`);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// Fallback to text placeholder for unknown icons
|
|
247
|
-
return `<span class="${prefix}-icon" aria-hidden="true">[${escapeHtml(name)}]</span>`;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// ===========================================
|
|
251
|
-
// Container Component Renderers
|
|
252
|
-
// ===========================================
|
|
253
|
-
|
|
254
|
-
export function renderCard(
|
|
255
|
-
node: CardNode,
|
|
256
|
-
context: RenderContext,
|
|
257
|
-
renderChildren: ChildrenRenderer
|
|
258
|
-
): string {
|
|
259
|
-
const prefix = context.options.classPrefix;
|
|
260
|
-
const classes = buildClassString([
|
|
261
|
-
`${prefix}-card`,
|
|
262
|
-
node.shadow ? `${prefix}-card-shadow-${node.shadow}` : undefined,
|
|
263
|
-
...getCommonClasses(node, prefix),
|
|
264
|
-
]);
|
|
265
|
-
|
|
266
|
-
const title = node.title
|
|
267
|
-
? `<h3 class="${prefix}-card-title">${escapeHtml(node.title)}</h3>\n`
|
|
268
|
-
: '';
|
|
269
|
-
const children = renderChildren(node.children);
|
|
270
|
-
|
|
271
|
-
return `<div class="${classes}">\n${title}${children}\n</div>`;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
export function renderModal(
|
|
275
|
-
node: ModalNode,
|
|
276
|
-
context: RenderContext,
|
|
277
|
-
renderChildren: ChildrenRenderer
|
|
278
|
-
): string {
|
|
279
|
-
const prefix = context.options.classPrefix;
|
|
280
|
-
const classes = buildClassString([
|
|
281
|
-
`${prefix}-modal`,
|
|
282
|
-
...getCommonClasses(node, prefix),
|
|
283
|
-
]);
|
|
284
|
-
|
|
285
|
-
// Build inline styles for numeric width/height
|
|
286
|
-
const styles: string[] = [];
|
|
287
|
-
if (typeof node.w === 'number') {
|
|
288
|
-
styles.push(`width: ${node.w}px`);
|
|
289
|
-
}
|
|
290
|
-
if (typeof node.h === 'number') {
|
|
291
|
-
styles.push(`height: ${node.h}px`);
|
|
292
|
-
}
|
|
293
|
-
const styleAttr = styles.length > 0 ? ` style="${styles.join('; ')}"` : '';
|
|
294
|
-
|
|
295
|
-
const title = node.title
|
|
296
|
-
? `<h2 class="${prefix}-modal-title">${escapeHtml(node.title)}</h2>\n`
|
|
297
|
-
: '';
|
|
298
|
-
const children = renderChildren(node.children);
|
|
299
|
-
|
|
300
|
-
return `<div class="${prefix}-modal-backdrop">
|
|
301
|
-
<div class="${classes}" role="dialog" aria-modal="true"${styleAttr}${node.title ? ` aria-labelledby="modal-title"` : ''}>
|
|
302
|
-
${title}${children}
|
|
303
|
-
</div>
|
|
304
|
-
</div>`;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
export function renderDrawer(
|
|
308
|
-
node: DrawerNode,
|
|
309
|
-
context: RenderContext,
|
|
310
|
-
renderChildren: ChildrenRenderer
|
|
311
|
-
): string {
|
|
312
|
-
const prefix = context.options.classPrefix;
|
|
313
|
-
const position = node.position || 'left';
|
|
314
|
-
const classes = buildClassString([
|
|
315
|
-
`${prefix}-drawer`,
|
|
316
|
-
`${prefix}-drawer-${position}`,
|
|
317
|
-
...getCommonClasses(node, prefix),
|
|
318
|
-
]);
|
|
319
|
-
|
|
320
|
-
const title = node.title
|
|
321
|
-
? `<h2 class="${prefix}-drawer-title">${escapeHtml(node.title)}</h2>\n`
|
|
322
|
-
: '';
|
|
323
|
-
const children = renderChildren(node.children);
|
|
324
|
-
|
|
325
|
-
return `<aside class="${classes}" role="complementary">\n${title}${children}\n</aside>`;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
export function renderAccordion(
|
|
329
|
-
node: AccordionNode,
|
|
330
|
-
context: RenderContext,
|
|
331
|
-
renderChildren: ChildrenRenderer
|
|
332
|
-
): string {
|
|
333
|
-
const prefix = context.options.classPrefix;
|
|
334
|
-
const classes = buildClassString([
|
|
335
|
-
`${prefix}-accordion`,
|
|
336
|
-
...getCommonClasses(node, prefix),
|
|
337
|
-
]);
|
|
338
|
-
|
|
339
|
-
const title = node.title
|
|
340
|
-
? `<button class="${prefix}-accordion-header" aria-expanded="false">${escapeHtml(node.title)}</button>\n`
|
|
341
|
-
: '';
|
|
342
|
-
const children = renderChildren(node.children);
|
|
343
|
-
|
|
344
|
-
return `<div class="${classes}">\n${title}<div class="${prefix}-accordion-content" role="region">\n${children}\n</div>\n</div>`;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// ===========================================
|
|
348
|
-
// Text Component Renderers
|
|
349
|
-
// ===========================================
|
|
350
|
-
|
|
351
|
-
export function renderText(node: TextNode, context: RenderContext): string {
|
|
352
|
-
const prefix = context.options.classPrefix;
|
|
353
|
-
|
|
354
|
-
// Handle size: token (string) vs direct value (ValueWithUnit)
|
|
355
|
-
const isTokenSize = typeof node.size === 'string';
|
|
356
|
-
const sizeClass = isTokenSize && node.size ? `${prefix}-text-${node.size}` : undefined;
|
|
357
|
-
const sizeStyle = !isTokenSize && node.size && typeof node.size === 'object' && 'value' in node.size
|
|
358
|
-
? `font-size: ${node.size.value}${node.size.unit}`
|
|
359
|
-
: undefined;
|
|
360
|
-
|
|
361
|
-
const classes = buildClassString([
|
|
362
|
-
`${prefix}-text`,
|
|
363
|
-
sizeClass,
|
|
364
|
-
node.weight ? `${prefix}-text-${node.weight}` : undefined,
|
|
365
|
-
node.align ? `${prefix}-text-${node.align}` : undefined,
|
|
366
|
-
node.muted ? `${prefix}-text-muted` : undefined,
|
|
367
|
-
...getCommonClasses(node, prefix),
|
|
368
|
-
]);
|
|
369
|
-
|
|
370
|
-
const styleAttr = sizeStyle ? ` style="${sizeStyle}"` : '';
|
|
371
|
-
|
|
372
|
-
return `<p class="${classes}"${styleAttr}>${escapeHtml(node.content)}</p>`;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
export function renderTitle(node: TitleNode, context: RenderContext): string {
|
|
376
|
-
const prefix = context.options.classPrefix;
|
|
377
|
-
const level = Math.min(Math.max(node.level || 1, 1), 6);
|
|
378
|
-
const tag = `h${level}`;
|
|
379
|
-
|
|
380
|
-
// Handle size: token (string) vs direct value (ValueWithUnit)
|
|
381
|
-
const isTokenSize = typeof node.size === 'string';
|
|
382
|
-
const sizeClass = isTokenSize && node.size ? `${prefix}-text-${node.size}` : undefined;
|
|
383
|
-
const sizeStyle = !isTokenSize && node.size && typeof node.size === 'object' && 'value' in node.size
|
|
384
|
-
? `font-size: ${node.size.value}${node.size.unit}`
|
|
385
|
-
: undefined;
|
|
386
|
-
|
|
387
|
-
const classes = buildClassString([
|
|
388
|
-
`${prefix}-title`,
|
|
389
|
-
sizeClass,
|
|
390
|
-
node.align ? `${prefix}-text-${node.align}` : undefined,
|
|
391
|
-
...getCommonClasses(node, prefix),
|
|
392
|
-
]);
|
|
393
|
-
|
|
394
|
-
const styleAttr = sizeStyle ? ` style="${sizeStyle}"` : '';
|
|
395
|
-
|
|
396
|
-
return `<${tag} class="${classes}"${styleAttr}>${escapeHtml(node.content)}</${tag}>`;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
export function renderLink(node: LinkNode, context: RenderContext): string {
|
|
400
|
-
const prefix = context.options.classPrefix;
|
|
401
|
-
const classes = buildClassString([
|
|
402
|
-
`${prefix}-link`,
|
|
403
|
-
...getCommonClasses(node, prefix),
|
|
404
|
-
]);
|
|
405
|
-
|
|
406
|
-
const attrs: Record<string, string | boolean | undefined> = {
|
|
407
|
-
class: classes,
|
|
408
|
-
href: node.href || '#',
|
|
409
|
-
};
|
|
410
|
-
|
|
411
|
-
if (node.external) {
|
|
412
|
-
attrs.target = '_blank';
|
|
413
|
-
attrs.rel = 'noopener noreferrer';
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
return `<a${buildAttrsString(attrs)}>${escapeHtml(node.content)}</a>`;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
// ===========================================
|
|
420
|
-
// Input Component Renderers
|
|
421
|
-
// ===========================================
|
|
422
|
-
|
|
423
|
-
export function renderInput(node: InputNode, context: RenderContext): string {
|
|
424
|
-
const prefix = context.options.classPrefix;
|
|
425
|
-
const classes = buildClassString([
|
|
426
|
-
`${prefix}-input`,
|
|
427
|
-
...getCommonClasses(node, prefix),
|
|
428
|
-
]);
|
|
429
|
-
|
|
430
|
-
const attrs: Record<string, string | boolean | undefined> = {
|
|
431
|
-
class: classes,
|
|
432
|
-
type: node.inputType || 'text',
|
|
433
|
-
placeholder: node.placeholder,
|
|
434
|
-
value: node.value,
|
|
435
|
-
disabled: node.disabled,
|
|
436
|
-
required: node.required,
|
|
437
|
-
readonly: node.readonly,
|
|
438
|
-
'aria-required': node.required ? 'true' : undefined,
|
|
439
|
-
};
|
|
440
|
-
|
|
441
|
-
const input = `<input${buildAttrsString(attrs)} />`;
|
|
442
|
-
|
|
443
|
-
if (node.label) {
|
|
444
|
-
const labelId = `input-${Math.random().toString(36).substr(2, 9)}`;
|
|
445
|
-
return `<div class="${prefix}-form-field">
|
|
446
|
-
<label class="${prefix}-input-label" for="${labelId}">${escapeHtml(node.label)}</label>
|
|
447
|
-
<input${buildAttrsString({ ...attrs, id: labelId })} />
|
|
448
|
-
</div>`;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
return input;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
export function renderTextarea(node: TextareaNode, context: RenderContext): string {
|
|
455
|
-
const prefix = context.options.classPrefix;
|
|
456
|
-
const classes = buildClassString([
|
|
457
|
-
`${prefix}-input`,
|
|
458
|
-
`${prefix}-textarea`,
|
|
459
|
-
...getCommonClasses(node, prefix),
|
|
460
|
-
]);
|
|
461
|
-
|
|
462
|
-
const attrs: Record<string, string | boolean | undefined> = {
|
|
463
|
-
class: classes,
|
|
464
|
-
placeholder: node.placeholder,
|
|
465
|
-
disabled: node.disabled,
|
|
466
|
-
required: node.required,
|
|
467
|
-
rows: node.rows?.toString(),
|
|
468
|
-
'aria-required': node.required ? 'true' : undefined,
|
|
469
|
-
};
|
|
470
|
-
|
|
471
|
-
const textarea = `<textarea${buildAttrsString(attrs)}>${escapeHtml(node.value || '')}</textarea>`;
|
|
472
|
-
|
|
473
|
-
if (node.label) {
|
|
474
|
-
return `<div class="${prefix}-form-field">
|
|
475
|
-
<label class="${prefix}-input-label">${escapeHtml(node.label)}</label>
|
|
476
|
-
${textarea}
|
|
477
|
-
</div>`;
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
return textarea;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
export function renderSelect(node: SelectNode, context: RenderContext): string {
|
|
484
|
-
const prefix = context.options.classPrefix;
|
|
485
|
-
const classes = buildClassString([
|
|
486
|
-
`${prefix}-input`,
|
|
487
|
-
`${prefix}-select`,
|
|
488
|
-
...getCommonClasses(node, prefix),
|
|
489
|
-
]);
|
|
490
|
-
|
|
491
|
-
const attrs: Record<string, string | boolean | undefined> = {
|
|
492
|
-
class: classes,
|
|
493
|
-
disabled: node.disabled,
|
|
494
|
-
required: node.required,
|
|
495
|
-
'aria-required': node.required ? 'true' : undefined,
|
|
496
|
-
};
|
|
497
|
-
|
|
498
|
-
const options = node.options
|
|
499
|
-
.map((opt) => {
|
|
500
|
-
if (typeof opt === 'string') {
|
|
501
|
-
const selected = opt === node.value ? ' selected' : '';
|
|
502
|
-
return `<option value="${escapeHtml(opt)}"${selected}>${escapeHtml(opt)}</option>`;
|
|
503
|
-
}
|
|
504
|
-
const selected = opt.value === node.value ? ' selected' : '';
|
|
505
|
-
return `<option value="${escapeHtml(opt.value)}"${selected}>${escapeHtml(opt.label)}</option>`;
|
|
506
|
-
})
|
|
507
|
-
.join('\n');
|
|
508
|
-
|
|
509
|
-
const placeholder = node.placeholder
|
|
510
|
-
? `<option value="" disabled selected>${escapeHtml(node.placeholder)}</option>\n`
|
|
511
|
-
: '';
|
|
512
|
-
|
|
513
|
-
const select = `<select${buildAttrsString(attrs)}>\n${placeholder}${options}\n</select>`;
|
|
514
|
-
|
|
515
|
-
if (node.label) {
|
|
516
|
-
return `<div class="${prefix}-form-field">
|
|
517
|
-
<label class="${prefix}-input-label">${escapeHtml(node.label)}</label>
|
|
518
|
-
${select}
|
|
519
|
-
</div>`;
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
return select;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
export function renderCheckbox(node: CheckboxNode, context: RenderContext): string {
|
|
526
|
-
const prefix = context.options.classPrefix;
|
|
527
|
-
|
|
528
|
-
const attrs: Record<string, string | boolean | undefined> = {
|
|
529
|
-
type: 'checkbox',
|
|
530
|
-
checked: node.checked,
|
|
531
|
-
disabled: node.disabled,
|
|
532
|
-
'aria-checked': node.checked ? 'true' : 'false',
|
|
533
|
-
};
|
|
534
|
-
|
|
535
|
-
const checkbox = `<input${buildAttrsString(attrs)} />`;
|
|
536
|
-
|
|
537
|
-
if (node.label) {
|
|
538
|
-
return `<label class="${prefix}-checkbox">
|
|
539
|
-
${checkbox}
|
|
540
|
-
<span class="${prefix}-checkbox-label">${escapeHtml(node.label)}</span>
|
|
541
|
-
</label>`;
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
return checkbox;
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
export function renderRadio(node: RadioNode, context: RenderContext): string {
|
|
548
|
-
const prefix = context.options.classPrefix;
|
|
549
|
-
|
|
550
|
-
const attrs: Record<string, string | boolean | undefined> = {
|
|
551
|
-
type: 'radio',
|
|
552
|
-
name: node.name,
|
|
553
|
-
checked: node.checked,
|
|
554
|
-
disabled: node.disabled,
|
|
555
|
-
'aria-checked': node.checked ? 'true' : 'false',
|
|
556
|
-
};
|
|
557
|
-
|
|
558
|
-
const radio = `<input${buildAttrsString(attrs)} />`;
|
|
559
|
-
|
|
560
|
-
if (node.label) {
|
|
561
|
-
return `<label class="${prefix}-radio">
|
|
562
|
-
${radio}
|
|
563
|
-
<span class="${prefix}-radio-label">${escapeHtml(node.label)}</span>
|
|
564
|
-
</label>`;
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
return radio;
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
export function renderSwitch(node: SwitchNode, context: RenderContext): string {
|
|
571
|
-
const prefix = context.options.classPrefix;
|
|
572
|
-
const classes = buildClassString([
|
|
573
|
-
`${prefix}-switch`,
|
|
574
|
-
...getCommonClasses(node, prefix),
|
|
575
|
-
]);
|
|
576
|
-
|
|
577
|
-
const attrs: Record<string, string | boolean | undefined> = {
|
|
578
|
-
type: 'checkbox',
|
|
579
|
-
role: 'switch',
|
|
580
|
-
checked: node.checked,
|
|
581
|
-
disabled: node.disabled,
|
|
582
|
-
'aria-checked': node.checked ? 'true' : 'false',
|
|
583
|
-
};
|
|
584
|
-
|
|
585
|
-
const switchEl = `<input${buildAttrsString(attrs)} />`;
|
|
586
|
-
|
|
587
|
-
if (node.label) {
|
|
588
|
-
return `<label class="${classes}">
|
|
589
|
-
${switchEl}
|
|
590
|
-
<span class="${prefix}-switch-label">${escapeHtml(node.label)}</span>
|
|
591
|
-
</label>`;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
// Always wrap in label with .wf-switch class for proper styling
|
|
595
|
-
return `<label class="${classes}">${switchEl}</label>`;
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
export function renderSlider(node: SliderNode, context: RenderContext): string {
|
|
599
|
-
const prefix = context.options.classPrefix;
|
|
600
|
-
const classes = buildClassString([
|
|
601
|
-
`${prefix}-slider`,
|
|
602
|
-
...getCommonClasses(node, prefix),
|
|
603
|
-
]);
|
|
604
|
-
|
|
605
|
-
const attrs: Record<string, string | boolean | undefined> = {
|
|
606
|
-
class: classes,
|
|
607
|
-
type: 'range',
|
|
608
|
-
min: node.min?.toString(),
|
|
609
|
-
max: node.max?.toString(),
|
|
610
|
-
step: node.step?.toString(),
|
|
611
|
-
value: node.value?.toString(),
|
|
612
|
-
disabled: node.disabled,
|
|
613
|
-
'aria-valuemin': node.min?.toString(),
|
|
614
|
-
'aria-valuemax': node.max?.toString(),
|
|
615
|
-
'aria-valuenow': node.value?.toString(),
|
|
616
|
-
};
|
|
617
|
-
|
|
618
|
-
const slider = `<input${buildAttrsString(attrs)} />`;
|
|
619
|
-
|
|
620
|
-
if (node.label) {
|
|
621
|
-
return `<div class="${prefix}-form-field">
|
|
622
|
-
<label class="${prefix}-input-label">${escapeHtml(node.label)}</label>
|
|
623
|
-
${slider}
|
|
624
|
-
</div>`;
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
return slider;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
// ===========================================
|
|
631
|
-
// Button Component Renderer
|
|
632
|
-
// ===========================================
|
|
633
|
-
|
|
634
|
-
export function renderButton(node: ButtonNode, context: RenderContext): string {
|
|
635
|
-
const prefix = context.options.classPrefix;
|
|
636
|
-
// Icon-only button: has icon but no text content
|
|
637
|
-
const isIconOnly = node.icon && !node.content.trim();
|
|
638
|
-
const classes = buildClassString([
|
|
639
|
-
`${prefix}-button`,
|
|
640
|
-
node.primary ? `${prefix}-button-primary` : undefined,
|
|
641
|
-
node.secondary ? `${prefix}-button-secondary` : undefined,
|
|
642
|
-
node.outline ? `${prefix}-button-outline` : undefined,
|
|
643
|
-
node.ghost ? `${prefix}-button-ghost` : undefined,
|
|
644
|
-
node.danger ? `${prefix}-button-danger` : undefined,
|
|
645
|
-
node.size ? `${prefix}-button-${node.size}` : undefined,
|
|
646
|
-
node.disabled ? `${prefix}-button-disabled` : undefined,
|
|
647
|
-
node.loading ? `${prefix}-button-loading` : undefined,
|
|
648
|
-
isIconOnly ? `${prefix}-button-icon-only` : undefined,
|
|
649
|
-
...getCommonClasses(node, prefix),
|
|
650
|
-
]);
|
|
651
|
-
|
|
652
|
-
const attrs: Record<string, string | boolean | undefined> = {
|
|
653
|
-
class: classes,
|
|
654
|
-
type: 'button',
|
|
655
|
-
disabled: node.disabled,
|
|
656
|
-
'aria-disabled': node.disabled ? 'true' : undefined,
|
|
657
|
-
'aria-busy': node.loading ? 'true' : undefined,
|
|
658
|
-
};
|
|
659
|
-
|
|
660
|
-
const icon = node.icon ? `${renderIconPlaceholder(node.icon, prefix)} ` : '';
|
|
661
|
-
const loading = node.loading ? `<span class="${prefix}-spinner ${prefix}-spinner-sm" aria-hidden="true"></span> ` : '';
|
|
662
|
-
const content = escapeHtml(node.content);
|
|
663
|
-
|
|
664
|
-
return `<button${buildAttrsString(attrs)}>${loading}${icon}${content}</button>`;
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
// ===========================================
|
|
668
|
-
// Display Component Renderers
|
|
669
|
-
// ===========================================
|
|
670
|
-
|
|
671
|
-
export function renderImage(node: ImageNode, context: RenderContext): string {
|
|
672
|
-
const prefix = context.options.classPrefix;
|
|
673
|
-
const classes = buildClassString([
|
|
674
|
-
`${prefix}-image`,
|
|
675
|
-
...getCommonClasses(node, prefix),
|
|
676
|
-
]);
|
|
677
|
-
|
|
678
|
-
const attrs: Record<string, string | boolean | undefined> = {
|
|
679
|
-
class: classes,
|
|
680
|
-
src: node.src || '',
|
|
681
|
-
alt: node.alt || 'Image',
|
|
682
|
-
loading: 'lazy',
|
|
683
|
-
};
|
|
684
|
-
|
|
685
|
-
return `<img${buildAttrsString(attrs)} />`;
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
export function renderPlaceholder(node: PlaceholderNode, context: RenderContext): string {
|
|
689
|
-
const prefix = context.options.classPrefix;
|
|
690
|
-
const classes = buildClassString([
|
|
691
|
-
`${prefix}-placeholder`,
|
|
692
|
-
...getCommonClasses(node, prefix),
|
|
693
|
-
]);
|
|
694
|
-
|
|
695
|
-
const label = node.label ? escapeHtml(node.label) : 'Placeholder';
|
|
696
|
-
return `<div class="${classes}" role="img" aria-label="${label}">${label}</div>`;
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
export function renderAvatar(node: AvatarNode, context: RenderContext): string {
|
|
700
|
-
const prefix = context.options.classPrefix;
|
|
701
|
-
|
|
702
|
-
// Resolve size: token string (xs, sm, md, lg, xl) or custom px number
|
|
703
|
-
const sizeResolved = resolveSizeValue(node.size, 'avatar', prefix);
|
|
704
|
-
|
|
705
|
-
const classes = buildClassString([
|
|
706
|
-
`${prefix}-avatar`,
|
|
707
|
-
sizeResolved.className,
|
|
708
|
-
...getCommonClasses(node, prefix),
|
|
709
|
-
]);
|
|
710
|
-
|
|
711
|
-
const styleAttr = sizeResolved.style ? ` style="${sizeResolved.style}"` : '';
|
|
712
|
-
|
|
713
|
-
const initials = node.name
|
|
714
|
-
? node.name
|
|
715
|
-
.split(' ')
|
|
716
|
-
.map((n) => n[0])
|
|
717
|
-
.join('')
|
|
718
|
-
.toUpperCase()
|
|
719
|
-
.slice(0, 2)
|
|
720
|
-
: '?';
|
|
721
|
-
|
|
722
|
-
const ariaLabel = escapeHtml(node.name || 'Avatar');
|
|
723
|
-
|
|
724
|
-
if (node.src) {
|
|
725
|
-
return `<img class="${classes}"${styleAttr} src="${node.src}" alt="${ariaLabel}" />`;
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
return `<div class="${classes}"${styleAttr} role="img" aria-label="${ariaLabel}">${initials}</div>`;
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
export function renderBadge(node: BadgeNode, context: RenderContext): string {
|
|
732
|
-
const prefix = context.options.classPrefix;
|
|
733
|
-
|
|
734
|
-
// If icon is provided, render as icon badge (circular background with icon)
|
|
735
|
-
if (node.icon) {
|
|
736
|
-
const iconData = getIconData(node.icon);
|
|
737
|
-
const classes = buildClassString([
|
|
738
|
-
`${prefix}-badge-icon`,
|
|
739
|
-
node.size ? `${prefix}-badge-icon-${node.size}` : undefined,
|
|
740
|
-
node.variant ? `${prefix}-badge-icon-${node.variant}` : undefined,
|
|
741
|
-
...getCommonClasses(node, prefix),
|
|
742
|
-
]);
|
|
743
|
-
|
|
744
|
-
if (iconData) {
|
|
745
|
-
const svg = renderIconSvg(iconData, 24, 2, `${prefix}-icon`);
|
|
746
|
-
return `<span class="${classes}" aria-label="${escapeHtml(node.icon)}">${svg}</span>`;
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
// Fallback for unknown icon
|
|
750
|
-
return `<span class="${classes}" aria-label="unknown icon">?</span>`;
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
// Default text badge (empty content = dot indicator)
|
|
754
|
-
const isDot = !node.content || node.content.trim() === '';
|
|
755
|
-
const classes = buildClassString([
|
|
756
|
-
`${prefix}-badge`,
|
|
757
|
-
isDot ? `${prefix}-badge-dot` : undefined,
|
|
758
|
-
node.variant ? `${prefix}-badge-${node.variant}` : undefined,
|
|
759
|
-
node.pill ? `${prefix}-badge-pill` : undefined,
|
|
760
|
-
...getCommonClasses(node, prefix),
|
|
761
|
-
]);
|
|
762
|
-
|
|
763
|
-
return `<span class="${classes}">${escapeHtml(node.content)}</span>`;
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
export function renderIcon(node: IconNode, context: RenderContext): string {
|
|
767
|
-
const prefix = context.options.classPrefix;
|
|
768
|
-
const iconData = getIconData(node.name);
|
|
769
|
-
|
|
770
|
-
// Resolve size: token string (xs, sm, md, lg, xl) or custom px number
|
|
771
|
-
const sizeResolved = resolveSizeValue(node.size, 'icon', prefix);
|
|
772
|
-
|
|
773
|
-
const wrapperClasses = buildClassString([
|
|
774
|
-
`${prefix}-icon-wrapper`,
|
|
775
|
-
node.muted ? `${prefix}-text-muted` : undefined,
|
|
776
|
-
...getCommonClasses(node, prefix),
|
|
777
|
-
]);
|
|
778
|
-
|
|
779
|
-
if (iconData) {
|
|
780
|
-
// Build icon class with optional size class
|
|
781
|
-
const iconClasses = buildClassString([
|
|
782
|
-
`${prefix}-icon`,
|
|
783
|
-
sizeResolved.className,
|
|
784
|
-
]);
|
|
785
|
-
const styleAttr = sizeResolved.style ? ` style="${sizeResolved.style}"` : '';
|
|
786
|
-
const svg = renderIconSvg(iconData, 24, 2, iconClasses, styleAttr);
|
|
787
|
-
return `<span class="${wrapperClasses}" aria-hidden="true">${svg}</span>`;
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
// Fallback for unknown icons - render a placeholder circle
|
|
791
|
-
const size = sizeResolved.style?.match(/(\d+)px/)?.[1] || '24';
|
|
792
|
-
const sizeNum = parseInt(size, 10);
|
|
793
|
-
const placeholderSvg = `<svg class="${prefix}-icon ${sizeResolved.className || ''}" width="${sizeNum}" height="${sizeNum}" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
794
|
-
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" stroke-dasharray="4 2" fill="none" opacity="0.5"/>
|
|
795
|
-
<text x="12" y="16" text-anchor="middle" font-size="10" fill="currentColor" opacity="0.7">?</text>
|
|
796
|
-
</svg>`;
|
|
797
|
-
return `<span class="${wrapperClasses}" aria-hidden="true" title="Unknown icon: ${escapeHtml(node.name)}">${placeholderSvg}</span>`;
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
// ===========================================
|
|
801
|
-
// Data Component Renderers
|
|
802
|
-
// ===========================================
|
|
803
|
-
|
|
804
|
-
export function renderTable(
|
|
805
|
-
node: TableNode,
|
|
806
|
-
context: RenderContext,
|
|
807
|
-
renderNode: (node: AnyNode) => string
|
|
808
|
-
): string {
|
|
809
|
-
const prefix = context.options.classPrefix;
|
|
810
|
-
const classes = buildClassString([
|
|
811
|
-
`${prefix}-table`,
|
|
812
|
-
node.striped ? `${prefix}-table-striped` : undefined,
|
|
813
|
-
node.bordered ? `${prefix}-table-bordered` : undefined,
|
|
814
|
-
node.hover ? `${prefix}-table-hover` : undefined,
|
|
815
|
-
...getCommonClasses(node, prefix),
|
|
816
|
-
]);
|
|
817
|
-
|
|
818
|
-
const thead = `<thead><tr>${node.columns
|
|
819
|
-
.map((col) => `<th scope="col">${escapeHtml(col)}</th>`)
|
|
820
|
-
.join('')}</tr></thead>`;
|
|
821
|
-
|
|
822
|
-
const tbody = `<tbody>${node.rows
|
|
823
|
-
.map(
|
|
824
|
-
(row) =>
|
|
825
|
-
`<tr>${row
|
|
826
|
-
.map((cell) => {
|
|
827
|
-
if (typeof cell === 'string') {
|
|
828
|
-
return `<td>${escapeHtml(cell)}</td>`;
|
|
829
|
-
}
|
|
830
|
-
return `<td>${renderNode(cell)}</td>`;
|
|
831
|
-
})
|
|
832
|
-
.join('')}</tr>`
|
|
833
|
-
)
|
|
834
|
-
.join('')}</tbody>`;
|
|
835
|
-
|
|
836
|
-
return `<table class="${classes}" role="table">\n${thead}\n${tbody}\n</table>`;
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
export function renderList(node: ListNode, context: RenderContext): string {
|
|
840
|
-
const prefix = context.options.classPrefix;
|
|
841
|
-
const tag = node.ordered ? 'ol' : 'ul';
|
|
842
|
-
const classes = buildClassString([
|
|
843
|
-
`${prefix}-list`,
|
|
844
|
-
node.ordered ? `${prefix}-list-ordered` : undefined,
|
|
845
|
-
node.none ? `${prefix}-list-none` : undefined,
|
|
846
|
-
...getCommonClasses(node, prefix),
|
|
847
|
-
]);
|
|
848
|
-
|
|
849
|
-
const items = node.items
|
|
850
|
-
.map((item) => {
|
|
851
|
-
if (typeof item === 'string') {
|
|
852
|
-
return `<li class="${prefix}-list-item">${escapeHtml(item)}</li>`;
|
|
853
|
-
}
|
|
854
|
-
const icon = item.icon ? `${renderIconPlaceholder(item.icon, prefix)} ` : '';
|
|
855
|
-
return `<li class="${prefix}-list-item">${icon}${escapeHtml(item.content)}</li>`;
|
|
856
|
-
})
|
|
857
|
-
.join('\n');
|
|
858
|
-
|
|
859
|
-
return `<${tag} class="${classes}" role="list">\n${items}\n</${tag}>`;
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
// ===========================================
|
|
863
|
-
// Feedback Component Renderers
|
|
864
|
-
// ===========================================
|
|
865
|
-
|
|
866
|
-
export function renderAlert(node: AlertNode, context: RenderContext): string {
|
|
867
|
-
const prefix = context.options.classPrefix;
|
|
868
|
-
const classes = buildClassString([
|
|
869
|
-
`${prefix}-alert`,
|
|
870
|
-
node.variant ? `${prefix}-alert-${node.variant}` : undefined,
|
|
871
|
-
...getCommonClasses(node, prefix),
|
|
872
|
-
]);
|
|
873
|
-
|
|
874
|
-
const icon = node.icon ? `${renderIconPlaceholder(node.icon, prefix)} ` : '';
|
|
875
|
-
const dismissBtn = node.dismissible
|
|
876
|
-
? ` <button class="${prefix}-alert-close" aria-label="Close alert">×</button>`
|
|
877
|
-
: '';
|
|
878
|
-
|
|
879
|
-
return `<div class="${classes}" role="alert">${icon}${escapeHtml(node.content)}${dismissBtn}</div>`;
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
export function renderToast(node: ToastNode, context: RenderContext): string {
|
|
883
|
-
const prefix = context.options.classPrefix;
|
|
884
|
-
const classes = buildClassString([
|
|
885
|
-
`${prefix}-toast`,
|
|
886
|
-
node.position ? `${prefix}-toast-${node.position}` : undefined,
|
|
887
|
-
node.variant ? `${prefix}-toast-${node.variant}` : undefined,
|
|
888
|
-
...getCommonClasses(node, prefix),
|
|
889
|
-
]);
|
|
890
|
-
|
|
891
|
-
return `<div class="${classes}" role="status" aria-live="polite">${escapeHtml(node.content)}</div>`;
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
export function renderProgress(node: ProgressNode, context: RenderContext): string {
|
|
895
|
-
const prefix = context.options.classPrefix;
|
|
896
|
-
const classes = buildClassString([
|
|
897
|
-
`${prefix}-progress`,
|
|
898
|
-
node.indeterminate ? `${prefix}-progress-indeterminate` : undefined,
|
|
899
|
-
...getCommonClasses(node, prefix),
|
|
900
|
-
]);
|
|
901
|
-
|
|
902
|
-
const value = node.value || 0;
|
|
903
|
-
const max = node.max || 100;
|
|
904
|
-
const percentage = Math.round((value / max) * 100);
|
|
905
|
-
|
|
906
|
-
const label = node.label
|
|
907
|
-
? `<span class="${prefix}-progress-label">${escapeHtml(node.label)}</span>`
|
|
908
|
-
: '';
|
|
909
|
-
|
|
910
|
-
if (node.indeterminate) {
|
|
911
|
-
return `<div class="${classes}" role="progressbar" aria-label="Loading">${label}</div>`;
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
return `<div class="${classes}" role="progressbar" aria-valuenow="${value}" aria-valuemin="0" aria-valuemax="${max}" aria-label="Progress: ${percentage}%">
|
|
915
|
-
${label}
|
|
916
|
-
<div class="${prefix}-progress-bar" style="width: ${percentage}%"></div>
|
|
917
|
-
</div>`;
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
export function renderSpinner(node: SpinnerNode, context: RenderContext): string {
|
|
921
|
-
const prefix = context.options.classPrefix;
|
|
922
|
-
|
|
923
|
-
// Resolve size: token string (xs, sm, md, lg, xl) or custom px number
|
|
924
|
-
const sizeResolved = resolveSizeValue(node.size, 'spinner', prefix);
|
|
925
|
-
|
|
926
|
-
const classes = buildClassString([
|
|
927
|
-
`${prefix}-spinner`,
|
|
928
|
-
sizeResolved.className,
|
|
929
|
-
...getCommonClasses(node, prefix),
|
|
930
|
-
]);
|
|
931
|
-
|
|
932
|
-
const styleAttr = sizeResolved.style ? ` style="${sizeResolved.style}"` : '';
|
|
933
|
-
const label = escapeHtml(node.label || 'Loading...');
|
|
934
|
-
return `<span class="${classes}"${styleAttr} role="status" aria-label="${label}"><span class="sr-only">${label}</span></span>`;
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
// ===========================================
|
|
938
|
-
// Overlay Component Renderers
|
|
939
|
-
// ===========================================
|
|
940
|
-
|
|
941
|
-
export function renderTooltip(
|
|
942
|
-
node: TooltipNode,
|
|
943
|
-
context: RenderContext,
|
|
944
|
-
renderChildren: ChildrenRenderer
|
|
945
|
-
): string {
|
|
946
|
-
const prefix = context.options.classPrefix;
|
|
947
|
-
const classes = buildClassString([
|
|
948
|
-
`${prefix}-tooltip-wrapper`,
|
|
949
|
-
...getCommonClasses(node, prefix),
|
|
950
|
-
]);
|
|
951
|
-
|
|
952
|
-
const position = node.position || 'top';
|
|
953
|
-
const children = renderChildren(node.children);
|
|
954
|
-
const tooltipId = `tooltip-${Math.random().toString(36).substr(2, 9)}`;
|
|
955
|
-
|
|
956
|
-
return `<div class="${classes}">
|
|
957
|
-
${children}
|
|
958
|
-
<div class="${prefix}-tooltip ${prefix}-tooltip-${position}" role="tooltip" id="${tooltipId}">${escapeHtml(node.content)}</div>
|
|
959
|
-
</div>`;
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
export function renderPopover(
|
|
963
|
-
node: PopoverNode,
|
|
964
|
-
context: RenderContext,
|
|
965
|
-
renderChildren: ChildrenRenderer
|
|
966
|
-
): string {
|
|
967
|
-
const prefix = context.options.classPrefix;
|
|
968
|
-
const classes = buildClassString([
|
|
969
|
-
`${prefix}-popover`,
|
|
970
|
-
...getCommonClasses(node, prefix),
|
|
971
|
-
]);
|
|
972
|
-
|
|
973
|
-
const title = node.title
|
|
974
|
-
? `<div class="${prefix}-popover-header">${escapeHtml(node.title)}</div>\n`
|
|
975
|
-
: '';
|
|
976
|
-
const children = renderChildren(node.children);
|
|
977
|
-
|
|
978
|
-
return `<div class="${classes}" role="dialog">\n${title}<div class="${prefix}-popover-body">\n${children}\n</div>\n</div>`;
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
export function renderDropdown(node: DropdownNode, context: RenderContext): string {
|
|
982
|
-
const prefix = context.options.classPrefix;
|
|
983
|
-
const classes = buildClassString([
|
|
984
|
-
`${prefix}-dropdown`,
|
|
985
|
-
...getCommonClasses(node, prefix),
|
|
986
|
-
]);
|
|
987
|
-
|
|
988
|
-
const items = node.items
|
|
989
|
-
.map((item) => {
|
|
990
|
-
if ('type' in item && item.type === 'divider') {
|
|
991
|
-
return `<hr class="${prefix}-divider" role="separator" />`;
|
|
992
|
-
}
|
|
993
|
-
// TypeScript narrowing: item is DropdownItemNode after the divider check
|
|
994
|
-
const dropdownItem = item as { label: string; icon?: string; danger?: boolean; disabled?: boolean };
|
|
995
|
-
const itemClasses = buildClassString([
|
|
996
|
-
`${prefix}-dropdown-item`,
|
|
997
|
-
dropdownItem.danger ? `${prefix}-dropdown-item-danger` : undefined,
|
|
998
|
-
dropdownItem.disabled ? `${prefix}-dropdown-item-disabled` : undefined,
|
|
999
|
-
]);
|
|
1000
|
-
const icon = dropdownItem.icon ? `${renderIconPlaceholder(dropdownItem.icon, prefix)} ` : '';
|
|
1001
|
-
return `<button class="${itemClasses}" role="menuitem"${dropdownItem.disabled ? ' disabled aria-disabled="true"' : ''}>${icon}${escapeHtml(dropdownItem.label)}</button>`;
|
|
1002
|
-
})
|
|
1003
|
-
.join('\n');
|
|
1004
|
-
|
|
1005
|
-
return `<div class="${classes}" role="menu">\n${items}\n</div>`;
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
// ===========================================
|
|
1009
|
-
// Navigation Component Renderers
|
|
1010
|
-
// ===========================================
|
|
1011
|
-
|
|
1012
|
-
export function renderNav(node: NavNode, context: RenderContext): string {
|
|
1013
|
-
const prefix = context.options.classPrefix;
|
|
1014
|
-
const classes = buildClassString([
|
|
1015
|
-
`${prefix}-nav`,
|
|
1016
|
-
node.vertical ? `${prefix}-nav-vertical` : undefined,
|
|
1017
|
-
...getCommonClasses(node, prefix),
|
|
1018
|
-
]);
|
|
1019
|
-
|
|
1020
|
-
const items = node.items
|
|
1021
|
-
.map((item) => {
|
|
1022
|
-
if (typeof item === 'string') {
|
|
1023
|
-
return `<a class="${prefix}-nav-link" href="#" role="menuitem">${escapeHtml(item)}</a>`;
|
|
1024
|
-
}
|
|
1025
|
-
const linkClasses = buildClassString([
|
|
1026
|
-
`${prefix}-nav-link`,
|
|
1027
|
-
item.active ? `${prefix}-nav-link-active` : undefined,
|
|
1028
|
-
item.disabled ? `${prefix}-nav-link-disabled` : undefined,
|
|
1029
|
-
]);
|
|
1030
|
-
const icon = item.icon ? `${renderIconPlaceholder(item.icon, prefix)} ` : '';
|
|
1031
|
-
const ariaCurrent = item.active ? ' aria-current="page"' : '';
|
|
1032
|
-
return `<a class="${linkClasses}" href="${item.href || '#'}" role="menuitem"${ariaCurrent}${item.disabled ? ' aria-disabled="true"' : ''}>${icon}${escapeHtml(item.label)}</a>`;
|
|
1033
|
-
})
|
|
1034
|
-
.join('\n');
|
|
1035
|
-
|
|
1036
|
-
return `<nav class="${classes}" role="navigation">\n${items}\n</nav>`;
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
export function renderTabs(node: TabsNode, context: RenderContext): string {
|
|
1040
|
-
const prefix = context.options.classPrefix;
|
|
1041
|
-
const classes = buildClassString([
|
|
1042
|
-
`${prefix}-tabs`,
|
|
1043
|
-
...getCommonClasses(node, prefix),
|
|
1044
|
-
]);
|
|
1045
|
-
|
|
1046
|
-
const tabList = node.items
|
|
1047
|
-
.map((label, idx) => {
|
|
1048
|
-
const isActive = idx === (node.active || 0);
|
|
1049
|
-
const tabClasses = `${prefix}-tab${isActive ? ` ${prefix}-tab-active` : ''}`;
|
|
1050
|
-
return `<button class="${tabClasses}" role="tab" aria-selected="${isActive}">${escapeHtml(label)}</button>`;
|
|
1051
|
-
})
|
|
1052
|
-
.join('\n');
|
|
1053
|
-
|
|
1054
|
-
return `<div class="${classes}">
|
|
1055
|
-
<div class="${prefix}-tab-list" role="tablist">
|
|
1056
|
-
${tabList}
|
|
1057
|
-
</div>
|
|
1058
|
-
</div>`;
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
export function renderBreadcrumb(node: BreadcrumbNode, context: RenderContext): string {
|
|
1062
|
-
const prefix = context.options.classPrefix;
|
|
1063
|
-
const classes = buildClassString([
|
|
1064
|
-
`${prefix}-breadcrumb`,
|
|
1065
|
-
...getCommonClasses(node, prefix),
|
|
1066
|
-
]);
|
|
1067
|
-
|
|
1068
|
-
const items = node.items
|
|
1069
|
-
.map((item, idx) => {
|
|
1070
|
-
const isLast = idx === node.items.length - 1;
|
|
1071
|
-
if (typeof item === 'string') {
|
|
1072
|
-
return isLast
|
|
1073
|
-
? `<span class="${prefix}-breadcrumb-item" aria-current="page">${escapeHtml(item)}</span>`
|
|
1074
|
-
: `<a class="${prefix}-breadcrumb-item" href="#">${escapeHtml(item)}</a>`;
|
|
1075
|
-
}
|
|
1076
|
-
return isLast
|
|
1077
|
-
? `<span class="${prefix}-breadcrumb-item" aria-current="page">${escapeHtml(item.label)}</span>`
|
|
1078
|
-
: `<a class="${prefix}-breadcrumb-item" href="${item.href || '#'}">${escapeHtml(item.label)}</a>`;
|
|
1079
|
-
})
|
|
1080
|
-
.join(` <span class="${prefix}-breadcrumb-separator" aria-hidden="true">/</span> `);
|
|
1081
|
-
|
|
1082
|
-
return `<nav class="${classes}" aria-label="Breadcrumb">${items}</nav>`;
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
// ===========================================
|
|
1086
|
-
// Divider Component Renderer
|
|
1087
|
-
// ===========================================
|
|
1088
|
-
|
|
1089
|
-
export function renderDivider(_node: DividerComponentNode, context: RenderContext): string {
|
|
1090
|
-
const prefix = context.options.classPrefix;
|
|
1091
|
-
return `<hr class="${prefix}-divider" role="separator" />`;
|
|
1092
|
-
}
|