@wire-dsl/engine 0.4.0 → 0.5.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/README.md CHANGED
@@ -92,7 +92,7 @@ validateIR(ir); // Throws if invalid
92
92
 
93
93
  **Display**: Divider, Badge, Link, Alert
94
94
 
95
- **Info**: StatCard, Code, ChartPlaceholder
95
+ **Info**: Stat, Code, ChartPlaceholder
96
96
 
97
97
  **Feedback**: Modal, Spinner
98
98
 
package/dist/index.cjs CHANGED
@@ -2245,7 +2245,21 @@ var IRComponentNodeSchema = import_zod.z.object({
2245
2245
  style: IRNodeStyleSchema,
2246
2246
  meta: IRMetaSchema
2247
2247
  });
2248
- var IRNodeSchema = import_zod.z.union([IRContainerNodeSchema, IRComponentNodeSchema]);
2248
+ var IRInstanceNodeSchema = import_zod.z.object({
2249
+ id: import_zod.z.string(),
2250
+ kind: import_zod.z.literal("instance"),
2251
+ definitionName: import_zod.z.string(),
2252
+ definitionKind: import_zod.z.enum(["component", "layout"]),
2253
+ invocationProps: import_zod.z.record(import_zod.z.string(), import_zod.z.union([import_zod.z.string(), import_zod.z.number()])),
2254
+ expandedRoot: import_zod.z.object({ ref: import_zod.z.string() }),
2255
+ style: IRNodeStyleSchema,
2256
+ meta: IRMetaSchema
2257
+ });
2258
+ var IRNodeSchema = import_zod.z.discriminatedUnion("kind", [
2259
+ IRContainerNodeSchema,
2260
+ IRComponentNodeSchema,
2261
+ IRInstanceNodeSchema
2262
+ ]);
2249
2263
  var IRScreenSchema = import_zod.z.object({
2250
2264
  id: import_zod.z.string(),
2251
2265
  name: import_zod.z.string(),
@@ -2468,7 +2482,7 @@ ${messages}`);
2468
2482
  const layoutChildren = layout.children;
2469
2483
  const layoutDefinition = this.definedLayouts.get(layout.layoutType);
2470
2484
  if (layoutDefinition) {
2471
- return this.expandDefinedLayout(layoutDefinition, layoutParams, layoutChildren, context);
2485
+ return this.expandDefinedLayout(layoutDefinition, layoutParams, layoutChildren, context, layout._meta?.nodeId);
2472
2486
  }
2473
2487
  const nodeId = this.idGen.generate("node");
2474
2488
  const childRefs = [];
@@ -2507,8 +2521,8 @@ ${messages}`);
2507
2521
  children: childRefs,
2508
2522
  style,
2509
2523
  meta: {
2510
- nodeId: layout._meta?.nodeId
2511
- // Pass SourceMap nodeId from AST
2524
+ // Scope nodeId per instance so each expansion gets a unique identifier
2525
+ nodeId: context?.instanceScope ? `${layout._meta?.nodeId}@${context.instanceScope}` : layout._meta?.nodeId
2512
2526
  }
2513
2527
  };
2514
2528
  this.nodes[nodeId] = containerNode;
@@ -2537,8 +2551,7 @@ ${messages}`);
2537
2551
  // Cells have no padding by default - grid gap handles spacing
2538
2552
  meta: {
2539
2553
  source: "cell",
2540
- nodeId: cell._meta?.nodeId
2541
- // Pass SourceMap nodeId from AST
2554
+ nodeId: context?.instanceScope ? `${cell._meta?.nodeId}@${context.instanceScope}` : cell._meta?.nodeId
2542
2555
  }
2543
2556
  };
2544
2557
  this.nodes[nodeId] = containerNode;
@@ -2565,7 +2578,7 @@ ${messages}`);
2565
2578
  const resolvedProps = this.resolveComponentProps(component.componentType, component.props, context);
2566
2579
  const definition = this.definedComponents.get(component.componentType);
2567
2580
  if (definition) {
2568
- return this.expandDefinedComponent(definition, resolvedProps, context);
2581
+ return this.expandDefinedComponent(definition, resolvedProps, component._meta?.nodeId, context);
2569
2582
  }
2570
2583
  const builtInComponents = /* @__PURE__ */ new Set([
2571
2584
  "Button",
@@ -2575,7 +2588,7 @@ ${messages}`);
2575
2588
  "Label",
2576
2589
  "Image",
2577
2590
  "Card",
2578
- "StatCard",
2591
+ "Stat",
2579
2592
  "Topbar",
2580
2593
  "Table",
2581
2594
  "Chart",
@@ -2611,35 +2624,49 @@ ${messages}`);
2611
2624
  props: resolvedProps,
2612
2625
  style: {},
2613
2626
  meta: {
2614
- nodeId: component._meta?.nodeId
2615
- // Pass SourceMap nodeId from AST
2627
+ // Scope nodeId per instance so each expansion gets a unique identifier
2628
+ nodeId: context?.instanceScope ? `${component._meta?.nodeId}@${context.instanceScope}` : component._meta?.nodeId
2616
2629
  }
2617
2630
  };
2618
2631
  this.nodes[nodeId] = componentNode;
2619
2632
  return nodeId;
2620
2633
  }
2621
- expandDefinedComponent(definition, invocationArgs, parentContext) {
2634
+ expandDefinedComponent(definition, invocationArgs, callSiteNodeId, parentContext) {
2622
2635
  const context = {
2623
2636
  args: invocationArgs,
2624
2637
  providedArgNames: new Set(Object.keys(invocationArgs)),
2625
2638
  usedArgNames: /* @__PURE__ */ new Set(),
2626
2639
  definitionName: definition.name,
2627
2640
  definitionKind: "component",
2628
- allowChildrenSlot: false
2641
+ allowChildrenSlot: false,
2642
+ // Scope internal nodeIds using the call-site nodeId
2643
+ instanceScope: callSiteNodeId
2629
2644
  };
2645
+ let expandedRootId = null;
2630
2646
  if (definition.body.type === "layout") {
2631
- const result = this.convertLayout(definition.body, context);
2632
- this.reportUnusedArguments(context);
2633
- return result;
2647
+ expandedRootId = this.convertLayout(definition.body, context);
2634
2648
  } else if (definition.body.type === "component") {
2635
- const result = this.convertComponent(definition.body, context);
2636
- this.reportUnusedArguments(context);
2637
- return result;
2649
+ expandedRootId = this.convertComponent(definition.body, context);
2638
2650
  } else {
2639
2651
  throw new Error(`Invalid defined component body type for "${definition.name}"`);
2640
2652
  }
2653
+ this.reportUnusedArguments(context);
2654
+ if (!expandedRootId) return null;
2655
+ const instanceNodeId = this.idGen.generate("node");
2656
+ const instanceNode = {
2657
+ id: instanceNodeId,
2658
+ kind: "instance",
2659
+ definitionName: definition.name,
2660
+ definitionKind: "component",
2661
+ invocationProps: invocationArgs,
2662
+ expandedRoot: { ref: expandedRootId },
2663
+ style: {},
2664
+ meta: { nodeId: callSiteNodeId }
2665
+ };
2666
+ this.nodes[instanceNodeId] = instanceNode;
2667
+ return instanceNodeId;
2641
2668
  }
2642
- expandDefinedLayout(definition, invocationParams, invocationChildren, parentContext) {
2669
+ expandDefinedLayout(definition, invocationParams, invocationChildren, parentContext, callSiteNodeId) {
2643
2670
  if (invocationChildren.length !== 1) {
2644
2671
  this.errors.push({
2645
2672
  type: "layout-children-arity",
@@ -2655,11 +2682,26 @@ ${messages}`);
2655
2682
  definitionName: definition.name,
2656
2683
  definitionKind: "layout",
2657
2684
  allowChildrenSlot: true,
2658
- childrenSlot: resolvedSlot
2685
+ childrenSlot: resolvedSlot,
2686
+ // Scope internal nodeIds using the call-site nodeId
2687
+ instanceScope: callSiteNodeId
2659
2688
  };
2660
- const nodeId = this.convertLayout(definition.body, context);
2689
+ const expandedRootId = this.convertLayout(definition.body, context);
2661
2690
  this.reportUnusedArguments(context);
2662
- return nodeId;
2691
+ if (!callSiteNodeId) return expandedRootId;
2692
+ const instanceNodeId = this.idGen.generate("node");
2693
+ const instanceNode = {
2694
+ id: instanceNodeId,
2695
+ kind: "instance",
2696
+ definitionName: definition.name,
2697
+ definitionKind: "layout",
2698
+ invocationProps: invocationParams,
2699
+ expandedRoot: { ref: expandedRootId },
2700
+ style: {},
2701
+ meta: { nodeId: callSiteNodeId }
2702
+ };
2703
+ this.nodes[instanceNodeId] = instanceNode;
2704
+ return instanceNodeId;
2663
2705
  }
2664
2706
  resolveChildrenSlot(slot, parentContext) {
2665
2707
  if (slot.type === "component" && slot.componentType === "Children") {
@@ -3025,6 +3067,8 @@ var LayoutEngine = class {
3025
3067
  }
3026
3068
  if (node.kind === "container") {
3027
3069
  this.calculateContainer(node, nodeId, x, y, width, height);
3070
+ } else if (node.kind === "instance") {
3071
+ this.calculateInstance(node, nodeId, x, y, width, height, parentContainerType);
3028
3072
  } else {
3029
3073
  this.calculateComponent(node, nodeId, x, y, width, height);
3030
3074
  }
@@ -3057,7 +3101,7 @@ var LayoutEngine = class {
3057
3101
  this.calculateCard(node, innerX, innerY, innerWidth, innerHeight);
3058
3102
  break;
3059
3103
  }
3060
- if (isVerticalStack || node.containerType === "card") {
3104
+ if ((isVerticalStack || node.containerType === "card") && node.children.length > 0) {
3061
3105
  let containerMaxY = y;
3062
3106
  node.children.forEach((childRef) => {
3063
3107
  const childPos = this.result[childRef.ref];
@@ -3200,6 +3244,10 @@ var LayoutEngine = class {
3200
3244
  const gap = this.resolveSpacing(node.style.gap);
3201
3245
  const padding = this.resolveSpacing(node.style.padding);
3202
3246
  let totalHeight = padding * 2;
3247
+ const EMPTY_CONTAINER_MIN_HEIGHT = 40;
3248
+ if (node.children.length === 0) {
3249
+ return Math.max(totalHeight, EMPTY_CONTAINER_MIN_HEIGHT);
3250
+ }
3203
3251
  if (node.containerType === "grid") {
3204
3252
  const columns = Number(node.params.columns) || 12;
3205
3253
  const colWidth = (availableWidth - gap * (columns - 1)) / columns;
@@ -3452,6 +3500,20 @@ var LayoutEngine = class {
3452
3500
  }
3453
3501
  });
3454
3502
  }
3503
+ /**
3504
+ * Calculate layout for an instance node.
3505
+ * The instance is a transparent wrapper — its bounding box equals the
3506
+ * expanded root's bounding box. We calculate the expanded root first and
3507
+ * then copy its position to the instance nodeId so the renderer can use it.
3508
+ */
3509
+ calculateInstance(node, nodeId, x, y, width, height, parentContainerType) {
3510
+ const expandedRootId = node.expandedRoot.ref;
3511
+ this.calculateNode(expandedRootId, x, y, width, height, parentContainerType);
3512
+ const expandedPos = this.result[expandedRootId];
3513
+ if (expandedPos) {
3514
+ this.result[nodeId] = { ...expandedPos };
3515
+ }
3516
+ }
3455
3517
  calculateComponent(node, nodeId, x, y, width, height) {
3456
3518
  if (node.kind !== "component") return;
3457
3519
  const componentWidth = Number(node.props.width) || width;
@@ -3669,7 +3731,7 @@ var LayoutEngine = class {
3669
3731
  if (node.componentType === "Textarea") return 100 + controlLabelOffset;
3670
3732
  if (node.componentType === "Modal") return 300;
3671
3733
  if (node.componentType === "Card") return 120;
3672
- if (node.componentType === "StatCard") return 120;
3734
+ if (node.componentType === "Stat") return 120;
3673
3735
  if (node.componentType === "Chart" || node.componentType === "ChartPlaceholder") return 250;
3674
3736
  if (node.componentType === "List") {
3675
3737
  const itemsFromProps = String(node.props.items || "").split(",").map((item) => item.trim()).filter(Boolean);
@@ -3720,7 +3782,10 @@ var LayoutEngine = class {
3720
3782
  const density = this.style.density || "normal";
3721
3783
  const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
3722
3784
  const textWidth = this.estimateTextWidth(text, fontSize);
3723
- return Math.max(60, Math.ceil(textWidth + (paddingX + extraPadding) * 2));
3785
+ const iconName = String(node.props.icon || "").trim();
3786
+ const iconSize = iconName ? Math.round(fontSize * 1.1) : 0;
3787
+ const iconGap = iconName ? 8 : 0;
3788
+ return Math.max(60, Math.ceil(textWidth + iconSize + iconGap + (paddingX + extraPadding) * 2));
3724
3789
  }
3725
3790
  if (node.componentType === "Label" || node.componentType === "Text") {
3726
3791
  const text = String(node.props.text || "");
@@ -3751,7 +3816,7 @@ var LayoutEngine = class {
3751
3816
  if (node.componentType === "Table") {
3752
3817
  return 400;
3753
3818
  }
3754
- if (node.componentType === "StatCard" || node.componentType === "Card") {
3819
+ if (node.componentType === "Stat" || node.componentType === "Card") {
3755
3820
  return 280;
3756
3821
  }
3757
3822
  if (node.componentType === "SidebarMenu") {
@@ -4617,7 +4682,8 @@ var SVGRenderer = class {
4617
4682
  height: options?.height || 720,
4618
4683
  theme: colorScheme,
4619
4684
  includeLabels: options?.includeLabels ?? true,
4620
- screenName: options?.screenName
4685
+ screenName: options?.screenName,
4686
+ showDiagnostics: options?.showDiagnostics ?? false
4621
4687
  };
4622
4688
  this.colorResolver = new ColorResolver();
4623
4689
  this.buildParentContainerIndex();
@@ -4717,13 +4783,30 @@ var SVGRenderer = class {
4717
4783
  if (node.containerType === "split") {
4718
4784
  this.renderSplitDecoration(node, pos, containerGroup);
4719
4785
  }
4720
- node.children.forEach((childRef) => {
4721
- this.renderNode(childRef.ref, containerGroup);
4722
- });
4786
+ if (node.children.length === 0 && this.options.showDiagnostics) {
4787
+ containerGroup.push(this.renderEmptyContainerDiagnostic(pos, node.containerType));
4788
+ } else {
4789
+ node.children.forEach((childRef) => {
4790
+ this.renderNode(childRef.ref, containerGroup);
4791
+ });
4792
+ }
4723
4793
  if (hasNodeId) {
4724
4794
  containerGroup.push("</g>");
4725
4795
  }
4726
4796
  output.push(...containerGroup);
4797
+ } else if (node.kind === "instance") {
4798
+ const instanceGroup = [];
4799
+ if (node.meta.nodeId) {
4800
+ instanceGroup.push(`<g data-node-id="${node.meta.nodeId}">`);
4801
+ }
4802
+ instanceGroup.push(
4803
+ `<rect x="${pos.x}" y="${pos.y}" width="${pos.width}" height="${pos.height}" fill="transparent" stroke="none" pointer-events="all"/>`
4804
+ );
4805
+ this.renderNode(node.expandedRoot.ref, instanceGroup);
4806
+ if (node.meta.nodeId) {
4807
+ instanceGroup.push("</g>");
4808
+ }
4809
+ output.push(...instanceGroup);
4727
4810
  } else if (node.kind === "component") {
4728
4811
  const componentSvg = this.renderComponent(node, pos);
4729
4812
  if (componentSvg) {
@@ -4789,8 +4872,8 @@ var SVGRenderer = class {
4789
4872
  return this.renderModal(node, pos);
4790
4873
  case "List":
4791
4874
  return this.renderList(node, pos);
4792
- case "StatCard":
4793
- return this.renderStatCard(node, pos);
4875
+ case "Stat":
4876
+ return this.renderStat(node, pos);
4794
4877
  case "Image":
4795
4878
  return this.renderImage(node, pos);
4796
4879
  // Icon components
@@ -4836,6 +4919,7 @@ var SVGRenderer = class {
4836
4919
  const text = String(node.props.text || "Button");
4837
4920
  const variant = String(node.props.variant || "default");
4838
4921
  const size = String(node.props.size || "md");
4922
+ const disabled = this.parseBooleanProp(node.props.disabled, false);
4839
4923
  const density = this.ir.project.style.density || "normal";
4840
4924
  const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
4841
4925
  const labelOffset = this.parseBooleanProp(node.props.labelSpace, false) ? 18 : 0;
@@ -4881,7 +4965,7 @@ var SVGRenderer = class {
4881
4965
  textX = pos.x + buttonWidth / 2;
4882
4966
  textAnchor = "middle";
4883
4967
  }
4884
- let svg = `<g${this.getDataNodeId(node)}>
4968
+ let svg = `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>
4885
4969
  <rect x="${pos.x}" y="${buttonY}"
4886
4970
  width="${buttonWidth}" height="${buttonHeight}"
4887
4971
  rx="${radius}"
@@ -4947,6 +5031,7 @@ var SVGRenderer = class {
4947
5031
  const placeholder = String(node.props.placeholder || "");
4948
5032
  const iconLeftName = String(node.props.iconLeft || "").trim();
4949
5033
  const iconRightName = String(node.props.iconRight || "").trim();
5034
+ const disabled = this.parseBooleanProp(node.props.disabled, false);
4950
5035
  const radius = this.tokens.input.radius;
4951
5036
  const fontSize = this.tokens.input.fontSize;
4952
5037
  const paddingX = this.tokens.input.paddingX;
@@ -4963,7 +5048,7 @@ var SVGRenderer = class {
4963
5048
  const textX = pos.x + (iconLeftSvg ? leftOffset : paddingX);
4964
5049
  const iconColor = this.hexToRgba(this.resolveMutedColor(), 0.8);
4965
5050
  const iconCenterY = controlY + (controlHeight - iconSize) / 2;
4966
- let svg = `<g${this.getDataNodeId(node)}>`;
5051
+ let svg = `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>`;
4967
5052
  if (label) {
4968
5053
  svg += `
4969
5054
  <text x="${pos.x + paddingX}" y="${this.getControlLabelBaselineY(pos.y)}"
@@ -5014,7 +5099,7 @@ var SVGRenderer = class {
5014
5099
  const variant = String(node.props.variant || "default");
5015
5100
  const accentColor = variant === "default" ? this.resolveAccentColor() : this.resolveVariantColor(variant, this.resolveAccentColor());
5016
5101
  const showBorder = this.parseBooleanProp(node.props.border, false);
5017
- const showBackground = this.parseBooleanProp(node.props.background ?? node.props.backround, false);
5102
+ const showBackground = this.parseBooleanProp(node.props.background, false);
5018
5103
  const radiusMap = {
5019
5104
  none: 0,
5020
5105
  sm: 4,
@@ -5153,6 +5238,21 @@ var SVGRenderer = class {
5153
5238
  </g>`;
5154
5239
  output.push(svg);
5155
5240
  }
5241
+ /**
5242
+ * Renders a yellow warning placeholder for containers with no children.
5243
+ * Only shown when `showDiagnostics` is enabled (editor/canvas mode).
5244
+ */
5245
+ renderEmptyContainerDiagnostic(pos, containerType) {
5246
+ const diagColor = "#F59E0B";
5247
+ const diagBg = "#FFFBEB";
5248
+ const diagText = "#92400E";
5249
+ const minHeight = 40;
5250
+ const h = Math.max(pos.height, minHeight);
5251
+ const cx = pos.x + pos.width / 2;
5252
+ const cy = pos.y + h / 2;
5253
+ const label = containerType ? `Empty ${containerType}` : "Empty layout";
5254
+ return `<g><rect x="${pos.x}" y="${pos.y}" width="${pos.width}" height="${h}" rx="4" fill="${diagBg}" stroke="${diagColor}" stroke-width="1" stroke-dasharray="6 3"/><text x="${cx}" y="${cy}" font-family="Arial, Helvetica, sans-serif" font-size="12" fill="${diagText}" text-anchor="middle" dominant-baseline="middle">${label}</text></g>`;
5255
+ }
5156
5256
  renderSplitDecoration(node, pos, output) {
5157
5257
  if (node.kind !== "container") return;
5158
5258
  const gap = this.resolveSpacing(node.style.gap);
@@ -5201,7 +5301,7 @@ var SVGRenderer = class {
5201
5301
  const hasCaption = caption.length > 0;
5202
5302
  const showOuterBorder = this.parseBooleanProp(node.props.border, false);
5203
5303
  const showOuterBackground = this.parseBooleanProp(
5204
- node.props.background ?? node.props.backround,
5304
+ node.props.background,
5205
5305
  false
5206
5306
  );
5207
5307
  const showInnerBorder = this.parseBooleanProp(node.props.innerBorder, true);
@@ -5586,6 +5686,7 @@ var SVGRenderer = class {
5586
5686
  const placeholder = String(node.props.placeholder || "Select...");
5587
5687
  const iconLeftName = String(node.props.iconLeft || "").trim();
5588
5688
  const iconRightName = String(node.props.iconRight || "").trim();
5689
+ const disabled = this.parseBooleanProp(node.props.disabled, false);
5589
5690
  const labelOffset = this.getControlLabelOffset(label);
5590
5691
  const controlY = pos.y + labelOffset;
5591
5692
  const controlHeight = Math.max(16, pos.height - labelOffset);
@@ -5599,7 +5700,7 @@ var SVGRenderer = class {
5599
5700
  const chevronWidth = 20;
5600
5701
  const iconColor = this.hexToRgba(this.resolveMutedColor(), 0.8);
5601
5702
  const iconCenterY = controlY + (controlHeight - iconSize) / 2;
5602
- let svg = `<g${this.getDataNodeId(node)}>`;
5703
+ let svg = `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>`;
5603
5704
  if (label) {
5604
5705
  svg += `
5605
5706
  <text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
@@ -5648,10 +5749,11 @@ var SVGRenderer = class {
5648
5749
  renderCheckbox(node, pos) {
5649
5750
  const label = String(node.props.label || "Checkbox");
5650
5751
  const checked = String(node.props.checked || "false").toLowerCase() === "true";
5752
+ const disabled = this.parseBooleanProp(node.props.disabled, false);
5651
5753
  const controlColor = this.resolveControlColor();
5652
5754
  const checkboxSize = 18;
5653
5755
  const checkboxY = pos.y + pos.height / 2 - checkboxSize / 2;
5654
- return `<g${this.getDataNodeId(node)}>
5756
+ return `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>
5655
5757
  <rect x="${pos.x}" y="${checkboxY}"
5656
5758
  width="${checkboxSize}" height="${checkboxSize}"
5657
5759
  rx="4"
@@ -5672,10 +5774,11 @@ var SVGRenderer = class {
5672
5774
  renderRadio(node, pos) {
5673
5775
  const label = String(node.props.label || "Radio");
5674
5776
  const checked = String(node.props.checked || "false").toLowerCase() === "true";
5777
+ const disabled = this.parseBooleanProp(node.props.disabled, false);
5675
5778
  const controlColor = this.resolveControlColor();
5676
5779
  const radioSize = 16;
5677
5780
  const radioY = pos.y + pos.height / 2 - radioSize / 2;
5678
- return `<g${this.getDataNodeId(node)}>
5781
+ return `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>
5679
5782
  <circle cx="${pos.x + radioSize / 2}" cy="${radioY + radioSize / 2}"
5680
5783
  r="${radioSize / 2}"
5681
5784
  fill="${this.renderTheme.cardBg}"
@@ -5693,11 +5796,12 @@ var SVGRenderer = class {
5693
5796
  renderToggle(node, pos) {
5694
5797
  const label = String(node.props.label || "Toggle");
5695
5798
  const enabled = String(node.props.enabled || "false").toLowerCase() === "true";
5799
+ const disabled = this.parseBooleanProp(node.props.disabled, false);
5696
5800
  const controlColor = this.resolveControlColor();
5697
5801
  const toggleWidth = 40;
5698
5802
  const toggleHeight = 20;
5699
5803
  const toggleY = pos.y + pos.height / 2 - toggleHeight / 2;
5700
- return `<g${this.getDataNodeId(node)}>
5804
+ return `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>
5701
5805
  <rect x="${pos.x}" y="${toggleY}"
5702
5806
  width="${toggleWidth}" height="${toggleHeight}"
5703
5807
  rx="10"
@@ -5993,7 +6097,7 @@ var SVGRenderer = class {
5993
6097
  text-anchor="middle">${node.componentType}</text>
5994
6098
  </g>`;
5995
6099
  }
5996
- renderStatCard(node, pos) {
6100
+ renderStat(node, pos) {
5997
6101
  const title = String(node.props.title || "Metric");
5998
6102
  const value = String(node.props.value || "0");
5999
6103
  const rawCaption = String(node.props.caption || "");
@@ -6001,7 +6105,9 @@ var SVGRenderer = class {
6001
6105
  const hasCaption = caption.trim().length > 0;
6002
6106
  const iconName = String(node.props.icon || "").trim();
6003
6107
  const iconSvg = iconName ? getIcon(iconName) : null;
6004
- const accentColor = this.resolveAccentColor();
6108
+ const variant = String(node.props.variant || "default");
6109
+ const baseAccent = this.resolveAccentColor();
6110
+ const accentColor = variant !== "default" ? this.resolveVariantColor(variant, baseAccent) : baseAccent;
6005
6111
  const padding = this.resolveSpacing(node.style.padding) || 16;
6006
6112
  const innerX = pos.x + padding;
6007
6113
  const innerY = pos.y + padding;
@@ -6025,7 +6131,7 @@ var SVGRenderer = class {
6025
6131
  const visibleTitle = this.truncateTextToWidth(title, titleMaxWidth, titleSize);
6026
6132
  const visibleCaption = hasCaption ? this.truncateTextToWidth(caption, Math.max(20, innerWidth), captionSize) : "";
6027
6133
  let svg = `<g${this.getDataNodeId(node)}>
6028
- <!-- StatCard Background -->
6134
+ <!-- Stat Background -->
6029
6135
  <rect x="${pos.x}" y="${pos.y}"
6030
6136
  width="${pos.width}" height="${pos.height}"
6031
6137
  rx="8"
@@ -6668,10 +6774,11 @@ var SVGRenderer = class {
6668
6774
  buildParentContainerIndex() {
6669
6775
  this.parentContainerByChildId.clear();
6670
6776
  Object.values(this.ir.project.nodes).forEach((node) => {
6671
- if (node.kind !== "container") return;
6672
- node.children.forEach((childRef) => {
6673
- this.parentContainerByChildId.set(childRef.ref, node);
6674
- });
6777
+ if (node.kind === "container") {
6778
+ node.children.forEach((childRef) => {
6779
+ this.parentContainerByChildId.set(childRef.ref, node);
6780
+ });
6781
+ }
6675
6782
  });
6676
6783
  }
6677
6784
  escapeXml(text) {
@@ -7106,7 +7213,7 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
7106
7213
  const hasCaption = String(node.props.caption || "").trim().length > 0;
7107
7214
  const showOuterBorder = this.parseBooleanProp(node.props.border, false);
7108
7215
  const showOuterBackground = this.parseBooleanProp(
7109
- node.props.background ?? node.props.backround,
7216
+ node.props.background,
7110
7217
  false
7111
7218
  );
7112
7219
  const showInnerBorder = this.parseBooleanProp(node.props.innerBorder, true);
@@ -7221,7 +7328,7 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
7221
7328
  const variant = String(node.props.variant || "default");
7222
7329
  const accentBlock = variant === "default" ? this.renderTheme.border : this.hexToRgba(this.resolveVariantColor(variant, this.resolveAccentColor()), 0.35);
7223
7330
  const showBorder = this.parseBooleanProp(node.props.border, false);
7224
- const showBackground = this.parseBooleanProp(node.props.background ?? node.props.backround, false);
7331
+ const showBackground = this.parseBooleanProp(node.props.background, false);
7225
7332
  const radiusMap = {
7226
7333
  none: 0,
7227
7334
  sm: 4,
@@ -7287,9 +7394,9 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
7287
7394
  return svg;
7288
7395
  }
7289
7396
  /**
7290
- * Render StatCard with gray blocks instead of values
7397
+ * Render Stat with gray blocks instead of values
7291
7398
  */
7292
- renderStatCard(node, pos) {
7399
+ renderStat(node, pos) {
7293
7400
  const hasIcon = String(node.props.icon || "").trim().length > 0;
7294
7401
  const hasCaption = String(node.props.caption || "").trim().length > 0;
7295
7402
  const iconSize = 20;
@@ -8416,7 +8523,7 @@ var SketchSVGRenderer = class extends SVGRenderer {
8416
8523
  /**
8417
8524
  * Render stat card with sketch filter and Comic Sans
8418
8525
  */
8419
- renderStatCard(node, pos) {
8526
+ renderStat(node, pos) {
8420
8527
  const title = String(node.props.title || "Metric");
8421
8528
  const value = String(node.props.value || "0");
8422
8529
  const rawCaption = String(node.props.caption || "");
@@ -8424,7 +8531,9 @@ var SketchSVGRenderer = class extends SVGRenderer {
8424
8531
  const hasCaption = caption.trim().length > 0;
8425
8532
  const iconName = String(node.props.icon || "").trim();
8426
8533
  const iconSvg = iconName ? getIcon(iconName) : null;
8427
- const accentColor = this.resolveAccentColor();
8534
+ const variant = String(node.props.variant || "default");
8535
+ const baseAccent = this.resolveAccentColor();
8536
+ const accentColor = variant !== "default" ? this.resolveVariantColor(variant, baseAccent) : baseAccent;
8428
8537
  const padding = this.resolveSpacing(node.style.padding);
8429
8538
  const innerX = pos.x + padding;
8430
8539
  const innerY = pos.y + padding;
@@ -8447,7 +8556,7 @@ var SketchSVGRenderer = class extends SVGRenderer {
8447
8556
  const visibleTitle = this.truncateTextToWidth(title, titleMaxWidth, titleSize);
8448
8557
  const visibleCaption = hasCaption ? this.truncateTextToWidth(caption, Math.max(20, pos.width - padding * 2), captionSize) : "";
8449
8558
  let svg = `<g${this.getDataNodeId(node)}>
8450
- <!-- StatCard Background -->
8559
+ <!-- Stat Background -->
8451
8560
  <rect x="${pos.x}" y="${pos.y}"
8452
8561
  width="${pos.width}" height="${pos.height}"
8453
8562
  rx="8"
package/dist/index.d.cts CHANGED
@@ -267,7 +267,7 @@ interface IRScreen {
267
267
  ref: string;
268
268
  };
269
269
  }
270
- type IRNode = IRContainerNode | IRComponentNode;
270
+ type IRNode = IRContainerNode | IRComponentNode | IRInstanceNode;
271
271
  interface IRContainerNode {
272
272
  id: string;
273
273
  kind: 'container';
@@ -298,6 +298,28 @@ interface IRMeta {
298
298
  source?: string;
299
299
  nodeId?: string;
300
300
  }
301
+ /**
302
+ * Wraps an expanded user-defined component or layout instance.
303
+ * Preserves the call-site identity (nodeId) in the SVG so the canvas
304
+ * can select and edit the instance independently from its definition.
305
+ */
306
+ interface IRInstanceNode {
307
+ id: string;
308
+ kind: 'instance';
309
+ /** Name of the user-defined component or layout (e.g. "MyComp") */
310
+ definitionName: string;
311
+ /** Whether it originated from a `define Component` or `define Layout` */
312
+ definitionKind: 'component' | 'layout';
313
+ /** Props/params passed at the call site (e.g. { text: "Hello" }) */
314
+ invocationProps: Record<string, string | number>;
315
+ /** Reference to the root IR node produced by expanding the definition */
316
+ expandedRoot: {
317
+ ref: string;
318
+ };
319
+ style: IRNodeStyle;
320
+ /** meta.nodeId = SourceMap nodeId of the call-site AST node */
321
+ meta: IRMeta;
322
+ }
301
323
  declare class IRGenerator {
302
324
  private idGen;
303
325
  private nodes;
@@ -441,6 +463,13 @@ declare class LayoutEngine {
441
463
  private calculateSplit;
442
464
  private calculatePanel;
443
465
  private calculateCard;
466
+ /**
467
+ * Calculate layout for an instance node.
468
+ * The instance is a transparent wrapper — its bounding box equals the
469
+ * expanded root's bounding box. We calculate the expanded root first and
470
+ * then copy its position to the instance nodeId so the renderer can use it.
471
+ */
472
+ private calculateInstance;
444
473
  private calculateComponent;
445
474
  private resolveSpacing;
446
475
  private getSeparateSize;
@@ -568,6 +597,12 @@ interface SVGRenderOptions {
568
597
  theme?: 'light' | 'dark';
569
598
  includeLabels?: boolean;
570
599
  screenName?: string;
600
+ /**
601
+ * When true, renders visual diagnostic overlays for invalid DSL states
602
+ * (e.g. empty containers). Enable in editor/canvas mode; leave false for
603
+ * clean exports (CLI, PDF, PNG).
604
+ */
605
+ showDiagnostics?: boolean;
571
606
  }
572
607
  interface SVGComponent {
573
608
  tag: string;
@@ -641,6 +676,16 @@ declare class SVGRenderer {
641
676
  protected renderTopbar(node: IRComponentNode, pos: any): string;
642
677
  protected renderPanelBorder(node: IRNode, pos: any, output: string[]): void;
643
678
  protected renderCardBorder(node: IRNode, pos: any, output: string[]): void;
679
+ /**
680
+ * Renders a yellow warning placeholder for containers with no children.
681
+ * Only shown when `showDiagnostics` is enabled (editor/canvas mode).
682
+ */
683
+ protected renderEmptyContainerDiagnostic(pos: {
684
+ x: number;
685
+ y: number;
686
+ width: number;
687
+ height: number;
688
+ }, containerType?: string): string;
644
689
  protected renderSplitDecoration(node: IRNode, pos: any, output: string[]): void;
645
690
  protected renderTable(node: IRComponentNode, pos: any): string;
646
691
  protected renderChartPlaceholder(node: IRComponentNode, pos: any): string;
@@ -661,7 +706,7 @@ declare class SVGRenderer {
661
706
  protected renderModal(node: IRComponentNode, pos: any): string;
662
707
  protected renderList(node: IRComponentNode, pos: any): string;
663
708
  protected renderGenericComponent(node: IRComponentNode, pos: any): string;
664
- protected renderStatCard(node: IRComponentNode, pos: any): string;
709
+ protected renderStat(node: IRComponentNode, pos: any): string;
665
710
  protected renderImage(node: IRComponentNode, pos: any): string;
666
711
  protected renderBreadcrumbs(node: IRComponentNode, pos: any): string;
667
712
  protected renderSidebarMenu(node: IRComponentNode, pos: any): string;
@@ -750,7 +795,7 @@ declare class SVGRenderer {
750
795
  * Get data-node-id attribute string for SVG elements
751
796
  * Enables bidirectional selection between code and canvas
752
797
  */
753
- protected getDataNodeId(node: IRComponentNode | IRContainerNode): string;
798
+ protected getDataNodeId(node: IRComponentNode | IRContainerNode | IRInstanceNode): string;
754
799
  }
755
800
  declare function renderToSVG(ir: IRContract, layout: LayoutResult, options?: SVGRenderOptions): string;
756
801
  declare function createSVGElement(tag: string, attrs: Record<string, string | number>, children?: string[]): string;
@@ -847,9 +892,9 @@ declare class SkeletonSVGRenderer extends SVGRenderer {
847
892
  */
848
893
  protected renderTopbar(node: IRComponentNode, pos: any): string;
849
894
  /**
850
- * Render StatCard with gray blocks instead of values
895
+ * Render Stat with gray blocks instead of values
851
896
  */
852
- protected renderStatCard(node: IRComponentNode, pos: any): string;
897
+ protected renderStat(node: IRComponentNode, pos: any): string;
853
898
  /**
854
899
  * Render icon as gray square instead of hiding it
855
900
  */
@@ -992,7 +1037,7 @@ declare class SketchSVGRenderer extends SVGRenderer {
992
1037
  /**
993
1038
  * Render stat card with sketch filter and Comic Sans
994
1039
  */
995
- protected renderStatCard(node: IRComponentNode, pos: any): string;
1040
+ protected renderStat(node: IRComponentNode, pos: any): string;
996
1041
  /**
997
1042
  * Render image with sketch filter
998
1043
  */
@@ -1348,4 +1393,4 @@ declare class SourceMapResolver {
1348
1393
 
1349
1394
  declare const version = "0.0.1";
1350
1395
 
1351
- export { type AST, type ASTCell, type ASTComponent, type ASTDefinedComponent, type ASTDefinedLayout, type ASTLayout, type ASTScreen, type CapturedTokens, type CodeRange, DENSITY_TOKENS, DEVICE_PRESETS, type DesignTokens, type DevicePreset, type IRComponent, type IRComponentNode, type IRContainerNode, type IRContract, IRGenerator, type IRLayout, type IRMeta, type IRMetadata, type IRNode, type IRNodeStyle, type IRProject, type IRScreen, type IRStyle, type IRWireframe, type InsertionPoint, LayoutEngine, type LayoutPosition, type LayoutResult, type ParseDiagnosticsResult, type ParseError, type ParseResult, type ParseWireDSLWithSourceMapOptions, type ParsedComponent, type ParsedWireframe, type Position, type PositionQueryResult, type PropertySourceMap, type SVGComponent, type SVGRenderOptions, SVGRenderer, SkeletonSVGRenderer, SketchSVGRenderer, SourceMapBuilder, type SourceMapEntry, type SourceMapNodeType, SourceMapResolver, buildSVG, calculateLayout, createSVGElement, generateIR, generateStableNodeId, getTypeFromNodeId, isValidDevice, isValidNodeId, parseWireDSL, parseWireDSLWithSourceMap, renderToSVG, resolveDevicePreset, resolveGridPosition, resolveTokens, version };
1396
+ export { type AST, type ASTCell, type ASTComponent, type ASTDefinedComponent, type ASTDefinedLayout, type ASTLayout, type ASTScreen, type CapturedTokens, type CodeRange, DENSITY_TOKENS, DEVICE_PRESETS, type DesignTokens, type DevicePreset, type IRComponent, type IRComponentNode, type IRContainerNode, type IRContract, IRGenerator, type IRInstanceNode, type IRLayout, type IRMeta, type IRMetadata, type IRNode, type IRNodeStyle, type IRProject, type IRScreen, type IRStyle, type IRWireframe, type InsertionPoint, LayoutEngine, type LayoutPosition, type LayoutResult, type ParseDiagnosticsResult, type ParseError, type ParseResult, type ParseWireDSLWithSourceMapOptions, type ParsedComponent, type ParsedWireframe, type Position, type PositionQueryResult, type PropertySourceMap, type SVGComponent, type SVGRenderOptions, SVGRenderer, SkeletonSVGRenderer, SketchSVGRenderer, SourceMapBuilder, type SourceMapEntry, type SourceMapNodeType, SourceMapResolver, buildSVG, calculateLayout, createSVGElement, generateIR, generateStableNodeId, getTypeFromNodeId, isValidDevice, isValidNodeId, parseWireDSL, parseWireDSLWithSourceMap, renderToSVG, resolveDevicePreset, resolveGridPosition, resolveTokens, version };
package/dist/index.d.ts CHANGED
@@ -267,7 +267,7 @@ interface IRScreen {
267
267
  ref: string;
268
268
  };
269
269
  }
270
- type IRNode = IRContainerNode | IRComponentNode;
270
+ type IRNode = IRContainerNode | IRComponentNode | IRInstanceNode;
271
271
  interface IRContainerNode {
272
272
  id: string;
273
273
  kind: 'container';
@@ -298,6 +298,28 @@ interface IRMeta {
298
298
  source?: string;
299
299
  nodeId?: string;
300
300
  }
301
+ /**
302
+ * Wraps an expanded user-defined component or layout instance.
303
+ * Preserves the call-site identity (nodeId) in the SVG so the canvas
304
+ * can select and edit the instance independently from its definition.
305
+ */
306
+ interface IRInstanceNode {
307
+ id: string;
308
+ kind: 'instance';
309
+ /** Name of the user-defined component or layout (e.g. "MyComp") */
310
+ definitionName: string;
311
+ /** Whether it originated from a `define Component` or `define Layout` */
312
+ definitionKind: 'component' | 'layout';
313
+ /** Props/params passed at the call site (e.g. { text: "Hello" }) */
314
+ invocationProps: Record<string, string | number>;
315
+ /** Reference to the root IR node produced by expanding the definition */
316
+ expandedRoot: {
317
+ ref: string;
318
+ };
319
+ style: IRNodeStyle;
320
+ /** meta.nodeId = SourceMap nodeId of the call-site AST node */
321
+ meta: IRMeta;
322
+ }
301
323
  declare class IRGenerator {
302
324
  private idGen;
303
325
  private nodes;
@@ -441,6 +463,13 @@ declare class LayoutEngine {
441
463
  private calculateSplit;
442
464
  private calculatePanel;
443
465
  private calculateCard;
466
+ /**
467
+ * Calculate layout for an instance node.
468
+ * The instance is a transparent wrapper — its bounding box equals the
469
+ * expanded root's bounding box. We calculate the expanded root first and
470
+ * then copy its position to the instance nodeId so the renderer can use it.
471
+ */
472
+ private calculateInstance;
444
473
  private calculateComponent;
445
474
  private resolveSpacing;
446
475
  private getSeparateSize;
@@ -568,6 +597,12 @@ interface SVGRenderOptions {
568
597
  theme?: 'light' | 'dark';
569
598
  includeLabels?: boolean;
570
599
  screenName?: string;
600
+ /**
601
+ * When true, renders visual diagnostic overlays for invalid DSL states
602
+ * (e.g. empty containers). Enable in editor/canvas mode; leave false for
603
+ * clean exports (CLI, PDF, PNG).
604
+ */
605
+ showDiagnostics?: boolean;
571
606
  }
572
607
  interface SVGComponent {
573
608
  tag: string;
@@ -641,6 +676,16 @@ declare class SVGRenderer {
641
676
  protected renderTopbar(node: IRComponentNode, pos: any): string;
642
677
  protected renderPanelBorder(node: IRNode, pos: any, output: string[]): void;
643
678
  protected renderCardBorder(node: IRNode, pos: any, output: string[]): void;
679
+ /**
680
+ * Renders a yellow warning placeholder for containers with no children.
681
+ * Only shown when `showDiagnostics` is enabled (editor/canvas mode).
682
+ */
683
+ protected renderEmptyContainerDiagnostic(pos: {
684
+ x: number;
685
+ y: number;
686
+ width: number;
687
+ height: number;
688
+ }, containerType?: string): string;
644
689
  protected renderSplitDecoration(node: IRNode, pos: any, output: string[]): void;
645
690
  protected renderTable(node: IRComponentNode, pos: any): string;
646
691
  protected renderChartPlaceholder(node: IRComponentNode, pos: any): string;
@@ -661,7 +706,7 @@ declare class SVGRenderer {
661
706
  protected renderModal(node: IRComponentNode, pos: any): string;
662
707
  protected renderList(node: IRComponentNode, pos: any): string;
663
708
  protected renderGenericComponent(node: IRComponentNode, pos: any): string;
664
- protected renderStatCard(node: IRComponentNode, pos: any): string;
709
+ protected renderStat(node: IRComponentNode, pos: any): string;
665
710
  protected renderImage(node: IRComponentNode, pos: any): string;
666
711
  protected renderBreadcrumbs(node: IRComponentNode, pos: any): string;
667
712
  protected renderSidebarMenu(node: IRComponentNode, pos: any): string;
@@ -750,7 +795,7 @@ declare class SVGRenderer {
750
795
  * Get data-node-id attribute string for SVG elements
751
796
  * Enables bidirectional selection between code and canvas
752
797
  */
753
- protected getDataNodeId(node: IRComponentNode | IRContainerNode): string;
798
+ protected getDataNodeId(node: IRComponentNode | IRContainerNode | IRInstanceNode): string;
754
799
  }
755
800
  declare function renderToSVG(ir: IRContract, layout: LayoutResult, options?: SVGRenderOptions): string;
756
801
  declare function createSVGElement(tag: string, attrs: Record<string, string | number>, children?: string[]): string;
@@ -847,9 +892,9 @@ declare class SkeletonSVGRenderer extends SVGRenderer {
847
892
  */
848
893
  protected renderTopbar(node: IRComponentNode, pos: any): string;
849
894
  /**
850
- * Render StatCard with gray blocks instead of values
895
+ * Render Stat with gray blocks instead of values
851
896
  */
852
- protected renderStatCard(node: IRComponentNode, pos: any): string;
897
+ protected renderStat(node: IRComponentNode, pos: any): string;
853
898
  /**
854
899
  * Render icon as gray square instead of hiding it
855
900
  */
@@ -992,7 +1037,7 @@ declare class SketchSVGRenderer extends SVGRenderer {
992
1037
  /**
993
1038
  * Render stat card with sketch filter and Comic Sans
994
1039
  */
995
- protected renderStatCard(node: IRComponentNode, pos: any): string;
1040
+ protected renderStat(node: IRComponentNode, pos: any): string;
996
1041
  /**
997
1042
  * Render image with sketch filter
998
1043
  */
@@ -1348,4 +1393,4 @@ declare class SourceMapResolver {
1348
1393
 
1349
1394
  declare const version = "0.0.1";
1350
1395
 
1351
- export { type AST, type ASTCell, type ASTComponent, type ASTDefinedComponent, type ASTDefinedLayout, type ASTLayout, type ASTScreen, type CapturedTokens, type CodeRange, DENSITY_TOKENS, DEVICE_PRESETS, type DesignTokens, type DevicePreset, type IRComponent, type IRComponentNode, type IRContainerNode, type IRContract, IRGenerator, type IRLayout, type IRMeta, type IRMetadata, type IRNode, type IRNodeStyle, type IRProject, type IRScreen, type IRStyle, type IRWireframe, type InsertionPoint, LayoutEngine, type LayoutPosition, type LayoutResult, type ParseDiagnosticsResult, type ParseError, type ParseResult, type ParseWireDSLWithSourceMapOptions, type ParsedComponent, type ParsedWireframe, type Position, type PositionQueryResult, type PropertySourceMap, type SVGComponent, type SVGRenderOptions, SVGRenderer, SkeletonSVGRenderer, SketchSVGRenderer, SourceMapBuilder, type SourceMapEntry, type SourceMapNodeType, SourceMapResolver, buildSVG, calculateLayout, createSVGElement, generateIR, generateStableNodeId, getTypeFromNodeId, isValidDevice, isValidNodeId, parseWireDSL, parseWireDSLWithSourceMap, renderToSVG, resolveDevicePreset, resolveGridPosition, resolveTokens, version };
1396
+ export { type AST, type ASTCell, type ASTComponent, type ASTDefinedComponent, type ASTDefinedLayout, type ASTLayout, type ASTScreen, type CapturedTokens, type CodeRange, DENSITY_TOKENS, DEVICE_PRESETS, type DesignTokens, type DevicePreset, type IRComponent, type IRComponentNode, type IRContainerNode, type IRContract, IRGenerator, type IRInstanceNode, type IRLayout, type IRMeta, type IRMetadata, type IRNode, type IRNodeStyle, type IRProject, type IRScreen, type IRStyle, type IRWireframe, type InsertionPoint, LayoutEngine, type LayoutPosition, type LayoutResult, type ParseDiagnosticsResult, type ParseError, type ParseResult, type ParseWireDSLWithSourceMapOptions, type ParsedComponent, type ParsedWireframe, type Position, type PositionQueryResult, type PropertySourceMap, type SVGComponent, type SVGRenderOptions, SVGRenderer, SkeletonSVGRenderer, SketchSVGRenderer, SourceMapBuilder, type SourceMapEntry, type SourceMapNodeType, SourceMapResolver, buildSVG, calculateLayout, createSVGElement, generateIR, generateStableNodeId, getTypeFromNodeId, isValidDevice, isValidNodeId, parseWireDSL, parseWireDSLWithSourceMap, renderToSVG, resolveDevicePreset, resolveGridPosition, resolveTokens, version };
package/dist/index.js CHANGED
@@ -2199,7 +2199,21 @@ var IRComponentNodeSchema = z.object({
2199
2199
  style: IRNodeStyleSchema,
2200
2200
  meta: IRMetaSchema
2201
2201
  });
2202
- var IRNodeSchema = z.union([IRContainerNodeSchema, IRComponentNodeSchema]);
2202
+ var IRInstanceNodeSchema = z.object({
2203
+ id: z.string(),
2204
+ kind: z.literal("instance"),
2205
+ definitionName: z.string(),
2206
+ definitionKind: z.enum(["component", "layout"]),
2207
+ invocationProps: z.record(z.string(), z.union([z.string(), z.number()])),
2208
+ expandedRoot: z.object({ ref: z.string() }),
2209
+ style: IRNodeStyleSchema,
2210
+ meta: IRMetaSchema
2211
+ });
2212
+ var IRNodeSchema = z.discriminatedUnion("kind", [
2213
+ IRContainerNodeSchema,
2214
+ IRComponentNodeSchema,
2215
+ IRInstanceNodeSchema
2216
+ ]);
2203
2217
  var IRScreenSchema = z.object({
2204
2218
  id: z.string(),
2205
2219
  name: z.string(),
@@ -2422,7 +2436,7 @@ ${messages}`);
2422
2436
  const layoutChildren = layout.children;
2423
2437
  const layoutDefinition = this.definedLayouts.get(layout.layoutType);
2424
2438
  if (layoutDefinition) {
2425
- return this.expandDefinedLayout(layoutDefinition, layoutParams, layoutChildren, context);
2439
+ return this.expandDefinedLayout(layoutDefinition, layoutParams, layoutChildren, context, layout._meta?.nodeId);
2426
2440
  }
2427
2441
  const nodeId = this.idGen.generate("node");
2428
2442
  const childRefs = [];
@@ -2461,8 +2475,8 @@ ${messages}`);
2461
2475
  children: childRefs,
2462
2476
  style,
2463
2477
  meta: {
2464
- nodeId: layout._meta?.nodeId
2465
- // Pass SourceMap nodeId from AST
2478
+ // Scope nodeId per instance so each expansion gets a unique identifier
2479
+ nodeId: context?.instanceScope ? `${layout._meta?.nodeId}@${context.instanceScope}` : layout._meta?.nodeId
2466
2480
  }
2467
2481
  };
2468
2482
  this.nodes[nodeId] = containerNode;
@@ -2491,8 +2505,7 @@ ${messages}`);
2491
2505
  // Cells have no padding by default - grid gap handles spacing
2492
2506
  meta: {
2493
2507
  source: "cell",
2494
- nodeId: cell._meta?.nodeId
2495
- // Pass SourceMap nodeId from AST
2508
+ nodeId: context?.instanceScope ? `${cell._meta?.nodeId}@${context.instanceScope}` : cell._meta?.nodeId
2496
2509
  }
2497
2510
  };
2498
2511
  this.nodes[nodeId] = containerNode;
@@ -2519,7 +2532,7 @@ ${messages}`);
2519
2532
  const resolvedProps = this.resolveComponentProps(component.componentType, component.props, context);
2520
2533
  const definition = this.definedComponents.get(component.componentType);
2521
2534
  if (definition) {
2522
- return this.expandDefinedComponent(definition, resolvedProps, context);
2535
+ return this.expandDefinedComponent(definition, resolvedProps, component._meta?.nodeId, context);
2523
2536
  }
2524
2537
  const builtInComponents = /* @__PURE__ */ new Set([
2525
2538
  "Button",
@@ -2529,7 +2542,7 @@ ${messages}`);
2529
2542
  "Label",
2530
2543
  "Image",
2531
2544
  "Card",
2532
- "StatCard",
2545
+ "Stat",
2533
2546
  "Topbar",
2534
2547
  "Table",
2535
2548
  "Chart",
@@ -2565,35 +2578,49 @@ ${messages}`);
2565
2578
  props: resolvedProps,
2566
2579
  style: {},
2567
2580
  meta: {
2568
- nodeId: component._meta?.nodeId
2569
- // Pass SourceMap nodeId from AST
2581
+ // Scope nodeId per instance so each expansion gets a unique identifier
2582
+ nodeId: context?.instanceScope ? `${component._meta?.nodeId}@${context.instanceScope}` : component._meta?.nodeId
2570
2583
  }
2571
2584
  };
2572
2585
  this.nodes[nodeId] = componentNode;
2573
2586
  return nodeId;
2574
2587
  }
2575
- expandDefinedComponent(definition, invocationArgs, parentContext) {
2588
+ expandDefinedComponent(definition, invocationArgs, callSiteNodeId, parentContext) {
2576
2589
  const context = {
2577
2590
  args: invocationArgs,
2578
2591
  providedArgNames: new Set(Object.keys(invocationArgs)),
2579
2592
  usedArgNames: /* @__PURE__ */ new Set(),
2580
2593
  definitionName: definition.name,
2581
2594
  definitionKind: "component",
2582
- allowChildrenSlot: false
2595
+ allowChildrenSlot: false,
2596
+ // Scope internal nodeIds using the call-site nodeId
2597
+ instanceScope: callSiteNodeId
2583
2598
  };
2599
+ let expandedRootId = null;
2584
2600
  if (definition.body.type === "layout") {
2585
- const result = this.convertLayout(definition.body, context);
2586
- this.reportUnusedArguments(context);
2587
- return result;
2601
+ expandedRootId = this.convertLayout(definition.body, context);
2588
2602
  } else if (definition.body.type === "component") {
2589
- const result = this.convertComponent(definition.body, context);
2590
- this.reportUnusedArguments(context);
2591
- return result;
2603
+ expandedRootId = this.convertComponent(definition.body, context);
2592
2604
  } else {
2593
2605
  throw new Error(`Invalid defined component body type for "${definition.name}"`);
2594
2606
  }
2607
+ this.reportUnusedArguments(context);
2608
+ if (!expandedRootId) return null;
2609
+ const instanceNodeId = this.idGen.generate("node");
2610
+ const instanceNode = {
2611
+ id: instanceNodeId,
2612
+ kind: "instance",
2613
+ definitionName: definition.name,
2614
+ definitionKind: "component",
2615
+ invocationProps: invocationArgs,
2616
+ expandedRoot: { ref: expandedRootId },
2617
+ style: {},
2618
+ meta: { nodeId: callSiteNodeId }
2619
+ };
2620
+ this.nodes[instanceNodeId] = instanceNode;
2621
+ return instanceNodeId;
2595
2622
  }
2596
- expandDefinedLayout(definition, invocationParams, invocationChildren, parentContext) {
2623
+ expandDefinedLayout(definition, invocationParams, invocationChildren, parentContext, callSiteNodeId) {
2597
2624
  if (invocationChildren.length !== 1) {
2598
2625
  this.errors.push({
2599
2626
  type: "layout-children-arity",
@@ -2609,11 +2636,26 @@ ${messages}`);
2609
2636
  definitionName: definition.name,
2610
2637
  definitionKind: "layout",
2611
2638
  allowChildrenSlot: true,
2612
- childrenSlot: resolvedSlot
2639
+ childrenSlot: resolvedSlot,
2640
+ // Scope internal nodeIds using the call-site nodeId
2641
+ instanceScope: callSiteNodeId
2613
2642
  };
2614
- const nodeId = this.convertLayout(definition.body, context);
2643
+ const expandedRootId = this.convertLayout(definition.body, context);
2615
2644
  this.reportUnusedArguments(context);
2616
- return nodeId;
2645
+ if (!callSiteNodeId) return expandedRootId;
2646
+ const instanceNodeId = this.idGen.generate("node");
2647
+ const instanceNode = {
2648
+ id: instanceNodeId,
2649
+ kind: "instance",
2650
+ definitionName: definition.name,
2651
+ definitionKind: "layout",
2652
+ invocationProps: invocationParams,
2653
+ expandedRoot: { ref: expandedRootId },
2654
+ style: {},
2655
+ meta: { nodeId: callSiteNodeId }
2656
+ };
2657
+ this.nodes[instanceNodeId] = instanceNode;
2658
+ return instanceNodeId;
2617
2659
  }
2618
2660
  resolveChildrenSlot(slot, parentContext) {
2619
2661
  if (slot.type === "component" && slot.componentType === "Children") {
@@ -2979,6 +3021,8 @@ var LayoutEngine = class {
2979
3021
  }
2980
3022
  if (node.kind === "container") {
2981
3023
  this.calculateContainer(node, nodeId, x, y, width, height);
3024
+ } else if (node.kind === "instance") {
3025
+ this.calculateInstance(node, nodeId, x, y, width, height, parentContainerType);
2982
3026
  } else {
2983
3027
  this.calculateComponent(node, nodeId, x, y, width, height);
2984
3028
  }
@@ -3011,7 +3055,7 @@ var LayoutEngine = class {
3011
3055
  this.calculateCard(node, innerX, innerY, innerWidth, innerHeight);
3012
3056
  break;
3013
3057
  }
3014
- if (isVerticalStack || node.containerType === "card") {
3058
+ if ((isVerticalStack || node.containerType === "card") && node.children.length > 0) {
3015
3059
  let containerMaxY = y;
3016
3060
  node.children.forEach((childRef) => {
3017
3061
  const childPos = this.result[childRef.ref];
@@ -3154,6 +3198,10 @@ var LayoutEngine = class {
3154
3198
  const gap = this.resolveSpacing(node.style.gap);
3155
3199
  const padding = this.resolveSpacing(node.style.padding);
3156
3200
  let totalHeight = padding * 2;
3201
+ const EMPTY_CONTAINER_MIN_HEIGHT = 40;
3202
+ if (node.children.length === 0) {
3203
+ return Math.max(totalHeight, EMPTY_CONTAINER_MIN_HEIGHT);
3204
+ }
3157
3205
  if (node.containerType === "grid") {
3158
3206
  const columns = Number(node.params.columns) || 12;
3159
3207
  const colWidth = (availableWidth - gap * (columns - 1)) / columns;
@@ -3406,6 +3454,20 @@ var LayoutEngine = class {
3406
3454
  }
3407
3455
  });
3408
3456
  }
3457
+ /**
3458
+ * Calculate layout for an instance node.
3459
+ * The instance is a transparent wrapper — its bounding box equals the
3460
+ * expanded root's bounding box. We calculate the expanded root first and
3461
+ * then copy its position to the instance nodeId so the renderer can use it.
3462
+ */
3463
+ calculateInstance(node, nodeId, x, y, width, height, parentContainerType) {
3464
+ const expandedRootId = node.expandedRoot.ref;
3465
+ this.calculateNode(expandedRootId, x, y, width, height, parentContainerType);
3466
+ const expandedPos = this.result[expandedRootId];
3467
+ if (expandedPos) {
3468
+ this.result[nodeId] = { ...expandedPos };
3469
+ }
3470
+ }
3409
3471
  calculateComponent(node, nodeId, x, y, width, height) {
3410
3472
  if (node.kind !== "component") return;
3411
3473
  const componentWidth = Number(node.props.width) || width;
@@ -3623,7 +3685,7 @@ var LayoutEngine = class {
3623
3685
  if (node.componentType === "Textarea") return 100 + controlLabelOffset;
3624
3686
  if (node.componentType === "Modal") return 300;
3625
3687
  if (node.componentType === "Card") return 120;
3626
- if (node.componentType === "StatCard") return 120;
3688
+ if (node.componentType === "Stat") return 120;
3627
3689
  if (node.componentType === "Chart" || node.componentType === "ChartPlaceholder") return 250;
3628
3690
  if (node.componentType === "List") {
3629
3691
  const itemsFromProps = String(node.props.items || "").split(",").map((item) => item.trim()).filter(Boolean);
@@ -3674,7 +3736,10 @@ var LayoutEngine = class {
3674
3736
  const density = this.style.density || "normal";
3675
3737
  const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
3676
3738
  const textWidth = this.estimateTextWidth(text, fontSize);
3677
- return Math.max(60, Math.ceil(textWidth + (paddingX + extraPadding) * 2));
3739
+ const iconName = String(node.props.icon || "").trim();
3740
+ const iconSize = iconName ? Math.round(fontSize * 1.1) : 0;
3741
+ const iconGap = iconName ? 8 : 0;
3742
+ return Math.max(60, Math.ceil(textWidth + iconSize + iconGap + (paddingX + extraPadding) * 2));
3678
3743
  }
3679
3744
  if (node.componentType === "Label" || node.componentType === "Text") {
3680
3745
  const text = String(node.props.text || "");
@@ -3705,7 +3770,7 @@ var LayoutEngine = class {
3705
3770
  if (node.componentType === "Table") {
3706
3771
  return 400;
3707
3772
  }
3708
- if (node.componentType === "StatCard" || node.componentType === "Card") {
3773
+ if (node.componentType === "Stat" || node.componentType === "Card") {
3709
3774
  return 280;
3710
3775
  }
3711
3776
  if (node.componentType === "SidebarMenu") {
@@ -4571,7 +4636,8 @@ var SVGRenderer = class {
4571
4636
  height: options?.height || 720,
4572
4637
  theme: colorScheme,
4573
4638
  includeLabels: options?.includeLabels ?? true,
4574
- screenName: options?.screenName
4639
+ screenName: options?.screenName,
4640
+ showDiagnostics: options?.showDiagnostics ?? false
4575
4641
  };
4576
4642
  this.colorResolver = new ColorResolver();
4577
4643
  this.buildParentContainerIndex();
@@ -4671,13 +4737,30 @@ var SVGRenderer = class {
4671
4737
  if (node.containerType === "split") {
4672
4738
  this.renderSplitDecoration(node, pos, containerGroup);
4673
4739
  }
4674
- node.children.forEach((childRef) => {
4675
- this.renderNode(childRef.ref, containerGroup);
4676
- });
4740
+ if (node.children.length === 0 && this.options.showDiagnostics) {
4741
+ containerGroup.push(this.renderEmptyContainerDiagnostic(pos, node.containerType));
4742
+ } else {
4743
+ node.children.forEach((childRef) => {
4744
+ this.renderNode(childRef.ref, containerGroup);
4745
+ });
4746
+ }
4677
4747
  if (hasNodeId) {
4678
4748
  containerGroup.push("</g>");
4679
4749
  }
4680
4750
  output.push(...containerGroup);
4751
+ } else if (node.kind === "instance") {
4752
+ const instanceGroup = [];
4753
+ if (node.meta.nodeId) {
4754
+ instanceGroup.push(`<g data-node-id="${node.meta.nodeId}">`);
4755
+ }
4756
+ instanceGroup.push(
4757
+ `<rect x="${pos.x}" y="${pos.y}" width="${pos.width}" height="${pos.height}" fill="transparent" stroke="none" pointer-events="all"/>`
4758
+ );
4759
+ this.renderNode(node.expandedRoot.ref, instanceGroup);
4760
+ if (node.meta.nodeId) {
4761
+ instanceGroup.push("</g>");
4762
+ }
4763
+ output.push(...instanceGroup);
4681
4764
  } else if (node.kind === "component") {
4682
4765
  const componentSvg = this.renderComponent(node, pos);
4683
4766
  if (componentSvg) {
@@ -4743,8 +4826,8 @@ var SVGRenderer = class {
4743
4826
  return this.renderModal(node, pos);
4744
4827
  case "List":
4745
4828
  return this.renderList(node, pos);
4746
- case "StatCard":
4747
- return this.renderStatCard(node, pos);
4829
+ case "Stat":
4830
+ return this.renderStat(node, pos);
4748
4831
  case "Image":
4749
4832
  return this.renderImage(node, pos);
4750
4833
  // Icon components
@@ -4790,6 +4873,7 @@ var SVGRenderer = class {
4790
4873
  const text = String(node.props.text || "Button");
4791
4874
  const variant = String(node.props.variant || "default");
4792
4875
  const size = String(node.props.size || "md");
4876
+ const disabled = this.parseBooleanProp(node.props.disabled, false);
4793
4877
  const density = this.ir.project.style.density || "normal";
4794
4878
  const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
4795
4879
  const labelOffset = this.parseBooleanProp(node.props.labelSpace, false) ? 18 : 0;
@@ -4835,7 +4919,7 @@ var SVGRenderer = class {
4835
4919
  textX = pos.x + buttonWidth / 2;
4836
4920
  textAnchor = "middle";
4837
4921
  }
4838
- let svg = `<g${this.getDataNodeId(node)}>
4922
+ let svg = `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>
4839
4923
  <rect x="${pos.x}" y="${buttonY}"
4840
4924
  width="${buttonWidth}" height="${buttonHeight}"
4841
4925
  rx="${radius}"
@@ -4901,6 +4985,7 @@ var SVGRenderer = class {
4901
4985
  const placeholder = String(node.props.placeholder || "");
4902
4986
  const iconLeftName = String(node.props.iconLeft || "").trim();
4903
4987
  const iconRightName = String(node.props.iconRight || "").trim();
4988
+ const disabled = this.parseBooleanProp(node.props.disabled, false);
4904
4989
  const radius = this.tokens.input.radius;
4905
4990
  const fontSize = this.tokens.input.fontSize;
4906
4991
  const paddingX = this.tokens.input.paddingX;
@@ -4917,7 +5002,7 @@ var SVGRenderer = class {
4917
5002
  const textX = pos.x + (iconLeftSvg ? leftOffset : paddingX);
4918
5003
  const iconColor = this.hexToRgba(this.resolveMutedColor(), 0.8);
4919
5004
  const iconCenterY = controlY + (controlHeight - iconSize) / 2;
4920
- let svg = `<g${this.getDataNodeId(node)}>`;
5005
+ let svg = `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>`;
4921
5006
  if (label) {
4922
5007
  svg += `
4923
5008
  <text x="${pos.x + paddingX}" y="${this.getControlLabelBaselineY(pos.y)}"
@@ -4968,7 +5053,7 @@ var SVGRenderer = class {
4968
5053
  const variant = String(node.props.variant || "default");
4969
5054
  const accentColor = variant === "default" ? this.resolveAccentColor() : this.resolveVariantColor(variant, this.resolveAccentColor());
4970
5055
  const showBorder = this.parseBooleanProp(node.props.border, false);
4971
- const showBackground = this.parseBooleanProp(node.props.background ?? node.props.backround, false);
5056
+ const showBackground = this.parseBooleanProp(node.props.background, false);
4972
5057
  const radiusMap = {
4973
5058
  none: 0,
4974
5059
  sm: 4,
@@ -5107,6 +5192,21 @@ var SVGRenderer = class {
5107
5192
  </g>`;
5108
5193
  output.push(svg);
5109
5194
  }
5195
+ /**
5196
+ * Renders a yellow warning placeholder for containers with no children.
5197
+ * Only shown when `showDiagnostics` is enabled (editor/canvas mode).
5198
+ */
5199
+ renderEmptyContainerDiagnostic(pos, containerType) {
5200
+ const diagColor = "#F59E0B";
5201
+ const diagBg = "#FFFBEB";
5202
+ const diagText = "#92400E";
5203
+ const minHeight = 40;
5204
+ const h = Math.max(pos.height, minHeight);
5205
+ const cx = pos.x + pos.width / 2;
5206
+ const cy = pos.y + h / 2;
5207
+ const label = containerType ? `Empty ${containerType}` : "Empty layout";
5208
+ return `<g><rect x="${pos.x}" y="${pos.y}" width="${pos.width}" height="${h}" rx="4" fill="${diagBg}" stroke="${diagColor}" stroke-width="1" stroke-dasharray="6 3"/><text x="${cx}" y="${cy}" font-family="Arial, Helvetica, sans-serif" font-size="12" fill="${diagText}" text-anchor="middle" dominant-baseline="middle">${label}</text></g>`;
5209
+ }
5110
5210
  renderSplitDecoration(node, pos, output) {
5111
5211
  if (node.kind !== "container") return;
5112
5212
  const gap = this.resolveSpacing(node.style.gap);
@@ -5155,7 +5255,7 @@ var SVGRenderer = class {
5155
5255
  const hasCaption = caption.length > 0;
5156
5256
  const showOuterBorder = this.parseBooleanProp(node.props.border, false);
5157
5257
  const showOuterBackground = this.parseBooleanProp(
5158
- node.props.background ?? node.props.backround,
5258
+ node.props.background,
5159
5259
  false
5160
5260
  );
5161
5261
  const showInnerBorder = this.parseBooleanProp(node.props.innerBorder, true);
@@ -5540,6 +5640,7 @@ var SVGRenderer = class {
5540
5640
  const placeholder = String(node.props.placeholder || "Select...");
5541
5641
  const iconLeftName = String(node.props.iconLeft || "").trim();
5542
5642
  const iconRightName = String(node.props.iconRight || "").trim();
5643
+ const disabled = this.parseBooleanProp(node.props.disabled, false);
5543
5644
  const labelOffset = this.getControlLabelOffset(label);
5544
5645
  const controlY = pos.y + labelOffset;
5545
5646
  const controlHeight = Math.max(16, pos.height - labelOffset);
@@ -5553,7 +5654,7 @@ var SVGRenderer = class {
5553
5654
  const chevronWidth = 20;
5554
5655
  const iconColor = this.hexToRgba(this.resolveMutedColor(), 0.8);
5555
5656
  const iconCenterY = controlY + (controlHeight - iconSize) / 2;
5556
- let svg = `<g${this.getDataNodeId(node)}>`;
5657
+ let svg = `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>`;
5557
5658
  if (label) {
5558
5659
  svg += `
5559
5660
  <text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
@@ -5602,10 +5703,11 @@ var SVGRenderer = class {
5602
5703
  renderCheckbox(node, pos) {
5603
5704
  const label = String(node.props.label || "Checkbox");
5604
5705
  const checked = String(node.props.checked || "false").toLowerCase() === "true";
5706
+ const disabled = this.parseBooleanProp(node.props.disabled, false);
5605
5707
  const controlColor = this.resolveControlColor();
5606
5708
  const checkboxSize = 18;
5607
5709
  const checkboxY = pos.y + pos.height / 2 - checkboxSize / 2;
5608
- return `<g${this.getDataNodeId(node)}>
5710
+ return `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>
5609
5711
  <rect x="${pos.x}" y="${checkboxY}"
5610
5712
  width="${checkboxSize}" height="${checkboxSize}"
5611
5713
  rx="4"
@@ -5626,10 +5728,11 @@ var SVGRenderer = class {
5626
5728
  renderRadio(node, pos) {
5627
5729
  const label = String(node.props.label || "Radio");
5628
5730
  const checked = String(node.props.checked || "false").toLowerCase() === "true";
5731
+ const disabled = this.parseBooleanProp(node.props.disabled, false);
5629
5732
  const controlColor = this.resolveControlColor();
5630
5733
  const radioSize = 16;
5631
5734
  const radioY = pos.y + pos.height / 2 - radioSize / 2;
5632
- return `<g${this.getDataNodeId(node)}>
5735
+ return `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>
5633
5736
  <circle cx="${pos.x + radioSize / 2}" cy="${radioY + radioSize / 2}"
5634
5737
  r="${radioSize / 2}"
5635
5738
  fill="${this.renderTheme.cardBg}"
@@ -5647,11 +5750,12 @@ var SVGRenderer = class {
5647
5750
  renderToggle(node, pos) {
5648
5751
  const label = String(node.props.label || "Toggle");
5649
5752
  const enabled = String(node.props.enabled || "false").toLowerCase() === "true";
5753
+ const disabled = this.parseBooleanProp(node.props.disabled, false);
5650
5754
  const controlColor = this.resolveControlColor();
5651
5755
  const toggleWidth = 40;
5652
5756
  const toggleHeight = 20;
5653
5757
  const toggleY = pos.y + pos.height / 2 - toggleHeight / 2;
5654
- return `<g${this.getDataNodeId(node)}>
5758
+ return `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>
5655
5759
  <rect x="${pos.x}" y="${toggleY}"
5656
5760
  width="${toggleWidth}" height="${toggleHeight}"
5657
5761
  rx="10"
@@ -5947,7 +6051,7 @@ var SVGRenderer = class {
5947
6051
  text-anchor="middle">${node.componentType}</text>
5948
6052
  </g>`;
5949
6053
  }
5950
- renderStatCard(node, pos) {
6054
+ renderStat(node, pos) {
5951
6055
  const title = String(node.props.title || "Metric");
5952
6056
  const value = String(node.props.value || "0");
5953
6057
  const rawCaption = String(node.props.caption || "");
@@ -5955,7 +6059,9 @@ var SVGRenderer = class {
5955
6059
  const hasCaption = caption.trim().length > 0;
5956
6060
  const iconName = String(node.props.icon || "").trim();
5957
6061
  const iconSvg = iconName ? getIcon(iconName) : null;
5958
- const accentColor = this.resolveAccentColor();
6062
+ const variant = String(node.props.variant || "default");
6063
+ const baseAccent = this.resolveAccentColor();
6064
+ const accentColor = variant !== "default" ? this.resolveVariantColor(variant, baseAccent) : baseAccent;
5959
6065
  const padding = this.resolveSpacing(node.style.padding) || 16;
5960
6066
  const innerX = pos.x + padding;
5961
6067
  const innerY = pos.y + padding;
@@ -5979,7 +6085,7 @@ var SVGRenderer = class {
5979
6085
  const visibleTitle = this.truncateTextToWidth(title, titleMaxWidth, titleSize);
5980
6086
  const visibleCaption = hasCaption ? this.truncateTextToWidth(caption, Math.max(20, innerWidth), captionSize) : "";
5981
6087
  let svg = `<g${this.getDataNodeId(node)}>
5982
- <!-- StatCard Background -->
6088
+ <!-- Stat Background -->
5983
6089
  <rect x="${pos.x}" y="${pos.y}"
5984
6090
  width="${pos.width}" height="${pos.height}"
5985
6091
  rx="8"
@@ -6622,10 +6728,11 @@ var SVGRenderer = class {
6622
6728
  buildParentContainerIndex() {
6623
6729
  this.parentContainerByChildId.clear();
6624
6730
  Object.values(this.ir.project.nodes).forEach((node) => {
6625
- if (node.kind !== "container") return;
6626
- node.children.forEach((childRef) => {
6627
- this.parentContainerByChildId.set(childRef.ref, node);
6628
- });
6731
+ if (node.kind === "container") {
6732
+ node.children.forEach((childRef) => {
6733
+ this.parentContainerByChildId.set(childRef.ref, node);
6734
+ });
6735
+ }
6629
6736
  });
6630
6737
  }
6631
6738
  escapeXml(text) {
@@ -7060,7 +7167,7 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
7060
7167
  const hasCaption = String(node.props.caption || "").trim().length > 0;
7061
7168
  const showOuterBorder = this.parseBooleanProp(node.props.border, false);
7062
7169
  const showOuterBackground = this.parseBooleanProp(
7063
- node.props.background ?? node.props.backround,
7170
+ node.props.background,
7064
7171
  false
7065
7172
  );
7066
7173
  const showInnerBorder = this.parseBooleanProp(node.props.innerBorder, true);
@@ -7175,7 +7282,7 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
7175
7282
  const variant = String(node.props.variant || "default");
7176
7283
  const accentBlock = variant === "default" ? this.renderTheme.border : this.hexToRgba(this.resolveVariantColor(variant, this.resolveAccentColor()), 0.35);
7177
7284
  const showBorder = this.parseBooleanProp(node.props.border, false);
7178
- const showBackground = this.parseBooleanProp(node.props.background ?? node.props.backround, false);
7285
+ const showBackground = this.parseBooleanProp(node.props.background, false);
7179
7286
  const radiusMap = {
7180
7287
  none: 0,
7181
7288
  sm: 4,
@@ -7241,9 +7348,9 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
7241
7348
  return svg;
7242
7349
  }
7243
7350
  /**
7244
- * Render StatCard with gray blocks instead of values
7351
+ * Render Stat with gray blocks instead of values
7245
7352
  */
7246
- renderStatCard(node, pos) {
7353
+ renderStat(node, pos) {
7247
7354
  const hasIcon = String(node.props.icon || "").trim().length > 0;
7248
7355
  const hasCaption = String(node.props.caption || "").trim().length > 0;
7249
7356
  const iconSize = 20;
@@ -8370,7 +8477,7 @@ var SketchSVGRenderer = class extends SVGRenderer {
8370
8477
  /**
8371
8478
  * Render stat card with sketch filter and Comic Sans
8372
8479
  */
8373
- renderStatCard(node, pos) {
8480
+ renderStat(node, pos) {
8374
8481
  const title = String(node.props.title || "Metric");
8375
8482
  const value = String(node.props.value || "0");
8376
8483
  const rawCaption = String(node.props.caption || "");
@@ -8378,7 +8485,9 @@ var SketchSVGRenderer = class extends SVGRenderer {
8378
8485
  const hasCaption = caption.trim().length > 0;
8379
8486
  const iconName = String(node.props.icon || "").trim();
8380
8487
  const iconSvg = iconName ? getIcon(iconName) : null;
8381
- const accentColor = this.resolveAccentColor();
8488
+ const variant = String(node.props.variant || "default");
8489
+ const baseAccent = this.resolveAccentColor();
8490
+ const accentColor = variant !== "default" ? this.resolveVariantColor(variant, baseAccent) : baseAccent;
8382
8491
  const padding = this.resolveSpacing(node.style.padding);
8383
8492
  const innerX = pos.x + padding;
8384
8493
  const innerY = pos.y + padding;
@@ -8401,7 +8510,7 @@ var SketchSVGRenderer = class extends SVGRenderer {
8401
8510
  const visibleTitle = this.truncateTextToWidth(title, titleMaxWidth, titleSize);
8402
8511
  const visibleCaption = hasCaption ? this.truncateTextToWidth(caption, Math.max(20, pos.width - padding * 2), captionSize) : "";
8403
8512
  let svg = `<g${this.getDataNodeId(node)}>
8404
- <!-- StatCard Background -->
8513
+ <!-- Stat Background -->
8405
8514
  <rect x="${pos.x}" y="${pos.y}"
8406
8515
  width="${pos.width}" height="${pos.height}"
8407
8516
  rx="8"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wire-dsl/engine",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "WireDSL engine - Parser, IR generator, layout engine, and SVG renderer (browser-safe, pure JS/TS)",
5
5
  "type": "module",
6
6
  "exports": {
@@ -32,7 +32,7 @@
32
32
  "dependencies": {
33
33
  "chevrotain": "11.1.1",
34
34
  "zod": "4.3.6",
35
- "@wire-dsl/language-support": "0.4.0"
35
+ "@wire-dsl/language-support": "0.5.0"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/node": "25.2.0",