{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "data-table", "title": "Data Table", "description": "Sortable, filterable data table with row actions, built on TanStack Table.", "dependencies": ["lucide-react"], "registryDependencies": ["alert-dialog", "button", "dropdown-menu", "table", "tooltip", "utils"], "files": [ { "path": "src/patterns/data-table/data-table.tsx", "content": "import { useState } from \"react\";\nimport {\n ArrowDown,\n ArrowUp,\n ArrowUpDown,\n Eye,\n Inbox,\n MoreHorizontal,\n Pencil,\n Trash2,\n} from \"lucide-react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n} from \"../../primitives/alert-dialog\";\nimport { Button } from \"../../primitives/button\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from \"../../primitives/dropdown-menu\";\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from \"../../primitives/table\";\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from \"../../primitives/tooltip\";\n\nexport interface DataTableColumn {\n /** Machine-readable key matching a key in DataTableRow.data */\n key: string;\n /** Header label displayed in the column */\n header: string;\n /** Whether this column can be sorted. Defaults to false. */\n sortable?: boolean;\n}\n\nexport interface DataTableRow {\n /** Unique identifier for this row */\n id: string;\n /** Column data keyed by DataTableColumn.key */\n data: Record;\n}\n\nexport interface SortOption {\n value: string;\n property: string;\n prompt: string;\n}\n\nexport interface DataTableProps {\n /** Entity identifier used in empty-state messaging and action callbacks */\n entityName: string;\n /** Human-readable entity title used in empty-state messaging */\n entityTitle?: string;\n /** Column definitions */\n columns: DataTableColumn[];\n /** Rows to display */\n rows: DataTableRow[];\n /** Currently active sort string, e.g. \"name,asc\" */\n currentSort?: string;\n /** Called when the user clicks a sortable column header */\n onSort?: (field: string) => void;\n /** Sort options for tooltip labels */\n sortOptions?: SortOption[];\n /** Called when the user clicks the create-new button in the empty state */\n onCreateClick?: () => void;\n /** Called when the user clicks \"View details\" from the row action menu */\n onViewDetails?: (id: string) => void;\n /** Called when the user clicks \"Edit\" from the row action menu */\n onEdit?: (id: string) => void;\n /** Called when the user confirms deletion. If undefined the Delete action is hidden. */\n onDelete?: (id: string) => void;\n /** When true, the delete confirmation dialog shows a loading state */\n isDeleting?: boolean;\n /** Called when the user clicks the row itself (outside the action menu) */\n onRowClick?: (id: string) => void;\n}\n\nexport function DataTable({\n entityName,\n entityTitle,\n columns,\n rows,\n currentSort,\n onSort,\n sortOptions,\n onCreateClick,\n onViewDetails,\n onEdit,\n onDelete,\n isDeleting,\n onRowClick,\n}: Readonly) {\n const [deleteId, setDeleteId] = useState(null);\n\n function getSortIcon(key: string) {\n const isAsc = currentSort === `${key},asc`;\n const isDesc = currentSort === `${key},desc`;\n if (isAsc) return ;\n if (isDesc) return ;\n return ;\n }\n\n function getSortTooltip(key: string): string | undefined {\n if (!sortOptions) return undefined;\n const isAsc = currentSort === `${key},asc`;\n const isDesc = currentSort === `${key},desc`;\n let nextSort: string | undefined;\n if (isAsc) {\n nextSort = `${key},desc`;\n } else if (isDesc) {\n nextSort = undefined;\n } else {\n nextSort = `${key},asc`;\n }\n const nextPrompt = nextSort\n ? sortOptions.find((o) => o.property === key && o.value === nextSort)?.prompt\n : undefined;\n const currentPrompt = sortOptions.find(\n (o) => o.property === key && o.value === currentSort,\n )?.prompt;\n return nextPrompt ?? currentPrompt;\n }\n\n const hasActions = !!(onViewDetails || onEdit || onDelete);\n\n return (\n <>\n \n
\n \n \n \n {columns.map((col, i) => (\n \n {col.sortable && onSort ? (\n \n \n onSort(col.key)}\n >\n {col.header}\n {getSortIcon(col.key)}\n \n \n {getSortTooltip(col.key) && (\n \n {getSortTooltip(col.key)}\n \n )}\n \n ) : (\n col.header\n )}\n \n ))}\n {hasActions && }\n \n \n \n {rows.length > 0 ? (\n rows.map((row) => (\n onRowClick?.(row.id)}\n >\n {columns.map((col, i) => (\n \n {row.data[col.key] == null ? \"—\" : String(row.data[col.key])}\n \n ))}\n {hasActions && (\n \n \n \n e.stopPropagation()}\n >\n \n Open menu\n \n \n \n {onViewDetails && (\n {\n e.stopPropagation();\n onViewDetails(row.id);\n }}\n >\n \n View details\n \n )}\n {onEdit && (\n {\n e.stopPropagation();\n onEdit(row.id);\n }}\n >\n \n Edit\n \n )}\n {onDelete && (\n <>\n \n {\n e.stopPropagation();\n setDeleteId(row.id);\n }}\n >\n \n Delete\n \n \n )}\n \n \n \n )}\n \n ))\n ) : (\n \n \n
\n \n

No items found

\n \n
\n
\n
\n )}\n
\n
\n
\n
\n\n {onDelete && deleteId && (\n {\n if (!open) setDeleteId(null);\n }}\n >\n \n \n Delete item\n \n Are you sure you want to delete this {(entityTitle ?? entityName).toLowerCase()}?\n This action cannot be undone.\n \n \n \n Cancel\n {\n onDelete(deleteId);\n setDeleteId(null);\n }}\n disabled={isDeleting}\n >\n {isDeleting ? \"Deleting...\" : \"Delete\"}\n \n \n \n \n )}\n \n );\n}\n", "type": "registry:ui" }, { "path": "src/patterns/data-table/index.ts", "content": "export { DataTable } from \"./data-table\";\nexport type { DataTableProps, DataTableColumn, DataTableRow, SortOption } from \"./data-table\";\n", "type": "registry:ui" } ], "type": "registry:ui" }