fix: Address high-severity bandit issues
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
DSS Figma Sync CLI
|
||||
DSS Figma Sync CLI.
|
||||
|
||||
This script is a lightweight CLI wrapper around the FigmaTokenSource from the
|
||||
dss.ingest module. It fetches tokens and components from Figma and saves them
|
||||
@@ -10,22 +10,21 @@ The core extraction and processing logic resides in:
|
||||
dss.ingest.sources.figma.FigmaTokenSource
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from dataclasses import asdict
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from dss.ingest.base import TokenCollection
|
||||
from dss.ingest.sources.figma import FigmaTokenSource
|
||||
|
||||
# Ensure the project root is in the Python path
|
||||
DSS_ROOT = Path(__file__).parent.parent
|
||||
if str(DSS_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(DSS_ROOT))
|
||||
|
||||
from dss.ingest.sources.figma import FigmaTokenSource
|
||||
from dss.ingest.base import TokenCollection
|
||||
|
||||
# =============================================================================
|
||||
# CONFIGURATION
|
||||
@@ -39,6 +38,7 @@ COMPONENTS_DIR = DSS_ROOT / ".dss/components"
|
||||
# OUTPUT WRITER
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class OutputWriter:
|
||||
"""Writes extraction results to the DSS file structure."""
|
||||
|
||||
@@ -49,10 +49,10 @@ class OutputWriter:
|
||||
"""Write TokenCollection to a structured JSON file."""
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
tokens_file = output_dir / "figma-tokens.json"
|
||||
|
||||
|
||||
if self.verbose:
|
||||
print(f" [OUT] Writing {len(collection)} tokens to {tokens_file}")
|
||||
|
||||
|
||||
with open(tokens_file, "w") as f:
|
||||
json.dump(json.loads(collection.to_json()), f, indent=2)
|
||||
print(f" [OUT] Tokens: {tokens_file}")
|
||||
@@ -61,18 +61,22 @@ class OutputWriter:
|
||||
"""Write component registry."""
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
comp_file = output_dir / "figma-registry.json"
|
||||
|
||||
|
||||
if self.verbose:
|
||||
print(f" [OUT] Writing {components.get('component_count', 0)} components to {comp_file}")
|
||||
print(
|
||||
f" [OUT] Writing {components.get('component_count', 0)} components to {comp_file}"
|
||||
)
|
||||
|
||||
with open(comp_file, "w") as f:
|
||||
json.dump(components, f, indent=2)
|
||||
print(f" [OUT] Components: {comp_file}")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# MAIN ORCHESTRATOR
|
||||
# =============================================================================
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main CLI orchestration function."""
|
||||
parser = argparse.ArgumentParser(description="DSS Intelligent Figma Sync")
|
||||
@@ -95,7 +99,7 @@ async def main():
|
||||
print("[ERROR] No Figma token found.", file=sys.stderr)
|
||||
print(" Set FIGMA_TOKEN env var or add 'token' to .dss/config/figma.json", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
print_header(file_key, token, args.force)
|
||||
|
||||
# --- Extraction ---
|
||||
@@ -107,6 +111,7 @@ async def main():
|
||||
# In verbose mode, print more details
|
||||
if args.verbose:
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
@@ -120,13 +125,14 @@ async def main():
|
||||
print_summary(
|
||||
file_name=component_registry.get("file_name", "Unknown"),
|
||||
token_count=len(token_collection),
|
||||
component_count=component_registry.get("component_count", 0)
|
||||
component_count=component_registry.get("component_count", 0),
|
||||
)
|
||||
|
||||
|
||||
print("\n[OK] Sync successful!")
|
||||
print(" Next: Run the translation and theming pipeline.")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def load_config() -> Dict:
|
||||
"""Load Figma config from .dss/config/figma.json."""
|
||||
config_path = DSS_ROOT / ".dss/config/figma.json"
|
||||
@@ -135,9 +141,12 @@ def load_config() -> Dict:
|
||||
with open(config_path) as f:
|
||||
return json.load(f)
|
||||
except (json.JSONDecodeError, IOError) as e:
|
||||
print(f"[WARN] Could not read or parse config file: {config_path}\n{e}", file=sys.stderr)
|
||||
print(
|
||||
f"[WARN] Could not read or parse config file: {config_path}\n{e}", file=sys.stderr
|
||||
)
|
||||
return {}
|
||||
|
||||
|
||||
def print_header(file_key: str, token: str, force: bool):
|
||||
"""Prints the CLI header."""
|
||||
print("╔══════════════════════════════════════════════════════════════╗")
|
||||
@@ -148,6 +157,7 @@ def print_header(file_key: str, token: str, force: bool):
|
||||
print(f" Force: {force}")
|
||||
print("\n[1/3] Initializing Figma Ingestion Source...")
|
||||
|
||||
|
||||
def print_summary(file_name: str, token_count: int, component_count: int):
|
||||
"""Prints the final summary."""
|
||||
print("\n" + "=" * 60)
|
||||
|
||||
@@ -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__":
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
DSS Token Resolver - 3-Layer Cascade
|
||||
DSS Token Resolver - 3-Layer Cascade.
|
||||
|
||||
Merges tokens from Core → Skin → Theme into a single style-dictionary input.
|
||||
|
||||
Usage: python3 scripts/resolve-tokens.py [--skin SKIN] [--theme THEME] [--output PATH]
|
||||
@@ -8,14 +9,12 @@ Usage: python3 scripts/resolve-tokens.py [--skin SKIN] [--theme THEME] [--output
|
||||
Default: --skin shadcn --theme default --output .dss/data/_system/tokens/tokens.json
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
DSS_ROOT = Path(__file__).parent.parent
|
||||
DSS_DATA = DSS_ROOT / ".dss"
|
||||
@@ -31,9 +30,25 @@ PRIMITIVE_ALIASES = {
|
||||
|
||||
# Generate aliases for neutral color scales (zinc, slate, gray, etc.)
|
||||
NEUTRAL_SCALES = ["slate", "gray", "zinc", "neutral", "stone"]
|
||||
SEMANTIC_SCALES = ["red", "orange", "amber", "yellow", "lime", "green", "emerald",
|
||||
"teal", "cyan", "sky", "blue", "indigo", "violet", "purple",
|
||||
"fuchsia", "pink", "rose"]
|
||||
SEMANTIC_SCALES = [
|
||||
"red",
|
||||
"orange",
|
||||
"amber",
|
||||
"yellow",
|
||||
"lime",
|
||||
"green",
|
||||
"emerald",
|
||||
"teal",
|
||||
"cyan",
|
||||
"sky",
|
||||
"blue",
|
||||
"indigo",
|
||||
"violet",
|
||||
"purple",
|
||||
"fuchsia",
|
||||
"pink",
|
||||
"rose",
|
||||
]
|
||||
SCALE_VALUES = ["50", "100", "200", "300", "400", "500", "600", "700", "800", "900", "950"]
|
||||
|
||||
for scale in NEUTRAL_SCALES:
|
||||
@@ -48,7 +63,7 @@ for scale in SEMANTIC_SCALES:
|
||||
|
||||
|
||||
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:
|
||||
@@ -56,7 +71,7 @@ def load_json(path: Path) -> dict:
|
||||
|
||||
|
||||
def deep_merge(base: dict, override: dict) -> dict:
|
||||
"""Deep merge override into base, returning new dict"""
|
||||
"""Deep merge override into base, returning new dict."""
|
||||
result = base.copy()
|
||||
for key, value in override.items():
|
||||
if key.startswith("_"):
|
||||
@@ -71,14 +86,15 @@ def deep_merge(base: dict, override: dict) -> dict:
|
||||
def resolve_references(tokens: dict, primitives: dict) -> dict:
|
||||
"""
|
||||
Resolve token references like {color.zinc.500} using primitives.
|
||||
|
||||
Uses translation dictionary (PRIMITIVE_ALIASES) to map shorthand paths.
|
||||
Works recursively through the token structure.
|
||||
"""
|
||||
ref_pattern = re.compile(r'\{([^}]+)\}')
|
||||
ref_pattern = re.compile(r"\{([^}]+)\}")
|
||||
unresolved = []
|
||||
|
||||
def get_nested(obj: dict, path: str) -> Any:
|
||||
"""Get nested value from dict using dot notation"""
|
||||
"""Get nested value from dict using dot notation."""
|
||||
parts = path.split(".")
|
||||
current = obj
|
||||
for part in parts:
|
||||
@@ -92,7 +108,7 @@ def resolve_references(tokens: dict, primitives: dict) -> dict:
|
||||
return current
|
||||
|
||||
def resolve_value(value: Any) -> Any:
|
||||
"""Resolve references in a value"""
|
||||
"""Resolve references in a value."""
|
||||
if not isinstance(value, str):
|
||||
return value
|
||||
|
||||
@@ -126,7 +142,7 @@ def resolve_references(tokens: dict, primitives: dict) -> dict:
|
||||
return ref_pattern.sub(replacer, value)
|
||||
|
||||
def resolve_obj(obj: Any) -> Any:
|
||||
"""Recursively resolve references in object"""
|
||||
"""Recursively resolve references in object."""
|
||||
if isinstance(obj, dict):
|
||||
result = {}
|
||||
for key, value in obj.items():
|
||||
@@ -156,12 +172,10 @@ def resolve_references(tokens: dict, primitives: dict) -> dict:
|
||||
return resolved
|
||||
|
||||
|
||||
def resolve_tokens(
|
||||
skin_name: str = "shadcn",
|
||||
theme_name: str = "default"
|
||||
) -> Dict[str, Any]:
|
||||
def resolve_tokens(skin_name: str = "shadcn", theme_name: str = "default") -> Dict[str, Any]:
|
||||
"""
|
||||
Resolve tokens through the 3-layer cascade:
|
||||
|
||||
1. Core primitives (base values)
|
||||
2. Skin tokens (semantic mappings)
|
||||
3. Theme overrides (brand customization)
|
||||
@@ -172,7 +186,7 @@ def resolve_tokens(
|
||||
theme = load_json(DSS_DATA / "themes" / f"{theme_name}.json")
|
||||
|
||||
# Report what we're loading
|
||||
print(f"[INFO] Resolving tokens:")
|
||||
print("[INFO] Resolving tokens:")
|
||||
print(f" Core: {len(primitives)} categories")
|
||||
print(f" Skin: {skin_name}")
|
||||
print(f" Theme: {theme_name}")
|
||||
@@ -201,11 +215,7 @@ def resolve_tokens(
|
||||
# Clean up internal metadata
|
||||
def clean_tokens(obj):
|
||||
if isinstance(obj, dict):
|
||||
return {
|
||||
k: clean_tokens(v)
|
||||
for k, v in obj.items()
|
||||
if not k.startswith("_")
|
||||
}
|
||||
return {k: clean_tokens(v) for k, v in obj.items() if not k.startswith("_")}
|
||||
return obj
|
||||
|
||||
resolved = clean_tokens(resolved)
|
||||
@@ -217,8 +227,11 @@ def main():
|
||||
parser = argparse.ArgumentParser(description="Resolve DSS 3-layer token cascade")
|
||||
parser.add_argument("--skin", default="shadcn", help="Skin to use (default: shadcn)")
|
||||
parser.add_argument("--theme", default="default", help="Theme to use (default: default)")
|
||||
parser.add_argument("--output", default=".dss/data/_system/tokens/tokens.json",
|
||||
help="Output path for resolved tokens")
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
default=".dss/data/_system/tokens/tokens.json",
|
||||
help="Output path for resolved tokens",
|
||||
)
|
||||
parser.add_argument("--dry-run", action="store_true", help="Print tokens without saving")
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -255,13 +268,17 @@ def main():
|
||||
# Also save metadata
|
||||
meta_path = output_path.parent / "resolved-meta.json"
|
||||
with open(meta_path, "w") as f:
|
||||
json.dump({
|
||||
"resolved_at": datetime.now().isoformat(),
|
||||
"skin": args.skin,
|
||||
"theme": args.theme,
|
||||
"token_count": total,
|
||||
"layers": ["core/primitives", f"skins/{args.skin}", f"themes/{args.theme}"]
|
||||
}, f, indent=2)
|
||||
json.dump(
|
||||
{
|
||||
"resolved_at": datetime.now().isoformat(),
|
||||
"skin": args.skin,
|
||||
"theme": args.theme,
|
||||
"token_count": total,
|
||||
"layers": ["core/primitives", f"skins/{args.skin}", f"themes/{args.theme}"],
|
||||
},
|
||||
f,
|
||||
indent=2,
|
||||
)
|
||||
|
||||
print("")
|
||||
print("[OK] Token resolution complete!")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
DSS Theme Validation Script
|
||||
DSS Theme Validation Script.
|
||||
|
||||
Validates that themes only override tokens defined in the skin contract.
|
||||
|
||||
Usage: python3 scripts/validate-theme.py [--theme THEME_NAME] [--skin SKIN_NAME]
|
||||
@@ -8,9 +9,8 @@ Usage: python3 scripts/validate-theme.py [--theme THEME_NAME] [--skin SKIN_NAME]
|
||||
Defaults to validating all themes against the skin contract.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Set, Tuple
|
||||
|
||||
@@ -19,7 +19,7 @@ DSS_DATA = DSS_ROOT / ".dss"
|
||||
|
||||
|
||||
def load_json(path: Path) -> dict:
|
||||
"""Load JSON file"""
|
||||
"""Load JSON file."""
|
||||
if not path.exists():
|
||||
return {}
|
||||
with open(path) as f:
|
||||
@@ -27,7 +27,7 @@ def load_json(path: Path) -> dict:
|
||||
|
||||
|
||||
def get_contract_tokens(contract: dict) -> Dict[str, Set[str]]:
|
||||
"""Extract required token names from contract by category"""
|
||||
"""Extract required token names from contract by category."""
|
||||
required = contract.get("required_tokens", {})
|
||||
result = {}
|
||||
for category, data in required.items():
|
||||
@@ -37,7 +37,7 @@ def get_contract_tokens(contract: dict) -> Dict[str, Set[str]]:
|
||||
|
||||
|
||||
def get_theme_tokens(theme: dict) -> Dict[str, Set[str]]:
|
||||
"""Extract token names from theme by category"""
|
||||
"""Extract token names from theme by category."""
|
||||
result = {}
|
||||
for key, value in theme.items():
|
||||
if key.startswith("_"):
|
||||
@@ -61,14 +61,12 @@ def get_theme_tokens(theme: dict) -> Dict[str, Set[str]]:
|
||||
|
||||
|
||||
def get_skin_tokens(skin: dict) -> Dict[str, Set[str]]:
|
||||
"""Extract token names from skin by category"""
|
||||
"""Extract token names from skin by category."""
|
||||
return get_theme_tokens(skin) # Same structure
|
||||
|
||||
|
||||
def validate_theme(
|
||||
theme_path: Path,
|
||||
contract_path: Path,
|
||||
skin_path: Path = None
|
||||
theme_path: Path, contract_path: Path, skin_path: Path = None
|
||||
) -> Tuple[bool, List[str], List[str]]:
|
||||
"""
|
||||
Validate a theme against the skin contract.
|
||||
@@ -137,10 +135,7 @@ def validate_theme(
|
||||
return is_valid, errors, warnings
|
||||
|
||||
|
||||
def validate_skin(
|
||||
skin_path: Path,
|
||||
contract_path: Path
|
||||
) -> Tuple[bool, List[str], List[str]]:
|
||||
def validate_skin(skin_path: Path, contract_path: Path) -> Tuple[bool, List[str], List[str]]:
|
||||
"""
|
||||
Validate that a skin provides all required contract tokens.
|
||||
|
||||
@@ -176,16 +171,12 @@ def validate_skin(
|
||||
skin_category = skin_tokens[category]
|
||||
missing = required - skin_category
|
||||
if missing:
|
||||
errors.append(
|
||||
f"Skin missing required tokens in '{category}': {sorted(missing)}"
|
||||
)
|
||||
errors.append(f"Skin missing required tokens in '{category}': {sorted(missing)}")
|
||||
|
||||
# Note extra tokens (not an error, just info)
|
||||
extra = skin_category - required
|
||||
if extra:
|
||||
warnings.append(
|
||||
f"Skin has extra tokens in '{category}' (OK): {sorted(extra)}"
|
||||
)
|
||||
warnings.append(f"Skin has extra tokens in '{category}' (OK): {sorted(extra)}")
|
||||
|
||||
is_valid = len(errors) == 0
|
||||
return is_valid, errors, warnings
|
||||
@@ -197,7 +188,9 @@ def main():
|
||||
parser = argparse.ArgumentParser(description="Validate DSS themes and skins")
|
||||
parser.add_argument("--theme", help="Theme name to validate (default: all)")
|
||||
parser.add_argument("--skin", help="Skin name to validate (default: shadcn)")
|
||||
parser.add_argument("--validate-skin", action="store_true", help="Validate skin against contract")
|
||||
parser.add_argument(
|
||||
"--validate-skin", action="store_true", help="Validate skin against contract"
|
||||
)
|
||||
parser.add_argument("--quiet", "-q", action="store_true", help="Only show errors")
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -238,10 +231,11 @@ def main():
|
||||
if args.theme:
|
||||
themes = [args.theme]
|
||||
else:
|
||||
themes = [
|
||||
p.stem for p in themes_dir.glob("*.json")
|
||||
if not p.stem.startswith("_")
|
||||
] if themes_dir.exists() else []
|
||||
themes = (
|
||||
[p.stem for p in themes_dir.glob("*.json") if not p.stem.startswith("_")]
|
||||
if themes_dir.exists()
|
||||
else []
|
||||
)
|
||||
|
||||
for theme_name in themes:
|
||||
theme_path = themes_dir / f"{theme_name}.json"
|
||||
@@ -249,9 +243,7 @@ def main():
|
||||
print(f"\n[THEME] Validating: {theme_name}")
|
||||
print("-" * 40)
|
||||
|
||||
is_valid, errors, warnings = validate_theme(
|
||||
theme_path, contract_path, skin_path
|
||||
)
|
||||
is_valid, errors, warnings = validate_theme(theme_path, contract_path, skin_path)
|
||||
|
||||
if errors:
|
||||
all_valid = False
|
||||
|
||||
Reference in New Issue
Block a user