@wire-dsl/engine 0.2.3 → 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.js CHANGED
@@ -45,20 +45,21 @@ var SourceMapBuilder = class {
45
45
  return nodeId;
46
46
  }
47
47
  /**
48
- * Generate semantic node ID based on type and subtype
49
- * Format: {type}-{subtype}-{counter} or {type}-{counter}
50
- *
51
- * Examples:
52
- * - project → "project"
53
- * - theme → "theme"
54
- * - mocks → "mocks"
55
- * - colors → "colors"
56
- * - screen → "screen-0", "screen-1"
57
- * - component Button → "component-button-0", "component-button-1"
58
- * - layout stack → "layout-stack-0", "layout-stack-1"
59
- * - cell → "cell-0", "cell-1"
60
- * - component-definition → "define-MyButton"
61
- */
48
+ * Generate semantic node ID based on type and subtype
49
+ * Format: {type}-{subtype}-{counter} or {type}-{counter}
50
+ *
51
+ * Examples:
52
+ * - project → "project"
53
+ * - theme → "theme"
54
+ * - mocks → "mocks"
55
+ * - colors → "colors"
56
+ * - screen → "screen-0", "screen-1"
57
+ * - component Button → "component-button-0", "component-button-1"
58
+ * - layout stack → "layout-stack-0", "layout-stack-1"
59
+ * - cell → "cell-0", "cell-1"
60
+ * - component-definition → "define-MyButton"
61
+ * - layout-definition → "define-layout-MyShell"
62
+ */
62
63
  generateNodeId(type, metadata) {
63
64
  switch (type) {
64
65
  case "project":
@@ -94,6 +95,8 @@ var SourceMapBuilder = class {
94
95
  }
95
96
  case "component-definition":
96
97
  return `define-${metadata?.name || "unknown"}`;
98
+ case "layout-definition":
99
+ return `define-layout-${metadata?.name || "unknown"}`;
97
100
  default:
98
101
  return `${type}-0`;
99
102
  }
@@ -179,7 +182,7 @@ var SourceMapBuilder = class {
179
182
  }
180
183
  /**
181
184
  * Calculate insertionPoints for all container nodes
182
- * Container nodes: project, screen, layout, cell, component-definition
185
+ * Container nodes: project, screen, layout, cell, component-definition, layout-definition
183
186
  */
184
187
  calculateAllInsertionPoints() {
185
188
  const containerTypes = [
@@ -187,7 +190,8 @@ var SourceMapBuilder = class {
187
190
  "screen",
188
191
  "layout",
189
192
  "cell",
190
- "component-definition"
193
+ "component-definition",
194
+ "layout-definition"
191
195
  ];
192
196
  for (const entry of this.entries) {
193
197
  if (containerTypes.includes(entry.type)) {
@@ -413,19 +417,23 @@ var SourceMapBuilder = class {
413
417
  };
414
418
 
415
419
  // src/parser/index.ts
416
- var Project = createToken({ name: "Project", pattern: /project/ });
417
- var Screen = createToken({ name: "Screen", pattern: /screen/ });
418
- var Layout = createToken({ name: "Layout", pattern: /layout/ });
419
- var Component = createToken({ name: "Component", pattern: /component/ });
420
+ var Project = createToken({ name: "Project", pattern: /project\b/ });
421
+ var Screen = createToken({ name: "Screen", pattern: /screen\b/ });
422
+ var Layout = createToken({ name: "Layout", pattern: /layout\b/ });
423
+ var Component = createToken({ name: "Component", pattern: /component\b/ });
420
424
  var ComponentKeyword = createToken({
421
425
  name: "ComponentKeyword",
422
426
  pattern: /Component\b/
423
427
  });
424
- var Define = createToken({ name: "Define", pattern: /define/ });
425
- var Style = createToken({ name: "Style", pattern: /style/ });
426
- var Mocks = createToken({ name: "Mocks", pattern: /mocks/ });
428
+ var LayoutKeyword = createToken({
429
+ name: "LayoutKeyword",
430
+ pattern: /Layout\b/
431
+ });
432
+ var Define = createToken({ name: "Define", pattern: /define\b/ });
433
+ var Style = createToken({ name: "Style", pattern: /style\b/ });
434
+ var Mocks = createToken({ name: "Mocks", pattern: /mocks\b/ });
427
435
  var Colors = createToken({ name: "Colors", pattern: /colors(?=\s*\{)/ });
428
- var Cell = createToken({ name: "Cell", pattern: /cell/ });
436
+ var Cell = createToken({ name: "Cell", pattern: /cell\b/ });
429
437
  var LCurly = createToken({ name: "LCurly", pattern: /{/ });
430
438
  var RCurly = createToken({ name: "RCurly", pattern: /}/ });
431
439
  var LParen = createToken({ name: "LParen", pattern: /\(/ });
@@ -472,6 +480,7 @@ var allTokens = [
472
480
  Project,
473
481
  Screen,
474
482
  Layout,
483
+ LayoutKeyword,
475
484
  ComponentKeyword,
476
485
  Component,
477
486
  Define,
@@ -504,6 +513,7 @@ var WireDSLParser = class extends CstParser {
504
513
  this.MANY(() => {
505
514
  this.OR([
506
515
  { ALT: () => this.SUBRULE(this.definedComponent) },
516
+ { ALT: () => this.SUBRULE(this.definedLayout) },
507
517
  { ALT: () => this.SUBRULE(this.styleDecl) },
508
518
  { ALT: () => this.SUBRULE(this.mocksDecl) },
509
519
  { ALT: () => this.SUBRULE(this.colorsDecl) },
@@ -572,6 +582,15 @@ var WireDSLParser = class extends CstParser {
572
582
  ]);
573
583
  this.CONSUME(RCurly);
574
584
  });
585
+ // define Layout "ScreenShell" { layout split { ... } }
586
+ this.definedLayout = this.RULE("definedLayout", () => {
587
+ this.CONSUME(Define);
588
+ this.CONSUME(LayoutKeyword, { LABEL: "layoutKeyword" });
589
+ this.CONSUME(StringLiteral, { LABEL: "layoutName" });
590
+ this.CONSUME(LCurly);
591
+ this.SUBRULE(this.layout);
592
+ this.CONSUME(RCurly);
593
+ });
575
594
  // screen Main(background: white) { ... }
576
595
  this.screen = this.RULE("screen", () => {
577
596
  this.CONSUME(Screen);
@@ -660,6 +679,7 @@ var WireDSLVisitor = class extends BaseCstVisitor {
660
679
  const mocks = {};
661
680
  const colors = {};
662
681
  const definedComponents = [];
682
+ const definedLayouts = [];
663
683
  const screens = [];
664
684
  if (ctx.styleDecl && ctx.styleDecl.length > 0) {
665
685
  const styleBlock = this.visit(ctx.styleDecl[0]);
@@ -678,6 +698,11 @@ var WireDSLVisitor = class extends BaseCstVisitor {
678
698
  definedComponents.push(this.visit(comp));
679
699
  });
680
700
  }
701
+ if (ctx.definedLayout) {
702
+ ctx.definedLayout.forEach((layoutDef) => {
703
+ definedLayouts.push(this.visit(layoutDef));
704
+ });
705
+ }
681
706
  if (ctx.screen) {
682
707
  ctx.screen.forEach((screen) => {
683
708
  screens.push(this.visit(screen));
@@ -690,6 +715,7 @@ var WireDSLVisitor = class extends BaseCstVisitor {
690
715
  mocks,
691
716
  colors,
692
717
  definedComponents,
718
+ definedLayouts,
693
719
  screens
694
720
  };
695
721
  }
@@ -754,6 +780,15 @@ var WireDSLVisitor = class extends BaseCstVisitor {
754
780
  body
755
781
  };
756
782
  }
783
+ definedLayout(ctx) {
784
+ const name = ctx.layoutName[0].image.slice(1, -1);
785
+ const body = this.visit(ctx.layout[0]);
786
+ return {
787
+ type: "definedLayout",
788
+ name,
789
+ body
790
+ };
791
+ }
757
792
  screen(ctx) {
758
793
  const params = ctx.paramList ? this.visit(ctx.paramList[0]) : {};
759
794
  return {
@@ -884,6 +919,7 @@ var WireDSLVisitorWithSourceMap = class extends WireDSLVisitor {
884
919
  constructor(sourceMapBuilder) {
885
920
  super();
886
921
  this.definedComponentNames = /* @__PURE__ */ new Set();
922
+ this.definedLayoutNames = /* @__PURE__ */ new Set();
887
923
  this.sourceMapBuilder = sourceMapBuilder;
888
924
  }
889
925
  project(ctx) {
@@ -892,6 +928,7 @@ var WireDSLVisitorWithSourceMap = class extends WireDSLVisitor {
892
928
  const mocks = {};
893
929
  const colors = {};
894
930
  const definedComponents = [];
931
+ const definedLayouts = [];
895
932
  const screens = [];
896
933
  const tokens = {
897
934
  keyword: ctx.Project[0],
@@ -906,6 +943,8 @@ var WireDSLVisitorWithSourceMap = class extends WireDSLVisitor {
906
943
  colors: {},
907
944
  definedComponents: [],
908
945
  // Will be filled after push
946
+ definedLayouts: [],
947
+ // Will be filled after push
909
948
  screens: []
910
949
  // Will be filled after push
911
950
  };
@@ -935,6 +974,11 @@ var WireDSLVisitorWithSourceMap = class extends WireDSLVisitor {
935
974
  ast.definedComponents.push(this.visit(comp));
936
975
  });
937
976
  }
977
+ if (ctx.definedLayout) {
978
+ ctx.definedLayout.forEach((layoutDef) => {
979
+ ast.definedLayouts.push(this.visit(layoutDef));
980
+ });
981
+ }
938
982
  if (ctx.screen) {
939
983
  ctx.screen.forEach((screen) => {
940
984
  ast.screens.push(this.visit(screen));
@@ -1179,6 +1223,35 @@ var WireDSLVisitorWithSourceMap = class extends WireDSLVisitor {
1179
1223
  }
1180
1224
  return ast;
1181
1225
  }
1226
+ definedLayout(ctx) {
1227
+ const name = ctx.layoutName[0].image.slice(1, -1);
1228
+ this.definedLayoutNames.add(name);
1229
+ const tokens = {
1230
+ keyword: ctx.Define[0],
1231
+ name: ctx.layoutName[0],
1232
+ body: ctx.RCurly[0]
1233
+ };
1234
+ const ast = {
1235
+ type: "definedLayout",
1236
+ name,
1237
+ body: {}
1238
+ // Will be filled after push
1239
+ };
1240
+ if (this.sourceMapBuilder) {
1241
+ const nodeId = this.sourceMapBuilder.addNode(
1242
+ "layout-definition",
1243
+ tokens,
1244
+ { name }
1245
+ );
1246
+ ast._meta = { nodeId };
1247
+ this.sourceMapBuilder.pushParent(nodeId);
1248
+ }
1249
+ ast.body = this.visit(ctx.layout[0]);
1250
+ if (this.sourceMapBuilder) {
1251
+ this.sourceMapBuilder.popParent();
1252
+ }
1253
+ return ast;
1254
+ }
1182
1255
  // Override styleDecl to capture style block in SourceMap
1183
1256
  styleDecl(ctx) {
1184
1257
  const style = {};
@@ -1358,6 +1431,13 @@ function buildLayoutRulesFromMetadata() {
1358
1431
  var BUILT_IN_COMPONENTS = new Set(Object.keys(COMPONENTS));
1359
1432
  var COMPONENT_RULES = buildComponentRulesFromMetadata();
1360
1433
  var LAYOUT_RULES = buildLayoutRulesFromMetadata();
1434
+ var BUILT_IN_LAYOUTS = new Set(Object.keys(LAYOUTS));
1435
+ function isPascalCaseIdentifier(name) {
1436
+ return /^[A-Z][A-Za-z0-9]*$/.test(name);
1437
+ }
1438
+ function isValidDefinedLayoutName(name) {
1439
+ return /^[a-z][a-z0-9_]*$/.test(name);
1440
+ }
1361
1441
  function toFallbackRange() {
1362
1442
  return {
1363
1443
  start: { line: 1, column: 0 },
@@ -1449,6 +1529,17 @@ function isBooleanLike(value) {
1449
1529
  const normalized = String(value).trim().toLowerCase();
1450
1530
  return normalized === "true" || normalized === "false";
1451
1531
  }
1532
+ function parseBooleanLike(value, fallback = false) {
1533
+ if (typeof value === "number") {
1534
+ if (value === 1) return true;
1535
+ if (value === 0) return false;
1536
+ return fallback;
1537
+ }
1538
+ const normalized = String(value).trim().toLowerCase();
1539
+ if (normalized === "true") return true;
1540
+ if (normalized === "false") return false;
1541
+ return fallback;
1542
+ }
1452
1543
  function getPropertyRange(entry, propertyName, mode = "full") {
1453
1544
  const prop = entry?.properties?.[propertyName];
1454
1545
  if (!prop) return entry?.range || toFallbackRange();
@@ -1466,21 +1557,55 @@ function validateSemanticDiagnostics(ast, sourceMap) {
1466
1557
  const diagnostics = [];
1467
1558
  const sourceMapByNodeId = new Map(sourceMap.map((entry) => [entry.nodeId, entry]));
1468
1559
  const definedComponents = new Set(ast.definedComponents.map((dc) => dc.name));
1469
- const emitWarning = (message, code, range, nodeId, suggestion) => {
1560
+ const definedLayouts = new Set(ast.definedLayouts.map((dl) => dl.name));
1561
+ const emitDiagnostic = (severity, message, code, range, nodeId, suggestion) => {
1470
1562
  diagnostics.push({
1471
1563
  message,
1472
1564
  code,
1473
- severity: "warning",
1565
+ severity,
1474
1566
  phase: "semantic",
1475
1567
  range,
1476
1568
  nodeId,
1477
1569
  suggestion
1478
1570
  });
1479
1571
  };
1480
- const checkComponent = (component) => {
1572
+ const emitWarning = (message, code, range, nodeId, suggestion) => emitDiagnostic("warning", message, code, range, nodeId, suggestion);
1573
+ const emitError = (message, code, range, nodeId, suggestion) => emitDiagnostic("error", message, code, range, nodeId, suggestion);
1574
+ const countChildrenSlots = (layout) => {
1575
+ let count = 0;
1576
+ for (const child of layout.children) {
1577
+ if (child.type === "component") {
1578
+ if (child.componentType === "Children") count += 1;
1579
+ } else if (child.type === "layout") {
1580
+ count += countChildrenSlots(child);
1581
+ } else if (child.type === "cell") {
1582
+ for (const cellChild of child.children) {
1583
+ if (cellChild.type === "component") {
1584
+ if (cellChild.componentType === "Children") count += 1;
1585
+ } else if (cellChild.type === "layout") {
1586
+ count += countChildrenSlots(cellChild);
1587
+ }
1588
+ }
1589
+ }
1590
+ }
1591
+ return count;
1592
+ };
1593
+ const checkComponent = (component, insideDefinedLayout) => {
1481
1594
  const nodeId = component._meta?.nodeId;
1482
1595
  const entry = nodeId ? sourceMapByNodeId.get(nodeId) : void 0;
1483
1596
  const componentType = component.componentType;
1597
+ if (componentType === "Children") {
1598
+ if (!insideDefinedLayout) {
1599
+ emitError(
1600
+ 'Component "Children" can only be used inside a define Layout body.',
1601
+ "CHILDREN_SLOT_OUTSIDE_LAYOUT_DEFINITION",
1602
+ entry?.nameRange || entry?.range || toFallbackRange(),
1603
+ nodeId,
1604
+ "Move this placeholder into a define Layout block."
1605
+ );
1606
+ }
1607
+ return;
1608
+ }
1484
1609
  if (!BUILT_IN_COMPONENTS.has(componentType) && !definedComponents.has(componentType)) {
1485
1610
  emitWarning(
1486
1611
  `Component "${componentType}" is not a built-in component and has no local definition.`,
@@ -1521,7 +1646,8 @@ function validateSemanticDiagnostics(ast, sourceMap) {
1521
1646
  const enumValues = rules.enumProps?.[propName];
1522
1647
  if (enumValues) {
1523
1648
  const normalizedValue = String(propValue);
1524
- if (!enumValues.includes(normalizedValue)) {
1649
+ const isCustomVariantFromColors = propName === "variant" && !enumValues.includes(normalizedValue) && Object.prototype.hasOwnProperty.call(ast.colors || {}, normalizedValue);
1650
+ if (!enumValues.includes(normalizedValue) && !isCustomVariantFromColors) {
1525
1651
  emitWarning(
1526
1652
  `Invalid value "${normalizedValue}" for property "${propName}" in component "${componentType}".`,
1527
1653
  "COMPONENT_INVALID_PROPERTY_VALUE",
@@ -1541,11 +1667,40 @@ function validateSemanticDiagnostics(ast, sourceMap) {
1541
1667
  );
1542
1668
  }
1543
1669
  }
1670
+ if (componentType === "Table") {
1671
+ const hasCaption = String(component.props.caption || "").trim().length > 0;
1672
+ const hasPagination = parseBooleanLike(component.props.pagination ?? "false", false);
1673
+ if (hasCaption && hasPagination) {
1674
+ const rawPaginationAlign = String(component.props.paginationAlign || "right");
1675
+ const paginationAlign = rawPaginationAlign === "left" || rawPaginationAlign === "center" || rawPaginationAlign === "right" ? rawPaginationAlign : "right";
1676
+ const rawCaptionAlign = String(component.props.captionAlign || "");
1677
+ const captionAlign = rawCaptionAlign === "left" || rawCaptionAlign === "center" || rawCaptionAlign === "right" ? rawCaptionAlign : paginationAlign === "left" ? "right" : "left";
1678
+ if (captionAlign === paginationAlign) {
1679
+ emitWarning(
1680
+ `Table footer collision: "captionAlign" and "paginationAlign" both resolve to "${captionAlign}".`,
1681
+ "TABLE_FOOTER_ALIGNMENT_COLLISION",
1682
+ entry?.range || toFallbackRange(),
1683
+ nodeId,
1684
+ "Use different alignments to avoid visual overlap."
1685
+ );
1686
+ }
1687
+ }
1688
+ }
1544
1689
  };
1545
- const checkLayout = (layout) => {
1690
+ const checkLayout = (layout, insideDefinedLayout) => {
1546
1691
  const nodeId = layout._meta?.nodeId;
1547
1692
  const entry = nodeId ? sourceMapByNodeId.get(nodeId) : void 0;
1548
1693
  const rules = LAYOUT_RULES[layout.layoutType];
1694
+ const isDefinedLayoutUsage = definedLayouts.has(layout.layoutType);
1695
+ if (isDefinedLayoutUsage && layout.children.length !== 1) {
1696
+ emitError(
1697
+ `Layout "${layout.layoutType}" expects exactly one child for its Children slot.`,
1698
+ "LAYOUT_DEFINITION_CHILDREN_ARITY",
1699
+ entry?.bodyRange || entry?.range || toFallbackRange(),
1700
+ nodeId,
1701
+ "Provide exactly one nested child block when using this layout."
1702
+ );
1703
+ }
1549
1704
  if (layout.children.length === 0) {
1550
1705
  emitWarning(
1551
1706
  `Layout "${layout.layoutType}" is empty.`,
@@ -1555,7 +1710,7 @@ function validateSemanticDiagnostics(ast, sourceMap) {
1555
1710
  "Add at least one child: component, layout, or cell."
1556
1711
  );
1557
1712
  }
1558
- if (!rules) {
1713
+ if (!rules && !isDefinedLayoutUsage) {
1559
1714
  emitWarning(
1560
1715
  `Layout type "${layout.layoutType}" is not recognized by semantic validation rules.`,
1561
1716
  "LAYOUT_UNKNOWN_TYPE",
@@ -1563,7 +1718,7 @@ function validateSemanticDiagnostics(ast, sourceMap) {
1563
1718
  nodeId,
1564
1719
  `Use one of: ${Object.keys(LAYOUT_RULES).join(", ")}.`
1565
1720
  );
1566
- } else {
1721
+ } else if (rules) {
1567
1722
  const missingRequiredParams = getMissingRequiredNames(rules.requiredParams, layout.params);
1568
1723
  if (missingRequiredParams.length > 0) {
1569
1724
  emitWarning(
@@ -1576,6 +1731,16 @@ function validateSemanticDiagnostics(ast, sourceMap) {
1576
1731
  }
1577
1732
  const allowed = new Set(rules.allowedParams);
1578
1733
  for (const [paramName, paramValue] of Object.entries(layout.params)) {
1734
+ if (layout.layoutType === "split" && paramName === "sidebar") {
1735
+ emitError(
1736
+ 'Split parameter "sidebar" was removed. Use "left" or "right" instead.',
1737
+ "LAYOUT_SPLIT_SIDEBAR_DEPRECATED",
1738
+ getPropertyRange(entry, paramName, "name"),
1739
+ nodeId,
1740
+ "Example: layout split(left: 260) { ... }"
1741
+ );
1742
+ continue;
1743
+ }
1579
1744
  if (!allowed.has(paramName)) {
1580
1745
  emitWarning(
1581
1746
  `Parameter "${paramName}" is not recognized for layout "${layout.layoutType}".`,
@@ -1614,31 +1779,62 @@ function validateSemanticDiagnostics(ast, sourceMap) {
1614
1779
  );
1615
1780
  }
1616
1781
  }
1617
- if (layout.layoutType === "split" && paramName === "sidebar") {
1618
- const sidebar = Number(paramValue);
1619
- if (!Number.isFinite(sidebar) || sidebar <= 0) {
1782
+ if (layout.layoutType === "split" && (paramName === "left" || paramName === "right")) {
1783
+ const splitSize = Number(paramValue);
1784
+ if (!Number.isFinite(splitSize) || splitSize <= 0) {
1620
1785
  emitWarning(
1621
- 'Split "sidebar" must be a positive number.',
1622
- "LAYOUT_SPLIT_SIDEBAR_INVALID",
1786
+ `Split "${paramName}" must be a positive number. Falling back to 250.`,
1787
+ "LAYOUT_SPLIT_WIDTH_INVALID",
1623
1788
  getPropertyRange(entry, paramName, "value"),
1624
1789
  nodeId,
1625
- "Use a value like sidebar: 240."
1790
+ `Use a value like ${paramName}: 260.`
1626
1791
  );
1627
1792
  }
1628
1793
  }
1629
1794
  }
1795
+ if (layout.layoutType === "split") {
1796
+ const hasLeft = layout.params.left !== void 0;
1797
+ const hasRight = layout.params.right !== void 0;
1798
+ if (!hasLeft && !hasRight) {
1799
+ emitError(
1800
+ 'Split layout requires exactly one fixed side width: "left" or "right".',
1801
+ "LAYOUT_SPLIT_SIDE_REQUIRED",
1802
+ entry?.nameRange || entry?.range || toFallbackRange(),
1803
+ nodeId,
1804
+ "Add either left: <number> or right: <number>."
1805
+ );
1806
+ }
1807
+ if (hasLeft && hasRight) {
1808
+ emitError(
1809
+ 'Split layout accepts only one fixed side width: use either "left" or "right", not both.',
1810
+ "LAYOUT_SPLIT_SIDE_CONFLICT",
1811
+ entry?.nameRange || entry?.range || toFallbackRange(),
1812
+ nodeId,
1813
+ "Remove one of the two parameters."
1814
+ );
1815
+ }
1816
+ if (layout.children.length !== 2) {
1817
+ emitError(
1818
+ `Split layout requires exactly 2 children, received ${layout.children.length}.`,
1819
+ "LAYOUT_SPLIT_CHILDREN_ARITY",
1820
+ entry?.bodyRange || entry?.range || toFallbackRange(),
1821
+ nodeId,
1822
+ "Provide exactly two child blocks (left/right content)."
1823
+ );
1824
+ }
1825
+ }
1630
1826
  }
1631
1827
  for (const child of layout.children) {
1632
1828
  if (child.type === "component") {
1633
- checkComponent(child);
1829
+ checkComponent(child, insideDefinedLayout);
1634
1830
  } else if (child.type === "layout") {
1635
- checkLayout(child);
1831
+ checkLayout(child, insideDefinedLayout);
1636
1832
  } else if (child.type === "cell") {
1637
- checkCell(child);
1833
+ checkCell(child, insideDefinedLayout);
1638
1834
  }
1639
1835
  }
1640
1836
  };
1641
- const checkCell = (cell) => {
1837
+ const checkCell = (cell, insideDefinedLayout) => {
1642
1838
  const nodeId = cell._meta?.nodeId;
1643
1839
  const entry = nodeId ? sourceMapByNodeId.get(nodeId) : void 0;
1644
1840
  if (cell.props.span !== void 0) {
@@ -1654,12 +1850,54 @@ function validateSemanticDiagnostics(ast, sourceMap) {
1654
1850
  }
1655
1851
  }
1656
1852
  for (const child of cell.children) {
1657
- if (child.type === "component") checkComponent(child);
1658
- if (child.type === "layout") checkLayout(child);
1853
+ if (child.type === "component") checkComponent(child, insideDefinedLayout);
1854
+ if (child.type === "layout") checkLayout(child, insideDefinedLayout);
1659
1855
  }
1660
1856
  };
1857
+ for (const componentDef of ast.definedComponents) {
1858
+ const nodeId = componentDef._meta?.nodeId;
1859
+ const entry = nodeId ? sourceMapByNodeId.get(nodeId) : void 0;
1860
+ if (!isPascalCaseIdentifier(componentDef.name)) {
1861
+ emitWarning(
1862
+ `Defined component "${componentDef.name}" should use PascalCase naming.`,
1863
+ "COMPONENT_DEFINITION_NAME_STYLE",
1864
+ entry?.nameRange || entry?.range || toFallbackRange(),
1865
+ nodeId,
1866
+ 'Use a name like "MyComponent".'
1867
+ );
1868
+ }
1869
+ if (componentDef.body.type === "component") {
1870
+ checkComponent(componentDef.body, false);
1871
+ } else {
1872
+ checkLayout(componentDef.body, false);
1873
+ }
1874
+ }
1875
+ for (const layoutDef of ast.definedLayouts) {
1876
+ const nodeId = layoutDef._meta?.nodeId;
1877
+ const entry = nodeId ? sourceMapByNodeId.get(nodeId) : void 0;
1878
+ if (!isValidDefinedLayoutName(layoutDef.name)) {
1879
+ emitError(
1880
+ `Defined layout "${layoutDef.name}" must match /^[a-z][a-z0-9_]*$/.`,
1881
+ "LAYOUT_DEFINITION_INVALID_NAME",
1882
+ entry?.nameRange || entry?.range || toFallbackRange(),
1883
+ nodeId,
1884
+ 'Use names like "screen_default" or "appShell".'
1885
+ );
1886
+ }
1887
+ const childrenSlotCount = countChildrenSlots(layoutDef.body);
1888
+ if (childrenSlotCount !== 1) {
1889
+ emitError(
1890
+ `Defined layout "${layoutDef.name}" must contain exactly one "component Children" placeholder.`,
1891
+ "LAYOUT_DEFINITION_CHILDREN_SLOT_COUNT",
1892
+ entry?.bodyRange || entry?.range || toFallbackRange(),
1893
+ nodeId,
1894
+ 'Add exactly one "component Children" in the layout body.'
1895
+ );
1896
+ }
1897
+ checkLayout(layoutDef.body, true);
1898
+ }
1661
1899
  ast.screens.forEach((screen) => {
1662
- checkLayout(screen.layout);
1900
+ checkLayout(screen.layout, false);
1663
1901
  });
1664
1902
  return diagnostics;
1665
1903
  }
@@ -1676,7 +1914,7 @@ ${lexResult.errors.map((e) => e.message).join("\n")}`);
1676
1914
  ${parserInstance.errors.map((e) => e.message).join("\n")}`);
1677
1915
  }
1678
1916
  const ast = visitor.visit(cst);
1679
- validateComponentDefinitionCycles(ast);
1917
+ validateDefinitionCycles(ast);
1680
1918
  return ast;
1681
1919
  }
1682
1920
  function parseWireDSLWithSourceMap(input, filePath = "<input>", options) {
@@ -1707,7 +1945,7 @@ ${parserInstance.errors.map((e) => e.message).join("\n")}`);
1707
1945
  const ast = visitorWithSourceMap.visit(cst);
1708
1946
  const sourceMap = sourceMapBuilder.build();
1709
1947
  try {
1710
- validateComponentDefinitionCycles(ast);
1948
+ validateDefinitionCycles(ast);
1711
1949
  } catch (error) {
1712
1950
  const projectEntry = sourceMap.find((entry) => entry.type === "project");
1713
1951
  diagnostics.push({
@@ -1730,83 +1968,101 @@ ${parserInstance.errors.map((e) => e.message).join("\n")}`);
1730
1968
  }
1731
1969
  return buildParseResult(ast, sourceMap, diagnostics);
1732
1970
  }
1733
- function validateComponentDefinitionCycles(ast) {
1734
- if (!ast.definedComponents || ast.definedComponents.length === 0) {
1971
+ function validateDefinitionCycles(ast) {
1972
+ if ((!ast.definedComponents || ast.definedComponents.length === 0) && (!ast.definedLayouts || ast.definedLayouts.length === 0)) {
1735
1973
  return;
1736
1974
  }
1737
1975
  const components = /* @__PURE__ */ new Map();
1976
+ const layouts = /* @__PURE__ */ new Map();
1738
1977
  ast.definedComponents.forEach((comp) => {
1739
1978
  components.set(comp.name, comp);
1740
1979
  });
1980
+ ast.definedLayouts.forEach((layoutDef) => {
1981
+ layouts.set(layoutDef.name, layoutDef);
1982
+ });
1983
+ const makeComponentKey = (name) => `component:${name}`;
1984
+ const makeLayoutKey = (name) => `layout:${name}`;
1985
+ const displayKey = (key) => key.split(":")[1];
1986
+ const shouldTrackComponentDependency = (name) => components.has(name) && !BUILT_IN_COMPONENTS.has(name);
1987
+ const shouldTrackLayoutDependency = (name) => layouts.has(name) && !BUILT_IN_LAYOUTS.has(name);
1741
1988
  const visited = /* @__PURE__ */ new Set();
1742
1989
  const recursionStack = /* @__PURE__ */ new Set();
1743
- function getComponentDependencies(node) {
1744
- const deps = /* @__PURE__ */ new Set();
1745
- if (node.type === "layout") {
1746
- const layout = node;
1747
- if (layout.children) {
1748
- layout.children.forEach((child) => {
1749
- if (child.type === "component") {
1750
- const component = child;
1751
- deps.add(component.componentType);
1752
- } else if (child.type === "layout") {
1753
- const nested = getComponentDependencies(child);
1754
- nested.forEach((d) => deps.add(d));
1755
- } else if (child.type === "cell") {
1756
- const cell = child;
1757
- if (cell.children) {
1758
- cell.children.forEach((cellChild) => {
1759
- if (cellChild.type === "component") {
1760
- deps.add(cellChild.componentType);
1761
- } else if (cellChild.type === "layout") {
1762
- const nested = getComponentDependencies(cellChild);
1763
- nested.forEach((d) => deps.add(d));
1764
- }
1765
- });
1990
+ const collectLayoutDependencies = (layout, deps) => {
1991
+ if (shouldTrackLayoutDependency(layout.layoutType)) {
1992
+ deps.add(makeLayoutKey(layout.layoutType));
1993
+ }
1994
+ for (const child of layout.children) {
1995
+ if (child.type === "component") {
1996
+ if (shouldTrackComponentDependency(child.componentType)) {
1997
+ deps.add(makeComponentKey(child.componentType));
1998
+ }
1999
+ } else if (child.type === "layout") {
2000
+ collectLayoutDependencies(child, deps);
2001
+ } else if (child.type === "cell") {
2002
+ for (const cellChild of child.children) {
2003
+ if (cellChild.type === "component") {
2004
+ if (shouldTrackComponentDependency(cellChild.componentType)) {
2005
+ deps.add(makeComponentKey(cellChild.componentType));
1766
2006
  }
2007
+ } else if (cellChild.type === "layout") {
2008
+ collectLayoutDependencies(cellChild, deps);
1767
2009
  }
1768
- });
2010
+ }
1769
2011
  }
1770
2012
  }
1771
- return deps;
1772
- }
1773
- function hasCycle(componentName, path = []) {
1774
- if (recursionStack.has(componentName)) {
1775
- const cycleStart = path.indexOf(componentName);
1776
- const cycle = path.slice(cycleStart).concat(componentName);
1777
- return cycle;
2013
+ };
2014
+ const getDependencies = (key) => {
2015
+ const deps = /* @__PURE__ */ new Set();
2016
+ if (key.startsWith("component:")) {
2017
+ const name2 = key.slice("component:".length);
2018
+ const def2 = components.get(name2);
2019
+ if (!def2) return deps;
2020
+ if (def2.body.type === "component") {
2021
+ if (shouldTrackComponentDependency(def2.body.componentType)) {
2022
+ deps.add(makeComponentKey(def2.body.componentType));
2023
+ }
2024
+ } else {
2025
+ collectLayoutDependencies(def2.body, deps);
2026
+ }
2027
+ return deps;
1778
2028
  }
1779
- if (visited.has(componentName)) {
1780
- return null;
2029
+ const name = key.slice("layout:".length);
2030
+ const def = layouts.get(name);
2031
+ if (!def) return deps;
2032
+ collectLayoutDependencies(def.body, deps);
2033
+ return deps;
2034
+ };
2035
+ const findCycle = (key, path = []) => {
2036
+ if (recursionStack.has(key)) {
2037
+ const cycleStart = path.indexOf(key);
2038
+ return path.slice(cycleStart).concat(key);
1781
2039
  }
1782
- const component = components.get(componentName);
1783
- if (!component) {
2040
+ if (visited.has(key)) {
1784
2041
  return null;
1785
2042
  }
1786
- recursionStack.add(componentName);
1787
- const currentPath = [...path, componentName];
1788
- const dependencies = getComponentDependencies(component.body);
2043
+ recursionStack.add(key);
2044
+ const currentPath = [...path, key];
2045
+ const dependencies = getDependencies(key);
1789
2046
  for (const dep of dependencies) {
1790
- const definedDep = components.has(dep);
1791
- if (definedDep) {
1792
- const cycle = hasCycle(dep, currentPath);
1793
- if (cycle) {
1794
- return cycle;
1795
- }
1796
- }
2047
+ const cycle = findCycle(dep, currentPath);
2048
+ if (cycle) return cycle;
1797
2049
  }
1798
- recursionStack.delete(componentName);
1799
- visited.add(componentName);
2050
+ recursionStack.delete(key);
2051
+ visited.add(key);
1800
2052
  return null;
1801
- }
1802
- for (const [componentName] of components) {
2053
+ };
2054
+ const allDefinitions = [
2055
+ ...Array.from(components.keys()).map(makeComponentKey),
2056
+ ...Array.from(layouts.keys()).map(makeLayoutKey)
2057
+ ];
2058
+ for (const key of allDefinitions) {
1803
2059
  visited.clear();
1804
2060
  recursionStack.clear();
1805
- const cycle = hasCycle(componentName);
2061
+ const cycle = findCycle(key);
1806
2062
  if (cycle) {
1807
2063
  throw new Error(
1808
- `Circular component definition detected: ${cycle.join(" \u2192 ")}
1809
- Components cannot reference each other in a cycle.`
2064
+ `Circular component definition detected: ${cycle.map(displayKey).join(" \u2192 ")}
2065
+ Components and layouts cannot reference each other in a cycle.`
1810
2066
  );
1811
2067
  }
1812
2068
  }
@@ -1814,6 +2070,10 @@ Components cannot reference each other in a cycle.`
1814
2070
 
1815
2071
  // src/ir/index.ts
1816
2072
  import { z } from "zod";
2073
+ import {
2074
+ COMPONENTS as COMPONENTS2,
2075
+ LAYOUTS as LAYOUTS2
2076
+ } from "@wire-dsl/language-support/components";
1817
2077
 
1818
2078
  // src/ir/device-presets.ts
1819
2079
  var DEVICE_PRESETS = {
@@ -1978,9 +2238,11 @@ var IRGenerator = class {
1978
2238
  this.idGen = new IDGenerator();
1979
2239
  this.nodes = {};
1980
2240
  this.definedComponents = /* @__PURE__ */ new Map();
2241
+ this.definedLayouts = /* @__PURE__ */ new Map();
1981
2242
  this.definedComponentIndices = /* @__PURE__ */ new Map();
1982
2243
  this.undefinedComponentsUsed = /* @__PURE__ */ new Set();
1983
2244
  this.warnings = [];
2245
+ this.errors = [];
1984
2246
  this.style = {
1985
2247
  density: "normal",
1986
2248
  spacing: "md",
@@ -1993,15 +2255,22 @@ var IRGenerator = class {
1993
2255
  this.idGen.reset();
1994
2256
  this.nodes = {};
1995
2257
  this.definedComponents.clear();
2258
+ this.definedLayouts.clear();
1996
2259
  this.definedComponentIndices.clear();
1997
2260
  this.undefinedComponentsUsed.clear();
1998
2261
  this.warnings = [];
2262
+ this.errors = [];
1999
2263
  if (ast.definedComponents && ast.definedComponents.length > 0) {
2000
2264
  ast.definedComponents.forEach((def, index) => {
2001
2265
  this.definedComponents.set(def.name, def);
2002
2266
  this.definedComponentIndices.set(def.name, index);
2003
2267
  });
2004
2268
  }
2269
+ if (ast.definedLayouts && ast.definedLayouts.length > 0) {
2270
+ ast.definedLayouts.forEach((def) => {
2271
+ this.definedLayouts.set(def.name, def);
2272
+ });
2273
+ }
2005
2274
  this.applyStyle(ast.style);
2006
2275
  const screens = ast.screens.map(
2007
2276
  (screen, screenIndex) => this.convertScreen(screen, screenIndex)
@@ -2013,6 +2282,11 @@ var IRGenerator = class {
2013
2282
  Define these components with: define Component "Name" { ... }`
2014
2283
  );
2015
2284
  }
2285
+ if (this.errors.length > 0) {
2286
+ const messages = this.errors.map((e) => `- [${e.type}] ${e.message}`).join("\n");
2287
+ throw new Error(`IR generation failed with semantic errors:
2288
+ ${messages}`);
2289
+ }
2016
2290
  const project = {
2017
2291
  id: this.sanitizeId(ast.name),
2018
2292
  name: ast.name,
@@ -2139,41 +2413,50 @@ Define these components with: define Component "Name" { ... }`
2139
2413
  getWarnings() {
2140
2414
  return this.warnings;
2141
2415
  }
2142
- convertLayout(layout) {
2416
+ convertLayout(layout, context) {
2417
+ let layoutParams = this.resolveLayoutParams(layout.layoutType, layout.params, context);
2418
+ if (layout.layoutType === "split") {
2419
+ layoutParams = this.normalizeSplitParams(layoutParams);
2420
+ }
2421
+ const layoutChildren = layout.children;
2422
+ const layoutDefinition = this.definedLayouts.get(layout.layoutType);
2423
+ if (layoutDefinition) {
2424
+ return this.expandDefinedLayout(layoutDefinition, layoutParams, layoutChildren, context);
2425
+ }
2143
2426
  const nodeId = this.idGen.generate("node");
2144
2427
  const childRefs = [];
2145
- for (const child of layout.children) {
2428
+ for (const child of layoutChildren) {
2146
2429
  if (child.type === "layout") {
2147
- const childId = this.convertLayout(child);
2148
- childRefs.push({ ref: childId });
2430
+ const childId = this.convertLayout(child, context);
2431
+ if (childId) childRefs.push({ ref: childId });
2149
2432
  } else if (child.type === "component") {
2150
- const childId = this.convertComponent(child);
2151
- childRefs.push({ ref: childId });
2433
+ const childId = this.convertComponent(child, context);
2434
+ if (childId) childRefs.push({ ref: childId });
2152
2435
  } else if (child.type === "cell") {
2153
- const childId = this.convertCell(child);
2154
- childRefs.push({ ref: childId });
2436
+ const childId = this.convertCell(child, context);
2437
+ if (childId) childRefs.push({ ref: childId });
2155
2438
  }
2156
2439
  }
2157
2440
  const style = {};
2158
- if (layout.params.padding) {
2159
- style.padding = String(layout.params.padding);
2441
+ if (layoutParams.padding !== void 0) {
2442
+ style.padding = String(layoutParams.padding);
2160
2443
  } else {
2161
2444
  style.padding = "none";
2162
2445
  }
2163
- if (layout.params.gap) {
2164
- style.gap = String(layout.params.gap);
2446
+ if (layoutParams.gap !== void 0) {
2447
+ style.gap = String(layoutParams.gap);
2165
2448
  }
2166
- if (layout.params.align) {
2167
- style.align = layout.params.align;
2449
+ if (layoutParams.align !== void 0) {
2450
+ style.align = layoutParams.align;
2168
2451
  }
2169
- if (layout.params.background) {
2170
- style.background = String(layout.params.background);
2452
+ if (layoutParams.background !== void 0) {
2453
+ style.background = String(layoutParams.background);
2171
2454
  }
2172
2455
  const containerNode = {
2173
2456
  id: nodeId,
2174
2457
  kind: "container",
2175
2458
  containerType: layout.layoutType,
2176
- params: this.cleanParams(layout.params),
2459
+ params: this.cleanParams(layoutParams),
2177
2460
  children: childRefs,
2178
2461
  style,
2179
2462
  meta: {
@@ -2184,16 +2467,16 @@ Define these components with: define Component "Name" { ... }`
2184
2467
  this.nodes[nodeId] = containerNode;
2185
2468
  return nodeId;
2186
2469
  }
2187
- convertCell(cell) {
2470
+ convertCell(cell, context) {
2188
2471
  const nodeId = this.idGen.generate("node");
2189
2472
  const childRefs = [];
2190
2473
  for (const child of cell.children) {
2191
2474
  if (child.type === "layout") {
2192
- const childId = this.convertLayout(child);
2193
- childRefs.push({ ref: childId });
2475
+ const childId = this.convertLayout(child, context);
2476
+ if (childId) childRefs.push({ ref: childId });
2194
2477
  } else if (child.type === "component") {
2195
- const childId = this.convertComponent(child);
2196
- childRefs.push({ ref: childId });
2478
+ const childId = this.convertComponent(child, context);
2479
+ if (childId) childRefs.push({ ref: childId });
2197
2480
  }
2198
2481
  }
2199
2482
  const containerNode = {
@@ -2214,10 +2497,28 @@ Define these components with: define Component "Name" { ... }`
2214
2497
  this.nodes[nodeId] = containerNode;
2215
2498
  return nodeId;
2216
2499
  }
2217
- convertComponent(component) {
2500
+ convertComponent(component, context) {
2501
+ if (component.componentType === "Children") {
2502
+ if (!context?.allowChildrenSlot) {
2503
+ this.errors.push({
2504
+ type: "children-slot-outside-layout-definition",
2505
+ message: '"Children" placeholder can only be used inside a define Layout body.'
2506
+ });
2507
+ return null;
2508
+ }
2509
+ if (!context.childrenSlot) {
2510
+ this.errors.push({
2511
+ type: "children-slot-missing-child",
2512
+ message: `Layout "${context.definitionName}" requires exactly one child for "Children".`
2513
+ });
2514
+ return null;
2515
+ }
2516
+ return this.convertASTNode(context.childrenSlot, context);
2517
+ }
2518
+ const resolvedProps = this.resolveComponentProps(component.componentType, component.props, context);
2218
2519
  const definition = this.definedComponents.get(component.componentType);
2219
2520
  if (definition) {
2220
- return this.expandDefinedComponent(definition);
2521
+ return this.expandDefinedComponent(definition, resolvedProps, context);
2221
2522
  }
2222
2523
  const builtInComponents = /* @__PURE__ */ new Set([
2223
2524
  "Button",
@@ -2260,7 +2561,7 @@ Define these components with: define Component "Name" { ... }`
2260
2561
  id: nodeId,
2261
2562
  kind: "component",
2262
2563
  componentType: component.componentType,
2263
- props: component.props,
2564
+ props: resolvedProps,
2264
2565
  style: {},
2265
2566
  meta: {
2266
2567
  nodeId: component._meta?.nodeId
@@ -2270,15 +2571,196 @@ Define these components with: define Component "Name" { ... }`
2270
2571
  this.nodes[nodeId] = componentNode;
2271
2572
  return nodeId;
2272
2573
  }
2273
- expandDefinedComponent(definition) {
2574
+ expandDefinedComponent(definition, invocationArgs, parentContext) {
2575
+ const context = {
2576
+ args: invocationArgs,
2577
+ providedArgNames: new Set(Object.keys(invocationArgs)),
2578
+ usedArgNames: /* @__PURE__ */ new Set(),
2579
+ definitionName: definition.name,
2580
+ definitionKind: "component",
2581
+ allowChildrenSlot: false
2582
+ };
2274
2583
  if (definition.body.type === "layout") {
2275
- return this.convertLayout(definition.body);
2584
+ const result = this.convertLayout(definition.body, context);
2585
+ this.reportUnusedArguments(context);
2586
+ return result;
2276
2587
  } else if (definition.body.type === "component") {
2277
- return this.convertComponent(definition.body);
2588
+ const result = this.convertComponent(definition.body, context);
2589
+ this.reportUnusedArguments(context);
2590
+ return result;
2278
2591
  } else {
2279
2592
  throw new Error(`Invalid defined component body type for "${definition.name}"`);
2280
2593
  }
2281
2594
  }
2595
+ expandDefinedLayout(definition, invocationParams, invocationChildren, parentContext) {
2596
+ if (invocationChildren.length !== 1) {
2597
+ this.errors.push({
2598
+ type: "layout-children-arity",
2599
+ message: `Layout "${definition.name}" expects exactly one child, received ${invocationChildren.length}.`
2600
+ });
2601
+ }
2602
+ const rawSlot = invocationChildren[0];
2603
+ const resolvedSlot = rawSlot ? this.resolveChildrenSlot(rawSlot, parentContext) : void 0;
2604
+ const context = {
2605
+ args: invocationParams,
2606
+ providedArgNames: new Set(Object.keys(invocationParams)),
2607
+ usedArgNames: /* @__PURE__ */ new Set(),
2608
+ definitionName: definition.name,
2609
+ definitionKind: "layout",
2610
+ allowChildrenSlot: true,
2611
+ childrenSlot: resolvedSlot
2612
+ };
2613
+ const nodeId = this.convertLayout(definition.body, context);
2614
+ this.reportUnusedArguments(context);
2615
+ return nodeId;
2616
+ }
2617
+ resolveChildrenSlot(slot, parentContext) {
2618
+ if (slot.type === "component" && slot.componentType === "Children") {
2619
+ if (parentContext?.allowChildrenSlot) {
2620
+ return parentContext.childrenSlot;
2621
+ }
2622
+ this.errors.push({
2623
+ type: "children-slot-outside-layout-definition",
2624
+ message: '"Children" placeholder forwarding is only valid inside define Layout bodies.'
2625
+ });
2626
+ return void 0;
2627
+ }
2628
+ return slot;
2629
+ }
2630
+ convertASTNode(node, context) {
2631
+ if (node.type === "layout") return this.convertLayout(node, context);
2632
+ if (node.type === "component") return this.convertComponent(node, context);
2633
+ return this.convertCell(node, context);
2634
+ }
2635
+ resolveLayoutParams(layoutType, params, context) {
2636
+ const resolved = {};
2637
+ for (const [key, value] of Object.entries(params)) {
2638
+ const resolvedValue = this.resolveBindingValue(
2639
+ value,
2640
+ context,
2641
+ "layout-parameter",
2642
+ layoutType,
2643
+ key
2644
+ );
2645
+ if (resolvedValue !== void 0) {
2646
+ resolved[key] = resolvedValue;
2647
+ }
2648
+ }
2649
+ return resolved;
2650
+ }
2651
+ normalizeSplitParams(params) {
2652
+ const normalized = { ...params };
2653
+ if (normalized.sidebar !== void 0 && normalized.left === void 0 && normalized.right === void 0) {
2654
+ normalized.left = normalized.sidebar;
2655
+ this.warnings.push({
2656
+ type: "split-sidebar-deprecated",
2657
+ message: 'Split parameter "sidebar" is deprecated. Use "left" or "right".'
2658
+ });
2659
+ }
2660
+ delete normalized.sidebar;
2661
+ const hasLeft = normalized.left !== void 0;
2662
+ const hasRight = normalized.right !== void 0;
2663
+ if (hasLeft && hasRight) {
2664
+ delete normalized.right;
2665
+ this.warnings.push({
2666
+ type: "split-side-conflict",
2667
+ message: 'Split layout received both "left" and "right"; keeping "left".'
2668
+ });
2669
+ }
2670
+ if (!hasLeft && !hasRight) {
2671
+ normalized.left = 250;
2672
+ this.warnings.push({
2673
+ type: "split-side-missing",
2674
+ message: 'Split layout missing both "left" and "right"; defaulting to left: 250.'
2675
+ });
2676
+ }
2677
+ if (normalized.left !== void 0) {
2678
+ const leftWidth = Number(normalized.left);
2679
+ if (!Number.isFinite(leftWidth) || leftWidth <= 0) {
2680
+ normalized.left = 250;
2681
+ this.warnings.push({
2682
+ type: "split-left-invalid",
2683
+ message: 'Split "left" must be a positive number. Falling back to 250.'
2684
+ });
2685
+ }
2686
+ }
2687
+ if (normalized.right !== void 0) {
2688
+ const rightWidth = Number(normalized.right);
2689
+ if (!Number.isFinite(rightWidth) || rightWidth <= 0) {
2690
+ normalized.right = 250;
2691
+ this.warnings.push({
2692
+ type: "split-right-invalid",
2693
+ message: 'Split "right" must be a positive number. Falling back to 250.'
2694
+ });
2695
+ }
2696
+ }
2697
+ return normalized;
2698
+ }
2699
+ resolveComponentProps(componentType, props, context) {
2700
+ const resolved = {};
2701
+ for (const [key, value] of Object.entries(props)) {
2702
+ const resolvedValue = this.resolveBindingValue(
2703
+ value,
2704
+ context,
2705
+ "component-property",
2706
+ componentType,
2707
+ key
2708
+ );
2709
+ if (resolvedValue !== void 0) {
2710
+ resolved[key] = resolvedValue;
2711
+ }
2712
+ }
2713
+ return resolved;
2714
+ }
2715
+ resolveBindingValue(value, context, kind, targetType, targetName) {
2716
+ if (typeof value !== "string" || !value.startsWith("prop_")) {
2717
+ return value;
2718
+ }
2719
+ const argName = value.slice("prop_".length);
2720
+ if (!context) {
2721
+ return value;
2722
+ }
2723
+ if (Object.prototype.hasOwnProperty.call(context.args, argName)) {
2724
+ context.usedArgNames.add(argName);
2725
+ return context.args[argName];
2726
+ }
2727
+ const required = this.isBindingTargetRequired(kind, targetType, targetName);
2728
+ const descriptor = kind === "component-property" ? "property" : "parameter";
2729
+ const message = `Missing required bound ${descriptor} "${targetName}" for ${kind === "component-property" ? "component" : "layout"} "${targetType}" in ${context.definitionKind} "${context.definitionName}" (expected arg "${argName}").`;
2730
+ if (required) {
2731
+ this.errors.push({ type: "missing-required-bound-value", message });
2732
+ } else {
2733
+ this.warnings.push({
2734
+ type: "missing-bound-value",
2735
+ 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}".`
2736
+ });
2737
+ }
2738
+ return void 0;
2739
+ }
2740
+ isBindingTargetRequired(kind, targetType, targetName) {
2741
+ if (kind === "component-property") {
2742
+ const metadata = COMPONENTS2[targetType];
2743
+ const property2 = metadata?.properties?.[targetName];
2744
+ if (!property2) return false;
2745
+ return property2.required === true && property2.defaultValue === void 0;
2746
+ }
2747
+ const layoutMetadata = LAYOUTS2[targetType];
2748
+ if (!layoutMetadata) return false;
2749
+ const property = layoutMetadata.properties?.[targetName];
2750
+ const requiredFromProperty = property?.required === true && property.defaultValue === void 0;
2751
+ const requiredFromLayout = (layoutMetadata.requiredProperties || []).includes(targetName);
2752
+ return requiredFromProperty || requiredFromLayout;
2753
+ }
2754
+ reportUnusedArguments(context) {
2755
+ for (const arg of context.providedArgNames) {
2756
+ if (!context.usedArgNames.has(arg)) {
2757
+ this.warnings.push({
2758
+ type: "unused-definition-argument",
2759
+ message: `Argument "${arg}" is not used by ${context.definitionKind} "${context.definitionName}".`
2760
+ });
2761
+ }
2762
+ }
2763
+ }
2282
2764
  cleanParams(params) {
2283
2765
  const cleaned = {};
2284
2766
  for (const [key, value] of Object.entries(params)) {
@@ -2328,9 +2810,24 @@ var ICON_SIZES_BY_DENSITY = {
2328
2810
  comfortable: { xs: 14, sm: 16, md: 20, lg: 28, xl: 36 }
2329
2811
  };
2330
2812
  var ICON_BUTTON_SIZES_BY_DENSITY = {
2331
- compact: { sm: 24, md: 28, lg: 32 },
2332
- normal: { sm: 28, md: 32, lg: 40 },
2333
- comfortable: { sm: 32, md: 40, lg: 48 }
2813
+ compact: { sm: 20, md: 24, lg: 32 },
2814
+ normal: { sm: 24, md: 32, lg: 40 },
2815
+ comfortable: { sm: 28, md: 40, lg: 48 }
2816
+ };
2817
+ var CONTROL_HEIGHTS_BY_DENSITY = {
2818
+ compact: { sm: 28, md: 32, lg: 36 },
2819
+ normal: { sm: 36, md: 40, lg: 48 },
2820
+ comfortable: { sm: 40, md: 48, lg: 56 }
2821
+ };
2822
+ var ACTION_CONTROL_HEIGHTS_BY_DENSITY = {
2823
+ compact: { sm: 20, md: 24, lg: 32 },
2824
+ normal: { sm: 24, md: 32, lg: 40 },
2825
+ comfortable: { sm: 28, md: 40, lg: 48 }
2826
+ };
2827
+ var CONTROL_PADDING_BY_DENSITY = {
2828
+ compact: { none: 0, xs: 4, sm: 8, md: 10, lg: 14, xl: 18 },
2829
+ normal: { none: 0, xs: 6, sm: 10, md: 14, lg: 18, xl: 24 },
2830
+ comfortable: { none: 0, xs: 8, sm: 12, md: 16, lg: 22, xl: 28 }
2334
2831
  };
2335
2832
  function resolveIconSize(size, density = "normal") {
2336
2833
  const map = ICON_SIZES_BY_DENSITY[density] || ICON_SIZES_BY_DENSITY.normal;
@@ -2340,6 +2837,18 @@ function resolveIconButtonSize(size, density = "normal") {
2340
2837
  const map = ICON_BUTTON_SIZES_BY_DENSITY[density] || ICON_BUTTON_SIZES_BY_DENSITY.normal;
2341
2838
  return map[size || "md"] || map.md;
2342
2839
  }
2840
+ function resolveControlHeight(size, density = "normal") {
2841
+ const map = CONTROL_HEIGHTS_BY_DENSITY[density] || CONTROL_HEIGHTS_BY_DENSITY.normal;
2842
+ return map[size || "md"] || map.md;
2843
+ }
2844
+ function resolveActionControlHeight(size, density = "normal") {
2845
+ const map = ACTION_CONTROL_HEIGHTS_BY_DENSITY[density] || ACTION_CONTROL_HEIGHTS_BY_DENSITY.normal;
2846
+ return map[size || "md"] || map.md;
2847
+ }
2848
+ function resolveControlHorizontalPadding(padding, density = "normal") {
2849
+ const map = CONTROL_PADDING_BY_DENSITY[density] || CONTROL_PADDING_BY_DENSITY.normal;
2850
+ return map[padding || "md"] ?? map.md;
2851
+ }
2343
2852
 
2344
2853
  // src/shared/heading-levels.ts
2345
2854
  var DEFAULT_LEVEL = "h2";
@@ -2658,6 +3167,41 @@ var LayoutEngine = class {
2658
3167
  }
2659
3168
  return totalHeight;
2660
3169
  }
3170
+ if (node.containerType === "split") {
3171
+ const splitGap = this.resolveSpacing(node.style.gap);
3172
+ const leftParam = node.params.left;
3173
+ const rightParam = node.params.right;
3174
+ const leftWidthRaw = Number(leftParam);
3175
+ const rightWidthRaw = Number(rightParam);
3176
+ const hasLeft = leftParam !== void 0;
3177
+ const hasRight = rightParam !== void 0;
3178
+ const leftWidth = Number.isFinite(leftWidthRaw) && leftWidthRaw > 0 ? leftWidthRaw : availableWidth / 2;
3179
+ const rightWidth = Number.isFinite(rightWidthRaw) && rightWidthRaw > 0 ? rightWidthRaw : availableWidth / 2;
3180
+ let maxHeight = 0;
3181
+ node.children.forEach((childRef, index) => {
3182
+ const child = this.nodes[childRef.ref];
3183
+ let childHeight = this.getComponentHeight();
3184
+ const isFirst = index === 0;
3185
+ let childWidth;
3186
+ if (node.children.length >= 2) {
3187
+ if (hasRight && !hasLeft) {
3188
+ childWidth = isFirst ? Math.max(1, availableWidth - rightWidth - splitGap) : rightWidth;
3189
+ } else {
3190
+ childWidth = isFirst ? leftWidth : Math.max(1, availableWidth - leftWidth - splitGap);
3191
+ }
3192
+ } else {
3193
+ childWidth = availableWidth;
3194
+ }
3195
+ if (child?.kind === "component") {
3196
+ childHeight = child.props.height ? Number(child.props.height) : this.getIntrinsicComponentHeight(child, childWidth);
3197
+ } else if (child?.kind === "container") {
3198
+ childHeight = this.calculateContainerHeight(child, childWidth);
3199
+ }
3200
+ maxHeight = Math.max(maxHeight, childHeight);
3201
+ });
3202
+ totalHeight += maxHeight;
3203
+ return totalHeight;
3204
+ }
2661
3205
  const direction = node.params.direction || "vertical";
2662
3206
  if (node.containerType === "stack" && direction === "horizontal") {
2663
3207
  let maxHeight = 0;
@@ -2780,14 +3324,28 @@ var LayoutEngine = class {
2780
3324
  calculateSplit(node, x, y, width, height) {
2781
3325
  if (node.kind !== "container") return;
2782
3326
  const gap = this.resolveSpacing(node.style.gap);
2783
- const sidebarWidth = Number(node.params.sidebar) || 260;
3327
+ const leftParam = node.params.left;
3328
+ const rightParam = node.params.right;
3329
+ const leftWidthRaw = Number(leftParam);
3330
+ const rightWidthRaw = Number(rightParam);
3331
+ const hasLeft = leftParam !== void 0;
3332
+ const hasRight = rightParam !== void 0;
3333
+ const leftWidth = Number.isFinite(leftWidthRaw) && leftWidthRaw > 0 ? leftWidthRaw : 250;
3334
+ const rightWidth = Number.isFinite(rightWidthRaw) && rightWidthRaw > 0 ? rightWidthRaw : 250;
2784
3335
  if (node.children.length === 1) {
2785
3336
  this.calculateNode(node.children[0].ref, x, y, width, height, "split");
2786
3337
  } else if (node.children.length >= 2) {
2787
- this.calculateNode(node.children[0].ref, x, y, sidebarWidth, height, "split");
2788
- const contentX = x + sidebarWidth + gap;
2789
- const contentWidth = width - sidebarWidth - gap;
2790
- this.calculateNode(node.children[1].ref, contentX, y, contentWidth, height, "split");
3338
+ if (hasRight && !hasLeft) {
3339
+ const flexibleLeftWidth = Math.max(1, width - rightWidth - gap);
3340
+ const rightX = x + flexibleLeftWidth + gap;
3341
+ this.calculateNode(node.children[0].ref, x, y, flexibleLeftWidth, height, "split");
3342
+ this.calculateNode(node.children[1].ref, rightX, y, rightWidth, height, "split");
3343
+ } else {
3344
+ const flexibleRightWidth = Math.max(1, width - leftWidth - gap);
3345
+ const rightX = x + leftWidth + gap;
3346
+ this.calculateNode(node.children[0].ref, x, y, leftWidth, height, "split");
3347
+ this.calculateNode(node.children[1].ref, rightX, y, flexibleRightWidth, height, "split");
3348
+ }
2791
3349
  }
2792
3350
  }
2793
3351
  calculatePanel(node, x, y, width, height) {
@@ -2936,7 +3494,11 @@ var LayoutEngine = class {
2936
3494
  }
2937
3495
  getIntrinsicComponentHeight(node, availableWidth) {
2938
3496
  if (node.kind !== "component") return this.getComponentHeight();
2939
- const controlLabelOffset = node.componentType === "Input" || node.componentType === "Textarea" || node.componentType === "Select" ? this.getControlLabelOffset(String(node.props.label || "")) : 0;
3497
+ const controlSize = String(node.props.size || "md");
3498
+ const density = this.style.density || "normal";
3499
+ const inputControlHeight = resolveControlHeight(controlSize, density);
3500
+ const actionControlHeight = resolveActionControlHeight(controlSize, density);
3501
+ const controlLabelOffset = node.componentType === "Input" || node.componentType === "Textarea" || node.componentType === "Select" ? this.getControlLabelOffset(String(node.props.label || "")) : (node.componentType === "Button" || node.componentType === "IconButton") && this.parseBooleanProp(node.props.labelSpace, false) ? 18 : 0;
2940
3502
  if (node.componentType === "Image") {
2941
3503
  const placeholder = String(node.props.placeholder || "landscape");
2942
3504
  const aspectRatios = {
@@ -2963,12 +3525,26 @@ var LayoutEngine = class {
2963
3525
  }
2964
3526
  const rowCount = Number(node.props.rows || node.props.rowsMock || 5);
2965
3527
  const hasTitle = !!node.props.title;
2966
- const hasPagination = String(node.props.pagination) === "true";
3528
+ const hasPagination = this.parseBooleanProp(node.props.pagination, false);
3529
+ const hasCaption = String(node.props.caption || "").trim().length > 0;
3530
+ const paginationAlign = String(node.props.paginationAlign || "right");
3531
+ const captionAlign = String(node.props.captionAlign || "");
3532
+ const effectiveCaptionAlign = captionAlign === "left" || captionAlign === "center" || captionAlign === "right" ? captionAlign : paginationAlign === "left" ? "right" : "left";
3533
+ const sameFooterAlign = hasCaption && hasPagination && effectiveCaptionAlign === paginationAlign;
2967
3534
  const headerHeight = 44;
2968
3535
  const rowHeight = 36;
2969
3536
  const titleHeight = hasTitle ? 32 : 0;
2970
- const paginationHeight = hasPagination ? 64 : 0;
2971
- return titleHeight + headerHeight + rowCount * rowHeight + paginationHeight;
3537
+ let footerHeight = 0;
3538
+ if (hasPagination || hasCaption) {
3539
+ const footerBottomPadding = 12;
3540
+ footerHeight += 16;
3541
+ footerHeight += hasPagination ? 32 : 18;
3542
+ if (sameFooterAlign) {
3543
+ footerHeight += 8 + 18;
3544
+ }
3545
+ footerHeight += footerBottomPadding;
3546
+ }
3547
+ return titleHeight + headerHeight + rowCount * rowHeight + footerHeight;
2972
3548
  }
2973
3549
  if (node.componentType === "Heading") {
2974
3550
  const text = String(node.props.text || "Heading");
@@ -2977,15 +3553,15 @@ var LayoutEngine = class {
2977
3553
  const maxWidth = availableWidth && availableWidth > 0 ? availableWidth : 200;
2978
3554
  const lines = this.wrapTextToLines(text, maxWidth, fontSize);
2979
3555
  const wrappedHeight = Math.max(1, lines.length) * lineHeightPx;
2980
- const density = this.style.density || "normal";
2981
- const verticalPadding = resolveHeadingVerticalPadding(node.props.spacing, density);
3556
+ const density2 = this.style.density || "normal";
3557
+ const verticalPadding = resolveHeadingVerticalPadding(node.props.spacing, density2);
2982
3558
  if (verticalPadding === null) {
2983
3559
  return Math.max(this.getComponentHeight(), wrappedHeight);
2984
3560
  }
2985
3561
  return Math.max(1, Math.ceil(wrappedHeight + verticalPadding * 2));
2986
3562
  }
2987
3563
  if (node.componentType === "Text") {
2988
- const content = String(node.props.content || "");
3564
+ const content = String(node.props.text || "");
2989
3565
  const { fontSize, lineHeight } = this.getTextMetricsForDensity();
2990
3566
  const lineHeightPx = Math.ceil(fontSize * lineHeight);
2991
3567
  const maxWidth = availableWidth && availableWidth > 0 ? availableWidth : 200;
@@ -3034,7 +3610,10 @@ var LayoutEngine = class {
3034
3610
  if (node.componentType === "Divider") return 1;
3035
3611
  if (node.componentType === "Separate") return this.getSeparateSize(node);
3036
3612
  if (node.componentType === "Input" || node.componentType === "Select") {
3037
- return this.getComponentHeight() + controlLabelOffset;
3613
+ return inputControlHeight + controlLabelOffset;
3614
+ }
3615
+ if (node.componentType === "Button" || node.componentType === "IconButton" || node.componentType === "Link") {
3616
+ return actionControlHeight + controlLabelOffset;
3038
3617
  }
3039
3618
  return this.getComponentHeight();
3040
3619
  }
@@ -3051,7 +3630,10 @@ var LayoutEngine = class {
3051
3630
  }
3052
3631
  if (node.componentType === "IconButton") {
3053
3632
  const size = String(node.props.size || "md");
3054
- return resolveIconButtonSize(size, this.style.density || "normal");
3633
+ const density = this.style.density || "normal";
3634
+ const baseSize = resolveIconButtonSize(size, density);
3635
+ const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
3636
+ return baseSize + extraPadding * 2;
3055
3637
  }
3056
3638
  if (node.componentType === "Checkbox" || node.componentType === "Radio") {
3057
3639
  return 24;
@@ -3060,11 +3642,13 @@ var LayoutEngine = class {
3060
3642
  if (node.componentType === "Button" || node.componentType === "Link") {
3061
3643
  const text = String(node.props.text || "");
3062
3644
  const { fontSize, paddingX } = this.getButtonMetricsForDensity();
3645
+ const density = this.style.density || "normal";
3646
+ const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
3063
3647
  const textWidth = this.estimateTextWidth(text, fontSize);
3064
- return Math.max(60, Math.ceil(textWidth + paddingX * 2));
3648
+ return Math.max(60, Math.ceil(textWidth + (paddingX + extraPadding) * 2));
3065
3649
  }
3066
3650
  if (node.componentType === "Label" || node.componentType === "Text") {
3067
- const text = String(node.props.content || node.props.text || "");
3651
+ const text = String(node.props.text || "");
3068
3652
  return Math.max(60, text.length * 8 + 16);
3069
3653
  }
3070
3654
  if (node.componentType === "Heading") {
@@ -3926,7 +4510,7 @@ var THEMES = {
3926
4510
  bg: "#F8FAFC",
3927
4511
  cardBg: "#FFFFFF",
3928
4512
  border: "#E2E8F0",
3929
- text: "#1E293B",
4513
+ text: "#000000",
3930
4514
  textMuted: "#64748B",
3931
4515
  primary: "#3B82F6",
3932
4516
  primaryHover: "#2563EB",
@@ -3936,7 +4520,7 @@ var THEMES = {
3936
4520
  bg: "#0F172A",
3937
4521
  cardBg: "#1E293B",
3938
4522
  border: "#334155",
3939
- text: "#F1F5F9",
4523
+ text: "#FFFFFF",
3940
4524
  textMuted: "#94A3B8",
3941
4525
  primary: "#60A5FA",
3942
4526
  primaryHover: "#3B82F6",
@@ -3946,7 +4530,7 @@ var THEMES = {
3946
4530
  var SVGRenderer = class {
3947
4531
  constructor(ir, layout, options) {
3948
4532
  this.renderedNodeIds = /* @__PURE__ */ new Set();
3949
- this.fontFamily = "system-ui, -apple-system, sans-serif";
4533
+ this.fontFamily = "Arial, Helvetica, sans-serif";
3950
4534
  this.parentContainerByChildId = /* @__PURE__ */ new Map();
3951
4535
  this.ir = ir;
3952
4536
  this.layout = layout;
@@ -3960,7 +4544,6 @@ var SVGRenderer = class {
3960
4544
  includeLabels: options?.includeLabels ?? true,
3961
4545
  screenName: options?.screenName
3962
4546
  };
3963
- this.renderTheme = THEMES[this.options.theme];
3964
4547
  this.colorResolver = new ColorResolver();
3965
4548
  this.buildParentContainerIndex();
3966
4549
  if (ir.project.mocks && Object.keys(ir.project.mocks).length > 0) {
@@ -3969,6 +4552,12 @@ var SVGRenderer = class {
3969
4552
  if (ir.project.colors && Object.keys(ir.project.colors).length > 0) {
3970
4553
  this.colorResolver.setCustomColors(ir.project.colors);
3971
4554
  }
4555
+ const themeDefaults = THEMES[this.options.theme];
4556
+ this.renderTheme = {
4557
+ ...themeDefaults,
4558
+ text: this.resolveTextColor(),
4559
+ textMuted: this.resolveMutedColor()
4560
+ };
3972
4561
  }
3973
4562
  /**
3974
4563
  * Get list of available screens in the project
@@ -4050,6 +4639,9 @@ var SVGRenderer = class {
4050
4639
  if (node.containerType === "card") {
4051
4640
  this.renderCardBorder(node, pos, containerGroup);
4052
4641
  }
4642
+ if (node.containerType === "split") {
4643
+ this.renderSplitDecoration(node, pos, containerGroup);
4644
+ }
4053
4645
  node.children.forEach((childRef) => {
4054
4646
  this.renderNode(childRef.ref, containerGroup);
4055
4647
  });
@@ -4137,6 +4729,8 @@ var SVGRenderer = class {
4137
4729
  }
4138
4730
  renderHeading(node, pos) {
4139
4731
  const text = String(node.props.text || "Heading");
4732
+ const variant = String(node.props.variant || "default");
4733
+ const headingColor = variant === "default" ? this.resolveTextColor() : this.resolveVariantColor(variant, this.resolveTextColor());
4140
4734
  const headingTypography = this.getHeadingTypography(node);
4141
4735
  const fontSize = headingTypography.fontSize;
4142
4736
  const fontWeight = headingTypography.fontWeight;
@@ -4146,10 +4740,10 @@ var SVGRenderer = class {
4146
4740
  if (lines.length <= 1) {
4147
4741
  return `<g${this.getDataNodeId(node)}>
4148
4742
  <text x="${pos.x}" y="${firstLineY}"
4149
- font-family="system-ui, -apple-system, sans-serif"
4743
+ font-family="Arial, Helvetica, sans-serif"
4150
4744
  font-size="${fontSize}"
4151
4745
  font-weight="${fontWeight}"
4152
- fill="${this.renderTheme.text}">${this.escapeXml(text)}</text>
4746
+ fill="${headingColor}">${this.escapeXml(text)}</text>
4153
4747
  </g>`;
4154
4748
  }
4155
4749
  const tspans = lines.map(
@@ -4157,41 +4751,46 @@ var SVGRenderer = class {
4157
4751
  ).join("");
4158
4752
  return `<g${this.getDataNodeId(node)}>
4159
4753
  <text x="${pos.x}" y="${firstLineY}"
4160
- font-family="system-ui, -apple-system, sans-serif"
4754
+ font-family="Arial, Helvetica, sans-serif"
4161
4755
  font-size="${fontSize}"
4162
4756
  font-weight="${fontWeight}"
4163
- fill="${this.renderTheme.text}">${tspans}</text>
4757
+ fill="${headingColor}">${tspans}</text>
4164
4758
  </g>`;
4165
4759
  }
4166
4760
  renderButton(node, pos) {
4167
4761
  const text = String(node.props.text || "Button");
4168
4762
  const variant = String(node.props.variant || "default");
4763
+ const size = String(node.props.size || "md");
4764
+ const density = this.ir.project.style.density || "normal";
4765
+ const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
4766
+ const labelOffset = this.parseBooleanProp(node.props.labelSpace, false) ? 18 : 0;
4169
4767
  const fullWidth = this.shouldButtonFillAvailableWidth(node);
4170
4768
  const radius = this.tokens.button.radius;
4171
4769
  const fontSize = this.tokens.button.fontSize;
4172
4770
  const fontWeight = this.tokens.button.fontWeight;
4173
4771
  const paddingX = this.tokens.button.paddingX;
4174
- const paddingY = this.tokens.button.paddingY;
4772
+ const controlHeight = resolveActionControlHeight(size, density);
4773
+ const buttonY = pos.y + labelOffset;
4774
+ const buttonHeight = Math.max(16, Math.min(controlHeight, pos.height - labelOffset));
4175
4775
  const idealTextWidth = this.estimateTextWidth(text, fontSize);
4176
- const buttonWidth = fullWidth ? Math.max(1, pos.width) : this.clampControlWidth(Math.max(Math.ceil(idealTextWidth + paddingX * 2), 60), pos.width);
4177
- const buttonHeight = fontSize + paddingY * 2;
4178
- const availableTextWidth = Math.max(0, buttonWidth - paddingX * 2);
4776
+ const buttonWidth = fullWidth ? Math.max(1, pos.width) : this.clampControlWidth(Math.max(Math.ceil(idealTextWidth + (paddingX + extraPadding) * 2), 60), pos.width);
4777
+ const availableTextWidth = Math.max(0, buttonWidth - (paddingX + extraPadding) * 2);
4179
4778
  const visibleText = this.truncateTextToWidth(text, availableTextWidth, fontSize);
4180
4779
  const semanticBase = this.getSemanticVariantColor(variant);
4181
4780
  const hasExplicitVariantColor = semanticBase !== void 0 || this.colorResolver.hasColor(variant);
4182
4781
  const resolvedBase = this.resolveVariantColor(variant, this.renderTheme.primary);
4183
4782
  const bgColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.85) : "rgba(226, 232, 240, 0.9)";
4184
- const textColor = hasExplicitVariantColor ? "#FFFFFF" : "rgba(30, 41, 59, 0.85)";
4783
+ const textColor = hasExplicitVariantColor ? "#FFFFFF" : this.hexToRgba(this.resolveTextColor(), 0.85);
4185
4784
  const borderColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.7) : "rgba(100, 116, 139, 0.4)";
4186
4785
  return `<g${this.getDataNodeId(node)}>
4187
- <rect x="${pos.x}" y="${pos.y}"
4786
+ <rect x="${pos.x}" y="${buttonY}"
4188
4787
  width="${buttonWidth}" height="${buttonHeight}"
4189
4788
  rx="${radius}"
4190
4789
  fill="${bgColor}"
4191
4790
  stroke="${borderColor}"
4192
4791
  stroke-width="1"/>
4193
- <text x="${pos.x + buttonWidth / 2}" y="${pos.y + buttonHeight / 2 + fontSize * 0.35}"
4194
- font-family="system-ui, -apple-system, sans-serif"
4792
+ <text x="${pos.x + buttonWidth / 2}" y="${buttonY + buttonHeight / 2 + fontSize * 0.35}"
4793
+ font-family="Arial, Helvetica, sans-serif"
4195
4794
  font-size="${fontSize}"
4196
4795
  font-weight="${fontWeight}"
4197
4796
  fill="${textColor}"
@@ -4201,17 +4800,18 @@ var SVGRenderer = class {
4201
4800
  renderLink(node, pos) {
4202
4801
  const text = String(node.props.text || "Link");
4203
4802
  const variant = String(node.props.variant || "primary");
4803
+ const size = String(node.props.size || "md");
4804
+ const density = this.ir.project.style.density || "normal";
4204
4805
  const fontSize = this.tokens.button.fontSize;
4205
4806
  const fontWeight = this.tokens.button.fontWeight;
4206
4807
  const paddingX = this.tokens.button.paddingX;
4207
- const paddingY = this.tokens.button.paddingY;
4208
4808
  const linkColor = this.resolveVariantColor(variant, this.renderTheme.primary);
4209
4809
  const idealTextWidth = this.estimateTextWidth(text, fontSize);
4210
4810
  const linkWidth = this.clampControlWidth(
4211
4811
  Math.max(Math.ceil(idealTextWidth + paddingX * 2), 60),
4212
4812
  pos.width
4213
4813
  );
4214
- const linkHeight = fontSize + paddingY * 2;
4814
+ const linkHeight = Math.max(16, Math.min(resolveActionControlHeight(size, density), pos.height));
4215
4815
  const availableTextWidth = Math.max(0, linkWidth - paddingX * 2);
4216
4816
  const visibleText = this.truncateTextToWidth(text, availableTextWidth, fontSize);
4217
4817
  const visibleTextWidth = Math.min(
@@ -4222,7 +4822,7 @@ var SVGRenderer = class {
4222
4822
  const underlineY = centerY + 3;
4223
4823
  return `<g${this.getDataNodeId(node)}>
4224
4824
  <text x="${pos.x + linkWidth / 2}" y="${centerY}"
4225
- font-family="system-ui, -apple-system, sans-serif"
4825
+ font-family="Arial, Helvetica, sans-serif"
4226
4826
  font-size="${fontSize}"
4227
4827
  font-weight="${fontWeight}"
4228
4828
  fill="${linkColor}"
@@ -4244,7 +4844,7 @@ var SVGRenderer = class {
4244
4844
  const controlHeight = Math.max(16, pos.height - labelOffset);
4245
4845
  return `<g${this.getDataNodeId(node)}>
4246
4846
  ${label ? `<text x="${pos.x + paddingX}" y="${this.getControlLabelBaselineY(pos.y)}"
4247
- font-family="system-ui, -apple-system, sans-serif"
4847
+ font-family="Arial, Helvetica, sans-serif"
4248
4848
  font-size="12"
4249
4849
  fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>` : ""}
4250
4850
  <rect x="${pos.x}" y="${controlY}"
@@ -4254,7 +4854,7 @@ var SVGRenderer = class {
4254
4854
  stroke="${this.renderTheme.border}"
4255
4855
  stroke-width="1"/>
4256
4856
  <text x="${pos.x + paddingX}" y="${controlY + controlHeight / 2 + 5}"
4257
- font-family="system-ui, -apple-system, sans-serif"
4857
+ font-family="Arial, Helvetica, sans-serif"
4258
4858
  font-size="${fontSize}"
4259
4859
  fill="${this.renderTheme.textMuted}">${this.escapeXml(placeholder)}</text>
4260
4860
  </g>`;
@@ -4264,25 +4864,42 @@ var SVGRenderer = class {
4264
4864
  const subtitle = String(node.props.subtitle || "");
4265
4865
  const actions = String(node.props.actions || "");
4266
4866
  const user = String(node.props.user || "");
4267
- const accentColor = this.resolveAccentColor();
4867
+ const variant = String(node.props.variant || "default");
4868
+ const accentColor = variant === "default" ? this.resolveAccentColor() : this.resolveVariantColor(variant, this.resolveAccentColor());
4869
+ const showBorder = this.parseBooleanProp(node.props.border, false);
4870
+ const showBackground = this.parseBooleanProp(node.props.background ?? node.props.backround, false);
4871
+ const radiusMap = {
4872
+ none: 0,
4873
+ sm: 4,
4874
+ md: this.tokens.card.radius,
4875
+ lg: 12,
4876
+ xl: 16
4877
+ };
4878
+ const topbarRadius = radiusMap[String(node.props.radius || "md")] ?? this.tokens.card.radius;
4268
4879
  const topbar = this.calculateTopbarLayout(node, pos, title, subtitle, actions, user);
4269
- let svg = `<g${this.getDataNodeId(node)}>
4880
+ let svg = `<g${this.getDataNodeId(node)}>`;
4881
+ if (showBorder || showBackground) {
4882
+ const bg = showBackground ? this.renderTheme.cardBg : "none";
4883
+ const stroke = showBorder ? this.renderTheme.border : "none";
4884
+ svg += `
4270
4885
  <rect x="${pos.x}" y="${pos.y}"
4271
4886
  width="${pos.width}" height="${pos.height}"
4272
- fill="${this.renderTheme.cardBg}"
4273
- stroke="${this.renderTheme.border}"
4274
- stroke-width="1"/>
4275
-
4887
+ rx="${topbarRadius}"
4888
+ fill="${bg}"
4889
+ stroke="${stroke}"
4890
+ stroke-width="1"/>`;
4891
+ }
4892
+ svg += `
4276
4893
  <!-- Title -->
4277
4894
  <text x="${topbar.textX}" y="${topbar.titleY}"
4278
- font-family="system-ui, -apple-system, sans-serif"
4895
+ font-family="Arial, Helvetica, sans-serif"
4279
4896
  font-size="18"
4280
4897
  font-weight="600"
4281
4898
  fill="${this.renderTheme.text}">${this.escapeXml(topbar.visibleTitle)}</text>`;
4282
4899
  if (topbar.hasSubtitle) {
4283
4900
  svg += `
4284
4901
  <text x="${topbar.textX}" y="${topbar.subtitleY}"
4285
- font-family="system-ui, -apple-system, sans-serif"
4902
+ font-family="Arial, Helvetica, sans-serif"
4286
4903
  font-size="13"
4287
4904
  fill="${this.renderTheme.textMuted}">${this.escapeXml(topbar.visibleSubtitle)}</text>`;
4288
4905
  }
@@ -4310,7 +4927,7 @@ var SVGRenderer = class {
4310
4927
  fill="${accentColor}"
4311
4928
  stroke="none"/>
4312
4929
  <text x="${action.x + action.width / 2}" y="${action.y + action.height / 2 + 4}"
4313
- font-family="system-ui, -apple-system, sans-serif"
4930
+ font-family="Arial, Helvetica, sans-serif"
4314
4931
  font-size="12"
4315
4932
  font-weight="600"
4316
4933
  fill="white"
@@ -4326,7 +4943,7 @@ var SVGRenderer = class {
4326
4943
  stroke="${this.renderTheme.border}"
4327
4944
  stroke-width="1"/>
4328
4945
  <text x="${topbar.userBadge.x + topbar.userBadge.width / 2}" y="${topbar.userBadge.y + topbar.userBadge.height / 2 + 4}"
4329
- font-family="system-ui, -apple-system, sans-serif"
4946
+ font-family="Arial, Helvetica, sans-serif"
4330
4947
  font-size="12"
4331
4948
  fill="${this.renderTheme.text}"
4332
4949
  text-anchor="middle">${this.escapeXml(topbar.userBadge.label)}</text>`;
@@ -4389,77 +5006,165 @@ var SVGRenderer = class {
4389
5006
  </g>`;
4390
5007
  output.push(svg);
4391
5008
  }
5009
+ renderSplitDecoration(node, pos, output) {
5010
+ if (node.kind !== "container") return;
5011
+ const gap = this.resolveSpacing(node.style.gap);
5012
+ const leftParam = Number(node.params.left);
5013
+ const rightParam = Number(node.params.right);
5014
+ const hasLeft = node.params.left !== void 0;
5015
+ const hasRight = node.params.right !== void 0 && node.params.left === void 0;
5016
+ const fixedLeftWidth = Number.isFinite(leftParam) && leftParam > 0 ? leftParam : 250;
5017
+ const fixedRightWidth = Number.isFinite(rightParam) && rightParam > 0 ? rightParam : 250;
5018
+ const backgroundKey = String(node.style.background || "").trim();
5019
+ const showBorder = this.parseBooleanProp(node.params.border, false);
5020
+ if (backgroundKey) {
5021
+ const fill = this.colorResolver.resolveColor(backgroundKey, this.renderTheme.cardBg);
5022
+ if (hasRight) {
5023
+ const panelX = pos.x + Math.max(0, pos.width - fixedRightWidth);
5024
+ output.push(`<g>
5025
+ <rect x="${panelX}" y="${pos.y}" width="${Math.max(1, fixedRightWidth)}" height="${pos.height}" fill="${fill}" stroke="none"/>
5026
+ </g>`);
5027
+ } else if (hasLeft || !hasRight) {
5028
+ output.push(`<g>
5029
+ <rect x="${pos.x}" y="${pos.y}" width="${Math.max(1, fixedLeftWidth)}" height="${pos.height}" fill="${fill}" stroke="none"/>
5030
+ </g>`);
5031
+ }
5032
+ }
5033
+ if (showBorder) {
5034
+ const dividerX = hasRight ? pos.x + Math.max(0, pos.width - fixedRightWidth - gap / 2) : pos.x + Math.max(0, fixedLeftWidth + gap / 2);
5035
+ output.push(`<g>
5036
+ <line x1="${dividerX}" y1="${pos.y}" x2="${dividerX}" y2="${pos.y + pos.height}" stroke="${this.renderTheme.border}" stroke-width="1"/>
5037
+ </g>`);
5038
+ }
5039
+ }
4392
5040
  renderTable(node, pos) {
4393
5041
  const title = String(node.props.title || "");
4394
5042
  const columnsStr = String(node.props.columns || "Col1,Col2,Col3");
4395
- const columns = columnsStr.split(",").map((c) => c.trim());
5043
+ const columns = columnsStr.split(",").map((c) => c.trim()).filter(Boolean);
4396
5044
  const rowCount = Number(node.props.rows || node.props.rowsMock || 5);
4397
5045
  const mockStr = String(node.props.mock || "");
4398
5046
  const random = this.parseBooleanProp(node.props.random, false);
4399
- const paginationValue = String(node.props.pagination || "false");
4400
- const pagination = paginationValue === "true";
4401
- const pageCount = Number(node.props.pages || 5);
5047
+ const pagination = this.parseBooleanProp(node.props.pagination, false);
5048
+ const parsedPageCount = Number(node.props.pages || 5);
5049
+ const pageCount = Number.isFinite(parsedPageCount) && parsedPageCount > 0 ? Math.floor(parsedPageCount) : 5;
4402
5050
  const paginationAlign = String(node.props.paginationAlign || "right");
5051
+ const actions = String(node.props.actions || "").split(",").map((value) => value.trim()).filter(Boolean);
5052
+ const hasActions = actions.length > 0;
5053
+ const caption = String(node.props.caption || "").trim();
5054
+ const hasCaption = caption.length > 0;
5055
+ const showOuterBorder = this.parseBooleanProp(node.props.border, false);
5056
+ const showOuterBackground = this.parseBooleanProp(
5057
+ node.props.background ?? node.props.backround,
5058
+ false
5059
+ );
5060
+ const showInnerBorder = this.parseBooleanProp(node.props.innerBorder, true);
5061
+ const rawCaptionAlign = String(node.props.captionAlign || "");
5062
+ const captionAlign = rawCaptionAlign === "left" || rawCaptionAlign === "center" || rawCaptionAlign === "right" ? rawCaptionAlign : paginationAlign === "left" ? "right" : "left";
5063
+ const sameFooterAlign = hasCaption && pagination && captionAlign === paginationAlign;
4403
5064
  const mockTypes = mockStr ? mockStr.split(",").map((m) => m.trim()).filter(Boolean) : [];
4404
- while (mockTypes.length < columns.length) {
4405
- const inferred = MockDataGenerator.inferMockTypeFromColumn(columns[mockTypes.length] || "item");
5065
+ const safeColumns = columns.length > 0 ? columns : ["Column"];
5066
+ while (mockTypes.length < safeColumns.length) {
5067
+ const inferred = MockDataGenerator.inferMockTypeFromColumn(safeColumns[mockTypes.length] || "item");
4406
5068
  mockTypes.push(inferred);
4407
5069
  }
4408
5070
  const headerHeight = 44;
4409
5071
  const rowHeight = 36;
4410
- const colWidth = pos.width / columns.length;
5072
+ const actionColumnWidth = hasActions ? Math.max(96, Math.min(180, actions.length * 26 + 28)) : 0;
5073
+ const dataWidth = Math.max(20, pos.width - actionColumnWidth);
5074
+ const dataColWidth = dataWidth / safeColumns.length;
4411
5075
  const mockRows = [];
4412
5076
  for (let rowIdx = 0; rowIdx < rowCount; rowIdx++) {
4413
5077
  const row = {};
4414
- columns.forEach((col, colIdx) => {
5078
+ safeColumns.forEach((col, colIdx) => {
4415
5079
  const mockType = mockTypes[colIdx] || MockDataGenerator.inferMockTypeFromColumn(col) || "item";
4416
5080
  row[col] = MockDataGenerator.getMockValue(mockType, rowIdx, random);
4417
5081
  });
4418
5082
  mockRows.push(row);
4419
5083
  }
4420
- let svg = `<g${this.getDataNodeId(node)}>
5084
+ let svg = `<g${this.getDataNodeId(node)}>`;
5085
+ if (showOuterBorder || showOuterBackground) {
5086
+ const outerFill = showOuterBackground ? this.renderTheme.cardBg : "none";
5087
+ const outerStroke = showOuterBorder ? this.renderTheme.border : "none";
5088
+ svg += `
4421
5089
  <rect x="${pos.x}" y="${pos.y}"
4422
5090
  width="${pos.width}" height="${pos.height}"
4423
5091
  rx="8"
4424
- fill="${this.renderTheme.cardBg}"
4425
- stroke="${this.renderTheme.border}"
5092
+ fill="${outerFill}"
5093
+ stroke="${outerStroke}"
4426
5094
  stroke-width="1"/>`;
5095
+ }
4427
5096
  if (title) {
4428
5097
  svg += `
4429
5098
  <text x="${pos.x + 16}" y="${pos.y + 24}"
4430
- font-family="system-ui, -apple-system, sans-serif"
5099
+ font-family="Arial, Helvetica, sans-serif"
4431
5100
  font-size="13"
4432
5101
  font-weight="600"
4433
5102
  fill="${this.renderTheme.text}">${this.escapeXml(title)}</text>`;
4434
5103
  }
4435
5104
  const headerY = pos.y + (title ? 32 : 0);
4436
- svg += `
5105
+ if (showInnerBorder) {
5106
+ svg += `
4437
5107
  <line x1="${pos.x}" y1="${headerY + headerHeight}" x2="${pos.x + pos.width}" y2="${headerY + headerHeight}"
4438
5108
  stroke="${this.renderTheme.border}" stroke-width="1"/>`;
4439
- columns.forEach((col, i) => {
5109
+ }
5110
+ safeColumns.forEach((col, i) => {
4440
5111
  svg += `
4441
- <text x="${pos.x + i * colWidth + 12}" y="${headerY + 26}"
4442
- font-family="system-ui, -apple-system, sans-serif"
5112
+ <text x="${pos.x + i * dataColWidth + 12}" y="${headerY + 26}"
5113
+ font-family="Arial, Helvetica, sans-serif"
4443
5114
  font-size="11"
4444
5115
  font-weight="600"
4445
5116
  fill="${this.renderTheme.textMuted}">${this.escapeXml(col)}</text>`;
4446
5117
  });
5118
+ if (hasActions && showInnerBorder) {
5119
+ const dividerX = pos.x + dataWidth;
5120
+ svg += `
5121
+ <line x1="${dividerX}" y1="${headerY + headerHeight}" x2="${dividerX}" y2="${headerY + headerHeight + mockRows.length * rowHeight}"
5122
+ stroke="${this.renderTheme.border}" stroke-width="1"/>`;
5123
+ }
4447
5124
  mockRows.forEach((row, rowIdx) => {
4448
5125
  const rowY = headerY + headerHeight + rowIdx * rowHeight;
4449
- svg += `
5126
+ if (showInnerBorder) {
5127
+ svg += `
4450
5128
  <line x1="${pos.x}" y1="${rowY + rowHeight}" x2="${pos.x + pos.width}" y2="${rowY + rowHeight}"
4451
5129
  stroke="${this.renderTheme.border}" stroke-width="0.5"/>`;
4452
- columns.forEach((col, colIdx) => {
5130
+ }
5131
+ safeColumns.forEach((col, colIdx) => {
4453
5132
  const cellValue = row[col] || "";
4454
5133
  svg += `
4455
- <text x="${pos.x + colIdx * colWidth + 12}" y="${rowY + 22}"
4456
- font-family="system-ui, -apple-system, sans-serif"
5134
+ <text x="${pos.x + colIdx * dataColWidth + 12}" y="${rowY + 22}"
5135
+ font-family="Arial, Helvetica, sans-serif"
4457
5136
  font-size="12"
4458
5137
  fill="${this.renderTheme.text}">${this.escapeXml(cellValue)}</text>`;
4459
5138
  });
5139
+ if (hasActions) {
5140
+ const iconSize = 14;
5141
+ const buttonSize = 22;
5142
+ const buttonGap = 6;
5143
+ const actionsWidth = actions.length * buttonSize + Math.max(0, actions.length - 1) * buttonGap;
5144
+ let currentX = pos.x + pos.width - 12 - actionsWidth;
5145
+ const buttonY = rowY + (rowHeight - buttonSize) / 2;
5146
+ actions.forEach((actionIcon) => {
5147
+ const iconSvg = getIcon(actionIcon);
5148
+ const iconX = currentX + (buttonSize - iconSize) / 2;
5149
+ const iconY = buttonY + (buttonSize - iconSize) / 2;
5150
+ svg += `
5151
+ <rect x="${currentX}" y="${buttonY}" width="${buttonSize}" height="${buttonSize}" rx="4"
5152
+ fill="${this.renderTheme.cardBg}" stroke="${showInnerBorder ? this.renderTheme.border : "none"}" stroke-width="1"/>`;
5153
+ if (iconSvg) {
5154
+ svg += `
5155
+ <g transform="translate(${iconX}, ${iconY})">
5156
+ <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">
5157
+ ${this.extractSvgContent(iconSvg)}
5158
+ </svg>
5159
+ </g>`;
5160
+ }
5161
+ currentX += buttonSize + buttonGap;
5162
+ });
5163
+ }
4460
5164
  });
5165
+ const footerTop = headerY + headerHeight + mockRows.length * rowHeight + 16;
4461
5166
  if (pagination) {
4462
- const paginationY = headerY + headerHeight + mockRows.length * rowHeight + 16;
5167
+ const paginationY = sameFooterAlign ? footerTop + 18 + 8 : footerTop;
4463
5168
  const buttonWidth = 40;
4464
5169
  const buttonHeight = 32;
4465
5170
  const gap = 8;
@@ -4472,18 +5177,25 @@ var SVGRenderer = class {
4472
5177
  } else {
4473
5178
  startX = pos.x + pos.width - totalWidth - 16;
4474
5179
  }
5180
+ const previousIcon = getIcon("chevron-left");
4475
5181
  svg += `
4476
5182
  <rect x="${startX}" y="${paginationY}"
4477
5183
  width="${buttonWidth}" height="${buttonHeight}"
4478
5184
  rx="4"
4479
5185
  fill="${this.renderTheme.cardBg}"
4480
5186
  stroke="${this.renderTheme.border}"
4481
- stroke-width="1"/>
4482
- <text x="${startX + buttonWidth / 2}" y="${paginationY + buttonHeight / 2 + 4}"
4483
- font-family="system-ui, -apple-system, sans-serif"
4484
- font-size="14"
4485
- fill="${this.renderTheme.text}"
4486
- text-anchor="middle">&lt;</text>`;
5187
+ stroke-width="1"/>`;
5188
+ if (previousIcon) {
5189
+ const iconSize = 14;
5190
+ const iconX = startX + (buttonWidth - iconSize) / 2;
5191
+ const iconY = paginationY + (buttonHeight - iconSize) / 2;
5192
+ svg += `
5193
+ <g transform="translate(${iconX}, ${iconY})">
5194
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${this.resolveTextColor()}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
5195
+ ${this.extractSvgContent(previousIcon)}
5196
+ </svg>
5197
+ </g>`;
5198
+ }
4487
5199
  for (let i = 1; i <= pageCount; i++) {
4488
5200
  const btnX = startX + (buttonWidth + gap) * i;
4489
5201
  const isActive = i === 1;
@@ -4497,24 +5209,49 @@ var SVGRenderer = class {
4497
5209
  stroke="${this.renderTheme.border}"
4498
5210
  stroke-width="1"/>
4499
5211
  <text x="${btnX + buttonWidth / 2}" y="${paginationY + buttonHeight / 2 + 4}"
4500
- font-family="system-ui, -apple-system, sans-serif"
5212
+ font-family="Arial, Helvetica, sans-serif"
4501
5213
  font-size="14"
4502
5214
  fill="${textColor}"
4503
5215
  text-anchor="middle">${i}</text>`;
4504
5216
  }
4505
5217
  const nextX = startX + (buttonWidth + gap) * (pageCount + 1);
5218
+ const nextIcon = getIcon("chevron-right");
4506
5219
  svg += `
4507
5220
  <rect x="${nextX}" y="${paginationY}"
4508
5221
  width="${buttonWidth}" height="${buttonHeight}"
4509
5222
  rx="4"
4510
5223
  fill="${this.renderTheme.cardBg}"
4511
5224
  stroke="${this.renderTheme.border}"
4512
- stroke-width="1"/>
4513
- <text x="${nextX + buttonWidth / 2}" y="${paginationY + buttonHeight / 2 + 4}"
4514
- font-family="system-ui, -apple-system, sans-serif"
4515
- font-size="14"
4516
- fill="${this.renderTheme.text}"
4517
- text-anchor="middle">&gt;</text>`;
5225
+ stroke-width="1"/>`;
5226
+ if (nextIcon) {
5227
+ const iconSize = 14;
5228
+ const iconX = nextX + (buttonWidth - iconSize) / 2;
5229
+ const iconY = paginationY + (buttonHeight - iconSize) / 2;
5230
+ svg += `
5231
+ <g transform="translate(${iconX}, ${iconY})">
5232
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${this.resolveTextColor()}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
5233
+ ${this.extractSvgContent(nextIcon)}
5234
+ </svg>
5235
+ </g>`;
5236
+ }
5237
+ }
5238
+ if (hasCaption) {
5239
+ const captionY = sameFooterAlign ? footerTop + 12 : footerTop + (pagination ? 21 : 12);
5240
+ let captionX = pos.x + 16;
5241
+ let textAnchor = "start";
5242
+ if (captionAlign === "center") {
5243
+ captionX = pos.x + pos.width / 2;
5244
+ textAnchor = "middle";
5245
+ } else if (captionAlign === "right") {
5246
+ captionX = pos.x + pos.width - 16;
5247
+ textAnchor = "end";
5248
+ }
5249
+ svg += `
5250
+ <text x="${captionX}" y="${captionY}"
5251
+ font-family="Arial, Helvetica, sans-serif"
5252
+ font-size="12"
5253
+ fill="${this.hexToRgba(this.resolveTextColor(), 0.75)}"
5254
+ text-anchor="${textAnchor}">${this.escapeXml(caption)}</text>`;
4518
5255
  }
4519
5256
  svg += "\n </g>";
4520
5257
  return svg;
@@ -4629,7 +5366,7 @@ var SVGRenderer = class {
4629
5366
  // TEXT/CONTENT COMPONENTS
4630
5367
  // ============================================================================
4631
5368
  renderText(node, pos) {
4632
- const text = String(node.props.content || "Text content");
5369
+ const text = String(node.props.text || "Text content");
4633
5370
  const fontSize = this.tokens.text.fontSize;
4634
5371
  const lineHeightPx = Math.ceil(fontSize * this.tokens.text.lineHeight);
4635
5372
  const lines = this.wrapTextToLines(text, pos.width, fontSize);
@@ -4639,7 +5376,7 @@ var SVGRenderer = class {
4639
5376
  ).join("");
4640
5377
  return `<g${this.getDataNodeId(node)}>
4641
5378
  <text x="${pos.x}" y="${firstLineY}"
4642
- font-family="system-ui, -apple-system, sans-serif"
5379
+ font-family="Arial, Helvetica, sans-serif"
4643
5380
  font-size="${fontSize}"
4644
5381
  fill="${this.renderTheme.text}">${tspans}</text>
4645
5382
  </g>`;
@@ -4648,7 +5385,7 @@ var SVGRenderer = class {
4648
5385
  const text = String(node.props.text || "Label");
4649
5386
  return `<g${this.getDataNodeId(node)}>
4650
5387
  <text x="${pos.x}" y="${pos.y + 12}"
4651
- font-family="system-ui, -apple-system, sans-serif"
5388
+ font-family="Arial, Helvetica, sans-serif"
4652
5389
  font-size="12"
4653
5390
  fill="${this.renderTheme.textMuted}">${this.escapeXml(text)}</text>
4654
5391
  </g>`;
@@ -4682,7 +5419,7 @@ var SVGRenderer = class {
4682
5419
  const placeholderY = controlY + fontSize + 6;
4683
5420
  return `<g${this.getDataNodeId(node)}>
4684
5421
  ${label ? `<text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
4685
- font-family="system-ui, -apple-system, sans-serif"
5422
+ font-family="Arial, Helvetica, sans-serif"
4686
5423
  font-size="12"
4687
5424
  fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>` : ""}
4688
5425
  <rect x="${pos.x}" y="${controlY}"
@@ -4692,7 +5429,7 @@ var SVGRenderer = class {
4692
5429
  stroke="${this.renderTheme.border}"
4693
5430
  stroke-width="1"/>
4694
5431
  <text x="${pos.x + paddingX}" y="${placeholderY}"
4695
- font-family="system-ui, -apple-system, sans-serif"
5432
+ font-family="Arial, Helvetica, sans-serif"
4696
5433
  font-size="${fontSize}"
4697
5434
  fill="${this.renderTheme.textMuted}">${this.escapeXml(placeholder)}</text>
4698
5435
  </g>`;
@@ -4706,7 +5443,7 @@ var SVGRenderer = class {
4706
5443
  const centerY = controlY + controlHeight / 2 + 5;
4707
5444
  return `<g${this.getDataNodeId(node)}>
4708
5445
  ${label ? `<text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
4709
- font-family="system-ui, -apple-system, sans-serif"
5446
+ font-family="Arial, Helvetica, sans-serif"
4710
5447
  font-size="12"
4711
5448
  fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>` : ""}
4712
5449
  <rect x="${pos.x}" y="${controlY}"
@@ -4716,11 +5453,11 @@ var SVGRenderer = class {
4716
5453
  stroke="${this.renderTheme.border}"
4717
5454
  stroke-width="1"/>
4718
5455
  <text x="${pos.x + 12}" y="${centerY}"
4719
- font-family="system-ui, -apple-system, sans-serif"
5456
+ font-family="Arial, Helvetica, sans-serif"
4720
5457
  font-size="14"
4721
5458
  fill="${this.renderTheme.textMuted}">${this.escapeXml(placeholder)}</text>
4722
5459
  <text x="${pos.x + pos.width - 20}" y="${centerY}"
4723
- font-family="system-ui, -apple-system, sans-serif"
5460
+ font-family="Arial, Helvetica, sans-serif"
4724
5461
  font-size="16"
4725
5462
  fill="${this.renderTheme.textMuted}">\u25BC</text>
4726
5463
  </g>`;
@@ -4739,12 +5476,12 @@ var SVGRenderer = class {
4739
5476
  stroke="${this.renderTheme.border}"
4740
5477
  stroke-width="1"/>
4741
5478
  ${checked ? `<text x="${pos.x + checkboxSize / 2}" y="${checkboxY + 14}"
4742
- font-family="system-ui, -apple-system, sans-serif"
5479
+ font-family="Arial, Helvetica, sans-serif"
4743
5480
  font-size="12"
4744
5481
  fill="white"
4745
5482
  text-anchor="middle">\u2713</text>` : ""}
4746
5483
  <text x="${pos.x + checkboxSize + 12}" y="${pos.y + pos.height / 2 + 5}"
4747
- font-family="system-ui, -apple-system, sans-serif"
5484
+ font-family="Arial, Helvetica, sans-serif"
4748
5485
  font-size="14"
4749
5486
  fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>
4750
5487
  </g>`;
@@ -4765,7 +5502,7 @@ var SVGRenderer = class {
4765
5502
  r="${radioSize / 3.5}"
4766
5503
  fill="${controlColor}"/>` : ""}
4767
5504
  <text x="${pos.x + radioSize + 12}" y="${pos.y + pos.height / 2 + 5}"
4768
- font-family="system-ui, -apple-system, sans-serif"
5505
+ font-family="Arial, Helvetica, sans-serif"
4769
5506
  font-size="14"
4770
5507
  fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>
4771
5508
  </g>`;
@@ -4787,7 +5524,7 @@ var SVGRenderer = class {
4787
5524
  r="8"
4788
5525
  fill="white"/>
4789
5526
  <text x="${pos.x + toggleWidth + 12}" y="${pos.y + pos.height / 2 + 5}"
4790
- font-family="system-ui, -apple-system, sans-serif"
5527
+ font-family="Arial, Helvetica, sans-serif"
4791
5528
  font-size="14"
4792
5529
  fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>
4793
5530
  </g>`;
@@ -4817,7 +5554,7 @@ var SVGRenderer = class {
4817
5554
  stroke-width="1"/>
4818
5555
  <!-- Title -->
4819
5556
  <text x="${pos.x + padding}" y="${pos.y + padding + 8}"
4820
- font-family="system-ui, -apple-system, sans-serif"
5557
+ font-family="Arial, Helvetica, sans-serif"
4821
5558
  font-size="14"
4822
5559
  font-weight="600"
4823
5560
  fill="${this.renderTheme.text}">${this.escapeXml(title)}</text>
@@ -4833,7 +5570,7 @@ var SVGRenderer = class {
4833
5570
  fill="${isActive ? this.renderTheme.primary : "transparent"}"
4834
5571
  stroke="none"/>
4835
5572
  <text x="${pos.x + 16}" y="${itemY + 22}"
4836
- font-family="system-ui, -apple-system, sans-serif"
5573
+ font-family="Arial, Helvetica, sans-serif"
4837
5574
  font-size="13"
4838
5575
  fill="${isActive ? "white" : this.renderTheme.textMuted}">${this.escapeXml(item)}</text>`;
4839
5576
  });
@@ -4859,7 +5596,7 @@ var SVGRenderer = class {
4859
5596
  stroke="${isActive ? "none" : this.renderTheme.border}"
4860
5597
  stroke-width="1"/>
4861
5598
  <text x="${tabX + tabWidth / 2}" y="${pos.y + 28}"
4862
- font-family="system-ui, -apple-system, sans-serif"
5599
+ font-family="Arial, Helvetica, sans-serif"
4863
5600
  font-size="13"
4864
5601
  font-weight="${isActive ? "600" : "500"}"
4865
5602
  fill="${isActive ? "white" : this.renderTheme.text}"
@@ -4922,12 +5659,12 @@ var SVGRenderer = class {
4922
5659
  rx="6"
4923
5660
  fill="${bgColor}"/>
4924
5661
  ${hasTitle ? `<text x="${contentX}" y="${titleStartY}"
4925
- font-family="system-ui, -apple-system, sans-serif"
5662
+ font-family="Arial, Helvetica, sans-serif"
4926
5663
  font-size="${fontSize}"
4927
5664
  font-weight="700"
4928
5665
  fill="${bgColor}">${titleTspans}</text>` : ""}
4929
5666
  <text x="${contentX}" y="${textStartY}"
4930
- font-family="system-ui, -apple-system, sans-serif"
5667
+ font-family="Arial, Helvetica, sans-serif"
4931
5668
  font-size="${fontSize}"
4932
5669
  fill="${bgColor}">${textTspans}</text>
4933
5670
  </g>`;
@@ -4948,7 +5685,7 @@ var SVGRenderer = class {
4948
5685
  fill="${bgColor}"
4949
5686
  stroke="none"/>
4950
5687
  <text x="${pos.x + pos.width / 2}" y="${pos.y + pos.height / 2 + 4}"
4951
- font-family="system-ui, -apple-system, sans-serif"
5688
+ font-family="Arial, Helvetica, sans-serif"
4952
5689
  font-size="${fontSize}"
4953
5690
  font-weight="600"
4954
5691
  fill="${textColor}"
@@ -4987,20 +5724,20 @@ var SVGRenderer = class {
4987
5724
  stroke-width="1"/>
4988
5725
 
4989
5726
  <text x="${modalX + padding}" y="${modalY + padding + 16}"
4990
- font-family="system-ui, -apple-system, sans-serif"
5727
+ font-family="Arial, Helvetica, sans-serif"
4991
5728
  font-size="16"
4992
5729
  font-weight="600"
4993
5730
  fill="${this.renderTheme.text}">${this.escapeXml(title)}</text>
4994
5731
 
4995
5732
  <!-- Close button -->
4996
5733
  <text x="${modalX + pos.width - 16}" y="${modalY + padding + 12}"
4997
- font-family="system-ui, -apple-system, sans-serif"
5734
+ font-family="Arial, Helvetica, sans-serif"
4998
5735
  font-size="18"
4999
5736
  fill="${this.renderTheme.textMuted}">\u2715</text>
5000
5737
 
5001
5738
  <!-- Content placeholder -->
5002
5739
  <text x="${modalX + pos.width / 2}" y="${modalY + headerHeight + (pos.height - headerHeight) / 2}"
5003
- font-family="system-ui, -apple-system, sans-serif"
5740
+ font-family="Arial, Helvetica, sans-serif"
5004
5741
  font-size="13"
5005
5742
  fill="${this.renderTheme.textMuted}"
5006
5743
  text-anchor="middle">Modal content</text>
@@ -5033,7 +5770,7 @@ var SVGRenderer = class {
5033
5770
  if (title) {
5034
5771
  svg += `
5035
5772
  <text x="${pos.x + padding}" y="${pos.y + 26}"
5036
- font-family="system-ui, -apple-system, sans-serif"
5773
+ font-family="Arial, Helvetica, sans-serif"
5037
5774
  font-size="13"
5038
5775
  font-weight="600"
5039
5776
  fill="${this.renderTheme.text}">${this.escapeXml(title)}</text>
@@ -5049,7 +5786,7 @@ var SVGRenderer = class {
5049
5786
  stroke="${this.renderTheme.border}"
5050
5787
  stroke-width="0.5"/>
5051
5788
  <text x="${pos.x + padding}" y="${itemY + 24}"
5052
- font-family="system-ui, -apple-system, sans-serif"
5789
+ font-family="Arial, Helvetica, sans-serif"
5053
5790
  font-size="13"
5054
5791
  fill="${this.renderTheme.text}">${this.escapeXml(item)}</text>`;
5055
5792
  }
@@ -5067,7 +5804,7 @@ var SVGRenderer = class {
5067
5804
  stroke-width="1"
5068
5805
  stroke-dasharray="4 4"/>
5069
5806
  <text x="${pos.x + pos.width / 2}" y="${pos.y + pos.height / 2}"
5070
- font-family="system-ui, -apple-system, sans-serif"
5807
+ font-family="Arial, Helvetica, sans-serif"
5071
5808
  font-size="12"
5072
5809
  fill="${this.renderTheme.textMuted}"
5073
5810
  text-anchor="middle">${node.componentType}</text>
@@ -5115,14 +5852,14 @@ var SVGRenderer = class {
5115
5852
 
5116
5853
  <!-- Title -->
5117
5854
  <text x="${innerX}" y="${titleY}"
5118
- font-family="system-ui, -apple-system, sans-serif"
5855
+ font-family="Arial, Helvetica, sans-serif"
5119
5856
  font-size="${titleSize}"
5120
5857
  font-weight="500"
5121
5858
  fill="${this.renderTheme.textMuted}">${this.escapeXml(visibleTitle)}</text>
5122
5859
 
5123
5860
  <!-- Value (Large) -->
5124
5861
  <text x="${innerX}" y="${valueY}"
5125
- font-family="system-ui, -apple-system, sans-serif"
5862
+ font-family="Arial, Helvetica, sans-serif"
5126
5863
  font-size="${valueSize}"
5127
5864
  font-weight="700"
5128
5865
  fill="${accentColor}">${this.escapeXml(value)}</text>`;
@@ -5145,7 +5882,7 @@ var SVGRenderer = class {
5145
5882
  svg += `
5146
5883
  <!-- Caption -->
5147
5884
  <text x="${innerX}" y="${captionY}"
5148
- font-family="system-ui, -apple-system, sans-serif"
5885
+ font-family="Arial, Helvetica, sans-serif"
5149
5886
  font-size="${captionSize}"
5150
5887
  fill="${this.renderTheme.textMuted}">${this.escapeXml(visibleCaption)}</text>`;
5151
5888
  }
@@ -5274,7 +6011,7 @@ var SVGRenderer = class {
5274
6011
  const fontWeight = isLast ? "500" : "400";
5275
6012
  svg += `
5276
6013
  <text x="${currentX}" y="${pos.y + pos.height / 2 + 4}"
5277
- font-family="system-ui, -apple-system, sans-serif"
6014
+ font-family="Arial, Helvetica, sans-serif"
5278
6015
  font-size="${fontSize}"
5279
6016
  font-weight="${fontWeight}"
5280
6017
  fill="${textColor}">${this.escapeXml(item)}</text>`;
@@ -5283,7 +6020,7 @@ var SVGRenderer = class {
5283
6020
  if (!isLast) {
5284
6021
  svg += `
5285
6022
  <text x="${currentX + 4}" y="${pos.y + pos.height / 2 + 4}"
5286
- font-family="system-ui, -apple-system, sans-serif"
6023
+ font-family="Arial, Helvetica, sans-serif"
5287
6024
  font-size="${fontSize}"
5288
6025
  fill="${this.renderTheme.textMuted}">${this.escapeXml(separator)}</text>`;
5289
6026
  currentX += separatorWidth;
@@ -5306,7 +6043,7 @@ var SVGRenderer = class {
5306
6043
  const itemY = pos.y + index * itemHeight;
5307
6044
  const isActive = index === activeIndex;
5308
6045
  const bgColor = isActive ? this.hexToRgba(accentColor, 0.15) : "transparent";
5309
- const textColor = isActive ? this.hexToRgba(accentColor, 0.9) : "rgba(30, 41, 59, 0.75)";
6046
+ const textColor = isActive ? this.hexToRgba(accentColor, 0.9) : this.hexToRgba(this.resolveTextColor(), 0.75);
5310
6047
  const fontWeight = isActive ? "500" : "400";
5311
6048
  if (isActive) {
5312
6049
  svg += `
@@ -5323,7 +6060,7 @@ var SVGRenderer = class {
5323
6060
  const iconY = itemY + (itemHeight - iconSize) / 2;
5324
6061
  svg += `
5325
6062
  <g transform="translate(${currentX}, ${iconY})">
5326
- <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="rgba(30, 41, 59, 0.6)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
6063
+ <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">
5327
6064
  ${this.extractSvgContent(iconSvg)}
5328
6065
  </svg>
5329
6066
  </g>`;
@@ -5332,7 +6069,7 @@ var SVGRenderer = class {
5332
6069
  }
5333
6070
  svg += `
5334
6071
  <text x="${currentX}" y="${itemY + itemHeight / 2 + 5}"
5335
- font-family="system-ui, -apple-system, sans-serif"
6072
+ font-family="Arial, Helvetica, sans-serif"
5336
6073
  font-size="${fontSize}"
5337
6074
  font-weight="${fontWeight}"
5338
6075
  fill="${textColor}">${this.escapeXml(item)}</text>`;
@@ -5341,18 +6078,19 @@ var SVGRenderer = class {
5341
6078
  return svg;
5342
6079
  }
5343
6080
  renderIcon(node, pos) {
5344
- const iconType = String(node.props.type || "help-circle");
6081
+ const iconType = String(node.props.icon || "help-circle");
5345
6082
  const size = String(node.props.size || "md");
6083
+ const variant = String(node.props.variant || "default");
5346
6084
  const iconSvg = getIcon(iconType);
6085
+ const iconColor = variant === "default" ? this.hexToRgba(this.resolveTextColor(), 0.75) : this.resolveVariantColor(variant, this.resolveTextColor());
5347
6086
  if (!iconSvg) {
5348
6087
  return `<g${this.getDataNodeId(node)}>
5349
6088
  <!-- Icon not found: ${iconType} -->
5350
- <circle cx="${pos.x + pos.width / 2}" cy="${pos.y + pos.height / 2}" r="${Math.min(pos.width, pos.height) / 2 - 2}" fill="none" stroke="rgba(100, 116, 139, 0.4)" stroke-width="1"/>
5351
- <text x="${pos.x + pos.width / 2}" y="${pos.y + pos.height / 2 + 4}" font-family="system-ui, -apple-system, sans-serif" font-size="12" fill="rgba(100, 116, 139, 0.6)" text-anchor="middle">?</text>
6089
+ <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"/>
6090
+ <text x="${pos.x + pos.width / 2}" y="${pos.y + pos.height / 2 + 4}" font-family="Arial, Helvetica, sans-serif" font-size="12" fill="${this.hexToRgba(this.resolveMutedColor(), 0.7)}" text-anchor="middle">?</text>
5352
6091
  </g>`;
5353
6092
  }
5354
6093
  const iconSize = this.getIconSize(size);
5355
- const iconColor = "rgba(30, 41, 59, 0.75)";
5356
6094
  const offsetX = pos.x + (pos.width - iconSize) / 2;
5357
6095
  const offsetY = pos.y + (pos.height - iconSize) / 2;
5358
6096
  const wrappedSvg = `<g${this.getDataNodeId(node)} transform="translate(${offsetX}, ${offsetY})">
@@ -5367,23 +6105,31 @@ var SVGRenderer = class {
5367
6105
  const variant = String(node.props.variant || "default");
5368
6106
  const size = String(node.props.size || "md");
5369
6107
  const disabled = String(node.props.disabled || "false") === "true";
6108
+ const density = this.ir.project.style.density || "normal";
6109
+ const labelOffset = this.parseBooleanProp(node.props.labelSpace, false) ? 18 : 0;
6110
+ const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
5370
6111
  const semanticBase = this.getSemanticVariantColor(variant);
5371
6112
  const hasExplicitVariantColor = semanticBase !== void 0 || this.colorResolver.hasColor(variant);
5372
6113
  const resolvedBase = this.resolveVariantColor(variant, this.renderTheme.primary);
5373
6114
  const bgColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.85) : "rgba(226, 232, 240, 0.9)";
5374
- const iconColor = hasExplicitVariantColor ? "#FFFFFF" : "rgba(30, 41, 59, 0.75)";
6115
+ const iconColor = hasExplicitVariantColor ? "#FFFFFF" : this.hexToRgba(this.resolveTextColor(), 0.75);
5375
6116
  const borderColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.7) : "rgba(100, 116, 139, 0.4)";
5376
6117
  const opacity = disabled ? "0.5" : "1";
5377
6118
  const iconSvg = getIcon(iconName);
5378
- const buttonSize = this.getIconButtonSize(size);
6119
+ const buttonSize = Math.max(
6120
+ 16,
6121
+ Math.min(resolveActionControlHeight(size, density), pos.height - labelOffset)
6122
+ );
6123
+ const buttonWidth = buttonSize + extraPadding * 2;
5379
6124
  const radius = 6;
6125
+ const buttonY = pos.y + labelOffset;
5380
6126
  let svg = `<g${this.getDataNodeId(node)} opacity="${opacity}">
5381
6127
  <!-- IconButton background -->
5382
- <rect x="${pos.x}" y="${pos.y}" width="${buttonSize}" height="${buttonSize}" rx="${radius}" fill="${bgColor}" stroke="${borderColor}" stroke-width="1"/>`;
6128
+ <rect x="${pos.x}" y="${buttonY}" width="${buttonWidth}" height="${buttonSize}" rx="${radius}" fill="${bgColor}" stroke="${borderColor}" stroke-width="1"/>`;
5383
6129
  if (iconSvg) {
5384
6130
  const iconSize = buttonSize * 0.6;
5385
- const offsetX = pos.x + (buttonSize - iconSize) / 2;
5386
- const offsetY = pos.y + (buttonSize - iconSize) / 2;
6131
+ const offsetX = pos.x + (buttonWidth - iconSize) / 2;
6132
+ const offsetY = buttonY + (buttonSize - iconSize) / 2;
5387
6133
  svg += `
5388
6134
  <!-- Icon -->
5389
6135
  <g transform="translate(${offsetX}, ${offsetY})">
@@ -5416,6 +6162,14 @@ var SVGRenderer = class {
5416
6162
  resolveChartColor() {
5417
6163
  return this.colorResolver.resolveColor("chart", this.renderTheme.primary);
5418
6164
  }
6165
+ resolveTextColor() {
6166
+ const fallback = this.options.theme === "dark" ? "#FFFFFF" : "#000000";
6167
+ return this.colorResolver.resolveColor("text", fallback);
6168
+ }
6169
+ resolveMutedColor() {
6170
+ const fallback = this.options.theme === "dark" ? "#94A3B8" : "#64748B";
6171
+ return this.colorResolver.resolveColor("muted", fallback);
6172
+ }
5419
6173
  getSemanticVariantColor(variant) {
5420
6174
  const semantic = {
5421
6175
  primary: this.renderTheme.primary,
@@ -5742,21 +6496,28 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
5742
6496
  renderButton(node, pos) {
5743
6497
  const text = String(node.props.text || "Button");
5744
6498
  const variant = String(node.props.variant || "default");
6499
+ const size = String(node.props.size || "md");
6500
+ const density = this.ir.project.style.density || "normal";
6501
+ const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
6502
+ const labelOffset = this.parseBooleanProp(node.props.labelSpace, false) ? 18 : 0;
5745
6503
  const fullWidth = this.shouldButtonFillAvailableWidth(node);
5746
6504
  const radius = this.tokens.button.radius;
5747
6505
  const fontSize = this.tokens.button.fontSize;
5748
6506
  const paddingX = this.tokens.button.paddingX;
5749
- const paddingY = this.tokens.button.paddingY;
6507
+ const buttonHeight = Math.max(
6508
+ 16,
6509
+ Math.min(resolveActionControlHeight(size, density), pos.height - labelOffset)
6510
+ );
6511
+ const buttonY = pos.y + labelOffset;
5750
6512
  const textWidth = text.length * fontSize * 0.6;
5751
- const buttonWidth = fullWidth ? Math.max(1, pos.width) : this.clampControlWidth(Math.max(textWidth + paddingX * 2, 60), pos.width);
5752
- const buttonHeight = fontSize + paddingY * 2;
6513
+ const buttonWidth = fullWidth ? Math.max(1, pos.width) : this.clampControlWidth(Math.max(textWidth + (paddingX + extraPadding) * 2, 60), pos.width);
5753
6514
  const semanticBase = this.getSemanticVariantColor(variant);
5754
6515
  const hasExplicitVariantColor = semanticBase !== void 0 || this.colorResolver.hasColor(variant);
5755
6516
  const resolvedBase = this.resolveVariantColor(variant, this.renderTheme.primary);
5756
6517
  const bgColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.85) : "rgba(226, 232, 240, 0.9)";
5757
6518
  const borderColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.7) : "rgba(100, 116, 139, 0.4)";
5758
6519
  return `<g${this.getDataNodeId(node)}>
5759
- <rect x="${pos.x}" y="${pos.y}"
6520
+ <rect x="${pos.x}" y="${buttonY}"
5760
6521
  width="${buttonWidth}" height="${buttonHeight}"
5761
6522
  rx="${radius}"
5762
6523
  fill="${bgColor}"
@@ -5770,13 +6531,14 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
5770
6531
  renderLink(node, pos) {
5771
6532
  const text = String(node.props.text || "Link");
5772
6533
  const variant = String(node.props.variant || "primary");
6534
+ const size = String(node.props.size || "md");
6535
+ const density = this.ir.project.style.density || "normal";
5773
6536
  const fontSize = this.tokens.button.fontSize;
5774
6537
  const paddingX = this.tokens.button.paddingX;
5775
- const paddingY = this.tokens.button.paddingY;
5776
6538
  const linkColor = this.resolveVariantColor(variant, this.renderTheme.primary);
5777
6539
  const textWidth = this.estimateTextWidth(text, fontSize);
5778
6540
  const linkWidth = this.clampControlWidth(Math.max(textWidth + paddingX * 2, 60), pos.width);
5779
- const linkHeight = fontSize + paddingY * 2;
6541
+ const linkHeight = Math.max(16, Math.min(resolveActionControlHeight(size, density), pos.height));
5780
6542
  const blockHeight = Math.max(8, Math.round(fontSize * 0.75));
5781
6543
  const blockWidth = Math.max(28, Math.min(textWidth, linkWidth - paddingX * 2));
5782
6544
  const blockX = pos.x + (linkWidth - blockWidth) / 2;
@@ -5793,17 +6555,70 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
5793
6555
  stroke-width="1"/>
5794
6556
  </g>`;
5795
6557
  }
6558
+ /**
6559
+ * Render breadcrumbs as skeleton blocks: <rect> / <rect> / <rect accent>
6560
+ */
6561
+ renderBreadcrumbs(node, pos) {
6562
+ const itemsStr = String(node.props.items || "Home");
6563
+ const items = itemsStr.split(",").map((s) => s.trim()).filter(Boolean);
6564
+ const separator = String(node.props.separator || "/");
6565
+ const blockColor = this.renderTheme.border;
6566
+ const charWidth = 6.2;
6567
+ const minBlockWidth = 28;
6568
+ const maxBlockWidth = Math.max(minBlockWidth, Math.floor(pos.width * 0.4));
6569
+ const blockHeight = 12;
6570
+ const blockY = pos.y + (pos.height - blockHeight) / 2;
6571
+ const blockRadius = 4;
6572
+ const blockPaddingX = 10;
6573
+ const itemSpacing = 8;
6574
+ const separatorWidth = 12;
6575
+ const contentRight = pos.x + pos.width;
6576
+ let currentX = pos.x;
6577
+ let svg = `<g${this.getDataNodeId(node)}>`;
6578
+ items.forEach((item, index) => {
6579
+ if (currentX >= contentRight) return;
6580
+ const isLast = index === items.length - 1;
6581
+ const estimatedTextWidth = item.length * charWidth;
6582
+ let blockWidth = Math.max(
6583
+ minBlockWidth,
6584
+ Math.min(maxBlockWidth, Math.ceil(estimatedTextWidth + blockPaddingX * 2))
6585
+ );
6586
+ blockWidth = Math.min(blockWidth, Math.max(0, contentRight - currentX));
6587
+ if (blockWidth < minBlockWidth) return;
6588
+ const fillColor = blockColor;
6589
+ svg += `
6590
+ <rect x="${currentX}" y="${blockY}"
6591
+ width="${blockWidth}" height="${blockHeight}"
6592
+ rx="${blockRadius}"
6593
+ fill="${fillColor}"
6594
+ stroke="none"/>`;
6595
+ currentX += blockWidth + itemSpacing;
6596
+ if (!isLast && currentX + separatorWidth <= contentRight) {
6597
+ svg += `
6598
+ <text x="${currentX + 2}" y="${pos.y + pos.height / 2 + 4}"
6599
+ font-family="Arial, Helvetica, sans-serif"
6600
+ font-size="12"
6601
+ fill="${blockColor}">${this.escapeXml(separator)}</text>`;
6602
+ currentX += separatorWidth;
6603
+ }
6604
+ });
6605
+ svg += "\n </g>";
6606
+ return svg;
6607
+ }
5796
6608
  /**
5797
6609
  * Render heading as gray block
5798
6610
  */
5799
6611
  renderHeading(node, pos) {
5800
6612
  const headingTypography = this.getHeadingTypography(node);
6613
+ const variant = String(node.props.variant || "default");
6614
+ const blockColor = variant === "default" ? this.renderTheme.border : this.hexToRgba(this.resolveVariantColor(variant, this.resolveTextColor()), 0.35);
5801
6615
  return this.renderTextBlock(
5802
6616
  node,
5803
6617
  pos,
5804
6618
  String(node.props.text || "Heading"),
5805
6619
  headingTypography.fontSize,
5806
- headingTypography.lineHeight
6620
+ headingTypography.lineHeight,
6621
+ blockColor
5807
6622
  );
5808
6623
  }
5809
6624
  /**
@@ -5813,7 +6628,7 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
5813
6628
  return this.renderTextBlock(
5814
6629
  node,
5815
6630
  pos,
5816
- String(node.props.content || "Text content"),
6631
+ String(node.props.text || "Text content"),
5817
6632
  this.tokens.text.fontSize,
5818
6633
  this.tokens.text.lineHeight
5819
6634
  );
@@ -6054,18 +6869,42 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
6054
6869
  renderTable(node, pos) {
6055
6870
  const title = String(node.props.title || "");
6056
6871
  const columnsStr = String(node.props.columns || "Col1,Col2,Col3");
6057
- const columns = columnsStr.split(",").map((c) => c.trim());
6872
+ const columns = columnsStr.split(",").map((c) => c.trim()).filter(Boolean);
6058
6873
  const rowCount = Number(node.props.rows || node.props.rowsMock || 5);
6874
+ const actions = String(node.props.actions || "").split(",").map((value) => value.trim()).filter(Boolean);
6875
+ const hasActions = actions.length > 0;
6876
+ const pagination = this.parseBooleanProp(node.props.pagination, false);
6877
+ const parsedPageCount = Number(node.props.pages || 5);
6878
+ const pageCount = Number.isFinite(parsedPageCount) && parsedPageCount > 0 ? Math.floor(parsedPageCount) : 5;
6879
+ const paginationAlign = String(node.props.paginationAlign || "right");
6880
+ const hasCaption = String(node.props.caption || "").trim().length > 0;
6881
+ const showOuterBorder = this.parseBooleanProp(node.props.border, false);
6882
+ const showOuterBackground = this.parseBooleanProp(
6883
+ node.props.background ?? node.props.backround,
6884
+ false
6885
+ );
6886
+ const showInnerBorder = this.parseBooleanProp(node.props.innerBorder, true);
6887
+ const rawCaptionAlign = String(node.props.captionAlign || "");
6888
+ const captionAlign = rawCaptionAlign === "left" || rawCaptionAlign === "center" || rawCaptionAlign === "right" ? rawCaptionAlign : paginationAlign === "left" ? "right" : "left";
6889
+ const sameFooterAlign = hasCaption && pagination && captionAlign === paginationAlign;
6890
+ const safeColumns = columns.length > 0 ? columns : ["Column"];
6059
6891
  const headerHeight = 44;
6060
6892
  const rowHeight = 36;
6061
- const colWidth = pos.width / columns.length;
6062
- let svg = `<g${this.getDataNodeId(node)}>
6893
+ const actionColumnWidth = hasActions ? Math.max(96, Math.min(180, actions.length * 26 + 28)) : 0;
6894
+ const dataWidth = Math.max(20, pos.width - actionColumnWidth);
6895
+ const colWidth = dataWidth / safeColumns.length;
6896
+ let svg = `<g${this.getDataNodeId(node)}>`;
6897
+ if (showOuterBorder || showOuterBackground) {
6898
+ const outerFill = showOuterBackground ? this.renderTheme.cardBg : "none";
6899
+ const outerStroke = showOuterBorder ? this.renderTheme.border : "none";
6900
+ svg += `
6063
6901
  <rect x="${pos.x}" y="${pos.y}"
6064
6902
  width="${pos.width}" height="${pos.height}"
6065
6903
  rx="8"
6066
- fill="${this.renderTheme.cardBg}"
6067
- stroke="${this.renderTheme.border}"
6904
+ fill="${outerFill}"
6905
+ stroke="${outerStroke}"
6068
6906
  stroke-width="1"/>`;
6907
+ }
6069
6908
  if (title) {
6070
6909
  svg += `<rect x="${pos.x + 16}" y="${pos.y + 12}"
6071
6910
  width="100" height="12"
@@ -6073,19 +6912,28 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
6073
6912
  fill="${this.renderTheme.border}"/>`;
6074
6913
  }
6075
6914
  const headerY = pos.y + (title ? 32 : 0);
6076
- svg += `<line x1="${pos.x}" y1="${headerY + headerHeight}" x2="${pos.x + pos.width}" y2="${headerY + headerHeight}"
6915
+ if (showInnerBorder) {
6916
+ svg += `<line x1="${pos.x}" y1="${headerY + headerHeight}" x2="${pos.x + pos.width}" y2="${headerY + headerHeight}"
6077
6917
  stroke="${this.renderTheme.border}" stroke-width="1"/>`;
6078
- columns.forEach((_, i) => {
6918
+ }
6919
+ safeColumns.forEach((_, i) => {
6079
6920
  svg += `<rect x="${pos.x + i * colWidth + 12}" y="${headerY + 16}"
6080
6921
  width="50" height="10"
6081
6922
  rx="4"
6082
6923
  fill="${this.renderTheme.border}"/>`;
6083
6924
  });
6925
+ if (hasActions && showInnerBorder) {
6926
+ const dividerX = pos.x + dataWidth;
6927
+ svg += `<line x1="${dividerX}" y1="${headerY + headerHeight}" x2="${dividerX}" y2="${headerY + headerHeight + rowCount * rowHeight}"
6928
+ stroke="${this.renderTheme.border}" stroke-width="1"/>`;
6929
+ }
6084
6930
  for (let rowIdx = 0; rowIdx < rowCount; rowIdx++) {
6085
6931
  const rowY = headerY + headerHeight + rowIdx * rowHeight;
6086
- svg += `<line x1="${pos.x}" y1="${rowY + rowHeight}" x2="${pos.x + pos.width}" y2="${rowY + rowHeight}"
6932
+ if (showInnerBorder) {
6933
+ svg += `<line x1="${pos.x}" y1="${rowY + rowHeight}" x2="${pos.x + pos.width}" y2="${rowY + rowHeight}"
6087
6934
  stroke="${this.renderTheme.border}" stroke-width="0.5"/>`;
6088
- columns.forEach((_, colIdx) => {
6935
+ }
6936
+ safeColumns.forEach((_, colIdx) => {
6089
6937
  const variance = (rowIdx * 17 + colIdx * 11) % 5 * 10;
6090
6938
  const blockWidth = Math.min(colWidth - 24, 60 + variance);
6091
6939
  svg += `<rect x="${pos.x + colIdx * colWidth + 12}" y="${rowY + 12}"
@@ -6093,6 +6941,45 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
6093
6941
  rx="4"
6094
6942
  fill="${this.renderTheme.border}"/>`;
6095
6943
  });
6944
+ if (hasActions) {
6945
+ const iconSize = 14;
6946
+ const iconGap = 8;
6947
+ const actionsWidth = actions.length * iconSize + Math.max(0, actions.length - 1) * iconGap;
6948
+ let currentX = pos.x + pos.width - 12 - actionsWidth;
6949
+ const iconY = rowY + (rowHeight - iconSize) / 2;
6950
+ actions.forEach(() => {
6951
+ svg += `<rect x="${currentX}" y="${iconY}" width="${iconSize}" height="${iconSize}" rx="3" fill="${this.renderTheme.border}"/>`;
6952
+ currentX += iconSize + iconGap;
6953
+ });
6954
+ }
6955
+ }
6956
+ const footerTop = headerY + headerHeight + rowCount * rowHeight + 16;
6957
+ if (hasCaption) {
6958
+ const captionY = sameFooterAlign ? footerTop : footerTop + (pagination ? 10 : 0);
6959
+ const captionWidth = Math.min(220, Math.max(90, pos.width * 0.34));
6960
+ let captionX = pos.x + 16;
6961
+ if (captionAlign === "center") {
6962
+ captionX = pos.x + (pos.width - captionWidth) / 2;
6963
+ } else if (captionAlign === "right") {
6964
+ captionX = pos.x + pos.width - 16 - captionWidth;
6965
+ }
6966
+ svg += `<rect x="${captionX}" y="${captionY}" width="${captionWidth}" height="10" rx="4" fill="${this.renderTheme.border}"/>`;
6967
+ }
6968
+ if (pagination) {
6969
+ const buttonWidth = 28;
6970
+ const buttonHeight = 24;
6971
+ const buttonGap = 8;
6972
+ const totalWidth = (pageCount + 2) * buttonWidth + (pageCount + 1) * buttonGap;
6973
+ const paginationY = sameFooterAlign ? footerTop + 18 : footerTop;
6974
+ let startX = pos.x + pos.width - totalWidth - 16;
6975
+ if (paginationAlign === "left") {
6976
+ startX = pos.x + 16;
6977
+ } else if (paginationAlign === "center") {
6978
+ startX = pos.x + (pos.width - totalWidth) / 2;
6979
+ }
6980
+ for (let i = 0; i < pageCount + 2; i++) {
6981
+ svg += `<rect x="${startX + i * (buttonWidth + buttonGap)}" y="${paginationY}" width="${buttonWidth}" height="${buttonHeight}" rx="4" fill="${this.renderTheme.border}"/>`;
6982
+ }
6096
6983
  }
6097
6984
  svg += "</g>";
6098
6985
  return svg;
@@ -6105,21 +6992,39 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
6105
6992
  const subtitle = String(node.props.subtitle || "");
6106
6993
  const actions = String(node.props.actions || "");
6107
6994
  const user = String(node.props.user || "");
6995
+ const variant = String(node.props.variant || "default");
6996
+ const accentBlock = variant === "default" ? this.renderTheme.border : this.hexToRgba(this.resolveVariantColor(variant, this.resolveAccentColor()), 0.35);
6997
+ const showBorder = this.parseBooleanProp(node.props.border, false);
6998
+ const showBackground = this.parseBooleanProp(node.props.background ?? node.props.backround, false);
6999
+ const radiusMap = {
7000
+ none: 0,
7001
+ sm: 4,
7002
+ md: this.tokens.card.radius,
7003
+ lg: 12,
7004
+ xl: 16
7005
+ };
7006
+ const topbarRadius = radiusMap[String(node.props.radius || "md")] ?? this.tokens.card.radius;
6108
7007
  const topbar = this.calculateTopbarLayout(node, pos, title, subtitle, actions, user);
6109
7008
  const titleWidth = Math.max(56, Math.min(topbar.titleMaxWidth * 0.55, topbar.titleMaxWidth));
6110
7009
  const subtitleWidth = Math.max(48, Math.min(topbar.titleMaxWidth * 0.4, topbar.titleMaxWidth));
6111
- let svg = `<g${this.getDataNodeId(node)}>
7010
+ let svg = `<g${this.getDataNodeId(node)}>`;
7011
+ if (showBorder || showBackground) {
7012
+ const bg = showBackground ? this.renderTheme.cardBg : "none";
7013
+ const stroke = showBorder ? this.renderTheme.border : "none";
7014
+ svg += `
6112
7015
  <rect x="${pos.x}" y="${pos.y}"
6113
7016
  width="${pos.width}" height="${pos.height}"
6114
- fill="${this.renderTheme.cardBg}"
6115
- stroke="${this.renderTheme.border}"
6116
- stroke-width="0 0 1 0"/>`;
7017
+ rx="${topbarRadius}"
7018
+ fill="${bg}"
7019
+ stroke="${stroke}"
7020
+ stroke-width="1"/>`;
7021
+ }
6117
7022
  if (topbar.leftIcon) {
6118
7023
  svg += `
6119
7024
  <rect x="${topbar.leftIcon.badgeX}" y="${topbar.leftIcon.badgeY}"
6120
7025
  width="${topbar.leftIcon.badgeSize}" height="${topbar.leftIcon.badgeSize}"
6121
7026
  rx="${topbar.leftIcon.badgeRadius}"
6122
- fill="${this.renderTheme.border}"/>`;
7027
+ fill="${accentBlock}"/>`;
6123
7028
  }
6124
7029
  svg += `
6125
7030
  <rect x="${topbar.textX}" y="${topbar.titleY - 12}"
@@ -6138,7 +7043,7 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
6138
7043
  <rect x="${action.x}" y="${action.y}"
6139
7044
  width="${action.width}" height="${action.height}"
6140
7045
  rx="6"
6141
- fill="${this.renderTheme.border}"/>`;
7046
+ fill="${accentBlock}"/>`;
6142
7047
  });
6143
7048
  if (topbar.userBadge) {
6144
7049
  svg += `
@@ -6206,12 +7111,14 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
6206
7111
  */
6207
7112
  renderIcon(node, pos) {
6208
7113
  const size = String(node.props.size || "md");
7114
+ const variant = String(node.props.variant || "default");
6209
7115
  const iconSize = this.getIconSize(size);
7116
+ const blockColor = variant === "default" ? this.renderTheme.border : this.hexToRgba(this.resolveVariantColor(variant, this.resolveTextColor()), 0.35);
6210
7117
  return `<g${this.getDataNodeId(node)}>
6211
7118
  <rect x="${pos.x}" y="${pos.y + (pos.height - iconSize) / 2}"
6212
7119
  width="${iconSize}" height="${iconSize}"
6213
7120
  rx="2"
6214
- fill="${this.renderTheme.border}"/>
7121
+ fill="${blockColor}"/>
6215
7122
  </g>`;
6216
7123
  }
6217
7124
  /**
@@ -6220,15 +7127,23 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
6220
7127
  renderIconButton(node, pos) {
6221
7128
  const variant = String(node.props.variant || "default");
6222
7129
  const size = String(node.props.size || "md");
7130
+ const density = this.ir.project.style.density || "normal";
7131
+ const labelOffset = this.parseBooleanProp(node.props.labelSpace, false) ? 18 : 0;
7132
+ const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
6223
7133
  const semanticBase = this.getSemanticVariantColor(variant);
6224
7134
  const hasExplicitVariantColor = semanticBase !== void 0 || this.colorResolver.hasColor(variant);
6225
7135
  const resolvedBase = this.resolveVariantColor(variant, this.renderTheme.primary);
6226
7136
  const bgColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.85) : "rgba(226, 232, 240, 0.9)";
6227
7137
  const borderColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.7) : "rgba(100, 116, 139, 0.4)";
6228
- const buttonSize = this.getIconButtonSize(size);
7138
+ const buttonSize = Math.max(
7139
+ 16,
7140
+ Math.min(resolveActionControlHeight(size, density), pos.height - labelOffset)
7141
+ );
7142
+ const buttonWidth = buttonSize + extraPadding * 2;
7143
+ const buttonY = pos.y + labelOffset;
6229
7144
  return `<g${this.getDataNodeId(node)}>
6230
- <rect x="${pos.x}" y="${pos.y}"
6231
- width="${buttonSize}" height="${buttonSize}"
7145
+ <rect x="${pos.x}" y="${buttonY}"
7146
+ width="${buttonWidth}" height="${buttonSize}"
6232
7147
  rx="6"
6233
7148
  fill="${bgColor}"
6234
7149
  stroke="${borderColor}"
@@ -6308,10 +7223,10 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
6308
7223
  /**
6309
7224
  * Private helper: Render text as gray block
6310
7225
  */
6311
- renderTextBlock(node, pos, text, fontSize, lineHeightMultiplier) {
7226
+ renderTextBlock(node, pos, text, fontSize, lineHeightMultiplier, color) {
6312
7227
  const lineHeight = Math.ceil(fontSize * lineHeightMultiplier);
6313
7228
  const blockHeight = Math.max(8, Math.round(fontSize * 0.75));
6314
- const blockColor = this.renderTheme.border;
7229
+ const blockColor = color || this.renderTheme.border;
6315
7230
  const lines = this.wrapTextToLines(text, pos.width, fontSize);
6316
7231
  const contentHeight = lines.length * lineHeight;
6317
7232
  const startY = pos.y + Math.max(0, (pos.height - contentHeight) / 2);
@@ -6393,32 +7308,39 @@ var SketchSVGRenderer = class extends SVGRenderer {
6393
7308
  renderButton(node, pos) {
6394
7309
  const text = String(node.props.text || "Button");
6395
7310
  const variant = String(node.props.variant || "default");
7311
+ const size = String(node.props.size || "md");
7312
+ const density = this.ir.project.style.density || "normal";
7313
+ const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
7314
+ const labelOffset = this.parseBooleanProp(node.props.labelSpace, false) ? 18 : 0;
6396
7315
  const fullWidth = this.shouldButtonFillAvailableWidth(node);
6397
7316
  const radius = this.tokens.button.radius;
6398
7317
  const fontSize = this.tokens.button.fontSize;
6399
7318
  const fontWeight = this.tokens.button.fontWeight;
6400
7319
  const paddingX = this.tokens.button.paddingX;
6401
- const paddingY = this.tokens.button.paddingY;
7320
+ const buttonHeight = Math.max(
7321
+ 16,
7322
+ Math.min(resolveControlHeight(size, density), pos.height - labelOffset)
7323
+ );
7324
+ const buttonY = pos.y + labelOffset;
6402
7325
  const idealTextWidth = text.length * fontSize * 0.6;
6403
- const buttonWidth = fullWidth ? Math.max(1, pos.width) : this.clampControlWidth(Math.max(idealTextWidth + paddingX * 2, 60), pos.width);
6404
- const buttonHeight = fontSize + paddingY * 2;
6405
- const availableTextWidth = Math.max(0, buttonWidth - paddingX * 2);
7326
+ const buttonWidth = fullWidth ? Math.max(1, pos.width) : this.clampControlWidth(Math.max(idealTextWidth + (paddingX + extraPadding) * 2, 60), pos.width);
7327
+ const availableTextWidth = Math.max(0, buttonWidth - (paddingX + extraPadding) * 2);
6406
7328
  const visibleText = this.truncateTextToWidth(text, availableTextWidth, fontSize);
6407
7329
  const semanticBase = this.getSemanticVariantColor(variant);
6408
7330
  const hasExplicitVariantColor = semanticBase !== void 0 || this.colorResolver.hasColor(variant);
6409
- const variantColor = hasExplicitVariantColor ? this.resolveVariantColor(variant, this.renderTheme.primary) : "#2D3748";
7331
+ const variantColor = hasExplicitVariantColor ? this.resolveVariantColor(variant, this.renderTheme.primary) : this.resolveTextColor();
6410
7332
  const borderColor = variantColor;
6411
7333
  const textColor = variantColor;
6412
7334
  const strokeWidth = 0.5;
6413
7335
  return `<g${this.getDataNodeId(node)}>
6414
- <rect x="${pos.x}" y="${pos.y}"
7336
+ <rect x="${pos.x}" y="${buttonY}"
6415
7337
  width="${buttonWidth}" height="${buttonHeight}"
6416
7338
  rx="${radius}"
6417
7339
  fill="none"
6418
7340
  stroke="${borderColor}"
6419
7341
  stroke-width="${strokeWidth}"
6420
7342
  filter="url(#sketch-rough)"/>
6421
- <text x="${pos.x + buttonWidth / 2}" y="${pos.y + buttonHeight / 2 + fontSize * 0.35}"
7343
+ <text x="${pos.x + buttonWidth / 2}" y="${buttonY + buttonHeight / 2 + fontSize * 0.35}"
6422
7344
  font-family="${this.fontFamily}"
6423
7345
  font-size="${fontSize}"
6424
7346
  font-weight="${fontWeight}"
@@ -6434,7 +7356,7 @@ var SketchSVGRenderer = class extends SVGRenderer {
6434
7356
  const variant = String(node.props.variant || "default");
6435
7357
  const semanticBase = this.getSemanticVariantColor(variant);
6436
7358
  const hasExplicitVariantColor = semanticBase !== void 0 || this.colorResolver.hasColor(variant);
6437
- const variantColor = hasExplicitVariantColor ? this.resolveVariantColor(variant, this.renderTheme.primary) : "#2D3748";
7359
+ const variantColor = hasExplicitVariantColor ? this.resolveVariantColor(variant, this.renderTheme.primary) : this.resolveTextColor();
6438
7360
  const borderColor = variantColor;
6439
7361
  const textColor = variantColor;
6440
7362
  const badgeRadius = this.tokens.badge.radius === "pill" ? pos.height / 2 : this.tokens.badge.radius;
@@ -6461,17 +7383,22 @@ var SketchSVGRenderer = class extends SVGRenderer {
6461
7383
  const iconName = String(node.props.icon || "help-circle");
6462
7384
  const variant = String(node.props.variant || "default");
6463
7385
  const size = String(node.props.size || "md");
7386
+ const density = this.ir.project.style.density || "normal";
7387
+ const labelOffset = this.parseBooleanProp(node.props.labelSpace, false) ? 18 : 0;
7388
+ const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
6464
7389
  const semanticBase = this.getSemanticVariantColor(variant);
6465
7390
  const hasExplicitVariantColor = semanticBase !== void 0 || this.colorResolver.hasColor(variant);
6466
- const variantColor = hasExplicitVariantColor ? this.resolveVariantColor(variant, this.renderTheme.primary) : "#2D3748";
7391
+ const variantColor = hasExplicitVariantColor ? this.resolveVariantColor(variant, this.renderTheme.primary) : this.resolveTextColor();
6467
7392
  const borderColor = variantColor;
6468
7393
  const iconColor = variantColor;
6469
- const buttonSize = this.getIconButtonSize(size);
7394
+ const buttonSize = Math.max(16, Math.min(resolveControlHeight(size, density), pos.height - labelOffset));
7395
+ const buttonWidth = buttonSize + extraPadding * 2;
6470
7396
  const radius = 6;
7397
+ const buttonY = pos.y + labelOffset;
6471
7398
  const iconSvg = this.getIconSvg(iconName);
6472
7399
  let svg = `<g${this.getDataNodeId(node)}>
6473
- <rect x="${pos.x}" y="${pos.y}"
6474
- width="${buttonSize}" height="${buttonSize}"
7400
+ <rect x="${pos.x}" y="${buttonY}"
7401
+ width="${buttonWidth}" height="${buttonSize}"
6475
7402
  rx="${radius}"
6476
7403
  fill="none"
6477
7404
  stroke="${borderColor}"
@@ -6479,8 +7406,8 @@ var SketchSVGRenderer = class extends SVGRenderer {
6479
7406
  filter="url(#sketch-rough)"/>`;
6480
7407
  if (iconSvg) {
6481
7408
  const iconSize = buttonSize * 0.6;
6482
- const offsetX = pos.x + (buttonSize - iconSize) / 2;
6483
- const offsetY = pos.y + (buttonSize - iconSize) / 2;
7409
+ const offsetX = pos.x + (buttonWidth - iconSize) / 2;
7410
+ const offsetY = buttonY + (buttonSize - iconSize) / 2;
6484
7411
  svg += `
6485
7412
  <g transform="translate(${offsetX}, ${offsetY})">
6486
7413
  <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round">
@@ -6631,6 +7558,8 @@ var SketchSVGRenderer = class extends SVGRenderer {
6631
7558
  */
6632
7559
  renderHeading(node, pos) {
6633
7560
  const text = String(node.props.text || "Heading");
7561
+ const variant = String(node.props.variant || "default");
7562
+ const headingColor = variant === "default" ? this.resolveTextColor() : this.resolveVariantColor(variant, this.resolveTextColor());
6634
7563
  const headingTypography = this.getHeadingTypography(node);
6635
7564
  const fontSize = headingTypography.fontSize;
6636
7565
  const fontWeight = headingTypography.fontWeight;
@@ -6643,7 +7572,7 @@ var SketchSVGRenderer = class extends SVGRenderer {
6643
7572
  font-family="${this.fontFamily}"
6644
7573
  font-size="${fontSize}"
6645
7574
  font-weight="${fontWeight}"
6646
- fill="${this.renderTheme.text}">${this.escapeXml(text)}</text>
7575
+ fill="${headingColor}">${this.escapeXml(text)}</text>
6647
7576
  </g>`;
6648
7577
  }
6649
7578
  const tspans = lines.map(
@@ -6654,7 +7583,7 @@ var SketchSVGRenderer = class extends SVGRenderer {
6654
7583
  font-family="${this.fontFamily}"
6655
7584
  font-size="${fontSize}"
6656
7585
  font-weight="${fontWeight}"
6657
- fill="${this.renderTheme.text}">${tspans}</text>
7586
+ fill="${headingColor}">${tspans}</text>
6658
7587
  </g>`;
6659
7588
  }
6660
7589
  /**
@@ -6665,7 +7594,8 @@ var SketchSVGRenderer = class extends SVGRenderer {
6665
7594
  const subtitle = String(node.props.subtitle || "");
6666
7595
  const actions = String(node.props.actions || "");
6667
7596
  const user = String(node.props.user || "");
6668
- const accentColor = this.resolveAccentColor();
7597
+ const variant = String(node.props.variant || "default");
7598
+ const accentColor = variant === "default" ? this.resolveAccentColor() : this.resolveVariantColor(variant, this.resolveAccentColor());
6669
7599
  const topbar = this.calculateTopbarLayout(node, pos, title, subtitle, actions, user);
6670
7600
  let svg = `<g${this.getDataNodeId(node)}>
6671
7601
  <rect x="${pos.x}" y="${pos.y}"
@@ -6758,79 +7688,14 @@ var SketchSVGRenderer = class extends SVGRenderer {
6758
7688
  * Render table with sketch filter and Comic Sans
6759
7689
  */
6760
7690
  renderTable(node, pos) {
6761
- const title = String(node.props.title || "");
6762
- const columnsStr = String(node.props.columns || "Col1,Col2,Col3");
6763
- const columns = columnsStr.split(",").map((c) => c.trim());
6764
- const rowCount = Number(node.props.rows || node.props.rowsMock || 5);
6765
- const mockStr = String(node.props.mock || "");
6766
- const random = this.parseBooleanProp(node.props.random, false);
6767
- const mockTypes = mockStr ? mockStr.split(",").map((m) => m.trim()).filter(Boolean) : [];
6768
- while (mockTypes.length < columns.length) {
6769
- const inferred = MockDataGenerator.inferMockTypeFromColumn(columns[mockTypes.length] || "item");
6770
- mockTypes.push(inferred);
6771
- }
6772
- const headerHeight = 44;
6773
- const rowHeight = 36;
6774
- const colWidth = pos.width / columns.length;
6775
- const mockRows = [];
6776
- for (let rowIdx = 0; rowIdx < rowCount; rowIdx++) {
6777
- const row = {};
6778
- columns.forEach((col, colIdx) => {
6779
- const mockType = mockTypes[colIdx] || MockDataGenerator.inferMockTypeFromColumn(col) || "item";
6780
- row[col] = MockDataGenerator.getMockValue(mockType, rowIdx, random);
6781
- });
6782
- mockRows.push(row);
6783
- }
6784
- let svg = `<g${this.getDataNodeId(node)}>
6785
- <rect x="${pos.x}" y="${pos.y}"
6786
- width="${pos.width}" height="${pos.height}"
6787
- rx="8"
6788
- fill="${this.renderTheme.cardBg}"
6789
- stroke="#2D3748"
6790
- stroke-width="0.5"
6791
- filter="url(#sketch-rough)"/>`;
6792
- if (title) {
6793
- svg += `
6794
- <text x="${pos.x + 16}" y="${pos.y + 24}"
6795
- font-family="${this.fontFamily}"
6796
- font-size="13"
6797
- font-weight="600"
6798
- fill="${this.renderTheme.text}">${this.escapeXml(title)}</text>`;
6799
- }
6800
- const headerY = pos.y + (title ? 32 : 0);
6801
- svg += `
6802
- <line x1="${pos.x}" y1="${headerY + headerHeight}" x2="${pos.x + pos.width}" y2="${headerY + headerHeight}"
6803
- stroke="#2D3748" stroke-width="0.5" filter="url(#sketch-rough)"/>`;
6804
- columns.forEach((col, i) => {
6805
- svg += `
6806
- <text x="${pos.x + i * colWidth + 12}" y="${headerY + 26}"
6807
- font-family="${this.fontFamily}"
6808
- font-size="11"
6809
- font-weight="600"
6810
- fill="${this.renderTheme.textMuted}">${this.escapeXml(col)}</text>`;
6811
- });
6812
- mockRows.forEach((row, rowIdx) => {
6813
- const rowY = headerY + headerHeight + rowIdx * rowHeight;
6814
- svg += `
6815
- <line x1="${pos.x}" y1="${rowY + rowHeight}" x2="${pos.x + pos.width}" y2="${rowY + rowHeight}"
6816
- stroke="#2D3748" stroke-width="0.5" filter="url(#sketch-rough)"/>`;
6817
- columns.forEach((col, colIdx) => {
6818
- const cellValue = row[col] || "";
6819
- svg += `
6820
- <text x="${pos.x + colIdx * colWidth + 12}" y="${rowY + 22}"
6821
- font-family="${this.fontFamily}"
6822
- font-size="12"
6823
- fill="${this.renderTheme.text}">${this.escapeXml(cellValue)}</text>`;
6824
- });
6825
- });
6826
- svg += "\n </g>";
6827
- return svg;
7691
+ const standard = super.renderTable(node, pos);
7692
+ return standard.replace("<g", '<g filter="url(#sketch-rough)"');
6828
7693
  }
6829
7694
  /**
6830
7695
  * Render text with Comic Sans
6831
7696
  */
6832
7697
  renderText(node, pos) {
6833
- const text = String(node.props.content || "Text content");
7698
+ const text = String(node.props.text || "Text content");
6834
7699
  const fontSize = this.tokens.text.fontSize;
6835
7700
  const lineHeightPx = Math.ceil(fontSize * this.tokens.text.lineHeight);
6836
7701
  const lines = this.wrapTextToLines(text, pos.width, fontSize);
@@ -7407,7 +8272,7 @@ var SketchSVGRenderer = class extends SVGRenderer {
7407
8272
  const itemY = pos.y + index * itemHeight;
7408
8273
  const isActive = index === activeIndex;
7409
8274
  const bgColor = isActive ? this.hexToRgba(accentColor, 0.15) : "transparent";
7410
- const textColor = isActive ? accentColor : "#2D3748";
8275
+ const textColor = isActive ? accentColor : this.resolveTextColor();
7411
8276
  const fontWeight = isActive ? "500" : "400";
7412
8277
  if (isActive) {
7413
8278
  svg += `
@@ -7431,22 +8296,23 @@ var SketchSVGRenderer = class extends SVGRenderer {
7431
8296
  * Render icon (same as base, icons don't need filter)
7432
8297
  */
7433
8298
  renderIcon(node, pos) {
7434
- const iconType = String(node.props.type || "help-circle");
8299
+ const iconType = String(node.props.icon || "help-circle");
7435
8300
  const size = String(node.props.size || "md");
8301
+ const variant = String(node.props.variant || "default");
7436
8302
  const iconSvg = getIcon(iconType);
7437
8303
  if (!iconSvg) {
7438
8304
  return `<g${this.getDataNodeId(node)}>
7439
8305
  <circle cx="${pos.x + pos.width / 2}" cy="${pos.y + pos.height / 2}"
7440
8306
  r="${Math.min(pos.width, pos.height) / 2 - 2}"
7441
- fill="none" stroke="#2D3748" stroke-width="0.5"
8307
+ fill="none" stroke="${this.resolveMutedColor()}" stroke-width="0.5"
7442
8308
  filter="url(#sketch-rough)"/>
7443
8309
  <text x="${pos.x + pos.width / 2}" y="${pos.y + pos.height / 2 + 4}"
7444
8310
  font-family="${this.fontFamily}"
7445
- font-size="12" fill="#2D3748" text-anchor="middle">?</text>
8311
+ font-size="12" fill="${this.resolveMutedColor()}" text-anchor="middle">?</text>
7446
8312
  </g>`;
7447
8313
  }
7448
8314
  const iconSize = this.getIconSize(size);
7449
- const iconColor = "#2D3748";
8315
+ const iconColor = variant === "default" ? this.resolveTextColor() : this.resolveVariantColor(variant, this.resolveTextColor());
7450
8316
  const offsetX = pos.x + (pos.width - iconSize) / 2;
7451
8317
  const offsetY = pos.y + (pos.height - iconSize) / 2;
7452
8318
  return `<g${this.getDataNodeId(node)} transform="translate(${offsetX}, ${offsetY})">