@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.
Files changed (152) hide show
  1. package/README.md +12 -8
  2. package/cli/cli.integration.test.ts +36 -0
  3. package/cli/index.mjs +43 -7
  4. package/cli/props.json +462 -3
  5. package/cli/registry.json +29 -0
  6. package/cli/rewrite-imports.mjs +29 -4
  7. package/cli/rewrite-imports.test.ts +35 -0
  8. package/dist/{chunk-WWKAJHIV.mjs → chunk-4PAHLHYF.mjs} +3 -3
  9. package/dist/{chunk-WWKAJHIV.mjs.map → chunk-4PAHLHYF.mjs.map} +1 -1
  10. package/dist/chunk-4SLVTSHM.js +241 -0
  11. package/dist/chunk-4SLVTSHM.js.map +1 -0
  12. package/dist/chunk-6OVDBAMI.js +19 -0
  13. package/dist/{chunk-3W2UUKWP.js.map → chunk-6OVDBAMI.js.map} +1 -1
  14. package/dist/chunk-74SKXGTM.js +4 -0
  15. package/dist/chunk-74SKXGTM.js.map +1 -0
  16. package/dist/{chunk-QE7OJW4J.js → chunk-BAAXQPZ7.js} +6 -6
  17. package/dist/{chunk-QE7OJW4J.js.map → chunk-BAAXQPZ7.js.map} +1 -1
  18. package/dist/chunk-CYKSS5S5.mjs +128 -0
  19. package/dist/chunk-CYKSS5S5.mjs.map +1 -0
  20. package/dist/chunk-D7ZTSAA6.mjs +221 -0
  21. package/dist/chunk-D7ZTSAA6.mjs.map +1 -0
  22. package/dist/{chunk-VA6SB6NN.js → chunk-DPNTQ4AK.js} +73 -6
  23. package/dist/chunk-DPNTQ4AK.js.map +1 -0
  24. package/dist/chunk-HMDH4BQJ.js +123 -0
  25. package/dist/chunk-HMDH4BQJ.js.map +1 -0
  26. package/dist/chunk-I7EBE7BD.js +98 -0
  27. package/dist/chunk-I7EBE7BD.js.map +1 -0
  28. package/dist/chunk-IHDM7AHY.mjs +233 -0
  29. package/dist/chunk-IHDM7AHY.mjs.map +1 -0
  30. package/dist/chunk-L5QORCUO.js +225 -0
  31. package/dist/chunk-L5QORCUO.js.map +1 -0
  32. package/dist/chunk-LHBJD57K.mjs +143 -0
  33. package/dist/chunk-LHBJD57K.mjs.map +1 -0
  34. package/dist/{chunk-CHI6MBTI.mjs → chunk-OWVQVAOY.mjs} +3 -3
  35. package/dist/{chunk-CHI6MBTI.mjs.map → chunk-OWVQVAOY.mjs.map} +1 -1
  36. package/dist/chunk-OYAJG2BO.js +83 -0
  37. package/dist/chunk-OYAJG2BO.js.map +1 -0
  38. package/dist/chunk-PTU5ZAYX.js +145 -0
  39. package/dist/chunk-PTU5ZAYX.js.map +1 -0
  40. package/dist/chunk-QKO5DA4N.mjs +81 -0
  41. package/dist/chunk-QKO5DA4N.mjs.map +1 -0
  42. package/dist/chunk-T7PIKDUZ.js +130 -0
  43. package/dist/chunk-T7PIKDUZ.js.map +1 -0
  44. package/dist/chunk-TDK5TVJE.mjs +3 -0
  45. package/dist/chunk-TDK5TVJE.mjs.map +1 -0
  46. package/dist/{chunk-A4IB3C23.mjs → chunk-UVP3MUBU.mjs} +58 -7
  47. package/dist/chunk-UVP3MUBU.mjs.map +1 -0
  48. package/dist/chunk-VBNW2B4D.mjs +3 -0
  49. package/dist/chunk-VBNW2B4D.mjs.map +1 -0
  50. package/dist/chunk-W6DO36XD.mjs +96 -0
  51. package/dist/chunk-W6DO36XD.mjs.map +1 -0
  52. package/dist/chunk-XR3J46TZ.js +4 -0
  53. package/dist/chunk-XR3J46TZ.js.map +1 -0
  54. package/dist/chunk-ZOHCADDL.mjs +121 -0
  55. package/dist/chunk-ZOHCADDL.mjs.map +1 -0
  56. package/dist/design-system/data-table.d.ts +8 -0
  57. package/dist/design-system/data-table.d.ts.map +1 -0
  58. package/dist/design-system/facade.js +6 -6
  59. package/dist/design-system/facade.mjs +5 -5
  60. package/dist/design-system/index.d.ts +2 -0
  61. package/dist/design-system/index.d.ts.map +1 -1
  62. package/dist/design-system/split-button.d.ts +25 -0
  63. package/dist/design-system/split-button.d.ts.map +1 -0
  64. package/dist/hooks/useTableFilter.js +6 -116
  65. package/dist/hooks/useTableFilter.js.map +1 -1
  66. package/dist/hooks/useTableFilter.mjs +1 -118
  67. package/dist/hooks/useTableFilter.mjs.map +1 -1
  68. package/dist/hooks/useTableSort.js +6 -91
  69. package/dist/hooks/useTableSort.js.map +1 -1
  70. package/dist/hooks/useTableSort.mjs +1 -93
  71. package/dist/hooks/useTableSort.mjs.map +1 -1
  72. package/dist/hooks/useVirtualList.js +6 -76
  73. package/dist/hooks/useVirtualList.js.map +1 -1
  74. package/dist/hooks/useVirtualList.mjs +1 -78
  75. package/dist/hooks/useVirtualList.mjs.map +1 -1
  76. package/dist/ui/buttons/animated.js +8 -8
  77. package/dist/ui/buttons/animated.mjs +6 -6
  78. package/dist/ui/buttons.js +10 -9
  79. package/dist/ui/buttons.mjs +8 -7
  80. package/dist/ui/checkbox.js +7 -123
  81. package/dist/ui/checkbox.js.map +1 -1
  82. package/dist/ui/checkbox.mjs +2 -126
  83. package/dist/ui/checkbox.mjs.map +1 -1
  84. package/dist/ui/data-table/data-table-base.d.ts +6 -0
  85. package/dist/ui/data-table/data-table-base.d.ts.map +1 -0
  86. package/dist/ui/data-table/data-table.d.ts +6 -0
  87. package/dist/ui/data-table/data-table.d.ts.map +1 -0
  88. package/dist/ui/data-table/index.d.ts +4 -0
  89. package/dist/ui/data-table/index.d.ts.map +1 -0
  90. package/dist/ui/data-table/types.d.ts +92 -0
  91. package/dist/ui/data-table/types.d.ts.map +1 -0
  92. package/dist/ui/data-table/variants.d.ts +8 -0
  93. package/dist/ui/data-table/variants.d.ts.map +1 -0
  94. package/dist/ui/data-table.js +620 -0
  95. package/dist/ui/data-table.js.map +1 -0
  96. package/dist/ui/data-table.mjs +611 -0
  97. package/dist/ui/data-table.mjs.map +1 -0
  98. package/dist/ui/dropdown/dropdown.d.ts +1 -1
  99. package/dist/ui/dropdown/dropdown.d.ts.map +1 -1
  100. package/dist/ui/dropdown/types.d.ts +2 -2
  101. package/dist/ui/dropdown/types.d.ts.map +1 -1
  102. package/dist/ui/dropdown.js +31 -231
  103. package/dist/ui/dropdown.js.map +1 -1
  104. package/dist/ui/dropdown.mjs +4 -229
  105. package/dist/ui/dropdown.mjs.map +1 -1
  106. package/dist/ui/dynamic-stepper.js +18 -18
  107. package/dist/ui/dynamic-stepper.mjs +7 -7
  108. package/dist/ui/inputs.js +7 -138
  109. package/dist/ui/inputs.js.map +1 -1
  110. package/dist/ui/inputs.mjs +2 -141
  111. package/dist/ui/inputs.mjs.map +1 -1
  112. package/dist/ui/pagination.js +20 -221
  113. package/dist/ui/pagination.js.map +1 -1
  114. package/dist/ui/pagination.mjs +8 -223
  115. package/dist/ui/pagination.mjs.map +1 -1
  116. package/dist/ui/split-button/index.d.ts +4 -0
  117. package/dist/ui/split-button/index.d.ts.map +1 -0
  118. package/dist/ui/split-button/split-button-base.d.ts +6 -0
  119. package/dist/ui/split-button/split-button-base.d.ts.map +1 -0
  120. package/dist/ui/split-button/split-button.d.ts +6 -0
  121. package/dist/ui/split-button/split-button.d.ts.map +1 -0
  122. package/dist/ui/split-button/types.d.ts +30 -0
  123. package/dist/ui/split-button/types.d.ts.map +1 -0
  124. package/dist/ui/split-button/variants.d.ts +16 -0
  125. package/dist/ui/split-button/variants.d.ts.map +1 -0
  126. package/dist/ui/split-button.js +287 -0
  127. package/dist/ui/split-button.js.map +1 -0
  128. package/dist/ui/split-button.mjs +278 -0
  129. package/dist/ui/split-button.mjs.map +1 -0
  130. package/dist/ui/table.js +1 -0
  131. package/dist/ui/table.mjs +1 -0
  132. package/package.json +1 -1
  133. package/src/design-system/data-table.ts +20 -0
  134. package/src/design-system/index.ts +2 -0
  135. package/src/design-system/split-button.ts +38 -0
  136. package/src/ui/data-table/data-table-base.tsx +701 -0
  137. package/src/ui/data-table/data-table.test.tsx +389 -0
  138. package/src/ui/data-table/data-table.tsx +11 -0
  139. package/src/ui/data-table/index.ts +24 -0
  140. package/src/ui/data-table/types.ts +121 -0
  141. package/src/ui/data-table/variants.ts +21 -0
  142. package/src/ui/dropdown/dropdown.tsx +7 -3
  143. package/src/ui/dropdown/types.ts +2 -2
  144. package/src/ui/split-button/index.ts +19 -0
  145. package/src/ui/split-button/split-button-base.tsx +232 -0
  146. package/src/ui/split-button/split-button.test.tsx +208 -0
  147. package/src/ui/split-button/split-button.tsx +9 -0
  148. package/src/ui/split-button/types.ts +46 -0
  149. package/src/ui/split-button/variants.ts +46 -0
  150. package/dist/chunk-3W2UUKWP.js +0 -19
  151. package/dist/chunk-A4IB3C23.mjs.map +0 -1
  152. 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";