""" Module 10 — CSS Generator. Generates CSS variables (design tokens) and targeted fix blocks from an AnalysisResult. No API calls — pure text generation from the parsed data and detected issues. """ from __future__ import annotations from backend.models.analysis import AnalysisResult from backend.utils.config_loader import load_scoring_config def generate(result: AnalysisResult) -> str: """Return a ready-to-paste CSS snippet from an AnalysisResult.""" cfg = load_scoring_config()["thresholds"] lines: list[str] = [] lines.append("/* ============================================================") lines.append(" UICopilot — Generated CSS Fixes") lines.append(f" Overall score: {result.overall_score}/100") lines.append(" Copy relevant blocks into your stylesheet.") lines.append(" ============================================================ */") lines.append("") _append_tokens(lines, result, cfg) _append_typography_fixes(lines, result, cfg) _append_spacing_fixes(lines, result, cfg) _append_button_fixes(lines, result, cfg) _append_form_fixes(lines, result, cfg) _append_table_fixes(lines, result, cfg) return "\n".join(lines) # ── section builders ────────────────────────────────────────────────────────── def _append_tokens(lines: list[str], result: AnalysisResult, cfg: dict) -> None: t_sp = cfg["spacing"] base = t_sp["grid_base_px"] lines.append("/* === Design Tokens === */") lines.append(":root {") lines.append(f" /* Spacing scale ({base}pt grid) */") for mult in (0.5, 1, 1.5, 2, 3, 4, 6): val = int(base * mult) lines.append(f" --space-{val}: {val}px;") lines.append("") # Typography tokens t_ty = cfg["typography"] lines.append(" /* Typography */") lines.append(f" --font-size-body: {t_ty['recommended_body_size_px']}px;") lines.append(f" --line-height-body: {t_ty['line_height_min']};") lines.append(" --font-family-body: system-ui, -apple-system, sans-serif;") lines.append(" --font-family-mono: ui-monospace, 'Cascadia Code', monospace;") lines.append("") # Button tokens t_bt = cfg["buttons"] t_sp2 = cfg["spacing"] lines.append(" /* Buttons */") lines.append(f" --btn-padding-y: {t_sp2['button_min_padding_y_px']}px;") lines.append(f" --btn-padding-x: {t_sp2['button_min_padding_x_px']}px;") lines.append(f" --btn-min-height: {t_bt['min_touch_target_px']}px;") lines.append(" --btn-radius: 6px;") lines.append(" --btn-primary-bg: #005fcc;") lines.append(" --btn-primary-color: #ffffff;") lines.append(" --btn-focus-outline: 2px solid #005fcc;") lines.append(" --btn-focus-offset: 2px;") lines.append("") # Card tokens t_cards = cfg["spacing"] lines.append(" /* Cards */") lines.append(f" --card-padding: {t_cards['card_min_padding_px']}px;") lines.append(" --card-radius: 8px;") lines.append(" --card-border: 1px solid #e5e7eb;") lines.append("}") lines.append("") def _append_typography_fixes(lines: list[str], result: AnalysisResult, cfg: dict) -> None: issues_by_id = {i.rule_id for i in result.issues} if not any(r in issues_by_id for r in ("T1_body_font_size", "T2_font_family_count", "T3_line_height", "T4_heading_hierarchy", "T5_heading_size")): return t = cfg["typography"] lines.append("/* === Typography Fixes === */") if "T1_body_font_size" in issues_by_id or "T3_line_height" in issues_by_id: lines.append("body, p, li, td, th {") lines.append(" font-size: var(--font-size-body);") lines.append(" line-height: var(--line-height-body);") lines.append(" font-family: var(--font-family-body);") lines.append("}") lines.append("") if "T4_heading_hierarchy" in issues_by_id or "T5_heading_size" in issues_by_id: min_h = t["min_heading_size_px"] lines.append(f"h1 {{ font-size: {int(min_h * 1.8)}px; font-weight: 700; }}") lines.append(f"h2 {{ font-size: {int(min_h * 1.4)}px; font-weight: 600; }}") lines.append(f"h3 {{ font-size: {int(min_h * 1.15)}px; font-weight: 600; }}") lines.append("") def _append_spacing_fixes(lines: list[str], result: AnalysisResult, cfg: dict) -> None: issues_by_id = {i.rule_id for i in result.issues} if not any(r in issues_by_id for r in ("S1_button_padding", "S2_card_padding", "S3_off_grid_spacing")): return lines.append("/* === Spacing Fixes === */") if "S2_card_padding" in issues_by_id: lines.append(".card, [class*='card'] {") lines.append(" padding: var(--card-padding);") lines.append(" border-radius: var(--card-radius);") lines.append(" border: var(--card-border);") lines.append("}") lines.append("") def _append_button_fixes(lines: list[str], result: AnalysisResult, cfg: dict) -> None: issues_by_id = {i.rule_id for i in result.issues} if not any(r in issues_by_id for r in ("S1_button_padding", "B1_button_style_count", "B2_border_radius_variance", "B3_focus_style", "B4_touch_target")): return lines.append("/* === Button Fixes === */") lines.append("button, [type='button'], [type='submit'], .btn {") lines.append(" padding: var(--btn-padding-y) var(--btn-padding-x);") lines.append(" min-height: var(--btn-min-height);") lines.append(" border-radius: var(--btn-radius);") lines.append(" font-size: var(--font-size-body);") lines.append(" cursor: pointer;") lines.append("}") lines.append("") if "B3_focus_style" in issues_by_id: lines.append("button:focus-visible,") lines.append("[type='button']:focus-visible,") lines.append("[type='submit']:focus-visible,") lines.append(".btn:focus-visible {") lines.append(" outline: var(--btn-focus-outline);") lines.append(" outline-offset: var(--btn-focus-offset);") lines.append("}") lines.append("") if "B1_button_style_count" in issues_by_id: lines.append(".btn-primary {") lines.append(" background: var(--btn-primary-bg);") lines.append(" color: var(--btn-primary-color);") lines.append(" border: none;") lines.append("}") lines.append("") lines.append(".btn-secondary {") lines.append(" background: transparent;") lines.append(" color: var(--btn-primary-bg);") lines.append(" border: 1px solid var(--btn-primary-bg);") lines.append("}") lines.append("") def _append_form_fixes(lines: list[str], result: AnalysisResult, cfg: dict) -> None: issues_by_id = {i.rule_id for i in result.issues} if not any(r in issues_by_id for r in ("F1_missing_label", "F2_placeholder_as_label", "F3_input_padding")): return t = cfg["forms"] lines.append("/* === Form Fixes === */") lines.append("label {") lines.append(" display: block;") lines.append(" font-size: var(--font-size-body);") lines.append(" font-weight: 500;") lines.append(" margin-bottom: 4px;") lines.append("}") lines.append("") if "F3_input_padding" in issues_by_id: lines.append("input, select, textarea {") lines.append(f" padding: {t['min_input_padding_px']}px 12px;") lines.append(" font-size: var(--font-size-body);") lines.append(" border: 1px solid #d1d5db;") lines.append(" border-radius: 4px;") lines.append(" width: 100%;") lines.append("}") lines.append("") def _append_table_fixes(lines: list[str], result: AnalysisResult, cfg: dict) -> None: issues_by_id = {i.rule_id for i in result.issues} if not any(r in issues_by_id for r in ("TBL1_missing_header", "TBL2_no_zebra", "TBL3_cell_padding")): return t = cfg["tables"] lines.append("/* === Table Fixes === */") lines.append("table { border-collapse: collapse; width: 100%; }") lines.append(f"td, th {{ padding: {t['min_cell_padding_px']}px 12px; text-align: left; }}") if "TBL1_missing_header" in issues_by_id: lines.append("th {") lines.append(" font-weight: 600;") lines.append(" background: #f3f4f6;") lines.append(" border-bottom: 2px solid #e5e7eb;") lines.append("}") lines.append("") if "TBL2_no_zebra" in issues_by_id: lines.append("tr:nth-child(even) { background: #f9fafb; }") lines.append("")