Initial commit: Clean DSS implementation

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
This commit is contained in:
Digital Production Factory
2025-12-09 18:45:48 -03:00
commit 276ed71f31
884 changed files with 373737 additions and 0 deletions

View File

@@ -0,0 +1,867 @@
"""
Design System Server (DSS) - Figma Tool Suite
Complete MCP tool suite for Figma integration:
1. figma_extract_variables - Extract design tokens/variables
2. figma_extract_components - Extract component definitions
3. figma_extract_styles - Extract text/color/effect styles
4. figma_sync_tokens - Sync tokens to code
5. figma_visual_diff - Compare visual changes
6. figma_validate_components - Validate against schema
7. figma_generate_code - Generate component code
Uses SQLite cache for persistence and config module for token management.
"""
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 API client with SQLite caching and rate limiting."""
def __init__(self, token: Optional[str] = None):
# Use token from config if not provided
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]:
"""Make authenticated request to Figma API with SQLite caching."""
if not self._use_real_api:
# Return mock data for local development
return self._get_mock_data(endpoint)
cache_key = self._cache_key(endpoint)
# Check SQLite cache first
cached = Cache.get(cache_key)
if cached is not None:
return cached
# Make real API request
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 in SQLite cache
Cache.set(cache_key, data, ttl=self.cache_ttl)
# Log activity
ActivityLog.log(
action="figma_api_call",
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:
"""Complete Figma tool suite for design system management."""
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 current mode: 'live' or 'mock'."""
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/variables from Figma file.
Args:
file_key: Figma file key
format: Output format (css, json, scss, js)
Returns:
Extracted tokens in specified 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 component definitions from Figma file.
Args:
file_key: Figma file key
Returns:
Component definitions with properties and variants
"""
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 text, color, and effect styles from Figma file.
Args:
file_key: Figma file key
Returns:
Style definitions organized 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]:
"""
Sync tokens from Figma to target code path.
Args:
file_key: Figma file key
target_path: Target file path for synced tokens
format: Output format
Returns:
Sync result with diff information
"""
# 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 components against design system schema.
Args:
file_key: Figma file key
schema_path: Optional path to validation schema
Returns:
Validation results with issues
"""
components = await self.extract_components(file_key)
issues: List[Dict[str, Any]] = []
# Run validation rules
for comp in components["components"]:
# Rule 1: Component naming convention
if not comp["name"][0].isupper():
issues.append({
"component": comp["name"],
"rule": "naming-convention",
"severity": "warning",
"message": f"Component '{comp['name']}' should start with uppercase"
})
# Rule 2: Description required
if not comp.get("description"):
issues.append({
"component": comp["name"],
"rule": "description-required",
"severity": "info",
"message": f"Component '{comp['name']}' missing 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: Name of component to generate
framework: Target framework (webcomponent, react, vue)
Returns:
Generated 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())