"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 (
)
}
/** 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,
]