fix(theme): #130 the chip moves, not the label; #129 pins the pair so it is checkable
All checks were successful
Check / eval (push) Successful in 3m44s
All checks were successful
Check / eval (push) Successful in 3m44s
Light-theme accent buttons sat at 2.72:1 — under AA and under even the 3:1
large-text floor. Bernardo: fix it. The plan on file ("darken the label")
turned out to be IMPOSSIBLE, and measuring all 24 palettes is what showed
it: on a saturated mid-tone accent nothing clears 4.5 — cream base 2.72,
dark text 1.65. That is why adw-gtk3 ships white-on-accent at ~2.7. The
chip has to move.
The chip is now the accent pulled AWAY from the label: toward white when
the label is dark, toward black when it is cream. One factor (0.70), only
the anchor flips, so "solid accent button + base label" stays the design
and just shifts tone. Worst dark 4.86 (nord/bad), worst light 5.59
(summer-day). The old 0.90 chip failed SEVEN themes (miasma/bad 3.43,
rose-pine 2.70) — never only a light-theme bug, contrary to the item.
#129 rides along, and the two are one idea. #98 shipped an invisible button
with every check green because the background was not a palette role —
adw-gtk3 computed it from currentColor. So the guard is not a CSS lint
(which would flag the harmless generic-button rule and get muted) but
arithmetic: pin both ends and the pair becomes checkable.
check-theme-contrast.py now models GTK's own mix(a,b,f) = a + (b-a)*f —
READ from gtkcsscolorvalue.c:234, not assumed — and asserts base-on-chip
>= 4.5 for all 24 themes. Its negative test reproduces the item's reported
2.72/2.74 exactly from an independent model, which is the best evidence
available that the arithmetic matches the pixels.
Invariant to keep: pin the pair, or pin neither. Both chips are pinned now
(suggested + .default + destructive), where #98 pinned only destructive.
tools/dialog-shot.nix deliberately not built: the checker covers the
numbers, and a VM that only ever confirms arithmetic is maintenance for
nothing. Render if a new widget class appears.
Also sweeps #127 with the latch finding (ff5017e): symptom 2 solved, not
DPMS — which raises the odds the mitigation treats the wrong cause.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -37,6 +37,48 @@ PAIRINGS = [
|
||||
]
|
||||
|
||||
|
||||
# GTK accent-button chips (#129/#130). Everything above is role-on-role, which
|
||||
# is why #98 shipped an invisible button with every check green: the button's
|
||||
# background is not a palette role at all, it is a colour GTK computes —
|
||||
# mix(anchor, accent, f) — from modules/home/stylix.nix. So this half models
|
||||
# that computation instead of ignoring it.
|
||||
#
|
||||
# It only works because the CSS pins BOTH ends. A pinned label on a background
|
||||
# adw-gtk3 derives from currentColor is unknowable from here — that is the #98
|
||||
# trap, and the rule this check enforces by construction: pin the pair, or pin
|
||||
# neither. If a future rule pins a label and leaves the background to the sheet,
|
||||
# this check will keep passing and mean nothing; that is the one way to defeat
|
||||
# it, so do not.
|
||||
#
|
||||
# Keep in lockstep with stylix.nix: same anchor rule, same factor, same roles.
|
||||
CHIP_FACTOR = 0.70 # `chip` in stylix.nix — rest state, the worst case
|
||||
CHIP_MIN = 4.5 # WCAG AA body text; button labels are body text
|
||||
CHIP_ROLES = [("accent", "suggested-action / .default"), ("bad", "destructive-action")]
|
||||
|
||||
|
||||
def mix(c1: str, c2: str, f: float) -> str:
|
||||
"""GTK's mix(): color1 + (color2 - color1) * factor (gtkcsscolorvalue.c).
|
||||
|
||||
Note the direction — f is the weight of the SECOND colour, so
|
||||
mix(white, accent, 0.7) is 70% accent, not 70% white.
|
||||
"""
|
||||
a = [int(c1.lstrip("#")[i:i + 2], 16) for i in (0, 2, 4)]
|
||||
b = [int(c2.lstrip("#")[i:i + 2], 16) for i in (0, 2, 4)]
|
||||
return "#%02X%02X%02X" % tuple(
|
||||
round(a[i] + (b[i] - a[i]) * f) for i in range(3)
|
||||
)
|
||||
|
||||
|
||||
def chip(colors: dict, role: str, mode: str) -> str:
|
||||
"""The rendered button background: the accent pulled away from the label.
|
||||
|
||||
The label is the palette's base — dark on a dark theme, cream on a light
|
||||
one — so the chip moves toward white or black accordingly.
|
||||
"""
|
||||
anchor = "#000000" if mode == "light" else "#FFFFFF"
|
||||
return mix(anchor, colors[role], CHIP_FACTOR)
|
||||
|
||||
|
||||
def channel(c: int) -> float:
|
||||
c /= 255
|
||||
return c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4
|
||||
@@ -61,7 +103,8 @@ def main() -> int:
|
||||
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"]
|
||||
theme = json.loads(path.read_text())
|
||||
colors = theme["colors"]
|
||||
for fg, bg, minimum, consumer in PAIRINGS:
|
||||
r = ratio(colors[fg], colors[bg])
|
||||
if r < minimum:
|
||||
@@ -69,12 +112,24 @@ def main() -> int:
|
||||
f"{path.stem}: {fg} on {bg} = {r:.2f} < {minimum}"
|
||||
f" ({consumer}; {colors[fg]} on {colors[bg]})"
|
||||
)
|
||||
mode = theme.get("mode", "dark")
|
||||
for role, consumer in CHIP_ROLES:
|
||||
bg = chip(colors, role, mode)
|
||||
r = ratio(colors["base"], bg)
|
||||
if r < CHIP_MIN:
|
||||
failures.append(
|
||||
f"{path.stem}: base on the {role} chip = {r:.2f} < {CHIP_MIN}"
|
||||
f" (GTK {consumer}; {colors['base']} on {bg}"
|
||||
f" = mix({'black' if mode == 'light' else 'white'},"
|
||||
f" {colors[role]}, {CHIP_FACTOR}))"
|
||||
)
|
||||
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")
|
||||
print(f"{len(themes)} themes x {len(PAIRINGS)} pairings"
|
||||
f" + {len(CHIP_ROLES)} GTK accent chips: all pass")
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user