mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 19:46:16 -08:00
fix(player): pin overlay reds to the --pc-* palette
The shared player overlay keeps a fixed, theme-independent palette, but its LIVE badge, record button and recording status still read the theme error token. The theme declares no system tokens yet, so the literal fallbacks won. Once it does, the dark LIVE badge becomes white on #ffb4ab (1.7:1) and the light recording text #ba1a1a on the glass (3.0:1). - Add --pc-live (#d32f2f, white label 5.0:1; the old #e53935 was 4.2:1) and --pc-danger (#ff5252, 6.2:1 on the glass) to the palette. - Colour the active record glyph as well. The icon-button rule sets the mat-icon colour directly, so the red on the button never reached it. - Add a spec that rejects --mat-sys-* in the player-controls styles and checks both reds against their contrast floors. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
963a431bb7
commit
cdb61960ee
6 files changed
+140
-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,11 @@
|
||||
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.
|
||||
.player-controls__record-button--active,
|
||||
.player-controls__record-button--active mat-icon {
|
||||
color: var(--pc-danger) !important;
|
||||
}
|
||||
|
||||
// Value chips (subtitles, speed): the two settings a viewer changes mid-film.
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
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'
|
||||
);
|
||||
|
||||
const STYLE_SOURCES = new Map(
|
||||
readdirSync(CONTROLS_DIR)
|
||||
.filter((file) => file.endsWith('.scss'))
|
||||
.map((file) => [
|
||||
file,
|
||||
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([]);
|
||||
});
|
||||
|
||||
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,\s*\.player-controls__record-button--active mat-icon\s*\{[^}]*color:\s*var\(--pc-danger\)/
|
||||
);
|
||||
});
|
||||
|
||||
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