Spaces:
Runtime error
v3.1: Rule-based color classifier — kill LLM naming
Browse files- NEW core/color_classifier.py: 100% deterministic color classification
- Aggressive dedup (RGB dist < 30 within same category)
- Capped categories: brand(3), text(3), bg(3), border(3), feedback(4)
- Supports semantic/tailwind/material naming conventions
- Every decision logged with evidence
- app.py: Convention picker dropdown + preview button in Stage 3
- Export functions now use classifier instead of LLM naming_map
- Removed _get_semantic_color_overrides() / _consolidate_colors() dependency
- AURORA demoted to advisory-only (no more naming_map output)
- CLAUDE.md updated with v3.1 architecture
Fixes: "nonary/decenary" Latin ordinal naming, raw hex keys,
mixed conventions, 3-way naming merge chaos
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- CLAUDE.md +38 -4
- agents/llm_agents.py +10 -12
- app.py +83 -27
- core/color_classifier.py +678 -0
- output_json/file (18).json +584 -0
|
@@ -1,15 +1,49 @@
|
|
| 1 |
-
# Design System Extractor
|
| 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 (free, no LLM)
|
| 8 |
-
2. **Stage 2 (LLM-powered)**: Brand
|
| 9 |
|
| 10 |
---
|
| 11 |
|
| 12 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
### What's Wrong (observed from real site tests)
|
| 15 |
|
|
|
|
| 1 |
+
# Design System Extractor v3.1 — 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 → Rule Engine analysis → **Rule-Based Color Classification** (free, no LLM)
|
| 8 |
+
2. **Stage 2 (LLM-powered, advisory only)**: Brand insights → Benchmark comparison → Best practices → Final synthesis
|
| 9 |
|
| 10 |
---
|
| 11 |
|
| 12 |
+
## v3.1 FIX: RULE-BASED COLOR NAMING (Feb 2026)
|
| 13 |
+
|
| 14 |
+
### What Changed
|
| 15 |
+
- **KILLED LLM color naming entirely.** New `core/color_classifier.py` handles all color naming with 100% deterministic rules.
|
| 16 |
+
- **Aggressive deduplication**: Colors within RGB distance < 30 AND same category get merged (e.g., 13 text grays → 3)
|
| 17 |
+
- **Capped categories**: brand (max 3), text (max 3), bg (max 3), border (max 3), feedback (max 4), palette (remaining)
|
| 18 |
+
- **User-selectable naming convention**: semantic, tailwind, or material — chosen BEFORE export
|
| 19 |
+
- **Preview before export**: User sees classification + decision log before committing
|
| 20 |
+
- **Every decision logged**: `[DEDUP]`, `[CLASSIFY]`, `[CAP]`, `[NAME]` with evidence
|
| 21 |
+
|
| 22 |
+
### How Classification Works (No LLM)
|
| 23 |
+
```
|
| 24 |
+
CSS Evidence → Category:
|
| 25 |
+
background-color on <button> + saturated + freq>5 → BRAND
|
| 26 |
+
color on <p>/<span> + low saturation → TEXT
|
| 27 |
+
background-color on <div>/<body> + neutral → BG
|
| 28 |
+
border-color + low saturation → BORDER
|
| 29 |
+
red hue + sat>0.6 + low freq → FEEDBACK (error)
|
| 30 |
+
everything else → PALETTE (named by hue.shade)
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
### What AURORA Does Now (Advisory Only)
|
| 34 |
+
- Does NOT output naming_map
|
| 35 |
+
- Provides brand insights, palette strategy, cohesion score
|
| 36 |
+
- LLM reasoning is shown in logs but doesn't affect token names
|
| 37 |
+
|
| 38 |
+
### Files Changed in v3.1
|
| 39 |
+
- `core/color_classifier.py` — NEW: Rule-based classifier with dedup, caps, naming conventions
|
| 40 |
+
- `app.py` — Export functions use classifier instead of LLM naming; convention picker in UI
|
| 41 |
+
- `agents/llm_agents.py` — AURORA prompt updated to advisory-only
|
| 42 |
+
- `CLAUDE.md` — This documentation
|
| 43 |
+
|
| 44 |
+
---
|
| 45 |
+
|
| 46 |
+
## PREVIOUS STATUS (v3.0 and earlier): BROKEN — RETHINK COMPLETED
|
| 47 |
|
| 48 |
### What's Wrong (observed from real site tests)
|
| 49 |
|
|
@@ -303,34 +303,33 @@ def _extract_hexes(tokens: dict) -> list:
|
|
| 303 |
class BrandIdentifierAgent:
|
| 304 |
"""
|
| 305 |
AURORA — Senior Brand & Visual Identity Analyst.
|
| 306 |
-
|
|
|
|
| 307 |
Model: Qwen 72B · Temperature: 0.4
|
| 308 |
"""
|
| 309 |
|
| 310 |
SYSTEM_PROMPT = """You are AURORA, a Senior Brand & Visual Identity Analyst.
|
| 311 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 312 |
## REASONING FRAMEWORK (ReAct)
|
| 313 |
Structure your response with explicit reasoning steps.
|
| 314 |
For each area: THINK → ACT → OBSERVE → VERIFY.
|
| 315 |
|
| 316 |
## ANALYZE ALL TOKEN TYPES:
|
| 317 |
|
| 318 |
-
### 1. COLORS (
|
| 319 |
-
|
| 320 |
-
- Name EVERY color: color.{role}.{sub} or color.{hue}.{shade}
|
| 321 |
-
- Shades MUST be numeric (50-900), NEVER words (light/dark/base)
|
| 322 |
-
- Role colors: color.brand.primary, color.text.primary, color.bg.primary
|
| 323 |
-
- Palette colors: color.blue.500, color.neutral.200
|
| 324 |
-
|
| 325 |
-
### 2. TYPOGRAPHY — Identify heading vs body hierarchy, font pairing
|
| 326 |
### 3. SPACING — Identify grid system, note consistency
|
| 327 |
### 4. RADIUS — Identify radius strategy (sharp/rounded/pill)
|
| 328 |
### 5. SHADOWS — Identify elevation strategy, blur progression
|
| 329 |
|
| 330 |
## QUALITY RULES
|
| 331 |
-
- naming_map MUST include EVERY hex color — no orphans
|
| 332 |
- Brand Primary MUST cite usage evidence (e.g. "47x on buttons")
|
| 333 |
- Cohesion 1-10: most sites score 5-7. Use the full range.
|
|
|
|
| 334 |
|
| 335 |
## OUTPUT (JSON)
|
| 336 |
|
|
@@ -339,7 +338,6 @@ For each area: THINK → ACT → OBSERVE → VERIFY.
|
|
| 339 |
{"step": "THINK", "area": "colors", "content": "..."},
|
| 340 |
{"step": "ACT", "area": "colors", "content": "..."},
|
| 341 |
{"step": "OBSERVE", "area": "typography", "content": "..."},
|
| 342 |
-
{"step": "ACT", "area": "typography", "content": "..."},
|
| 343 |
{"step": "ACT", "area": "spacing", "content": "..."},
|
| 344 |
{"step": "ACT", "area": "radius", "content": "..."},
|
| 345 |
{"step": "ACT", "area": "shadows", "content": "..."},
|
|
@@ -351,7 +349,7 @@ For each area: THINK → ACT → OBSERVE → VERIFY.
|
|
| 351 |
"palette_strategy": "complementary|analogous|triadic|monochromatic|random",
|
| 352 |
"cohesion_score": N,
|
| 353 |
"cohesion_notes": "...",
|
| 354 |
-
"naming_map": {
|
| 355 |
"typography_notes": "Heading: Inter 700, Body: Inter 400. Clean hierarchy.",
|
| 356 |
"spacing_notes": "8px grid, 92% aligned.",
|
| 357 |
"radius_notes": "Rounded style: 4px inputs, 8px cards.",
|
|
|
|
| 303 |
class BrandIdentifierAgent:
|
| 304 |
"""
|
| 305 |
AURORA — Senior Brand & Visual Identity Analyst.
|
| 306 |
+
v3.1: ADVISORY ONLY — does NOT name colors (rule-based classifier does that).
|
| 307 |
+
Provides brand insights, palette strategy, cohesion assessment.
|
| 308 |
Model: Qwen 72B · Temperature: 0.4
|
| 309 |
"""
|
| 310 |
|
| 311 |
SYSTEM_PROMPT = """You are AURORA, a Senior Brand & Visual Identity Analyst.
|
| 312 |
|
| 313 |
+
## YOUR ROLE (v3.1: Advisory Only)
|
| 314 |
+
Color NAMING is handled by a rule-based classifier. Do NOT output naming_map.
|
| 315 |
+
Your job is to provide INSIGHTS about the brand identity and design cohesion.
|
| 316 |
+
|
| 317 |
## REASONING FRAMEWORK (ReAct)
|
| 318 |
Structure your response with explicit reasoning steps.
|
| 319 |
For each area: THINK → ACT → OBSERVE → VERIFY.
|
| 320 |
|
| 321 |
## ANALYZE ALL TOKEN TYPES:
|
| 322 |
|
| 323 |
+
### 1. COLORS — Identify brand strategy (complementary? analogous? monochromatic?)
|
| 324 |
+
### 2. TYPOGRAPHY — Identify heading vs body hierarchy, font pairing quality
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 325 |
### 3. SPACING — Identify grid system, note consistency
|
| 326 |
### 4. RADIUS — Identify radius strategy (sharp/rounded/pill)
|
| 327 |
### 5. SHADOWS — Identify elevation strategy, blur progression
|
| 328 |
|
| 329 |
## QUALITY RULES
|
|
|
|
| 330 |
- Brand Primary MUST cite usage evidence (e.g. "47x on buttons")
|
| 331 |
- Cohesion 1-10: most sites score 5-7. Use the full range.
|
| 332 |
+
- Do NOT invent names. Focus on analysis and insights.
|
| 333 |
|
| 334 |
## OUTPUT (JSON)
|
| 335 |
|
|
|
|
| 338 |
{"step": "THINK", "area": "colors", "content": "..."},
|
| 339 |
{"step": "ACT", "area": "colors", "content": "..."},
|
| 340 |
{"step": "OBSERVE", "area": "typography", "content": "..."},
|
|
|
|
| 341 |
{"step": "ACT", "area": "spacing", "content": "..."},
|
| 342 |
{"step": "ACT", "area": "radius", "content": "..."},
|
| 343 |
{"step": "ACT", "area": "shadows", "content": "..."},
|
|
|
|
| 349 |
"palette_strategy": "complementary|analogous|triadic|monochromatic|random",
|
| 350 |
"cohesion_score": N,
|
| 351 |
"cohesion_notes": "...",
|
| 352 |
+
"naming_map": {},
|
| 353 |
"typography_notes": "Heading: Inter 700, Body: Inter 400. Clean hierarchy.",
|
| 354 |
"spacing_notes": "8px grid, 92% aligned.",
|
| 355 |
"radius_notes": "Rounded style: 4px inputs, 8px cards.",
|
|
@@ -42,6 +42,7 @@ class AppState:
|
|
| 42 |
self.mobile_normalized = None # NormalizedTokens
|
| 43 |
self.upgrade_recommendations = None # UpgradeRecommendations
|
| 44 |
self.selected_upgrades = {} # User selections
|
|
|
|
| 45 |
self.logs = []
|
| 46 |
|
| 47 |
def log(self, message: str):
|
|
@@ -3101,7 +3102,31 @@ def _consolidate_colors(colors_dict: dict, overrides: dict, max_colors: int = 30
|
|
| 3101 |
return result
|
| 3102 |
|
| 3103 |
|
| 3104 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3105 |
"""Export Stage 1 tokens (as-is extraction) to W3C DTCG format."""
|
| 3106 |
if not state.desktop_normalized:
|
| 3107 |
gr.Warning("No tokens extracted yet. Complete Stage 1 extraction first.")
|
|
@@ -3119,16 +3144,15 @@ def export_stage1_json():
|
|
| 3119 |
# COLORS — Nested structure with $value, $type, $description
|
| 3120 |
# =========================================================================
|
| 3121 |
if state.desktop_normalized and state.desktop_normalized.colors:
|
| 3122 |
-
|
| 3123 |
-
|
| 3124 |
-
state.desktop_normalized.colors,
|
|
|
|
|
|
|
| 3125 |
)
|
| 3126 |
-
for
|
| 3127 |
-
|
| 3128 |
-
|
| 3129 |
-
source_label = "LLM Semantic" if source == "semantic" else "Auto-Generated" if source == "detected" else "Extracted"
|
| 3130 |
-
dtcg_token = _to_dtcg_token(entry["value"], "color", description=source_label)
|
| 3131 |
-
_flat_key_to_nested(flat_key, dtcg_token, result)
|
| 3132 |
token_count += 1
|
| 3133 |
|
| 3134 |
# =========================================================================
|
|
@@ -3238,7 +3262,7 @@ def export_stage1_json():
|
|
| 3238 |
return json_str
|
| 3239 |
|
| 3240 |
|
| 3241 |
-
def export_tokens_json():
|
| 3242 |
"""Export final tokens with selected upgrades applied - FLAT structure for Figma Tokens Studio."""
|
| 3243 |
if not state.desktop_normalized:
|
| 3244 |
gr.Warning("No tokens extracted yet. Complete Stage 1 extraction first.")
|
|
@@ -3280,34 +3304,37 @@ def export_tokens_json():
|
|
| 3280 |
primary_font = fonts_info.get("primary", "sans-serif")
|
| 3281 |
|
| 3282 |
# =========================================================================
|
| 3283 |
-
# COLORS —
|
| 3284 |
# =========================================================================
|
| 3285 |
if state.desktop_normalized and state.desktop_normalized.colors:
|
| 3286 |
from core.color_utils import generate_color_ramp
|
|
|
|
| 3287 |
|
| 3288 |
-
|
| 3289 |
-
|
| 3290 |
-
|
|
|
|
| 3291 |
)
|
| 3292 |
|
| 3293 |
-
for
|
|
|
|
| 3294 |
if apply_ramps:
|
| 3295 |
try:
|
| 3296 |
-
ramp = generate_color_ramp(
|
| 3297 |
shades = ["50", "100", "200", "300", "400", "500", "600", "700", "800", "900", "950"]
|
| 3298 |
for i, shade in enumerate(shades):
|
| 3299 |
if i < len(ramp):
|
| 3300 |
shade_key = f"{flat_key}.{shade}"
|
| 3301 |
-
color_val = ramp[i] if isinstance(ramp[i], str) else ramp[i].get("hex",
|
| 3302 |
dtcg_token = _to_dtcg_token(color_val, "color")
|
| 3303 |
_flat_key_to_nested(shade_key, dtcg_token, result)
|
| 3304 |
token_count += 1
|
| 3305 |
except (ValueError, KeyError, TypeError, IndexError):
|
| 3306 |
-
dtcg_token = _to_dtcg_token(
|
| 3307 |
_flat_key_to_nested(flat_key, dtcg_token, result)
|
| 3308 |
token_count += 1
|
| 3309 |
else:
|
| 3310 |
-
dtcg_token = _to_dtcg_token(
|
| 3311 |
_flat_key_to_nested(flat_key, dtcg_token, result)
|
| 3312 |
token_count += 1
|
| 3313 |
|
|
@@ -4500,12 +4527,28 @@ def create_ui():
|
|
| 4500 |
gr.Markdown("Export your finalized design tokens as JSON, compatible with **Figma Tokens Studio**.",
|
| 4501 |
elem_classes=["section-desc"])
|
| 4502 |
gr.Markdown("""
|
| 4503 |
-
- **
|
| 4504 |
-
- **
|
| 4505 |
-
|
| 4506 |
-
Copy the JSON output below or save it as a `.json` file for import into Figma.
|
| 4507 |
""", elem_classes=["section-desc"])
|
| 4508 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4509 |
with gr.Row():
|
| 4510 |
export_stage1_btn = gr.Button("📥 Export Stage 1 (As-Is)", variant="secondary")
|
| 4511 |
export_final_btn = gr.Button("📥 Export Final (Upgraded)", variant="primary")
|
|
@@ -4514,9 +4557,22 @@ Copy the JSON output below or save it as a `.json` file for import into Figma.
|
|
| 4514 |
"Copy the contents or save as a `.json` file.*",
|
| 4515 |
elem_classes=["section-desc"])
|
| 4516 |
export_output = gr.Code(label="Tokens JSON", language="json", lines=25)
|
| 4517 |
-
|
| 4518 |
-
|
| 4519 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4520 |
|
| 4521 |
# =================================================================
|
| 4522 |
# EVENT HANDLERS
|
|
|
|
| 42 |
self.mobile_normalized = None # NormalizedTokens
|
| 43 |
self.upgrade_recommendations = None # UpgradeRecommendations
|
| 44 |
self.selected_upgrades = {} # User selections
|
| 45 |
+
self.color_classification = None # ClassificationResult from rule-based classifier
|
| 46 |
self.logs = []
|
| 47 |
|
| 48 |
def log(self, message: str):
|
|
|
|
| 3102 |
return result
|
| 3103 |
|
| 3104 |
|
| 3105 |
+
def preview_color_classification(convention: str = "semantic"):
|
| 3106 |
+
"""Preview color classification before export. 100% rule-based, no LLM."""
|
| 3107 |
+
from core.color_classifier import classify_colors, generate_classification_preview
|
| 3108 |
+
|
| 3109 |
+
if not state.desktop_normalized or not state.desktop_normalized.colors:
|
| 3110 |
+
return "⚠️ No colors extracted yet. Run Stage 1 extraction first."
|
| 3111 |
+
|
| 3112 |
+
result = classify_colors(
|
| 3113 |
+
state.desktop_normalized.colors,
|
| 3114 |
+
convention=convention or "semantic",
|
| 3115 |
+
log_callback=state.log,
|
| 3116 |
+
)
|
| 3117 |
+
|
| 3118 |
+
# Store for use by export functions
|
| 3119 |
+
state.color_classification = result
|
| 3120 |
+
|
| 3121 |
+
preview = generate_classification_preview(result)
|
| 3122 |
+
|
| 3123 |
+
# Also append the decision log
|
| 3124 |
+
log_section = "\n\n📋 DECISION LOG:\n" + "\n".join(result.log)
|
| 3125 |
+
|
| 3126 |
+
return preview + log_section
|
| 3127 |
+
|
| 3128 |
+
|
| 3129 |
+
def export_stage1_json(convention: str = "semantic"):
|
| 3130 |
"""Export Stage 1 tokens (as-is extraction) to W3C DTCG format."""
|
| 3131 |
if not state.desktop_normalized:
|
| 3132 |
gr.Warning("No tokens extracted yet. Complete Stage 1 extraction first.")
|
|
|
|
| 3144 |
# COLORS — Nested structure with $value, $type, $description
|
| 3145 |
# =========================================================================
|
| 3146 |
if state.desktop_normalized and state.desktop_normalized.colors:
|
| 3147 |
+
from core.color_classifier import classify_colors
|
| 3148 |
+
classification = classify_colors(
|
| 3149 |
+
state.desktop_normalized.colors,
|
| 3150 |
+
convention=convention or "semantic",
|
| 3151 |
+
log_callback=state.log,
|
| 3152 |
)
|
| 3153 |
+
for c in classification.colors:
|
| 3154 |
+
dtcg_token = _to_dtcg_token(c.hex, "color", description=f"Rule-based: {c.category}")
|
| 3155 |
+
_flat_key_to_nested(c.token_name, dtcg_token, result)
|
|
|
|
|
|
|
|
|
|
| 3156 |
token_count += 1
|
| 3157 |
|
| 3158 |
# =========================================================================
|
|
|
|
| 3262 |
return json_str
|
| 3263 |
|
| 3264 |
|
| 3265 |
+
def export_tokens_json(convention: str = "semantic"):
|
| 3266 |
"""Export final tokens with selected upgrades applied - FLAT structure for Figma Tokens Studio."""
|
| 3267 |
if not state.desktop_normalized:
|
| 3268 |
gr.Warning("No tokens extracted yet. Complete Stage 1 extraction first.")
|
|
|
|
| 3304 |
primary_font = fonts_info.get("primary", "sans-serif")
|
| 3305 |
|
| 3306 |
# =========================================================================
|
| 3307 |
+
# COLORS — Rule-based classification + optional ramps
|
| 3308 |
# =========================================================================
|
| 3309 |
if state.desktop_normalized and state.desktop_normalized.colors:
|
| 3310 |
from core.color_utils import generate_color_ramp
|
| 3311 |
+
from core.color_classifier import classify_colors
|
| 3312 |
|
| 3313 |
+
classification = classify_colors(
|
| 3314 |
+
state.desktop_normalized.colors,
|
| 3315 |
+
convention=convention or "semantic",
|
| 3316 |
+
log_callback=state.log,
|
| 3317 |
)
|
| 3318 |
|
| 3319 |
+
for c in classification.colors:
|
| 3320 |
+
flat_key = c.token_name
|
| 3321 |
if apply_ramps:
|
| 3322 |
try:
|
| 3323 |
+
ramp = generate_color_ramp(c.hex)
|
| 3324 |
shades = ["50", "100", "200", "300", "400", "500", "600", "700", "800", "900", "950"]
|
| 3325 |
for i, shade in enumerate(shades):
|
| 3326 |
if i < len(ramp):
|
| 3327 |
shade_key = f"{flat_key}.{shade}"
|
| 3328 |
+
color_val = ramp[i] if isinstance(ramp[i], str) else ramp[i].get("hex", c.hex)
|
| 3329 |
dtcg_token = _to_dtcg_token(color_val, "color")
|
| 3330 |
_flat_key_to_nested(shade_key, dtcg_token, result)
|
| 3331 |
token_count += 1
|
| 3332 |
except (ValueError, KeyError, TypeError, IndexError):
|
| 3333 |
+
dtcg_token = _to_dtcg_token(c.hex, "color")
|
| 3334 |
_flat_key_to_nested(flat_key, dtcg_token, result)
|
| 3335 |
token_count += 1
|
| 3336 |
else:
|
| 3337 |
+
dtcg_token = _to_dtcg_token(c.hex, "color")
|
| 3338 |
_flat_key_to_nested(flat_key, dtcg_token, result)
|
| 3339 |
token_count += 1
|
| 3340 |
|
|
|
|
| 4527 |
gr.Markdown("Export your finalized design tokens as JSON, compatible with **Figma Tokens Studio**.",
|
| 4528 |
elem_classes=["section-desc"])
|
| 4529 |
gr.Markdown("""
|
| 4530 |
+
- **Naming Convention:** Choose how colors are named in the export. Preview before exporting to verify.
|
| 4531 |
+
- **Stage 1 JSON (As-Is):** Raw extracted tokens — useful for archival or baseline comparison.
|
| 4532 |
+
- **Final JSON (Upgraded):** Tokens with your selected improvements applied. **Recommended export.**
|
|
|
|
| 4533 |
""", elem_classes=["section-desc"])
|
| 4534 |
|
| 4535 |
+
with gr.Row():
|
| 4536 |
+
naming_convention = gr.Dropdown(
|
| 4537 |
+
choices=["semantic", "tailwind", "material"],
|
| 4538 |
+
value="semantic",
|
| 4539 |
+
label="🎨 Naming Convention",
|
| 4540 |
+
info="semantic = color.brand.primary | tailwind = brand-primary | material = color.brand.primary",
|
| 4541 |
+
scale=2,
|
| 4542 |
+
)
|
| 4543 |
+
preview_colors_btn = gr.Button("👁️ Preview Color Names", variant="secondary", scale=1)
|
| 4544 |
+
|
| 4545 |
+
color_preview_output = gr.Code(
|
| 4546 |
+
label="Color Classification Preview (Rule-Based — No LLM)",
|
| 4547 |
+
language="text",
|
| 4548 |
+
lines=15,
|
| 4549 |
+
visible=True,
|
| 4550 |
+
)
|
| 4551 |
+
|
| 4552 |
with gr.Row():
|
| 4553 |
export_stage1_btn = gr.Button("📥 Export Stage 1 (As-Is)", variant="secondary")
|
| 4554 |
export_final_btn = gr.Button("📥 Export Final (Upgraded)", variant="primary")
|
|
|
|
| 4557 |
"Copy the contents or save as a `.json` file.*",
|
| 4558 |
elem_classes=["section-desc"])
|
| 4559 |
export_output = gr.Code(label="Tokens JSON", language="json", lines=25)
|
| 4560 |
+
|
| 4561 |
+
preview_colors_btn.click(
|
| 4562 |
+
preview_color_classification,
|
| 4563 |
+
inputs=[naming_convention],
|
| 4564 |
+
outputs=[color_preview_output],
|
| 4565 |
+
)
|
| 4566 |
+
export_stage1_btn.click(
|
| 4567 |
+
export_stage1_json,
|
| 4568 |
+
inputs=[naming_convention],
|
| 4569 |
+
outputs=[export_output],
|
| 4570 |
+
)
|
| 4571 |
+
export_final_btn.click(
|
| 4572 |
+
export_tokens_json,
|
| 4573 |
+
inputs=[naming_convention],
|
| 4574 |
+
outputs=[export_output],
|
| 4575 |
+
)
|
| 4576 |
|
| 4577 |
# =================================================================
|
| 4578 |
# EVENT HANDLERS
|
|
@@ -0,0 +1,678 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Rule-Based Color Classifier
|
| 3 |
+
Design System Extractor v3.1
|
| 4 |
+
|
| 5 |
+
100% deterministic color classification and naming.
|
| 6 |
+
NO LLM involved. Every decision logged with evidence.
|
| 7 |
+
|
| 8 |
+
The classifier:
|
| 9 |
+
1. Aggressively deduplicates similar colors (same role, RGB distance < 30)
|
| 10 |
+
2. Classifies by CSS evidence (property + element + hue)
|
| 11 |
+
3. Names with capped counts per category (no "nonary" ever again)
|
| 12 |
+
4. Supports user-selectable naming conventions (Tailwind, Material, Semantic, Custom)
|
| 13 |
+
5. Logs every decision with evidence trail
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
import colorsys
|
| 17 |
+
from dataclasses import dataclass, field
|
| 18 |
+
from typing import Optional
|
| 19 |
+
from core.color_utils import parse_color, normalize_hex, categorize_color
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
# =============================================================================
|
| 23 |
+
# NAMING CONVENTIONS
|
| 24 |
+
# =============================================================================
|
| 25 |
+
|
| 26 |
+
CONVENTIONS = {
|
| 27 |
+
"tailwind": {
|
| 28 |
+
"separator": "-", # blue-500
|
| 29 |
+
"shade_format": "numeric", # 50, 100, 200...900
|
| 30 |
+
"prefix": "",
|
| 31 |
+
},
|
| 32 |
+
"material": {
|
| 33 |
+
"separator": ".", # blue.500
|
| 34 |
+
"shade_format": "numeric",
|
| 35 |
+
"prefix": "color.",
|
| 36 |
+
},
|
| 37 |
+
"semantic": {
|
| 38 |
+
"separator": ".", # color.brand.primary
|
| 39 |
+
"shade_format": "role", # primary, secondary, muted
|
| 40 |
+
"prefix": "color.",
|
| 41 |
+
},
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
# Role shade names for semantic convention
|
| 45 |
+
ROLE_SHADE_NAMES = {
|
| 46 |
+
"brand": ["primary", "secondary", "accent"],
|
| 47 |
+
"text": ["primary", "secondary", "muted"],
|
| 48 |
+
"bg": ["primary", "secondary", "tertiary"],
|
| 49 |
+
"border": ["light", "DEFAULT", "dark"],
|
| 50 |
+
"feedback": ["error", "warning", "success", "info"],
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
# Feedback hue ranges (hue in degrees)
|
| 54 |
+
FEEDBACK_HUES = {
|
| 55 |
+
"error": (345, 15), # Red
|
| 56 |
+
"warning": (15, 55), # Orange/Yellow
|
| 57 |
+
"success": (85, 155), # Green
|
| 58 |
+
"info": (175, 215), # Cyan/Blue
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
# =============================================================================
|
| 63 |
+
# DATA CLASSES
|
| 64 |
+
# =============================================================================
|
| 65 |
+
|
| 66 |
+
@dataclass
|
| 67 |
+
class ClassifiedColor:
|
| 68 |
+
"""A color with its classification and evidence."""
|
| 69 |
+
hex: str
|
| 70 |
+
frequency: int
|
| 71 |
+
category: str # brand, text, bg, border, feedback, palette
|
| 72 |
+
role: str # primary, secondary, etc. OR numeric shade
|
| 73 |
+
token_name: str # Final token name (e.g., "color.brand.primary")
|
| 74 |
+
evidence: list[str] = field(default_factory=list)
|
| 75 |
+
confidence: str = "medium" # high, medium, low
|
| 76 |
+
css_properties: list[str] = field(default_factory=list)
|
| 77 |
+
elements: list[str] = field(default_factory=list)
|
| 78 |
+
contexts: list[str] = field(default_factory=list)
|
| 79 |
+
merged_from: list[str] = field(default_factory=list) # Hex values merged into this
|
| 80 |
+
hue_family: str = "neutral"
|
| 81 |
+
luminance: float = 0.5
|
| 82 |
+
saturation: float = 0.0
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
@dataclass
|
| 86 |
+
class ClassificationResult:
|
| 87 |
+
"""Complete classification output with log."""
|
| 88 |
+
colors: list[ClassifiedColor]
|
| 89 |
+
log: list[str] # Human-readable decision log
|
| 90 |
+
convention: str # Which convention was used
|
| 91 |
+
stats: dict = field(default_factory=dict)
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
# =============================================================================
|
| 95 |
+
# HELPER FUNCTIONS
|
| 96 |
+
# =============================================================================
|
| 97 |
+
|
| 98 |
+
def _rgb_distance(hex1: str, hex2: str) -> float:
|
| 99 |
+
"""Euclidean distance in RGB space."""
|
| 100 |
+
p1 = parse_color(hex1)
|
| 101 |
+
p2 = parse_color(hex2)
|
| 102 |
+
if not p1 or not p2:
|
| 103 |
+
return 999
|
| 104 |
+
r1, g1, b1 = p1.rgb
|
| 105 |
+
r2, g2, b2 = p2.rgb
|
| 106 |
+
return ((r1-r2)**2 + (g1-g2)**2 + (b1-b2)**2) ** 0.5
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def _get_luminance(hex_color: str) -> float:
|
| 110 |
+
"""Get perceptual luminance 0-1."""
|
| 111 |
+
parsed = parse_color(hex_color)
|
| 112 |
+
if not parsed:
|
| 113 |
+
return 0.5
|
| 114 |
+
r, g, b = parsed.rgb
|
| 115 |
+
return (0.299 * r + 0.587 * g + 0.114 * b) / 255
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def _get_saturation(hex_color: str) -> float:
|
| 119 |
+
"""Get saturation 0-1."""
|
| 120 |
+
parsed = parse_color(hex_color)
|
| 121 |
+
if not parsed:
|
| 122 |
+
return 0
|
| 123 |
+
h, s, l = parsed.hsl
|
| 124 |
+
return s / 100 # hsl returns 0-100
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def _get_hue(hex_color: str) -> float:
|
| 128 |
+
"""Get hue in degrees 0-360."""
|
| 129 |
+
parsed = parse_color(hex_color)
|
| 130 |
+
if not parsed:
|
| 131 |
+
return 0
|
| 132 |
+
h, s, l = parsed.hsl
|
| 133 |
+
return h
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def _hue_in_range(hue: float, range_tuple: tuple) -> bool:
|
| 137 |
+
"""Check if hue is in range, handling wrap-around."""
|
| 138 |
+
low, high = range_tuple
|
| 139 |
+
if low < high:
|
| 140 |
+
return low <= hue <= high
|
| 141 |
+
else: # Wraps around 360 (e.g., red: 345-15)
|
| 142 |
+
return hue >= low or hue <= high
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def _lightness_to_shade(lightness_pct: float) -> str:
|
| 146 |
+
"""Map HSL lightness (0-100) to numeric shade (50-900)."""
|
| 147 |
+
if lightness_pct >= 95:
|
| 148 |
+
return "50"
|
| 149 |
+
elif lightness_pct >= 85:
|
| 150 |
+
return "100"
|
| 151 |
+
elif lightness_pct >= 75:
|
| 152 |
+
return "200"
|
| 153 |
+
elif lightness_pct >= 65:
|
| 154 |
+
return "300"
|
| 155 |
+
elif lightness_pct >= 55:
|
| 156 |
+
return "400"
|
| 157 |
+
elif lightness_pct >= 45:
|
| 158 |
+
return "500"
|
| 159 |
+
elif lightness_pct >= 35:
|
| 160 |
+
return "600"
|
| 161 |
+
elif lightness_pct >= 25:
|
| 162 |
+
return "700"
|
| 163 |
+
elif lightness_pct >= 15:
|
| 164 |
+
return "800"
|
| 165 |
+
else:
|
| 166 |
+
return "900"
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
# =============================================================================
|
| 170 |
+
# MAIN CLASSIFIER
|
| 171 |
+
# =============================================================================
|
| 172 |
+
|
| 173 |
+
def classify_colors(
|
| 174 |
+
colors_dict: dict,
|
| 175 |
+
convention: str = "semantic",
|
| 176 |
+
log_callback=None,
|
| 177 |
+
) -> ClassificationResult:
|
| 178 |
+
"""
|
| 179 |
+
Classify and name all colors using 100% rule-based logic.
|
| 180 |
+
|
| 181 |
+
Args:
|
| 182 |
+
colors_dict: Dict of name -> ColorToken (from normalizer)
|
| 183 |
+
convention: "tailwind", "material", or "semantic"
|
| 184 |
+
log_callback: Optional callback for real-time logging
|
| 185 |
+
|
| 186 |
+
Returns:
|
| 187 |
+
ClassificationResult with classified colors and decision log
|
| 188 |
+
"""
|
| 189 |
+
log_lines = []
|
| 190 |
+
|
| 191 |
+
def log(msg):
|
| 192 |
+
log_lines.append(msg)
|
| 193 |
+
if log_callback:
|
| 194 |
+
log_callback(msg)
|
| 195 |
+
|
| 196 |
+
log("📊 COLOR CLASSIFICATION (Rule-Based v3.1)")
|
| 197 |
+
log("─" * 50)
|
| 198 |
+
|
| 199 |
+
if not colors_dict:
|
| 200 |
+
log("⚠️ No colors to classify")
|
| 201 |
+
return ClassificationResult(colors=[], log=log_lines, convention=convention)
|
| 202 |
+
|
| 203 |
+
# =========================================================================
|
| 204 |
+
# STEP 1: Build flat color list with metadata
|
| 205 |
+
# =========================================================================
|
| 206 |
+
raw_colors = []
|
| 207 |
+
for name, c in colors_dict.items():
|
| 208 |
+
hex_val = c.value if hasattr(c, 'value') else c.get('value', '')
|
| 209 |
+
hex_val = normalize_hex(hex_val)
|
| 210 |
+
freq = c.frequency if hasattr(c, 'frequency') else c.get('frequency', 0)
|
| 211 |
+
css_props = c.css_properties if hasattr(c, 'css_properties') else c.get('css_properties', [])
|
| 212 |
+
elements = c.elements if hasattr(c, 'elements') else c.get('elements', [])
|
| 213 |
+
contexts = c.contexts if hasattr(c, 'contexts') else c.get('contexts', [])
|
| 214 |
+
role_hint = c.role_hint if hasattr(c, 'role_hint') else c.get('role_hint', None)
|
| 215 |
+
|
| 216 |
+
raw_colors.append({
|
| 217 |
+
"hex": hex_val,
|
| 218 |
+
"frequency": freq,
|
| 219 |
+
"css_properties": [p.lower() for p in css_props],
|
| 220 |
+
"elements": [e.lower() for e in elements],
|
| 221 |
+
"contexts": [c.lower() for c in contexts],
|
| 222 |
+
"role_hint": role_hint,
|
| 223 |
+
"luminance": _get_luminance(hex_val),
|
| 224 |
+
"saturation": _get_saturation(hex_val),
|
| 225 |
+
"hue": _get_hue(hex_val),
|
| 226 |
+
"hue_family": categorize_color(hex_val),
|
| 227 |
+
})
|
| 228 |
+
|
| 229 |
+
log(f"Input: {len(raw_colors)} unique colors")
|
| 230 |
+
|
| 231 |
+
# =========================================================================
|
| 232 |
+
# STEP 2: Classify each color by CSS evidence
|
| 233 |
+
# =========================================================================
|
| 234 |
+
classified = []
|
| 235 |
+
for c in raw_colors:
|
| 236 |
+
category = _classify_single_color(c)
|
| 237 |
+
c["category"] = category
|
| 238 |
+
classified.append(c)
|
| 239 |
+
|
| 240 |
+
# Count per category
|
| 241 |
+
cat_counts = {}
|
| 242 |
+
for c in classified:
|
| 243 |
+
cat_counts[c["category"]] = cat_counts.get(c["category"], 0) + 1
|
| 244 |
+
log(f"Pre-dedup classification: {cat_counts}")
|
| 245 |
+
|
| 246 |
+
# =========================================================================
|
| 247 |
+
# STEP 3: Aggressive dedup WITHIN each category
|
| 248 |
+
# =========================================================================
|
| 249 |
+
deduped = _aggressive_dedup(classified, log)
|
| 250 |
+
|
| 251 |
+
cat_counts_after = {}
|
| 252 |
+
for c in deduped:
|
| 253 |
+
cat_counts_after[c["category"]] = cat_counts_after.get(c["category"], 0) + 1
|
| 254 |
+
log(f"Post-dedup: {len(deduped)} colors — {cat_counts_after}")
|
| 255 |
+
|
| 256 |
+
# =========================================================================
|
| 257 |
+
# STEP 4: Cap and rank within each category
|
| 258 |
+
# =========================================================================
|
| 259 |
+
capped = _cap_per_category(deduped, log)
|
| 260 |
+
|
| 261 |
+
# =========================================================================
|
| 262 |
+
# STEP 5: Assign final names using chosen convention
|
| 263 |
+
# =========================================================================
|
| 264 |
+
result_colors = _assign_names(capped, convention, log)
|
| 265 |
+
|
| 266 |
+
log(f"─" * 50)
|
| 267 |
+
log(f"✅ Final: {len(result_colors)} color tokens using '{convention}' convention")
|
| 268 |
+
|
| 269 |
+
stats = {
|
| 270 |
+
"input_count": len(raw_colors),
|
| 271 |
+
"output_count": len(result_colors),
|
| 272 |
+
"merged_count": len(raw_colors) - len(deduped),
|
| 273 |
+
"categories": cat_counts_after,
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
return ClassificationResult(
|
| 277 |
+
colors=result_colors,
|
| 278 |
+
log=log_lines,
|
| 279 |
+
convention=convention,
|
| 280 |
+
stats=stats,
|
| 281 |
+
)
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
# =============================================================================
|
| 285 |
+
# STEP 2: CLASSIFY SINGLE COLOR
|
| 286 |
+
# =============================================================================
|
| 287 |
+
|
| 288 |
+
def _classify_single_color(c: dict) -> str:
|
| 289 |
+
"""
|
| 290 |
+
Classify a single color into category based on CSS evidence.
|
| 291 |
+
Returns: "brand", "text", "bg", "border", "feedback", "palette"
|
| 292 |
+
"""
|
| 293 |
+
css_props = c["css_properties"]
|
| 294 |
+
elements = c["elements"]
|
| 295 |
+
all_context = " ".join(css_props + elements + c["contexts"])
|
| 296 |
+
sat = c["saturation"]
|
| 297 |
+
lum = c["luminance"]
|
| 298 |
+
hue = c["hue"]
|
| 299 |
+
freq = c["frequency"]
|
| 300 |
+
|
| 301 |
+
# --- FEEDBACK: by hue + saturation (check FIRST, before brand) ---
|
| 302 |
+
if sat > 0.4:
|
| 303 |
+
# Check explicit feedback keywords in context
|
| 304 |
+
feedback_keywords = ["error", "danger", "invalid", "success", "valid",
|
| 305 |
+
"warning", "caution", "alert", "info", "notice"]
|
| 306 |
+
if any(kw in all_context for kw in feedback_keywords):
|
| 307 |
+
return "feedback"
|
| 308 |
+
|
| 309 |
+
# Check by hue if frequency is low (feedback colors are rarely used often)
|
| 310 |
+
if freq < 15:
|
| 311 |
+
for fb_type, hue_range in FEEDBACK_HUES.items():
|
| 312 |
+
if _hue_in_range(hue, hue_range):
|
| 313 |
+
# Additional check: pure saturated colors with low freq = likely feedback
|
| 314 |
+
if sat > 0.6:
|
| 315 |
+
return "feedback"
|
| 316 |
+
|
| 317 |
+
# --- BRAND: interactive elements + background-color + saturated ---
|
| 318 |
+
interactive = ["button", "a", "input", "select", "submit", "btn", "cta", "link"]
|
| 319 |
+
is_interactive = any(el in all_context for el in interactive)
|
| 320 |
+
has_bg_prop = any("background" in p for p in css_props)
|
| 321 |
+
|
| 322 |
+
if sat > 0.25 and is_interactive and has_bg_prop and freq > 5:
|
| 323 |
+
return "brand"
|
| 324 |
+
if sat > 0.3 and freq > 20:
|
| 325 |
+
return "brand"
|
| 326 |
+
|
| 327 |
+
# --- TEXT: "color" property on text elements + low saturation ---
|
| 328 |
+
has_color_prop = any(p == "color" for p in css_props)
|
| 329 |
+
text_els = ["p", "span", "h1", "h2", "h3", "h4", "h5", "h6", "label", "li", "td", "a"]
|
| 330 |
+
is_text_el = any(el in all_context for el in text_els)
|
| 331 |
+
|
| 332 |
+
if has_color_prop and (is_text_el or sat < 0.15):
|
| 333 |
+
if sat < 0.2:
|
| 334 |
+
return "text"
|
| 335 |
+
|
| 336 |
+
# --- BACKGROUND: background-color on containers + light/neutral ---
|
| 337 |
+
containers = ["div", "section", "main", "body", "article", "header", "footer", "card", "nav"]
|
| 338 |
+
is_container = any(el in all_context for el in containers)
|
| 339 |
+
|
| 340 |
+
if has_bg_prop and is_container and sat < 0.15:
|
| 341 |
+
return "bg"
|
| 342 |
+
if lum > 0.9 and sat < 0.1:
|
| 343 |
+
return "bg"
|
| 344 |
+
|
| 345 |
+
# --- BORDER: border properties ---
|
| 346 |
+
has_border_prop = any("border" in p for p in css_props)
|
| 347 |
+
if has_border_prop and sat < 0.2:
|
| 348 |
+
return "border"
|
| 349 |
+
|
| 350 |
+
# --- Neutral with high freq but no clear CSS evidence → likely text ---
|
| 351 |
+
if sat < 0.1 and lum < 0.6 and freq > 10:
|
| 352 |
+
return "text"
|
| 353 |
+
if sat < 0.1 and lum > 0.8:
|
| 354 |
+
return "bg"
|
| 355 |
+
|
| 356 |
+
# --- Everything else → palette ---
|
| 357 |
+
return "palette"
|
| 358 |
+
|
| 359 |
+
|
| 360 |
+
# =============================================================================
|
| 361 |
+
# STEP 3: AGGRESSIVE DEDUP
|
| 362 |
+
# =============================================================================
|
| 363 |
+
|
| 364 |
+
def _aggressive_dedup(colors: list[dict], log) -> list[dict]:
|
| 365 |
+
"""
|
| 366 |
+
Aggressively merge similar colors WITHIN the same category.
|
| 367 |
+
Threshold: RGB distance < 30 for same-category colors.
|
| 368 |
+
"""
|
| 369 |
+
# Group by category
|
| 370 |
+
by_category = {}
|
| 371 |
+
for c in colors:
|
| 372 |
+
cat = c["category"]
|
| 373 |
+
if cat not in by_category:
|
| 374 |
+
by_category[cat] = []
|
| 375 |
+
by_category[cat].append(c)
|
| 376 |
+
|
| 377 |
+
result = []
|
| 378 |
+
total_merged = 0
|
| 379 |
+
|
| 380 |
+
for cat, cat_colors in by_category.items():
|
| 381 |
+
if len(cat_colors) <= 1:
|
| 382 |
+
result.extend(cat_colors)
|
| 383 |
+
continue
|
| 384 |
+
|
| 385 |
+
# Sort by frequency (most used first — these survive merges)
|
| 386 |
+
cat_colors.sort(key=lambda x: -x["frequency"])
|
| 387 |
+
|
| 388 |
+
merged = []
|
| 389 |
+
used = set()
|
| 390 |
+
|
| 391 |
+
for i, c1 in enumerate(cat_colors):
|
| 392 |
+
if i in used:
|
| 393 |
+
continue
|
| 394 |
+
|
| 395 |
+
group = [c1]
|
| 396 |
+
for j, c2 in enumerate(cat_colors[i+1:], i+1):
|
| 397 |
+
if j in used:
|
| 398 |
+
continue
|
| 399 |
+
dist = _rgb_distance(c1["hex"], c2["hex"])
|
| 400 |
+
if dist < 30:
|
| 401 |
+
group.append(c2)
|
| 402 |
+
used.add(j)
|
| 403 |
+
|
| 404 |
+
# Merge into the highest-frequency color
|
| 405 |
+
primary = group[0]
|
| 406 |
+
merged_hexes = []
|
| 407 |
+
for other in group[1:]:
|
| 408 |
+
primary["frequency"] += other["frequency"]
|
| 409 |
+
primary["css_properties"] = list(set(primary["css_properties"] + other["css_properties"]))
|
| 410 |
+
primary["elements"] = list(set(primary["elements"] + other["elements"]))
|
| 411 |
+
primary["contexts"] = list(set(primary["contexts"] + other["contexts"]))
|
| 412 |
+
merged_hexes.append(other["hex"])
|
| 413 |
+
|
| 414 |
+
primary["merged_from"] = merged_hexes
|
| 415 |
+
merged.append(primary)
|
| 416 |
+
used.add(i)
|
| 417 |
+
|
| 418 |
+
if merged_hexes:
|
| 419 |
+
total_merged += len(merged_hexes)
|
| 420 |
+
log(f"[DEDUP] {cat}: {primary['hex']} absorbed {merged_hexes} (dist<30)")
|
| 421 |
+
|
| 422 |
+
result.extend(merged)
|
| 423 |
+
|
| 424 |
+
if total_merged > 0:
|
| 425 |
+
log(f"[DEDUP] Total: {total_merged} near-duplicate colors merged")
|
| 426 |
+
|
| 427 |
+
return result
|
| 428 |
+
|
| 429 |
+
|
| 430 |
+
# =============================================================================
|
| 431 |
+
# STEP 4: CAP PER CATEGORY
|
| 432 |
+
# =============================================================================
|
| 433 |
+
|
| 434 |
+
# Maximum colors per category
|
| 435 |
+
CATEGORY_CAPS = {
|
| 436 |
+
"brand": 3, # primary, secondary, accent
|
| 437 |
+
"text": 3, # primary, secondary, muted
|
| 438 |
+
"bg": 3, # primary, secondary, tertiary
|
| 439 |
+
"border": 3, # light, default, dark
|
| 440 |
+
"feedback": 4, # error, warning, success, info
|
| 441 |
+
"palette": 20, # generous cap for remaining
|
| 442 |
+
}
|
| 443 |
+
|
| 444 |
+
|
| 445 |
+
def _cap_per_category(colors: list[dict], log) -> list[dict]:
|
| 446 |
+
"""
|
| 447 |
+
Limit colors per category. Excess become palette colors.
|
| 448 |
+
Within each category, keep by frequency (most used survives).
|
| 449 |
+
"""
|
| 450 |
+
by_category = {}
|
| 451 |
+
for c in colors:
|
| 452 |
+
cat = c["category"]
|
| 453 |
+
if cat not in by_category:
|
| 454 |
+
by_category[cat] = []
|
| 455 |
+
by_category[cat].append(c)
|
| 456 |
+
|
| 457 |
+
result = []
|
| 458 |
+
|
| 459 |
+
for cat, cat_colors in by_category.items():
|
| 460 |
+
cap = CATEGORY_CAPS.get(cat, 10)
|
| 461 |
+
cat_colors.sort(key=lambda x: -x["frequency"])
|
| 462 |
+
|
| 463 |
+
kept = cat_colors[:cap]
|
| 464 |
+
overflow = cat_colors[cap:]
|
| 465 |
+
|
| 466 |
+
result.extend(kept)
|
| 467 |
+
|
| 468 |
+
# Overflow colors become palette
|
| 469 |
+
for c in overflow:
|
| 470 |
+
old_cat = c["category"]
|
| 471 |
+
c["category"] = "palette"
|
| 472 |
+
log(f"[CAP] {c['hex']} demoted: {old_cat} → palette (category full, freq={c['frequency']})")
|
| 473 |
+
|
| 474 |
+
result.extend(overflow)
|
| 475 |
+
|
| 476 |
+
return result
|
| 477 |
+
|
| 478 |
+
|
| 479 |
+
# =============================================================================
|
| 480 |
+
# STEP 5: ASSIGN NAMES
|
| 481 |
+
# =============================================================================
|
| 482 |
+
|
| 483 |
+
def _assign_names(colors: list[dict], convention: str, log) -> list[ClassifiedColor]:
|
| 484 |
+
"""
|
| 485 |
+
Assign final token names based on chosen convention.
|
| 486 |
+
"""
|
| 487 |
+
conv = CONVENTIONS.get(convention, CONVENTIONS["semantic"])
|
| 488 |
+
prefix = conv["prefix"]
|
| 489 |
+
sep = conv["separator"]
|
| 490 |
+
|
| 491 |
+
# Group by category for role assignment
|
| 492 |
+
by_category = {}
|
| 493 |
+
for c in colors:
|
| 494 |
+
cat = c["category"]
|
| 495 |
+
if cat not in by_category:
|
| 496 |
+
by_category[cat] = []
|
| 497 |
+
by_category[cat].append(c)
|
| 498 |
+
|
| 499 |
+
result = []
|
| 500 |
+
used_names = set()
|
| 501 |
+
|
| 502 |
+
for cat in ["brand", "text", "bg", "border", "feedback", "palette"]:
|
| 503 |
+
cat_colors = by_category.get(cat, [])
|
| 504 |
+
if not cat_colors:
|
| 505 |
+
continue
|
| 506 |
+
|
| 507 |
+
# Sort by frequency for consistent ordering
|
| 508 |
+
cat_colors.sort(key=lambda x: -x["frequency"])
|
| 509 |
+
|
| 510 |
+
for idx, c in enumerate(cat_colors):
|
| 511 |
+
name_cat = cat # Local var — don't override loop variable
|
| 512 |
+
|
| 513 |
+
if cat == "feedback":
|
| 514 |
+
role = _assign_feedback_role(c, idx, by_category.get("feedback", []))
|
| 515 |
+
elif cat == "palette":
|
| 516 |
+
# Palette: use hue family + numeric shade (ALWAYS)
|
| 517 |
+
name_cat = c["hue_family"] # Override with hue family
|
| 518 |
+
parsed = parse_color(c["hex"])
|
| 519 |
+
if parsed:
|
| 520 |
+
role = _lightness_to_shade(parsed.hsl[2])
|
| 521 |
+
else:
|
| 522 |
+
role = "500"
|
| 523 |
+
elif convention == "semantic":
|
| 524 |
+
# Semantic: use role names (primary, secondary, muted, etc.)
|
| 525 |
+
role_names = ROLE_SHADE_NAMES.get(c["category"], ["primary", "secondary", "tertiary"])
|
| 526 |
+
if idx < len(role_names):
|
| 527 |
+
role = role_names[idx]
|
| 528 |
+
else:
|
| 529 |
+
role = f"{idx + 1}"
|
| 530 |
+
else:
|
| 531 |
+
# Tailwind/Material: even role colors get descriptive names
|
| 532 |
+
role_names = ROLE_SHADE_NAMES.get(c["category"], ["primary", "secondary", "tertiary"])
|
| 533 |
+
if idx < len(role_names):
|
| 534 |
+
role = role_names[idx]
|
| 535 |
+
else:
|
| 536 |
+
role = f"{idx + 1}"
|
| 537 |
+
|
| 538 |
+
# Build token name
|
| 539 |
+
if convention == "tailwind":
|
| 540 |
+
token_name = f"{name_cat}{sep}{role}"
|
| 541 |
+
else:
|
| 542 |
+
token_name = f"{prefix}{name_cat}{sep}{role}"
|
| 543 |
+
|
| 544 |
+
# Handle name collisions
|
| 545 |
+
base_name = token_name
|
| 546 |
+
suffix = 2
|
| 547 |
+
while token_name in used_names:
|
| 548 |
+
token_name = f"{base_name}{sep}{suffix}"
|
| 549 |
+
suffix += 1
|
| 550 |
+
used_names.add(token_name)
|
| 551 |
+
|
| 552 |
+
# Build evidence
|
| 553 |
+
evidence = _build_evidence(c)
|
| 554 |
+
|
| 555 |
+
log(f"[NAME] {c['hex']} → {token_name} ({c['category']}, freq={c['frequency']})")
|
| 556 |
+
|
| 557 |
+
result.append(ClassifiedColor(
|
| 558 |
+
hex=c["hex"],
|
| 559 |
+
frequency=c["frequency"],
|
| 560 |
+
category=c["category"],
|
| 561 |
+
role=role,
|
| 562 |
+
token_name=token_name,
|
| 563 |
+
evidence=evidence,
|
| 564 |
+
confidence="high" if c["frequency"] > 10 else "medium" if c["frequency"] > 3 else "low",
|
| 565 |
+
css_properties=c["css_properties"],
|
| 566 |
+
elements=c["elements"],
|
| 567 |
+
contexts=c["contexts"],
|
| 568 |
+
merged_from=c.get("merged_from", []),
|
| 569 |
+
hue_family=c["hue_family"],
|
| 570 |
+
luminance=c["luminance"],
|
| 571 |
+
saturation=c["saturation"],
|
| 572 |
+
))
|
| 573 |
+
|
| 574 |
+
return result
|
| 575 |
+
|
| 576 |
+
|
| 577 |
+
def _assign_feedback_role(c: dict, idx: int, all_feedback: list) -> str:
|
| 578 |
+
"""Assign feedback role by hue matching."""
|
| 579 |
+
hue = c["hue"]
|
| 580 |
+
sat = c["saturation"]
|
| 581 |
+
|
| 582 |
+
# Try to match by hue
|
| 583 |
+
if _hue_in_range(hue, FEEDBACK_HUES["error"]) and sat > 0.4:
|
| 584 |
+
return "error"
|
| 585 |
+
if _hue_in_range(hue, FEEDBACK_HUES["warning"]) and sat > 0.4:
|
| 586 |
+
return "warning"
|
| 587 |
+
if _hue_in_range(hue, FEEDBACK_HUES["success"]) and sat > 0.4:
|
| 588 |
+
return "success"
|
| 589 |
+
if _hue_in_range(hue, FEEDBACK_HUES["info"]) and sat > 0.3:
|
| 590 |
+
return "info"
|
| 591 |
+
|
| 592 |
+
# Fallback: by index
|
| 593 |
+
fallback_names = ["error", "warning", "success", "info"]
|
| 594 |
+
return fallback_names[idx % len(fallback_names)]
|
| 595 |
+
|
| 596 |
+
|
| 597 |
+
def _build_evidence(c: dict) -> list[str]:
|
| 598 |
+
"""Build human-readable evidence list for classification."""
|
| 599 |
+
ev = []
|
| 600 |
+
if c["frequency"] > 0:
|
| 601 |
+
ev.append(f"Used {c['frequency']}x")
|
| 602 |
+
if c["css_properties"]:
|
| 603 |
+
ev.append(f"CSS: {', '.join(c['css_properties'][:3])}")
|
| 604 |
+
if c["elements"]:
|
| 605 |
+
ev.append(f"Elements: {', '.join(c['elements'][:3])}")
|
| 606 |
+
if c.get("merged_from"):
|
| 607 |
+
ev.append(f"Merged {len(c['merged_from'])} similar colors")
|
| 608 |
+
ev.append(f"Hue: {c['hue_family']}, Lum: {c['luminance']:.2f}, Sat: {c['saturation']:.2f}")
|
| 609 |
+
return ev
|
| 610 |
+
|
| 611 |
+
|
| 612 |
+
# =============================================================================
|
| 613 |
+
# PREVIEW GENERATOR
|
| 614 |
+
# =============================================================================
|
| 615 |
+
|
| 616 |
+
def generate_classification_preview(result: ClassificationResult) -> str:
|
| 617 |
+
"""
|
| 618 |
+
Generate a human-readable preview of the classification.
|
| 619 |
+
Shows what the user will get before export.
|
| 620 |
+
"""
|
| 621 |
+
lines = []
|
| 622 |
+
lines.append(f"🎨 COLOR CLASSIFICATION PREVIEW ({result.convention} convention)")
|
| 623 |
+
lines.append(f"{'=' * 60}")
|
| 624 |
+
lines.append("")
|
| 625 |
+
|
| 626 |
+
# Group by category
|
| 627 |
+
by_cat = {}
|
| 628 |
+
for c in result.colors:
|
| 629 |
+
if c.category not in by_cat:
|
| 630 |
+
by_cat[c.category] = []
|
| 631 |
+
by_cat[c.category].append(c)
|
| 632 |
+
|
| 633 |
+
cat_labels = {
|
| 634 |
+
"brand": "🏷️ BRAND (from buttons/CTAs)",
|
| 635 |
+
"text": "📝 TEXT (from color property)",
|
| 636 |
+
"bg": "🖼️ BACKGROUND (from containers)",
|
| 637 |
+
"border": "📐 BORDER (from border properties)",
|
| 638 |
+
"feedback": "⚡ FEEDBACK (by hue convention)",
|
| 639 |
+
}
|
| 640 |
+
|
| 641 |
+
for cat in ["brand", "text", "bg", "border", "feedback"]:
|
| 642 |
+
cat_colors = by_cat.get(cat, [])
|
| 643 |
+
if not cat_colors:
|
| 644 |
+
continue
|
| 645 |
+
|
| 646 |
+
label = cat_labels.get(cat, f"📦 {cat.upper()}")
|
| 647 |
+
lines.append(label)
|
| 648 |
+
|
| 649 |
+
for c in cat_colors:
|
| 650 |
+
ev_short = f"({c.frequency}x" + (f", merged {len(c.merged_from)}" if c.merged_from else "") + ")"
|
| 651 |
+
lines.append(f" ■ {c.hex} → {c.token_name} {ev_short}")
|
| 652 |
+
|
| 653 |
+
lines.append("")
|
| 654 |
+
|
| 655 |
+
# Palette colors (group by hue family)
|
| 656 |
+
palette = by_cat.get("palette", [])
|
| 657 |
+
if palette:
|
| 658 |
+
lines.append("🎨 PALETTE (remaining by hue)")
|
| 659 |
+
hue_groups = {}
|
| 660 |
+
for c in palette:
|
| 661 |
+
if c.hue_family not in hue_groups:
|
| 662 |
+
hue_groups[c.hue_family] = []
|
| 663 |
+
hue_groups[c.hue_family].append(c)
|
| 664 |
+
|
| 665 |
+
for hue_fam, hue_colors in sorted(hue_groups.items()):
|
| 666 |
+
for c in hue_colors:
|
| 667 |
+
lines.append(f" ■ {c.hex} → {c.token_name} ({c.frequency}x)")
|
| 668 |
+
|
| 669 |
+
lines.append("")
|
| 670 |
+
|
| 671 |
+
# Stats
|
| 672 |
+
lines.append(f"{'─' * 60}")
|
| 673 |
+
if result.stats:
|
| 674 |
+
lines.append(f"Total: {result.stats.get('output_count', 0)} tokens")
|
| 675 |
+
if result.stats.get("merged_count", 0) > 0:
|
| 676 |
+
lines.append(f"⚠️ {result.stats['merged_count']} near-duplicate colors were merged")
|
| 677 |
+
|
| 678 |
+
return "\n".join(lines)
|
|
@@ -0,0 +1,584 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"color": {
|
| 3 |
+
"text": {
|
| 4 |
+
"primary": {
|
| 5 |
+
"$type": "color",
|
| 6 |
+
"$value": "#373737"
|
| 7 |
+
},
|
| 8 |
+
"secondary": {
|
| 9 |
+
"$type": "color",
|
| 10 |
+
"$value": "#000000"
|
| 11 |
+
},
|
| 12 |
+
"tertiary": {
|
| 13 |
+
"$type": "color",
|
| 14 |
+
"$value": "#999999"
|
| 15 |
+
},
|
| 16 |
+
"quaternary": {
|
| 17 |
+
"$type": "color",
|
| 18 |
+
"$value": "#4e4c4a"
|
| 19 |
+
},
|
| 20 |
+
"quinary": {
|
| 21 |
+
"$type": "color",
|
| 22 |
+
"$value": "#808080"
|
| 23 |
+
},
|
| 24 |
+
"senary": {
|
| 25 |
+
"$type": "color",
|
| 26 |
+
"$value": "#cccccc"
|
| 27 |
+
},
|
| 28 |
+
"septenary": {
|
| 29 |
+
"$type": "color",
|
| 30 |
+
"$value": "#404040"
|
| 31 |
+
},
|
| 32 |
+
"octonary": {
|
| 33 |
+
"$type": "color",
|
| 34 |
+
"$value": "#727272"
|
| 35 |
+
},
|
| 36 |
+
"nonary": {
|
| 37 |
+
"$type": "color",
|
| 38 |
+
"$value": "#aaaaaa"
|
| 39 |
+
},
|
| 40 |
+
"decenary": {
|
| 41 |
+
"$type": "color",
|
| 42 |
+
"$value": "#656565"
|
| 43 |
+
},
|
| 44 |
+
"undecenary": {
|
| 45 |
+
"$type": "color",
|
| 46 |
+
"$value": "#0e0c24"
|
| 47 |
+
},
|
| 48 |
+
"duodecenary": {
|
| 49 |
+
"$type": "color",
|
| 50 |
+
"$value": "#282828"
|
| 51 |
+
},
|
| 52 |
+
"tredecenary": {
|
| 53 |
+
"$type": "color",
|
| 54 |
+
"$value": "#151414"
|
| 55 |
+
}
|
| 56 |
+
},
|
| 57 |
+
"bg": {
|
| 58 |
+
"primary": {
|
| 59 |
+
"$type": "color",
|
| 60 |
+
"$value": "#ffffff"
|
| 61 |
+
},
|
| 62 |
+
"light": {
|
| 63 |
+
"$type": "color",
|
| 64 |
+
"$value": "#f6f6f6"
|
| 65 |
+
},
|
| 66 |
+
"medium": {
|
| 67 |
+
"$type": "color",
|
| 68 |
+
"$value": "#ecedee"
|
| 69 |
+
},
|
| 70 |
+
"error": {
|
| 71 |
+
"$type": "color",
|
| 72 |
+
"$value": "#fff2f2"
|
| 73 |
+
}
|
| 74 |
+
},
|
| 75 |
+
"border": {
|
| 76 |
+
"light": {
|
| 77 |
+
"$type": "color",
|
| 78 |
+
"$value": "#d3d3d3"
|
| 79 |
+
},
|
| 80 |
+
"medium": {
|
| 81 |
+
"$type": "color",
|
| 82 |
+
"$value": "#e4e4e4"
|
| 83 |
+
},
|
| 84 |
+
"dark": {
|
| 85 |
+
"$type": "color",
|
| 86 |
+
"$value": "#b3b3b3"
|
| 87 |
+
},
|
| 88 |
+
"heavy": {
|
| 89 |
+
"$type": "color",
|
| 90 |
+
"$value": "#2c3e50"
|
| 91 |
+
}
|
| 92 |
+
},
|
| 93 |
+
"brand": {
|
| 94 |
+
"primary": {
|
| 95 |
+
"$type": "color",
|
| 96 |
+
"$value": "#06b2c4"
|
| 97 |
+
},
|
| 98 |
+
"secondary": {
|
| 99 |
+
"$type": "color",
|
| 100 |
+
"$value": "#bcd432"
|
| 101 |
+
},
|
| 102 |
+
"accent": {
|
| 103 |
+
"$type": "color",
|
| 104 |
+
"$value": "#ff1857"
|
| 105 |
+
},
|
| 106 |
+
"error": {
|
| 107 |
+
"$type": "color",
|
| 108 |
+
"$value": "#f20000"
|
| 109 |
+
},
|
| 110 |
+
"info": {
|
| 111 |
+
"$type": "color",
|
| 112 |
+
"$value": "#33cccc"
|
| 113 |
+
},
|
| 114 |
+
"warning": {
|
| 115 |
+
"$type": "color",
|
| 116 |
+
"$value": "#ff8f00"
|
| 117 |
+
},
|
| 118 |
+
"success": {
|
| 119 |
+
"$type": "color",
|
| 120 |
+
"$value": "#65a121"
|
| 121 |
+
}
|
| 122 |
+
},
|
| 123 |
+
"#333333": {
|
| 124 |
+
"$type": "color",
|
| 125 |
+
"$value": "#333333"
|
| 126 |
+
},
|
| 127 |
+
"neutral": {
|
| 128 |
+
"400": {
|
| 129 |
+
"$type": "color",
|
| 130 |
+
"$value": "#78808e"
|
| 131 |
+
}
|
| 132 |
+
}
|
| 133 |
+
},
|
| 134 |
+
"font": {
|
| 135 |
+
"display": {
|
| 136 |
+
"2xl": {
|
| 137 |
+
"desktop": {
|
| 138 |
+
"$type": "typography",
|
| 139 |
+
"$value": {
|
| 140 |
+
"fontFamily": "Open Sans",
|
| 141 |
+
"fontSize": "68px",
|
| 142 |
+
"fontWeight": "700",
|
| 143 |
+
"lineHeight": "1.2"
|
| 144 |
+
}
|
| 145 |
+
},
|
| 146 |
+
"mobile": {
|
| 147 |
+
"$type": "typography",
|
| 148 |
+
"$value": {
|
| 149 |
+
"fontFamily": "Open Sans",
|
| 150 |
+
"fontSize": "60px",
|
| 151 |
+
"fontWeight": "700",
|
| 152 |
+
"lineHeight": "1.2"
|
| 153 |
+
}
|
| 154 |
+
}
|
| 155 |
+
},
|
| 156 |
+
"xl": {
|
| 157 |
+
"desktop": {
|
| 158 |
+
"$type": "typography",
|
| 159 |
+
"$value": {
|
| 160 |
+
"fontFamily": "Open Sans",
|
| 161 |
+
"fontSize": "58px",
|
| 162 |
+
"fontWeight": "700",
|
| 163 |
+
"lineHeight": "1.2"
|
| 164 |
+
}
|
| 165 |
+
},
|
| 166 |
+
"mobile": {
|
| 167 |
+
"$type": "typography",
|
| 168 |
+
"$value": {
|
| 169 |
+
"fontFamily": "Open Sans",
|
| 170 |
+
"fontSize": "50px",
|
| 171 |
+
"fontWeight": "700",
|
| 172 |
+
"lineHeight": "1.2"
|
| 173 |
+
}
|
| 174 |
+
}
|
| 175 |
+
},
|
| 176 |
+
"lg": {
|
| 177 |
+
"desktop": {
|
| 178 |
+
"$type": "typography",
|
| 179 |
+
"$value": {
|
| 180 |
+
"fontFamily": "Open Sans",
|
| 181 |
+
"fontSize": "48px",
|
| 182 |
+
"fontWeight": "700",
|
| 183 |
+
"lineHeight": "1.2"
|
| 184 |
+
}
|
| 185 |
+
},
|
| 186 |
+
"mobile": {
|
| 187 |
+
"$type": "typography",
|
| 188 |
+
"$value": {
|
| 189 |
+
"fontFamily": "Open Sans",
|
| 190 |
+
"fontSize": "42px",
|
| 191 |
+
"fontWeight": "700",
|
| 192 |
+
"lineHeight": "1.2"
|
| 193 |
+
}
|
| 194 |
+
}
|
| 195 |
+
},
|
| 196 |
+
"md": {
|
| 197 |
+
"desktop": {
|
| 198 |
+
"$type": "typography",
|
| 199 |
+
"$value": {
|
| 200 |
+
"fontFamily": "Open Sans",
|
| 201 |
+
"fontSize": "40px",
|
| 202 |
+
"fontWeight": "700",
|
| 203 |
+
"lineHeight": "1.2"
|
| 204 |
+
}
|
| 205 |
+
},
|
| 206 |
+
"mobile": {
|
| 207 |
+
"$type": "typography",
|
| 208 |
+
"$value": {
|
| 209 |
+
"fontFamily": "Open Sans",
|
| 210 |
+
"fontSize": "34px",
|
| 211 |
+
"fontWeight": "700",
|
| 212 |
+
"lineHeight": "1.2"
|
| 213 |
+
}
|
| 214 |
+
}
|
| 215 |
+
}
|
| 216 |
+
},
|
| 217 |
+
"heading": {
|
| 218 |
+
"xl": {
|
| 219 |
+
"desktop": {
|
| 220 |
+
"$type": "typography",
|
| 221 |
+
"$value": {
|
| 222 |
+
"fontFamily": "Open Sans",
|
| 223 |
+
"fontSize": "34px",
|
| 224 |
+
"fontWeight": "600",
|
| 225 |
+
"lineHeight": "1.3"
|
| 226 |
+
}
|
| 227 |
+
},
|
| 228 |
+
"mobile": {
|
| 229 |
+
"$type": "typography",
|
| 230 |
+
"$value": {
|
| 231 |
+
"fontFamily": "Open Sans",
|
| 232 |
+
"fontSize": "30px",
|
| 233 |
+
"fontWeight": "600",
|
| 234 |
+
"lineHeight": "1.3"
|
| 235 |
+
}
|
| 236 |
+
}
|
| 237 |
+
},
|
| 238 |
+
"lg": {
|
| 239 |
+
"desktop": {
|
| 240 |
+
"$type": "typography",
|
| 241 |
+
"$value": {
|
| 242 |
+
"fontFamily": "Open Sans",
|
| 243 |
+
"fontSize": "28px",
|
| 244 |
+
"fontWeight": "600",
|
| 245 |
+
"lineHeight": "1.3"
|
| 246 |
+
}
|
| 247 |
+
},
|
| 248 |
+
"mobile": {
|
| 249 |
+
"$type": "typography",
|
| 250 |
+
"$value": {
|
| 251 |
+
"fontFamily": "Open Sans",
|
| 252 |
+
"fontSize": "24px",
|
| 253 |
+
"fontWeight": "600",
|
| 254 |
+
"lineHeight": "1.3"
|
| 255 |
+
}
|
| 256 |
+
}
|
| 257 |
+
},
|
| 258 |
+
"md": {
|
| 259 |
+
"desktop": {
|
| 260 |
+
"$type": "typography",
|
| 261 |
+
"$value": {
|
| 262 |
+
"fontFamily": "Open Sans",
|
| 263 |
+
"fontSize": "24px",
|
| 264 |
+
"fontWeight": "600",
|
| 265 |
+
"lineHeight": "1.3"
|
| 266 |
+
}
|
| 267 |
+
},
|
| 268 |
+
"mobile": {
|
| 269 |
+
"$type": "typography",
|
| 270 |
+
"$value": {
|
| 271 |
+
"fontFamily": "Open Sans",
|
| 272 |
+
"fontSize": "20px",
|
| 273 |
+
"fontWeight": "600",
|
| 274 |
+
"lineHeight": "1.3"
|
| 275 |
+
}
|
| 276 |
+
}
|
| 277 |
+
},
|
| 278 |
+
"sm": {
|
| 279 |
+
"desktop": {
|
| 280 |
+
"$type": "typography",
|
| 281 |
+
"$value": {
|
| 282 |
+
"fontFamily": "Open Sans",
|
| 283 |
+
"fontSize": "20px",
|
| 284 |
+
"fontWeight": "600",
|
| 285 |
+
"lineHeight": "1.3"
|
| 286 |
+
}
|
| 287 |
+
},
|
| 288 |
+
"mobile": {
|
| 289 |
+
"$type": "typography",
|
| 290 |
+
"$value": {
|
| 291 |
+
"fontFamily": "Open Sans",
|
| 292 |
+
"fontSize": "16px",
|
| 293 |
+
"fontWeight": "600",
|
| 294 |
+
"lineHeight": "1.3"
|
| 295 |
+
}
|
| 296 |
+
}
|
| 297 |
+
}
|
| 298 |
+
},
|
| 299 |
+
"body": {
|
| 300 |
+
"lg": {
|
| 301 |
+
"desktop": {
|
| 302 |
+
"$type": "typography",
|
| 303 |
+
"$value": {
|
| 304 |
+
"fontFamily": "Open Sans",
|
| 305 |
+
"fontSize": "16px",
|
| 306 |
+
"fontWeight": "400",
|
| 307 |
+
"lineHeight": "1.5"
|
| 308 |
+
}
|
| 309 |
+
},
|
| 310 |
+
"mobile": {
|
| 311 |
+
"$type": "typography",
|
| 312 |
+
"$value": {
|
| 313 |
+
"fontFamily": "Open Sans",
|
| 314 |
+
"fontSize": "14px",
|
| 315 |
+
"fontWeight": "400",
|
| 316 |
+
"lineHeight": "1.5"
|
| 317 |
+
}
|
| 318 |
+
}
|
| 319 |
+
},
|
| 320 |
+
"md": {
|
| 321 |
+
"desktop": {
|
| 322 |
+
"$type": "typography",
|
| 323 |
+
"$value": {
|
| 324 |
+
"fontFamily": "Open Sans",
|
| 325 |
+
"fontSize": "14px",
|
| 326 |
+
"fontWeight": "400",
|
| 327 |
+
"lineHeight": "1.5"
|
| 328 |
+
}
|
| 329 |
+
},
|
| 330 |
+
"mobile": {
|
| 331 |
+
"$type": "typography",
|
| 332 |
+
"$value": {
|
| 333 |
+
"fontFamily": "Open Sans",
|
| 334 |
+
"fontSize": "12px",
|
| 335 |
+
"fontWeight": "400",
|
| 336 |
+
"lineHeight": "1.5"
|
| 337 |
+
}
|
| 338 |
+
}
|
| 339 |
+
},
|
| 340 |
+
"sm": {
|
| 341 |
+
"desktop": {
|
| 342 |
+
"$type": "typography",
|
| 343 |
+
"$value": {
|
| 344 |
+
"fontFamily": "Open Sans",
|
| 345 |
+
"fontSize": "12px",
|
| 346 |
+
"fontWeight": "400",
|
| 347 |
+
"lineHeight": "1.5"
|
| 348 |
+
}
|
| 349 |
+
},
|
| 350 |
+
"mobile": {
|
| 351 |
+
"$type": "typography",
|
| 352 |
+
"$value": {
|
| 353 |
+
"fontFamily": "Open Sans",
|
| 354 |
+
"fontSize": "10px",
|
| 355 |
+
"fontWeight": "400",
|
| 356 |
+
"lineHeight": "1.5"
|
| 357 |
+
}
|
| 358 |
+
}
|
| 359 |
+
}
|
| 360 |
+
},
|
| 361 |
+
"caption": {
|
| 362 |
+
"desktop": {
|
| 363 |
+
"$type": "typography",
|
| 364 |
+
"$value": {
|
| 365 |
+
"fontFamily": "Open Sans",
|
| 366 |
+
"fontSize": "10px",
|
| 367 |
+
"fontWeight": "400",
|
| 368 |
+
"lineHeight": "1.4"
|
| 369 |
+
}
|
| 370 |
+
},
|
| 371 |
+
"mobile": {
|
| 372 |
+
"$type": "typography",
|
| 373 |
+
"$value": {
|
| 374 |
+
"fontFamily": "Open Sans",
|
| 375 |
+
"fontSize": "8px",
|
| 376 |
+
"fontWeight": "400",
|
| 377 |
+
"lineHeight": "1.4"
|
| 378 |
+
}
|
| 379 |
+
}
|
| 380 |
+
},
|
| 381 |
+
"overline": {
|
| 382 |
+
"desktop": {
|
| 383 |
+
"$type": "typography",
|
| 384 |
+
"$value": {
|
| 385 |
+
"fontFamily": "Open Sans",
|
| 386 |
+
"fontSize": "8px",
|
| 387 |
+
"fontWeight": "500",
|
| 388 |
+
"lineHeight": "1.2"
|
| 389 |
+
}
|
| 390 |
+
},
|
| 391 |
+
"mobile": {
|
| 392 |
+
"$type": "typography",
|
| 393 |
+
"$value": {
|
| 394 |
+
"fontFamily": "Open Sans",
|
| 395 |
+
"fontSize": "6px",
|
| 396 |
+
"fontWeight": "500",
|
| 397 |
+
"lineHeight": "1.2"
|
| 398 |
+
}
|
| 399 |
+
}
|
| 400 |
+
}
|
| 401 |
+
},
|
| 402 |
+
"space": {
|
| 403 |
+
"1": {
|
| 404 |
+
"desktop": {
|
| 405 |
+
"$type": "dimension",
|
| 406 |
+
"$value": "8px"
|
| 407 |
+
},
|
| 408 |
+
"mobile": {
|
| 409 |
+
"$type": "dimension",
|
| 410 |
+
"$value": "8px"
|
| 411 |
+
}
|
| 412 |
+
},
|
| 413 |
+
"2": {
|
| 414 |
+
"desktop": {
|
| 415 |
+
"$type": "dimension",
|
| 416 |
+
"$value": "16px"
|
| 417 |
+
},
|
| 418 |
+
"mobile": {
|
| 419 |
+
"$type": "dimension",
|
| 420 |
+
"$value": "16px"
|
| 421 |
+
}
|
| 422 |
+
},
|
| 423 |
+
"3": {
|
| 424 |
+
"desktop": {
|
| 425 |
+
"$type": "dimension",
|
| 426 |
+
"$value": "24px"
|
| 427 |
+
},
|
| 428 |
+
"mobile": {
|
| 429 |
+
"$type": "dimension",
|
| 430 |
+
"$value": "24px"
|
| 431 |
+
}
|
| 432 |
+
},
|
| 433 |
+
"4": {
|
| 434 |
+
"desktop": {
|
| 435 |
+
"$type": "dimension",
|
| 436 |
+
"$value": "32px"
|
| 437 |
+
},
|
| 438 |
+
"mobile": {
|
| 439 |
+
"$type": "dimension",
|
| 440 |
+
"$value": "32px"
|
| 441 |
+
}
|
| 442 |
+
},
|
| 443 |
+
"5": {
|
| 444 |
+
"desktop": {
|
| 445 |
+
"$type": "dimension",
|
| 446 |
+
"$value": "40px"
|
| 447 |
+
},
|
| 448 |
+
"mobile": {
|
| 449 |
+
"$type": "dimension",
|
| 450 |
+
"$value": "40px"
|
| 451 |
+
}
|
| 452 |
+
},
|
| 453 |
+
"6": {
|
| 454 |
+
"desktop": {
|
| 455 |
+
"$type": "dimension",
|
| 456 |
+
"$value": "48px"
|
| 457 |
+
},
|
| 458 |
+
"mobile": {
|
| 459 |
+
"$type": "dimension",
|
| 460 |
+
"$value": "48px"
|
| 461 |
+
}
|
| 462 |
+
},
|
| 463 |
+
"8": {
|
| 464 |
+
"desktop": {
|
| 465 |
+
"$type": "dimension",
|
| 466 |
+
"$value": "56px"
|
| 467 |
+
},
|
| 468 |
+
"mobile": {
|
| 469 |
+
"$type": "dimension",
|
| 470 |
+
"$value": "56px"
|
| 471 |
+
}
|
| 472 |
+
},
|
| 473 |
+
"10": {
|
| 474 |
+
"desktop": {
|
| 475 |
+
"$type": "dimension",
|
| 476 |
+
"$value": "64px"
|
| 477 |
+
},
|
| 478 |
+
"mobile": {
|
| 479 |
+
"$type": "dimension",
|
| 480 |
+
"$value": "64px"
|
| 481 |
+
}
|
| 482 |
+
},
|
| 483 |
+
"12": {
|
| 484 |
+
"desktop": {
|
| 485 |
+
"$type": "dimension",
|
| 486 |
+
"$value": "72px"
|
| 487 |
+
},
|
| 488 |
+
"mobile": {
|
| 489 |
+
"$type": "dimension",
|
| 490 |
+
"$value": "72px"
|
| 491 |
+
}
|
| 492 |
+
},
|
| 493 |
+
"16": {
|
| 494 |
+
"desktop": {
|
| 495 |
+
"$type": "dimension",
|
| 496 |
+
"$value": "80px"
|
| 497 |
+
},
|
| 498 |
+
"mobile": {
|
| 499 |
+
"$type": "dimension",
|
| 500 |
+
"$value": "80px"
|
| 501 |
+
}
|
| 502 |
+
}
|
| 503 |
+
},
|
| 504 |
+
"radius": {
|
| 505 |
+
"xs": {
|
| 506 |
+
"$type": "dimension",
|
| 507 |
+
"$value": "1px"
|
| 508 |
+
},
|
| 509 |
+
"sm": {
|
| 510 |
+
"$type": "dimension",
|
| 511 |
+
"$value": "2px",
|
| 512 |
+
"3": {
|
| 513 |
+
"$type": "dimension",
|
| 514 |
+
"$value": "3px"
|
| 515 |
+
}
|
| 516 |
+
},
|
| 517 |
+
"md": {
|
| 518 |
+
"$type": "dimension",
|
| 519 |
+
"$value": "4px",
|
| 520 |
+
"5": {
|
| 521 |
+
"$type": "dimension",
|
| 522 |
+
"$value": "5px"
|
| 523 |
+
},
|
| 524 |
+
"6": {
|
| 525 |
+
"$type": "dimension",
|
| 526 |
+
"$value": "6px"
|
| 527 |
+
},
|
| 528 |
+
"100": {
|
| 529 |
+
"$type": "dimension",
|
| 530 |
+
"$value": "100px"
|
| 531 |
+
}
|
| 532 |
+
},
|
| 533 |
+
"lg": {
|
| 534 |
+
"$type": "dimension",
|
| 535 |
+
"$value": "8px",
|
| 536 |
+
"10": {
|
| 537 |
+
"$type": "dimension",
|
| 538 |
+
"$value": "10px"
|
| 539 |
+
}
|
| 540 |
+
},
|
| 541 |
+
"xl": {
|
| 542 |
+
"$type": "dimension",
|
| 543 |
+
"$value": "16px",
|
| 544 |
+
"17": {
|
| 545 |
+
"$type": "dimension",
|
| 546 |
+
"$value": "17px"
|
| 547 |
+
}
|
| 548 |
+
},
|
| 549 |
+
"2xl": {
|
| 550 |
+
"$type": "dimension",
|
| 551 |
+
"$value": "20px"
|
| 552 |
+
},
|
| 553 |
+
"3xl": {
|
| 554 |
+
"$type": "dimension",
|
| 555 |
+
"$value": "50px"
|
| 556 |
+
},
|
| 557 |
+
"full": {
|
| 558 |
+
"$type": "dimension",
|
| 559 |
+
"$value": "9999px"
|
| 560 |
+
}
|
| 561 |
+
},
|
| 562 |
+
"shadow": {
|
| 563 |
+
"xs": {
|
| 564 |
+
"$type": "shadow",
|
| 565 |
+
"$value": {
|
| 566 |
+
"color": "rgba(0, 0, 0, 0.5)",
|
| 567 |
+
"offsetX": "0px",
|
| 568 |
+
"offsetY": "2px",
|
| 569 |
+
"blur": "4px",
|
| 570 |
+
"spread": "0px"
|
| 571 |
+
}
|
| 572 |
+
},
|
| 573 |
+
"sm": {
|
| 574 |
+
"$type": "shadow",
|
| 575 |
+
"$value": {
|
| 576 |
+
"color": "rgba(0, 0, 0, 0.15)",
|
| 577 |
+
"offsetX": "0px",
|
| 578 |
+
"offsetY": "0px",
|
| 579 |
+
"blur": "16px",
|
| 580 |
+
"spread": "0px"
|
| 581 |
+
}
|
| 582 |
+
}
|
| 583 |
+
}
|
| 584 |
+
}
|