Skip to content

feat(theme): ship Default Light theme (Phase 4 milestone)#3129

Merged
ten9876 merged 2 commits into
mainfrom
auto/default-light-theme
May 25, 2026
Merged

feat(theme): ship Default Light theme (Phase 4 milestone)#3129
ten9876 merged 2 commits into
mainfrom
auto/default-light-theme

Conversation

@ten9876

@ten9876 ten9876 commented May 25, 2026

Copy link
Copy Markdown
Collaborator

Summary

The final Phase 4 milestone for the theming subsystem — every load-bearing colour decision in the GUI now flows through the token system, so authoring a second theme is purely a JSON exercise.

What Default Light is

Daylight-readable variant of the canonical taxonomy. Backgrounds invert (panel ramp goes light→dark for elevation), text colours invert (primary text now dark on light), accents darken to maintain contrast against the light canvas. Slice and meter colours keep semantic meaning but shift toward the darker / more-saturated end of their hue family so they read clearly on the light background.

Design choices, by category

Category Approach
Backgrounds Light at the base (`#f5f5f8`), darker with elevation (`#b0bcc8` at the highest tier). `background.tx` warms to `#ffeacc` to preserve "transmit zone" feel. `background.spectrum` flips to pure white.
Accents Pulled toward the darker end of each hue family. Cyan `#00b4d8` → `#0088b0`; warning amber → `#d08010`; danger red → `#c02020`; success green → `#1a8040`.
Text Full inversion of the dark ramp. primary `#c8d8e8` → `#1a2a3a`; secondary / label / disabled shift accordingly.
Borders Light grey at subtle, medium grey at strong. TX border keeps the warm cast: `#5a4a28` → `#b08840`.
Meters Semantic colours preserved (red crest, cyan rms, amber threshold, gold GR). All darkened for contrast on the light meter background. `peak` reads dark on light instead of near-white.
Spectrum trace / peakHold / average shift to darker tones. grid lightens to `#d0d8e0` so it stays visible-but-subtle on the white canvas.
Waterfall Colormap gradients UNCHANGED across all 5 presets. The waterfall renders OPAQUE pixels from a heatmap; canvas underneath doesn't show through. Same Default / Grayscale / Blue-Green / Fire / Plasma in both themes.
Slice Active hues stay distinct (cyan / magenta / green / yellow / orange / teal / coral / lavender) but shift toward saturated mid-tones. Dim variants become washed-out light pastels instead of half-brightness saturated versions — they sit visibly above the light background without being attention-grabbing.

Wiring

  • `resources/themes/default-light.json` — new file, 152 lines.
  • `resources/resources.qrc` — one-line entry registering the new theme as `:/themes/default-light.json`.

No C++ changes needed. `ThemeManager::scanAvailableThemes()` already auto-discovers any `*.json` in `:/themes/` at startup, so the new theme appears in `availableThemes()` and is settable via `setActiveTheme("Default Light")`. All widgets that registered through `ThemeManager::applyStyleSheet` (50+ sites from the Phase 2/3 migration) live-re-theme automatically. Slice indicators, waterfall colormaps, FFT background fill, primary sliders — all flip on the next paint.

Build verified clean

  • AetherSDR builds clean
  • All 320 targets including every test target
  • Both themes appear in the generated qrc_resources.cpp

Phase 4 status — complete

Item PR
Slice indicators runtime themed #3121
Waterfall colormap runtime themed #3122
Default Light theme this PR

Phase 5 picks up the theme editor UI (modeless dialog with inspector mode), import/export, and custom-theme persistence in `~/.config/AetherSDR/themes/`.

Test plan

  • CI: build / check-paths / check-windows / CodeQL
  • Visual smoke: launch the app, open Settings → switch active theme to "Default Light", confirm:
    • Panels invert correctly (light backgrounds, dark text)
    • Spectrum widget renders with a white canvas, dark FFT trace, light grid lines
    • Waterfall colormap unchanged (still black→…→hot)
    • All 8 slice indicators readable on the light spectrum
    • Primary sliders (sub-page fill + light handle) still visible with darker accent fill
    • Strip panels' amber-sub-page sliders still TX-coloured
  • Bonus: switch back to "Default Dark" — every widget should snap back without restart

73, Jeremy KK7GWY & Claude (AI dev partner)

🤖 Generated with Claude Code

The final Phase 4 milestone for the theming subsystem — every load-
bearing colour decision in the GUI now flows through the token system,
so authoring a second theme is purely a JSON exercise.

## What Default Light is

Daylight-readable variant of the canonical taxonomy.  Backgrounds invert
(panel ramp goes light→dark for elevation), text colours invert (primary
text now dark on light), accents darken to maintain contrast against the
light canvas.  Slice and meter colours keep semantic meaning but shift
toward the darker / more-saturated end of their hue family so they read
clearly on the light background.

## Design choices, by category

  Backgrounds — light at the base, slightly darker as elevation
                  increases.  background.0 (panel base) `#f5f5f8`,
                  through background.3 (highest raised) `#b0bcc8`.
                  background.tx becomes a warm cream `#ffeacc` to
                  preserve the "transmit area" feel without overwhelming
                  the surrounding light panels.  background.spectrum
                  flips to white `#ffffff`.

  Accents     — pulled toward the darker end of each hue family for
                  contrast against the light canvas.  Cyan accent
                  `#00b4d8` → `#0088b0`; warning amber
                  `#ffb84d` → `#d08010`; danger red
                  `#ff4d4d` → `#c02020`; success green
                  `#4dd87a` → `#1a8040`.

  Text         — full inversion of the dark ramp.  primary
                  `#c8d8e8` → `#1a2a3a` (dark blue-grey on light);
                  secondary, label, disabled tiers shift accordingly.

  Borders     — light grey at the subtle tier, medium grey at strong,
                  accent border matches accent colour.  TX border keeps
                  its warm cast: `#5a4a28` → `#b08840`.

  Meters       — semantic colours preserved (red crest, cyan rms,
                  amber threshold, gold gain-reduction).  All darkened
                  for contrast on the light meter background.  peak
                  reads dark on light (`#1a2a3a`) instead of near-white.

  Spectrum     — trace, peakHold, average shift to the darker tier of
                  their dark-theme hues.  grid lightens from
                  `#1a2330` to `#d0d8e0` so it's visible-but-subtle
                  against the white spectrum canvas.

  Waterfall    — colormap gradients UNCHANGED across all 5 presets.
                  The waterfall renders OPAQUE pixels from a heatmap;
                  the canvas underneath doesn't show through.  Same
                  Default / Grayscale / Blue-Green / Fire / Plasma in
                  both themes.

  Slice        — active hues stay distinct (cyan, magenta, green,
                  yellow, orange, teal, coral, lavender) but shift
                  toward saturated mid-tones.  Dim variants become
                  WASHED-OUT light pastels rather than half-brightness
                  saturated versions — they sit visibly above the
                  light background without being attention-grabbing.

## Wiring

  resources/themes/default-light.json   new file, 152 lines.
  resources/resources.qrc                +1 line registering the new
                                          theme as `:/themes/default-light.json`.

  ThemeManager::scanAvailableThemes() already auto-discovers any *.json
  in `:/themes/` at startup, so the new theme appears in
  availableThemes() and is settable via setActiveTheme("Default Light")
  with no further code changes.

## Build verified clean

  All 320 targets including every test target.  Both themes appear in
  the bundled qrc_resources.cpp.

## Phase 4 status

  Phase 4 is now complete:
    ✅ Slice indicators runtime themed (#3121)
    ✅ Waterfall colormap runtime themed (#3122)
    ✅ Default Light theme (this commit)

Phase 5 picks up the theme editor UI (modeless dialog with inspector
mode), import/export, and custom-theme persistence in
~/.config/AetherSDR/themes/.

73, Jeremy KK7GWY & Claude (AI dev partner)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ten9876 ten9876 requested a review from a team as a code owner May 25, 2026 09:18
Default-light shipped in this PR's first commit but had no user-reachable
switch — `setActiveTheme()` existed only as an API call with no UI
binding.  An operator had to edit the AppSettings file by hand to load
the light theme, which defeats the whole point of bundling it.

Adds a "Theme" submenu under the View menu that:

  * Lists every theme `ThemeManager::availableThemes()` knows about
    (built-ins under `:/themes/`, plus user themes under
    `~/.config/AetherSDR/themes/` once Phase 5's editor starts writing
    there).
  * Marks the currently active theme via QActionGroup.
  * On click, calls `setActiveTheme(name)` which:
      - swaps the token table
      - persists `ActiveTheme` to AppSettings
      - emits `themeChanged()`, which kicks every widget registered
        through `applyStyleSheet` into a re-paint with the new tokens.
  * Rebuilds itself on `themeChanged` so Phase-5 user themes appearing
    in the user dir get picked up without a restart, and so the check-
    mark moves to the right entry after the swap.

Same lazy-rebuild pattern as the existing Band Plan submenu so the
implementation is self-explanatory by analogy.

73, Jeremy KK7GWY & Claude (AI dev partner)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ten9876 ten9876 merged commit 1a2f45b into main May 25, 2026
5 checks passed
@ten9876 ten9876 deleted the auto/default-light-theme branch May 25, 2026 13:55
ten9876 added a commit that referenced this pull request May 25, 2026
…+ Save As) (#3130)

## Summary

First user-facing surface of the theming subsystem. Operators can now
edit any colour token in the active theme and ship a custom theme to
disk, all without touching a JSON file.

**View → Theme Editor…** opens a modeless dialog with:

- "Editing: \<active theme\>" header
- Filter bar — type-to-match against dotted token names (e.g.
\`accent\`, \`slice\`, \`meter\`)
- Sorted list of every colour token with a 16×16 swatch + hex value per
row
- Click row → \`QColorDialog\` opens seeded with the current value
- Accept → \`ThemeManager::setColor()\` → emits \`themeChanged\` → every
widget registered through \`applyStyleSheet\` repaints on the next
event-loop turn. **No restart**.
- "Save As…" prompts for a theme name and writes the current tokens to
\`~/.config/AetherSDR/themes/\<name\>.json\` via
\`saveCurrentThemeAs()\`. The new theme is registered + made active
immediately.

## ThemeManager additions

| Method | Purpose |
|---|---|
| \`allTokenKeys()\` | Sorted enumeration of every loaded token — used
by the editor to populate its list. |
| \`setColor(token, color)\` | In-memory mutation + themeChanged. Hex
round-trip so \`cssFragment()\` / \`resolve()\` pick up the new value
the same way they pick up loaded tokens. No-op on a same-value write. |
| \`setSizing(token, int)\` | Integer-token equivalent for panel padding
/ border widths. Not yet exposed in the editor UI; will land in PR 2
alongside font/sizing surface. |
| \`saveCurrentThemeAs(name)\` | Rebuilds nested JSON from the flat
dotted-key map (so on-disk shape matches bundled themes), writes to user
themes dir, registers + activates. Handles QString/int/double/bool
scalars + full \`ThemeGradient\` round-trip so the waterfall colormap
and slice.dim tokens persist correctly. |

## Explicitly NOT in this PR (deferred to Phase 5 PR 2-4)

1. **Inspector mode** (click-on-widget to find tokens that paint it).
\`ThemeManager::tokensForWidget()\` already exposes the reverse-map
populated by \`applyStyleSheet\`; wiring is a few hundred lines once we
settle on the latch/unlatch UX. Probably matches the AetherControl
double-click convention (#3103).
2. **Gradient editing** for
\`waterfall.colormap.{default,grayscale,fire,…}\` and
\`slice.dim.{a..h}\`. The list filters them out via the
\`brush().gradient()\` probe so they don't render as
misleading-first-stop swatches.
3. **Font / sizing editing** — same Phase 5 PR 2 surface as the
inspector mode.
4. **Import** (file picker for arbitrary JSON), drag-and-drop, theme
deletion, theme renaming.

## Build verified clean

- [x] AetherSDR builds clean
- [x] All 320 targets including every test target

## Phase status

| Phase | Item | Status |
|---|---|---|
| 1 | ThemeManager scaffolding + JSON loader | ✅ |
| 2 | Gradient tokens + applyStyleSheet reverse-map + mass migration | ✅
|
| 3 | Paint-code migration + canonical-table closeout | ✅ |
| 4 | Default Light theme | ⏳ (#3129, in review) |
| **5** | **Editor UI (PR 1: live colour editing + Save As)** | **this
PR** |

## Test plan

- [ ] CI: build / check-paths / check-windows / CodeQL
- [ ] Visual smoke:
  - [ ] View → Theme Editor… opens the modeless dialog
  - [ ] Title shows "Editing: Default Dark" (or whatever's active)
  - [ ] Filter bar — type \`accent\` → only accent.* rows visible
- [ ] Click an accent row → QColorDialog opens, pick a wild hue, accept
→ that colour appears immediately wherever \`color.accent\` paints (most
buttons, slider handles, badges)
  - [ ] Save As… → name it "Test", confirm:
    - File written to \`~/.config/AetherSDR/themes/Test.json\`
    - Title bar updates to "Editing: Test"
    - View → Theme submenu (from #3129) now lists "Test"
- [ ] Switch back to Default Dark via the View menu → original colours
restored, custom edits gone (file on disk preserved)
- [ ] Reopen Test from the menu → custom edits restored from the saved
file

73, Jeremy KK7GWY & Claude (AI dev partner)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
…3129)

## Summary

The final Phase 4 milestone for the theming subsystem — every
load-bearing colour decision in the GUI now flows through the token
system, so authoring a second theme is purely a JSON exercise.

## What Default Light is

Daylight-readable variant of the canonical taxonomy. Backgrounds invert
(panel ramp goes light→dark for elevation), text colours invert (primary
text now dark on light), accents darken to maintain contrast against the
light canvas. Slice and meter colours keep semantic meaning but shift
toward the darker / more-saturated end of their hue family so they read
clearly on the light background.

## Design choices, by category

| Category | Approach |
|---|---|
| **Backgrounds** | Light at the base (\`#f5f5f8\`), darker with
elevation (\`#b0bcc8\` at the highest tier). \`background.tx\` warms to
\`#ffeacc\` to preserve "transmit zone" feel. \`background.spectrum\`
flips to pure white. |
| **Accents** | Pulled toward the darker end of each hue family. Cyan
\`#00b4d8\` → \`#0088b0\`; warning amber → \`#d08010\`; danger red →
\`#c02020\`; success green → \`#1a8040\`. |
| **Text** | Full inversion of the dark ramp. primary \`#c8d8e8\` →
\`#1a2a3a\`; secondary / label / disabled shift accordingly. |
| **Borders** | Light grey at subtle, medium grey at strong. TX border
keeps the warm cast: \`#5a4a28\` → \`#b08840\`. |
| **Meters** | Semantic colours preserved (red crest, cyan rms, amber
threshold, gold GR). All darkened for contrast on the light meter
background. \`peak\` reads dark on light instead of near-white. |
| **Spectrum** | trace / peakHold / average shift to darker tones. grid
lightens to \`#d0d8e0\` so it stays visible-but-subtle on the white
canvas. |
| **Waterfall** | Colormap gradients **UNCHANGED** across all 5 presets.
The waterfall renders OPAQUE pixels from a heatmap; canvas underneath
doesn't show through. Same Default / Grayscale / Blue-Green / Fire /
Plasma in both themes. |
| **Slice** | Active hues stay distinct (cyan / magenta / green / yellow
/ orange / teal / coral / lavender) but shift toward saturated
mid-tones. Dim variants become **washed-out light pastels** instead of
half-brightness saturated versions — they sit visibly above the light
background without being attention-grabbing. |

## Wiring

- \`resources/themes/default-light.json\` — new file, 152 lines.
- \`resources/resources.qrc\` — one-line entry registering the new theme
as \`:/themes/default-light.json\`.

No C++ changes needed. \`ThemeManager::scanAvailableThemes()\` already
auto-discovers any \`*.json\` in \`:/themes/\` at startup, so the new
theme appears in \`availableThemes()\` and is settable via
\`setActiveTheme(\"Default Light\")\`. All widgets that registered
through \`ThemeManager::applyStyleSheet\` (50+ sites from the Phase 2/3
migration) live-re-theme automatically. Slice indicators, waterfall
colormaps, FFT background fill, primary sliders — all flip on the next
paint.

## Build verified clean

- [x] AetherSDR builds clean
- [x] All 320 targets including every test target
- [x] Both themes appear in the generated qrc_resources.cpp

## Phase 4 status — complete

| Item | PR |
|---|---|
| Slice indicators runtime themed | aethersdr#3121 ✅ |
| Waterfall colormap runtime themed | aethersdr#3122 ✅ |
| **Default Light theme** | **this PR** ✅ |

Phase 5 picks up the theme editor UI (modeless dialog with inspector
mode), import/export, and custom-theme persistence in
\`~/.config/AetherSDR/themes/\`.

## Test plan

- [ ] CI: build / check-paths / check-windows / CodeQL
- [ ] Visual smoke: launch the app, open Settings → switch active theme
to "Default Light", confirm:
  - [ ] Panels invert correctly (light backgrounds, dark text)
- [ ] Spectrum widget renders with a white canvas, dark FFT trace, light
grid lines
  - [ ] Waterfall colormap unchanged (still black→…→hot)
  - [ ] All 8 slice indicators readable on the light spectrum
- [ ] Primary sliders (sub-page fill + light handle) still visible with
darker accent fill
  - [ ] Strip panels' amber-sub-page sliders still TX-coloured
- [ ] Bonus: switch back to "Default Dark" — every widget should snap
back without restart

73, Jeremy KK7GWY & Claude (AI dev partner)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
…+ Save As) (aethersdr#3130)

## Summary

First user-facing surface of the theming subsystem. Operators can now
edit any colour token in the active theme and ship a custom theme to
disk, all without touching a JSON file.

**View → Theme Editor…** opens a modeless dialog with:

- "Editing: \<active theme\>" header
- Filter bar — type-to-match against dotted token names (e.g.
\`accent\`, \`slice\`, \`meter\`)
- Sorted list of every colour token with a 16×16 swatch + hex value per
row
- Click row → \`QColorDialog\` opens seeded with the current value
- Accept → \`ThemeManager::setColor()\` → emits \`themeChanged\` → every
widget registered through \`applyStyleSheet\` repaints on the next
event-loop turn. **No restart**.
- "Save As…" prompts for a theme name and writes the current tokens to
\`~/.config/AetherSDR/themes/\<name\>.json\` via
\`saveCurrentThemeAs()\`. The new theme is registered + made active
immediately.

## ThemeManager additions

| Method | Purpose |
|---|---|
| \`allTokenKeys()\` | Sorted enumeration of every loaded token — used
by the editor to populate its list. |
| \`setColor(token, color)\` | In-memory mutation + themeChanged. Hex
round-trip so \`cssFragment()\` / \`resolve()\` pick up the new value
the same way they pick up loaded tokens. No-op on a same-value write. |
| \`setSizing(token, int)\` | Integer-token equivalent for panel padding
/ border widths. Not yet exposed in the editor UI; will land in PR 2
alongside font/sizing surface. |
| \`saveCurrentThemeAs(name)\` | Rebuilds nested JSON from the flat
dotted-key map (so on-disk shape matches bundled themes), writes to user
themes dir, registers + activates. Handles QString/int/double/bool
scalars + full \`ThemeGradient\` round-trip so the waterfall colormap
and slice.dim tokens persist correctly. |

## Explicitly NOT in this PR (deferred to Phase 5 PR 2-4)

1. **Inspector mode** (click-on-widget to find tokens that paint it).
\`ThemeManager::tokensForWidget()\` already exposes the reverse-map
populated by \`applyStyleSheet\`; wiring is a few hundred lines once we
settle on the latch/unlatch UX. Probably matches the AetherControl
double-click convention (aethersdr#3103).
2. **Gradient editing** for
\`waterfall.colormap.{default,grayscale,fire,…}\` and
\`slice.dim.{a..h}\`. The list filters them out via the
\`brush().gradient()\` probe so they don't render as
misleading-first-stop swatches.
3. **Font / sizing editing** — same Phase 5 PR 2 surface as the
inspector mode.
4. **Import** (file picker for arbitrary JSON), drag-and-drop, theme
deletion, theme renaming.

## Build verified clean

- [x] AetherSDR builds clean
- [x] All 320 targets including every test target

## Phase status

| Phase | Item | Status |
|---|---|---|
| 1 | ThemeManager scaffolding + JSON loader | ✅ |
| 2 | Gradient tokens + applyStyleSheet reverse-map + mass migration | ✅
|
| 3 | Paint-code migration + canonical-table closeout | ✅ |
| 4 | Default Light theme | ⏳ (aethersdr#3129, in review) |
| **5** | **Editor UI (PR 1: live colour editing + Save As)** | **this
PR** |

## Test plan

- [ ] CI: build / check-paths / check-windows / CodeQL
- [ ] Visual smoke:
  - [ ] View → Theme Editor… opens the modeless dialog
  - [ ] Title shows "Editing: Default Dark" (or whatever's active)
  - [ ] Filter bar — type \`accent\` → only accent.* rows visible
- [ ] Click an accent row → QColorDialog opens, pick a wild hue, accept
→ that colour appears immediately wherever \`color.accent\` paints (most
buttons, slider handles, badges)
  - [ ] Save As… → name it "Test", confirm:
    - File written to \`~/.config/AetherSDR/themes/Test.json\`
    - Title bar updates to "Editing: Test"
    - View → Theme submenu (from aethersdr#3129) now lists "Test"
- [ ] Switch back to Default Dark via the View menu → original colours
restored, custom edits gone (file on disk preserved)
- [ ] Reopen Test from the menu → custom edits restored from the saved
file

73, Jeremy KK7GWY & Claude (AI dev partner)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant