import { splitProps, mergeProps, createSignal, For, JSX } from 'solid-js'; import clsx from 'clsx'; export interface TableColumn { key: string; header: string; sortable?: boolean; width?: string; render?: (value: any, row: T) => JSX.Element; } export interface TableProps { columns: TableColumn[]; data: T[]; onSort?: (key: string, direction: 'asc' | 'desc') => void; size?: 'sm' | 'md' | 'lg'; accent?: string; class?: string; } interface SortState { key: string; direction: 'asc' | 'desc'; } /** * Table -- data table with sortable columns, row hover, and minimal cyberpunk styling. * * Uses semantic elements for accessibility. * Minimal row-based layout: no vertical column lines, subtle row dividers. * Header row stands out with accent color and display font. * Sortable columns show angular sort indicators with aria-sort. */ export function Table>(rawProps: TableProps) { const merged = mergeProps({ size: 'md' as const }, rawProps); const [local] = splitProps(merged, [ 'columns', 'data', 'onSort', 'size', 'accent', 'class', ]); const [sortState, setSortState] = createSignal(null); // Handle sort column click const handleSort = (col: TableColumn) => { if (!col.sortable) return; const current = sortState(); let next: SortState | null; if (current?.key === col.key) { if (current.direction === 'asc') { next = { key: col.key, direction: 'desc' }; } else { // Clear sort next = null; } } else { next = { key: col.key, direction: 'asc' }; } setSortState(next); if (next && local.onSort) { local.onSort(next.key, next.direction); } }; // Get sorted data (internal sorting when no onSort callback) const sortedData = (): T[] => { const sort = sortState(); if (!sort || local.onSort) { // External sorting or no sort -- return data as-is return local.data; } // Internal sorting return [...local.data].sort((a, b) => { const aVal = a[sort.key]; const bVal = b[sort.key]; if (aVal == null && bVal == null) return 0; if (aVal == null) return sort.direction === 'asc' ? -1 : 1; if (bVal == null) return sort.direction === 'asc' ? 1 : -1; let cmp: number; if (typeof aVal === 'number' && typeof bVal === 'number') { cmp = aVal - bVal; } else { cmp = String(aVal).localeCompare(String(bVal)); } return sort.direction === 'asc' ? cmp : -cmp; }); }; // Get cell value const getCellValue = (row: T, col: TableColumn): JSX.Element => { const value = row[col.key]; if (col.render) { return col.render(value, row); } return <>{value != null ? String(value) : ''}; }; // Get aria-sort for a column const getAriaSort = (col: TableColumn): 'ascending' | 'descending' | undefined => { const sort = sortState(); if (!sort || sort.key !== col.key) return undefined; return sort.direction === 'asc' ? 'ascending' : 'descending'; }; const accentStyle = (): JSX.CSSProperties | undefined => { if (local.accent) { return { '--hh-comp-accent': local.accent } as JSX.CSSProperties; } return undefined; }; return (
{(col) => ( )} {(row) => ( {(col) => ( )} )}
handleSort(col)} > {col.header} {col.sortable && ( )}
{getCellValue(row, col)}
); }