Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- CLAUDE.md +152 -44
- agents/llm_agents.py +28 -0
- agents/semantic_analyzer.py +9 -0
- app.py +138 -101
- output_json/figma-plugin-extracted/figma-design-token-creator 5/src/code.js +464 -241
- storage/benchmark_cache.json +1 -1
CLAUDE.md
CHANGED
|
@@ -1,11 +1,77 @@
|
|
| 1 |
-
# Design System Extractor v3.
|
| 2 |
|
| 3 |
## Overview
|
| 4 |
|
| 5 |
A multi-agent system that extracts, analyzes, and recommends improvements for design systems from websites. The system operates in two stages:
|
| 6 |
|
| 7 |
-
1. **Stage 1 (Deterministic)**: Extract CSS values β Normalize β Rule Engine analysis β **Rule-Based Color Classification** (free, no LLM)
|
| 8 |
-
2. **Stage 2 (LLM-powered
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
---
|
| 11 |
|
|
@@ -30,10 +96,11 @@ CSS Evidence β Category:
|
|
| 30 |
everything else β PALETTE (named by hue.shade)
|
| 31 |
```
|
| 32 |
|
| 33 |
-
### What AURORA Does Now
|
| 34 |
-
- Does NOT output naming_map
|
| 35 |
- Provides brand insights, palette strategy, cohesion score
|
| 36 |
-
-
|
|
|
|
|
|
|
| 37 |
|
| 38 |
### Files Changed in v3.1
|
| 39 |
- `core/color_classifier.py` β NEW: Rule-based classifier with dedup, caps, naming conventions
|
|
@@ -43,6 +110,24 @@ CSS Evidence β Category:
|
|
| 43 |
|
| 44 |
---
|
| 45 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
## PREVIOUS STATUS (v3.0 and earlier): BROKEN β RETHINK COMPLETED
|
| 47 |
|
| 48 |
### What's Wrong (observed from real site tests)
|
|
@@ -1008,54 +1093,77 @@ NormalizedTokens:
|
|
| 1008 |
|
| 1009 |
---
|
| 1010 |
|
| 1011 |
-
##
|
| 1012 |
-
|
| 1013 |
-
The original plan was "fix Stage 2 first, Stage 1 later." But the audit reveals:
|
| 1014 |
-
**If normalizer sends word-based shade names to AURORA, AURORA's ReAct naming will STILL conflict with normalizer names in the export merge.**
|
| 1015 |
-
|
| 1016 |
-
The pre-processing layer (Step 2 in the old plan) was supposed to fix this. But that's a bandaid β it re-normalizes what the normalizer already normalized. It's cleaner to fix the normalizer itself so it produces correct output from the start.
|
| 1017 |
|
| 1018 |
-
###
|
| 1019 |
|
| 1020 |
```
|
| 1021 |
-
PHASE 1: FIX NORMALIZER
|
| 1022 |
-
1a. Unify color naming β numeric shades only
|
| 1023 |
-
1b. Add radius normalization (parse, deduplicate, sort, name)
|
| 1024 |
-
1c. Add shadow normalization (parse, sort by blur, name)
|
| 1025 |
-
1d. Feed
|
| 1026 |
-
|
| 1027 |
-
PHASE 2: FIX STAGE 2
|
| 1028 |
-
2a.
|
| 1029 |
-
2b.
|
| 1030 |
-
2c.
|
| 1031 |
-
2d.
|
| 1032 |
-
2e.
|
| 1033 |
-
|
| 1034 |
-
PHASE 3: FIX EXPORT
|
| 1035 |
-
3a.
|
| 1036 |
-
3b. Radius/shadow export uses normalizer output directly
|
| 1037 |
-
3c.
|
| 1038 |
-
|
| 1039 |
-
|
| 1040 |
-
|
| 1041 |
-
|
| 1042 |
-
|
|
|
|
| 1043 |
```
|
| 1044 |
|
| 1045 |
-
###
|
| 1046 |
|
| 1047 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1048 |
|
| 1049 |
-
|
| 1050 |
|
| 1051 |
-
|
| 1052 |
|
| 1053 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1054 |
|
| 1055 |
-
|
| 1056 |
-
-
|
| 1057 |
-
-
|
| 1058 |
-
-
|
|
|
|
| 1059 |
|
| 1060 |
---
|
| 1061 |
|
|
|
|
| 1 |
+
# Design System Extractor v3.2 β Project Context
|
| 2 |
|
| 3 |
## Overview
|
| 4 |
|
| 5 |
A multi-agent system that extracts, analyzes, and recommends improvements for design systems from websites. The system operates in two stages:
|
| 6 |
|
| 7 |
+
1. **Stage 1 (Deterministic)**: Extract CSS values β Normalize (colors, radius, shadows, typography, spacing) β Rule Engine analysis β **Rule-Based Color Classification** (free, no LLM)
|
| 8 |
+
2. **Stage 2 (LLM-powered)**: Brand identification (AURORA) β Benchmark comparison (ATLAS) β Best practices (SENTINEL) β Synthesis (NEXUS)
|
| 9 |
+
3. **Export**: W3C DTCG v1 compliant JSON β Figma Plugin (visual spec + styles/variables)
|
| 10 |
+
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
## CURRENT STATUS: v3.2 (Feb 2026)
|
| 14 |
+
|
| 15 |
+
### What's Working
|
| 16 |
+
|
| 17 |
+
| Component | Status | Notes |
|
| 18 |
+
|-----------|--------|-------|
|
| 19 |
+
| CSS Extraction (Playwright) | β
Working | Desktop + mobile viewports |
|
| 20 |
+
| Color normalization | β
Working | Single numeric shade system (50-900) |
|
| 21 |
+
| Color classification | β
Working | `core/color_classifier.py` (815 lines, 100% deterministic) |
|
| 22 |
+
| Radius normalization | β
Working | Parse, deduplicate, sort, name (none/sm/md/lg/xl/2xl/full) |
|
| 23 |
+
| Shadow normalization | β
Working | Parse, sort by blur, deduplicate, name (xs/sm/md/lg/xl) |
|
| 24 |
+
| Typography normalization | β
Working | Desktop/mobile split, weight suffix |
|
| 25 |
+
| Spacing normalization | β
Working | GCD-based grid detection, base-8 alignment |
|
| 26 |
+
| Rule engine | β
Working | Type scale, WCAG AA, spacing grid, color statistics |
|
| 27 |
+
| LLM agents (ReAct) | β
Working | AURORA, ATLAS, SENTINEL, NEXUS with critic/retry |
|
| 28 |
+
| W3C DTCG export | β
Working | $value, $type, $description, $extensions |
|
| 29 |
+
| Figma plugin - visual spec | β
Working | Separate frames, AA badges, horizontal layout |
|
| 30 |
+
| Figma plugin - styles/variables | β
Working | Paint, text, effect styles + variable collections |
|
| 31 |
+
| Shadow interpolation | β
Working | Always produces 5 levels (xsβxl), interpolates if fewer extracted |
|
| 32 |
+
|
| 33 |
+
### Architecture Decisions (v3.2)
|
| 34 |
+
|
| 35 |
+
#### Naming Authority Chain (RESOLVED)
|
| 36 |
+
The three-naming-system conflict from v2/v3.0 is resolved:
|
| 37 |
+
|
| 38 |
+
```
|
| 39 |
+
1. Color Classifier (PRIMARY) β deterministic, covers ALL colors
|
| 40 |
+
βββ Rule-based: CSS evidence β category β token name
|
| 41 |
+
βββ 100% reproducible, logged with evidence
|
| 42 |
+
|
| 43 |
+
2. AURORA LLM (SECONDARY) β semantic role enhancer ONLY
|
| 44 |
+
βββ Can promote "color.blue.500" β "color.brand.primary"
|
| 45 |
+
βββ CANNOT rename palette colors
|
| 46 |
+
βββ Only brand/text/bg/border/feedback roles accepted
|
| 47 |
+
βββ filter_aurora_naming_map() enforces this boundary
|
| 48 |
+
|
| 49 |
+
3. Normalizer (FALLBACK) β preliminary hue+shade names
|
| 50 |
+
βββ Only used if classifier hasn't run yet
|
| 51 |
+
βββ _generate_preliminary_name() β "color.blue.500"
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
**app.py `_get_semantic_color_overrides()`** implements this chain:
|
| 55 |
+
- PRIMARY: `state.color_classification.colors` (from color_classifier)
|
| 56 |
+
- SECONDARY: `state.brand_result.naming_map` (from AURORA, filtered to semantic roles only)
|
| 57 |
+
|
| 58 |
+
**`_generate_color_name_from_hex()`** is DEPRECATED β kept as thin wrapper for edge cases.
|
| 59 |
+
|
| 60 |
+
#### W3C DTCG v1 Compliance (2025.10 Spec)
|
| 61 |
+
- `$type` values: `color`, `dimension`, `typography`, `shadow`
|
| 62 |
+
- `$value` for all token values
|
| 63 |
+
- `$description` for human-readable descriptions
|
| 64 |
+
- `$extensions` with namespaced metadata: `com.design-system-extractor`
|
| 65 |
+
- Colors: `{frequency, confidence, category, evidence}`
|
| 66 |
+
- Radius: `{frequency, fitsBase4, fitsBase8}`
|
| 67 |
+
- Shadows: `{frequency, rawCSS, blurPx}`
|
| 68 |
+
- Nested structure (not flat)
|
| 69 |
+
- `_flat_key_to_nested()` prevents nesting inside DTCG leaf nodes
|
| 70 |
+
|
| 71 |
+
#### Deprecated Components
|
| 72 |
+
- `agents/semantic_analyzer.py` β superseded by color_classifier + normalizer._infer_role_hint()
|
| 73 |
+
- `agents/stage2_graph.py` β old LangGraph parallel system, replaced by direct async in app.py
|
| 74 |
+
- `app.py _generate_color_name_from_hex()` β third naming system, now thin wrapper
|
| 75 |
|
| 76 |
---
|
| 77 |
|
|
|
|
| 96 |
everything else β PALETTE (named by hue.shade)
|
| 97 |
```
|
| 98 |
|
| 99 |
+
### What AURORA Does Now
|
|
|
|
| 100 |
- Provides brand insights, palette strategy, cohesion score
|
| 101 |
+
- naming_map is filtered to semantic roles only (brand/text/bg/border/feedback)
|
| 102 |
+
- LLM reasoning is shown in logs
|
| 103 |
+
- `filter_aurora_naming_map()` in llm_agents.py enforces the boundary
|
| 104 |
|
| 105 |
### Files Changed in v3.1
|
| 106 |
- `core/color_classifier.py` β NEW: Rule-based classifier with dedup, caps, naming conventions
|
|
|
|
| 110 |
|
| 111 |
---
|
| 112 |
|
| 113 |
+
## v3.2 FIX: DTCG COMPLIANCE + NAMING AUTHORITY (Feb 2026)
|
| 114 |
+
|
| 115 |
+
### What Changed
|
| 116 |
+
1. **W3C DTCG v1 strict compliance**: `_to_dtcg_token()` now supports `$extensions` with namespaced metadata
|
| 117 |
+
2. **Single naming authority resolved**: Color classifier is PRIMARY, AURORA is SECONDARY (semantic roles only)
|
| 118 |
+
3. **`_get_semantic_color_overrides()` rewritten**: Uses classifier as primary, AURORA filtered to role-only names
|
| 119 |
+
4. **`filter_aurora_naming_map()` added**: In llm_agents.py, strips non-semantic names from AURORA output
|
| 120 |
+
5. **`_generate_color_name_from_hex()` deprecated**: Thin wrapper using `categorize_color()` from color_utils
|
| 121 |
+
6. **`semantic_analyzer.py` deprecated**: Marked with deprecation notice, functionality absorbed elsewhere
|
| 122 |
+
|
| 123 |
+
### Files Changed in v3.2
|
| 124 |
+
- `app.py` β DTCG helpers enhanced, `_get_semantic_color_overrides()` rewritten, hex-name function deprecated
|
| 125 |
+
- `agents/llm_agents.py` β Added `filter_aurora_naming_map()` function
|
| 126 |
+
- `agents/semantic_analyzer.py` β Deprecated with notice
|
| 127 |
+
- `CLAUDE.md` β Updated to current status
|
| 128 |
+
|
| 129 |
+
---
|
| 130 |
+
|
| 131 |
## PREVIOUS STATUS (v3.0 and earlier): BROKEN β RETHINK COMPLETED
|
| 132 |
|
| 133 |
### What's Wrong (observed from real site tests)
|
|
|
|
| 1093 |
|
| 1094 |
---
|
| 1095 |
|
| 1096 |
+
## EXECUTION STATUS (Updated Feb 2026)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1097 |
|
| 1098 |
+
### Phases 1-3: COMPLETED
|
| 1099 |
|
| 1100 |
```
|
| 1101 |
+
PHASE 1: FIX NORMALIZER β
DONE
|
| 1102 |
+
1a. β
Unify color naming β numeric shades only (_generate_preliminary_name)
|
| 1103 |
+
1b. β
Add radius normalization (parse, deduplicate, sort, name) β normalizer.py:626-778
|
| 1104 |
+
1c. β
Add shadow normalization (parse, sort by blur, name) β normalizer.py:784-940
|
| 1105 |
+
1d. β
Feed role hints into normalizer β normalizer._infer_role_hint()
|
| 1106 |
+
|
| 1107 |
+
PHASE 2: FIX STAGE 2 β
DONE
|
| 1108 |
+
2a. β
Consolidated β llm_agents.py is primary, stage2_graph.py deprecated
|
| 1109 |
+
2b. β
AURORA with ReAct + critic + retry β llm_agents.py:420-470
|
| 1110 |
+
2c. β
SENTINEL with grounded scoring + cross-reference critic
|
| 1111 |
+
2d. β
NEXUS with ToT (two-perspective evaluation)
|
| 1112 |
+
2e. β
Post-validation layer β post_validate_stage2()
|
| 1113 |
+
|
| 1114 |
+
PHASE 3: FIX EXPORT β
DONE (v3.2)
|
| 1115 |
+
3a. β
Color classifier = PRIMARY authority, AURORA = semantic roles only
|
| 1116 |
+
3b. β
Radius/shadow export uses normalizer output directly
|
| 1117 |
+
3c. β
W3C DTCG v1 compliance with $extensions metadata
|
| 1118 |
+
3d. β
filter_aurora_naming_map() enforces role-only boundary
|
| 1119 |
+
|
| 1120 |
+
PHASE 4: EXTRACTION IMPROVEMENTS (NOT STARTED)
|
| 1121 |
+
4a. β Font family detection β still returns "sans-serif" fallback
|
| 1122 |
+
4b. β Rule engine: radius grid analysis
|
| 1123 |
+
4c. β Rule engine: shadow elevation analysis
|
| 1124 |
```
|
| 1125 |
|
| 1126 |
+
### PHASE 5: COMPONENT GENERATION (FUTURE β NOT STARTED)
|
| 1127 |
|
| 1128 |
+
Based on strategic research (Feb 2026), the next major feature is automated component generation in Figma:
|
| 1129 |
+
|
| 1130 |
+
```
|
| 1131 |
+
PHASE 5: FIGMA COMPONENT GENERATION
|
| 1132 |
+
5a. Component Definition Schema (JSON defining anatomy + token bindings + variants)
|
| 1133 |
+
5b. Token-to-Component binding engine
|
| 1134 |
+
5c. Figma Plugin: createComponent() + combineAsVariants() + setBoundVariable()
|
| 1135 |
+
5d. MVP Components: Button (60 variants), TextInput (8), Card (2), Toast (4), Checkbox+Radio (12)
|
| 1136 |
+
5e. Variable Collections: Primitives, Semantic, Spacing, Radius, Typography
|
| 1137 |
+
|
| 1138 |
+
PHASE 6: ECOSYSTEM INTEGRATION
|
| 1139 |
+
6a. Style Dictionary v4 compatible output (50+ platform formats for free)
|
| 1140 |
+
6b. Tokens Studio compatible JSON import
|
| 1141 |
+
6c. Dembrandt JSON as alternative input source
|
| 1142 |
+
6d. CI/CD GitHub Action for design system regression checks
|
| 1143 |
+
|
| 1144 |
+
PHASE 7: MCP INTEGRATION
|
| 1145 |
+
7a. Expose extractor as MCP tool server
|
| 1146 |
+
7b. Claude Desktop: "Extract design system from example.com"
|
| 1147 |
+
7c. Community Figma MCP bridge for push-to-Figma
|
| 1148 |
+
```
|
| 1149 |
|
| 1150 |
+
### Strategic Positioning
|
| 1151 |
|
| 1152 |
+
**"Lighthouse for Design Systems"** β We are NOT a token management platform (Tokens Studio), NOT a documentation platform (Zeroheight), NOT an extraction tool (Dembrandt). We are the **automated audit + bootstrap tool** that sits upstream of all of those.
|
| 1153 |
|
| 1154 |
+
**Unique differentiators no competitor has:**
|
| 1155 |
+
- Type scale ratio detection + standard scale matching
|
| 1156 |
+
- Spacing grid detection (GCD-based, base-8 alignment scoring)
|
| 1157 |
+
- LLM brand identification from CSS usage patterns
|
| 1158 |
+
- Holistic design system quality score (0-100)
|
| 1159 |
+
- Visual spec page auto-generated in Figma
|
| 1160 |
+
- Benchmark comparison against established design systems
|
| 1161 |
|
| 1162 |
+
**Key competitors to watch:**
|
| 1163 |
+
- Dembrandt (1,300β
) β does extraction better, but no analysis
|
| 1164 |
+
- Tokens Studio (264K users) β does Figma management better, but no extraction
|
| 1165 |
+
- Knapsack ($10M funding) β building ingestion engine, biggest strategic threat
|
| 1166 |
+
- html.to.design β captures layouts but not tokens/variables
|
| 1167 |
|
| 1168 |
---
|
| 1169 |
|
agents/llm_agents.py
CHANGED
|
@@ -1214,6 +1214,34 @@ def _apply_sentinel_fixes(result: BestPracticesResult, rule_engine, errors: list
|
|
| 1214 |
return result
|
| 1215 |
|
| 1216 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1217 |
def post_validate_stage2(
|
| 1218 |
aurora: BrandIdentification,
|
| 1219 |
sentinel: BestPracticesResult,
|
|
|
|
| 1214 |
return result
|
| 1215 |
|
| 1216 |
|
| 1217 |
+
def filter_aurora_naming_map(aurora: BrandIdentification) -> dict:
|
| 1218 |
+
"""Filter AURORA naming_map to only keep semantic role assignments.
|
| 1219 |
+
|
| 1220 |
+
AURORA is a secondary naming authority β it can assign semantic roles
|
| 1221 |
+
(brand.primary, text.secondary, bg.primary, feedback.error, etc.)
|
| 1222 |
+
but cannot override palette names (blue.500, neutral.700, etc.).
|
| 1223 |
+
|
| 1224 |
+
The color_classifier is the primary naming authority.
|
| 1225 |
+
|
| 1226 |
+
Returns:
|
| 1227 |
+
Dict of hex -> semantic_name (only role-based names).
|
| 1228 |
+
"""
|
| 1229 |
+
SEMANTIC_PREFIXES = ('brand.', 'text.', 'bg.', 'border.', 'feedback.')
|
| 1230 |
+
filtered = {}
|
| 1231 |
+
|
| 1232 |
+
for hex_val, name in (aurora.naming_map or {}).items():
|
| 1233 |
+
hex_clean = str(hex_val).strip().lower()
|
| 1234 |
+
if not hex_clean.startswith('#') or not name:
|
| 1235 |
+
continue
|
| 1236 |
+
clean_name = name if name.startswith('color.') else f'color.{name}'
|
| 1237 |
+
# Extract the part after "color."
|
| 1238 |
+
after_prefix = clean_name[6:] # "brand.primary", "blue.500", etc.
|
| 1239 |
+
if any(after_prefix.startswith(sp) for sp in SEMANTIC_PREFIXES):
|
| 1240 |
+
filtered[hex_clean] = clean_name
|
| 1241 |
+
|
| 1242 |
+
return filtered
|
| 1243 |
+
|
| 1244 |
+
|
| 1245 |
def post_validate_stage2(
|
| 1246 |
aurora: BrandIdentification,
|
| 1247 |
sentinel: BestPracticesResult,
|
agents/semantic_analyzer.py
CHANGED
|
@@ -2,6 +2,15 @@
|
|
| 2 |
Agent 1C: Semantic Color Analyzer
|
| 3 |
Design System Extractor v2
|
| 4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
Persona: Design System Semanticist
|
| 6 |
|
| 7 |
Responsibilities:
|
|
|
|
| 2 |
Agent 1C: Semantic Color Analyzer
|
| 3 |
Design System Extractor v2
|
| 4 |
|
| 5 |
+
β οΈ DEPRECATED in v3.2 β Superseded by:
|
| 6 |
+
- core/color_classifier.py (rule-based, primary naming authority)
|
| 7 |
+
- agents/normalizer.py._infer_role_hint() (role hints for AURORA)
|
| 8 |
+
- AURORA agent in llm_agents.py (semantic role enhancement only)
|
| 9 |
+
|
| 10 |
+
This module is kept for backward compatibility but should not be called
|
| 11 |
+
in the main pipeline. Its heuristics have been absorbed into
|
| 12 |
+
normalizer._infer_role_hint() and color_classifier.classify_colors().
|
| 13 |
+
|
| 14 |
Persona: Design System Semanticist
|
| 15 |
|
| 16 |
Responsibilities:
|
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 =
|
| 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)
|
|
@@ -2949,20 +2947,29 @@ def _flat_key_to_nested(flat_key: str, value: dict, root: dict):
|
|
| 2949 |
current[parts[-1]] = value
|
| 2950 |
|
| 2951 |
|
| 2952 |
-
def _to_dtcg_token(value, token_type: str, description: str = None,
|
| 2953 |
-
|
|
|
|
|
|
|
|
|
|
| 2954 |
|
| 2955 |
Args:
|
| 2956 |
value: The token value
|
| 2957 |
-
token_type: W3C DTCG type
|
|
|
|
|
|
|
|
|
|
| 2958 |
description: Optional human-readable description
|
| 2959 |
source: Optional source indicator (extracted, recommended, semantic)
|
|
|
|
| 2960 |
"""
|
| 2961 |
token = {"$type": token_type, "$value": value}
|
| 2962 |
-
if description:
|
|
|
|
|
|
|
| 2963 |
token["$description"] = description
|
| 2964 |
-
if
|
| 2965 |
-
token["$
|
| 2966 |
return token
|
| 2967 |
|
| 2968 |
|
|
@@ -2984,21 +2991,42 @@ def _shadow_to_dtcg(shadow_dict: dict) -> dict:
|
|
| 2984 |
def _get_semantic_color_overrides() -> dict:
|
| 2985 |
"""Build color hex->semantic name map.
|
| 2986 |
|
| 2987 |
-
v3:
|
| 2988 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2989 |
"""
|
| 2990 |
overrides = {} # hex -> semantic_name
|
| 2991 |
|
| 2992 |
-
# PRIMARY:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2993 |
brand_result = getattr(state, 'brand_result', None)
|
| 2994 |
if brand_result:
|
| 2995 |
naming_map = getattr(brand_result, 'naming_map', None)
|
| 2996 |
if isinstance(naming_map, dict) and naming_map:
|
| 2997 |
for hex_val, name in naming_map.items():
|
| 2998 |
hex_clean = str(hex_val).strip().lower()
|
| 2999 |
-
if hex_clean.startswith('#')
|
| 3000 |
-
|
| 3001 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3002 |
overrides[hex_clean] = clean_name
|
| 3003 |
|
| 3004 |
return overrides
|
|
@@ -3015,90 +3043,48 @@ def _is_valid_hex_color(value: str) -> bool:
|
|
| 3015 |
|
| 3016 |
|
| 3017 |
def _generate_color_name_from_hex(hex_val: str, used_names: set = None) -> str:
|
| 3018 |
-
"""
|
| 3019 |
|
| 3020 |
-
|
| 3021 |
-
|
| 3022 |
"""
|
|
|
|
| 3023 |
import colorsys
|
| 3024 |
|
| 3025 |
used_names = used_names or set()
|
| 3026 |
|
| 3027 |
-
# Parse hex to RGB
|
| 3028 |
hex_clean = hex_val.lstrip('#').lower()
|
| 3029 |
if len(hex_clean) == 3:
|
| 3030 |
-
hex_clean = ''.join([c*2 for c in hex_clean])
|
| 3031 |
|
| 3032 |
try:
|
| 3033 |
r = int(hex_clean[0:2], 16) / 255
|
| 3034 |
g = int(hex_clean[2:4], 16) / 255
|
| 3035 |
b = int(hex_clean[4:6], 16) / 255
|
| 3036 |
except (ValueError, IndexError):
|
| 3037 |
-
return "color.other.
|
| 3038 |
|
| 3039 |
-
# Convert to HSL
|
| 3040 |
h, l, s = colorsys.rgb_to_hls(r, g, b)
|
| 3041 |
-
|
| 3042 |
-
|
| 3043 |
-
lightness
|
| 3044 |
-
|
| 3045 |
-
|
| 3046 |
-
|
| 3047 |
-
|
| 3048 |
-
|
| 3049 |
-
|
| 3050 |
-
|
| 3051 |
-
|
| 3052 |
-
|
| 3053 |
-
|
| 3054 |
-
color_family = "orange"
|
| 3055 |
-
elif hue < 75:
|
| 3056 |
-
color_family = "yellow"
|
| 3057 |
-
elif hue < 150:
|
| 3058 |
-
color_family = "green"
|
| 3059 |
-
elif hue < 195:
|
| 3060 |
-
color_family = "teal"
|
| 3061 |
-
elif hue < 255:
|
| 3062 |
-
color_family = "blue"
|
| 3063 |
-
elif hue < 285:
|
| 3064 |
-
color_family = "purple"
|
| 3065 |
-
elif hue < 345:
|
| 3066 |
-
color_family = "pink"
|
| 3067 |
-
else:
|
| 3068 |
-
color_family = "red"
|
| 3069 |
-
|
| 3070 |
-
# Determine shade based on lightness (100-900 scale)
|
| 3071 |
-
if lightness >= 0.95:
|
| 3072 |
-
shade = "50"
|
| 3073 |
-
elif lightness >= 0.85:
|
| 3074 |
-
shade = "100"
|
| 3075 |
-
elif lightness >= 0.75:
|
| 3076 |
-
shade = "200"
|
| 3077 |
-
elif lightness >= 0.65:
|
| 3078 |
-
shade = "300"
|
| 3079 |
-
elif lightness >= 0.50:
|
| 3080 |
-
shade = "400"
|
| 3081 |
-
elif lightness >= 0.40:
|
| 3082 |
-
shade = "500"
|
| 3083 |
-
elif lightness >= 0.30:
|
| 3084 |
-
shade = "600"
|
| 3085 |
-
elif lightness >= 0.20:
|
| 3086 |
-
shade = "700"
|
| 3087 |
-
elif lightness >= 0.10:
|
| 3088 |
-
shade = "800"
|
| 3089 |
-
else:
|
| 3090 |
-
shade = "900"
|
| 3091 |
|
| 3092 |
-
# Generate base name
|
| 3093 |
base_name = f"color.{color_family}.{shade}"
|
| 3094 |
-
|
| 3095 |
-
# Handle conflicts by adding suffix
|
| 3096 |
final_name = base_name
|
| 3097 |
suffix = 1
|
| 3098 |
while final_name in used_names:
|
| 3099 |
suffix += 1
|
| 3100 |
final_name = f"{base_name}_{suffix}"
|
| 3101 |
-
|
| 3102 |
return final_name
|
| 3103 |
|
| 3104 |
|
|
@@ -3212,7 +3198,14 @@ def export_stage1_json(convention: str = "semantic"):
|
|
| 3212 |
log_callback=state.log,
|
| 3213 |
)
|
| 3214 |
for c in classification.colors:
|
| 3215 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3216 |
_flat_key_to_nested(c.token_name, dtcg_token, result)
|
| 3217 |
token_count += 1
|
| 3218 |
|
|
@@ -3287,7 +3280,7 @@ def export_stage1_json(convention: str = "semantic"):
|
|
| 3287 |
token_count += 1
|
| 3288 |
|
| 3289 |
# =========================================================================
|
| 3290 |
-
# BORDER RADIUS β
|
| 3291 |
# =========================================================================
|
| 3292 |
if state.desktop_normalized and state.desktop_normalized.radius:
|
| 3293 |
seen_radius = {}
|
|
@@ -3296,7 +3289,14 @@ def export_stage1_json(convention: str = "semantic"):
|
|
| 3296 |
if token_name is None:
|
| 3297 |
continue # Duplicate radius β skip
|
| 3298 |
flat_key = token_name
|
| 3299 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3300 |
_flat_key_to_nested(flat_key, dtcg_token, result)
|
| 3301 |
token_count += 1
|
| 3302 |
|
|
@@ -3304,18 +3304,22 @@ def export_stage1_json(convention: str = "semantic"):
|
|
| 3304 |
# SHADOWS β W3C DTCG shadow format
|
| 3305 |
# =========================================================================
|
| 3306 |
if state.desktop_normalized and state.desktop_normalized.shadows:
|
| 3307 |
-
|
| 3308 |
sorted_shadows = sorted(
|
| 3309 |
state.desktop_normalized.shadows.items(),
|
| 3310 |
key=lambda x: _get_shadow_blur(x[1].value),
|
| 3311 |
)
|
| 3312 |
for i, (name, s) in enumerate(sorted_shadows):
|
| 3313 |
-
size_name =
|
| 3314 |
flat_key = f"shadow.{size_name}"
|
| 3315 |
-
# Parse CSS shadow and convert to DTCG format
|
| 3316 |
parsed = _parse_shadow_to_tokens_studio(s.value)
|
| 3317 |
dtcg_shadow_value = _shadow_to_dtcg(parsed)
|
| 3318 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3319 |
_flat_key_to_nested(flat_key, dtcg_token, result)
|
| 3320 |
token_count += 1
|
| 3321 |
|
|
@@ -3605,16 +3609,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 +3633,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)
|
|
|
|
| 2947 |
current[parts[-1]] = value
|
| 2948 |
|
| 2949 |
|
| 2950 |
+
def _to_dtcg_token(value, token_type: str, description: str = None,
|
| 2951 |
+
source: str = None, extensions: dict = None) -> dict:
|
| 2952 |
+
"""Wrap value in W3C DTCG v1 (2025.10) format.
|
| 2953 |
+
|
| 2954 |
+
Spec: https://www.designtokens.org/tr/drafts/format/
|
| 2955 |
|
| 2956 |
Args:
|
| 2957 |
value: The token value
|
| 2958 |
+
token_type: W3C DTCG type β must be one of:
|
| 2959 |
+
color, dimension, fontFamily, fontWeight, number,
|
| 2960 |
+
duration, cubicBezier, shadow, strokeStyle, border,
|
| 2961 |
+
transition, gradient, typography
|
| 2962 |
description: Optional human-readable description
|
| 2963 |
source: Optional source indicator (extracted, recommended, semantic)
|
| 2964 |
+
extensions: Optional dict for $extensions (custom metadata like frequency, confidence)
|
| 2965 |
"""
|
| 2966 |
token = {"$type": token_type, "$value": value}
|
| 2967 |
+
if description and source:
|
| 2968 |
+
token["$description"] = f"[{source}] {description}"
|
| 2969 |
+
elif description:
|
| 2970 |
token["$description"] = description
|
| 2971 |
+
if extensions:
|
| 2972 |
+
token["$extensions"] = {"com.design-system-extractor": extensions}
|
| 2973 |
return token
|
| 2974 |
|
| 2975 |
|
|
|
|
| 2991 |
def _get_semantic_color_overrides() -> dict:
|
| 2992 |
"""Build color hex->semantic name map.
|
| 2993 |
|
| 2994 |
+
v3.2: Color classifier is the PRIMARY naming authority (deterministic, reproducible).
|
| 2995 |
+
AURORA is a SECONDARY enhancer β it can only ADD semantic role names
|
| 2996 |
+
(brand.primary, text.secondary, etc.) but cannot override palette names.
|
| 2997 |
+
|
| 2998 |
+
Authority chain:
|
| 2999 |
+
1. Color classifier (rule-based, covers ALL colors)
|
| 3000 |
+
2. AURORA naming_map (LLM, only brand/text/bg/border/feedback roles accepted)
|
| 3001 |
+
3. Normalizer suggested_name (fallback)
|
| 3002 |
"""
|
| 3003 |
overrides = {} # hex -> semantic_name
|
| 3004 |
|
| 3005 |
+
# PRIMARY: Color classifier (deterministic, covers ALL colors)
|
| 3006 |
+
classified = getattr(state, 'color_classification', None)
|
| 3007 |
+
if classified and hasattr(classified, 'colors'):
|
| 3008 |
+
for cc in classified.colors:
|
| 3009 |
+
hex_clean = cc.hex.strip().lower()
|
| 3010 |
+
if hex_clean.startswith('#') and cc.token_name:
|
| 3011 |
+
overrides[hex_clean] = cc.token_name
|
| 3012 |
+
|
| 3013 |
+
# SECONDARY: AURORA naming_map β ONLY accept semantic role upgrades
|
| 3014 |
+
# AURORA can promote "color.blue.500" to "color.brand.primary"
|
| 3015 |
+
# but cannot rename palette colors to different palette names
|
| 3016 |
+
_SEMANTIC_ROLES = {'brand.', 'text.', 'bg.', 'border.', 'feedback.'}
|
| 3017 |
brand_result = getattr(state, 'brand_result', None)
|
| 3018 |
if brand_result:
|
| 3019 |
naming_map = getattr(brand_result, 'naming_map', None)
|
| 3020 |
if isinstance(naming_map, dict) and naming_map:
|
| 3021 |
for hex_val, name in naming_map.items():
|
| 3022 |
hex_clean = str(hex_val).strip().lower()
|
| 3023 |
+
if not hex_clean.startswith('#') or not name:
|
| 3024 |
+
continue
|
| 3025 |
+
clean_name = name if name.startswith('color.') else f'color.{name}'
|
| 3026 |
+
# Only accept semantic role names from AURORA
|
| 3027 |
+
name_after_color = clean_name[6:] # strip "color."
|
| 3028 |
+
is_semantic_role = any(name_after_color.startswith(r) for r in _SEMANTIC_ROLES)
|
| 3029 |
+
if is_semantic_role:
|
| 3030 |
overrides[hex_clean] = clean_name
|
| 3031 |
|
| 3032 |
return overrides
|
|
|
|
| 3043 |
|
| 3044 |
|
| 3045 |
def _generate_color_name_from_hex(hex_val: str, used_names: set = None) -> str:
|
| 3046 |
+
"""DEPRECATED: Use normalizer._generate_preliminary_name() instead.
|
| 3047 |
|
| 3048 |
+
Kept as thin wrapper for backward compatibility.
|
| 3049 |
+
Delegates to normalizer's naming logic via color_utils.categorize_color().
|
| 3050 |
"""
|
| 3051 |
+
from core.color_utils import categorize_color, parse_color
|
| 3052 |
import colorsys
|
| 3053 |
|
| 3054 |
used_names = used_names or set()
|
| 3055 |
|
|
|
|
| 3056 |
hex_clean = hex_val.lstrip('#').lower()
|
| 3057 |
if len(hex_clean) == 3:
|
| 3058 |
+
hex_clean = ''.join([c * 2 for c in hex_clean])
|
| 3059 |
|
| 3060 |
try:
|
| 3061 |
r = int(hex_clean[0:2], 16) / 255
|
| 3062 |
g = int(hex_clean[2:4], 16) / 255
|
| 3063 |
b = int(hex_clean[4:6], 16) / 255
|
| 3064 |
except (ValueError, IndexError):
|
| 3065 |
+
return "color.other.500"
|
| 3066 |
|
|
|
|
| 3067 |
h, l, s = colorsys.rgb_to_hls(r, g, b)
|
| 3068 |
+
color_family = categorize_color(hex_val) or "neutral"
|
| 3069 |
+
|
| 3070 |
+
# Numeric shade from lightness (matches normalizer._generate_preliminary_name)
|
| 3071 |
+
if l >= 0.95: shade = "50"
|
| 3072 |
+
elif l >= 0.85: shade = "100"
|
| 3073 |
+
elif l >= 0.75: shade = "200"
|
| 3074 |
+
elif l >= 0.65: shade = "300"
|
| 3075 |
+
elif l >= 0.50: shade = "400"
|
| 3076 |
+
elif l >= 0.40: shade = "500"
|
| 3077 |
+
elif l >= 0.30: shade = "600"
|
| 3078 |
+
elif l >= 0.20: shade = "700"
|
| 3079 |
+
elif l >= 0.10: shade = "800"
|
| 3080 |
+
else: shade = "900"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3081 |
|
|
|
|
| 3082 |
base_name = f"color.{color_family}.{shade}"
|
|
|
|
|
|
|
| 3083 |
final_name = base_name
|
| 3084 |
suffix = 1
|
| 3085 |
while final_name in used_names:
|
| 3086 |
suffix += 1
|
| 3087 |
final_name = f"{base_name}_{suffix}"
|
|
|
|
| 3088 |
return final_name
|
| 3089 |
|
| 3090 |
|
|
|
|
| 3198 |
log_callback=state.log,
|
| 3199 |
)
|
| 3200 |
for c in classification.colors:
|
| 3201 |
+
ext = {"frequency": c.frequency, "confidence": c.confidence, "category": c.category}
|
| 3202 |
+
if c.evidence:
|
| 3203 |
+
ext["evidence"] = c.evidence[:3] # Top 3 evidence items
|
| 3204 |
+
dtcg_token = _to_dtcg_token(
|
| 3205 |
+
c.hex, "color",
|
| 3206 |
+
description=f"{c.category}: {c.role}",
|
| 3207 |
+
extensions=ext,
|
| 3208 |
+
)
|
| 3209 |
_flat_key_to_nested(c.token_name, dtcg_token, result)
|
| 3210 |
token_count += 1
|
| 3211 |
|
|
|
|
| 3280 |
token_count += 1
|
| 3281 |
|
| 3282 |
# =========================================================================
|
| 3283 |
+
# BORDER RADIUS β W3C DTCG "dimension" type
|
| 3284 |
# =========================================================================
|
| 3285 |
if state.desktop_normalized and state.desktop_normalized.radius:
|
| 3286 |
seen_radius = {}
|
|
|
|
| 3289 |
if token_name is None:
|
| 3290 |
continue # Duplicate radius β skip
|
| 3291 |
flat_key = token_name
|
| 3292 |
+
ext = {"frequency": r.frequency}
|
| 3293 |
+
if hasattr(r, 'fits_base_4') and r.fits_base_4 is not None:
|
| 3294 |
+
ext["fitsBase4"] = r.fits_base_4
|
| 3295 |
+
if hasattr(r, 'fits_base_8') and r.fits_base_8 is not None:
|
| 3296 |
+
ext["fitsBase8"] = r.fits_base_8
|
| 3297 |
+
dtcg_token = _to_dtcg_token(r.value, "dimension",
|
| 3298 |
+
description=f"Border radius ({name})",
|
| 3299 |
+
extensions=ext)
|
| 3300 |
_flat_key_to_nested(flat_key, dtcg_token, result)
|
| 3301 |
token_count += 1
|
| 3302 |
|
|
|
|
| 3304 |
# SHADOWS β W3C DTCG shadow format
|
| 3305 |
# =========================================================================
|
| 3306 |
if state.desktop_normalized and state.desktop_normalized.shadows:
|
| 3307 |
+
shadow_tier_names = ["xs", "sm", "md", "lg", "xl", "2xl"]
|
| 3308 |
sorted_shadows = sorted(
|
| 3309 |
state.desktop_normalized.shadows.items(),
|
| 3310 |
key=lambda x: _get_shadow_blur(x[1].value),
|
| 3311 |
)
|
| 3312 |
for i, (name, s) in enumerate(sorted_shadows):
|
| 3313 |
+
size_name = shadow_tier_names[i] if i < len(shadow_tier_names) else str(i + 1)
|
| 3314 |
flat_key = f"shadow.{size_name}"
|
|
|
|
| 3315 |
parsed = _parse_shadow_to_tokens_studio(s.value)
|
| 3316 |
dtcg_shadow_value = _shadow_to_dtcg(parsed)
|
| 3317 |
+
ext = {"frequency": s.frequency, "rawCSS": s.value}
|
| 3318 |
+
if hasattr(s, 'blur_px') and s.blur_px is not None:
|
| 3319 |
+
ext["blurPx"] = s.blur_px
|
| 3320 |
+
dtcg_token = _to_dtcg_token(dtcg_shadow_value, "shadow",
|
| 3321 |
+
description=f"Elevation {size_name}",
|
| 3322 |
+
extensions=ext)
|
| 3323 |
_flat_key_to_nested(flat_key, dtcg_token, result)
|
| 3324 |
token_count += 1
|
| 3325 |
|
|
|
|
| 3609 |
token_count += 1
|
| 3610 |
|
| 3611 |
# =========================================================================
|
| 3612 |
+
# SHADOWS β W3C DTCG format β always produce 5 elevation levels (xsβxl)
|
| 3613 |
+
# Interpolates between extracted shadows to fill missing levels.
|
| 3614 |
# =========================================================================
|
| 3615 |
+
TARGET_SHADOW_COUNT = 5
|
| 3616 |
+
shadow_names = ["shadow.xs", "shadow.sm", "shadow.md", "shadow.lg", "shadow.xl"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3617 |
|
| 3618 |
if state.desktop_normalized and state.desktop_normalized.shadows:
|
| 3619 |
sorted_shadows = sorted(
|
|
|
|
| 3633 |
"color": p.get("color", "rgba(0,0,0,0.25)"),
|
| 3634 |
})
|
| 3635 |
|
| 3636 |
+
# Interpolation helpers
|
| 3637 |
+
def _lerp(a, b, t):
|
| 3638 |
+
return a + (b - a) * t
|
| 3639 |
+
|
| 3640 |
+
def _lerp_shadow(s1, s2, t):
|
| 3641 |
+
"""Interpolate between two shadow dicts at factor t (0.0=s1, 1.0=s2)."""
|
| 3642 |
+
import re
|
| 3643 |
+
interp = {
|
| 3644 |
+
"x": round(_lerp(s1["x"], s2["x"], t), 1),
|
| 3645 |
+
"y": round(_lerp(s1["y"], s2["y"], t), 1),
|
| 3646 |
+
"blur": round(_lerp(s1["blur"], s2["blur"], t), 1),
|
| 3647 |
+
"spread": round(_lerp(s1["spread"], s2["spread"], t), 1),
|
| 3648 |
+
}
|
| 3649 |
+
alpha1, alpha2 = 0.25, 0.25
|
| 3650 |
+
m1 = re.search(r'rgba?\([^)]*,\s*([\d.]+)\)', s1["color"])
|
| 3651 |
+
m2 = re.search(r'rgba?\([^)]*,\s*([\d.]+)\)', s2["color"])
|
| 3652 |
+
if m1: alpha1 = float(m1.group(1))
|
| 3653 |
+
if m2: alpha2 = float(m2.group(1))
|
| 3654 |
+
interp_alpha = round(_lerp(alpha1, alpha2, t), 3)
|
| 3655 |
+
interp["color"] = f"rgba(0, 0, 0, {interp_alpha})"
|
| 3656 |
+
return interp
|
| 3657 |
+
|
| 3658 |
+
final_shadows = []
|
| 3659 |
n = len(parsed_shadows)
|
| 3660 |
+
if n >= TARGET_SHADOW_COUNT:
|
| 3661 |
+
final_shadows = parsed_shadows[:TARGET_SHADOW_COUNT]
|
| 3662 |
+
elif n == 1:
|
| 3663 |
+
base = parsed_shadows[0]
|
| 3664 |
+
for i in range(TARGET_SHADOW_COUNT):
|
| 3665 |
+
factor = (i + 1) / 3.0
|
| 3666 |
+
final_shadows.append({
|
| 3667 |
+
"x": round(base["x"] * factor, 1),
|
| 3668 |
+
"y": round(max(1, base["y"] * factor), 1),
|
| 3669 |
+
"blur": round(max(1, base["blur"] * factor), 1),
|
| 3670 |
+
"spread": round(base["spread"] * factor, 1),
|
| 3671 |
+
"color": f"rgba(0, 0, 0, {round(0.04 + i * 0.04, 3)})",
|
| 3672 |
+
})
|
| 3673 |
+
elif n >= 2:
|
| 3674 |
+
for i in range(TARGET_SHADOW_COUNT):
|
| 3675 |
+
t = i / (TARGET_SHADOW_COUNT - 1)
|
| 3676 |
+
src_pos = t * (n - 1)
|
| 3677 |
+
lo = int(src_pos)
|
| 3678 |
+
hi = min(lo + 1, n - 1)
|
| 3679 |
+
frac = src_pos - lo
|
| 3680 |
+
final_shadows.append(_lerp_shadow(parsed_shadows[lo], parsed_shadows[hi], frac))
|
| 3681 |
|
| 3682 |
for i, shadow in enumerate(final_shadows):
|
| 3683 |
+
token_name = shadow_names[i] if i < len(shadow_names) else f"shadow.{i + 1}"
|
| 3684 |
dtcg_value = {
|
| 3685 |
"color": shadow["color"],
|
| 3686 |
"offsetX": f"{shadow['x']}px",
|
output_json/figma-plugin-extracted/figma-design-token-creator 5/src/code.js
CHANGED
|
@@ -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,301 +807,499 @@ 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 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 |
-
//
|
| 810 |
-
|
| 811 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 812 |
}
|
| 813 |
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
|
| 817 |
-
|
| 818 |
-
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
headingStyle = 'Bold';
|
| 825 |
-
} catch (e) {
|
| 826 |
-
console.warn('Inter Bold not available, using Regular for headings');
|
| 827 |
}
|
| 828 |
|
| 829 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 830 |
if (tokens.colors.length > 0) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 831 |
// Section title
|
| 832 |
-
|
| 833 |
-
|
| 834 |
-
|
| 835 |
-
|
| 836 |
-
|
| 837 |
-
|
| 838 |
-
|
| 839 |
-
|
| 840 |
-
|
| 841 |
-
|
| 842 |
-
|
| 843 |
-
|
| 844 |
-
|
| 845 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 846 |
|
| 847 |
-
|
| 848 |
-
|
| 849 |
-
|
| 850 |
-
|
| 851 |
-
|
| 852 |
-
|
| 853 |
-
|
| 854 |
-
|
| 855 |
-
|
| 856 |
-
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
|
| 860 |
-
|
| 861 |
-
|
| 862 |
-
|
| 863 |
-
|
| 864 |
-
|
| 865 |
-
|
| 866 |
-
|
| 867 |
-
|
| 868 |
-
|
| 869 |
-
|
| 870 |
-
|
| 871 |
-
|
| 872 |
-
|
| 873 |
-
|
| 874 |
-
|
| 875 |
-
|
| 876 |
-
|
| 877 |
-
|
| 878 |
-
|
| 879 |
-
|
| 880 |
-
|
| 881 |
-
|
| 882 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 883 |
}
|
| 884 |
|
| 885 |
-
|
| 886 |
-
|
| 887 |
}
|
| 888 |
|
| 889 |
-
//
|
| 890 |
-
//
|
| 891 |
-
//
|
| 892 |
-
|
| 893 |
-
|
| 894 |
-
|
| 895 |
-
|
| 896 |
-
|
| 897 |
-
|
| 898 |
-
|
| 899 |
-
|
| 900 |
-
|
| 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 |
-
|
| 908 |
-
|
| 909 |
-
|
| 910 |
-
|
| 911 |
-
|
| 912 |
-
|
| 913 |
-
|
| 914 |
-
|
| 915 |
-
|
| 916 |
-
|
| 917 |
-
|
| 918 |
-
|
| 919 |
-
|
| 920 |
-
|
| 921 |
-
|
| 922 |
-
|
| 923 |
-
|
| 924 |
-
|
| 925 |
-
|
| 926 |
-
|
| 927 |
-
|
| 928 |
-
|
| 929 |
-
|
| 930 |
-
|
| 931 |
-
|
| 932 |
-
|
| 933 |
-
|
| 934 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 935 |
}
|
| 936 |
|
| 937 |
-
|
|
|
|
| 938 |
}
|
| 939 |
|
| 940 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 941 |
if (tokens.spacing.length > 0) {
|
| 942 |
-
var
|
| 943 |
-
|
| 944 |
-
|
| 945 |
-
|
| 946 |
-
|
| 947 |
-
|
| 948 |
-
|
| 949 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 978 |
}
|
| 979 |
|
| 980 |
-
//
|
|
|
|
|
|
|
| 981 |
if (tokens.borderRadius.length > 0) {
|
| 982 |
-
var
|
| 983 |
-
|
| 984 |
-
|
| 985 |
-
|
| 986 |
-
|
| 987 |
-
|
| 988 |
-
|
| 989 |
-
|
| 990 |
-
|
| 991 |
-
var radiusX = xOffset;
|
| 992 |
for (var ri = 0; ri < tokens.borderRadius.length; ri++) {
|
| 993 |
-
var
|
| 994 |
-
var
|
| 995 |
|
| 996 |
-
// Rounded rectangle
|
| 997 |
var rect = figma.createRectangle();
|
| 998 |
-
rect.resize(
|
| 999 |
-
rect.x =
|
| 1000 |
-
rect.
|
| 1001 |
-
rect.
|
| 1002 |
-
rect.
|
| 1003 |
-
rect.strokes = [{ type: 'SOLID', color: { r: 0.8, g: 0.8, b: 0.8 } }];
|
| 1004 |
rect.strokeWeight = 2;
|
| 1005 |
-
|
| 1006 |
-
|
| 1007 |
-
//
|
| 1008 |
-
|
| 1009 |
-
|
| 1010 |
-
|
| 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 |
-
|
|
|
|
| 1021 |
}
|
| 1022 |
|
| 1023 |
-
//
|
|
|
|
|
|
|
| 1024 |
if (tokens.shadows.length > 0) {
|
| 1025 |
-
var
|
| 1026 |
-
|
| 1027 |
-
|
| 1028 |
-
|
| 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++) {
|
| 1036 |
-
var shadowToken = tokens.shadows[shi];
|
| 1037 |
-
var sv = shadowToken.value;
|
| 1038 |
|
| 1039 |
-
|
| 1040 |
-
|
| 1041 |
-
|
| 1042 |
-
var spreadVal = parseNumericValue(sv.spread || '0');
|
| 1043 |
-
var shColor = parseColorToRGBA(sv.color || 'rgba(0,0,0,0.25)');
|
| 1044 |
|
| 1045 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1046 |
var card = figma.createRectangle();
|
| 1047 |
-
card.resize(
|
| 1048 |
-
card.x =
|
| 1049 |
-
card.
|
| 1050 |
-
card.cornerRadius = 8;
|
| 1051 |
card.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }];
|
| 1052 |
card.effects = [{
|
| 1053 |
type: 'DROP_SHADOW',
|
| 1054 |
-
color: { r:
|
| 1055 |
-
offset: { x:
|
| 1056 |
-
radius:
|
| 1057 |
-
spread:
|
| 1058 |
visible: true,
|
| 1059 |
blendMode: 'NORMAL'
|
| 1060 |
}];
|
| 1061 |
-
|
| 1062 |
-
|
| 1063 |
-
//
|
| 1064 |
-
|
| 1065 |
-
|
| 1066 |
-
|
| 1067 |
-
|
| 1068 |
-
|
| 1069 |
-
|
| 1070 |
-
|
| 1071 |
-
|
| 1072 |
-
shadowX += 110;
|
| 1073 |
}
|
| 1074 |
}
|
| 1075 |
|
| 1076 |
-
|
| 1077 |
-
|
| 1078 |
-
|
| 1079 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1080 |
|
| 1081 |
} catch (error) {
|
| 1082 |
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 |
+
// Collect all frames β position them at the very end
|
| 862 |
+
var allFrames = [];
|
| 863 |
+
|
| 864 |
+
// ββ Helper: create a section FRAME (positioned later) ββ
|
| 865 |
+
function createSectionFrame(name, contentHeight) {
|
| 866 |
+
var frame = figma.createFrame();
|
| 867 |
+
frame.name = name;
|
| 868 |
+
frame.resize(FRAME_W, contentHeight);
|
| 869 |
+
frame.x = 0;
|
| 870 |
+
frame.y = 0; // will be repositioned at the end
|
| 871 |
+
frame.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }];
|
| 872 |
+
frame.clipsContent = false;
|
| 873 |
+
specPage.appendChild(frame);
|
| 874 |
+
allFrames.push(frame);
|
| 875 |
+
return frame;
|
| 876 |
}
|
| 877 |
|
| 878 |
+
// ββ Helper: add text to a frame ββ
|
| 879 |
+
function addText(frame, text, font, size, x, y, color) {
|
| 880 |
+
var t = figma.createText();
|
| 881 |
+
t.fontName = font;
|
| 882 |
+
t.fontSize = size;
|
| 883 |
+
t.characters = text;
|
| 884 |
+
t.fills = [{ type: 'SOLID', color: color || { r: 0.1, g: 0.1, b: 0.1 } }];
|
| 885 |
+
t.x = x; t.y = y;
|
| 886 |
+
frame.appendChild(t);
|
| 887 |
+
return t;
|
|
|
|
|
|
|
|
|
|
| 888 |
}
|
| 889 |
|
| 890 |
+
var MUTED = { r: 0.5, g: 0.5, b: 0.5 };
|
| 891 |
+
var DARK = { r: 0.12, g: 0.12, b: 0.12 };
|
| 892 |
+
var BLUE = { r: 0.15, g: 0.45, b: 0.95 };
|
| 893 |
+
|
| 894 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 895 |
+
// TITLE FRAME
|
| 896 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 897 |
+
var titleFrame = createSectionFrame('Title', 120);
|
| 898 |
+
addText(titleFrame, 'Design System Specification', boldFont, 36, MARGIN, 32, DARK);
|
| 899 |
+
addText(titleFrame, 'Font: ' + sampleFontFamily, labelFont, 14, MARGIN, 80, MUTED);
|
| 900 |
+
|
| 901 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 902 |
+
// COLORS FRAME β grouped by category with AA badges
|
| 903 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 904 |
if (tokens.colors.length > 0) {
|
| 905 |
+
// Group colors by category
|
| 906 |
+
var colorGroups = {};
|
| 907 |
+
var groupOrder = [];
|
| 908 |
+
for (var ci = 0; ci < tokens.colors.length; ci++) {
|
| 909 |
+
var cat = tokens.colors[ci].name.split('/')[0] || 'other';
|
| 910 |
+
if (!colorGroups[cat]) { colorGroups[cat] = []; groupOrder.push(cat); }
|
| 911 |
+
colorGroups[cat].push(tokens.colors[ci]);
|
| 912 |
+
}
|
| 913 |
+
// Sort: semantic first, then palette
|
| 914 |
+
var semOrder = ['brand', 'text', 'bg', 'background', 'border', 'feedback'];
|
| 915 |
+
var sortedGroups = [];
|
| 916 |
+
for (var so = 0; so < semOrder.length; so++) { if (colorGroups[semOrder[so]]) sortedGroups.push(semOrder[so]); }
|
| 917 |
+
for (var go = 0; go < groupOrder.length; go++) { if (sortedGroups.indexOf(groupOrder[go]) === -1) sortedGroups.push(groupOrder[go]); }
|
| 918 |
+
|
| 919 |
+
// Calculate initial height estimate (will be resized at end)
|
| 920 |
+
var colorsPerRow = Math.floor((CONTENT_W - 140 + SWATCH_GAP) / (SWATCH_W + SWATCH_GAP));
|
| 921 |
+
var totalColorH = 2000; // generous initial estimate, resized later
|
| 922 |
+
|
| 923 |
+
var colorFrame = createSectionFrame('Colors', totalColorH);
|
| 924 |
+
var cy = SECTION_PAD;
|
| 925 |
+
|
| 926 |
// Section title
|
| 927 |
+
addText(colorFrame, 'COLORS', boldFont, 32, MARGIN, cy, DARK);
|
| 928 |
+
cy += 48;
|
| 929 |
+
|
| 930 |
+
// Helper to render a single color card at position
|
| 931 |
+
function renderColorCard(frame, ct, sx, sy) {
|
| 932 |
+
var swatch = figma.createRectangle();
|
| 933 |
+
swatch.resize(SWATCH_W, SWATCH_H_COLOR);
|
| 934 |
+
swatch.x = sx; swatch.y = sy;
|
| 935 |
+
swatch.topLeftRadius = 8; swatch.topRightRadius = 8;
|
| 936 |
+
swatch.fills = [{ type: 'SOLID', color: hexToRgb(ct.value) }];
|
| 937 |
+
frame.appendChild(swatch);
|
| 938 |
+
|
| 939 |
+
// AA badge
|
| 940 |
+
var aa = getAAResult(ct.value);
|
| 941 |
+
var badgeText = aa.passAA ? 'AA β ' + aa.ratio + ':1' : 'AA β ' + aa.ratio + ':1';
|
| 942 |
+
var badgeColor = aa.bestOn === 'white' ? { r: 1, g: 1, b: 1 } : { r: 0, g: 0, b: 0 };
|
| 943 |
+
addText(frame, badgeText, labelFont, 10, sx + 8, sy + SWATCH_H_COLOR - 18, badgeColor);
|
| 944 |
+
|
| 945 |
+
// Metadata area
|
| 946 |
+
var metaBg = figma.createRectangle();
|
| 947 |
+
metaBg.resize(SWATCH_W, SWATCH_META_H);
|
| 948 |
+
metaBg.x = sx; metaBg.y = sy + SWATCH_H_COLOR;
|
| 949 |
+
metaBg.bottomLeftRadius = 8; metaBg.bottomRightRadius = 8;
|
| 950 |
+
metaBg.fills = [{ type: 'SOLID', color: { r: 0.97, g: 0.97, b: 0.98 } }];
|
| 951 |
+
metaBg.strokes = [{ type: 'SOLID', color: { r: 0.9, g: 0.9, b: 0.9 } }];
|
| 952 |
+
metaBg.strokeWeight = 1;
|
| 953 |
+
frame.appendChild(metaBg);
|
| 954 |
+
|
| 955 |
+
var nameParts = ct.name.split('/');
|
| 956 |
+
var displayName = nameParts.filter(function(p) { return p !== 'DEFAULT'; }).join('/');
|
| 957 |
+
addText(frame, displayName, boldFont, 11, sx + 10, sy + SWATCH_H_COLOR + 8, DARK);
|
| 958 |
+
addText(frame, ct.value.toUpperCase(), labelFont, 11, sx + 10, sy + SWATCH_H_COLOR + 26, MUTED);
|
| 959 |
+
|
| 960 |
+
var contrastText = aa.passAA ? 'AA Pass on ' + aa.bestOn : 'AA Fail (' + aa.ratio + ':1)';
|
| 961 |
+
var contrastColor = aa.passAA ? { r: 0.13, g: 0.55, b: 0.13 } : { r: 0.85, g: 0.18, b: 0.18 };
|
| 962 |
+
addText(frame, contrastText, labelFont, 10, sx + 10, sy + SWATCH_H_COLOR + 44, contrastColor);
|
| 963 |
+
}
|
| 964 |
|
| 965 |
+
var isSemantic = { 'brand': 1, 'text': 1, 'bg': 1, 'background': 1, 'border': 1, 'feedback': 1 };
|
| 966 |
+
var CARD_H = SWATCH_H_COLOR + SWATCH_META_H + ITEM_GAP;
|
| 967 |
+
|
| 968 |
+
for (var gi = 0; gi < sortedGroups.length; gi++) {
|
| 969 |
+
var gName = sortedGroups[gi];
|
| 970 |
+
var gColors = colorGroups[gName];
|
| 971 |
+
|
| 972 |
+
// Group heading
|
| 973 |
+
addText(colorFrame, gName.charAt(0).toUpperCase() + gName.slice(1), boldFont, 18, MARGIN, cy, DARK);
|
| 974 |
+
cy += 36;
|
| 975 |
+
|
| 976 |
+
if (isSemantic[gName]) {
|
| 977 |
+
// ββ SEMANTIC LAYOUT: sub-group by 2nd path segment, each on own row ββ
|
| 978 |
+
// e.g. brand/primary/DEFAULT, brand/primary/50 β sub-group "primary"
|
| 979 |
+
// feedback/error/DEFAULT, feedback/info/DEFAULT β sub-groups "error", "info"
|
| 980 |
+
var subGroups = {};
|
| 981 |
+
var subOrder = [];
|
| 982 |
+
for (var si = 0; si < gColors.length; si++) {
|
| 983 |
+
var sp = gColors[si].name.split('/');
|
| 984 |
+
var subKey = sp.length > 1 ? sp[1] : sp[0];
|
| 985 |
+
if (!subGroups[subKey]) { subGroups[subKey] = []; subOrder.push(subKey); }
|
| 986 |
+
subGroups[subKey].push(gColors[si]);
|
| 987 |
+
}
|
| 988 |
+
|
| 989 |
+
for (var ski = 0; ski < subOrder.length; ski++) {
|
| 990 |
+
var subName = subOrder[ski];
|
| 991 |
+
var subColors = subGroups[subName];
|
| 992 |
+
|
| 993 |
+
// Row label on the left
|
| 994 |
+
addText(colorFrame, subName, boldFont, 13, MARGIN, cy + 30, MUTED);
|
| 995 |
+
|
| 996 |
+
// Swatches start after label
|
| 997 |
+
var labelW = 140;
|
| 998 |
+
for (var sci = 0; sci < subColors.length; sci++) {
|
| 999 |
+
var scx = MARGIN + labelW + sci * (SWATCH_W + SWATCH_GAP);
|
| 1000 |
+
renderColorCard(colorFrame, subColors[sci], scx, cy);
|
| 1001 |
+
}
|
| 1002 |
+
|
| 1003 |
+
cy += CARD_H + 8;
|
| 1004 |
+
}
|
| 1005 |
+
} else {
|
| 1006 |
+
// ββ PALETTE LAYOUT: horizontal grid (50-900 ramp) ββ
|
| 1007 |
+
for (var ci2 = 0; ci2 < gColors.length; ci2++) {
|
| 1008 |
+
var ct = gColors[ci2];
|
| 1009 |
+
var col = ci2 % colorsPerRow;
|
| 1010 |
+
var row = Math.floor(ci2 / colorsPerRow);
|
| 1011 |
+
var sx = MARGIN + col * (SWATCH_W + SWATCH_GAP);
|
| 1012 |
+
var sy = cy + row * CARD_H;
|
| 1013 |
+
renderColorCard(colorFrame, ct, sx, sy);
|
| 1014 |
+
}
|
| 1015 |
+
|
| 1016 |
+
var gRows = Math.ceil(gColors.length / colorsPerRow);
|
| 1017 |
+
cy += gRows * CARD_H + 16;
|
| 1018 |
+
}
|
| 1019 |
+
|
| 1020 |
+
cy += 8; // gap between groups
|
| 1021 |
}
|
| 1022 |
|
| 1023 |
+
// Resize color frame to actual content
|
| 1024 |
+
colorFrame.resize(FRAME_W, cy + SECTION_PAD);
|
| 1025 |
}
|
| 1026 |
|
| 1027 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1028 |
+
// TYPOGRAPHY DESKTOP FRAME
|
| 1029 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1030 |
+
var desktopTypo = [];
|
| 1031 |
+
var mobileTypo = [];
|
| 1032 |
+
for (var tti = 0; tti < tokens.typography.length; tti++) {
|
| 1033 |
+
if (tokens.typography[tti].name.toLowerCase().indexOf('mobile') > -1) {
|
| 1034 |
+
mobileTypo.push(tokens.typography[tti]);
|
| 1035 |
+
} else {
|
| 1036 |
+
desktopTypo.push(tokens.typography[tti]);
|
| 1037 |
+
}
|
| 1038 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1039 |
|
| 1040 |
+
function getSampleText(name) {
|
| 1041 |
+
var n = name.toLowerCase();
|
| 1042 |
+
if (n.indexOf('display') > -1) return 'The quick brown fox jumps over the lazy dog';
|
| 1043 |
+
if (n.indexOf('heading') > -1) return 'The quick brown fox jumps over';
|
| 1044 |
+
if (n.indexOf('body') > -1) return 'The quick brown fox jumps over the lazy dog. Pack my box with five dozen liquor jugs.';
|
| 1045 |
+
if (n.indexOf('caption') > -1) return 'The quick brown fox jumps over the lazy dog';
|
| 1046 |
+
if (n.indexOf('overline') > -1) return 'THE QUICK BROWN FOX JUMPS';
|
| 1047 |
+
return 'The quick brown fox jumps over the lazy dog';
|
| 1048 |
+
}
|
| 1049 |
+
|
| 1050 |
+
function renderTypoFrame(typoList, frameName) {
|
| 1051 |
+
if (typoList.length === 0) return;
|
| 1052 |
+
|
| 1053 |
+
// Use generous initial height β will be resized to actual content
|
| 1054 |
+
var estH = 3000;
|
| 1055 |
+
|
| 1056 |
+
var frame = createSectionFrame(frameName, estH);
|
| 1057 |
+
var fy = SECTION_PAD;
|
| 1058 |
+
|
| 1059 |
+
addText(frame, frameName.toUpperCase(), boldFont, 32, MARGIN, fy, DARK);
|
| 1060 |
+
fy += 52;
|
| 1061 |
+
|
| 1062 |
+
// Column headers
|
| 1063 |
+
var COL_NAME = MARGIN;
|
| 1064 |
+
var COL_SAMPLE = MARGIN + 180;
|
| 1065 |
+
var COL_SPECS = MARGIN + 760;
|
| 1066 |
+
|
| 1067 |
+
addText(frame, 'Token', boldFont, 12, COL_NAME, fy, MUTED);
|
| 1068 |
+
addText(frame, 'Sample', boldFont, 12, COL_SAMPLE, fy, MUTED);
|
| 1069 |
+
addText(frame, 'Specifications', boldFont, 12, COL_SPECS, fy, MUTED);
|
| 1070 |
+
fy += 28;
|
| 1071 |
+
|
| 1072 |
+
// Divider
|
| 1073 |
+
var div = figma.createRectangle();
|
| 1074 |
+
div.resize(CONTENT_W, 1); div.x = MARGIN; div.y = fy;
|
| 1075 |
+
div.fills = [{ type: 'SOLID', color: { r: 0.88, g: 0.88, b: 0.88 } }];
|
| 1076 |
+
frame.appendChild(div);
|
| 1077 |
+
fy += 16;
|
| 1078 |
+
|
| 1079 |
+
for (var ti = 0; ti < typoList.length; ti++) {
|
| 1080 |
+
var tt = typoList[ti];
|
| 1081 |
+
var val = tt.value;
|
| 1082 |
+
var fFamily = (val.fontFamily || 'Inter').split(',')[0].trim();
|
| 1083 |
+
var fSize = parseNumericValue(val.fontSize) || 16;
|
| 1084 |
+
var fWeight = val.fontWeight || '400';
|
| 1085 |
+
var fLH = val.lineHeight || '1.5';
|
| 1086 |
+
var displaySize = Math.min(fSize, 56);
|
| 1087 |
+
|
| 1088 |
+
// Token name column
|
| 1089 |
+
var tierParts = tt.name.split('/');
|
| 1090 |
+
var tierName = tierParts.filter(function(p) { return p !== 'desktop' && p !== 'mobile'; }).join('.');
|
| 1091 |
+
addText(frame, tierName, boldFont, 13, COL_NAME, fy + 4, DARK);
|
| 1092 |
+
|
| 1093 |
+
// Sample text in actual font
|
| 1094 |
+
var useBold = (tierName.indexOf('display') > -1 || tierName.indexOf('heading') > -1);
|
| 1095 |
+
var sampleText = getSampleText(tt.name);
|
| 1096 |
+
var sample = figma.createText();
|
| 1097 |
+
sample.fontName = useBold ? sampleFontBold : sampleFont;
|
| 1098 |
+
sample.fontSize = displaySize;
|
| 1099 |
+
sample.textAutoResize = 'HEIGHT';
|
| 1100 |
+
sample.resize(540, displaySize * 2);
|
| 1101 |
+
sample.characters = sampleText;
|
| 1102 |
+
sample.x = COL_SAMPLE; sample.y = fy;
|
| 1103 |
+
frame.appendChild(sample);
|
| 1104 |
+
|
| 1105 |
+
// Manual height calculation β sample.height is unreliable in plugin API
|
| 1106 |
+
var avgCharWidth = displaySize * 0.52;
|
| 1107 |
+
var sampleColW = 540;
|
| 1108 |
+
var charsPerLine = Math.max(1, Math.floor(sampleColW / avgCharWidth));
|
| 1109 |
+
var numLines = Math.ceil(sampleText.length / charsPerLine);
|
| 1110 |
+
var lineH = displaySize * 1.4;
|
| 1111 |
+
var sampleH = Math.max(numLines * lineH, displaySize * 1.5);
|
| 1112 |
+
|
| 1113 |
+
// Specs column β stacked chips
|
| 1114 |
+
var specY = fy;
|
| 1115 |
+
addText(frame, 'Size: ' + fSize + 'px', boldFont, 12, COL_SPECS, specY, DARK);
|
| 1116 |
+
specY += 18;
|
| 1117 |
+
addText(frame, 'Weight: ' + fWeight, labelFont, 12, COL_SPECS, specY, MUTED);
|
| 1118 |
+
specY += 18;
|
| 1119 |
+
addText(frame, 'Line Height: ' + fLH, labelFont, 12, COL_SPECS, specY, MUTED);
|
| 1120 |
+
specY += 18;
|
| 1121 |
+
addText(frame, 'Font: ' + fFamily, labelFont, 12, COL_SPECS, specY, BLUE);
|
| 1122 |
+
|
| 1123 |
+
// Row height = max of (sample text height, spec labels height, minimum 40px)
|
| 1124 |
+
var specsH = specY - fy + 24;
|
| 1125 |
+
var rowH = Math.max(sampleH + 12, specsH, 40);
|
| 1126 |
+
fy += rowH + 8;
|
| 1127 |
+
|
| 1128 |
+
// Row separator
|
| 1129 |
+
var sep = figma.createRectangle();
|
| 1130 |
+
sep.resize(CONTENT_W, 1); sep.x = MARGIN; sep.y = fy;
|
| 1131 |
+
sep.fills = [{ type: 'SOLID', color: { r: 0.94, g: 0.94, b: 0.94 } }];
|
| 1132 |
+
frame.appendChild(sep);
|
| 1133 |
+
fy += 12;
|
| 1134 |
}
|
| 1135 |
|
| 1136 |
+
// Resize frame to actual content
|
| 1137 |
+
frame.resize(FRAME_W, fy + SECTION_PAD);
|
| 1138 |
}
|
| 1139 |
|
| 1140 |
+
renderTypoFrame(desktopTypo, 'Typography β Desktop');
|
| 1141 |
+
renderTypoFrame(mobileTypo, 'Typography β Mobile');
|
| 1142 |
+
|
| 1143 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1144 |
+
// SPACING FRAME β Desktop & Mobile side by side, bars not squares
|
| 1145 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1146 |
if (tokens.spacing.length > 0) {
|
| 1147 |
+
var dSpacing = [];
|
| 1148 |
+
var mSpacing = [];
|
| 1149 |
+
for (var spi = 0; spi < tokens.spacing.length; spi++) {
|
| 1150 |
+
if (tokens.spacing[spi].name.toLowerCase().indexOf('mobile') > -1) {
|
| 1151 |
+
mSpacing.push(tokens.spacing[spi]);
|
| 1152 |
+
} else {
|
| 1153 |
+
dSpacing.push(tokens.spacing[spi]);
|
| 1154 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1155 |
}
|
| 1156 |
|
| 1157 |
+
var maxItems = Math.max(dSpacing.length, mSpacing.length);
|
| 1158 |
+
var spFrameH = SECTION_PAD + 60 + maxItems * 48 + SECTION_PAD;
|
| 1159 |
+
|
| 1160 |
+
var spFrame = createSectionFrame('Spacing', spFrameH);
|
| 1161 |
+
var spy = SECTION_PAD;
|
| 1162 |
+
|
| 1163 |
+
addText(spFrame, 'SPACING', boldFont, 32, MARGIN, spy, DARK);
|
| 1164 |
+
spy += 52;
|
| 1165 |
+
|
| 1166 |
+
// Render spacing column as horizontal bars (Carbon-style)
|
| 1167 |
+
function renderSpacingBars(list, startX, title, y0) {
|
| 1168 |
+
addText(spFrame, title, boldFont, 16, startX, y0, BLUE);
|
| 1169 |
+
var ly = y0 + 32;
|
| 1170 |
+
|
| 1171 |
+
for (var si = 0; si < Math.min(list.length, 12); si++) {
|
| 1172 |
+
var sp = list[si];
|
| 1173 |
+
var spVal = parseNumericValue(sp.value);
|
| 1174 |
+
var barW = Math.max(spVal * 3, 8); // Scale 3x for visibility
|
| 1175 |
+
barW = Math.min(barW, 400); // Cap
|
| 1176 |
+
|
| 1177 |
+
// Token label
|
| 1178 |
+
var spParts = sp.name.split('/');
|
| 1179 |
+
var spDisplayName = spParts.filter(function(p) { return p !== 'desktop' && p !== 'mobile'; }).join('/');
|
| 1180 |
+
addText(spFrame, spDisplayName, labelFont, 12, startX, ly + 4, DARK);
|
| 1181 |
+
|
| 1182 |
+
// Bar
|
| 1183 |
+
var bar = figma.createRectangle();
|
| 1184 |
+
bar.resize(barW, 24);
|
| 1185 |
+
bar.x = startX + 120;
|
| 1186 |
+
bar.y = ly;
|
| 1187 |
+
bar.cornerRadius = 4;
|
| 1188 |
+
bar.fills = [{ type: 'SOLID', color: { r: 0.2, g: 0.55, b: 1 } }];
|
| 1189 |
+
spFrame.appendChild(bar);
|
| 1190 |
+
|
| 1191 |
+
// Value label
|
| 1192 |
+
addText(spFrame, sp.value, labelFont, 12, startX + 120 + barW + 12, ly + 4, MUTED);
|
| 1193 |
+
|
| 1194 |
+
ly += 40;
|
| 1195 |
+
}
|
| 1196 |
+
return ly;
|
| 1197 |
+
}
|
| 1198 |
+
|
| 1199 |
+
var dEnd = renderSpacingBars(dSpacing, MARGIN, 'π₯ Desktop', spy);
|
| 1200 |
+
var mEnd = renderSpacingBars(mSpacing, MARGIN + 560, 'π± Mobile', spy);
|
| 1201 |
+
|
| 1202 |
+
spFrame.resize(FRAME_W, Math.max(dEnd, mEnd) + SECTION_PAD);
|
| 1203 |
}
|
| 1204 |
|
| 1205 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1206 |
+
// BORDER RADIUS FRAME
|
| 1207 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1208 |
if (tokens.borderRadius.length > 0) {
|
| 1209 |
+
var radFrameH = SECTION_PAD + 60 + 120 + SECTION_PAD;
|
| 1210 |
+
var radFrame = createSectionFrame('Border Radius', radFrameH);
|
| 1211 |
+
var ry = SECTION_PAD;
|
| 1212 |
+
|
| 1213 |
+
addText(radFrame, 'BORDER RADIUS', boldFont, 32, MARGIN, ry, DARK);
|
| 1214 |
+
ry += 52;
|
| 1215 |
+
|
| 1216 |
+
var rx = MARGIN;
|
|
|
|
|
|
|
| 1217 |
for (var ri = 0; ri < tokens.borderRadius.length; ri++) {
|
| 1218 |
+
var rToken = tokens.borderRadius[ri];
|
| 1219 |
+
var rVal = parseNumericValue(rToken.value);
|
| 1220 |
|
|
|
|
| 1221 |
var rect = figma.createRectangle();
|
| 1222 |
+
rect.resize(64, 64);
|
| 1223 |
+
rect.x = rx; rect.y = ry;
|
| 1224 |
+
rect.cornerRadius = Math.min(rVal, 32);
|
| 1225 |
+
rect.fills = [{ type: 'SOLID', color: { r: 0.93, g: 0.93, b: 0.96 } }];
|
| 1226 |
+
rect.strokes = [{ type: 'SOLID', color: { r: 0.75, g: 0.75, b: 0.82 } }];
|
|
|
|
| 1227 |
rect.strokeWeight = 2;
|
| 1228 |
+
radFrame.appendChild(rect);
|
| 1229 |
+
|
| 1230 |
+
// Name
|
| 1231 |
+
addText(radFrame, rToken.name.split('/').pop(), boldFont, 11, rx, ry + 72, DARK);
|
| 1232 |
+
// Value
|
| 1233 |
+
addText(radFrame, rToken.value, labelFont, 11, rx, ry + 88, MUTED);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1234 |
|
| 1235 |
+
rx += 90;
|
| 1236 |
+
}
|
| 1237 |
}
|
| 1238 |
|
| 1239 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1240 |
+
// SHADOWS FRAME β on light gray background (MD pattern)
|
| 1241 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1242 |
if (tokens.shadows.length > 0) {
|
| 1243 |
+
var shFrameH = SECTION_PAD + 60 + 200 + SECTION_PAD;
|
| 1244 |
+
var shFrame = createSectionFrame('Shadows', shFrameH);
|
| 1245 |
+
// Light gray background for shadow visibility
|
| 1246 |
+
shFrame.fills = [{ type: 'SOLID', color: { r: 0.96, g: 0.96, b: 0.96 } }];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1247 |
|
| 1248 |
+
var shy = SECTION_PAD;
|
| 1249 |
+
addText(shFrame, 'SHADOWS / ELEVATION', boldFont, 32, MARGIN, shy, DARK);
|
| 1250 |
+
shy += 52;
|
|
|
|
|
|
|
| 1251 |
|
| 1252 |
+
var shx = MARGIN;
|
| 1253 |
+
for (var shi = 0; shi < tokens.shadows.length; shi++) {
|
| 1254 |
+
var shToken = tokens.shadows[shi];
|
| 1255 |
+
var sv = shToken.value;
|
| 1256 |
+
var oxV = parseNumericValue(sv.offsetX || sv.x || '0');
|
| 1257 |
+
var oyV = parseNumericValue(sv.offsetY || sv.y || '0');
|
| 1258 |
+
var blV = parseNumericValue(sv.blur || '0');
|
| 1259 |
+
var spV = parseNumericValue(sv.spread || '0');
|
| 1260 |
+
var shC = parseColorToRGBA(sv.color || 'rgba(0,0,0,0.25)');
|
| 1261 |
+
|
| 1262 |
+
// White card with shadow
|
| 1263 |
var card = figma.createRectangle();
|
| 1264 |
+
card.resize(140, 140);
|
| 1265 |
+
card.x = shx; card.y = shy;
|
| 1266 |
+
card.cornerRadius = 12;
|
|
|
|
| 1267 |
card.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }];
|
| 1268 |
card.effects = [{
|
| 1269 |
type: 'DROP_SHADOW',
|
| 1270 |
+
color: { r: shC.r, g: shC.g, b: shC.b, a: shC.a },
|
| 1271 |
+
offset: { x: oxV, y: oyV },
|
| 1272 |
+
radius: blV,
|
| 1273 |
+
spread: spV,
|
| 1274 |
visible: true,
|
| 1275 |
blendMode: 'NORMAL'
|
| 1276 |
}];
|
| 1277 |
+
shFrame.appendChild(card);
|
| 1278 |
+
|
| 1279 |
+
// Level name centered on card
|
| 1280 |
+
addText(shFrame, shToken.name.split('/').pop(), boldFont, 14, shx + 20, shy + 30, DARK);
|
| 1281 |
+
|
| 1282 |
+
// Specs on card
|
| 1283 |
+
addText(shFrame, 'blur: ' + blV + 'px', labelFont, 11, shx + 20, shy + 56, MUTED);
|
| 1284 |
+
addText(shFrame, 'y: ' + oyV + 'px', labelFont, 11, shx + 20, shy + 72, MUTED);
|
| 1285 |
+
addText(shFrame, 'spread: ' + spV + 'px', labelFont, 11, shx + 20, shy + 88, MUTED);
|
| 1286 |
+
|
| 1287 |
+
shx += 180;
|
|
|
|
| 1288 |
}
|
| 1289 |
}
|
| 1290 |
|
| 1291 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1292 |
+
// POSITION ALL FRAMES β stack HORIZONTALLY with proper spacing
|
| 1293 |
+
// This runs AFTER all frames are created and resized to actual content.
|
| 1294 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1295 |
+
var stackX = 0;
|
| 1296 |
+
for (var fi = 0; fi < allFrames.length; fi++) {
|
| 1297 |
+
allFrames[fi].x = stackX;
|
| 1298 |
+
allFrames[fi].y = 0;
|
| 1299 |
+
stackX += allFrames[fi].width + SECTION_GAP;
|
| 1300 |
+
}
|
| 1301 |
+
|
| 1302 |
+
figma.ui.postMessage({ type: 'spec-complete', message: 'Visual spec created with ' + allFrames.length + ' section frames!' });
|
| 1303 |
|
| 1304 |
} catch (error) {
|
| 1305 |
console.error('Error creating visual spec:', error);
|
storage/benchmark_cache.json
CHANGED
|
@@ -15,7 +15,7 @@
|
|
| 15 |
"colors": {},
|
| 16 |
"radius": {},
|
| 17 |
"shadows": {},
|
| 18 |
-
"fetched_at": "2026-02-
|
| 19 |
"confidence": "high",
|
| 20 |
"best_for": []
|
| 21 |
}
|
|
|
|
| 15 |
"colors": {},
|
| 16 |
"radius": {},
|
| 17 |
"shadows": {},
|
| 18 |
+
"fetched_at": "2026-02-22T17:56:25.598103",
|
| 19 |
"confidence": "high",
|
| 20 |
"best_for": []
|
| 21 |
}
|