fix(swaync): palette-safe pairings — text was invisible on summer-day (item 25)
All checks were successful
Check / eval (push) Successful in 2m52s
All checks were successful
Check / eval (push) Successful in 2m52s
Palette roles aren't uniform: summer-day and flexoki-light use subtext as text-ON-surface (== base) and surface as a dark chip (== text), so swaync's @subtext-on-@base body text and @surface/@text buttons were self-colored (caught on the Latitude). - body/summary: @text on @base — the one text pairing that passes in every palette (survey: worst WCAG ratio 5.18 across all 21 themes; subtext/base bottomed at 1.00 twice, muted/base at 1.27). - hover rows + widget-title buttons: alpha(@text, 0.1) tints — contrast by construction; subtext/surface defines dropped. - NEW checks.theme-contrast (tools/check-theme-contrast.py): asserts the hex-on-hex text pairings across themes/*.json, cheap (no VM). The audit also found the generated waybar/rofi CSS shares the bug class, live on flexoki-light (no whole-swap) → BACKLOG item 27. Verified: V0; V1 (checks.theme-contrast builds green, 21 themes; rendered services.swaync.style has zero @subtext/@surface). Honesty: NOT visually verified — V3 queued (notify-send on summer-day). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
69
tools/check-theme-contrast.py
Normal file
69
tools/check-theme-contrast.py
Normal file
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Guard against invisible-text palettes (BACKLOG item 25).
|
||||
|
||||
Asserts, for every themes/*.json, the WCAG contrast ratio of each
|
||||
hex-on-hex fg/bg pairing the generated swaync CSS uses. Pairings built
|
||||
from alpha(@text) tints over @base are contrast-by-construction and
|
||||
need no assert. Extend PAIRINGS as more generated surfaces are fixed to
|
||||
palette-safe roles (item 27 tracks waybar/rofi, which still use
|
||||
subtext/surface and would fail here today).
|
||||
|
||||
Wired as `checks.theme-contrast` in flake.nix; also runnable directly:
|
||||
python3 tools/check-theme-contrast.py themes/
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# (fg role, bg role, minimum ratio, consumer). Text pairings only —
|
||||
# borders (@accent/@bad on @base in swaync) are decorative and not held
|
||||
# to a text ratio (miasma's bad-on-base is 2.3, fine for a border).
|
||||
PAIRINGS = [
|
||||
("text", "base", 4.5, "swaync body/summary/titles on the toast"),
|
||||
("base", "accent", 3.0, "swaync widget-title button:hover"),
|
||||
]
|
||||
|
||||
|
||||
def channel(c: int) -> float:
|
||||
c /= 255
|
||||
return c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4
|
||||
|
||||
|
||||
def luminance(hexstr: str) -> float:
|
||||
hexstr = hexstr.lstrip("#")
|
||||
r, g, b = (int(hexstr[i:i + 2], 16) for i in (0, 2, 4))
|
||||
return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b)
|
||||
|
||||
|
||||
def ratio(fg: str, bg: str) -> float:
|
||||
hi, lo = sorted((luminance(fg), luminance(bg)), reverse=True)
|
||||
return (hi + 0.05) / (lo + 0.05)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
themes_dir = Path(sys.argv[1] if len(sys.argv) > 1 else "themes")
|
||||
failures = []
|
||||
themes = sorted(themes_dir.glob("*.json"))
|
||||
if not themes:
|
||||
print(f"no theme JSONs found under {themes_dir}", file=sys.stderr)
|
||||
return 1
|
||||
for path in themes:
|
||||
colors = json.loads(path.read_text())["colors"]
|
||||
for fg, bg, minimum, consumer in PAIRINGS:
|
||||
r = ratio(colors[fg], colors[bg])
|
||||
if r < minimum:
|
||||
failures.append(
|
||||
f"{path.stem}: {fg} on {bg} = {r:.2f} < {minimum}"
|
||||
f" ({consumer}; {colors[fg]} on {colors[bg]})"
|
||||
)
|
||||
if failures:
|
||||
print("theme contrast failures:", file=sys.stderr)
|
||||
for f in failures:
|
||||
print(f" {f}", file=sys.stderr)
|
||||
return 1
|
||||
print(f"{len(themes)} themes x {len(PAIRINGS)} pairings: all pass")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user