"use client" /** * ListPageBoardTemplate — reusable kanban shell for list pages (Team, custom hubs). * * - Columns are defined with predicates; each row is placed in the **first** matching column. * - Cards are rendered by the caller (`renderCard`) — compose **`ListPageBoardCard`** + primitives (`BoardCardTwoLineBlock`, etc.). * - Vertical scroll: same as table — toolbar + columns grow with `[data-page-scroll]` (metrics, tabs, toolbar, and board scroll together). * - Placements keeps richer column headers (search, menus); this template is for simpler hubs. * * @see `docs/data-views-pattern.md` — board primitives */ import * as React from "react" import { cn } from "../../lib/utils" import { Badge } from "../ui/badge" import { HorizontalScrollRegion } from "../ui/horizontal-scroll-region" import { BoardNewCardPlaceholder } from "./board-card-primitives" export type ListPageBoardColumnDef = { id: string label: string /** Shown beside the title on larger breakpoints */ description?: string /** First matching column wins; columns should be mutually exclusive for most domains. */ filter: (row: T) => boolean } export type ListPageBoardTemplateProps = { columns: ListPageBoardColumnDef[] rows: T[] getRowKey: (row: T) => string | number renderCard: (row: T) => React.ReactNode /** Tailwind classes for the count pill, keyed by column `id` */ columnCountBadgeClassName?: Record /** Copy when a column has no rows */ emptyColumnLabel?: string } function ListPageBoardColumnHeader({ label, description, count, badgeClassName, }: { label: string description?: string count: number badgeClassName?: string }) { return (
{label} {description ? ( {description} ) : null}
{count}
) } const EMPTY_COLUMN_BADGE_CLASS: Record = {} function BoardColumnCard({ row, renderCard, }: { row: T renderCard: (row: T) => React.ReactNode }) { return <>{renderCard(row)} } export function ListPageBoardTemplate({ columns, rows, getRowKey, renderCard, columnCountBadgeClassName = EMPTY_COLUMN_BADGE_CLASS, emptyColumnLabel = "No items", }: ListPageBoardTemplateProps) { const grouped = React.useMemo(() => { const map: Record = {} for (const col of columns) map[col.id] = [] for (const row of rows) { for (const col of columns) { if (col.filter(row)) { map[col.id].push(row) break } } } return map }, [columns, rows]) return (
{columns.map(col => (
{(grouped[col.id]?.length ?? 0) === 0 ? (

{emptyColumnLabel}

) : ( grouped[col.id]!.map(row => (
)) )}
))}
) }