import * as React from "react"; import { cn } from "@/lib/utils"; /** * Field descriptor for a {@link MobileCardRow}. * * The consuming page decides which fields to show and in what order; this * primitive does not pick them. Exactly one field should set `primary: true` — * it renders as the card title (bold, larger). The rest render as a key/value * stack below the title. */ export interface MobileCardField { key: string; label: string; render: (row: T) => React.ReactNode; /** When true, render as the card title (bold, larger). One per card. */ primary?: boolean; } export interface MobileCardRowProps { rows: T[]; fields: MobileCardField[]; /** Stable per-row identity; falls back to the row index when omitted. */ getRowId?: (row: T) => string; /** When set, the whole card becomes a button (44px min height). */ onRowClick?: (row: T) => void; /** Optional right-aligned action slot (edit/delete icon buttons). */ actions?: (row: T) => React.ReactNode; /** Optional className for the outer list container. */ className?: string; } /** * Stacked card list for wide tables below the `md:` breakpoint. * * Each row renders as a card: the `primary` field as the title and the * remaining fields as a key/value stack. When `onRowClick` is provided the * whole card is a button with a 44px minimum touch target (spec R6.1). An * optional `actions` slot renders right-aligned controls. * * This is the mobile counterpart to {@link DataTable}; pages branch on * `useIsMobile()`. See OpenSpec change `mobile-responsive-parity`, design * §`MobileCardRow`. */ export function MobileCardRow({ rows, fields, getRowId, onRowClick, actions, className, }: MobileCardRowProps) { const primary = fields.find((f) => f.primary); const rest = fields.filter((f) => !f.primary); return (
{rows.map((row, index) => { const rowKey = getRowId?.(row) ?? String(index); const body = (
{primary ? (
{primary.render(row)}
) : null} {rest.length > 0 ? (
{rest.map((field) => (
{field.label}
{field.render(row)}
))}
) : null}
{actions ? (
{actions(row)}
) : null}
); if (onRowClick) { return (
onRowClick(row)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onRowClick(row); } }} className="mobile-touch-target min-h-11 w-full cursor-pointer rounded-lg border border-border bg-card p-3 text-left transition-colors hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" > {body}
); } return (
{body}
); })}
); }