@datum-cloud/datum-ui 1.0.0 → 1.1.0

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 (28) hide show
  1. package/dist/components/features/data-table/core/filter-engine.d.ts +5 -2
  2. package/dist/components/features/data-table/core/filter-engine.d.ts.map +1 -1
  3. package/dist/components/features/data-table/index.d.ts +4 -0
  4. package/dist/components/features/data-table/index.d.ts.map +1 -1
  5. package/dist/components/features/grouped-table/components/grouped-skeleton.d.ts +7 -0
  6. package/dist/components/features/grouped-table/components/grouped-skeleton.d.ts.map +1 -0
  7. package/dist/components/features/grouped-table/components/grouped-toolbar.d.ts +9 -0
  8. package/dist/components/features/grouped-table/components/grouped-toolbar.d.ts.map +1 -0
  9. package/dist/components/features/grouped-table/grouped-table.d.ts +3 -0
  10. package/dist/components/features/grouped-table/grouped-table.d.ts.map +1 -0
  11. package/dist/components/features/grouped-table/index.d.ts +3 -0
  12. package/dist/components/features/grouped-table/index.d.ts.map +1 -0
  13. package/dist/components/features/grouped-table/lib/bucket-rows.d.ts +14 -0
  14. package/dist/components/features/grouped-table/lib/bucket-rows.d.ts.map +1 -0
  15. package/dist/components/features/grouped-table/lib/compose-columns.d.ts +11 -0
  16. package/dist/components/features/grouped-table/lib/compose-columns.d.ts.map +1 -0
  17. package/dist/components/features/grouped-table/lib/sort-rows.d.ts +7 -0
  18. package/dist/components/features/grouped-table/lib/sort-rows.d.ts.map +1 -0
  19. package/dist/components/features/grouped-table/lib/use-controllable-state.d.ts +8 -0
  20. package/dist/components/features/grouped-table/lib/use-controllable-state.d.ts.map +1 -0
  21. package/dist/components/features/grouped-table/types.d.ts +57 -0
  22. package/dist/components/features/grouped-table/types.d.ts.map +1 -0
  23. package/dist/components/features/grouped-table/use-grouped-expansion.d.ts +10 -0
  24. package/dist/components/features/grouped-table/use-grouped-expansion.d.ts.map +1 -0
  25. package/dist/data-table/index.mjs +2 -1588
  26. package/dist/data-table-BTIxzB7O.mjs +1588 -0
  27. package/dist/grouped-table/index.mjs +339 -0
  28. package/package.json +8 -3
@@ -1,1588 +1,2 @@
1
- import { t as cn } from "../cn-dlASUkDY.mjs";
2
- import { t as Badge } from "../badge-DO2_XsXD.mjs";
3
- import { t as Button } from "../button-B_N7A8gv.mjs";
4
- import { t as Checkbox } from "../checkbox-BZr9bkke.mjs";
5
- import { t as Input } from "../input-DBzgl-pN.mjs";
6
- import { t as ResponsiveDropdown } from "../responsive-dropdown-BhvBzT_1.mjs";
7
- import { i as SelectItem, l as SelectTrigger, n as SelectContent, t as Select, u as SelectValue } from "../select-DNnPDWSW.mjs";
8
- import { t as Skeleton } from "../skeleton-CGU89HPB.mjs";
9
- import { c as TableRow, i as TableCell, n as TableBody, o as TableHead, s as TableHeader, t as Table } from "../table-BR3mwU8X.mjs";
10
- import { t as Autocomplete } from "../autocomplete-Bg187x6x.mjs";
11
- import { t as CalendarDatePicker } from "../calendar-date-picker-DAVXW7Jg.mjs";
12
- import { t as ActionRow } from "../action-row-BhMyMSep.mjs";
13
- import { t as MultiSelect } from "../multi-select-C9ocz07C.mjs";
14
- import { ArrowDown, ArrowUp, ArrowUpDown, ChevronLeft, ChevronRight, MoreHorizontal, X } from "lucide-react";
15
- import { createContext, memo, use, useCallback, useContext, useEffect, useId, useMemo, useRef, useState, useSyncExternalStore } from "react";
16
- import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
17
- import { parseAsInteger, parseAsString, useQueryStates } from "nuqs";
18
- import { flexRender, getCoreRowModel, getPaginationRowModel, getSortedRowModel, useReactTable } from "@tanstack/react-table";
19
- //#region src/components/features/data-table/constants.ts
20
- const DEFAULT_PAGE_SIZE = 20;
21
- const DEFAULT_PAGE_SIZES = [
22
- 10,
23
- 20,
24
- 30,
25
- 50
26
- ];
27
- const DEFAULT_DEBOUNCE_MS = 300;
28
- const DEFAULT_LOADING_ROWS = 5;
29
- //#endregion
30
- //#region src/components/features/data-table/adapters/nuqs-adapter.ts
31
- /**
32
- * Serialize SortingState to URL-friendly string.
33
- * Format: "name" (asc), "-name" (desc), comma-separated for multi-sort.
34
- * Example: "-department,name" → [{id:"department",desc:true},{id:"name",desc:false}]
35
- */
36
- function serializeSorting(sorting) {
37
- if (sorting.length === 0) return "";
38
- return sorting.map((s) => s.desc ? `-${s.id}` : s.id).join(",");
39
- }
40
- /**
41
- * Parse URL sort string back to SortingState.
42
- */
43
- function parseSorting(value) {
44
- if (!value) return [];
45
- return value.split(",").filter(Boolean).map((part) => {
46
- if (part.startsWith("-")) return {
47
- id: part.slice(1),
48
- desc: true
49
- };
50
- return {
51
- id: part,
52
- desc: false
53
- };
54
- });
55
- }
56
- const coreSearchParams = {
57
- sort: parseAsString.withDefault(""),
58
- q: parseAsString.withDefault(""),
59
- page: parseAsInteger.withDefault(0),
60
- size: parseAsInteger.withDefault(20)
61
- };
62
- const EMPTY_FILTER_PARSERS_PLACEHOLDER = { _dt: parseAsString.withDefault("") };
63
- /**
64
- * Hook that creates a StateAdapter backed by nuqs URL query state.
65
- *
66
- * URL format:
67
- * - `?sort=name` (asc) or `?sort=-name` (desc), comma-separated for multi-sort
68
- * - `?q=search` for search text
69
- * - `?page=0&size=20` for pagination
70
- * - Custom filter keys as declared in options
71
- *
72
- * Requires `nuqs` to be installed in the consumer app.
73
- *
74
- * @example
75
- * ```tsx
76
- * const stateAdapter = useNuqsAdapter({
77
- * filters: {
78
- * status: parseAsString.withDefault(''),
79
- * department: parseAsArrayOf(parseAsString).withDefault([]),
80
- * },
81
- * })
82
- * const tableState = useDataTableClient({ data, columns, stateAdapter })
83
- * ```
84
- */
85
- function useNuqsAdapter(options = {}) {
86
- const { filters: filterParsers } = options;
87
- const [coreState, setCoreState] = useQueryStates(coreSearchParams);
88
- const hasFilters = filterParsers != null && Object.keys(filterParsers).length > 0;
89
- const [filterState, setFilterState] = useQueryStates(hasFilters ? filterParsers : EMPTY_FILTER_PARSERS_PLACEHOLDER);
90
- return useMemo(() => ({
91
- read: () => ({
92
- sorting: parseSorting(coreState.sort),
93
- search: coreState.q,
94
- pageIndex: coreState.page,
95
- pageSize: coreState.size,
96
- ...hasFilters ? { filters: filterState } : {}
97
- }),
98
- write: (state) => {
99
- setCoreState({
100
- sort: serializeSorting(state.sorting),
101
- q: state.search,
102
- page: state.pageIndex ?? 0,
103
- size: state.pageSize ?? 20
104
- });
105
- if (hasFilters && filterParsers) {
106
- const update = {};
107
- for (const key of Object.keys(filterParsers)) update[key] = state.filters?.[key] ?? null;
108
- setFilterState(update);
109
- }
110
- }
111
- }), [
112
- coreState,
113
- filterState,
114
- hasFilters,
115
- setCoreState,
116
- setFilterState,
117
- filterParsers
118
- ]);
119
- }
120
- //#endregion
121
- //#region src/components/features/data-table/columns/selection-column.tsx
122
- const SELECTION_COLUMN_ID = "select";
123
- function createSelectionColumn(options = {}) {
124
- const { className, headerClassName, renderHeader, renderCell } = options;
125
- return {
126
- id: SELECTION_COLUMN_ID,
127
- size: 40,
128
- enableSorting: false,
129
- enableHiding: false,
130
- header: renderHeader ?? (({ table }) => /* @__PURE__ */ jsx(Checkbox, {
131
- checked: table.getIsAllPageRowsSelected() || table.getIsSomePageRowsSelected() && "indeterminate",
132
- onCheckedChange: (value) => table.toggleAllPageRowsSelected(!!value),
133
- "aria-label": "Select all",
134
- className: headerClassName
135
- })),
136
- cell: renderCell ?? (({ row }) => /* @__PURE__ */ jsx(Checkbox, {
137
- checked: row.getIsSelected(),
138
- onCheckedChange: (value) => row.toggleSelected(!!value),
139
- "aria-label": "Select row",
140
- className
141
- }))
142
- };
143
- }
144
- function hasSelectionColumn(columns) {
145
- return columns.some((col) => "id" in col && col.id === SELECTION_COLUMN_ID);
146
- }
147
- function withSelectionColumn(columns, options = {}) {
148
- if (hasSelectionColumn(columns)) return columns;
149
- return [createSelectionColumn(options), ...columns];
150
- }
151
- //#endregion
152
- //#region src/components/features/data-table/core/filter-engine.ts
153
- /**
154
- * Resolve a dot-path on an object (e.g. "status.registrationApproval").
155
- * Falls back to a flat key lookup when the path has no dots.
156
- */
157
- function resolvePath(obj, path) {
158
- if (obj == null) return void 0;
159
- const record = obj;
160
- if (!path.includes(".")) return record[path];
161
- return path.split(".").reduce((acc, key) => acc != null ? acc[key] : void 0, record);
162
- }
163
- const FILTER_STRATEGIES = {
164
- "checkbox": (cellValue, filterValue) => {
165
- if (filterValue == null) return true;
166
- if (Array.isArray(filterValue) && filterValue.length === 0) return true;
167
- if (!Array.isArray(filterValue)) return cellValue === filterValue;
168
- if (Array.isArray(cellValue)) return cellValue.some((v) => filterValue.includes(v));
169
- return filterValue.includes(cellValue);
170
- },
171
- "select": (cellValue, filterValue) => {
172
- if (filterValue == null || filterValue === "") return true;
173
- return cellValue === filterValue;
174
- },
175
- "date-gte": (cellValue, filterValue) => {
176
- if (!filterValue) return true;
177
- const cell = new Date(cellValue);
178
- const filter = new Date(filterValue);
179
- if (Number.isNaN(cell.getTime()) || Number.isNaN(filter.getTime())) return true;
180
- return cell >= filter;
181
- },
182
- "date-lte": (cellValue, filterValue) => {
183
- if (!filterValue) return true;
184
- const cell = new Date(cellValue);
185
- const filter = new Date(filterValue);
186
- if (Number.isNaN(cell.getTime()) || Number.isNaN(filter.getTime())) return true;
187
- return cell <= filter;
188
- }
189
- };
190
- function resolveStrategy(strategy) {
191
- if (!strategy) return void 0;
192
- if (typeof strategy === "function") return strategy;
193
- return FILTER_STRATEGIES[strategy];
194
- }
195
- function applyFilters(data, filters, search, registeredFilters, customFilterFns, searchConfig) {
196
- const hasFilters = Object.keys(filters).length > 0;
197
- const hasSearch = search.length > 0;
198
- if (!hasFilters && !hasSearch) return data;
199
- return data.filter((row) => {
200
- if (hasFilters) for (const [column, value] of Object.entries(filters)) {
201
- const fn = customFilterFns[column] ?? resolveStrategy(registeredFilters.get(column));
202
- if (!fn) {
203
- console.warn(`[DataTable] No filter strategy registered for column "${column}". Filter ignored.`);
204
- continue;
205
- }
206
- if (!fn(resolvePath(row, column), value)) return false;
207
- }
208
- if (hasSearch) {
209
- const query = search.toLowerCase();
210
- if (searchConfig.searchFn) return searchConfig.searchFn(row, search);
211
- if (searchConfig.searchableColumns && searchConfig.searchableColumns.length > 0) return searchConfig.searchableColumns.some((col) => {
212
- const cellValue = resolvePath(row, col);
213
- return cellValue != null && String(cellValue).toLowerCase().includes(query);
214
- });
215
- return Object.values(row).some((val) => {
216
- if (val == null) return false;
217
- return String(val).toLowerCase().includes(query);
218
- });
219
- }
220
- return true;
221
- });
222
- }
223
- //#endregion
224
- //#region src/components/features/data-table/core/store.ts
225
- function createDataTableStore(options) {
226
- let registeredFilters = /* @__PURE__ */ new Map();
227
- const listeners = /* @__PURE__ */ new Set();
228
- function computeFilteredData(s) {
229
- if (s.mode === "server") return s.data;
230
- return applyFilters(s.data, s.filters, s.search, registeredFilters, options.filterFns ?? {}, {
231
- searchFn: options.searchFn,
232
- searchableColumns: options.searchableColumns
233
- });
234
- }
235
- let state = {
236
- data: options.data,
237
- filteredData: options.data,
238
- sorting: options.defaultSort ?? [],
239
- filters: options.defaultFilters ?? {},
240
- search: "",
241
- rowSelection: {},
242
- pageIndex: 0,
243
- pageSize: options.pageSize ?? 20,
244
- columnCount: options.columnCount ?? 0,
245
- mode: options.mode,
246
- isLoading: options.isLoading ?? false,
247
- error: null,
248
- inlineContents: [],
249
- _version: 0
250
- };
251
- if (options.defaultFilters && Object.keys(options.defaultFilters).length > 0) state = {
252
- ...state,
253
- filteredData: computeFilteredData(state)
254
- };
255
- function notify() {
256
- for (const listener of listeners) listener();
257
- }
258
- function setState(next) {
259
- state = {
260
- ...next,
261
- _version: state._version + 1
262
- };
263
- notify();
264
- }
265
- return {
266
- getSnapshot: () => state,
267
- subscribe: (listener) => {
268
- listeners.add(listener);
269
- return () => listeners.delete(listener);
270
- },
271
- setData: (data) => {
272
- const next = {
273
- ...state,
274
- data,
275
- pageIndex: 0,
276
- rowSelection: {}
277
- };
278
- setState({
279
- ...next,
280
- filteredData: computeFilteredData(next)
281
- });
282
- },
283
- setServerData: (data) => {
284
- setState({
285
- ...state,
286
- data,
287
- filteredData: data
288
- });
289
- },
290
- setSorting: (sorting) => {
291
- setState({
292
- ...state,
293
- sorting,
294
- rowSelection: {}
295
- });
296
- },
297
- setFilter: (key, value) => {
298
- const next = {
299
- ...state,
300
- filters: {
301
- ...state.filters,
302
- [key]: value
303
- },
304
- rowSelection: {},
305
- pageIndex: 0
306
- };
307
- setState({
308
- ...next,
309
- filteredData: computeFilteredData(next)
310
- });
311
- },
312
- clearFilter: (key) => {
313
- const filters = Object.fromEntries(Object.entries(state.filters).filter(([k]) => k !== key));
314
- const next = {
315
- ...state,
316
- filters,
317
- rowSelection: {},
318
- pageIndex: 0
319
- };
320
- setState({
321
- ...next,
322
- filteredData: computeFilteredData(next)
323
- });
324
- },
325
- clearAllFilters: () => {
326
- const next = {
327
- ...state,
328
- filters: {},
329
- rowSelection: {},
330
- pageIndex: 0
331
- };
332
- setState({
333
- ...next,
334
- filteredData: computeFilteredData(next)
335
- });
336
- },
337
- setSearch: (search) => {
338
- const next = {
339
- ...state,
340
- search,
341
- rowSelection: {},
342
- pageIndex: 0
343
- };
344
- setState({
345
- ...next,
346
- filteredData: computeFilteredData(next)
347
- });
348
- },
349
- clearSearch: () => {
350
- const next = {
351
- ...state,
352
- search: "",
353
- rowSelection: {},
354
- pageIndex: 0
355
- };
356
- setState({
357
- ...next,
358
- filteredData: computeFilteredData(next)
359
- });
360
- },
361
- setRowSelection: (rowSelection) => {
362
- setState({
363
- ...state,
364
- rowSelection
365
- });
366
- },
367
- setPageIndex: (pageIndex) => {
368
- if (!Number.isFinite(pageIndex) || pageIndex < 0) return;
369
- setState({
370
- ...state,
371
- pageIndex: Math.floor(pageIndex),
372
- rowSelection: {}
373
- });
374
- },
375
- setPageSize: (pageSize) => {
376
- if (!Number.isFinite(pageSize) || pageSize < 1) return;
377
- setState({
378
- ...state,
379
- pageSize: Math.floor(pageSize),
380
- pageIndex: 0,
381
- rowSelection: {}
382
- });
383
- },
384
- setPagination: (pageIndex, pageSize) => {
385
- const safeIndex = Number.isFinite(pageIndex) ? Math.max(0, Math.floor(pageIndex)) : state.pageIndex;
386
- const safeSize = Number.isFinite(pageSize) ? Math.max(1, Math.floor(pageSize)) : state.pageSize;
387
- setState({
388
- ...state,
389
- pageIndex: safeIndex,
390
- pageSize: safeSize,
391
- rowSelection: {}
392
- });
393
- },
394
- setLoading: (isLoading) => {
395
- setState({
396
- ...state,
397
- isLoading
398
- });
399
- },
400
- setError: (error) => {
401
- setState({
402
- ...state,
403
- error
404
- });
405
- },
406
- registerFilter: (column, strategy) => {
407
- const next = new Map(registeredFilters);
408
- next.set(column, strategy);
409
- registeredFilters = next;
410
- const filteredData = computeFilteredData(state);
411
- setState({
412
- ...state,
413
- filteredData
414
- });
415
- },
416
- unregisterFilter: (column) => {
417
- const next = new Map(registeredFilters);
418
- next.delete(column);
419
- registeredFilters = next;
420
- if (column in state.filters) {
421
- const filteredData = computeFilteredData(state);
422
- setState({
423
- ...state,
424
- filteredData
425
- });
426
- }
427
- },
428
- registerInlineContent: (entry) => {
429
- const existing = state.inlineContents.findIndex((e) => e.id === entry.id);
430
- const inlineContents = existing >= 0 ? state.inlineContents.map((e, i) => i === existing ? entry : e) : [...state.inlineContents, entry];
431
- setState({
432
- ...state,
433
- inlineContents
434
- });
435
- },
436
- unregisterInlineContent: (id) => {
437
- const inlineContents = state.inlineContents.filter((e) => e.id !== id);
438
- setState({
439
- ...state,
440
- inlineContents
441
- });
442
- }
443
- };
444
- }
445
- //#endregion
446
- //#region src/components/features/data-table/core/data-table-context.tsx
447
- const DataTableStoreContext = createContext(null);
448
- const TableInstanceContext = createContext(null);
449
- /**
450
- * Monotonic counter that increments on every store mutation.
451
- * Table-dependent hooks consume this context to force re-renders
452
- * through React's {children} composition boundary, since the
453
- * mutable table singleton (stable ref) cannot trigger context updates.
454
- */
455
- const DataTableRenderKeyContext = createContext(0);
456
- /**
457
- * Forces a re-render when the store changes. Used by table-dependent hooks.
458
- * Uses useContext (not use()) because use() does not reliably register
459
- * context subscriptions in SSR/hydration scenarios (React Router SSR),
460
- * preventing re-renders when the context value changes.
461
- */
462
- function useRenderKey() {
463
- return useContext(DataTableRenderKeyContext);
464
- }
465
- createContext(null);
466
- function useDataTableStore() {
467
- const store = use(DataTableStoreContext);
468
- if (!store) throw new Error("useDataTableStore must be used within a <DataTable.Client> or <DataTable.Server> provider");
469
- return store;
470
- }
471
- /**
472
- * Returns the table instance or null if not yet available.
473
- * Used by hooks that need to handle the null-table window during SSR.
474
- */
475
- function useTableInstanceOrNull() {
476
- return use(TableInstanceContext);
477
- }
478
- //#endregion
479
- //#region src/components/features/data-table/hooks/use-selectors.ts
480
- function shallowEqual(a, b) {
481
- const keysA = Object.keys(a);
482
- const keysB = Object.keys(b);
483
- if (keysA.length !== keysB.length) return false;
484
- for (const key of keysA) {
485
- const va = a[key];
486
- const vb = b[key];
487
- if (va === vb) continue;
488
- if (Array.isArray(va) && Array.isArray(vb)) {
489
- if (va.length !== vb.length) return false;
490
- for (let i = 0; i < va.length; i++) if (va[i] !== vb[i]) return false;
491
- continue;
492
- }
493
- return false;
494
- }
495
- return true;
496
- }
497
- function useSliceSelector(selector) {
498
- const store = useDataTableStore();
499
- const cachedRef = useRef(null);
500
- const getSnapshot = useCallback(() => {
501
- const next = selector(store.getSnapshot());
502
- if (cachedRef.current && shallowEqual(cachedRef.current, next)) return cachedRef.current;
503
- cachedRef.current = next;
504
- return next;
505
- }, [store, selector]);
506
- return useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);
507
- }
508
- function useDataTableFilters() {
509
- const store = useDataTableStore();
510
- return useSliceSelector(useCallback((state) => ({
511
- filters: state.filters,
512
- setFilter: store.setFilter,
513
- clearFilter: store.clearFilter,
514
- clearAllFilters: store.clearAllFilters,
515
- registerFilter: store.registerFilter,
516
- unregisterFilter: store.unregisterFilter
517
- }), [store]));
518
- }
519
- function useDataTableSearch() {
520
- const store = useDataTableStore();
521
- return useSliceSelector(useCallback((state) => ({
522
- search: state.search,
523
- setSearch: store.setSearch,
524
- clearSearch: store.clearSearch
525
- }), [store]));
526
- }
527
- function useDataTableSorting() {
528
- const store = useDataTableStore();
529
- return useSliceSelector(useCallback((state) => ({
530
- sorting: state.sorting,
531
- setSorting: store.setSorting
532
- }), [store]));
533
- }
534
- function useDataTableSelection() {
535
- useRenderKey();
536
- const store = useDataTableStore();
537
- const table = useTableInstanceOrNull();
538
- return {
539
- rowSelection: store.getSnapshot().rowSelection,
540
- setRowSelection: store.setRowSelection,
541
- selectedRows: table ? table.getFilteredSelectedRowModel().rows.map((r) => r.original) : []
542
- };
543
- }
544
- function useDataTablePagination() {
545
- useRenderKey();
546
- const store = useDataTableStore();
547
- const table = useTableInstanceOrNull();
548
- const state = store.getSnapshot();
549
- if (!table) return {
550
- canNextPage: false,
551
- canPrevPage: false,
552
- nextPage: () => {},
553
- prevPage: () => {},
554
- pageIndex: state.pageIndex,
555
- pageCount: 0,
556
- setPageIndex: store.setPageIndex,
557
- pageSize: state.pageSize,
558
- setPageSize: store.setPageSize,
559
- totalRows: 0
560
- };
561
- return {
562
- canNextPage: table.getCanNextPage(),
563
- canPrevPage: table.getCanPreviousPage(),
564
- nextPage: () => table.nextPage(),
565
- prevPage: () => table.previousPage(),
566
- pageIndex: state.pageIndex,
567
- pageCount: table.getPageCount(),
568
- setPageIndex: store.setPageIndex,
569
- pageSize: state.pageSize,
570
- setPageSize: store.setPageSize,
571
- totalRows: state.filteredData.length
572
- };
573
- }
574
- function useDataTableRows() {
575
- useRenderKey();
576
- const table = useTableInstanceOrNull();
577
- if (!table) return {
578
- rows: [],
579
- headerGroups: [],
580
- totalColumns: 0
581
- };
582
- return {
583
- rows: table.getRowModel().rows,
584
- headerGroups: table.getHeaderGroups(),
585
- totalColumns: table.getAllColumns().length
586
- };
587
- }
588
- function useDataTableLoading() {
589
- return useSliceSelector(useCallback((state) => ({
590
- isLoading: state.isLoading,
591
- error: state.error,
592
- columnCount: state.columnCount
593
- }), []));
594
- }
595
- function useDataTableInlineContents() {
596
- const store = useDataTableStore();
597
- return useSliceSelector(useCallback((state) => ({
598
- inlineContents: state.inlineContents,
599
- registerInlineContent: store.registerInlineContent,
600
- unregisterInlineContent: store.unregisterInlineContent
601
- }), [store]));
602
- }
603
- //#endregion
604
- //#region src/components/features/data-table/components/active-filters.tsx
605
- function formatValue(column, value, formatters) {
606
- const fn = formatters?.[column];
607
- if (fn) return fn(value);
608
- return String(value);
609
- }
610
- function FilterGroup({ label, children, className }) {
611
- return /* @__PURE__ */ jsxs("div", {
612
- className: cn("flex items-center gap-2 rounded-md border px-2 py-1", className),
613
- "data-slot": "dt-filter-group",
614
- "data-testid": "dt-filter-group",
615
- children: [/* @__PURE__ */ jsx("span", {
616
- className: "text-muted-foreground border-r pr-2 text-xs",
617
- children: label
618
- }), children]
619
- });
620
- }
621
- const EMPTY_LABELS = {};
622
- function ActiveFiltersInner({ label = "Selected Filters", excludeFilters, filterLabels = EMPTY_LABELS, formatFilterValue: formatters, clearAll = "icon", clearAllLabel = "Clear all", className, groupClassName, badgeClassName }) {
623
- const { filters, setFilter, clearFilter, clearAllFilters } = useDataTableFilters();
624
- const { search, clearSearch } = useDataTableSearch();
625
- const excludeSet = useMemo(() => new Set(excludeFilters ?? []), [excludeFilters]);
626
- const activeFilterEntries = Object.entries(filters).filter(([key, value]) => !excludeSet.has(key) && value != null && value !== "" && !(Array.isArray(value) && value.length === 0));
627
- const showSearch = search.length > 0 && !excludeSet.has("search");
628
- const hasFilters = activeFilterEntries.length > 0;
629
- if (!showSearch && !hasFilters) return null;
630
- const totalGroups = activeFilterEntries.length + (showSearch ? 1 : 0);
631
- const removeArrayItem = (column, items, item) => {
632
- const remaining = items.filter((v) => v !== item);
633
- if (remaining.length > 0) setFilter(column, remaining);
634
- else clearFilter(column);
635
- };
636
- const handleClearAll = () => {
637
- clearAllFilters();
638
- if (search.length > 0) clearSearch();
639
- };
640
- const badgeCn = cn("flex items-center gap-1.5 px-2 py-0.5 text-xs", badgeClassName);
641
- return /* @__PURE__ */ jsxs("div", {
642
- className: cn("flex flex-wrap items-center gap-2", className),
643
- "data-slot": "dt-active-filters",
644
- "data-testid": "dt-active-filters",
645
- children: [
646
- label !== null && /* @__PURE__ */ jsx("span", {
647
- className: "text-sm text-muted-foreground",
648
- "data-slot": "dt-active-filters-label",
649
- children: label
650
- }),
651
- showSearch && /* @__PURE__ */ jsx(FilterGroup, {
652
- label: "Search",
653
- className: groupClassName,
654
- children: /* @__PURE__ */ jsxs(Badge, {
655
- type: "muted",
656
- theme: "solid",
657
- className: badgeCn,
658
- children: [/* @__PURE__ */ jsx("span", { children: search }), /* @__PURE__ */ jsx(Button, {
659
- theme: "borderless",
660
- size: "small",
661
- "aria-label": "Clear search",
662
- className: "h-auto p-0 text-muted-foreground hover:text-foreground",
663
- onClick: clearSearch,
664
- children: /* @__PURE__ */ jsx(X, {
665
- className: "size-2.5",
666
- "aria-hidden": "true"
667
- })
668
- })]
669
- })
670
- }),
671
- activeFilterEntries.map(([column, value]) => {
672
- const groupLabel = filterLabels[column] ?? column;
673
- if (Array.isArray(value)) return /* @__PURE__ */ jsx(FilterGroup, {
674
- label: groupLabel,
675
- className: groupClassName,
676
- children: value.map((item) => /* @__PURE__ */ jsxs(Badge, {
677
- type: "muted",
678
- theme: "solid",
679
- className: badgeCn,
680
- children: [/* @__PURE__ */ jsx("span", { children: formatValue(column, item, formatters) }), /* @__PURE__ */ jsx(Button, {
681
- theme: "borderless",
682
- size: "small",
683
- "aria-label": `Remove ${formatValue(column, item, formatters)} from ${groupLabel}`,
684
- className: "h-auto p-0 text-muted-foreground hover:text-foreground",
685
- onClick: () => removeArrayItem(column, value, item),
686
- children: /* @__PURE__ */ jsx(X, {
687
- className: "size-2.5",
688
- "aria-hidden": "true"
689
- })
690
- })]
691
- }, item))
692
- }, column);
693
- return /* @__PURE__ */ jsx(FilterGroup, {
694
- label: groupLabel,
695
- className: groupClassName,
696
- children: /* @__PURE__ */ jsxs(Badge, {
697
- type: "muted",
698
- theme: "solid",
699
- className: badgeCn,
700
- children: [/* @__PURE__ */ jsx("span", { children: formatValue(column, value, formatters) }), /* @__PURE__ */ jsx(Button, {
701
- theme: "borderless",
702
- size: "small",
703
- "aria-label": `Clear ${groupLabel} filter`,
704
- className: "h-auto p-0 text-muted-foreground hover:text-foreground",
705
- onClick: () => clearFilter(column),
706
- children: /* @__PURE__ */ jsx(X, {
707
- className: "size-2.5",
708
- "aria-hidden": "true"
709
- })
710
- })]
711
- })
712
- }, column);
713
- }),
714
- totalGroups > 1 && /* @__PURE__ */ jsxs(Fragment$1, { children: [
715
- clearAll === "icon" && /* @__PURE__ */ jsx(Button, {
716
- theme: "borderless",
717
- size: "small",
718
- "aria-label": clearAllLabel,
719
- title: clearAllLabel,
720
- className: "h-auto p-1 text-muted-foreground hover:text-foreground",
721
- "data-slot": "dt-clear-all-filters",
722
- "data-testid": "dt-clear-all-filters",
723
- onClick: handleClearAll,
724
- children: /* @__PURE__ */ jsx(X, { className: "size-4" })
725
- }),
726
- clearAll === "button" && /* @__PURE__ */ jsxs(Button, {
727
- theme: "outline",
728
- size: "small",
729
- className: "h-auto px-2 py-1 text-xs",
730
- "data-slot": "dt-clear-all-filters",
731
- "data-testid": "dt-clear-all-filters",
732
- onClick: handleClearAll,
733
- children: [/* @__PURE__ */ jsx(X, { className: "size-3 mr-1" }), clearAllLabel]
734
- }),
735
- clearAll === "text" && /* @__PURE__ */ jsx(Button, {
736
- theme: "borderless",
737
- size: "small",
738
- className: "h-auto px-1 py-0.5 text-xs text-muted-foreground hover:text-foreground",
739
- "data-slot": "dt-clear-all-filters",
740
- "data-testid": "dt-clear-all-filters",
741
- onClick: handleClearAll,
742
- children: clearAllLabel
743
- })
744
- ] })
745
- ]
746
- });
747
- }
748
- const DataTableActiveFilters = memo(ActiveFiltersInner);
749
- //#endregion
750
- //#region src/components/features/data-table/components/bulk-actions.tsx
751
- function DataTableBulkActions({ children, className }) {
752
- const { selectedRows } = useDataTableSelection();
753
- if (selectedRows.length === 0) return null;
754
- return /* @__PURE__ */ jsx("div", {
755
- "data-slot": "dt-bulk-actions",
756
- className,
757
- children: children(selectedRows)
758
- });
759
- }
760
- //#endregion
761
- //#region src/components/features/data-table/components/column-header.tsx
762
- function DataTableColumnHeader({ column, title, className }) {
763
- if (!column.getCanSort()) return /* @__PURE__ */ jsx("div", {
764
- className: cn(className),
765
- "data-slot": "dt-column-header",
766
- children: title
767
- });
768
- const sorted = column.getIsSorted();
769
- return /* @__PURE__ */ jsx("div", {
770
- className: cn("flex items-center gap-2", className),
771
- "data-slot": "dt-column-header",
772
- children: /* @__PURE__ */ jsxs("button", {
773
- type: "button",
774
- className: "flex items-center gap-1 hover:text-foreground -ml-3 h-8 px-3 cursor-pointer",
775
- onClick: column.getToggleSortingHandler(),
776
- "aria-label": `Sort by ${title}${sorted === "asc" ? ", sorted ascending" : sorted === "desc" ? ", sorted descending" : ""}`,
777
- children: [/* @__PURE__ */ jsx("span", { children: title }), sorted === "desc" ? /* @__PURE__ */ jsx(ArrowDown, { className: "size-4" }) : sorted === "asc" ? /* @__PURE__ */ jsx(ArrowUp, { className: "size-4" }) : /* @__PURE__ */ jsx(ArrowUpDown, { className: "size-4" })]
778
- })
779
- });
780
- }
781
- //#endregion
782
- //#region src/components/features/data-table/components/content.tsx
783
- function resolveClassName(value, item) {
784
- if (typeof value === "function") return value(item);
785
- return value;
786
- }
787
- function renderInlineContentRow(entry, colSpan, rows) {
788
- return /* @__PURE__ */ jsx(TableRow, {
789
- "data-slot": "dt-inline-content",
790
- "data-position": entry.position,
791
- className: cn("transition-all duration-200", entry.className),
792
- children: /* @__PURE__ */ jsx(TableCell, {
793
- colSpan,
794
- children: entry.render({
795
- onClose: entry.onClose,
796
- rowData: entry.position === "row" ? rows.find((r) => r.id === entry.rowId)?.original ?? null : null
797
- })
798
- })
799
- }, entry.id);
800
- }
801
- function DataTableContent({ emptyMessage, className, tableClassName, headerClassName, headerRowClassName, headerCellClassName, bodyClassName, rowClassName, cellClassName }) {
802
- const { rows, headerGroups, totalColumns } = useDataTableRows();
803
- const { isLoading, columnCount } = useDataTableLoading();
804
- const { pageSize } = useDataTablePagination();
805
- const { inlineContents } = useDataTableInlineContents();
806
- const openInlineContents = useMemo(() => inlineContents.filter((e) => e.open), [inlineContents]);
807
- const colSpan = totalColumns;
808
- const skeletonColumns = totalColumns || columnCount || 5;
809
- return /* @__PURE__ */ jsx("div", {
810
- className: cn("datum-ui-data-table", className),
811
- "data-slot": "dt",
812
- style: { overflowX: "auto" },
813
- children: /* @__PURE__ */ jsxs(Table, {
814
- className: cn(tableClassName),
815
- "data-slot": "dt-table",
816
- children: [/* @__PURE__ */ jsx(TableHeader, {
817
- className: cn(headerClassName),
818
- "data-slot": "dt-header",
819
- children: headerGroups.map((headerGroup) => /* @__PURE__ */ jsx(TableRow, {
820
- className: cn(headerRowClassName),
821
- "data-slot": "dt-header-row",
822
- children: headerGroup.headers.map((header) => /* @__PURE__ */ jsx(TableHead, {
823
- className: cn(headerCellClassName),
824
- "data-slot": "dt-header-cell",
825
- children: header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())
826
- }, header.id))
827
- }, headerGroup.id))
828
- }), /* @__PURE__ */ jsxs(TableBody, {
829
- className: cn(bodyClassName),
830
- "data-slot": "dt-body",
831
- children: [openInlineContents.filter((e) => e.position === "top").map((entry) => renderInlineContentRow(entry, colSpan, rows)), rows.length > 0 ? rows.map((row) => {
832
- const rowEntry = openInlineContents.find((e) => e.position === "row" && e.rowId === row.id);
833
- if (rowEntry) return renderInlineContentRow(rowEntry, colSpan, rows);
834
- return /* @__PURE__ */ jsx(TableRow, {
835
- className: cn(resolveClassName(rowClassName, row)),
836
- style: { transitionProperty: "none" },
837
- "data-slot": "dt-row",
838
- "data-state": row.getIsSelected() ? "selected" : void 0,
839
- children: row.getVisibleCells().map((cell) => /* @__PURE__ */ jsx(TableCell, {
840
- className: cn(resolveClassName(cellClassName, cell)),
841
- "data-slot": "dt-cell",
842
- children: flexRender(cell.column.columnDef.cell, cell.getContext())
843
- }, cell.id))
844
- }, row.id);
845
- }) : isLoading ? Array.from({ length: pageSize }, (_, i) => /* @__PURE__ */ jsx(TableRow, {
846
- "data-slot": "dt-skeleton-row",
847
- children: Array.from({ length: skeletonColumns }, (_, j) => /* @__PURE__ */ jsx(TableCell, {
848
- "data-slot": "dt-skeleton-cell",
849
- children: /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-full" })
850
- }, j))
851
- }, i)) : /* @__PURE__ */ jsx(TableRow, {
852
- "data-slot": "dt-row",
853
- children: /* @__PURE__ */ jsx(TableCell, {
854
- colSpan,
855
- className: "h-24 text-center",
856
- "data-slot": "dt-empty",
857
- children: emptyMessage ?? "No results."
858
- })
859
- })]
860
- })]
861
- })
862
- });
863
- }
864
- //#endregion
865
- //#region src/components/features/data-table/components/inline-content.tsx
866
- function DataTableInlineContent({ position, rowId, open, onClose, className, children }) {
867
- const id = useId();
868
- const { registerInlineContent, unregisterInlineContent } = useDataTableInlineContents();
869
- const initialRender = useRef(true);
870
- useEffect(() => {
871
- registerInlineContent({
872
- id,
873
- position,
874
- rowId,
875
- open,
876
- onClose,
877
- className,
878
- render: children
879
- });
880
- return () => {
881
- unregisterInlineContent(id);
882
- };
883
- }, [
884
- id,
885
- registerInlineContent,
886
- unregisterInlineContent
887
- ]);
888
- useEffect(() => {
889
- if (initialRender.current) {
890
- initialRender.current = false;
891
- return;
892
- }
893
- registerInlineContent({
894
- id,
895
- position,
896
- rowId,
897
- open,
898
- onClose,
899
- className,
900
- render: children
901
- });
902
- }, [
903
- id,
904
- position,
905
- rowId,
906
- open,
907
- onClose,
908
- className,
909
- children,
910
- registerInlineContent
911
- ]);
912
- return null;
913
- }
914
- //#endregion
915
- //#region src/components/features/data-table/components/loading.tsx
916
- function DataTableLoading({ rows = 5, columns = 4, className }) {
917
- return /* @__PURE__ */ jsx("div", {
918
- className,
919
- "data-slot": "dt-loading",
920
- style: { overflowX: "auto" },
921
- children: /* @__PURE__ */ jsxs(Table, { children: [/* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsx(TableRow, { children: Array.from({ length: columns }, (_, i) => /* @__PURE__ */ jsx(TableHead, { children: /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-24" }) }, i)) }) }), /* @__PURE__ */ jsx(TableBody, { children: Array.from({ length: rows }, (_, rowIndex) => /* @__PURE__ */ jsx(TableRow, { children: Array.from({ length: columns }, (_, colIndex) => /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-full" }) }, colIndex)) }, rowIndex)) })] })
922
- });
923
- }
924
- //#endregion
925
- //#region src/components/features/data-table/components/pagination.tsx
926
- /**
927
- * Generates page numbers with ellipsis for large page counts.
928
- * Shows up to 7 items: first, last, current +/- 1 neighbor, and ellipsis gaps.
929
- */
930
- function getPageNumbers(currentPage, totalPages) {
931
- if (totalPages <= 7) return Array.from({ length: totalPages }, (_, i) => i + 1);
932
- const pages = [1];
933
- const current = currentPage + 1;
934
- if (current <= 4) {
935
- for (let i = 2; i <= 5; i++) pages.push(i);
936
- pages.push("...");
937
- pages.push(totalPages);
938
- } else if (current >= totalPages - 3) {
939
- pages.push("...");
940
- for (let i = totalPages - 4; i <= totalPages; i++) pages.push(i);
941
- } else {
942
- pages.push("...");
943
- for (let i = current - 1; i <= current + 1; i++) pages.push(i);
944
- pages.push("...");
945
- pages.push(totalPages);
946
- }
947
- return pages;
948
- }
949
- function DataTablePagination({ pageSizes = DEFAULT_PAGE_SIZES, className }) {
950
- const { canNextPage, canPrevPage, nextPage, prevPage, pageIndex, pageCount, setPageIndex, pageSize, setPageSize, totalRows } = useDataTablePagination();
951
- const isClientMode = pageCount > 0;
952
- const startRow = pageIndex * pageSize + 1;
953
- const endRow = Math.min((pageIndex + 1) * pageSize, totalRows);
954
- const pageNumbers = useMemo(() => getPageNumbers(pageIndex, pageCount), [pageIndex, pageCount]);
955
- return /* @__PURE__ */ jsxs("div", {
956
- className: cn("flex flex-col-reverse items-center justify-between gap-4 px-2 py-4 sm:flex-row", className),
957
- "data-slot": "dt-pagination",
958
- children: [/* @__PURE__ */ jsxs("div", {
959
- className: "flex items-center gap-4",
960
- children: [isClientMode && totalRows > 0 && /* @__PURE__ */ jsxs("span", {
961
- className: "text-sm text-muted-foreground whitespace-nowrap",
962
- children: [
963
- "Showing",
964
- " ",
965
- startRow,
966
- " ",
967
- "to",
968
- " ",
969
- endRow,
970
- " ",
971
- "of",
972
- " ",
973
- totalRows,
974
- " ",
975
- "rows"
976
- ]
977
- }), /* @__PURE__ */ jsxs("div", {
978
- className: "flex items-center gap-2",
979
- children: [/* @__PURE__ */ jsx("span", {
980
- className: "text-sm text-muted-foreground whitespace-nowrap",
981
- children: "Rows per page"
982
- }), /* @__PURE__ */ jsxs(Select, {
983
- value: String(pageSize),
984
- onValueChange: (value) => setPageSize(Number(value)),
985
- children: [/* @__PURE__ */ jsx(SelectTrigger, {
986
- className: "h-8 w-[70px]",
987
- children: /* @__PURE__ */ jsx(SelectValue, { placeholder: String(pageSize) })
988
- }), /* @__PURE__ */ jsx(SelectContent, {
989
- side: "top",
990
- children: pageSizes.map((size) => /* @__PURE__ */ jsx(SelectItem, {
991
- value: String(size),
992
- children: size
993
- }, size))
994
- })]
995
- })]
996
- })]
997
- }), /* @__PURE__ */ jsxs("nav", {
998
- "aria-label": "Table pagination",
999
- className: "flex items-center gap-1",
1000
- children: [
1001
- /* @__PURE__ */ jsx(Button, {
1002
- theme: "outline",
1003
- size: "icon",
1004
- className: "size-8",
1005
- onClick: prevPage,
1006
- disabled: !canPrevPage,
1007
- "aria-label": "Previous page",
1008
- children: /* @__PURE__ */ jsx(ChevronLeft, { className: "size-4" })
1009
- }),
1010
- isClientMode && pageCount > 1 ? pageNumbers.map((page, index) => {
1011
- if (page === "...") return /* @__PURE__ */ jsx("span", {
1012
- className: "px-2 text-sm text-muted-foreground",
1013
- children: "..."
1014
- }, `ellipsis-${index}`);
1015
- const isActive = page === pageIndex + 1;
1016
- return /* @__PURE__ */ jsx(Button, {
1017
- theme: isActive ? "solid" : "outline",
1018
- size: "small",
1019
- className: cn("h-8 min-w-8 px-2", isActive && "pointer-events-none font-semibold"),
1020
- onClick: () => setPageIndex(page - 1),
1021
- "aria-disabled": isActive || void 0,
1022
- "aria-label": `Page ${page}`,
1023
- "aria-current": isActive ? "page" : void 0,
1024
- children: page
1025
- }, page);
1026
- }) : !isClientMode && /* @__PURE__ */ jsxs("span", {
1027
- className: "px-2 text-sm text-muted-foreground",
1028
- children: [
1029
- "Page",
1030
- " ",
1031
- pageIndex + 1
1032
- ]
1033
- }),
1034
- /* @__PURE__ */ jsx(Button, {
1035
- theme: "outline",
1036
- size: "icon",
1037
- className: "size-8",
1038
- onClick: nextPage,
1039
- disabled: !canNextPage,
1040
- "aria-label": "Next page",
1041
- children: /* @__PURE__ */ jsx(ChevronRight, { className: "size-4" })
1042
- })
1043
- ]
1044
- })]
1045
- });
1046
- }
1047
- //#endregion
1048
- //#region src/components/features/data-table/components/row-actions.tsx
1049
- function DataTableRowActions({ row, actions, isLoading = false, className, responsive = true, sheetTitle = "Actions" }) {
1050
- const [open, setOpen] = useState(false);
1051
- const data = row.original;
1052
- const visibleActions = actions.filter((action) => {
1053
- if (action.hidden === void 0) return true;
1054
- return typeof action.hidden === "function" ? !action.hidden(data) : !action.hidden;
1055
- });
1056
- if (visibleActions.length === 0) return null;
1057
- return /* @__PURE__ */ jsx(ResponsiveDropdown, {
1058
- open,
1059
- onOpenChange: setOpen,
1060
- trigger: /* @__PURE__ */ jsxs(Button, {
1061
- theme: "borderless",
1062
- size: "small",
1063
- className,
1064
- disabled: isLoading,
1065
- "data-slot": "dt-row-actions",
1066
- onClick: () => setOpen(!open),
1067
- children: [/* @__PURE__ */ jsx(MoreHorizontal, { className: "size-4" }), /* @__PURE__ */ jsx("span", {
1068
- className: "sr-only",
1069
- children: "Open menu"
1070
- })]
1071
- }),
1072
- sheetTitle,
1073
- align: "end",
1074
- responsive,
1075
- children: visibleActions.map((action) => /* @__PURE__ */ jsx(ActionRow, {
1076
- action,
1077
- data,
1078
- onSelect: () => setOpen(false)
1079
- }, action.label))
1080
- });
1081
- }
1082
- //#endregion
1083
- //#region src/components/features/data-table/components/search.tsx
1084
- function DataTableSearch({ placeholder = "Search...", debounceMs = 300, className, disabled }) {
1085
- const { search, setSearch } = useDataTableSearch();
1086
- const [inputValue, setInputValue] = useState(search);
1087
- useEffect(() => {
1088
- setInputValue(search);
1089
- }, [search]);
1090
- useEffect(() => {
1091
- const timer = setTimeout(() => {
1092
- if (inputValue !== search) setSearch(inputValue);
1093
- }, debounceMs);
1094
- return () => clearTimeout(timer);
1095
- }, [
1096
- inputValue,
1097
- debounceMs,
1098
- search,
1099
- setSearch
1100
- ]);
1101
- return /* @__PURE__ */ jsx(Input, {
1102
- placeholder,
1103
- value: inputValue,
1104
- onChange: (e) => setInputValue(e.target.value),
1105
- className,
1106
- disabled,
1107
- "aria-label": placeholder,
1108
- "data-slot": "dt-search"
1109
- });
1110
- }
1111
- //#endregion
1112
- //#region src/components/features/data-table/hooks/use-data-table-client.ts
1113
- /**
1114
- * Creates a TanStack Table instance from an existing store.
1115
- * Does NOT create the store or sync data — the caller is responsible for that.
1116
- */
1117
- function useClientTable(store, options) {
1118
- const { columns, getRowId, enableRowSelection = false, stateAdapter } = options;
1119
- const resolvedColumns = useMemo(() => enableRowSelection ? withSelectionColumn(columns, typeof enableRowSelection === "object" ? enableRowSelection : {}) : columns, [columns, enableRowSelection]);
1120
- const { filteredData, sorting, rowSelection, pageIndex, pageSize: storePageSize, filters, search } = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
1121
- const table = useReactTable({
1122
- data: filteredData,
1123
- columns: resolvedColumns,
1124
- state: {
1125
- sorting,
1126
- rowSelection,
1127
- pagination: {
1128
- pageIndex,
1129
- pageSize: storePageSize
1130
- }
1131
- },
1132
- onSortingChange: (updater) => {
1133
- const next = typeof updater === "function" ? updater(sorting) : updater;
1134
- store.setSorting(next);
1135
- },
1136
- onRowSelectionChange: (updater) => {
1137
- const next = typeof updater === "function" ? updater(rowSelection) : updater;
1138
- store.setRowSelection(next);
1139
- },
1140
- onPaginationChange: (updater) => {
1141
- const next = typeof updater === "function" ? updater({
1142
- pageIndex,
1143
- pageSize: storePageSize
1144
- }) : updater;
1145
- store.setPagination(next.pageIndex, next.pageSize);
1146
- },
1147
- getCoreRowModel: getCoreRowModel(),
1148
- getSortedRowModel: getSortedRowModel(),
1149
- getPaginationRowModel: getPaginationRowModel(),
1150
- getRowId,
1151
- enableRowSelection: !!enableRowSelection
1152
- });
1153
- const hydratedRef = useRef(false);
1154
- useEffect(() => {
1155
- if (stateAdapter && !hydratedRef.current) {
1156
- hydratedRef.current = true;
1157
- const persisted = stateAdapter.read();
1158
- if (persisted.sorting && persisted.sorting.length > 0) store.setSorting(persisted.sorting);
1159
- if (persisted.filters) {
1160
- for (const [key, value] of Object.entries(persisted.filters)) if (value != null) store.setFilter(key, value);
1161
- }
1162
- if (persisted.search) store.setSearch(persisted.search);
1163
- if (persisted.pageIndex != null && persisted.pageIndex > 0) store.setPageIndex(persisted.pageIndex);
1164
- if (persisted.pageSize != null) store.setPageSize(persisted.pageSize);
1165
- }
1166
- }, []);
1167
- const isFirstWrite = useRef(true);
1168
- useEffect(() => {
1169
- if (!stateAdapter) return;
1170
- if (isFirstWrite.current) {
1171
- isFirstWrite.current = false;
1172
- return;
1173
- }
1174
- stateAdapter.write({
1175
- sorting,
1176
- filters,
1177
- search,
1178
- pageIndex,
1179
- pageSize: storePageSize
1180
- });
1181
- }, [
1182
- sorting,
1183
- filters,
1184
- search,
1185
- pageIndex,
1186
- storePageSize,
1187
- stateAdapter
1188
- ]);
1189
- return { table };
1190
- }
1191
- //#endregion
1192
- //#region src/components/features/data-table/core/client-provider.tsx
1193
- /**
1194
- * Inner component that calls useClientTable.
1195
- * Only rendered after hydration (gated by tableReady).
1196
- */
1197
- function ClientProviderInner({ store, className, children, ...options }) {
1198
- const { table } = useClientTable(store, options);
1199
- return /* @__PURE__ */ jsx(TableInstanceContext, {
1200
- value: table,
1201
- children: /* @__PURE__ */ jsx(DataTableRenderKeyContext, {
1202
- value: store.getSnapshot()._version,
1203
- children: /* @__PURE__ */ jsx("div", {
1204
- className,
1205
- children
1206
- })
1207
- })
1208
- });
1209
- }
1210
- function ClientProvider(props) {
1211
- const { data, columns, loading, pageSize, getRowId, enableRowSelection, defaultSort, defaultFilters, searchableColumns, searchFn, filterFns, stateAdapter, className, children } = props;
1212
- const store = useMemo(() => createDataTableStore({
1213
- data,
1214
- mode: "client",
1215
- isLoading: true,
1216
- defaultSort,
1217
- defaultFilters,
1218
- pageSize,
1219
- columnCount: columns.length,
1220
- searchableColumns,
1221
- searchFn,
1222
- filterFns
1223
- }), []);
1224
- const isInitialRender = useRef(true);
1225
- useEffect(() => {
1226
- if (isInitialRender.current) {
1227
- isInitialRender.current = false;
1228
- return;
1229
- }
1230
- store.setData(data);
1231
- }, [data, store]);
1232
- const isPageSizeInitial = useRef(true);
1233
- useEffect(() => {
1234
- if (isPageSizeInitial.current) {
1235
- isPageSizeInitial.current = false;
1236
- return;
1237
- }
1238
- if (pageSize != null) store.setPageSize(pageSize);
1239
- }, [pageSize, store]);
1240
- const [tableReady, setTableReady] = useState(false);
1241
- useEffect(() => setTableReady(true), []);
1242
- useEffect(() => {
1243
- if (!tableReady) return;
1244
- store.setLoading(loading ?? false);
1245
- }, [
1246
- loading,
1247
- tableReady,
1248
- store
1249
- ]);
1250
- return /* @__PURE__ */ jsx(DataTableStoreContext, {
1251
- value: store,
1252
- children: tableReady ? /* @__PURE__ */ jsx(ClientProviderInner, {
1253
- store,
1254
- columns,
1255
- getRowId,
1256
- enableRowSelection,
1257
- stateAdapter,
1258
- className,
1259
- children
1260
- }) : /* @__PURE__ */ jsx(TableInstanceContext, {
1261
- value: null,
1262
- children: /* @__PURE__ */ jsx(DataTableRenderKeyContext, {
1263
- value: 0,
1264
- children: /* @__PURE__ */ jsx("div", {
1265
- className,
1266
- children
1267
- })
1268
- })
1269
- })
1270
- });
1271
- }
1272
- //#endregion
1273
- //#region src/components/features/data-table/hooks/use-data-table-server.ts
1274
- /**
1275
- * Creates a TanStack Table instance from an existing store.
1276
- * Handles fetch-on-query-change, cursor pagination, and state adapter sync.
1277
- * Does NOT create the store — the caller is responsible for that.
1278
- */
1279
- function useServerTable(store, options) {
1280
- const { columns, fetchFn, transform, getRowId, enableRowSelection = false, stateAdapter } = options;
1281
- const fetchRef = useRef(fetchFn);
1282
- const transformRef = useRef(transform);
1283
- useEffect(() => {
1284
- fetchRef.current = fetchFn;
1285
- }, [fetchFn]);
1286
- useEffect(() => {
1287
- transformRef.current = transform;
1288
- }, [transform]);
1289
- const cursorMapRef = useRef(/* @__PURE__ */ new Map());
1290
- const hasNextPageRef = useRef(false);
1291
- const { sorting, filters, search, rowSelection, pageSize, pageIndex } = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
1292
- const prevQueryRef = useRef({
1293
- sorting,
1294
- filters,
1295
- search,
1296
- pageSize
1297
- });
1298
- useEffect(() => {
1299
- const prev = prevQueryRef.current;
1300
- if (prev.sorting !== sorting || prev.filters !== filters || prev.search !== search || prev.pageSize !== pageSize) {
1301
- cursorMapRef.current = /* @__PURE__ */ new Map();
1302
- hasNextPageRef.current = false;
1303
- if (pageIndex !== 0) {
1304
- prevQueryRef.current = {
1305
- sorting,
1306
- filters,
1307
- search,
1308
- pageSize
1309
- };
1310
- store.setPageIndex(0);
1311
- return;
1312
- }
1313
- }
1314
- prevQueryRef.current = {
1315
- sorting,
1316
- filters,
1317
- search,
1318
- pageSize
1319
- };
1320
- let cancelled = false;
1321
- store.setLoading(true);
1322
- const cursor = cursorMapRef.current.get(pageIndex);
1323
- fetchRef.current({
1324
- sorting,
1325
- filters,
1326
- search,
1327
- cursor,
1328
- limit: pageSize
1329
- }).then((response) => {
1330
- if (cancelled) return;
1331
- const result = transformRef.current(response);
1332
- store.setServerData(result.data);
1333
- store.setError(null);
1334
- if (result.cursor) cursorMapRef.current.set(pageIndex + 1, result.cursor);
1335
- hasNextPageRef.current = result.hasNextPage;
1336
- }).catch((error) => {
1337
- if (cancelled) return;
1338
- store.setServerData([]);
1339
- store.setError(error instanceof Error ? error : new Error(String(error)));
1340
- hasNextPageRef.current = false;
1341
- }).finally(() => {
1342
- if (!cancelled) store.setLoading(false);
1343
- });
1344
- return () => {
1345
- cancelled = true;
1346
- };
1347
- }, [
1348
- sorting,
1349
- filters,
1350
- search,
1351
- pageSize,
1352
- pageIndex,
1353
- store
1354
- ]);
1355
- const resolvedColumns = useMemo(() => enableRowSelection ? withSelectionColumn(columns, typeof enableRowSelection === "object" ? enableRowSelection : {}) : columns, [columns, enableRowSelection]);
1356
- const table = useReactTable({
1357
- data: store.getSnapshot().data,
1358
- columns: resolvedColumns,
1359
- state: {
1360
- sorting,
1361
- rowSelection,
1362
- pagination: {
1363
- pageIndex,
1364
- pageSize
1365
- }
1366
- },
1367
- manualPagination: true,
1368
- manualSorting: true,
1369
- manualFiltering: true,
1370
- pageCount: hasNextPageRef.current ? pageIndex + 2 : pageIndex + 1,
1371
- getCoreRowModel: getCoreRowModel(),
1372
- getRowId,
1373
- enableRowSelection: !!enableRowSelection,
1374
- onSortingChange: (updater) => {
1375
- const next = typeof updater === "function" ? updater(sorting) : updater;
1376
- store.setSorting(next);
1377
- },
1378
- onRowSelectionChange: (updater) => {
1379
- const next = typeof updater === "function" ? updater(rowSelection) : updater;
1380
- store.setRowSelection(next);
1381
- },
1382
- onPaginationChange: (updater) => {
1383
- const next = typeof updater === "function" ? updater({
1384
- pageIndex,
1385
- pageSize
1386
- }) : updater;
1387
- store.setPagination(next.pageIndex, next.pageSize);
1388
- }
1389
- });
1390
- const stateAdapterRef = useRef(stateAdapter);
1391
- useEffect(() => {
1392
- stateAdapterRef.current = stateAdapter;
1393
- }, [stateAdapter]);
1394
- useEffect(() => {
1395
- stateAdapterRef.current?.write({
1396
- sorting,
1397
- filters,
1398
- search,
1399
- pageSize
1400
- });
1401
- }, [
1402
- sorting,
1403
- filters,
1404
- search,
1405
- pageSize
1406
- ]);
1407
- return { table };
1408
- }
1409
- //#endregion
1410
- //#region src/components/features/data-table/core/server-provider.tsx
1411
- /**
1412
- * Inner component that calls useServerTable.
1413
- * Only rendered after hydration (gated by tableReady).
1414
- */
1415
- function ServerProviderInner({ store, className, children, ...options }) {
1416
- const { table } = useServerTable(store, options);
1417
- return /* @__PURE__ */ jsx(TableInstanceContext, {
1418
- value: table,
1419
- children: /* @__PURE__ */ jsx(DataTableRenderKeyContext, {
1420
- value: store.getSnapshot()._version,
1421
- children: /* @__PURE__ */ jsx("div", {
1422
- className,
1423
- children
1424
- })
1425
- })
1426
- });
1427
- }
1428
- function ServerProvider(props) {
1429
- const { columns, fetchFn, transform, limit = 20, getRowId, enableRowSelection, defaultSort, defaultFilters, stateAdapter, className, children } = props;
1430
- const store = useMemo(() => createDataTableStore({
1431
- data: [],
1432
- mode: "server",
1433
- isLoading: true,
1434
- defaultSort,
1435
- defaultFilters,
1436
- pageSize: limit,
1437
- columnCount: columns.length
1438
- }), []);
1439
- const [tableReady, setTableReady] = useState(false);
1440
- useEffect(() => setTableReady(true), []);
1441
- return /* @__PURE__ */ jsx(DataTableStoreContext, {
1442
- value: store,
1443
- children: tableReady ? /* @__PURE__ */ jsx(ServerProviderInner, {
1444
- store,
1445
- columns,
1446
- fetchFn,
1447
- transform,
1448
- limit,
1449
- getRowId,
1450
- enableRowSelection,
1451
- stateAdapter,
1452
- className,
1453
- children
1454
- }) : /* @__PURE__ */ jsx(TableInstanceContext, {
1455
- value: null,
1456
- children: /* @__PURE__ */ jsx(DataTableRenderKeyContext, {
1457
- value: 0,
1458
- children: /* @__PURE__ */ jsx("div", {
1459
- className,
1460
- children
1461
- })
1462
- })
1463
- })
1464
- });
1465
- }
1466
- //#endregion
1467
- //#region src/components/features/data-table/filters/checkbox-filter.tsx
1468
- function CheckboxFilter({ column, label, options, className, disabled, responsive, sheetTitle, modal }) {
1469
- const { filters, setFilter, clearFilter, registerFilter, unregisterFilter } = useDataTableFilters();
1470
- useEffect(() => {
1471
- registerFilter(column, "checkbox");
1472
- return () => unregisterFilter(column);
1473
- }, [
1474
- column,
1475
- registerFilter,
1476
- unregisterFilter
1477
- ]);
1478
- return /* @__PURE__ */ jsx(MultiSelect, {
1479
- options,
1480
- value: filters[column] ?? [],
1481
- onValueChange: (next) => {
1482
- if (next.length > 0) setFilter(column, next);
1483
- else clearFilter(column);
1484
- },
1485
- placeholder: label,
1486
- sheetTitle: sheetTitle ?? label,
1487
- responsive,
1488
- modalPopover: modal,
1489
- maxCount: 2,
1490
- showClearButton: true,
1491
- showSelectAll: false,
1492
- disabled,
1493
- className
1494
- });
1495
- }
1496
- //#endregion
1497
- //#region src/components/features/data-table/filters/date-picker-filter.tsx
1498
- function DatePickerFilter({ column, label, className, datePickerPopoverClassName, disableFuture, disablePast, minDate, maxDate, disabled }) {
1499
- const { filters, setFilter, clearFilter, registerFilter, unregisterFilter } = useDataTableFilters();
1500
- const rawValue = filters[column];
1501
- useEffect(() => {
1502
- registerFilter(column, "date-gte");
1503
- return () => unregisterFilter(column);
1504
- }, [
1505
- column,
1506
- registerFilter,
1507
- unregisterFilter
1508
- ]);
1509
- return /* @__PURE__ */ jsx("div", {
1510
- "data-slot": "dt-filter",
1511
- children: /* @__PURE__ */ jsx(CalendarDatePicker, {
1512
- date: useMemo(() => {
1513
- const date = rawValue ? new Date(rawValue) : void 0;
1514
- return {
1515
- from: date,
1516
- to: date
1517
- };
1518
- }, [rawValue]),
1519
- numberOfMonths: 1,
1520
- closeOnSelect: true,
1521
- placeholder: label,
1522
- triggerClassName: cn("h-10", className),
1523
- variant: "outline",
1524
- disabled,
1525
- disableFuture,
1526
- disablePast,
1527
- minDate,
1528
- maxDate,
1529
- popoverClassName: datePickerPopoverClassName,
1530
- onDateSelect: (range) => {
1531
- if (range?.from) setFilter(column, range.from.toISOString());
1532
- else clearFilter(column);
1533
- }
1534
- })
1535
- });
1536
- }
1537
- //#endregion
1538
- //#region src/components/features/data-table/filters/select-filter.tsx
1539
- function SelectFilter({ column, label, options, placeholder, searchable = true, className, selectPopoverClassName, disabled, responsive, sheetTitle, modal }) {
1540
- const { filters, setFilter, clearFilter, registerFilter, unregisterFilter } = useDataTableFilters();
1541
- const value = filters[column];
1542
- useEffect(() => {
1543
- registerFilter(column, "select");
1544
- return () => unregisterFilter(column);
1545
- }, [
1546
- column,
1547
- registerFilter,
1548
- unregisterFilter
1549
- ]);
1550
- return /* @__PURE__ */ jsx(Autocomplete, {
1551
- options,
1552
- value,
1553
- onValueChange: (next) => {
1554
- if (next === "" || next === void 0) clearFilter(column);
1555
- else setFilter(column, next);
1556
- },
1557
- placeholder: placeholder ?? label,
1558
- searchPlaceholder: `Search ${label.toLowerCase()}...`,
1559
- disableSearch: !searchable,
1560
- sheetTitle: sheetTitle ?? label,
1561
- responsive,
1562
- modal,
1563
- disabled,
1564
- className,
1565
- contentClassName: selectPopoverClassName,
1566
- triggerClassName: "h-10"
1567
- });
1568
- }
1569
- //#endregion
1570
- //#region src/components/features/data-table/data-table.tsx
1571
- const DataTable = {
1572
- Client: ClientProvider,
1573
- Server: ServerProvider,
1574
- ActiveFilters: DataTableActiveFilters,
1575
- Content: DataTableContent,
1576
- InlineContent: DataTableInlineContent,
1577
- ColumnHeader: DataTableColumnHeader,
1578
- Pagination: DataTablePagination,
1579
- Search: DataTableSearch,
1580
- RowActions: DataTableRowActions,
1581
- BulkActions: DataTableBulkActions,
1582
- Loading: DataTableLoading,
1583
- SelectFilter,
1584
- CheckboxFilter,
1585
- DatePickerFilter
1586
- };
1587
- //#endregion
1588
- export { DEFAULT_DEBOUNCE_MS, DEFAULT_LOADING_ROWS, DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZES, DataTable, createDataTableStore, createSelectionColumn, useDataTableFilters, useDataTableInlineContents, useDataTableLoading, useDataTablePagination, useDataTableRows, useDataTableSearch, useDataTableSelection, useDataTableSorting, useNuqsAdapter };
1
+ import { _ as DEFAULT_DEBOUNCE_MS, a as useDataTablePagination, b as DEFAULT_PAGE_SIZES, c as useDataTableSelection, d as resolvePath, f as rowMatchesSearch, g as useNuqsAdapter, h as createSelectionColumn, i as useDataTableLoading, l as useDataTableSorting, m as DataTableColumnHeader, n as useDataTableFilters, o as useDataTableRows, p as DataTableRowActions, r as useDataTableInlineContents, s as useDataTableSearch, t as DataTable, u as createDataTableStore, v as DEFAULT_LOADING_ROWS, y as DEFAULT_PAGE_SIZE } from "../data-table-BTIxzB7O.mjs";
2
+ export { DEFAULT_DEBOUNCE_MS, DEFAULT_LOADING_ROWS, DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZES, DataTable, DataTableColumnHeader, DataTableRowActions, createDataTableStore, createSelectionColumn, resolvePath, rowMatchesSearch, useDataTableFilters, useDataTableInlineContents, useDataTableLoading, useDataTablePagination, useDataTableRows, useDataTableSearch, useDataTableSelection, useDataTableSorting, useNuqsAdapter };