ui-copilot / backend /rules /typography_rules.py
ksri77's picture
chore: deploy from CI
8c816e3 verified
Raw
History Blame Contribute Delete
6.55 kB
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