@wire-dsl/engine 0.2.4 → 0.3.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,8 @@ 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
+ if (!enumValues.includes(normalizedValue) && !isCustomVariantFromColors) {
1574
1700
  emitWarning(
1575
1701
  `Invalid value "${normalizedValue}" for property "${propName}" in component "${componentType}".`,
1576
1702
  "COMPONENT_INVALID_PROPERTY_VALUE",
@@ -1590,11 +1716,40 @@ function validateSemanticDiagnostics(ast, sourceMap) {
1590
1716
  );
1591
1717
  }
1592
1718
  }
1719
+ if (componentType === "Table") {
1720
+ const hasCaption = String(component.props.caption || "").trim().length > 0;
1721
+ const hasPagination = parseBooleanLike(component.props.pagination ?? "false", false);
1722
+ if (hasCaption && hasPagination) {
1723
+ const rawPaginationAlign = String(component.props.paginationAlign || "right");
1724
+ const paginationAlign = rawPaginationAlign === "left" || rawPaginationAlign === "center" || rawPaginationAlign === "right" ? rawPaginationAlign : "right";
1725
+ const rawCaptionAlign = String(component.props.captionAlign || "");
1726
+ const captionAlign = rawCaptionAlign === "left" || rawCaptionAlign === "center" || rawCaptionAlign === "right" ? rawCaptionAlign : paginationAlign === "left" ? "right" : "left";
1727
+ if (captionAlign === paginationAlign) {
1728
+ emitWarning(
1729
+ `Table footer collision: "captionAlign" and "paginationAlign" both resolve to "${captionAlign}".`,
1730
+ "TABLE_FOOTER_ALIGNMENT_COLLISION",
1731
+ entry?.range || toFallbackRange(),
1732
+ nodeId,
1733
+ "Use different alignments to avoid visual overlap."
1734
+ );
1735
+ }
1736
+ }
1737
+ }
1593
1738
  };
1594
- const checkLayout = (layout) => {
1739
+ const checkLayout = (layout, insideDefinedLayout) => {
1595
1740
  const nodeId = layout._meta?.nodeId;
1596
1741
  const entry = nodeId ? sourceMapByNodeId.get(nodeId) : void 0;
1597
1742
  const rules = LAYOUT_RULES[layout.layoutType];
1743
+ const isDefinedLayoutUsage = definedLayouts.has(layout.layoutType);
1744
+ if (isDefinedLayoutUsage && layout.children.length !== 1) {
1745
+ emitError(
1746
+ `Layout "${layout.layoutType}" expects exactly one child for its Children slot.`,
1747
+ "LAYOUT_DEFINITION_CHILDREN_ARITY",
1748
+ entry?.bodyRange || entry?.range || toFallbackRange(),
1749
+ nodeId,
1750
+ "Provide exactly one nested child block when using this layout."
1751
+ );
1752
+ }
1598
1753
  if (layout.children.length === 0) {
1599
1754
  emitWarning(
1600
1755
  `Layout "${layout.layoutType}" is empty.`,
@@ -1604,7 +1759,7 @@ function validateSemanticDiagnostics(ast, sourceMap) {
1604
1759
  "Add at least one child: component, layout, or cell."
1605
1760
  );
1606
1761
  }
1607
- if (!rules) {
1762
+ if (!rules && !isDefinedLayoutUsage) {
1608
1763
  emitWarning(
1609
1764
  `Layout type "${layout.layoutType}" is not recognized by semantic validation rules.`,
1610
1765
  "LAYOUT_UNKNOWN_TYPE",
@@ -1612,7 +1767,7 @@ function validateSemanticDiagnostics(ast, sourceMap) {
1612
1767
  nodeId,
1613
1768
  `Use one of: ${Object.keys(LAYOUT_RULES).join(", ")}.`
1614
1769
  );
1615
- } else {
1770
+ } else if (rules) {
1616
1771
  const missingRequiredParams = getMissingRequiredNames(rules.requiredParams, layout.params);
1617
1772
  if (missingRequiredParams.length > 0) {
1618
1773
  emitWarning(
@@ -1625,6 +1780,16 @@ function validateSemanticDiagnostics(ast, sourceMap) {
1625
1780
  }
1626
1781
  const allowed = new Set(rules.allowedParams);
1627
1782
  for (const [paramName, paramValue] of Object.entries(layout.params)) {
1783
+ if (layout.layoutType === "split" && paramName === "sidebar") {
1784
+ emitError(
1785
+ 'Split parameter "sidebar" was removed. Use "left" or "right" instead.',
1786
+ "LAYOUT_SPLIT_SIDEBAR_DEPRECATED",
1787
+ getPropertyRange(entry, paramName, "name"),
1788
+ nodeId,
1789
+ "Example: layout split(left: 260) { ... }"
1790
+ );
1791
+ continue;
1792
+ }
1628
1793
  if (!allowed.has(paramName)) {
1629
1794
  emitWarning(
1630
1795
  `Parameter "${paramName}" is not recognized for layout "${layout.layoutType}".`,
@@ -1663,31 +1828,62 @@ function validateSemanticDiagnostics(ast, sourceMap) {
1663
1828
  );
1664
1829
  }
1665
1830
  }
1666
- if (layout.layoutType === "split" && paramName === "sidebar") {
1667
- const sidebar = Number(paramValue);
1668
- if (!Number.isFinite(sidebar) || sidebar <= 0) {
1831
+ if (layout.layoutType === "split" && (paramName === "left" || paramName === "right")) {
1832
+ const splitSize = Number(paramValue);
1833
+ if (!Number.isFinite(splitSize) || splitSize <= 0) {
1669
1834
  emitWarning(
1670
- 'Split "sidebar" must be a positive number.',
1671
- "LAYOUT_SPLIT_SIDEBAR_INVALID",
1835
+ `Split "${paramName}" must be a positive number. Falling back to 250.`,
1836
+ "LAYOUT_SPLIT_WIDTH_INVALID",
1672
1837
  getPropertyRange(entry, paramName, "value"),
1673
1838
  nodeId,
1674
- "Use a value like sidebar: 240."
1839
+ `Use a value like ${paramName}: 260.`
1675
1840
  );
1676
1841
  }
1677
1842
  }
1678
1843
  }
1844
+ if (layout.layoutType === "split") {
1845
+ const hasLeft = layout.params.left !== void 0;
1846
+ const hasRight = layout.params.right !== void 0;
1847
+ if (!hasLeft && !hasRight) {
1848
+ emitError(
1849
+ 'Split layout requires exactly one fixed side width: "left" or "right".',
1850
+ "LAYOUT_SPLIT_SIDE_REQUIRED",
1851
+ entry?.nameRange || entry?.range || toFallbackRange(),
1852
+ nodeId,
1853
+ "Add either left: <number> or right: <number>."
1854
+ );
1855
+ }
1856
+ if (hasLeft && hasRight) {
1857
+ emitError(
1858
+ 'Split layout accepts only one fixed side width: use either "left" or "right", not both.',
1859
+ "LAYOUT_SPLIT_SIDE_CONFLICT",
1860
+ entry?.nameRange || entry?.range || toFallbackRange(),
1861
+ nodeId,
1862
+ "Remove one of the two parameters."
1863
+ );
1864
+ }
1865
+ if (layout.children.length !== 2) {
1866
+ emitError(
1867
+ `Split layout requires exactly 2 children, received ${layout.children.length}.`,
1868
+ "LAYOUT_SPLIT_CHILDREN_ARITY",
1869
+ entry?.bodyRange || entry?.range || toFallbackRange(),
1870
+ nodeId,
1871
+ "Provide exactly two child blocks (left/right content)."
1872
+ );
1873
+ }
1874
+ }
1679
1875
  }
1680
1876
  for (const child of layout.children) {
1681
1877
  if (child.type === "component") {
1682
- checkComponent(child);
1878
+ checkComponent(child, insideDefinedLayout);
1683
1879
  } else if (child.type === "layout") {
1684
- checkLayout(child);
1880
+ checkLayout(child, insideDefinedLayout);
1685
1881
  } else if (child.type === "cell") {
1686
- checkCell(child);
1882
+ checkCell(child, insideDefinedLayout);
1687
1883
  }
1688
1884
  }
1689
1885
  };
1690
- const checkCell = (cell) => {
1886
+ const checkCell = (cell, insideDefinedLayout) => {
1691
1887
  const nodeId = cell._meta?.nodeId;
1692
1888
  const entry = nodeId ? sourceMapByNodeId.get(nodeId) : void 0;
1693
1889
  if (cell.props.span !== void 0) {
@@ -1703,12 +1899,54 @@ function validateSemanticDiagnostics(ast, sourceMap) {
1703
1899
  }
1704
1900
  }
1705
1901
  for (const child of cell.children) {
1706
- if (child.type === "component") checkComponent(child);
1707
- if (child.type === "layout") checkLayout(child);
1902
+ if (child.type === "component") checkComponent(child, insideDefinedLayout);
1903
+ if (child.type === "layout") checkLayout(child, insideDefinedLayout);
1708
1904
  }
1709
1905
  };
1906
+ for (const componentDef of ast.definedComponents) {
1907
+ const nodeId = componentDef._meta?.nodeId;
1908
+ const entry = nodeId ? sourceMapByNodeId.get(nodeId) : void 0;
1909
+ if (!isPascalCaseIdentifier(componentDef.name)) {
1910
+ emitWarning(
1911
+ `Defined component "${componentDef.name}" should use PascalCase naming.`,
1912
+ "COMPONENT_DEFINITION_NAME_STYLE",
1913
+ entry?.nameRange || entry?.range || toFallbackRange(),
1914
+ nodeId,
1915
+ 'Use a name like "MyComponent".'
1916
+ );
1917
+ }
1918
+ if (componentDef.body.type === "component") {
1919
+ checkComponent(componentDef.body, false);
1920
+ } else {
1921
+ checkLayout(componentDef.body, false);
1922
+ }
1923
+ }
1924
+ for (const layoutDef of ast.definedLayouts) {
1925
+ const nodeId = layoutDef._meta?.nodeId;
1926
+ const entry = nodeId ? sourceMapByNodeId.get(nodeId) : void 0;
1927
+ if (!isValidDefinedLayoutName(layoutDef.name)) {
1928
+ emitError(
1929
+ `Defined layout "${layoutDef.name}" must match /^[a-z][a-z0-9_]*$/.`,
1930
+ "LAYOUT_DEFINITION_INVALID_NAME",
1931
+ entry?.nameRange || entry?.range || toFallbackRange(),
1932
+ nodeId,
1933
+ 'Use names like "screen_default" or "appShell".'
1934
+ );
1935
+ }
1936
+ const childrenSlotCount = countChildrenSlots(layoutDef.body);
1937
+ if (childrenSlotCount !== 1) {
1938
+ emitError(
1939
+ `Defined layout "${layoutDef.name}" must contain exactly one "component Children" placeholder.`,
1940
+ "LAYOUT_DEFINITION_CHILDREN_SLOT_COUNT",
1941
+ entry?.bodyRange || entry?.range || toFallbackRange(),
1942
+ nodeId,
1943
+ 'Add exactly one "component Children" in the layout body.'
1944
+ );
1945
+ }
1946
+ checkLayout(layoutDef.body, true);
1947
+ }
1710
1948
  ast.screens.forEach((screen) => {
1711
- checkLayout(screen.layout);
1949
+ checkLayout(screen.layout, false);
1712
1950
  });
1713
1951
  return diagnostics;
1714
1952
  }
@@ -1725,7 +1963,7 @@ ${lexResult.errors.map((e) => e.message).join("\n")}`);
1725
1963
  ${parserInstance.errors.map((e) => e.message).join("\n")}`);
1726
1964
  }
1727
1965
  const ast = visitor.visit(cst);
1728
- validateComponentDefinitionCycles(ast);
1966
+ validateDefinitionCycles(ast);
1729
1967
  return ast;
1730
1968
  }
1731
1969
  function parseWireDSLWithSourceMap(input, filePath = "<input>", options) {
@@ -1756,7 +1994,7 @@ ${parserInstance.errors.map((e) => e.message).join("\n")}`);
1756
1994
  const ast = visitorWithSourceMap.visit(cst);
1757
1995
  const sourceMap = sourceMapBuilder.build();
1758
1996
  try {
1759
- validateComponentDefinitionCycles(ast);
1997
+ validateDefinitionCycles(ast);
1760
1998
  } catch (error) {
1761
1999
  const projectEntry = sourceMap.find((entry) => entry.type === "project");
1762
2000
  diagnostics.push({
@@ -1779,83 +2017,101 @@ ${parserInstance.errors.map((e) => e.message).join("\n")}`);
1779
2017
  }
1780
2018
  return buildParseResult(ast, sourceMap, diagnostics);
1781
2019
  }
1782
- function validateComponentDefinitionCycles(ast) {
1783
- if (!ast.definedComponents || ast.definedComponents.length === 0) {
2020
+ function validateDefinitionCycles(ast) {
2021
+ if ((!ast.definedComponents || ast.definedComponents.length === 0) && (!ast.definedLayouts || ast.definedLayouts.length === 0)) {
1784
2022
  return;
1785
2023
  }
1786
2024
  const components = /* @__PURE__ */ new Map();
2025
+ const layouts = /* @__PURE__ */ new Map();
1787
2026
  ast.definedComponents.forEach((comp) => {
1788
2027
  components.set(comp.name, comp);
1789
2028
  });
2029
+ ast.definedLayouts.forEach((layoutDef) => {
2030
+ layouts.set(layoutDef.name, layoutDef);
2031
+ });
2032
+ const makeComponentKey = (name) => `component:${name}`;
2033
+ const makeLayoutKey = (name) => `layout:${name}`;
2034
+ const displayKey = (key) => key.split(":")[1];
2035
+ const shouldTrackComponentDependency = (name) => components.has(name) && !BUILT_IN_COMPONENTS.has(name);
2036
+ const shouldTrackLayoutDependency = (name) => layouts.has(name) && !BUILT_IN_LAYOUTS.has(name);
1790
2037
  const visited = /* @__PURE__ */ new Set();
1791
2038
  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
- });
2039
+ const collectLayoutDependencies = (layout, deps) => {
2040
+ if (shouldTrackLayoutDependency(layout.layoutType)) {
2041
+ deps.add(makeLayoutKey(layout.layoutType));
2042
+ }
2043
+ for (const child of layout.children) {
2044
+ if (child.type === "component") {
2045
+ if (shouldTrackComponentDependency(child.componentType)) {
2046
+ deps.add(makeComponentKey(child.componentType));
2047
+ }
2048
+ } else if (child.type === "layout") {
2049
+ collectLayoutDependencies(child, deps);
2050
+ } else if (child.type === "cell") {
2051
+ for (const cellChild of child.children) {
2052
+ if (cellChild.type === "component") {
2053
+ if (shouldTrackComponentDependency(cellChild.componentType)) {
2054
+ deps.add(makeComponentKey(cellChild.componentType));
1815
2055
  }
2056
+ } else if (cellChild.type === "layout") {
2057
+ collectLayoutDependencies(cellChild, deps);
1816
2058
  }
1817
- });
2059
+ }
1818
2060
  }
1819
2061
  }
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;
2062
+ };
2063
+ const getDependencies = (key) => {
2064
+ const deps = /* @__PURE__ */ new Set();
2065
+ if (key.startsWith("component:")) {
2066
+ const name2 = key.slice("component:".length);
2067
+ const def2 = components.get(name2);
2068
+ if (!def2) return deps;
2069
+ if (def2.body.type === "component") {
2070
+ if (shouldTrackComponentDependency(def2.body.componentType)) {
2071
+ deps.add(makeComponentKey(def2.body.componentType));
2072
+ }
2073
+ } else {
2074
+ collectLayoutDependencies(def2.body, deps);
2075
+ }
2076
+ return deps;
1827
2077
  }
1828
- if (visited.has(componentName)) {
1829
- return null;
2078
+ const name = key.slice("layout:".length);
2079
+ const def = layouts.get(name);
2080
+ if (!def) return deps;
2081
+ collectLayoutDependencies(def.body, deps);
2082
+ return deps;
2083
+ };
2084
+ const findCycle = (key, path = []) => {
2085
+ if (recursionStack.has(key)) {
2086
+ const cycleStart = path.indexOf(key);
2087
+ return path.slice(cycleStart).concat(key);
1830
2088
  }
1831
- const component = components.get(componentName);
1832
- if (!component) {
2089
+ if (visited.has(key)) {
1833
2090
  return null;
1834
2091
  }
1835
- recursionStack.add(componentName);
1836
- const currentPath = [...path, componentName];
1837
- const dependencies = getComponentDependencies(component.body);
2092
+ recursionStack.add(key);
2093
+ const currentPath = [...path, key];
2094
+ const dependencies = getDependencies(key);
1838
2095
  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
- }
2096
+ const cycle = findCycle(dep, currentPath);
2097
+ if (cycle) return cycle;
1846
2098
  }
1847
- recursionStack.delete(componentName);
1848
- visited.add(componentName);
2099
+ recursionStack.delete(key);
2100
+ visited.add(key);
1849
2101
  return null;
1850
- }
1851
- for (const [componentName] of components) {
2102
+ };
2103
+ const allDefinitions = [
2104
+ ...Array.from(components.keys()).map(makeComponentKey),
2105
+ ...Array.from(layouts.keys()).map(makeLayoutKey)
2106
+ ];
2107
+ for (const key of allDefinitions) {
1852
2108
  visited.clear();
1853
2109
  recursionStack.clear();
1854
- const cycle = hasCycle(componentName);
2110
+ const cycle = findCycle(key);
1855
2111
  if (cycle) {
1856
2112
  throw new Error(
1857
- `Circular component definition detected: ${cycle.join(" \u2192 ")}
1858
- Components cannot reference each other in a cycle.`
2113
+ `Circular component definition detected: ${cycle.map(displayKey).join(" \u2192 ")}
2114
+ Components and layouts cannot reference each other in a cycle.`
1859
2115
  );
1860
2116
  }
1861
2117
  }
@@ -1863,6 +2119,7 @@ Components cannot reference each other in a cycle.`
1863
2119
 
1864
2120
  // src/ir/index.ts
1865
2121
  var import_zod = require("zod");
2122
+ var import_components2 = require("@wire-dsl/language-support/components");
1866
2123
 
1867
2124
  // src/ir/device-presets.ts
1868
2125
  var DEVICE_PRESETS = {
@@ -2027,9 +2284,11 @@ var IRGenerator = class {
2027
2284
  this.idGen = new IDGenerator();
2028
2285
  this.nodes = {};
2029
2286
  this.definedComponents = /* @__PURE__ */ new Map();
2287
+ this.definedLayouts = /* @__PURE__ */ new Map();
2030
2288
  this.definedComponentIndices = /* @__PURE__ */ new Map();
2031
2289
  this.undefinedComponentsUsed = /* @__PURE__ */ new Set();
2032
2290
  this.warnings = [];
2291
+ this.errors = [];
2033
2292
  this.style = {
2034
2293
  density: "normal",
2035
2294
  spacing: "md",
@@ -2042,15 +2301,22 @@ var IRGenerator = class {
2042
2301
  this.idGen.reset();
2043
2302
  this.nodes = {};
2044
2303
  this.definedComponents.clear();
2304
+ this.definedLayouts.clear();
2045
2305
  this.definedComponentIndices.clear();
2046
2306
  this.undefinedComponentsUsed.clear();
2047
2307
  this.warnings = [];
2308
+ this.errors = [];
2048
2309
  if (ast.definedComponents && ast.definedComponents.length > 0) {
2049
2310
  ast.definedComponents.forEach((def, index) => {
2050
2311
  this.definedComponents.set(def.name, def);
2051
2312
  this.definedComponentIndices.set(def.name, index);
2052
2313
  });
2053
2314
  }
2315
+ if (ast.definedLayouts && ast.definedLayouts.length > 0) {
2316
+ ast.definedLayouts.forEach((def) => {
2317
+ this.definedLayouts.set(def.name, def);
2318
+ });
2319
+ }
2054
2320
  this.applyStyle(ast.style);
2055
2321
  const screens = ast.screens.map(
2056
2322
  (screen, screenIndex) => this.convertScreen(screen, screenIndex)
@@ -2062,6 +2328,11 @@ var IRGenerator = class {
2062
2328
  Define these components with: define Component "Name" { ... }`
2063
2329
  );
2064
2330
  }
2331
+ if (this.errors.length > 0) {
2332
+ const messages = this.errors.map((e) => `- [${e.type}] ${e.message}`).join("\n");
2333
+ throw new Error(`IR generation failed with semantic errors:
2334
+ ${messages}`);
2335
+ }
2065
2336
  const project = {
2066
2337
  id: this.sanitizeId(ast.name),
2067
2338
  name: ast.name,
@@ -2188,41 +2459,50 @@ Define these components with: define Component "Name" { ... }`
2188
2459
  getWarnings() {
2189
2460
  return this.warnings;
2190
2461
  }
2191
- convertLayout(layout) {
2462
+ convertLayout(layout, context) {
2463
+ let layoutParams = this.resolveLayoutParams(layout.layoutType, layout.params, context);
2464
+ if (layout.layoutType === "split") {
2465
+ layoutParams = this.normalizeSplitParams(layoutParams);
2466
+ }
2467
+ const layoutChildren = layout.children;
2468
+ const layoutDefinition = this.definedLayouts.get(layout.layoutType);
2469
+ if (layoutDefinition) {
2470
+ return this.expandDefinedLayout(layoutDefinition, layoutParams, layoutChildren, context);
2471
+ }
2192
2472
  const nodeId = this.idGen.generate("node");
2193
2473
  const childRefs = [];
2194
- for (const child of layout.children) {
2474
+ for (const child of layoutChildren) {
2195
2475
  if (child.type === "layout") {
2196
- const childId = this.convertLayout(child);
2197
- childRefs.push({ ref: childId });
2476
+ const childId = this.convertLayout(child, context);
2477
+ if (childId) childRefs.push({ ref: childId });
2198
2478
  } else if (child.type === "component") {
2199
- const childId = this.convertComponent(child);
2200
- childRefs.push({ ref: childId });
2479
+ const childId = this.convertComponent(child, context);
2480
+ if (childId) childRefs.push({ ref: childId });
2201
2481
  } else if (child.type === "cell") {
2202
- const childId = this.convertCell(child);
2203
- childRefs.push({ ref: childId });
2482
+ const childId = this.convertCell(child, context);
2483
+ if (childId) childRefs.push({ ref: childId });
2204
2484
  }
2205
2485
  }
2206
2486
  const style = {};
2207
- if (layout.params.padding) {
2208
- style.padding = String(layout.params.padding);
2487
+ if (layoutParams.padding !== void 0) {
2488
+ style.padding = String(layoutParams.padding);
2209
2489
  } else {
2210
2490
  style.padding = "none";
2211
2491
  }
2212
- if (layout.params.gap) {
2213
- style.gap = String(layout.params.gap);
2492
+ if (layoutParams.gap !== void 0) {
2493
+ style.gap = String(layoutParams.gap);
2214
2494
  }
2215
- if (layout.params.align) {
2216
- style.align = layout.params.align;
2495
+ if (layoutParams.align !== void 0) {
2496
+ style.align = layoutParams.align;
2217
2497
  }
2218
- if (layout.params.background) {
2219
- style.background = String(layout.params.background);
2498
+ if (layoutParams.background !== void 0) {
2499
+ style.background = String(layoutParams.background);
2220
2500
  }
2221
2501
  const containerNode = {
2222
2502
  id: nodeId,
2223
2503
  kind: "container",
2224
2504
  containerType: layout.layoutType,
2225
- params: this.cleanParams(layout.params),
2505
+ params: this.cleanParams(layoutParams),
2226
2506
  children: childRefs,
2227
2507
  style,
2228
2508
  meta: {
@@ -2233,16 +2513,16 @@ Define these components with: define Component "Name" { ... }`
2233
2513
  this.nodes[nodeId] = containerNode;
2234
2514
  return nodeId;
2235
2515
  }
2236
- convertCell(cell) {
2516
+ convertCell(cell, context) {
2237
2517
  const nodeId = this.idGen.generate("node");
2238
2518
  const childRefs = [];
2239
2519
  for (const child of cell.children) {
2240
2520
  if (child.type === "layout") {
2241
- const childId = this.convertLayout(child);
2242
- childRefs.push({ ref: childId });
2521
+ const childId = this.convertLayout(child, context);
2522
+ if (childId) childRefs.push({ ref: childId });
2243
2523
  } else if (child.type === "component") {
2244
- const childId = this.convertComponent(child);
2245
- childRefs.push({ ref: childId });
2524
+ const childId = this.convertComponent(child, context);
2525
+ if (childId) childRefs.push({ ref: childId });
2246
2526
  }
2247
2527
  }
2248
2528
  const containerNode = {
@@ -2263,10 +2543,28 @@ Define these components with: define Component "Name" { ... }`
2263
2543
  this.nodes[nodeId] = containerNode;
2264
2544
  return nodeId;
2265
2545
  }
2266
- convertComponent(component) {
2546
+ convertComponent(component, context) {
2547
+ if (component.componentType === "Children") {
2548
+ if (!context?.allowChildrenSlot) {
2549
+ this.errors.push({
2550
+ type: "children-slot-outside-layout-definition",
2551
+ message: '"Children" placeholder can only be used inside a define Layout body.'
2552
+ });
2553
+ return null;
2554
+ }
2555
+ if (!context.childrenSlot) {
2556
+ this.errors.push({
2557
+ type: "children-slot-missing-child",
2558
+ message: `Layout "${context.definitionName}" requires exactly one child for "Children".`
2559
+ });
2560
+ return null;
2561
+ }
2562
+ return this.convertASTNode(context.childrenSlot, context);
2563
+ }
2564
+ const resolvedProps = this.resolveComponentProps(component.componentType, component.props, context);
2267
2565
  const definition = this.definedComponents.get(component.componentType);
2268
2566
  if (definition) {
2269
- return this.expandDefinedComponent(definition);
2567
+ return this.expandDefinedComponent(definition, resolvedProps, context);
2270
2568
  }
2271
2569
  const builtInComponents = /* @__PURE__ */ new Set([
2272
2570
  "Button",
@@ -2309,7 +2607,7 @@ Define these components with: define Component "Name" { ... }`
2309
2607
  id: nodeId,
2310
2608
  kind: "component",
2311
2609
  componentType: component.componentType,
2312
- props: component.props,
2610
+ props: resolvedProps,
2313
2611
  style: {},
2314
2612
  meta: {
2315
2613
  nodeId: component._meta?.nodeId
@@ -2319,15 +2617,196 @@ Define these components with: define Component "Name" { ... }`
2319
2617
  this.nodes[nodeId] = componentNode;
2320
2618
  return nodeId;
2321
2619
  }
2322
- expandDefinedComponent(definition) {
2620
+ expandDefinedComponent(definition, invocationArgs, parentContext) {
2621
+ const context = {
2622
+ args: invocationArgs,
2623
+ providedArgNames: new Set(Object.keys(invocationArgs)),
2624
+ usedArgNames: /* @__PURE__ */ new Set(),
2625
+ definitionName: definition.name,
2626
+ definitionKind: "component",
2627
+ allowChildrenSlot: false
2628
+ };
2323
2629
  if (definition.body.type === "layout") {
2324
- return this.convertLayout(definition.body);
2630
+ const result = this.convertLayout(definition.body, context);
2631
+ this.reportUnusedArguments(context);
2632
+ return result;
2325
2633
  } else if (definition.body.type === "component") {
2326
- return this.convertComponent(definition.body);
2634
+ const result = this.convertComponent(definition.body, context);
2635
+ this.reportUnusedArguments(context);
2636
+ return result;
2327
2637
  } else {
2328
2638
  throw new Error(`Invalid defined component body type for "${definition.name}"`);
2329
2639
  }
2330
2640
  }
2641
+ expandDefinedLayout(definition, invocationParams, invocationChildren, parentContext) {
2642
+ if (invocationChildren.length !== 1) {
2643
+ this.errors.push({
2644
+ type: "layout-children-arity",
2645
+ message: `Layout "${definition.name}" expects exactly one child, received ${invocationChildren.length}.`
2646
+ });
2647
+ }
2648
+ const rawSlot = invocationChildren[0];
2649
+ const resolvedSlot = rawSlot ? this.resolveChildrenSlot(rawSlot, parentContext) : void 0;
2650
+ const context = {
2651
+ args: invocationParams,
2652
+ providedArgNames: new Set(Object.keys(invocationParams)),
2653
+ usedArgNames: /* @__PURE__ */ new Set(),
2654
+ definitionName: definition.name,
2655
+ definitionKind: "layout",
2656
+ allowChildrenSlot: true,
2657
+ childrenSlot: resolvedSlot
2658
+ };
2659
+ const nodeId = this.convertLayout(definition.body, context);
2660
+ this.reportUnusedArguments(context);
2661
+ return nodeId;
2662
+ }
2663
+ resolveChildrenSlot(slot, parentContext) {
2664
+ if (slot.type === "component" && slot.componentType === "Children") {
2665
+ if (parentContext?.allowChildrenSlot) {
2666
+ return parentContext.childrenSlot;
2667
+ }
2668
+ this.errors.push({
2669
+ type: "children-slot-outside-layout-definition",
2670
+ message: '"Children" placeholder forwarding is only valid inside define Layout bodies.'
2671
+ });
2672
+ return void 0;
2673
+ }
2674
+ return slot;
2675
+ }
2676
+ convertASTNode(node, context) {
2677
+ if (node.type === "layout") return this.convertLayout(node, context);
2678
+ if (node.type === "component") return this.convertComponent(node, context);
2679
+ return this.convertCell(node, context);
2680
+ }
2681
+ resolveLayoutParams(layoutType, params, context) {
2682
+ const resolved = {};
2683
+ for (const [key, value] of Object.entries(params)) {
2684
+ const resolvedValue = this.resolveBindingValue(
2685
+ value,
2686
+ context,
2687
+ "layout-parameter",
2688
+ layoutType,
2689
+ key
2690
+ );
2691
+ if (resolvedValue !== void 0) {
2692
+ resolved[key] = resolvedValue;
2693
+ }
2694
+ }
2695
+ return resolved;
2696
+ }
2697
+ normalizeSplitParams(params) {
2698
+ const normalized = { ...params };
2699
+ if (normalized.sidebar !== void 0 && normalized.left === void 0 && normalized.right === void 0) {
2700
+ normalized.left = normalized.sidebar;
2701
+ this.warnings.push({
2702
+ type: "split-sidebar-deprecated",
2703
+ message: 'Split parameter "sidebar" is deprecated. Use "left" or "right".'
2704
+ });
2705
+ }
2706
+ delete normalized.sidebar;
2707
+ const hasLeft = normalized.left !== void 0;
2708
+ const hasRight = normalized.right !== void 0;
2709
+ if (hasLeft && hasRight) {
2710
+ delete normalized.right;
2711
+ this.warnings.push({
2712
+ type: "split-side-conflict",
2713
+ message: 'Split layout received both "left" and "right"; keeping "left".'
2714
+ });
2715
+ }
2716
+ if (!hasLeft && !hasRight) {
2717
+ normalized.left = 250;
2718
+ this.warnings.push({
2719
+ type: "split-side-missing",
2720
+ message: 'Split layout missing both "left" and "right"; defaulting to left: 250.'
2721
+ });
2722
+ }
2723
+ if (normalized.left !== void 0) {
2724
+ const leftWidth = Number(normalized.left);
2725
+ if (!Number.isFinite(leftWidth) || leftWidth <= 0) {
2726
+ normalized.left = 250;
2727
+ this.warnings.push({
2728
+ type: "split-left-invalid",
2729
+ message: 'Split "left" must be a positive number. Falling back to 250.'
2730
+ });
2731
+ }
2732
+ }
2733
+ if (normalized.right !== void 0) {
2734
+ const rightWidth = Number(normalized.right);
2735
+ if (!Number.isFinite(rightWidth) || rightWidth <= 0) {
2736
+ normalized.right = 250;
2737
+ this.warnings.push({
2738
+ type: "split-right-invalid",
2739
+ message: 'Split "right" must be a positive number. Falling back to 250.'
2740
+ });
2741
+ }
2742
+ }
2743
+ return normalized;
2744
+ }
2745
+ resolveComponentProps(componentType, props, context) {
2746
+ const resolved = {};
2747
+ for (const [key, value] of Object.entries(props)) {
2748
+ const resolvedValue = this.resolveBindingValue(
2749
+ value,
2750
+ context,
2751
+ "component-property",
2752
+ componentType,
2753
+ key
2754
+ );
2755
+ if (resolvedValue !== void 0) {
2756
+ resolved[key] = resolvedValue;
2757
+ }
2758
+ }
2759
+ return resolved;
2760
+ }
2761
+ resolveBindingValue(value, context, kind, targetType, targetName) {
2762
+ if (typeof value !== "string" || !value.startsWith("prop_")) {
2763
+ return value;
2764
+ }
2765
+ const argName = value.slice("prop_".length);
2766
+ if (!context) {
2767
+ return value;
2768
+ }
2769
+ if (Object.prototype.hasOwnProperty.call(context.args, argName)) {
2770
+ context.usedArgNames.add(argName);
2771
+ return context.args[argName];
2772
+ }
2773
+ const required = this.isBindingTargetRequired(kind, targetType, targetName);
2774
+ const descriptor = kind === "component-property" ? "property" : "parameter";
2775
+ const message = `Missing required bound ${descriptor} "${targetName}" for ${kind === "component-property" ? "component" : "layout"} "${targetType}" in ${context.definitionKind} "${context.definitionName}" (expected arg "${argName}").`;
2776
+ if (required) {
2777
+ this.errors.push({ type: "missing-required-bound-value", message });
2778
+ } else {
2779
+ this.warnings.push({
2780
+ type: "missing-bound-value",
2781
+ 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}".`
2782
+ });
2783
+ }
2784
+ return void 0;
2785
+ }
2786
+ isBindingTargetRequired(kind, targetType, targetName) {
2787
+ if (kind === "component-property") {
2788
+ const metadata = import_components2.COMPONENTS[targetType];
2789
+ const property2 = metadata?.properties?.[targetName];
2790
+ if (!property2) return false;
2791
+ return property2.required === true && property2.defaultValue === void 0;
2792
+ }
2793
+ const layoutMetadata = import_components2.LAYOUTS[targetType];
2794
+ if (!layoutMetadata) return false;
2795
+ const property = layoutMetadata.properties?.[targetName];
2796
+ const requiredFromProperty = property?.required === true && property.defaultValue === void 0;
2797
+ const requiredFromLayout = (layoutMetadata.requiredProperties || []).includes(targetName);
2798
+ return requiredFromProperty || requiredFromLayout;
2799
+ }
2800
+ reportUnusedArguments(context) {
2801
+ for (const arg of context.providedArgNames) {
2802
+ if (!context.usedArgNames.has(arg)) {
2803
+ this.warnings.push({
2804
+ type: "unused-definition-argument",
2805
+ message: `Argument "${arg}" is not used by ${context.definitionKind} "${context.definitionName}".`
2806
+ });
2807
+ }
2808
+ }
2809
+ }
2331
2810
  cleanParams(params) {
2332
2811
  const cleaned = {};
2333
2812
  for (const [key, value] of Object.entries(params)) {
@@ -2377,9 +2856,24 @@ var ICON_SIZES_BY_DENSITY = {
2377
2856
  comfortable: { xs: 14, sm: 16, md: 20, lg: 28, xl: 36 }
2378
2857
  };
2379
2858
  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 }
2859
+ compact: { sm: 20, md: 24, lg: 32 },
2860
+ normal: { sm: 24, md: 32, lg: 40 },
2861
+ comfortable: { sm: 28, md: 40, lg: 48 }
2862
+ };
2863
+ var CONTROL_HEIGHTS_BY_DENSITY = {
2864
+ compact: { sm: 28, md: 32, lg: 36 },
2865
+ normal: { sm: 36, md: 40, lg: 48 },
2866
+ comfortable: { sm: 40, md: 48, lg: 56 }
2867
+ };
2868
+ var ACTION_CONTROL_HEIGHTS_BY_DENSITY = {
2869
+ compact: { sm: 20, md: 24, lg: 32 },
2870
+ normal: { sm: 24, md: 32, lg: 40 },
2871
+ comfortable: { sm: 28, md: 40, lg: 48 }
2872
+ };
2873
+ var CONTROL_PADDING_BY_DENSITY = {
2874
+ compact: { none: 0, xs: 4, sm: 8, md: 10, lg: 14, xl: 18 },
2875
+ normal: { none: 0, xs: 6, sm: 10, md: 14, lg: 18, xl: 24 },
2876
+ comfortable: { none: 0, xs: 8, sm: 12, md: 16, lg: 22, xl: 28 }
2383
2877
  };
2384
2878
  function resolveIconSize(size, density = "normal") {
2385
2879
  const map = ICON_SIZES_BY_DENSITY[density] || ICON_SIZES_BY_DENSITY.normal;
@@ -2389,6 +2883,18 @@ function resolveIconButtonSize(size, density = "normal") {
2389
2883
  const map = ICON_BUTTON_SIZES_BY_DENSITY[density] || ICON_BUTTON_SIZES_BY_DENSITY.normal;
2390
2884
  return map[size || "md"] || map.md;
2391
2885
  }
2886
+ function resolveControlHeight(size, density = "normal") {
2887
+ const map = CONTROL_HEIGHTS_BY_DENSITY[density] || CONTROL_HEIGHTS_BY_DENSITY.normal;
2888
+ return map[size || "md"] || map.md;
2889
+ }
2890
+ function resolveActionControlHeight(size, density = "normal") {
2891
+ const map = ACTION_CONTROL_HEIGHTS_BY_DENSITY[density] || ACTION_CONTROL_HEIGHTS_BY_DENSITY.normal;
2892
+ return map[size || "md"] || map.md;
2893
+ }
2894
+ function resolveControlHorizontalPadding(padding, density = "normal") {
2895
+ const map = CONTROL_PADDING_BY_DENSITY[density] || CONTROL_PADDING_BY_DENSITY.normal;
2896
+ return map[padding || "md"] ?? map.md;
2897
+ }
2392
2898
 
2393
2899
  // src/shared/heading-levels.ts
2394
2900
  var DEFAULT_LEVEL = "h2";
@@ -2707,6 +3213,41 @@ var LayoutEngine = class {
2707
3213
  }
2708
3214
  return totalHeight;
2709
3215
  }
3216
+ if (node.containerType === "split") {
3217
+ const splitGap = this.resolveSpacing(node.style.gap);
3218
+ const leftParam = node.params.left;
3219
+ const rightParam = node.params.right;
3220
+ const leftWidthRaw = Number(leftParam);
3221
+ const rightWidthRaw = Number(rightParam);
3222
+ const hasLeft = leftParam !== void 0;
3223
+ const hasRight = rightParam !== void 0;
3224
+ const leftWidth = Number.isFinite(leftWidthRaw) && leftWidthRaw > 0 ? leftWidthRaw : availableWidth / 2;
3225
+ const rightWidth = Number.isFinite(rightWidthRaw) && rightWidthRaw > 0 ? rightWidthRaw : availableWidth / 2;
3226
+ let maxHeight = 0;
3227
+ node.children.forEach((childRef, index) => {
3228
+ const child = this.nodes[childRef.ref];
3229
+ let childHeight = this.getComponentHeight();
3230
+ const isFirst = index === 0;
3231
+ let childWidth;
3232
+ if (node.children.length >= 2) {
3233
+ if (hasRight && !hasLeft) {
3234
+ childWidth = isFirst ? Math.max(1, availableWidth - rightWidth - splitGap) : rightWidth;
3235
+ } else {
3236
+ childWidth = isFirst ? leftWidth : Math.max(1, availableWidth - leftWidth - splitGap);
3237
+ }
3238
+ } else {
3239
+ childWidth = availableWidth;
3240
+ }
3241
+ if (child?.kind === "component") {
3242
+ childHeight = child.props.height ? Number(child.props.height) : this.getIntrinsicComponentHeight(child, childWidth);
3243
+ } else if (child?.kind === "container") {
3244
+ childHeight = this.calculateContainerHeight(child, childWidth);
3245
+ }
3246
+ maxHeight = Math.max(maxHeight, childHeight);
3247
+ });
3248
+ totalHeight += maxHeight;
3249
+ return totalHeight;
3250
+ }
2710
3251
  const direction = node.params.direction || "vertical";
2711
3252
  if (node.containerType === "stack" && direction === "horizontal") {
2712
3253
  let maxHeight = 0;
@@ -2829,14 +3370,28 @@ var LayoutEngine = class {
2829
3370
  calculateSplit(node, x, y, width, height) {
2830
3371
  if (node.kind !== "container") return;
2831
3372
  const gap = this.resolveSpacing(node.style.gap);
2832
- const sidebarWidth = Number(node.params.sidebar) || 260;
3373
+ const leftParam = node.params.left;
3374
+ const rightParam = node.params.right;
3375
+ const leftWidthRaw = Number(leftParam);
3376
+ const rightWidthRaw = Number(rightParam);
3377
+ const hasLeft = leftParam !== void 0;
3378
+ const hasRight = rightParam !== void 0;
3379
+ const leftWidth = Number.isFinite(leftWidthRaw) && leftWidthRaw > 0 ? leftWidthRaw : 250;
3380
+ const rightWidth = Number.isFinite(rightWidthRaw) && rightWidthRaw > 0 ? rightWidthRaw : 250;
2833
3381
  if (node.children.length === 1) {
2834
3382
  this.calculateNode(node.children[0].ref, x, y, width, height, "split");
2835
3383
  } 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");
3384
+ if (hasRight && !hasLeft) {
3385
+ const flexibleLeftWidth = Math.max(1, width - rightWidth - gap);
3386
+ const rightX = x + flexibleLeftWidth + gap;
3387
+ this.calculateNode(node.children[0].ref, x, y, flexibleLeftWidth, height, "split");
3388
+ this.calculateNode(node.children[1].ref, rightX, y, rightWidth, height, "split");
3389
+ } else {
3390
+ const flexibleRightWidth = Math.max(1, width - leftWidth - gap);
3391
+ const rightX = x + leftWidth + gap;
3392
+ this.calculateNode(node.children[0].ref, x, y, leftWidth, height, "split");
3393
+ this.calculateNode(node.children[1].ref, rightX, y, flexibleRightWidth, height, "split");
3394
+ }
2840
3395
  }
2841
3396
  }
2842
3397
  calculatePanel(node, x, y, width, height) {
@@ -2985,7 +3540,11 @@ var LayoutEngine = class {
2985
3540
  }
2986
3541
  getIntrinsicComponentHeight(node, availableWidth) {
2987
3542
  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;
3543
+ const controlSize = String(node.props.size || "md");
3544
+ const density = this.style.density || "normal";
3545
+ const inputControlHeight = resolveControlHeight(controlSize, density);
3546
+ const actionControlHeight = resolveActionControlHeight(controlSize, density);
3547
+ 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
3548
  if (node.componentType === "Image") {
2990
3549
  const placeholder = String(node.props.placeholder || "landscape");
2991
3550
  const aspectRatios = {
@@ -3012,12 +3571,26 @@ var LayoutEngine = class {
3012
3571
  }
3013
3572
  const rowCount = Number(node.props.rows || node.props.rowsMock || 5);
3014
3573
  const hasTitle = !!node.props.title;
3015
- const hasPagination = String(node.props.pagination) === "true";
3574
+ const hasPagination = this.parseBooleanProp(node.props.pagination, false);
3575
+ const hasCaption = String(node.props.caption || "").trim().length > 0;
3576
+ const paginationAlign = String(node.props.paginationAlign || "right");
3577
+ const captionAlign = String(node.props.captionAlign || "");
3578
+ const effectiveCaptionAlign = captionAlign === "left" || captionAlign === "center" || captionAlign === "right" ? captionAlign : paginationAlign === "left" ? "right" : "left";
3579
+ const sameFooterAlign = hasCaption && hasPagination && effectiveCaptionAlign === paginationAlign;
3016
3580
  const headerHeight = 44;
3017
3581
  const rowHeight = 36;
3018
3582
  const titleHeight = hasTitle ? 32 : 0;
3019
- const paginationHeight = hasPagination ? 64 : 0;
3020
- return titleHeight + headerHeight + rowCount * rowHeight + paginationHeight;
3583
+ let footerHeight = 0;
3584
+ if (hasPagination || hasCaption) {
3585
+ const footerBottomPadding = 12;
3586
+ footerHeight += 16;
3587
+ footerHeight += hasPagination ? 32 : 18;
3588
+ if (sameFooterAlign) {
3589
+ footerHeight += 8 + 18;
3590
+ }
3591
+ footerHeight += footerBottomPadding;
3592
+ }
3593
+ return titleHeight + headerHeight + rowCount * rowHeight + footerHeight;
3021
3594
  }
3022
3595
  if (node.componentType === "Heading") {
3023
3596
  const text = String(node.props.text || "Heading");
@@ -3026,15 +3599,15 @@ var LayoutEngine = class {
3026
3599
  const maxWidth = availableWidth && availableWidth > 0 ? availableWidth : 200;
3027
3600
  const lines = this.wrapTextToLines(text, maxWidth, fontSize);
3028
3601
  const wrappedHeight = Math.max(1, lines.length) * lineHeightPx;
3029
- const density = this.style.density || "normal";
3030
- const verticalPadding = resolveHeadingVerticalPadding(node.props.spacing, density);
3602
+ const density2 = this.style.density || "normal";
3603
+ const verticalPadding = resolveHeadingVerticalPadding(node.props.spacing, density2);
3031
3604
  if (verticalPadding === null) {
3032
3605
  return Math.max(this.getComponentHeight(), wrappedHeight);
3033
3606
  }
3034
3607
  return Math.max(1, Math.ceil(wrappedHeight + verticalPadding * 2));
3035
3608
  }
3036
3609
  if (node.componentType === "Text") {
3037
- const content = String(node.props.content || "");
3610
+ const content = String(node.props.text || "");
3038
3611
  const { fontSize, lineHeight } = this.getTextMetricsForDensity();
3039
3612
  const lineHeightPx = Math.ceil(fontSize * lineHeight);
3040
3613
  const maxWidth = availableWidth && availableWidth > 0 ? availableWidth : 200;
@@ -3083,7 +3656,10 @@ var LayoutEngine = class {
3083
3656
  if (node.componentType === "Divider") return 1;
3084
3657
  if (node.componentType === "Separate") return this.getSeparateSize(node);
3085
3658
  if (node.componentType === "Input" || node.componentType === "Select") {
3086
- return this.getComponentHeight() + controlLabelOffset;
3659
+ return inputControlHeight + controlLabelOffset;
3660
+ }
3661
+ if (node.componentType === "Button" || node.componentType === "IconButton" || node.componentType === "Link") {
3662
+ return actionControlHeight + controlLabelOffset;
3087
3663
  }
3088
3664
  return this.getComponentHeight();
3089
3665
  }
@@ -3100,7 +3676,10 @@ var LayoutEngine = class {
3100
3676
  }
3101
3677
  if (node.componentType === "IconButton") {
3102
3678
  const size = String(node.props.size || "md");
3103
- return resolveIconButtonSize(size, this.style.density || "normal");
3679
+ const density = this.style.density || "normal";
3680
+ const baseSize = resolveIconButtonSize(size, density);
3681
+ const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
3682
+ return baseSize + extraPadding * 2;
3104
3683
  }
3105
3684
  if (node.componentType === "Checkbox" || node.componentType === "Radio") {
3106
3685
  return 24;
@@ -3109,11 +3688,13 @@ var LayoutEngine = class {
3109
3688
  if (node.componentType === "Button" || node.componentType === "Link") {
3110
3689
  const text = String(node.props.text || "");
3111
3690
  const { fontSize, paddingX } = this.getButtonMetricsForDensity();
3691
+ const density = this.style.density || "normal";
3692
+ const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
3112
3693
  const textWidth = this.estimateTextWidth(text, fontSize);
3113
- return Math.max(60, Math.ceil(textWidth + paddingX * 2));
3694
+ return Math.max(60, Math.ceil(textWidth + (paddingX + extraPadding) * 2));
3114
3695
  }
3115
3696
  if (node.componentType === "Label" || node.componentType === "Text") {
3116
- const text = String(node.props.content || node.props.text || "");
3697
+ const text = String(node.props.text || "");
3117
3698
  return Math.max(60, text.length * 8 + 16);
3118
3699
  }
3119
3700
  if (node.componentType === "Heading") {
@@ -3975,7 +4556,7 @@ var THEMES = {
3975
4556
  bg: "#F8FAFC",
3976
4557
  cardBg: "#FFFFFF",
3977
4558
  border: "#E2E8F0",
3978
- text: "#1E293B",
4559
+ text: "#000000",
3979
4560
  textMuted: "#64748B",
3980
4561
  primary: "#3B82F6",
3981
4562
  primaryHover: "#2563EB",
@@ -3985,7 +4566,7 @@ var THEMES = {
3985
4566
  bg: "#0F172A",
3986
4567
  cardBg: "#1E293B",
3987
4568
  border: "#334155",
3988
- text: "#F1F5F9",
4569
+ text: "#FFFFFF",
3989
4570
  textMuted: "#94A3B8",
3990
4571
  primary: "#60A5FA",
3991
4572
  primaryHover: "#3B82F6",
@@ -3995,7 +4576,7 @@ var THEMES = {
3995
4576
  var SVGRenderer = class {
3996
4577
  constructor(ir, layout, options) {
3997
4578
  this.renderedNodeIds = /* @__PURE__ */ new Set();
3998
- this.fontFamily = "system-ui, -apple-system, sans-serif";
4579
+ this.fontFamily = "Arial, Helvetica, sans-serif";
3999
4580
  this.parentContainerByChildId = /* @__PURE__ */ new Map();
4000
4581
  this.ir = ir;
4001
4582
  this.layout = layout;
@@ -4009,7 +4590,6 @@ var SVGRenderer = class {
4009
4590
  includeLabels: options?.includeLabels ?? true,
4010
4591
  screenName: options?.screenName
4011
4592
  };
4012
- this.renderTheme = THEMES[this.options.theme];
4013
4593
  this.colorResolver = new ColorResolver();
4014
4594
  this.buildParentContainerIndex();
4015
4595
  if (ir.project.mocks && Object.keys(ir.project.mocks).length > 0) {
@@ -4018,6 +4598,12 @@ var SVGRenderer = class {
4018
4598
  if (ir.project.colors && Object.keys(ir.project.colors).length > 0) {
4019
4599
  this.colorResolver.setCustomColors(ir.project.colors);
4020
4600
  }
4601
+ const themeDefaults = THEMES[this.options.theme];
4602
+ this.renderTheme = {
4603
+ ...themeDefaults,
4604
+ text: this.resolveTextColor(),
4605
+ textMuted: this.resolveMutedColor()
4606
+ };
4021
4607
  }
4022
4608
  /**
4023
4609
  * Get list of available screens in the project
@@ -4099,6 +4685,9 @@ var SVGRenderer = class {
4099
4685
  if (node.containerType === "card") {
4100
4686
  this.renderCardBorder(node, pos, containerGroup);
4101
4687
  }
4688
+ if (node.containerType === "split") {
4689
+ this.renderSplitDecoration(node, pos, containerGroup);
4690
+ }
4102
4691
  node.children.forEach((childRef) => {
4103
4692
  this.renderNode(childRef.ref, containerGroup);
4104
4693
  });
@@ -4186,6 +4775,8 @@ var SVGRenderer = class {
4186
4775
  }
4187
4776
  renderHeading(node, pos) {
4188
4777
  const text = String(node.props.text || "Heading");
4778
+ const variant = String(node.props.variant || "default");
4779
+ const headingColor = variant === "default" ? this.resolveTextColor() : this.resolveVariantColor(variant, this.resolveTextColor());
4189
4780
  const headingTypography = this.getHeadingTypography(node);
4190
4781
  const fontSize = headingTypography.fontSize;
4191
4782
  const fontWeight = headingTypography.fontWeight;
@@ -4195,10 +4786,10 @@ var SVGRenderer = class {
4195
4786
  if (lines.length <= 1) {
4196
4787
  return `<g${this.getDataNodeId(node)}>
4197
4788
  <text x="${pos.x}" y="${firstLineY}"
4198
- font-family="system-ui, -apple-system, sans-serif"
4789
+ font-family="Arial, Helvetica, sans-serif"
4199
4790
  font-size="${fontSize}"
4200
4791
  font-weight="${fontWeight}"
4201
- fill="${this.renderTheme.text}">${this.escapeXml(text)}</text>
4792
+ fill="${headingColor}">${this.escapeXml(text)}</text>
4202
4793
  </g>`;
4203
4794
  }
4204
4795
  const tspans = lines.map(
@@ -4206,41 +4797,46 @@ var SVGRenderer = class {
4206
4797
  ).join("");
4207
4798
  return `<g${this.getDataNodeId(node)}>
4208
4799
  <text x="${pos.x}" y="${firstLineY}"
4209
- font-family="system-ui, -apple-system, sans-serif"
4800
+ font-family="Arial, Helvetica, sans-serif"
4210
4801
  font-size="${fontSize}"
4211
4802
  font-weight="${fontWeight}"
4212
- fill="${this.renderTheme.text}">${tspans}</text>
4803
+ fill="${headingColor}">${tspans}</text>
4213
4804
  </g>`;
4214
4805
  }
4215
4806
  renderButton(node, pos) {
4216
4807
  const text = String(node.props.text || "Button");
4217
4808
  const variant = String(node.props.variant || "default");
4809
+ const size = String(node.props.size || "md");
4810
+ const density = this.ir.project.style.density || "normal";
4811
+ const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
4812
+ const labelOffset = this.parseBooleanProp(node.props.labelSpace, false) ? 18 : 0;
4218
4813
  const fullWidth = this.shouldButtonFillAvailableWidth(node);
4219
4814
  const radius = this.tokens.button.radius;
4220
4815
  const fontSize = this.tokens.button.fontSize;
4221
4816
  const fontWeight = this.tokens.button.fontWeight;
4222
4817
  const paddingX = this.tokens.button.paddingX;
4223
- const paddingY = this.tokens.button.paddingY;
4818
+ const controlHeight = resolveActionControlHeight(size, density);
4819
+ const buttonY = pos.y + labelOffset;
4820
+ const buttonHeight = Math.max(16, Math.min(controlHeight, pos.height - labelOffset));
4224
4821
  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);
4822
+ const buttonWidth = fullWidth ? Math.max(1, pos.width) : this.clampControlWidth(Math.max(Math.ceil(idealTextWidth + (paddingX + extraPadding) * 2), 60), pos.width);
4823
+ const availableTextWidth = Math.max(0, buttonWidth - (paddingX + extraPadding) * 2);
4228
4824
  const visibleText = this.truncateTextToWidth(text, availableTextWidth, fontSize);
4229
4825
  const semanticBase = this.getSemanticVariantColor(variant);
4230
4826
  const hasExplicitVariantColor = semanticBase !== void 0 || this.colorResolver.hasColor(variant);
4231
4827
  const resolvedBase = this.resolveVariantColor(variant, this.renderTheme.primary);
4232
4828
  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)";
4829
+ const textColor = hasExplicitVariantColor ? "#FFFFFF" : this.hexToRgba(this.resolveTextColor(), 0.85);
4234
4830
  const borderColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.7) : "rgba(100, 116, 139, 0.4)";
4235
4831
  return `<g${this.getDataNodeId(node)}>
4236
- <rect x="${pos.x}" y="${pos.y}"
4832
+ <rect x="${pos.x}" y="${buttonY}"
4237
4833
  width="${buttonWidth}" height="${buttonHeight}"
4238
4834
  rx="${radius}"
4239
4835
  fill="${bgColor}"
4240
4836
  stroke="${borderColor}"
4241
4837
  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"
4838
+ <text x="${pos.x + buttonWidth / 2}" y="${buttonY + buttonHeight / 2 + fontSize * 0.35}"
4839
+ font-family="Arial, Helvetica, sans-serif"
4244
4840
  font-size="${fontSize}"
4245
4841
  font-weight="${fontWeight}"
4246
4842
  fill="${textColor}"
@@ -4250,17 +4846,18 @@ var SVGRenderer = class {
4250
4846
  renderLink(node, pos) {
4251
4847
  const text = String(node.props.text || "Link");
4252
4848
  const variant = String(node.props.variant || "primary");
4849
+ const size = String(node.props.size || "md");
4850
+ const density = this.ir.project.style.density || "normal";
4253
4851
  const fontSize = this.tokens.button.fontSize;
4254
4852
  const fontWeight = this.tokens.button.fontWeight;
4255
4853
  const paddingX = this.tokens.button.paddingX;
4256
- const paddingY = this.tokens.button.paddingY;
4257
4854
  const linkColor = this.resolveVariantColor(variant, this.renderTheme.primary);
4258
4855
  const idealTextWidth = this.estimateTextWidth(text, fontSize);
4259
4856
  const linkWidth = this.clampControlWidth(
4260
4857
  Math.max(Math.ceil(idealTextWidth + paddingX * 2), 60),
4261
4858
  pos.width
4262
4859
  );
4263
- const linkHeight = fontSize + paddingY * 2;
4860
+ const linkHeight = Math.max(16, Math.min(resolveActionControlHeight(size, density), pos.height));
4264
4861
  const availableTextWidth = Math.max(0, linkWidth - paddingX * 2);
4265
4862
  const visibleText = this.truncateTextToWidth(text, availableTextWidth, fontSize);
4266
4863
  const visibleTextWidth = Math.min(
@@ -4271,7 +4868,7 @@ var SVGRenderer = class {
4271
4868
  const underlineY = centerY + 3;
4272
4869
  return `<g${this.getDataNodeId(node)}>
4273
4870
  <text x="${pos.x + linkWidth / 2}" y="${centerY}"
4274
- font-family="system-ui, -apple-system, sans-serif"
4871
+ font-family="Arial, Helvetica, sans-serif"
4275
4872
  font-size="${fontSize}"
4276
4873
  font-weight="${fontWeight}"
4277
4874
  fill="${linkColor}"
@@ -4293,7 +4890,7 @@ var SVGRenderer = class {
4293
4890
  const controlHeight = Math.max(16, pos.height - labelOffset);
4294
4891
  return `<g${this.getDataNodeId(node)}>
4295
4892
  ${label ? `<text x="${pos.x + paddingX}" y="${this.getControlLabelBaselineY(pos.y)}"
4296
- font-family="system-ui, -apple-system, sans-serif"
4893
+ font-family="Arial, Helvetica, sans-serif"
4297
4894
  font-size="12"
4298
4895
  fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>` : ""}
4299
4896
  <rect x="${pos.x}" y="${controlY}"
@@ -4303,7 +4900,7 @@ var SVGRenderer = class {
4303
4900
  stroke="${this.renderTheme.border}"
4304
4901
  stroke-width="1"/>
4305
4902
  <text x="${pos.x + paddingX}" y="${controlY + controlHeight / 2 + 5}"
4306
- font-family="system-ui, -apple-system, sans-serif"
4903
+ font-family="Arial, Helvetica, sans-serif"
4307
4904
  font-size="${fontSize}"
4308
4905
  fill="${this.renderTheme.textMuted}">${this.escapeXml(placeholder)}</text>
4309
4906
  </g>`;
@@ -4313,25 +4910,42 @@ var SVGRenderer = class {
4313
4910
  const subtitle = String(node.props.subtitle || "");
4314
4911
  const actions = String(node.props.actions || "");
4315
4912
  const user = String(node.props.user || "");
4316
- const accentColor = this.resolveAccentColor();
4913
+ const variant = String(node.props.variant || "default");
4914
+ const accentColor = variant === "default" ? this.resolveAccentColor() : this.resolveVariantColor(variant, this.resolveAccentColor());
4915
+ const showBorder = this.parseBooleanProp(node.props.border, false);
4916
+ const showBackground = this.parseBooleanProp(node.props.background ?? node.props.backround, false);
4917
+ const radiusMap = {
4918
+ none: 0,
4919
+ sm: 4,
4920
+ md: this.tokens.card.radius,
4921
+ lg: 12,
4922
+ xl: 16
4923
+ };
4924
+ const topbarRadius = radiusMap[String(node.props.radius || "md")] ?? this.tokens.card.radius;
4317
4925
  const topbar = this.calculateTopbarLayout(node, pos, title, subtitle, actions, user);
4318
- let svg = `<g${this.getDataNodeId(node)}>
4926
+ let svg = `<g${this.getDataNodeId(node)}>`;
4927
+ if (showBorder || showBackground) {
4928
+ const bg = showBackground ? this.renderTheme.cardBg : "none";
4929
+ const stroke = showBorder ? this.renderTheme.border : "none";
4930
+ svg += `
4319
4931
  <rect x="${pos.x}" y="${pos.y}"
4320
4932
  width="${pos.width}" height="${pos.height}"
4321
- fill="${this.renderTheme.cardBg}"
4322
- stroke="${this.renderTheme.border}"
4323
- stroke-width="1"/>
4324
-
4933
+ rx="${topbarRadius}"
4934
+ fill="${bg}"
4935
+ stroke="${stroke}"
4936
+ stroke-width="1"/>`;
4937
+ }
4938
+ svg += `
4325
4939
  <!-- Title -->
4326
4940
  <text x="${topbar.textX}" y="${topbar.titleY}"
4327
- font-family="system-ui, -apple-system, sans-serif"
4941
+ font-family="Arial, Helvetica, sans-serif"
4328
4942
  font-size="18"
4329
4943
  font-weight="600"
4330
4944
  fill="${this.renderTheme.text}">${this.escapeXml(topbar.visibleTitle)}</text>`;
4331
4945
  if (topbar.hasSubtitle) {
4332
4946
  svg += `
4333
4947
  <text x="${topbar.textX}" y="${topbar.subtitleY}"
4334
- font-family="system-ui, -apple-system, sans-serif"
4948
+ font-family="Arial, Helvetica, sans-serif"
4335
4949
  font-size="13"
4336
4950
  fill="${this.renderTheme.textMuted}">${this.escapeXml(topbar.visibleSubtitle)}</text>`;
4337
4951
  }
@@ -4359,7 +4973,7 @@ var SVGRenderer = class {
4359
4973
  fill="${accentColor}"
4360
4974
  stroke="none"/>
4361
4975
  <text x="${action.x + action.width / 2}" y="${action.y + action.height / 2 + 4}"
4362
- font-family="system-ui, -apple-system, sans-serif"
4976
+ font-family="Arial, Helvetica, sans-serif"
4363
4977
  font-size="12"
4364
4978
  font-weight="600"
4365
4979
  fill="white"
@@ -4375,7 +4989,7 @@ var SVGRenderer = class {
4375
4989
  stroke="${this.renderTheme.border}"
4376
4990
  stroke-width="1"/>
4377
4991
  <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"
4992
+ font-family="Arial, Helvetica, sans-serif"
4379
4993
  font-size="12"
4380
4994
  fill="${this.renderTheme.text}"
4381
4995
  text-anchor="middle">${this.escapeXml(topbar.userBadge.label)}</text>`;
@@ -4438,77 +5052,165 @@ var SVGRenderer = class {
4438
5052
  </g>`;
4439
5053
  output.push(svg);
4440
5054
  }
5055
+ renderSplitDecoration(node, pos, output) {
5056
+ if (node.kind !== "container") return;
5057
+ const gap = this.resolveSpacing(node.style.gap);
5058
+ const leftParam = Number(node.params.left);
5059
+ const rightParam = Number(node.params.right);
5060
+ const hasLeft = node.params.left !== void 0;
5061
+ const hasRight = node.params.right !== void 0 && node.params.left === void 0;
5062
+ const fixedLeftWidth = Number.isFinite(leftParam) && leftParam > 0 ? leftParam : 250;
5063
+ const fixedRightWidth = Number.isFinite(rightParam) && rightParam > 0 ? rightParam : 250;
5064
+ const backgroundKey = String(node.style.background || "").trim();
5065
+ const showBorder = this.parseBooleanProp(node.params.border, false);
5066
+ if (backgroundKey) {
5067
+ const fill = this.colorResolver.resolveColor(backgroundKey, this.renderTheme.cardBg);
5068
+ if (hasRight) {
5069
+ const panelX = pos.x + Math.max(0, pos.width - fixedRightWidth);
5070
+ output.push(`<g>
5071
+ <rect x="${panelX}" y="${pos.y}" width="${Math.max(1, fixedRightWidth)}" height="${pos.height}" fill="${fill}" stroke="none"/>
5072
+ </g>`);
5073
+ } else if (hasLeft || !hasRight) {
5074
+ output.push(`<g>
5075
+ <rect x="${pos.x}" y="${pos.y}" width="${Math.max(1, fixedLeftWidth)}" height="${pos.height}" fill="${fill}" stroke="none"/>
5076
+ </g>`);
5077
+ }
5078
+ }
5079
+ if (showBorder) {
5080
+ const dividerX = hasRight ? pos.x + Math.max(0, pos.width - fixedRightWidth - gap / 2) : pos.x + Math.max(0, fixedLeftWidth + gap / 2);
5081
+ output.push(`<g>
5082
+ <line x1="${dividerX}" y1="${pos.y}" x2="${dividerX}" y2="${pos.y + pos.height}" stroke="${this.renderTheme.border}" stroke-width="1"/>
5083
+ </g>`);
5084
+ }
5085
+ }
4441
5086
  renderTable(node, pos) {
4442
5087
  const title = String(node.props.title || "");
4443
5088
  const columnsStr = String(node.props.columns || "Col1,Col2,Col3");
4444
- const columns = columnsStr.split(",").map((c) => c.trim());
5089
+ const columns = columnsStr.split(",").map((c) => c.trim()).filter(Boolean);
4445
5090
  const rowCount = Number(node.props.rows || node.props.rowsMock || 5);
4446
5091
  const mockStr = String(node.props.mock || "");
4447
5092
  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);
5093
+ const pagination = this.parseBooleanProp(node.props.pagination, false);
5094
+ const parsedPageCount = Number(node.props.pages || 5);
5095
+ const pageCount = Number.isFinite(parsedPageCount) && parsedPageCount > 0 ? Math.floor(parsedPageCount) : 5;
4451
5096
  const paginationAlign = String(node.props.paginationAlign || "right");
5097
+ const actions = String(node.props.actions || "").split(",").map((value) => value.trim()).filter(Boolean);
5098
+ const hasActions = actions.length > 0;
5099
+ const caption = String(node.props.caption || "").trim();
5100
+ const hasCaption = caption.length > 0;
5101
+ const showOuterBorder = this.parseBooleanProp(node.props.border, false);
5102
+ const showOuterBackground = this.parseBooleanProp(
5103
+ node.props.background ?? node.props.backround,
5104
+ false
5105
+ );
5106
+ const showInnerBorder = this.parseBooleanProp(node.props.innerBorder, true);
5107
+ const rawCaptionAlign = String(node.props.captionAlign || "");
5108
+ const captionAlign = rawCaptionAlign === "left" || rawCaptionAlign === "center" || rawCaptionAlign === "right" ? rawCaptionAlign : paginationAlign === "left" ? "right" : "left";
5109
+ const sameFooterAlign = hasCaption && pagination && captionAlign === paginationAlign;
4452
5110
  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");
5111
+ const safeColumns = columns.length > 0 ? columns : ["Column"];
5112
+ while (mockTypes.length < safeColumns.length) {
5113
+ const inferred = MockDataGenerator.inferMockTypeFromColumn(safeColumns[mockTypes.length] || "item");
4455
5114
  mockTypes.push(inferred);
4456
5115
  }
4457
5116
  const headerHeight = 44;
4458
5117
  const rowHeight = 36;
4459
- const colWidth = pos.width / columns.length;
5118
+ const actionColumnWidth = hasActions ? Math.max(96, Math.min(180, actions.length * 26 + 28)) : 0;
5119
+ const dataWidth = Math.max(20, pos.width - actionColumnWidth);
5120
+ const dataColWidth = dataWidth / safeColumns.length;
4460
5121
  const mockRows = [];
4461
5122
  for (let rowIdx = 0; rowIdx < rowCount; rowIdx++) {
4462
5123
  const row = {};
4463
- columns.forEach((col, colIdx) => {
5124
+ safeColumns.forEach((col, colIdx) => {
4464
5125
  const mockType = mockTypes[colIdx] || MockDataGenerator.inferMockTypeFromColumn(col) || "item";
4465
5126
  row[col] = MockDataGenerator.getMockValue(mockType, rowIdx, random);
4466
5127
  });
4467
5128
  mockRows.push(row);
4468
5129
  }
4469
- let svg = `<g${this.getDataNodeId(node)}>
5130
+ let svg = `<g${this.getDataNodeId(node)}>`;
5131
+ if (showOuterBorder || showOuterBackground) {
5132
+ const outerFill = showOuterBackground ? this.renderTheme.cardBg : "none";
5133
+ const outerStroke = showOuterBorder ? this.renderTheme.border : "none";
5134
+ svg += `
4470
5135
  <rect x="${pos.x}" y="${pos.y}"
4471
5136
  width="${pos.width}" height="${pos.height}"
4472
5137
  rx="8"
4473
- fill="${this.renderTheme.cardBg}"
4474
- stroke="${this.renderTheme.border}"
5138
+ fill="${outerFill}"
5139
+ stroke="${outerStroke}"
4475
5140
  stroke-width="1"/>`;
5141
+ }
4476
5142
  if (title) {
4477
5143
  svg += `
4478
5144
  <text x="${pos.x + 16}" y="${pos.y + 24}"
4479
- font-family="system-ui, -apple-system, sans-serif"
5145
+ font-family="Arial, Helvetica, sans-serif"
4480
5146
  font-size="13"
4481
5147
  font-weight="600"
4482
5148
  fill="${this.renderTheme.text}">${this.escapeXml(title)}</text>`;
4483
5149
  }
4484
5150
  const headerY = pos.y + (title ? 32 : 0);
4485
- svg += `
5151
+ if (showInnerBorder) {
5152
+ svg += `
4486
5153
  <line x1="${pos.x}" y1="${headerY + headerHeight}" x2="${pos.x + pos.width}" y2="${headerY + headerHeight}"
4487
5154
  stroke="${this.renderTheme.border}" stroke-width="1"/>`;
4488
- columns.forEach((col, i) => {
5155
+ }
5156
+ safeColumns.forEach((col, i) => {
4489
5157
  svg += `
4490
- <text x="${pos.x + i * colWidth + 12}" y="${headerY + 26}"
4491
- font-family="system-ui, -apple-system, sans-serif"
5158
+ <text x="${pos.x + i * dataColWidth + 12}" y="${headerY + 26}"
5159
+ font-family="Arial, Helvetica, sans-serif"
4492
5160
  font-size="11"
4493
5161
  font-weight="600"
4494
5162
  fill="${this.renderTheme.textMuted}">${this.escapeXml(col)}</text>`;
4495
5163
  });
5164
+ if (hasActions && showInnerBorder) {
5165
+ const dividerX = pos.x + dataWidth;
5166
+ svg += `
5167
+ <line x1="${dividerX}" y1="${headerY + headerHeight}" x2="${dividerX}" y2="${headerY + headerHeight + mockRows.length * rowHeight}"
5168
+ stroke="${this.renderTheme.border}" stroke-width="1"/>`;
5169
+ }
4496
5170
  mockRows.forEach((row, rowIdx) => {
4497
5171
  const rowY = headerY + headerHeight + rowIdx * rowHeight;
4498
- svg += `
5172
+ if (showInnerBorder) {
5173
+ svg += `
4499
5174
  <line x1="${pos.x}" y1="${rowY + rowHeight}" x2="${pos.x + pos.width}" y2="${rowY + rowHeight}"
4500
5175
  stroke="${this.renderTheme.border}" stroke-width="0.5"/>`;
4501
- columns.forEach((col, colIdx) => {
5176
+ }
5177
+ safeColumns.forEach((col, colIdx) => {
4502
5178
  const cellValue = row[col] || "";
4503
5179
  svg += `
4504
- <text x="${pos.x + colIdx * colWidth + 12}" y="${rowY + 22}"
4505
- font-family="system-ui, -apple-system, sans-serif"
5180
+ <text x="${pos.x + colIdx * dataColWidth + 12}" y="${rowY + 22}"
5181
+ font-family="Arial, Helvetica, sans-serif"
4506
5182
  font-size="12"
4507
5183
  fill="${this.renderTheme.text}">${this.escapeXml(cellValue)}</text>`;
4508
5184
  });
5185
+ if (hasActions) {
5186
+ const iconSize = 14;
5187
+ const buttonSize = 22;
5188
+ const buttonGap = 6;
5189
+ const actionsWidth = actions.length * buttonSize + Math.max(0, actions.length - 1) * buttonGap;
5190
+ let currentX = pos.x + pos.width - 12 - actionsWidth;
5191
+ const buttonY = rowY + (rowHeight - buttonSize) / 2;
5192
+ actions.forEach((actionIcon) => {
5193
+ const iconSvg = getIcon(actionIcon);
5194
+ const iconX = currentX + (buttonSize - iconSize) / 2;
5195
+ const iconY = buttonY + (buttonSize - iconSize) / 2;
5196
+ svg += `
5197
+ <rect x="${currentX}" y="${buttonY}" width="${buttonSize}" height="${buttonSize}" rx="4"
5198
+ fill="${this.renderTheme.cardBg}" stroke="${showInnerBorder ? this.renderTheme.border : "none"}" stroke-width="1"/>`;
5199
+ if (iconSvg) {
5200
+ svg += `
5201
+ <g transform="translate(${iconX}, ${iconY})">
5202
+ <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">
5203
+ ${this.extractSvgContent(iconSvg)}
5204
+ </svg>
5205
+ </g>`;
5206
+ }
5207
+ currentX += buttonSize + buttonGap;
5208
+ });
5209
+ }
4509
5210
  });
5211
+ const footerTop = headerY + headerHeight + mockRows.length * rowHeight + 16;
4510
5212
  if (pagination) {
4511
- const paginationY = headerY + headerHeight + mockRows.length * rowHeight + 16;
5213
+ const paginationY = sameFooterAlign ? footerTop + 18 + 8 : footerTop;
4512
5214
  const buttonWidth = 40;
4513
5215
  const buttonHeight = 32;
4514
5216
  const gap = 8;
@@ -4521,18 +5223,25 @@ var SVGRenderer = class {
4521
5223
  } else {
4522
5224
  startX = pos.x + pos.width - totalWidth - 16;
4523
5225
  }
5226
+ const previousIcon = getIcon("chevron-left");
4524
5227
  svg += `
4525
5228
  <rect x="${startX}" y="${paginationY}"
4526
5229
  width="${buttonWidth}" height="${buttonHeight}"
4527
5230
  rx="4"
4528
5231
  fill="${this.renderTheme.cardBg}"
4529
5232
  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>`;
5233
+ stroke-width="1"/>`;
5234
+ if (previousIcon) {
5235
+ const iconSize = 14;
5236
+ const iconX = startX + (buttonWidth - iconSize) / 2;
5237
+ const iconY = paginationY + (buttonHeight - iconSize) / 2;
5238
+ svg += `
5239
+ <g transform="translate(${iconX}, ${iconY})">
5240
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${this.resolveTextColor()}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
5241
+ ${this.extractSvgContent(previousIcon)}
5242
+ </svg>
5243
+ </g>`;
5244
+ }
4536
5245
  for (let i = 1; i <= pageCount; i++) {
4537
5246
  const btnX = startX + (buttonWidth + gap) * i;
4538
5247
  const isActive = i === 1;
@@ -4546,24 +5255,49 @@ var SVGRenderer = class {
4546
5255
  stroke="${this.renderTheme.border}"
4547
5256
  stroke-width="1"/>
4548
5257
  <text x="${btnX + buttonWidth / 2}" y="${paginationY + buttonHeight / 2 + 4}"
4549
- font-family="system-ui, -apple-system, sans-serif"
5258
+ font-family="Arial, Helvetica, sans-serif"
4550
5259
  font-size="14"
4551
5260
  fill="${textColor}"
4552
5261
  text-anchor="middle">${i}</text>`;
4553
5262
  }
4554
5263
  const nextX = startX + (buttonWidth + gap) * (pageCount + 1);
5264
+ const nextIcon = getIcon("chevron-right");
4555
5265
  svg += `
4556
5266
  <rect x="${nextX}" y="${paginationY}"
4557
5267
  width="${buttonWidth}" height="${buttonHeight}"
4558
5268
  rx="4"
4559
5269
  fill="${this.renderTheme.cardBg}"
4560
5270
  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>`;
5271
+ stroke-width="1"/>`;
5272
+ if (nextIcon) {
5273
+ const iconSize = 14;
5274
+ const iconX = nextX + (buttonWidth - iconSize) / 2;
5275
+ const iconY = paginationY + (buttonHeight - iconSize) / 2;
5276
+ svg += `
5277
+ <g transform="translate(${iconX}, ${iconY})">
5278
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${this.resolveTextColor()}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
5279
+ ${this.extractSvgContent(nextIcon)}
5280
+ </svg>
5281
+ </g>`;
5282
+ }
5283
+ }
5284
+ if (hasCaption) {
5285
+ const captionY = sameFooterAlign ? footerTop + 12 : footerTop + (pagination ? 21 : 12);
5286
+ let captionX = pos.x + 16;
5287
+ let textAnchor = "start";
5288
+ if (captionAlign === "center") {
5289
+ captionX = pos.x + pos.width / 2;
5290
+ textAnchor = "middle";
5291
+ } else if (captionAlign === "right") {
5292
+ captionX = pos.x + pos.width - 16;
5293
+ textAnchor = "end";
5294
+ }
5295
+ svg += `
5296
+ <text x="${captionX}" y="${captionY}"
5297
+ font-family="Arial, Helvetica, sans-serif"
5298
+ font-size="12"
5299
+ fill="${this.hexToRgba(this.resolveTextColor(), 0.75)}"
5300
+ text-anchor="${textAnchor}">${this.escapeXml(caption)}</text>`;
4567
5301
  }
4568
5302
  svg += "\n </g>";
4569
5303
  return svg;
@@ -4678,7 +5412,7 @@ var SVGRenderer = class {
4678
5412
  // TEXT/CONTENT COMPONENTS
4679
5413
  // ============================================================================
4680
5414
  renderText(node, pos) {
4681
- const text = String(node.props.content || "Text content");
5415
+ const text = String(node.props.text || "Text content");
4682
5416
  const fontSize = this.tokens.text.fontSize;
4683
5417
  const lineHeightPx = Math.ceil(fontSize * this.tokens.text.lineHeight);
4684
5418
  const lines = this.wrapTextToLines(text, pos.width, fontSize);
@@ -4688,7 +5422,7 @@ var SVGRenderer = class {
4688
5422
  ).join("");
4689
5423
  return `<g${this.getDataNodeId(node)}>
4690
5424
  <text x="${pos.x}" y="${firstLineY}"
4691
- font-family="system-ui, -apple-system, sans-serif"
5425
+ font-family="Arial, Helvetica, sans-serif"
4692
5426
  font-size="${fontSize}"
4693
5427
  fill="${this.renderTheme.text}">${tspans}</text>
4694
5428
  </g>`;
@@ -4697,7 +5431,7 @@ var SVGRenderer = class {
4697
5431
  const text = String(node.props.text || "Label");
4698
5432
  return `<g${this.getDataNodeId(node)}>
4699
5433
  <text x="${pos.x}" y="${pos.y + 12}"
4700
- font-family="system-ui, -apple-system, sans-serif"
5434
+ font-family="Arial, Helvetica, sans-serif"
4701
5435
  font-size="12"
4702
5436
  fill="${this.renderTheme.textMuted}">${this.escapeXml(text)}</text>
4703
5437
  </g>`;
@@ -4731,7 +5465,7 @@ var SVGRenderer = class {
4731
5465
  const placeholderY = controlY + fontSize + 6;
4732
5466
  return `<g${this.getDataNodeId(node)}>
4733
5467
  ${label ? `<text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
4734
- font-family="system-ui, -apple-system, sans-serif"
5468
+ font-family="Arial, Helvetica, sans-serif"
4735
5469
  font-size="12"
4736
5470
  fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>` : ""}
4737
5471
  <rect x="${pos.x}" y="${controlY}"
@@ -4741,7 +5475,7 @@ var SVGRenderer = class {
4741
5475
  stroke="${this.renderTheme.border}"
4742
5476
  stroke-width="1"/>
4743
5477
  <text x="${pos.x + paddingX}" y="${placeholderY}"
4744
- font-family="system-ui, -apple-system, sans-serif"
5478
+ font-family="Arial, Helvetica, sans-serif"
4745
5479
  font-size="${fontSize}"
4746
5480
  fill="${this.renderTheme.textMuted}">${this.escapeXml(placeholder)}</text>
4747
5481
  </g>`;
@@ -4755,7 +5489,7 @@ var SVGRenderer = class {
4755
5489
  const centerY = controlY + controlHeight / 2 + 5;
4756
5490
  return `<g${this.getDataNodeId(node)}>
4757
5491
  ${label ? `<text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
4758
- font-family="system-ui, -apple-system, sans-serif"
5492
+ font-family="Arial, Helvetica, sans-serif"
4759
5493
  font-size="12"
4760
5494
  fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>` : ""}
4761
5495
  <rect x="${pos.x}" y="${controlY}"
@@ -4765,11 +5499,11 @@ var SVGRenderer = class {
4765
5499
  stroke="${this.renderTheme.border}"
4766
5500
  stroke-width="1"/>
4767
5501
  <text x="${pos.x + 12}" y="${centerY}"
4768
- font-family="system-ui, -apple-system, sans-serif"
5502
+ font-family="Arial, Helvetica, sans-serif"
4769
5503
  font-size="14"
4770
5504
  fill="${this.renderTheme.textMuted}">${this.escapeXml(placeholder)}</text>
4771
5505
  <text x="${pos.x + pos.width - 20}" y="${centerY}"
4772
- font-family="system-ui, -apple-system, sans-serif"
5506
+ font-family="Arial, Helvetica, sans-serif"
4773
5507
  font-size="16"
4774
5508
  fill="${this.renderTheme.textMuted}">\u25BC</text>
4775
5509
  </g>`;
@@ -4788,12 +5522,12 @@ var SVGRenderer = class {
4788
5522
  stroke="${this.renderTheme.border}"
4789
5523
  stroke-width="1"/>
4790
5524
  ${checked ? `<text x="${pos.x + checkboxSize / 2}" y="${checkboxY + 14}"
4791
- font-family="system-ui, -apple-system, sans-serif"
5525
+ font-family="Arial, Helvetica, sans-serif"
4792
5526
  font-size="12"
4793
5527
  fill="white"
4794
5528
  text-anchor="middle">\u2713</text>` : ""}
4795
5529
  <text x="${pos.x + checkboxSize + 12}" y="${pos.y + pos.height / 2 + 5}"
4796
- font-family="system-ui, -apple-system, sans-serif"
5530
+ font-family="Arial, Helvetica, sans-serif"
4797
5531
  font-size="14"
4798
5532
  fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>
4799
5533
  </g>`;
@@ -4814,7 +5548,7 @@ var SVGRenderer = class {
4814
5548
  r="${radioSize / 3.5}"
4815
5549
  fill="${controlColor}"/>` : ""}
4816
5550
  <text x="${pos.x + radioSize + 12}" y="${pos.y + pos.height / 2 + 5}"
4817
- font-family="system-ui, -apple-system, sans-serif"
5551
+ font-family="Arial, Helvetica, sans-serif"
4818
5552
  font-size="14"
4819
5553
  fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>
4820
5554
  </g>`;
@@ -4836,7 +5570,7 @@ var SVGRenderer = class {
4836
5570
  r="8"
4837
5571
  fill="white"/>
4838
5572
  <text x="${pos.x + toggleWidth + 12}" y="${pos.y + pos.height / 2 + 5}"
4839
- font-family="system-ui, -apple-system, sans-serif"
5573
+ font-family="Arial, Helvetica, sans-serif"
4840
5574
  font-size="14"
4841
5575
  fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>
4842
5576
  </g>`;
@@ -4866,7 +5600,7 @@ var SVGRenderer = class {
4866
5600
  stroke-width="1"/>
4867
5601
  <!-- Title -->
4868
5602
  <text x="${pos.x + padding}" y="${pos.y + padding + 8}"
4869
- font-family="system-ui, -apple-system, sans-serif"
5603
+ font-family="Arial, Helvetica, sans-serif"
4870
5604
  font-size="14"
4871
5605
  font-weight="600"
4872
5606
  fill="${this.renderTheme.text}">${this.escapeXml(title)}</text>
@@ -4882,7 +5616,7 @@ var SVGRenderer = class {
4882
5616
  fill="${isActive ? this.renderTheme.primary : "transparent"}"
4883
5617
  stroke="none"/>
4884
5618
  <text x="${pos.x + 16}" y="${itemY + 22}"
4885
- font-family="system-ui, -apple-system, sans-serif"
5619
+ font-family="Arial, Helvetica, sans-serif"
4886
5620
  font-size="13"
4887
5621
  fill="${isActive ? "white" : this.renderTheme.textMuted}">${this.escapeXml(item)}</text>`;
4888
5622
  });
@@ -4908,7 +5642,7 @@ var SVGRenderer = class {
4908
5642
  stroke="${isActive ? "none" : this.renderTheme.border}"
4909
5643
  stroke-width="1"/>
4910
5644
  <text x="${tabX + tabWidth / 2}" y="${pos.y + 28}"
4911
- font-family="system-ui, -apple-system, sans-serif"
5645
+ font-family="Arial, Helvetica, sans-serif"
4912
5646
  font-size="13"
4913
5647
  font-weight="${isActive ? "600" : "500"}"
4914
5648
  fill="${isActive ? "white" : this.renderTheme.text}"
@@ -4971,12 +5705,12 @@ var SVGRenderer = class {
4971
5705
  rx="6"
4972
5706
  fill="${bgColor}"/>
4973
5707
  ${hasTitle ? `<text x="${contentX}" y="${titleStartY}"
4974
- font-family="system-ui, -apple-system, sans-serif"
5708
+ font-family="Arial, Helvetica, sans-serif"
4975
5709
  font-size="${fontSize}"
4976
5710
  font-weight="700"
4977
5711
  fill="${bgColor}">${titleTspans}</text>` : ""}
4978
5712
  <text x="${contentX}" y="${textStartY}"
4979
- font-family="system-ui, -apple-system, sans-serif"
5713
+ font-family="Arial, Helvetica, sans-serif"
4980
5714
  font-size="${fontSize}"
4981
5715
  fill="${bgColor}">${textTspans}</text>
4982
5716
  </g>`;
@@ -4997,7 +5731,7 @@ var SVGRenderer = class {
4997
5731
  fill="${bgColor}"
4998
5732
  stroke="none"/>
4999
5733
  <text x="${pos.x + pos.width / 2}" y="${pos.y + pos.height / 2 + 4}"
5000
- font-family="system-ui, -apple-system, sans-serif"
5734
+ font-family="Arial, Helvetica, sans-serif"
5001
5735
  font-size="${fontSize}"
5002
5736
  font-weight="600"
5003
5737
  fill="${textColor}"
@@ -5036,20 +5770,20 @@ var SVGRenderer = class {
5036
5770
  stroke-width="1"/>
5037
5771
 
5038
5772
  <text x="${modalX + padding}" y="${modalY + padding + 16}"
5039
- font-family="system-ui, -apple-system, sans-serif"
5773
+ font-family="Arial, Helvetica, sans-serif"
5040
5774
  font-size="16"
5041
5775
  font-weight="600"
5042
5776
  fill="${this.renderTheme.text}">${this.escapeXml(title)}</text>
5043
5777
 
5044
5778
  <!-- Close button -->
5045
5779
  <text x="${modalX + pos.width - 16}" y="${modalY + padding + 12}"
5046
- font-family="system-ui, -apple-system, sans-serif"
5780
+ font-family="Arial, Helvetica, sans-serif"
5047
5781
  font-size="18"
5048
5782
  fill="${this.renderTheme.textMuted}">\u2715</text>
5049
5783
 
5050
5784
  <!-- Content placeholder -->
5051
5785
  <text x="${modalX + pos.width / 2}" y="${modalY + headerHeight + (pos.height - headerHeight) / 2}"
5052
- font-family="system-ui, -apple-system, sans-serif"
5786
+ font-family="Arial, Helvetica, sans-serif"
5053
5787
  font-size="13"
5054
5788
  fill="${this.renderTheme.textMuted}"
5055
5789
  text-anchor="middle">Modal content</text>
@@ -5082,7 +5816,7 @@ var SVGRenderer = class {
5082
5816
  if (title) {
5083
5817
  svg += `
5084
5818
  <text x="${pos.x + padding}" y="${pos.y + 26}"
5085
- font-family="system-ui, -apple-system, sans-serif"
5819
+ font-family="Arial, Helvetica, sans-serif"
5086
5820
  font-size="13"
5087
5821
  font-weight="600"
5088
5822
  fill="${this.renderTheme.text}">${this.escapeXml(title)}</text>
@@ -5098,7 +5832,7 @@ var SVGRenderer = class {
5098
5832
  stroke="${this.renderTheme.border}"
5099
5833
  stroke-width="0.5"/>
5100
5834
  <text x="${pos.x + padding}" y="${itemY + 24}"
5101
- font-family="system-ui, -apple-system, sans-serif"
5835
+ font-family="Arial, Helvetica, sans-serif"
5102
5836
  font-size="13"
5103
5837
  fill="${this.renderTheme.text}">${this.escapeXml(item)}</text>`;
5104
5838
  }
@@ -5116,7 +5850,7 @@ var SVGRenderer = class {
5116
5850
  stroke-width="1"
5117
5851
  stroke-dasharray="4 4"/>
5118
5852
  <text x="${pos.x + pos.width / 2}" y="${pos.y + pos.height / 2}"
5119
- font-family="system-ui, -apple-system, sans-serif"
5853
+ font-family="Arial, Helvetica, sans-serif"
5120
5854
  font-size="12"
5121
5855
  fill="${this.renderTheme.textMuted}"
5122
5856
  text-anchor="middle">${node.componentType}</text>
@@ -5164,14 +5898,14 @@ var SVGRenderer = class {
5164
5898
 
5165
5899
  <!-- Title -->
5166
5900
  <text x="${innerX}" y="${titleY}"
5167
- font-family="system-ui, -apple-system, sans-serif"
5901
+ font-family="Arial, Helvetica, sans-serif"
5168
5902
  font-size="${titleSize}"
5169
5903
  font-weight="500"
5170
5904
  fill="${this.renderTheme.textMuted}">${this.escapeXml(visibleTitle)}</text>
5171
5905
 
5172
5906
  <!-- Value (Large) -->
5173
5907
  <text x="${innerX}" y="${valueY}"
5174
- font-family="system-ui, -apple-system, sans-serif"
5908
+ font-family="Arial, Helvetica, sans-serif"
5175
5909
  font-size="${valueSize}"
5176
5910
  font-weight="700"
5177
5911
  fill="${accentColor}">${this.escapeXml(value)}</text>`;
@@ -5194,7 +5928,7 @@ var SVGRenderer = class {
5194
5928
  svg += `
5195
5929
  <!-- Caption -->
5196
5930
  <text x="${innerX}" y="${captionY}"
5197
- font-family="system-ui, -apple-system, sans-serif"
5931
+ font-family="Arial, Helvetica, sans-serif"
5198
5932
  font-size="${captionSize}"
5199
5933
  fill="${this.renderTheme.textMuted}">${this.escapeXml(visibleCaption)}</text>`;
5200
5934
  }
@@ -5323,7 +6057,7 @@ var SVGRenderer = class {
5323
6057
  const fontWeight = isLast ? "500" : "400";
5324
6058
  svg += `
5325
6059
  <text x="${currentX}" y="${pos.y + pos.height / 2 + 4}"
5326
- font-family="system-ui, -apple-system, sans-serif"
6060
+ font-family="Arial, Helvetica, sans-serif"
5327
6061
  font-size="${fontSize}"
5328
6062
  font-weight="${fontWeight}"
5329
6063
  fill="${textColor}">${this.escapeXml(item)}</text>`;
@@ -5332,7 +6066,7 @@ var SVGRenderer = class {
5332
6066
  if (!isLast) {
5333
6067
  svg += `
5334
6068
  <text x="${currentX + 4}" y="${pos.y + pos.height / 2 + 4}"
5335
- font-family="system-ui, -apple-system, sans-serif"
6069
+ font-family="Arial, Helvetica, sans-serif"
5336
6070
  font-size="${fontSize}"
5337
6071
  fill="${this.renderTheme.textMuted}">${this.escapeXml(separator)}</text>`;
5338
6072
  currentX += separatorWidth;
@@ -5355,7 +6089,7 @@ var SVGRenderer = class {
5355
6089
  const itemY = pos.y + index * itemHeight;
5356
6090
  const isActive = index === activeIndex;
5357
6091
  const bgColor = isActive ? this.hexToRgba(accentColor, 0.15) : "transparent";
5358
- const textColor = isActive ? this.hexToRgba(accentColor, 0.9) : "rgba(30, 41, 59, 0.75)";
6092
+ const textColor = isActive ? this.hexToRgba(accentColor, 0.9) : this.hexToRgba(this.resolveTextColor(), 0.75);
5359
6093
  const fontWeight = isActive ? "500" : "400";
5360
6094
  if (isActive) {
5361
6095
  svg += `
@@ -5372,7 +6106,7 @@ var SVGRenderer = class {
5372
6106
  const iconY = itemY + (itemHeight - iconSize) / 2;
5373
6107
  svg += `
5374
6108
  <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">
6109
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${this.hexToRgba(this.resolveMutedColor(), 0.9)}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
5376
6110
  ${this.extractSvgContent(iconSvg)}
5377
6111
  </svg>
5378
6112
  </g>`;
@@ -5381,7 +6115,7 @@ var SVGRenderer = class {
5381
6115
  }
5382
6116
  svg += `
5383
6117
  <text x="${currentX}" y="${itemY + itemHeight / 2 + 5}"
5384
- font-family="system-ui, -apple-system, sans-serif"
6118
+ font-family="Arial, Helvetica, sans-serif"
5385
6119
  font-size="${fontSize}"
5386
6120
  font-weight="${fontWeight}"
5387
6121
  fill="${textColor}">${this.escapeXml(item)}</text>`;
@@ -5390,18 +6124,19 @@ var SVGRenderer = class {
5390
6124
  return svg;
5391
6125
  }
5392
6126
  renderIcon(node, pos) {
5393
- const iconType = String(node.props.type || "help-circle");
6127
+ const iconType = String(node.props.icon || "help-circle");
5394
6128
  const size = String(node.props.size || "md");
6129
+ const variant = String(node.props.variant || "default");
5395
6130
  const iconSvg = getIcon(iconType);
6131
+ const iconColor = variant === "default" ? this.hexToRgba(this.resolveTextColor(), 0.75) : this.resolveVariantColor(variant, this.resolveTextColor());
5396
6132
  if (!iconSvg) {
5397
6133
  return `<g${this.getDataNodeId(node)}>
5398
6134
  <!-- 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>
6135
+ <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"/>
6136
+ <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
6137
  </g>`;
5402
6138
  }
5403
6139
  const iconSize = this.getIconSize(size);
5404
- const iconColor = "rgba(30, 41, 59, 0.75)";
5405
6140
  const offsetX = pos.x + (pos.width - iconSize) / 2;
5406
6141
  const offsetY = pos.y + (pos.height - iconSize) / 2;
5407
6142
  const wrappedSvg = `<g${this.getDataNodeId(node)} transform="translate(${offsetX}, ${offsetY})">
@@ -5416,23 +6151,31 @@ var SVGRenderer = class {
5416
6151
  const variant = String(node.props.variant || "default");
5417
6152
  const size = String(node.props.size || "md");
5418
6153
  const disabled = String(node.props.disabled || "false") === "true";
6154
+ const density = this.ir.project.style.density || "normal";
6155
+ const labelOffset = this.parseBooleanProp(node.props.labelSpace, false) ? 18 : 0;
6156
+ const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
5419
6157
  const semanticBase = this.getSemanticVariantColor(variant);
5420
6158
  const hasExplicitVariantColor = semanticBase !== void 0 || this.colorResolver.hasColor(variant);
5421
6159
  const resolvedBase = this.resolveVariantColor(variant, this.renderTheme.primary);
5422
6160
  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)";
6161
+ const iconColor = hasExplicitVariantColor ? "#FFFFFF" : this.hexToRgba(this.resolveTextColor(), 0.75);
5424
6162
  const borderColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.7) : "rgba(100, 116, 139, 0.4)";
5425
6163
  const opacity = disabled ? "0.5" : "1";
5426
6164
  const iconSvg = getIcon(iconName);
5427
- const buttonSize = this.getIconButtonSize(size);
6165
+ const buttonSize = Math.max(
6166
+ 16,
6167
+ Math.min(resolveActionControlHeight(size, density), pos.height - labelOffset)
6168
+ );
6169
+ const buttonWidth = buttonSize + extraPadding * 2;
5428
6170
  const radius = 6;
6171
+ const buttonY = pos.y + labelOffset;
5429
6172
  let svg = `<g${this.getDataNodeId(node)} opacity="${opacity}">
5430
6173
  <!-- IconButton background -->
5431
- <rect x="${pos.x}" y="${pos.y}" width="${buttonSize}" height="${buttonSize}" rx="${radius}" fill="${bgColor}" stroke="${borderColor}" stroke-width="1"/>`;
6174
+ <rect x="${pos.x}" y="${buttonY}" width="${buttonWidth}" height="${buttonSize}" rx="${radius}" fill="${bgColor}" stroke="${borderColor}" stroke-width="1"/>`;
5432
6175
  if (iconSvg) {
5433
6176
  const iconSize = buttonSize * 0.6;
5434
- const offsetX = pos.x + (buttonSize - iconSize) / 2;
5435
- const offsetY = pos.y + (buttonSize - iconSize) / 2;
6177
+ const offsetX = pos.x + (buttonWidth - iconSize) / 2;
6178
+ const offsetY = buttonY + (buttonSize - iconSize) / 2;
5436
6179
  svg += `
5437
6180
  <!-- Icon -->
5438
6181
  <g transform="translate(${offsetX}, ${offsetY})">
@@ -5465,6 +6208,14 @@ var SVGRenderer = class {
5465
6208
  resolveChartColor() {
5466
6209
  return this.colorResolver.resolveColor("chart", this.renderTheme.primary);
5467
6210
  }
6211
+ resolveTextColor() {
6212
+ const fallback = this.options.theme === "dark" ? "#FFFFFF" : "#000000";
6213
+ return this.colorResolver.resolveColor("text", fallback);
6214
+ }
6215
+ resolveMutedColor() {
6216
+ const fallback = this.options.theme === "dark" ? "#94A3B8" : "#64748B";
6217
+ return this.colorResolver.resolveColor("muted", fallback);
6218
+ }
5468
6219
  getSemanticVariantColor(variant) {
5469
6220
  const semantic = {
5470
6221
  primary: this.renderTheme.primary,
@@ -5791,21 +6542,28 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
5791
6542
  renderButton(node, pos) {
5792
6543
  const text = String(node.props.text || "Button");
5793
6544
  const variant = String(node.props.variant || "default");
6545
+ const size = String(node.props.size || "md");
6546
+ const density = this.ir.project.style.density || "normal";
6547
+ const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
6548
+ const labelOffset = this.parseBooleanProp(node.props.labelSpace, false) ? 18 : 0;
5794
6549
  const fullWidth = this.shouldButtonFillAvailableWidth(node);
5795
6550
  const radius = this.tokens.button.radius;
5796
6551
  const fontSize = this.tokens.button.fontSize;
5797
6552
  const paddingX = this.tokens.button.paddingX;
5798
- const paddingY = this.tokens.button.paddingY;
6553
+ const buttonHeight = Math.max(
6554
+ 16,
6555
+ Math.min(resolveActionControlHeight(size, density), pos.height - labelOffset)
6556
+ );
6557
+ const buttonY = pos.y + labelOffset;
5799
6558
  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;
6559
+ const buttonWidth = fullWidth ? Math.max(1, pos.width) : this.clampControlWidth(Math.max(textWidth + (paddingX + extraPadding) * 2, 60), pos.width);
5802
6560
  const semanticBase = this.getSemanticVariantColor(variant);
5803
6561
  const hasExplicitVariantColor = semanticBase !== void 0 || this.colorResolver.hasColor(variant);
5804
6562
  const resolvedBase = this.resolveVariantColor(variant, this.renderTheme.primary);
5805
6563
  const bgColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.85) : "rgba(226, 232, 240, 0.9)";
5806
6564
  const borderColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.7) : "rgba(100, 116, 139, 0.4)";
5807
6565
  return `<g${this.getDataNodeId(node)}>
5808
- <rect x="${pos.x}" y="${pos.y}"
6566
+ <rect x="${pos.x}" y="${buttonY}"
5809
6567
  width="${buttonWidth}" height="${buttonHeight}"
5810
6568
  rx="${radius}"
5811
6569
  fill="${bgColor}"
@@ -5819,13 +6577,14 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
5819
6577
  renderLink(node, pos) {
5820
6578
  const text = String(node.props.text || "Link");
5821
6579
  const variant = String(node.props.variant || "primary");
6580
+ const size = String(node.props.size || "md");
6581
+ const density = this.ir.project.style.density || "normal";
5822
6582
  const fontSize = this.tokens.button.fontSize;
5823
6583
  const paddingX = this.tokens.button.paddingX;
5824
- const paddingY = this.tokens.button.paddingY;
5825
6584
  const linkColor = this.resolveVariantColor(variant, this.renderTheme.primary);
5826
6585
  const textWidth = this.estimateTextWidth(text, fontSize);
5827
6586
  const linkWidth = this.clampControlWidth(Math.max(textWidth + paddingX * 2, 60), pos.width);
5828
- const linkHeight = fontSize + paddingY * 2;
6587
+ const linkHeight = Math.max(16, Math.min(resolveActionControlHeight(size, density), pos.height));
5829
6588
  const blockHeight = Math.max(8, Math.round(fontSize * 0.75));
5830
6589
  const blockWidth = Math.max(28, Math.min(textWidth, linkWidth - paddingX * 2));
5831
6590
  const blockX = pos.x + (linkWidth - blockWidth) / 2;
@@ -5842,17 +6601,70 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
5842
6601
  stroke-width="1"/>
5843
6602
  </g>`;
5844
6603
  }
6604
+ /**
6605
+ * Render breadcrumbs as skeleton blocks: <rect> / <rect> / <rect accent>
6606
+ */
6607
+ renderBreadcrumbs(node, pos) {
6608
+ const itemsStr = String(node.props.items || "Home");
6609
+ const items = itemsStr.split(",").map((s) => s.trim()).filter(Boolean);
6610
+ const separator = String(node.props.separator || "/");
6611
+ const blockColor = this.renderTheme.border;
6612
+ const charWidth = 6.2;
6613
+ const minBlockWidth = 28;
6614
+ const maxBlockWidth = Math.max(minBlockWidth, Math.floor(pos.width * 0.4));
6615
+ const blockHeight = 12;
6616
+ const blockY = pos.y + (pos.height - blockHeight) / 2;
6617
+ const blockRadius = 4;
6618
+ const blockPaddingX = 10;
6619
+ const itemSpacing = 8;
6620
+ const separatorWidth = 12;
6621
+ const contentRight = pos.x + pos.width;
6622
+ let currentX = pos.x;
6623
+ let svg = `<g${this.getDataNodeId(node)}>`;
6624
+ items.forEach((item, index) => {
6625
+ if (currentX >= contentRight) return;
6626
+ const isLast = index === items.length - 1;
6627
+ const estimatedTextWidth = item.length * charWidth;
6628
+ let blockWidth = Math.max(
6629
+ minBlockWidth,
6630
+ Math.min(maxBlockWidth, Math.ceil(estimatedTextWidth + blockPaddingX * 2))
6631
+ );
6632
+ blockWidth = Math.min(blockWidth, Math.max(0, contentRight - currentX));
6633
+ if (blockWidth < minBlockWidth) return;
6634
+ const fillColor = blockColor;
6635
+ svg += `
6636
+ <rect x="${currentX}" y="${blockY}"
6637
+ width="${blockWidth}" height="${blockHeight}"
6638
+ rx="${blockRadius}"
6639
+ fill="${fillColor}"
6640
+ stroke="none"/>`;
6641
+ currentX += blockWidth + itemSpacing;
6642
+ if (!isLast && currentX + separatorWidth <= contentRight) {
6643
+ svg += `
6644
+ <text x="${currentX + 2}" y="${pos.y + pos.height / 2 + 4}"
6645
+ font-family="Arial, Helvetica, sans-serif"
6646
+ font-size="12"
6647
+ fill="${blockColor}">${this.escapeXml(separator)}</text>`;
6648
+ currentX += separatorWidth;
6649
+ }
6650
+ });
6651
+ svg += "\n </g>";
6652
+ return svg;
6653
+ }
5845
6654
  /**
5846
6655
  * Render heading as gray block
5847
6656
  */
5848
6657
  renderHeading(node, pos) {
5849
6658
  const headingTypography = this.getHeadingTypography(node);
6659
+ const variant = String(node.props.variant || "default");
6660
+ const blockColor = variant === "default" ? this.renderTheme.border : this.hexToRgba(this.resolveVariantColor(variant, this.resolveTextColor()), 0.35);
5850
6661
  return this.renderTextBlock(
5851
6662
  node,
5852
6663
  pos,
5853
6664
  String(node.props.text || "Heading"),
5854
6665
  headingTypography.fontSize,
5855
- headingTypography.lineHeight
6666
+ headingTypography.lineHeight,
6667
+ blockColor
5856
6668
  );
5857
6669
  }
5858
6670
  /**
@@ -5862,7 +6674,7 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
5862
6674
  return this.renderTextBlock(
5863
6675
  node,
5864
6676
  pos,
5865
- String(node.props.content || "Text content"),
6677
+ String(node.props.text || "Text content"),
5866
6678
  this.tokens.text.fontSize,
5867
6679
  this.tokens.text.lineHeight
5868
6680
  );
@@ -6103,18 +6915,42 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
6103
6915
  renderTable(node, pos) {
6104
6916
  const title = String(node.props.title || "");
6105
6917
  const columnsStr = String(node.props.columns || "Col1,Col2,Col3");
6106
- const columns = columnsStr.split(",").map((c) => c.trim());
6918
+ const columns = columnsStr.split(",").map((c) => c.trim()).filter(Boolean);
6107
6919
  const rowCount = Number(node.props.rows || node.props.rowsMock || 5);
6920
+ const actions = String(node.props.actions || "").split(",").map((value) => value.trim()).filter(Boolean);
6921
+ const hasActions = actions.length > 0;
6922
+ const pagination = this.parseBooleanProp(node.props.pagination, false);
6923
+ const parsedPageCount = Number(node.props.pages || 5);
6924
+ const pageCount = Number.isFinite(parsedPageCount) && parsedPageCount > 0 ? Math.floor(parsedPageCount) : 5;
6925
+ const paginationAlign = String(node.props.paginationAlign || "right");
6926
+ const hasCaption = String(node.props.caption || "").trim().length > 0;
6927
+ const showOuterBorder = this.parseBooleanProp(node.props.border, false);
6928
+ const showOuterBackground = this.parseBooleanProp(
6929
+ node.props.background ?? node.props.backround,
6930
+ false
6931
+ );
6932
+ const showInnerBorder = this.parseBooleanProp(node.props.innerBorder, true);
6933
+ const rawCaptionAlign = String(node.props.captionAlign || "");
6934
+ const captionAlign = rawCaptionAlign === "left" || rawCaptionAlign === "center" || rawCaptionAlign === "right" ? rawCaptionAlign : paginationAlign === "left" ? "right" : "left";
6935
+ const sameFooterAlign = hasCaption && pagination && captionAlign === paginationAlign;
6936
+ const safeColumns = columns.length > 0 ? columns : ["Column"];
6108
6937
  const headerHeight = 44;
6109
6938
  const rowHeight = 36;
6110
- const colWidth = pos.width / columns.length;
6111
- let svg = `<g${this.getDataNodeId(node)}>
6939
+ const actionColumnWidth = hasActions ? Math.max(96, Math.min(180, actions.length * 26 + 28)) : 0;
6940
+ const dataWidth = Math.max(20, pos.width - actionColumnWidth);
6941
+ const colWidth = dataWidth / safeColumns.length;
6942
+ let svg = `<g${this.getDataNodeId(node)}>`;
6943
+ if (showOuterBorder || showOuterBackground) {
6944
+ const outerFill = showOuterBackground ? this.renderTheme.cardBg : "none";
6945
+ const outerStroke = showOuterBorder ? this.renderTheme.border : "none";
6946
+ svg += `
6112
6947
  <rect x="${pos.x}" y="${pos.y}"
6113
6948
  width="${pos.width}" height="${pos.height}"
6114
6949
  rx="8"
6115
- fill="${this.renderTheme.cardBg}"
6116
- stroke="${this.renderTheme.border}"
6950
+ fill="${outerFill}"
6951
+ stroke="${outerStroke}"
6117
6952
  stroke-width="1"/>`;
6953
+ }
6118
6954
  if (title) {
6119
6955
  svg += `<rect x="${pos.x + 16}" y="${pos.y + 12}"
6120
6956
  width="100" height="12"
@@ -6122,19 +6958,28 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
6122
6958
  fill="${this.renderTheme.border}"/>`;
6123
6959
  }
6124
6960
  const headerY = pos.y + (title ? 32 : 0);
6125
- svg += `<line x1="${pos.x}" y1="${headerY + headerHeight}" x2="${pos.x + pos.width}" y2="${headerY + headerHeight}"
6961
+ if (showInnerBorder) {
6962
+ svg += `<line x1="${pos.x}" y1="${headerY + headerHeight}" x2="${pos.x + pos.width}" y2="${headerY + headerHeight}"
6126
6963
  stroke="${this.renderTheme.border}" stroke-width="1"/>`;
6127
- columns.forEach((_, i) => {
6964
+ }
6965
+ safeColumns.forEach((_, i) => {
6128
6966
  svg += `<rect x="${pos.x + i * colWidth + 12}" y="${headerY + 16}"
6129
6967
  width="50" height="10"
6130
6968
  rx="4"
6131
6969
  fill="${this.renderTheme.border}"/>`;
6132
6970
  });
6971
+ if (hasActions && showInnerBorder) {
6972
+ const dividerX = pos.x + dataWidth;
6973
+ svg += `<line x1="${dividerX}" y1="${headerY + headerHeight}" x2="${dividerX}" y2="${headerY + headerHeight + rowCount * rowHeight}"
6974
+ stroke="${this.renderTheme.border}" stroke-width="1"/>`;
6975
+ }
6133
6976
  for (let rowIdx = 0; rowIdx < rowCount; rowIdx++) {
6134
6977
  const rowY = headerY + headerHeight + rowIdx * rowHeight;
6135
- svg += `<line x1="${pos.x}" y1="${rowY + rowHeight}" x2="${pos.x + pos.width}" y2="${rowY + rowHeight}"
6978
+ if (showInnerBorder) {
6979
+ svg += `<line x1="${pos.x}" y1="${rowY + rowHeight}" x2="${pos.x + pos.width}" y2="${rowY + rowHeight}"
6136
6980
  stroke="${this.renderTheme.border}" stroke-width="0.5"/>`;
6137
- columns.forEach((_, colIdx) => {
6981
+ }
6982
+ safeColumns.forEach((_, colIdx) => {
6138
6983
  const variance = (rowIdx * 17 + colIdx * 11) % 5 * 10;
6139
6984
  const blockWidth = Math.min(colWidth - 24, 60 + variance);
6140
6985
  svg += `<rect x="${pos.x + colIdx * colWidth + 12}" y="${rowY + 12}"
@@ -6142,6 +6987,45 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
6142
6987
  rx="4"
6143
6988
  fill="${this.renderTheme.border}"/>`;
6144
6989
  });
6990
+ if (hasActions) {
6991
+ const iconSize = 14;
6992
+ const iconGap = 8;
6993
+ const actionsWidth = actions.length * iconSize + Math.max(0, actions.length - 1) * iconGap;
6994
+ let currentX = pos.x + pos.width - 12 - actionsWidth;
6995
+ const iconY = rowY + (rowHeight - iconSize) / 2;
6996
+ actions.forEach(() => {
6997
+ svg += `<rect x="${currentX}" y="${iconY}" width="${iconSize}" height="${iconSize}" rx="3" fill="${this.renderTheme.border}"/>`;
6998
+ currentX += iconSize + iconGap;
6999
+ });
7000
+ }
7001
+ }
7002
+ const footerTop = headerY + headerHeight + rowCount * rowHeight + 16;
7003
+ if (hasCaption) {
7004
+ const captionY = sameFooterAlign ? footerTop : footerTop + (pagination ? 10 : 0);
7005
+ const captionWidth = Math.min(220, Math.max(90, pos.width * 0.34));
7006
+ let captionX = pos.x + 16;
7007
+ if (captionAlign === "center") {
7008
+ captionX = pos.x + (pos.width - captionWidth) / 2;
7009
+ } else if (captionAlign === "right") {
7010
+ captionX = pos.x + pos.width - 16 - captionWidth;
7011
+ }
7012
+ svg += `<rect x="${captionX}" y="${captionY}" width="${captionWidth}" height="10" rx="4" fill="${this.renderTheme.border}"/>`;
7013
+ }
7014
+ if (pagination) {
7015
+ const buttonWidth = 28;
7016
+ const buttonHeight = 24;
7017
+ const buttonGap = 8;
7018
+ const totalWidth = (pageCount + 2) * buttonWidth + (pageCount + 1) * buttonGap;
7019
+ const paginationY = sameFooterAlign ? footerTop + 18 : footerTop;
7020
+ let startX = pos.x + pos.width - totalWidth - 16;
7021
+ if (paginationAlign === "left") {
7022
+ startX = pos.x + 16;
7023
+ } else if (paginationAlign === "center") {
7024
+ startX = pos.x + (pos.width - totalWidth) / 2;
7025
+ }
7026
+ for (let i = 0; i < pageCount + 2; i++) {
7027
+ svg += `<rect x="${startX + i * (buttonWidth + buttonGap)}" y="${paginationY}" width="${buttonWidth}" height="${buttonHeight}" rx="4" fill="${this.renderTheme.border}"/>`;
7028
+ }
6145
7029
  }
6146
7030
  svg += "</g>";
6147
7031
  return svg;
@@ -6154,21 +7038,39 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
6154
7038
  const subtitle = String(node.props.subtitle || "");
6155
7039
  const actions = String(node.props.actions || "");
6156
7040
  const user = String(node.props.user || "");
7041
+ const variant = String(node.props.variant || "default");
7042
+ const accentBlock = variant === "default" ? this.renderTheme.border : this.hexToRgba(this.resolveVariantColor(variant, this.resolveAccentColor()), 0.35);
7043
+ const showBorder = this.parseBooleanProp(node.props.border, false);
7044
+ const showBackground = this.parseBooleanProp(node.props.background ?? node.props.backround, false);
7045
+ const radiusMap = {
7046
+ none: 0,
7047
+ sm: 4,
7048
+ md: this.tokens.card.radius,
7049
+ lg: 12,
7050
+ xl: 16
7051
+ };
7052
+ const topbarRadius = radiusMap[String(node.props.radius || "md")] ?? this.tokens.card.radius;
6157
7053
  const topbar = this.calculateTopbarLayout(node, pos, title, subtitle, actions, user);
6158
7054
  const titleWidth = Math.max(56, Math.min(topbar.titleMaxWidth * 0.55, topbar.titleMaxWidth));
6159
7055
  const subtitleWidth = Math.max(48, Math.min(topbar.titleMaxWidth * 0.4, topbar.titleMaxWidth));
6160
- let svg = `<g${this.getDataNodeId(node)}>
7056
+ let svg = `<g${this.getDataNodeId(node)}>`;
7057
+ if (showBorder || showBackground) {
7058
+ const bg = showBackground ? this.renderTheme.cardBg : "none";
7059
+ const stroke = showBorder ? this.renderTheme.border : "none";
7060
+ svg += `
6161
7061
  <rect x="${pos.x}" y="${pos.y}"
6162
7062
  width="${pos.width}" height="${pos.height}"
6163
- fill="${this.renderTheme.cardBg}"
6164
- stroke="${this.renderTheme.border}"
6165
- stroke-width="0 0 1 0"/>`;
7063
+ rx="${topbarRadius}"
7064
+ fill="${bg}"
7065
+ stroke="${stroke}"
7066
+ stroke-width="1"/>`;
7067
+ }
6166
7068
  if (topbar.leftIcon) {
6167
7069
  svg += `
6168
7070
  <rect x="${topbar.leftIcon.badgeX}" y="${topbar.leftIcon.badgeY}"
6169
7071
  width="${topbar.leftIcon.badgeSize}" height="${topbar.leftIcon.badgeSize}"
6170
7072
  rx="${topbar.leftIcon.badgeRadius}"
6171
- fill="${this.renderTheme.border}"/>`;
7073
+ fill="${accentBlock}"/>`;
6172
7074
  }
6173
7075
  svg += `
6174
7076
  <rect x="${topbar.textX}" y="${topbar.titleY - 12}"
@@ -6187,7 +7089,7 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
6187
7089
  <rect x="${action.x}" y="${action.y}"
6188
7090
  width="${action.width}" height="${action.height}"
6189
7091
  rx="6"
6190
- fill="${this.renderTheme.border}"/>`;
7092
+ fill="${accentBlock}"/>`;
6191
7093
  });
6192
7094
  if (topbar.userBadge) {
6193
7095
  svg += `
@@ -6255,12 +7157,14 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
6255
7157
  */
6256
7158
  renderIcon(node, pos) {
6257
7159
  const size = String(node.props.size || "md");
7160
+ const variant = String(node.props.variant || "default");
6258
7161
  const iconSize = this.getIconSize(size);
7162
+ const blockColor = variant === "default" ? this.renderTheme.border : this.hexToRgba(this.resolveVariantColor(variant, this.resolveTextColor()), 0.35);
6259
7163
  return `<g${this.getDataNodeId(node)}>
6260
7164
  <rect x="${pos.x}" y="${pos.y + (pos.height - iconSize) / 2}"
6261
7165
  width="${iconSize}" height="${iconSize}"
6262
7166
  rx="2"
6263
- fill="${this.renderTheme.border}"/>
7167
+ fill="${blockColor}"/>
6264
7168
  </g>`;
6265
7169
  }
6266
7170
  /**
@@ -6269,15 +7173,23 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
6269
7173
  renderIconButton(node, pos) {
6270
7174
  const variant = String(node.props.variant || "default");
6271
7175
  const size = String(node.props.size || "md");
7176
+ const density = this.ir.project.style.density || "normal";
7177
+ const labelOffset = this.parseBooleanProp(node.props.labelSpace, false) ? 18 : 0;
7178
+ const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
6272
7179
  const semanticBase = this.getSemanticVariantColor(variant);
6273
7180
  const hasExplicitVariantColor = semanticBase !== void 0 || this.colorResolver.hasColor(variant);
6274
7181
  const resolvedBase = this.resolveVariantColor(variant, this.renderTheme.primary);
6275
7182
  const bgColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.85) : "rgba(226, 232, 240, 0.9)";
6276
7183
  const borderColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.7) : "rgba(100, 116, 139, 0.4)";
6277
- const buttonSize = this.getIconButtonSize(size);
7184
+ const buttonSize = Math.max(
7185
+ 16,
7186
+ Math.min(resolveActionControlHeight(size, density), pos.height - labelOffset)
7187
+ );
7188
+ const buttonWidth = buttonSize + extraPadding * 2;
7189
+ const buttonY = pos.y + labelOffset;
6278
7190
  return `<g${this.getDataNodeId(node)}>
6279
- <rect x="${pos.x}" y="${pos.y}"
6280
- width="${buttonSize}" height="${buttonSize}"
7191
+ <rect x="${pos.x}" y="${buttonY}"
7192
+ width="${buttonWidth}" height="${buttonSize}"
6281
7193
  rx="6"
6282
7194
  fill="${bgColor}"
6283
7195
  stroke="${borderColor}"
@@ -6357,10 +7269,10 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
6357
7269
  /**
6358
7270
  * Private helper: Render text as gray block
6359
7271
  */
6360
- renderTextBlock(node, pos, text, fontSize, lineHeightMultiplier) {
7272
+ renderTextBlock(node, pos, text, fontSize, lineHeightMultiplier, color) {
6361
7273
  const lineHeight = Math.ceil(fontSize * lineHeightMultiplier);
6362
7274
  const blockHeight = Math.max(8, Math.round(fontSize * 0.75));
6363
- const blockColor = this.renderTheme.border;
7275
+ const blockColor = color || this.renderTheme.border;
6364
7276
  const lines = this.wrapTextToLines(text, pos.width, fontSize);
6365
7277
  const contentHeight = lines.length * lineHeight;
6366
7278
  const startY = pos.y + Math.max(0, (pos.height - contentHeight) / 2);
@@ -6442,32 +7354,39 @@ var SketchSVGRenderer = class extends SVGRenderer {
6442
7354
  renderButton(node, pos) {
6443
7355
  const text = String(node.props.text || "Button");
6444
7356
  const variant = String(node.props.variant || "default");
7357
+ const size = String(node.props.size || "md");
7358
+ const density = this.ir.project.style.density || "normal";
7359
+ const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
7360
+ const labelOffset = this.parseBooleanProp(node.props.labelSpace, false) ? 18 : 0;
6445
7361
  const fullWidth = this.shouldButtonFillAvailableWidth(node);
6446
7362
  const radius = this.tokens.button.radius;
6447
7363
  const fontSize = this.tokens.button.fontSize;
6448
7364
  const fontWeight = this.tokens.button.fontWeight;
6449
7365
  const paddingX = this.tokens.button.paddingX;
6450
- const paddingY = this.tokens.button.paddingY;
7366
+ const buttonHeight = Math.max(
7367
+ 16,
7368
+ Math.min(resolveControlHeight(size, density), pos.height - labelOffset)
7369
+ );
7370
+ const buttonY = pos.y + labelOffset;
6451
7371
  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);
7372
+ const buttonWidth = fullWidth ? Math.max(1, pos.width) : this.clampControlWidth(Math.max(idealTextWidth + (paddingX + extraPadding) * 2, 60), pos.width);
7373
+ const availableTextWidth = Math.max(0, buttonWidth - (paddingX + extraPadding) * 2);
6455
7374
  const visibleText = this.truncateTextToWidth(text, availableTextWidth, fontSize);
6456
7375
  const semanticBase = this.getSemanticVariantColor(variant);
6457
7376
  const hasExplicitVariantColor = semanticBase !== void 0 || this.colorResolver.hasColor(variant);
6458
- const variantColor = hasExplicitVariantColor ? this.resolveVariantColor(variant, this.renderTheme.primary) : "#2D3748";
7377
+ const variantColor = hasExplicitVariantColor ? this.resolveVariantColor(variant, this.renderTheme.primary) : this.resolveTextColor();
6459
7378
  const borderColor = variantColor;
6460
7379
  const textColor = variantColor;
6461
7380
  const strokeWidth = 0.5;
6462
7381
  return `<g${this.getDataNodeId(node)}>
6463
- <rect x="${pos.x}" y="${pos.y}"
7382
+ <rect x="${pos.x}" y="${buttonY}"
6464
7383
  width="${buttonWidth}" height="${buttonHeight}"
6465
7384
  rx="${radius}"
6466
7385
  fill="none"
6467
7386
  stroke="${borderColor}"
6468
7387
  stroke-width="${strokeWidth}"
6469
7388
  filter="url(#sketch-rough)"/>
6470
- <text x="${pos.x + buttonWidth / 2}" y="${pos.y + buttonHeight / 2 + fontSize * 0.35}"
7389
+ <text x="${pos.x + buttonWidth / 2}" y="${buttonY + buttonHeight / 2 + fontSize * 0.35}"
6471
7390
  font-family="${this.fontFamily}"
6472
7391
  font-size="${fontSize}"
6473
7392
  font-weight="${fontWeight}"
@@ -6483,7 +7402,7 @@ var SketchSVGRenderer = class extends SVGRenderer {
6483
7402
  const variant = String(node.props.variant || "default");
6484
7403
  const semanticBase = this.getSemanticVariantColor(variant);
6485
7404
  const hasExplicitVariantColor = semanticBase !== void 0 || this.colorResolver.hasColor(variant);
6486
- const variantColor = hasExplicitVariantColor ? this.resolveVariantColor(variant, this.renderTheme.primary) : "#2D3748";
7405
+ const variantColor = hasExplicitVariantColor ? this.resolveVariantColor(variant, this.renderTheme.primary) : this.resolveTextColor();
6487
7406
  const borderColor = variantColor;
6488
7407
  const textColor = variantColor;
6489
7408
  const badgeRadius = this.tokens.badge.radius === "pill" ? pos.height / 2 : this.tokens.badge.radius;
@@ -6510,17 +7429,22 @@ var SketchSVGRenderer = class extends SVGRenderer {
6510
7429
  const iconName = String(node.props.icon || "help-circle");
6511
7430
  const variant = String(node.props.variant || "default");
6512
7431
  const size = String(node.props.size || "md");
7432
+ const density = this.ir.project.style.density || "normal";
7433
+ const labelOffset = this.parseBooleanProp(node.props.labelSpace, false) ? 18 : 0;
7434
+ const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
6513
7435
  const semanticBase = this.getSemanticVariantColor(variant);
6514
7436
  const hasExplicitVariantColor = semanticBase !== void 0 || this.colorResolver.hasColor(variant);
6515
- const variantColor = hasExplicitVariantColor ? this.resolveVariantColor(variant, this.renderTheme.primary) : "#2D3748";
7437
+ const variantColor = hasExplicitVariantColor ? this.resolveVariantColor(variant, this.renderTheme.primary) : this.resolveTextColor();
6516
7438
  const borderColor = variantColor;
6517
7439
  const iconColor = variantColor;
6518
- const buttonSize = this.getIconButtonSize(size);
7440
+ const buttonSize = Math.max(16, Math.min(resolveControlHeight(size, density), pos.height - labelOffset));
7441
+ const buttonWidth = buttonSize + extraPadding * 2;
6519
7442
  const radius = 6;
7443
+ const buttonY = pos.y + labelOffset;
6520
7444
  const iconSvg = this.getIconSvg(iconName);
6521
7445
  let svg = `<g${this.getDataNodeId(node)}>
6522
- <rect x="${pos.x}" y="${pos.y}"
6523
- width="${buttonSize}" height="${buttonSize}"
7446
+ <rect x="${pos.x}" y="${buttonY}"
7447
+ width="${buttonWidth}" height="${buttonSize}"
6524
7448
  rx="${radius}"
6525
7449
  fill="none"
6526
7450
  stroke="${borderColor}"
@@ -6528,8 +7452,8 @@ var SketchSVGRenderer = class extends SVGRenderer {
6528
7452
  filter="url(#sketch-rough)"/>`;
6529
7453
  if (iconSvg) {
6530
7454
  const iconSize = buttonSize * 0.6;
6531
- const offsetX = pos.x + (buttonSize - iconSize) / 2;
6532
- const offsetY = pos.y + (buttonSize - iconSize) / 2;
7455
+ const offsetX = pos.x + (buttonWidth - iconSize) / 2;
7456
+ const offsetY = buttonY + (buttonSize - iconSize) / 2;
6533
7457
  svg += `
6534
7458
  <g transform="translate(${offsetX}, ${offsetY})">
6535
7459
  <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round">
@@ -6680,6 +7604,8 @@ var SketchSVGRenderer = class extends SVGRenderer {
6680
7604
  */
6681
7605
  renderHeading(node, pos) {
6682
7606
  const text = String(node.props.text || "Heading");
7607
+ const variant = String(node.props.variant || "default");
7608
+ const headingColor = variant === "default" ? this.resolveTextColor() : this.resolveVariantColor(variant, this.resolveTextColor());
6683
7609
  const headingTypography = this.getHeadingTypography(node);
6684
7610
  const fontSize = headingTypography.fontSize;
6685
7611
  const fontWeight = headingTypography.fontWeight;
@@ -6692,7 +7618,7 @@ var SketchSVGRenderer = class extends SVGRenderer {
6692
7618
  font-family="${this.fontFamily}"
6693
7619
  font-size="${fontSize}"
6694
7620
  font-weight="${fontWeight}"
6695
- fill="${this.renderTheme.text}">${this.escapeXml(text)}</text>
7621
+ fill="${headingColor}">${this.escapeXml(text)}</text>
6696
7622
  </g>`;
6697
7623
  }
6698
7624
  const tspans = lines.map(
@@ -6703,7 +7629,7 @@ var SketchSVGRenderer = class extends SVGRenderer {
6703
7629
  font-family="${this.fontFamily}"
6704
7630
  font-size="${fontSize}"
6705
7631
  font-weight="${fontWeight}"
6706
- fill="${this.renderTheme.text}">${tspans}</text>
7632
+ fill="${headingColor}">${tspans}</text>
6707
7633
  </g>`;
6708
7634
  }
6709
7635
  /**
@@ -6714,7 +7640,8 @@ var SketchSVGRenderer = class extends SVGRenderer {
6714
7640
  const subtitle = String(node.props.subtitle || "");
6715
7641
  const actions = String(node.props.actions || "");
6716
7642
  const user = String(node.props.user || "");
6717
- const accentColor = this.resolveAccentColor();
7643
+ const variant = String(node.props.variant || "default");
7644
+ const accentColor = variant === "default" ? this.resolveAccentColor() : this.resolveVariantColor(variant, this.resolveAccentColor());
6718
7645
  const topbar = this.calculateTopbarLayout(node, pos, title, subtitle, actions, user);
6719
7646
  let svg = `<g${this.getDataNodeId(node)}>
6720
7647
  <rect x="${pos.x}" y="${pos.y}"
@@ -6807,79 +7734,14 @@ var SketchSVGRenderer = class extends SVGRenderer {
6807
7734
  * Render table with sketch filter and Comic Sans
6808
7735
  */
6809
7736
  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;
7737
+ const standard = super.renderTable(node, pos);
7738
+ return standard.replace("<g", '<g filter="url(#sketch-rough)"');
6877
7739
  }
6878
7740
  /**
6879
7741
  * Render text with Comic Sans
6880
7742
  */
6881
7743
  renderText(node, pos) {
6882
- const text = String(node.props.content || "Text content");
7744
+ const text = String(node.props.text || "Text content");
6883
7745
  const fontSize = this.tokens.text.fontSize;
6884
7746
  const lineHeightPx = Math.ceil(fontSize * this.tokens.text.lineHeight);
6885
7747
  const lines = this.wrapTextToLines(text, pos.width, fontSize);
@@ -7456,7 +8318,7 @@ var SketchSVGRenderer = class extends SVGRenderer {
7456
8318
  const itemY = pos.y + index * itemHeight;
7457
8319
  const isActive = index === activeIndex;
7458
8320
  const bgColor = isActive ? this.hexToRgba(accentColor, 0.15) : "transparent";
7459
- const textColor = isActive ? accentColor : "#2D3748";
8321
+ const textColor = isActive ? accentColor : this.resolveTextColor();
7460
8322
  const fontWeight = isActive ? "500" : "400";
7461
8323
  if (isActive) {
7462
8324
  svg += `
@@ -7480,22 +8342,23 @@ var SketchSVGRenderer = class extends SVGRenderer {
7480
8342
  * Render icon (same as base, icons don't need filter)
7481
8343
  */
7482
8344
  renderIcon(node, pos) {
7483
- const iconType = String(node.props.type || "help-circle");
8345
+ const iconType = String(node.props.icon || "help-circle");
7484
8346
  const size = String(node.props.size || "md");
8347
+ const variant = String(node.props.variant || "default");
7485
8348
  const iconSvg = getIcon(iconType);
7486
8349
  if (!iconSvg) {
7487
8350
  return `<g${this.getDataNodeId(node)}>
7488
8351
  <circle cx="${pos.x + pos.width / 2}" cy="${pos.y + pos.height / 2}"
7489
8352
  r="${Math.min(pos.width, pos.height) / 2 - 2}"
7490
- fill="none" stroke="#2D3748" stroke-width="0.5"
8353
+ fill="none" stroke="${this.resolveMutedColor()}" stroke-width="0.5"
7491
8354
  filter="url(#sketch-rough)"/>
7492
8355
  <text x="${pos.x + pos.width / 2}" y="${pos.y + pos.height / 2 + 4}"
7493
8356
  font-family="${this.fontFamily}"
7494
- font-size="12" fill="#2D3748" text-anchor="middle">?</text>
8357
+ font-size="12" fill="${this.resolveMutedColor()}" text-anchor="middle">?</text>
7495
8358
  </g>`;
7496
8359
  }
7497
8360
  const iconSize = this.getIconSize(size);
7498
- const iconColor = "#2D3748";
8361
+ const iconColor = variant === "default" ? this.resolveTextColor() : this.resolveVariantColor(variant, this.resolveTextColor());
7499
8362
  const offsetX = pos.x + (pos.width - iconSize) / 2;
7500
8363
  const offsetY = pos.y + (pos.height - iconSize) / 2;
7501
8364
  return `<g${this.getDataNodeId(node)} transform="translate(${offsetX}, ${offsetY})">