Theming — BCC is dark-only; add a light theme and system-following #75

Closed
opened 2026-07-20 12:07:29 -04:00 by the_og · 1 comment
Owner

Correction to the original filing

I first wrote this up as "BCC renders as a bright white window on a dark desktop." That's backwards, and I should have read the constants before filing. BCC is already dark — BG = "#1b1d23", PANEL = "#23262e", TEXT = "#e7e9ee", applied through a single module-level STYLESHEET in bcc.py.

The actual gap: the theme is hardcoded dark with no light option and no awareness of the OS appearance. A user on a light desktop gets a dark window that matches nothing else on their screen, with no way to change it.

What exists

  • A complete dark palette as module-level constants (ACCENT, BG, PANEL, PANEL_2, TEXT, MUTED, BORDER, GOOD, BAD, WARN).
  • One f-string STYLESHEET built from them, applied globally.
  • ~20 per-widget setStyleSheet(f"color: {MUTED}")-style calls scattered through bcc.py, plus STATUS_COLORS / HEALTH_COLORS lookup tables.

So the colour vocabulary is already factored — it's just bound to one palette at import time.

Scope

  • A Palette value type plus DARK (byte-identical to today's colours — this must not be a redesign) and a new LIGHT.
  • Theme setting with three states: System (default), Light, Dark; persisted in QSettings next to the existing splitter geometry and update/autoCheck.
  • Pure resolve_theme(setting, system_is_dark) -> "light" | "dark" in bcc_core so the decision logic is testable without a Qt app.
  • STYLESHEET becomes build_stylesheet(palette).
  • Toggle in the existing menu, near the About action.

The hard part

The ~20 inline setStyleSheet calls read module-level constants at call time. Switching theme live means those either re-run or go stale, showing dark-theme colours on a light window. Options: re-apply global QSS and refresh the affected panes on switch, or accept a "restart to apply" for inline-styled bits. Prefer live if it's clean; don't ship a half-updated window.

Constraints

  • No -apple-system or any web font stack in the QSS. Removed deliberately; the comment above STYLESHEET explains why (forces a costly font-alias scan). Native system fonts only.
  • GOOD (#4ade80) and WARN (#fbbf24) were picked against a dark background and will not carry to white — the health column and status dots need light-palette variants that actually meet contrast.
  • Several hardcoded near-black values are baked into the QSS (#1a1205 as on-accent text, #202229 for the disabled table, #16181d for the diagnostics pane). These need palette slots, not literals.
  • The masked-secret rendering must stay visually distinct from real values in both palettes.

Tests

resolve_theme across all three settings × both OS appearances; palette completeness (every slot the stylesheet references exists in both palettes, so a missing key fails a test rather than rendering a broken window).

## Correction to the original filing I first wrote this up as "BCC renders as a bright white window on a dark desktop." That's backwards, and I should have read the constants before filing. BCC is **already dark** — `BG = "#1b1d23"`, `PANEL = "#23262e"`, `TEXT = "#e7e9ee"`, applied through a single module-level `STYLESHEET` in `bcc.py`. The actual gap: the theme is **hardcoded dark with no light option and no awareness of the OS appearance**. A user on a light desktop gets a dark window that matches nothing else on their screen, with no way to change it. ## What exists - A complete dark palette as module-level constants (`ACCENT`, `BG`, `PANEL`, `PANEL_2`, `TEXT`, `MUTED`, `BORDER`, `GOOD`, `BAD`, `WARN`). - One f-string `STYLESHEET` built from them, applied globally. - ~20 per-widget `setStyleSheet(f"color: {MUTED}")`-style calls scattered through `bcc.py`, plus `STATUS_COLORS` / `HEALTH_COLORS` lookup tables. So the colour vocabulary is already factored — it's just bound to one palette at import time. ## Scope - A `Palette` value type plus `DARK` (byte-identical to today's colours — this must not be a redesign) and a new `LIGHT`. - Theme setting with three states: **System** (default), **Light**, **Dark**; persisted in `QSettings` next to the existing splitter geometry and `update/autoCheck`. - Pure `resolve_theme(setting, system_is_dark) -> "light" | "dark"` in `bcc_core` so the decision logic is testable without a Qt app. - `STYLESHEET` becomes `build_stylesheet(palette)`. - Toggle in the existing menu, near the About action. ## The hard part The ~20 inline `setStyleSheet` calls read module-level constants at call time. Switching theme live means those either re-run or go stale, showing dark-theme colours on a light window. Options: re-apply global QSS and refresh the affected panes on switch, or accept a "restart to apply" for inline-styled bits. Prefer live if it's clean; don't ship a half-updated window. ## Constraints - **No `-apple-system` or any web font stack in the QSS.** Removed deliberately; the comment above `STYLESHEET` explains why (forces a costly font-alias scan). Native system fonts only. - `GOOD` (`#4ade80`) and `WARN` (`#fbbf24`) were picked against a dark background and will not carry to white — the health column and status dots need light-palette variants that actually meet contrast. - Several hardcoded near-black values are baked into the QSS (`#1a1205` as on-accent text, `#202229` for the disabled table, `#16181d` for the diagnostics pane). These need palette slots, not literals. - The masked-secret rendering must stay visually distinct from real values in both palettes. ## Tests `resolve_theme` across all three settings × both OS appearances; palette completeness (every slot the stylesheet references exists in both palettes, so a missing key fails a test rather than rendering a broken window).
the_og added the P2 label 2026-07-20 12:07:29 -04:00
the_og changed title from Dark mode / theming — light/dark toggle with persistence to Theming — BCC is dark-only; add a light theme and system-following 2026-07-20 12:13:03 -04:00
Author
Owner

Implemented and merged in #80 (commit febd617, merge 9a04332). Every item in this issue's scope landed:

  • Palette value type in bcc_core with DARK_PALETTE (byte-identical to the shipped look — asserted by test_dark_palette_is_unchanged_from_the_shipped_look) and a new LIGHT_PALETTE.
  • Pure resolve_theme(setting, system_is_dark) → "light" | "dark", tested across all three settings × both OS appearances.
  • STYLESHEET → build_stylesheet(palette); apply_palette() rebinds the module-level colour names so the ~20 inline setStyleSheet call sites pick up the new colour on re-render (live switch, no restart).
  • Three-state setting Match system / Light / Dark under View ▸ Theme, persisted in QSettings under ui/theme, defaulting to following the system.
  • The three baked-in near-black literals (#1a1205, #202229, #16181d) now have palette slots; test_stylesheet_builder_has_no_hardcoded_colours forbids reintroducing any, and test_every_palette_slot_is_consumed + test_both_palettes_define_every_slot keep the two palettes in lockstep.
  • Light-palette GOOD/WARN were darkened to clear WCAG AA; test_palette_meets_contrast_on_panel enforces ≥ 4.5:1 for every text colour in both palettes. Native system fonts only (no -apple-system).

The one thing CI can't prove is the live look — light-mode rendering and the live re-render of inline-styled widgets on switch — since there's no PySide6 in CI. That's a human eyeball; if anything reads wrong when you toggle themes, reopen. Closing as done.

Implemented and merged in **#80** (commit `febd617`, merge `9a04332`). Every item in this issue's scope landed: - `Palette` value type in `bcc_core` with `DARK_PALETTE` (byte-identical to the shipped look — asserted by `test_dark_palette_is_unchanged_from_the_shipped_look`) and a new `LIGHT_PALETTE`. - Pure `resolve_theme(setting, system_is_dark) → "light" | "dark"`, tested across all three settings × both OS appearances. - `STYLESHEET` → `build_stylesheet(palette)`; `apply_palette()` rebinds the module-level colour names so the ~20 inline `setStyleSheet` call sites pick up the new colour on re-render (live switch, no restart). - Three-state setting **Match system / Light / Dark** under **View ▸ Theme**, persisted in `QSettings` under `ui/theme`, defaulting to following the system. - The three baked-in near-black literals (`#1a1205`, `#202229`, `#16181d`) now have palette slots; `test_stylesheet_builder_has_no_hardcoded_colours` forbids reintroducing any, and `test_every_palette_slot_is_consumed` + `test_both_palettes_define_every_slot` keep the two palettes in lockstep. - Light-palette `GOOD`/`WARN` were darkened to clear WCAG AA; `test_palette_meets_contrast_on_panel` enforces ≥ 4.5:1 for every text colour in both palettes. Native system fonts only (no `-apple-system`). The one thing CI can't prove is the live *look* — light-mode rendering and the live re-render of inline-styled widgets on switch — since there's no PySide6 in CI. That's a human eyeball; if anything reads wrong when you toggle themes, reopen. Closing as done.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: the_og/better-claude-config#75