mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
fix(ui): one watch-progress colour in app chrome and in the player (#1798)
This commit is contained in:
1 parent
a8dd1eaa97
commit
4adc3ba20f
23 files changed
+441
-20
No files matched your search
@@ -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.
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
// A custom property that reads another one is resolved where it is declared,
|
||||
// so a token derived from a per-theme colour has to be declared again inside
|
||||
// `.dark-theme`; inherited from `html`, it would keep the light value there.
|
||||
const THEME = readFileSync(
|
||||
resolve(process.cwd(), 'apps/web/src/m3-theme.scss'),
|
||||
'utf8'
|
||||
)
|
||||
.replace(/\/\*[\s\S]*?\*\//g, '')
|
||||
.replace(/(^|\s)\/\/[^\n]*/g, '$1');
|
||||
|
||||
/** The body of the brace block that opens at `start` (an index of `{`). */
|
||||
function block(source: string, start: number): string {
|
||||
let depth = 0;
|
||||
for (let index = start; index < source.length; index++) {
|
||||
if (source[index] === '{') depth++;
|
||||
if (source[index] === '}' && --depth === 0) {
|
||||
return source.slice(start + 1, index);
|
||||
}
|
||||
}
|
||||
throw new Error('Unbalanced braces in m3-theme.scss');
|
||||
}
|
||||
|
||||
/** Light declarations (the `html` block minus `.dark-theme`) and dark ones. */
|
||||
function themeContexts(): { light: string; dark: string } {
|
||||
const darkStart = THEME.indexOf('.dark-theme {');
|
||||
const htmlStart = THEME.lastIndexOf('html {', darkStart);
|
||||
expect(darkStart).toBeGreaterThan(-1);
|
||||
expect(htmlStart).toBeGreaterThan(-1);
|
||||
|
||||
const html = block(THEME, THEME.indexOf('{', htmlStart));
|
||||
const dark = block(THEME, THEME.indexOf('{', darkStart));
|
||||
return { light: html.replace(dark, ''), dark };
|
||||
}
|
||||
|
||||
describe('m3-theme watch-progress token', () => {
|
||||
it('declares --app-progress-color from the selection colour in both themes', () => {
|
||||
const { light, dark } = themeContexts();
|
||||
const progress =
|
||||
/--app-progress-color:\s*var\(--app-selection-color\);/;
|
||||
const selection = /--app-selection-color:\s*#[0-9a-f]{6};/i;
|
||||
|
||||
for (const context of [light, dark]) {
|
||||
expect(context).toMatch(selection);
|
||||
expect(context).toMatch(progress);
|
||||
}
|
||||
expect(light.match(selection)?.[0]).not.toBe(
|
||||
dark.match(selection)?.[0]
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -211,7 +211,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
|
||||
|
||||
@@ -525,6 +525,39 @@ 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`, and the season list rows' own fill):
|
||||
`--app-progress-color`, declared per theme in `apps/web/src/m3-theme.scss`
|
||||
as that theme's `--app-selection-color`, and declared again inside
|
||||
`.dark-theme` because a derived custom property resolves where it is
|
||||
declared. 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: `m3-theme.spec.ts` (web: the token
|
||||
in both theme contexts), `player-progress.palette.spec.ts` (ui-playback),
|
||||
`progress-capsule.component.spec.ts` and
|
||||
`season-container.progress-colour.spec.ts` (components) and
|
||||
`dashboard-progress-colour.spec.ts` (workspace-dashboard-feature).
|
||||
|
||||
### Track
|
||||
|
||||
- Height:
|
||||
|
||||
@@ -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 `<li>`s of a `<ul>`, 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
|
||||
|
||||
@@ -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<ProgressCapsuleComponent>;
|
||||
|
||||
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<HTMLElement>('.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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -671,7 +671,7 @@ app-season-tabs {
|
||||
|
||||
&__progress-fill {
|
||||
height: 100%;
|
||||
background: var(--mat-sys-primary);
|
||||
background: var(--app-progress-color);
|
||||
border-radius: 2px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
// Watch progress in app chrome reads one theme token in both season view
|
||||
// modes; the list rows draw their own fill rather than the grid's capsule
|
||||
// (see the UI guidelines' Progress Bars section).
|
||||
const DIR = resolve(
|
||||
process.cwd(),
|
||||
'libs/ui/components/src/lib/season-container'
|
||||
);
|
||||
|
||||
function read(file: string): string {
|
||||
return readFileSync(resolve(DIR, file), 'utf8');
|
||||
}
|
||||
|
||||
describe('season container watch-progress colour', () => {
|
||||
it('fills list rows with the app watch-progress token', () => {
|
||||
const styles = read('season-container.component.scss');
|
||||
const listItem = styles.slice(styles.indexOf('.episode-list-item {'));
|
||||
const fill = listItem.match(/&__progress-fill\s*\{([^}]*)\}/)?.[1];
|
||||
|
||||
expect(read('season-container.component.html')).toContain(
|
||||
'class="episode-list-item__progress-fill"'
|
||||
);
|
||||
expect(fill).toMatch(/background:\s*var\(--app-progress-color\);/);
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
+6
-1
@@ -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 ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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';
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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/
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user