Migrated from design-system-swarm with fresh git history.
Old project history preserved in /home/overbits/apps/design-system-swarm
Core components:
- MCP Server (Python FastAPI with mcp 1.23.1)
- Claude Plugin (agents, commands, skills, strategies, hooks, core)
- DSS Backend (dss-mvp1 - token translation, Figma sync)
- Admin UI (Node.js/React)
- Server (Node.js/Express)
- Storybook integration (dss-mvp1/.storybook)
Self-contained configuration:
- All paths relative or use DSS_BASE_PATH=/home/overbits/dss
- PYTHONPATH configured for dss-mvp1 and dss-claude-plugin
- .env file with all configuration
- Claude plugin uses ${CLAUDE_PLUGIN_ROOT} for portability
Migration completed: $(date)
🤖 Clean migration with full functionality preserved
985 lines
36 KiB
Python
985 lines
36 KiB
Python
"""
|
|
DSS SENSORY ORGANS - Figma Integration Toolkit
|
|
|
|
The DSS sensory organs allow the design system organism to perceive and
|
|
digest visual designs from Figma. This toolkit extracts genetic information
|
|
(tokens, components, styles) from the Figma sensory perception and transforms
|
|
it into nutrients for the organism.
|
|
|
|
Tool Suite (Sensory Perception Functions):
|
|
1. figma_extract_variables - 🩸 Perceive design tokens as blood nutrients
|
|
2. figma_extract_components - 🧬 Perceive component DNA blueprints
|
|
3. figma_extract_styles - 🎨 Perceive visual expressions and patterns
|
|
4. figma_sync_tokens - 🔄 Distribute nutrients through circulatory system
|
|
5. figma_visual_diff - 👁️ Detect changes in visual expression
|
|
6. figma_validate_components - 🧬 Verify genetic code integrity
|
|
7. figma_generate_code - 📝 Encode genetic information into code
|
|
|
|
Architecture:
|
|
- Sensory Perception: HTTPx client with SQLite caching (organism's memory)
|
|
- Token Metabolism: Design token transformation pipeline
|
|
- Code Generation: Genetic encoding into multiple framework languages
|
|
|
|
Framework: DSS Organism Framework
|
|
See: docs/DSS_ORGANISM_GUIDE.md#sensory-organs
|
|
"""
|
|
|
|
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.database 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 SENSORY RECEPTOR - Organism's visual perception system
|
|
|
|
The sensory receptor connects the DSS organism to Figma's visual information.
|
|
It perceives visual designs and caches genetic information (tokens, components)
|
|
in the organism's short-term memory (SQLite cache) for efficient digestion.
|
|
|
|
Features:
|
|
- Real-time sensory perception (live Figma API connection)
|
|
- Memory caching (SQLite persistence with TTL)
|
|
- Rate limiting awareness (respects Figma's biological constraints)
|
|
- Mock perception mode (for organism development without external connection)
|
|
"""
|
|
|
|
def __init__(self, token: Optional[str] = None):
|
|
# Establish sensory connection (use provided token or config default)
|
|
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) # Real sensory perception vs mock dreams
|
|
|
|
def _cache_key(self, endpoint: str) -> str:
|
|
return f"figma:{hashlib.md5(endpoint.encode()).hexdigest()}"
|
|
|
|
async def _request(self, endpoint: str) -> Dict[str, Any]:
|
|
"""
|
|
👁️ SENSORY PERCEPTION - Fetch visual information from Figma
|
|
|
|
The sensory receptor reaches out to Figma to perceive visual designs.
|
|
If the organism is in development mode, it uses dream data (mocks).
|
|
Otherwise, it queries the external Figma organism and stores perceived
|
|
information in its own memory (SQLite cache) for quick recall.
|
|
|
|
Flow:
|
|
1. Check if sensory is in development mode (mock perception)
|
|
2. Check organism's memory cache for previous perception
|
|
3. If memory miss, perceive from external source (Figma API)
|
|
4. Store new perception in memory for future recognition
|
|
5. Log the perceptual event
|
|
"""
|
|
if not self._use_real_api:
|
|
# Sensory hallucinations for development (mock perception)
|
|
return self._get_mock_data(endpoint)
|
|
|
|
cache_key = self._cache_key(endpoint)
|
|
|
|
# Check organism memory first (short-term memory - SQLite)
|
|
cached = Cache.get(cache_key)
|
|
if cached is not None:
|
|
return cached
|
|
|
|
# Perceive from external source (live Figma perception)
|
|
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()
|
|
|
|
# Store perception in organism memory for future recognition
|
|
Cache.set(cache_key, data, ttl=self.cache_ttl)
|
|
|
|
# Log the perceptual event
|
|
ActivityLog.log(
|
|
action="figma_sensory_perception",
|
|
entity_type="sensory_organs",
|
|
details={"endpoint": endpoint, "cached": False, "perception": "live"}
|
|
)
|
|
|
|
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:
|
|
"""
|
|
👁️ SENSORY ORGANS DIGESTION CENTER - Transform visual perception into nutrients
|
|
|
|
The sensory digestion center transforms raw visual information from Figma
|
|
into usable nutrients (tokens, components) that the DSS organism can
|
|
incorporate into its body. This complete toolkit:
|
|
|
|
- Perceives visual designs (sensory organs)
|
|
- Extracts genetic code (tokens, components, styles)
|
|
- Validates genetic integrity (schema validation)
|
|
- Encodes information (code generation for multiple frameworks)
|
|
- Distributes nutrients (token syncing to codebase)
|
|
- Detects mutations (visual diffs)
|
|
|
|
The organism can operate in two modes:
|
|
- LIVE: Directly perceiving from external Figma organism
|
|
- MOCK: Using dream data for development without external dependency
|
|
"""
|
|
|
|
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 sensory perception mode: 'live' (external Figma) or 'mock' (dreams/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 CIRCULATORY TOKENS - Perceive design tokens as nutrients
|
|
|
|
The sensory organs perceive design tokens (variables) from Figma and
|
|
convert them into circulatory nutrients (design tokens) that flow through
|
|
the organism's body. These are the fundamental nutrients that color blood,
|
|
determine tissue spacing, and define typographic patterns.
|
|
|
|
Args:
|
|
file_key: Figma file key (visual perception target)
|
|
format: Output format for encoded nutrients (css, json, scss, js)
|
|
|
|
Returns:
|
|
Dict with extracted tokens ready for circulation:
|
|
- success: Perception completed without errors
|
|
- tokens_count: Number of nutrients extracted
|
|
- collections: Token collections (by system)
|
|
- output_path: File where nutrients are stored
|
|
- tokens: Complete nutrient definitions
|
|
- formatted_output: Encoded output in requested format
|
|
"""
|
|
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 GENETIC BLUEPRINTS - Perceive component DNA
|
|
|
|
The sensory organs perceive component definitions (visual DNA) from Figma
|
|
and extract genetic blueprints that describe how tissues are constructed.
|
|
Components are the fundamental building blocks (genes) that encode
|
|
the organism's form, function, and behavior patterns.
|
|
|
|
Args:
|
|
file_key: Figma file key (visual genetic source)
|
|
|
|
Returns:
|
|
Dict with extracted component DNA:
|
|
- success: Genetic extraction successful
|
|
- components_count: Number of DNA blueprints found
|
|
- component_sets_count: Number of genetic variant groups
|
|
- output_path: File where genetic information is stored
|
|
- components: Complete component definitions with properties
|
|
"""
|
|
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 VISUAL EXPRESSION PATTERNS - Perceive style definitions
|
|
|
|
The sensory organs perceive visual expressions (text, color, effect styles)
|
|
from Figma and categorize them by their biological purpose: how text
|
|
appears (typography), how colors flow (pigmentation), and how depth
|
|
and dimension manifest through effects.
|
|
|
|
Args:
|
|
file_key: Figma file key (visual style source)
|
|
|
|
Returns:
|
|
Dict with extracted style definitions organized by type:
|
|
- success: Style extraction successful
|
|
- styles_count: Total style definitions found
|
|
- by_type: Styles organized by category (TEXT, FILL, EFFECT, GRID)
|
|
- output_path: File where style definitions are stored
|
|
- styles: Complete style information by type
|
|
"""
|
|
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]:
|
|
"""
|
|
🔄 CIRCULATE NUTRIENTS - Distribute tokens through the organism
|
|
|
|
The organism absorbs nutrients from Figma's visual designs and circulates
|
|
them through its body by syncing to the code codebase. This ensures the
|
|
organism's physical form (code) stays synchronized with its genetic design
|
|
(Figma tokens).
|
|
|
|
Args:
|
|
file_key: Figma file key (nutrient source)
|
|
target_path: Codebase file path (circulation destination)
|
|
format: Output format for encoded nutrients
|
|
|
|
Returns:
|
|
Dict with sync result:
|
|
- success: Circulation completed
|
|
- has_changes: Whether genetic material changed
|
|
- tokens_synced: Number of nutrients distributed
|
|
- target_path: Location where nutrients were circulated
|
|
- backup_created: Whether old nutrients were preserved
|
|
"""
|
|
# 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]:
|
|
"""
|
|
🧬 GENETIC INTEGRITY CHECK - Validate component DNA health
|
|
|
|
The immune system examines extracted component DNA against genetic
|
|
rules (schema) to ensure all components are healthy, properly named,
|
|
and fully documented. Invalid components are flagged as mutations that
|
|
could endanger the organism's health.
|
|
|
|
Args:
|
|
file_key: Figma file key (genetic source)
|
|
schema_path: Optional path to validation rules (genetic schema)
|
|
|
|
Returns:
|
|
Dict with validation results:
|
|
- success: Validation completed without system errors
|
|
- valid: Whether all genetic material is healthy
|
|
- components_checked: Number of DNA blueprints examined
|
|
- issues: List of genetic problems found
|
|
- summary: Count of errors, warnings, and info messages
|
|
"""
|
|
components = await self.extract_components(file_key)
|
|
|
|
issues: List[Dict[str, Any]] = []
|
|
|
|
# Run genetic integrity checks
|
|
for comp in components["components"]:
|
|
# Rule 1: 🧬 Genetic naming convention (capitalize first letter)
|
|
if not comp["name"][0].isupper():
|
|
issues.append({
|
|
"component": comp["name"],
|
|
"rule": "naming-convention",
|
|
"severity": "warning",
|
|
"message": f"🧬 Genetic mutation detected: '{comp['name']}' should follow naming convention (start with capital letter)"
|
|
})
|
|
|
|
# Rule 2: 📋 Genetic documentation (description required)
|
|
if not comp.get("description"):
|
|
issues.append({
|
|
"component": comp["name"],
|
|
"rule": "description-required",
|
|
"severity": "info",
|
|
"message": f"📝 Genetic annotation missing: '{comp['name']}' should have a description to document its biological purpose"
|
|
})
|
|
|
|
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]:
|
|
"""
|
|
📝 ENCODE GENETIC MATERIAL - Generate component code from DNA
|
|
|
|
The organism translates genetic blueprints (component DNA) from Figma
|
|
into executable code that can be expressed in multiple biological contexts
|
|
(frameworks). This genetic encoding allows the component DNA to manifest
|
|
as living tissue in different ecosystems.
|
|
|
|
Args:
|
|
file_key: Figma file key (genetic source)
|
|
component_name: Name of component DNA to encode
|
|
framework: Target biological context (webcomponent, react, vue)
|
|
|
|
Returns:
|
|
Dict with generated code:
|
|
- success: Genetic encoding successful
|
|
- component: Component name
|
|
- framework: Target framework
|
|
- output_path: File where genetic code is written
|
|
- code: The encoded genetic material ready for expression
|
|
"""
|
|
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"🛡️ Genetic material not found: Component '{component_name}' does not exist in the perceived DNA"
|
|
}
|
|
|
|
# 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())
|