@wire-dsl/engine 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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",
@@ -2689,6 +2690,20 @@ ${messages}`);
2689
2690
  key
2690
2691
  );
2691
2692
  if (resolvedValue !== void 0) {
2693
+ const wasPropReference = typeof value === "string" && value.startsWith("prop_");
2694
+ if (wasPropReference) {
2695
+ const layoutMetadata = import_components2.LAYOUTS[layoutType];
2696
+ const property = layoutMetadata?.properties?.[key];
2697
+ if (property?.type === "enum" && Array.isArray(property.options)) {
2698
+ const normalizedValue = String(resolvedValue);
2699
+ if (!property.options.includes(normalizedValue)) {
2700
+ this.warnings.push({
2701
+ type: "invalid-bound-enum-value",
2702
+ message: `Invalid value "${normalizedValue}" for parameter "${key}" in layout "${layoutType}". Expected one of: ${property.options.join(", ")}.`
2703
+ });
2704
+ }
2705
+ }
2706
+ }
2692
2707
  resolved[key] = resolvedValue;
2693
2708
  }
2694
2709
  }
@@ -2753,6 +2768,20 @@ ${messages}`);
2753
2768
  key
2754
2769
  );
2755
2770
  if (resolvedValue !== void 0) {
2771
+ const wasPropReference = typeof value === "string" && value.startsWith("prop_");
2772
+ if (wasPropReference) {
2773
+ const metadata = import_components2.COMPONENTS[componentType];
2774
+ const property = metadata?.properties?.[key];
2775
+ if (property?.type === "enum" && Array.isArray(property.options)) {
2776
+ const normalizedValue = String(resolvedValue);
2777
+ if (!property.options.includes(normalizedValue)) {
2778
+ this.warnings.push({
2779
+ type: "invalid-bound-enum-value",
2780
+ message: `Invalid value "${normalizedValue}" for property "${key}" in component "${componentType}". Expected one of: ${property.options.join(", ")}.`
2781
+ });
2782
+ }
2783
+ }
2784
+ }
2756
2785
  resolved[key] = resolvedValue;
2757
2786
  }
2758
2787
  }
@@ -4563,14 +4592,14 @@ var THEMES = {
4563
4592
  primaryLight: "#EFF6FF"
4564
4593
  },
4565
4594
  dark: {
4566
- bg: "#0F172A",
4567
- cardBg: "#1E293B",
4568
- border: "#334155",
4569
- text: "#FFFFFF",
4570
- textMuted: "#94A3B8",
4595
+ bg: "#111111",
4596
+ cardBg: "#1C1C1C",
4597
+ border: "#303030",
4598
+ text: "#F0F0F0",
4599
+ textMuted: "#808080",
4571
4600
  primary: "#60A5FA",
4572
4601
  primaryHover: "#3B82F6",
4573
- primaryLight: "#1E3A8A"
4602
+ primaryLight: "#1C2A3A"
4574
4603
  }
4575
4604
  };
4576
4605
  var SVGRenderer = class {
@@ -4811,6 +4840,8 @@ var SVGRenderer = class {
4811
4840
  const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
4812
4841
  const labelOffset = this.parseBooleanProp(node.props.labelSpace, false) ? 18 : 0;
4813
4842
  const fullWidth = this.shouldButtonFillAvailableWidth(node);
4843
+ const iconName = String(node.props.icon || "").trim();
4844
+ const iconAlign = String(node.props.iconAlign || "left").toLowerCase();
4814
4845
  const radius = this.tokens.button.radius;
4815
4846
  const fontSize = this.tokens.button.fontSize;
4816
4847
  const fontWeight = this.tokens.button.fontWeight;
@@ -4818,30 +4849,62 @@ var SVGRenderer = class {
4818
4849
  const controlHeight = resolveActionControlHeight(size, density);
4819
4850
  const buttonY = pos.y + labelOffset;
4820
4851
  const buttonHeight = Math.max(16, Math.min(controlHeight, pos.height - labelOffset));
4852
+ const iconSvg = iconName ? getIcon(iconName) : null;
4853
+ const iconSize = iconSvg ? Math.round(fontSize * 1.1) : 0;
4854
+ const iconGap = iconSvg ? 8 : 0;
4855
+ const edgePad = 12;
4856
+ const textPad = paddingX + extraPadding;
4821
4857
  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);
4858
+ const buttonWidth = fullWidth ? Math.max(1, pos.width) : this.clampControlWidth(Math.max(Math.ceil(idealTextWidth + (iconSvg ? iconSize + iconGap : 0) + textPad * 2), 60), pos.width);
4859
+ const availableTextWidth = Math.max(0, buttonWidth - textPad * 2 - (iconSvg ? iconSize + iconGap : 0));
4824
4860
  const visibleText = this.truncateTextToWidth(text, availableTextWidth, fontSize);
4825
4861
  const semanticBase = this.getSemanticVariantColor(variant);
4826
4862
  const hasExplicitVariantColor = semanticBase !== void 0 || this.colorResolver.hasColor(variant);
4827
4863
  const resolvedBase = this.resolveVariantColor(variant, this.renderTheme.primary);
4828
- const bgColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.85) : "rgba(226, 232, 240, 0.9)";
4864
+ const isDarkMode = this.options.theme === "dark";
4865
+ const bgColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.85) : isDarkMode ? "rgba(48, 48, 55, 0.9)" : "rgba(226, 232, 240, 0.9)";
4829
4866
  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)}>
4867
+ const borderColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.7) : isDarkMode ? "rgba(75, 75, 88, 0.8)" : "rgba(100, 116, 139, 0.4)";
4868
+ const iconOffsetY = buttonY + (buttonHeight - iconSize) / 2;
4869
+ const iconX = iconAlign === "right" ? pos.x + buttonWidth - edgePad - iconSize : pos.x + edgePad;
4870
+ const textAlign = String(node.props.align || "center").toLowerCase();
4871
+ const sidePad = textPad + 4;
4872
+ let textX;
4873
+ let textAnchor;
4874
+ if (textAlign === "left") {
4875
+ textX = iconSvg && iconAlign === "left" ? pos.x + edgePad + iconSize + iconGap : pos.x + sidePad;
4876
+ textAnchor = "start";
4877
+ } else if (textAlign === "right") {
4878
+ textX = iconSvg && iconAlign === "right" ? pos.x + buttonWidth - edgePad - iconSize - iconGap : pos.x + buttonWidth - sidePad;
4879
+ textAnchor = "end";
4880
+ } else {
4881
+ textX = pos.x + buttonWidth / 2;
4882
+ textAnchor = "middle";
4883
+ }
4884
+ let svg = `<g${this.getDataNodeId(node)}>
4832
4885
  <rect x="${pos.x}" y="${buttonY}"
4833
4886
  width="${buttonWidth}" height="${buttonHeight}"
4834
4887
  rx="${radius}"
4835
4888
  fill="${bgColor}"
4836
4889
  stroke="${borderColor}"
4837
- stroke-width="1"/>
4838
- <text x="${pos.x + buttonWidth / 2}" y="${buttonY + buttonHeight / 2 + fontSize * 0.35}"
4890
+ stroke-width="1"/>`;
4891
+ if (iconSvg) {
4892
+ svg += `
4893
+ <g transform="translate(${iconX}, ${iconOffsetY})">
4894
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${textColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
4895
+ ${this.extractSvgContent(iconSvg)}
4896
+ </svg>
4897
+ </g>`;
4898
+ }
4899
+ svg += `
4900
+ <text x="${textX}" y="${buttonY + buttonHeight / 2 + fontSize * 0.35}"
4839
4901
  font-family="Arial, Helvetica, sans-serif"
4840
4902
  font-size="${fontSize}"
4841
4903
  font-weight="${fontWeight}"
4842
4904
  fill="${textColor}"
4843
- text-anchor="middle">${this.escapeXml(visibleText)}</text>
4905
+ text-anchor="${textAnchor}">${this.escapeXml(visibleText)}</text>
4844
4906
  </g>`;
4907
+ return svg;
4845
4908
  }
4846
4909
  renderLink(node, pos) {
4847
4910
  const text = String(node.props.text || "Link");
@@ -4882,28 +4945,66 @@ var SVGRenderer = class {
4882
4945
  renderInput(node, pos) {
4883
4946
  const label = String(node.props.label || "");
4884
4947
  const placeholder = String(node.props.placeholder || "");
4948
+ const iconLeftName = String(node.props.iconLeft || "").trim();
4949
+ const iconRightName = String(node.props.iconRight || "").trim();
4885
4950
  const radius = this.tokens.input.radius;
4886
4951
  const fontSize = this.tokens.input.fontSize;
4887
4952
  const paddingX = this.tokens.input.paddingX;
4888
4953
  const labelOffset = this.getControlLabelOffset(label);
4889
4954
  const controlY = pos.y + labelOffset;
4890
4955
  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)}"
4956
+ const iconSize = 16;
4957
+ const iconPad = 12;
4958
+ const iconInnerGap = 8;
4959
+ const iconLeftSvg = iconLeftName ? getIcon(iconLeftName) : null;
4960
+ const iconRightSvg = iconRightName ? getIcon(iconRightName) : null;
4961
+ const leftOffset = iconLeftSvg ? iconPad + iconSize + iconInnerGap : 0;
4962
+ const rightOffset = iconRightSvg ? iconPad + iconSize + iconInnerGap : 0;
4963
+ const textX = pos.x + (iconLeftSvg ? leftOffset : paddingX);
4964
+ const iconColor = this.hexToRgba(this.resolveMutedColor(), 0.8);
4965
+ const iconCenterY = controlY + (controlHeight - iconSize) / 2;
4966
+ let svg = `<g${this.getDataNodeId(node)}>`;
4967
+ if (label) {
4968
+ svg += `
4969
+ <text x="${pos.x + paddingX}" y="${this.getControlLabelBaselineY(pos.y)}"
4893
4970
  font-family="Arial, Helvetica, sans-serif"
4894
4971
  font-size="12"
4895
- fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>` : ""}
4972
+ fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>`;
4973
+ }
4974
+ svg += `
4896
4975
  <rect x="${pos.x}" y="${controlY}"
4897
4976
  width="${pos.width}" height="${controlHeight}"
4898
4977
  rx="${radius}"
4899
4978
  fill="${this.renderTheme.cardBg}"
4900
4979
  stroke="${this.renderTheme.border}"
4901
- stroke-width="1"/>
4902
- <text x="${pos.x + paddingX}" y="${controlY + controlHeight / 2 + 5}"
4980
+ stroke-width="1"/>`;
4981
+ if (iconLeftSvg) {
4982
+ svg += `
4983
+ <g transform="translate(${pos.x + iconPad}, ${iconCenterY})">
4984
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
4985
+ ${this.extractSvgContent(iconLeftSvg)}
4986
+ </svg>
4987
+ </g>`;
4988
+ }
4989
+ if (iconRightSvg) {
4990
+ svg += `
4991
+ <g transform="translate(${pos.x + pos.width - iconPad - iconSize}, ${iconCenterY})">
4992
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
4993
+ ${this.extractSvgContent(iconRightSvg)}
4994
+ </svg>
4995
+ </g>`;
4996
+ }
4997
+ if (placeholder) {
4998
+ const availPlaceholderWidth = pos.width - (iconLeftSvg ? leftOffset : paddingX) - (iconRightSvg ? rightOffset : paddingX);
4999
+ const visiblePlaceholder = this.truncateTextToWidth(placeholder, Math.max(0, availPlaceholderWidth), fontSize);
5000
+ svg += `
5001
+ <text x="${textX}" y="${controlY + controlHeight / 2 + 5}"
4903
5002
  font-family="Arial, Helvetica, sans-serif"
4904
5003
  font-size="${fontSize}"
4905
- fill="${this.renderTheme.textMuted}">${this.escapeXml(placeholder)}</text>
4906
- </g>`;
5004
+ fill="${this.renderTheme.textMuted}">${this.escapeXml(visiblePlaceholder)}</text>`;
5005
+ }
5006
+ svg += "\n </g>";
5007
+ return svg;
4907
5008
  }
4908
5009
  renderTopbar(node, pos) {
4909
5010
  const title = String(node.props.title || "App");
@@ -5483,30 +5584,66 @@ var SVGRenderer = class {
5483
5584
  renderSelect(node, pos) {
5484
5585
  const label = String(node.props.label || "");
5485
5586
  const placeholder = String(node.props.placeholder || "Select...");
5587
+ const iconLeftName = String(node.props.iconLeft || "").trim();
5588
+ const iconRightName = String(node.props.iconRight || "").trim();
5486
5589
  const labelOffset = this.getControlLabelOffset(label);
5487
5590
  const controlY = pos.y + labelOffset;
5488
5591
  const controlHeight = Math.max(16, pos.height - labelOffset);
5489
5592
  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"
5593
+ const iconSize = 16;
5594
+ const iconPad = 12;
5595
+ const iconInnerGap = 8;
5596
+ const iconLeftSvg = iconLeftName ? getIcon(iconLeftName) : null;
5597
+ const iconRightSvg = iconRightName ? getIcon(iconRightName) : null;
5598
+ const leftOffset = iconLeftSvg ? iconPad + iconSize + iconInnerGap : 0;
5599
+ const chevronWidth = 20;
5600
+ const iconColor = this.hexToRgba(this.resolveMutedColor(), 0.8);
5601
+ const iconCenterY = controlY + (controlHeight - iconSize) / 2;
5602
+ let svg = `<g${this.getDataNodeId(node)}>`;
5603
+ if (label) {
5604
+ svg += `
5605
+ <text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
5606
+ font-family="Arial, Helvetica, sans-serif"
5607
+ font-size="12"
5608
+ fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>`;
5609
+ }
5610
+ svg += `
5611
+ <rect x="${pos.x}" y="${controlY}"
5612
+ width="${pos.width}" height="${controlHeight}"
5613
+ rx="6"
5614
+ fill="${this.renderTheme.cardBg}"
5615
+ stroke="${this.renderTheme.border}"
5616
+ stroke-width="1"/>`;
5617
+ if (iconLeftSvg) {
5618
+ svg += `
5619
+ <g transform="translate(${pos.x + iconPad}, ${iconCenterY})">
5620
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
5621
+ ${this.extractSvgContent(iconLeftSvg)}
5622
+ </svg>
5623
+ </g>`;
5624
+ }
5625
+ if (iconRightSvg) {
5626
+ svg += `
5627
+ <g transform="translate(${pos.x + pos.width - chevronWidth - iconPad - iconSize}, ${iconCenterY})">
5628
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
5629
+ ${this.extractSvgContent(iconRightSvg)}
5630
+ </svg>
5631
+ </g>`;
5632
+ }
5633
+ const textX = pos.x + (iconLeftSvg ? leftOffset : 12);
5634
+ const availPlaceholderWidth = pos.width - (iconLeftSvg ? leftOffset : 12) - chevronWidth - (iconRightSvg ? iconPad + iconSize + iconInnerGap : 0);
5635
+ const visiblePlaceholder = this.truncateTextToWidth(placeholder, Math.max(0, availPlaceholderWidth), 14);
5636
+ svg += `
5637
+ <text x="${textX}" y="${centerY}"
5638
+ font-family="Arial, Helvetica, sans-serif"
5639
+ font-size="14"
5640
+ fill="${this.renderTheme.textMuted}">${this.escapeXml(visiblePlaceholder)}</text>
5641
+ <text x="${pos.x + pos.width - 20}" y="${centerY}"
5642
+ font-family="Arial, Helvetica, sans-serif"
5643
+ font-size="16"
5508
5644
  fill="${this.renderTheme.textMuted}">\u25BC</text>
5509
5645
  </g>`;
5646
+ return svg;
5510
5647
  }
5511
5648
  renderCheckbox(node, pos) {
5512
5649
  const label = String(node.props.label || "Checkbox");
@@ -5939,7 +6076,9 @@ var SVGRenderer = class {
5939
6076
  renderImage(node, pos) {
5940
6077
  const placeholder = String(node.props.placeholder || "landscape").toLowerCase();
5941
6078
  const placeholderIcon = String(node.props.icon || "").trim();
6079
+ const variant = String(node.props.variant || "").trim();
5942
6080
  const placeholderIconSvg = placeholder === "icon" && placeholderIcon ? getIcon(placeholderIcon) : null;
6081
+ const imageBg = this.options.theme === "dark" ? "#2A2A2A" : "#E8E8E8";
5943
6082
  const aspectRatios = {
5944
6083
  landscape: 16 / 9,
5945
6084
  portrait: 2 / 3,
@@ -5957,30 +6096,29 @@ var SVGRenderer = class {
5957
6096
  }
5958
6097
  const offsetX = pos.x + (pos.width - iconWidth) / 2;
5959
6098
  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
6099
  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"/>
6100
+ const semanticBase = variant ? this.getSemanticVariantColor(variant) : void 0;
6101
+ const hasVariant = variant.length > 0 && (semanticBase !== void 0 || this.colorResolver.hasColor(variant));
6102
+ const variantColor = hasVariant ? this.resolveVariantColor(variant, this.renderTheme.primary) : null;
6103
+ const bgColor = hasVariant ? this.hexToRgba(variantColor, 0.12) : imageBg;
6104
+ const iconColor = hasVariant ? variantColor : this.options.theme === "dark" ? "#888888" : "#666666";
6105
+ const iconSize = Math.max(16, Math.min(pos.width, pos.height) * 0.6);
6106
+ const iconOffsetX = pos.x + (pos.width - iconSize) / 2;
6107
+ const iconOffsetY = pos.y + (pos.height - iconSize) / 2;
6108
+ return `<g${this.getDataNodeId(node)}>
6109
+ <rect x="${pos.x}" y="${pos.y}" width="${pos.width}" height="${pos.height}" fill="${bgColor}" rx="4"/>
5978
6110
  <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">
6111
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
5980
6112
  ${this.extractSvgContent(placeholderIconSvg)}
5981
6113
  </svg>
5982
- </g>`;
5983
- } else if (["landscape", "portrait", "square"].includes(placeholder)) {
6114
+ </g>
6115
+ <rect x="${pos.x}" y="${pos.y}" width="${pos.width}" height="${pos.height}" fill="none" stroke="${this.renderTheme.border}" stroke-width="1" rx="4"/>
6116
+ </g>`;
6117
+ }
6118
+ let svg = `<g${this.getDataNodeId(node)}>
6119
+ <!-- Image Background -->
6120
+ <rect x="${pos.x}" y="${pos.y}" width="${pos.width}" height="${pos.height}" fill="${imageBg}"/>`;
6121
+ if (["landscape", "portrait", "square"].includes(placeholder)) {
5984
6122
  const cameraCx = offsetX + iconWidth / 2;
5985
6123
  const cameraCy = offsetY + iconHeight / 2;
5986
6124
  const scale = Math.min(iconWidth, iconHeight) / 24;
@@ -6084,18 +6222,22 @@ var SVGRenderer = class {
6084
6222
  const fontSize = 14;
6085
6223
  const activeIndex = Number(node.props.active || 0);
6086
6224
  const accentColor = this.resolveAccentColor();
6225
+ const variantProp = String(node.props.variant || "").trim();
6226
+ const semanticVariant = variantProp ? this.getSemanticVariantColor(variantProp) : void 0;
6227
+ const hasVariant = variantProp.length > 0 && (semanticVariant !== void 0 || this.colorResolver.hasColor(variantProp));
6228
+ const activeColor = hasVariant ? this.resolveVariantColor(variantProp, accentColor) : accentColor;
6087
6229
  let svg = `<g${this.getDataNodeId(node)}>`;
6088
6230
  items.forEach((item, index) => {
6089
6231
  const itemY = pos.y + index * itemHeight;
6090
6232
  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);
6233
+ const bgColor = isActive ? this.hexToRgba(activeColor, 0.15) : "transparent";
6234
+ const textColor = isActive ? this.hexToRgba(activeColor, 0.9) : this.hexToRgba(this.resolveTextColor(), 0.75);
6093
6235
  const fontWeight = isActive ? "500" : "400";
6094
6236
  if (isActive) {
6095
6237
  svg += `
6096
- <rect x="${pos.x}" y="${itemY}"
6097
- width="${pos.width}" height="${itemHeight}"
6098
- rx="6"
6238
+ <rect x="${pos.x}" y="${itemY}"
6239
+ width="${pos.width}" height="${itemHeight}"
6240
+ rx="6"
6099
6241
  fill="${bgColor}"/>`;
6100
6242
  }
6101
6243
  let currentX = pos.x + 12;
@@ -6104,9 +6246,10 @@ var SVGRenderer = class {
6104
6246
  if (iconSvg) {
6105
6247
  const iconSize = 16;
6106
6248
  const iconY = itemY + (itemHeight - iconSize) / 2;
6249
+ const iconColor = isActive ? this.hexToRgba(activeColor, 0.9) : this.hexToRgba(this.resolveMutedColor(), 0.9);
6107
6250
  svg += `
6108
6251
  <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">
6252
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
6110
6253
  ${this.extractSvgContent(iconSvg)}
6111
6254
  </svg>
6112
6255
  </g>`;
@@ -6114,10 +6257,10 @@ var SVGRenderer = class {
6114
6257
  }
6115
6258
  }
6116
6259
  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}"
6260
+ <text x="${currentX}" y="${itemY + itemHeight / 2 + 5}"
6261
+ font-family="Arial, Helvetica, sans-serif"
6262
+ font-size="${fontSize}"
6263
+ font-weight="${fontWeight}"
6121
6264
  fill="${textColor}">${this.escapeXml(item)}</text>`;
6122
6265
  });
6123
6266
  svg += "\n </g>";
@@ -6157,9 +6300,10 @@ var SVGRenderer = class {
6157
6300
  const semanticBase = this.getSemanticVariantColor(variant);
6158
6301
  const hasExplicitVariantColor = semanticBase !== void 0 || this.colorResolver.hasColor(variant);
6159
6302
  const resolvedBase = this.resolveVariantColor(variant, this.renderTheme.primary);
6160
- const bgColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.85) : "rgba(226, 232, 240, 0.9)";
6303
+ const isDarkMode = this.options.theme === "dark";
6304
+ const bgColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.85) : isDarkMode ? "rgba(48, 48, 55, 0.9)" : "rgba(226, 232, 240, 0.9)";
6161
6305
  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)";
6306
+ const borderColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.7) : isDarkMode ? "rgba(75, 75, 88, 0.8)" : "rgba(100, 116, 139, 0.4)";
6163
6307
  const opacity = disabled ? "0.5" : "1";
6164
6308
  const iconSvg = getIcon(iconName);
6165
6309
  const buttonSize = Math.max(
@@ -6217,14 +6361,37 @@ var SVGRenderer = class {
6217
6361
  return this.colorResolver.resolveColor("muted", fallback);
6218
6362
  }
6219
6363
  getSemanticVariantColor(variant) {
6220
- const semantic = {
6364
+ const isDark = this.options.theme === "dark";
6365
+ const semantic = isDark ? {
6366
+ // Muted mid-range — readable on #111111 without being neon
6367
+ primary: this.renderTheme.primary,
6368
+ // already theme-aware (#60A5FA)
6369
+ secondary: "#7E8EA2",
6370
+ // desaturated slate
6371
+ success: "#22A06B",
6372
+ // muted emerald
6373
+ warning: "#B38010",
6374
+ // deep amber
6375
+ danger: "#CC4444",
6376
+ // muted red
6377
+ error: "#CC4444",
6378
+ info: "#2485AF"
6379
+ // muted sky
6380
+ } : {
6381
+ // Tailwind 500-level — works on white/light backgrounds
6221
6382
  primary: this.renderTheme.primary,
6383
+ // #3B82F6
6222
6384
  secondary: "#64748B",
6385
+ // Slate 500
6223
6386
  success: "#10B981",
6387
+ // Emerald 500
6224
6388
  warning: "#F59E0B",
6389
+ // Amber 500
6225
6390
  danger: "#EF4444",
6391
+ // Red 500
6226
6392
  error: "#EF4444",
6227
6393
  info: "#0EA5E9"
6394
+ // Sky 500
6228
6395
  };
6229
6396
  return semantic[variant];
6230
6397
  }
@@ -6685,6 +6852,19 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
6685
6852
  renderLabel(node, pos) {
6686
6853
  return this.renderTextBlock(node, pos, String(node.props.text || "Label"), 12, 1.2);
6687
6854
  }
6855
+ /**
6856
+ * Render image as a plain skeleton rectangle — no icon, no placeholder label,
6857
+ * just a filled block with the correct dimensions (aspect-ratio is preserved
6858
+ * by the layout engine, so pos already has the right size).
6859
+ */
6860
+ renderImage(node, pos) {
6861
+ return `<g${this.getDataNodeId(node)}>
6862
+ <rect x="${pos.x}" y="${pos.y}"
6863
+ width="${pos.width}" height="${pos.height}"
6864
+ rx="4"
6865
+ fill="${this.renderTheme.border}"/>
6866
+ </g>`;
6867
+ }
6688
6868
  /**
6689
6869
  * Render badge as shape only (no text)
6690
6870
  */
@@ -7359,6 +7539,8 @@ var SketchSVGRenderer = class extends SVGRenderer {
7359
7539
  const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
7360
7540
  const labelOffset = this.parseBooleanProp(node.props.labelSpace, false) ? 18 : 0;
7361
7541
  const fullWidth = this.shouldButtonFillAvailableWidth(node);
7542
+ const iconName = String(node.props.icon || "").trim();
7543
+ const iconAlign = String(node.props.iconAlign || "left").toLowerCase();
7362
7544
  const radius = this.tokens.button.radius;
7363
7545
  const fontSize = this.tokens.button.fontSize;
7364
7546
  const fontWeight = this.tokens.button.fontWeight;
@@ -7368,9 +7550,14 @@ var SketchSVGRenderer = class extends SVGRenderer {
7368
7550
  Math.min(resolveControlHeight(size, density), pos.height - labelOffset)
7369
7551
  );
7370
7552
  const buttonY = pos.y + labelOffset;
7553
+ const iconSvg = iconName ? getIcon(iconName) : null;
7554
+ const iconSize = iconSvg ? Math.round(fontSize * 1.1) : 0;
7555
+ const iconGap = iconSvg ? 8 : 0;
7556
+ const edgePad = 12;
7557
+ const textPad = paddingX + extraPadding;
7371
7558
  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);
7559
+ const buttonWidth = fullWidth ? Math.max(1, pos.width) : this.clampControlWidth(Math.max(idealTextWidth + (iconSvg ? iconSize + iconGap : 0) + textPad * 2, 60), pos.width);
7560
+ const availableTextWidth = Math.max(0, buttonWidth - textPad * 2 - (iconSvg ? iconSize + iconGap : 0));
7374
7561
  const visibleText = this.truncateTextToWidth(text, availableTextWidth, fontSize);
7375
7562
  const semanticBase = this.getSemanticVariantColor(variant);
7376
7563
  const hasExplicitVariantColor = semanticBase !== void 0 || this.colorResolver.hasColor(variant);
@@ -7378,21 +7565,47 @@ var SketchSVGRenderer = class extends SVGRenderer {
7378
7565
  const borderColor = variantColor;
7379
7566
  const textColor = variantColor;
7380
7567
  const strokeWidth = 0.5;
7381
- return `<g${this.getDataNodeId(node)}>
7568
+ const iconOffsetY = buttonY + (buttonHeight - iconSize) / 2;
7569
+ const iconX = iconAlign === "right" ? pos.x + buttonWidth - edgePad - iconSize : pos.x + edgePad;
7570
+ const textAlign = String(node.props.align || "center").toLowerCase();
7571
+ const sidePad = textPad + 4;
7572
+ let textX;
7573
+ let textAnchor;
7574
+ if (textAlign === "left") {
7575
+ textX = iconSvg && iconAlign === "left" ? pos.x + edgePad + iconSize + iconGap : pos.x + sidePad;
7576
+ textAnchor = "start";
7577
+ } else if (textAlign === "right") {
7578
+ textX = iconSvg && iconAlign === "right" ? pos.x + buttonWidth - edgePad - iconSize - iconGap : pos.x + buttonWidth - sidePad;
7579
+ textAnchor = "end";
7580
+ } else {
7581
+ textX = pos.x + buttonWidth / 2;
7582
+ textAnchor = "middle";
7583
+ }
7584
+ let svg = `<g${this.getDataNodeId(node)}>
7382
7585
  <rect x="${pos.x}" y="${buttonY}"
7383
7586
  width="${buttonWidth}" height="${buttonHeight}"
7384
7587
  rx="${radius}"
7385
7588
  fill="none"
7386
7589
  stroke="${borderColor}"
7387
7590
  stroke-width="${strokeWidth}"
7388
- filter="url(#sketch-rough)"/>
7389
- <text x="${pos.x + buttonWidth / 2}" y="${buttonY + buttonHeight / 2 + fontSize * 0.35}"
7591
+ filter="url(#sketch-rough)"/>`;
7592
+ if (iconSvg) {
7593
+ svg += `
7594
+ <g transform="translate(${iconX}, ${iconOffsetY})">
7595
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${textColor}" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round">
7596
+ ${this.extractSvgContent(iconSvg)}
7597
+ </svg>
7598
+ </g>`;
7599
+ }
7600
+ svg += `
7601
+ <text x="${textX}" y="${buttonY + buttonHeight / 2 + fontSize * 0.35}"
7390
7602
  font-family="${this.fontFamily}"
7391
7603
  font-size="${fontSize}"
7392
7604
  font-weight="${fontWeight}"
7393
7605
  fill="${textColor}"
7394
- text-anchor="middle">${this.escapeXml(visibleText)}</text>
7606
+ text-anchor="${textAnchor}">${this.escapeXml(visibleText)}</text>
7395
7607
  </g>`;
7608
+ return svg;
7396
7609
  }
7397
7610
  /**
7398
7611
  * Render badge with colored border instead of fill
@@ -7517,29 +7730,67 @@ var SketchSVGRenderer = class extends SVGRenderer {
7517
7730
  renderInput(node, pos) {
7518
7731
  const label = String(node.props.label || "");
7519
7732
  const placeholder = String(node.props.placeholder || "");
7733
+ const iconLeftName = String(node.props.iconLeft || "").trim();
7734
+ const iconRightName = String(node.props.iconRight || "").trim();
7520
7735
  const radius = this.tokens.input.radius;
7521
7736
  const fontSize = this.tokens.input.fontSize;
7522
7737
  const paddingX = this.tokens.input.paddingX;
7523
7738
  const labelOffset = this.getControlLabelOffset(label);
7524
7739
  const controlY = pos.y + labelOffset;
7525
7740
  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)}"
7741
+ const iconSize = 16;
7742
+ const iconPad = 12;
7743
+ const iconInnerGap = 8;
7744
+ const iconLeftSvg = iconLeftName ? getIcon(iconLeftName) : null;
7745
+ const iconRightSvg = iconRightName ? getIcon(iconRightName) : null;
7746
+ const leftOffset = iconLeftSvg ? iconPad + iconSize + iconInnerGap : 0;
7747
+ const rightOffset = iconRightSvg ? iconPad + iconSize + iconInnerGap : 0;
7748
+ const textX = pos.x + (iconLeftSvg ? leftOffset : paddingX);
7749
+ const iconColor = "#888888";
7750
+ const iconCenterY = controlY + (controlHeight - iconSize) / 2;
7751
+ let svg = `<g${this.getDataNodeId(node)}>`;
7752
+ if (label) {
7753
+ svg += `
7754
+ <text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
7528
7755
  font-family="${this.fontFamily}"
7529
7756
  font-size="12"
7530
- fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>` : ""}
7757
+ fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>`;
7758
+ }
7759
+ svg += `
7531
7760
  <rect x="${pos.x}" y="${controlY}"
7532
7761
  width="${pos.width}" height="${controlHeight}"
7533
7762
  rx="${radius}"
7534
7763
  fill="${this.renderTheme.cardBg}"
7535
7764
  stroke="#2D3748"
7536
7765
  stroke-width="0.5"
7537
- filter="url(#sketch-rough)"/>
7538
- ${placeholder ? `<text x="${pos.x + paddingX}" y="${controlY + controlHeight / 2 + 5}"
7766
+ filter="url(#sketch-rough)"/>`;
7767
+ if (iconLeftSvg) {
7768
+ svg += `
7769
+ <g transform="translate(${pos.x + iconPad}, ${iconCenterY})">
7770
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
7771
+ ${this.extractSvgContent(iconLeftSvg)}
7772
+ </svg>
7773
+ </g>`;
7774
+ }
7775
+ if (iconRightSvg) {
7776
+ svg += `
7777
+ <g transform="translate(${pos.x + pos.width - iconPad - iconSize}, ${iconCenterY})">
7778
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
7779
+ ${this.extractSvgContent(iconRightSvg)}
7780
+ </svg>
7781
+ </g>`;
7782
+ }
7783
+ if (placeholder) {
7784
+ const availWidth = pos.width - (iconLeftSvg ? leftOffset : paddingX) - (iconRightSvg ? rightOffset : paddingX);
7785
+ const visiblePh = this.truncateTextToWidth(placeholder, Math.max(0, availWidth), fontSize);
7786
+ svg += `
7787
+ <text x="${textX}" y="${controlY + controlHeight / 2 + 5}"
7539
7788
  font-family="${this.fontFamily}"
7540
7789
  font-size="${fontSize}"
7541
- fill="${this.renderTheme.textMuted}">${this.escapeXml(placeholder)}</text>` : ""}
7542
- </g>`;
7790
+ fill="${this.renderTheme.textMuted}">${this.escapeXml(visiblePh)}</text>`;
7791
+ }
7792
+ svg += "\n </g>";
7793
+ return svg;
7543
7794
  }
7544
7795
  /**
7545
7796
  * Render textarea with thicker border
@@ -7793,31 +8044,67 @@ var SketchSVGRenderer = class extends SVGRenderer {
7793
8044
  renderSelect(node, pos) {
7794
8045
  const label = String(node.props.label || "");
7795
8046
  const placeholder = String(node.props.placeholder || "Select...");
8047
+ const iconLeftName = String(node.props.iconLeft || "").trim();
8048
+ const iconRightName = String(node.props.iconRight || "").trim();
7796
8049
  const labelOffset = this.getControlLabelOffset(label);
7797
8050
  const controlY = pos.y + labelOffset;
7798
8051
  const controlHeight = Math.max(16, pos.height - labelOffset);
7799
8052
  const centerY = controlY + controlHeight / 2 + 5;
7800
- return `<g${this.getDataNodeId(node)}>
7801
- ${label ? `<text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
8053
+ const iconSize = 16;
8054
+ const iconPad = 12;
8055
+ const iconInnerGap = 8;
8056
+ const iconLeftSvg = iconLeftName ? getIcon(iconLeftName) : null;
8057
+ const iconRightSvg = iconRightName ? getIcon(iconRightName) : null;
8058
+ const leftOffset = iconLeftSvg ? iconPad + iconSize + iconInnerGap : 0;
8059
+ const chevronWidth = 20;
8060
+ const iconColor = "#888888";
8061
+ const iconCenterY = controlY + (controlHeight - iconSize) / 2;
8062
+ let svg = `<g${this.getDataNodeId(node)}>`;
8063
+ if (label) {
8064
+ svg += `
8065
+ <text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
7802
8066
  font-family="${this.fontFamily}"
7803
8067
  font-size="12"
7804
- fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>` : ""}
8068
+ fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>`;
8069
+ }
8070
+ svg += `
7805
8071
  <rect x="${pos.x}" y="${controlY}"
7806
8072
  width="${pos.width}" height="${controlHeight}"
7807
8073
  rx="6"
7808
8074
  fill="${this.renderTheme.cardBg}"
7809
8075
  stroke="#2D3748"
7810
8076
  stroke-width="0.5"
7811
- filter="url(#sketch-rough)"/>
7812
- <text x="${pos.x + 12}" y="${centerY}"
8077
+ filter="url(#sketch-rough)"/>`;
8078
+ if (iconLeftSvg) {
8079
+ svg += `
8080
+ <g transform="translate(${pos.x + iconPad}, ${iconCenterY})">
8081
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
8082
+ ${this.extractSvgContent(iconLeftSvg)}
8083
+ </svg>
8084
+ </g>`;
8085
+ }
8086
+ if (iconRightSvg) {
8087
+ svg += `
8088
+ <g transform="translate(${pos.x + pos.width - chevronWidth - iconPad - iconSize}, ${iconCenterY})">
8089
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
8090
+ ${this.extractSvgContent(iconRightSvg)}
8091
+ </svg>
8092
+ </g>`;
8093
+ }
8094
+ const textX = pos.x + (iconLeftSvg ? leftOffset : 12);
8095
+ const availWidth = pos.width - (iconLeftSvg ? leftOffset : 12) - chevronWidth - (iconRightSvg ? iconPad + iconSize + iconInnerGap : 0);
8096
+ const visiblePh = this.truncateTextToWidth(placeholder, Math.max(0, availWidth), 14);
8097
+ svg += `
8098
+ <text x="${textX}" y="${centerY}"
7813
8099
  font-family="${this.fontFamily}"
7814
8100
  font-size="14"
7815
- fill="${this.renderTheme.textMuted}">${this.escapeXml(placeholder)}</text>
8101
+ fill="${this.renderTheme.textMuted}">${this.escapeXml(visiblePh)}</text>
7816
8102
  <text x="${pos.x + pos.width - 20}" y="${centerY}"
7817
8103
  font-family="${this.fontFamily}"
7818
8104
  font-size="16"
7819
8105
  fill="${this.renderTheme.textMuted}">\u25BC</text>
7820
8106
  </g>`;
8107
+ return svg;
7821
8108
  }
7822
8109
  /**
7823
8110
  * Render checkbox with sketch filter and Comic Sans
@@ -8216,44 +8503,43 @@ var SketchSVGRenderer = class extends SVGRenderer {
8216
8503
  renderImage(node, pos) {
8217
8504
  const placeholder = String(node.props.placeholder || "landscape").toLowerCase();
8218
8505
  const iconType = String(node.props.icon || "").trim();
8506
+ const variant = String(node.props.variant || "").trim();
8219
8507
  const iconSvg = placeholder === "icon" && iconType.length > 0 ? getIcon(iconType) : null;
8508
+ const imageBg = this.options.theme === "dark" ? "#2A2A2A" : "#E8E8E8";
8220
8509
  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;
8510
+ const semanticBase = variant ? this.getSemanticVariantColor(variant) : void 0;
8511
+ const hasVariant = variant.length > 0 && (semanticBase !== void 0 || this.colorResolver.hasColor(variant));
8512
+ const variantColor = hasVariant ? this.resolveVariantColor(variant, this.renderTheme.primary) : null;
8513
+ const bgColor = hasVariant ? this.hexToRgba(variantColor, 0.12) : imageBg;
8514
+ const iconColor = hasVariant ? variantColor : "#666666";
8515
+ const iconSize = Math.max(16, Math.min(pos.width, pos.height) * 0.6);
8516
+ const iconOffsetX = pos.x + (pos.width - iconSize) / 2;
8517
+ const iconOffsetY = pos.y + (pos.height - iconSize) / 2;
8227
8518
  return `<g${this.getDataNodeId(node)}>
8228
- <!-- Image Background -->
8229
8519
  <rect x="${pos.x}" y="${pos.y}"
8230
8520
  width="${pos.width}" height="${pos.height}"
8231
- fill="#E8E8E8"
8232
- stroke="#2D3748"
8233
- stroke-width="0.5"
8521
+ fill="${bgColor}"
8234
8522
  rx="4"
8235
8523
  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
8524
  <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">
8525
+ <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
8526
  ${this.extractSvgContent(iconSvg)}
8248
8527
  </svg>
8249
8528
  </g>
8529
+ <rect x="${pos.x}" y="${pos.y}"
8530
+ width="${pos.width}" height="${pos.height}"
8531
+ fill="none"
8532
+ stroke="#2D3748"
8533
+ stroke-width="0.5"
8534
+ rx="4"
8535
+ filter="url(#sketch-rough)"/>
8250
8536
  </g>`;
8251
8537
  }
8252
8538
  return `<g${this.getDataNodeId(node)}>
8253
8539
  <!-- Image Background -->
8254
8540
  <rect x="${pos.x}" y="${pos.y}"
8255
8541
  width="${pos.width}" height="${pos.height}"
8256
- fill="#E8E8E8"
8542
+ fill="${imageBg}"
8257
8543
  stroke="#2D3748"
8258
8544
  stroke-width="0.5"
8259
8545
  rx="4"
@@ -8308,17 +8594,23 @@ var SketchSVGRenderer = class extends SVGRenderer {
8308
8594
  */
8309
8595
  renderSidebarMenu(node, pos) {
8310
8596
  const itemsStr = String(node.props.items || "Item 1,Item 2,Item 3");
8597
+ const iconsStr = String(node.props.icons || "");
8311
8598
  const items = itemsStr.split(",").map((s) => s.trim());
8599
+ const icons = iconsStr ? iconsStr.split(",").map((s) => s.trim()) : [];
8312
8600
  const itemHeight = 40;
8313
8601
  const fontSize = 14;
8314
8602
  const activeIndex = Number(node.props.active || 0);
8315
8603
  const accentColor = this.resolveAccentColor();
8604
+ const variantProp = String(node.props.variant || "").trim();
8605
+ const semanticVariant = variantProp ? this.getSemanticVariantColor(variantProp) : void 0;
8606
+ const hasVariant = variantProp.length > 0 && (semanticVariant !== void 0 || this.colorResolver.hasColor(variantProp));
8607
+ const activeColor = hasVariant ? this.resolveVariantColor(variantProp, accentColor) : accentColor;
8316
8608
  let svg = `<g${this.getDataNodeId(node)}>`;
8317
8609
  items.forEach((item, index) => {
8318
8610
  const itemY = pos.y + index * itemHeight;
8319
8611
  const isActive = index === activeIndex;
8320
- const bgColor = isActive ? this.hexToRgba(accentColor, 0.15) : "transparent";
8321
- const textColor = isActive ? accentColor : this.resolveTextColor();
8612
+ const bgColor = isActive ? this.hexToRgba(activeColor, 0.15) : "transparent";
8613
+ const textColor = isActive ? activeColor : this.resolveTextColor();
8322
8614
  const fontWeight = isActive ? "500" : "400";
8323
8615
  if (isActive) {
8324
8616
  svg += `
@@ -8328,8 +8620,24 @@ var SketchSVGRenderer = class extends SVGRenderer {
8328
8620
  fill="${bgColor}"
8329
8621
  filter="url(#sketch-rough)"/>`;
8330
8622
  }
8623
+ let currentX = pos.x + 12;
8624
+ if (icons[index]) {
8625
+ const iconSvg = getIcon(icons[index]);
8626
+ if (iconSvg) {
8627
+ const iconSize = 16;
8628
+ const iconY = itemY + (itemHeight - iconSize) / 2;
8629
+ const iconColor = isActive ? activeColor : this.resolveMutedColor();
8630
+ svg += `
8631
+ <g transform="translate(${currentX}, ${iconY})">
8632
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
8633
+ ${this.extractSvgContent(iconSvg)}
8634
+ </svg>
8635
+ </g>`;
8636
+ currentX += iconSize + 8;
8637
+ }
8638
+ }
8331
8639
  svg += `
8332
- <text x="${pos.x + 12}" y="${itemY + itemHeight / 2 + 5}"
8640
+ <text x="${currentX}" y="${itemY + itemHeight / 2 + 5}"
8333
8641
  font-family="${this.fontFamily}"
8334
8642
  font-size="${fontSize}"
8335
8643
  font-weight="${fontWeight}"