@zentauri-ui/zentauri-components 2.1.6 → 2.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -8
- package/cli/cli.integration.test.ts +36 -0
- package/cli/index.mjs +43 -7
- package/cli/props.json +462 -3
- package/cli/registry.json +29 -0
- package/cli/rewrite-imports.mjs +29 -4
- package/cli/rewrite-imports.test.ts +35 -0
- package/dist/{chunk-WWKAJHIV.mjs → chunk-4PAHLHYF.mjs} +3 -3
- package/dist/{chunk-WWKAJHIV.mjs.map → chunk-4PAHLHYF.mjs.map} +1 -1
- package/dist/chunk-4SLVTSHM.js +241 -0
- package/dist/chunk-4SLVTSHM.js.map +1 -0
- package/dist/chunk-6OVDBAMI.js +19 -0
- package/dist/{chunk-3W2UUKWP.js.map → chunk-6OVDBAMI.js.map} +1 -1
- package/dist/chunk-74SKXGTM.js +4 -0
- package/dist/chunk-74SKXGTM.js.map +1 -0
- package/dist/{chunk-QE7OJW4J.js → chunk-BAAXQPZ7.js} +6 -6
- package/dist/{chunk-QE7OJW4J.js.map → chunk-BAAXQPZ7.js.map} +1 -1
- package/dist/chunk-CYKSS5S5.mjs +128 -0
- package/dist/chunk-CYKSS5S5.mjs.map +1 -0
- package/dist/chunk-D7ZTSAA6.mjs +221 -0
- package/dist/chunk-D7ZTSAA6.mjs.map +1 -0
- package/dist/{chunk-VA6SB6NN.js → chunk-DPNTQ4AK.js} +73 -6
- package/dist/chunk-DPNTQ4AK.js.map +1 -0
- package/dist/chunk-HMDH4BQJ.js +123 -0
- package/dist/chunk-HMDH4BQJ.js.map +1 -0
- package/dist/chunk-I7EBE7BD.js +98 -0
- package/dist/chunk-I7EBE7BD.js.map +1 -0
- package/dist/chunk-IHDM7AHY.mjs +233 -0
- package/dist/chunk-IHDM7AHY.mjs.map +1 -0
- package/dist/chunk-L5QORCUO.js +225 -0
- package/dist/chunk-L5QORCUO.js.map +1 -0
- package/dist/chunk-LHBJD57K.mjs +143 -0
- package/dist/chunk-LHBJD57K.mjs.map +1 -0
- package/dist/{chunk-CHI6MBTI.mjs → chunk-OWVQVAOY.mjs} +3 -3
- package/dist/{chunk-CHI6MBTI.mjs.map → chunk-OWVQVAOY.mjs.map} +1 -1
- package/dist/chunk-OYAJG2BO.js +83 -0
- package/dist/chunk-OYAJG2BO.js.map +1 -0
- package/dist/chunk-PTU5ZAYX.js +145 -0
- package/dist/chunk-PTU5ZAYX.js.map +1 -0
- package/dist/chunk-QKO5DA4N.mjs +81 -0
- package/dist/chunk-QKO5DA4N.mjs.map +1 -0
- package/dist/chunk-T7PIKDUZ.js +130 -0
- package/dist/chunk-T7PIKDUZ.js.map +1 -0
- package/dist/chunk-TDK5TVJE.mjs +3 -0
- package/dist/chunk-TDK5TVJE.mjs.map +1 -0
- package/dist/{chunk-A4IB3C23.mjs → chunk-UVP3MUBU.mjs} +58 -7
- package/dist/chunk-UVP3MUBU.mjs.map +1 -0
- package/dist/chunk-VBNW2B4D.mjs +3 -0
- package/dist/chunk-VBNW2B4D.mjs.map +1 -0
- package/dist/chunk-W6DO36XD.mjs +96 -0
- package/dist/chunk-W6DO36XD.mjs.map +1 -0
- package/dist/chunk-XR3J46TZ.js +4 -0
- package/dist/chunk-XR3J46TZ.js.map +1 -0
- package/dist/chunk-ZOHCADDL.mjs +121 -0
- package/dist/chunk-ZOHCADDL.mjs.map +1 -0
- package/dist/design-system/data-table.d.ts +8 -0
- package/dist/design-system/data-table.d.ts.map +1 -0
- package/dist/design-system/facade.js +6 -6
- package/dist/design-system/facade.mjs +5 -5
- package/dist/design-system/index.d.ts +2 -0
- package/dist/design-system/index.d.ts.map +1 -1
- package/dist/design-system/split-button.d.ts +25 -0
- package/dist/design-system/split-button.d.ts.map +1 -0
- package/dist/hooks/useTableFilter.js +6 -116
- package/dist/hooks/useTableFilter.js.map +1 -1
- package/dist/hooks/useTableFilter.mjs +1 -118
- package/dist/hooks/useTableFilter.mjs.map +1 -1
- package/dist/hooks/useTableSort.js +6 -91
- package/dist/hooks/useTableSort.js.map +1 -1
- package/dist/hooks/useTableSort.mjs +1 -93
- package/dist/hooks/useTableSort.mjs.map +1 -1
- package/dist/hooks/useVirtualList.js +6 -76
- package/dist/hooks/useVirtualList.js.map +1 -1
- package/dist/hooks/useVirtualList.mjs +1 -78
- package/dist/hooks/useVirtualList.mjs.map +1 -1
- package/dist/ui/buttons/animated.js +8 -8
- package/dist/ui/buttons/animated.mjs +6 -6
- package/dist/ui/buttons.js +10 -9
- package/dist/ui/buttons.mjs +8 -7
- package/dist/ui/checkbox.js +7 -123
- package/dist/ui/checkbox.js.map +1 -1
- package/dist/ui/checkbox.mjs +2 -126
- package/dist/ui/checkbox.mjs.map +1 -1
- package/dist/ui/data-table/data-table-base.d.ts +6 -0
- package/dist/ui/data-table/data-table-base.d.ts.map +1 -0
- package/dist/ui/data-table/data-table.d.ts +6 -0
- package/dist/ui/data-table/data-table.d.ts.map +1 -0
- package/dist/ui/data-table/index.d.ts +4 -0
- package/dist/ui/data-table/index.d.ts.map +1 -0
- package/dist/ui/data-table/types.d.ts +92 -0
- package/dist/ui/data-table/types.d.ts.map +1 -0
- package/dist/ui/data-table/variants.d.ts +8 -0
- package/dist/ui/data-table/variants.d.ts.map +1 -0
- package/dist/ui/data-table.js +620 -0
- package/dist/ui/data-table.js.map +1 -0
- package/dist/ui/data-table.mjs +611 -0
- package/dist/ui/data-table.mjs.map +1 -0
- package/dist/ui/dropdown/dropdown.d.ts +1 -1
- package/dist/ui/dropdown/dropdown.d.ts.map +1 -1
- package/dist/ui/dropdown/types.d.ts +2 -2
- package/dist/ui/dropdown/types.d.ts.map +1 -1
- package/dist/ui/dropdown.js +31 -231
- package/dist/ui/dropdown.js.map +1 -1
- package/dist/ui/dropdown.mjs +4 -229
- package/dist/ui/dropdown.mjs.map +1 -1
- package/dist/ui/dynamic-stepper.js +18 -18
- package/dist/ui/dynamic-stepper.mjs +7 -7
- package/dist/ui/inputs.js +7 -138
- package/dist/ui/inputs.js.map +1 -1
- package/dist/ui/inputs.mjs +2 -141
- package/dist/ui/inputs.mjs.map +1 -1
- package/dist/ui/pagination.js +20 -221
- package/dist/ui/pagination.js.map +1 -1
- package/dist/ui/pagination.mjs +8 -223
- package/dist/ui/pagination.mjs.map +1 -1
- package/dist/ui/split-button/index.d.ts +4 -0
- package/dist/ui/split-button/index.d.ts.map +1 -0
- package/dist/ui/split-button/split-button-base.d.ts +6 -0
- package/dist/ui/split-button/split-button-base.d.ts.map +1 -0
- package/dist/ui/split-button/split-button.d.ts +6 -0
- package/dist/ui/split-button/split-button.d.ts.map +1 -0
- package/dist/ui/split-button/types.d.ts +30 -0
- package/dist/ui/split-button/types.d.ts.map +1 -0
- package/dist/ui/split-button/variants.d.ts +16 -0
- package/dist/ui/split-button/variants.d.ts.map +1 -0
- package/dist/ui/split-button.js +287 -0
- package/dist/ui/split-button.js.map +1 -0
- package/dist/ui/split-button.mjs +278 -0
- package/dist/ui/split-button.mjs.map +1 -0
- package/dist/ui/table.js +1 -0
- package/dist/ui/table.mjs +1 -0
- package/package.json +1 -1
- package/src/design-system/data-table.ts +20 -0
- package/src/design-system/index.ts +2 -0
- package/src/design-system/split-button.ts +38 -0
- package/src/ui/data-table/data-table-base.tsx +701 -0
- package/src/ui/data-table/data-table.test.tsx +389 -0
- package/src/ui/data-table/data-table.tsx +11 -0
- package/src/ui/data-table/index.ts +24 -0
- package/src/ui/data-table/types.ts +121 -0
- package/src/ui/data-table/variants.ts +21 -0
- package/src/ui/dropdown/dropdown.tsx +7 -3
- package/src/ui/dropdown/types.ts +2 -2
- package/src/ui/split-button/index.ts +19 -0
- package/src/ui/split-button/split-button-base.tsx +232 -0
- package/src/ui/split-button/split-button.test.tsx +208 -0
- package/src/ui/split-button/split-button.tsx +9 -0
- package/src/ui/split-button/types.ts +46 -0
- package/src/ui/split-button/variants.ts +46 -0
- package/dist/chunk-3W2UUKWP.js +0 -19
- package/dist/chunk-A4IB3C23.mjs.map +0 -1
- package/dist/chunk-VA6SB6NN.js.map +0 -1
|
@@ -0,0 +1,701 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useId, useMemo, useState } from "react";
|
|
4
|
+
|
|
5
|
+
import { usePagination } from "../../hooks/usePagination";
|
|
6
|
+
import { useTableFilter } from "../../hooks/useTableFilter";
|
|
7
|
+
import { useTableSort } from "../../hooks/useTableSort";
|
|
8
|
+
import { useVirtualList } from "../../hooks/useVirtualList";
|
|
9
|
+
import { cn } from "../../lib/utils";
|
|
10
|
+
import { Button } from "../buttons";
|
|
11
|
+
import { Checkbox } from "../checkbox";
|
|
12
|
+
import { Input } from "../inputs";
|
|
13
|
+
import { Pagination } from "../pagination";
|
|
14
|
+
import type { PaginationAppearance } from "../pagination";
|
|
15
|
+
import {
|
|
16
|
+
Table,
|
|
17
|
+
TableBody,
|
|
18
|
+
TableCaption,
|
|
19
|
+
TableCell,
|
|
20
|
+
TableHead,
|
|
21
|
+
TableHeader,
|
|
22
|
+
TableRow,
|
|
23
|
+
} from "../table";
|
|
24
|
+
|
|
25
|
+
import type {
|
|
26
|
+
DataTableColumn,
|
|
27
|
+
DataTableColumnValue,
|
|
28
|
+
DataTableProps,
|
|
29
|
+
DataTableSearchOptions,
|
|
30
|
+
} from "./types";
|
|
31
|
+
import {
|
|
32
|
+
dataTableColumnPanelVariants,
|
|
33
|
+
dataTableRootVariants,
|
|
34
|
+
dataTableStateCellVariants,
|
|
35
|
+
dataTableStatusVariants,
|
|
36
|
+
dataTableToolbarGroupVariants,
|
|
37
|
+
dataTableToolbarVariants,
|
|
38
|
+
dataTableVirtualScrollVariants,
|
|
39
|
+
} from "./variants";
|
|
40
|
+
|
|
41
|
+
const defaultLoadingContent = "Loading data";
|
|
42
|
+
const defaultEmptyContent = "No results found";
|
|
43
|
+
const globalFilterKey = "__zui_global_filter__";
|
|
44
|
+
|
|
45
|
+
function getAccessorValue<TData>(
|
|
46
|
+
row: TData,
|
|
47
|
+
column: DataTableColumn<TData>,
|
|
48
|
+
): unknown {
|
|
49
|
+
if (typeof column.accessor === "function") {
|
|
50
|
+
return column.accessor(row);
|
|
51
|
+
}
|
|
52
|
+
if (column.accessor && row && typeof row === "object") {
|
|
53
|
+
return (row as Record<PropertyKey, unknown>)[column.accessor];
|
|
54
|
+
}
|
|
55
|
+
if (row && typeof row === "object" && column.id in row) {
|
|
56
|
+
return (row as Record<string, unknown>)[column.id];
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizeComparable(value: DataTableColumnValue): string | number {
|
|
62
|
+
if (value instanceof Date) {
|
|
63
|
+
return value.getTime();
|
|
64
|
+
}
|
|
65
|
+
if (typeof value === "number") {
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
if (typeof value === "boolean") {
|
|
69
|
+
return value ? 1 : 0;
|
|
70
|
+
}
|
|
71
|
+
return value == null ? "" : String(value).toLowerCase();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function compareValues(
|
|
75
|
+
left: DataTableColumnValue,
|
|
76
|
+
right: DataTableColumnValue,
|
|
77
|
+
): number {
|
|
78
|
+
const normalizedLeft = normalizeComparable(left);
|
|
79
|
+
const normalizedRight = normalizeComparable(right);
|
|
80
|
+
|
|
81
|
+
if (
|
|
82
|
+
typeof normalizedLeft === "number" &&
|
|
83
|
+
typeof normalizedRight === "number"
|
|
84
|
+
) {
|
|
85
|
+
return normalizedLeft - normalizedRight;
|
|
86
|
+
}
|
|
87
|
+
return String(normalizedLeft).localeCompare(String(normalizedRight));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function resolveSearchOptions<TKey extends string>(
|
|
91
|
+
search: DataTableProps<unknown, TKey>["search"],
|
|
92
|
+
): DataTableSearchOptions<TKey> | null {
|
|
93
|
+
if (!search) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
if (search === true) {
|
|
97
|
+
return {};
|
|
98
|
+
}
|
|
99
|
+
return search;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function validateSearchOptions<TKey extends string>(
|
|
103
|
+
searchOptions: DataTableSearchOptions<TKey> | null,
|
|
104
|
+
) {
|
|
105
|
+
if (
|
|
106
|
+
searchOptions?.filterColumnIds &&
|
|
107
|
+
searchOptions.filterColumnIds.length === 0
|
|
108
|
+
) {
|
|
109
|
+
console.warn(
|
|
110
|
+
"DataTable search.filterColumnIds is empty. It should include at least one column id.",
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function isSelected(selectedRowIds: readonly string[], rowId: string) {
|
|
116
|
+
return selectedRowIds.includes(rowId);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function toggleId(ids: readonly string[], id: string, checked: boolean) {
|
|
120
|
+
if (checked) {
|
|
121
|
+
return ids.includes(id) ? [...ids] : [...ids, id];
|
|
122
|
+
}
|
|
123
|
+
return ids.filter((item) => item !== id);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function textAlignClass(textAlign: "left" | "center" | "right" | undefined) {
|
|
127
|
+
if (textAlign === "center") {
|
|
128
|
+
return "text-center";
|
|
129
|
+
}
|
|
130
|
+
if (textAlign === "right") {
|
|
131
|
+
return "text-right";
|
|
132
|
+
}
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function paginationAppearanceFor(
|
|
137
|
+
appearance: NonNullable<DataTableProps<unknown>["appearance"]>,
|
|
138
|
+
): PaginationAppearance {
|
|
139
|
+
if (
|
|
140
|
+
appearance === "striped" ||
|
|
141
|
+
appearance === "bordered" ||
|
|
142
|
+
appearance === "ghost"
|
|
143
|
+
) {
|
|
144
|
+
return "default";
|
|
145
|
+
}
|
|
146
|
+
return appearance as PaginationAppearance;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function uniqueVisibleColumnIds<TData, TKey extends string>(
|
|
150
|
+
columns: readonly DataTableColumn<TData, TKey>[],
|
|
151
|
+
ids: readonly TKey[] | undefined,
|
|
152
|
+
) {
|
|
153
|
+
const columnIds = new Set(columns.map((column) => column.id));
|
|
154
|
+
const initialIds =
|
|
155
|
+
ids ??
|
|
156
|
+
columns
|
|
157
|
+
.filter((column) => column.visible !== false)
|
|
158
|
+
.map((column) => column.id);
|
|
159
|
+
|
|
160
|
+
return initialIds.filter((id, index) => {
|
|
161
|
+
return columnIds.has(id) && initialIds.indexOf(id) === index;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function DataTableBase<TData, TKey extends string = string>(
|
|
166
|
+
props: DataTableProps<TData, TKey>,
|
|
167
|
+
) {
|
|
168
|
+
const {
|
|
169
|
+
className,
|
|
170
|
+
columns,
|
|
171
|
+
data,
|
|
172
|
+
getRowId,
|
|
173
|
+
caption,
|
|
174
|
+
tableClassName,
|
|
175
|
+
tableScrollAreaAriaLabel,
|
|
176
|
+
appearance = "default",
|
|
177
|
+
size = "md",
|
|
178
|
+
stickyHeader,
|
|
179
|
+
textAlign,
|
|
180
|
+
search,
|
|
181
|
+
sortKey,
|
|
182
|
+
defaultSortKey,
|
|
183
|
+
sortDirection,
|
|
184
|
+
defaultSortDirection = "none",
|
|
185
|
+
onSortChange,
|
|
186
|
+
enableRowSelection = false,
|
|
187
|
+
selectedRowIds,
|
|
188
|
+
defaultSelectedRowIds = [],
|
|
189
|
+
onRowSelectionChange,
|
|
190
|
+
enableColumnVisibility = false,
|
|
191
|
+
visibleColumnIds,
|
|
192
|
+
defaultVisibleColumnIds,
|
|
193
|
+
onColumnVisibilityChange,
|
|
194
|
+
bulkActions = [],
|
|
195
|
+
pagination,
|
|
196
|
+
virtualization,
|
|
197
|
+
showRowCount = true,
|
|
198
|
+
loading = false,
|
|
199
|
+
loadingContent = defaultLoadingContent,
|
|
200
|
+
emptyContent = defaultEmptyContent,
|
|
201
|
+
ref,
|
|
202
|
+
"aria-label": ariaLabel = "Data table",
|
|
203
|
+
...rest
|
|
204
|
+
} = props;
|
|
205
|
+
const searchOptions = resolveSearchOptions(search);
|
|
206
|
+
validateSearchOptions(searchOptions);
|
|
207
|
+
|
|
208
|
+
// Pair every row with a stable id derived from its position in `data` (or the
|
|
209
|
+
// caller's getRowId). Threading the id alongside each row through the
|
|
210
|
+
// filter/sort/paginate pipeline keeps it correct even when `data` contains
|
|
211
|
+
// duplicate references or primitive values — a Map keyed by the row would
|
|
212
|
+
// collapse those into a single id, breaking React keys and selection.
|
|
213
|
+
type KeyedRow = { row: TData; id: string };
|
|
214
|
+
const keyedData = useMemo<KeyedRow[]>(
|
|
215
|
+
() =>
|
|
216
|
+
data.map((row, index) => ({
|
|
217
|
+
row,
|
|
218
|
+
id: getRowId ? getRowId(row, index) : String(index),
|
|
219
|
+
})),
|
|
220
|
+
[data, getRowId],
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
const resolvedAppearance = appearance ?? "default";
|
|
224
|
+
const resolvedSize = size ?? "md";
|
|
225
|
+
const resolvedPaginationAppearance =
|
|
226
|
+
paginationAppearanceFor(resolvedAppearance);
|
|
227
|
+
const columnPanelId = useId();
|
|
228
|
+
const [columnPanelOpen, setColumnPanelOpen] = useState(false);
|
|
229
|
+
|
|
230
|
+
const [internalSelectedRowIds, setInternalSelectedRowIds] = useState(() => [
|
|
231
|
+
...defaultSelectedRowIds,
|
|
232
|
+
]);
|
|
233
|
+
const selectedIds = useMemo(
|
|
234
|
+
() => (selectedRowIds ? [...selectedRowIds] : internalSelectedRowIds),
|
|
235
|
+
[internalSelectedRowIds, selectedRowIds],
|
|
236
|
+
);
|
|
237
|
+
const isSelectionControlled = selectedRowIds !== undefined;
|
|
238
|
+
|
|
239
|
+
const [internalVisibleColumnIds, setInternalVisibleColumnIds] = useState<
|
|
240
|
+
TKey[]
|
|
241
|
+
>(() => uniqueVisibleColumnIds(columns, defaultVisibleColumnIds));
|
|
242
|
+
const currentVisibleColumnIds = visibleColumnIds
|
|
243
|
+
? uniqueVisibleColumnIds(columns, visibleColumnIds)
|
|
244
|
+
: uniqueVisibleColumnIds(columns, internalVisibleColumnIds);
|
|
245
|
+
const isVisibilityControlled = visibleColumnIds !== undefined;
|
|
246
|
+
|
|
247
|
+
const visibleColumns = useMemo(
|
|
248
|
+
() =>
|
|
249
|
+
columns.filter((column) => currentVisibleColumnIds.includes(column.id)),
|
|
250
|
+
[columns, currentVisibleColumnIds],
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
const [internalSearchValue, setInternalSearchValue] = useState(
|
|
254
|
+
() => searchOptions?.defaultValue ?? "",
|
|
255
|
+
);
|
|
256
|
+
const searchValue = searchOptions?.value ?? internalSearchValue;
|
|
257
|
+
const isSearchControlled = searchOptions?.value !== undefined;
|
|
258
|
+
|
|
259
|
+
const searchableColumns = useMemo(() => {
|
|
260
|
+
const filterColumnIds = searchOptions?.filterColumnIds;
|
|
261
|
+
const hasFilterColumns = Boolean(
|
|
262
|
+
filterColumnIds && filterColumnIds.length > 0,
|
|
263
|
+
);
|
|
264
|
+
return visibleColumns.filter((column) => {
|
|
265
|
+
if (hasFilterColumns && !filterColumnIds?.includes(column.id)) {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
return column.filterable !== false;
|
|
269
|
+
});
|
|
270
|
+
}, [searchOptions?.filterColumnIds, visibleColumns]);
|
|
271
|
+
|
|
272
|
+
const filterState = searchValue
|
|
273
|
+
? { [globalFilterKey]: searchValue }
|
|
274
|
+
: undefined;
|
|
275
|
+
|
|
276
|
+
const { filteredData } = useTableFilter<KeyedRow, string>({
|
|
277
|
+
data: keyedData,
|
|
278
|
+
filters: filterState,
|
|
279
|
+
filterPredicate: ({ row }, filterValue) => {
|
|
280
|
+
const query = filterValue.trim().toLowerCase();
|
|
281
|
+
if (!query) {
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
return searchableColumns.some((column) => {
|
|
285
|
+
const value = column.filterValue
|
|
286
|
+
? column.filterValue(row)
|
|
287
|
+
: getAccessorValue(row, column);
|
|
288
|
+
return String(value ?? "")
|
|
289
|
+
.toLowerCase()
|
|
290
|
+
.includes(query);
|
|
291
|
+
});
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
const tableSort = useTableSort<TKey>({
|
|
296
|
+
sortKey,
|
|
297
|
+
defaultSortKey,
|
|
298
|
+
sortDirection,
|
|
299
|
+
defaultSortDirection,
|
|
300
|
+
onSortChange,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const sortedData = useMemo(() => {
|
|
304
|
+
if (!tableSort.sortKey || tableSort.sortDirection === "none") {
|
|
305
|
+
return filteredData;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const sortedColumn = columns.find(
|
|
309
|
+
(column) => column.id === tableSort.sortKey,
|
|
310
|
+
);
|
|
311
|
+
if (!sortedColumn || !sortedColumn.sortable) {
|
|
312
|
+
return filteredData;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const direction = tableSort.sortDirection === "ascending" ? 1 : -1;
|
|
316
|
+
return [...filteredData].sort((left, right) => {
|
|
317
|
+
const leftValue = sortedColumn.sortValue
|
|
318
|
+
? sortedColumn.sortValue(left.row)
|
|
319
|
+
: (getAccessorValue(left.row, sortedColumn) as DataTableColumnValue);
|
|
320
|
+
const rightValue = sortedColumn.sortValue
|
|
321
|
+
? sortedColumn.sortValue(right.row)
|
|
322
|
+
: (getAccessorValue(right.row, sortedColumn) as DataTableColumnValue);
|
|
323
|
+
|
|
324
|
+
return compareValues(leftValue, rightValue) * direction;
|
|
325
|
+
});
|
|
326
|
+
}, [columns, filteredData, tableSort.sortDirection, tableSort.sortKey]);
|
|
327
|
+
|
|
328
|
+
const paginationOptions =
|
|
329
|
+
pagination && pagination !== true ? pagination : undefined;
|
|
330
|
+
const paginationEnabled = Boolean(pagination);
|
|
331
|
+
const pageSize = Math.max(1, paginationOptions?.pageSize ?? 10);
|
|
332
|
+
const pageCount = paginationEnabled
|
|
333
|
+
? Math.ceil(sortedData.length / pageSize)
|
|
334
|
+
: 1;
|
|
335
|
+
const paginationState = usePagination({
|
|
336
|
+
pageCount,
|
|
337
|
+
page: paginationOptions?.page,
|
|
338
|
+
defaultPage: paginationOptions?.defaultPage,
|
|
339
|
+
siblingCount: paginationOptions?.siblingCount,
|
|
340
|
+
boundaryCount: paginationOptions?.boundaryCount,
|
|
341
|
+
onPageChange: paginationOptions?.onPageChange,
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
const processedRows = paginationEnabled
|
|
345
|
+
? sortedData.slice(
|
|
346
|
+
(paginationState.currentPage - 1) * pageSize,
|
|
347
|
+
paginationState.currentPage * pageSize,
|
|
348
|
+
)
|
|
349
|
+
: sortedData;
|
|
350
|
+
|
|
351
|
+
const virtualizationEnabled =
|
|
352
|
+
Boolean(virtualization?.enabled) && processedRows.length > 0;
|
|
353
|
+
const virtualList = useVirtualList({
|
|
354
|
+
itemCount: processedRows.length,
|
|
355
|
+
itemHeight: virtualization?.rowHeight ?? 48,
|
|
356
|
+
overscan: virtualization?.overscan,
|
|
357
|
+
});
|
|
358
|
+
const renderedRows = virtualizationEnabled
|
|
359
|
+
? virtualList.virtualItems.map((item) => ({
|
|
360
|
+
entry: processedRows[item.index] as KeyedRow,
|
|
361
|
+
index: item.index,
|
|
362
|
+
start: item.start,
|
|
363
|
+
}))
|
|
364
|
+
: processedRows.map((entry, index) => ({ entry, index, start: 0 }));
|
|
365
|
+
const virtualOffset = virtualizationEnabled
|
|
366
|
+
? (virtualList.virtualItems[0]?.start ?? 0)
|
|
367
|
+
: 0;
|
|
368
|
+
|
|
369
|
+
const selectableRows = useMemo(
|
|
370
|
+
() =>
|
|
371
|
+
processedRows.map((entry) => ({
|
|
372
|
+
row: entry.row,
|
|
373
|
+
id: entry.id,
|
|
374
|
+
})),
|
|
375
|
+
[processedRows],
|
|
376
|
+
);
|
|
377
|
+
const selectedRows = useMemo(
|
|
378
|
+
() =>
|
|
379
|
+
keyedData
|
|
380
|
+
.filter((entry) => isSelected(selectedIds, entry.id))
|
|
381
|
+
.map((entry) => entry.row),
|
|
382
|
+
[keyedData, selectedIds],
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
const allVisibleSelected =
|
|
386
|
+
selectableRows.length > 0 &&
|
|
387
|
+
selectableRows.every((item) => isSelected(selectedIds, item.id));
|
|
388
|
+
const someVisibleSelected =
|
|
389
|
+
selectableRows.some((item) => isSelected(selectedIds, item.id)) &&
|
|
390
|
+
!allVisibleSelected;
|
|
391
|
+
|
|
392
|
+
const setSelectedIds = useCallback(
|
|
393
|
+
(nextIds: string[]) => {
|
|
394
|
+
if (!isSelectionControlled) {
|
|
395
|
+
setInternalSelectedRowIds(nextIds);
|
|
396
|
+
}
|
|
397
|
+
const nextRows = keyedData
|
|
398
|
+
.filter((entry) => nextIds.includes(entry.id))
|
|
399
|
+
.map((entry) => entry.row);
|
|
400
|
+
onRowSelectionChange?.(nextIds, nextRows);
|
|
401
|
+
},
|
|
402
|
+
[keyedData, isSelectionControlled, onRowSelectionChange],
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
const setColumnVisible = useCallback(
|
|
406
|
+
(columnId: TKey, checked: boolean) => {
|
|
407
|
+
const nextIds = checked
|
|
408
|
+
? [...currentVisibleColumnIds, columnId]
|
|
409
|
+
: currentVisibleColumnIds.filter((id) => id !== columnId);
|
|
410
|
+
const normalized = uniqueVisibleColumnIds(columns, nextIds);
|
|
411
|
+
if (!isVisibilityControlled) {
|
|
412
|
+
setInternalVisibleColumnIds(normalized);
|
|
413
|
+
}
|
|
414
|
+
onColumnVisibilityChange?.(normalized);
|
|
415
|
+
},
|
|
416
|
+
[
|
|
417
|
+
columns,
|
|
418
|
+
currentVisibleColumnIds,
|
|
419
|
+
isVisibilityControlled,
|
|
420
|
+
onColumnVisibilityChange,
|
|
421
|
+
],
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
const handleSearchChange = useCallback(
|
|
425
|
+
(value: string) => {
|
|
426
|
+
if (!isSearchControlled) {
|
|
427
|
+
setInternalSearchValue(value);
|
|
428
|
+
}
|
|
429
|
+
searchOptions?.onValueChange?.(value);
|
|
430
|
+
},
|
|
431
|
+
[isSearchControlled, searchOptions],
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
const headerColSpan =
|
|
435
|
+
visibleColumns.length + (enableRowSelection ? 1 : 0) || 1;
|
|
436
|
+
|
|
437
|
+
const renderStateRow = (content: React.ReactNode) => (
|
|
438
|
+
<TableRow>
|
|
439
|
+
<TableCell
|
|
440
|
+
colSpan={headerColSpan}
|
|
441
|
+
className={dataTableStateCellVariants()}
|
|
442
|
+
>
|
|
443
|
+
{content}
|
|
444
|
+
</TableCell>
|
|
445
|
+
</TableRow>
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
const renderRow = (entry: KeyedRow, rowIndex: number) => {
|
|
449
|
+
const { row, id: rowId } = entry;
|
|
450
|
+
const rowSelected = isSelected(selectedIds, rowId);
|
|
451
|
+
const labelColumn = visibleColumns[0] ?? columns[0];
|
|
452
|
+
const rowSelectionLabel = labelColumn
|
|
453
|
+
? String(getAccessorValue(row, labelColumn) ?? rowId)
|
|
454
|
+
: rowId;
|
|
455
|
+
|
|
456
|
+
return (
|
|
457
|
+
<TableRow key={rowId} data-state={rowSelected ? "selected" : undefined}>
|
|
458
|
+
{enableRowSelection ? (
|
|
459
|
+
<TableCell className="w-10">
|
|
460
|
+
<Checkbox
|
|
461
|
+
aria-label={`Select ${rowSelectionLabel}`}
|
|
462
|
+
checked={rowSelected}
|
|
463
|
+
onCheckedChange={(checked) => {
|
|
464
|
+
setSelectedIds(toggleId(selectedIds, rowId, checked));
|
|
465
|
+
}}
|
|
466
|
+
/>
|
|
467
|
+
</TableCell>
|
|
468
|
+
) : null}
|
|
469
|
+
{visibleColumns.map((column, columnIndex) => {
|
|
470
|
+
const value = getAccessorValue(row, column);
|
|
471
|
+
const content = column.cell
|
|
472
|
+
? column.cell({ row, value, column, rowIndex })
|
|
473
|
+
: String(value ?? "");
|
|
474
|
+
const isRowHeader = columnIndex === 0;
|
|
475
|
+
|
|
476
|
+
return (
|
|
477
|
+
<TableCell
|
|
478
|
+
key={column.id}
|
|
479
|
+
scope={isRowHeader ? "row" : undefined}
|
|
480
|
+
className={cn(
|
|
481
|
+
textAlignClass(column.textAlign),
|
|
482
|
+
column.className,
|
|
483
|
+
column.cellClassName,
|
|
484
|
+
)}
|
|
485
|
+
{...column.cellProps}
|
|
486
|
+
>
|
|
487
|
+
{content}
|
|
488
|
+
</TableCell>
|
|
489
|
+
);
|
|
490
|
+
})}
|
|
491
|
+
</TableRow>
|
|
492
|
+
);
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
const tableElement = (
|
|
496
|
+
<Table
|
|
497
|
+
aria-label={ariaLabel}
|
|
498
|
+
appearance={resolvedAppearance}
|
|
499
|
+
size={resolvedSize}
|
|
500
|
+
stickyHeader={stickyHeader}
|
|
501
|
+
textAlign={textAlign}
|
|
502
|
+
scrollAreaAriaLabel={tableScrollAreaAriaLabel}
|
|
503
|
+
className={tableClassName}
|
|
504
|
+
>
|
|
505
|
+
{caption ? <TableCaption>{caption}</TableCaption> : null}
|
|
506
|
+
<TableHeader>
|
|
507
|
+
<TableRow>
|
|
508
|
+
{enableRowSelection ? (
|
|
509
|
+
<TableHead className="w-10">
|
|
510
|
+
<Checkbox
|
|
511
|
+
aria-label="Select all rows"
|
|
512
|
+
checked={allVisibleSelected}
|
|
513
|
+
indeterminate={someVisibleSelected}
|
|
514
|
+
onCheckedChange={(checked) => {
|
|
515
|
+
const visibleIds = selectableRows.map((item) => item.id);
|
|
516
|
+
const nextIds = checked
|
|
517
|
+
? [...new Set([...selectedIds, ...visibleIds])]
|
|
518
|
+
: selectedIds.filter((id) => !visibleIds.includes(id));
|
|
519
|
+
setSelectedIds(nextIds);
|
|
520
|
+
}}
|
|
521
|
+
/>
|
|
522
|
+
</TableHead>
|
|
523
|
+
) : null}
|
|
524
|
+
{visibleColumns.map((column) => {
|
|
525
|
+
const header =
|
|
526
|
+
typeof column.header === "function"
|
|
527
|
+
? column.header({ column })
|
|
528
|
+
: column.header;
|
|
529
|
+
return (
|
|
530
|
+
<TableHead
|
|
531
|
+
key={column.id}
|
|
532
|
+
className={cn(
|
|
533
|
+
textAlignClass(column.textAlign),
|
|
534
|
+
column.className,
|
|
535
|
+
column.headerClassName,
|
|
536
|
+
)}
|
|
537
|
+
{...(column.sortable ? tableSort.getSortProps(column.id) : {})}
|
|
538
|
+
{...column.headerProps}
|
|
539
|
+
>
|
|
540
|
+
{header}
|
|
541
|
+
</TableHead>
|
|
542
|
+
);
|
|
543
|
+
})}
|
|
544
|
+
</TableRow>
|
|
545
|
+
</TableHeader>
|
|
546
|
+
<TableBody>
|
|
547
|
+
{loading
|
|
548
|
+
? renderStateRow(loadingContent)
|
|
549
|
+
: renderedRows.length === 0
|
|
550
|
+
? renderStateRow(emptyContent)
|
|
551
|
+
: renderedRows.map(({ entry, index }) => renderRow(entry, index))}
|
|
552
|
+
</TableBody>
|
|
553
|
+
</Table>
|
|
554
|
+
);
|
|
555
|
+
|
|
556
|
+
return (
|
|
557
|
+
<section
|
|
558
|
+
ref={ref}
|
|
559
|
+
data-slot="data-table"
|
|
560
|
+
className={cn(dataTableRootVariants(), className)}
|
|
561
|
+
{...rest}
|
|
562
|
+
>
|
|
563
|
+
{searchOptions || enableColumnVisibility || bulkActions.length > 0 ? (
|
|
564
|
+
<div
|
|
565
|
+
data-slot="data-table-toolbar"
|
|
566
|
+
className={dataTableToolbarVariants()}
|
|
567
|
+
>
|
|
568
|
+
<div
|
|
569
|
+
data-slot="data-table-toolbar-primary"
|
|
570
|
+
className={dataTableToolbarGroupVariants()}
|
|
571
|
+
>
|
|
572
|
+
{searchOptions ? (
|
|
573
|
+
<Input
|
|
574
|
+
as="input"
|
|
575
|
+
type="search"
|
|
576
|
+
aria-label={searchOptions.label ?? "Search table"}
|
|
577
|
+
placeholder={searchOptions.placeholder ?? "Search table"}
|
|
578
|
+
value={searchValue}
|
|
579
|
+
onChange={(event) =>
|
|
580
|
+
handleSearchChange(event.currentTarget.value)
|
|
581
|
+
}
|
|
582
|
+
className="min-w-56"
|
|
583
|
+
/>
|
|
584
|
+
) : null}
|
|
585
|
+
{selectedRows.length > 0 ? (
|
|
586
|
+
<span className={dataTableStatusVariants()} aria-live="polite">
|
|
587
|
+
{selectedRows.length} selected
|
|
588
|
+
</span>
|
|
589
|
+
) : null}
|
|
590
|
+
</div>
|
|
591
|
+
<div
|
|
592
|
+
data-slot="data-table-toolbar-actions"
|
|
593
|
+
className={dataTableToolbarGroupVariants()}
|
|
594
|
+
>
|
|
595
|
+
{bulkActions.map((action, index) => (
|
|
596
|
+
<Button
|
|
597
|
+
key={index}
|
|
598
|
+
type="button"
|
|
599
|
+
appearance="outline"
|
|
600
|
+
size="sm"
|
|
601
|
+
disabled={action.disabled || selectedRows.length === 0}
|
|
602
|
+
onClick={() => action.onSelect(selectedRows)}
|
|
603
|
+
>
|
|
604
|
+
{action.label}
|
|
605
|
+
</Button>
|
|
606
|
+
))}
|
|
607
|
+
{enableColumnVisibility ? (
|
|
608
|
+
<div className="relative">
|
|
609
|
+
<Button
|
|
610
|
+
type="button"
|
|
611
|
+
appearance="outline"
|
|
612
|
+
size="sm"
|
|
613
|
+
aria-expanded={columnPanelOpen}
|
|
614
|
+
aria-controls={columnPanelId}
|
|
615
|
+
onClick={() => setColumnPanelOpen((open) => !open)}
|
|
616
|
+
>
|
|
617
|
+
Columns
|
|
618
|
+
</Button>
|
|
619
|
+
{columnPanelOpen ? (
|
|
620
|
+
<div
|
|
621
|
+
id={columnPanelId}
|
|
622
|
+
data-slot="data-table-column-panel"
|
|
623
|
+
className={dataTableColumnPanelVariants()}
|
|
624
|
+
>
|
|
625
|
+
{columns.map((column) => {
|
|
626
|
+
const canHide = column.enableHiding !== false;
|
|
627
|
+
return (
|
|
628
|
+
<Checkbox
|
|
629
|
+
key={column.id}
|
|
630
|
+
checked={currentVisibleColumnIds.includes(column.id)}
|
|
631
|
+
disabled={!canHide}
|
|
632
|
+
onCheckedChange={(checked) =>
|
|
633
|
+
setColumnVisible(column.id, checked)
|
|
634
|
+
}
|
|
635
|
+
>
|
|
636
|
+
{String(
|
|
637
|
+
typeof column.header === "function"
|
|
638
|
+
? column.id
|
|
639
|
+
: column.header,
|
|
640
|
+
)}{" "}
|
|
641
|
+
column
|
|
642
|
+
</Checkbox>
|
|
643
|
+
);
|
|
644
|
+
})}
|
|
645
|
+
</div>
|
|
646
|
+
) : null}
|
|
647
|
+
</div>
|
|
648
|
+
) : null}
|
|
649
|
+
</div>
|
|
650
|
+
</div>
|
|
651
|
+
) : null}
|
|
652
|
+
|
|
653
|
+
{virtualizationEnabled ? (
|
|
654
|
+
<div
|
|
655
|
+
ref={virtualList.setContainerRef}
|
|
656
|
+
data-slot="data-table-virtual-scroll"
|
|
657
|
+
className={dataTableVirtualScrollVariants()}
|
|
658
|
+
style={{ maxHeight: virtualization?.height }}
|
|
659
|
+
>
|
|
660
|
+
<div style={{ minHeight: virtualList.totalHeight }}>
|
|
661
|
+
<div
|
|
662
|
+
data-slot="data-table-virtual-offset"
|
|
663
|
+
style={{ transform: `translateY(${virtualOffset}px)` }}
|
|
664
|
+
>
|
|
665
|
+
{tableElement}
|
|
666
|
+
</div>
|
|
667
|
+
</div>
|
|
668
|
+
</div>
|
|
669
|
+
) : (
|
|
670
|
+
tableElement
|
|
671
|
+
)}
|
|
672
|
+
|
|
673
|
+
{paginationEnabled && pageCount > 1 ? (
|
|
674
|
+
<div
|
|
675
|
+
className={cn(
|
|
676
|
+
"flex flex-wrap items-center gap-3",
|
|
677
|
+
showRowCount ? "justify-between" : "justify-end",
|
|
678
|
+
)}
|
|
679
|
+
>
|
|
680
|
+
{showRowCount ? (
|
|
681
|
+
<span className={dataTableStatusVariants()}>
|
|
682
|
+
Showing {processedRows.length} of {sortedData.length}
|
|
683
|
+
</span>
|
|
684
|
+
) : null}
|
|
685
|
+
<Pagination
|
|
686
|
+
appearance={resolvedPaginationAppearance}
|
|
687
|
+
size={resolvedSize}
|
|
688
|
+
pageCount={pageCount}
|
|
689
|
+
page={paginationState.currentPage}
|
|
690
|
+
siblingCount={paginationOptions?.siblingCount}
|
|
691
|
+
boundaryCount={paginationOptions?.boundaryCount}
|
|
692
|
+
onPageChange={paginationState.setPage}
|
|
693
|
+
className="w-auto"
|
|
694
|
+
/>
|
|
695
|
+
</div>
|
|
696
|
+
) : null}
|
|
697
|
+
</section>
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
DataTableBase.displayName = "DataTable";
|