import { Fragment } from "react"; import { useTranslation } from "react-i18next"; import { type Column } from "./ui/DataTable"; import VirtualGrid from "./ui/VirtualGrid"; import { CHANNEL_LAYOUT_SPEC } from "../lib/channelLayout"; import { navItemAttrs } from "../lib/listNav"; import { SURFACE } from "./ui/surfaces"; // The channel manager's `cards` layout (E4 S3), built by reusing the SAME `Column` render closures // the DataTable draws — so the subscribed and discovery tables (which have different columns) both get // the card view for free, and a cell's behaviour (priority bump, tag toggle, sync, actions) is defined // once. The `table` layout keeps DataTable; this renders only when the manager's layout is `cards`. // // Named slots place the columns that need bespoke positions; everything else flows in the meta line. // Unlike DataTable's cramped narrow-screen card fallback, the desktop card is roomy, so it shows the // `hideInCard` columns too (About, priority) — the user wants those affordances in every layout. // // Each card is a keyboard item (lib/listNav): `j`/`k` and the arrows walk the cards, and Enter opens // the channel — the name ChannelLink renders is the card's open element. interface ChannelLayoutGridProps { rows: T[]; columns: Column[]; rowKey: (row: T) => string; /** Column key docked at the top-right of the card (the priority stepper). */ leadKey?: string; /** Column key rendered as a full-width description line (the About cell + its hover overlay). */ 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. */ rowProps?: (row: T) => Record; emptyText?: string; } export default function ChannelLayoutGrid({ rows, columns, rowKey, leadKey, aboutKey, actionsKey, moreKey, rowClassName, rowProps, emptyText, }: ChannelLayoutGridProps) { const { t } = useTranslation(); const spec = CHANNEL_LAYOUT_SPEC.cards; 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)); const renderCard = (row: T, index: number) => (
{primary.map((col) => ( {col.render(row)} ))}
{lead &&
{lead.render(row)}
} {more &&
{more.render(row)}
}
{about &&
{about.render(row)}
}
{meta.map((col) => ( {col.cardLabel !== false && {col.header}} {col.render(row)} ))}
{actions &&
{actions.render(row)}
}
); if (rows.length === 0) { return (
{emptyText ?? t("datatable.empty")}
); } return ( renderCard(row, index)} /> ); }