{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "data-table", "title": "Data Table", "description": "Feature-rich TanStack data table with sorting, text and faceted filters, pagination, row selection, column visibility, pinning, and empty/action states.", "dependencies": [ "@tanstack/react-table" ], "registryDependencies": [ "https://zeron-ui.vercel.app/r/surfaces.json", "utils", "https://zeron-ui.vercel.app/r/shape-context.json", "https://zeron-ui.vercel.app/r/icon-context.json", "https://zeron-ui.vercel.app/r/badge.json", "https://zeron-ui.vercel.app/r/button.json", "https://zeron-ui.vercel.app/r/checkbox.json", "https://zeron-ui.vercel.app/r/checkbox-group.json", "https://zeron-ui.vercel.app/r/dropdown.json", "https://zeron-ui.vercel.app/r/input.json", "https://zeron-ui.vercel.app/r/popover.json", "https://zeron-ui.vercel.app/r/select.json", "https://zeron-ui.vercel.app/r/table.json" ], "files": [ { "path": "src/components/ui/data-table.tsx", "content": "\"use client\";\n\nimport {\n type Column,\n type ColumnDef,\n type ColumnFiltersState,\n type ColumnPinningState,\n flexRender,\n getCoreRowModel,\n getFacetedRowModel,\n getFacetedUniqueValues,\n getFilteredRowModel,\n getPaginationRowModel,\n getSortedRowModel,\n type PaginationState,\n type RowSelectionState,\n type SortingState,\n type Table as TanstackTable,\n type TableOptions,\n useReactTable,\n type VisibilityState,\n} from \"@tanstack/react-table\";\nimport * as React from \"react\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n CheckboxGroup,\n CheckboxItem,\n} from \"@/components/ui/checkbox-group\";\nimport {\n DropdownContent,\n DropdownLabel,\n DropdownMenu,\n DropdownSeparator,\n DropdownTrigger,\n} from \"@/components/ui/dropdown\";\nimport { MenuItem } from \"@/components/ui/menu-item\";\nimport { Input } from \"@/components/ui/input\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n} from \"@/components/ui/select\";\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from \"@/components/ui/table\";\nimport { cn } from \"@/lib/utils\";\nimport { useIcon, type IconComponent } from \"@/lib/icon-context\";\nimport { useShape } from \"@/lib/shape-context\";\n\nexport type DataTableColumnMeta = {\n label?: string;\n options?: DataTableFilterOption[];\n placeholder?: string;\n unit?: string;\n variant?: \"text\" | \"number\" | \"select\" | \"multiSelect\";\n};\n\nexport type DataTableFilterOption = {\n icon?: IconComponent;\n label: string;\n value: string;\n};\n\ndeclare module \"@tanstack/react-table\" {\n // TanStack exposes this empty interface specifically for declaration\n // merging; the type parameters must match its upstream declaration.\n // eslint-disable-next-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-unused-vars\n interface ColumnMeta extends DataTableColumnMeta {}\n}\n\nexport type UseDataTableProps = Omit<\n TableOptions,\n | \"getCoreRowModel\"\n | \"getFacetedRowModel\"\n | \"getFacetedUniqueValues\"\n | \"getFilteredRowModel\"\n | \"getPaginationRowModel\"\n | \"getSortedRowModel\"\n | \"onColumnFiltersChange\"\n | \"onColumnPinningChange\"\n | \"onColumnVisibilityChange\"\n | \"onPaginationChange\"\n | \"onRowSelectionChange\"\n | \"onSortingChange\"\n | \"state\"\n> & {\n columns: ColumnDef[];\n initialState?: TableOptions[\"initialState\"];\n};\n\nfunction useDataTable({\n initialState,\n ...options\n}: UseDataTableProps) {\n const [columnFilters, setColumnFilters] = React.useState(\n initialState?.columnFilters ?? []\n );\n const [columnPinning, setColumnPinning] = React.useState(\n initialState?.columnPinning ?? {}\n );\n const [columnVisibility, setColumnVisibility] =\n React.useState(initialState?.columnVisibility ?? {});\n const [pagination, setPagination] = React.useState({\n pageIndex: initialState?.pagination?.pageIndex ?? 0,\n pageSize: initialState?.pagination?.pageSize ?? 10,\n });\n const [rowSelection, setRowSelection] = React.useState(\n initialState?.rowSelection ?? {}\n );\n const [sorting, setSorting] = React.useState(\n initialState?.sorting ?? []\n );\n\n const table = useReactTable({\n ...options,\n getCoreRowModel: getCoreRowModel(),\n getFacetedRowModel: getFacetedRowModel(),\n getFacetedUniqueValues: getFacetedUniqueValues(),\n getFilteredRowModel: getFilteredRowModel(),\n getPaginationRowModel: getPaginationRowModel(),\n getSortedRowModel: getSortedRowModel(),\n initialState,\n onColumnFiltersChange: setColumnFilters,\n onColumnPinningChange: setColumnPinning,\n onColumnVisibilityChange: setColumnVisibility,\n onPaginationChange: setPagination,\n onRowSelectionChange: setRowSelection,\n onSortingChange: setSorting,\n state: {\n columnFilters,\n columnPinning,\n columnVisibility,\n pagination,\n rowSelection,\n sorting,\n },\n });\n\n return { table };\n}\n\nexport type DataTableProps = React.ComponentProps<\"div\"> & {\n actionBar?: React.ReactNode;\n emptyMessage?: React.ReactNode;\n table: TanstackTable;\n};\n\nfunction DataTable({\n actionBar,\n children,\n className,\n emptyMessage = \"No results.\",\n table,\n ...props\n}: DataTableProps) {\n const shape = useShape();\n\n return (\n \n {children}\n \n
\n \n \n {table.getHeaderGroups().map((headerGroup) => (\n \n {headerGroup.headers.map((header) => (\n \n {header.isPlaceholder\n ? null\n : flexRender(\n header.column.columnDef.header,\n header.getContext()\n )}\n \n ))}\n \n ))}\n \n \n {table.getRowModel().rows.length ? (\n table.getRowModel().rows.map((row, rowIndex) => (\n \n {row.getVisibleCells().map((cell) => (\n \n {flexRender(cell.column.columnDef.cell, cell.getContext())}\n \n ))}\n \n ))\n ) : (\n \n \n {emptyMessage}\n \n \n )}\n \n
\n
\n \n
\n \n {actionBar &&\n table.getFilteredSelectedRowModel().rows.length > 0 &&\n actionBar}\n
\n \n );\n}\n\nexport type DataTableColumnHeaderProps =\n React.ButtonHTMLAttributes & {\n column: Column;\n label: string;\n };\n\nfunction DataTableColumnHeader({\n className,\n column,\n label,\n ...props\n}: DataTableColumnHeaderProps) {\n const [open, setOpen] = React.useState(false);\n const ChevronUp = useIcon(\"chevron-up\");\n const ChevronDown = useIcon(\"chevron-down\");\n const ChevronsUpDown = useIcon(\"chevrons-up-down\");\n const EyeOff = useIcon(\"eye-off\");\n const X = useIcon(\"x\");\n const sorted = column.getIsSorted();\n const canSort = column.getCanSort();\n const canHide = column.getCanHide();\n const SortIcon = !canSort\n ? ChevronDown\n : sorted === \"asc\"\n ? ChevronUp\n : sorted === \"desc\"\n ? ChevronDown\n : ChevronsUpDown;\n\n if (!canSort && !canHide) {\n return
{label}
;\n }\n\n return (\n \n \n {label}\n \n }\n />\n \n {label}\n {canSort && (\n <>\n column.toggleSorting(false)}\n />\n column.toggleSorting(true)}\n />\n {sorted && (\n column.clearSorting()}\n />\n )}\n \n )}\n {canSort && canHide && }\n {canHide && (\n column.toggleVisibility(false)}\n />\n )}\n \n \n );\n}\n\nexport type DataTableToolbarProps = React.ComponentProps<\"div\"> & {\n table: TanstackTable;\n};\n\nfunction DataTableToolbar({\n children,\n className,\n table,\n ...props\n}: DataTableToolbarProps) {\n const CircleX = useIcon(\"circle-x\");\n const isFiltered = table.getState().columnFilters.length > 0;\n const columns = React.useMemo(\n () =>\n table\n .getAllColumns()\n .filter(\n (column) => column.getCanFilter() && column.columnDef.meta?.variant\n ),\n [table]\n );\n\n return (\n \n
\n {columns.map((column) => (\n \n ))}\n {isFiltered && (\n table.resetColumnFilters()}\n size=\"md\"\n variant=\"tertiary\"\n >\n Reset\n \n )}\n
\n
\n {children}\n \n
\n \n );\n}\n\ntype DataTableToolbarFilterProps = {\n column: Column;\n};\n\nfunction DataTableToolbarFilter({\n column,\n}: DataTableToolbarFilterProps) {\n const meta = column.columnDef.meta;\n const variant = meta?.variant;\n\n if (!variant) return null;\n\n if (variant === \"select\" || variant === \"multiSelect\") {\n return (\n \n );\n }\n\n if (variant === \"number\") {\n return (\n
\n column.setFilterValue(event.target.value)}\n placeholder={meta?.placeholder ?? meta?.label ?? column.id}\n size=\"default\"\n type=\"number\"\n value={(column.getFilterValue() as string) ?? \"\"}\n />\n {meta?.unit && (\n \n {meta.unit}\n \n )}\n
\n );\n }\n\n return (\n column.setFilterValue(event.target.value)}\n placeholder={meta?.placeholder ?? meta?.label ?? column.id}\n size=\"default\"\n value={(column.getFilterValue() as string) ?? \"\"}\n />\n );\n}\n\nexport type DataTableFacetedFilterProps = {\n column: Column;\n multiple?: boolean;\n options: DataTableFilterOption[];\n title: string;\n};\n\nfunction DataTableFacetedFilter({\n column,\n multiple = false,\n options,\n title,\n}: DataTableFacetedFilterProps) {\n const [open, setOpen] = React.useState(false);\n const CirclePlus = useIcon(\"circle-plus\");\n const CircleX = useIcon(\"circle-x\");\n const columnFilterValue = column.getFilterValue();\n const selectedValues = new Set(\n Array.isArray(columnFilterValue) ? columnFilterValue : []\n );\n const checkedIndices = new Set(\n options.flatMap((option, index) =>\n selectedValues.has(option.value) ? [index] : []\n )\n );\n const selectedOptions = options.filter((option) =>\n selectedValues.has(option.value)\n );\n\n const toggleOption = (option: DataTableFilterOption) => {\n const isSelected = selectedValues.has(option.value);\n if (multiple) {\n const nextValues = new Set(selectedValues);\n if (isSelected) nextValues.delete(option.value);\n else nextValues.add(option.value);\n const filterValue = Array.from(nextValues);\n column.setFilterValue(filterValue.length ? filterValue : undefined);\n } else {\n column.setFilterValue(isSelected ? undefined : [option.value]);\n setOpen(false);\n }\n };\n\n return (\n \n 0\n ? `${title}: ${selectedOptions.map((option) => option.label).join(\", \")}`\n : title\n }\n className=\"shrink-0 border-dashed\"\n leadingIcon={selectedValues.size > 0 ? CircleX : CirclePlus}\n size=\"md\"\n variant=\"tertiary\"\n >\n {selectedValues.size === 0 ? (\n title\n ) : (\n <>\n {selectedOptions.slice(0, 2).map((option) => (\n \n {option.label}\n \n ))}\n {selectedOptions.length > 2 && (\n \n +{selectedOptions.length - 2}\n \n )}\n \n )}\n \n }\n />\n \n
\n {title}\n
\n
\n \n {options.map((option, index) => (\n toggleOption(option)}\n trailing={column.getFacetedUniqueValues().get(option.value) ?? 0}\n />\n ))}\n \n {selectedValues.size > 0 && (\n {\n column.setFilterValue(undefined);\n setOpen(false);\n }}\n size=\"md\"\n variant=\"ghost\"\n >\n Clear filters\n \n )}\n \n \n );\n}\n\nexport type DataTablePaginationProps = React.ComponentProps<\"div\"> & {\n pageSizeOptions?: number[];\n table: TanstackTable;\n};\n\nfunction DataTablePagination({\n className,\n pageSizeOptions = [10, 20, 30, 40, 50],\n table,\n ...props\n}: DataTablePaginationProps) {\n const ChevronLeft = useIcon(\"chevron-left\");\n const ChevronRight = useIcon(\"chevron-right\");\n const ChevronsLeft = useIcon(\"chevrons-left\");\n const ChevronsRight = useIcon(\"chevrons-right\");\n\n return (\n \n
\n {table.getFilteredSelectedRowModel().rows.length} of{\" \"}\n {table.getFilteredRowModel().rows.length} row(s) selected.\n
\n
\n
\n Rows per page\n table.setPageSize(Number(value))}\n value={`${table.getState().pagination.pageSize}`}\n >\n \n \n {pageSizeOptions.map((pageSize) => (\n \n {pageSize}\n \n ))}\n \n \n
\n
\n Page {table.getState().pagination.pageIndex + 1} of{\" \"}\n {Math.max(table.getPageCount(), 1)}\n
\n
\n table.setPageIndex(0)}\n size=\"icon-sm\"\n variant=\"tertiary\"\n >\n \n \n table.previousPage()}\n size=\"icon-sm\"\n variant=\"tertiary\"\n >\n \n \n table.nextPage()}\n size=\"icon-sm\"\n variant=\"tertiary\"\n >\n \n \n table.setPageIndex(Math.max(table.getPageCount() - 1, 0))}\n size=\"icon-sm\"\n variant=\"tertiary\"\n >\n \n \n
\n
\n
\n );\n}\n\nexport type DataTableViewOptionsProps = {\n className?: string;\n table: TanstackTable;\n};\n\nfunction DataTableViewOptions({\n className,\n table,\n}: DataTableViewOptionsProps) {\n const [open, setOpen] = React.useState(false);\n const Settings = useIcon(\"settings\");\n const columns = React.useMemo(\n () =>\n table\n .getAllColumns()\n .filter(\n (column) =>\n typeof column.accessorFn !== \"undefined\" && column.getCanHide()\n ),\n [table]\n );\n\n if (!columns.length) return null;\n\n const checkedIndices = new Set(\n columns.flatMap((column, index) => (column.getIsVisible() ? [index] : []))\n );\n\n return (\n \n \n View\n \n }\n />\n \n
\n Toggle columns\n
\n
\n \n {columns.map((column, index) => (\n column.toggleVisibility(!column.getIsVisible())}\n />\n ))}\n \n \n \n );\n}\n\nfunction getCommonPinningStyles(\n column: Column,\n {\n backgroundColor = \"var(--surface-floating)\",\n backgroundImage,\n }: {\n backgroundColor?: string;\n backgroundImage?: string;\n } = {}\n): React.CSSProperties {\n const isPinned = column.getIsPinned();\n const isLastLeftPinnedColumn =\n isPinned === \"left\" && column.getIsLastColumn(\"left\");\n const isFirstRightPinnedColumn =\n isPinned === \"right\" && column.getIsFirstColumn(\"right\");\n\n return {\n backgroundColor: isPinned ? backgroundColor : undefined,\n backgroundImage: isPinned ? backgroundImage : undefined,\n boxShadow: isLastLeftPinnedColumn\n ? \"-4px 0 4px -4px var(--border) inset\"\n : isFirstRightPinnedColumn\n ? \"4px 0 4px -4px var(--border) inset\"\n : undefined,\n left: isPinned === \"left\" ? `${column.getStart(\"left\")}px` : undefined,\n position: isPinned ? \"sticky\" : \"relative\",\n right: isPinned === \"right\" ? `${column.getAfter(\"right\")}px` : undefined,\n width: column.getSize(),\n zIndex: isPinned ? \"var(--layer-control)\" : undefined,\n };\n}\n\nexport {\n DataTable,\n DataTableColumnHeader,\n DataTableFacetedFilter,\n DataTablePagination,\n DataTableToolbar,\n DataTableViewOptions,\n useDataTable,\n};\n", "type": "registry:ui", "target": "components/ui/data-table.tsx" } ], "type": "registry:ui" }