Spaces:
Running
feat: visual spec v3 — separate frames, AA badges, MD best practices
Browse filesComplete rewrite of visual spec following Material Design / Carbon patterns:
Separate Figma frames for each section:
- Title frame with font name
- Colors frame with category groups + AA contrast badges
- Typography Desktop frame (table layout: token | sample | specs)
- Typography Mobile frame (same layout)
- Spacing frame (desktop/mobile horizontal bars, Carbon-style)
- Border Radius frame
- Shadows frame (light gray background, 140x140 white cards)
Color improvements:
- 160x136px cards (64px swatch + 72px metadata area)
- WCAG AA contrast check on every color (ratio, pass/fail, best-on)
- Green "AA Pass" or red "AA Fail" badges with contrast ratio
- Full token name path visible (not just last segment)
- Grouped: brand → text → bg → border → feedback → palette hues
Typography improvements:
- 3-column table layout: Token Name | Sample Text | Specifications
- Sample text in actual extracted font (e.g. Open Sans)
- Pangram text per tier (display, heading, body, caption, overline)
- Prominent specs: Size, Weight, Line Height, Font — each on own line
- Column headers and row separators for readability
Spacing improvements:
- Horizontal bars (Carbon pattern) instead of squares
- Bar width proportional to value (3x scale, capped at 400px)
- Token name + bar + value label in clean rows
- Desktop and Mobile side by side
Shadow improvements:
- Light gray frame background (#F5F5F5) for shadow visibility
- 140x140px white cards with actual drop shadow applied
- Specs printed on each card (blur, y-offset, spread)
Also: 5-level shadow interpolation restored in app.py
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@@ -40,6 +40,31 @@ function hexToRgb(hex) {
|
|
| 40 |
} : { r: 0, g: 0, b: 0 };
|
| 41 |
}
|
| 42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
// Helper to convert font weight to Figma font style name
|
| 44 |
function getFontStyleFromWeight(weight) {
|
| 45 |
var weightStr = String(weight).toLowerCase();
|
|
@@ -782,526 +807,449 @@ figma.ui.onmessage = async function(msg) {
|
|
| 782 |
}
|
| 783 |
|
| 784 |
// ==========================================
|
| 785 |
-
//
|
| 786 |
-
//
|
|
|
|
| 787 |
// ==========================================
|
| 788 |
if (msg.type === 'create-visual-spec') {
|
| 789 |
try {
|
| 790 |
var rawTokens = msg.tokens;
|
| 791 |
-
|
| 792 |
-
// Validate tokens exist
|
| 793 |
if (!rawTokens) {
|
| 794 |
-
figma.ui.postMessage({
|
| 795 |
-
type: 'error',
|
| 796 |
-
message: 'No tokens provided. Please load a JSON file first.'
|
| 797 |
-
});
|
| 798 |
return;
|
| 799 |
}
|
| 800 |
|
| 801 |
-
console.log('Creating visual spec with tokens:', Object.keys(rawTokens));
|
| 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 |
-
|
| 810 |
-
|
| 811 |
-
|
| 812 |
-
|
| 813 |
-
|
| 814 |
-
|
| 815 |
-
var
|
| 816 |
-
var SECTION_GAP = 100;
|
| 817 |
var ITEM_GAP = 16;
|
| 818 |
-
var
|
| 819 |
-
var
|
| 820 |
-
var
|
|
|
|
| 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 |
-
//
|
| 836 |
var sampleFontFamily = 'Inter';
|
| 837 |
-
var
|
| 838 |
-
var
|
| 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 |
-
|
| 851 |
-
|
| 852 |
-
|
| 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 |
-
|
| 866 |
-
var
|
| 867 |
-
|
| 868 |
-
// ── Helper:
|
| 869 |
-
function
|
| 870 |
-
var
|
| 871 |
-
|
| 872 |
-
|
| 873 |
-
|
| 874 |
-
|
| 875 |
-
|
| 876 |
-
|
| 877 |
-
specPage.appendChild(
|
| 878 |
-
|
| 879 |
-
|
| 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
|
| 890 |
-
function
|
| 891 |
-
var
|
| 892 |
-
|
| 893 |
-
|
| 894 |
-
|
| 895 |
-
|
| 896 |
-
|
| 897 |
-
|
| 898 |
-
|
| 899 |
-
yOffset += 32;
|
| 900 |
}
|
| 901 |
|
| 902 |
-
|
| 903 |
-
|
| 904 |
-
|
| 905 |
-
|
| 906 |
-
//
|
| 907 |
-
|
| 908 |
-
|
| 909 |
-
|
| 910 |
-
|
| 911 |
-
|
| 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 |
-
|
| 927 |
-
|
| 928 |
-
|
|
|
|
|
|
|
| 929 |
var colorGroups = {};
|
| 930 |
var groupOrder = [];
|
| 931 |
for (var ci = 0; ci < tokens.colors.length; ci++) {
|
| 932 |
-
var
|
| 933 |
-
|
| 934 |
-
|
| 935 |
-
if (!colorGroups[category]) {
|
| 936 |
-
colorGroups[category] = [];
|
| 937 |
-
groupOrder.push(category);
|
| 938 |
-
}
|
| 939 |
-
colorGroups[category].push(cToken);
|
| 940 |
}
|
| 941 |
-
|
| 942 |
-
|
| 943 |
-
var semanticOrder = ['brand', 'text', 'bg', 'background', 'border', 'feedback'];
|
| 944 |
var sortedGroups = [];
|
| 945 |
-
for (var so = 0; so <
|
| 946 |
-
|
| 947 |
-
|
| 948 |
-
|
| 949 |
-
|
| 950 |
-
|
| 951 |
-
|
| 952 |
-
|
| 953 |
-
|
| 954 |
}
|
|
|
|
| 955 |
|
| 956 |
-
var
|
| 957 |
-
var
|
|
|
|
|
|
|
|
|
|
|
|
|
| 958 |
|
| 959 |
for (var gi = 0; gi < sortedGroups.length; gi++) {
|
| 960 |
-
var
|
| 961 |
-
var
|
| 962 |
|
| 963 |
-
//
|
| 964 |
-
|
|
|
|
| 965 |
|
| 966 |
-
var
|
| 967 |
-
|
| 968 |
-
var
|
|
|
|
|
|
|
|
|
|
| 969 |
|
| 970 |
-
//
|
| 971 |
var swatch = figma.createRectangle();
|
| 972 |
-
swatch.resize(
|
| 973 |
-
swatch.x =
|
| 974 |
-
|
| 975 |
-
swatch.
|
| 976 |
-
|
| 977 |
-
swatch.fills = [{ type: 'SOLID', color:
|
| 978 |
-
|
| 979 |
-
|
| 980 |
-
|
| 981 |
-
|
| 982 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 983 |
var nameParts = ct.name.split('/');
|
| 984 |
-
var displayName = nameParts
|
| 985 |
-
|
| 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 |
-
|
| 998 |
-
|
| 999 |
-
|
| 1000 |
-
|
| 1001 |
-
|
| 1002 |
-
|
| 1003 |
-
hexLabel.y = yOffset + swatchSize + 16;
|
| 1004 |
-
specPage.appendChild(hexLabel);
|
| 1005 |
-
|
| 1006 |
-
colorX += swatchSize + swatchGap;
|
| 1007 |
}
|
| 1008 |
|
| 1009 |
-
|
|
|
|
| 1010 |
}
|
| 1011 |
-
|
| 1012 |
-
yOffset += SECTION_GAP - ITEM_GAP;
|
| 1013 |
}
|
| 1014 |
|
| 1015 |
-
// ══════════════════════════════════════════
|
| 1016 |
-
//
|
| 1017 |
-
// ══════════════════════════════════════════
|
| 1018 |
-
|
| 1019 |
-
|
| 1020 |
-
|
| 1021 |
-
|
| 1022 |
-
|
| 1023 |
-
|
| 1024 |
-
|
| 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 |
-
|
| 1035 |
-
|
| 1036 |
-
|
| 1037 |
-
|
| 1038 |
-
|
| 1039 |
-
|
| 1040 |
-
|
| 1041 |
-
|
| 1042 |
-
|
| 1043 |
-
}
|
| 1044 |
|
| 1045 |
-
|
| 1046 |
-
|
| 1047 |
-
var n = tokenName.toLowerCase();
|
| 1048 |
-
if (n.indexOf('display') > -1 || n.indexOf('heading') > -1) return sampleFontBold;
|
| 1049 |
-
return sampleFont;
|
| 1050 |
-
}
|
| 1051 |
|
| 1052 |
-
//
|
| 1053 |
-
|
| 1054 |
-
|
| 1055 |
-
|
| 1056 |
-
|
| 1057 |
-
|
| 1058 |
-
|
| 1059 |
-
|
| 1060 |
-
|
| 1061 |
-
|
| 1062 |
-
|
| 1063 |
-
|
| 1064 |
-
|
| 1065 |
-
|
| 1066 |
-
|
| 1067 |
-
|
| 1068 |
-
|
| 1069 |
-
|
| 1070 |
-
|
| 1071 |
-
|
| 1072 |
-
|
| 1073 |
-
|
| 1074 |
-
|
| 1075 |
-
|
| 1076 |
-
|
| 1077 |
-
|
| 1078 |
-
|
| 1079 |
-
|
| 1080 |
-
|
| 1081 |
-
|
| 1082 |
-
|
| 1083 |
-
|
| 1084 |
-
|
| 1085 |
-
|
| 1086 |
-
|
| 1087 |
-
|
| 1088 |
-
|
| 1089 |
-
|
| 1090 |
-
|
| 1091 |
-
|
| 1092 |
-
|
| 1093 |
-
|
| 1094 |
-
|
| 1095 |
-
|
| 1096 |
-
|
| 1097 |
-
|
| 1098 |
-
|
| 1099 |
-
|
| 1100 |
-
|
| 1101 |
-
|
| 1102 |
-
|
| 1103 |
-
|
| 1104 |
-
|
| 1105 |
-
|
| 1106 |
-
|
| 1107 |
-
|
| 1108 |
-
|
| 1109 |
-
|
| 1110 |
-
|
| 1111 |
-
|
| 1112 |
-
|
| 1113 |
-
|
| 1114 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1115 |
}
|
| 1116 |
|
| 1117 |
-
|
| 1118 |
-
|
| 1119 |
-
var mobileEndY = renderTypoColumn(mobileTypo, xOffset + colWidth, '📱 Mobile');
|
| 1120 |
-
|
| 1121 |
-
yOffset = Math.max(desktopEndY, mobileEndY) + SECTION_GAP;
|
| 1122 |
}
|
| 1123 |
|
| 1124 |
-
|
| 1125 |
-
|
| 1126 |
-
// ══════════════════════════════════════════
|
| 1127 |
-
if (tokens.spacing.length > 0) {
|
| 1128 |
-
addSectionTitle('SPACING');
|
| 1129 |
|
| 1130 |
-
|
| 1131 |
-
|
| 1132 |
-
|
|
|
|
|
|
|
|
|
|
| 1133 |
for (var spi = 0; spi < tokens.spacing.length; spi++) {
|
| 1134 |
-
|
| 1135 |
-
|
| 1136 |
-
if (spName.indexOf('mobile') > -1) {
|
| 1137 |
-
mobileSpacing.push(spToken);
|
| 1138 |
} else {
|
| 1139 |
-
|
| 1140 |
}
|
| 1141 |
}
|
| 1142 |
|
| 1143 |
-
|
| 1144 |
-
|
| 1145 |
-
|
| 1146 |
-
|
| 1147 |
-
|
| 1148 |
-
|
| 1149 |
-
|
| 1150 |
-
|
| 1151 |
-
|
| 1152 |
-
|
| 1153 |
-
|
| 1154 |
-
|
| 1155 |
-
var
|
| 1156 |
-
|
| 1157 |
-
for (var
|
| 1158 |
-
var sp =
|
| 1159 |
var spVal = parseNumericValue(sp.value);
|
| 1160 |
-
var
|
| 1161 |
-
|
| 1162 |
-
|
| 1163 |
-
//
|
| 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 |
-
|
| 1176 |
-
|
| 1177 |
-
|
| 1178 |
-
|
| 1179 |
-
|
| 1180 |
-
|
| 1181 |
-
|
| 1182 |
-
|
| 1183 |
-
|
| 1184 |
-
|
| 1185 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1186 |
}
|
| 1187 |
-
return
|
| 1188 |
}
|
| 1189 |
|
| 1190 |
-
var
|
| 1191 |
-
var
|
| 1192 |
|
| 1193 |
-
|
| 1194 |
}
|
| 1195 |
|
| 1196 |
-
// ══════════════════════════════════════════
|
| 1197 |
-
//
|
| 1198 |
-
// ══════════════════════════════════════════
|
| 1199 |
if (tokens.borderRadius.length > 0) {
|
| 1200 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1201 |
|
| 1202 |
-
var
|
| 1203 |
for (var ri = 0; ri < tokens.borderRadius.length; ri++) {
|
| 1204 |
-
var
|
| 1205 |
-
var
|
| 1206 |
|
| 1207 |
var rect = figma.createRectangle();
|
| 1208 |
-
rect.resize(
|
| 1209 |
-
rect.x =
|
| 1210 |
-
rect.
|
| 1211 |
-
rect.
|
| 1212 |
-
rect.
|
| 1213 |
-
rect.strokes = [{ type: 'SOLID', color: { r: 0.8, g: 0.8, b: 0.85 } }];
|
| 1214 |
rect.strokeWeight = 2;
|
| 1215 |
-
|
| 1216 |
-
|
| 1217 |
-
|
| 1218 |
-
|
| 1219 |
-
|
| 1220 |
-
|
| 1221 |
-
rLabel.textAlignHorizontal = 'CENTER';
|
| 1222 |
-
rLabel.x = radiusX;
|
| 1223 |
-
rLabel.y = yOffset + 62;
|
| 1224 |
-
specPage.appendChild(rLabel);
|
| 1225 |
-
|
| 1226 |
-
radiusX += 80;
|
| 1227 |
-
}
|
| 1228 |
|
| 1229 |
-
|
|
|
|
| 1230 |
}
|
| 1231 |
|
| 1232 |
-
// ══════════════════════════════════════════
|
| 1233 |
-
//
|
| 1234 |
-
// ══════════════════════════════════════════
|
| 1235 |
if (tokens.shadows.length > 0) {
|
| 1236 |
-
|
| 1237 |
-
|
| 1238 |
-
|
| 1239 |
-
|
| 1240 |
-
var shadowToken = tokens.shadows[shi];
|
| 1241 |
-
var sv = shadowToken.value;
|
| 1242 |
|
| 1243 |
-
|
| 1244 |
-
|
| 1245 |
-
|
| 1246 |
-
var spreadVal = parseNumericValue(sv.spread || '0');
|
| 1247 |
-
var shColor = parseColorToRGBA(sv.color || 'rgba(0,0,0,0.25)');
|
| 1248 |
|
| 1249 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1250 |
var card = figma.createRectangle();
|
| 1251 |
-
card.resize(
|
| 1252 |
-
card.x =
|
| 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',
|
| 1258 |
-
color: { r:
|
| 1259 |
-
offset: { x:
|
| 1260 |
-
radius:
|
| 1261 |
-
spread:
|
| 1262 |
visible: true,
|
| 1263 |
blendMode: 'NORMAL'
|
| 1264 |
}];
|
| 1265 |
-
|
| 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 |
-
|
| 1281 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1282 |
|
| 1283 |
-
|
| 1284 |
-
|
| 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({
|
| 1302 |
-
type: 'spec-complete',
|
| 1303 |
-
message: 'Visual spec page created!'
|
| 1304 |
-
});
|
| 1305 |
|
| 1306 |
} catch (error) {
|
| 1307 |
console.error('Error creating visual spec:', error);
|
|
|
|
| 40 |
} : { r: 0, g: 0, b: 0 };
|
| 41 |
}
|
| 42 |
|
| 43 |
+
// WCAG 2.1 contrast ratio calculation
|
| 44 |
+
function getRelativeLuminance(rgb) {
|
| 45 |
+
function sRGB(c) { return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); }
|
| 46 |
+
return 0.2126 * sRGB(rgb.r) + 0.7152 * sRGB(rgb.g) + 0.0722 * sRGB(rgb.b);
|
| 47 |
+
}
|
| 48 |
+
function getContrastRatio(hex1, hex2) {
|
| 49 |
+
var l1 = getRelativeLuminance(hexToRgb(hex1));
|
| 50 |
+
var l2 = getRelativeLuminance(hexToRgb(hex2));
|
| 51 |
+
var lighter = Math.max(l1, l2);
|
| 52 |
+
var darker = Math.min(l1, l2);
|
| 53 |
+
return (lighter + 0.05) / (darker + 0.05);
|
| 54 |
+
}
|
| 55 |
+
function getAAResult(hex) {
|
| 56 |
+
var onWhite = getContrastRatio(hex, '#ffffff');
|
| 57 |
+
var onBlack = getContrastRatio(hex, '#000000');
|
| 58 |
+
var bestRatio = Math.max(onWhite, onBlack);
|
| 59 |
+
var bestOn = onWhite >= onBlack ? 'white' : 'black';
|
| 60 |
+
return {
|
| 61 |
+
ratio: Math.round(bestRatio * 10) / 10,
|
| 62 |
+
passAA: bestRatio >= 4.5,
|
| 63 |
+
passAAA: bestRatio >= 7,
|
| 64 |
+
bestOn: bestOn
|
| 65 |
+
};
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
// Helper to convert font weight to Figma font style name
|
| 69 |
function getFontStyleFromWeight(weight) {
|
| 70 |
var weightStr = String(weight).toLowerCase();
|
|
|
|
| 807 |
}
|
| 808 |
|
| 809 |
// ==========================================
|
| 810 |
+
// ==========================================
|
| 811 |
+
// VISUAL SPEC GENERATOR v3 — Material Design best-practices layout
|
| 812 |
+
// Each section in its own Figma FRAME. AA contrast badges. Prominent labels.
|
| 813 |
// ==========================================
|
| 814 |
if (msg.type === 'create-visual-spec') {
|
| 815 |
try {
|
| 816 |
var rawTokens = msg.tokens;
|
|
|
|
|
|
|
| 817 |
if (!rawTokens) {
|
| 818 |
+
figma.ui.postMessage({ type: 'error', message: 'No tokens provided. Please load a JSON file first.' });
|
|
|
|
|
|
|
|
|
|
| 819 |
return;
|
| 820 |
}
|
| 821 |
|
|
|
|
| 822 |
var tokens = normalizeTokens(rawTokens);
|
|
|
|
|
|
|
|
|
|
| 823 |
var specPage = figma.currentPage;
|
| 824 |
specPage.name = '🎨 Design System Spec';
|
| 825 |
+
while (specPage.children.length > 0) { specPage.children[0].remove(); }
|
| 826 |
+
|
| 827 |
+
// ── Constants (Material Design / Carbon inspired) ──
|
| 828 |
+
var FRAME_W = 1440;
|
| 829 |
+
var CONTENT_W = 1200;
|
| 830 |
+
var MARGIN = 120;
|
| 831 |
+
var SECTION_PAD = 48;
|
| 832 |
+
var SECTION_GAP = 40; // gap between frames on the page
|
|
|
|
| 833 |
var ITEM_GAP = 16;
|
| 834 |
+
var SWATCH_W = 160;
|
| 835 |
+
var SWATCH_H_COLOR = 64;
|
| 836 |
+
var SWATCH_META_H = 72;
|
| 837 |
+
var SWATCH_GAP = 12;
|
| 838 |
|
| 839 |
// ── Load fonts ──
|
|
|
|
|
|
|
| 840 |
var labelFont = { family: 'Inter', style: 'Regular' };
|
| 841 |
+
var boldFont = { family: 'Inter', style: 'Regular' };
|
| 842 |
await figma.loadFontAsync(labelFont);
|
| 843 |
+
try { await figma.loadFontAsync({ family: 'Inter', style: 'Bold' }); boldFont = { family: 'Inter', style: 'Bold' }; } catch (e) {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 844 |
|
| 845 |
+
// Try loading extracted font
|
| 846 |
var sampleFontFamily = 'Inter';
|
| 847 |
+
var sampleFont = labelFont;
|
| 848 |
+
var sampleFontBold = boldFont;
|
|
|
|
| 849 |
for (var fi = 0; fi < tokens.typography.length; fi++) {
|
| 850 |
var ff = (tokens.typography[fi].value.fontFamily || '').split(',')[0].trim();
|
| 851 |
+
if (ff && ff !== 'Inter' && ff !== 'sans-serif' && ff !== 'serif') { sampleFontFamily = ff; break; }
|
|
|
|
|
|
|
|
|
|
| 852 |
}
|
| 853 |
if (sampleFontFamily !== 'Inter') {
|
| 854 |
try {
|
| 855 |
await figma.loadFontAsync({ family: sampleFontFamily, style: 'Regular' });
|
| 856 |
+
sampleFont = { family: sampleFontFamily, style: 'Regular' };
|
| 857 |
+
try { await figma.loadFontAsync({ family: sampleFontFamily, style: 'Bold' }); sampleFontBold = { family: sampleFontFamily, style: 'Bold' }; } catch (e2) { sampleFontBold = sampleFont; }
|
| 858 |
+
} catch (e) { sampleFontFamily = 'Inter'; sampleFont = labelFont; sampleFontBold = boldFont; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 859 |
}
|
| 860 |
+
|
| 861 |
+
var pageY = 0; // tracks vertical position of next frame on the page
|
| 862 |
+
|
| 863 |
+
// ── Helper: create a section FRAME ──
|
| 864 |
+
function createSectionFrame(name, contentHeight) {
|
| 865 |
+
var frame = figma.createFrame();
|
| 866 |
+
frame.name = name;
|
| 867 |
+
frame.resize(FRAME_W, contentHeight);
|
| 868 |
+
frame.x = 0;
|
| 869 |
+
frame.y = pageY;
|
| 870 |
+
frame.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }];
|
| 871 |
+
frame.clipsContent = false;
|
| 872 |
+
specPage.appendChild(frame);
|
| 873 |
+
pageY += contentHeight + SECTION_GAP;
|
| 874 |
+
return frame;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 875 |
}
|
| 876 |
|
| 877 |
+
// ── Helper: add text to a frame ──
|
| 878 |
+
function addText(frame, text, font, size, x, y, color) {
|
| 879 |
+
var t = figma.createText();
|
| 880 |
+
t.fontName = font;
|
| 881 |
+
t.fontSize = size;
|
| 882 |
+
t.characters = text;
|
| 883 |
+
t.fills = [{ type: 'SOLID', color: color || { r: 0.1, g: 0.1, b: 0.1 } }];
|
| 884 |
+
t.x = x; t.y = y;
|
| 885 |
+
frame.appendChild(t);
|
| 886 |
+
return t;
|
|
|
|
| 887 |
}
|
| 888 |
|
| 889 |
+
var MUTED = { r: 0.5, g: 0.5, b: 0.5 };
|
| 890 |
+
var DARK = { r: 0.12, g: 0.12, b: 0.12 };
|
| 891 |
+
var BLUE = { r: 0.15, g: 0.45, b: 0.95 };
|
| 892 |
+
|
| 893 |
+
// ══════════════════════════════════════════════════════════
|
| 894 |
+
// TITLE FRAME
|
| 895 |
+
// ══════════════════════════════════════════════════════════
|
| 896 |
+
var titleFrame = createSectionFrame('Title', 120);
|
| 897 |
+
addText(titleFrame, 'Design System Specification', boldFont, 36, MARGIN, 32, DARK);
|
| 898 |
+
addText(titleFrame, 'Font: ' + sampleFontFamily, labelFont, 14, MARGIN, 80, MUTED);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 899 |
|
| 900 |
+
// ══════════════════════════════════════════════════════════
|
| 901 |
+
// COLORS FRAME — grouped by category with AA badges
|
| 902 |
+
// ══════════════════════════════════════════════════════════
|
| 903 |
+
if (tokens.colors.length > 0) {
|
| 904 |
+
// Group colors by category
|
| 905 |
var colorGroups = {};
|
| 906 |
var groupOrder = [];
|
| 907 |
for (var ci = 0; ci < tokens.colors.length; ci++) {
|
| 908 |
+
var cat = tokens.colors[ci].name.split('/')[0] || 'other';
|
| 909 |
+
if (!colorGroups[cat]) { colorGroups[cat] = []; groupOrder.push(cat); }
|
| 910 |
+
colorGroups[cat].push(tokens.colors[ci]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 911 |
}
|
| 912 |
+
// Sort: semantic first, then palette
|
| 913 |
+
var semOrder = ['brand', 'text', 'bg', 'background', 'border', 'feedback'];
|
|
|
|
| 914 |
var sortedGroups = [];
|
| 915 |
+
for (var so = 0; so < semOrder.length; so++) { if (colorGroups[semOrder[so]]) sortedGroups.push(semOrder[so]); }
|
| 916 |
+
for (var go = 0; go < groupOrder.length; go++) { if (sortedGroups.indexOf(groupOrder[go]) === -1) sortedGroups.push(groupOrder[go]); }
|
| 917 |
+
|
| 918 |
+
// Calculate height: each group = heading(40) + one row of swatches(SWATCH_H_COLOR + SWATCH_META_H + 16)
|
| 919 |
+
var colorsPerRow = Math.floor((CONTENT_W + SWATCH_GAP) / (SWATCH_W + SWATCH_GAP));
|
| 920 |
+
var totalColorH = SECTION_PAD;
|
| 921 |
+
for (var cgi = 0; cgi < sortedGroups.length; cgi++) {
|
| 922 |
+
var rows = Math.ceil(colorGroups[sortedGroups[cgi]].length / colorsPerRow);
|
| 923 |
+
totalColorH += 40 + rows * (SWATCH_H_COLOR + SWATCH_META_H + ITEM_GAP) + 16;
|
| 924 |
}
|
| 925 |
+
totalColorH += SECTION_PAD;
|
| 926 |
|
| 927 |
+
var colorFrame = createSectionFrame('Colors', totalColorH);
|
| 928 |
+
var cy = SECTION_PAD;
|
| 929 |
+
|
| 930 |
+
// Section title
|
| 931 |
+
addText(colorFrame, 'COLORS', boldFont, 32, MARGIN, cy, DARK);
|
| 932 |
+
cy += 48;
|
| 933 |
|
| 934 |
for (var gi = 0; gi < sortedGroups.length; gi++) {
|
| 935 |
+
var gName = sortedGroups[gi];
|
| 936 |
+
var gColors = colorGroups[gName];
|
| 937 |
|
| 938 |
+
// Group heading
|
| 939 |
+
addText(colorFrame, gName.charAt(0).toUpperCase() + gName.slice(1), boldFont, 18, MARGIN, cy, DARK);
|
| 940 |
+
cy += 36;
|
| 941 |
|
| 942 |
+
for (var ci2 = 0; ci2 < gColors.length; ci2++) {
|
| 943 |
+
var ct = gColors[ci2];
|
| 944 |
+
var col = ci2 % colorsPerRow;
|
| 945 |
+
var row = Math.floor(ci2 / colorsPerRow);
|
| 946 |
+
var sx = MARGIN + col * (SWATCH_W + SWATCH_GAP);
|
| 947 |
+
var sy = cy + row * (SWATCH_H_COLOR + SWATCH_META_H + ITEM_GAP);
|
| 948 |
|
| 949 |
+
// Color fill rectangle (top part of card)
|
| 950 |
var swatch = figma.createRectangle();
|
| 951 |
+
swatch.resize(SWATCH_W, SWATCH_H_COLOR);
|
| 952 |
+
swatch.x = sx; swatch.y = sy;
|
| 953 |
+
var tl = 8, tr = 8, bl = 0, br = 0;
|
| 954 |
+
swatch.topLeftRadius = tl; swatch.topRightRadius = tr;
|
| 955 |
+
swatch.bottomLeftRadius = bl; swatch.bottomRightRadius = br;
|
| 956 |
+
swatch.fills = [{ type: 'SOLID', color: hexToRgb(ct.value) }];
|
| 957 |
+
colorFrame.appendChild(swatch);
|
| 958 |
+
|
| 959 |
+
// AA badge on swatch
|
| 960 |
+
var aa = getAAResult(ct.value);
|
| 961 |
+
var badgeText = aa.passAA ? 'AA ✓ ' + aa.ratio + ':1' : 'AA ✗ ' + aa.ratio + ':1';
|
| 962 |
+
var badgeColor = aa.bestOn === 'white' ? { r: 1, g: 1, b: 1 } : { r: 0, g: 0, b: 0 };
|
| 963 |
+
addText(colorFrame, badgeText, labelFont, 10, sx + 8, sy + SWATCH_H_COLOR - 18, badgeColor);
|
| 964 |
+
|
| 965 |
+
// Metadata area (below swatch)
|
| 966 |
+
var metaBg = figma.createRectangle();
|
| 967 |
+
metaBg.resize(SWATCH_W, SWATCH_META_H);
|
| 968 |
+
metaBg.x = sx; metaBg.y = sy + SWATCH_H_COLOR;
|
| 969 |
+
metaBg.bottomLeftRadius = 8; metaBg.bottomRightRadius = 8;
|
| 970 |
+
metaBg.fills = [{ type: 'SOLID', color: { r: 0.97, g: 0.97, b: 0.98 } }];
|
| 971 |
+
metaBg.strokes = [{ type: 'SOLID', color: { r: 0.9, g: 0.9, b: 0.9 } }];
|
| 972 |
+
metaBg.strokeWeight = 1;
|
| 973 |
+
colorFrame.appendChild(metaBg);
|
| 974 |
+
|
| 975 |
+
// Token name
|
| 976 |
var nameParts = ct.name.split('/');
|
| 977 |
+
var displayName = nameParts.filter(function(p) { return p !== 'DEFAULT'; }).join('/');
|
| 978 |
+
addText(colorFrame, displayName, boldFont, 11, sx + 10, sy + SWATCH_H_COLOR + 8, DARK);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 979 |
|
| 980 |
// Hex value
|
| 981 |
+
addText(colorFrame, ct.value.toUpperCase(), labelFont, 11, sx + 10, sy + SWATCH_H_COLOR + 26, MUTED);
|
| 982 |
+
|
| 983 |
+
// Contrast info
|
| 984 |
+
var contrastText = aa.passAA ? 'AA Pass on ' + aa.bestOn : 'AA Fail (' + aa.ratio + ':1)';
|
| 985 |
+
var contrastColor = aa.passAA ? { r: 0.13, g: 0.55, b: 0.13 } : { r: 0.85, g: 0.18, b: 0.18 };
|
| 986 |
+
addText(colorFrame, contrastText, labelFont, 10, sx + 10, sy + SWATCH_H_COLOR + 44, contrastColor);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 987 |
}
|
| 988 |
|
| 989 |
+
var gRows = Math.ceil(gColors.length / colorsPerRow);
|
| 990 |
+
cy += gRows * (SWATCH_H_COLOR + SWATCH_META_H + ITEM_GAP) + 16;
|
| 991 |
}
|
|
|
|
|
|
|
| 992 |
}
|
| 993 |
|
| 994 |
+
// ══════════════════════════════════════════════════════════
|
| 995 |
+
// TYPOGRAPHY DESKTOP FRAME
|
| 996 |
+
// ══════════════════════════════════════════════════════════
|
| 997 |
+
var desktopTypo = [];
|
| 998 |
+
var mobileTypo = [];
|
| 999 |
+
for (var tti = 0; tti < tokens.typography.length; tti++) {
|
| 1000 |
+
if (tokens.typography[tti].name.toLowerCase().indexOf('mobile') > -1) {
|
| 1001 |
+
mobileTypo.push(tokens.typography[tti]);
|
| 1002 |
+
} else {
|
| 1003 |
+
desktopTypo.push(tokens.typography[tti]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1004 |
}
|
| 1005 |
+
}
|
| 1006 |
|
| 1007 |
+
function getSampleText(name) {
|
| 1008 |
+
var n = name.toLowerCase();
|
| 1009 |
+
if (n.indexOf('display') > -1) return 'The quick brown fox jumps over the lazy dog';
|
| 1010 |
+
if (n.indexOf('heading') > -1) return 'The quick brown fox jumps over';
|
| 1011 |
+
if (n.indexOf('body') > -1) return 'The quick brown fox jumps over the lazy dog. Pack my box with five dozen liquor jugs.';
|
| 1012 |
+
if (n.indexOf('caption') > -1) return 'The quick brown fox jumps over the lazy dog';
|
| 1013 |
+
if (n.indexOf('overline') > -1) return 'THE QUICK BROWN FOX JUMPS';
|
| 1014 |
+
return 'The quick brown fox jumps over the lazy dog';
|
| 1015 |
+
}
|
|
|
|
| 1016 |
|
| 1017 |
+
function renderTypoFrame(typoList, frameName) {
|
| 1018 |
+
if (typoList.length === 0) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1019 |
|
| 1020 |
+
// Estimate height: each row ~ 80-120px depending on font size
|
| 1021 |
+
var estH = SECTION_PAD + 60;
|
| 1022 |
+
for (var i = 0; i < typoList.length; i++) {
|
| 1023 |
+
var fs = parseNumericValue(typoList[i].value.fontSize) || 16;
|
| 1024 |
+
estH += Math.max(Math.min(fs, 56), 24) + 64; // sample + labels + gap
|
| 1025 |
+
}
|
| 1026 |
+
estH += SECTION_PAD;
|
| 1027 |
+
|
| 1028 |
+
var frame = createSectionFrame(frameName, estH);
|
| 1029 |
+
var fy = SECTION_PAD;
|
| 1030 |
+
|
| 1031 |
+
addText(frame, frameName.toUpperCase(), boldFont, 32, MARGIN, fy, DARK);
|
| 1032 |
+
fy += 52;
|
| 1033 |
+
|
| 1034 |
+
// Column headers
|
| 1035 |
+
var COL_NAME = MARGIN;
|
| 1036 |
+
var COL_SAMPLE = MARGIN + 180;
|
| 1037 |
+
var COL_SPECS = MARGIN + 760;
|
| 1038 |
+
|
| 1039 |
+
addText(frame, 'Token', boldFont, 12, COL_NAME, fy, MUTED);
|
| 1040 |
+
addText(frame, 'Sample', boldFont, 12, COL_SAMPLE, fy, MUTED);
|
| 1041 |
+
addText(frame, 'Specifications', boldFont, 12, COL_SPECS, fy, MUTED);
|
| 1042 |
+
fy += 28;
|
| 1043 |
+
|
| 1044 |
+
// Divider
|
| 1045 |
+
var div = figma.createRectangle();
|
| 1046 |
+
div.resize(CONTENT_W, 1); div.x = MARGIN; div.y = fy;
|
| 1047 |
+
div.fills = [{ type: 'SOLID', color: { r: 0.88, g: 0.88, b: 0.88 } }];
|
| 1048 |
+
frame.appendChild(div);
|
| 1049 |
+
fy += 16;
|
| 1050 |
+
|
| 1051 |
+
for (var ti = 0; ti < typoList.length; ti++) {
|
| 1052 |
+
var tt = typoList[ti];
|
| 1053 |
+
var val = tt.value;
|
| 1054 |
+
var fFamily = (val.fontFamily || 'Inter').split(',')[0].trim();
|
| 1055 |
+
var fSize = parseNumericValue(val.fontSize) || 16;
|
| 1056 |
+
var fWeight = val.fontWeight || '400';
|
| 1057 |
+
var fLH = val.lineHeight || '1.5';
|
| 1058 |
+
var displaySize = Math.min(fSize, 56);
|
| 1059 |
+
|
| 1060 |
+
// Token name column
|
| 1061 |
+
var tierParts = tt.name.split('/');
|
| 1062 |
+
var tierName = tierParts.filter(function(p) { return p !== 'desktop' && p !== 'mobile'; }).join('.');
|
| 1063 |
+
addText(frame, tierName, boldFont, 13, COL_NAME, fy + 4, DARK);
|
| 1064 |
+
|
| 1065 |
+
// Sample text in actual font
|
| 1066 |
+
var useBold = (tierName.indexOf('display') > -1 || tierName.indexOf('heading') > -1);
|
| 1067 |
+
var sample = figma.createText();
|
| 1068 |
+
sample.fontName = useBold ? sampleFontBold : sampleFont;
|
| 1069 |
+
sample.fontSize = displaySize;
|
| 1070 |
+
sample.characters = getSampleText(tt.name);
|
| 1071 |
+
sample.x = COL_SAMPLE; sample.y = fy;
|
| 1072 |
+
sample.resize(540, sample.height);
|
| 1073 |
+
sample.textAutoResize = 'HEIGHT';
|
| 1074 |
+
frame.appendChild(sample);
|
| 1075 |
+
|
| 1076 |
+
// Specs column — stacked chips
|
| 1077 |
+
var specY = fy;
|
| 1078 |
+
addText(frame, 'Size: ' + fSize + 'px', boldFont, 12, COL_SPECS, specY, DARK);
|
| 1079 |
+
specY += 18;
|
| 1080 |
+
addText(frame, 'Weight: ' + fWeight, labelFont, 12, COL_SPECS, specY, MUTED);
|
| 1081 |
+
specY += 18;
|
| 1082 |
+
addText(frame, 'Line Height: ' + fLH, labelFont, 12, COL_SPECS, specY, MUTED);
|
| 1083 |
+
specY += 18;
|
| 1084 |
+
addText(frame, 'Font: ' + fFamily, labelFont, 12, COL_SPECS, specY, BLUE);
|
| 1085 |
+
|
| 1086 |
+
var rowH = Math.max(displaySize + 8, specY - fy + 24);
|
| 1087 |
+
fy += rowH + 8;
|
| 1088 |
+
|
| 1089 |
+
// Row separator
|
| 1090 |
+
var sep = figma.createRectangle();
|
| 1091 |
+
sep.resize(CONTENT_W, 1); sep.x = MARGIN; sep.y = fy;
|
| 1092 |
+
sep.fills = [{ type: 'SOLID', color: { r: 0.94, g: 0.94, b: 0.94 } }];
|
| 1093 |
+
frame.appendChild(sep);
|
| 1094 |
+
fy += 12;
|
| 1095 |
}
|
| 1096 |
|
| 1097 |
+
// Resize frame to actual content
|
| 1098 |
+
frame.resize(FRAME_W, fy + SECTION_PAD);
|
|
|
|
|
|
|
|
|
|
| 1099 |
}
|
| 1100 |
|
| 1101 |
+
renderTypoFrame(desktopTypo, 'Typography — Desktop');
|
| 1102 |
+
renderTypoFrame(mobileTypo, 'Typography — Mobile');
|
|
|
|
|
|
|
|
|
|
| 1103 |
|
| 1104 |
+
// ══════════════════════════════════════════════════════════
|
| 1105 |
+
// SPACING FRAME — Desktop & Mobile side by side, bars not squares
|
| 1106 |
+
// ══════════════════════════════════════════════════════════
|
| 1107 |
+
if (tokens.spacing.length > 0) {
|
| 1108 |
+
var dSpacing = [];
|
| 1109 |
+
var mSpacing = [];
|
| 1110 |
for (var spi = 0; spi < tokens.spacing.length; spi++) {
|
| 1111 |
+
if (tokens.spacing[spi].name.toLowerCase().indexOf('mobile') > -1) {
|
| 1112 |
+
mSpacing.push(tokens.spacing[spi]);
|
|
|
|
|
|
|
| 1113 |
} else {
|
| 1114 |
+
dSpacing.push(tokens.spacing[spi]);
|
| 1115 |
}
|
| 1116 |
}
|
| 1117 |
|
| 1118 |
+
var maxItems = Math.max(dSpacing.length, mSpacing.length);
|
| 1119 |
+
var spFrameH = SECTION_PAD + 60 + maxItems * 48 + SECTION_PAD;
|
| 1120 |
+
|
| 1121 |
+
var spFrame = createSectionFrame('Spacing', spFrameH);
|
| 1122 |
+
var spy = SECTION_PAD;
|
| 1123 |
+
|
| 1124 |
+
addText(spFrame, 'SPACING', boldFont, 32, MARGIN, spy, DARK);
|
| 1125 |
+
spy += 52;
|
| 1126 |
+
|
| 1127 |
+
// Render spacing column as horizontal bars (Carbon-style)
|
| 1128 |
+
function renderSpacingBars(list, startX, title, y0) {
|
| 1129 |
+
addText(spFrame, title, boldFont, 16, startX, y0, BLUE);
|
| 1130 |
+
var ly = y0 + 32;
|
| 1131 |
+
|
| 1132 |
+
for (var si = 0; si < Math.min(list.length, 12); si++) {
|
| 1133 |
+
var sp = list[si];
|
| 1134 |
var spVal = parseNumericValue(sp.value);
|
| 1135 |
+
var barW = Math.max(spVal * 3, 8); // Scale 3x for visibility
|
| 1136 |
+
barW = Math.min(barW, 400); // Cap
|
| 1137 |
+
|
| 1138 |
+
// Token label
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1139 |
var spParts = sp.name.split('/');
|
| 1140 |
var spDisplayName = spParts.filter(function(p) { return p !== 'desktop' && p !== 'mobile'; }).join('/');
|
| 1141 |
+
addText(spFrame, spDisplayName, labelFont, 12, startX, ly + 4, DARK);
|
| 1142 |
+
|
| 1143 |
+
// Bar
|
| 1144 |
+
var bar = figma.createRectangle();
|
| 1145 |
+
bar.resize(barW, 24);
|
| 1146 |
+
bar.x = startX + 120;
|
| 1147 |
+
bar.y = ly;
|
| 1148 |
+
bar.cornerRadius = 4;
|
| 1149 |
+
bar.fills = [{ type: 'SOLID', color: { r: 0.2, g: 0.55, b: 1 } }];
|
| 1150 |
+
spFrame.appendChild(bar);
|
| 1151 |
+
|
| 1152 |
+
// Value label
|
| 1153 |
+
addText(spFrame, sp.value, labelFont, 12, startX + 120 + barW + 12, ly + 4, MUTED);
|
| 1154 |
+
|
| 1155 |
+
ly += 40;
|
| 1156 |
}
|
| 1157 |
+
return ly;
|
| 1158 |
}
|
| 1159 |
|
| 1160 |
+
var dEnd = renderSpacingBars(dSpacing, MARGIN, '🖥 Desktop', spy);
|
| 1161 |
+
var mEnd = renderSpacingBars(mSpacing, MARGIN + 560, '📱 Mobile', spy);
|
| 1162 |
|
| 1163 |
+
spFrame.resize(FRAME_W, Math.max(dEnd, mEnd) + SECTION_PAD);
|
| 1164 |
}
|
| 1165 |
|
| 1166 |
+
// ══════════════════════════════════════════════════════════
|
| 1167 |
+
// BORDER RADIUS FRAME
|
| 1168 |
+
// ══════════════════════════════════════════════════════════
|
| 1169 |
if (tokens.borderRadius.length > 0) {
|
| 1170 |
+
var radFrameH = SECTION_PAD + 60 + 120 + SECTION_PAD;
|
| 1171 |
+
var radFrame = createSectionFrame('Border Radius', radFrameH);
|
| 1172 |
+
var ry = SECTION_PAD;
|
| 1173 |
+
|
| 1174 |
+
addText(radFrame, 'BORDER RADIUS', boldFont, 32, MARGIN, ry, DARK);
|
| 1175 |
+
ry += 52;
|
| 1176 |
|
| 1177 |
+
var rx = MARGIN;
|
| 1178 |
for (var ri = 0; ri < tokens.borderRadius.length; ri++) {
|
| 1179 |
+
var rToken = tokens.borderRadius[ri];
|
| 1180 |
+
var rVal = parseNumericValue(rToken.value);
|
| 1181 |
|
| 1182 |
var rect = figma.createRectangle();
|
| 1183 |
+
rect.resize(64, 64);
|
| 1184 |
+
rect.x = rx; rect.y = ry;
|
| 1185 |
+
rect.cornerRadius = Math.min(rVal, 32);
|
| 1186 |
+
rect.fills = [{ type: 'SOLID', color: { r: 0.93, g: 0.93, b: 0.96 } }];
|
| 1187 |
+
rect.strokes = [{ type: 'SOLID', color: { r: 0.75, g: 0.75, b: 0.82 } }];
|
|
|
|
| 1188 |
rect.strokeWeight = 2;
|
| 1189 |
+
radFrame.appendChild(rect);
|
| 1190 |
+
|
| 1191 |
+
// Name
|
| 1192 |
+
addText(radFrame, rToken.name.split('/').pop(), boldFont, 11, rx, ry + 72, DARK);
|
| 1193 |
+
// Value
|
| 1194 |
+
addText(radFrame, rToken.value, labelFont, 11, rx, ry + 88, MUTED);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1195 |
|
| 1196 |
+
rx += 90;
|
| 1197 |
+
}
|
| 1198 |
}
|
| 1199 |
|
| 1200 |
+
// ══════════════════════════════════════════════════════════
|
| 1201 |
+
// SHADOWS FRAME — on light gray background (MD pattern)
|
| 1202 |
+
// ══════════════════════════════════════════════════════════
|
| 1203 |
if (tokens.shadows.length > 0) {
|
| 1204 |
+
var shFrameH = SECTION_PAD + 60 + 200 + SECTION_PAD;
|
| 1205 |
+
var shFrame = createSectionFrame('Shadows', shFrameH);
|
| 1206 |
+
// Light gray background for shadow visibility
|
| 1207 |
+
shFrame.fills = [{ type: 'SOLID', color: { r: 0.96, g: 0.96, b: 0.96 } }];
|
|
|
|
|
|
|
| 1208 |
|
| 1209 |
+
var shy = SECTION_PAD;
|
| 1210 |
+
addText(shFrame, 'SHADOWS / ELEVATION', boldFont, 32, MARGIN, shy, DARK);
|
| 1211 |
+
shy += 52;
|
|
|
|
|
|
|
| 1212 |
|
| 1213 |
+
var shx = MARGIN;
|
| 1214 |
+
for (var shi = 0; shi < tokens.shadows.length; shi++) {
|
| 1215 |
+
var shToken = tokens.shadows[shi];
|
| 1216 |
+
var sv = shToken.value;
|
| 1217 |
+
var oxV = parseNumericValue(sv.offsetX || sv.x || '0');
|
| 1218 |
+
var oyV = parseNumericValue(sv.offsetY || sv.y || '0');
|
| 1219 |
+
var blV = parseNumericValue(sv.blur || '0');
|
| 1220 |
+
var spV = parseNumericValue(sv.spread || '0');
|
| 1221 |
+
var shC = parseColorToRGBA(sv.color || 'rgba(0,0,0,0.25)');
|
| 1222 |
+
|
| 1223 |
+
// White card with shadow
|
| 1224 |
var card = figma.createRectangle();
|
| 1225 |
+
card.resize(140, 140);
|
| 1226 |
+
card.x = shx; card.y = shy;
|
|
|
|
| 1227 |
card.cornerRadius = 12;
|
| 1228 |
card.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }];
|
| 1229 |
card.effects = [{
|
| 1230 |
type: 'DROP_SHADOW',
|
| 1231 |
+
color: { r: shC.r, g: shC.g, b: shC.b, a: shC.a },
|
| 1232 |
+
offset: { x: oxV, y: oyV },
|
| 1233 |
+
radius: blV,
|
| 1234 |
+
spread: spV,
|
| 1235 |
visible: true,
|
| 1236 |
blendMode: 'NORMAL'
|
| 1237 |
}];
|
| 1238 |
+
shFrame.appendChild(card);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1239 |
|
| 1240 |
+
// Level name centered on card
|
| 1241 |
+
addText(shFrame, shToken.name.split('/').pop(), boldFont, 14, shx + 20, shy + 30, DARK);
|
| 1242 |
+
|
| 1243 |
+
// Specs on card
|
| 1244 |
+
addText(shFrame, 'blur: ' + blV + 'px', labelFont, 11, shx + 20, shy + 56, MUTED);
|
| 1245 |
+
addText(shFrame, 'y: ' + oyV + 'px', labelFont, 11, shx + 20, shy + 72, MUTED);
|
| 1246 |
+
addText(shFrame, 'spread: ' + spV + 'px', labelFont, 11, shx + 20, shy + 88, MUTED);
|
| 1247 |
|
| 1248 |
+
shx += 180;
|
| 1249 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1250 |
}
|
| 1251 |
|
| 1252 |
+
figma.ui.postMessage({ type: 'spec-complete', message: 'Visual spec created with ' + specPage.children.length + ' section frames!' });
|
|
|
|
|
|
|
|
|
|
| 1253 |
|
| 1254 |
} catch (error) {
|
| 1255 |
console.error('Error creating visual spec:', error);
|