#!/usr/bin/env python3 """ CSS Code Generator
Generates CSS custom properties, utility classes, and component styles from design-system.json. Part of the Protocode rendering pipeline (ADR-091).
Usage: python css_generator.py design-system.json -o styles/ python css_generator.py design-system.json --format variables python css_generator.py design-system.json --format utilities
Author: CODITECT Architecture Team Version: 1.0.0 ADR: ADR-091 """
import json import re import sys from pathlib import Path from typing import Any, Dict, List, Optional
class CSSGenerator: """ Generates CSS from design system tokens.
Supports multiple output formats:
- CSS custom properties (variables)
- Utility classes (like Tailwind)
- Component-specific styles
"""
def __init__(self, design_system: Dict[str, Any]):
"""
Initialize the CSS generator.
Args:
design_system: Parsed design-system.json
"""
self.ds = design_system
def generate_variables(self, prefix: str = "") -> str:
"""
Generate CSS custom properties from design system.
Args:
prefix: Optional prefix for variable names
Returns:
CSS string with :root variables
"""
lines = [":root {"]
def walk(obj: Any, path: str):
if isinstance(obj, dict):
for key, value in obj.items():
new_path = f"{path}-{key}" if path else key
walk(value, new_path)
elif obj is not None:
var_name = f"--{prefix}{path}" if prefix else f"--{path}"
lines.append(f" {var_name}: {obj};")
# Skip metadata fields
for key in ["colors", "typography", "spacing", "borderRadius", "shadows",
"transitions", "breakpoints", "zIndex"]:
if key in self.ds:
walk(self.ds[key], key)
lines.append("}")
return "\n".join(lines)
def generate_utilities(self) -> str:
"""
Generate utility classes similar to Tailwind CSS.
Returns:
CSS string with utility classes
"""
sections = []
# Color utilities
if "colors" in self.ds:
sections.append(self._generate_color_utilities())
# Spacing utilities
if "spacing" in self.ds:
sections.append(self._generate_spacing_utilities())
# Typography utilities
if "typography" in self.ds:
sections.append(self._generate_typography_utilities())
# Border radius utilities
if "borderRadius" in self.ds:
sections.append(self._generate_border_radius_utilities())
# Shadow utilities
if "shadows" in self.ds:
sections.append(self._generate_shadow_utilities())
return "\n\n".join(sections)
def _generate_color_utilities(self) -> str:
"""Generate color utility classes."""
lines = ["/* Color Utilities */"]
def add_colors(prefix: str, prop: str, colors: Dict[str, Any], path: str = ""):
for key, value in colors.items():
if isinstance(value, dict):
add_colors(prefix, prop, value, f"{path}-{key}" if path else key)
else:
class_name = f".{prefix}{path}-{key}" if path else f".{prefix}{key}"
lines.append(f"{class_name} {{ {prop}: {value}; }}")
if "colors" in self.ds:
# Background colors
add_colors("bg", "background-color", self.ds["colors"])
lines.append("")
# Text colors
add_colors("text", "color", self.ds["colors"])
lines.append("")
# Border colors
add_colors("border", "border-color", self.ds["colors"])
return "\n".join(lines)
def _generate_spacing_utilities(self) -> str:
"""Generate spacing utility classes."""
lines = ["/* Spacing Utilities */"]
spacing = self.ds.get("spacing", {})
for key, value in spacing.items():
# Padding utilities
lines.append(f".p-{key} {{ padding: {value}; }}")
lines.append(f".px-{key} {{ padding-left: {value}; padding-right: {value}; }}")
lines.append(f".py-{key} {{ padding-top: {value}; padding-bottom: {value}; }}")
lines.append(f".pt-{key} {{ padding-top: {value}; }}")
lines.append(f".pr-{key} {{ padding-right: {value}; }}")
lines.append(f".pb-{key} {{ padding-bottom: {value}; }}")
lines.append(f".pl-{key} {{ padding-left: {value}; }}")
# Margin utilities
lines.append(f".m-{key} {{ margin: {value}; }}")
lines.append(f".mx-{key} {{ margin-left: {value}; margin-right: {value}; }}")
lines.append(f".my-{key} {{ margin-top: {value}; margin-bottom: {value}; }}")
lines.append(f".mt-{key} {{ margin-top: {value}; }}")
lines.append(f".mr-{key} {{ margin-right: {value}; }}")
lines.append(f".mb-{key} {{ margin-bottom: {value}; }}")
lines.append(f".ml-{key} {{ margin-left: {value}; }}")
# Gap utilities
lines.append(f".gap-{key} {{ gap: {value}; }}")
lines.append("")
return "\n".join(lines)
def _generate_typography_utilities(self) -> str:
"""Generate typography utility classes."""
lines = ["/* Typography Utilities */"]
typography = self.ds.get("typography", {})
# Font family
for key, value in typography.get("fontFamily", {}).items():
lines.append(f".font-{key} {{ font-family: {value}; }}")
lines.append("")
# Font size
for key, value in typography.get("fontSize", {}).items():
lines.append(f".text-{key} {{ font-size: {value}; }}")
lines.append("")
# Font weight
for key, value in typography.get("fontWeight", {}).items():
lines.append(f".font-{key} {{ font-weight: {value}; }}")
lines.append("")
# Line height
for key, value in typography.get("lineHeight", {}).items():
lines.append(f".leading-{key} {{ line-height: {value}; }}")
return "\n".join(lines)
def _generate_border_radius_utilities(self) -> str:
"""Generate border radius utility classes."""
lines = ["/* Border Radius Utilities */"]
for key, value in self.ds.get("borderRadius", {}).items():
lines.append(f".rounded-{key} {{ border-radius: {value}; }}")
lines.append(f".rounded-t-{key} {{ border-top-left-radius: {value}; border-top-right-radius: {value}; }}")
lines.append(f".rounded-b-{key} {{ border-bottom-left-radius: {value}; border-bottom-right-radius: {value}; }}")
lines.append(f".rounded-l-{key} {{ border-top-left-radius: {value}; border-bottom-left-radius: {value}; }}")
lines.append(f".rounded-r-{key} {{ border-top-right-radius: {value}; border-bottom-right-radius: {value}; }}")
return "\n".join(lines)
def _generate_shadow_utilities(self) -> str:
"""Generate shadow utility classes."""
lines = ["/* Shadow Utilities */"]
for key, value in self.ds.get("shadows", {}).items():
lines.append(f".shadow-{key} {{ box-shadow: {value}; }}")
return "\n".join(lines)
def generate_component_styles(self) -> str:
"""
Generate component-specific styles from design system.
Returns:
CSS string with component styles
"""
lines = ["/* Component Styles */"]
components = self.ds.get("components", {})
for component_name, tokens in components.items():
class_name = self._to_kebab_case(component_name)
lines.append(f"\n.{class_name} {{")
for prop, value in tokens.items():
css_prop = self._to_css_property(prop)
# Resolve token references
if isinstance(value, str) and value.startswith("$"):
value = f"var(--{value[1:].replace('.', '-')})"
lines.append(f" {css_prop}: {value};")
lines.append("}")
return "\n".join(lines)
def generate_responsive_utilities(self) -> str:
"""
Generate responsive utility classes with media queries.
Returns:
CSS string with responsive utilities
"""
breakpoints = self.ds.get("breakpoints", {
"sm": "640px",
"md": "768px",
"lg": "1024px",
"xl": "1280px",
"2xl": "1536px",
})
lines = ["/* Responsive Utilities */"]
for bp_name, bp_value in breakpoints.items():
lines.append(f"\n@media (min-width: {bp_value}) {{")
# Display utilities
lines.append(f" .{bp_name}\\:block {{ display: block; }}")
lines.append(f" .{bp_name}\\:hidden {{ display: none; }}")
lines.append(f" .{bp_name}\\:flex {{ display: flex; }}")
lines.append(f" .{bp_name}\\:grid {{ display: grid; }}")
# Width utilities
lines.append(f" .{bp_name}\\:w-full {{ width: 100%; }}")
lines.append(f" .{bp_name}\\:w-auto {{ width: auto; }}")
# Flex utilities
lines.append(f" .{bp_name}\\:flex-row {{ flex-direction: row; }}")
lines.append(f" .{bp_name}\\:flex-col {{ flex-direction: column; }}")
lines.append("}")
return "\n".join(lines)
def generate_all(self) -> Dict[str, str]:
"""
Generate all CSS files.
Returns:
Dict mapping filenames to CSS content
"""
return {
"variables.css": self.generate_variables(),
"utilities.css": self.generate_utilities(),
"components.css": self.generate_component_styles(),
"responsive.css": self.generate_responsive_utilities(),
}
def _to_kebab_case(self, s: str) -> str:
"""Convert string to kebab-case."""
s = re.sub(r'([A-Z])', r'-\1', s)
return s.lower().lstrip('-')
def _to_css_property(self, prop: str) -> str:
"""Convert camelCase property to CSS property."""
# Handle special mappings
mappings = {
"paddingX": "padding-inline",
"paddingY": "padding-block",
"marginX": "margin-inline",
"marginY": "margin-block",
}
if prop in mappings:
return mappings[prop]
return self._to_kebab_case(prop)
def main(): """CLI entry point.""" import argparse
parser = argparse.ArgumentParser(
description='Generate CSS from design system',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
Generate all CSS files
python css_generator.py design-system.json -o styles/
Generate only CSS variables
python css_generator.py design-system.json --format variables
Generate utility classes
python css_generator.py design-system.json --format utilities
Generate with custom prefix
python css_generator.py design-system.json --prefix ds- """ )
parser.add_argument('design_system', help='Path to design-system.json')
parser.add_argument('--output', '-o', help='Output directory')
parser.add_argument('--format', '-f',
choices=['all', 'variables', 'utilities', 'components', 'responsive'],
default='all', help='Output format')
parser.add_argument('--prefix', '-p', default='', help='Variable name prefix')
args = parser.parse_args()
try:
# Load design system
with open(args.design_system, 'r', encoding='utf-8') as f:
ds = json.load(f)
generator = CSSGenerator(ds)
# Generate based on format
if args.format == 'all':
files = generator.generate_all()
elif args.format == 'variables':
files = {"variables.css": generator.generate_variables(args.prefix)}
elif args.format == 'utilities':
files = {"utilities.css": generator.generate_utilities()}
elif args.format == 'components':
files = {"components.css": generator.generate_component_styles()}
elif args.format == 'responsive':
files = {"responsive.css": generator.generate_responsive_utilities()}
# Output
if args.output:
output_dir = Path(args.output)
output_dir.mkdir(parents=True, exist_ok=True)
for filename, content in files.items():
filepath = output_dir / filename
filepath.write_text(content)
print(f"Generated: {filepath}")
else:
for filename, content in files.items():
print(f"/* === {filename} === */")
print(content)
print()
return 0
except FileNotFoundError as e:
print(f"Error: {e}", file=sys.stderr)
return 1
except json.JSONDecodeError as e:
print(f"Invalid JSON: {e}", file=sys.stderr)
return 1
if name == 'main': sys.exit(main())