Spaces:
Runtime error
feat: visual spec v2 — all 9 improvements + shadow interpolation restored
Browse filesVisual Spec Plugin (complete rewrite):
1. White background behind entire spec page
2. Colors grouped by category (brand, text, bg, border, palette)
with sub-headings — semantic groups first, then palette ramps
3. Typography split into Desktop and Mobile side-by-side columns
4. Loads actual extracted font (e.g. Open Sans) with safe cascade
fallback to Inter if unavailable
5. Pangram sample text per tier ("The quick brown fox...")
6. Full typography specs: font family, size, weight, line height
7. Spacing: desktop/mobile columns with blue square blocks
(square size = spacing value)
8. Spacing uses Variables API (not Styles) — shows in Variables panel.
Starter plan may limit variable creation.
9. Shadows: restored interpolation to always produce 5 elevation
levels (xs/sm/md/lg/xl) from whatever was extracted
Also: copied plugin to v2 directory so Figma loads it regardless
of which registration is active.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@@ -1502,14 +1502,12 @@ async def run_stage2_analysis_v2(
|
|
| 1502 |
shadow_count = 0
|
| 1503 |
if state.desktop_normalized:
|
| 1504 |
shadow_count = len(getattr(state.desktop_normalized, 'shadows', {}))
|
| 1505 |
-
tobe_shadow_count =
|
| 1506 |
-
_SHADOW_LABELS = {1: "md", 2: "sm → lg", 3: "sm → md → lg", 4: "xs → sm → lg → xl", 5: "xs → sm → md → lg → xl"}
|
| 1507 |
-
tobe_label = _SHADOW_LABELS.get(tobe_shadow_count, f"{tobe_shadow_count} levels")
|
| 1508 |
cards.append(_render_as_is_to_be(
|
| 1509 |
-
"Shadows", f"{shadow_count}
|
| 1510 |
"Elevation tokens" if shadow_count > 0 else "No shadows found",
|
| 1511 |
f"{tobe_shadow_count} levels",
|
| 1512 |
-
|
| 1513 |
icon="🌫️"
|
| 1514 |
))
|
| 1515 |
asis_tobe_html = "".join(cards)
|
|
@@ -3605,16 +3603,11 @@ def export_tokens_json(convention: str = "semantic"):
|
|
| 3605 |
token_count += 1
|
| 3606 |
|
| 3607 |
# =========================================================================
|
| 3608 |
-
# SHADOWS — W3C DTCG format —
|
|
|
|
| 3609 |
# =========================================================================
|
| 3610 |
-
|
| 3611 |
-
|
| 3612 |
-
1: ["shadow.md"],
|
| 3613 |
-
2: ["shadow.sm", "shadow.lg"],
|
| 3614 |
-
3: ["shadow.sm", "shadow.md", "shadow.lg"],
|
| 3615 |
-
4: ["shadow.xs", "shadow.sm", "shadow.lg", "shadow.xl"],
|
| 3616 |
-
5: ["shadow.xs", "shadow.sm", "shadow.md", "shadow.lg", "shadow.xl"],
|
| 3617 |
-
}
|
| 3618 |
|
| 3619 |
if state.desktop_normalized and state.desktop_normalized.shadows:
|
| 3620 |
sorted_shadows = sorted(
|
|
@@ -3634,16 +3627,54 @@ def export_tokens_json(convention: str = "semantic"):
|
|
| 3634 |
"color": p.get("color", "rgba(0,0,0,0.25)"),
|
| 3635 |
})
|
| 3636 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3637 |
n = len(parsed_shadows)
|
| 3638 |
-
|
| 3639 |
-
|
| 3640 |
-
|
| 3641 |
-
|
| 3642 |
-
|
| 3643 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3644 |
|
| 3645 |
for i, shadow in enumerate(final_shadows):
|
| 3646 |
-
token_name =
|
| 3647 |
dtcg_value = {
|
| 3648 |
"color": shadow["color"],
|
| 3649 |
"offsetX": f"{shadow['x']}px",
|
|
|
|
| 1502 |
shadow_count = 0
|
| 1503 |
if state.desktop_normalized:
|
| 1504 |
shadow_count = len(getattr(state.desktop_normalized, 'shadows', {}))
|
| 1505 |
+
tobe_shadow_count = max(shadow_count, 5) if shadow_count > 0 else 0 # Always 5 levels (interpolated)
|
|
|
|
|
|
|
| 1506 |
cards.append(_render_as_is_to_be(
|
| 1507 |
+
"Shadows", f"{shadow_count} extracted",
|
| 1508 |
"Elevation tokens" if shadow_count > 0 else "No shadows found",
|
| 1509 |
f"{tobe_shadow_count} levels",
|
| 1510 |
+
"xs → sm → md → lg → xl" + (f" (interpolated from {shadow_count})" if shadow_count < 5 else ""),
|
| 1511 |
icon="🌫️"
|
| 1512 |
))
|
| 1513 |
asis_tobe_html = "".join(cards)
|
|
|
|
| 3603 |
token_count += 1
|
| 3604 |
|
| 3605 |
# =========================================================================
|
| 3606 |
+
# SHADOWS — W3C DTCG format — always produce 5 elevation levels (xs→xl)
|
| 3607 |
+
# Interpolates between extracted shadows to fill missing levels.
|
| 3608 |
# =========================================================================
|
| 3609 |
+
TARGET_SHADOW_COUNT = 5
|
| 3610 |
+
shadow_names = ["shadow.xs", "shadow.sm", "shadow.md", "shadow.lg", "shadow.xl"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3611 |
|
| 3612 |
if state.desktop_normalized and state.desktop_normalized.shadows:
|
| 3613 |
sorted_shadows = sorted(
|
|
|
|
| 3627 |
"color": p.get("color", "rgba(0,0,0,0.25)"),
|
| 3628 |
})
|
| 3629 |
|
| 3630 |
+
# Interpolation helpers
|
| 3631 |
+
def _lerp(a, b, t):
|
| 3632 |
+
return a + (b - a) * t
|
| 3633 |
+
|
| 3634 |
+
def _lerp_shadow(s1, s2, t):
|
| 3635 |
+
"""Interpolate between two shadow dicts at factor t (0.0=s1, 1.0=s2)."""
|
| 3636 |
+
import re
|
| 3637 |
+
interp = {
|
| 3638 |
+
"x": round(_lerp(s1["x"], s2["x"], t), 1),
|
| 3639 |
+
"y": round(_lerp(s1["y"], s2["y"], t), 1),
|
| 3640 |
+
"blur": round(_lerp(s1["blur"], s2["blur"], t), 1),
|
| 3641 |
+
"spread": round(_lerp(s1["spread"], s2["spread"], t), 1),
|
| 3642 |
+
}
|
| 3643 |
+
alpha1, alpha2 = 0.25, 0.25
|
| 3644 |
+
m1 = re.search(r'rgba?\([^)]*,\s*([\d.]+)\)', s1["color"])
|
| 3645 |
+
m2 = re.search(r'rgba?\([^)]*,\s*([\d.]+)\)', s2["color"])
|
| 3646 |
+
if m1: alpha1 = float(m1.group(1))
|
| 3647 |
+
if m2: alpha2 = float(m2.group(1))
|
| 3648 |
+
interp_alpha = round(_lerp(alpha1, alpha2, t), 3)
|
| 3649 |
+
interp["color"] = f"rgba(0, 0, 0, {interp_alpha})"
|
| 3650 |
+
return interp
|
| 3651 |
+
|
| 3652 |
+
final_shadows = []
|
| 3653 |
n = len(parsed_shadows)
|
| 3654 |
+
if n >= TARGET_SHADOW_COUNT:
|
| 3655 |
+
final_shadows = parsed_shadows[:TARGET_SHADOW_COUNT]
|
| 3656 |
+
elif n == 1:
|
| 3657 |
+
base = parsed_shadows[0]
|
| 3658 |
+
for i in range(TARGET_SHADOW_COUNT):
|
| 3659 |
+
factor = (i + 1) / 3.0
|
| 3660 |
+
final_shadows.append({
|
| 3661 |
+
"x": round(base["x"] * factor, 1),
|
| 3662 |
+
"y": round(max(1, base["y"] * factor), 1),
|
| 3663 |
+
"blur": round(max(1, base["blur"] * factor), 1),
|
| 3664 |
+
"spread": round(base["spread"] * factor, 1),
|
| 3665 |
+
"color": f"rgba(0, 0, 0, {round(0.04 + i * 0.04, 3)})",
|
| 3666 |
+
})
|
| 3667 |
+
elif n >= 2:
|
| 3668 |
+
for i in range(TARGET_SHADOW_COUNT):
|
| 3669 |
+
t = i / (TARGET_SHADOW_COUNT - 1)
|
| 3670 |
+
src_pos = t * (n - 1)
|
| 3671 |
+
lo = int(src_pos)
|
| 3672 |
+
hi = min(lo + 1, n - 1)
|
| 3673 |
+
frac = src_pos - lo
|
| 3674 |
+
final_shadows.append(_lerp_shadow(parsed_shadows[lo], parsed_shadows[hi], frac))
|
| 3675 |
|
| 3676 |
for i, shadow in enumerate(final_shadows):
|
| 3677 |
+
token_name = shadow_names[i] if i < len(shadow_names) else f"shadow.{i + 1}"
|
| 3678 |
dtcg_value = {
|
| 3679 |
"color": shadow["color"],
|
| 3680 |
"offsetX": f"{shadow['x']}px",
|
|
@@ -782,8 +782,8 @@ figma.ui.onmessage = async function(msg) {
|
|
| 782 |
}
|
| 783 |
|
| 784 |
// ==========================================
|
| 785 |
-
// VISUAL SPEC GENERATOR
|
| 786 |
-
// Creates a visual reference page showing all tokens
|
| 787 |
// ==========================================
|
| 788 |
if (msg.type === 'create-visual-spec') {
|
| 789 |
try {
|
|
@@ -802,234 +802,438 @@ figma.ui.onmessage = async function(msg) {
|
|
| 802 |
var tokens = normalizeTokens(rawTokens);
|
| 803 |
console.log('Normalized tokens - colors:', tokens.colors.length, 'typography:', tokens.typography.length);
|
| 804 |
|
| 805 |
-
// Use current page
|
| 806 |
var specPage = figma.currentPage;
|
| 807 |
specPage.name = '🎨 Design System Spec';
|
| 808 |
|
| 809 |
-
// Clear existing children
|
| 810 |
while (specPage.children.length > 0) {
|
| 811 |
specPage.children[0].remove();
|
| 812 |
}
|
| 813 |
|
| 814 |
-
|
| 815 |
-
var
|
| 816 |
-
var
|
| 817 |
-
var
|
|
|
|
|
|
|
|
|
|
| 818 |
|
| 819 |
-
// Load
|
|
|
|
| 820 |
var headingStyle = 'Regular';
|
| 821 |
-
|
|
|
|
| 822 |
try {
|
| 823 |
await figma.loadFontAsync({ family: 'Inter', style: 'Bold' });
|
| 824 |
headingStyle = 'Bold';
|
| 825 |
} catch (e) {
|
| 826 |
-
console.warn('Inter Bold not available
|
| 827 |
}
|
| 828 |
-
|
| 829 |
-
|
| 830 |
-
|
| 831 |
-
|
| 832 |
-
|
| 833 |
-
|
| 834 |
-
|
| 835 |
-
|
| 836 |
-
|
| 837 |
-
|
| 838 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 839 |
yOffset += 50;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 840 |
|
| 841 |
-
|
| 842 |
-
|
| 843 |
-
var
|
| 844 |
-
|
| 845 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 846 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 847 |
for (var ci = 0; ci < tokens.colors.length; ci++) {
|
| 848 |
-
var
|
| 849 |
-
var
|
| 850 |
-
var
|
| 851 |
-
|
| 852 |
-
|
| 853 |
-
|
| 854 |
-
|
| 855 |
-
|
| 856 |
-
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
|
| 860 |
-
|
| 861 |
-
|
| 862 |
-
|
| 863 |
-
|
| 864 |
-
|
| 865 |
-
|
| 866 |
-
|
| 867 |
-
|
| 868 |
-
|
| 869 |
-
|
| 870 |
-
colorLabel.x = swatch.x;
|
| 871 |
-
colorLabel.y = swatch.y + swatchSize + 4;
|
| 872 |
-
specPage.appendChild(colorLabel);
|
| 873 |
-
|
| 874 |
-
// Color value label
|
| 875 |
-
var valueLabel = figma.createText();
|
| 876 |
-
valueLabel.characters = colorToken.value.toUpperCase();
|
| 877 |
-
valueLabel.fontName = { family: 'Inter', style: 'Regular' };
|
| 878 |
-
valueLabel.fontSize = 9;
|
| 879 |
-
valueLabel.fills = [{ type: 'SOLID', color: { r: 0.5, g: 0.5, b: 0.5 } }];
|
| 880 |
-
valueLabel.x = swatch.x;
|
| 881 |
-
valueLabel.y = swatch.y + swatchSize + 16;
|
| 882 |
-
specPage.appendChild(valueLabel);
|
| 883 |
}
|
| 884 |
|
| 885 |
-
var
|
| 886 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 887 |
}
|
| 888 |
|
| 889 |
-
//
|
| 890 |
-
//
|
| 891 |
-
//
|
| 892 |
-
// and would crash the entire spec generation. Font details are shown as labels.
|
| 893 |
if (tokens.typography.length > 0) {
|
| 894 |
-
|
| 895 |
-
typoTitle.characters = 'TYPOGRAPHY';
|
| 896 |
-
typoTitle.fontName = { family: 'Inter', style: headingStyle };
|
| 897 |
-
typoTitle.fontSize = 24;
|
| 898 |
-
typoTitle.x = xOffset;
|
| 899 |
-
typoTitle.y = yOffset;
|
| 900 |
-
specPage.appendChild(typoTitle);
|
| 901 |
-
yOffset += 50;
|
| 902 |
|
|
|
|
|
|
|
|
|
|
| 903 |
for (var ti = 0; ti < tokens.typography.length; ti++) {
|
| 904 |
-
var
|
| 905 |
-
var
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 906 |
|
| 907 |
-
|
| 908 |
-
|
| 909 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 910 |
}
|
| 911 |
-
|
| 912 |
-
var fontWeight = value.fontWeight || '400';
|
| 913 |
-
var displaySize = Math.min(fontSize, 48); // Cap display size
|
| 914 |
-
|
| 915 |
-
// Sample text — always use Inter (safe), size shows relative scale
|
| 916 |
-
var sampleText = figma.createText();
|
| 917 |
-
sampleText.fontName = { family: 'Inter', style: headingStyle };
|
| 918 |
-
sampleText.fontSize = displaySize;
|
| 919 |
-
sampleText.characters = typoToken.name.split('/').pop() || 'Sample Text';
|
| 920 |
-
sampleText.x = xOffset;
|
| 921 |
-
sampleText.y = yOffset;
|
| 922 |
-
specPage.appendChild(sampleText);
|
| 923 |
-
|
| 924 |
-
// Specs label — show the ACTUAL font specs as readable text
|
| 925 |
-
var specsLabel = figma.createText();
|
| 926 |
-
specsLabel.fontName = { family: 'Inter', style: 'Regular' };
|
| 927 |
-
specsLabel.fontSize = 11;
|
| 928 |
-
specsLabel.characters = fontFamily + ' · ' + fontSize + 'px · wt ' + fontWeight;
|
| 929 |
-
specsLabel.fills = [{ type: 'SOLID', color: { r: 0.5, g: 0.5, b: 0.5 } }];
|
| 930 |
-
specsLabel.x = xOffset + 300;
|
| 931 |
-
specsLabel.y = yOffset + 4;
|
| 932 |
-
specPage.appendChild(specsLabel);
|
| 933 |
-
|
| 934 |
-
yOffset += Math.max(displaySize, 24) + itemGap;
|
| 935 |
}
|
| 936 |
|
| 937 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 938 |
}
|
| 939 |
|
| 940 |
-
//
|
|
|
|
|
|
|
| 941 |
if (tokens.spacing.length > 0) {
|
| 942 |
-
|
| 943 |
-
|
| 944 |
-
|
| 945 |
-
|
| 946 |
-
|
| 947 |
-
|
| 948 |
-
|
| 949 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 950 |
|
| 951 |
-
|
| 952 |
-
|
| 953 |
-
var
|
| 954 |
-
|
| 955 |
-
|
| 956 |
-
|
| 957 |
-
|
| 958 |
-
|
| 959 |
-
|
| 960 |
-
|
| 961 |
-
|
| 962 |
-
|
| 963 |
-
|
| 964 |
-
|
| 965 |
-
|
| 966 |
-
|
| 967 |
-
|
| 968 |
-
|
| 969 |
-
|
| 970 |
-
|
| 971 |
-
|
| 972 |
-
|
| 973 |
-
|
| 974 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 975 |
}
|
| 976 |
|
| 977 |
-
|
|
|
|
|
|
|
|
|
|
| 978 |
}
|
| 979 |
|
| 980 |
-
//
|
|
|
|
|
|
|
| 981 |
if (tokens.borderRadius.length > 0) {
|
| 982 |
-
|
| 983 |
-
radiusTitle.characters = 'BORDER RADIUS';
|
| 984 |
-
radiusTitle.fontName = { family: 'Inter', style: headingStyle };
|
| 985 |
-
radiusTitle.fontSize = 24;
|
| 986 |
-
radiusTitle.x = xOffset;
|
| 987 |
-
radiusTitle.y = yOffset;
|
| 988 |
-
specPage.appendChild(radiusTitle);
|
| 989 |
-
yOffset += 50;
|
| 990 |
|
| 991 |
var radiusX = xOffset;
|
| 992 |
for (var ri = 0; ri < tokens.borderRadius.length; ri++) {
|
| 993 |
var radiusToken = tokens.borderRadius[ri];
|
| 994 |
var radiusValue = parseNumericValue(radiusToken.value);
|
| 995 |
|
| 996 |
-
// Rounded rectangle
|
| 997 |
var rect = figma.createRectangle();
|
| 998 |
-
rect.resize(
|
| 999 |
rect.x = radiusX;
|
| 1000 |
rect.y = yOffset;
|
| 1001 |
-
rect.cornerRadius = Math.min(radiusValue,
|
| 1002 |
-
rect.fills = [{ type: 'SOLID', color: { r: 0.95, g: 0.95, b: 0.
|
| 1003 |
-
rect.strokes = [{ type: 'SOLID', color: { r: 0.8, g: 0.8, b: 0.
|
| 1004 |
rect.strokeWeight = 2;
|
| 1005 |
specPage.appendChild(rect);
|
| 1006 |
|
| 1007 |
-
// Label
|
| 1008 |
var rLabel = figma.createText();
|
|
|
|
|
|
|
| 1009 |
rLabel.characters = radiusToken.name.split('/').pop() + '\n' + radiusToken.value;
|
| 1010 |
-
rLabel.fontName = { family: 'Inter', style: 'Regular' };
|
| 1011 |
-
rLabel.fontSize = 10;
|
| 1012 |
rLabel.textAlignHorizontal = 'CENTER';
|
| 1013 |
rLabel.x = radiusX;
|
| 1014 |
-
rLabel.y = yOffset +
|
| 1015 |
specPage.appendChild(rLabel);
|
| 1016 |
|
| 1017 |
-
radiusX +=
|
| 1018 |
}
|
| 1019 |
|
| 1020 |
-
yOffset +=
|
| 1021 |
}
|
| 1022 |
|
| 1023 |
-
//
|
|
|
|
|
|
|
| 1024 |
if (tokens.shadows.length > 0) {
|
| 1025 |
-
|
| 1026 |
-
shadowTitle.characters = 'SHADOWS';
|
| 1027 |
-
shadowTitle.fontName = { family: 'Inter', style: headingStyle };
|
| 1028 |
-
shadowTitle.fontSize = 24;
|
| 1029 |
-
shadowTitle.x = xOffset;
|
| 1030 |
-
shadowTitle.y = yOffset;
|
| 1031 |
-
specPage.appendChild(shadowTitle);
|
| 1032 |
-
yOffset += 50;
|
| 1033 |
|
| 1034 |
var shadowX = xOffset;
|
| 1035 |
for (var shi = 0; shi < tokens.shadows.length; shi++) {
|
|
@@ -1044,10 +1248,10 @@ figma.ui.onmessage = async function(msg) {
|
|
| 1044 |
|
| 1045 |
// Shadow card
|
| 1046 |
var card = figma.createRectangle();
|
| 1047 |
-
card.resize(
|
| 1048 |
card.x = shadowX;
|
| 1049 |
card.y = yOffset;
|
| 1050 |
-
card.cornerRadius =
|
| 1051 |
card.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }];
|
| 1052 |
card.effects = [{
|
| 1053 |
type: 'DROP_SHADOW',
|
|
@@ -1060,17 +1264,38 @@ figma.ui.onmessage = async function(msg) {
|
|
| 1060 |
}];
|
| 1061 |
specPage.appendChild(card);
|
| 1062 |
|
| 1063 |
-
//
|
| 1064 |
var shLabel = figma.createText();
|
| 1065 |
-
shLabel.
|
| 1066 |
-
shLabel.
|
| 1067 |
-
shLabel.
|
|
|
|
| 1068 |
shLabel.x = shadowX;
|
| 1069 |
-
shLabel.y = yOffset +
|
| 1070 |
specPage.appendChild(shLabel);
|
| 1071 |
|
| 1072 |
-
shadowX +=
|
| 1073 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1074 |
}
|
| 1075 |
|
| 1076 |
figma.ui.postMessage({
|
|
|
|
| 782 |
}
|
| 783 |
|
| 784 |
// ==========================================
|
| 785 |
+
// VISUAL SPEC GENERATOR v2
|
| 786 |
+
// Creates a professional visual reference page showing all tokens
|
| 787 |
// ==========================================
|
| 788 |
if (msg.type === 'create-visual-spec') {
|
| 789 |
try {
|
|
|
|
| 802 |
var tokens = normalizeTokens(rawTokens);
|
| 803 |
console.log('Normalized tokens - colors:', tokens.colors.length, 'typography:', tokens.typography.length);
|
| 804 |
|
| 805 |
+
// Use current page (Figma Starter plan has 3-page limit)
|
| 806 |
var specPage = figma.currentPage;
|
| 807 |
specPage.name = '🎨 Design System Spec';
|
| 808 |
|
| 809 |
+
// Clear existing children
|
| 810 |
while (specPage.children.length > 0) {
|
| 811 |
specPage.children[0].remove();
|
| 812 |
}
|
| 813 |
|
| 814 |
+
// ── Layout constants ──
|
| 815 |
+
var PAGE_PADDING = 60;
|
| 816 |
+
var SECTION_GAP = 100;
|
| 817 |
+
var ITEM_GAP = 16;
|
| 818 |
+
var xOffset = PAGE_PADDING;
|
| 819 |
+
var yOffset = PAGE_PADDING;
|
| 820 |
+
var PAGE_WIDTH = 1200;
|
| 821 |
|
| 822 |
+
// ── Load fonts ──
|
| 823 |
+
// 1. Inter Regular/Bold for labels (always available)
|
| 824 |
var headingStyle = 'Regular';
|
| 825 |
+
var labelFont = { family: 'Inter', style: 'Regular' };
|
| 826 |
+
await figma.loadFontAsync(labelFont);
|
| 827 |
try {
|
| 828 |
await figma.loadFontAsync({ family: 'Inter', style: 'Bold' });
|
| 829 |
headingStyle = 'Bold';
|
| 830 |
} catch (e) {
|
| 831 |
+
console.warn('Inter Bold not available');
|
| 832 |
}
|
| 833 |
+
var headingFont = { family: 'Inter', style: headingStyle };
|
| 834 |
+
|
| 835 |
+
// 2. Try to load the extracted font for sample text
|
| 836 |
+
var sampleFontFamily = 'Inter';
|
| 837 |
+
var sampleFontStyle = 'Regular';
|
| 838 |
+
var sampleFontBoldStyle = headingStyle;
|
| 839 |
+
// Find the primary font from typography tokens
|
| 840 |
+
for (var fi = 0; fi < tokens.typography.length; fi++) {
|
| 841 |
+
var ff = (tokens.typography[fi].value.fontFamily || '').split(',')[0].trim();
|
| 842 |
+
if (ff && ff !== 'Inter' && ff !== 'sans-serif') {
|
| 843 |
+
sampleFontFamily = ff;
|
| 844 |
+
break;
|
| 845 |
+
}
|
| 846 |
+
}
|
| 847 |
+
if (sampleFontFamily !== 'Inter') {
|
| 848 |
+
try {
|
| 849 |
+
await figma.loadFontAsync({ family: sampleFontFamily, style: 'Regular' });
|
| 850 |
+
sampleFontStyle = 'Regular';
|
| 851 |
+
// Try Bold
|
| 852 |
+
try {
|
| 853 |
+
await figma.loadFontAsync({ family: sampleFontFamily, style: 'Bold' });
|
| 854 |
+
sampleFontBoldStyle = 'Bold';
|
| 855 |
+
} catch (e2) {
|
| 856 |
+
sampleFontBoldStyle = 'Regular';
|
| 857 |
+
}
|
| 858 |
+
} catch (e) {
|
| 859 |
+
console.warn(sampleFontFamily + ' not available, falling back to Inter');
|
| 860 |
+
sampleFontFamily = 'Inter';
|
| 861 |
+
sampleFontStyle = 'Regular';
|
| 862 |
+
sampleFontBoldStyle = headingStyle;
|
| 863 |
+
}
|
| 864 |
+
}
|
| 865 |
+
var sampleFont = { family: sampleFontFamily, style: sampleFontStyle };
|
| 866 |
+
var sampleFontBold = { family: sampleFontFamily, style: sampleFontBoldStyle };
|
| 867 |
+
|
| 868 |
+
// ── Helper: add section title ──
|
| 869 |
+
function addSectionTitle(text) {
|
| 870 |
+
var title = figma.createText();
|
| 871 |
+
title.fontName = headingFont;
|
| 872 |
+
title.fontSize = 28;
|
| 873 |
+
title.characters = text;
|
| 874 |
+
title.fills = [{ type: 'SOLID', color: { r: 0.1, g: 0.1, b: 0.1 } }];
|
| 875 |
+
title.x = xOffset;
|
| 876 |
+
title.y = yOffset;
|
| 877 |
+
specPage.appendChild(title);
|
| 878 |
yOffset += 50;
|
| 879 |
+
// Divider line
|
| 880 |
+
var line = figma.createRectangle();
|
| 881 |
+
line.resize(PAGE_WIDTH - PAGE_PADDING * 2, 1);
|
| 882 |
+
line.x = xOffset;
|
| 883 |
+
line.y = yOffset;
|
| 884 |
+
line.fills = [{ type: 'SOLID', color: { r: 0.85, g: 0.85, b: 0.85 } }];
|
| 885 |
+
specPage.appendChild(line);
|
| 886 |
+
yOffset += 24;
|
| 887 |
+
}
|
| 888 |
|
| 889 |
+
// ── Helper: add sub-heading ──
|
| 890 |
+
function addSubHeading(text) {
|
| 891 |
+
var sub = figma.createText();
|
| 892 |
+
sub.fontName = headingFont;
|
| 893 |
+
sub.fontSize = 16;
|
| 894 |
+
sub.characters = text;
|
| 895 |
+
sub.fills = [{ type: 'SOLID', color: { r: 0.35, g: 0.35, b: 0.35 } }];
|
| 896 |
+
sub.x = xOffset;
|
| 897 |
+
sub.y = yOffset;
|
| 898 |
+
specPage.appendChild(sub);
|
| 899 |
+
yOffset += 32;
|
| 900 |
+
}
|
| 901 |
+
|
| 902 |
+
// ══════════════════════════════════════════
|
| 903 |
+
// 1. WHITE BACKGROUND
|
| 904 |
+
// ══════════════════════════════════════════
|
| 905 |
+
// We'll place it at the end once we know the total height.
|
| 906 |
+
// For now, track startY.
|
| 907 |
+
var bgStartY = 0;
|
| 908 |
+
|
| 909 |
+
// ── Page title ──
|
| 910 |
+
var pageTitle = figma.createText();
|
| 911 |
+
pageTitle.fontName = headingFont;
|
| 912 |
+
pageTitle.fontSize = 36;
|
| 913 |
+
pageTitle.characters = 'Design System Specification';
|
| 914 |
+
pageTitle.fills = [{ type: 'SOLID', color: { r: 0.1, g: 0.1, b: 0.1 } }];
|
| 915 |
+
pageTitle.x = xOffset;
|
| 916 |
+
pageTitle.y = yOffset;
|
| 917 |
+
specPage.appendChild(pageTitle);
|
| 918 |
+
yOffset += 60;
|
| 919 |
+
|
| 920 |
+
// ══════════════════════════════════════════
|
| 921 |
+
// 2. COLORS — grouped by category
|
| 922 |
+
// ══════════════════════════════════════════
|
| 923 |
+
if (tokens.colors.length > 0) {
|
| 924 |
+
addSectionTitle('COLORS');
|
| 925 |
|
| 926 |
+
// Group colors by top-level category from name path
|
| 927 |
+
// e.g. "brand/primary/DEFAULT" → category "brand"
|
| 928 |
+
// "blue/50" → category "blue" (palette)
|
| 929 |
+
var colorGroups = {};
|
| 930 |
+
var groupOrder = [];
|
| 931 |
for (var ci = 0; ci < tokens.colors.length; ci++) {
|
| 932 |
+
var cToken = tokens.colors[ci];
|
| 933 |
+
var parts = cToken.name.split('/');
|
| 934 |
+
var category = parts[0] || 'other';
|
| 935 |
+
if (!colorGroups[category]) {
|
| 936 |
+
colorGroups[category] = [];
|
| 937 |
+
groupOrder.push(category);
|
| 938 |
+
}
|
| 939 |
+
colorGroups[category].push(cToken);
|
| 940 |
+
}
|
| 941 |
+
|
| 942 |
+
// Semantic categories first, then palette
|
| 943 |
+
var semanticOrder = ['brand', 'text', 'bg', 'background', 'border', 'feedback'];
|
| 944 |
+
var sortedGroups = [];
|
| 945 |
+
for (var so = 0; so < semanticOrder.length; so++) {
|
| 946 |
+
if (colorGroups[semanticOrder[so]]) {
|
| 947 |
+
sortedGroups.push(semanticOrder[so]);
|
| 948 |
+
}
|
| 949 |
+
}
|
| 950 |
+
for (var go = 0; go < groupOrder.length; go++) {
|
| 951 |
+
if (sortedGroups.indexOf(groupOrder[go]) === -1) {
|
| 952 |
+
sortedGroups.push(groupOrder[go]);
|
| 953 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 954 |
}
|
| 955 |
|
| 956 |
+
var swatchSize = 56;
|
| 957 |
+
var swatchGap = 12;
|
| 958 |
+
|
| 959 |
+
for (var gi = 0; gi < sortedGroups.length; gi++) {
|
| 960 |
+
var groupName = sortedGroups[gi];
|
| 961 |
+
var groupColors = colorGroups[groupName];
|
| 962 |
+
|
| 963 |
+
// Category label
|
| 964 |
+
addSubHeading(groupName.charAt(0).toUpperCase() + groupName.slice(1));
|
| 965 |
+
|
| 966 |
+
var colorX = xOffset;
|
| 967 |
+
for (var ci2 = 0; ci2 < groupColors.length; ci2++) {
|
| 968 |
+
var ct = groupColors[ci2];
|
| 969 |
+
|
| 970 |
+
// Swatch
|
| 971 |
+
var swatch = figma.createRectangle();
|
| 972 |
+
swatch.resize(swatchSize, swatchSize);
|
| 973 |
+
swatch.x = colorX;
|
| 974 |
+
swatch.y = yOffset;
|
| 975 |
+
swatch.cornerRadius = 8;
|
| 976 |
+
var rgb = hexToRgb(ct.value);
|
| 977 |
+
swatch.fills = [{ type: 'SOLID', color: rgb }];
|
| 978 |
+
swatch.strokes = [{ type: 'SOLID', color: { r: 0.88, g: 0.88, b: 0.88 } }];
|
| 979 |
+
swatch.strokeWeight = 1;
|
| 980 |
+
specPage.appendChild(swatch);
|
| 981 |
+
|
| 982 |
+
// Token name (last segment, skip DEFAULT)
|
| 983 |
+
var nameParts = ct.name.split('/');
|
| 984 |
+
var displayName = nameParts[nameParts.length - 1];
|
| 985 |
+
if (displayName === 'DEFAULT') {
|
| 986 |
+
displayName = nameParts.length > 1 ? nameParts[nameParts.length - 2] : displayName;
|
| 987 |
+
}
|
| 988 |
+
var cLabel = figma.createText();
|
| 989 |
+
cLabel.fontName = labelFont;
|
| 990 |
+
cLabel.fontSize = 9;
|
| 991 |
+
cLabel.characters = displayName;
|
| 992 |
+
cLabel.x = colorX;
|
| 993 |
+
cLabel.y = yOffset + swatchSize + 4;
|
| 994 |
+
specPage.appendChild(cLabel);
|
| 995 |
+
|
| 996 |
+
// Hex value
|
| 997 |
+
var hexLabel = figma.createText();
|
| 998 |
+
hexLabel.fontName = labelFont;
|
| 999 |
+
hexLabel.fontSize = 8;
|
| 1000 |
+
hexLabel.characters = ct.value.toUpperCase();
|
| 1001 |
+
hexLabel.fills = [{ type: 'SOLID', color: { r: 0.55, g: 0.55, b: 0.55 } }];
|
| 1002 |
+
hexLabel.x = colorX;
|
| 1003 |
+
hexLabel.y = yOffset + swatchSize + 16;
|
| 1004 |
+
specPage.appendChild(hexLabel);
|
| 1005 |
+
|
| 1006 |
+
colorX += swatchSize + swatchGap;
|
| 1007 |
+
}
|
| 1008 |
+
|
| 1009 |
+
yOffset += swatchSize + 36 + ITEM_GAP;
|
| 1010 |
+
}
|
| 1011 |
+
|
| 1012 |
+
yOffset += SECTION_GAP - ITEM_GAP;
|
| 1013 |
}
|
| 1014 |
|
| 1015 |
+
// ══════════════════════════════════════════
|
| 1016 |
+
// 3. TYPOGRAPHY — Desktop & Mobile side by side
|
| 1017 |
+
// ══════════════════════════════════════════
|
|
|
|
| 1018 |
if (tokens.typography.length > 0) {
|
| 1019 |
+
addSectionTitle('TYPOGRAPHY');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1020 |
|
| 1021 |
+
// Separate desktop and mobile tokens
|
| 1022 |
+
var desktopTypo = [];
|
| 1023 |
+
var mobileTypo = [];
|
| 1024 |
for (var ti = 0; ti < tokens.typography.length; ti++) {
|
| 1025 |
+
var tToken = tokens.typography[ti];
|
| 1026 |
+
var tName = tToken.name.toLowerCase();
|
| 1027 |
+
if (tName.indexOf('mobile') > -1) {
|
| 1028 |
+
mobileTypo.push(tToken);
|
| 1029 |
+
} else {
|
| 1030 |
+
desktopTypo.push(tToken);
|
| 1031 |
+
}
|
| 1032 |
+
}
|
| 1033 |
|
| 1034 |
+
// Pangram sample texts per tier
|
| 1035 |
+
function getSampleText(tokenName) {
|
| 1036 |
+
var n = tokenName.toLowerCase();
|
| 1037 |
+
if (n.indexOf('display') > -1) return 'The quick brown fox jumps over the lazy dog';
|
| 1038 |
+
if (n.indexOf('heading') > -1) return 'The quick brown fox jumps over';
|
| 1039 |
+
if (n.indexOf('body') > -1) return 'The quick brown fox jumps over the lazy dog. Pack my box with five dozen liquor jugs.';
|
| 1040 |
+
if (n.indexOf('caption') > -1) return 'The quick brown fox jumps over the lazy dog';
|
| 1041 |
+
if (n.indexOf('overline') > -1) return 'THE QUICK BROWN FOX';
|
| 1042 |
+
return 'The quick brown fox jumps over the lazy dog';
|
| 1043 |
+
}
|
| 1044 |
+
|
| 1045 |
+
// Get display tier for font weight
|
| 1046 |
+
function getTierFont(tokenName) {
|
| 1047 |
+
var n = tokenName.toLowerCase();
|
| 1048 |
+
if (n.indexOf('display') > -1 || n.indexOf('heading') > -1) return sampleFontBold;
|
| 1049 |
+
return sampleFont;
|
| 1050 |
+
}
|
| 1051 |
+
|
| 1052 |
+
// Render a typography column
|
| 1053 |
+
function renderTypoColumn(typoList, columnX, columnTitle) {
|
| 1054 |
+
var localY = yOffset;
|
| 1055 |
+
// Column title
|
| 1056 |
+
var colTitle = figma.createText();
|
| 1057 |
+
colTitle.fontName = headingFont;
|
| 1058 |
+
colTitle.fontSize = 14;
|
| 1059 |
+
colTitle.characters = columnTitle;
|
| 1060 |
+
colTitle.fills = [{ type: 'SOLID', color: { r: 0.2, g: 0.5, b: 1 } }];
|
| 1061 |
+
colTitle.x = columnX;
|
| 1062 |
+
colTitle.y = localY;
|
| 1063 |
+
specPage.appendChild(colTitle);
|
| 1064 |
+
localY += 30;
|
| 1065 |
+
|
| 1066 |
+
for (var tti = 0; tti < typoList.length; tti++) {
|
| 1067 |
+
var tt = typoList[tti];
|
| 1068 |
+
var val = tt.value;
|
| 1069 |
+
var fFamily = (val.fontFamily || 'Inter').split(',')[0].trim();
|
| 1070 |
+
var fSize = parseNumericValue(val.fontSize) || 16;
|
| 1071 |
+
var fWeight = val.fontWeight || '400';
|
| 1072 |
+
var fLineHeight = val.lineHeight || '1.5';
|
| 1073 |
+
var displaySize = Math.min(fSize, 48);
|
| 1074 |
+
|
| 1075 |
+
// Token name label (e.g. "heading.lg")
|
| 1076 |
+
var tierParts = tt.name.split('/');
|
| 1077 |
+
// Remove 'desktop'/'mobile' from display
|
| 1078 |
+
var tierName = tierParts.filter(function(p) { return p !== 'desktop' && p !== 'mobile'; }).join('/');
|
| 1079 |
+
|
| 1080 |
+
var nameLabel = figma.createText();
|
| 1081 |
+
nameLabel.fontName = labelFont;
|
| 1082 |
+
nameLabel.fontSize = 10;
|
| 1083 |
+
nameLabel.characters = tierName;
|
| 1084 |
+
nameLabel.fills = [{ type: 'SOLID', color: { r: 0.5, g: 0.5, b: 0.5 } }];
|
| 1085 |
+
nameLabel.x = columnX;
|
| 1086 |
+
nameLabel.y = localY;
|
| 1087 |
+
specPage.appendChild(nameLabel);
|
| 1088 |
+
localY += 16;
|
| 1089 |
+
|
| 1090 |
+
// Sample text in actual font
|
| 1091 |
+
var sample = figma.createText();
|
| 1092 |
+
sample.fontName = getTierFont(tt.name);
|
| 1093 |
+
sample.fontSize = displaySize;
|
| 1094 |
+
sample.characters = getSampleText(tt.name);
|
| 1095 |
+
sample.x = columnX;
|
| 1096 |
+
sample.y = localY;
|
| 1097 |
+
// Constrain width so long text wraps
|
| 1098 |
+
sample.resize(480, sample.height);
|
| 1099 |
+
sample.textAutoResize = 'HEIGHT';
|
| 1100 |
+
specPage.appendChild(sample);
|
| 1101 |
+
localY += sample.height + 6;
|
| 1102 |
+
|
| 1103 |
+
// Spec details — font size, weight, line height
|
| 1104 |
+
var specLine = figma.createText();
|
| 1105 |
+
specLine.fontName = labelFont;
|
| 1106 |
+
specLine.fontSize = 10;
|
| 1107 |
+
specLine.characters = fFamily + ' · ' + fSize + 'px · wt ' + fWeight + ' · LH ' + fLineHeight;
|
| 1108 |
+
specLine.fills = [{ type: 'SOLID', color: { r: 0.6, g: 0.6, b: 0.6 } }];
|
| 1109 |
+
specLine.x = columnX;
|
| 1110 |
+
specLine.y = localY;
|
| 1111 |
+
specPage.appendChild(specLine);
|
| 1112 |
+
localY += 28;
|
| 1113 |
}
|
| 1114 |
+
return localY;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1115 |
}
|
| 1116 |
|
| 1117 |
+
var colWidth = 520;
|
| 1118 |
+
var desktopEndY = renderTypoColumn(desktopTypo, xOffset, '🖥 Desktop');
|
| 1119 |
+
var mobileEndY = renderTypoColumn(mobileTypo, xOffset + colWidth, '📱 Mobile');
|
| 1120 |
+
|
| 1121 |
+
yOffset = Math.max(desktopEndY, mobileEndY) + SECTION_GAP;
|
| 1122 |
}
|
| 1123 |
|
| 1124 |
+
// ══════════════════════════════════════════
|
| 1125 |
+
// 4. SPACING — Desktop & Mobile with square blocks
|
| 1126 |
+
// ══════════════════════════════════════════
|
| 1127 |
if (tokens.spacing.length > 0) {
|
| 1128 |
+
addSectionTitle('SPACING');
|
| 1129 |
+
|
| 1130 |
+
// Separate desktop and mobile
|
| 1131 |
+
var desktopSpacing = [];
|
| 1132 |
+
var mobileSpacing = [];
|
| 1133 |
+
for (var spi = 0; spi < tokens.spacing.length; spi++) {
|
| 1134 |
+
var spToken = tokens.spacing[spi];
|
| 1135 |
+
var spName = spToken.name.toLowerCase();
|
| 1136 |
+
if (spName.indexOf('mobile') > -1) {
|
| 1137 |
+
mobileSpacing.push(spToken);
|
| 1138 |
+
} else {
|
| 1139 |
+
desktopSpacing.push(spToken);
|
| 1140 |
+
}
|
| 1141 |
+
}
|
| 1142 |
|
| 1143 |
+
function renderSpacingColumn(spacingList, columnX, columnTitle) {
|
| 1144 |
+
var localY = yOffset;
|
| 1145 |
+
var colTitle = figma.createText();
|
| 1146 |
+
colTitle.fontName = headingFont;
|
| 1147 |
+
colTitle.fontSize = 14;
|
| 1148 |
+
colTitle.characters = columnTitle;
|
| 1149 |
+
colTitle.fills = [{ type: 'SOLID', color: { r: 0.2, g: 0.5, b: 1 } }];
|
| 1150 |
+
colTitle.x = columnX;
|
| 1151 |
+
colTitle.y = localY;
|
| 1152 |
+
specPage.appendChild(colTitle);
|
| 1153 |
+
localY += 30;
|
| 1154 |
+
|
| 1155 |
+
var spX = columnX;
|
| 1156 |
+
var maxH = 0;
|
| 1157 |
+
for (var ssi = 0; ssi < Math.min(spacingList.length, 10); ssi++) {
|
| 1158 |
+
var sp = spacingList[ssi];
|
| 1159 |
+
var spVal = parseNumericValue(sp.value);
|
| 1160 |
+
var blockSize = Math.max(spVal, 8); // Minimum visible size
|
| 1161 |
+
var displayBlock = Math.min(blockSize, 80); // Cap for display
|
| 1162 |
+
|
| 1163 |
+
// Blue square
|
| 1164 |
+
var sq = figma.createRectangle();
|
| 1165 |
+
sq.resize(displayBlock, displayBlock);
|
| 1166 |
+
sq.x = spX;
|
| 1167 |
+
sq.y = localY;
|
| 1168 |
+
sq.cornerRadius = 4;
|
| 1169 |
+
sq.fills = [{ type: 'SOLID', color: { r: 0.2, g: 0.55, b: 1 } }];
|
| 1170 |
+
specPage.appendChild(sq);
|
| 1171 |
+
|
| 1172 |
+
// Label below
|
| 1173 |
+
var spParts = sp.name.split('/');
|
| 1174 |
+
var spDisplayName = spParts.filter(function(p) { return p !== 'desktop' && p !== 'mobile'; }).join('/');
|
| 1175 |
+
var sqLabel = figma.createText();
|
| 1176 |
+
sqLabel.fontName = labelFont;
|
| 1177 |
+
sqLabel.fontSize = 9;
|
| 1178 |
+
sqLabel.characters = spDisplayName + '\n' + sp.value;
|
| 1179 |
+
sqLabel.textAlignHorizontal = 'CENTER';
|
| 1180 |
+
sqLabel.x = spX;
|
| 1181 |
+
sqLabel.y = localY + displayBlock + 6;
|
| 1182 |
+
specPage.appendChild(sqLabel);
|
| 1183 |
+
|
| 1184 |
+
if (displayBlock > maxH) maxH = displayBlock;
|
| 1185 |
+
spX += displayBlock + 20;
|
| 1186 |
+
}
|
| 1187 |
+
return localY + maxH + 40;
|
| 1188 |
}
|
| 1189 |
|
| 1190 |
+
var dsEndY = renderSpacingColumn(desktopSpacing, xOffset, '🖥 Desktop');
|
| 1191 |
+
var msEndY = renderSpacingColumn(mobileSpacing, xOffset + colWidth, '📱 Mobile');
|
| 1192 |
+
|
| 1193 |
+
yOffset = Math.max(dsEndY, msEndY) + SECTION_GAP;
|
| 1194 |
}
|
| 1195 |
|
| 1196 |
+
// ══════════════════════════════════════════
|
| 1197 |
+
// 5. BORDER RADIUS
|
| 1198 |
+
// ══════════════════════════════════════════
|
| 1199 |
if (tokens.borderRadius.length > 0) {
|
| 1200 |
+
addSectionTitle('BORDER RADIUS');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1201 |
|
| 1202 |
var radiusX = xOffset;
|
| 1203 |
for (var ri = 0; ri < tokens.borderRadius.length; ri++) {
|
| 1204 |
var radiusToken = tokens.borderRadius[ri];
|
| 1205 |
var radiusValue = parseNumericValue(radiusToken.value);
|
| 1206 |
|
|
|
|
| 1207 |
var rect = figma.createRectangle();
|
| 1208 |
+
rect.resize(56, 56);
|
| 1209 |
rect.x = radiusX;
|
| 1210 |
rect.y = yOffset;
|
| 1211 |
+
rect.cornerRadius = Math.min(radiusValue, 28);
|
| 1212 |
+
rect.fills = [{ type: 'SOLID', color: { r: 0.95, g: 0.95, b: 0.97 } }];
|
| 1213 |
+
rect.strokes = [{ type: 'SOLID', color: { r: 0.8, g: 0.8, b: 0.85 } }];
|
| 1214 |
rect.strokeWeight = 2;
|
| 1215 |
specPage.appendChild(rect);
|
| 1216 |
|
|
|
|
| 1217 |
var rLabel = figma.createText();
|
| 1218 |
+
rLabel.fontName = labelFont;
|
| 1219 |
+
rLabel.fontSize = 9;
|
| 1220 |
rLabel.characters = radiusToken.name.split('/').pop() + '\n' + radiusToken.value;
|
|
|
|
|
|
|
| 1221 |
rLabel.textAlignHorizontal = 'CENTER';
|
| 1222 |
rLabel.x = radiusX;
|
| 1223 |
+
rLabel.y = yOffset + 62;
|
| 1224 |
specPage.appendChild(rLabel);
|
| 1225 |
|
| 1226 |
+
radiusX += 80;
|
| 1227 |
}
|
| 1228 |
|
| 1229 |
+
yOffset += 110 + SECTION_GAP;
|
| 1230 |
}
|
| 1231 |
|
| 1232 |
+
// ══════════════════════════════════════════
|
| 1233 |
+
// 6. SHADOWS
|
| 1234 |
+
// ══════════════════════════════════════════
|
| 1235 |
if (tokens.shadows.length > 0) {
|
| 1236 |
+
addSectionTitle('SHADOWS');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1237 |
|
| 1238 |
var shadowX = xOffset;
|
| 1239 |
for (var shi = 0; shi < tokens.shadows.length; shi++) {
|
|
|
|
| 1248 |
|
| 1249 |
// Shadow card
|
| 1250 |
var card = figma.createRectangle();
|
| 1251 |
+
card.resize(90, 90);
|
| 1252 |
card.x = shadowX;
|
| 1253 |
card.y = yOffset;
|
| 1254 |
+
card.cornerRadius = 12;
|
| 1255 |
card.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }];
|
| 1256 |
card.effects = [{
|
| 1257 |
type: 'DROP_SHADOW',
|
|
|
|
| 1264 |
}];
|
| 1265 |
specPage.appendChild(card);
|
| 1266 |
|
| 1267 |
+
// Name + specs
|
| 1268 |
var shLabel = figma.createText();
|
| 1269 |
+
shLabel.fontName = labelFont;
|
| 1270 |
+
shLabel.fontSize = 9;
|
| 1271 |
+
shLabel.characters = shadowToken.name.split('/').pop() + '\nblur: ' + blurVal + 'px y: ' + offsetYVal + 'px';
|
| 1272 |
+
shLabel.fills = [{ type: 'SOLID', color: { r: 0.5, g: 0.5, b: 0.5 } }];
|
| 1273 |
shLabel.x = shadowX;
|
| 1274 |
+
shLabel.y = yOffset + 100;
|
| 1275 |
specPage.appendChild(shLabel);
|
| 1276 |
|
| 1277 |
+
shadowX += 130;
|
| 1278 |
}
|
| 1279 |
+
|
| 1280 |
+
yOffset += 140 + SECTION_GAP;
|
| 1281 |
+
}
|
| 1282 |
+
|
| 1283 |
+
// ══════════════════════════════════════════
|
| 1284 |
+
// WHITE BACKGROUND — placed behind everything
|
| 1285 |
+
// ══════════════════════════════════════════
|
| 1286 |
+
var bgRect = figma.createRectangle();
|
| 1287 |
+
bgRect.resize(PAGE_WIDTH, yOffset + PAGE_PADDING);
|
| 1288 |
+
bgRect.x = 0;
|
| 1289 |
+
bgRect.y = 0;
|
| 1290 |
+
bgRect.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }];
|
| 1291 |
+
bgRect.locked = true;
|
| 1292 |
+
bgRect.name = 'Background';
|
| 1293 |
+
specPage.appendChild(bgRect);
|
| 1294 |
+
|
| 1295 |
+
// Send background to back (behind all other elements)
|
| 1296 |
+
var children = specPage.children;
|
| 1297 |
+
if (children.length > 1) {
|
| 1298 |
+
specPage.insertChild(0, bgRect);
|
| 1299 |
}
|
| 1300 |
|
| 1301 |
figma.ui.postMessage({
|