@wire-dsl/engine 0.3.0 → 0.4.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.js CHANGED
@@ -1647,7 +1647,8 @@ function validateSemanticDiagnostics(ast, sourceMap) {
1647
1647
  if (enumValues) {
1648
1648
  const normalizedValue = String(propValue);
1649
1649
  const isCustomVariantFromColors = propName === "variant" && !enumValues.includes(normalizedValue) && Object.prototype.hasOwnProperty.call(ast.colors || {}, normalizedValue);
1650
- if (!enumValues.includes(normalizedValue) && !isCustomVariantFromColors) {
1650
+ const isPropReference = normalizedValue.startsWith("prop_");
1651
+ if (!enumValues.includes(normalizedValue) && !isCustomVariantFromColors && !isPropReference) {
1651
1652
  emitWarning(
1652
1653
  `Invalid value "${normalizedValue}" for property "${propName}" in component "${componentType}".`,
1653
1654
  "COMPONENT_INVALID_PROPERTY_VALUE",
@@ -1757,7 +1758,7 @@ function validateSemanticDiagnostics(ast, sourceMap) {
1757
1758
  const enumValues = rules.enumParams?.[paramName];
1758
1759
  if (enumValues) {
1759
1760
  const normalizedValue = String(paramValue);
1760
- if (!enumValues.includes(normalizedValue)) {
1761
+ if (!enumValues.includes(normalizedValue) && !normalizedValue.startsWith("prop_")) {
1761
1762
  emitWarning(
1762
1763
  `Invalid value "${normalizedValue}" for parameter "${paramName}" in layout "${layout.layoutType}".`,
1763
1764
  "LAYOUT_INVALID_PARAMETER_VALUE",
@@ -2198,7 +2199,21 @@ var IRComponentNodeSchema = z.object({
2198
2199
  style: IRNodeStyleSchema,
2199
2200
  meta: IRMetaSchema
2200
2201
  });
2201
- 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
+ ]);
2202
2217
  var IRScreenSchema = z.object({
2203
2218
  id: z.string(),
2204
2219
  name: z.string(),
@@ -2421,7 +2436,7 @@ ${messages}`);
2421
2436
  const layoutChildren = layout.children;
2422
2437
  const layoutDefinition = this.definedLayouts.get(layout.layoutType);
2423
2438
  if (layoutDefinition) {
2424
- return this.expandDefinedLayout(layoutDefinition, layoutParams, layoutChildren, context);
2439
+ return this.expandDefinedLayout(layoutDefinition, layoutParams, layoutChildren, context, layout._meta?.nodeId);
2425
2440
  }
2426
2441
  const nodeId = this.idGen.generate("node");
2427
2442
  const childRefs = [];
@@ -2460,8 +2475,8 @@ ${messages}`);
2460
2475
  children: childRefs,
2461
2476
  style,
2462
2477
  meta: {
2463
- nodeId: layout._meta?.nodeId
2464
- // 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
2465
2480
  }
2466
2481
  };
2467
2482
  this.nodes[nodeId] = containerNode;
@@ -2490,8 +2505,7 @@ ${messages}`);
2490
2505
  // Cells have no padding by default - grid gap handles spacing
2491
2506
  meta: {
2492
2507
  source: "cell",
2493
- nodeId: cell._meta?.nodeId
2494
- // Pass SourceMap nodeId from AST
2508
+ nodeId: context?.instanceScope ? `${cell._meta?.nodeId}@${context.instanceScope}` : cell._meta?.nodeId
2495
2509
  }
2496
2510
  };
2497
2511
  this.nodes[nodeId] = containerNode;
@@ -2518,7 +2532,7 @@ ${messages}`);
2518
2532
  const resolvedProps = this.resolveComponentProps(component.componentType, component.props, context);
2519
2533
  const definition = this.definedComponents.get(component.componentType);
2520
2534
  if (definition) {
2521
- return this.expandDefinedComponent(definition, resolvedProps, context);
2535
+ return this.expandDefinedComponent(definition, resolvedProps, component._meta?.nodeId, context);
2522
2536
  }
2523
2537
  const builtInComponents = /* @__PURE__ */ new Set([
2524
2538
  "Button",
@@ -2564,35 +2578,49 @@ ${messages}`);
2564
2578
  props: resolvedProps,
2565
2579
  style: {},
2566
2580
  meta: {
2567
- nodeId: component._meta?.nodeId
2568
- // 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
2569
2583
  }
2570
2584
  };
2571
2585
  this.nodes[nodeId] = componentNode;
2572
2586
  return nodeId;
2573
2587
  }
2574
- expandDefinedComponent(definition, invocationArgs, parentContext) {
2588
+ expandDefinedComponent(definition, invocationArgs, callSiteNodeId, parentContext) {
2575
2589
  const context = {
2576
2590
  args: invocationArgs,
2577
2591
  providedArgNames: new Set(Object.keys(invocationArgs)),
2578
2592
  usedArgNames: /* @__PURE__ */ new Set(),
2579
2593
  definitionName: definition.name,
2580
2594
  definitionKind: "component",
2581
- allowChildrenSlot: false
2595
+ allowChildrenSlot: false,
2596
+ // Scope internal nodeIds using the call-site nodeId
2597
+ instanceScope: callSiteNodeId
2582
2598
  };
2599
+ let expandedRootId = null;
2583
2600
  if (definition.body.type === "layout") {
2584
- const result = this.convertLayout(definition.body, context);
2585
- this.reportUnusedArguments(context);
2586
- return result;
2601
+ expandedRootId = this.convertLayout(definition.body, context);
2587
2602
  } else if (definition.body.type === "component") {
2588
- const result = this.convertComponent(definition.body, context);
2589
- this.reportUnusedArguments(context);
2590
- return result;
2603
+ expandedRootId = this.convertComponent(definition.body, context);
2591
2604
  } else {
2592
2605
  throw new Error(`Invalid defined component body type for "${definition.name}"`);
2593
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;
2594
2622
  }
2595
- expandDefinedLayout(definition, invocationParams, invocationChildren, parentContext) {
2623
+ expandDefinedLayout(definition, invocationParams, invocationChildren, parentContext, callSiteNodeId) {
2596
2624
  if (invocationChildren.length !== 1) {
2597
2625
  this.errors.push({
2598
2626
  type: "layout-children-arity",
@@ -2608,11 +2636,26 @@ ${messages}`);
2608
2636
  definitionName: definition.name,
2609
2637
  definitionKind: "layout",
2610
2638
  allowChildrenSlot: true,
2611
- childrenSlot: resolvedSlot
2639
+ childrenSlot: resolvedSlot,
2640
+ // Scope internal nodeIds using the call-site nodeId
2641
+ instanceScope: callSiteNodeId
2612
2642
  };
2613
- const nodeId = this.convertLayout(definition.body, context);
2643
+ const expandedRootId = this.convertLayout(definition.body, context);
2614
2644
  this.reportUnusedArguments(context);
2615
- 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;
2616
2659
  }
2617
2660
  resolveChildrenSlot(slot, parentContext) {
2618
2661
  if (slot.type === "component" && slot.componentType === "Children") {
@@ -2643,6 +2686,20 @@ ${messages}`);
2643
2686
  key
2644
2687
  );
2645
2688
  if (resolvedValue !== void 0) {
2689
+ const wasPropReference = typeof value === "string" && value.startsWith("prop_");
2690
+ if (wasPropReference) {
2691
+ const layoutMetadata = LAYOUTS2[layoutType];
2692
+ const property = layoutMetadata?.properties?.[key];
2693
+ if (property?.type === "enum" && Array.isArray(property.options)) {
2694
+ const normalizedValue = String(resolvedValue);
2695
+ if (!property.options.includes(normalizedValue)) {
2696
+ this.warnings.push({
2697
+ type: "invalid-bound-enum-value",
2698
+ message: `Invalid value "${normalizedValue}" for parameter "${key}" in layout "${layoutType}". Expected one of: ${property.options.join(", ")}.`
2699
+ });
2700
+ }
2701
+ }
2702
+ }
2646
2703
  resolved[key] = resolvedValue;
2647
2704
  }
2648
2705
  }
@@ -2707,6 +2764,20 @@ ${messages}`);
2707
2764
  key
2708
2765
  );
2709
2766
  if (resolvedValue !== void 0) {
2767
+ const wasPropReference = typeof value === "string" && value.startsWith("prop_");
2768
+ if (wasPropReference) {
2769
+ const metadata = COMPONENTS2[componentType];
2770
+ const property = metadata?.properties?.[key];
2771
+ if (property?.type === "enum" && Array.isArray(property.options)) {
2772
+ const normalizedValue = String(resolvedValue);
2773
+ if (!property.options.includes(normalizedValue)) {
2774
+ this.warnings.push({
2775
+ type: "invalid-bound-enum-value",
2776
+ message: `Invalid value "${normalizedValue}" for property "${key}" in component "${componentType}". Expected one of: ${property.options.join(", ")}.`
2777
+ });
2778
+ }
2779
+ }
2780
+ }
2710
2781
  resolved[key] = resolvedValue;
2711
2782
  }
2712
2783
  }
@@ -2950,6 +3021,8 @@ var LayoutEngine = class {
2950
3021
  }
2951
3022
  if (node.kind === "container") {
2952
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);
2953
3026
  } else {
2954
3027
  this.calculateComponent(node, nodeId, x, y, width, height);
2955
3028
  }
@@ -2982,7 +3055,7 @@ var LayoutEngine = class {
2982
3055
  this.calculateCard(node, innerX, innerY, innerWidth, innerHeight);
2983
3056
  break;
2984
3057
  }
2985
- if (isVerticalStack || node.containerType === "card") {
3058
+ if ((isVerticalStack || node.containerType === "card") && node.children.length > 0) {
2986
3059
  let containerMaxY = y;
2987
3060
  node.children.forEach((childRef) => {
2988
3061
  const childPos = this.result[childRef.ref];
@@ -3125,6 +3198,10 @@ var LayoutEngine = class {
3125
3198
  const gap = this.resolveSpacing(node.style.gap);
3126
3199
  const padding = this.resolveSpacing(node.style.padding);
3127
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
+ }
3128
3205
  if (node.containerType === "grid") {
3129
3206
  const columns = Number(node.params.columns) || 12;
3130
3207
  const colWidth = (availableWidth - gap * (columns - 1)) / columns;
@@ -3377,6 +3454,20 @@ var LayoutEngine = class {
3377
3454
  }
3378
3455
  });
3379
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
+ }
3380
3471
  calculateComponent(node, nodeId, x, y, width, height) {
3381
3472
  if (node.kind !== "component") return;
3382
3473
  const componentWidth = Number(node.props.width) || width;
@@ -4517,14 +4608,14 @@ var THEMES = {
4517
4608
  primaryLight: "#EFF6FF"
4518
4609
  },
4519
4610
  dark: {
4520
- bg: "#0F172A",
4521
- cardBg: "#1E293B",
4522
- border: "#334155",
4523
- text: "#FFFFFF",
4524
- textMuted: "#94A3B8",
4611
+ bg: "#111111",
4612
+ cardBg: "#1C1C1C",
4613
+ border: "#303030",
4614
+ text: "#F0F0F0",
4615
+ textMuted: "#808080",
4525
4616
  primary: "#60A5FA",
4526
4617
  primaryHover: "#3B82F6",
4527
- primaryLight: "#1E3A8A"
4618
+ primaryLight: "#1C2A3A"
4528
4619
  }
4529
4620
  };
4530
4621
  var SVGRenderer = class {
@@ -4542,7 +4633,8 @@ var SVGRenderer = class {
4542
4633
  height: options?.height || 720,
4543
4634
  theme: colorScheme,
4544
4635
  includeLabels: options?.includeLabels ?? true,
4545
- screenName: options?.screenName
4636
+ screenName: options?.screenName,
4637
+ showDiagnostics: options?.showDiagnostics ?? false
4546
4638
  };
4547
4639
  this.colorResolver = new ColorResolver();
4548
4640
  this.buildParentContainerIndex();
@@ -4642,13 +4734,30 @@ var SVGRenderer = class {
4642
4734
  if (node.containerType === "split") {
4643
4735
  this.renderSplitDecoration(node, pos, containerGroup);
4644
4736
  }
4645
- node.children.forEach((childRef) => {
4646
- this.renderNode(childRef.ref, containerGroup);
4647
- });
4737
+ if (node.children.length === 0 && this.options.showDiagnostics) {
4738
+ containerGroup.push(this.renderEmptyContainerDiagnostic(pos, node.containerType));
4739
+ } else {
4740
+ node.children.forEach((childRef) => {
4741
+ this.renderNode(childRef.ref, containerGroup);
4742
+ });
4743
+ }
4648
4744
  if (hasNodeId) {
4649
4745
  containerGroup.push("</g>");
4650
4746
  }
4651
4747
  output.push(...containerGroup);
4748
+ } else if (node.kind === "instance") {
4749
+ const instanceGroup = [];
4750
+ if (node.meta.nodeId) {
4751
+ instanceGroup.push(`<g data-node-id="${node.meta.nodeId}">`);
4752
+ }
4753
+ instanceGroup.push(
4754
+ `<rect x="${pos.x}" y="${pos.y}" width="${pos.width}" height="${pos.height}" fill="transparent" stroke="none" pointer-events="all"/>`
4755
+ );
4756
+ this.renderNode(node.expandedRoot.ref, instanceGroup);
4757
+ if (node.meta.nodeId) {
4758
+ instanceGroup.push("</g>");
4759
+ }
4760
+ output.push(...instanceGroup);
4652
4761
  } else if (node.kind === "component") {
4653
4762
  const componentSvg = this.renderComponent(node, pos);
4654
4763
  if (componentSvg) {
@@ -4765,6 +4874,8 @@ var SVGRenderer = class {
4765
4874
  const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
4766
4875
  const labelOffset = this.parseBooleanProp(node.props.labelSpace, false) ? 18 : 0;
4767
4876
  const fullWidth = this.shouldButtonFillAvailableWidth(node);
4877
+ const iconName = String(node.props.icon || "").trim();
4878
+ const iconAlign = String(node.props.iconAlign || "left").toLowerCase();
4768
4879
  const radius = this.tokens.button.radius;
4769
4880
  const fontSize = this.tokens.button.fontSize;
4770
4881
  const fontWeight = this.tokens.button.fontWeight;
@@ -4772,30 +4883,62 @@ var SVGRenderer = class {
4772
4883
  const controlHeight = resolveActionControlHeight(size, density);
4773
4884
  const buttonY = pos.y + labelOffset;
4774
4885
  const buttonHeight = Math.max(16, Math.min(controlHeight, pos.height - labelOffset));
4886
+ const iconSvg = iconName ? getIcon(iconName) : null;
4887
+ const iconSize = iconSvg ? Math.round(fontSize * 1.1) : 0;
4888
+ const iconGap = iconSvg ? 8 : 0;
4889
+ const edgePad = 12;
4890
+ const textPad = paddingX + extraPadding;
4775
4891
  const idealTextWidth = this.estimateTextWidth(text, fontSize);
4776
- const buttonWidth = fullWidth ? Math.max(1, pos.width) : this.clampControlWidth(Math.max(Math.ceil(idealTextWidth + (paddingX + extraPadding) * 2), 60), pos.width);
4777
- const availableTextWidth = Math.max(0, buttonWidth - (paddingX + extraPadding) * 2);
4892
+ const buttonWidth = fullWidth ? Math.max(1, pos.width) : this.clampControlWidth(Math.max(Math.ceil(idealTextWidth + (iconSvg ? iconSize + iconGap : 0) + textPad * 2), 60), pos.width);
4893
+ const availableTextWidth = Math.max(0, buttonWidth - textPad * 2 - (iconSvg ? iconSize + iconGap : 0));
4778
4894
  const visibleText = this.truncateTextToWidth(text, availableTextWidth, fontSize);
4779
4895
  const semanticBase = this.getSemanticVariantColor(variant);
4780
4896
  const hasExplicitVariantColor = semanticBase !== void 0 || this.colorResolver.hasColor(variant);
4781
4897
  const resolvedBase = this.resolveVariantColor(variant, this.renderTheme.primary);
4782
- const bgColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.85) : "rgba(226, 232, 240, 0.9)";
4898
+ const isDarkMode = this.options.theme === "dark";
4899
+ const bgColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.85) : isDarkMode ? "rgba(48, 48, 55, 0.9)" : "rgba(226, 232, 240, 0.9)";
4783
4900
  const textColor = hasExplicitVariantColor ? "#FFFFFF" : this.hexToRgba(this.resolveTextColor(), 0.85);
4784
- const borderColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.7) : "rgba(100, 116, 139, 0.4)";
4785
- return `<g${this.getDataNodeId(node)}>
4901
+ const borderColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.7) : isDarkMode ? "rgba(75, 75, 88, 0.8)" : "rgba(100, 116, 139, 0.4)";
4902
+ const iconOffsetY = buttonY + (buttonHeight - iconSize) / 2;
4903
+ const iconX = iconAlign === "right" ? pos.x + buttonWidth - edgePad - iconSize : pos.x + edgePad;
4904
+ const textAlign = String(node.props.align || "center").toLowerCase();
4905
+ const sidePad = textPad + 4;
4906
+ let textX;
4907
+ let textAnchor;
4908
+ if (textAlign === "left") {
4909
+ textX = iconSvg && iconAlign === "left" ? pos.x + edgePad + iconSize + iconGap : pos.x + sidePad;
4910
+ textAnchor = "start";
4911
+ } else if (textAlign === "right") {
4912
+ textX = iconSvg && iconAlign === "right" ? pos.x + buttonWidth - edgePad - iconSize - iconGap : pos.x + buttonWidth - sidePad;
4913
+ textAnchor = "end";
4914
+ } else {
4915
+ textX = pos.x + buttonWidth / 2;
4916
+ textAnchor = "middle";
4917
+ }
4918
+ let svg = `<g${this.getDataNodeId(node)}>
4786
4919
  <rect x="${pos.x}" y="${buttonY}"
4787
4920
  width="${buttonWidth}" height="${buttonHeight}"
4788
4921
  rx="${radius}"
4789
4922
  fill="${bgColor}"
4790
4923
  stroke="${borderColor}"
4791
- stroke-width="1"/>
4792
- <text x="${pos.x + buttonWidth / 2}" y="${buttonY + buttonHeight / 2 + fontSize * 0.35}"
4924
+ stroke-width="1"/>`;
4925
+ if (iconSvg) {
4926
+ svg += `
4927
+ <g transform="translate(${iconX}, ${iconOffsetY})">
4928
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${textColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
4929
+ ${this.extractSvgContent(iconSvg)}
4930
+ </svg>
4931
+ </g>`;
4932
+ }
4933
+ svg += `
4934
+ <text x="${textX}" y="${buttonY + buttonHeight / 2 + fontSize * 0.35}"
4793
4935
  font-family="Arial, Helvetica, sans-serif"
4794
4936
  font-size="${fontSize}"
4795
4937
  font-weight="${fontWeight}"
4796
4938
  fill="${textColor}"
4797
- text-anchor="middle">${this.escapeXml(visibleText)}</text>
4939
+ text-anchor="${textAnchor}">${this.escapeXml(visibleText)}</text>
4798
4940
  </g>`;
4941
+ return svg;
4799
4942
  }
4800
4943
  renderLink(node, pos) {
4801
4944
  const text = String(node.props.text || "Link");
@@ -4836,28 +4979,66 @@ var SVGRenderer = class {
4836
4979
  renderInput(node, pos) {
4837
4980
  const label = String(node.props.label || "");
4838
4981
  const placeholder = String(node.props.placeholder || "");
4982
+ const iconLeftName = String(node.props.iconLeft || "").trim();
4983
+ const iconRightName = String(node.props.iconRight || "").trim();
4839
4984
  const radius = this.tokens.input.radius;
4840
4985
  const fontSize = this.tokens.input.fontSize;
4841
4986
  const paddingX = this.tokens.input.paddingX;
4842
4987
  const labelOffset = this.getControlLabelOffset(label);
4843
4988
  const controlY = pos.y + labelOffset;
4844
4989
  const controlHeight = Math.max(16, pos.height - labelOffset);
4845
- return `<g${this.getDataNodeId(node)}>
4846
- ${label ? `<text x="${pos.x + paddingX}" y="${this.getControlLabelBaselineY(pos.y)}"
4990
+ const iconSize = 16;
4991
+ const iconPad = 12;
4992
+ const iconInnerGap = 8;
4993
+ const iconLeftSvg = iconLeftName ? getIcon(iconLeftName) : null;
4994
+ const iconRightSvg = iconRightName ? getIcon(iconRightName) : null;
4995
+ const leftOffset = iconLeftSvg ? iconPad + iconSize + iconInnerGap : 0;
4996
+ const rightOffset = iconRightSvg ? iconPad + iconSize + iconInnerGap : 0;
4997
+ const textX = pos.x + (iconLeftSvg ? leftOffset : paddingX);
4998
+ const iconColor = this.hexToRgba(this.resolveMutedColor(), 0.8);
4999
+ const iconCenterY = controlY + (controlHeight - iconSize) / 2;
5000
+ let svg = `<g${this.getDataNodeId(node)}>`;
5001
+ if (label) {
5002
+ svg += `
5003
+ <text x="${pos.x + paddingX}" y="${this.getControlLabelBaselineY(pos.y)}"
4847
5004
  font-family="Arial, Helvetica, sans-serif"
4848
5005
  font-size="12"
4849
- fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>` : ""}
5006
+ fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>`;
5007
+ }
5008
+ svg += `
4850
5009
  <rect x="${pos.x}" y="${controlY}"
4851
5010
  width="${pos.width}" height="${controlHeight}"
4852
5011
  rx="${radius}"
4853
5012
  fill="${this.renderTheme.cardBg}"
4854
5013
  stroke="${this.renderTheme.border}"
4855
- stroke-width="1"/>
4856
- <text x="${pos.x + paddingX}" y="${controlY + controlHeight / 2 + 5}"
5014
+ stroke-width="1"/>`;
5015
+ if (iconLeftSvg) {
5016
+ svg += `
5017
+ <g transform="translate(${pos.x + iconPad}, ${iconCenterY})">
5018
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
5019
+ ${this.extractSvgContent(iconLeftSvg)}
5020
+ </svg>
5021
+ </g>`;
5022
+ }
5023
+ if (iconRightSvg) {
5024
+ svg += `
5025
+ <g transform="translate(${pos.x + pos.width - iconPad - iconSize}, ${iconCenterY})">
5026
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
5027
+ ${this.extractSvgContent(iconRightSvg)}
5028
+ </svg>
5029
+ </g>`;
5030
+ }
5031
+ if (placeholder) {
5032
+ const availPlaceholderWidth = pos.width - (iconLeftSvg ? leftOffset : paddingX) - (iconRightSvg ? rightOffset : paddingX);
5033
+ const visiblePlaceholder = this.truncateTextToWidth(placeholder, Math.max(0, availPlaceholderWidth), fontSize);
5034
+ svg += `
5035
+ <text x="${textX}" y="${controlY + controlHeight / 2 + 5}"
4857
5036
  font-family="Arial, Helvetica, sans-serif"
4858
5037
  font-size="${fontSize}"
4859
- fill="${this.renderTheme.textMuted}">${this.escapeXml(placeholder)}</text>
4860
- </g>`;
5038
+ fill="${this.renderTheme.textMuted}">${this.escapeXml(visiblePlaceholder)}</text>`;
5039
+ }
5040
+ svg += "\n </g>";
5041
+ return svg;
4861
5042
  }
4862
5043
  renderTopbar(node, pos) {
4863
5044
  const title = String(node.props.title || "App");
@@ -4867,7 +5048,7 @@ var SVGRenderer = class {
4867
5048
  const variant = String(node.props.variant || "default");
4868
5049
  const accentColor = variant === "default" ? this.resolveAccentColor() : this.resolveVariantColor(variant, this.resolveAccentColor());
4869
5050
  const showBorder = this.parseBooleanProp(node.props.border, false);
4870
- const showBackground = this.parseBooleanProp(node.props.background ?? node.props.backround, false);
5051
+ const showBackground = this.parseBooleanProp(node.props.background, false);
4871
5052
  const radiusMap = {
4872
5053
  none: 0,
4873
5054
  sm: 4,
@@ -5006,6 +5187,21 @@ var SVGRenderer = class {
5006
5187
  </g>`;
5007
5188
  output.push(svg);
5008
5189
  }
5190
+ /**
5191
+ * Renders a yellow warning placeholder for containers with no children.
5192
+ * Only shown when `showDiagnostics` is enabled (editor/canvas mode).
5193
+ */
5194
+ renderEmptyContainerDiagnostic(pos, containerType) {
5195
+ const diagColor = "#F59E0B";
5196
+ const diagBg = "#FFFBEB";
5197
+ const diagText = "#92400E";
5198
+ const minHeight = 40;
5199
+ const h = Math.max(pos.height, minHeight);
5200
+ const cx = pos.x + pos.width / 2;
5201
+ const cy = pos.y + h / 2;
5202
+ const label = containerType ? `Empty ${containerType}` : "Empty layout";
5203
+ 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>`;
5204
+ }
5009
5205
  renderSplitDecoration(node, pos, output) {
5010
5206
  if (node.kind !== "container") return;
5011
5207
  const gap = this.resolveSpacing(node.style.gap);
@@ -5054,7 +5250,7 @@ var SVGRenderer = class {
5054
5250
  const hasCaption = caption.length > 0;
5055
5251
  const showOuterBorder = this.parseBooleanProp(node.props.border, false);
5056
5252
  const showOuterBackground = this.parseBooleanProp(
5057
- node.props.background ?? node.props.backround,
5253
+ node.props.background,
5058
5254
  false
5059
5255
  );
5060
5256
  const showInnerBorder = this.parseBooleanProp(node.props.innerBorder, true);
@@ -5437,30 +5633,66 @@ var SVGRenderer = class {
5437
5633
  renderSelect(node, pos) {
5438
5634
  const label = String(node.props.label || "");
5439
5635
  const placeholder = String(node.props.placeholder || "Select...");
5636
+ const iconLeftName = String(node.props.iconLeft || "").trim();
5637
+ const iconRightName = String(node.props.iconRight || "").trim();
5440
5638
  const labelOffset = this.getControlLabelOffset(label);
5441
5639
  const controlY = pos.y + labelOffset;
5442
5640
  const controlHeight = Math.max(16, pos.height - labelOffset);
5443
5641
  const centerY = controlY + controlHeight / 2 + 5;
5444
- return `<g${this.getDataNodeId(node)}>
5445
- ${label ? `<text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
5446
- font-family="Arial, Helvetica, sans-serif"
5447
- font-size="12"
5448
- fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>` : ""}
5449
- <rect x="${pos.x}" y="${controlY}"
5450
- width="${pos.width}" height="${controlHeight}"
5451
- rx="6"
5452
- fill="${this.renderTheme.cardBg}"
5453
- stroke="${this.renderTheme.border}"
5454
- stroke-width="1"/>
5455
- <text x="${pos.x + 12}" y="${centerY}"
5456
- font-family="Arial, Helvetica, sans-serif"
5457
- font-size="14"
5458
- fill="${this.renderTheme.textMuted}">${this.escapeXml(placeholder)}</text>
5459
- <text x="${pos.x + pos.width - 20}" y="${centerY}"
5460
- font-family="Arial, Helvetica, sans-serif"
5461
- font-size="16"
5642
+ const iconSize = 16;
5643
+ const iconPad = 12;
5644
+ const iconInnerGap = 8;
5645
+ const iconLeftSvg = iconLeftName ? getIcon(iconLeftName) : null;
5646
+ const iconRightSvg = iconRightName ? getIcon(iconRightName) : null;
5647
+ const leftOffset = iconLeftSvg ? iconPad + iconSize + iconInnerGap : 0;
5648
+ const chevronWidth = 20;
5649
+ const iconColor = this.hexToRgba(this.resolveMutedColor(), 0.8);
5650
+ const iconCenterY = controlY + (controlHeight - iconSize) / 2;
5651
+ let svg = `<g${this.getDataNodeId(node)}>`;
5652
+ if (label) {
5653
+ svg += `
5654
+ <text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
5655
+ font-family="Arial, Helvetica, sans-serif"
5656
+ font-size="12"
5657
+ fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>`;
5658
+ }
5659
+ svg += `
5660
+ <rect x="${pos.x}" y="${controlY}"
5661
+ width="${pos.width}" height="${controlHeight}"
5662
+ rx="6"
5663
+ fill="${this.renderTheme.cardBg}"
5664
+ stroke="${this.renderTheme.border}"
5665
+ stroke-width="1"/>`;
5666
+ if (iconLeftSvg) {
5667
+ svg += `
5668
+ <g transform="translate(${pos.x + iconPad}, ${iconCenterY})">
5669
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
5670
+ ${this.extractSvgContent(iconLeftSvg)}
5671
+ </svg>
5672
+ </g>`;
5673
+ }
5674
+ if (iconRightSvg) {
5675
+ svg += `
5676
+ <g transform="translate(${pos.x + pos.width - chevronWidth - iconPad - iconSize}, ${iconCenterY})">
5677
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
5678
+ ${this.extractSvgContent(iconRightSvg)}
5679
+ </svg>
5680
+ </g>`;
5681
+ }
5682
+ const textX = pos.x + (iconLeftSvg ? leftOffset : 12);
5683
+ const availPlaceholderWidth = pos.width - (iconLeftSvg ? leftOffset : 12) - chevronWidth - (iconRightSvg ? iconPad + iconSize + iconInnerGap : 0);
5684
+ const visiblePlaceholder = this.truncateTextToWidth(placeholder, Math.max(0, availPlaceholderWidth), 14);
5685
+ svg += `
5686
+ <text x="${textX}" y="${centerY}"
5687
+ font-family="Arial, Helvetica, sans-serif"
5688
+ font-size="14"
5689
+ fill="${this.renderTheme.textMuted}">${this.escapeXml(visiblePlaceholder)}</text>
5690
+ <text x="${pos.x + pos.width - 20}" y="${centerY}"
5691
+ font-family="Arial, Helvetica, sans-serif"
5692
+ font-size="16"
5462
5693
  fill="${this.renderTheme.textMuted}">\u25BC</text>
5463
5694
  </g>`;
5695
+ return svg;
5464
5696
  }
5465
5697
  renderCheckbox(node, pos) {
5466
5698
  const label = String(node.props.label || "Checkbox");
@@ -5893,7 +6125,9 @@ var SVGRenderer = class {
5893
6125
  renderImage(node, pos) {
5894
6126
  const placeholder = String(node.props.placeholder || "landscape").toLowerCase();
5895
6127
  const placeholderIcon = String(node.props.icon || "").trim();
6128
+ const variant = String(node.props.variant || "").trim();
5896
6129
  const placeholderIconSvg = placeholder === "icon" && placeholderIcon ? getIcon(placeholderIcon) : null;
6130
+ const imageBg = this.options.theme === "dark" ? "#2A2A2A" : "#E8E8E8";
5897
6131
  const aspectRatios = {
5898
6132
  landscape: 16 / 9,
5899
6133
  portrait: 2 / 3,
@@ -5911,30 +6145,29 @@ var SVGRenderer = class {
5911
6145
  }
5912
6146
  const offsetX = pos.x + (pos.width - iconWidth) / 2;
5913
6147
  const offsetY = pos.y + (pos.height - iconHeight) / 2;
5914
- let svg = `<g${this.getDataNodeId(node)}>
5915
- <!-- Image Background -->
5916
- <rect x="${pos.x}" y="${pos.y}" width="${pos.width}" height="${pos.height}" fill="#E8E8E8"/>`;
5917
6148
  if (placeholder === "icon" && placeholderIconSvg) {
5918
- const badgeSize = Math.max(24, Math.min(iconWidth, iconHeight) * 0.78);
5919
- const badgeX = pos.x + (pos.width - badgeSize) / 2;
5920
- const badgeY = pos.y + (pos.height - badgeSize) / 2;
5921
- const iconSize = badgeSize * 0.62;
5922
- const iconOffsetX = badgeX + (badgeSize - iconSize) / 2;
5923
- const iconOffsetY = badgeY + (badgeSize - iconSize) / 2;
5924
- svg += `
5925
- <!-- Custom Icon Placeholder -->
5926
- <rect x="${badgeX}" y="${badgeY}"
5927
- width="${badgeSize}" height="${badgeSize}"
5928
- rx="${Math.max(4, badgeSize * 0.2)}"
5929
- fill="rgba(255, 255, 255, 0.6)"
5930
- stroke="#888"
5931
- stroke-width="1"/>
6149
+ const semanticBase = variant ? this.getSemanticVariantColor(variant) : void 0;
6150
+ const hasVariant = variant.length > 0 && (semanticBase !== void 0 || this.colorResolver.hasColor(variant));
6151
+ const variantColor = hasVariant ? this.resolveVariantColor(variant, this.renderTheme.primary) : null;
6152
+ const bgColor = hasVariant ? this.hexToRgba(variantColor, 0.12) : imageBg;
6153
+ const iconColor = hasVariant ? variantColor : this.options.theme === "dark" ? "#888888" : "#666666";
6154
+ const iconSize = Math.max(16, Math.min(pos.width, pos.height) * 0.6);
6155
+ const iconOffsetX = pos.x + (pos.width - iconSize) / 2;
6156
+ const iconOffsetY = pos.y + (pos.height - iconSize) / 2;
6157
+ return `<g${this.getDataNodeId(node)}>
6158
+ <rect x="${pos.x}" y="${pos.y}" width="${pos.width}" height="${pos.height}" fill="${bgColor}" rx="4"/>
5932
6159
  <g transform="translate(${iconOffsetX}, ${iconOffsetY})">
5933
- <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="#555" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
6160
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
5934
6161
  ${this.extractSvgContent(placeholderIconSvg)}
5935
6162
  </svg>
5936
- </g>`;
5937
- } else if (["landscape", "portrait", "square"].includes(placeholder)) {
6163
+ </g>
6164
+ <rect x="${pos.x}" y="${pos.y}" width="${pos.width}" height="${pos.height}" fill="none" stroke="${this.renderTheme.border}" stroke-width="1" rx="4"/>
6165
+ </g>`;
6166
+ }
6167
+ let svg = `<g${this.getDataNodeId(node)}>
6168
+ <!-- Image Background -->
6169
+ <rect x="${pos.x}" y="${pos.y}" width="${pos.width}" height="${pos.height}" fill="${imageBg}"/>`;
6170
+ if (["landscape", "portrait", "square"].includes(placeholder)) {
5938
6171
  const cameraCx = offsetX + iconWidth / 2;
5939
6172
  const cameraCy = offsetY + iconHeight / 2;
5940
6173
  const scale = Math.min(iconWidth, iconHeight) / 24;
@@ -6038,18 +6271,22 @@ var SVGRenderer = class {
6038
6271
  const fontSize = 14;
6039
6272
  const activeIndex = Number(node.props.active || 0);
6040
6273
  const accentColor = this.resolveAccentColor();
6274
+ const variantProp = String(node.props.variant || "").trim();
6275
+ const semanticVariant = variantProp ? this.getSemanticVariantColor(variantProp) : void 0;
6276
+ const hasVariant = variantProp.length > 0 && (semanticVariant !== void 0 || this.colorResolver.hasColor(variantProp));
6277
+ const activeColor = hasVariant ? this.resolveVariantColor(variantProp, accentColor) : accentColor;
6041
6278
  let svg = `<g${this.getDataNodeId(node)}>`;
6042
6279
  items.forEach((item, index) => {
6043
6280
  const itemY = pos.y + index * itemHeight;
6044
6281
  const isActive = index === activeIndex;
6045
- const bgColor = isActive ? this.hexToRgba(accentColor, 0.15) : "transparent";
6046
- const textColor = isActive ? this.hexToRgba(accentColor, 0.9) : this.hexToRgba(this.resolveTextColor(), 0.75);
6282
+ const bgColor = isActive ? this.hexToRgba(activeColor, 0.15) : "transparent";
6283
+ const textColor = isActive ? this.hexToRgba(activeColor, 0.9) : this.hexToRgba(this.resolveTextColor(), 0.75);
6047
6284
  const fontWeight = isActive ? "500" : "400";
6048
6285
  if (isActive) {
6049
6286
  svg += `
6050
- <rect x="${pos.x}" y="${itemY}"
6051
- width="${pos.width}" height="${itemHeight}"
6052
- rx="6"
6287
+ <rect x="${pos.x}" y="${itemY}"
6288
+ width="${pos.width}" height="${itemHeight}"
6289
+ rx="6"
6053
6290
  fill="${bgColor}"/>`;
6054
6291
  }
6055
6292
  let currentX = pos.x + 12;
@@ -6058,9 +6295,10 @@ var SVGRenderer = class {
6058
6295
  if (iconSvg) {
6059
6296
  const iconSize = 16;
6060
6297
  const iconY = itemY + (itemHeight - iconSize) / 2;
6298
+ const iconColor = isActive ? this.hexToRgba(activeColor, 0.9) : this.hexToRgba(this.resolveMutedColor(), 0.9);
6061
6299
  svg += `
6062
6300
  <g transform="translate(${currentX}, ${iconY})">
6063
- <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${this.hexToRgba(this.resolveMutedColor(), 0.9)}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
6301
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
6064
6302
  ${this.extractSvgContent(iconSvg)}
6065
6303
  </svg>
6066
6304
  </g>`;
@@ -6068,10 +6306,10 @@ var SVGRenderer = class {
6068
6306
  }
6069
6307
  }
6070
6308
  svg += `
6071
- <text x="${currentX}" y="${itemY + itemHeight / 2 + 5}"
6072
- font-family="Arial, Helvetica, sans-serif"
6073
- font-size="${fontSize}"
6074
- font-weight="${fontWeight}"
6309
+ <text x="${currentX}" y="${itemY + itemHeight / 2 + 5}"
6310
+ font-family="Arial, Helvetica, sans-serif"
6311
+ font-size="${fontSize}"
6312
+ font-weight="${fontWeight}"
6075
6313
  fill="${textColor}">${this.escapeXml(item)}</text>`;
6076
6314
  });
6077
6315
  svg += "\n </g>";
@@ -6111,9 +6349,10 @@ var SVGRenderer = class {
6111
6349
  const semanticBase = this.getSemanticVariantColor(variant);
6112
6350
  const hasExplicitVariantColor = semanticBase !== void 0 || this.colorResolver.hasColor(variant);
6113
6351
  const resolvedBase = this.resolveVariantColor(variant, this.renderTheme.primary);
6114
- const bgColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.85) : "rgba(226, 232, 240, 0.9)";
6352
+ const isDarkMode = this.options.theme === "dark";
6353
+ const bgColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.85) : isDarkMode ? "rgba(48, 48, 55, 0.9)" : "rgba(226, 232, 240, 0.9)";
6115
6354
  const iconColor = hasExplicitVariantColor ? "#FFFFFF" : this.hexToRgba(this.resolveTextColor(), 0.75);
6116
- const borderColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.7) : "rgba(100, 116, 139, 0.4)";
6355
+ const borderColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.7) : isDarkMode ? "rgba(75, 75, 88, 0.8)" : "rgba(100, 116, 139, 0.4)";
6117
6356
  const opacity = disabled ? "0.5" : "1";
6118
6357
  const iconSvg = getIcon(iconName);
6119
6358
  const buttonSize = Math.max(
@@ -6171,14 +6410,37 @@ var SVGRenderer = class {
6171
6410
  return this.colorResolver.resolveColor("muted", fallback);
6172
6411
  }
6173
6412
  getSemanticVariantColor(variant) {
6174
- const semantic = {
6413
+ const isDark = this.options.theme === "dark";
6414
+ const semantic = isDark ? {
6415
+ // Muted mid-range — readable on #111111 without being neon
6416
+ primary: this.renderTheme.primary,
6417
+ // already theme-aware (#60A5FA)
6418
+ secondary: "#7E8EA2",
6419
+ // desaturated slate
6420
+ success: "#22A06B",
6421
+ // muted emerald
6422
+ warning: "#B38010",
6423
+ // deep amber
6424
+ danger: "#CC4444",
6425
+ // muted red
6426
+ error: "#CC4444",
6427
+ info: "#2485AF"
6428
+ // muted sky
6429
+ } : {
6430
+ // Tailwind 500-level — works on white/light backgrounds
6175
6431
  primary: this.renderTheme.primary,
6432
+ // #3B82F6
6176
6433
  secondary: "#64748B",
6434
+ // Slate 500
6177
6435
  success: "#10B981",
6436
+ // Emerald 500
6178
6437
  warning: "#F59E0B",
6438
+ // Amber 500
6179
6439
  danger: "#EF4444",
6440
+ // Red 500
6180
6441
  error: "#EF4444",
6181
6442
  info: "#0EA5E9"
6443
+ // Sky 500
6182
6444
  };
6183
6445
  return semantic[variant];
6184
6446
  }
@@ -6455,10 +6717,11 @@ var SVGRenderer = class {
6455
6717
  buildParentContainerIndex() {
6456
6718
  this.parentContainerByChildId.clear();
6457
6719
  Object.values(this.ir.project.nodes).forEach((node) => {
6458
- if (node.kind !== "container") return;
6459
- node.children.forEach((childRef) => {
6460
- this.parentContainerByChildId.set(childRef.ref, node);
6461
- });
6720
+ if (node.kind === "container") {
6721
+ node.children.forEach((childRef) => {
6722
+ this.parentContainerByChildId.set(childRef.ref, node);
6723
+ });
6724
+ }
6462
6725
  });
6463
6726
  }
6464
6727
  escapeXml(text) {
@@ -6639,6 +6902,19 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
6639
6902
  renderLabel(node, pos) {
6640
6903
  return this.renderTextBlock(node, pos, String(node.props.text || "Label"), 12, 1.2);
6641
6904
  }
6905
+ /**
6906
+ * Render image as a plain skeleton rectangle — no icon, no placeholder label,
6907
+ * just a filled block with the correct dimensions (aspect-ratio is preserved
6908
+ * by the layout engine, so pos already has the right size).
6909
+ */
6910
+ renderImage(node, pos) {
6911
+ return `<g${this.getDataNodeId(node)}>
6912
+ <rect x="${pos.x}" y="${pos.y}"
6913
+ width="${pos.width}" height="${pos.height}"
6914
+ rx="4"
6915
+ fill="${this.renderTheme.border}"/>
6916
+ </g>`;
6917
+ }
6642
6918
  /**
6643
6919
  * Render badge as shape only (no text)
6644
6920
  */
@@ -6880,7 +7156,7 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
6880
7156
  const hasCaption = String(node.props.caption || "").trim().length > 0;
6881
7157
  const showOuterBorder = this.parseBooleanProp(node.props.border, false);
6882
7158
  const showOuterBackground = this.parseBooleanProp(
6883
- node.props.background ?? node.props.backround,
7159
+ node.props.background,
6884
7160
  false
6885
7161
  );
6886
7162
  const showInnerBorder = this.parseBooleanProp(node.props.innerBorder, true);
@@ -6995,7 +7271,7 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
6995
7271
  const variant = String(node.props.variant || "default");
6996
7272
  const accentBlock = variant === "default" ? this.renderTheme.border : this.hexToRgba(this.resolveVariantColor(variant, this.resolveAccentColor()), 0.35);
6997
7273
  const showBorder = this.parseBooleanProp(node.props.border, false);
6998
- const showBackground = this.parseBooleanProp(node.props.background ?? node.props.backround, false);
7274
+ const showBackground = this.parseBooleanProp(node.props.background, false);
6999
7275
  const radiusMap = {
7000
7276
  none: 0,
7001
7277
  sm: 4,
@@ -7313,6 +7589,8 @@ var SketchSVGRenderer = class extends SVGRenderer {
7313
7589
  const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
7314
7590
  const labelOffset = this.parseBooleanProp(node.props.labelSpace, false) ? 18 : 0;
7315
7591
  const fullWidth = this.shouldButtonFillAvailableWidth(node);
7592
+ const iconName = String(node.props.icon || "").trim();
7593
+ const iconAlign = String(node.props.iconAlign || "left").toLowerCase();
7316
7594
  const radius = this.tokens.button.radius;
7317
7595
  const fontSize = this.tokens.button.fontSize;
7318
7596
  const fontWeight = this.tokens.button.fontWeight;
@@ -7322,9 +7600,14 @@ var SketchSVGRenderer = class extends SVGRenderer {
7322
7600
  Math.min(resolveControlHeight(size, density), pos.height - labelOffset)
7323
7601
  );
7324
7602
  const buttonY = pos.y + labelOffset;
7603
+ const iconSvg = iconName ? getIcon(iconName) : null;
7604
+ const iconSize = iconSvg ? Math.round(fontSize * 1.1) : 0;
7605
+ const iconGap = iconSvg ? 8 : 0;
7606
+ const edgePad = 12;
7607
+ const textPad = paddingX + extraPadding;
7325
7608
  const idealTextWidth = text.length * fontSize * 0.6;
7326
- const buttonWidth = fullWidth ? Math.max(1, pos.width) : this.clampControlWidth(Math.max(idealTextWidth + (paddingX + extraPadding) * 2, 60), pos.width);
7327
- const availableTextWidth = Math.max(0, buttonWidth - (paddingX + extraPadding) * 2);
7609
+ const buttonWidth = fullWidth ? Math.max(1, pos.width) : this.clampControlWidth(Math.max(idealTextWidth + (iconSvg ? iconSize + iconGap : 0) + textPad * 2, 60), pos.width);
7610
+ const availableTextWidth = Math.max(0, buttonWidth - textPad * 2 - (iconSvg ? iconSize + iconGap : 0));
7328
7611
  const visibleText = this.truncateTextToWidth(text, availableTextWidth, fontSize);
7329
7612
  const semanticBase = this.getSemanticVariantColor(variant);
7330
7613
  const hasExplicitVariantColor = semanticBase !== void 0 || this.colorResolver.hasColor(variant);
@@ -7332,21 +7615,47 @@ var SketchSVGRenderer = class extends SVGRenderer {
7332
7615
  const borderColor = variantColor;
7333
7616
  const textColor = variantColor;
7334
7617
  const strokeWidth = 0.5;
7335
- return `<g${this.getDataNodeId(node)}>
7618
+ const iconOffsetY = buttonY + (buttonHeight - iconSize) / 2;
7619
+ const iconX = iconAlign === "right" ? pos.x + buttonWidth - edgePad - iconSize : pos.x + edgePad;
7620
+ const textAlign = String(node.props.align || "center").toLowerCase();
7621
+ const sidePad = textPad + 4;
7622
+ let textX;
7623
+ let textAnchor;
7624
+ if (textAlign === "left") {
7625
+ textX = iconSvg && iconAlign === "left" ? pos.x + edgePad + iconSize + iconGap : pos.x + sidePad;
7626
+ textAnchor = "start";
7627
+ } else if (textAlign === "right") {
7628
+ textX = iconSvg && iconAlign === "right" ? pos.x + buttonWidth - edgePad - iconSize - iconGap : pos.x + buttonWidth - sidePad;
7629
+ textAnchor = "end";
7630
+ } else {
7631
+ textX = pos.x + buttonWidth / 2;
7632
+ textAnchor = "middle";
7633
+ }
7634
+ let svg = `<g${this.getDataNodeId(node)}>
7336
7635
  <rect x="${pos.x}" y="${buttonY}"
7337
7636
  width="${buttonWidth}" height="${buttonHeight}"
7338
7637
  rx="${radius}"
7339
7638
  fill="none"
7340
7639
  stroke="${borderColor}"
7341
7640
  stroke-width="${strokeWidth}"
7342
- filter="url(#sketch-rough)"/>
7343
- <text x="${pos.x + buttonWidth / 2}" y="${buttonY + buttonHeight / 2 + fontSize * 0.35}"
7641
+ filter="url(#sketch-rough)"/>`;
7642
+ if (iconSvg) {
7643
+ svg += `
7644
+ <g transform="translate(${iconX}, ${iconOffsetY})">
7645
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${textColor}" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round">
7646
+ ${this.extractSvgContent(iconSvg)}
7647
+ </svg>
7648
+ </g>`;
7649
+ }
7650
+ svg += `
7651
+ <text x="${textX}" y="${buttonY + buttonHeight / 2 + fontSize * 0.35}"
7344
7652
  font-family="${this.fontFamily}"
7345
7653
  font-size="${fontSize}"
7346
7654
  font-weight="${fontWeight}"
7347
7655
  fill="${textColor}"
7348
- text-anchor="middle">${this.escapeXml(visibleText)}</text>
7656
+ text-anchor="${textAnchor}">${this.escapeXml(visibleText)}</text>
7349
7657
  </g>`;
7658
+ return svg;
7350
7659
  }
7351
7660
  /**
7352
7661
  * Render badge with colored border instead of fill
@@ -7471,29 +7780,67 @@ var SketchSVGRenderer = class extends SVGRenderer {
7471
7780
  renderInput(node, pos) {
7472
7781
  const label = String(node.props.label || "");
7473
7782
  const placeholder = String(node.props.placeholder || "");
7783
+ const iconLeftName = String(node.props.iconLeft || "").trim();
7784
+ const iconRightName = String(node.props.iconRight || "").trim();
7474
7785
  const radius = this.tokens.input.radius;
7475
7786
  const fontSize = this.tokens.input.fontSize;
7476
7787
  const paddingX = this.tokens.input.paddingX;
7477
7788
  const labelOffset = this.getControlLabelOffset(label);
7478
7789
  const controlY = pos.y + labelOffset;
7479
7790
  const controlHeight = Math.max(16, pos.height - labelOffset);
7480
- return `<g${this.getDataNodeId(node)}>
7481
- ${label ? `<text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
7791
+ const iconSize = 16;
7792
+ const iconPad = 12;
7793
+ const iconInnerGap = 8;
7794
+ const iconLeftSvg = iconLeftName ? getIcon(iconLeftName) : null;
7795
+ const iconRightSvg = iconRightName ? getIcon(iconRightName) : null;
7796
+ const leftOffset = iconLeftSvg ? iconPad + iconSize + iconInnerGap : 0;
7797
+ const rightOffset = iconRightSvg ? iconPad + iconSize + iconInnerGap : 0;
7798
+ const textX = pos.x + (iconLeftSvg ? leftOffset : paddingX);
7799
+ const iconColor = "#888888";
7800
+ const iconCenterY = controlY + (controlHeight - iconSize) / 2;
7801
+ let svg = `<g${this.getDataNodeId(node)}>`;
7802
+ if (label) {
7803
+ svg += `
7804
+ <text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
7482
7805
  font-family="${this.fontFamily}"
7483
7806
  font-size="12"
7484
- fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>` : ""}
7807
+ fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>`;
7808
+ }
7809
+ svg += `
7485
7810
  <rect x="${pos.x}" y="${controlY}"
7486
7811
  width="${pos.width}" height="${controlHeight}"
7487
7812
  rx="${radius}"
7488
7813
  fill="${this.renderTheme.cardBg}"
7489
7814
  stroke="#2D3748"
7490
7815
  stroke-width="0.5"
7491
- filter="url(#sketch-rough)"/>
7492
- ${placeholder ? `<text x="${pos.x + paddingX}" y="${controlY + controlHeight / 2 + 5}"
7816
+ filter="url(#sketch-rough)"/>`;
7817
+ if (iconLeftSvg) {
7818
+ svg += `
7819
+ <g transform="translate(${pos.x + iconPad}, ${iconCenterY})">
7820
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
7821
+ ${this.extractSvgContent(iconLeftSvg)}
7822
+ </svg>
7823
+ </g>`;
7824
+ }
7825
+ if (iconRightSvg) {
7826
+ svg += `
7827
+ <g transform="translate(${pos.x + pos.width - iconPad - iconSize}, ${iconCenterY})">
7828
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
7829
+ ${this.extractSvgContent(iconRightSvg)}
7830
+ </svg>
7831
+ </g>`;
7832
+ }
7833
+ if (placeholder) {
7834
+ const availWidth = pos.width - (iconLeftSvg ? leftOffset : paddingX) - (iconRightSvg ? rightOffset : paddingX);
7835
+ const visiblePh = this.truncateTextToWidth(placeholder, Math.max(0, availWidth), fontSize);
7836
+ svg += `
7837
+ <text x="${textX}" y="${controlY + controlHeight / 2 + 5}"
7493
7838
  font-family="${this.fontFamily}"
7494
7839
  font-size="${fontSize}"
7495
- fill="${this.renderTheme.textMuted}">${this.escapeXml(placeholder)}</text>` : ""}
7496
- </g>`;
7840
+ fill="${this.renderTheme.textMuted}">${this.escapeXml(visiblePh)}</text>`;
7841
+ }
7842
+ svg += "\n </g>";
7843
+ return svg;
7497
7844
  }
7498
7845
  /**
7499
7846
  * Render textarea with thicker border
@@ -7747,31 +8094,67 @@ var SketchSVGRenderer = class extends SVGRenderer {
7747
8094
  renderSelect(node, pos) {
7748
8095
  const label = String(node.props.label || "");
7749
8096
  const placeholder = String(node.props.placeholder || "Select...");
8097
+ const iconLeftName = String(node.props.iconLeft || "").trim();
8098
+ const iconRightName = String(node.props.iconRight || "").trim();
7750
8099
  const labelOffset = this.getControlLabelOffset(label);
7751
8100
  const controlY = pos.y + labelOffset;
7752
8101
  const controlHeight = Math.max(16, pos.height - labelOffset);
7753
8102
  const centerY = controlY + controlHeight / 2 + 5;
7754
- return `<g${this.getDataNodeId(node)}>
7755
- ${label ? `<text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
8103
+ const iconSize = 16;
8104
+ const iconPad = 12;
8105
+ const iconInnerGap = 8;
8106
+ const iconLeftSvg = iconLeftName ? getIcon(iconLeftName) : null;
8107
+ const iconRightSvg = iconRightName ? getIcon(iconRightName) : null;
8108
+ const leftOffset = iconLeftSvg ? iconPad + iconSize + iconInnerGap : 0;
8109
+ const chevronWidth = 20;
8110
+ const iconColor = "#888888";
8111
+ const iconCenterY = controlY + (controlHeight - iconSize) / 2;
8112
+ let svg = `<g${this.getDataNodeId(node)}>`;
8113
+ if (label) {
8114
+ svg += `
8115
+ <text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
7756
8116
  font-family="${this.fontFamily}"
7757
8117
  font-size="12"
7758
- fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>` : ""}
8118
+ fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>`;
8119
+ }
8120
+ svg += `
7759
8121
  <rect x="${pos.x}" y="${controlY}"
7760
8122
  width="${pos.width}" height="${controlHeight}"
7761
8123
  rx="6"
7762
8124
  fill="${this.renderTheme.cardBg}"
7763
8125
  stroke="#2D3748"
7764
8126
  stroke-width="0.5"
7765
- filter="url(#sketch-rough)"/>
7766
- <text x="${pos.x + 12}" y="${centerY}"
8127
+ filter="url(#sketch-rough)"/>`;
8128
+ if (iconLeftSvg) {
8129
+ svg += `
8130
+ <g transform="translate(${pos.x + iconPad}, ${iconCenterY})">
8131
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
8132
+ ${this.extractSvgContent(iconLeftSvg)}
8133
+ </svg>
8134
+ </g>`;
8135
+ }
8136
+ if (iconRightSvg) {
8137
+ svg += `
8138
+ <g transform="translate(${pos.x + pos.width - chevronWidth - iconPad - iconSize}, ${iconCenterY})">
8139
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
8140
+ ${this.extractSvgContent(iconRightSvg)}
8141
+ </svg>
8142
+ </g>`;
8143
+ }
8144
+ const textX = pos.x + (iconLeftSvg ? leftOffset : 12);
8145
+ const availWidth = pos.width - (iconLeftSvg ? leftOffset : 12) - chevronWidth - (iconRightSvg ? iconPad + iconSize + iconInnerGap : 0);
8146
+ const visiblePh = this.truncateTextToWidth(placeholder, Math.max(0, availWidth), 14);
8147
+ svg += `
8148
+ <text x="${textX}" y="${centerY}"
7767
8149
  font-family="${this.fontFamily}"
7768
8150
  font-size="14"
7769
- fill="${this.renderTheme.textMuted}">${this.escapeXml(placeholder)}</text>
8151
+ fill="${this.renderTheme.textMuted}">${this.escapeXml(visiblePh)}</text>
7770
8152
  <text x="${pos.x + pos.width - 20}" y="${centerY}"
7771
8153
  font-family="${this.fontFamily}"
7772
8154
  font-size="16"
7773
8155
  fill="${this.renderTheme.textMuted}">\u25BC</text>
7774
8156
  </g>`;
8157
+ return svg;
7775
8158
  }
7776
8159
  /**
7777
8160
  * Render checkbox with sketch filter and Comic Sans
@@ -8170,44 +8553,43 @@ var SketchSVGRenderer = class extends SVGRenderer {
8170
8553
  renderImage(node, pos) {
8171
8554
  const placeholder = String(node.props.placeholder || "landscape").toLowerCase();
8172
8555
  const iconType = String(node.props.icon || "").trim();
8556
+ const variant = String(node.props.variant || "").trim();
8173
8557
  const iconSvg = placeholder === "icon" && iconType.length > 0 ? getIcon(iconType) : null;
8558
+ const imageBg = this.options.theme === "dark" ? "#2A2A2A" : "#E8E8E8";
8174
8559
  if (iconSvg) {
8175
- const badgeSize = Math.max(24, Math.min(pos.width, pos.height) * 0.6);
8176
- const badgeX = pos.x + (pos.width - badgeSize) / 2;
8177
- const badgeY = pos.y + (pos.height - badgeSize) / 2;
8178
- const iconSize = badgeSize * 0.62;
8179
- const iconOffsetX = badgeX + (badgeSize - iconSize) / 2;
8180
- const iconOffsetY = badgeY + (badgeSize - iconSize) / 2;
8560
+ const semanticBase = variant ? this.getSemanticVariantColor(variant) : void 0;
8561
+ const hasVariant = variant.length > 0 && (semanticBase !== void 0 || this.colorResolver.hasColor(variant));
8562
+ const variantColor = hasVariant ? this.resolveVariantColor(variant, this.renderTheme.primary) : null;
8563
+ const bgColor = hasVariant ? this.hexToRgba(variantColor, 0.12) : imageBg;
8564
+ const iconColor = hasVariant ? variantColor : "#666666";
8565
+ const iconSize = Math.max(16, Math.min(pos.width, pos.height) * 0.6);
8566
+ const iconOffsetX = pos.x + (pos.width - iconSize) / 2;
8567
+ const iconOffsetY = pos.y + (pos.height - iconSize) / 2;
8181
8568
  return `<g${this.getDataNodeId(node)}>
8182
- <!-- Image Background -->
8183
8569
  <rect x="${pos.x}" y="${pos.y}"
8184
8570
  width="${pos.width}" height="${pos.height}"
8185
- fill="#E8E8E8"
8186
- stroke="#2D3748"
8187
- stroke-width="0.5"
8571
+ fill="${bgColor}"
8188
8572
  rx="4"
8189
8573
  filter="url(#sketch-rough)"/>
8190
-
8191
- <!-- Custom Icon Placeholder -->
8192
- <rect x="${badgeX}" y="${badgeY}"
8193
- width="${badgeSize}" height="${badgeSize}"
8194
- rx="${Math.max(4, badgeSize * 0.2)}"
8195
- fill="none"
8196
- stroke="#2D3748"
8197
- stroke-width="0.5"
8198
- filter="url(#sketch-rough)"/>
8199
8574
  <g transform="translate(${iconOffsetX}, ${iconOffsetY})">
8200
- <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="#2D3748" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
8575
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
8201
8576
  ${this.extractSvgContent(iconSvg)}
8202
8577
  </svg>
8203
8578
  </g>
8579
+ <rect x="${pos.x}" y="${pos.y}"
8580
+ width="${pos.width}" height="${pos.height}"
8581
+ fill="none"
8582
+ stroke="#2D3748"
8583
+ stroke-width="0.5"
8584
+ rx="4"
8585
+ filter="url(#sketch-rough)"/>
8204
8586
  </g>`;
8205
8587
  }
8206
8588
  return `<g${this.getDataNodeId(node)}>
8207
8589
  <!-- Image Background -->
8208
8590
  <rect x="${pos.x}" y="${pos.y}"
8209
8591
  width="${pos.width}" height="${pos.height}"
8210
- fill="#E8E8E8"
8592
+ fill="${imageBg}"
8211
8593
  stroke="#2D3748"
8212
8594
  stroke-width="0.5"
8213
8595
  rx="4"
@@ -8262,17 +8644,23 @@ var SketchSVGRenderer = class extends SVGRenderer {
8262
8644
  */
8263
8645
  renderSidebarMenu(node, pos) {
8264
8646
  const itemsStr = String(node.props.items || "Item 1,Item 2,Item 3");
8647
+ const iconsStr = String(node.props.icons || "");
8265
8648
  const items = itemsStr.split(",").map((s) => s.trim());
8649
+ const icons = iconsStr ? iconsStr.split(",").map((s) => s.trim()) : [];
8266
8650
  const itemHeight = 40;
8267
8651
  const fontSize = 14;
8268
8652
  const activeIndex = Number(node.props.active || 0);
8269
8653
  const accentColor = this.resolveAccentColor();
8654
+ const variantProp = String(node.props.variant || "").trim();
8655
+ const semanticVariant = variantProp ? this.getSemanticVariantColor(variantProp) : void 0;
8656
+ const hasVariant = variantProp.length > 0 && (semanticVariant !== void 0 || this.colorResolver.hasColor(variantProp));
8657
+ const activeColor = hasVariant ? this.resolveVariantColor(variantProp, accentColor) : accentColor;
8270
8658
  let svg = `<g${this.getDataNodeId(node)}>`;
8271
8659
  items.forEach((item, index) => {
8272
8660
  const itemY = pos.y + index * itemHeight;
8273
8661
  const isActive = index === activeIndex;
8274
- const bgColor = isActive ? this.hexToRgba(accentColor, 0.15) : "transparent";
8275
- const textColor = isActive ? accentColor : this.resolveTextColor();
8662
+ const bgColor = isActive ? this.hexToRgba(activeColor, 0.15) : "transparent";
8663
+ const textColor = isActive ? activeColor : this.resolveTextColor();
8276
8664
  const fontWeight = isActive ? "500" : "400";
8277
8665
  if (isActive) {
8278
8666
  svg += `
@@ -8282,8 +8670,24 @@ var SketchSVGRenderer = class extends SVGRenderer {
8282
8670
  fill="${bgColor}"
8283
8671
  filter="url(#sketch-rough)"/>`;
8284
8672
  }
8673
+ let currentX = pos.x + 12;
8674
+ if (icons[index]) {
8675
+ const iconSvg = getIcon(icons[index]);
8676
+ if (iconSvg) {
8677
+ const iconSize = 16;
8678
+ const iconY = itemY + (itemHeight - iconSize) / 2;
8679
+ const iconColor = isActive ? activeColor : this.resolveMutedColor();
8680
+ svg += `
8681
+ <g transform="translate(${currentX}, ${iconY})">
8682
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
8683
+ ${this.extractSvgContent(iconSvg)}
8684
+ </svg>
8685
+ </g>`;
8686
+ currentX += iconSize + 8;
8687
+ }
8688
+ }
8285
8689
  svg += `
8286
- <text x="${pos.x + 12}" y="${itemY + itemHeight / 2 + 5}"
8690
+ <text x="${currentX}" y="${itemY + itemHeight / 2 + 5}"
8287
8691
  font-family="${this.fontFamily}"
8288
8692
  font-size="${fontSize}"
8289
8693
  font-weight="${fontWeight}"