fix: Address high-severity bandit issues
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
DSS Storybook Generator
|
||||
DSS Storybook Generator.
|
||||
|
||||
Generates Storybook stories from DSS tokens and component registry.
|
||||
|
||||
Hierarchy:
|
||||
@@ -13,20 +14,17 @@ Usage: python3 scripts/generate-storybook.py [--output PATH] [--skin SKIN]
|
||||
Default output: admin-ui/src/stories/
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import json
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, List
|
||||
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"""
|
||||
"""Load JSON file, return empty dict if not found."""
|
||||
if not path.exists():
|
||||
return {}
|
||||
with open(path) as f:
|
||||
@@ -34,12 +32,12 @@ def load_json(path: Path) -> dict:
|
||||
|
||||
|
||||
def ensure_dir(path: Path):
|
||||
"""Ensure directory exists"""
|
||||
"""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)"""
|
||||
"""Generate story for color primitives (full Tailwind palette)."""
|
||||
colors = primitives.get("color", {})
|
||||
if not colors:
|
||||
return
|
||||
@@ -56,19 +54,23 @@ def generate_color_primitives_story(primitives: dict, output_dir: Path):
|
||||
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'''
|
||||
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>''')
|
||||
</div>"""
|
||||
)
|
||||
if base_swatches:
|
||||
base_section = f'''
|
||||
base_section = f"""
|
||||
<div class="color-section">
|
||||
<h2>Base</h2>
|
||||
<div class="swatch-row">{''.join(base_swatches)}</div>
|
||||
</div>'''
|
||||
</div>"""
|
||||
|
||||
# Neutral scales
|
||||
neutrals = colors.get("neutral", {})
|
||||
@@ -78,25 +80,31 @@ def generate_color_primitives_story(primitives: dict, output_dir: Path):
|
||||
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):
|
||||
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'''
|
||||
shades.append(
|
||||
f"""
|
||||
<div class="shade" style="background: {data['value']}; color: {text_color};">
|
||||
<span>{shade}</span>
|
||||
</div>''')
|
||||
</div>"""
|
||||
)
|
||||
if shades:
|
||||
neutral_palettes.append(f'''
|
||||
neutral_palettes.append(
|
||||
f"""
|
||||
<div class="color-palette">
|
||||
<h3>{scale_name}</h3>
|
||||
<div class="shades">{''.join(shades)}</div>
|
||||
</div>''')
|
||||
</div>"""
|
||||
)
|
||||
if neutral_palettes:
|
||||
neutral_section = f'''
|
||||
neutral_section = f"""
|
||||
<div class="color-section">
|
||||
<h2>Neutral Scales</h2>
|
||||
<div class="palette-grid">{''.join(neutral_palettes)}</div>
|
||||
</div>'''
|
||||
</div>"""
|
||||
|
||||
# Semantic scales
|
||||
semantics = colors.get("semantic", {})
|
||||
@@ -106,27 +114,33 @@ def generate_color_primitives_story(primitives: dict, output_dir: Path):
|
||||
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):
|
||||
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'''
|
||||
shades.append(
|
||||
f"""
|
||||
<div class="shade" style="background: {data['value']}; color: {text_color};">
|
||||
<span>{shade}</span>
|
||||
</div>''')
|
||||
</div>"""
|
||||
)
|
||||
if shades:
|
||||
semantic_palettes.append(f'''
|
||||
semantic_palettes.append(
|
||||
f"""
|
||||
<div class="color-palette">
|
||||
<h3>{scale_name}</h3>
|
||||
<div class="shades">{''.join(shades)}</div>
|
||||
</div>''')
|
||||
</div>"""
|
||||
)
|
||||
if semantic_palettes:
|
||||
semantic_section = f'''
|
||||
semantic_section = f"""
|
||||
<div class="color-section">
|
||||
<h2>Semantic Scales</h2>
|
||||
<div class="palette-grid">{''.join(semantic_palettes)}</div>
|
||||
</div>'''
|
||||
</div>"""
|
||||
|
||||
story = f'''/**
|
||||
story = f"""/**
|
||||
* Color Primitives - Foundation
|
||||
* Full Tailwind color palette organized by category
|
||||
* @generated {datetime.now().isoformat()}
|
||||
@@ -169,26 +183,29 @@ export const AllColors = {{
|
||||
</div>
|
||||
`
|
||||
}};
|
||||
'''
|
||||
"""
|
||||
|
||||
with open(output_dir / "ColorPrimitives.stories.js", "w") as f:
|
||||
f.write(story)
|
||||
print(f" [OK] ColorPrimitives.stories.js")
|
||||
print(" [OK] ColorPrimitives.stories.js")
|
||||
|
||||
|
||||
def generate_spacing_story(primitives: dict, output_dir: Path):
|
||||
"""Generate story for spacing primitives"""
|
||||
"""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):
|
||||
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'''
|
||||
items.append(
|
||||
f"""
|
||||
<div class="spacing-item">
|
||||
<div class="bar" style="width: {data['value']};"></div>
|
||||
<div class="info">
|
||||
@@ -196,9 +213,10 @@ def generate_spacing_story(primitives: dict, output_dir: Path):
|
||||
<span class="value">{data['value']}</span>
|
||||
<span class="comment">{comment}</span>
|
||||
</div>
|
||||
</div>''')
|
||||
</div>"""
|
||||
)
|
||||
|
||||
story = f'''/**
|
||||
story = f"""/**
|
||||
* Spacing Primitives - Foundation
|
||||
* @generated {datetime.now().isoformat()}
|
||||
*/
|
||||
@@ -232,15 +250,15 @@ export const SpacingScale = {{
|
||||
</div>
|
||||
`
|
||||
}};
|
||||
'''
|
||||
"""
|
||||
|
||||
with open(output_dir / "Spacing.stories.js", "w") as f:
|
||||
f.write(story)
|
||||
print(f" [OK] Spacing.stories.js")
|
||||
print(" [OK] Spacing.stories.js")
|
||||
|
||||
|
||||
def generate_typography_story(primitives: dict, resolved: dict, output_dir: Path):
|
||||
"""Generate story for typography tokens"""
|
||||
"""Generate story for typography tokens."""
|
||||
font = primitives.get("font", {})
|
||||
typography = resolved.get("typography", {})
|
||||
|
||||
@@ -251,7 +269,8 @@ def generate_typography_story(primitives: dict, resolved: dict, output_dir: Path
|
||||
if name.startswith("_"):
|
||||
continue
|
||||
if isinstance(data, dict) and "value" in data:
|
||||
family_samples.append(f'''
|
||||
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
|
||||
@@ -260,21 +279,29 @@ def generate_typography_story(primitives: dict, resolved: dict, output_dir: Path
|
||||
<span class="name">{name}</span>
|
||||
<span class="value">{data['value'][:40]}...</span>
|
||||
</div>
|
||||
</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):
|
||||
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'''
|
||||
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>''')
|
||||
</div>"""
|
||||
)
|
||||
|
||||
# Font weights
|
||||
weights = font.get("weight", {})
|
||||
@@ -283,12 +310,14 @@ def generate_typography_story(primitives: dict, resolved: dict, output_dir: Path
|
||||
if name.startswith("_"):
|
||||
continue
|
||||
if isinstance(data, dict) and "value" in data:
|
||||
weight_samples.append(f'''
|
||||
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>''')
|
||||
</div>"""
|
||||
)
|
||||
|
||||
# Typography styles from resolved tokens
|
||||
style_samples = []
|
||||
@@ -299,7 +328,8 @@ def generate_typography_story(primitives: dict, resolved: dict, output_dir: Path
|
||||
font_weight = props.get("font-weight", {}).get("value", 400)
|
||||
line_height = props.get("line-height", {}).get("value", "1.5")
|
||||
|
||||
style_samples.append(f'''
|
||||
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
|
||||
@@ -308,9 +338,10 @@ def generate_typography_story(primitives: dict, resolved: dict, output_dir: Path
|
||||
<span class="name">{name}</span>
|
||||
<span class="props">{font_size} / {font_weight}</span>
|
||||
</div>
|
||||
</div>''')
|
||||
</div>"""
|
||||
)
|
||||
|
||||
story = f'''/**
|
||||
story = f"""/**
|
||||
* Typography - Foundation
|
||||
* Font families, sizes, weights, and composed styles
|
||||
* @generated {datetime.now().isoformat()}
|
||||
@@ -394,15 +425,15 @@ export const TextStyles = {{
|
||||
</div>
|
||||
`
|
||||
}};
|
||||
'''
|
||||
"""
|
||||
|
||||
with open(output_dir / "Typography.stories.js", "w") as f:
|
||||
f.write(story)
|
||||
print(f" [OK] Typography.stories.js")
|
||||
print(" [OK] Typography.stories.js")
|
||||
|
||||
|
||||
def generate_shadows_story(primitives: dict, resolved: dict, output_dir: Path):
|
||||
"""Generate story for shadow tokens"""
|
||||
"""Generate story for shadow tokens."""
|
||||
shadows = primitives.get("shadow", {})
|
||||
effects = resolved.get("effect", {})
|
||||
|
||||
@@ -420,14 +451,16 @@ def generate_shadows_story(primitives: dict, resolved: dict, output_dir: Path):
|
||||
|
||||
items = []
|
||||
for name, value in all_shadows.items():
|
||||
items.append(f'''
|
||||
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>''')
|
||||
</div>"""
|
||||
)
|
||||
|
||||
story = f'''/**
|
||||
story = f"""/**
|
||||
* Shadows - Foundation
|
||||
* Box shadow scale
|
||||
* @generated {datetime.now().isoformat()}
|
||||
@@ -460,15 +493,15 @@ export const AllShadows = {{
|
||||
</div>
|
||||
`
|
||||
}};
|
||||
'''
|
||||
"""
|
||||
|
||||
with open(output_dir / "Shadows.stories.js", "w") as f:
|
||||
f.write(story)
|
||||
print(f" [OK] Shadows.stories.js")
|
||||
print(" [OK] Shadows.stories.js")
|
||||
|
||||
|
||||
def generate_radius_story(primitives: dict, output_dir: Path):
|
||||
"""Generate story for border radius tokens"""
|
||||
"""Generate story for border radius tokens."""
|
||||
radius = primitives.get("radius", {})
|
||||
if not radius:
|
||||
return
|
||||
@@ -479,15 +512,17 @@ def generate_radius_story(primitives: dict, output_dir: Path):
|
||||
continue
|
||||
if isinstance(data, dict) and "value" in data:
|
||||
comment = data.get("_comment", "")
|
||||
items.append(f'''
|
||||
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>''')
|
||||
</div>"""
|
||||
)
|
||||
|
||||
story = f'''/**
|
||||
story = f"""/**
|
||||
* Border Radius - Foundation
|
||||
* @generated {datetime.now().isoformat()}
|
||||
*/
|
||||
@@ -520,15 +555,15 @@ export const RadiusScale = {{
|
||||
</div>
|
||||
`
|
||||
}};
|
||||
'''
|
||||
"""
|
||||
|
||||
with open(output_dir / "Radius.stories.js", "w") as f:
|
||||
f.write(story)
|
||||
print(f" [OK] Radius.stories.js")
|
||||
print(" [OK] Radius.stories.js")
|
||||
|
||||
|
||||
def generate_semantic_colors_story(resolved: dict, output_dir: Path):
|
||||
"""Generate story for semantic color tokens"""
|
||||
"""Generate story for semantic color tokens."""
|
||||
colors = resolved.get("color", {})
|
||||
if not colors:
|
||||
return
|
||||
@@ -541,20 +576,20 @@ def generate_semantic_colors_story(resolved: dict, output_dir: Path):
|
||||
"Accent": [],
|
||||
"Muted": [],
|
||||
"Destructive": [],
|
||||
"Other": []
|
||||
"Other": [],
|
||||
}
|
||||
|
||||
for name, data in colors.items():
|
||||
if isinstance(data, dict) and "value" in data:
|
||||
value = data["value"]
|
||||
comment = data.get("comment", "")
|
||||
card = f'''
|
||||
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>'''
|
||||
</div>"""
|
||||
|
||||
if any(x in name for x in ["background", "foreground", "card", "popover"]):
|
||||
groups["Surface"].append(card)
|
||||
@@ -574,13 +609,15 @@ def generate_semantic_colors_story(resolved: dict, output_dir: Path):
|
||||
sections = []
|
||||
for group_name, cards in groups.items():
|
||||
if cards:
|
||||
sections.append(f'''
|
||||
sections.append(
|
||||
f"""
|
||||
<div class="token-group">
|
||||
<h3>{group_name}</h3>
|
||||
<div class="token-row">{''.join(cards)}</div>
|
||||
</div>''')
|
||||
</div>"""
|
||||
)
|
||||
|
||||
story = f'''/**
|
||||
story = f"""/**
|
||||
* Semantic Colors - Design Tokens
|
||||
* Resolved color tokens for light theme
|
||||
* @generated {datetime.now().isoformat()}
|
||||
@@ -616,15 +653,15 @@ export const LightTheme = {{
|
||||
</div>
|
||||
`
|
||||
}};
|
||||
'''
|
||||
"""
|
||||
|
||||
with open(output_dir / "SemanticColors.stories.js", "w") as f:
|
||||
f.write(story)
|
||||
print(f" [OK] SemanticColors.stories.js")
|
||||
print(" [OK] SemanticColors.stories.js")
|
||||
|
||||
|
||||
def generate_component_stories(registry: dict, output_dir: Path):
|
||||
"""Generate stories for shadcn components from registry"""
|
||||
"""Generate stories for shadcn components from registry."""
|
||||
components = registry.get("components", {})
|
||||
categories = registry.get("categories", {})
|
||||
|
||||
@@ -659,7 +696,8 @@ def generate_component_stories(registry: dict, output_dir: Path):
|
||||
radix_badge = f'<span class="radix-badge">{radix}</span>' if radix else ""
|
||||
deps_text = ", ".join(deps[:3]) if deps else ""
|
||||
|
||||
component_cards.append(f'''
|
||||
component_cards.append(
|
||||
f"""
|
||||
<div class="component-card">
|
||||
<div class="card-header">
|
||||
<h3>{name}</h3>
|
||||
@@ -668,9 +706,10 @@ def generate_component_stories(registry: dict, output_dir: Path):
|
||||
<p class="description">{desc}</p>
|
||||
<div class="variants">{variant_badges}</div>
|
||||
{f'<div class="deps">deps: {deps_text}</div>' if deps_text else ''}
|
||||
</div>''')
|
||||
</div>"""
|
||||
)
|
||||
|
||||
story = f'''/**
|
||||
story = f"""/**
|
||||
* {cat_name}
|
||||
* {cat_desc}
|
||||
* @generated {datetime.now().isoformat()}
|
||||
@@ -735,7 +774,7 @@ export const Overview = {{
|
||||
</div>
|
||||
`
|
||||
}};
|
||||
'''
|
||||
"""
|
||||
|
||||
# Create safe filename
|
||||
filename = f"Components{cat_name.replace(' ', '')}.stories.js"
|
||||
@@ -745,8 +784,8 @@ export const Overview = {{
|
||||
|
||||
|
||||
def generate_overview_story(output_dir: Path):
|
||||
"""Generate overview/introduction story"""
|
||||
story = f'''/**
|
||||
"""Generate overview/introduction story."""
|
||||
story = f"""/**
|
||||
* Design System Overview
|
||||
* @generated {datetime.now().isoformat()}
|
||||
*/
|
||||
@@ -840,17 +879,18 @@ background: var(--color-background);</code></pre>
|
||||
</div>
|
||||
`
|
||||
}};
|
||||
'''
|
||||
"""
|
||||
|
||||
with open(output_dir / "Overview.stories.js", "w") as f:
|
||||
f.write(story)
|
||||
print(f" [OK] Overview.stories.js")
|
||||
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(
|
||||
"--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()
|
||||
|
||||
@@ -875,7 +915,9 @@ def main():
|
||||
|
||||
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] 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("")
|
||||
|
||||
@@ -898,7 +940,7 @@ def main():
|
||||
print("")
|
||||
story_count = len(list(output_dir.glob("*.stories.js")))
|
||||
print(f"[OK] Generated {story_count} story files")
|
||||
print(f"[OK] Run: cd admin-ui && npm run storybook")
|
||||
print("[OK] Run: cd admin-ui && npm run storybook")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user