@wire-dsl/engine 0.2.4 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1646 -475
- package/dist/index.d.cts +40 -6
- package/dist/index.d.ts +40 -6
- package/dist/index.js +1649 -475
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -45,20 +45,21 @@ var SourceMapBuilder = class {
|
|
|
45
45
|
return nodeId;
|
|
46
46
|
}
|
|
47
47
|
/**
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
48
|
+
* Generate semantic node ID based on type and subtype
|
|
49
|
+
* Format: {type}-{subtype}-{counter} or {type}-{counter}
|
|
50
|
+
*
|
|
51
|
+
* Examples:
|
|
52
|
+
* - project → "project"
|
|
53
|
+
* - theme → "theme"
|
|
54
|
+
* - mocks → "mocks"
|
|
55
|
+
* - colors → "colors"
|
|
56
|
+
* - screen → "screen-0", "screen-1"
|
|
57
|
+
* - component Button → "component-button-0", "component-button-1"
|
|
58
|
+
* - layout stack → "layout-stack-0", "layout-stack-1"
|
|
59
|
+
* - cell → "cell-0", "cell-1"
|
|
60
|
+
* - component-definition → "define-MyButton"
|
|
61
|
+
* - layout-definition → "define-layout-MyShell"
|
|
62
|
+
*/
|
|
62
63
|
generateNodeId(type, metadata) {
|
|
63
64
|
switch (type) {
|
|
64
65
|
case "project":
|
|
@@ -94,6 +95,8 @@ var SourceMapBuilder = class {
|
|
|
94
95
|
}
|
|
95
96
|
case "component-definition":
|
|
96
97
|
return `define-${metadata?.name || "unknown"}`;
|
|
98
|
+
case "layout-definition":
|
|
99
|
+
return `define-layout-${metadata?.name || "unknown"}`;
|
|
97
100
|
default:
|
|
98
101
|
return `${type}-0`;
|
|
99
102
|
}
|
|
@@ -179,7 +182,7 @@ var SourceMapBuilder = class {
|
|
|
179
182
|
}
|
|
180
183
|
/**
|
|
181
184
|
* Calculate insertionPoints for all container nodes
|
|
182
|
-
* Container nodes: project, screen, layout, cell, component-definition
|
|
185
|
+
* Container nodes: project, screen, layout, cell, component-definition, layout-definition
|
|
183
186
|
*/
|
|
184
187
|
calculateAllInsertionPoints() {
|
|
185
188
|
const containerTypes = [
|
|
@@ -187,7 +190,8 @@ var SourceMapBuilder = class {
|
|
|
187
190
|
"screen",
|
|
188
191
|
"layout",
|
|
189
192
|
"cell",
|
|
190
|
-
"component-definition"
|
|
193
|
+
"component-definition",
|
|
194
|
+
"layout-definition"
|
|
191
195
|
];
|
|
192
196
|
for (const entry of this.entries) {
|
|
193
197
|
if (containerTypes.includes(entry.type)) {
|
|
@@ -413,19 +417,23 @@ var SourceMapBuilder = class {
|
|
|
413
417
|
};
|
|
414
418
|
|
|
415
419
|
// src/parser/index.ts
|
|
416
|
-
var Project = createToken({ name: "Project", pattern: /project/ });
|
|
417
|
-
var Screen = createToken({ name: "Screen", pattern: /screen/ });
|
|
418
|
-
var Layout = createToken({ name: "Layout", pattern: /layout/ });
|
|
419
|
-
var Component = createToken({ name: "Component", pattern: /component/ });
|
|
420
|
+
var Project = createToken({ name: "Project", pattern: /project\b/ });
|
|
421
|
+
var Screen = createToken({ name: "Screen", pattern: /screen\b/ });
|
|
422
|
+
var Layout = createToken({ name: "Layout", pattern: /layout\b/ });
|
|
423
|
+
var Component = createToken({ name: "Component", pattern: /component\b/ });
|
|
420
424
|
var ComponentKeyword = createToken({
|
|
421
425
|
name: "ComponentKeyword",
|
|
422
426
|
pattern: /Component\b/
|
|
423
427
|
});
|
|
424
|
-
var
|
|
425
|
-
|
|
426
|
-
|
|
428
|
+
var LayoutKeyword = createToken({
|
|
429
|
+
name: "LayoutKeyword",
|
|
430
|
+
pattern: /Layout\b/
|
|
431
|
+
});
|
|
432
|
+
var Define = createToken({ name: "Define", pattern: /define\b/ });
|
|
433
|
+
var Style = createToken({ name: "Style", pattern: /style\b/ });
|
|
434
|
+
var Mocks = createToken({ name: "Mocks", pattern: /mocks\b/ });
|
|
427
435
|
var Colors = createToken({ name: "Colors", pattern: /colors(?=\s*\{)/ });
|
|
428
|
-
var Cell = createToken({ name: "Cell", pattern: /cell/ });
|
|
436
|
+
var Cell = createToken({ name: "Cell", pattern: /cell\b/ });
|
|
429
437
|
var LCurly = createToken({ name: "LCurly", pattern: /{/ });
|
|
430
438
|
var RCurly = createToken({ name: "RCurly", pattern: /}/ });
|
|
431
439
|
var LParen = createToken({ name: "LParen", pattern: /\(/ });
|
|
@@ -472,6 +480,7 @@ var allTokens = [
|
|
|
472
480
|
Project,
|
|
473
481
|
Screen,
|
|
474
482
|
Layout,
|
|
483
|
+
LayoutKeyword,
|
|
475
484
|
ComponentKeyword,
|
|
476
485
|
Component,
|
|
477
486
|
Define,
|
|
@@ -504,6 +513,7 @@ var WireDSLParser = class extends CstParser {
|
|
|
504
513
|
this.MANY(() => {
|
|
505
514
|
this.OR([
|
|
506
515
|
{ ALT: () => this.SUBRULE(this.definedComponent) },
|
|
516
|
+
{ ALT: () => this.SUBRULE(this.definedLayout) },
|
|
507
517
|
{ ALT: () => this.SUBRULE(this.styleDecl) },
|
|
508
518
|
{ ALT: () => this.SUBRULE(this.mocksDecl) },
|
|
509
519
|
{ ALT: () => this.SUBRULE(this.colorsDecl) },
|
|
@@ -572,6 +582,15 @@ var WireDSLParser = class extends CstParser {
|
|
|
572
582
|
]);
|
|
573
583
|
this.CONSUME(RCurly);
|
|
574
584
|
});
|
|
585
|
+
// define Layout "ScreenShell" { layout split { ... } }
|
|
586
|
+
this.definedLayout = this.RULE("definedLayout", () => {
|
|
587
|
+
this.CONSUME(Define);
|
|
588
|
+
this.CONSUME(LayoutKeyword, { LABEL: "layoutKeyword" });
|
|
589
|
+
this.CONSUME(StringLiteral, { LABEL: "layoutName" });
|
|
590
|
+
this.CONSUME(LCurly);
|
|
591
|
+
this.SUBRULE(this.layout);
|
|
592
|
+
this.CONSUME(RCurly);
|
|
593
|
+
});
|
|
575
594
|
// screen Main(background: white) { ... }
|
|
576
595
|
this.screen = this.RULE("screen", () => {
|
|
577
596
|
this.CONSUME(Screen);
|
|
@@ -660,6 +679,7 @@ var WireDSLVisitor = class extends BaseCstVisitor {
|
|
|
660
679
|
const mocks = {};
|
|
661
680
|
const colors = {};
|
|
662
681
|
const definedComponents = [];
|
|
682
|
+
const definedLayouts = [];
|
|
663
683
|
const screens = [];
|
|
664
684
|
if (ctx.styleDecl && ctx.styleDecl.length > 0) {
|
|
665
685
|
const styleBlock = this.visit(ctx.styleDecl[0]);
|
|
@@ -678,6 +698,11 @@ var WireDSLVisitor = class extends BaseCstVisitor {
|
|
|
678
698
|
definedComponents.push(this.visit(comp));
|
|
679
699
|
});
|
|
680
700
|
}
|
|
701
|
+
if (ctx.definedLayout) {
|
|
702
|
+
ctx.definedLayout.forEach((layoutDef) => {
|
|
703
|
+
definedLayouts.push(this.visit(layoutDef));
|
|
704
|
+
});
|
|
705
|
+
}
|
|
681
706
|
if (ctx.screen) {
|
|
682
707
|
ctx.screen.forEach((screen) => {
|
|
683
708
|
screens.push(this.visit(screen));
|
|
@@ -690,6 +715,7 @@ var WireDSLVisitor = class extends BaseCstVisitor {
|
|
|
690
715
|
mocks,
|
|
691
716
|
colors,
|
|
692
717
|
definedComponents,
|
|
718
|
+
definedLayouts,
|
|
693
719
|
screens
|
|
694
720
|
};
|
|
695
721
|
}
|
|
@@ -754,6 +780,15 @@ var WireDSLVisitor = class extends BaseCstVisitor {
|
|
|
754
780
|
body
|
|
755
781
|
};
|
|
756
782
|
}
|
|
783
|
+
definedLayout(ctx) {
|
|
784
|
+
const name = ctx.layoutName[0].image.slice(1, -1);
|
|
785
|
+
const body = this.visit(ctx.layout[0]);
|
|
786
|
+
return {
|
|
787
|
+
type: "definedLayout",
|
|
788
|
+
name,
|
|
789
|
+
body
|
|
790
|
+
};
|
|
791
|
+
}
|
|
757
792
|
screen(ctx) {
|
|
758
793
|
const params = ctx.paramList ? this.visit(ctx.paramList[0]) : {};
|
|
759
794
|
return {
|
|
@@ -884,6 +919,7 @@ var WireDSLVisitorWithSourceMap = class extends WireDSLVisitor {
|
|
|
884
919
|
constructor(sourceMapBuilder) {
|
|
885
920
|
super();
|
|
886
921
|
this.definedComponentNames = /* @__PURE__ */ new Set();
|
|
922
|
+
this.definedLayoutNames = /* @__PURE__ */ new Set();
|
|
887
923
|
this.sourceMapBuilder = sourceMapBuilder;
|
|
888
924
|
}
|
|
889
925
|
project(ctx) {
|
|
@@ -892,6 +928,7 @@ var WireDSLVisitorWithSourceMap = class extends WireDSLVisitor {
|
|
|
892
928
|
const mocks = {};
|
|
893
929
|
const colors = {};
|
|
894
930
|
const definedComponents = [];
|
|
931
|
+
const definedLayouts = [];
|
|
895
932
|
const screens = [];
|
|
896
933
|
const tokens = {
|
|
897
934
|
keyword: ctx.Project[0],
|
|
@@ -906,6 +943,8 @@ var WireDSLVisitorWithSourceMap = class extends WireDSLVisitor {
|
|
|
906
943
|
colors: {},
|
|
907
944
|
definedComponents: [],
|
|
908
945
|
// Will be filled after push
|
|
946
|
+
definedLayouts: [],
|
|
947
|
+
// Will be filled after push
|
|
909
948
|
screens: []
|
|
910
949
|
// Will be filled after push
|
|
911
950
|
};
|
|
@@ -935,6 +974,11 @@ var WireDSLVisitorWithSourceMap = class extends WireDSLVisitor {
|
|
|
935
974
|
ast.definedComponents.push(this.visit(comp));
|
|
936
975
|
});
|
|
937
976
|
}
|
|
977
|
+
if (ctx.definedLayout) {
|
|
978
|
+
ctx.definedLayout.forEach((layoutDef) => {
|
|
979
|
+
ast.definedLayouts.push(this.visit(layoutDef));
|
|
980
|
+
});
|
|
981
|
+
}
|
|
938
982
|
if (ctx.screen) {
|
|
939
983
|
ctx.screen.forEach((screen) => {
|
|
940
984
|
ast.screens.push(this.visit(screen));
|
|
@@ -1179,6 +1223,35 @@ var WireDSLVisitorWithSourceMap = class extends WireDSLVisitor {
|
|
|
1179
1223
|
}
|
|
1180
1224
|
return ast;
|
|
1181
1225
|
}
|
|
1226
|
+
definedLayout(ctx) {
|
|
1227
|
+
const name = ctx.layoutName[0].image.slice(1, -1);
|
|
1228
|
+
this.definedLayoutNames.add(name);
|
|
1229
|
+
const tokens = {
|
|
1230
|
+
keyword: ctx.Define[0],
|
|
1231
|
+
name: ctx.layoutName[0],
|
|
1232
|
+
body: ctx.RCurly[0]
|
|
1233
|
+
};
|
|
1234
|
+
const ast = {
|
|
1235
|
+
type: "definedLayout",
|
|
1236
|
+
name,
|
|
1237
|
+
body: {}
|
|
1238
|
+
// Will be filled after push
|
|
1239
|
+
};
|
|
1240
|
+
if (this.sourceMapBuilder) {
|
|
1241
|
+
const nodeId = this.sourceMapBuilder.addNode(
|
|
1242
|
+
"layout-definition",
|
|
1243
|
+
tokens,
|
|
1244
|
+
{ name }
|
|
1245
|
+
);
|
|
1246
|
+
ast._meta = { nodeId };
|
|
1247
|
+
this.sourceMapBuilder.pushParent(nodeId);
|
|
1248
|
+
}
|
|
1249
|
+
ast.body = this.visit(ctx.layout[0]);
|
|
1250
|
+
if (this.sourceMapBuilder) {
|
|
1251
|
+
this.sourceMapBuilder.popParent();
|
|
1252
|
+
}
|
|
1253
|
+
return ast;
|
|
1254
|
+
}
|
|
1182
1255
|
// Override styleDecl to capture style block in SourceMap
|
|
1183
1256
|
styleDecl(ctx) {
|
|
1184
1257
|
const style = {};
|
|
@@ -1358,6 +1431,13 @@ function buildLayoutRulesFromMetadata() {
|
|
|
1358
1431
|
var BUILT_IN_COMPONENTS = new Set(Object.keys(COMPONENTS));
|
|
1359
1432
|
var COMPONENT_RULES = buildComponentRulesFromMetadata();
|
|
1360
1433
|
var LAYOUT_RULES = buildLayoutRulesFromMetadata();
|
|
1434
|
+
var BUILT_IN_LAYOUTS = new Set(Object.keys(LAYOUTS));
|
|
1435
|
+
function isPascalCaseIdentifier(name) {
|
|
1436
|
+
return /^[A-Z][A-Za-z0-9]*$/.test(name);
|
|
1437
|
+
}
|
|
1438
|
+
function isValidDefinedLayoutName(name) {
|
|
1439
|
+
return /^[a-z][a-z0-9_]*$/.test(name);
|
|
1440
|
+
}
|
|
1361
1441
|
function toFallbackRange() {
|
|
1362
1442
|
return {
|
|
1363
1443
|
start: { line: 1, column: 0 },
|
|
@@ -1449,6 +1529,17 @@ function isBooleanLike(value) {
|
|
|
1449
1529
|
const normalized = String(value).trim().toLowerCase();
|
|
1450
1530
|
return normalized === "true" || normalized === "false";
|
|
1451
1531
|
}
|
|
1532
|
+
function parseBooleanLike(value, fallback = false) {
|
|
1533
|
+
if (typeof value === "number") {
|
|
1534
|
+
if (value === 1) return true;
|
|
1535
|
+
if (value === 0) return false;
|
|
1536
|
+
return fallback;
|
|
1537
|
+
}
|
|
1538
|
+
const normalized = String(value).trim().toLowerCase();
|
|
1539
|
+
if (normalized === "true") return true;
|
|
1540
|
+
if (normalized === "false") return false;
|
|
1541
|
+
return fallback;
|
|
1542
|
+
}
|
|
1452
1543
|
function getPropertyRange(entry, propertyName, mode = "full") {
|
|
1453
1544
|
const prop = entry?.properties?.[propertyName];
|
|
1454
1545
|
if (!prop) return entry?.range || toFallbackRange();
|
|
@@ -1466,21 +1557,55 @@ function validateSemanticDiagnostics(ast, sourceMap) {
|
|
|
1466
1557
|
const diagnostics = [];
|
|
1467
1558
|
const sourceMapByNodeId = new Map(sourceMap.map((entry) => [entry.nodeId, entry]));
|
|
1468
1559
|
const definedComponents = new Set(ast.definedComponents.map((dc) => dc.name));
|
|
1469
|
-
const
|
|
1560
|
+
const definedLayouts = new Set(ast.definedLayouts.map((dl) => dl.name));
|
|
1561
|
+
const emitDiagnostic = (severity, message, code, range, nodeId, suggestion) => {
|
|
1470
1562
|
diagnostics.push({
|
|
1471
1563
|
message,
|
|
1472
1564
|
code,
|
|
1473
|
-
severity
|
|
1565
|
+
severity,
|
|
1474
1566
|
phase: "semantic",
|
|
1475
1567
|
range,
|
|
1476
1568
|
nodeId,
|
|
1477
1569
|
suggestion
|
|
1478
1570
|
});
|
|
1479
1571
|
};
|
|
1480
|
-
const
|
|
1572
|
+
const emitWarning = (message, code, range, nodeId, suggestion) => emitDiagnostic("warning", message, code, range, nodeId, suggestion);
|
|
1573
|
+
const emitError = (message, code, range, nodeId, suggestion) => emitDiagnostic("error", message, code, range, nodeId, suggestion);
|
|
1574
|
+
const countChildrenSlots = (layout) => {
|
|
1575
|
+
let count = 0;
|
|
1576
|
+
for (const child of layout.children) {
|
|
1577
|
+
if (child.type === "component") {
|
|
1578
|
+
if (child.componentType === "Children") count += 1;
|
|
1579
|
+
} else if (child.type === "layout") {
|
|
1580
|
+
count += countChildrenSlots(child);
|
|
1581
|
+
} else if (child.type === "cell") {
|
|
1582
|
+
for (const cellChild of child.children) {
|
|
1583
|
+
if (cellChild.type === "component") {
|
|
1584
|
+
if (cellChild.componentType === "Children") count += 1;
|
|
1585
|
+
} else if (cellChild.type === "layout") {
|
|
1586
|
+
count += countChildrenSlots(cellChild);
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
return count;
|
|
1592
|
+
};
|
|
1593
|
+
const checkComponent = (component, insideDefinedLayout) => {
|
|
1481
1594
|
const nodeId = component._meta?.nodeId;
|
|
1482
1595
|
const entry = nodeId ? sourceMapByNodeId.get(nodeId) : void 0;
|
|
1483
1596
|
const componentType = component.componentType;
|
|
1597
|
+
if (componentType === "Children") {
|
|
1598
|
+
if (!insideDefinedLayout) {
|
|
1599
|
+
emitError(
|
|
1600
|
+
'Component "Children" can only be used inside a define Layout body.',
|
|
1601
|
+
"CHILDREN_SLOT_OUTSIDE_LAYOUT_DEFINITION",
|
|
1602
|
+
entry?.nameRange || entry?.range || toFallbackRange(),
|
|
1603
|
+
nodeId,
|
|
1604
|
+
"Move this placeholder into a define Layout block."
|
|
1605
|
+
);
|
|
1606
|
+
}
|
|
1607
|
+
return;
|
|
1608
|
+
}
|
|
1484
1609
|
if (!BUILT_IN_COMPONENTS.has(componentType) && !definedComponents.has(componentType)) {
|
|
1485
1610
|
emitWarning(
|
|
1486
1611
|
`Component "${componentType}" is not a built-in component and has no local definition.`,
|
|
@@ -1521,7 +1646,9 @@ function validateSemanticDiagnostics(ast, sourceMap) {
|
|
|
1521
1646
|
const enumValues = rules.enumProps?.[propName];
|
|
1522
1647
|
if (enumValues) {
|
|
1523
1648
|
const normalizedValue = String(propValue);
|
|
1524
|
-
|
|
1649
|
+
const isCustomVariantFromColors = propName === "variant" && !enumValues.includes(normalizedValue) && Object.prototype.hasOwnProperty.call(ast.colors || {}, normalizedValue);
|
|
1650
|
+
const isPropReference = normalizedValue.startsWith("prop_");
|
|
1651
|
+
if (!enumValues.includes(normalizedValue) && !isCustomVariantFromColors && !isPropReference) {
|
|
1525
1652
|
emitWarning(
|
|
1526
1653
|
`Invalid value "${normalizedValue}" for property "${propName}" in component "${componentType}".`,
|
|
1527
1654
|
"COMPONENT_INVALID_PROPERTY_VALUE",
|
|
@@ -1541,11 +1668,40 @@ function validateSemanticDiagnostics(ast, sourceMap) {
|
|
|
1541
1668
|
);
|
|
1542
1669
|
}
|
|
1543
1670
|
}
|
|
1671
|
+
if (componentType === "Table") {
|
|
1672
|
+
const hasCaption = String(component.props.caption || "").trim().length > 0;
|
|
1673
|
+
const hasPagination = parseBooleanLike(component.props.pagination ?? "false", false);
|
|
1674
|
+
if (hasCaption && hasPagination) {
|
|
1675
|
+
const rawPaginationAlign = String(component.props.paginationAlign || "right");
|
|
1676
|
+
const paginationAlign = rawPaginationAlign === "left" || rawPaginationAlign === "center" || rawPaginationAlign === "right" ? rawPaginationAlign : "right";
|
|
1677
|
+
const rawCaptionAlign = String(component.props.captionAlign || "");
|
|
1678
|
+
const captionAlign = rawCaptionAlign === "left" || rawCaptionAlign === "center" || rawCaptionAlign === "right" ? rawCaptionAlign : paginationAlign === "left" ? "right" : "left";
|
|
1679
|
+
if (captionAlign === paginationAlign) {
|
|
1680
|
+
emitWarning(
|
|
1681
|
+
`Table footer collision: "captionAlign" and "paginationAlign" both resolve to "${captionAlign}".`,
|
|
1682
|
+
"TABLE_FOOTER_ALIGNMENT_COLLISION",
|
|
1683
|
+
entry?.range || toFallbackRange(),
|
|
1684
|
+
nodeId,
|
|
1685
|
+
"Use different alignments to avoid visual overlap."
|
|
1686
|
+
);
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1544
1690
|
};
|
|
1545
|
-
const checkLayout = (layout) => {
|
|
1691
|
+
const checkLayout = (layout, insideDefinedLayout) => {
|
|
1546
1692
|
const nodeId = layout._meta?.nodeId;
|
|
1547
1693
|
const entry = nodeId ? sourceMapByNodeId.get(nodeId) : void 0;
|
|
1548
1694
|
const rules = LAYOUT_RULES[layout.layoutType];
|
|
1695
|
+
const isDefinedLayoutUsage = definedLayouts.has(layout.layoutType);
|
|
1696
|
+
if (isDefinedLayoutUsage && layout.children.length !== 1) {
|
|
1697
|
+
emitError(
|
|
1698
|
+
`Layout "${layout.layoutType}" expects exactly one child for its Children slot.`,
|
|
1699
|
+
"LAYOUT_DEFINITION_CHILDREN_ARITY",
|
|
1700
|
+
entry?.bodyRange || entry?.range || toFallbackRange(),
|
|
1701
|
+
nodeId,
|
|
1702
|
+
"Provide exactly one nested child block when using this layout."
|
|
1703
|
+
);
|
|
1704
|
+
}
|
|
1549
1705
|
if (layout.children.length === 0) {
|
|
1550
1706
|
emitWarning(
|
|
1551
1707
|
`Layout "${layout.layoutType}" is empty.`,
|
|
@@ -1555,7 +1711,7 @@ function validateSemanticDiagnostics(ast, sourceMap) {
|
|
|
1555
1711
|
"Add at least one child: component, layout, or cell."
|
|
1556
1712
|
);
|
|
1557
1713
|
}
|
|
1558
|
-
if (!rules) {
|
|
1714
|
+
if (!rules && !isDefinedLayoutUsage) {
|
|
1559
1715
|
emitWarning(
|
|
1560
1716
|
`Layout type "${layout.layoutType}" is not recognized by semantic validation rules.`,
|
|
1561
1717
|
"LAYOUT_UNKNOWN_TYPE",
|
|
@@ -1563,7 +1719,7 @@ function validateSemanticDiagnostics(ast, sourceMap) {
|
|
|
1563
1719
|
nodeId,
|
|
1564
1720
|
`Use one of: ${Object.keys(LAYOUT_RULES).join(", ")}.`
|
|
1565
1721
|
);
|
|
1566
|
-
} else {
|
|
1722
|
+
} else if (rules) {
|
|
1567
1723
|
const missingRequiredParams = getMissingRequiredNames(rules.requiredParams, layout.params);
|
|
1568
1724
|
if (missingRequiredParams.length > 0) {
|
|
1569
1725
|
emitWarning(
|
|
@@ -1576,6 +1732,16 @@ function validateSemanticDiagnostics(ast, sourceMap) {
|
|
|
1576
1732
|
}
|
|
1577
1733
|
const allowed = new Set(rules.allowedParams);
|
|
1578
1734
|
for (const [paramName, paramValue] of Object.entries(layout.params)) {
|
|
1735
|
+
if (layout.layoutType === "split" && paramName === "sidebar") {
|
|
1736
|
+
emitError(
|
|
1737
|
+
'Split parameter "sidebar" was removed. Use "left" or "right" instead.',
|
|
1738
|
+
"LAYOUT_SPLIT_SIDEBAR_DEPRECATED",
|
|
1739
|
+
getPropertyRange(entry, paramName, "name"),
|
|
1740
|
+
nodeId,
|
|
1741
|
+
"Example: layout split(left: 260) { ... }"
|
|
1742
|
+
);
|
|
1743
|
+
continue;
|
|
1744
|
+
}
|
|
1579
1745
|
if (!allowed.has(paramName)) {
|
|
1580
1746
|
emitWarning(
|
|
1581
1747
|
`Parameter "${paramName}" is not recognized for layout "${layout.layoutType}".`,
|
|
@@ -1592,7 +1758,7 @@ function validateSemanticDiagnostics(ast, sourceMap) {
|
|
|
1592
1758
|
const enumValues = rules.enumParams?.[paramName];
|
|
1593
1759
|
if (enumValues) {
|
|
1594
1760
|
const normalizedValue = String(paramValue);
|
|
1595
|
-
if (!enumValues.includes(normalizedValue)) {
|
|
1761
|
+
if (!enumValues.includes(normalizedValue) && !normalizedValue.startsWith("prop_")) {
|
|
1596
1762
|
emitWarning(
|
|
1597
1763
|
`Invalid value "${normalizedValue}" for parameter "${paramName}" in layout "${layout.layoutType}".`,
|
|
1598
1764
|
"LAYOUT_INVALID_PARAMETER_VALUE",
|
|
@@ -1614,31 +1780,62 @@ function validateSemanticDiagnostics(ast, sourceMap) {
|
|
|
1614
1780
|
);
|
|
1615
1781
|
}
|
|
1616
1782
|
}
|
|
1617
|
-
if (layout.layoutType === "split" && paramName === "
|
|
1618
|
-
const
|
|
1619
|
-
if (!Number.isFinite(
|
|
1783
|
+
if (layout.layoutType === "split" && (paramName === "left" || paramName === "right")) {
|
|
1784
|
+
const splitSize = Number(paramValue);
|
|
1785
|
+
if (!Number.isFinite(splitSize) || splitSize <= 0) {
|
|
1620
1786
|
emitWarning(
|
|
1621
|
-
|
|
1622
|
-
"
|
|
1787
|
+
`Split "${paramName}" must be a positive number. Falling back to 250.`,
|
|
1788
|
+
"LAYOUT_SPLIT_WIDTH_INVALID",
|
|
1623
1789
|
getPropertyRange(entry, paramName, "value"),
|
|
1624
1790
|
nodeId,
|
|
1625
|
-
|
|
1791
|
+
`Use a value like ${paramName}: 260.`
|
|
1626
1792
|
);
|
|
1627
1793
|
}
|
|
1628
1794
|
}
|
|
1629
1795
|
}
|
|
1796
|
+
if (layout.layoutType === "split") {
|
|
1797
|
+
const hasLeft = layout.params.left !== void 0;
|
|
1798
|
+
const hasRight = layout.params.right !== void 0;
|
|
1799
|
+
if (!hasLeft && !hasRight) {
|
|
1800
|
+
emitError(
|
|
1801
|
+
'Split layout requires exactly one fixed side width: "left" or "right".',
|
|
1802
|
+
"LAYOUT_SPLIT_SIDE_REQUIRED",
|
|
1803
|
+
entry?.nameRange || entry?.range || toFallbackRange(),
|
|
1804
|
+
nodeId,
|
|
1805
|
+
"Add either left: <number> or right: <number>."
|
|
1806
|
+
);
|
|
1807
|
+
}
|
|
1808
|
+
if (hasLeft && hasRight) {
|
|
1809
|
+
emitError(
|
|
1810
|
+
'Split layout accepts only one fixed side width: use either "left" or "right", not both.',
|
|
1811
|
+
"LAYOUT_SPLIT_SIDE_CONFLICT",
|
|
1812
|
+
entry?.nameRange || entry?.range || toFallbackRange(),
|
|
1813
|
+
nodeId,
|
|
1814
|
+
"Remove one of the two parameters."
|
|
1815
|
+
);
|
|
1816
|
+
}
|
|
1817
|
+
if (layout.children.length !== 2) {
|
|
1818
|
+
emitError(
|
|
1819
|
+
`Split layout requires exactly 2 children, received ${layout.children.length}.`,
|
|
1820
|
+
"LAYOUT_SPLIT_CHILDREN_ARITY",
|
|
1821
|
+
entry?.bodyRange || entry?.range || toFallbackRange(),
|
|
1822
|
+
nodeId,
|
|
1823
|
+
"Provide exactly two child blocks (left/right content)."
|
|
1824
|
+
);
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1630
1827
|
}
|
|
1631
1828
|
for (const child of layout.children) {
|
|
1632
1829
|
if (child.type === "component") {
|
|
1633
|
-
checkComponent(child);
|
|
1830
|
+
checkComponent(child, insideDefinedLayout);
|
|
1634
1831
|
} else if (child.type === "layout") {
|
|
1635
|
-
checkLayout(child);
|
|
1832
|
+
checkLayout(child, insideDefinedLayout);
|
|
1636
1833
|
} else if (child.type === "cell") {
|
|
1637
|
-
checkCell(child);
|
|
1834
|
+
checkCell(child, insideDefinedLayout);
|
|
1638
1835
|
}
|
|
1639
1836
|
}
|
|
1640
1837
|
};
|
|
1641
|
-
const checkCell = (cell) => {
|
|
1838
|
+
const checkCell = (cell, insideDefinedLayout) => {
|
|
1642
1839
|
const nodeId = cell._meta?.nodeId;
|
|
1643
1840
|
const entry = nodeId ? sourceMapByNodeId.get(nodeId) : void 0;
|
|
1644
1841
|
if (cell.props.span !== void 0) {
|
|
@@ -1654,12 +1851,54 @@ function validateSemanticDiagnostics(ast, sourceMap) {
|
|
|
1654
1851
|
}
|
|
1655
1852
|
}
|
|
1656
1853
|
for (const child of cell.children) {
|
|
1657
|
-
if (child.type === "component") checkComponent(child);
|
|
1658
|
-
if (child.type === "layout") checkLayout(child);
|
|
1854
|
+
if (child.type === "component") checkComponent(child, insideDefinedLayout);
|
|
1855
|
+
if (child.type === "layout") checkLayout(child, insideDefinedLayout);
|
|
1659
1856
|
}
|
|
1660
1857
|
};
|
|
1858
|
+
for (const componentDef of ast.definedComponents) {
|
|
1859
|
+
const nodeId = componentDef._meta?.nodeId;
|
|
1860
|
+
const entry = nodeId ? sourceMapByNodeId.get(nodeId) : void 0;
|
|
1861
|
+
if (!isPascalCaseIdentifier(componentDef.name)) {
|
|
1862
|
+
emitWarning(
|
|
1863
|
+
`Defined component "${componentDef.name}" should use PascalCase naming.`,
|
|
1864
|
+
"COMPONENT_DEFINITION_NAME_STYLE",
|
|
1865
|
+
entry?.nameRange || entry?.range || toFallbackRange(),
|
|
1866
|
+
nodeId,
|
|
1867
|
+
'Use a name like "MyComponent".'
|
|
1868
|
+
);
|
|
1869
|
+
}
|
|
1870
|
+
if (componentDef.body.type === "component") {
|
|
1871
|
+
checkComponent(componentDef.body, false);
|
|
1872
|
+
} else {
|
|
1873
|
+
checkLayout(componentDef.body, false);
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
for (const layoutDef of ast.definedLayouts) {
|
|
1877
|
+
const nodeId = layoutDef._meta?.nodeId;
|
|
1878
|
+
const entry = nodeId ? sourceMapByNodeId.get(nodeId) : void 0;
|
|
1879
|
+
if (!isValidDefinedLayoutName(layoutDef.name)) {
|
|
1880
|
+
emitError(
|
|
1881
|
+
`Defined layout "${layoutDef.name}" must match /^[a-z][a-z0-9_]*$/.`,
|
|
1882
|
+
"LAYOUT_DEFINITION_INVALID_NAME",
|
|
1883
|
+
entry?.nameRange || entry?.range || toFallbackRange(),
|
|
1884
|
+
nodeId,
|
|
1885
|
+
'Use names like "screen_default" or "appShell".'
|
|
1886
|
+
);
|
|
1887
|
+
}
|
|
1888
|
+
const childrenSlotCount = countChildrenSlots(layoutDef.body);
|
|
1889
|
+
if (childrenSlotCount !== 1) {
|
|
1890
|
+
emitError(
|
|
1891
|
+
`Defined layout "${layoutDef.name}" must contain exactly one "component Children" placeholder.`,
|
|
1892
|
+
"LAYOUT_DEFINITION_CHILDREN_SLOT_COUNT",
|
|
1893
|
+
entry?.bodyRange || entry?.range || toFallbackRange(),
|
|
1894
|
+
nodeId,
|
|
1895
|
+
'Add exactly one "component Children" in the layout body.'
|
|
1896
|
+
);
|
|
1897
|
+
}
|
|
1898
|
+
checkLayout(layoutDef.body, true);
|
|
1899
|
+
}
|
|
1661
1900
|
ast.screens.forEach((screen) => {
|
|
1662
|
-
checkLayout(screen.layout);
|
|
1901
|
+
checkLayout(screen.layout, false);
|
|
1663
1902
|
});
|
|
1664
1903
|
return diagnostics;
|
|
1665
1904
|
}
|
|
@@ -1676,7 +1915,7 @@ ${lexResult.errors.map((e) => e.message).join("\n")}`);
|
|
|
1676
1915
|
${parserInstance.errors.map((e) => e.message).join("\n")}`);
|
|
1677
1916
|
}
|
|
1678
1917
|
const ast = visitor.visit(cst);
|
|
1679
|
-
|
|
1918
|
+
validateDefinitionCycles(ast);
|
|
1680
1919
|
return ast;
|
|
1681
1920
|
}
|
|
1682
1921
|
function parseWireDSLWithSourceMap(input, filePath = "<input>", options) {
|
|
@@ -1707,7 +1946,7 @@ ${parserInstance.errors.map((e) => e.message).join("\n")}`);
|
|
|
1707
1946
|
const ast = visitorWithSourceMap.visit(cst);
|
|
1708
1947
|
const sourceMap = sourceMapBuilder.build();
|
|
1709
1948
|
try {
|
|
1710
|
-
|
|
1949
|
+
validateDefinitionCycles(ast);
|
|
1711
1950
|
} catch (error) {
|
|
1712
1951
|
const projectEntry = sourceMap.find((entry) => entry.type === "project");
|
|
1713
1952
|
diagnostics.push({
|
|
@@ -1730,83 +1969,101 @@ ${parserInstance.errors.map((e) => e.message).join("\n")}`);
|
|
|
1730
1969
|
}
|
|
1731
1970
|
return buildParseResult(ast, sourceMap, diagnostics);
|
|
1732
1971
|
}
|
|
1733
|
-
function
|
|
1734
|
-
if (!ast.definedComponents || ast.definedComponents.length === 0) {
|
|
1972
|
+
function validateDefinitionCycles(ast) {
|
|
1973
|
+
if ((!ast.definedComponents || ast.definedComponents.length === 0) && (!ast.definedLayouts || ast.definedLayouts.length === 0)) {
|
|
1735
1974
|
return;
|
|
1736
1975
|
}
|
|
1737
1976
|
const components = /* @__PURE__ */ new Map();
|
|
1977
|
+
const layouts = /* @__PURE__ */ new Map();
|
|
1738
1978
|
ast.definedComponents.forEach((comp) => {
|
|
1739
1979
|
components.set(comp.name, comp);
|
|
1740
1980
|
});
|
|
1981
|
+
ast.definedLayouts.forEach((layoutDef) => {
|
|
1982
|
+
layouts.set(layoutDef.name, layoutDef);
|
|
1983
|
+
});
|
|
1984
|
+
const makeComponentKey = (name) => `component:${name}`;
|
|
1985
|
+
const makeLayoutKey = (name) => `layout:${name}`;
|
|
1986
|
+
const displayKey = (key) => key.split(":")[1];
|
|
1987
|
+
const shouldTrackComponentDependency = (name) => components.has(name) && !BUILT_IN_COMPONENTS.has(name);
|
|
1988
|
+
const shouldTrackLayoutDependency = (name) => layouts.has(name) && !BUILT_IN_LAYOUTS.has(name);
|
|
1741
1989
|
const visited = /* @__PURE__ */ new Set();
|
|
1742
1990
|
const recursionStack = /* @__PURE__ */ new Set();
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
if (
|
|
1758
|
-
|
|
1759
|
-
if (cellChild.type === "component") {
|
|
1760
|
-
deps.add(cellChild.componentType);
|
|
1761
|
-
} else if (cellChild.type === "layout") {
|
|
1762
|
-
const nested = getComponentDependencies(cellChild);
|
|
1763
|
-
nested.forEach((d) => deps.add(d));
|
|
1764
|
-
}
|
|
1765
|
-
});
|
|
1991
|
+
const collectLayoutDependencies = (layout, deps) => {
|
|
1992
|
+
if (shouldTrackLayoutDependency(layout.layoutType)) {
|
|
1993
|
+
deps.add(makeLayoutKey(layout.layoutType));
|
|
1994
|
+
}
|
|
1995
|
+
for (const child of layout.children) {
|
|
1996
|
+
if (child.type === "component") {
|
|
1997
|
+
if (shouldTrackComponentDependency(child.componentType)) {
|
|
1998
|
+
deps.add(makeComponentKey(child.componentType));
|
|
1999
|
+
}
|
|
2000
|
+
} else if (child.type === "layout") {
|
|
2001
|
+
collectLayoutDependencies(child, deps);
|
|
2002
|
+
} else if (child.type === "cell") {
|
|
2003
|
+
for (const cellChild of child.children) {
|
|
2004
|
+
if (cellChild.type === "component") {
|
|
2005
|
+
if (shouldTrackComponentDependency(cellChild.componentType)) {
|
|
2006
|
+
deps.add(makeComponentKey(cellChild.componentType));
|
|
1766
2007
|
}
|
|
2008
|
+
} else if (cellChild.type === "layout") {
|
|
2009
|
+
collectLayoutDependencies(cellChild, deps);
|
|
1767
2010
|
}
|
|
1768
|
-
}
|
|
2011
|
+
}
|
|
1769
2012
|
}
|
|
1770
2013
|
}
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
if (
|
|
1775
|
-
const
|
|
1776
|
-
const
|
|
1777
|
-
return
|
|
2014
|
+
};
|
|
2015
|
+
const getDependencies = (key) => {
|
|
2016
|
+
const deps = /* @__PURE__ */ new Set();
|
|
2017
|
+
if (key.startsWith("component:")) {
|
|
2018
|
+
const name2 = key.slice("component:".length);
|
|
2019
|
+
const def2 = components.get(name2);
|
|
2020
|
+
if (!def2) return deps;
|
|
2021
|
+
if (def2.body.type === "component") {
|
|
2022
|
+
if (shouldTrackComponentDependency(def2.body.componentType)) {
|
|
2023
|
+
deps.add(makeComponentKey(def2.body.componentType));
|
|
2024
|
+
}
|
|
2025
|
+
} else {
|
|
2026
|
+
collectLayoutDependencies(def2.body, deps);
|
|
2027
|
+
}
|
|
2028
|
+
return deps;
|
|
1778
2029
|
}
|
|
1779
|
-
|
|
1780
|
-
|
|
2030
|
+
const name = key.slice("layout:".length);
|
|
2031
|
+
const def = layouts.get(name);
|
|
2032
|
+
if (!def) return deps;
|
|
2033
|
+
collectLayoutDependencies(def.body, deps);
|
|
2034
|
+
return deps;
|
|
2035
|
+
};
|
|
2036
|
+
const findCycle = (key, path = []) => {
|
|
2037
|
+
if (recursionStack.has(key)) {
|
|
2038
|
+
const cycleStart = path.indexOf(key);
|
|
2039
|
+
return path.slice(cycleStart).concat(key);
|
|
1781
2040
|
}
|
|
1782
|
-
|
|
1783
|
-
if (!component) {
|
|
2041
|
+
if (visited.has(key)) {
|
|
1784
2042
|
return null;
|
|
1785
2043
|
}
|
|
1786
|
-
recursionStack.add(
|
|
1787
|
-
const currentPath = [...path,
|
|
1788
|
-
const dependencies =
|
|
2044
|
+
recursionStack.add(key);
|
|
2045
|
+
const currentPath = [...path, key];
|
|
2046
|
+
const dependencies = getDependencies(key);
|
|
1789
2047
|
for (const dep of dependencies) {
|
|
1790
|
-
const
|
|
1791
|
-
if (
|
|
1792
|
-
const cycle = hasCycle(dep, currentPath);
|
|
1793
|
-
if (cycle) {
|
|
1794
|
-
return cycle;
|
|
1795
|
-
}
|
|
1796
|
-
}
|
|
2048
|
+
const cycle = findCycle(dep, currentPath);
|
|
2049
|
+
if (cycle) return cycle;
|
|
1797
2050
|
}
|
|
1798
|
-
recursionStack.delete(
|
|
1799
|
-
visited.add(
|
|
2051
|
+
recursionStack.delete(key);
|
|
2052
|
+
visited.add(key);
|
|
1800
2053
|
return null;
|
|
1801
|
-
}
|
|
1802
|
-
|
|
2054
|
+
};
|
|
2055
|
+
const allDefinitions = [
|
|
2056
|
+
...Array.from(components.keys()).map(makeComponentKey),
|
|
2057
|
+
...Array.from(layouts.keys()).map(makeLayoutKey)
|
|
2058
|
+
];
|
|
2059
|
+
for (const key of allDefinitions) {
|
|
1803
2060
|
visited.clear();
|
|
1804
2061
|
recursionStack.clear();
|
|
1805
|
-
const cycle =
|
|
2062
|
+
const cycle = findCycle(key);
|
|
1806
2063
|
if (cycle) {
|
|
1807
2064
|
throw new Error(
|
|
1808
|
-
`Circular component definition detected: ${cycle.join(" \u2192 ")}
|
|
1809
|
-
Components cannot reference each other in a cycle.`
|
|
2065
|
+
`Circular component definition detected: ${cycle.map(displayKey).join(" \u2192 ")}
|
|
2066
|
+
Components and layouts cannot reference each other in a cycle.`
|
|
1810
2067
|
);
|
|
1811
2068
|
}
|
|
1812
2069
|
}
|
|
@@ -1814,6 +2071,10 @@ Components cannot reference each other in a cycle.`
|
|
|
1814
2071
|
|
|
1815
2072
|
// src/ir/index.ts
|
|
1816
2073
|
import { z } from "zod";
|
|
2074
|
+
import {
|
|
2075
|
+
COMPONENTS as COMPONENTS2,
|
|
2076
|
+
LAYOUTS as LAYOUTS2
|
|
2077
|
+
} from "@wire-dsl/language-support/components";
|
|
1817
2078
|
|
|
1818
2079
|
// src/ir/device-presets.ts
|
|
1819
2080
|
var DEVICE_PRESETS = {
|
|
@@ -1978,9 +2239,11 @@ var IRGenerator = class {
|
|
|
1978
2239
|
this.idGen = new IDGenerator();
|
|
1979
2240
|
this.nodes = {};
|
|
1980
2241
|
this.definedComponents = /* @__PURE__ */ new Map();
|
|
2242
|
+
this.definedLayouts = /* @__PURE__ */ new Map();
|
|
1981
2243
|
this.definedComponentIndices = /* @__PURE__ */ new Map();
|
|
1982
2244
|
this.undefinedComponentsUsed = /* @__PURE__ */ new Set();
|
|
1983
2245
|
this.warnings = [];
|
|
2246
|
+
this.errors = [];
|
|
1984
2247
|
this.style = {
|
|
1985
2248
|
density: "normal",
|
|
1986
2249
|
spacing: "md",
|
|
@@ -1993,15 +2256,22 @@ var IRGenerator = class {
|
|
|
1993
2256
|
this.idGen.reset();
|
|
1994
2257
|
this.nodes = {};
|
|
1995
2258
|
this.definedComponents.clear();
|
|
2259
|
+
this.definedLayouts.clear();
|
|
1996
2260
|
this.definedComponentIndices.clear();
|
|
1997
2261
|
this.undefinedComponentsUsed.clear();
|
|
1998
2262
|
this.warnings = [];
|
|
2263
|
+
this.errors = [];
|
|
1999
2264
|
if (ast.definedComponents && ast.definedComponents.length > 0) {
|
|
2000
2265
|
ast.definedComponents.forEach((def, index) => {
|
|
2001
2266
|
this.definedComponents.set(def.name, def);
|
|
2002
2267
|
this.definedComponentIndices.set(def.name, index);
|
|
2003
2268
|
});
|
|
2004
2269
|
}
|
|
2270
|
+
if (ast.definedLayouts && ast.definedLayouts.length > 0) {
|
|
2271
|
+
ast.definedLayouts.forEach((def) => {
|
|
2272
|
+
this.definedLayouts.set(def.name, def);
|
|
2273
|
+
});
|
|
2274
|
+
}
|
|
2005
2275
|
this.applyStyle(ast.style);
|
|
2006
2276
|
const screens = ast.screens.map(
|
|
2007
2277
|
(screen, screenIndex) => this.convertScreen(screen, screenIndex)
|
|
@@ -2013,6 +2283,11 @@ var IRGenerator = class {
|
|
|
2013
2283
|
Define these components with: define Component "Name" { ... }`
|
|
2014
2284
|
);
|
|
2015
2285
|
}
|
|
2286
|
+
if (this.errors.length > 0) {
|
|
2287
|
+
const messages = this.errors.map((e) => `- [${e.type}] ${e.message}`).join("\n");
|
|
2288
|
+
throw new Error(`IR generation failed with semantic errors:
|
|
2289
|
+
${messages}`);
|
|
2290
|
+
}
|
|
2016
2291
|
const project = {
|
|
2017
2292
|
id: this.sanitizeId(ast.name),
|
|
2018
2293
|
name: ast.name,
|
|
@@ -2139,41 +2414,50 @@ Define these components with: define Component "Name" { ... }`
|
|
|
2139
2414
|
getWarnings() {
|
|
2140
2415
|
return this.warnings;
|
|
2141
2416
|
}
|
|
2142
|
-
convertLayout(layout) {
|
|
2417
|
+
convertLayout(layout, context) {
|
|
2418
|
+
let layoutParams = this.resolveLayoutParams(layout.layoutType, layout.params, context);
|
|
2419
|
+
if (layout.layoutType === "split") {
|
|
2420
|
+
layoutParams = this.normalizeSplitParams(layoutParams);
|
|
2421
|
+
}
|
|
2422
|
+
const layoutChildren = layout.children;
|
|
2423
|
+
const layoutDefinition = this.definedLayouts.get(layout.layoutType);
|
|
2424
|
+
if (layoutDefinition) {
|
|
2425
|
+
return this.expandDefinedLayout(layoutDefinition, layoutParams, layoutChildren, context);
|
|
2426
|
+
}
|
|
2143
2427
|
const nodeId = this.idGen.generate("node");
|
|
2144
2428
|
const childRefs = [];
|
|
2145
|
-
for (const child of
|
|
2429
|
+
for (const child of layoutChildren) {
|
|
2146
2430
|
if (child.type === "layout") {
|
|
2147
|
-
const childId = this.convertLayout(child);
|
|
2148
|
-
childRefs.push({ ref: childId });
|
|
2431
|
+
const childId = this.convertLayout(child, context);
|
|
2432
|
+
if (childId) childRefs.push({ ref: childId });
|
|
2149
2433
|
} else if (child.type === "component") {
|
|
2150
|
-
const childId = this.convertComponent(child);
|
|
2151
|
-
childRefs.push({ ref: childId });
|
|
2434
|
+
const childId = this.convertComponent(child, context);
|
|
2435
|
+
if (childId) childRefs.push({ ref: childId });
|
|
2152
2436
|
} else if (child.type === "cell") {
|
|
2153
|
-
const childId = this.convertCell(child);
|
|
2154
|
-
childRefs.push({ ref: childId });
|
|
2437
|
+
const childId = this.convertCell(child, context);
|
|
2438
|
+
if (childId) childRefs.push({ ref: childId });
|
|
2155
2439
|
}
|
|
2156
2440
|
}
|
|
2157
2441
|
const style = {};
|
|
2158
|
-
if (
|
|
2159
|
-
style.padding = String(
|
|
2442
|
+
if (layoutParams.padding !== void 0) {
|
|
2443
|
+
style.padding = String(layoutParams.padding);
|
|
2160
2444
|
} else {
|
|
2161
2445
|
style.padding = "none";
|
|
2162
2446
|
}
|
|
2163
|
-
if (
|
|
2164
|
-
style.gap = String(
|
|
2447
|
+
if (layoutParams.gap !== void 0) {
|
|
2448
|
+
style.gap = String(layoutParams.gap);
|
|
2165
2449
|
}
|
|
2166
|
-
if (
|
|
2167
|
-
style.align =
|
|
2450
|
+
if (layoutParams.align !== void 0) {
|
|
2451
|
+
style.align = layoutParams.align;
|
|
2168
2452
|
}
|
|
2169
|
-
if (
|
|
2170
|
-
style.background = String(
|
|
2453
|
+
if (layoutParams.background !== void 0) {
|
|
2454
|
+
style.background = String(layoutParams.background);
|
|
2171
2455
|
}
|
|
2172
2456
|
const containerNode = {
|
|
2173
2457
|
id: nodeId,
|
|
2174
2458
|
kind: "container",
|
|
2175
2459
|
containerType: layout.layoutType,
|
|
2176
|
-
params: this.cleanParams(
|
|
2460
|
+
params: this.cleanParams(layoutParams),
|
|
2177
2461
|
children: childRefs,
|
|
2178
2462
|
style,
|
|
2179
2463
|
meta: {
|
|
@@ -2184,16 +2468,16 @@ Define these components with: define Component "Name" { ... }`
|
|
|
2184
2468
|
this.nodes[nodeId] = containerNode;
|
|
2185
2469
|
return nodeId;
|
|
2186
2470
|
}
|
|
2187
|
-
convertCell(cell) {
|
|
2471
|
+
convertCell(cell, context) {
|
|
2188
2472
|
const nodeId = this.idGen.generate("node");
|
|
2189
2473
|
const childRefs = [];
|
|
2190
2474
|
for (const child of cell.children) {
|
|
2191
2475
|
if (child.type === "layout") {
|
|
2192
|
-
const childId = this.convertLayout(child);
|
|
2193
|
-
childRefs.push({ ref: childId });
|
|
2476
|
+
const childId = this.convertLayout(child, context);
|
|
2477
|
+
if (childId) childRefs.push({ ref: childId });
|
|
2194
2478
|
} else if (child.type === "component") {
|
|
2195
|
-
const childId = this.convertComponent(child);
|
|
2196
|
-
childRefs.push({ ref: childId });
|
|
2479
|
+
const childId = this.convertComponent(child, context);
|
|
2480
|
+
if (childId) childRefs.push({ ref: childId });
|
|
2197
2481
|
}
|
|
2198
2482
|
}
|
|
2199
2483
|
const containerNode = {
|
|
@@ -2214,10 +2498,28 @@ Define these components with: define Component "Name" { ... }`
|
|
|
2214
2498
|
this.nodes[nodeId] = containerNode;
|
|
2215
2499
|
return nodeId;
|
|
2216
2500
|
}
|
|
2217
|
-
convertComponent(component) {
|
|
2501
|
+
convertComponent(component, context) {
|
|
2502
|
+
if (component.componentType === "Children") {
|
|
2503
|
+
if (!context?.allowChildrenSlot) {
|
|
2504
|
+
this.errors.push({
|
|
2505
|
+
type: "children-slot-outside-layout-definition",
|
|
2506
|
+
message: '"Children" placeholder can only be used inside a define Layout body.'
|
|
2507
|
+
});
|
|
2508
|
+
return null;
|
|
2509
|
+
}
|
|
2510
|
+
if (!context.childrenSlot) {
|
|
2511
|
+
this.errors.push({
|
|
2512
|
+
type: "children-slot-missing-child",
|
|
2513
|
+
message: `Layout "${context.definitionName}" requires exactly one child for "Children".`
|
|
2514
|
+
});
|
|
2515
|
+
return null;
|
|
2516
|
+
}
|
|
2517
|
+
return this.convertASTNode(context.childrenSlot, context);
|
|
2518
|
+
}
|
|
2519
|
+
const resolvedProps = this.resolveComponentProps(component.componentType, component.props, context);
|
|
2218
2520
|
const definition = this.definedComponents.get(component.componentType);
|
|
2219
2521
|
if (definition) {
|
|
2220
|
-
return this.expandDefinedComponent(definition);
|
|
2522
|
+
return this.expandDefinedComponent(definition, resolvedProps, context);
|
|
2221
2523
|
}
|
|
2222
2524
|
const builtInComponents = /* @__PURE__ */ new Set([
|
|
2223
2525
|
"Button",
|
|
@@ -2260,7 +2562,7 @@ Define these components with: define Component "Name" { ... }`
|
|
|
2260
2562
|
id: nodeId,
|
|
2261
2563
|
kind: "component",
|
|
2262
2564
|
componentType: component.componentType,
|
|
2263
|
-
props:
|
|
2565
|
+
props: resolvedProps,
|
|
2264
2566
|
style: {},
|
|
2265
2567
|
meta: {
|
|
2266
2568
|
nodeId: component._meta?.nodeId
|
|
@@ -2270,15 +2572,224 @@ Define these components with: define Component "Name" { ... }`
|
|
|
2270
2572
|
this.nodes[nodeId] = componentNode;
|
|
2271
2573
|
return nodeId;
|
|
2272
2574
|
}
|
|
2273
|
-
expandDefinedComponent(definition) {
|
|
2575
|
+
expandDefinedComponent(definition, invocationArgs, parentContext) {
|
|
2576
|
+
const context = {
|
|
2577
|
+
args: invocationArgs,
|
|
2578
|
+
providedArgNames: new Set(Object.keys(invocationArgs)),
|
|
2579
|
+
usedArgNames: /* @__PURE__ */ new Set(),
|
|
2580
|
+
definitionName: definition.name,
|
|
2581
|
+
definitionKind: "component",
|
|
2582
|
+
allowChildrenSlot: false
|
|
2583
|
+
};
|
|
2274
2584
|
if (definition.body.type === "layout") {
|
|
2275
|
-
|
|
2585
|
+
const result = this.convertLayout(definition.body, context);
|
|
2586
|
+
this.reportUnusedArguments(context);
|
|
2587
|
+
return result;
|
|
2276
2588
|
} else if (definition.body.type === "component") {
|
|
2277
|
-
|
|
2589
|
+
const result = this.convertComponent(definition.body, context);
|
|
2590
|
+
this.reportUnusedArguments(context);
|
|
2591
|
+
return result;
|
|
2278
2592
|
} else {
|
|
2279
2593
|
throw new Error(`Invalid defined component body type for "${definition.name}"`);
|
|
2280
2594
|
}
|
|
2281
2595
|
}
|
|
2596
|
+
expandDefinedLayout(definition, invocationParams, invocationChildren, parentContext) {
|
|
2597
|
+
if (invocationChildren.length !== 1) {
|
|
2598
|
+
this.errors.push({
|
|
2599
|
+
type: "layout-children-arity",
|
|
2600
|
+
message: `Layout "${definition.name}" expects exactly one child, received ${invocationChildren.length}.`
|
|
2601
|
+
});
|
|
2602
|
+
}
|
|
2603
|
+
const rawSlot = invocationChildren[0];
|
|
2604
|
+
const resolvedSlot = rawSlot ? this.resolveChildrenSlot(rawSlot, parentContext) : void 0;
|
|
2605
|
+
const context = {
|
|
2606
|
+
args: invocationParams,
|
|
2607
|
+
providedArgNames: new Set(Object.keys(invocationParams)),
|
|
2608
|
+
usedArgNames: /* @__PURE__ */ new Set(),
|
|
2609
|
+
definitionName: definition.name,
|
|
2610
|
+
definitionKind: "layout",
|
|
2611
|
+
allowChildrenSlot: true,
|
|
2612
|
+
childrenSlot: resolvedSlot
|
|
2613
|
+
};
|
|
2614
|
+
const nodeId = this.convertLayout(definition.body, context);
|
|
2615
|
+
this.reportUnusedArguments(context);
|
|
2616
|
+
return nodeId;
|
|
2617
|
+
}
|
|
2618
|
+
resolveChildrenSlot(slot, parentContext) {
|
|
2619
|
+
if (slot.type === "component" && slot.componentType === "Children") {
|
|
2620
|
+
if (parentContext?.allowChildrenSlot) {
|
|
2621
|
+
return parentContext.childrenSlot;
|
|
2622
|
+
}
|
|
2623
|
+
this.errors.push({
|
|
2624
|
+
type: "children-slot-outside-layout-definition",
|
|
2625
|
+
message: '"Children" placeholder forwarding is only valid inside define Layout bodies.'
|
|
2626
|
+
});
|
|
2627
|
+
return void 0;
|
|
2628
|
+
}
|
|
2629
|
+
return slot;
|
|
2630
|
+
}
|
|
2631
|
+
convertASTNode(node, context) {
|
|
2632
|
+
if (node.type === "layout") return this.convertLayout(node, context);
|
|
2633
|
+
if (node.type === "component") return this.convertComponent(node, context);
|
|
2634
|
+
return this.convertCell(node, context);
|
|
2635
|
+
}
|
|
2636
|
+
resolveLayoutParams(layoutType, params, context) {
|
|
2637
|
+
const resolved = {};
|
|
2638
|
+
for (const [key, value] of Object.entries(params)) {
|
|
2639
|
+
const resolvedValue = this.resolveBindingValue(
|
|
2640
|
+
value,
|
|
2641
|
+
context,
|
|
2642
|
+
"layout-parameter",
|
|
2643
|
+
layoutType,
|
|
2644
|
+
key
|
|
2645
|
+
);
|
|
2646
|
+
if (resolvedValue !== void 0) {
|
|
2647
|
+
const wasPropReference = typeof value === "string" && value.startsWith("prop_");
|
|
2648
|
+
if (wasPropReference) {
|
|
2649
|
+
const layoutMetadata = LAYOUTS2[layoutType];
|
|
2650
|
+
const property = layoutMetadata?.properties?.[key];
|
|
2651
|
+
if (property?.type === "enum" && Array.isArray(property.options)) {
|
|
2652
|
+
const normalizedValue = String(resolvedValue);
|
|
2653
|
+
if (!property.options.includes(normalizedValue)) {
|
|
2654
|
+
this.warnings.push({
|
|
2655
|
+
type: "invalid-bound-enum-value",
|
|
2656
|
+
message: `Invalid value "${normalizedValue}" for parameter "${key}" in layout "${layoutType}". Expected one of: ${property.options.join(", ")}.`
|
|
2657
|
+
});
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
resolved[key] = resolvedValue;
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
return resolved;
|
|
2665
|
+
}
|
|
2666
|
+
normalizeSplitParams(params) {
|
|
2667
|
+
const normalized = { ...params };
|
|
2668
|
+
if (normalized.sidebar !== void 0 && normalized.left === void 0 && normalized.right === void 0) {
|
|
2669
|
+
normalized.left = normalized.sidebar;
|
|
2670
|
+
this.warnings.push({
|
|
2671
|
+
type: "split-sidebar-deprecated",
|
|
2672
|
+
message: 'Split parameter "sidebar" is deprecated. Use "left" or "right".'
|
|
2673
|
+
});
|
|
2674
|
+
}
|
|
2675
|
+
delete normalized.sidebar;
|
|
2676
|
+
const hasLeft = normalized.left !== void 0;
|
|
2677
|
+
const hasRight = normalized.right !== void 0;
|
|
2678
|
+
if (hasLeft && hasRight) {
|
|
2679
|
+
delete normalized.right;
|
|
2680
|
+
this.warnings.push({
|
|
2681
|
+
type: "split-side-conflict",
|
|
2682
|
+
message: 'Split layout received both "left" and "right"; keeping "left".'
|
|
2683
|
+
});
|
|
2684
|
+
}
|
|
2685
|
+
if (!hasLeft && !hasRight) {
|
|
2686
|
+
normalized.left = 250;
|
|
2687
|
+
this.warnings.push({
|
|
2688
|
+
type: "split-side-missing",
|
|
2689
|
+
message: 'Split layout missing both "left" and "right"; defaulting to left: 250.'
|
|
2690
|
+
});
|
|
2691
|
+
}
|
|
2692
|
+
if (normalized.left !== void 0) {
|
|
2693
|
+
const leftWidth = Number(normalized.left);
|
|
2694
|
+
if (!Number.isFinite(leftWidth) || leftWidth <= 0) {
|
|
2695
|
+
normalized.left = 250;
|
|
2696
|
+
this.warnings.push({
|
|
2697
|
+
type: "split-left-invalid",
|
|
2698
|
+
message: 'Split "left" must be a positive number. Falling back to 250.'
|
|
2699
|
+
});
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
if (normalized.right !== void 0) {
|
|
2703
|
+
const rightWidth = Number(normalized.right);
|
|
2704
|
+
if (!Number.isFinite(rightWidth) || rightWidth <= 0) {
|
|
2705
|
+
normalized.right = 250;
|
|
2706
|
+
this.warnings.push({
|
|
2707
|
+
type: "split-right-invalid",
|
|
2708
|
+
message: 'Split "right" must be a positive number. Falling back to 250.'
|
|
2709
|
+
});
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
return normalized;
|
|
2713
|
+
}
|
|
2714
|
+
resolveComponentProps(componentType, props, context) {
|
|
2715
|
+
const resolved = {};
|
|
2716
|
+
for (const [key, value] of Object.entries(props)) {
|
|
2717
|
+
const resolvedValue = this.resolveBindingValue(
|
|
2718
|
+
value,
|
|
2719
|
+
context,
|
|
2720
|
+
"component-property",
|
|
2721
|
+
componentType,
|
|
2722
|
+
key
|
|
2723
|
+
);
|
|
2724
|
+
if (resolvedValue !== void 0) {
|
|
2725
|
+
const wasPropReference = typeof value === "string" && value.startsWith("prop_");
|
|
2726
|
+
if (wasPropReference) {
|
|
2727
|
+
const metadata = COMPONENTS2[componentType];
|
|
2728
|
+
const property = metadata?.properties?.[key];
|
|
2729
|
+
if (property?.type === "enum" && Array.isArray(property.options)) {
|
|
2730
|
+
const normalizedValue = String(resolvedValue);
|
|
2731
|
+
if (!property.options.includes(normalizedValue)) {
|
|
2732
|
+
this.warnings.push({
|
|
2733
|
+
type: "invalid-bound-enum-value",
|
|
2734
|
+
message: `Invalid value "${normalizedValue}" for property "${key}" in component "${componentType}". Expected one of: ${property.options.join(", ")}.`
|
|
2735
|
+
});
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
resolved[key] = resolvedValue;
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
return resolved;
|
|
2743
|
+
}
|
|
2744
|
+
resolveBindingValue(value, context, kind, targetType, targetName) {
|
|
2745
|
+
if (typeof value !== "string" || !value.startsWith("prop_")) {
|
|
2746
|
+
return value;
|
|
2747
|
+
}
|
|
2748
|
+
const argName = value.slice("prop_".length);
|
|
2749
|
+
if (!context) {
|
|
2750
|
+
return value;
|
|
2751
|
+
}
|
|
2752
|
+
if (Object.prototype.hasOwnProperty.call(context.args, argName)) {
|
|
2753
|
+
context.usedArgNames.add(argName);
|
|
2754
|
+
return context.args[argName];
|
|
2755
|
+
}
|
|
2756
|
+
const required = this.isBindingTargetRequired(kind, targetType, targetName);
|
|
2757
|
+
const descriptor = kind === "component-property" ? "property" : "parameter";
|
|
2758
|
+
const message = `Missing required bound ${descriptor} "${targetName}" for ${kind === "component-property" ? "component" : "layout"} "${targetType}" in ${context.definitionKind} "${context.definitionName}" (expected arg "${argName}").`;
|
|
2759
|
+
if (required) {
|
|
2760
|
+
this.errors.push({ type: "missing-required-bound-value", message });
|
|
2761
|
+
} else {
|
|
2762
|
+
this.warnings.push({
|
|
2763
|
+
type: "missing-bound-value",
|
|
2764
|
+
message: `Optional ${descriptor} "${targetName}" in ${kind === "component-property" ? "component" : "layout"} "${targetType}" was omitted because arg "${argName}" was not provided while expanding ${context.definitionKind} "${context.definitionName}".`
|
|
2765
|
+
});
|
|
2766
|
+
}
|
|
2767
|
+
return void 0;
|
|
2768
|
+
}
|
|
2769
|
+
isBindingTargetRequired(kind, targetType, targetName) {
|
|
2770
|
+
if (kind === "component-property") {
|
|
2771
|
+
const metadata = COMPONENTS2[targetType];
|
|
2772
|
+
const property2 = metadata?.properties?.[targetName];
|
|
2773
|
+
if (!property2) return false;
|
|
2774
|
+
return property2.required === true && property2.defaultValue === void 0;
|
|
2775
|
+
}
|
|
2776
|
+
const layoutMetadata = LAYOUTS2[targetType];
|
|
2777
|
+
if (!layoutMetadata) return false;
|
|
2778
|
+
const property = layoutMetadata.properties?.[targetName];
|
|
2779
|
+
const requiredFromProperty = property?.required === true && property.defaultValue === void 0;
|
|
2780
|
+
const requiredFromLayout = (layoutMetadata.requiredProperties || []).includes(targetName);
|
|
2781
|
+
return requiredFromProperty || requiredFromLayout;
|
|
2782
|
+
}
|
|
2783
|
+
reportUnusedArguments(context) {
|
|
2784
|
+
for (const arg of context.providedArgNames) {
|
|
2785
|
+
if (!context.usedArgNames.has(arg)) {
|
|
2786
|
+
this.warnings.push({
|
|
2787
|
+
type: "unused-definition-argument",
|
|
2788
|
+
message: `Argument "${arg}" is not used by ${context.definitionKind} "${context.definitionName}".`
|
|
2789
|
+
});
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2282
2793
|
cleanParams(params) {
|
|
2283
2794
|
const cleaned = {};
|
|
2284
2795
|
for (const [key, value] of Object.entries(params)) {
|
|
@@ -2328,9 +2839,24 @@ var ICON_SIZES_BY_DENSITY = {
|
|
|
2328
2839
|
comfortable: { xs: 14, sm: 16, md: 20, lg: 28, xl: 36 }
|
|
2329
2840
|
};
|
|
2330
2841
|
var ICON_BUTTON_SIZES_BY_DENSITY = {
|
|
2331
|
-
compact: { sm:
|
|
2332
|
-
normal: { sm:
|
|
2333
|
-
comfortable: { sm:
|
|
2842
|
+
compact: { sm: 20, md: 24, lg: 32 },
|
|
2843
|
+
normal: { sm: 24, md: 32, lg: 40 },
|
|
2844
|
+
comfortable: { sm: 28, md: 40, lg: 48 }
|
|
2845
|
+
};
|
|
2846
|
+
var CONTROL_HEIGHTS_BY_DENSITY = {
|
|
2847
|
+
compact: { sm: 28, md: 32, lg: 36 },
|
|
2848
|
+
normal: { sm: 36, md: 40, lg: 48 },
|
|
2849
|
+
comfortable: { sm: 40, md: 48, lg: 56 }
|
|
2850
|
+
};
|
|
2851
|
+
var ACTION_CONTROL_HEIGHTS_BY_DENSITY = {
|
|
2852
|
+
compact: { sm: 20, md: 24, lg: 32 },
|
|
2853
|
+
normal: { sm: 24, md: 32, lg: 40 },
|
|
2854
|
+
comfortable: { sm: 28, md: 40, lg: 48 }
|
|
2855
|
+
};
|
|
2856
|
+
var CONTROL_PADDING_BY_DENSITY = {
|
|
2857
|
+
compact: { none: 0, xs: 4, sm: 8, md: 10, lg: 14, xl: 18 },
|
|
2858
|
+
normal: { none: 0, xs: 6, sm: 10, md: 14, lg: 18, xl: 24 },
|
|
2859
|
+
comfortable: { none: 0, xs: 8, sm: 12, md: 16, lg: 22, xl: 28 }
|
|
2334
2860
|
};
|
|
2335
2861
|
function resolveIconSize(size, density = "normal") {
|
|
2336
2862
|
const map = ICON_SIZES_BY_DENSITY[density] || ICON_SIZES_BY_DENSITY.normal;
|
|
@@ -2340,6 +2866,18 @@ function resolveIconButtonSize(size, density = "normal") {
|
|
|
2340
2866
|
const map = ICON_BUTTON_SIZES_BY_DENSITY[density] || ICON_BUTTON_SIZES_BY_DENSITY.normal;
|
|
2341
2867
|
return map[size || "md"] || map.md;
|
|
2342
2868
|
}
|
|
2869
|
+
function resolveControlHeight(size, density = "normal") {
|
|
2870
|
+
const map = CONTROL_HEIGHTS_BY_DENSITY[density] || CONTROL_HEIGHTS_BY_DENSITY.normal;
|
|
2871
|
+
return map[size || "md"] || map.md;
|
|
2872
|
+
}
|
|
2873
|
+
function resolveActionControlHeight(size, density = "normal") {
|
|
2874
|
+
const map = ACTION_CONTROL_HEIGHTS_BY_DENSITY[density] || ACTION_CONTROL_HEIGHTS_BY_DENSITY.normal;
|
|
2875
|
+
return map[size || "md"] || map.md;
|
|
2876
|
+
}
|
|
2877
|
+
function resolveControlHorizontalPadding(padding, density = "normal") {
|
|
2878
|
+
const map = CONTROL_PADDING_BY_DENSITY[density] || CONTROL_PADDING_BY_DENSITY.normal;
|
|
2879
|
+
return map[padding || "md"] ?? map.md;
|
|
2880
|
+
}
|
|
2343
2881
|
|
|
2344
2882
|
// src/shared/heading-levels.ts
|
|
2345
2883
|
var DEFAULT_LEVEL = "h2";
|
|
@@ -2658,6 +3196,41 @@ var LayoutEngine = class {
|
|
|
2658
3196
|
}
|
|
2659
3197
|
return totalHeight;
|
|
2660
3198
|
}
|
|
3199
|
+
if (node.containerType === "split") {
|
|
3200
|
+
const splitGap = this.resolveSpacing(node.style.gap);
|
|
3201
|
+
const leftParam = node.params.left;
|
|
3202
|
+
const rightParam = node.params.right;
|
|
3203
|
+
const leftWidthRaw = Number(leftParam);
|
|
3204
|
+
const rightWidthRaw = Number(rightParam);
|
|
3205
|
+
const hasLeft = leftParam !== void 0;
|
|
3206
|
+
const hasRight = rightParam !== void 0;
|
|
3207
|
+
const leftWidth = Number.isFinite(leftWidthRaw) && leftWidthRaw > 0 ? leftWidthRaw : availableWidth / 2;
|
|
3208
|
+
const rightWidth = Number.isFinite(rightWidthRaw) && rightWidthRaw > 0 ? rightWidthRaw : availableWidth / 2;
|
|
3209
|
+
let maxHeight = 0;
|
|
3210
|
+
node.children.forEach((childRef, index) => {
|
|
3211
|
+
const child = this.nodes[childRef.ref];
|
|
3212
|
+
let childHeight = this.getComponentHeight();
|
|
3213
|
+
const isFirst = index === 0;
|
|
3214
|
+
let childWidth;
|
|
3215
|
+
if (node.children.length >= 2) {
|
|
3216
|
+
if (hasRight && !hasLeft) {
|
|
3217
|
+
childWidth = isFirst ? Math.max(1, availableWidth - rightWidth - splitGap) : rightWidth;
|
|
3218
|
+
} else {
|
|
3219
|
+
childWidth = isFirst ? leftWidth : Math.max(1, availableWidth - leftWidth - splitGap);
|
|
3220
|
+
}
|
|
3221
|
+
} else {
|
|
3222
|
+
childWidth = availableWidth;
|
|
3223
|
+
}
|
|
3224
|
+
if (child?.kind === "component") {
|
|
3225
|
+
childHeight = child.props.height ? Number(child.props.height) : this.getIntrinsicComponentHeight(child, childWidth);
|
|
3226
|
+
} else if (child?.kind === "container") {
|
|
3227
|
+
childHeight = this.calculateContainerHeight(child, childWidth);
|
|
3228
|
+
}
|
|
3229
|
+
maxHeight = Math.max(maxHeight, childHeight);
|
|
3230
|
+
});
|
|
3231
|
+
totalHeight += maxHeight;
|
|
3232
|
+
return totalHeight;
|
|
3233
|
+
}
|
|
2661
3234
|
const direction = node.params.direction || "vertical";
|
|
2662
3235
|
if (node.containerType === "stack" && direction === "horizontal") {
|
|
2663
3236
|
let maxHeight = 0;
|
|
@@ -2780,14 +3353,28 @@ var LayoutEngine = class {
|
|
|
2780
3353
|
calculateSplit(node, x, y, width, height) {
|
|
2781
3354
|
if (node.kind !== "container") return;
|
|
2782
3355
|
const gap = this.resolveSpacing(node.style.gap);
|
|
2783
|
-
const
|
|
3356
|
+
const leftParam = node.params.left;
|
|
3357
|
+
const rightParam = node.params.right;
|
|
3358
|
+
const leftWidthRaw = Number(leftParam);
|
|
3359
|
+
const rightWidthRaw = Number(rightParam);
|
|
3360
|
+
const hasLeft = leftParam !== void 0;
|
|
3361
|
+
const hasRight = rightParam !== void 0;
|
|
3362
|
+
const leftWidth = Number.isFinite(leftWidthRaw) && leftWidthRaw > 0 ? leftWidthRaw : 250;
|
|
3363
|
+
const rightWidth = Number.isFinite(rightWidthRaw) && rightWidthRaw > 0 ? rightWidthRaw : 250;
|
|
2784
3364
|
if (node.children.length === 1) {
|
|
2785
3365
|
this.calculateNode(node.children[0].ref, x, y, width, height, "split");
|
|
2786
3366
|
} else if (node.children.length >= 2) {
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
3367
|
+
if (hasRight && !hasLeft) {
|
|
3368
|
+
const flexibleLeftWidth = Math.max(1, width - rightWidth - gap);
|
|
3369
|
+
const rightX = x + flexibleLeftWidth + gap;
|
|
3370
|
+
this.calculateNode(node.children[0].ref, x, y, flexibleLeftWidth, height, "split");
|
|
3371
|
+
this.calculateNode(node.children[1].ref, rightX, y, rightWidth, height, "split");
|
|
3372
|
+
} else {
|
|
3373
|
+
const flexibleRightWidth = Math.max(1, width - leftWidth - gap);
|
|
3374
|
+
const rightX = x + leftWidth + gap;
|
|
3375
|
+
this.calculateNode(node.children[0].ref, x, y, leftWidth, height, "split");
|
|
3376
|
+
this.calculateNode(node.children[1].ref, rightX, y, flexibleRightWidth, height, "split");
|
|
3377
|
+
}
|
|
2791
3378
|
}
|
|
2792
3379
|
}
|
|
2793
3380
|
calculatePanel(node, x, y, width, height) {
|
|
@@ -2936,7 +3523,11 @@ var LayoutEngine = class {
|
|
|
2936
3523
|
}
|
|
2937
3524
|
getIntrinsicComponentHeight(node, availableWidth) {
|
|
2938
3525
|
if (node.kind !== "component") return this.getComponentHeight();
|
|
2939
|
-
const
|
|
3526
|
+
const controlSize = String(node.props.size || "md");
|
|
3527
|
+
const density = this.style.density || "normal";
|
|
3528
|
+
const inputControlHeight = resolveControlHeight(controlSize, density);
|
|
3529
|
+
const actionControlHeight = resolveActionControlHeight(controlSize, density);
|
|
3530
|
+
const controlLabelOffset = node.componentType === "Input" || node.componentType === "Textarea" || node.componentType === "Select" ? this.getControlLabelOffset(String(node.props.label || "")) : (node.componentType === "Button" || node.componentType === "IconButton") && this.parseBooleanProp(node.props.labelSpace, false) ? 18 : 0;
|
|
2940
3531
|
if (node.componentType === "Image") {
|
|
2941
3532
|
const placeholder = String(node.props.placeholder || "landscape");
|
|
2942
3533
|
const aspectRatios = {
|
|
@@ -2963,12 +3554,26 @@ var LayoutEngine = class {
|
|
|
2963
3554
|
}
|
|
2964
3555
|
const rowCount = Number(node.props.rows || node.props.rowsMock || 5);
|
|
2965
3556
|
const hasTitle = !!node.props.title;
|
|
2966
|
-
const hasPagination =
|
|
3557
|
+
const hasPagination = this.parseBooleanProp(node.props.pagination, false);
|
|
3558
|
+
const hasCaption = String(node.props.caption || "").trim().length > 0;
|
|
3559
|
+
const paginationAlign = String(node.props.paginationAlign || "right");
|
|
3560
|
+
const captionAlign = String(node.props.captionAlign || "");
|
|
3561
|
+
const effectiveCaptionAlign = captionAlign === "left" || captionAlign === "center" || captionAlign === "right" ? captionAlign : paginationAlign === "left" ? "right" : "left";
|
|
3562
|
+
const sameFooterAlign = hasCaption && hasPagination && effectiveCaptionAlign === paginationAlign;
|
|
2967
3563
|
const headerHeight = 44;
|
|
2968
3564
|
const rowHeight = 36;
|
|
2969
3565
|
const titleHeight = hasTitle ? 32 : 0;
|
|
2970
|
-
|
|
2971
|
-
|
|
3566
|
+
let footerHeight = 0;
|
|
3567
|
+
if (hasPagination || hasCaption) {
|
|
3568
|
+
const footerBottomPadding = 12;
|
|
3569
|
+
footerHeight += 16;
|
|
3570
|
+
footerHeight += hasPagination ? 32 : 18;
|
|
3571
|
+
if (sameFooterAlign) {
|
|
3572
|
+
footerHeight += 8 + 18;
|
|
3573
|
+
}
|
|
3574
|
+
footerHeight += footerBottomPadding;
|
|
3575
|
+
}
|
|
3576
|
+
return titleHeight + headerHeight + rowCount * rowHeight + footerHeight;
|
|
2972
3577
|
}
|
|
2973
3578
|
if (node.componentType === "Heading") {
|
|
2974
3579
|
const text = String(node.props.text || "Heading");
|
|
@@ -2977,15 +3582,15 @@ var LayoutEngine = class {
|
|
|
2977
3582
|
const maxWidth = availableWidth && availableWidth > 0 ? availableWidth : 200;
|
|
2978
3583
|
const lines = this.wrapTextToLines(text, maxWidth, fontSize);
|
|
2979
3584
|
const wrappedHeight = Math.max(1, lines.length) * lineHeightPx;
|
|
2980
|
-
const
|
|
2981
|
-
const verticalPadding = resolveHeadingVerticalPadding(node.props.spacing,
|
|
3585
|
+
const density2 = this.style.density || "normal";
|
|
3586
|
+
const verticalPadding = resolveHeadingVerticalPadding(node.props.spacing, density2);
|
|
2982
3587
|
if (verticalPadding === null) {
|
|
2983
3588
|
return Math.max(this.getComponentHeight(), wrappedHeight);
|
|
2984
3589
|
}
|
|
2985
3590
|
return Math.max(1, Math.ceil(wrappedHeight + verticalPadding * 2));
|
|
2986
3591
|
}
|
|
2987
3592
|
if (node.componentType === "Text") {
|
|
2988
|
-
const content = String(node.props.
|
|
3593
|
+
const content = String(node.props.text || "");
|
|
2989
3594
|
const { fontSize, lineHeight } = this.getTextMetricsForDensity();
|
|
2990
3595
|
const lineHeightPx = Math.ceil(fontSize * lineHeight);
|
|
2991
3596
|
const maxWidth = availableWidth && availableWidth > 0 ? availableWidth : 200;
|
|
@@ -3034,7 +3639,10 @@ var LayoutEngine = class {
|
|
|
3034
3639
|
if (node.componentType === "Divider") return 1;
|
|
3035
3640
|
if (node.componentType === "Separate") return this.getSeparateSize(node);
|
|
3036
3641
|
if (node.componentType === "Input" || node.componentType === "Select") {
|
|
3037
|
-
return
|
|
3642
|
+
return inputControlHeight + controlLabelOffset;
|
|
3643
|
+
}
|
|
3644
|
+
if (node.componentType === "Button" || node.componentType === "IconButton" || node.componentType === "Link") {
|
|
3645
|
+
return actionControlHeight + controlLabelOffset;
|
|
3038
3646
|
}
|
|
3039
3647
|
return this.getComponentHeight();
|
|
3040
3648
|
}
|
|
@@ -3051,7 +3659,10 @@ var LayoutEngine = class {
|
|
|
3051
3659
|
}
|
|
3052
3660
|
if (node.componentType === "IconButton") {
|
|
3053
3661
|
const size = String(node.props.size || "md");
|
|
3054
|
-
|
|
3662
|
+
const density = this.style.density || "normal";
|
|
3663
|
+
const baseSize = resolveIconButtonSize(size, density);
|
|
3664
|
+
const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
|
|
3665
|
+
return baseSize + extraPadding * 2;
|
|
3055
3666
|
}
|
|
3056
3667
|
if (node.componentType === "Checkbox" || node.componentType === "Radio") {
|
|
3057
3668
|
return 24;
|
|
@@ -3060,11 +3671,13 @@ var LayoutEngine = class {
|
|
|
3060
3671
|
if (node.componentType === "Button" || node.componentType === "Link") {
|
|
3061
3672
|
const text = String(node.props.text || "");
|
|
3062
3673
|
const { fontSize, paddingX } = this.getButtonMetricsForDensity();
|
|
3674
|
+
const density = this.style.density || "normal";
|
|
3675
|
+
const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
|
|
3063
3676
|
const textWidth = this.estimateTextWidth(text, fontSize);
|
|
3064
|
-
return Math.max(60, Math.ceil(textWidth + paddingX * 2));
|
|
3677
|
+
return Math.max(60, Math.ceil(textWidth + (paddingX + extraPadding) * 2));
|
|
3065
3678
|
}
|
|
3066
3679
|
if (node.componentType === "Label" || node.componentType === "Text") {
|
|
3067
|
-
const text = String(node.props.
|
|
3680
|
+
const text = String(node.props.text || "");
|
|
3068
3681
|
return Math.max(60, text.length * 8 + 16);
|
|
3069
3682
|
}
|
|
3070
3683
|
if (node.componentType === "Heading") {
|
|
@@ -3926,27 +4539,27 @@ var THEMES = {
|
|
|
3926
4539
|
bg: "#F8FAFC",
|
|
3927
4540
|
cardBg: "#FFFFFF",
|
|
3928
4541
|
border: "#E2E8F0",
|
|
3929
|
-
text: "#
|
|
4542
|
+
text: "#000000",
|
|
3930
4543
|
textMuted: "#64748B",
|
|
3931
4544
|
primary: "#3B82F6",
|
|
3932
4545
|
primaryHover: "#2563EB",
|
|
3933
4546
|
primaryLight: "#EFF6FF"
|
|
3934
4547
|
},
|
|
3935
4548
|
dark: {
|
|
3936
|
-
bg: "#
|
|
3937
|
-
cardBg: "#
|
|
3938
|
-
border: "#
|
|
3939
|
-
text: "#
|
|
3940
|
-
textMuted: "#
|
|
4549
|
+
bg: "#111111",
|
|
4550
|
+
cardBg: "#1C1C1C",
|
|
4551
|
+
border: "#303030",
|
|
4552
|
+
text: "#F0F0F0",
|
|
4553
|
+
textMuted: "#808080",
|
|
3941
4554
|
primary: "#60A5FA",
|
|
3942
4555
|
primaryHover: "#3B82F6",
|
|
3943
|
-
primaryLight: "#
|
|
4556
|
+
primaryLight: "#1C2A3A"
|
|
3944
4557
|
}
|
|
3945
4558
|
};
|
|
3946
4559
|
var SVGRenderer = class {
|
|
3947
4560
|
constructor(ir, layout, options) {
|
|
3948
4561
|
this.renderedNodeIds = /* @__PURE__ */ new Set();
|
|
3949
|
-
this.fontFamily = "
|
|
4562
|
+
this.fontFamily = "Arial, Helvetica, sans-serif";
|
|
3950
4563
|
this.parentContainerByChildId = /* @__PURE__ */ new Map();
|
|
3951
4564
|
this.ir = ir;
|
|
3952
4565
|
this.layout = layout;
|
|
@@ -3960,7 +4573,6 @@ var SVGRenderer = class {
|
|
|
3960
4573
|
includeLabels: options?.includeLabels ?? true,
|
|
3961
4574
|
screenName: options?.screenName
|
|
3962
4575
|
};
|
|
3963
|
-
this.renderTheme = THEMES[this.options.theme];
|
|
3964
4576
|
this.colorResolver = new ColorResolver();
|
|
3965
4577
|
this.buildParentContainerIndex();
|
|
3966
4578
|
if (ir.project.mocks && Object.keys(ir.project.mocks).length > 0) {
|
|
@@ -3969,6 +4581,12 @@ var SVGRenderer = class {
|
|
|
3969
4581
|
if (ir.project.colors && Object.keys(ir.project.colors).length > 0) {
|
|
3970
4582
|
this.colorResolver.setCustomColors(ir.project.colors);
|
|
3971
4583
|
}
|
|
4584
|
+
const themeDefaults = THEMES[this.options.theme];
|
|
4585
|
+
this.renderTheme = {
|
|
4586
|
+
...themeDefaults,
|
|
4587
|
+
text: this.resolveTextColor(),
|
|
4588
|
+
textMuted: this.resolveMutedColor()
|
|
4589
|
+
};
|
|
3972
4590
|
}
|
|
3973
4591
|
/**
|
|
3974
4592
|
* Get list of available screens in the project
|
|
@@ -4050,6 +4668,9 @@ var SVGRenderer = class {
|
|
|
4050
4668
|
if (node.containerType === "card") {
|
|
4051
4669
|
this.renderCardBorder(node, pos, containerGroup);
|
|
4052
4670
|
}
|
|
4671
|
+
if (node.containerType === "split") {
|
|
4672
|
+
this.renderSplitDecoration(node, pos, containerGroup);
|
|
4673
|
+
}
|
|
4053
4674
|
node.children.forEach((childRef) => {
|
|
4054
4675
|
this.renderNode(childRef.ref, containerGroup);
|
|
4055
4676
|
});
|
|
@@ -4137,6 +4758,8 @@ var SVGRenderer = class {
|
|
|
4137
4758
|
}
|
|
4138
4759
|
renderHeading(node, pos) {
|
|
4139
4760
|
const text = String(node.props.text || "Heading");
|
|
4761
|
+
const variant = String(node.props.variant || "default");
|
|
4762
|
+
const headingColor = variant === "default" ? this.resolveTextColor() : this.resolveVariantColor(variant, this.resolveTextColor());
|
|
4140
4763
|
const headingTypography = this.getHeadingTypography(node);
|
|
4141
4764
|
const fontSize = headingTypography.fontSize;
|
|
4142
4765
|
const fontWeight = headingTypography.fontWeight;
|
|
@@ -4146,10 +4769,10 @@ var SVGRenderer = class {
|
|
|
4146
4769
|
if (lines.length <= 1) {
|
|
4147
4770
|
return `<g${this.getDataNodeId(node)}>
|
|
4148
4771
|
<text x="${pos.x}" y="${firstLineY}"
|
|
4149
|
-
font-family="
|
|
4772
|
+
font-family="Arial, Helvetica, sans-serif"
|
|
4150
4773
|
font-size="${fontSize}"
|
|
4151
4774
|
font-weight="${fontWeight}"
|
|
4152
|
-
fill="${
|
|
4775
|
+
fill="${headingColor}">${this.escapeXml(text)}</text>
|
|
4153
4776
|
</g>`;
|
|
4154
4777
|
}
|
|
4155
4778
|
const tspans = lines.map(
|
|
@@ -4157,61 +4780,101 @@ var SVGRenderer = class {
|
|
|
4157
4780
|
).join("");
|
|
4158
4781
|
return `<g${this.getDataNodeId(node)}>
|
|
4159
4782
|
<text x="${pos.x}" y="${firstLineY}"
|
|
4160
|
-
font-family="
|
|
4783
|
+
font-family="Arial, Helvetica, sans-serif"
|
|
4161
4784
|
font-size="${fontSize}"
|
|
4162
4785
|
font-weight="${fontWeight}"
|
|
4163
|
-
fill="${
|
|
4786
|
+
fill="${headingColor}">${tspans}</text>
|
|
4164
4787
|
</g>`;
|
|
4165
4788
|
}
|
|
4166
4789
|
renderButton(node, pos) {
|
|
4167
4790
|
const text = String(node.props.text || "Button");
|
|
4168
4791
|
const variant = String(node.props.variant || "default");
|
|
4792
|
+
const size = String(node.props.size || "md");
|
|
4793
|
+
const density = this.ir.project.style.density || "normal";
|
|
4794
|
+
const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
|
|
4795
|
+
const labelOffset = this.parseBooleanProp(node.props.labelSpace, false) ? 18 : 0;
|
|
4169
4796
|
const fullWidth = this.shouldButtonFillAvailableWidth(node);
|
|
4797
|
+
const iconName = String(node.props.icon || "").trim();
|
|
4798
|
+
const iconAlign = String(node.props.iconAlign || "left").toLowerCase();
|
|
4170
4799
|
const radius = this.tokens.button.radius;
|
|
4171
4800
|
const fontSize = this.tokens.button.fontSize;
|
|
4172
4801
|
const fontWeight = this.tokens.button.fontWeight;
|
|
4173
4802
|
const paddingX = this.tokens.button.paddingX;
|
|
4174
|
-
const
|
|
4803
|
+
const controlHeight = resolveActionControlHeight(size, density);
|
|
4804
|
+
const buttonY = pos.y + labelOffset;
|
|
4805
|
+
const buttonHeight = Math.max(16, Math.min(controlHeight, pos.height - labelOffset));
|
|
4806
|
+
const iconSvg = iconName ? getIcon(iconName) : null;
|
|
4807
|
+
const iconSize = iconSvg ? Math.round(fontSize * 1.1) : 0;
|
|
4808
|
+
const iconGap = iconSvg ? 8 : 0;
|
|
4809
|
+
const edgePad = 12;
|
|
4810
|
+
const textPad = paddingX + extraPadding;
|
|
4175
4811
|
const idealTextWidth = this.estimateTextWidth(text, fontSize);
|
|
4176
|
-
const buttonWidth = fullWidth ? Math.max(1, pos.width) : this.clampControlWidth(Math.max(Math.ceil(idealTextWidth +
|
|
4177
|
-
const
|
|
4178
|
-
const availableTextWidth = Math.max(0, buttonWidth - paddingX * 2);
|
|
4812
|
+
const buttonWidth = fullWidth ? Math.max(1, pos.width) : this.clampControlWidth(Math.max(Math.ceil(idealTextWidth + (iconSvg ? iconSize + iconGap : 0) + textPad * 2), 60), pos.width);
|
|
4813
|
+
const availableTextWidth = Math.max(0, buttonWidth - textPad * 2 - (iconSvg ? iconSize + iconGap : 0));
|
|
4179
4814
|
const visibleText = this.truncateTextToWidth(text, availableTextWidth, fontSize);
|
|
4180
4815
|
const semanticBase = this.getSemanticVariantColor(variant);
|
|
4181
4816
|
const hasExplicitVariantColor = semanticBase !== void 0 || this.colorResolver.hasColor(variant);
|
|
4182
4817
|
const resolvedBase = this.resolveVariantColor(variant, this.renderTheme.primary);
|
|
4183
|
-
const
|
|
4184
|
-
const
|
|
4185
|
-
const
|
|
4186
|
-
|
|
4187
|
-
|
|
4818
|
+
const isDarkMode = this.options.theme === "dark";
|
|
4819
|
+
const bgColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.85) : isDarkMode ? "rgba(48, 48, 55, 0.9)" : "rgba(226, 232, 240, 0.9)";
|
|
4820
|
+
const textColor = hasExplicitVariantColor ? "#FFFFFF" : this.hexToRgba(this.resolveTextColor(), 0.85);
|
|
4821
|
+
const borderColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.7) : isDarkMode ? "rgba(75, 75, 88, 0.8)" : "rgba(100, 116, 139, 0.4)";
|
|
4822
|
+
const iconOffsetY = buttonY + (buttonHeight - iconSize) / 2;
|
|
4823
|
+
const iconX = iconAlign === "right" ? pos.x + buttonWidth - edgePad - iconSize : pos.x + edgePad;
|
|
4824
|
+
const textAlign = String(node.props.align || "center").toLowerCase();
|
|
4825
|
+
const sidePad = textPad + 4;
|
|
4826
|
+
let textX;
|
|
4827
|
+
let textAnchor;
|
|
4828
|
+
if (textAlign === "left") {
|
|
4829
|
+
textX = iconSvg && iconAlign === "left" ? pos.x + edgePad + iconSize + iconGap : pos.x + sidePad;
|
|
4830
|
+
textAnchor = "start";
|
|
4831
|
+
} else if (textAlign === "right") {
|
|
4832
|
+
textX = iconSvg && iconAlign === "right" ? pos.x + buttonWidth - edgePad - iconSize - iconGap : pos.x + buttonWidth - sidePad;
|
|
4833
|
+
textAnchor = "end";
|
|
4834
|
+
} else {
|
|
4835
|
+
textX = pos.x + buttonWidth / 2;
|
|
4836
|
+
textAnchor = "middle";
|
|
4837
|
+
}
|
|
4838
|
+
let svg = `<g${this.getDataNodeId(node)}>
|
|
4839
|
+
<rect x="${pos.x}" y="${buttonY}"
|
|
4188
4840
|
width="${buttonWidth}" height="${buttonHeight}"
|
|
4189
4841
|
rx="${radius}"
|
|
4190
4842
|
fill="${bgColor}"
|
|
4191
4843
|
stroke="${borderColor}"
|
|
4192
|
-
stroke-width="1"
|
|
4193
|
-
|
|
4194
|
-
|
|
4844
|
+
stroke-width="1"/>`;
|
|
4845
|
+
if (iconSvg) {
|
|
4846
|
+
svg += `
|
|
4847
|
+
<g transform="translate(${iconX}, ${iconOffsetY})">
|
|
4848
|
+
<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${textColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
4849
|
+
${this.extractSvgContent(iconSvg)}
|
|
4850
|
+
</svg>
|
|
4851
|
+
</g>`;
|
|
4852
|
+
}
|
|
4853
|
+
svg += `
|
|
4854
|
+
<text x="${textX}" y="${buttonY + buttonHeight / 2 + fontSize * 0.35}"
|
|
4855
|
+
font-family="Arial, Helvetica, sans-serif"
|
|
4195
4856
|
font-size="${fontSize}"
|
|
4196
4857
|
font-weight="${fontWeight}"
|
|
4197
4858
|
fill="${textColor}"
|
|
4198
|
-
text-anchor="
|
|
4859
|
+
text-anchor="${textAnchor}">${this.escapeXml(visibleText)}</text>
|
|
4199
4860
|
</g>`;
|
|
4861
|
+
return svg;
|
|
4200
4862
|
}
|
|
4201
4863
|
renderLink(node, pos) {
|
|
4202
4864
|
const text = String(node.props.text || "Link");
|
|
4203
4865
|
const variant = String(node.props.variant || "primary");
|
|
4866
|
+
const size = String(node.props.size || "md");
|
|
4867
|
+
const density = this.ir.project.style.density || "normal";
|
|
4204
4868
|
const fontSize = this.tokens.button.fontSize;
|
|
4205
4869
|
const fontWeight = this.tokens.button.fontWeight;
|
|
4206
4870
|
const paddingX = this.tokens.button.paddingX;
|
|
4207
|
-
const paddingY = this.tokens.button.paddingY;
|
|
4208
4871
|
const linkColor = this.resolveVariantColor(variant, this.renderTheme.primary);
|
|
4209
4872
|
const idealTextWidth = this.estimateTextWidth(text, fontSize);
|
|
4210
4873
|
const linkWidth = this.clampControlWidth(
|
|
4211
4874
|
Math.max(Math.ceil(idealTextWidth + paddingX * 2), 60),
|
|
4212
4875
|
pos.width
|
|
4213
4876
|
);
|
|
4214
|
-
const linkHeight =
|
|
4877
|
+
const linkHeight = Math.max(16, Math.min(resolveActionControlHeight(size, density), pos.height));
|
|
4215
4878
|
const availableTextWidth = Math.max(0, linkWidth - paddingX * 2);
|
|
4216
4879
|
const visibleText = this.truncateTextToWidth(text, availableTextWidth, fontSize);
|
|
4217
4880
|
const visibleTextWidth = Math.min(
|
|
@@ -4222,7 +4885,7 @@ var SVGRenderer = class {
|
|
|
4222
4885
|
const underlineY = centerY + 3;
|
|
4223
4886
|
return `<g${this.getDataNodeId(node)}>
|
|
4224
4887
|
<text x="${pos.x + linkWidth / 2}" y="${centerY}"
|
|
4225
|
-
font-family="
|
|
4888
|
+
font-family="Arial, Helvetica, sans-serif"
|
|
4226
4889
|
font-size="${fontSize}"
|
|
4227
4890
|
font-weight="${fontWeight}"
|
|
4228
4891
|
fill="${linkColor}"
|
|
@@ -4236,53 +4899,108 @@ var SVGRenderer = class {
|
|
|
4236
4899
|
renderInput(node, pos) {
|
|
4237
4900
|
const label = String(node.props.label || "");
|
|
4238
4901
|
const placeholder = String(node.props.placeholder || "");
|
|
4902
|
+
const iconLeftName = String(node.props.iconLeft || "").trim();
|
|
4903
|
+
const iconRightName = String(node.props.iconRight || "").trim();
|
|
4239
4904
|
const radius = this.tokens.input.radius;
|
|
4240
4905
|
const fontSize = this.tokens.input.fontSize;
|
|
4241
4906
|
const paddingX = this.tokens.input.paddingX;
|
|
4242
4907
|
const labelOffset = this.getControlLabelOffset(label);
|
|
4243
4908
|
const controlY = pos.y + labelOffset;
|
|
4244
4909
|
const controlHeight = Math.max(16, pos.height - labelOffset);
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4910
|
+
const iconSize = 16;
|
|
4911
|
+
const iconPad = 12;
|
|
4912
|
+
const iconInnerGap = 8;
|
|
4913
|
+
const iconLeftSvg = iconLeftName ? getIcon(iconLeftName) : null;
|
|
4914
|
+
const iconRightSvg = iconRightName ? getIcon(iconRightName) : null;
|
|
4915
|
+
const leftOffset = iconLeftSvg ? iconPad + iconSize + iconInnerGap : 0;
|
|
4916
|
+
const rightOffset = iconRightSvg ? iconPad + iconSize + iconInnerGap : 0;
|
|
4917
|
+
const textX = pos.x + (iconLeftSvg ? leftOffset : paddingX);
|
|
4918
|
+
const iconColor = this.hexToRgba(this.resolveMutedColor(), 0.8);
|
|
4919
|
+
const iconCenterY = controlY + (controlHeight - iconSize) / 2;
|
|
4920
|
+
let svg = `<g${this.getDataNodeId(node)}>`;
|
|
4921
|
+
if (label) {
|
|
4922
|
+
svg += `
|
|
4923
|
+
<text x="${pos.x + paddingX}" y="${this.getControlLabelBaselineY(pos.y)}"
|
|
4924
|
+
font-family="Arial, Helvetica, sans-serif"
|
|
4925
|
+
font-size="12"
|
|
4926
|
+
fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>`;
|
|
4927
|
+
}
|
|
4928
|
+
svg += `
|
|
4250
4929
|
<rect x="${pos.x}" y="${controlY}"
|
|
4251
4930
|
width="${pos.width}" height="${controlHeight}"
|
|
4252
4931
|
rx="${radius}"
|
|
4253
4932
|
fill="${this.renderTheme.cardBg}"
|
|
4254
4933
|
stroke="${this.renderTheme.border}"
|
|
4255
|
-
stroke-width="1"
|
|
4256
|
-
|
|
4257
|
-
|
|
4934
|
+
stroke-width="1"/>`;
|
|
4935
|
+
if (iconLeftSvg) {
|
|
4936
|
+
svg += `
|
|
4937
|
+
<g transform="translate(${pos.x + iconPad}, ${iconCenterY})">
|
|
4938
|
+
<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
4939
|
+
${this.extractSvgContent(iconLeftSvg)}
|
|
4940
|
+
</svg>
|
|
4941
|
+
</g>`;
|
|
4942
|
+
}
|
|
4943
|
+
if (iconRightSvg) {
|
|
4944
|
+
svg += `
|
|
4945
|
+
<g transform="translate(${pos.x + pos.width - iconPad - iconSize}, ${iconCenterY})">
|
|
4946
|
+
<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
4947
|
+
${this.extractSvgContent(iconRightSvg)}
|
|
4948
|
+
</svg>
|
|
4949
|
+
</g>`;
|
|
4950
|
+
}
|
|
4951
|
+
if (placeholder) {
|
|
4952
|
+
const availPlaceholderWidth = pos.width - (iconLeftSvg ? leftOffset : paddingX) - (iconRightSvg ? rightOffset : paddingX);
|
|
4953
|
+
const visiblePlaceholder = this.truncateTextToWidth(placeholder, Math.max(0, availPlaceholderWidth), fontSize);
|
|
4954
|
+
svg += `
|
|
4955
|
+
<text x="${textX}" y="${controlY + controlHeight / 2 + 5}"
|
|
4956
|
+
font-family="Arial, Helvetica, sans-serif"
|
|
4258
4957
|
font-size="${fontSize}"
|
|
4259
|
-
fill="${this.renderTheme.textMuted}">${this.escapeXml(
|
|
4260
|
-
|
|
4958
|
+
fill="${this.renderTheme.textMuted}">${this.escapeXml(visiblePlaceholder)}</text>`;
|
|
4959
|
+
}
|
|
4960
|
+
svg += "\n </g>";
|
|
4961
|
+
return svg;
|
|
4261
4962
|
}
|
|
4262
4963
|
renderTopbar(node, pos) {
|
|
4263
4964
|
const title = String(node.props.title || "App");
|
|
4264
4965
|
const subtitle = String(node.props.subtitle || "");
|
|
4265
4966
|
const actions = String(node.props.actions || "");
|
|
4266
4967
|
const user = String(node.props.user || "");
|
|
4267
|
-
const
|
|
4968
|
+
const variant = String(node.props.variant || "default");
|
|
4969
|
+
const accentColor = variant === "default" ? this.resolveAccentColor() : this.resolveVariantColor(variant, this.resolveAccentColor());
|
|
4970
|
+
const showBorder = this.parseBooleanProp(node.props.border, false);
|
|
4971
|
+
const showBackground = this.parseBooleanProp(node.props.background ?? node.props.backround, false);
|
|
4972
|
+
const radiusMap = {
|
|
4973
|
+
none: 0,
|
|
4974
|
+
sm: 4,
|
|
4975
|
+
md: this.tokens.card.radius,
|
|
4976
|
+
lg: 12,
|
|
4977
|
+
xl: 16
|
|
4978
|
+
};
|
|
4979
|
+
const topbarRadius = radiusMap[String(node.props.radius || "md")] ?? this.tokens.card.radius;
|
|
4268
4980
|
const topbar = this.calculateTopbarLayout(node, pos, title, subtitle, actions, user);
|
|
4269
|
-
let svg = `<g${this.getDataNodeId(node)}
|
|
4981
|
+
let svg = `<g${this.getDataNodeId(node)}>`;
|
|
4982
|
+
if (showBorder || showBackground) {
|
|
4983
|
+
const bg = showBackground ? this.renderTheme.cardBg : "none";
|
|
4984
|
+
const stroke = showBorder ? this.renderTheme.border : "none";
|
|
4985
|
+
svg += `
|
|
4270
4986
|
<rect x="${pos.x}" y="${pos.y}"
|
|
4271
4987
|
width="${pos.width}" height="${pos.height}"
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
stroke
|
|
4275
|
-
|
|
4988
|
+
rx="${topbarRadius}"
|
|
4989
|
+
fill="${bg}"
|
|
4990
|
+
stroke="${stroke}"
|
|
4991
|
+
stroke-width="1"/>`;
|
|
4992
|
+
}
|
|
4993
|
+
svg += `
|
|
4276
4994
|
<!-- Title -->
|
|
4277
4995
|
<text x="${topbar.textX}" y="${topbar.titleY}"
|
|
4278
|
-
font-family="
|
|
4996
|
+
font-family="Arial, Helvetica, sans-serif"
|
|
4279
4997
|
font-size="18"
|
|
4280
4998
|
font-weight="600"
|
|
4281
4999
|
fill="${this.renderTheme.text}">${this.escapeXml(topbar.visibleTitle)}</text>`;
|
|
4282
5000
|
if (topbar.hasSubtitle) {
|
|
4283
5001
|
svg += `
|
|
4284
5002
|
<text x="${topbar.textX}" y="${topbar.subtitleY}"
|
|
4285
|
-
font-family="
|
|
5003
|
+
font-family="Arial, Helvetica, sans-serif"
|
|
4286
5004
|
font-size="13"
|
|
4287
5005
|
fill="${this.renderTheme.textMuted}">${this.escapeXml(topbar.visibleSubtitle)}</text>`;
|
|
4288
5006
|
}
|
|
@@ -4310,7 +5028,7 @@ var SVGRenderer = class {
|
|
|
4310
5028
|
fill="${accentColor}"
|
|
4311
5029
|
stroke="none"/>
|
|
4312
5030
|
<text x="${action.x + action.width / 2}" y="${action.y + action.height / 2 + 4}"
|
|
4313
|
-
font-family="
|
|
5031
|
+
font-family="Arial, Helvetica, sans-serif"
|
|
4314
5032
|
font-size="12"
|
|
4315
5033
|
font-weight="600"
|
|
4316
5034
|
fill="white"
|
|
@@ -4326,7 +5044,7 @@ var SVGRenderer = class {
|
|
|
4326
5044
|
stroke="${this.renderTheme.border}"
|
|
4327
5045
|
stroke-width="1"/>
|
|
4328
5046
|
<text x="${topbar.userBadge.x + topbar.userBadge.width / 2}" y="${topbar.userBadge.y + topbar.userBadge.height / 2 + 4}"
|
|
4329
|
-
font-family="
|
|
5047
|
+
font-family="Arial, Helvetica, sans-serif"
|
|
4330
5048
|
font-size="12"
|
|
4331
5049
|
fill="${this.renderTheme.text}"
|
|
4332
5050
|
text-anchor="middle">${this.escapeXml(topbar.userBadge.label)}</text>`;
|
|
@@ -4389,77 +5107,165 @@ var SVGRenderer = class {
|
|
|
4389
5107
|
</g>`;
|
|
4390
5108
|
output.push(svg);
|
|
4391
5109
|
}
|
|
5110
|
+
renderSplitDecoration(node, pos, output) {
|
|
5111
|
+
if (node.kind !== "container") return;
|
|
5112
|
+
const gap = this.resolveSpacing(node.style.gap);
|
|
5113
|
+
const leftParam = Number(node.params.left);
|
|
5114
|
+
const rightParam = Number(node.params.right);
|
|
5115
|
+
const hasLeft = node.params.left !== void 0;
|
|
5116
|
+
const hasRight = node.params.right !== void 0 && node.params.left === void 0;
|
|
5117
|
+
const fixedLeftWidth = Number.isFinite(leftParam) && leftParam > 0 ? leftParam : 250;
|
|
5118
|
+
const fixedRightWidth = Number.isFinite(rightParam) && rightParam > 0 ? rightParam : 250;
|
|
5119
|
+
const backgroundKey = String(node.style.background || "").trim();
|
|
5120
|
+
const showBorder = this.parseBooleanProp(node.params.border, false);
|
|
5121
|
+
if (backgroundKey) {
|
|
5122
|
+
const fill = this.colorResolver.resolveColor(backgroundKey, this.renderTheme.cardBg);
|
|
5123
|
+
if (hasRight) {
|
|
5124
|
+
const panelX = pos.x + Math.max(0, pos.width - fixedRightWidth);
|
|
5125
|
+
output.push(`<g>
|
|
5126
|
+
<rect x="${panelX}" y="${pos.y}" width="${Math.max(1, fixedRightWidth)}" height="${pos.height}" fill="${fill}" stroke="none"/>
|
|
5127
|
+
</g>`);
|
|
5128
|
+
} else if (hasLeft || !hasRight) {
|
|
5129
|
+
output.push(`<g>
|
|
5130
|
+
<rect x="${pos.x}" y="${pos.y}" width="${Math.max(1, fixedLeftWidth)}" height="${pos.height}" fill="${fill}" stroke="none"/>
|
|
5131
|
+
</g>`);
|
|
5132
|
+
}
|
|
5133
|
+
}
|
|
5134
|
+
if (showBorder) {
|
|
5135
|
+
const dividerX = hasRight ? pos.x + Math.max(0, pos.width - fixedRightWidth - gap / 2) : pos.x + Math.max(0, fixedLeftWidth + gap / 2);
|
|
5136
|
+
output.push(`<g>
|
|
5137
|
+
<line x1="${dividerX}" y1="${pos.y}" x2="${dividerX}" y2="${pos.y + pos.height}" stroke="${this.renderTheme.border}" stroke-width="1"/>
|
|
5138
|
+
</g>`);
|
|
5139
|
+
}
|
|
5140
|
+
}
|
|
4392
5141
|
renderTable(node, pos) {
|
|
4393
5142
|
const title = String(node.props.title || "");
|
|
4394
5143
|
const columnsStr = String(node.props.columns || "Col1,Col2,Col3");
|
|
4395
|
-
const columns = columnsStr.split(",").map((c) => c.trim());
|
|
5144
|
+
const columns = columnsStr.split(",").map((c) => c.trim()).filter(Boolean);
|
|
4396
5145
|
const rowCount = Number(node.props.rows || node.props.rowsMock || 5);
|
|
4397
5146
|
const mockStr = String(node.props.mock || "");
|
|
4398
5147
|
const random = this.parseBooleanProp(node.props.random, false);
|
|
4399
|
-
const
|
|
4400
|
-
const
|
|
4401
|
-
const pageCount = Number(
|
|
5148
|
+
const pagination = this.parseBooleanProp(node.props.pagination, false);
|
|
5149
|
+
const parsedPageCount = Number(node.props.pages || 5);
|
|
5150
|
+
const pageCount = Number.isFinite(parsedPageCount) && parsedPageCount > 0 ? Math.floor(parsedPageCount) : 5;
|
|
4402
5151
|
const paginationAlign = String(node.props.paginationAlign || "right");
|
|
5152
|
+
const actions = String(node.props.actions || "").split(",").map((value) => value.trim()).filter(Boolean);
|
|
5153
|
+
const hasActions = actions.length > 0;
|
|
5154
|
+
const caption = String(node.props.caption || "").trim();
|
|
5155
|
+
const hasCaption = caption.length > 0;
|
|
5156
|
+
const showOuterBorder = this.parseBooleanProp(node.props.border, false);
|
|
5157
|
+
const showOuterBackground = this.parseBooleanProp(
|
|
5158
|
+
node.props.background ?? node.props.backround,
|
|
5159
|
+
false
|
|
5160
|
+
);
|
|
5161
|
+
const showInnerBorder = this.parseBooleanProp(node.props.innerBorder, true);
|
|
5162
|
+
const rawCaptionAlign = String(node.props.captionAlign || "");
|
|
5163
|
+
const captionAlign = rawCaptionAlign === "left" || rawCaptionAlign === "center" || rawCaptionAlign === "right" ? rawCaptionAlign : paginationAlign === "left" ? "right" : "left";
|
|
5164
|
+
const sameFooterAlign = hasCaption && pagination && captionAlign === paginationAlign;
|
|
4403
5165
|
const mockTypes = mockStr ? mockStr.split(",").map((m) => m.trim()).filter(Boolean) : [];
|
|
4404
|
-
|
|
4405
|
-
|
|
5166
|
+
const safeColumns = columns.length > 0 ? columns : ["Column"];
|
|
5167
|
+
while (mockTypes.length < safeColumns.length) {
|
|
5168
|
+
const inferred = MockDataGenerator.inferMockTypeFromColumn(safeColumns[mockTypes.length] || "item");
|
|
4406
5169
|
mockTypes.push(inferred);
|
|
4407
5170
|
}
|
|
4408
5171
|
const headerHeight = 44;
|
|
4409
5172
|
const rowHeight = 36;
|
|
4410
|
-
const
|
|
5173
|
+
const actionColumnWidth = hasActions ? Math.max(96, Math.min(180, actions.length * 26 + 28)) : 0;
|
|
5174
|
+
const dataWidth = Math.max(20, pos.width - actionColumnWidth);
|
|
5175
|
+
const dataColWidth = dataWidth / safeColumns.length;
|
|
4411
5176
|
const mockRows = [];
|
|
4412
5177
|
for (let rowIdx = 0; rowIdx < rowCount; rowIdx++) {
|
|
4413
5178
|
const row = {};
|
|
4414
|
-
|
|
5179
|
+
safeColumns.forEach((col, colIdx) => {
|
|
4415
5180
|
const mockType = mockTypes[colIdx] || MockDataGenerator.inferMockTypeFromColumn(col) || "item";
|
|
4416
5181
|
row[col] = MockDataGenerator.getMockValue(mockType, rowIdx, random);
|
|
4417
5182
|
});
|
|
4418
5183
|
mockRows.push(row);
|
|
4419
5184
|
}
|
|
4420
|
-
let svg = `<g${this.getDataNodeId(node)}
|
|
5185
|
+
let svg = `<g${this.getDataNodeId(node)}>`;
|
|
5186
|
+
if (showOuterBorder || showOuterBackground) {
|
|
5187
|
+
const outerFill = showOuterBackground ? this.renderTheme.cardBg : "none";
|
|
5188
|
+
const outerStroke = showOuterBorder ? this.renderTheme.border : "none";
|
|
5189
|
+
svg += `
|
|
4421
5190
|
<rect x="${pos.x}" y="${pos.y}"
|
|
4422
5191
|
width="${pos.width}" height="${pos.height}"
|
|
4423
5192
|
rx="8"
|
|
4424
|
-
fill="${
|
|
4425
|
-
stroke="${
|
|
5193
|
+
fill="${outerFill}"
|
|
5194
|
+
stroke="${outerStroke}"
|
|
4426
5195
|
stroke-width="1"/>`;
|
|
5196
|
+
}
|
|
4427
5197
|
if (title) {
|
|
4428
5198
|
svg += `
|
|
4429
5199
|
<text x="${pos.x + 16}" y="${pos.y + 24}"
|
|
4430
|
-
font-family="
|
|
5200
|
+
font-family="Arial, Helvetica, sans-serif"
|
|
4431
5201
|
font-size="13"
|
|
4432
5202
|
font-weight="600"
|
|
4433
5203
|
fill="${this.renderTheme.text}">${this.escapeXml(title)}</text>`;
|
|
4434
5204
|
}
|
|
4435
5205
|
const headerY = pos.y + (title ? 32 : 0);
|
|
4436
|
-
|
|
5206
|
+
if (showInnerBorder) {
|
|
5207
|
+
svg += `
|
|
4437
5208
|
<line x1="${pos.x}" y1="${headerY + headerHeight}" x2="${pos.x + pos.width}" y2="${headerY + headerHeight}"
|
|
4438
5209
|
stroke="${this.renderTheme.border}" stroke-width="1"/>`;
|
|
4439
|
-
|
|
5210
|
+
}
|
|
5211
|
+
safeColumns.forEach((col, i) => {
|
|
4440
5212
|
svg += `
|
|
4441
|
-
<text x="${pos.x + i *
|
|
4442
|
-
font-family="
|
|
5213
|
+
<text x="${pos.x + i * dataColWidth + 12}" y="${headerY + 26}"
|
|
5214
|
+
font-family="Arial, Helvetica, sans-serif"
|
|
4443
5215
|
font-size="11"
|
|
4444
5216
|
font-weight="600"
|
|
4445
5217
|
fill="${this.renderTheme.textMuted}">${this.escapeXml(col)}</text>`;
|
|
4446
5218
|
});
|
|
5219
|
+
if (hasActions && showInnerBorder) {
|
|
5220
|
+
const dividerX = pos.x + dataWidth;
|
|
5221
|
+
svg += `
|
|
5222
|
+
<line x1="${dividerX}" y1="${headerY + headerHeight}" x2="${dividerX}" y2="${headerY + headerHeight + mockRows.length * rowHeight}"
|
|
5223
|
+
stroke="${this.renderTheme.border}" stroke-width="1"/>`;
|
|
5224
|
+
}
|
|
4447
5225
|
mockRows.forEach((row, rowIdx) => {
|
|
4448
5226
|
const rowY = headerY + headerHeight + rowIdx * rowHeight;
|
|
4449
|
-
|
|
5227
|
+
if (showInnerBorder) {
|
|
5228
|
+
svg += `
|
|
4450
5229
|
<line x1="${pos.x}" y1="${rowY + rowHeight}" x2="${pos.x + pos.width}" y2="${rowY + rowHeight}"
|
|
4451
5230
|
stroke="${this.renderTheme.border}" stroke-width="0.5"/>`;
|
|
4452
|
-
|
|
5231
|
+
}
|
|
5232
|
+
safeColumns.forEach((col, colIdx) => {
|
|
4453
5233
|
const cellValue = row[col] || "";
|
|
4454
5234
|
svg += `
|
|
4455
|
-
<text x="${pos.x + colIdx *
|
|
4456
|
-
font-family="
|
|
5235
|
+
<text x="${pos.x + colIdx * dataColWidth + 12}" y="${rowY + 22}"
|
|
5236
|
+
font-family="Arial, Helvetica, sans-serif"
|
|
4457
5237
|
font-size="12"
|
|
4458
5238
|
fill="${this.renderTheme.text}">${this.escapeXml(cellValue)}</text>`;
|
|
4459
5239
|
});
|
|
5240
|
+
if (hasActions) {
|
|
5241
|
+
const iconSize = 14;
|
|
5242
|
+
const buttonSize = 22;
|
|
5243
|
+
const buttonGap = 6;
|
|
5244
|
+
const actionsWidth = actions.length * buttonSize + Math.max(0, actions.length - 1) * buttonGap;
|
|
5245
|
+
let currentX = pos.x + pos.width - 12 - actionsWidth;
|
|
5246
|
+
const buttonY = rowY + (rowHeight - buttonSize) / 2;
|
|
5247
|
+
actions.forEach((actionIcon) => {
|
|
5248
|
+
const iconSvg = getIcon(actionIcon);
|
|
5249
|
+
const iconX = currentX + (buttonSize - iconSize) / 2;
|
|
5250
|
+
const iconY = buttonY + (buttonSize - iconSize) / 2;
|
|
5251
|
+
svg += `
|
|
5252
|
+
<rect x="${currentX}" y="${buttonY}" width="${buttonSize}" height="${buttonSize}" rx="4"
|
|
5253
|
+
fill="${this.renderTheme.cardBg}" stroke="${showInnerBorder ? this.renderTheme.border : "none"}" stroke-width="1"/>`;
|
|
5254
|
+
if (iconSvg) {
|
|
5255
|
+
svg += `
|
|
5256
|
+
<g transform="translate(${iconX}, ${iconY})">
|
|
5257
|
+
<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${this.hexToRgba(this.resolveTextColor(), 0.75)}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
5258
|
+
${this.extractSvgContent(iconSvg)}
|
|
5259
|
+
</svg>
|
|
5260
|
+
</g>`;
|
|
5261
|
+
}
|
|
5262
|
+
currentX += buttonSize + buttonGap;
|
|
5263
|
+
});
|
|
5264
|
+
}
|
|
4460
5265
|
});
|
|
5266
|
+
const footerTop = headerY + headerHeight + mockRows.length * rowHeight + 16;
|
|
4461
5267
|
if (pagination) {
|
|
4462
|
-
const paginationY =
|
|
5268
|
+
const paginationY = sameFooterAlign ? footerTop + 18 + 8 : footerTop;
|
|
4463
5269
|
const buttonWidth = 40;
|
|
4464
5270
|
const buttonHeight = 32;
|
|
4465
5271
|
const gap = 8;
|
|
@@ -4472,18 +5278,25 @@ var SVGRenderer = class {
|
|
|
4472
5278
|
} else {
|
|
4473
5279
|
startX = pos.x + pos.width - totalWidth - 16;
|
|
4474
5280
|
}
|
|
5281
|
+
const previousIcon = getIcon("chevron-left");
|
|
4475
5282
|
svg += `
|
|
4476
5283
|
<rect x="${startX}" y="${paginationY}"
|
|
4477
5284
|
width="${buttonWidth}" height="${buttonHeight}"
|
|
4478
5285
|
rx="4"
|
|
4479
5286
|
fill="${this.renderTheme.cardBg}"
|
|
4480
5287
|
stroke="${this.renderTheme.border}"
|
|
4481
|
-
stroke-width="1"
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
5288
|
+
stroke-width="1"/>`;
|
|
5289
|
+
if (previousIcon) {
|
|
5290
|
+
const iconSize = 14;
|
|
5291
|
+
const iconX = startX + (buttonWidth - iconSize) / 2;
|
|
5292
|
+
const iconY = paginationY + (buttonHeight - iconSize) / 2;
|
|
5293
|
+
svg += `
|
|
5294
|
+
<g transform="translate(${iconX}, ${iconY})">
|
|
5295
|
+
<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${this.resolveTextColor()}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
5296
|
+
${this.extractSvgContent(previousIcon)}
|
|
5297
|
+
</svg>
|
|
5298
|
+
</g>`;
|
|
5299
|
+
}
|
|
4487
5300
|
for (let i = 1; i <= pageCount; i++) {
|
|
4488
5301
|
const btnX = startX + (buttonWidth + gap) * i;
|
|
4489
5302
|
const isActive = i === 1;
|
|
@@ -4497,24 +5310,49 @@ var SVGRenderer = class {
|
|
|
4497
5310
|
stroke="${this.renderTheme.border}"
|
|
4498
5311
|
stroke-width="1"/>
|
|
4499
5312
|
<text x="${btnX + buttonWidth / 2}" y="${paginationY + buttonHeight / 2 + 4}"
|
|
4500
|
-
font-family="
|
|
5313
|
+
font-family="Arial, Helvetica, sans-serif"
|
|
4501
5314
|
font-size="14"
|
|
4502
5315
|
fill="${textColor}"
|
|
4503
5316
|
text-anchor="middle">${i}</text>`;
|
|
4504
5317
|
}
|
|
4505
5318
|
const nextX = startX + (buttonWidth + gap) * (pageCount + 1);
|
|
5319
|
+
const nextIcon = getIcon("chevron-right");
|
|
4506
5320
|
svg += `
|
|
4507
5321
|
<rect x="${nextX}" y="${paginationY}"
|
|
4508
5322
|
width="${buttonWidth}" height="${buttonHeight}"
|
|
4509
5323
|
rx="4"
|
|
4510
5324
|
fill="${this.renderTheme.cardBg}"
|
|
4511
5325
|
stroke="${this.renderTheme.border}"
|
|
4512
|
-
stroke-width="1"
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
5326
|
+
stroke-width="1"/>`;
|
|
5327
|
+
if (nextIcon) {
|
|
5328
|
+
const iconSize = 14;
|
|
5329
|
+
const iconX = nextX + (buttonWidth - iconSize) / 2;
|
|
5330
|
+
const iconY = paginationY + (buttonHeight - iconSize) / 2;
|
|
5331
|
+
svg += `
|
|
5332
|
+
<g transform="translate(${iconX}, ${iconY})">
|
|
5333
|
+
<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${this.resolveTextColor()}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
5334
|
+
${this.extractSvgContent(nextIcon)}
|
|
5335
|
+
</svg>
|
|
5336
|
+
</g>`;
|
|
5337
|
+
}
|
|
5338
|
+
}
|
|
5339
|
+
if (hasCaption) {
|
|
5340
|
+
const captionY = sameFooterAlign ? footerTop + 12 : footerTop + (pagination ? 21 : 12);
|
|
5341
|
+
let captionX = pos.x + 16;
|
|
5342
|
+
let textAnchor = "start";
|
|
5343
|
+
if (captionAlign === "center") {
|
|
5344
|
+
captionX = pos.x + pos.width / 2;
|
|
5345
|
+
textAnchor = "middle";
|
|
5346
|
+
} else if (captionAlign === "right") {
|
|
5347
|
+
captionX = pos.x + pos.width - 16;
|
|
5348
|
+
textAnchor = "end";
|
|
5349
|
+
}
|
|
5350
|
+
svg += `
|
|
5351
|
+
<text x="${captionX}" y="${captionY}"
|
|
5352
|
+
font-family="Arial, Helvetica, sans-serif"
|
|
5353
|
+
font-size="12"
|
|
5354
|
+
fill="${this.hexToRgba(this.resolveTextColor(), 0.75)}"
|
|
5355
|
+
text-anchor="${textAnchor}">${this.escapeXml(caption)}</text>`;
|
|
4518
5356
|
}
|
|
4519
5357
|
svg += "\n </g>";
|
|
4520
5358
|
return svg;
|
|
@@ -4629,7 +5467,7 @@ var SVGRenderer = class {
|
|
|
4629
5467
|
// TEXT/CONTENT COMPONENTS
|
|
4630
5468
|
// ============================================================================
|
|
4631
5469
|
renderText(node, pos) {
|
|
4632
|
-
const text = String(node.props.
|
|
5470
|
+
const text = String(node.props.text || "Text content");
|
|
4633
5471
|
const fontSize = this.tokens.text.fontSize;
|
|
4634
5472
|
const lineHeightPx = Math.ceil(fontSize * this.tokens.text.lineHeight);
|
|
4635
5473
|
const lines = this.wrapTextToLines(text, pos.width, fontSize);
|
|
@@ -4639,7 +5477,7 @@ var SVGRenderer = class {
|
|
|
4639
5477
|
).join("");
|
|
4640
5478
|
return `<g${this.getDataNodeId(node)}>
|
|
4641
5479
|
<text x="${pos.x}" y="${firstLineY}"
|
|
4642
|
-
font-family="
|
|
5480
|
+
font-family="Arial, Helvetica, sans-serif"
|
|
4643
5481
|
font-size="${fontSize}"
|
|
4644
5482
|
fill="${this.renderTheme.text}">${tspans}</text>
|
|
4645
5483
|
</g>`;
|
|
@@ -4648,7 +5486,7 @@ var SVGRenderer = class {
|
|
|
4648
5486
|
const text = String(node.props.text || "Label");
|
|
4649
5487
|
return `<g${this.getDataNodeId(node)}>
|
|
4650
5488
|
<text x="${pos.x}" y="${pos.y + 12}"
|
|
4651
|
-
font-family="
|
|
5489
|
+
font-family="Arial, Helvetica, sans-serif"
|
|
4652
5490
|
font-size="12"
|
|
4653
5491
|
fill="${this.renderTheme.textMuted}">${this.escapeXml(text)}</text>
|
|
4654
5492
|
</g>`;
|
|
@@ -4682,7 +5520,7 @@ var SVGRenderer = class {
|
|
|
4682
5520
|
const placeholderY = controlY + fontSize + 6;
|
|
4683
5521
|
return `<g${this.getDataNodeId(node)}>
|
|
4684
5522
|
${label ? `<text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
|
|
4685
|
-
font-family="
|
|
5523
|
+
font-family="Arial, Helvetica, sans-serif"
|
|
4686
5524
|
font-size="12"
|
|
4687
5525
|
fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>` : ""}
|
|
4688
5526
|
<rect x="${pos.x}" y="${controlY}"
|
|
@@ -4692,7 +5530,7 @@ var SVGRenderer = class {
|
|
|
4692
5530
|
stroke="${this.renderTheme.border}"
|
|
4693
5531
|
stroke-width="1"/>
|
|
4694
5532
|
<text x="${pos.x + paddingX}" y="${placeholderY}"
|
|
4695
|
-
font-family="
|
|
5533
|
+
font-family="Arial, Helvetica, sans-serif"
|
|
4696
5534
|
font-size="${fontSize}"
|
|
4697
5535
|
fill="${this.renderTheme.textMuted}">${this.escapeXml(placeholder)}</text>
|
|
4698
5536
|
</g>`;
|
|
@@ -4700,30 +5538,66 @@ var SVGRenderer = class {
|
|
|
4700
5538
|
renderSelect(node, pos) {
|
|
4701
5539
|
const label = String(node.props.label || "");
|
|
4702
5540
|
const placeholder = String(node.props.placeholder || "Select...");
|
|
5541
|
+
const iconLeftName = String(node.props.iconLeft || "").trim();
|
|
5542
|
+
const iconRightName = String(node.props.iconRight || "").trim();
|
|
4703
5543
|
const labelOffset = this.getControlLabelOffset(label);
|
|
4704
5544
|
const controlY = pos.y + labelOffset;
|
|
4705
5545
|
const controlHeight = Math.max(16, pos.height - labelOffset);
|
|
4706
5546
|
const centerY = controlY + controlHeight / 2 + 5;
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
font-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
5547
|
+
const iconSize = 16;
|
|
5548
|
+
const iconPad = 12;
|
|
5549
|
+
const iconInnerGap = 8;
|
|
5550
|
+
const iconLeftSvg = iconLeftName ? getIcon(iconLeftName) : null;
|
|
5551
|
+
const iconRightSvg = iconRightName ? getIcon(iconRightName) : null;
|
|
5552
|
+
const leftOffset = iconLeftSvg ? iconPad + iconSize + iconInnerGap : 0;
|
|
5553
|
+
const chevronWidth = 20;
|
|
5554
|
+
const iconColor = this.hexToRgba(this.resolveMutedColor(), 0.8);
|
|
5555
|
+
const iconCenterY = controlY + (controlHeight - iconSize) / 2;
|
|
5556
|
+
let svg = `<g${this.getDataNodeId(node)}>`;
|
|
5557
|
+
if (label) {
|
|
5558
|
+
svg += `
|
|
5559
|
+
<text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
|
|
5560
|
+
font-family="Arial, Helvetica, sans-serif"
|
|
5561
|
+
font-size="12"
|
|
5562
|
+
fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>`;
|
|
5563
|
+
}
|
|
5564
|
+
svg += `
|
|
5565
|
+
<rect x="${pos.x}" y="${controlY}"
|
|
5566
|
+
width="${pos.width}" height="${controlHeight}"
|
|
5567
|
+
rx="6"
|
|
5568
|
+
fill="${this.renderTheme.cardBg}"
|
|
5569
|
+
stroke="${this.renderTheme.border}"
|
|
5570
|
+
stroke-width="1"/>`;
|
|
5571
|
+
if (iconLeftSvg) {
|
|
5572
|
+
svg += `
|
|
5573
|
+
<g transform="translate(${pos.x + iconPad}, ${iconCenterY})">
|
|
5574
|
+
<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
5575
|
+
${this.extractSvgContent(iconLeftSvg)}
|
|
5576
|
+
</svg>
|
|
5577
|
+
</g>`;
|
|
5578
|
+
}
|
|
5579
|
+
if (iconRightSvg) {
|
|
5580
|
+
svg += `
|
|
5581
|
+
<g transform="translate(${pos.x + pos.width - chevronWidth - iconPad - iconSize}, ${iconCenterY})">
|
|
5582
|
+
<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
5583
|
+
${this.extractSvgContent(iconRightSvg)}
|
|
5584
|
+
</svg>
|
|
5585
|
+
</g>`;
|
|
5586
|
+
}
|
|
5587
|
+
const textX = pos.x + (iconLeftSvg ? leftOffset : 12);
|
|
5588
|
+
const availPlaceholderWidth = pos.width - (iconLeftSvg ? leftOffset : 12) - chevronWidth - (iconRightSvg ? iconPad + iconSize + iconInnerGap : 0);
|
|
5589
|
+
const visiblePlaceholder = this.truncateTextToWidth(placeholder, Math.max(0, availPlaceholderWidth), 14);
|
|
5590
|
+
svg += `
|
|
5591
|
+
<text x="${textX}" y="${centerY}"
|
|
5592
|
+
font-family="Arial, Helvetica, sans-serif"
|
|
5593
|
+
font-size="14"
|
|
5594
|
+
fill="${this.renderTheme.textMuted}">${this.escapeXml(visiblePlaceholder)}</text>
|
|
5595
|
+
<text x="${pos.x + pos.width - 20}" y="${centerY}"
|
|
5596
|
+
font-family="Arial, Helvetica, sans-serif"
|
|
5597
|
+
font-size="16"
|
|
4725
5598
|
fill="${this.renderTheme.textMuted}">\u25BC</text>
|
|
4726
5599
|
</g>`;
|
|
5600
|
+
return svg;
|
|
4727
5601
|
}
|
|
4728
5602
|
renderCheckbox(node, pos) {
|
|
4729
5603
|
const label = String(node.props.label || "Checkbox");
|
|
@@ -4739,12 +5613,12 @@ var SVGRenderer = class {
|
|
|
4739
5613
|
stroke="${this.renderTheme.border}"
|
|
4740
5614
|
stroke-width="1"/>
|
|
4741
5615
|
${checked ? `<text x="${pos.x + checkboxSize / 2}" y="${checkboxY + 14}"
|
|
4742
|
-
font-family="
|
|
5616
|
+
font-family="Arial, Helvetica, sans-serif"
|
|
4743
5617
|
font-size="12"
|
|
4744
5618
|
fill="white"
|
|
4745
5619
|
text-anchor="middle">\u2713</text>` : ""}
|
|
4746
5620
|
<text x="${pos.x + checkboxSize + 12}" y="${pos.y + pos.height / 2 + 5}"
|
|
4747
|
-
font-family="
|
|
5621
|
+
font-family="Arial, Helvetica, sans-serif"
|
|
4748
5622
|
font-size="14"
|
|
4749
5623
|
fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>
|
|
4750
5624
|
</g>`;
|
|
@@ -4765,7 +5639,7 @@ var SVGRenderer = class {
|
|
|
4765
5639
|
r="${radioSize / 3.5}"
|
|
4766
5640
|
fill="${controlColor}"/>` : ""}
|
|
4767
5641
|
<text x="${pos.x + radioSize + 12}" y="${pos.y + pos.height / 2 + 5}"
|
|
4768
|
-
font-family="
|
|
5642
|
+
font-family="Arial, Helvetica, sans-serif"
|
|
4769
5643
|
font-size="14"
|
|
4770
5644
|
fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>
|
|
4771
5645
|
</g>`;
|
|
@@ -4787,7 +5661,7 @@ var SVGRenderer = class {
|
|
|
4787
5661
|
r="8"
|
|
4788
5662
|
fill="white"/>
|
|
4789
5663
|
<text x="${pos.x + toggleWidth + 12}" y="${pos.y + pos.height / 2 + 5}"
|
|
4790
|
-
font-family="
|
|
5664
|
+
font-family="Arial, Helvetica, sans-serif"
|
|
4791
5665
|
font-size="14"
|
|
4792
5666
|
fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>
|
|
4793
5667
|
</g>`;
|
|
@@ -4817,7 +5691,7 @@ var SVGRenderer = class {
|
|
|
4817
5691
|
stroke-width="1"/>
|
|
4818
5692
|
<!-- Title -->
|
|
4819
5693
|
<text x="${pos.x + padding}" y="${pos.y + padding + 8}"
|
|
4820
|
-
font-family="
|
|
5694
|
+
font-family="Arial, Helvetica, sans-serif"
|
|
4821
5695
|
font-size="14"
|
|
4822
5696
|
font-weight="600"
|
|
4823
5697
|
fill="${this.renderTheme.text}">${this.escapeXml(title)}</text>
|
|
@@ -4833,7 +5707,7 @@ var SVGRenderer = class {
|
|
|
4833
5707
|
fill="${isActive ? this.renderTheme.primary : "transparent"}"
|
|
4834
5708
|
stroke="none"/>
|
|
4835
5709
|
<text x="${pos.x + 16}" y="${itemY + 22}"
|
|
4836
|
-
font-family="
|
|
5710
|
+
font-family="Arial, Helvetica, sans-serif"
|
|
4837
5711
|
font-size="13"
|
|
4838
5712
|
fill="${isActive ? "white" : this.renderTheme.textMuted}">${this.escapeXml(item)}</text>`;
|
|
4839
5713
|
});
|
|
@@ -4859,7 +5733,7 @@ var SVGRenderer = class {
|
|
|
4859
5733
|
stroke="${isActive ? "none" : this.renderTheme.border}"
|
|
4860
5734
|
stroke-width="1"/>
|
|
4861
5735
|
<text x="${tabX + tabWidth / 2}" y="${pos.y + 28}"
|
|
4862
|
-
font-family="
|
|
5736
|
+
font-family="Arial, Helvetica, sans-serif"
|
|
4863
5737
|
font-size="13"
|
|
4864
5738
|
font-weight="${isActive ? "600" : "500"}"
|
|
4865
5739
|
fill="${isActive ? "white" : this.renderTheme.text}"
|
|
@@ -4922,12 +5796,12 @@ var SVGRenderer = class {
|
|
|
4922
5796
|
rx="6"
|
|
4923
5797
|
fill="${bgColor}"/>
|
|
4924
5798
|
${hasTitle ? `<text x="${contentX}" y="${titleStartY}"
|
|
4925
|
-
font-family="
|
|
5799
|
+
font-family="Arial, Helvetica, sans-serif"
|
|
4926
5800
|
font-size="${fontSize}"
|
|
4927
5801
|
font-weight="700"
|
|
4928
5802
|
fill="${bgColor}">${titleTspans}</text>` : ""}
|
|
4929
5803
|
<text x="${contentX}" y="${textStartY}"
|
|
4930
|
-
font-family="
|
|
5804
|
+
font-family="Arial, Helvetica, sans-serif"
|
|
4931
5805
|
font-size="${fontSize}"
|
|
4932
5806
|
fill="${bgColor}">${textTspans}</text>
|
|
4933
5807
|
</g>`;
|
|
@@ -4948,7 +5822,7 @@ var SVGRenderer = class {
|
|
|
4948
5822
|
fill="${bgColor}"
|
|
4949
5823
|
stroke="none"/>
|
|
4950
5824
|
<text x="${pos.x + pos.width / 2}" y="${pos.y + pos.height / 2 + 4}"
|
|
4951
|
-
font-family="
|
|
5825
|
+
font-family="Arial, Helvetica, sans-serif"
|
|
4952
5826
|
font-size="${fontSize}"
|
|
4953
5827
|
font-weight="600"
|
|
4954
5828
|
fill="${textColor}"
|
|
@@ -4987,20 +5861,20 @@ var SVGRenderer = class {
|
|
|
4987
5861
|
stroke-width="1"/>
|
|
4988
5862
|
|
|
4989
5863
|
<text x="${modalX + padding}" y="${modalY + padding + 16}"
|
|
4990
|
-
font-family="
|
|
5864
|
+
font-family="Arial, Helvetica, sans-serif"
|
|
4991
5865
|
font-size="16"
|
|
4992
5866
|
font-weight="600"
|
|
4993
5867
|
fill="${this.renderTheme.text}">${this.escapeXml(title)}</text>
|
|
4994
5868
|
|
|
4995
5869
|
<!-- Close button -->
|
|
4996
5870
|
<text x="${modalX + pos.width - 16}" y="${modalY + padding + 12}"
|
|
4997
|
-
font-family="
|
|
5871
|
+
font-family="Arial, Helvetica, sans-serif"
|
|
4998
5872
|
font-size="18"
|
|
4999
5873
|
fill="${this.renderTheme.textMuted}">\u2715</text>
|
|
5000
5874
|
|
|
5001
5875
|
<!-- Content placeholder -->
|
|
5002
5876
|
<text x="${modalX + pos.width / 2}" y="${modalY + headerHeight + (pos.height - headerHeight) / 2}"
|
|
5003
|
-
font-family="
|
|
5877
|
+
font-family="Arial, Helvetica, sans-serif"
|
|
5004
5878
|
font-size="13"
|
|
5005
5879
|
fill="${this.renderTheme.textMuted}"
|
|
5006
5880
|
text-anchor="middle">Modal content</text>
|
|
@@ -5033,7 +5907,7 @@ var SVGRenderer = class {
|
|
|
5033
5907
|
if (title) {
|
|
5034
5908
|
svg += `
|
|
5035
5909
|
<text x="${pos.x + padding}" y="${pos.y + 26}"
|
|
5036
|
-
font-family="
|
|
5910
|
+
font-family="Arial, Helvetica, sans-serif"
|
|
5037
5911
|
font-size="13"
|
|
5038
5912
|
font-weight="600"
|
|
5039
5913
|
fill="${this.renderTheme.text}">${this.escapeXml(title)}</text>
|
|
@@ -5049,7 +5923,7 @@ var SVGRenderer = class {
|
|
|
5049
5923
|
stroke="${this.renderTheme.border}"
|
|
5050
5924
|
stroke-width="0.5"/>
|
|
5051
5925
|
<text x="${pos.x + padding}" y="${itemY + 24}"
|
|
5052
|
-
font-family="
|
|
5926
|
+
font-family="Arial, Helvetica, sans-serif"
|
|
5053
5927
|
font-size="13"
|
|
5054
5928
|
fill="${this.renderTheme.text}">${this.escapeXml(item)}</text>`;
|
|
5055
5929
|
}
|
|
@@ -5067,7 +5941,7 @@ var SVGRenderer = class {
|
|
|
5067
5941
|
stroke-width="1"
|
|
5068
5942
|
stroke-dasharray="4 4"/>
|
|
5069
5943
|
<text x="${pos.x + pos.width / 2}" y="${pos.y + pos.height / 2}"
|
|
5070
|
-
font-family="
|
|
5944
|
+
font-family="Arial, Helvetica, sans-serif"
|
|
5071
5945
|
font-size="12"
|
|
5072
5946
|
fill="${this.renderTheme.textMuted}"
|
|
5073
5947
|
text-anchor="middle">${node.componentType}</text>
|
|
@@ -5115,14 +5989,14 @@ var SVGRenderer = class {
|
|
|
5115
5989
|
|
|
5116
5990
|
<!-- Title -->
|
|
5117
5991
|
<text x="${innerX}" y="${titleY}"
|
|
5118
|
-
font-family="
|
|
5992
|
+
font-family="Arial, Helvetica, sans-serif"
|
|
5119
5993
|
font-size="${titleSize}"
|
|
5120
5994
|
font-weight="500"
|
|
5121
5995
|
fill="${this.renderTheme.textMuted}">${this.escapeXml(visibleTitle)}</text>
|
|
5122
5996
|
|
|
5123
5997
|
<!-- Value (Large) -->
|
|
5124
5998
|
<text x="${innerX}" y="${valueY}"
|
|
5125
|
-
font-family="
|
|
5999
|
+
font-family="Arial, Helvetica, sans-serif"
|
|
5126
6000
|
font-size="${valueSize}"
|
|
5127
6001
|
font-weight="700"
|
|
5128
6002
|
fill="${accentColor}">${this.escapeXml(value)}</text>`;
|
|
@@ -5145,7 +6019,7 @@ var SVGRenderer = class {
|
|
|
5145
6019
|
svg += `
|
|
5146
6020
|
<!-- Caption -->
|
|
5147
6021
|
<text x="${innerX}" y="${captionY}"
|
|
5148
|
-
font-family="
|
|
6022
|
+
font-family="Arial, Helvetica, sans-serif"
|
|
5149
6023
|
font-size="${captionSize}"
|
|
5150
6024
|
fill="${this.renderTheme.textMuted}">${this.escapeXml(visibleCaption)}</text>`;
|
|
5151
6025
|
}
|
|
@@ -5156,7 +6030,9 @@ var SVGRenderer = class {
|
|
|
5156
6030
|
renderImage(node, pos) {
|
|
5157
6031
|
const placeholder = String(node.props.placeholder || "landscape").toLowerCase();
|
|
5158
6032
|
const placeholderIcon = String(node.props.icon || "").trim();
|
|
6033
|
+
const variant = String(node.props.variant || "").trim();
|
|
5159
6034
|
const placeholderIconSvg = placeholder === "icon" && placeholderIcon ? getIcon(placeholderIcon) : null;
|
|
6035
|
+
const imageBg = this.options.theme === "dark" ? "#2A2A2A" : "#E8E8E8";
|
|
5160
6036
|
const aspectRatios = {
|
|
5161
6037
|
landscape: 16 / 9,
|
|
5162
6038
|
portrait: 2 / 3,
|
|
@@ -5174,30 +6050,29 @@ var SVGRenderer = class {
|
|
|
5174
6050
|
}
|
|
5175
6051
|
const offsetX = pos.x + (pos.width - iconWidth) / 2;
|
|
5176
6052
|
const offsetY = pos.y + (pos.height - iconHeight) / 2;
|
|
5177
|
-
let svg = `<g${this.getDataNodeId(node)}>
|
|
5178
|
-
<!-- Image Background -->
|
|
5179
|
-
<rect x="${pos.x}" y="${pos.y}" width="${pos.width}" height="${pos.height}" fill="#E8E8E8"/>`;
|
|
5180
6053
|
if (placeholder === "icon" && placeholderIconSvg) {
|
|
5181
|
-
const
|
|
5182
|
-
const
|
|
5183
|
-
const
|
|
5184
|
-
const
|
|
5185
|
-
const
|
|
5186
|
-
const
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
rx="${Math.max(4, badgeSize * 0.2)}"
|
|
5192
|
-
fill="rgba(255, 255, 255, 0.6)"
|
|
5193
|
-
stroke="#888"
|
|
5194
|
-
stroke-width="1"/>
|
|
6054
|
+
const semanticBase = variant ? this.getSemanticVariantColor(variant) : void 0;
|
|
6055
|
+
const hasVariant = variant.length > 0 && (semanticBase !== void 0 || this.colorResolver.hasColor(variant));
|
|
6056
|
+
const variantColor = hasVariant ? this.resolveVariantColor(variant, this.renderTheme.primary) : null;
|
|
6057
|
+
const bgColor = hasVariant ? this.hexToRgba(variantColor, 0.12) : imageBg;
|
|
6058
|
+
const iconColor = hasVariant ? variantColor : this.options.theme === "dark" ? "#888888" : "#666666";
|
|
6059
|
+
const iconSize = Math.max(16, Math.min(pos.width, pos.height) * 0.6);
|
|
6060
|
+
const iconOffsetX = pos.x + (pos.width - iconSize) / 2;
|
|
6061
|
+
const iconOffsetY = pos.y + (pos.height - iconSize) / 2;
|
|
6062
|
+
return `<g${this.getDataNodeId(node)}>
|
|
6063
|
+
<rect x="${pos.x}" y="${pos.y}" width="${pos.width}" height="${pos.height}" fill="${bgColor}" rx="4"/>
|
|
5195
6064
|
<g transform="translate(${iconOffsetX}, ${iconOffsetY})">
|
|
5196
|
-
<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="
|
|
6065
|
+
<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
5197
6066
|
${this.extractSvgContent(placeholderIconSvg)}
|
|
5198
6067
|
</svg>
|
|
5199
|
-
</g
|
|
5200
|
-
}
|
|
6068
|
+
</g>
|
|
6069
|
+
<rect x="${pos.x}" y="${pos.y}" width="${pos.width}" height="${pos.height}" fill="none" stroke="${this.renderTheme.border}" stroke-width="1" rx="4"/>
|
|
6070
|
+
</g>`;
|
|
6071
|
+
}
|
|
6072
|
+
let svg = `<g${this.getDataNodeId(node)}>
|
|
6073
|
+
<!-- Image Background -->
|
|
6074
|
+
<rect x="${pos.x}" y="${pos.y}" width="${pos.width}" height="${pos.height}" fill="${imageBg}"/>`;
|
|
6075
|
+
if (["landscape", "portrait", "square"].includes(placeholder)) {
|
|
5201
6076
|
const cameraCx = offsetX + iconWidth / 2;
|
|
5202
6077
|
const cameraCy = offsetY + iconHeight / 2;
|
|
5203
6078
|
const scale = Math.min(iconWidth, iconHeight) / 24;
|
|
@@ -5274,7 +6149,7 @@ var SVGRenderer = class {
|
|
|
5274
6149
|
const fontWeight = isLast ? "500" : "400";
|
|
5275
6150
|
svg += `
|
|
5276
6151
|
<text x="${currentX}" y="${pos.y + pos.height / 2 + 4}"
|
|
5277
|
-
font-family="
|
|
6152
|
+
font-family="Arial, Helvetica, sans-serif"
|
|
5278
6153
|
font-size="${fontSize}"
|
|
5279
6154
|
font-weight="${fontWeight}"
|
|
5280
6155
|
fill="${textColor}">${this.escapeXml(item)}</text>`;
|
|
@@ -5283,7 +6158,7 @@ var SVGRenderer = class {
|
|
|
5283
6158
|
if (!isLast) {
|
|
5284
6159
|
svg += `
|
|
5285
6160
|
<text x="${currentX + 4}" y="${pos.y + pos.height / 2 + 4}"
|
|
5286
|
-
font-family="
|
|
6161
|
+
font-family="Arial, Helvetica, sans-serif"
|
|
5287
6162
|
font-size="${fontSize}"
|
|
5288
6163
|
fill="${this.renderTheme.textMuted}">${this.escapeXml(separator)}</text>`;
|
|
5289
6164
|
currentX += separatorWidth;
|
|
@@ -5301,18 +6176,22 @@ var SVGRenderer = class {
|
|
|
5301
6176
|
const fontSize = 14;
|
|
5302
6177
|
const activeIndex = Number(node.props.active || 0);
|
|
5303
6178
|
const accentColor = this.resolveAccentColor();
|
|
6179
|
+
const variantProp = String(node.props.variant || "").trim();
|
|
6180
|
+
const semanticVariant = variantProp ? this.getSemanticVariantColor(variantProp) : void 0;
|
|
6181
|
+
const hasVariant = variantProp.length > 0 && (semanticVariant !== void 0 || this.colorResolver.hasColor(variantProp));
|
|
6182
|
+
const activeColor = hasVariant ? this.resolveVariantColor(variantProp, accentColor) : accentColor;
|
|
5304
6183
|
let svg = `<g${this.getDataNodeId(node)}>`;
|
|
5305
6184
|
items.forEach((item, index) => {
|
|
5306
6185
|
const itemY = pos.y + index * itemHeight;
|
|
5307
6186
|
const isActive = index === activeIndex;
|
|
5308
|
-
const bgColor = isActive ? this.hexToRgba(
|
|
5309
|
-
const textColor = isActive ? this.hexToRgba(
|
|
6187
|
+
const bgColor = isActive ? this.hexToRgba(activeColor, 0.15) : "transparent";
|
|
6188
|
+
const textColor = isActive ? this.hexToRgba(activeColor, 0.9) : this.hexToRgba(this.resolveTextColor(), 0.75);
|
|
5310
6189
|
const fontWeight = isActive ? "500" : "400";
|
|
5311
6190
|
if (isActive) {
|
|
5312
6191
|
svg += `
|
|
5313
|
-
<rect x="${pos.x}" y="${itemY}"
|
|
5314
|
-
width="${pos.width}" height="${itemHeight}"
|
|
5315
|
-
rx="6"
|
|
6192
|
+
<rect x="${pos.x}" y="${itemY}"
|
|
6193
|
+
width="${pos.width}" height="${itemHeight}"
|
|
6194
|
+
rx="6"
|
|
5316
6195
|
fill="${bgColor}"/>`;
|
|
5317
6196
|
}
|
|
5318
6197
|
let currentX = pos.x + 12;
|
|
@@ -5321,9 +6200,10 @@ var SVGRenderer = class {
|
|
|
5321
6200
|
if (iconSvg) {
|
|
5322
6201
|
const iconSize = 16;
|
|
5323
6202
|
const iconY = itemY + (itemHeight - iconSize) / 2;
|
|
6203
|
+
const iconColor = isActive ? this.hexToRgba(activeColor, 0.9) : this.hexToRgba(this.resolveMutedColor(), 0.9);
|
|
5324
6204
|
svg += `
|
|
5325
6205
|
<g transform="translate(${currentX}, ${iconY})">
|
|
5326
|
-
<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="
|
|
6206
|
+
<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
5327
6207
|
${this.extractSvgContent(iconSvg)}
|
|
5328
6208
|
</svg>
|
|
5329
6209
|
</g>`;
|
|
@@ -5331,28 +6211,29 @@ var SVGRenderer = class {
|
|
|
5331
6211
|
}
|
|
5332
6212
|
}
|
|
5333
6213
|
svg += `
|
|
5334
|
-
<text x="${currentX}" y="${itemY + itemHeight / 2 + 5}"
|
|
5335
|
-
font-family="
|
|
5336
|
-
font-size="${fontSize}"
|
|
5337
|
-
font-weight="${fontWeight}"
|
|
6214
|
+
<text x="${currentX}" y="${itemY + itemHeight / 2 + 5}"
|
|
6215
|
+
font-family="Arial, Helvetica, sans-serif"
|
|
6216
|
+
font-size="${fontSize}"
|
|
6217
|
+
font-weight="${fontWeight}"
|
|
5338
6218
|
fill="${textColor}">${this.escapeXml(item)}</text>`;
|
|
5339
6219
|
});
|
|
5340
6220
|
svg += "\n </g>";
|
|
5341
6221
|
return svg;
|
|
5342
6222
|
}
|
|
5343
6223
|
renderIcon(node, pos) {
|
|
5344
|
-
const iconType = String(node.props.
|
|
6224
|
+
const iconType = String(node.props.icon || "help-circle");
|
|
5345
6225
|
const size = String(node.props.size || "md");
|
|
6226
|
+
const variant = String(node.props.variant || "default");
|
|
5346
6227
|
const iconSvg = getIcon(iconType);
|
|
6228
|
+
const iconColor = variant === "default" ? this.hexToRgba(this.resolveTextColor(), 0.75) : this.resolveVariantColor(variant, this.resolveTextColor());
|
|
5347
6229
|
if (!iconSvg) {
|
|
5348
6230
|
return `<g${this.getDataNodeId(node)}>
|
|
5349
6231
|
<!-- Icon not found: ${iconType} -->
|
|
5350
|
-
<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="
|
|
5351
|
-
<text x="${pos.x + pos.width / 2}" y="${pos.y + pos.height / 2 + 4}" font-family="
|
|
6232
|
+
<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="${this.hexToRgba(this.resolveMutedColor(), 0.4)}" stroke-width="1"/>
|
|
6233
|
+
<text x="${pos.x + pos.width / 2}" y="${pos.y + pos.height / 2 + 4}" font-family="Arial, Helvetica, sans-serif" font-size="12" fill="${this.hexToRgba(this.resolveMutedColor(), 0.7)}" text-anchor="middle">?</text>
|
|
5352
6234
|
</g>`;
|
|
5353
6235
|
}
|
|
5354
6236
|
const iconSize = this.getIconSize(size);
|
|
5355
|
-
const iconColor = "rgba(30, 41, 59, 0.75)";
|
|
5356
6237
|
const offsetX = pos.x + (pos.width - iconSize) / 2;
|
|
5357
6238
|
const offsetY = pos.y + (pos.height - iconSize) / 2;
|
|
5358
6239
|
const wrappedSvg = `<g${this.getDataNodeId(node)} transform="translate(${offsetX}, ${offsetY})">
|
|
@@ -5367,23 +6248,32 @@ var SVGRenderer = class {
|
|
|
5367
6248
|
const variant = String(node.props.variant || "default");
|
|
5368
6249
|
const size = String(node.props.size || "md");
|
|
5369
6250
|
const disabled = String(node.props.disabled || "false") === "true";
|
|
6251
|
+
const density = this.ir.project.style.density || "normal";
|
|
6252
|
+
const labelOffset = this.parseBooleanProp(node.props.labelSpace, false) ? 18 : 0;
|
|
6253
|
+
const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
|
|
5370
6254
|
const semanticBase = this.getSemanticVariantColor(variant);
|
|
5371
6255
|
const hasExplicitVariantColor = semanticBase !== void 0 || this.colorResolver.hasColor(variant);
|
|
5372
6256
|
const resolvedBase = this.resolveVariantColor(variant, this.renderTheme.primary);
|
|
5373
|
-
const
|
|
5374
|
-
const
|
|
5375
|
-
const
|
|
6257
|
+
const isDarkMode = this.options.theme === "dark";
|
|
6258
|
+
const bgColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.85) : isDarkMode ? "rgba(48, 48, 55, 0.9)" : "rgba(226, 232, 240, 0.9)";
|
|
6259
|
+
const iconColor = hasExplicitVariantColor ? "#FFFFFF" : this.hexToRgba(this.resolveTextColor(), 0.75);
|
|
6260
|
+
const borderColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.7) : isDarkMode ? "rgba(75, 75, 88, 0.8)" : "rgba(100, 116, 139, 0.4)";
|
|
5376
6261
|
const opacity = disabled ? "0.5" : "1";
|
|
5377
6262
|
const iconSvg = getIcon(iconName);
|
|
5378
|
-
const buttonSize =
|
|
6263
|
+
const buttonSize = Math.max(
|
|
6264
|
+
16,
|
|
6265
|
+
Math.min(resolveActionControlHeight(size, density), pos.height - labelOffset)
|
|
6266
|
+
);
|
|
6267
|
+
const buttonWidth = buttonSize + extraPadding * 2;
|
|
5379
6268
|
const radius = 6;
|
|
6269
|
+
const buttonY = pos.y + labelOffset;
|
|
5380
6270
|
let svg = `<g${this.getDataNodeId(node)} opacity="${opacity}">
|
|
5381
6271
|
<!-- IconButton background -->
|
|
5382
|
-
<rect x="${pos.x}" y="${
|
|
6272
|
+
<rect x="${pos.x}" y="${buttonY}" width="${buttonWidth}" height="${buttonSize}" rx="${radius}" fill="${bgColor}" stroke="${borderColor}" stroke-width="1"/>`;
|
|
5383
6273
|
if (iconSvg) {
|
|
5384
6274
|
const iconSize = buttonSize * 0.6;
|
|
5385
|
-
const offsetX = pos.x + (
|
|
5386
|
-
const offsetY =
|
|
6275
|
+
const offsetX = pos.x + (buttonWidth - iconSize) / 2;
|
|
6276
|
+
const offsetY = buttonY + (buttonSize - iconSize) / 2;
|
|
5387
6277
|
svg += `
|
|
5388
6278
|
<!-- Icon -->
|
|
5389
6279
|
<g transform="translate(${offsetX}, ${offsetY})">
|
|
@@ -5416,15 +6306,46 @@ var SVGRenderer = class {
|
|
|
5416
6306
|
resolveChartColor() {
|
|
5417
6307
|
return this.colorResolver.resolveColor("chart", this.renderTheme.primary);
|
|
5418
6308
|
}
|
|
6309
|
+
resolveTextColor() {
|
|
6310
|
+
const fallback = this.options.theme === "dark" ? "#FFFFFF" : "#000000";
|
|
6311
|
+
return this.colorResolver.resolveColor("text", fallback);
|
|
6312
|
+
}
|
|
6313
|
+
resolveMutedColor() {
|
|
6314
|
+
const fallback = this.options.theme === "dark" ? "#94A3B8" : "#64748B";
|
|
6315
|
+
return this.colorResolver.resolveColor("muted", fallback);
|
|
6316
|
+
}
|
|
5419
6317
|
getSemanticVariantColor(variant) {
|
|
5420
|
-
const
|
|
6318
|
+
const isDark = this.options.theme === "dark";
|
|
6319
|
+
const semantic = isDark ? {
|
|
6320
|
+
// Muted mid-range — readable on #111111 without being neon
|
|
6321
|
+
primary: this.renderTheme.primary,
|
|
6322
|
+
// already theme-aware (#60A5FA)
|
|
6323
|
+
secondary: "#7E8EA2",
|
|
6324
|
+
// desaturated slate
|
|
6325
|
+
success: "#22A06B",
|
|
6326
|
+
// muted emerald
|
|
6327
|
+
warning: "#B38010",
|
|
6328
|
+
// deep amber
|
|
6329
|
+
danger: "#CC4444",
|
|
6330
|
+
// muted red
|
|
6331
|
+
error: "#CC4444",
|
|
6332
|
+
info: "#2485AF"
|
|
6333
|
+
// muted sky
|
|
6334
|
+
} : {
|
|
6335
|
+
// Tailwind 500-level — works on white/light backgrounds
|
|
5421
6336
|
primary: this.renderTheme.primary,
|
|
6337
|
+
// #3B82F6
|
|
5422
6338
|
secondary: "#64748B",
|
|
6339
|
+
// Slate 500
|
|
5423
6340
|
success: "#10B981",
|
|
6341
|
+
// Emerald 500
|
|
5424
6342
|
warning: "#F59E0B",
|
|
6343
|
+
// Amber 500
|
|
5425
6344
|
danger: "#EF4444",
|
|
6345
|
+
// Red 500
|
|
5426
6346
|
error: "#EF4444",
|
|
5427
6347
|
info: "#0EA5E9"
|
|
6348
|
+
// Sky 500
|
|
5428
6349
|
};
|
|
5429
6350
|
return semantic[variant];
|
|
5430
6351
|
}
|
|
@@ -5742,21 +6663,28 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
|
|
|
5742
6663
|
renderButton(node, pos) {
|
|
5743
6664
|
const text = String(node.props.text || "Button");
|
|
5744
6665
|
const variant = String(node.props.variant || "default");
|
|
6666
|
+
const size = String(node.props.size || "md");
|
|
6667
|
+
const density = this.ir.project.style.density || "normal";
|
|
6668
|
+
const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
|
|
6669
|
+
const labelOffset = this.parseBooleanProp(node.props.labelSpace, false) ? 18 : 0;
|
|
5745
6670
|
const fullWidth = this.shouldButtonFillAvailableWidth(node);
|
|
5746
6671
|
const radius = this.tokens.button.radius;
|
|
5747
6672
|
const fontSize = this.tokens.button.fontSize;
|
|
5748
6673
|
const paddingX = this.tokens.button.paddingX;
|
|
5749
|
-
const
|
|
6674
|
+
const buttonHeight = Math.max(
|
|
6675
|
+
16,
|
|
6676
|
+
Math.min(resolveActionControlHeight(size, density), pos.height - labelOffset)
|
|
6677
|
+
);
|
|
6678
|
+
const buttonY = pos.y + labelOffset;
|
|
5750
6679
|
const textWidth = text.length * fontSize * 0.6;
|
|
5751
|
-
const buttonWidth = fullWidth ? Math.max(1, pos.width) : this.clampControlWidth(Math.max(textWidth + paddingX * 2, 60), pos.width);
|
|
5752
|
-
const buttonHeight = fontSize + paddingY * 2;
|
|
6680
|
+
const buttonWidth = fullWidth ? Math.max(1, pos.width) : this.clampControlWidth(Math.max(textWidth + (paddingX + extraPadding) * 2, 60), pos.width);
|
|
5753
6681
|
const semanticBase = this.getSemanticVariantColor(variant);
|
|
5754
6682
|
const hasExplicitVariantColor = semanticBase !== void 0 || this.colorResolver.hasColor(variant);
|
|
5755
6683
|
const resolvedBase = this.resolveVariantColor(variant, this.renderTheme.primary);
|
|
5756
6684
|
const bgColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.85) : "rgba(226, 232, 240, 0.9)";
|
|
5757
6685
|
const borderColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.7) : "rgba(100, 116, 139, 0.4)";
|
|
5758
6686
|
return `<g${this.getDataNodeId(node)}>
|
|
5759
|
-
<rect x="${pos.x}" y="${
|
|
6687
|
+
<rect x="${pos.x}" y="${buttonY}"
|
|
5760
6688
|
width="${buttonWidth}" height="${buttonHeight}"
|
|
5761
6689
|
rx="${radius}"
|
|
5762
6690
|
fill="${bgColor}"
|
|
@@ -5770,13 +6698,14 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
|
|
|
5770
6698
|
renderLink(node, pos) {
|
|
5771
6699
|
const text = String(node.props.text || "Link");
|
|
5772
6700
|
const variant = String(node.props.variant || "primary");
|
|
6701
|
+
const size = String(node.props.size || "md");
|
|
6702
|
+
const density = this.ir.project.style.density || "normal";
|
|
5773
6703
|
const fontSize = this.tokens.button.fontSize;
|
|
5774
6704
|
const paddingX = this.tokens.button.paddingX;
|
|
5775
|
-
const paddingY = this.tokens.button.paddingY;
|
|
5776
6705
|
const linkColor = this.resolveVariantColor(variant, this.renderTheme.primary);
|
|
5777
6706
|
const textWidth = this.estimateTextWidth(text, fontSize);
|
|
5778
6707
|
const linkWidth = this.clampControlWidth(Math.max(textWidth + paddingX * 2, 60), pos.width);
|
|
5779
|
-
const linkHeight =
|
|
6708
|
+
const linkHeight = Math.max(16, Math.min(resolveActionControlHeight(size, density), pos.height));
|
|
5780
6709
|
const blockHeight = Math.max(8, Math.round(fontSize * 0.75));
|
|
5781
6710
|
const blockWidth = Math.max(28, Math.min(textWidth, linkWidth - paddingX * 2));
|
|
5782
6711
|
const blockX = pos.x + (linkWidth - blockWidth) / 2;
|
|
@@ -5793,17 +6722,70 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
|
|
|
5793
6722
|
stroke-width="1"/>
|
|
5794
6723
|
</g>`;
|
|
5795
6724
|
}
|
|
6725
|
+
/**
|
|
6726
|
+
* Render breadcrumbs as skeleton blocks: <rect> / <rect> / <rect accent>
|
|
6727
|
+
*/
|
|
6728
|
+
renderBreadcrumbs(node, pos) {
|
|
6729
|
+
const itemsStr = String(node.props.items || "Home");
|
|
6730
|
+
const items = itemsStr.split(",").map((s) => s.trim()).filter(Boolean);
|
|
6731
|
+
const separator = String(node.props.separator || "/");
|
|
6732
|
+
const blockColor = this.renderTheme.border;
|
|
6733
|
+
const charWidth = 6.2;
|
|
6734
|
+
const minBlockWidth = 28;
|
|
6735
|
+
const maxBlockWidth = Math.max(minBlockWidth, Math.floor(pos.width * 0.4));
|
|
6736
|
+
const blockHeight = 12;
|
|
6737
|
+
const blockY = pos.y + (pos.height - blockHeight) / 2;
|
|
6738
|
+
const blockRadius = 4;
|
|
6739
|
+
const blockPaddingX = 10;
|
|
6740
|
+
const itemSpacing = 8;
|
|
6741
|
+
const separatorWidth = 12;
|
|
6742
|
+
const contentRight = pos.x + pos.width;
|
|
6743
|
+
let currentX = pos.x;
|
|
6744
|
+
let svg = `<g${this.getDataNodeId(node)}>`;
|
|
6745
|
+
items.forEach((item, index) => {
|
|
6746
|
+
if (currentX >= contentRight) return;
|
|
6747
|
+
const isLast = index === items.length - 1;
|
|
6748
|
+
const estimatedTextWidth = item.length * charWidth;
|
|
6749
|
+
let blockWidth = Math.max(
|
|
6750
|
+
minBlockWidth,
|
|
6751
|
+
Math.min(maxBlockWidth, Math.ceil(estimatedTextWidth + blockPaddingX * 2))
|
|
6752
|
+
);
|
|
6753
|
+
blockWidth = Math.min(blockWidth, Math.max(0, contentRight - currentX));
|
|
6754
|
+
if (blockWidth < minBlockWidth) return;
|
|
6755
|
+
const fillColor = blockColor;
|
|
6756
|
+
svg += `
|
|
6757
|
+
<rect x="${currentX}" y="${blockY}"
|
|
6758
|
+
width="${blockWidth}" height="${blockHeight}"
|
|
6759
|
+
rx="${blockRadius}"
|
|
6760
|
+
fill="${fillColor}"
|
|
6761
|
+
stroke="none"/>`;
|
|
6762
|
+
currentX += blockWidth + itemSpacing;
|
|
6763
|
+
if (!isLast && currentX + separatorWidth <= contentRight) {
|
|
6764
|
+
svg += `
|
|
6765
|
+
<text x="${currentX + 2}" y="${pos.y + pos.height / 2 + 4}"
|
|
6766
|
+
font-family="Arial, Helvetica, sans-serif"
|
|
6767
|
+
font-size="12"
|
|
6768
|
+
fill="${blockColor}">${this.escapeXml(separator)}</text>`;
|
|
6769
|
+
currentX += separatorWidth;
|
|
6770
|
+
}
|
|
6771
|
+
});
|
|
6772
|
+
svg += "\n </g>";
|
|
6773
|
+
return svg;
|
|
6774
|
+
}
|
|
5796
6775
|
/**
|
|
5797
6776
|
* Render heading as gray block
|
|
5798
6777
|
*/
|
|
5799
6778
|
renderHeading(node, pos) {
|
|
5800
6779
|
const headingTypography = this.getHeadingTypography(node);
|
|
6780
|
+
const variant = String(node.props.variant || "default");
|
|
6781
|
+
const blockColor = variant === "default" ? this.renderTheme.border : this.hexToRgba(this.resolveVariantColor(variant, this.resolveTextColor()), 0.35);
|
|
5801
6782
|
return this.renderTextBlock(
|
|
5802
6783
|
node,
|
|
5803
6784
|
pos,
|
|
5804
6785
|
String(node.props.text || "Heading"),
|
|
5805
6786
|
headingTypography.fontSize,
|
|
5806
|
-
headingTypography.lineHeight
|
|
6787
|
+
headingTypography.lineHeight,
|
|
6788
|
+
blockColor
|
|
5807
6789
|
);
|
|
5808
6790
|
}
|
|
5809
6791
|
/**
|
|
@@ -5813,7 +6795,7 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
|
|
|
5813
6795
|
return this.renderTextBlock(
|
|
5814
6796
|
node,
|
|
5815
6797
|
pos,
|
|
5816
|
-
String(node.props.
|
|
6798
|
+
String(node.props.text || "Text content"),
|
|
5817
6799
|
this.tokens.text.fontSize,
|
|
5818
6800
|
this.tokens.text.lineHeight
|
|
5819
6801
|
);
|
|
@@ -5824,6 +6806,19 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
|
|
|
5824
6806
|
renderLabel(node, pos) {
|
|
5825
6807
|
return this.renderTextBlock(node, pos, String(node.props.text || "Label"), 12, 1.2);
|
|
5826
6808
|
}
|
|
6809
|
+
/**
|
|
6810
|
+
* Render image as a plain skeleton rectangle — no icon, no placeholder label,
|
|
6811
|
+
* just a filled block with the correct dimensions (aspect-ratio is preserved
|
|
6812
|
+
* by the layout engine, so pos already has the right size).
|
|
6813
|
+
*/
|
|
6814
|
+
renderImage(node, pos) {
|
|
6815
|
+
return `<g${this.getDataNodeId(node)}>
|
|
6816
|
+
<rect x="${pos.x}" y="${pos.y}"
|
|
6817
|
+
width="${pos.width}" height="${pos.height}"
|
|
6818
|
+
rx="4"
|
|
6819
|
+
fill="${this.renderTheme.border}"/>
|
|
6820
|
+
</g>`;
|
|
6821
|
+
}
|
|
5827
6822
|
/**
|
|
5828
6823
|
* Render badge as shape only (no text)
|
|
5829
6824
|
*/
|
|
@@ -6054,18 +7049,42 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
|
|
|
6054
7049
|
renderTable(node, pos) {
|
|
6055
7050
|
const title = String(node.props.title || "");
|
|
6056
7051
|
const columnsStr = String(node.props.columns || "Col1,Col2,Col3");
|
|
6057
|
-
const columns = columnsStr.split(",").map((c) => c.trim());
|
|
7052
|
+
const columns = columnsStr.split(",").map((c) => c.trim()).filter(Boolean);
|
|
6058
7053
|
const rowCount = Number(node.props.rows || node.props.rowsMock || 5);
|
|
7054
|
+
const actions = String(node.props.actions || "").split(",").map((value) => value.trim()).filter(Boolean);
|
|
7055
|
+
const hasActions = actions.length > 0;
|
|
7056
|
+
const pagination = this.parseBooleanProp(node.props.pagination, false);
|
|
7057
|
+
const parsedPageCount = Number(node.props.pages || 5);
|
|
7058
|
+
const pageCount = Number.isFinite(parsedPageCount) && parsedPageCount > 0 ? Math.floor(parsedPageCount) : 5;
|
|
7059
|
+
const paginationAlign = String(node.props.paginationAlign || "right");
|
|
7060
|
+
const hasCaption = String(node.props.caption || "").trim().length > 0;
|
|
7061
|
+
const showOuterBorder = this.parseBooleanProp(node.props.border, false);
|
|
7062
|
+
const showOuterBackground = this.parseBooleanProp(
|
|
7063
|
+
node.props.background ?? node.props.backround,
|
|
7064
|
+
false
|
|
7065
|
+
);
|
|
7066
|
+
const showInnerBorder = this.parseBooleanProp(node.props.innerBorder, true);
|
|
7067
|
+
const rawCaptionAlign = String(node.props.captionAlign || "");
|
|
7068
|
+
const captionAlign = rawCaptionAlign === "left" || rawCaptionAlign === "center" || rawCaptionAlign === "right" ? rawCaptionAlign : paginationAlign === "left" ? "right" : "left";
|
|
7069
|
+
const sameFooterAlign = hasCaption && pagination && captionAlign === paginationAlign;
|
|
7070
|
+
const safeColumns = columns.length > 0 ? columns : ["Column"];
|
|
6059
7071
|
const headerHeight = 44;
|
|
6060
7072
|
const rowHeight = 36;
|
|
6061
|
-
const
|
|
6062
|
-
|
|
7073
|
+
const actionColumnWidth = hasActions ? Math.max(96, Math.min(180, actions.length * 26 + 28)) : 0;
|
|
7074
|
+
const dataWidth = Math.max(20, pos.width - actionColumnWidth);
|
|
7075
|
+
const colWidth = dataWidth / safeColumns.length;
|
|
7076
|
+
let svg = `<g${this.getDataNodeId(node)}>`;
|
|
7077
|
+
if (showOuterBorder || showOuterBackground) {
|
|
7078
|
+
const outerFill = showOuterBackground ? this.renderTheme.cardBg : "none";
|
|
7079
|
+
const outerStroke = showOuterBorder ? this.renderTheme.border : "none";
|
|
7080
|
+
svg += `
|
|
6063
7081
|
<rect x="${pos.x}" y="${pos.y}"
|
|
6064
7082
|
width="${pos.width}" height="${pos.height}"
|
|
6065
7083
|
rx="8"
|
|
6066
|
-
fill="${
|
|
6067
|
-
stroke="${
|
|
7084
|
+
fill="${outerFill}"
|
|
7085
|
+
stroke="${outerStroke}"
|
|
6068
7086
|
stroke-width="1"/>`;
|
|
7087
|
+
}
|
|
6069
7088
|
if (title) {
|
|
6070
7089
|
svg += `<rect x="${pos.x + 16}" y="${pos.y + 12}"
|
|
6071
7090
|
width="100" height="12"
|
|
@@ -6073,19 +7092,28 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
|
|
|
6073
7092
|
fill="${this.renderTheme.border}"/>`;
|
|
6074
7093
|
}
|
|
6075
7094
|
const headerY = pos.y + (title ? 32 : 0);
|
|
6076
|
-
|
|
7095
|
+
if (showInnerBorder) {
|
|
7096
|
+
svg += `<line x1="${pos.x}" y1="${headerY + headerHeight}" x2="${pos.x + pos.width}" y2="${headerY + headerHeight}"
|
|
6077
7097
|
stroke="${this.renderTheme.border}" stroke-width="1"/>`;
|
|
6078
|
-
|
|
7098
|
+
}
|
|
7099
|
+
safeColumns.forEach((_, i) => {
|
|
6079
7100
|
svg += `<rect x="${pos.x + i * colWidth + 12}" y="${headerY + 16}"
|
|
6080
7101
|
width="50" height="10"
|
|
6081
7102
|
rx="4"
|
|
6082
7103
|
fill="${this.renderTheme.border}"/>`;
|
|
6083
7104
|
});
|
|
7105
|
+
if (hasActions && showInnerBorder) {
|
|
7106
|
+
const dividerX = pos.x + dataWidth;
|
|
7107
|
+
svg += `<line x1="${dividerX}" y1="${headerY + headerHeight}" x2="${dividerX}" y2="${headerY + headerHeight + rowCount * rowHeight}"
|
|
7108
|
+
stroke="${this.renderTheme.border}" stroke-width="1"/>`;
|
|
7109
|
+
}
|
|
6084
7110
|
for (let rowIdx = 0; rowIdx < rowCount; rowIdx++) {
|
|
6085
7111
|
const rowY = headerY + headerHeight + rowIdx * rowHeight;
|
|
6086
|
-
|
|
7112
|
+
if (showInnerBorder) {
|
|
7113
|
+
svg += `<line x1="${pos.x}" y1="${rowY + rowHeight}" x2="${pos.x + pos.width}" y2="${rowY + rowHeight}"
|
|
6087
7114
|
stroke="${this.renderTheme.border}" stroke-width="0.5"/>`;
|
|
6088
|
-
|
|
7115
|
+
}
|
|
7116
|
+
safeColumns.forEach((_, colIdx) => {
|
|
6089
7117
|
const variance = (rowIdx * 17 + colIdx * 11) % 5 * 10;
|
|
6090
7118
|
const blockWidth = Math.min(colWidth - 24, 60 + variance);
|
|
6091
7119
|
svg += `<rect x="${pos.x + colIdx * colWidth + 12}" y="${rowY + 12}"
|
|
@@ -6093,6 +7121,45 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
|
|
|
6093
7121
|
rx="4"
|
|
6094
7122
|
fill="${this.renderTheme.border}"/>`;
|
|
6095
7123
|
});
|
|
7124
|
+
if (hasActions) {
|
|
7125
|
+
const iconSize = 14;
|
|
7126
|
+
const iconGap = 8;
|
|
7127
|
+
const actionsWidth = actions.length * iconSize + Math.max(0, actions.length - 1) * iconGap;
|
|
7128
|
+
let currentX = pos.x + pos.width - 12 - actionsWidth;
|
|
7129
|
+
const iconY = rowY + (rowHeight - iconSize) / 2;
|
|
7130
|
+
actions.forEach(() => {
|
|
7131
|
+
svg += `<rect x="${currentX}" y="${iconY}" width="${iconSize}" height="${iconSize}" rx="3" fill="${this.renderTheme.border}"/>`;
|
|
7132
|
+
currentX += iconSize + iconGap;
|
|
7133
|
+
});
|
|
7134
|
+
}
|
|
7135
|
+
}
|
|
7136
|
+
const footerTop = headerY + headerHeight + rowCount * rowHeight + 16;
|
|
7137
|
+
if (hasCaption) {
|
|
7138
|
+
const captionY = sameFooterAlign ? footerTop : footerTop + (pagination ? 10 : 0);
|
|
7139
|
+
const captionWidth = Math.min(220, Math.max(90, pos.width * 0.34));
|
|
7140
|
+
let captionX = pos.x + 16;
|
|
7141
|
+
if (captionAlign === "center") {
|
|
7142
|
+
captionX = pos.x + (pos.width - captionWidth) / 2;
|
|
7143
|
+
} else if (captionAlign === "right") {
|
|
7144
|
+
captionX = pos.x + pos.width - 16 - captionWidth;
|
|
7145
|
+
}
|
|
7146
|
+
svg += `<rect x="${captionX}" y="${captionY}" width="${captionWidth}" height="10" rx="4" fill="${this.renderTheme.border}"/>`;
|
|
7147
|
+
}
|
|
7148
|
+
if (pagination) {
|
|
7149
|
+
const buttonWidth = 28;
|
|
7150
|
+
const buttonHeight = 24;
|
|
7151
|
+
const buttonGap = 8;
|
|
7152
|
+
const totalWidth = (pageCount + 2) * buttonWidth + (pageCount + 1) * buttonGap;
|
|
7153
|
+
const paginationY = sameFooterAlign ? footerTop + 18 : footerTop;
|
|
7154
|
+
let startX = pos.x + pos.width - totalWidth - 16;
|
|
7155
|
+
if (paginationAlign === "left") {
|
|
7156
|
+
startX = pos.x + 16;
|
|
7157
|
+
} else if (paginationAlign === "center") {
|
|
7158
|
+
startX = pos.x + (pos.width - totalWidth) / 2;
|
|
7159
|
+
}
|
|
7160
|
+
for (let i = 0; i < pageCount + 2; i++) {
|
|
7161
|
+
svg += `<rect x="${startX + i * (buttonWidth + buttonGap)}" y="${paginationY}" width="${buttonWidth}" height="${buttonHeight}" rx="4" fill="${this.renderTheme.border}"/>`;
|
|
7162
|
+
}
|
|
6096
7163
|
}
|
|
6097
7164
|
svg += "</g>";
|
|
6098
7165
|
return svg;
|
|
@@ -6105,21 +7172,39 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
|
|
|
6105
7172
|
const subtitle = String(node.props.subtitle || "");
|
|
6106
7173
|
const actions = String(node.props.actions || "");
|
|
6107
7174
|
const user = String(node.props.user || "");
|
|
7175
|
+
const variant = String(node.props.variant || "default");
|
|
7176
|
+
const accentBlock = variant === "default" ? this.renderTheme.border : this.hexToRgba(this.resolveVariantColor(variant, this.resolveAccentColor()), 0.35);
|
|
7177
|
+
const showBorder = this.parseBooleanProp(node.props.border, false);
|
|
7178
|
+
const showBackground = this.parseBooleanProp(node.props.background ?? node.props.backround, false);
|
|
7179
|
+
const radiusMap = {
|
|
7180
|
+
none: 0,
|
|
7181
|
+
sm: 4,
|
|
7182
|
+
md: this.tokens.card.radius,
|
|
7183
|
+
lg: 12,
|
|
7184
|
+
xl: 16
|
|
7185
|
+
};
|
|
7186
|
+
const topbarRadius = radiusMap[String(node.props.radius || "md")] ?? this.tokens.card.radius;
|
|
6108
7187
|
const topbar = this.calculateTopbarLayout(node, pos, title, subtitle, actions, user);
|
|
6109
7188
|
const titleWidth = Math.max(56, Math.min(topbar.titleMaxWidth * 0.55, topbar.titleMaxWidth));
|
|
6110
7189
|
const subtitleWidth = Math.max(48, Math.min(topbar.titleMaxWidth * 0.4, topbar.titleMaxWidth));
|
|
6111
|
-
let svg = `<g${this.getDataNodeId(node)}
|
|
7190
|
+
let svg = `<g${this.getDataNodeId(node)}>`;
|
|
7191
|
+
if (showBorder || showBackground) {
|
|
7192
|
+
const bg = showBackground ? this.renderTheme.cardBg : "none";
|
|
7193
|
+
const stroke = showBorder ? this.renderTheme.border : "none";
|
|
7194
|
+
svg += `
|
|
6112
7195
|
<rect x="${pos.x}" y="${pos.y}"
|
|
6113
7196
|
width="${pos.width}" height="${pos.height}"
|
|
6114
|
-
|
|
6115
|
-
|
|
6116
|
-
stroke
|
|
7197
|
+
rx="${topbarRadius}"
|
|
7198
|
+
fill="${bg}"
|
|
7199
|
+
stroke="${stroke}"
|
|
7200
|
+
stroke-width="1"/>`;
|
|
7201
|
+
}
|
|
6117
7202
|
if (topbar.leftIcon) {
|
|
6118
7203
|
svg += `
|
|
6119
7204
|
<rect x="${topbar.leftIcon.badgeX}" y="${topbar.leftIcon.badgeY}"
|
|
6120
7205
|
width="${topbar.leftIcon.badgeSize}" height="${topbar.leftIcon.badgeSize}"
|
|
6121
7206
|
rx="${topbar.leftIcon.badgeRadius}"
|
|
6122
|
-
fill="${
|
|
7207
|
+
fill="${accentBlock}"/>`;
|
|
6123
7208
|
}
|
|
6124
7209
|
svg += `
|
|
6125
7210
|
<rect x="${topbar.textX}" y="${topbar.titleY - 12}"
|
|
@@ -6138,7 +7223,7 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
|
|
|
6138
7223
|
<rect x="${action.x}" y="${action.y}"
|
|
6139
7224
|
width="${action.width}" height="${action.height}"
|
|
6140
7225
|
rx="6"
|
|
6141
|
-
fill="${
|
|
7226
|
+
fill="${accentBlock}"/>`;
|
|
6142
7227
|
});
|
|
6143
7228
|
if (topbar.userBadge) {
|
|
6144
7229
|
svg += `
|
|
@@ -6206,12 +7291,14 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
|
|
|
6206
7291
|
*/
|
|
6207
7292
|
renderIcon(node, pos) {
|
|
6208
7293
|
const size = String(node.props.size || "md");
|
|
7294
|
+
const variant = String(node.props.variant || "default");
|
|
6209
7295
|
const iconSize = this.getIconSize(size);
|
|
7296
|
+
const blockColor = variant === "default" ? this.renderTheme.border : this.hexToRgba(this.resolveVariantColor(variant, this.resolveTextColor()), 0.35);
|
|
6210
7297
|
return `<g${this.getDataNodeId(node)}>
|
|
6211
7298
|
<rect x="${pos.x}" y="${pos.y + (pos.height - iconSize) / 2}"
|
|
6212
7299
|
width="${iconSize}" height="${iconSize}"
|
|
6213
7300
|
rx="2"
|
|
6214
|
-
fill="${
|
|
7301
|
+
fill="${blockColor}"/>
|
|
6215
7302
|
</g>`;
|
|
6216
7303
|
}
|
|
6217
7304
|
/**
|
|
@@ -6220,15 +7307,23 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
|
|
|
6220
7307
|
renderIconButton(node, pos) {
|
|
6221
7308
|
const variant = String(node.props.variant || "default");
|
|
6222
7309
|
const size = String(node.props.size || "md");
|
|
7310
|
+
const density = this.ir.project.style.density || "normal";
|
|
7311
|
+
const labelOffset = this.parseBooleanProp(node.props.labelSpace, false) ? 18 : 0;
|
|
7312
|
+
const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
|
|
6223
7313
|
const semanticBase = this.getSemanticVariantColor(variant);
|
|
6224
7314
|
const hasExplicitVariantColor = semanticBase !== void 0 || this.colorResolver.hasColor(variant);
|
|
6225
7315
|
const resolvedBase = this.resolveVariantColor(variant, this.renderTheme.primary);
|
|
6226
7316
|
const bgColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.85) : "rgba(226, 232, 240, 0.9)";
|
|
6227
7317
|
const borderColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.7) : "rgba(100, 116, 139, 0.4)";
|
|
6228
|
-
const buttonSize =
|
|
7318
|
+
const buttonSize = Math.max(
|
|
7319
|
+
16,
|
|
7320
|
+
Math.min(resolveActionControlHeight(size, density), pos.height - labelOffset)
|
|
7321
|
+
);
|
|
7322
|
+
const buttonWidth = buttonSize + extraPadding * 2;
|
|
7323
|
+
const buttonY = pos.y + labelOffset;
|
|
6229
7324
|
return `<g${this.getDataNodeId(node)}>
|
|
6230
|
-
<rect x="${pos.x}" y="${
|
|
6231
|
-
width="${
|
|
7325
|
+
<rect x="${pos.x}" y="${buttonY}"
|
|
7326
|
+
width="${buttonWidth}" height="${buttonSize}"
|
|
6232
7327
|
rx="6"
|
|
6233
7328
|
fill="${bgColor}"
|
|
6234
7329
|
stroke="${borderColor}"
|
|
@@ -6308,10 +7403,10 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
|
|
|
6308
7403
|
/**
|
|
6309
7404
|
* Private helper: Render text as gray block
|
|
6310
7405
|
*/
|
|
6311
|
-
renderTextBlock(node, pos, text, fontSize, lineHeightMultiplier) {
|
|
7406
|
+
renderTextBlock(node, pos, text, fontSize, lineHeightMultiplier, color) {
|
|
6312
7407
|
const lineHeight = Math.ceil(fontSize * lineHeightMultiplier);
|
|
6313
7408
|
const blockHeight = Math.max(8, Math.round(fontSize * 0.75));
|
|
6314
|
-
const blockColor = this.renderTheme.border;
|
|
7409
|
+
const blockColor = color || this.renderTheme.border;
|
|
6315
7410
|
const lines = this.wrapTextToLines(text, pos.width, fontSize);
|
|
6316
7411
|
const contentHeight = lines.length * lineHeight;
|
|
6317
7412
|
const startY = pos.y + Math.max(0, (pos.height - contentHeight) / 2);
|
|
@@ -6393,38 +7488,78 @@ var SketchSVGRenderer = class extends SVGRenderer {
|
|
|
6393
7488
|
renderButton(node, pos) {
|
|
6394
7489
|
const text = String(node.props.text || "Button");
|
|
6395
7490
|
const variant = String(node.props.variant || "default");
|
|
7491
|
+
const size = String(node.props.size || "md");
|
|
7492
|
+
const density = this.ir.project.style.density || "normal";
|
|
7493
|
+
const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
|
|
7494
|
+
const labelOffset = this.parseBooleanProp(node.props.labelSpace, false) ? 18 : 0;
|
|
6396
7495
|
const fullWidth = this.shouldButtonFillAvailableWidth(node);
|
|
7496
|
+
const iconName = String(node.props.icon || "").trim();
|
|
7497
|
+
const iconAlign = String(node.props.iconAlign || "left").toLowerCase();
|
|
6397
7498
|
const radius = this.tokens.button.radius;
|
|
6398
7499
|
const fontSize = this.tokens.button.fontSize;
|
|
6399
7500
|
const fontWeight = this.tokens.button.fontWeight;
|
|
6400
7501
|
const paddingX = this.tokens.button.paddingX;
|
|
6401
|
-
const
|
|
7502
|
+
const buttonHeight = Math.max(
|
|
7503
|
+
16,
|
|
7504
|
+
Math.min(resolveControlHeight(size, density), pos.height - labelOffset)
|
|
7505
|
+
);
|
|
7506
|
+
const buttonY = pos.y + labelOffset;
|
|
7507
|
+
const iconSvg = iconName ? getIcon(iconName) : null;
|
|
7508
|
+
const iconSize = iconSvg ? Math.round(fontSize * 1.1) : 0;
|
|
7509
|
+
const iconGap = iconSvg ? 8 : 0;
|
|
7510
|
+
const edgePad = 12;
|
|
7511
|
+
const textPad = paddingX + extraPadding;
|
|
6402
7512
|
const idealTextWidth = text.length * fontSize * 0.6;
|
|
6403
|
-
const buttonWidth = fullWidth ? Math.max(1, pos.width) : this.clampControlWidth(Math.max(idealTextWidth +
|
|
6404
|
-
const
|
|
6405
|
-
const availableTextWidth = Math.max(0, buttonWidth - paddingX * 2);
|
|
7513
|
+
const buttonWidth = fullWidth ? Math.max(1, pos.width) : this.clampControlWidth(Math.max(idealTextWidth + (iconSvg ? iconSize + iconGap : 0) + textPad * 2, 60), pos.width);
|
|
7514
|
+
const availableTextWidth = Math.max(0, buttonWidth - textPad * 2 - (iconSvg ? iconSize + iconGap : 0));
|
|
6406
7515
|
const visibleText = this.truncateTextToWidth(text, availableTextWidth, fontSize);
|
|
6407
7516
|
const semanticBase = this.getSemanticVariantColor(variant);
|
|
6408
7517
|
const hasExplicitVariantColor = semanticBase !== void 0 || this.colorResolver.hasColor(variant);
|
|
6409
|
-
const variantColor = hasExplicitVariantColor ? this.resolveVariantColor(variant, this.renderTheme.primary) :
|
|
7518
|
+
const variantColor = hasExplicitVariantColor ? this.resolveVariantColor(variant, this.renderTheme.primary) : this.resolveTextColor();
|
|
6410
7519
|
const borderColor = variantColor;
|
|
6411
7520
|
const textColor = variantColor;
|
|
6412
7521
|
const strokeWidth = 0.5;
|
|
6413
|
-
|
|
6414
|
-
|
|
7522
|
+
const iconOffsetY = buttonY + (buttonHeight - iconSize) / 2;
|
|
7523
|
+
const iconX = iconAlign === "right" ? pos.x + buttonWidth - edgePad - iconSize : pos.x + edgePad;
|
|
7524
|
+
const textAlign = String(node.props.align || "center").toLowerCase();
|
|
7525
|
+
const sidePad = textPad + 4;
|
|
7526
|
+
let textX;
|
|
7527
|
+
let textAnchor;
|
|
7528
|
+
if (textAlign === "left") {
|
|
7529
|
+
textX = iconSvg && iconAlign === "left" ? pos.x + edgePad + iconSize + iconGap : pos.x + sidePad;
|
|
7530
|
+
textAnchor = "start";
|
|
7531
|
+
} else if (textAlign === "right") {
|
|
7532
|
+
textX = iconSvg && iconAlign === "right" ? pos.x + buttonWidth - edgePad - iconSize - iconGap : pos.x + buttonWidth - sidePad;
|
|
7533
|
+
textAnchor = "end";
|
|
7534
|
+
} else {
|
|
7535
|
+
textX = pos.x + buttonWidth / 2;
|
|
7536
|
+
textAnchor = "middle";
|
|
7537
|
+
}
|
|
7538
|
+
let svg = `<g${this.getDataNodeId(node)}>
|
|
7539
|
+
<rect x="${pos.x}" y="${buttonY}"
|
|
6415
7540
|
width="${buttonWidth}" height="${buttonHeight}"
|
|
6416
7541
|
rx="${radius}"
|
|
6417
7542
|
fill="none"
|
|
6418
7543
|
stroke="${borderColor}"
|
|
6419
7544
|
stroke-width="${strokeWidth}"
|
|
6420
|
-
filter="url(#sketch-rough)"
|
|
6421
|
-
|
|
7545
|
+
filter="url(#sketch-rough)"/>`;
|
|
7546
|
+
if (iconSvg) {
|
|
7547
|
+
svg += `
|
|
7548
|
+
<g transform="translate(${iconX}, ${iconOffsetY})">
|
|
7549
|
+
<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${textColor}" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round">
|
|
7550
|
+
${this.extractSvgContent(iconSvg)}
|
|
7551
|
+
</svg>
|
|
7552
|
+
</g>`;
|
|
7553
|
+
}
|
|
7554
|
+
svg += `
|
|
7555
|
+
<text x="${textX}" y="${buttonY + buttonHeight / 2 + fontSize * 0.35}"
|
|
6422
7556
|
font-family="${this.fontFamily}"
|
|
6423
7557
|
font-size="${fontSize}"
|
|
6424
7558
|
font-weight="${fontWeight}"
|
|
6425
7559
|
fill="${textColor}"
|
|
6426
|
-
text-anchor="
|
|
7560
|
+
text-anchor="${textAnchor}">${this.escapeXml(visibleText)}</text>
|
|
6427
7561
|
</g>`;
|
|
7562
|
+
return svg;
|
|
6428
7563
|
}
|
|
6429
7564
|
/**
|
|
6430
7565
|
* Render badge with colored border instead of fill
|
|
@@ -6434,7 +7569,7 @@ var SketchSVGRenderer = class extends SVGRenderer {
|
|
|
6434
7569
|
const variant = String(node.props.variant || "default");
|
|
6435
7570
|
const semanticBase = this.getSemanticVariantColor(variant);
|
|
6436
7571
|
const hasExplicitVariantColor = semanticBase !== void 0 || this.colorResolver.hasColor(variant);
|
|
6437
|
-
const variantColor = hasExplicitVariantColor ? this.resolveVariantColor(variant, this.renderTheme.primary) :
|
|
7572
|
+
const variantColor = hasExplicitVariantColor ? this.resolveVariantColor(variant, this.renderTheme.primary) : this.resolveTextColor();
|
|
6438
7573
|
const borderColor = variantColor;
|
|
6439
7574
|
const textColor = variantColor;
|
|
6440
7575
|
const badgeRadius = this.tokens.badge.radius === "pill" ? pos.height / 2 : this.tokens.badge.radius;
|
|
@@ -6461,17 +7596,22 @@ var SketchSVGRenderer = class extends SVGRenderer {
|
|
|
6461
7596
|
const iconName = String(node.props.icon || "help-circle");
|
|
6462
7597
|
const variant = String(node.props.variant || "default");
|
|
6463
7598
|
const size = String(node.props.size || "md");
|
|
7599
|
+
const density = this.ir.project.style.density || "normal";
|
|
7600
|
+
const labelOffset = this.parseBooleanProp(node.props.labelSpace, false) ? 18 : 0;
|
|
7601
|
+
const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
|
|
6464
7602
|
const semanticBase = this.getSemanticVariantColor(variant);
|
|
6465
7603
|
const hasExplicitVariantColor = semanticBase !== void 0 || this.colorResolver.hasColor(variant);
|
|
6466
|
-
const variantColor = hasExplicitVariantColor ? this.resolveVariantColor(variant, this.renderTheme.primary) :
|
|
7604
|
+
const variantColor = hasExplicitVariantColor ? this.resolveVariantColor(variant, this.renderTheme.primary) : this.resolveTextColor();
|
|
6467
7605
|
const borderColor = variantColor;
|
|
6468
7606
|
const iconColor = variantColor;
|
|
6469
|
-
const buttonSize =
|
|
7607
|
+
const buttonSize = Math.max(16, Math.min(resolveControlHeight(size, density), pos.height - labelOffset));
|
|
7608
|
+
const buttonWidth = buttonSize + extraPadding * 2;
|
|
6470
7609
|
const radius = 6;
|
|
7610
|
+
const buttonY = pos.y + labelOffset;
|
|
6471
7611
|
const iconSvg = this.getIconSvg(iconName);
|
|
6472
7612
|
let svg = `<g${this.getDataNodeId(node)}>
|
|
6473
|
-
<rect x="${pos.x}" y="${
|
|
6474
|
-
width="${
|
|
7613
|
+
<rect x="${pos.x}" y="${buttonY}"
|
|
7614
|
+
width="${buttonWidth}" height="${buttonSize}"
|
|
6475
7615
|
rx="${radius}"
|
|
6476
7616
|
fill="none"
|
|
6477
7617
|
stroke="${borderColor}"
|
|
@@ -6479,8 +7619,8 @@ var SketchSVGRenderer = class extends SVGRenderer {
|
|
|
6479
7619
|
filter="url(#sketch-rough)"/>`;
|
|
6480
7620
|
if (iconSvg) {
|
|
6481
7621
|
const iconSize = buttonSize * 0.6;
|
|
6482
|
-
const offsetX = pos.x + (
|
|
6483
|
-
const offsetY =
|
|
7622
|
+
const offsetX = pos.x + (buttonWidth - iconSize) / 2;
|
|
7623
|
+
const offsetY = buttonY + (buttonSize - iconSize) / 2;
|
|
6484
7624
|
svg += `
|
|
6485
7625
|
<g transform="translate(${offsetX}, ${offsetY})">
|
|
6486
7626
|
<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round">
|
|
@@ -6544,29 +7684,67 @@ var SketchSVGRenderer = class extends SVGRenderer {
|
|
|
6544
7684
|
renderInput(node, pos) {
|
|
6545
7685
|
const label = String(node.props.label || "");
|
|
6546
7686
|
const placeholder = String(node.props.placeholder || "");
|
|
7687
|
+
const iconLeftName = String(node.props.iconLeft || "").trim();
|
|
7688
|
+
const iconRightName = String(node.props.iconRight || "").trim();
|
|
6547
7689
|
const radius = this.tokens.input.radius;
|
|
6548
7690
|
const fontSize = this.tokens.input.fontSize;
|
|
6549
7691
|
const paddingX = this.tokens.input.paddingX;
|
|
6550
7692
|
const labelOffset = this.getControlLabelOffset(label);
|
|
6551
7693
|
const controlY = pos.y + labelOffset;
|
|
6552
7694
|
const controlHeight = Math.max(16, pos.height - labelOffset);
|
|
6553
|
-
|
|
6554
|
-
|
|
7695
|
+
const iconSize = 16;
|
|
7696
|
+
const iconPad = 12;
|
|
7697
|
+
const iconInnerGap = 8;
|
|
7698
|
+
const iconLeftSvg = iconLeftName ? getIcon(iconLeftName) : null;
|
|
7699
|
+
const iconRightSvg = iconRightName ? getIcon(iconRightName) : null;
|
|
7700
|
+
const leftOffset = iconLeftSvg ? iconPad + iconSize + iconInnerGap : 0;
|
|
7701
|
+
const rightOffset = iconRightSvg ? iconPad + iconSize + iconInnerGap : 0;
|
|
7702
|
+
const textX = pos.x + (iconLeftSvg ? leftOffset : paddingX);
|
|
7703
|
+
const iconColor = "#888888";
|
|
7704
|
+
const iconCenterY = controlY + (controlHeight - iconSize) / 2;
|
|
7705
|
+
let svg = `<g${this.getDataNodeId(node)}>`;
|
|
7706
|
+
if (label) {
|
|
7707
|
+
svg += `
|
|
7708
|
+
<text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
|
|
6555
7709
|
font-family="${this.fontFamily}"
|
|
6556
7710
|
font-size="12"
|
|
6557
|
-
fill="${this.renderTheme.text}">${this.escapeXml(label)}</text
|
|
7711
|
+
fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>`;
|
|
7712
|
+
}
|
|
7713
|
+
svg += `
|
|
6558
7714
|
<rect x="${pos.x}" y="${controlY}"
|
|
6559
7715
|
width="${pos.width}" height="${controlHeight}"
|
|
6560
7716
|
rx="${radius}"
|
|
6561
7717
|
fill="${this.renderTheme.cardBg}"
|
|
6562
7718
|
stroke="#2D3748"
|
|
6563
7719
|
stroke-width="0.5"
|
|
6564
|
-
filter="url(#sketch-rough)"
|
|
6565
|
-
|
|
7720
|
+
filter="url(#sketch-rough)"/>`;
|
|
7721
|
+
if (iconLeftSvg) {
|
|
7722
|
+
svg += `
|
|
7723
|
+
<g transform="translate(${pos.x + iconPad}, ${iconCenterY})">
|
|
7724
|
+
<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
7725
|
+
${this.extractSvgContent(iconLeftSvg)}
|
|
7726
|
+
</svg>
|
|
7727
|
+
</g>`;
|
|
7728
|
+
}
|
|
7729
|
+
if (iconRightSvg) {
|
|
7730
|
+
svg += `
|
|
7731
|
+
<g transform="translate(${pos.x + pos.width - iconPad - iconSize}, ${iconCenterY})">
|
|
7732
|
+
<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
7733
|
+
${this.extractSvgContent(iconRightSvg)}
|
|
7734
|
+
</svg>
|
|
7735
|
+
</g>`;
|
|
7736
|
+
}
|
|
7737
|
+
if (placeholder) {
|
|
7738
|
+
const availWidth = pos.width - (iconLeftSvg ? leftOffset : paddingX) - (iconRightSvg ? rightOffset : paddingX);
|
|
7739
|
+
const visiblePh = this.truncateTextToWidth(placeholder, Math.max(0, availWidth), fontSize);
|
|
7740
|
+
svg += `
|
|
7741
|
+
<text x="${textX}" y="${controlY + controlHeight / 2 + 5}"
|
|
6566
7742
|
font-family="${this.fontFamily}"
|
|
6567
7743
|
font-size="${fontSize}"
|
|
6568
|
-
fill="${this.renderTheme.textMuted}">${this.escapeXml(
|
|
6569
|
-
|
|
7744
|
+
fill="${this.renderTheme.textMuted}">${this.escapeXml(visiblePh)}</text>`;
|
|
7745
|
+
}
|
|
7746
|
+
svg += "\n </g>";
|
|
7747
|
+
return svg;
|
|
6570
7748
|
}
|
|
6571
7749
|
/**
|
|
6572
7750
|
* Render textarea with thicker border
|
|
@@ -6631,6 +7809,8 @@ var SketchSVGRenderer = class extends SVGRenderer {
|
|
|
6631
7809
|
*/
|
|
6632
7810
|
renderHeading(node, pos) {
|
|
6633
7811
|
const text = String(node.props.text || "Heading");
|
|
7812
|
+
const variant = String(node.props.variant || "default");
|
|
7813
|
+
const headingColor = variant === "default" ? this.resolveTextColor() : this.resolveVariantColor(variant, this.resolveTextColor());
|
|
6634
7814
|
const headingTypography = this.getHeadingTypography(node);
|
|
6635
7815
|
const fontSize = headingTypography.fontSize;
|
|
6636
7816
|
const fontWeight = headingTypography.fontWeight;
|
|
@@ -6643,7 +7823,7 @@ var SketchSVGRenderer = class extends SVGRenderer {
|
|
|
6643
7823
|
font-family="${this.fontFamily}"
|
|
6644
7824
|
font-size="${fontSize}"
|
|
6645
7825
|
font-weight="${fontWeight}"
|
|
6646
|
-
fill="${
|
|
7826
|
+
fill="${headingColor}">${this.escapeXml(text)}</text>
|
|
6647
7827
|
</g>`;
|
|
6648
7828
|
}
|
|
6649
7829
|
const tspans = lines.map(
|
|
@@ -6654,7 +7834,7 @@ var SketchSVGRenderer = class extends SVGRenderer {
|
|
|
6654
7834
|
font-family="${this.fontFamily}"
|
|
6655
7835
|
font-size="${fontSize}"
|
|
6656
7836
|
font-weight="${fontWeight}"
|
|
6657
|
-
fill="${
|
|
7837
|
+
fill="${headingColor}">${tspans}</text>
|
|
6658
7838
|
</g>`;
|
|
6659
7839
|
}
|
|
6660
7840
|
/**
|
|
@@ -6665,7 +7845,8 @@ var SketchSVGRenderer = class extends SVGRenderer {
|
|
|
6665
7845
|
const subtitle = String(node.props.subtitle || "");
|
|
6666
7846
|
const actions = String(node.props.actions || "");
|
|
6667
7847
|
const user = String(node.props.user || "");
|
|
6668
|
-
const
|
|
7848
|
+
const variant = String(node.props.variant || "default");
|
|
7849
|
+
const accentColor = variant === "default" ? this.resolveAccentColor() : this.resolveVariantColor(variant, this.resolveAccentColor());
|
|
6669
7850
|
const topbar = this.calculateTopbarLayout(node, pos, title, subtitle, actions, user);
|
|
6670
7851
|
let svg = `<g${this.getDataNodeId(node)}>
|
|
6671
7852
|
<rect x="${pos.x}" y="${pos.y}"
|
|
@@ -6758,79 +7939,14 @@ var SketchSVGRenderer = class extends SVGRenderer {
|
|
|
6758
7939
|
* Render table with sketch filter and Comic Sans
|
|
6759
7940
|
*/
|
|
6760
7941
|
renderTable(node, pos) {
|
|
6761
|
-
const
|
|
6762
|
-
|
|
6763
|
-
const columns = columnsStr.split(",").map((c) => c.trim());
|
|
6764
|
-
const rowCount = Number(node.props.rows || node.props.rowsMock || 5);
|
|
6765
|
-
const mockStr = String(node.props.mock || "");
|
|
6766
|
-
const random = this.parseBooleanProp(node.props.random, false);
|
|
6767
|
-
const mockTypes = mockStr ? mockStr.split(",").map((m) => m.trim()).filter(Boolean) : [];
|
|
6768
|
-
while (mockTypes.length < columns.length) {
|
|
6769
|
-
const inferred = MockDataGenerator.inferMockTypeFromColumn(columns[mockTypes.length] || "item");
|
|
6770
|
-
mockTypes.push(inferred);
|
|
6771
|
-
}
|
|
6772
|
-
const headerHeight = 44;
|
|
6773
|
-
const rowHeight = 36;
|
|
6774
|
-
const colWidth = pos.width / columns.length;
|
|
6775
|
-
const mockRows = [];
|
|
6776
|
-
for (let rowIdx = 0; rowIdx < rowCount; rowIdx++) {
|
|
6777
|
-
const row = {};
|
|
6778
|
-
columns.forEach((col, colIdx) => {
|
|
6779
|
-
const mockType = mockTypes[colIdx] || MockDataGenerator.inferMockTypeFromColumn(col) || "item";
|
|
6780
|
-
row[col] = MockDataGenerator.getMockValue(mockType, rowIdx, random);
|
|
6781
|
-
});
|
|
6782
|
-
mockRows.push(row);
|
|
6783
|
-
}
|
|
6784
|
-
let svg = `<g${this.getDataNodeId(node)}>
|
|
6785
|
-
<rect x="${pos.x}" y="${pos.y}"
|
|
6786
|
-
width="${pos.width}" height="${pos.height}"
|
|
6787
|
-
rx="8"
|
|
6788
|
-
fill="${this.renderTheme.cardBg}"
|
|
6789
|
-
stroke="#2D3748"
|
|
6790
|
-
stroke-width="0.5"
|
|
6791
|
-
filter="url(#sketch-rough)"/>`;
|
|
6792
|
-
if (title) {
|
|
6793
|
-
svg += `
|
|
6794
|
-
<text x="${pos.x + 16}" y="${pos.y + 24}"
|
|
6795
|
-
font-family="${this.fontFamily}"
|
|
6796
|
-
font-size="13"
|
|
6797
|
-
font-weight="600"
|
|
6798
|
-
fill="${this.renderTheme.text}">${this.escapeXml(title)}</text>`;
|
|
6799
|
-
}
|
|
6800
|
-
const headerY = pos.y + (title ? 32 : 0);
|
|
6801
|
-
svg += `
|
|
6802
|
-
<line x1="${pos.x}" y1="${headerY + headerHeight}" x2="${pos.x + pos.width}" y2="${headerY + headerHeight}"
|
|
6803
|
-
stroke="#2D3748" stroke-width="0.5" filter="url(#sketch-rough)"/>`;
|
|
6804
|
-
columns.forEach((col, i) => {
|
|
6805
|
-
svg += `
|
|
6806
|
-
<text x="${pos.x + i * colWidth + 12}" y="${headerY + 26}"
|
|
6807
|
-
font-family="${this.fontFamily}"
|
|
6808
|
-
font-size="11"
|
|
6809
|
-
font-weight="600"
|
|
6810
|
-
fill="${this.renderTheme.textMuted}">${this.escapeXml(col)}</text>`;
|
|
6811
|
-
});
|
|
6812
|
-
mockRows.forEach((row, rowIdx) => {
|
|
6813
|
-
const rowY = headerY + headerHeight + rowIdx * rowHeight;
|
|
6814
|
-
svg += `
|
|
6815
|
-
<line x1="${pos.x}" y1="${rowY + rowHeight}" x2="${pos.x + pos.width}" y2="${rowY + rowHeight}"
|
|
6816
|
-
stroke="#2D3748" stroke-width="0.5" filter="url(#sketch-rough)"/>`;
|
|
6817
|
-
columns.forEach((col, colIdx) => {
|
|
6818
|
-
const cellValue = row[col] || "";
|
|
6819
|
-
svg += `
|
|
6820
|
-
<text x="${pos.x + colIdx * colWidth + 12}" y="${rowY + 22}"
|
|
6821
|
-
font-family="${this.fontFamily}"
|
|
6822
|
-
font-size="12"
|
|
6823
|
-
fill="${this.renderTheme.text}">${this.escapeXml(cellValue)}</text>`;
|
|
6824
|
-
});
|
|
6825
|
-
});
|
|
6826
|
-
svg += "\n </g>";
|
|
6827
|
-
return svg;
|
|
7942
|
+
const standard = super.renderTable(node, pos);
|
|
7943
|
+
return standard.replace("<g", '<g filter="url(#sketch-rough)"');
|
|
6828
7944
|
}
|
|
6829
7945
|
/**
|
|
6830
7946
|
* Render text with Comic Sans
|
|
6831
7947
|
*/
|
|
6832
7948
|
renderText(node, pos) {
|
|
6833
|
-
const text = String(node.props.
|
|
7949
|
+
const text = String(node.props.text || "Text content");
|
|
6834
7950
|
const fontSize = this.tokens.text.fontSize;
|
|
6835
7951
|
const lineHeightPx = Math.ceil(fontSize * this.tokens.text.lineHeight);
|
|
6836
7952
|
const lines = this.wrapTextToLines(text, pos.width, fontSize);
|
|
@@ -6882,31 +7998,67 @@ var SketchSVGRenderer = class extends SVGRenderer {
|
|
|
6882
7998
|
renderSelect(node, pos) {
|
|
6883
7999
|
const label = String(node.props.label || "");
|
|
6884
8000
|
const placeholder = String(node.props.placeholder || "Select...");
|
|
8001
|
+
const iconLeftName = String(node.props.iconLeft || "").trim();
|
|
8002
|
+
const iconRightName = String(node.props.iconRight || "").trim();
|
|
6885
8003
|
const labelOffset = this.getControlLabelOffset(label);
|
|
6886
8004
|
const controlY = pos.y + labelOffset;
|
|
6887
8005
|
const controlHeight = Math.max(16, pos.height - labelOffset);
|
|
6888
8006
|
const centerY = controlY + controlHeight / 2 + 5;
|
|
6889
|
-
|
|
6890
|
-
|
|
8007
|
+
const iconSize = 16;
|
|
8008
|
+
const iconPad = 12;
|
|
8009
|
+
const iconInnerGap = 8;
|
|
8010
|
+
const iconLeftSvg = iconLeftName ? getIcon(iconLeftName) : null;
|
|
8011
|
+
const iconRightSvg = iconRightName ? getIcon(iconRightName) : null;
|
|
8012
|
+
const leftOffset = iconLeftSvg ? iconPad + iconSize + iconInnerGap : 0;
|
|
8013
|
+
const chevronWidth = 20;
|
|
8014
|
+
const iconColor = "#888888";
|
|
8015
|
+
const iconCenterY = controlY + (controlHeight - iconSize) / 2;
|
|
8016
|
+
let svg = `<g${this.getDataNodeId(node)}>`;
|
|
8017
|
+
if (label) {
|
|
8018
|
+
svg += `
|
|
8019
|
+
<text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
|
|
6891
8020
|
font-family="${this.fontFamily}"
|
|
6892
8021
|
font-size="12"
|
|
6893
|
-
fill="${this.renderTheme.text}">${this.escapeXml(label)}</text
|
|
8022
|
+
fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>`;
|
|
8023
|
+
}
|
|
8024
|
+
svg += `
|
|
6894
8025
|
<rect x="${pos.x}" y="${controlY}"
|
|
6895
8026
|
width="${pos.width}" height="${controlHeight}"
|
|
6896
8027
|
rx="6"
|
|
6897
8028
|
fill="${this.renderTheme.cardBg}"
|
|
6898
8029
|
stroke="#2D3748"
|
|
6899
8030
|
stroke-width="0.5"
|
|
6900
|
-
filter="url(#sketch-rough)"
|
|
6901
|
-
|
|
8031
|
+
filter="url(#sketch-rough)"/>`;
|
|
8032
|
+
if (iconLeftSvg) {
|
|
8033
|
+
svg += `
|
|
8034
|
+
<g transform="translate(${pos.x + iconPad}, ${iconCenterY})">
|
|
8035
|
+
<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
8036
|
+
${this.extractSvgContent(iconLeftSvg)}
|
|
8037
|
+
</svg>
|
|
8038
|
+
</g>`;
|
|
8039
|
+
}
|
|
8040
|
+
if (iconRightSvg) {
|
|
8041
|
+
svg += `
|
|
8042
|
+
<g transform="translate(${pos.x + pos.width - chevronWidth - iconPad - iconSize}, ${iconCenterY})">
|
|
8043
|
+
<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
8044
|
+
${this.extractSvgContent(iconRightSvg)}
|
|
8045
|
+
</svg>
|
|
8046
|
+
</g>`;
|
|
8047
|
+
}
|
|
8048
|
+
const textX = pos.x + (iconLeftSvg ? leftOffset : 12);
|
|
8049
|
+
const availWidth = pos.width - (iconLeftSvg ? leftOffset : 12) - chevronWidth - (iconRightSvg ? iconPad + iconSize + iconInnerGap : 0);
|
|
8050
|
+
const visiblePh = this.truncateTextToWidth(placeholder, Math.max(0, availWidth), 14);
|
|
8051
|
+
svg += `
|
|
8052
|
+
<text x="${textX}" y="${centerY}"
|
|
6902
8053
|
font-family="${this.fontFamily}"
|
|
6903
8054
|
font-size="14"
|
|
6904
|
-
fill="${this.renderTheme.textMuted}">${this.escapeXml(
|
|
8055
|
+
fill="${this.renderTheme.textMuted}">${this.escapeXml(visiblePh)}</text>
|
|
6905
8056
|
<text x="${pos.x + pos.width - 20}" y="${centerY}"
|
|
6906
8057
|
font-family="${this.fontFamily}"
|
|
6907
8058
|
font-size="16"
|
|
6908
8059
|
fill="${this.renderTheme.textMuted}">\u25BC</text>
|
|
6909
8060
|
</g>`;
|
|
8061
|
+
return svg;
|
|
6910
8062
|
}
|
|
6911
8063
|
/**
|
|
6912
8064
|
* Render checkbox with sketch filter and Comic Sans
|
|
@@ -7305,44 +8457,43 @@ var SketchSVGRenderer = class extends SVGRenderer {
|
|
|
7305
8457
|
renderImage(node, pos) {
|
|
7306
8458
|
const placeholder = String(node.props.placeholder || "landscape").toLowerCase();
|
|
7307
8459
|
const iconType = String(node.props.icon || "").trim();
|
|
8460
|
+
const variant = String(node.props.variant || "").trim();
|
|
7308
8461
|
const iconSvg = placeholder === "icon" && iconType.length > 0 ? getIcon(iconType) : null;
|
|
8462
|
+
const imageBg = this.options.theme === "dark" ? "#2A2A2A" : "#E8E8E8";
|
|
7309
8463
|
if (iconSvg) {
|
|
7310
|
-
const
|
|
7311
|
-
const
|
|
7312
|
-
const
|
|
7313
|
-
const
|
|
7314
|
-
const
|
|
7315
|
-
const
|
|
8464
|
+
const semanticBase = variant ? this.getSemanticVariantColor(variant) : void 0;
|
|
8465
|
+
const hasVariant = variant.length > 0 && (semanticBase !== void 0 || this.colorResolver.hasColor(variant));
|
|
8466
|
+
const variantColor = hasVariant ? this.resolveVariantColor(variant, this.renderTheme.primary) : null;
|
|
8467
|
+
const bgColor = hasVariant ? this.hexToRgba(variantColor, 0.12) : imageBg;
|
|
8468
|
+
const iconColor = hasVariant ? variantColor : "#666666";
|
|
8469
|
+
const iconSize = Math.max(16, Math.min(pos.width, pos.height) * 0.6);
|
|
8470
|
+
const iconOffsetX = pos.x + (pos.width - iconSize) / 2;
|
|
8471
|
+
const iconOffsetY = pos.y + (pos.height - iconSize) / 2;
|
|
7316
8472
|
return `<g${this.getDataNodeId(node)}>
|
|
7317
|
-
<!-- Image Background -->
|
|
7318
8473
|
<rect x="${pos.x}" y="${pos.y}"
|
|
7319
8474
|
width="${pos.width}" height="${pos.height}"
|
|
7320
|
-
fill="
|
|
7321
|
-
stroke="#2D3748"
|
|
7322
|
-
stroke-width="0.5"
|
|
8475
|
+
fill="${bgColor}"
|
|
7323
8476
|
rx="4"
|
|
7324
8477
|
filter="url(#sketch-rough)"/>
|
|
7325
|
-
|
|
7326
|
-
<!-- Custom Icon Placeholder -->
|
|
7327
|
-
<rect x="${badgeX}" y="${badgeY}"
|
|
7328
|
-
width="${badgeSize}" height="${badgeSize}"
|
|
7329
|
-
rx="${Math.max(4, badgeSize * 0.2)}"
|
|
7330
|
-
fill="none"
|
|
7331
|
-
stroke="#2D3748"
|
|
7332
|
-
stroke-width="0.5"
|
|
7333
|
-
filter="url(#sketch-rough)"/>
|
|
7334
8478
|
<g transform="translate(${iconOffsetX}, ${iconOffsetY})">
|
|
7335
|
-
<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="
|
|
8479
|
+
<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
7336
8480
|
${this.extractSvgContent(iconSvg)}
|
|
7337
8481
|
</svg>
|
|
7338
8482
|
</g>
|
|
8483
|
+
<rect x="${pos.x}" y="${pos.y}"
|
|
8484
|
+
width="${pos.width}" height="${pos.height}"
|
|
8485
|
+
fill="none"
|
|
8486
|
+
stroke="#2D3748"
|
|
8487
|
+
stroke-width="0.5"
|
|
8488
|
+
rx="4"
|
|
8489
|
+
filter="url(#sketch-rough)"/>
|
|
7339
8490
|
</g>`;
|
|
7340
8491
|
}
|
|
7341
8492
|
return `<g${this.getDataNodeId(node)}>
|
|
7342
8493
|
<!-- Image Background -->
|
|
7343
8494
|
<rect x="${pos.x}" y="${pos.y}"
|
|
7344
8495
|
width="${pos.width}" height="${pos.height}"
|
|
7345
|
-
fill="
|
|
8496
|
+
fill="${imageBg}"
|
|
7346
8497
|
stroke="#2D3748"
|
|
7347
8498
|
stroke-width="0.5"
|
|
7348
8499
|
rx="4"
|
|
@@ -7397,17 +8548,23 @@ var SketchSVGRenderer = class extends SVGRenderer {
|
|
|
7397
8548
|
*/
|
|
7398
8549
|
renderSidebarMenu(node, pos) {
|
|
7399
8550
|
const itemsStr = String(node.props.items || "Item 1,Item 2,Item 3");
|
|
8551
|
+
const iconsStr = String(node.props.icons || "");
|
|
7400
8552
|
const items = itemsStr.split(",").map((s) => s.trim());
|
|
8553
|
+
const icons = iconsStr ? iconsStr.split(",").map((s) => s.trim()) : [];
|
|
7401
8554
|
const itemHeight = 40;
|
|
7402
8555
|
const fontSize = 14;
|
|
7403
8556
|
const activeIndex = Number(node.props.active || 0);
|
|
7404
8557
|
const accentColor = this.resolveAccentColor();
|
|
8558
|
+
const variantProp = String(node.props.variant || "").trim();
|
|
8559
|
+
const semanticVariant = variantProp ? this.getSemanticVariantColor(variantProp) : void 0;
|
|
8560
|
+
const hasVariant = variantProp.length > 0 && (semanticVariant !== void 0 || this.colorResolver.hasColor(variantProp));
|
|
8561
|
+
const activeColor = hasVariant ? this.resolveVariantColor(variantProp, accentColor) : accentColor;
|
|
7405
8562
|
let svg = `<g${this.getDataNodeId(node)}>`;
|
|
7406
8563
|
items.forEach((item, index) => {
|
|
7407
8564
|
const itemY = pos.y + index * itemHeight;
|
|
7408
8565
|
const isActive = index === activeIndex;
|
|
7409
|
-
const bgColor = isActive ? this.hexToRgba(
|
|
7410
|
-
const textColor = isActive ?
|
|
8566
|
+
const bgColor = isActive ? this.hexToRgba(activeColor, 0.15) : "transparent";
|
|
8567
|
+
const textColor = isActive ? activeColor : this.resolveTextColor();
|
|
7411
8568
|
const fontWeight = isActive ? "500" : "400";
|
|
7412
8569
|
if (isActive) {
|
|
7413
8570
|
svg += `
|
|
@@ -7417,8 +8574,24 @@ var SketchSVGRenderer = class extends SVGRenderer {
|
|
|
7417
8574
|
fill="${bgColor}"
|
|
7418
8575
|
filter="url(#sketch-rough)"/>`;
|
|
7419
8576
|
}
|
|
8577
|
+
let currentX = pos.x + 12;
|
|
8578
|
+
if (icons[index]) {
|
|
8579
|
+
const iconSvg = getIcon(icons[index]);
|
|
8580
|
+
if (iconSvg) {
|
|
8581
|
+
const iconSize = 16;
|
|
8582
|
+
const iconY = itemY + (itemHeight - iconSize) / 2;
|
|
8583
|
+
const iconColor = isActive ? activeColor : this.resolveMutedColor();
|
|
8584
|
+
svg += `
|
|
8585
|
+
<g transform="translate(${currentX}, ${iconY})">
|
|
8586
|
+
<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
8587
|
+
${this.extractSvgContent(iconSvg)}
|
|
8588
|
+
</svg>
|
|
8589
|
+
</g>`;
|
|
8590
|
+
currentX += iconSize + 8;
|
|
8591
|
+
}
|
|
8592
|
+
}
|
|
7420
8593
|
svg += `
|
|
7421
|
-
<text x="${
|
|
8594
|
+
<text x="${currentX}" y="${itemY + itemHeight / 2 + 5}"
|
|
7422
8595
|
font-family="${this.fontFamily}"
|
|
7423
8596
|
font-size="${fontSize}"
|
|
7424
8597
|
font-weight="${fontWeight}"
|
|
@@ -7431,22 +8604,23 @@ var SketchSVGRenderer = class extends SVGRenderer {
|
|
|
7431
8604
|
* Render icon (same as base, icons don't need filter)
|
|
7432
8605
|
*/
|
|
7433
8606
|
renderIcon(node, pos) {
|
|
7434
|
-
const iconType = String(node.props.
|
|
8607
|
+
const iconType = String(node.props.icon || "help-circle");
|
|
7435
8608
|
const size = String(node.props.size || "md");
|
|
8609
|
+
const variant = String(node.props.variant || "default");
|
|
7436
8610
|
const iconSvg = getIcon(iconType);
|
|
7437
8611
|
if (!iconSvg) {
|
|
7438
8612
|
return `<g${this.getDataNodeId(node)}>
|
|
7439
8613
|
<circle cx="${pos.x + pos.width / 2}" cy="${pos.y + pos.height / 2}"
|
|
7440
8614
|
r="${Math.min(pos.width, pos.height) / 2 - 2}"
|
|
7441
|
-
fill="none" stroke="
|
|
8615
|
+
fill="none" stroke="${this.resolveMutedColor()}" stroke-width="0.5"
|
|
7442
8616
|
filter="url(#sketch-rough)"/>
|
|
7443
8617
|
<text x="${pos.x + pos.width / 2}" y="${pos.y + pos.height / 2 + 4}"
|
|
7444
8618
|
font-family="${this.fontFamily}"
|
|
7445
|
-
font-size="12" fill="
|
|
8619
|
+
font-size="12" fill="${this.resolveMutedColor()}" text-anchor="middle">?</text>
|
|
7446
8620
|
</g>`;
|
|
7447
8621
|
}
|
|
7448
8622
|
const iconSize = this.getIconSize(size);
|
|
7449
|
-
const iconColor = "
|
|
8623
|
+
const iconColor = variant === "default" ? this.resolveTextColor() : this.resolveVariantColor(variant, this.resolveTextColor());
|
|
7450
8624
|
const offsetX = pos.x + (pos.width - iconSize) / 2;
|
|
7451
8625
|
const offsetY = pos.y + (pos.height - iconSize) / 2;
|
|
7452
8626
|
return `<g${this.getDataNodeId(node)} transform="translate(${offsetX}, ${offsetY})">
|