Skip to content

feat(theme): carve sliders + knobs into dedicated namespaces with per-applet overrides#3188

Merged
ten9876 merged 9 commits into
mainfrom
auto/theme-slider-knob-tokens
May 26, 2026
Merged

feat(theme): carve sliders + knobs into dedicated namespaces with per-applet overrides#3188
ten9876 merged 9 commits into
mainfrom
auto/theme-slider-knob-tokens

Conversation

@ten9876

@ten9876 ten9876 commented May 26, 2026

Copy link
Copy Markdown
Collaborator

First worked example of carving a control type out of the generic theme categories (color.accent / color.background.1 / color.text.primary) into dedicated namespaces with per-applet overrides through the v2 scope tree. Sets the pattern for follow-up sweeps (toggle buttons, spinboxes, progress bars, …).

Draft — opening early for visibility; more polish may layer on top.

What's in this PR

New token namespaces (12 entries)

color.slider.background           ← groove + add-page
color.slider.foreground           ← sub-page (filled portion)
color.slider.handle               ← thumb
color.slider.background.disabled
color.slider.foreground.disabled
color.slider.handle.disabled

color.knob.background             ← ring / track
color.knob.foreground             ← indicator arc
color.knob.handle                 ← center pointer / tick
color.knob.background.disabled
color.knob.foreground.disabled
color.knob.handle.disabled

State suffix (.disabled) matches existing color.text.disabled / color.accent.bright pattern. Hover / pressed deferred — explicit design call.

Per-applet cascade (visible result)

Bundled themes ship nested-scope overrides:

Scope color.slider.foreground & color.knob.foreground
applet/tx {color.red.500}
applet/rx {color.green.500}
applet/comp {color.amber.500}
(everywhere else) inherits root → {color.blue.500}

Open the TX applet → sliders + the comp knob (if a comp lives inside) render red. Open RX → green. Open the compressor applet → amber. All other surfaces stay the canonical blue.

The ParentChange event filter (general fix)

While building this, surfaced a latent bug: helpers like applyPrimarySliderStyle are commonly called before the widget is added to its parent layout (configure → applyStyle → addWidget). At apply time the widget has no parent, so containerPathFor walks nothing and resolves the QSS against root scope. The applet-scope override never reaches the widget.

Fix: ThemeManager now installs itself as an event filter on every widget tracked through applyStyleSheet. QEvent::ParentChange re-resolves the template against the now-correct scope chain. Cheap — only fires during initial construction / rare deliberate reparents.

This makes every helper that configures a widget pre-parent work correctly (applyAppTheme, applyPrimarySliderStyle, the dozens of inline applyStyleSheet sites in ctor bodies) without per-call-site changes.

seedBuiltinDefaults safety net

Older user themes (forked before this PR exists) get:

  1. Root-scope hex values for all 12 tokens (raw hex, no primitives dependency).
  2. applet/tx, applet/rx, applet/comp scope-tree seeds via scopeOrCreate() with the red/green/amber overrides.

Bundled themes' JSON re-asserts these via {color.red.500} aliases — idempotent, but the seed is what makes per-applet differentiation work on a "My Default Dark" forked last week.

Files touched

File Role
resources/themes/default-dark.json, default-light.json Token defs + nested scope overrides
src/core/ThemeManager.{h,cpp} seedBuiltinDefaults extension + ParentChange filter
src/gui/Theme.h Global QSS migration + applyPrimarySliderStyle default arg
src/gui/TitleBar.cpp Master + headphone sliders read from color.slider.*
src/gui/TxApplet.cpp / RxApplet.cpp / ClientCompApplet.cpp Applet-level slider QSS override (catches sliders not using the helper)
src/gui/ClientCompKnob.cpp Widget-aware paint-code migration to color.knob.*
docs/theming/slider-knob-tokens.md Full writeup of the namespace + cascade + ParentChange pattern

Out of scope (follow-ups)

  • PhaseKnob paint code still uses 6 hardcoded QColor literals — needs more design judgment than a rename.
  • Hardcoded-hex slider sites in PanadapterApplet, EqApplet, RadioSetupDialog, FlexControlDialog bypass the theme system entirely. Each needs its local stylesheet dropped in favour of the global QSS + per-applet override path.
  • Dialog-scope knob overrides — available today (namespace + scope tree both support it); nothing declared yet.

Test plan

  • Open TX applet — every slider's filled portion is red.
  • Open RX applet — every slider's filled portion is green.
  • Open the compressor applet — every knob arc + slider fill is amber.
  • Every other applet (DAX, EQ, Phone, …) renders the canonical blue fill from root scope.
  • Disable an AGC slider (or any slider in a disabled section) — groove/fill/handle shift to the muted disabled variants.
  • Open Theme Editor, pick scope applet/tx — the columnar view shows color.slider.foreground overridden at the tx column.
  • Inspector click on a slider surfaces the 6 color.slider.* entries; click on a comp knob surfaces the 6 color.knob.* entries.
  • Smoke-test on both Default Dark and Default Light.
  • Verify a user theme forked before this PR (e.g. "My Default Dark") still gets the per-applet colours via the seeded defaults.

🤖 Generated with Claude Code

ten9876 and others added 9 commits May 26, 2026 07:39
…s only)

First slice of carving slider and knob theming out of the generic
color.accent / color.background.1 / color.text.primary categories
they currently share with buttons, panels, body text, etc.  Adds 12
new tokens (6 per namespace) to both bundled themes, aliased to
existing primitives so the rendered output is bit-identical.

Slider tokens (Default Dark / Default Light):
  color.slider.background           = gray.800 / gray.700
  color.slider.foreground           = blue.500 / blue.500
  color.slider.handle               = gray.200 / gray.50
  color.slider.background.disabled  = gray.850 / gray.800
  color.slider.foreground.disabled  = gray.600 / gray.500
  color.slider.handle.disabled      = gray.500 / gray.400

Knob tokens (parallel namespace for ClientCompKnob + PhaseKnob):
  color.knob.background             = gray.700 / gray.700
  color.knob.foreground             = blue.500 / blue.500
  color.knob.handle                 = gray.200 / gray.50
  color.knob.background.disabled    = gray.800 / gray.800
  color.knob.foreground.disabled    = gray.600 / gray.500
  color.knob.handle.disabled        = gray.400 / gray.400

Suffix-as-state matches the existing `color.text.disabled` /
`color.accent.bright` pattern.  Editor groups them naturally in
alphabetical order in the token list.

No QSS migration yet — Theme.h still references the borrowed tokens.
Step 2 (slider QSS migration) and step 3 (knob paint-code migration)
follow in subsequent commits.  theme_manager_test still passes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ation)

Routes every QSlider rendering path through the new color.slider.*
namespace.  Rendered output unchanged — the new tokens alias to the
exact primitives the borrowed tokens (color.background.1, color.accent,
color.text.primary) point at, so the visual baseline is identical.
Designers can now retint sliders without rippling into buttons,
borders, body text, or other accent-consumers.

Two QSS surfaces migrated:

1. appStylesheetTemplate() in Theme.h — the global app-wide QSS.
   The previous version only had horizontal rules + no add-page
   handling.  Brings it up to the canonical shape:
     * Both orientations (h + v).
     * groove + sub-page + add-page + handle, each reading from
       color.slider.{background,foreground,background,handle}.
     * Disabled-state overrides via :disabled selectors that read
       from color.slider.*.disabled tokens.

2. primarySliderStyleTemplate() — the per-call-site helper used by
   slicePrimarySliderStyle and TX-warning sliders.  The accentToken
   parameter that lets call sites override the foreground (per-slice
   colours, TX amber) is preserved unchanged — anything that passes
   a custom token still wins.

Also migrated TitleBar's two inline slider stylesheets (master
volume + headphone) which previously hardcoded color.background.1 /
color.accent.

Out of scope here: PanadapterApplet, EqApplet, RadioSetupDialog, and
FlexControlDialog each have slider QSS that bypasses the theme system
with hardcoded hex (`#1a2a3a` / `#c8d8e8` / etc).  Migrating those is
a separate sweep — they need to drop their local stylesheets entirely
and trust the global QSS to paint them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…igration)

Routes ClientCompKnob's three knob-component colours (ring background,
indicator arc, pointer) through the new color.knob.* namespace.  The
two label/value text colours stay on color.text.* — those render text
below the knob, not knob componentry.

Used by ~10 sites (comp, gate, deess, pudu, reverb, tube, eq output,
phone, ...).  Per-applet container overrides (next commit) will
re-tint all of them in one go.

Knob defaults adjusted in both themes so the rendered output stays
bit-identical to pre-change:
  * color.knob.background: gray.700 → gray.800
    (matches color.background.1's hex — what ClientCompKnob used)
  * color.knob.foreground: blue.500 → blue.700
    (matches color.accent.dim — the indicator arc's previous tone)
  * Disabled variants tightened to match the new defaults' shade.

PhaseKnob deferred — its paintEvent hardcodes 6 raw QColor literals
(no tm.color() lookups at all), so migrating it requires more design
judgment than a rename pass.  Filed as a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First visible per-applet differentiation through the scope tree.
TX applet sliders + knobs render red, RX renders green, Comp renders
amber — every other slider / knob keeps the canonical blue baseline.

Bundled themes (both light + dark) gain nested-scope overrides under
`scopes.applet.scopes.{tx,rx,comp}`:
  * applet/tx:   color.slider.foreground = color.knob.foreground = {color.red.500}
  * applet/rx:                                                      {color.green.500}
  * applet/comp:                                                    {color.amber.500}

To make the QSS path scope-aware (the global QSS is resolved once at
the main-window scope, which is root), each of TxApplet / RxApplet /
ClientCompApplet now calls applyStyleSheet at its own constructor
with a slim slider override.  resolveFor walks the applet's scope
chain (applet/<name> → applet → root) and picks up the foreground
override; Qt's QSS cascade applies it to every QSlider descendant.

ClientCompKnob's color helpers gain a `const QWidget*` parameter and
route through ThemeManager's widget-aware overload, so the applet/comp
scope override naturally reaches the rendered output — no per-knob
applyStyleSheet plumbing needed because knobs are paint-code, not QSS.

Visual verification:
  1. Open TX applet — every slider's filled portion is red.
  2. Open RX applet — every slider's filled portion is green.
  3. Open the compressor applet — every knob arc is amber, sliders too.
  4. Every other applet (DAX, EQ, Phone, ...) renders the canonical
     blue-500 fill from root scope.
  5. Open Theme Editor, pick scope `applet/tx`, see the columnar
     view show color.slider.foreground overridden at `tx` column.

Closes the four-step plan for slider + knob theming.  PhaseKnob
migration (hardcoded raw QColor literals → tokens) is still
outstanding — separate sweep with design judgment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…in builtin defaults

The new color.slider.* / color.knob.* tokens weren't applying for users
whose active theme pre-dates this PR (e.g. a "My Default Dark" forked
before the namespace existed).  Their theme's JSON has no slider/knob
entries AND no nested-scope overrides, so the lookups returned empty
QSS — sliders fell back to Qt's default rendering instead of carrying
the new canonical look.

Two seeds added to seedBuiltinDefaults():

1. Root-scope colour seeds for the 12 new tokens.  Raw hex (not
   {alias} primitives) because older user themes may not have a
   primitives section.  Default Dark / Default Light's JSON
   re-asserts via aliases — idempotent.

2. Per-applet scope-tree seeds for applet/tx, applet/rx, applet/comp.
   Creates the scopes via scopeOrCreate() and inserts
   color.slider.foreground + color.knob.foreground overrides
   (red / green / amber).  Bundled themes' JSON re-asserts these
   too; user themes without them inherit the canonical TX-is-red /
   RX-is-green / comp-is-amber semantic.

Net effect: per-applet slider + knob colours now visible regardless
of whether the user is on a bundled theme or an older user-forked copy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ound

Step 2's QSS migration changed primarySliderStyleTemplate's default
arg but missed its wrapper applyPrimarySliderStyle() — which is what
~20+ call sites actually use.  The wrapper kept defaulting to
"color.accent" so every RxApplet / PhoneCwApplet / PhoneApplet /
WaveApplet slider was still resolving against the root-scope cyan,
ignoring the applet/* scope overrides.

Call sites that pass an explicit accentToken (StripFinalOutputPanel
passes color.accent.warning for TX-adjacent amber) keep working
unchanged — that path was already correct.

After this fix:
  * applyPrimarySliderStyle(slider) at any depth inside RxApplet
    resolves color.slider.foreground via the slider's container
    chain → walks to applet/rx → returns green.
  * Same for TxApplet (red) and ClientCompApplet (amber).
  * All other applets fall through to root scope → blue baseline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ange

The slider tokens were stored correctly and the editor confirmed the
applet/tx scope held the red override — but RF Power slider in the
TX applet still rendered cyan.  Root cause: applyPrimarySliderStyle
is called BEFORE the slider is added to its layout (common pattern
in TxApplet, RxApplet, PhoneCwApplet — configure, then addWidget).
At apply time the slider has no parent, so containerPathFor walks
nothing and resolves against root scope.

Fix: ThemeManager installs itself as an event filter on every
tracked widget.  When a QEvent::ParentChange fires, the widget's
template is re-resolved against the now-correct scope chain and
re-applied.  Cheap — ParentChange only fires during initial
construction (and on rare deliberate reparents like floating
panels).  Not a hot path.

Net effect: any helper that configures a widget pre-parent works
correctly — applyPrimarySliderStyle, applyAppTheme, and the dozens
of inline applyStyleSheet sites that happen during ctor bodies all
get their scope chain right once the widget is finally attached.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…pattern

Captures the full pattern from this PR so the next control-type
carve-out follows the same shape:

- The two new namespaces (color.slider.*, color.knob.*) with state
  suffix convention matching the existing color.text.disabled pattern.
- The cascade (root → applet → applet/<name>) and how it resolves at
  runtime, with the concrete TX-red / RX-green / Comp-amber example.
- seedBuiltinDefaults's role as a compile-time safety net for user
  themes that pre-date the namespace.
- The QSS migration surfaces (global QSS, applyPrimarySliderStyle
  helper, per-call-site applet stylesheets).
- The knob paint-code migration (widget-aware tm.color(w, token)
  overload routes the cascade naturally — no per-knob applyStyleSheet
  plumbing).
- The ParentChange event filter — the subtle bug we hit (helpers
  configure widgets pre-parent, resolution locks to root scope) and
  the generic fix (ThemeManager re-resolves on reparent for every
  tracked widget).

Includes a "pattern for adding new control namespaces" section as the
canonical recipe for follow-up sweeps (toggle buttons, spinboxes,
progress bars, ...).  Lists the four out-of-scope items still to do
(PhaseKnob, hardcoded-hex slider sites in 4 dialogs/applets,
dialog-scope knob overrides).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e-out

Addresses pre-merge review asks on #3188.

Three new test blocks in theme_manager_test:

1. Slider + knob tokens resolve to the canonical Wave-blue look on
   Default Dark — guards against the seedBuiltinDefaults coverage
   we added for forked-pre-PR user themes.

2. Per-applet scope cascade resolves correctly: applet/tx → red,
   applet/rx → green, applet/comp → amber, applet/dax → root blue.
   isOverriddenAt distinguishes "overridden here" vs "inherited".

3. ParentChange re-resolution: applyStyleSheet on a parentless
   QLabel resolves to root blue.  After setParent to a widget
   marked themeContainer="applet/tx", the event filter fires and
   re-resolves to applet/tx red.  This is the direct regression
   guard for the "still cyan" bug that surfaced mid-PR.

Code-side comment additions:

- loadThemeFromPath now documents the ordering dependency between
  m_rootScope->children.clear() and seedBuiltinDefaults() (the seeds
  re-create the per-applet scope children that clear() just wiped).

- seedBuiltinDefaults flags the hex-value duplication with the
  primitives palette in default-dark.json as KEEP IN SYNC, so a
  future palette shift updates both sites.

- applyStyleSheet's installEventFilter call carries a perf note: the
  early-out keeps per-event cost to one int compare, but with N
  tracked widgets the cumulative wakeups in drag / paint storms is
  N×events; gate to "no-parent-at-apply" if profiling flags it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ten9876 ten9876 marked this pull request as ready for review May 26, 2026 17:30
@ten9876 ten9876 requested review from a team as code owners May 26, 2026 17:30
@ten9876 ten9876 merged commit 5a56363 into main May 26, 2026
5 checks passed
@ten9876 ten9876 deleted the auto/theme-slider-knob-tokens branch May 26, 2026 17:30
ten9876 pushed a commit that referenced this pull request May 27, 2026
…dioSetup / FlexControl) (#3204)

Closes the second half of Pat's "both" answer to **(a)** the toggle
namespace work (landed as
[#3198](#3198)) and **(b)**
the hardcoded-hex slider sites called out in
[#3188](#3188 *Out of
scope* section.

## Background

[#3188](#3188) tagged four
sites as bypassing the `color.slider.*` namespace entirely:

> **Hardcoded-hex slider sites** in `PanadapterApplet`, `EqApplet`,
`RadioSetupDialog`, `FlexControlDialog` bypass the theme system
entirely. Each needs its local stylesheet dropped in favour of the
global QSS + per-applet override path.

Each site used inline `setStyleSheet(...)` calls full of hardcoded hex
values — so the v2 per-applet override path couldn't reach them, and
theme switches required an app restart. This PR tokenizes the colours
and routes through `ThemeManager::applyStyleSheet` so:

- the per-applet override cascade lights up automatically once a
relevant scope exists (none of the four currently sits under
`applet/tx|rx|comp`, so the immediate effect at root scope is the
canonical Wave-blue / accent-success treatment);
- live theme switching (Dark ↔ Light without restart) re-resolves the
tokens and repaints, matching the rest of the namespace-aware UI.

## Approach — Option A (tokenize colours, preserve sizes)

Site-local slider **dimensions** are preserved intentionally — the four
sites each chose bespoke handle widths / groove heights for the UX
context they live in (Panadapter's tiny 8px handle for the CW bar,
EqApplet's 10×16 vertical handle for band-column rhythm, RadioSetup's
emphasis-sized 14px handle, FlexControl's 16px bordered ring).

Homogenizing them via `applyPrimarySliderStyle` would have been a more
literal interpretation of "drop the local stylesheet" but would have
introduced visible regressions across all four sites. **Option A**
(tokenize colours, preserve sizes) achieves Pat's stated goal — sliders
honour the theme system — without the visual cost.

## Per-site notes

| File | Sliders | Token migration |
|---|---|---|
| `PanadapterApplet.cpp` | 2 pitch sliders (CW decoder bar) | `#1a2a3a`
groove → `{{color.slider.background}}`<br>`#6a8090` handle →
`{{color.slider.handle}}` |
| `EqApplet.cpp` | 8 vertical EQ band sliders | `#203040` groove →
`{{color.slider.background}}`<br>`#00b4d8` handle →
`{{color.slider.foreground}}` (the EQ idiom puts the accent colour on
the handle itself — no sub-page rule) |
| `RadioSetupDialog.cpp` | 3 filter-sharpness sliders (voice / cw /
digital) | `#1a2a3a` groove → `{{color.slider.background}}`<br>`#c8d8e8`
handle → `{{color.slider.handle}}`<br>(`lineoutSlider` + `hpSlider`
already inherit from the global QSS — no migration needed) |
| `FlexControlDialog.cpp` | 2 sliders (spin / sensitivity) inside the
dialog-wide `kFlexControlStyle` template | `#162437` groove →
`{{color.slider.background}}`<br>`#65d379` sub-page →
`{{color.accent.success}}` (preserves FlexControl green
identity)<br>`#d8e2ef` handle → `{{color.slider.handle}}`<br>`#65d379`
handle border → `{{color.accent.success}}`<br>Body widget switched to
`applyStyleSheet` so the tokens resolve + live re-theme registers |

## Verification

Windows 11 (MSVC + Ninja + Qt 6.10.3) on top of `aethersdr/AetherSDR`
main at `4ee99f78`.

| Site | Default Dark | Default Light |
|---|---|---|
| PanadapterApplet pitch sliders | ✓ dark groove + light handle | ✓
light groove + dark handle — **live re-theme adapts cleanly** |
| EqApplet band sliders | ✓ dark groove + accent-cyan handle | ✓ handle
resolves to light-theme accent (`#0088b0`) |
| RadioSetupDialog filter sliders | ✓ dark groove + light handle | ✓
light groove + dark handle — **live re-theme adapts cleanly** |
| FlexControlDialog spin + sensitivity | ✓ dark groove + green sub-page
+ bordered handle | ✓ sub-page green preserved (the green is
`color.accent.success` which is green in both bundled themes; gray
shades adapt around it) |

`theme_manager_test`: zero new failures. The 4 pre-existing
`importThemeFromFile`-related failures on clean main remain
(Windows-vs-CI env issue, not introduced here).

## Observed during testing — out of scope for this PR

While verifying in Default Light, two **broader migration debt** items
came to light that go beyond this slider sweep:

- **`RadioSetupDialog` light-theme contrast** — `kLabelStyle` (`color:
#c8d8e8`), `kValueStyle` (`color: #00c8ff`), `kGroupStyle` (border
`#304050`, title `#8aa8c0`), and `kEditStyle` (background `#1a2a3a`) all
hardcode dark-theme colours. Labels render as low-contrast pale text
against the light-theme `#f5f5f8` background, and edit fields look
misplaced. Migrating those constants needs the same `{{token}}` +
`applyStyleSheet` pattern and touches dozens of call sites across the
dialog — a separate follow-up PR.
- **`FlexControlDialog` whole-dialog migration debt** — the dialog
template hardcodes `#0b1625` button backgrounds, `#31455f` borders,
`#65d379` FlexControl-green accents throughout. The slider rules now
adapt to theme; the rest of the dialog stays dark in light theme. A
separate "FlexControl light-theme migration" PR would close this;
touching it here would have ballooned the slider-cleanup scope.

Both observations were flagged as candidates for the next cleanup sweep
that follows this one.

## Together with #3198

This PR completes Pat's "both" answer. With #3198 (toggle namespace
carve-out) and this PR (slider site cleanup) merged, the four
originally-cited problem sites from #3188's Out-of-scope section all
become theme-system-aware and the toggle button class joins the slider +
knob namespace pattern as a third carved-out widget type.

cc @ten9876 @jensenpat @chibondking

73 Nigel G0JKN

🤖 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 27, 2026
…rrides (#3198)

Second control-type carve-out following the slider + knob pattern from
[#3188](#3188). References
the toggle-buttons bullet under "follow-up sweeps" in
[#3184](#3184).

## Why tribes (and not a single namespace)

Where sliders have one canonical visual identity (track + fill), toggle
buttons carry *semantic* colour: the "ON" state communicates meaning —
enable / activate / warning / generic-mode — not just a value. A single
`color.toggle.background.checked` token can't represent all four colour
tribes that exist in the current codebase (green-success, blue-accent,
amber-warning, deep-blue-panel), so the carve-out splits the
**checked-state** tokens across three tribes:

```cpp
enum class ToggleTribe { Accent, Success, Warning };
```

Unchecked + disabled state styling is shared across tribes. Only the
**Accent** tribe additionally carries per-applet overrides through the
v2 scope tree — Success and Warning are semantic and look identical
wherever they live.

## Namespaces (16 new tokens, both bundled themes)

```
color.toggle.background           ← {color.gray.800}   (alias to primitive)
color.toggle.foreground           ← {color.gray.200}
color.toggle.border               ← {color.gray.700}
color.toggle.background.disabled  ← {color.gray.900}
color.toggle.foreground.disabled  ← {color.gray.600}
color.toggle.border.disabled      ← {color.gray.900}

color.toggle.accent.background.checked    ← {color.blue.700}    (TX red / RX green / comp amber per applet)
color.toggle.accent.foreground.checked    ← {color.blue.500}
color.toggle.accent.border.checked        ← {color.blue.500}

color.toggle.success.background.checked   ← #006040 / #c8e8d0
color.toggle.success.foreground.checked   ← {color.green.500}
color.toggle.success.border.checked       ← {color.green.500}

color.toggle.warning.background.checked   ← #5a3a0a / #f5e8d0
color.toggle.warning.foreground.checked   ← {color.amber.500}
color.toggle.warning.border.checked       ← {color.amber.500}
```

Also adds **`color.background.warning`** primitive (dark `#5a3a0a` /
light `#f5e8d0`) for tribe symmetry with the existing
`color.background.success` (which was added in PR #3130).

All aliases resolve **directly to primitives** (single-hop) so the
`resolveAlias()` lookup against `m_primitives` succeeds — chained
semantic→semantic aliases don't resolve through the current path, which
would land the literal `{color.background.1}` string into QSS /
`color()` callers and break rendering. Caught and fixed during test
development.

## Cascade — root → applet → applet/&lt;name&gt;

Per-applet overrides live under `scopes.applet.scopes.<name>.tokens`
alongside the existing slider + knob overrides. Single-token override
(`background.checked` only) matches the slider precedent of one token
per applet:

```json
"applet": {
  "scopes": {
    "tx":   { "tokens": { ..., "color.toggle.accent.background.checked": "{color.red.500}" } },
    "rx":   { "tokens": { ..., "color.toggle.accent.background.checked": "{color.green.500}" } },
    "comp": { "tokens": { ..., "color.toggle.accent.background.checked": "{color.amber.500}" } }
  }
}
```

## What lands

| File | Role |
|---|---|
| `resources/themes/default-dark.json`, `default-light.json` | Token
defs + per-applet overrides + new `color.background.warning` primitive |
| `src/core/ThemeManager.cpp` | `seedBuiltinDefaults` extension (16
token seeds + 3 applet-scope overrides) so user themes forked before
this PR still resolve correctly |
| `src/gui/Theme.h` | `ToggleTribe` enum + `applyToggleButtonStyle(btn,
tribe)` helper + template |
| `src/gui/CatControlApplet.cpp` | Drop inline `kGreenToggle` constant,
route through helper (Success tribe) — single source of truth going
forward |
| `tests/theme_manager_test.cpp` | Coverage for base + per-tribe +
per-applet cascade |
| `docs/theming/toggle-button-tokens.md` | Following the
slider-knob-tokens.md shape with the migration pattern for the next
sweep |

## Why no global QSS rule

Unlike sliders, **this PR does NOT add a `QPushButton:checked` rule to
`appStylesheetTemplate`**. Every checkable button in the codebase that
currently has no explicit `:checked` styling would have suddenly
acquired the Accent-tribe look — a subtle but real visual regression for
~30+ sites that aren't part of this sweep. Opt-in via the helper keeps
the namespace landing isolated; the global QSS rule can land in the
follow-up after per-site auditing.

## Verification (Windows 11, MSVC + Ninja + Qt 6.10.3, branched from
`aethersdr/AetherSDR` main at `4ee99f78`)

**Unit tests:**
- 10/10 new toggle assertions pass — base token validity, per-tribe
checked values, per-applet cascade for accent (tx=`#ff4d4d`,
rx=`#4dd87a`, comp=`#ffb84d`), success + warning stable across applet
scopes, `isOverriddenAt` distinguishes own override from inherited.
- The 4 pre-existing failures on this branch (lines 452/454/505/644 —
`importThemeFromFile` related) are also present on clean `main` at the
same source lines (398/400/451/590 there). **Not introduced here.**
Looks like a Windows-vs-CI environment difference around temp dir
handling. Happy to investigate separately if useful.

**Theme Editor inspection (live cascade):**

| Scope | `color.toggle.accent.background.checked` resolved |
|---|---|
| `root` | `#0070c0` (blue, alias to `{color.blue.700}`) |
| `applet/tx` | `#ff4d4d` (red, alias to `{color.red.500}`) — verified
in Editor columnar view |
| `applet/rx` | `#4dd87a` (green) — verified in Editor |
| `applet/comp` | `#ffb84d` (amber) — verified in Editor; columnar view
shows root `#0070c0` / applet `inherited` / comp `#ffb84d` |

**CatControlApplet helper-driven refactor:**
- Default Dark, `:checked` → `#006040` dark green background, `#4dd87a`
text/border — identical to pre-PR.
- Default Light, `:checked` → `#c8e8d0` soft mint background, `#1a8040`
text/border — identical to post-#3195.
- Docked + floating variants both verified.
- Live theme-switch (Dark↔Light without restart) re-renders cleanly via
the helper's `applyStyleSheet` registration.

**Cross-test sanity:** ran the full test suite. Other test failures
(`async_log_writer_test`, `CAT_Flex_test`, `CAT_TS-2000_test`,
`ole_compound_file_test`, `rigctld_test`) are unrelated to theme work
and present on clean main — not introduced here either.

## One intentional addition to flag

The new helper template includes a `QPushButton:disabled` rule that the
original `kGreenToggle` constant didn't have. In practice the Enable CAT
button is never set disabled in `CatControlApplet`, so this is invisible
there. The addition is intentional — gives every toggle that opts into
the helper a consistent disabled appearance using the
`color.toggle.*.disabled` tokens.

## Out of scope (follow-up sweep)

- **Per-site QSS migration** — `AetherDspWidget`, `AetherialAudioStrip`,
`AntennaGeniusApplet`, `AppletPanel`, `ClientChainApplet`,
`ClientCompApplet` (bypass toggle), `ClientCompEditor`, etc. Each
currently has its own inline `QPushButton:checked` stylesheet; multiple
sites per file, each needs a tribe judgement before migration. Mirrors
the punt on hardcoded slider sites in #3188.
- **Multi-token per-applet overrides** — current override on `applet/tx`
only touches `color.toggle.accent.background.checked`;
`foreground.checked` and `border.checked` stay at the root blue, which
on a red background reads visually inconsistent (red bg + blue border +
blue text). Adding `red.700`/`green.700`/`amber.700` primitives, or
per-applet overrides of all three checked tokens, is the next design
step. Documented in the Out of scope section of
`toggle-button-tokens.md`.
- **Indicator-style toggles** — `QCheckBox::indicator` /
`QRadioButton::indicator` are a different visual primitive, separate
namespace + sweep.
- **Hover / pressed state tokens** — same deferral as sliders.
- **`color.toggle.panel.*` tribe** — `AppletPanel.cpp`'s deep blue
`#0a3060` for tab-like toggles doesn't fit Accent / Success / Warning
cleanly. A fourth `panel` or `muted` tribe may earn its keep in a later
sweep.
- **Global `QPushButton:checked` rule** in `appStylesheetTemplate` —
per-site auditing first.

cc @ten9876 @jensenpat @chibondking

73 Nigel G0JKN

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

---------

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

First worked example of carving a control type out of the generic theme
categories (`color.accent` / `color.background.1` /
`color.text.primary`) into dedicated namespaces with per-applet
overrides through the v2 scope tree. Sets the pattern for follow-up
sweeps (toggle buttons, spinboxes, progress bars, …).

**Draft** — opening early for visibility; more polish may layer on top.

## What's in this PR

### New token namespaces (12 entries)

```
color.slider.background           ← groove + add-page
color.slider.foreground           ← sub-page (filled portion)
color.slider.handle               ← thumb
color.slider.background.disabled
color.slider.foreground.disabled
color.slider.handle.disabled

color.knob.background             ← ring / track
color.knob.foreground             ← indicator arc
color.knob.handle                 ← center pointer / tick
color.knob.background.disabled
color.knob.foreground.disabled
color.knob.handle.disabled
```

State suffix (`.disabled`) matches existing `color.text.disabled` /
`color.accent.bright` pattern. Hover / pressed deferred — explicit
design call.

### Per-applet cascade (visible result)

Bundled themes ship nested-scope overrides:

| Scope | `color.slider.foreground` & `color.knob.foreground` |
|---|---|
| `applet/tx` | `{color.red.500}` |
| `applet/rx` | `{color.green.500}` |
| `applet/comp` | `{color.amber.500}` |
| (everywhere else) | inherits root → `{color.blue.500}` |

Open the TX applet → sliders + the comp knob (if a comp lives inside)
render red. Open RX → green. Open the compressor applet → amber. All
other surfaces stay the canonical blue.

### The ParentChange event filter (general fix)

While building this, surfaced a latent bug: helpers like
`applyPrimarySliderStyle` are commonly called **before** the widget is
added to its parent layout (configure → applyStyle → addWidget). At
apply time the widget has no parent, so `containerPathFor` walks nothing
and resolves the QSS against root scope. The applet-scope override never
reaches the widget.

Fix: `ThemeManager` now installs itself as an event filter on every
widget tracked through `applyStyleSheet`. `QEvent::ParentChange`
re-resolves the template against the now-correct scope chain. Cheap —
only fires during initial construction / rare deliberate reparents.

This makes **every** helper that configures a widget pre-parent work
correctly (applyAppTheme, applyPrimarySliderStyle, the dozens of inline
applyStyleSheet sites in ctor bodies) without per-call-site changes.

### seedBuiltinDefaults safety net

Older user themes (forked before this PR exists) get:
1. Root-scope hex values for all 12 tokens (raw hex, no primitives
dependency).
2. `applet/tx`, `applet/rx`, `applet/comp` scope-tree seeds via
`scopeOrCreate()` with the red/green/amber overrides.

Bundled themes' JSON re-asserts these via `{color.red.500}` aliases —
idempotent, but the seed is what makes per-applet differentiation work
on a "My Default Dark" forked last week.

## Files touched

| File | Role |
|---|---|
| `resources/themes/default-dark.json`, `default-light.json` | Token
defs + nested scope overrides |
| `src/core/ThemeManager.{h,cpp}` | seedBuiltinDefaults extension +
ParentChange filter |
| `src/gui/Theme.h` | Global QSS migration + `applyPrimarySliderStyle`
default arg |
| `src/gui/TitleBar.cpp` | Master + headphone sliders read from
`color.slider.*` |
| `src/gui/TxApplet.cpp` / `RxApplet.cpp` / `ClientCompApplet.cpp` |
Applet-level slider QSS override (catches sliders not using the helper)
|
| `src/gui/ClientCompKnob.cpp` | Widget-aware paint-code migration to
`color.knob.*` |
| `docs/theming/slider-knob-tokens.md` | Full writeup of the namespace +
cascade + ParentChange pattern |

## Out of scope (follow-ups)

- **`PhaseKnob`** paint code still uses 6 hardcoded `QColor` literals —
needs more design judgment than a rename.
- **Hardcoded-hex slider sites** in `PanadapterApplet`, `EqApplet`,
`RadioSetupDialog`, `FlexControlDialog` bypass the theme system
entirely. Each needs its local stylesheet dropped in favour of the
global QSS + per-applet override path.
- **Dialog-scope knob overrides** — available today (namespace + scope
tree both support it); nothing declared yet.

## Test plan

- [ ] Open TX applet — every slider's filled portion is **red**.
- [ ] Open RX applet — every slider's filled portion is **green**.
- [ ] Open the compressor applet — every knob arc + slider fill is
**amber**.
- [ ] Every other applet (DAX, EQ, Phone, …) renders the canonical
**blue** fill from root scope.
- [ ] Disable an AGC slider (or any slider in a disabled section) —
groove/fill/handle shift to the muted disabled variants.
- [ ] Open Theme Editor, pick scope `applet/tx` — the columnar view
shows `color.slider.foreground` overridden at the `tx` column.
- [ ] Inspector click on a slider surfaces the 6 `color.slider.*`
entries; click on a comp knob surfaces the 6 `color.knob.*` entries.
- [ ] Smoke-test on both Default Dark and Default Light.
- [ ] Verify a user theme forked before this PR (e.g. "My Default Dark")
still gets the per-applet colours via the seeded defaults.

🤖 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
…dioSetup / FlexControl) (aethersdr#3204)

Closes the second half of Pat's "both" answer to **(a)** the toggle
namespace work (landed as
[aethersdr#3198](aethersdr#3198)) and **(b)**
the hardcoded-hex slider sites called out in
[aethersdr#3188](aethersdr#3188 *Out of
scope* section.

## Background

[aethersdr#3188](aethersdr#3188) tagged four
sites as bypassing the `color.slider.*` namespace entirely:

> **Hardcoded-hex slider sites** in `PanadapterApplet`, `EqApplet`,
`RadioSetupDialog`, `FlexControlDialog` bypass the theme system
entirely. Each needs its local stylesheet dropped in favour of the
global QSS + per-applet override path.

Each site used inline `setStyleSheet(...)` calls full of hardcoded hex
values — so the v2 per-applet override path couldn't reach them, and
theme switches required an app restart. This PR tokenizes the colours
and routes through `ThemeManager::applyStyleSheet` so:

- the per-applet override cascade lights up automatically once a
relevant scope exists (none of the four currently sits under
`applet/tx|rx|comp`, so the immediate effect at root scope is the
canonical Wave-blue / accent-success treatment);
- live theme switching (Dark ↔ Light without restart) re-resolves the
tokens and repaints, matching the rest of the namespace-aware UI.

## Approach — Option A (tokenize colours, preserve sizes)

Site-local slider **dimensions** are preserved intentionally — the four
sites each chose bespoke handle widths / groove heights for the UX
context they live in (Panadapter's tiny 8px handle for the CW bar,
EqApplet's 10×16 vertical handle for band-column rhythm, RadioSetup's
emphasis-sized 14px handle, FlexControl's 16px bordered ring).

Homogenizing them via `applyPrimarySliderStyle` would have been a more
literal interpretation of "drop the local stylesheet" but would have
introduced visible regressions across all four sites. **Option A**
(tokenize colours, preserve sizes) achieves Pat's stated goal — sliders
honour the theme system — without the visual cost.

## Per-site notes

| File | Sliders | Token migration |
|---|---|---|
| `PanadapterApplet.cpp` | 2 pitch sliders (CW decoder bar) | `#1a2a3a`
groove → `{{color.slider.background}}`<br>`#6a8090` handle →
`{{color.slider.handle}}` |
| `EqApplet.cpp` | 8 vertical EQ band sliders | `#203040` groove →
`{{color.slider.background}}`<br>`#00b4d8` handle →
`{{color.slider.foreground}}` (the EQ idiom puts the accent colour on
the handle itself — no sub-page rule) |
| `RadioSetupDialog.cpp` | 3 filter-sharpness sliders (voice / cw /
digital) | `#1a2a3a` groove → `{{color.slider.background}}`<br>`#c8d8e8`
handle → `{{color.slider.handle}}`<br>(`lineoutSlider` + `hpSlider`
already inherit from the global QSS — no migration needed) |
| `FlexControlDialog.cpp` | 2 sliders (spin / sensitivity) inside the
dialog-wide `kFlexControlStyle` template | `#162437` groove →
`{{color.slider.background}}`<br>`#65d379` sub-page →
`{{color.accent.success}}` (preserves FlexControl green
identity)<br>`#d8e2ef` handle → `{{color.slider.handle}}`<br>`#65d379`
handle border → `{{color.accent.success}}`<br>Body widget switched to
`applyStyleSheet` so the tokens resolve + live re-theme registers |

## Verification

Windows 11 (MSVC + Ninja + Qt 6.10.3) on top of `aethersdr/AetherSDR`
main at `4ee99f78`.

| Site | Default Dark | Default Light |
|---|---|---|
| PanadapterApplet pitch sliders | ✓ dark groove + light handle | ✓
light groove + dark handle — **live re-theme adapts cleanly** |
| EqApplet band sliders | ✓ dark groove + accent-cyan handle | ✓ handle
resolves to light-theme accent (`#0088b0`) |
| RadioSetupDialog filter sliders | ✓ dark groove + light handle | ✓
light groove + dark handle — **live re-theme adapts cleanly** |
| FlexControlDialog spin + sensitivity | ✓ dark groove + green sub-page
+ bordered handle | ✓ sub-page green preserved (the green is
`color.accent.success` which is green in both bundled themes; gray
shades adapt around it) |

`theme_manager_test`: zero new failures. The 4 pre-existing
`importThemeFromFile`-related failures on clean main remain
(Windows-vs-CI env issue, not introduced here).

## Observed during testing — out of scope for this PR

While verifying in Default Light, two **broader migration debt** items
came to light that go beyond this slider sweep:

- **`RadioSetupDialog` light-theme contrast** — `kLabelStyle` (`color:
#c8d8e8`), `kValueStyle` (`color: #00c8ff`), `kGroupStyle` (border
`#304050`, title `#8aa8c0`), and `kEditStyle` (background `#1a2a3a`) all
hardcode dark-theme colours. Labels render as low-contrast pale text
against the light-theme `#f5f5f8` background, and edit fields look
misplaced. Migrating those constants needs the same `{{token}}` +
`applyStyleSheet` pattern and touches dozens of call sites across the
dialog — a separate follow-up PR.
- **`FlexControlDialog` whole-dialog migration debt** — the dialog
template hardcodes `#0b1625` button backgrounds, `#31455f` borders,
`#65d379` FlexControl-green accents throughout. The slider rules now
adapt to theme; the rest of the dialog stays dark in light theme. A
separate "FlexControl light-theme migration" PR would close this;
touching it here would have ballooned the slider-cleanup scope.

Both observations were flagged as candidates for the next cleanup sweep
that follows this one.

## Together with aethersdr#3198

This PR completes Pat's "both" answer. With aethersdr#3198 (toggle namespace
carve-out) and this PR (slider site cleanup) merged, the four
originally-cited problem sites from aethersdr#3188's Out-of-scope section all
become theme-system-aware and the toggle button class joins the slider +
knob namespace pattern as a third carved-out widget type.

cc @ten9876 @jensenpat @chibondking

73 Nigel G0JKN

🤖 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
…rrides (aethersdr#3198)

Second control-type carve-out following the slider + knob pattern from
[aethersdr#3188](aethersdr#3188). References
the toggle-buttons bullet under "follow-up sweeps" in
[aethersdr#3184](aethersdr#3184).

## Why tribes (and not a single namespace)

Where sliders have one canonical visual identity (track + fill), toggle
buttons carry *semantic* colour: the "ON" state communicates meaning —
enable / activate / warning / generic-mode — not just a value. A single
`color.toggle.background.checked` token can't represent all four colour
tribes that exist in the current codebase (green-success, blue-accent,
amber-warning, deep-blue-panel), so the carve-out splits the
**checked-state** tokens across three tribes:

```cpp
enum class ToggleTribe { Accent, Success, Warning };
```

Unchecked + disabled state styling is shared across tribes. Only the
**Accent** tribe additionally carries per-applet overrides through the
v2 scope tree — Success and Warning are semantic and look identical
wherever they live.

## Namespaces (16 new tokens, both bundled themes)

```
color.toggle.background           ← {color.gray.800}   (alias to primitive)
color.toggle.foreground           ← {color.gray.200}
color.toggle.border               ← {color.gray.700}
color.toggle.background.disabled  ← {color.gray.900}
color.toggle.foreground.disabled  ← {color.gray.600}
color.toggle.border.disabled      ← {color.gray.900}

color.toggle.accent.background.checked    ← {color.blue.700}    (TX red / RX green / comp amber per applet)
color.toggle.accent.foreground.checked    ← {color.blue.500}
color.toggle.accent.border.checked        ← {color.blue.500}

color.toggle.success.background.checked   ← #006040 / #c8e8d0
color.toggle.success.foreground.checked   ← {color.green.500}
color.toggle.success.border.checked       ← {color.green.500}

color.toggle.warning.background.checked   ← #5a3a0a / #f5e8d0
color.toggle.warning.foreground.checked   ← {color.amber.500}
color.toggle.warning.border.checked       ← {color.amber.500}
```

Also adds **`color.background.warning`** primitive (dark `#5a3a0a` /
light `#f5e8d0`) for tribe symmetry with the existing
`color.background.success` (which was added in PR aethersdr#3130).

All aliases resolve **directly to primitives** (single-hop) so the
`resolveAlias()` lookup against `m_primitives` succeeds — chained
semantic→semantic aliases don't resolve through the current path, which
would land the literal `{color.background.1}` string into QSS /
`color()` callers and break rendering. Caught and fixed during test
development.

## Cascade — root → applet → applet/&lt;name&gt;

Per-applet overrides live under `scopes.applet.scopes.<name>.tokens`
alongside the existing slider + knob overrides. Single-token override
(`background.checked` only) matches the slider precedent of one token
per applet:

```json
"applet": {
  "scopes": {
    "tx":   { "tokens": { ..., "color.toggle.accent.background.checked": "{color.red.500}" } },
    "rx":   { "tokens": { ..., "color.toggle.accent.background.checked": "{color.green.500}" } },
    "comp": { "tokens": { ..., "color.toggle.accent.background.checked": "{color.amber.500}" } }
  }
}
```

## What lands

| File | Role |
|---|---|
| `resources/themes/default-dark.json`, `default-light.json` | Token
defs + per-applet overrides + new `color.background.warning` primitive |
| `src/core/ThemeManager.cpp` | `seedBuiltinDefaults` extension (16
token seeds + 3 applet-scope overrides) so user themes forked before
this PR still resolve correctly |
| `src/gui/Theme.h` | `ToggleTribe` enum + `applyToggleButtonStyle(btn,
tribe)` helper + template |
| `src/gui/CatControlApplet.cpp` | Drop inline `kGreenToggle` constant,
route through helper (Success tribe) — single source of truth going
forward |
| `tests/theme_manager_test.cpp` | Coverage for base + per-tribe +
per-applet cascade |
| `docs/theming/toggle-button-tokens.md` | Following the
slider-knob-tokens.md shape with the migration pattern for the next
sweep |

## Why no global QSS rule

Unlike sliders, **this PR does NOT add a `QPushButton:checked` rule to
`appStylesheetTemplate`**. Every checkable button in the codebase that
currently has no explicit `:checked` styling would have suddenly
acquired the Accent-tribe look — a subtle but real visual regression for
~30+ sites that aren't part of this sweep. Opt-in via the helper keeps
the namespace landing isolated; the global QSS rule can land in the
follow-up after per-site auditing.

## Verification (Windows 11, MSVC + Ninja + Qt 6.10.3, branched from
`aethersdr/AetherSDR` main at `4ee99f78`)

**Unit tests:**
- 10/10 new toggle assertions pass — base token validity, per-tribe
checked values, per-applet cascade for accent (tx=`#ff4d4d`,
rx=`#4dd87a`, comp=`#ffb84d`), success + warning stable across applet
scopes, `isOverriddenAt` distinguishes own override from inherited.
- The 4 pre-existing failures on this branch (lines 452/454/505/644 —
`importThemeFromFile` related) are also present on clean `main` at the
same source lines (398/400/451/590 there). **Not introduced here.**
Looks like a Windows-vs-CI environment difference around temp dir
handling. Happy to investigate separately if useful.

**Theme Editor inspection (live cascade):**

| Scope | `color.toggle.accent.background.checked` resolved |
|---|---|
| `root` | `#0070c0` (blue, alias to `{color.blue.700}`) |
| `applet/tx` | `#ff4d4d` (red, alias to `{color.red.500}`) — verified
in Editor columnar view |
| `applet/rx` | `#4dd87a` (green) — verified in Editor |
| `applet/comp` | `#ffb84d` (amber) — verified in Editor; columnar view
shows root `#0070c0` / applet `inherited` / comp `#ffb84d` |

**CatControlApplet helper-driven refactor:**
- Default Dark, `:checked` → `#006040` dark green background, `#4dd87a`
text/border — identical to pre-PR.
- Default Light, `:checked` → `#c8e8d0` soft mint background, `#1a8040`
text/border — identical to post-aethersdr#3195.
- Docked + floating variants both verified.
- Live theme-switch (Dark↔Light without restart) re-renders cleanly via
the helper's `applyStyleSheet` registration.

**Cross-test sanity:** ran the full test suite. Other test failures
(`async_log_writer_test`, `CAT_Flex_test`, `CAT_TS-2000_test`,
`ole_compound_file_test`, `rigctld_test`) are unrelated to theme work
and present on clean main — not introduced here either.

## One intentional addition to flag

The new helper template includes a `QPushButton:disabled` rule that the
original `kGreenToggle` constant didn't have. In practice the Enable CAT
button is never set disabled in `CatControlApplet`, so this is invisible
there. The addition is intentional — gives every toggle that opts into
the helper a consistent disabled appearance using the
`color.toggle.*.disabled` tokens.

## Out of scope (follow-up sweep)

- **Per-site QSS migration** — `AetherDspWidget`, `AetherialAudioStrip`,
`AntennaGeniusApplet`, `AppletPanel`, `ClientChainApplet`,
`ClientCompApplet` (bypass toggle), `ClientCompEditor`, etc. Each
currently has its own inline `QPushButton:checked` stylesheet; multiple
sites per file, each needs a tribe judgement before migration. Mirrors
the punt on hardcoded slider sites in aethersdr#3188.
- **Multi-token per-applet overrides** — current override on `applet/tx`
only touches `color.toggle.accent.background.checked`;
`foreground.checked` and `border.checked` stay at the root blue, which
on a red background reads visually inconsistent (red bg + blue border +
blue text). Adding `red.700`/`green.700`/`amber.700` primitives, or
per-applet overrides of all three checked tokens, is the next design
step. Documented in the Out of scope section of
`toggle-button-tokens.md`.
- **Indicator-style toggles** — `QCheckBox::indicator` /
`QRadioButton::indicator` are a different visual primitive, separate
namespace + sweep.
- **Hover / pressed state tokens** — same deferral as sliders.
- **`color.toggle.panel.*` tribe** — `AppletPanel.cpp`'s deep blue
`#0a3060` for tab-like toggles doesn't fit Accent / Success / Warning
cleanly. A fourth `panel` or `muted` tribe may earn its keep in a later
sweep.
- **Global `QPushButton:checked` rule** in `appStylesheetTemplate` —
per-site auditing first.

cc @ten9876 @jensenpat @chibondking

73 Nigel G0JKN

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Jeremy Fielder <kk7gwy@aethersdr.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