@wire-dsl/engine 0.2.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -94,20 +94,21 @@ var SourceMapBuilder = class {
94
94
  return nodeId;
95
95
  }
96
96
  /**
97
- * Generate semantic node ID based on type and subtype
98
- * Format: {type}-{subtype}-{counter} or {type}-{counter}
99
- *
100
- * Examples:
101
- * - project → "project"
102
- * - theme → "theme"
103
- * - mocks → "mocks"
104
- * - colors → "colors"
105
- * - screen → "screen-0", "screen-1"
106
- * - component Button → "component-button-0", "component-button-1"
107
- * - layout stack → "layout-stack-0", "layout-stack-1"
108
- * - cell → "cell-0", "cell-1"
109
- * - component-definition → "define-MyButton"
110
- */
97
+ * Generate semantic node ID based on type and subtype
98
+ * Format: {type}-{subtype}-{counter} or {type}-{counter}
99
+ *
100
+ * Examples:
101
+ * - project → "project"
102
+ * - theme → "theme"
103
+ * - mocks → "mocks"
104
+ * - colors → "colors"
105
+ * - screen → "screen-0", "screen-1"
106
+ * - component Button → "component-button-0", "component-button-1"
107
+ * - layout stack → "layout-stack-0", "layout-stack-1"
108
+ * - cell → "cell-0", "cell-1"
109
+ * - component-definition → "define-MyButton"
110
+ * - layout-definition → "define-layout-MyShell"
111
+ */
111
112
  generateNodeId(type, metadata) {
112
113
  switch (type) {
113
114
  case "project":
@@ -143,6 +144,8 @@ var SourceMapBuilder = class {
143
144
  }
144
145
  case "component-definition":
145
146
  return `define-${metadata?.name || "unknown"}`;
147
+ case "layout-definition":
148
+ return `define-layout-${metadata?.name || "unknown"}`;
146
149
  default:
147
150
  return `${type}-0`;
148
151
  }
@@ -228,7 +231,7 @@ var SourceMapBuilder = class {
228
231
  }
229
232
  /**
230
233
  * Calculate insertionPoints for all container nodes
231
- * Container nodes: project, screen, layout, cell, component-definition
234
+ * Container nodes: project, screen, layout, cell, component-definition, layout-definition
232
235
  */
233
236
  calculateAllInsertionPoints() {
234
237
  const containerTypes = [
@@ -236,7 +239,8 @@ var SourceMapBuilder = class {
236
239
  "screen",
237
240
  "layout",
238
241
  "cell",
239
- "component-definition"
242
+ "component-definition",
243
+ "layout-definition"
240
244
  ];
241
245
  for (const entry of this.entries) {
242
246
  if (containerTypes.includes(entry.type)) {
@@ -462,19 +466,23 @@ var SourceMapBuilder = class {
462
466
  };
463
467
 
464
468
  // src/parser/index.ts
465
- var Project = (0, import_chevrotain.createToken)({ name: "Project", pattern: /project/ });
466
- var Screen = (0, import_chevrotain.createToken)({ name: "Screen", pattern: /screen/ });
467
- var Layout = (0, import_chevrotain.createToken)({ name: "Layout", pattern: /layout/ });
468
- var Component = (0, import_chevrotain.createToken)({ name: "Component", pattern: /component/ });
469
+ var Project = (0, import_chevrotain.createToken)({ name: "Project", pattern: /project\b/ });
470
+ var Screen = (0, import_chevrotain.createToken)({ name: "Screen", pattern: /screen\b/ });
471
+ var Layout = (0, import_chevrotain.createToken)({ name: "Layout", pattern: /layout\b/ });
472
+ var Component = (0, import_chevrotain.createToken)({ name: "Component", pattern: /component\b/ });
469
473
  var ComponentKeyword = (0, import_chevrotain.createToken)({
470
474
  name: "ComponentKeyword",
471
475
  pattern: /Component\b/
472
476
  });
473
- var Define = (0, import_chevrotain.createToken)({ name: "Define", pattern: /define/ });
474
- var Style = (0, import_chevrotain.createToken)({ name: "Style", pattern: /style/ });
475
- var Mocks = (0, import_chevrotain.createToken)({ name: "Mocks", pattern: /mocks/ });
477
+ var LayoutKeyword = (0, import_chevrotain.createToken)({
478
+ name: "LayoutKeyword",
479
+ pattern: /Layout\b/
480
+ });
481
+ var Define = (0, import_chevrotain.createToken)({ name: "Define", pattern: /define\b/ });
482
+ var Style = (0, import_chevrotain.createToken)({ name: "Style", pattern: /style\b/ });
483
+ var Mocks = (0, import_chevrotain.createToken)({ name: "Mocks", pattern: /mocks\b/ });
476
484
  var Colors = (0, import_chevrotain.createToken)({ name: "Colors", pattern: /colors(?=\s*\{)/ });
477
- var Cell = (0, import_chevrotain.createToken)({ name: "Cell", pattern: /cell/ });
485
+ var Cell = (0, import_chevrotain.createToken)({ name: "Cell", pattern: /cell\b/ });
478
486
  var LCurly = (0, import_chevrotain.createToken)({ name: "LCurly", pattern: /{/ });
479
487
  var RCurly = (0, import_chevrotain.createToken)({ name: "RCurly", pattern: /}/ });
480
488
  var LParen = (0, import_chevrotain.createToken)({ name: "LParen", pattern: /\(/ });
@@ -521,6 +529,7 @@ var allTokens = [
521
529
  Project,
522
530
  Screen,
523
531
  Layout,
532
+ LayoutKeyword,
524
533
  ComponentKeyword,
525
534
  Component,
526
535
  Define,
@@ -553,6 +562,7 @@ var WireDSLParser = class extends import_chevrotain.CstParser {
553
562
  this.MANY(() => {
554
563
  this.OR([
555
564
  { ALT: () => this.SUBRULE(this.definedComponent) },
565
+ { ALT: () => this.SUBRULE(this.definedLayout) },
556
566
  { ALT: () => this.SUBRULE(this.styleDecl) },
557
567
  { ALT: () => this.SUBRULE(this.mocksDecl) },
558
568
  { ALT: () => this.SUBRULE(this.colorsDecl) },
@@ -621,6 +631,15 @@ var WireDSLParser = class extends import_chevrotain.CstParser {
621
631
  ]);
622
632
  this.CONSUME(RCurly);
623
633
  });
634
+ // define Layout "ScreenShell" { layout split { ... } }
635
+ this.definedLayout = this.RULE("definedLayout", () => {
636
+ this.CONSUME(Define);
637
+ this.CONSUME(LayoutKeyword, { LABEL: "layoutKeyword" });
638
+ this.CONSUME(StringLiteral, { LABEL: "layoutName" });
639
+ this.CONSUME(LCurly);
640
+ this.SUBRULE(this.layout);
641
+ this.CONSUME(RCurly);
642
+ });
624
643
  // screen Main(background: white) { ... }
625
644
  this.screen = this.RULE("screen", () => {
626
645
  this.CONSUME(Screen);
@@ -709,6 +728,7 @@ var WireDSLVisitor = class extends BaseCstVisitor {
709
728
  const mocks = {};
710
729
  const colors = {};
711
730
  const definedComponents = [];
731
+ const definedLayouts = [];
712
732
  const screens = [];
713
733
  if (ctx.styleDecl && ctx.styleDecl.length > 0) {
714
734
  const styleBlock = this.visit(ctx.styleDecl[0]);
@@ -727,6 +747,11 @@ var WireDSLVisitor = class extends BaseCstVisitor {
727
747
  definedComponents.push(this.visit(comp));
728
748
  });
729
749
  }
750
+ if (ctx.definedLayout) {
751
+ ctx.definedLayout.forEach((layoutDef) => {
752
+ definedLayouts.push(this.visit(layoutDef));
753
+ });
754
+ }
730
755
  if (ctx.screen) {
731
756
  ctx.screen.forEach((screen) => {
732
757
  screens.push(this.visit(screen));
@@ -739,6 +764,7 @@ var WireDSLVisitor = class extends BaseCstVisitor {
739
764
  mocks,
740
765
  colors,
741
766
  definedComponents,
767
+ definedLayouts,
742
768
  screens
743
769
  };
744
770
  }
@@ -803,6 +829,15 @@ var WireDSLVisitor = class extends BaseCstVisitor {
803
829
  body
804
830
  };
805
831
  }
832
+ definedLayout(ctx) {
833
+ const name = ctx.layoutName[0].image.slice(1, -1);
834
+ const body = this.visit(ctx.layout[0]);
835
+ return {
836
+ type: "definedLayout",
837
+ name,
838
+ body
839
+ };
840
+ }
806
841
  screen(ctx) {
807
842
  const params = ctx.paramList ? this.visit(ctx.paramList[0]) : {};
808
843
  return {
@@ -933,6 +968,7 @@ var WireDSLVisitorWithSourceMap = class extends WireDSLVisitor {
933
968
  constructor(sourceMapBuilder) {
934
969
  super();
935
970
  this.definedComponentNames = /* @__PURE__ */ new Set();
971
+ this.definedLayoutNames = /* @__PURE__ */ new Set();
936
972
  this.sourceMapBuilder = sourceMapBuilder;
937
973
  }
938
974
  project(ctx) {
@@ -941,6 +977,7 @@ var WireDSLVisitorWithSourceMap = class extends WireDSLVisitor {
941
977
  const mocks = {};
942
978
  const colors = {};
943
979
  const definedComponents = [];
980
+ const definedLayouts = [];
944
981
  const screens = [];
945
982
  const tokens = {
946
983
  keyword: ctx.Project[0],
@@ -955,6 +992,8 @@ var WireDSLVisitorWithSourceMap = class extends WireDSLVisitor {
955
992
  colors: {},
956
993
  definedComponents: [],
957
994
  // Will be filled after push
995
+ definedLayouts: [],
996
+ // Will be filled after push
958
997
  screens: []
959
998
  // Will be filled after push
960
999
  };
@@ -984,6 +1023,11 @@ var WireDSLVisitorWithSourceMap = class extends WireDSLVisitor {
984
1023
  ast.definedComponents.push(this.visit(comp));
985
1024
  });
986
1025
  }
1026
+ if (ctx.definedLayout) {
1027
+ ctx.definedLayout.forEach((layoutDef) => {
1028
+ ast.definedLayouts.push(this.visit(layoutDef));
1029
+ });
1030
+ }
987
1031
  if (ctx.screen) {
988
1032
  ctx.screen.forEach((screen) => {
989
1033
  ast.screens.push(this.visit(screen));
@@ -1228,6 +1272,35 @@ var WireDSLVisitorWithSourceMap = class extends WireDSLVisitor {
1228
1272
  }
1229
1273
  return ast;
1230
1274
  }
1275
+ definedLayout(ctx) {
1276
+ const name = ctx.layoutName[0].image.slice(1, -1);
1277
+ this.definedLayoutNames.add(name);
1278
+ const tokens = {
1279
+ keyword: ctx.Define[0],
1280
+ name: ctx.layoutName[0],
1281
+ body: ctx.RCurly[0]
1282
+ };
1283
+ const ast = {
1284
+ type: "definedLayout",
1285
+ name,
1286
+ body: {}
1287
+ // Will be filled after push
1288
+ };
1289
+ if (this.sourceMapBuilder) {
1290
+ const nodeId = this.sourceMapBuilder.addNode(
1291
+ "layout-definition",
1292
+ tokens,
1293
+ { name }
1294
+ );
1295
+ ast._meta = { nodeId };
1296
+ this.sourceMapBuilder.pushParent(nodeId);
1297
+ }
1298
+ ast.body = this.visit(ctx.layout[0]);
1299
+ if (this.sourceMapBuilder) {
1300
+ this.sourceMapBuilder.popParent();
1301
+ }
1302
+ return ast;
1303
+ }
1231
1304
  // Override styleDecl to capture style block in SourceMap
1232
1305
  styleDecl(ctx) {
1233
1306
  const style = {};
@@ -1407,6 +1480,13 @@ function buildLayoutRulesFromMetadata() {
1407
1480
  var BUILT_IN_COMPONENTS = new Set(Object.keys(import_components.COMPONENTS));
1408
1481
  var COMPONENT_RULES = buildComponentRulesFromMetadata();
1409
1482
  var LAYOUT_RULES = buildLayoutRulesFromMetadata();
1483
+ var BUILT_IN_LAYOUTS = new Set(Object.keys(import_components.LAYOUTS));
1484
+ function isPascalCaseIdentifier(name) {
1485
+ return /^[A-Z][A-Za-z0-9]*$/.test(name);
1486
+ }
1487
+ function isValidDefinedLayoutName(name) {
1488
+ return /^[a-z][a-z0-9_]*$/.test(name);
1489
+ }
1410
1490
  function toFallbackRange() {
1411
1491
  return {
1412
1492
  start: { line: 1, column: 0 },
@@ -1498,6 +1578,17 @@ function isBooleanLike(value) {
1498
1578
  const normalized = String(value).trim().toLowerCase();
1499
1579
  return normalized === "true" || normalized === "false";
1500
1580
  }
1581
+ function parseBooleanLike(value, fallback = false) {
1582
+ if (typeof value === "number") {
1583
+ if (value === 1) return true;
1584
+ if (value === 0) return false;
1585
+ return fallback;
1586
+ }
1587
+ const normalized = String(value).trim().toLowerCase();
1588
+ if (normalized === "true") return true;
1589
+ if (normalized === "false") return false;
1590
+ return fallback;
1591
+ }
1501
1592
  function getPropertyRange(entry, propertyName, mode = "full") {
1502
1593
  const prop = entry?.properties?.[propertyName];
1503
1594
  if (!prop) return entry?.range || toFallbackRange();
@@ -1515,21 +1606,55 @@ function validateSemanticDiagnostics(ast, sourceMap) {
1515
1606
  const diagnostics = [];
1516
1607
  const sourceMapByNodeId = new Map(sourceMap.map((entry) => [entry.nodeId, entry]));
1517
1608
  const definedComponents = new Set(ast.definedComponents.map((dc) => dc.name));
1518
- const emitWarning = (message, code, range, nodeId, suggestion) => {
1609
+ const definedLayouts = new Set(ast.definedLayouts.map((dl) => dl.name));
1610
+ const emitDiagnostic = (severity, message, code, range, nodeId, suggestion) => {
1519
1611
  diagnostics.push({
1520
1612
  message,
1521
1613
  code,
1522
- severity: "warning",
1614
+ severity,
1523
1615
  phase: "semantic",
1524
1616
  range,
1525
1617
  nodeId,
1526
1618
  suggestion
1527
1619
  });
1528
1620
  };
1529
- const checkComponent = (component) => {
1621
+ const emitWarning = (message, code, range, nodeId, suggestion) => emitDiagnostic("warning", message, code, range, nodeId, suggestion);
1622
+ const emitError = (message, code, range, nodeId, suggestion) => emitDiagnostic("error", message, code, range, nodeId, suggestion);
1623
+ const countChildrenSlots = (layout) => {
1624
+ let count = 0;
1625
+ for (const child of layout.children) {
1626
+ if (child.type === "component") {
1627
+ if (child.componentType === "Children") count += 1;
1628
+ } else if (child.type === "layout") {
1629
+ count += countChildrenSlots(child);
1630
+ } else if (child.type === "cell") {
1631
+ for (const cellChild of child.children) {
1632
+ if (cellChild.type === "component") {
1633
+ if (cellChild.componentType === "Children") count += 1;
1634
+ } else if (cellChild.type === "layout") {
1635
+ count += countChildrenSlots(cellChild);
1636
+ }
1637
+ }
1638
+ }
1639
+ }
1640
+ return count;
1641
+ };
1642
+ const checkComponent = (component, insideDefinedLayout) => {
1530
1643
  const nodeId = component._meta?.nodeId;
1531
1644
  const entry = nodeId ? sourceMapByNodeId.get(nodeId) : void 0;
1532
1645
  const componentType = component.componentType;
1646
+ if (componentType === "Children") {
1647
+ if (!insideDefinedLayout) {
1648
+ emitError(
1649
+ 'Component "Children" can only be used inside a define Layout body.',
1650
+ "CHILDREN_SLOT_OUTSIDE_LAYOUT_DEFINITION",
1651
+ entry?.nameRange || entry?.range || toFallbackRange(),
1652
+ nodeId,
1653
+ "Move this placeholder into a define Layout block."
1654
+ );
1655
+ }
1656
+ return;
1657
+ }
1533
1658
  if (!BUILT_IN_COMPONENTS.has(componentType) && !definedComponents.has(componentType)) {
1534
1659
  emitWarning(
1535
1660
  `Component "${componentType}" is not a built-in component and has no local definition.`,
@@ -1570,7 +1695,9 @@ function validateSemanticDiagnostics(ast, sourceMap) {
1570
1695
  const enumValues = rules.enumProps?.[propName];
1571
1696
  if (enumValues) {
1572
1697
  const normalizedValue = String(propValue);
1573
- if (!enumValues.includes(normalizedValue)) {
1698
+ const isCustomVariantFromColors = propName === "variant" && !enumValues.includes(normalizedValue) && Object.prototype.hasOwnProperty.call(ast.colors || {}, normalizedValue);
1699
+ const isPropReference = normalizedValue.startsWith("prop_");
1700
+ if (!enumValues.includes(normalizedValue) && !isCustomVariantFromColors && !isPropReference) {
1574
1701
  emitWarning(
1575
1702
  `Invalid value "${normalizedValue}" for property "${propName}" in component "${componentType}".`,
1576
1703
  "COMPONENT_INVALID_PROPERTY_VALUE",
@@ -1590,11 +1717,40 @@ function validateSemanticDiagnostics(ast, sourceMap) {
1590
1717
  );
1591
1718
  }
1592
1719
  }
1720
+ if (componentType === "Table") {
1721
+ const hasCaption = String(component.props.caption || "").trim().length > 0;
1722
+ const hasPagination = parseBooleanLike(component.props.pagination ?? "false", false);
1723
+ if (hasCaption && hasPagination) {
1724
+ const rawPaginationAlign = String(component.props.paginationAlign || "right");
1725
+ const paginationAlign = rawPaginationAlign === "left" || rawPaginationAlign === "center" || rawPaginationAlign === "right" ? rawPaginationAlign : "right";
1726
+ const rawCaptionAlign = String(component.props.captionAlign || "");
1727
+ const captionAlign = rawCaptionAlign === "left" || rawCaptionAlign === "center" || rawCaptionAlign === "right" ? rawCaptionAlign : paginationAlign === "left" ? "right" : "left";
1728
+ if (captionAlign === paginationAlign) {
1729
+ emitWarning(
1730
+ `Table footer collision: "captionAlign" and "paginationAlign" both resolve to "${captionAlign}".`,
1731
+ "TABLE_FOOTER_ALIGNMENT_COLLISION",
1732
+ entry?.range || toFallbackRange(),
1733
+ nodeId,
1734
+ "Use different alignments to avoid visual overlap."
1735
+ );
1736
+ }
1737
+ }
1738
+ }
1593
1739
  };
1594
- const checkLayout = (layout) => {
1740
+ const checkLayout = (layout, insideDefinedLayout) => {
1595
1741
  const nodeId = layout._meta?.nodeId;
1596
1742
  const entry = nodeId ? sourceMapByNodeId.get(nodeId) : void 0;
1597
1743
  const rules = LAYOUT_RULES[layout.layoutType];
1744
+ const isDefinedLayoutUsage = definedLayouts.has(layout.layoutType);
1745
+ if (isDefinedLayoutUsage && layout.children.length !== 1) {
1746
+ emitError(
1747
+ `Layout "${layout.layoutType}" expects exactly one child for its Children slot.`,
1748
+ "LAYOUT_DEFINITION_CHILDREN_ARITY",
1749
+ entry?.bodyRange || entry?.range || toFallbackRange(),
1750
+ nodeId,
1751
+ "Provide exactly one nested child block when using this layout."
1752
+ );
1753
+ }
1598
1754
  if (layout.children.length === 0) {
1599
1755
  emitWarning(
1600
1756
  `Layout "${layout.layoutType}" is empty.`,
@@ -1604,7 +1760,7 @@ function validateSemanticDiagnostics(ast, sourceMap) {
1604
1760
  "Add at least one child: component, layout, or cell."
1605
1761
  );
1606
1762
  }
1607
- if (!rules) {
1763
+ if (!rules && !isDefinedLayoutUsage) {
1608
1764
  emitWarning(
1609
1765
  `Layout type "${layout.layoutType}" is not recognized by semantic validation rules.`,
1610
1766
  "LAYOUT_UNKNOWN_TYPE",
@@ -1612,7 +1768,7 @@ function validateSemanticDiagnostics(ast, sourceMap) {
1612
1768
  nodeId,
1613
1769
  `Use one of: ${Object.keys(LAYOUT_RULES).join(", ")}.`
1614
1770
  );
1615
- } else {
1771
+ } else if (rules) {
1616
1772
  const missingRequiredParams = getMissingRequiredNames(rules.requiredParams, layout.params);
1617
1773
  if (missingRequiredParams.length > 0) {
1618
1774
  emitWarning(
@@ -1625,6 +1781,16 @@ function validateSemanticDiagnostics(ast, sourceMap) {
1625
1781
  }
1626
1782
  const allowed = new Set(rules.allowedParams);
1627
1783
  for (const [paramName, paramValue] of Object.entries(layout.params)) {
1784
+ if (layout.layoutType === "split" && paramName === "sidebar") {
1785
+ emitError(
1786
+ 'Split parameter "sidebar" was removed. Use "left" or "right" instead.',
1787
+ "LAYOUT_SPLIT_SIDEBAR_DEPRECATED",
1788
+ getPropertyRange(entry, paramName, "name"),
1789
+ nodeId,
1790
+ "Example: layout split(left: 260) { ... }"
1791
+ );
1792
+ continue;
1793
+ }
1628
1794
  if (!allowed.has(paramName)) {
1629
1795
  emitWarning(
1630
1796
  `Parameter "${paramName}" is not recognized for layout "${layout.layoutType}".`,
@@ -1641,7 +1807,7 @@ function validateSemanticDiagnostics(ast, sourceMap) {
1641
1807
  const enumValues = rules.enumParams?.[paramName];
1642
1808
  if (enumValues) {
1643
1809
  const normalizedValue = String(paramValue);
1644
- if (!enumValues.includes(normalizedValue)) {
1810
+ if (!enumValues.includes(normalizedValue) && !normalizedValue.startsWith("prop_")) {
1645
1811
  emitWarning(
1646
1812
  `Invalid value "${normalizedValue}" for parameter "${paramName}" in layout "${layout.layoutType}".`,
1647
1813
  "LAYOUT_INVALID_PARAMETER_VALUE",
@@ -1663,31 +1829,62 @@ function validateSemanticDiagnostics(ast, sourceMap) {
1663
1829
  );
1664
1830
  }
1665
1831
  }
1666
- if (layout.layoutType === "split" && paramName === "sidebar") {
1667
- const sidebar = Number(paramValue);
1668
- if (!Number.isFinite(sidebar) || sidebar <= 0) {
1832
+ if (layout.layoutType === "split" && (paramName === "left" || paramName === "right")) {
1833
+ const splitSize = Number(paramValue);
1834
+ if (!Number.isFinite(splitSize) || splitSize <= 0) {
1669
1835
  emitWarning(
1670
- 'Split "sidebar" must be a positive number.',
1671
- "LAYOUT_SPLIT_SIDEBAR_INVALID",
1836
+ `Split "${paramName}" must be a positive number. Falling back to 250.`,
1837
+ "LAYOUT_SPLIT_WIDTH_INVALID",
1672
1838
  getPropertyRange(entry, paramName, "value"),
1673
1839
  nodeId,
1674
- "Use a value like sidebar: 240."
1840
+ `Use a value like ${paramName}: 260.`
1675
1841
  );
1676
1842
  }
1677
1843
  }
1678
1844
  }
1845
+ if (layout.layoutType === "split") {
1846
+ const hasLeft = layout.params.left !== void 0;
1847
+ const hasRight = layout.params.right !== void 0;
1848
+ if (!hasLeft && !hasRight) {
1849
+ emitError(
1850
+ 'Split layout requires exactly one fixed side width: "left" or "right".',
1851
+ "LAYOUT_SPLIT_SIDE_REQUIRED",
1852
+ entry?.nameRange || entry?.range || toFallbackRange(),
1853
+ nodeId,
1854
+ "Add either left: <number> or right: <number>."
1855
+ );
1856
+ }
1857
+ if (hasLeft && hasRight) {
1858
+ emitError(
1859
+ 'Split layout accepts only one fixed side width: use either "left" or "right", not both.',
1860
+ "LAYOUT_SPLIT_SIDE_CONFLICT",
1861
+ entry?.nameRange || entry?.range || toFallbackRange(),
1862
+ nodeId,
1863
+ "Remove one of the two parameters."
1864
+ );
1865
+ }
1866
+ if (layout.children.length !== 2) {
1867
+ emitError(
1868
+ `Split layout requires exactly 2 children, received ${layout.children.length}.`,
1869
+ "LAYOUT_SPLIT_CHILDREN_ARITY",
1870
+ entry?.bodyRange || entry?.range || toFallbackRange(),
1871
+ nodeId,
1872
+ "Provide exactly two child blocks (left/right content)."
1873
+ );
1874
+ }
1875
+ }
1679
1876
  }
1680
1877
  for (const child of layout.children) {
1681
1878
  if (child.type === "component") {
1682
- checkComponent(child);
1879
+ checkComponent(child, insideDefinedLayout);
1683
1880
  } else if (child.type === "layout") {
1684
- checkLayout(child);
1881
+ checkLayout(child, insideDefinedLayout);
1685
1882
  } else if (child.type === "cell") {
1686
- checkCell(child);
1883
+ checkCell(child, insideDefinedLayout);
1687
1884
  }
1688
1885
  }
1689
1886
  };
1690
- const checkCell = (cell) => {
1887
+ const checkCell = (cell, insideDefinedLayout) => {
1691
1888
  const nodeId = cell._meta?.nodeId;
1692
1889
  const entry = nodeId ? sourceMapByNodeId.get(nodeId) : void 0;
1693
1890
  if (cell.props.span !== void 0) {
@@ -1703,12 +1900,54 @@ function validateSemanticDiagnostics(ast, sourceMap) {
1703
1900
  }
1704
1901
  }
1705
1902
  for (const child of cell.children) {
1706
- if (child.type === "component") checkComponent(child);
1707
- if (child.type === "layout") checkLayout(child);
1903
+ if (child.type === "component") checkComponent(child, insideDefinedLayout);
1904
+ if (child.type === "layout") checkLayout(child, insideDefinedLayout);
1708
1905
  }
1709
1906
  };
1907
+ for (const componentDef of ast.definedComponents) {
1908
+ const nodeId = componentDef._meta?.nodeId;
1909
+ const entry = nodeId ? sourceMapByNodeId.get(nodeId) : void 0;
1910
+ if (!isPascalCaseIdentifier(componentDef.name)) {
1911
+ emitWarning(
1912
+ `Defined component "${componentDef.name}" should use PascalCase naming.`,
1913
+ "COMPONENT_DEFINITION_NAME_STYLE",
1914
+ entry?.nameRange || entry?.range || toFallbackRange(),
1915
+ nodeId,
1916
+ 'Use a name like "MyComponent".'
1917
+ );
1918
+ }
1919
+ if (componentDef.body.type === "component") {
1920
+ checkComponent(componentDef.body, false);
1921
+ } else {
1922
+ checkLayout(componentDef.body, false);
1923
+ }
1924
+ }
1925
+ for (const layoutDef of ast.definedLayouts) {
1926
+ const nodeId = layoutDef._meta?.nodeId;
1927
+ const entry = nodeId ? sourceMapByNodeId.get(nodeId) : void 0;
1928
+ if (!isValidDefinedLayoutName(layoutDef.name)) {
1929
+ emitError(
1930
+ `Defined layout "${layoutDef.name}" must match /^[a-z][a-z0-9_]*$/.`,
1931
+ "LAYOUT_DEFINITION_INVALID_NAME",
1932
+ entry?.nameRange || entry?.range || toFallbackRange(),
1933
+ nodeId,
1934
+ 'Use names like "screen_default" or "appShell".'
1935
+ );
1936
+ }
1937
+ const childrenSlotCount = countChildrenSlots(layoutDef.body);
1938
+ if (childrenSlotCount !== 1) {
1939
+ emitError(
1940
+ `Defined layout "${layoutDef.name}" must contain exactly one "component Children" placeholder.`,
1941
+ "LAYOUT_DEFINITION_CHILDREN_SLOT_COUNT",
1942
+ entry?.bodyRange || entry?.range || toFallbackRange(),
1943
+ nodeId,
1944
+ 'Add exactly one "component Children" in the layout body.'
1945
+ );
1946
+ }
1947
+ checkLayout(layoutDef.body, true);
1948
+ }
1710
1949
  ast.screens.forEach((screen) => {
1711
- checkLayout(screen.layout);
1950
+ checkLayout(screen.layout, false);
1712
1951
  });
1713
1952
  return diagnostics;
1714
1953
  }
@@ -1725,7 +1964,7 @@ ${lexResult.errors.map((e) => e.message).join("\n")}`);
1725
1964
  ${parserInstance.errors.map((e) => e.message).join("\n")}`);
1726
1965
  }
1727
1966
  const ast = visitor.visit(cst);
1728
- validateComponentDefinitionCycles(ast);
1967
+ validateDefinitionCycles(ast);
1729
1968
  return ast;
1730
1969
  }
1731
1970
  function parseWireDSLWithSourceMap(input, filePath = "<input>", options) {
@@ -1756,7 +1995,7 @@ ${parserInstance.errors.map((e) => e.message).join("\n")}`);
1756
1995
  const ast = visitorWithSourceMap.visit(cst);
1757
1996
  const sourceMap = sourceMapBuilder.build();
1758
1997
  try {
1759
- validateComponentDefinitionCycles(ast);
1998
+ validateDefinitionCycles(ast);
1760
1999
  } catch (error) {
1761
2000
  const projectEntry = sourceMap.find((entry) => entry.type === "project");
1762
2001
  diagnostics.push({
@@ -1779,83 +2018,101 @@ ${parserInstance.errors.map((e) => e.message).join("\n")}`);
1779
2018
  }
1780
2019
  return buildParseResult(ast, sourceMap, diagnostics);
1781
2020
  }
1782
- function validateComponentDefinitionCycles(ast) {
1783
- if (!ast.definedComponents || ast.definedComponents.length === 0) {
2021
+ function validateDefinitionCycles(ast) {
2022
+ if ((!ast.definedComponents || ast.definedComponents.length === 0) && (!ast.definedLayouts || ast.definedLayouts.length === 0)) {
1784
2023
  return;
1785
2024
  }
1786
2025
  const components = /* @__PURE__ */ new Map();
2026
+ const layouts = /* @__PURE__ */ new Map();
1787
2027
  ast.definedComponents.forEach((comp) => {
1788
2028
  components.set(comp.name, comp);
1789
2029
  });
2030
+ ast.definedLayouts.forEach((layoutDef) => {
2031
+ layouts.set(layoutDef.name, layoutDef);
2032
+ });
2033
+ const makeComponentKey = (name) => `component:${name}`;
2034
+ const makeLayoutKey = (name) => `layout:${name}`;
2035
+ const displayKey = (key) => key.split(":")[1];
2036
+ const shouldTrackComponentDependency = (name) => components.has(name) && !BUILT_IN_COMPONENTS.has(name);
2037
+ const shouldTrackLayoutDependency = (name) => layouts.has(name) && !BUILT_IN_LAYOUTS.has(name);
1790
2038
  const visited = /* @__PURE__ */ new Set();
1791
2039
  const recursionStack = /* @__PURE__ */ new Set();
1792
- function getComponentDependencies(node) {
1793
- const deps = /* @__PURE__ */ new Set();
1794
- if (node.type === "layout") {
1795
- const layout = node;
1796
- if (layout.children) {
1797
- layout.children.forEach((child) => {
1798
- if (child.type === "component") {
1799
- const component = child;
1800
- deps.add(component.componentType);
1801
- } else if (child.type === "layout") {
1802
- const nested = getComponentDependencies(child);
1803
- nested.forEach((d) => deps.add(d));
1804
- } else if (child.type === "cell") {
1805
- const cell = child;
1806
- if (cell.children) {
1807
- cell.children.forEach((cellChild) => {
1808
- if (cellChild.type === "component") {
1809
- deps.add(cellChild.componentType);
1810
- } else if (cellChild.type === "layout") {
1811
- const nested = getComponentDependencies(cellChild);
1812
- nested.forEach((d) => deps.add(d));
1813
- }
1814
- });
2040
+ const collectLayoutDependencies = (layout, deps) => {
2041
+ if (shouldTrackLayoutDependency(layout.layoutType)) {
2042
+ deps.add(makeLayoutKey(layout.layoutType));
2043
+ }
2044
+ for (const child of layout.children) {
2045
+ if (child.type === "component") {
2046
+ if (shouldTrackComponentDependency(child.componentType)) {
2047
+ deps.add(makeComponentKey(child.componentType));
2048
+ }
2049
+ } else if (child.type === "layout") {
2050
+ collectLayoutDependencies(child, deps);
2051
+ } else if (child.type === "cell") {
2052
+ for (const cellChild of child.children) {
2053
+ if (cellChild.type === "component") {
2054
+ if (shouldTrackComponentDependency(cellChild.componentType)) {
2055
+ deps.add(makeComponentKey(cellChild.componentType));
1815
2056
  }
2057
+ } else if (cellChild.type === "layout") {
2058
+ collectLayoutDependencies(cellChild, deps);
1816
2059
  }
1817
- });
2060
+ }
1818
2061
  }
1819
2062
  }
1820
- return deps;
1821
- }
1822
- function hasCycle(componentName, path = []) {
1823
- if (recursionStack.has(componentName)) {
1824
- const cycleStart = path.indexOf(componentName);
1825
- const cycle = path.slice(cycleStart).concat(componentName);
1826
- return cycle;
2063
+ };
2064
+ const getDependencies = (key) => {
2065
+ const deps = /* @__PURE__ */ new Set();
2066
+ if (key.startsWith("component:")) {
2067
+ const name2 = key.slice("component:".length);
2068
+ const def2 = components.get(name2);
2069
+ if (!def2) return deps;
2070
+ if (def2.body.type === "component") {
2071
+ if (shouldTrackComponentDependency(def2.body.componentType)) {
2072
+ deps.add(makeComponentKey(def2.body.componentType));
2073
+ }
2074
+ } else {
2075
+ collectLayoutDependencies(def2.body, deps);
2076
+ }
2077
+ return deps;
1827
2078
  }
1828
- if (visited.has(componentName)) {
1829
- return null;
2079
+ const name = key.slice("layout:".length);
2080
+ const def = layouts.get(name);
2081
+ if (!def) return deps;
2082
+ collectLayoutDependencies(def.body, deps);
2083
+ return deps;
2084
+ };
2085
+ const findCycle = (key, path = []) => {
2086
+ if (recursionStack.has(key)) {
2087
+ const cycleStart = path.indexOf(key);
2088
+ return path.slice(cycleStart).concat(key);
1830
2089
  }
1831
- const component = components.get(componentName);
1832
- if (!component) {
2090
+ if (visited.has(key)) {
1833
2091
  return null;
1834
2092
  }
1835
- recursionStack.add(componentName);
1836
- const currentPath = [...path, componentName];
1837
- const dependencies = getComponentDependencies(component.body);
2093
+ recursionStack.add(key);
2094
+ const currentPath = [...path, key];
2095
+ const dependencies = getDependencies(key);
1838
2096
  for (const dep of dependencies) {
1839
- const definedDep = components.has(dep);
1840
- if (definedDep) {
1841
- const cycle = hasCycle(dep, currentPath);
1842
- if (cycle) {
1843
- return cycle;
1844
- }
1845
- }
2097
+ const cycle = findCycle(dep, currentPath);
2098
+ if (cycle) return cycle;
1846
2099
  }
1847
- recursionStack.delete(componentName);
1848
- visited.add(componentName);
2100
+ recursionStack.delete(key);
2101
+ visited.add(key);
1849
2102
  return null;
1850
- }
1851
- for (const [componentName] of components) {
2103
+ };
2104
+ const allDefinitions = [
2105
+ ...Array.from(components.keys()).map(makeComponentKey),
2106
+ ...Array.from(layouts.keys()).map(makeLayoutKey)
2107
+ ];
2108
+ for (const key of allDefinitions) {
1852
2109
  visited.clear();
1853
2110
  recursionStack.clear();
1854
- const cycle = hasCycle(componentName);
2111
+ const cycle = findCycle(key);
1855
2112
  if (cycle) {
1856
2113
  throw new Error(
1857
- `Circular component definition detected: ${cycle.join(" \u2192 ")}
1858
- Components cannot reference each other in a cycle.`
2114
+ `Circular component definition detected: ${cycle.map(displayKey).join(" \u2192 ")}
2115
+ Components and layouts cannot reference each other in a cycle.`
1859
2116
  );
1860
2117
  }
1861
2118
  }
@@ -1863,6 +2120,7 @@ Components cannot reference each other in a cycle.`
1863
2120
 
1864
2121
  // src/ir/index.ts
1865
2122
  var import_zod = require("zod");
2123
+ var import_components2 = require("@wire-dsl/language-support/components");
1866
2124
 
1867
2125
  // src/ir/device-presets.ts
1868
2126
  var DEVICE_PRESETS = {
@@ -2027,9 +2285,11 @@ var IRGenerator = class {
2027
2285
  this.idGen = new IDGenerator();
2028
2286
  this.nodes = {};
2029
2287
  this.definedComponents = /* @__PURE__ */ new Map();
2288
+ this.definedLayouts = /* @__PURE__ */ new Map();
2030
2289
  this.definedComponentIndices = /* @__PURE__ */ new Map();
2031
2290
  this.undefinedComponentsUsed = /* @__PURE__ */ new Set();
2032
2291
  this.warnings = [];
2292
+ this.errors = [];
2033
2293
  this.style = {
2034
2294
  density: "normal",
2035
2295
  spacing: "md",
@@ -2042,15 +2302,22 @@ var IRGenerator = class {
2042
2302
  this.idGen.reset();
2043
2303
  this.nodes = {};
2044
2304
  this.definedComponents.clear();
2305
+ this.definedLayouts.clear();
2045
2306
  this.definedComponentIndices.clear();
2046
2307
  this.undefinedComponentsUsed.clear();
2047
2308
  this.warnings = [];
2309
+ this.errors = [];
2048
2310
  if (ast.definedComponents && ast.definedComponents.length > 0) {
2049
2311
  ast.definedComponents.forEach((def, index) => {
2050
2312
  this.definedComponents.set(def.name, def);
2051
2313
  this.definedComponentIndices.set(def.name, index);
2052
2314
  });
2053
2315
  }
2316
+ if (ast.definedLayouts && ast.definedLayouts.length > 0) {
2317
+ ast.definedLayouts.forEach((def) => {
2318
+ this.definedLayouts.set(def.name, def);
2319
+ });
2320
+ }
2054
2321
  this.applyStyle(ast.style);
2055
2322
  const screens = ast.screens.map(
2056
2323
  (screen, screenIndex) => this.convertScreen(screen, screenIndex)
@@ -2062,6 +2329,11 @@ var IRGenerator = class {
2062
2329
  Define these components with: define Component "Name" { ... }`
2063
2330
  );
2064
2331
  }
2332
+ if (this.errors.length > 0) {
2333
+ const messages = this.errors.map((e) => `- [${e.type}] ${e.message}`).join("\n");
2334
+ throw new Error(`IR generation failed with semantic errors:
2335
+ ${messages}`);
2336
+ }
2065
2337
  const project = {
2066
2338
  id: this.sanitizeId(ast.name),
2067
2339
  name: ast.name,
@@ -2188,41 +2460,50 @@ Define these components with: define Component "Name" { ... }`
2188
2460
  getWarnings() {
2189
2461
  return this.warnings;
2190
2462
  }
2191
- convertLayout(layout) {
2463
+ convertLayout(layout, context) {
2464
+ let layoutParams = this.resolveLayoutParams(layout.layoutType, layout.params, context);
2465
+ if (layout.layoutType === "split") {
2466
+ layoutParams = this.normalizeSplitParams(layoutParams);
2467
+ }
2468
+ const layoutChildren = layout.children;
2469
+ const layoutDefinition = this.definedLayouts.get(layout.layoutType);
2470
+ if (layoutDefinition) {
2471
+ return this.expandDefinedLayout(layoutDefinition, layoutParams, layoutChildren, context);
2472
+ }
2192
2473
  const nodeId = this.idGen.generate("node");
2193
2474
  const childRefs = [];
2194
- for (const child of layout.children) {
2475
+ for (const child of layoutChildren) {
2195
2476
  if (child.type === "layout") {
2196
- const childId = this.convertLayout(child);
2197
- childRefs.push({ ref: childId });
2477
+ const childId = this.convertLayout(child, context);
2478
+ if (childId) childRefs.push({ ref: childId });
2198
2479
  } else if (child.type === "component") {
2199
- const childId = this.convertComponent(child);
2200
- childRefs.push({ ref: childId });
2480
+ const childId = this.convertComponent(child, context);
2481
+ if (childId) childRefs.push({ ref: childId });
2201
2482
  } else if (child.type === "cell") {
2202
- const childId = this.convertCell(child);
2203
- childRefs.push({ ref: childId });
2483
+ const childId = this.convertCell(child, context);
2484
+ if (childId) childRefs.push({ ref: childId });
2204
2485
  }
2205
2486
  }
2206
2487
  const style = {};
2207
- if (layout.params.padding) {
2208
- style.padding = String(layout.params.padding);
2488
+ if (layoutParams.padding !== void 0) {
2489
+ style.padding = String(layoutParams.padding);
2209
2490
  } else {
2210
2491
  style.padding = "none";
2211
2492
  }
2212
- if (layout.params.gap) {
2213
- style.gap = String(layout.params.gap);
2493
+ if (layoutParams.gap !== void 0) {
2494
+ style.gap = String(layoutParams.gap);
2214
2495
  }
2215
- if (layout.params.align) {
2216
- style.align = layout.params.align;
2496
+ if (layoutParams.align !== void 0) {
2497
+ style.align = layoutParams.align;
2217
2498
  }
2218
- if (layout.params.background) {
2219
- style.background = String(layout.params.background);
2499
+ if (layoutParams.background !== void 0) {
2500
+ style.background = String(layoutParams.background);
2220
2501
  }
2221
2502
  const containerNode = {
2222
2503
  id: nodeId,
2223
2504
  kind: "container",
2224
2505
  containerType: layout.layoutType,
2225
- params: this.cleanParams(layout.params),
2506
+ params: this.cleanParams(layoutParams),
2226
2507
  children: childRefs,
2227
2508
  style,
2228
2509
  meta: {
@@ -2233,16 +2514,16 @@ Define these components with: define Component "Name" { ... }`
2233
2514
  this.nodes[nodeId] = containerNode;
2234
2515
  return nodeId;
2235
2516
  }
2236
- convertCell(cell) {
2517
+ convertCell(cell, context) {
2237
2518
  const nodeId = this.idGen.generate("node");
2238
2519
  const childRefs = [];
2239
2520
  for (const child of cell.children) {
2240
2521
  if (child.type === "layout") {
2241
- const childId = this.convertLayout(child);
2242
- childRefs.push({ ref: childId });
2522
+ const childId = this.convertLayout(child, context);
2523
+ if (childId) childRefs.push({ ref: childId });
2243
2524
  } else if (child.type === "component") {
2244
- const childId = this.convertComponent(child);
2245
- childRefs.push({ ref: childId });
2525
+ const childId = this.convertComponent(child, context);
2526
+ if (childId) childRefs.push({ ref: childId });
2246
2527
  }
2247
2528
  }
2248
2529
  const containerNode = {
@@ -2263,10 +2544,28 @@ Define these components with: define Component "Name" { ... }`
2263
2544
  this.nodes[nodeId] = containerNode;
2264
2545
  return nodeId;
2265
2546
  }
2266
- convertComponent(component) {
2547
+ convertComponent(component, context) {
2548
+ if (component.componentType === "Children") {
2549
+ if (!context?.allowChildrenSlot) {
2550
+ this.errors.push({
2551
+ type: "children-slot-outside-layout-definition",
2552
+ message: '"Children" placeholder can only be used inside a define Layout body.'
2553
+ });
2554
+ return null;
2555
+ }
2556
+ if (!context.childrenSlot) {
2557
+ this.errors.push({
2558
+ type: "children-slot-missing-child",
2559
+ message: `Layout "${context.definitionName}" requires exactly one child for "Children".`
2560
+ });
2561
+ return null;
2562
+ }
2563
+ return this.convertASTNode(context.childrenSlot, context);
2564
+ }
2565
+ const resolvedProps = this.resolveComponentProps(component.componentType, component.props, context);
2267
2566
  const definition = this.definedComponents.get(component.componentType);
2268
2567
  if (definition) {
2269
- return this.expandDefinedComponent(definition);
2568
+ return this.expandDefinedComponent(definition, resolvedProps, context);
2270
2569
  }
2271
2570
  const builtInComponents = /* @__PURE__ */ new Set([
2272
2571
  "Button",
@@ -2309,7 +2608,7 @@ Define these components with: define Component "Name" { ... }`
2309
2608
  id: nodeId,
2310
2609
  kind: "component",
2311
2610
  componentType: component.componentType,
2312
- props: component.props,
2611
+ props: resolvedProps,
2313
2612
  style: {},
2314
2613
  meta: {
2315
2614
  nodeId: component._meta?.nodeId
@@ -2319,15 +2618,224 @@ Define these components with: define Component "Name" { ... }`
2319
2618
  this.nodes[nodeId] = componentNode;
2320
2619
  return nodeId;
2321
2620
  }
2322
- expandDefinedComponent(definition) {
2621
+ expandDefinedComponent(definition, invocationArgs, parentContext) {
2622
+ const context = {
2623
+ args: invocationArgs,
2624
+ providedArgNames: new Set(Object.keys(invocationArgs)),
2625
+ usedArgNames: /* @__PURE__ */ new Set(),
2626
+ definitionName: definition.name,
2627
+ definitionKind: "component",
2628
+ allowChildrenSlot: false
2629
+ };
2323
2630
  if (definition.body.type === "layout") {
2324
- return this.convertLayout(definition.body);
2631
+ const result = this.convertLayout(definition.body, context);
2632
+ this.reportUnusedArguments(context);
2633
+ return result;
2325
2634
  } else if (definition.body.type === "component") {
2326
- return this.convertComponent(definition.body);
2635
+ const result = this.convertComponent(definition.body, context);
2636
+ this.reportUnusedArguments(context);
2637
+ return result;
2327
2638
  } else {
2328
2639
  throw new Error(`Invalid defined component body type for "${definition.name}"`);
2329
2640
  }
2330
2641
  }
2642
+ expandDefinedLayout(definition, invocationParams, invocationChildren, parentContext) {
2643
+ if (invocationChildren.length !== 1) {
2644
+ this.errors.push({
2645
+ type: "layout-children-arity",
2646
+ message: `Layout "${definition.name}" expects exactly one child, received ${invocationChildren.length}.`
2647
+ });
2648
+ }
2649
+ const rawSlot = invocationChildren[0];
2650
+ const resolvedSlot = rawSlot ? this.resolveChildrenSlot(rawSlot, parentContext) : void 0;
2651
+ const context = {
2652
+ args: invocationParams,
2653
+ providedArgNames: new Set(Object.keys(invocationParams)),
2654
+ usedArgNames: /* @__PURE__ */ new Set(),
2655
+ definitionName: definition.name,
2656
+ definitionKind: "layout",
2657
+ allowChildrenSlot: true,
2658
+ childrenSlot: resolvedSlot
2659
+ };
2660
+ const nodeId = this.convertLayout(definition.body, context);
2661
+ this.reportUnusedArguments(context);
2662
+ return nodeId;
2663
+ }
2664
+ resolveChildrenSlot(slot, parentContext) {
2665
+ if (slot.type === "component" && slot.componentType === "Children") {
2666
+ if (parentContext?.allowChildrenSlot) {
2667
+ return parentContext.childrenSlot;
2668
+ }
2669
+ this.errors.push({
2670
+ type: "children-slot-outside-layout-definition",
2671
+ message: '"Children" placeholder forwarding is only valid inside define Layout bodies.'
2672
+ });
2673
+ return void 0;
2674
+ }
2675
+ return slot;
2676
+ }
2677
+ convertASTNode(node, context) {
2678
+ if (node.type === "layout") return this.convertLayout(node, context);
2679
+ if (node.type === "component") return this.convertComponent(node, context);
2680
+ return this.convertCell(node, context);
2681
+ }
2682
+ resolveLayoutParams(layoutType, params, context) {
2683
+ const resolved = {};
2684
+ for (const [key, value] of Object.entries(params)) {
2685
+ const resolvedValue = this.resolveBindingValue(
2686
+ value,
2687
+ context,
2688
+ "layout-parameter",
2689
+ layoutType,
2690
+ key
2691
+ );
2692
+ if (resolvedValue !== void 0) {
2693
+ const wasPropReference = typeof value === "string" && value.startsWith("prop_");
2694
+ if (wasPropReference) {
2695
+ const layoutMetadata = import_components2.LAYOUTS[layoutType];
2696
+ const property = layoutMetadata?.properties?.[key];
2697
+ if (property?.type === "enum" && Array.isArray(property.options)) {
2698
+ const normalizedValue = String(resolvedValue);
2699
+ if (!property.options.includes(normalizedValue)) {
2700
+ this.warnings.push({
2701
+ type: "invalid-bound-enum-value",
2702
+ message: `Invalid value "${normalizedValue}" for parameter "${key}" in layout "${layoutType}". Expected one of: ${property.options.join(", ")}.`
2703
+ });
2704
+ }
2705
+ }
2706
+ }
2707
+ resolved[key] = resolvedValue;
2708
+ }
2709
+ }
2710
+ return resolved;
2711
+ }
2712
+ normalizeSplitParams(params) {
2713
+ const normalized = { ...params };
2714
+ if (normalized.sidebar !== void 0 && normalized.left === void 0 && normalized.right === void 0) {
2715
+ normalized.left = normalized.sidebar;
2716
+ this.warnings.push({
2717
+ type: "split-sidebar-deprecated",
2718
+ message: 'Split parameter "sidebar" is deprecated. Use "left" or "right".'
2719
+ });
2720
+ }
2721
+ delete normalized.sidebar;
2722
+ const hasLeft = normalized.left !== void 0;
2723
+ const hasRight = normalized.right !== void 0;
2724
+ if (hasLeft && hasRight) {
2725
+ delete normalized.right;
2726
+ this.warnings.push({
2727
+ type: "split-side-conflict",
2728
+ message: 'Split layout received both "left" and "right"; keeping "left".'
2729
+ });
2730
+ }
2731
+ if (!hasLeft && !hasRight) {
2732
+ normalized.left = 250;
2733
+ this.warnings.push({
2734
+ type: "split-side-missing",
2735
+ message: 'Split layout missing both "left" and "right"; defaulting to left: 250.'
2736
+ });
2737
+ }
2738
+ if (normalized.left !== void 0) {
2739
+ const leftWidth = Number(normalized.left);
2740
+ if (!Number.isFinite(leftWidth) || leftWidth <= 0) {
2741
+ normalized.left = 250;
2742
+ this.warnings.push({
2743
+ type: "split-left-invalid",
2744
+ message: 'Split "left" must be a positive number. Falling back to 250.'
2745
+ });
2746
+ }
2747
+ }
2748
+ if (normalized.right !== void 0) {
2749
+ const rightWidth = Number(normalized.right);
2750
+ if (!Number.isFinite(rightWidth) || rightWidth <= 0) {
2751
+ normalized.right = 250;
2752
+ this.warnings.push({
2753
+ type: "split-right-invalid",
2754
+ message: 'Split "right" must be a positive number. Falling back to 250.'
2755
+ });
2756
+ }
2757
+ }
2758
+ return normalized;
2759
+ }
2760
+ resolveComponentProps(componentType, props, context) {
2761
+ const resolved = {};
2762
+ for (const [key, value] of Object.entries(props)) {
2763
+ const resolvedValue = this.resolveBindingValue(
2764
+ value,
2765
+ context,
2766
+ "component-property",
2767
+ componentType,
2768
+ key
2769
+ );
2770
+ if (resolvedValue !== void 0) {
2771
+ const wasPropReference = typeof value === "string" && value.startsWith("prop_");
2772
+ if (wasPropReference) {
2773
+ const metadata = import_components2.COMPONENTS[componentType];
2774
+ const property = metadata?.properties?.[key];
2775
+ if (property?.type === "enum" && Array.isArray(property.options)) {
2776
+ const normalizedValue = String(resolvedValue);
2777
+ if (!property.options.includes(normalizedValue)) {
2778
+ this.warnings.push({
2779
+ type: "invalid-bound-enum-value",
2780
+ message: `Invalid value "${normalizedValue}" for property "${key}" in component "${componentType}". Expected one of: ${property.options.join(", ")}.`
2781
+ });
2782
+ }
2783
+ }
2784
+ }
2785
+ resolved[key] = resolvedValue;
2786
+ }
2787
+ }
2788
+ return resolved;
2789
+ }
2790
+ resolveBindingValue(value, context, kind, targetType, targetName) {
2791
+ if (typeof value !== "string" || !value.startsWith("prop_")) {
2792
+ return value;
2793
+ }
2794
+ const argName = value.slice("prop_".length);
2795
+ if (!context) {
2796
+ return value;
2797
+ }
2798
+ if (Object.prototype.hasOwnProperty.call(context.args, argName)) {
2799
+ context.usedArgNames.add(argName);
2800
+ return context.args[argName];
2801
+ }
2802
+ const required = this.isBindingTargetRequired(kind, targetType, targetName);
2803
+ const descriptor = kind === "component-property" ? "property" : "parameter";
2804
+ const message = `Missing required bound ${descriptor} "${targetName}" for ${kind === "component-property" ? "component" : "layout"} "${targetType}" in ${context.definitionKind} "${context.definitionName}" (expected arg "${argName}").`;
2805
+ if (required) {
2806
+ this.errors.push({ type: "missing-required-bound-value", message });
2807
+ } else {
2808
+ this.warnings.push({
2809
+ type: "missing-bound-value",
2810
+ 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}".`
2811
+ });
2812
+ }
2813
+ return void 0;
2814
+ }
2815
+ isBindingTargetRequired(kind, targetType, targetName) {
2816
+ if (kind === "component-property") {
2817
+ const metadata = import_components2.COMPONENTS[targetType];
2818
+ const property2 = metadata?.properties?.[targetName];
2819
+ if (!property2) return false;
2820
+ return property2.required === true && property2.defaultValue === void 0;
2821
+ }
2822
+ const layoutMetadata = import_components2.LAYOUTS[targetType];
2823
+ if (!layoutMetadata) return false;
2824
+ const property = layoutMetadata.properties?.[targetName];
2825
+ const requiredFromProperty = property?.required === true && property.defaultValue === void 0;
2826
+ const requiredFromLayout = (layoutMetadata.requiredProperties || []).includes(targetName);
2827
+ return requiredFromProperty || requiredFromLayout;
2828
+ }
2829
+ reportUnusedArguments(context) {
2830
+ for (const arg of context.providedArgNames) {
2831
+ if (!context.usedArgNames.has(arg)) {
2832
+ this.warnings.push({
2833
+ type: "unused-definition-argument",
2834
+ message: `Argument "${arg}" is not used by ${context.definitionKind} "${context.definitionName}".`
2835
+ });
2836
+ }
2837
+ }
2838
+ }
2331
2839
  cleanParams(params) {
2332
2840
  const cleaned = {};
2333
2841
  for (const [key, value] of Object.entries(params)) {
@@ -2377,9 +2885,24 @@ var ICON_SIZES_BY_DENSITY = {
2377
2885
  comfortable: { xs: 14, sm: 16, md: 20, lg: 28, xl: 36 }
2378
2886
  };
2379
2887
  var ICON_BUTTON_SIZES_BY_DENSITY = {
2380
- compact: { sm: 24, md: 28, lg: 32 },
2381
- normal: { sm: 28, md: 32, lg: 40 },
2382
- comfortable: { sm: 32, md: 40, lg: 48 }
2888
+ compact: { sm: 20, md: 24, lg: 32 },
2889
+ normal: { sm: 24, md: 32, lg: 40 },
2890
+ comfortable: { sm: 28, md: 40, lg: 48 }
2891
+ };
2892
+ var CONTROL_HEIGHTS_BY_DENSITY = {
2893
+ compact: { sm: 28, md: 32, lg: 36 },
2894
+ normal: { sm: 36, md: 40, lg: 48 },
2895
+ comfortable: { sm: 40, md: 48, lg: 56 }
2896
+ };
2897
+ var ACTION_CONTROL_HEIGHTS_BY_DENSITY = {
2898
+ compact: { sm: 20, md: 24, lg: 32 },
2899
+ normal: { sm: 24, md: 32, lg: 40 },
2900
+ comfortable: { sm: 28, md: 40, lg: 48 }
2901
+ };
2902
+ var CONTROL_PADDING_BY_DENSITY = {
2903
+ compact: { none: 0, xs: 4, sm: 8, md: 10, lg: 14, xl: 18 },
2904
+ normal: { none: 0, xs: 6, sm: 10, md: 14, lg: 18, xl: 24 },
2905
+ comfortable: { none: 0, xs: 8, sm: 12, md: 16, lg: 22, xl: 28 }
2383
2906
  };
2384
2907
  function resolveIconSize(size, density = "normal") {
2385
2908
  const map = ICON_SIZES_BY_DENSITY[density] || ICON_SIZES_BY_DENSITY.normal;
@@ -2389,6 +2912,18 @@ function resolveIconButtonSize(size, density = "normal") {
2389
2912
  const map = ICON_BUTTON_SIZES_BY_DENSITY[density] || ICON_BUTTON_SIZES_BY_DENSITY.normal;
2390
2913
  return map[size || "md"] || map.md;
2391
2914
  }
2915
+ function resolveControlHeight(size, density = "normal") {
2916
+ const map = CONTROL_HEIGHTS_BY_DENSITY[density] || CONTROL_HEIGHTS_BY_DENSITY.normal;
2917
+ return map[size || "md"] || map.md;
2918
+ }
2919
+ function resolveActionControlHeight(size, density = "normal") {
2920
+ const map = ACTION_CONTROL_HEIGHTS_BY_DENSITY[density] || ACTION_CONTROL_HEIGHTS_BY_DENSITY.normal;
2921
+ return map[size || "md"] || map.md;
2922
+ }
2923
+ function resolveControlHorizontalPadding(padding, density = "normal") {
2924
+ const map = CONTROL_PADDING_BY_DENSITY[density] || CONTROL_PADDING_BY_DENSITY.normal;
2925
+ return map[padding || "md"] ?? map.md;
2926
+ }
2392
2927
 
2393
2928
  // src/shared/heading-levels.ts
2394
2929
  var DEFAULT_LEVEL = "h2";
@@ -2707,6 +3242,41 @@ var LayoutEngine = class {
2707
3242
  }
2708
3243
  return totalHeight;
2709
3244
  }
3245
+ if (node.containerType === "split") {
3246
+ const splitGap = this.resolveSpacing(node.style.gap);
3247
+ const leftParam = node.params.left;
3248
+ const rightParam = node.params.right;
3249
+ const leftWidthRaw = Number(leftParam);
3250
+ const rightWidthRaw = Number(rightParam);
3251
+ const hasLeft = leftParam !== void 0;
3252
+ const hasRight = rightParam !== void 0;
3253
+ const leftWidth = Number.isFinite(leftWidthRaw) && leftWidthRaw > 0 ? leftWidthRaw : availableWidth / 2;
3254
+ const rightWidth = Number.isFinite(rightWidthRaw) && rightWidthRaw > 0 ? rightWidthRaw : availableWidth / 2;
3255
+ let maxHeight = 0;
3256
+ node.children.forEach((childRef, index) => {
3257
+ const child = this.nodes[childRef.ref];
3258
+ let childHeight = this.getComponentHeight();
3259
+ const isFirst = index === 0;
3260
+ let childWidth;
3261
+ if (node.children.length >= 2) {
3262
+ if (hasRight && !hasLeft) {
3263
+ childWidth = isFirst ? Math.max(1, availableWidth - rightWidth - splitGap) : rightWidth;
3264
+ } else {
3265
+ childWidth = isFirst ? leftWidth : Math.max(1, availableWidth - leftWidth - splitGap);
3266
+ }
3267
+ } else {
3268
+ childWidth = availableWidth;
3269
+ }
3270
+ if (child?.kind === "component") {
3271
+ childHeight = child.props.height ? Number(child.props.height) : this.getIntrinsicComponentHeight(child, childWidth);
3272
+ } else if (child?.kind === "container") {
3273
+ childHeight = this.calculateContainerHeight(child, childWidth);
3274
+ }
3275
+ maxHeight = Math.max(maxHeight, childHeight);
3276
+ });
3277
+ totalHeight += maxHeight;
3278
+ return totalHeight;
3279
+ }
2710
3280
  const direction = node.params.direction || "vertical";
2711
3281
  if (node.containerType === "stack" && direction === "horizontal") {
2712
3282
  let maxHeight = 0;
@@ -2829,14 +3399,28 @@ var LayoutEngine = class {
2829
3399
  calculateSplit(node, x, y, width, height) {
2830
3400
  if (node.kind !== "container") return;
2831
3401
  const gap = this.resolveSpacing(node.style.gap);
2832
- const sidebarWidth = Number(node.params.sidebar) || 260;
3402
+ const leftParam = node.params.left;
3403
+ const rightParam = node.params.right;
3404
+ const leftWidthRaw = Number(leftParam);
3405
+ const rightWidthRaw = Number(rightParam);
3406
+ const hasLeft = leftParam !== void 0;
3407
+ const hasRight = rightParam !== void 0;
3408
+ const leftWidth = Number.isFinite(leftWidthRaw) && leftWidthRaw > 0 ? leftWidthRaw : 250;
3409
+ const rightWidth = Number.isFinite(rightWidthRaw) && rightWidthRaw > 0 ? rightWidthRaw : 250;
2833
3410
  if (node.children.length === 1) {
2834
3411
  this.calculateNode(node.children[0].ref, x, y, width, height, "split");
2835
3412
  } else if (node.children.length >= 2) {
2836
- this.calculateNode(node.children[0].ref, x, y, sidebarWidth, height, "split");
2837
- const contentX = x + sidebarWidth + gap;
2838
- const contentWidth = width - sidebarWidth - gap;
2839
- this.calculateNode(node.children[1].ref, contentX, y, contentWidth, height, "split");
3413
+ if (hasRight && !hasLeft) {
3414
+ const flexibleLeftWidth = Math.max(1, width - rightWidth - gap);
3415
+ const rightX = x + flexibleLeftWidth + gap;
3416
+ this.calculateNode(node.children[0].ref, x, y, flexibleLeftWidth, height, "split");
3417
+ this.calculateNode(node.children[1].ref, rightX, y, rightWidth, height, "split");
3418
+ } else {
3419
+ const flexibleRightWidth = Math.max(1, width - leftWidth - gap);
3420
+ const rightX = x + leftWidth + gap;
3421
+ this.calculateNode(node.children[0].ref, x, y, leftWidth, height, "split");
3422
+ this.calculateNode(node.children[1].ref, rightX, y, flexibleRightWidth, height, "split");
3423
+ }
2840
3424
  }
2841
3425
  }
2842
3426
  calculatePanel(node, x, y, width, height) {
@@ -2985,7 +3569,11 @@ var LayoutEngine = class {
2985
3569
  }
2986
3570
  getIntrinsicComponentHeight(node, availableWidth) {
2987
3571
  if (node.kind !== "component") return this.getComponentHeight();
2988
- const controlLabelOffset = node.componentType === "Input" || node.componentType === "Textarea" || node.componentType === "Select" ? this.getControlLabelOffset(String(node.props.label || "")) : 0;
3572
+ const controlSize = String(node.props.size || "md");
3573
+ const density = this.style.density || "normal";
3574
+ const inputControlHeight = resolveControlHeight(controlSize, density);
3575
+ const actionControlHeight = resolveActionControlHeight(controlSize, density);
3576
+ 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;
2989
3577
  if (node.componentType === "Image") {
2990
3578
  const placeholder = String(node.props.placeholder || "landscape");
2991
3579
  const aspectRatios = {
@@ -3012,12 +3600,26 @@ var LayoutEngine = class {
3012
3600
  }
3013
3601
  const rowCount = Number(node.props.rows || node.props.rowsMock || 5);
3014
3602
  const hasTitle = !!node.props.title;
3015
- const hasPagination = String(node.props.pagination) === "true";
3603
+ const hasPagination = this.parseBooleanProp(node.props.pagination, false);
3604
+ const hasCaption = String(node.props.caption || "").trim().length > 0;
3605
+ const paginationAlign = String(node.props.paginationAlign || "right");
3606
+ const captionAlign = String(node.props.captionAlign || "");
3607
+ const effectiveCaptionAlign = captionAlign === "left" || captionAlign === "center" || captionAlign === "right" ? captionAlign : paginationAlign === "left" ? "right" : "left";
3608
+ const sameFooterAlign = hasCaption && hasPagination && effectiveCaptionAlign === paginationAlign;
3016
3609
  const headerHeight = 44;
3017
3610
  const rowHeight = 36;
3018
3611
  const titleHeight = hasTitle ? 32 : 0;
3019
- const paginationHeight = hasPagination ? 64 : 0;
3020
- return titleHeight + headerHeight + rowCount * rowHeight + paginationHeight;
3612
+ let footerHeight = 0;
3613
+ if (hasPagination || hasCaption) {
3614
+ const footerBottomPadding = 12;
3615
+ footerHeight += 16;
3616
+ footerHeight += hasPagination ? 32 : 18;
3617
+ if (sameFooterAlign) {
3618
+ footerHeight += 8 + 18;
3619
+ }
3620
+ footerHeight += footerBottomPadding;
3621
+ }
3622
+ return titleHeight + headerHeight + rowCount * rowHeight + footerHeight;
3021
3623
  }
3022
3624
  if (node.componentType === "Heading") {
3023
3625
  const text = String(node.props.text || "Heading");
@@ -3026,15 +3628,15 @@ var LayoutEngine = class {
3026
3628
  const maxWidth = availableWidth && availableWidth > 0 ? availableWidth : 200;
3027
3629
  const lines = this.wrapTextToLines(text, maxWidth, fontSize);
3028
3630
  const wrappedHeight = Math.max(1, lines.length) * lineHeightPx;
3029
- const density = this.style.density || "normal";
3030
- const verticalPadding = resolveHeadingVerticalPadding(node.props.spacing, density);
3631
+ const density2 = this.style.density || "normal";
3632
+ const verticalPadding = resolveHeadingVerticalPadding(node.props.spacing, density2);
3031
3633
  if (verticalPadding === null) {
3032
3634
  return Math.max(this.getComponentHeight(), wrappedHeight);
3033
3635
  }
3034
3636
  return Math.max(1, Math.ceil(wrappedHeight + verticalPadding * 2));
3035
3637
  }
3036
3638
  if (node.componentType === "Text") {
3037
- const content = String(node.props.content || "");
3639
+ const content = String(node.props.text || "");
3038
3640
  const { fontSize, lineHeight } = this.getTextMetricsForDensity();
3039
3641
  const lineHeightPx = Math.ceil(fontSize * lineHeight);
3040
3642
  const maxWidth = availableWidth && availableWidth > 0 ? availableWidth : 200;
@@ -3083,7 +3685,10 @@ var LayoutEngine = class {
3083
3685
  if (node.componentType === "Divider") return 1;
3084
3686
  if (node.componentType === "Separate") return this.getSeparateSize(node);
3085
3687
  if (node.componentType === "Input" || node.componentType === "Select") {
3086
- return this.getComponentHeight() + controlLabelOffset;
3688
+ return inputControlHeight + controlLabelOffset;
3689
+ }
3690
+ if (node.componentType === "Button" || node.componentType === "IconButton" || node.componentType === "Link") {
3691
+ return actionControlHeight + controlLabelOffset;
3087
3692
  }
3088
3693
  return this.getComponentHeight();
3089
3694
  }
@@ -3100,7 +3705,10 @@ var LayoutEngine = class {
3100
3705
  }
3101
3706
  if (node.componentType === "IconButton") {
3102
3707
  const size = String(node.props.size || "md");
3103
- return resolveIconButtonSize(size, this.style.density || "normal");
3708
+ const density = this.style.density || "normal";
3709
+ const baseSize = resolveIconButtonSize(size, density);
3710
+ const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
3711
+ return baseSize + extraPadding * 2;
3104
3712
  }
3105
3713
  if (node.componentType === "Checkbox" || node.componentType === "Radio") {
3106
3714
  return 24;
@@ -3109,11 +3717,13 @@ var LayoutEngine = class {
3109
3717
  if (node.componentType === "Button" || node.componentType === "Link") {
3110
3718
  const text = String(node.props.text || "");
3111
3719
  const { fontSize, paddingX } = this.getButtonMetricsForDensity();
3720
+ const density = this.style.density || "normal";
3721
+ const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
3112
3722
  const textWidth = this.estimateTextWidth(text, fontSize);
3113
- return Math.max(60, Math.ceil(textWidth + paddingX * 2));
3723
+ return Math.max(60, Math.ceil(textWidth + (paddingX + extraPadding) * 2));
3114
3724
  }
3115
3725
  if (node.componentType === "Label" || node.componentType === "Text") {
3116
- const text = String(node.props.content || node.props.text || "");
3726
+ const text = String(node.props.text || "");
3117
3727
  return Math.max(60, text.length * 8 + 16);
3118
3728
  }
3119
3729
  if (node.componentType === "Heading") {
@@ -3975,27 +4585,27 @@ var THEMES = {
3975
4585
  bg: "#F8FAFC",
3976
4586
  cardBg: "#FFFFFF",
3977
4587
  border: "#E2E8F0",
3978
- text: "#1E293B",
4588
+ text: "#000000",
3979
4589
  textMuted: "#64748B",
3980
4590
  primary: "#3B82F6",
3981
4591
  primaryHover: "#2563EB",
3982
4592
  primaryLight: "#EFF6FF"
3983
4593
  },
3984
4594
  dark: {
3985
- bg: "#0F172A",
3986
- cardBg: "#1E293B",
3987
- border: "#334155",
3988
- text: "#F1F5F9",
3989
- textMuted: "#94A3B8",
4595
+ bg: "#111111",
4596
+ cardBg: "#1C1C1C",
4597
+ border: "#303030",
4598
+ text: "#F0F0F0",
4599
+ textMuted: "#808080",
3990
4600
  primary: "#60A5FA",
3991
4601
  primaryHover: "#3B82F6",
3992
- primaryLight: "#1E3A8A"
4602
+ primaryLight: "#1C2A3A"
3993
4603
  }
3994
4604
  };
3995
4605
  var SVGRenderer = class {
3996
4606
  constructor(ir, layout, options) {
3997
4607
  this.renderedNodeIds = /* @__PURE__ */ new Set();
3998
- this.fontFamily = "system-ui, -apple-system, sans-serif";
4608
+ this.fontFamily = "Arial, Helvetica, sans-serif";
3999
4609
  this.parentContainerByChildId = /* @__PURE__ */ new Map();
4000
4610
  this.ir = ir;
4001
4611
  this.layout = layout;
@@ -4009,7 +4619,6 @@ var SVGRenderer = class {
4009
4619
  includeLabels: options?.includeLabels ?? true,
4010
4620
  screenName: options?.screenName
4011
4621
  };
4012
- this.renderTheme = THEMES[this.options.theme];
4013
4622
  this.colorResolver = new ColorResolver();
4014
4623
  this.buildParentContainerIndex();
4015
4624
  if (ir.project.mocks && Object.keys(ir.project.mocks).length > 0) {
@@ -4018,6 +4627,12 @@ var SVGRenderer = class {
4018
4627
  if (ir.project.colors && Object.keys(ir.project.colors).length > 0) {
4019
4628
  this.colorResolver.setCustomColors(ir.project.colors);
4020
4629
  }
4630
+ const themeDefaults = THEMES[this.options.theme];
4631
+ this.renderTheme = {
4632
+ ...themeDefaults,
4633
+ text: this.resolveTextColor(),
4634
+ textMuted: this.resolveMutedColor()
4635
+ };
4021
4636
  }
4022
4637
  /**
4023
4638
  * Get list of available screens in the project
@@ -4099,6 +4714,9 @@ var SVGRenderer = class {
4099
4714
  if (node.containerType === "card") {
4100
4715
  this.renderCardBorder(node, pos, containerGroup);
4101
4716
  }
4717
+ if (node.containerType === "split") {
4718
+ this.renderSplitDecoration(node, pos, containerGroup);
4719
+ }
4102
4720
  node.children.forEach((childRef) => {
4103
4721
  this.renderNode(childRef.ref, containerGroup);
4104
4722
  });
@@ -4186,6 +4804,8 @@ var SVGRenderer = class {
4186
4804
  }
4187
4805
  renderHeading(node, pos) {
4188
4806
  const text = String(node.props.text || "Heading");
4807
+ const variant = String(node.props.variant || "default");
4808
+ const headingColor = variant === "default" ? this.resolveTextColor() : this.resolveVariantColor(variant, this.resolveTextColor());
4189
4809
  const headingTypography = this.getHeadingTypography(node);
4190
4810
  const fontSize = headingTypography.fontSize;
4191
4811
  const fontWeight = headingTypography.fontWeight;
@@ -4195,10 +4815,10 @@ var SVGRenderer = class {
4195
4815
  if (lines.length <= 1) {
4196
4816
  return `<g${this.getDataNodeId(node)}>
4197
4817
  <text x="${pos.x}" y="${firstLineY}"
4198
- font-family="system-ui, -apple-system, sans-serif"
4818
+ font-family="Arial, Helvetica, sans-serif"
4199
4819
  font-size="${fontSize}"
4200
4820
  font-weight="${fontWeight}"
4201
- fill="${this.renderTheme.text}">${this.escapeXml(text)}</text>
4821
+ fill="${headingColor}">${this.escapeXml(text)}</text>
4202
4822
  </g>`;
4203
4823
  }
4204
4824
  const tspans = lines.map(
@@ -4206,61 +4826,101 @@ var SVGRenderer = class {
4206
4826
  ).join("");
4207
4827
  return `<g${this.getDataNodeId(node)}>
4208
4828
  <text x="${pos.x}" y="${firstLineY}"
4209
- font-family="system-ui, -apple-system, sans-serif"
4829
+ font-family="Arial, Helvetica, sans-serif"
4210
4830
  font-size="${fontSize}"
4211
4831
  font-weight="${fontWeight}"
4212
- fill="${this.renderTheme.text}">${tspans}</text>
4832
+ fill="${headingColor}">${tspans}</text>
4213
4833
  </g>`;
4214
4834
  }
4215
4835
  renderButton(node, pos) {
4216
4836
  const text = String(node.props.text || "Button");
4217
4837
  const variant = String(node.props.variant || "default");
4838
+ const size = String(node.props.size || "md");
4839
+ const density = this.ir.project.style.density || "normal";
4840
+ const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
4841
+ const labelOffset = this.parseBooleanProp(node.props.labelSpace, false) ? 18 : 0;
4218
4842
  const fullWidth = this.shouldButtonFillAvailableWidth(node);
4843
+ const iconName = String(node.props.icon || "").trim();
4844
+ const iconAlign = String(node.props.iconAlign || "left").toLowerCase();
4219
4845
  const radius = this.tokens.button.radius;
4220
4846
  const fontSize = this.tokens.button.fontSize;
4221
4847
  const fontWeight = this.tokens.button.fontWeight;
4222
4848
  const paddingX = this.tokens.button.paddingX;
4223
- const paddingY = this.tokens.button.paddingY;
4849
+ const controlHeight = resolveActionControlHeight(size, density);
4850
+ const buttonY = pos.y + labelOffset;
4851
+ const buttonHeight = Math.max(16, Math.min(controlHeight, pos.height - labelOffset));
4852
+ const iconSvg = iconName ? getIcon(iconName) : null;
4853
+ const iconSize = iconSvg ? Math.round(fontSize * 1.1) : 0;
4854
+ const iconGap = iconSvg ? 8 : 0;
4855
+ const edgePad = 12;
4856
+ const textPad = paddingX + extraPadding;
4224
4857
  const idealTextWidth = this.estimateTextWidth(text, fontSize);
4225
- const buttonWidth = fullWidth ? Math.max(1, pos.width) : this.clampControlWidth(Math.max(Math.ceil(idealTextWidth + paddingX * 2), 60), pos.width);
4226
- const buttonHeight = fontSize + paddingY * 2;
4227
- const availableTextWidth = Math.max(0, buttonWidth - paddingX * 2);
4858
+ const buttonWidth = fullWidth ? Math.max(1, pos.width) : this.clampControlWidth(Math.max(Math.ceil(idealTextWidth + (iconSvg ? iconSize + iconGap : 0) + textPad * 2), 60), pos.width);
4859
+ const availableTextWidth = Math.max(0, buttonWidth - textPad * 2 - (iconSvg ? iconSize + iconGap : 0));
4228
4860
  const visibleText = this.truncateTextToWidth(text, availableTextWidth, fontSize);
4229
4861
  const semanticBase = this.getSemanticVariantColor(variant);
4230
4862
  const hasExplicitVariantColor = semanticBase !== void 0 || this.colorResolver.hasColor(variant);
4231
4863
  const resolvedBase = this.resolveVariantColor(variant, this.renderTheme.primary);
4232
- const bgColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.85) : "rgba(226, 232, 240, 0.9)";
4233
- const textColor = hasExplicitVariantColor ? "#FFFFFF" : "rgba(30, 41, 59, 0.85)";
4234
- const borderColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.7) : "rgba(100, 116, 139, 0.4)";
4235
- return `<g${this.getDataNodeId(node)}>
4236
- <rect x="${pos.x}" y="${pos.y}"
4864
+ const isDarkMode = this.options.theme === "dark";
4865
+ const bgColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.85) : isDarkMode ? "rgba(48, 48, 55, 0.9)" : "rgba(226, 232, 240, 0.9)";
4866
+ const textColor = hasExplicitVariantColor ? "#FFFFFF" : this.hexToRgba(this.resolveTextColor(), 0.85);
4867
+ const borderColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.7) : isDarkMode ? "rgba(75, 75, 88, 0.8)" : "rgba(100, 116, 139, 0.4)";
4868
+ const iconOffsetY = buttonY + (buttonHeight - iconSize) / 2;
4869
+ const iconX = iconAlign === "right" ? pos.x + buttonWidth - edgePad - iconSize : pos.x + edgePad;
4870
+ const textAlign = String(node.props.align || "center").toLowerCase();
4871
+ const sidePad = textPad + 4;
4872
+ let textX;
4873
+ let textAnchor;
4874
+ if (textAlign === "left") {
4875
+ textX = iconSvg && iconAlign === "left" ? pos.x + edgePad + iconSize + iconGap : pos.x + sidePad;
4876
+ textAnchor = "start";
4877
+ } else if (textAlign === "right") {
4878
+ textX = iconSvg && iconAlign === "right" ? pos.x + buttonWidth - edgePad - iconSize - iconGap : pos.x + buttonWidth - sidePad;
4879
+ textAnchor = "end";
4880
+ } else {
4881
+ textX = pos.x + buttonWidth / 2;
4882
+ textAnchor = "middle";
4883
+ }
4884
+ let svg = `<g${this.getDataNodeId(node)}>
4885
+ <rect x="${pos.x}" y="${buttonY}"
4237
4886
  width="${buttonWidth}" height="${buttonHeight}"
4238
4887
  rx="${radius}"
4239
4888
  fill="${bgColor}"
4240
4889
  stroke="${borderColor}"
4241
- stroke-width="1"/>
4242
- <text x="${pos.x + buttonWidth / 2}" y="${pos.y + buttonHeight / 2 + fontSize * 0.35}"
4243
- font-family="system-ui, -apple-system, sans-serif"
4890
+ stroke-width="1"/>`;
4891
+ if (iconSvg) {
4892
+ svg += `
4893
+ <g transform="translate(${iconX}, ${iconOffsetY})">
4894
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${textColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
4895
+ ${this.extractSvgContent(iconSvg)}
4896
+ </svg>
4897
+ </g>`;
4898
+ }
4899
+ svg += `
4900
+ <text x="${textX}" y="${buttonY + buttonHeight / 2 + fontSize * 0.35}"
4901
+ font-family="Arial, Helvetica, sans-serif"
4244
4902
  font-size="${fontSize}"
4245
4903
  font-weight="${fontWeight}"
4246
4904
  fill="${textColor}"
4247
- text-anchor="middle">${this.escapeXml(visibleText)}</text>
4905
+ text-anchor="${textAnchor}">${this.escapeXml(visibleText)}</text>
4248
4906
  </g>`;
4907
+ return svg;
4249
4908
  }
4250
4909
  renderLink(node, pos) {
4251
4910
  const text = String(node.props.text || "Link");
4252
4911
  const variant = String(node.props.variant || "primary");
4912
+ const size = String(node.props.size || "md");
4913
+ const density = this.ir.project.style.density || "normal";
4253
4914
  const fontSize = this.tokens.button.fontSize;
4254
4915
  const fontWeight = this.tokens.button.fontWeight;
4255
4916
  const paddingX = this.tokens.button.paddingX;
4256
- const paddingY = this.tokens.button.paddingY;
4257
4917
  const linkColor = this.resolveVariantColor(variant, this.renderTheme.primary);
4258
4918
  const idealTextWidth = this.estimateTextWidth(text, fontSize);
4259
4919
  const linkWidth = this.clampControlWidth(
4260
4920
  Math.max(Math.ceil(idealTextWidth + paddingX * 2), 60),
4261
4921
  pos.width
4262
4922
  );
4263
- const linkHeight = fontSize + paddingY * 2;
4923
+ const linkHeight = Math.max(16, Math.min(resolveActionControlHeight(size, density), pos.height));
4264
4924
  const availableTextWidth = Math.max(0, linkWidth - paddingX * 2);
4265
4925
  const visibleText = this.truncateTextToWidth(text, availableTextWidth, fontSize);
4266
4926
  const visibleTextWidth = Math.min(
@@ -4271,7 +4931,7 @@ var SVGRenderer = class {
4271
4931
  const underlineY = centerY + 3;
4272
4932
  return `<g${this.getDataNodeId(node)}>
4273
4933
  <text x="${pos.x + linkWidth / 2}" y="${centerY}"
4274
- font-family="system-ui, -apple-system, sans-serif"
4934
+ font-family="Arial, Helvetica, sans-serif"
4275
4935
  font-size="${fontSize}"
4276
4936
  font-weight="${fontWeight}"
4277
4937
  fill="${linkColor}"
@@ -4285,53 +4945,108 @@ var SVGRenderer = class {
4285
4945
  renderInput(node, pos) {
4286
4946
  const label = String(node.props.label || "");
4287
4947
  const placeholder = String(node.props.placeholder || "");
4948
+ const iconLeftName = String(node.props.iconLeft || "").trim();
4949
+ const iconRightName = String(node.props.iconRight || "").trim();
4288
4950
  const radius = this.tokens.input.radius;
4289
4951
  const fontSize = this.tokens.input.fontSize;
4290
4952
  const paddingX = this.tokens.input.paddingX;
4291
4953
  const labelOffset = this.getControlLabelOffset(label);
4292
4954
  const controlY = pos.y + labelOffset;
4293
4955
  const controlHeight = Math.max(16, pos.height - labelOffset);
4294
- return `<g${this.getDataNodeId(node)}>
4295
- ${label ? `<text x="${pos.x + paddingX}" y="${this.getControlLabelBaselineY(pos.y)}"
4296
- font-family="system-ui, -apple-system, sans-serif"
4297
- font-size="12"
4298
- fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>` : ""}
4956
+ const iconSize = 16;
4957
+ const iconPad = 12;
4958
+ const iconInnerGap = 8;
4959
+ const iconLeftSvg = iconLeftName ? getIcon(iconLeftName) : null;
4960
+ const iconRightSvg = iconRightName ? getIcon(iconRightName) : null;
4961
+ const leftOffset = iconLeftSvg ? iconPad + iconSize + iconInnerGap : 0;
4962
+ const rightOffset = iconRightSvg ? iconPad + iconSize + iconInnerGap : 0;
4963
+ const textX = pos.x + (iconLeftSvg ? leftOffset : paddingX);
4964
+ const iconColor = this.hexToRgba(this.resolveMutedColor(), 0.8);
4965
+ const iconCenterY = controlY + (controlHeight - iconSize) / 2;
4966
+ let svg = `<g${this.getDataNodeId(node)}>`;
4967
+ if (label) {
4968
+ svg += `
4969
+ <text x="${pos.x + paddingX}" y="${this.getControlLabelBaselineY(pos.y)}"
4970
+ font-family="Arial, Helvetica, sans-serif"
4971
+ font-size="12"
4972
+ fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>`;
4973
+ }
4974
+ svg += `
4299
4975
  <rect x="${pos.x}" y="${controlY}"
4300
4976
  width="${pos.width}" height="${controlHeight}"
4301
4977
  rx="${radius}"
4302
4978
  fill="${this.renderTheme.cardBg}"
4303
4979
  stroke="${this.renderTheme.border}"
4304
- stroke-width="1"/>
4305
- <text x="${pos.x + paddingX}" y="${controlY + controlHeight / 2 + 5}"
4306
- font-family="system-ui, -apple-system, sans-serif"
4980
+ stroke-width="1"/>`;
4981
+ if (iconLeftSvg) {
4982
+ svg += `
4983
+ <g transform="translate(${pos.x + iconPad}, ${iconCenterY})">
4984
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
4985
+ ${this.extractSvgContent(iconLeftSvg)}
4986
+ </svg>
4987
+ </g>`;
4988
+ }
4989
+ if (iconRightSvg) {
4990
+ svg += `
4991
+ <g transform="translate(${pos.x + pos.width - iconPad - iconSize}, ${iconCenterY})">
4992
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
4993
+ ${this.extractSvgContent(iconRightSvg)}
4994
+ </svg>
4995
+ </g>`;
4996
+ }
4997
+ if (placeholder) {
4998
+ const availPlaceholderWidth = pos.width - (iconLeftSvg ? leftOffset : paddingX) - (iconRightSvg ? rightOffset : paddingX);
4999
+ const visiblePlaceholder = this.truncateTextToWidth(placeholder, Math.max(0, availPlaceholderWidth), fontSize);
5000
+ svg += `
5001
+ <text x="${textX}" y="${controlY + controlHeight / 2 + 5}"
5002
+ font-family="Arial, Helvetica, sans-serif"
4307
5003
  font-size="${fontSize}"
4308
- fill="${this.renderTheme.textMuted}">${this.escapeXml(placeholder)}</text>
4309
- </g>`;
5004
+ fill="${this.renderTheme.textMuted}">${this.escapeXml(visiblePlaceholder)}</text>`;
5005
+ }
5006
+ svg += "\n </g>";
5007
+ return svg;
4310
5008
  }
4311
5009
  renderTopbar(node, pos) {
4312
5010
  const title = String(node.props.title || "App");
4313
5011
  const subtitle = String(node.props.subtitle || "");
4314
5012
  const actions = String(node.props.actions || "");
4315
5013
  const user = String(node.props.user || "");
4316
- const accentColor = this.resolveAccentColor();
5014
+ const variant = String(node.props.variant || "default");
5015
+ const accentColor = variant === "default" ? this.resolveAccentColor() : this.resolveVariantColor(variant, this.resolveAccentColor());
5016
+ const showBorder = this.parseBooleanProp(node.props.border, false);
5017
+ const showBackground = this.parseBooleanProp(node.props.background ?? node.props.backround, false);
5018
+ const radiusMap = {
5019
+ none: 0,
5020
+ sm: 4,
5021
+ md: this.tokens.card.radius,
5022
+ lg: 12,
5023
+ xl: 16
5024
+ };
5025
+ const topbarRadius = radiusMap[String(node.props.radius || "md")] ?? this.tokens.card.radius;
4317
5026
  const topbar = this.calculateTopbarLayout(node, pos, title, subtitle, actions, user);
4318
- let svg = `<g${this.getDataNodeId(node)}>
5027
+ let svg = `<g${this.getDataNodeId(node)}>`;
5028
+ if (showBorder || showBackground) {
5029
+ const bg = showBackground ? this.renderTheme.cardBg : "none";
5030
+ const stroke = showBorder ? this.renderTheme.border : "none";
5031
+ svg += `
4319
5032
  <rect x="${pos.x}" y="${pos.y}"
4320
5033
  width="${pos.width}" height="${pos.height}"
4321
- fill="${this.renderTheme.cardBg}"
4322
- stroke="${this.renderTheme.border}"
4323
- stroke-width="1"/>
4324
-
5034
+ rx="${topbarRadius}"
5035
+ fill="${bg}"
5036
+ stroke="${stroke}"
5037
+ stroke-width="1"/>`;
5038
+ }
5039
+ svg += `
4325
5040
  <!-- Title -->
4326
5041
  <text x="${topbar.textX}" y="${topbar.titleY}"
4327
- font-family="system-ui, -apple-system, sans-serif"
5042
+ font-family="Arial, Helvetica, sans-serif"
4328
5043
  font-size="18"
4329
5044
  font-weight="600"
4330
5045
  fill="${this.renderTheme.text}">${this.escapeXml(topbar.visibleTitle)}</text>`;
4331
5046
  if (topbar.hasSubtitle) {
4332
5047
  svg += `
4333
5048
  <text x="${topbar.textX}" y="${topbar.subtitleY}"
4334
- font-family="system-ui, -apple-system, sans-serif"
5049
+ font-family="Arial, Helvetica, sans-serif"
4335
5050
  font-size="13"
4336
5051
  fill="${this.renderTheme.textMuted}">${this.escapeXml(topbar.visibleSubtitle)}</text>`;
4337
5052
  }
@@ -4359,7 +5074,7 @@ var SVGRenderer = class {
4359
5074
  fill="${accentColor}"
4360
5075
  stroke="none"/>
4361
5076
  <text x="${action.x + action.width / 2}" y="${action.y + action.height / 2 + 4}"
4362
- font-family="system-ui, -apple-system, sans-serif"
5077
+ font-family="Arial, Helvetica, sans-serif"
4363
5078
  font-size="12"
4364
5079
  font-weight="600"
4365
5080
  fill="white"
@@ -4375,7 +5090,7 @@ var SVGRenderer = class {
4375
5090
  stroke="${this.renderTheme.border}"
4376
5091
  stroke-width="1"/>
4377
5092
  <text x="${topbar.userBadge.x + topbar.userBadge.width / 2}" y="${topbar.userBadge.y + topbar.userBadge.height / 2 + 4}"
4378
- font-family="system-ui, -apple-system, sans-serif"
5093
+ font-family="Arial, Helvetica, sans-serif"
4379
5094
  font-size="12"
4380
5095
  fill="${this.renderTheme.text}"
4381
5096
  text-anchor="middle">${this.escapeXml(topbar.userBadge.label)}</text>`;
@@ -4438,77 +5153,165 @@ var SVGRenderer = class {
4438
5153
  </g>`;
4439
5154
  output.push(svg);
4440
5155
  }
5156
+ renderSplitDecoration(node, pos, output) {
5157
+ if (node.kind !== "container") return;
5158
+ const gap = this.resolveSpacing(node.style.gap);
5159
+ const leftParam = Number(node.params.left);
5160
+ const rightParam = Number(node.params.right);
5161
+ const hasLeft = node.params.left !== void 0;
5162
+ const hasRight = node.params.right !== void 0 && node.params.left === void 0;
5163
+ const fixedLeftWidth = Number.isFinite(leftParam) && leftParam > 0 ? leftParam : 250;
5164
+ const fixedRightWidth = Number.isFinite(rightParam) && rightParam > 0 ? rightParam : 250;
5165
+ const backgroundKey = String(node.style.background || "").trim();
5166
+ const showBorder = this.parseBooleanProp(node.params.border, false);
5167
+ if (backgroundKey) {
5168
+ const fill = this.colorResolver.resolveColor(backgroundKey, this.renderTheme.cardBg);
5169
+ if (hasRight) {
5170
+ const panelX = pos.x + Math.max(0, pos.width - fixedRightWidth);
5171
+ output.push(`<g>
5172
+ <rect x="${panelX}" y="${pos.y}" width="${Math.max(1, fixedRightWidth)}" height="${pos.height}" fill="${fill}" stroke="none"/>
5173
+ </g>`);
5174
+ } else if (hasLeft || !hasRight) {
5175
+ output.push(`<g>
5176
+ <rect x="${pos.x}" y="${pos.y}" width="${Math.max(1, fixedLeftWidth)}" height="${pos.height}" fill="${fill}" stroke="none"/>
5177
+ </g>`);
5178
+ }
5179
+ }
5180
+ if (showBorder) {
5181
+ const dividerX = hasRight ? pos.x + Math.max(0, pos.width - fixedRightWidth - gap / 2) : pos.x + Math.max(0, fixedLeftWidth + gap / 2);
5182
+ output.push(`<g>
5183
+ <line x1="${dividerX}" y1="${pos.y}" x2="${dividerX}" y2="${pos.y + pos.height}" stroke="${this.renderTheme.border}" stroke-width="1"/>
5184
+ </g>`);
5185
+ }
5186
+ }
4441
5187
  renderTable(node, pos) {
4442
5188
  const title = String(node.props.title || "");
4443
5189
  const columnsStr = String(node.props.columns || "Col1,Col2,Col3");
4444
- const columns = columnsStr.split(",").map((c) => c.trim());
5190
+ const columns = columnsStr.split(",").map((c) => c.trim()).filter(Boolean);
4445
5191
  const rowCount = Number(node.props.rows || node.props.rowsMock || 5);
4446
5192
  const mockStr = String(node.props.mock || "");
4447
5193
  const random = this.parseBooleanProp(node.props.random, false);
4448
- const paginationValue = String(node.props.pagination || "false");
4449
- const pagination = paginationValue === "true";
4450
- const pageCount = Number(node.props.pages || 5);
5194
+ const pagination = this.parseBooleanProp(node.props.pagination, false);
5195
+ const parsedPageCount = Number(node.props.pages || 5);
5196
+ const pageCount = Number.isFinite(parsedPageCount) && parsedPageCount > 0 ? Math.floor(parsedPageCount) : 5;
4451
5197
  const paginationAlign = String(node.props.paginationAlign || "right");
5198
+ const actions = String(node.props.actions || "").split(",").map((value) => value.trim()).filter(Boolean);
5199
+ const hasActions = actions.length > 0;
5200
+ const caption = String(node.props.caption || "").trim();
5201
+ const hasCaption = caption.length > 0;
5202
+ const showOuterBorder = this.parseBooleanProp(node.props.border, false);
5203
+ const showOuterBackground = this.parseBooleanProp(
5204
+ node.props.background ?? node.props.backround,
5205
+ false
5206
+ );
5207
+ const showInnerBorder = this.parseBooleanProp(node.props.innerBorder, true);
5208
+ const rawCaptionAlign = String(node.props.captionAlign || "");
5209
+ const captionAlign = rawCaptionAlign === "left" || rawCaptionAlign === "center" || rawCaptionAlign === "right" ? rawCaptionAlign : paginationAlign === "left" ? "right" : "left";
5210
+ const sameFooterAlign = hasCaption && pagination && captionAlign === paginationAlign;
4452
5211
  const mockTypes = mockStr ? mockStr.split(",").map((m) => m.trim()).filter(Boolean) : [];
4453
- while (mockTypes.length < columns.length) {
4454
- const inferred = MockDataGenerator.inferMockTypeFromColumn(columns[mockTypes.length] || "item");
5212
+ const safeColumns = columns.length > 0 ? columns : ["Column"];
5213
+ while (mockTypes.length < safeColumns.length) {
5214
+ const inferred = MockDataGenerator.inferMockTypeFromColumn(safeColumns[mockTypes.length] || "item");
4455
5215
  mockTypes.push(inferred);
4456
5216
  }
4457
5217
  const headerHeight = 44;
4458
5218
  const rowHeight = 36;
4459
- const colWidth = pos.width / columns.length;
5219
+ const actionColumnWidth = hasActions ? Math.max(96, Math.min(180, actions.length * 26 + 28)) : 0;
5220
+ const dataWidth = Math.max(20, pos.width - actionColumnWidth);
5221
+ const dataColWidth = dataWidth / safeColumns.length;
4460
5222
  const mockRows = [];
4461
5223
  for (let rowIdx = 0; rowIdx < rowCount; rowIdx++) {
4462
5224
  const row = {};
4463
- columns.forEach((col, colIdx) => {
5225
+ safeColumns.forEach((col, colIdx) => {
4464
5226
  const mockType = mockTypes[colIdx] || MockDataGenerator.inferMockTypeFromColumn(col) || "item";
4465
5227
  row[col] = MockDataGenerator.getMockValue(mockType, rowIdx, random);
4466
5228
  });
4467
5229
  mockRows.push(row);
4468
5230
  }
4469
- let svg = `<g${this.getDataNodeId(node)}>
5231
+ let svg = `<g${this.getDataNodeId(node)}>`;
5232
+ if (showOuterBorder || showOuterBackground) {
5233
+ const outerFill = showOuterBackground ? this.renderTheme.cardBg : "none";
5234
+ const outerStroke = showOuterBorder ? this.renderTheme.border : "none";
5235
+ svg += `
4470
5236
  <rect x="${pos.x}" y="${pos.y}"
4471
5237
  width="${pos.width}" height="${pos.height}"
4472
5238
  rx="8"
4473
- fill="${this.renderTheme.cardBg}"
4474
- stroke="${this.renderTheme.border}"
5239
+ fill="${outerFill}"
5240
+ stroke="${outerStroke}"
4475
5241
  stroke-width="1"/>`;
5242
+ }
4476
5243
  if (title) {
4477
5244
  svg += `
4478
5245
  <text x="${pos.x + 16}" y="${pos.y + 24}"
4479
- font-family="system-ui, -apple-system, sans-serif"
5246
+ font-family="Arial, Helvetica, sans-serif"
4480
5247
  font-size="13"
4481
5248
  font-weight="600"
4482
5249
  fill="${this.renderTheme.text}">${this.escapeXml(title)}</text>`;
4483
5250
  }
4484
5251
  const headerY = pos.y + (title ? 32 : 0);
4485
- svg += `
5252
+ if (showInnerBorder) {
5253
+ svg += `
4486
5254
  <line x1="${pos.x}" y1="${headerY + headerHeight}" x2="${pos.x + pos.width}" y2="${headerY + headerHeight}"
4487
5255
  stroke="${this.renderTheme.border}" stroke-width="1"/>`;
4488
- columns.forEach((col, i) => {
5256
+ }
5257
+ safeColumns.forEach((col, i) => {
4489
5258
  svg += `
4490
- <text x="${pos.x + i * colWidth + 12}" y="${headerY + 26}"
4491
- font-family="system-ui, -apple-system, sans-serif"
5259
+ <text x="${pos.x + i * dataColWidth + 12}" y="${headerY + 26}"
5260
+ font-family="Arial, Helvetica, sans-serif"
4492
5261
  font-size="11"
4493
5262
  font-weight="600"
4494
5263
  fill="${this.renderTheme.textMuted}">${this.escapeXml(col)}</text>`;
4495
5264
  });
5265
+ if (hasActions && showInnerBorder) {
5266
+ const dividerX = pos.x + dataWidth;
5267
+ svg += `
5268
+ <line x1="${dividerX}" y1="${headerY + headerHeight}" x2="${dividerX}" y2="${headerY + headerHeight + mockRows.length * rowHeight}"
5269
+ stroke="${this.renderTheme.border}" stroke-width="1"/>`;
5270
+ }
4496
5271
  mockRows.forEach((row, rowIdx) => {
4497
5272
  const rowY = headerY + headerHeight + rowIdx * rowHeight;
4498
- svg += `
5273
+ if (showInnerBorder) {
5274
+ svg += `
4499
5275
  <line x1="${pos.x}" y1="${rowY + rowHeight}" x2="${pos.x + pos.width}" y2="${rowY + rowHeight}"
4500
5276
  stroke="${this.renderTheme.border}" stroke-width="0.5"/>`;
4501
- columns.forEach((col, colIdx) => {
5277
+ }
5278
+ safeColumns.forEach((col, colIdx) => {
4502
5279
  const cellValue = row[col] || "";
4503
5280
  svg += `
4504
- <text x="${pos.x + colIdx * colWidth + 12}" y="${rowY + 22}"
4505
- font-family="system-ui, -apple-system, sans-serif"
5281
+ <text x="${pos.x + colIdx * dataColWidth + 12}" y="${rowY + 22}"
5282
+ font-family="Arial, Helvetica, sans-serif"
4506
5283
  font-size="12"
4507
5284
  fill="${this.renderTheme.text}">${this.escapeXml(cellValue)}</text>`;
4508
5285
  });
5286
+ if (hasActions) {
5287
+ const iconSize = 14;
5288
+ const buttonSize = 22;
5289
+ const buttonGap = 6;
5290
+ const actionsWidth = actions.length * buttonSize + Math.max(0, actions.length - 1) * buttonGap;
5291
+ let currentX = pos.x + pos.width - 12 - actionsWidth;
5292
+ const buttonY = rowY + (rowHeight - buttonSize) / 2;
5293
+ actions.forEach((actionIcon) => {
5294
+ const iconSvg = getIcon(actionIcon);
5295
+ const iconX = currentX + (buttonSize - iconSize) / 2;
5296
+ const iconY = buttonY + (buttonSize - iconSize) / 2;
5297
+ svg += `
5298
+ <rect x="${currentX}" y="${buttonY}" width="${buttonSize}" height="${buttonSize}" rx="4"
5299
+ fill="${this.renderTheme.cardBg}" stroke="${showInnerBorder ? this.renderTheme.border : "none"}" stroke-width="1"/>`;
5300
+ if (iconSvg) {
5301
+ svg += `
5302
+ <g transform="translate(${iconX}, ${iconY})">
5303
+ <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">
5304
+ ${this.extractSvgContent(iconSvg)}
5305
+ </svg>
5306
+ </g>`;
5307
+ }
5308
+ currentX += buttonSize + buttonGap;
5309
+ });
5310
+ }
4509
5311
  });
5312
+ const footerTop = headerY + headerHeight + mockRows.length * rowHeight + 16;
4510
5313
  if (pagination) {
4511
- const paginationY = headerY + headerHeight + mockRows.length * rowHeight + 16;
5314
+ const paginationY = sameFooterAlign ? footerTop + 18 + 8 : footerTop;
4512
5315
  const buttonWidth = 40;
4513
5316
  const buttonHeight = 32;
4514
5317
  const gap = 8;
@@ -4521,18 +5324,25 @@ var SVGRenderer = class {
4521
5324
  } else {
4522
5325
  startX = pos.x + pos.width - totalWidth - 16;
4523
5326
  }
5327
+ const previousIcon = getIcon("chevron-left");
4524
5328
  svg += `
4525
5329
  <rect x="${startX}" y="${paginationY}"
4526
5330
  width="${buttonWidth}" height="${buttonHeight}"
4527
5331
  rx="4"
4528
5332
  fill="${this.renderTheme.cardBg}"
4529
5333
  stroke="${this.renderTheme.border}"
4530
- stroke-width="1"/>
4531
- <text x="${startX + buttonWidth / 2}" y="${paginationY + buttonHeight / 2 + 4}"
4532
- font-family="system-ui, -apple-system, sans-serif"
4533
- font-size="14"
4534
- fill="${this.renderTheme.text}"
4535
- text-anchor="middle">&lt;</text>`;
5334
+ stroke-width="1"/>`;
5335
+ if (previousIcon) {
5336
+ const iconSize = 14;
5337
+ const iconX = startX + (buttonWidth - iconSize) / 2;
5338
+ const iconY = paginationY + (buttonHeight - iconSize) / 2;
5339
+ svg += `
5340
+ <g transform="translate(${iconX}, ${iconY})">
5341
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${this.resolveTextColor()}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
5342
+ ${this.extractSvgContent(previousIcon)}
5343
+ </svg>
5344
+ </g>`;
5345
+ }
4536
5346
  for (let i = 1; i <= pageCount; i++) {
4537
5347
  const btnX = startX + (buttonWidth + gap) * i;
4538
5348
  const isActive = i === 1;
@@ -4546,24 +5356,49 @@ var SVGRenderer = class {
4546
5356
  stroke="${this.renderTheme.border}"
4547
5357
  stroke-width="1"/>
4548
5358
  <text x="${btnX + buttonWidth / 2}" y="${paginationY + buttonHeight / 2 + 4}"
4549
- font-family="system-ui, -apple-system, sans-serif"
5359
+ font-family="Arial, Helvetica, sans-serif"
4550
5360
  font-size="14"
4551
5361
  fill="${textColor}"
4552
5362
  text-anchor="middle">${i}</text>`;
4553
5363
  }
4554
5364
  const nextX = startX + (buttonWidth + gap) * (pageCount + 1);
5365
+ const nextIcon = getIcon("chevron-right");
4555
5366
  svg += `
4556
5367
  <rect x="${nextX}" y="${paginationY}"
4557
5368
  width="${buttonWidth}" height="${buttonHeight}"
4558
5369
  rx="4"
4559
5370
  fill="${this.renderTheme.cardBg}"
4560
5371
  stroke="${this.renderTheme.border}"
4561
- stroke-width="1"/>
4562
- <text x="${nextX + buttonWidth / 2}" y="${paginationY + buttonHeight / 2 + 4}"
4563
- font-family="system-ui, -apple-system, sans-serif"
4564
- font-size="14"
4565
- fill="${this.renderTheme.text}"
4566
- text-anchor="middle">&gt;</text>`;
5372
+ stroke-width="1"/>`;
5373
+ if (nextIcon) {
5374
+ const iconSize = 14;
5375
+ const iconX = nextX + (buttonWidth - iconSize) / 2;
5376
+ const iconY = paginationY + (buttonHeight - iconSize) / 2;
5377
+ svg += `
5378
+ <g transform="translate(${iconX}, ${iconY})">
5379
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${this.resolveTextColor()}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
5380
+ ${this.extractSvgContent(nextIcon)}
5381
+ </svg>
5382
+ </g>`;
5383
+ }
5384
+ }
5385
+ if (hasCaption) {
5386
+ const captionY = sameFooterAlign ? footerTop + 12 : footerTop + (pagination ? 21 : 12);
5387
+ let captionX = pos.x + 16;
5388
+ let textAnchor = "start";
5389
+ if (captionAlign === "center") {
5390
+ captionX = pos.x + pos.width / 2;
5391
+ textAnchor = "middle";
5392
+ } else if (captionAlign === "right") {
5393
+ captionX = pos.x + pos.width - 16;
5394
+ textAnchor = "end";
5395
+ }
5396
+ svg += `
5397
+ <text x="${captionX}" y="${captionY}"
5398
+ font-family="Arial, Helvetica, sans-serif"
5399
+ font-size="12"
5400
+ fill="${this.hexToRgba(this.resolveTextColor(), 0.75)}"
5401
+ text-anchor="${textAnchor}">${this.escapeXml(caption)}</text>`;
4567
5402
  }
4568
5403
  svg += "\n </g>";
4569
5404
  return svg;
@@ -4678,7 +5513,7 @@ var SVGRenderer = class {
4678
5513
  // TEXT/CONTENT COMPONENTS
4679
5514
  // ============================================================================
4680
5515
  renderText(node, pos) {
4681
- const text = String(node.props.content || "Text content");
5516
+ const text = String(node.props.text || "Text content");
4682
5517
  const fontSize = this.tokens.text.fontSize;
4683
5518
  const lineHeightPx = Math.ceil(fontSize * this.tokens.text.lineHeight);
4684
5519
  const lines = this.wrapTextToLines(text, pos.width, fontSize);
@@ -4688,7 +5523,7 @@ var SVGRenderer = class {
4688
5523
  ).join("");
4689
5524
  return `<g${this.getDataNodeId(node)}>
4690
5525
  <text x="${pos.x}" y="${firstLineY}"
4691
- font-family="system-ui, -apple-system, sans-serif"
5526
+ font-family="Arial, Helvetica, sans-serif"
4692
5527
  font-size="${fontSize}"
4693
5528
  fill="${this.renderTheme.text}">${tspans}</text>
4694
5529
  </g>`;
@@ -4697,7 +5532,7 @@ var SVGRenderer = class {
4697
5532
  const text = String(node.props.text || "Label");
4698
5533
  return `<g${this.getDataNodeId(node)}>
4699
5534
  <text x="${pos.x}" y="${pos.y + 12}"
4700
- font-family="system-ui, -apple-system, sans-serif"
5535
+ font-family="Arial, Helvetica, sans-serif"
4701
5536
  font-size="12"
4702
5537
  fill="${this.renderTheme.textMuted}">${this.escapeXml(text)}</text>
4703
5538
  </g>`;
@@ -4731,7 +5566,7 @@ var SVGRenderer = class {
4731
5566
  const placeholderY = controlY + fontSize + 6;
4732
5567
  return `<g${this.getDataNodeId(node)}>
4733
5568
  ${label ? `<text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
4734
- font-family="system-ui, -apple-system, sans-serif"
5569
+ font-family="Arial, Helvetica, sans-serif"
4735
5570
  font-size="12"
4736
5571
  fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>` : ""}
4737
5572
  <rect x="${pos.x}" y="${controlY}"
@@ -4741,7 +5576,7 @@ var SVGRenderer = class {
4741
5576
  stroke="${this.renderTheme.border}"
4742
5577
  stroke-width="1"/>
4743
5578
  <text x="${pos.x + paddingX}" y="${placeholderY}"
4744
- font-family="system-ui, -apple-system, sans-serif"
5579
+ font-family="Arial, Helvetica, sans-serif"
4745
5580
  font-size="${fontSize}"
4746
5581
  fill="${this.renderTheme.textMuted}">${this.escapeXml(placeholder)}</text>
4747
5582
  </g>`;
@@ -4749,30 +5584,66 @@ var SVGRenderer = class {
4749
5584
  renderSelect(node, pos) {
4750
5585
  const label = String(node.props.label || "");
4751
5586
  const placeholder = String(node.props.placeholder || "Select...");
5587
+ const iconLeftName = String(node.props.iconLeft || "").trim();
5588
+ const iconRightName = String(node.props.iconRight || "").trim();
4752
5589
  const labelOffset = this.getControlLabelOffset(label);
4753
5590
  const controlY = pos.y + labelOffset;
4754
5591
  const controlHeight = Math.max(16, pos.height - labelOffset);
4755
5592
  const centerY = controlY + controlHeight / 2 + 5;
4756
- return `<g${this.getDataNodeId(node)}>
4757
- ${label ? `<text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
4758
- font-family="system-ui, -apple-system, sans-serif"
4759
- font-size="12"
4760
- fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>` : ""}
4761
- <rect x="${pos.x}" y="${controlY}"
4762
- width="${pos.width}" height="${controlHeight}"
4763
- rx="6"
4764
- fill="${this.renderTheme.cardBg}"
4765
- stroke="${this.renderTheme.border}"
4766
- stroke-width="1"/>
4767
- <text x="${pos.x + 12}" y="${centerY}"
4768
- font-family="system-ui, -apple-system, sans-serif"
4769
- font-size="14"
4770
- fill="${this.renderTheme.textMuted}">${this.escapeXml(placeholder)}</text>
4771
- <text x="${pos.x + pos.width - 20}" y="${centerY}"
4772
- font-family="system-ui, -apple-system, sans-serif"
4773
- font-size="16"
5593
+ const iconSize = 16;
5594
+ const iconPad = 12;
5595
+ const iconInnerGap = 8;
5596
+ const iconLeftSvg = iconLeftName ? getIcon(iconLeftName) : null;
5597
+ const iconRightSvg = iconRightName ? getIcon(iconRightName) : null;
5598
+ const leftOffset = iconLeftSvg ? iconPad + iconSize + iconInnerGap : 0;
5599
+ const chevronWidth = 20;
5600
+ const iconColor = this.hexToRgba(this.resolveMutedColor(), 0.8);
5601
+ const iconCenterY = controlY + (controlHeight - iconSize) / 2;
5602
+ let svg = `<g${this.getDataNodeId(node)}>`;
5603
+ if (label) {
5604
+ svg += `
5605
+ <text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
5606
+ font-family="Arial, Helvetica, sans-serif"
5607
+ font-size="12"
5608
+ fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>`;
5609
+ }
5610
+ svg += `
5611
+ <rect x="${pos.x}" y="${controlY}"
5612
+ width="${pos.width}" height="${controlHeight}"
5613
+ rx="6"
5614
+ fill="${this.renderTheme.cardBg}"
5615
+ stroke="${this.renderTheme.border}"
5616
+ stroke-width="1"/>`;
5617
+ if (iconLeftSvg) {
5618
+ svg += `
5619
+ <g transform="translate(${pos.x + iconPad}, ${iconCenterY})">
5620
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
5621
+ ${this.extractSvgContent(iconLeftSvg)}
5622
+ </svg>
5623
+ </g>`;
5624
+ }
5625
+ if (iconRightSvg) {
5626
+ svg += `
5627
+ <g transform="translate(${pos.x + pos.width - chevronWidth - iconPad - iconSize}, ${iconCenterY})">
5628
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
5629
+ ${this.extractSvgContent(iconRightSvg)}
5630
+ </svg>
5631
+ </g>`;
5632
+ }
5633
+ const textX = pos.x + (iconLeftSvg ? leftOffset : 12);
5634
+ const availPlaceholderWidth = pos.width - (iconLeftSvg ? leftOffset : 12) - chevronWidth - (iconRightSvg ? iconPad + iconSize + iconInnerGap : 0);
5635
+ const visiblePlaceholder = this.truncateTextToWidth(placeholder, Math.max(0, availPlaceholderWidth), 14);
5636
+ svg += `
5637
+ <text x="${textX}" y="${centerY}"
5638
+ font-family="Arial, Helvetica, sans-serif"
5639
+ font-size="14"
5640
+ fill="${this.renderTheme.textMuted}">${this.escapeXml(visiblePlaceholder)}</text>
5641
+ <text x="${pos.x + pos.width - 20}" y="${centerY}"
5642
+ font-family="Arial, Helvetica, sans-serif"
5643
+ font-size="16"
4774
5644
  fill="${this.renderTheme.textMuted}">\u25BC</text>
4775
5645
  </g>`;
5646
+ return svg;
4776
5647
  }
4777
5648
  renderCheckbox(node, pos) {
4778
5649
  const label = String(node.props.label || "Checkbox");
@@ -4788,12 +5659,12 @@ var SVGRenderer = class {
4788
5659
  stroke="${this.renderTheme.border}"
4789
5660
  stroke-width="1"/>
4790
5661
  ${checked ? `<text x="${pos.x + checkboxSize / 2}" y="${checkboxY + 14}"
4791
- font-family="system-ui, -apple-system, sans-serif"
5662
+ font-family="Arial, Helvetica, sans-serif"
4792
5663
  font-size="12"
4793
5664
  fill="white"
4794
5665
  text-anchor="middle">\u2713</text>` : ""}
4795
5666
  <text x="${pos.x + checkboxSize + 12}" y="${pos.y + pos.height / 2 + 5}"
4796
- font-family="system-ui, -apple-system, sans-serif"
5667
+ font-family="Arial, Helvetica, sans-serif"
4797
5668
  font-size="14"
4798
5669
  fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>
4799
5670
  </g>`;
@@ -4814,7 +5685,7 @@ var SVGRenderer = class {
4814
5685
  r="${radioSize / 3.5}"
4815
5686
  fill="${controlColor}"/>` : ""}
4816
5687
  <text x="${pos.x + radioSize + 12}" y="${pos.y + pos.height / 2 + 5}"
4817
- font-family="system-ui, -apple-system, sans-serif"
5688
+ font-family="Arial, Helvetica, sans-serif"
4818
5689
  font-size="14"
4819
5690
  fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>
4820
5691
  </g>`;
@@ -4836,7 +5707,7 @@ var SVGRenderer = class {
4836
5707
  r="8"
4837
5708
  fill="white"/>
4838
5709
  <text x="${pos.x + toggleWidth + 12}" y="${pos.y + pos.height / 2 + 5}"
4839
- font-family="system-ui, -apple-system, sans-serif"
5710
+ font-family="Arial, Helvetica, sans-serif"
4840
5711
  font-size="14"
4841
5712
  fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>
4842
5713
  </g>`;
@@ -4866,7 +5737,7 @@ var SVGRenderer = class {
4866
5737
  stroke-width="1"/>
4867
5738
  <!-- Title -->
4868
5739
  <text x="${pos.x + padding}" y="${pos.y + padding + 8}"
4869
- font-family="system-ui, -apple-system, sans-serif"
5740
+ font-family="Arial, Helvetica, sans-serif"
4870
5741
  font-size="14"
4871
5742
  font-weight="600"
4872
5743
  fill="${this.renderTheme.text}">${this.escapeXml(title)}</text>
@@ -4882,7 +5753,7 @@ var SVGRenderer = class {
4882
5753
  fill="${isActive ? this.renderTheme.primary : "transparent"}"
4883
5754
  stroke="none"/>
4884
5755
  <text x="${pos.x + 16}" y="${itemY + 22}"
4885
- font-family="system-ui, -apple-system, sans-serif"
5756
+ font-family="Arial, Helvetica, sans-serif"
4886
5757
  font-size="13"
4887
5758
  fill="${isActive ? "white" : this.renderTheme.textMuted}">${this.escapeXml(item)}</text>`;
4888
5759
  });
@@ -4908,7 +5779,7 @@ var SVGRenderer = class {
4908
5779
  stroke="${isActive ? "none" : this.renderTheme.border}"
4909
5780
  stroke-width="1"/>
4910
5781
  <text x="${tabX + tabWidth / 2}" y="${pos.y + 28}"
4911
- font-family="system-ui, -apple-system, sans-serif"
5782
+ font-family="Arial, Helvetica, sans-serif"
4912
5783
  font-size="13"
4913
5784
  font-weight="${isActive ? "600" : "500"}"
4914
5785
  fill="${isActive ? "white" : this.renderTheme.text}"
@@ -4971,12 +5842,12 @@ var SVGRenderer = class {
4971
5842
  rx="6"
4972
5843
  fill="${bgColor}"/>
4973
5844
  ${hasTitle ? `<text x="${contentX}" y="${titleStartY}"
4974
- font-family="system-ui, -apple-system, sans-serif"
5845
+ font-family="Arial, Helvetica, sans-serif"
4975
5846
  font-size="${fontSize}"
4976
5847
  font-weight="700"
4977
5848
  fill="${bgColor}">${titleTspans}</text>` : ""}
4978
5849
  <text x="${contentX}" y="${textStartY}"
4979
- font-family="system-ui, -apple-system, sans-serif"
5850
+ font-family="Arial, Helvetica, sans-serif"
4980
5851
  font-size="${fontSize}"
4981
5852
  fill="${bgColor}">${textTspans}</text>
4982
5853
  </g>`;
@@ -4997,7 +5868,7 @@ var SVGRenderer = class {
4997
5868
  fill="${bgColor}"
4998
5869
  stroke="none"/>
4999
5870
  <text x="${pos.x + pos.width / 2}" y="${pos.y + pos.height / 2 + 4}"
5000
- font-family="system-ui, -apple-system, sans-serif"
5871
+ font-family="Arial, Helvetica, sans-serif"
5001
5872
  font-size="${fontSize}"
5002
5873
  font-weight="600"
5003
5874
  fill="${textColor}"
@@ -5036,20 +5907,20 @@ var SVGRenderer = class {
5036
5907
  stroke-width="1"/>
5037
5908
 
5038
5909
  <text x="${modalX + padding}" y="${modalY + padding + 16}"
5039
- font-family="system-ui, -apple-system, sans-serif"
5910
+ font-family="Arial, Helvetica, sans-serif"
5040
5911
  font-size="16"
5041
5912
  font-weight="600"
5042
5913
  fill="${this.renderTheme.text}">${this.escapeXml(title)}</text>
5043
5914
 
5044
5915
  <!-- Close button -->
5045
5916
  <text x="${modalX + pos.width - 16}" y="${modalY + padding + 12}"
5046
- font-family="system-ui, -apple-system, sans-serif"
5917
+ font-family="Arial, Helvetica, sans-serif"
5047
5918
  font-size="18"
5048
5919
  fill="${this.renderTheme.textMuted}">\u2715</text>
5049
5920
 
5050
5921
  <!-- Content placeholder -->
5051
5922
  <text x="${modalX + pos.width / 2}" y="${modalY + headerHeight + (pos.height - headerHeight) / 2}"
5052
- font-family="system-ui, -apple-system, sans-serif"
5923
+ font-family="Arial, Helvetica, sans-serif"
5053
5924
  font-size="13"
5054
5925
  fill="${this.renderTheme.textMuted}"
5055
5926
  text-anchor="middle">Modal content</text>
@@ -5082,7 +5953,7 @@ var SVGRenderer = class {
5082
5953
  if (title) {
5083
5954
  svg += `
5084
5955
  <text x="${pos.x + padding}" y="${pos.y + 26}"
5085
- font-family="system-ui, -apple-system, sans-serif"
5956
+ font-family="Arial, Helvetica, sans-serif"
5086
5957
  font-size="13"
5087
5958
  font-weight="600"
5088
5959
  fill="${this.renderTheme.text}">${this.escapeXml(title)}</text>
@@ -5098,7 +5969,7 @@ var SVGRenderer = class {
5098
5969
  stroke="${this.renderTheme.border}"
5099
5970
  stroke-width="0.5"/>
5100
5971
  <text x="${pos.x + padding}" y="${itemY + 24}"
5101
- font-family="system-ui, -apple-system, sans-serif"
5972
+ font-family="Arial, Helvetica, sans-serif"
5102
5973
  font-size="13"
5103
5974
  fill="${this.renderTheme.text}">${this.escapeXml(item)}</text>`;
5104
5975
  }
@@ -5116,7 +5987,7 @@ var SVGRenderer = class {
5116
5987
  stroke-width="1"
5117
5988
  stroke-dasharray="4 4"/>
5118
5989
  <text x="${pos.x + pos.width / 2}" y="${pos.y + pos.height / 2}"
5119
- font-family="system-ui, -apple-system, sans-serif"
5990
+ font-family="Arial, Helvetica, sans-serif"
5120
5991
  font-size="12"
5121
5992
  fill="${this.renderTheme.textMuted}"
5122
5993
  text-anchor="middle">${node.componentType}</text>
@@ -5164,14 +6035,14 @@ var SVGRenderer = class {
5164
6035
 
5165
6036
  <!-- Title -->
5166
6037
  <text x="${innerX}" y="${titleY}"
5167
- font-family="system-ui, -apple-system, sans-serif"
6038
+ font-family="Arial, Helvetica, sans-serif"
5168
6039
  font-size="${titleSize}"
5169
6040
  font-weight="500"
5170
6041
  fill="${this.renderTheme.textMuted}">${this.escapeXml(visibleTitle)}</text>
5171
6042
 
5172
6043
  <!-- Value (Large) -->
5173
6044
  <text x="${innerX}" y="${valueY}"
5174
- font-family="system-ui, -apple-system, sans-serif"
6045
+ font-family="Arial, Helvetica, sans-serif"
5175
6046
  font-size="${valueSize}"
5176
6047
  font-weight="700"
5177
6048
  fill="${accentColor}">${this.escapeXml(value)}</text>`;
@@ -5194,7 +6065,7 @@ var SVGRenderer = class {
5194
6065
  svg += `
5195
6066
  <!-- Caption -->
5196
6067
  <text x="${innerX}" y="${captionY}"
5197
- font-family="system-ui, -apple-system, sans-serif"
6068
+ font-family="Arial, Helvetica, sans-serif"
5198
6069
  font-size="${captionSize}"
5199
6070
  fill="${this.renderTheme.textMuted}">${this.escapeXml(visibleCaption)}</text>`;
5200
6071
  }
@@ -5205,7 +6076,9 @@ var SVGRenderer = class {
5205
6076
  renderImage(node, pos) {
5206
6077
  const placeholder = String(node.props.placeholder || "landscape").toLowerCase();
5207
6078
  const placeholderIcon = String(node.props.icon || "").trim();
6079
+ const variant = String(node.props.variant || "").trim();
5208
6080
  const placeholderIconSvg = placeholder === "icon" && placeholderIcon ? getIcon(placeholderIcon) : null;
6081
+ const imageBg = this.options.theme === "dark" ? "#2A2A2A" : "#E8E8E8";
5209
6082
  const aspectRatios = {
5210
6083
  landscape: 16 / 9,
5211
6084
  portrait: 2 / 3,
@@ -5223,30 +6096,29 @@ var SVGRenderer = class {
5223
6096
  }
5224
6097
  const offsetX = pos.x + (pos.width - iconWidth) / 2;
5225
6098
  const offsetY = pos.y + (pos.height - iconHeight) / 2;
5226
- let svg = `<g${this.getDataNodeId(node)}>
5227
- <!-- Image Background -->
5228
- <rect x="${pos.x}" y="${pos.y}" width="${pos.width}" height="${pos.height}" fill="#E8E8E8"/>`;
5229
6099
  if (placeholder === "icon" && placeholderIconSvg) {
5230
- const badgeSize = Math.max(24, Math.min(iconWidth, iconHeight) * 0.78);
5231
- const badgeX = pos.x + (pos.width - badgeSize) / 2;
5232
- const badgeY = pos.y + (pos.height - badgeSize) / 2;
5233
- const iconSize = badgeSize * 0.62;
5234
- const iconOffsetX = badgeX + (badgeSize - iconSize) / 2;
5235
- const iconOffsetY = badgeY + (badgeSize - iconSize) / 2;
5236
- svg += `
5237
- <!-- Custom Icon Placeholder -->
5238
- <rect x="${badgeX}" y="${badgeY}"
5239
- width="${badgeSize}" height="${badgeSize}"
5240
- rx="${Math.max(4, badgeSize * 0.2)}"
5241
- fill="rgba(255, 255, 255, 0.6)"
5242
- stroke="#888"
5243
- stroke-width="1"/>
6100
+ const semanticBase = variant ? this.getSemanticVariantColor(variant) : void 0;
6101
+ const hasVariant = variant.length > 0 && (semanticBase !== void 0 || this.colorResolver.hasColor(variant));
6102
+ const variantColor = hasVariant ? this.resolveVariantColor(variant, this.renderTheme.primary) : null;
6103
+ const bgColor = hasVariant ? this.hexToRgba(variantColor, 0.12) : imageBg;
6104
+ const iconColor = hasVariant ? variantColor : this.options.theme === "dark" ? "#888888" : "#666666";
6105
+ const iconSize = Math.max(16, Math.min(pos.width, pos.height) * 0.6);
6106
+ const iconOffsetX = pos.x + (pos.width - iconSize) / 2;
6107
+ const iconOffsetY = pos.y + (pos.height - iconSize) / 2;
6108
+ return `<g${this.getDataNodeId(node)}>
6109
+ <rect x="${pos.x}" y="${pos.y}" width="${pos.width}" height="${pos.height}" fill="${bgColor}" rx="4"/>
5244
6110
  <g transform="translate(${iconOffsetX}, ${iconOffsetY})">
5245
- <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="#555" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
6111
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
5246
6112
  ${this.extractSvgContent(placeholderIconSvg)}
5247
6113
  </svg>
5248
- </g>`;
5249
- } else if (["landscape", "portrait", "square"].includes(placeholder)) {
6114
+ </g>
6115
+ <rect x="${pos.x}" y="${pos.y}" width="${pos.width}" height="${pos.height}" fill="none" stroke="${this.renderTheme.border}" stroke-width="1" rx="4"/>
6116
+ </g>`;
6117
+ }
6118
+ let svg = `<g${this.getDataNodeId(node)}>
6119
+ <!-- Image Background -->
6120
+ <rect x="${pos.x}" y="${pos.y}" width="${pos.width}" height="${pos.height}" fill="${imageBg}"/>`;
6121
+ if (["landscape", "portrait", "square"].includes(placeholder)) {
5250
6122
  const cameraCx = offsetX + iconWidth / 2;
5251
6123
  const cameraCy = offsetY + iconHeight / 2;
5252
6124
  const scale = Math.min(iconWidth, iconHeight) / 24;
@@ -5323,7 +6195,7 @@ var SVGRenderer = class {
5323
6195
  const fontWeight = isLast ? "500" : "400";
5324
6196
  svg += `
5325
6197
  <text x="${currentX}" y="${pos.y + pos.height / 2 + 4}"
5326
- font-family="system-ui, -apple-system, sans-serif"
6198
+ font-family="Arial, Helvetica, sans-serif"
5327
6199
  font-size="${fontSize}"
5328
6200
  font-weight="${fontWeight}"
5329
6201
  fill="${textColor}">${this.escapeXml(item)}</text>`;
@@ -5332,7 +6204,7 @@ var SVGRenderer = class {
5332
6204
  if (!isLast) {
5333
6205
  svg += `
5334
6206
  <text x="${currentX + 4}" y="${pos.y + pos.height / 2 + 4}"
5335
- font-family="system-ui, -apple-system, sans-serif"
6207
+ font-family="Arial, Helvetica, sans-serif"
5336
6208
  font-size="${fontSize}"
5337
6209
  fill="${this.renderTheme.textMuted}">${this.escapeXml(separator)}</text>`;
5338
6210
  currentX += separatorWidth;
@@ -5350,18 +6222,22 @@ var SVGRenderer = class {
5350
6222
  const fontSize = 14;
5351
6223
  const activeIndex = Number(node.props.active || 0);
5352
6224
  const accentColor = this.resolveAccentColor();
6225
+ const variantProp = String(node.props.variant || "").trim();
6226
+ const semanticVariant = variantProp ? this.getSemanticVariantColor(variantProp) : void 0;
6227
+ const hasVariant = variantProp.length > 0 && (semanticVariant !== void 0 || this.colorResolver.hasColor(variantProp));
6228
+ const activeColor = hasVariant ? this.resolveVariantColor(variantProp, accentColor) : accentColor;
5353
6229
  let svg = `<g${this.getDataNodeId(node)}>`;
5354
6230
  items.forEach((item, index) => {
5355
6231
  const itemY = pos.y + index * itemHeight;
5356
6232
  const isActive = index === activeIndex;
5357
- const bgColor = isActive ? this.hexToRgba(accentColor, 0.15) : "transparent";
5358
- const textColor = isActive ? this.hexToRgba(accentColor, 0.9) : "rgba(30, 41, 59, 0.75)";
6233
+ const bgColor = isActive ? this.hexToRgba(activeColor, 0.15) : "transparent";
6234
+ const textColor = isActive ? this.hexToRgba(activeColor, 0.9) : this.hexToRgba(this.resolveTextColor(), 0.75);
5359
6235
  const fontWeight = isActive ? "500" : "400";
5360
6236
  if (isActive) {
5361
6237
  svg += `
5362
- <rect x="${pos.x}" y="${itemY}"
5363
- width="${pos.width}" height="${itemHeight}"
5364
- rx="6"
6238
+ <rect x="${pos.x}" y="${itemY}"
6239
+ width="${pos.width}" height="${itemHeight}"
6240
+ rx="6"
5365
6241
  fill="${bgColor}"/>`;
5366
6242
  }
5367
6243
  let currentX = pos.x + 12;
@@ -5370,9 +6246,10 @@ var SVGRenderer = class {
5370
6246
  if (iconSvg) {
5371
6247
  const iconSize = 16;
5372
6248
  const iconY = itemY + (itemHeight - iconSize) / 2;
6249
+ const iconColor = isActive ? this.hexToRgba(activeColor, 0.9) : this.hexToRgba(this.resolveMutedColor(), 0.9);
5373
6250
  svg += `
5374
6251
  <g transform="translate(${currentX}, ${iconY})">
5375
- <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">
6252
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
5376
6253
  ${this.extractSvgContent(iconSvg)}
5377
6254
  </svg>
5378
6255
  </g>`;
@@ -5380,28 +6257,29 @@ var SVGRenderer = class {
5380
6257
  }
5381
6258
  }
5382
6259
  svg += `
5383
- <text x="${currentX}" y="${itemY + itemHeight / 2 + 5}"
5384
- font-family="system-ui, -apple-system, sans-serif"
5385
- font-size="${fontSize}"
5386
- font-weight="${fontWeight}"
6260
+ <text x="${currentX}" y="${itemY + itemHeight / 2 + 5}"
6261
+ font-family="Arial, Helvetica, sans-serif"
6262
+ font-size="${fontSize}"
6263
+ font-weight="${fontWeight}"
5387
6264
  fill="${textColor}">${this.escapeXml(item)}</text>`;
5388
6265
  });
5389
6266
  svg += "\n </g>";
5390
6267
  return svg;
5391
6268
  }
5392
6269
  renderIcon(node, pos) {
5393
- const iconType = String(node.props.type || "help-circle");
6270
+ const iconType = String(node.props.icon || "help-circle");
5394
6271
  const size = String(node.props.size || "md");
6272
+ const variant = String(node.props.variant || "default");
5395
6273
  const iconSvg = getIcon(iconType);
6274
+ const iconColor = variant === "default" ? this.hexToRgba(this.resolveTextColor(), 0.75) : this.resolveVariantColor(variant, this.resolveTextColor());
5396
6275
  if (!iconSvg) {
5397
6276
  return `<g${this.getDataNodeId(node)}>
5398
6277
  <!-- Icon not found: ${iconType} -->
5399
- <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"/>
5400
- <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>
6278
+ <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"/>
6279
+ <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>
5401
6280
  </g>`;
5402
6281
  }
5403
6282
  const iconSize = this.getIconSize(size);
5404
- const iconColor = "rgba(30, 41, 59, 0.75)";
5405
6283
  const offsetX = pos.x + (pos.width - iconSize) / 2;
5406
6284
  const offsetY = pos.y + (pos.height - iconSize) / 2;
5407
6285
  const wrappedSvg = `<g${this.getDataNodeId(node)} transform="translate(${offsetX}, ${offsetY})">
@@ -5416,23 +6294,32 @@ var SVGRenderer = class {
5416
6294
  const variant = String(node.props.variant || "default");
5417
6295
  const size = String(node.props.size || "md");
5418
6296
  const disabled = String(node.props.disabled || "false") === "true";
6297
+ const density = this.ir.project.style.density || "normal";
6298
+ const labelOffset = this.parseBooleanProp(node.props.labelSpace, false) ? 18 : 0;
6299
+ const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
5419
6300
  const semanticBase = this.getSemanticVariantColor(variant);
5420
6301
  const hasExplicitVariantColor = semanticBase !== void 0 || this.colorResolver.hasColor(variant);
5421
6302
  const resolvedBase = this.resolveVariantColor(variant, this.renderTheme.primary);
5422
- const bgColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.85) : "rgba(226, 232, 240, 0.9)";
5423
- const iconColor = hasExplicitVariantColor ? "#FFFFFF" : "rgba(30, 41, 59, 0.75)";
5424
- const borderColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.7) : "rgba(100, 116, 139, 0.4)";
6303
+ const isDarkMode = this.options.theme === "dark";
6304
+ const bgColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.85) : isDarkMode ? "rgba(48, 48, 55, 0.9)" : "rgba(226, 232, 240, 0.9)";
6305
+ const iconColor = hasExplicitVariantColor ? "#FFFFFF" : this.hexToRgba(this.resolveTextColor(), 0.75);
6306
+ const borderColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.7) : isDarkMode ? "rgba(75, 75, 88, 0.8)" : "rgba(100, 116, 139, 0.4)";
5425
6307
  const opacity = disabled ? "0.5" : "1";
5426
6308
  const iconSvg = getIcon(iconName);
5427
- const buttonSize = this.getIconButtonSize(size);
6309
+ const buttonSize = Math.max(
6310
+ 16,
6311
+ Math.min(resolveActionControlHeight(size, density), pos.height - labelOffset)
6312
+ );
6313
+ const buttonWidth = buttonSize + extraPadding * 2;
5428
6314
  const radius = 6;
6315
+ const buttonY = pos.y + labelOffset;
5429
6316
  let svg = `<g${this.getDataNodeId(node)} opacity="${opacity}">
5430
6317
  <!-- IconButton background -->
5431
- <rect x="${pos.x}" y="${pos.y}" width="${buttonSize}" height="${buttonSize}" rx="${radius}" fill="${bgColor}" stroke="${borderColor}" stroke-width="1"/>`;
6318
+ <rect x="${pos.x}" y="${buttonY}" width="${buttonWidth}" height="${buttonSize}" rx="${radius}" fill="${bgColor}" stroke="${borderColor}" stroke-width="1"/>`;
5432
6319
  if (iconSvg) {
5433
6320
  const iconSize = buttonSize * 0.6;
5434
- const offsetX = pos.x + (buttonSize - iconSize) / 2;
5435
- const offsetY = pos.y + (buttonSize - iconSize) / 2;
6321
+ const offsetX = pos.x + (buttonWidth - iconSize) / 2;
6322
+ const offsetY = buttonY + (buttonSize - iconSize) / 2;
5436
6323
  svg += `
5437
6324
  <!-- Icon -->
5438
6325
  <g transform="translate(${offsetX}, ${offsetY})">
@@ -5465,15 +6352,46 @@ var SVGRenderer = class {
5465
6352
  resolveChartColor() {
5466
6353
  return this.colorResolver.resolveColor("chart", this.renderTheme.primary);
5467
6354
  }
6355
+ resolveTextColor() {
6356
+ const fallback = this.options.theme === "dark" ? "#FFFFFF" : "#000000";
6357
+ return this.colorResolver.resolveColor("text", fallback);
6358
+ }
6359
+ resolveMutedColor() {
6360
+ const fallback = this.options.theme === "dark" ? "#94A3B8" : "#64748B";
6361
+ return this.colorResolver.resolveColor("muted", fallback);
6362
+ }
5468
6363
  getSemanticVariantColor(variant) {
5469
- const semantic = {
6364
+ const isDark = this.options.theme === "dark";
6365
+ const semantic = isDark ? {
6366
+ // Muted mid-range — readable on #111111 without being neon
6367
+ primary: this.renderTheme.primary,
6368
+ // already theme-aware (#60A5FA)
6369
+ secondary: "#7E8EA2",
6370
+ // desaturated slate
6371
+ success: "#22A06B",
6372
+ // muted emerald
6373
+ warning: "#B38010",
6374
+ // deep amber
6375
+ danger: "#CC4444",
6376
+ // muted red
6377
+ error: "#CC4444",
6378
+ info: "#2485AF"
6379
+ // muted sky
6380
+ } : {
6381
+ // Tailwind 500-level — works on white/light backgrounds
5470
6382
  primary: this.renderTheme.primary,
6383
+ // #3B82F6
5471
6384
  secondary: "#64748B",
6385
+ // Slate 500
5472
6386
  success: "#10B981",
6387
+ // Emerald 500
5473
6388
  warning: "#F59E0B",
6389
+ // Amber 500
5474
6390
  danger: "#EF4444",
6391
+ // Red 500
5475
6392
  error: "#EF4444",
5476
6393
  info: "#0EA5E9"
6394
+ // Sky 500
5477
6395
  };
5478
6396
  return semantic[variant];
5479
6397
  }
@@ -5791,21 +6709,28 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
5791
6709
  renderButton(node, pos) {
5792
6710
  const text = String(node.props.text || "Button");
5793
6711
  const variant = String(node.props.variant || "default");
6712
+ const size = String(node.props.size || "md");
6713
+ const density = this.ir.project.style.density || "normal";
6714
+ const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
6715
+ const labelOffset = this.parseBooleanProp(node.props.labelSpace, false) ? 18 : 0;
5794
6716
  const fullWidth = this.shouldButtonFillAvailableWidth(node);
5795
6717
  const radius = this.tokens.button.radius;
5796
6718
  const fontSize = this.tokens.button.fontSize;
5797
6719
  const paddingX = this.tokens.button.paddingX;
5798
- const paddingY = this.tokens.button.paddingY;
6720
+ const buttonHeight = Math.max(
6721
+ 16,
6722
+ Math.min(resolveActionControlHeight(size, density), pos.height - labelOffset)
6723
+ );
6724
+ const buttonY = pos.y + labelOffset;
5799
6725
  const textWidth = text.length * fontSize * 0.6;
5800
- const buttonWidth = fullWidth ? Math.max(1, pos.width) : this.clampControlWidth(Math.max(textWidth + paddingX * 2, 60), pos.width);
5801
- const buttonHeight = fontSize + paddingY * 2;
6726
+ const buttonWidth = fullWidth ? Math.max(1, pos.width) : this.clampControlWidth(Math.max(textWidth + (paddingX + extraPadding) * 2, 60), pos.width);
5802
6727
  const semanticBase = this.getSemanticVariantColor(variant);
5803
6728
  const hasExplicitVariantColor = semanticBase !== void 0 || this.colorResolver.hasColor(variant);
5804
6729
  const resolvedBase = this.resolveVariantColor(variant, this.renderTheme.primary);
5805
6730
  const bgColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.85) : "rgba(226, 232, 240, 0.9)";
5806
6731
  const borderColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.7) : "rgba(100, 116, 139, 0.4)";
5807
6732
  return `<g${this.getDataNodeId(node)}>
5808
- <rect x="${pos.x}" y="${pos.y}"
6733
+ <rect x="${pos.x}" y="${buttonY}"
5809
6734
  width="${buttonWidth}" height="${buttonHeight}"
5810
6735
  rx="${radius}"
5811
6736
  fill="${bgColor}"
@@ -5819,13 +6744,14 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
5819
6744
  renderLink(node, pos) {
5820
6745
  const text = String(node.props.text || "Link");
5821
6746
  const variant = String(node.props.variant || "primary");
6747
+ const size = String(node.props.size || "md");
6748
+ const density = this.ir.project.style.density || "normal";
5822
6749
  const fontSize = this.tokens.button.fontSize;
5823
6750
  const paddingX = this.tokens.button.paddingX;
5824
- const paddingY = this.tokens.button.paddingY;
5825
6751
  const linkColor = this.resolveVariantColor(variant, this.renderTheme.primary);
5826
6752
  const textWidth = this.estimateTextWidth(text, fontSize);
5827
6753
  const linkWidth = this.clampControlWidth(Math.max(textWidth + paddingX * 2, 60), pos.width);
5828
- const linkHeight = fontSize + paddingY * 2;
6754
+ const linkHeight = Math.max(16, Math.min(resolveActionControlHeight(size, density), pos.height));
5829
6755
  const blockHeight = Math.max(8, Math.round(fontSize * 0.75));
5830
6756
  const blockWidth = Math.max(28, Math.min(textWidth, linkWidth - paddingX * 2));
5831
6757
  const blockX = pos.x + (linkWidth - blockWidth) / 2;
@@ -5842,17 +6768,70 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
5842
6768
  stroke-width="1"/>
5843
6769
  </g>`;
5844
6770
  }
6771
+ /**
6772
+ * Render breadcrumbs as skeleton blocks: <rect> / <rect> / <rect accent>
6773
+ */
6774
+ renderBreadcrumbs(node, pos) {
6775
+ const itemsStr = String(node.props.items || "Home");
6776
+ const items = itemsStr.split(",").map((s) => s.trim()).filter(Boolean);
6777
+ const separator = String(node.props.separator || "/");
6778
+ const blockColor = this.renderTheme.border;
6779
+ const charWidth = 6.2;
6780
+ const minBlockWidth = 28;
6781
+ const maxBlockWidth = Math.max(minBlockWidth, Math.floor(pos.width * 0.4));
6782
+ const blockHeight = 12;
6783
+ const blockY = pos.y + (pos.height - blockHeight) / 2;
6784
+ const blockRadius = 4;
6785
+ const blockPaddingX = 10;
6786
+ const itemSpacing = 8;
6787
+ const separatorWidth = 12;
6788
+ const contentRight = pos.x + pos.width;
6789
+ let currentX = pos.x;
6790
+ let svg = `<g${this.getDataNodeId(node)}>`;
6791
+ items.forEach((item, index) => {
6792
+ if (currentX >= contentRight) return;
6793
+ const isLast = index === items.length - 1;
6794
+ const estimatedTextWidth = item.length * charWidth;
6795
+ let blockWidth = Math.max(
6796
+ minBlockWidth,
6797
+ Math.min(maxBlockWidth, Math.ceil(estimatedTextWidth + blockPaddingX * 2))
6798
+ );
6799
+ blockWidth = Math.min(blockWidth, Math.max(0, contentRight - currentX));
6800
+ if (blockWidth < minBlockWidth) return;
6801
+ const fillColor = blockColor;
6802
+ svg += `
6803
+ <rect x="${currentX}" y="${blockY}"
6804
+ width="${blockWidth}" height="${blockHeight}"
6805
+ rx="${blockRadius}"
6806
+ fill="${fillColor}"
6807
+ stroke="none"/>`;
6808
+ currentX += blockWidth + itemSpacing;
6809
+ if (!isLast && currentX + separatorWidth <= contentRight) {
6810
+ svg += `
6811
+ <text x="${currentX + 2}" y="${pos.y + pos.height / 2 + 4}"
6812
+ font-family="Arial, Helvetica, sans-serif"
6813
+ font-size="12"
6814
+ fill="${blockColor}">${this.escapeXml(separator)}</text>`;
6815
+ currentX += separatorWidth;
6816
+ }
6817
+ });
6818
+ svg += "\n </g>";
6819
+ return svg;
6820
+ }
5845
6821
  /**
5846
6822
  * Render heading as gray block
5847
6823
  */
5848
6824
  renderHeading(node, pos) {
5849
6825
  const headingTypography = this.getHeadingTypography(node);
6826
+ const variant = String(node.props.variant || "default");
6827
+ const blockColor = variant === "default" ? this.renderTheme.border : this.hexToRgba(this.resolveVariantColor(variant, this.resolveTextColor()), 0.35);
5850
6828
  return this.renderTextBlock(
5851
6829
  node,
5852
6830
  pos,
5853
6831
  String(node.props.text || "Heading"),
5854
6832
  headingTypography.fontSize,
5855
- headingTypography.lineHeight
6833
+ headingTypography.lineHeight,
6834
+ blockColor
5856
6835
  );
5857
6836
  }
5858
6837
  /**
@@ -5862,7 +6841,7 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
5862
6841
  return this.renderTextBlock(
5863
6842
  node,
5864
6843
  pos,
5865
- String(node.props.content || "Text content"),
6844
+ String(node.props.text || "Text content"),
5866
6845
  this.tokens.text.fontSize,
5867
6846
  this.tokens.text.lineHeight
5868
6847
  );
@@ -5873,6 +6852,19 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
5873
6852
  renderLabel(node, pos) {
5874
6853
  return this.renderTextBlock(node, pos, String(node.props.text || "Label"), 12, 1.2);
5875
6854
  }
6855
+ /**
6856
+ * Render image as a plain skeleton rectangle — no icon, no placeholder label,
6857
+ * just a filled block with the correct dimensions (aspect-ratio is preserved
6858
+ * by the layout engine, so pos already has the right size).
6859
+ */
6860
+ renderImage(node, pos) {
6861
+ return `<g${this.getDataNodeId(node)}>
6862
+ <rect x="${pos.x}" y="${pos.y}"
6863
+ width="${pos.width}" height="${pos.height}"
6864
+ rx="4"
6865
+ fill="${this.renderTheme.border}"/>
6866
+ </g>`;
6867
+ }
5876
6868
  /**
5877
6869
  * Render badge as shape only (no text)
5878
6870
  */
@@ -6103,18 +7095,42 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
6103
7095
  renderTable(node, pos) {
6104
7096
  const title = String(node.props.title || "");
6105
7097
  const columnsStr = String(node.props.columns || "Col1,Col2,Col3");
6106
- const columns = columnsStr.split(",").map((c) => c.trim());
7098
+ const columns = columnsStr.split(",").map((c) => c.trim()).filter(Boolean);
6107
7099
  const rowCount = Number(node.props.rows || node.props.rowsMock || 5);
7100
+ const actions = String(node.props.actions || "").split(",").map((value) => value.trim()).filter(Boolean);
7101
+ const hasActions = actions.length > 0;
7102
+ const pagination = this.parseBooleanProp(node.props.pagination, false);
7103
+ const parsedPageCount = Number(node.props.pages || 5);
7104
+ const pageCount = Number.isFinite(parsedPageCount) && parsedPageCount > 0 ? Math.floor(parsedPageCount) : 5;
7105
+ const paginationAlign = String(node.props.paginationAlign || "right");
7106
+ const hasCaption = String(node.props.caption || "").trim().length > 0;
7107
+ const showOuterBorder = this.parseBooleanProp(node.props.border, false);
7108
+ const showOuterBackground = this.parseBooleanProp(
7109
+ node.props.background ?? node.props.backround,
7110
+ false
7111
+ );
7112
+ const showInnerBorder = this.parseBooleanProp(node.props.innerBorder, true);
7113
+ const rawCaptionAlign = String(node.props.captionAlign || "");
7114
+ const captionAlign = rawCaptionAlign === "left" || rawCaptionAlign === "center" || rawCaptionAlign === "right" ? rawCaptionAlign : paginationAlign === "left" ? "right" : "left";
7115
+ const sameFooterAlign = hasCaption && pagination && captionAlign === paginationAlign;
7116
+ const safeColumns = columns.length > 0 ? columns : ["Column"];
6108
7117
  const headerHeight = 44;
6109
7118
  const rowHeight = 36;
6110
- const colWidth = pos.width / columns.length;
6111
- let svg = `<g${this.getDataNodeId(node)}>
7119
+ const actionColumnWidth = hasActions ? Math.max(96, Math.min(180, actions.length * 26 + 28)) : 0;
7120
+ const dataWidth = Math.max(20, pos.width - actionColumnWidth);
7121
+ const colWidth = dataWidth / safeColumns.length;
7122
+ let svg = `<g${this.getDataNodeId(node)}>`;
7123
+ if (showOuterBorder || showOuterBackground) {
7124
+ const outerFill = showOuterBackground ? this.renderTheme.cardBg : "none";
7125
+ const outerStroke = showOuterBorder ? this.renderTheme.border : "none";
7126
+ svg += `
6112
7127
  <rect x="${pos.x}" y="${pos.y}"
6113
7128
  width="${pos.width}" height="${pos.height}"
6114
7129
  rx="8"
6115
- fill="${this.renderTheme.cardBg}"
6116
- stroke="${this.renderTheme.border}"
7130
+ fill="${outerFill}"
7131
+ stroke="${outerStroke}"
6117
7132
  stroke-width="1"/>`;
7133
+ }
6118
7134
  if (title) {
6119
7135
  svg += `<rect x="${pos.x + 16}" y="${pos.y + 12}"
6120
7136
  width="100" height="12"
@@ -6122,19 +7138,28 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
6122
7138
  fill="${this.renderTheme.border}"/>`;
6123
7139
  }
6124
7140
  const headerY = pos.y + (title ? 32 : 0);
6125
- svg += `<line x1="${pos.x}" y1="${headerY + headerHeight}" x2="${pos.x + pos.width}" y2="${headerY + headerHeight}"
7141
+ if (showInnerBorder) {
7142
+ svg += `<line x1="${pos.x}" y1="${headerY + headerHeight}" x2="${pos.x + pos.width}" y2="${headerY + headerHeight}"
6126
7143
  stroke="${this.renderTheme.border}" stroke-width="1"/>`;
6127
- columns.forEach((_, i) => {
7144
+ }
7145
+ safeColumns.forEach((_, i) => {
6128
7146
  svg += `<rect x="${pos.x + i * colWidth + 12}" y="${headerY + 16}"
6129
7147
  width="50" height="10"
6130
7148
  rx="4"
6131
7149
  fill="${this.renderTheme.border}"/>`;
6132
7150
  });
7151
+ if (hasActions && showInnerBorder) {
7152
+ const dividerX = pos.x + dataWidth;
7153
+ svg += `<line x1="${dividerX}" y1="${headerY + headerHeight}" x2="${dividerX}" y2="${headerY + headerHeight + rowCount * rowHeight}"
7154
+ stroke="${this.renderTheme.border}" stroke-width="1"/>`;
7155
+ }
6133
7156
  for (let rowIdx = 0; rowIdx < rowCount; rowIdx++) {
6134
7157
  const rowY = headerY + headerHeight + rowIdx * rowHeight;
6135
- svg += `<line x1="${pos.x}" y1="${rowY + rowHeight}" x2="${pos.x + pos.width}" y2="${rowY + rowHeight}"
7158
+ if (showInnerBorder) {
7159
+ svg += `<line x1="${pos.x}" y1="${rowY + rowHeight}" x2="${pos.x + pos.width}" y2="${rowY + rowHeight}"
6136
7160
  stroke="${this.renderTheme.border}" stroke-width="0.5"/>`;
6137
- columns.forEach((_, colIdx) => {
7161
+ }
7162
+ safeColumns.forEach((_, colIdx) => {
6138
7163
  const variance = (rowIdx * 17 + colIdx * 11) % 5 * 10;
6139
7164
  const blockWidth = Math.min(colWidth - 24, 60 + variance);
6140
7165
  svg += `<rect x="${pos.x + colIdx * colWidth + 12}" y="${rowY + 12}"
@@ -6142,6 +7167,45 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
6142
7167
  rx="4"
6143
7168
  fill="${this.renderTheme.border}"/>`;
6144
7169
  });
7170
+ if (hasActions) {
7171
+ const iconSize = 14;
7172
+ const iconGap = 8;
7173
+ const actionsWidth = actions.length * iconSize + Math.max(0, actions.length - 1) * iconGap;
7174
+ let currentX = pos.x + pos.width - 12 - actionsWidth;
7175
+ const iconY = rowY + (rowHeight - iconSize) / 2;
7176
+ actions.forEach(() => {
7177
+ svg += `<rect x="${currentX}" y="${iconY}" width="${iconSize}" height="${iconSize}" rx="3" fill="${this.renderTheme.border}"/>`;
7178
+ currentX += iconSize + iconGap;
7179
+ });
7180
+ }
7181
+ }
7182
+ const footerTop = headerY + headerHeight + rowCount * rowHeight + 16;
7183
+ if (hasCaption) {
7184
+ const captionY = sameFooterAlign ? footerTop : footerTop + (pagination ? 10 : 0);
7185
+ const captionWidth = Math.min(220, Math.max(90, pos.width * 0.34));
7186
+ let captionX = pos.x + 16;
7187
+ if (captionAlign === "center") {
7188
+ captionX = pos.x + (pos.width - captionWidth) / 2;
7189
+ } else if (captionAlign === "right") {
7190
+ captionX = pos.x + pos.width - 16 - captionWidth;
7191
+ }
7192
+ svg += `<rect x="${captionX}" y="${captionY}" width="${captionWidth}" height="10" rx="4" fill="${this.renderTheme.border}"/>`;
7193
+ }
7194
+ if (pagination) {
7195
+ const buttonWidth = 28;
7196
+ const buttonHeight = 24;
7197
+ const buttonGap = 8;
7198
+ const totalWidth = (pageCount + 2) * buttonWidth + (pageCount + 1) * buttonGap;
7199
+ const paginationY = sameFooterAlign ? footerTop + 18 : footerTop;
7200
+ let startX = pos.x + pos.width - totalWidth - 16;
7201
+ if (paginationAlign === "left") {
7202
+ startX = pos.x + 16;
7203
+ } else if (paginationAlign === "center") {
7204
+ startX = pos.x + (pos.width - totalWidth) / 2;
7205
+ }
7206
+ for (let i = 0; i < pageCount + 2; i++) {
7207
+ svg += `<rect x="${startX + i * (buttonWidth + buttonGap)}" y="${paginationY}" width="${buttonWidth}" height="${buttonHeight}" rx="4" fill="${this.renderTheme.border}"/>`;
7208
+ }
6145
7209
  }
6146
7210
  svg += "</g>";
6147
7211
  return svg;
@@ -6154,21 +7218,39 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
6154
7218
  const subtitle = String(node.props.subtitle || "");
6155
7219
  const actions = String(node.props.actions || "");
6156
7220
  const user = String(node.props.user || "");
7221
+ const variant = String(node.props.variant || "default");
7222
+ const accentBlock = variant === "default" ? this.renderTheme.border : this.hexToRgba(this.resolveVariantColor(variant, this.resolveAccentColor()), 0.35);
7223
+ const showBorder = this.parseBooleanProp(node.props.border, false);
7224
+ const showBackground = this.parseBooleanProp(node.props.background ?? node.props.backround, false);
7225
+ const radiusMap = {
7226
+ none: 0,
7227
+ sm: 4,
7228
+ md: this.tokens.card.radius,
7229
+ lg: 12,
7230
+ xl: 16
7231
+ };
7232
+ const topbarRadius = radiusMap[String(node.props.radius || "md")] ?? this.tokens.card.radius;
6157
7233
  const topbar = this.calculateTopbarLayout(node, pos, title, subtitle, actions, user);
6158
7234
  const titleWidth = Math.max(56, Math.min(topbar.titleMaxWidth * 0.55, topbar.titleMaxWidth));
6159
7235
  const subtitleWidth = Math.max(48, Math.min(topbar.titleMaxWidth * 0.4, topbar.titleMaxWidth));
6160
- let svg = `<g${this.getDataNodeId(node)}>
7236
+ let svg = `<g${this.getDataNodeId(node)}>`;
7237
+ if (showBorder || showBackground) {
7238
+ const bg = showBackground ? this.renderTheme.cardBg : "none";
7239
+ const stroke = showBorder ? this.renderTheme.border : "none";
7240
+ svg += `
6161
7241
  <rect x="${pos.x}" y="${pos.y}"
6162
7242
  width="${pos.width}" height="${pos.height}"
6163
- fill="${this.renderTheme.cardBg}"
6164
- stroke="${this.renderTheme.border}"
6165
- stroke-width="0 0 1 0"/>`;
7243
+ rx="${topbarRadius}"
7244
+ fill="${bg}"
7245
+ stroke="${stroke}"
7246
+ stroke-width="1"/>`;
7247
+ }
6166
7248
  if (topbar.leftIcon) {
6167
7249
  svg += `
6168
7250
  <rect x="${topbar.leftIcon.badgeX}" y="${topbar.leftIcon.badgeY}"
6169
7251
  width="${topbar.leftIcon.badgeSize}" height="${topbar.leftIcon.badgeSize}"
6170
7252
  rx="${topbar.leftIcon.badgeRadius}"
6171
- fill="${this.renderTheme.border}"/>`;
7253
+ fill="${accentBlock}"/>`;
6172
7254
  }
6173
7255
  svg += `
6174
7256
  <rect x="${topbar.textX}" y="${topbar.titleY - 12}"
@@ -6187,7 +7269,7 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
6187
7269
  <rect x="${action.x}" y="${action.y}"
6188
7270
  width="${action.width}" height="${action.height}"
6189
7271
  rx="6"
6190
- fill="${this.renderTheme.border}"/>`;
7272
+ fill="${accentBlock}"/>`;
6191
7273
  });
6192
7274
  if (topbar.userBadge) {
6193
7275
  svg += `
@@ -6255,12 +7337,14 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
6255
7337
  */
6256
7338
  renderIcon(node, pos) {
6257
7339
  const size = String(node.props.size || "md");
7340
+ const variant = String(node.props.variant || "default");
6258
7341
  const iconSize = this.getIconSize(size);
7342
+ const blockColor = variant === "default" ? this.renderTheme.border : this.hexToRgba(this.resolveVariantColor(variant, this.resolveTextColor()), 0.35);
6259
7343
  return `<g${this.getDataNodeId(node)}>
6260
7344
  <rect x="${pos.x}" y="${pos.y + (pos.height - iconSize) / 2}"
6261
7345
  width="${iconSize}" height="${iconSize}"
6262
7346
  rx="2"
6263
- fill="${this.renderTheme.border}"/>
7347
+ fill="${blockColor}"/>
6264
7348
  </g>`;
6265
7349
  }
6266
7350
  /**
@@ -6269,15 +7353,23 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
6269
7353
  renderIconButton(node, pos) {
6270
7354
  const variant = String(node.props.variant || "default");
6271
7355
  const size = String(node.props.size || "md");
7356
+ const density = this.ir.project.style.density || "normal";
7357
+ const labelOffset = this.parseBooleanProp(node.props.labelSpace, false) ? 18 : 0;
7358
+ const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
6272
7359
  const semanticBase = this.getSemanticVariantColor(variant);
6273
7360
  const hasExplicitVariantColor = semanticBase !== void 0 || this.colorResolver.hasColor(variant);
6274
7361
  const resolvedBase = this.resolveVariantColor(variant, this.renderTheme.primary);
6275
7362
  const bgColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.85) : "rgba(226, 232, 240, 0.9)";
6276
7363
  const borderColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.7) : "rgba(100, 116, 139, 0.4)";
6277
- const buttonSize = this.getIconButtonSize(size);
7364
+ const buttonSize = Math.max(
7365
+ 16,
7366
+ Math.min(resolveActionControlHeight(size, density), pos.height - labelOffset)
7367
+ );
7368
+ const buttonWidth = buttonSize + extraPadding * 2;
7369
+ const buttonY = pos.y + labelOffset;
6278
7370
  return `<g${this.getDataNodeId(node)}>
6279
- <rect x="${pos.x}" y="${pos.y}"
6280
- width="${buttonSize}" height="${buttonSize}"
7371
+ <rect x="${pos.x}" y="${buttonY}"
7372
+ width="${buttonWidth}" height="${buttonSize}"
6281
7373
  rx="6"
6282
7374
  fill="${bgColor}"
6283
7375
  stroke="${borderColor}"
@@ -6357,10 +7449,10 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
6357
7449
  /**
6358
7450
  * Private helper: Render text as gray block
6359
7451
  */
6360
- renderTextBlock(node, pos, text, fontSize, lineHeightMultiplier) {
7452
+ renderTextBlock(node, pos, text, fontSize, lineHeightMultiplier, color) {
6361
7453
  const lineHeight = Math.ceil(fontSize * lineHeightMultiplier);
6362
7454
  const blockHeight = Math.max(8, Math.round(fontSize * 0.75));
6363
- const blockColor = this.renderTheme.border;
7455
+ const blockColor = color || this.renderTheme.border;
6364
7456
  const lines = this.wrapTextToLines(text, pos.width, fontSize);
6365
7457
  const contentHeight = lines.length * lineHeight;
6366
7458
  const startY = pos.y + Math.max(0, (pos.height - contentHeight) / 2);
@@ -6442,38 +7534,78 @@ var SketchSVGRenderer = class extends SVGRenderer {
6442
7534
  renderButton(node, pos) {
6443
7535
  const text = String(node.props.text || "Button");
6444
7536
  const variant = String(node.props.variant || "default");
7537
+ const size = String(node.props.size || "md");
7538
+ const density = this.ir.project.style.density || "normal";
7539
+ const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
7540
+ const labelOffset = this.parseBooleanProp(node.props.labelSpace, false) ? 18 : 0;
6445
7541
  const fullWidth = this.shouldButtonFillAvailableWidth(node);
7542
+ const iconName = String(node.props.icon || "").trim();
7543
+ const iconAlign = String(node.props.iconAlign || "left").toLowerCase();
6446
7544
  const radius = this.tokens.button.radius;
6447
7545
  const fontSize = this.tokens.button.fontSize;
6448
7546
  const fontWeight = this.tokens.button.fontWeight;
6449
7547
  const paddingX = this.tokens.button.paddingX;
6450
- const paddingY = this.tokens.button.paddingY;
7548
+ const buttonHeight = Math.max(
7549
+ 16,
7550
+ Math.min(resolveControlHeight(size, density), pos.height - labelOffset)
7551
+ );
7552
+ const buttonY = pos.y + labelOffset;
7553
+ const iconSvg = iconName ? getIcon(iconName) : null;
7554
+ const iconSize = iconSvg ? Math.round(fontSize * 1.1) : 0;
7555
+ const iconGap = iconSvg ? 8 : 0;
7556
+ const edgePad = 12;
7557
+ const textPad = paddingX + extraPadding;
6451
7558
  const idealTextWidth = text.length * fontSize * 0.6;
6452
- const buttonWidth = fullWidth ? Math.max(1, pos.width) : this.clampControlWidth(Math.max(idealTextWidth + paddingX * 2, 60), pos.width);
6453
- const buttonHeight = fontSize + paddingY * 2;
6454
- const availableTextWidth = Math.max(0, buttonWidth - paddingX * 2);
7559
+ const buttonWidth = fullWidth ? Math.max(1, pos.width) : this.clampControlWidth(Math.max(idealTextWidth + (iconSvg ? iconSize + iconGap : 0) + textPad * 2, 60), pos.width);
7560
+ const availableTextWidth = Math.max(0, buttonWidth - textPad * 2 - (iconSvg ? iconSize + iconGap : 0));
6455
7561
  const visibleText = this.truncateTextToWidth(text, availableTextWidth, fontSize);
6456
7562
  const semanticBase = this.getSemanticVariantColor(variant);
6457
7563
  const hasExplicitVariantColor = semanticBase !== void 0 || this.colorResolver.hasColor(variant);
6458
- const variantColor = hasExplicitVariantColor ? this.resolveVariantColor(variant, this.renderTheme.primary) : "#2D3748";
7564
+ const variantColor = hasExplicitVariantColor ? this.resolveVariantColor(variant, this.renderTheme.primary) : this.resolveTextColor();
6459
7565
  const borderColor = variantColor;
6460
7566
  const textColor = variantColor;
6461
7567
  const strokeWidth = 0.5;
6462
- return `<g${this.getDataNodeId(node)}>
6463
- <rect x="${pos.x}" y="${pos.y}"
7568
+ const iconOffsetY = buttonY + (buttonHeight - iconSize) / 2;
7569
+ const iconX = iconAlign === "right" ? pos.x + buttonWidth - edgePad - iconSize : pos.x + edgePad;
7570
+ const textAlign = String(node.props.align || "center").toLowerCase();
7571
+ const sidePad = textPad + 4;
7572
+ let textX;
7573
+ let textAnchor;
7574
+ if (textAlign === "left") {
7575
+ textX = iconSvg && iconAlign === "left" ? pos.x + edgePad + iconSize + iconGap : pos.x + sidePad;
7576
+ textAnchor = "start";
7577
+ } else if (textAlign === "right") {
7578
+ textX = iconSvg && iconAlign === "right" ? pos.x + buttonWidth - edgePad - iconSize - iconGap : pos.x + buttonWidth - sidePad;
7579
+ textAnchor = "end";
7580
+ } else {
7581
+ textX = pos.x + buttonWidth / 2;
7582
+ textAnchor = "middle";
7583
+ }
7584
+ let svg = `<g${this.getDataNodeId(node)}>
7585
+ <rect x="${pos.x}" y="${buttonY}"
6464
7586
  width="${buttonWidth}" height="${buttonHeight}"
6465
7587
  rx="${radius}"
6466
7588
  fill="none"
6467
7589
  stroke="${borderColor}"
6468
7590
  stroke-width="${strokeWidth}"
6469
- filter="url(#sketch-rough)"/>
6470
- <text x="${pos.x + buttonWidth / 2}" y="${pos.y + buttonHeight / 2 + fontSize * 0.35}"
7591
+ filter="url(#sketch-rough)"/>`;
7592
+ if (iconSvg) {
7593
+ svg += `
7594
+ <g transform="translate(${iconX}, ${iconOffsetY})">
7595
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${textColor}" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round">
7596
+ ${this.extractSvgContent(iconSvg)}
7597
+ </svg>
7598
+ </g>`;
7599
+ }
7600
+ svg += `
7601
+ <text x="${textX}" y="${buttonY + buttonHeight / 2 + fontSize * 0.35}"
6471
7602
  font-family="${this.fontFamily}"
6472
7603
  font-size="${fontSize}"
6473
7604
  font-weight="${fontWeight}"
6474
7605
  fill="${textColor}"
6475
- text-anchor="middle">${this.escapeXml(visibleText)}</text>
7606
+ text-anchor="${textAnchor}">${this.escapeXml(visibleText)}</text>
6476
7607
  </g>`;
7608
+ return svg;
6477
7609
  }
6478
7610
  /**
6479
7611
  * Render badge with colored border instead of fill
@@ -6483,7 +7615,7 @@ var SketchSVGRenderer = class extends SVGRenderer {
6483
7615
  const variant = String(node.props.variant || "default");
6484
7616
  const semanticBase = this.getSemanticVariantColor(variant);
6485
7617
  const hasExplicitVariantColor = semanticBase !== void 0 || this.colorResolver.hasColor(variant);
6486
- const variantColor = hasExplicitVariantColor ? this.resolveVariantColor(variant, this.renderTheme.primary) : "#2D3748";
7618
+ const variantColor = hasExplicitVariantColor ? this.resolveVariantColor(variant, this.renderTheme.primary) : this.resolveTextColor();
6487
7619
  const borderColor = variantColor;
6488
7620
  const textColor = variantColor;
6489
7621
  const badgeRadius = this.tokens.badge.radius === "pill" ? pos.height / 2 : this.tokens.badge.radius;
@@ -6510,17 +7642,22 @@ var SketchSVGRenderer = class extends SVGRenderer {
6510
7642
  const iconName = String(node.props.icon || "help-circle");
6511
7643
  const variant = String(node.props.variant || "default");
6512
7644
  const size = String(node.props.size || "md");
7645
+ const density = this.ir.project.style.density || "normal";
7646
+ const labelOffset = this.parseBooleanProp(node.props.labelSpace, false) ? 18 : 0;
7647
+ const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
6513
7648
  const semanticBase = this.getSemanticVariantColor(variant);
6514
7649
  const hasExplicitVariantColor = semanticBase !== void 0 || this.colorResolver.hasColor(variant);
6515
- const variantColor = hasExplicitVariantColor ? this.resolveVariantColor(variant, this.renderTheme.primary) : "#2D3748";
7650
+ const variantColor = hasExplicitVariantColor ? this.resolveVariantColor(variant, this.renderTheme.primary) : this.resolveTextColor();
6516
7651
  const borderColor = variantColor;
6517
7652
  const iconColor = variantColor;
6518
- const buttonSize = this.getIconButtonSize(size);
7653
+ const buttonSize = Math.max(16, Math.min(resolveControlHeight(size, density), pos.height - labelOffset));
7654
+ const buttonWidth = buttonSize + extraPadding * 2;
6519
7655
  const radius = 6;
7656
+ const buttonY = pos.y + labelOffset;
6520
7657
  const iconSvg = this.getIconSvg(iconName);
6521
7658
  let svg = `<g${this.getDataNodeId(node)}>
6522
- <rect x="${pos.x}" y="${pos.y}"
6523
- width="${buttonSize}" height="${buttonSize}"
7659
+ <rect x="${pos.x}" y="${buttonY}"
7660
+ width="${buttonWidth}" height="${buttonSize}"
6524
7661
  rx="${radius}"
6525
7662
  fill="none"
6526
7663
  stroke="${borderColor}"
@@ -6528,8 +7665,8 @@ var SketchSVGRenderer = class extends SVGRenderer {
6528
7665
  filter="url(#sketch-rough)"/>`;
6529
7666
  if (iconSvg) {
6530
7667
  const iconSize = buttonSize * 0.6;
6531
- const offsetX = pos.x + (buttonSize - iconSize) / 2;
6532
- const offsetY = pos.y + (buttonSize - iconSize) / 2;
7668
+ const offsetX = pos.x + (buttonWidth - iconSize) / 2;
7669
+ const offsetY = buttonY + (buttonSize - iconSize) / 2;
6533
7670
  svg += `
6534
7671
  <g transform="translate(${offsetX}, ${offsetY})">
6535
7672
  <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round">
@@ -6593,29 +7730,67 @@ var SketchSVGRenderer = class extends SVGRenderer {
6593
7730
  renderInput(node, pos) {
6594
7731
  const label = String(node.props.label || "");
6595
7732
  const placeholder = String(node.props.placeholder || "");
7733
+ const iconLeftName = String(node.props.iconLeft || "").trim();
7734
+ const iconRightName = String(node.props.iconRight || "").trim();
6596
7735
  const radius = this.tokens.input.radius;
6597
7736
  const fontSize = this.tokens.input.fontSize;
6598
7737
  const paddingX = this.tokens.input.paddingX;
6599
7738
  const labelOffset = this.getControlLabelOffset(label);
6600
7739
  const controlY = pos.y + labelOffset;
6601
7740
  const controlHeight = Math.max(16, pos.height - labelOffset);
6602
- return `<g${this.getDataNodeId(node)}>
6603
- ${label ? `<text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
7741
+ const iconSize = 16;
7742
+ const iconPad = 12;
7743
+ const iconInnerGap = 8;
7744
+ const iconLeftSvg = iconLeftName ? getIcon(iconLeftName) : null;
7745
+ const iconRightSvg = iconRightName ? getIcon(iconRightName) : null;
7746
+ const leftOffset = iconLeftSvg ? iconPad + iconSize + iconInnerGap : 0;
7747
+ const rightOffset = iconRightSvg ? iconPad + iconSize + iconInnerGap : 0;
7748
+ const textX = pos.x + (iconLeftSvg ? leftOffset : paddingX);
7749
+ const iconColor = "#888888";
7750
+ const iconCenterY = controlY + (controlHeight - iconSize) / 2;
7751
+ let svg = `<g${this.getDataNodeId(node)}>`;
7752
+ if (label) {
7753
+ svg += `
7754
+ <text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
6604
7755
  font-family="${this.fontFamily}"
6605
7756
  font-size="12"
6606
- fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>` : ""}
7757
+ fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>`;
7758
+ }
7759
+ svg += `
6607
7760
  <rect x="${pos.x}" y="${controlY}"
6608
7761
  width="${pos.width}" height="${controlHeight}"
6609
7762
  rx="${radius}"
6610
7763
  fill="${this.renderTheme.cardBg}"
6611
7764
  stroke="#2D3748"
6612
7765
  stroke-width="0.5"
6613
- filter="url(#sketch-rough)"/>
6614
- ${placeholder ? `<text x="${pos.x + paddingX}" y="${controlY + controlHeight / 2 + 5}"
7766
+ filter="url(#sketch-rough)"/>`;
7767
+ if (iconLeftSvg) {
7768
+ svg += `
7769
+ <g transform="translate(${pos.x + iconPad}, ${iconCenterY})">
7770
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
7771
+ ${this.extractSvgContent(iconLeftSvg)}
7772
+ </svg>
7773
+ </g>`;
7774
+ }
7775
+ if (iconRightSvg) {
7776
+ svg += `
7777
+ <g transform="translate(${pos.x + pos.width - iconPad - iconSize}, ${iconCenterY})">
7778
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
7779
+ ${this.extractSvgContent(iconRightSvg)}
7780
+ </svg>
7781
+ </g>`;
7782
+ }
7783
+ if (placeholder) {
7784
+ const availWidth = pos.width - (iconLeftSvg ? leftOffset : paddingX) - (iconRightSvg ? rightOffset : paddingX);
7785
+ const visiblePh = this.truncateTextToWidth(placeholder, Math.max(0, availWidth), fontSize);
7786
+ svg += `
7787
+ <text x="${textX}" y="${controlY + controlHeight / 2 + 5}"
6615
7788
  font-family="${this.fontFamily}"
6616
7789
  font-size="${fontSize}"
6617
- fill="${this.renderTheme.textMuted}">${this.escapeXml(placeholder)}</text>` : ""}
6618
- </g>`;
7790
+ fill="${this.renderTheme.textMuted}">${this.escapeXml(visiblePh)}</text>`;
7791
+ }
7792
+ svg += "\n </g>";
7793
+ return svg;
6619
7794
  }
6620
7795
  /**
6621
7796
  * Render textarea with thicker border
@@ -6680,6 +7855,8 @@ var SketchSVGRenderer = class extends SVGRenderer {
6680
7855
  */
6681
7856
  renderHeading(node, pos) {
6682
7857
  const text = String(node.props.text || "Heading");
7858
+ const variant = String(node.props.variant || "default");
7859
+ const headingColor = variant === "default" ? this.resolveTextColor() : this.resolveVariantColor(variant, this.resolveTextColor());
6683
7860
  const headingTypography = this.getHeadingTypography(node);
6684
7861
  const fontSize = headingTypography.fontSize;
6685
7862
  const fontWeight = headingTypography.fontWeight;
@@ -6692,7 +7869,7 @@ var SketchSVGRenderer = class extends SVGRenderer {
6692
7869
  font-family="${this.fontFamily}"
6693
7870
  font-size="${fontSize}"
6694
7871
  font-weight="${fontWeight}"
6695
- fill="${this.renderTheme.text}">${this.escapeXml(text)}</text>
7872
+ fill="${headingColor}">${this.escapeXml(text)}</text>
6696
7873
  </g>`;
6697
7874
  }
6698
7875
  const tspans = lines.map(
@@ -6703,7 +7880,7 @@ var SketchSVGRenderer = class extends SVGRenderer {
6703
7880
  font-family="${this.fontFamily}"
6704
7881
  font-size="${fontSize}"
6705
7882
  font-weight="${fontWeight}"
6706
- fill="${this.renderTheme.text}">${tspans}</text>
7883
+ fill="${headingColor}">${tspans}</text>
6707
7884
  </g>`;
6708
7885
  }
6709
7886
  /**
@@ -6714,7 +7891,8 @@ var SketchSVGRenderer = class extends SVGRenderer {
6714
7891
  const subtitle = String(node.props.subtitle || "");
6715
7892
  const actions = String(node.props.actions || "");
6716
7893
  const user = String(node.props.user || "");
6717
- const accentColor = this.resolveAccentColor();
7894
+ const variant = String(node.props.variant || "default");
7895
+ const accentColor = variant === "default" ? this.resolveAccentColor() : this.resolveVariantColor(variant, this.resolveAccentColor());
6718
7896
  const topbar = this.calculateTopbarLayout(node, pos, title, subtitle, actions, user);
6719
7897
  let svg = `<g${this.getDataNodeId(node)}>
6720
7898
  <rect x="${pos.x}" y="${pos.y}"
@@ -6807,79 +7985,14 @@ var SketchSVGRenderer = class extends SVGRenderer {
6807
7985
  * Render table with sketch filter and Comic Sans
6808
7986
  */
6809
7987
  renderTable(node, pos) {
6810
- const title = String(node.props.title || "");
6811
- const columnsStr = String(node.props.columns || "Col1,Col2,Col3");
6812
- const columns = columnsStr.split(",").map((c) => c.trim());
6813
- const rowCount = Number(node.props.rows || node.props.rowsMock || 5);
6814
- const mockStr = String(node.props.mock || "");
6815
- const random = this.parseBooleanProp(node.props.random, false);
6816
- const mockTypes = mockStr ? mockStr.split(",").map((m) => m.trim()).filter(Boolean) : [];
6817
- while (mockTypes.length < columns.length) {
6818
- const inferred = MockDataGenerator.inferMockTypeFromColumn(columns[mockTypes.length] || "item");
6819
- mockTypes.push(inferred);
6820
- }
6821
- const headerHeight = 44;
6822
- const rowHeight = 36;
6823
- const colWidth = pos.width / columns.length;
6824
- const mockRows = [];
6825
- for (let rowIdx = 0; rowIdx < rowCount; rowIdx++) {
6826
- const row = {};
6827
- columns.forEach((col, colIdx) => {
6828
- const mockType = mockTypes[colIdx] || MockDataGenerator.inferMockTypeFromColumn(col) || "item";
6829
- row[col] = MockDataGenerator.getMockValue(mockType, rowIdx, random);
6830
- });
6831
- mockRows.push(row);
6832
- }
6833
- let svg = `<g${this.getDataNodeId(node)}>
6834
- <rect x="${pos.x}" y="${pos.y}"
6835
- width="${pos.width}" height="${pos.height}"
6836
- rx="8"
6837
- fill="${this.renderTheme.cardBg}"
6838
- stroke="#2D3748"
6839
- stroke-width="0.5"
6840
- filter="url(#sketch-rough)"/>`;
6841
- if (title) {
6842
- svg += `
6843
- <text x="${pos.x + 16}" y="${pos.y + 24}"
6844
- font-family="${this.fontFamily}"
6845
- font-size="13"
6846
- font-weight="600"
6847
- fill="${this.renderTheme.text}">${this.escapeXml(title)}</text>`;
6848
- }
6849
- const headerY = pos.y + (title ? 32 : 0);
6850
- svg += `
6851
- <line x1="${pos.x}" y1="${headerY + headerHeight}" x2="${pos.x + pos.width}" y2="${headerY + headerHeight}"
6852
- stroke="#2D3748" stroke-width="0.5" filter="url(#sketch-rough)"/>`;
6853
- columns.forEach((col, i) => {
6854
- svg += `
6855
- <text x="${pos.x + i * colWidth + 12}" y="${headerY + 26}"
6856
- font-family="${this.fontFamily}"
6857
- font-size="11"
6858
- font-weight="600"
6859
- fill="${this.renderTheme.textMuted}">${this.escapeXml(col)}</text>`;
6860
- });
6861
- mockRows.forEach((row, rowIdx) => {
6862
- const rowY = headerY + headerHeight + rowIdx * rowHeight;
6863
- svg += `
6864
- <line x1="${pos.x}" y1="${rowY + rowHeight}" x2="${pos.x + pos.width}" y2="${rowY + rowHeight}"
6865
- stroke="#2D3748" stroke-width="0.5" filter="url(#sketch-rough)"/>`;
6866
- columns.forEach((col, colIdx) => {
6867
- const cellValue = row[col] || "";
6868
- svg += `
6869
- <text x="${pos.x + colIdx * colWidth + 12}" y="${rowY + 22}"
6870
- font-family="${this.fontFamily}"
6871
- font-size="12"
6872
- fill="${this.renderTheme.text}">${this.escapeXml(cellValue)}</text>`;
6873
- });
6874
- });
6875
- svg += "\n </g>";
6876
- return svg;
7988
+ const standard = super.renderTable(node, pos);
7989
+ return standard.replace("<g", '<g filter="url(#sketch-rough)"');
6877
7990
  }
6878
7991
  /**
6879
7992
  * Render text with Comic Sans
6880
7993
  */
6881
7994
  renderText(node, pos) {
6882
- const text = String(node.props.content || "Text content");
7995
+ const text = String(node.props.text || "Text content");
6883
7996
  const fontSize = this.tokens.text.fontSize;
6884
7997
  const lineHeightPx = Math.ceil(fontSize * this.tokens.text.lineHeight);
6885
7998
  const lines = this.wrapTextToLines(text, pos.width, fontSize);
@@ -6931,31 +8044,67 @@ var SketchSVGRenderer = class extends SVGRenderer {
6931
8044
  renderSelect(node, pos) {
6932
8045
  const label = String(node.props.label || "");
6933
8046
  const placeholder = String(node.props.placeholder || "Select...");
8047
+ const iconLeftName = String(node.props.iconLeft || "").trim();
8048
+ const iconRightName = String(node.props.iconRight || "").trim();
6934
8049
  const labelOffset = this.getControlLabelOffset(label);
6935
8050
  const controlY = pos.y + labelOffset;
6936
8051
  const controlHeight = Math.max(16, pos.height - labelOffset);
6937
8052
  const centerY = controlY + controlHeight / 2 + 5;
6938
- return `<g${this.getDataNodeId(node)}>
6939
- ${label ? `<text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
8053
+ const iconSize = 16;
8054
+ const iconPad = 12;
8055
+ const iconInnerGap = 8;
8056
+ const iconLeftSvg = iconLeftName ? getIcon(iconLeftName) : null;
8057
+ const iconRightSvg = iconRightName ? getIcon(iconRightName) : null;
8058
+ const leftOffset = iconLeftSvg ? iconPad + iconSize + iconInnerGap : 0;
8059
+ const chevronWidth = 20;
8060
+ const iconColor = "#888888";
8061
+ const iconCenterY = controlY + (controlHeight - iconSize) / 2;
8062
+ let svg = `<g${this.getDataNodeId(node)}>`;
8063
+ if (label) {
8064
+ svg += `
8065
+ <text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
6940
8066
  font-family="${this.fontFamily}"
6941
8067
  font-size="12"
6942
- fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>` : ""}
8068
+ fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>`;
8069
+ }
8070
+ svg += `
6943
8071
  <rect x="${pos.x}" y="${controlY}"
6944
8072
  width="${pos.width}" height="${controlHeight}"
6945
8073
  rx="6"
6946
8074
  fill="${this.renderTheme.cardBg}"
6947
8075
  stroke="#2D3748"
6948
8076
  stroke-width="0.5"
6949
- filter="url(#sketch-rough)"/>
6950
- <text x="${pos.x + 12}" y="${centerY}"
8077
+ filter="url(#sketch-rough)"/>`;
8078
+ if (iconLeftSvg) {
8079
+ svg += `
8080
+ <g transform="translate(${pos.x + iconPad}, ${iconCenterY})">
8081
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
8082
+ ${this.extractSvgContent(iconLeftSvg)}
8083
+ </svg>
8084
+ </g>`;
8085
+ }
8086
+ if (iconRightSvg) {
8087
+ svg += `
8088
+ <g transform="translate(${pos.x + pos.width - chevronWidth - iconPad - iconSize}, ${iconCenterY})">
8089
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
8090
+ ${this.extractSvgContent(iconRightSvg)}
8091
+ </svg>
8092
+ </g>`;
8093
+ }
8094
+ const textX = pos.x + (iconLeftSvg ? leftOffset : 12);
8095
+ const availWidth = pos.width - (iconLeftSvg ? leftOffset : 12) - chevronWidth - (iconRightSvg ? iconPad + iconSize + iconInnerGap : 0);
8096
+ const visiblePh = this.truncateTextToWidth(placeholder, Math.max(0, availWidth), 14);
8097
+ svg += `
8098
+ <text x="${textX}" y="${centerY}"
6951
8099
  font-family="${this.fontFamily}"
6952
8100
  font-size="14"
6953
- fill="${this.renderTheme.textMuted}">${this.escapeXml(placeholder)}</text>
8101
+ fill="${this.renderTheme.textMuted}">${this.escapeXml(visiblePh)}</text>
6954
8102
  <text x="${pos.x + pos.width - 20}" y="${centerY}"
6955
8103
  font-family="${this.fontFamily}"
6956
8104
  font-size="16"
6957
8105
  fill="${this.renderTheme.textMuted}">\u25BC</text>
6958
8106
  </g>`;
8107
+ return svg;
6959
8108
  }
6960
8109
  /**
6961
8110
  * Render checkbox with sketch filter and Comic Sans
@@ -7354,44 +8503,43 @@ var SketchSVGRenderer = class extends SVGRenderer {
7354
8503
  renderImage(node, pos) {
7355
8504
  const placeholder = String(node.props.placeholder || "landscape").toLowerCase();
7356
8505
  const iconType = String(node.props.icon || "").trim();
8506
+ const variant = String(node.props.variant || "").trim();
7357
8507
  const iconSvg = placeholder === "icon" && iconType.length > 0 ? getIcon(iconType) : null;
8508
+ const imageBg = this.options.theme === "dark" ? "#2A2A2A" : "#E8E8E8";
7358
8509
  if (iconSvg) {
7359
- const badgeSize = Math.max(24, Math.min(pos.width, pos.height) * 0.6);
7360
- const badgeX = pos.x + (pos.width - badgeSize) / 2;
7361
- const badgeY = pos.y + (pos.height - badgeSize) / 2;
7362
- const iconSize = badgeSize * 0.62;
7363
- const iconOffsetX = badgeX + (badgeSize - iconSize) / 2;
7364
- const iconOffsetY = badgeY + (badgeSize - iconSize) / 2;
8510
+ const semanticBase = variant ? this.getSemanticVariantColor(variant) : void 0;
8511
+ const hasVariant = variant.length > 0 && (semanticBase !== void 0 || this.colorResolver.hasColor(variant));
8512
+ const variantColor = hasVariant ? this.resolveVariantColor(variant, this.renderTheme.primary) : null;
8513
+ const bgColor = hasVariant ? this.hexToRgba(variantColor, 0.12) : imageBg;
8514
+ const iconColor = hasVariant ? variantColor : "#666666";
8515
+ const iconSize = Math.max(16, Math.min(pos.width, pos.height) * 0.6);
8516
+ const iconOffsetX = pos.x + (pos.width - iconSize) / 2;
8517
+ const iconOffsetY = pos.y + (pos.height - iconSize) / 2;
7365
8518
  return `<g${this.getDataNodeId(node)}>
7366
- <!-- Image Background -->
7367
8519
  <rect x="${pos.x}" y="${pos.y}"
7368
8520
  width="${pos.width}" height="${pos.height}"
7369
- fill="#E8E8E8"
7370
- stroke="#2D3748"
7371
- stroke-width="0.5"
8521
+ fill="${bgColor}"
7372
8522
  rx="4"
7373
8523
  filter="url(#sketch-rough)"/>
7374
-
7375
- <!-- Custom Icon Placeholder -->
7376
- <rect x="${badgeX}" y="${badgeY}"
7377
- width="${badgeSize}" height="${badgeSize}"
7378
- rx="${Math.max(4, badgeSize * 0.2)}"
7379
- fill="none"
7380
- stroke="#2D3748"
7381
- stroke-width="0.5"
7382
- filter="url(#sketch-rough)"/>
7383
8524
  <g transform="translate(${iconOffsetX}, ${iconOffsetY})">
7384
- <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="#2D3748" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
8525
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
7385
8526
  ${this.extractSvgContent(iconSvg)}
7386
8527
  </svg>
7387
8528
  </g>
8529
+ <rect x="${pos.x}" y="${pos.y}"
8530
+ width="${pos.width}" height="${pos.height}"
8531
+ fill="none"
8532
+ stroke="#2D3748"
8533
+ stroke-width="0.5"
8534
+ rx="4"
8535
+ filter="url(#sketch-rough)"/>
7388
8536
  </g>`;
7389
8537
  }
7390
8538
  return `<g${this.getDataNodeId(node)}>
7391
8539
  <!-- Image Background -->
7392
8540
  <rect x="${pos.x}" y="${pos.y}"
7393
8541
  width="${pos.width}" height="${pos.height}"
7394
- fill="#E8E8E8"
8542
+ fill="${imageBg}"
7395
8543
  stroke="#2D3748"
7396
8544
  stroke-width="0.5"
7397
8545
  rx="4"
@@ -7446,17 +8594,23 @@ var SketchSVGRenderer = class extends SVGRenderer {
7446
8594
  */
7447
8595
  renderSidebarMenu(node, pos) {
7448
8596
  const itemsStr = String(node.props.items || "Item 1,Item 2,Item 3");
8597
+ const iconsStr = String(node.props.icons || "");
7449
8598
  const items = itemsStr.split(",").map((s) => s.trim());
8599
+ const icons = iconsStr ? iconsStr.split(",").map((s) => s.trim()) : [];
7450
8600
  const itemHeight = 40;
7451
8601
  const fontSize = 14;
7452
8602
  const activeIndex = Number(node.props.active || 0);
7453
8603
  const accentColor = this.resolveAccentColor();
8604
+ const variantProp = String(node.props.variant || "").trim();
8605
+ const semanticVariant = variantProp ? this.getSemanticVariantColor(variantProp) : void 0;
8606
+ const hasVariant = variantProp.length > 0 && (semanticVariant !== void 0 || this.colorResolver.hasColor(variantProp));
8607
+ const activeColor = hasVariant ? this.resolveVariantColor(variantProp, accentColor) : accentColor;
7454
8608
  let svg = `<g${this.getDataNodeId(node)}>`;
7455
8609
  items.forEach((item, index) => {
7456
8610
  const itemY = pos.y + index * itemHeight;
7457
8611
  const isActive = index === activeIndex;
7458
- const bgColor = isActive ? this.hexToRgba(accentColor, 0.15) : "transparent";
7459
- const textColor = isActive ? accentColor : "#2D3748";
8612
+ const bgColor = isActive ? this.hexToRgba(activeColor, 0.15) : "transparent";
8613
+ const textColor = isActive ? activeColor : this.resolveTextColor();
7460
8614
  const fontWeight = isActive ? "500" : "400";
7461
8615
  if (isActive) {
7462
8616
  svg += `
@@ -7466,8 +8620,24 @@ var SketchSVGRenderer = class extends SVGRenderer {
7466
8620
  fill="${bgColor}"
7467
8621
  filter="url(#sketch-rough)"/>`;
7468
8622
  }
8623
+ let currentX = pos.x + 12;
8624
+ if (icons[index]) {
8625
+ const iconSvg = getIcon(icons[index]);
8626
+ if (iconSvg) {
8627
+ const iconSize = 16;
8628
+ const iconY = itemY + (itemHeight - iconSize) / 2;
8629
+ const iconColor = isActive ? activeColor : this.resolveMutedColor();
8630
+ svg += `
8631
+ <g transform="translate(${currentX}, ${iconY})">
8632
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
8633
+ ${this.extractSvgContent(iconSvg)}
8634
+ </svg>
8635
+ </g>`;
8636
+ currentX += iconSize + 8;
8637
+ }
8638
+ }
7469
8639
  svg += `
7470
- <text x="${pos.x + 12}" y="${itemY + itemHeight / 2 + 5}"
8640
+ <text x="${currentX}" y="${itemY + itemHeight / 2 + 5}"
7471
8641
  font-family="${this.fontFamily}"
7472
8642
  font-size="${fontSize}"
7473
8643
  font-weight="${fontWeight}"
@@ -7480,22 +8650,23 @@ var SketchSVGRenderer = class extends SVGRenderer {
7480
8650
  * Render icon (same as base, icons don't need filter)
7481
8651
  */
7482
8652
  renderIcon(node, pos) {
7483
- const iconType = String(node.props.type || "help-circle");
8653
+ const iconType = String(node.props.icon || "help-circle");
7484
8654
  const size = String(node.props.size || "md");
8655
+ const variant = String(node.props.variant || "default");
7485
8656
  const iconSvg = getIcon(iconType);
7486
8657
  if (!iconSvg) {
7487
8658
  return `<g${this.getDataNodeId(node)}>
7488
8659
  <circle cx="${pos.x + pos.width / 2}" cy="${pos.y + pos.height / 2}"
7489
8660
  r="${Math.min(pos.width, pos.height) / 2 - 2}"
7490
- fill="none" stroke="#2D3748" stroke-width="0.5"
8661
+ fill="none" stroke="${this.resolveMutedColor()}" stroke-width="0.5"
7491
8662
  filter="url(#sketch-rough)"/>
7492
8663
  <text x="${pos.x + pos.width / 2}" y="${pos.y + pos.height / 2 + 4}"
7493
8664
  font-family="${this.fontFamily}"
7494
- font-size="12" fill="#2D3748" text-anchor="middle">?</text>
8665
+ font-size="12" fill="${this.resolveMutedColor()}" text-anchor="middle">?</text>
7495
8666
  </g>`;
7496
8667
  }
7497
8668
  const iconSize = this.getIconSize(size);
7498
- const iconColor = "#2D3748";
8669
+ const iconColor = variant === "default" ? this.resolveTextColor() : this.resolveVariantColor(variant, this.resolveTextColor());
7499
8670
  const offsetX = pos.x + (pos.width - iconSize) / 2;
7500
8671
  const offsetY = pos.y + (pos.height - iconSize) / 2;
7501
8672
  return `<g${this.getDataNodeId(node)} transform="translate(${offsetX}, ${offsetY})">