948 lines
30 KiB
Python
Executable File
948 lines
30 KiB
Python
Executable File
#!/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"""
|
|
<div class="color-swatch">
|
|
<div class="swatch" style="background: {data['value']}; {border}"></div>
|
|
<div class="label">{name}</div>
|
|
<div class="value">{data['value']}</div>
|
|
</div>"""
|
|
)
|
|
if base_swatches:
|
|
base_section = f"""
|
|
<div class="color-section">
|
|
<h2>Base</h2>
|
|
<div class="swatch-row">{''.join(base_swatches)}</div>
|
|
</div>"""
|
|
|
|
# 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"""
|
|
<div class="shade" style="background: {data['value']}; color: {text_color};">
|
|
<span>{shade}</span>
|
|
</div>"""
|
|
)
|
|
if shades:
|
|
neutral_palettes.append(
|
|
f"""
|
|
<div class="color-palette">
|
|
<h3>{scale_name}</h3>
|
|
<div class="shades">{''.join(shades)}</div>
|
|
</div>"""
|
|
)
|
|
if neutral_palettes:
|
|
neutral_section = f"""
|
|
<div class="color-section">
|
|
<h2>Neutral Scales</h2>
|
|
<div class="palette-grid">{''.join(neutral_palettes)}</div>
|
|
</div>"""
|
|
|
|
# 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"""
|
|
<div class="shade" style="background: {data['value']}; color: {text_color};">
|
|
<span>{shade}</span>
|
|
</div>"""
|
|
)
|
|
if shades:
|
|
semantic_palettes.append(
|
|
f"""
|
|
<div class="color-palette">
|
|
<h3>{scale_name}</h3>
|
|
<div class="shades">{''.join(shades)}</div>
|
|
</div>"""
|
|
)
|
|
if semantic_palettes:
|
|
semantic_section = f"""
|
|
<div class="color-section">
|
|
<h2>Semantic Scales</h2>
|
|
<div class="palette-grid">{''.join(semantic_palettes)}</div>
|
|
</div>"""
|
|
|
|
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: () => `
|
|
<style>${{styles}}</style>
|
|
<div class="color-container">
|
|
{base_section}
|
|
{neutral_section}
|
|
{semantic_section}
|
|
</div>
|
|
`
|
|
}};
|
|
"""
|
|
|
|
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"""
|
|
<div class="spacing-item">
|
|
<div class="bar" style="width: {data['value']};"></div>
|
|
<div class="info">
|
|
<span class="name">{name}</span>
|
|
<span class="value">{data['value']}</span>
|
|
<span class="comment">{comment}</span>
|
|
</div>
|
|
</div>"""
|
|
)
|
|
|
|
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: () => `
|
|
<style>${{styles}}</style>
|
|
<div class="spacing-grid">
|
|
{''.join(items)}
|
|
</div>
|
|
`
|
|
}};
|
|
"""
|
|
|
|
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"""
|
|
<div class="font-sample">
|
|
<div class="sample-text" style="font-family: {data['value']};">
|
|
The quick brown fox jumps over the lazy dog
|
|
</div>
|
|
<div class="meta">
|
|
<span class="name">{name}</span>
|
|
<span class="value">{data['value'][:40]}...</span>
|
|
</div>
|
|
</div>"""
|
|
)
|
|
|
|
# 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"""
|
|
<div class="size-sample">
|
|
<span class="text" style="font-size: {data['value']};">Aa</span>
|
|
<span class="name">{name}</span>
|
|
<span class="value">{data['value']}</span>
|
|
</div>"""
|
|
)
|
|
|
|
# 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"""
|
|
<div class="weight-sample">
|
|
<span class="text" style="font-weight: {data['value']};">Aa</span>
|
|
<span class="name">{name}</span>
|
|
<span class="value">{data['value']}</span>
|
|
</div>"""
|
|
)
|
|
|
|
# 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"""
|
|
<div class="style-sample">
|
|
<div class="text" style="font-family: {font_family}; font-size: {font_size}; font-weight: {font_weight}; line-height: {line_height};">
|
|
The quick brown fox
|
|
</div>
|
|
<div class="meta">
|
|
<span class="name">{name}</span>
|
|
<span class="props">{font_size} / {font_weight}</span>
|
|
</div>
|
|
</div>"""
|
|
)
|
|
|
|
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: () => `
|
|
<style>${{styles}}</style>
|
|
<div class="typo-container">
|
|
<div class="section">
|
|
<h2>Font Families</h2>
|
|
{''.join(family_samples)}
|
|
</div>
|
|
</div>
|
|
`
|
|
}};
|
|
|
|
export const FontSizes = {{
|
|
render: () => `
|
|
<style>${{styles}}</style>
|
|
<div class="typo-container">
|
|
<div class="section">
|
|
<h2>Font Sizes</h2>
|
|
<div class="size-grid">{''.join(size_samples)}</div>
|
|
</div>
|
|
</div>
|
|
`
|
|
}};
|
|
|
|
export const FontWeights = {{
|
|
render: () => `
|
|
<style>${{styles}}</style>
|
|
<div class="typo-container">
|
|
<div class="section">
|
|
<h2>Font Weights</h2>
|
|
<div class="weight-grid">{''.join(weight_samples)}</div>
|
|
</div>
|
|
</div>
|
|
`
|
|
}};
|
|
|
|
export const TextStyles = {{
|
|
render: () => `
|
|
<style>${{styles}}</style>
|
|
<div class="typo-container">
|
|
<div class="section">
|
|
<h2>Composed Text Styles</h2>
|
|
{''.join(style_samples)}
|
|
</div>
|
|
</div>
|
|
`
|
|
}};
|
|
"""
|
|
|
|
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"""
|
|
<div class="shadow-card">
|
|
<div class="box" style="box-shadow: {value};"></div>
|
|
<div class="name">{name}</div>
|
|
<div class="value">{value[:50]}{"..." if len(value) > 50 else ""}</div>
|
|
</div>"""
|
|
)
|
|
|
|
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: () => `
|
|
<style>${{styles}}</style>
|
|
<div class="shadows-grid">
|
|
{''.join(items)}
|
|
</div>
|
|
`
|
|
}};
|
|
"""
|
|
|
|
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"""
|
|
<div class="radius-item">
|
|
<div class="box" style="border-radius: {data['value']};"></div>
|
|
<div class="name">{name}</div>
|
|
<div class="value">{data['value']}</div>
|
|
<div class="comment">{comment}</div>
|
|
</div>"""
|
|
)
|
|
|
|
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: () => `
|
|
<style>${{styles}}</style>
|
|
<div class="radius-grid">
|
|
{''.join(items)}
|
|
</div>
|
|
`
|
|
}};
|
|
"""
|
|
|
|
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"""
|
|
<div class="token-card">
|
|
<div class="swatch" style="background: {value};"></div>
|
|
<div class="name">{name}</div>
|
|
<div class="value">{value}</div>
|
|
<div class="comment">{comment}</div>
|
|
</div>"""
|
|
|
|
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"""
|
|
<div class="token-group">
|
|
<h3>{group_name}</h3>
|
|
<div class="token-row">{''.join(cards)}</div>
|
|
</div>"""
|
|
)
|
|
|
|
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: () => `
|
|
<style>${{styles}}</style>
|
|
<div class="semantic-colors">
|
|
{''.join(sections)}
|
|
</div>
|
|
`
|
|
}};
|
|
"""
|
|
|
|
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'<span class="badge">{v}</span>' for v in var_values[:4]])
|
|
variant_badges += f'<div class="variant-row"><span class="var-name">{var_name}:</span> {badges}</div>'
|
|
|
|
radix_badge = f'<span class="radix-badge">{radix}</span>' if radix else ""
|
|
deps_text = ", ".join(deps[:3]) if deps else ""
|
|
|
|
component_cards.append(
|
|
f"""
|
|
<div class="component-card">
|
|
<div class="card-header">
|
|
<h3>{name}</h3>
|
|
{radix_badge}
|
|
</div>
|
|
<p class="description">{desc}</p>
|
|
<div class="variants">{variant_badges}</div>
|
|
{f'<div class="deps">deps: {deps_text}</div>' if deps_text else ''}
|
|
</div>"""
|
|
)
|
|
|
|
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: () => `
|
|
<style>${{styles}}</style>
|
|
<div class="component-grid">
|
|
{''.join(component_cards)}
|
|
</div>
|
|
`
|
|
}};
|
|
"""
|
|
|
|
# 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: () => `
|
|
<style>${{styles}}</style>
|
|
<div class="overview">
|
|
<h1>DSS Design System</h1>
|
|
<p class="subtitle">Token-driven design system with 3-layer architecture</p>
|
|
|
|
<div class="stats">
|
|
<div class="stat">
|
|
<div class="stat-value">22</div>
|
|
<div class="stat-label">Color Scales</div>
|
|
</div>
|
|
<div class="stat">
|
|
<div class="stat-value">59</div>
|
|
<div class="stat-label">Components</div>
|
|
</div>
|
|
<div class="stat">
|
|
<div class="stat-value">6</div>
|
|
<div class="stat-label">Categories</div>
|
|
</div>
|
|
</div>
|
|
|
|
<h2>Architecture</h2>
|
|
<div class="layer">
|
|
<span class="layer-num">1</span>
|
|
<strong>Core Primitives</strong> - Raw Tailwind values (colors, spacing, fonts)
|
|
</div>
|
|
<div class="layer">
|
|
<span class="layer-num">2</span>
|
|
<strong>Skin</strong> - Semantic mapping (primary, secondary, etc.)
|
|
</div>
|
|
<div class="layer">
|
|
<span class="layer-num">3</span>
|
|
<strong>Theme</strong> - Brand overrides
|
|
</div>
|
|
|
|
<h2>Navigation</h2>
|
|
<ul>
|
|
<li><strong>Foundation</strong> - Core primitives (colors, spacing, typography, radius, shadows)</li>
|
|
<li><strong>Tokens</strong> - Semantic design tokens from skin</li>
|
|
<li><strong>Components</strong> - 59 shadcn/ui components organized by category</li>
|
|
</ul>
|
|
|
|
<h2>Component Categories</h2>
|
|
<ul>
|
|
<li><strong>Form</strong> - Button, Input, Select, Checkbox, etc.</li>
|
|
<li><strong>Data Display</strong> - Table, Badge, Avatar, Chart, etc.</li>
|
|
<li><strong>Feedback</strong> - Alert, Toast, Progress, Spinner, etc.</li>
|
|
<li><strong>Navigation</strong> - Tabs, Breadcrumb, Sidebar, etc.</li>
|
|
<li><strong>Overlay</strong> - Dialog, Sheet, Dropdown, Tooltip, etc.</li>
|
|
<li><strong>Layout</strong> - Card, Separator, Scroll Area, etc.</li>
|
|
</ul>
|
|
|
|
<h2>Usage</h2>
|
|
<p>Import tokens CSS in your project:</p>
|
|
<pre><code>import '.dss/data/_system/themes/tokens.css';</code></pre>
|
|
<p>Use CSS variables in your styles:</p>
|
|
<pre><code>color: var(--color-primary);
|
|
background: var(--color-background);</code></pre>
|
|
</div>
|
|
`
|
|
}};
|
|
"""
|
|
|
|
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()
|