import * as React from 'react' import { Table, Text, Center, Loader } from '@mantine/core' interface ColumnDef { key: string label: string /** Format string: ',.2f' | '$,.2f' | 'datetime' | ',' */ format?: string /** Alignment override. Numbers default to right, strings to left. */ align?: 'left' | 'right' | 'center' } type DataTableDensity = 'compact' | 'cozy' | 'roomy' interface DataTableProps { data: Record[] columns?: ColumnDef[] /** Column keys that should be colored by value intensity (heatmap). */ heatmapColumns?: string[] maxHeight?: number | string loading?: boolean error?: Error | null /** Row padding preset. compact=4/8, cozy=6/12 (default), roomy=10/16. */ density?: DataTableDensity } const DENSITY_MAP: Record = { compact: { py: 4, px: 'xs' }, cozy: { py: 6, px: 'sm' }, roomy: { py: 10, px: 'md' }, } function formatCell(value: unknown, format?: string): string { if (value == null) return '—' if (!format) return String(value) if (format === 'datetime' && !isNaN(Date.parse(String(value)))) { return new Date(String(value)).toLocaleString() } const num = Number(value) if (!isNaN(num)) { if (format.includes('f')) { const match = format.match(/\.(\d+)f/) const d = match ? parseInt(match[1]!) : 0 let str = num.toFixed(d) if (format.includes(',')) { str = Number(str).toLocaleString('en-US', { minimumFractionDigits: d, maximumFractionDigits: d }) } if (format.startsWith('$')) str = '$' + str return str } if (format === ',') return num.toLocaleString() } return String(value) } function DataTableComponent({ data, columns, heatmapColumns = [], maxHeight = 500, loading = false, error = null, density = 'cozy', }: DataTableProps) { const pad = DENSITY_MAP[density] // Auto-detect columns from first row if not provided const effectiveColumns: ColumnDef[] = (columns && columns.length > 0) ? columns : (data && data.length > 0) ? Object.keys(data[0]!).map(k => ({ key: k, label: k })) : [] // Compute heatmap ranges per column const heatmapRanges = React.useMemo(() => { const ranges: Record = {} if (heatmapColumns.length > 0 && data && data.length > 0) { for (const key of heatmapColumns) { const values = data.map(r => Number(r[key])).filter(n => !isNaN(n)) if (values.length > 0) { ranges[key] = { min: Math.min(...values), max: Math.max(...values) } } } } return ranges }, [data, heatmapColumns]) function heatmapStyle(key: string, value: unknown): React.CSSProperties | undefined { const range = heatmapRanges[key] if (!range || range.max === range.min) return undefined const num = Number(value) if (isNaN(num)) return undefined const t = (num - range.min) / (range.max - range.min) const alpha = 0.1 + t * 0.55 return { backgroundColor: `rgba(59, 130, 246, ${alpha})` } } if (loading && (!data || data.length === 0)) { return (
) } if (error) { return (
{error.message}
) } return ( {effectiveColumns.map(col => ( {col.label} ))} {(data ?? []).map((row, i) => ( {effectiveColumns.map(col => { const align = col.align ?? (typeof row[col.key] === 'number' ? 'right' : 'left') return ( {formatCell(row[col.key], col.format)} ) })} ))}
{(!data || data.length === 0) && (
No data
)}
) } export const DataTable = DataTableComponent export type { DataTableProps, ColumnDef, DataTableDensity }