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
+54 -19
View File
@@ -31,6 +31,10 @@ import { Pressable } from "./ui/Pressable";
import { Input } from "./ui/Input";
import { SURFACE } from "./ui/surfaces";
import { SectionLabel } from "./ui/Headings";
import { useContextMenu } from "./ui/useContextMenu";
import { ContextMenu } from "./ui/ContextMenu";
import { MoreButton } from "./ui/MoreButton";
import { type ActionMenuItem } from "./ui/ActionMenu";
const POLL_MS = 4000;
// While any job is running, poll faster so short scheduled runs aren't missed between ticks.
@@ -487,6 +491,10 @@ export default function Scheduler() {
},
});
// The page's own menu: the status strip's "…" (E28 C1). Run all and Pause stay in sight; the
// destructive Purge discovery lives only here, behind its confirm.
const pageMenu = useContextMenu<"scheduler">();
if (q.isError && !data)
return (
<StateMessage tone="error" onRetry={() => q.refetch()}>
@@ -496,6 +504,45 @@ export default function Scheduler() {
if (q.isLoading && !data) return <LoadingState label={t("scheduler.loading")} />;
if (!data) return <div className="p-8 text-muted">{t("common.somethingWrong")}</div>;
async function purge() {
const ok = await confirm({
title: t("scheduler.purgeConfirmTitle"),
message: t("scheduler.purgeDiscoveryHint"),
confirmLabel: t("scheduler.purgeDiscovery"),
danger: true,
});
if (ok) purgeMut.mutate();
}
const paused = data.paused;
const pageItems: ActionMenuItem[] = [
{
id: "run-all",
label: t("scheduler.runAll"),
icon: <Play className="w-4 h-4" />,
disabled: paused || runAllMut.isPending,
note: paused ? t("scheduler.runAllPausedHint") : undefined,
onSelect: () => runAllMut.mutate(),
},
{
id: "pause",
label: paused ? t("scheduler.resume") : t("scheduler.pause"),
title: t("scheduler.pauseHint"),
icon: paused ? <Play className="w-4 h-4" /> : <Pause className="w-4 h-4" />,
disabled: pauseResume.isPending,
onSelect: () => pauseResume.mutate(paused),
},
{
id: "purge",
label: t("scheduler.purgeDiscovery"),
title: t("scheduler.purgeDiscoveryHint"),
icon: <Trash2 className="w-4 h-4" />,
danger: true,
separatorBefore: true,
disabled: purgeMut.isPending,
onSelect: () => void purge(),
},
];
const quotaPct = data.quota.daily_budget
? Math.min(100, Math.round((data.quota.used_today / data.quota.daily_budget) * 100))
: 0;
@@ -539,25 +586,6 @@ export default function Scheduler() {
{t("scheduler.runAll")}
</Button>
</Tooltip>
<Tooltip hint={t("scheduler.purgeDiscoveryHint")}>
<Button
variant="glass"
size="sm"
onClick={async () => {
const ok = await confirm({
title: t("scheduler.purgeConfirmTitle"),
message: t("scheduler.purgeDiscoveryHint"),
confirmLabel: t("scheduler.purgeDiscovery"),
danger: true,
});
if (ok) purgeMut.mutate();
}}
disabled={purgeMut.isPending}
>
<Trash2 className="w-4 h-4" />
{t("scheduler.purgeDiscovery")}
</Button>
</Tooltip>
<Tooltip hint={t("scheduler.pauseHint")}>
<Button
variant="glass"
@@ -569,6 +597,13 @@ export default function Scheduler() {
{data.paused ? t("scheduler.resume") : t("scheduler.pause")}
</Button>
</Tooltip>
<MoreButton
label={t("scheduler.menu")}
variant="glass"
expanded={pageMenu.open}
menuProps={pageMenu.buttonProps("scheduler")}
/>
<ContextMenu menu={pageMenu} ariaLabel={t("scheduler.menu")} items={pageItems} />
</div>
{!data.running_here && (