@wire-dsl/engine 0.0.2
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/LICENSE +36 -0
- package/README.md +102 -0
- package/dist/index.cjs +3042 -0
- package/dist/index.d.cts +312 -0
- package/dist/index.d.ts +312 -0
- package/dist/index.js +3005 -0
- package/package.json +58 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,3042 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
IRGenerator: () => IRGenerator,
|
|
24
|
+
LayoutEngine: () => LayoutEngine,
|
|
25
|
+
SVGRenderer: () => SVGRenderer,
|
|
26
|
+
buildSVG: () => buildSVG,
|
|
27
|
+
calculateLayout: () => calculateLayout,
|
|
28
|
+
createSVGElement: () => createSVGElement,
|
|
29
|
+
generateIR: () => generateIR,
|
|
30
|
+
parseWireDSL: () => parseWireDSL,
|
|
31
|
+
renderToSVG: () => renderToSVG,
|
|
32
|
+
resolveGridPosition: () => resolveGridPosition,
|
|
33
|
+
version: () => version
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(index_exports);
|
|
36
|
+
|
|
37
|
+
// src/parser/index.ts
|
|
38
|
+
var import_chevrotain = require("chevrotain");
|
|
39
|
+
var Project = (0, import_chevrotain.createToken)({ name: "Project", pattern: /project/ });
|
|
40
|
+
var Screen = (0, import_chevrotain.createToken)({ name: "Screen", pattern: /screen/ });
|
|
41
|
+
var Layout = (0, import_chevrotain.createToken)({ name: "Layout", pattern: /layout/ });
|
|
42
|
+
var Component = (0, import_chevrotain.createToken)({ name: "Component", pattern: /component/ });
|
|
43
|
+
var ComponentKeyword = (0, import_chevrotain.createToken)({
|
|
44
|
+
name: "ComponentKeyword",
|
|
45
|
+
pattern: /Component\b/
|
|
46
|
+
});
|
|
47
|
+
var Define = (0, import_chevrotain.createToken)({ name: "Define", pattern: /define/ });
|
|
48
|
+
var Theme = (0, import_chevrotain.createToken)({ name: "Theme", pattern: /theme/ });
|
|
49
|
+
var Mocks = (0, import_chevrotain.createToken)({ name: "Mocks", pattern: /mocks/ });
|
|
50
|
+
var Colors = (0, import_chevrotain.createToken)({ name: "Colors", pattern: /colors/ });
|
|
51
|
+
var Cell = (0, import_chevrotain.createToken)({ name: "Cell", pattern: /cell/ });
|
|
52
|
+
var LCurly = (0, import_chevrotain.createToken)({ name: "LCurly", pattern: /{/ });
|
|
53
|
+
var RCurly = (0, import_chevrotain.createToken)({ name: "RCurly", pattern: /}/ });
|
|
54
|
+
var LParen = (0, import_chevrotain.createToken)({ name: "LParen", pattern: /\(/ });
|
|
55
|
+
var RParen = (0, import_chevrotain.createToken)({ name: "RParen", pattern: /\)/ });
|
|
56
|
+
var Colon = (0, import_chevrotain.createToken)({ name: "Colon", pattern: /:/ });
|
|
57
|
+
var Comma = (0, import_chevrotain.createToken)({ name: "Comma", pattern: /,/ });
|
|
58
|
+
var StringLiteral = (0, import_chevrotain.createToken)({
|
|
59
|
+
name: "StringLiteral",
|
|
60
|
+
pattern: /"(?:[^"\\]|\\.)*"/
|
|
61
|
+
});
|
|
62
|
+
var NumberLiteral = (0, import_chevrotain.createToken)({
|
|
63
|
+
name: "NumberLiteral",
|
|
64
|
+
pattern: /\d+(\.\d+)?/
|
|
65
|
+
});
|
|
66
|
+
var HexColor = (0, import_chevrotain.createToken)({
|
|
67
|
+
name: "HexColor",
|
|
68
|
+
pattern: /#[0-9A-Fa-f]{6}/
|
|
69
|
+
});
|
|
70
|
+
var Identifier = (0, import_chevrotain.createToken)({
|
|
71
|
+
name: "Identifier",
|
|
72
|
+
pattern: /[a-zA-Z_][a-zA-Z0-9_]*/
|
|
73
|
+
});
|
|
74
|
+
var WhiteSpace = (0, import_chevrotain.createToken)({
|
|
75
|
+
name: "WhiteSpace",
|
|
76
|
+
pattern: /\s+/,
|
|
77
|
+
group: import_chevrotain.Lexer.SKIPPED
|
|
78
|
+
});
|
|
79
|
+
var LineComment = (0, import_chevrotain.createToken)({
|
|
80
|
+
name: "LineComment",
|
|
81
|
+
pattern: /\/\/[^\n]*/,
|
|
82
|
+
group: import_chevrotain.Lexer.SKIPPED
|
|
83
|
+
});
|
|
84
|
+
var BlockComment = (0, import_chevrotain.createToken)({
|
|
85
|
+
name: "BlockComment",
|
|
86
|
+
pattern: /\/\*[\s\S]*?\*\//,
|
|
87
|
+
group: import_chevrotain.Lexer.SKIPPED
|
|
88
|
+
});
|
|
89
|
+
var allTokens = [
|
|
90
|
+
WhiteSpace,
|
|
91
|
+
BlockComment,
|
|
92
|
+
// Must come before LineComment to avoid conflicts
|
|
93
|
+
LineComment,
|
|
94
|
+
// Keywords (must come before Identifier)
|
|
95
|
+
Project,
|
|
96
|
+
Screen,
|
|
97
|
+
Layout,
|
|
98
|
+
ComponentKeyword,
|
|
99
|
+
Component,
|
|
100
|
+
Define,
|
|
101
|
+
Theme,
|
|
102
|
+
Mocks,
|
|
103
|
+
Colors,
|
|
104
|
+
Cell,
|
|
105
|
+
// Punctuation
|
|
106
|
+
LCurly,
|
|
107
|
+
RCurly,
|
|
108
|
+
LParen,
|
|
109
|
+
RParen,
|
|
110
|
+
Colon,
|
|
111
|
+
Comma,
|
|
112
|
+
// Literals
|
|
113
|
+
StringLiteral,
|
|
114
|
+
NumberLiteral,
|
|
115
|
+
HexColor,
|
|
116
|
+
Identifier
|
|
117
|
+
];
|
|
118
|
+
var WireDSLLexer = new import_chevrotain.Lexer(allTokens);
|
|
119
|
+
var WireDSLParser = class extends import_chevrotain.CstParser {
|
|
120
|
+
constructor() {
|
|
121
|
+
super(allTokens);
|
|
122
|
+
// Root rule: project definition
|
|
123
|
+
this.project = this.RULE("project", () => {
|
|
124
|
+
this.CONSUME(Project);
|
|
125
|
+
this.CONSUME(StringLiteral, { LABEL: "projectName" });
|
|
126
|
+
this.CONSUME(LCurly);
|
|
127
|
+
this.MANY(() => {
|
|
128
|
+
this.OR([
|
|
129
|
+
{ ALT: () => this.SUBRULE(this.definedComponent) },
|
|
130
|
+
{ ALT: () => this.SUBRULE(this.themeDecl) },
|
|
131
|
+
{ ALT: () => this.SUBRULE(this.mocksDecl) },
|
|
132
|
+
{ ALT: () => this.SUBRULE(this.colorsDecl) },
|
|
133
|
+
{ ALT: () => this.SUBRULE(this.screen) }
|
|
134
|
+
]);
|
|
135
|
+
});
|
|
136
|
+
this.CONSUME(RCurly);
|
|
137
|
+
});
|
|
138
|
+
// theme { density: "normal" }
|
|
139
|
+
this.themeDecl = this.RULE("themeDecl", () => {
|
|
140
|
+
this.CONSUME(Theme);
|
|
141
|
+
this.CONSUME(LCurly);
|
|
142
|
+
this.MANY(() => {
|
|
143
|
+
this.SUBRULE(this.themeProperty);
|
|
144
|
+
});
|
|
145
|
+
this.CONSUME(RCurly);
|
|
146
|
+
});
|
|
147
|
+
// density: "normal"
|
|
148
|
+
this.themeProperty = this.RULE("themeProperty", () => {
|
|
149
|
+
this.CONSUME(Identifier, { LABEL: "themeKey" });
|
|
150
|
+
this.CONSUME(Colon);
|
|
151
|
+
this.CONSUME(StringLiteral, { LABEL: "themeValue" });
|
|
152
|
+
});
|
|
153
|
+
// mocks { status: "A,B,C" ... }
|
|
154
|
+
this.mocksDecl = this.RULE("mocksDecl", () => {
|
|
155
|
+
this.CONSUME(Mocks);
|
|
156
|
+
this.CONSUME(LCurly);
|
|
157
|
+
this.MANY(() => {
|
|
158
|
+
this.SUBRULE(this.mockEntry);
|
|
159
|
+
});
|
|
160
|
+
this.CONSUME(RCurly);
|
|
161
|
+
});
|
|
162
|
+
// status: "A,B,C"
|
|
163
|
+
this.mockEntry = this.RULE("mockEntry", () => {
|
|
164
|
+
this.CONSUME(Identifier, { LABEL: "mockKey" });
|
|
165
|
+
this.CONSUME(Colon);
|
|
166
|
+
this.CONSUME(StringLiteral, { LABEL: "mockValue" });
|
|
167
|
+
});
|
|
168
|
+
// colors { primary: #3B82F6, ... }
|
|
169
|
+
this.colorsDecl = this.RULE("colorsDecl", () => {
|
|
170
|
+
this.CONSUME(Colors);
|
|
171
|
+
this.CONSUME(LCurly);
|
|
172
|
+
this.MANY(() => {
|
|
173
|
+
this.SUBRULE(this.colorEntry);
|
|
174
|
+
});
|
|
175
|
+
this.CONSUME(RCurly);
|
|
176
|
+
});
|
|
177
|
+
// primary: #3B82F6 or primary: lightBlue
|
|
178
|
+
this.colorEntry = this.RULE("colorEntry", () => {
|
|
179
|
+
this.CONSUME(Identifier, { LABEL: "colorKey" });
|
|
180
|
+
this.CONSUME(Colon);
|
|
181
|
+
this.OR([
|
|
182
|
+
{ ALT: () => this.CONSUME(HexColor, { LABEL: "colorValue" }) },
|
|
183
|
+
{ ALT: () => this.CONSUME2(Identifier, { LABEL: "colorValue" }) }
|
|
184
|
+
]);
|
|
185
|
+
});
|
|
186
|
+
// define Component "ButtonGroup" { layout stack { ... } }
|
|
187
|
+
this.definedComponent = this.RULE("definedComponent", () => {
|
|
188
|
+
this.CONSUME(Define);
|
|
189
|
+
this.CONSUME(ComponentKeyword, { LABEL: "componentKeyword" });
|
|
190
|
+
this.CONSUME(StringLiteral, { LABEL: "componentName" });
|
|
191
|
+
this.CONSUME(LCurly);
|
|
192
|
+
this.OR([
|
|
193
|
+
{ ALT: () => this.SUBRULE(this.layout) },
|
|
194
|
+
{ ALT: () => this.SUBRULE(this.component) }
|
|
195
|
+
]);
|
|
196
|
+
this.CONSUME(RCurly);
|
|
197
|
+
});
|
|
198
|
+
// screen Main(background: white) { ... }
|
|
199
|
+
this.screen = this.RULE("screen", () => {
|
|
200
|
+
this.CONSUME(Screen);
|
|
201
|
+
this.CONSUME(Identifier, { LABEL: "screenName" });
|
|
202
|
+
this.OPTION(() => {
|
|
203
|
+
this.SUBRULE(this.paramList);
|
|
204
|
+
});
|
|
205
|
+
this.CONSUME(LCurly);
|
|
206
|
+
this.SUBRULE(this.layout);
|
|
207
|
+
this.CONSUME(RCurly);
|
|
208
|
+
});
|
|
209
|
+
// layout stack(...) { ... }
|
|
210
|
+
this.layout = this.RULE("layout", () => {
|
|
211
|
+
this.CONSUME(Layout);
|
|
212
|
+
this.CONSUME(Identifier, { LABEL: "layoutType" });
|
|
213
|
+
this.OPTION(() => {
|
|
214
|
+
this.SUBRULE(this.paramList);
|
|
215
|
+
});
|
|
216
|
+
this.CONSUME(LCurly);
|
|
217
|
+
this.MANY(() => {
|
|
218
|
+
this.OR([
|
|
219
|
+
{ ALT: () => this.SUBRULE(this.component) },
|
|
220
|
+
{ ALT: () => this.SUBRULE2(this.layout) },
|
|
221
|
+
{ ALT: () => this.SUBRULE(this.cell) }
|
|
222
|
+
]);
|
|
223
|
+
});
|
|
224
|
+
this.CONSUME(RCurly);
|
|
225
|
+
});
|
|
226
|
+
// cell span: 4 { ... }
|
|
227
|
+
this.cell = this.RULE("cell", () => {
|
|
228
|
+
this.CONSUME(Cell);
|
|
229
|
+
this.MANY(() => {
|
|
230
|
+
this.SUBRULE(this.property);
|
|
231
|
+
});
|
|
232
|
+
this.CONSUME(LCurly);
|
|
233
|
+
this.MANY2(() => {
|
|
234
|
+
this.OR([
|
|
235
|
+
{ ALT: () => this.SUBRULE(this.component) },
|
|
236
|
+
{ ALT: () => this.SUBRULE(this.layout) }
|
|
237
|
+
]);
|
|
238
|
+
});
|
|
239
|
+
this.CONSUME(RCurly);
|
|
240
|
+
});
|
|
241
|
+
// component Heading text: "Hello"
|
|
242
|
+
this.component = this.RULE("component", () => {
|
|
243
|
+
this.CONSUME(Component);
|
|
244
|
+
this.CONSUME(Identifier, { LABEL: "componentType" });
|
|
245
|
+
this.MANY(() => {
|
|
246
|
+
this.SUBRULE(this.property);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
// property: key: value
|
|
250
|
+
this.property = this.RULE("property", () => {
|
|
251
|
+
this.CONSUME(Identifier, { LABEL: "propKey" });
|
|
252
|
+
this.CONSUME(Colon);
|
|
253
|
+
this.OR([
|
|
254
|
+
{ ALT: () => this.CONSUME(StringLiteral, { LABEL: "propValue" }) },
|
|
255
|
+
{ ALT: () => this.CONSUME(NumberLiteral, { LABEL: "propValue" }) },
|
|
256
|
+
{ ALT: () => this.CONSUME2(Identifier, { LABEL: "propValue" }) }
|
|
257
|
+
]);
|
|
258
|
+
});
|
|
259
|
+
// (param1: value1, param2: value2)
|
|
260
|
+
this.paramList = this.RULE("paramList", () => {
|
|
261
|
+
this.CONSUME(LParen);
|
|
262
|
+
this.MANY_SEP({
|
|
263
|
+
SEP: Comma,
|
|
264
|
+
DEF: () => {
|
|
265
|
+
this.SUBRULE(this.property);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
this.CONSUME(RParen);
|
|
269
|
+
});
|
|
270
|
+
this.performSelfAnalysis();
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
var parserInstance = new WireDSLParser();
|
|
274
|
+
var BaseCstVisitor = parserInstance.getBaseCstVisitorConstructor();
|
|
275
|
+
var WireDSLVisitor = class extends BaseCstVisitor {
|
|
276
|
+
constructor() {
|
|
277
|
+
super();
|
|
278
|
+
this.validateVisitor();
|
|
279
|
+
}
|
|
280
|
+
project(ctx) {
|
|
281
|
+
const projectName = ctx.projectName[0].image.slice(1, -1);
|
|
282
|
+
const theme = {};
|
|
283
|
+
const mocks = {};
|
|
284
|
+
const colors = {};
|
|
285
|
+
const definedComponents = [];
|
|
286
|
+
const screens = [];
|
|
287
|
+
if (ctx.themeDecl && ctx.themeDecl.length > 0) {
|
|
288
|
+
const themeBlock = this.visit(ctx.themeDecl[0]);
|
|
289
|
+
Object.assign(theme, themeBlock);
|
|
290
|
+
}
|
|
291
|
+
if (ctx.mocksDecl && ctx.mocksDecl.length > 0) {
|
|
292
|
+
const mocksBlock = this.visit(ctx.mocksDecl[0]);
|
|
293
|
+
Object.assign(mocks, mocksBlock);
|
|
294
|
+
}
|
|
295
|
+
if (ctx.colorsDecl && ctx.colorsDecl.length > 0) {
|
|
296
|
+
const colorsBlock = this.visit(ctx.colorsDecl[0]);
|
|
297
|
+
Object.assign(colors, colorsBlock);
|
|
298
|
+
}
|
|
299
|
+
if (ctx.definedComponent) {
|
|
300
|
+
ctx.definedComponent.forEach((comp) => {
|
|
301
|
+
definedComponents.push(this.visit(comp));
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
if (ctx.screen) {
|
|
305
|
+
ctx.screen.forEach((screen) => {
|
|
306
|
+
screens.push(this.visit(screen));
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
type: "project",
|
|
311
|
+
name: projectName,
|
|
312
|
+
theme,
|
|
313
|
+
mocks,
|
|
314
|
+
colors,
|
|
315
|
+
definedComponents,
|
|
316
|
+
screens
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
themeDecl(ctx) {
|
|
320
|
+
const theme = {};
|
|
321
|
+
if (ctx.themeProperty) {
|
|
322
|
+
ctx.themeProperty.forEach((prop) => {
|
|
323
|
+
const { key, value } = this.visit(prop);
|
|
324
|
+
theme[key] = value;
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
return theme;
|
|
328
|
+
}
|
|
329
|
+
themeProperty(ctx) {
|
|
330
|
+
const key = ctx.themeKey[0].image;
|
|
331
|
+
const value = ctx.themeValue[0].image.slice(1, -1);
|
|
332
|
+
return { key, value };
|
|
333
|
+
}
|
|
334
|
+
mocksDecl(ctx) {
|
|
335
|
+
const mocks = {};
|
|
336
|
+
if (ctx.mockEntry) {
|
|
337
|
+
ctx.mockEntry.forEach((entry) => {
|
|
338
|
+
const { key, value } = this.visit(entry);
|
|
339
|
+
mocks[key] = value;
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
return mocks;
|
|
343
|
+
}
|
|
344
|
+
mockEntry(ctx) {
|
|
345
|
+
const key = ctx.mockKey[0].image;
|
|
346
|
+
const value = ctx.mockValue[0].image.slice(1, -1);
|
|
347
|
+
return { key, value };
|
|
348
|
+
}
|
|
349
|
+
colorsDecl(ctx) {
|
|
350
|
+
const colors = {};
|
|
351
|
+
if (ctx.colorEntry) {
|
|
352
|
+
ctx.colorEntry.forEach((entry) => {
|
|
353
|
+
const { key, value } = this.visit(entry);
|
|
354
|
+
colors[key] = value;
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
return colors;
|
|
358
|
+
}
|
|
359
|
+
colorEntry(ctx) {
|
|
360
|
+
const key = ctx.colorKey[0].image;
|
|
361
|
+
const value = ctx.colorValue[0].image;
|
|
362
|
+
return { key, value };
|
|
363
|
+
}
|
|
364
|
+
definedComponent(ctx) {
|
|
365
|
+
const name = ctx.componentName[0].image.slice(1, -1);
|
|
366
|
+
let body;
|
|
367
|
+
if (ctx.layout && ctx.layout.length > 0) {
|
|
368
|
+
body = this.visit(ctx.layout[0]);
|
|
369
|
+
} else if (ctx.component && ctx.component.length > 0) {
|
|
370
|
+
body = this.visit(ctx.component[0]);
|
|
371
|
+
} else {
|
|
372
|
+
throw new Error(`Defined component "${name}" must contain either a layout or component`);
|
|
373
|
+
}
|
|
374
|
+
return {
|
|
375
|
+
type: "definedComponent",
|
|
376
|
+
name,
|
|
377
|
+
body
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
screen(ctx) {
|
|
381
|
+
const params = ctx.paramList ? this.visit(ctx.paramList[0]) : {};
|
|
382
|
+
return {
|
|
383
|
+
type: "screen",
|
|
384
|
+
name: ctx.screenName[0].image,
|
|
385
|
+
params,
|
|
386
|
+
layout: this.visit(ctx.layout[0])
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
layout(ctx) {
|
|
390
|
+
const layoutType = ctx.layoutType[0].image;
|
|
391
|
+
const params = {};
|
|
392
|
+
const children = [];
|
|
393
|
+
if (ctx.paramList) {
|
|
394
|
+
const paramResult = this.visit(ctx.paramList);
|
|
395
|
+
Object.assign(params, paramResult);
|
|
396
|
+
}
|
|
397
|
+
const childNodes = [];
|
|
398
|
+
if (ctx.component) {
|
|
399
|
+
ctx.component.forEach((comp) => {
|
|
400
|
+
const startToken = comp.children?.Component?.[0] || comp.children?.componentType?.[0];
|
|
401
|
+
childNodes.push({ type: "component", node: comp, index: startToken.startOffset });
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
if (ctx.layout) {
|
|
405
|
+
ctx.layout.forEach((layout) => {
|
|
406
|
+
const startToken = layout.children?.Layout?.[0] || layout.children?.layoutType?.[0];
|
|
407
|
+
childNodes.push({ type: "layout", node: layout, index: startToken.startOffset });
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
if (ctx.cell) {
|
|
411
|
+
ctx.cell.forEach((cell) => {
|
|
412
|
+
const startToken = cell.children?.Cell?.[0];
|
|
413
|
+
childNodes.push({ type: "cell", node: cell, index: startToken.startOffset });
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
childNodes.sort((a, b) => a.index - b.index);
|
|
417
|
+
childNodes.forEach((item) => {
|
|
418
|
+
if (item.type === "component") {
|
|
419
|
+
children.push(this.visit(item.node));
|
|
420
|
+
} else if (item.type === "layout") {
|
|
421
|
+
children.push(this.visit(item.node));
|
|
422
|
+
} else if (item.type === "cell") {
|
|
423
|
+
children.push(this.visit(item.node));
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
return {
|
|
427
|
+
type: "layout",
|
|
428
|
+
layoutType,
|
|
429
|
+
params,
|
|
430
|
+
children
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
cell(ctx) {
|
|
434
|
+
const props = {};
|
|
435
|
+
const children = [];
|
|
436
|
+
if (ctx.property) {
|
|
437
|
+
ctx.property.forEach((prop) => {
|
|
438
|
+
const result = this.visit(prop);
|
|
439
|
+
props[result.key] = result.value;
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
const childNodes = [];
|
|
443
|
+
if (ctx.component) {
|
|
444
|
+
ctx.component.forEach((comp) => {
|
|
445
|
+
const startToken = comp.children?.Component?.[0] || comp.children?.componentType?.[0];
|
|
446
|
+
childNodes.push({ type: "component", node: comp, index: startToken.startOffset });
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
if (ctx.layout) {
|
|
450
|
+
ctx.layout.forEach((layout) => {
|
|
451
|
+
const startToken = layout.children?.Layout?.[0] || layout.children?.layoutType?.[0];
|
|
452
|
+
childNodes.push({ type: "layout", node: layout, index: startToken.startOffset });
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
childNodes.sort((a, b) => a.index - b.index);
|
|
456
|
+
childNodes.forEach((item) => {
|
|
457
|
+
if (item.type === "component") {
|
|
458
|
+
children.push(this.visit(item.node));
|
|
459
|
+
} else if (item.type === "layout") {
|
|
460
|
+
children.push(this.visit(item.node));
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
return {
|
|
464
|
+
type: "cell",
|
|
465
|
+
props,
|
|
466
|
+
children
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
component(ctx) {
|
|
470
|
+
const componentType = ctx.componentType[0].image;
|
|
471
|
+
const props = {};
|
|
472
|
+
if (ctx.property) {
|
|
473
|
+
ctx.property.forEach((prop) => {
|
|
474
|
+
const result = this.visit(prop);
|
|
475
|
+
props[result.key] = result.value;
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
return {
|
|
479
|
+
type: "component",
|
|
480
|
+
componentType,
|
|
481
|
+
props
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
property(ctx) {
|
|
485
|
+
const key = ctx.propKey[0].image;
|
|
486
|
+
const rawValue = ctx.propValue[0].image;
|
|
487
|
+
let value = rawValue;
|
|
488
|
+
if (typeof rawValue === "string" && rawValue.startsWith('"')) {
|
|
489
|
+
value = rawValue.slice(1, -1);
|
|
490
|
+
} else if (!isNaN(Number(rawValue))) {
|
|
491
|
+
value = Number(rawValue);
|
|
492
|
+
}
|
|
493
|
+
return { key, value };
|
|
494
|
+
}
|
|
495
|
+
paramList(ctx) {
|
|
496
|
+
const params = {};
|
|
497
|
+
if (ctx.property) {
|
|
498
|
+
ctx.property.forEach((prop) => {
|
|
499
|
+
const result = this.visit(prop);
|
|
500
|
+
params[result.key] = result.value;
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
return params;
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
var visitor = new WireDSLVisitor();
|
|
507
|
+
function parseWireDSL(input) {
|
|
508
|
+
const lexResult = WireDSLLexer.tokenize(input);
|
|
509
|
+
if (lexResult.errors.length > 0) {
|
|
510
|
+
throw new Error(`Lexer errors:
|
|
511
|
+
${lexResult.errors.map((e) => e.message).join("\n")}`);
|
|
512
|
+
}
|
|
513
|
+
parserInstance.input = lexResult.tokens;
|
|
514
|
+
const cst = parserInstance.project();
|
|
515
|
+
if (parserInstance.errors.length > 0) {
|
|
516
|
+
throw new Error(`Parser errors:
|
|
517
|
+
${parserInstance.errors.map((e) => e.message).join("\n")}`);
|
|
518
|
+
}
|
|
519
|
+
const ast = visitor.visit(cst);
|
|
520
|
+
validateComponentDefinitionCycles(ast);
|
|
521
|
+
return ast;
|
|
522
|
+
}
|
|
523
|
+
function validateComponentDefinitionCycles(ast) {
|
|
524
|
+
if (!ast.definedComponents || ast.definedComponents.length === 0) {
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
const components = /* @__PURE__ */ new Map();
|
|
528
|
+
ast.definedComponents.forEach((comp) => {
|
|
529
|
+
components.set(comp.name, comp);
|
|
530
|
+
});
|
|
531
|
+
const visited = /* @__PURE__ */ new Set();
|
|
532
|
+
const recursionStack = /* @__PURE__ */ new Set();
|
|
533
|
+
function getComponentDependencies(node) {
|
|
534
|
+
const deps = /* @__PURE__ */ new Set();
|
|
535
|
+
if (node.type === "layout") {
|
|
536
|
+
const layout = node;
|
|
537
|
+
if (layout.children) {
|
|
538
|
+
layout.children.forEach((child) => {
|
|
539
|
+
if (child.type === "component") {
|
|
540
|
+
const component = child;
|
|
541
|
+
deps.add(component.componentType);
|
|
542
|
+
} else if (child.type === "layout") {
|
|
543
|
+
const nested = getComponentDependencies(child);
|
|
544
|
+
nested.forEach((d) => deps.add(d));
|
|
545
|
+
} else if (child.type === "cell") {
|
|
546
|
+
const cell = child;
|
|
547
|
+
if (cell.children) {
|
|
548
|
+
cell.children.forEach((cellChild) => {
|
|
549
|
+
if (cellChild.type === "component") {
|
|
550
|
+
deps.add(cellChild.componentType);
|
|
551
|
+
} else if (cellChild.type === "layout") {
|
|
552
|
+
const nested = getComponentDependencies(cellChild);
|
|
553
|
+
nested.forEach((d) => deps.add(d));
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return deps;
|
|
562
|
+
}
|
|
563
|
+
function hasCycle(componentName, path = []) {
|
|
564
|
+
if (recursionStack.has(componentName)) {
|
|
565
|
+
const cycleStart = path.indexOf(componentName);
|
|
566
|
+
const cycle = path.slice(cycleStart).concat(componentName);
|
|
567
|
+
return cycle;
|
|
568
|
+
}
|
|
569
|
+
if (visited.has(componentName)) {
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
const component = components.get(componentName);
|
|
573
|
+
if (!component) {
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
recursionStack.add(componentName);
|
|
577
|
+
const currentPath = [...path, componentName];
|
|
578
|
+
const dependencies = getComponentDependencies(component.body);
|
|
579
|
+
for (const dep of dependencies) {
|
|
580
|
+
const definedDep = components.has(dep);
|
|
581
|
+
if (definedDep) {
|
|
582
|
+
const cycle = hasCycle(dep, currentPath);
|
|
583
|
+
if (cycle) {
|
|
584
|
+
return cycle;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
recursionStack.delete(componentName);
|
|
589
|
+
visited.add(componentName);
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
for (const [componentName] of components) {
|
|
593
|
+
visited.clear();
|
|
594
|
+
recursionStack.clear();
|
|
595
|
+
const cycle = hasCycle(componentName);
|
|
596
|
+
if (cycle) {
|
|
597
|
+
throw new Error(
|
|
598
|
+
`Circular component definition detected: ${cycle.join(" \u2192 ")}
|
|
599
|
+
Components cannot reference each other in a cycle.`
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// src/ir/index.ts
|
|
606
|
+
var import_zod = require("zod");
|
|
607
|
+
var IRThemeSchema = import_zod.z.object({
|
|
608
|
+
density: import_zod.z.enum(["compact", "normal", "comfortable"]),
|
|
609
|
+
spacing: import_zod.z.enum(["xs", "sm", "md", "lg", "xl"]),
|
|
610
|
+
radius: import_zod.z.enum(["none", "sm", "md", "lg", "full"]),
|
|
611
|
+
stroke: import_zod.z.enum(["thin", "normal", "thick"]),
|
|
612
|
+
font: import_zod.z.enum(["sm", "base", "lg"]),
|
|
613
|
+
background: import_zod.z.string().optional()
|
|
614
|
+
});
|
|
615
|
+
var IRStyleSchema = import_zod.z.object({
|
|
616
|
+
padding: import_zod.z.string().optional(),
|
|
617
|
+
gap: import_zod.z.string().optional(),
|
|
618
|
+
align: import_zod.z.enum(["left", "center", "right", "justify"]).optional(),
|
|
619
|
+
justify: import_zod.z.enum(["start", "center", "end"]).optional(),
|
|
620
|
+
background: import_zod.z.string().optional()
|
|
621
|
+
});
|
|
622
|
+
var IRMetaSchema = import_zod.z.object({
|
|
623
|
+
source: import_zod.z.string().optional()
|
|
624
|
+
});
|
|
625
|
+
var IRContainerNodeSchema = import_zod.z.object({
|
|
626
|
+
id: import_zod.z.string(),
|
|
627
|
+
kind: import_zod.z.literal("container"),
|
|
628
|
+
containerType: import_zod.z.enum(["stack", "grid", "split", "panel", "card"]),
|
|
629
|
+
params: import_zod.z.record(import_zod.z.string(), import_zod.z.union([import_zod.z.string(), import_zod.z.number()])),
|
|
630
|
+
children: import_zod.z.array(import_zod.z.object({ ref: import_zod.z.string() })),
|
|
631
|
+
style: IRStyleSchema,
|
|
632
|
+
meta: IRMetaSchema
|
|
633
|
+
});
|
|
634
|
+
var IRComponentNodeSchema = import_zod.z.object({
|
|
635
|
+
id: import_zod.z.string(),
|
|
636
|
+
kind: import_zod.z.literal("component"),
|
|
637
|
+
componentType: import_zod.z.string(),
|
|
638
|
+
props: import_zod.z.record(import_zod.z.string(), import_zod.z.union([import_zod.z.string(), import_zod.z.number()])),
|
|
639
|
+
style: IRStyleSchema,
|
|
640
|
+
meta: IRMetaSchema
|
|
641
|
+
});
|
|
642
|
+
var IRNodeSchema = import_zod.z.union([IRContainerNodeSchema, IRComponentNodeSchema]);
|
|
643
|
+
var IRScreenSchema = import_zod.z.object({
|
|
644
|
+
id: import_zod.z.string(),
|
|
645
|
+
name: import_zod.z.string(),
|
|
646
|
+
viewport: import_zod.z.object({ width: import_zod.z.number(), height: import_zod.z.number() }),
|
|
647
|
+
background: import_zod.z.string().optional(),
|
|
648
|
+
root: import_zod.z.object({ ref: import_zod.z.string() })
|
|
649
|
+
});
|
|
650
|
+
var IRProjectSchema = import_zod.z.object({
|
|
651
|
+
id: import_zod.z.string(),
|
|
652
|
+
name: import_zod.z.string(),
|
|
653
|
+
theme: IRThemeSchema,
|
|
654
|
+
mocks: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()),
|
|
655
|
+
colors: import_zod.z.record(import_zod.z.string(), import_zod.z.string()),
|
|
656
|
+
screens: import_zod.z.array(IRScreenSchema),
|
|
657
|
+
nodes: import_zod.z.record(import_zod.z.string(), IRNodeSchema)
|
|
658
|
+
});
|
|
659
|
+
var IRContractSchema = import_zod.z.object({
|
|
660
|
+
irVersion: import_zod.z.string(),
|
|
661
|
+
project: IRProjectSchema
|
|
662
|
+
});
|
|
663
|
+
var IDGenerator = class {
|
|
664
|
+
constructor() {
|
|
665
|
+
this.counters = /* @__PURE__ */ new Map();
|
|
666
|
+
}
|
|
667
|
+
generate(prefix) {
|
|
668
|
+
const current = this.counters.get(prefix) || 0;
|
|
669
|
+
const next = current + 1;
|
|
670
|
+
this.counters.set(prefix, next);
|
|
671
|
+
return `${prefix}_${next}`;
|
|
672
|
+
}
|
|
673
|
+
reset() {
|
|
674
|
+
this.counters.clear();
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
var IRGenerator = class {
|
|
678
|
+
constructor() {
|
|
679
|
+
this.idGen = new IDGenerator();
|
|
680
|
+
this.nodes = {};
|
|
681
|
+
this.definedComponents = /* @__PURE__ */ new Map();
|
|
682
|
+
this.definedComponentIndices = /* @__PURE__ */ new Map();
|
|
683
|
+
this.undefinedComponentsUsed = /* @__PURE__ */ new Set();
|
|
684
|
+
this.warnings = [];
|
|
685
|
+
this.theme = {
|
|
686
|
+
density: "normal",
|
|
687
|
+
spacing: "md",
|
|
688
|
+
radius: "md",
|
|
689
|
+
stroke: "normal",
|
|
690
|
+
font: "base"
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
generate(ast) {
|
|
694
|
+
this.idGen.reset();
|
|
695
|
+
this.nodes = {};
|
|
696
|
+
this.definedComponents.clear();
|
|
697
|
+
this.definedComponentIndices.clear();
|
|
698
|
+
this.undefinedComponentsUsed.clear();
|
|
699
|
+
this.warnings = [];
|
|
700
|
+
if (ast.definedComponents && ast.definedComponents.length > 0) {
|
|
701
|
+
ast.definedComponents.forEach((def, index) => {
|
|
702
|
+
this.definedComponents.set(def.name, def);
|
|
703
|
+
this.definedComponentIndices.set(def.name, index);
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
this.applyTheme(ast.theme);
|
|
707
|
+
const screens = ast.screens.map(
|
|
708
|
+
(screen, screenIndex) => this.convertScreen(screen, screenIndex)
|
|
709
|
+
);
|
|
710
|
+
if (this.undefinedComponentsUsed.size > 0) {
|
|
711
|
+
const undefinedList = Array.from(this.undefinedComponentsUsed).sort();
|
|
712
|
+
throw new Error(
|
|
713
|
+
`Components used but not defined: ${undefinedList.join(", ")}
|
|
714
|
+
Define these components with: define Component "Name" { ... }`
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
const project = {
|
|
718
|
+
id: this.sanitizeId(ast.name),
|
|
719
|
+
name: ast.name,
|
|
720
|
+
theme: this.theme,
|
|
721
|
+
mocks: ast.mocks || {},
|
|
722
|
+
colors: ast.colors || {},
|
|
723
|
+
screens,
|
|
724
|
+
nodes: this.nodes
|
|
725
|
+
};
|
|
726
|
+
const ir = {
|
|
727
|
+
irVersion: "1.0",
|
|
728
|
+
project
|
|
729
|
+
};
|
|
730
|
+
return IRContractSchema.parse(ir);
|
|
731
|
+
}
|
|
732
|
+
applyTheme(astTheme) {
|
|
733
|
+
if (astTheme.density) {
|
|
734
|
+
this.theme.density = astTheme.density;
|
|
735
|
+
}
|
|
736
|
+
if (astTheme.spacing) {
|
|
737
|
+
this.theme.spacing = astTheme.spacing;
|
|
738
|
+
}
|
|
739
|
+
if (astTheme.radius) {
|
|
740
|
+
this.theme.radius = astTheme.radius;
|
|
741
|
+
}
|
|
742
|
+
if (astTheme.stroke) {
|
|
743
|
+
this.theme.stroke = astTheme.stroke;
|
|
744
|
+
}
|
|
745
|
+
if (astTheme.font) {
|
|
746
|
+
this.theme.font = astTheme.font;
|
|
747
|
+
}
|
|
748
|
+
if (astTheme.background) {
|
|
749
|
+
this.theme.background = astTheme.background;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
convertScreen(screen, screenIndex) {
|
|
753
|
+
const rootNodeId = this.convertLayout(screen.layout);
|
|
754
|
+
const irScreen = {
|
|
755
|
+
id: this.sanitizeId(screen.name),
|
|
756
|
+
name: screen.name,
|
|
757
|
+
viewport: { width: 1280, height: 720 },
|
|
758
|
+
root: { ref: rootNodeId }
|
|
759
|
+
};
|
|
760
|
+
if (screen.params.background) {
|
|
761
|
+
irScreen.background = String(screen.params.background);
|
|
762
|
+
} else if (this.theme.background) {
|
|
763
|
+
irScreen.background = this.theme.background;
|
|
764
|
+
}
|
|
765
|
+
return irScreen;
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* Validates that component definitions appear before their first usage
|
|
769
|
+
* Generates warnings (not errors) to allow flexible code organization
|
|
770
|
+
*/
|
|
771
|
+
validateComponentOrder(ast) {
|
|
772
|
+
if (!ast.definedComponents || ast.definedComponents.length === 0) {
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
const definedComponentNames = new Set(ast.definedComponents.map((d) => d.name));
|
|
776
|
+
ast.screens.forEach((screen) => {
|
|
777
|
+
const usedComponents = this.findComponentsInLayout(screen.layout);
|
|
778
|
+
usedComponents.forEach(({ componentType }) => {
|
|
779
|
+
if (definedComponentNames.has(componentType)) {
|
|
780
|
+
const defIdx = this.definedComponentIndices.get(componentType) ?? -1;
|
|
781
|
+
const screenIdx = ast.screens.indexOf(screen);
|
|
782
|
+
this.warnings.push({
|
|
783
|
+
type: "component-order",
|
|
784
|
+
message: `Component "${componentType}" is used in screen "${screen.name}". Consider defining "${componentType}" before this screen for better code clarity.`
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
});
|
|
789
|
+
if (this.warnings.length > 0) {
|
|
790
|
+
this.warnings.forEach((w) => {
|
|
791
|
+
console.warn(`\u26A0\uFE0F [${w.type.toUpperCase()}] ${w.message}`);
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Recursively finds all component usages in a layout
|
|
797
|
+
*/
|
|
798
|
+
findComponentsInLayout(layout, found = []) {
|
|
799
|
+
if (layout.children && layout.children.length > 0) {
|
|
800
|
+
layout.children.forEach((child) => {
|
|
801
|
+
if (child.type === "component") {
|
|
802
|
+
found.push({
|
|
803
|
+
componentType: child.componentType,
|
|
804
|
+
location: "layout"
|
|
805
|
+
});
|
|
806
|
+
} else if (child.type === "layout") {
|
|
807
|
+
this.findComponentsInLayout(child, found);
|
|
808
|
+
} else if (child.type === "cell") {
|
|
809
|
+
if (child.children) {
|
|
810
|
+
child.children.forEach((cellChild) => {
|
|
811
|
+
if (cellChild.type === "component") {
|
|
812
|
+
found.push({
|
|
813
|
+
componentType: cellChild.componentType,
|
|
814
|
+
location: "cell"
|
|
815
|
+
});
|
|
816
|
+
} else if (cellChild.type === "layout") {
|
|
817
|
+
this.findComponentsInLayout(cellChild, found);
|
|
818
|
+
}
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
return found;
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Get all warnings generated during IR generation
|
|
828
|
+
*/
|
|
829
|
+
getWarnings() {
|
|
830
|
+
return this.warnings;
|
|
831
|
+
}
|
|
832
|
+
convertLayout(layout) {
|
|
833
|
+
const nodeId = this.idGen.generate("node");
|
|
834
|
+
const childRefs = [];
|
|
835
|
+
for (const child of layout.children) {
|
|
836
|
+
if (child.type === "layout") {
|
|
837
|
+
const childId = this.convertLayout(child);
|
|
838
|
+
childRefs.push({ ref: childId });
|
|
839
|
+
} else if (child.type === "component") {
|
|
840
|
+
const childId = this.convertComponent(child);
|
|
841
|
+
childRefs.push({ ref: childId });
|
|
842
|
+
} else if (child.type === "cell") {
|
|
843
|
+
const childId = this.convertCell(child);
|
|
844
|
+
childRefs.push({ ref: childId });
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
const style = {};
|
|
848
|
+
if (layout.params.padding) {
|
|
849
|
+
style.padding = String(layout.params.padding);
|
|
850
|
+
} else {
|
|
851
|
+
style.padding = "none";
|
|
852
|
+
}
|
|
853
|
+
if (layout.params.gap) {
|
|
854
|
+
style.gap = String(layout.params.gap);
|
|
855
|
+
}
|
|
856
|
+
if (layout.params.align) {
|
|
857
|
+
style.align = layout.params.align;
|
|
858
|
+
}
|
|
859
|
+
if (layout.params.background) {
|
|
860
|
+
style.background = String(layout.params.background);
|
|
861
|
+
}
|
|
862
|
+
const containerNode = {
|
|
863
|
+
id: nodeId,
|
|
864
|
+
kind: "container",
|
|
865
|
+
containerType: layout.layoutType,
|
|
866
|
+
params: this.cleanParams(layout.params),
|
|
867
|
+
children: childRefs,
|
|
868
|
+
style,
|
|
869
|
+
meta: {}
|
|
870
|
+
};
|
|
871
|
+
this.nodes[nodeId] = containerNode;
|
|
872
|
+
return nodeId;
|
|
873
|
+
}
|
|
874
|
+
convertCell(cell) {
|
|
875
|
+
const nodeId = this.idGen.generate("node");
|
|
876
|
+
const childRefs = [];
|
|
877
|
+
for (const child of cell.children) {
|
|
878
|
+
if (child.type === "layout") {
|
|
879
|
+
const childId = this.convertLayout(child);
|
|
880
|
+
childRefs.push({ ref: childId });
|
|
881
|
+
} else if (child.type === "component") {
|
|
882
|
+
const childId = this.convertComponent(child);
|
|
883
|
+
childRefs.push({ ref: childId });
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
const containerNode = {
|
|
887
|
+
id: nodeId,
|
|
888
|
+
kind: "container",
|
|
889
|
+
containerType: "stack",
|
|
890
|
+
// Cells contain content in a stack by default
|
|
891
|
+
params: cell.props,
|
|
892
|
+
children: childRefs,
|
|
893
|
+
style: { padding: "none" },
|
|
894
|
+
// Cells have no padding by default - grid gap handles spacing
|
|
895
|
+
meta: { source: "cell" }
|
|
896
|
+
};
|
|
897
|
+
this.nodes[nodeId] = containerNode;
|
|
898
|
+
return nodeId;
|
|
899
|
+
}
|
|
900
|
+
convertComponent(component) {
|
|
901
|
+
const definition = this.definedComponents.get(component.componentType);
|
|
902
|
+
if (definition) {
|
|
903
|
+
return this.expandDefinedComponent(definition);
|
|
904
|
+
}
|
|
905
|
+
const builtInComponents = /* @__PURE__ */ new Set([
|
|
906
|
+
"Button",
|
|
907
|
+
"Input",
|
|
908
|
+
"Heading",
|
|
909
|
+
"Text",
|
|
910
|
+
"Label",
|
|
911
|
+
"Image",
|
|
912
|
+
"Card",
|
|
913
|
+
"StatCard",
|
|
914
|
+
"Topbar",
|
|
915
|
+
"Table",
|
|
916
|
+
"Chart",
|
|
917
|
+
"ChartPlaceholder",
|
|
918
|
+
"Textarea",
|
|
919
|
+
"Select",
|
|
920
|
+
"Checkbox",
|
|
921
|
+
"Toggle",
|
|
922
|
+
"Divider",
|
|
923
|
+
"Breadcrumbs",
|
|
924
|
+
"SidebarMenu",
|
|
925
|
+
"Radio",
|
|
926
|
+
"Icon",
|
|
927
|
+
"IconButton",
|
|
928
|
+
"Alert",
|
|
929
|
+
"Badge",
|
|
930
|
+
"Modal",
|
|
931
|
+
"List",
|
|
932
|
+
"Sidebar",
|
|
933
|
+
"Tabs",
|
|
934
|
+
"Code",
|
|
935
|
+
"Paragraph"
|
|
936
|
+
]);
|
|
937
|
+
if (!builtInComponents.has(component.componentType)) {
|
|
938
|
+
this.undefinedComponentsUsed.add(component.componentType);
|
|
939
|
+
}
|
|
940
|
+
const nodeId = this.idGen.generate("node");
|
|
941
|
+
const componentNode = {
|
|
942
|
+
id: nodeId,
|
|
943
|
+
kind: "component",
|
|
944
|
+
componentType: component.componentType,
|
|
945
|
+
props: component.props,
|
|
946
|
+
style: {},
|
|
947
|
+
meta: {}
|
|
948
|
+
};
|
|
949
|
+
this.nodes[nodeId] = componentNode;
|
|
950
|
+
return nodeId;
|
|
951
|
+
}
|
|
952
|
+
expandDefinedComponent(definition) {
|
|
953
|
+
if (definition.body.type === "layout") {
|
|
954
|
+
return this.convertLayout(definition.body);
|
|
955
|
+
} else if (definition.body.type === "component") {
|
|
956
|
+
return this.convertComponent(definition.body);
|
|
957
|
+
} else {
|
|
958
|
+
throw new Error(`Invalid defined component body type for "${definition.name}"`);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
cleanParams(params) {
|
|
962
|
+
const cleaned = {};
|
|
963
|
+
for (const [key, value] of Object.entries(params)) {
|
|
964
|
+
if (!["padding", "gap", "align", "justify"].includes(key)) {
|
|
965
|
+
cleaned[key] = value;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
return cleaned;
|
|
969
|
+
}
|
|
970
|
+
sanitizeId(name) {
|
|
971
|
+
return name.toLowerCase().replace(/\s+/g, "_").replace(/[^a-z0-9_]/g, "");
|
|
972
|
+
}
|
|
973
|
+
};
|
|
974
|
+
function generateIR(ast) {
|
|
975
|
+
const generator = new IRGenerator();
|
|
976
|
+
return generator.generate(ast);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// src/layout/index.ts
|
|
980
|
+
var SPACING_VALUES = {
|
|
981
|
+
none: 0,
|
|
982
|
+
xs: 4,
|
|
983
|
+
sm: 8,
|
|
984
|
+
md: 16,
|
|
985
|
+
lg: 24,
|
|
986
|
+
xl: 32
|
|
987
|
+
};
|
|
988
|
+
var DENSITY_HEIGHTS = {
|
|
989
|
+
compact: 32,
|
|
990
|
+
normal: 40,
|
|
991
|
+
comfortable: 48
|
|
992
|
+
};
|
|
993
|
+
var LayoutEngine = class {
|
|
994
|
+
// Track parent container types
|
|
995
|
+
constructor(ir) {
|
|
996
|
+
this.result = {};
|
|
997
|
+
this.parentContainerTypes = /* @__PURE__ */ new Map();
|
|
998
|
+
this.ir = ir;
|
|
999
|
+
this.nodes = ir.project.nodes;
|
|
1000
|
+
this.theme = ir.project.theme;
|
|
1001
|
+
this.viewport = ir.project.screens[0]?.viewport || { width: 1280, height: 720 };
|
|
1002
|
+
}
|
|
1003
|
+
calculate() {
|
|
1004
|
+
this.result = {};
|
|
1005
|
+
for (const screen of this.ir.project.screens) {
|
|
1006
|
+
const rootId = screen.root.ref;
|
|
1007
|
+
if (rootId) {
|
|
1008
|
+
this.calculateNode(rootId, 0, 0, this.viewport.width, this.viewport.height);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
return this.result;
|
|
1012
|
+
}
|
|
1013
|
+
calculateNode(nodeId, x, y, width, height, parentContainerType) {
|
|
1014
|
+
const node = this.nodes[nodeId];
|
|
1015
|
+
if (!node) return;
|
|
1016
|
+
if (parentContainerType && node.kind === "container") {
|
|
1017
|
+
this.parentContainerTypes.set(nodeId, parentContainerType);
|
|
1018
|
+
}
|
|
1019
|
+
if (node.kind === "container") {
|
|
1020
|
+
this.calculateContainer(node, nodeId, x, y, width, height);
|
|
1021
|
+
} else {
|
|
1022
|
+
this.calculateComponent(node, nodeId, x, y, width, height);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
calculateContainer(node, nodeId, x, y, width, height) {
|
|
1026
|
+
if (node.kind !== "container") return;
|
|
1027
|
+
const padding = this.resolveSpacing(node.style.padding);
|
|
1028
|
+
const innerX = x + padding;
|
|
1029
|
+
const innerY = y + padding;
|
|
1030
|
+
const innerWidth = width - padding * 2;
|
|
1031
|
+
const direction = node.params.direction || "vertical";
|
|
1032
|
+
const isVerticalStack = node.containerType === "stack" && direction === "vertical";
|
|
1033
|
+
const innerHeight = isVerticalStack ? height : height - padding * 2;
|
|
1034
|
+
this.result[nodeId] = { x, y, width, height };
|
|
1035
|
+
switch (node.containerType) {
|
|
1036
|
+
case "stack":
|
|
1037
|
+
this.calculateStack(node, innerX, innerY, innerWidth, innerHeight);
|
|
1038
|
+
break;
|
|
1039
|
+
case "grid":
|
|
1040
|
+
this.calculateGrid(node, innerX, innerY, innerWidth, innerHeight);
|
|
1041
|
+
break;
|
|
1042
|
+
case "split":
|
|
1043
|
+
this.calculateSplit(node, innerX, innerY, innerWidth, innerHeight);
|
|
1044
|
+
break;
|
|
1045
|
+
case "panel":
|
|
1046
|
+
this.calculatePanel(node, innerX, innerY, innerWidth, innerHeight);
|
|
1047
|
+
break;
|
|
1048
|
+
case "card":
|
|
1049
|
+
this.calculateCard(node, innerX, innerY, innerWidth, innerHeight);
|
|
1050
|
+
break;
|
|
1051
|
+
}
|
|
1052
|
+
if (isVerticalStack || node.containerType === "card") {
|
|
1053
|
+
let containerMaxY = y;
|
|
1054
|
+
node.children.forEach((childRef) => {
|
|
1055
|
+
const childPos = this.result[childRef.ref];
|
|
1056
|
+
if (childPos) {
|
|
1057
|
+
containerMaxY = Math.max(containerMaxY, childPos.y + childPos.height);
|
|
1058
|
+
}
|
|
1059
|
+
});
|
|
1060
|
+
const cardPadding = node.containerType === "card" ? this.resolveSpacing(node.style.padding) : padding;
|
|
1061
|
+
const calculatedHeight = containerMaxY - y + cardPadding;
|
|
1062
|
+
this.result[nodeId].height = calculatedHeight;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
calculateStack(node, x, y, width, height) {
|
|
1066
|
+
if (node.kind !== "container") return;
|
|
1067
|
+
const direction = node.params.direction || "vertical";
|
|
1068
|
+
const gap = this.resolveSpacing(node.style.gap);
|
|
1069
|
+
const children = node.children;
|
|
1070
|
+
if (direction === "vertical") {
|
|
1071
|
+
let currentY = y;
|
|
1072
|
+
children.forEach((childRef, index) => {
|
|
1073
|
+
const childNode = this.nodes[childRef.ref];
|
|
1074
|
+
let childHeight = this.getComponentHeight();
|
|
1075
|
+
if (childNode?.kind === "component" && childNode.props.height) {
|
|
1076
|
+
childHeight = Number(childNode.props.height);
|
|
1077
|
+
} else if (childNode?.kind === "container") {
|
|
1078
|
+
childHeight = this.calculateContainerHeight(childNode, width);
|
|
1079
|
+
} else if (childNode?.kind === "component") {
|
|
1080
|
+
childHeight = this.getIntrinsicComponentHeight(childNode, width);
|
|
1081
|
+
}
|
|
1082
|
+
this.calculateNode(childRef.ref, x, currentY, width, childHeight, "stack");
|
|
1083
|
+
currentY += childHeight;
|
|
1084
|
+
if (index < children.length - 1) {
|
|
1085
|
+
currentY += gap;
|
|
1086
|
+
}
|
|
1087
|
+
});
|
|
1088
|
+
let adjustedY = y;
|
|
1089
|
+
children.forEach((childRef, index) => {
|
|
1090
|
+
const childPos = this.result[childRef.ref];
|
|
1091
|
+
if (childPos) {
|
|
1092
|
+
const deltaY = adjustedY - childPos.y;
|
|
1093
|
+
childPos.y = adjustedY;
|
|
1094
|
+
if (deltaY !== 0) {
|
|
1095
|
+
this.adjustNodeYPositions(childRef.ref, deltaY);
|
|
1096
|
+
}
|
|
1097
|
+
adjustedY += childPos.height;
|
|
1098
|
+
if (index < children.length - 1) {
|
|
1099
|
+
adjustedY += gap;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
});
|
|
1103
|
+
} else {
|
|
1104
|
+
const align = node.style.align || "justify";
|
|
1105
|
+
if (align === "justify") {
|
|
1106
|
+
let currentX = x;
|
|
1107
|
+
const childWidth = this.calculateChildWidth(children.length, width, gap);
|
|
1108
|
+
let stackHeight = 0;
|
|
1109
|
+
children.forEach((childRef) => {
|
|
1110
|
+
const childNode = this.nodes[childRef.ref];
|
|
1111
|
+
let childHeight = this.getComponentHeight();
|
|
1112
|
+
if (childNode?.kind === "component" && childNode.props.height) {
|
|
1113
|
+
childHeight = Number(childNode.props.height);
|
|
1114
|
+
} else if (childNode?.kind === "container") {
|
|
1115
|
+
childHeight = this.calculateContainerHeight(childNode, childWidth);
|
|
1116
|
+
} else if (childNode?.kind === "component") {
|
|
1117
|
+
childHeight = this.getIntrinsicComponentHeight(childNode);
|
|
1118
|
+
}
|
|
1119
|
+
stackHeight = Math.max(stackHeight, childHeight);
|
|
1120
|
+
});
|
|
1121
|
+
children.forEach((childRef) => {
|
|
1122
|
+
this.calculateNode(childRef.ref, currentX, y, childWidth, stackHeight, "stack");
|
|
1123
|
+
currentX += childWidth + gap;
|
|
1124
|
+
});
|
|
1125
|
+
} else {
|
|
1126
|
+
const childWidths = [];
|
|
1127
|
+
let stackHeight = 0;
|
|
1128
|
+
children.forEach((childRef) => {
|
|
1129
|
+
const childNode = this.nodes[childRef.ref];
|
|
1130
|
+
let childWidth = this.getIntrinsicComponentWidth(childNode);
|
|
1131
|
+
let childHeight = this.getComponentHeight();
|
|
1132
|
+
if (childNode?.kind === "component" && childNode.props.height) {
|
|
1133
|
+
childHeight = Number(childNode.props.height);
|
|
1134
|
+
} else if (childNode?.kind === "component" && childNode.props.width) {
|
|
1135
|
+
childWidth = Number(childNode.props.width);
|
|
1136
|
+
} else if (childNode?.kind === "container") {
|
|
1137
|
+
childHeight = this.calculateContainerHeight(childNode, childWidth);
|
|
1138
|
+
} else if (childNode?.kind === "component") {
|
|
1139
|
+
childHeight = this.getIntrinsicComponentHeight(childNode);
|
|
1140
|
+
}
|
|
1141
|
+
childWidths.push(childWidth);
|
|
1142
|
+
stackHeight = Math.max(stackHeight, childHeight);
|
|
1143
|
+
});
|
|
1144
|
+
const totalChildWidth = childWidths.reduce((sum, w) => sum + w, 0);
|
|
1145
|
+
const totalGapWidth = gap * Math.max(0, children.length - 1);
|
|
1146
|
+
const totalContentWidth = totalChildWidth + totalGapWidth;
|
|
1147
|
+
let startX = x;
|
|
1148
|
+
if (align === "center") {
|
|
1149
|
+
startX = x + (width - totalContentWidth) / 2;
|
|
1150
|
+
} else if (align === "right") {
|
|
1151
|
+
startX = x + width - totalContentWidth;
|
|
1152
|
+
}
|
|
1153
|
+
let currentX = startX;
|
|
1154
|
+
children.forEach((childRef, index) => {
|
|
1155
|
+
const childWidth = childWidths[index];
|
|
1156
|
+
this.calculateNode(childRef.ref, currentX, y, childWidth, stackHeight, "stack");
|
|
1157
|
+
currentX += childWidth + gap;
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
calculateContainerHeight(node, availableWidth) {
|
|
1163
|
+
if (node.kind !== "container") return this.getComponentHeight();
|
|
1164
|
+
const gap = this.resolveSpacing(node.style.gap);
|
|
1165
|
+
const padding = this.resolveSpacing(node.style.padding);
|
|
1166
|
+
let totalHeight = padding * 2;
|
|
1167
|
+
if (node.containerType === "grid") {
|
|
1168
|
+
const columns = Number(node.params.columns) || 12;
|
|
1169
|
+
const colWidth = availableWidth / columns;
|
|
1170
|
+
let currentRow = 0;
|
|
1171
|
+
let currentCol = 0;
|
|
1172
|
+
let currentRowMaxHeight = 0;
|
|
1173
|
+
const rowHeights = [0];
|
|
1174
|
+
node.children.forEach((childRef) => {
|
|
1175
|
+
const child = this.nodes[childRef.ref];
|
|
1176
|
+
let span = 1;
|
|
1177
|
+
let childHeight = this.getComponentHeight();
|
|
1178
|
+
if (child?.kind === "container" && child.meta?.source === "cell") {
|
|
1179
|
+
span = Number(child.params.span) || 1;
|
|
1180
|
+
}
|
|
1181
|
+
if (child?.kind === "component") {
|
|
1182
|
+
if (child.props.height) {
|
|
1183
|
+
childHeight = Number(child.props.height);
|
|
1184
|
+
} else {
|
|
1185
|
+
childHeight = this.getIntrinsicComponentHeight(child);
|
|
1186
|
+
}
|
|
1187
|
+
} else if (child?.kind === "container") {
|
|
1188
|
+
childHeight = this.calculateContainerHeight(child, colWidth * span);
|
|
1189
|
+
}
|
|
1190
|
+
if (currentCol + span > columns) {
|
|
1191
|
+
rowHeights[currentRow] = currentRowMaxHeight;
|
|
1192
|
+
currentRow++;
|
|
1193
|
+
currentCol = 0;
|
|
1194
|
+
currentRowMaxHeight = 0;
|
|
1195
|
+
}
|
|
1196
|
+
currentRowMaxHeight = Math.max(currentRowMaxHeight, childHeight);
|
|
1197
|
+
currentCol += span;
|
|
1198
|
+
});
|
|
1199
|
+
rowHeights[currentRow] = currentRowMaxHeight;
|
|
1200
|
+
for (let r = 0; r <= currentRow; r++) {
|
|
1201
|
+
totalHeight += rowHeights[r];
|
|
1202
|
+
if (r < currentRow) {
|
|
1203
|
+
totalHeight += gap;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
return totalHeight;
|
|
1207
|
+
}
|
|
1208
|
+
const direction = node.params.direction || "vertical";
|
|
1209
|
+
if (node.containerType === "stack" && direction === "horizontal") {
|
|
1210
|
+
let maxHeight = 0;
|
|
1211
|
+
node.children.forEach((childRef) => {
|
|
1212
|
+
const child = this.nodes[childRef.ref];
|
|
1213
|
+
let childHeight = this.getComponentHeight();
|
|
1214
|
+
if (child?.kind === "component") {
|
|
1215
|
+
if (child.props.height) {
|
|
1216
|
+
childHeight = Number(child.props.height);
|
|
1217
|
+
} else {
|
|
1218
|
+
childHeight = this.getIntrinsicComponentHeight(child);
|
|
1219
|
+
}
|
|
1220
|
+
} else if (child?.kind === "container") {
|
|
1221
|
+
childHeight = this.calculateContainerHeight(child, availableWidth);
|
|
1222
|
+
}
|
|
1223
|
+
maxHeight = Math.max(maxHeight, childHeight);
|
|
1224
|
+
});
|
|
1225
|
+
totalHeight += maxHeight;
|
|
1226
|
+
return totalHeight;
|
|
1227
|
+
}
|
|
1228
|
+
node.children.forEach((childRef, index) => {
|
|
1229
|
+
const child = this.nodes[childRef.ref];
|
|
1230
|
+
let childHeight = this.getComponentHeight();
|
|
1231
|
+
if (child?.kind === "component") {
|
|
1232
|
+
if (child.props.height) {
|
|
1233
|
+
childHeight = Number(child.props.height);
|
|
1234
|
+
} else {
|
|
1235
|
+
childHeight = this.getIntrinsicComponentHeight(child);
|
|
1236
|
+
}
|
|
1237
|
+
} else if (child?.kind === "container") {
|
|
1238
|
+
childHeight = this.calculateContainerHeight(child, availableWidth);
|
|
1239
|
+
}
|
|
1240
|
+
totalHeight += childHeight;
|
|
1241
|
+
if (index < node.children.length - 1) {
|
|
1242
|
+
totalHeight += gap;
|
|
1243
|
+
}
|
|
1244
|
+
});
|
|
1245
|
+
return totalHeight;
|
|
1246
|
+
}
|
|
1247
|
+
calculateGrid(node, x, y, width, height) {
|
|
1248
|
+
if (node.kind !== "container") return;
|
|
1249
|
+
const columns = Number(node.params.columns) || 12;
|
|
1250
|
+
const gap = this.resolveSpacing(node.style.gap);
|
|
1251
|
+
const colWidth = (width - gap * (columns - 1)) / columns;
|
|
1252
|
+
const cellHeights = {};
|
|
1253
|
+
node.children.forEach((childRef, cellIndex) => {
|
|
1254
|
+
const child = this.nodes[childRef.ref];
|
|
1255
|
+
let cellHeight = this.getComponentHeight();
|
|
1256
|
+
if (child?.kind === "container") {
|
|
1257
|
+
cellHeight = this.calculateContainerHeight(child, colWidth);
|
|
1258
|
+
} else if (child?.kind === "component") {
|
|
1259
|
+
if (child.props.height) {
|
|
1260
|
+
cellHeight = Number(child.props.height);
|
|
1261
|
+
} else {
|
|
1262
|
+
cellHeight = this.getIntrinsicComponentHeight(child);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
cellHeights[cellIndex] = cellHeight;
|
|
1266
|
+
});
|
|
1267
|
+
let currentRow = 0;
|
|
1268
|
+
let currentCol = 0;
|
|
1269
|
+
let currentRowMaxHeight = 0;
|
|
1270
|
+
const rowHeights = [0];
|
|
1271
|
+
const cellPositions = [];
|
|
1272
|
+
node.children.forEach((childRef, cellIndex) => {
|
|
1273
|
+
const child = this.nodes[childRef.ref];
|
|
1274
|
+
let span = 1;
|
|
1275
|
+
if (child?.kind === "container" && child.meta?.source === "cell") {
|
|
1276
|
+
span = Number(child.params.span) || 1;
|
|
1277
|
+
}
|
|
1278
|
+
if (currentCol + span > columns) {
|
|
1279
|
+
rowHeights[currentRow] = currentRowMaxHeight;
|
|
1280
|
+
currentRow++;
|
|
1281
|
+
currentCol = 0;
|
|
1282
|
+
currentRowMaxHeight = 0;
|
|
1283
|
+
}
|
|
1284
|
+
cellPositions.push({ row: currentRow, col: currentCol, span });
|
|
1285
|
+
currentRowMaxHeight = Math.max(currentRowMaxHeight, cellHeights[cellIndex]);
|
|
1286
|
+
currentCol += span;
|
|
1287
|
+
});
|
|
1288
|
+
rowHeights[currentRow] = currentRowMaxHeight;
|
|
1289
|
+
node.children.forEach((childRef, cellIndex) => {
|
|
1290
|
+
const { row, col, span } = cellPositions[cellIndex];
|
|
1291
|
+
const cellHeight = rowHeights[row];
|
|
1292
|
+
let cellY = y;
|
|
1293
|
+
for (let r = 0; r < row; r++) {
|
|
1294
|
+
cellY += rowHeights[r] + gap;
|
|
1295
|
+
}
|
|
1296
|
+
const cellWidth = colWidth * span + gap * (span - 1);
|
|
1297
|
+
const cellX = x + col * (colWidth + gap);
|
|
1298
|
+
this.calculateNode(childRef.ref, cellX, cellY, cellWidth, cellHeight, "grid");
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
calculateSplit(node, x, y, width, height) {
|
|
1302
|
+
if (node.kind !== "container") return;
|
|
1303
|
+
const gap = this.resolveSpacing(node.style.gap);
|
|
1304
|
+
const sidebarWidth = Number(node.params.sidebar) || 260;
|
|
1305
|
+
if (node.children.length === 1) {
|
|
1306
|
+
this.calculateNode(node.children[0].ref, x, y, width, height, "split");
|
|
1307
|
+
} else if (node.children.length >= 2) {
|
|
1308
|
+
this.calculateNode(node.children[0].ref, x, y, sidebarWidth, height, "split");
|
|
1309
|
+
const contentX = x + sidebarWidth + gap;
|
|
1310
|
+
const contentWidth = width - sidebarWidth - gap;
|
|
1311
|
+
this.calculateNode(node.children[1].ref, contentX, y, contentWidth, height, "split");
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
calculatePanel(node, x, y, width, height) {
|
|
1315
|
+
if (node.kind !== "container" || node.children.length === 0) return;
|
|
1316
|
+
const childRef = node.children[0];
|
|
1317
|
+
this.calculateNode(childRef.ref, x, y, width, height, "panel");
|
|
1318
|
+
}
|
|
1319
|
+
calculateCard(node, x, y, width, height) {
|
|
1320
|
+
if (node.kind !== "container" || node.children.length === 0) return;
|
|
1321
|
+
const cardPadding = this.resolveSpacing(node.style.padding);
|
|
1322
|
+
const gap = this.resolveSpacing(node.style.gap);
|
|
1323
|
+
const innerCardWidth = width - cardPadding * 2;
|
|
1324
|
+
const children = node.children;
|
|
1325
|
+
let currentY = y + cardPadding;
|
|
1326
|
+
children.forEach((childRef, index) => {
|
|
1327
|
+
const childNode = this.nodes[childRef.ref];
|
|
1328
|
+
let childHeight = this.getComponentHeight();
|
|
1329
|
+
if (childNode?.kind === "component" && childNode.props.height) {
|
|
1330
|
+
childHeight = Number(childNode.props.height);
|
|
1331
|
+
} else if (childNode?.kind === "container") {
|
|
1332
|
+
childHeight = this.calculateContainerHeight(childNode, innerCardWidth);
|
|
1333
|
+
} else if (childNode?.kind === "component") {
|
|
1334
|
+
childHeight = this.getIntrinsicComponentHeight(childNode, innerCardWidth);
|
|
1335
|
+
}
|
|
1336
|
+
this.calculateNode(childRef.ref, x + cardPadding, currentY, innerCardWidth, childHeight, "card");
|
|
1337
|
+
currentY += childHeight;
|
|
1338
|
+
if (index < children.length - 1) {
|
|
1339
|
+
currentY += gap;
|
|
1340
|
+
}
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
calculateComponent(node, nodeId, x, y, width, height) {
|
|
1344
|
+
if (node.kind !== "component") return;
|
|
1345
|
+
const componentWidth = Number(node.props.width) || width;
|
|
1346
|
+
const componentHeight = Number(node.props.height) || this.getIntrinsicComponentHeight(node, componentWidth);
|
|
1347
|
+
this.result[nodeId] = {
|
|
1348
|
+
x,
|
|
1349
|
+
y,
|
|
1350
|
+
width: componentWidth,
|
|
1351
|
+
height: componentHeight
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
resolveSpacing(spacing) {
|
|
1355
|
+
if (!spacing) return SPACING_VALUES[this.theme.spacing];
|
|
1356
|
+
const value = SPACING_VALUES[spacing];
|
|
1357
|
+
return value !== void 0 ? value : SPACING_VALUES.md;
|
|
1358
|
+
}
|
|
1359
|
+
getComponentHeight() {
|
|
1360
|
+
return DENSITY_HEIGHTS[this.theme.density] || DENSITY_HEIGHTS.normal;
|
|
1361
|
+
}
|
|
1362
|
+
getIntrinsicComponentHeight(node, availableWidth) {
|
|
1363
|
+
if (node.kind !== "component") return this.getComponentHeight();
|
|
1364
|
+
if (node.componentType === "Image") {
|
|
1365
|
+
const placeholder = String(node.props.placeholder || "landscape");
|
|
1366
|
+
const aspectRatios = {
|
|
1367
|
+
landscape: 16 / 9,
|
|
1368
|
+
portrait: 2 / 3,
|
|
1369
|
+
square: 1,
|
|
1370
|
+
icon: 1,
|
|
1371
|
+
avatar: 1
|
|
1372
|
+
};
|
|
1373
|
+
const ratio = aspectRatios[placeholder] || 16 / 9;
|
|
1374
|
+
const explicitHeight = Number(node.props.height);
|
|
1375
|
+
if (!isNaN(explicitHeight) && explicitHeight > 0) {
|
|
1376
|
+
return explicitHeight;
|
|
1377
|
+
}
|
|
1378
|
+
if (availableWidth && availableWidth > 0) {
|
|
1379
|
+
return availableWidth / ratio;
|
|
1380
|
+
}
|
|
1381
|
+
return 200;
|
|
1382
|
+
}
|
|
1383
|
+
if (node.componentType === "Table") {
|
|
1384
|
+
const explicitHeight = Number(node.props.height);
|
|
1385
|
+
if (!isNaN(explicitHeight) && explicitHeight > 0) {
|
|
1386
|
+
return explicitHeight;
|
|
1387
|
+
}
|
|
1388
|
+
const rowCount = Number(node.props.rows || 5);
|
|
1389
|
+
const hasTitle = !!node.props.title;
|
|
1390
|
+
const hasPagination = String(node.props.pagination) === "true";
|
|
1391
|
+
const headerHeight = 44;
|
|
1392
|
+
const rowHeight = 36;
|
|
1393
|
+
const titleHeight = hasTitle ? 32 : 0;
|
|
1394
|
+
const paginationHeight = hasPagination ? 64 : 0;
|
|
1395
|
+
return titleHeight + headerHeight + rowCount * rowHeight + paginationHeight;
|
|
1396
|
+
}
|
|
1397
|
+
if (node.componentType === "Textarea") return 100;
|
|
1398
|
+
if (node.componentType === "Modal") return 300;
|
|
1399
|
+
if (node.componentType === "Card") return 120;
|
|
1400
|
+
if (node.componentType === "StatCard") return 120;
|
|
1401
|
+
if (node.componentType === "ChartPlaceholder") return 250;
|
|
1402
|
+
if (node.componentType === "List") return 180;
|
|
1403
|
+
if (node.componentType === "Topbar") return 56;
|
|
1404
|
+
if (node.componentType === "Divider") return 1;
|
|
1405
|
+
return this.getComponentHeight();
|
|
1406
|
+
}
|
|
1407
|
+
getIntrinsicComponentWidth(node) {
|
|
1408
|
+
if (!node || node.kind !== "component") {
|
|
1409
|
+
return 120;
|
|
1410
|
+
}
|
|
1411
|
+
if (node.componentType === "Icon") {
|
|
1412
|
+
const size = String(node.props.size || "md");
|
|
1413
|
+
const sizes = {
|
|
1414
|
+
sm: 16,
|
|
1415
|
+
md: 24,
|
|
1416
|
+
lg: 32,
|
|
1417
|
+
xl: 40
|
|
1418
|
+
};
|
|
1419
|
+
return sizes[size] || 24;
|
|
1420
|
+
}
|
|
1421
|
+
if (node.componentType === "IconButton") {
|
|
1422
|
+
return 40;
|
|
1423
|
+
}
|
|
1424
|
+
if (node.componentType === "Checkbox" || node.componentType === "Radio") {
|
|
1425
|
+
return 24;
|
|
1426
|
+
}
|
|
1427
|
+
if (node.componentType === "Button") {
|
|
1428
|
+
const text = String(node.props.text || "");
|
|
1429
|
+
return Math.max(80, text.length * 8 + 32);
|
|
1430
|
+
}
|
|
1431
|
+
if (node.componentType === "Label" || node.componentType === "Text") {
|
|
1432
|
+
const text = String(node.props.content || node.props.text || "");
|
|
1433
|
+
return Math.max(60, text.length * 8 + 16);
|
|
1434
|
+
}
|
|
1435
|
+
if (node.componentType === "Heading") {
|
|
1436
|
+
const text = String(node.props.text || "");
|
|
1437
|
+
return Math.max(80, text.length * 12 + 16);
|
|
1438
|
+
}
|
|
1439
|
+
if (node.componentType === "Input" || node.componentType === "Select") {
|
|
1440
|
+
return 200;
|
|
1441
|
+
}
|
|
1442
|
+
if (node.componentType === "Textarea") {
|
|
1443
|
+
return 200;
|
|
1444
|
+
}
|
|
1445
|
+
if (node.componentType === "Image") {
|
|
1446
|
+
const placeholder = String(node.props.placeholder || "landscape");
|
|
1447
|
+
const widths = {
|
|
1448
|
+
landscape: 300,
|
|
1449
|
+
portrait: 200,
|
|
1450
|
+
square: 200,
|
|
1451
|
+
icon: 64,
|
|
1452
|
+
avatar: 64
|
|
1453
|
+
};
|
|
1454
|
+
return widths[placeholder] || 300;
|
|
1455
|
+
}
|
|
1456
|
+
if (node.componentType === "Table") {
|
|
1457
|
+
return 400;
|
|
1458
|
+
}
|
|
1459
|
+
if (node.componentType === "StatCard" || node.componentType === "Card") {
|
|
1460
|
+
return 280;
|
|
1461
|
+
}
|
|
1462
|
+
if (node.componentType === "Avatar") {
|
|
1463
|
+
const size = String(node.props.size || "md");
|
|
1464
|
+
const sizes = {
|
|
1465
|
+
sm: 32,
|
|
1466
|
+
md: 40,
|
|
1467
|
+
lg: 56,
|
|
1468
|
+
xl: 72
|
|
1469
|
+
};
|
|
1470
|
+
return sizes[size] || 40;
|
|
1471
|
+
}
|
|
1472
|
+
if (node.componentType === "SidebarMenu") {
|
|
1473
|
+
return 260;
|
|
1474
|
+
}
|
|
1475
|
+
if (node.componentType === "Badge" || node.componentType === "Chip") {
|
|
1476
|
+
const text = String(node.props.text || "");
|
|
1477
|
+
return Math.max(50, text.length * 7 + 16);
|
|
1478
|
+
}
|
|
1479
|
+
return 120;
|
|
1480
|
+
}
|
|
1481
|
+
calculateChildHeight(count, totalHeight, gap) {
|
|
1482
|
+
if (count === 0) return 0;
|
|
1483
|
+
const totalGap = gap * (count - 1);
|
|
1484
|
+
return (totalHeight - totalGap) / count;
|
|
1485
|
+
}
|
|
1486
|
+
calculateChildWidth(count, totalWidth, gap) {
|
|
1487
|
+
if (count === 0) return 0;
|
|
1488
|
+
const totalGap = gap * (count - 1);
|
|
1489
|
+
return (totalWidth - totalGap) / count;
|
|
1490
|
+
}
|
|
1491
|
+
adjustNodeYPositions(nodeId, deltaY) {
|
|
1492
|
+
const node = this.nodes[nodeId];
|
|
1493
|
+
if (!node) return;
|
|
1494
|
+
if (node.kind === "container" && node.children) {
|
|
1495
|
+
node.children.forEach((childRef) => {
|
|
1496
|
+
const childPos = this.result[childRef.ref];
|
|
1497
|
+
if (childPos) {
|
|
1498
|
+
childPos.y += deltaY;
|
|
1499
|
+
this.adjustNodeYPositions(childRef.ref, deltaY);
|
|
1500
|
+
}
|
|
1501
|
+
});
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
};
|
|
1505
|
+
function calculateLayout(ir) {
|
|
1506
|
+
const engine = new LayoutEngine(ir);
|
|
1507
|
+
return engine.calculate();
|
|
1508
|
+
}
|
|
1509
|
+
function resolveGridPosition(row, col, rowSpan = 1, colSpan = 1, gridWidth = 1200, gridHeight = 800, gridCols = 12, gridRows = 8) {
|
|
1510
|
+
const colWidth = gridWidth / gridCols;
|
|
1511
|
+
const rowHeight = gridHeight / gridRows;
|
|
1512
|
+
return {
|
|
1513
|
+
x: col * colWidth,
|
|
1514
|
+
y: row * rowHeight,
|
|
1515
|
+
width: colSpan * colWidth,
|
|
1516
|
+
height: rowSpan * rowHeight
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
// src/renderer/mock-data.ts
|
|
1521
|
+
var MOCK_NAMES = ["Juan", "Mar\xEDa", "Carlos", "Sofia", "Alex", "Luna", "Diego", "Emma"];
|
|
1522
|
+
var MOCK_LAST_NAMES = [
|
|
1523
|
+
"Garc\xEDa",
|
|
1524
|
+
"L\xF3pez",
|
|
1525
|
+
"Rodr\xEDguez",
|
|
1526
|
+
"Mart\xEDnez",
|
|
1527
|
+
"P\xE9rez",
|
|
1528
|
+
"S\xE1nchez",
|
|
1529
|
+
"Torres",
|
|
1530
|
+
"Flores"
|
|
1531
|
+
];
|
|
1532
|
+
var MOCK_STATUSES = ["Active", "Pending", "Completed", "On Hold", "Review", "Approved"];
|
|
1533
|
+
var MOCK_STAGES = ["Lead", "Qualified", "Proposal", "Negotiation", "Won", "Lost"];
|
|
1534
|
+
var MOCK_EMAILS = [
|
|
1535
|
+
"juan@example.com",
|
|
1536
|
+
"maria@example.com",
|
|
1537
|
+
"carlos@example.com",
|
|
1538
|
+
"sofia@example.com",
|
|
1539
|
+
"alex@example.com",
|
|
1540
|
+
"luna@example.com"
|
|
1541
|
+
];
|
|
1542
|
+
var MockDataGenerator = class {
|
|
1543
|
+
/**
|
|
1544
|
+
* Set custom mocks from project metadata
|
|
1545
|
+
* Accepts string (comma-separated) or string[] values; ignores others.
|
|
1546
|
+
* Format: { "status": "Active,Pending,Completed", "stage": ["Lead","Won","Lost"] }
|
|
1547
|
+
*/
|
|
1548
|
+
static setCustomMocks(mocks) {
|
|
1549
|
+
this.customMocks = {};
|
|
1550
|
+
for (const [key, rawValues] of Object.entries(mocks)) {
|
|
1551
|
+
let values = [];
|
|
1552
|
+
if (typeof rawValues === "string") {
|
|
1553
|
+
values = rawValues.split(",").map((v) => v.trim()).filter((v) => v.length > 0);
|
|
1554
|
+
} else if (Array.isArray(rawValues)) {
|
|
1555
|
+
values = rawValues.map((v) => typeof v === "string" ? v.trim() : "").filter((v) => v.length > 0);
|
|
1556
|
+
}
|
|
1557
|
+
if (values.length > 0) {
|
|
1558
|
+
this.customMocks[key.toLowerCase()] = values;
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
/**
|
|
1563
|
+
* Get deterministic mock data by type and index
|
|
1564
|
+
*/
|
|
1565
|
+
static getMockValue(type, index) {
|
|
1566
|
+
const normalizedType = type.toLowerCase().trim();
|
|
1567
|
+
if (this.customMocks[normalizedType]) {
|
|
1568
|
+
return this.customMocks[normalizedType][index % this.customMocks[normalizedType].length];
|
|
1569
|
+
}
|
|
1570
|
+
switch (normalizedType) {
|
|
1571
|
+
case "id":
|
|
1572
|
+
return (index + 1).toString();
|
|
1573
|
+
case "name":
|
|
1574
|
+
case "owner":
|
|
1575
|
+
case "contact":
|
|
1576
|
+
case "requester":
|
|
1577
|
+
case "user":
|
|
1578
|
+
return MOCK_NAMES[index % MOCK_NAMES.length];
|
|
1579
|
+
case "last_name":
|
|
1580
|
+
return MOCK_LAST_NAMES[index % MOCK_LAST_NAMES.length];
|
|
1581
|
+
case "full_name":
|
|
1582
|
+
case "fullname":
|
|
1583
|
+
return `${MOCK_NAMES[index % MOCK_NAMES.length]} ${MOCK_LAST_NAMES[index % MOCK_LAST_NAMES.length]}`;
|
|
1584
|
+
case "email":
|
|
1585
|
+
return MOCK_EMAILS[index % MOCK_EMAILS.length];
|
|
1586
|
+
case "status":
|
|
1587
|
+
return MOCK_STATUSES[index % MOCK_STATUSES.length];
|
|
1588
|
+
case "stage":
|
|
1589
|
+
return MOCK_STAGES[index % MOCK_STAGES.length];
|
|
1590
|
+
case "price":
|
|
1591
|
+
case "cost":
|
|
1592
|
+
return `$${((index + 1) * 99.99).toFixed(2)}`;
|
|
1593
|
+
case "amount":
|
|
1594
|
+
return `$${((index + 1) * 5e3).toLocaleString()}`;
|
|
1595
|
+
case "account":
|
|
1596
|
+
case "company":
|
|
1597
|
+
return `Company ${index + 1}`;
|
|
1598
|
+
case "country":
|
|
1599
|
+
case "region":
|
|
1600
|
+
return ["USA", "EU", "APAC", "LATAM"][index % 4];
|
|
1601
|
+
case "device":
|
|
1602
|
+
return ["Desktop", "Mobile", "Tablet"][index % 3];
|
|
1603
|
+
case "time":
|
|
1604
|
+
case "updated":
|
|
1605
|
+
return `${2 + index % 20}h ago`;
|
|
1606
|
+
case "priority":
|
|
1607
|
+
return ["High", "Medium", "Low"][index % 3];
|
|
1608
|
+
default:
|
|
1609
|
+
if (normalizedType.startsWith("literal:")) {
|
|
1610
|
+
return type.substring(8);
|
|
1611
|
+
}
|
|
1612
|
+
return `Item ${index + 1}`;
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
/**
|
|
1616
|
+
* Parse mock string and return list of values
|
|
1617
|
+
* Formats:
|
|
1618
|
+
* "name" → ["Juan", "María", "Carlos", ...]
|
|
1619
|
+
* "id" → ["1", "2", "3", ...]
|
|
1620
|
+
* "literal:No data" → ["No data", "No data", ...]
|
|
1621
|
+
*/
|
|
1622
|
+
static generateMockList(mockType, count) {
|
|
1623
|
+
const result = [];
|
|
1624
|
+
for (let i = 0; i < count; i++) {
|
|
1625
|
+
result.push(this.getMockValue(mockType, i));
|
|
1626
|
+
}
|
|
1627
|
+
return result;
|
|
1628
|
+
}
|
|
1629
|
+
/**
|
|
1630
|
+
* Generate a row of mock data for a table given column types
|
|
1631
|
+
* columns: "id,name,status,amount"
|
|
1632
|
+
*/
|
|
1633
|
+
static generateMockRow(columns, rowIndex) {
|
|
1634
|
+
const columnNames = columns.split(",").map((c) => c.trim());
|
|
1635
|
+
const row = {};
|
|
1636
|
+
columnNames.forEach((col) => {
|
|
1637
|
+
row[col] = this.getMockValue(col, rowIndex);
|
|
1638
|
+
});
|
|
1639
|
+
return row;
|
|
1640
|
+
}
|
|
1641
|
+
/**
|
|
1642
|
+
* Generate multiple mock rows for a table
|
|
1643
|
+
*/
|
|
1644
|
+
static generateMockRows(columns, rowCount) {
|
|
1645
|
+
const rows = [];
|
|
1646
|
+
for (let i = 0; i < rowCount; i++) {
|
|
1647
|
+
rows.push(this.generateMockRow(columns, i));
|
|
1648
|
+
}
|
|
1649
|
+
return rows;
|
|
1650
|
+
}
|
|
1651
|
+
};
|
|
1652
|
+
MockDataGenerator.customMocks = {};
|
|
1653
|
+
|
|
1654
|
+
// src/renderer/colors.ts
|
|
1655
|
+
var ColorResolver = class {
|
|
1656
|
+
constructor() {
|
|
1657
|
+
this.customColors = {};
|
|
1658
|
+
// Named colors palette
|
|
1659
|
+
this.namedColors = {
|
|
1660
|
+
// Grayscale
|
|
1661
|
+
white: "#FFFFFF",
|
|
1662
|
+
black: "#000000",
|
|
1663
|
+
gray: "#6B7280",
|
|
1664
|
+
slate: "#64748B",
|
|
1665
|
+
zinc: "#71717A",
|
|
1666
|
+
// Colors
|
|
1667
|
+
red: "#EF4444",
|
|
1668
|
+
orange: "#F97316",
|
|
1669
|
+
yellow: "#EAB308",
|
|
1670
|
+
lime: "#84CC16",
|
|
1671
|
+
green: "#22C55E",
|
|
1672
|
+
emerald: "#10B981",
|
|
1673
|
+
teal: "#14B8A6",
|
|
1674
|
+
cyan: "#06B6D4",
|
|
1675
|
+
blue: "#3B82F6",
|
|
1676
|
+
indigo: "#6366F1",
|
|
1677
|
+
violet: "#8B5CF6",
|
|
1678
|
+
purple: "#A855F7",
|
|
1679
|
+
fuchsia: "#D946EF",
|
|
1680
|
+
pink: "#EC4899",
|
|
1681
|
+
rose: "#F43F5E",
|
|
1682
|
+
// Light variants
|
|
1683
|
+
"red-light": "#FEE2E2",
|
|
1684
|
+
"blue-light": "#DBEAFE",
|
|
1685
|
+
"green-light": "#DCFCE7"
|
|
1686
|
+
};
|
|
1687
|
+
}
|
|
1688
|
+
/**
|
|
1689
|
+
* Set custom colors from project definition
|
|
1690
|
+
*/
|
|
1691
|
+
setCustomColors(colors) {
|
|
1692
|
+
this.customColors = colors || {};
|
|
1693
|
+
}
|
|
1694
|
+
/**
|
|
1695
|
+
* Resolve a color reference to a hex value
|
|
1696
|
+
* Priority: custom colors > named colors > hex validation > default
|
|
1697
|
+
*/
|
|
1698
|
+
resolveColor(colorRef, defaultColor = "#000000") {
|
|
1699
|
+
if (!colorRef) return defaultColor;
|
|
1700
|
+
if (this.customColors[colorRef]) {
|
|
1701
|
+
return this.validateHex(this.customColors[colorRef]) || defaultColor;
|
|
1702
|
+
}
|
|
1703
|
+
if (this.namedColors[colorRef]) {
|
|
1704
|
+
return this.namedColors[colorRef];
|
|
1705
|
+
}
|
|
1706
|
+
if (this.isValidHex(colorRef)) {
|
|
1707
|
+
return colorRef;
|
|
1708
|
+
}
|
|
1709
|
+
return defaultColor;
|
|
1710
|
+
}
|
|
1711
|
+
/**
|
|
1712
|
+
* Validate hex color (6-character hex code)
|
|
1713
|
+
*/
|
|
1714
|
+
isValidHex(color) {
|
|
1715
|
+
return /^#[0-9A-Fa-f]{6}$/.test(color);
|
|
1716
|
+
}
|
|
1717
|
+
/**
|
|
1718
|
+
* Validate and return hex, or undefined if invalid
|
|
1719
|
+
*/
|
|
1720
|
+
validateHex(color) {
|
|
1721
|
+
return this.isValidHex(color) ? color : void 0;
|
|
1722
|
+
}
|
|
1723
|
+
};
|
|
1724
|
+
|
|
1725
|
+
// src/renderer/icons/iconLibrary.ts
|
|
1726
|
+
var ICON_LIBRARY = {
|
|
1727
|
+
// UI Navigation
|
|
1728
|
+
home: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>`,
|
|
1729
|
+
menu: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg>`,
|
|
1730
|
+
x: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>`,
|
|
1731
|
+
"chevron-right": `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>`,
|
|
1732
|
+
"chevron-left": `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg>`,
|
|
1733
|
+
"chevron-down": `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>`,
|
|
1734
|
+
"chevron-up": `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"></polyline></svg>`,
|
|
1735
|
+
"arrow-right": `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>`,
|
|
1736
|
+
"arrow-left": `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"></line><polyline points="12 19 5 12 12 5"></polyline></svg>`,
|
|
1737
|
+
"arrow-down": `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><polyline points="19 12 12 19 5 12"></polyline></svg>`,
|
|
1738
|
+
"arrow-up": `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="19" x2="12" y2="5"></line><polyline points="5 12 12 5 19 12"></polyline></svg>`,
|
|
1739
|
+
// Search & Input
|
|
1740
|
+
search: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>`,
|
|
1741
|
+
filter: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon></svg>`,
|
|
1742
|
+
// Edit & Actions
|
|
1743
|
+
edit: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>`,
|
|
1744
|
+
"edit-2": `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.83 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"></path></svg>`,
|
|
1745
|
+
trash: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>`,
|
|
1746
|
+
"trash-2": `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>`,
|
|
1747
|
+
copy: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect></svg>`,
|
|
1748
|
+
download: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>`,
|
|
1749
|
+
upload: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="17 8 12 3 7 8"></polyline><line x1="12" y1="3" x2="12" y2="15"></line></svg>`,
|
|
1750
|
+
plus: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>`,
|
|
1751
|
+
minus: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line></svg>`,
|
|
1752
|
+
check: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>`,
|
|
1753
|
+
// Notifications & Status
|
|
1754
|
+
bell: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path><path d="M13.73 21a2 2 0 0 1-3.46 0"></path></svg>`,
|
|
1755
|
+
"alert-circle": `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>`,
|
|
1756
|
+
"alert-triangle": `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3.05h16.94a2 2 0 0 0 1.71-3.05L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>`,
|
|
1757
|
+
info: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>`,
|
|
1758
|
+
// User & Account
|
|
1759
|
+
user: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>`,
|
|
1760
|
+
"user-check": `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><polyline points="17 11 19 13 23 9"></polyline></svg>`,
|
|
1761
|
+
"user-x": `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="18" y1="8" x2="23" y2="13"></line><line x1="23" y1="8" x2="18" y2="13"></line></svg>`,
|
|
1762
|
+
settings: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M12 1v6m0 6v6M4.22 4.22l4.24 4.24m5.08 5.08l4.24 4.24M1 12h6m6 0h6M4.22 19.78l4.24-4.24m5.08-5.08l4.24-4.24"></path></svg>`,
|
|
1763
|
+
"log-out": `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path><polyline points="16 17 21 12 16 7"></polyline><line x1="21" y1="12" x2="9" y2="12"></line></svg>`,
|
|
1764
|
+
"log-in": `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"></path><polyline points="10 17 15 12 10 7"></polyline><line x1="15" y1="12" x2="3" y2="12"></line></svg>`,
|
|
1765
|
+
// Communication
|
|
1766
|
+
mail: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2"></rect><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"></path></svg>`,
|
|
1767
|
+
phone: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path></svg>`,
|
|
1768
|
+
share: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3"></circle><circle cx="6" cy="12" r="3"></circle><circle cx="18" cy="19" r="3"></circle><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"></line><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"></line></svg>`,
|
|
1769
|
+
"share-2": `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3"></circle><circle cx="6" cy="12" r="3"></circle><circle cx="18" cy="19" r="3"></circle><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"></line><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"></line></svg>`,
|
|
1770
|
+
// Data & Info
|
|
1771
|
+
calendar: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg>`,
|
|
1772
|
+
clock: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>`,
|
|
1773
|
+
"map-pin": `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></svg>`,
|
|
1774
|
+
wifi: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"></path><path d="M1.42 9a16 16 0 0 1 21.16 0"></path><path d="M9 20.55a4 4 0 0 1 6 0"></path><circle cx="12" cy="20" r="1"></circle></svg>`,
|
|
1775
|
+
"wifi-off": `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="1" y1="1" x2="23" y2="23"></line><path d="M16.72 11.06A10.94 10.94 0 0 1 19 12.55"></path><path d="M5 12.55a10.94 10.94 0 0 1 5.17-2.39"></path><path d="M10.88 16.92a6 6 0 0 1 2.24 0"></path><circle cx="12" cy="20" r="1"></circle></svg>`,
|
|
1776
|
+
// Media & Visuals
|
|
1777
|
+
eye: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>`,
|
|
1778
|
+
"eye-off": `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path><line x1="1" y1="1" x2="23" y2="23"></line></svg>`,
|
|
1779
|
+
image: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline></svg>`,
|
|
1780
|
+
// Feedback & Social
|
|
1781
|
+
heart: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg>`,
|
|
1782
|
+
star: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 15.09 10.26 24 10.35 17.18 16.54 19.34 24.76 12 19.77 4.66 24.76 6.82 16.54 0 10.35 8.91 10.26 12 2"></polygon></svg>`,
|
|
1783
|
+
// Loading & Misc
|
|
1784
|
+
loader: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="2" x2="12" y2="6"></line><line x1="12" y1="18" x2="12" y2="22"></line><line x1="4.22" y1="4.22" x2="7.07" y2="7.07"></line><line x1="16.93" y1="16.93" x2="19.78" y2="19.78"></line><line x1="2" y1="12" x2="6" y2="12"></line><line x1="18" y1="12" x2="22" y2="12"></line><line x1="4.22" y1="19.78" x2="7.07" y2="16.93"></line><line x1="16.93" y1="7.07" x2="19.78" y2="4.22"></line></svg>`,
|
|
1785
|
+
"help-circle": `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M12 16v-4M12 8h.01"></path></svg>`,
|
|
1786
|
+
lock: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>`
|
|
1787
|
+
};
|
|
1788
|
+
function getIcon(name) {
|
|
1789
|
+
return ICON_LIBRARY[name] || null;
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
// src/renderer/index.ts
|
|
1793
|
+
var THEMES = {
|
|
1794
|
+
light: {
|
|
1795
|
+
bg: "#F8FAFC",
|
|
1796
|
+
cardBg: "#FFFFFF",
|
|
1797
|
+
border: "#E2E8F0",
|
|
1798
|
+
text: "#1E293B",
|
|
1799
|
+
textMuted: "#64748B",
|
|
1800
|
+
primary: "#3B82F6",
|
|
1801
|
+
primaryHover: "#2563EB",
|
|
1802
|
+
primaryLight: "#EFF6FF"
|
|
1803
|
+
},
|
|
1804
|
+
dark: {
|
|
1805
|
+
bg: "#0F172A",
|
|
1806
|
+
cardBg: "#1E293B",
|
|
1807
|
+
border: "#334155",
|
|
1808
|
+
text: "#F1F5F9",
|
|
1809
|
+
textMuted: "#94A3B8",
|
|
1810
|
+
primary: "#60A5FA",
|
|
1811
|
+
primaryHover: "#3B82F6",
|
|
1812
|
+
primaryLight: "#1E3A8A"
|
|
1813
|
+
}
|
|
1814
|
+
};
|
|
1815
|
+
var SVGRenderer = class {
|
|
1816
|
+
constructor(ir, layout, options) {
|
|
1817
|
+
this.renderedNodeIds = /* @__PURE__ */ new Set();
|
|
1818
|
+
this.ir = ir;
|
|
1819
|
+
this.layout = layout;
|
|
1820
|
+
this.selectedScreenName = options?.screenName;
|
|
1821
|
+
this.options = {
|
|
1822
|
+
width: options?.width || 1280,
|
|
1823
|
+
height: options?.height || 720,
|
|
1824
|
+
theme: options?.theme || "light",
|
|
1825
|
+
includeLabels: options?.includeLabels ?? true,
|
|
1826
|
+
screenName: options?.screenName
|
|
1827
|
+
};
|
|
1828
|
+
this.renderTheme = THEMES[this.options.theme];
|
|
1829
|
+
this.colorResolver = new ColorResolver();
|
|
1830
|
+
if (ir.project.mocks && Object.keys(ir.project.mocks).length > 0) {
|
|
1831
|
+
MockDataGenerator.setCustomMocks(ir.project.mocks);
|
|
1832
|
+
}
|
|
1833
|
+
if (ir.project.colors && Object.keys(ir.project.colors).length > 0) {
|
|
1834
|
+
this.colorResolver.setCustomColors(ir.project.colors);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
/**
|
|
1838
|
+
* Get list of available screens in the project
|
|
1839
|
+
*/
|
|
1840
|
+
getAvailableScreens() {
|
|
1841
|
+
return this.ir.project.screens.map((screen) => ({
|
|
1842
|
+
name: screen.name,
|
|
1843
|
+
id: screen.id
|
|
1844
|
+
}));
|
|
1845
|
+
}
|
|
1846
|
+
/**
|
|
1847
|
+
* Get the currently selected or first screen
|
|
1848
|
+
*/
|
|
1849
|
+
getSelectedScreen() {
|
|
1850
|
+
let screen = this.ir.project.screens[0];
|
|
1851
|
+
let screenName = screen?.name || "Unknown";
|
|
1852
|
+
if (this.selectedScreenName) {
|
|
1853
|
+
const found = this.ir.project.screens.find(
|
|
1854
|
+
(s) => s.name.toLowerCase() === this.selectedScreenName.toLowerCase()
|
|
1855
|
+
);
|
|
1856
|
+
if (found) {
|
|
1857
|
+
screen = found;
|
|
1858
|
+
screenName = found.name;
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
return { screen, name: screenName };
|
|
1862
|
+
}
|
|
1863
|
+
render() {
|
|
1864
|
+
const { screen } = this.getSelectedScreen();
|
|
1865
|
+
if (!screen) return "<svg></svg>";
|
|
1866
|
+
const rootId = screen.root.ref;
|
|
1867
|
+
const children = [];
|
|
1868
|
+
this.renderedNodeIds.clear();
|
|
1869
|
+
this.renderNode(rootId, children);
|
|
1870
|
+
const actualHeight = this.calculateContentHeight();
|
|
1871
|
+
const svgHeight = Math.max(this.options.height, actualHeight);
|
|
1872
|
+
let backgroundColor = this.renderTheme.bg;
|
|
1873
|
+
if (screen.background) {
|
|
1874
|
+
backgroundColor = this.colorResolver.resolveColor(screen.background, this.renderTheme.bg);
|
|
1875
|
+
}
|
|
1876
|
+
return `<svg width="${this.options.width}" height="${svgHeight}" viewBox="0 0 ${this.options.width} ${svgHeight}" xmlns="http://www.w3.org/2000/svg">
|
|
1877
|
+
<rect width="100%" height="100%" fill="${backgroundColor}"/>
|
|
1878
|
+
${children.join("\n ")}
|
|
1879
|
+
</svg>`;
|
|
1880
|
+
}
|
|
1881
|
+
calculateContentHeight() {
|
|
1882
|
+
let maxY = 0;
|
|
1883
|
+
for (const nodeId of this.renderedNodeIds) {
|
|
1884
|
+
const pos = this.layout[nodeId];
|
|
1885
|
+
if (pos) {
|
|
1886
|
+
const bottom = pos.y + pos.height;
|
|
1887
|
+
if (bottom > maxY) {
|
|
1888
|
+
maxY = bottom;
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
return Math.max(maxY + 40, this.options.height);
|
|
1893
|
+
}
|
|
1894
|
+
renderNode(nodeId, output) {
|
|
1895
|
+
const node = this.ir.project.nodes[nodeId];
|
|
1896
|
+
const pos = this.layout[nodeId];
|
|
1897
|
+
if (!node || !pos) return;
|
|
1898
|
+
this.renderedNodeIds.add(nodeId);
|
|
1899
|
+
if (node.kind === "container") {
|
|
1900
|
+
if (node.containerType === "panel") {
|
|
1901
|
+
this.renderPanelBorder(node, pos, output);
|
|
1902
|
+
}
|
|
1903
|
+
if (node.containerType === "card") {
|
|
1904
|
+
this.renderCardBorder(node, pos, output);
|
|
1905
|
+
}
|
|
1906
|
+
node.children.forEach((childRef) => {
|
|
1907
|
+
this.renderNode(childRef.ref, output);
|
|
1908
|
+
});
|
|
1909
|
+
} else if (node.kind === "component") {
|
|
1910
|
+
const componentSvg = this.renderComponent(node, pos);
|
|
1911
|
+
if (componentSvg) {
|
|
1912
|
+
output.push(componentSvg);
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
renderComponent(node, pos) {
|
|
1917
|
+
switch (node.componentType) {
|
|
1918
|
+
// Existing components
|
|
1919
|
+
case "Heading":
|
|
1920
|
+
return this.renderHeading(node, pos);
|
|
1921
|
+
case "Button":
|
|
1922
|
+
return this.renderButton(node, pos);
|
|
1923
|
+
case "Input":
|
|
1924
|
+
return this.renderInput(node, pos);
|
|
1925
|
+
case "Topbar":
|
|
1926
|
+
return this.renderTopbar(node, pos);
|
|
1927
|
+
case "Table":
|
|
1928
|
+
return this.renderTable(node, pos);
|
|
1929
|
+
case "ChartPlaceholder":
|
|
1930
|
+
return this.renderChartPlaceholder(node, pos);
|
|
1931
|
+
case "Breadcrumbs":
|
|
1932
|
+
return this.renderBreadcrumbs(node, pos);
|
|
1933
|
+
case "SidebarMenu":
|
|
1934
|
+
return this.renderSidebarMenu(node, pos);
|
|
1935
|
+
// Text/Content components
|
|
1936
|
+
case "Text":
|
|
1937
|
+
return this.renderText(node, pos);
|
|
1938
|
+
case "Label":
|
|
1939
|
+
return this.renderLabel(node, pos);
|
|
1940
|
+
case "Code":
|
|
1941
|
+
return this.renderCode(node, pos);
|
|
1942
|
+
// Form components
|
|
1943
|
+
case "Textarea":
|
|
1944
|
+
return this.renderTextarea(node, pos);
|
|
1945
|
+
case "Select":
|
|
1946
|
+
return this.renderSelect(node, pos);
|
|
1947
|
+
case "Checkbox":
|
|
1948
|
+
return this.renderCheckbox(node, pos);
|
|
1949
|
+
case "Radio":
|
|
1950
|
+
return this.renderRadio(node, pos);
|
|
1951
|
+
case "Toggle":
|
|
1952
|
+
return this.renderToggle(node, pos);
|
|
1953
|
+
// Layout/Structure components
|
|
1954
|
+
case "Sidebar":
|
|
1955
|
+
return this.renderSidebar(node, pos);
|
|
1956
|
+
case "Tabs":
|
|
1957
|
+
return this.renderTabs(node, pos);
|
|
1958
|
+
case "Divider":
|
|
1959
|
+
return this.renderDivider(node, pos);
|
|
1960
|
+
// Feedback/Alert components
|
|
1961
|
+
case "Alert":
|
|
1962
|
+
return this.renderAlert(node, pos);
|
|
1963
|
+
case "Badge":
|
|
1964
|
+
return this.renderBadge(node, pos);
|
|
1965
|
+
case "Modal":
|
|
1966
|
+
return this.renderModal(node, pos);
|
|
1967
|
+
case "List":
|
|
1968
|
+
return this.renderList(node, pos);
|
|
1969
|
+
case "StatCard":
|
|
1970
|
+
return this.renderStatCard(node, pos);
|
|
1971
|
+
case "Image":
|
|
1972
|
+
return this.renderImage(node, pos);
|
|
1973
|
+
// Icon components
|
|
1974
|
+
case "Icon":
|
|
1975
|
+
return this.renderIcon(node, pos);
|
|
1976
|
+
case "IconButton":
|
|
1977
|
+
return this.renderIconButton(node, pos);
|
|
1978
|
+
default:
|
|
1979
|
+
return this.renderGenericComponent(node, pos);
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
renderHeading(node, pos) {
|
|
1983
|
+
const text = String(node.props.text || "Heading");
|
|
1984
|
+
const fontSize = 20;
|
|
1985
|
+
return `<g>
|
|
1986
|
+
<text x="${pos.x}" y="${pos.y + pos.height / 2 + 6}"
|
|
1987
|
+
font-family="system-ui, -apple-system, sans-serif"
|
|
1988
|
+
font-size="${fontSize}"
|
|
1989
|
+
font-weight="600"
|
|
1990
|
+
fill="${this.renderTheme.text}">${this.escapeXml(text)}</text>
|
|
1991
|
+
</g>`;
|
|
1992
|
+
}
|
|
1993
|
+
renderButton(node, pos) {
|
|
1994
|
+
const text = String(node.props.text || "Button");
|
|
1995
|
+
const variant = String(node.props.variant || "default");
|
|
1996
|
+
const size = String(node.props.size || "md");
|
|
1997
|
+
const bgColor = variant === "primary" ? "rgba(59, 130, 246, 0.85)" : "rgba(226, 232, 240, 0.9)";
|
|
1998
|
+
const textColor = variant === "primary" ? "#FFFFFF" : "rgba(30, 41, 59, 0.85)";
|
|
1999
|
+
const borderColor = variant === "primary" ? "rgba(59, 130, 246, 0.7)" : "rgba(100, 116, 139, 0.4)";
|
|
2000
|
+
const fontSizeMap = { "sm": 12, "md": 14, "lg": 16 };
|
|
2001
|
+
const fontSize = fontSizeMap[size] || 14;
|
|
2002
|
+
const buttonWidth = Math.max(pos.width, 60);
|
|
2003
|
+
return `<g>
|
|
2004
|
+
<rect x="${pos.x}" y="${pos.y}"
|
|
2005
|
+
width="${buttonWidth}" height="${pos.height}"
|
|
2006
|
+
rx="6"
|
|
2007
|
+
fill="${bgColor}"
|
|
2008
|
+
stroke="${borderColor}"
|
|
2009
|
+
stroke-width="1"/>
|
|
2010
|
+
<text x="${pos.x + buttonWidth / 2}" y="${pos.y + pos.height / 2 + 5}"
|
|
2011
|
+
font-family="system-ui, -apple-system, sans-serif"
|
|
2012
|
+
font-size="${fontSize}"
|
|
2013
|
+
font-weight="500"
|
|
2014
|
+
fill="${textColor}"
|
|
2015
|
+
text-anchor="middle">${this.escapeXml(text)}</text>
|
|
2016
|
+
</g>`;
|
|
2017
|
+
}
|
|
2018
|
+
renderInput(node, pos) {
|
|
2019
|
+
const label = String(node.props.label || "");
|
|
2020
|
+
const placeholder = String(node.props.placeholder || "");
|
|
2021
|
+
return `<g>
|
|
2022
|
+
${label ? `<text x="${pos.x + 8}" y="${pos.y - 6}"
|
|
2023
|
+
font-family="system-ui, -apple-system, sans-serif"
|
|
2024
|
+
font-size="12"
|
|
2025
|
+
fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>` : ""}
|
|
2026
|
+
<rect x="${pos.x}" y="${pos.y}"
|
|
2027
|
+
width="${pos.width}" height="${pos.height}"
|
|
2028
|
+
rx="6"
|
|
2029
|
+
fill="${this.renderTheme.cardBg}"
|
|
2030
|
+
stroke="${this.renderTheme.border}"
|
|
2031
|
+
stroke-width="1"/>
|
|
2032
|
+
<text x="${pos.x + 12}" y="${pos.y + pos.height / 2 + 5}"
|
|
2033
|
+
font-family="system-ui, -apple-system, sans-serif"
|
|
2034
|
+
font-size="14"
|
|
2035
|
+
fill="${this.renderTheme.textMuted}">${this.escapeXml(placeholder)}</text>
|
|
2036
|
+
</g>`;
|
|
2037
|
+
}
|
|
2038
|
+
renderTopbar(node, pos) {
|
|
2039
|
+
const title = String(node.props.title || "App");
|
|
2040
|
+
const subtitle = String(node.props.subtitle || "");
|
|
2041
|
+
const actions = String(node.props.actions || "");
|
|
2042
|
+
const user = String(node.props.user || "");
|
|
2043
|
+
const titleLineHeight = 18;
|
|
2044
|
+
const paddingTop = 24;
|
|
2045
|
+
let titleY;
|
|
2046
|
+
let subtitleY = 0;
|
|
2047
|
+
if (subtitle) {
|
|
2048
|
+
titleY = pos.y + paddingTop;
|
|
2049
|
+
subtitleY = titleY + 20;
|
|
2050
|
+
} else {
|
|
2051
|
+
titleY = pos.y + pos.height / 2 + titleLineHeight / 2 - 4;
|
|
2052
|
+
}
|
|
2053
|
+
let svg = `<g>
|
|
2054
|
+
<rect x="${pos.x}" y="${pos.y}"
|
|
2055
|
+
width="${pos.width}" height="${pos.height}"
|
|
2056
|
+
fill="${this.renderTheme.cardBg}"
|
|
2057
|
+
stroke="${this.renderTheme.border}"
|
|
2058
|
+
stroke-width="1"/>
|
|
2059
|
+
|
|
2060
|
+
<!-- Title -->
|
|
2061
|
+
<text x="${pos.x + 16}" y="${titleY}"
|
|
2062
|
+
font-family="system-ui, -apple-system, sans-serif"
|
|
2063
|
+
font-size="18"
|
|
2064
|
+
font-weight="600"
|
|
2065
|
+
fill="${this.renderTheme.text}">${this.escapeXml(title)}</text>`;
|
|
2066
|
+
if (subtitle) {
|
|
2067
|
+
svg += `
|
|
2068
|
+
<text x="${pos.x + 16}" y="${subtitleY}"
|
|
2069
|
+
font-family="system-ui, -apple-system, sans-serif"
|
|
2070
|
+
font-size="13"
|
|
2071
|
+
fill="${this.renderTheme.textMuted}">${this.escapeXml(subtitle)}</text>`;
|
|
2072
|
+
}
|
|
2073
|
+
if (user) {
|
|
2074
|
+
const badgeHeight = 28;
|
|
2075
|
+
const badgePaddingX = 12;
|
|
2076
|
+
const badgeX = pos.x + pos.width - 16 - badgePaddingX * 2 - user.length * 7.5;
|
|
2077
|
+
const badgeY = pos.y + 12;
|
|
2078
|
+
svg += `
|
|
2079
|
+
<!-- User badge -->
|
|
2080
|
+
<rect x="${badgeX}" y="${badgeY}"
|
|
2081
|
+
width="${badgePaddingX * 2 + user.length * 7.5}" height="${badgeHeight}"
|
|
2082
|
+
rx="4"
|
|
2083
|
+
fill="${this.renderTheme.cardBg}"
|
|
2084
|
+
stroke="${this.renderTheme.border}"
|
|
2085
|
+
stroke-width="1"/>
|
|
2086
|
+
<text x="${badgeX + badgePaddingX + user.length * 3.75}" y="${badgeY + badgeHeight / 2 + 4}"
|
|
2087
|
+
font-family="system-ui, -apple-system, sans-serif"
|
|
2088
|
+
font-size="12"
|
|
2089
|
+
fill="${this.renderTheme.text}"
|
|
2090
|
+
text-anchor="middle">${this.escapeXml(user)}</text>`;
|
|
2091
|
+
}
|
|
2092
|
+
if (actions) {
|
|
2093
|
+
const actionList = actions.split(",").map((a) => a.trim()).filter(Boolean);
|
|
2094
|
+
const buttonWidth = 100;
|
|
2095
|
+
const buttonHeight = 32;
|
|
2096
|
+
const buttonStartX = pos.x + pos.width - 16 - actionList.length * (buttonWidth + 8);
|
|
2097
|
+
const buttonY = pos.y + (pos.height - buttonHeight) / 2;
|
|
2098
|
+
actionList.forEach((action, idx) => {
|
|
2099
|
+
const bx = buttonStartX + idx * (buttonWidth + 8);
|
|
2100
|
+
svg += `
|
|
2101
|
+
<!-- Action button: ${action} -->
|
|
2102
|
+
<rect x="${bx}" y="${buttonY}"
|
|
2103
|
+
width="${buttonWidth}" height="${buttonHeight}"
|
|
2104
|
+
rx="6"
|
|
2105
|
+
fill="${this.renderTheme.primary}"
|
|
2106
|
+
stroke="none"/>
|
|
2107
|
+
<text x="${bx + buttonWidth / 2}" y="${buttonY + buttonHeight / 2 + 4}"
|
|
2108
|
+
font-family="system-ui, -apple-system, sans-serif"
|
|
2109
|
+
font-size="12"
|
|
2110
|
+
font-weight="600"
|
|
2111
|
+
fill="white"
|
|
2112
|
+
text-anchor="middle">${this.escapeXml(action)}</text>`;
|
|
2113
|
+
});
|
|
2114
|
+
}
|
|
2115
|
+
svg += "\n </g>";
|
|
2116
|
+
return svg;
|
|
2117
|
+
}
|
|
2118
|
+
renderPanelBorder(node, pos, output) {
|
|
2119
|
+
if (node.kind !== "container") return;
|
|
2120
|
+
let fillColor = this.renderTheme.cardBg;
|
|
2121
|
+
if (node.style.background) {
|
|
2122
|
+
fillColor = this.colorResolver.resolveColor(node.style.background, this.renderTheme.cardBg);
|
|
2123
|
+
}
|
|
2124
|
+
const svg = `<g>
|
|
2125
|
+
<rect x="${pos.x}" y="${pos.y}"
|
|
2126
|
+
width="${pos.width}" height="${pos.height}"
|
|
2127
|
+
rx="8"
|
|
2128
|
+
fill="${fillColor}"
|
|
2129
|
+
stroke="${this.renderTheme.border}"
|
|
2130
|
+
stroke-width="1"/>
|
|
2131
|
+
</g>`;
|
|
2132
|
+
output.push(svg);
|
|
2133
|
+
}
|
|
2134
|
+
renderCardBorder(node, pos, output) {
|
|
2135
|
+
if (node.kind !== "container") return;
|
|
2136
|
+
const radiusMap = {
|
|
2137
|
+
none: 0,
|
|
2138
|
+
sm: 4,
|
|
2139
|
+
md: 8,
|
|
2140
|
+
lg: 12
|
|
2141
|
+
};
|
|
2142
|
+
const radius = radiusMap[String(node.params.radius) || "md"] || 8;
|
|
2143
|
+
let fillColor = this.renderTheme.cardBg;
|
|
2144
|
+
if (node.style.background) {
|
|
2145
|
+
fillColor = this.colorResolver.resolveColor(node.style.background, this.renderTheme.cardBg);
|
|
2146
|
+
}
|
|
2147
|
+
const borderParam = String(node.params.border || "true");
|
|
2148
|
+
const showBorder = borderParam !== "false";
|
|
2149
|
+
const strokeWidth = showBorder ? "1" : "0";
|
|
2150
|
+
const svg = `<g>
|
|
2151
|
+
<rect x="${pos.x}" y="${pos.y}"
|
|
2152
|
+
width="${pos.width}" height="${pos.height}"
|
|
2153
|
+
rx="${radius}"
|
|
2154
|
+
fill="${fillColor}"
|
|
2155
|
+
stroke="${this.renderTheme.border}"
|
|
2156
|
+
stroke-width="${strokeWidth}"/>
|
|
2157
|
+
</g>`;
|
|
2158
|
+
output.push(svg);
|
|
2159
|
+
}
|
|
2160
|
+
renderTable(node, pos) {
|
|
2161
|
+
const title = String(node.props.title || "");
|
|
2162
|
+
const columnsStr = String(node.props.columns || "Col1,Col2,Col3");
|
|
2163
|
+
const columns = columnsStr.split(",").map((c) => c.trim());
|
|
2164
|
+
const rowCount = Number(node.props.rows || 5);
|
|
2165
|
+
const mockStr = String(node.props.mock || "");
|
|
2166
|
+
const paginationValue = String(node.props.pagination || "false");
|
|
2167
|
+
const pagination = paginationValue === "true";
|
|
2168
|
+
const pageCount = Number(node.props.pages || 5);
|
|
2169
|
+
const paginationAlign = String(node.props.paginationAlign || "right");
|
|
2170
|
+
const mockTypes = mockStr ? mockStr.split(",").map((m) => m.trim()) : columns.map(() => "item");
|
|
2171
|
+
while (mockTypes.length < columns.length) {
|
|
2172
|
+
mockTypes.push("item");
|
|
2173
|
+
}
|
|
2174
|
+
const headerHeight = 44;
|
|
2175
|
+
const rowHeight = 36;
|
|
2176
|
+
const colWidth = pos.width / columns.length;
|
|
2177
|
+
const mockRows = [];
|
|
2178
|
+
for (let rowIdx = 0; rowIdx < rowCount; rowIdx++) {
|
|
2179
|
+
const row = {};
|
|
2180
|
+
columns.forEach((col, colIdx) => {
|
|
2181
|
+
const mockType = mockTypes[colIdx] || "item";
|
|
2182
|
+
row[col] = MockDataGenerator.getMockValue(mockType, rowIdx);
|
|
2183
|
+
});
|
|
2184
|
+
mockRows.push(row);
|
|
2185
|
+
}
|
|
2186
|
+
let svg = `<g>
|
|
2187
|
+
<rect x="${pos.x}" y="${pos.y}"
|
|
2188
|
+
width="${pos.width}" height="${pos.height}"
|
|
2189
|
+
rx="8"
|
|
2190
|
+
fill="${this.renderTheme.cardBg}"
|
|
2191
|
+
stroke="${this.renderTheme.border}"
|
|
2192
|
+
stroke-width="1"/>`;
|
|
2193
|
+
if (title) {
|
|
2194
|
+
svg += `
|
|
2195
|
+
<text x="${pos.x + 16}" y="${pos.y + 24}"
|
|
2196
|
+
font-family="system-ui, -apple-system, sans-serif"
|
|
2197
|
+
font-size="13"
|
|
2198
|
+
font-weight="600"
|
|
2199
|
+
fill="${this.renderTheme.text}">${this.escapeXml(title)}</text>`;
|
|
2200
|
+
}
|
|
2201
|
+
const headerY = pos.y + (title ? 32 : 0);
|
|
2202
|
+
svg += `
|
|
2203
|
+
<line x1="${pos.x}" y1="${headerY + headerHeight}" x2="${pos.x + pos.width}" y2="${headerY + headerHeight}"
|
|
2204
|
+
stroke="${this.renderTheme.border}" stroke-width="1"/>`;
|
|
2205
|
+
columns.forEach((col, i) => {
|
|
2206
|
+
svg += `
|
|
2207
|
+
<text x="${pos.x + i * colWidth + 12}" y="${headerY + 26}"
|
|
2208
|
+
font-family="system-ui, -apple-system, sans-serif"
|
|
2209
|
+
font-size="11"
|
|
2210
|
+
font-weight="600"
|
|
2211
|
+
fill="${this.renderTheme.textMuted}">${this.escapeXml(col)}</text>`;
|
|
2212
|
+
});
|
|
2213
|
+
mockRows.forEach((row, rowIdx) => {
|
|
2214
|
+
const rowY = headerY + headerHeight + rowIdx * rowHeight;
|
|
2215
|
+
svg += `
|
|
2216
|
+
<line x1="${pos.x}" y1="${rowY + rowHeight}" x2="${pos.x + pos.width}" y2="${rowY + rowHeight}"
|
|
2217
|
+
stroke="${this.renderTheme.border}" stroke-width="0.5"/>`;
|
|
2218
|
+
columns.forEach((col, colIdx) => {
|
|
2219
|
+
const cellValue = row[col] || "";
|
|
2220
|
+
svg += `
|
|
2221
|
+
<text x="${pos.x + colIdx * colWidth + 12}" y="${rowY + 22}"
|
|
2222
|
+
font-family="system-ui, -apple-system, sans-serif"
|
|
2223
|
+
font-size="12"
|
|
2224
|
+
fill="${this.renderTheme.text}">${this.escapeXml(cellValue)}</text>`;
|
|
2225
|
+
});
|
|
2226
|
+
});
|
|
2227
|
+
if (pagination) {
|
|
2228
|
+
const paginationY = headerY + headerHeight + mockRows.length * rowHeight + 16;
|
|
2229
|
+
const buttonWidth = 40;
|
|
2230
|
+
const buttonHeight = 32;
|
|
2231
|
+
const gap = 8;
|
|
2232
|
+
const totalWidth = (pageCount + 2) * (buttonWidth + gap) - gap;
|
|
2233
|
+
let startX;
|
|
2234
|
+
if (paginationAlign === "left") {
|
|
2235
|
+
startX = pos.x + 16;
|
|
2236
|
+
} else if (paginationAlign === "center") {
|
|
2237
|
+
startX = pos.x + (pos.width - totalWidth) / 2;
|
|
2238
|
+
} else {
|
|
2239
|
+
startX = pos.x + pos.width - totalWidth - 16;
|
|
2240
|
+
}
|
|
2241
|
+
svg += `
|
|
2242
|
+
<rect x="${startX}" y="${paginationY}"
|
|
2243
|
+
width="${buttonWidth}" height="${buttonHeight}"
|
|
2244
|
+
rx="4"
|
|
2245
|
+
fill="${this.renderTheme.cardBg}"
|
|
2246
|
+
stroke="${this.renderTheme.border}"
|
|
2247
|
+
stroke-width="1"/>
|
|
2248
|
+
<text x="${startX + buttonWidth / 2}" y="${paginationY + buttonHeight / 2 + 4}"
|
|
2249
|
+
font-family="system-ui, -apple-system, sans-serif"
|
|
2250
|
+
font-size="14"
|
|
2251
|
+
fill="${this.renderTheme.text}"
|
|
2252
|
+
text-anchor="middle"><</text>`;
|
|
2253
|
+
for (let i = 1; i <= pageCount; i++) {
|
|
2254
|
+
const btnX = startX + (buttonWidth + gap) * i;
|
|
2255
|
+
const isActive = i === 1;
|
|
2256
|
+
const bgColor = isActive ? this.renderTheme.primary : this.renderTheme.cardBg;
|
|
2257
|
+
const textColor = isActive ? "#FFFFFF" : this.renderTheme.text;
|
|
2258
|
+
svg += `
|
|
2259
|
+
<rect x="${btnX}" y="${paginationY}"
|
|
2260
|
+
width="${buttonWidth}" height="${buttonHeight}"
|
|
2261
|
+
rx="4"
|
|
2262
|
+
fill="${bgColor}"
|
|
2263
|
+
stroke="${this.renderTheme.border}"
|
|
2264
|
+
stroke-width="1"/>
|
|
2265
|
+
<text x="${btnX + buttonWidth / 2}" y="${paginationY + buttonHeight / 2 + 4}"
|
|
2266
|
+
font-family="system-ui, -apple-system, sans-serif"
|
|
2267
|
+
font-size="14"
|
|
2268
|
+
fill="${textColor}"
|
|
2269
|
+
text-anchor="middle">${i}</text>`;
|
|
2270
|
+
}
|
|
2271
|
+
const nextX = startX + (buttonWidth + gap) * (pageCount + 1);
|
|
2272
|
+
svg += `
|
|
2273
|
+
<rect x="${nextX}" y="${paginationY}"
|
|
2274
|
+
width="${buttonWidth}" height="${buttonHeight}"
|
|
2275
|
+
rx="4"
|
|
2276
|
+
fill="${this.renderTheme.cardBg}"
|
|
2277
|
+
stroke="${this.renderTheme.border}"
|
|
2278
|
+
stroke-width="1"/>
|
|
2279
|
+
<text x="${nextX + buttonWidth / 2}" y="${paginationY + buttonHeight / 2 + 4}"
|
|
2280
|
+
font-family="system-ui, -apple-system, sans-serif"
|
|
2281
|
+
font-size="14"
|
|
2282
|
+
fill="${this.renderTheme.text}"
|
|
2283
|
+
text-anchor="middle">></text>`;
|
|
2284
|
+
}
|
|
2285
|
+
svg += "\n </g>";
|
|
2286
|
+
return svg;
|
|
2287
|
+
}
|
|
2288
|
+
renderChartPlaceholder(node, pos) {
|
|
2289
|
+
const type = String(node.props.type || "bar");
|
|
2290
|
+
return `<g>
|
|
2291
|
+
<rect x="${pos.x}" y="${pos.y}"
|
|
2292
|
+
width="${pos.width}" height="${pos.height}"
|
|
2293
|
+
rx="8"
|
|
2294
|
+
fill="${this.renderTheme.cardBg}"
|
|
2295
|
+
stroke="${this.renderTheme.border}"
|
|
2296
|
+
stroke-width="1"/>
|
|
2297
|
+
<text x="${pos.x + pos.width / 2}" y="${pos.y + pos.height / 2}"
|
|
2298
|
+
font-family="system-ui, -apple-system, sans-serif"
|
|
2299
|
+
font-size="14"
|
|
2300
|
+
fill="${this.renderTheme.textMuted}"
|
|
2301
|
+
text-anchor="middle">[${this.escapeXml(type.toUpperCase())} CHART]</text>
|
|
2302
|
+
</g>`;
|
|
2303
|
+
}
|
|
2304
|
+
// ============================================================================
|
|
2305
|
+
// TEXT/CONTENT COMPONENTS
|
|
2306
|
+
// ============================================================================
|
|
2307
|
+
renderText(node, pos) {
|
|
2308
|
+
const text = String(node.props.content || "Text content");
|
|
2309
|
+
const fontSize = 14;
|
|
2310
|
+
return `<g>
|
|
2311
|
+
<text x="${pos.x}" y="${pos.y + 16}"
|
|
2312
|
+
font-family="system-ui, -apple-system, sans-serif"
|
|
2313
|
+
font-size="${fontSize}"
|
|
2314
|
+
fill="${this.renderTheme.text}">${this.escapeXml(text)}</text>
|
|
2315
|
+
</g>`;
|
|
2316
|
+
}
|
|
2317
|
+
renderLabel(node, pos) {
|
|
2318
|
+
const text = String(node.props.text || "Label");
|
|
2319
|
+
return `<g>
|
|
2320
|
+
<text x="${pos.x}" y="${pos.y + 12}"
|
|
2321
|
+
font-family="system-ui, -apple-system, sans-serif"
|
|
2322
|
+
font-size="12"
|
|
2323
|
+
fill="${this.renderTheme.textMuted}">${this.escapeXml(text)}</text>
|
|
2324
|
+
</g>`;
|
|
2325
|
+
}
|
|
2326
|
+
renderCode(node, pos) {
|
|
2327
|
+
const code = String(node.props.code || "const x = 42;");
|
|
2328
|
+
return `<g>
|
|
2329
|
+
<rect x="${pos.x}" y="${pos.y}"
|
|
2330
|
+
width="${pos.width}" height="${pos.height}"
|
|
2331
|
+
rx="4"
|
|
2332
|
+
fill="${this.renderTheme.bg}"
|
|
2333
|
+
stroke="${this.renderTheme.border}"
|
|
2334
|
+
stroke-width="1"/>
|
|
2335
|
+
<text x="${pos.x + 8}" y="${pos.y + 18}"
|
|
2336
|
+
font-family="monospace"
|
|
2337
|
+
font-size="11"
|
|
2338
|
+
fill="${this.renderTheme.text}">${this.escapeXml(code.substring(0, 30))}</text>
|
|
2339
|
+
</g>`;
|
|
2340
|
+
}
|
|
2341
|
+
// ============================================================================
|
|
2342
|
+
// FORM COMPONENTS
|
|
2343
|
+
// ============================================================================
|
|
2344
|
+
renderTextarea(node, pos) {
|
|
2345
|
+
const label = String(node.props.label || "");
|
|
2346
|
+
const placeholder = String(node.props.placeholder || "Enter text...");
|
|
2347
|
+
return `<g>
|
|
2348
|
+
${label ? `<text x="${pos.x}" y="${pos.y - 6}"
|
|
2349
|
+
font-family="system-ui, -apple-system, sans-serif"
|
|
2350
|
+
font-size="12"
|
|
2351
|
+
fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>` : ""}
|
|
2352
|
+
<rect x="${pos.x}" y="${pos.y}"
|
|
2353
|
+
width="${pos.width}" height="${pos.height}"
|
|
2354
|
+
rx="6"
|
|
2355
|
+
fill="${this.renderTheme.cardBg}"
|
|
2356
|
+
stroke="${this.renderTheme.border}"
|
|
2357
|
+
stroke-width="1"/>
|
|
2358
|
+
<text x="${pos.x + 12}" y="${pos.y + 20}"
|
|
2359
|
+
font-family="system-ui, -apple-system, sans-serif"
|
|
2360
|
+
font-size="13"
|
|
2361
|
+
fill="${this.renderTheme.textMuted}">${this.escapeXml(placeholder)}</text>
|
|
2362
|
+
</g>`;
|
|
2363
|
+
}
|
|
2364
|
+
renderSelect(node, pos) {
|
|
2365
|
+
const label = String(node.props.label || "");
|
|
2366
|
+
const placeholder = String(node.props.placeholder || "Select...");
|
|
2367
|
+
return `<g>
|
|
2368
|
+
${label ? `<text x="${pos.x}" y="${pos.y - 6}"
|
|
2369
|
+
font-family="system-ui, -apple-system, sans-serif"
|
|
2370
|
+
font-size="12"
|
|
2371
|
+
fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>` : ""}
|
|
2372
|
+
<rect x="${pos.x}" y="${pos.y}"
|
|
2373
|
+
width="${pos.width}" height="${pos.height}"
|
|
2374
|
+
rx="6"
|
|
2375
|
+
fill="${this.renderTheme.cardBg}"
|
|
2376
|
+
stroke="${this.renderTheme.border}"
|
|
2377
|
+
stroke-width="1"/>
|
|
2378
|
+
<text x="${pos.x + 12}" y="${pos.y + pos.height / 2 + 5}"
|
|
2379
|
+
font-family="system-ui, -apple-system, sans-serif"
|
|
2380
|
+
font-size="14"
|
|
2381
|
+
fill="${this.renderTheme.textMuted}">${this.escapeXml(placeholder)}</text>
|
|
2382
|
+
<text x="${pos.x + pos.width - 20}" y="${pos.y + pos.height / 2 + 5}"
|
|
2383
|
+
font-family="system-ui, -apple-system, sans-serif"
|
|
2384
|
+
font-size="16"
|
|
2385
|
+
fill="${this.renderTheme.textMuted}">\u25BC</text>
|
|
2386
|
+
</g>`;
|
|
2387
|
+
}
|
|
2388
|
+
renderCheckbox(node, pos) {
|
|
2389
|
+
const label = String(node.props.label || "Checkbox");
|
|
2390
|
+
const checked = String(node.props.checked || "false").toLowerCase() === "true";
|
|
2391
|
+
const checkboxSize = 18;
|
|
2392
|
+
const checkboxY = pos.y + pos.height / 2 - checkboxSize / 2;
|
|
2393
|
+
return `<g>
|
|
2394
|
+
<rect x="${pos.x}" y="${checkboxY}"
|
|
2395
|
+
width="${checkboxSize}" height="${checkboxSize}"
|
|
2396
|
+
rx="4"
|
|
2397
|
+
fill="${checked ? this.renderTheme.primary : this.renderTheme.cardBg}"
|
|
2398
|
+
stroke="${this.renderTheme.border}"
|
|
2399
|
+
stroke-width="1"/>
|
|
2400
|
+
${checked ? `<text x="${pos.x + checkboxSize / 2}" y="${checkboxY + 14}"
|
|
2401
|
+
font-family="system-ui, -apple-system, sans-serif"
|
|
2402
|
+
font-size="12"
|
|
2403
|
+
fill="white"
|
|
2404
|
+
text-anchor="middle">\u2713</text>` : ""}
|
|
2405
|
+
<text x="${pos.x + checkboxSize + 12}" y="${pos.y + pos.height / 2 + 5}"
|
|
2406
|
+
font-family="system-ui, -apple-system, sans-serif"
|
|
2407
|
+
font-size="14"
|
|
2408
|
+
fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>
|
|
2409
|
+
</g>`;
|
|
2410
|
+
}
|
|
2411
|
+
renderRadio(node, pos) {
|
|
2412
|
+
const label = String(node.props.label || "Radio");
|
|
2413
|
+
const checked = String(node.props.checked || "false").toLowerCase() === "true";
|
|
2414
|
+
const radioSize = 16;
|
|
2415
|
+
const radioY = pos.y + pos.height / 2 - radioSize / 2;
|
|
2416
|
+
return `<g>
|
|
2417
|
+
<circle cx="${pos.x + radioSize / 2}" cy="${radioY + radioSize / 2}"
|
|
2418
|
+
r="${radioSize / 2}"
|
|
2419
|
+
fill="${this.renderTheme.cardBg}"
|
|
2420
|
+
stroke="${this.renderTheme.border}"
|
|
2421
|
+
stroke-width="1"/>
|
|
2422
|
+
${checked ? `<circle cx="${pos.x + radioSize / 2}" cy="${radioY + radioSize / 2}"
|
|
2423
|
+
r="${radioSize / 3.5}"
|
|
2424
|
+
fill="${this.renderTheme.primary}"/>` : ""}
|
|
2425
|
+
<text x="${pos.x + radioSize + 12}" y="${pos.y + pos.height / 2 + 5}"
|
|
2426
|
+
font-family="system-ui, -apple-system, sans-serif"
|
|
2427
|
+
font-size="14"
|
|
2428
|
+
fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>
|
|
2429
|
+
</g>`;
|
|
2430
|
+
}
|
|
2431
|
+
renderToggle(node, pos) {
|
|
2432
|
+
const label = String(node.props.label || "Toggle");
|
|
2433
|
+
const enabled = String(node.props.enabled || "false").toLowerCase() === "true";
|
|
2434
|
+
const toggleWidth = 40;
|
|
2435
|
+
const toggleHeight = 20;
|
|
2436
|
+
const toggleY = pos.y + pos.height / 2 - toggleHeight / 2;
|
|
2437
|
+
return `<g>
|
|
2438
|
+
<rect x="${pos.x}" y="${toggleY}"
|
|
2439
|
+
width="${toggleWidth}" height="${toggleHeight}"
|
|
2440
|
+
rx="10"
|
|
2441
|
+
fill="${enabled ? this.renderTheme.primary : this.renderTheme.border}"
|
|
2442
|
+
stroke="none"/>
|
|
2443
|
+
<circle cx="${pos.x + (enabled ? toggleWidth - 10 : 10)}" cy="${toggleY + toggleHeight / 2}"
|
|
2444
|
+
r="8"
|
|
2445
|
+
fill="white"/>
|
|
2446
|
+
<text x="${pos.x + toggleWidth + 12}" y="${pos.y + pos.height / 2 + 5}"
|
|
2447
|
+
font-family="system-ui, -apple-system, sans-serif"
|
|
2448
|
+
font-size="14"
|
|
2449
|
+
fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>
|
|
2450
|
+
</g>`;
|
|
2451
|
+
}
|
|
2452
|
+
// ============================================================================
|
|
2453
|
+
// LAYOUT/STRUCTURE COMPONENTS
|
|
2454
|
+
// ============================================================================
|
|
2455
|
+
renderSidebar(node, pos) {
|
|
2456
|
+
const title = String(node.props.title || "Sidebar");
|
|
2457
|
+
const itemsStr = String(node.props.items || "");
|
|
2458
|
+
const activeItem = String(node.props.active || "");
|
|
2459
|
+
let items = [];
|
|
2460
|
+
if (itemsStr) {
|
|
2461
|
+
items = itemsStr.split(",").map((i) => i.trim());
|
|
2462
|
+
} else {
|
|
2463
|
+
const itemCount = Number(node.props.itemsMock || 6);
|
|
2464
|
+
items = MockDataGenerator.generateMockList("name", itemCount);
|
|
2465
|
+
}
|
|
2466
|
+
const itemHeight = 40;
|
|
2467
|
+
const padding = 16;
|
|
2468
|
+
const titleHeight = 40;
|
|
2469
|
+
let svg = `<g>
|
|
2470
|
+
<rect x="${pos.x}" y="${pos.y}"
|
|
2471
|
+
width="${pos.width}" height="${pos.height}"
|
|
2472
|
+
fill="${this.renderTheme.cardBg}"
|
|
2473
|
+
stroke="${this.renderTheme.border}"
|
|
2474
|
+
stroke-width="1"/>
|
|
2475
|
+
<!-- Title -->
|
|
2476
|
+
<text x="${pos.x + padding}" y="${pos.y + padding + 8}"
|
|
2477
|
+
font-family="system-ui, -apple-system, sans-serif"
|
|
2478
|
+
font-size="14"
|
|
2479
|
+
font-weight="600"
|
|
2480
|
+
fill="${this.renderTheme.text}">${this.escapeXml(title)}</text>
|
|
2481
|
+
<line x1="${pos.x}" y1="${pos.y + titleHeight}" x2="${pos.x + pos.width}" y2="${pos.y + titleHeight}"
|
|
2482
|
+
stroke="${this.renderTheme.border}" stroke-width="1"/>`;
|
|
2483
|
+
items.forEach((item, i) => {
|
|
2484
|
+
const itemY = pos.y + titleHeight + padding + i * itemHeight;
|
|
2485
|
+
const isActive = item === activeItem;
|
|
2486
|
+
svg += `
|
|
2487
|
+
<rect x="${pos.x + 8}" y="${itemY}"
|
|
2488
|
+
width="${pos.width - 16}" height="36"
|
|
2489
|
+
rx="4"
|
|
2490
|
+
fill="${isActive ? this.renderTheme.primary : "transparent"}"
|
|
2491
|
+
stroke="none"/>
|
|
2492
|
+
<text x="${pos.x + 16}" y="${itemY + 22}"
|
|
2493
|
+
font-family="system-ui, -apple-system, sans-serif"
|
|
2494
|
+
font-size="13"
|
|
2495
|
+
fill="${isActive ? "white" : this.renderTheme.textMuted}">${this.escapeXml(item)}</text>`;
|
|
2496
|
+
});
|
|
2497
|
+
svg += "\n </g>";
|
|
2498
|
+
return svg;
|
|
2499
|
+
}
|
|
2500
|
+
renderTabs(node, pos) {
|
|
2501
|
+
const itemsStr = String(node.props.items || "");
|
|
2502
|
+
const tabs = itemsStr ? itemsStr.split(",").map((t) => t.trim()) : ["Tab 1", "Tab 2", "Tab 3"];
|
|
2503
|
+
const tabWidth = pos.width / tabs.length;
|
|
2504
|
+
let svg = `<g>
|
|
2505
|
+
<!-- Tab headers -->`;
|
|
2506
|
+
tabs.forEach((tab, i) => {
|
|
2507
|
+
const tabX = pos.x + i * tabWidth;
|
|
2508
|
+
const isActive = i === 0;
|
|
2509
|
+
svg += `
|
|
2510
|
+
<rect x="${tabX}" y="${pos.y}"
|
|
2511
|
+
width="${tabWidth}" height="44"
|
|
2512
|
+
fill="${isActive ? this.renderTheme.primary : "transparent"}"
|
|
2513
|
+
stroke="${isActive ? "none" : this.renderTheme.border}"
|
|
2514
|
+
stroke-width="1"/>
|
|
2515
|
+
<text x="${tabX + tabWidth / 2}" y="${pos.y + 28}"
|
|
2516
|
+
font-family="system-ui, -apple-system, sans-serif"
|
|
2517
|
+
font-size="13"
|
|
2518
|
+
font-weight="${isActive ? "600" : "500"}"
|
|
2519
|
+
fill="${isActive ? "white" : this.renderTheme.text}"
|
|
2520
|
+
text-anchor="middle">${this.escapeXml(tab)}</text>`;
|
|
2521
|
+
});
|
|
2522
|
+
svg += `
|
|
2523
|
+
<!-- Tab content area -->
|
|
2524
|
+
<rect x="${pos.x}" y="${pos.y + 44}"
|
|
2525
|
+
width="${pos.width}" height="${pos.height - 44}"
|
|
2526
|
+
fill="${this.renderTheme.cardBg}"
|
|
2527
|
+
stroke="${this.renderTheme.border}"
|
|
2528
|
+
stroke-width="1"/>
|
|
2529
|
+
</g>`;
|
|
2530
|
+
return svg;
|
|
2531
|
+
}
|
|
2532
|
+
renderDivider(node, pos) {
|
|
2533
|
+
return `<g>
|
|
2534
|
+
<line x1="${pos.x}" y1="${pos.y + pos.height / 2}"
|
|
2535
|
+
x2="${pos.x + pos.width}" y2="${pos.y + pos.height / 2}"
|
|
2536
|
+
stroke="${this.renderTheme.border}"
|
|
2537
|
+
stroke-width="1"/>
|
|
2538
|
+
</g>`;
|
|
2539
|
+
}
|
|
2540
|
+
// ============================================================================
|
|
2541
|
+
// FEEDBACK/ALERT COMPONENTS
|
|
2542
|
+
// ============================================================================
|
|
2543
|
+
renderAlert(node, pos) {
|
|
2544
|
+
const type = String(node.props.type || "info");
|
|
2545
|
+
const message = String(node.props.message || "Alert message");
|
|
2546
|
+
const typeColors = {
|
|
2547
|
+
info: "#3B82F6",
|
|
2548
|
+
warning: "#F59E0B",
|
|
2549
|
+
error: "#EF4444",
|
|
2550
|
+
success: "#10B981"
|
|
2551
|
+
};
|
|
2552
|
+
const bgColor = typeColors[type] || typeColors.info;
|
|
2553
|
+
return `<g>
|
|
2554
|
+
<rect x="${pos.x}" y="${pos.y}"
|
|
2555
|
+
width="${pos.width}" height="${pos.height}"
|
|
2556
|
+
rx="6"
|
|
2557
|
+
fill="${bgColor}15"
|
|
2558
|
+
stroke="${bgColor}"
|
|
2559
|
+
stroke-width="1"/>
|
|
2560
|
+
<rect x="${pos.x}" y="${pos.y}"
|
|
2561
|
+
width="4" height="${pos.height}"
|
|
2562
|
+
rx="6"
|
|
2563
|
+
fill="${bgColor}"/>
|
|
2564
|
+
<text x="${pos.x + 16}" y="${pos.y + pos.height / 2 + 5}"
|
|
2565
|
+
font-family="system-ui, -apple-system, sans-serif"
|
|
2566
|
+
font-size="13"
|
|
2567
|
+
fill="${bgColor}">${this.escapeXml(message)}</text>
|
|
2568
|
+
</g>`;
|
|
2569
|
+
}
|
|
2570
|
+
renderBadge(node, pos) {
|
|
2571
|
+
const text = String(node.props.text || "Badge");
|
|
2572
|
+
const variant = String(node.props.variant || "default");
|
|
2573
|
+
const bgColor = variant === "primary" ? this.renderTheme.primary : this.renderTheme.border;
|
|
2574
|
+
const textColor = variant === "primary" ? "white" : this.renderTheme.text;
|
|
2575
|
+
return `<g>
|
|
2576
|
+
<rect x="${pos.x}" y="${pos.y}"
|
|
2577
|
+
width="${pos.width}" height="${pos.height}"
|
|
2578
|
+
rx="${pos.height / 2}"
|
|
2579
|
+
fill="${bgColor}"
|
|
2580
|
+
stroke="none"/>
|
|
2581
|
+
<text x="${pos.x + pos.width / 2}" y="${pos.y + pos.height / 2 + 4}"
|
|
2582
|
+
font-family="system-ui, -apple-system, sans-serif"
|
|
2583
|
+
font-size="12"
|
|
2584
|
+
font-weight="600"
|
|
2585
|
+
fill="${textColor}"
|
|
2586
|
+
text-anchor="middle">${this.escapeXml(text)}</text>
|
|
2587
|
+
</g>`;
|
|
2588
|
+
}
|
|
2589
|
+
renderModal(node, pos) {
|
|
2590
|
+
const title = String(node.props.title || "Modal");
|
|
2591
|
+
const padding = 16;
|
|
2592
|
+
const headerHeight = 48;
|
|
2593
|
+
const overlayHeight = Math.max(this.options.height, this.calculateContentHeight());
|
|
2594
|
+
const modalX = (this.options.width - pos.width) / 2;
|
|
2595
|
+
const modalY = Math.max(40, (overlayHeight - pos.height) / 2);
|
|
2596
|
+
return `<g>
|
|
2597
|
+
<!-- Modal backdrop -->
|
|
2598
|
+
<rect x="0" y="0"
|
|
2599
|
+
width="${this.options.width}" height="${overlayHeight}"
|
|
2600
|
+
fill="black" opacity="0.28"/>
|
|
2601
|
+
|
|
2602
|
+
<!-- Modal box -->
|
|
2603
|
+
<rect x="${modalX}" y="${modalY}"
|
|
2604
|
+
width="${pos.width}" height="${pos.height}"
|
|
2605
|
+
rx="8"
|
|
2606
|
+
fill="${this.renderTheme.cardBg}"
|
|
2607
|
+
stroke="${this.renderTheme.border}"
|
|
2608
|
+
stroke-width="1"/>
|
|
2609
|
+
|
|
2610
|
+
<!-- Header -->
|
|
2611
|
+
<line x1="${modalX}" y1="${modalY + headerHeight}"
|
|
2612
|
+
x2="${modalX + pos.width}" y2="${modalY + headerHeight}"
|
|
2613
|
+
stroke="${this.renderTheme.border}"
|
|
2614
|
+
stroke-width="1"/>
|
|
2615
|
+
|
|
2616
|
+
<text x="${modalX + padding}" y="${modalY + padding + 16}"
|
|
2617
|
+
font-family="system-ui, -apple-system, sans-serif"
|
|
2618
|
+
font-size="16"
|
|
2619
|
+
font-weight="600"
|
|
2620
|
+
fill="${this.renderTheme.text}">${this.escapeXml(title)}</text>
|
|
2621
|
+
|
|
2622
|
+
<!-- Close button -->
|
|
2623
|
+
<text x="${modalX + pos.width - 16}" y="${modalY + padding + 12}"
|
|
2624
|
+
font-family="system-ui, -apple-system, sans-serif"
|
|
2625
|
+
font-size="18"
|
|
2626
|
+
fill="${this.renderTheme.textMuted}">\u2715</text>
|
|
2627
|
+
|
|
2628
|
+
<!-- Content placeholder -->
|
|
2629
|
+
<text x="${modalX + pos.width / 2}" y="${modalY + headerHeight + (pos.height - headerHeight) / 2}"
|
|
2630
|
+
font-family="system-ui, -apple-system, sans-serif"
|
|
2631
|
+
font-size="13"
|
|
2632
|
+
fill="${this.renderTheme.textMuted}"
|
|
2633
|
+
text-anchor="middle">Modal content</text>
|
|
2634
|
+
</g>`;
|
|
2635
|
+
}
|
|
2636
|
+
renderList(node, pos) {
|
|
2637
|
+
const title = String(node.props.title || "");
|
|
2638
|
+
const itemsStr = String(node.props.items || "");
|
|
2639
|
+
let items = [];
|
|
2640
|
+
if (itemsStr) {
|
|
2641
|
+
items = itemsStr.split(",").map((i) => i.trim());
|
|
2642
|
+
} else {
|
|
2643
|
+
const itemCount = Number(node.props.itemsMock || 4);
|
|
2644
|
+
items = MockDataGenerator.generateMockList("name", itemCount);
|
|
2645
|
+
}
|
|
2646
|
+
const padding = 12;
|
|
2647
|
+
const itemHeight = 36;
|
|
2648
|
+
const titleHeight = title ? 40 : 0;
|
|
2649
|
+
let svg = `<g>
|
|
2650
|
+
<rect x="${pos.x}" y="${pos.y}"
|
|
2651
|
+
width="${pos.width}" height="${pos.height}"
|
|
2652
|
+
rx="8"
|
|
2653
|
+
fill="${this.renderTheme.cardBg}"
|
|
2654
|
+
stroke="${this.renderTheme.border}"
|
|
2655
|
+
stroke-width="1"/>`;
|
|
2656
|
+
if (title) {
|
|
2657
|
+
svg += `
|
|
2658
|
+
<text x="${pos.x + padding}" y="${pos.y + 26}"
|
|
2659
|
+
font-family="system-ui, -apple-system, sans-serif"
|
|
2660
|
+
font-size="13"
|
|
2661
|
+
font-weight="600"
|
|
2662
|
+
fill="${this.renderTheme.text}">${this.escapeXml(title)}</text>
|
|
2663
|
+
<line x1="${pos.x}" y1="${pos.y + titleHeight}" x2="${pos.x + pos.width}" y2="${pos.y + titleHeight}"
|
|
2664
|
+
stroke="${this.renderTheme.border}" stroke-width="1"/>`;
|
|
2665
|
+
}
|
|
2666
|
+
items.forEach((item, i) => {
|
|
2667
|
+
const itemY = pos.y + titleHeight + i * itemHeight;
|
|
2668
|
+
if (itemY + itemHeight < pos.y + pos.height) {
|
|
2669
|
+
svg += `
|
|
2670
|
+
<line x1="${pos.x}" y1="${itemY + itemHeight}"
|
|
2671
|
+
x2="${pos.x + pos.width}" y2="${itemY + itemHeight}"
|
|
2672
|
+
stroke="${this.renderTheme.border}"
|
|
2673
|
+
stroke-width="0.5"/>
|
|
2674
|
+
<text x="${pos.x + padding}" y="${itemY + 24}"
|
|
2675
|
+
font-family="system-ui, -apple-system, sans-serif"
|
|
2676
|
+
font-size="13"
|
|
2677
|
+
fill="${this.renderTheme.text}">${this.escapeXml(item)}</text>`;
|
|
2678
|
+
}
|
|
2679
|
+
});
|
|
2680
|
+
svg += "\n </g>";
|
|
2681
|
+
return svg;
|
|
2682
|
+
}
|
|
2683
|
+
renderGenericComponent(node, pos) {
|
|
2684
|
+
return `<g>
|
|
2685
|
+
<rect x="${pos.x}" y="${pos.y}"
|
|
2686
|
+
width="${pos.width}" height="${pos.height}"
|
|
2687
|
+
rx="4"
|
|
2688
|
+
fill="${this.renderTheme.cardBg}"
|
|
2689
|
+
stroke="${this.renderTheme.border}"
|
|
2690
|
+
stroke-width="1"
|
|
2691
|
+
stroke-dasharray="4 4"/>
|
|
2692
|
+
<text x="${pos.x + pos.width / 2}" y="${pos.y + pos.height / 2}"
|
|
2693
|
+
font-family="system-ui, -apple-system, sans-serif"
|
|
2694
|
+
font-size="12"
|
|
2695
|
+
fill="${this.renderTheme.textMuted}"
|
|
2696
|
+
text-anchor="middle">${node.componentType}</text>
|
|
2697
|
+
</g>`;
|
|
2698
|
+
}
|
|
2699
|
+
renderStatCard(node, pos) {
|
|
2700
|
+
const title = String(node.props.title || "Metric");
|
|
2701
|
+
const value = String(node.props.value || "0");
|
|
2702
|
+
const caption = String(node.props.caption || "");
|
|
2703
|
+
const padding = this.resolveSpacing(node.style.padding) || 16;
|
|
2704
|
+
const innerX = pos.x + padding;
|
|
2705
|
+
const innerY = pos.y + padding;
|
|
2706
|
+
const innerWidth = pos.width - padding * 2;
|
|
2707
|
+
const innerHeight = pos.height - padding * 2;
|
|
2708
|
+
const valueSize = 32;
|
|
2709
|
+
const titleSize = 14;
|
|
2710
|
+
const captionSize = 12;
|
|
2711
|
+
const lineHeight = 18;
|
|
2712
|
+
const topGap = 8;
|
|
2713
|
+
const valueGap = 12;
|
|
2714
|
+
const captionGap = 12;
|
|
2715
|
+
const titleY = innerY + topGap + titleSize;
|
|
2716
|
+
const valueY = titleY + valueGap + valueSize;
|
|
2717
|
+
const captionY = valueY + captionGap + captionSize;
|
|
2718
|
+
let svg = `<g>
|
|
2719
|
+
<!-- StatCard Background -->
|
|
2720
|
+
<rect x="${pos.x}" y="${pos.y}"
|
|
2721
|
+
width="${pos.width}" height="${pos.height}"
|
|
2722
|
+
rx="8"
|
|
2723
|
+
fill="${this.renderTheme.cardBg}"
|
|
2724
|
+
stroke="${this.renderTheme.border}"
|
|
2725
|
+
stroke-width="1"/>
|
|
2726
|
+
|
|
2727
|
+
<!-- Title -->
|
|
2728
|
+
<text x="${innerX}" y="${titleY}"
|
|
2729
|
+
font-family="system-ui, -apple-system, sans-serif"
|
|
2730
|
+
font-size="${titleSize}"
|
|
2731
|
+
font-weight="500"
|
|
2732
|
+
fill="${this.renderTheme.textMuted}">${this.escapeXml(title)}</text>
|
|
2733
|
+
|
|
2734
|
+
<!-- Value (Large) -->
|
|
2735
|
+
<text x="${innerX}" y="${valueY}"
|
|
2736
|
+
font-family="system-ui, -apple-system, sans-serif"
|
|
2737
|
+
font-size="${valueSize}"
|
|
2738
|
+
font-weight="700"
|
|
2739
|
+
fill="${this.renderTheme.primary}">${this.escapeXml(value)}</text>`;
|
|
2740
|
+
if (caption) {
|
|
2741
|
+
svg += `
|
|
2742
|
+
<!-- Caption -->
|
|
2743
|
+
<text x="${innerX}" y="${captionY}"
|
|
2744
|
+
font-family="system-ui, -apple-system, sans-serif"
|
|
2745
|
+
font-size="${captionSize}"
|
|
2746
|
+
fill="${this.renderTheme.textMuted}">${this.escapeXml(caption)}</text>`;
|
|
2747
|
+
}
|
|
2748
|
+
svg += `
|
|
2749
|
+
</g>`;
|
|
2750
|
+
return svg;
|
|
2751
|
+
}
|
|
2752
|
+
renderImage(node, pos) {
|
|
2753
|
+
const placeholder = String(node.props.placeholder || "landscape");
|
|
2754
|
+
const aspectRatios = {
|
|
2755
|
+
landscape: 16 / 9,
|
|
2756
|
+
portrait: 2 / 3,
|
|
2757
|
+
square: 1,
|
|
2758
|
+
icon: 1,
|
|
2759
|
+
avatar: 1
|
|
2760
|
+
};
|
|
2761
|
+
const ratio = aspectRatios[placeholder] || 16 / 9;
|
|
2762
|
+
const maxSize = Math.min(pos.width, pos.height) * 0.8;
|
|
2763
|
+
let iconWidth = maxSize;
|
|
2764
|
+
let iconHeight = maxSize / ratio;
|
|
2765
|
+
if (iconHeight > pos.height * 0.8) {
|
|
2766
|
+
iconHeight = pos.height * 0.8;
|
|
2767
|
+
iconWidth = iconHeight * ratio;
|
|
2768
|
+
}
|
|
2769
|
+
const offsetX = pos.x + (pos.width - iconWidth) / 2;
|
|
2770
|
+
const offsetY = pos.y + (pos.height - iconHeight) / 2;
|
|
2771
|
+
let svgContent = "";
|
|
2772
|
+
let svg = `<g>
|
|
2773
|
+
<!-- Image Background -->
|
|
2774
|
+
<rect x="${pos.x}" y="${pos.y}" width="${pos.width}" height="${pos.height}" fill="#E8E8E8"/>`;
|
|
2775
|
+
if (["landscape", "portrait", "square"].includes(placeholder)) {
|
|
2776
|
+
const cameraCx = offsetX + iconWidth / 2;
|
|
2777
|
+
const cameraCy = offsetY + iconHeight / 2;
|
|
2778
|
+
const scale = Math.min(iconWidth, iconHeight) / 24;
|
|
2779
|
+
const scaledX = cameraCx - 24 / 2 * scale;
|
|
2780
|
+
const scaledY = cameraCy - 24 / 2 * scale;
|
|
2781
|
+
const bodyLeft = scaledX + 2 * scale;
|
|
2782
|
+
const bodyTop = scaledY + 5 * scale;
|
|
2783
|
+
const bodyWidth = 20 * scale;
|
|
2784
|
+
const bodyHeight = 16 * scale;
|
|
2785
|
+
const bodyRadius = 2 * scale;
|
|
2786
|
+
const lensCx = cameraCx;
|
|
2787
|
+
const lensCy = cameraCy + 0.5 * scale;
|
|
2788
|
+
const lensRadius = 4 * scale;
|
|
2789
|
+
svg += `
|
|
2790
|
+
<!-- Camera Icon - Modern Digital Design -->
|
|
2791
|
+
<!-- Camera body (rounded rectangle with fill) -->
|
|
2792
|
+
<g>
|
|
2793
|
+
<!-- Body with fill -->
|
|
2794
|
+
<path d="M ${bodyLeft + bodyRadius} ${bodyTop}
|
|
2795
|
+
L ${bodyLeft + bodyWidth - bodyRadius} ${bodyTop}
|
|
2796
|
+
Q ${bodyLeft + bodyWidth} ${bodyTop} ${bodyLeft + bodyWidth} ${bodyTop + bodyRadius}
|
|
2797
|
+
L ${bodyLeft + bodyWidth} ${bodyTop + bodyHeight - bodyRadius}
|
|
2798
|
+
Q ${bodyLeft + bodyWidth} ${bodyTop + bodyHeight} ${bodyLeft + bodyWidth - bodyRadius} ${bodyTop + bodyHeight}
|
|
2799
|
+
L ${bodyLeft + bodyRadius} ${bodyTop + bodyHeight}
|
|
2800
|
+
Q ${bodyLeft} ${bodyTop + bodyHeight} ${bodyLeft} ${bodyTop + bodyHeight - bodyRadius}
|
|
2801
|
+
L ${bodyLeft} ${bodyTop + bodyRadius}
|
|
2802
|
+
Q ${bodyLeft} ${bodyTop} ${bodyLeft + bodyRadius} ${bodyTop}"
|
|
2803
|
+
fill="#666" stroke="#666" stroke-width="${1.5 * scale}" stroke-linecap="round" stroke-linejoin="round"/>
|
|
2804
|
+
</g>
|
|
2805
|
+
<!-- Main lens circle (prominent) -->
|
|
2806
|
+
<circle cx="${lensCx}" cy="${lensCy}" r="${lensRadius}"
|
|
2807
|
+
fill="none" stroke="#333" stroke-width="${2 * scale}" stroke-linecap="round" stroke-linejoin="round"/>
|
|
2808
|
+
<!-- Lens inner circle (glass effect) -->
|
|
2809
|
+
<circle cx="${lensCx}" cy="${lensCy}" r="${lensRadius * 0.7}"
|
|
2810
|
+
fill="#C0C0C0" stroke="#999" stroke-width="${0.8 * scale}"/>
|
|
2811
|
+
<!-- Lens highlight (reflection) -->
|
|
2812
|
+
<circle cx="${lensCx - lensRadius * 0.25}" cy="${lensCy - lensRadius * 0.25}"
|
|
2813
|
+
r="${lensRadius * 0.25}"
|
|
2814
|
+
fill="#E0E0E0" opacity="0.6"/>`;
|
|
2815
|
+
} else if (["avatar", "icon"].includes(placeholder)) {
|
|
2816
|
+
const personWidth = iconWidth * 0.5;
|
|
2817
|
+
const personHeight = iconHeight * 0.7;
|
|
2818
|
+
const personCx = offsetX + iconWidth / 2;
|
|
2819
|
+
const personCy = offsetY + iconHeight / 2 - iconHeight * 0.1;
|
|
2820
|
+
svg += `
|
|
2821
|
+
<!-- Person Silhouette -->
|
|
2822
|
+
<!-- Head -->
|
|
2823
|
+
<circle cx="${personCx}" cy="${personCy - personHeight * 0.25}"
|
|
2824
|
+
r="${personWidth * 0.25}" fill="#666"/>
|
|
2825
|
+
<!-- Body -->
|
|
2826
|
+
<rect x="${personCx - personWidth * 0.3}" y="${personCy - personHeight * 0.05}"
|
|
2827
|
+
width="${personWidth * 0.6}" height="${personHeight * 0.5}"
|
|
2828
|
+
fill="#666" rx="3"/>`;
|
|
2829
|
+
}
|
|
2830
|
+
svg += `
|
|
2831
|
+
<!-- Border -->
|
|
2832
|
+
<rect x="${pos.x}" y="${pos.y}" width="${pos.width}" height="${pos.height}"
|
|
2833
|
+
fill="none" stroke="${this.renderTheme.border}" stroke-width="1" rx="4"/>
|
|
2834
|
+
</g>`;
|
|
2835
|
+
return svg;
|
|
2836
|
+
}
|
|
2837
|
+
renderBreadcrumbs(node, pos) {
|
|
2838
|
+
const itemsStr = String(node.props.items || "Home");
|
|
2839
|
+
const items = itemsStr.split(",").map((s) => s.trim());
|
|
2840
|
+
const separator = String(node.props.separator || "/");
|
|
2841
|
+
const fontSize = 12;
|
|
2842
|
+
const separatorWidth = 20;
|
|
2843
|
+
const itemSpacing = 8;
|
|
2844
|
+
let currentX = pos.x;
|
|
2845
|
+
let svg = "<g>";
|
|
2846
|
+
items.forEach((item, index) => {
|
|
2847
|
+
const isLast = index === items.length - 1;
|
|
2848
|
+
const textColor = isLast ? this.renderTheme.text : this.renderTheme.textMuted;
|
|
2849
|
+
const fontWeight = isLast ? "500" : "400";
|
|
2850
|
+
svg += `
|
|
2851
|
+
<text x="${currentX}" y="${pos.y + pos.height / 2 + 4}"
|
|
2852
|
+
font-family="system-ui, -apple-system, sans-serif"
|
|
2853
|
+
font-size="${fontSize}"
|
|
2854
|
+
font-weight="${fontWeight}"
|
|
2855
|
+
fill="${textColor}">${this.escapeXml(item)}</text>`;
|
|
2856
|
+
const textWidth = item.length * 6.5;
|
|
2857
|
+
currentX += textWidth + itemSpacing;
|
|
2858
|
+
if (!isLast) {
|
|
2859
|
+
svg += `
|
|
2860
|
+
<text x="${currentX + 4}" y="${pos.y + pos.height / 2 + 4}"
|
|
2861
|
+
font-family="system-ui, -apple-system, sans-serif"
|
|
2862
|
+
font-size="${fontSize}"
|
|
2863
|
+
fill="${this.renderTheme.textMuted}">${this.escapeXml(separator)}</text>`;
|
|
2864
|
+
currentX += separatorWidth;
|
|
2865
|
+
}
|
|
2866
|
+
});
|
|
2867
|
+
svg += "\n </g>";
|
|
2868
|
+
return svg;
|
|
2869
|
+
}
|
|
2870
|
+
renderSidebarMenu(node, pos) {
|
|
2871
|
+
const itemsStr = String(node.props.items || "Item 1,Item 2,Item 3");
|
|
2872
|
+
const iconsStr = String(node.props.icons || "");
|
|
2873
|
+
const items = itemsStr.split(",").map((s) => s.trim());
|
|
2874
|
+
const icons = iconsStr ? iconsStr.split(",").map((s) => s.trim()) : [];
|
|
2875
|
+
const itemHeight = 40;
|
|
2876
|
+
const fontSize = 14;
|
|
2877
|
+
const activeIndex = Number(node.props.active || 0);
|
|
2878
|
+
let svg = "<g>";
|
|
2879
|
+
items.forEach((item, index) => {
|
|
2880
|
+
const itemY = pos.y + index * itemHeight;
|
|
2881
|
+
const isActive = index === activeIndex;
|
|
2882
|
+
const bgColor = isActive ? "rgba(59, 130, 246, 0.15)" : "transparent";
|
|
2883
|
+
const textColor = isActive ? "rgba(59, 130, 246, 0.9)" : "rgba(30, 41, 59, 0.75)";
|
|
2884
|
+
const fontWeight = isActive ? "500" : "400";
|
|
2885
|
+
if (isActive) {
|
|
2886
|
+
svg += `
|
|
2887
|
+
<rect x="${pos.x}" y="${itemY}"
|
|
2888
|
+
width="${pos.width}" height="${itemHeight}"
|
|
2889
|
+
rx="6"
|
|
2890
|
+
fill="${bgColor}"/>`;
|
|
2891
|
+
}
|
|
2892
|
+
let currentX = pos.x + 12;
|
|
2893
|
+
if (icons[index]) {
|
|
2894
|
+
const iconSvg = getIcon(icons[index]);
|
|
2895
|
+
if (iconSvg) {
|
|
2896
|
+
const iconSize = 16;
|
|
2897
|
+
const iconY = itemY + (itemHeight - iconSize) / 2;
|
|
2898
|
+
svg += `
|
|
2899
|
+
<g transform="translate(${currentX}, ${iconY})">
|
|
2900
|
+
<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="rgba(30, 41, 59, 0.6)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
2901
|
+
${this.extractSvgContent(iconSvg)}
|
|
2902
|
+
</svg>
|
|
2903
|
+
</g>`;
|
|
2904
|
+
currentX += iconSize + 8;
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
svg += `
|
|
2908
|
+
<text x="${currentX}" y="${itemY + itemHeight / 2 + 5}"
|
|
2909
|
+
font-family="system-ui, -apple-system, sans-serif"
|
|
2910
|
+
font-size="${fontSize}"
|
|
2911
|
+
font-weight="${fontWeight}"
|
|
2912
|
+
fill="${textColor}">${this.escapeXml(item)}</text>`;
|
|
2913
|
+
});
|
|
2914
|
+
svg += "\n </g>";
|
|
2915
|
+
return svg;
|
|
2916
|
+
}
|
|
2917
|
+
renderIcon(node, pos) {
|
|
2918
|
+
const iconType = String(node.props.type || "help-circle");
|
|
2919
|
+
const size = String(node.props.size || "md");
|
|
2920
|
+
const iconSvg = getIcon(iconType);
|
|
2921
|
+
if (!iconSvg) {
|
|
2922
|
+
return `<g>
|
|
2923
|
+
<!-- Icon not found: ${iconType} -->
|
|
2924
|
+
<circle cx="${pos.x + pos.width / 2}" cy="${pos.y + pos.height / 2}" r="${Math.min(pos.width, pos.height) / 2 - 2}" fill="none" stroke="rgba(100, 116, 139, 0.4)" stroke-width="1"/>
|
|
2925
|
+
<text x="${pos.x + pos.width / 2}" y="${pos.y + pos.height / 2 + 4}" font-family="system-ui, -apple-system, sans-serif" font-size="12" fill="rgba(100, 116, 139, 0.6)" text-anchor="middle">?</text>
|
|
2926
|
+
</g>`;
|
|
2927
|
+
}
|
|
2928
|
+
const sizeMap = { "sm": 14, "md": 18, "lg": 24 };
|
|
2929
|
+
const iconSize = sizeMap[size] || 18;
|
|
2930
|
+
const iconColor = "rgba(30, 41, 59, 0.75)";
|
|
2931
|
+
const offsetX = pos.x + (pos.width - iconSize) / 2;
|
|
2932
|
+
const offsetY = pos.y + (pos.height - iconSize) / 2;
|
|
2933
|
+
const wrappedSvg = `<g transform="translate(${offsetX}, ${offsetY})">
|
|
2934
|
+
<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
2935
|
+
${this.extractSvgContent(iconSvg)}
|
|
2936
|
+
</svg>
|
|
2937
|
+
</g>`;
|
|
2938
|
+
return wrappedSvg;
|
|
2939
|
+
}
|
|
2940
|
+
renderIconButton(node, pos) {
|
|
2941
|
+
const iconName = String(node.props.icon || "help-circle");
|
|
2942
|
+
const variant = String(node.props.variant || "default");
|
|
2943
|
+
const size = String(node.props.size || "md");
|
|
2944
|
+
const disabled = String(node.props.disabled || "false") === "true";
|
|
2945
|
+
const bgColorMap = {
|
|
2946
|
+
"primary": "rgba(59, 130, 246, 0.85)",
|
|
2947
|
+
"danger": "rgba(239, 68, 68, 0.85)",
|
|
2948
|
+
"default": "rgba(226, 232, 240, 0.9)"
|
|
2949
|
+
};
|
|
2950
|
+
const bgColor = bgColorMap[variant] || bgColorMap["default"];
|
|
2951
|
+
const iconColorMap = {
|
|
2952
|
+
"primary": "#FFFFFF",
|
|
2953
|
+
"danger": "#FFFFFF",
|
|
2954
|
+
"default": "rgba(30, 41, 59, 0.75)"
|
|
2955
|
+
};
|
|
2956
|
+
const iconColor = iconColorMap[variant] || iconColorMap["default"];
|
|
2957
|
+
const borderColorMap = {
|
|
2958
|
+
"primary": "rgba(59, 130, 246, 0.7)",
|
|
2959
|
+
"danger": "rgba(239, 68, 68, 0.7)",
|
|
2960
|
+
"default": "rgba(100, 116, 139, 0.4)"
|
|
2961
|
+
};
|
|
2962
|
+
const borderColor = borderColorMap[variant] || borderColorMap["default"];
|
|
2963
|
+
const opacity = disabled ? "0.5" : "1";
|
|
2964
|
+
const iconSvg = getIcon(iconName);
|
|
2965
|
+
const sizeMap = { "sm": 28, "md": 32, "lg": 40 };
|
|
2966
|
+
const buttonSize = sizeMap[size] || 32;
|
|
2967
|
+
const radius = 6;
|
|
2968
|
+
let svg = `<g opacity="${opacity}">
|
|
2969
|
+
<!-- IconButton background -->
|
|
2970
|
+
<rect x="${pos.x}" y="${pos.y}" width="${buttonSize}" height="${buttonSize}" rx="${radius}" fill="${bgColor}" stroke="${borderColor}" stroke-width="1"/>`;
|
|
2971
|
+
if (iconSvg) {
|
|
2972
|
+
const iconSize = buttonSize * 0.6;
|
|
2973
|
+
const offsetX = pos.x + (buttonSize - iconSize) / 2;
|
|
2974
|
+
const offsetY = pos.y + (buttonSize - iconSize) / 2;
|
|
2975
|
+
svg += `
|
|
2976
|
+
<!-- Icon -->
|
|
2977
|
+
<g transform="translate(${offsetX}, ${offsetY})">
|
|
2978
|
+
<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
2979
|
+
${this.extractSvgContent(iconSvg)}
|
|
2980
|
+
</svg>
|
|
2981
|
+
</g>`;
|
|
2982
|
+
}
|
|
2983
|
+
svg += "\n </g>";
|
|
2984
|
+
return svg;
|
|
2985
|
+
}
|
|
2986
|
+
/**
|
|
2987
|
+
* Extract SVG path/element content from a full SVG string
|
|
2988
|
+
* Removes the outer <svg> tag but keeps the content
|
|
2989
|
+
*/
|
|
2990
|
+
extractSvgContent(svgString) {
|
|
2991
|
+
const match = svgString.match(/<svg[^>]*>([\s\S]*?)<\/svg>/);
|
|
2992
|
+
return match ? match[1] : svgString;
|
|
2993
|
+
}
|
|
2994
|
+
resolveSpacing(spacing) {
|
|
2995
|
+
const spacingMap = {
|
|
2996
|
+
none: 0,
|
|
2997
|
+
xs: 4,
|
|
2998
|
+
sm: 8,
|
|
2999
|
+
md: 16,
|
|
3000
|
+
lg: 24,
|
|
3001
|
+
xl: 32
|
|
3002
|
+
};
|
|
3003
|
+
if (!spacing) return spacingMap[this.ir.project.theme.spacing] || 16;
|
|
3004
|
+
const value = spacingMap[spacing];
|
|
3005
|
+
return value !== void 0 ? value : spacingMap.md;
|
|
3006
|
+
}
|
|
3007
|
+
escapeXml(text) {
|
|
3008
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
3009
|
+
}
|
|
3010
|
+
};
|
|
3011
|
+
function renderToSVG(ir, layout, options) {
|
|
3012
|
+
const renderer = new SVGRenderer(ir, layout, options);
|
|
3013
|
+
return renderer.render();
|
|
3014
|
+
}
|
|
3015
|
+
function createSVGElement(tag, attrs, children = []) {
|
|
3016
|
+
const attrStr = Object.entries(attrs).map(([key, value]) => `${key}="${value}"`).join(" ");
|
|
3017
|
+
return `<${tag} ${attrStr}>${children.join("")}</${tag}>`;
|
|
3018
|
+
}
|
|
3019
|
+
function buildSVG(component) {
|
|
3020
|
+
const children = component.children?.map(buildSVG) ?? [];
|
|
3021
|
+
if (component.text) {
|
|
3022
|
+
children.push(component.text);
|
|
3023
|
+
}
|
|
3024
|
+
return createSVGElement(component.tag, component.attrs, children);
|
|
3025
|
+
}
|
|
3026
|
+
|
|
3027
|
+
// src/index.ts
|
|
3028
|
+
var version = "0.0.1";
|
|
3029
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
3030
|
+
0 && (module.exports = {
|
|
3031
|
+
IRGenerator,
|
|
3032
|
+
LayoutEngine,
|
|
3033
|
+
SVGRenderer,
|
|
3034
|
+
buildSVG,
|
|
3035
|
+
calculateLayout,
|
|
3036
|
+
createSVGElement,
|
|
3037
|
+
generateIR,
|
|
3038
|
+
parseWireDSL,
|
|
3039
|
+
renderToSVG,
|
|
3040
|
+
resolveGridPosition,
|
|
3041
|
+
version
|
|
3042
|
+
});
|