@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.cjs CHANGED
@@ -1696,7 +1696,8 @@ function validateSemanticDiagnostics(ast, sourceMap) {
1696
1696
  if (enumValues) {
1697
1697
  const normalizedValue = String(propValue);
1698
1698
  const isCustomVariantFromColors = propName === "variant" && !enumValues.includes(normalizedValue) && Object.prototype.hasOwnProperty.call(ast.colors || {}, normalizedValue);
1699
- if (!enumValues.includes(normalizedValue) && !isCustomVariantFromColors) {
1699
+ const isPropReference = normalizedValue.startsWith("prop_");
1700
+ if (!enumValues.includes(normalizedValue) && !isCustomVariantFromColors && !isPropReference) {
1700
1701
  emitWarning(
1701
1702
  `Invalid value "${normalizedValue}" for property "${propName}" in component "${componentType}".`,
1702
1703
  "COMPONENT_INVALID_PROPERTY_VALUE",
@@ -1806,7 +1807,7 @@ function validateSemanticDiagnostics(ast, sourceMap) {
1806
1807
  const enumValues = rules.enumParams?.[paramName];
1807
1808
  if (enumValues) {
1808
1809
  const normalizedValue = String(paramValue);
1809
- if (!enumValues.includes(normalizedValue)) {
1810
+ if (!enumValues.includes(normalizedValue) && !normalizedValue.startsWith("prop_")) {
1810
1811
  emitWarning(
1811
1812
  `Invalid value "${normalizedValue}" for parameter "${paramName}" in layout "${layout.layoutType}".`,
1812
1813
  "LAYOUT_INVALID_PARAMETER_VALUE",
@@ -2244,7 +2245,21 @@ var IRComponentNodeSchema = import_zod.z.object({
2244
2245
  style: IRNodeStyleSchema,
2245
2246
  meta: IRMetaSchema
2246
2247
  });
2247
- var IRNodeSchema = import_zod.z.union([IRContainerNodeSchema, IRComponentNodeSchema]);
2248
+ var IRInstanceNodeSchema = import_zod.z.object({
2249
+ id: import_zod.z.string(),
2250
+ kind: import_zod.z.literal("instance"),
2251
+ definitionName: import_zod.z.string(),
2252
+ definitionKind: import_zod.z.enum(["component", "layout"]),
2253
+ invocationProps: import_zod.z.record(import_zod.z.string(), import_zod.z.union([import_zod.z.string(), import_zod.z.number()])),
2254
+ expandedRoot: import_zod.z.object({ ref: import_zod.z.string() }),
2255
+ style: IRNodeStyleSchema,
2256
+ meta: IRMetaSchema
2257
+ });
2258
+ var IRNodeSchema = import_zod.z.discriminatedUnion("kind", [
2259
+ IRContainerNodeSchema,
2260
+ IRComponentNodeSchema,
2261
+ IRInstanceNodeSchema
2262
+ ]);
2248
2263
  var IRScreenSchema = import_zod.z.object({
2249
2264
  id: import_zod.z.string(),
2250
2265
  name: import_zod.z.string(),
@@ -2467,7 +2482,7 @@ ${messages}`);
2467
2482
  const layoutChildren = layout.children;
2468
2483
  const layoutDefinition = this.definedLayouts.get(layout.layoutType);
2469
2484
  if (layoutDefinition) {
2470
- return this.expandDefinedLayout(layoutDefinition, layoutParams, layoutChildren, context);
2485
+ return this.expandDefinedLayout(layoutDefinition, layoutParams, layoutChildren, context, layout._meta?.nodeId);
2471
2486
  }
2472
2487
  const nodeId = this.idGen.generate("node");
2473
2488
  const childRefs = [];
@@ -2506,8 +2521,8 @@ ${messages}`);
2506
2521
  children: childRefs,
2507
2522
  style,
2508
2523
  meta: {
2509
- nodeId: layout._meta?.nodeId
2510
- // Pass SourceMap nodeId from AST
2524
+ // Scope nodeId per instance so each expansion gets a unique identifier
2525
+ nodeId: context?.instanceScope ? `${layout._meta?.nodeId}@${context.instanceScope}` : layout._meta?.nodeId
2511
2526
  }
2512
2527
  };
2513
2528
  this.nodes[nodeId] = containerNode;
@@ -2536,8 +2551,7 @@ ${messages}`);
2536
2551
  // Cells have no padding by default - grid gap handles spacing
2537
2552
  meta: {
2538
2553
  source: "cell",
2539
- nodeId: cell._meta?.nodeId
2540
- // Pass SourceMap nodeId from AST
2554
+ nodeId: context?.instanceScope ? `${cell._meta?.nodeId}@${context.instanceScope}` : cell._meta?.nodeId
2541
2555
  }
2542
2556
  };
2543
2557
  this.nodes[nodeId] = containerNode;
@@ -2564,7 +2578,7 @@ ${messages}`);
2564
2578
  const resolvedProps = this.resolveComponentProps(component.componentType, component.props, context);
2565
2579
  const definition = this.definedComponents.get(component.componentType);
2566
2580
  if (definition) {
2567
- return this.expandDefinedComponent(definition, resolvedProps, context);
2581
+ return this.expandDefinedComponent(definition, resolvedProps, component._meta?.nodeId, context);
2568
2582
  }
2569
2583
  const builtInComponents = /* @__PURE__ */ new Set([
2570
2584
  "Button",
@@ -2610,35 +2624,49 @@ ${messages}`);
2610
2624
  props: resolvedProps,
2611
2625
  style: {},
2612
2626
  meta: {
2613
- nodeId: component._meta?.nodeId
2614
- // Pass SourceMap nodeId from AST
2627
+ // Scope nodeId per instance so each expansion gets a unique identifier
2628
+ nodeId: context?.instanceScope ? `${component._meta?.nodeId}@${context.instanceScope}` : component._meta?.nodeId
2615
2629
  }
2616
2630
  };
2617
2631
  this.nodes[nodeId] = componentNode;
2618
2632
  return nodeId;
2619
2633
  }
2620
- expandDefinedComponent(definition, invocationArgs, parentContext) {
2634
+ expandDefinedComponent(definition, invocationArgs, callSiteNodeId, parentContext) {
2621
2635
  const context = {
2622
2636
  args: invocationArgs,
2623
2637
  providedArgNames: new Set(Object.keys(invocationArgs)),
2624
2638
  usedArgNames: /* @__PURE__ */ new Set(),
2625
2639
  definitionName: definition.name,
2626
2640
  definitionKind: "component",
2627
- allowChildrenSlot: false
2641
+ allowChildrenSlot: false,
2642
+ // Scope internal nodeIds using the call-site nodeId
2643
+ instanceScope: callSiteNodeId
2628
2644
  };
2645
+ let expandedRootId = null;
2629
2646
  if (definition.body.type === "layout") {
2630
- const result = this.convertLayout(definition.body, context);
2631
- this.reportUnusedArguments(context);
2632
- return result;
2647
+ expandedRootId = this.convertLayout(definition.body, context);
2633
2648
  } else if (definition.body.type === "component") {
2634
- const result = this.convertComponent(definition.body, context);
2635
- this.reportUnusedArguments(context);
2636
- return result;
2649
+ expandedRootId = this.convertComponent(definition.body, context);
2637
2650
  } else {
2638
2651
  throw new Error(`Invalid defined component body type for "${definition.name}"`);
2639
2652
  }
2653
+ this.reportUnusedArguments(context);
2654
+ if (!expandedRootId) return null;
2655
+ const instanceNodeId = this.idGen.generate("node");
2656
+ const instanceNode = {
2657
+ id: instanceNodeId,
2658
+ kind: "instance",
2659
+ definitionName: definition.name,
2660
+ definitionKind: "component",
2661
+ invocationProps: invocationArgs,
2662
+ expandedRoot: { ref: expandedRootId },
2663
+ style: {},
2664
+ meta: { nodeId: callSiteNodeId }
2665
+ };
2666
+ this.nodes[instanceNodeId] = instanceNode;
2667
+ return instanceNodeId;
2640
2668
  }
2641
- expandDefinedLayout(definition, invocationParams, invocationChildren, parentContext) {
2669
+ expandDefinedLayout(definition, invocationParams, invocationChildren, parentContext, callSiteNodeId) {
2642
2670
  if (invocationChildren.length !== 1) {
2643
2671
  this.errors.push({
2644
2672
  type: "layout-children-arity",
@@ -2654,11 +2682,26 @@ ${messages}`);
2654
2682
  definitionName: definition.name,
2655
2683
  definitionKind: "layout",
2656
2684
  allowChildrenSlot: true,
2657
- childrenSlot: resolvedSlot
2685
+ childrenSlot: resolvedSlot,
2686
+ // Scope internal nodeIds using the call-site nodeId
2687
+ instanceScope: callSiteNodeId
2658
2688
  };
2659
- const nodeId = this.convertLayout(definition.body, context);
2689
+ const expandedRootId = this.convertLayout(definition.body, context);
2660
2690
  this.reportUnusedArguments(context);
2661
- return nodeId;
2691
+ if (!callSiteNodeId) return expandedRootId;
2692
+ const instanceNodeId = this.idGen.generate("node");
2693
+ const instanceNode = {
2694
+ id: instanceNodeId,
2695
+ kind: "instance",
2696
+ definitionName: definition.name,
2697
+ definitionKind: "layout",
2698
+ invocationProps: invocationParams,
2699
+ expandedRoot: { ref: expandedRootId },
2700
+ style: {},
2701
+ meta: { nodeId: callSiteNodeId }
2702
+ };
2703
+ this.nodes[instanceNodeId] = instanceNode;
2704
+ return instanceNodeId;
2662
2705
  }
2663
2706
  resolveChildrenSlot(slot, parentContext) {
2664
2707
  if (slot.type === "component" && slot.componentType === "Children") {
@@ -2689,6 +2732,20 @@ ${messages}`);
2689
2732
  key
2690
2733
  );
2691
2734
  if (resolvedValue !== void 0) {
2735
+ const wasPropReference = typeof value === "string" && value.startsWith("prop_");
2736
+ if (wasPropReference) {
2737
+ const layoutMetadata = import_components2.LAYOUTS[layoutType];
2738
+ const property = layoutMetadata?.properties?.[key];
2739
+ if (property?.type === "enum" && Array.isArray(property.options)) {
2740
+ const normalizedValue = String(resolvedValue);
2741
+ if (!property.options.includes(normalizedValue)) {
2742
+ this.warnings.push({
2743
+ type: "invalid-bound-enum-value",
2744
+ message: `Invalid value "${normalizedValue}" for parameter "${key}" in layout "${layoutType}". Expected one of: ${property.options.join(", ")}.`
2745
+ });
2746
+ }
2747
+ }
2748
+ }
2692
2749
  resolved[key] = resolvedValue;
2693
2750
  }
2694
2751
  }
@@ -2753,6 +2810,20 @@ ${messages}`);
2753
2810
  key
2754
2811
  );
2755
2812
  if (resolvedValue !== void 0) {
2813
+ const wasPropReference = typeof value === "string" && value.startsWith("prop_");
2814
+ if (wasPropReference) {
2815
+ const metadata = import_components2.COMPONENTS[componentType];
2816
+ const property = metadata?.properties?.[key];
2817
+ if (property?.type === "enum" && Array.isArray(property.options)) {
2818
+ const normalizedValue = String(resolvedValue);
2819
+ if (!property.options.includes(normalizedValue)) {
2820
+ this.warnings.push({
2821
+ type: "invalid-bound-enum-value",
2822
+ message: `Invalid value "${normalizedValue}" for property "${key}" in component "${componentType}". Expected one of: ${property.options.join(", ")}.`
2823
+ });
2824
+ }
2825
+ }
2826
+ }
2756
2827
  resolved[key] = resolvedValue;
2757
2828
  }
2758
2829
  }
@@ -2996,6 +3067,8 @@ var LayoutEngine = class {
2996
3067
  }
2997
3068
  if (node.kind === "container") {
2998
3069
  this.calculateContainer(node, nodeId, x, y, width, height);
3070
+ } else if (node.kind === "instance") {
3071
+ this.calculateInstance(node, nodeId, x, y, width, height, parentContainerType);
2999
3072
  } else {
3000
3073
  this.calculateComponent(node, nodeId, x, y, width, height);
3001
3074
  }
@@ -3028,7 +3101,7 @@ var LayoutEngine = class {
3028
3101
  this.calculateCard(node, innerX, innerY, innerWidth, innerHeight);
3029
3102
  break;
3030
3103
  }
3031
- if (isVerticalStack || node.containerType === "card") {
3104
+ if ((isVerticalStack || node.containerType === "card") && node.children.length > 0) {
3032
3105
  let containerMaxY = y;
3033
3106
  node.children.forEach((childRef) => {
3034
3107
  const childPos = this.result[childRef.ref];
@@ -3171,6 +3244,10 @@ var LayoutEngine = class {
3171
3244
  const gap = this.resolveSpacing(node.style.gap);
3172
3245
  const padding = this.resolveSpacing(node.style.padding);
3173
3246
  let totalHeight = padding * 2;
3247
+ const EMPTY_CONTAINER_MIN_HEIGHT = 40;
3248
+ if (node.children.length === 0) {
3249
+ return Math.max(totalHeight, EMPTY_CONTAINER_MIN_HEIGHT);
3250
+ }
3174
3251
  if (node.containerType === "grid") {
3175
3252
  const columns = Number(node.params.columns) || 12;
3176
3253
  const colWidth = (availableWidth - gap * (columns - 1)) / columns;
@@ -3423,6 +3500,20 @@ var LayoutEngine = class {
3423
3500
  }
3424
3501
  });
3425
3502
  }
3503
+ /**
3504
+ * Calculate layout for an instance node.
3505
+ * The instance is a transparent wrapper — its bounding box equals the
3506
+ * expanded root's bounding box. We calculate the expanded root first and
3507
+ * then copy its position to the instance nodeId so the renderer can use it.
3508
+ */
3509
+ calculateInstance(node, nodeId, x, y, width, height, parentContainerType) {
3510
+ const expandedRootId = node.expandedRoot.ref;
3511
+ this.calculateNode(expandedRootId, x, y, width, height, parentContainerType);
3512
+ const expandedPos = this.result[expandedRootId];
3513
+ if (expandedPos) {
3514
+ this.result[nodeId] = { ...expandedPos };
3515
+ }
3516
+ }
3426
3517
  calculateComponent(node, nodeId, x, y, width, height) {
3427
3518
  if (node.kind !== "component") return;
3428
3519
  const componentWidth = Number(node.props.width) || width;
@@ -4563,14 +4654,14 @@ var THEMES = {
4563
4654
  primaryLight: "#EFF6FF"
4564
4655
  },
4565
4656
  dark: {
4566
- bg: "#0F172A",
4567
- cardBg: "#1E293B",
4568
- border: "#334155",
4569
- text: "#FFFFFF",
4570
- textMuted: "#94A3B8",
4657
+ bg: "#111111",
4658
+ cardBg: "#1C1C1C",
4659
+ border: "#303030",
4660
+ text: "#F0F0F0",
4661
+ textMuted: "#808080",
4571
4662
  primary: "#60A5FA",
4572
4663
  primaryHover: "#3B82F6",
4573
- primaryLight: "#1E3A8A"
4664
+ primaryLight: "#1C2A3A"
4574
4665
  }
4575
4666
  };
4576
4667
  var SVGRenderer = class {
@@ -4588,7 +4679,8 @@ var SVGRenderer = class {
4588
4679
  height: options?.height || 720,
4589
4680
  theme: colorScheme,
4590
4681
  includeLabels: options?.includeLabels ?? true,
4591
- screenName: options?.screenName
4682
+ screenName: options?.screenName,
4683
+ showDiagnostics: options?.showDiagnostics ?? false
4592
4684
  };
4593
4685
  this.colorResolver = new ColorResolver();
4594
4686
  this.buildParentContainerIndex();
@@ -4688,13 +4780,30 @@ var SVGRenderer = class {
4688
4780
  if (node.containerType === "split") {
4689
4781
  this.renderSplitDecoration(node, pos, containerGroup);
4690
4782
  }
4691
- node.children.forEach((childRef) => {
4692
- this.renderNode(childRef.ref, containerGroup);
4693
- });
4783
+ if (node.children.length === 0 && this.options.showDiagnostics) {
4784
+ containerGroup.push(this.renderEmptyContainerDiagnostic(pos, node.containerType));
4785
+ } else {
4786
+ node.children.forEach((childRef) => {
4787
+ this.renderNode(childRef.ref, containerGroup);
4788
+ });
4789
+ }
4694
4790
  if (hasNodeId) {
4695
4791
  containerGroup.push("</g>");
4696
4792
  }
4697
4793
  output.push(...containerGroup);
4794
+ } else if (node.kind === "instance") {
4795
+ const instanceGroup = [];
4796
+ if (node.meta.nodeId) {
4797
+ instanceGroup.push(`<g data-node-id="${node.meta.nodeId}">`);
4798
+ }
4799
+ instanceGroup.push(
4800
+ `<rect x="${pos.x}" y="${pos.y}" width="${pos.width}" height="${pos.height}" fill="transparent" stroke="none" pointer-events="all"/>`
4801
+ );
4802
+ this.renderNode(node.expandedRoot.ref, instanceGroup);
4803
+ if (node.meta.nodeId) {
4804
+ instanceGroup.push("</g>");
4805
+ }
4806
+ output.push(...instanceGroup);
4698
4807
  } else if (node.kind === "component") {
4699
4808
  const componentSvg = this.renderComponent(node, pos);
4700
4809
  if (componentSvg) {
@@ -4811,6 +4920,8 @@ var SVGRenderer = class {
4811
4920
  const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
4812
4921
  const labelOffset = this.parseBooleanProp(node.props.labelSpace, false) ? 18 : 0;
4813
4922
  const fullWidth = this.shouldButtonFillAvailableWidth(node);
4923
+ const iconName = String(node.props.icon || "").trim();
4924
+ const iconAlign = String(node.props.iconAlign || "left").toLowerCase();
4814
4925
  const radius = this.tokens.button.radius;
4815
4926
  const fontSize = this.tokens.button.fontSize;
4816
4927
  const fontWeight = this.tokens.button.fontWeight;
@@ -4818,30 +4929,62 @@ var SVGRenderer = class {
4818
4929
  const controlHeight = resolveActionControlHeight(size, density);
4819
4930
  const buttonY = pos.y + labelOffset;
4820
4931
  const buttonHeight = Math.max(16, Math.min(controlHeight, pos.height - labelOffset));
4932
+ const iconSvg = iconName ? getIcon(iconName) : null;
4933
+ const iconSize = iconSvg ? Math.round(fontSize * 1.1) : 0;
4934
+ const iconGap = iconSvg ? 8 : 0;
4935
+ const edgePad = 12;
4936
+ const textPad = paddingX + extraPadding;
4821
4937
  const idealTextWidth = this.estimateTextWidth(text, fontSize);
4822
- const buttonWidth = fullWidth ? Math.max(1, pos.width) : this.clampControlWidth(Math.max(Math.ceil(idealTextWidth + (paddingX + extraPadding) * 2), 60), pos.width);
4823
- const availableTextWidth = Math.max(0, buttonWidth - (paddingX + extraPadding) * 2);
4938
+ const buttonWidth = fullWidth ? Math.max(1, pos.width) : this.clampControlWidth(Math.max(Math.ceil(idealTextWidth + (iconSvg ? iconSize + iconGap : 0) + textPad * 2), 60), pos.width);
4939
+ const availableTextWidth = Math.max(0, buttonWidth - textPad * 2 - (iconSvg ? iconSize + iconGap : 0));
4824
4940
  const visibleText = this.truncateTextToWidth(text, availableTextWidth, fontSize);
4825
4941
  const semanticBase = this.getSemanticVariantColor(variant);
4826
4942
  const hasExplicitVariantColor = semanticBase !== void 0 || this.colorResolver.hasColor(variant);
4827
4943
  const resolvedBase = this.resolveVariantColor(variant, this.renderTheme.primary);
4828
- const bgColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.85) : "rgba(226, 232, 240, 0.9)";
4944
+ const isDarkMode = this.options.theme === "dark";
4945
+ const bgColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.85) : isDarkMode ? "rgba(48, 48, 55, 0.9)" : "rgba(226, 232, 240, 0.9)";
4829
4946
  const textColor = hasExplicitVariantColor ? "#FFFFFF" : this.hexToRgba(this.resolveTextColor(), 0.85);
4830
- const borderColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.7) : "rgba(100, 116, 139, 0.4)";
4831
- return `<g${this.getDataNodeId(node)}>
4947
+ const borderColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.7) : isDarkMode ? "rgba(75, 75, 88, 0.8)" : "rgba(100, 116, 139, 0.4)";
4948
+ const iconOffsetY = buttonY + (buttonHeight - iconSize) / 2;
4949
+ const iconX = iconAlign === "right" ? pos.x + buttonWidth - edgePad - iconSize : pos.x + edgePad;
4950
+ const textAlign = String(node.props.align || "center").toLowerCase();
4951
+ const sidePad = textPad + 4;
4952
+ let textX;
4953
+ let textAnchor;
4954
+ if (textAlign === "left") {
4955
+ textX = iconSvg && iconAlign === "left" ? pos.x + edgePad + iconSize + iconGap : pos.x + sidePad;
4956
+ textAnchor = "start";
4957
+ } else if (textAlign === "right") {
4958
+ textX = iconSvg && iconAlign === "right" ? pos.x + buttonWidth - edgePad - iconSize - iconGap : pos.x + buttonWidth - sidePad;
4959
+ textAnchor = "end";
4960
+ } else {
4961
+ textX = pos.x + buttonWidth / 2;
4962
+ textAnchor = "middle";
4963
+ }
4964
+ let svg = `<g${this.getDataNodeId(node)}>
4832
4965
  <rect x="${pos.x}" y="${buttonY}"
4833
4966
  width="${buttonWidth}" height="${buttonHeight}"
4834
4967
  rx="${radius}"
4835
4968
  fill="${bgColor}"
4836
4969
  stroke="${borderColor}"
4837
- stroke-width="1"/>
4838
- <text x="${pos.x + buttonWidth / 2}" y="${buttonY + buttonHeight / 2 + fontSize * 0.35}"
4970
+ stroke-width="1"/>`;
4971
+ if (iconSvg) {
4972
+ svg += `
4973
+ <g transform="translate(${iconX}, ${iconOffsetY})">
4974
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${textColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
4975
+ ${this.extractSvgContent(iconSvg)}
4976
+ </svg>
4977
+ </g>`;
4978
+ }
4979
+ svg += `
4980
+ <text x="${textX}" y="${buttonY + buttonHeight / 2 + fontSize * 0.35}"
4839
4981
  font-family="Arial, Helvetica, sans-serif"
4840
4982
  font-size="${fontSize}"
4841
4983
  font-weight="${fontWeight}"
4842
4984
  fill="${textColor}"
4843
- text-anchor="middle">${this.escapeXml(visibleText)}</text>
4985
+ text-anchor="${textAnchor}">${this.escapeXml(visibleText)}</text>
4844
4986
  </g>`;
4987
+ return svg;
4845
4988
  }
4846
4989
  renderLink(node, pos) {
4847
4990
  const text = String(node.props.text || "Link");
@@ -4882,28 +5025,66 @@ var SVGRenderer = class {
4882
5025
  renderInput(node, pos) {
4883
5026
  const label = String(node.props.label || "");
4884
5027
  const placeholder = String(node.props.placeholder || "");
5028
+ const iconLeftName = String(node.props.iconLeft || "").trim();
5029
+ const iconRightName = String(node.props.iconRight || "").trim();
4885
5030
  const radius = this.tokens.input.radius;
4886
5031
  const fontSize = this.tokens.input.fontSize;
4887
5032
  const paddingX = this.tokens.input.paddingX;
4888
5033
  const labelOffset = this.getControlLabelOffset(label);
4889
5034
  const controlY = pos.y + labelOffset;
4890
5035
  const controlHeight = Math.max(16, pos.height - labelOffset);
4891
- return `<g${this.getDataNodeId(node)}>
4892
- ${label ? `<text x="${pos.x + paddingX}" y="${this.getControlLabelBaselineY(pos.y)}"
5036
+ const iconSize = 16;
5037
+ const iconPad = 12;
5038
+ const iconInnerGap = 8;
5039
+ const iconLeftSvg = iconLeftName ? getIcon(iconLeftName) : null;
5040
+ const iconRightSvg = iconRightName ? getIcon(iconRightName) : null;
5041
+ const leftOffset = iconLeftSvg ? iconPad + iconSize + iconInnerGap : 0;
5042
+ const rightOffset = iconRightSvg ? iconPad + iconSize + iconInnerGap : 0;
5043
+ const textX = pos.x + (iconLeftSvg ? leftOffset : paddingX);
5044
+ const iconColor = this.hexToRgba(this.resolveMutedColor(), 0.8);
5045
+ const iconCenterY = controlY + (controlHeight - iconSize) / 2;
5046
+ let svg = `<g${this.getDataNodeId(node)}>`;
5047
+ if (label) {
5048
+ svg += `
5049
+ <text x="${pos.x + paddingX}" y="${this.getControlLabelBaselineY(pos.y)}"
4893
5050
  font-family="Arial, Helvetica, sans-serif"
4894
5051
  font-size="12"
4895
- fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>` : ""}
5052
+ fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>`;
5053
+ }
5054
+ svg += `
4896
5055
  <rect x="${pos.x}" y="${controlY}"
4897
5056
  width="${pos.width}" height="${controlHeight}"
4898
5057
  rx="${radius}"
4899
5058
  fill="${this.renderTheme.cardBg}"
4900
5059
  stroke="${this.renderTheme.border}"
4901
- stroke-width="1"/>
4902
- <text x="${pos.x + paddingX}" y="${controlY + controlHeight / 2 + 5}"
5060
+ stroke-width="1"/>`;
5061
+ if (iconLeftSvg) {
5062
+ svg += `
5063
+ <g transform="translate(${pos.x + iconPad}, ${iconCenterY})">
5064
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
5065
+ ${this.extractSvgContent(iconLeftSvg)}
5066
+ </svg>
5067
+ </g>`;
5068
+ }
5069
+ if (iconRightSvg) {
5070
+ svg += `
5071
+ <g transform="translate(${pos.x + pos.width - iconPad - iconSize}, ${iconCenterY})">
5072
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
5073
+ ${this.extractSvgContent(iconRightSvg)}
5074
+ </svg>
5075
+ </g>`;
5076
+ }
5077
+ if (placeholder) {
5078
+ const availPlaceholderWidth = pos.width - (iconLeftSvg ? leftOffset : paddingX) - (iconRightSvg ? rightOffset : paddingX);
5079
+ const visiblePlaceholder = this.truncateTextToWidth(placeholder, Math.max(0, availPlaceholderWidth), fontSize);
5080
+ svg += `
5081
+ <text x="${textX}" y="${controlY + controlHeight / 2 + 5}"
4903
5082
  font-family="Arial, Helvetica, sans-serif"
4904
5083
  font-size="${fontSize}"
4905
- fill="${this.renderTheme.textMuted}">${this.escapeXml(placeholder)}</text>
4906
- </g>`;
5084
+ fill="${this.renderTheme.textMuted}">${this.escapeXml(visiblePlaceholder)}</text>`;
5085
+ }
5086
+ svg += "\n </g>";
5087
+ return svg;
4907
5088
  }
4908
5089
  renderTopbar(node, pos) {
4909
5090
  const title = String(node.props.title || "App");
@@ -4913,7 +5094,7 @@ var SVGRenderer = class {
4913
5094
  const variant = String(node.props.variant || "default");
4914
5095
  const accentColor = variant === "default" ? this.resolveAccentColor() : this.resolveVariantColor(variant, this.resolveAccentColor());
4915
5096
  const showBorder = this.parseBooleanProp(node.props.border, false);
4916
- const showBackground = this.parseBooleanProp(node.props.background ?? node.props.backround, false);
5097
+ const showBackground = this.parseBooleanProp(node.props.background, false);
4917
5098
  const radiusMap = {
4918
5099
  none: 0,
4919
5100
  sm: 4,
@@ -5052,6 +5233,21 @@ var SVGRenderer = class {
5052
5233
  </g>`;
5053
5234
  output.push(svg);
5054
5235
  }
5236
+ /**
5237
+ * Renders a yellow warning placeholder for containers with no children.
5238
+ * Only shown when `showDiagnostics` is enabled (editor/canvas mode).
5239
+ */
5240
+ renderEmptyContainerDiagnostic(pos, containerType) {
5241
+ const diagColor = "#F59E0B";
5242
+ const diagBg = "#FFFBEB";
5243
+ const diagText = "#92400E";
5244
+ const minHeight = 40;
5245
+ const h = Math.max(pos.height, minHeight);
5246
+ const cx = pos.x + pos.width / 2;
5247
+ const cy = pos.y + h / 2;
5248
+ const label = containerType ? `Empty ${containerType}` : "Empty layout";
5249
+ 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>`;
5250
+ }
5055
5251
  renderSplitDecoration(node, pos, output) {
5056
5252
  if (node.kind !== "container") return;
5057
5253
  const gap = this.resolveSpacing(node.style.gap);
@@ -5100,7 +5296,7 @@ var SVGRenderer = class {
5100
5296
  const hasCaption = caption.length > 0;
5101
5297
  const showOuterBorder = this.parseBooleanProp(node.props.border, false);
5102
5298
  const showOuterBackground = this.parseBooleanProp(
5103
- node.props.background ?? node.props.backround,
5299
+ node.props.background,
5104
5300
  false
5105
5301
  );
5106
5302
  const showInnerBorder = this.parseBooleanProp(node.props.innerBorder, true);
@@ -5483,30 +5679,66 @@ var SVGRenderer = class {
5483
5679
  renderSelect(node, pos) {
5484
5680
  const label = String(node.props.label || "");
5485
5681
  const placeholder = String(node.props.placeholder || "Select...");
5682
+ const iconLeftName = String(node.props.iconLeft || "").trim();
5683
+ const iconRightName = String(node.props.iconRight || "").trim();
5486
5684
  const labelOffset = this.getControlLabelOffset(label);
5487
5685
  const controlY = pos.y + labelOffset;
5488
5686
  const controlHeight = Math.max(16, pos.height - labelOffset);
5489
5687
  const centerY = controlY + controlHeight / 2 + 5;
5490
- return `<g${this.getDataNodeId(node)}>
5491
- ${label ? `<text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
5492
- font-family="Arial, Helvetica, sans-serif"
5493
- font-size="12"
5494
- fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>` : ""}
5495
- <rect x="${pos.x}" y="${controlY}"
5496
- width="${pos.width}" height="${controlHeight}"
5497
- rx="6"
5498
- fill="${this.renderTheme.cardBg}"
5499
- stroke="${this.renderTheme.border}"
5500
- stroke-width="1"/>
5501
- <text x="${pos.x + 12}" y="${centerY}"
5502
- font-family="Arial, Helvetica, sans-serif"
5503
- font-size="14"
5504
- fill="${this.renderTheme.textMuted}">${this.escapeXml(placeholder)}</text>
5505
- <text x="${pos.x + pos.width - 20}" y="${centerY}"
5506
- font-family="Arial, Helvetica, sans-serif"
5507
- font-size="16"
5688
+ const iconSize = 16;
5689
+ const iconPad = 12;
5690
+ const iconInnerGap = 8;
5691
+ const iconLeftSvg = iconLeftName ? getIcon(iconLeftName) : null;
5692
+ const iconRightSvg = iconRightName ? getIcon(iconRightName) : null;
5693
+ const leftOffset = iconLeftSvg ? iconPad + iconSize + iconInnerGap : 0;
5694
+ const chevronWidth = 20;
5695
+ const iconColor = this.hexToRgba(this.resolveMutedColor(), 0.8);
5696
+ const iconCenterY = controlY + (controlHeight - iconSize) / 2;
5697
+ let svg = `<g${this.getDataNodeId(node)}>`;
5698
+ if (label) {
5699
+ svg += `
5700
+ <text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
5701
+ font-family="Arial, Helvetica, sans-serif"
5702
+ font-size="12"
5703
+ fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>`;
5704
+ }
5705
+ svg += `
5706
+ <rect x="${pos.x}" y="${controlY}"
5707
+ width="${pos.width}" height="${controlHeight}"
5708
+ rx="6"
5709
+ fill="${this.renderTheme.cardBg}"
5710
+ stroke="${this.renderTheme.border}"
5711
+ stroke-width="1"/>`;
5712
+ if (iconLeftSvg) {
5713
+ svg += `
5714
+ <g transform="translate(${pos.x + iconPad}, ${iconCenterY})">
5715
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
5716
+ ${this.extractSvgContent(iconLeftSvg)}
5717
+ </svg>
5718
+ </g>`;
5719
+ }
5720
+ if (iconRightSvg) {
5721
+ svg += `
5722
+ <g transform="translate(${pos.x + pos.width - chevronWidth - iconPad - iconSize}, ${iconCenterY})">
5723
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
5724
+ ${this.extractSvgContent(iconRightSvg)}
5725
+ </svg>
5726
+ </g>`;
5727
+ }
5728
+ const textX = pos.x + (iconLeftSvg ? leftOffset : 12);
5729
+ const availPlaceholderWidth = pos.width - (iconLeftSvg ? leftOffset : 12) - chevronWidth - (iconRightSvg ? iconPad + iconSize + iconInnerGap : 0);
5730
+ const visiblePlaceholder = this.truncateTextToWidth(placeholder, Math.max(0, availPlaceholderWidth), 14);
5731
+ svg += `
5732
+ <text x="${textX}" y="${centerY}"
5733
+ font-family="Arial, Helvetica, sans-serif"
5734
+ font-size="14"
5735
+ fill="${this.renderTheme.textMuted}">${this.escapeXml(visiblePlaceholder)}</text>
5736
+ <text x="${pos.x + pos.width - 20}" y="${centerY}"
5737
+ font-family="Arial, Helvetica, sans-serif"
5738
+ font-size="16"
5508
5739
  fill="${this.renderTheme.textMuted}">\u25BC</text>
5509
5740
  </g>`;
5741
+ return svg;
5510
5742
  }
5511
5743
  renderCheckbox(node, pos) {
5512
5744
  const label = String(node.props.label || "Checkbox");
@@ -5939,7 +6171,9 @@ var SVGRenderer = class {
5939
6171
  renderImage(node, pos) {
5940
6172
  const placeholder = String(node.props.placeholder || "landscape").toLowerCase();
5941
6173
  const placeholderIcon = String(node.props.icon || "").trim();
6174
+ const variant = String(node.props.variant || "").trim();
5942
6175
  const placeholderIconSvg = placeholder === "icon" && placeholderIcon ? getIcon(placeholderIcon) : null;
6176
+ const imageBg = this.options.theme === "dark" ? "#2A2A2A" : "#E8E8E8";
5943
6177
  const aspectRatios = {
5944
6178
  landscape: 16 / 9,
5945
6179
  portrait: 2 / 3,
@@ -5957,30 +6191,29 @@ var SVGRenderer = class {
5957
6191
  }
5958
6192
  const offsetX = pos.x + (pos.width - iconWidth) / 2;
5959
6193
  const offsetY = pos.y + (pos.height - iconHeight) / 2;
5960
- let svg = `<g${this.getDataNodeId(node)}>
5961
- <!-- Image Background -->
5962
- <rect x="${pos.x}" y="${pos.y}" width="${pos.width}" height="${pos.height}" fill="#E8E8E8"/>`;
5963
6194
  if (placeholder === "icon" && placeholderIconSvg) {
5964
- const badgeSize = Math.max(24, Math.min(iconWidth, iconHeight) * 0.78);
5965
- const badgeX = pos.x + (pos.width - badgeSize) / 2;
5966
- const badgeY = pos.y + (pos.height - badgeSize) / 2;
5967
- const iconSize = badgeSize * 0.62;
5968
- const iconOffsetX = badgeX + (badgeSize - iconSize) / 2;
5969
- const iconOffsetY = badgeY + (badgeSize - iconSize) / 2;
5970
- svg += `
5971
- <!-- Custom Icon Placeholder -->
5972
- <rect x="${badgeX}" y="${badgeY}"
5973
- width="${badgeSize}" height="${badgeSize}"
5974
- rx="${Math.max(4, badgeSize * 0.2)}"
5975
- fill="rgba(255, 255, 255, 0.6)"
5976
- stroke="#888"
5977
- stroke-width="1"/>
6195
+ const semanticBase = variant ? this.getSemanticVariantColor(variant) : void 0;
6196
+ const hasVariant = variant.length > 0 && (semanticBase !== void 0 || this.colorResolver.hasColor(variant));
6197
+ const variantColor = hasVariant ? this.resolveVariantColor(variant, this.renderTheme.primary) : null;
6198
+ const bgColor = hasVariant ? this.hexToRgba(variantColor, 0.12) : imageBg;
6199
+ const iconColor = hasVariant ? variantColor : this.options.theme === "dark" ? "#888888" : "#666666";
6200
+ const iconSize = Math.max(16, Math.min(pos.width, pos.height) * 0.6);
6201
+ const iconOffsetX = pos.x + (pos.width - iconSize) / 2;
6202
+ const iconOffsetY = pos.y + (pos.height - iconSize) / 2;
6203
+ return `<g${this.getDataNodeId(node)}>
6204
+ <rect x="${pos.x}" y="${pos.y}" width="${pos.width}" height="${pos.height}" fill="${bgColor}" rx="4"/>
5978
6205
  <g transform="translate(${iconOffsetX}, ${iconOffsetY})">
5979
- <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="#555" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
6206
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
5980
6207
  ${this.extractSvgContent(placeholderIconSvg)}
5981
6208
  </svg>
5982
- </g>`;
5983
- } else if (["landscape", "portrait", "square"].includes(placeholder)) {
6209
+ </g>
6210
+ <rect x="${pos.x}" y="${pos.y}" width="${pos.width}" height="${pos.height}" fill="none" stroke="${this.renderTheme.border}" stroke-width="1" rx="4"/>
6211
+ </g>`;
6212
+ }
6213
+ let svg = `<g${this.getDataNodeId(node)}>
6214
+ <!-- Image Background -->
6215
+ <rect x="${pos.x}" y="${pos.y}" width="${pos.width}" height="${pos.height}" fill="${imageBg}"/>`;
6216
+ if (["landscape", "portrait", "square"].includes(placeholder)) {
5984
6217
  const cameraCx = offsetX + iconWidth / 2;
5985
6218
  const cameraCy = offsetY + iconHeight / 2;
5986
6219
  const scale = Math.min(iconWidth, iconHeight) / 24;
@@ -6084,18 +6317,22 @@ var SVGRenderer = class {
6084
6317
  const fontSize = 14;
6085
6318
  const activeIndex = Number(node.props.active || 0);
6086
6319
  const accentColor = this.resolveAccentColor();
6320
+ const variantProp = String(node.props.variant || "").trim();
6321
+ const semanticVariant = variantProp ? this.getSemanticVariantColor(variantProp) : void 0;
6322
+ const hasVariant = variantProp.length > 0 && (semanticVariant !== void 0 || this.colorResolver.hasColor(variantProp));
6323
+ const activeColor = hasVariant ? this.resolveVariantColor(variantProp, accentColor) : accentColor;
6087
6324
  let svg = `<g${this.getDataNodeId(node)}>`;
6088
6325
  items.forEach((item, index) => {
6089
6326
  const itemY = pos.y + index * itemHeight;
6090
6327
  const isActive = index === activeIndex;
6091
- const bgColor = isActive ? this.hexToRgba(accentColor, 0.15) : "transparent";
6092
- const textColor = isActive ? this.hexToRgba(accentColor, 0.9) : this.hexToRgba(this.resolveTextColor(), 0.75);
6328
+ const bgColor = isActive ? this.hexToRgba(activeColor, 0.15) : "transparent";
6329
+ const textColor = isActive ? this.hexToRgba(activeColor, 0.9) : this.hexToRgba(this.resolveTextColor(), 0.75);
6093
6330
  const fontWeight = isActive ? "500" : "400";
6094
6331
  if (isActive) {
6095
6332
  svg += `
6096
- <rect x="${pos.x}" y="${itemY}"
6097
- width="${pos.width}" height="${itemHeight}"
6098
- rx="6"
6333
+ <rect x="${pos.x}" y="${itemY}"
6334
+ width="${pos.width}" height="${itemHeight}"
6335
+ rx="6"
6099
6336
  fill="${bgColor}"/>`;
6100
6337
  }
6101
6338
  let currentX = pos.x + 12;
@@ -6104,9 +6341,10 @@ var SVGRenderer = class {
6104
6341
  if (iconSvg) {
6105
6342
  const iconSize = 16;
6106
6343
  const iconY = itemY + (itemHeight - iconSize) / 2;
6344
+ const iconColor = isActive ? this.hexToRgba(activeColor, 0.9) : this.hexToRgba(this.resolveMutedColor(), 0.9);
6107
6345
  svg += `
6108
6346
  <g transform="translate(${currentX}, ${iconY})">
6109
- <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">
6347
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
6110
6348
  ${this.extractSvgContent(iconSvg)}
6111
6349
  </svg>
6112
6350
  </g>`;
@@ -6114,10 +6352,10 @@ var SVGRenderer = class {
6114
6352
  }
6115
6353
  }
6116
6354
  svg += `
6117
- <text x="${currentX}" y="${itemY + itemHeight / 2 + 5}"
6118
- font-family="Arial, Helvetica, sans-serif"
6119
- font-size="${fontSize}"
6120
- font-weight="${fontWeight}"
6355
+ <text x="${currentX}" y="${itemY + itemHeight / 2 + 5}"
6356
+ font-family="Arial, Helvetica, sans-serif"
6357
+ font-size="${fontSize}"
6358
+ font-weight="${fontWeight}"
6121
6359
  fill="${textColor}">${this.escapeXml(item)}</text>`;
6122
6360
  });
6123
6361
  svg += "\n </g>";
@@ -6157,9 +6395,10 @@ var SVGRenderer = class {
6157
6395
  const semanticBase = this.getSemanticVariantColor(variant);
6158
6396
  const hasExplicitVariantColor = semanticBase !== void 0 || this.colorResolver.hasColor(variant);
6159
6397
  const resolvedBase = this.resolveVariantColor(variant, this.renderTheme.primary);
6160
- const bgColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.85) : "rgba(226, 232, 240, 0.9)";
6398
+ const isDarkMode = this.options.theme === "dark";
6399
+ const bgColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.85) : isDarkMode ? "rgba(48, 48, 55, 0.9)" : "rgba(226, 232, 240, 0.9)";
6161
6400
  const iconColor = hasExplicitVariantColor ? "#FFFFFF" : this.hexToRgba(this.resolveTextColor(), 0.75);
6162
- const borderColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.7) : "rgba(100, 116, 139, 0.4)";
6401
+ const borderColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.7) : isDarkMode ? "rgba(75, 75, 88, 0.8)" : "rgba(100, 116, 139, 0.4)";
6163
6402
  const opacity = disabled ? "0.5" : "1";
6164
6403
  const iconSvg = getIcon(iconName);
6165
6404
  const buttonSize = Math.max(
@@ -6217,14 +6456,37 @@ var SVGRenderer = class {
6217
6456
  return this.colorResolver.resolveColor("muted", fallback);
6218
6457
  }
6219
6458
  getSemanticVariantColor(variant) {
6220
- const semantic = {
6459
+ const isDark = this.options.theme === "dark";
6460
+ const semantic = isDark ? {
6461
+ // Muted mid-range — readable on #111111 without being neon
6462
+ primary: this.renderTheme.primary,
6463
+ // already theme-aware (#60A5FA)
6464
+ secondary: "#7E8EA2",
6465
+ // desaturated slate
6466
+ success: "#22A06B",
6467
+ // muted emerald
6468
+ warning: "#B38010",
6469
+ // deep amber
6470
+ danger: "#CC4444",
6471
+ // muted red
6472
+ error: "#CC4444",
6473
+ info: "#2485AF"
6474
+ // muted sky
6475
+ } : {
6476
+ // Tailwind 500-level — works on white/light backgrounds
6221
6477
  primary: this.renderTheme.primary,
6478
+ // #3B82F6
6222
6479
  secondary: "#64748B",
6480
+ // Slate 500
6223
6481
  success: "#10B981",
6482
+ // Emerald 500
6224
6483
  warning: "#F59E0B",
6484
+ // Amber 500
6225
6485
  danger: "#EF4444",
6486
+ // Red 500
6226
6487
  error: "#EF4444",
6227
6488
  info: "#0EA5E9"
6489
+ // Sky 500
6228
6490
  };
6229
6491
  return semantic[variant];
6230
6492
  }
@@ -6501,10 +6763,11 @@ var SVGRenderer = class {
6501
6763
  buildParentContainerIndex() {
6502
6764
  this.parentContainerByChildId.clear();
6503
6765
  Object.values(this.ir.project.nodes).forEach((node) => {
6504
- if (node.kind !== "container") return;
6505
- node.children.forEach((childRef) => {
6506
- this.parentContainerByChildId.set(childRef.ref, node);
6507
- });
6766
+ if (node.kind === "container") {
6767
+ node.children.forEach((childRef) => {
6768
+ this.parentContainerByChildId.set(childRef.ref, node);
6769
+ });
6770
+ }
6508
6771
  });
6509
6772
  }
6510
6773
  escapeXml(text) {
@@ -6685,6 +6948,19 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
6685
6948
  renderLabel(node, pos) {
6686
6949
  return this.renderTextBlock(node, pos, String(node.props.text || "Label"), 12, 1.2);
6687
6950
  }
6951
+ /**
6952
+ * Render image as a plain skeleton rectangle — no icon, no placeholder label,
6953
+ * just a filled block with the correct dimensions (aspect-ratio is preserved
6954
+ * by the layout engine, so pos already has the right size).
6955
+ */
6956
+ renderImage(node, pos) {
6957
+ return `<g${this.getDataNodeId(node)}>
6958
+ <rect x="${pos.x}" y="${pos.y}"
6959
+ width="${pos.width}" height="${pos.height}"
6960
+ rx="4"
6961
+ fill="${this.renderTheme.border}"/>
6962
+ </g>`;
6963
+ }
6688
6964
  /**
6689
6965
  * Render badge as shape only (no text)
6690
6966
  */
@@ -6926,7 +7202,7 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
6926
7202
  const hasCaption = String(node.props.caption || "").trim().length > 0;
6927
7203
  const showOuterBorder = this.parseBooleanProp(node.props.border, false);
6928
7204
  const showOuterBackground = this.parseBooleanProp(
6929
- node.props.background ?? node.props.backround,
7205
+ node.props.background,
6930
7206
  false
6931
7207
  );
6932
7208
  const showInnerBorder = this.parseBooleanProp(node.props.innerBorder, true);
@@ -7041,7 +7317,7 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
7041
7317
  const variant = String(node.props.variant || "default");
7042
7318
  const accentBlock = variant === "default" ? this.renderTheme.border : this.hexToRgba(this.resolveVariantColor(variant, this.resolveAccentColor()), 0.35);
7043
7319
  const showBorder = this.parseBooleanProp(node.props.border, false);
7044
- const showBackground = this.parseBooleanProp(node.props.background ?? node.props.backround, false);
7320
+ const showBackground = this.parseBooleanProp(node.props.background, false);
7045
7321
  const radiusMap = {
7046
7322
  none: 0,
7047
7323
  sm: 4,
@@ -7359,6 +7635,8 @@ var SketchSVGRenderer = class extends SVGRenderer {
7359
7635
  const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
7360
7636
  const labelOffset = this.parseBooleanProp(node.props.labelSpace, false) ? 18 : 0;
7361
7637
  const fullWidth = this.shouldButtonFillAvailableWidth(node);
7638
+ const iconName = String(node.props.icon || "").trim();
7639
+ const iconAlign = String(node.props.iconAlign || "left").toLowerCase();
7362
7640
  const radius = this.tokens.button.radius;
7363
7641
  const fontSize = this.tokens.button.fontSize;
7364
7642
  const fontWeight = this.tokens.button.fontWeight;
@@ -7368,9 +7646,14 @@ var SketchSVGRenderer = class extends SVGRenderer {
7368
7646
  Math.min(resolveControlHeight(size, density), pos.height - labelOffset)
7369
7647
  );
7370
7648
  const buttonY = pos.y + labelOffset;
7649
+ const iconSvg = iconName ? getIcon(iconName) : null;
7650
+ const iconSize = iconSvg ? Math.round(fontSize * 1.1) : 0;
7651
+ const iconGap = iconSvg ? 8 : 0;
7652
+ const edgePad = 12;
7653
+ const textPad = paddingX + extraPadding;
7371
7654
  const idealTextWidth = text.length * fontSize * 0.6;
7372
- const buttonWidth = fullWidth ? Math.max(1, pos.width) : this.clampControlWidth(Math.max(idealTextWidth + (paddingX + extraPadding) * 2, 60), pos.width);
7373
- const availableTextWidth = Math.max(0, buttonWidth - (paddingX + extraPadding) * 2);
7655
+ const buttonWidth = fullWidth ? Math.max(1, pos.width) : this.clampControlWidth(Math.max(idealTextWidth + (iconSvg ? iconSize + iconGap : 0) + textPad * 2, 60), pos.width);
7656
+ const availableTextWidth = Math.max(0, buttonWidth - textPad * 2 - (iconSvg ? iconSize + iconGap : 0));
7374
7657
  const visibleText = this.truncateTextToWidth(text, availableTextWidth, fontSize);
7375
7658
  const semanticBase = this.getSemanticVariantColor(variant);
7376
7659
  const hasExplicitVariantColor = semanticBase !== void 0 || this.colorResolver.hasColor(variant);
@@ -7378,21 +7661,47 @@ var SketchSVGRenderer = class extends SVGRenderer {
7378
7661
  const borderColor = variantColor;
7379
7662
  const textColor = variantColor;
7380
7663
  const strokeWidth = 0.5;
7381
- return `<g${this.getDataNodeId(node)}>
7664
+ const iconOffsetY = buttonY + (buttonHeight - iconSize) / 2;
7665
+ const iconX = iconAlign === "right" ? pos.x + buttonWidth - edgePad - iconSize : pos.x + edgePad;
7666
+ const textAlign = String(node.props.align || "center").toLowerCase();
7667
+ const sidePad = textPad + 4;
7668
+ let textX;
7669
+ let textAnchor;
7670
+ if (textAlign === "left") {
7671
+ textX = iconSvg && iconAlign === "left" ? pos.x + edgePad + iconSize + iconGap : pos.x + sidePad;
7672
+ textAnchor = "start";
7673
+ } else if (textAlign === "right") {
7674
+ textX = iconSvg && iconAlign === "right" ? pos.x + buttonWidth - edgePad - iconSize - iconGap : pos.x + buttonWidth - sidePad;
7675
+ textAnchor = "end";
7676
+ } else {
7677
+ textX = pos.x + buttonWidth / 2;
7678
+ textAnchor = "middle";
7679
+ }
7680
+ let svg = `<g${this.getDataNodeId(node)}>
7382
7681
  <rect x="${pos.x}" y="${buttonY}"
7383
7682
  width="${buttonWidth}" height="${buttonHeight}"
7384
7683
  rx="${radius}"
7385
7684
  fill="none"
7386
7685
  stroke="${borderColor}"
7387
7686
  stroke-width="${strokeWidth}"
7388
- filter="url(#sketch-rough)"/>
7389
- <text x="${pos.x + buttonWidth / 2}" y="${buttonY + buttonHeight / 2 + fontSize * 0.35}"
7687
+ filter="url(#sketch-rough)"/>`;
7688
+ if (iconSvg) {
7689
+ svg += `
7690
+ <g transform="translate(${iconX}, ${iconOffsetY})">
7691
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${textColor}" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round">
7692
+ ${this.extractSvgContent(iconSvg)}
7693
+ </svg>
7694
+ </g>`;
7695
+ }
7696
+ svg += `
7697
+ <text x="${textX}" y="${buttonY + buttonHeight / 2 + fontSize * 0.35}"
7390
7698
  font-family="${this.fontFamily}"
7391
7699
  font-size="${fontSize}"
7392
7700
  font-weight="${fontWeight}"
7393
7701
  fill="${textColor}"
7394
- text-anchor="middle">${this.escapeXml(visibleText)}</text>
7702
+ text-anchor="${textAnchor}">${this.escapeXml(visibleText)}</text>
7395
7703
  </g>`;
7704
+ return svg;
7396
7705
  }
7397
7706
  /**
7398
7707
  * Render badge with colored border instead of fill
@@ -7517,29 +7826,67 @@ var SketchSVGRenderer = class extends SVGRenderer {
7517
7826
  renderInput(node, pos) {
7518
7827
  const label = String(node.props.label || "");
7519
7828
  const placeholder = String(node.props.placeholder || "");
7829
+ const iconLeftName = String(node.props.iconLeft || "").trim();
7830
+ const iconRightName = String(node.props.iconRight || "").trim();
7520
7831
  const radius = this.tokens.input.radius;
7521
7832
  const fontSize = this.tokens.input.fontSize;
7522
7833
  const paddingX = this.tokens.input.paddingX;
7523
7834
  const labelOffset = this.getControlLabelOffset(label);
7524
7835
  const controlY = pos.y + labelOffset;
7525
7836
  const controlHeight = Math.max(16, pos.height - labelOffset);
7526
- return `<g${this.getDataNodeId(node)}>
7527
- ${label ? `<text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
7837
+ const iconSize = 16;
7838
+ const iconPad = 12;
7839
+ const iconInnerGap = 8;
7840
+ const iconLeftSvg = iconLeftName ? getIcon(iconLeftName) : null;
7841
+ const iconRightSvg = iconRightName ? getIcon(iconRightName) : null;
7842
+ const leftOffset = iconLeftSvg ? iconPad + iconSize + iconInnerGap : 0;
7843
+ const rightOffset = iconRightSvg ? iconPad + iconSize + iconInnerGap : 0;
7844
+ const textX = pos.x + (iconLeftSvg ? leftOffset : paddingX);
7845
+ const iconColor = "#888888";
7846
+ const iconCenterY = controlY + (controlHeight - iconSize) / 2;
7847
+ let svg = `<g${this.getDataNodeId(node)}>`;
7848
+ if (label) {
7849
+ svg += `
7850
+ <text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
7528
7851
  font-family="${this.fontFamily}"
7529
7852
  font-size="12"
7530
- fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>` : ""}
7853
+ fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>`;
7854
+ }
7855
+ svg += `
7531
7856
  <rect x="${pos.x}" y="${controlY}"
7532
7857
  width="${pos.width}" height="${controlHeight}"
7533
7858
  rx="${radius}"
7534
7859
  fill="${this.renderTheme.cardBg}"
7535
7860
  stroke="#2D3748"
7536
7861
  stroke-width="0.5"
7537
- filter="url(#sketch-rough)"/>
7538
- ${placeholder ? `<text x="${pos.x + paddingX}" y="${controlY + controlHeight / 2 + 5}"
7862
+ filter="url(#sketch-rough)"/>`;
7863
+ if (iconLeftSvg) {
7864
+ svg += `
7865
+ <g transform="translate(${pos.x + iconPad}, ${iconCenterY})">
7866
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
7867
+ ${this.extractSvgContent(iconLeftSvg)}
7868
+ </svg>
7869
+ </g>`;
7870
+ }
7871
+ if (iconRightSvg) {
7872
+ svg += `
7873
+ <g transform="translate(${pos.x + pos.width - iconPad - iconSize}, ${iconCenterY})">
7874
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
7875
+ ${this.extractSvgContent(iconRightSvg)}
7876
+ </svg>
7877
+ </g>`;
7878
+ }
7879
+ if (placeholder) {
7880
+ const availWidth = pos.width - (iconLeftSvg ? leftOffset : paddingX) - (iconRightSvg ? rightOffset : paddingX);
7881
+ const visiblePh = this.truncateTextToWidth(placeholder, Math.max(0, availWidth), fontSize);
7882
+ svg += `
7883
+ <text x="${textX}" y="${controlY + controlHeight / 2 + 5}"
7539
7884
  font-family="${this.fontFamily}"
7540
7885
  font-size="${fontSize}"
7541
- fill="${this.renderTheme.textMuted}">${this.escapeXml(placeholder)}</text>` : ""}
7542
- </g>`;
7886
+ fill="${this.renderTheme.textMuted}">${this.escapeXml(visiblePh)}</text>`;
7887
+ }
7888
+ svg += "\n </g>";
7889
+ return svg;
7543
7890
  }
7544
7891
  /**
7545
7892
  * Render textarea with thicker border
@@ -7793,31 +8140,67 @@ var SketchSVGRenderer = class extends SVGRenderer {
7793
8140
  renderSelect(node, pos) {
7794
8141
  const label = String(node.props.label || "");
7795
8142
  const placeholder = String(node.props.placeholder || "Select...");
8143
+ const iconLeftName = String(node.props.iconLeft || "").trim();
8144
+ const iconRightName = String(node.props.iconRight || "").trim();
7796
8145
  const labelOffset = this.getControlLabelOffset(label);
7797
8146
  const controlY = pos.y + labelOffset;
7798
8147
  const controlHeight = Math.max(16, pos.height - labelOffset);
7799
8148
  const centerY = controlY + controlHeight / 2 + 5;
7800
- return `<g${this.getDataNodeId(node)}>
7801
- ${label ? `<text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
8149
+ const iconSize = 16;
8150
+ const iconPad = 12;
8151
+ const iconInnerGap = 8;
8152
+ const iconLeftSvg = iconLeftName ? getIcon(iconLeftName) : null;
8153
+ const iconRightSvg = iconRightName ? getIcon(iconRightName) : null;
8154
+ const leftOffset = iconLeftSvg ? iconPad + iconSize + iconInnerGap : 0;
8155
+ const chevronWidth = 20;
8156
+ const iconColor = "#888888";
8157
+ const iconCenterY = controlY + (controlHeight - iconSize) / 2;
8158
+ let svg = `<g${this.getDataNodeId(node)}>`;
8159
+ if (label) {
8160
+ svg += `
8161
+ <text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
7802
8162
  font-family="${this.fontFamily}"
7803
8163
  font-size="12"
7804
- fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>` : ""}
8164
+ fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>`;
8165
+ }
8166
+ svg += `
7805
8167
  <rect x="${pos.x}" y="${controlY}"
7806
8168
  width="${pos.width}" height="${controlHeight}"
7807
8169
  rx="6"
7808
8170
  fill="${this.renderTheme.cardBg}"
7809
8171
  stroke="#2D3748"
7810
8172
  stroke-width="0.5"
7811
- filter="url(#sketch-rough)"/>
7812
- <text x="${pos.x + 12}" y="${centerY}"
8173
+ filter="url(#sketch-rough)"/>`;
8174
+ if (iconLeftSvg) {
8175
+ svg += `
8176
+ <g transform="translate(${pos.x + iconPad}, ${iconCenterY})">
8177
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
8178
+ ${this.extractSvgContent(iconLeftSvg)}
8179
+ </svg>
8180
+ </g>`;
8181
+ }
8182
+ if (iconRightSvg) {
8183
+ svg += `
8184
+ <g transform="translate(${pos.x + pos.width - chevronWidth - iconPad - iconSize}, ${iconCenterY})">
8185
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
8186
+ ${this.extractSvgContent(iconRightSvg)}
8187
+ </svg>
8188
+ </g>`;
8189
+ }
8190
+ const textX = pos.x + (iconLeftSvg ? leftOffset : 12);
8191
+ const availWidth = pos.width - (iconLeftSvg ? leftOffset : 12) - chevronWidth - (iconRightSvg ? iconPad + iconSize + iconInnerGap : 0);
8192
+ const visiblePh = this.truncateTextToWidth(placeholder, Math.max(0, availWidth), 14);
8193
+ svg += `
8194
+ <text x="${textX}" y="${centerY}"
7813
8195
  font-family="${this.fontFamily}"
7814
8196
  font-size="14"
7815
- fill="${this.renderTheme.textMuted}">${this.escapeXml(placeholder)}</text>
8197
+ fill="${this.renderTheme.textMuted}">${this.escapeXml(visiblePh)}</text>
7816
8198
  <text x="${pos.x + pos.width - 20}" y="${centerY}"
7817
8199
  font-family="${this.fontFamily}"
7818
8200
  font-size="16"
7819
8201
  fill="${this.renderTheme.textMuted}">\u25BC</text>
7820
8202
  </g>`;
8203
+ return svg;
7821
8204
  }
7822
8205
  /**
7823
8206
  * Render checkbox with sketch filter and Comic Sans
@@ -8216,44 +8599,43 @@ var SketchSVGRenderer = class extends SVGRenderer {
8216
8599
  renderImage(node, pos) {
8217
8600
  const placeholder = String(node.props.placeholder || "landscape").toLowerCase();
8218
8601
  const iconType = String(node.props.icon || "").trim();
8602
+ const variant = String(node.props.variant || "").trim();
8219
8603
  const iconSvg = placeholder === "icon" && iconType.length > 0 ? getIcon(iconType) : null;
8604
+ const imageBg = this.options.theme === "dark" ? "#2A2A2A" : "#E8E8E8";
8220
8605
  if (iconSvg) {
8221
- const badgeSize = Math.max(24, Math.min(pos.width, pos.height) * 0.6);
8222
- const badgeX = pos.x + (pos.width - badgeSize) / 2;
8223
- const badgeY = pos.y + (pos.height - badgeSize) / 2;
8224
- const iconSize = badgeSize * 0.62;
8225
- const iconOffsetX = badgeX + (badgeSize - iconSize) / 2;
8226
- const iconOffsetY = badgeY + (badgeSize - iconSize) / 2;
8606
+ const semanticBase = variant ? this.getSemanticVariantColor(variant) : void 0;
8607
+ const hasVariant = variant.length > 0 && (semanticBase !== void 0 || this.colorResolver.hasColor(variant));
8608
+ const variantColor = hasVariant ? this.resolveVariantColor(variant, this.renderTheme.primary) : null;
8609
+ const bgColor = hasVariant ? this.hexToRgba(variantColor, 0.12) : imageBg;
8610
+ const iconColor = hasVariant ? variantColor : "#666666";
8611
+ const iconSize = Math.max(16, Math.min(pos.width, pos.height) * 0.6);
8612
+ const iconOffsetX = pos.x + (pos.width - iconSize) / 2;
8613
+ const iconOffsetY = pos.y + (pos.height - iconSize) / 2;
8227
8614
  return `<g${this.getDataNodeId(node)}>
8228
- <!-- Image Background -->
8229
8615
  <rect x="${pos.x}" y="${pos.y}"
8230
8616
  width="${pos.width}" height="${pos.height}"
8231
- fill="#E8E8E8"
8232
- stroke="#2D3748"
8233
- stroke-width="0.5"
8617
+ fill="${bgColor}"
8234
8618
  rx="4"
8235
8619
  filter="url(#sketch-rough)"/>
8236
-
8237
- <!-- Custom Icon Placeholder -->
8238
- <rect x="${badgeX}" y="${badgeY}"
8239
- width="${badgeSize}" height="${badgeSize}"
8240
- rx="${Math.max(4, badgeSize * 0.2)}"
8241
- fill="none"
8242
- stroke="#2D3748"
8243
- stroke-width="0.5"
8244
- filter="url(#sketch-rough)"/>
8245
8620
  <g transform="translate(${iconOffsetX}, ${iconOffsetY})">
8246
- <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="#2D3748" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
8621
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
8247
8622
  ${this.extractSvgContent(iconSvg)}
8248
8623
  </svg>
8249
8624
  </g>
8625
+ <rect x="${pos.x}" y="${pos.y}"
8626
+ width="${pos.width}" height="${pos.height}"
8627
+ fill="none"
8628
+ stroke="#2D3748"
8629
+ stroke-width="0.5"
8630
+ rx="4"
8631
+ filter="url(#sketch-rough)"/>
8250
8632
  </g>`;
8251
8633
  }
8252
8634
  return `<g${this.getDataNodeId(node)}>
8253
8635
  <!-- Image Background -->
8254
8636
  <rect x="${pos.x}" y="${pos.y}"
8255
8637
  width="${pos.width}" height="${pos.height}"
8256
- fill="#E8E8E8"
8638
+ fill="${imageBg}"
8257
8639
  stroke="#2D3748"
8258
8640
  stroke-width="0.5"
8259
8641
  rx="4"
@@ -8308,17 +8690,23 @@ var SketchSVGRenderer = class extends SVGRenderer {
8308
8690
  */
8309
8691
  renderSidebarMenu(node, pos) {
8310
8692
  const itemsStr = String(node.props.items || "Item 1,Item 2,Item 3");
8693
+ const iconsStr = String(node.props.icons || "");
8311
8694
  const items = itemsStr.split(",").map((s) => s.trim());
8695
+ const icons = iconsStr ? iconsStr.split(",").map((s) => s.trim()) : [];
8312
8696
  const itemHeight = 40;
8313
8697
  const fontSize = 14;
8314
8698
  const activeIndex = Number(node.props.active || 0);
8315
8699
  const accentColor = this.resolveAccentColor();
8700
+ const variantProp = String(node.props.variant || "").trim();
8701
+ const semanticVariant = variantProp ? this.getSemanticVariantColor(variantProp) : void 0;
8702
+ const hasVariant = variantProp.length > 0 && (semanticVariant !== void 0 || this.colorResolver.hasColor(variantProp));
8703
+ const activeColor = hasVariant ? this.resolveVariantColor(variantProp, accentColor) : accentColor;
8316
8704
  let svg = `<g${this.getDataNodeId(node)}>`;
8317
8705
  items.forEach((item, index) => {
8318
8706
  const itemY = pos.y + index * itemHeight;
8319
8707
  const isActive = index === activeIndex;
8320
- const bgColor = isActive ? this.hexToRgba(accentColor, 0.15) : "transparent";
8321
- const textColor = isActive ? accentColor : this.resolveTextColor();
8708
+ const bgColor = isActive ? this.hexToRgba(activeColor, 0.15) : "transparent";
8709
+ const textColor = isActive ? activeColor : this.resolveTextColor();
8322
8710
  const fontWeight = isActive ? "500" : "400";
8323
8711
  if (isActive) {
8324
8712
  svg += `
@@ -8328,8 +8716,24 @@ var SketchSVGRenderer = class extends SVGRenderer {
8328
8716
  fill="${bgColor}"
8329
8717
  filter="url(#sketch-rough)"/>`;
8330
8718
  }
8719
+ let currentX = pos.x + 12;
8720
+ if (icons[index]) {
8721
+ const iconSvg = getIcon(icons[index]);
8722
+ if (iconSvg) {
8723
+ const iconSize = 16;
8724
+ const iconY = itemY + (itemHeight - iconSize) / 2;
8725
+ const iconColor = isActive ? activeColor : this.resolveMutedColor();
8726
+ svg += `
8727
+ <g transform="translate(${currentX}, ${iconY})">
8728
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
8729
+ ${this.extractSvgContent(iconSvg)}
8730
+ </svg>
8731
+ </g>`;
8732
+ currentX += iconSize + 8;
8733
+ }
8734
+ }
8331
8735
  svg += `
8332
- <text x="${pos.x + 12}" y="${itemY + itemHeight / 2 + 5}"
8736
+ <text x="${currentX}" y="${itemY + itemHeight / 2 + 5}"
8333
8737
  font-family="${this.fontFamily}"
8334
8738
  font-size="${fontSize}"
8335
8739
  font-weight="${fontWeight}"