feat(managers): richer channel cards + drop the rows layout, cap the channel column
UAT round 1 feedback: - Retire the `rows` layout — it barely differed from the table; keep Table + Cards. - The card now shows the About cell (+ its hover overlay) and the priority stepper in every layout, not just the table: ChannelLayoutGrid gains named slots (lead / about / actions) and stops hiding the `hideInCard` columns, since the roomy desktop card has space the cramped mobile fallback did not. - Fix the table losing its centering with "Rows per page: All": a long-titled channel stretched the uncapped Channel column (~289→429px) and overflowed the centered table. Cap it with a max-w element inside the cell (table-layout:auto ignores a cell max-width), the same trick AboutCell already uses. Gaps now 163/162.
This commit is contained in:
@@ -4,22 +4,24 @@ import { type Column } from "./DataTable";
|
||||
import VirtualGrid from "./VirtualGrid";
|
||||
import { CHANNEL_LAYOUT_SPEC } from "../lib/channelLayout";
|
||||
|
||||
// The channel manager's non-table layouts (E4 S3): the `cards` and `rows` densities, built by
|
||||
// reusing the SAME `Column<T>` render closures the DataTable draws — so the subscribed and discovery
|
||||
// tables (which have different columns) both get card/row views 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 or rows.
|
||||
// The channel manager's `cards` layout (E4 S3), built by reusing the SAME `Column<T>` 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`.
|
||||
//
|
||||
// Column flags drive the composition, exactly like DataTable's narrow-screen card fallback:
|
||||
// `cardPrimary` → the heading; `hideInCard` → omitted; `cardLabel:false` → value without its header.
|
||||
// `actionsKey` (optional) is docked to the right edge in `rows`, and shown last in `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.
|
||||
|
||||
interface ChannelLayoutGridProps<T> {
|
||||
rows: T[];
|
||||
columns: Column<T>[];
|
||||
rowKey: (row: T) => string;
|
||||
layout: "cards" | "rows";
|
||||
/** Column key whose cell is the action cluster — docked right in `rows`. */
|
||||
/** 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;
|
||||
rowClassName?: (row: T) => string;
|
||||
emptyText?: string;
|
||||
@@ -29,53 +31,43 @@ export default function ChannelLayoutGrid<T>({
|
||||
rows,
|
||||
columns,
|
||||
rowKey,
|
||||
layout,
|
||||
leadKey,
|
||||
aboutKey,
|
||||
actionsKey,
|
||||
rowClassName,
|
||||
emptyText,
|
||||
}: ChannelLayoutGridProps<T>) {
|
||||
const { t } = useTranslation();
|
||||
const spec = CHANNEL_LAYOUT_SPEC[layout];
|
||||
const spec = CHANNEL_LAYOUT_SPEC.cards;
|
||||
|
||||
const slotted = new Set([leadKey, aboutKey, actionsKey].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;
|
||||
// Everything shown in the meta flow: not the heading, not hidden, not the docked action cluster.
|
||||
const meta = columns.filter(
|
||||
(c) => !c.cardPrimary && !c.hideInCard && c.key !== actionsKey
|
||||
);
|
||||
|
||||
const metaFlow = (row: T) => (
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-1.5 text-sm min-w-0">
|
||||
{meta.map((col) => (
|
||||
<span key={col.key} className="inline-flex items-center gap-1">
|
||||
{col.cardLabel !== false && <span className="text-muted text-xs">{col.header}</span>}
|
||||
{col.render(row)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
// 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) => (
|
||||
<div className={`glass-card rounded-xl p-3 h-full flex flex-col ${rowClassName?.(row) ?? ""}`}>
|
||||
{primary.map((col) => (
|
||||
<div key={col.key} className="font-medium">
|
||||
{col.render(row)}
|
||||
<div className={`glass-card rounded-xl p-3 h-full flex flex-col gap-2 ${rowClassName?.(row) ?? ""}`}>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0 flex-1 font-medium">
|
||||
{primary.map((col) => (
|
||||
<Fragment key={col.key}>{col.render(row)}</Fragment>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
<div className="mt-2">{metaFlow(row)}</div>
|
||||
{actions && <div className="mt-auto pt-2">{actions.render(row)}</div>}
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderRow = (row: T) => (
|
||||
<div className={`glass-card rounded-xl p-3 flex items-center gap-4 ${rowClassName?.(row) ?? ""}`}>
|
||||
<div className="shrink-0 font-medium">
|
||||
{primary.map((col) => (
|
||||
<Fragment key={col.key}>{col.render(row)}</Fragment>
|
||||
{lead && <div className="shrink-0">{lead.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">
|
||||
{meta.map((col) => (
|
||||
<span key={col.key} className="inline-flex items-center gap-1">
|
||||
{col.cardLabel !== false && <span className="text-muted text-xs">{col.header}</span>}
|
||||
{col.render(row)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
{metaFlow(row)}
|
||||
{actions && <div className="ml-auto shrink-0">{actions.render(row)}</div>}
|
||||
{actions && <div className="mt-auto pt-1">{actions.render(row)}</div>}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -90,9 +82,9 @@ export default function ChannelLayoutGrid<T>({
|
||||
items={rows}
|
||||
getKey={rowKey}
|
||||
minCol={spec.minCol}
|
||||
maxCol={spec.maxCol}
|
||||
maxCol={null}
|
||||
rowEst={spec.rowEst}
|
||||
renderItem={(row) => (layout === "cards" ? renderCard(row) : renderRow(row))}
|
||||
renderItem={renderCard}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user