import * as React from 'react' import { ChevronUp, ChevronDown, ChevronsUpDown } from 'lucide-react' import { cn } from '../../utils/cn' import { Checkbox } from './Checkbox' export interface Column { key: string header: string cell: (row: T) => React.ReactNode sortable?: boolean width?: string } export interface DataTableProps { data: T[] columns: Column[] rowKey: (row: T) => string selectable?: boolean selectedKeys?: Set onSelectionChange?: (keys: Set) => void onRowClick?: (row: T) => void emptyState?: React.ReactNode isLoading?: boolean stickyHeader?: boolean className?: string } /** * DataTable - Reusable data table component * * Features: * - Generic type for row data * - Sortable columns with chevron icons * - Row selection with Checkbox component * - Sticky header support * - Row hover states * - Selected row highlighting * - Empty state slot * - Responsive horizontal scroll */ export function DataTable({ data, columns, rowKey, selectable = false, selectedKeys = new Set(), onSelectionChange, onRowClick, emptyState, isLoading = false, stickyHeader = false, className, }: DataTableProps) { const [sortConfig, setSortConfig] = React.useState<{ key: string direction: 'asc' | 'desc' } | null>(null) const handleSort = (key: string) => { setSortConfig((prev) => { if (prev?.key === key) { if (prev.direction === 'asc') { return { key, direction: 'desc' } } // Reset sort if clicking third time return null } return { key, direction: 'asc' } }) } const handleSelectAll = () => { if (!onSelectionChange) return if (selectedKeys.size === data.length) { // All selected, deselect all onSelectionChange(new Set()) } else { // Select all onSelectionChange(new Set(data.map(rowKey))) } } const handleSelectRow = (key: string) => { if (!onSelectionChange) return const newKeys = new Set(selectedKeys) if (newKeys.has(key)) { newKeys.delete(key) } else { newKeys.add(key) } onSelectionChange(newKeys) } const allSelected = data.length > 0 && selectedKeys.size === data.length const someSelected = selectedKeys.size > 0 && selectedKeys.size < data.length const colSpan = columns.length + (selectable ? 1 : 0) return (
{selectable && ( )} {columns.map((col) => ( ))} {isLoading ? ( ) : data.length === 0 ? ( ) : ( data.map((row) => { const key = rowKey(row) const isSelected = selectedKeys.has(key) return ( onRowClick?.(row)} role={onRowClick ? 'button' : undefined} tabIndex={onRowClick ? 0 : undefined} onKeyDown={(e) => { if (onRowClick && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault() onRowClick(row) } }} > {selectable && ( )} {columns.map((col) => ( ))} ) }) )}
col.sortable && handleSort(col.key)} role={col.sortable ? 'button' : undefined} tabIndex={col.sortable ? 0 : undefined} onKeyDown={(e) => { if (col.sortable && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault() handleSort(col.key) } }} aria-sort={ sortConfig?.key === col.key ? sortConfig.direction === 'asc' ? 'ascending' : 'descending' : undefined } >
{col.header} {col.sortable && ( {sortConfig?.key === col.key ? ( sortConfig.direction === 'asc' ? ( ) : ( ) ) : ( )} )}
{emptyState || (
No data available
)}
e.stopPropagation()} > handleSelectRow(key)} aria-label={`Select row ${key}`} /> {col.cell(row)}
) }