mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 09:01:03 -08:00
fix(player): pin overlay reds to the --pc-* palette (#1759)
This commit is contained in:
1 parent
963a431bb7
commit
03dfafd68b
6 files changed
+181
-8
No files matched your search
@@ -0,0 +1,6 @@
|
||||
---
|
||||
type: fix
|
||||
area: playback
|
||||
---
|
||||
|
||||
The player's LIVE badge uses a slightly deeper red so its label is easier to read, and the record button now turns red while a recording runs. Both reds stay the same in the light and dark themes.
|
||||
@@ -89,7 +89,9 @@ app text. Loader/stall and transient feedback overlays own a light foreground
|
||||
and dark scrim because they cover video. Video viewports remain black in both
|
||||
themes and fullscreen; frame-copy and built-in shared controls keep their
|
||||
light-on-dark overlay palette — the fixed `--pc-*` token set of the shared
|
||||
dock (accent blue, cyan, violet and a light text ramp), never the app theme.
|
||||
dock (accent blue, cyan, violet, the `--pc-live` / `--pc-danger` reds and a
|
||||
light text ramp), never the app theme. The overlay styles in
|
||||
`player-controls/` never read a `--mat-sys-*` token.
|
||||
|
||||
EPG timeline, list, empty states and programme details use the library-local
|
||||
`libs/ui/epg/src/lib/_epg-theme.scss` palette, based on app surfaces, separators,
|
||||
|
||||
@@ -309,8 +309,11 @@ of its own: a timeline row above a three-column control row, sitting
|
||||
directly on the video over the bottom scrim. The palette is a fixed set of
|
||||
`--pc-*` custom properties on `:host` — accent blue `#4f8eff` for the
|
||||
primary action and progress, cyan `#5cd6ff` for "something is on", violet
|
||||
`#b599ff` for "a value was changed", and the `#e7ecf3` / `#9aa3b2` /
|
||||
`#6b7384` text ramp. They are literal on purpose: the overlay is
|
||||
`#b599ff` for "a value was changed", the `#e7ecf3` / `#9aa3b2` /
|
||||
`#6b7384` text ramp, and two reds: `--pc-live` `#d32f2f` fills the LIVE
|
||||
badge (white label 5.0:1), and `--pc-danger` `#ff5252` colours the active
|
||||
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.
|
||||
|
||||
|
||||
@@ -12,6 +12,13 @@
|
||||
--pc-accent-blue-hover: #2a66d6;
|
||||
--pc-accent-cyan: #5cd6ff;
|
||||
--pc-accent-violet: #b599ff;
|
||||
// Reds, literal like the rest: the theme's error colour changes with the
|
||||
// theme, and its dark-theme tone is too pale to carry a white label.
|
||||
// White on the LIVE fill is 5.0:1; the earlier #e53935 gave only 4.2:1,
|
||||
// under the 4.5:1 text floor. The record glyph and the recording status
|
||||
// text read 6.2:1 on the glass over a black frame.
|
||||
--pc-live: #d32f2f;
|
||||
--pc-danger: #ff5252;
|
||||
--pc-text: #e7ecf3;
|
||||
--pc-text-secondary: #9aa3b2;
|
||||
--pc-text-tertiary: #6b7384;
|
||||
@@ -316,8 +323,13 @@
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.player-controls__record-button--active {
|
||||
color: var(--mat-sys-error, #ff5252) !important;
|
||||
// The glyph is the only visible part of the button, and the icon-button rule
|
||||
// below colours the mat-icon directly, so the red must name the icon too.
|
||||
// A disabled button (loading, e.g. a reconnect mid-recording) keeps the muted
|
||||
// disabled colour, so it does not look clickable.
|
||||
.player-controls__record-button--active:not([disabled]),
|
||||
.player-controls__record-button--active:not([disabled]) mat-icon {
|
||||
color: var(--pc-danger) !important;
|
||||
}
|
||||
|
||||
// Value chips (subtitles, speed): the two settings a viewer changes mid-film.
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
import { readdirSync, readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
// The overlay sits on video in both app themes, so its colours come from the
|
||||
// fixed `--pc-*` palette. A theme token read here stays invisible only while
|
||||
// the theme leaves it undeclared; once declared, the overlay follows the theme.
|
||||
const CONTROLS_DIR = resolve(
|
||||
process.cwd(),
|
||||
'libs/ui/playback/src/lib/player-controls'
|
||||
);
|
||||
|
||||
// Comments are dropped so prose that names a token can neither trip nor
|
||||
// satisfy the checks below. A `//` counts only after whitespace, which keeps
|
||||
// the `://` of a URL intact.
|
||||
function stripComments(source: string): string {
|
||||
return source
|
||||
.replace(/\/\*[\s\S]*?\*\//g, '')
|
||||
.replace(/(^|\s)\/\/[^\n]*/g, '$1');
|
||||
}
|
||||
|
||||
const STYLE_SOURCES = new Map(
|
||||
readdirSync(CONTROLS_DIR)
|
||||
.filter((file) => file.endsWith('.scss'))
|
||||
.map((file) => [
|
||||
file,
|
||||
stripComments(readFileSync(resolve(CONTROLS_DIR, file), 'utf8')),
|
||||
])
|
||||
);
|
||||
|
||||
const HOST_STYLES = STYLE_SOURCES.get('player-controls.component.scss') ?? '';
|
||||
const TIMELINE_STYLES =
|
||||
STYLE_SOURCES.get('player-timeline.component.scss') ?? '';
|
||||
|
||||
type Rgb = [number, number, number];
|
||||
|
||||
function paletteColor(token: string): { rgb: Rgb; alpha: number } {
|
||||
const value =
|
||||
HOST_STYLES.match(new RegExp(`${token}:\\s*([^;]+);`))?.[1].trim() ??
|
||||
'';
|
||||
const hex = value.match(/^#([0-9a-f]{6})$/i)?.[1];
|
||||
if (hex) {
|
||||
const rgb = [0, 2, 4].map((offset) =>
|
||||
Number.parseInt(hex.slice(offset, offset + 2), 16)
|
||||
) as Rgb;
|
||||
return { rgb, alpha: 1 };
|
||||
}
|
||||
const rgba = value.match(/^rgba\((\d+),\s*(\d+),\s*(\d+),\s*([\d.]+)\)$/);
|
||||
if (rgba) {
|
||||
const rgb = rgba.slice(1, 4).map(Number) as Rgb;
|
||||
return { rgb, alpha: Number(rgba[4]) };
|
||||
}
|
||||
throw new Error(`${token} is not a literal palette colour: "${value}"`);
|
||||
}
|
||||
|
||||
function contrastRatio(foreground: Rgb, background: Rgb): number {
|
||||
const luminance = (rgb: Rgb): number => {
|
||||
const [red, green, blue] = rgb.map((channel) => {
|
||||
const normalized = channel / 255;
|
||||
return normalized <= 0.04045
|
||||
? normalized / 12.92
|
||||
: ((normalized + 0.055) / 1.055) ** 2.4;
|
||||
});
|
||||
return red * 0.2126 + green * 0.7152 + blue * 0.0722;
|
||||
};
|
||||
const values = [luminance(foreground), luminance(background)].sort(
|
||||
(left, right) => right - left
|
||||
);
|
||||
return (values[0] + 0.05) / (values[1] + 0.05);
|
||||
}
|
||||
|
||||
describe('player controls overlay palette', () => {
|
||||
it('never reads the app theme system tokens', () => {
|
||||
expect([...STYLE_SOURCES.keys()]).toEqual(
|
||||
expect.arrayContaining([
|
||||
'player-controls.component.scss',
|
||||
'player-timeline.component.scss',
|
||||
])
|
||||
);
|
||||
const offenders = [...STYLE_SOURCES]
|
||||
.filter(([, source]) => source.includes('--mat-sys-'))
|
||||
.map(([file]) => file);
|
||||
|
||||
expect(offenders).toEqual([]);
|
||||
expect(
|
||||
stripComments('// names --mat-sys-error\n/* and --mat-sys-x */')
|
||||
).not.toContain('--mat-sys-');
|
||||
expect(stripComments("url('https://example.com/a.svg')")).toBe(
|
||||
"url('https://example.com/a.svg')"
|
||||
);
|
||||
});
|
||||
|
||||
it('draws the LIVE badge and the recording reds from the palette', () => {
|
||||
expect(TIMELINE_STYLES).toMatch(
|
||||
/\.player-controls__live-badge\s*\{[^}]*background:\s*var\(--pc-live\);/
|
||||
);
|
||||
expect(TIMELINE_STYLES).toMatch(
|
||||
/\.player-controls__recording-status--active\s*\{[^}]*color:\s*var\(--pc-danger\);/
|
||||
);
|
||||
expect(TIMELINE_STYLES).toMatch(
|
||||
/\.player-controls__recording-status--active mat-icon\s*\{[^}]*color:\s*var\(--pc-danger\);/
|
||||
);
|
||||
// The icon-button rule colours the mat-icon itself, so a red set only
|
||||
// on the button never reaches the glyph.
|
||||
expect(HOST_STYLES).toMatch(
|
||||
/\.player-controls__record-button--active:not\(\[disabled\]\),\s*\.player-controls__record-button--active:not\(\[disabled\]\) mat-icon\s*\{[^}]*color:\s*var\(--pc-danger\)/
|
||||
);
|
||||
});
|
||||
|
||||
it('lets a disabled record button keep its muted colour while recording', () => {
|
||||
// The red is `!important`, so an unscoped rule would also beat the
|
||||
// disabled icon colour: a recording that reconnects (loading, so the
|
||||
// button is disabled) would show a red glyph that cannot be clicked.
|
||||
const activeRecordSelectors = [
|
||||
...HOST_STYLES.matchAll(/([^{}]*)\{[^}]*var\(--pc-danger\)/g),
|
||||
].flatMap(([, selectors]) =>
|
||||
selectors
|
||||
.split(',')
|
||||
.map((selector) => selector.trim())
|
||||
.filter((selector) =>
|
||||
selector.includes('player-controls__record-button--active')
|
||||
)
|
||||
);
|
||||
|
||||
expect(activeRecordSelectors).toEqual([
|
||||
'.player-controls__record-button--active:not([disabled])',
|
||||
'.player-controls__record-button--active:not([disabled]) mat-icon',
|
||||
]);
|
||||
expect(HOST_STYLES).toMatch(
|
||||
/:host :is\(button\[mat-icon-button\]\[disabled\]\) mat-icon\s*\{[^}]*color:\s*var\(--pc-text-tertiary\);/
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps the palette reds readable on video', () => {
|
||||
const white: Rgb = [255, 255, 255];
|
||||
const glass = paletteColor('--pc-glass-bg');
|
||||
const glassOnBlackFrame = glass.rgb.map(
|
||||
(channel) => channel * glass.alpha
|
||||
) as Rgb;
|
||||
|
||||
// The white LIVE label is small bold text: the 4.5:1 text floor.
|
||||
expect(
|
||||
contrastRatio(white, paletteColor('--pc-live').rgb)
|
||||
).toBeGreaterThanOrEqual(4.5);
|
||||
// "REC 0:12" is text too, so it gets the text floor as well, not
|
||||
// just the 3:1 a glyph would need.
|
||||
expect(
|
||||
contrastRatio(paletteColor('--pc-danger').rgb, glassOnBlackFrame)
|
||||
).toBeGreaterThanOrEqual(4.5);
|
||||
});
|
||||
});
|
||||
@@ -141,12 +141,12 @@
|
||||
}
|
||||
|
||||
.player-controls__recording-status--active {
|
||||
color: var(--mat-sys-error, #ff5252);
|
||||
color: var(--pc-danger);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.player-controls__recording-status--active mat-icon {
|
||||
color: var(--mat-sys-error, #ff5252);
|
||||
color: var(--pc-danger);
|
||||
}
|
||||
|
||||
.player-controls__live-badge {
|
||||
@@ -156,7 +156,7 @@
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
color: #ffffff;
|
||||
background: var(--mat-sys-error, #e53935);
|
||||
background: var(--pc-live);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
|
||||
Reference in new issue
Block a user