import type { ReactNode } from "react"; import Table from "@mui/material/Table"; import TableBody from "@mui/material/TableBody"; import TableCell from "@mui/material/TableCell"; import TableContainer from "@mui/material/TableContainer"; import TableHead from "@mui/material/TableHead"; import TableRow from "@mui/material/TableRow"; import TableSortLabel from "@mui/material/TableSortLabel"; export interface DataColumn { key: string; header: string; align?: "left" | "right"; /** Render the cell with the DM Mono numeric face. */ mono?: boolean; /** Emphasize the cell (font-weight 500). */ bold?: boolean; /** If set, the header becomes a clickable sort control (requires onSort). */ sortKey?: string; /** * Fixed column width (number = px, string = any CSS width incl. "%"). When * ANY column sets a width the table switches to `table-layout: fixed`, so * columns keep a stable size regardless of cell content (e.g. while * filtering). Overflowing text is clipped with an ellipsis. */ width?: number | string; render: (row: T) => ReactNode; } export interface DataTableProps { columns: DataColumn[]; rows: T[]; rowKey: (row: T) => string | number; rowInactive?: (row: T) => boolean; /** When set, each row is clickable and shows a pointer cursor. */ onRowClick?: (row: T) => void; /** When it returns true, the row is tinted with the error palette (danger highlight). */ rowDanger?: (row: T) => boolean; empty?: ReactNode; /** Server-sort wiring: active key (matches a column's sortKey), direction, handler. */ sortBy?: string; sortDir?: "asc" | "desc"; onSort?: (key: string) => void; } /** Dense, Soft-SaaS-styled data table. Styling only — never re-formats values. */ export default function DataTable({ columns, rows, rowKey, rowInactive, onRowClick, rowDanger, empty, sortBy, sortDir, onSort, }: DataTableProps) { if (import.meta.env.DEV && columns.some((c) => c.sortKey) && !onSort) { console.warn("DataTable: columns with sortKey require an onSort prop."); } if (rows.length === 0 && empty) return <>{empty}; // When any column declares a width, lock the table to fixed layout so column // widths come from the colgroup (stable) instead of from cell content (which // jumps as rows are filtered). Tables that set no widths keep auto layout. const hasWidths = columns.some((c) => c.width != null); const colWidth = (w: number | string | undefined) => w == null ? undefined : typeof w === "number" ? `${w}px` : w; return ( {hasWidths && ( {columns.map((c) => ( ))} )} {columns.map((c) => ( {c.sortKey && onSort ? ( onSort(c.sortKey!)} > {c.header} ) : ( c.header )} ))} {rows.map((row) => ( onRowClick(row) : undefined} sx={[ onRowClick ? { cursor: "pointer" } : {}, rowDanger?.(row) ? { bgcolor: "error.light", opacity: 0.88, "&:hover": { bgcolor: "error.light", filter: "brightness(0.97)", }, } : {}, rowInactive?.(row) ? { opacity: 0.55 } : {}, ]} > {columns.map((c) => ( {c.render(row)} ))} ))}
); }