fix(theme): replace no-op --mat-sys-* tokens with --app-* design tokens

The Material theme setup in apps/web/src/m3-theme.scss uses the older M3
API (mat.define-theme + mat.all-component-themes), which emits component
tokens but never emits the --mat-sys-* system tokens. As a result ~540
var(--mat-sys-*) references across the app were silent no-ops: the
declarations were invalid at computed-value time and fell back to
inherited/initial values, so many backgrounds, borders and dividers never
rendered and several text/icon colors sat at the wrong contrast.

Rather than migrate to mat.theme() (which would flip every no-op at once
with M3 azure-palette values foreign to the hand-tuned design), remap the
high-visibility usages to the existing --app-* design tokens defined for
both themes in m3-theme.scss, verified screenshot-driven in light and dark
via Electron CDP.

Changes:
- Add --app-error-color and --app-accent-color tokens (light + dark).
- Fix high-visibility surfaces: settings area, workspace shell header,
  playlist switcher, downloads, channel-details dialog, account-info,
  group/category management dialogs, sources context/filters panels,
  nav-list, and the .dark-theme app background.
- Point the Tailwind color palette at --app-* tokens instead of --mat-sys-*.
- Strip ~150 dead var(--app-x, var(--mat-sys-y)) fallbacks repo-wide (the
  fallback can never be taken).
- UI polish surfaced by the remap: suppress Material's circular icon-button
  state layer in the header (keep the rounded-rectangle hover), remove the
  orphan divider before the download button, remove the redundant right-edge
  accent bar on active rail items (the filled chip is enough), and raise the
  header/switcher control icons from the muted tier to --app-body-color so
  they no longer read as disabled.

Guard against regressions:
- tools/styles/check-mat-sys-usage.mjs fails on any new var(--mat-sys-*)
  reference; legacy usages are grandfathered in a shrink-only baseline
  (tools/styles/mat-sys-baseline.mjs). Wired into the CI Lint job and
  exposed as `pnpm run lint:styles`.

Docs:
- docs/architecture/theme-design-tokens.md documents why the tokens are
  no-ops, the --app-* mapping table, and the screenshot-driven migration
  protocol. CLAUDE.md updated with the token policy and the new lint check.

Scrollbar-related --mat-sys-* usages are intentionally left untouched here;
they belong to the separate branch claude/iptvnator-dark-scrollbar-2600aa.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5 committed 2026-07-12 22:31:46 +02:00
1 parent f856226dc3
commit eac1dfa09e
45 files changed
+801 -488

No files matched your search

+87
View File
@@ -0,0 +1,87 @@
# Theme design tokens: `--app-*` is canonical, `--mat-sys-*` is a no-op
## TL;DR
- The only CSS design tokens that exist at runtime are the `--app-*` custom
properties defined (for light and dark) in `apps/web/src/m3-theme.scss`.
- `var(--mat-sys-*)` references **do not work anywhere in this app**. The
theme uses Angular Material's older M3 API (`mat.define-theme` +
`mat.all-component-themes` / `mat.all-component-colors`), which emits
_component_ tokens with literal values but never emits the `--mat-sys-*`
_system_ tokens. Verified empirically via CDP on 2026-07-12: no stylesheet
rule defines any `--mat-sys-*` variable.
- CI fails when new `var(--mat-sys-*)` references are added
(`tools/styles/check-mat-sys-usage.mjs`); legacy usages are grandfathered
in `tools/styles/mat-sys-baseline.mjs`, which must only shrink.
## What a `--mat-sys-*` reference actually does today
Because the token is never defined, any declaration containing
`var(--mat-sys-x)` without a fallback becomes _invalid at computed-value
time_, which behaves like `unset`:
| Declaration shape | Actual rendering |
| ------------------------------------------------------------------ | -------------------------------------------------------------- |
| `color: var(--mat-sys-on-surface-variant)` | inherited text color |
| `background: var(--mat-sys-surface-container)` | transparent |
| `background: color-mix(..., var(--mat-sys-x) 12%, ...)` | transparent |
| `border: 1px solid var(--mat-sys-outline-variant)` | **no border at all** |
| `border-color: var(--mat-sys-primary)` (width/style set elsewhere) | `currentColor` border |
| `--local-var: ... var(--mat-sys-x) ...` | `--local-var` becomes invalid too, cascading to every consumer |
| `var(--app-x, var(--mat-sys-y))` | `--app-x` (fallback is dead but harmless) |
This means the app's accepted visual design was produced _with_ these
no-ops. Making the tokens suddenly resolve (e.g. by migrating the theme to
`mat.theme()`, available since Material v19) would flip hundreds of sites at
once: surfaces, borders, and dividers that are currently invisible would
appear app-wide with M3 azure-palette values that were never part of the
hand-tuned design. That is why the fix strategy is **(b) replace usages
incrementally with `--app-*` tokens**, not (a) emit the system tokens.
Every migration step must be screenshot-verified in both themes (see
"Migration protocol" below) because replacing a no-op is a deliberate visual
change: the borders/backgrounds the original author intended will start
rendering.
## Token mapping for migration
When migrating a file, map by _role in context_, not mechanically:
| `--mat-sys-*` token | Replacement |
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------- |
| `on-surface` (text) | `--app-heading-color` (emphasis/hover text), `--app-body-color` (regular copy) |
| `on-surface` (inside `color-mix` overlays) | `--app-on-surface` |
| `on-surface-variant` | `--app-muted-color` (icons, captions, placeholders, meta), `--app-body-color` (secondary copy) |
| `outline-variant`, `outline` | `--app-separator` (hairlines/dividers), `--app-widget-header-border` (faintest) |
| `surface` | `--app-content-bg` (page-level), `--app-widget-bg` (component surface) |
| `surface-container`, `-low` | `--app-widget-bg`; for chips/pills on a widget surface prefer `color-mix(in srgb, var(--app-on-surface) 6%, transparent)` |
| `surface-container-high`, `-highest` | `--app-widget-header-bg` (raised surface), `--app-card-hover-bg` (hover state) |
| `primary` | `--app-selection-color` |
| `on-primary` | `--app-selection-on-color` |
| `primary-container` | `--app-selection-surface` (`-strong` for emphasis) |
| `error`, `error-container` | `--app-error-color`; surfaces via `color-mix(in srgb, var(--app-error-color) N%, transparent)` |
| `tertiary`, `tertiary-container` | `--app-accent-color` (cyan accent: radio badge, external-playback and success states); surfaces via `color-mix` |
Dead-fallback usages `var(--app-x, var(--mat-sys-y))` are always safe to
simplify to `var(--app-x)` — the fallback can never be taken (and if
`--app-x` were undefined, the fallback would be invalid anyway, so behavior
is identical in every case).
## Migration protocol
1. Replace the file's `var(--mat-sys-*)` references using the table above.
2. Run the app (`pnpm nx serve electron-backend`, CDP on 9222) and
screenshot the affected views in **both** light and dark themes; compare
against pre-change screenshots. Newly appearing borders/backgrounds must
look intentional and consistent with the `--app-*` design language.
3. Regenerate the baseline: `node tools/styles/generate-mat-sys-baseline.mjs`
and commit the shrunken `tools/styles/mat-sys-baseline.mjs`.
4. `pnpm run lint:styles` must pass.
## Guard
- `pnpm run lint:styles` → `tools/styles/check-mat-sys-usage.mjs` scans
`apps/` and `libs/` (`.scss`, `.css`, `.ts`, `.html`, `.js`) and fails if
any file has more `var(--mat-sys-` references than its baseline entry.
- CI runs it in the Lint job (`.github/workflows/ci.yml`).
- Never add new entries or raise counts in the baseline.