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
375 lines
11 KiB
Python
375 lines
11 KiB
Python
"""
|
|
Storybook Theme Generator
|
|
|
|
Generates Storybook theme configurations from design tokens.
|
|
"""
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from typing import List, Dict, Any, Optional
|
|
from dataclasses import dataclass, field
|
|
|
|
|
|
@dataclass
|
|
class StorybookTheme:
|
|
"""Storybook theme configuration."""
|
|
name: str = "dss-theme"
|
|
base: str = "light" # 'light' or 'dark'
|
|
|
|
# Brand
|
|
brand_title: str = "Design System"
|
|
brand_url: str = ""
|
|
brand_image: str = ""
|
|
brand_target: str = "_self"
|
|
|
|
# Colors
|
|
color_primary: str = "#3B82F6"
|
|
color_secondary: str = "#10B981"
|
|
|
|
# UI Colors
|
|
app_bg: str = "#FFFFFF"
|
|
app_content_bg: str = "#FFFFFF"
|
|
app_border_color: str = "#E5E7EB"
|
|
|
|
# Text colors
|
|
text_color: str = "#1F2937"
|
|
text_inverse_color: str = "#FFFFFF"
|
|
text_muted_color: str = "#6B7280"
|
|
|
|
# Toolbar
|
|
bar_text_color: str = "#6B7280"
|
|
bar_selected_color: str = "#3B82F6"
|
|
bar_bg: str = "#FFFFFF"
|
|
|
|
# Form colors
|
|
input_bg: str = "#FFFFFF"
|
|
input_border: str = "#D1D5DB"
|
|
input_text_color: str = "#1F2937"
|
|
input_border_radius: int = 4
|
|
|
|
# Typography
|
|
font_base: str = '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
|
|
font_code: str = '"Fira Code", "Monaco", monospace'
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
return {
|
|
"base": self.base,
|
|
"brandTitle": self.brand_title,
|
|
"brandUrl": self.brand_url,
|
|
"brandImage": self.brand_image,
|
|
"brandTarget": self.brand_target,
|
|
"colorPrimary": self.color_primary,
|
|
"colorSecondary": self.color_secondary,
|
|
"appBg": self.app_bg,
|
|
"appContentBg": self.app_content_bg,
|
|
"appBorderColor": self.app_border_color,
|
|
"textColor": self.text_color,
|
|
"textInverseColor": self.text_inverse_color,
|
|
"textMutedColor": self.text_muted_color,
|
|
"barTextColor": self.bar_text_color,
|
|
"barSelectedColor": self.bar_selected_color,
|
|
"barBg": self.bar_bg,
|
|
"inputBg": self.input_bg,
|
|
"inputBorder": self.input_border,
|
|
"inputTextColor": self.input_text_color,
|
|
"inputBorderRadius": self.input_border_radius,
|
|
"fontBase": self.font_base,
|
|
"fontCode": self.font_code,
|
|
}
|
|
|
|
|
|
class ThemeGenerator:
|
|
"""
|
|
Generates Storybook theme configurations from design tokens.
|
|
"""
|
|
|
|
# Token name mappings to Storybook theme properties
|
|
TOKEN_MAPPINGS = {
|
|
# Primary/Secondary
|
|
"color.primary.500": "color_primary",
|
|
"color.primary.600": "color_primary",
|
|
"color.secondary.500": "color_secondary",
|
|
"color.accent.500": "color_secondary",
|
|
|
|
# Backgrounds
|
|
"color.neutral.50": "app_bg",
|
|
"color.background": "app_bg",
|
|
"color.surface": "app_content_bg",
|
|
|
|
# Borders
|
|
"color.neutral.200": "app_border_color",
|
|
"color.border": "app_border_color",
|
|
|
|
# Text
|
|
"color.neutral.900": "text_color",
|
|
"color.neutral.800": "text_color",
|
|
"color.foreground": "text_color",
|
|
"color.neutral.500": "text_muted_color",
|
|
"color.muted": "text_muted_color",
|
|
|
|
# Input
|
|
"color.neutral.300": "input_border",
|
|
"radius.md": "input_border_radius",
|
|
}
|
|
|
|
def __init__(self):
|
|
pass
|
|
|
|
def generate_from_tokens(
|
|
self,
|
|
tokens: List[Dict[str, Any]],
|
|
brand_title: str = "Design System",
|
|
base: str = "light",
|
|
) -> StorybookTheme:
|
|
"""
|
|
Generate Storybook theme from design tokens.
|
|
|
|
Args:
|
|
tokens: List of token dicts with 'name' and 'value'
|
|
brand_title: Brand title for Storybook
|
|
base: Base theme ('light' or 'dark')
|
|
|
|
Returns:
|
|
StorybookTheme configured from tokens
|
|
"""
|
|
theme = StorybookTheme(
|
|
name="dss-theme",
|
|
base=base,
|
|
brand_title=brand_title,
|
|
)
|
|
|
|
# Map tokens to theme properties
|
|
for token in tokens:
|
|
name = token.get("name", "")
|
|
value = token.get("value", "")
|
|
|
|
# Check direct mappings
|
|
if name in self.TOKEN_MAPPINGS:
|
|
prop = self.TOKEN_MAPPINGS[name]
|
|
setattr(theme, prop, value)
|
|
continue
|
|
|
|
# Check partial matches
|
|
name_lower = name.lower()
|
|
|
|
if "primary" in name_lower and "500" in name_lower:
|
|
theme.color_primary = value
|
|
elif "secondary" in name_lower and "500" in name_lower:
|
|
theme.color_secondary = value
|
|
elif "background" in name_lower and self._is_light_color(value):
|
|
theme.app_bg = value
|
|
elif "foreground" in name_lower or ("text" in name_lower and "color" in name_lower):
|
|
theme.text_color = value
|
|
|
|
# Adjust for dark mode
|
|
if base == "dark":
|
|
theme = self._adjust_for_dark_mode(theme)
|
|
|
|
return theme
|
|
|
|
def _is_light_color(self, value: str) -> bool:
|
|
"""Check if a color value is light (for background suitability)."""
|
|
if not value.startswith("#"):
|
|
return True # Assume light if not hex
|
|
|
|
# Parse hex color
|
|
hex_color = value.lstrip("#")
|
|
if len(hex_color) == 3:
|
|
hex_color = "".join(c * 2 for c in hex_color)
|
|
|
|
try:
|
|
r = int(hex_color[0:2], 16)
|
|
g = int(hex_color[2:4], 16)
|
|
b = int(hex_color[4:6], 16)
|
|
# Calculate luminance
|
|
luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
|
|
return luminance > 0.5
|
|
except (ValueError, IndexError):
|
|
return True
|
|
|
|
def _adjust_for_dark_mode(self, theme: StorybookTheme) -> StorybookTheme:
|
|
"""Adjust theme for dark mode if colors aren't already dark."""
|
|
# Swap light/dark if needed
|
|
if self._is_light_color(theme.app_bg):
|
|
theme.app_bg = "#1F2937"
|
|
theme.app_content_bg = "#111827"
|
|
theme.app_border_color = "#374151"
|
|
theme.text_color = "#F9FAFB"
|
|
theme.text_muted_color = "#9CA3AF"
|
|
theme.bar_bg = "#1F2937"
|
|
theme.bar_text_color = "#9CA3AF"
|
|
theme.input_bg = "#374151"
|
|
theme.input_border = "#4B5563"
|
|
theme.input_text_color = "#F9FAFB"
|
|
|
|
return theme
|
|
|
|
def generate_theme_file(
|
|
self,
|
|
theme: StorybookTheme,
|
|
format: str = "ts",
|
|
) -> str:
|
|
"""
|
|
Generate Storybook theme file content.
|
|
|
|
Args:
|
|
theme: StorybookTheme to export
|
|
format: Output format ('ts', 'js', 'json')
|
|
|
|
Returns:
|
|
Theme file content as string
|
|
"""
|
|
if format == "json":
|
|
return json.dumps(theme.to_dict(), indent=2)
|
|
|
|
theme_dict = theme.to_dict()
|
|
|
|
if format == "ts":
|
|
lines = [
|
|
"import { create } from '@storybook/theming/create';",
|
|
"",
|
|
"export const dssTheme = create({",
|
|
]
|
|
else: # js
|
|
lines = [
|
|
"const { create } = require('@storybook/theming/create');",
|
|
"",
|
|
"module.exports = create({",
|
|
]
|
|
|
|
for key, value in theme_dict.items():
|
|
if isinstance(value, str):
|
|
lines.append(f" {key}: '{value}',")
|
|
else:
|
|
lines.append(f" {key}: {value},")
|
|
|
|
lines.extend([
|
|
"});",
|
|
"",
|
|
])
|
|
|
|
return "\n".join(lines)
|
|
|
|
def generate_manager_file(self, theme_import: str = "./dss-theme") -> str:
|
|
"""
|
|
Generate Storybook manager.ts file.
|
|
|
|
Args:
|
|
theme_import: Import path for theme
|
|
|
|
Returns:
|
|
Manager file content
|
|
"""
|
|
return f"""import {{ addons }} from '@storybook/manager-api';
|
|
import {{ dssTheme }} from '{theme_import}';
|
|
|
|
addons.setConfig({{
|
|
theme: dssTheme,
|
|
}});
|
|
"""
|
|
|
|
def generate_preview_file(
|
|
self,
|
|
tokens: List[Dict[str, Any]],
|
|
include_css_vars: bool = True,
|
|
) -> str:
|
|
"""
|
|
Generate Storybook preview.ts file with token CSS variables.
|
|
|
|
Args:
|
|
tokens: List of token dicts
|
|
include_css_vars: Include CSS variable injection
|
|
|
|
Returns:
|
|
Preview file content
|
|
"""
|
|
lines = [
|
|
"import type { Preview } from '@storybook/react';",
|
|
"",
|
|
]
|
|
|
|
if include_css_vars:
|
|
# Generate CSS variables from tokens
|
|
css_vars = []
|
|
for token in tokens:
|
|
name = token.get("name", "").replace(".", "-")
|
|
value = token.get("value", "")
|
|
css_vars.append(f" --{name}: {value};")
|
|
|
|
lines.extend([
|
|
"// Inject design tokens as CSS variables",
|
|
"const tokenStyles = `",
|
|
":root {",
|
|
])
|
|
lines.extend(css_vars)
|
|
lines.extend([
|
|
"}",
|
|
"`;",
|
|
"",
|
|
"// Add styles to document",
|
|
"const styleSheet = document.createElement('style');",
|
|
"styleSheet.textContent = tokenStyles;",
|
|
"document.head.appendChild(styleSheet);",
|
|
"",
|
|
])
|
|
|
|
lines.extend([
|
|
"const preview: Preview = {",
|
|
" parameters: {",
|
|
" controls: {",
|
|
" matchers: {",
|
|
" color: /(background|color)$/i,",
|
|
" date: /Date$/i,",
|
|
" },",
|
|
" },",
|
|
" backgrounds: {",
|
|
" default: 'light',",
|
|
" values: [",
|
|
" { name: 'light', value: '#FFFFFF' },",
|
|
" { name: 'dark', value: '#1F2937' },",
|
|
" ],",
|
|
" },",
|
|
" },",
|
|
"};",
|
|
"",
|
|
"export default preview;",
|
|
])
|
|
|
|
return "\n".join(lines)
|
|
|
|
def generate_full_config(
|
|
self,
|
|
tokens: List[Dict[str, Any]],
|
|
brand_title: str = "Design System",
|
|
output_dir: Optional[str] = None,
|
|
) -> Dict[str, str]:
|
|
"""
|
|
Generate complete Storybook configuration files.
|
|
|
|
Args:
|
|
tokens: List of token dicts
|
|
brand_title: Brand title
|
|
output_dir: Optional directory to write files
|
|
|
|
Returns:
|
|
Dict mapping filenames to content
|
|
"""
|
|
# Generate theme
|
|
theme = self.generate_from_tokens(tokens, brand_title)
|
|
|
|
files = {
|
|
"dss-theme.ts": self.generate_theme_file(theme, "ts"),
|
|
"manager.ts": self.generate_manager_file(),
|
|
"preview.ts": self.generate_preview_file(tokens),
|
|
}
|
|
|
|
# Write files if output_dir provided
|
|
if output_dir:
|
|
out_path = Path(output_dir)
|
|
out_path.mkdir(parents=True, exist_ok=True)
|
|
|
|
for filename, content in files.items():
|
|
(out_path / filename).write_text(content)
|
|
|
|
return files
|