@wire-dsl/engine 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1498,12 +1498,47 @@ var DEVICE_PRESETS = {
1498
1498
  category: "print",
1499
1499
  description: "A4 portrait at 96 DPI"
1500
1500
  },
1501
- a4: {
1502
- name: "Print A4",
1503
- width: 794,
1504
- minHeight: 1123,
1501
+ "mobile-landscape": {
1502
+ name: "Mobile Landscape",
1503
+ width: 812,
1504
+ minHeight: 375,
1505
+ category: "mobile",
1506
+ description: "Mobile landscape viewport"
1507
+ },
1508
+ "mobile-min": {
1509
+ name: "Mobile Minimum",
1510
+ width: 375,
1511
+ minHeight: 375,
1512
+ category: "mobile",
1513
+ description: "Mobile minimum viewport"
1514
+ },
1515
+ "desktop-min": {
1516
+ name: "Desktop Minimum",
1517
+ width: 1280,
1518
+ minHeight: 640,
1519
+ category: "desktop",
1520
+ description: "Desktop minimum viewport"
1521
+ },
1522
+ "tablet-landscape": {
1523
+ name: "Tablet Landscape",
1524
+ width: 1024,
1525
+ minHeight: 768,
1526
+ category: "tablet",
1527
+ description: "Tablet landscape viewport"
1528
+ },
1529
+ "tablet-min": {
1530
+ name: "Tablet Minimum",
1531
+ width: 768,
1532
+ minHeight: 384,
1533
+ category: "tablet",
1534
+ description: "Tablet minimum viewport"
1535
+ },
1536
+ "print-landscape": {
1537
+ name: "Print A4 Landscape",
1538
+ width: 1123,
1539
+ minHeight: 794,
1505
1540
  category: "print",
1506
- description: "A4 alias at 96 DPI"
1541
+ description: "A4 landscape at 96 DPI"
1507
1542
  }
1508
1543
  };
1509
1544
  function resolveDevicePreset(device) {
@@ -2166,25 +2201,53 @@ var LayoutEngine = class {
2166
2201
  });
2167
2202
  } else {
2168
2203
  const childWidths = [];
2204
+ const explicitHeightFlags = [];
2205
+ const blockButtonIndices = /* @__PURE__ */ new Set();
2169
2206
  let stackHeight = 0;
2170
- children.forEach((childRef) => {
2207
+ children.forEach((childRef, index) => {
2171
2208
  const childNode = this.nodes[childRef.ref];
2172
2209
  let childWidth = this.getIntrinsicComponentWidth(childNode);
2173
2210
  let childHeight = this.getComponentHeight();
2174
- if (childNode?.kind === "component" && childNode.props.height) {
2175
- childHeight = Number(childNode.props.height);
2176
- } else if (childNode?.kind === "component" && childNode.props.width) {
2211
+ const hasExplicitHeight = childNode?.kind === "component" && !!childNode.props.height;
2212
+ const hasExplicitWidth = childNode?.kind === "component" && !!childNode.props.width;
2213
+ const isBlockButton = childNode?.kind === "component" && childNode.componentType === "Button" && !hasExplicitWidth && this.parseBooleanProp(childNode.props.block, false);
2214
+ if (isBlockButton) {
2215
+ childWidth = 0;
2216
+ blockButtonIndices.add(index);
2217
+ } else if (hasExplicitWidth) {
2177
2218
  childWidth = Number(childNode.props.width);
2219
+ }
2220
+ if (hasExplicitHeight) {
2221
+ childHeight = Number(childNode.props.height);
2222
+ }
2223
+ childWidths.push(childWidth);
2224
+ explicitHeightFlags.push(hasExplicitHeight);
2225
+ });
2226
+ const totalGapWidth = gap * Math.max(0, children.length - 1);
2227
+ if (blockButtonIndices.size > 0) {
2228
+ const fixedWidth = childWidths.reduce((sum, w, idx) => {
2229
+ return blockButtonIndices.has(idx) ? sum : sum + w;
2230
+ }, 0);
2231
+ const remainingWidth = width - totalGapWidth - fixedWidth;
2232
+ const widthPerBlock = Math.max(1, remainingWidth / blockButtonIndices.size);
2233
+ blockButtonIndices.forEach((index) => {
2234
+ childWidths[index] = widthPerBlock;
2235
+ });
2236
+ }
2237
+ children.forEach((childRef, index) => {
2238
+ const childNode = this.nodes[childRef.ref];
2239
+ let childHeight = this.getComponentHeight();
2240
+ const childWidth = childWidths[index];
2241
+ if (explicitHeightFlags[index] && childNode?.kind === "component") {
2242
+ childHeight = Number(childNode.props.height);
2178
2243
  } else if (childNode?.kind === "container") {
2179
2244
  childHeight = this.calculateContainerHeight(childNode, childWidth);
2180
2245
  } else if (childNode?.kind === "component") {
2181
2246
  childHeight = this.getIntrinsicComponentHeight(childNode, childWidth);
2182
2247
  }
2183
- childWidths.push(childWidth);
2184
2248
  stackHeight = Math.max(stackHeight, childHeight);
2185
2249
  });
2186
2250
  const totalChildWidth = childWidths.reduce((sum, w) => sum + w, 0);
2187
- const totalGapWidth = gap * Math.max(0, children.length - 1);
2188
2251
  const totalContentWidth = totalChildWidth + totalGapWidth;
2189
2252
  let startX = x;
2190
2253
  if (align === "center") {
@@ -2460,12 +2523,13 @@ var LayoutEngine = class {
2460
2523
  getButtonMetricsForDensity() {
2461
2524
  switch (this.style.density) {
2462
2525
  case "compact":
2463
- return { fontSize: 12, paddingX: 8 };
2464
- case "comfortable":
2465
- return { fontSize: 16, paddingX: 16 };
2526
+ return { fontSize: 12, paddingX: 12 };
2466
2527
  case "normal":
2528
+ return { fontSize: 14, paddingX: 18 };
2529
+ case "comfortable":
2530
+ return { fontSize: 16, paddingX: 24 };
2467
2531
  default:
2468
- return { fontSize: 14, paddingX: 12 };
2532
+ return { fontSize: 14, paddingX: 18 };
2469
2533
  }
2470
2534
  }
2471
2535
  getHeadingMetricsForDensity(level) {
@@ -2718,6 +2782,15 @@ var LayoutEngine = class {
2718
2782
  }
2719
2783
  return width;
2720
2784
  }
2785
+ parseBooleanProp(value, fallback = false) {
2786
+ if (typeof value === "boolean") return value;
2787
+ if (typeof value === "string") {
2788
+ const normalized = value.toLowerCase().trim();
2789
+ if (normalized === "true") return true;
2790
+ if (normalized === "false") return false;
2791
+ }
2792
+ return fallback;
2793
+ }
2721
2794
  adjustNodeYPositions(nodeId, deltaY) {
2722
2795
  const node = this.nodes[nodeId];
2723
2796
  if (!node) return;
@@ -3359,8 +3432,8 @@ var DENSITY_TOKENS = {
3359
3432
  xl: 12
3360
3433
  },
3361
3434
  button: {
3362
- paddingX: 8,
3363
- paddingY: 4,
3435
+ paddingX: 12,
3436
+ paddingY: 6,
3364
3437
  radius: 4,
3365
3438
  fontSize: 12,
3366
3439
  fontWeight: 500
@@ -3407,8 +3480,8 @@ var DENSITY_TOKENS = {
3407
3480
  xl: 24
3408
3481
  },
3409
3482
  button: {
3410
- paddingX: 12,
3411
- paddingY: 6,
3483
+ paddingX: 18,
3484
+ paddingY: 9,
3412
3485
  radius: 6,
3413
3486
  fontSize: 14,
3414
3487
  fontWeight: 500
@@ -3455,8 +3528,8 @@ var DENSITY_TOKENS = {
3455
3528
  xl: 32
3456
3529
  },
3457
3530
  button: {
3458
- paddingX: 16,
3459
- paddingY: 8,
3531
+ paddingX: 24,
3532
+ paddingY: 12,
3460
3533
  radius: 8,
3461
3534
  fontSize: 16,
3462
3535
  fontWeight: 500
@@ -3527,6 +3600,7 @@ var SVGRenderer = class {
3527
3600
  constructor(ir, layout, options) {
3528
3601
  this.renderedNodeIds = /* @__PURE__ */ new Set();
3529
3602
  this.fontFamily = "system-ui, -apple-system, sans-serif";
3603
+ this.parentContainerByChildId = /* @__PURE__ */ new Map();
3530
3604
  this.ir = ir;
3531
3605
  this.layout = layout;
3532
3606
  this.selectedScreenName = options?.screenName;
@@ -3541,6 +3615,7 @@ var SVGRenderer = class {
3541
3615
  };
3542
3616
  this.renderTheme = THEMES[this.options.theme];
3543
3617
  this.colorResolver = new ColorResolver();
3618
+ this.buildParentContainerIndex();
3544
3619
  if (ir.project.mocks && Object.keys(ir.project.mocks).length > 0) {
3545
3620
  MockDataGenerator.setCustomMocks(ir.project.mocks);
3546
3621
  }
@@ -3744,16 +3819,14 @@ var SVGRenderer = class {
3744
3819
  renderButton(node, pos) {
3745
3820
  const text = String(node.props.text || "Button");
3746
3821
  const variant = String(node.props.variant || "default");
3822
+ const fullWidth = this.shouldButtonFillAvailableWidth(node);
3747
3823
  const radius = this.tokens.button.radius;
3748
3824
  const fontSize = this.tokens.button.fontSize;
3749
3825
  const fontWeight = this.tokens.button.fontWeight;
3750
3826
  const paddingX = this.tokens.button.paddingX;
3751
3827
  const paddingY = this.tokens.button.paddingY;
3752
3828
  const idealTextWidth = this.estimateTextWidth(text, fontSize);
3753
- const buttonWidth = this.clampControlWidth(
3754
- Math.max(Math.ceil(idealTextWidth + paddingX * 2), 60),
3755
- pos.width
3756
- );
3829
+ const buttonWidth = fullWidth ? Math.max(1, pos.width) : this.clampControlWidth(Math.max(Math.ceil(idealTextWidth + paddingX * 2), 60), pos.width);
3757
3830
  const buttonHeight = fontSize + paddingY * 2;
3758
3831
  const availableTextWidth = Math.max(0, buttonWidth - paddingX * 2);
3759
3832
  const visibleText = this.truncateTextToWidth(text, availableTextWidth, fontSize);
@@ -5266,6 +5339,27 @@ var SVGRenderer = class {
5266
5339
  }
5267
5340
  return fallback;
5268
5341
  }
5342
+ shouldButtonFillAvailableWidth(node) {
5343
+ if (this.parseBooleanProp(node.props.block, false)) {
5344
+ return true;
5345
+ }
5346
+ const parent = this.parentContainerByChildId.get(node.id);
5347
+ if (!parent || parent.containerType !== "stack") {
5348
+ return false;
5349
+ }
5350
+ const direction = String(parent.params.direction || "vertical");
5351
+ const align = parent.style.align || "justify";
5352
+ return direction === "horizontal" && align === "justify";
5353
+ }
5354
+ buildParentContainerIndex() {
5355
+ this.parentContainerByChildId.clear();
5356
+ Object.values(this.ir.project.nodes).forEach((node) => {
5357
+ if (node.kind !== "container") return;
5358
+ node.children.forEach((childRef) => {
5359
+ this.parentContainerByChildId.set(childRef.ref, node);
5360
+ });
5361
+ });
5362
+ }
5269
5363
  escapeXml(text) {
5270
5364
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
5271
5365
  }
@@ -5301,12 +5395,13 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
5301
5395
  renderButton(node, pos) {
5302
5396
  const text = String(node.props.text || "Button");
5303
5397
  const variant = String(node.props.variant || "default");
5398
+ const fullWidth = this.shouldButtonFillAvailableWidth(node);
5304
5399
  const radius = this.tokens.button.radius;
5305
5400
  const fontSize = this.tokens.button.fontSize;
5306
5401
  const paddingX = this.tokens.button.paddingX;
5307
5402
  const paddingY = this.tokens.button.paddingY;
5308
5403
  const textWidth = text.length * fontSize * 0.6;
5309
- const buttonWidth = this.clampControlWidth(Math.max(textWidth + paddingX * 2, 60), pos.width);
5404
+ const buttonWidth = fullWidth ? Math.max(1, pos.width) : this.clampControlWidth(Math.max(textWidth + paddingX * 2, 60), pos.width);
5310
5405
  const buttonHeight = fontSize + paddingY * 2;
5311
5406
  const semanticBase = this.getSemanticVariantColor(variant);
5312
5407
  const hasExplicitVariantColor = semanticBase !== void 0 || this.colorResolver.hasColor(variant);
@@ -5951,13 +6046,14 @@ var SketchSVGRenderer = class extends SVGRenderer {
5951
6046
  renderButton(node, pos) {
5952
6047
  const text = String(node.props.text || "Button");
5953
6048
  const variant = String(node.props.variant || "default");
6049
+ const fullWidth = this.shouldButtonFillAvailableWidth(node);
5954
6050
  const radius = this.tokens.button.radius;
5955
6051
  const fontSize = this.tokens.button.fontSize;
5956
6052
  const fontWeight = this.tokens.button.fontWeight;
5957
6053
  const paddingX = this.tokens.button.paddingX;
5958
6054
  const paddingY = this.tokens.button.paddingY;
5959
6055
  const idealTextWidth = text.length * fontSize * 0.6;
5960
- const buttonWidth = this.clampControlWidth(Math.max(idealTextWidth + paddingX * 2, 60), pos.width);
6056
+ const buttonWidth = fullWidth ? Math.max(1, pos.width) : this.clampControlWidth(Math.max(idealTextWidth + paddingX * 2, 60), pos.width);
5961
6057
  const buttonHeight = fontSize + paddingY * 2;
5962
6058
  const availableTextWidth = Math.max(0, buttonWidth - paddingX * 2);
5963
6059
  const visibleText = this.truncateTextToWidth(text, availableTextWidth, fontSize);
package/dist/index.d.cts CHANGED
@@ -408,6 +408,7 @@ declare class LayoutEngine {
408
408
  private calculateChildHeight;
409
409
  private calculateChildWidth;
410
410
  private estimateTextWidth;
411
+ private parseBooleanProp;
411
412
  private adjustNodeYPositions;
412
413
  }
413
414
  declare function calculateLayout(ir: IRContract): LayoutResult;
@@ -561,6 +562,7 @@ declare class SVGRenderer {
561
562
  protected renderedNodeIds: Set<string>;
562
563
  protected colorResolver: ColorResolver;
563
564
  protected fontFamily: string;
565
+ private parentContainerByChildId;
564
566
  constructor(ir: IRContract, layout: LayoutResult, options?: SVGRenderOptions);
565
567
  /**
566
568
  * Get list of available screens in the project
@@ -691,6 +693,8 @@ declare class SVGRenderer {
691
693
  };
692
694
  };
693
695
  protected parseBooleanProp(value: unknown, fallback?: boolean): boolean;
696
+ protected shouldButtonFillAvailableWidth(node: IRComponentNode): boolean;
697
+ private buildParentContainerIndex;
694
698
  protected escapeXml(text: string): string;
695
699
  /**
696
700
  * Get data-node-id attribute string for SVG elements
package/dist/index.d.ts CHANGED
@@ -408,6 +408,7 @@ declare class LayoutEngine {
408
408
  private calculateChildHeight;
409
409
  private calculateChildWidth;
410
410
  private estimateTextWidth;
411
+ private parseBooleanProp;
411
412
  private adjustNodeYPositions;
412
413
  }
413
414
  declare function calculateLayout(ir: IRContract): LayoutResult;
@@ -561,6 +562,7 @@ declare class SVGRenderer {
561
562
  protected renderedNodeIds: Set<string>;
562
563
  protected colorResolver: ColorResolver;
563
564
  protected fontFamily: string;
565
+ private parentContainerByChildId;
564
566
  constructor(ir: IRContract, layout: LayoutResult, options?: SVGRenderOptions);
565
567
  /**
566
568
  * Get list of available screens in the project
@@ -691,6 +693,8 @@ declare class SVGRenderer {
691
693
  };
692
694
  };
693
695
  protected parseBooleanProp(value: unknown, fallback?: boolean): boolean;
696
+ protected shouldButtonFillAvailableWidth(node: IRComponentNode): boolean;
697
+ private buildParentContainerIndex;
694
698
  protected escapeXml(text: string): string;
695
699
  /**
696
700
  * Get data-node-id attribute string for SVG elements
package/dist/index.js CHANGED
@@ -1449,12 +1449,47 @@ var DEVICE_PRESETS = {
1449
1449
  category: "print",
1450
1450
  description: "A4 portrait at 96 DPI"
1451
1451
  },
1452
- a4: {
1453
- name: "Print A4",
1454
- width: 794,
1455
- minHeight: 1123,
1452
+ "mobile-landscape": {
1453
+ name: "Mobile Landscape",
1454
+ width: 812,
1455
+ minHeight: 375,
1456
+ category: "mobile",
1457
+ description: "Mobile landscape viewport"
1458
+ },
1459
+ "mobile-min": {
1460
+ name: "Mobile Minimum",
1461
+ width: 375,
1462
+ minHeight: 375,
1463
+ category: "mobile",
1464
+ description: "Mobile minimum viewport"
1465
+ },
1466
+ "desktop-min": {
1467
+ name: "Desktop Minimum",
1468
+ width: 1280,
1469
+ minHeight: 640,
1470
+ category: "desktop",
1471
+ description: "Desktop minimum viewport"
1472
+ },
1473
+ "tablet-landscape": {
1474
+ name: "Tablet Landscape",
1475
+ width: 1024,
1476
+ minHeight: 768,
1477
+ category: "tablet",
1478
+ description: "Tablet landscape viewport"
1479
+ },
1480
+ "tablet-min": {
1481
+ name: "Tablet Minimum",
1482
+ width: 768,
1483
+ minHeight: 384,
1484
+ category: "tablet",
1485
+ description: "Tablet minimum viewport"
1486
+ },
1487
+ "print-landscape": {
1488
+ name: "Print A4 Landscape",
1489
+ width: 1123,
1490
+ minHeight: 794,
1456
1491
  category: "print",
1457
- description: "A4 alias at 96 DPI"
1492
+ description: "A4 landscape at 96 DPI"
1458
1493
  }
1459
1494
  };
1460
1495
  function resolveDevicePreset(device) {
@@ -2117,25 +2152,53 @@ var LayoutEngine = class {
2117
2152
  });
2118
2153
  } else {
2119
2154
  const childWidths = [];
2155
+ const explicitHeightFlags = [];
2156
+ const blockButtonIndices = /* @__PURE__ */ new Set();
2120
2157
  let stackHeight = 0;
2121
- children.forEach((childRef) => {
2158
+ children.forEach((childRef, index) => {
2122
2159
  const childNode = this.nodes[childRef.ref];
2123
2160
  let childWidth = this.getIntrinsicComponentWidth(childNode);
2124
2161
  let childHeight = this.getComponentHeight();
2125
- if (childNode?.kind === "component" && childNode.props.height) {
2126
- childHeight = Number(childNode.props.height);
2127
- } else if (childNode?.kind === "component" && childNode.props.width) {
2162
+ const hasExplicitHeight = childNode?.kind === "component" && !!childNode.props.height;
2163
+ const hasExplicitWidth = childNode?.kind === "component" && !!childNode.props.width;
2164
+ const isBlockButton = childNode?.kind === "component" && childNode.componentType === "Button" && !hasExplicitWidth && this.parseBooleanProp(childNode.props.block, false);
2165
+ if (isBlockButton) {
2166
+ childWidth = 0;
2167
+ blockButtonIndices.add(index);
2168
+ } else if (hasExplicitWidth) {
2128
2169
  childWidth = Number(childNode.props.width);
2170
+ }
2171
+ if (hasExplicitHeight) {
2172
+ childHeight = Number(childNode.props.height);
2173
+ }
2174
+ childWidths.push(childWidth);
2175
+ explicitHeightFlags.push(hasExplicitHeight);
2176
+ });
2177
+ const totalGapWidth = gap * Math.max(0, children.length - 1);
2178
+ if (blockButtonIndices.size > 0) {
2179
+ const fixedWidth = childWidths.reduce((sum, w, idx) => {
2180
+ return blockButtonIndices.has(idx) ? sum : sum + w;
2181
+ }, 0);
2182
+ const remainingWidth = width - totalGapWidth - fixedWidth;
2183
+ const widthPerBlock = Math.max(1, remainingWidth / blockButtonIndices.size);
2184
+ blockButtonIndices.forEach((index) => {
2185
+ childWidths[index] = widthPerBlock;
2186
+ });
2187
+ }
2188
+ children.forEach((childRef, index) => {
2189
+ const childNode = this.nodes[childRef.ref];
2190
+ let childHeight = this.getComponentHeight();
2191
+ const childWidth = childWidths[index];
2192
+ if (explicitHeightFlags[index] && childNode?.kind === "component") {
2193
+ childHeight = Number(childNode.props.height);
2129
2194
  } else if (childNode?.kind === "container") {
2130
2195
  childHeight = this.calculateContainerHeight(childNode, childWidth);
2131
2196
  } else if (childNode?.kind === "component") {
2132
2197
  childHeight = this.getIntrinsicComponentHeight(childNode, childWidth);
2133
2198
  }
2134
- childWidths.push(childWidth);
2135
2199
  stackHeight = Math.max(stackHeight, childHeight);
2136
2200
  });
2137
2201
  const totalChildWidth = childWidths.reduce((sum, w) => sum + w, 0);
2138
- const totalGapWidth = gap * Math.max(0, children.length - 1);
2139
2202
  const totalContentWidth = totalChildWidth + totalGapWidth;
2140
2203
  let startX = x;
2141
2204
  if (align === "center") {
@@ -2411,12 +2474,13 @@ var LayoutEngine = class {
2411
2474
  getButtonMetricsForDensity() {
2412
2475
  switch (this.style.density) {
2413
2476
  case "compact":
2414
- return { fontSize: 12, paddingX: 8 };
2415
- case "comfortable":
2416
- return { fontSize: 16, paddingX: 16 };
2477
+ return { fontSize: 12, paddingX: 12 };
2417
2478
  case "normal":
2479
+ return { fontSize: 14, paddingX: 18 };
2480
+ case "comfortable":
2481
+ return { fontSize: 16, paddingX: 24 };
2418
2482
  default:
2419
- return { fontSize: 14, paddingX: 12 };
2483
+ return { fontSize: 14, paddingX: 18 };
2420
2484
  }
2421
2485
  }
2422
2486
  getHeadingMetricsForDensity(level) {
@@ -2669,6 +2733,15 @@ var LayoutEngine = class {
2669
2733
  }
2670
2734
  return width;
2671
2735
  }
2736
+ parseBooleanProp(value, fallback = false) {
2737
+ if (typeof value === "boolean") return value;
2738
+ if (typeof value === "string") {
2739
+ const normalized = value.toLowerCase().trim();
2740
+ if (normalized === "true") return true;
2741
+ if (normalized === "false") return false;
2742
+ }
2743
+ return fallback;
2744
+ }
2672
2745
  adjustNodeYPositions(nodeId, deltaY) {
2673
2746
  const node = this.nodes[nodeId];
2674
2747
  if (!node) return;
@@ -3310,8 +3383,8 @@ var DENSITY_TOKENS = {
3310
3383
  xl: 12
3311
3384
  },
3312
3385
  button: {
3313
- paddingX: 8,
3314
- paddingY: 4,
3386
+ paddingX: 12,
3387
+ paddingY: 6,
3315
3388
  radius: 4,
3316
3389
  fontSize: 12,
3317
3390
  fontWeight: 500
@@ -3358,8 +3431,8 @@ var DENSITY_TOKENS = {
3358
3431
  xl: 24
3359
3432
  },
3360
3433
  button: {
3361
- paddingX: 12,
3362
- paddingY: 6,
3434
+ paddingX: 18,
3435
+ paddingY: 9,
3363
3436
  radius: 6,
3364
3437
  fontSize: 14,
3365
3438
  fontWeight: 500
@@ -3406,8 +3479,8 @@ var DENSITY_TOKENS = {
3406
3479
  xl: 32
3407
3480
  },
3408
3481
  button: {
3409
- paddingX: 16,
3410
- paddingY: 8,
3482
+ paddingX: 24,
3483
+ paddingY: 12,
3411
3484
  radius: 8,
3412
3485
  fontSize: 16,
3413
3486
  fontWeight: 500
@@ -3478,6 +3551,7 @@ var SVGRenderer = class {
3478
3551
  constructor(ir, layout, options) {
3479
3552
  this.renderedNodeIds = /* @__PURE__ */ new Set();
3480
3553
  this.fontFamily = "system-ui, -apple-system, sans-serif";
3554
+ this.parentContainerByChildId = /* @__PURE__ */ new Map();
3481
3555
  this.ir = ir;
3482
3556
  this.layout = layout;
3483
3557
  this.selectedScreenName = options?.screenName;
@@ -3492,6 +3566,7 @@ var SVGRenderer = class {
3492
3566
  };
3493
3567
  this.renderTheme = THEMES[this.options.theme];
3494
3568
  this.colorResolver = new ColorResolver();
3569
+ this.buildParentContainerIndex();
3495
3570
  if (ir.project.mocks && Object.keys(ir.project.mocks).length > 0) {
3496
3571
  MockDataGenerator.setCustomMocks(ir.project.mocks);
3497
3572
  }
@@ -3695,16 +3770,14 @@ var SVGRenderer = class {
3695
3770
  renderButton(node, pos) {
3696
3771
  const text = String(node.props.text || "Button");
3697
3772
  const variant = String(node.props.variant || "default");
3773
+ const fullWidth = this.shouldButtonFillAvailableWidth(node);
3698
3774
  const radius = this.tokens.button.radius;
3699
3775
  const fontSize = this.tokens.button.fontSize;
3700
3776
  const fontWeight = this.tokens.button.fontWeight;
3701
3777
  const paddingX = this.tokens.button.paddingX;
3702
3778
  const paddingY = this.tokens.button.paddingY;
3703
3779
  const idealTextWidth = this.estimateTextWidth(text, fontSize);
3704
- const buttonWidth = this.clampControlWidth(
3705
- Math.max(Math.ceil(idealTextWidth + paddingX * 2), 60),
3706
- pos.width
3707
- );
3780
+ const buttonWidth = fullWidth ? Math.max(1, pos.width) : this.clampControlWidth(Math.max(Math.ceil(idealTextWidth + paddingX * 2), 60), pos.width);
3708
3781
  const buttonHeight = fontSize + paddingY * 2;
3709
3782
  const availableTextWidth = Math.max(0, buttonWidth - paddingX * 2);
3710
3783
  const visibleText = this.truncateTextToWidth(text, availableTextWidth, fontSize);
@@ -5217,6 +5290,27 @@ var SVGRenderer = class {
5217
5290
  }
5218
5291
  return fallback;
5219
5292
  }
5293
+ shouldButtonFillAvailableWidth(node) {
5294
+ if (this.parseBooleanProp(node.props.block, false)) {
5295
+ return true;
5296
+ }
5297
+ const parent = this.parentContainerByChildId.get(node.id);
5298
+ if (!parent || parent.containerType !== "stack") {
5299
+ return false;
5300
+ }
5301
+ const direction = String(parent.params.direction || "vertical");
5302
+ const align = parent.style.align || "justify";
5303
+ return direction === "horizontal" && align === "justify";
5304
+ }
5305
+ buildParentContainerIndex() {
5306
+ this.parentContainerByChildId.clear();
5307
+ Object.values(this.ir.project.nodes).forEach((node) => {
5308
+ if (node.kind !== "container") return;
5309
+ node.children.forEach((childRef) => {
5310
+ this.parentContainerByChildId.set(childRef.ref, node);
5311
+ });
5312
+ });
5313
+ }
5220
5314
  escapeXml(text) {
5221
5315
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
5222
5316
  }
@@ -5252,12 +5346,13 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
5252
5346
  renderButton(node, pos) {
5253
5347
  const text = String(node.props.text || "Button");
5254
5348
  const variant = String(node.props.variant || "default");
5349
+ const fullWidth = this.shouldButtonFillAvailableWidth(node);
5255
5350
  const radius = this.tokens.button.radius;
5256
5351
  const fontSize = this.tokens.button.fontSize;
5257
5352
  const paddingX = this.tokens.button.paddingX;
5258
5353
  const paddingY = this.tokens.button.paddingY;
5259
5354
  const textWidth = text.length * fontSize * 0.6;
5260
- const buttonWidth = this.clampControlWidth(Math.max(textWidth + paddingX * 2, 60), pos.width);
5355
+ const buttonWidth = fullWidth ? Math.max(1, pos.width) : this.clampControlWidth(Math.max(textWidth + paddingX * 2, 60), pos.width);
5261
5356
  const buttonHeight = fontSize + paddingY * 2;
5262
5357
  const semanticBase = this.getSemanticVariantColor(variant);
5263
5358
  const hasExplicitVariantColor = semanticBase !== void 0 || this.colorResolver.hasColor(variant);
@@ -5902,13 +5997,14 @@ var SketchSVGRenderer = class extends SVGRenderer {
5902
5997
  renderButton(node, pos) {
5903
5998
  const text = String(node.props.text || "Button");
5904
5999
  const variant = String(node.props.variant || "default");
6000
+ const fullWidth = this.shouldButtonFillAvailableWidth(node);
5905
6001
  const radius = this.tokens.button.radius;
5906
6002
  const fontSize = this.tokens.button.fontSize;
5907
6003
  const fontWeight = this.tokens.button.fontWeight;
5908
6004
  const paddingX = this.tokens.button.paddingX;
5909
6005
  const paddingY = this.tokens.button.paddingY;
5910
6006
  const idealTextWidth = text.length * fontSize * 0.6;
5911
- const buttonWidth = this.clampControlWidth(Math.max(idealTextWidth + paddingX * 2, 60), pos.width);
6007
+ const buttonWidth = fullWidth ? Math.max(1, pos.width) : this.clampControlWidth(Math.max(idealTextWidth + paddingX * 2, 60), pos.width);
5912
6008
  const buttonHeight = fontSize + paddingY * 2;
5913
6009
  const availableTextWidth = Math.max(0, buttonWidth - paddingX * 2);
5914
6010
  const visibleText = this.truncateTextToWidth(text, availableTextWidth, fontSize);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wire-dsl/engine",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
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": {