Files
cadence-ui/packages/ui/src/components/data-table.tsx
T

964 lines
30 KiB
TypeScript

import {
type CellContext,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
type HeaderContext,
useReactTable,
type ColumnDef,
type FilterFn,
type PaginationState,
type RowSelectionState,
type SortingState
} from "@tanstack/react-table";
import {
forwardRef,
useEffect,
useState,
type ComponentPropsWithoutRef,
type CSSProperties,
type ForwardedRef,
type JSX,
type ReactNode
} from "react";
import { Button } from "./button";
import { Checkbox } from "./checkbox";
import {
EmptyState,
EmptyStateActions,
EmptyStateDescription,
EmptyStateHeader,
EmptyStateTitle
} from "./empty-state";
import { Input, type InputProps } from "./input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select";
import { Skeleton } from "./skeleton";
import { Spinner } from "./spinner";
import { cn } from "../lib/cn";
import type { VariantProps } from "../lib/cva";
import { createDataAttributes, createSlot } from "../lib/contracts";
import {
SortAscendingIcon,
SortDescendingIcon,
SortUnsortedIcon
} from "../lib/icons";
import {
dataTableBodyVariants,
dataTableCellVariants,
dataTableContentVariants,
dataTableHeaderCellVariants,
dataTableHeaderVariants,
dataTablePaginationVariants,
dataTableRootVariants,
dataTableRowVariants,
dataTableSearchContainerVariants,
dataTableSelectionBarVariants,
dataTableStatusVariants,
dataTableTableVariants,
dataTableToolbarVariants
} from "./data-table.variants";
export type DataTableAlignment = "start" | "center" | "end";
export type DataTableSort = {
desc?: boolean;
id: string;
};
export type DataTableColumnContext<TData> = {
column: DataTableColumn<TData>;
sorted: "asc" | "desc" | false;
};
export type DataTableColumn<TData> = {
accessor?: keyof TData | ((row: TData) => unknown);
align?: DataTableAlignment;
cell?: (row: TData) => ReactNode;
header: ReactNode | ((column: DataTableColumnContext<TData>) => ReactNode);
id: string;
searchValue?: (row: TData) => string;
searchable?: boolean;
sortable?: boolean;
width?: number | string;
};
export type DataTableProps<TData> = Omit<ComponentPropsWithoutRef<"div">, "children"> & {
columns: DataTableColumn<TData>[];
defaultPageIndex?: number;
defaultPageSize?: number;
defaultSearchValue?: string;
defaultSelection?: Record<string, boolean>;
defaultSorting?: DataTableSort[];
empty?: ReactNode;
enableSelection?: boolean;
getRowId?: (row: TData, index: number) => string;
loading?: boolean;
loadingRowCount?: number;
onPageIndexChange?: (pageIndex: number) => void;
onPageSizeChange?: (pageSize: number) => void;
onSearchValueChange?: (searchValue: string) => void;
onSelectionChange?: (selection: Record<string, boolean>) => void;
onSortingChange?: (sorting: DataTableSort[]) => void;
pageIndex?: number;
pageSize?: number;
pageSizeOptions?: number[];
renderRowActions?: (row: TData) => ReactNode;
rows: TData[];
searchLabel?: string;
searchPlaceholder?: string;
searchValue?: string;
selection?: Record<string, boolean>;
selectionLabel?: ReactNode | ((rows: TData[]) => ReactNode);
selectionActions?: (rows: TData[]) => ReactNode;
sorting?: DataTableSort[];
tableLabel?: string;
toolbarActions?: ReactNode;
};
type InternalColumnMeta<TData> = {
align: DataTableAlignment;
sourceColumn?: DataTableColumn<TData>;
width?: number | string;
};
function useControllableState<T>({
controlledValue,
defaultValue,
onChange
}: {
controlledValue: T | undefined;
defaultValue: T;
onChange?: (value: T) => void;
}) {
const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
const value = controlledValue ?? uncontrolledValue;
const setValue = (nextValue: T | ((currentValue: T) => T)) => {
const resolvedValue =
typeof nextValue === "function"
? (nextValue as (currentValue: T) => T)(value)
: nextValue;
if (controlledValue === undefined) {
setUncontrolledValue(resolvedValue);
}
onChange?.(resolvedValue);
};
return [value, setValue] as const;
}
function stringifySearchValue(value: unknown): string {
if (value === null || value === undefined) {
return "";
}
if (Array.isArray(value)) {
return value.map((item) => stringifySearchValue(item)).join(" ");
}
if (value instanceof Date) {
return value.toISOString();
}
if (typeof value === "object") {
return Object.values(value as Record<string, unknown>)
.map((item) => stringifySearchValue(item))
.join(" ");
}
return String(value);
}
function getColumnAccessorValue<TData>(row: TData, column: DataTableColumn<TData>) {
if (typeof column.accessor === "function") {
return column.accessor(row);
}
if (typeof column.accessor === "string") {
return row[column.accessor];
}
return undefined;
}
function getColumnSearchText<TData>(row: TData, column: DataTableColumn<TData>) {
if (column.searchValue) {
return column.searchValue(row);
}
return stringifySearchValue(getColumnAccessorValue(row, column));
}
function getColumnWidthStyle(width?: number | string): CSSProperties | undefined {
if (width === undefined) {
return undefined;
}
return {
width: typeof width === "number" ? `${width}px` : width
};
}
function getHeaderLabel<TData>(column: DataTableColumn<TData>) {
return typeof column.header === "string" ? column.header : column.id;
}
function DataTableInner<TData>(
{
className,
columns,
defaultPageIndex = 0,
defaultPageSize = 5,
defaultSearchValue = "",
defaultSelection = {},
defaultSorting = [],
empty,
enableSelection = false,
getRowId,
loading = false,
loadingRowCount = 5,
onPageIndexChange,
onPageSizeChange,
onSearchValueChange,
onSelectionChange,
onSortingChange,
pageIndex,
pageSize,
pageSizeOptions = [5, 10, 20],
renderRowActions,
rows,
searchLabel = "Search rows",
searchPlaceholder = "Search rows",
searchValue,
selection,
selectionLabel,
selectionActions,
sorting,
tableLabel = "Data table",
toolbarActions,
...props
}: DataTableProps<TData>,
ref: ForwardedRef<HTMLDivElement>
) {
const selectionEnabled = enableSelection || selection !== undefined || onSelectionChange !== undefined;
const searchableColumns = columns.filter(
(column) => (column.searchable ?? false) || column.searchValue || column.accessor
);
const [currentSearchValue, setCurrentSearchValue] = useControllableState({
controlledValue: searchValue,
defaultValue: defaultSearchValue,
onChange: onSearchValueChange
});
const [currentSorting, setCurrentSorting] = useControllableState<DataTableSort[]>({
controlledValue: sorting,
defaultValue: defaultSorting,
onChange: onSortingChange
});
const [currentSelection, setCurrentSelection] = useControllableState<Record<string, boolean>>({
controlledValue: selection,
defaultValue: defaultSelection,
onChange: onSelectionChange
});
const [currentPageIndex, setCurrentPageIndex] = useControllableState({
controlledValue: pageIndex,
defaultValue: defaultPageIndex,
onChange: onPageIndexChange
});
const [currentPageSize, setCurrentPageSize] = useControllableState({
controlledValue: pageSize,
defaultValue: defaultPageSize,
onChange: onPageSizeChange
});
const resolvedPageSizeOptions = [...new Set([...pageSizeOptions, currentPageSize])].sort(
(left, right) => left - right
);
const globalFilterFn: FilterFn<TData> = (row, _columnId, filterValue) => {
const query = String(filterValue ?? "").trim().toLowerCase();
if (query.length === 0) {
return true;
}
return searchableColumns.some((column) =>
getColumnSearchText(row.original, column).toLowerCase().includes(query)
);
};
const tableColumns: ColumnDef<TData>[] = [
...(selectionEnabled
? [
{
cell: ({ row }) => (
<Checkbox
aria-label={`Select row ${row.index + 1}`}
checked={row.getIsSelected()}
onCheckedChange={(checked) => {
row.toggleSelected(Boolean(checked));
}}
/>
),
enableGlobalFilter: false,
enableHiding: false,
enableSorting: false,
header: ({ table }) => (
<Checkbox
aria-label="Select all rows"
checked={table.getIsAllPageRowsSelected()}
onCheckedChange={(checked) => {
table.toggleAllPageRowsSelected(Boolean(checked));
}}
/>
),
id: "__select",
meta: {
align: "center",
width: 56
} satisfies InternalColumnMeta<TData>
} satisfies ColumnDef<TData>
]
: []),
...columns.map((column) => ({
accessorFn: column.accessor
? (row: TData) => getColumnAccessorValue(row, column)
: undefined,
cell: ({ row }: CellContext<TData, unknown>) =>
column.cell
? column.cell(row.original)
: stringifySearchValue(getColumnAccessorValue(row.original, column)),
enableGlobalFilter: searchableColumns.includes(column),
enableSorting: column.sortable ?? false,
header: ({ column: tanstackColumn }: HeaderContext<TData, unknown>) =>
typeof column.header === "function"
? column.header({
column,
sorted: tanstackColumn.getIsSorted()
})
: column.header,
id: column.id,
meta: {
align: column.align ?? "start",
sourceColumn: column,
width: column.width
} satisfies InternalColumnMeta<TData>,
sortDescFirst: false
})),
...(renderRowActions
? [
{
cell: ({ row }) => (
<div {...createSlot("actions")} className="flex items-center justify-end">
{renderRowActions(row.original)}
</div>
),
enableGlobalFilter: false,
enableHiding: false,
enableSorting: false,
header: () => <span className="sr-only">Row actions</span>,
id: "__actions",
meta: {
align: "end",
width: 88
} satisfies InternalColumnMeta<TData>
} satisfies ColumnDef<TData>
]
: [])
];
// eslint-disable-next-line react-hooks/incompatible-library -- TanStack Table is the intentional row-model engine behind this source-owned component.
const table = useReactTable({
autoResetPageIndex: true,
columns: tableColumns,
data: rows,
enableRowSelection: selectionEnabled,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getRowId: (row, index) => {
if (getRowId) {
return getRowId(row, index);
}
const candidateId = (row as { id?: unknown }).id;
if (typeof candidateId === "string" || typeof candidateId === "number") {
return String(candidateId);
}
return String(index);
},
getSortedRowModel: getSortedRowModel(),
globalFilterFn,
onPaginationChange: (updater) => {
const nextValue =
typeof updater === "function"
? updater({
pageIndex: currentPageIndex,
pageSize: currentPageSize
})
: updater;
setCurrentPageIndex(nextValue.pageIndex);
setCurrentPageSize(nextValue.pageSize);
},
onRowSelectionChange: (updater) => {
const nextValue =
typeof updater === "function"
? updater(currentSelection as RowSelectionState)
: updater;
setCurrentSelection(nextValue);
},
onSortingChange: (updater) => {
const nextValue =
typeof updater === "function"
? updater(currentSorting as SortingState)
: updater;
setCurrentSorting(nextValue.map((item) => ({ desc: item.desc, id: item.id })));
},
state: {
globalFilter: currentSearchValue,
pagination: {
pageIndex: currentPageIndex,
pageSize: currentPageSize
} satisfies PaginationState,
rowSelection: currentSelection,
sorting: currentSorting as SortingState
}
});
const totalColumns = table.getAllLeafColumns().length;
const filteredRowCount = table.getFilteredRowModel().rows.length;
const selectedRows = table.getSelectedRowModel().rows.map((row) => row.original);
const pageCount = table.getPageCount();
const isEmpty = !loading && filteredRowCount === 0;
const shouldRenderSearch = searchableColumns.length > 0;
const shouldRenderToolbar = shouldRenderSearch || toolbarActions !== undefined;
const pageStart =
filteredRowCount === 0 ? 0 : currentPageIndex * currentPageSize + 1;
const pageEnd = Math.min((currentPageIndex + 1) * currentPageSize, filteredRowCount);
useEffect(() => {
const maxPageIndex = Math.max(pageCount - 1, 0);
if (currentPageIndex > maxPageIndex) {
setCurrentPageIndex(maxPageIndex);
}
}, [currentPageIndex, pageCount, setCurrentPageIndex]);
return (
<div
{...props}
{...createSlot("root")}
{...createDataAttributes({
empty: isEmpty,
loading,
selected: selectedRows.length > 0
})}
className={cn(dataTableRootVariants(), className)}
ref={ref}
>
{shouldRenderToolbar ? (
<DataTableToolbar>
<div className="flex flex-1 flex-wrap items-center gap-3">
{shouldRenderSearch ? (
<div className={dataTableSearchContainerVariants()}>
<DataTableSearch
aria-label={searchLabel}
onChange={(event) => {
setCurrentSearchValue(event.currentTarget.value);
setCurrentPageIndex(0);
}}
placeholder={searchPlaceholder}
value={currentSearchValue}
/>
</div>
) : null}
</div>
{toolbarActions ? <DataTableFilters>{toolbarActions}</DataTableFilters> : null}
</DataTableToolbar>
) : null}
{selectionEnabled && selectedRows.length > 0 ? (
<DataTableSelectionBar>
<div className="text-sm font-medium text-[var(--color-foreground)]">
{typeof selectionLabel === "function"
? selectionLabel(selectedRows)
: selectionLabel ?? `${selectedRows.length} selected`}
</div>
<div {...createSlot("actions")} className="flex flex-wrap items-center gap-2">
{selectionActions?.(selectedRows)}
<Button
size="sm"
variant="ghost"
onClick={() => {
table.resetRowSelection();
}}
>
Clear selection
</Button>
</div>
</DataTableSelectionBar>
) : null}
<DataTableContent loading={loading}>
{loading ? <DataTableLoading className="sr-only min-h-0 px-0 py-0" /> : null}
<div className="overflow-x-auto">
<DataTableTable aria-label={tableLabel}>
<DataTableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => {
const meta = header.column.columnDef.meta as InternalColumnMeta<TData> | undefined;
const sortState = header.column.getIsSorted();
const align: DataTableAlignment = meta?.align ?? "start";
return (
<DataTableHeaderCell
key={header.id}
align={align}
aria-sort={
sortState === "asc"
? "ascending"
: sortState === "desc"
? "descending"
: undefined
}
scope="col"
sortable={header.column.getCanSort()}
sort={sortState}
style={getColumnWidthStyle(meta?.width)}
>
{header.isPlaceholder ? null : header.column.getCanSort() ? (
<button
className={[
"inline-flex w-full items-center gap-2 rounded-[var(--radius-sm)] px-2 py-1.5",
"outline-none transition-colors duration-200 hover:bg-[var(--color-surface)]",
"focus-visible:ring-2 focus-visible:ring-[var(--color-ring)]",
align === "end" ? "justify-end" : align === "center" ? "justify-center" : "justify-start"
].join(" ")}
onClick={header.column.getToggleSortingHandler()}
type="button"
>
<span>{flexRender(header.column.columnDef.header, header.getContext())}</span>
<span
aria-hidden="true"
className="inline-flex items-center justify-center text-[var(--color-muted-foreground)]"
>
{sortState === "asc" ? (
<SortAscendingIcon className="size-3.5" />
) : sortState === "desc" ? (
<SortDescendingIcon className="size-3.5" />
) : (
<SortUnsortedIcon className="size-3.5" />
)}
</span>
<span className="sr-only">
Sort by{" "}
{meta?.sourceColumn ? getHeaderLabel(meta.sourceColumn) : header.column.id}
</span>
</button>
) : (
flexRender(header.column.columnDef.header, header.getContext())
)}
</DataTableHeaderCell>
);
})}
</tr>
))}
</DataTableHeader>
<DataTableBody>
{loading
? Array.from({ length: loadingRowCount }, (_, index) => (
<DataTableRow
interactive={false}
key={`loading-row-${index}`}
>
{Array.from({ length: totalColumns }, (_, cellIndex) => (
<DataTableCell key={`loading-cell-${cellIndex}`}>
<Skeleton
className={cellIndex === totalColumns - 1 ? "ml-auto w-12" : undefined}
/>
</DataTableCell>
))}
</DataTableRow>
))
: isEmpty
? (
<tr>
<td colSpan={totalColumns}>
{empty ?? (
<DataTableEmpty
description="Try another search, clear filters, or create a new record to repopulate this view."
title="No matching rows"
/>
)}
</td>
</tr>
)
: table.getRowModel().rows.map((row) => (
<DataTableRow
data-selected={row.getIsSelected() ? "" : undefined}
key={row.id}
selected={row.getIsSelected()}
>
{row.getVisibleCells().map((cell) => {
const meta = cell.column.columnDef.meta as InternalColumnMeta<TData> | undefined;
const align: DataTableAlignment = meta?.align ?? "start";
return (
<DataTableCell
align={align}
key={cell.id}
style={getColumnWidthStyle(meta?.width)}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</DataTableCell>
);
})}
</DataTableRow>
))}
</DataTableBody>
</DataTableTable>
</div>
<DataTablePagination>
<div className="text-sm text-[var(--color-muted-foreground)]">
{pageStart}-{pageEnd} of {filteredRowCount}
</div>
<div className="flex flex-wrap items-center gap-3">
{resolvedPageSizeOptions.length > 1 ? (
<div className="flex items-center gap-2">
<span className="text-sm text-[var(--color-muted-foreground)]">Rows</span>
<Select
value={String(currentPageSize)}
onValueChange={(value) => {
setCurrentPageSize(Number(value));
setCurrentPageIndex(0);
}}
>
<SelectTrigger aria-label="Rows per page" className="w-[5.5rem]">
<SelectValue placeholder={String(currentPageSize)} />
</SelectTrigger>
<SelectContent>
{resolvedPageSizeOptions.map((option) => (
<SelectItem key={option} value={String(option)}>
{option}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
) : null}
<div className="text-sm text-[var(--color-muted-foreground)]">
Page {Math.min(currentPageIndex + 1, Math.max(pageCount, 1))} of {Math.max(pageCount, 1)}
</div>
<div className="flex items-center gap-2">
<Button
disabled={!table.getCanPreviousPage()}
size="sm"
variant="ghost"
onClick={() => {
table.previousPage();
}}
>
Previous
</Button>
<Button
disabled={!table.getCanNextPage()}
size="sm"
variant="ghost"
onClick={() => {
table.nextPage();
}}
>
Next
</Button>
</div>
</div>
</DataTablePagination>
</DataTableContent>
</div>
);
}
type DataTableComponent = <TData>(
props: DataTableProps<TData> & { ref?: ForwardedRef<HTMLDivElement> }
) => JSX.Element;
export const DataTable = forwardRef(DataTableInner) as DataTableComponent;
export type DataTableToolbarProps = ComponentPropsWithoutRef<"div">;
export const DataTableToolbar = forwardRef<HTMLDivElement, DataTableToolbarProps>(
function DataTableToolbar({ className, ...props }, ref) {
return (
<div
{...props}
{...createSlot("toolbar")}
className={cn(dataTableToolbarVariants(), className)}
ref={ref}
/>
);
}
);
export type DataTableFiltersProps = ComponentPropsWithoutRef<"div">;
export const DataTableFilters = forwardRef<HTMLDivElement, DataTableFiltersProps>(
function DataTableFilters({ className, ...props }, ref) {
return (
<div
{...props}
{...createSlot("actions")}
className={cn("flex flex-wrap items-center gap-2", className)}
ref={ref}
/>
);
}
);
export type DataTableSearchProps = Omit<InputProps, "size">;
export const DataTableSearch = forwardRef<HTMLInputElement, DataTableSearchProps>(
function DataTableSearch({ className, type = "search", ...props }, ref) {
return <Input {...props} className={className} ref={ref} type={type} />;
}
);
export type DataTableContentProps = ComponentPropsWithoutRef<"div"> &
VariantProps<typeof dataTableContentVariants>;
export const DataTableContent = forwardRef<HTMLDivElement, DataTableContentProps>(
function DataTableContent({ className, loading, ...props }, ref) {
return (
<div
{...props}
{...createSlot("content")}
{...createDataAttributes({ loading })}
className={cn(dataTableContentVariants({ loading }), className)}
ref={ref}
/>
);
}
);
export type DataTableTableProps = ComponentPropsWithoutRef<"table">;
export const DataTableTable = forwardRef<HTMLTableElement, DataTableTableProps>(
function DataTableTable({ className, ...props }, ref) {
return (
<table
{...props}
{...createSlot("table")}
className={cn(dataTableTableVariants(), className)}
ref={ref}
/>
);
}
);
export type DataTableHeaderProps = ComponentPropsWithoutRef<"thead">;
export const DataTableHeader = forwardRef<HTMLTableSectionElement, DataTableHeaderProps>(
function DataTableHeader({ className, ...props }, ref) {
return (
<thead
{...props}
{...createSlot("header")}
className={cn(dataTableHeaderVariants(), className)}
ref={ref}
/>
);
}
);
export type DataTableHeaderCellProps = Omit<ComponentPropsWithoutRef<"th">, "align"> &
VariantProps<typeof dataTableHeaderCellVariants> & {
sort?: "asc" | "desc" | false;
};
export const DataTableHeaderCell = forwardRef<HTMLTableCellElement, DataTableHeaderCellProps>(
function DataTableHeaderCell(
{ align = "start", className, sort = false, sortable = false, ...props },
ref
) {
return (
<th
{...props}
{...createSlot("header")}
{...createDataAttributes({
sort: sort || undefined
})}
className={cn(dataTableHeaderCellVariants({ align, sortable }), className)}
ref={ref}
/>
);
}
);
export type DataTableBodyProps = ComponentPropsWithoutRef<"tbody">;
export const DataTableBody = forwardRef<HTMLTableSectionElement, DataTableBodyProps>(
function DataTableBody({ className, ...props }, ref) {
return (
<tbody
{...props}
className={cn(dataTableBodyVariants(), className)}
ref={ref}
/>
);
}
);
export type DataTableRowProps = ComponentPropsWithoutRef<"tr"> &
VariantProps<typeof dataTableRowVariants>;
export const DataTableRow = forwardRef<HTMLTableRowElement, DataTableRowProps>(
function DataTableRow(
{ className, interactive = true, selected = false, ...props },
ref
) {
return (
<tr
{...props}
{...createSlot("row")}
{...createDataAttributes({
selected
})}
className={cn(dataTableRowVariants({ interactive, selected }), className)}
ref={ref}
/>
);
}
);
export type DataTableCellProps = Omit<ComponentPropsWithoutRef<"td">, "align"> &
VariantProps<typeof dataTableCellVariants>;
export const DataTableCell = forwardRef<HTMLTableCellElement, DataTableCellProps>(
function DataTableCell({ align = "start", className, ...props }, ref) {
return (
<td
{...props}
{...createSlot("cell")}
className={cn(dataTableCellVariants({ align }), className)}
ref={ref}
/>
);
}
);
export type DataTableEmptyProps = ComponentPropsWithoutRef<"div"> & {
actions?: ReactNode;
description?: ReactNode;
title?: ReactNode;
};
export const DataTableEmpty = forwardRef<HTMLDivElement, DataTableEmptyProps>(
function DataTableEmpty(
{
actions,
className,
description = "No rows match the current search or filter state.",
title = "Nothing to show",
...props
},
ref
) {
return (
<div
{...props}
{...createSlot("empty")}
className={cn(dataTableStatusVariants(), className)}
ref={ref}
>
<EmptyState className="w-full border-none bg-transparent p-0 shadow-none">
<EmptyStateHeader>
<EmptyStateTitle>{title}</EmptyStateTitle>
<EmptyStateDescription>{description}</EmptyStateDescription>
</EmptyStateHeader>
{actions ? <EmptyStateActions>{actions}</EmptyStateActions> : null}
</EmptyState>
</div>
);
}
);
export type DataTableLoadingProps = ComponentPropsWithoutRef<"div"> & {
description?: ReactNode;
label?: ReactNode;
};
export const DataTableLoading = forwardRef<HTMLDivElement, DataTableLoadingProps>(
function DataTableLoading(
{
className,
description = "Preparing the next set of rows and controls.",
label = "Loading rows",
...props
},
ref
) {
return (
<div
{...props}
className={cn(dataTableStatusVariants(), className)}
ref={ref}
role="status"
>
<div className="grid justify-items-center gap-3 text-center">
<Spinner aria-label={String(label)} size="lg" tone="primary" />
<div className="space-y-1">
<p className="text-sm font-medium text-[var(--color-foreground)]">{label}</p>
<p className="text-sm leading-6 text-[var(--color-muted-foreground)]">
{description}
</p>
</div>
</div>
</div>
);
}
);
export type DataTablePaginationProps = ComponentPropsWithoutRef<"div">;
export const DataTablePagination = forwardRef<HTMLDivElement, DataTablePaginationProps>(
function DataTablePagination({ className, ...props }, ref) {
return (
<div
{...props}
{...createSlot("pagination")}
className={cn(dataTablePaginationVariants(), className)}
ref={ref}
/>
);
}
);
export type DataTableSelectionBarProps = ComponentPropsWithoutRef<"div">;
export const DataTableSelectionBar = forwardRef<HTMLDivElement, DataTableSelectionBarProps>(
function DataTableSelectionBar({ className, ...props }, ref) {
return (
<div
{...props}
className={cn(dataTableSelectionBarVariants(), className)}
ref={ref}
/>
);
}
);