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

+3
View File
@@ -37,6 +37,9 @@ jobs:
CI: true
NX_TASKS_RUNNER_DYNAMIC_OUTPUT: false
- name: Check for new --mat-sys-* token usages
run: node tools/styles/check-mat-sys-usage.mjs
unit-and-typecheck:
name: Unit Tests and Typechecks
runs-on: ubuntu-latest
+14
View File
@@ -210,6 +210,11 @@ are baselined in `tools/eslint/max-lines-baseline.mjs`; regenerate the baseline
with `node tools/eslint/generate-max-lines-baseline.mjs` after splitting a file.
Never add new files to the baseline.
The CI lint job also runs `pnpm run lint:styles`
(`tools/styles/check-mat-sys-usage.mjs`), which fails on new `var(--mat-sys-*)`
references — those tokens are never emitted by the current theme; use the
`--app-*` design tokens instead (see `docs/architecture/theme-design-tokens.md`).
## Architecture
### Monorepo Structure (Nx Workspace)
@@ -477,6 +482,15 @@ This rule exists to keep the codebase navigable and reviewable. A 150-line file
---
**Design Tokens (Theming)**:
- The app theme (light + dark) lives in `apps/web/src/m3-theme.scss` (older Material M3 API: `mat.define-theme` + `mat.all-component-themes`). It defines the canonical `--app-*` design tokens for both themes.
- **Never reference `--mat-sys-*` tokens in styles** — this theme setup never emits Material system tokens, so such references are silent no-ops (invalid at computed-value time). Use the `--app-*` tokens instead.
- CI fails on new `var(--mat-sys-*)` usages: `pnpm run lint:styles` (`tools/styles/check-mat-sys-usage.mjs`; legacy usages are baselined in `tools/styles/mat-sys-baseline.mjs`, which must only shrink).
- Token mapping and screenshot-driven migration protocol: `docs/architecture/theme-design-tokens.md`.
---
**Angular Coding Standards**:
This project uses modern Angular signal-based APIs and patterns. **ALWAYS** use the following:
+18 -30
View File
@@ -16,12 +16,12 @@
--settings-action-bar-space: 0px;
--settings-page-header-bg: color-mix(
in srgb,
var(--mat-sys-surface-container-low) 94%,
var(--app-widget-bg) 94%,
rgba(255, 255, 255, 0.98)
);
--settings-page-header-border: color-mix(
in srgb,
var(--mat-sys-outline-variant) 72%,
var(--app-separator) 72%,
transparent
);
--settings-page-header-shadow:
@@ -29,12 +29,12 @@
0 1px 0 rgba(255, 255, 255, 0.68) inset;
--settings-group-bg: color-mix(
in srgb,
var(--mat-sys-surface) 96%,
var(--app-content-bg) 96%,
rgba(255, 255, 255, 0.96)
);
--settings-group-border: color-mix(
in srgb,
var(--mat-sys-outline-variant) 78%,
var(--app-separator) 78%,
transparent
);
--settings-group-shadow: 0 14px 28px -32px rgba(15, 23, 42, 0.14);
@@ -59,42 +59,38 @@
--settings-item-border: var(--app-separator);
--settings-item-danger-bg: color-mix(
in srgb,
var(--mat-sys-error) 4%,
var(--app-error-color) 4%,
rgba(255, 255, 255, 0.96)
);
--settings-item-danger-border: color-mix(
in srgb,
var(--mat-sys-error) 14%,
var(--app-error-color) 14%,
transparent
);
--settings-action-bar-bg: color-mix(
in srgb,
var(--mat-sys-surface-container-low) 94%,
var(--app-widget-bg) 94%,
rgba(255, 255, 255, 0.9)
);
--settings-action-bar-border: color-mix(
in srgb,
var(--mat-sys-outline-variant) 62%,
var(--app-separator) 62%,
transparent
);
--settings-action-bar-shadow: 0 10px 26px -26px rgba(15, 23, 42, 0.22);
--settings-muted: color-mix(
in srgb,
var(--mat-sys-on-surface) 64%,
transparent
);
--settings-muted: var(--app-muted-color);
}
:where(.dark-theme, [data-theme='dark'], .mat-app-background.dark)
.settings-container {
--settings-page-header-bg: color-mix(
in srgb,
var(--mat-sys-surface-container) 92%,
var(--app-widget-bg) 92%,
rgba(12, 16, 24, 0.94)
);
--settings-page-header-border: color-mix(
in srgb,
var(--mat-sys-outline-variant) 38%,
var(--app-separator) 38%,
transparent
);
--settings-page-header-shadow:
@@ -102,7 +98,7 @@
0 16px 26px -34px rgba(0, 0, 0, 0.4);
--settings-group-bg: color-mix(
in srgb,
var(--mat-sys-surface-container) 94%,
var(--app-widget-bg) 94%,
rgba(10, 14, 22, 0.82)
);
--settings-group-border: rgba(255, 255, 255, 0.08);
@@ -127,26 +123,22 @@
0 10px 22px -28px var(--settings-group-active-glow);
--settings-item-danger-bg: color-mix(
in srgb,
var(--mat-sys-error) 8%,
var(--app-error-color) 8%,
rgba(24, 10, 12, 0.86)
);
--settings-item-danger-border: color-mix(
in srgb,
var(--mat-sys-error) 24%,
var(--app-error-color) 24%,
transparent
);
--settings-action-bar-bg: color-mix(
in srgb,
var(--mat-sys-surface-container) 92%,
var(--app-widget-bg) 92%,
transparent
);
--settings-action-bar-border: rgba(255, 255, 255, 0.08);
--settings-action-bar-shadow: 0 8px 30px rgba(0, 0, 0, 0.18);
--settings-muted: color-mix(
in srgb,
var(--mat-sys-on-surface) 68%,
transparent
);
--settings-muted: var(--app-muted-color);
}
.settings-container {
@@ -171,11 +163,7 @@
color-mix(in srgb, #ffb347 14%, transparent) 0%,
transparent 60%
),
color-mix(
in srgb,
var(--mat-sys-surface-container-low) 92%,
transparent
);
color-mix(in srgb, var(--app-widget-bg) 92%, transparent);
overflow: hidden;
&::after {
@@ -272,7 +260,7 @@
&:focus-visible {
outline: none;
box-shadow:
0 0 0 2px var(--mat-sys-surface),
0 0 0 2px var(--app-content-bg),
0 0 0 4px
color-mix(
in srgb,
@@ -28,7 +28,7 @@
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--mat-sys-on-surface-variant);
color: var(--app-body-color);
}
.settings-delete-dialog__summary {
@@ -42,14 +42,9 @@
justify-content: space-between;
gap: 12px;
padding: 10px 12px;
border: 1px solid
color-mix(in srgb, var(--mat-sys-outline) 46%, transparent);
border: 1px solid color-mix(in srgb, var(--app-separator) 46%, transparent);
border-radius: 14px;
background: color-mix(
in srgb,
var(--mat-sys-surface-container) 84%,
transparent
);
background: color-mix(in srgb, var(--app-widget-bg) 84%, transparent);
strong {
font-size: 1rem;
@@ -68,7 +63,7 @@
width: 18px;
height: 18px;
font-size: 18px;
color: var(--mat-sys-primary);
color: var(--app-selection-color);
}
.settings-delete-dialog__list {
@@ -86,7 +81,7 @@
var(--app-selection-surface) 32%,
transparent
);
color: var(--mat-sys-on-surface-variant);
color: var(--app-body-color);
}
.settings-delete-dialog__actions {
@@ -278,13 +278,8 @@
display: inline-flex;
padding: 3px;
gap: 2px;
background: color-mix(
in srgb,
var(--mat-sys-surface-container-low) 80%,
transparent
);
border: 1px solid
color-mix(in srgb, var(--mat-sys-outline-variant) 60%, transparent);
background: color-mix(in srgb, var(--app-widget-bg) 80%, transparent);
border: 1px solid color-mix(in srgb, var(--app-separator) 60%, transparent);
border-radius: 10px;
}
@@ -299,7 +294,7 @@
border: 0;
border-radius: 7px;
background: transparent;
color: var(--mat-sys-on-surface-variant);
color: var(--app-body-color);
font: inherit;
font-size: 0.82rem;
font-weight: 500;
@@ -320,10 +315,10 @@
&:hover {
background: color-mix(
in srgb,
var(--mat-sys-surface-container-highest) 70%,
var(--app-card-hover-bg) 70%,
transparent
);
color: var(--mat-sys-on-surface);
color: var(--app-heading-color);
}
&:focus-visible {
@@ -334,7 +329,7 @@
}
.theme-switcher__option--selected {
background: var(--app-widget-bg, var(--mat-sys-surface));
background: var(--app-widget-bg);
color: var(--app-selection-color);
font-weight: 600;
box-shadow:
@@ -358,7 +353,7 @@
text-align: center;
gap: 8px;
padding: 24px 16px 16px;
color: var(--mat-sys-on-surface-variant);
color: var(--app-body-color);
.epg-sources-empty__icon {
font-size: 40px;
@@ -372,7 +367,7 @@
margin: 0;
font-size: 0.95rem;
font-weight: 600;
color: var(--mat-sys-on-surface);
color: var(--app-heading-color);
}
.epg-sources-empty__hint {
@@ -479,7 +474,7 @@
small {
margin-top: 4px;
color: var(--mat-sys-on-surface-variant);
color: var(--app-body-color);
text-align: right;
}
}
@@ -499,15 +494,10 @@
align-items: center;
gap: 12px;
padding: 12px 14px;
border: 1px solid
color-mix(in srgb, var(--mat-sys-outline-variant) 70%, transparent);
border: 1px solid color-mix(in srgb, var(--app-separator) 70%, transparent);
border-radius: 16px;
background: color-mix(
in srgb,
var(--mat-sys-surface-container-low) 88%,
transparent
);
color: var(--mat-sys-on-surface);
background: color-mix(in srgb, var(--app-widget-bg) 88%, transparent);
color: var(--app-heading-color);
text-decoration: none;
transition:
border-color 0.18s ease,
@@ -520,11 +510,11 @@
border-color: color-mix(
in srgb,
var(--app-selection-border) 54%,
var(--mat-sys-outline-variant)
var(--app-separator)
);
background: color-mix(
in srgb,
var(--mat-sys-surface-container-high) 92%,
var(--app-widget-header-bg) 92%,
transparent
);
box-shadow: 0 10px 20px -22px rgba(15, 23, 42, 0.5);
@@ -538,7 +528,7 @@
&:focus-visible {
outline: none;
box-shadow:
0 0 0 2px var(--mat-sys-surface),
0 0 0 2px var(--app-content-bg),
0 0 0 4px
color-mix(in srgb, var(--app-selection-color) 40%, transparent);
}
@@ -553,10 +543,10 @@
border-radius: 12px;
background: color-mix(
in srgb,
var(--mat-sys-surface-container-high) 82%,
var(--app-widget-header-bg) 82%,
transparent
);
color: var(--mat-sys-on-surface-variant);
color: var(--app-body-color);
svg {
width: 20px;
@@ -571,8 +561,8 @@
}
.link-card--github .link-card__glyph {
background: color-mix(in srgb, var(--mat-sys-on-surface) 10%, transparent);
color: var(--mat-sys-on-surface);
background: color-mix(in srgb, var(--app-heading-color) 10%, transparent);
color: var(--app-heading-color);
}
.link-card--blog .link-card__glyph {
@@ -625,9 +615,9 @@
padding: 10px 12px;
border-radius: 14px;
font-size: 0.84rem;
background: color-mix(in srgb, var(--mat-sys-primary) 8%, transparent);
background: color-mix(in srgb, var(--app-selection-color) 8%, transparent);
border: 1px solid
color-mix(in srgb, var(--mat-sys-primary) 30%, transparent);
color-mix(in srgb, var(--app-selection-color) 30%, transparent);
line-height: 1.45;
a {
@@ -675,20 +665,20 @@
border-radius: 12px;
background: color-mix(
in srgb,
var(--mat-sys-surface-container-high) 78%,
var(--app-widget-header-bg) 78%,
transparent
);
border: 1px solid
color-mix(in srgb, var(--settings-item-danger-border) 60%, transparent);
font-size: 0.82rem;
line-height: 1.2;
color: var(--mat-sys-on-surface);
color: var(--app-heading-color);
.danger-zone__summary-icon {
font-size: 18px;
width: 18px;
height: 18px;
color: var(--mat-sys-on-surface-variant);
color: var(--app-body-color);
flex-shrink: 0;
}
@@ -707,10 +697,10 @@
border-radius: 999px;
background: color-mix(
in srgb,
var(--mat-sys-on-surface) 10%,
var(--app-heading-color) 10%,
transparent
);
color: var(--mat-sys-on-surface);
color: var(--app-heading-color);
font-size: 0.78rem;
font-weight: 700;
font-variant-numeric: tabular-nums;
@@ -720,7 +710,7 @@
.danger-zone__progress {
margin: 0;
color: var(--mat-sys-on-surface-variant);
color: var(--app-body-color);
font-size: 0.84rem;
}
@@ -812,10 +802,10 @@
&[disabled] {
background: color-mix(
in srgb,
var(--mat-sys-on-surface) 12%,
var(--app-heading-color) 12%,
transparent
);
color: color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent);
color: color-mix(in srgb, var(--app-heading-color) 38%, transparent);
box-shadow: none;
}
}
+14
View File
@@ -89,6 +89,13 @@ html {
// Live / on-air accent (broadcast indicator)
--app-live-color: #d12c2c;
// Error / destructive accent (M3 error role)
--app-error-color: #ba1a1a;
// Secondary cyan accent (M3 "tertiary" role): radio badges,
// external-playback + success states
--app-accent-color: #0e7490;
// Content area
--app-content-shadow: -2px 4px 12px rgba(0, 0, 0, 0.06);
@@ -164,6 +171,13 @@ html {
// Live / on-air accent (broadcast indicator)
--app-live-color: #ff8f8f;
// Error / destructive accent (M3 error role)
--app-error-color: #ffb4ab;
// Secondary cyan accent (M3 "tertiary" role): radio badges,
// external-playback + success states
--app-accent-color: #5cd6ff;
// Content area
--app-content-shadow: -4px 8px 20px rgba(0, 0, 0, 0.18);
+3 -3
View File
@@ -18,7 +18,7 @@
border: 1px solid transparent;
border-radius: 10px;
background: transparent;
color: var(--mat-sys-on-surface-variant);
color: var(--app-body-color);
text-align: left;
text-decoration: none;
cursor: pointer;
@@ -54,13 +54,13 @@
min-width: 2ch;
text-align: right;
flex-shrink: 0;
color: var(--mat-sys-on-surface-variant);
color: var(--app-body-color);
opacity: 0.7;
}
&:hover:not(.selected):not(.active):not(.is-active) {
background: var(--app-selection-surface);
color: var(--mat-sys-on-surface);
color: var(--app-heading-color);
}
&.selected,
+1 -1
View File
@@ -154,7 +154,7 @@ textarea,
// ─── Dark Theme ───────────────────────────────────────────────────────────────
.dark-theme {
background: var(--mat-sys-surface) !important;
background: var(--app-shell-bg) !important;
// Refined scrollbar for dark mode
::-webkit-scrollbar {
+26 -22
View File
@@ -13,28 +13,32 @@ module.exports = {
sans: ['Inter', 'Roboto', 'Helvetica Neue', 'sans-serif'],
},
colors: {
// Surface colors wired to Material sys vars so Tailwind
// utilities automatically respect the active theme
surface: 'var(--mat-sys-surface)',
'surface-low': 'var(--mat-sys-surface-container-low)',
'surface-mid': 'var(--mat-sys-surface-container)',
'surface-high': 'var(--mat-sys-surface-container-high)',
'on-surface': 'var(--mat-sys-on-surface)',
'on-surface-variant': 'var(--mat-sys-on-surface-variant)',
outline: 'var(--mat-sys-outline)',
'outline-variant': 'var(--mat-sys-outline-variant)',
primary: 'var(--mat-sys-primary)',
'on-primary': 'var(--mat-sys-on-primary)',
'primary-container': 'var(--mat-sys-primary-container)',
'on-primary-container': 'var(--mat-sys-on-primary-container)',
secondary: 'var(--mat-sys-secondary)',
'secondary-container': 'var(--mat-sys-secondary-container)',
'on-secondary-container':
'var(--mat-sys-on-secondary-container)',
tertiary: 'var(--mat-sys-tertiary)',
'tertiary-container': 'var(--mat-sys-tertiary-container)',
error: 'var(--mat-sys-error)',
'error-container': 'var(--mat-sys-error-container)',
// Surface colors wired to the --app-* design tokens
// (m3-theme.scss) so Tailwind utilities automatically respect
// the active theme. Never wire these to --mat-sys-* vars: the
// current Material theme setup does not emit system tokens
// (see docs/architecture/theme-design-tokens.md).
surface: 'var(--app-content-bg)',
'surface-low': 'var(--app-widget-bg)',
'surface-mid': 'var(--app-widget-bg)',
'surface-high': 'var(--app-widget-header-bg)',
'on-surface': 'var(--app-heading-color)',
'on-surface-variant': 'var(--app-body-color)',
outline: 'var(--app-separator)',
'outline-variant': 'var(--app-separator)',
primary: 'var(--app-selection-color)',
'on-primary': 'var(--app-selection-on-color)',
'primary-container': 'var(--app-selection-surface)',
'on-primary-container': 'var(--app-selection-color)',
secondary: 'var(--app-body-color)',
'secondary-container': 'var(--app-hover-overlay)',
'on-secondary-container': 'var(--app-heading-color)',
tertiary: 'var(--app-accent-color)',
'tertiary-container':
'color-mix(in srgb, var(--app-accent-color) 15%, transparent)',
error: 'var(--app-error-color)',
'error-container':
'color-mix(in srgb, var(--app-error-color) 12%, transparent)',
},
borderRadius: {
widget: '14px',
+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.
@@ -15,7 +15,7 @@
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
background: color-mix(in srgb, var(--app-on-surface) 5%, transparent);
border: 1px solid
color-mix(in srgb, var(--mat-sys-outline-variant) 36%, transparent);
color-mix(in srgb, var(--app-separator) 36%, transparent);
app-region: no-drag;
&:hover {
@@ -26,7 +26,7 @@
);
border-color: color-mix(
in srgb,
var(--mat-sys-outline-variant) 64%,
var(--app-separator) 64%,
transparent
);
}
@@ -39,7 +39,7 @@
);
border-color: color-mix(
in srgb,
var(--mat-sys-outline-variant) 72%,
var(--app-separator) 72%,
transparent
);
}
@@ -68,7 +68,7 @@
font-size: 18px;
width: 18px;
height: 18px;
color: var(--mat-sys-on-surface-variant);
color: var(--app-muted-color);
}
.playlist-info {
@@ -89,7 +89,7 @@
font-size: 0.74em;
line-height: 1.3;
letter-spacing: 0.02em;
color: var(--mat-sys-on-surface-variant);
color: var(--app-muted-color);
}
}
@@ -105,14 +105,15 @@
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--mat-sys-on-surface-variant);
// Body-tier so the refresh control reads as active, not disabled.
color: var(--app-body-color);
transition:
color 0.15s ease,
background 0.15s ease;
&:hover:not(:disabled) {
color: var(--mat-sys-on-surface);
background: var(--mat-sys-surface-container);
color: var(--app-heading-color);
background: var(--app-hover-overlay);
}
::ng-deep .mat-mdc-button-touch-target {
@@ -155,7 +156,7 @@
font-size: 20px;
width: 20px;
height: 20px;
color: var(--mat-sys-on-surface-variant);
color: var(--app-body-color);
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
margin-left: 4px;
@@ -165,7 +166,7 @@
}
::ng-deep .playlist-switcher-menu.mat-mdc-menu-panel {
--mat-menu-container-color: var(--mat-sys-surface-container-high);
--mat-menu-container-color: var(--app-widget-header-bg);
width: min(
var(--playlist-switcher-overlay-width, 400px),
calc(100vw - 24px)
@@ -180,7 +181,7 @@
) !important;
margin-top: -1px !important;
border-radius: 0 0 12px 12px !important;
border: 1px solid var(--mat-sys-outline) !important;
border: 1px solid var(--app-separator) !important;
border-top: 0 !important;
overflow: hidden !important;
background: var(--mat-menu-container-color) !important;
@@ -225,14 +226,14 @@
width: 30px;
height: 30px;
line-height: 30px;
color: var(--mat-sys-on-surface-variant);
color: var(--app-muted-color);
opacity: 0.8;
padding: 0;
&:hover,
&.is-active {
opacity: 1;
color: var(--mat-sys-on-surface);
color: var(--app-heading-color);
}
mat-icon {
@@ -260,15 +261,15 @@
}
.search-container:focus-within {
border-color: var(--mat-sys-primary);
background: var(--mat-sys-surface-container-highest);
border-color: var(--app-selection-color);
background: var(--app-card-hover-bg);
}
.search-container .search-icon {
font-size: 18px;
width: 18px;
height: 18px;
color: var(--mat-sys-on-surface-variant);
color: var(--app-muted-color);
}
.search-container .search-input {
@@ -284,7 +285,7 @@
}
.search-container .search-input::placeholder {
color: var(--mat-sys-on-surface-variant);
color: var(--app-muted-color);
}
.search-container .clear-btn {
@@ -313,13 +314,9 @@
padding: 0 10px;
border-radius: 999px;
border: 1px solid
color-mix(in srgb, var(--mat-sys-outline-variant) 72%, transparent);
background: color-mix(
in srgb,
var(--mat-sys-surface-container-high) 78%,
transparent
);
color: var(--mat-sys-on-surface-variant);
color-mix(in srgb, var(--app-separator) 72%, transparent);
background: var(--app-hover-overlay);
color: var(--app-muted-color);
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.02em;
@@ -328,8 +325,8 @@
transition: all 0.16s ease;
&:hover {
background: var(--mat-sys-surface-container);
color: var(--mat-sys-on-surface);
background: var(--app-hover-overlay);
color: var(--app-heading-color);
}
&.selected {
@@ -338,7 +335,7 @@
--app-selection-surface-strong,
var(--app-selection-surface)
);
color: var(--app-selection-color, var(--mat-sys-primary));
color: var(--app-selection-color);
/* box-shadow:
inset 0 0 0 1px
color-mix(in srgb, var(--app-selection-color) 18%, transparent),
@@ -348,7 +345,7 @@
}
&:focus-visible {
outline: 2px solid var(--app-selection-border, var(--mat-sys-primary));
outline: 2px solid var(--app-selection-border);
outline-offset: 1px;
}
}
@@ -409,7 +406,7 @@
height: 32px;
line-height: 32px;
flex-shrink: 0;
color: var(--mat-sys-on-surface-variant);padding: 0;
color: var(--app-muted-color);padding: 0;
mat-icon {
font-size: 18px;
@@ -429,7 +426,7 @@
::ng-deep .playlist-item-action-menu {
.danger,
.danger mat-icon {
color: var(--mat-sys-error, #f44336);
color: var(--app-error-color);
}
}
@@ -461,7 +458,7 @@
font-size: 20px;
width: 20px;
height: 20px;
color: var(--mat-sys-on-surface-variant);
color: var(--app-muted-color);
}
.status-dot {
@@ -524,8 +521,8 @@
font-size: 12px !important;
width: 14px !important;
height: 14px !important;
color: var(--mat-sys-primary);
background-color: var(--mat-menu-container-color, var(--mat-sys-surface));
color: var(--app-selection-color);
background-color: var(--mat-menu-container-color);
border-radius: 50%;
display: flex;
align-items: center;
@@ -553,7 +550,7 @@
font-size: 0.73em;
line-height: 1.25;
letter-spacing: 0.01em;
color: var(--mat-sys-on-surface-variant);
color: var(--app-muted-color);
display: flex;
align-items: center;
gap: 4px;
@@ -562,7 +559,7 @@
white-space: nowrap;
.separator {
color: var(--mat-sys-on-surface-variant);
color: var(--app-muted-color);
}
}
@@ -573,7 +570,7 @@
justify-content: center;
padding: 32px 16px;
gap: 12px;
color: var(--mat-sys-on-surface-variant);
color: var(--app-muted-color);
mat-icon {
font-size: 36px;
@@ -599,7 +596,7 @@
border: none;
border-radius: 8px;
background: transparent;
color: var(--mat-sys-on-surface-variant);
color: var(--app-muted-color);
font: inherit;
font-size: 0.86em;
font-weight: 500;
@@ -609,8 +606,8 @@
color 0.15s ease;
&:hover {
background: var(--mat-sys-surface-container);
color: var(--mat-sys-on-surface);
background: var(--app-hover-overlay);
color: var(--app-heading-color);
}
mat-icon {
@@ -87,11 +87,7 @@ const EPG_URL_PATTERN = /^\s*(http|https|file):\/\/[^ "]+\s*$/;
flex-direction: column;
gap: 10px;
padding: 12px;
border: 1px solid
var(
--app-widget-header-border,
var(--mat-sys-outline-variant)
);
border: 1px solid var(--app-widget-header-border);
border-radius: 8px;
background: var(--mat-sys-surface-container-low);
}
@@ -73,7 +73,7 @@
flex-shrink: 0;
min-height: 32px;
border-radius: 999px;
color: var(--app-body-color, var(--mat-sys-on-surface-variant));
color: var(--app-body-color);
--mdc-text-button-label-text-color: currentColor;
--mat-text-button-state-layer-color: currentColor;
@@ -154,7 +154,7 @@
font-size: 0.72rem;
font-weight: 700;
line-height: 1.2;
color: var(--app-body-color, var(--mat-sys-on-surface-variant));
color: var(--app-body-color);
text-transform: uppercase;
letter-spacing: 0.04em;
}
@@ -174,10 +174,7 @@ mat-paginator {
flex-shrink: 0;
// Shrink paginator text to fit header height comfortably
--mat-paginator-container-size: 40px;
--mat-paginator-enabled-icon-color: var(
--app-body-color,
var(--mat-sys-on-surface-variant)
);
--mat-paginator-enabled-icon-color: var(--app-body-color);
--mat-paginator-disabled-icon-color: rgba(255, 255, 255, 0.18);
}
@@ -10,8 +10,8 @@
overflow: hidden;
background: color-mix(
in srgb,
var(--app-widget-header-bg, var(--mat-sys-surface-container-high)) 56%,
var(--app-body-color, var(--mat-sys-on-surface-variant)) 44%
var(--app-widget-header-bg) 56%,
var(--app-body-color) 44%
);
&::after {
@@ -23,7 +23,7 @@
transparent 8%,
color-mix(
in srgb,
var(--app-heading-color, var(--mat-sys-on-surface)) 18%,
var(--app-heading-color) 18%,
transparent
)
44%,
@@ -57,7 +57,7 @@
justify-content: space-between;
margin-bottom: 1rem;
padding: 24px 16px 12px;
border-bottom: 1px solid var(--mat-sys-outline-variant);
border-bottom: 1px solid var(--app-separator);
&-left {
display: flex;
@@ -92,8 +92,8 @@
}
&__badge {
background: var(--mat-sys-primary);
color: var(--mat-sys-on-primary);
background: var(--app-selection-color);
color: var(--app-selection-on-color);
padding: 0.125rem 0.5rem;
border-radius: 12px;
font-size: 0.75rem;
@@ -108,19 +108,19 @@
padding: 7px 10px;
background: color-mix(
in srgb,
var(--mat-sys-surface-container-high) 86%,
var(--app-widget-header-bg) 86%,
transparent
);
border: 1px solid var(--mat-sys-outline-variant);
border: 1px solid var(--app-separator);
border-radius: 8px;
box-shadow: inset 0 1px 0
color-mix(in srgb, var(--mat-sys-on-surface) 8%, transparent);
color-mix(in srgb, var(--app-on-surface) 8%, transparent);
mat-icon {
width: 16px;
height: 16px;
font-size: 16px;
color: var(--mat-sys-primary);
color: var(--app-selection-color);
flex-shrink: 0;
}
}
@@ -130,7 +130,7 @@
'JetBrains Mono', 'SFMono-Regular', Menlo, Monaco, Consolas,
monospace;
font-size: 0.78rem;
color: var(--mat-sys-on-surface);
color: var(--app-heading-color);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -143,7 +143,7 @@
justify-content: center;
flex: 1;
text-align: center;
color: var(--mat-sys-on-surface-variant);
color: var(--app-body-color);
mat-icon {
font-size: 4rem;
@@ -158,7 +158,7 @@
font-size: 1.05rem;
font-weight: 520;
letter-spacing: -0.01em;
color: var(--mat-sys-on-surface);
color: var(--app-heading-color);
}
span {
@@ -198,7 +198,7 @@
font-size: 0.86rem;
font-weight: 600;
letter-spacing: -0.01em;
color: var(--mat-sys-on-surface);
color: var(--app-heading-color);
}
&__skeleton-item {
@@ -212,10 +212,10 @@
border-radius: 14px;
background: color-mix(
in srgb,
var(--app-widget-bg, var(--mat-sys-surface-container-high)) 88%,
var(--mat-sys-surface-container) 12%
var(--app-widget-bg) 88%,
var(--app-widget-header-bg) 12%
);
border: 1px solid var(--app-separator, var(--mat-sys-outline-variant));
border: 1px solid var(--app-separator);
box-shadow: var(--app-widget-shadow, none);
}
@@ -224,7 +224,7 @@
width: 64px;
height: 90px;
border-radius: 10px;
border: 1px solid var(--app-separator, var(--mat-sys-outline-variant));
border: 1px solid var(--app-separator);
}
&__skeleton-info {
@@ -281,7 +281,7 @@
border-top: 1px solid
color-mix(
in srgb,
var(--app-separator, var(--mat-sys-outline-variant)) 72%,
var(--app-separator) 72%,
transparent
);
@@ -300,12 +300,9 @@
gap: 1rem;
padding: 1rem;
min-width: 0;
background: var(
--app-widget-bg,
var(--mat-sys-surface-container-high)
);
background: var(--app-widget-bg);
border-radius: 14px;
border: 1px solid var(--app-separator, var(--mat-sys-outline-variant));
border: 1px solid var(--app-separator);
box-shadow: var(--app-widget-shadow, none);
transition:
background 0.18s ease,
@@ -317,7 +314,7 @@
cursor: pointer;
&:focus-visible {
outline: 2px solid var(--mat-sys-primary);
outline: 2px solid var(--app-selection-color);
outline-offset: 2px;
}
}
@@ -341,14 +338,11 @@
border-radius: 10px;
overflow: hidden;
flex-shrink: 0;
background: var(
--app-widget-header-bg,
var(--mat-sys-surface-container-high)
);
background: var(--app-widget-header-bg);
display: flex;
align-items: center;
justify-content: center;
border: 1px solid var(--app-separator, var(--mat-sys-outline-variant));
border: 1px solid var(--app-separator);
img {
width: 100%;
@@ -360,7 +354,7 @@
font-size: 2rem;
width: 2rem;
height: 2rem;
color: var(--mat-sys-on-surface-variant);
color: var(--app-body-color);
opacity: 0.5;
}
}
@@ -377,7 +371,7 @@
&-title {
font-size: 1.05rem;
font-weight: 600;
color: var(--mat-sys-on-surface);
color: var(--app-heading-color);
display: flex;
align-items: flex-start;
gap: 0.5rem;
@@ -399,7 +393,7 @@
flex-wrap: wrap;
gap: 1rem;
font-size: 0.85rem;
color: var(--mat-sys-on-surface-variant);
color: var(--app-body-color);
}
&-status {
@@ -409,10 +403,7 @@
font-weight: 500;
padding: 2px 8px;
border-radius: 12px;
background: var(
--app-widget-header-bg,
var(--mat-sys-surface-container-high)
);
background: var(--app-widget-header-bg);
mat-icon {
font-size: 1.1rem;
@@ -431,16 +422,16 @@
}
&.status-failed {
color: var(--mat-sys-error);
color: var(--app-error-color);
background: color-mix(
in srgb,
var(--mat-sys-error) 12%,
var(--app-error-color) 12%,
transparent
);
}
&.status-queued {
color: var(--mat-sys-on-surface-variant);
color: var(--app-body-color);
}
}
@@ -454,11 +445,11 @@
}
&-error {
color: var(--mat-sys-error);
color: var(--app-error-color);
font-size: 0.8rem;
background: color-mix(
in srgb,
var(--mat-sys-error) 12%,
var(--app-error-color) 12%,
transparent
);
padding: 2px 8px;
@@ -475,7 +466,7 @@
border-top: 1px solid
color-mix(
in srgb,
var(--app-separator, var(--mat-sys-outline-variant)) 72%,
var(--app-separator) 72%,
transparent
);
@@ -514,8 +505,8 @@
}
&__episode-badge {
background: var(--mat-sys-primary);
color: var(--mat-sys-on-primary);
background: var(--app-selection-color);
color: var(--app-selection-on-color);
padding: 0.125rem 0.5rem;
border-radius: 6px;
font-size: 0.7rem;
@@ -19,7 +19,7 @@
.content-header {
padding: 8px 12px;
min-height: 48px;
background: var(--app-widget-header-bg, var(--mat-sys-surface-container-high));
background: var(--app-widget-header-bg);
border-bottom: 1px solid var(--app-separator);
display: flex;
align-items: center;
@@ -59,7 +59,7 @@ mat-card:hover {
box-sizing: border-box;
display: grid;
place-items: center;
color: var(--app-muted-color, var(--mat-sys-on-surface-variant));
color: var(--app-muted-color);
border-radius: 10px;
mat-icon {
@@ -101,7 +101,7 @@ mat-card:hover {
overflow: hidden;
font-size: 0.8rem;
line-height: 1.35;
color: var(--app-body-color, var(--mat-sys-on-surface-variant));
color: var(--app-body-color);
}
}
}
@@ -34,7 +34,7 @@
gap: 14px;
text-align: center;
box-sizing: border-box;
color: var(--app-body-color, var(--mat-sys-on-surface-variant));
color: var(--app-body-color);
img {
width: min(100%, var(--error-view-illustration-size));
@@ -69,7 +69,7 @@
font-size: var(--error-view-title-size);
font-weight: var(--error-view-title-weight);
line-height: 1.12;
color: var(--app-heading-color, var(--mat-sys-on-surface));
color: var(--app-heading-color);
letter-spacing: var(--error-view-title-spacing);
text-wrap: balance;
}
@@ -78,17 +78,3 @@ nav.flex .portal-rail-link.active {
inset 0 0 0 1px var(--app-selection-border),
0 10px 20px -16px var(--app-selection-glow);
}
nav.flex .portal-rail-link.is-active::after,
nav.flex .portal-rail-link.active::after {
content: '';
position: absolute;
right: -4px;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 20px;
border-radius: 999px;
background: var(--app-selection-color);
box-shadow: 0 0 10px var(--app-selection-glow);
}
@@ -15,13 +15,13 @@
overflow: hidden;
background: color-mix(
in srgb,
var(--app-widget-header-bg, var(--mat-sys-surface-container-high)) 84%,
var(--app-widget-bg, var(--mat-sys-surface-container)) 16%
var(--app-widget-header-bg) 84%,
var(--app-widget-bg) 16%
);
border: 1px solid
color-mix(
in srgb,
var(--app-widget-header-border, var(--mat-sys-outline-variant)) 94%,
var(--app-widget-header-border) 94%,
transparent
);
}
@@ -35,8 +35,8 @@
border-radius: 999px;
background: color-mix(
in srgb,
var(--app-widget-header-bg, var(--mat-sys-surface-container-high)) 56%,
var(--app-body-color, var(--mat-sys-on-surface-variant)) 44%
var(--app-widget-header-bg) 56%,
var(--app-body-color) 44%
);
}
@@ -52,8 +52,8 @@
border-radius: 999px;
background: color-mix(
in srgb,
var(--app-widget-header-bg, var(--mat-sys-surface-container-high)) 52%,
var(--app-body-color, var(--mat-sys-on-surface-variant)) 48%
var(--app-widget-header-bg) 52%,
var(--app-body-color) 48%
);
&--primary {
@@ -87,8 +87,8 @@
flex-shrink: 0;
background: color-mix(
in srgb,
var(--app-widget-header-bg, var(--mat-sys-surface-container-high)) 60%,
var(--app-body-color, var(--mat-sys-on-surface-variant)) 40%
var(--app-widget-header-bg) 60%,
var(--app-body-color) 40%
);
}
@@ -99,8 +99,8 @@
flex-shrink: 0;
background: color-mix(
in srgb,
var(--app-widget-header-bg, var(--mat-sys-surface-container-high)) 84%,
var(--app-widget-bg, var(--mat-sys-surface-container)) 16%
var(--app-widget-header-bg) 84%,
var(--app-widget-bg) 16%
);
}
@@ -118,8 +118,8 @@
flex-shrink: 0;
background: color-mix(
in srgb,
var(--app-widget-header-bg, var(--mat-sys-surface-container-high)) 60%,
var(--app-body-color, var(--mat-sys-on-surface-variant)) 40%
var(--app-widget-header-bg) 60%,
var(--app-body-color) 40%
);
}
@@ -144,7 +144,7 @@
transparent 10%,
color-mix(
in srgb,
var(--app-heading-color, var(--mat-sys-on-surface)) 20%,
var(--app-heading-color) 20%,
transparent
)
46%,
@@ -1,6 +1,6 @@
:host {
display: block;
color: var(--app-body-color, var(--mat-sys-on-surface));
color: var(--app-body-color);
}
// ─── Hero header ────────────────────────────────────────────────────────────
@@ -11,7 +11,7 @@
gap: 18px;
background:
var(--app-accent-gradient),
var(--app-widget-header-bg, var(--mat-sys-surface-container-high));
var(--app-widget-header-bg);
border-bottom: 1px solid var(--app-widget-header-border, rgba(255, 255, 255, 0.05));
}
@@ -25,7 +25,7 @@
align-items: center;
gap: 8px;
margin: 0;
color: var(--app-eyebrow-color, var(--mat-sys-on-surface-variant));
color: var(--app-eyebrow-color);
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.1em;
@@ -36,8 +36,8 @@
width: 8px;
height: 8px;
border-radius: 999px;
background: var(--app-provider-xtream, var(--mat-sys-primary));
box-shadow: 0 0 0 4px color-mix(in srgb, var(--app-provider-xtream, var(--mat-sys-primary)) 14%, transparent);
background: var(--app-provider-xtream);
box-shadow: 0 0 0 4px color-mix(in srgb, var(--app-provider-xtream) 14%, transparent);
}
.account-dialog__eyebrow-divider {
@@ -61,12 +61,12 @@
line-height: 1.2;
font-weight: 650;
letter-spacing: -0.02em;
color: var(--app-heading-color, var(--mat-sys-on-surface));
color: var(--app-heading-color);
}
.account-dialog__subtitle {
margin: 4px 0 0;
color: var(--app-muted-color, var(--mat-sys-on-surface-variant));
color: var(--app-muted-color);
font-size: 0.88rem;
}
@@ -86,8 +86,8 @@
padding: 0 10px;
border-radius: 999px;
border: 1px solid var(--app-separator);
background: var(--mat-sys-surface-container);
color: var(--app-body-color, var(--mat-sys-on-surface));
background: var(--app-hover-overlay);
color: var(--app-body-color);
font-size: 0.76rem;
font-weight: 600;
letter-spacing: 0.06em;
@@ -111,14 +111,14 @@
.account-dialog__loading-copy {
margin: 0;
max-width: 60ch;
color: var(--app-body-color, var(--mat-sys-on-surface));
color: var(--app-body-color);
}
.account-dialog__message {
padding: 10px 14px;
border-radius: 10px;
border: 1px solid var(--app-separator);
background: var(--mat-sys-surface-container);
background: var(--app-hover-overlay);
line-height: 1.5;
font-size: 0.88rem;
}
@@ -136,7 +136,7 @@
padding: 12px 14px;
border-radius: 12px;
border: 1px solid var(--app-widget-border, transparent);
background: var(--app-widget-bg, var(--mat-sys-surface-container));
background: var(--app-widget-bg);
box-shadow: var(--app-widget-shadow, none);
// Subtle inset highlight matching widget-shell pattern
outline: 1px solid rgba(255, 255, 255, 0.04);
@@ -150,8 +150,8 @@
align-items: center;
justify-content: center;
border-radius: 10px;
background: color-mix(in srgb, var(--app-provider-xtream, var(--mat-sys-primary)) 12%, transparent);
color: var(--app-provider-xtream, var(--mat-sys-primary));
background: color-mix(in srgb, var(--app-provider-xtream) 12%, transparent);
color: var(--app-provider-xtream);
mat-icon {
font-size: 18px;
@@ -161,7 +161,7 @@
}
.account-stat__label {
color: var(--app-muted-color, var(--mat-sys-on-surface-variant));
color: var(--app-muted-color);
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.1em;
@@ -172,7 +172,7 @@
font-size: 1.3rem;
font-weight: 650;
letter-spacing: -0.03em;
color: var(--app-heading-color, var(--mat-sys-on-surface));
color: var(--app-heading-color);
}
.account-stat__meter {
@@ -187,14 +187,14 @@
display: block;
height: 100%;
border-radius: inherit;
background: var(--app-provider-xtream, var(--mat-sys-primary));
background: var(--app-provider-xtream);
}
// ─── Content area ───────────────────────────────────────────────────────────
.account-dialog__content {
margin: 0;
padding: 20px;
background: var(--app-widget-bg, var(--mat-sys-surface));
background: var(--app-widget-bg);
}
.account-dialog__grid {
@@ -210,7 +210,7 @@
padding: 16px;
border-radius: 16px;
border: 1px solid var(--app-widget-border, transparent);
background: var(--app-widget-bg, var(--mat-sys-surface-container-low));
background: var(--app-widget-bg);
box-shadow: var(--app-widget-shadow, none);
outline: 1px solid rgba(255, 255, 255, 0.04);
outline-offset: -1px;
@@ -226,12 +226,12 @@
font-size: 1rem;
font-weight: 600;
letter-spacing: -0.01em;
color: var(--app-heading-color, var(--mat-sys-on-surface));
color: var(--app-heading-color);
}
.account-panel__eyebrow {
margin: 0;
color: var(--app-eyebrow-color, var(--mat-sys-on-surface-variant));
color: var(--app-eyebrow-color);
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.1em;
@@ -261,7 +261,7 @@
.account-detail-row dt {
margin: 0;
color: var(--app-muted-color, var(--mat-sys-on-surface-variant));
color: var(--app-muted-color);
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.1em;
@@ -270,7 +270,7 @@
.account-detail-row dd {
margin: 0;
color: var(--app-body-color, var(--mat-sys-on-surface));
color: var(--app-body-color);
font-size: 0.9rem;
font-weight: 500;
word-break: break-word;
@@ -282,7 +282,7 @@
}
.account-detail-row__value--accent {
color: var(--app-provider-xtream, var(--mat-sys-primary));
color: var(--app-provider-xtream);
}
.account-detail-row__value--positive {
@@ -301,7 +301,7 @@
.account-chip-section__label {
margin: 0;
color: var(--app-muted-color, var(--mat-sys-on-surface-variant));
color: var(--app-muted-color);
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.1em;
@@ -320,9 +320,9 @@
min-height: 28px;
padding: 0 10px;
border-radius: 999px;
border: 1px solid color-mix(in srgb, var(--app-provider-xtream, var(--mat-sys-primary)) 16%, transparent);
background: color-mix(in srgb, var(--app-provider-xtream, var(--mat-sys-primary)) 8%, transparent);
color: var(--app-body-color, var(--mat-sys-on-surface));
border: 1px solid color-mix(in srgb, var(--app-provider-xtream) 16%, transparent);
background: color-mix(in srgb, var(--app-provider-xtream) 8%, transparent);
color: var(--app-body-color);
font-size: 0.8rem;
font-weight: 600;
text-transform: uppercase;
@@ -335,8 +335,8 @@
.account-chip--muted {
border-color: var(--app-separator);
background: var(--mat-sys-surface-container);
color: var(--app-muted-color, var(--mat-sys-on-surface-variant));
background: var(--app-hover-overlay);
color: var(--app-muted-color);
}
// ─── Error state ────────────────────────────────────────────────────────────
@@ -347,7 +347,7 @@
padding: 36px 20px;
border-radius: 16px;
border: 1px dashed var(--app-separator);
background: var(--app-widget-bg, var(--mat-sys-surface-container-low));
background: var(--app-widget-bg);
text-align: center;
}
@@ -358,7 +358,7 @@
.account-dialog__error-state p {
max-width: 52ch;
color: var(--app-muted-color, var(--mat-sys-on-surface-variant));
color: var(--app-muted-color);
line-height: 1.5;
font-size: 0.88rem;
}
@@ -370,8 +370,8 @@
align-items: center;
justify-content: center;
border-radius: 14px;
background: color-mix(in srgb, var(--mat-sys-error) 12%, transparent);
color: var(--mat-sys-error);
background: color-mix(in srgb, var(--app-error-color) 12%, transparent);
color: var(--app-error-color);
}
// ─── Skeleton loading ───────────────────────────────────────────────────────
@@ -394,7 +394,7 @@
background: linear-gradient(
90deg,
transparent,
color-mix(in srgb, var(--app-heading-color, var(--mat-sys-on-surface)) 10%, transparent),
color-mix(in srgb, var(--app-heading-color) 10%, transparent),
transparent
);
animation: account-dialog-shimmer 1.4s infinite;
@@ -426,7 +426,7 @@
justify-content: flex-end;
gap: 10px;
border-top: 1px solid var(--app-widget-header-border, rgba(255, 255, 255, 0.05));
background: var(--app-widget-bg, var(--mat-sys-surface));
background: var(--app-widget-bg);
}
.account-dialog__close {
@@ -27,10 +27,10 @@
.selection-info {
font-size: 0.8125rem;
color: var(--mat-sys-on-surface-variant);
color: var(--app-muted-color);
strong {
color: var(--mat-sys-primary);
color: var(--app-selection-color);
}
}
@@ -50,17 +50,17 @@
padding: 0 10px;
min-height: 36px;
border-radius: 10px;
border: 1px solid var(--app-search-border, var(--mat-sys-outline-variant));
background: var(--app-search-bg, var(--mat-sys-surface-container));
border: 1px solid var(--app-search-border);
background: var(--app-search-bg);
cursor: text;
transition:
border-color 0.15s ease,
box-shadow 0.15s ease;
&:focus-within {
border-color: var(--mat-sys-primary);
border-color: var(--app-selection-color);
box-shadow: 0 0 0 3px
color-mix(in srgb, var(--mat-sys-primary) 12%, transparent);
color-mix(in srgb, var(--app-selection-color) 12%, transparent);
}
}
@@ -68,7 +68,7 @@
font-size: 18px;
width: 18px;
height: 18px;
color: var(--mat-sys-on-surface-variant);
color: var(--app-muted-color);
flex-shrink: 0;
}
@@ -77,14 +77,14 @@
border: none;
outline: none;
background: transparent;
color: var(--mat-sys-on-surface);
color: var(--app-heading-color);
font: inherit;
font-size: 0.84rem;
width: 100%;
min-width: 0;
&::placeholder {
color: var(--mat-sys-on-surface-variant);
color: var(--app-muted-color);
opacity: 0.6;
}
@@ -112,7 +112,7 @@
flex: 1;
min-height: 200px;
overflow-y: auto;
border: 1px solid var(--mat-sys-outline-variant);
border: 1px solid var(--app-separator);
border-radius: 8px;
}
@@ -126,11 +126,11 @@
transition: background-color 0.12s ease;
&:hover {
background-color: var(--mat-sys-surface-container);
background-color: var(--app-hover-overlay);
}
&:not(:last-child) {
border-bottom: 1px solid var(--mat-sys-surface-container);
border-bottom: 1px solid var(--app-widget-header-border);
}
mat-checkbox {
@@ -149,7 +149,7 @@
.empty-message {
padding: 24px;
text-align: center;
color: var(--mat-sys-on-surface-variant);
color: var(--app-muted-color);
font-size: 0.8125rem;
}
@@ -61,10 +61,7 @@ mat-paginator {
background: transparent !important;
flex-shrink: 0;
--mat-paginator-container-size: 40px;
--mat-paginator-enabled-icon-color: var(
--app-body-color,
var(--mat-sys-on-surface-variant)
);
--mat-paginator-enabled-icon-color: var(--app-body-color);
--mat-paginator-disabled-icon-color: rgba(255, 255, 255, 0.18);
}
@@ -35,7 +35,7 @@
box-sizing: border-box;
display: flex;
align-items: center;
background: var(--app-widget-header-bg, var(--mat-sys-surface-container-high));
background: var(--app-widget-header-bg);
border-bottom: 1px solid var(--app-separator);
app-region: drag;
}
@@ -1,6 +1,6 @@
:host {
display: block;
color: var(--app-body-color, var(--mat-sys-on-surface));
color: var(--app-body-color);
}
// ─── Hero header ────────────────────────────────────────────────────────────
@@ -11,7 +11,7 @@
gap: 18px;
background:
var(--app-accent-gradient),
var(--app-widget-header-bg, var(--mat-sys-surface-container-high));
var(--app-widget-header-bg);
border-bottom: 1px solid var(--app-widget-header-border, rgba(255, 255, 255, 0.05));
}
@@ -25,7 +25,7 @@
align-items: center;
gap: 8px;
margin: 0;
color: var(--app-eyebrow-color, var(--mat-sys-on-surface-variant));
color: var(--app-eyebrow-color);
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.1em;
@@ -36,8 +36,8 @@
width: 8px;
height: 8px;
border-radius: 999px;
background: var(--mat-sys-primary);
box-shadow: 0 0 0 4px color-mix(in srgb, var(--mat-sys-primary) 14%, transparent);
background: var(--app-selection-color);
box-shadow: 0 0 0 4px color-mix(in srgb, var(--app-selection-color) 14%, transparent);
}
.channel-dialog__headline-row {
@@ -59,7 +59,7 @@
height: 48px;
border-radius: 12px;
object-fit: contain;
background: var(--mat-sys-surface-container);
background: var(--app-widget-bg);
border: 1px solid var(--app-separator);
flex-shrink: 0;
}
@@ -70,12 +70,12 @@
line-height: 1.2;
font-weight: 650;
letter-spacing: -0.02em;
color: var(--app-heading-color, var(--mat-sys-on-surface));
color: var(--app-heading-color);
}
.channel-dialog__subtitle {
margin: 4px 0 0;
color: var(--app-muted-color, var(--mat-sys-on-surface-variant));
color: var(--app-muted-color);
font-size: 0.88rem;
}
@@ -96,8 +96,8 @@
padding: 0 10px;
border-radius: 999px;
border: 1px solid var(--app-separator);
background: var(--mat-sys-surface-container);
color: var(--app-body-color, var(--mat-sys-on-surface));
background: var(--app-widget-bg);
color: var(--app-body-color);
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.06em;
@@ -111,15 +111,15 @@
}
.channel-dialog__status-pill--radio {
border-color: color-mix(in srgb, var(--mat-sys-tertiary) 30%, transparent);
background: color-mix(in srgb, var(--mat-sys-tertiary) 12%, transparent);
color: var(--mat-sys-tertiary);
border-color: color-mix(in srgb, var(--app-accent-color) 30%, transparent);
background: color-mix(in srgb, var(--app-accent-color) 12%, transparent);
color: var(--app-accent-color);
}
.channel-dialog__status-pill--playback {
border-color: color-mix(in srgb, var(--mat-sys-primary) 30%, transparent);
background: color-mix(in srgb, var(--mat-sys-primary) 12%, transparent);
color: var(--mat-sys-primary);
border-color: color-mix(in srgb, var(--app-selection-color) 30%, transparent);
background: color-mix(in srgb, var(--app-selection-color) 12%, transparent);
color: var(--app-selection-color);
}
// ─── Hero stat cards ────────────────────────────────────────────────────────
@@ -135,7 +135,7 @@
padding: 12px 14px;
border-radius: 12px;
border: 1px solid var(--app-widget-border, transparent);
background: var(--app-widget-bg, var(--mat-sys-surface-container));
background: var(--app-widget-bg);
box-shadow: var(--app-widget-shadow, none);
outline: 1px solid rgba(255, 255, 255, 0.04);
outline-offset: -1px;
@@ -148,8 +148,8 @@
align-items: center;
justify-content: center;
border-radius: 10px;
background: color-mix(in srgb, var(--mat-sys-primary) 12%, transparent);
color: var(--mat-sys-primary);
background: color-mix(in srgb, var(--app-selection-color) 12%, transparent);
color: var(--app-selection-color);
mat-icon {
font-size: 18px;
@@ -159,7 +159,7 @@
}
.channel-stat__label {
color: var(--app-muted-color, var(--mat-sys-on-surface-variant));
color: var(--app-muted-color);
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.1em;
@@ -170,7 +170,7 @@
font-size: 1.1rem;
font-weight: 650;
letter-spacing: -0.02em;
color: var(--app-heading-color, var(--mat-sys-on-surface));
color: var(--app-heading-color);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -181,7 +181,7 @@
}
&--empty {
color: var(--app-muted-color, var(--mat-sys-on-surface-variant));
color: var(--app-muted-color);
font-style: italic;
font-weight: 500;
font-size: 0.88rem;
@@ -192,7 +192,7 @@
.channel-dialog__content {
margin: 0;
padding: 20px;
background: var(--app-widget-bg, var(--mat-sys-surface));
background: var(--app-widget-bg);
}
.channel-dialog__grid {
@@ -209,7 +209,7 @@
padding: 10px 10px 10px 14px;
border-radius: 10px;
border: 1px solid var(--app-separator);
background: var(--mat-sys-surface-container);
background: var(--app-widget-bg);
code {
flex: 1;
@@ -217,7 +217,7 @@
font-family: 'JetBrains Mono', 'Roboto Mono', monospace;
font-size: 0.82rem;
line-height: 1.5;
color: var(--app-body-color, var(--mat-sys-on-surface));
color: var(--app-body-color);
overflow-wrap: anywhere;
word-break: break-word;
}
@@ -229,7 +229,7 @@
height: 32px;
--mdc-icon-button-state-layer-size: 32px;
--mdc-icon-button-icon-size: 18px;
color: var(--app-muted-color, var(--mat-sys-on-surface-variant));
color: var(--app-muted-color);
}
// ─── Detail panels ──────────────────────────────────────────────────────────
@@ -239,7 +239,7 @@
padding: 16px;
border-radius: 16px;
border: 1px solid var(--app-widget-border, transparent);
background: var(--app-widget-bg, var(--mat-sys-surface-container-low));
background: var(--app-widget-bg);
box-shadow: var(--app-widget-shadow, none);
outline: 1px solid rgba(255, 255, 255, 0.04);
outline-offset: -1px;
@@ -256,12 +256,12 @@
font-size: 1rem;
font-weight: 600;
letter-spacing: -0.01em;
color: var(--app-heading-color, var(--mat-sys-on-surface));
color: var(--app-heading-color);
}
.channel-panel__eyebrow {
margin: 0;
color: var(--app-eyebrow-color, var(--mat-sys-on-surface-variant));
color: var(--app-eyebrow-color);
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.1em;
@@ -291,7 +291,7 @@
.channel-detail-row dt {
margin: 0;
color: var(--app-muted-color, var(--mat-sys-on-surface-variant));
color: var(--app-muted-color);
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.1em;
@@ -300,7 +300,7 @@
.channel-detail-row dd {
margin: 0;
color: var(--app-body-color, var(--mat-sys-on-surface));
color: var(--app-body-color);
font-size: 0.88rem;
font-weight: 500;
word-break: break-word;
@@ -313,7 +313,7 @@
}
.channel-detail-row__value--empty {
color: var(--app-muted-color, var(--mat-sys-on-surface-variant));
color: var(--app-muted-color);
font-style: italic;
}
@@ -326,7 +326,7 @@
.channel-chip-section__label {
margin: 0;
color: var(--app-eyebrow-color, var(--mat-sys-on-surface-variant));
color: var(--app-eyebrow-color);
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.1em;
@@ -342,7 +342,7 @@
justify-content: flex-end;
gap: 10px;
border-top: 1px solid var(--app-widget-header-border, rgba(255, 255, 255, 0.05));
background: var(--app-widget-bg, var(--mat-sys-surface));
background: var(--app-widget-bg);
}
.channel-dialog__close {
@@ -18,10 +18,10 @@
.selection-info {
font-size: 0.8125rem;
color: var(--mat-sys-on-surface-variant);
color: var(--app-muted-color);
strong {
color: var(--mat-sys-primary);
color: var(--app-selection-color);
}
}
@@ -41,17 +41,17 @@
padding: 0 10px;
min-height: 36px;
border-radius: 10px;
border: 1px solid var(--app-search-border, var(--mat-sys-outline-variant));
background: var(--app-search-bg, var(--mat-sys-surface-container));
border: 1px solid var(--app-search-border);
background: var(--app-search-bg);
cursor: text;
transition:
border-color 0.15s ease,
box-shadow 0.15s ease;
&:focus-within {
border-color: var(--mat-sys-primary);
border-color: var(--app-selection-color);
box-shadow: 0 0 0 3px
color-mix(in srgb, var(--mat-sys-primary) 12%, transparent);
color-mix(in srgb, var(--app-selection-color) 12%, transparent);
}
}
@@ -59,7 +59,7 @@
width: 18px;
height: 18px;
font-size: 18px;
color: var(--mat-sys-on-surface-variant);
color: var(--app-muted-color);
flex-shrink: 0;
}
@@ -70,12 +70,12 @@
border: none;
outline: none;
background: transparent;
color: var(--mat-sys-on-surface);
color: var(--app-heading-color);
font: inherit;
font-size: 0.84rem;
&::placeholder {
color: var(--mat-sys-on-surface-variant);
color: var(--app-muted-color);
opacity: 0.6;
}
@@ -103,7 +103,7 @@
flex: 1;
min-height: 200px;
overflow-y: auto;
border: 1px solid var(--mat-sys-outline-variant);
border: 1px solid var(--app-separator);
border-radius: 8px;
}
@@ -117,11 +117,11 @@
transition: background-color 0.12s ease;
&:hover {
background-color: var(--mat-sys-surface-container);
background-color: var(--app-hover-overlay);
}
&:not(:last-child) {
border-bottom: 1px solid var(--mat-sys-surface-container);
border-bottom: 1px solid var(--app-widget-header-border);
}
mat-checkbox {
@@ -139,12 +139,12 @@
.item-count {
font-size: 0.8rem;
color: var(--mat-sys-on-surface-variant);
color: var(--app-muted-color);
}
.empty-message {
padding: 24px;
text-align: center;
color: var(--mat-sys-on-surface-variant);
color: var(--app-muted-color);
font-size: 0.8125rem;
}
@@ -16,7 +16,7 @@
height: 100%;
min-height: 0;
overflow: hidden;
background: var(--app-content-bg, var(--mat-sys-surface));
background: var(--app-content-bg);
}
.groups-nav-panel {
@@ -27,7 +27,7 @@
min-height: 0;
display: flex;
overflow: hidden;
background: var(--app-rail-bg, var(--mat-sys-surface-container-low));
background: var(--app-rail-bg);
padding: 12px 10px 10px 12px;
box-sizing: border-box;
container-type: inline-size;
@@ -96,8 +96,8 @@
padding: 0 10px;
min-height: 36px;
border-radius: 10px;
border: 1px solid var(--app-search-border, var(--mat-sys-outline-variant));
background: var(--app-search-bg, var(--mat-sys-surface-container));
border: 1px solid var(--app-search-border);
background: var(--app-search-bg);
cursor: text;
transition:
border-color 0.15s ease,
@@ -194,7 +194,7 @@
align-items: center;
text-align: center;
padding: 24px 12px;
color: var(--app-muted-color, var(--mat-sys-on-surface-variant));
color: var(--app-muted-color);
&__icon {
width: 28px;
@@ -243,7 +243,7 @@
flex-direction: column;
overflow: hidden;
border-right: none;
background: var(--app-content-bg, var(--mat-sys-surface));
background: var(--app-content-bg);
border-top-left-radius: 16px;
}
@@ -61,7 +61,7 @@
height: 100%;
min-height: 0;
overflow: hidden;
background: var(--app-content-bg, var(--mat-sys-surface));
background: var(--app-content-bg);
}
.groups-loading-nav {
@@ -73,7 +73,7 @@
display: flex;
flex-direction: column;
overflow: hidden;
background: var(--app-rail-bg, var(--mat-sys-surface-container-low));
background: var(--app-rail-bg);
padding: 12px 10px 10px 12px;
box-sizing: border-box;
}
@@ -100,15 +100,11 @@
border-radius: 12px;
background: color-mix(
in srgb,
var(--app-widget-header-bg, var(--mat-sys-surface-container-high)) 88%,
var(--app-widget-bg, var(--mat-sys-surface-container)) 12%
var(--app-widget-header-bg) 88%,
var(--app-widget-bg) 12%
);
border: 1px solid
color-mix(
in srgb,
var(--app-widget-header-border, var(--mat-sys-outline-variant)) 96%,
transparent
);
color-mix(in srgb, var(--app-widget-header-border) 96%, transparent);
}
.groups-loading-nav__label,
@@ -135,7 +131,7 @@
display: flex;
flex-direction: column;
overflow: hidden;
background: var(--app-content-bg, var(--mat-sys-surface));
background: var(--app-content-bg);
border-top-left-radius: 16px;
}
@@ -116,7 +116,7 @@
font-size: 64px;
width: 64px;
height: 64px;
color: var(--app-selection-color, var(--mat-sys-primary));
color: var(--app-selection-color);
opacity: 0.5;
}
}
@@ -205,8 +205,8 @@
// Play / Pause uses the unified app primary, not the station's dominant color
// — same blue as every other CTA so radio doesn't feel like a different app.
.play-btn {
--mdc-fab-container-color: var(--app-selection-color, var(--mat-sys-primary));
--mat-fab-container-color: var(--app-selection-color, var(--mat-sys-primary));
--mdc-fab-container-color: var(--app-selection-color);
--mat-fab-container-color: var(--app-selection-color);
--mdc-fab-icon-color: #fff;
--mat-fab-foreground-color: #fff;
--mat-fab-icon-color: #fff;
@@ -243,10 +243,10 @@
.vol-slider {
flex: 1;
--mdc-slider-active-track-color: var(--app-selection-color, var(--mat-sys-primary));
--mdc-slider-handle-color: var(--app-selection-color, var(--mat-sys-primary));
--mat-slider-active-track-color: var(--app-selection-color, var(--mat-sys-primary));
--mat-slider-handle-color: var(--app-selection-color, var(--mat-sys-primary));
--mdc-slider-active-track-color: var(--app-selection-color);
--mdc-slider-handle-color: var(--app-selection-color);
--mat-slider-active-track-color: var(--app-selection-color);
--mat-slider-handle-color: var(--app-selection-color);
--mdc-slider-inactive-track-color: var(--mat-sys-outline-variant);
--mdc-slider-handle-width: 12px;
--mdc-slider-handle-height: 12px;
+3 -3
View File
@@ -13,7 +13,7 @@
padding: $padding;
min-height: $min-height;
flex-shrink: 0;
background: var(--app-widget-header-bg, var(--mat-sys-surface-container-high));
background: var(--app-widget-header-bg);
border-bottom: 1px solid $border-color;
@if $sticky {
@@ -38,7 +38,7 @@
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: var(--app-heading-color, var(--mat-sys-on-surface));
color: var(--app-heading-color);
letter-spacing: -0.01em;
}
@@ -46,7 +46,7 @@
$font-size: 0.78rem,
$font-weight: 500,
$padding: 2px 8px,
$color: var(--app-body-color, var(--mat-sys-on-surface-variant)),
$color: var(--app-body-color),
$background: var(--mat-sys-surface-container)
) {
white-space: nowrap;
+1 -1
View File
@@ -17,7 +17,7 @@
.sidebar-header {
padding: 8px 12px;
background: var(--app-widget-header-bg, var(--mat-sys-surface-container-high));
background: var(--app-widget-header-bg);
border-bottom: 1px solid var(--app-separator);
app-region: drag;
display: flex;
@@ -16,13 +16,13 @@
border-radius: 12px;
background: color-mix(
in srgb,
var(--app-selection-surface, var(--mat-sys-surface-container)) 72%,
var(--app-selection-surface) 72%,
transparent
);
border: 1px solid
color-mix(
in srgb,
var(--app-selection-border, var(--mat-sys-outline-variant)) 70%,
var(--app-selection-border) 70%,
transparent
);
color: var(--mat-sys-on-surface-variant);
@@ -52,8 +52,8 @@
border-radius: 999px;
background: color-mix(
in srgb,
var(--app-widget-header-bg, var(--mat-sys-surface-container-high)) 52%,
var(--app-body-color, var(--mat-sys-on-surface-variant)) 48%
var(--app-widget-header-bg) 52%,
var(--app-body-color) 48%
);
&::after {
@@ -65,7 +65,7 @@
transparent 8%,
color-mix(
in srgb,
var(--app-heading-color, var(--mat-sys-on-surface)) 18%,
var(--app-heading-color) 18%,
transparent
)
44%,
@@ -34,7 +34,7 @@
gap: 14px;
text-align: center;
box-sizing: border-box;
color: var(--app-body-color, var(--mat-sys-on-surface-variant));
color: var(--app-body-color);
img {
width: min(100%, var(--error-view-illustration-size));
@@ -68,7 +68,7 @@
font-size: var(--error-view-title-size);
font-weight: var(--error-view-title-weight);
line-height: 1.12;
color: var(--app-heading-color, var(--mat-sys-on-surface));
color: var(--app-heading-color);
letter-spacing: var(--error-view-title-spacing);
text-wrap: balance;
}
@@ -31,7 +31,7 @@
font-size: 0.94rem;
font-weight: 600;
letter-spacing: -0.01em;
color: var(--mat-sys-on-surface);
color: var(--app-heading-color);
}
}
@@ -54,14 +54,14 @@
}
&--active mat-icon {
color: var(--mat-sys-primary);
color: var(--app-selection-color);
}
mat-icon {
font-size: 18px;
width: 18px;
height: 18px;
color: var(--mat-sys-on-surface-variant);
color: var(--app-muted-color);
}
}
@@ -72,8 +72,8 @@
margin-top: 5px;
padding: 2px 0;
border-radius: 999px;
background: var(--mat-sys-surface-container);
color: var(--mat-sys-on-surface-variant);
background: var(--app-hover-overlay);
color: var(--app-muted-color);
font-size: 0.72rem;
font-weight: 500;
letter-spacing: 0.04em;
@@ -87,7 +87,7 @@
.context-header p {
margin: 4px 0 0;
font-size: 0.78rem;
color: var(--mat-sys-on-surface-variant);
color: var(--app-muted-color);
}
// ─── Search Bar ──────────────────────────────────────────────────────────────
@@ -99,8 +99,8 @@
padding: 0 10px;
min-height: 36px;
border-radius: 10px;
border: 1px solid var(--app-search-border, var(--mat-sys-outline-variant));
background: var(--app-search-bg, var(--mat-sys-surface-container));
border: 1px solid var(--app-search-border);
background: var(--app-search-bg);
flex-shrink: 0;
cursor: text;
transition:
@@ -108,9 +108,9 @@
box-shadow 0.15s ease;
&:focus-within {
border-color: var(--mat-sys-primary);
border-color: var(--app-selection-color);
box-shadow: 0 0 0 3px
color-mix(in srgb, var(--mat-sys-primary) 12%, transparent);
color-mix(in srgb, var(--app-selection-color) 12%, transparent);
}
}
@@ -118,7 +118,7 @@
font-size: 18px;
width: 18px;
height: 18px;
color: var(--mat-sys-on-surface-variant);
color: var(--app-muted-color);
flex-shrink: 0;
}
@@ -127,14 +127,14 @@
border: none;
outline: none;
background: transparent;
color: var(--mat-sys-on-surface);
color: var(--app-heading-color);
font: inherit;
font-size: 0.84rem;
width: 100%;
min-width: 0;
&::placeholder {
color: var(--mat-sys-on-surface-variant);
color: var(--app-muted-color);
opacity: 0.6;
}
@@ -161,7 +161,7 @@
// ─── Divider ──────────────────────────────────────────────────────────────────
.context-divider {
height: 1px;
background: var(--mat-sys-outline-variant);
background: var(--app-separator);
margin: 0 0 6px;
opacity: 0.6;
flex-shrink: 0;
@@ -197,15 +197,11 @@ app-workspace-context-category-view {
border-radius: 12px;
background: color-mix(
in srgb,
var(--app-widget-header-bg, var(--mat-sys-surface-container-high)) 88%,
var(--app-widget-bg, var(--mat-sys-surface-container)) 12%
var(--app-widget-header-bg) 88%,
var(--app-widget-bg) 12%
);
border: 1px solid
color-mix(
in srgb,
var(--app-widget-header-border, var(--mat-sys-outline-variant)) 96%,
transparent
);
color-mix(in srgb, var(--app-widget-header-border) 96%, transparent);
}
.context-skeleton__dot,
@@ -215,8 +211,8 @@ app-workspace-context-category-view {
overflow: hidden;
background: color-mix(
in srgb,
var(--app-widget-header-bg, var(--mat-sys-surface-container-high)) 52%,
var(--app-body-color, var(--mat-sys-on-surface-variant)) 48%
var(--app-widget-header-bg) 52%,
var(--app-body-color) 48%
);
&::after {
@@ -226,12 +222,7 @@ app-workspace-context-category-view {
background: linear-gradient(
100deg,
transparent 8%,
color-mix(
in srgb,
var(--app-heading-color, var(--mat-sys-on-surface)) 18%,
transparent
)
44%,
color-mix(in srgb, var(--app-heading-color) 18%, transparent) 44%,
transparent 74%
);
transform: translateX(-120%);
@@ -6,7 +6,7 @@
.context-panel {
border-right: 1px solid var(--mat-sys-outline-variant);
background: var(--app-rail-bg, var(--mat-sys-surface-container-low));
background: var(--app-rail-bg);
display: flex;
flex-direction: column;
gap: 6px;
@@ -9,8 +9,8 @@
gap: 12px;
height: 56px;
padding: 0 24px;
border-bottom: 1px solid var(--mat-sys-outline-variant);
background: var(--app-header-bg, var(--mat-sys-surface-container-low));
border-bottom: 1px solid var(--app-widget-header-border);
background: var(--app-header-bg);
app-region: drag;
}
@@ -26,8 +26,8 @@
max-width: 560px;
height: 36px;
border-radius: 10px;
border: 1px solid var(--app-search-border, var(--mat-sys-outline-variant));
background: var(--app-search-bg, var(--mat-sys-surface-container));
border: 1px solid var(--app-search-border);
background: var(--app-search-bg);
display: flex;
align-items: center;
gap: 8px;
@@ -39,16 +39,16 @@
app-region: no-drag;
&:focus-within {
border-color: var(--mat-sys-primary);
border-color: var(--app-selection-color);
box-shadow: 0 0 0 3px
color-mix(in srgb, var(--mat-sys-primary) 12%, transparent);
color-mix(in srgb, var(--app-selection-color) 12%, transparent);
}
mat-icon {
font-size: 18px;
width: 18px;
height: 18px;
color: var(--mat-sys-on-surface-variant);
color: var(--app-muted-color);
flex-shrink: 0;
}
@@ -60,14 +60,10 @@
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--mat-sys-on-surface-variant);
background: color-mix(
in srgb,
var(--mat-sys-surface-container-high) 76%,
transparent
);
color: var(--app-muted-color);
background: var(--app-hover-overlay);
border: 1px solid
color-mix(in srgb, var(--mat-sys-outline-variant) 82%, transparent);
color-mix(in srgb, var(--app-separator) 82%, transparent);
font-size: 0.75rem;
font-weight: 600;
line-height: 1;
@@ -79,19 +75,19 @@
}
.search-chip--scope {
color: var(--mat-sys-on-surface);
color: var(--app-heading-color);
}
.search-chip--status {
color: var(--app-selection-color, var(--mat-sys-primary));
color: var(--app-selection-color);
background: color-mix(
in srgb,
var(--app-selection-surface, var(--mat-sys-primary-container)) 70%,
var(--app-selection-surface) 70%,
transparent
);
border-color: color-mix(
in srgb,
var(--app-selection-border, var(--mat-sys-primary)) 52%,
var(--app-selection-border) 52%,
transparent
);
}
@@ -101,7 +97,7 @@
border: none;
outline: none;
background: transparent;
color: var(--mat-sys-on-surface);
color: var(--app-heading-color);
font: inherit;
font-size: 0.875rem;
@@ -111,7 +107,7 @@
}
&::placeholder {
color: var(--mat-sys-on-surface-variant);
color: var(--app-muted-color);
opacity: 0.7;
}
}
@@ -124,14 +120,10 @@
min-width: 36px;
padding: 0 6px;
margin-right: -2px;
color: var(--mat-sys-on-surface-variant);
background: color-mix(
in srgb,
var(--mat-sys-surface-container-high) 76%,
transparent
);
color: var(--app-muted-color);
background: var(--app-hover-overlay);
border: 1px solid
color-mix(in srgb, var(--mat-sys-outline-variant) 70%, transparent);
color-mix(in srgb, var(--app-separator) 70%, transparent);
border-radius: 5px;
font-family: inherit;
font-size: 0.7rem;
@@ -146,17 +138,17 @@
border-color 0.15s ease;
&:hover {
background: var(--mat-sys-surface-container-highest);
color: var(--mat-sys-on-surface);
background: var(--app-card-hover-bg);
color: var(--app-heading-color);
border-color: color-mix(
in srgb,
var(--mat-sys-outline-variant) 100%,
var(--app-separator) 100%,
transparent
);
}
&:focus-visible {
outline: 2px solid var(--mat-sys-primary);
outline: 2px solid var(--app-selection-color);
outline-offset: 1px;
}
}
@@ -170,15 +162,26 @@
button,
a {
color: var(--mat-sys-on-surface-variant);
// Body-tier (not muted) so these read as active controls, not
// disabled. Hover brightens to --app-heading-color below.
color: var(--app-body-color);
border-radius: 8px;
transition:
color 0.15s ease,
background 0.15s ease;
// Suppress Material's circular state-layer overlay so only the
// rounded-rectangle hover/active states below render. Keeps the
// header consistent with the rail, nav items, and the .is-active
// state (all rounded rectangles). The click ripple is left intact —
// it is clipped to the button's 8px radius.
::ng-deep .mat-mdc-button-persistent-ripple {
display: none;
}
&:hover {
color: var(--mat-sys-on-surface);
background: var(--mat-sys-surface-container);
color: var(--app-heading-color);
background: var(--app-hover-overlay);
}
&.is-active {
@@ -202,7 +205,6 @@
.header-actions--trailing {
margin-left: auto;
padding-left: 8px;
border-left: 1px solid var(--mat-sys-outline-variant);
app-region: no-drag;
}
@@ -45,7 +45,7 @@
font-size: 1.55rem;
font-weight: 650;
letter-spacing: -0.03em;
color: var(--app-heading-color, var(--mat-sys-on-surface));
color: var(--app-heading-color);
}
}
@@ -85,13 +85,13 @@
.workspace-loading-overlay__phase {
font-size: 1rem;
font-weight: 560;
color: var(--app-body-color, var(--mat-sys-on-surface));
color: var(--app-body-color);
}
.workspace-loading-overlay__detail {
max-width: 32ch;
justify-self: center;
color: var(--app-muted-color, var(--mat-sys-on-surface-variant));
color: var(--app-muted-color);
line-height: 1.45;
}
@@ -100,7 +100,7 @@
justify-self: center;
line-height: 1.45;
font-variant-numeric: tabular-nums;
color: var(--app-muted-color, var(--mat-sys-on-surface-variant));
color: var(--app-muted-color);
text-wrap: balance;
}
@@ -83,17 +83,3 @@
color: var(--rail-link-active-icon, var(--app-selection-color));
}
}
.portal-rail-link.is-active::after,
.portal-rail-link.active::after {
content: '';
position: absolute;
right: -4px;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 20px;
border-radius: 999px;
background: var(--rail-link-active-icon, var(--app-selection-color));
box-shadow: 0 0 10px var(--rail-link-active-glow, var(--app-selection-glow));
}
@@ -13,8 +13,8 @@
box-sizing: border-box;
padding: 10px 8px;
border-right: 1px solid
var(--app-rail-border, var(--mat-sys-outline-variant));
background: var(--app-rail-bg, var(--mat-sys-surface-container-low));
var(--app-rail-border);
background: var(--app-rail-bg);
app-region: drag;
overflow-y: auto;
overflow-x: hidden;
@@ -83,19 +83,6 @@
mat-icon {
color: var(--rail-link-active-icon);
}
&::after {
content: '';
position: absolute;
right: -4px;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 18px;
border-radius: 999px;
background: var(--rail-link-active-icon);
box-shadow: 0 0 10px var(--rail-link-active-glow);
}
}
.brand {
@@ -208,19 +195,6 @@
background: var(--app-hover-overlay);
color: var(--app-on-surface);
}
&.is-active::after {
content: '';
position: absolute;
right: -4px;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 20px;
border-radius: 999px;
background: var(--app-selection-color);
box-shadow: 0 0 10px var(--app-selection-glow);
}
}
}
@@ -13,7 +13,7 @@
height: 100%;
min-height: 0;
overflow: hidden;
background: var(--app-shell-bg, var(--mat-sys-surface));
background: var(--app-shell-bg);
color: var(--mat-sys-on-surface);
}
@@ -37,7 +37,7 @@ app-workspace-shell-header {
min-height: 0;
min-width: 0;
overflow: hidden;
background: var(--app-rail-bg, var(--mat-sys-surface-container-low));
background: var(--app-rail-bg);
&.single-pane {
grid-template-columns: 1fr;
@@ -53,14 +53,14 @@ app-workspace-shell-context-sidebar {
grid-column: 2;
grid-row: 3;
padding: 0;
background: var(--app-content-bg, var(--mat-sys-surface));
background: var(--app-content-bg);
}
.workspace-content {
min-width: 0;
min-height: 0;
overflow: auto;
background: var(--app-content-bg, var(--mat-sys-surface));
background: var(--app-content-bg);
border-radius: 16px 0 0 0;
box-shadow: -4px 8px 20px var(--app-content-shadow, rgba(0, 0, 0, 0.18));
}
@@ -32,20 +32,20 @@
font-size: 0.94rem;
font-weight: 600;
letter-spacing: -0.01em;
color: var(--mat-sys-on-surface);
color: var(--app-heading-color);
}
}
.context-header p {
margin: 4px 0 0;
font-size: 0.78rem;
color: var(--mat-sys-on-surface-variant);
color: var(--app-muted-color);
}
// ─── Divider ──────────────────────────────────────────────────────────────────
.context-divider {
height: 1px;
background: var(--mat-sys-outline-variant);
background: var(--app-separator);
margin: 0 0 6px;
opacity: 0.6;
flex-shrink: 0;
@@ -97,6 +97,6 @@
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--mat-sys-on-surface-variant);
color: var(--app-muted-color);
opacity: 0.85;
}
+1
View File
@@ -61,6 +61,7 @@
"release:artwork:generate": "tsx tools/release/generate-marketing-artwork.ts --generate",
"release:artwork:validate": "tsx tools/release/generate-marketing-artwork.ts --validate",
"lint": "nx run-many --target=lint --all",
"lint:styles": "node tools/styles/check-mat-sys-usage.mjs",
"build": "nx build electron-backend"
},
"private": true,
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env node
/**
* Fails when new `var(--mat-sys-*)` references are added anywhere in
* apps/ or libs/. The current Material theme (m3-theme.scss, older M3 API)
* never emits `--mat-sys-*` system tokens, so such references are silent
* no-ops: the declaration is invalid at computed-value time and falls back
* to inherited/initial values. Use the `--app-*` design tokens from
* apps/web/src/m3-theme.scss instead.
*
* Pre-existing usages are grandfathered in tools/styles/mat-sys-baseline.mjs
* (regenerate with `node tools/styles/generate-mat-sys-baseline.mjs` after
* migrating a file — the baseline should only shrink).
*
* Usage: node tools/styles/check-mat-sys-usage.mjs
*/
import process from 'node:process';
import { matSysBaseline } from './mat-sys-baseline.mjs';
import { scanMatSysUsages } from './mat-sys-usage.shared.mjs';
const usages = scanMatSysUsages(process.cwd());
const violations = usages.filter(
({ file, count }) => count > (matSysBaseline.get(file) ?? 0)
);
if (violations.length > 0) {
console.error(
'New var(--mat-sys-*) references detected. These tokens are never\n' +
'emitted by the current Material theme setup and resolve to\n' +
'nothing — use the --app-* design tokens from\n' +
'apps/web/src/m3-theme.scss instead\n' +
'(see docs/architecture/theme-design-tokens.md).\n'
);
for (const { file, count } of violations) {
const baselined = matSysBaseline.get(file) ?? 0;
console.error(` ${file}: ${count} usages (baseline: ${baselined})`);
}
process.exit(1);
}
const stale = [...matSysBaseline.keys()].filter(
(file) => !usages.some((usage) => usage.file === file)
);
const shrunk = usages.filter(
({ file, count }) => count < (matSysBaseline.get(file) ?? 0)
);
if (stale.length > 0 || shrunk.length > 0) {
console.log(
'Baseline is stale (files were migrated). Consider running\n' +
'`node tools/styles/generate-mat-sys-baseline.mjs` to shrink it.'
);
}
console.log('No new var(--mat-sys-*) references. OK.');
@@ -0,0 +1,41 @@
#!/usr/bin/env node
/**
* Regenerates tools/styles/mat-sys-baseline.mjs: the per-file counts of
* legacy `var(--mat-sys-*)` references that predate the guard enforced by
* tools/styles/check-mat-sys-usage.mjs. These tokens are never emitted by
* the current Material theme setup, so every reference is a silent no-op.
* The list should only shrink: migrate a file to `--app-*` tokens, rerun
* this generator, and commit the result. Never add new entries.
*
* Usage: node tools/styles/generate-mat-sys-baseline.mjs
*/
import { writeFileSync } from 'node:fs';
import path from 'node:path';
import process from 'node:process';
import { scanMatSysUsages } from './mat-sys-usage.shared.mjs';
const workspaceRoot = process.cwd();
const usages = scanMatSysUsages(workspaceRoot);
const banner = `// Generated by tools/styles/generate-mat-sys-baseline.mjs — do not edit by hand.
// Files with legacy var(--mat-sys-*) references that predate the guard.
// These tokens are never emitted by the current theme (m3-theme.scss), so
// every reference is a silent no-op. Use the --app-* design tokens instead
// (see docs/architecture/theme-design-tokens.md). This list should only
// shrink: migrate a file, rerun the generator, and commit the result.
`;
const body = usages
.map(({ file, count }) => ` ['${file}', ${count}],`)
.join('\n');
writeFileSync(
path.join(workspaceRoot, 'tools/styles/mat-sys-baseline.mjs'),
`${banner}export const matSysBaseline = new Map([\n${body}\n]);\n`
);
const total = usages.reduce((sum, { count }) => sum + count, 0);
console.log(`Baselined ${total} usages across ${usages.length} files.`);
+158
View File
@@ -0,0 +1,158 @@
// Generated by tools/styles/generate-mat-sys-baseline.mjs — do not edit by hand.
// Files with legacy var(--mat-sys-*) references that predate the guard.
// These tokens are never emitted by the current theme (m3-theme.scss), so
// every reference is a silent no-op. Use the --app-* design tokens instead
// (see docs/architecture/theme-design-tokens.md). This list should only
// shrink: migrate a file, rerun the generator, and commit the result.
export const matSysBaseline = new Map([
['apps/web/src/styles.scss', 2],
[
'libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.scss',
6,
],
[
'libs/playlist/shared/ui/src/lib/recent-playlists/playlist-info/playlist-info.component.ts',
3,
],
[
'libs/playlist/shared/ui/src/lib/recent-playlists/playlist-item/playlist-item.component.scss',
4,
],
[
'libs/portal/catalog/feature/src/lib/category-content-view/category-content-view.component.scss',
5,
],
[
'libs/portal/shared/ui/src/lib/components/favorites-layout/favorites-layout.component.scss',
5,
],
[
'libs/portal/shared/ui/src/lib/components/global-favorites-list/global-favorites-list.component.scss',
1,
],
[
'libs/portal/shared/ui/src/lib/components/grid-list/grid-list.component.scss',
2,
],
[
'libs/portal/shared/ui/src/lib/components/portal-empty-state/portal-empty-state.component.scss',
1,
],
[
'libs/portal/shared/ui/src/lib/components/unified-collection/unified-collection-page.component.scss',
3,
],
[
'libs/portal/shared/ui/src/lib/components/unified-collection/unified-grid-tab.component.scss',
1,
],
[
'libs/portal/shared/ui/src/lib/components/unified-collection/unified-live-tab.component.scss',
6,
],
[
'libs/portal/shared/ui/src/lib/navigation/portal-rail-links.component.scss',
1,
],
['libs/portal/shared/ui/src/lib/styles/_skeleton.scss', 1],
[
'libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.scss',
3,
],
[
'libs/portal/xtream/feature/src/lib/portal-channels-list/portal-channels-list.component.scss',
1,
],
[
'libs/portal/xtream/feature/src/lib/search-results/search-results.component.scss',
5,
],
[
'libs/portal/xtream/feature/src/lib/xtream-cached-offline-notice.component.ts',
2,
],
[
'libs/ui/components/src/lib/channel-list-container/all-channels-view/all-channels-view.component.scss',
2,
],
[
'libs/ui/components/src/lib/channel-list-container/channel-list-container.component.scss',
2,
],
[
'libs/ui/components/src/lib/channel-list-container/groups-view/groups-view.component.scss',
6,
],
[
'libs/ui/components/src/lib/channel-list-loading-state/channel-list-loading-state.component.scss',
1,
],
[
'libs/ui/components/src/lib/external-playback-dock/external-playback-dock.component.scss',
14,
],
[
'libs/ui/components/src/lib/season-container/season-container.component.scss',
24,
],
[
'libs/ui/components/src/lib/window-controls/window-controls.component.scss',
1,
],
['libs/ui/playback/src/lib/audio-player/audio-player.component.scss', 14],
[
'libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-player.component.scss',
25,
],
[
'libs/ui/playback/src/lib/portal-inline-player/series-playback-navigation-controls.component.scss',
4,
],
['libs/ui/styles/_panel-header.scss', 1],
['libs/ui/styles/_portal-layout.scss', 1],
['libs/ui/styles/_portal-sidebar.scss', 4],
[
'libs/workspace/dashboard/feature/src/lib/rails/dashboard-rail.component.scss',
2,
],
[
'libs/workspace/shell/feature/src/lib/global-favorites/global-favorites-list.component.scss',
1,
],
[
'libs/workspace/shell/feature/src/lib/workspace-command-palette/workspace-command-palette.component.scss',
12,
],
[
'libs/workspace/shell/feature/src/lib/workspace-context-panel/components/workspace-context-category-view.component.scss',
1,
],
[
'libs/workspace/shell/feature/src/lib/workspace-context-panel/workspace-context-panel.component.scss',
1,
],
[
'libs/workspace/shell/feature/src/lib/workspace-context-panel/workspace-settings-context-panel.component.scss',
1,
],
[
'libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-context-sidebar/workspace-shell-context-sidebar.component.scss',
1,
],
[
'libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-import-overlay/workspace-shell-import-overlay.component.scss',
1,
],
[
'libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-rail-links/workspace-shell-rail-links.component.scss',
3,
],
[
'libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-rail/workspace-shell-rail.component.scss',
5,
],
[
'libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.scss',
1,
],
]);
+48
View File
@@ -0,0 +1,48 @@
// Shared scanner for the --mat-sys-* usage guard.
//
// The Angular Material theme 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 `--mat-sys-*` system tokens. Every
// `var(--mat-sys-*)` reference therefore fails at computed-value time and
// silently falls back to inherited/initial values. New styles must use the
// `--app-*` design tokens defined in m3-theme.scss instead.
//
// See docs/architecture/theme-design-tokens.md for the token mapping.
import { readdirSync, readFileSync } from 'node:fs';
import path from 'node:path';
export const SCAN_ROOTS = ['apps', 'libs'];
export const SCAN_EXTENSIONS = ['.scss', '.css', '.ts', '.html', '.js'];
const SKIP_DIRS = new Set(['node_modules', 'dist', '.nx', 'coverage']);
const USAGE_PATTERN = /var\(--mat-sys-/g;
function collectFiles(dir, results) {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (entry.isDirectory()) {
if (!SKIP_DIRS.has(entry.name)) {
collectFiles(path.join(dir, entry.name), results);
}
} else if (SCAN_EXTENSIONS.some((ext) => entry.name.endsWith(ext))) {
results.push(path.join(dir, entry.name));
}
}
return results;
}
/** Returns a sorted array of { file, count } for files with usages. */
export function scanMatSysUsages(workspaceRoot) {
return SCAN_ROOTS.flatMap((root) =>
collectFiles(path.join(workspaceRoot, root), [])
)
.map((filePath) => ({
file: path
.relative(workspaceRoot, filePath)
.split(path.sep)
.join('/'),
count: (readFileSync(filePath, 'utf8').match(USAGE_PATTERN) || [])
.length,
}))
.filter(({ count }) => count > 0)
.sort((a, b) => a.file.localeCompare(b.file));
}