Last active
April 27, 2025 14:46
-
-
Save AntonioErdeljac/ee40539ec03bce178b6301d4436fc198 to your computer and use it in GitHub Desktop.
dice-ui-registry
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"$schema": "https://ui.shadcn.com/schema/registry-item.json", | |
"name": "data-table", | |
"type": "registry:component", | |
"title": "Data Table", | |
"description": "A feature-rich data table component with server-side filtering, sorting, and pagination", | |
"dependencies": [ | |
"@tanstack/react-table", | |
"lucide-react", | |
"nuqs" | |
], | |
"registryDependencies": [ | |
"badge", | |
"button", | |
"calendar", | |
"command", | |
"dropdown-menu", | |
"input", | |
"popover", | |
"select", | |
"separator", | |
"slider", | |
"table" | |
], | |
"files": [ | |
{ | |
"path": "src/components/data-table.tsx", | |
"content": "import { type Table as TanstackTable, flexRender } from \"@tanstack/react-table\";\nimport type * as React from \"react\";\n\nimport { DataTablePagination } from \"@/components/data-table-pagination\";\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from \"@/components/ui/table\";\nimport { getCommonPinningStyles } from \"@/lib/data-table\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DataTableProps\u003CTData\u003E extends React.ComponentProps\u003C\"div\"\u003E {\n table: TanstackTable\u003CTData\u003E;\n actionBar?: React.ReactNode;\n}\n\nexport function DataTable\u003CTData\u003E({\n table,\n actionBar,\n children,\n className,\n ...props\n}: DataTableProps\u003CTData\u003E) {\n return (\n \u003Cdiv\n className={cn(\"flex w-full flex-col gap-2.5 overflow-auto\", className)}\n {...props}\n \u003E\n {children}\n \u003Cdiv className=\"overflow-hidden rounded-md border\"\u003E\n \u003CTable\u003E\n \u003CTableHeader\u003E\n {table.getHeaderGroups().map((headerGroup) =\u003E (\n \u003CTableRow key={headerGroup.id}\u003E\n {headerGroup.headers.map((header) =\u003E (\n \u003CTableHead\n key={header.id}\n colSpan={header.colSpan}\n style={{\n ...getCommonPinningStyles({ column: header.column }),\n }}\n \u003E\n {header.isPlaceholder\n ? null\n : flexRender(\n header.column.columnDef.header,\n header.getContext(),\n )}\n \u003C/TableHead\u003E\n ))}\n \u003C/TableRow\u003E\n ))}\n \u003C/TableHeader\u003E\n \u003CTableBody\u003E\n {table.getRowModel().rows?.length ? (\n table.getRowModel().rows.map((row) =\u003E (\n \u003CTableRow\n key={row.id}\n data-state={row.getIsSelected() && \"selected\"}\n \u003E\n {row.getVisibleCells().map((cell) =\u003E (\n \u003CTableCell\n key={cell.id}\n style={{\n ...getCommonPinningStyles({ column: cell.column }),\n }}\n \u003E\n {flexRender(\n cell.column.columnDef.cell,\n cell.getContext(),\n )}\n \u003C/TableCell\u003E\n ))}\n \u003C/TableRow\u003E\n ))\n ) : (\n \u003CTableRow\u003E\n \u003CTableCell\n colSpan={table.getAllColumns().length}\n className=\"h-24 text-center\"\n \u003E\n No results.\n \u003C/TableCell\u003E\n \u003C/TableRow\u003E\n )}\n \u003C/TableBody\u003E\n \u003C/Table\u003E\n \u003C/div\u003E\n \u003Cdiv className=\"flex flex-col gap-2.5\"\u003E\n \u003CDataTablePagination table={table} /\u003E\n {actionBar &&\n table.getFilteredSelectedRowModel().rows.length \u003E 0 &&\n actionBar}\n \u003C/div\u003E\n \u003C/div\u003E\n );\n}\n", | |
"type": "registry:component" | |
}, | |
{ | |
"path": "src/components/data-table-column-header.tsx", | |
"content": "\"use client\";\n\nimport type { Column } from \"@tanstack/react-table\";\nimport {\n ChevronDown,\n ChevronUp,\n ChevronsUpDown,\n EyeOff,\n X,\n} from \"lucide-react\";\n\nimport {\n DropdownMenu,\n DropdownMenuCheckboxItem,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DataTableColumnHeaderProps\u003CTData, TValue\u003E\n extends React.ComponentProps\u003Ctypeof DropdownMenuTrigger\u003E {\n column: Column\u003CTData, TValue\u003E;\n title: string;\n}\n\nexport function DataTableColumnHeader\u003CTData, TValue\u003E({\n column,\n title,\n className,\n ...props\n}: DataTableColumnHeaderProps\u003CTData, TValue\u003E) {\n if (!column.getCanSort() && !column.getCanHide()) {\n return \u003Cdiv className={cn(className)}\u003E{title}\u003C/div\u003E;\n }\n\n return (\n \u003CDropdownMenu\u003E\n \u003CDropdownMenuTrigger\n className={cn(\n \"-ml-1.5 flex h-8 items-center gap-1.5 rounded-md px-2 py-1.5 hover:bg-accent focus:outline-none focus:ring-1 focus:ring-ring data-[state=open]:bg-accent [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-muted-foreground\",\n className,\n )}\n {...props}\n \u003E\n {title}\n {column.getCanSort() &&\n (column.getIsSorted() === \"desc\" ? (\n \u003CChevronDown /\u003E\n ) : column.getIsSorted() === \"asc\" ? (\n \u003CChevronUp /\u003E\n ) : (\n \u003CChevronsUpDown /\u003E\n ))}\n \u003C/DropdownMenuTrigger\u003E\n \u003CDropdownMenuContent align=\"start\" className=\"w-28\"\u003E\n {column.getCanSort() && (\n \u003C\u003E\n \u003CDropdownMenuCheckboxItem\n className=\"relative pr-8 pl-2 [&\u003Espan:first-child]:right-2 [&\u003Espan:first-child]:left-auto [&_svg]:text-muted-foreground\"\n checked={column.getIsSorted() === \"asc\"}\n onClick={() =\u003E column.toggleSorting(false)}\n \u003E\n \u003CChevronUp /\u003E\n Asc\n \u003C/DropdownMenuCheckboxItem\u003E\n \u003CDropdownMenuCheckboxItem\n className=\"relative pr-8 pl-2 [&\u003Espan:first-child]:right-2 [&\u003Espan:first-child]:left-auto [&_svg]:text-muted-foreground\"\n checked={column.getIsSorted() === \"desc\"}\n onClick={() =\u003E column.toggleSorting(true)}\n \u003E\n \u003CChevronDown /\u003E\n Desc\n \u003C/DropdownMenuCheckboxItem\u003E\n {column.getIsSorted() && (\n \u003CDropdownMenuItem\n className=\"pl-2 [&_svg]:text-muted-foreground\"\n onClick={() =\u003E column.clearSorting()}\n \u003E\n \u003CX /\u003E\n Reset\n \u003C/DropdownMenuItem\u003E\n )}\n \u003C/\u003E\n )}\n {column.getCanHide() && (\n \u003CDropdownMenuCheckboxItem\n className=\"relative pr-8 pl-2 [&\u003Espan:first-child]:right-2 [&\u003Espan:first-child]:left-auto [&_svg]:text-muted-foreground\"\n checked={!column.getIsVisible()}\n onClick={() =\u003E column.toggleVisibility(false)}\n \u003E\n \u003CEyeOff /\u003E\n Hide\n \u003C/DropdownMenuCheckboxItem\u003E\n )}\n \u003C/DropdownMenuContent\u003E\n \u003C/DropdownMenu\u003E\n );\n}\n", | |
"type": "registry:component" | |
}, | |
{ | |
"path": "src/components/data-table-pagination.tsx", | |
"content": "import type { Table } from \"@tanstack/react-table\";\nimport {\n ChevronLeft,\n ChevronRight,\n ChevronsLeft,\n ChevronsRight,\n} from \"lucide-react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DataTablePaginationProps\u003CTData\u003E extends React.ComponentProps\u003C\"div\"\u003E {\n table: Table\u003CTData\u003E;\n pageSizeOptions?: number[];\n}\n\nexport function DataTablePagination\u003CTData\u003E({\n table,\n pageSizeOptions = [10, 20, 30, 40, 50],\n className,\n ...props\n}: DataTablePaginationProps\u003CTData\u003E) {\n return (\n \u003Cdiv\n className={cn(\n \"flex w-full flex-col-reverse items-center justify-between gap-4 overflow-auto p-1 sm:flex-row sm:gap-8\",\n className,\n )}\n {...props}\n \u003E\n \u003Cdiv className=\"flex-1 whitespace-nowrap text-muted-foreground text-sm\"\u003E\n {table.getFilteredSelectedRowModel().rows.length} of{\" \"}\n {table.getFilteredRowModel().rows.length} row(s) selected.\n \u003C/div\u003E\n \u003Cdiv className=\"flex flex-col-reverse items-center gap-4 sm:flex-row sm:gap-6 lg:gap-8\"\u003E\n \u003Cdiv className=\"flex items-center space-x-2\"\u003E\n \u003Cp className=\"whitespace-nowrap font-medium text-sm\"\u003ERows per page\u003C/p\u003E\n \u003CSelect\n value={`${table.getState().pagination.pageSize}`}\n onValueChange={(value) =\u003E {\n table.setPageSize(Number(value));\n }}\n \u003E\n \u003CSelectTrigger className=\"h-8 w-[4.5rem] [&[data-size]]:h-8\"\u003E\n \u003CSelectValue placeholder={table.getState().pagination.pageSize} /\u003E\n \u003C/SelectTrigger\u003E\n \u003CSelectContent side=\"top\"\u003E\n {pageSizeOptions.map((pageSize) =\u003E (\n \u003CSelectItem key={pageSize} value={`${pageSize}`}\u003E\n {pageSize}\n \u003C/SelectItem\u003E\n ))}\n \u003C/SelectContent\u003E\n \u003C/Select\u003E\n \u003C/div\u003E\n \u003Cdiv className=\"flex items-center justify-center font-medium text-sm\"\u003E\n Page {table.getState().pagination.pageIndex + 1} of{\" \"}\n {table.getPageCount()}\n \u003C/div\u003E\n \u003Cdiv className=\"flex items-center space-x-2\"\u003E\n \u003CButton\n aria-label=\"Go to first page\"\n variant=\"outline\"\n size=\"icon\"\n className=\"hidden size-8 lg:flex\"\n onClick={() =\u003E table.setPageIndex(0)}\n disabled={!table.getCanPreviousPage()}\n \u003E\n \u003CChevronsLeft /\u003E\n \u003C/Button\u003E\n \u003CButton\n aria-label=\"Go to previous page\"\n variant=\"outline\"\n size=\"icon\"\n className=\"size-8\"\n onClick={() =\u003E table.previousPage()}\n disabled={!table.getCanPreviousPage()}\n \u003E\n \u003CChevronLeft /\u003E\n \u003C/Button\u003E\n \u003CButton\n aria-label=\"Go to next page\"\n variant=\"outline\"\n size=\"icon\"\n className=\"size-8\"\n onClick={() =\u003E table.nextPage()}\n disabled={!table.getCanNextPage()}\n \u003E\n \u003CChevronRight /\u003E\n \u003C/Button\u003E\n \u003CButton\n aria-label=\"Go to last page\"\n variant=\"outline\"\n size=\"icon\"\n className=\"hidden size-8 lg:flex\"\n onClick={() =\u003E table.setPageIndex(table.getPageCount() - 1)}\n disabled={!table.getCanNextPage()}\n \u003E\n \u003CChevronsRight /\u003E\n \u003C/Button\u003E\n \u003C/div\u003E\n \u003C/div\u003E\n \u003C/div\u003E\n );\n}\n", | |
"type": "registry:component" | |
}, | |
{ | |
"path": "src/components/data-table-view-options.tsx", | |
"content": "\"use client\";\n\nimport type { Table } from \"@tanstack/react-table\";\nimport { Check, ChevronsUpDown, Settings2 } from \"lucide-react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n} from \"@/components/ui/command\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { cn } from \"@/lib/utils\";\nimport * as React from \"react\";\n\ninterface DataTableViewOptionsProps\u003CTData\u003E {\n table: Table\u003CTData\u003E;\n}\n\nexport function DataTableViewOptions\u003CTData\u003E({\n table,\n}: DataTableViewOptionsProps\u003CTData\u003E) {\n const columns = React.useMemo(\n () =\u003E\n table\n .getAllColumns()\n .filter(\n (column) =\u003E\n typeof column.accessorFn !== \"undefined\" && column.getCanHide(),\n ),\n [table],\n );\n\n return (\n \u003CPopover\u003E\n \u003CPopoverTrigger asChild\u003E\n \u003CButton\n aria-label=\"Toggle columns\"\n role=\"combobox\"\n variant=\"outline\"\n size=\"sm\"\n className=\"ml-auto hidden h-8 lg:flex\"\n \u003E\n \u003CSettings2 /\u003E\n View\n \u003CChevronsUpDown className=\"ml-auto opacity-50\" /\u003E\n \u003C/Button\u003E\n \u003C/PopoverTrigger\u003E\n \u003CPopoverContent align=\"end\" className=\"w-44 p-0\"\u003E\n \u003CCommand\u003E\n \u003CCommandInput placeholder=\"Search columns...\" /\u003E\n \u003CCommandList\u003E\n \u003CCommandEmpty\u003ENo columns found.\u003C/CommandEmpty\u003E\n \u003CCommandGroup\u003E\n {columns.map((column) =\u003E (\n \u003CCommandItem\n key={column.id}\n onSelect={() =\u003E\n column.toggleVisibility(!column.getIsVisible())\n }\n \u003E\n \u003Cspan className=\"truncate\"\u003E\n {column.columnDef.meta?.label ?? column.id}\n \u003C/span\u003E\n \u003CCheck\n className={cn(\n \"ml-auto size-4 shrink-0\",\n column.getIsVisible() ? \"opacity-100\" : \"opacity-0\",\n )}\n /\u003E\n \u003C/CommandItem\u003E\n ))}\n \u003C/CommandGroup\u003E\n \u003C/CommandList\u003E\n \u003C/Command\u003E\n \u003C/PopoverContent\u003E\n \u003C/Popover\u003E\n );\n}\n", | |
"type": "registry:component" | |
}, | |
{ | |
"path": "src/components/data-table-faceted-filter.tsx", | |
"content": "\"use client\";\n\nimport type { Option } from \"@/types/data-table\";\nimport type { Column } from \"@tanstack/react-table\";\nimport { Check, PlusCircle, XCircle } from \"lucide-react\";\n\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n CommandSeparator,\n} from \"@/components/ui/command\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { cn } from \"@/lib/utils\";\nimport * as React from \"react\";\n\ninterface DataTableFacetedFilterProps\u003CTData, TValue\u003E {\n column?: Column\u003CTData, TValue\u003E;\n title?: string;\n options: Option[];\n multiple?: boolean;\n}\n\nexport function DataTableFacetedFilter\u003CTData, TValue\u003E({\n column,\n title,\n options,\n multiple,\n}: DataTableFacetedFilterProps\u003CTData, TValue\u003E) {\n const [open, setOpen] = React.useState(false);\n\n const columnFilterValue = column?.getFilterValue();\n const selectedValues = new Set(\n Array.isArray(columnFilterValue) ? columnFilterValue : [],\n );\n\n const onItemSelect = React.useCallback(\n (option: Option, isSelected: boolean) =\u003E {\n if (!column) return;\n\n if (multiple) {\n const newSelectedValues = new Set(selectedValues);\n if (isSelected) {\n newSelectedValues.delete(option.value);\n } else {\n newSelectedValues.add(option.value);\n }\n const filterValues = Array.from(newSelectedValues);\n column.setFilterValue(filterValues.length ? filterValues : undefined);\n } else {\n column.setFilterValue(isSelected ? undefined : [option.value]);\n setOpen(false);\n }\n },\n [column, multiple, selectedValues],\n );\n\n const onReset = React.useCallback(\n (event?: React.MouseEvent) =\u003E {\n event?.stopPropagation();\n column?.setFilterValue(undefined);\n },\n [column],\n );\n\n return (\n \u003CPopover open={open} onOpenChange={setOpen}\u003E\n \u003CPopoverTrigger asChild\u003E\n \u003CButton variant=\"outline\" size=\"sm\" className=\"border-dashed\"\u003E\n {selectedValues?.size \u003E 0 ? (\n \u003Cdiv\n role=\"button\"\n aria-label={`Clear ${title} filter`}\n tabIndex={0}\n onClick={onReset}\n className=\"rounded-sm opacity-70 transition-opacity hover:opacity-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring\"\n \u003E\n \u003CXCircle /\u003E\n \u003C/div\u003E\n ) : (\n \u003CPlusCircle /\u003E\n )}\n {title}\n {selectedValues?.size \u003E 0 && (\n \u003C\u003E\n \u003CSeparator\n orientation=\"vertical\"\n className=\"mx-0.5 data-[orientation=vertical]:h-4\"\n /\u003E\n \u003CBadge\n variant=\"secondary\"\n className=\"rounded-sm px-1 font-normal lg:hidden\"\n \u003E\n {selectedValues.size}\n \u003C/Badge\u003E\n \u003Cdiv className=\"hidden items-center gap-1 lg:flex\"\u003E\n {selectedValues.size \u003E 2 ? (\n \u003CBadge\n variant=\"secondary\"\n className=\"rounded-sm px-1 font-normal\"\n \u003E\n {selectedValues.size} selected\n \u003C/Badge\u003E\n ) : (\n options\n .filter((option) =\u003E selectedValues.has(option.value))\n .map((option) =\u003E (\n \u003CBadge\n variant=\"secondary\"\n key={option.value}\n className=\"rounded-sm px-1 font-normal\"\n \u003E\n {option.label}\n \u003C/Badge\u003E\n ))\n )}\n \u003C/div\u003E\n \u003C/\u003E\n )}\n \u003C/Button\u003E\n \u003C/PopoverTrigger\u003E\n \u003CPopoverContent className=\"w-[12.5rem] p-0\" align=\"start\"\u003E\n \u003CCommand\u003E\n \u003CCommandInput placeholder={title} /\u003E\n \u003CCommandList className=\"max-h-full\"\u003E\n \u003CCommandEmpty\u003ENo results found.\u003C/CommandEmpty\u003E\n \u003CCommandGroup className=\"max-h-[18.75rem] overflow-y-auto overflow-x-hidden\"\u003E\n {options.map((option) =\u003E {\n const isSelected = selectedValues.has(option.value);\n\n return (\n \u003CCommandItem\n key={option.value}\n onSelect={() =\u003E onItemSelect(option, isSelected)}\n \u003E\n \u003Cdiv\n className={cn(\n \"flex size-4 items-center justify-center rounded-sm border border-primary\",\n isSelected\n ? \"bg-primary\"\n : \"opacity-50 [&_svg]:invisible\",\n )}\n \u003E\n \u003CCheck /\u003E\n \u003C/div\u003E\n {option.icon && \u003Coption.icon /\u003E}\n \u003Cspan className=\"truncate\"\u003E{option.label}\u003C/span\u003E\n {option.count && (\n \u003Cspan className=\"ml-auto font-mono text-xs\"\u003E\n {option.count}\n \u003C/span\u003E\n )}\n \u003C/CommandItem\u003E\n );\n })}\n \u003C/CommandGroup\u003E\n {selectedValues.size \u003E 0 && (\n \u003C\u003E\n \u003CCommandSeparator /\u003E\n \u003CCommandGroup\u003E\n \u003CCommandItem\n onSelect={() =\u003E onReset()}\n className=\"justify-center text-center\"\n \u003E\n Clear filters\n \u003C/CommandItem\u003E\n \u003C/CommandGroup\u003E\n \u003C/\u003E\n )}\n \u003C/CommandList\u003E\n \u003C/Command\u003E\n \u003C/PopoverContent\u003E\n \u003C/Popover\u003E\n );\n}\n", | |
"type": "registry:component" | |
}, | |
{ | |
"path": "src/components/data-table-toolbar.tsx", | |
"content": "\"use client\";\n\nimport type { Column, Table } from \"@tanstack/react-table\";\nimport { X } from \"lucide-react\";\nimport * as React from \"react\";\n\nimport { DataTableDateFilter } from \"@/components/data-table-date-filter\";\nimport { DataTableFacetedFilter } from \"@/components/data-table-faceted-filter\";\nimport { DataTableSliderFilter } from \"@/components/data-table-slider-filter\";\nimport { DataTableViewOptions } from \"@/components/data-table-view-options\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DataTableToolbarProps\u003CTData\u003E extends React.ComponentProps\u003C\"div\"\u003E {\n table: Table\u003CTData\u003E;\n}\n\nexport function DataTableToolbar\u003CTData\u003E({\n table,\n children,\n className,\n ...props\n}: DataTableToolbarProps\u003CTData\u003E) {\n const isFiltered = table.getState().columnFilters.length \u003E 0;\n\n const columns = React.useMemo(\n () =\u003E table.getAllColumns().filter((column) =\u003E column.getCanFilter()),\n [table],\n );\n\n const onReset = React.useCallback(() =\u003E {\n table.resetColumnFilters();\n }, [table]);\n\n return (\n \u003Cdiv\n role=\"toolbar\"\n aria-orientation=\"horizontal\"\n className={cn(\n \"flex w-full items-start justify-between gap-2 p-1\",\n className,\n )}\n {...props}\n \u003E\n \u003Cdiv className=\"flex flex-1 flex-wrap items-center gap-2\"\u003E\n {columns.map((column) =\u003E (\n \u003CDataTableToolbarFilter key={column.id} column={column} /\u003E\n ))}\n {isFiltered && (\n \u003CButton\n aria-label=\"Reset filters\"\n variant=\"outline\"\n size=\"sm\"\n className=\"border-dashed\"\n onClick={onReset}\n \u003E\n \u003CX /\u003E\n Reset\n \u003C/Button\u003E\n )}\n \u003C/div\u003E\n \u003Cdiv className=\"flex items-center gap-2\"\u003E\n {children}\n \u003CDataTableViewOptions table={table} /\u003E\n \u003C/div\u003E\n \u003C/div\u003E\n );\n}\ninterface DataTableToolbarFilterProps\u003CTData\u003E {\n column: Column\u003CTData\u003E;\n}\n\nfunction DataTableToolbarFilter\u003CTData\u003E({\n column,\n}: DataTableToolbarFilterProps\u003CTData\u003E) {\n {\n const columnMeta = column.columnDef.meta;\n\n const onFilterRender = React.useCallback(() =\u003E {\n if (!columnMeta?.variant) return null;\n\n switch (columnMeta.variant) {\n case \"text\":\n return (\n \u003CInput\n placeholder={columnMeta.placeholder ?? columnMeta.label}\n value={(column.getFilterValue() as string) ?? \"\"}\n onChange={(event) =\u003E column.setFilterValue(event.target.value)}\n className=\"h-8 w-40 lg:w-56\"\n /\u003E\n );\n\n case \"number\":\n return (\n \u003Cdiv className=\"relative\"\u003E\n \u003CInput\n type=\"number\"\n inputMode=\"numeric\"\n placeholder={columnMeta.placeholder ?? columnMeta.label}\n value={(column.getFilterValue() as string) ?? \"\"}\n onChange={(event) =\u003E column.setFilterValue(event.target.value)}\n className={cn(\"h-8 w-[120px]\", columnMeta.unit && \"pr-8\")}\n /\u003E\n {columnMeta.unit && (\n \u003Cspan className=\"absolute top-0 right-0 bottom-0 flex items-center rounded-r-md bg-accent px-2 text-muted-foreground text-sm\"\u003E\n {columnMeta.unit}\n \u003C/span\u003E\n )}\n \u003C/div\u003E\n );\n\n case \"range\":\n return (\n \u003CDataTableSliderFilter\n column={column}\n title={columnMeta.label ?? column.id}\n /\u003E\n );\n\n case \"date\":\n case \"dateRange\":\n return (\n \u003CDataTableDateFilter\n column={column}\n title={columnMeta.label ?? column.id}\n multiple={columnMeta.variant === \"dateRange\"}\n /\u003E\n );\n\n case \"select\":\n case \"multiSelect\":\n return (\n \u003CDataTableFacetedFilter\n column={column}\n title={columnMeta.label ?? column.id}\n options={columnMeta.options ?? []}\n multiple={columnMeta.variant === \"multiSelect\"}\n /\u003E\n );\n\n default:\n return null;\n }\n }, [column, columnMeta]);\n\n return onFilterRender();\n }\n}\n", | |
"type": "registry:component" | |
}, | |
{ | |
"path": "src/components/data-table-slider-filter.tsx", | |
"content": "\"use client\";\n\nimport type { Column } from \"@tanstack/react-table\";\nimport * as React from \"react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { Slider } from \"@/components/ui/slider\";\nimport { cn } from \"@/lib/utils\";\nimport { PlusCircle, XCircle } from \"lucide-react\";\n\ninterface Range {\n min: number;\n max: number;\n}\n\ntype RangeValue = [number, number];\n\nfunction getIsValidRange(value: unknown): value is RangeValue {\n return (\n Array.isArray(value) &&\n value.length === 2 &&\n typeof value[0] === \"number\" &&\n typeof value[1] === \"number\"\n );\n}\n\ninterface DataTableSliderFilterProps\u003CTData\u003E {\n column: Column\u003CTData, unknown\u003E;\n title?: string;\n}\n\nexport function DataTableSliderFilter\u003CTData\u003E({\n column,\n title,\n}: DataTableSliderFilterProps\u003CTData\u003E) {\n const id = React.useId();\n\n const columnFilterValue = getIsValidRange(column.getFilterValue())\n ? (column.getFilterValue() as RangeValue)\n : undefined;\n\n const defaultRange = column.columnDef.meta?.range;\n const unit = column.columnDef.meta?.unit;\n\n const { min, max, step } = React.useMemo\u003CRange & { step: number }\u003E(() =\u003E {\n let minValue = 0;\n let maxValue = 100;\n\n if (defaultRange && getIsValidRange(defaultRange)) {\n [minValue, maxValue] = defaultRange;\n } else {\n const values = column.getFacetedMinMaxValues();\n if (values && Array.isArray(values) && values.length === 2) {\n const [facetMinValue, facetMaxValue] = values;\n if (\n typeof facetMinValue === \"number\" &&\n typeof facetMaxValue === \"number\"\n ) {\n minValue = facetMinValue;\n maxValue = facetMaxValue;\n }\n }\n }\n\n const rangeSize = maxValue - minValue;\n const step =\n rangeSize \u003C= 20\n ? 1\n : rangeSize \u003C= 100\n ? Math.ceil(rangeSize / 20)\n : Math.ceil(rangeSize / 50);\n\n return { min: minValue, max: maxValue, step };\n }, [column, defaultRange]);\n\n const range = React.useMemo((): RangeValue =\u003E {\n return columnFilterValue ?? [min, max];\n }, [columnFilterValue, min, max]);\n\n const formatValue = React.useCallback((value: number) =\u003E {\n return value.toLocaleString(undefined, { maximumFractionDigits: 0 });\n }, []);\n\n const onFromInputChange = React.useCallback(\n (event: React.ChangeEvent\u003CHTMLInputElement\u003E) =\u003E {\n const numValue = Number(event.target.value);\n if (!Number.isNaN(numValue) && numValue \u003E= min && numValue \u003C= range[1]) {\n column.setFilterValue([numValue, range[1]]);\n }\n },\n [column, min, range],\n );\n\n const onToInputChange = React.useCallback(\n (event: React.ChangeEvent\u003CHTMLInputElement\u003E) =\u003E {\n const numValue = Number(event.target.value);\n if (!Number.isNaN(numValue) && numValue \u003C= max && numValue \u003E= range[0]) {\n column.setFilterValue([range[0], numValue]);\n }\n },\n [column, max, range],\n );\n\n const onSliderValueChange = React.useCallback(\n (value: RangeValue) =\u003E {\n if (Array.isArray(value) && value.length === 2) {\n column.setFilterValue(value);\n }\n },\n [column],\n );\n\n const onReset = React.useCallback(\n (event: React.MouseEvent) =\u003E {\n if (event.target instanceof HTMLDivElement) {\n event.stopPropagation();\n }\n column.setFilterValue(undefined);\n },\n [column],\n );\n\n return (\n \u003CPopover\u003E\n \u003CPopoverTrigger asChild\u003E\n \u003CButton variant=\"outline\" size=\"sm\" className=\"border-dashed\"\u003E\n {columnFilterValue ? (\n \u003Cdiv\n role=\"button\"\n aria-label={`Clear ${title} filter`}\n tabIndex={0}\n className=\"rounded-sm opacity-70 transition-opacity hover:opacity-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring\"\n onClick={onReset}\n \u003E\n \u003CXCircle /\u003E\n \u003C/div\u003E\n ) : (\n \u003CPlusCircle /\u003E\n )}\n \u003Cspan\u003E{title}\u003C/span\u003E\n {columnFilterValue ? (\n \u003C\u003E\n \u003CSeparator\n orientation=\"vertical\"\n className=\"mx-0.5 data-[orientation=vertical]:h-4\"\n /\u003E\n {formatValue(columnFilterValue[0])} -{\" \"}\n {formatValue(columnFilterValue[1])}\n {unit ? ` ${unit}` : \"\"}\n \u003C/\u003E\n ) : null}\n \u003C/Button\u003E\n \u003C/PopoverTrigger\u003E\n \u003CPopoverContent align=\"start\" className=\"flex w-auto flex-col gap-4\"\u003E\n \u003Cdiv className=\"flex flex-col gap-3\"\u003E\n \u003Cp className=\"font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\"\u003E\n {title}\n \u003C/p\u003E\n \u003Cdiv className=\"flex items-center gap-4\"\u003E\n \u003CLabel htmlFor={`${id}-from`} className=\"sr-only\"\u003E\n From\n \u003C/Label\u003E\n \u003Cdiv className=\"relative\"\u003E\n \u003CInput\n id={`${id}-from`}\n type=\"number\"\n aria-valuemin={min}\n aria-valuemax={max}\n inputMode=\"numeric\"\n pattern=\"[0-9]*\"\n placeholder={min.toString()}\n min={min}\n max={max}\n value={range[0]?.toString()}\n onChange={onFromInputChange}\n className={cn(\"h-8 w-24\", unit && \"pr-8\")}\n /\u003E\n {unit && (\n \u003Cspan className=\"absolute top-0 right-0 bottom-0 flex items-center rounded-r-md bg-accent px-2 text-muted-foreground text-sm\"\u003E\n {unit}\n \u003C/span\u003E\n )}\n \u003C/div\u003E\n \u003CLabel htmlFor={`${id}-to`} className=\"sr-only\"\u003E\n to\n \u003C/Label\u003E\n \u003Cdiv className=\"relative\"\u003E\n \u003CInput\n id={`${id}-to`}\n type=\"number\"\n aria-valuemin={min}\n aria-valuemax={max}\n inputMode=\"numeric\"\n pattern=\"[0-9]*\"\n placeholder={max.toString()}\n min={min}\n max={max}\n value={range[1]?.toString()}\n onChange={onToInputChange}\n className={cn(\"h-8 w-24\", unit && \"pr-8\")}\n /\u003E\n {unit && (\n \u003Cspan className=\"absolute top-0 right-0 bottom-0 flex items-center rounded-r-md bg-accent px-2 text-muted-foreground text-sm\"\u003E\n {unit}\n \u003C/span\u003E\n )}\n \u003C/div\u003E\n \u003C/div\u003E\n \u003CLabel htmlFor={`${id}-slider`} className=\"sr-only\"\u003E\n {title} slider\n \u003C/Label\u003E\n \u003CSlider\n id={`${id}-slider`}\n min={min}\n max={max}\n step={step}\n value={range}\n onValueChange={onSliderValueChange}\n /\u003E\n \u003C/div\u003E\n \u003CButton\n aria-label={`Clear ${title} filter`}\n variant=\"outline\"\n size=\"sm\"\n onClick={onReset}\n \u003E\n Clear\n \u003C/Button\u003E\n \u003C/PopoverContent\u003E\n \u003C/Popover\u003E\n );\n}\n", | |
"type": "registry:component" | |
}, | |
{ | |
"path": "src/components/data-table-date-filter.tsx", | |
"content": "\"use client\";\n\nimport type { Column } from \"@tanstack/react-table\";\nimport { CalendarIcon, XCircle } from \"lucide-react\";\nimport * as React from \"react\";\nimport type { DateRange } from \"react-day-picker\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { Calendar } from \"@/components/ui/calendar\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { formatDate } from \"@/lib/format\";\n\ntype DateSelection = Date[] | DateRange;\n\nfunction getIsDateRange(value: DateSelection): value is DateRange {\n return value && typeof value === \"object\" && !Array.isArray(value);\n}\n\nfunction parseAsDate(timestamp: number | string | undefined): Date | undefined {\n if (!timestamp) return undefined;\n const numericTimestamp =\n typeof timestamp === \"string\" ? Number(timestamp) : timestamp;\n const date = new Date(numericTimestamp);\n return !Number.isNaN(date.getTime()) ? date : undefined;\n}\n\nfunction parseColumnFilterValue(value: unknown) {\n if (value === null || value === undefined) {\n return [];\n }\n\n if (Array.isArray(value)) {\n return value.map((item) =\u003E {\n if (typeof item === \"number\" || typeof item === \"string\") {\n return item;\n }\n return undefined;\n });\n }\n\n if (typeof value === \"string\" || typeof value === \"number\") {\n return [value];\n }\n\n return [];\n}\n\ninterface DataTableDateFilterProps\u003CTData\u003E {\n column: Column\u003CTData, unknown\u003E;\n title?: string;\n multiple?: boolean;\n}\n\nexport function DataTableDateFilter\u003CTData\u003E({\n column,\n title,\n multiple,\n}: DataTableDateFilterProps\u003CTData\u003E) {\n const columnFilterValue = column.getFilterValue();\n\n const selectedDates = React.useMemo\u003CDateSelection\u003E(() =\u003E {\n if (!columnFilterValue) {\n return multiple ? { from: undefined, to: undefined } : [];\n }\n\n if (multiple) {\n const timestamps = parseColumnFilterValue(columnFilterValue);\n return {\n from: parseAsDate(timestamps[0]),\n to: parseAsDate(timestamps[1]),\n };\n }\n\n const timestamps = parseColumnFilterValue(columnFilterValue);\n const date = parseAsDate(timestamps[0]);\n return date ? [date] : [];\n }, [columnFilterValue, multiple]);\n\n const onSelect = React.useCallback(\n (date: Date | DateRange | undefined) =\u003E {\n if (!date) {\n column.setFilterValue(undefined);\n return;\n }\n\n if (multiple && !(\"getTime\" in date)) {\n const from = date.from?.getTime();\n const to = date.to?.getTime();\n column.setFilterValue(from || to ? [from, to] : undefined);\n } else if (!multiple && \"getTime\" in date) {\n column.setFilterValue(date.getTime());\n }\n },\n [column, multiple],\n );\n\n const onReset = React.useCallback(\n (event: React.MouseEvent) =\u003E {\n event.stopPropagation();\n column.setFilterValue(undefined);\n },\n [column],\n );\n\n const hasValue = React.useMemo(() =\u003E {\n if (multiple) {\n if (!getIsDateRange(selectedDates)) return false;\n return selectedDates.from || selectedDates.to;\n }\n if (!Array.isArray(selectedDates)) return false;\n return selectedDates.length \u003E 0;\n }, [multiple, selectedDates]);\n\n const formatDateRange = React.useCallback((range: DateRange) =\u003E {\n if (!range.from && !range.to) return \"\";\n if (range.from && range.to) {\n return `${formatDate(range.from)} - ${formatDate(range.to)}`;\n }\n return formatDate(range.from ?? range.to);\n }, []);\n\n const label = React.useMemo(() =\u003E {\n if (multiple) {\n if (!getIsDateRange(selectedDates)) return null;\n\n const hasSelectedDates = selectedDates.from || selectedDates.to;\n const dateText = hasSelectedDates\n ? formatDateRange(selectedDates)\n : \"Select date range\";\n\n return (\n \u003Cspan className=\"flex items-center gap-2\"\u003E\n \u003Cspan\u003E{title}\u003C/span\u003E\n {hasSelectedDates && (\n \u003C\u003E\n \u003CSeparator\n orientation=\"vertical\"\n className=\"mx-0.5 data-[orientation=vertical]:h-4\"\n /\u003E\n \u003Cspan\u003E{dateText}\u003C/span\u003E\n \u003C/\u003E\n )}\n \u003C/span\u003E\n );\n }\n\n if (getIsDateRange(selectedDates)) return null;\n\n const hasSelectedDate = selectedDates.length \u003E 0;\n const dateText = hasSelectedDate\n ? formatDate(selectedDates[0])\n : \"Select date\";\n\n return (\n \u003Cspan className=\"flex items-center gap-2\"\u003E\n \u003Cspan\u003E{title}\u003C/span\u003E\n {hasSelectedDate && (\n \u003C\u003E\n \u003CSeparator\n orientation=\"vertical\"\n className=\"mx-0.5 data-[orientation=vertical]:h-4\"\n /\u003E\n \u003Cspan\u003E{dateText}\u003C/span\u003E\n \u003C/\u003E\n )}\n \u003C/span\u003E\n );\n }, [selectedDates, multiple, formatDateRange, title]);\n\n return (\n \u003CPopover\u003E\n \u003CPopoverTrigger asChild\u003E\n \u003CButton variant=\"outline\" size=\"sm\" className=\"border-dashed\"\u003E\n {hasValue ? (\n \u003Cdiv\n role=\"button\"\n aria-label={`Clear ${title} filter`}\n tabIndex={0}\n onClick={onReset}\n className=\"rounded-sm opacity-70 transition-opacity hover:opacity-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring\"\n \u003E\n \u003CXCircle /\u003E\n \u003C/div\u003E\n ) : (\n \u003CCalendarIcon /\u003E\n )}\n {label}\n \u003C/Button\u003E\n \u003C/PopoverTrigger\u003E\n \u003CPopoverContent className=\"w-auto p-0\" align=\"start\"\u003E\n {multiple ? (\n \u003CCalendar\n initialFocus\n mode=\"range\"\n selected={\n getIsDateRange(selectedDates)\n ? selectedDates\n : { from: undefined, to: undefined }\n }\n onSelect={onSelect}\n /\u003E\n ) : (\n \u003CCalendar\n initialFocus\n mode=\"single\"\n selected={\n !getIsDateRange(selectedDates) ? selectedDates[0] : undefined\n }\n onSelect={onSelect}\n /\u003E\n )}\n \u003C/PopoverContent\u003E\n \u003C/Popover\u003E\n );\n}\n", | |
"type": "registry:component" | |
}, | |
{ | |
"path": "src/components/data-table-skeleton.tsx", | |
"content": "import { Skeleton } from \"@/components/ui/skeleton\";\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from \"@/components/ui/table\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DataTableSkeletonProps extends React.ComponentProps\u003C\"div\"\u003E {\n columnCount: number;\n rowCount?: number;\n filterCount?: number;\n cellWidths?: string[];\n withViewOptions?: boolean;\n withPagination?: boolean;\n shrinkZero?: boolean;\n}\n\nexport function DataTableSkeleton({\n columnCount,\n rowCount = 10,\n filterCount = 0,\n cellWidths = [\"auto\"],\n withViewOptions = true,\n withPagination = true,\n shrinkZero = false,\n className,\n ...props\n}: DataTableSkeletonProps) {\n const cozyCellWidths = Array.from(\n { length: columnCount },\n (_, index) =\u003E cellWidths[index % cellWidths.length] ?? \"auto\",\n );\n\n return (\n \u003Cdiv\n className={cn(\"flex w-full flex-col gap-2.5 overflow-auto\", className)}\n {...props}\n \u003E\n \u003Cdiv className=\"flex w-full items-center justify-between gap-2 overflow-auto p-1\"\u003E\n \u003Cdiv className=\"flex flex-1 items-center gap-2\"\u003E\n {filterCount \u003E 0\n ? Array.from({ length: filterCount }).map((_, i) =\u003E (\n \u003CSkeleton key={i} className=\"h-7 w-[4.5rem] border-dashed\" /\u003E\n ))\n : null}\n \u003C/div\u003E\n {withViewOptions ? (\n \u003CSkeleton className=\"ml-auto hidden h-7 w-[4.5rem] lg:flex\" /\u003E\n ) : null}\n \u003C/div\u003E\n \u003Cdiv className=\"rounded-md border\"\u003E\n \u003CTable\u003E\n \u003CTableHeader\u003E\n {Array.from({ length: 1 }).map((_, i) =\u003E (\n \u003CTableRow key={i} className=\"hover:bg-transparent\"\u003E\n {Array.from({ length: columnCount }).map((_, j) =\u003E (\n \u003CTableHead\n key={j}\n style={{\n width: cozyCellWidths[j],\n minWidth: shrinkZero ? cozyCellWidths[j] : \"auto\",\n }}\n \u003E\n \u003CSkeleton className=\"h-6 w-full\" /\u003E\n \u003C/TableHead\u003E\n ))}\n \u003C/TableRow\u003E\n ))}\n \u003C/TableHeader\u003E\n \u003CTableBody\u003E\n {Array.from({ length: rowCount }).map((_, i) =\u003E (\n \u003CTableRow key={i} className=\"hover:bg-transparent\"\u003E\n {Array.from({ length: columnCount }).map((_, j) =\u003E (\n \u003CTableCell\n key={j}\n style={{\n width: cozyCellWidths[j],\n minWidth: shrinkZero ? cozyCellWidths[j] : \"auto\",\n }}\n \u003E\n \u003CSkeleton className=\"h-6 w-full\" /\u003E\n \u003C/TableCell\u003E\n ))}\n \u003C/TableRow\u003E\n ))}\n \u003C/TableBody\u003E\n \u003C/Table\u003E\n \u003C/div\u003E\n {withPagination ? (\n \u003Cdiv className=\"flex w-full items-center justify-between gap-4 overflow-auto p-1 sm:gap-8\"\u003E\n \u003CSkeleton className=\"h-7 w-40 shrink-0\" /\u003E\n \u003Cdiv className=\"flex items-center gap-4 sm:gap-6 lg:gap-8\"\u003E\n \u003Cdiv className=\"flex items-center gap-2\"\u003E\n \u003CSkeleton className=\"h-7 w-24\" /\u003E\n \u003CSkeleton className=\"h-7 w-[4.5rem]\" /\u003E\n \u003C/div\u003E\n \u003Cdiv className=\"flex items-center justify-center font-medium text-sm\"\u003E\n \u003CSkeleton className=\"h-7 w-20\" /\u003E\n \u003C/div\u003E\n \u003Cdiv className=\"flex items-center gap-2\"\u003E\n \u003CSkeleton className=\"hidden size-7 lg:block\" /\u003E\n \u003CSkeleton className=\"size-7\" /\u003E\n \u003CSkeleton className=\"size-7\" /\u003E\n \u003CSkeleton className=\"hidden size-7 lg:block\" /\u003E\n \u003C/div\u003E\n \u003C/div\u003E\n \u003C/div\u003E\n ) : null}\n \u003C/div\u003E\n );\n}\n", | |
"type": "registry:component" | |
}, | |
{ | |
"path": "src/hooks/use-callback-ref.ts", | |
"content": "import * as React from \"react\";\n\n/**\n * @see https://github.com/radix-ui/primitives/blob/main/packages/react/use-callback-ref/src/useCallbackRef.tsx\n */\n\n/**\n * A custom hook that converts a callback to a ref to avoid triggering re-renders when passed as a\n * prop or avoid re-executing effects when passed as a dependency\n */\nfunction useCallbackRef\u003CT extends (...args: never[]) =\u003E unknown\u003E(\n callback: T | undefined,\n): T {\n const callbackRef = React.useRef(callback);\n\n React.useEffect(() =\u003E {\n callbackRef.current = callback;\n });\n\n // https://github.com/facebook/react/issues/19240\n return React.useMemo(\n () =\u003E ((...args) =\u003E callbackRef.current?.(...args)) as T,\n [],\n );\n}\n\nexport { useCallbackRef };\n", | |
"type": "registry:hook" | |
}, | |
{ | |
"path": "src/hooks/use-data-table.ts", | |
"content": "\"use client\";\n\nimport {\n type ColumnFiltersState,\n type PaginationState,\n type RowSelectionState,\n type SortingState,\n type TableOptions,\n type TableState,\n type Updater,\n type VisibilityState,\n getCoreRowModel,\n getFacetedMinMaxValues,\n getFacetedRowModel,\n getFacetedUniqueValues,\n getFilteredRowModel,\n getPaginationRowModel,\n getSortedRowModel,\n useReactTable,\n} from \"@tanstack/react-table\";\nimport {\n type Parser,\n type UseQueryStateOptions,\n parseAsArrayOf,\n parseAsInteger,\n parseAsString,\n useQueryState,\n useQueryStates,\n} from \"nuqs\";\nimport * as React from \"react\";\n\nimport { useDebouncedCallback } from \"@/hooks/use-debounced-callback\";\nimport { getSortingStateParser } from \"@/lib/parsers\";\nimport type { ExtendedColumnSort } from \"@/types/data-table\";\n\nconst PAGE_KEY = \"page\";\nconst PER_PAGE_KEY = \"perPage\";\nconst SORT_KEY = \"sort\";\nconst ARRAY_SEPARATOR = \",\";\nconst DEBOUNCE_MS = 300;\nconst THROTTLE_MS = 50;\n\ninterface UseDataTableProps\u003CTData\u003E\n extends Omit\u003C\n TableOptions\u003CTData\u003E,\n | \"state\"\n | \"pageCount\"\n | \"getCoreRowModel\"\n | \"manualFiltering\"\n | \"manualPagination\"\n | \"manualSorting\"\n \u003E,\n Required\u003CPick\u003CTableOptions\u003CTData\u003E, \"pageCount\"\u003E\u003E {\n initialState?: Omit\u003CPartial\u003CTableState\u003E, \"sorting\"\u003E & {\n sorting?: ExtendedColumnSort\u003CTData\u003E[];\n };\n history?: \"push\" | \"replace\";\n debounceMs?: number;\n throttleMs?: number;\n clearOnDefault?: boolean;\n enableAdvancedFilter?: boolean;\n scroll?: boolean;\n shallow?: boolean;\n startTransition?: React.TransitionStartFunction;\n}\n\nexport function useDataTable\u003CTData\u003E(props: UseDataTableProps\u003CTData\u003E) {\n const {\n columns,\n pageCount = -1,\n initialState,\n history = \"replace\",\n debounceMs = DEBOUNCE_MS,\n throttleMs = THROTTLE_MS,\n clearOnDefault = false,\n enableAdvancedFilter = false,\n scroll = false,\n shallow = true,\n startTransition,\n ...tableProps\n } = props;\n\n const queryStateOptions = React.useMemo\u003C\n Omit\u003CUseQueryStateOptions\u003Cstring\u003E, \"parse\"\u003E\n \u003E(\n () =\u003E ({\n history,\n scroll,\n shallow,\n throttleMs,\n debounceMs,\n clearOnDefault,\n startTransition,\n }),\n [\n history,\n scroll,\n shallow,\n throttleMs,\n debounceMs,\n clearOnDefault,\n startTransition,\n ],\n );\n\n const [rowSelection, setRowSelection] = React.useState\u003CRowSelectionState\u003E(\n initialState?.rowSelection ?? {},\n );\n const [columnVisibility, setColumnVisibility] =\n React.useState\u003CVisibilityState\u003E(initialState?.columnVisibility ?? {});\n\n const [page, setPage] = useQueryState(\n PAGE_KEY,\n parseAsInteger.withOptions(queryStateOptions).withDefault(1),\n );\n const [perPage, setPerPage] = useQueryState(\n PER_PAGE_KEY,\n parseAsInteger\n .withOptions(queryStateOptions)\n .withDefault(initialState?.pagination?.pageSize ?? 10),\n );\n\n const pagination: PaginationState = React.useMemo(() =\u003E {\n return {\n pageIndex: page - 1, // zero-based index -\u003E one-based index\n pageSize: perPage,\n };\n }, [page, perPage]);\n\n const onPaginationChange = React.useCallback(\n (updaterOrValue: Updater\u003CPaginationState\u003E) =\u003E {\n if (typeof updaterOrValue === \"function\") {\n const newPagination = updaterOrValue(pagination);\n void setPage(newPagination.pageIndex + 1);\n void setPerPage(newPagination.pageSize);\n } else {\n void setPage(updaterOrValue.pageIndex + 1);\n void setPerPage(updaterOrValue.pageSize);\n }\n },\n [pagination, setPage, setPerPage],\n );\n\n const columnIds = React.useMemo(() =\u003E {\n return new Set(\n columns.map((column) =\u003E column.id).filter(Boolean) as string[],\n );\n }, [columns]);\n\n const [sorting, setSorting] = useQueryState(\n SORT_KEY,\n getSortingStateParser\u003CTData\u003E(columnIds)\n .withOptions(queryStateOptions)\n .withDefault(initialState?.sorting ?? []),\n );\n\n const onSortingChange = React.useCallback(\n (updaterOrValue: Updater\u003CSortingState\u003E) =\u003E {\n if (typeof updaterOrValue === \"function\") {\n const newSorting = updaterOrValue(sorting);\n setSorting(newSorting as ExtendedColumnSort\u003CTData\u003E[]);\n } else {\n setSorting(updaterOrValue as ExtendedColumnSort\u003CTData\u003E[]);\n }\n },\n [sorting, setSorting],\n );\n\n const filterableColumns = React.useMemo(() =\u003E {\n if (enableAdvancedFilter) return [];\n\n return columns.filter((column) =\u003E column.enableColumnFilter);\n }, [columns, enableAdvancedFilter]);\n\n const filterParsers = React.useMemo(() =\u003E {\n if (enableAdvancedFilter) return {};\n\n return filterableColumns.reduce\u003C\n Record\u003Cstring, Parser\u003Cstring\u003E | Parser\u003Cstring[]\u003E\u003E\n \u003E((acc, column) =\u003E {\n if (column.meta?.options) {\n acc[column.id ?? \"\"] = parseAsArrayOf(\n parseAsString,\n ARRAY_SEPARATOR,\n ).withOptions(queryStateOptions);\n } else {\n acc[column.id ?? \"\"] = parseAsString.withOptions(queryStateOptions);\n }\n return acc;\n }, {});\n }, [filterableColumns, queryStateOptions, enableAdvancedFilter]);\n\n const [filterValues, setFilterValues] = useQueryStates(filterParsers);\n\n const debouncedSetFilterValues = useDebouncedCallback(\n (values: typeof filterValues) =\u003E {\n void setPage(1);\n void setFilterValues(values);\n },\n debounceMs,\n );\n\n const initialColumnFilters: ColumnFiltersState = React.useMemo(() =\u003E {\n if (enableAdvancedFilter) return [];\n\n return Object.entries(filterValues).reduce\u003CColumnFiltersState\u003E(\n (filters, [key, value]) =\u003E {\n if (value !== null) {\n const processedValue = Array.isArray(value)\n ? value\n : typeof value === \"string\" && /[^a-zA-Z0-9]/.test(value)\n ? value.split(/[^a-zA-Z0-9]+/).filter(Boolean)\n : [value];\n\n filters.push({\n id: key,\n value: processedValue,\n });\n }\n return filters;\n },\n [],\n );\n }, [filterValues, enableAdvancedFilter]);\n\n const [columnFilters, setColumnFilters] =\n React.useState\u003CColumnFiltersState\u003E(initialColumnFilters);\n\n const onColumnFiltersChange = React.useCallback(\n (updaterOrValue: Updater\u003CColumnFiltersState\u003E) =\u003E {\n if (enableAdvancedFilter) return;\n\n setColumnFilters((prev) =\u003E {\n const next =\n typeof updaterOrValue === \"function\"\n ? updaterOrValue(prev)\n : updaterOrValue;\n\n const filterUpdates = next.reduce\u003C\n Record\u003Cstring, string | string[] | null\u003E\n \u003E((acc, filter) =\u003E {\n if (filterableColumns.find((column) =\u003E column.id === filter.id)) {\n acc[filter.id] = filter.value as string | string[];\n }\n return acc;\n }, {});\n\n for (const prevFilter of prev) {\n if (!next.some((filter) =\u003E filter.id === prevFilter.id)) {\n filterUpdates[prevFilter.id] = null;\n }\n }\n\n debouncedSetFilterValues(filterUpdates);\n return next;\n });\n },\n [debouncedSetFilterValues, filterableColumns, enableAdvancedFilter],\n );\n\n const table = useReactTable({\n ...tableProps,\n columns,\n initialState,\n pageCount,\n state: {\n pagination,\n sorting,\n columnVisibility,\n rowSelection,\n columnFilters,\n },\n defaultColumn: {\n ...tableProps.defaultColumn,\n enableColumnFilter: false,\n },\n enableRowSelection: true,\n onRowSelectionChange: setRowSelection,\n onPaginationChange,\n onSortingChange,\n onColumnFiltersChange,\n onColumnVisibilityChange: setColumnVisibility,\n getCoreRowModel: getCoreRowModel(),\n getFilteredRowModel: getFilteredRowModel(),\n getPaginationRowModel: getPaginationRowModel(),\n getSortedRowModel: getSortedRowModel(),\n getFacetedRowModel: getFacetedRowModel(),\n getFacetedUniqueValues: getFacetedUniqueValues(),\n getFacetedMinMaxValues: getFacetedMinMaxValues(),\n manualPagination: true,\n manualSorting: true,\n manualFiltering: true,\n });\n\n return { table, shallow, debounceMs, throttleMs };\n}\n", | |
"type": "registry:hook" | |
}, | |
{ | |
"path": "src/hooks/use-debounced-callback.ts", | |
"content": "/**\n * @see https://github.com/mantinedev/mantine/blob/master/packages/@mantine/hooks/src/use-debounced-callback/use-debounced-callback.ts\n */\n\nimport * as React from \"react\";\n\nimport { useCallbackRef } from \"@/hooks/use-callback-ref\";\n\nexport function useDebouncedCallback\u003CT extends (...args: never[]) =\u003E unknown\u003E(\n callback: T,\n delay: number,\n) {\n const handleCallback = useCallbackRef(callback);\n const debounceTimerRef = React.useRef(0);\n React.useEffect(\n () =\u003E () =\u003E window.clearTimeout(debounceTimerRef.current),\n [],\n );\n\n const setValue = React.useCallback(\n (...args: Parameters\u003CT\u003E) =\u003E {\n window.clearTimeout(debounceTimerRef.current);\n debounceTimerRef.current = window.setTimeout(\n () =\u003E handleCallback(...args),\n delay,\n );\n },\n [handleCallback, delay],\n );\n\n return setValue;\n}\n", | |
"type": "registry:hook" | |
}, | |
{ | |
"path": "src/lib/data-table.ts", | |
"content": "import type {\n ExtendedColumnFilter,\n FilterOperator,\n FilterVariant,\n} from \"@/types/data-table\";\nimport type { Column } from \"@tanstack/react-table\";\n\nimport { dataTableConfig } from \"@/config/data-table\";\n\nexport function getCommonPinningStyles\u003CTData\u003E({\n column,\n withBorder = false,\n}: {\n column: Column\u003CTData\u003E;\n withBorder?: boolean;\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 boxShadow: withBorder\n ? isLastLeftPinnedColumn\n ? \"-4px 0 4px -4px hsl(var(--border)) inset\"\n : isFirstRightPinnedColumn\n ? \"4px 0 4px -4px hsl(var(--border)) inset\"\n : undefined\n : undefined,\n left: isPinned === \"left\" ? `${column.getStart(\"left\")}px` : undefined,\n right: isPinned === \"right\" ? `${column.getAfter(\"right\")}px` : undefined,\n opacity: isPinned ? 0.97 : 1,\n position: isPinned ? \"sticky\" : \"relative\",\n background: isPinned ? \"hsl(var(--background))\" : \"hsl(var(--background))\",\n width: column.getSize(),\n zIndex: isPinned ? 1 : 0,\n };\n}\n\nexport function getFilterOperators(filterVariant: FilterVariant) {\n const operatorMap: Record\u003C\n FilterVariant,\n { label: string; value: FilterOperator }[]\n \u003E = {\n text: dataTableConfig.textOperators,\n number: dataTableConfig.numericOperators,\n range: dataTableConfig.numericOperators,\n date: dataTableConfig.dateOperators,\n dateRange: dataTableConfig.dateOperators,\n boolean: dataTableConfig.booleanOperators,\n select: dataTableConfig.selectOperators,\n multiSelect: dataTableConfig.multiSelectOperators,\n };\n\n return operatorMap[filterVariant] ?? dataTableConfig.textOperators;\n}\n\nexport function getDefaultFilterOperator(filterVariant: FilterVariant) {\n const operators = getFilterOperators(filterVariant);\n\n return operators[0]?.value ?? (filterVariant === \"text\" ? \"iLike\" : \"eq\");\n}\n\nexport function getValidFilters\u003CTData\u003E(\n filters: ExtendedColumnFilter\u003CTData\u003E[],\n): ExtendedColumnFilter\u003CTData\u003E[] {\n return filters.filter(\n (filter) =\u003E\n filter.operator === \"isEmpty\" ||\n filter.operator === \"isNotEmpty\" ||\n (Array.isArray(filter.value)\n ? filter.value.length \u003E 0\n : filter.value !== \"\" &&\n filter.value !== null &&\n filter.value !== undefined),\n );\n}\n", | |
"type": "registry:lib" | |
}, | |
{ | |
"path": "src/lib/format.ts", | |
"content": "export function formatDate(\n date: Date | string | number | undefined,\n opts: Intl.DateTimeFormatOptions = {},\n) {\n if (!date) return \"\";\n\n try {\n return new Intl.DateTimeFormat(\"en-US\", {\n month: opts.month ?? \"long\",\n day: opts.day ?? \"numeric\",\n year: opts.year ?? \"numeric\",\n ...opts,\n }).format(new Date(date));\n } catch (_err) {\n return \"\";\n }\n}\n", | |
"type": "registry:lib" | |
}, | |
{ | |
"path": "src/lib/parsers.ts", | |
"content": "import { createParser } from \"nuqs/server\";\nimport { z } from \"zod\";\n\nimport { dataTableConfig } from \"@/config/data-table\";\n\nimport type {\n ExtendedColumnFilter,\n ExtendedColumnSort,\n} from \"@/types/data-table\";\n\nconst sortingItemSchema = z.object({\n id: z.string(),\n desc: z.boolean(),\n});\n\nexport const getSortingStateParser = \u003CTData\u003E(\n columnIds?: string[] | Set\u003Cstring\u003E,\n) =\u003E {\n const validKeys = columnIds\n ? columnIds instanceof Set\n ? columnIds\n : new Set(columnIds)\n : null;\n\n return createParser({\n parse: (value) =\u003E {\n try {\n const parsed = JSON.parse(value);\n const result = z.array(sortingItemSchema).safeParse(parsed);\n\n if (!result.success) return null;\n\n if (validKeys && result.data.some((item) =\u003E !validKeys.has(item.id))) {\n return null;\n }\n\n return result.data as ExtendedColumnSort\u003CTData\u003E[];\n } catch {\n return null;\n }\n },\n serialize: (value) =\u003E JSON.stringify(value),\n eq: (a, b) =\u003E\n a.length === b.length &&\n a.every(\n (item, index) =\u003E\n item.id === b[index]?.id && item.desc === b[index]?.desc,\n ),\n });\n};\n\nconst filterItemSchema = z.object({\n id: z.string(),\n value: z.union([z.string(), z.array(z.string())]),\n variant: z.enum(dataTableConfig.filterVariants),\n operator: z.enum(dataTableConfig.operators),\n filterId: z.string(),\n});\n\nexport type FilterItemSchema = z.infer\u003Ctypeof filterItemSchema\u003E;\n\nexport const getFiltersStateParser = \u003CTData\u003E(\n columnIds?: string[] | Set\u003Cstring\u003E,\n) =\u003E {\n const validKeys = columnIds\n ? columnIds instanceof Set\n ? columnIds\n : new Set(columnIds)\n : null;\n\n return createParser({\n parse: (value) =\u003E {\n try {\n const parsed = JSON.parse(value);\n const result = z.array(filterItemSchema).safeParse(parsed);\n\n if (!result.success) return null;\n\n if (validKeys && result.data.some((item) =\u003E !validKeys.has(item.id))) {\n return null;\n }\n\n return result.data as ExtendedColumnFilter\u003CTData\u003E[];\n } catch {\n return null;\n }\n },\n serialize: (value) =\u003E JSON.stringify(value),\n eq: (a, b) =\u003E\n a.length === b.length &&\n a.every(\n (filter, index) =\u003E\n filter.id === b[index]?.id &&\n filter.value === b[index]?.value &&\n filter.variant === b[index]?.variant &&\n filter.operator === b[index]?.operator,\n ),\n });\n};\n", | |
"type": "registry:lib" | |
}, | |
{ | |
"path": "src/config/data-table.ts", | |
"content": "export type DataTableConfig = typeof dataTableConfig;\n\nexport const dataTableConfig = {\n textOperators: [\n { label: \"Contains\", value: \"iLike\" as const },\n { label: \"Does not contain\", value: \"notILike\" as const },\n { label: \"Is\", value: \"eq\" as const },\n { label: \"Is not\", value: \"ne\" as const },\n { label: \"Is empty\", value: \"isEmpty\" as const },\n { label: \"Is not empty\", value: \"isNotEmpty\" as const },\n ],\n numericOperators: [\n { label: \"Is\", value: \"eq\" as const },\n { label: \"Is not\", value: \"ne\" as const },\n { label: \"Is less than\", value: \"lt\" as const },\n { label: \"Is less than or equal to\", value: \"lte\" as const },\n { label: \"Is greater than\", value: \"gt\" as const },\n { label: \"Is greater than or equal to\", value: \"gte\" as const },\n { label: \"Is between\", value: \"isBetween\" as const },\n { label: \"Is empty\", value: \"isEmpty\" as const },\n { label: \"Is not empty\", value: \"isNotEmpty\" as const },\n ],\n dateOperators: [\n { label: \"Is\", value: \"eq\" as const },\n { label: \"Is not\", value: \"ne\" as const },\n { label: \"Is before\", value: \"lt\" as const },\n { label: \"Is after\", value: \"gt\" as const },\n { label: \"Is on or before\", value: \"lte\" as const },\n { label: \"Is on or after\", value: \"gte\" as const },\n { label: \"Is between\", value: \"isBetween\" as const },\n { label: \"Is relative to today\", value: \"isRelativeToToday\" as const },\n { label: \"Is empty\", value: \"isEmpty\" as const },\n { label: \"Is not empty\", value: \"isNotEmpty\" as const },\n ],\n selectOperators: [\n { label: \"Is\", value: \"eq\" as const },\n { label: \"Is not\", value: \"ne\" as const },\n { label: \"Is empty\", value: \"isEmpty\" as const },\n { label: \"Is not empty\", value: \"isNotEmpty\" as const },\n ],\n multiSelectOperators: [\n { label: \"Has any of\", value: \"inArray\" as const },\n { label: \"Has none of\", value: \"notInArray\" as const },\n { label: \"Is empty\", value: \"isEmpty\" as const },\n { label: \"Is not empty\", value: \"isNotEmpty\" as const },\n ],\n booleanOperators: [\n { label: \"Is\", value: \"eq\" as const },\n { label: \"Is not\", value: \"ne\" as const },\n ],\n sortOrders: [\n { label: \"Asc\", value: \"asc\" as const },\n { label: \"Desc\", value: \"desc\" as const },\n ],\n filterVariants: [\n \"text\",\n \"number\",\n \"range\",\n \"date\",\n \"dateRange\",\n \"boolean\",\n \"select\",\n \"multiSelect\",\n ] as const,\n operators: [\n \"iLike\",\n \"notILike\",\n \"eq\",\n \"ne\",\n \"inArray\",\n \"notInArray\",\n \"isEmpty\",\n \"isNotEmpty\",\n \"lt\",\n \"lte\",\n \"gt\",\n \"gte\",\n \"isBetween\",\n \"isRelativeToToday\",\n ] as const,\n joinOperators: [\"and\", \"or\"] as const,\n};\n", | |
"type": "registry:file", | |
"target": "src/config/data-table.ts" | |
}, | |
{ | |
"path": "src/types/data-table.ts", | |
"content": "import type { DataTableConfig } from \"@/config/data-table\";\nimport type { FilterItemSchema } from \"@/lib/parsers\";\nimport type { ColumnSort, Row, RowData } from \"@tanstack/react-table\";\n\ndeclare module \"@tanstack/react-table\" {\n // biome-ignore lint/correctness/noUnusedVariables: \u003Cexplanation\u003E\n interface ColumnMeta\u003CTData extends RowData, TValue\u003E {\n label?: string;\n placeholder?: string;\n variant?: FilterVariant;\n options?: Option[];\n range?: [number, number];\n unit?: string;\n icon?: React.FC\u003CReact.SVGProps\u003CSVGSVGElement\u003E\u003E;\n }\n}\n\nexport interface Option {\n label: string;\n value: string;\n count?: number;\n icon?: React.FC\u003CReact.SVGProps\u003CSVGSVGElement\u003E\u003E;\n}\n\nexport type FilterOperator = DataTableConfig[\"operators\"][number];\nexport type FilterVariant = DataTableConfig[\"filterVariants\"][number];\nexport type JoinOperator = DataTableConfig[\"joinOperators\"][number];\n\nexport interface ExtendedColumnSort\u003CTData\u003E extends Omit\u003CColumnSort, \"id\"\u003E {\n id: Extract\u003Ckeyof TData, string\u003E;\n}\n\nexport interface ExtendedColumnFilter\u003CTData\u003E extends FilterItemSchema {\n id: Extract\u003Ckeyof TData, string\u003E;\n}\n\nexport interface DataTableRowAction\u003CTData\u003E {\n row: Row\u003CTData\u003E;\n variant: \"update\" | \"delete\";\n}\n", | |
"type": "registry:file", | |
"target": "src/types/data-table.ts" | |
} | |
] | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment