Merge #165: state ramp on status and counts chips
This commit is contained in:
@@ -205,7 +205,7 @@
|
||||
"refersTo": "status-channel",
|
||||
"description": "Library status (§4.2): airing / incomplete / waiting / complete / ended. Informational, not severity — colour marks activity in one channel-violet family (airing brightest, incomplete, waiting dimmest), satisfaction is neutral (complete bright ink, ended faint). The mono word is always present.",
|
||||
"html": "<span class=\"chip readout\" data-status=\"airing\">airing</span>",
|
||||
"css": ".chip[data-status=\"airing\"] { color: var(--status-airing); border-color: oklch(from var(--status-airing) l c h / 45%); } .chip[data-status=\"incomplete\"] { color: var(--status-incomplete); } .chip[data-status=\"waiting\"] { color: var(--status-waiting); } .chip[data-status=\"complete\"] { color: var(--status-complete); } .chip[data-status=\"ended\"] { color: var(--status-ended); }"
|
||||
"css": ".chip[data-status=\"airing\"] { color: var(--status-airing); border-color: oklch(from var(--status-airing) l c h / 45%); } .chip[data-status=\"incomplete\"] { color: var(--signal-warn); border-color: oklch(from var(--signal-warn) l c h / 55%); } .chip[data-status=\"complete\"], .chip[data-status=\"ended\"] { color: var(--signal-ok); border-color: oklch(from var(--signal-ok) l c h / 45%); } .chip[data-counts=\"complete\"] { color: var(--signal-ok); } .chip[data-counts=\"partial\"] { color: var(--signal-warn); }"
|
||||
},
|
||||
{
|
||||
"name": "Module (channel strip)",
|
||||
@@ -300,7 +300,7 @@
|
||||
"Do take every colour, spacing and font-size from the :root tokens; the token check (web/scripts/check-tokens.mjs) fails CI on literals."
|
||||
],
|
||||
"donts": [
|
||||
"Don't colour `complete` green or `incomplete` amber — library status is informational, not a severity ramp.",
|
||||
"Colour state chips on the ramp — `complete` and `ended` green, `incomplete` and a partial count amber, `airing` and `downloading` violet, `waiting` and `untracked` neutral. The word stays in every chip, so state survives with colour removed.",
|
||||
"Don't use fault red for a rejected release; red means the daemon itself is broken.",
|
||||
"Don't introduce a second interactive hue or use cyan for a status.",
|
||||
"Don't box content in soft-shadowed rounded cards or add glass/blur.",
|
||||
|
||||
+59
-44
@@ -715,6 +715,37 @@ function chip(text: string, extra?: (chip: HTMLSpanElement) => void): HTMLSpanEl
|
||||
return span;
|
||||
}
|
||||
|
||||
/** §4.2 state ramp on a media-state chip; the word carries, colour accelerates. */
|
||||
function mediaStateChip(state: string, wanted: boolean): HTMLSpanElement {
|
||||
return chip(state, (span) => {
|
||||
span.dataset.movieState = state;
|
||||
span.dataset.wanted = String(wanted);
|
||||
});
|
||||
}
|
||||
|
||||
/** The ramp read off N/M itself: green when full, amber while a gap remains. */
|
||||
function countsTone(available: number, wanted: number): "complete" | "partial" | null {
|
||||
if (wanted === 0) {
|
||||
return null;
|
||||
}
|
||||
return available >= wanted ? "complete" : "partial";
|
||||
}
|
||||
|
||||
function countsChip(
|
||||
label: string,
|
||||
available: number,
|
||||
wanted: number,
|
||||
ariaLabel: string,
|
||||
): HTMLSpanElement {
|
||||
return chip(label, (span) => {
|
||||
const tone = countsTone(available, wanted);
|
||||
if (tone !== null) {
|
||||
span.dataset.counts = tone;
|
||||
}
|
||||
span.setAttribute("aria-label", ariaLabel);
|
||||
});
|
||||
}
|
||||
|
||||
function rowTitle(title: string, year: number | null): HTMLElement {
|
||||
const wrap = document.createElement("span");
|
||||
wrap.className = "row-id";
|
||||
@@ -873,11 +904,7 @@ function libraryRow(
|
||||
const { item, row, body, chips } = richRow();
|
||||
const root = roots.find((candidate) => candidate.id === movie.root_id);
|
||||
chips.append(chip(root ? root.audience : `root ${movie.root_id}`));
|
||||
chips.append(
|
||||
chip(movie.state, (span) => {
|
||||
span.dataset.movieState = movie.state;
|
||||
}),
|
||||
);
|
||||
chips.append(mediaStateChip(movie.state, movie.wanted));
|
||||
// §5.7 honesty: a waived import is never presented as a clean match
|
||||
const waiver = waiverLabel(movie.waiver);
|
||||
if (waiver !== null) {
|
||||
@@ -1412,11 +1439,7 @@ function movieMain(board: HTMLElement, views: HideableView[]): MovieView {
|
||||
chipsEl.replaceChildren();
|
||||
const root = roots.find((candidate) => candidate.id === movie.root_id);
|
||||
chipsEl.append(chip(root ? root.audience : `root ${movie.root_id}`));
|
||||
chipsEl.append(
|
||||
chip(movie.state, (span) => {
|
||||
span.dataset.movieState = movie.state;
|
||||
}),
|
||||
);
|
||||
chipsEl.append(mediaStateChip(movie.state, movie.wanted));
|
||||
// §5.7 honesty: a waived import is never presented as a clean match
|
||||
const waiver = waiverLabel(movie.waiver);
|
||||
if (waiver !== null) {
|
||||
@@ -2573,12 +2596,12 @@ function seriesRow(
|
||||
chips.append(chip(root ? root.audience : `root ${series.root_id}`));
|
||||
if (series.wanted_episodes > 0) {
|
||||
chips.append(
|
||||
chip(`${series.available_episodes}/${series.wanted_episodes} eps`, (span) => {
|
||||
span.setAttribute(
|
||||
"aria-label",
|
||||
`${series.available_episodes} of ${series.wanted_episodes} wanted episodes on disk`,
|
||||
);
|
||||
}),
|
||||
countsChip(
|
||||
`${series.available_episodes}/${series.wanted_episodes} eps`,
|
||||
series.available_episodes,
|
||||
series.wanted_episodes,
|
||||
`${series.available_episodes} of ${series.wanted_episodes} wanted episodes on disk`,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (series.blocked) {
|
||||
@@ -2685,12 +2708,12 @@ function seriesCard(
|
||||
const chips = [chip(root ? root.audience : `root ${series.root_id}`)];
|
||||
if (series.wanted_episodes > 0) {
|
||||
chips.push(
|
||||
chip(`${series.available_episodes}/${series.wanted_episodes} eps`, (span) => {
|
||||
span.setAttribute(
|
||||
"aria-label",
|
||||
`${series.available_episodes} of ${series.wanted_episodes} wanted episodes on disk`,
|
||||
);
|
||||
}),
|
||||
countsChip(
|
||||
`${series.available_episodes}/${series.wanted_episodes} eps`,
|
||||
series.available_episodes,
|
||||
series.wanted_episodes,
|
||||
`${series.available_episodes} of ${series.wanted_episodes} wanted episodes on disk`,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (series.blocked) {
|
||||
@@ -2717,11 +2740,7 @@ function movieCard(
|
||||
): HTMLLIElement {
|
||||
const root = roots.find((candidate) => candidate.id === movie.root_id);
|
||||
const chips = [chip(root ? root.audience : `root ${movie.root_id}`)];
|
||||
chips.push(
|
||||
chip(movie.state, (span) => {
|
||||
span.dataset.movieState = movie.state;
|
||||
}),
|
||||
);
|
||||
chips.push(mediaStateChip(movie.state, movie.wanted));
|
||||
// §5.7 honesty: a waived import is never presented as a clean match
|
||||
const waiver = waiverLabel(movie.waiver);
|
||||
if (waiver !== null) {
|
||||
@@ -3020,12 +3039,12 @@ function seriesMain(board: HTMLElement, tvDeck: TvReleasesView, views: HideableV
|
||||
const root = roots.find((candidate) => candidate.id === current.root_id);
|
||||
chipsEl.append(chip(root ? root.audience : `root ${current.root_id}`));
|
||||
chipsEl.append(
|
||||
chip(`${current.available_episodes}/${current.wanted_episodes} eps`, (span) => {
|
||||
span.setAttribute(
|
||||
"aria-label",
|
||||
`${current.available_episodes} of ${current.wanted_episodes} wanted episodes on disk`,
|
||||
);
|
||||
}),
|
||||
countsChip(
|
||||
`${current.available_episodes}/${current.wanted_episodes} eps`,
|
||||
current.available_episodes,
|
||||
current.wanted_episodes,
|
||||
`${current.available_episodes} of ${current.wanted_episodes} wanted episodes on disk`,
|
||||
),
|
||||
);
|
||||
chipsEl.append(
|
||||
chip(current.status, (span) => {
|
||||
@@ -3132,12 +3151,12 @@ function seriesMain(board: HTMLElement, tvDeck: TvReleasesView, views: HideableV
|
||||
|
||||
function seasonCountsChip(season: ApiSeason): HTMLSpanElement {
|
||||
const counts = seasonCounts(season);
|
||||
return chip(`${counts.available}/${counts.wanted} on disk`, (span) => {
|
||||
span.setAttribute(
|
||||
"aria-label",
|
||||
`${counts.available} of ${counts.wanted} wanted episodes on disk`,
|
||||
);
|
||||
});
|
||||
return countsChip(
|
||||
`${counts.available}/${counts.wanted} on disk`,
|
||||
counts.available,
|
||||
counts.wanted,
|
||||
`${counts.available} of ${counts.wanted} wanted episodes on disk`,
|
||||
);
|
||||
}
|
||||
|
||||
function seasonRow(season: ApiSeason): HTMLLIElement {
|
||||
@@ -3287,11 +3306,7 @@ function seriesMain(board: HTMLElement, tvDeck: TvReleasesView, views: HideableV
|
||||
|
||||
const chips = document.createElement("span");
|
||||
chips.className = "row-chips ep-chips";
|
||||
chips.append(
|
||||
chip(episode.state, (span) => {
|
||||
span.dataset.movieState = episode.state;
|
||||
}),
|
||||
);
|
||||
chips.append(mediaStateChip(episode.state, episode.wanted));
|
||||
// issue 122: gone upstream while its file remained — a conflict, not a state
|
||||
if (episode.vanished) {
|
||||
chips.append(
|
||||
|
||||
+40
-21
@@ -61,13 +61,11 @@
|
||||
--score-5: oklch(0.79 0.165 129);
|
||||
--score-6: oklch(0.8 0.17 150);
|
||||
|
||||
/* library status (§4.2) — informational, never a severity ramp:
|
||||
activity in one violet family, satisfaction in neutrals */
|
||||
/* library state ramp (§4.2): green satisfied, amber gap, violet active,
|
||||
neutral nothing-happening. Satisfaction reuses --signal-ok and a gap
|
||||
--signal-warn, so the only hue this family still owns is the active one,
|
||||
which doubles as the downloading colour. */
|
||||
--status-airing: oklch(0.78 0.14 305);
|
||||
--status-incomplete: oklch(0.72 0.09 305);
|
||||
--status-waiting: oklch(0.64 0.045 305);
|
||||
--status-complete: oklch(0.85 0.01 250);
|
||||
--status-ended: oklch(0.62 0.015 250);
|
||||
|
||||
/* type */
|
||||
--font-label: system-ui, "Segoe UI", sans-serif;
|
||||
@@ -665,32 +663,52 @@ body {
|
||||
color: var(--verdict-rejected);
|
||||
}
|
||||
|
||||
/* media state ramp (§4.2): green on disk, violet downloading, amber wanted
|
||||
and still missing. Unwanted-missing and parked are nothing-happening and
|
||||
stay neutral; `parked` exists so a vanished grab never reads as a gap. */
|
||||
.chip[data-movie-state="available"] {
|
||||
color: var(--ink);
|
||||
color: var(--signal-ok);
|
||||
border-color: oklch(from var(--signal-ok) l c h / 45%);
|
||||
}
|
||||
|
||||
/* library status (§4.2): informational, never a severity ramp — activity in
|
||||
the channel-violet family, satisfaction in neutrals; the word is always
|
||||
present, so the state survives with colour removed */
|
||||
.chip[data-movie-state="downloading"] {
|
||||
color: var(--status-airing);
|
||||
border-color: oklch(from var(--status-airing) l c h / 45%);
|
||||
}
|
||||
|
||||
.chip[data-movie-state="missing"][data-wanted="true"] {
|
||||
color: var(--signal-warn);
|
||||
border-color: oklch(from var(--signal-warn) l c h / 55%);
|
||||
}
|
||||
|
||||
/* library status ramp (§4.2): violet while activity is live, green once
|
||||
everything wanted is on disk, amber for a grabbable gap; waiting is
|
||||
between seasons with nothing to do, so it stays neutral */
|
||||
.chip[data-status="airing"] {
|
||||
color: var(--status-airing);
|
||||
border-color: oklch(from var(--status-airing) l c h / 45%);
|
||||
}
|
||||
|
||||
.chip[data-status="incomplete"] {
|
||||
color: var(--status-incomplete);
|
||||
}
|
||||
|
||||
.chip[data-status="waiting"] {
|
||||
color: var(--status-waiting);
|
||||
}
|
||||
|
||||
.chip[data-status="complete"] {
|
||||
color: var(--status-complete);
|
||||
color: var(--signal-warn);
|
||||
border-color: oklch(from var(--signal-warn) l c h / 55%);
|
||||
}
|
||||
|
||||
.chip[data-status="complete"],
|
||||
.chip[data-status="ended"] {
|
||||
color: var(--status-ended);
|
||||
color: var(--signal-ok);
|
||||
border-color: oklch(from var(--signal-ok) l c h / 45%);
|
||||
}
|
||||
|
||||
/* season/series counts chip (§4.2): the same ramp read off N/M itself */
|
||||
.chip[data-counts="complete"] {
|
||||
color: var(--signal-ok);
|
||||
border-color: oklch(from var(--signal-ok) l c h / 45%);
|
||||
}
|
||||
|
||||
.chip[data-counts="partial"] {
|
||||
color: var(--signal-warn);
|
||||
border-color: oklch(from var(--signal-warn) l c h / 55%);
|
||||
}
|
||||
|
||||
/* a TMDB row is the affordance: the whole row opens the add flow */
|
||||
@@ -1598,10 +1616,11 @@ body {
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
/* the star is amber (§4.2): rating reads at a glance, the number stays ink */
|
||||
.star {
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
fill: var(--ink-muted);
|
||||
fill: var(--signal-warn);
|
||||
}
|
||||
|
||||
/* a row chip is smaller than the detail page's rating readout */
|
||||
|
||||
Reference in New Issue
Block a user