fix: Address high-severity bandit issues

This commit is contained in:
DSS
2025-12-11 07:13:06 -03:00
parent bcb4475744
commit 5b2a328dd1
167 changed files with 7051 additions and 7168 deletions

View File

@@ -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)

View File

@@ -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__":

View File

@@ -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!")

View File

@@ -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