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:
@@ -22,26 +22,28 @@ next, in what order*.
|
|||||||
|
|
||||||
## NOW
|
## NOW
|
||||||
|
|
||||||
*Items 21–26: real-hardware QA findings from the Latitude 5410
|
*Items 21–27: real-hardware QA findings from the Latitude 5410
|
||||||
(Bernardo, 2026-07-04) — bugs found on metal outrank the queue.*
|
(Bernardo, 2026-07-04) — bugs found on metal outrank the queue.*
|
||||||
|
|
||||||
### 25. swaync text unreadable on summer-day (light-theme contrast)
|
### 27. Generated waybar/rofi CSS invisible on on-surface palettes
|
||||||
Reported: on summer-day, notification/control-centre text is invisible
|
Found by item 25's audit: the generated Waybar CSS colors `#window` and
|
||||||
(contrast). Root cause (traced 2026-07-04): swaync.nix renders `.body`
|
the whole module row `@subtext`, and the generated rofi theme puts `@fg`
|
||||||
as `@subtext` on `alpha(@base, 0.95)` — but in summer-day (and
|
text on `@surface` backgrounds (inputbar, textbox, `element alternate`).
|
||||||
flexoki-light) `subtext` *equals/≈ base* because those palettes use
|
In palettes where subtext/surface mean "on-surface" (flexoki-light:
|
||||||
`surface` as a dark contrast surface and `subtext` as text-*on*-surface,
|
subtext==base AND surface==text) those are invisible. summer-day/night
|
||||||
not "dimmer text" (catppuccin-latte-style). summer-day also has
|
escape via their whole-swap waybar.css/rofi.rasi — **flexoki-light does
|
||||||
`surface == text` (#5c6a72), so `.widget-title > button`
|
not** and runs the generated CSS. Same class of fix as the shipped
|
||||||
(@surface bg, @text fg) is self-colored too. Fix direction: swaync.nix
|
swaync one (63136a8-follow-on): move the generated surfaces to pairings
|
||||||
should pair roles that contrast by construction — `muted` (dim-text
|
that contrast by construction (alpha(@text) tints; @text for module
|
||||||
role in every palette) for body text on @base, and button fg that
|
text with per-module accents kept), then extend
|
||||||
contrasts @surface (`subtext` is arguably the *right* role there) —
|
tools/check-theme-contrast.py's PAIRINGS to cover waybar/rofi so
|
||||||
or normalize the palette semantics across the light themes; audit the
|
`checks.theme-contrast` guards them too. Waybar changes must respect
|
||||||
other `@subtext`/`@surface` consumers (waybar css, rofi) for the same
|
the parity rule (generated bar only — the summer whole-swaps are
|
||||||
mispairing before choosing. **Verify:** V1 + a contrast assert over all
|
hand-authored and unaffected). Alternative worth weighing in-iteration:
|
||||||
themes (text≠bg for each swaync pairing); V2 themed-VM screenshot with
|
normalize the flexoki-light palette itself (make subtext dim-text,
|
||||||
a `notify-send`; V3 re-check on the Latitude.
|
surface elevated-bg) — smaller diff but changes the theme's look
|
||||||
|
everywhere. **Verify:** V1 + the extended contrast check across all 21
|
||||||
|
themes; V2 themed-VM screenshot on flexoki-light if cheap; V3 eyeball.
|
||||||
|
|
||||||
### 22. Network menu shows nameless/weird entries
|
### 22. Network menu shows nameless/weird entries
|
||||||
Reported: `networkmanager_dmenu` lists entries with no names (likely
|
Reported: `networkmanager_dmenu` lists entries with no names (likely
|
||||||
|
|||||||
@@ -39,6 +39,11 @@ QA machine), the **T14s** (webcam case).
|
|||||||
`home-update` + relogin (or `hyprctl reload`), SUPER+? must open
|
`home-update` + relogin (or `hyprctl reload`), SUPER+? must open
|
||||||
the keybindings cheatsheet, whose row still reads `SUPER + ?`
|
the keybindings cheatsheet, whose row still reads `SUPER + ?`
|
||||||
(not `SUPER + SHIFT + ?`).
|
(not `SUPER + SHIFT + ?`).
|
||||||
|
- [ ] **swaync readable on summer-day** (item 25 fix) — on summer-day
|
||||||
|
after `home-update`: `notify-send "title" "body text"` shows
|
||||||
|
readable body text; open the control centre (SUPER+N), hover a
|
||||||
|
notification row and check the Clear button — all text legible
|
||||||
|
(body/buttons now @text on tinted chips, not subtext/surface).
|
||||||
- [ ] **Keyboard hotplug picker (re-verify after in-flake graduation)** —
|
- [ ] **Keyboard hotplug picker (re-verify after in-flake graduation)** —
|
||||||
plug an external keyboard post-login, pick a layout in rofi, confirm
|
plug an external keyboard post-login, pick a layout in rofi, confirm
|
||||||
it applies per-device only, persists in `settings.keyboard.devices`,
|
it applies per-device only, persists in `settings.keyboard.devices`,
|
||||||
|
|||||||
@@ -17,6 +17,22 @@ Template:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-07-04 — swaync contrast fix + theme-contrast check (iteration #9, item 25)
|
||||||
|
- **Task:** BACKLOG NOW#25 — swaync text invisible on summer-day.
|
||||||
|
- **Did:** swaync.nix now uses only palette-safe pairings: body/summary
|
||||||
|
= @text on @base (worst ratio 5.18 across all 21 themes; subtext/base
|
||||||
|
was 1.00 on summer-day + flexoki-light), hover rows + widget buttons
|
||||||
|
= alpha(@text, 0.1) tints (contrast by construction); dropped the
|
||||||
|
subtext/surface defines. NEW permanent check `checks.theme-contrast`
|
||||||
|
(tools/check-theme-contrast.py) asserts the hex-on-hex text pairings
|
||||||
|
across every theme JSON. Audit found the same mispairing in generated
|
||||||
|
waybar/rofi CSS, live on flexoki-light (no whole-swap) → NOW item 27.
|
||||||
|
- **Verified:** V0; V1 — checks.theme-contrast builds green (21 themes),
|
||||||
|
rendered services.swaync.style contains zero @subtext/@surface.
|
||||||
|
NOT V2/V3-verified visually — queued (Bernardo is at the Latitude).
|
||||||
|
- **Pending:** V3 notify-send readability check on summer-day; item 27.
|
||||||
|
- **Next suggestion:** item 27 (same bug class, waybar/rofi surfaces).
|
||||||
|
|
||||||
## 2026-07-04 — SUPER+? bind fix (iteration #8, item 26)
|
## 2026-07-04 — SUPER+? bind fix (iteration #8, item 26)
|
||||||
- **Task:** BACKLOG NOW#26 — cheatsheet bind never fires on hardware.
|
- **Task:** BACKLOG NOW#26 — cheatsheet bind never fires on hardware.
|
||||||
- **Did:** keybinds.nix: `$mod, question` → `$mod SHIFT, question`
|
- **Did:** keybinds.nix: `$mod, question` → `$mod SHIFT, question`
|
||||||
|
|||||||
@@ -849,6 +849,17 @@ how to override it. Items marked ✓ are shipped.
|
|||||||
your theme / essentials" flow (ties into the branding work).
|
your theme / essentials" flow (ties into the branding work).
|
||||||
|
|
||||||
## Known issues & follow-ups
|
## Known issues & follow-ups
|
||||||
|
- ✓ **swaync text invisible on summer-day** (Latitude QA, 2026-07-04):
|
||||||
|
the themed CSS paired @subtext on @base, but palette roles are not
|
||||||
|
uniform across themes — summer-day/flexoki-light use subtext as
|
||||||
|
text-*on*-surface (== base) and surface as a dark chip (== text), so
|
||||||
|
body text and widget buttons self-colored. Fix: swaync uses only
|
||||||
|
pairings safe in every palette — @text on @base for text (survey:
|
||||||
|
worst ratio 5.18 across 21 themes) and alpha(@text, 0.1) tints for
|
||||||
|
chips/hovers — guarded permanently by `checks.theme-contrast`
|
||||||
|
(tools/check-theme-contrast.py, WCAG ratios over themes/*.json).
|
||||||
|
The same audit found generated waybar/rofi CSS shares the bug class,
|
||||||
|
exposed on flexoki-light (BACKLOG item 27).
|
||||||
- ✓ **Waybar crash on theme switch left the session bar-less** (Latitude
|
- ✓ **Waybar crash on theme switch left the session bar-less** (Latitude
|
||||||
hardware QA, 2026-07-04): exec-once has no supervisor, so any crash
|
hardware QA, 2026-07-04): exec-once has no supervisor, so any crash
|
||||||
orphaned the session until relogin — and the switch path itself was
|
orphaned the session until relogin — and the switch path itself was
|
||||||
|
|||||||
12
flake.nix
12
flake.nix
@@ -135,6 +135,18 @@
|
|||||||
downstream-template-home =
|
downstream-template-home =
|
||||||
downstream.homeConfigurations.me.activationPackage;
|
downstream.homeConfigurations.me.activationPackage;
|
||||||
|
|
||||||
|
# Every hex-on-hex text pairing the generated swaync CSS uses
|
||||||
|
# must contrast in EVERY palette — summer-day's body text was
|
||||||
|
# invisible on hardware (item 25: subtext==base there). Cheap
|
||||||
|
# (no VM); extend the script's PAIRINGS as more generated
|
||||||
|
# surfaces move to palette-safe roles (item 27: waybar/rofi).
|
||||||
|
theme-contrast = pkgs.runCommand "nomarchy-theme-contrast"
|
||||||
|
{ nativeBuildInputs = [ pkgs.python3 ]; }
|
||||||
|
''
|
||||||
|
python3 ${./tools/check-theme-contrast.py} ${./themes}
|
||||||
|
touch $out
|
||||||
|
'';
|
||||||
|
|
||||||
# Config-level VM assertions for the nomarchy.hardware.* toggles —
|
# Config-level VM assertions for the nomarchy.hardware.* toggles —
|
||||||
# what they SET (kernel cmdline, fprintd, PAM). Real hardware
|
# what they SET (kernel cmdline, fprintd, PAM). Real hardware
|
||||||
# behaviour (firmware/driver/device) needs bare metal and is out of
|
# behaviour (firmware/driver/device) needs bare metal and is out of
|
||||||
|
|||||||
@@ -29,11 +29,14 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
style = ''
|
style = ''
|
||||||
/* Palette baked from theme-state.json */
|
/* Palette baked from theme-state.json. Only roles guaranteed to
|
||||||
|
contrast @base in EVERY palette are used: subtext/surface mean
|
||||||
|
"on-surface" in some light themes (summer-day: subtext==base,
|
||||||
|
surface==text — body text was invisible on hardware, item 25).
|
||||||
|
Chips/hovers are alpha(@text) tints — contrast by construction.
|
||||||
|
tools/check-theme-contrast.py guards the hex-on-hex pairings. */
|
||||||
@define-color base ${c.base};
|
@define-color base ${c.base};
|
||||||
@define-color surface ${c.surface};
|
|
||||||
@define-color text ${c.text};
|
@define-color text ${c.text};
|
||||||
@define-color subtext ${c.subtext};
|
|
||||||
@define-color accent ${c.accent};
|
@define-color accent ${c.accent};
|
||||||
@define-color bad ${c.bad};
|
@define-color bad ${c.bad};
|
||||||
|
|
||||||
@@ -50,7 +53,7 @@ in
|
|||||||
}
|
}
|
||||||
|
|
||||||
.notification-content .body {
|
.notification-content .body {
|
||||||
color: @subtext;
|
color: @text;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification.critical {
|
.notification.critical {
|
||||||
@@ -66,7 +69,7 @@ in
|
|||||||
|
|
||||||
.control-center .notification-row:focus,
|
.control-center .notification-row:focus,
|
||||||
.control-center .notification-row:hover {
|
.control-center .notification-row:hover {
|
||||||
background: alpha(@surface, 0.6);
|
background: alpha(@text, 0.1);
|
||||||
border-radius: ${r}px;
|
border-radius: ${r}px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +79,7 @@ in
|
|||||||
}
|
}
|
||||||
|
|
||||||
.widget-title > button {
|
.widget-title > button {
|
||||||
background: @surface;
|
background: alpha(@text, 0.1);
|
||||||
color: @text;
|
color: @text;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: ${r}px;
|
border-radius: ${r}px;
|
||||||
|
|||||||
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