From f12dfabcceac418552118fd540b9c82ec1ec704c Mon Sep 17 00:00:00 2001 From: 4gray Date: Sat, 3 Oct 2026 10:09:56 +0200 Subject: [PATCH] fix(ui): one watch-progress colour in app chrome and in the player The same progressPercent was drawn red (#e50914) in the Up Next rail, the fullscreen episode panel and the catalog capsule, blue (#4f8eff) in the dock's timeline and Up next card, and the selection colour on the dashboard, so one episode changed colour inside a single player overlay. - App chrome reads a new --app-progress-color, declared per theme in m3-theme.scss as the theme's --app-selection-color: progress capsule, dashboard rail card and hero resume bar. - Over video, watch progress reads --pc-progress from a shared _player-palette.scss partial. The controls host, the Up Next rail and the fullscreen episode panel (both outside the host's --pc-* scope) include its progress-token mixin. - ArtPlayer's legacy skin gets PLAYER_PROGRESS_COLOR instead of #ff0000; a spec pins it to the Sass value. - The rule is written into the UI guidelines' Progress Bars section, with specs in ui-playback, components and workspace-dashboard-feature. Co-Authored-By: Claude Opus 5.5 --- .changes/ui-watch-progress-colour.md | 9 + apps/web/src/m3-theme.scss | 6 + docs/architecture/embedded-inline-playback.md | 3 +- docs/architecture/iptvnator-ui-guidelines.md | 29 ++++ docs/architecture/player-controls-contract.md | 10 +- .../progress-capsule.component.spec.ts | 47 ++++++ .../progress-capsule.component.ts | 6 +- .../art-player/art-player.component.spec.ts | 2 + .../lib/art-player/art-player.component.ts | 5 +- .../fullscreen-episode-panel.component.scss | 7 +- .../lib/player-controls/_player-palette.scss | 19 +++ .../player-controls.component.scss | 7 +- .../src/lib/player-controls/player-palette.ts | 7 + .../player-progress.palette.spec.ts | 156 ++++++++++++++++++ .../player-timeline.component.scss | 8 +- .../player-up-next-card.component.scss | 2 +- .../up-next-rail.component.scss | 8 +- .../lib/rails/dashboard-hero.component.scss | 2 +- .../rails/dashboard-progress-colour.spec.ts | 40 +++++ .../lib/rails/dashboard-rail.component.scss | 2 +- 20 files changed, 356 insertions(+), 19 deletions(-) create mode 100644 .changes/ui-watch-progress-colour.md create mode 100644 libs/ui/components/src/lib/progress-capsule/progress-capsule.component.spec.ts create mode 100644 libs/ui/playback/src/lib/player-controls/_player-palette.scss create mode 100644 libs/ui/playback/src/lib/player-controls/player-palette.ts create mode 100644 libs/ui/playback/src/lib/player-controls/player-progress.palette.spec.ts create mode 100644 libs/workspace/dashboard/feature/src/lib/rails/dashboard-progress-colour.spec.ts diff --git a/.changes/ui-watch-progress-colour.md b/.changes/ui-watch-progress-colour.md new file mode 100644 index 000000000..900803ea6 --- /dev/null +++ b/.changes/ui-watch-progress-colour.md @@ -0,0 +1,9 @@ +--- +type: fix +area: ui +--- + +Watch progress has one colour again: your theme's accent on dashboard cards +and catalog posters, and the player's blue everywhere inside the player, so +the next episode's bar no longer turns red in the Up Next rail and the +fullscreen episode list. diff --git a/apps/web/src/m3-theme.scss b/apps/web/src/m3-theme.scss index 8044b7b89..a3c36fd3a 100644 --- a/apps/web/src/m3-theme.scss +++ b/apps/web/src/m3-theme.scss @@ -104,6 +104,9 @@ html { var(--app-selection-color) 34%, transparent ); + // Watch progress in app chrome (rail cards, catalog capsules, the hero). + // Over video the player's fixed `--pc-progress` applies instead. + --app-progress-color: var(--app-selection-color); // Live / on-air accent (broadcast indicator) --app-live-color: #d12c2c; @@ -190,6 +193,9 @@ html { var(--app-selection-color) 44%, transparent ); + // Re-declared so it resolves against this context's selection colour + // rather than inheriting the light value computed on `html`. + --app-progress-color: var(--app-selection-color); // Live / on-air accent (broadcast indicator) --app-live-color: #ff8f8f; diff --git a/docs/architecture/embedded-inline-playback.md b/docs/architecture/embedded-inline-playback.md index 22753815b..f50bb7da4 100644 --- a/docs/architecture/embedded-inline-playback.md +++ b/docs/architecture/embedded-inline-playback.md @@ -210,7 +210,8 @@ becomes an "Up Next" episode rail (`app-up-next-rail`, `libs/ui/playback/src/lib/portal-inline-player/up-next-rail.component.ts`). The rail lists the currently playing episode (highlighted, click-inert) followed by the rest of its season and a spillover into the following -seasons, with per-episode watch-progress bars from playback positions. +seasons, with per-episode watch-progress bars from playback positions, +drawn in the player's `--pc-progress` like the dock's Up next card. - Data flow: the hosts (Xtream `SerialDetailsComponent`, Stalker `StalkerSeriesViewComponent`) build the entries with diff --git a/docs/architecture/iptvnator-ui-guidelines.md b/docs/architecture/iptvnator-ui-guidelines.md index 8492c7dde..488066df5 100644 --- a/docs/architecture/iptvnator-ui-guidelines.md +++ b/docs/architecture/iptvnator-ui-guidelines.md @@ -525,6 +525,35 @@ a cache read open and checks text contrast across live theme changes. Channel preview progress and EPG current-program progress should stay visually aligned. +### Watch progress colour + +A title's watch progress (its resume share, `progressPercent`) has exactly one +colour per context, and never a literal of its own: + +- **App chrome** — dashboard rail cards and the hero, catalog grids and season + episodes (`app-progress-capsule`): `--app-progress-color`, declared per theme + in `apps/web/src/m3-theme.scss` as that theme's `--app-selection-color`. The + capsule's green from 90 % marks a finished title; it is a status, not + progress. +- **Over video** — the dock timeline, the Up next card, the Up Next rail and + the fullscreen episode panel: the player's fixed `--pc-progress` (accent + blue `#4f8eff`), never an app token, because the player palette is + theme-independent (see Player And EPG Theme Boundaries). An episode + therefore reads the same in every player surface. The rail and the episode + panel render beside the controls host, outside its `--pc-*` scope, so they + declare the token on their own `:host` with the `progress-token` mixin of + `libs/ui/playback/src/lib/player-controls/_player-palette.scss`. +- ArtPlayer's legacy skin takes a colour string, not a custom property, so it + gets `PLAYER_PROGRESS_COLOR` (`player-palette.ts`), which a spec pins to the + Sass value. +- Live programme progress next to a LIVE marker (the dashboard's live rail and + live hero slides) keeps `--app-live-color`; EPG programme progress keeps the + fill described below. + +Specs hold the rule in each owning project: `player-progress.palette.spec.ts` +(ui-playback), `progress-capsule.component.spec.ts` (components) and +`dashboard-progress-colour.spec.ts` (workspace-dashboard-feature). + ### Track - Height: diff --git a/docs/architecture/player-controls-contract.md b/docs/architecture/player-controls-contract.md index 4bbdd20fd..6924e2712 100644 --- a/docs/architecture/player-controls-contract.md +++ b/docs/architecture/player-controls-contract.md @@ -317,6 +317,10 @@ record glyph and the recording status (6.2:1 on the glass over a black frame). They are literal on purpose: the overlay is theme-independent (see the UI guidelines' player theme boundary), and the app's `--app-selection-color` is a different blue that would fight the video. +Watch progress reads `--pc-progress`, the accent blue declared by the +`progress-token` mixin of `_player-palette.scss`; the Up Next rail and the +fullscreen episode panel include the same mixin, so one episode's bar has +one colour in every player surface (UI guidelines, "Watch progress colour"). - **Timeline row**: current time (`--pc-font-mono`, tabular) · drawn track (`.player-controls__timeline-track` with one segment and an accent fill, @@ -406,7 +410,7 @@ the native-view legacy dock keeps its plain slider. Near the end of a series episode the dock shows an **"Up next" card** (`app-player-up-next-card`, `data-test-id="player-controls-up-next"`) in the bottom-right corner above the controls: the next episode's still (or its -`S01E03` label as a tile), a 3px accent progress line when it was partly +`S01E03` label as a tile), a 3px `--pc-progress` line when it was partly watched, "Up next · in 7 min" and the title. The host supplies the item through the optional `upNext` input (`PlayerUpNextItem { label, title, thumbnailUrl, progressPercent }`); `ControlsUpNext` decides when it shows — @@ -879,7 +883,7 @@ be. rows: a 16:9 still or, without one, a large numeral tile so the no-TMDB case still looks designed; the `S01E03` label, runtime, watched check or "Now playing" marker; the title (label as fallback); a 3-line clamped - overview when there is one; a progress bar on the thumbnail. Rows are + overview when there is one; a `--pc-progress` bar on the thumbnail. Rows are buttons inside `
  • `s of a `
      `, so their button role survives for assistive technology. A loading season shows a spinner row, a loaded empty one the season-empty copy, and an unanswered one a "could not be loaded" @@ -1652,6 +1656,8 @@ libs/ui/playback/src/lib/player-controls/ ├── player-controls.component.ts ├── player-controls.component.html ├── player-controls.component.scss +├── _player-palette.scss +├── player-palette.ts ├── controls-feedback.ts ├── controls-format.utils.ts ├── controls-layout.ts diff --git a/libs/ui/components/src/lib/progress-capsule/progress-capsule.component.spec.ts b/libs/ui/components/src/lib/progress-capsule/progress-capsule.component.spec.ts new file mode 100644 index 000000000..6732b4d52 --- /dev/null +++ b/libs/ui/components/src/lib/progress-capsule/progress-capsule.component.spec.ts @@ -0,0 +1,47 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { ProgressCapsuleComponent } from './progress-capsule.component'; + +describe('ProgressCapsuleComponent', () => { + let fixture: ComponentFixture; + + function render(progress: number): HTMLElement { + fixture = TestBed.createComponent(ProgressCapsuleComponent); + fixture.componentRef.setInput('progress', progress); + fixture.detectChanges(); + return fixture.nativeElement as HTMLElement; + } + + it('fills the capsule to the watched share', () => { + const host = render(42); + + expect( + host.querySelector('.progress-capsule__fill')?.style + .width + ).toBe('42%'); + expect(host.querySelector('.progress-capsule--watched')).toBeNull(); + }); + + it('marks an item watched from 90 %', () => { + expect( + render(90).querySelector('.progress-capsule--watched') + ).not.toBeNull(); + }); + + // The capsule sits in app chrome (catalog grids, season episodes), so its + // fill is the theme's watch-progress token, like the dashboard rail card. + it('draws progress with the app watch-progress token', () => { + const source = readFileSync( + resolve( + process.cwd(), + 'libs/ui/components/src/lib/progress-capsule/progress-capsule.component.ts' + ), + 'utf8' + ); + const fill = source.match(/&__fill\s*\{([^}]*)\}/)?.[1] ?? ''; + + expect(fill).toMatch(/background:\s*var\(--app-progress-color\);/); + expect(source).not.toMatch(/#e50914|#ff4d4d/i); + }); +}); diff --git a/libs/ui/components/src/lib/progress-capsule/progress-capsule.component.ts b/libs/ui/components/src/lib/progress-capsule/progress-capsule.component.ts index 943f9a878..c1394d165 100644 --- a/libs/ui/components/src/lib/progress-capsule/progress-capsule.component.ts +++ b/libs/ui/components/src/lib/progress-capsule/progress-capsule.component.ts @@ -34,11 +34,7 @@ import { &__fill { height: 100%; - background: linear-gradient( - 90deg, - #e50914 0%, - #ff4d4d 100% - ); + background: var(--app-progress-color); transition: width 0.3s ease-out; border-radius: 0 2px 2px 0; } diff --git a/libs/ui/playback/src/lib/art-player/art-player.component.spec.ts b/libs/ui/playback/src/lib/art-player/art-player.component.spec.ts index 58a724723..81cddc8b0 100644 --- a/libs/ui/playback/src/lib/art-player/art-player.component.spec.ts +++ b/libs/ui/playback/src/lib/art-player/art-player.component.spec.ts @@ -287,6 +287,8 @@ describe('ArtPlayerComponent', () => { setting: true, fullscreen: true, fullscreenWeb: true, + // The skin's progress bar matches the shared dock's fill. + theme: '#4f8eff', }) ); // The app-level legacy shortcuts own the keyboard instead of diff --git a/libs/ui/playback/src/lib/art-player/art-player.component.ts b/libs/ui/playback/src/lib/art-player/art-player.component.ts index fd216f783..208b36c4f 100644 --- a/libs/ui/playback/src/lib/art-player/art-player.component.ts +++ b/libs/ui/playback/src/lib/art-player/art-player.component.ts @@ -15,6 +15,7 @@ import { import Artplayer from 'artplayer'; import { Channel, createDevLogger } from '@iptvnator/shared/interfaces'; import { releaseVideoPictureInPicture } from '../player-controls/web-video-picture-in-picture-lifecycle'; +import { PLAYER_PROGRESS_COLOR } from '../player-controls/player-palette'; import type { PlaybackDiagnostic } from '@iptvnator/playback/util'; import { type LegacyPlayerShortcuts, @@ -179,7 +180,9 @@ export class ArtPlayerComponent implements OnInit, OnDestroy, OnChanges { playsInline: true, backdrop: true, mutex: true, - theme: '#ff0000', + // Colours the vendor chrome (legacy skin only; shared controls + // hide it) with the player's progress colour, not a stray red. + theme: PLAYER_PROGRESS_COLOR, ...buildArtPlayerChrome(this.sharedControls), customType: sourceSession.customType, }); diff --git a/libs/ui/playback/src/lib/fullscreen-episode-panel/fullscreen-episode-panel.component.scss b/libs/ui/playback/src/lib/fullscreen-episode-panel/fullscreen-episode-panel.component.scss index ba06cf187..2ec56938c 100644 --- a/libs/ui/playback/src/lib/fullscreen-episode-panel/fullscreen-episode-panel.component.scss +++ b/libs/ui/playback/src/lib/fullscreen-episode-panel/fullscreen-episode-panel.component.scss @@ -1,7 +1,12 @@ +@use '../player-controls/player-palette' as palette; + // Stamped into the fullscreen panel's body, which is its single flex child; // the list below is the body's only scroll owner. The panel wears the // `dark-theme` context class, so the light-on-dark palette here is fixed. +// The panel sits beside the controls host, outside its `--pc-*` scope, so it +// declares the player's progress colour itself. :host { + @include palette.progress-token; display: flex; flex-direction: column; min-height: 0; @@ -238,7 +243,7 @@ .episode-panel__progress-bar { display: block; height: 100%; - background: #e50914; + background: var(--pc-progress); } // ─── Text ──────────────────────────────────────────────────────────────────── diff --git a/libs/ui/playback/src/lib/player-controls/_player-palette.scss b/libs/ui/playback/src/lib/player-controls/_player-palette.scss new file mode 100644 index 000000000..d33d887c5 --- /dev/null +++ b/libs/ui/playback/src/lib/player-controls/_player-palette.scss @@ -0,0 +1,19 @@ +// Player colours that surfaces outside the controls host need too. The +// controls declare their `--pc-*` palette on `app-player-controls`' `:host`; +// the Up Next rail and the fullscreen episode panel render beside that host, +// not inside it, so they include `progress-token` on their own `:host` to +// draw watch progress in the same colour as the dock. +// +// ArtPlayer's vendor chrome takes the same value as a string at construction: +// `PLAYER_PROGRESS_COLOR` in `player-palette.ts`, pinned to `$accent-blue` by +// `player-progress.palette.spec.ts`. + +// Accent blue: the primary action and watch progress over video. +$accent-blue: #4f8eff; + +// `--pc-progress`, the watch-progress fill over video. Literal like the rest +// of the overlay palette: the player is theme-independent, and the app's +// selection blue would change with the theme and fight the video. +@mixin progress-token { + --pc-progress: #{$accent-blue}; +} diff --git a/libs/ui/playback/src/lib/player-controls/player-controls.component.scss b/libs/ui/playback/src/lib/player-controls/player-controls.component.scss index 7bd311009..a8ca40172 100644 --- a/libs/ui/playback/src/lib/player-controls/player-controls.component.scss +++ b/libs/ui/playback/src/lib/player-controls/player-controls.component.scss @@ -1,10 +1,15 @@ +@use 'player-palette' as palette; + :host { // Overlay palette. Deliberately literal and theme-independent: the shared // controls always sit on video, and the UI guidelines keep them on one // light-on-dark palette in both app themes. The values are the design // tokens of the "Hybrid" controls concept (accent blue = primary action // and progress; cyan = something is on; violet = a value was changed). - --pc-accent-blue: #4f8eff; + // Accent blue and the progress fill come from the shared player palette, + // which the Up Next rail and the fullscreen episode panel also read. + --pc-accent-blue: #{palette.$accent-blue}; + @include palette.progress-token; // Fills that carry a white glyph (play, active tune) use deeper blues: // white on #4f8eff is only 3.2:1, too close to the 3:1 floor once a // 1x display antialiases the glyph. These give 4.4:1 and 5.3:1. diff --git a/libs/ui/playback/src/lib/player-controls/player-palette.ts b/libs/ui/playback/src/lib/player-controls/player-palette.ts new file mode 100644 index 000000000..2644137fb --- /dev/null +++ b/libs/ui/playback/src/lib/player-controls/player-palette.ts @@ -0,0 +1,7 @@ +/** + * The player's watch-progress colour (`--pc-progress`) for code that needs a + * colour string rather than a CSS custom property, such as ArtPlayer's + * `theme` option. It must equal `$accent-blue` in `_player-palette.scss`; + * `player-progress.palette.spec.ts` keeps the two in step. + */ +export const PLAYER_PROGRESS_COLOR = '#4f8eff'; diff --git a/libs/ui/playback/src/lib/player-controls/player-progress.palette.spec.ts b/libs/ui/playback/src/lib/player-controls/player-progress.palette.spec.ts new file mode 100644 index 000000000..4e44a840e --- /dev/null +++ b/libs/ui/playback/src/lib/player-controls/player-progress.palette.spec.ts @@ -0,0 +1,156 @@ +import { readdirSync, readFileSync } from 'node:fs'; +import { relative, resolve, sep } from 'node:path'; +import { PLAYER_PROGRESS_COLOR } from './player-palette'; + +// Watch progress inside the player is one colour: the fixed `--pc-progress`, +// never the app theme and never a literal of its own. The same episode must +// read the same in the dock's Up next card, the Up Next rail and the +// fullscreen episode panel (see the UI guidelines' Progress Bars section). +const LIB_DIR = resolve(process.cwd(), 'libs/ui/playback/src/lib'); +const CONTROLS_DIR = resolve(LIB_DIR, 'player-controls'); + +function stripComments(source: string): string { + return source + .replace(/\/\*[\s\S]*?\*\//g, '') + .replace(/(^|\s)\/\/[^\n]*/g, '$1'); +} + +function listSources(dir: string, extension: string): string[] { + return readdirSync(dir, { recursive: true, encoding: 'utf8' }) + .filter((file) => file.endsWith(extension)) + .filter((file) => !/\.spec(-[a-z]+)?\.ts$/.test(file)) + .map((file) => resolve(dir, file)); +} + +function read(file: string): string { + return stripComments(readFileSync(file, 'utf8')); +} + +/** The declarations of the first rule whose selector list is exactly `selector`. */ +function ruleBody(source: string, selector: string): string { + const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + return ( + source.match( + new RegExp(`(?:^|[}\\s])${escaped}\\s*\\{([^}]*)\\}`) + )?.[1] ?? '' + ); +} + +const PALETTE = read(resolve(CONTROLS_DIR, '_player-palette.scss')); +const STYLESHEETS = listSources(LIB_DIR, '.scss'); + +const PROGRESS_FILLS: ReadonlyArray<[file: string, selector: string]> = [ + [ + 'player-controls/player-timeline.component.scss', + '.player-controls__timeline-fill', + ], + [ + 'player-controls/player-up-next-card.component.scss', + '.player-up-next__progress-bar', + ], + [ + 'portal-inline-player/up-next-rail.component.scss', + '.up-next__progress-bar', + ], + [ + 'fullscreen-episode-panel/fullscreen-episode-panel.component.scss', + '.episode-panel__progress-bar', + ], +]; + +describe('player watch-progress colour', () => { + it('draws every progress fill inside the player with --pc-progress', () => { + for (const [file, selector] of PROGRESS_FILLS) { + expect({ + file, + body: ruleBody(read(resolve(LIB_DIR, file)), selector), + }).toEqual({ + file, + body: expect.stringMatching( + /background:\s*var\(--pc-progress\);/ + ), + }); + } + }); + + it('leaves no other BEM progress fill on a colour of its own', () => { + const fills = STYLESHEETS.flatMap((file) => + [ + ...read(file).matchAll( + /([^{}]*__progress-(?:bar|fill))\s*\{([^}]*)\}/g + ), + ].map(([, selector, body]) => ({ + fill: `${relative(LIB_DIR, file)} ${selector.trim()}`, + background: body.match( + /background(?:-color)?:\s*([^;]+);/ + )?.[1], + })) + ); + + expect(fills.length).toBeGreaterThanOrEqual(3); + expect( + fills.filter( + ({ background }) => background !== 'var(--pc-progress)' + ) + ).toEqual([]); + }); + + it('declares --pc-progress wherever it is read outside the controls host', () => { + // A top-level `:host` block runs to the first unindented `}`; Sass + // interpolation (`#{...}`) inside it would end `ruleBody` too early. + const declaresOnHost = (source: string) => + /@include palette\.progress-token;/.test( + source.match(/^:host\s*\{([\s\S]*?)^\}/m)?.[1] ?? '' + ); + + // The host declares the palette its own children (timeline, card) read. + expect( + declaresOnHost( + read(resolve(CONTROLS_DIR, 'player-controls.component.scss')) + ) + ).toBe(true); + + const readers = STYLESHEETS.filter( + (file) => + !file.startsWith(CONTROLS_DIR + sep) && + read(file).includes('var(--pc-progress') + ); + expect(readers.map((file) => relative(LIB_DIR, file)).sort()).toEqual([ + 'fullscreen-episode-panel/fullscreen-episode-panel.component.scss', + 'portal-inline-player/up-next-rail.component.scss', + ]); + for (const file of readers) { + expect({ file, declared: declaresOnHost(read(file)) }).toEqual({ + file, + declared: true, + }); + } + }); + + it('gives ArtPlayer the same colour as the Sass palette', () => { + const accent = PALETTE.match(/\$accent-blue:\s*(#[0-9a-f]{6});/i)?.[1]; + + expect(accent).toBeDefined(); + expect(PALETTE).toMatch( + /@mixin progress-token\s*\{\s*--pc-progress:\s*#\{\$accent-blue\};\s*\}/ + ); + expect(PLAYER_PROGRESS_COLOR.toLowerCase()).toBe(accent?.toLowerCase()); + expect( + read(resolve(LIB_DIR, 'art-player/art-player.component.ts')) + ).toMatch(/theme:\s*PLAYER_PROGRESS_COLOR,/); + }); + + it('keeps the old progress reds out of the player', () => { + const sources = [ + ...STYLESHEETS, + ...listSources(LIB_DIR, '.ts'), + ...listSources(LIB_DIR, '.html'), + ]; + const offenders = sources + .filter((file) => /#e50914|#ff0000\b/i.test(read(file))) + .map((file) => relative(LIB_DIR, file)); + + expect(sources.length).toBeGreaterThan(50); + expect(offenders).toEqual([]); + }); +}); diff --git a/libs/ui/playback/src/lib/player-controls/player-timeline.component.scss b/libs/ui/playback/src/lib/player-controls/player-timeline.component.scss index ebd986e32..f659b3b76 100644 --- a/libs/ui/playback/src/lib/player-controls/player-timeline.component.scss +++ b/libs/ui/playback/src/lib/player-controls/player-timeline.component.scss @@ -63,7 +63,7 @@ .player-controls__timeline-fill { height: 100%; - background: var(--pc-accent-blue); + background: var(--pc-progress); border-radius: inherit; } @@ -75,7 +75,8 @@ margin: -7px 0 0 -7px; border-radius: 50%; background: #ffffff; - box-shadow: 0 0 0 5px rgba(79, 142, 255, 0.3); + box-shadow: 0 0 0 5px + color-mix(in srgb, var(--pc-progress) 30%, transparent); pointer-events: none; transition: transform 120ms ease-out; } @@ -228,6 +229,7 @@ width: 11px; height: 11px; margin: -5.5px 0 0 -5.5px; - box-shadow: 0 0 0 4px rgba(79, 142, 255, 0.3); + box-shadow: 0 0 0 4px + color-mix(in srgb, var(--pc-progress) 30%, transparent); } } diff --git a/libs/ui/playback/src/lib/player-controls/player-up-next-card.component.scss b/libs/ui/playback/src/lib/player-controls/player-up-next-card.component.scss index 61d18bded..3351ff015 100644 --- a/libs/ui/playback/src/lib/player-controls/player-up-next-card.component.scss +++ b/libs/ui/playback/src/lib/player-controls/player-up-next-card.component.scss @@ -106,7 +106,7 @@ .player-up-next__progress-bar { display: block; height: 100%; - background: var(--pc-accent-blue, #4f8eff); + background: var(--pc-progress); } .player-up-next__meta { diff --git a/libs/ui/playback/src/lib/portal-inline-player/up-next-rail.component.scss b/libs/ui/playback/src/lib/portal-inline-player/up-next-rail.component.scss index b6c695c58..88a754d13 100644 --- a/libs/ui/playback/src/lib/portal-inline-player/up-next-rail.component.scss +++ b/libs/ui/playback/src/lib/portal-inline-player/up-next-rail.component.scss @@ -1,7 +1,11 @@ +@use '../player-controls/player-palette' as palette; + // Opaque panel docked on the stage's right column — it sits on top of the // black stage (or the ambient fill), so its own dark surface works for both -// app themes without :host-context branching. +// app themes without :host-context branching. Watch progress takes the +// player's `--pc-progress`, so an episode's bar matches the dock's Up next card. :host { + @include palette.progress-token; display: block; height: 100%; min-height: 0; @@ -112,7 +116,7 @@ .up-next__progress-bar { display: block; height: 100%; - background: #e50914; + background: var(--pc-progress); } .up-next__meta { diff --git a/libs/workspace/dashboard/feature/src/lib/rails/dashboard-hero.component.scss b/libs/workspace/dashboard/feature/src/lib/rails/dashboard-hero.component.scss index e42788f7a..bd214058b 100644 --- a/libs/workspace/dashboard/feature/src/lib/rails/dashboard-hero.component.scss +++ b/libs/workspace/dashboard/feature/src/lib/rails/dashboard-hero.component.scss @@ -342,7 +342,7 @@ width: 100%; height: 100%; border-radius: inherit; - background: var(--app-selection-color, #78adff); + background: var(--app-progress-color); // A full-width fill slid into place, neither a width nor animated: a // live-EPG tick moves it by under one percent, so an animation was // invisible yet cost a layout pass (width) or a burst of compositor diff --git a/libs/workspace/dashboard/feature/src/lib/rails/dashboard-progress-colour.spec.ts b/libs/workspace/dashboard/feature/src/lib/rails/dashboard-progress-colour.spec.ts new file mode 100644 index 000000000..52abf34b0 --- /dev/null +++ b/libs/workspace/dashboard/feature/src/lib/rails/dashboard-progress-colour.spec.ts @@ -0,0 +1,40 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +// Watch progress in app chrome reads one theme token, so a title's bar on a +// rail card, in the hero and in a catalog grid is the same colour. Live +// programme progress keeps the live accent (see the UI guidelines). +function styles(file: string): string { + return readFileSync(resolve(__dirname, file), 'utf8'); +} + +/** The declarations of the first `selector { … }` rule, nesting excluded. */ +function ruleBody(source: string, selector: RegExp): string { + return ( + source.match(new RegExp(`${selector.source}\\s*\\{([^}]*)\\}`))?.[1] ?? + '' + ); +} + +describe('dashboard watch-progress colour', () => { + it('fills the rail card artwork bar with the watch-progress token', () => { + const rail = styles('dashboard-rail.component.scss'); + const bar = rail.slice(rail.indexOf('.rail__art-progress {')); + + expect(ruleBody(bar, /\bi/)).toMatch( + /background:\s*var\(--app-progress-color\);/ + ); + }); + + it('fills the hero resume bar with the token and live bars with the live accent', () => { + const hero = styles('dashboard-hero.component.scss'); + const bar = hero.slice(hero.indexOf('.hero__progress {')); + + expect(ruleBody(bar, /\bi/)).toMatch( + /background:\s*var\(--app-progress-color\);/ + ); + expect(ruleBody(bar, /&--live i/)).toMatch( + /background:\s*var\(--app-live-color\b/ + ); + }); +}); diff --git a/libs/workspace/dashboard/feature/src/lib/rails/dashboard-rail.component.scss b/libs/workspace/dashboard/feature/src/lib/rails/dashboard-rail.component.scss index 608f761df..67841af2a 100644 --- a/libs/workspace/dashboard/feature/src/lib/rails/dashboard-rail.component.scss +++ b/libs/workspace/dashboard/feature/src/lib/rails/dashboard-rail.component.scss @@ -273,7 +273,7 @@ i { display: block; height: 100%; - background: var(--app-selection-color, #78adff); + background: var(--app-progress-color); transition: width 0.4s ease; } }