import type { ReactNode } from "react"; import "./table.css"; export interface Column { /** Unique column key. */ key: string; /** Header label. Omit for the status / actions rails. */ header?: ReactNode; /** Cell renderer. */ render: (row: T) => ReactNode; /** Extra class on the (e.g. dt__status, dt__actions). */ cellClass?: string; } interface TableProps { columns: Column[]; rows: T[]; rowKey: (row: T) => string; /** Optional per-row activation (opens detail). Bound to click, Enter, Space. */ onRowClick?: (row: T) => void; /** Accessible label for the row's primary activation, e.g. the hostname. */ rowLabel?: (row: T) => string; /** Cap the staggered fade-in so large lists don't crawl in. */ maxStaggerRows?: number; } /** * Dense, console-style data table. Sticky header, hover highlight, hover- * revealed row actions, and a staggered fade-in on mount (capped so big lists * appear promptly). Column-driven so callers compose cells declaratively. */ export function Table({ columns, rows, rowKey, onRowClick, rowLabel, maxStaggerRows = 14, }: TableProps) { return (
{columns.map((c) => ( ))} {rows.map((row, i) => { const delay = i < maxStaggerRows ? `${i * 22}ms` : "0ms"; return ( onRowClick(row) : undefined} tabIndex={onRowClick ? 0 : undefined} aria-label={ onRowClick && rowLabel ? `Open detail for ${rowLabel(row)}` : undefined } onKeyDown={ onRowClick ? (e) => { // Activate on Enter or Space, the standard for a // button-like row. Space must not scroll the page. if (e.key === "Enter" || e.key === " ") { if (e.target !== e.currentTarget) return; e.preventDefault(); onRowClick(row); } } : undefined } > {columns.map((c) => ( ))} ); })}
{c.header}
{c.render(row)}
); }