diff --git a/apps/web/src/m3-theme.spec.ts b/apps/web/src/m3-theme.spec.ts new file mode 100644 index 000000000..bdacc25f6 --- /dev/null +++ b/apps/web/src/m3-theme.spec.ts @@ -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] + ); + }); +}); diff --git a/docs/architecture/iptvnator-ui-guidelines.md b/docs/architecture/iptvnator-ui-guidelines.md index 488066df5..14aeee560 100644 --- a/docs/architecture/iptvnator-ui-guidelines.md +++ b/docs/architecture/iptvnator-ui-guidelines.md @@ -531,10 +531,12 @@ 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. + 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 @@ -550,8 +552,10 @@ colour per context, and never a literal of its own: 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 +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 diff --git a/libs/ui/components/src/lib/season-container/season-container.component.scss b/libs/ui/components/src/lib/season-container/season-container.component.scss index ac8c8df6a..3ea92289a 100644 --- a/libs/ui/components/src/lib/season-container/season-container.component.scss +++ b/libs/ui/components/src/lib/season-container/season-container.component.scss @@ -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; } diff --git a/libs/ui/components/src/lib/season-container/season-container.progress-colour.spec.ts b/libs/ui/components/src/lib/season-container/season-container.progress-colour.spec.ts new file mode 100644 index 000000000..a8728c0dd --- /dev/null +++ b/libs/ui/components/src/lib/season-container/season-container.progress-colour.spec.ts @@ -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\);/); + }); +});