@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,1050 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SVG Renderer for wireweave
|
|
3
|
-
*
|
|
4
|
-
* Renders wireframe AST to SVG image format
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type {
|
|
8
|
-
WireframeDocument,
|
|
9
|
-
PageNode,
|
|
10
|
-
AnyNode,
|
|
11
|
-
RowNode,
|
|
12
|
-
ColNode,
|
|
13
|
-
HeaderNode,
|
|
14
|
-
MainNode,
|
|
15
|
-
FooterNode,
|
|
16
|
-
SidebarNode,
|
|
17
|
-
CardNode,
|
|
18
|
-
TextNode,
|
|
19
|
-
TitleNode,
|
|
20
|
-
ButtonNode,
|
|
21
|
-
InputNode,
|
|
22
|
-
TextareaNode,
|
|
23
|
-
TableNode,
|
|
24
|
-
ListNode,
|
|
25
|
-
AlertNode,
|
|
26
|
-
BadgeNode,
|
|
27
|
-
ProgressNode,
|
|
28
|
-
SpinnerNode,
|
|
29
|
-
CheckboxNode,
|
|
30
|
-
RadioNode,
|
|
31
|
-
SwitchNode,
|
|
32
|
-
ImageNode,
|
|
33
|
-
PlaceholderNode,
|
|
34
|
-
AvatarNode,
|
|
35
|
-
ModalNode,
|
|
36
|
-
NavNode,
|
|
37
|
-
TabsNode,
|
|
38
|
-
BreadcrumbNode,
|
|
39
|
-
SelectNode,
|
|
40
|
-
LinkNode,
|
|
41
|
-
} from '../../ast/types';
|
|
42
|
-
import type { SvgRenderOptions, SvgRenderResult, ThemeConfig } from '../types';
|
|
43
|
-
import { defaultTheme } from '../types';
|
|
44
|
-
import { resolveViewport } from '../../viewport';
|
|
45
|
-
import { getIconData, type IconData } from '../../icons/lucide-icons';
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Bounding box for layout calculations
|
|
49
|
-
*/
|
|
50
|
-
export interface BoundingBox {
|
|
51
|
-
x: number;
|
|
52
|
-
y: number;
|
|
53
|
-
width: number;
|
|
54
|
-
height: number;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* SVG Renderer class
|
|
59
|
-
*
|
|
60
|
-
* Renders wireframe AST nodes to SVG elements
|
|
61
|
-
*/
|
|
62
|
-
export class SvgRenderer {
|
|
63
|
-
private options: Required<SvgRenderOptions>;
|
|
64
|
-
private theme: ThemeConfig;
|
|
65
|
-
private currentX: number = 0;
|
|
66
|
-
private currentY: number = 0;
|
|
67
|
-
private contentWidth: number = 0;
|
|
68
|
-
|
|
69
|
-
constructor(options: SvgRenderOptions = {}) {
|
|
70
|
-
this.options = {
|
|
71
|
-
width: options.width ?? 800,
|
|
72
|
-
height: options.height ?? 600,
|
|
73
|
-
scale: options.scale ?? 1,
|
|
74
|
-
background: options.background ?? '#ffffff',
|
|
75
|
-
padding: options.padding ?? 20,
|
|
76
|
-
fontFamily: options.fontFamily ?? 'system-ui, -apple-system, sans-serif',
|
|
77
|
-
};
|
|
78
|
-
this.theme = defaultTheme;
|
|
79
|
-
this.contentWidth = this.options.width - this.options.padding * 2;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Render a wireframe document to SVG
|
|
84
|
-
*/
|
|
85
|
-
render(doc: WireframeDocument): SvgRenderResult {
|
|
86
|
-
this.currentX = this.options.padding;
|
|
87
|
-
this.currentY = this.options.padding;
|
|
88
|
-
|
|
89
|
-
// Get viewport from first page (only if explicitly set), or use options
|
|
90
|
-
const firstPage = doc.children[0];
|
|
91
|
-
let width = this.options.width;
|
|
92
|
-
let height = this.options.height;
|
|
93
|
-
|
|
94
|
-
if (firstPage && (firstPage.viewport !== undefined || firstPage.device !== undefined)) {
|
|
95
|
-
const viewport = resolveViewport(firstPage.viewport, firstPage.device);
|
|
96
|
-
width = viewport.width;
|
|
97
|
-
height = viewport.height;
|
|
98
|
-
this.contentWidth = width - this.options.padding * 2;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const content = doc.children.map((page) => this.renderPage(page)).join('\n');
|
|
102
|
-
|
|
103
|
-
const svg = `<?xml version="1.0" encoding="UTF-8"?>
|
|
104
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}">
|
|
105
|
-
<defs>
|
|
106
|
-
${this.generateDefs()}
|
|
107
|
-
</defs>
|
|
108
|
-
<rect width="100%" height="100%" fill="${this.options.background}"/>
|
|
109
|
-
<g transform="scale(${this.options.scale})">
|
|
110
|
-
${content}
|
|
111
|
-
</g>
|
|
112
|
-
</svg>`;
|
|
113
|
-
|
|
114
|
-
return { svg, width, height };
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Generate SVG defs (styles, patterns, etc.)
|
|
119
|
-
*/
|
|
120
|
-
private generateDefs(): string {
|
|
121
|
-
return `
|
|
122
|
-
<style>
|
|
123
|
-
text { font-family: ${this.options.fontFamily}; }
|
|
124
|
-
.wf-title { font-weight: 600; }
|
|
125
|
-
.wf-muted { fill: ${this.theme.colors.muted}; }
|
|
126
|
-
</style>
|
|
127
|
-
`;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Render a page node
|
|
132
|
-
*/
|
|
133
|
-
private renderPage(node: PageNode): string {
|
|
134
|
-
const elements: string[] = [];
|
|
135
|
-
|
|
136
|
-
// Render page title if present
|
|
137
|
-
if (node.title) {
|
|
138
|
-
elements.push(this.renderPageTitle(node.title));
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
for (const child of node.children) {
|
|
142
|
-
elements.push(this.renderNode(child));
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return elements.join('\n');
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Render page title
|
|
150
|
-
*/
|
|
151
|
-
private renderPageTitle(title: string): string {
|
|
152
|
-
const fontSize = 24;
|
|
153
|
-
const y = this.currentY + fontSize;
|
|
154
|
-
this.currentY += fontSize + 16;
|
|
155
|
-
|
|
156
|
-
return `<text x="${this.currentX}" y="${y}" font-size="${fontSize}" font-weight="600" fill="${this.theme.colors.foreground}">${this.escapeXml(title)}</text>`;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Render any AST node
|
|
161
|
-
*/
|
|
162
|
-
private renderNode(node: AnyNode): string {
|
|
163
|
-
switch (node.type) {
|
|
164
|
-
// Layout nodes
|
|
165
|
-
case 'Row':
|
|
166
|
-
return this.renderRow(node as RowNode);
|
|
167
|
-
case 'Col':
|
|
168
|
-
return this.renderCol(node as ColNode);
|
|
169
|
-
case 'Header':
|
|
170
|
-
return this.renderHeader(node as HeaderNode);
|
|
171
|
-
case 'Main':
|
|
172
|
-
return this.renderMain(node as MainNode);
|
|
173
|
-
case 'Footer':
|
|
174
|
-
return this.renderFooter(node as FooterNode);
|
|
175
|
-
case 'Sidebar':
|
|
176
|
-
return this.renderSidebar(node as SidebarNode);
|
|
177
|
-
|
|
178
|
-
// Container nodes
|
|
179
|
-
case 'Card':
|
|
180
|
-
return this.renderCard(node as CardNode);
|
|
181
|
-
case 'Modal':
|
|
182
|
-
return this.renderModal(node as ModalNode);
|
|
183
|
-
|
|
184
|
-
// Text nodes
|
|
185
|
-
case 'Text':
|
|
186
|
-
return this.renderText(node as TextNode);
|
|
187
|
-
case 'Title':
|
|
188
|
-
return this.renderTitle(node as TitleNode);
|
|
189
|
-
case 'Link':
|
|
190
|
-
return this.renderLink(node as LinkNode);
|
|
191
|
-
|
|
192
|
-
// Input nodes
|
|
193
|
-
case 'Input':
|
|
194
|
-
return this.renderInput(node as InputNode);
|
|
195
|
-
case 'Textarea':
|
|
196
|
-
return this.renderTextarea(node as TextareaNode);
|
|
197
|
-
case 'Select':
|
|
198
|
-
return this.renderSelect(node as SelectNode);
|
|
199
|
-
case 'Checkbox':
|
|
200
|
-
return this.renderCheckbox(node as CheckboxNode);
|
|
201
|
-
case 'Radio':
|
|
202
|
-
return this.renderRadio(node as RadioNode);
|
|
203
|
-
case 'Switch':
|
|
204
|
-
return this.renderSwitch(node as SwitchNode);
|
|
205
|
-
|
|
206
|
-
// Button
|
|
207
|
-
case 'Button':
|
|
208
|
-
return this.renderButton(node as ButtonNode);
|
|
209
|
-
|
|
210
|
-
// Display nodes
|
|
211
|
-
case 'Image':
|
|
212
|
-
return this.renderImage(node as ImageNode);
|
|
213
|
-
case 'Placeholder':
|
|
214
|
-
return this.renderPlaceholder(node as PlaceholderNode);
|
|
215
|
-
case 'Avatar':
|
|
216
|
-
return this.renderAvatar(node as AvatarNode);
|
|
217
|
-
case 'Badge':
|
|
218
|
-
return this.renderBadge(node as BadgeNode);
|
|
219
|
-
|
|
220
|
-
// Data nodes
|
|
221
|
-
case 'Table':
|
|
222
|
-
return this.renderTable(node as TableNode);
|
|
223
|
-
case 'List':
|
|
224
|
-
return this.renderList(node as ListNode);
|
|
225
|
-
|
|
226
|
-
// Feedback nodes
|
|
227
|
-
case 'Alert':
|
|
228
|
-
return this.renderAlert(node as AlertNode);
|
|
229
|
-
case 'Progress':
|
|
230
|
-
return this.renderProgress(node as ProgressNode);
|
|
231
|
-
case 'Spinner':
|
|
232
|
-
return this.renderSpinner(node as SpinnerNode);
|
|
233
|
-
|
|
234
|
-
// Navigation nodes
|
|
235
|
-
case 'Nav':
|
|
236
|
-
return this.renderNav(node as NavNode);
|
|
237
|
-
case 'Tabs':
|
|
238
|
-
return this.renderTabs(node as TabsNode);
|
|
239
|
-
case 'Breadcrumb':
|
|
240
|
-
return this.renderBreadcrumb(node as BreadcrumbNode);
|
|
241
|
-
|
|
242
|
-
default:
|
|
243
|
-
return `<!-- Unsupported: ${node.type} -->`;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// ===========================================
|
|
248
|
-
// Layout Renderers
|
|
249
|
-
// ===========================================
|
|
250
|
-
|
|
251
|
-
private renderRow(node: RowNode): string {
|
|
252
|
-
const savedX = this.currentX;
|
|
253
|
-
const savedY = this.currentY;
|
|
254
|
-
const elements: string[] = [];
|
|
255
|
-
|
|
256
|
-
// Calculate column widths
|
|
257
|
-
const totalSpan = node.children.reduce((sum, child) => {
|
|
258
|
-
if ('span' in child && typeof child.span === 'number') {
|
|
259
|
-
return sum + child.span;
|
|
260
|
-
}
|
|
261
|
-
return sum + 1;
|
|
262
|
-
}, 0);
|
|
263
|
-
|
|
264
|
-
const colWidth = this.contentWidth / Math.max(totalSpan, 1);
|
|
265
|
-
let maxHeight = 0;
|
|
266
|
-
|
|
267
|
-
for (const child of node.children) {
|
|
268
|
-
const span = 'span' in child && typeof child.span === 'number' ? child.span : 1;
|
|
269
|
-
const childWidth = colWidth * span;
|
|
270
|
-
const startY = this.currentY;
|
|
271
|
-
|
|
272
|
-
elements.push(this.renderNode(child));
|
|
273
|
-
|
|
274
|
-
const childHeight = this.currentY - startY;
|
|
275
|
-
maxHeight = Math.max(maxHeight, childHeight);
|
|
276
|
-
|
|
277
|
-
this.currentX += childWidth;
|
|
278
|
-
this.currentY = savedY;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
this.currentX = savedX;
|
|
282
|
-
this.currentY = savedY + maxHeight;
|
|
283
|
-
|
|
284
|
-
return elements.join('\n');
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
private renderCol(node: ColNode): string {
|
|
288
|
-
return node.children.map((child) => this.renderNode(child)).join('\n');
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
private renderHeader(node: HeaderNode): string {
|
|
292
|
-
const height = 60;
|
|
293
|
-
const x = this.currentX;
|
|
294
|
-
const y = this.currentY;
|
|
295
|
-
|
|
296
|
-
const savedY = this.currentY;
|
|
297
|
-
this.currentY += 16;
|
|
298
|
-
const children = node.children.map((c) => this.renderNode(c)).join('\n');
|
|
299
|
-
this.currentY = savedY + height + 8;
|
|
300
|
-
|
|
301
|
-
return `
|
|
302
|
-
<g transform="translate(${x}, ${y})">
|
|
303
|
-
<rect width="${this.contentWidth}" height="${height}" fill="${this.theme.colors.background}" stroke="${this.theme.colors.border}" stroke-width="1"/>
|
|
304
|
-
${children}
|
|
305
|
-
</g>`;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
private renderMain(node: MainNode): string {
|
|
309
|
-
return node.children.map((c) => this.renderNode(c)).join('\n');
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
private renderFooter(node: FooterNode): string {
|
|
313
|
-
const height = 60;
|
|
314
|
-
const x = this.currentX;
|
|
315
|
-
const y = this.currentY;
|
|
316
|
-
|
|
317
|
-
const savedY = this.currentY;
|
|
318
|
-
this.currentY += 16;
|
|
319
|
-
const children = node.children.map((c) => this.renderNode(c)).join('\n');
|
|
320
|
-
this.currentY = savedY + height + 8;
|
|
321
|
-
|
|
322
|
-
return `
|
|
323
|
-
<g transform="translate(${x}, ${y})">
|
|
324
|
-
<rect width="${this.contentWidth}" height="${height}" fill="${this.theme.colors.background}" stroke="${this.theme.colors.border}" stroke-width="1"/>
|
|
325
|
-
${children}
|
|
326
|
-
</g>`;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
private renderSidebar(node: SidebarNode): string {
|
|
330
|
-
const width = 200;
|
|
331
|
-
const height = 300;
|
|
332
|
-
const x = this.currentX;
|
|
333
|
-
const y = this.currentY;
|
|
334
|
-
|
|
335
|
-
const savedY = this.currentY;
|
|
336
|
-
this.currentY += 16;
|
|
337
|
-
const children = node.children.map((c) => this.renderNode(c)).join('\n');
|
|
338
|
-
this.currentY = savedY + height + 8;
|
|
339
|
-
|
|
340
|
-
return `
|
|
341
|
-
<g transform="translate(${x}, ${y})">
|
|
342
|
-
<rect width="${width}" height="${height}" fill="${this.theme.colors.background}" stroke="${this.theme.colors.border}" stroke-width="1"/>
|
|
343
|
-
${children}
|
|
344
|
-
</g>`;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// ===========================================
|
|
348
|
-
// Container Renderers
|
|
349
|
-
// ===========================================
|
|
350
|
-
|
|
351
|
-
private renderCard(node: CardNode): string {
|
|
352
|
-
const width = Math.min(300, this.contentWidth);
|
|
353
|
-
const x = this.currentX;
|
|
354
|
-
const y = this.currentY;
|
|
355
|
-
|
|
356
|
-
const savedY = this.currentY;
|
|
357
|
-
this.currentY += 16;
|
|
358
|
-
|
|
359
|
-
// Render title if present
|
|
360
|
-
let titleSvg = '';
|
|
361
|
-
if (node.title) {
|
|
362
|
-
const titleFontSize = 16;
|
|
363
|
-
titleSvg = `<text x="16" y="${titleFontSize + 12}" font-size="${titleFontSize}" font-weight="600" fill="${this.theme.colors.foreground}">${this.escapeXml(node.title)}</text>`;
|
|
364
|
-
this.currentY += titleFontSize + 8;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
const childStartY = this.currentY - savedY;
|
|
368
|
-
const children = node.children.map((c) => this.renderNode(c)).join('\n');
|
|
369
|
-
const contentHeight = Math.max(this.currentY - savedY, 100);
|
|
370
|
-
|
|
371
|
-
this.currentY = savedY + contentHeight + 16;
|
|
372
|
-
|
|
373
|
-
return `
|
|
374
|
-
<g transform="translate(${x}, ${y})">
|
|
375
|
-
<rect width="${width}" height="${contentHeight}" rx="8" fill="white" stroke="${this.theme.colors.border}" stroke-width="1"/>
|
|
376
|
-
${titleSvg}
|
|
377
|
-
<g transform="translate(16, ${childStartY})">
|
|
378
|
-
${children}
|
|
379
|
-
</g>
|
|
380
|
-
</g>`;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
private renderModal(node: ModalNode): string {
|
|
384
|
-
const width = 400;
|
|
385
|
-
const height = 300;
|
|
386
|
-
const x = (this.options.width - width) / 2;
|
|
387
|
-
const y = (this.options.height - height) / 2;
|
|
388
|
-
|
|
389
|
-
let titleSvg = '';
|
|
390
|
-
if (node.title) {
|
|
391
|
-
titleSvg = `<text x="20" y="30" font-size="18" font-weight="600" fill="${this.theme.colors.foreground}">${this.escapeXml(node.title)}</text>`;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
const savedX = this.currentX;
|
|
395
|
-
const savedY = this.currentY;
|
|
396
|
-
this.currentX = 20;
|
|
397
|
-
this.currentY = 50;
|
|
398
|
-
|
|
399
|
-
const children = node.children.map((c) => this.renderNode(c)).join('\n');
|
|
400
|
-
|
|
401
|
-
this.currentX = savedX;
|
|
402
|
-
this.currentY = savedY;
|
|
403
|
-
|
|
404
|
-
return `
|
|
405
|
-
<g>
|
|
406
|
-
<rect width="100%" height="100%" fill="rgba(0,0,0,0.5)" opacity="0.5"/>
|
|
407
|
-
<g transform="translate(${x}, ${y})">
|
|
408
|
-
<rect width="${width}" height="${height}" rx="8" fill="white" stroke="${this.theme.colors.border}" stroke-width="1"/>
|
|
409
|
-
${titleSvg}
|
|
410
|
-
${children}
|
|
411
|
-
</g>
|
|
412
|
-
</g>`;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// ===========================================
|
|
416
|
-
// Text Renderers
|
|
417
|
-
// ===========================================
|
|
418
|
-
|
|
419
|
-
private renderText(node: TextNode): string {
|
|
420
|
-
const fontSize = this.resolveFontSize(node.size);
|
|
421
|
-
const fill = node.muted ? this.theme.colors.muted : this.theme.colors.foreground;
|
|
422
|
-
const fontWeight = node.weight || 'normal';
|
|
423
|
-
|
|
424
|
-
const y = this.currentY + fontSize;
|
|
425
|
-
this.currentY += fontSize + 8;
|
|
426
|
-
|
|
427
|
-
return `<text x="${this.currentX}" y="${y}" font-size="${fontSize}" font-weight="${fontWeight}" fill="${fill}">${this.escapeXml(node.content)}</text>`;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
private renderTitle(node: TitleNode): string {
|
|
431
|
-
const level = node.level || 1;
|
|
432
|
-
const fontSize = this.getTitleFontSize(level);
|
|
433
|
-
|
|
434
|
-
const y = this.currentY + fontSize;
|
|
435
|
-
this.currentY += fontSize + 12;
|
|
436
|
-
|
|
437
|
-
return `<text x="${this.currentX}" y="${y}" font-size="${fontSize}" font-weight="600" fill="${this.theme.colors.foreground}">${this.escapeXml(node.content)}</text>`;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
private renderLink(node: LinkNode): string {
|
|
441
|
-
const fontSize = 14;
|
|
442
|
-
const y = this.currentY + fontSize;
|
|
443
|
-
this.currentY += fontSize + 8;
|
|
444
|
-
|
|
445
|
-
return `<text x="${this.currentX}" y="${y}" font-size="${fontSize}" fill="${this.theme.colors.primary}" text-decoration="underline">${this.escapeXml(node.content)}</text>`;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
// ===========================================
|
|
449
|
-
// Input Renderers
|
|
450
|
-
// ===========================================
|
|
451
|
-
|
|
452
|
-
private renderInput(node: InputNode): string {
|
|
453
|
-
const width = 280;
|
|
454
|
-
const height = 40;
|
|
455
|
-
const x = this.currentX;
|
|
456
|
-
let y = this.currentY;
|
|
457
|
-
|
|
458
|
-
let result = '';
|
|
459
|
-
|
|
460
|
-
if (node.label) {
|
|
461
|
-
result += `<text x="${x}" y="${y + 14}" font-size="14" fill="${this.theme.colors.foreground}">${this.escapeXml(node.label)}</text>`;
|
|
462
|
-
y += 24;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
const placeholder = node.placeholder || '';
|
|
466
|
-
|
|
467
|
-
result += `
|
|
468
|
-
<g transform="translate(${x}, ${y})">
|
|
469
|
-
<rect width="${width}" height="${height}" rx="4" fill="white" stroke="${this.theme.colors.border}" stroke-width="1"/>
|
|
470
|
-
<text x="12" y="${height / 2 + 5}" font-size="14" fill="${this.theme.colors.muted}">${this.escapeXml(placeholder)}</text>
|
|
471
|
-
</g>`;
|
|
472
|
-
|
|
473
|
-
this.currentY = y + height + 12;
|
|
474
|
-
|
|
475
|
-
return result;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
private renderTextarea(node: TextareaNode): string {
|
|
479
|
-
const width = 280;
|
|
480
|
-
const height = 100;
|
|
481
|
-
const x = this.currentX;
|
|
482
|
-
let y = this.currentY;
|
|
483
|
-
|
|
484
|
-
let result = '';
|
|
485
|
-
|
|
486
|
-
if (node.label) {
|
|
487
|
-
result += `<text x="${x}" y="${y + 14}" font-size="14" fill="${this.theme.colors.foreground}">${this.escapeXml(node.label)}</text>`;
|
|
488
|
-
y += 24;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
const placeholder = node.placeholder || '';
|
|
492
|
-
|
|
493
|
-
result += `
|
|
494
|
-
<g transform="translate(${x}, ${y})">
|
|
495
|
-
<rect width="${width}" height="${height}" rx="4" fill="white" stroke="${this.theme.colors.border}" stroke-width="1"/>
|
|
496
|
-
<text x="12" y="24" font-size="14" fill="${this.theme.colors.muted}">${this.escapeXml(placeholder)}</text>
|
|
497
|
-
</g>`;
|
|
498
|
-
|
|
499
|
-
this.currentY = y + height + 12;
|
|
500
|
-
|
|
501
|
-
return result;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
private renderSelect(node: SelectNode): string {
|
|
505
|
-
const width = 280;
|
|
506
|
-
const height = 40;
|
|
507
|
-
const x = this.currentX;
|
|
508
|
-
let y = this.currentY;
|
|
509
|
-
|
|
510
|
-
let result = '';
|
|
511
|
-
|
|
512
|
-
if (node.label) {
|
|
513
|
-
result += `<text x="${x}" y="${y + 14}" font-size="14" fill="${this.theme.colors.foreground}">${this.escapeXml(node.label)}</text>`;
|
|
514
|
-
y += 24;
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
const placeholder = node.placeholder || 'Select...';
|
|
518
|
-
|
|
519
|
-
result += `
|
|
520
|
-
<g transform="translate(${x}, ${y})">
|
|
521
|
-
<rect width="${width}" height="${height}" rx="4" fill="white" stroke="${this.theme.colors.border}" stroke-width="1"/>
|
|
522
|
-
<text x="12" y="${height / 2 + 5}" font-size="14" fill="${this.theme.colors.muted}">${this.escapeXml(placeholder)}</text>
|
|
523
|
-
<path d="M${width - 24} ${height / 2 - 3} l6 6 l6 -6" fill="none" stroke="${this.theme.colors.muted}" stroke-width="1.5"/>
|
|
524
|
-
</g>`;
|
|
525
|
-
|
|
526
|
-
this.currentY = y + height + 12;
|
|
527
|
-
|
|
528
|
-
return result;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
private renderCheckbox(node: CheckboxNode): string {
|
|
532
|
-
const x = this.currentX;
|
|
533
|
-
const y = this.currentY;
|
|
534
|
-
const size = 18;
|
|
535
|
-
|
|
536
|
-
let result = `
|
|
537
|
-
<g transform="translate(${x}, ${y})">
|
|
538
|
-
<rect width="${size}" height="${size}" rx="3" fill="white" stroke="${this.theme.colors.border}" stroke-width="1"/>`;
|
|
539
|
-
|
|
540
|
-
if (node.checked) {
|
|
541
|
-
result += `<path d="M4 9 L7 12 L14 5" fill="none" stroke="${this.theme.colors.foreground}" stroke-width="2"/>`;
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
if (node.label) {
|
|
545
|
-
result += `<text x="${size + 8}" y="${size - 3}" font-size="14" fill="${this.theme.colors.foreground}">${this.escapeXml(node.label)}</text>`;
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
result += '</g>';
|
|
549
|
-
|
|
550
|
-
this.currentY += size + 12;
|
|
551
|
-
|
|
552
|
-
return result;
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
private renderRadio(node: RadioNode): string {
|
|
556
|
-
const x = this.currentX;
|
|
557
|
-
const y = this.currentY;
|
|
558
|
-
const size = 18;
|
|
559
|
-
const radius = size / 2;
|
|
560
|
-
|
|
561
|
-
let result = `
|
|
562
|
-
<g transform="translate(${x}, ${y})">
|
|
563
|
-
<circle cx="${radius}" cy="${radius}" r="${radius - 1}" fill="white" stroke="${this.theme.colors.border}" stroke-width="1"/>`;
|
|
564
|
-
|
|
565
|
-
if (node.checked) {
|
|
566
|
-
result += `<circle cx="${radius}" cy="${radius}" r="${radius - 5}" fill="${this.theme.colors.foreground}"/>`;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
if (node.label) {
|
|
570
|
-
result += `<text x="${size + 8}" y="${size - 3}" font-size="14" fill="${this.theme.colors.foreground}">${this.escapeXml(node.label)}</text>`;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
result += '</g>';
|
|
574
|
-
|
|
575
|
-
this.currentY += size + 12;
|
|
576
|
-
|
|
577
|
-
return result;
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
private renderSwitch(node: SwitchNode): string {
|
|
581
|
-
const x = this.currentX;
|
|
582
|
-
const y = this.currentY;
|
|
583
|
-
const width = 44;
|
|
584
|
-
const height = 24;
|
|
585
|
-
const radius = height / 2;
|
|
586
|
-
|
|
587
|
-
const isOn = node.checked;
|
|
588
|
-
const bgColor = isOn ? this.theme.colors.primary : this.theme.colors.border;
|
|
589
|
-
const knobX = isOn ? width - radius : radius;
|
|
590
|
-
|
|
591
|
-
let result = `
|
|
592
|
-
<g transform="translate(${x}, ${y})">
|
|
593
|
-
<rect width="${width}" height="${height}" rx="${radius}" fill="${bgColor}"/>
|
|
594
|
-
<circle cx="${knobX}" cy="${radius}" r="${radius - 3}" fill="white"/>`;
|
|
595
|
-
|
|
596
|
-
if (node.label) {
|
|
597
|
-
result += `<text x="${width + 8}" y="${height - 6}" font-size="14" fill="${this.theme.colors.foreground}">${this.escapeXml(node.label)}</text>`;
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
result += '</g>';
|
|
601
|
-
|
|
602
|
-
this.currentY += height + 12;
|
|
603
|
-
|
|
604
|
-
return result;
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
// ===========================================
|
|
608
|
-
// Button Renderer
|
|
609
|
-
// ===========================================
|
|
610
|
-
|
|
611
|
-
private renderButton(node: ButtonNode): string {
|
|
612
|
-
const content = node.content;
|
|
613
|
-
const hasIcon = !!node.icon;
|
|
614
|
-
const isIconOnly = hasIcon && !content.trim();
|
|
615
|
-
|
|
616
|
-
// Calculate button dimensions
|
|
617
|
-
const iconSize = 16;
|
|
618
|
-
const padding = isIconOnly ? 8 : 16;
|
|
619
|
-
let width: number;
|
|
620
|
-
|
|
621
|
-
if (isIconOnly) {
|
|
622
|
-
width = iconSize + padding * 2;
|
|
623
|
-
} else if (hasIcon) {
|
|
624
|
-
width = Math.max(80, content.length * 10 + iconSize + 40);
|
|
625
|
-
} else {
|
|
626
|
-
width = Math.max(80, content.length * 10 + 32);
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
const height = 36;
|
|
630
|
-
const x = this.currentX;
|
|
631
|
-
const y = this.currentY;
|
|
632
|
-
|
|
633
|
-
// Determine button variant from boolean flags
|
|
634
|
-
let fill = this.theme.colors.primary;
|
|
635
|
-
let textFill = '#ffffff';
|
|
636
|
-
let isOutline = false;
|
|
637
|
-
|
|
638
|
-
if (node.secondary) {
|
|
639
|
-
fill = this.theme.colors.secondary;
|
|
640
|
-
} else if (node.outline) {
|
|
641
|
-
fill = 'white';
|
|
642
|
-
textFill = this.theme.colors.foreground;
|
|
643
|
-
isOutline = true;
|
|
644
|
-
} else if (node.ghost) {
|
|
645
|
-
fill = 'transparent';
|
|
646
|
-
textFill = this.theme.colors.foreground;
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
this.currentY += height + 8;
|
|
650
|
-
|
|
651
|
-
const strokeAttr = isOutline ? `stroke="${this.theme.colors.border}" stroke-width="1"` : '';
|
|
652
|
-
|
|
653
|
-
// Render icon if present
|
|
654
|
-
let iconSvg = '';
|
|
655
|
-
if (hasIcon) {
|
|
656
|
-
const iconData = getIconData(node.icon!);
|
|
657
|
-
if (iconData) {
|
|
658
|
-
const iconX = isIconOnly ? (width - iconSize) / 2 : padding;
|
|
659
|
-
const iconY = (height - iconSize) / 2;
|
|
660
|
-
iconSvg = this.renderIconPaths(iconData, iconX, iconY, iconSize, textFill);
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
// Calculate text position
|
|
665
|
-
const textX = hasIcon && !isIconOnly ? padding + iconSize + 8 + (width - padding - iconSize - 8 - padding) / 2 : width / 2;
|
|
666
|
-
const textContent = isIconOnly ? '' : `<text x="${textX}" y="${height / 2 + 5}" font-size="14" fill="${textFill}" text-anchor="middle">${this.escapeXml(content)}</text>`;
|
|
667
|
-
|
|
668
|
-
return `
|
|
669
|
-
<g transform="translate(${x}, ${y})">
|
|
670
|
-
<rect width="${width}" height="${height}" rx="4" fill="${fill}" ${strokeAttr}/>
|
|
671
|
-
${iconSvg}
|
|
672
|
-
${textContent}
|
|
673
|
-
</g>`;
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
/**
|
|
677
|
-
* Render icon paths for SVG
|
|
678
|
-
*/
|
|
679
|
-
private renderIconPaths(data: IconData, x: number, y: number, size: number, color: string): string {
|
|
680
|
-
const scale = size / 24;
|
|
681
|
-
const paths = data.map(([tag, attrs]) => {
|
|
682
|
-
const attrStr = Object.entries(attrs)
|
|
683
|
-
.map(([key, value]) => `${key}="${value}"`)
|
|
684
|
-
.join(' ');
|
|
685
|
-
return `<${tag} ${attrStr} />`;
|
|
686
|
-
}).join('');
|
|
687
|
-
|
|
688
|
-
return `<g transform="translate(${x}, ${y}) scale(${scale})" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${paths}</g>`;
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
// ===========================================
|
|
692
|
-
// Display Renderers
|
|
693
|
-
// ===========================================
|
|
694
|
-
|
|
695
|
-
private renderImage(node: ImageNode): string {
|
|
696
|
-
const width = node.w && typeof node.w === 'number' ? node.w : 200;
|
|
697
|
-
const height = node.h && typeof node.h === 'number' ? node.h : 150;
|
|
698
|
-
const x = this.currentX;
|
|
699
|
-
const y = this.currentY;
|
|
700
|
-
|
|
701
|
-
this.currentY += height + 12;
|
|
702
|
-
|
|
703
|
-
return `
|
|
704
|
-
<g transform="translate(${x}, ${y})">
|
|
705
|
-
<rect width="${width}" height="${height}" fill="${this.theme.colors.muted}" stroke="${this.theme.colors.border}" stroke-width="1"/>
|
|
706
|
-
<line x1="0" y1="0" x2="${width}" y2="${height}" stroke="${this.theme.colors.border}" stroke-width="1"/>
|
|
707
|
-
<line x1="${width}" y1="0" x2="0" y2="${height}" stroke="${this.theme.colors.border}" stroke-width="1"/>
|
|
708
|
-
<text x="${width / 2}" y="${height / 2 + 5}" font-size="14" fill="${this.theme.colors.foreground}" text-anchor="middle">${this.escapeXml(node.alt || 'Image')}</text>
|
|
709
|
-
</g>`;
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
private renderPlaceholder(node: PlaceholderNode): string {
|
|
713
|
-
const width = node.w && typeof node.w === 'number' ? node.w : 200;
|
|
714
|
-
const height = node.h && typeof node.h === 'number' ? node.h : 100;
|
|
715
|
-
const x = this.currentX;
|
|
716
|
-
const y = this.currentY;
|
|
717
|
-
|
|
718
|
-
this.currentY += height + 12;
|
|
719
|
-
|
|
720
|
-
return `
|
|
721
|
-
<g transform="translate(${x}, ${y})">
|
|
722
|
-
<rect width="${width}" height="${height}" fill="${this.theme.colors.muted}" stroke="${this.theme.colors.border}" stroke-width="1" stroke-dasharray="4,4"/>
|
|
723
|
-
<text x="${width / 2}" y="${height / 2 + 5}" font-size="14" fill="${this.theme.colors.foreground}" text-anchor="middle">${this.escapeXml(node.label || 'Placeholder')}</text>
|
|
724
|
-
</g>`;
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
private renderAvatar(node: AvatarNode): string {
|
|
728
|
-
const sizes: Record<string, number> = { xs: 24, sm: 32, md: 40, lg: 48, xl: 64 };
|
|
729
|
-
const size = sizes[node.size || 'md'] || 40;
|
|
730
|
-
const radius = size / 2;
|
|
731
|
-
const x = this.currentX;
|
|
732
|
-
const y = this.currentY;
|
|
733
|
-
|
|
734
|
-
const initial = node.name ? node.name.charAt(0).toUpperCase() : '?';
|
|
735
|
-
|
|
736
|
-
this.currentY += size + 12;
|
|
737
|
-
|
|
738
|
-
return `
|
|
739
|
-
<g transform="translate(${x}, ${y})">
|
|
740
|
-
<circle cx="${radius}" cy="${radius}" r="${radius}" fill="${this.theme.colors.muted}" stroke="${this.theme.colors.border}" stroke-width="1"/>
|
|
741
|
-
<text x="${radius}" y="${radius + 5}" font-size="${size / 2.5}" fill="${this.theme.colors.foreground}" text-anchor="middle">${initial}</text>
|
|
742
|
-
</g>`;
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
private renderBadge(node: BadgeNode): string {
|
|
746
|
-
const content = node.content;
|
|
747
|
-
const width = Math.max(24, content.length * 8 + 16);
|
|
748
|
-
const height = 22;
|
|
749
|
-
const x = this.currentX;
|
|
750
|
-
const y = this.currentY;
|
|
751
|
-
|
|
752
|
-
const fill = this.theme.colors.muted;
|
|
753
|
-
const textFill = this.theme.colors.foreground;
|
|
754
|
-
|
|
755
|
-
this.currentY += height + 8;
|
|
756
|
-
|
|
757
|
-
return `
|
|
758
|
-
<g transform="translate(${x}, ${y})">
|
|
759
|
-
<rect width="${width}" height="${height}" rx="11" fill="${fill}" stroke="${this.theme.colors.border}" stroke-width="1"/>
|
|
760
|
-
<text x="${width / 2}" y="${height / 2 + 4}" font-size="12" fill="${textFill}" text-anchor="middle">${this.escapeXml(content)}</text>
|
|
761
|
-
</g>`;
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
// ===========================================
|
|
765
|
-
// Data Renderers
|
|
766
|
-
// ===========================================
|
|
767
|
-
|
|
768
|
-
private renderTable(node: TableNode): string {
|
|
769
|
-
const columns = node.columns || [];
|
|
770
|
-
const rows = node.rows || [];
|
|
771
|
-
const rowCount = rows.length || 3;
|
|
772
|
-
const colWidth = 120;
|
|
773
|
-
const rowHeight = 40;
|
|
774
|
-
const x = this.currentX;
|
|
775
|
-
const y = this.currentY;
|
|
776
|
-
|
|
777
|
-
let svg = `<g transform="translate(${x}, ${y})">`;
|
|
778
|
-
|
|
779
|
-
// Header
|
|
780
|
-
svg += `<rect width="${columns.length * colWidth}" height="${rowHeight}" fill="${this.theme.colors.muted}"/>`;
|
|
781
|
-
columns.forEach((col, i) => {
|
|
782
|
-
svg += `<text x="${i * colWidth + 12}" y="${rowHeight / 2 + 5}" font-size="14" font-weight="600">${this.escapeXml(col)}</text>`;
|
|
783
|
-
});
|
|
784
|
-
|
|
785
|
-
// Rows - if we have actual row data, use it; otherwise show placeholders
|
|
786
|
-
const displayRowCount = rows.length > 0 ? rows.length : Math.max(rowCount, 3);
|
|
787
|
-
for (let rowIdx = 0; rowIdx < displayRowCount; rowIdx++) {
|
|
788
|
-
const rowY = (rowIdx + 1) * rowHeight;
|
|
789
|
-
svg += `<rect y="${rowY}" width="${columns.length * colWidth}" height="${rowHeight}" fill="white" stroke="${this.theme.colors.border}" stroke-width="1"/>`;
|
|
790
|
-
columns.forEach((_, colIdx) => {
|
|
791
|
-
// If we have row data, try to render it
|
|
792
|
-
const cellContent = rows[rowIdx] && rows[rowIdx][colIdx]
|
|
793
|
-
? String(typeof rows[rowIdx][colIdx] === 'object' ? '...' : rows[rowIdx][colIdx])
|
|
794
|
-
: '—';
|
|
795
|
-
svg += `<text x="${colIdx * colWidth + 12}" y="${rowY + rowHeight / 2 + 5}" font-size="14" fill="${this.theme.colors.muted}">${this.escapeXml(cellContent)}</text>`;
|
|
796
|
-
});
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
svg += '</g>';
|
|
800
|
-
|
|
801
|
-
this.currentY += (displayRowCount + 1) * rowHeight + 16;
|
|
802
|
-
|
|
803
|
-
return svg;
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
private renderList(node: ListNode): string {
|
|
807
|
-
const x = this.currentX;
|
|
808
|
-
let y = this.currentY;
|
|
809
|
-
const items = node.items || [];
|
|
810
|
-
const ordered = node.ordered || false;
|
|
811
|
-
|
|
812
|
-
let svg = `<g transform="translate(${x}, ${y})">`;
|
|
813
|
-
|
|
814
|
-
items.forEach((item, idx) => {
|
|
815
|
-
const marker = ordered ? `${idx + 1}.` : '•';
|
|
816
|
-
const content = typeof item === 'string' ? item : item.content;
|
|
817
|
-
svg += `<text x="0" y="${idx * 24 + 16}" font-size="14" fill="${this.theme.colors.foreground}">${marker} ${this.escapeXml(content)}</text>`;
|
|
818
|
-
});
|
|
819
|
-
|
|
820
|
-
svg += '</g>';
|
|
821
|
-
|
|
822
|
-
this.currentY += items.length * 24 + 12;
|
|
823
|
-
|
|
824
|
-
return svg;
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
// ===========================================
|
|
828
|
-
// Feedback Renderers
|
|
829
|
-
// ===========================================
|
|
830
|
-
|
|
831
|
-
private renderAlert(node: AlertNode): string {
|
|
832
|
-
const width = Math.min(400, this.contentWidth);
|
|
833
|
-
const height = 48;
|
|
834
|
-
const x = this.currentX;
|
|
835
|
-
const y = this.currentY;
|
|
836
|
-
|
|
837
|
-
this.currentY += height + 12;
|
|
838
|
-
|
|
839
|
-
return `
|
|
840
|
-
<g transform="translate(${x}, ${y})">
|
|
841
|
-
<rect width="${width}" height="${height}" rx="4" fill="white" stroke="${this.theme.colors.border}" stroke-width="1"/>
|
|
842
|
-
<text x="16" y="${height / 2 + 5}" font-size="14" fill="${this.theme.colors.foreground}">${this.escapeXml(node.content)}</text>
|
|
843
|
-
</g>`;
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
private renderProgress(node: ProgressNode): string {
|
|
847
|
-
const width = 200;
|
|
848
|
-
const height = 8;
|
|
849
|
-
const x = this.currentX;
|
|
850
|
-
let y = this.currentY;
|
|
851
|
-
|
|
852
|
-
let result = '';
|
|
853
|
-
|
|
854
|
-
if (node.label) {
|
|
855
|
-
result += `<text x="${x}" y="${y + 14}" font-size="14" fill="${this.theme.colors.foreground}">${this.escapeXml(node.label)}</text>`;
|
|
856
|
-
y += 24;
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
const value = node.value || 0;
|
|
860
|
-
const max = node.max || 100;
|
|
861
|
-
const percent = Math.min(100, Math.max(0, (value / max) * 100));
|
|
862
|
-
|
|
863
|
-
result += `
|
|
864
|
-
<g transform="translate(${x}, ${y})">
|
|
865
|
-
<rect width="${width}" height="${height}" rx="${height / 2}" fill="${this.theme.colors.muted}"/>
|
|
866
|
-
<rect width="${(width * percent) / 100}" height="${height}" rx="${height / 2}" fill="${this.theme.colors.primary}"/>
|
|
867
|
-
</g>`;
|
|
868
|
-
|
|
869
|
-
this.currentY = y + height + 12;
|
|
870
|
-
|
|
871
|
-
return result;
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
private renderSpinner(node: SpinnerNode): string {
|
|
875
|
-
const sizes: Record<string, number> = { xs: 16, sm: 20, md: 24, lg: 32, xl: 40 };
|
|
876
|
-
const size = sizes[node.size || 'md'] || 24;
|
|
877
|
-
const x = this.currentX + size / 2;
|
|
878
|
-
const y = this.currentY + size / 2;
|
|
879
|
-
const radius = size / 2 - 2;
|
|
880
|
-
|
|
881
|
-
this.currentY += size + 12;
|
|
882
|
-
|
|
883
|
-
return `
|
|
884
|
-
<g transform="translate(${x}, ${y})">
|
|
885
|
-
<circle r="${radius}" fill="none" stroke="${this.theme.colors.muted}" stroke-width="2"/>
|
|
886
|
-
<path d="M0,-${radius} A${radius},${radius} 0 0,1 ${radius},0" fill="none" stroke="${this.theme.colors.primary}" stroke-width="2" stroke-linecap="round"/>
|
|
887
|
-
</g>`;
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
// ===========================================
|
|
891
|
-
// Navigation Renderers
|
|
892
|
-
// ===========================================
|
|
893
|
-
|
|
894
|
-
private renderNav(node: NavNode): string {
|
|
895
|
-
const items = node.items || [];
|
|
896
|
-
const x = this.currentX;
|
|
897
|
-
const y = this.currentY;
|
|
898
|
-
const vertical = node.vertical || false;
|
|
899
|
-
|
|
900
|
-
let svg = `<g transform="translate(${x}, ${y})">`;
|
|
901
|
-
|
|
902
|
-
if (vertical) {
|
|
903
|
-
items.forEach((item, idx) => {
|
|
904
|
-
const label = typeof item === 'string' ? item : item.label;
|
|
905
|
-
const isActive = typeof item === 'object' && item.active;
|
|
906
|
-
const fill = isActive ? this.theme.colors.foreground : this.theme.colors.muted;
|
|
907
|
-
svg += `<text x="0" y="${idx * 32 + 16}" font-size="14" fill="${fill}">${this.escapeXml(label)}</text>`;
|
|
908
|
-
});
|
|
909
|
-
this.currentY += items.length * 32 + 12;
|
|
910
|
-
} else {
|
|
911
|
-
let offsetX = 0;
|
|
912
|
-
items.forEach((item) => {
|
|
913
|
-
const label = typeof item === 'string' ? item : item.label;
|
|
914
|
-
const isActive = typeof item === 'object' && item.active;
|
|
915
|
-
const fill = isActive ? this.theme.colors.foreground : this.theme.colors.muted;
|
|
916
|
-
svg += `<text x="${offsetX}" y="16" font-size="14" fill="${fill}">${this.escapeXml(label)}</text>`;
|
|
917
|
-
offsetX += label.length * 8 + 24;
|
|
918
|
-
});
|
|
919
|
-
this.currentY += 32;
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
svg += '</g>';
|
|
923
|
-
|
|
924
|
-
return svg;
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
private renderTabs(node: TabsNode): string {
|
|
928
|
-
const items = node.items || [];
|
|
929
|
-
const x = this.currentX;
|
|
930
|
-
const y = this.currentY;
|
|
931
|
-
const tabHeight = 40;
|
|
932
|
-
|
|
933
|
-
let svg = `<g transform="translate(${x}, ${y})">`;
|
|
934
|
-
|
|
935
|
-
let offsetX = 0;
|
|
936
|
-
items.forEach((item) => {
|
|
937
|
-
const label = typeof item === 'string' ? item : item;
|
|
938
|
-
const tabWidth = label.length * 10 + 24;
|
|
939
|
-
svg += `<rect x="${offsetX}" width="${tabWidth}" height="${tabHeight}" fill="white" stroke="${this.theme.colors.border}" stroke-width="1"/>`;
|
|
940
|
-
svg += `<text x="${offsetX + tabWidth / 2}" y="${tabHeight / 2 + 5}" font-size="14" text-anchor="middle">${this.escapeXml(label)}</text>`;
|
|
941
|
-
offsetX += tabWidth;
|
|
942
|
-
});
|
|
943
|
-
|
|
944
|
-
svg += '</g>';
|
|
945
|
-
|
|
946
|
-
this.currentY += tabHeight + 12;
|
|
947
|
-
|
|
948
|
-
return svg;
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
private renderBreadcrumb(node: BreadcrumbNode): string {
|
|
952
|
-
const items = node.items || [];
|
|
953
|
-
const separator = '/';
|
|
954
|
-
const x = this.currentX;
|
|
955
|
-
const y = this.currentY;
|
|
956
|
-
|
|
957
|
-
let svg = `<g transform="translate(${x}, ${y})">`;
|
|
958
|
-
|
|
959
|
-
let offsetX = 0;
|
|
960
|
-
items.forEach((item, idx) => {
|
|
961
|
-
const label = typeof item === 'string' ? item : item.label;
|
|
962
|
-
const isLast = idx === items.length - 1;
|
|
963
|
-
const fill = isLast ? this.theme.colors.foreground : this.theme.colors.muted;
|
|
964
|
-
|
|
965
|
-
svg += `<text x="${offsetX}" y="16" font-size="14" fill="${fill}">${this.escapeXml(label)}</text>`;
|
|
966
|
-
offsetX += label.length * 8 + 8;
|
|
967
|
-
|
|
968
|
-
if (!isLast) {
|
|
969
|
-
svg += `<text x="${offsetX}" y="16" font-size="14" fill="${this.theme.colors.muted}">${separator}</text>`;
|
|
970
|
-
offsetX += 16;
|
|
971
|
-
}
|
|
972
|
-
});
|
|
973
|
-
|
|
974
|
-
svg += '</g>';
|
|
975
|
-
|
|
976
|
-
this.currentY += 28;
|
|
977
|
-
|
|
978
|
-
return svg;
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
// ===========================================
|
|
982
|
-
// Utility Methods
|
|
983
|
-
// ===========================================
|
|
984
|
-
|
|
985
|
-
private getFontSize(size: string): number {
|
|
986
|
-
const sizes: Record<string, number> = {
|
|
987
|
-
xs: 12,
|
|
988
|
-
sm: 14,
|
|
989
|
-
base: 16,
|
|
990
|
-
md: 16,
|
|
991
|
-
lg: 18,
|
|
992
|
-
xl: 20,
|
|
993
|
-
'2xl': 24,
|
|
994
|
-
'3xl': 30,
|
|
995
|
-
};
|
|
996
|
-
return sizes[size] || 16;
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
private resolveFontSize(size: TextNode['size']): number {
|
|
1000
|
-
if (!size) return 16; // default 'base'
|
|
1001
|
-
if (typeof size === 'string') {
|
|
1002
|
-
return this.getFontSize(size);
|
|
1003
|
-
}
|
|
1004
|
-
// ValueWithUnit object
|
|
1005
|
-
if (typeof size === 'object' && 'value' in size) {
|
|
1006
|
-
// Convert to px if needed
|
|
1007
|
-
if (size.unit === 'px') return size.value;
|
|
1008
|
-
if (size.unit === 'rem') return size.value * 16;
|
|
1009
|
-
if (size.unit === 'em') return size.value * 16;
|
|
1010
|
-
return size.value;
|
|
1011
|
-
}
|
|
1012
|
-
return 16;
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
private getTitleFontSize(level: number): number {
|
|
1016
|
-
const sizes: Record<number, number> = {
|
|
1017
|
-
1: 32,
|
|
1018
|
-
2: 28,
|
|
1019
|
-
3: 24,
|
|
1020
|
-
4: 20,
|
|
1021
|
-
5: 18,
|
|
1022
|
-
6: 16,
|
|
1023
|
-
};
|
|
1024
|
-
return sizes[level] || 24;
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
private escapeXml(str: string): string {
|
|
1028
|
-
return str
|
|
1029
|
-
.replace(/&/g, '&')
|
|
1030
|
-
.replace(/</g, '<')
|
|
1031
|
-
.replace(/>/g, '>')
|
|
1032
|
-
.replace(/"/g, '"')
|
|
1033
|
-
.replace(/'/g, ''');
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
/**
|
|
1038
|
-
* Create a new SVG renderer instance
|
|
1039
|
-
*/
|
|
1040
|
-
export function createSvgRenderer(options?: SvgRenderOptions): SvgRenderer {
|
|
1041
|
-
return new SvgRenderer(options);
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
/**
|
|
1045
|
-
* Render a wireframe document to SVG
|
|
1046
|
-
*/
|
|
1047
|
-
export function renderToSvg(doc: WireframeDocument, options?: SvgRenderOptions): SvgRenderResult {
|
|
1048
|
-
const renderer = new SvgRenderer(options);
|
|
1049
|
-
return renderer.render(doc);
|
|
1050
|
-
}
|