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:
374
tools/storybook/theme.py
Normal file
374
tools/storybook/theme.py
Normal file
@@ -0,0 +1,374 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user