Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Generate complete design systems with styles, colors, fonts, and UX rules for web and mobile apps
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/design_system.py
1#!/usr/bin/env python32# -*- coding: utf-8 -*-3"""4Design System Generator - Aggregates search results and applies reasoning5to generate comprehensive design system recommendations.67Usage:8from design_system import generate_design_system9result = generate_design_system("SaaS dashboard", "My Project")1011# With persistence (Master + Overrides pattern)12result = generate_design_system("SaaS dashboard", "My Project", persist=True)13result = generate_design_system("SaaS dashboard", "My Project", persist=True, page="dashboard")14"""1516import csv17import json18import os19from datetime import datetime20from pathlib import Path21from core import search, DATA_DIR222324# ============ CONFIGURATION ============25REASONING_FILE = "ui-reasoning.csv"2627SEARCH_CONFIG = {28"product": {"max_results": 1},29"style": {"max_results": 3},30"color": {"max_results": 2},31"landing": {"max_results": 2},32"typography": {"max_results": 2}33}343536# ============ DESIGN SYSTEM GENERATOR ============37class DesignSystemGenerator:38"""Generates design system recommendations from aggregated searches."""3940def __init__(self):41self.reasoning_data = self._load_reasoning()4243def _load_reasoning(self) -> list:44"""Load reasoning rules from CSV."""45filepath = DATA_DIR / REASONING_FILE46if not filepath.exists():47return []48with open(filepath, 'r', encoding='utf-8') as f:49return list(csv.DictReader(f))5051def _multi_domain_search(self, query: str, style_priority: list = None) -> dict:52"""Execute searches across multiple domains."""53results = {}54for domain, config in SEARCH_CONFIG.items():55if domain == "style" and style_priority:56# For style, also search with priority keywords57priority_query = " ".join(style_priority[:2]) if style_priority else query58combined_query = f"{query} {priority_query}"59results[domain] = search(combined_query, domain, config["max_results"])60else:61results[domain] = search(query, domain, config["max_results"])62return results6364def _find_reasoning_rule(self, category: str) -> dict:65"""Find matching reasoning rule for a category."""66category_lower = category.lower()6768# Try exact match first69for rule in self.reasoning_data:70if rule.get("UI_Category", "").lower() == category_lower:71return rule7273# Try partial match74for rule in self.reasoning_data:75ui_cat = rule.get("UI_Category", "").lower()76if ui_cat in category_lower or category_lower in ui_cat:77return rule7879# Try keyword match80for rule in self.reasoning_data:81ui_cat = rule.get("UI_Category", "").lower()82keywords = ui_cat.replace("/", " ").replace("-", " ").split()83if any(kw in category_lower for kw in keywords):84return rule8586return {}8788def _apply_reasoning(self, category: str, search_results: dict) -> dict:89"""Apply reasoning rules to search results."""90rule = self._find_reasoning_rule(category)9192if not rule:93return {94"pattern": "Hero + Features + CTA",95"style_priority": ["Minimalism", "Flat Design"],96"color_mood": "Professional",97"typography_mood": "Clean",98"key_effects": "Subtle hover transitions",99"anti_patterns": "",100"decision_rules": {},101"severity": "MEDIUM"102}103104# Parse decision rules JSON105decision_rules = {}106try:107decision_rules = json.loads(rule.get("Decision_Rules", "{}"))108except json.JSONDecodeError:109pass110111return {112"pattern": rule.get("Recommended_Pattern", ""),113"style_priority": [s.strip() for s in rule.get("Style_Priority", "").split("+")],114"color_mood": rule.get("Color_Mood", ""),115"typography_mood": rule.get("Typography_Mood", ""),116"key_effects": rule.get("Key_Effects", ""),117"anti_patterns": rule.get("Anti_Patterns", ""),118"decision_rules": decision_rules,119"severity": rule.get("Severity", "MEDIUM")120}121122def _select_best_match(self, results: list, priority_keywords: list) -> dict:123"""Select best matching result based on priority keywords."""124if not results:125return {}126127if not priority_keywords:128return results[0]129130# First: try exact style name match131for priority in priority_keywords:132priority_lower = priority.lower().strip()133for result in results:134style_name = result.get("Style Category", "").lower()135if priority_lower in style_name or style_name in priority_lower:136return result137138# Second: score by keyword match in all fields139scored = []140for result in results:141result_str = str(result).lower()142score = 0143for kw in priority_keywords:144kw_lower = kw.lower().strip()145# Higher score for style name match146if kw_lower in result.get("Style Category", "").lower():147score += 10148# Lower score for keyword field match149elif kw_lower in result.get("Keywords", "").lower():150score += 3151# Even lower for other field matches152elif kw_lower in result_str:153score += 1154scored.append((score, result))155156scored.sort(key=lambda x: x[0], reverse=True)157return scored[0][1] if scored and scored[0][0] > 0 else results[0]158159def _extract_results(self, search_result: dict) -> list:160"""Extract results list from search result dict."""161return search_result.get("results", [])162163def generate(self, query: str, project_name: str = None) -> dict:164"""Generate complete design system recommendation."""165# Step 1: First search product to get category166product_result = search(query, "product", 1)167product_results = product_result.get("results", [])168category = "General"169if product_results:170category = product_results[0].get("Product Type", "General")171172# Step 2: Get reasoning rules for this category173reasoning = self._apply_reasoning(category, {})174style_priority = reasoning.get("style_priority", [])175176# Step 3: Multi-domain search with style priority hints177search_results = self._multi_domain_search(query, style_priority)178search_results["product"] = product_result # Reuse product search179180# Step 4: Select best matches from each domain using priority181style_results = self._extract_results(search_results.get("style", {}))182color_results = self._extract_results(search_results.get("color", {}))183typography_results = self._extract_results(search_results.get("typography", {}))184landing_results = self._extract_results(search_results.get("landing", {}))185186best_style = self._select_best_match(style_results, reasoning.get("style_priority", []))187best_color = color_results[0] if color_results else {}188best_typography = typography_results[0] if typography_results else {}189best_landing = landing_results[0] if landing_results else {}190191# Step 5: Build final recommendation192# Combine effects from both reasoning and style search193style_effects = best_style.get("Effects & Animation", "")194reasoning_effects = reasoning.get("key_effects", "")195combined_effects = style_effects if style_effects else reasoning_effects196197return {198"project_name": project_name or query.upper(),199"category": category,200"pattern": {201"name": best_landing.get("Pattern Name", reasoning.get("pattern", "Hero + Features + CTA")),202"sections": best_landing.get("Section Order", "Hero > Features > CTA"),203"cta_placement": best_landing.get("Primary CTA Placement", "Above fold"),204"color_strategy": best_landing.get("Color Strategy", ""),205"conversion": best_landing.get("Conversion Optimization", "")206},207"style": {208"name": best_style.get("Style Category", "Minimalism"),209"type": best_style.get("Type", "General"),210"effects": style_effects,211"keywords": best_style.get("Keywords", ""),212"best_for": best_style.get("Best For", ""),213"performance": best_style.get("Performance", ""),214"accessibility": best_style.get("Accessibility", ""),215"light_mode": best_style.get("Light Mode ✓", ""),216"dark_mode": best_style.get("Dark Mode ✓", ""),217},218"colors": {219"primary": best_color.get("Primary", "#2563EB"),220"on_primary": best_color.get("On Primary", ""),221"secondary": best_color.get("Secondary", "#3B82F6"),222"accent": best_color.get("Accent", "#F97316"),223"background": best_color.get("Background", "#F8FAFC"),224"foreground": best_color.get("Foreground", "#1E293B"),225"muted": best_color.get("Muted", ""),226"border": best_color.get("Border", ""),227"destructive": best_color.get("Destructive", ""),228"ring": best_color.get("Ring", ""),229"notes": best_color.get("Notes", ""),230# Keep legacy keys for backward compat in MASTER.md231"cta": best_color.get("Accent", "#F97316"),232"text": best_color.get("Foreground", "#1E293B"),233},234"typography": {235"heading": best_typography.get("Heading Font", "Inter"),236"body": best_typography.get("Body Font", "Inter"),237"mood": best_typography.get("Mood/Style Keywords", reasoning.get("typography_mood", "")),238"best_for": best_typography.get("Best For", ""),239"google_fonts_url": best_typography.get("Google Fonts URL", ""),240"css_import": best_typography.get("CSS Import", "")241},242"key_effects": combined_effects,243"anti_patterns": reasoning.get("anti_patterns", ""),244"decision_rules": reasoning.get("decision_rules", {}),245"severity": reasoning.get("severity", "MEDIUM")246}247248249# ============ OUTPUT FORMATTERS ============250BOX_WIDTH = 90 # Wider box for more content251252253def hex_to_ansi(hex_color: str) -> str:254"""Convert hex color to ANSI True Color swatch (██) with fallback."""255if not hex_color or not hex_color.startswith('#'):256return ""257colorterm = os.environ.get('COLORTERM', '')258if colorterm not in ('truecolor', '24bit'):259return ""260hex_color = hex_color.lstrip('#')261if len(hex_color) != 6:262return ""263r, g, b = int(hex_color[0:2], 16), int(hex_color[2:4], 16), int(hex_color[4:6], 16)264return f"\033[38;2;{r};{g};{b}m██\033[0m "265266267def ansi_ljust(s: str, width: int) -> str:268"""Like str.ljust but accounts for zero-width ANSI escape sequences."""269import re270visible_len = len(re.sub(r'\033\[[0-9;]*m', '', s))271pad = width - visible_len272return s + (" " * max(0, pad))273274275def section_header(name: str, width: int) -> str:276"""Create a Unicode section separator: ├─── NAME ───...┤"""277label = f"─── {name} "278fill = "─" * (width - len(label) - 1)279return f"├{label}{fill}┤"280281282def format_ascii_box(design_system: dict) -> str:283"""Format design system as Unicode box with ANSI color swatches."""284project = design_system.get("project_name", "PROJECT")285pattern = design_system.get("pattern", {})286style = design_system.get("style", {})287colors = design_system.get("colors", {})288typography = design_system.get("typography", {})289effects = design_system.get("key_effects", "")290anti_patterns = design_system.get("anti_patterns", "")291292def wrap_text(text: str, prefix: str, width: int) -> list:293"""Wrap long text into multiple lines."""294if not text:295return []296words = text.split()297lines = []298current_line = prefix299for word in words:300if len(current_line) + len(word) + 1 <= width - 2:301current_line += (" " if current_line != prefix else "") + word302else:303if current_line != prefix:304lines.append(current_line)305current_line = prefix + word306if current_line != prefix:307lines.append(current_line)308return lines309310# Build sections from pattern311sections = pattern.get("sections", "").split(">")312sections = [s.strip() for s in sections if s.strip()]313314# Build output lines315lines = []316w = BOX_WIDTH - 1317318# Header with double-line box319lines.append("╔" + "═" * w + "╗")320lines.append(ansi_ljust(f"║ TARGET: {project} - RECOMMENDED DESIGN SYSTEM", BOX_WIDTH) + "║")321lines.append("╚" + "═" * w + "╝")322lines.append("┌" + "─" * w + "┐")323324# Pattern section325lines.append(section_header("PATTERN", BOX_WIDTH + 1))326lines.append(f"│ Name: {pattern.get('name', '')}".ljust(BOX_WIDTH) + "│")327if pattern.get('conversion'):328lines.append(f"│ Conversion: {pattern.get('conversion', '')}".ljust(BOX_WIDTH) + "│")329if pattern.get('cta_placement'):330lines.append(f"│ CTA: {pattern.get('cta_placement', '')}".ljust(BOX_WIDTH) + "│")331lines.append("│ Sections:".ljust(BOX_WIDTH) + "│")332for i, section in enumerate(sections, 1):333lines.append(f"│ {i}. {section}".ljust(BOX_WIDTH) + "│")334335# Style section336lines.append(section_header("STYLE", BOX_WIDTH + 1))337lines.append(f"│ Name: {style.get('name', '')}".ljust(BOX_WIDTH) + "│")338light = style.get("light_mode", "")339dark = style.get("dark_mode", "")340if light or dark:341lines.append(f"│ Mode Support: Light {light} Dark {dark}".ljust(BOX_WIDTH) + "│")342if style.get("keywords"):343for line in wrap_text(f"Keywords: {style.get('keywords', '')}", "│ ", BOX_WIDTH):344lines.append(line.ljust(BOX_WIDTH) + "│")345if style.get("best_for"):346for line in wrap_text(f"Best For: {style.get('best_for', '')}", "│ ", BOX_WIDTH):347lines.append(line.ljust(BOX_WIDTH) + "│")348if style.get("performance") or style.get("accessibility"):349perf_a11y = f"Performance: {style.get('performance', '')} | Accessibility: {style.get('accessibility', '')}"350lines.append(f"│ {perf_a11y}".ljust(BOX_WIDTH) + "│")351352# Colors section (extended palette with ANSI swatches)353lines.append(section_header("COLORS", BOX_WIDTH + 1))354color_entries = [355("Primary", "primary", "--color-primary"),356("On Primary", "on_primary", "--color-on-primary"),357("Secondary", "secondary", "--color-secondary"),358("Accent/CTA", "accent", "--color-accent"),359("Background", "background", "--color-background"),360("Foreground", "foreground", "--color-foreground"),361("Muted", "muted", "--color-muted"),362("Border", "border", "--color-border"),363("Destructive", "destructive", "--color-destructive"),364("Ring", "ring", "--color-ring"),365]366for label, key, css_var in color_entries:367hex_val = colors.get(key, "")368if not hex_val:369continue370swatch = hex_to_ansi(hex_val)371content = f"│ {swatch}{label + ':':14s} {hex_val:10s} ({css_var})"372lines.append(ansi_ljust(content, BOX_WIDTH) + "│")373if colors.get("notes"):374for line in wrap_text(f"Notes: {colors.get('notes', '')}", "│ ", BOX_WIDTH):375lines.append(line.ljust(BOX_WIDTH) + "│")376377# Typography section378lines.append(section_header("TYPOGRAPHY", BOX_WIDTH + 1))379lines.append(f"│ {typography.get('heading', '')} / {typography.get('body', '')}".ljust(BOX_WIDTH) + "│")380if typography.get("mood"):381for line in wrap_text(f"Mood: {typography.get('mood', '')}", "│ ", BOX_WIDTH):382lines.append(line.ljust(BOX_WIDTH) + "│")383if typography.get("best_for"):384for line in wrap_text(f"Best For: {typography.get('best_for', '')}", "│ ", BOX_WIDTH):385lines.append(line.ljust(BOX_WIDTH) + "│")386if typography.get("google_fonts_url"):387lines.append(f"│ Google Fonts: {typography.get('google_fonts_url', '')}".ljust(BOX_WIDTH) + "│")388if typography.get("css_import"):389lines.append(f"│ CSS Import: {typography.get('css_import', '')[:70]}...".ljust(BOX_WIDTH) + "│")390391# Key Effects section392if effects:393lines.append(section_header("KEY EFFECTS", BOX_WIDTH + 1))394for line in wrap_text(effects, "│ ", BOX_WIDTH):395lines.append(line.ljust(BOX_WIDTH) + "│")396397# Anti-patterns section398if anti_patterns:399lines.append(section_header("AVOID", BOX_WIDTH + 1))400for line in wrap_text(anti_patterns, "│ ", BOX_WIDTH):401lines.append(line.ljust(BOX_WIDTH) + "│")402403# Pre-Delivery Checklist section404lines.append(section_header("PRE-DELIVERY CHECKLIST", BOX_WIDTH + 1))405checklist_items = [406"[ ] No emojis as icons (use SVG: Heroicons/Lucide)",407"[ ] cursor-pointer on all clickable elements",408"[ ] Hover states with smooth transitions (150-300ms)",409"[ ] Light mode: text contrast 4.5:1 minimum",410"[ ] Focus states visible for keyboard nav",411"[ ] prefers-reduced-motion respected",412"[ ] Responsive: 375px, 768px, 1024px, 1440px"413]414for item in checklist_items:415lines.append(f"│ {item}".ljust(BOX_WIDTH) + "│")416417lines.append("└" + "─" * w + "┘")418419return "\n".join(lines)420421422def format_markdown(design_system: dict) -> str:423"""Format design system as markdown."""424project = design_system.get("project_name", "PROJECT")425pattern = design_system.get("pattern", {})426style = design_system.get("style", {})427colors = design_system.get("colors", {})428typography = design_system.get("typography", {})429effects = design_system.get("key_effects", "")430anti_patterns = design_system.get("anti_patterns", "")431432lines = []433lines.append(f"## Design System: {project}")434lines.append("")435436# Pattern section437lines.append("### Pattern")438lines.append(f"- **Name:** {pattern.get('name', '')}")439if pattern.get('conversion'):440lines.append(f"- **Conversion Focus:** {pattern.get('conversion', '')}")441if pattern.get('cta_placement'):442lines.append(f"- **CTA Placement:** {pattern.get('cta_placement', '')}")443if pattern.get('color_strategy'):444lines.append(f"- **Color Strategy:** {pattern.get('color_strategy', '')}")445lines.append(f"- **Sections:** {pattern.get('sections', '')}")446lines.append("")447448# Style section449lines.append("### Style")450lines.append(f"- **Name:** {style.get('name', '')}")451light = style.get("light_mode", "")452dark = style.get("dark_mode", "")453if light or dark:454lines.append(f"- **Mode Support:** Light {light} | Dark {dark}")455if style.get('keywords'):456lines.append(f"- **Keywords:** {style.get('keywords', '')}")457if style.get('best_for'):458lines.append(f"- **Best For:** {style.get('best_for', '')}")459if style.get('performance') or style.get('accessibility'):460lines.append(f"- **Performance:** {style.get('performance', '')} | **Accessibility:** {style.get('accessibility', '')}")461lines.append("")462463# Colors section (extended palette)464lines.append("### Colors")465lines.append("| Role | Hex | CSS Variable |")466lines.append("|------|-----|--------------|")467md_color_entries = [468("Primary", "primary", "--color-primary"),469("On Primary", "on_primary", "--color-on-primary"),470("Secondary", "secondary", "--color-secondary"),471("Accent/CTA", "accent", "--color-accent"),472("Background", "background", "--color-background"),473("Foreground", "foreground", "--color-foreground"),474("Muted", "muted", "--color-muted"),475("Border", "border", "--color-border"),476("Destructive", "destructive", "--color-destructive"),477("Ring", "ring", "--color-ring"),478]479for label, key, css_var in md_color_entries:480hex_val = colors.get(key, "")481if hex_val:482lines.append(f"| {label} | `{hex_val}` | `{css_var}` |")483if colors.get("notes"):484lines.append(f"\n*Notes: {colors.get('notes', '')}*")485lines.append("")486487# Typography section488lines.append("### Typography")489lines.append(f"- **Heading:** {typography.get('heading', '')}")490lines.append(f"- **Body:** {typography.get('body', '')}")491if typography.get("mood"):492lines.append(f"- **Mood:** {typography.get('mood', '')}")493if typography.get("best_for"):494lines.append(f"- **Best For:** {typography.get('best_for', '')}")495if typography.get("google_fonts_url"):496lines.append(f"- **Google Fonts:** {typography.get('google_fonts_url', '')}")497if typography.get("css_import"):498lines.append(f"- **CSS Import:**")499lines.append(f"```css")500lines.append(f"{typography.get('css_import', '')}")501lines.append(f"```")502lines.append("")503504# Key Effects section505if effects:506lines.append("### Key Effects")507lines.append(f"{effects}")508lines.append("")509510# Anti-patterns section511if anti_patterns:512lines.append("### Avoid (Anti-patterns)")513newline_bullet = '\n- '514lines.append(f"- {anti_patterns.replace(' + ', newline_bullet)}")515lines.append("")516517# Pre-Delivery Checklist section518lines.append("### Pre-Delivery Checklist")519lines.append("- [ ] No emojis as icons (use SVG: Heroicons/Lucide)")520lines.append("- [ ] cursor-pointer on all clickable elements")521lines.append("- [ ] Hover states with smooth transitions (150-300ms)")522lines.append("- [ ] Light mode: text contrast 4.5:1 minimum")523lines.append("- [ ] Focus states visible for keyboard nav")524lines.append("- [ ] prefers-reduced-motion respected")525lines.append("- [ ] Responsive: 375px, 768px, 1024px, 1440px")526lines.append("")527528return "\n".join(lines)529530531# ============ MAIN ENTRY POINT ============532def generate_design_system(query: str, project_name: str = None, output_format: str = "ascii",533persist: bool = False, page: str = None, output_dir: str = None) -> str:534"""535Main entry point for design system generation.536537Args:538query: Search query (e.g., "SaaS dashboard", "e-commerce luxury")539project_name: Optional project name for output header540output_format: "ascii" (default) or "markdown"541persist: If True, save design system to design-system/ folder542page: Optional page name for page-specific override file543output_dir: Optional output directory (defaults to current working directory)544545Returns:546Formatted design system string547"""548generator = DesignSystemGenerator()549design_system = generator.generate(query, project_name)550551# Persist to files if requested552if persist:553persist_design_system(design_system, page, output_dir, query)554555if output_format == "markdown":556return format_markdown(design_system)557return format_ascii_box(design_system)558559560# ============ PERSISTENCE FUNCTIONS ============561def persist_design_system(design_system: dict, page: str = None, output_dir: str = None, page_query: str = None) -> dict:562"""563Persist design system to design-system/<project>/ folder using Master + Overrides pattern.564565Args:566design_system: The generated design system dictionary567page: Optional page name for page-specific override file568output_dir: Optional output directory (defaults to current working directory)569page_query: Optional query string for intelligent page override generation570571Returns:572dict with created file paths and status573"""574base_dir = Path(output_dir) if output_dir else Path.cwd()575576# Use project name for project-specific folder577project_name = design_system.get("project_name", "default")578project_slug = project_name.lower().replace(' ', '-')579580design_system_dir = base_dir / "design-system" / project_slug581pages_dir = design_system_dir / "pages"582583created_files = []584585# Create directories586design_system_dir.mkdir(parents=True, exist_ok=True)587pages_dir.mkdir(parents=True, exist_ok=True)588589master_file = design_system_dir / "MASTER.md"590591# Generate and write MASTER.md592master_content = format_master_md(design_system)593with open(master_file, 'w', encoding='utf-8') as f:594f.write(master_content)595created_files.append(str(master_file))596597# If page is specified, create page override file with intelligent content598if page:599page_file = pages_dir / f"{page.lower().replace(' ', '-')}.md"600page_content = format_page_override_md(design_system, page, page_query)601with open(page_file, 'w', encoding='utf-8') as f:602f.write(page_content)603created_files.append(str(page_file))604605return {606"status": "success",607"design_system_dir": str(design_system_dir),608"created_files": created_files609}610611612def format_master_md(design_system: dict) -> str:613"""Format design system as MASTER.md with hierarchical override logic."""614project = design_system.get("project_name", "PROJECT")615pattern = design_system.get("pattern", {})616style = design_system.get("style", {})617colors = design_system.get("colors", {})618typography = design_system.get("typography", {})619effects = design_system.get("key_effects", "")620anti_patterns = design_system.get("anti_patterns", "")621622timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")623624lines = []625626# Logic header627lines.append("# Design System Master File")628lines.append("")629lines.append("> **LOGIC:** When building a specific page, first check `design-system/pages/[page-name].md`.")630lines.append("> If that file exists, its rules **override** this Master file.")631lines.append("> If not, strictly follow the rules below.")632lines.append("")633lines.append("---")634lines.append("")635lines.append(f"**Project:** {project}")636lines.append(f"**Generated:** {timestamp}")637lines.append(f"**Category:** {design_system.get('category', 'General')}")638lines.append("")639lines.append("---")640lines.append("")641642# Global Rules section643lines.append("## Global Rules")644lines.append("")645646# Color Palette647lines.append("### Color Palette")648lines.append("")649lines.append("| Role | Hex | CSS Variable |")650lines.append("|------|-----|--------------|")651master_color_entries = [652("Primary", "primary", "--color-primary"),653("On Primary", "on_primary", "--color-on-primary"),654("Secondary", "secondary", "--color-secondary"),655("Accent/CTA", "accent", "--color-accent"),656("Background", "background", "--color-background"),657("Foreground", "foreground", "--color-foreground"),658("Muted", "muted", "--color-muted"),659("Border", "border", "--color-border"),660("Destructive", "destructive", "--color-destructive"),661("Ring", "ring", "--color-ring"),662]663for label, key, css_var in master_color_entries:664hex_val = colors.get(key, "")665if hex_val:666lines.append(f"| {label} | `{hex_val}` | `{css_var}` |")667lines.append("")668if colors.get("notes"):669lines.append(f"**Color Notes:** {colors.get('notes', '')}")670lines.append("")671672# Typography673lines.append("### Typography")674lines.append("")675lines.append(f"- **Heading Font:** {typography.get('heading', 'Inter')}")676lines.append(f"- **Body Font:** {typography.get('body', 'Inter')}")677if typography.get("mood"):678lines.append(f"- **Mood:** {typography.get('mood', '')}")679if typography.get("google_fonts_url"):680lines.append(f"- **Google Fonts:** [{typography.get('heading', '')} + {typography.get('body', '')}]({typography.get('google_fonts_url', '')})")681lines.append("")682if typography.get("css_import"):683lines.append("**CSS Import:**")684lines.append("```css")685lines.append(typography.get("css_import", ""))686lines.append("```")687lines.append("")688689# Spacing Variables690lines.append("### Spacing Variables")691lines.append("")692lines.append("| Token | Value | Usage |")693lines.append("|-------|-------|-------|")694lines.append("| `--space-xs` | `4px` / `0.25rem` | Tight gaps |")695lines.append("| `--space-sm` | `8px` / `0.5rem` | Icon gaps, inline spacing |")696lines.append("| `--space-md` | `16px` / `1rem` | Standard padding |")697lines.append("| `--space-lg` | `24px` / `1.5rem` | Section padding |")698lines.append("| `--space-xl` | `32px` / `2rem` | Large gaps |")699lines.append("| `--space-2xl` | `48px` / `3rem` | Section margins |")700lines.append("| `--space-3xl` | `64px` / `4rem` | Hero padding |")701lines.append("")702703# Shadow Depths704lines.append("### Shadow Depths")705lines.append("")706lines.append("| Level | Value | Usage |")707lines.append("|-------|-------|-------|")708lines.append("| `--shadow-sm` | `0 1px 2px rgba(0,0,0,0.05)` | Subtle lift |")709lines.append("| `--shadow-md` | `0 4px 6px rgba(0,0,0,0.1)` | Cards, buttons |")710lines.append("| `--shadow-lg` | `0 10px 15px rgba(0,0,0,0.1)` | Modals, dropdowns |")711lines.append("| `--shadow-xl` | `0 20px 25px rgba(0,0,0,0.15)` | Hero images, featured cards |")712lines.append("")713714# Component Specs section715lines.append("---")716lines.append("")717lines.append("## Component Specs")718lines.append("")719720# Buttons721lines.append("### Buttons")722lines.append("")723lines.append("```css")724lines.append("/* Primary Button */")725lines.append(".btn-primary {")726lines.append(f" background: {colors.get('cta', '#F97316')};")727lines.append(" color: white;")728lines.append(" padding: 12px 24px;")729lines.append(" border-radius: 8px;")730lines.append(" font-weight: 600;")731lines.append(" transition: all 200ms ease;")732lines.append(" cursor: pointer;")733lines.append("}")734lines.append("")735lines.append(".btn-primary:hover {")736lines.append(" opacity: 0.9;")737lines.append(" transform: translateY(-1px);")738lines.append("}")739lines.append("")740lines.append("/* Secondary Button */")741lines.append(".btn-secondary {")742lines.append(f" background: transparent;")743lines.append(f" color: {colors.get('primary', '#2563EB')};")744lines.append(f" border: 2px solid {colors.get('primary', '#2563EB')};")745lines.append(" padding: 12px 24px;")746lines.append(" border-radius: 8px;")747lines.append(" font-weight: 600;")748lines.append(" transition: all 200ms ease;")749lines.append(" cursor: pointer;")750lines.append("}")751lines.append("```")752lines.append("")753754# Cards755lines.append("### Cards")756lines.append("")757lines.append("```css")758lines.append(".card {")759lines.append(f" background: {colors.get('background', '#FFFFFF')};")760lines.append(" border-radius: 12px;")761lines.append(" padding: 24px;")762lines.append(" box-shadow: var(--shadow-md);")763lines.append(" transition: all 200ms ease;")764lines.append(" cursor: pointer;")765lines.append("}")766lines.append("")767lines.append(".card:hover {")768lines.append(" box-shadow: var(--shadow-lg);")769lines.append(" transform: translateY(-2px);")770lines.append("}")771lines.append("```")772lines.append("")773774# Inputs775lines.append("### Inputs")776lines.append("")777lines.append("```css")778lines.append(".input {")779lines.append(" padding: 12px 16px;")780lines.append(" border: 1px solid #E2E8F0;")781lines.append(" border-radius: 8px;")782lines.append(" font-size: 16px;")783lines.append(" transition: border-color 200ms ease;")784lines.append("}")785lines.append("")786lines.append(".input:focus {")787lines.append(f" border-color: {colors.get('primary', '#2563EB')};")788lines.append(" outline: none;")789lines.append(f" box-shadow: 0 0 0 3px {colors.get('primary', '#2563EB')}20;")790lines.append("}")791lines.append("```")792lines.append("")793794# Modals795lines.append("### Modals")796lines.append("")797lines.append("```css")798lines.append(".modal-overlay {")799lines.append(" background: rgba(0, 0, 0, 0.5);")800lines.append(" backdrop-filter: blur(4px);")801lines.append("}")802lines.append("")803lines.append(".modal {")804lines.append(" background: white;")805lines.append(" border-radius: 16px;")806lines.append(" padding: 32px;")807lines.append(" box-shadow: var(--shadow-xl);")808lines.append(" max-width: 500px;")809lines.append(" width: 90%;")810lines.append("}")811lines.append("```")812lines.append("")813814# Style section815lines.append("---")816lines.append("")817lines.append("## Style Guidelines")818lines.append("")819lines.append(f"**Style:** {style.get('name', 'Minimalism')}")820lines.append("")821if style.get("keywords"):822lines.append(f"**Keywords:** {style.get('keywords', '')}")823lines.append("")824if style.get("best_for"):825lines.append(f"**Best For:** {style.get('best_for', '')}")826lines.append("")827if effects:828lines.append(f"**Key Effects:** {effects}")829lines.append("")830831# Layout Pattern832lines.append("### Page Pattern")833lines.append("")834lines.append(f"**Pattern Name:** {pattern.get('name', '')}")835lines.append("")836if pattern.get('conversion'):837lines.append(f"- **Conversion Strategy:** {pattern.get('conversion', '')}")838if pattern.get('cta_placement'):839lines.append(f"- **CTA Placement:** {pattern.get('cta_placement', '')}")840lines.append(f"- **Section Order:** {pattern.get('sections', '')}")841lines.append("")842843# Anti-Patterns section844lines.append("---")845lines.append("")846lines.append("## Anti-Patterns (Do NOT Use)")847lines.append("")848if anti_patterns:849anti_list = [a.strip() for a in anti_patterns.split("+")]850for anti in anti_list:851if anti:852lines.append(f"- ❌ {anti}")853lines.append("")854lines.append("### Additional Forbidden Patterns")855lines.append("")856lines.append("- ❌ **Emojis as icons** — Use SVG icons (Heroicons, Lucide, Simple Icons)")857lines.append("- ❌ **Missing cursor:pointer** — All clickable elements must have cursor:pointer")858lines.append("- ❌ **Layout-shifting hovers** — Avoid scale transforms that shift layout")859lines.append("- ❌ **Low contrast text** — Maintain 4.5:1 minimum contrast ratio")860lines.append("- ❌ **Instant state changes** — Always use transitions (150-300ms)")861lines.append("- ❌ **Invisible focus states** — Focus states must be visible for a11y")862lines.append("")863864# Pre-Delivery Checklist865lines.append("---")866lines.append("")867lines.append("## Pre-Delivery Checklist")868lines.append("")869lines.append("Before delivering any UI code, verify:")870lines.append("")871lines.append("- [ ] No emojis used as icons (use SVG instead)")872lines.append("- [ ] All icons from consistent icon set (Heroicons/Lucide)")873lines.append("- [ ] `cursor-pointer` on all clickable elements")874lines.append("- [ ] Hover states with smooth transitions (150-300ms)")875lines.append("- [ ] Light mode: text contrast 4.5:1 minimum")876lines.append("- [ ] Focus states visible for keyboard navigation")877lines.append("- [ ] `prefers-reduced-motion` respected")878lines.append("- [ ] Responsive: 375px, 768px, 1024px, 1440px")879lines.append("- [ ] No content hidden behind fixed navbars")880lines.append("- [ ] No horizontal scroll on mobile")881lines.append("")882883return "\n".join(lines)884885886def format_page_override_md(design_system: dict, page_name: str, page_query: str = None) -> str:887"""Format a page-specific override file with intelligent AI-generated content."""888project = design_system.get("project_name", "PROJECT")889timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")890page_title = page_name.replace("-", " ").replace("_", " ").title()891892# Detect page type and generate intelligent overrides893page_overrides = _generate_intelligent_overrides(page_name, page_query, design_system)894895lines = []896897lines.append(f"# {page_title} Page Overrides")898lines.append("")899lines.append(f"> **PROJECT:** {project}")900lines.append(f"> **Generated:** {timestamp}")901lines.append(f"> **Page Type:** {page_overrides.get('page_type', 'General')}")902lines.append("")903lines.append("> ⚠️ **IMPORTANT:** Rules in this file **override** the Master file (`design-system/MASTER.md`).")904lines.append("> Only deviations from the Master are documented here. For all other rules, refer to the Master.")905lines.append("")906lines.append("---")907lines.append("")908909# Page-specific rules with actual content910lines.append("## Page-Specific Rules")911lines.append("")912913# Layout Overrides914lines.append("### Layout Overrides")915lines.append("")916layout = page_overrides.get("layout", {})917if layout:918for key, value in layout.items():919lines.append(f"- **{key}:** {value}")920else:921lines.append("- No overrides — use Master layout")922lines.append("")923924# Spacing Overrides925lines.append("### Spacing Overrides")926lines.append("")927spacing = page_overrides.get("spacing", {})928if spacing:929for key, value in spacing.items():930lines.append(f"- **{key}:** {value}")931else:932lines.append("- No overrides — use Master spacing")933lines.append("")934935# Typography Overrides936lines.append("### Typography Overrides")937lines.append("")938typography = page_overrides.get("typography", {})939if typography:940for key, value in typography.items():941lines.append(f"- **{key}:** {value}")942else:943lines.append("- No overrides — use Master typography")944lines.append("")945946# Color Overrides947lines.append("### Color Overrides")948lines.append("")949colors = page_overrides.get("colors", {})950if colors:951for key, value in colors.items():952lines.append(f"- **{key}:** {value}")953else:954lines.append("- No overrides — use Master colors")955lines.append("")956957# Component Overrides958lines.append("### Component Overrides")959lines.append("")960components = page_overrides.get("components", [])961if components:962for comp in components:963lines.append(f"- {comp}")964else:965lines.append("- No overrides — use Master component specs")966lines.append("")967968# Page-Specific Components969lines.append("---")970lines.append("")971lines.append("## Page-Specific Components")972lines.append("")973unique_components = page_overrides.get("unique_components", [])974if unique_components:975for comp in unique_components:976lines.append(f"- {comp}")977else:978lines.append("- No unique components for this page")979lines.append("")980981# Recommendations982lines.append("---")983lines.append("")984lines.append("## Recommendations")985lines.append("")986recommendations = page_overrides.get("recommendations", [])987if recommendations:988for rec in recommendations:989lines.append(f"- {rec}")990lines.append("")991992return "\n".join(lines)993994995def _generate_intelligent_overrides(page_name: str, page_query: str, design_system: dict) -> dict:996"""997Generate intelligent overrides based on page type using layered search.998999Uses the existing search infrastructure to find relevant style, UX, and layout1000data instead of hardcoded page types.1001"""1002from core import search10031004page_lower = page_name.lower()1005query_lower = (page_query or "").lower()1006combined_context = f"{page_lower} {query_lower}"10071008# Search across multiple domains for page-specific guidance1009style_search = search(combined_context, "style", max_results=1)1010ux_search = search(combined_context, "ux", max_results=3)1011landing_search = search(combined_context, "landing", max_results=1)10121013# Extract results from search response1014style_results = style_search.get("results", [])1015ux_results = ux_search.get("results", [])1016landing_results = landing_search.get("results", [])10171018# Detect page type from search results or context1019page_type = _detect_page_type(combined_context, style_results)10201021# Build overrides from search results1022layout = {}1023spacing = {}1024typography = {}1025colors = {}1026components = []1027unique_components = []1028recommendations = []10291030# Extract style-based overrides1031if style_results:1032style = style_results[0]1033style_name = style.get("Style Category", "")1034keywords = style.get("Keywords", "")1035best_for = style.get("Best For", "")1036effects = style.get("Effects & Animation", "")10371038# Infer layout from style keywords1039if any(kw in keywords.lower() for kw in ["data", "dense", "dashboard", "grid"]):1040layout["Max Width"] = "1400px or full-width"1041layout["Grid"] = "12-column grid for data flexibility"1042spacing["Content Density"] = "High — optimize for information display"1043elif any(kw in keywords.lower() for kw in ["minimal", "simple", "clean", "single"]):1044layout["Max Width"] = "800px (narrow, focused)"1045layout["Layout"] = "Single column, centered"1046spacing["Content Density"] = "Low — focus on clarity"1047else:1048layout["Max Width"] = "1200px (standard)"1049layout["Layout"] = "Full-width sections, centered content"10501051if effects:1052recommendations.append(f"Effects: {effects}")10531054# Extract UX guidelines as recommendations1055for ux in ux_results:1056category = ux.get("Category", "")1057do_text = ux.get("Do", "")1058dont_text = ux.get("Don't", "")1059if do_text:1060recommendations.append(f"{category}: {do_text}")1061if dont_text:1062components.append(f"Avoid: {dont_text}")10631064# Extract landing pattern info for section structure1065if landing_results:1066landing = landing_results[0]1067sections = landing.get("Section Order", "")1068cta_placement = landing.get("Primary CTA Placement", "")1069color_strategy = landing.get("Color Strategy", "")10701071if sections:1072layout["Sections"] = sections1073if cta_placement:1074recommendations.append(f"CTA Placement: {cta_placement}")1075if color_strategy:1076colors["Strategy"] = color_strategy10771078# Add page-type specific defaults if no search results1079if not layout:1080layout["Max Width"] = "1200px"1081layout["Layout"] = "Responsive grid"10821083if not recommendations:1084recommendations = [1085"Refer to MASTER.md for all design rules",1086"Add specific overrides as needed for this page"1087]10881089return {1090"page_type": page_type,1091"layout": layout,1092"spacing": spacing,1093"typography": typography,1094"colors": colors,1095"components": components,1096"unique_components": unique_components,1097"recommendations": recommendations1098}109911001101def _detect_page_type(context: str, style_results: list) -> str:1102"""Detect page type from context and search results."""1103context_lower = context.lower()11041105# Check for common page type patterns1106page_patterns = [1107(["dashboard", "admin", "analytics", "data", "metrics", "stats", "monitor", "overview"], "Dashboard / Data View"),1108(["checkout", "payment", "cart", "purchase", "order", "billing"], "Checkout / Payment"),1109(["settings", "profile", "account", "preferences", "config"], "Settings / Profile"),1110(["landing", "marketing", "homepage", "hero", "home", "promo"], "Landing / Marketing"),1111(["login", "signin", "signup", "register", "auth", "password"], "Authentication"),1112(["pricing", "plans", "subscription", "tiers", "packages"], "Pricing / Plans"),1113(["blog", "article", "post", "news", "content", "story"], "Blog / Article"),1114(["product", "item", "detail", "pdp", "shop", "store"], "Product Detail"),1115(["search", "results", "browse", "filter", "catalog", "list"], "Search Results"),1116(["empty", "404", "error", "not found", "zero"], "Empty State"),1117]11181119for keywords, page_type in page_patterns:1120if any(kw in context_lower for kw in keywords):1121return page_type11221123# Fallback: try to infer from style results1124if style_results:1125style_name = style_results[0].get("Style Category", "").lower()1126best_for = style_results[0].get("Best For", "").lower()11271128if "dashboard" in best_for or "data" in best_for:1129return "Dashboard / Data View"1130elif "landing" in best_for or "marketing" in best_for:1131return "Landing / Marketing"11321133return "General"113411351136# ============ CLI SUPPORT ============1137if __name__ == "__main__":1138import argparse11391140parser = argparse.ArgumentParser(description="Generate Design System")1141parser.add_argument("query", help="Search query (e.g., 'SaaS dashboard')")1142parser.add_argument("--project-name", "-p", type=str, default=None, help="Project name")1143parser.add_argument("--format", "-f", choices=["ascii", "markdown"], default="ascii", help="Output format")11441145args = parser.parse_args()11461147result = generate_design_system(args.query, args.project_name, args.format)1148print(result)1149