Skip to content

feat(theme): slice indicator runtime theming via ThemeManager#3121

Merged
ten9876 merged 1 commit into
mainfrom
auto/slice-runtime-theming
May 25, 2026
Merged

feat(theme): slice indicator runtime theming via ThemeManager#3121
ten9876 merged 1 commit into
mainfrom
auto/slice-runtime-theming

Conversation

@ten9876

@ten9876 ten9876 commented May 25, 2026

Copy link
Copy Markdown
Collaborator

Summary

Wires the per-slice colour catalogue through `ThemeManager` so a theme switch repaints the slice markers, VFO badges, and rx headers with the new palette. Closes the second of the three deferred items from #3117's Phase 3 closeout (the other two: default-light theme, waterfall colormap runtime theming).

What changed

The 8-slot per-slice palette (A=cyan, B=magenta, C=green, D=yellow, E=orange, F=teal, G=coral, H=lavender) used to live in a compile-time `SliceColorEntry kSliceColors[]` table. `default-dark.json` had a `color.slice.{a..h}` block but nothing read from it — the JSON values were stale (different hues than the actual code) since they were never wired through.

This commit makes the theme JSON authoritative:

`resources/themes/default-dark.json`

  • Slice tokens updated to the hex values currently shipped via `kSliceColors[]` (cyan / magenta / green / yellow / orange / teal / coral / lavender — visually identical to what users see today).
  • New nested `color.slice.dim.{a..h}` block carrying the dim-variant hex values used by inactive-slice rendering.

`src/gui/SliceColorManager.cpp`

  • `defaultActive(idx)` and `defaultDim(idx)` now resolve through `ThemeManager::color()` against `color.slice.{letter}` / `color.slice.dim.{letter}`.
  • The singleton's constructor subscribes to `ThemeManager::themeChanged`, rebuilds the hex cache, and re-emits `colorsChanged()` so every listener (panadapter slice markers, VFO badges, RxApplet header) repaints with the new palette.

`src/gui/SliceColors.h`

  • `SliceColorEntry` and `kSliceColors[]` deleted (no remaining consumers).
  • `kSliceColorCount = 8` constant retained — `SpectrumWidget`, `SliceColorManager`, and `RadioSetupDialog` still use it to size loops and arrays.
  • Header comment now points readers at the theme JSON for the actual values.

Visual continuity

The new JSON values are identical to the previously-compile-time defaults, so the dark theme renders the same slice palette as before. Only the source of truth has moved.

Custom-colour users

Unaffected — `m_customColors` and the persisted `SliceColor{0..7}` app-settings paths still work as before. The theme change refreshes `m_hexCache` so anything reading `hexActive()` gets the new active hue when the per-user override is off.

Build verified clean

  • AetherSDR builds clean (236 ninja steps)
  • All 320 targets including every test target

Phase 4 readiness

Once the default-light theme ships, it can define a brighter slice palette appropriate for daylight viewing (or just use desaturated variants of the dark theme). Operators see slice colours change live without restart. The remaining deferred item from #3117 (waterfall colormap runtime theming) is independent and can land separately.

Test plan

  • CI: build / check-paths / check-windows / CodeQL
  • Visual smoke: open a panadapter with multiple slices and confirm slice colours render identically to current main (sub-perceptual or zero delta — the JSON values match kSliceColors exactly)
  • Verify dim/active states still look right when toggling slice activation

73, Jeremy KK7GWY & Claude (AI dev partner)

🤖 Generated with Claude Code

The 8-slot per-slice colour catalogue (A=cyan, B=magenta, C=green, …)
used by SpectrumWidget overlays, VfoWidget badges, and the RxApplet
header lived in a compile-time SliceColorEntry kSliceColors[] table.
The default-dark.json had a `color.slice.{a..h}` block but nothing
read from it — the JSON values were stale (different hues than the
actual code) since they were never wired through.

This commit makes the theme JSON authoritative:

  * `resources/themes/default-dark.json` — slice tokens updated to
    the hex values currently shipped via kSliceColors[], plus a new
    `slice.dim.{a..h}` nested block carrying the dim variants used
    by inactive-slice rendering.

  * `src/gui/SliceColorManager.cpp` — `defaultActive(idx)` and
    `defaultDim(idx)` now resolve through `ThemeManager::color()`
    against `color.slice.{letter}` and `color.slice.dim.{letter}`.
    The singleton's constructor subscribes to `ThemeManager::themeChanged`
    and re-emits `colorsChanged()` after a cache rebuild so every
    listener (panadapter slice markers, VFO badges, the rx header)
    repaints with the new palette when the user switches themes.

  * `src/gui/SliceColors.h` — `SliceColorEntry` and `kSliceColors[]`
    deleted; only the `kSliceColorCount = 8` constant is retained
    because SpectrumWidget / SliceColorManager / RadioSetupDialog
    still use it to size loops and arrays.  The header's comment now
    points readers at the theme JSON for the actual values.

Visual continuity: the new JSON values are identical to the
previously-compile-time defaults, so the dark theme renders the same
slice palette as before.  Only the source of truth has moved.

Custom-colour users are unaffected — `m_customColors` and the persisted
SliceColor{0..7} app-settings paths still work as before; the theme
change just refreshes the m_hexCache so anything reading hexActive()
gets the new active hue when the override is off.

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 07:21
@ten9876 ten9876 merged commit 0f8d434 into main May 25, 2026
4 checks passed
@ten9876 ten9876 deleted the auto/slice-runtime-theming branch May 25, 2026 07:45
ten9876 added a commit that referenced this pull request May 25, 2026
)

## Summary

Closes the third (and last mechanical) deferred item from the Phase 3
closeout. All five waterfall colormap presets exposed via the Spectrum
Overlay → Display → \`Scheme:\` dropdown (**Default / Grayscale /
Blue-Green / Fire / Plasma**) now resolve through \`ThemeManager\`
against named gradient tokens.

## Why all five (not one)

The user-facing dropdown stays — operators still pick from the same five
presets and the choice still drives which colormap paints. **What
changed is the *content* of each preset**: a future light theme can ship
a brighter \"Default\" for daylight viewing, redesign \"Fire\" with a
softer stop set, etc., entirely from JSON without touching code.

## JSON shape

The previous flat \`color.waterfall.colormap\` gradient (never wired up
— its stops had drifted from the actual compile-time defaults) is
replaced by a nested object with five named gradient sub-tokens:

\`\`\`
color.waterfall.colormap.default     (7 stops, black→…→red)
color.waterfall.colormap.grayscale   (2 stops, black→white)
color.waterfall.colormap.blueGreen (5 stops,
black→blue→teal→green→white)
color.waterfall.colormap.fire (5 stops, black→red→orange→yellow→white)
color.waterfall.colormap.plasma (5 stops,
black→purple→magenta→orange→yellow)
\`\`\`

\`ThemeManager::flattenTokens()\` already routes inner objects through
\`parseGradient()\` whenever they carry the \`type\` discriminator, so
each sub-token resolves through \`brush()\` / \`cssFragment()\` like any
other gradient.

## C++ wiring — \`src/gui/SpectrumWidget.cpp\`

A file-static \`WfStopsCache\` holds five
\`std::vector<WfGradientStop>\` — the converted form of each named
gradient's stop list. Lazy-populated on first \`wfSchemeStops()\` call
so the function works during early static-init sequences where the
SpectrumWidget hasn't been constructed yet.

SpectrumWidget's constructor connects to \`ThemeManager::themeChanged\`:

1. Rebuild the cache from the new theme's gradient tokens.
2. Mark the static overlay dirty.
3. \`update()\`.

**Already-rendered waterfall rows keep their pre-switch colours** — same
behaviour as changing the \`Scheme:\` dropdown today, since recolouring
the history image would force an O(rows × cols) repaint we don't
currently amortise. New rows pick up the new palette on the next push.

The five compile-time \`kDefaultStops\` / \`kGrayscaleStops\` / etc.
tables are deleted; JSON is now the source of truth.

## Visual continuity

Default-dark's stop hex values mirror the deleted compile-time tables
exactly, so the dark theme renders the same waterfall across all five
presets. Only the source of truth has moved.

## Build verified clean

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

## Phase 3/4 status

| Item | Status |
|---|---|
| Slice indicators runtime themed | ✅ #3121 |
| Waterfall colormap runtime themed | ✅ this PR |
| default-light theme | ⏳ last Phase 4 milestone |

After this lands, every load-bearing colour decision in the GUI flows
through the token system. Default-light becomes a pure JSON authoring
exercise (plus a contrast pass).

## Test plan

- [ ] CI: build / check-paths / check-windows / CodeQL
- [ ] Visual smoke: open the Spectrum Overlay → Display tab and cycle
through all 5 colormap presets, confirm each renders identically to
current main on default-dark
- [ ] Bonus: drop a custom theme JSON into
\`~/.config/AetherSDR/themes/\` with a re-coloured
\`color.waterfall.colormap.fire\` to confirm theme switching repaints
new rows with the override

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>
ten9876 added a commit that referenced this pull request May 25, 2026
## 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 | #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](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
…sdr#3121)

## Summary

Wires the per-slice colour catalogue through \`ThemeManager\` so a theme
switch repaints the slice markers, VFO badges, and rx headers with the
new palette. Closes the second of the three deferred items from aethersdr#3117's
Phase 3 closeout (the other two: default-light theme, waterfall colormap
runtime theming).

## What changed

The 8-slot per-slice palette (A=cyan, B=magenta, C=green, D=yellow,
E=orange, F=teal, G=coral, H=lavender) used to live in a compile-time
\`SliceColorEntry kSliceColors[]\` table. \`default-dark.json\` had a
\`color.slice.{a..h}\` block but **nothing read from it** — the JSON
values were stale (different hues than the actual code) since they were
never wired through.

This commit makes the theme JSON authoritative:

### \`resources/themes/default-dark.json\`

- Slice tokens updated to the hex values currently shipped via
\`kSliceColors[]\` (cyan / magenta / green / yellow / orange / teal /
coral / lavender — visually identical to what users see today).
- New nested \`color.slice.dim.{a..h}\` block carrying the dim-variant
hex values used by inactive-slice rendering.

### \`src/gui/SliceColorManager.cpp\`

- \`defaultActive(idx)\` and \`defaultDim(idx)\` now resolve through
\`ThemeManager::color()\` against \`color.slice.{letter}\` /
\`color.slice.dim.{letter}\`.
- The singleton's constructor subscribes to
\`ThemeManager::themeChanged\`, rebuilds the hex cache, and re-emits
\`colorsChanged()\` so every listener (panadapter slice markers, VFO
badges, RxApplet header) repaints with the new palette.

### \`src/gui/SliceColors.h\`

- \`SliceColorEntry\` and \`kSliceColors[]\` deleted (no remaining
consumers).
- \`kSliceColorCount = 8\` constant retained — \`SpectrumWidget\`,
\`SliceColorManager\`, and \`RadioSetupDialog\` still use it to size
loops and arrays.
- Header comment now points readers at the theme JSON for the actual
values.

## Visual continuity

The new JSON values are **identical** to the previously-compile-time
defaults, so the dark theme renders the same slice palette as before.
Only the source of truth has moved.

## Custom-colour users

Unaffected — \`m_customColors\` and the persisted \`SliceColor{0..7}\`
app-settings paths still work as before. The theme change refreshes
\`m_hexCache\` so anything reading \`hexActive()\` gets the new active
hue when the per-user override is off.

## Build verified clean

- [x] AetherSDR builds clean (236 ninja steps)
- [x] All 320 targets including every test target

## Phase 4 readiness

Once the default-light theme ships, it can define a brighter slice
palette appropriate for daylight viewing (or just use desaturated
variants of the dark theme). Operators see slice colours change live
without restart. The remaining deferred item from aethersdr#3117 (waterfall
colormap runtime theming) is independent and can land separately.

## Test plan

- [ ] CI: build / check-paths / check-windows / CodeQL
- [ ] Visual smoke: open a panadapter with multiple slices and confirm
slice colours render identically to current main (sub-perceptual or zero
delta — the JSON values match kSliceColors exactly)
- [ ] Verify dim/active states still look right when toggling slice
activation

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
…thersdr#3122)

## Summary

Closes the third (and last mechanical) deferred item from the Phase 3
closeout. All five waterfall colormap presets exposed via the Spectrum
Overlay → Display → \`Scheme:\` dropdown (**Default / Grayscale /
Blue-Green / Fire / Plasma**) now resolve through \`ThemeManager\`
against named gradient tokens.

## Why all five (not one)

The user-facing dropdown stays — operators still pick from the same five
presets and the choice still drives which colormap paints. **What
changed is the *content* of each preset**: a future light theme can ship
a brighter \"Default\" for daylight viewing, redesign \"Fire\" with a
softer stop set, etc., entirely from JSON without touching code.

## JSON shape

The previous flat \`color.waterfall.colormap\` gradient (never wired up
— its stops had drifted from the actual compile-time defaults) is
replaced by a nested object with five named gradient sub-tokens:

\`\`\`
color.waterfall.colormap.default     (7 stops, black→…→red)
color.waterfall.colormap.grayscale   (2 stops, black→white)
color.waterfall.colormap.blueGreen (5 stops,
black→blue→teal→green→white)
color.waterfall.colormap.fire (5 stops, black→red→orange→yellow→white)
color.waterfall.colormap.plasma (5 stops,
black→purple→magenta→orange→yellow)
\`\`\`

\`ThemeManager::flattenTokens()\` already routes inner objects through
\`parseGradient()\` whenever they carry the \`type\` discriminator, so
each sub-token resolves through \`brush()\` / \`cssFragment()\` like any
other gradient.

## C++ wiring — \`src/gui/SpectrumWidget.cpp\`

A file-static \`WfStopsCache\` holds five
\`std::vector<WfGradientStop>\` — the converted form of each named
gradient's stop list. Lazy-populated on first \`wfSchemeStops()\` call
so the function works during early static-init sequences where the
SpectrumWidget hasn't been constructed yet.

SpectrumWidget's constructor connects to \`ThemeManager::themeChanged\`:

1. Rebuild the cache from the new theme's gradient tokens.
2. Mark the static overlay dirty.
3. \`update()\`.

**Already-rendered waterfall rows keep their pre-switch colours** — same
behaviour as changing the \`Scheme:\` dropdown today, since recolouring
the history image would force an O(rows × cols) repaint we don't
currently amortise. New rows pick up the new palette on the next push.

The five compile-time \`kDefaultStops\` / \`kGrayscaleStops\` / etc.
tables are deleted; JSON is now the source of truth.

## Visual continuity

Default-dark's stop hex values mirror the deleted compile-time tables
exactly, so the dark theme renders the same waterfall across all five
presets. Only the source of truth has moved.

## Build verified clean

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

## Phase 3/4 status

| Item | Status |
|---|---|
| Slice indicators runtime themed | ✅ aethersdr#3121 |
| Waterfall colormap runtime themed | ✅ this PR |
| default-light theme | ⏳ last Phase 4 milestone |

After this lands, every load-bearing colour decision in the GUI flows
through the token system. Default-light becomes a pure JSON authoring
exercise (plus a contrast pass).

## Test plan

- [ ] CI: build / check-paths / check-windows / CodeQL
- [ ] Visual smoke: open the Spectrum Overlay → Display tab and cycle
through all 5 colormap presets, confirm each renders identically to
current main on default-dark
- [ ] Bonus: drop a custom theme JSON into
\`~/.config/AetherSDR/themes/\` with a re-coloured
\`color.waterfall.colormap.fire\` to confirm theme switching repaints
new rows with the override

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>
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