fix(web): steady action slot, red delete, clickable season

Closes #254
This commit is contained in:
Miguel Palhas
2026-08-25 19:06:25 +01:00
parent c9e44b9c03
commit dee7340a01
3 changed files with 59 additions and 4 deletions
+1
View File
@@ -52,6 +52,7 @@ export function armedDeleteIcon(label: string, execute: () => void): HTMLButtonE
const button = document.createElement("button");
button.type = "button";
button.className = "control control-quiet control-icon";
button.dataset.kind = "delete";
button.setAttribute("aria-label", label);
button.title = label;
button.replaceChildren(icon("trash"));
+28 -4
View File
@@ -1285,6 +1285,14 @@ function wireCollapsedToggle(bucket: CollapsedBucketDom) {
// icon() and armedDeleteIcon() live in icons.ts, shared with the settings
// rows (#231); the §9.6 painters below stay here as their only consumer.
/** Invisible spacer that reserves a delete control's slot (#254). */
function placeholderSlot(): HTMLSpanElement {
const slot = document.createElement("span");
slot.className = "slot-icon";
slot.setAttribute("aria-hidden", "true");
return slot;
}
/** §9.6 provider marks, drawn and shipped inline — never a remote image. */
function providerMark(name: "tmdb" | "tvdb" | "imdb" | "rt"): SVGSVGElement {
const svg = document.createElementNS(SVG_NS, "svg");
@@ -3605,6 +3613,15 @@ function seriesMain(tvDeck: TvReleasesView, views: HideableView[]): SeriesView {
disclose.focus();
});
// the whole line expands the season — the chevron stays the accessible
// control and this is a second way in; clicks on real controls opt out
line.addEventListener("click", (event) => {
if (event.target instanceof Element && event.target.closest("button")) {
return;
}
disclose.click();
});
// season 0 never counts toward tracking or totals (§4.1, §4.2); say so
const name = document.createElement("span");
name.className = "season-name";
@@ -3697,8 +3714,9 @@ function seriesMain(tvDeck: TvReleasesView, views: HideableView[]): SeriesView {
line.append(space, deckBtn);
// #174: files go, episodes stop being wanted, the season stays listed.
// Only offered with files on disk — intent alone is the tracked toggle.
let clear: HTMLButtonElement | null = null;
if (season.episodes.some((episode) => filesByEpisode.has(episode.id))) {
const clear = armedDeleteIcon(
clear = armedDeleteIcon(
`remove ${season.number === 0 ? "specials" : `season ${PAD_TWO(season.number)}`} files from disk and stop wanting its episodes — the season stays listed`,
() => {
const currentId = seriesId;
@@ -3707,7 +3725,9 @@ function seriesMain(tvDeck: TvReleasesView, views: HideableView[]): SeriesView {
}
void removeSeasonFiles(currentId, season.number).then((outcome) => {
if (outcome.kind === "error") {
clear.disabled = false;
if (clear) {
clear.disabled = false;
}
setStatus(`remove failed — ${outcome.detail}`, "fault");
return;
}
@@ -3718,8 +3738,9 @@ function seriesMain(tvDeck: TvReleasesView, views: HideableView[]): SeriesView {
});
},
);
line.append(clear);
}
// rows without a delete still reserve its slot, so search keeps its column
line.append(space, deckBtn, clear ?? placeholderSlot());
item.append(line);
const episodes = document.createElement("ul");
@@ -3804,7 +3825,7 @@ function seriesMain(tvDeck: TvReleasesView, views: HideableView[]): SeriesView {
const note = document.createElement("span");
note.className = "readout dim ep-unaired";
note.textContent = "unaired";
actions.append(note);
actions.append(note, placeholderSlot());
} else {
const want = document.createElement("button");
want.type = "button";
@@ -3851,6 +3872,9 @@ function seriesMain(tvDeck: TvReleasesView, views: HideableView[]): SeriesView {
});
});
actions.append(deckBtn);
// no delete on a wantable episode, but its slot is still reserved so
// search sits in the same column as on an on-disk row
actions.append(placeholderSlot());
}
item.append(tag, name, air, chips, actions);
+30
View File
@@ -1152,6 +1152,18 @@ body {
border-color: var(--signal-warn);
}
/* a destructive control rests red, so delete never reads like the search
glyph beside it; arming hands over to the shared amber treatment below */
.control[data-kind="delete"]:not(:disabled) {
color: var(--signal-fault);
border-color: oklch(from var(--signal-fault) l c h / 40%);
}
.control[data-kind="delete"]:hover:not(:disabled),
.control[data-kind="delete"]:focus-visible:not(:disabled) {
border-color: var(--signal-fault);
}
/* the settings arm-then-confirm, shared with season and episode rows: an
armed control turns caution amber until it disarms or fires */
.control[data-armed="true"] {
@@ -1510,6 +1522,15 @@ body {
flex-wrap: wrap;
gap: var(--space-2) var(--space-3);
padding: var(--space-2) var(--space-1);
cursor: pointer;
transition: background 150ms var(--ease-out);
}
/* a row under the pointer is visibly the row the click will land on —
the same lift a search result row gets */
.season-line:hover,
.episode:hover {
background: var(--panel);
}
/* the disclosure is a drawn chevron — no glyph standing in for an icon */
@@ -1578,6 +1599,7 @@ body {
gap: var(--space-1) var(--space-2);
padding: var(--space-2) var(--space-1);
border-bottom: 1px solid oklch(from var(--line) l c h / 45%);
transition: background 150ms var(--ease-out);
}
.ep-tag {
@@ -1613,6 +1635,14 @@ body {
font-size: var(--text-xs);
}
/* an invisible spacer sized as an icon control: rows without a delete
reserve its slot so search sits in the same column on every row */
.slot-icon {
flex: none;
width: var(--control-h);
height: var(--control-h);
}
/* #227: the season the operator's own report was about read `0/10` beside
this chip's absence. It sits next to the counts because that number is what
looked like nothing had ever been tried. */