From eac1dfa09e921ca43c12062bab785b4b6cda8c2e Mon Sep 17 00:00:00 2001 From: 4gray <4gray@users.noreply.github.com> Date: Sun, 12 Jul 2026 22:31:46 +0200 Subject: [PATCH] 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 --- .github/workflows/ci.yml | 3 + CLAUDE.md | 14 ++ apps/web/src/_settings-theme.scss | 48 ++---- ...delete-all-playlists-dialog.component.scss | 15 +- .../src/app/settings/settings.component.scss | 68 ++++---- apps/web/src/m3-theme.scss | 14 ++ apps/web/src/nav-list.scss | 6 +- apps/web/src/styles.scss | 2 +- apps/web/tailwind.config.js | 48 +++--- docs/architecture/theme-design-tokens.md | 87 ++++++++++ .../playlist-switcher.component.scss | 75 ++++----- .../playlist-info/playlist-info.component.ts | 6 +- .../category-content-view.component.scss | 9 +- .../feature/src/lib/downloads.component.scss | 81 ++++----- .../favorites-layout.component.scss | 2 +- .../grid-list/grid-list.component.scss | 4 +- .../playlist-error-view.component.scss | 4 +- .../portal-rail-links.component.scss | 14 -- .../shared/ui/src/lib/styles/_skeleton.scss | 28 ++-- .../account-info/account-info.component.scss | 72 ++++---- .../category-management-dialog.component.scss | 26 +-- .../live-stream-layout.component.scss | 5 +- .../portal-channels-list.component.scss | 2 +- .../channel-details-dialog.component.scss | 68 ++++---- .../group-management-dialog.component.scss | 28 ++-- .../groups-view/groups-view.component.scss | 12 +- .../channel-list-loading-state.component.scss | 16 +- .../audio-player/audio-player.component.scss | 14 +- libs/ui/styles/_panel-header.scss | 6 +- libs/ui/styles/_portal-sidebar.scss | 2 +- ...space-context-category-view.component.scss | 10 +- ...orkspace-context-error-view.component.scss | 4 +- .../workspace-context-panel.component.scss | 49 +++--- ...space-shell-context-sidebar.component.scss | 2 +- .../workspace-shell-header.component.scss | 72 ++++---- ...kspace-shell-import-overlay.component.scss | 8 +- .../workspace-shell-rail-links.component.scss | 14 -- .../workspace-shell-rail.component.scss | 30 +--- .../workspace-shell.component.scss | 8 +- ...space-sources-filters-panel.component.scss | 8 +- package.json | 1 + tools/styles/check-mat-sys-usage.mjs | 57 +++++++ tools/styles/generate-mat-sys-baseline.mjs | 41 +++++ tools/styles/mat-sys-baseline.mjs | 158 ++++++++++++++++++ tools/styles/mat-sys-usage.shared.mjs | 48 ++++++ 45 files changed, 801 insertions(+), 488 deletions(-) create mode 100644 docs/architecture/theme-design-tokens.md create mode 100644 tools/styles/check-mat-sys-usage.mjs create mode 100644 tools/styles/generate-mat-sys-baseline.mjs create mode 100644 tools/styles/mat-sys-baseline.mjs create mode 100644 tools/styles/mat-sys-usage.shared.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bf4da6b24..926eabb4c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md index a68c0c124..d6b1ee905 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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: diff --git a/apps/web/src/_settings-theme.scss b/apps/web/src/_settings-theme.scss index 8a843b60e..9cb7575fb 100644 --- a/apps/web/src/_settings-theme.scss +++ b/apps/web/src/_settings-theme.scss @@ -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, diff --git a/apps/web/src/app/settings/settings-delete-all-playlists-dialog.component.scss b/apps/web/src/app/settings/settings-delete-all-playlists-dialog.component.scss index 9178d1806..7df0926e3 100644 --- a/apps/web/src/app/settings/settings-delete-all-playlists-dialog.component.scss +++ b/apps/web/src/app/settings/settings-delete-all-playlists-dialog.component.scss @@ -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 { diff --git a/apps/web/src/app/settings/settings.component.scss b/apps/web/src/app/settings/settings.component.scss index f3b7d22df..7acf90cc9 100644 --- a/apps/web/src/app/settings/settings.component.scss +++ b/apps/web/src/app/settings/settings.component.scss @@ -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; } } diff --git a/apps/web/src/m3-theme.scss b/apps/web/src/m3-theme.scss index 4f8ac3325..80a274177 100644 --- a/apps/web/src/m3-theme.scss +++ b/apps/web/src/m3-theme.scss @@ -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); diff --git a/apps/web/src/nav-list.scss b/apps/web/src/nav-list.scss index dfb6df175..4aabc2b1a 100644 --- a/apps/web/src/nav-list.scss +++ b/apps/web/src/nav-list.scss @@ -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, diff --git a/apps/web/src/styles.scss b/apps/web/src/styles.scss index 60e038639..4440fc60e 100644 --- a/apps/web/src/styles.scss +++ b/apps/web/src/styles.scss @@ -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 { diff --git a/apps/web/tailwind.config.js b/apps/web/tailwind.config.js index 371bbc8c5..4fbb8049d 100644 --- a/apps/web/tailwind.config.js +++ b/apps/web/tailwind.config.js @@ -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', diff --git a/docs/architecture/theme-design-tokens.md b/docs/architecture/theme-design-tokens.md new file mode 100644 index 000000000..fb18e245b --- /dev/null +++ b/docs/architecture/theme-design-tokens.md @@ -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. diff --git a/libs/playlist/shared/ui/src/lib/playlist-switcher/playlist-switcher.component.scss b/libs/playlist/shared/ui/src/lib/playlist-switcher/playlist-switcher.component.scss index fdd6f4df9..c6d783b60 100644 --- a/libs/playlist/shared/ui/src/lib/playlist-switcher/playlist-switcher.component.scss +++ b/libs/playlist/shared/ui/src/lib/playlist-switcher/playlist-switcher.component.scss @@ -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 { diff --git a/libs/playlist/shared/ui/src/lib/recent-playlists/playlist-info/playlist-info.component.ts b/libs/playlist/shared/ui/src/lib/recent-playlists/playlist-info/playlist-info.component.ts index 3d0882346..79b5793f8 100644 --- a/libs/playlist/shared/ui/src/lib/recent-playlists/playlist-info/playlist-info.component.ts +++ b/libs/playlist/shared/ui/src/lib/recent-playlists/playlist-info/playlist-info.component.ts @@ -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); } diff --git a/libs/portal/catalog/feature/src/lib/category-content-view/category-content-view.component.scss b/libs/portal/catalog/feature/src/lib/category-content-view/category-content-view.component.scss index 36340c715..87adc4916 100644 --- a/libs/portal/catalog/feature/src/lib/category-content-view/category-content-view.component.scss +++ b/libs/portal/catalog/feature/src/lib/category-content-view/category-content-view.component.scss @@ -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); } diff --git a/libs/portal/downloads/feature/src/lib/downloads.component.scss b/libs/portal/downloads/feature/src/lib/downloads.component.scss index 5ffd989e3..90e67764e 100644 --- a/libs/portal/downloads/feature/src/lib/downloads.component.scss +++ b/libs/portal/downloads/feature/src/lib/downloads.component.scss @@ -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; diff --git a/libs/portal/shared/ui/src/lib/components/favorites-layout/favorites-layout.component.scss b/libs/portal/shared/ui/src/lib/components/favorites-layout/favorites-layout.component.scss index 232c9e901..c308f4f7e 100644 --- a/libs/portal/shared/ui/src/lib/components/favorites-layout/favorites-layout.component.scss +++ b/libs/portal/shared/ui/src/lib/components/favorites-layout/favorites-layout.component.scss @@ -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; diff --git a/libs/portal/shared/ui/src/lib/components/grid-list/grid-list.component.scss b/libs/portal/shared/ui/src/lib/components/grid-list/grid-list.component.scss index 4c5c04304..02d7885e7 100644 --- a/libs/portal/shared/ui/src/lib/components/grid-list/grid-list.component.scss +++ b/libs/portal/shared/ui/src/lib/components/grid-list/grid-list.component.scss @@ -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); } } } diff --git a/libs/portal/shared/ui/src/lib/components/playlist-error-view/playlist-error-view.component.scss b/libs/portal/shared/ui/src/lib/components/playlist-error-view/playlist-error-view.component.scss index f10e66a92..fac11e54c 100644 --- a/libs/portal/shared/ui/src/lib/components/playlist-error-view/playlist-error-view.component.scss +++ b/libs/portal/shared/ui/src/lib/components/playlist-error-view/playlist-error-view.component.scss @@ -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; } diff --git a/libs/portal/shared/ui/src/lib/navigation/portal-rail-links.component.scss b/libs/portal/shared/ui/src/lib/navigation/portal-rail-links.component.scss index dda2ea519..38163e5fd 100644 --- a/libs/portal/shared/ui/src/lib/navigation/portal-rail-links.component.scss +++ b/libs/portal/shared/ui/src/lib/navigation/portal-rail-links.component.scss @@ -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); -} diff --git a/libs/portal/shared/ui/src/lib/styles/_skeleton.scss b/libs/portal/shared/ui/src/lib/styles/_skeleton.scss index 2ac9351d5..741b57d2d 100644 --- a/libs/portal/shared/ui/src/lib/styles/_skeleton.scss +++ b/libs/portal/shared/ui/src/lib/styles/_skeleton.scss @@ -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%, diff --git a/libs/portal/xtream/feature/src/lib/account-info/account-info.component.scss b/libs/portal/xtream/feature/src/lib/account-info/account-info.component.scss index f553b6fb3..a0bbec161 100644 --- a/libs/portal/xtream/feature/src/lib/account-info/account-info.component.scss +++ b/libs/portal/xtream/feature/src/lib/account-info/account-info.component.scss @@ -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 { diff --git a/libs/portal/xtream/feature/src/lib/category-management-dialog/category-management-dialog.component.scss b/libs/portal/xtream/feature/src/lib/category-management-dialog/category-management-dialog.component.scss index 2f1944edf..2a1f93d73 100644 --- a/libs/portal/xtream/feature/src/lib/category-management-dialog/category-management-dialog.component.scss +++ b/libs/portal/xtream/feature/src/lib/category-management-dialog/category-management-dialog.component.scss @@ -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; } diff --git a/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.component.scss b/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.component.scss index ee772f77f..36d59dd1c 100644 --- a/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.component.scss +++ b/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.component.scss @@ -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); } diff --git a/libs/portal/xtream/feature/src/lib/portal-channels-list/portal-channels-list.component.scss b/libs/portal/xtream/feature/src/lib/portal-channels-list/portal-channels-list.component.scss index 0df50214b..9021157af 100644 --- a/libs/portal/xtream/feature/src/lib/portal-channels-list/portal-channels-list.component.scss +++ b/libs/portal/xtream/feature/src/lib/portal-channels-list/portal-channels-list.component.scss @@ -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; } diff --git a/libs/ui/components/src/lib/channel-list-container/channel-details-dialog/channel-details-dialog.component.scss b/libs/ui/components/src/lib/channel-list-container/channel-details-dialog/channel-details-dialog.component.scss index 7b53595d4..316dd6f92 100644 --- a/libs/ui/components/src/lib/channel-list-container/channel-details-dialog/channel-details-dialog.component.scss +++ b/libs/ui/components/src/lib/channel-list-container/channel-details-dialog/channel-details-dialog.component.scss @@ -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 { diff --git a/libs/ui/components/src/lib/channel-list-container/groups-view/group-management-dialog/group-management-dialog.component.scss b/libs/ui/components/src/lib/channel-list-container/groups-view/group-management-dialog/group-management-dialog.component.scss index e4bfd5b2f..e4c746559 100644 --- a/libs/ui/components/src/lib/channel-list-container/groups-view/group-management-dialog/group-management-dialog.component.scss +++ b/libs/ui/components/src/lib/channel-list-container/groups-view/group-management-dialog/group-management-dialog.component.scss @@ -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; } diff --git a/libs/ui/components/src/lib/channel-list-container/groups-view/groups-view.component.scss b/libs/ui/components/src/lib/channel-list-container/groups-view/groups-view.component.scss index 61c8f61b4..6414be623 100644 --- a/libs/ui/components/src/lib/channel-list-container/groups-view/groups-view.component.scss +++ b/libs/ui/components/src/lib/channel-list-container/groups-view/groups-view.component.scss @@ -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; } diff --git a/libs/ui/components/src/lib/channel-list-loading-state/channel-list-loading-state.component.scss b/libs/ui/components/src/lib/channel-list-loading-state/channel-list-loading-state.component.scss index 3292c762c..75e2a494b 100644 --- a/libs/ui/components/src/lib/channel-list-loading-state/channel-list-loading-state.component.scss +++ b/libs/ui/components/src/lib/channel-list-loading-state/channel-list-loading-state.component.scss @@ -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; } diff --git a/libs/ui/playback/src/lib/audio-player/audio-player.component.scss b/libs/ui/playback/src/lib/audio-player/audio-player.component.scss index 148235b42..b0991bf56 100644 --- a/libs/ui/playback/src/lib/audio-player/audio-player.component.scss +++ b/libs/ui/playback/src/lib/audio-player/audio-player.component.scss @@ -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; diff --git a/libs/ui/styles/_panel-header.scss b/libs/ui/styles/_panel-header.scss index 0738bf244..2305f4578 100644 --- a/libs/ui/styles/_panel-header.scss +++ b/libs/ui/styles/_panel-header.scss @@ -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; diff --git a/libs/ui/styles/_portal-sidebar.scss b/libs/ui/styles/_portal-sidebar.scss index 44813b427..1c52580f3 100644 --- a/libs/ui/styles/_portal-sidebar.scss +++ b/libs/ui/styles/_portal-sidebar.scss @@ -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; diff --git a/libs/workspace/shell/feature/src/lib/workspace-context-panel/components/workspace-context-category-view.component.scss b/libs/workspace/shell/feature/src/lib/workspace-context-panel/components/workspace-context-category-view.component.scss index 01c956545..26b3faf97 100644 --- a/libs/workspace/shell/feature/src/lib/workspace-context-panel/components/workspace-context-category-view.component.scss +++ b/libs/workspace/shell/feature/src/lib/workspace-context-panel/components/workspace-context-category-view.component.scss @@ -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%, diff --git a/libs/workspace/shell/feature/src/lib/workspace-context-panel/components/workspace-context-error-view.component.scss b/libs/workspace/shell/feature/src/lib/workspace-context-panel/components/workspace-context-error-view.component.scss index 50f5f9caf..13351a5e2 100644 --- a/libs/workspace/shell/feature/src/lib/workspace-context-panel/components/workspace-context-error-view.component.scss +++ b/libs/workspace/shell/feature/src/lib/workspace-context-panel/components/workspace-context-error-view.component.scss @@ -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; } diff --git a/libs/workspace/shell/feature/src/lib/workspace-context-panel/workspace-context-panel.component.scss b/libs/workspace/shell/feature/src/lib/workspace-context-panel/workspace-context-panel.component.scss index e2e03a3f5..286136c65 100644 --- a/libs/workspace/shell/feature/src/lib/workspace-context-panel/workspace-context-panel.component.scss +++ b/libs/workspace/shell/feature/src/lib/workspace-context-panel/workspace-context-panel.component.scss @@ -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%); diff --git a/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-context-sidebar/workspace-shell-context-sidebar.component.scss b/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-context-sidebar/workspace-shell-context-sidebar.component.scss index d71915cd3..a5f15b16b 100644 --- a/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-context-sidebar/workspace-shell-context-sidebar.component.scss +++ b/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-context-sidebar/workspace-shell-context-sidebar.component.scss @@ -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; diff --git a/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-header/workspace-shell-header.component.scss b/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-header/workspace-shell-header.component.scss index 83c610edc..6c7894f79 100644 --- a/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-header/workspace-shell-header.component.scss +++ b/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-header/workspace-shell-header.component.scss @@ -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; } diff --git a/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-import-overlay/workspace-shell-import-overlay.component.scss b/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-import-overlay/workspace-shell-import-overlay.component.scss index 2976f683a..abf83dc4f 100644 --- a/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-import-overlay/workspace-shell-import-overlay.component.scss +++ b/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-import-overlay/workspace-shell-import-overlay.component.scss @@ -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; } diff --git a/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-rail-links/workspace-shell-rail-links.component.scss b/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-rail-links/workspace-shell-rail-links.component.scss index 802f2e668..b75f69506 100644 --- a/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-rail-links/workspace-shell-rail-links.component.scss +++ b/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-rail-links/workspace-shell-rail-links.component.scss @@ -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)); -} diff --git a/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-rail/workspace-shell-rail.component.scss b/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-rail/workspace-shell-rail.component.scss index eb6610c47..d59ce9897 100644 --- a/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-rail/workspace-shell-rail.component.scss +++ b/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-rail/workspace-shell-rail.component.scss @@ -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); - } } } diff --git a/libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.scss b/libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.scss index 39156e85d..32e05174b 100644 --- a/libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.scss +++ b/libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.scss @@ -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)); } diff --git a/libs/workspace/shell/feature/src/lib/workspace-sources-filters-panel/workspace-sources-filters-panel.component.scss b/libs/workspace/shell/feature/src/lib/workspace-sources-filters-panel/workspace-sources-filters-panel.component.scss index 8c0df9dec..3118e7d4f 100644 --- a/libs/workspace/shell/feature/src/lib/workspace-sources-filters-panel/workspace-sources-filters-panel.component.scss +++ b/libs/workspace/shell/feature/src/lib/workspace-sources-filters-panel/workspace-sources-filters-panel.component.scss @@ -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; } diff --git a/package.json b/package.json index f5286fbe4..796af9732 100644 --- a/package.json +++ b/package.json @@ -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, diff --git a/tools/styles/check-mat-sys-usage.mjs b/tools/styles/check-mat-sys-usage.mjs new file mode 100644 index 000000000..302e48184 --- /dev/null +++ b/tools/styles/check-mat-sys-usage.mjs @@ -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.'); diff --git a/tools/styles/generate-mat-sys-baseline.mjs b/tools/styles/generate-mat-sys-baseline.mjs new file mode 100644 index 000000000..2446beb26 --- /dev/null +++ b/tools/styles/generate-mat-sys-baseline.mjs @@ -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.`); diff --git a/tools/styles/mat-sys-baseline.mjs b/tools/styles/mat-sys-baseline.mjs new file mode 100644 index 000000000..d6ad325b8 --- /dev/null +++ b/tools/styles/mat-sys-baseline.mjs @@ -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, + ], +]); diff --git a/tools/styles/mat-sys-usage.shared.mjs b/tools/styles/mat-sys-usage.shared.mjs new file mode 100644 index 000000000..1c7caa43a --- /dev/null +++ b/tools/styles/mat-sys-usage.shared.mjs @@ -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)); +}