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:
330
demo/tools/ingest/tailwind.py
Normal file
330
demo/tools/ingest/tailwind.py
Normal 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
|
||||
Reference in New Issue
Block a user