Merge pull request #184 from Jenser77/feat/design-system-visual-improvements

Improve design system output visuals
This commit is contained in:
Duy /zuey/ 2026-04-03 12:08:19 +07:00 committed by GitHub
commit b7e3af80f6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 308 additions and 140 deletions

View File

@ -18,7 +18,7 @@ CSV_CONFIG = {
"style": { "style": {
"file": "styles.csv", "file": "styles.csv",
"search_cols": ["Style Category", "Keywords", "Best For", "Type", "AI Prompt Keywords"], "search_cols": ["Style Category", "Keywords", "Best For", "Type", "AI Prompt Keywords"],
"output_cols": ["Style Category", "Type", "Keywords", "Primary Colors", "Effects & Animation", "Best For", "Performance", "Accessibility", "Framework Compatibility", "Complexity", "AI Prompt Keywords", "CSS/Technical Keywords", "Implementation Checklist", "Design System Variables"] "output_cols": ["Style Category", "Type", "Keywords", "Primary Colors", "Effects & Animation", "Best For", "Light Mode ✓", "Dark Mode ✓", "Performance", "Accessibility", "Framework Compatibility", "Complexity", "AI Prompt Keywords", "CSS/Technical Keywords", "Implementation Checklist", "Design System Variables"]
}, },
"color": { "color": {
"file": "colors.csv", "file": "colors.csv",
@ -64,6 +64,11 @@ CSV_CONFIG = {
"file": "app-interface.csv", "file": "app-interface.csv",
"search_cols": ["Category", "Issue", "Keywords", "Description"], "search_cols": ["Category", "Issue", "Keywords", "Description"],
"output_cols": ["Category", "Issue", "Platform", "Description", "Do", "Don't", "Code Example Good", "Code Example Bad", "Severity"] "output_cols": ["Category", "Issue", "Platform", "Description", "Do", "Don't", "Code Example Good", "Code Example Bad", "Severity"]
},
"google-fonts": {
"file": "google-fonts.csv",
"search_cols": ["Family", "Category", "Stroke", "Classifications", "Keywords", "Subsets", "Designers"],
"output_cols": ["Family", "Category", "Stroke", "Classifications", "Styles", "Variable Axes", "Subsets", "Designers", "Popularity Rank", "Google Fonts URL"]
} }
} }
@ -186,13 +191,14 @@ def detect_domain(query):
"product": ["saas", "ecommerce", "e-commerce", "fintech", "healthcare", "gaming", "portfolio", "crypto", "dashboard", "fitness", "restaurant", "hotel", "travel", "music", "education", "learning", "legal", "insurance", "medical", "beauty", "pharmacy", "dental", "pet", "dating", "wedding", "recipe", "delivery", "ride", "booking", "calendar", "timer", "tracker", "diary", "note", "chat", "messenger", "crm", "invoice", "parking", "transit", "vpn", "alarm", "weather", "sleep", "meditation", "fasting", "habit", "grocery", "meme", "wardrobe", "plant care", "reading", "flashcard", "puzzle", "trivia", "arcade", "photography", "streaming", "podcast", "newsletter", "marketplace", "freelancer", "coworking", "airline", "museum", "theater", "church", "non-profit", "charity", "kindergarten", "daycare", "senior care", "veterinary", "florist", "bakery", "brewery", "construction", "automotive", "real estate", "logistics", "agriculture", "coding bootcamp"], "product": ["saas", "ecommerce", "e-commerce", "fintech", "healthcare", "gaming", "portfolio", "crypto", "dashboard", "fitness", "restaurant", "hotel", "travel", "music", "education", "learning", "legal", "insurance", "medical", "beauty", "pharmacy", "dental", "pet", "dating", "wedding", "recipe", "delivery", "ride", "booking", "calendar", "timer", "tracker", "diary", "note", "chat", "messenger", "crm", "invoice", "parking", "transit", "vpn", "alarm", "weather", "sleep", "meditation", "fasting", "habit", "grocery", "meme", "wardrobe", "plant care", "reading", "flashcard", "puzzle", "trivia", "arcade", "photography", "streaming", "podcast", "newsletter", "marketplace", "freelancer", "coworking", "airline", "museum", "theater", "church", "non-profit", "charity", "kindergarten", "daycare", "senior care", "veterinary", "florist", "bakery", "brewery", "construction", "automotive", "real estate", "logistics", "agriculture", "coding bootcamp"],
"style": ["style", "design", "ui", "minimalism", "glassmorphism", "neumorphism", "brutalism", "dark mode", "flat", "aurora", "prompt", "css", "implementation", "variable", "checklist", "tailwind"], "style": ["style", "design", "ui", "minimalism", "glassmorphism", "neumorphism", "brutalism", "dark mode", "flat", "aurora", "prompt", "css", "implementation", "variable", "checklist", "tailwind"],
"ux": ["ux", "usability", "accessibility", "wcag", "touch", "scroll", "animation", "keyboard", "navigation", "mobile"], "ux": ["ux", "usability", "accessibility", "wcag", "touch", "scroll", "animation", "keyboard", "navigation", "mobile"],
"typography": ["font", "typography", "heading", "serif", "sans"], "typography": ["font pairing", "typography pairing", "heading font", "body font"],
"google-fonts": ["google font", "font family", "font weight", "font style", "variable font", "noto", "font for", "find font", "font subset", "font language", "monospace font", "serif font", "sans serif font", "display font", "handwriting font", "font", "typography", "serif", "sans"],
"icons": ["icon", "icons", "lucide", "heroicons", "symbol", "glyph", "pictogram", "svg icon"], "icons": ["icon", "icons", "lucide", "heroicons", "symbol", "glyph", "pictogram", "svg icon"],
"react": ["react", "next.js", "nextjs", "suspense", "memo", "usecallback", "useeffect", "rerender", "bundle", "waterfall", "barrel", "dynamic import", "rsc", "server component"], "react": ["react", "next.js", "nextjs", "suspense", "memo", "usecallback", "useeffect", "rerender", "bundle", "waterfall", "barrel", "dynamic import", "rsc", "server component"],
"web": ["aria", "focus", "outline", "semantic", "virtualize", "autocomplete", "form", "input type", "preconnect"] "web": ["aria", "focus", "outline", "semantic", "virtualize", "autocomplete", "form", "input type", "preconnect"]
} }
scores = {domain: sum(1 for kw in keywords if kw in query_lower) for domain, keywords in domain_keywords.items()} scores = {domain: sum(1 for kw in keywords if re.search(r'\b' + re.escape(kw) + r'\b', query_lower)) for domain, keywords in domain_keywords.items()}
best = max(scores, key=scores.get) best = max(scores, key=scores.get)
return best if scores[best] > 0 else "style" return best if scores[best] > 0 else "style"

View File

@ -211,15 +211,25 @@ class DesignSystemGenerator:
"keywords": best_style.get("Keywords", ""), "keywords": best_style.get("Keywords", ""),
"best_for": best_style.get("Best For", ""), "best_for": best_style.get("Best For", ""),
"performance": best_style.get("Performance", ""), "performance": best_style.get("Performance", ""),
"accessibility": best_style.get("Accessibility", "") "accessibility": best_style.get("Accessibility", ""),
"light_mode": best_style.get("Light Mode ✓", ""),
"dark_mode": best_style.get("Dark Mode ✓", ""),
}, },
"colors": { "colors": {
"primary": best_color.get("Primary (Hex)", "#2563EB"), "primary": best_color.get("Primary", "#2563EB"),
"secondary": best_color.get("Secondary (Hex)", "#3B82F6"), "on_primary": best_color.get("On Primary", ""),
"cta": best_color.get("CTA (Hex)", "#F97316"), "secondary": best_color.get("Secondary", "#3B82F6"),
"background": best_color.get("Background (Hex)", "#F8FAFC"), "accent": best_color.get("Accent", "#F97316"),
"text": best_color.get("Text (Hex)", "#1E293B"), "background": best_color.get("Background", "#F8FAFC"),
"notes": best_color.get("Notes", "") "foreground": best_color.get("Foreground", "#1E293B"),
"muted": best_color.get("Muted", ""),
"border": best_color.get("Border", ""),
"destructive": best_color.get("Destructive", ""),
"ring": best_color.get("Ring", ""),
"notes": best_color.get("Notes", ""),
# Keep legacy keys for backward compat in MASTER.md
"cta": best_color.get("Accent", "#F97316"),
"text": best_color.get("Foreground", "#1E293B"),
}, },
"typography": { "typography": {
"heading": best_typography.get("Heading Font", "Inter"), "heading": best_typography.get("Heading Font", "Inter"),
@ -239,8 +249,38 @@ class DesignSystemGenerator:
# ============ OUTPUT FORMATTERS ============ # ============ OUTPUT FORMATTERS ============
BOX_WIDTH = 90 # Wider box for more content BOX_WIDTH = 90 # Wider box for more content
def hex_to_ansi(hex_color: str) -> str:
"""Convert hex color to ANSI True Color swatch (██) with fallback."""
if not hex_color or not hex_color.startswith('#'):
return ""
colorterm = os.environ.get('COLORTERM', '')
if colorterm not in ('truecolor', '24bit'):
return ""
hex_color = hex_color.lstrip('#')
if len(hex_color) != 6:
return ""
r, g, b = int(hex_color[0:2], 16), int(hex_color[2:4], 16), int(hex_color[4:6], 16)
return f"\033[38;2;{r};{g};{b}m██\033[0m "
def ansi_ljust(s: str, width: int) -> str:
"""Like str.ljust but accounts for zero-width ANSI escape sequences."""
import re
visible_len = len(re.sub(r'\033\[[0-9;]*m', '', s))
pad = width - visible_len
return s + (" " * max(0, pad))
def section_header(name: str, width: int) -> str:
"""Create a Unicode section separator: ├─── NAME ───...┤"""
label = f"─── {name} "
fill = "" * (width - len(label) - 1)
return f"{label}{fill}"
def format_ascii_box(design_system: dict) -> str: def format_ascii_box(design_system: dict) -> str:
"""Format design system as ASCII box with emojis (MCP-style).""" """Format design system as Unicode box with ANSI color swatches."""
project = design_system.get("project_name", "PROJECT") project = design_system.get("project_name", "PROJECT")
pattern = design_system.get("pattern", {}) pattern = design_system.get("pattern", {})
style = design_system.get("style", {}) style = design_system.get("style", {})
@ -275,77 +315,93 @@ def format_ascii_box(design_system: dict) -> str:
lines = [] lines = []
w = BOX_WIDTH - 1 w = BOX_WIDTH - 1
lines.append("+" + "-" * w + "+") # Header with double-line box
lines.append(f"| TARGET: {project} - RECOMMENDED DESIGN SYSTEM".ljust(BOX_WIDTH) + "|") lines.append("" + "" * w + "")
lines.append("+" + "-" * w + "+") lines.append(ansi_ljust(f"║ TARGET: {project} - RECOMMENDED DESIGN SYSTEM", BOX_WIDTH) + "")
lines.append("|" + " " * BOX_WIDTH + "|") lines.append("" + "" * w + "")
lines.append("" + "" * w + "")
# Pattern section # Pattern section
lines.append(f"| PATTERN: {pattern.get('name', '')}".ljust(BOX_WIDTH) + "|") lines.append(section_header("PATTERN", BOX_WIDTH + 1))
lines.append(f"│ Name: {pattern.get('name', '')}".ljust(BOX_WIDTH) + "")
if pattern.get('conversion'): if pattern.get('conversion'):
lines.append(f"| Conversion: {pattern.get('conversion', '')}".ljust(BOX_WIDTH) + "|") lines.append(f" Conversion: {pattern.get('conversion', '')}".ljust(BOX_WIDTH) + "")
if pattern.get('cta_placement'): if pattern.get('cta_placement'):
lines.append(f"| CTA: {pattern.get('cta_placement', '')}".ljust(BOX_WIDTH) + "|") lines.append(f" CTA: {pattern.get('cta_placement', '')}".ljust(BOX_WIDTH) + "")
lines.append("| Sections:".ljust(BOX_WIDTH) + "|") lines.append(" Sections:".ljust(BOX_WIDTH) + "")
for i, section in enumerate(sections, 1): for i, section in enumerate(sections, 1):
lines.append(f"| {i}. {section}".ljust(BOX_WIDTH) + "|") lines.append(f"{i}. {section}".ljust(BOX_WIDTH) + "")
lines.append("|" + " " * BOX_WIDTH + "|")
# Style section # Style section
lines.append(f"| STYLE: {style.get('name', '')}".ljust(BOX_WIDTH) + "|") lines.append(section_header("STYLE", BOX_WIDTH + 1))
lines.append(f"│ Name: {style.get('name', '')}".ljust(BOX_WIDTH) + "")
light = style.get("light_mode", "")
dark = style.get("dark_mode", "")
if light or dark:
lines.append(f"│ Mode Support: Light {light} Dark {dark}".ljust(BOX_WIDTH) + "")
if style.get("keywords"): if style.get("keywords"):
for line in wrap_text(f"Keywords: {style.get('keywords', '')}", "| ", BOX_WIDTH): for line in wrap_text(f"Keywords: {style.get('keywords', '')}", " ", BOX_WIDTH):
lines.append(line.ljust(BOX_WIDTH) + "|") lines.append(line.ljust(BOX_WIDTH) + "")
if style.get("best_for"): if style.get("best_for"):
for line in wrap_text(f"Best For: {style.get('best_for', '')}", "| ", BOX_WIDTH): for line in wrap_text(f"Best For: {style.get('best_for', '')}", " ", BOX_WIDTH):
lines.append(line.ljust(BOX_WIDTH) + "|") lines.append(line.ljust(BOX_WIDTH) + "")
if style.get("performance") or style.get("accessibility"): if style.get("performance") or style.get("accessibility"):
perf_a11y = f"Performance: {style.get('performance', '')} | Accessibility: {style.get('accessibility', '')}" perf_a11y = f"Performance: {style.get('performance', '')} | Accessibility: {style.get('accessibility', '')}"
lines.append(f"| {perf_a11y}".ljust(BOX_WIDTH) + "|") lines.append(f"{perf_a11y}".ljust(BOX_WIDTH) + "")
lines.append("|" + " " * BOX_WIDTH + "|")
# Colors section # Colors section (extended palette with ANSI swatches)
lines.append("| COLORS:".ljust(BOX_WIDTH) + "|") lines.append(section_header("COLORS", BOX_WIDTH + 1))
lines.append(f"| Primary: {colors.get('primary', '')}".ljust(BOX_WIDTH) + "|") color_entries = [
lines.append(f"| Secondary: {colors.get('secondary', '')}".ljust(BOX_WIDTH) + "|") ("Primary", "primary", "--color-primary"),
lines.append(f"| CTA: {colors.get('cta', '')}".ljust(BOX_WIDTH) + "|") ("On Primary", "on_primary", "--color-on-primary"),
lines.append(f"| Background: {colors.get('background', '')}".ljust(BOX_WIDTH) + "|") ("Secondary", "secondary", "--color-secondary"),
lines.append(f"| Text: {colors.get('text', '')}".ljust(BOX_WIDTH) + "|") ("Accent/CTA", "accent", "--color-accent"),
("Background", "background", "--color-background"),
("Foreground", "foreground", "--color-foreground"),
("Muted", "muted", "--color-muted"),
("Border", "border", "--color-border"),
("Destructive", "destructive", "--color-destructive"),
("Ring", "ring", "--color-ring"),
]
for label, key, css_var in color_entries:
hex_val = colors.get(key, "")
if not hex_val:
continue
swatch = hex_to_ansi(hex_val)
content = f"{swatch}{label + ':':14s} {hex_val:10s} ({css_var})"
lines.append(ansi_ljust(content, BOX_WIDTH) + "")
if colors.get("notes"): if colors.get("notes"):
for line in wrap_text(f"Notes: {colors.get('notes', '')}", "| ", BOX_WIDTH): for line in wrap_text(f"Notes: {colors.get('notes', '')}", "", BOX_WIDTH):
lines.append(line.ljust(BOX_WIDTH) + "|") lines.append(line.ljust(BOX_WIDTH) + "")
lines.append("|" + " " * BOX_WIDTH + "|")
# Typography section # Typography section
lines.append(f"| TYPOGRAPHY: {typography.get('heading', '')} / {typography.get('body', '')}".ljust(BOX_WIDTH) + "|") lines.append(section_header("TYPOGRAPHY", BOX_WIDTH + 1))
lines.append(f"{typography.get('heading', '')} / {typography.get('body', '')}".ljust(BOX_WIDTH) + "")
if typography.get("mood"): if typography.get("mood"):
for line in wrap_text(f"Mood: {typography.get('mood', '')}", "| ", BOX_WIDTH): for line in wrap_text(f"Mood: {typography.get('mood', '')}", " ", BOX_WIDTH):
lines.append(line.ljust(BOX_WIDTH) + "|") lines.append(line.ljust(BOX_WIDTH) + "")
if typography.get("best_for"): if typography.get("best_for"):
for line in wrap_text(f"Best For: {typography.get('best_for', '')}", "| ", BOX_WIDTH): for line in wrap_text(f"Best For: {typography.get('best_for', '')}", " ", BOX_WIDTH):
lines.append(line.ljust(BOX_WIDTH) + "|") lines.append(line.ljust(BOX_WIDTH) + "")
if typography.get("google_fonts_url"): if typography.get("google_fonts_url"):
lines.append(f"| Google Fonts: {typography.get('google_fonts_url', '')}".ljust(BOX_WIDTH) + "|") lines.append(f" Google Fonts: {typography.get('google_fonts_url', '')}".ljust(BOX_WIDTH) + "")
if typography.get("css_import"): if typography.get("css_import"):
lines.append(f"| CSS Import: {typography.get('css_import', '')[:70]}...".ljust(BOX_WIDTH) + "|") lines.append(f"│ CSS Import: {typography.get('css_import', '')[:70]}...".ljust(BOX_WIDTH) + "")
lines.append("|" + " " * BOX_WIDTH + "|")
# Key Effects section # Key Effects section
if effects: if effects:
lines.append("| KEY EFFECTS:".ljust(BOX_WIDTH) + "|") lines.append(section_header("KEY EFFECTS", BOX_WIDTH + 1))
for line in wrap_text(effects, "| ", BOX_WIDTH): for line in wrap_text(effects, "", BOX_WIDTH):
lines.append(line.ljust(BOX_WIDTH) + "|") lines.append(line.ljust(BOX_WIDTH) + "")
lines.append("|" + " " * BOX_WIDTH + "|")
# Anti-patterns section # Anti-patterns section
if anti_patterns: if anti_patterns:
lines.append("| AVOID (Anti-patterns):".ljust(BOX_WIDTH) + "|") lines.append(section_header("AVOID", BOX_WIDTH + 1))
for line in wrap_text(anti_patterns, "| ", BOX_WIDTH): for line in wrap_text(anti_patterns, "", BOX_WIDTH):
lines.append(line.ljust(BOX_WIDTH) + "|") lines.append(line.ljust(BOX_WIDTH) + "")
lines.append("|" + " " * BOX_WIDTH + "|")
# Pre-Delivery Checklist section # Pre-Delivery Checklist section
lines.append("| PRE-DELIVERY CHECKLIST:".ljust(BOX_WIDTH) + "|") lines.append(section_header("PRE-DELIVERY CHECKLIST", BOX_WIDTH + 1))
checklist_items = [ checklist_items = [
"[ ] No emojis as icons (use SVG: Heroicons/Lucide)", "[ ] No emojis as icons (use SVG: Heroicons/Lucide)",
"[ ] cursor-pointer on all clickable elements", "[ ] cursor-pointer on all clickable elements",
@ -356,10 +412,9 @@ def format_ascii_box(design_system: dict) -> str:
"[ ] Responsive: 375px, 768px, 1024px, 1440px" "[ ] Responsive: 375px, 768px, 1024px, 1440px"
] ]
for item in checklist_items: for item in checklist_items:
lines.append(f"| {item}".ljust(BOX_WIDTH) + "|") lines.append(f"{item}".ljust(BOX_WIDTH) + "")
lines.append("|" + " " * BOX_WIDTH + "|")
lines.append("+" + "-" * w + "+") lines.append("" + "" * w + "")
return "\n".join(lines) return "\n".join(lines)
@ -393,6 +448,10 @@ def format_markdown(design_system: dict) -> str:
# Style section # Style section
lines.append("### Style") lines.append("### Style")
lines.append(f"- **Name:** {style.get('name', '')}") lines.append(f"- **Name:** {style.get('name', '')}")
light = style.get("light_mode", "")
dark = style.get("dark_mode", "")
if light or dark:
lines.append(f"- **Mode Support:** Light {light} | Dark {dark}")
if style.get('keywords'): if style.get('keywords'):
lines.append(f"- **Keywords:** {style.get('keywords', '')}") lines.append(f"- **Keywords:** {style.get('keywords', '')}")
if style.get('best_for'): if style.get('best_for'):
@ -401,15 +460,26 @@ def format_markdown(design_system: dict) -> str:
lines.append(f"- **Performance:** {style.get('performance', '')} | **Accessibility:** {style.get('accessibility', '')}") lines.append(f"- **Performance:** {style.get('performance', '')} | **Accessibility:** {style.get('accessibility', '')}")
lines.append("") lines.append("")
# Colors section # Colors section (extended palette)
lines.append("### Colors") lines.append("### Colors")
lines.append(f"| Role | Hex |") lines.append("| Role | Hex | CSS Variable |")
lines.append(f"|------|-----|") lines.append("|------|-----|--------------|")
lines.append(f"| Primary | {colors.get('primary', '')} |") md_color_entries = [
lines.append(f"| Secondary | {colors.get('secondary', '')} |") ("Primary", "primary", "--color-primary"),
lines.append(f"| CTA | {colors.get('cta', '')} |") ("On Primary", "on_primary", "--color-on-primary"),
lines.append(f"| Background | {colors.get('background', '')} |") ("Secondary", "secondary", "--color-secondary"),
lines.append(f"| Text | {colors.get('text', '')} |") ("Accent/CTA", "accent", "--color-accent"),
("Background", "background", "--color-background"),
("Foreground", "foreground", "--color-foreground"),
("Muted", "muted", "--color-muted"),
("Border", "border", "--color-border"),
("Destructive", "destructive", "--color-destructive"),
("Ring", "ring", "--color-ring"),
]
for label, key, css_var in md_color_entries:
hex_val = colors.get(key, "")
if hex_val:
lines.append(f"| {label} | `{hex_val}` | `{css_var}` |")
if colors.get("notes"): if colors.get("notes"):
lines.append(f"\n*Notes: {colors.get('notes', '')}*") lines.append(f"\n*Notes: {colors.get('notes', '')}*")
lines.append("") lines.append("")
@ -578,11 +648,22 @@ def format_master_md(design_system: dict) -> str:
lines.append("") lines.append("")
lines.append("| Role | Hex | CSS Variable |") lines.append("| Role | Hex | CSS Variable |")
lines.append("|------|-----|--------------|") lines.append("|------|-----|--------------|")
lines.append(f"| Primary | `{colors.get('primary', '#2563EB')}` | `--color-primary` |") master_color_entries = [
lines.append(f"| Secondary | `{colors.get('secondary', '#3B82F6')}` | `--color-secondary` |") ("Primary", "primary", "--color-primary"),
lines.append(f"| CTA/Accent | `{colors.get('cta', '#F97316')}` | `--color-cta` |") ("On Primary", "on_primary", "--color-on-primary"),
lines.append(f"| Background | `{colors.get('background', '#F8FAFC')}` | `--color-background` |") ("Secondary", "secondary", "--color-secondary"),
lines.append(f"| Text | `{colors.get('text', '#1E293B')}` | `--color-text` |") ("Accent/CTA", "accent", "--color-accent"),
("Background", "background", "--color-background"),
("Foreground", "foreground", "--color-foreground"),
("Muted", "muted", "--color-muted"),
("Border", "border", "--color-border"),
("Destructive", "destructive", "--color-destructive"),
("Ring", "ring", "--color-ring"),
]
for label, key, css_var in master_color_entries:
hex_val = colors.get(key, "")
if hex_val:
lines.append(f"| {label} | `{hex_val}` | `{css_var}` |")
lines.append("") lines.append("")
if colors.get("notes"): if colors.get("notes"):
lines.append(f"**Color Notes:** {colors.get('notes', '')}") lines.append(f"**Color Notes:** {colors.get('notes', '')}")

View File

@ -18,7 +18,7 @@ CSV_CONFIG = {
"style": { "style": {
"file": "styles.csv", "file": "styles.csv",
"search_cols": ["Style Category", "Keywords", "Best For", "Type", "AI Prompt Keywords"], "search_cols": ["Style Category", "Keywords", "Best For", "Type", "AI Prompt Keywords"],
"output_cols": ["Style Category", "Type", "Keywords", "Primary Colors", "Effects & Animation", "Best For", "Performance", "Accessibility", "Framework Compatibility", "Complexity", "AI Prompt Keywords", "CSS/Technical Keywords", "Implementation Checklist", "Design System Variables"] "output_cols": ["Style Category", "Type", "Keywords", "Primary Colors", "Effects & Animation", "Best For", "Light Mode ✓", "Dark Mode ✓", "Performance", "Accessibility", "Framework Compatibility", "Complexity", "AI Prompt Keywords", "CSS/Technical Keywords", "Implementation Checklist", "Design System Variables"]
}, },
"color": { "color": {
"file": "colors.csv", "file": "colors.csv",

View File

@ -211,15 +211,25 @@ class DesignSystemGenerator:
"keywords": best_style.get("Keywords", ""), "keywords": best_style.get("Keywords", ""),
"best_for": best_style.get("Best For", ""), "best_for": best_style.get("Best For", ""),
"performance": best_style.get("Performance", ""), "performance": best_style.get("Performance", ""),
"accessibility": best_style.get("Accessibility", "") "accessibility": best_style.get("Accessibility", ""),
"light_mode": best_style.get("Light Mode ✓", ""),
"dark_mode": best_style.get("Dark Mode ✓", ""),
}, },
"colors": { "colors": {
"primary": best_color.get("Primary (Hex)", "#2563EB"), "primary": best_color.get("Primary", "#2563EB"),
"secondary": best_color.get("Secondary (Hex)", "#3B82F6"), "on_primary": best_color.get("On Primary", ""),
"cta": best_color.get("CTA (Hex)", "#F97316"), "secondary": best_color.get("Secondary", "#3B82F6"),
"background": best_color.get("Background (Hex)", "#F8FAFC"), "accent": best_color.get("Accent", "#F97316"),
"text": best_color.get("Text (Hex)", "#1E293B"), "background": best_color.get("Background", "#F8FAFC"),
"notes": best_color.get("Notes", "") "foreground": best_color.get("Foreground", "#1E293B"),
"muted": best_color.get("Muted", ""),
"border": best_color.get("Border", ""),
"destructive": best_color.get("Destructive", ""),
"ring": best_color.get("Ring", ""),
"notes": best_color.get("Notes", ""),
# Keep legacy keys for backward compat in MASTER.md
"cta": best_color.get("Accent", "#F97316"),
"text": best_color.get("Foreground", "#1E293B"),
}, },
"typography": { "typography": {
"heading": best_typography.get("Heading Font", "Inter"), "heading": best_typography.get("Heading Font", "Inter"),
@ -239,8 +249,38 @@ class DesignSystemGenerator:
# ============ OUTPUT FORMATTERS ============ # ============ OUTPUT FORMATTERS ============
BOX_WIDTH = 90 # Wider box for more content BOX_WIDTH = 90 # Wider box for more content
def hex_to_ansi(hex_color: str) -> str:
"""Convert hex color to ANSI True Color swatch (██) with fallback."""
if not hex_color or not hex_color.startswith('#'):
return ""
colorterm = os.environ.get('COLORTERM', '')
if colorterm not in ('truecolor', '24bit'):
return ""
hex_color = hex_color.lstrip('#')
if len(hex_color) != 6:
return ""
r, g, b = int(hex_color[0:2], 16), int(hex_color[2:4], 16), int(hex_color[4:6], 16)
return f"\033[38;2;{r};{g};{b}m██\033[0m "
def ansi_ljust(s: str, width: int) -> str:
"""Like str.ljust but accounts for zero-width ANSI escape sequences."""
import re
visible_len = len(re.sub(r'\033\[[0-9;]*m', '', s))
pad = width - visible_len
return s + (" " * max(0, pad))
def section_header(name: str, width: int) -> str:
"""Create a Unicode section separator: ├─── NAME ───...┤"""
label = f"─── {name} "
fill = "" * (width - len(label) - 1)
return f"{label}{fill}"
def format_ascii_box(design_system: dict) -> str: def format_ascii_box(design_system: dict) -> str:
"""Format design system as ASCII box with emojis (MCP-style).""" """Format design system as Unicode box with ANSI color swatches."""
project = design_system.get("project_name", "PROJECT") project = design_system.get("project_name", "PROJECT")
pattern = design_system.get("pattern", {}) pattern = design_system.get("pattern", {})
style = design_system.get("style", {}) style = design_system.get("style", {})
@ -275,77 +315,93 @@ def format_ascii_box(design_system: dict) -> str:
lines = [] lines = []
w = BOX_WIDTH - 1 w = BOX_WIDTH - 1
lines.append("+" + "-" * w + "+") # Header with double-line box
lines.append(f"| TARGET: {project} - RECOMMENDED DESIGN SYSTEM".ljust(BOX_WIDTH) + "|") lines.append("" + "" * w + "")
lines.append("+" + "-" * w + "+") lines.append(ansi_ljust(f"║ TARGET: {project} - RECOMMENDED DESIGN SYSTEM", BOX_WIDTH) + "")
lines.append("|" + " " * BOX_WIDTH + "|") lines.append("" + "" * w + "")
lines.append("" + "" * w + "")
# Pattern section # Pattern section
lines.append(f"| PATTERN: {pattern.get('name', '')}".ljust(BOX_WIDTH) + "|") lines.append(section_header("PATTERN", BOX_WIDTH + 1))
lines.append(f"│ Name: {pattern.get('name', '')}".ljust(BOX_WIDTH) + "")
if pattern.get('conversion'): if pattern.get('conversion'):
lines.append(f"| Conversion: {pattern.get('conversion', '')}".ljust(BOX_WIDTH) + "|") lines.append(f" Conversion: {pattern.get('conversion', '')}".ljust(BOX_WIDTH) + "")
if pattern.get('cta_placement'): if pattern.get('cta_placement'):
lines.append(f"| CTA: {pattern.get('cta_placement', '')}".ljust(BOX_WIDTH) + "|") lines.append(f" CTA: {pattern.get('cta_placement', '')}".ljust(BOX_WIDTH) + "")
lines.append("| Sections:".ljust(BOX_WIDTH) + "|") lines.append(" Sections:".ljust(BOX_WIDTH) + "")
for i, section in enumerate(sections, 1): for i, section in enumerate(sections, 1):
lines.append(f"| {i}. {section}".ljust(BOX_WIDTH) + "|") lines.append(f"{i}. {section}".ljust(BOX_WIDTH) + "")
lines.append("|" + " " * BOX_WIDTH + "|")
# Style section # Style section
lines.append(f"| STYLE: {style.get('name', '')}".ljust(BOX_WIDTH) + "|") lines.append(section_header("STYLE", BOX_WIDTH + 1))
lines.append(f"│ Name: {style.get('name', '')}".ljust(BOX_WIDTH) + "")
light = style.get("light_mode", "")
dark = style.get("dark_mode", "")
if light or dark:
lines.append(f"│ Mode Support: Light {light} Dark {dark}".ljust(BOX_WIDTH) + "")
if style.get("keywords"): if style.get("keywords"):
for line in wrap_text(f"Keywords: {style.get('keywords', '')}", "| ", BOX_WIDTH): for line in wrap_text(f"Keywords: {style.get('keywords', '')}", " ", BOX_WIDTH):
lines.append(line.ljust(BOX_WIDTH) + "|") lines.append(line.ljust(BOX_WIDTH) + "")
if style.get("best_for"): if style.get("best_for"):
for line in wrap_text(f"Best For: {style.get('best_for', '')}", "| ", BOX_WIDTH): for line in wrap_text(f"Best For: {style.get('best_for', '')}", " ", BOX_WIDTH):
lines.append(line.ljust(BOX_WIDTH) + "|") lines.append(line.ljust(BOX_WIDTH) + "")
if style.get("performance") or style.get("accessibility"): if style.get("performance") or style.get("accessibility"):
perf_a11y = f"Performance: {style.get('performance', '')} | Accessibility: {style.get('accessibility', '')}" perf_a11y = f"Performance: {style.get('performance', '')} | Accessibility: {style.get('accessibility', '')}"
lines.append(f"| {perf_a11y}".ljust(BOX_WIDTH) + "|") lines.append(f"{perf_a11y}".ljust(BOX_WIDTH) + "")
lines.append("|" + " " * BOX_WIDTH + "|")
# Colors section # Colors section (extended palette with ANSI swatches)
lines.append("| COLORS:".ljust(BOX_WIDTH) + "|") lines.append(section_header("COLORS", BOX_WIDTH + 1))
lines.append(f"| Primary: {colors.get('primary', '')}".ljust(BOX_WIDTH) + "|") color_entries = [
lines.append(f"| Secondary: {colors.get('secondary', '')}".ljust(BOX_WIDTH) + "|") ("Primary", "primary", "--color-primary"),
lines.append(f"| CTA: {colors.get('cta', '')}".ljust(BOX_WIDTH) + "|") ("On Primary", "on_primary", "--color-on-primary"),
lines.append(f"| Background: {colors.get('background', '')}".ljust(BOX_WIDTH) + "|") ("Secondary", "secondary", "--color-secondary"),
lines.append(f"| Text: {colors.get('text', '')}".ljust(BOX_WIDTH) + "|") ("Accent/CTA", "accent", "--color-accent"),
("Background", "background", "--color-background"),
("Foreground", "foreground", "--color-foreground"),
("Muted", "muted", "--color-muted"),
("Border", "border", "--color-border"),
("Destructive", "destructive", "--color-destructive"),
("Ring", "ring", "--color-ring"),
]
for label, key, css_var in color_entries:
hex_val = colors.get(key, "")
if not hex_val:
continue
swatch = hex_to_ansi(hex_val)
content = f"{swatch}{label + ':':14s} {hex_val:10s} ({css_var})"
lines.append(ansi_ljust(content, BOX_WIDTH) + "")
if colors.get("notes"): if colors.get("notes"):
for line in wrap_text(f"Notes: {colors.get('notes', '')}", "| ", BOX_WIDTH): for line in wrap_text(f"Notes: {colors.get('notes', '')}", "", BOX_WIDTH):
lines.append(line.ljust(BOX_WIDTH) + "|") lines.append(line.ljust(BOX_WIDTH) + "")
lines.append("|" + " " * BOX_WIDTH + "|")
# Typography section # Typography section
lines.append(f"| TYPOGRAPHY: {typography.get('heading', '')} / {typography.get('body', '')}".ljust(BOX_WIDTH) + "|") lines.append(section_header("TYPOGRAPHY", BOX_WIDTH + 1))
lines.append(f"{typography.get('heading', '')} / {typography.get('body', '')}".ljust(BOX_WIDTH) + "")
if typography.get("mood"): if typography.get("mood"):
for line in wrap_text(f"Mood: {typography.get('mood', '')}", "| ", BOX_WIDTH): for line in wrap_text(f"Mood: {typography.get('mood', '')}", " ", BOX_WIDTH):
lines.append(line.ljust(BOX_WIDTH) + "|") lines.append(line.ljust(BOX_WIDTH) + "")
if typography.get("best_for"): if typography.get("best_for"):
for line in wrap_text(f"Best For: {typography.get('best_for', '')}", "| ", BOX_WIDTH): for line in wrap_text(f"Best For: {typography.get('best_for', '')}", " ", BOX_WIDTH):
lines.append(line.ljust(BOX_WIDTH) + "|") lines.append(line.ljust(BOX_WIDTH) + "")
if typography.get("google_fonts_url"): if typography.get("google_fonts_url"):
lines.append(f"| Google Fonts: {typography.get('google_fonts_url', '')}".ljust(BOX_WIDTH) + "|") lines.append(f" Google Fonts: {typography.get('google_fonts_url', '')}".ljust(BOX_WIDTH) + "")
if typography.get("css_import"): if typography.get("css_import"):
lines.append(f"| CSS Import: {typography.get('css_import', '')[:70]}...".ljust(BOX_WIDTH) + "|") lines.append(f"│ CSS Import: {typography.get('css_import', '')[:70]}...".ljust(BOX_WIDTH) + "")
lines.append("|" + " " * BOX_WIDTH + "|")
# Key Effects section # Key Effects section
if effects: if effects:
lines.append("| KEY EFFECTS:".ljust(BOX_WIDTH) + "|") lines.append(section_header("KEY EFFECTS", BOX_WIDTH + 1))
for line in wrap_text(effects, "| ", BOX_WIDTH): for line in wrap_text(effects, "", BOX_WIDTH):
lines.append(line.ljust(BOX_WIDTH) + "|") lines.append(line.ljust(BOX_WIDTH) + "")
lines.append("|" + " " * BOX_WIDTH + "|")
# Anti-patterns section # Anti-patterns section
if anti_patterns: if anti_patterns:
lines.append("| AVOID (Anti-patterns):".ljust(BOX_WIDTH) + "|") lines.append(section_header("AVOID", BOX_WIDTH + 1))
for line in wrap_text(anti_patterns, "| ", BOX_WIDTH): for line in wrap_text(anti_patterns, "", BOX_WIDTH):
lines.append(line.ljust(BOX_WIDTH) + "|") lines.append(line.ljust(BOX_WIDTH) + "")
lines.append("|" + " " * BOX_WIDTH + "|")
# Pre-Delivery Checklist section # Pre-Delivery Checklist section
lines.append("| PRE-DELIVERY CHECKLIST:".ljust(BOX_WIDTH) + "|") lines.append(section_header("PRE-DELIVERY CHECKLIST", BOX_WIDTH + 1))
checklist_items = [ checklist_items = [
"[ ] No emojis as icons (use SVG: Heroicons/Lucide)", "[ ] No emojis as icons (use SVG: Heroicons/Lucide)",
"[ ] cursor-pointer on all clickable elements", "[ ] cursor-pointer on all clickable elements",
@ -356,10 +412,9 @@ def format_ascii_box(design_system: dict) -> str:
"[ ] Responsive: 375px, 768px, 1024px, 1440px" "[ ] Responsive: 375px, 768px, 1024px, 1440px"
] ]
for item in checklist_items: for item in checklist_items:
lines.append(f"| {item}".ljust(BOX_WIDTH) + "|") lines.append(f"{item}".ljust(BOX_WIDTH) + "")
lines.append("|" + " " * BOX_WIDTH + "|")
lines.append("+" + "-" * w + "+") lines.append("" + "" * w + "")
return "\n".join(lines) return "\n".join(lines)
@ -393,6 +448,10 @@ def format_markdown(design_system: dict) -> str:
# Style section # Style section
lines.append("### Style") lines.append("### Style")
lines.append(f"- **Name:** {style.get('name', '')}") lines.append(f"- **Name:** {style.get('name', '')}")
light = style.get("light_mode", "")
dark = style.get("dark_mode", "")
if light or dark:
lines.append(f"- **Mode Support:** Light {light} | Dark {dark}")
if style.get('keywords'): if style.get('keywords'):
lines.append(f"- **Keywords:** {style.get('keywords', '')}") lines.append(f"- **Keywords:** {style.get('keywords', '')}")
if style.get('best_for'): if style.get('best_for'):
@ -401,15 +460,26 @@ def format_markdown(design_system: dict) -> str:
lines.append(f"- **Performance:** {style.get('performance', '')} | **Accessibility:** {style.get('accessibility', '')}") lines.append(f"- **Performance:** {style.get('performance', '')} | **Accessibility:** {style.get('accessibility', '')}")
lines.append("") lines.append("")
# Colors section # Colors section (extended palette)
lines.append("### Colors") lines.append("### Colors")
lines.append(f"| Role | Hex |") lines.append("| Role | Hex | CSS Variable |")
lines.append(f"|------|-----|") lines.append("|------|-----|--------------|")
lines.append(f"| Primary | {colors.get('primary', '')} |") md_color_entries = [
lines.append(f"| Secondary | {colors.get('secondary', '')} |") ("Primary", "primary", "--color-primary"),
lines.append(f"| CTA | {colors.get('cta', '')} |") ("On Primary", "on_primary", "--color-on-primary"),
lines.append(f"| Background | {colors.get('background', '')} |") ("Secondary", "secondary", "--color-secondary"),
lines.append(f"| Text | {colors.get('text', '')} |") ("Accent/CTA", "accent", "--color-accent"),
("Background", "background", "--color-background"),
("Foreground", "foreground", "--color-foreground"),
("Muted", "muted", "--color-muted"),
("Border", "border", "--color-border"),
("Destructive", "destructive", "--color-destructive"),
("Ring", "ring", "--color-ring"),
]
for label, key, css_var in md_color_entries:
hex_val = colors.get(key, "")
if hex_val:
lines.append(f"| {label} | `{hex_val}` | `{css_var}` |")
if colors.get("notes"): if colors.get("notes"):
lines.append(f"\n*Notes: {colors.get('notes', '')}*") lines.append(f"\n*Notes: {colors.get('notes', '')}*")
lines.append("") lines.append("")
@ -578,11 +648,22 @@ def format_master_md(design_system: dict) -> str:
lines.append("") lines.append("")
lines.append("| Role | Hex | CSS Variable |") lines.append("| Role | Hex | CSS Variable |")
lines.append("|------|-----|--------------|") lines.append("|------|-----|--------------|")
lines.append(f"| Primary | `{colors.get('primary', '#2563EB')}` | `--color-primary` |") master_color_entries = [
lines.append(f"| Secondary | `{colors.get('secondary', '#3B82F6')}` | `--color-secondary` |") ("Primary", "primary", "--color-primary"),
lines.append(f"| CTA/Accent | `{colors.get('cta', '#F97316')}` | `--color-cta` |") ("On Primary", "on_primary", "--color-on-primary"),
lines.append(f"| Background | `{colors.get('background', '#F8FAFC')}` | `--color-background` |") ("Secondary", "secondary", "--color-secondary"),
lines.append(f"| Text | `{colors.get('text', '#1E293B')}` | `--color-text` |") ("Accent/CTA", "accent", "--color-accent"),
("Background", "background", "--color-background"),
("Foreground", "foreground", "--color-foreground"),
("Muted", "muted", "--color-muted"),
("Border", "border", "--color-border"),
("Destructive", "destructive", "--color-destructive"),
("Ring", "ring", "--color-ring"),
]
for label, key, css_var in master_color_entries:
hex_val = colors.get(key, "")
if hex_val:
lines.append(f"| {label} | `{hex_val}` | `{css_var}` |")
lines.append("") lines.append("")
if colors.get("notes"): if colors.get("notes"):
lines.append(f"**Color Notes:** {colors.get('notes', '')}") lines.append(f"**Color Notes:** {colors.get('notes', '')}")