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

330
tools/ingest/tailwind.py Normal file
View File

@@ -0,0 +1,330 @@
"""
Tailwind Token Source
Extracts design tokens from Tailwind CSS configuration files.
Supports tailwind.config.js/ts and CSS-based Tailwind v4 configurations.
"""
import re
import json
from pathlib import Path
from typing import List, Dict, Any, Optional
from .base import DesignToken, TokenCollection, TokenSource, TokenCategory
class TailwindTokenSource(TokenSource):
"""
Extract tokens from Tailwind CSS configuration.
Parses:
- tailwind.config.js/ts (theme and extend sections)
- Tailwind v4 CSS-based configuration
- CSS custom properties from Tailwind output
"""
# Tailwind category mappings
TAILWIND_CATEGORIES = {
'colors': TokenCategory.COLORS,
'backgroundColor': TokenCategory.COLORS,
'textColor': TokenCategory.COLORS,
'borderColor': TokenCategory.COLORS,
'spacing': TokenCategory.SPACING,
'padding': TokenCategory.SPACING,
'margin': TokenCategory.SPACING,
'gap': TokenCategory.SPACING,
'fontSize': TokenCategory.TYPOGRAPHY,
'fontFamily': TokenCategory.TYPOGRAPHY,
'fontWeight': TokenCategory.TYPOGRAPHY,
'lineHeight': TokenCategory.TYPOGRAPHY,
'letterSpacing': TokenCategory.TYPOGRAPHY,
'width': TokenCategory.SIZING,
'height': TokenCategory.SIZING,
'maxWidth': TokenCategory.SIZING,
'maxHeight': TokenCategory.SIZING,
'minWidth': TokenCategory.SIZING,
'minHeight': TokenCategory.SIZING,
'borderRadius': TokenCategory.BORDERS,
'borderWidth': TokenCategory.BORDERS,
'boxShadow': TokenCategory.SHADOWS,
'dropShadow': TokenCategory.SHADOWS,
'opacity': TokenCategory.OPACITY,
'zIndex': TokenCategory.Z_INDEX,
'transitionDuration': TokenCategory.MOTION,
'transitionTimingFunction': TokenCategory.MOTION,
'animation': TokenCategory.MOTION,
'screens': TokenCategory.BREAKPOINTS,
}
@property
def source_type(self) -> str:
return "tailwind"
async def extract(self, source: str) -> TokenCollection:
"""
Extract tokens from Tailwind config.
Args:
source: Path to tailwind.config.js/ts or directory containing it
Returns:
TokenCollection with extracted tokens
"""
config_path = self._find_config(source)
if not config_path:
raise FileNotFoundError(f"Tailwind config not found in: {source}")
content = config_path.read_text(encoding="utf-8")
source_file = str(config_path.absolute())
# Parse based on file type
if config_path.suffix in ('.js', '.cjs', '.mjs', '.ts'):
tokens = self._parse_js_config(content, source_file)
elif config_path.suffix == '.css':
tokens = self._parse_css_config(content, source_file)
else:
tokens = []
return TokenCollection(
tokens=tokens,
name=f"Tailwind Tokens from {config_path.name}",
sources=[self._create_source_id(source_file)],
)
def _find_config(self, source: str) -> Optional[Path]:
"""Find Tailwind config file."""
path = Path(source)
# If it's a file, use it directly
if path.is_file():
return path
# If it's a directory, look for config files
if path.is_dir():
config_names = [
'tailwind.config.js',
'tailwind.config.cjs',
'tailwind.config.mjs',
'tailwind.config.ts',
]
for name in config_names:
config_path = path / name
if config_path.exists():
return config_path
return None
def _parse_js_config(self, content: str, source_file: str) -> List[DesignToken]:
"""Parse JavaScript/TypeScript Tailwind config."""
tokens = []
# Extract theme object using regex (simplified parsing)
# This handles common patterns but may not cover all edge cases
# Look for theme: { ... } or theme.extend: { ... }
theme_match = re.search(
r'theme\s*:\s*\{([\s\S]*?)\n\s*\}(?=\s*[,}])',
content
)
extend_match = re.search(
r'extend\s*:\s*\{([\s\S]*?)\n\s{4}\}',
content
)
if extend_match:
theme_content = extend_match.group(1)
tokens.extend(self._parse_theme_object(theme_content, source_file, "extend"))
if theme_match and not extend_match:
theme_content = theme_match.group(1)
tokens.extend(self._parse_theme_object(theme_content, source_file, "theme"))
return tokens
def _parse_theme_object(self, content: str, source_file: str, prefix: str) -> List[DesignToken]:
"""Parse theme object content."""
tokens = []
# Find property blocks like: colors: { primary: '#3B82F6', ... }
prop_pattern = re.compile(
r"(\w+)\s*:\s*\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}",
re.MULTILINE
)
for match in prop_pattern.finditer(content):
category_name = match.group(1)
category_content = match.group(2)
category = self.TAILWIND_CATEGORIES.get(
category_name, TokenCategory.OTHER
)
# Parse values in this category
tokens.extend(
self._parse_category_values(
category_name,
category_content,
source_file,
category
)
)
return tokens
def _parse_category_values(
self,
category_name: str,
content: str,
source_file: str,
category: TokenCategory
) -> List[DesignToken]:
"""Parse values within a category."""
tokens = []
# Match key: value pairs
# Handles: key: 'value', key: "value", key: value, 'key': value
value_pattern = re.compile(
r"['\"]?(\w[\w-]*)['\"]?\s*:\s*['\"]?([^,'\"}\n]+)['\"]?",
)
for match in value_pattern.finditer(content):
key = match.group(1)
value = match.group(2).strip()
# Skip function calls and complex values for now
if '(' in value or '{' in value:
continue
# Skip references to other values
if value.startswith('colors.') or value.startswith('theme('):
continue
token = DesignToken(
name=f"{category_name}.{key}",
value=value,
source=self._create_source_id(source_file),
source_file=source_file,
original_name=f"{category_name}.{key}",
original_value=value,
category=category,
)
token.tags.append("tailwind")
tokens.append(token)
return tokens
def _parse_css_config(self, content: str, source_file: str) -> List[DesignToken]:
"""Parse Tailwind v4 CSS-based configuration."""
tokens = []
# Tailwind v4 uses @theme directive
theme_match = re.search(
r'@theme\s*\{([\s\S]*?)\}',
content
)
if theme_match:
theme_content = theme_match.group(1)
# Parse CSS custom properties
var_pattern = re.compile(
r'(--[\w-]+)\s*:\s*([^;]+);'
)
for match in var_pattern.finditer(theme_content):
var_name = match.group(1)
var_value = match.group(2).strip()
# Determine category from variable name
category = self._category_from_var_name(var_name)
token = DesignToken(
name=self._normalize_var_name(var_name),
value=var_value,
source=self._create_source_id(source_file),
source_file=source_file,
original_name=var_name,
original_value=var_value,
category=category,
)
token.tags.append("tailwind-v4")
tokens.append(token)
return tokens
def _normalize_var_name(self, var_name: str) -> str:
"""Convert CSS variable name to token name."""
name = var_name.lstrip('-')
name = name.replace('-', '.')
return name.lower()
def _category_from_var_name(self, var_name: str) -> TokenCategory:
"""Determine category from variable name."""
name_lower = var_name.lower()
if 'color' in name_lower or 'bg' in name_lower:
return TokenCategory.COLORS
if 'spacing' in name_lower or 'gap' in name_lower:
return TokenCategory.SPACING
if 'font' in name_lower or 'text' in name_lower:
return TokenCategory.TYPOGRAPHY
if 'radius' in name_lower or 'border' in name_lower:
return TokenCategory.BORDERS
if 'shadow' in name_lower:
return TokenCategory.SHADOWS
return TokenCategory.OTHER
class TailwindClassExtractor:
"""
Extract Tailwind class usage from source files.
Identifies Tailwind utility classes for analysis and migration.
"""
# Common Tailwind class prefixes
TAILWIND_PREFIXES = [
'bg-', 'text-', 'border-', 'ring-',
'p-', 'px-', 'py-', 'pt-', 'pr-', 'pb-', 'pl-',
'm-', 'mx-', 'my-', 'mt-', 'mr-', 'mb-', 'ml-',
'w-', 'h-', 'min-w-', 'min-h-', 'max-w-', 'max-h-',
'flex-', 'grid-', 'gap-',
'font-', 'text-', 'leading-', 'tracking-',
'rounded-', 'shadow-', 'opacity-',
'z-', 'transition-', 'duration-', 'ease-',
]
async def extract_usage(self, source: str) -> Dict[str, List[str]]:
"""
Extract Tailwind class usage from file.
Returns dict mapping class categories to list of used classes.
"""
if Path(source).exists():
content = Path(source).read_text(encoding="utf-8")
else:
content = source
usage: Dict[str, List[str]] = {}
# Find className or class attributes
class_pattern = re.compile(
r'(?:className|class)\s*=\s*["\']([^"\']+)["\']'
)
for match in class_pattern.finditer(content):
classes = match.group(1).split()
for cls in classes:
# Check if it's a Tailwind class
for prefix in self.TAILWIND_PREFIXES:
if cls.startswith(prefix):
category = prefix.rstrip('-')
if category not in usage:
usage[category] = []
if cls not in usage[category]:
usage[category].append(cls)
break
return usage