riazmo Claude Opus 4.6 commited on
Commit
da7654a
·
1 Parent(s): a721079

feat: visual spec v2 — all 9 improvements + shadow interpolation restored

Browse files

Visual 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>

app.py CHANGED
@@ -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 = min(shadow_count, 5) # Export only what was extracted (capped at 5)
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} levels",
1510
  "Elevation tokens" if shadow_count > 0 else "No shadows found",
1511
  f"{tobe_shadow_count} levels",
1512
- tobe_label,
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 — export ONLY what was actually extracted
 
3609
  # =========================================================================
3610
- # Name mapping: assign best-fit names based on how many shadows were found
3611
- _SHADOW_NAMES_BY_COUNT = {
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
- # Cap at 5 maximum, take first 5 sorted by blur
3639
- final_shadows = parsed_shadows[:5]
3640
- names = _SHADOW_NAMES_BY_COUNT.get(len(final_shadows))
3641
- if names is None:
3642
- # Fallback for n > 5: xs, sm, md, lg, xl
3643
- names = [f"shadow.{i+1}" for i in range(len(final_shadows))]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3644
 
3645
  for i, shadow in enumerate(final_shadows):
3646
- token_name = names[i] if i < len(names) else f"shadow.{i + 1}"
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",
output_json/figma-plugin-extracted/figma-design-token-creator 5/src/code.js CHANGED
@@ -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 instead of creating new page (Figma Starter plan has 3-page limit)
806
  var specPage = figma.currentPage;
807
  specPage.name = '🎨 Design System Spec';
808
 
809
- // Clear existing children on the page so spec starts fresh
810
  while (specPage.children.length > 0) {
811
  specPage.children[0].remove();
812
  }
813
 
814
- var yOffset = 0;
815
- var xOffset = 0;
816
- var sectionGap = 80;
817
- var itemGap = 16;
 
 
 
818
 
819
- // Load Inter font for labels (with fallbacks for environments missing styles)
 
820
  var headingStyle = 'Regular';
821
- await figma.loadFontAsync({ family: 'Inter', style: 'Regular' });
 
822
  try {
823
  await figma.loadFontAsync({ family: 'Inter', style: 'Bold' });
824
  headingStyle = 'Bold';
825
  } catch (e) {
826
- console.warn('Inter Bold not available, using Regular for headings');
827
  }
828
-
829
- // === COLORS SECTION ===
830
- if (tokens.colors.length > 0) {
831
- // Section title
832
- var colorTitle = figma.createText();
833
- colorTitle.characters = 'COLORS';
834
- colorTitle.fontName = { family: 'Inter', style: headingStyle };
835
- colorTitle.fontSize = 24;
836
- colorTitle.x = xOffset;
837
- colorTitle.y = yOffset;
838
- specPage.appendChild(colorTitle);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
839
  yOffset += 50;
 
 
 
 
 
 
 
 
 
840
 
841
- var colorX = xOffset;
842
- var colorY = yOffset;
843
- var swatchSize = 60;
844
- var swatchGap = 16;
845
- var colsPerRow = 6;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
846
 
 
 
 
 
 
847
  for (var ci = 0; ci < tokens.colors.length; ci++) {
848
- var colorToken = tokens.colors[ci];
849
- var col = ci % colsPerRow;
850
- var row = Math.floor(ci / colsPerRow);
851
-
852
- // Color swatch
853
- var swatch = figma.createRectangle();
854
- swatch.resize(swatchSize, swatchSize);
855
- swatch.x = colorX + col * (swatchSize + swatchGap);
856
- swatch.y = colorY + row * (swatchSize + swatchGap + 30);
857
- swatch.cornerRadius = 8;
858
-
859
- var rgb = hexToRgb(colorToken.value);
860
- swatch.fills = [{ type: 'SOLID', color: rgb }];
861
- swatch.strokes = [{ type: 'SOLID', color: { r: 0.9, g: 0.9, b: 0.9 } }];
862
- swatch.strokeWeight = 1;
863
- specPage.appendChild(swatch);
864
-
865
- // Color name label
866
- var colorLabel = figma.createText();
867
- colorLabel.characters = colorToken.name.split('/').pop() || colorToken.name;
868
- colorLabel.fontName = { family: 'Inter', style: 'Regular' };
869
- colorLabel.fontSize = 10;
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 colorRows = Math.ceil(tokens.colors.length / colsPerRow);
886
- yOffset = colorY + colorRows * (swatchSize + swatchGap + 30) + sectionGap;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
887
  }
888
 
889
- // === TYPOGRAPHY SECTION ===
890
- // IMPORTANT: Only use already-loaded Inter Regular/Bold for ALL text in the spec.
891
- // Never attempt to load custom fonts — they may not exist in the Figma environment
892
- // and would crash the entire spec generation. Font details are shown as labels.
893
  if (tokens.typography.length > 0) {
894
- var typoTitle = figma.createText();
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 typoToken = tokens.typography[ti];
905
- var value = typoToken.value;
 
 
 
 
 
 
906
 
907
- var fontFamily = value.fontFamily || 'Inter';
908
- if (fontFamily.indexOf(',') > -1) {
909
- fontFamily = fontFamily.split(',')[0].trim();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
910
  }
911
- var fontSize = parseNumericValue(value.fontSize) || 16;
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
- yOffset += sectionGap;
 
 
 
 
938
  }
939
 
940
- // === SPACING SECTION ===
 
 
941
  if (tokens.spacing.length > 0) {
942
- var spacingTitle = figma.createText();
943
- spacingTitle.characters = 'SPACING';
944
- spacingTitle.fontName = { family: 'Inter', style: headingStyle };
945
- spacingTitle.fontSize = 24;
946
- spacingTitle.x = xOffset;
947
- spacingTitle.y = yOffset;
948
- specPage.appendChild(spacingTitle);
949
- yOffset += 50;
 
 
 
 
 
 
950
 
951
- var spacingX = xOffset;
952
- for (var si = 0; si < Math.min(tokens.spacing.length, 12); si++) {
953
- var spacingToken = tokens.spacing[si];
954
- var spacingValue = parseNumericValue(spacingToken.value);
955
-
956
- // Spacing bar
957
- var bar = figma.createRectangle();
958
- bar.resize(Math.max(spacingValue, 4), 24);
959
- bar.x = spacingX;
960
- bar.y = yOffset;
961
- bar.cornerRadius = 4;
962
- bar.fills = [{ type: 'SOLID', color: { r: 0.2, g: 0.6, b: 1 } }];
963
- specPage.appendChild(bar);
964
-
965
- // Label
966
- var spLabel = figma.createText();
967
- spLabel.characters = spacingToken.name.split('/').pop() + ' = ' + spacingToken.value;
968
- spLabel.fontName = { family: 'Inter', style: 'Regular' };
969
- spLabel.fontSize = 11;
970
- spLabel.x = spacingX;
971
- spLabel.y = yOffset + 30;
972
- specPage.appendChild(spLabel);
973
-
974
- spacingX += Math.max(spacingValue, 40) + 24;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
975
  }
976
 
977
- yOffset += 80 + sectionGap;
 
 
 
978
  }
979
 
980
- // === BORDER RADIUS SECTION ===
 
 
981
  if (tokens.borderRadius.length > 0) {
982
- var radiusTitle = figma.createText();
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(50, 50);
999
  rect.x = radiusX;
1000
  rect.y = yOffset;
1001
- rect.cornerRadius = Math.min(radiusValue, 25);
1002
- rect.fills = [{ type: 'SOLID', color: { r: 0.95, g: 0.95, b: 0.95 } }];
1003
- rect.strokes = [{ type: 'SOLID', color: { r: 0.8, g: 0.8, b: 0.8 } }];
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 + 56;
1015
  specPage.appendChild(rLabel);
1016
 
1017
- radiusX += 70;
1018
  }
1019
 
1020
- yOffset += 100 + sectionGap;
1021
  }
1022
 
1023
- // === SHADOWS SECTION ===
 
 
1024
  if (tokens.shadows.length > 0) {
1025
- var shadowTitle = figma.createText();
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(80, 80);
1048
  card.x = shadowX;
1049
  card.y = yOffset;
1050
- card.cornerRadius = 8;
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
- // Label
1064
  var shLabel = figma.createText();
1065
- shLabel.characters = shadowToken.name.split('/').pop();
1066
- shLabel.fontName = { family: 'Inter', style: 'Regular' };
1067
- shLabel.fontSize = 11;
 
1068
  shLabel.x = shadowX;
1069
- shLabel.y = yOffset + 90;
1070
  specPage.appendChild(shLabel);
1071
 
1072
- shadowX += 110;
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({