feat(design): the download, notification and Scheduler action lists (C1) — the rows' "…", the band's and the strip's "…" (E28 S3e3)

A download row keeps play (pause / resume / retry in the queue) and the save link; the editors,
sharing, re-download, the copies and cancel / delete / remove moved into its "…"
(useDownloadActions). A link inside the row keeps the browser's own right-click menu. A
notification row keeps its link and its unread mark and shows its dismiss on hover; an activity
notice's Find in feed and Undo are in its "…". The inbox band keeps Mark all read and puts Clear
everything, now behind a confirm, in its "…". The Scheduler strip keeps Run all and Pause and puts
Purge discovery in its "…". Polled lists key their menus by id (keyOf). ui/menuGroups draws a
rule only between two non-empty groups. Find in feed also drops the tag, search and age filters
that could hide the video it looks for.
This commit is contained in:
2026-09-26 07:10:02 +02:00
parent c4917619ee
commit a2d17d693c
16 changed files with 1455 additions and 324 deletions
+24 -19
View File
@@ -1,12 +1,10 @@
import { useState, type ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { CalendarClock, Copy, File, Link2, Scissors } from "lucide-react";
import { CalendarClock, File, Link2, Scissors } from "lucide-react";
import clsx from "clsx";
import { qk } from "../lib/queryKeys";
import { useLiveQuery } from "../lib/useLiveQuery";
import { api, type DownloadJob } from "../lib/api";
import { notify } from "../lib/notifications";
import { copyText } from "../lib/clipboard";
import {
formatBytes,
formatDateTime,
@@ -161,16 +159,14 @@ function displayUrl(url: string): string {
}
}
// "Downloaded from" reference: opens the original page in a new tab, or copies the link.
// A link inside the row keeps the BROWSER's own right-click menu (open in a new tab, copy the
// address): the row's menu answers everywhere else on it (E28 C1).
const nativeMenu = { onContextMenu: (e: React.MouseEvent) => e.stopPropagation() };
// "Downloaded from" reference: opens the original page in a new tab. Its copy moved into the row's
// menu ("Copy link"); an extra link copies through the browser's own link menu.
function SourceRef({ url }: { url: string }) {
const { t } = useTranslation();
const copy = async () => {
if (await copyText(url)) {
notify({ level: "success", message: t("downloads.source.copied") });
} else {
notify({ level: "error", message: t("downloads.source.copyFailed") });
}
};
return (
<div className="flex items-center gap-1 mt-1 text-sm text-muted min-w-0">
<Link2 className="w-3.5 h-3.5 shrink-0" />
@@ -180,12 +176,10 @@ function SourceRef({ url }: { url: string }) {
rel="noopener noreferrer"
title={`${t("downloads.source.open")} — ${url}`}
className="truncate leading-6 hover:text-accent hover:underline pointer-coarse:leading-(--touch-target)"
{...nativeMenu}
>
{displayUrl(url)}
</a>
<IconButton onClick={copy} label={t("downloads.source.copy")}>
<Copy />
</IconButton>
</div>
);
}
@@ -202,6 +196,7 @@ function ChannelLine({ job }: { job: DownloadJob }) {
rel="noopener noreferrer"
title={url}
className="text-muted hover:text-accent hover:underline"
{...nativeMenu}
>
{name}
</a>
@@ -215,12 +210,18 @@ export function DownloadRow({
job,
onShowError,
navIndex,
rowProps,
more,
children,
}: {
job: DownloadJob;
onShowError?: (job: DownloadJob) => void;
/** Its position in a keyboard-walkable list (lib/listNav). */
navIndex?: number;
/** The list's menu triggers (right-click, long-press, Shift+F10): `useContextMenu().triggerProps`. */
rowProps?: Record<string, unknown>;
/** The row's "…", last in its action strip (E28 C1). */
more?: ReactNode;
children?: ReactNode;
}) {
const { t } = useTranslation();
@@ -228,7 +229,8 @@ export function DownloadRow({
return (
<div
{...(navIndex !== undefined && navItemAttrs(navIndex, jobTitle(job)))}
className={`${SURFACE.card} flex gap-3 p-2.5 glass-hover transition`}
{...rowProps}
className={`${SURFACE.card} group flex gap-3 p-2.5 glass-hover transition`}
>
<Thumb job={job} />
<div className="min-w-0 flex-1">
@@ -302,10 +304,13 @@ export function DownloadRow({
</div>
)}
</div>
{/* Row action buttons: the md rung here (the row stands alone), the sm rung in the modals, one
IconBtn for both. `shrink-0` + no wrap keeps them on the title's line; the text column
shrinks instead. */}
<div className="flex items-start gap-1 shrink-0">{children}</div>
{/* The row's visible actions — its primary one and the save link — then its "…": the md rung
here (the row stands alone), the sm rung in the modals, one IconBtn for both. `shrink-0` + no
wrap keeps them on the title's line; the text column shrinks instead. */}
<div className="flex items-start gap-1 shrink-0">
{children}
{more}
</div>
</div>
);
}