Skip to content

feat(theme): Phase 3 PR 2/5 — MeterSlider paint-code migration#3106

Merged
ten9876 merged 1 commit into
mainfrom
auto/theme-phase3-meters
May 25, 2026
Merged

feat(theme): Phase 3 PR 2/5 — MeterSlider paint-code migration#3106
ten9876 merged 1 commit into
mainfrom
auto/theme-phase3-meters

Conversation

@ten9876

@ten9876 ten9876 commented May 25, 2026

Copy link
Copy Markdown
Collaborator

Summary

PR 2 of 5 in the Phase 2/3 wrap-up sweep. First paint-code migration, demonstrating the pattern that the remaining meters / panadapter / waterfall PRs follow.

What changed

src/gui/MeterSlider.h — header-only widget used by TCI + DAX applets for the 4 RX channels + TX combined level/gain controls. 8 hardcoded QColor literals in its paintEvent now resolve through ThemeManager.

Pattern

  1. `#include "core/ThemeManager.h"`
  2. Connect `ThemeManager::themeChanged → update()` in ctor for live re-theme
  3. In paintEvent: resolve theme tokens once into locals at the top, use throughout
  4. Preserve alpha via `QColor::setAlpha(N)` on the resolved colour (translucent level-meter fills, gain-fill overlay)

Token map

Old hex New token Notes
`#0a0a18` (background) `color.background.0` meter chrome
`#1e2e3e` (border) `color.background.1` meter chrome
`#0080a0 alpha 120` (safe level) `color.accent.dim` α=120 3-stop heat
`#a0a020 alpha 120` (approaching peak) `color.accent.warning` α=120 visible shift: warmer amber
`#c03030 alpha 120` (clip) `color.accent.danger` α=120 3-stop heat
`#00b4d8` (thumb / accent fill) `color.accent` × 3 sites

The "approaching-peak" mid-range was the only intentional visual shift — old dim olive yellow → canonical bright amber. All other surfaces resolve to the same colour as before (or sub-perceptual canonicalisation shifts).

Verified locally

  • Linux build clean (7 affected translation units rebuild + link)
  • AetherSDR launches without crash (offscreen QPA smoke)
  • No visible regression in MeterSlider chrome

Out of scope (planned for PR 3-5)

  • ClientCompMeter / ClientLevelMeter / ClientEqOutputFader / ClientCompThresholdFader — all have file-scope `const QColor` initializations + paint-event literals. The file-scope pattern needs a different conversion approach (static-inline functions resolving at call time, not at static-init time before ThemeManager is ready). PR 3 batches the remaining meter widgets with spectrum / panadapter / curves.
  • Waterfall colormap — PR 4 (first real consumer of the gradient token from feat(theme): Phase 2 gradient token support (linear + radial) #3084).
  • Cleanup — file-scope-const pattern across remaining widgets, audit verification — PR 5.

73, Jeremy KK7GWY & Claude (AI dev partner)

🤖 Generated with Claude Code

First Phase 3 paint-code migration, demonstrating the pattern that
subsequent meter / panadapter / waterfall PRs follow.

MeterSlider (header-only widget used by TCI + DAX applets for the 4
RX channels + TX combined level/gain controls) had 8 hardcoded QColor
literals in its paintEvent.  Each one now resolves through
ThemeManager:

  - background.0 / background.1 → meter chrome
  - color.accent → thumb line, triangle, gain fill
  - color.accent.dim / .warning / .danger → 3-stop level-meter heat
    (safe / approaching-peak / clip)

Pattern:
  1. #include "core/ThemeManager.h"
  2. Connect ThemeManager::themeChanged → update() in constructor for
     live re-theme on theme switch
  3. In paintEvent, resolve theme tokens once into local QColors at
     the top, then use them throughout the paint code
  4. Preserve alpha by calling QColor::setAlpha(N) on the resolved
     colour (translucent level-meter fills, gain-fill overlay)

The "no-canonical-yellow-warning" decision: the prior hardcoded mid-
range fill colour was QColor(0xa0, 0xa0, 0x20, 120) — a custom dim
yellow that doesn't match any canonical token exactly.  Mapped to
color.accent.warning which is canonical bright amber #ffb84d at alpha
120.  Slight visual shift (warmer/brighter mid-range than before) —
acceptable; Phase 5 editor will let users tune.

Verified: Linux build clean (7 affected translation units rebuilt and
linked), AetherSDR launches without crash.  Visible diff: minor —
level meter approaching-peak band shifts from dim olive yellow to
brighter amber.  All other meter chrome resolves to the same colours
as before (or sub-perceptual shifts where canonical tokens absorbed
close variants).

Out of scope for this PR (planned for PR 3-5 of the sweep):
  - ClientCompMeter / ClientLevelMeter / ClientEqOutputFader /
    ClientCompThresholdFader — all have file-scope const QColor
    initializations + paint-event literals; the file-scope pattern
    needs a different conversion approach (static-inline functions
    that resolve at call time, not at static-init time before
    ThemeManager is ready).  PR 3 batches the remaining meter
    widgets with the spectrum / panadapter / curve widgets.

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 04:36
@ten9876 ten9876 merged commit a1ff1a9 into main May 25, 2026
5 checks passed
@ten9876 ten9876 deleted the auto/theme-phase3-meters branch May 25, 2026 05:12
ten9876 added a commit that referenced this pull request May 25, 2026
…migration, canonical table closeout (#3117)

## Summary

**PR 5 of 5** in the Phase 2/3 wrap-up — closes the mechanical migration
sweep. 159 \`QColor\` literals migrated across 11 widget files via the
now-mature \`migrate_paint_colours.py\` tool, plus a closing 50-entry
canonical-table expansion.

## Files migrated (11)

| File | Literals | Notes |
|---|---|---|
| \`SpectrumWidget\` | 40 | Residual paint code from #3113 |
| \`ClientChainWidget\` | 20 | |
| \`StripChainWidget\` | 20 | |
| \`ClientRxChainWidget\` | 16 | |
| \`StripRxChainWidget\` | 16 | |
| \`WaveformWidget\` | 15 | rx + strip-mode share the same palette |
| \`StripWaveform\` | 15 | |
| \`KeyboardMapWidget\` | 8 | 10 \`categoryColor()\` returns left as-is
(see Skips) |
| \`ClientCompThresholdFader\` | 5 | Meter gradient stops deferred from
#3116 |
| \`ClientCompEditorCanvas\` | 3 | |
| \`ClientGateCurveWidget\` | 1 | Translucent cyan fill deferred from
#3116 |

## Canonical-table additions — 50 new close-mappings

Three logical groups in \`tools/migrate_colours.py\`, each entry
annotated with its sibling canonical value and ΔRGB so the rationale is
auditable later:

\`\`\`python
# Spectrum paint code (25)
\"#80d0ff\": \"color.accent.bright\",        # peak hold light cyan
\"#ff5858\": \"color.accent.danger\",        # bright alert red
\"#ffc040\": \"color.accent.warning\", # warning amber, ΔRGB≈0 from
#ffb84d
# … 22 more

# Chain / waveform (11)
\"#587890\": \"color.text.label\",           # waveform centerline
\"#ffd166\": \"color.accent.warning\", # peak-hold, ΔRGB≈1 from #ffd070
# … 9 more

# Threshold fader / gate gradient stops (6)
\"#2f9e6a\": \"color.accent.success\",       # comp meter lo green
\"#f2362a\": \"color.accent.danger\",        # comp meter peak red
\"#50b4dc\": \"color.accent.dim\", # gate curve translucent cyan
# … 3 more
\`\`\`

## Build verified clean

- [x] AetherSDR builds clean
- [x] All 308 targets build clean (every test target)
- Two warnings surface (\`sliceColor\` / \`effectiveGridStepMhz\`
unused) — both pre-existing on \`main\`, confirmed via local stash
bisect

## Intentional skips (deferred to Phase 4)

1. **10 \`KeyboardMapWidget::categoryColor()\` returns** — semantically
distinct hues for Frequency / TX / Audio / DSP / etc. categories. No
shared visual language applies; converting would homogenize them and
defeat the categorization. Left as raw \`QColor\`.

2. **1 \`ClientCompMeter\` no-go-zone tint (\`#3a1810\`)** — unique
meter visual, no canonical equivalent worth absorbing.

3. **Slice indicator runtime theming** — \`SliceColorManager\` still
reads from the compile-time \`kSliceColors[]\` table in
\`SliceColors.h\`. Default-dark.json already has \`color.slice.a\`
through \`.h\` tokens, but wiring \`SliceColorManager\` to read them
requires touching \`SpectrumWidget\` overlay rendering + \`VfoWidget\`
badges + \`RxApplet\`. Warrants its own PR.

4. **Waterfall colormap runtime theming** — \`color.waterfall.colormap\`
gradient token is already shipped in default-dark.json (validating
gradient-token support added in Phase 2), but the \`SpectrumWidget\`
waterfall renderer hard-codes its colormap stop set. Conversion warrants
its own PR.

## Phase 2/3 closeout

This closes the **5-PR Phase 2/3 wrap-up** as constrained. Recap:

| PR | What | Status |
|---|---|---|
| #3102 | Mass migration + token taxonomy | merged |
| #3106 | MeterSlider pilot (Phase 3 PR 2/5) | merged |
| #3113 | Paint-color tool + SpectrumWidget (PR 3/5) | merged |
| #3116 | Comp/gate/curve widgets (PR 4/5) | merged |
| **#XXXX** | **Spectrum + chain + waveform closeout (PR 5/5)** | **this
PR** |

Phase 4 picks up: default-light theme + slice indicator runtime theming
+ waterfall colormap runtime theming.

## Test plan

- [ ] CI: build / check-paths / check-windows / CodeQL
- [ ] Visual smoke: open the app and confirm the spectrum + chain +
waveform widgets render to-within-ΔRGB-of-50 of current main (the 159
migrated literals resolve to canonical token values that fall within
audited deviation thresholds)

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
… Theme.h helper (#3118)

## Summary

Pre-consolidation the codebase had **eight near-duplicate \`QSlider\`
stylesheet constants** scattered across the applet family. Same slider
widget, different look in three adjacent applets:

| Style | Files | Distinctive properties |
|---|---|---|
| A — Cyan handle | TxApplet, PhoneApplet, PhoneCwApplet, RxApplet |
groove \`#203040\`, handle 10×10 \`#00b4d8\`, no fill |
| B — Sub-page + bordered | WaveApplet | sub-page cyan, handle 12×12
light + cyan border |
| C — Hover-cyan | AetherDspWidget, DspParamPopup | groove \`#304050\`,
handle 12×12 light, hover→cyan |
| D — Plain light | VfoWidget | groove \`#1a2a3a\`, handle 12×12 light,
no hover |
| E — Themed cyan | PanadapterApplet (CW sens) | already token-based but
cyan-handle look |
| F — Themed amber | StripFinalOutputPanel, StripWaveformPanel |
sub-page amber for TX state |
| G — Tiny gray | PanadapterApplet (Lo/Hi range) | tiny gray handle —
**kept** as deliberate de-emphasis |
| H — Plain light variant | SpectrumOverlayMenu | yet another local
variant |

## The canonical look (per Jeremy's design call)

> Sub-page fill from Wave-style value indicator + Plain light handle
from Vfo-style.

\`\`\`
groove:    4px, {{color.background.1}}, radius 2px
sub-page:  {{color.accent}} (or caller-supplied token), radius 2px
handle: 12×12, {{color.text.primary}}, radius 6px — no border, no hover
\`\`\`

## API

Two helpers added to [Theme.h](src/gui/Theme.h):

\`\`\`cpp
namespace Theme {
QString primarySliderStyleTemplate(const QString& accentToken =
\"color.accent\");
    void    applyPrimarySliderStyle(QWidget* slider,
const QString& accentToken = \"color.accent\");
}
\`\`\`

\`applyPrimarySliderStyle()\` routes through
\`ThemeManager::applyStyleSheet()\` so each slider gets:
- Token-resolved colours
- **Free live re-theme** on theme change via the reverse-map
- Both horizontal and vertical orientation rules

Variant tokens — the two transmit-adjacent Strip panels pass
\`color.accent.warning\` for the amber sub-page that signals TX state.
Same shape, different colour token; no per-call-site stylesheet needed.

## Files migrated (13)

- \`Theme.h\` — new helpers + updated default \`QSlider\` rules in
\`appStylesheetTemplate()\`
- \`TxApplet\` / \`PhoneApplet\` / \`PhoneCwApplet\` / \`RxApplet\` —
Style A → canonical
- \`WaveApplet\` — Style B → canonical (handle simplified)
- \`AetherDspWidget\` / \`DspParamPopup\` — Style C → canonical (hover
state dropped)
- \`VfoWidget\` — Style D → canonical (now also has the sub-page
indicator)
- \`PanadapterApplet\` (CW sens slider only) — Style E → canonical
- \`SpectrumOverlayMenu\` — both local Style H variants → canonical
- \`StripFinalOutputPanel\` / \`StripWaveformPanel\` — Style F →
canonical with \`color.accent.warning\` amber

## Intentionally untouched (2)

- **\`PanadapterApplet\` Lo/Hi range sliders** — Style G is a deliberate
visual de-emphasis for secondary range controls; the tiny gray handle is
a load-bearing visual cue, not drift.
- **\`MeterSlider\`** — already its own widget for VU/level meters with
its own canonical style (#3106).

## Net code reduction

| | |
|---|---|
| Lines added | 108 |
| Lines removed | 134 |
| Net | **-26 lines** |
| kSliderStyle constants deleted | 8 |
| QSlider rules extracted out of dialog/widget mega-stylesheets | 3 |

## Build verified clean

- [x] AetherSDR builds clean (615 ninja steps, no errors)
- [x] All 320 targets including every test target

## Phase 4 readiness

When the default-light theme ships, slider colours adapt automatically —
every site reads through the same three tokens (\`color.background.1\`,
\`color.accent\`/\`color.accent.warning\`, \`color.text.primary\`). No
follow-up sweep needed.

## Test plan

- [ ] CI: build / check-paths / check-windows / CodeQL
- [ ] Visual smoke: confirm the TX Controls, Wave, RX, Phone, CW, Vfo,
AetherDSP, Quindar editor, and Test Tone dialog all render the same
canonical slider; confirm StripFinalOutput + StripWaveform sliders
render with amber sub-page fill (TX-state visual)

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
…rsdr#3106)

## Summary

**PR 2 of 5** in the Phase 2/3 wrap-up sweep. First paint-code
migration, demonstrating the pattern that the remaining meters /
panadapter / waterfall PRs follow.

## What changed

[src/gui/MeterSlider.h](src/gui/MeterSlider.h) — header-only widget used
by TCI + DAX applets for the 4 RX channels + TX combined level/gain
controls. 8 hardcoded QColor literals in its paintEvent now resolve
through ThemeManager.

## Pattern

1. \`#include "core/ThemeManager.h"\`
2. Connect \`ThemeManager::themeChanged → update()\` in ctor for live
re-theme
3. In paintEvent: resolve theme tokens once into locals at the top, use
throughout
4. Preserve alpha via \`QColor::setAlpha(N)\` on the resolved colour
(translucent level-meter fills, gain-fill overlay)

## Token map

| Old hex | New token | Notes |
|---|---|---|
| \`#0a0a18\` (background) | \`color.background.0\` | meter chrome |
| \`#1e2e3e\` (border) | \`color.background.1\` | meter chrome |
| \`#0080a0 alpha 120\` (safe level) | \`color.accent.dim\` α=120 |
3-stop heat |
| \`#a0a020 alpha 120\` (approaching peak) | \`color.accent.warning\`
α=120 | **visible shift: warmer amber** |
| \`#c03030 alpha 120\` (clip) | \`color.accent.danger\` α=120 | 3-stop
heat |
| \`#00b4d8\` (thumb / accent fill) | \`color.accent\` | × 3 sites |

The "approaching-peak" mid-range was the only intentional visual shift —
old dim olive yellow → canonical bright amber. All other surfaces
resolve to the same colour as before (or sub-perceptual canonicalisation
shifts).

## Verified locally

- [x] Linux build clean (7 affected translation units rebuild + link)
- [x] AetherSDR launches without crash (offscreen QPA smoke)
- [x] No visible regression in MeterSlider chrome

## Out of scope (planned for PR 3-5)

- **ClientCompMeter / ClientLevelMeter / ClientEqOutputFader /
ClientCompThresholdFader** — all have **file-scope \`const QColor\`
initializations** + paint-event literals. The file-scope pattern needs a
different conversion approach (static-inline functions resolving at call
time, not at static-init time before ThemeManager is ready). PR 3
batches the remaining meter widgets with spectrum / panadapter / curves.
- **Waterfall colormap** — PR 4 (first real consumer of the gradient
token from aethersdr#3084).
- **Cleanup** — file-scope-const pattern across remaining widgets, audit
verification — PR 5.

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
…migration, canonical table closeout (aethersdr#3117)

## Summary

**PR 5 of 5** in the Phase 2/3 wrap-up — closes the mechanical migration
sweep. 159 \`QColor\` literals migrated across 11 widget files via the
now-mature \`migrate_paint_colours.py\` tool, plus a closing 50-entry
canonical-table expansion.

## Files migrated (11)

| File | Literals | Notes |
|---|---|---|
| \`SpectrumWidget\` | 40 | Residual paint code from aethersdr#3113 |
| \`ClientChainWidget\` | 20 | |
| \`StripChainWidget\` | 20 | |
| \`ClientRxChainWidget\` | 16 | |
| \`StripRxChainWidget\` | 16 | |
| \`WaveformWidget\` | 15 | rx + strip-mode share the same palette |
| \`StripWaveform\` | 15 | |
| \`KeyboardMapWidget\` | 8 | 10 \`categoryColor()\` returns left as-is
(see Skips) |
| \`ClientCompThresholdFader\` | 5 | Meter gradient stops deferred from
aethersdr#3116 |
| \`ClientCompEditorCanvas\` | 3 | |
| \`ClientGateCurveWidget\` | 1 | Translucent cyan fill deferred from
aethersdr#3116 |

## Canonical-table additions — 50 new close-mappings

Three logical groups in \`tools/migrate_colours.py\`, each entry
annotated with its sibling canonical value and ΔRGB so the rationale is
auditable later:

\`\`\`python
# Spectrum paint code (25)
\"#80d0ff\": \"color.accent.bright\",        # peak hold light cyan
\"#ff5858\": \"color.accent.danger\",        # bright alert red
\"#ffc040\": \"color.accent.warning\", # warning amber, ΔRGB≈0 from
#ffb84d
# … 22 more

# Chain / waveform (11)
\"#587890\": \"color.text.label\",           # waveform centerline
\"#ffd166\": \"color.accent.warning\", # peak-hold, ΔRGB≈1 from #ffd070
# … 9 more

# Threshold fader / gate gradient stops (6)
\"#2f9e6a\": \"color.accent.success\",       # comp meter lo green
\"#f2362a\": \"color.accent.danger\",        # comp meter peak red
\"#50b4dc\": \"color.accent.dim\", # gate curve translucent cyan
# … 3 more
\`\`\`

## Build verified clean

- [x] AetherSDR builds clean
- [x] All 308 targets build clean (every test target)
- Two warnings surface (\`sliceColor\` / \`effectiveGridStepMhz\`
unused) — both pre-existing on \`main\`, confirmed via local stash
bisect

## Intentional skips (deferred to Phase 4)

1. **10 \`KeyboardMapWidget::categoryColor()\` returns** — semantically
distinct hues for Frequency / TX / Audio / DSP / etc. categories. No
shared visual language applies; converting would homogenize them and
defeat the categorization. Left as raw \`QColor\`.

2. **1 \`ClientCompMeter\` no-go-zone tint (\`#3a1810\`)** — unique
meter visual, no canonical equivalent worth absorbing.

3. **Slice indicator runtime theming** — \`SliceColorManager\` still
reads from the compile-time \`kSliceColors[]\` table in
\`SliceColors.h\`. Default-dark.json already has \`color.slice.a\`
through \`.h\` tokens, but wiring \`SliceColorManager\` to read them
requires touching \`SpectrumWidget\` overlay rendering + \`VfoWidget\`
badges + \`RxApplet\`. Warrants its own PR.

4. **Waterfall colormap runtime theming** — \`color.waterfall.colormap\`
gradient token is already shipped in default-dark.json (validating
gradient-token support added in Phase 2), but the \`SpectrumWidget\`
waterfall renderer hard-codes its colormap stop set. Conversion warrants
its own PR.

## Phase 2/3 closeout

This closes the **5-PR Phase 2/3 wrap-up** as constrained. Recap:

| PR | What | Status |
|---|---|---|
| aethersdr#3102 | Mass migration + token taxonomy | merged |
| aethersdr#3106 | MeterSlider pilot (Phase 3 PR 2/5) | merged |
| aethersdr#3113 | Paint-color tool + SpectrumWidget (PR 3/5) | merged |
| aethersdr#3116 | Comp/gate/curve widgets (PR 4/5) | merged |
| **#XXXX** | **Spectrum + chain + waveform closeout (PR 5/5)** | **this
PR** |

Phase 4 picks up: default-light theme + slice indicator runtime theming
+ waterfall colormap runtime theming.

## Test plan

- [ ] CI: build / check-paths / check-windows / CodeQL
- [ ] Visual smoke: open the app and confirm the spectrum + chain +
waveform widgets render to-within-ΔRGB-of-50 of current main (the 159
migrated literals resolve to canonical token values that fall within
audited deviation thresholds)

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
… Theme.h helper (aethersdr#3118)

## Summary

Pre-consolidation the codebase had **eight near-duplicate \`QSlider\`
stylesheet constants** scattered across the applet family. Same slider
widget, different look in three adjacent applets:

| Style | Files | Distinctive properties |
|---|---|---|
| A — Cyan handle | TxApplet, PhoneApplet, PhoneCwApplet, RxApplet |
groove \`#203040\`, handle 10×10 \`#00b4d8\`, no fill |
| B — Sub-page + bordered | WaveApplet | sub-page cyan, handle 12×12
light + cyan border |
| C — Hover-cyan | AetherDspWidget, DspParamPopup | groove \`#304050\`,
handle 12×12 light, hover→cyan |
| D — Plain light | VfoWidget | groove \`#1a2a3a\`, handle 12×12 light,
no hover |
| E — Themed cyan | PanadapterApplet (CW sens) | already token-based but
cyan-handle look |
| F — Themed amber | StripFinalOutputPanel, StripWaveformPanel |
sub-page amber for TX state |
| G — Tiny gray | PanadapterApplet (Lo/Hi range) | tiny gray handle —
**kept** as deliberate de-emphasis |
| H — Plain light variant | SpectrumOverlayMenu | yet another local
variant |

## The canonical look (per Jeremy's design call)

> Sub-page fill from Wave-style value indicator + Plain light handle
from Vfo-style.

\`\`\`
groove:    4px, {{color.background.1}}, radius 2px
sub-page:  {{color.accent}} (or caller-supplied token), radius 2px
handle: 12×12, {{color.text.primary}}, radius 6px — no border, no hover
\`\`\`

## API

Two helpers added to [Theme.h](src/gui/Theme.h):

\`\`\`cpp
namespace Theme {
QString primarySliderStyleTemplate(const QString& accentToken =
\"color.accent\");
    void    applyPrimarySliderStyle(QWidget* slider,
const QString& accentToken = \"color.accent\");
}
\`\`\`

\`applyPrimarySliderStyle()\` routes through
\`ThemeManager::applyStyleSheet()\` so each slider gets:
- Token-resolved colours
- **Free live re-theme** on theme change via the reverse-map
- Both horizontal and vertical orientation rules

Variant tokens — the two transmit-adjacent Strip panels pass
\`color.accent.warning\` for the amber sub-page that signals TX state.
Same shape, different colour token; no per-call-site stylesheet needed.

## Files migrated (13)

- \`Theme.h\` — new helpers + updated default \`QSlider\` rules in
\`appStylesheetTemplate()\`
- \`TxApplet\` / \`PhoneApplet\` / \`PhoneCwApplet\` / \`RxApplet\` —
Style A → canonical
- \`WaveApplet\` — Style B → canonical (handle simplified)
- \`AetherDspWidget\` / \`DspParamPopup\` — Style C → canonical (hover
state dropped)
- \`VfoWidget\` — Style D → canonical (now also has the sub-page
indicator)
- \`PanadapterApplet\` (CW sens slider only) — Style E → canonical
- \`SpectrumOverlayMenu\` — both local Style H variants → canonical
- \`StripFinalOutputPanel\` / \`StripWaveformPanel\` — Style F →
canonical with \`color.accent.warning\` amber

## Intentionally untouched (2)

- **\`PanadapterApplet\` Lo/Hi range sliders** — Style G is a deliberate
visual de-emphasis for secondary range controls; the tiny gray handle is
a load-bearing visual cue, not drift.
- **\`MeterSlider\`** — already its own widget for VU/level meters with
its own canonical style (aethersdr#3106).

## Net code reduction

| | |
|---|---|
| Lines added | 108 |
| Lines removed | 134 |
| Net | **-26 lines** |
| kSliderStyle constants deleted | 8 |
| QSlider rules extracted out of dialog/widget mega-stylesheets | 3 |

## Build verified clean

- [x] AetherSDR builds clean (615 ninja steps, no errors)
- [x] All 320 targets including every test target

## Phase 4 readiness

When the default-light theme ships, slider colours adapt automatically —
every site reads through the same three tokens (\`color.background.1\`,
\`color.accent\`/\`color.accent.warning\`, \`color.text.primary\`). No
follow-up sweep needed.

## Test plan

- [ ] CI: build / check-paths / check-windows / CodeQL
- [ ] Visual smoke: confirm the TX Controls, Wave, RX, Phone, CW, Vfo,
AetherDSP, Quindar editor, and Test Tone dialog all render the same
canonical slider; confirm StripFinalOutput + StripWaveform sliders
render with amber sub-page fill (TX-state visual)

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