ui-copilot / backend /rules /spacing_rules.py
ksri77's picture
chore: deploy from CI
8c816e3 verified
Raw
History Blame Contribute Delete
5.07 kB
from __future__ import annotations
from backend.models.issue import Category, Issue, Severity
def _on_grid(value: float, base: float, tolerance: float = 0.5) -> bool:
if value == 0:
return True
return (value % base) <= tolerance or (base - value % base) <= tolerance
def analyze(parsed_page: dict, thresholds: dict) -> list[Issue]:
issues: list[Issue] = []
t = thresholds["spacing"]
# S1 β€” button padding too small
for btn in parsed_page.get("buttons", []):
too_short = btn["padding_top_px"] < t["button_min_padding_y_px"]
too_narrow = btn["padding_left_px"] < t["button_min_padding_x_px"]
if too_short or too_narrow:
py = t["button_min_padding_y_px"]
px = t["button_min_padding_x_px"]
issues.append(Issue(
rule_id="S1_button_padding",
category=Category.SPACING,
severity=Severity.HIGH,
confidence=1.0,
message=(
f"Button \"{btn['text']}\" has insufficient padding "
f"({btn['padding_top_px']}px Γ— {btn['padding_left_px']}px)."
),
recommendation=(
f"Set button padding to at least {py}px vertical "
f"and {px}px horizontal."
),
evidence=(
f"padding-top:{btn['padding_top_px']}px; "
f"padding-left:{btn['padding_left_px']}px"
),
estimated_time="5 minutes",
why=(
"Cramped buttons are harder to click on touch devices and feel cheap. "
"WCAG 2.5.8 requires a 24px minimum target area, but leading products "
"use 44px+ height to reduce mis-taps and convey quality."
),
references=["Apple HIG", "Material Design", "WCAG 2.5.8"],
fix_snippet=f"button, .btn {{\n padding: {py}px {px}px;\n min-height: 44px;\n}}",
))
# S2 β€” card padding too small
for i, card in enumerate(parsed_page.get("cards", [])):
min_pad = min(
card["padding_top_px"],
card["padding_right_px"],
card["padding_bottom_px"],
card["padding_left_px"],
)
if min_pad < t["card_min_padding_px"]:
rec_pad = t["card_min_padding_px"]
issues.append(Issue(
rule_id="S2_card_padding",
category=Category.SPACING,
severity=Severity.MEDIUM,
confidence=0.9,
message=(
f"Card #{i + 1} padding ({min_pad}px) is below the "
f"recommended minimum ({rec_pad}px)."
),
recommendation=f"Set card padding to at least {rec_pad}px on all sides.",
evidence=(
f"padding:{card['padding_top_px']}px {card['padding_right_px']}px "
f"{card['padding_bottom_px']}px {card['padding_left_px']}px"
),
estimated_time="5 minutes",
why=(
"Insufficient card padding compresses content against the edge, "
"removing the visual breathing room that separates content from chrome. "
"White space inside a card communicates the card's boundaries and makes "
"the content feel considered, not thrown together."
),
references=["Refactoring UI", "Notion", "Stripe"],
fix_snippet=f".card, [class*='card'] {{\n padding: {rec_pad}px;\n}}",
))
# S3 β€” spacing values off the 8pt grid β†’ consistency issue
base = t["grid_base_px"]
tol = t.get("off_grid_tolerance", 0.5)
off_grid = [
v for v in parsed_page.get("spacing_values_px", [])
if v > 0 and not _on_grid(v, base, tol)
]
if len(off_grid) > 2:
sample = sorted({round(v) for v in off_grid})[:5]
issues.append(Issue(
rule_id="S3_off_grid_spacing",
category=Category.CONSISTENCY,
severity=Severity.MEDIUM,
confidence=0.8,
message=f"{len(off_grid)} spacing values are not on the {base}pt grid.",
recommendation=(
f"Adopt a strict {base}pt spacing scale "
f"(e.g. 8, 16, 24, 32, 48 px) and remove arbitrary values."
),
evidence=f"Off-grid values (sample): {sample}",
estimated_time="30 minutes",
why=(
"Arbitrary spacing values accumulate silently into a visually inconsistent UI. "
"An 8pt grid means every spacing decision is predictable β€” developers can "
"reason about layout without opening a design file, and the result looks "
"intentional rather than hand-coded."
),
references=["Tailwind CSS", "Material Design", "8pt Grid System"],
))
return issues