@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.js CHANGED
@@ -1647,7 +1647,8 @@ function validateSemanticDiagnostics(ast, sourceMap) {
1647
1647
  if (enumValues) {
1648
1648
  const normalizedValue = String(propValue);
1649
1649
  const isCustomVariantFromColors = propName === "variant" && !enumValues.includes(normalizedValue) && Object.prototype.hasOwnProperty.call(ast.colors || {}, normalizedValue);
1650
- if (!enumValues.includes(normalizedValue) && !isCustomVariantFromColors) {
1650
+ const isPropReference = normalizedValue.startsWith("prop_");
1651
+ if (!enumValues.includes(normalizedValue) && !isCustomVariantFromColors && !isPropReference) {
1651
1652
  emitWarning(
1652
1653
  `Invalid value "${normalizedValue}" for property "${propName}" in component "${componentType}".`,
1653
1654
  "COMPONENT_INVALID_PROPERTY_VALUE",
@@ -1757,7 +1758,7 @@ function validateSemanticDiagnostics(ast, sourceMap) {
1757
1758
  const enumValues = rules.enumParams?.[paramName];
1758
1759
  if (enumValues) {
1759
1760
  const normalizedValue = String(paramValue);
1760
- if (!enumValues.includes(normalizedValue)) {
1761
+ if (!enumValues.includes(normalizedValue) && !normalizedValue.startsWith("prop_")) {
1761
1762
  emitWarning(
1762
1763
  `Invalid value "${normalizedValue}" for parameter "${paramName}" in layout "${layout.layoutType}".`,
1763
1764
  "LAYOUT_INVALID_PARAMETER_VALUE",
@@ -2643,6 +2644,20 @@ ${messages}`);
2643
2644
  key
2644
2645
  );
2645
2646
  if (resolvedValue !== void 0) {
2647
+ const wasPropReference = typeof value === "string" && value.startsWith("prop_");
2648
+ if (wasPropReference) {
2649
+ const layoutMetadata = LAYOUTS2[layoutType];
2650
+ const property = layoutMetadata?.properties?.[key];
2651
+ if (property?.type === "enum" && Array.isArray(property.options)) {
2652
+ const normalizedValue = String(resolvedValue);
2653
+ if (!property.options.includes(normalizedValue)) {
2654
+ this.warnings.push({
2655
+ type: "invalid-bound-enum-value",
2656
+ message: `Invalid value "${normalizedValue}" for parameter "${key}" in layout "${layoutType}". Expected one of: ${property.options.join(", ")}.`
2657
+ });
2658
+ }
2659
+ }
2660
+ }
2646
2661
  resolved[key] = resolvedValue;
2647
2662
  }
2648
2663
  }
@@ -2707,6 +2722,20 @@ ${messages}`);
2707
2722
  key
2708
2723
  );
2709
2724
  if (resolvedValue !== void 0) {
2725
+ const wasPropReference = typeof value === "string" && value.startsWith("prop_");
2726
+ if (wasPropReference) {
2727
+ const metadata = COMPONENTS2[componentType];
2728
+ const property = metadata?.properties?.[key];
2729
+ if (property?.type === "enum" && Array.isArray(property.options)) {
2730
+ const normalizedValue = String(resolvedValue);
2731
+ if (!property.options.includes(normalizedValue)) {
2732
+ this.warnings.push({
2733
+ type: "invalid-bound-enum-value",
2734
+ message: `Invalid value "${normalizedValue}" for property "${key}" in component "${componentType}". Expected one of: ${property.options.join(", ")}.`
2735
+ });
2736
+ }
2737
+ }
2738
+ }
2710
2739
  resolved[key] = resolvedValue;
2711
2740
  }
2712
2741
  }
@@ -4517,14 +4546,14 @@ var THEMES = {
4517
4546
  primaryLight: "#EFF6FF"
4518
4547
  },
4519
4548
  dark: {
4520
- bg: "#0F172A",
4521
- cardBg: "#1E293B",
4522
- border: "#334155",
4523
- text: "#FFFFFF",
4524
- textMuted: "#94A3B8",
4549
+ bg: "#111111",
4550
+ cardBg: "#1C1C1C",
4551
+ border: "#303030",
4552
+ text: "#F0F0F0",
4553
+ textMuted: "#808080",
4525
4554
  primary: "#60A5FA",
4526
4555
  primaryHover: "#3B82F6",
4527
- primaryLight: "#1E3A8A"
4556
+ primaryLight: "#1C2A3A"
4528
4557
  }
4529
4558
  };
4530
4559
  var SVGRenderer = class {
@@ -4765,6 +4794,8 @@ var SVGRenderer = class {
4765
4794
  const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
4766
4795
  const labelOffset = this.parseBooleanProp(node.props.labelSpace, false) ? 18 : 0;
4767
4796
  const fullWidth = this.shouldButtonFillAvailableWidth(node);
4797
+ const iconName = String(node.props.icon || "").trim();
4798
+ const iconAlign = String(node.props.iconAlign || "left").toLowerCase();
4768
4799
  const radius = this.tokens.button.radius;
4769
4800
  const fontSize = this.tokens.button.fontSize;
4770
4801
  const fontWeight = this.tokens.button.fontWeight;
@@ -4772,30 +4803,62 @@ var SVGRenderer = class {
4772
4803
  const controlHeight = resolveActionControlHeight(size, density);
4773
4804
  const buttonY = pos.y + labelOffset;
4774
4805
  const buttonHeight = Math.max(16, Math.min(controlHeight, pos.height - labelOffset));
4806
+ const iconSvg = iconName ? getIcon(iconName) : null;
4807
+ const iconSize = iconSvg ? Math.round(fontSize * 1.1) : 0;
4808
+ const iconGap = iconSvg ? 8 : 0;
4809
+ const edgePad = 12;
4810
+ const textPad = paddingX + extraPadding;
4775
4811
  const idealTextWidth = this.estimateTextWidth(text, fontSize);
4776
- const buttonWidth = fullWidth ? Math.max(1, pos.width) : this.clampControlWidth(Math.max(Math.ceil(idealTextWidth + (paddingX + extraPadding) * 2), 60), pos.width);
4777
- const availableTextWidth = Math.max(0, buttonWidth - (paddingX + extraPadding) * 2);
4812
+ const buttonWidth = fullWidth ? Math.max(1, pos.width) : this.clampControlWidth(Math.max(Math.ceil(idealTextWidth + (iconSvg ? iconSize + iconGap : 0) + textPad * 2), 60), pos.width);
4813
+ const availableTextWidth = Math.max(0, buttonWidth - textPad * 2 - (iconSvg ? iconSize + iconGap : 0));
4778
4814
  const visibleText = this.truncateTextToWidth(text, availableTextWidth, fontSize);
4779
4815
  const semanticBase = this.getSemanticVariantColor(variant);
4780
4816
  const hasExplicitVariantColor = semanticBase !== void 0 || this.colorResolver.hasColor(variant);
4781
4817
  const resolvedBase = this.resolveVariantColor(variant, this.renderTheme.primary);
4782
- const bgColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.85) : "rgba(226, 232, 240, 0.9)";
4818
+ const isDarkMode = this.options.theme === "dark";
4819
+ const bgColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.85) : isDarkMode ? "rgba(48, 48, 55, 0.9)" : "rgba(226, 232, 240, 0.9)";
4783
4820
  const textColor = hasExplicitVariantColor ? "#FFFFFF" : this.hexToRgba(this.resolveTextColor(), 0.85);
4784
- const borderColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.7) : "rgba(100, 116, 139, 0.4)";
4785
- return `<g${this.getDataNodeId(node)}>
4821
+ const borderColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.7) : isDarkMode ? "rgba(75, 75, 88, 0.8)" : "rgba(100, 116, 139, 0.4)";
4822
+ const iconOffsetY = buttonY + (buttonHeight - iconSize) / 2;
4823
+ const iconX = iconAlign === "right" ? pos.x + buttonWidth - edgePad - iconSize : pos.x + edgePad;
4824
+ const textAlign = String(node.props.align || "center").toLowerCase();
4825
+ const sidePad = textPad + 4;
4826
+ let textX;
4827
+ let textAnchor;
4828
+ if (textAlign === "left") {
4829
+ textX = iconSvg && iconAlign === "left" ? pos.x + edgePad + iconSize + iconGap : pos.x + sidePad;
4830
+ textAnchor = "start";
4831
+ } else if (textAlign === "right") {
4832
+ textX = iconSvg && iconAlign === "right" ? pos.x + buttonWidth - edgePad - iconSize - iconGap : pos.x + buttonWidth - sidePad;
4833
+ textAnchor = "end";
4834
+ } else {
4835
+ textX = pos.x + buttonWidth / 2;
4836
+ textAnchor = "middle";
4837
+ }
4838
+ let svg = `<g${this.getDataNodeId(node)}>
4786
4839
  <rect x="${pos.x}" y="${buttonY}"
4787
4840
  width="${buttonWidth}" height="${buttonHeight}"
4788
4841
  rx="${radius}"
4789
4842
  fill="${bgColor}"
4790
4843
  stroke="${borderColor}"
4791
- stroke-width="1"/>
4792
- <text x="${pos.x + buttonWidth / 2}" y="${buttonY + buttonHeight / 2 + fontSize * 0.35}"
4844
+ stroke-width="1"/>`;
4845
+ if (iconSvg) {
4846
+ svg += `
4847
+ <g transform="translate(${iconX}, ${iconOffsetY})">
4848
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${textColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
4849
+ ${this.extractSvgContent(iconSvg)}
4850
+ </svg>
4851
+ </g>`;
4852
+ }
4853
+ svg += `
4854
+ <text x="${textX}" y="${buttonY + buttonHeight / 2 + fontSize * 0.35}"
4793
4855
  font-family="Arial, Helvetica, sans-serif"
4794
4856
  font-size="${fontSize}"
4795
4857
  font-weight="${fontWeight}"
4796
4858
  fill="${textColor}"
4797
- text-anchor="middle">${this.escapeXml(visibleText)}</text>
4859
+ text-anchor="${textAnchor}">${this.escapeXml(visibleText)}</text>
4798
4860
  </g>`;
4861
+ return svg;
4799
4862
  }
4800
4863
  renderLink(node, pos) {
4801
4864
  const text = String(node.props.text || "Link");
@@ -4836,28 +4899,66 @@ var SVGRenderer = class {
4836
4899
  renderInput(node, pos) {
4837
4900
  const label = String(node.props.label || "");
4838
4901
  const placeholder = String(node.props.placeholder || "");
4902
+ const iconLeftName = String(node.props.iconLeft || "").trim();
4903
+ const iconRightName = String(node.props.iconRight || "").trim();
4839
4904
  const radius = this.tokens.input.radius;
4840
4905
  const fontSize = this.tokens.input.fontSize;
4841
4906
  const paddingX = this.tokens.input.paddingX;
4842
4907
  const labelOffset = this.getControlLabelOffset(label);
4843
4908
  const controlY = pos.y + labelOffset;
4844
4909
  const controlHeight = Math.max(16, pos.height - labelOffset);
4845
- return `<g${this.getDataNodeId(node)}>
4846
- ${label ? `<text x="${pos.x + paddingX}" y="${this.getControlLabelBaselineY(pos.y)}"
4910
+ const iconSize = 16;
4911
+ const iconPad = 12;
4912
+ const iconInnerGap = 8;
4913
+ const iconLeftSvg = iconLeftName ? getIcon(iconLeftName) : null;
4914
+ const iconRightSvg = iconRightName ? getIcon(iconRightName) : null;
4915
+ const leftOffset = iconLeftSvg ? iconPad + iconSize + iconInnerGap : 0;
4916
+ const rightOffset = iconRightSvg ? iconPad + iconSize + iconInnerGap : 0;
4917
+ const textX = pos.x + (iconLeftSvg ? leftOffset : paddingX);
4918
+ const iconColor = this.hexToRgba(this.resolveMutedColor(), 0.8);
4919
+ const iconCenterY = controlY + (controlHeight - iconSize) / 2;
4920
+ let svg = `<g${this.getDataNodeId(node)}>`;
4921
+ if (label) {
4922
+ svg += `
4923
+ <text x="${pos.x + paddingX}" y="${this.getControlLabelBaselineY(pos.y)}"
4847
4924
  font-family="Arial, Helvetica, sans-serif"
4848
4925
  font-size="12"
4849
- fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>` : ""}
4926
+ fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>`;
4927
+ }
4928
+ svg += `
4850
4929
  <rect x="${pos.x}" y="${controlY}"
4851
4930
  width="${pos.width}" height="${controlHeight}"
4852
4931
  rx="${radius}"
4853
4932
  fill="${this.renderTheme.cardBg}"
4854
4933
  stroke="${this.renderTheme.border}"
4855
- stroke-width="1"/>
4856
- <text x="${pos.x + paddingX}" y="${controlY + controlHeight / 2 + 5}"
4934
+ stroke-width="1"/>`;
4935
+ if (iconLeftSvg) {
4936
+ svg += `
4937
+ <g transform="translate(${pos.x + iconPad}, ${iconCenterY})">
4938
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
4939
+ ${this.extractSvgContent(iconLeftSvg)}
4940
+ </svg>
4941
+ </g>`;
4942
+ }
4943
+ if (iconRightSvg) {
4944
+ svg += `
4945
+ <g transform="translate(${pos.x + pos.width - iconPad - iconSize}, ${iconCenterY})">
4946
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
4947
+ ${this.extractSvgContent(iconRightSvg)}
4948
+ </svg>
4949
+ </g>`;
4950
+ }
4951
+ if (placeholder) {
4952
+ const availPlaceholderWidth = pos.width - (iconLeftSvg ? leftOffset : paddingX) - (iconRightSvg ? rightOffset : paddingX);
4953
+ const visiblePlaceholder = this.truncateTextToWidth(placeholder, Math.max(0, availPlaceholderWidth), fontSize);
4954
+ svg += `
4955
+ <text x="${textX}" y="${controlY + controlHeight / 2 + 5}"
4857
4956
  font-family="Arial, Helvetica, sans-serif"
4858
4957
  font-size="${fontSize}"
4859
- fill="${this.renderTheme.textMuted}">${this.escapeXml(placeholder)}</text>
4860
- </g>`;
4958
+ fill="${this.renderTheme.textMuted}">${this.escapeXml(visiblePlaceholder)}</text>`;
4959
+ }
4960
+ svg += "\n </g>";
4961
+ return svg;
4861
4962
  }
4862
4963
  renderTopbar(node, pos) {
4863
4964
  const title = String(node.props.title || "App");
@@ -5437,30 +5538,66 @@ var SVGRenderer = class {
5437
5538
  renderSelect(node, pos) {
5438
5539
  const label = String(node.props.label || "");
5439
5540
  const placeholder = String(node.props.placeholder || "Select...");
5541
+ const iconLeftName = String(node.props.iconLeft || "").trim();
5542
+ const iconRightName = String(node.props.iconRight || "").trim();
5440
5543
  const labelOffset = this.getControlLabelOffset(label);
5441
5544
  const controlY = pos.y + labelOffset;
5442
5545
  const controlHeight = Math.max(16, pos.height - labelOffset);
5443
5546
  const centerY = controlY + controlHeight / 2 + 5;
5444
- return `<g${this.getDataNodeId(node)}>
5445
- ${label ? `<text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
5446
- font-family="Arial, Helvetica, sans-serif"
5447
- font-size="12"
5448
- fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>` : ""}
5449
- <rect x="${pos.x}" y="${controlY}"
5450
- width="${pos.width}" height="${controlHeight}"
5451
- rx="6"
5452
- fill="${this.renderTheme.cardBg}"
5453
- stroke="${this.renderTheme.border}"
5454
- stroke-width="1"/>
5455
- <text x="${pos.x + 12}" y="${centerY}"
5456
- font-family="Arial, Helvetica, sans-serif"
5457
- font-size="14"
5458
- fill="${this.renderTheme.textMuted}">${this.escapeXml(placeholder)}</text>
5459
- <text x="${pos.x + pos.width - 20}" y="${centerY}"
5460
- font-family="Arial, Helvetica, sans-serif"
5461
- font-size="16"
5547
+ const iconSize = 16;
5548
+ const iconPad = 12;
5549
+ const iconInnerGap = 8;
5550
+ const iconLeftSvg = iconLeftName ? getIcon(iconLeftName) : null;
5551
+ const iconRightSvg = iconRightName ? getIcon(iconRightName) : null;
5552
+ const leftOffset = iconLeftSvg ? iconPad + iconSize + iconInnerGap : 0;
5553
+ const chevronWidth = 20;
5554
+ const iconColor = this.hexToRgba(this.resolveMutedColor(), 0.8);
5555
+ const iconCenterY = controlY + (controlHeight - iconSize) / 2;
5556
+ let svg = `<g${this.getDataNodeId(node)}>`;
5557
+ if (label) {
5558
+ svg += `
5559
+ <text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
5560
+ font-family="Arial, Helvetica, sans-serif"
5561
+ font-size="12"
5562
+ fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>`;
5563
+ }
5564
+ svg += `
5565
+ <rect x="${pos.x}" y="${controlY}"
5566
+ width="${pos.width}" height="${controlHeight}"
5567
+ rx="6"
5568
+ fill="${this.renderTheme.cardBg}"
5569
+ stroke="${this.renderTheme.border}"
5570
+ stroke-width="1"/>`;
5571
+ if (iconLeftSvg) {
5572
+ svg += `
5573
+ <g transform="translate(${pos.x + iconPad}, ${iconCenterY})">
5574
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
5575
+ ${this.extractSvgContent(iconLeftSvg)}
5576
+ </svg>
5577
+ </g>`;
5578
+ }
5579
+ if (iconRightSvg) {
5580
+ svg += `
5581
+ <g transform="translate(${pos.x + pos.width - chevronWidth - iconPad - iconSize}, ${iconCenterY})">
5582
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
5583
+ ${this.extractSvgContent(iconRightSvg)}
5584
+ </svg>
5585
+ </g>`;
5586
+ }
5587
+ const textX = pos.x + (iconLeftSvg ? leftOffset : 12);
5588
+ const availPlaceholderWidth = pos.width - (iconLeftSvg ? leftOffset : 12) - chevronWidth - (iconRightSvg ? iconPad + iconSize + iconInnerGap : 0);
5589
+ const visiblePlaceholder = this.truncateTextToWidth(placeholder, Math.max(0, availPlaceholderWidth), 14);
5590
+ svg += `
5591
+ <text x="${textX}" y="${centerY}"
5592
+ font-family="Arial, Helvetica, sans-serif"
5593
+ font-size="14"
5594
+ fill="${this.renderTheme.textMuted}">${this.escapeXml(visiblePlaceholder)}</text>
5595
+ <text x="${pos.x + pos.width - 20}" y="${centerY}"
5596
+ font-family="Arial, Helvetica, sans-serif"
5597
+ font-size="16"
5462
5598
  fill="${this.renderTheme.textMuted}">\u25BC</text>
5463
5599
  </g>`;
5600
+ return svg;
5464
5601
  }
5465
5602
  renderCheckbox(node, pos) {
5466
5603
  const label = String(node.props.label || "Checkbox");
@@ -5893,7 +6030,9 @@ var SVGRenderer = class {
5893
6030
  renderImage(node, pos) {
5894
6031
  const placeholder = String(node.props.placeholder || "landscape").toLowerCase();
5895
6032
  const placeholderIcon = String(node.props.icon || "").trim();
6033
+ const variant = String(node.props.variant || "").trim();
5896
6034
  const placeholderIconSvg = placeholder === "icon" && placeholderIcon ? getIcon(placeholderIcon) : null;
6035
+ const imageBg = this.options.theme === "dark" ? "#2A2A2A" : "#E8E8E8";
5897
6036
  const aspectRatios = {
5898
6037
  landscape: 16 / 9,
5899
6038
  portrait: 2 / 3,
@@ -5911,30 +6050,29 @@ var SVGRenderer = class {
5911
6050
  }
5912
6051
  const offsetX = pos.x + (pos.width - iconWidth) / 2;
5913
6052
  const offsetY = pos.y + (pos.height - iconHeight) / 2;
5914
- let svg = `<g${this.getDataNodeId(node)}>
5915
- <!-- Image Background -->
5916
- <rect x="${pos.x}" y="${pos.y}" width="${pos.width}" height="${pos.height}" fill="#E8E8E8"/>`;
5917
6053
  if (placeholder === "icon" && placeholderIconSvg) {
5918
- const badgeSize = Math.max(24, Math.min(iconWidth, iconHeight) * 0.78);
5919
- const badgeX = pos.x + (pos.width - badgeSize) / 2;
5920
- const badgeY = pos.y + (pos.height - badgeSize) / 2;
5921
- const iconSize = badgeSize * 0.62;
5922
- const iconOffsetX = badgeX + (badgeSize - iconSize) / 2;
5923
- const iconOffsetY = badgeY + (badgeSize - iconSize) / 2;
5924
- svg += `
5925
- <!-- Custom Icon Placeholder -->
5926
- <rect x="${badgeX}" y="${badgeY}"
5927
- width="${badgeSize}" height="${badgeSize}"
5928
- rx="${Math.max(4, badgeSize * 0.2)}"
5929
- fill="rgba(255, 255, 255, 0.6)"
5930
- stroke="#888"
5931
- stroke-width="1"/>
6054
+ const semanticBase = variant ? this.getSemanticVariantColor(variant) : void 0;
6055
+ const hasVariant = variant.length > 0 && (semanticBase !== void 0 || this.colorResolver.hasColor(variant));
6056
+ const variantColor = hasVariant ? this.resolveVariantColor(variant, this.renderTheme.primary) : null;
6057
+ const bgColor = hasVariant ? this.hexToRgba(variantColor, 0.12) : imageBg;
6058
+ const iconColor = hasVariant ? variantColor : this.options.theme === "dark" ? "#888888" : "#666666";
6059
+ const iconSize = Math.max(16, Math.min(pos.width, pos.height) * 0.6);
6060
+ const iconOffsetX = pos.x + (pos.width - iconSize) / 2;
6061
+ const iconOffsetY = pos.y + (pos.height - iconSize) / 2;
6062
+ return `<g${this.getDataNodeId(node)}>
6063
+ <rect x="${pos.x}" y="${pos.y}" width="${pos.width}" height="${pos.height}" fill="${bgColor}" rx="4"/>
5932
6064
  <g transform="translate(${iconOffsetX}, ${iconOffsetY})">
5933
- <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="#555" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
6065
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
5934
6066
  ${this.extractSvgContent(placeholderIconSvg)}
5935
6067
  </svg>
5936
- </g>`;
5937
- } else if (["landscape", "portrait", "square"].includes(placeholder)) {
6068
+ </g>
6069
+ <rect x="${pos.x}" y="${pos.y}" width="${pos.width}" height="${pos.height}" fill="none" stroke="${this.renderTheme.border}" stroke-width="1" rx="4"/>
6070
+ </g>`;
6071
+ }
6072
+ let svg = `<g${this.getDataNodeId(node)}>
6073
+ <!-- Image Background -->
6074
+ <rect x="${pos.x}" y="${pos.y}" width="${pos.width}" height="${pos.height}" fill="${imageBg}"/>`;
6075
+ if (["landscape", "portrait", "square"].includes(placeholder)) {
5938
6076
  const cameraCx = offsetX + iconWidth / 2;
5939
6077
  const cameraCy = offsetY + iconHeight / 2;
5940
6078
  const scale = Math.min(iconWidth, iconHeight) / 24;
@@ -6038,18 +6176,22 @@ var SVGRenderer = class {
6038
6176
  const fontSize = 14;
6039
6177
  const activeIndex = Number(node.props.active || 0);
6040
6178
  const accentColor = this.resolveAccentColor();
6179
+ const variantProp = String(node.props.variant || "").trim();
6180
+ const semanticVariant = variantProp ? this.getSemanticVariantColor(variantProp) : void 0;
6181
+ const hasVariant = variantProp.length > 0 && (semanticVariant !== void 0 || this.colorResolver.hasColor(variantProp));
6182
+ const activeColor = hasVariant ? this.resolveVariantColor(variantProp, accentColor) : accentColor;
6041
6183
  let svg = `<g${this.getDataNodeId(node)}>`;
6042
6184
  items.forEach((item, index) => {
6043
6185
  const itemY = pos.y + index * itemHeight;
6044
6186
  const isActive = index === activeIndex;
6045
- const bgColor = isActive ? this.hexToRgba(accentColor, 0.15) : "transparent";
6046
- const textColor = isActive ? this.hexToRgba(accentColor, 0.9) : this.hexToRgba(this.resolveTextColor(), 0.75);
6187
+ const bgColor = isActive ? this.hexToRgba(activeColor, 0.15) : "transparent";
6188
+ const textColor = isActive ? this.hexToRgba(activeColor, 0.9) : this.hexToRgba(this.resolveTextColor(), 0.75);
6047
6189
  const fontWeight = isActive ? "500" : "400";
6048
6190
  if (isActive) {
6049
6191
  svg += `
6050
- <rect x="${pos.x}" y="${itemY}"
6051
- width="${pos.width}" height="${itemHeight}"
6052
- rx="6"
6192
+ <rect x="${pos.x}" y="${itemY}"
6193
+ width="${pos.width}" height="${itemHeight}"
6194
+ rx="6"
6053
6195
  fill="${bgColor}"/>`;
6054
6196
  }
6055
6197
  let currentX = pos.x + 12;
@@ -6058,9 +6200,10 @@ var SVGRenderer = class {
6058
6200
  if (iconSvg) {
6059
6201
  const iconSize = 16;
6060
6202
  const iconY = itemY + (itemHeight - iconSize) / 2;
6203
+ const iconColor = isActive ? this.hexToRgba(activeColor, 0.9) : this.hexToRgba(this.resolveMutedColor(), 0.9);
6061
6204
  svg += `
6062
6205
  <g transform="translate(${currentX}, ${iconY})">
6063
- <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${this.hexToRgba(this.resolveMutedColor(), 0.9)}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
6206
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
6064
6207
  ${this.extractSvgContent(iconSvg)}
6065
6208
  </svg>
6066
6209
  </g>`;
@@ -6068,10 +6211,10 @@ var SVGRenderer = class {
6068
6211
  }
6069
6212
  }
6070
6213
  svg += `
6071
- <text x="${currentX}" y="${itemY + itemHeight / 2 + 5}"
6072
- font-family="Arial, Helvetica, sans-serif"
6073
- font-size="${fontSize}"
6074
- font-weight="${fontWeight}"
6214
+ <text x="${currentX}" y="${itemY + itemHeight / 2 + 5}"
6215
+ font-family="Arial, Helvetica, sans-serif"
6216
+ font-size="${fontSize}"
6217
+ font-weight="${fontWeight}"
6075
6218
  fill="${textColor}">${this.escapeXml(item)}</text>`;
6076
6219
  });
6077
6220
  svg += "\n </g>";
@@ -6111,9 +6254,10 @@ var SVGRenderer = class {
6111
6254
  const semanticBase = this.getSemanticVariantColor(variant);
6112
6255
  const hasExplicitVariantColor = semanticBase !== void 0 || this.colorResolver.hasColor(variant);
6113
6256
  const resolvedBase = this.resolveVariantColor(variant, this.renderTheme.primary);
6114
- const bgColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.85) : "rgba(226, 232, 240, 0.9)";
6257
+ const isDarkMode = this.options.theme === "dark";
6258
+ const bgColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.85) : isDarkMode ? "rgba(48, 48, 55, 0.9)" : "rgba(226, 232, 240, 0.9)";
6115
6259
  const iconColor = hasExplicitVariantColor ? "#FFFFFF" : this.hexToRgba(this.resolveTextColor(), 0.75);
6116
- const borderColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.7) : "rgba(100, 116, 139, 0.4)";
6260
+ const borderColor = hasExplicitVariantColor ? this.hexToRgba(resolvedBase, 0.7) : isDarkMode ? "rgba(75, 75, 88, 0.8)" : "rgba(100, 116, 139, 0.4)";
6117
6261
  const opacity = disabled ? "0.5" : "1";
6118
6262
  const iconSvg = getIcon(iconName);
6119
6263
  const buttonSize = Math.max(
@@ -6171,14 +6315,37 @@ var SVGRenderer = class {
6171
6315
  return this.colorResolver.resolveColor("muted", fallback);
6172
6316
  }
6173
6317
  getSemanticVariantColor(variant) {
6174
- const semantic = {
6318
+ const isDark = this.options.theme === "dark";
6319
+ const semantic = isDark ? {
6320
+ // Muted mid-range — readable on #111111 without being neon
6321
+ primary: this.renderTheme.primary,
6322
+ // already theme-aware (#60A5FA)
6323
+ secondary: "#7E8EA2",
6324
+ // desaturated slate
6325
+ success: "#22A06B",
6326
+ // muted emerald
6327
+ warning: "#B38010",
6328
+ // deep amber
6329
+ danger: "#CC4444",
6330
+ // muted red
6331
+ error: "#CC4444",
6332
+ info: "#2485AF"
6333
+ // muted sky
6334
+ } : {
6335
+ // Tailwind 500-level — works on white/light backgrounds
6175
6336
  primary: this.renderTheme.primary,
6337
+ // #3B82F6
6176
6338
  secondary: "#64748B",
6339
+ // Slate 500
6177
6340
  success: "#10B981",
6341
+ // Emerald 500
6178
6342
  warning: "#F59E0B",
6343
+ // Amber 500
6179
6344
  danger: "#EF4444",
6345
+ // Red 500
6180
6346
  error: "#EF4444",
6181
6347
  info: "#0EA5E9"
6348
+ // Sky 500
6182
6349
  };
6183
6350
  return semantic[variant];
6184
6351
  }
@@ -6639,6 +6806,19 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
6639
6806
  renderLabel(node, pos) {
6640
6807
  return this.renderTextBlock(node, pos, String(node.props.text || "Label"), 12, 1.2);
6641
6808
  }
6809
+ /**
6810
+ * Render image as a plain skeleton rectangle — no icon, no placeholder label,
6811
+ * just a filled block with the correct dimensions (aspect-ratio is preserved
6812
+ * by the layout engine, so pos already has the right size).
6813
+ */
6814
+ renderImage(node, pos) {
6815
+ return `<g${this.getDataNodeId(node)}>
6816
+ <rect x="${pos.x}" y="${pos.y}"
6817
+ width="${pos.width}" height="${pos.height}"
6818
+ rx="4"
6819
+ fill="${this.renderTheme.border}"/>
6820
+ </g>`;
6821
+ }
6642
6822
  /**
6643
6823
  * Render badge as shape only (no text)
6644
6824
  */
@@ -7313,6 +7493,8 @@ var SketchSVGRenderer = class extends SVGRenderer {
7313
7493
  const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
7314
7494
  const labelOffset = this.parseBooleanProp(node.props.labelSpace, false) ? 18 : 0;
7315
7495
  const fullWidth = this.shouldButtonFillAvailableWidth(node);
7496
+ const iconName = String(node.props.icon || "").trim();
7497
+ const iconAlign = String(node.props.iconAlign || "left").toLowerCase();
7316
7498
  const radius = this.tokens.button.radius;
7317
7499
  const fontSize = this.tokens.button.fontSize;
7318
7500
  const fontWeight = this.tokens.button.fontWeight;
@@ -7322,9 +7504,14 @@ var SketchSVGRenderer = class extends SVGRenderer {
7322
7504
  Math.min(resolveControlHeight(size, density), pos.height - labelOffset)
7323
7505
  );
7324
7506
  const buttonY = pos.y + labelOffset;
7507
+ const iconSvg = iconName ? getIcon(iconName) : null;
7508
+ const iconSize = iconSvg ? Math.round(fontSize * 1.1) : 0;
7509
+ const iconGap = iconSvg ? 8 : 0;
7510
+ const edgePad = 12;
7511
+ const textPad = paddingX + extraPadding;
7325
7512
  const idealTextWidth = text.length * fontSize * 0.6;
7326
- const buttonWidth = fullWidth ? Math.max(1, pos.width) : this.clampControlWidth(Math.max(idealTextWidth + (paddingX + extraPadding) * 2, 60), pos.width);
7327
- const availableTextWidth = Math.max(0, buttonWidth - (paddingX + extraPadding) * 2);
7513
+ const buttonWidth = fullWidth ? Math.max(1, pos.width) : this.clampControlWidth(Math.max(idealTextWidth + (iconSvg ? iconSize + iconGap : 0) + textPad * 2, 60), pos.width);
7514
+ const availableTextWidth = Math.max(0, buttonWidth - textPad * 2 - (iconSvg ? iconSize + iconGap : 0));
7328
7515
  const visibleText = this.truncateTextToWidth(text, availableTextWidth, fontSize);
7329
7516
  const semanticBase = this.getSemanticVariantColor(variant);
7330
7517
  const hasExplicitVariantColor = semanticBase !== void 0 || this.colorResolver.hasColor(variant);
@@ -7332,21 +7519,47 @@ var SketchSVGRenderer = class extends SVGRenderer {
7332
7519
  const borderColor = variantColor;
7333
7520
  const textColor = variantColor;
7334
7521
  const strokeWidth = 0.5;
7335
- return `<g${this.getDataNodeId(node)}>
7522
+ const iconOffsetY = buttonY + (buttonHeight - iconSize) / 2;
7523
+ const iconX = iconAlign === "right" ? pos.x + buttonWidth - edgePad - iconSize : pos.x + edgePad;
7524
+ const textAlign = String(node.props.align || "center").toLowerCase();
7525
+ const sidePad = textPad + 4;
7526
+ let textX;
7527
+ let textAnchor;
7528
+ if (textAlign === "left") {
7529
+ textX = iconSvg && iconAlign === "left" ? pos.x + edgePad + iconSize + iconGap : pos.x + sidePad;
7530
+ textAnchor = "start";
7531
+ } else if (textAlign === "right") {
7532
+ textX = iconSvg && iconAlign === "right" ? pos.x + buttonWidth - edgePad - iconSize - iconGap : pos.x + buttonWidth - sidePad;
7533
+ textAnchor = "end";
7534
+ } else {
7535
+ textX = pos.x + buttonWidth / 2;
7536
+ textAnchor = "middle";
7537
+ }
7538
+ let svg = `<g${this.getDataNodeId(node)}>
7336
7539
  <rect x="${pos.x}" y="${buttonY}"
7337
7540
  width="${buttonWidth}" height="${buttonHeight}"
7338
7541
  rx="${radius}"
7339
7542
  fill="none"
7340
7543
  stroke="${borderColor}"
7341
7544
  stroke-width="${strokeWidth}"
7342
- filter="url(#sketch-rough)"/>
7343
- <text x="${pos.x + buttonWidth / 2}" y="${buttonY + buttonHeight / 2 + fontSize * 0.35}"
7545
+ filter="url(#sketch-rough)"/>`;
7546
+ if (iconSvg) {
7547
+ svg += `
7548
+ <g transform="translate(${iconX}, ${iconOffsetY})">
7549
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${textColor}" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round">
7550
+ ${this.extractSvgContent(iconSvg)}
7551
+ </svg>
7552
+ </g>`;
7553
+ }
7554
+ svg += `
7555
+ <text x="${textX}" y="${buttonY + buttonHeight / 2 + fontSize * 0.35}"
7344
7556
  font-family="${this.fontFamily}"
7345
7557
  font-size="${fontSize}"
7346
7558
  font-weight="${fontWeight}"
7347
7559
  fill="${textColor}"
7348
- text-anchor="middle">${this.escapeXml(visibleText)}</text>
7560
+ text-anchor="${textAnchor}">${this.escapeXml(visibleText)}</text>
7349
7561
  </g>`;
7562
+ return svg;
7350
7563
  }
7351
7564
  /**
7352
7565
  * Render badge with colored border instead of fill
@@ -7471,29 +7684,67 @@ var SketchSVGRenderer = class extends SVGRenderer {
7471
7684
  renderInput(node, pos) {
7472
7685
  const label = String(node.props.label || "");
7473
7686
  const placeholder = String(node.props.placeholder || "");
7687
+ const iconLeftName = String(node.props.iconLeft || "").trim();
7688
+ const iconRightName = String(node.props.iconRight || "").trim();
7474
7689
  const radius = this.tokens.input.radius;
7475
7690
  const fontSize = this.tokens.input.fontSize;
7476
7691
  const paddingX = this.tokens.input.paddingX;
7477
7692
  const labelOffset = this.getControlLabelOffset(label);
7478
7693
  const controlY = pos.y + labelOffset;
7479
7694
  const controlHeight = Math.max(16, pos.height - labelOffset);
7480
- return `<g${this.getDataNodeId(node)}>
7481
- ${label ? `<text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
7695
+ const iconSize = 16;
7696
+ const iconPad = 12;
7697
+ const iconInnerGap = 8;
7698
+ const iconLeftSvg = iconLeftName ? getIcon(iconLeftName) : null;
7699
+ const iconRightSvg = iconRightName ? getIcon(iconRightName) : null;
7700
+ const leftOffset = iconLeftSvg ? iconPad + iconSize + iconInnerGap : 0;
7701
+ const rightOffset = iconRightSvg ? iconPad + iconSize + iconInnerGap : 0;
7702
+ const textX = pos.x + (iconLeftSvg ? leftOffset : paddingX);
7703
+ const iconColor = "#888888";
7704
+ const iconCenterY = controlY + (controlHeight - iconSize) / 2;
7705
+ let svg = `<g${this.getDataNodeId(node)}>`;
7706
+ if (label) {
7707
+ svg += `
7708
+ <text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
7482
7709
  font-family="${this.fontFamily}"
7483
7710
  font-size="12"
7484
- fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>` : ""}
7711
+ fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>`;
7712
+ }
7713
+ svg += `
7485
7714
  <rect x="${pos.x}" y="${controlY}"
7486
7715
  width="${pos.width}" height="${controlHeight}"
7487
7716
  rx="${radius}"
7488
7717
  fill="${this.renderTheme.cardBg}"
7489
7718
  stroke="#2D3748"
7490
7719
  stroke-width="0.5"
7491
- filter="url(#sketch-rough)"/>
7492
- ${placeholder ? `<text x="${pos.x + paddingX}" y="${controlY + controlHeight / 2 + 5}"
7720
+ filter="url(#sketch-rough)"/>`;
7721
+ if (iconLeftSvg) {
7722
+ svg += `
7723
+ <g transform="translate(${pos.x + iconPad}, ${iconCenterY})">
7724
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
7725
+ ${this.extractSvgContent(iconLeftSvg)}
7726
+ </svg>
7727
+ </g>`;
7728
+ }
7729
+ if (iconRightSvg) {
7730
+ svg += `
7731
+ <g transform="translate(${pos.x + pos.width - iconPad - iconSize}, ${iconCenterY})">
7732
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
7733
+ ${this.extractSvgContent(iconRightSvg)}
7734
+ </svg>
7735
+ </g>`;
7736
+ }
7737
+ if (placeholder) {
7738
+ const availWidth = pos.width - (iconLeftSvg ? leftOffset : paddingX) - (iconRightSvg ? rightOffset : paddingX);
7739
+ const visiblePh = this.truncateTextToWidth(placeholder, Math.max(0, availWidth), fontSize);
7740
+ svg += `
7741
+ <text x="${textX}" y="${controlY + controlHeight / 2 + 5}"
7493
7742
  font-family="${this.fontFamily}"
7494
7743
  font-size="${fontSize}"
7495
- fill="${this.renderTheme.textMuted}">${this.escapeXml(placeholder)}</text>` : ""}
7496
- </g>`;
7744
+ fill="${this.renderTheme.textMuted}">${this.escapeXml(visiblePh)}</text>`;
7745
+ }
7746
+ svg += "\n </g>";
7747
+ return svg;
7497
7748
  }
7498
7749
  /**
7499
7750
  * Render textarea with thicker border
@@ -7747,31 +7998,67 @@ var SketchSVGRenderer = class extends SVGRenderer {
7747
7998
  renderSelect(node, pos) {
7748
7999
  const label = String(node.props.label || "");
7749
8000
  const placeholder = String(node.props.placeholder || "Select...");
8001
+ const iconLeftName = String(node.props.iconLeft || "").trim();
8002
+ const iconRightName = String(node.props.iconRight || "").trim();
7750
8003
  const labelOffset = this.getControlLabelOffset(label);
7751
8004
  const controlY = pos.y + labelOffset;
7752
8005
  const controlHeight = Math.max(16, pos.height - labelOffset);
7753
8006
  const centerY = controlY + controlHeight / 2 + 5;
7754
- return `<g${this.getDataNodeId(node)}>
7755
- ${label ? `<text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
8007
+ const iconSize = 16;
8008
+ const iconPad = 12;
8009
+ const iconInnerGap = 8;
8010
+ const iconLeftSvg = iconLeftName ? getIcon(iconLeftName) : null;
8011
+ const iconRightSvg = iconRightName ? getIcon(iconRightName) : null;
8012
+ const leftOffset = iconLeftSvg ? iconPad + iconSize + iconInnerGap : 0;
8013
+ const chevronWidth = 20;
8014
+ const iconColor = "#888888";
8015
+ const iconCenterY = controlY + (controlHeight - iconSize) / 2;
8016
+ let svg = `<g${this.getDataNodeId(node)}>`;
8017
+ if (label) {
8018
+ svg += `
8019
+ <text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
7756
8020
  font-family="${this.fontFamily}"
7757
8021
  font-size="12"
7758
- fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>` : ""}
8022
+ fill="${this.renderTheme.text}">${this.escapeXml(label)}</text>`;
8023
+ }
8024
+ svg += `
7759
8025
  <rect x="${pos.x}" y="${controlY}"
7760
8026
  width="${pos.width}" height="${controlHeight}"
7761
8027
  rx="6"
7762
8028
  fill="${this.renderTheme.cardBg}"
7763
8029
  stroke="#2D3748"
7764
8030
  stroke-width="0.5"
7765
- filter="url(#sketch-rough)"/>
7766
- <text x="${pos.x + 12}" y="${centerY}"
8031
+ filter="url(#sketch-rough)"/>`;
8032
+ if (iconLeftSvg) {
8033
+ svg += `
8034
+ <g transform="translate(${pos.x + iconPad}, ${iconCenterY})">
8035
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
8036
+ ${this.extractSvgContent(iconLeftSvg)}
8037
+ </svg>
8038
+ </g>`;
8039
+ }
8040
+ if (iconRightSvg) {
8041
+ svg += `
8042
+ <g transform="translate(${pos.x + pos.width - chevronWidth - iconPad - iconSize}, ${iconCenterY})">
8043
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
8044
+ ${this.extractSvgContent(iconRightSvg)}
8045
+ </svg>
8046
+ </g>`;
8047
+ }
8048
+ const textX = pos.x + (iconLeftSvg ? leftOffset : 12);
8049
+ const availWidth = pos.width - (iconLeftSvg ? leftOffset : 12) - chevronWidth - (iconRightSvg ? iconPad + iconSize + iconInnerGap : 0);
8050
+ const visiblePh = this.truncateTextToWidth(placeholder, Math.max(0, availWidth), 14);
8051
+ svg += `
8052
+ <text x="${textX}" y="${centerY}"
7767
8053
  font-family="${this.fontFamily}"
7768
8054
  font-size="14"
7769
- fill="${this.renderTheme.textMuted}">${this.escapeXml(placeholder)}</text>
8055
+ fill="${this.renderTheme.textMuted}">${this.escapeXml(visiblePh)}</text>
7770
8056
  <text x="${pos.x + pos.width - 20}" y="${centerY}"
7771
8057
  font-family="${this.fontFamily}"
7772
8058
  font-size="16"
7773
8059
  fill="${this.renderTheme.textMuted}">\u25BC</text>
7774
8060
  </g>`;
8061
+ return svg;
7775
8062
  }
7776
8063
  /**
7777
8064
  * Render checkbox with sketch filter and Comic Sans
@@ -8170,44 +8457,43 @@ var SketchSVGRenderer = class extends SVGRenderer {
8170
8457
  renderImage(node, pos) {
8171
8458
  const placeholder = String(node.props.placeholder || "landscape").toLowerCase();
8172
8459
  const iconType = String(node.props.icon || "").trim();
8460
+ const variant = String(node.props.variant || "").trim();
8173
8461
  const iconSvg = placeholder === "icon" && iconType.length > 0 ? getIcon(iconType) : null;
8462
+ const imageBg = this.options.theme === "dark" ? "#2A2A2A" : "#E8E8E8";
8174
8463
  if (iconSvg) {
8175
- const badgeSize = Math.max(24, Math.min(pos.width, pos.height) * 0.6);
8176
- const badgeX = pos.x + (pos.width - badgeSize) / 2;
8177
- const badgeY = pos.y + (pos.height - badgeSize) / 2;
8178
- const iconSize = badgeSize * 0.62;
8179
- const iconOffsetX = badgeX + (badgeSize - iconSize) / 2;
8180
- const iconOffsetY = badgeY + (badgeSize - iconSize) / 2;
8464
+ const semanticBase = variant ? this.getSemanticVariantColor(variant) : void 0;
8465
+ const hasVariant = variant.length > 0 && (semanticBase !== void 0 || this.colorResolver.hasColor(variant));
8466
+ const variantColor = hasVariant ? this.resolveVariantColor(variant, this.renderTheme.primary) : null;
8467
+ const bgColor = hasVariant ? this.hexToRgba(variantColor, 0.12) : imageBg;
8468
+ const iconColor = hasVariant ? variantColor : "#666666";
8469
+ const iconSize = Math.max(16, Math.min(pos.width, pos.height) * 0.6);
8470
+ const iconOffsetX = pos.x + (pos.width - iconSize) / 2;
8471
+ const iconOffsetY = pos.y + (pos.height - iconSize) / 2;
8181
8472
  return `<g${this.getDataNodeId(node)}>
8182
- <!-- Image Background -->
8183
8473
  <rect x="${pos.x}" y="${pos.y}"
8184
8474
  width="${pos.width}" height="${pos.height}"
8185
- fill="#E8E8E8"
8186
- stroke="#2D3748"
8187
- stroke-width="0.5"
8475
+ fill="${bgColor}"
8188
8476
  rx="4"
8189
8477
  filter="url(#sketch-rough)"/>
8190
-
8191
- <!-- Custom Icon Placeholder -->
8192
- <rect x="${badgeX}" y="${badgeY}"
8193
- width="${badgeSize}" height="${badgeSize}"
8194
- rx="${Math.max(4, badgeSize * 0.2)}"
8195
- fill="none"
8196
- stroke="#2D3748"
8197
- stroke-width="0.5"
8198
- filter="url(#sketch-rough)"/>
8199
8478
  <g transform="translate(${iconOffsetX}, ${iconOffsetY})">
8200
- <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="#2D3748" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
8479
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
8201
8480
  ${this.extractSvgContent(iconSvg)}
8202
8481
  </svg>
8203
8482
  </g>
8483
+ <rect x="${pos.x}" y="${pos.y}"
8484
+ width="${pos.width}" height="${pos.height}"
8485
+ fill="none"
8486
+ stroke="#2D3748"
8487
+ stroke-width="0.5"
8488
+ rx="4"
8489
+ filter="url(#sketch-rough)"/>
8204
8490
  </g>`;
8205
8491
  }
8206
8492
  return `<g${this.getDataNodeId(node)}>
8207
8493
  <!-- Image Background -->
8208
8494
  <rect x="${pos.x}" y="${pos.y}"
8209
8495
  width="${pos.width}" height="${pos.height}"
8210
- fill="#E8E8E8"
8496
+ fill="${imageBg}"
8211
8497
  stroke="#2D3748"
8212
8498
  stroke-width="0.5"
8213
8499
  rx="4"
@@ -8262,17 +8548,23 @@ var SketchSVGRenderer = class extends SVGRenderer {
8262
8548
  */
8263
8549
  renderSidebarMenu(node, pos) {
8264
8550
  const itemsStr = String(node.props.items || "Item 1,Item 2,Item 3");
8551
+ const iconsStr = String(node.props.icons || "");
8265
8552
  const items = itemsStr.split(",").map((s) => s.trim());
8553
+ const icons = iconsStr ? iconsStr.split(",").map((s) => s.trim()) : [];
8266
8554
  const itemHeight = 40;
8267
8555
  const fontSize = 14;
8268
8556
  const activeIndex = Number(node.props.active || 0);
8269
8557
  const accentColor = this.resolveAccentColor();
8558
+ const variantProp = String(node.props.variant || "").trim();
8559
+ const semanticVariant = variantProp ? this.getSemanticVariantColor(variantProp) : void 0;
8560
+ const hasVariant = variantProp.length > 0 && (semanticVariant !== void 0 || this.colorResolver.hasColor(variantProp));
8561
+ const activeColor = hasVariant ? this.resolveVariantColor(variantProp, accentColor) : accentColor;
8270
8562
  let svg = `<g${this.getDataNodeId(node)}>`;
8271
8563
  items.forEach((item, index) => {
8272
8564
  const itemY = pos.y + index * itemHeight;
8273
8565
  const isActive = index === activeIndex;
8274
- const bgColor = isActive ? this.hexToRgba(accentColor, 0.15) : "transparent";
8275
- const textColor = isActive ? accentColor : this.resolveTextColor();
8566
+ const bgColor = isActive ? this.hexToRgba(activeColor, 0.15) : "transparent";
8567
+ const textColor = isActive ? activeColor : this.resolveTextColor();
8276
8568
  const fontWeight = isActive ? "500" : "400";
8277
8569
  if (isActive) {
8278
8570
  svg += `
@@ -8282,8 +8574,24 @@ var SketchSVGRenderer = class extends SVGRenderer {
8282
8574
  fill="${bgColor}"
8283
8575
  filter="url(#sketch-rough)"/>`;
8284
8576
  }
8577
+ let currentX = pos.x + 12;
8578
+ if (icons[index]) {
8579
+ const iconSvg = getIcon(icons[index]);
8580
+ if (iconSvg) {
8581
+ const iconSize = 16;
8582
+ const iconY = itemY + (itemHeight - iconSize) / 2;
8583
+ const iconColor = isActive ? activeColor : this.resolveMutedColor();
8584
+ svg += `
8585
+ <g transform="translate(${currentX}, ${iconY})">
8586
+ <svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
8587
+ ${this.extractSvgContent(iconSvg)}
8588
+ </svg>
8589
+ </g>`;
8590
+ currentX += iconSize + 8;
8591
+ }
8592
+ }
8285
8593
  svg += `
8286
- <text x="${pos.x + 12}" y="${itemY + itemHeight / 2 + 5}"
8594
+ <text x="${currentX}" y="${itemY + itemHeight / 2 + 5}"
8287
8595
  font-family="${this.fontFamily}"
8288
8596
  font-size="${fontSize}"
8289
8597
  font-weight="${fontWeight}"