From eeada0ef6532654bbd8dc62e54726214ea0a9fe3 Mon Sep 17 00:00:00 2001 From: Miguel Palhas Date: Tue, 25 Aug 2026 21:14:29 +0100 Subject: [PATCH] fix(web): keep season controls on one line --- web/src/main.ts | 33 +++++++++++++++++++++-------- web/src/style.css | 54 ++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 68 insertions(+), 19 deletions(-) diff --git a/web/src/main.ts b/web/src/main.ts index ae4d895..e3f47bd 100644 --- a/web/src/main.ts +++ b/web/src/main.ts @@ -3628,10 +3628,20 @@ function seriesMain(tvDeck: TvReleasesView, views: HideableView[]): SeriesView { disclose.click(); }); - // season 0 never counts toward tracking or totals (§4.1, §4.2); say so + // season 0 never counts toward tracking or totals (§4.1, §4.2); say so. + // Both forms are rendered and CSS picks one: narrow viewports show the + // SxxEyy-style tag the rest of the app already uses, so the row's controls + // keep their place instead of wrapping under it. const name = document.createElement("span"); name.className = "season-name"; - name.textContent = season.number === 0 ? "specials · s00" : `season ${PAD_TWO(season.number)}`; + const nameLong = document.createElement("span"); + nameLong.className = "season-name-long"; + nameLong.textContent = + season.number === 0 ? "specials · s00" : `season ${PAD_TWO(season.number)}`; + const nameShort = document.createElement("span"); + nameShort.className = "season-name-short"; + nameShort.textContent = seasonTag(season.number); + name.append(nameLong, nameShort); const track = document.createElement("button"); track.type = "button"; @@ -3658,8 +3668,13 @@ function seriesMain(tvDeck: TvReleasesView, views: HideableView[]): SeriesView { }); }); - const space = document.createElement("span"); - space.className = "season-space"; + // everything that describes the season shares one shrinkable box; the + // controls sit outside it, so a long flag chip wraps within the head + // rather than pushing search and delete onto a line of their own. + const head = document.createElement("span"); + head.className = "season-head"; + const actions = document.createElement("span"); + actions.className = "season-actions"; const deckBtn = document.createElement("button"); deckBtn.type = "button"; @@ -3689,7 +3704,7 @@ function seriesMain(tvDeck: TvReleasesView, views: HideableView[]): SeriesView { }); }); - line.append(disclose, name, track, seasonCountsChip(season)); + head.append(name, track, seasonCountsChip(season)); // #227: the season the operator's report is about read `0/10` with // nothing saying a pack had been grabbed, downloaded in full and thrown // out at import. The counts chip beside this one is exactly the number @@ -3699,7 +3714,7 @@ function seriesMain(tvDeck: TvReleasesView, views: HideableView[]): SeriesView { const kind = blacklistClass(failure.reason); const when = failure.failed_at === null ? "" : ` ${formatSweepAge(failure.failed_at)}`; const told = `a pack for this season downloaded in full and failed at import${when} — ${failure.release} · ${blacklistAdvice(failure.reason)}`; - line.append( + head.append( chip(`import failed · ${blacklistReasonLabel(failure.reason)}`, (span) => { span.dataset.flag = "import-failed"; span.dataset.blacklist = kind; @@ -3711,13 +3726,12 @@ function seriesMain(tvDeck: TvReleasesView, views: HideableView[]): SeriesView { // The season-level twin of the episode flag above: gone upstream while // files under it remained. if (season.vanished) { - line.append( + head.append( chip("vanished", (span) => { span.dataset.flag = "vanished"; }), ); } - 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; @@ -3746,7 +3760,8 @@ function seriesMain(tvDeck: TvReleasesView, views: HideableView[]): SeriesView { ); } // rows without a delete still reserve its slot, so search keeps its column - line.append(space, deckBtn, clear ?? placeholderSlot()); + actions.append(deckBtn, clear ?? placeholderSlot()); + line.append(disclose, head, actions); item.append(line); const episodes = document.createElement("ul"); diff --git a/web/src/style.css b/web/src/style.css index 0d43ac2..cb8e957 100644 --- a/web/src/style.css +++ b/web/src/style.css @@ -1516,16 +1516,34 @@ body { border-bottom: 1px solid oklch(from var(--line) l c h / 45%); } +/* the line never wraps: the head shrinks and wraps inside itself, so the + controls keep the right edge on every viewport */ .season-line { display: flex; align-items: center; - flex-wrap: wrap; + flex-wrap: nowrap; gap: var(--space-2) var(--space-3); padding: var(--space-2) var(--space-1); cursor: pointer; transition: background 150ms var(--ease-out); } +.season-head { + display: flex; + align-items: center; + flex-wrap: wrap; + flex: 1; + min-width: 0; + gap: var(--space-2) var(--space-3); +} + +.season-actions { + display: flex; + align-items: center; + flex: none; + gap: var(--space-3); +} + /* 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, @@ -1568,6 +1586,7 @@ body { } .season-name { + flex: none; font-family: var(--font-display); font-size: var(--text-base); font-weight: 700; @@ -1576,16 +1595,19 @@ body { color: var(--ink); } +/* one name, two lengths — the wide one reads as prose, the narrow one is the + SxxEyy tag used everywhere else. display:none keeps the unused one out of + the accessibility tree, so the row is never announced twice. */ +.season-name-short { + display: none; +} + /* the tracked toggle reads as the rule it is; pressed = on */ .season-line .control, .ep-actions .control { padding: 0 var(--space-3); } -.season-space { - flex: 1; -} - .episodes { margin: 0; padding: 0 0 var(--space-2) var(--space-8); @@ -1968,8 +1990,10 @@ body { margin-left: auto; } + /* no side gutter on a phone: the width belongs to the content, and the + hero reaches both edges the way the rail already does */ .deck { - padding: var(--space-6) var(--space-4) var(--space-8); + padding: var(--space-6) 0 var(--space-8); } .row-chips { @@ -1977,6 +2001,20 @@ body { flex-basis: 100%; } + /* the season name shrinks to its tag so the head keeps fitting beside the + controls; the long form has nowhere to go on a phone */ + .season-name-long { + display: none; + } + + .season-name-short { + display: inline; + } + + .episodes { + padding-left: var(--space-4); + } + /* an episode row wraps: identity first, chips full-width, actions right */ .ep-chips { margin-left: 0; @@ -1987,10 +2025,6 @@ body { margin-left: auto; } - .episodes { - padding-left: var(--space-4); - } - .releases-id { order: -1; flex-basis: 100%;