feat(design): a channel's and a Plex playlist's one action list (C1) — the row's "…", Delete + undo, an action toast's 8 s floor (E28 S3e2)

- Channels: the row's full-history, hide and unsubscribe buttons become its "…" at the row end
  (useChannelActions: open, pin, YouTube, copy name / link, full history or the admin reset,
  hide with an undo toast, unsubscribe behind its danger confirm); the card layout docks the "…"
  beside the name (ChannelLayoutGrid moreKey). The admin reset's confirm now names the reset.
- Plex playlist page: a title, a show group and a season each open the page's one row menu (a
  title's is its usePlexCardActions list plus the removal); no X at rest; removals take an undo
  toast whose Undo puts every row back at its own index in one request (bulk add `positions`);
  j/k walk the visible rows, Delete removes the walked one; rename and delete in the header's "…".
- notify: a toast with an action lives at least 8 s (the owner's call), whatever the toast setting.
This commit is contained in:
2026-09-26 05:56:07 +02:00
parent b0e51c6f89
commit 1b946113c6
27 changed files with 1354 additions and 293 deletions
+17 -33
View File
@@ -1,4 +1,4 @@
import type { ComponentProps } from "react";
import type { ComponentProps, ReactNode } from "react";
import type { TFunction } from "i18next";
import { NO_TAG_SENTINEL, type ManagedChannel } from "../lib/api";
import { formatCountOrDash, formatTotalHours, relativeTime } from "../lib/format";
@@ -7,7 +7,7 @@ import { PriorityStepper } from "./ui/PriorityStepper";
import ChannelLink from "./ChannelLink";
import AboutCell from "./AboutCell";
import { ChannelTagsControl, type SuggestedTag } from "./ChannelTags";
import { ActionsCell, StoredCell } from "./channelCells";
import { StoredCell } from "./channelCells";
import { isFullySynced } from "./channelSync";
/** The shared "subscribers" DataTable column (right-aligned, sortable, em-dash for null) — identical
@@ -49,16 +49,12 @@ export function tagsColumnTest(c: ManagedChannel, values: string[]): boolean {
* column block lives outside the page component (its cell components are in channelCells). */
export interface ChannelColumnDeps {
t: TFunction;
isAdmin: boolean;
canWrite: boolean;
prioOptions: { value: string; label: string }[];
userTags: ComponentProps<typeof ChannelTagsControl>["userTags"];
bumpPriority: (id: string, delta: number) => void;
setPriority: (id: string, value: number) => void;
onHide: (c: ManagedChannel) => void;
onDeep: (c: ManagedChannel) => void;
onReset: (c: ManagedChannel) => void;
onUnsub: (c: ManagedChannel) => void;
/** The row's "…" — the door to the channel's one action list (`useChannelActions`). */
more: (c: ManagedChannel) => ReactNode;
onView: (c: ManagedChannel) => void;
toggleTag: (channelId: string, tagId: number, hasTag: boolean) => void;
createAndAttachTag: (channelId: string, name: string) => void;
@@ -72,16 +68,11 @@ export interface ChannelColumnDeps {
export function buildChannelColumns(deps: ChannelColumnDeps): Column<ManagedChannel>[] {
const {
t,
isAdmin,
canWrite,
prioOptions,
userTags,
bumpPriority,
setPriority,
onHide,
onDeep,
onReset,
onUnsub,
more,
onView,
toggleTag,
createAndAttachTag,
@@ -109,25 +100,6 @@ export function buildChannelColumns(deps: ChannelColumnDeps): Column<ManagedChan
/>
),
},
{
key: "actions",
header: t("channels.cols.actions"),
align: "left",
nowrap: true,
width: "5.5rem",
cardLabel: false,
render: (c) => (
<ActionsCell
c={c}
isAdmin={isAdmin}
canWrite={canWrite}
onHide={() => onHide(c)}
onDeep={() => onDeep(c)}
onReset={() => onReset(c)}
onUnsubscribe={() => onUnsub(c)}
/>
),
},
{
key: "channel",
header: t("channels.cols.channel"),
@@ -217,5 +189,17 @@ export function buildChannelColumns(deps: ChannelColumnDeps): Column<ManagedChan
/>
),
},
{
key: "actions",
header: "",
align: "right",
// Room for the "…" at 44px on touch (`.touch-target`), not just its 28px under a mouse.
width: "3.25rem",
nowrap: true,
cardLabel: false,
// The row's full-history, hide and unsubscribe buttons went into the menu (E28 C1): the row
// keeps what is read or tuned all the time — priority, tags, the ✓ — and ends in its "…".
render: more,
},
];
}