105 lines
2.6 KiB
TypeScript

import type { ReactNode } from "react";
import { EmptyBlock, ErrorBlock } from "./StateBlock";
export interface Column<T> {
key: string;
header: ReactNode;
render: (row: T) => ReactNode;
/** Right-align (numeric) cells. */
numeric?: boolean;
/** Extra class on the td/th. */
className?: string;
}
interface DataTableProps<T> {
columns: Column<T>[];
rows: T[];
rowKey: (row: T) => string;
loading?: boolean;
error?: unknown;
onRetry?: () => void;
emptyTitle?: string;
emptyHint?: string;
caption?: string;
/** Row click handler — renders rows as clickable. */
onRowClick?: (row: T) => void;
/** Number of skeleton rows while loading. */
skeletonRows?: number;
}
/**
* Data-dense table with unified loading / error / empty states.
* Renders inside a horizontal-scroll wrapper so narrow viewports never
* overflow the document.
*/
export function DataTable<T>({
columns,
rows,
rowKey,
loading = false,
error,
onRetry,
emptyTitle = "No rows",
emptyHint,
caption,
onRowClick,
skeletonRows = 6,
}: DataTableProps<T>) {
if (error !== undefined && error !== null) {
return <ErrorBlock error={error} onRetry={onRetry} />;
}
if (loading) {
return (
<div role="status" aria-label="Loading table rows">
{Array.from({ length: skeletonRows }, (_, i) => (
<div className="skeleton-row" key={i} />
))}
</div>
);
}
if (rows.length === 0) {
return <EmptyBlock title={emptyTitle} hint={emptyHint} />;
}
return (
<div className="table-wrap">
<table className="data-table">
{caption ? <caption className="sr-only">{caption}</caption> : null}
<thead>
<tr>
{columns.map((col) => (
<th
key={col.key}
scope="col"
className={`${col.numeric ? "num" : ""} ${col.className ?? ""}`.trim()}
>
{col.header}
</th>
))}
</tr>
</thead>
<tbody>
{rows.map((row) => (
<tr
key={rowKey(row)}
className={onRowClick ? "clickable" : undefined}
onClick={onRowClick ? () => onRowClick(row) : undefined}
>
{columns.map((col) => (
<td
key={col.key}
className={`${col.numeric ? "num" : ""} ${col.className ?? ""}`.trim()}
>
{col.render(row)}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
}