refactor(ui): unify primary color to blue across all surfaces

Swap M3 primary palette from violet to azure so checkboxes, radio
buttons, raised CTAs, and active states read as the same blue used by
the rail selection token. Cascading template + SCSS updates align the
remaining hand-rolled surfaces (Add Playlist dialog, VOD play button,
radio player, multi-EPG, empty-state CTAs) with the unified system.

LIVE stays red (broadcast role), cyan stays on EPG "now" indicator,
green stays on completed-download — semantic colors keep their meaning;
only the indiscriminate accent uses get folded into the blue primary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 4.7 committed 2026-05-19 00:12:34 +02:00
1 parent 20e7322353
commit a27a29fcf2
11 files changed
+97 -75

No files matched your search

+15 -9
View File
@@ -15,11 +15,17 @@ $my-typography: mat.m2-define-typography-config(
@include mat.typography-hierarchy($my-typography);
// ─── M3 Themes ───────────────────────────────────────────────────────────────
// One primary color across the app. Azure is the closest M3 prebuilt to the
// product's "active/selection" blue (#2f7bff / #78adff), so checkboxes, radio
// buttons, raised CTAs, etc. now read as the same blue as rail selection.
//
// Cyan stays on tertiary so the EPG "now" indicator (which uses Material
// tertiary surfaces) keeps its existing visual role.
$light-theme: mat.define-theme(
(
color: (
theme-type: light,
primary: mat.$violet-palette,
primary: mat.$azure-palette,
tertiary: mat.$cyan-palette,
),
typography: (
@@ -33,7 +39,7 @@ $dark-theme: mat.define-theme(
(
color: (
theme-type: dark,
primary: mat.$violet-palette,
primary: mat.$azure-palette,
tertiary: mat.$cyan-palette,
),
typography: (
@@ -102,17 +108,17 @@ html {
--app-eyebrow-color: #7c8495;
// Provider accent colors
--app-provider-xtream: #6366f1;
--app-provider-xtream: #2f7bff;
--app-provider-stalker: #10b981;
--app-provider-m3u: #d97706;
// Hero
--app-accent-gradient: linear-gradient(
135deg,
rgba(99, 102, 241, 0.08) 0%,
rgba(168, 85, 247, 0.05) 100%
rgba(47, 123, 255, 0.08) 0%,
rgba(92, 214, 255, 0.05) 100%
);
--app-hero-surface: rgba(99, 102, 241, 0.04);
--app-hero-surface: rgba(47, 123, 255, 0.04);
.dark-theme {
@include mat.all-component-colors($dark-theme);
@@ -178,15 +184,15 @@ html {
--app-eyebrow-color: #6b7494; // eyebrow labels
// Provider accents (slightly brighter for dark bg)
--app-provider-xtream: #818cf8;
--app-provider-xtream: #78adff;
--app-provider-stalker: #34d399;
--app-provider-m3u: #fbbf24;
// Hero section
--app-accent-gradient: linear-gradient(
135deg,
rgba(99, 102, 241, 0.12) 0%,
rgba(168, 85, 247, 0.07) 100%
rgba(120, 173, 255, 0.12) 0%,
rgba(92, 214, 255, 0.07) 100%
);
--app-hero-surface: rgba(30, 35, 48, 0.9);
}
@@ -90,7 +90,7 @@
@case ('url') {
<button
mat-flat-button
color="accent"
color="primary"
type="button"
[disabled]="!urlUpload()?.form?.valid"
(click)="submitUrlPlaylist()"
@@ -101,7 +101,7 @@
@case ('file') {
<button
mat-flat-button
color="accent"
color="primary"
type="button"
[disabled]="!fileUpload()?.selectedFile()"
(click)="fileUpload()?.confirm()"
@@ -112,7 +112,7 @@
@case ('text') {
<button
mat-flat-button
color="accent"
color="primary"
type="button"
[disabled]="!textImport()?.textForm?.valid"
(click)="
@@ -142,7 +142,7 @@
</button>
<button
mat-flat-button
color="accent"
color="primary"
type="button"
[disabled]="!xtreamImport()?.form?.valid"
(click)="xtreamImport()?.addPlaylist()"
@@ -154,7 +154,7 @@
@if (stalkerImport()?.isLoading()) {
<button
mat-flat-button
color="accent"
color="primary"
type="button"
disabled
>
@@ -165,7 +165,7 @@
} @else {
<button
mat-flat-button
color="accent"
color="primary"
type="button"
[disabled]="!stalkerImport()?.form?.valid"
(click)="stalkerImport()?.addPlaylist()"
@@ -110,14 +110,14 @@
flex-wrap: wrap;
.primary-button {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
color: white;
background: var(--app-selection-color);
color: var(--app-selection-on-color, white);
font-weight: 500;
padding: 10px 24px;
border-radius: 10px;
text-transform: none;
letter-spacing: 0.01em;
box-shadow: 0 4px 14px rgba(59, 130, 246, 0.25);
box-shadow: 0 4px 14px var(--app-selection-glow);
transition: all 0.25s ease;
mat-icon {
@@ -126,12 +126,12 @@
&:hover {
transform: translateY(-1px);
box-shadow: 0 6px 18px rgba(59, 130, 246, 0.35);
box-shadow: 0 6px 18px var(--app-selection-border);
}
}
.secondary-button {
color: #60a5fa;
color: var(--app-selection-color);
font-weight: 500;
padding: 10px 18px;
text-transform: none;
@@ -168,7 +168,7 @@
@if (!playlist.serverUrl && !playlist.portalUrl) {
<button
mat-flat-button
color="accent"
color="primary"
type="button"
(click)="exportPlaylist()"
>
@@ -183,7 +183,7 @@
<button
mat-flat-button
mat-dialog-close
color="accent"
color="primary"
type="submit"
[disabled]="!playlistDetails.valid || playlistDetails.pristine"
>
@@ -344,7 +344,7 @@
@case ('failed') {
<button
mat-icon-button
color="accent"
color="primary"
(click)="
$event.stopPropagation();
retry(item)
@@ -372,7 +372,7 @@
@case ('canceled') {
<button
mat-icon-button
color="accent"
color="primary"
(click)="
$event.stopPropagation();
retry(item)
@@ -22,10 +22,10 @@ export interface ConfirmDialogData {
{{ dialogData.message }}
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button mat-dialog-close cdkFocusInitial color="accent">
<button mat-button mat-dialog-close cdkFocusInitial>
{{ dialogData.cancelLabel || 'NO' | translate }}
</button>
<button mat-flat-button [mat-dialog-close]="true" color="accent">
<button mat-flat-button [mat-dialog-close]="true" color="primary">
{{ dialogData.confirmLabel || 'YES' | translate }}
</button>
</mat-dialog-actions>
@@ -6,10 +6,12 @@ $bg-hover: #24243a;
$border-subtle: rgba(255, 255, 255, 0.06);
$border-medium: rgba(255, 255, 255, 0.12);
// Accent colors - TV broadcast vibes
$accent-primary: #00d4aa; // Cyan/teal for time
$accent-secondary: #8b5cf6; // Purple for programs
$accent-tertiary: #f472b6; // Pink for highlights
// Accent colors — unified with the rest of the app:
// cyan is reserved for the EPG "now" indicator, blue is the app primary,
// purple/pink had no semantic role and are dropped per the UX audit.
$accent-primary: #5cd6ff; // Cyan — reserved for "now" indicator
$accent-secondary: #4f8eff; // Blue — programs (matches --app-selection-color)
$accent-tertiary: #4f8eff; // Blue — highlights
$accent-warning: #fbbf24;
// Text colors
@@ -20,17 +22,17 @@ $text-muted: rgba(248, 250, 252, 0.4);
// Gradients
$gradient-program: linear-gradient(
135deg,
rgba(139, 92, 246, 0.15) 0%,
rgba(139, 92, 246, 0.05) 100%
rgba(79, 142, 255, 0.15) 0%,
rgba(79, 142, 255, 0.05) 100%
);
$gradient-program-hover: linear-gradient(
135deg,
rgba(139, 92, 246, 0.3) 0%,
rgba(139, 92, 246, 0.15) 100%
rgba(79, 142, 255, 0.3) 0%,
rgba(79, 142, 255, 0.15) 100%
);
$gradient-header: linear-gradient(
180deg,
rgba(0, 212, 170, 0.08) 0%,
rgba(92, 214, 255, 0.08) 0%,
transparent 100%
);
@@ -155,10 +155,13 @@
background: color-mix(in srgb, var(--mat-sys-surface-container) 60%, transparent);
backdrop-filter: blur(6px);
// LIVE stays in the app's red "broadcasting now" role across every
// surface (channel rows, EPG now-bar, badges). The station's extracted
// dominant color is only used as a soft aura on the artwork.
&.live {
color: var(--radio-accent, var(--app-selection-color, var(--mat-sys-primary)));
border-color: color-mix(in srgb, var(--radio-accent, var(--app-selection-color)) 52%, transparent);
background: color-mix(in srgb, var(--radio-accent, var(--app-selection-color)) 14%, transparent);
color: var(--app-live-color, #ff8f8f);
border-color: color-mix(in srgb, var(--app-live-color, #ff8f8f) 52%, transparent);
background: color-mix(in srgb, var(--app-live-color, #ff8f8f) 14%, transparent);
}
}
@@ -199,9 +202,11 @@
}
}
// 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(--radio-accent, var(--app-selection-color, var(--mat-sys-primary)));
--mat-fab-container-color: var(--radio-accent, var(--app-selection-color, var(--mat-sys-primary)));
--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-icon-color: #fff;
--mat-fab-foreground-color: #fff;
--mat-fab-icon-color: #fff;
@@ -238,10 +243,10 @@
.vol-slider {
flex: 1;
--mdc-slider-active-track-color: var(--radio-accent, var(--app-selection-color, var(--mat-sys-primary)));
--mdc-slider-handle-color: var(--radio-accent, var(--app-selection-color, var(--mat-sys-primary)));
--mat-slider-active-track-color: var(--radio-accent, var(--app-selection-color, var(--mat-sys-primary)));
--mat-slider-handle-color: var(--radio-accent, var(--app-selection-color, var(--mat-sys-primary)));
--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-inactive-track-color: var(--mat-sys-outline-variant);
--mdc-slider-handle-width: 12px;
--mdc-slider-handle-height: 12px;
@@ -37,7 +37,7 @@ import { TranslateModule } from '@ngx-translate/core';
>Don't show anymore</mat-checkbox
>
</div>
<button mat-button mat-dialog-close cdkFocusInitial color="accent">
<button mat-button mat-dialog-close cdkFocusInitial>
{{ 'CLOSE' | translate }}
</button>
</mat-dialog-actions>
+22 -16
View File
@@ -29,6 +29,10 @@
}
}
// Secondary detail-view actions (Favorite, Download). Per UX audit: only Play
// should read as the primary CTA, so these stay ghost/neutral. State accents
// (favorite=red destructive, download=green completed) keep their semantic
// meaning but ride on transparent fills so they never out-shout Play.
@mixin secondary-action-button {
display: inline-flex;
align-items: center;
@@ -37,15 +41,16 @@
padding: 0 24px;
font-size: 1rem;
font-weight: 600;
background: rgba(109, 109, 110, 0.7);
color: #fff;
border: none;
background: rgba(255, 255, 255, 0.08);
color: var(--text-primary, #fff);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 16px;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
background: rgba(109, 109, 110, 0.5);
background: rgba(255, 255, 255, 0.14);
border-color: rgba(255, 255, 255, 0.2);
}
mat-icon {
@@ -57,36 +62,37 @@
@mixin favorite-button-state {
&--active {
background: rgba(229, 9, 20, 0.8);
background: rgba(255, 91, 110, 0.16);
border-color: rgba(255, 91, 110, 0.4);
color: #ff8f8f;
&:hover {
background: rgba(229, 9, 20, 0.6);
background: rgba(255, 91, 110, 0.24);
}
}
}
@mixin download-button-state {
background: rgba(33, 150, 243, 0.8);
&:hover {
background: rgba(33, 150, 243, 0.6);
}
// Idle download stays neutral — extends secondary-action-button.
&--completed {
background: rgba(76, 175, 80, 0.8);
background: rgba(74, 222, 128, 0.16);
border-color: rgba(74, 222, 128, 0.4);
color: #4ade80;
&:hover {
background: rgba(76, 175, 80, 0.6);
background: rgba(74, 222, 128, 0.24);
}
}
&--downloading {
background: rgba(109, 109, 110, 0.5);
background: rgba(255, 255, 255, 0.04);
border-color: rgba(255, 255, 255, 0.08);
cursor: not-allowed;
opacity: 0.7;
opacity: 0.6;
&:hover {
background: rgba(109, 109, 110, 0.5);
background: rgba(255, 255, 255, 0.04);
}
}
}
+17 -14
View File
@@ -24,18 +24,21 @@
--text-primary: #1a1a1a;
--text-secondary: rgba(26, 26, 26, 0.75);
--text-muted: rgba(26, 26, 26, 0.5);
--accent-color: #b81d24;
// Single primary CTA color — matches --app-selection-color so the
// Play button reads as the same blue used everywhere else for
// "active / call to action".
--accent-color: #2f7bff;
--tag-bg: rgba(0, 0, 0, 0.06);
--tag-border: rgba(0, 0, 0, 0.12);
--play-idle-bg: linear-gradient(
135deg,
#fffaf2 0%,
#ffffff 52%,
#f7ecdf 100%
#2f7bff 0%,
#3b86ff 52%,
#4a90ff 100%
);
--play-idle-fg: #161310;
--play-idle-border: rgba(88, 48, 18, 0.12);
--play-idle-shadow: 0 18px 38px rgba(91, 56, 24, 0.14);
--play-idle-fg: #ffffff;
--play-idle-border: rgba(47, 123, 255, 0.4);
--play-idle-shadow: 0 18px 38px rgba(47, 123, 255, 0.32);
--play-launching-bg: linear-gradient(
135deg,
#ffd36f 0%,
@@ -74,19 +77,19 @@
--text-primary: #fff;
--text-secondary: rgba(255, 255, 255, 0.7);
--text-muted: rgba(255, 255, 255, 0.5);
--accent-color: #e50914;
--accent-color: #78adff;
--tag-bg: rgba(255, 255, 255, 0.1);
--tag-border: rgba(255, 255, 255, 0.2);
--poster-shadow: 0 8px 40px rgba(0, 0, 0, 0.5);
--play-idle-bg: linear-gradient(
135deg,
#fff5e6 0%,
#f6efe5 58%,
#e7dac9 100%
#2f7bff 0%,
#4f8eff 58%,
#78adff 100%
);
--play-idle-fg: #15110d;
--play-idle-border: rgba(255, 255, 255, 0.18);
--play-idle-shadow: 0 20px 42px rgba(0, 0, 0, 0.36);
--play-idle-fg: #ffffff;
--play-idle-border: rgba(120, 173, 255, 0.32);
--play-idle-shadow: 0 20px 42px rgba(47, 123, 255, 0.32);
--play-launching-bg: linear-gradient(
135deg,
#ffdd79 0%,