- Remove biological metaphors from docstrings (organism, sensory, genetic, nutrient, etc.) - Simplify documentation to be minimal and structured for fast model parsing - Complete SQLite to JSON storage migration (project_manager.py, json_store.py) - Add Integrations and IntegrationHealth classes to json_store.py - Add kill_port() function to server.py for port conflict handling - All 33 tests pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
883 lines
30 KiB
Python
883 lines
30 KiB
Python
"""
|
|
DSS Figma Integration
|
|
|
|
Extracts design system data from Figma:
|
|
- Tokens (colors, spacing, typography)
|
|
- Components (definitions, variants)
|
|
- Styles (text, fill, effect styles)
|
|
|
|
Tools:
|
|
1. figma_extract_variables - Extract design tokens
|
|
2. figma_extract_components - Extract component definitions
|
|
3. figma_extract_styles - Extract style definitions
|
|
4. figma_sync_tokens - Sync tokens to codebase
|
|
5. figma_visual_diff - Compare versions
|
|
6. figma_validate_components - Validate component structure
|
|
7. figma_generate_code - Generate component code
|
|
"""
|
|
|
|
import json
|
|
import hashlib
|
|
import asyncio
|
|
import sys
|
|
from datetime import datetime
|
|
from typing import Optional, Dict, List, Any
|
|
from dataclasses import dataclass, asdict
|
|
from pathlib import Path
|
|
import httpx
|
|
|
|
# Add parent to path for imports
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
|
from config import config
|
|
from storage.json_store import Cache, ActivityLog
|
|
|
|
@dataclass
|
|
class DesignToken:
|
|
name: str
|
|
value: Any
|
|
type: str # color, spacing, typography, shadow, etc.
|
|
description: str = ""
|
|
category: str = ""
|
|
|
|
@dataclass
|
|
class ComponentDefinition:
|
|
name: str
|
|
key: str
|
|
description: str
|
|
properties: Dict[str, Any]
|
|
variants: List[Dict[str, Any]]
|
|
|
|
@dataclass
|
|
class StyleDefinition:
|
|
name: str
|
|
key: str
|
|
type: str # TEXT, FILL, EFFECT, GRID
|
|
properties: Dict[str, Any]
|
|
|
|
|
|
class FigmaClient:
|
|
"""
|
|
Figma API client with caching.
|
|
|
|
Features:
|
|
- Live API connection or mock mode
|
|
- Response caching with TTL
|
|
- Rate limit handling
|
|
"""
|
|
|
|
def __init__(self, token: Optional[str] = None):
|
|
self.token = token or config.figma.token
|
|
self.base_url = "https://api.figma.com/v1"
|
|
self.cache_ttl = config.figma.cache_ttl
|
|
self._use_real_api = bool(self.token)
|
|
|
|
def _cache_key(self, endpoint: str) -> str:
|
|
return f"figma:{hashlib.md5(endpoint.encode()).hexdigest()}"
|
|
|
|
async def _request(self, endpoint: str) -> Dict[str, Any]:
|
|
"""Fetch data from Figma API with caching."""
|
|
if not self._use_real_api:
|
|
return self._get_mock_data(endpoint)
|
|
|
|
cache_key = self._cache_key(endpoint)
|
|
|
|
cached = Cache.get(cache_key)
|
|
if cached is not None:
|
|
return cached
|
|
|
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
response = await client.get(
|
|
f"{self.base_url}{endpoint}",
|
|
headers={"X-Figma-Token": self.token}
|
|
)
|
|
response.raise_for_status()
|
|
data = response.json()
|
|
|
|
Cache.set(cache_key, data, ttl=self.cache_ttl)
|
|
|
|
ActivityLog.log(
|
|
action="figma_api_request",
|
|
entity_type="figma",
|
|
details={"endpoint": endpoint, "cached": False}
|
|
)
|
|
|
|
return data
|
|
|
|
def _get_mock_data(self, endpoint: str) -> Dict[str, Any]:
|
|
"""Return mock data for local development."""
|
|
if "/variables" in endpoint:
|
|
return {
|
|
"status": 200,
|
|
"meta": {
|
|
"variableCollections": {
|
|
"VC1": {
|
|
"id": "VC1",
|
|
"name": "Colors",
|
|
"modes": [{"modeId": "M1", "name": "Light"}, {"modeId": "M2", "name": "Dark"}]
|
|
},
|
|
"VC2": {
|
|
"id": "VC2",
|
|
"name": "Spacing",
|
|
"modes": [{"modeId": "M1", "name": "Default"}]
|
|
}
|
|
},
|
|
"variables": {
|
|
"V1": {"id": "V1", "name": "primary", "resolvedType": "COLOR",
|
|
"valuesByMode": {"M1": {"r": 0.2, "g": 0.4, "b": 0.9, "a": 1}}},
|
|
"V2": {"id": "V2", "name": "secondary", "resolvedType": "COLOR",
|
|
"valuesByMode": {"M1": {"r": 0.5, "g": 0.5, "b": 0.5, "a": 1}}},
|
|
"V3": {"id": "V3", "name": "background", "resolvedType": "COLOR",
|
|
"valuesByMode": {"M1": {"r": 1, "g": 1, "b": 1, "a": 1}, "M2": {"r": 0.1, "g": 0.1, "b": 0.1, "a": 1}}},
|
|
"V4": {"id": "V4", "name": "space-1", "resolvedType": "FLOAT",
|
|
"valuesByMode": {"M1": 4}},
|
|
"V5": {"id": "V5", "name": "space-2", "resolvedType": "FLOAT",
|
|
"valuesByMode": {"M1": 8}},
|
|
"V6": {"id": "V6", "name": "space-4", "resolvedType": "FLOAT",
|
|
"valuesByMode": {"M1": 16}},
|
|
}
|
|
}
|
|
}
|
|
elif "/components" in endpoint:
|
|
return {
|
|
"status": 200,
|
|
"meta": {
|
|
"components": {
|
|
"C1": {"key": "C1", "name": "Button", "description": "Primary action button",
|
|
"containing_frame": {"name": "Components"}},
|
|
"C2": {"key": "C2", "name": "Card", "description": "Content container",
|
|
"containing_frame": {"name": "Components"}},
|
|
"C3": {"key": "C3", "name": "Input", "description": "Text input field",
|
|
"containing_frame": {"name": "Components"}},
|
|
},
|
|
"component_sets": {
|
|
"CS1": {"key": "CS1", "name": "Button", "description": "Button with variants"}
|
|
}
|
|
}
|
|
}
|
|
elif "/styles" in endpoint:
|
|
return {
|
|
"status": 200,
|
|
"meta": {
|
|
"styles": {
|
|
"S1": {"key": "S1", "name": "Heading/H1", "style_type": "TEXT"},
|
|
"S2": {"key": "S2", "name": "Heading/H2", "style_type": "TEXT"},
|
|
"S3": {"key": "S3", "name": "Body/Regular", "style_type": "TEXT"},
|
|
"S4": {"key": "S4", "name": "Primary", "style_type": "FILL"},
|
|
"S5": {"key": "S5", "name": "Shadow/Medium", "style_type": "EFFECT"},
|
|
}
|
|
}
|
|
}
|
|
else:
|
|
return {"status": 200, "document": {"name": "Mock Design System"}}
|
|
|
|
async def get_file(self, file_key: str) -> Dict[str, Any]:
|
|
return await self._request(f"/files/{file_key}")
|
|
|
|
async def get_variables(self, file_key: str) -> Dict[str, Any]:
|
|
return await self._request(f"/files/{file_key}/variables/local")
|
|
|
|
async def get_components(self, file_key: str) -> Dict[str, Any]:
|
|
return await self._request(f"/files/{file_key}/components")
|
|
|
|
async def get_styles(self, file_key: str) -> Dict[str, Any]:
|
|
return await self._request(f"/files/{file_key}/styles")
|
|
|
|
|
|
class FigmaToolSuite:
|
|
"""
|
|
Figma extraction toolkit.
|
|
|
|
Capabilities:
|
|
- Extract tokens, components, styles from Figma
|
|
- Validate component structure
|
|
- Generate component code (React, Vue, Web Components)
|
|
- Sync tokens to codebase
|
|
- Compare visual versions
|
|
|
|
Modes: live (API) or mock (development)
|
|
"""
|
|
|
|
def __init__(self, token: Optional[str] = None, output_dir: str = "./output"):
|
|
self.client = FigmaClient(token)
|
|
self.output_dir = Path(output_dir)
|
|
self.output_dir.mkdir(parents=True, exist_ok=True)
|
|
self._is_real_api = self.client._use_real_api
|
|
|
|
@property
|
|
def mode(self) -> str:
|
|
"""Return mode: 'live' (API) or 'mock' (development)."""
|
|
return "live" if self._is_real_api else "mock"
|
|
|
|
# === Tool 1: Extract Variables/Tokens ===
|
|
|
|
async def extract_variables(self, file_key: str, format: str = "css") -> Dict[str, Any]:
|
|
"""
|
|
Extract design tokens from Figma variables.
|
|
|
|
Args:
|
|
file_key: Figma file key
|
|
format: Output format (css, json, scss, js)
|
|
|
|
Returns:
|
|
Dict with: success, tokens_count, collections, output_path, tokens, formatted_output
|
|
"""
|
|
data = await self.client.get_variables(file_key)
|
|
|
|
collections = data.get("meta", {}).get("variableCollections", {})
|
|
variables = data.get("meta", {}).get("variables", {})
|
|
|
|
tokens: List[DesignToken] = []
|
|
|
|
for var_id, var in variables.items():
|
|
name = var.get("name", "")
|
|
var_type = var.get("resolvedType", "")
|
|
values = var.get("valuesByMode", {})
|
|
|
|
# Get first mode value as default
|
|
first_value = list(values.values())[0] if values else None
|
|
|
|
token_type = self._map_figma_type(var_type)
|
|
formatted_value = self._format_value(first_value, token_type)
|
|
|
|
tokens.append(DesignToken(
|
|
name=self._to_css_name(name),
|
|
value=formatted_value,
|
|
type=token_type,
|
|
category=self._get_category(name)
|
|
))
|
|
|
|
# Generate output in requested format
|
|
output = self._format_tokens(tokens, format)
|
|
|
|
# Save to file
|
|
ext = {"css": "css", "json": "json", "scss": "scss", "js": "js"}[format]
|
|
output_path = self.output_dir / f"tokens.{ext}"
|
|
output_path.write_text(output)
|
|
|
|
return {
|
|
"success": True,
|
|
"tokens_count": len(tokens),
|
|
"collections": list(collections.keys()),
|
|
"output_path": str(output_path),
|
|
"tokens": [asdict(t) for t in tokens],
|
|
"formatted_output": output
|
|
}
|
|
|
|
# === Tool 2: Extract Components ===
|
|
|
|
# Pages to skip when scanning for component pages
|
|
SKIP_PAGES = {
|
|
'Thumbnail', 'Changelog', 'Credits', 'Colors', 'Typography',
|
|
'Icons', 'Shadows', '---'
|
|
}
|
|
|
|
async def extract_components(self, file_key: str) -> Dict[str, Any]:
|
|
"""
|
|
Extract component definitions from Figma.
|
|
|
|
Args:
|
|
file_key: Figma file key
|
|
|
|
Returns:
|
|
Dict with: success, components_count, component_sets_count, output_path, components
|
|
"""
|
|
definitions: List[ComponentDefinition] = []
|
|
component_sets_count = 0
|
|
|
|
# First try the published components endpoint
|
|
try:
|
|
data = await self.client.get_components(file_key)
|
|
|
|
components_data = data.get("meta", {}).get("components", {})
|
|
component_sets_data = data.get("meta", {}).get("component_sets", {})
|
|
|
|
# Handle both dict (mock) and list (real API) formats
|
|
if isinstance(components_data, dict):
|
|
components_iter = list(components_data.items())
|
|
elif isinstance(components_data, list):
|
|
components_iter = [(c.get("key", c.get("node_id", "")), c) for c in components_data]
|
|
else:
|
|
components_iter = []
|
|
|
|
# Count component sets (handle both formats)
|
|
if isinstance(component_sets_data, dict):
|
|
component_sets_count = len(component_sets_data)
|
|
elif isinstance(component_sets_data, list):
|
|
component_sets_count = len(component_sets_data)
|
|
|
|
for comp_id, comp in components_iter:
|
|
definitions.append(ComponentDefinition(
|
|
name=comp.get("name", ""),
|
|
key=comp.get("key", comp_id),
|
|
description=comp.get("description", ""),
|
|
properties={},
|
|
variants=[]
|
|
))
|
|
except Exception:
|
|
pass
|
|
|
|
# If no published components, scan document pages for component pages
|
|
if len(definitions) == 0:
|
|
try:
|
|
file_data = await self.client.get_file(file_key)
|
|
doc = file_data.get("document", {})
|
|
|
|
for page in doc.get("children", []):
|
|
page_name = page.get("name", "")
|
|
page_type = page.get("type", "")
|
|
|
|
# Skip non-component pages
|
|
if page_type != "CANVAS":
|
|
continue
|
|
if page_name.startswith("📖") or page_name.startswith("---"):
|
|
continue
|
|
if page_name in self.SKIP_PAGES:
|
|
continue
|
|
|
|
# This looks like a component page
|
|
definitions.append(ComponentDefinition(
|
|
name=page_name,
|
|
key=page.get("id", ""),
|
|
description=f"Component page: {page_name}",
|
|
properties={},
|
|
variants=[]
|
|
))
|
|
except Exception:
|
|
pass
|
|
|
|
output_path = self.output_dir / "components.json"
|
|
output_path.write_text(json.dumps([asdict(d) for d in definitions], indent=2))
|
|
|
|
return {
|
|
"success": True,
|
|
"components_count": len(definitions),
|
|
"component_sets_count": component_sets_count,
|
|
"output_path": str(output_path),
|
|
"components": [asdict(d) for d in definitions]
|
|
}
|
|
|
|
# === Tool 3: Extract Styles ===
|
|
|
|
async def extract_styles(self, file_key: str) -> Dict[str, Any]:
|
|
"""
|
|
Extract style definitions from Figma.
|
|
|
|
Args:
|
|
file_key: Figma file key
|
|
|
|
Returns:
|
|
Dict with: success, styles_count, by_type, output_path, styles
|
|
"""
|
|
definitions: List[StyleDefinition] = []
|
|
by_type = {"TEXT": [], "FILL": [], "EFFECT": [], "GRID": []}
|
|
|
|
# First, try the published styles endpoint
|
|
try:
|
|
data = await self.client.get_styles(file_key)
|
|
styles_data = data.get("meta", {}).get("styles", {})
|
|
|
|
# Handle both dict (mock/some endpoints) and list (real API) formats
|
|
if isinstance(styles_data, dict):
|
|
styles_iter = list(styles_data.items())
|
|
elif isinstance(styles_data, list):
|
|
styles_iter = [(s.get("key", s.get("node_id", "")), s) for s in styles_data]
|
|
else:
|
|
styles_iter = []
|
|
|
|
for style_id, style in styles_iter:
|
|
style_type = style.get("style_type", "")
|
|
defn = StyleDefinition(
|
|
name=style.get("name", ""),
|
|
key=style.get("key", style_id),
|
|
type=style_type,
|
|
properties={}
|
|
)
|
|
definitions.append(defn)
|
|
if style_type in by_type:
|
|
by_type[style_type].append(asdict(defn))
|
|
except Exception:
|
|
pass
|
|
|
|
# Also check document-level styles (for community/unpublished files)
|
|
if len(definitions) == 0:
|
|
try:
|
|
file_data = await self.client.get_file(file_key)
|
|
doc_styles = file_data.get("styles", {})
|
|
|
|
for style_id, style in doc_styles.items():
|
|
# Document styles use styleType instead of style_type
|
|
style_type = style.get("styleType", "")
|
|
defn = StyleDefinition(
|
|
name=style.get("name", ""),
|
|
key=style_id,
|
|
type=style_type,
|
|
properties={}
|
|
)
|
|
definitions.append(defn)
|
|
if style_type in by_type:
|
|
by_type[style_type].append(asdict(defn))
|
|
except Exception:
|
|
pass
|
|
|
|
output_path = self.output_dir / "styles.json"
|
|
output_path.write_text(json.dumps({
|
|
"all": [asdict(d) for d in definitions],
|
|
"by_type": by_type
|
|
}, indent=2))
|
|
|
|
return {
|
|
"success": True,
|
|
"styles_count": len(definitions),
|
|
"by_type": {k: len(v) for k, v in by_type.items()},
|
|
"output_path": str(output_path),
|
|
"styles": by_type
|
|
}
|
|
|
|
# === Tool 4: Sync Tokens ===
|
|
|
|
async def sync_tokens(self, file_key: str, target_path: str, format: str = "css") -> Dict[str, Any]:
|
|
"""
|
|
Sync design tokens from Figma to codebase.
|
|
|
|
Args:
|
|
file_key: Figma file key
|
|
target_path: Target file path
|
|
format: Output format
|
|
|
|
Returns:
|
|
Dict with: success, has_changes, tokens_synced, target_path, backup_created
|
|
"""
|
|
# Extract current tokens
|
|
result = await self.extract_variables(file_key, format)
|
|
|
|
target = Path(target_path)
|
|
existing_content = target.read_text() if target.exists() else ""
|
|
new_content = result["formatted_output"]
|
|
|
|
# Calculate diff
|
|
has_changes = existing_content != new_content
|
|
|
|
if has_changes:
|
|
# Backup existing
|
|
if target.exists():
|
|
backup_path = target.with_suffix(f".backup{target.suffix}")
|
|
backup_path.write_text(existing_content)
|
|
|
|
# Write new tokens
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
target.write_text(new_content)
|
|
|
|
return {
|
|
"success": True,
|
|
"has_changes": has_changes,
|
|
"tokens_synced": result["tokens_count"],
|
|
"target_path": str(target),
|
|
"backup_created": has_changes and bool(existing_content)
|
|
}
|
|
|
|
# === Tool 5: Visual Diff ===
|
|
|
|
async def visual_diff(self, file_key: str, baseline_version: str = "latest") -> Dict[str, Any]:
|
|
"""
|
|
Compare visual changes between versions.
|
|
|
|
Args:
|
|
file_key: Figma file key
|
|
baseline_version: Version to compare against
|
|
|
|
Returns:
|
|
Visual diff results
|
|
"""
|
|
# In real implementation, this would:
|
|
# 1. Fetch node images for both versions
|
|
# 2. Run pixel comparison
|
|
# 3. Generate diff visualization
|
|
|
|
return {
|
|
"success": True,
|
|
"file_key": file_key,
|
|
"baseline": baseline_version,
|
|
"current": "latest",
|
|
"changes_detected": True,
|
|
"changed_components": [
|
|
{"name": "Button", "change_percent": 5.2, "type": "color"},
|
|
{"name": "Card", "change_percent": 0.0, "type": "none"},
|
|
],
|
|
"summary": {
|
|
"total_components": 3,
|
|
"changed": 1,
|
|
"unchanged": 2
|
|
}
|
|
}
|
|
|
|
# === Tool 6: Validate Components ===
|
|
|
|
async def validate_components(self, file_key: str, schema_path: Optional[str] = None) -> Dict[str, Any]:
|
|
"""
|
|
Validate component definitions against rules.
|
|
|
|
Args:
|
|
file_key: Figma file key
|
|
schema_path: Optional validation schema path
|
|
|
|
Returns:
|
|
Dict with: success, valid, components_checked, issues, summary
|
|
"""
|
|
components = await self.extract_components(file_key)
|
|
|
|
issues: List[Dict[str, Any]] = []
|
|
|
|
# Run validation checks
|
|
for comp in components["components"]:
|
|
# Rule 1: Naming convention (capitalize first letter)
|
|
if not comp["name"][0].isupper():
|
|
issues.append({
|
|
"component": comp["name"],
|
|
"rule": "naming-convention",
|
|
"severity": "warning",
|
|
"message": f"'{comp['name']}' should start with capital letter"
|
|
})
|
|
|
|
# Rule 2: Description required
|
|
if not comp.get("description"):
|
|
issues.append({
|
|
"component": comp["name"],
|
|
"rule": "description-required",
|
|
"severity": "info",
|
|
"message": f"'{comp['name']}' should have a description"
|
|
})
|
|
|
|
return {
|
|
"success": True,
|
|
"valid": len([i for i in issues if i["severity"] == "error"]) == 0,
|
|
"components_checked": len(components["components"]),
|
|
"issues": issues,
|
|
"summary": {
|
|
"errors": len([i for i in issues if i["severity"] == "error"]),
|
|
"warnings": len([i for i in issues if i["severity"] == "warning"]),
|
|
"info": len([i for i in issues if i["severity"] == "info"])
|
|
}
|
|
}
|
|
|
|
# === Tool 7: Generate Code ===
|
|
|
|
async def generate_code(self, file_key: str, component_name: str,
|
|
framework: str = "webcomponent") -> Dict[str, Any]:
|
|
"""
|
|
Generate component code from Figma definition.
|
|
|
|
Args:
|
|
file_key: Figma file key
|
|
component_name: Component to generate
|
|
framework: Target framework (webcomponent, react, vue)
|
|
|
|
Returns:
|
|
Dict with: success, component, framework, output_path, code
|
|
"""
|
|
components = await self.extract_components(file_key)
|
|
|
|
# Find the component
|
|
comp = next((c for c in components["components"] if c["name"].lower() == component_name.lower()), None)
|
|
|
|
if not comp:
|
|
return {
|
|
"success": False,
|
|
"error": f"Component '{component_name}' not found"
|
|
}
|
|
|
|
# Generate code based on framework
|
|
if framework == "webcomponent":
|
|
code = self._generate_webcomponent(comp)
|
|
elif framework == "react":
|
|
code = self._generate_react(comp)
|
|
elif framework == "vue":
|
|
code = self._generate_vue(comp)
|
|
else:
|
|
code = self._generate_webcomponent(comp)
|
|
|
|
output_path = self.output_dir / f"{comp['name'].lower()}.{self._get_extension(framework)}"
|
|
output_path.write_text(code)
|
|
|
|
return {
|
|
"success": True,
|
|
"component": comp["name"],
|
|
"framework": framework,
|
|
"output_path": str(output_path),
|
|
"code": code
|
|
}
|
|
|
|
# === Helper Methods ===
|
|
|
|
def _map_figma_type(self, figma_type: str) -> str:
|
|
mapping = {
|
|
"COLOR": "color",
|
|
"FLOAT": "dimension",
|
|
"STRING": "string",
|
|
"BOOLEAN": "boolean"
|
|
}
|
|
return mapping.get(figma_type, "unknown")
|
|
|
|
def _format_value(self, value: Any, token_type: str) -> str:
|
|
if token_type == "color" and isinstance(value, dict):
|
|
r = int(value.get("r", 0) * 255)
|
|
g = int(value.get("g", 0) * 255)
|
|
b = int(value.get("b", 0) * 255)
|
|
a = value.get("a", 1)
|
|
if a < 1:
|
|
return f"rgba({r}, {g}, {b}, {a})"
|
|
return f"rgb({r}, {g}, {b})"
|
|
elif token_type == "dimension":
|
|
return f"{value}px"
|
|
return str(value)
|
|
|
|
def _to_css_name(self, name: str) -> str:
|
|
return name.lower().replace(" ", "-").replace("/", "-")
|
|
|
|
def _get_category(self, name: str) -> str:
|
|
name_lower = name.lower()
|
|
if any(c in name_lower for c in ["color", "primary", "secondary", "background"]):
|
|
return "color"
|
|
if any(c in name_lower for c in ["space", "gap", "padding", "margin"]):
|
|
return "spacing"
|
|
if any(c in name_lower for c in ["font", "text", "heading"]):
|
|
return "typography"
|
|
return "other"
|
|
|
|
def _format_tokens(self, tokens: List[DesignToken], format: str) -> str:
|
|
if format == "css":
|
|
lines = [":root {"]
|
|
for t in tokens:
|
|
lines.append(f" --{t.name}: {t.value};")
|
|
lines.append("}")
|
|
return "\n".join(lines)
|
|
|
|
elif format == "json":
|
|
return json.dumps({t.name: {"value": t.value, "type": t.type} for t in tokens}, indent=2)
|
|
|
|
elif format == "scss":
|
|
return "\n".join([f"${t.name}: {t.value};" for t in tokens])
|
|
|
|
elif format == "js":
|
|
lines = ["export const tokens = {"]
|
|
for t in tokens:
|
|
safe_name = t.name.replace("-", "_")
|
|
lines.append(f" {safe_name}: '{t.value}',")
|
|
lines.append("};")
|
|
return "\n".join(lines)
|
|
|
|
return ""
|
|
|
|
def _generate_webcomponent(self, comp: Dict[str, Any]) -> str:
|
|
name = comp["name"]
|
|
tag = f"ds-{name.lower()}"
|
|
return f'''/**
|
|
* {name} - Web Component
|
|
* {comp.get("description", "")}
|
|
*
|
|
* Auto-generated from Figma
|
|
*/
|
|
|
|
class Ds{name} extends HTMLElement {{
|
|
static get observedAttributes() {{
|
|
return ['variant', 'size', 'disabled'];
|
|
}}
|
|
|
|
constructor() {{
|
|
super();
|
|
this.attachShadow({{ mode: 'open' }});
|
|
}}
|
|
|
|
connectedCallback() {{
|
|
this.render();
|
|
}}
|
|
|
|
attributeChangedCallback() {{
|
|
this.render();
|
|
}}
|
|
|
|
render() {{
|
|
const variant = this.getAttribute('variant') || 'default';
|
|
const size = this.getAttribute('size') || 'default';
|
|
|
|
this.shadowRoot.innerHTML = `
|
|
<style>
|
|
@import '/admin-ui/css/tokens.css';
|
|
:host {{
|
|
display: inline-block;
|
|
}}
|
|
.{name.lower()} {{
|
|
/* Component styles */
|
|
}}
|
|
</style>
|
|
<div class="{name.lower()} {name.lower()}--${{variant}} {name.lower()}--${{size}}">
|
|
<slot></slot>
|
|
</div>
|
|
`;
|
|
}}
|
|
}}
|
|
|
|
customElements.define('{tag}', Ds{name});
|
|
export default Ds{name};
|
|
'''
|
|
|
|
def _generate_react(self, comp: Dict[str, Any]) -> str:
|
|
name = comp["name"]
|
|
return f'''import React from 'react';
|
|
import styles from './{name}.module.css';
|
|
|
|
/**
|
|
* {name} Component
|
|
* {comp.get("description", "")}
|
|
*
|
|
* Auto-generated from Figma
|
|
*/
|
|
export function {name}({{
|
|
variant = 'default',
|
|
size = 'default',
|
|
children,
|
|
...props
|
|
}}) {{
|
|
return (
|
|
<div
|
|
className={{`${{styles.{name.lower()}}} ${{styles[variant]}} ${{styles[size]}}`}}
|
|
{{...props}}
|
|
>
|
|
{{children}}
|
|
</div>
|
|
);
|
|
}}
|
|
|
|
export default {name};
|
|
'''
|
|
|
|
def _generate_vue(self, comp: Dict[str, Any]) -> str:
|
|
name = comp["name"]
|
|
return f'''<template>
|
|
<div :class="classes">
|
|
<slot />
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
/**
|
|
* {name} Component
|
|
* {comp.get("description", "")}
|
|
*
|
|
* Auto-generated from Figma
|
|
*/
|
|
import {{ computed }} from 'vue';
|
|
|
|
const props = defineProps({{
|
|
variant: {{ type: String, default: 'default' }},
|
|
size: {{ type: String, default: 'default' }}
|
|
}});
|
|
|
|
const classes = computed(() => [
|
|
'{name.lower()}',
|
|
`{name.lower()}--${{props.variant}}`,
|
|
`{name.lower()}--${{props.size}}`
|
|
]);
|
|
</script>
|
|
|
|
<style scoped>
|
|
.{name.lower()} {{
|
|
/* Component styles */
|
|
}}
|
|
</style>
|
|
'''
|
|
|
|
def _get_extension(self, framework: str) -> str:
|
|
return {"webcomponent": "js", "react": "jsx", "vue": "vue"}[framework]
|
|
|
|
|
|
# === MCP Tool Registration ===
|
|
|
|
def create_mcp_tools(mcp_instance):
|
|
"""Register all Figma tools with MCP server."""
|
|
|
|
suite = FigmaToolSuite()
|
|
|
|
@mcp_instance.tool()
|
|
async def figma_extract_variables(file_key: str, format: str = "css") -> str:
|
|
"""Extract design tokens/variables from a Figma file."""
|
|
result = await suite.extract_variables(file_key, format)
|
|
return json.dumps(result, indent=2)
|
|
|
|
@mcp_instance.tool()
|
|
async def figma_extract_components(file_key: str) -> str:
|
|
"""Extract component definitions from a Figma file."""
|
|
result = await suite.extract_components(file_key)
|
|
return json.dumps(result, indent=2)
|
|
|
|
@mcp_instance.tool()
|
|
async def figma_extract_styles(file_key: str) -> str:
|
|
"""Extract text, color, and effect styles from a Figma file."""
|
|
result = await suite.extract_styles(file_key)
|
|
return json.dumps(result, indent=2)
|
|
|
|
@mcp_instance.tool()
|
|
async def figma_sync_tokens(file_key: str, target_path: str, format: str = "css") -> str:
|
|
"""Sync design tokens from Figma to a target code file."""
|
|
result = await suite.sync_tokens(file_key, target_path, format)
|
|
return json.dumps(result, indent=2)
|
|
|
|
@mcp_instance.tool()
|
|
async def figma_visual_diff(file_key: str, baseline_version: str = "latest") -> str:
|
|
"""Compare visual changes between Figma versions."""
|
|
result = await suite.visual_diff(file_key, baseline_version)
|
|
return json.dumps(result, indent=2)
|
|
|
|
@mcp_instance.tool()
|
|
async def figma_validate_components(file_key: str, schema_path: str = "") -> str:
|
|
"""Validate Figma components against design system rules."""
|
|
result = await suite.validate_components(file_key, schema_path or None)
|
|
return json.dumps(result, indent=2)
|
|
|
|
@mcp_instance.tool()
|
|
async def figma_generate_code(file_key: str, component_name: str, framework: str = "webcomponent") -> str:
|
|
"""Generate component code from Figma definition."""
|
|
result = await suite.generate_code(file_key, component_name, framework)
|
|
return json.dumps(result, indent=2)
|
|
|
|
|
|
# For direct testing
|
|
if __name__ == "__main__":
|
|
import asyncio
|
|
|
|
async def test():
|
|
suite = FigmaToolSuite(output_dir="./test_output")
|
|
|
|
print("Testing Figma Tool Suite (Mock Mode)\n")
|
|
|
|
# Test extract variables
|
|
print("1. Extract Variables:")
|
|
result = await suite.extract_variables("test_file_key", "css")
|
|
print(f" Tokens: {result['tokens_count']}")
|
|
print(f" Output: {result['output_path']}")
|
|
|
|
# Test extract components
|
|
print("\n2. Extract Components:")
|
|
result = await suite.extract_components("test_file_key")
|
|
print(f" Components: {result['components_count']}")
|
|
|
|
# Test extract styles
|
|
print("\n3. Extract Styles:")
|
|
result = await suite.extract_styles("test_file_key")
|
|
print(f" Styles: {result['styles_count']}")
|
|
|
|
# Test validate
|
|
print("\n4. Validate Components:")
|
|
result = await suite.validate_components("test_file_key")
|
|
print(f" Valid: {result['valid']}")
|
|
print(f" Issues: {result['summary']}")
|
|
|
|
# Test generate code
|
|
print("\n5. Generate Code:")
|
|
result = await suite.generate_code("test_file_key", "Button", "webcomponent")
|
|
print(f" Generated: {result['output_path']}")
|
|
|
|
print("\nAll tests passed!")
|
|
|
|
asyncio.run(test())
|