'use client'; import React, { forwardRef, useMemo, useState } from 'react'; import { type ColumnDef, type SortingState, type ColumnFiltersState, type PaginationState, type RowSelectionState, type VisibilityState, type OnChangeFn, type Updater, type Row, type Table as TanStackTable, type ColumnHelper, flexRender, getCoreRowModel, getSortedRowModel, getFilteredRowModel, getPaginationRowModel, useReactTable, createColumnHelper, } from '@tanstack/react-table'; // Re-export the TanStack types/helpers consumers need to type their column // definitions without taking a direct dependency on `@tanstack/react-table`. export type { ColumnDef, Row, ColumnHelper, SortingState, ColumnFiltersState, PaginationState, RowSelectionState, VisibilityState, }; export type PixelDataTableInstance = TanStackTable; export { createColumnHelper }; import { Surface, cn, surfaceClasses, useEffectiveSurface, focusRing, } from '../common'; export type PixelDataTableDensity = 'compact' | 'normal' | 'comfortable'; export interface PixelDataTableProps { data: TData[]; columns: ColumnDef[]; sorting?: { id: string; desc: boolean }[]; onSortingChange?: (next: { id: string; desc: boolean }[]) => void; filtering?: Record; onFilteringChange?: (next: Record) => void; pagination?: { pageIndex: number; pageSize: number }; onPaginationChange?: (next: { pageIndex: number; pageSize: number }) => void; rowSelection?: Record; onRowSelectionChange?: (next: Record) => void; columnVisibility?: Record; onColumnVisibilityChange?: (next: Record) => void; getRowId?: (row: TData, idx: number) => string; density?: PixelDataTableDensity; stickyHeader?: boolean; loading?: boolean; emptyState?: React.ReactNode; onRowClick?: (row: TData) => void; surface?: Surface; className?: string; /** Render with surface-aware border + radius chrome. Defaults to true — data table needs visible chrome. */ bordered?: boolean; } const cellPad: Record = { compact: 'px-3 py-1', normal: 'px-4 py-2.5', comfortable: 'px-4 py-4', }; function resolveUpdater(updater: Updater, prev: T): T { return typeof updater === 'function' ? (updater as (p: T) => T)(prev) : updater; } function ChevronUp({ className }: { className?: string }) { return ( ); } function ChevronDown({ className }: { className?: string }) { return ( ); } function PixelDataTableInner( { data, columns, sorting, onSortingChange, filtering, onFilteringChange, pagination, onPaginationChange, rowSelection, onRowSelectionChange, columnVisibility, onColumnVisibilityChange, getRowId, density = 'normal', stickyHeader = false, loading = false, emptyState, onRowClick, surface: surfaceProp, className, bordered = true, }: PixelDataTableProps, ref: React.Ref, ) { const surface = useEffectiveSurface(surfaceProp); const s = surfaceClasses(surface); // Merge a selection column when row selection is enabled. const hasRowSelection = rowSelection !== undefined; const mergedColumns = useMemo[]>(() => { if (!hasRowSelection) return columns; const selectionCol: ColumnDef = { id: '__select__', enableSorting: false, header: ({ table }) => ( { if (el) el.indeterminate = table.getIsSomePageRowsSelected() && !table.getIsAllPageRowsSelected(); }} onChange={table.getToggleAllPageRowsSelectedHandler()} className={cn('h-4 w-4', focusRing)} /> ), cell: ({ row }) => ( ), }; return [selectionCol, ...columns]; }, [columns, hasRowSelection]); // Internal state fallbacks when consumer omits the matching on*Change. // Mirrors useControllableState pattern: controlled prop overrides internal. const [internalSorting, setInternalSorting] = useState([]); const [internalFiltering, setInternalFiltering] = useState([]); const [internalPagination, setInternalPagination] = useState( pagination ?? { pageIndex: 0, pageSize: 10 }, ); const [internalRowSelection, setInternalRowSelection] = useState({}); const [internalVisibility, setInternalVisibility] = useState({}); // Adapter callbacks: TanStack uses Updater; consumers want plain values. const sortingState: SortingState = (sorting as SortingState | undefined) ?? internalSorting; const filteringState: ColumnFiltersState = useMemo(() => { if (!filtering) return internalFiltering; return Object.entries(filtering).map(([id, value]) => ({ id, value })); }, [filtering, internalFiltering]); const paginationState: PaginationState = pagination ?? internalPagination; const rowSelectionState: RowSelectionState = rowSelection ?? internalRowSelection; const visibilityState: VisibilityState = columnVisibility ?? internalVisibility; const handleSorting: OnChangeFn = (updater) => { const next = resolveUpdater(updater, sortingState); if (sorting === undefined) setInternalSorting(next); onSortingChange?.(next.map((sv) => ({ id: sv.id, desc: sv.desc }))); }; const handleFilters: OnChangeFn = (updater) => { const next = resolveUpdater(updater, filteringState); if (filtering === undefined) setInternalFiltering(next); const dict: Record = {}; for (const f of next) dict[f.id] = String(f.value ?? ''); onFilteringChange?.(dict); }; const handlePagination: OnChangeFn = (updater) => { const next = resolveUpdater(updater, paginationState); if (pagination === undefined) setInternalPagination(next); onPaginationChange?.({ pageIndex: next.pageIndex, pageSize: next.pageSize }); }; const handleRowSelection: OnChangeFn = (updater) => { const next = resolveUpdater(updater, rowSelectionState); if (rowSelection === undefined) setInternalRowSelection(next); onRowSelectionChange?.(next); }; const handleVisibility: OnChangeFn = (updater) => { const next = resolveUpdater(updater, visibilityState); if (columnVisibility === undefined) setInternalVisibility(next); onColumnVisibilityChange?.(next); }; // Enable pagination when EITHER controlled prop OR internal pagination should be used. // Default: pagination on if data > pageSize (matches user expectation of an interactive // table). To stay backwards-compatible we only enable when consumer opts in OR provides data. const paginationEnabled = pagination !== undefined; const table = useReactTable({ data, columns: mergedColumns, state: { sorting: sortingState, columnFilters: filteringState, pagination: paginationEnabled ? paginationState : undefined, rowSelection: rowSelectionState, columnVisibility: visibilityState, }, getRowId: getRowId ? (row, idx) => getRowId(row, idx) : undefined, enableRowSelection: hasRowSelection, onSortingChange: handleSorting, onColumnFiltersChange: handleFilters, onPaginationChange: handlePagination, onRowSelectionChange: handleRowSelection, onColumnVisibilityChange: handleVisibility, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), getFilteredRowModel: getFilteredRowModel(), getPaginationRowModel: paginationEnabled ? getPaginationRowModel() : undefined, manualPagination: false, }); const headerGroups = table.getHeaderGroups(); const rows = table.getRowModel().rows; const visibleColCount = mergedColumns.length; const skeletonRowCount = paginationState.pageSize > 0 ? Math.min(paginationState.pageSize, 5) : 5; return (
{headerGroups.map((hg) => ( {hg.headers.map((header) => { const canSort = header.column.getCanSort(); const sortDir = header.column.getIsSorted(); const label = typeof header.column.columnDef.header === 'string' ? header.column.columnDef.header : header.column.id; return ( ); })} ))} {loading && ( )} {loading ? ( Array.from({ length: skeletonRowCount }).map((_, i) => ( {Array.from({ length: visibleColCount }).map((_, j) => ( ))} )) ) : rows.length === 0 ? ( ) : ( rows.map((row, idx) => { const isSelected = row.getIsSelected(); return ( onRowClick(row.original) : undefined} className={cn( 'border-b border-retro-border/20 transition-colors', onRowClick && 'cursor-pointer', idx % 2 === 1 && 'bg-retro-surface/15', 'hover:bg-retro-surface/30', isSelected && 'bg-retro-surface/40', )} > {row.getVisibleCells().map((cell) => ( ))} ); }) )}
{header.isPlaceholder ? null : canSort ? ( ) : ( flexRender(header.column.columnDef.header, header.getContext()) )}
Loading data…
{emptyState ?? No data.}
{flexRender(cell.column.columnDef.cell, cell.getContext())}
{paginationEnabled && ( table.previousPage()} onNext={() => table.nextPage()} onPageSizeChange={(size) => table.setPageSize(size)} /> )}
); } interface PaginationBarProps { surface: Surface; pageIndex: number; pageSize: number; totalRows: number; pageCount: number; canPrev: boolean; canNext: boolean; onPrev: () => void; onNext: () => void; onPageSizeChange: (size: number) => void; } function PixelDataTablePagination({ surface, pageIndex, pageSize, totalRows, pageCount, canPrev, canNext, onPrev, onNext, onPageSizeChange, }: PaginationBarProps) { const s = surfaceClasses(surface); const start = totalRows === 0 ? 0 : pageIndex * pageSize + 1; const end = Math.min(totalRows, (pageIndex + 1) * pageSize); const sizeOptions = [5, 10, 20, 50]; return (
Showing {start}-{end} of {totalRows}
Page {pageCount === 0 ? 0 : pageIndex + 1} of {pageCount}
); } /** * `PixelDataTable` — TanStack-powered surface-aware data table with sorting, * filtering, pagination, row selection, column visibility, density, sticky * header, loading skeletons, and empty state. All state is controllable. * * Polymorphic-ref note: rendered root is a `
` wrapping ``; ref * targets the wrapper div for measurement / scroll control. */ export const PixelDataTable = forwardRef(PixelDataTableInner) as ( props: PixelDataTableProps & { ref?: React.Ref }, ) => ReturnType; (PixelDataTable as unknown as { displayName: string }).displayName = 'PixelDataTable';