@wire-dsl/engine 0.4.1 → 0.6.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
@@ -2220,8 +2220,8 @@ var IRStyleSchema = import_zod.z.object({
2220
2220
  var IRNodeStyleSchema = import_zod.z.object({
2221
2221
  padding: import_zod.z.string().optional(),
2222
2222
  gap: import_zod.z.string().optional(),
2223
- align: import_zod.z.enum(["left", "center", "right", "justify"]).optional(),
2224
- justify: import_zod.z.enum(["start", "center", "end"]).optional(),
2223
+ justify: import_zod.z.enum(["start", "center", "end", "stretch", "spaceBetween", "spaceAround"]).optional(),
2224
+ align: import_zod.z.enum(["start", "center", "end"]).optional(),
2225
2225
  background: import_zod.z.string().optional()
2226
2226
  });
2227
2227
  var IRMetaSchema = import_zod.z.object({
@@ -2507,6 +2507,9 @@ ${messages}`);
2507
2507
  if (layoutParams.gap !== void 0) {
2508
2508
  style.gap = String(layoutParams.gap);
2509
2509
  }
2510
+ if (layoutParams.justify !== void 0) {
2511
+ style.justify = layoutParams.justify;
2512
+ }
2510
2513
  if (layoutParams.align !== void 0) {
2511
2514
  style.align = layoutParams.align;
2512
2515
  }
@@ -2588,7 +2591,7 @@ ${messages}`);
2588
2591
  "Label",
2589
2592
  "Image",
2590
2593
  "Card",
2591
- "StatCard",
2594
+ "Stat",
2592
2595
  "Topbar",
2593
2596
  "Table",
2594
2597
  "Chart",
@@ -3153,8 +3156,9 @@ var LayoutEngine = class {
3153
3156
  }
3154
3157
  });
3155
3158
  } else {
3156
- const align = node.style.align || "justify";
3157
- if (align === "justify") {
3159
+ const justify = node.style.justify || "stretch";
3160
+ const crossAlign = node.style.align || "start";
3161
+ if (justify === "stretch") {
3158
3162
  let currentX = x;
3159
3163
  const childWidth = this.calculateChildWidth(children.length, width, gap);
3160
3164
  let stackHeight = 0;
@@ -3176,43 +3180,44 @@ var LayoutEngine = class {
3176
3180
  });
3177
3181
  } else {
3178
3182
  const childWidths = [];
3183
+ const childHeights = [];
3179
3184
  const explicitHeightFlags = [];
3180
- const blockButtonIndices = /* @__PURE__ */ new Set();
3185
+ const flexIndices = /* @__PURE__ */ new Set();
3181
3186
  let stackHeight = 0;
3182
3187
  children.forEach((childRef, index) => {
3183
3188
  const childNode = this.nodes[childRef.ref];
3184
- let childWidth = this.getIntrinsicComponentWidth(childNode);
3185
- let childHeight = this.getComponentHeight();
3186
3189
  const hasExplicitHeight = childNode?.kind === "component" && !!childNode.props.height;
3187
3190
  const hasExplicitWidth = childNode?.kind === "component" && !!childNode.props.width;
3188
3191
  const isBlockButton = childNode?.kind === "component" && childNode.componentType === "Button" && !hasExplicitWidth && this.parseBooleanProp(childNode.props.block, false);
3189
- if (isBlockButton) {
3192
+ const isFlexContainer = !hasExplicitWidth && childNode?.kind === "container" && !this.containerHasIntrinsicWidth(childNode);
3193
+ let childWidth;
3194
+ if (isBlockButton || isFlexContainer) {
3190
3195
  childWidth = 0;
3191
- blockButtonIndices.add(index);
3196
+ flexIndices.add(index);
3192
3197
  } else if (hasExplicitWidth) {
3193
3198
  childWidth = Number(childNode.props.width);
3194
- }
3195
- if (hasExplicitHeight) {
3196
- childHeight = Number(childNode.props.height);
3199
+ } else {
3200
+ childWidth = this.getIntrinsicWidth(childNode, width);
3197
3201
  }
3198
3202
  childWidths.push(childWidth);
3203
+ childHeights.push(this.getComponentHeight());
3199
3204
  explicitHeightFlags.push(hasExplicitHeight);
3200
3205
  });
3201
3206
  const totalGapWidth = gap * Math.max(0, children.length - 1);
3202
- if (blockButtonIndices.size > 0) {
3207
+ if (flexIndices.size > 0) {
3203
3208
  const fixedWidth = childWidths.reduce((sum, w, idx) => {
3204
- return blockButtonIndices.has(idx) ? sum : sum + w;
3209
+ return flexIndices.has(idx) ? sum : sum + w;
3205
3210
  }, 0);
3206
3211
  const remainingWidth = width - totalGapWidth - fixedWidth;
3207
- const widthPerBlock = Math.max(1, remainingWidth / blockButtonIndices.size);
3208
- blockButtonIndices.forEach((index) => {
3209
- childWidths[index] = widthPerBlock;
3212
+ const widthPerFlex = Math.max(1, remainingWidth / flexIndices.size);
3213
+ flexIndices.forEach((idx) => {
3214
+ childWidths[idx] = widthPerFlex;
3210
3215
  });
3211
3216
  }
3212
3217
  children.forEach((childRef, index) => {
3213
3218
  const childNode = this.nodes[childRef.ref];
3214
- let childHeight = this.getComponentHeight();
3215
3219
  const childWidth = childWidths[index];
3220
+ let childHeight = this.getComponentHeight();
3216
3221
  if (explicitHeightFlags[index] && childNode?.kind === "component") {
3217
3222
  childHeight = Number(childNode.props.height);
3218
3223
  } else if (childNode?.kind === "container") {
@@ -3220,21 +3225,37 @@ var LayoutEngine = class {
3220
3225
  } else if (childNode?.kind === "component") {
3221
3226
  childHeight = this.getIntrinsicComponentHeight(childNode, childWidth);
3222
3227
  }
3228
+ childHeights[index] = childHeight;
3223
3229
  stackHeight = Math.max(stackHeight, childHeight);
3224
3230
  });
3225
3231
  const totalChildWidth = childWidths.reduce((sum, w) => sum + w, 0);
3226
3232
  const totalContentWidth = totalChildWidth + totalGapWidth;
3227
3233
  let startX = x;
3228
- if (align === "center") {
3234
+ let dynamicGap = gap;
3235
+ if (justify === "center") {
3229
3236
  startX = x + (width - totalContentWidth) / 2;
3230
- } else if (align === "right") {
3237
+ } else if (justify === "end") {
3231
3238
  startX = x + width - totalContentWidth;
3239
+ } else if (justify === "spaceBetween") {
3240
+ startX = x;
3241
+ dynamicGap = children.length > 1 ? (width - totalChildWidth) / (children.length - 1) : 0;
3242
+ } else if (justify === "spaceAround") {
3243
+ const spacing = children.length > 0 ? (width - totalChildWidth) / children.length : 0;
3244
+ startX = x + spacing / 2;
3245
+ dynamicGap = spacing;
3232
3246
  }
3233
3247
  let currentX = startX;
3234
3248
  children.forEach((childRef, index) => {
3235
3249
  const childWidth = childWidths[index];
3236
- this.calculateNode(childRef.ref, currentX, y, childWidth, stackHeight, "stack");
3237
- currentX += childWidth + gap;
3250
+ const childHeight = childHeights[index];
3251
+ let childY = y;
3252
+ if (crossAlign === "center") {
3253
+ childY = y + Math.round((stackHeight - childHeight) / 2);
3254
+ } else if (crossAlign === "end") {
3255
+ childY = y + stackHeight - childHeight;
3256
+ }
3257
+ this.calculateNode(childRef.ref, currentX, childY, childWidth, childHeight, "stack");
3258
+ currentX += childWidth + dynamicGap;
3238
3259
  });
3239
3260
  }
3240
3261
  }
@@ -3731,7 +3752,7 @@ var LayoutEngine = class {
3731
3752
  if (node.componentType === "Textarea") return 100 + controlLabelOffset;
3732
3753
  if (node.componentType === "Modal") return 300;
3733
3754
  if (node.componentType === "Card") return 120;
3734
- if (node.componentType === "StatCard") return 120;
3755
+ if (node.componentType === "Stat") return 120;
3735
3756
  if (node.componentType === "Chart" || node.componentType === "ChartPlaceholder") return 250;
3736
3757
  if (node.componentType === "List") {
3737
3758
  const itemsFromProps = String(node.props.items || "").split(",").map((item) => item.trim()).filter(Boolean);
@@ -3757,6 +3778,47 @@ var LayoutEngine = class {
3757
3778
  getControlLabelOffset(label) {
3758
3779
  return label.trim().length > 0 ? 18 : 0;
3759
3780
  }
3781
+ /**
3782
+ * Returns true when a container's width can be calculated from its children
3783
+ * (i.e. it is a horizontal non-stretch stack). False means the container
3784
+ * behaves like `flex-grow:1` and should absorb remaining space.
3785
+ */
3786
+ containerHasIntrinsicWidth(node) {
3787
+ if (node.kind !== "container") return false;
3788
+ return node.containerType === "stack" && String(node.params.direction || "vertical") === "horizontal" && (node.style.justify || "stretch") !== "stretch";
3789
+ }
3790
+ /**
3791
+ * Returns the natural (intrinsic) width of any node — component or container.
3792
+ * For horizontal non-stretch containers the width is the sum of their children's
3793
+ * intrinsic widths plus gaps, capped at `availableWidth`. All other containers
3794
+ * are assumed to take the full available width (they stretch or grow).
3795
+ */
3796
+ getIntrinsicWidth(node, availableWidth) {
3797
+ if (!node) return 120;
3798
+ if (node.kind === "component") {
3799
+ return this.getIntrinsicComponentWidth(node);
3800
+ }
3801
+ if (node.kind === "container") {
3802
+ if (this.containerHasIntrinsicWidth(node)) {
3803
+ const gap = this.resolveSpacing(node.style.gap);
3804
+ const padding = this.resolveSpacing(node.style.padding);
3805
+ const innerAvailable = Math.max(0, availableWidth - padding * 2);
3806
+ const children = node.children ?? [];
3807
+ let total = padding * 2;
3808
+ children.forEach((childRef, idx) => {
3809
+ const child = this.nodes[childRef.ref];
3810
+ total += this.getIntrinsicWidth(child, innerAvailable);
3811
+ if (idx < children.length - 1) total += gap;
3812
+ });
3813
+ return Math.min(total, availableWidth);
3814
+ }
3815
+ return availableWidth;
3816
+ }
3817
+ if (node.kind === "instance") {
3818
+ return availableWidth;
3819
+ }
3820
+ return 120;
3821
+ }
3760
3822
  getIntrinsicComponentWidth(node) {
3761
3823
  if (!node || node.kind !== "component") {
3762
3824
  return 120;
@@ -3782,7 +3844,10 @@ var LayoutEngine = class {
3782
3844
  const density = this.style.density || "normal";
3783
3845
  const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
3784
3846
  const textWidth = this.estimateTextWidth(text, fontSize);
3785
- return Math.max(60, Math.ceil(textWidth + (paddingX + extraPadding) * 2));
3847
+ const iconName = String(node.props.icon || "").trim();
3848
+ const iconSize = iconName ? Math.round(fontSize * 1.1) : 0;
3849
+ const iconGap = iconName ? 8 : 0;
3850
+ return Math.max(60, Math.ceil(textWidth + iconSize + iconGap + (paddingX + extraPadding) * 2));
3786
3851
  }
3787
3852
  if (node.componentType === "Label" || node.componentType === "Text") {
3788
3853
  const text = String(node.props.text || "");
@@ -3813,7 +3878,7 @@ var LayoutEngine = class {
3813
3878
  if (node.componentType === "Table") {
3814
3879
  return 400;
3815
3880
  }
3816
- if (node.componentType === "StatCard" || node.componentType === "Card") {
3881
+ if (node.componentType === "Stat" || node.componentType === "Card") {
3817
3882
  return 280;
3818
3883
  }
3819
3884
  if (node.componentType === "SidebarMenu") {
@@ -4869,8 +4934,8 @@ var SVGRenderer = class {
4869
4934
  return this.renderModal(node, pos);
4870
4935
  case "List":
4871
4936
  return this.renderList(node, pos);
4872
- case "StatCard":
4873
- return this.renderStatCard(node, pos);
4937
+ case "Stat":
4938
+ return this.renderStat(node, pos);
4874
4939
  case "Image":
4875
4940
  return this.renderImage(node, pos);
4876
4941
  // Icon components
@@ -4916,6 +4981,7 @@ var SVGRenderer = class {
4916
4981
  const text = String(node.props.text || "Button");
4917
4982
  const variant = String(node.props.variant || "default");
4918
4983
  const size = String(node.props.size || "md");
4984
+ const disabled = this.parseBooleanProp(node.props.disabled, false);
4919
4985
  const density = this.ir.project.style.density || "normal";
4920
4986
  const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
4921
4987
  const labelOffset = this.parseBooleanProp(node.props.labelSpace, false) ? 18 : 0;
@@ -4961,7 +5027,7 @@ var SVGRenderer = class {
4961
5027
  textX = pos.x + buttonWidth / 2;
4962
5028
  textAnchor = "middle";
4963
5029
  }
4964
- let svg = `<g${this.getDataNodeId(node)}>
5030
+ let svg = `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>
4965
5031
  <rect x="${pos.x}" y="${buttonY}"
4966
5032
  width="${buttonWidth}" height="${buttonHeight}"
4967
5033
  rx="${radius}"
@@ -5027,6 +5093,7 @@ var SVGRenderer = class {
5027
5093
  const placeholder = String(node.props.placeholder || "");
5028
5094
  const iconLeftName = String(node.props.iconLeft || "").trim();
5029
5095
  const iconRightName = String(node.props.iconRight || "").trim();
5096
+ const disabled = this.parseBooleanProp(node.props.disabled, false);
5030
5097
  const radius = this.tokens.input.radius;
5031
5098
  const fontSize = this.tokens.input.fontSize;
5032
5099
  const paddingX = this.tokens.input.paddingX;
@@ -5043,7 +5110,7 @@ var SVGRenderer = class {
5043
5110
  const textX = pos.x + (iconLeftSvg ? leftOffset : paddingX);
5044
5111
  const iconColor = this.hexToRgba(this.resolveMutedColor(), 0.8);
5045
5112
  const iconCenterY = controlY + (controlHeight - iconSize) / 2;
5046
- let svg = `<g${this.getDataNodeId(node)}>`;
5113
+ let svg = `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>`;
5047
5114
  if (label) {
5048
5115
  svg += `
5049
5116
  <text x="${pos.x + paddingX}" y="${this.getControlLabelBaselineY(pos.y)}"
@@ -5612,7 +5679,8 @@ var SVGRenderer = class {
5612
5679
  const fontSize = this.tokens.text.fontSize;
5613
5680
  const lineHeightPx = Math.ceil(fontSize * this.tokens.text.lineHeight);
5614
5681
  const lines = this.wrapTextToLines(text, pos.width, fontSize);
5615
- const firstLineY = pos.y + fontSize;
5682
+ const totalTextHeight = lines.length * lineHeightPx;
5683
+ const firstLineY = pos.y + Math.round(Math.max(0, (pos.height - totalTextHeight) / 2)) + fontSize;
5616
5684
  const tspans = lines.map(
5617
5685
  (line, index) => `<tspan x="${pos.x}" dy="${index === 0 ? 0 : lineHeightPx}">${this.escapeXml(line)}</tspan>`
5618
5686
  ).join("");
@@ -5625,8 +5693,9 @@ var SVGRenderer = class {
5625
5693
  }
5626
5694
  renderLabel(node, pos) {
5627
5695
  const text = String(node.props.text || "Label");
5696
+ const textY = pos.y + Math.round(pos.height / 2) + 4;
5628
5697
  return `<g${this.getDataNodeId(node)}>
5629
- <text x="${pos.x}" y="${pos.y + 12}"
5698
+ <text x="${pos.x}" y="${textY}"
5630
5699
  font-family="Arial, Helvetica, sans-serif"
5631
5700
  font-size="12"
5632
5701
  fill="${this.renderTheme.textMuted}">${this.escapeXml(text)}</text>
@@ -5681,6 +5750,7 @@ var SVGRenderer = class {
5681
5750
  const placeholder = String(node.props.placeholder || "Select...");
5682
5751
  const iconLeftName = String(node.props.iconLeft || "").trim();
5683
5752
  const iconRightName = String(node.props.iconRight || "").trim();
5753
+ const disabled = this.parseBooleanProp(node.props.disabled, false);
5684
5754
  const labelOffset = this.getControlLabelOffset(label);
5685
5755
  const controlY = pos.y + labelOffset;
5686
5756
  const controlHeight = Math.max(16, pos.height - labelOffset);
@@ -5694,7 +5764,7 @@ var SVGRenderer = class {
5694
5764
  const chevronWidth = 20;
5695
5765
  const iconColor = this.hexToRgba(this.resolveMutedColor(), 0.8);
5696
5766
  const iconCenterY = controlY + (controlHeight - iconSize) / 2;
5697
- let svg = `<g${this.getDataNodeId(node)}>`;
5767
+ let svg = `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>`;
5698
5768
  if (label) {
5699
5769
  svg += `
5700
5770
  <text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
@@ -5743,10 +5813,11 @@ var SVGRenderer = class {
5743
5813
  renderCheckbox(node, pos) {
5744
5814
  const label = String(node.props.label || "Checkbox");
5745
5815
  const checked = String(node.props.checked || "false").toLowerCase() === "true";
5816
+ const disabled = this.parseBooleanProp(node.props.disabled, false);
5746
5817
  const controlColor = this.resolveControlColor();
5747
5818
  const checkboxSize = 18;
5748
5819
  const checkboxY = pos.y + pos.height / 2 - checkboxSize / 2;
5749
- return `<g${this.getDataNodeId(node)}>
5820
+ return `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>
5750
5821
  <rect x="${pos.x}" y="${checkboxY}"
5751
5822
  width="${checkboxSize}" height="${checkboxSize}"
5752
5823
  rx="4"
@@ -5767,10 +5838,11 @@ var SVGRenderer = class {
5767
5838
  renderRadio(node, pos) {
5768
5839
  const label = String(node.props.label || "Radio");
5769
5840
  const checked = String(node.props.checked || "false").toLowerCase() === "true";
5841
+ const disabled = this.parseBooleanProp(node.props.disabled, false);
5770
5842
  const controlColor = this.resolveControlColor();
5771
5843
  const radioSize = 16;
5772
5844
  const radioY = pos.y + pos.height / 2 - radioSize / 2;
5773
- return `<g${this.getDataNodeId(node)}>
5845
+ return `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>
5774
5846
  <circle cx="${pos.x + radioSize / 2}" cy="${radioY + radioSize / 2}"
5775
5847
  r="${radioSize / 2}"
5776
5848
  fill="${this.renderTheme.cardBg}"
@@ -5788,11 +5860,12 @@ var SVGRenderer = class {
5788
5860
  renderToggle(node, pos) {
5789
5861
  const label = String(node.props.label || "Toggle");
5790
5862
  const enabled = String(node.props.enabled || "false").toLowerCase() === "true";
5863
+ const disabled = this.parseBooleanProp(node.props.disabled, false);
5791
5864
  const controlColor = this.resolveControlColor();
5792
5865
  const toggleWidth = 40;
5793
5866
  const toggleHeight = 20;
5794
5867
  const toggleY = pos.y + pos.height / 2 - toggleHeight / 2;
5795
- return `<g${this.getDataNodeId(node)}>
5868
+ return `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>
5796
5869
  <rect x="${pos.x}" y="${toggleY}"
5797
5870
  width="${toggleWidth}" height="${toggleHeight}"
5798
5871
  rx="10"
@@ -6088,7 +6161,7 @@ var SVGRenderer = class {
6088
6161
  text-anchor="middle">${node.componentType}</text>
6089
6162
  </g>`;
6090
6163
  }
6091
- renderStatCard(node, pos) {
6164
+ renderStat(node, pos) {
6092
6165
  const title = String(node.props.title || "Metric");
6093
6166
  const value = String(node.props.value || "0");
6094
6167
  const rawCaption = String(node.props.caption || "");
@@ -6096,7 +6169,9 @@ var SVGRenderer = class {
6096
6169
  const hasCaption = caption.trim().length > 0;
6097
6170
  const iconName = String(node.props.icon || "").trim();
6098
6171
  const iconSvg = iconName ? getIcon(iconName) : null;
6099
- const accentColor = this.resolveAccentColor();
6172
+ const variant = String(node.props.variant || "default");
6173
+ const baseAccent = this.resolveAccentColor();
6174
+ const accentColor = variant !== "default" ? this.resolveVariantColor(variant, baseAccent) : baseAccent;
6100
6175
  const padding = this.resolveSpacing(node.style.padding) || 16;
6101
6176
  const innerX = pos.x + padding;
6102
6177
  const innerY = pos.y + padding;
@@ -6120,7 +6195,7 @@ var SVGRenderer = class {
6120
6195
  const visibleTitle = this.truncateTextToWidth(title, titleMaxWidth, titleSize);
6121
6196
  const visibleCaption = hasCaption ? this.truncateTextToWidth(caption, Math.max(20, innerWidth), captionSize) : "";
6122
6197
  let svg = `<g${this.getDataNodeId(node)}>
6123
- <!-- StatCard Background -->
6198
+ <!-- Stat Background -->
6124
6199
  <rect x="${pos.x}" y="${pos.y}"
6125
6200
  width="${pos.width}" height="${pos.height}"
6126
6201
  rx="8"
@@ -6757,8 +6832,8 @@ var SVGRenderer = class {
6757
6832
  return false;
6758
6833
  }
6759
6834
  const direction = String(parent.params.direction || "vertical");
6760
- const align = parent.style.align || "justify";
6761
- return direction === "horizontal" && align === "justify";
6835
+ const justify = parent.style.justify || "stretch";
6836
+ return direction === "horizontal" && justify === "stretch";
6762
6837
  }
6763
6838
  buildParentContainerIndex() {
6764
6839
  this.parentContainerByChildId.clear();
@@ -7383,9 +7458,9 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
7383
7458
  return svg;
7384
7459
  }
7385
7460
  /**
7386
- * Render StatCard with gray blocks instead of values
7461
+ * Render Stat with gray blocks instead of values
7387
7462
  */
7388
- renderStatCard(node, pos) {
7463
+ renderStat(node, pos) {
7389
7464
  const hasIcon = String(node.props.icon || "").trim().length > 0;
7390
7465
  const hasCaption = String(node.props.caption || "").trim().length > 0;
7391
7466
  const iconSize = 20;
@@ -8512,7 +8587,7 @@ var SketchSVGRenderer = class extends SVGRenderer {
8512
8587
  /**
8513
8588
  * Render stat card with sketch filter and Comic Sans
8514
8589
  */
8515
- renderStatCard(node, pos) {
8590
+ renderStat(node, pos) {
8516
8591
  const title = String(node.props.title || "Metric");
8517
8592
  const value = String(node.props.value || "0");
8518
8593
  const rawCaption = String(node.props.caption || "");
@@ -8520,7 +8595,9 @@ var SketchSVGRenderer = class extends SVGRenderer {
8520
8595
  const hasCaption = caption.trim().length > 0;
8521
8596
  const iconName = String(node.props.icon || "").trim();
8522
8597
  const iconSvg = iconName ? getIcon(iconName) : null;
8523
- const accentColor = this.resolveAccentColor();
8598
+ const variant = String(node.props.variant || "default");
8599
+ const baseAccent = this.resolveAccentColor();
8600
+ const accentColor = variant !== "default" ? this.resolveVariantColor(variant, baseAccent) : baseAccent;
8524
8601
  const padding = this.resolveSpacing(node.style.padding);
8525
8602
  const innerX = pos.x + padding;
8526
8603
  const innerY = pos.y + padding;
@@ -8543,7 +8620,7 @@ var SketchSVGRenderer = class extends SVGRenderer {
8543
8620
  const visibleTitle = this.truncateTextToWidth(title, titleMaxWidth, titleSize);
8544
8621
  const visibleCaption = hasCaption ? this.truncateTextToWidth(caption, Math.max(20, pos.width - padding * 2), captionSize) : "";
8545
8622
  let svg = `<g${this.getDataNodeId(node)}>
8546
- <!-- StatCard Background -->
8623
+ <!-- Stat Background -->
8547
8624
  <rect x="${pos.x}" y="${pos.y}"
8548
8625
  width="${pos.width}" height="${pos.height}"
8549
8626
  rx="8"
package/dist/index.d.cts CHANGED
@@ -290,8 +290,8 @@ interface IRComponentNode {
290
290
  interface IRNodeStyle {
291
291
  padding?: string;
292
292
  gap?: string;
293
- align?: 'left' | 'center' | 'right' | 'justify';
294
- justify?: 'start' | 'center' | 'end';
293
+ justify?: 'start' | 'center' | 'end' | 'stretch' | 'spaceBetween' | 'spaceAround';
294
+ align?: 'start' | 'center' | 'end';
295
295
  background?: string;
296
296
  }
297
297
  interface IRMeta {
@@ -480,6 +480,19 @@ declare class LayoutEngine {
480
480
  private wrapTextToLines;
481
481
  private getIntrinsicComponentHeight;
482
482
  private getControlLabelOffset;
483
+ /**
484
+ * Returns true when a container's width can be calculated from its children
485
+ * (i.e. it is a horizontal non-stretch stack). False means the container
486
+ * behaves like `flex-grow:1` and should absorb remaining space.
487
+ */
488
+ private containerHasIntrinsicWidth;
489
+ /**
490
+ * Returns the natural (intrinsic) width of any node — component or container.
491
+ * For horizontal non-stretch containers the width is the sum of their children's
492
+ * intrinsic widths plus gaps, capped at `availableWidth`. All other containers
493
+ * are assumed to take the full available width (they stretch or grow).
494
+ */
495
+ private getIntrinsicWidth;
483
496
  private getIntrinsicComponentWidth;
484
497
  private calculateChildHeight;
485
498
  private calculateChildWidth;
@@ -706,7 +719,7 @@ declare class SVGRenderer {
706
719
  protected renderModal(node: IRComponentNode, pos: any): string;
707
720
  protected renderList(node: IRComponentNode, pos: any): string;
708
721
  protected renderGenericComponent(node: IRComponentNode, pos: any): string;
709
- protected renderStatCard(node: IRComponentNode, pos: any): string;
722
+ protected renderStat(node: IRComponentNode, pos: any): string;
710
723
  protected renderImage(node: IRComponentNode, pos: any): string;
711
724
  protected renderBreadcrumbs(node: IRComponentNode, pos: any): string;
712
725
  protected renderSidebarMenu(node: IRComponentNode, pos: any): string;
@@ -892,9 +905,9 @@ declare class SkeletonSVGRenderer extends SVGRenderer {
892
905
  */
893
906
  protected renderTopbar(node: IRComponentNode, pos: any): string;
894
907
  /**
895
- * Render StatCard with gray blocks instead of values
908
+ * Render Stat with gray blocks instead of values
896
909
  */
897
- protected renderStatCard(node: IRComponentNode, pos: any): string;
910
+ protected renderStat(node: IRComponentNode, pos: any): string;
898
911
  /**
899
912
  * Render icon as gray square instead of hiding it
900
913
  */
@@ -1037,7 +1050,7 @@ declare class SketchSVGRenderer extends SVGRenderer {
1037
1050
  /**
1038
1051
  * Render stat card with sketch filter and Comic Sans
1039
1052
  */
1040
- protected renderStatCard(node: IRComponentNode, pos: any): string;
1053
+ protected renderStat(node: IRComponentNode, pos: any): string;
1041
1054
  /**
1042
1055
  * Render image with sketch filter
1043
1056
  */
package/dist/index.d.ts CHANGED
@@ -290,8 +290,8 @@ interface IRComponentNode {
290
290
  interface IRNodeStyle {
291
291
  padding?: string;
292
292
  gap?: string;
293
- align?: 'left' | 'center' | 'right' | 'justify';
294
- justify?: 'start' | 'center' | 'end';
293
+ justify?: 'start' | 'center' | 'end' | 'stretch' | 'spaceBetween' | 'spaceAround';
294
+ align?: 'start' | 'center' | 'end';
295
295
  background?: string;
296
296
  }
297
297
  interface IRMeta {
@@ -480,6 +480,19 @@ declare class LayoutEngine {
480
480
  private wrapTextToLines;
481
481
  private getIntrinsicComponentHeight;
482
482
  private getControlLabelOffset;
483
+ /**
484
+ * Returns true when a container's width can be calculated from its children
485
+ * (i.e. it is a horizontal non-stretch stack). False means the container
486
+ * behaves like `flex-grow:1` and should absorb remaining space.
487
+ */
488
+ private containerHasIntrinsicWidth;
489
+ /**
490
+ * Returns the natural (intrinsic) width of any node — component or container.
491
+ * For horizontal non-stretch containers the width is the sum of their children's
492
+ * intrinsic widths plus gaps, capped at `availableWidth`. All other containers
493
+ * are assumed to take the full available width (they stretch or grow).
494
+ */
495
+ private getIntrinsicWidth;
483
496
  private getIntrinsicComponentWidth;
484
497
  private calculateChildHeight;
485
498
  private calculateChildWidth;
@@ -706,7 +719,7 @@ declare class SVGRenderer {
706
719
  protected renderModal(node: IRComponentNode, pos: any): string;
707
720
  protected renderList(node: IRComponentNode, pos: any): string;
708
721
  protected renderGenericComponent(node: IRComponentNode, pos: any): string;
709
- protected renderStatCard(node: IRComponentNode, pos: any): string;
722
+ protected renderStat(node: IRComponentNode, pos: any): string;
710
723
  protected renderImage(node: IRComponentNode, pos: any): string;
711
724
  protected renderBreadcrumbs(node: IRComponentNode, pos: any): string;
712
725
  protected renderSidebarMenu(node: IRComponentNode, pos: any): string;
@@ -892,9 +905,9 @@ declare class SkeletonSVGRenderer extends SVGRenderer {
892
905
  */
893
906
  protected renderTopbar(node: IRComponentNode, pos: any): string;
894
907
  /**
895
- * Render StatCard with gray blocks instead of values
908
+ * Render Stat with gray blocks instead of values
896
909
  */
897
- protected renderStatCard(node: IRComponentNode, pos: any): string;
910
+ protected renderStat(node: IRComponentNode, pos: any): string;
898
911
  /**
899
912
  * Render icon as gray square instead of hiding it
900
913
  */
@@ -1037,7 +1050,7 @@ declare class SketchSVGRenderer extends SVGRenderer {
1037
1050
  /**
1038
1051
  * Render stat card with sketch filter and Comic Sans
1039
1052
  */
1040
- protected renderStatCard(node: IRComponentNode, pos: any): string;
1053
+ protected renderStat(node: IRComponentNode, pos: any): string;
1041
1054
  /**
1042
1055
  * Render image with sketch filter
1043
1056
  */
package/dist/index.js CHANGED
@@ -2174,8 +2174,8 @@ var IRStyleSchema = z.object({
2174
2174
  var IRNodeStyleSchema = z.object({
2175
2175
  padding: z.string().optional(),
2176
2176
  gap: z.string().optional(),
2177
- align: z.enum(["left", "center", "right", "justify"]).optional(),
2178
- justify: z.enum(["start", "center", "end"]).optional(),
2177
+ justify: z.enum(["start", "center", "end", "stretch", "spaceBetween", "spaceAround"]).optional(),
2178
+ align: z.enum(["start", "center", "end"]).optional(),
2179
2179
  background: z.string().optional()
2180
2180
  });
2181
2181
  var IRMetaSchema = z.object({
@@ -2461,6 +2461,9 @@ ${messages}`);
2461
2461
  if (layoutParams.gap !== void 0) {
2462
2462
  style.gap = String(layoutParams.gap);
2463
2463
  }
2464
+ if (layoutParams.justify !== void 0) {
2465
+ style.justify = layoutParams.justify;
2466
+ }
2464
2467
  if (layoutParams.align !== void 0) {
2465
2468
  style.align = layoutParams.align;
2466
2469
  }
@@ -2542,7 +2545,7 @@ ${messages}`);
2542
2545
  "Label",
2543
2546
  "Image",
2544
2547
  "Card",
2545
- "StatCard",
2548
+ "Stat",
2546
2549
  "Topbar",
2547
2550
  "Table",
2548
2551
  "Chart",
@@ -3107,8 +3110,9 @@ var LayoutEngine = class {
3107
3110
  }
3108
3111
  });
3109
3112
  } else {
3110
- const align = node.style.align || "justify";
3111
- if (align === "justify") {
3113
+ const justify = node.style.justify || "stretch";
3114
+ const crossAlign = node.style.align || "start";
3115
+ if (justify === "stretch") {
3112
3116
  let currentX = x;
3113
3117
  const childWidth = this.calculateChildWidth(children.length, width, gap);
3114
3118
  let stackHeight = 0;
@@ -3130,43 +3134,44 @@ var LayoutEngine = class {
3130
3134
  });
3131
3135
  } else {
3132
3136
  const childWidths = [];
3137
+ const childHeights = [];
3133
3138
  const explicitHeightFlags = [];
3134
- const blockButtonIndices = /* @__PURE__ */ new Set();
3139
+ const flexIndices = /* @__PURE__ */ new Set();
3135
3140
  let stackHeight = 0;
3136
3141
  children.forEach((childRef, index) => {
3137
3142
  const childNode = this.nodes[childRef.ref];
3138
- let childWidth = this.getIntrinsicComponentWidth(childNode);
3139
- let childHeight = this.getComponentHeight();
3140
3143
  const hasExplicitHeight = childNode?.kind === "component" && !!childNode.props.height;
3141
3144
  const hasExplicitWidth = childNode?.kind === "component" && !!childNode.props.width;
3142
3145
  const isBlockButton = childNode?.kind === "component" && childNode.componentType === "Button" && !hasExplicitWidth && this.parseBooleanProp(childNode.props.block, false);
3143
- if (isBlockButton) {
3146
+ const isFlexContainer = !hasExplicitWidth && childNode?.kind === "container" && !this.containerHasIntrinsicWidth(childNode);
3147
+ let childWidth;
3148
+ if (isBlockButton || isFlexContainer) {
3144
3149
  childWidth = 0;
3145
- blockButtonIndices.add(index);
3150
+ flexIndices.add(index);
3146
3151
  } else if (hasExplicitWidth) {
3147
3152
  childWidth = Number(childNode.props.width);
3148
- }
3149
- if (hasExplicitHeight) {
3150
- childHeight = Number(childNode.props.height);
3153
+ } else {
3154
+ childWidth = this.getIntrinsicWidth(childNode, width);
3151
3155
  }
3152
3156
  childWidths.push(childWidth);
3157
+ childHeights.push(this.getComponentHeight());
3153
3158
  explicitHeightFlags.push(hasExplicitHeight);
3154
3159
  });
3155
3160
  const totalGapWidth = gap * Math.max(0, children.length - 1);
3156
- if (blockButtonIndices.size > 0) {
3161
+ if (flexIndices.size > 0) {
3157
3162
  const fixedWidth = childWidths.reduce((sum, w, idx) => {
3158
- return blockButtonIndices.has(idx) ? sum : sum + w;
3163
+ return flexIndices.has(idx) ? sum : sum + w;
3159
3164
  }, 0);
3160
3165
  const remainingWidth = width - totalGapWidth - fixedWidth;
3161
- const widthPerBlock = Math.max(1, remainingWidth / blockButtonIndices.size);
3162
- blockButtonIndices.forEach((index) => {
3163
- childWidths[index] = widthPerBlock;
3166
+ const widthPerFlex = Math.max(1, remainingWidth / flexIndices.size);
3167
+ flexIndices.forEach((idx) => {
3168
+ childWidths[idx] = widthPerFlex;
3164
3169
  });
3165
3170
  }
3166
3171
  children.forEach((childRef, index) => {
3167
3172
  const childNode = this.nodes[childRef.ref];
3168
- let childHeight = this.getComponentHeight();
3169
3173
  const childWidth = childWidths[index];
3174
+ let childHeight = this.getComponentHeight();
3170
3175
  if (explicitHeightFlags[index] && childNode?.kind === "component") {
3171
3176
  childHeight = Number(childNode.props.height);
3172
3177
  } else if (childNode?.kind === "container") {
@@ -3174,21 +3179,37 @@ var LayoutEngine = class {
3174
3179
  } else if (childNode?.kind === "component") {
3175
3180
  childHeight = this.getIntrinsicComponentHeight(childNode, childWidth);
3176
3181
  }
3182
+ childHeights[index] = childHeight;
3177
3183
  stackHeight = Math.max(stackHeight, childHeight);
3178
3184
  });
3179
3185
  const totalChildWidth = childWidths.reduce((sum, w) => sum + w, 0);
3180
3186
  const totalContentWidth = totalChildWidth + totalGapWidth;
3181
3187
  let startX = x;
3182
- if (align === "center") {
3188
+ let dynamicGap = gap;
3189
+ if (justify === "center") {
3183
3190
  startX = x + (width - totalContentWidth) / 2;
3184
- } else if (align === "right") {
3191
+ } else if (justify === "end") {
3185
3192
  startX = x + width - totalContentWidth;
3193
+ } else if (justify === "spaceBetween") {
3194
+ startX = x;
3195
+ dynamicGap = children.length > 1 ? (width - totalChildWidth) / (children.length - 1) : 0;
3196
+ } else if (justify === "spaceAround") {
3197
+ const spacing = children.length > 0 ? (width - totalChildWidth) / children.length : 0;
3198
+ startX = x + spacing / 2;
3199
+ dynamicGap = spacing;
3186
3200
  }
3187
3201
  let currentX = startX;
3188
3202
  children.forEach((childRef, index) => {
3189
3203
  const childWidth = childWidths[index];
3190
- this.calculateNode(childRef.ref, currentX, y, childWidth, stackHeight, "stack");
3191
- currentX += childWidth + gap;
3204
+ const childHeight = childHeights[index];
3205
+ let childY = y;
3206
+ if (crossAlign === "center") {
3207
+ childY = y + Math.round((stackHeight - childHeight) / 2);
3208
+ } else if (crossAlign === "end") {
3209
+ childY = y + stackHeight - childHeight;
3210
+ }
3211
+ this.calculateNode(childRef.ref, currentX, childY, childWidth, childHeight, "stack");
3212
+ currentX += childWidth + dynamicGap;
3192
3213
  });
3193
3214
  }
3194
3215
  }
@@ -3685,7 +3706,7 @@ var LayoutEngine = class {
3685
3706
  if (node.componentType === "Textarea") return 100 + controlLabelOffset;
3686
3707
  if (node.componentType === "Modal") return 300;
3687
3708
  if (node.componentType === "Card") return 120;
3688
- if (node.componentType === "StatCard") return 120;
3709
+ if (node.componentType === "Stat") return 120;
3689
3710
  if (node.componentType === "Chart" || node.componentType === "ChartPlaceholder") return 250;
3690
3711
  if (node.componentType === "List") {
3691
3712
  const itemsFromProps = String(node.props.items || "").split(",").map((item) => item.trim()).filter(Boolean);
@@ -3711,6 +3732,47 @@ var LayoutEngine = class {
3711
3732
  getControlLabelOffset(label) {
3712
3733
  return label.trim().length > 0 ? 18 : 0;
3713
3734
  }
3735
+ /**
3736
+ * Returns true when a container's width can be calculated from its children
3737
+ * (i.e. it is a horizontal non-stretch stack). False means the container
3738
+ * behaves like `flex-grow:1` and should absorb remaining space.
3739
+ */
3740
+ containerHasIntrinsicWidth(node) {
3741
+ if (node.kind !== "container") return false;
3742
+ return node.containerType === "stack" && String(node.params.direction || "vertical") === "horizontal" && (node.style.justify || "stretch") !== "stretch";
3743
+ }
3744
+ /**
3745
+ * Returns the natural (intrinsic) width of any node — component or container.
3746
+ * For horizontal non-stretch containers the width is the sum of their children's
3747
+ * intrinsic widths plus gaps, capped at `availableWidth`. All other containers
3748
+ * are assumed to take the full available width (they stretch or grow).
3749
+ */
3750
+ getIntrinsicWidth(node, availableWidth) {
3751
+ if (!node) return 120;
3752
+ if (node.kind === "component") {
3753
+ return this.getIntrinsicComponentWidth(node);
3754
+ }
3755
+ if (node.kind === "container") {
3756
+ if (this.containerHasIntrinsicWidth(node)) {
3757
+ const gap = this.resolveSpacing(node.style.gap);
3758
+ const padding = this.resolveSpacing(node.style.padding);
3759
+ const innerAvailable = Math.max(0, availableWidth - padding * 2);
3760
+ const children = node.children ?? [];
3761
+ let total = padding * 2;
3762
+ children.forEach((childRef, idx) => {
3763
+ const child = this.nodes[childRef.ref];
3764
+ total += this.getIntrinsicWidth(child, innerAvailable);
3765
+ if (idx < children.length - 1) total += gap;
3766
+ });
3767
+ return Math.min(total, availableWidth);
3768
+ }
3769
+ return availableWidth;
3770
+ }
3771
+ if (node.kind === "instance") {
3772
+ return availableWidth;
3773
+ }
3774
+ return 120;
3775
+ }
3714
3776
  getIntrinsicComponentWidth(node) {
3715
3777
  if (!node || node.kind !== "component") {
3716
3778
  return 120;
@@ -3736,7 +3798,10 @@ var LayoutEngine = class {
3736
3798
  const density = this.style.density || "normal";
3737
3799
  const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
3738
3800
  const textWidth = this.estimateTextWidth(text, fontSize);
3739
- return Math.max(60, Math.ceil(textWidth + (paddingX + extraPadding) * 2));
3801
+ const iconName = String(node.props.icon || "").trim();
3802
+ const iconSize = iconName ? Math.round(fontSize * 1.1) : 0;
3803
+ const iconGap = iconName ? 8 : 0;
3804
+ return Math.max(60, Math.ceil(textWidth + iconSize + iconGap + (paddingX + extraPadding) * 2));
3740
3805
  }
3741
3806
  if (node.componentType === "Label" || node.componentType === "Text") {
3742
3807
  const text = String(node.props.text || "");
@@ -3767,7 +3832,7 @@ var LayoutEngine = class {
3767
3832
  if (node.componentType === "Table") {
3768
3833
  return 400;
3769
3834
  }
3770
- if (node.componentType === "StatCard" || node.componentType === "Card") {
3835
+ if (node.componentType === "Stat" || node.componentType === "Card") {
3771
3836
  return 280;
3772
3837
  }
3773
3838
  if (node.componentType === "SidebarMenu") {
@@ -4823,8 +4888,8 @@ var SVGRenderer = class {
4823
4888
  return this.renderModal(node, pos);
4824
4889
  case "List":
4825
4890
  return this.renderList(node, pos);
4826
- case "StatCard":
4827
- return this.renderStatCard(node, pos);
4891
+ case "Stat":
4892
+ return this.renderStat(node, pos);
4828
4893
  case "Image":
4829
4894
  return this.renderImage(node, pos);
4830
4895
  // Icon components
@@ -4870,6 +4935,7 @@ var SVGRenderer = class {
4870
4935
  const text = String(node.props.text || "Button");
4871
4936
  const variant = String(node.props.variant || "default");
4872
4937
  const size = String(node.props.size || "md");
4938
+ const disabled = this.parseBooleanProp(node.props.disabled, false);
4873
4939
  const density = this.ir.project.style.density || "normal";
4874
4940
  const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
4875
4941
  const labelOffset = this.parseBooleanProp(node.props.labelSpace, false) ? 18 : 0;
@@ -4915,7 +4981,7 @@ var SVGRenderer = class {
4915
4981
  textX = pos.x + buttonWidth / 2;
4916
4982
  textAnchor = "middle";
4917
4983
  }
4918
- let svg = `<g${this.getDataNodeId(node)}>
4984
+ let svg = `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>
4919
4985
  <rect x="${pos.x}" y="${buttonY}"
4920
4986
  width="${buttonWidth}" height="${buttonHeight}"
4921
4987
  rx="${radius}"
@@ -4981,6 +5047,7 @@ var SVGRenderer = class {
4981
5047
  const placeholder = String(node.props.placeholder || "");
4982
5048
  const iconLeftName = String(node.props.iconLeft || "").trim();
4983
5049
  const iconRightName = String(node.props.iconRight || "").trim();
5050
+ const disabled = this.parseBooleanProp(node.props.disabled, false);
4984
5051
  const radius = this.tokens.input.radius;
4985
5052
  const fontSize = this.tokens.input.fontSize;
4986
5053
  const paddingX = this.tokens.input.paddingX;
@@ -4997,7 +5064,7 @@ var SVGRenderer = class {
4997
5064
  const textX = pos.x + (iconLeftSvg ? leftOffset : paddingX);
4998
5065
  const iconColor = this.hexToRgba(this.resolveMutedColor(), 0.8);
4999
5066
  const iconCenterY = controlY + (controlHeight - iconSize) / 2;
5000
- let svg = `<g${this.getDataNodeId(node)}>`;
5067
+ let svg = `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>`;
5001
5068
  if (label) {
5002
5069
  svg += `
5003
5070
  <text x="${pos.x + paddingX}" y="${this.getControlLabelBaselineY(pos.y)}"
@@ -5566,7 +5633,8 @@ var SVGRenderer = class {
5566
5633
  const fontSize = this.tokens.text.fontSize;
5567
5634
  const lineHeightPx = Math.ceil(fontSize * this.tokens.text.lineHeight);
5568
5635
  const lines = this.wrapTextToLines(text, pos.width, fontSize);
5569
- const firstLineY = pos.y + fontSize;
5636
+ const totalTextHeight = lines.length * lineHeightPx;
5637
+ const firstLineY = pos.y + Math.round(Math.max(0, (pos.height - totalTextHeight) / 2)) + fontSize;
5570
5638
  const tspans = lines.map(
5571
5639
  (line, index) => `<tspan x="${pos.x}" dy="${index === 0 ? 0 : lineHeightPx}">${this.escapeXml(line)}</tspan>`
5572
5640
  ).join("");
@@ -5579,8 +5647,9 @@ var SVGRenderer = class {
5579
5647
  }
5580
5648
  renderLabel(node, pos) {
5581
5649
  const text = String(node.props.text || "Label");
5650
+ const textY = pos.y + Math.round(pos.height / 2) + 4;
5582
5651
  return `<g${this.getDataNodeId(node)}>
5583
- <text x="${pos.x}" y="${pos.y + 12}"
5652
+ <text x="${pos.x}" y="${textY}"
5584
5653
  font-family="Arial, Helvetica, sans-serif"
5585
5654
  font-size="12"
5586
5655
  fill="${this.renderTheme.textMuted}">${this.escapeXml(text)}</text>
@@ -5635,6 +5704,7 @@ var SVGRenderer = class {
5635
5704
  const placeholder = String(node.props.placeholder || "Select...");
5636
5705
  const iconLeftName = String(node.props.iconLeft || "").trim();
5637
5706
  const iconRightName = String(node.props.iconRight || "").trim();
5707
+ const disabled = this.parseBooleanProp(node.props.disabled, false);
5638
5708
  const labelOffset = this.getControlLabelOffset(label);
5639
5709
  const controlY = pos.y + labelOffset;
5640
5710
  const controlHeight = Math.max(16, pos.height - labelOffset);
@@ -5648,7 +5718,7 @@ var SVGRenderer = class {
5648
5718
  const chevronWidth = 20;
5649
5719
  const iconColor = this.hexToRgba(this.resolveMutedColor(), 0.8);
5650
5720
  const iconCenterY = controlY + (controlHeight - iconSize) / 2;
5651
- let svg = `<g${this.getDataNodeId(node)}>`;
5721
+ let svg = `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>`;
5652
5722
  if (label) {
5653
5723
  svg += `
5654
5724
  <text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
@@ -5697,10 +5767,11 @@ var SVGRenderer = class {
5697
5767
  renderCheckbox(node, pos) {
5698
5768
  const label = String(node.props.label || "Checkbox");
5699
5769
  const checked = String(node.props.checked || "false").toLowerCase() === "true";
5770
+ const disabled = this.parseBooleanProp(node.props.disabled, false);
5700
5771
  const controlColor = this.resolveControlColor();
5701
5772
  const checkboxSize = 18;
5702
5773
  const checkboxY = pos.y + pos.height / 2 - checkboxSize / 2;
5703
- return `<g${this.getDataNodeId(node)}>
5774
+ return `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>
5704
5775
  <rect x="${pos.x}" y="${checkboxY}"
5705
5776
  width="${checkboxSize}" height="${checkboxSize}"
5706
5777
  rx="4"
@@ -5721,10 +5792,11 @@ var SVGRenderer = class {
5721
5792
  renderRadio(node, pos) {
5722
5793
  const label = String(node.props.label || "Radio");
5723
5794
  const checked = String(node.props.checked || "false").toLowerCase() === "true";
5795
+ const disabled = this.parseBooleanProp(node.props.disabled, false);
5724
5796
  const controlColor = this.resolveControlColor();
5725
5797
  const radioSize = 16;
5726
5798
  const radioY = pos.y + pos.height / 2 - radioSize / 2;
5727
- return `<g${this.getDataNodeId(node)}>
5799
+ return `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>
5728
5800
  <circle cx="${pos.x + radioSize / 2}" cy="${radioY + radioSize / 2}"
5729
5801
  r="${radioSize / 2}"
5730
5802
  fill="${this.renderTheme.cardBg}"
@@ -5742,11 +5814,12 @@ var SVGRenderer = class {
5742
5814
  renderToggle(node, pos) {
5743
5815
  const label = String(node.props.label || "Toggle");
5744
5816
  const enabled = String(node.props.enabled || "false").toLowerCase() === "true";
5817
+ const disabled = this.parseBooleanProp(node.props.disabled, false);
5745
5818
  const controlColor = this.resolveControlColor();
5746
5819
  const toggleWidth = 40;
5747
5820
  const toggleHeight = 20;
5748
5821
  const toggleY = pos.y + pos.height / 2 - toggleHeight / 2;
5749
- return `<g${this.getDataNodeId(node)}>
5822
+ return `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>
5750
5823
  <rect x="${pos.x}" y="${toggleY}"
5751
5824
  width="${toggleWidth}" height="${toggleHeight}"
5752
5825
  rx="10"
@@ -6042,7 +6115,7 @@ var SVGRenderer = class {
6042
6115
  text-anchor="middle">${node.componentType}</text>
6043
6116
  </g>`;
6044
6117
  }
6045
- renderStatCard(node, pos) {
6118
+ renderStat(node, pos) {
6046
6119
  const title = String(node.props.title || "Metric");
6047
6120
  const value = String(node.props.value || "0");
6048
6121
  const rawCaption = String(node.props.caption || "");
@@ -6050,7 +6123,9 @@ var SVGRenderer = class {
6050
6123
  const hasCaption = caption.trim().length > 0;
6051
6124
  const iconName = String(node.props.icon || "").trim();
6052
6125
  const iconSvg = iconName ? getIcon(iconName) : null;
6053
- const accentColor = this.resolveAccentColor();
6126
+ const variant = String(node.props.variant || "default");
6127
+ const baseAccent = this.resolveAccentColor();
6128
+ const accentColor = variant !== "default" ? this.resolveVariantColor(variant, baseAccent) : baseAccent;
6054
6129
  const padding = this.resolveSpacing(node.style.padding) || 16;
6055
6130
  const innerX = pos.x + padding;
6056
6131
  const innerY = pos.y + padding;
@@ -6074,7 +6149,7 @@ var SVGRenderer = class {
6074
6149
  const visibleTitle = this.truncateTextToWidth(title, titleMaxWidth, titleSize);
6075
6150
  const visibleCaption = hasCaption ? this.truncateTextToWidth(caption, Math.max(20, innerWidth), captionSize) : "";
6076
6151
  let svg = `<g${this.getDataNodeId(node)}>
6077
- <!-- StatCard Background -->
6152
+ <!-- Stat Background -->
6078
6153
  <rect x="${pos.x}" y="${pos.y}"
6079
6154
  width="${pos.width}" height="${pos.height}"
6080
6155
  rx="8"
@@ -6711,8 +6786,8 @@ var SVGRenderer = class {
6711
6786
  return false;
6712
6787
  }
6713
6788
  const direction = String(parent.params.direction || "vertical");
6714
- const align = parent.style.align || "justify";
6715
- return direction === "horizontal" && align === "justify";
6789
+ const justify = parent.style.justify || "stretch";
6790
+ return direction === "horizontal" && justify === "stretch";
6716
6791
  }
6717
6792
  buildParentContainerIndex() {
6718
6793
  this.parentContainerByChildId.clear();
@@ -7337,9 +7412,9 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
7337
7412
  return svg;
7338
7413
  }
7339
7414
  /**
7340
- * Render StatCard with gray blocks instead of values
7415
+ * Render Stat with gray blocks instead of values
7341
7416
  */
7342
- renderStatCard(node, pos) {
7417
+ renderStat(node, pos) {
7343
7418
  const hasIcon = String(node.props.icon || "").trim().length > 0;
7344
7419
  const hasCaption = String(node.props.caption || "").trim().length > 0;
7345
7420
  const iconSize = 20;
@@ -8466,7 +8541,7 @@ var SketchSVGRenderer = class extends SVGRenderer {
8466
8541
  /**
8467
8542
  * Render stat card with sketch filter and Comic Sans
8468
8543
  */
8469
- renderStatCard(node, pos) {
8544
+ renderStat(node, pos) {
8470
8545
  const title = String(node.props.title || "Metric");
8471
8546
  const value = String(node.props.value || "0");
8472
8547
  const rawCaption = String(node.props.caption || "");
@@ -8474,7 +8549,9 @@ var SketchSVGRenderer = class extends SVGRenderer {
8474
8549
  const hasCaption = caption.trim().length > 0;
8475
8550
  const iconName = String(node.props.icon || "").trim();
8476
8551
  const iconSvg = iconName ? getIcon(iconName) : null;
8477
- const accentColor = this.resolveAccentColor();
8552
+ const variant = String(node.props.variant || "default");
8553
+ const baseAccent = this.resolveAccentColor();
8554
+ const accentColor = variant !== "default" ? this.resolveVariantColor(variant, baseAccent) : baseAccent;
8478
8555
  const padding = this.resolveSpacing(node.style.padding);
8479
8556
  const innerX = pos.x + padding;
8480
8557
  const innerY = pos.y + padding;
@@ -8497,7 +8574,7 @@ var SketchSVGRenderer = class extends SVGRenderer {
8497
8574
  const visibleTitle = this.truncateTextToWidth(title, titleMaxWidth, titleSize);
8498
8575
  const visibleCaption = hasCaption ? this.truncateTextToWidth(caption, Math.max(20, pos.width - padding * 2), captionSize) : "";
8499
8576
  let svg = `<g${this.getDataNodeId(node)}>
8500
- <!-- StatCard Background -->
8577
+ <!-- Stat Background -->
8501
8578
  <rect x="${pos.x}" y="${pos.y}"
8502
8579
  width="${pos.width}" height="${pos.height}"
8503
8580
  rx="8"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wire-dsl/engine",
3
- "version": "0.4.1",
3
+ "version": "0.6.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.6.0"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/node": "25.2.0",