from __future__ import annotations from backend.models.issue import Category, Issue, Severity def analyze(parsed_page: dict, thresholds: dict) -> list[Issue]: issues: list[Issue] = [] t = thresholds["typography"] # T1 — body font size too small body_size = parsed_page.get("body_font_size_px") if body_size is not None and body_size < t["min_body_size_px"]: rec_size = t["recommended_body_size_px"] issues.append(Issue( rule_id="T1_body_font_size", category=Category.TYPOGRAPHY, severity=Severity.HIGH, confidence=1.0, message=f"Body font size ({body_size}px) is below the recommended minimum ({t['min_body_size_px']}px).", recommendation=f"Set body font-size to at least {rec_size}px for comfortable reading.", evidence=f"font-size: {body_size}px", estimated_time="2 minutes", why=( "Text below 14px strains the eyes of users over 40, and mobile browsers " "auto-zoom when inputs use font-size < 16px, breaking your layout. " "WCAG SC 1.4.4 requires content to reflow at 200% zoom without loss of information." ), references=["WCAG 2.1 SC 1.4.4", "Material Design", "iOS HIG"], fix_snippet=f"body, p, li, td {{\n font-size: {rec_size}px;\n}}", )) # T2 — too many font families fonts = parsed_page.get("fonts", []) if len(fonts) > t["max_font_families"]: issues.append(Issue( rule_id="T2_font_family_count", category=Category.TYPOGRAPHY, severity=Severity.MEDIUM, confidence=0.9, message=f"{len(fonts)} font families in use (max recommended: {t['max_font_families']}).", recommendation="Limit to two font families: one for headings, one for body text.", evidence=f"Fonts found: {', '.join(fonts)}", estimated_time="15 minutes", why=( "Multiple font families create visual noise and signal lack of design " "intentionality. Top products use 1–2 fonts at most — variety comes from " "weight, size, and spacing, not typeface. Each extra font also adds a " "network request that slows page load." ), references=["Stripe", "Linear", "Apple"], )) # T3 — line height too tight line_height = parsed_page.get("line_height") if line_height is not None and line_height < t["line_height_min"]: rec_lh = t["line_height_min"] issues.append(Issue( rule_id="T3_line_height", category=Category.TYPOGRAPHY, severity=Severity.MEDIUM, confidence=1.0, message=f"Line height ({line_height}) is below the comfortable minimum ({rec_lh}).", recommendation=f"Set line-height to {rec_lh}–{t['line_height_max']} for readability.", evidence=f"line-height: {line_height}", estimated_time="2 minutes", why=( "Tight line spacing makes paragraphs hard to scan — eyes struggle to track " "from the end of one line to the start of the next. WCAG 1.4.8 recommends " "a line spacing of at least 1.5 within paragraphs. Increasing line-height " "is one of the highest-ROI readability improvements." ), references=["WCAG 2.1 SC 1.4.8", "Notion", "GitHub"], fix_snippet=f"body, p, li {{\n line-height: {rec_lh};\n}}", )) # T4 — no clear heading hierarchy (h1 and h2 too similar in size) headings = parsed_page.get("headings", []) h1_list = [h for h in headings if h["level"] == 1] h2_list = [h for h in headings if h["level"] == 2] if h1_list and h2_list: h1_size = h1_list[0]["font_size_px"] h2_size = h2_list[0]["font_size_px"] if h2_size > 0 and (h1_size / h2_size) < t["min_h1_h2_ratio"]: issues.append(Issue( rule_id="T4_heading_hierarchy", category=Category.VISUAL_HIERARCHY, severity=Severity.HIGH, confidence=0.95, message=( f"H1 ({h1_size}px) and H2 ({h2_size}px) are too close in size — " f"hierarchy ratio {h1_size / h2_size:.2f} < {t['min_h1_h2_ratio']}." ), recommendation="Increase the size difference between heading levels to create clear visual hierarchy.", evidence=f"h1: {h1_size}px, h2: {h2_size}px", estimated_time="5 minutes", why=( "Users scan headings before reading. When H1 and H2 are similar in size, " "there are no visual anchors — users lose their place and can't build a " "mental model of the page structure. A minimum 1.2× ratio between levels " "is the industry-standard starting point." ), references=["Refactoring UI", "Stripe", "Linear"], )) # T5 — heading font size too small for h in headings: if h["level"] <= 2 and h["font_size_px"] < t["min_heading_size_px"]: min_sz = t["min_heading_size_px"] tag = f"h{h['level']}" issues.append(Issue( rule_id="T5_heading_size", category=Category.VISUAL_HIERARCHY, severity=Severity.MEDIUM, confidence=0.9, message=( f"H{h['level']} \"{h['text'][:40]}\" is only {h['font_size_px']}px — " f"too small to establish hierarchy." ), recommendation=f"H1/H2 should be at least {min_sz}px.", evidence=f"h{h['level']}: font-size {h['font_size_px']}px", estimated_time="5 minutes", why=( "Headings smaller than 18px are often indistinguishable from bold body text, " "collapsing the visual hierarchy. The heading is the first thing a user's eye " "should land on in a section — if it doesn't stand out, users must read " "linearly rather than scan." ), references=["Material Design", "Apple HIG", "WCAG 2.1"], fix_snippet=f"{tag} {{\n font-size: {min_sz}px;\n font-weight: 600;\n}}", )) return issues