feat: light theme + system-following, dark preserved exactly (#75) #80
Reference in New Issue
Block a user
Delete Branch "feat/75-theming"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Refs #75
BCC has always been dark-only —
BG = "#1b1d23", bound at import time, with no light option and no awareness of the desktop appearance. On a light desktop it matches nothing else on screen and there's no way to change it.What's here
Palettevalue type inbcc_core, withDARK_PALETTE(byte-identical to v1.3.0's colours) and a newLIGHT_PALETTE.resolve_theme(setting, system_is_dark)— pure, testable without a Qt app.QSettingsunderui/theme, defaulting to system.STYLESHEET→build_stylesheet(palette).Three decisions worth reviewing
The light palette is not the dark one lightened.
#4ade80sits near 1.7:1 against white — illegible. The semantic colours are darkened to clear WCAG AA, andtest_palette_meets_contrast_on_panelenforces ≥4.5:1 for every text colour against its surface in both palettes, so nobody harmonises them back toward the dark hues later. Measured: lightgood5.02,warn4.92,accent5.18,muted6.11.Three near-black literals were baked into the QSS —
#1a1205(on-accent text),#202229(disabled table),#16181d(diagnostics pane). Harmless with one theme, invisible breakage with two. Each has a palette slot now, andtest_stylesheet_builder_has_no_hardcoded_coloursasserts the builder contains no hex literals at all.The ~20 inline
setStyleSheet(f"color: {MUTED}")call sites are untouched.apply_paletterebinds the module-level colour names, and an f-string resolves its names when it runs — so every call site picks up the new colour on its next render, with no signature churn. The tradeoff: already-rendered widgets keep their old inline style until re-rendered, so_set_themereapplies the global QSS and re-renders the inline-styled widgets rather than leaving dark-on-light text behind.Tests
389 passed, 1 skipped (+15). Ruff clean.
Covers
resolve_themeacross all three settings × both appearances; junk/hand-edited settings falling back to system rather than a fixed theme; dark palette asserted unchanged from the shipped look; contrast on every slot in both palettes; and the two source-level guards above.test_contrast_ratio_endpointspins the WCAG maths against known values (black/white = 21.0, identical = 1.0) so the contrast assertions can't pass by way of a broken formula.What I could not verify
No visual confirmation. I tried rendering
MainWindowoffscreen under both palettes, but PySide6 needslibEGL.so.1and I don't have root in this sandbox to install it. So this is verified at the level of "the QSS is built from the palette, contains no stray literals, and every colour meets contrast" — not "I looked at the light theme and it appears correct."Worth a human eyeballing the light theme before merge, particularly: the disabled/parked servers table against the main table, the diagnostics pane, and selected rows (
on_accentonaccent).Note
One test I wrote (
every_palette_slot_is_used_by_the_stylesheet) failed on first run and was wrong rather than the code:goodandremotelegitimately never appear in the QSS — they feed the inline status dots throughSTATUS_COLORS/HEALTH_COLORS. Rewritten astest_every_palette_slot_is_consumed, with a docstring stating what it does and doesn't catch.