fix(design): rating and live colour tokens, the player's menus over its notices, the info overlay on a light theme (E28)
- F16: --rating (theme-scoped gold, 4.5:1 on the page and its chip), --rating-media, --live/--live-fg, --media-chrome replace 14 palette literals; a lint rule refuses palette classes; the theme contract measures them. - The Plex player's notices drew over its open settings/tracks popovers: notices z-base, popovers z-menu. - The player's info overlay rendered theme tokens on its always-dark panel: on a light theme the score (2.6:1), collection years (3.2:1) and cast initials (1.05:1) were unreadable; onDark variants now. - The lint-rule contract tests share one ESLint with a one-off warm-up (a cold first lint took 8 s, past vitest's 5 s).
This commit is contained in:
@@ -30,6 +30,25 @@ function block(selector: string): Record<string, string> {
|
||||
|
||||
const status = (mode: Mode) => block(`html[data-theme="${mode}"]`);
|
||||
const scheme = (id: string, mode: Mode) => block(`html[data-scheme="${id}"][data-theme="${mode}"]`);
|
||||
// Theme-independent colours: what sits on media (a thumbnail, a poster, the player) looks the same in
|
||||
// dark and light, because the picture under it does.
|
||||
const media = () => block(":root");
|
||||
|
||||
// F16: a rating's gold and a broadcast's red are their own tokens, not palette literals and not a status
|
||||
// (a score is not a warning, "live" is not an error).
|
||||
describe("the media colours", () => {
|
||||
it("the live badge's text holds 4.5:1 on its fill", () => {
|
||||
expect(hexContrast(media()["live-fg"]!, media().live!)).toBeGreaterThanOrEqual(4.5);
|
||||
});
|
||||
// A poster card's score sits on a black 85–95% scrim over any picture: the worst case is 85% black
|
||||
// over white, #262626.
|
||||
it("a score on a poster's scrim holds 4.5:1", () => {
|
||||
expect(hexContrast(media()["rating-media"]!, "#262626")).toBeGreaterThanOrEqual(4.5);
|
||||
});
|
||||
it("the player chrome's white text holds 4.5:1", () => {
|
||||
expect(hexContrast("#ffffff", media()["media-chrome"]!)).toBeGreaterThanOrEqual(4.5);
|
||||
});
|
||||
});
|
||||
|
||||
it("styles exactly the schemes the app offers", () => {
|
||||
const inCss = new Set(
|
||||
@@ -94,8 +113,13 @@ describe.each(SCHEMES.map((s) => s.id))("scheme %s", (id) => {
|
||||
danger: hexContrast(s.danger!, bg),
|
||||
warn: hexContrast(s.warn!, bg),
|
||||
success: hexContrast(s.success!, bg),
|
||||
// A score is text, on the page and on its own 15% chip (PlexInfo); the live glyph is not text.
|
||||
rating: hexContrast(s.rating!, bg),
|
||||
ratingOnChip: hexContrast(s.rating!, mixHex(s.rating!, bg, 0.15)),
|
||||
live: hexContrast(media().live!, bg),
|
||||
};
|
||||
const failing = Object.entries(measured).filter(([k, v]) => v < (k === "edge" ? 3 : 4.5));
|
||||
const nonText = new Set(["edge", "live"]);
|
||||
const failing = Object.entries(measured).filter(([k, v]) => v < (nonText.has(k) ? 3 : 4.5));
|
||||
expect(failing.map(([k, v]) => `${k} ${v.toFixed(2)}:1`)).toEqual([]);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user