Spaces:
Sleeping
Sleeping
Commit
·
06966eb
1
Parent(s):
addf2c4
Implement complete Design Token Extractor system
Browse files- Add Gradio-based web interface for UI screenshot analysis
- Implement multi-model extraction pipeline with color, spacing, and typography detection
- Support 5 output formats: CSS Variables, Tailwind Config, JSON Tokens, Style Dictionary, SCSS
- Add computer vision-based spacing detection using OpenCV
- Include Pix2Struct integration for component understanding
- Optimize for HuggingFace Spaces deployment with resource management
- Add comprehensive error handling and fallback mechanisms
- Create modular architecture with separate extraction and generation components
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- README.md +91 -6
- app.py +292 -0
- examples/placeholder.txt +6 -0
- requirements.txt +9 -0
- test_structure.py +49 -0
- utils/__init__.py +1 -0
- utils/extractor.py +182 -0
- utils/token_generator.py +203 -0
README.md
CHANGED
|
@@ -1,13 +1,98 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
app_file: app.py
|
|
|
|
| 9 |
pinned: false
|
| 10 |
-
|
|
|
|
| 11 |
---
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Design Token Extractor
|
| 3 |
+
emoji: 🎨
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 4.44.1
|
| 8 |
app_file: app.py
|
| 9 |
+
python_version: 3.10
|
| 10 |
pinned: false
|
| 11 |
+
license: apache-2.0
|
| 12 |
+
short_description: 'Transform UI screenshots into structured design token libraries'
|
| 13 |
---
|
| 14 |
|
| 15 |
+
# 🎨 Design Token Extractor
|
| 16 |
+
|
| 17 |
+
Transform UI screenshots into structured design token libraries using AI-powered analysis.
|
| 18 |
+
|
| 19 |
+
## Features
|
| 20 |
+
|
| 21 |
+
- **Color Extraction**: Identifies dominant colors and creates semantic color roles
|
| 22 |
+
- **Spacing Detection**: Analyzes layout patterns to extract consistent spacing values
|
| 23 |
+
- **Typography Analysis**: Detects font styles and creates text hierarchy tokens
|
| 24 |
+
- **Component Recognition**: Uses vision models to understand UI components
|
| 25 |
+
- **Multiple Output Formats**: Export to CSS Variables, Tailwind Config, JSON Tokens, Style Dictionary, or SCSS
|
| 26 |
+
|
| 27 |
+
## How It Works
|
| 28 |
+
|
| 29 |
+
1. **Upload a UI Screenshot**: Drag and drop or paste from clipboard
|
| 30 |
+
2. **Select Output Format**: Choose your preferred token format
|
| 31 |
+
3. **Extract Tokens**: The system analyzes your screenshot using computer vision
|
| 32 |
+
4. **Download Results**: Get your design tokens in the selected format
|
| 33 |
+
|
| 34 |
+
## Technology Stack
|
| 35 |
+
|
| 36 |
+
- **Gradio**: Interactive web interface
|
| 37 |
+
- **Colorgram.py**: Fast color extraction
|
| 38 |
+
- **OpenCV**: Image processing and spacing detection
|
| 39 |
+
- **Pix2Struct**: Layout and component understanding
|
| 40 |
+
- **PyTorch**: Deep learning framework
|
| 41 |
+
|
| 42 |
+
## Output Formats
|
| 43 |
+
|
| 44 |
+
### CSS Variables
|
| 45 |
+
```css
|
| 46 |
+
:root {
|
| 47 |
+
--color-primary: #3B82F6;
|
| 48 |
+
--spacing-medium: 16px;
|
| 49 |
+
--font-heading: sans-serif;
|
| 50 |
+
}
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
### Tailwind Config
|
| 54 |
+
```javascript
|
| 55 |
+
module.exports = {
|
| 56 |
+
theme: {
|
| 57 |
+
extend: {
|
| 58 |
+
colors: {
|
| 59 |
+
primary: '#3B82F6'
|
| 60 |
+
}
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
}
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
### JSON Tokens (W3C Format)
|
| 67 |
+
```json
|
| 68 |
+
{
|
| 69 |
+
"color": {
|
| 70 |
+
"primary": {
|
| 71 |
+
"$value": "#3B82F6",
|
| 72 |
+
"$type": "color"
|
| 73 |
+
}
|
| 74 |
+
}
|
| 75 |
+
}
|
| 76 |
+
```
|
| 77 |
+
|
| 78 |
+
## Tips for Best Results
|
| 79 |
+
|
| 80 |
+
- Use high-quality screenshots (minimum 800px width)
|
| 81 |
+
- Include various UI elements for comprehensive extraction
|
| 82 |
+
- Screenshots with clear color hierarchy work best
|
| 83 |
+
- Ensure good contrast between elements
|
| 84 |
+
|
| 85 |
+
## Development
|
| 86 |
+
|
| 87 |
+
To run locally:
|
| 88 |
+
|
| 89 |
+
```bash
|
| 90 |
+
pip install -r requirements.txt
|
| 91 |
+
python app.py
|
| 92 |
+
```
|
| 93 |
+
|
| 94 |
+
## License
|
| 95 |
+
|
| 96 |
+
Apache 2.0
|
| 97 |
+
|
| 98 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
app.py
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import json
|
| 3 |
+
import os
|
| 4 |
+
from PIL import Image
|
| 5 |
+
import tempfile
|
| 6 |
+
from utils.extractor import DesignTokenExtractor
|
| 7 |
+
from utils.token_generator import TokenCodeGenerator
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def create_token_preview(tokens):
|
| 11 |
+
"""Create HTML preview of extracted tokens"""
|
| 12 |
+
html = """
|
| 13 |
+
<div style="font-family: system-ui, sans-serif; padding: 20px; background: #f9fafb; border-radius: 8px;">
|
| 14 |
+
<h3 style="margin-top: 0; color: #1f2937;">Extracted Design Tokens</h3>
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
# Color palette preview
|
| 18 |
+
if 'colors' in tokens and tokens['colors']:
|
| 19 |
+
html += """
|
| 20 |
+
<div style="margin-bottom: 24px;">
|
| 21 |
+
<h4 style="color: #6b7280; font-size: 14px; text-transform: uppercase; letter-spacing: 0.05em;">Colors</h4>
|
| 22 |
+
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
|
| 23 |
+
"""
|
| 24 |
+
for name, color in tokens['colors'].items():
|
| 25 |
+
html += f"""
|
| 26 |
+
<div style="text-align: center;">
|
| 27 |
+
<div style="width: 80px; height: 80px; background: {color['hex']};
|
| 28 |
+
border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);"></div>
|
| 29 |
+
<div style="margin-top: 8px;">
|
| 30 |
+
<div style="font-size: 12px; font-weight: 600; color: #374151;">{name}</div>
|
| 31 |
+
<div style="font-size: 11px; color: #9ca3af;">{color['hex']}</div>
|
| 32 |
+
<div style="font-size: 10px; color: #9ca3af;">{int(color.get('proportion', 0) * 100)}%</div>
|
| 33 |
+
</div>
|
| 34 |
+
</div>
|
| 35 |
+
"""
|
| 36 |
+
html += "</div></div>"
|
| 37 |
+
|
| 38 |
+
# Spacing preview
|
| 39 |
+
if 'spacing' in tokens and tokens['spacing']:
|
| 40 |
+
html += """
|
| 41 |
+
<div style="margin-bottom: 24px;">
|
| 42 |
+
<h4 style="color: #6b7280; font-size: 14px; text-transform: uppercase; letter-spacing: 0.05em;">Spacing</h4>
|
| 43 |
+
<div style="display: flex; gap: 16px; align-items: flex-end;">
|
| 44 |
+
"""
|
| 45 |
+
for name, value in tokens['spacing'].items():
|
| 46 |
+
try:
|
| 47 |
+
height = value.replace('px', '')
|
| 48 |
+
html += f"""
|
| 49 |
+
<div style="text-align: center;">
|
| 50 |
+
<div style="width: 60px; height: {height}px; background: #3b82f6;
|
| 51 |
+
border-radius: 4px; opacity: 0.8;"></div>
|
| 52 |
+
<div style="margin-top: 8px;">
|
| 53 |
+
<div style="font-size: 12px; font-weight: 600; color: #374151;">{name}</div>
|
| 54 |
+
<div style="font-size: 11px; color: #9ca3af;">{value}</div>
|
| 55 |
+
</div>
|
| 56 |
+
</div>
|
| 57 |
+
"""
|
| 58 |
+
except:
|
| 59 |
+
pass
|
| 60 |
+
html += "</div></div>"
|
| 61 |
+
|
| 62 |
+
# Typography preview
|
| 63 |
+
if 'typography' in tokens and tokens['typography']:
|
| 64 |
+
html += """
|
| 65 |
+
<div style="margin-bottom: 24px;">
|
| 66 |
+
<h4 style="color: #6b7280; font-size: 14px; text-transform: uppercase; letter-spacing: 0.05em;">Typography</h4>
|
| 67 |
+
"""
|
| 68 |
+
for name, props in tokens['typography'].items():
|
| 69 |
+
size = props.get('size', '16px')
|
| 70 |
+
weight = props.get('weight', '400')
|
| 71 |
+
family = props.get('family', 'sans-serif')
|
| 72 |
+
html += f"""
|
| 73 |
+
<div style="margin-bottom: 12px; padding: 12px; background: white; border-radius: 6px;">
|
| 74 |
+
<div style="font-size: {size}; font-weight: {weight}; font-family: {family}; color: #1f2937;">
|
| 75 |
+
Sample {name.title()} Text
|
| 76 |
+
</div>
|
| 77 |
+
<div style="font-size: 11px; color: #9ca3af; margin-top: 4px;">
|
| 78 |
+
{family} • {size} • Weight {weight}
|
| 79 |
+
</div>
|
| 80 |
+
</div>
|
| 81 |
+
"""
|
| 82 |
+
html += "</div>"
|
| 83 |
+
|
| 84 |
+
html += "</div>"
|
| 85 |
+
return html
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def process_screenshot(image, output_format, progress=gr.Progress()):
|
| 89 |
+
"""Process uploaded screenshot and extract design tokens"""
|
| 90 |
+
if image is None:
|
| 91 |
+
return None, "Please upload a screenshot", None
|
| 92 |
+
|
| 93 |
+
extractor = DesignTokenExtractor()
|
| 94 |
+
generator = TokenCodeGenerator()
|
| 95 |
+
|
| 96 |
+
try:
|
| 97 |
+
progress(0.1, desc="Initializing extraction...")
|
| 98 |
+
|
| 99 |
+
# Resize image if needed
|
| 100 |
+
image = extractor.resize_for_processing(image)
|
| 101 |
+
|
| 102 |
+
# Save temporary file for colorgram
|
| 103 |
+
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp:
|
| 104 |
+
temp_path = tmp.name
|
| 105 |
+
image.save(temp_path)
|
| 106 |
+
|
| 107 |
+
progress(0.3, desc="Extracting colors...")
|
| 108 |
+
colors = extractor.extract_colors(temp_path)
|
| 109 |
+
|
| 110 |
+
progress(0.5, desc="Detecting spacing...")
|
| 111 |
+
spacing = extractor.detect_spacing(image)
|
| 112 |
+
|
| 113 |
+
progress(0.6, desc="Analyzing typography...")
|
| 114 |
+
typography = extractor.detect_typography(image)
|
| 115 |
+
|
| 116 |
+
progress(0.7, desc="Analyzing components...")
|
| 117 |
+
components = extractor.analyze_components(image)
|
| 118 |
+
|
| 119 |
+
# Combine all tokens
|
| 120 |
+
tokens = {
|
| 121 |
+
"colors": colors,
|
| 122 |
+
"spacing": spacing,
|
| 123 |
+
"typography": typography,
|
| 124 |
+
"components": components
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
progress(0.8, desc="Generating code...")
|
| 128 |
+
|
| 129 |
+
# Generate output based on selected format
|
| 130 |
+
if output_format == "CSS Variables":
|
| 131 |
+
code_output = generator.generate_css_variables(tokens)
|
| 132 |
+
file_ext = "css"
|
| 133 |
+
elif output_format == "Tailwind Config":
|
| 134 |
+
code_output = generator.generate_tailwind_config(tokens)
|
| 135 |
+
file_ext = "js"
|
| 136 |
+
elif output_format == "JSON Tokens":
|
| 137 |
+
code_output = generator.generate_json_tokens(tokens)
|
| 138 |
+
file_ext = "json"
|
| 139 |
+
elif output_format == "Style Dictionary":
|
| 140 |
+
code_output = generator.generate_style_dictionary(tokens)
|
| 141 |
+
file_ext = "json"
|
| 142 |
+
elif output_format == "SCSS Variables":
|
| 143 |
+
code_output = generator.generate_scss_variables(tokens)
|
| 144 |
+
file_ext = "scss"
|
| 145 |
+
else:
|
| 146 |
+
code_output = json.dumps(tokens, indent=2)
|
| 147 |
+
file_ext = "json"
|
| 148 |
+
|
| 149 |
+
# Save output file
|
| 150 |
+
output_filename = f"design_tokens.{file_ext}"
|
| 151 |
+
with open(output_filename, "w") as f:
|
| 152 |
+
f.write(code_output)
|
| 153 |
+
|
| 154 |
+
# Clean up temp file
|
| 155 |
+
try:
|
| 156 |
+
os.unlink(temp_path)
|
| 157 |
+
except:
|
| 158 |
+
pass
|
| 159 |
+
|
| 160 |
+
progress(1.0, desc="Complete!")
|
| 161 |
+
|
| 162 |
+
# Create preview visualization
|
| 163 |
+
preview_html = create_token_preview(tokens)
|
| 164 |
+
|
| 165 |
+
return preview_html, code_output, output_filename
|
| 166 |
+
|
| 167 |
+
except Exception as e:
|
| 168 |
+
return None, f"Error processing screenshot: {str(e)}", None
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def create_gradio_app():
|
| 172 |
+
"""Create the main Gradio application"""
|
| 173 |
+
|
| 174 |
+
with gr.Blocks(
|
| 175 |
+
title="Design Token Extractor",
|
| 176 |
+
theme=gr.themes.Soft(),
|
| 177 |
+
css="""
|
| 178 |
+
.gradio-container {
|
| 179 |
+
font-family: 'Inter', system-ui, sans-serif;
|
| 180 |
+
}
|
| 181 |
+
.gr-button-primary {
|
| 182 |
+
background-color: #3b82f6 !important;
|
| 183 |
+
}
|
| 184 |
+
"""
|
| 185 |
+
) as app:
|
| 186 |
+
gr.Markdown(
|
| 187 |
+
"""
|
| 188 |
+
# 🎨 Design Token Extractor
|
| 189 |
+
|
| 190 |
+
Transform UI screenshots into structured design token libraries using AI-powered analysis.
|
| 191 |
+
Upload a screenshot to automatically extract colors, spacing, typography, and component tokens.
|
| 192 |
+
|
| 193 |
+
---
|
| 194 |
+
"""
|
| 195 |
+
)
|
| 196 |
+
|
| 197 |
+
with gr.Row():
|
| 198 |
+
with gr.Column(scale=1):
|
| 199 |
+
input_image = gr.Image(
|
| 200 |
+
label="Upload UI Screenshot",
|
| 201 |
+
type="pil",
|
| 202 |
+
sources=['upload', 'clipboard'],
|
| 203 |
+
height=400
|
| 204 |
+
)
|
| 205 |
+
|
| 206 |
+
output_format = gr.Radio(
|
| 207 |
+
choices=[
|
| 208 |
+
"CSS Variables",
|
| 209 |
+
"Tailwind Config",
|
| 210 |
+
"JSON Tokens",
|
| 211 |
+
"Style Dictionary",
|
| 212 |
+
"SCSS Variables"
|
| 213 |
+
],
|
| 214 |
+
value="CSS Variables",
|
| 215 |
+
label="Output Format",
|
| 216 |
+
info="Choose the format for your design tokens"
|
| 217 |
+
)
|
| 218 |
+
|
| 219 |
+
extract_btn = gr.Button(
|
| 220 |
+
"🚀 Extract Design Tokens",
|
| 221 |
+
variant="primary",
|
| 222 |
+
size="lg"
|
| 223 |
+
)
|
| 224 |
+
|
| 225 |
+
gr.Markdown(
|
| 226 |
+
"""
|
| 227 |
+
### Tips for best results:
|
| 228 |
+
- Use high-quality screenshots (min 800px width)
|
| 229 |
+
- Include various UI elements for comprehensive extraction
|
| 230 |
+
- Screenshots with clear color hierarchy work best
|
| 231 |
+
- Ensure good contrast between elements
|
| 232 |
+
"""
|
| 233 |
+
)
|
| 234 |
+
|
| 235 |
+
with gr.Column(scale=1):
|
| 236 |
+
preview = gr.HTML(
|
| 237 |
+
label="Token Preview",
|
| 238 |
+
value="<div style='padding: 20px; text-align: center; color: #9ca3af;'>Upload a screenshot to see extracted tokens</div>"
|
| 239 |
+
)
|
| 240 |
+
|
| 241 |
+
code_output = gr.Code(
|
| 242 |
+
label="Generated Code",
|
| 243 |
+
language="css",
|
| 244 |
+
lines=20,
|
| 245 |
+
value="// Your design tokens will appear here"
|
| 246 |
+
)
|
| 247 |
+
|
| 248 |
+
download_file = gr.File(
|
| 249 |
+
label="Download Tokens",
|
| 250 |
+
visible=True
|
| 251 |
+
)
|
| 252 |
+
|
| 253 |
+
# Add examples
|
| 254 |
+
gr.Markdown("### Example Screenshots")
|
| 255 |
+
gr.Examples(
|
| 256 |
+
examples=[
|
| 257 |
+
["examples/dashboard.png", "CSS Variables"],
|
| 258 |
+
["examples/landing_page.png", "Tailwind Config"],
|
| 259 |
+
["examples/mobile_app.png", "JSON Tokens"]
|
| 260 |
+
],
|
| 261 |
+
inputs=[input_image, output_format],
|
| 262 |
+
cache_examples=False
|
| 263 |
+
)
|
| 264 |
+
|
| 265 |
+
# Connect the extraction function
|
| 266 |
+
extract_btn.click(
|
| 267 |
+
fn=process_screenshot,
|
| 268 |
+
inputs=[input_image, output_format],
|
| 269 |
+
outputs=[preview, code_output, download_file]
|
| 270 |
+
)
|
| 271 |
+
|
| 272 |
+
# Add footer
|
| 273 |
+
gr.Markdown(
|
| 274 |
+
"""
|
| 275 |
+
---
|
| 276 |
+
|
| 277 |
+
### Features:
|
| 278 |
+
- **Color Extraction**: Identifies dominant colors and creates semantic color roles
|
| 279 |
+
- **Spacing Detection**: Analyzes layout patterns to extract consistent spacing values
|
| 280 |
+
- **Typography Analysis**: Detects font styles and creates text hierarchy tokens
|
| 281 |
+
- **Multiple Output Formats**: Export to CSS, Tailwind, JSON, Style Dictionary, or SCSS
|
| 282 |
+
|
| 283 |
+
Built with ❤️ using Gradio and computer vision models
|
| 284 |
+
"""
|
| 285 |
+
)
|
| 286 |
+
|
| 287 |
+
return app
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
if __name__ == "__main__":
|
| 291 |
+
app = create_gradio_app()
|
| 292 |
+
app.launch()
|
examples/placeholder.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Add your example screenshots here:
|
| 2 |
+
- dashboard.png
|
| 3 |
+
- landing_page.png
|
| 4 |
+
- mobile_app.png
|
| 5 |
+
|
| 6 |
+
These will be used as examples in the Gradio interface.
|
requirements.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
transformers>=4.35.0
|
| 2 |
+
torch>=2.1.0
|
| 3 |
+
torchvision>=0.16.0
|
| 4 |
+
Pillow>=10.0.0
|
| 5 |
+
opencv-python-headless==4.8.0.74
|
| 6 |
+
colorgram.py==1.2.0
|
| 7 |
+
gradio>=4.44.1
|
| 8 |
+
numpy>=1.24.0
|
| 9 |
+
huggingface-hub>=0.19.0
|
test_structure.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Test script to verify project structure"""
|
| 3 |
+
|
| 4 |
+
import os
|
| 5 |
+
import sys
|
| 6 |
+
|
| 7 |
+
def check_file_exists(filepath, description):
|
| 8 |
+
if os.path.exists(filepath):
|
| 9 |
+
print(f"[OK] {description}: {filepath}")
|
| 10 |
+
return True
|
| 11 |
+
else:
|
| 12 |
+
print(f"[MISSING] {description}: {filepath} NOT FOUND")
|
| 13 |
+
return False
|
| 14 |
+
|
| 15 |
+
def main():
|
| 16 |
+
print("Design Token Extractor - Project Structure Check")
|
| 17 |
+
print("=" * 50)
|
| 18 |
+
|
| 19 |
+
checks = [
|
| 20 |
+
("app.py", "Main application file"),
|
| 21 |
+
("requirements.txt", "Dependencies file"),
|
| 22 |
+
("README.md", "Documentation with HF config"),
|
| 23 |
+
("utils/__init__.py", "Utils module init"),
|
| 24 |
+
("utils/extractor.py", "Core extraction pipeline"),
|
| 25 |
+
("utils/token_generator.py", "Token code generator"),
|
| 26 |
+
("examples/", "Examples directory"),
|
| 27 |
+
("models/", "Models directory"),
|
| 28 |
+
("assets/", "Assets directory")
|
| 29 |
+
]
|
| 30 |
+
|
| 31 |
+
all_good = True
|
| 32 |
+
for filepath, description in checks:
|
| 33 |
+
if not check_file_exists(filepath, description):
|
| 34 |
+
all_good = False
|
| 35 |
+
|
| 36 |
+
print("=" * 50)
|
| 37 |
+
if all_good:
|
| 38 |
+
print("[SUCCESS] All project files are in place!")
|
| 39 |
+
print("\nTo deploy to Hugging Face Spaces:")
|
| 40 |
+
print("1. Install Git LFS: git lfs install")
|
| 41 |
+
print("2. Add remote: git remote add hf https://huggingface.co/spaces/YOUR_USERNAME/DesignTokenExtractor")
|
| 42 |
+
print("3. Push to HF: git push hf main")
|
| 43 |
+
else:
|
| 44 |
+
print("[ERROR] Some files are missing. Please check the structure.")
|
| 45 |
+
|
| 46 |
+
return 0 if all_good else 1
|
| 47 |
+
|
| 48 |
+
if __name__ == "__main__":
|
| 49 |
+
sys.exit(main())
|
utils/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Utils module for Design Token Extractor
|
utils/extractor.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import colorgram
|
| 2 |
+
import cv2
|
| 3 |
+
import numpy as np
|
| 4 |
+
from PIL import Image
|
| 5 |
+
import json
|
| 6 |
+
import torch
|
| 7 |
+
from transformers import Pix2StructForConditionalGeneration, Pix2StructProcessor
|
| 8 |
+
import functools
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class DesignTokenExtractor:
|
| 12 |
+
def __init__(self):
|
| 13 |
+
# Load models once at startup
|
| 14 |
+
self.pix2struct_model = None
|
| 15 |
+
self.pix2struct_processor = None
|
| 16 |
+
self._load_models()
|
| 17 |
+
|
| 18 |
+
@functools.lru_cache(maxsize=1)
|
| 19 |
+
def _load_models(self):
|
| 20 |
+
"""Load models with caching to prevent repeated initialization"""
|
| 21 |
+
try:
|
| 22 |
+
self.pix2struct_processor = Pix2StructProcessor.from_pretrained(
|
| 23 |
+
"google/pix2struct-screen2words-base"
|
| 24 |
+
)
|
| 25 |
+
self.pix2struct_model = Pix2StructForConditionalGeneration.from_pretrained(
|
| 26 |
+
"google/pix2struct-screen2words-base",
|
| 27 |
+
torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32
|
| 28 |
+
)
|
| 29 |
+
except Exception as e:
|
| 30 |
+
print(f"Warning: Could not load Pix2Struct model: {e}")
|
| 31 |
+
# Continue without the model for basic extraction
|
| 32 |
+
|
| 33 |
+
def extract_colors(self, image_path, num_colors=8):
|
| 34 |
+
"""Extract dominant colors using colorgram"""
|
| 35 |
+
try:
|
| 36 |
+
colors = colorgram.extract(image_path, num_colors)
|
| 37 |
+
palette = {}
|
| 38 |
+
|
| 39 |
+
for i, color in enumerate(colors):
|
| 40 |
+
# Determine semantic color role based on proportion
|
| 41 |
+
if i == 0 and color.proportion > 0.3:
|
| 42 |
+
name = "background"
|
| 43 |
+
elif i == 1:
|
| 44 |
+
name = "primary"
|
| 45 |
+
elif i == 2:
|
| 46 |
+
name = "secondary"
|
| 47 |
+
else:
|
| 48 |
+
name = f"accent-{i-2}"
|
| 49 |
+
|
| 50 |
+
palette[name] = {
|
| 51 |
+
"hex": f"#{color.rgb.r:02x}{color.rgb.g:02x}{color.rgb.b:02x}",
|
| 52 |
+
"rgb": f"rgb({color.rgb.r}, {color.rgb.g}, {color.rgb.b})",
|
| 53 |
+
"proportion": round(color.proportion, 3)
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
return palette
|
| 57 |
+
except Exception as e:
|
| 58 |
+
print(f"Error extracting colors: {e}")
|
| 59 |
+
return self._get_default_colors()
|
| 60 |
+
|
| 61 |
+
def detect_spacing(self, image):
|
| 62 |
+
"""Analyze spacing patterns using OpenCV"""
|
| 63 |
+
try:
|
| 64 |
+
gray = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2GRAY)
|
| 65 |
+
edges = cv2.Canny(gray, 50, 150)
|
| 66 |
+
|
| 67 |
+
# Find contours for element detection
|
| 68 |
+
contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
| 69 |
+
|
| 70 |
+
# Calculate spacing between elements
|
| 71 |
+
bounding_boxes = [cv2.boundingRect(c) for c in contours if cv2.contourArea(c) > 100]
|
| 72 |
+
|
| 73 |
+
if len(bounding_boxes) > 1:
|
| 74 |
+
# Sort by y-coordinate to find vertical spacing
|
| 75 |
+
bounding_boxes.sort(key=lambda x: x[1])
|
| 76 |
+
|
| 77 |
+
vertical_gaps = []
|
| 78 |
+
for i in range(len(bounding_boxes)-1):
|
| 79 |
+
gap = bounding_boxes[i+1][1] - (bounding_boxes[i][1] + bounding_boxes[i][3])
|
| 80 |
+
if gap > 0:
|
| 81 |
+
vertical_gaps.append(gap)
|
| 82 |
+
|
| 83 |
+
# Find common spacing values using clustering
|
| 84 |
+
spacing_system = self._cluster_spacing_values(vertical_gaps)
|
| 85 |
+
return spacing_system
|
| 86 |
+
except Exception as e:
|
| 87 |
+
print(f"Error detecting spacing: {e}")
|
| 88 |
+
|
| 89 |
+
return {"small": "8px", "medium": "16px", "large": "32px"} # Defaults
|
| 90 |
+
|
| 91 |
+
def _cluster_spacing_values(self, gaps):
|
| 92 |
+
"""Group similar spacing values"""
|
| 93 |
+
if not gaps:
|
| 94 |
+
return {"small": "8px", "medium": "16px", "large": "32px"}
|
| 95 |
+
|
| 96 |
+
gaps.sort()
|
| 97 |
+
|
| 98 |
+
# Simple clustering for common spacing values
|
| 99 |
+
unique_gaps = list(set(gaps))
|
| 100 |
+
|
| 101 |
+
if len(unique_gaps) >= 3:
|
| 102 |
+
return {
|
| 103 |
+
"small": f"{unique_gaps[0]}px",
|
| 104 |
+
"medium": f"{unique_gaps[len(unique_gaps)//2]}px",
|
| 105 |
+
"large": f"{unique_gaps[-1]}px"
|
| 106 |
+
}
|
| 107 |
+
elif len(unique_gaps) == 2:
|
| 108 |
+
return {
|
| 109 |
+
"small": f"{unique_gaps[0]}px",
|
| 110 |
+
"large": f"{unique_gaps[1]}px"
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
return {"base": f"{unique_gaps[0]}px" if unique_gaps else "16px"}
|
| 114 |
+
|
| 115 |
+
def analyze_components(self, image):
|
| 116 |
+
"""Use Pix2Struct for component understanding"""
|
| 117 |
+
if self.pix2struct_model is None or self.pix2struct_processor is None:
|
| 118 |
+
# Fallback if model loading failed
|
| 119 |
+
return {
|
| 120 |
+
"detected_elements": "Model not available - basic extraction only",
|
| 121 |
+
"layout": "responsive"
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
try:
|
| 125 |
+
inputs = self.pix2struct_processor(images=image, return_tensors="pt")
|
| 126 |
+
|
| 127 |
+
with torch.no_grad():
|
| 128 |
+
generated_ids = self.pix2struct_model.generate(**inputs, max_length=100)
|
| 129 |
+
|
| 130 |
+
description = self.pix2struct_processor.batch_decode(generated_ids, skip_special_tokens=True)[0]
|
| 131 |
+
|
| 132 |
+
# Parse description for component types
|
| 133 |
+
components = {
|
| 134 |
+
"detected_elements": description,
|
| 135 |
+
"layout": "responsive" if "responsive" in description.lower() else "fixed"
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
return components
|
| 139 |
+
except Exception as e:
|
| 140 |
+
print(f"Error analyzing components: {e}")
|
| 141 |
+
return {
|
| 142 |
+
"detected_elements": "Error during analysis",
|
| 143 |
+
"layout": "responsive"
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
def detect_typography(self, image):
|
| 147 |
+
"""Basic typography detection"""
|
| 148 |
+
# Simplified typography detection without EasyOCR for initial implementation
|
| 149 |
+
return {
|
| 150 |
+
"heading": {
|
| 151 |
+
"family": "sans-serif",
|
| 152 |
+
"size": "32px",
|
| 153 |
+
"weight": "700"
|
| 154 |
+
},
|
| 155 |
+
"body": {
|
| 156 |
+
"family": "sans-serif",
|
| 157 |
+
"size": "16px",
|
| 158 |
+
"weight": "400"
|
| 159 |
+
},
|
| 160 |
+
"caption": {
|
| 161 |
+
"family": "sans-serif",
|
| 162 |
+
"size": "14px",
|
| 163 |
+
"weight": "400"
|
| 164 |
+
}
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
def _get_default_colors(self):
|
| 168 |
+
"""Return default color palette"""
|
| 169 |
+
return {
|
| 170 |
+
"primary": {"hex": "#3B82F6", "rgb": "rgb(59, 130, 246)", "proportion": 0.25},
|
| 171 |
+
"secondary": {"hex": "#8B5CF6", "rgb": "rgb(139, 92, 246)", "proportion": 0.15},
|
| 172 |
+
"background": {"hex": "#FFFFFF", "rgb": "rgb(255, 255, 255)", "proportion": 0.40},
|
| 173 |
+
"text": {"hex": "#1F2937", "rgb": "rgb(31, 41, 55)", "proportion": 0.20}
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
def resize_for_processing(self, image, max_dimension=1024):
|
| 177 |
+
"""Resize large images while maintaining aspect ratio"""
|
| 178 |
+
if max(image.size) > max_dimension:
|
| 179 |
+
ratio = max_dimension / max(image.size)
|
| 180 |
+
new_size = tuple(int(dim * ratio) for dim in image.size)
|
| 181 |
+
return image.resize(new_size, Image.Resampling.LANCZOS)
|
| 182 |
+
return image
|
utils/token_generator.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class TokenCodeGenerator:
|
| 5 |
+
def generate_css_variables(self, tokens):
|
| 6 |
+
"""Generate CSS custom properties"""
|
| 7 |
+
css = ":root {\n"
|
| 8 |
+
|
| 9 |
+
# Colors
|
| 10 |
+
for name, color in tokens.get('colors', {}).items():
|
| 11 |
+
css += f" --color-{name}: {color['hex']};\n"
|
| 12 |
+
css += f" --color-{name}-rgb: {color['rgb']};\n"
|
| 13 |
+
|
| 14 |
+
css += "\n"
|
| 15 |
+
|
| 16 |
+
# Spacing
|
| 17 |
+
for name, value in tokens.get('spacing', {}).items():
|
| 18 |
+
css += f" --spacing-{name}: {value};\n"
|
| 19 |
+
|
| 20 |
+
css += "\n"
|
| 21 |
+
|
| 22 |
+
# Typography
|
| 23 |
+
if 'typography' in tokens:
|
| 24 |
+
for name, props in tokens['typography'].items():
|
| 25 |
+
css += f" --font-{name}: {props.get('family', 'sans-serif')};\n"
|
| 26 |
+
css += f" --font-size-{name}: {props.get('size', '16px')};\n"
|
| 27 |
+
css += f" --font-weight-{name}: {props.get('weight', '400')};\n"
|
| 28 |
+
|
| 29 |
+
css += "}\n\n"
|
| 30 |
+
|
| 31 |
+
# Add example usage comments
|
| 32 |
+
css += "/* Example usage:\n"
|
| 33 |
+
css += " * color: var(--color-primary);\n"
|
| 34 |
+
css += " * padding: var(--spacing-medium);\n"
|
| 35 |
+
css += " * font-family: var(--font-body);\n"
|
| 36 |
+
css += " */\n"
|
| 37 |
+
|
| 38 |
+
return css
|
| 39 |
+
|
| 40 |
+
def generate_tailwind_config(self, tokens):
|
| 41 |
+
"""Generate Tailwind configuration"""
|
| 42 |
+
config = {
|
| 43 |
+
"theme": {
|
| 44 |
+
"extend": {
|
| 45 |
+
"colors": {},
|
| 46 |
+
"spacing": {},
|
| 47 |
+
"fontFamily": {},
|
| 48 |
+
"fontSize": {},
|
| 49 |
+
"fontWeight": {}
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
# Add colors
|
| 55 |
+
for name, color in tokens.get('colors', {}).items():
|
| 56 |
+
config["theme"]["extend"]["colors"][name] = color['hex']
|
| 57 |
+
|
| 58 |
+
# Add spacing
|
| 59 |
+
for name, value in tokens.get('spacing', {}).items():
|
| 60 |
+
config["theme"]["extend"]["spacing"][name] = value
|
| 61 |
+
|
| 62 |
+
# Add typography
|
| 63 |
+
if 'typography' in tokens:
|
| 64 |
+
for name, props in tokens['typography'].items():
|
| 65 |
+
if 'family' in props:
|
| 66 |
+
config["theme"]["extend"]["fontFamily"][name] = props['family']
|
| 67 |
+
if 'size' in props:
|
| 68 |
+
config["theme"]["extend"]["fontSize"][name] = props['size']
|
| 69 |
+
if 'weight' in props:
|
| 70 |
+
config["theme"]["extend"]["fontWeight"][name] = props['weight']
|
| 71 |
+
|
| 72 |
+
# Format as JavaScript module
|
| 73 |
+
output = "/** @type {import('tailwindcss').Config} */\n"
|
| 74 |
+
output += f"module.exports = {json.dumps(config, indent=2)}"
|
| 75 |
+
|
| 76 |
+
return output
|
| 77 |
+
|
| 78 |
+
def generate_json_tokens(self, tokens):
|
| 79 |
+
"""Generate W3C Design Token Community Group format"""
|
| 80 |
+
formatted_tokens = {
|
| 81 |
+
"$schema": "https://design-tokens.github.io/community-group/format.json",
|
| 82 |
+
"tokens": {}
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
# Colors
|
| 86 |
+
if 'colors' in tokens:
|
| 87 |
+
formatted_tokens["tokens"]["color"] = {}
|
| 88 |
+
for name, color in tokens['colors'].items():
|
| 89 |
+
formatted_tokens["tokens"]["color"][name] = {
|
| 90 |
+
"$value": color['hex'],
|
| 91 |
+
"$type": "color",
|
| 92 |
+
"$description": f"Color {name} - {color.get('proportion', 0)*100:.1f}% of design"
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
# Spacing
|
| 96 |
+
if 'spacing' in tokens:
|
| 97 |
+
formatted_tokens["tokens"]["spacing"] = {}
|
| 98 |
+
for name, value in tokens['spacing'].items():
|
| 99 |
+
formatted_tokens["tokens"]["spacing"][name] = {
|
| 100 |
+
"$value": value,
|
| 101 |
+
"$type": "dimension"
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
# Typography
|
| 105 |
+
if 'typography' in tokens:
|
| 106 |
+
formatted_tokens["tokens"]["typography"] = {}
|
| 107 |
+
for name, props in tokens['typography'].items():
|
| 108 |
+
formatted_tokens["tokens"]["typography"][name] = {
|
| 109 |
+
"fontFamily": {
|
| 110 |
+
"$value": props.get('family', 'sans-serif'),
|
| 111 |
+
"$type": "fontFamily"
|
| 112 |
+
},
|
| 113 |
+
"fontSize": {
|
| 114 |
+
"$value": props.get('size', '16px'),
|
| 115 |
+
"$type": "dimension"
|
| 116 |
+
},
|
| 117 |
+
"fontWeight": {
|
| 118 |
+
"$value": props.get('weight', '400'),
|
| 119 |
+
"$type": "fontWeight"
|
| 120 |
+
}
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
return json.dumps(formatted_tokens, indent=2)
|
| 124 |
+
|
| 125 |
+
def generate_style_dictionary(self, tokens):
|
| 126 |
+
"""Generate Style Dictionary format tokens"""
|
| 127 |
+
sd_tokens = {
|
| 128 |
+
"color": {},
|
| 129 |
+
"spacing": {},
|
| 130 |
+
"typography": {}
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
# Transform colors
|
| 134 |
+
for name, color in tokens.get('colors', {}).items():
|
| 135 |
+
sd_tokens["color"][name] = {
|
| 136 |
+
"value": color['hex'],
|
| 137 |
+
"type": "color",
|
| 138 |
+
"attributes": {
|
| 139 |
+
"rgb": color.get('rgb', ''),
|
| 140 |
+
"proportion": color.get('proportion', 0)
|
| 141 |
+
}
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
# Transform spacing
|
| 145 |
+
for name, value in tokens.get('spacing', {}).items():
|
| 146 |
+
sd_tokens["spacing"][name] = {
|
| 147 |
+
"value": value,
|
| 148 |
+
"type": "spacing"
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
# Transform typography
|
| 152 |
+
if 'typography' in tokens:
|
| 153 |
+
for name, props in tokens['typography'].items():
|
| 154 |
+
sd_tokens["typography"][name] = {
|
| 155 |
+
"fontFamily": {
|
| 156 |
+
"value": props.get('family', 'sans-serif')
|
| 157 |
+
},
|
| 158 |
+
"fontSize": {
|
| 159 |
+
"value": props.get('size', '16px')
|
| 160 |
+
},
|
| 161 |
+
"fontWeight": {
|
| 162 |
+
"value": props.get('weight', '400')
|
| 163 |
+
}
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
return json.dumps(sd_tokens, indent=2)
|
| 167 |
+
|
| 168 |
+
def generate_scss_variables(self, tokens):
|
| 169 |
+
"""Generate SCSS variables"""
|
| 170 |
+
scss = "// Design Tokens - SCSS Variables\n\n"
|
| 171 |
+
|
| 172 |
+
# Colors
|
| 173 |
+
scss += "// Colors\n"
|
| 174 |
+
for name, color in tokens.get('colors', {}).items():
|
| 175 |
+
scss += f"$color-{name}: {color['hex']};\n"
|
| 176 |
+
|
| 177 |
+
scss += "\n// Spacing\n"
|
| 178 |
+
for name, value in tokens.get('spacing', {}).items():
|
| 179 |
+
scss += f"$spacing-{name}: {value};\n"
|
| 180 |
+
|
| 181 |
+
scss += "\n// Typography\n"
|
| 182 |
+
if 'typography' in tokens:
|
| 183 |
+
for name, props in tokens['typography'].items():
|
| 184 |
+
scss += f"$font-{name}: {props.get('family', 'sans-serif')};\n"
|
| 185 |
+
scss += f"$font-size-{name}: {props.get('size', '16px')};\n"
|
| 186 |
+
scss += f"$font-weight-{name}: {props.get('weight', '400')};\n"
|
| 187 |
+
scss += "\n"
|
| 188 |
+
|
| 189 |
+
# Add mixins for common patterns
|
| 190 |
+
scss += "\n// Utility Mixins\n"
|
| 191 |
+
scss += "@mixin text-style($style) {\n"
|
| 192 |
+
scss += " @if $style == 'heading' {\n"
|
| 193 |
+
scss += " font-family: $font-heading;\n"
|
| 194 |
+
scss += " font-size: $font-size-heading;\n"
|
| 195 |
+
scss += " font-weight: $font-weight-heading;\n"
|
| 196 |
+
scss += " } @else if $style == 'body' {\n"
|
| 197 |
+
scss += " font-family: $font-body;\n"
|
| 198 |
+
scss += " font-size: $font-size-body;\n"
|
| 199 |
+
scss += " font-weight: $font-weight-body;\n"
|
| 200 |
+
scss += " }\n"
|
| 201 |
+
scss += "}\n"
|
| 202 |
+
|
| 203 |
+
return scss
|