#!/usr/bin/env python3 """ DSS Storybook Generator. Generates Storybook stories from DSS tokens and component registry. Hierarchy: 1. Primitives (Foundation) - colors, spacing, typography, radius, shadows 2. Semantic Tokens (Design Tokens) - from skin 3. Components - from shadcn registry Usage: python3 scripts/generate-storybook.py [--output PATH] [--skin SKIN] Default output: admin-ui/src/stories/ """ import argparse import json from datetime import datetime from pathlib import Path DSS_ROOT = Path(__file__).parent.parent DSS_DATA = DSS_ROOT / ".dss" def load_json(path: Path) -> dict: """Load JSON file, return empty dict if not found.""" if not path.exists(): return {} with open(path) as f: return json.load(f) def ensure_dir(path: Path): """Ensure directory exists.""" path.mkdir(parents=True, exist_ok=True) def generate_color_primitives_story(primitives: dict, output_dir: Path): """Generate story for color primitives (full Tailwind palette).""" colors = primitives.get("color", {}) if not colors: return # Build organized sections base_section = "" neutral_section = "" semantic_section = "" # Base colors base = colors.get("base", {}) base_swatches = [] for name, data in base.items(): if name.startswith("_"): continue if isinstance(data, dict) and "value" in data: border = ( "border: 1px solid #e5e7eb;" if data["value"] in ["#ffffff", "transparent"] else "" ) base_swatches.append( f"""
{name}
{data['value']}
""" ) if base_swatches: base_section = f"""

Base

{''.join(base_swatches)}
""" # Neutral scales neutrals = colors.get("neutral", {}) neutral_palettes = [] for scale_name, scale in neutrals.items(): if scale_name.startswith("_"): continue if isinstance(scale, dict): shades = [] for shade, data in sorted( scale.items(), key=lambda x: int(x[0]) if x[0].isdigit() else 0 ): if isinstance(data, dict) and "value" in data: text_color = "#000" if int(shade) < 500 else "#fff" shades.append( f"""
{shade}
""" ) if shades: neutral_palettes.append( f"""

{scale_name}

{''.join(shades)}
""" ) if neutral_palettes: neutral_section = f"""

Neutral Scales

{''.join(neutral_palettes)}
""" # Semantic scales semantics = colors.get("semantic", {}) semantic_palettes = [] for scale_name, scale in semantics.items(): if scale_name.startswith("_"): continue if isinstance(scale, dict): shades = [] for shade, data in sorted( scale.items(), key=lambda x: int(x[0]) if x[0].isdigit() else 0 ): if isinstance(data, dict) and "value" in data: text_color = "#000" if int(shade) < 500 else "#fff" shades.append( f"""
{shade}
""" ) if shades: semantic_palettes.append( f"""

{scale_name}

{''.join(shades)}
""" ) if semantic_palettes: semantic_section = f"""

Semantic Scales

{''.join(semantic_palettes)}
""" story = f"""/** * Color Primitives - Foundation * Full Tailwind color palette organized by category * @generated {datetime.now().isoformat()} */ export default {{ title: 'Foundation/Colors/Primitives', tags: ['autodocs'], parameters: {{ docs: {{ description: {{ component: 'Core color primitives from Tailwind palette. Organized into Base, Neutral, and Semantic scales.' }} }} }} }}; const styles = ` .color-container {{ font-family: system-ui, sans-serif; }} .color-section {{ margin-bottom: 3rem; }} .color-section h2 {{ font-size: 1.25rem; border-bottom: 1px solid #e5e7eb; padding-bottom: 0.5rem; margin-bottom: 1rem; }} .swatch-row {{ display: flex; flex-wrap: wrap; gap: 1rem; }} .color-swatch {{ text-align: center; }} .swatch {{ width: 80px; height: 80px; border-radius: 8px; margin-bottom: 0.5rem; }} .label {{ font-weight: 500; font-size: 0.875rem; }} .value {{ font-family: monospace; font-size: 0.75rem; color: #6b7280; }} .palette-grid {{ display: flex; flex-wrap: wrap; gap: 2rem; }} .color-palette {{ min-width: 140px; }} .color-palette h3 {{ margin: 0 0 0.5rem; text-transform: capitalize; font-size: 0.875rem; font-weight: 600; }} .shades {{ display: flex; flex-direction: column; border-radius: 8px; overflow: hidden; }} .shade {{ padding: 0.5rem 0.75rem; font-family: monospace; font-size: 0.65rem; }} `; export const AllColors = {{ render: () => `
{base_section} {neutral_section} {semantic_section}
` }}; """ with open(output_dir / "ColorPrimitives.stories.js", "w") as f: f.write(story) print(" [OK] ColorPrimitives.stories.js") def generate_spacing_story(primitives: dict, output_dir: Path): """Generate story for spacing primitives.""" spacing = primitives.get("spacing", {}) if not spacing: return items = [] for name, data in sorted( spacing.items(), key=lambda x: float(x[0]) if x[0].replace(".", "").isdigit() else -1 ): if name.startswith("_"): continue if isinstance(data, dict) and "value" in data: comment = data.get("_comment", "") items.append( f"""
{name} {data['value']} {comment}
""" ) story = f"""/** * Spacing Primitives - Foundation * @generated {datetime.now().isoformat()} */ export default {{ title: 'Foundation/Spacing', tags: ['autodocs'], parameters: {{ docs: {{ description: {{ component: 'Spacing scale based on 4px grid. Use for margins, padding, and gaps.' }} }} }} }}; const styles = ` .spacing-grid {{ display: flex; flex-direction: column; gap: 0.25rem; max-width: 600px; }} .spacing-item {{ display: flex; align-items: center; gap: 1rem; }} .bar {{ height: 20px; background: var(--color-primary, #18181b); border-radius: 2px; min-width: 2px; }} .info {{ display: flex; gap: 1rem; font-family: monospace; font-size: 0.75rem; }} .name {{ font-weight: 600; min-width: 32px; }} .value {{ color: #6b7280; min-width: 80px; }} .comment {{ color: #9ca3af; font-size: 0.65rem; }} `; export const SpacingScale = {{ render: () => `
{''.join(items)}
` }}; """ with open(output_dir / "Spacing.stories.js", "w") as f: f.write(story) print(" [OK] Spacing.stories.js") def generate_typography_story(primitives: dict, resolved: dict, output_dir: Path): """Generate story for typography tokens.""" font = primitives.get("font", {}) typography = resolved.get("typography", {}) # Font families families = font.get("family", {}) family_samples = [] for name, data in families.items(): if name.startswith("_"): continue if isinstance(data, dict) and "value" in data: family_samples.append( f"""
The quick brown fox jumps over the lazy dog
{name} {data['value'][:40]}...
""" ) # Font sizes sizes = font.get("size", {}) size_samples = [] for name, data in sorted( sizes.items(), key=lambda x: float(x[0].replace("xl", "").replace("x", "")) if x[0].replace("xl", "").replace("x", "").replace(".", "").isdigit() else 0, ): if name.startswith("_"): continue if isinstance(data, dict) and "value" in data: size_samples.append( f"""
Aa {name} {data['value']}
""" ) # Font weights weights = font.get("weight", {}) weight_samples = [] for name, data in weights.items(): if name.startswith("_"): continue if isinstance(data, dict) and "value" in data: weight_samples.append( f"""
Aa {name} {data['value']}
""" ) # Typography styles from resolved tokens style_samples = [] for name, props in typography.items(): if isinstance(props, dict): font_family = props.get("font-family", {}).get("value", "Inter") font_size = props.get("font-size", {}).get("value", "16px") font_weight = props.get("font-weight", {}).get("value", 400) line_height = props.get("line-height", {}).get("value", "1.5") style_samples.append( f"""
The quick brown fox
{name} {font_size} / {font_weight}
""" ) story = f"""/** * Typography - Foundation * Font families, sizes, weights, and composed styles * @generated {datetime.now().isoformat()} */ export default {{ title: 'Foundation/Typography', tags: ['autodocs'], parameters: {{ docs: {{ description: {{ component: 'Typography primitives and composed text styles.' }} }} }} }}; const styles = ` .typo-container {{ font-family: system-ui, sans-serif; }} .section {{ margin-bottom: 3rem; }} .section h2 {{ font-size: 1.25rem; border-bottom: 1px solid #e5e7eb; padding-bottom: 0.5rem; margin-bottom: 1rem; }} .font-sample {{ margin-bottom: 1.5rem; }} .sample-text {{ font-size: 1.5rem; margin-bottom: 0.25rem; }} .meta {{ font-size: 0.75rem; font-family: monospace; }} .meta .name {{ font-weight: 600; margin-right: 1rem; }} .meta .value {{ color: #6b7280; }} .size-grid, .weight-grid {{ display: flex; flex-wrap: wrap; gap: 1.5rem; }} .size-sample, .weight-sample {{ text-align: center; min-width: 60px; }} .size-sample .text {{ display: block; margin-bottom: 0.25rem; }} .weight-sample .text {{ display: block; margin-bottom: 0.25rem; font-size: 1.5rem; }} .name {{ font-size: 0.75rem; font-weight: 500; display: block; }} .value {{ font-size: 0.65rem; color: #6b7280; font-family: monospace; }} .props {{ color: #6b7280; font-family: monospace; }} .style-sample {{ margin-bottom: 1rem; border-bottom: 1px solid #f3f4f6; padding-bottom: 1rem; }} .style-sample .text {{ margin-bottom: 0.25rem; color: var(--color-foreground, #18181b); }} `; export const FontFamilies = {{ render: () => `

Font Families

{''.join(family_samples)}
` }}; export const FontSizes = {{ render: () => `

Font Sizes

{''.join(size_samples)}
` }}; export const FontWeights = {{ render: () => `

Font Weights

{''.join(weight_samples)}
` }}; export const TextStyles = {{ render: () => `

Composed Text Styles

{''.join(style_samples)}
` }}; """ with open(output_dir / "Typography.stories.js", "w") as f: f.write(story) print(" [OK] Typography.stories.js") def generate_shadows_story(primitives: dict, resolved: dict, output_dir: Path): """Generate story for shadow tokens.""" shadows = primitives.get("shadow", {}) effects = resolved.get("effect", {}) # Combine both sources all_shadows = {} for name, data in shadows.items(): if name.startswith("_"): continue if isinstance(data, dict) and "value" in data: all_shadows[name] = data["value"] for name, data in effects.items(): if isinstance(data, dict) and "value" in data and "shadow" in name: all_shadows[name] = data["value"] items = [] for name, value in all_shadows.items(): items.append( f"""
{name}
{value[:50]}{"..." if len(value) > 50 else ""}
""" ) story = f"""/** * Shadows - Foundation * Box shadow scale * @generated {datetime.now().isoformat()} */ export default {{ title: 'Foundation/Effects/Shadows', tags: ['autodocs'], parameters: {{ docs: {{ description: {{ component: 'Box shadow tokens for elevation and depth.' }} }} }} }}; const styles = ` .shadows-grid {{ display: flex; flex-wrap: wrap; gap: 2rem; padding: 2rem; background: #f9fafb; }} .shadow-card {{ text-align: center; }} .box {{ width: 120px; height: 80px; background: white; border-radius: 8px; margin-bottom: 0.5rem; }} .name {{ font-size: 0.75rem; font-weight: 500; }} .value {{ font-size: 0.6rem; color: #6b7280; font-family: monospace; max-width: 120px; word-wrap: break-word; }} `; export const AllShadows = {{ render: () => `
{''.join(items)}
` }}; """ with open(output_dir / "Shadows.stories.js", "w") as f: f.write(story) print(" [OK] Shadows.stories.js") def generate_radius_story(primitives: dict, output_dir: Path): """Generate story for border radius tokens.""" radius = primitives.get("radius", {}) if not radius: return items = [] for name, data in radius.items(): if name.startswith("_"): continue if isinstance(data, dict) and "value" in data: comment = data.get("_comment", "") items.append( f"""
{name}
{data['value']}
{comment}
""" ) story = f"""/** * Border Radius - Foundation * @generated {datetime.now().isoformat()} */ export default {{ title: 'Foundation/Radius', tags: ['autodocs'], parameters: {{ docs: {{ description: {{ component: 'Border radius scale for consistent rounded corners.' }} }} }} }}; const styles = ` .radius-grid {{ display: flex; flex-wrap: wrap; gap: 2rem; }} .radius-item {{ text-align: center; }} .box {{ width: 80px; height: 80px; background: var(--color-primary, #18181b); margin-bottom: 0.5rem; }} .name {{ font-weight: 500; font-size: 0.875rem; }} .value {{ font-family: monospace; font-size: 0.75rem; color: #6b7280; }} .comment {{ font-size: 0.65rem; color: #9ca3af; }} `; export const RadiusScale = {{ render: () => `
{''.join(items)}
` }}; """ with open(output_dir / "Radius.stories.js", "w") as f: f.write(story) print(" [OK] Radius.stories.js") def generate_semantic_colors_story(resolved: dict, output_dir: Path): """Generate story for semantic color tokens.""" colors = resolved.get("color", {}) if not colors: return # Group by semantic category groups = { "Surface": [], "Primary": [], "Secondary": [], "Accent": [], "Muted": [], "Destructive": [], "Other": [], } for name, data in colors.items(): if isinstance(data, dict) and "value" in data: value = data["value"] comment = data.get("comment", "") card = f"""
{name}
{value}
{comment}
""" if any(x in name for x in ["background", "foreground", "card", "popover"]): groups["Surface"].append(card) elif "primary" in name: groups["Primary"].append(card) elif "secondary" in name: groups["Secondary"].append(card) elif "accent" in name: groups["Accent"].append(card) elif "muted" in name: groups["Muted"].append(card) elif "destructive" in name: groups["Destructive"].append(card) else: groups["Other"].append(card) sections = [] for group_name, cards in groups.items(): if cards: sections.append( f"""

{group_name}

{''.join(cards)}
""" ) story = f"""/** * Semantic Colors - Design Tokens * Resolved color tokens for light theme * @generated {datetime.now().isoformat()} */ export default {{ title: 'Tokens/Semantic Colors', tags: ['autodocs'], parameters: {{ docs: {{ description: {{ component: 'Semantic color tokens mapped from primitives. Use these in components via CSS variables.' }} }} }} }}; const styles = ` .semantic-colors {{ display: flex; flex-direction: column; gap: 2rem; }} .token-group h3 {{ margin: 0 0 1rem; font-size: 1rem; border-bottom: 1px solid #e5e7eb; padding-bottom: 0.5rem; }} .token-row {{ display: flex; flex-wrap: wrap; gap: 1rem; }} .token-card {{ text-align: center; }} .swatch {{ width: 80px; height: 60px; border-radius: 8px; border: 1px solid #e5e7eb; margin-bottom: 0.5rem; }} .name {{ font-size: 0.75rem; font-weight: 500; }} .value {{ font-family: monospace; font-size: 0.65rem; color: #6b7280; }} .comment {{ font-size: 0.6rem; color: #9ca3af; max-width: 80px; }} `; export const LightTheme = {{ render: () => `
{''.join(sections)}
` }}; """ with open(output_dir / "SemanticColors.stories.js", "w") as f: f.write(story) print(" [OK] SemanticColors.stories.js") def generate_component_stories(registry: dict, output_dir: Path): """Generate stories for shadcn components from registry.""" components = registry.get("components", {}) categories = registry.get("categories", {}) if not components: print(" [SKIP] No components in registry") return # Generate a story file per category for cat_id, cat_data in categories.items(): cat_name = cat_data.get("name", cat_id.title()) cat_desc = cat_data.get("description", "") cat_components = cat_data.get("components", []) component_cards = [] for comp_id in cat_components: comp = components.get(comp_id, {}) if not comp: continue name = comp.get("name", comp_id) desc = comp.get("description", "") variants = comp.get("variants", {}) radix = comp.get("radixPrimitive", "") deps = comp.get("dependencies", []) variant_badges = "" if variants: for var_name, var_values in variants.items(): badges = " ".join([f'{v}' for v in var_values[:4]]) variant_badges += f'
{var_name}: {badges}
' radix_badge = f'{radix}' if radix else "" deps_text = ", ".join(deps[:3]) if deps else "" component_cards.append( f"""

{name}

{radix_badge}

{desc}

{variant_badges}
{f'
deps: {deps_text}
' if deps_text else ''}
""" ) story = f"""/** * {cat_name} * {cat_desc} * @generated {datetime.now().isoformat()} */ export default {{ title: 'Components/{cat_name}', tags: ['autodocs'], parameters: {{ docs: {{ description: {{ component: '{cat_desc}' }} }} }} }}; const styles = ` .component-grid {{ display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 1.5rem; }} .component-card {{ border: 1px solid #e5e7eb; border-radius: 8px; padding: 1rem; background: white; }} .card-header {{ display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem; }} .card-header h3 {{ margin: 0; font-size: 1rem; }} .radix-badge {{ font-size: 0.6rem; background: #dbeafe; color: #1d4ed8; padding: 0.125rem 0.375rem; border-radius: 4px; font-family: monospace; }} .description {{ font-size: 0.8rem; color: #6b7280; margin: 0 0 0.75rem; }} .variants {{ margin-bottom: 0.5rem; }} .variant-row {{ font-size: 0.7rem; margin-bottom: 0.25rem; }} .var-name {{ font-weight: 500; margin-right: 0.25rem; }} .badge {{ display: inline-block; background: #f3f4f6; padding: 0.125rem 0.375rem; border-radius: 4px; margin-right: 0.25rem; font-family: monospace; font-size: 0.65rem; }} .deps {{ font-size: 0.65rem; color: #9ca3af; font-family: monospace; }} `; export const Overview = {{ name: 'Component Catalog', render: () => `
{''.join(component_cards)}
` }}; """ # Create safe filename filename = f"Components{cat_name.replace(' ', '')}.stories.js" with open(output_dir / filename, "w") as f: f.write(story) print(f" [OK] {filename} ({len(cat_components)} components)") def generate_overview_story(output_dir: Path): """Generate overview/introduction story.""" story = f"""/** * Design System Overview * @generated {datetime.now().isoformat()} */ export default {{ title: 'Overview', tags: ['autodocs'], parameters: {{ docs: {{ description: {{ component: 'DSS Design System - Token documentation and component library' }} }} }} }}; const styles = ` .overview {{ max-width: 800px; font-family: system-ui, sans-serif; }} .overview h1 {{ font-size: 2rem; margin-bottom: 0.5rem; }} .overview .subtitle {{ color: #6b7280; margin-bottom: 2rem; }} .overview h2 {{ font-size: 1.25rem; margin-top: 2rem; border-bottom: 1px solid #e5e7eb; padding-bottom: 0.5rem; }} .overview ul {{ padding-left: 1.5rem; }} .overview li {{ margin: 0.5rem 0; }} .overview code {{ background: #f3f4f6; padding: 0.125rem 0.375rem; border-radius: 4px; font-size: 0.875rem; }} .layer {{ display: flex; align-items: center; gap: 0.5rem; padding: 0.75rem; background: #f9fafb; border-radius: 8px; margin: 0.5rem 0; }} .layer-num {{ width: 24px; height: 24px; background: #18181b; color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 0.75rem; font-weight: 600; }} .stats {{ display: flex; gap: 1rem; margin: 1rem 0; }} .stat {{ background: #f9fafb; padding: 1rem; border-radius: 8px; text-align: center; }} .stat-value {{ font-size: 2rem; font-weight: 700; color: #18181b; }} .stat-label {{ font-size: 0.75rem; color: #6b7280; }} `; export const Introduction = {{ render: () => `

DSS Design System

Token-driven design system with 3-layer architecture

22
Color Scales
59
Components
6
Categories

Architecture

1 Core Primitives - Raw Tailwind values (colors, spacing, fonts)
2 Skin - Semantic mapping (primary, secondary, etc.)
3 Theme - Brand overrides

Navigation

Component Categories

Usage

Import tokens CSS in your project:

import '.dss/data/_system/themes/tokens.css';

Use CSS variables in your styles:

color: var(--color-primary);
background: var(--color-background);
` }}; """ with open(output_dir / "Overview.stories.js", "w") as f: f.write(story) print(" [OK] Overview.stories.js") def main(): parser = argparse.ArgumentParser(description="Generate Storybook stories from DSS tokens") parser.add_argument( "--output", default="admin-ui/src/stories", help="Output directory for stories" ) parser.add_argument("--skin", default="shadcn", help="Skin to use") args = parser.parse_args() print("=" * 60) print("DSS STORYBOOK GENERATOR") print("=" * 60) print("") # Determine output directory output_dir = Path(args.output) if not output_dir.is_absolute(): output_dir = DSS_ROOT / output_dir ensure_dir(output_dir) print(f"[INFO] Output: {output_dir}") # Load token sources primitives = load_json(DSS_DATA / "core" / "primitives.json") skin = load_json(DSS_DATA / "skins" / args.skin / "tokens.json") resolved = load_json(DSS_DATA / "data" / "_system" / "tokens" / "tokens.json") registry = load_json(DSS_DATA / "components" / "shadcn-registry.json") print(f"[INFO] Core primitives: {len(primitives)} categories") print(f"[INFO] Skin: {args.skin}") print( f"[INFO] Resolved tokens: {sum(len(v) if isinstance(v, dict) else 0 for v in resolved.values())} tokens" ) print(f"[INFO] Component registry: {len(registry.get('components', {}))} components") print("") print("[STEP] Generating Foundation stories...") generate_overview_story(output_dir) generate_color_primitives_story(primitives, output_dir) generate_spacing_story(primitives, output_dir) generate_typography_story(primitives, resolved, output_dir) generate_radius_story(primitives, output_dir) generate_shadows_story(primitives, resolved, output_dir) print("") print("[STEP] Generating Token stories...") generate_semantic_colors_story(resolved, output_dir) print("") print("[STEP] Generating Component stories...") generate_component_stories(registry, output_dir) print("") story_count = len(list(output_dir.glob("*.stories.js"))) print(f"[OK] Generated {story_count} story files") print("[OK] Run: cd admin-ui && npm run storybook") if __name__ == "__main__": main()