@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.js CHANGED
@@ -45,20 +45,21 @@ var SourceMapBuilder = class {
45
45
  return nodeId;
46
46
  }
47
47
  /**
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
- */
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 Define = createToken({ name: "Define", pattern: /define/ });
425
- var Style = createToken({ name: "Style", pattern: /style/ });
426
- var Mocks = createToken({ name: "Mocks", pattern: /mocks/ });
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 emitWarning = (message, code, range, nodeId, suggestion) => {
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: "warning",
1565
+ severity,
1474
1566
  phase: "semantic",
1475
1567
  range,
1476
1568
  nodeId,
1477
1569
  suggestion
1478
1570
  });
1479
1571
  };
1480
- const checkComponent = (component) => {
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
- if (!enumValues.includes(normalizedValue)) {
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 === "sidebar") {
1618
- const sidebar = Number(paramValue);
1619
- if (!Number.isFinite(sidebar) || sidebar <= 0) {
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
- 'Split "sidebar" must be a positive number.',
1622
- "LAYOUT_SPLIT_SIDEBAR_INVALID",
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
- "Use a value like sidebar: 240."
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
- validateComponentDefinitionCycles(ast);
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
- validateComponentDefinitionCycles(ast);
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 validateComponentDefinitionCycles(ast) {
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
- function getComponentDependencies(node) {
1744
- const deps = /* @__PURE__ */ new Set();
1745
- if (node.type === "layout") {
1746
- const layout = node;
1747
- if (layout.children) {
1748
- layout.children.forEach((child) => {
1749
- if (child.type === "component") {
1750
- const component = child;
1751
- deps.add(component.componentType);
1752
- } else if (child.type === "layout") {
1753
- const nested = getComponentDependencies(child);
1754
- nested.forEach((d) => deps.add(d));
1755
- } else if (child.type === "cell") {
1756
- const cell = child;
1757
- if (cell.children) {
1758
- cell.children.forEach((cellChild) => {
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
- return deps;
1772
- }
1773
- function hasCycle(componentName, path = []) {
1774
- if (recursionStack.has(componentName)) {
1775
- const cycleStart = path.indexOf(componentName);
1776
- const cycle = path.slice(cycleStart).concat(componentName);
1777
- return cycle;
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
- if (visited.has(componentName)) {
1780
- return null;
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
- const component = components.get(componentName);
1783
- if (!component) {
2041
+ if (visited.has(key)) {
1784
2042
  return null;
1785
2043
  }
1786
- recursionStack.add(componentName);
1787
- const currentPath = [...path, componentName];
1788
- const dependencies = getComponentDependencies(component.body);
2044
+ recursionStack.add(key);
2045
+ const currentPath = [...path, key];
2046
+ const dependencies = getDependencies(key);
1789
2047
  for (const dep of dependencies) {
1790
- const definedDep = components.has(dep);
1791
- if (definedDep) {
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(componentName);
1799
- visited.add(componentName);
2051
+ recursionStack.delete(key);
2052
+ visited.add(key);
1800
2053
  return null;
1801
- }
1802
- for (const [componentName] of components) {
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 = hasCycle(componentName);
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 layout.children) {
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 (layout.params.padding) {
2159
- style.padding = String(layout.params.padding);
2442
+ if (layoutParams.padding !== void 0) {
2443
+ style.padding = String(layoutParams.padding);
2160
2444
  } else {
2161
2445
  style.padding = "none";
2162
2446
  }
2163
- if (layout.params.gap) {
2164
- style.gap = String(layout.params.gap);
2447
+ if (layoutParams.gap !== void 0) {
2448
+ style.gap = String(layoutParams.gap);
2165
2449
  }
2166
- if (layout.params.align) {
2167
- style.align = layout.params.align;
2450
+ if (layoutParams.align !== void 0) {
2451
+ style.align = layoutParams.align;
2168
2452
  }
2169
- if (layout.params.background) {
2170
- style.background = String(layout.params.background);
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(layout.params),
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: component.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
- return this.convertLayout(definition.body);
2585
+ const result = this.convertLayout(definition.body, context);
2586
+ this.reportUnusedArguments(context);
2587
+ return result;
2276
2588
  } else if (definition.body.type === "component") {
2277
- return this.convertComponent(definition.body);
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: 24, md: 28, lg: 32 },
2332
- normal: { sm: 28, md: 32, lg: 40 },
2333
- comfortable: { sm: 32, md: 40, lg: 48 }
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 sidebarWidth = Number(node.params.sidebar) || 260;
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
- this.calculateNode(node.children[0].ref, x, y, sidebarWidth, height, "split");
2788
- const contentX = x + sidebarWidth + gap;
2789
- const contentWidth = width - sidebarWidth - gap;
2790
- this.calculateNode(node.children[1].ref, contentX, y, contentWidth, height, "split");
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 controlLabelOffset = node.componentType === "Input" || node.componentType === "Textarea" || node.componentType === "Select" ? this.getControlLabelOffset(String(node.props.label || "")) : 0;
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 = String(node.props.pagination) === "true";
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
- const paginationHeight = hasPagination ? 64 : 0;
2971
- return titleHeight + headerHeight + rowCount * rowHeight + paginationHeight;
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 density = this.style.density || "normal";
2981
- const verticalPadding = resolveHeadingVerticalPadding(node.props.spacing, density);
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.content || "");
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 this.getComponentHeight() + controlLabelOffset;
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
- return resolveIconButtonSize(size, this.style.density || "normal");
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.content || node.props.text || "");
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: "#1E293B",
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: "#0F172A",
3937
- cardBg: "#1E293B",
3938
- border: "#334155",
3939
- text: "#F1F5F9",
3940
- textMuted: "#94A3B8",
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: "#1E3A8A"
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 = "system-ui, -apple-system, sans-serif";
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="system-ui, -apple-system, sans-serif"
4772
+ font-family="Arial, Helvetica, sans-serif"
4150
4773
  font-size="${fontSize}"
4151
4774
  font-weight="${fontWeight}"
4152
- fill="${this.renderTheme.text}">${this.escapeXml(text)}</text>
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="system-ui, -apple-system, sans-serif"
4783
+ font-family="Arial, Helvetica, sans-serif"
4161
4784
  font-size="${fontSize}"
4162
4785
  font-weight="${fontWeight}"
4163
- fill="${this.renderTheme.text}">${tspans}</text>
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 paddingY = this.tokens.button.paddingY;
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 + paddingX * 2), 60), pos.width);
4177
- const buttonHeight = fontSize + paddingY * 2;
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 bgColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.85) : "rgba(226, 232, 240, 0.9)";
4184
- const textColor = hasExplicitVariantColor ? "#FFFFFF" : "rgba(30, 41, 59, 0.85)";
4185
- const borderColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.7) : "rgba(100, 116, 139, 0.4)";
4186
- return `<g${this.getDataNodeId(node)}>
4187
- <rect x="${pos.x}" y="${pos.y}"
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
- <text x="${pos.x + buttonWidth / 2}" y="${pos.y + buttonHeight / 2 + fontSize * 0.35}"
4194
- font-family="system-ui, -apple-system, sans-serif"
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="middle">${this.escapeXml(visibleText)}</text>
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 = fontSize + paddingY * 2;
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="system-ui, -apple-system, sans-serif"
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
- return `<g${this.getDataNodeId(node)}>
4246
- ${label ? `<text x="${pos.x + paddingX}" y="${this.getControlLabelBaselineY(pos.y)}"
4247
- font-family="system-ui, -apple-system, sans-serif"
4248
- font-size="12"
4249
- fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>` : ""}
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
- <text x="${pos.x + paddingX}" y="${controlY + controlHeight / 2 + 5}"
4257
- font-family="system-ui, -apple-system, sans-serif"
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(placeholder)}</text>
4260
- </g>`;
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 accentColor = this.resolveAccentColor();
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
- fill="${this.renderTheme.cardBg}"
4273
- stroke="${this.renderTheme.border}"
4274
- stroke-width="1"/>
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="system-ui, -apple-system, sans-serif"
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="system-ui, -apple-system, sans-serif"
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="system-ui, -apple-system, sans-serif"
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="system-ui, -apple-system, sans-serif"
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 paginationValue = String(node.props.pagination || "false");
4400
- const pagination = paginationValue === "true";
4401
- const pageCount = Number(node.props.pages || 5);
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
- while (mockTypes.length < columns.length) {
4405
- const inferred = MockDataGenerator.inferMockTypeFromColumn(columns[mockTypes.length] || "item");
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 colWidth = pos.width / columns.length;
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
- columns.forEach((col, colIdx) => {
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="${this.renderTheme.cardBg}"
4425
- stroke="${this.renderTheme.border}"
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="system-ui, -apple-system, sans-serif"
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
- svg += `
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
- columns.forEach((col, i) => {
5210
+ }
5211
+ safeColumns.forEach((col, i) => {
4440
5212
  svg += `
4441
- <text x="${pos.x + i * colWidth + 12}" y="${headerY + 26}"
4442
- font-family="system-ui, -apple-system, sans-serif"
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
- svg += `
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
- columns.forEach((col, colIdx) => {
5231
+ }
5232
+ safeColumns.forEach((col, colIdx) => {
4453
5233
  const cellValue = row[col] || "";
4454
5234
  svg += `
4455
- <text x="${pos.x + colIdx * colWidth + 12}" y="${rowY + 22}"
4456
- font-family="system-ui, -apple-system, sans-serif"
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 = headerY + headerHeight + mockRows.length * rowHeight + 16;
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
- <text x="${startX + buttonWidth / 2}" y="${paginationY + buttonHeight / 2 + 4}"
4483
- font-family="system-ui, -apple-system, sans-serif"
4484
- font-size="14"
4485
- fill="${this.renderTheme.text}"
4486
- text-anchor="middle">&lt;</text>`;
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="system-ui, -apple-system, sans-serif"
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
- <text x="${nextX + buttonWidth / 2}" y="${paginationY + buttonHeight / 2 + 4}"
4514
- font-family="system-ui, -apple-system, sans-serif"
4515
- font-size="14"
4516
- fill="${this.renderTheme.text}"
4517
- text-anchor="middle">&gt;</text>`;
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.content || "Text content");
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="system-ui, -apple-system, sans-serif"
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="system-ui, -apple-system, sans-serif"
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="system-ui, -apple-system, sans-serif"
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="system-ui, -apple-system, sans-serif"
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
- return `<g${this.getDataNodeId(node)}>
4708
- ${label ? `<text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
4709
- font-family="system-ui, -apple-system, sans-serif"
4710
- font-size="12"
4711
- fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>` : ""}
4712
- <rect x="${pos.x}" y="${controlY}"
4713
- width="${pos.width}" height="${controlHeight}"
4714
- rx="6"
4715
- fill="${this.renderTheme.cardBg}"
4716
- stroke="${this.renderTheme.border}"
4717
- stroke-width="1"/>
4718
- <text x="${pos.x + 12}" y="${centerY}"
4719
- font-family="system-ui, -apple-system, sans-serif"
4720
- font-size="14"
4721
- fill="${this.renderTheme.textMuted}">${this.escapeXml(placeholder)}</text>
4722
- <text x="${pos.x + pos.width - 20}" y="${centerY}"
4723
- font-family="system-ui, -apple-system, sans-serif"
4724
- font-size="16"
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="system-ui, -apple-system, sans-serif"
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="system-ui, -apple-system, sans-serif"
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="system-ui, -apple-system, sans-serif"
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="system-ui, -apple-system, sans-serif"
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="system-ui, -apple-system, sans-serif"
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="system-ui, -apple-system, sans-serif"
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="system-ui, -apple-system, sans-serif"
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="system-ui, -apple-system, sans-serif"
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="system-ui, -apple-system, sans-serif"
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="system-ui, -apple-system, sans-serif"
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="system-ui, -apple-system, sans-serif"
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="system-ui, -apple-system, sans-serif"
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="system-ui, -apple-system, sans-serif"
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="system-ui, -apple-system, sans-serif"
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="system-ui, -apple-system, sans-serif"
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="system-ui, -apple-system, sans-serif"
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="system-ui, -apple-system, sans-serif"
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="system-ui, -apple-system, sans-serif"
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="system-ui, -apple-system, sans-serif"
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 badgeSize = Math.max(24, Math.min(iconWidth, iconHeight) * 0.78);
5182
- const badgeX = pos.x + (pos.width - badgeSize) / 2;
5183
- const badgeY = pos.y + (pos.height - badgeSize) / 2;
5184
- const iconSize = badgeSize * 0.62;
5185
- const iconOffsetX = badgeX + (badgeSize - iconSize) / 2;
5186
- const iconOffsetY = badgeY + (badgeSize - iconSize) / 2;
5187
- svg += `
5188
- <!-- Custom Icon Placeholder -->
5189
- <rect x="${badgeX}" y="${badgeY}"
5190
- width="${badgeSize}" height="${badgeSize}"
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="#555" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
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
- } else if (["landscape", "portrait", "square"].includes(placeholder)) {
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="system-ui, -apple-system, sans-serif"
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="system-ui, -apple-system, sans-serif"
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(accentColor, 0.15) : "transparent";
5309
- const textColor = isActive ? this.hexToRgba(accentColor, 0.9) : "rgba(30, 41, 59, 0.75)";
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="rgba(30, 41, 59, 0.6)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
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="system-ui, -apple-system, sans-serif"
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.type || "help-circle");
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="rgba(100, 116, 139, 0.4)" stroke-width="1"/>
5351
- <text x="${pos.x + pos.width / 2}" y="${pos.y + pos.height / 2 + 4}" font-family="system-ui, -apple-system, sans-serif" font-size="12" fill="rgba(100, 116, 139, 0.6)" text-anchor="middle">?</text>
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 bgColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.85) : "rgba(226, 232, 240, 0.9)";
5374
- const iconColor = hasExplicitVariantColor ? "#FFFFFF" : "rgba(30, 41, 59, 0.75)";
5375
- const borderColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.7) : "rgba(100, 116, 139, 0.4)";
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 = this.getIconButtonSize(size);
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="${pos.y}" width="${buttonSize}" height="${buttonSize}" rx="${radius}" fill="${bgColor}" stroke="${borderColor}" stroke-width="1"/>`;
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 + (buttonSize - iconSize) / 2;
5386
- const offsetY = pos.y + (buttonSize - iconSize) / 2;
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 semantic = {
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 paddingY = this.tokens.button.paddingY;
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="${pos.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 = fontSize + paddingY * 2;
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.content || "Text content"),
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 colWidth = pos.width / columns.length;
6062
- let svg = `<g${this.getDataNodeId(node)}>
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="${this.renderTheme.cardBg}"
6067
- stroke="${this.renderTheme.border}"
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
- svg += `<line x1="${pos.x}" y1="${headerY + headerHeight}" x2="${pos.x + pos.width}" y2="${headerY + headerHeight}"
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
- columns.forEach((_, i) => {
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
- svg += `<line x1="${pos.x}" y1="${rowY + rowHeight}" x2="${pos.x + pos.width}" y2="${rowY + rowHeight}"
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
- columns.forEach((_, colIdx) => {
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
- fill="${this.renderTheme.cardBg}"
6115
- stroke="${this.renderTheme.border}"
6116
- stroke-width="0 0 1 0"/>`;
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="${this.renderTheme.border}"/>`;
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="${this.renderTheme.border}"/>`;
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="${this.renderTheme.border}"/>
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 = this.getIconButtonSize(size);
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="${pos.y}"
6231
- width="${buttonSize}" height="${buttonSize}"
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 paddingY = this.tokens.button.paddingY;
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 + paddingX * 2, 60), pos.width);
6404
- const buttonHeight = fontSize + paddingY * 2;
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) : "#2D3748";
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
- return `<g${this.getDataNodeId(node)}>
6414
- <rect x="${pos.x}" y="${pos.y}"
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
- <text x="${pos.x + buttonWidth / 2}" y="${pos.y + buttonHeight / 2 + fontSize * 0.35}"
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="middle">${this.escapeXml(visibleText)}</text>
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) : "#2D3748";
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) : "#2D3748";
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 = this.getIconButtonSize(size);
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="${pos.y}"
6474
- width="${buttonSize}" height="${buttonSize}"
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 + (buttonSize - iconSize) / 2;
6483
- const offsetY = pos.y + (buttonSize - iconSize) / 2;
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
- return `<g${this.getDataNodeId(node)}>
6554
- ${label ? `<text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
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
- ${placeholder ? `<text x="${pos.x + paddingX}" y="${controlY + controlHeight / 2 + 5}"
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(placeholder)}</text>` : ""}
6569
- </g>`;
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="${this.renderTheme.text}">${this.escapeXml(text)}</text>
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="${this.renderTheme.text}">${tspans}</text>
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 accentColor = this.resolveAccentColor();
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 title = String(node.props.title || "");
6762
- const columnsStr = String(node.props.columns || "Col1,Col2,Col3");
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.content || "Text content");
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
- return `<g${this.getDataNodeId(node)}>
6890
- ${label ? `<text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
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
- <text x="${pos.x + 12}" y="${centerY}"
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(placeholder)}</text>
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 badgeSize = Math.max(24, Math.min(pos.width, pos.height) * 0.6);
7311
- const badgeX = pos.x + (pos.width - badgeSize) / 2;
7312
- const badgeY = pos.y + (pos.height - badgeSize) / 2;
7313
- const iconSize = badgeSize * 0.62;
7314
- const iconOffsetX = badgeX + (badgeSize - iconSize) / 2;
7315
- const iconOffsetY = badgeY + (badgeSize - iconSize) / 2;
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="#E8E8E8"
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="#2D3748" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
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="#E8E8E8"
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(accentColor, 0.15) : "transparent";
7410
- const textColor = isActive ? accentColor : "#2D3748";
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="${pos.x + 12}" y="${itemY + itemHeight / 2 + 5}"
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.type || "help-circle");
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="#2D3748" stroke-width="0.5"
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="#2D3748" text-anchor="middle">?</text>
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 = "#2D3748";
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})">