ui-copilot / backend /rules /button_rules.py
ksri77's picture
chore: deploy from CI
8c816e3 verified
Raw
History Blame Contribute Delete
6.38 kB
from __future__ import annotations
from backend.models.issue import Category, Issue, Severity
def _distinct_backgrounds(buttons: list[dict]) -> list[str]:
seen: list[str] = []
for btn in buttons:
color = btn.get("background_color", "").lower()
if color and color not in seen:
seen.append(color)
return seen
def analyze(parsed_page: dict, thresholds: dict) -> list[Issue]:
issues: list[Issue] = []
t = thresholds["buttons"]
buttons = parsed_page.get("buttons", [])
if not buttons:
return issues
# B1 β€” too many distinct button styles (background colour as proxy for style)
distinct_bg = _distinct_backgrounds(buttons)
if len(distinct_bg) > t["max_distinct_styles"]:
issues.append(Issue(
rule_id="B1_button_style_count",
category=Category.CONSISTENCY,
severity=Severity.HIGH,
confidence=0.85,
message=(
f"{len(distinct_bg)} distinct button background colours detected "
f"(max recommended: {t['max_distinct_styles']})."
),
recommendation=(
"Consolidate to one primary button style and at most one secondary/ghost style. "
"Define them as design tokens."
),
evidence=f"Background colours: {', '.join(distinct_bg)}",
estimated_time="30 minutes",
why=(
"Too many button variants confuse users about which action is primary. "
"Inconsistent buttons also signal an absent design system β€” each variant "
"costs designers and developers time to maintain and creates visual "
"fragmentation that erodes trust in the product."
),
references=["Stripe", "Linear", "Shopify Polaris"],
))
# B2 β€” inconsistent border-radius
radii = [btn.get("border_radius_px", 0) for btn in buttons]
if radii and (max(radii) - min(radii)) > t["max_border_radius_variance_px"]:
issues.append(Issue(
rule_id="B2_border_radius_variance",
category=Category.CONSISTENCY,
severity=Severity.MEDIUM,
confidence=0.9,
message=(
f"Button border-radius ranges from {min(radii)}px to {max(radii)}px β€” "
"inconsistent across the UI."
),
recommendation="Pick one border-radius value for all buttons and apply it via a design token.",
evidence=f"Radii found: {sorted(set(radii))}",
estimated_time="10 minutes",
why=(
"Inconsistent border-radius across buttons breaks the visual rhythm of a UI. "
"A single radius value is often the first token a design system defines β€” "
"its consistency signals that the product is purposefully designed, "
"not assembled from disparate components."
),
references=["Tailwind CSS", "Material Design", "Shopify Polaris"],
))
# B3 β€” missing focus styles (accessibility)
no_focus = [btn for btn in buttons if not btn.get("has_focus_style", True)]
if no_focus:
issues.append(Issue(
rule_id="B3_focus_style",
category=Category.ACCESSIBILITY,
severity=Severity.HIGH,
confidence=0.95,
message=f"{len(no_focus)} button(s) have no visible focus style.",
recommendation=(
"Add a visible :focus-visible outline to all interactive elements. "
"E.g. outline: 2px solid #005fcc; outline-offset: 2px;"
),
evidence=f"Buttons without focus style: {[b['text'] for b in no_focus]}",
estimated_time="15 minutes",
why=(
"Keyboard-only users β€” including people with motor disabilities and power "
"users β€” navigate entirely via focus state. Without a visible outline, "
"your UI is functionally unusable for them. WCAG 2.4.7 (Level AA) requires "
"visible focus indicators on all interactive elements."
),
references=["WCAG 2.1 SC 2.4.7", "GOV.UK Design System", "WebAIM"],
fix_snippet=(
"/* Add to your global stylesheet */\n"
":is(button, a, input, select, textarea, [tabindex]):focus-visible {\n"
" outline: 2px solid #005fcc;\n"
" outline-offset: 2px;\n"
"}\n\n"
"/* Remove the default focus ring (only after adding :focus-visible) */\n"
":is(button, a):focus:not(:focus-visible) {\n"
" outline: none;\n"
"}"
),
))
# B4 β€” touch target too small
small_targets = [
btn for btn in buttons
if btn.get("height_px", 44) < t["min_touch_target_px"]
]
if small_targets:
min_h = t["min_touch_target_px"]
issues.append(Issue(
rule_id="B4_touch_target",
category=Category.ACCESSIBILITY,
severity=Severity.MEDIUM,
confidence=0.8,
message=(
f"{len(small_targets)} button(s) have a height below the "
f"{min_h}px minimum touch target."
),
recommendation=f"Ensure all buttons are at least {min_h}px tall (WCAG 2.5.5).",
evidence=f"Heights: {[b['height_px'] for b in small_targets]}",
estimated_time="10 minutes",
why=(
"Targets smaller than 44Γ—44px cause tap errors on mobile, especially for "
"users with motor impairments, larger fingers, or devices in motion. "
"Both Apple HIG and WCAG 2.5.8 recommend 44px as the minimum touch target. "
"Small targets also feel cheap and unpolished."
),
references=["Apple HIG", "WCAG 2.5.8", "Material Design"],
fix_snippet=(
f"button, a, [role='button'] {{\n"
f" min-height: {min_h}px;\n"
f" min-width: {min_h}px;\n"
f" display: inline-flex;\n"
f" align-items: center;\n"
f" justify-content: center;\n"
f"}}"
),
))
return issues