"use client" /** * ListPageBoardCard — single board-card shell for all list hubs (Placements, Team, Compliance, …). * * Information hierarchy (top → bottom): * 1. **Title row** — `ListPageBoardCardTitleRow` — primary label (`text-sm font-semibold`), optional `trailing` (`ListPageBoardCardAvatar` initials chip, same as Placements). * 2. **Badge row** — `ListPageBoardCardBadgeRow` — optional status / tags (`flex-wrap`). * 3. **Body** — `ListPageBoardCardBody` — primary facts: icon rows, two-line blocks (`BoardCardIconRow` / `BoardCardTwoLineBlock` from board-card-primitives). * 4. **Secondary** — `ListPageBoardCardSecondary` — optional muted supporting line (`text-xs text-muted-foreground`). * * Styling matches **`BoardPlacementCard`**: `Card` `size="sm"`, ring, hover shadow, optional **new** ring accent. * * **Interaction:** Pass **`onClick`** (e.g. `BoardPlacementCard`) or **`interactive`** when the card is * wrapped in `` / router navigation so hover/cursor match placement board tiles. * * **Layouts:** * - **`stack`** (default) — kanban / grid tile: compose with `ListPageBoardCardHeader`, title row, body. * - **`row`** — full-width list row: optional **`leading`**, **`children`** as main column, optional **`rowEnd`**. * Use **`rowContainerClassName`** for responsive shells (e.g. `flex-col sm:flex-row`). */ import * as React from "react" import { AvatarInitials } from "../ui/avatar" import { cn } from "../../lib/utils" import { Card, CardContent, CardHeader, CardTitle } from "../ui/card" export type ListPageBoardCardLayout = "stack" | "row" export type ListPageBoardCardProps = { layout?: ListPageBoardCardLayout /** `row` only — avatar / icon column. */ leading?: React.ReactNode /** `row` only — status + chevron, etc. */ rowEnd?: React.ReactNode /** * `row` only — flex shell for the card (default `flex flex-row items-start gap-3`). * Use e.g. `flex flex-col gap-1 sm:flex-row sm:items-center sm:gap-4` for compliance-style rows. */ rowContainerClassName?: string /** Entire card is clickable (e.g. open detail) — enables pointer + hover shadow. */ onClick?: () => void /** * Same hover/cursor as `onClick` when the parent uses `` or `button` instead of Card click * (avoids nested interactive targets and keeps keyboard focus on the link). */ interactive?: boolean className?: string style?: React.CSSProperties /** Subtle brand ring — e.g. newly created row. */ isNew?: boolean children: React.ReactNode } export function ListPageBoardCard({ layout = "stack", leading, rowEnd, rowContainerClassName, onClick, interactive = false, className, style, isNew, children, }: ListPageBoardCardProps) { const isRow = layout === "row" const isInteractive = Boolean(onClick) || interactive const rowShell = rowContainerClassName ?? "flex flex-row items-start gap-3" const devWarnedRef = React.useRef(false) // Dev-only composition warning, raised after commit rather than during // render: the "already warned" flag is a ref write, and a render React // discards or replays must not be able to flip it. React.useEffect(() => { if (process.env.NODE_ENV === "production" || isRow || devWarnedRef.current) return const childArray = React.Children.toArray(children) const hasDsPrimitive = childArray.some( child => React.isValidElement(child) && LIST_PAGE_BOARD_CARD_VALID_PRIMITIVES.includes(child.type as React.ElementType), ) if (childArray.length === 0 || hasDsPrimitive) return devWarnedRef.current = true console.warn( '[Exxat DS][ListPageBoardCard] layout="stack" children are not composed with DS primitives. ' + "Wrap content in ///etc. " + "Raw children render without horizontal padding because Card only carries py-*. " + "See `apps/web/components/library-board-view.tsx` (canonical) and `.cursor/rules/exxat-board-cards.mdc`.", ) }) return ( {isRow ? ( <> {leading}
{children}
{rowEnd} ) : ( children )}
) } /** Preferred public name — same component as `ListPageBoardCard`. */ export const HubRecordCard = ListPageBoardCard export function ListPageBoardCardHeader({ className, children, }: { className?: string children: React.ReactNode }) { return {children} } /** * Level 1 — Primary title (dominant text on the card). */ export function ListPageBoardCardTitleRow({ title, titleClassName, trailing, }: { title: React.ReactNode /** Extra classes on the title (e.g. line-clamp from `lineClampClass`). */ titleClassName?: string /** End-aligned: avatar, icon, etc. */ trailing?: React.ReactNode }) { return (
{title}
{trailing}
) } /** Initials avatar — Radix `Avatar` + `--avatar-initials-*` tokens (same as tables / placement board). */ export function ListPageBoardCardAvatar({ initials, className, }: { initials: string className?: string }) { return ( ) } /** * Level 2 — Status / metadata chips (below title, above primary body). */ export function ListPageBoardCardBadgeRow({ children }: { children: React.ReactNode }) { return
{children}
} /** * Level 3 — Main facts: compose with `BoardCardIconRow`, `BoardCardTwoLineBlock`, etc. */ export function ListPageBoardCardBody({ className, children, }: { className?: string children: React.ReactNode }) { return ( {children} ) } /** * Level 4 — Optional muted supporting line (caption, hint, extra context). */ export function ListPageBoardCardSecondary({ className, children, }: { className?: string children: React.ReactNode }) { return (

{children}

) } const LIST_PAGE_BOARD_CARD_VALID_PRIMITIVES: ReadonlyArray = [ ListPageBoardCardHeader, ListPageBoardCardBody, ListPageBoardCardTitleRow, ListPageBoardCardBadgeRow, ListPageBoardCardSecondary, CardHeader, CardContent, ]