@datum-cloud/datum-ui 0.10.2 → 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 (33) 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/empty-content/empty-content.d.ts +17 -7
  6. package/dist/components/features/empty-content/empty-content.d.ts.map +1 -1
  7. package/dist/components/features/grouped-table/components/grouped-skeleton.d.ts +7 -0
  8. package/dist/components/features/grouped-table/components/grouped-skeleton.d.ts.map +1 -0
  9. package/dist/components/features/grouped-table/components/grouped-toolbar.d.ts +9 -0
  10. package/dist/components/features/grouped-table/components/grouped-toolbar.d.ts.map +1 -0
  11. package/dist/components/features/grouped-table/grouped-table.d.ts +3 -0
  12. package/dist/components/features/grouped-table/grouped-table.d.ts.map +1 -0
  13. package/dist/components/features/grouped-table/index.d.ts +3 -0
  14. package/dist/components/features/grouped-table/index.d.ts.map +1 -0
  15. package/dist/components/features/grouped-table/lib/bucket-rows.d.ts +14 -0
  16. package/dist/components/features/grouped-table/lib/bucket-rows.d.ts.map +1 -0
  17. package/dist/components/features/grouped-table/lib/compose-columns.d.ts +11 -0
  18. package/dist/components/features/grouped-table/lib/compose-columns.d.ts.map +1 -0
  19. package/dist/components/features/grouped-table/lib/sort-rows.d.ts +7 -0
  20. package/dist/components/features/grouped-table/lib/sort-rows.d.ts.map +1 -0
  21. package/dist/components/features/grouped-table/lib/use-controllable-state.d.ts +8 -0
  22. package/dist/components/features/grouped-table/lib/use-controllable-state.d.ts.map +1 -0
  23. package/dist/components/features/grouped-table/types.d.ts +57 -0
  24. package/dist/components/features/grouped-table/types.d.ts.map +1 -0
  25. package/dist/components/features/grouped-table/use-grouped-expansion.d.ts +10 -0
  26. package/dist/components/features/grouped-table/use-grouped-expansion.d.ts.map +1 -0
  27. package/dist/data-table/index.mjs +2 -1588
  28. package/dist/data-table-BTIxzB7O.mjs +1588 -0
  29. package/dist/empty-content/index.mjs +1 -1
  30. package/dist/{empty-content-CDYMYCKj.mjs → empty-content-C4Z0QVfg.mjs} +45 -31
  31. package/dist/grouped-table/index.mjs +339 -0
  32. package/dist/index.mjs +1 -1
  33. package/package.json +8 -3
@@ -0,0 +1,1588 @@
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/components/column-header.tsx
153
+ function DataTableColumnHeader({ column, title, className }) {
154
+ if (!column.getCanSort()) return /* @__PURE__ */ jsx("div", {
155
+ className: cn(className),
156
+ "data-slot": "dt-column-header",
157
+ children: title
158
+ });
159
+ const sorted = column.getIsSorted();
160
+ return /* @__PURE__ */ jsx("div", {
161
+ className: cn("flex items-center gap-2", className),
162
+ "data-slot": "dt-column-header",
163
+ children: /* @__PURE__ */ jsxs("button", {
164
+ type: "button",
165
+ className: "flex items-center gap-1 hover:text-foreground -ml-3 h-8 px-3 cursor-pointer",
166
+ onClick: column.getToggleSortingHandler(),
167
+ "aria-label": `Sort by ${title}${sorted === "asc" ? ", sorted ascending" : sorted === "desc" ? ", sorted descending" : ""}`,
168
+ 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" })]
169
+ })
170
+ });
171
+ }
172
+ //#endregion
173
+ //#region src/components/features/data-table/components/row-actions.tsx
174
+ function DataTableRowActions({ row, actions, isLoading = false, className, responsive = true, sheetTitle = "Actions" }) {
175
+ const [open, setOpen] = useState(false);
176
+ const data = row.original;
177
+ const visibleActions = actions.filter((action) => {
178
+ if (action.hidden === void 0) return true;
179
+ return typeof action.hidden === "function" ? !action.hidden(data) : !action.hidden;
180
+ });
181
+ if (visibleActions.length === 0) return null;
182
+ return /* @__PURE__ */ jsx(ResponsiveDropdown, {
183
+ open,
184
+ onOpenChange: setOpen,
185
+ trigger: /* @__PURE__ */ jsxs(Button, {
186
+ theme: "borderless",
187
+ size: "small",
188
+ className,
189
+ disabled: isLoading,
190
+ "data-slot": "dt-row-actions",
191
+ onClick: () => setOpen(!open),
192
+ children: [/* @__PURE__ */ jsx(MoreHorizontal, { className: "size-4" }), /* @__PURE__ */ jsx("span", {
193
+ className: "sr-only",
194
+ children: "Open menu"
195
+ })]
196
+ }),
197
+ sheetTitle,
198
+ align: "end",
199
+ responsive,
200
+ children: visibleActions.map((action) => /* @__PURE__ */ jsx(ActionRow, {
201
+ action,
202
+ data,
203
+ onSelect: () => setOpen(false)
204
+ }, action.label))
205
+ });
206
+ }
207
+ //#endregion
208
+ //#region src/components/features/data-table/core/filter-engine.ts
209
+ /**
210
+ * Resolve a dot-path on an object (e.g. "status.registrationApproval").
211
+ * Falls back to a flat key lookup when the path has no dots.
212
+ */
213
+ function resolvePath(obj, path) {
214
+ if (obj == null) return void 0;
215
+ const record = obj;
216
+ if (!path.includes(".")) return record[path];
217
+ return path.split(".").reduce((acc, key) => acc != null ? acc[key] : void 0, record);
218
+ }
219
+ const FILTER_STRATEGIES = {
220
+ "checkbox": (cellValue, filterValue) => {
221
+ if (filterValue == null) return true;
222
+ if (Array.isArray(filterValue) && filterValue.length === 0) return true;
223
+ if (!Array.isArray(filterValue)) return cellValue === filterValue;
224
+ if (Array.isArray(cellValue)) return cellValue.some((v) => filterValue.includes(v));
225
+ return filterValue.includes(cellValue);
226
+ },
227
+ "select": (cellValue, filterValue) => {
228
+ if (filterValue == null || filterValue === "") return true;
229
+ return cellValue === filterValue;
230
+ },
231
+ "date-gte": (cellValue, filterValue) => {
232
+ if (!filterValue) return true;
233
+ const cell = new Date(cellValue);
234
+ const filter = new Date(filterValue);
235
+ if (Number.isNaN(cell.getTime()) || Number.isNaN(filter.getTime())) return true;
236
+ return cell >= filter;
237
+ },
238
+ "date-lte": (cellValue, filterValue) => {
239
+ if (!filterValue) return true;
240
+ const cell = new Date(cellValue);
241
+ const filter = new Date(filterValue);
242
+ if (Number.isNaN(cell.getTime()) || Number.isNaN(filter.getTime())) return true;
243
+ return cell <= filter;
244
+ }
245
+ };
246
+ function resolveStrategy(strategy) {
247
+ if (!strategy) return void 0;
248
+ if (typeof strategy === "function") return strategy;
249
+ return FILTER_STRATEGIES[strategy];
250
+ }
251
+ /** True when a row matches a free-text query (custom fn → columns → all values). */
252
+ function rowMatchesSearch(row, search, config) {
253
+ if (!search || search.length === 0) return true;
254
+ if (config.searchFn) return config.searchFn(row, search);
255
+ const query = search.toLowerCase();
256
+ if (config.searchableColumns && config.searchableColumns.length > 0) return config.searchableColumns.some((col) => {
257
+ const cellValue = resolvePath(row, col);
258
+ return cellValue != null && String(cellValue).toLowerCase().includes(query);
259
+ });
260
+ return Object.values(row).some((val) => val != null && String(val).toLowerCase().includes(query));
261
+ }
262
+ function applyFilters(data, filters, search, registeredFilters, customFilterFns, searchConfig) {
263
+ const hasFilters = Object.keys(filters).length > 0;
264
+ const hasSearch = search.length > 0;
265
+ if (!hasFilters && !hasSearch) return data;
266
+ return data.filter((row) => {
267
+ if (hasFilters) for (const [column, value] of Object.entries(filters)) {
268
+ const fn = customFilterFns[column] ?? resolveStrategy(registeredFilters.get(column));
269
+ if (!fn) {
270
+ console.warn(`[DataTable] No filter strategy registered for column "${column}". Filter ignored.`);
271
+ continue;
272
+ }
273
+ if (!fn(resolvePath(row, column), value)) return false;
274
+ }
275
+ if (hasSearch && !rowMatchesSearch(row, search, searchConfig)) return false;
276
+ return true;
277
+ });
278
+ }
279
+ //#endregion
280
+ //#region src/components/features/data-table/core/store.ts
281
+ function createDataTableStore(options) {
282
+ let registeredFilters = /* @__PURE__ */ new Map();
283
+ const listeners = /* @__PURE__ */ new Set();
284
+ function computeFilteredData(s) {
285
+ if (s.mode === "server") return s.data;
286
+ return applyFilters(s.data, s.filters, s.search, registeredFilters, options.filterFns ?? {}, {
287
+ searchFn: options.searchFn,
288
+ searchableColumns: options.searchableColumns
289
+ });
290
+ }
291
+ let state = {
292
+ data: options.data,
293
+ filteredData: options.data,
294
+ sorting: options.defaultSort ?? [],
295
+ filters: options.defaultFilters ?? {},
296
+ search: "",
297
+ rowSelection: {},
298
+ pageIndex: 0,
299
+ pageSize: options.pageSize ?? 20,
300
+ columnCount: options.columnCount ?? 0,
301
+ mode: options.mode,
302
+ isLoading: options.isLoading ?? false,
303
+ error: null,
304
+ inlineContents: [],
305
+ _version: 0
306
+ };
307
+ if (options.defaultFilters && Object.keys(options.defaultFilters).length > 0) state = {
308
+ ...state,
309
+ filteredData: computeFilteredData(state)
310
+ };
311
+ function notify() {
312
+ for (const listener of listeners) listener();
313
+ }
314
+ function setState(next) {
315
+ state = {
316
+ ...next,
317
+ _version: state._version + 1
318
+ };
319
+ notify();
320
+ }
321
+ return {
322
+ getSnapshot: () => state,
323
+ subscribe: (listener) => {
324
+ listeners.add(listener);
325
+ return () => listeners.delete(listener);
326
+ },
327
+ setData: (data) => {
328
+ const next = {
329
+ ...state,
330
+ data,
331
+ pageIndex: 0,
332
+ rowSelection: {}
333
+ };
334
+ setState({
335
+ ...next,
336
+ filteredData: computeFilteredData(next)
337
+ });
338
+ },
339
+ setServerData: (data) => {
340
+ setState({
341
+ ...state,
342
+ data,
343
+ filteredData: data
344
+ });
345
+ },
346
+ setSorting: (sorting) => {
347
+ setState({
348
+ ...state,
349
+ sorting,
350
+ rowSelection: {}
351
+ });
352
+ },
353
+ setFilter: (key, value) => {
354
+ const next = {
355
+ ...state,
356
+ filters: {
357
+ ...state.filters,
358
+ [key]: value
359
+ },
360
+ rowSelection: {},
361
+ pageIndex: 0
362
+ };
363
+ setState({
364
+ ...next,
365
+ filteredData: computeFilteredData(next)
366
+ });
367
+ },
368
+ clearFilter: (key) => {
369
+ const filters = Object.fromEntries(Object.entries(state.filters).filter(([k]) => k !== key));
370
+ const next = {
371
+ ...state,
372
+ filters,
373
+ rowSelection: {},
374
+ pageIndex: 0
375
+ };
376
+ setState({
377
+ ...next,
378
+ filteredData: computeFilteredData(next)
379
+ });
380
+ },
381
+ clearAllFilters: () => {
382
+ const next = {
383
+ ...state,
384
+ filters: {},
385
+ rowSelection: {},
386
+ pageIndex: 0
387
+ };
388
+ setState({
389
+ ...next,
390
+ filteredData: computeFilteredData(next)
391
+ });
392
+ },
393
+ setSearch: (search) => {
394
+ const next = {
395
+ ...state,
396
+ search,
397
+ rowSelection: {},
398
+ pageIndex: 0
399
+ };
400
+ setState({
401
+ ...next,
402
+ filteredData: computeFilteredData(next)
403
+ });
404
+ },
405
+ clearSearch: () => {
406
+ const next = {
407
+ ...state,
408
+ search: "",
409
+ rowSelection: {},
410
+ pageIndex: 0
411
+ };
412
+ setState({
413
+ ...next,
414
+ filteredData: computeFilteredData(next)
415
+ });
416
+ },
417
+ setRowSelection: (rowSelection) => {
418
+ setState({
419
+ ...state,
420
+ rowSelection
421
+ });
422
+ },
423
+ setPageIndex: (pageIndex) => {
424
+ if (!Number.isFinite(pageIndex) || pageIndex < 0) return;
425
+ setState({
426
+ ...state,
427
+ pageIndex: Math.floor(pageIndex),
428
+ rowSelection: {}
429
+ });
430
+ },
431
+ setPageSize: (pageSize) => {
432
+ if (!Number.isFinite(pageSize) || pageSize < 1) return;
433
+ setState({
434
+ ...state,
435
+ pageSize: Math.floor(pageSize),
436
+ pageIndex: 0,
437
+ rowSelection: {}
438
+ });
439
+ },
440
+ setPagination: (pageIndex, pageSize) => {
441
+ const safeIndex = Number.isFinite(pageIndex) ? Math.max(0, Math.floor(pageIndex)) : state.pageIndex;
442
+ const safeSize = Number.isFinite(pageSize) ? Math.max(1, Math.floor(pageSize)) : state.pageSize;
443
+ setState({
444
+ ...state,
445
+ pageIndex: safeIndex,
446
+ pageSize: safeSize,
447
+ rowSelection: {}
448
+ });
449
+ },
450
+ setLoading: (isLoading) => {
451
+ setState({
452
+ ...state,
453
+ isLoading
454
+ });
455
+ },
456
+ setError: (error) => {
457
+ setState({
458
+ ...state,
459
+ error
460
+ });
461
+ },
462
+ registerFilter: (column, strategy) => {
463
+ const next = new Map(registeredFilters);
464
+ next.set(column, strategy);
465
+ registeredFilters = next;
466
+ const filteredData = computeFilteredData(state);
467
+ setState({
468
+ ...state,
469
+ filteredData
470
+ });
471
+ },
472
+ unregisterFilter: (column) => {
473
+ const next = new Map(registeredFilters);
474
+ next.delete(column);
475
+ registeredFilters = next;
476
+ if (column in state.filters) {
477
+ const filteredData = computeFilteredData(state);
478
+ setState({
479
+ ...state,
480
+ filteredData
481
+ });
482
+ }
483
+ },
484
+ registerInlineContent: (entry) => {
485
+ const existing = state.inlineContents.findIndex((e) => e.id === entry.id);
486
+ const inlineContents = existing >= 0 ? state.inlineContents.map((e, i) => i === existing ? entry : e) : [...state.inlineContents, entry];
487
+ setState({
488
+ ...state,
489
+ inlineContents
490
+ });
491
+ },
492
+ unregisterInlineContent: (id) => {
493
+ const inlineContents = state.inlineContents.filter((e) => e.id !== id);
494
+ setState({
495
+ ...state,
496
+ inlineContents
497
+ });
498
+ }
499
+ };
500
+ }
501
+ //#endregion
502
+ //#region src/components/features/data-table/core/data-table-context.tsx
503
+ const DataTableStoreContext = createContext(null);
504
+ const TableInstanceContext = createContext(null);
505
+ /**
506
+ * Monotonic counter that increments on every store mutation.
507
+ * Table-dependent hooks consume this context to force re-renders
508
+ * through React's {children} composition boundary, since the
509
+ * mutable table singleton (stable ref) cannot trigger context updates.
510
+ */
511
+ const DataTableRenderKeyContext = createContext(0);
512
+ /**
513
+ * Forces a re-render when the store changes. Used by table-dependent hooks.
514
+ * Uses useContext (not use()) because use() does not reliably register
515
+ * context subscriptions in SSR/hydration scenarios (React Router SSR),
516
+ * preventing re-renders when the context value changes.
517
+ */
518
+ function useRenderKey() {
519
+ return useContext(DataTableRenderKeyContext);
520
+ }
521
+ createContext(null);
522
+ function useDataTableStore() {
523
+ const store = use(DataTableStoreContext);
524
+ if (!store) throw new Error("useDataTableStore must be used within a <DataTable.Client> or <DataTable.Server> provider");
525
+ return store;
526
+ }
527
+ /**
528
+ * Returns the table instance or null if not yet available.
529
+ * Used by hooks that need to handle the null-table window during SSR.
530
+ */
531
+ function useTableInstanceOrNull() {
532
+ return use(TableInstanceContext);
533
+ }
534
+ //#endregion
535
+ //#region src/components/features/data-table/hooks/use-selectors.ts
536
+ function shallowEqual(a, b) {
537
+ const keysA = Object.keys(a);
538
+ const keysB = Object.keys(b);
539
+ if (keysA.length !== keysB.length) return false;
540
+ for (const key of keysA) {
541
+ const va = a[key];
542
+ const vb = b[key];
543
+ if (va === vb) continue;
544
+ if (Array.isArray(va) && Array.isArray(vb)) {
545
+ if (va.length !== vb.length) return false;
546
+ for (let i = 0; i < va.length; i++) if (va[i] !== vb[i]) return false;
547
+ continue;
548
+ }
549
+ return false;
550
+ }
551
+ return true;
552
+ }
553
+ function useSliceSelector(selector) {
554
+ const store = useDataTableStore();
555
+ const cachedRef = useRef(null);
556
+ const getSnapshot = useCallback(() => {
557
+ const next = selector(store.getSnapshot());
558
+ if (cachedRef.current && shallowEqual(cachedRef.current, next)) return cachedRef.current;
559
+ cachedRef.current = next;
560
+ return next;
561
+ }, [store, selector]);
562
+ return useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);
563
+ }
564
+ function useDataTableFilters() {
565
+ const store = useDataTableStore();
566
+ return useSliceSelector(useCallback((state) => ({
567
+ filters: state.filters,
568
+ setFilter: store.setFilter,
569
+ clearFilter: store.clearFilter,
570
+ clearAllFilters: store.clearAllFilters,
571
+ registerFilter: store.registerFilter,
572
+ unregisterFilter: store.unregisterFilter
573
+ }), [store]));
574
+ }
575
+ function useDataTableSearch() {
576
+ const store = useDataTableStore();
577
+ return useSliceSelector(useCallback((state) => ({
578
+ search: state.search,
579
+ setSearch: store.setSearch,
580
+ clearSearch: store.clearSearch
581
+ }), [store]));
582
+ }
583
+ function useDataTableSorting() {
584
+ const store = useDataTableStore();
585
+ return useSliceSelector(useCallback((state) => ({
586
+ sorting: state.sorting,
587
+ setSorting: store.setSorting
588
+ }), [store]));
589
+ }
590
+ function useDataTableSelection() {
591
+ useRenderKey();
592
+ const store = useDataTableStore();
593
+ const table = useTableInstanceOrNull();
594
+ return {
595
+ rowSelection: store.getSnapshot().rowSelection,
596
+ setRowSelection: store.setRowSelection,
597
+ selectedRows: table ? table.getFilteredSelectedRowModel().rows.map((r) => r.original) : []
598
+ };
599
+ }
600
+ function useDataTablePagination() {
601
+ useRenderKey();
602
+ const store = useDataTableStore();
603
+ const table = useTableInstanceOrNull();
604
+ const state = store.getSnapshot();
605
+ if (!table) return {
606
+ canNextPage: false,
607
+ canPrevPage: false,
608
+ nextPage: () => {},
609
+ prevPage: () => {},
610
+ pageIndex: state.pageIndex,
611
+ pageCount: 0,
612
+ setPageIndex: store.setPageIndex,
613
+ pageSize: state.pageSize,
614
+ setPageSize: store.setPageSize,
615
+ totalRows: 0
616
+ };
617
+ return {
618
+ canNextPage: table.getCanNextPage(),
619
+ canPrevPage: table.getCanPreviousPage(),
620
+ nextPage: () => table.nextPage(),
621
+ prevPage: () => table.previousPage(),
622
+ pageIndex: state.pageIndex,
623
+ pageCount: table.getPageCount(),
624
+ setPageIndex: store.setPageIndex,
625
+ pageSize: state.pageSize,
626
+ setPageSize: store.setPageSize,
627
+ totalRows: state.filteredData.length
628
+ };
629
+ }
630
+ function useDataTableRows() {
631
+ useRenderKey();
632
+ const table = useTableInstanceOrNull();
633
+ if (!table) return {
634
+ rows: [],
635
+ headerGroups: [],
636
+ totalColumns: 0
637
+ };
638
+ return {
639
+ rows: table.getRowModel().rows,
640
+ headerGroups: table.getHeaderGroups(),
641
+ totalColumns: table.getAllColumns().length
642
+ };
643
+ }
644
+ function useDataTableLoading() {
645
+ return useSliceSelector(useCallback((state) => ({
646
+ isLoading: state.isLoading,
647
+ error: state.error,
648
+ columnCount: state.columnCount
649
+ }), []));
650
+ }
651
+ function useDataTableInlineContents() {
652
+ const store = useDataTableStore();
653
+ return useSliceSelector(useCallback((state) => ({
654
+ inlineContents: state.inlineContents,
655
+ registerInlineContent: store.registerInlineContent,
656
+ unregisterInlineContent: store.unregisterInlineContent
657
+ }), [store]));
658
+ }
659
+ //#endregion
660
+ //#region src/components/features/data-table/components/active-filters.tsx
661
+ function formatValue(column, value, formatters) {
662
+ const fn = formatters?.[column];
663
+ if (fn) return fn(value);
664
+ return String(value);
665
+ }
666
+ function FilterGroup({ label, children, className }) {
667
+ return /* @__PURE__ */ jsxs("div", {
668
+ className: cn("flex items-center gap-2 rounded-md border px-2 py-1", className),
669
+ "data-slot": "dt-filter-group",
670
+ "data-testid": "dt-filter-group",
671
+ children: [/* @__PURE__ */ jsx("span", {
672
+ className: "text-muted-foreground border-r pr-2 text-xs",
673
+ children: label
674
+ }), children]
675
+ });
676
+ }
677
+ const EMPTY_LABELS = {};
678
+ function ActiveFiltersInner({ label = "Selected Filters", excludeFilters, filterLabels = EMPTY_LABELS, formatFilterValue: formatters, clearAll = "icon", clearAllLabel = "Clear all", className, groupClassName, badgeClassName }) {
679
+ const { filters, setFilter, clearFilter, clearAllFilters } = useDataTableFilters();
680
+ const { search, clearSearch } = useDataTableSearch();
681
+ const excludeSet = useMemo(() => new Set(excludeFilters ?? []), [excludeFilters]);
682
+ const activeFilterEntries = Object.entries(filters).filter(([key, value]) => !excludeSet.has(key) && value != null && value !== "" && !(Array.isArray(value) && value.length === 0));
683
+ const showSearch = search.length > 0 && !excludeSet.has("search");
684
+ const hasFilters = activeFilterEntries.length > 0;
685
+ if (!showSearch && !hasFilters) return null;
686
+ const totalGroups = activeFilterEntries.length + (showSearch ? 1 : 0);
687
+ const removeArrayItem = (column, items, item) => {
688
+ const remaining = items.filter((v) => v !== item);
689
+ if (remaining.length > 0) setFilter(column, remaining);
690
+ else clearFilter(column);
691
+ };
692
+ const handleClearAll = () => {
693
+ clearAllFilters();
694
+ if (search.length > 0) clearSearch();
695
+ };
696
+ const badgeCn = cn("flex items-center gap-1.5 px-2 py-0.5 text-xs", badgeClassName);
697
+ return /* @__PURE__ */ jsxs("div", {
698
+ className: cn("flex flex-wrap items-center gap-2", className),
699
+ "data-slot": "dt-active-filters",
700
+ "data-testid": "dt-active-filters",
701
+ children: [
702
+ label !== null && /* @__PURE__ */ jsx("span", {
703
+ className: "text-sm text-muted-foreground",
704
+ "data-slot": "dt-active-filters-label",
705
+ children: label
706
+ }),
707
+ showSearch && /* @__PURE__ */ jsx(FilterGroup, {
708
+ label: "Search",
709
+ className: groupClassName,
710
+ children: /* @__PURE__ */ jsxs(Badge, {
711
+ type: "muted",
712
+ theme: "solid",
713
+ className: badgeCn,
714
+ children: [/* @__PURE__ */ jsx("span", { children: search }), /* @__PURE__ */ jsx(Button, {
715
+ theme: "borderless",
716
+ size: "small",
717
+ "aria-label": "Clear search",
718
+ className: "h-auto p-0 text-muted-foreground hover:text-foreground",
719
+ onClick: clearSearch,
720
+ children: /* @__PURE__ */ jsx(X, {
721
+ className: "size-2.5",
722
+ "aria-hidden": "true"
723
+ })
724
+ })]
725
+ })
726
+ }),
727
+ activeFilterEntries.map(([column, value]) => {
728
+ const groupLabel = filterLabels[column] ?? column;
729
+ if (Array.isArray(value)) return /* @__PURE__ */ jsx(FilterGroup, {
730
+ label: groupLabel,
731
+ className: groupClassName,
732
+ children: value.map((item) => /* @__PURE__ */ jsxs(Badge, {
733
+ type: "muted",
734
+ theme: "solid",
735
+ className: badgeCn,
736
+ children: [/* @__PURE__ */ jsx("span", { children: formatValue(column, item, formatters) }), /* @__PURE__ */ jsx(Button, {
737
+ theme: "borderless",
738
+ size: "small",
739
+ "aria-label": `Remove ${formatValue(column, item, formatters)} from ${groupLabel}`,
740
+ className: "h-auto p-0 text-muted-foreground hover:text-foreground",
741
+ onClick: () => removeArrayItem(column, value, item),
742
+ children: /* @__PURE__ */ jsx(X, {
743
+ className: "size-2.5",
744
+ "aria-hidden": "true"
745
+ })
746
+ })]
747
+ }, item))
748
+ }, column);
749
+ return /* @__PURE__ */ jsx(FilterGroup, {
750
+ label: groupLabel,
751
+ className: groupClassName,
752
+ children: /* @__PURE__ */ jsxs(Badge, {
753
+ type: "muted",
754
+ theme: "solid",
755
+ className: badgeCn,
756
+ children: [/* @__PURE__ */ jsx("span", { children: formatValue(column, value, formatters) }), /* @__PURE__ */ jsx(Button, {
757
+ theme: "borderless",
758
+ size: "small",
759
+ "aria-label": `Clear ${groupLabel} filter`,
760
+ className: "h-auto p-0 text-muted-foreground hover:text-foreground",
761
+ onClick: () => clearFilter(column),
762
+ children: /* @__PURE__ */ jsx(X, {
763
+ className: "size-2.5",
764
+ "aria-hidden": "true"
765
+ })
766
+ })]
767
+ })
768
+ }, column);
769
+ }),
770
+ totalGroups > 1 && /* @__PURE__ */ jsxs(Fragment$1, { children: [
771
+ clearAll === "icon" && /* @__PURE__ */ jsx(Button, {
772
+ theme: "borderless",
773
+ size: "small",
774
+ "aria-label": clearAllLabel,
775
+ title: clearAllLabel,
776
+ className: "h-auto p-1 text-muted-foreground hover:text-foreground",
777
+ "data-slot": "dt-clear-all-filters",
778
+ "data-testid": "dt-clear-all-filters",
779
+ onClick: handleClearAll,
780
+ children: /* @__PURE__ */ jsx(X, { className: "size-4" })
781
+ }),
782
+ clearAll === "button" && /* @__PURE__ */ jsxs(Button, {
783
+ theme: "outline",
784
+ size: "small",
785
+ className: "h-auto px-2 py-1 text-xs",
786
+ "data-slot": "dt-clear-all-filters",
787
+ "data-testid": "dt-clear-all-filters",
788
+ onClick: handleClearAll,
789
+ children: [/* @__PURE__ */ jsx(X, { className: "size-3 mr-1" }), clearAllLabel]
790
+ }),
791
+ clearAll === "text" && /* @__PURE__ */ jsx(Button, {
792
+ theme: "borderless",
793
+ size: "small",
794
+ className: "h-auto px-1 py-0.5 text-xs text-muted-foreground hover:text-foreground",
795
+ "data-slot": "dt-clear-all-filters",
796
+ "data-testid": "dt-clear-all-filters",
797
+ onClick: handleClearAll,
798
+ children: clearAllLabel
799
+ })
800
+ ] })
801
+ ]
802
+ });
803
+ }
804
+ const DataTableActiveFilters = memo(ActiveFiltersInner);
805
+ //#endregion
806
+ //#region src/components/features/data-table/components/bulk-actions.tsx
807
+ function DataTableBulkActions({ children, className }) {
808
+ const { selectedRows } = useDataTableSelection();
809
+ if (selectedRows.length === 0) return null;
810
+ return /* @__PURE__ */ jsx("div", {
811
+ "data-slot": "dt-bulk-actions",
812
+ className,
813
+ children: children(selectedRows)
814
+ });
815
+ }
816
+ //#endregion
817
+ //#region src/components/features/data-table/components/content.tsx
818
+ function resolveClassName(value, item) {
819
+ if (typeof value === "function") return value(item);
820
+ return value;
821
+ }
822
+ function renderInlineContentRow(entry, colSpan, rows) {
823
+ return /* @__PURE__ */ jsx(TableRow, {
824
+ "data-slot": "dt-inline-content",
825
+ "data-position": entry.position,
826
+ className: cn("transition-all duration-200", entry.className),
827
+ children: /* @__PURE__ */ jsx(TableCell, {
828
+ colSpan,
829
+ children: entry.render({
830
+ onClose: entry.onClose,
831
+ rowData: entry.position === "row" ? rows.find((r) => r.id === entry.rowId)?.original ?? null : null
832
+ })
833
+ })
834
+ }, entry.id);
835
+ }
836
+ function DataTableContent({ emptyMessage, className, tableClassName, headerClassName, headerRowClassName, headerCellClassName, bodyClassName, rowClassName, cellClassName }) {
837
+ const { rows, headerGroups, totalColumns } = useDataTableRows();
838
+ const { isLoading, columnCount } = useDataTableLoading();
839
+ const { pageSize } = useDataTablePagination();
840
+ const { inlineContents } = useDataTableInlineContents();
841
+ const openInlineContents = useMemo(() => inlineContents.filter((e) => e.open), [inlineContents]);
842
+ const colSpan = totalColumns;
843
+ const skeletonColumns = totalColumns || columnCount || 5;
844
+ return /* @__PURE__ */ jsx("div", {
845
+ className: cn("datum-ui-data-table", className),
846
+ "data-slot": "dt",
847
+ style: { overflowX: "auto" },
848
+ children: /* @__PURE__ */ jsxs(Table, {
849
+ className: cn(tableClassName),
850
+ "data-slot": "dt-table",
851
+ children: [/* @__PURE__ */ jsx(TableHeader, {
852
+ className: cn(headerClassName),
853
+ "data-slot": "dt-header",
854
+ children: headerGroups.map((headerGroup) => /* @__PURE__ */ jsx(TableRow, {
855
+ className: cn(headerRowClassName),
856
+ "data-slot": "dt-header-row",
857
+ children: headerGroup.headers.map((header) => /* @__PURE__ */ jsx(TableHead, {
858
+ className: cn(headerCellClassName),
859
+ "data-slot": "dt-header-cell",
860
+ children: header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())
861
+ }, header.id))
862
+ }, headerGroup.id))
863
+ }), /* @__PURE__ */ jsxs(TableBody, {
864
+ className: cn(bodyClassName),
865
+ "data-slot": "dt-body",
866
+ children: [openInlineContents.filter((e) => e.position === "top").map((entry) => renderInlineContentRow(entry, colSpan, rows)), rows.length > 0 ? rows.map((row) => {
867
+ const rowEntry = openInlineContents.find((e) => e.position === "row" && e.rowId === row.id);
868
+ if (rowEntry) return renderInlineContentRow(rowEntry, colSpan, rows);
869
+ return /* @__PURE__ */ jsx(TableRow, {
870
+ className: cn(resolveClassName(rowClassName, row)),
871
+ style: { transitionProperty: "none" },
872
+ "data-slot": "dt-row",
873
+ "data-state": row.getIsSelected() ? "selected" : void 0,
874
+ children: row.getVisibleCells().map((cell) => /* @__PURE__ */ jsx(TableCell, {
875
+ className: cn(resolveClassName(cellClassName, cell)),
876
+ "data-slot": "dt-cell",
877
+ children: flexRender(cell.column.columnDef.cell, cell.getContext())
878
+ }, cell.id))
879
+ }, row.id);
880
+ }) : isLoading ? Array.from({ length: pageSize }, (_, i) => /* @__PURE__ */ jsx(TableRow, {
881
+ "data-slot": "dt-skeleton-row",
882
+ children: Array.from({ length: skeletonColumns }, (_, j) => /* @__PURE__ */ jsx(TableCell, {
883
+ "data-slot": "dt-skeleton-cell",
884
+ children: /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-full" })
885
+ }, j))
886
+ }, i)) : /* @__PURE__ */ jsx(TableRow, {
887
+ "data-slot": "dt-row",
888
+ children: /* @__PURE__ */ jsx(TableCell, {
889
+ colSpan,
890
+ className: "h-24 text-center",
891
+ "data-slot": "dt-empty",
892
+ children: emptyMessage ?? "No results."
893
+ })
894
+ })]
895
+ })]
896
+ })
897
+ });
898
+ }
899
+ //#endregion
900
+ //#region src/components/features/data-table/components/inline-content.tsx
901
+ function DataTableInlineContent({ position, rowId, open, onClose, className, children }) {
902
+ const id = useId();
903
+ const { registerInlineContent, unregisterInlineContent } = useDataTableInlineContents();
904
+ const initialRender = useRef(true);
905
+ useEffect(() => {
906
+ registerInlineContent({
907
+ id,
908
+ position,
909
+ rowId,
910
+ open,
911
+ onClose,
912
+ className,
913
+ render: children
914
+ });
915
+ return () => {
916
+ unregisterInlineContent(id);
917
+ };
918
+ }, [
919
+ id,
920
+ registerInlineContent,
921
+ unregisterInlineContent
922
+ ]);
923
+ useEffect(() => {
924
+ if (initialRender.current) {
925
+ initialRender.current = false;
926
+ return;
927
+ }
928
+ registerInlineContent({
929
+ id,
930
+ position,
931
+ rowId,
932
+ open,
933
+ onClose,
934
+ className,
935
+ render: children
936
+ });
937
+ }, [
938
+ id,
939
+ position,
940
+ rowId,
941
+ open,
942
+ onClose,
943
+ className,
944
+ children,
945
+ registerInlineContent
946
+ ]);
947
+ return null;
948
+ }
949
+ //#endregion
950
+ //#region src/components/features/data-table/components/loading.tsx
951
+ function DataTableLoading({ rows = 5, columns = 4, className }) {
952
+ return /* @__PURE__ */ jsx("div", {
953
+ className,
954
+ "data-slot": "dt-loading",
955
+ style: { overflowX: "auto" },
956
+ 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)) })] })
957
+ });
958
+ }
959
+ //#endregion
960
+ //#region src/components/features/data-table/components/pagination.tsx
961
+ /**
962
+ * Generates page numbers with ellipsis for large page counts.
963
+ * Shows up to 7 items: first, last, current +/- 1 neighbor, and ellipsis gaps.
964
+ */
965
+ function getPageNumbers(currentPage, totalPages) {
966
+ if (totalPages <= 7) return Array.from({ length: totalPages }, (_, i) => i + 1);
967
+ const pages = [1];
968
+ const current = currentPage + 1;
969
+ if (current <= 4) {
970
+ for (let i = 2; i <= 5; i++) pages.push(i);
971
+ pages.push("...");
972
+ pages.push(totalPages);
973
+ } else if (current >= totalPages - 3) {
974
+ pages.push("...");
975
+ for (let i = totalPages - 4; i <= totalPages; i++) pages.push(i);
976
+ } else {
977
+ pages.push("...");
978
+ for (let i = current - 1; i <= current + 1; i++) pages.push(i);
979
+ pages.push("...");
980
+ pages.push(totalPages);
981
+ }
982
+ return pages;
983
+ }
984
+ function DataTablePagination({ pageSizes = DEFAULT_PAGE_SIZES, className }) {
985
+ const { canNextPage, canPrevPage, nextPage, prevPage, pageIndex, pageCount, setPageIndex, pageSize, setPageSize, totalRows } = useDataTablePagination();
986
+ const isClientMode = pageCount > 0;
987
+ const startRow = pageIndex * pageSize + 1;
988
+ const endRow = Math.min((pageIndex + 1) * pageSize, totalRows);
989
+ const pageNumbers = useMemo(() => getPageNumbers(pageIndex, pageCount), [pageIndex, pageCount]);
990
+ return /* @__PURE__ */ jsxs("div", {
991
+ className: cn("flex flex-col-reverse items-center justify-between gap-4 px-2 py-4 sm:flex-row", className),
992
+ "data-slot": "dt-pagination",
993
+ children: [/* @__PURE__ */ jsxs("div", {
994
+ className: "flex items-center gap-4",
995
+ children: [isClientMode && totalRows > 0 && /* @__PURE__ */ jsxs("span", {
996
+ className: "text-sm text-muted-foreground whitespace-nowrap",
997
+ children: [
998
+ "Showing",
999
+ " ",
1000
+ startRow,
1001
+ " ",
1002
+ "to",
1003
+ " ",
1004
+ endRow,
1005
+ " ",
1006
+ "of",
1007
+ " ",
1008
+ totalRows,
1009
+ " ",
1010
+ "rows"
1011
+ ]
1012
+ }), /* @__PURE__ */ jsxs("div", {
1013
+ className: "flex items-center gap-2",
1014
+ children: [/* @__PURE__ */ jsx("span", {
1015
+ className: "text-sm text-muted-foreground whitespace-nowrap",
1016
+ children: "Rows per page"
1017
+ }), /* @__PURE__ */ jsxs(Select, {
1018
+ value: String(pageSize),
1019
+ onValueChange: (value) => setPageSize(Number(value)),
1020
+ children: [/* @__PURE__ */ jsx(SelectTrigger, {
1021
+ className: "h-8 w-[70px]",
1022
+ children: /* @__PURE__ */ jsx(SelectValue, { placeholder: String(pageSize) })
1023
+ }), /* @__PURE__ */ jsx(SelectContent, {
1024
+ side: "top",
1025
+ children: pageSizes.map((size) => /* @__PURE__ */ jsx(SelectItem, {
1026
+ value: String(size),
1027
+ children: size
1028
+ }, size))
1029
+ })]
1030
+ })]
1031
+ })]
1032
+ }), /* @__PURE__ */ jsxs("nav", {
1033
+ "aria-label": "Table pagination",
1034
+ className: "flex items-center gap-1",
1035
+ children: [
1036
+ /* @__PURE__ */ jsx(Button, {
1037
+ theme: "outline",
1038
+ size: "icon",
1039
+ className: "size-8",
1040
+ onClick: prevPage,
1041
+ disabled: !canPrevPage,
1042
+ "aria-label": "Previous page",
1043
+ children: /* @__PURE__ */ jsx(ChevronLeft, { className: "size-4" })
1044
+ }),
1045
+ isClientMode && pageCount > 1 ? pageNumbers.map((page, index) => {
1046
+ if (page === "...") return /* @__PURE__ */ jsx("span", {
1047
+ className: "px-2 text-sm text-muted-foreground",
1048
+ children: "..."
1049
+ }, `ellipsis-${index}`);
1050
+ const isActive = page === pageIndex + 1;
1051
+ return /* @__PURE__ */ jsx(Button, {
1052
+ theme: isActive ? "solid" : "outline",
1053
+ size: "small",
1054
+ className: cn("h-8 min-w-8 px-2", isActive && "pointer-events-none font-semibold"),
1055
+ onClick: () => setPageIndex(page - 1),
1056
+ "aria-disabled": isActive || void 0,
1057
+ "aria-label": `Page ${page}`,
1058
+ "aria-current": isActive ? "page" : void 0,
1059
+ children: page
1060
+ }, page);
1061
+ }) : !isClientMode && /* @__PURE__ */ jsxs("span", {
1062
+ className: "px-2 text-sm text-muted-foreground",
1063
+ children: [
1064
+ "Page",
1065
+ " ",
1066
+ pageIndex + 1
1067
+ ]
1068
+ }),
1069
+ /* @__PURE__ */ jsx(Button, {
1070
+ theme: "outline",
1071
+ size: "icon",
1072
+ className: "size-8",
1073
+ onClick: nextPage,
1074
+ disabled: !canNextPage,
1075
+ "aria-label": "Next page",
1076
+ children: /* @__PURE__ */ jsx(ChevronRight, { className: "size-4" })
1077
+ })
1078
+ ]
1079
+ })]
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 as _, useDataTablePagination as a, DEFAULT_PAGE_SIZES as b, useDataTableSelection as c, resolvePath as d, rowMatchesSearch as f, useNuqsAdapter as g, createSelectionColumn as h, useDataTableLoading as i, useDataTableSorting as l, DataTableColumnHeader as m, useDataTableFilters as n, useDataTableRows as o, DataTableRowActions as p, useDataTableInlineContents as r, useDataTableSearch as s, DataTable as t, createDataTableStore as u, DEFAULT_LOADING_ROWS as v, DEFAULT_PAGE_SIZE as y };