From ae9c2489871ba9827787bc75a35988dab5d29ce8 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Sun, 27 Sep 2026 06:35:40 +0200 Subject: [PATCH] fix(downloads): Retry draws the refresh glyph, not Play (E28 S4) The first UI-review run (Downloads) found a failed row whose Retry wore the Play glyph in the row and its menu, promising a video. DESIGN.md gains 'a glyph means one thing' and a log row. --- docs/DESIGN.md | 4 ++++ .../components/DownloadCenter.menu.dom.test.tsx | 16 ++++++++++++++++ frontend/src/components/DownloadCenter.tsx | 8 +++++++- frontend/src/components/useDownloadActions.tsx | 3 ++- 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/docs/DESIGN.md b/docs/DESIGN.md index e6f4201b..123b8d08 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -216,6 +216,9 @@ Also in force: - A single-select row of filter chips goes through `FilterChipGroup` (its options are `Chip`s). - A button that starts slow work shows `loading` (a spinner, `aria-busy`) and can't be pressed twice. [Nielsen #1] +- **A glyph means one thing.** Play (▷) starts playback, or resumes a paused transfer; a retry is the + refresh arrow (`RefreshCw`); a reset or revert is the counter-clockwise arrow (`RotateCcw`). A failed + download's Retry had worn the Play glyph, which promised a video. [Nielsen #4] ## Surfaces and page chrome — _Decided_ (batch B) @@ -613,3 +616,4 @@ pick a density step; it never changes the anatomy or loses an action. [Nielsen # | 2026-09-27 | Conformance check (S3g) | A four-step check before a UI change is done: primitive at a named rung, the chapter re-read, the rendered page measured by kind and look, every state seen | A reviewer's eye per sprint | The S1 probe found 69 button looks; after S3 every control kind is drawn by its primitive, and the check keeps a new look from arriving unnoticed | | 2026-09-27 | Segmented rung | `SegmentedToggle` defaults to the toolbar rung (28, `text-xs`); 24 only for a panel's captioned match toggle | The small default | Two toolbars had inherited 10px type from the default beside 28px buttons | | 2026-09-27 | Copy rule (S3g) | One name per feature (the rail may shorten it); four verbs for four meanings; sentence case with the app's places capitalised; "…" when a command asks first; one-sentence messages without a stop; curly quotes; dates relative in lists with the exact minute on hover, three absolute forms; units from Intl, short | Word-for-word names everywhere; the verbs as they were; all lower case; no "…"; always a stop; always absolute dates; long or compact units | The copy survey found 7 EN and 6 HU double names, 22 Delete against 27 Remove, five date shapes and three spellings of a minute; the owner picked every recommended option | +| 2026-09-27 | One glyph, one meaning (S4) | Play only for playback or resuming a paused transfer; Retry = `RefreshCw`; reset / revert = `RotateCcw` | Retry drawn with Play | The first UI-review run (Downloads) found a failed row whose Retry showed ▷ in the row and its menu | diff --git a/frontend/src/components/DownloadCenter.menu.dom.test.tsx b/frontend/src/components/DownloadCenter.menu.dom.test.tsx index 72b8629a..6b7eb269 100644 --- a/frontend/src/components/DownloadCenter.menu.dom.test.tsx +++ b/frontend/src/components/DownloadCenter.menu.dom.test.tsx @@ -132,6 +132,22 @@ describe("DownloadCenter · the queue row", () => { ]); }); + it("draws Retry as 'do it again', never the Play glyph (row and menu)", async () => { + mocks.jobs = [job(1, "error"), job(2, "paused")]; + mount(); + const glyph = (el: Element) => el.querySelector("svg")?.getAttribute("class") ?? ""; + const retry = rowOf("Clip 1").querySelector('[aria-label="downloads.actions.retry"]')!; + expect(glyph(retry)).toContain("lucide-refresh-cw"); + // A paused job resumes: that one keeps the transport glyph. + const resume = rowOf("Clip 2").querySelector('[aria-label="downloads.actions.resume"]')!; + expect(glyph(resume)).toContain("lucide-play"); + await openMenu(() => fireEvent.contextMenu(rowOf("Clip 1"))); + const row = screen + .getAllByRole("menuitem", { hidden: true }) + .find((b) => b.textContent === "downloads.actions.retry")!; + expect(glyph(row)).toContain("lucide-refresh-cw"); + }); + it("closes on a second press of the same '…'", async () => { mocks.jobs = [job(1, "running")]; mount(); diff --git a/frontend/src/components/DownloadCenter.tsx b/frontend/src/components/DownloadCenter.tsx index 208b3d3f..bca8487c 100644 --- a/frontend/src/components/DownloadCenter.tsx +++ b/frontend/src/components/DownloadCenter.tsx @@ -9,6 +9,7 @@ import { Play, PlayCircle, Plus, + RefreshCw, Settings2, Share2, UploadCloud, @@ -225,7 +226,12 @@ export default function DownloadCenter() { : "downloads.actions.resume", )} > - + {/* Retry is "do it again", not playback: the Play glyph promised a video. */} + {job.status === "error" ? ( + + ) : ( + + )} )} diff --git a/frontend/src/components/useDownloadActions.tsx b/frontend/src/components/useDownloadActions.tsx index 183adc14..3e31a8dd 100644 --- a/frontend/src/components/useDownloadActions.tsx +++ b/frontend/src/components/useDownloadActions.tsx @@ -9,6 +9,7 @@ import { Pencil, Play, PlayCircle, + RefreshCw, RotateCcw, Scissors, Share2, @@ -139,7 +140,7 @@ export function useDownloadActions({ label: t( job.status === "error" ? "downloads.actions.retry" : "downloads.actions.resume", ), - icon: icon(Play), + icon: icon(job.status === "error" ? RefreshCw : Play), onSelect: () => act.mutate({ id: job.id, op: "resume" }), }, ],