"""Every text colour Steward draws is readable on the surfaces it can sit on. Tokens are resolved from theme.css (:root; nothing sets data-theme, so the dark theme is the only one) and steward.css, color-mix() and transparent tints included, the tints composited over each surface. Text is held to 4.5:1 (WCAG AA) on page, card and hovered row, and status text on its own tint over each of them; a control's outline to 3:1 (WCAG 1.4.11). The pairs come from every steward.css rule and every inline style in the templates (#5096). """ import re from pathlib import Path ROOT = Path(__file__).resolve().parent.parent TEXT_FLOOR, CONTROL_FLOOR = 4.5, 3.0 SURFACES = ("--bg", "--bg-card", "--bg-elevated") TOKEN_VALUE = r"(var\(--[\w-]+\)|color-mix\([^;{}]*\)|#[0-9a-fA-F]{3,6})" def _root_block(css: str) -> str: i = css.index(":root {") return re.sub(r"/\*.*?\*/", "", css[i:css.index("\n}", i)], flags=re.S) def _tokens() -> dict[str, str]: out: dict[str, str] = {} for name in ("theme.css", "steward.css"): css = (ROOT / "steward/static" / name).read_text() out.update((k, v.strip()) for k, v in re.findall(r"(--[\w-]+)\s*:\s*([^;]+);", _root_block(css))) return out TOKENS = _tokens() def _args(s: str) -> list[str]: depth, cur, out = 0, "", [] for ch in s: depth += (ch == "(") - (ch == ")") if ch == "," and depth == 0: out.append(cur.strip()) cur = "" else: cur += ch return out + [cur.strip()] def color(expr: str) -> tuple: """(r, g, b, alpha) for a token expression.""" expr = expr.strip() if expr == "transparent": return (0, 0, 0, 0.0) if expr == "black": return (0, 0, 0, 1.0) if expr == "white": return (255, 255, 255, 1.0) if expr.startswith("#"): h = expr[1:] if len(expr) == 7 else "".join(c * 2 for c in expr[1:]) return (int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16), 1.0) if m := re.fullmatch(r"var\((--[\w-]+)\)", expr): return color(TOKENS[m.group(1)]) if m := re.fullmatch(r"color-mix\(in srgb,(.*)\)", expr, re.S): a, b = _args(m.group(1)) pa, pb = re.search(r"\s([\d.]+)%$", a), re.search(r"\s([\d.]+)%$", b) ca, cb = color(a[:pa.start()] if pa else a), color(b[:pb.start()] if pb else b) wa = float(pa.group(1)) / 100 if pa else (1 - float(pb.group(1)) / 100 if pb else 0.5) alpha = ca[3] * wa + cb[3] * (1 - wa) if alpha == 0: return (0, 0, 0, 0.0) return (*( (ca[i] * ca[3] * wa + cb[i] * cb[3] * (1 - wa)) / alpha for i in range(3)), alpha) raise ValueError(f"cannot resolve {expr!r}") def _over(fg: tuple, bg: tuple) -> tuple: return (*(fg[i] * fg[3] + bg[i] * (1 - fg[3]) for i in range(3)), 1.0) def _luminance(c: tuple) -> float: def ch(v): v /= 255 return v / 12.92 if v <= 0.03928 else ((v + 0.055) / 1.055) ** 2.4 return 0.2126 * ch(c[0]) + 0.7152 * ch(c[1]) + 0.0722 * ch(c[2]) def ratio(a: tuple, b: tuple) -> float: hi, lo = sorted((_luminance(a), _luminance(b)), reverse=True) return (hi + 0.05) / (lo + 0.05) def worst(fg: str, bg: str | None = None) -> tuple[float, str]: """The lowest contrast of fg (on bg, if given) over page, card and hover.""" lows = [] for s in SURFACES: back = _over(color(bg), color(f"var({s})")) if bg else color(f"var({s})") lows.append((ratio(_over(color(fg), back), back), s)) return min(lows) def drawn_pairs(): """(where, text colour, its own background or None) for every rule in steward.css and every inline style in the templates that sets a colour.""" sheet = (ROOT / "steward/static/steward.css").read_text() body = re.sub(r"/\*.*?\*/", "", sheet[sheet.index("\n}"):], flags=re.S) for sel, rule in re.findall(r"([^{}]+)\{([^{}]*)\}", body): if fg := re.search(r"(? 200, "the pair scan found almost nothing: the parser broke" assert below(TEXT_FLOOR, pairs) == [] def test_status_text_reads_on_its_own_tint_over_a_hovered_row(): for tok in ("green", "yellow", "orange", "red", "blue"): r, surface = worst(f"var(--{tok})", f"var(--{tok}-dim)") assert r >= TEXT_FLOOR, f"--{tok} on --{tok}-dim over {surface}: {r:.2f}" def test_a_control_outline_marks_the_control(): r, surface = worst("var(--border-control)") assert r >= CONTROL_FLOOR, f"--border-control over {surface}: {r:.2f}" for rule in ("input[type=text]", ".input {", ".btn-ghost {", ".range-btn {"): sheet = (ROOT / "steward/static/steward.css").read_text() decl = sheet[sheet.index(rule):sheet.index("}", sheet.index(rule))] assert "var(--border-control)" in decl, rule def test_the_guard_can_fail(): # The divider colour as text, and the house error colour on its tint over # a hovered row (the pair #5096 found at 4.05:1), must both be caught. assert below(TEXT_FLOOR, [("probe", "var(--border-mid)", None)]) assert below(TEXT_FLOOR, [("probe", "var(--fs-error-fg)", "var(--red-dim)")]) assert worst("var(--border-mid)")[0] < CONTROL_FLOOR