{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "data-table", "type": "registry:block", "title": "Data Table", "description": "A complete data table primitive that composes Table building blocks with built-in search, column sorting, row selection, pagination, and expandable detail rows. All features compose - enable only what you need.", "author": "Wensity ", "dependencies": [ "@tabler/icons-react", "clsx", "framer-motion", "tailwind-merge" ], "registryDependencies": [ "@wensity/table" ], "files": [ { "path": "registry/wensity/lib/utils.ts", "type": "registry:lib", "target": "@lib/utils.ts", "content": "import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n" }, { "path": "registry/wensity/data-table.tsx", "type": "registry:component", "target": "@components/wensity/data-table.tsx", "content": "\"use client\";\n\nimport * as React from \"react\";\nimport {\n IconChevronLeft,\n IconChevronRight,\n IconSearch,\n IconX,\n} from \"@tabler/icons-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"framer-motion\";\nimport { cn } from \"@/lib/utils\";\nimport {\n Table,\n TableBody,\n TableCaption,\n TableCell,\n TableExpandTriggerCell,\n TableExpandableContent,\n TableHead,\n TableHeader,\n TableRow,\n TableSelectCell,\n type SortDirection,\n type TableDensity,\n} from \"@/components/wensity/table\";\n\nexport type { SortDirection, TableDensity };\n\n/* ─── Types ─────────────────────────────────────────────────── */\n\nexport interface DataTableColumn {\n /** Unique column identifier. */\n id: string;\n /** Header label displayed in the column head. */\n header: string;\n /** Key on the row data object used for default cell rendering and filtering. */\n accessorKey?: keyof TData;\n /** Custom cell renderer. Receives the full row. */\n cell?: (info: { row: TData }) => React.ReactNode;\n /** Whether this column can be sorted. */\n sortable?: boolean;\n}\n\nexport interface DataTableFilterConfig {\n /** Placeholder text for the search input. */\n placeholder?: string;\n /** Keys on the row data to search against. Defaults to all accessorKey columns. */\n searchKeys?: string[];\n /** Debounce delay in ms. Default 240. */\n debounceMs?: number;\n}\n\nexport interface DataTablePaginationConfig {\n /** Rows per page. Default 10. */\n pageSize?: number;\n}\n\nexport interface DataTableExpandableConfig {\n /** Render function for expanded row content. */\n renderExpanded: (row: TData) => React.ReactNode;\n}\n\nexport interface DataTableSelectableConfig {\n /** Controlled selected row IDs. */\n selectedIds?: string[];\n /** Called when selection changes. */\n onSelectionChange?: (ids: string[]) => void;\n /** Label for the select-all checkbox. */\n selectAllLabel?: string;\n}\n\nexport interface DataTableProps> {\n /** Column definitions. */\n columns: DataTableColumn[];\n /** Row data. */\n data: TData[];\n /** Unique row identifier function. Defaults to \\`row.id\\` as string. */\n getRowId?: (row: TData) => string;\n\n /* ── Feature toggles ── */\n\n /** Enable column sorting. When true, any column with \\`sortable: true\\` becomes interactive. */\n sortable?: boolean;\n /** Enable search/filter. Pass a config object or \\`true\\` for defaults. */\n filterable?: boolean | DataTableFilterConfig;\n /** Enable row selection with checkboxes. Pass a config object for controlled mode or \\`true\\` for uncontrolled. */\n selectable?: boolean | DataTableSelectableConfig;\n /** Enable pagination. Pass a config object or \\`true\\` for defaults (10 rows/page). */\n paginated?: boolean | DataTablePaginationConfig;\n /** Enable expandable rows. Pass a config object with \\`renderExpanded\\`. */\n expandable?: boolean | DataTableExpandableConfig;\n\n /* ── Styling ── */\n\n /** Row density preset. */\n density?: TableDensity;\n /** Stick the header row on scroll. */\n stickyHeader?: boolean;\n /** Optional caption below the table. */\n caption?: string;\n /** Additional class name for the root wrapper. */\n className?: string;\n /** Empty state message when no data matches. */\n emptyState?: React.ReactNode;\n}\n\n/* ─── Constants ─────────────────────────────────────────────── */\n\nconst DEFAULT_PAGE_SIZE = 10;\n\nconst EASE_OUT = [0.16, 1, 0.3, 1] as const;\n\n/* ─── Helpers ───────────────────────────────────────────────── */\n\nfunction defaultGetRowId>(row: T): string {\n if (\"id\" in row && (typeof row.id === \"string\" || typeof row.id === \"number\")) {\n return String(row.id);\n }\n return \"\";\n}\n\nfunction getCellValue>(\n row: T,\n column: DataTableColumn,\n): string {\n if (column.accessorKey) {\n const raw = row[column.accessorKey];\n if (raw == null) return \"\";\n return String(raw);\n }\n return \"\";\n}\n\nfunction getSearchValue>(\n row: T,\n columns: DataTableColumn[],\n key: string,\n): string {\n const column = columns.find(\n (c) => c.id === key || (c.accessorKey !== undefined && String(c.accessorKey) === key),\n );\n\n if (column) {\n return getCellValue(row, column);\n }\n\n const raw = row[key as keyof T];\n if (raw == null) return \"\";\n return String(raw);\n}\n\nfunction matchesSearch>(\n row: T,\n query: string,\n columns: DataTableColumn[],\n searchKeys?: string[],\n): boolean {\n if (!query.trim()) return true;\n const lowerQuery = query.toLowerCase();\n\n const keysToSearch =\n searchKeys?.length\n ? searchKeys\n : columns\n .filter((col) => col.accessorKey)\n .map((col) => String(col.accessorKey));\n\n return keysToSearch.some((key) => {\n const value = getSearchValue(row, columns, key);\n return value.toLowerCase().includes(lowerQuery);\n });\n}\n\nfunction getPageRange(page: number, totalPages: number, siblingCount = 1) {\n const totalSlots = siblingCount * 2 + 5;\n if (totalPages <= totalSlots) {\n return Array.from({ length: totalPages }, (_, i) => i + 1);\n }\n\n const leftSiblingIndex = Math.max(page - siblingCount, 1);\n const rightSiblingIndex = Math.min(page + siblingCount, totalPages);\n const showLeftEllipsis = leftSiblingIndex > 2;\n const showRightEllipsis = rightSiblingIndex < totalPages - 1;\n\n const items: (number | \"ellipsis-start\" | \"ellipsis-end\")[] = [1];\n\n if (showLeftEllipsis) {\n items.push(\"ellipsis-start\");\n } else {\n for (let i = 2; i < leftSiblingIndex; i++) items.push(i);\n }\n\n for (let i = leftSiblingIndex; i <= rightSiblingIndex; i++) {\n if (i !== 1 && i !== totalPages) items.push(i);\n }\n\n if (showRightEllipsis) {\n items.push(\"ellipsis-end\");\n } else {\n for (let i = rightSiblingIndex + 1; i < totalPages; i++) items.push(i);\n }\n\n if (totalPages > 1) items.push(totalPages);\n return items;\n}\n\n/* ─── Sub-components ────────────────────────────────────────── */\n\nfunction SelectAllCheckbox({\n checked,\n indeterminate,\n onCheckedChange,\n ariaLabel,\n}: {\n checked: boolean;\n indeterminate: boolean;\n onCheckedChange?: (checked: boolean) => void;\n ariaLabel: string;\n}) {\n const inputRef = React.useRef(null);\n\n React.useEffect(() => {\n if (inputRef.current) inputRef.current.indeterminate = indeterminate;\n }, [indeterminate]);\n\n return (\n \n onCheckedChange?.(event.currentTarget.checked)}\n className=\"sr-only\"\n />\n \n {indeterminate ? (\n \n ) : (\n \n )}\n \n \n );\n}\n\nfunction SearchBar({\n value,\n onChange,\n placeholder = \"Search…\",\n}: {\n value: string;\n onChange: (value: string) => void;\n placeholder?: string;\n}) {\n const shouldReduceMotion = useReducedMotion();\n\n return (\n
\n \n onChange(e.target.value)}\n placeholder={placeholder}\n className={cn(\n \"w-full bg-transparent text-[var(--foreground)]\",\n \"placeholder:text-[var(--primitive-text-placeholder,var(--muted-foreground))]\",\n \"outline-none\",\n )}\n />\n \n {value ? (\n onChange(\"\")}\n initial={shouldReduceMotion ? false : { opacity: 0, scale: 0.9 }}\n animate={{ opacity: 1, scale: 1 }}\n exit={shouldReduceMotion ? undefined : { opacity: 0, scale: 0.9 }}\n transition={{ duration: 0.15, ease: EASE_OUT }}\n whileTap={{ scale: 0.92 }}\n className={cn(\n \"flex size-5 shrink-0 items-center justify-center rounded-[var(--primitive-radius-control-sm)]\",\n \"text-[var(--muted-foreground)] hover:bg-[var(--primitive-surface-hover)] hover:text-[var(--foreground)]\",\n \"transition-colors duration-150\",\n )}\n >\n \n \n ) : null}\n \n
\n );\n}\n\nfunction PaginationBar({\n page,\n totalPages,\n onPageChange,\n}: {\n page: number;\n totalPages: number;\n onPageChange: (page: number) => void;\n}) {\n if (totalPages <= 1) return null;\n\n const items = getPageRange(page, totalPages);\n\n const navButtonClass = cn(\n \"relative isolate inline-flex size-7 shrink-0 items-center justify-center rounded-[var(--primitive-radius-control-sm)]\",\n \"text-xs font-medium leading-none select-none touch-manipulation outline-none\",\n \"transition-[color,background-color] duration-[150ms] ease-[cubic-bezier(0.23,1,0.32,1)]\",\n \"text-[var(--muted-foreground)]\",\n \"hover:bg-[var(--primitive-surface-hover)] hover:text-[var(--foreground)]\",\n \"focus-visible:ring-2 focus-visible:ring-[var(--primitive-ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--background)]\",\n \"disabled:pointer-events-none disabled:opacity-35\",\n \"[&_svg]:pointer-events-none [&_svg]:shrink-0\",\n );\n\n return (\n \n onPageChange(page - 1)}\n className={navButtonClass}\n aria-label=\"Previous page\"\n >\n \n \n\n {items.map((item) => {\n if (item === \"ellipsis-start\" || item === \"ellipsis-end\") {\n return (\n \n …\n \n );\n }\n\n const isActive = item === page;\n return (\n onPageChange(item)}\n className={cn(\n navButtonClass,\n \"rounded-[var(--primitive-radius-control-sm)] tabular-nums\",\n isActive &&\n \"bg-[var(--primitive-surface-selected)] text-[var(--foreground)]\",\n )}\n >\n {item}\n \n );\n })}\n\n = totalPages}\n onClick={() => onPageChange(page + 1)}\n className={navButtonClass}\n aria-label=\"Next page\"\n >\n \n \n \n );\n}\n\n/* ─── DataTable ─────────────────────────────────────────────── */\n\nfunction DataTableInner>({\n columns,\n data,\n getRowId = defaultGetRowId,\n sortable = false,\n filterable = false,\n selectable = false,\n paginated = false,\n expandable = false,\n density = \"comfortable\",\n stickyHeader = false,\n caption,\n className,\n emptyState,\n}: DataTableProps) {\n /* ── Resolve configs ── */\n\n const filterConfig: DataTableFilterConfig =\n typeof filterable === \"object\" ? filterable : {};\n const paginationConfig: DataTablePaginationConfig =\n typeof paginated === \"object\" ? paginated : {};\n const selectableConfig: DataTableSelectableConfig | null =\n typeof selectable === \"object\" ? selectable : null;\n const expandableConfig: DataTableExpandableConfig | null =\n typeof expandable === \"object\" ? expandable : null;\n\n const isFilterable = filterable !== false;\n const isPaginated = paginated !== false;\n const isSelectable = selectable !== false;\n const isExpandable = expandable !== false;\n const isSortable = sortable !== false;\n\n const pageSize = paginationConfig.pageSize ?? DEFAULT_PAGE_SIZE;\n\n /* ── State ── */\n\n const [searchQuery, setSearchQuery] = React.useState(\"\");\n const [debouncedQuery, setDebouncedQuery] = React.useState(\"\");\n const [sortKey, setSortKey] = React.useState(null);\n const [sortDirection, setSortDirection] = React.useState(null);\n const [currentPage, setCurrentPage] = React.useState(1);\n const [selectedIds, setSelectedIds] = React.useState>(new Set());\n const [expandedId, setExpandedId] = React.useState(null);\n\n /* ── Debounce search ── */\n\n React.useEffect(() => {\n const timer = setTimeout(\n () => setDebouncedQuery(searchQuery),\n filterConfig.debounceMs ?? 240,\n );\n return () => clearTimeout(timer);\n }, [searchQuery, filterConfig.debounceMs]);\n\n /* ── Controlled selection ── */\n\n const effectiveSelectedIds = selectableConfig?.selectedIds\n ? new Set(selectableConfig.selectedIds)\n : selectedIds;\n\n const handleSelectionChange = React.useCallback(\n (ids: Set) => {\n if (selectableConfig?.onSelectionChange) {\n selectableConfig.onSelectionChange([...ids]);\n } else {\n setSelectedIds(ids);\n }\n },\n [selectableConfig],\n );\n\n /* ── Sorting ── */\n\n const toggleSort = React.useCallback(\n (key: string) => {\n if (sortKey === key) {\n setSortDirection((prev) =>\n prev === \"asc\" ? \"desc\" : prev === \"desc\" ? null : \"asc\",\n );\n } else {\n setSortKey(key);\n setSortDirection(\"asc\");\n }\n setCurrentPage(1);\n },\n [sortKey],\n );\n\n /* ── Process: filter → sort → paginate ── */\n\n const filtered = React.useMemo(() => {\n if (!debouncedQuery.trim()) return data;\n return data.filter((row) =>\n matchesSearch(row, debouncedQuery, columns, filterConfig.searchKeys),\n );\n }, [data, debouncedQuery, columns, filterConfig.searchKeys]);\n\n const sorted = React.useMemo(() => {\n if (!sortKey || !sortDirection) return filtered;\n const col = columns.find((c) => c.id === sortKey);\n if (!col) return filtered;\n\n return [...filtered].sort((a, b) => {\n const aVal = getCellValue(a, col);\n const bVal = getCellValue(b, col);\n const cmp = aVal.localeCompare(bVal, undefined, { numeric: true });\n return sortDirection === \"desc\" ? -cmp : cmp;\n });\n }, [filtered, sortKey, sortDirection, columns]);\n\n const totalPages = Math.max(1, Math.ceil(sorted.length / pageSize));\n const safePage = Math.min(currentPage, totalPages);\n\n const paginatedData = React.useMemo(() => {\n if (!isPaginated) return sorted;\n const start = (safePage - 1) * pageSize;\n return sorted.slice(start, start + pageSize);\n }, [sorted, isPaginated, safePage, pageSize]);\n\n /* ── Reset page on filter change ── */\n\n React.useEffect(() => {\n setCurrentPage(1);\n }, [debouncedQuery]);\n\n /* ── Selection helpers ── */\n\n const allRowIds = React.useMemo(\n () => paginatedData.map((row) => getRowId(row)).filter(Boolean),\n [paginatedData, getRowId],\n );\n\n const allSelected =\n allRowIds.length > 0 && allRowIds.every((id) => effectiveSelectedIds.has(id));\n const isIndeterminate =\n allRowIds.some((id) => effectiveSelectedIds.has(id)) && !allSelected;\n\n /* ── Total colSpan for empty/expansion cells ── */\n\n const totalColSpan =\n columns.length +\n (isSelectable ? 1 : 0) +\n (isExpandable && expandableConfig ? 1 : 0);\n\n const resolveSortDir = (colId: string): SortDirection =>\n sortKey === colId ? sortDirection : null;\n\n return (\n
\n {isFilterable ? (\n \n ) : null}\n\n \n {caption ? {caption} : null}\n\n \n \n {isSelectable ? (\n \n {\n if (checked) {\n handleSelectionChange(new Set(allRowIds));\n } else {\n handleSelectionChange(new Set());\n }\n }}\n ariaLabel={selectableConfig?.selectAllLabel ?? \"Select all rows\"}\n />\n \n ) : null}\n\n {isExpandable && expandableConfig ? (\n \n ) : null}\n\n {columns.map((col) => {\n const canSort = isSortable && col.sortable;\n return (\n toggleSort(col.id) : undefined}\n >\n {col.header}\n \n );\n })}\n \n \n\n \n {paginatedData.length === 0 ? (\n \n \n \n {emptyState ?? \"No results found.\"}\n \n \n \n ) : (\n paginatedData.map((row) => {\n const rowId = getRowId(row);\n const isSelected = effectiveSelectedIds.has(rowId);\n const isExpanded = expandedId === rowId;\n\n const handleSelectRow = (next: boolean) => {\n const nextSet = new Set(effectiveSelectedIds);\n if (next) {\n nextSet.add(rowId);\n } else {\n nextSet.delete(rowId);\n }\n handleSelectionChange(nextSet);\n };\n\n const handleExpandRow = (next: boolean) => {\n setExpandedId(next ? rowId : null);\n };\n\n return (\n \n \n {isSelectable ? (\n handleSelectRow(v === true)}\n ariaLabel={`Select ${getCellValue(row, columns[0])}`}\n />\n ) : null}\n\n {isExpandable && expandableConfig ? (\n handleExpandRow(!isExpanded)}\n />\n ) : null}\n\n {columns.map((col) => (\n \n {col.cell ? col.cell({ row }) : getCellValue(row, col)}\n \n ))}\n \n\n {isExpandable && expandableConfig ? (\n \n {expandableConfig.renderExpanded(row)}\n \n ) : null}\n \n );\n })\n )}\n \n
\n\n {isPaginated && totalPages > 1 ? (\n
\n \n Showing {(safePage - 1) * pageSize + 1}–\n {Math.min(safePage * pageSize, sorted.length)} of {sorted.length}\n \n \n
\n ) : null}\n
\n );\n}\n\n/* ─── Export ────────────────────────────────────────────────── */\n\nexport const DataTable = DataTableInner as >(\n props: DataTableProps,\n) => React.ReactElement;\n" } ], "cssVars": { "theme": { "font-sans": "var(--font-satoshi, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif)", "font-display": "var(--font-cabinet, ui-sans-serif, system-ui, sans-serif)", "color-background": "var(--background)", "color-foreground": "var(--foreground)", "color-surface": "var(--surface)", "color-surface-muted": "var(--surface-muted)", "color-border": "var(--border)", "color-border-strong": "var(--border-strong)", "color-muted": "var(--muted)", "color-muted-foreground": "var(--muted-foreground)", "color-ring": "var(--ring)", "color-chili-50": "#fff1ee", "color-chili-100": "#ffe2db", "color-chili-200": "#ffa896", "color-chili-300": "#ff8a73", "color-chili-400": "#f15a45", "color-chili-500": "#cd1c18", "color-chili-600": "#b31614", "color-chili-700": "#9b1313", "color-chili-800": "#6a0d0e", "color-chili-900": "#38000a", "color-chili-950": "#1f0006", "color-primitive-surface-elevated": "var(--primitive-surface-elevated)", "color-primitive-surface-overlay": "var(--primitive-surface-overlay)", "color-primitive-surface-hover": "var(--primitive-surface-hover)", "color-primitive-surface-active": "var(--primitive-surface-active)", "color-primitive-surface-selected": "var(--primitive-surface-selected)", "color-primitive-border-subtle": "var(--primitive-border-subtle)", "color-primitive-ring": "var(--primitive-ring)", "color-primitive-text-secondary": "var(--primitive-text-secondary)", "color-primitive-text-placeholder": "var(--primitive-text-placeholder)", "color-primitive-text-inverse": "var(--primitive-text-inverse)", "color-primitive-destructive": "var(--primitive-destructive)", "color-primitive-destructive-hover": "var(--primitive-destructive-hover)", "color-primitive-destructive-active": "var(--primitive-destructive-active)", "color-primitive-destructive-foreground": "var(--primitive-destructive-foreground)", "color-primitive-destructive-surface": "var(--primitive-destructive-surface)", "color-primitive-destructive-border": "var(--primitive-destructive-border)", "color-primitive-success": "var(--primitive-success)", "color-primitive-warning": "var(--primitive-warning)", "color-primitive-info": "var(--primitive-info)", "color-primitive-control-solid": "var(--primitive-control-solid)", "color-primitive-control-solid-hover": "var(--primitive-control-solid-hover)", "color-primitive-control-solid-active": "var(--primitive-control-solid-active)", "color-primitive-control-solid-foreground": "var(--primitive-control-solid-foreground)", "color-primitive-chart-1": "var(--primitive-chart-1)", "color-primitive-chart-2": "var(--primitive-chart-2)", "color-primitive-chart-3": "var(--primitive-chart-3)", "color-primitive-chart-4": "var(--primitive-chart-4)", "color-primitive-chart-5": "var(--primitive-chart-5)", "color-primitive-chart-6": "var(--primitive-chart-6)", "radius-primitive": "var(--primitive-radius)", "radius-primitive-control": "var(--primitive-radius-control)", "radius-primitive-control-sm": "var(--primitive-radius-control-sm)", "radius-primitive-surface": "var(--primitive-radius-surface)", "radius-primitive-item": "var(--primitive-radius-item)", "font-primitive-sans": "var(--primitive-font-sans)", "font-primitive-display": "var(--primitive-font-display)", "font-primitive-mono": "var(--primitive-font-mono)", "spacing-primitive-control-height-sm": "var(--primitive-control-height-sm)", "spacing-primitive-control-height-md": "var(--primitive-control-height-md)", "spacing-primitive-control-height-lg": "var(--primitive-control-height-lg)" }, "light": { "background": "#fafafa", "foreground": "#0a0a0a", "surface": "#ffffff", "surface-muted": "#f4f4f5", "border": "rgba(10, 10, 10, 0.08)", "border-strong": "rgba(10, 10, 10, 0.16)", "muted": "#f4f4f5", "muted-foreground": "#52525b", "ring": "#cd1c18", "pattern-fg": "rgba(10, 10, 10, 0.07)", "llb-primary": "#18181b", "llb-primary-fg": "#ffffff", "llb-success": "#1f883d", "llb-error": "#cf222e", "primitive-surface-elevated": "#ffffff", "primitive-surface-overlay": "#ffffff", "primitive-surface-hover": "color-mix(in srgb, var(--foreground) 4%, transparent)", "primitive-surface-active": "color-mix(in srgb, var(--foreground) 7%, transparent)", "primitive-surface-selected": "color-mix(in srgb, var(--foreground) 6%, transparent)", "primitive-border-subtle": "color-mix(in srgb, var(--border) 60%, transparent)", "primitive-ring": "color-mix(in srgb, var(--foreground) 45%, transparent)", "primitive-text-secondary": "color-mix(in srgb, var(--foreground) 72%, transparent)", "primitive-text-placeholder": "color-mix(in srgb, var(--muted-foreground) 85%, transparent)", "primitive-radius": "0.875rem", "primitive-font-sans": "var(--font-sans)", "primitive-font-display": "var(--font-display)", "primitive-font-mono": "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace", "primitive-control-solid": "#24292d", "primitive-control-solid-hover": "#2c3237", "primitive-control-solid-active": "#1e2326", "primitive-control-solid-foreground": "#ffffff", "primitive-chart-1": "#2a78d6", "primitive-chart-2": "#1baf7a", "primitive-chart-3": "#eda100", "primitive-chart-4": "#008300", "primitive-chart-5": "#4a3aa7", "primitive-chart-6": "#e34948", "primitive-text-inverse": "#ffffff", "primitive-text-hint": "0.6875rem", "primitive-destructive": "#dc2626", "primitive-destructive-hover": "#b91c1c", "primitive-destructive-active": "#991b1b", "primitive-destructive-foreground": "#ffffff", "primitive-destructive-surface": "rgba(220, 38, 38, 0.10)", "primitive-destructive-border": "rgba(220, 38, 38, 0.45)", "primitive-success": "#047857", "primitive-success-surface": "rgba(4, 120, 87, 0.10)", "primitive-success-border": "rgba(4, 120, 87, 0.45)", "primitive-warning": "#b45309", "primitive-warning-surface": "rgba(180, 83, 9, 0.10)", "primitive-warning-border": "rgba(180, 83, 9, 0.45)", "primitive-info": "#0369a1", "primitive-info-surface": "rgba(3, 105, 161, 0.10)", "primitive-info-border": "rgba(3, 105, 161, 0.45)", "primitive-radius-control": "var(--primitive-radius)", "primitive-radius-control-sm": "max(0px, calc(var(--primitive-radius) - 4px))", "primitive-radius-surface": "calc(var(--primitive-radius) + min(2px, var(--primitive-radius)))", "primitive-radius-item": "max(0px, calc(var(--primitive-radius-surface) - 6px))", "primitive-control-height-sm": "2rem", "primitive-control-height-md": "2.25rem", "primitive-control-height-lg": "2.5rem", "primitive-shadow-raised": "0 1px 2px rgba(0, 0, 0, 0.06), 0 8px 24px -12px rgba(0, 0, 0, 0.08)", "primitive-shadow-overlay": "0 1px 2px rgba(0, 0, 0, 0.06), 0 18px 48px -24px rgba(0, 0, 0, 0.35)", "primitive-shadow-modal": "0 1px 2px rgba(0, 0, 0, 0.08), 0 24px 64px -28px rgba(0, 0, 0, 0.42)", "primitive-z-overlay": "100", "primitive-z-popover": "130", "primitive-z-toast": "140", "primitive-backdrop": "rgba(0, 0, 0, 0.6)", "primitive-ease": "cubic-bezier(0.23, 1, 0.32, 1)" }, "dark": { "background": "#0a0a0b", "foreground": "#f5f5f6", "surface": "#111113", "surface-muted": "#18181b", "border": "rgba(255, 255, 255, 0.08)", "border-strong": "rgba(255, 255, 255, 0.14)", "muted": "#1c1c1f", "muted-foreground": "#a1a1aa", "ring": "#cd1c18", "pattern-fg": "rgba(255, 255, 255, 0.06)", "llb-primary": "#f5f5f6", "llb-primary-fg": "#0a0a0b", "llb-success": "#2da44e", "llb-error": "#f85149", "primitive-surface-elevated": "#0f0f10", "primitive-surface-overlay": "#141415", "primitive-surface-hover": "color-mix(in srgb, var(--foreground) 5%, transparent)", "primitive-surface-active": "color-mix(in srgb, var(--foreground) 10%, transparent)", "primitive-surface-selected": "color-mix(in srgb, var(--foreground) 8%, transparent)", "primitive-border-subtle": "color-mix(in srgb, var(--border) 60%, transparent)", "primitive-ring": "color-mix(in srgb, var(--foreground) 45%, transparent)", "primitive-text-secondary": "color-mix(in srgb, var(--foreground) 72%, transparent)", "primitive-text-placeholder": "color-mix(in srgb, var(--muted-foreground) 85%, transparent)", "primitive-radius": "0.875rem", "primitive-font-sans": "var(--font-sans)", "primitive-font-display": "var(--font-display)", "primitive-font-mono": "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace", "primitive-control-solid": "#f5f3ee", "primitive-control-solid-hover": "#ffffff", "primitive-control-solid-active": "#e8e4da", "primitive-control-solid-foreground": "#151719", "primitive-chart-1": "#3987e5", "primitive-chart-2": "#199e70", "primitive-chart-3": "#c98500", "primitive-chart-4": "#008300", "primitive-chart-5": "#9085e9", "primitive-chart-6": "#e66767", "primitive-text-inverse": "#151719", "primitive-text-hint": "0.6875rem", "primitive-destructive": "#e11d2e", "primitive-destructive-hover": "#c1121f", "primitive-destructive-active": "#a4161a", "primitive-destructive-foreground": "#ffffff", "primitive-destructive-surface": "rgba(225, 29, 46, 0.12)", "primitive-destructive-border": "rgba(225, 29, 46, 0.52)", "primitive-success": "#34d399", "primitive-success-surface": "rgba(52, 211, 153, 0.12)", "primitive-success-border": "rgba(52, 211, 153, 0.45)", "primitive-warning": "#fbbf24", "primitive-warning-surface": "rgba(251, 191, 36, 0.12)", "primitive-warning-border": "rgba(251, 191, 36, 0.45)", "primitive-info": "#38bdf8", "primitive-info-surface": "rgba(56, 189, 248, 0.12)", "primitive-info-border": "rgba(56, 189, 248, 0.45)", "primitive-radius-control": "var(--primitive-radius)", "primitive-radius-control-sm": "max(0px, calc(var(--primitive-radius) - 4px))", "primitive-radius-surface": "calc(var(--primitive-radius) + min(2px, var(--primitive-radius)))", "primitive-radius-item": "max(0px, calc(var(--primitive-radius-surface) - 6px))", "primitive-control-height-sm": "2rem", "primitive-control-height-md": "2.25rem", "primitive-control-height-lg": "2.5rem", "primitive-shadow-raised": "0 1px 2px rgba(0, 0, 0, 0.4), 0 8px 24px -12px rgba(0, 0, 0, 0.6)", "primitive-shadow-overlay": "0 1px 2px rgba(0, 0, 0, 0.4), 0 20px 56px -28px rgba(0, 0, 0, 0.72)", "primitive-shadow-modal": "0 1px 2px rgba(0, 0, 0, 0.45), 0 28px 72px -32px rgba(0, 0, 0, 0.8)", "primitive-z-overlay": "100", "primitive-z-popover": "130", "primitive-z-toast": "140", "primitive-backdrop": "rgba(0, 0, 0, 0.6)", "primitive-ease": "cubic-bezier(0.23, 1, 0.32, 1)" } }, "css": { "@layer base": { ":where([data-wensity-primitive])": { "font-family": "var(--primitive-font-sans)" } }, "@utility scrollbar-hidden": { "scrollbar-width": "none", "-ms-overflow-style": "none", "&::-webkit-scrollbar": { "display": "none" } }, "@keyframes wensity-marquee-x": { "from": { "transform": "translate3d(0, 0, 0)" }, "to": { "transform": "translate3d(-50%, 0, 0)" } }, "@keyframes wensity-marquee-x-reverse": { "from": { "transform": "translate3d(-50%, 0, 0)" }, "to": { "transform": "translate3d(0, 0, 0)" } }, "@keyframes wensity-morph-rot-cw": { "from": { "transform": "rotate(0deg)" }, "to": { "transform": "rotate(360deg)" } }, "@keyframes wensity-morph-rot-ccw": { "from": { "transform": "rotate(0deg)" }, "to": { "transform": "rotate(-360deg)" } } }, "docs": "Free Wensity component. Installs to @components/wensity/data-table.tsx. For Pro components and updates, use pnpm dlx wensity@latest add data-table.", "categories": [ "UI Primitives", "wensity", "free" ], "meta": { "wensity": { "slug": "data-table", "access": "free", "category": "UI Primitives", "kind": "component", "componentUrl": "https://ui.wensity.com/primitives/data-table", "shadcnUrl": "https://ui.wensity.com/r/data-table", "cliInstall": "pnpm dlx wensity@latest add data-table" } } }