Spaces:
Running
Running
| 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 | |