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:
@@ -28,6 +28,9 @@ interface ChannelLayoutGridProps<T> {
|
||||
aboutKey?: string;
|
||||
/** Column key rendered as the action cluster, pinned to the card's bottom edge. */
|
||||
actionsKey?: string;
|
||||
/** Column key docked at the top-right corner after the lead: the card's "…" (E28 C1), which
|
||||
* belongs beside the name it acts on, not under the card's last line. */
|
||||
moreKey?: string;
|
||||
rowClassName?: (row: T) => string;
|
||||
/** Extra DOM props per card — same escape hatch, same single consumer, as `DataTable.rowProps`, so
|
||||
* a channel answers the right-click in the card layout exactly as it does in the table. */
|
||||
@@ -42,6 +45,7 @@ export default function ChannelLayoutGrid<T>({
|
||||
leadKey,
|
||||
aboutKey,
|
||||
actionsKey,
|
||||
moreKey,
|
||||
rowClassName,
|
||||
rowProps,
|
||||
emptyText,
|
||||
@@ -49,11 +53,12 @@ export default function ChannelLayoutGrid<T>({
|
||||
const { t } = useTranslation();
|
||||
const spec = CHANNEL_LAYOUT_SPEC.cards;
|
||||
|
||||
const slotted = new Set([leadKey, aboutKey, actionsKey].filter(Boolean) as string[]);
|
||||
const slotted = new Set([leadKey, aboutKey, actionsKey, moreKey].filter(Boolean) as string[]);
|
||||
const primary = columns.filter((c) => c.cardPrimary);
|
||||
const lead = leadKey ? columns.find((c) => c.key === leadKey) : undefined;
|
||||
const about = aboutKey ? columns.find((c) => c.key === aboutKey) : undefined;
|
||||
const actions = actionsKey ? columns.find((c) => c.key === actionsKey) : undefined;
|
||||
const more = moreKey ? columns.find((c) => c.key === moreKey) : undefined;
|
||||
// Everything else flows in the meta line: not the heading, not a slotted column.
|
||||
const meta = columns.filter((c) => !c.cardPrimary && !slotted.has(c.key));
|
||||
|
||||
@@ -70,6 +75,7 @@ export default function ChannelLayoutGrid<T>({
|
||||
))}
|
||||
</div>
|
||||
{lead && <div className="shrink-0">{lead.render(row)}</div>}
|
||||
{more && <div className="shrink-0 -my-1">{more.render(row)}</div>}
|
||||
</div>
|
||||
{about && <div className="min-w-0 text-sm">{about.render(row)}</div>}
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-1.5 text-sm min-w-0">
|
||||
|
||||
Reference in New Issue
Block a user