@datum-cloud/datum-ui 0.3.0-alpha.3670fb2 → 0.3.0-alpha.919cfab

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 (44) hide show
  1. package/dist/{calendar-date-picker-mlbzp3xR.mjs → calendar-date-picker-BMzLl1Yn.mjs} +2 -1
  2. package/dist/components/features/calendar-date-picker/calendar-date-picker.d.ts +2 -1
  3. package/dist/components/features/calendar-date-picker/calendar-date-picker.d.ts.map +1 -1
  4. package/dist/components/features/data-table/components/active-filters.d.ts +1 -1
  5. package/dist/components/features/data-table/components/active-filters.d.ts.map +1 -1
  6. package/dist/components/features/data-table/components/loading.d.ts.map +1 -1
  7. package/dist/components/features/data-table/components/search.d.ts +1 -1
  8. package/dist/components/features/data-table/components/search.d.ts.map +1 -1
  9. package/dist/components/features/data-table/core/client-provider.d.ts +6 -7
  10. package/dist/components/features/data-table/core/client-provider.d.ts.map +1 -1
  11. package/dist/components/features/data-table/core/data-table-context.d.ts +14 -0
  12. package/dist/components/features/data-table/core/data-table-context.d.ts.map +1 -1
  13. package/dist/components/features/data-table/core/filter-engine.d.ts +5 -0
  14. package/dist/components/features/data-table/core/filter-engine.d.ts.map +1 -1
  15. package/dist/components/features/data-table/core/server-provider.d.ts +6 -7
  16. package/dist/components/features/data-table/core/server-provider.d.ts.map +1 -1
  17. package/dist/components/features/data-table/core/store.d.ts.map +1 -1
  18. package/dist/components/features/data-table/data-table.d.ts +1 -1
  19. package/dist/components/features/data-table/filters/checkbox-filter.d.ts +1 -1
  20. package/dist/components/features/data-table/filters/checkbox-filter.d.ts.map +1 -1
  21. package/dist/components/features/data-table/filters/date-picker-filter.d.ts +1 -1
  22. package/dist/components/features/data-table/filters/date-picker-filter.d.ts.map +1 -1
  23. package/dist/components/features/data-table/filters/select-filter.d.ts +1 -1
  24. package/dist/components/features/data-table/filters/select-filter.d.ts.map +1 -1
  25. package/dist/components/features/data-table/hooks/index.d.ts +1 -1
  26. package/dist/components/features/data-table/hooks/index.d.ts.map +1 -1
  27. package/dist/components/features/data-table/hooks/use-data-table-client.d.ts +2 -15
  28. package/dist/components/features/data-table/hooks/use-data-table-client.d.ts.map +1 -1
  29. package/dist/components/features/data-table/hooks/use-data-table-server.d.ts +2 -25
  30. package/dist/components/features/data-table/hooks/use-data-table-server.d.ts.map +1 -1
  31. package/dist/components/features/data-table/hooks/use-is-client.d.ts +8 -0
  32. package/dist/components/features/data-table/hooks/use-is-client.d.ts.map +1 -0
  33. package/dist/components/features/data-table/hooks/use-selectors.d.ts +4 -35
  34. package/dist/components/features/data-table/hooks/use-selectors.d.ts.map +1 -1
  35. package/dist/components/features/data-table/index.d.ts +2 -4
  36. package/dist/components/features/data-table/index.d.ts.map +1 -1
  37. package/dist/components/features/data-table/types.d.ts +23 -30
  38. package/dist/components/features/data-table/types.d.ts.map +1 -1
  39. package/dist/data-table/index.mjs +383 -358
  40. package/dist/date-picker/index.mjs +1 -1
  41. package/dist/index.mjs +1 -1
  42. package/package.json +55 -1
  43. package/dist/components/features/data-table/hooks/use-data-table-context.d.ts +0 -2
  44. package/dist/components/features/data-table/hooks/use-data-table-context.d.ts.map +0 -1
@@ -14,9 +14,9 @@ import { i as PopoverTrigger, r as PopoverContent, t as Popover } from "../popov
14
14
  import { i as SelectItem, l as SelectTrigger, n as SelectContent, t as Select, u as SelectValue } from "../select-CwVIFWFO.mjs";
15
15
  import { t as Skeleton } from "../skeleton-Cs6Q5GQc.mjs";
16
16
  import { c as TableRow, i as TableCell, n as TableBody, o as TableHead, s as TableHeader, t as Table } from "../table-Dc3HfbM4.mjs";
17
- import { t as CalendarDatePicker } from "../calendar-date-picker-mlbzp3xR.mjs";
17
+ import { t as CalendarDatePicker } from "../calendar-date-picker-BMzLl1Yn.mjs";
18
18
  import { ArrowDown, ArrowUp, ArrowUpDown, Check, ChevronDown, ChevronLeft, ChevronRight, MoreHorizontal, X } from "lucide-react";
19
- import { createContext, memo, use, useCallback, useEffect, useId, useMemo, useRef, useState, useSyncExternalStore } from "react";
19
+ import { createContext, memo, use, useCallback, useContext, useEffect, useId, useMemo, useRef, useState, useSyncExternalStore } from "react";
20
20
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
21
21
  import { parseAsInteger, parseAsString, useQueryStates } from "nuqs";
22
22
  import { flexRender, getCoreRowModel, getPaginationRowModel, getSortedRowModel, useReactTable } from "@tanstack/react-table";
@@ -157,6 +157,16 @@ function withSelectionColumn(columns, options = {}) {
157
157
 
158
158
  //#endregion
159
159
  //#region src/components/features/data-table/core/filter-engine.ts
160
+ /**
161
+ * Resolve a dot-path on an object (e.g. "status.registrationApproval").
162
+ * Falls back to a flat key lookup when the path has no dots.
163
+ */
164
+ function resolvePath(obj, path) {
165
+ if (obj == null) return void 0;
166
+ const record = obj;
167
+ if (!path.includes(".")) return record[path];
168
+ return path.split(".").reduce((acc, key) => acc != null ? acc[key] : void 0, record);
169
+ }
160
170
  const FILTER_STRATEGIES = {
161
171
  "checkbox": (cellValue, filterValue) => {
162
172
  if (filterValue == null) return true;
@@ -200,14 +210,13 @@ function applyFilters(data, filters, search, registeredFilters, customFilterFns,
200
210
  console.warn(`[DataTable] No filter strategy registered for column "${column}". Filter ignored.`);
201
211
  continue;
202
212
  }
203
- const cellValue = row[column];
204
- if (!fn(cellValue, value)) return false;
213
+ if (!fn(resolvePath(row, column), value)) return false;
205
214
  }
206
215
  if (hasSearch) {
207
216
  const query = search.toLowerCase();
208
217
  if (searchConfig.searchFn) return searchConfig.searchFn(row, search);
209
218
  if (searchConfig.searchableColumns && searchConfig.searchableColumns.length > 0) return searchConfig.searchableColumns.some((col) => {
210
- const cellValue = row[col];
219
+ const cellValue = resolvePath(row, col);
211
220
  return cellValue != null && String(cellValue).toLowerCase().includes(query);
212
221
  });
213
222
  return Object.values(row).some((val) => {
@@ -243,7 +252,8 @@ function createDataTableStore(options) {
243
252
  mode: options.mode,
244
253
  isLoading: false,
245
254
  error: null,
246
- inlineContents: []
255
+ inlineContents: [],
256
+ _version: 0
247
257
  };
248
258
  if (options.defaultFilters && Object.keys(options.defaultFilters).length > 0) state = {
249
259
  ...state,
@@ -253,7 +263,10 @@ function createDataTableStore(options) {
253
263
  for (const listener of listeners) listener();
254
264
  }
255
265
  function setState(next) {
256
- state = next;
266
+ state = {
267
+ ...next,
268
+ _version: state._version + 1
269
+ };
257
270
  notify();
258
271
  }
259
272
  return {
@@ -442,6 +455,22 @@ function createDataTableStore(options) {
442
455
  //#region src/components/features/data-table/core/data-table-context.tsx
443
456
  const DataTableStoreContext = createContext(null);
444
457
  const TableInstanceContext = createContext(null);
458
+ /**
459
+ * Monotonic counter that increments on every store mutation.
460
+ * Table-dependent hooks consume this context to force re-renders
461
+ * through React's {children} composition boundary, since the
462
+ * mutable table singleton (stable ref) cannot trigger context updates.
463
+ */
464
+ const DataTableRenderKeyContext = createContext(0);
465
+ /**
466
+ * Forces a re-render when the store changes. Used by table-dependent hooks.
467
+ * Uses useContext (not use()) because use() does not reliably register
468
+ * context subscriptions in SSR/hydration scenarios (React Router SSR),
469
+ * preventing re-renders when the context value changes.
470
+ */
471
+ function useRenderKey() {
472
+ return useContext(DataTableRenderKeyContext);
473
+ }
445
474
  const DataTableContext = createContext(null);
446
475
  function useDataTableStore() {
447
476
  const store = use(DataTableStoreContext);
@@ -482,7 +511,7 @@ function useSliceSelector(selector) {
482
511
  cachedRef.current = next;
483
512
  return next;
484
513
  }, [store, selector]);
485
- return useSyncExternalStore(store.subscribe, getSnapshot);
514
+ return useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);
486
515
  }
487
516
  function useDataTableFilters() {
488
517
  const store = useDataTableStore();
@@ -511,20 +540,23 @@ function useDataTableSorting() {
511
540
  }), [store]));
512
541
  }
513
542
  function useDataTableSelection() {
543
+ useRenderKey();
514
544
  const store = useDataTableStore();
515
545
  const table = useTableInstance();
516
- return useSliceSelector(useCallback((state) => ({
517
- rowSelection: state.rowSelection,
546
+ return {
547
+ rowSelection: store.getSnapshot().rowSelection,
518
548
  setRowSelection: store.setRowSelection,
519
549
  selectedRows: table.getFilteredSelectedRowModel().rows.map((r) => r.original)
520
- }), [store, table]));
550
+ };
521
551
  }
522
552
  function useDataTablePagination() {
553
+ useRenderKey();
523
554
  const store = useDataTableStore();
524
555
  const table = useTableInstance();
556
+ const state = store.getSnapshot();
525
557
  const nextPage = useCallback(() => table.nextPage(), [table]);
526
558
  const prevPage = useCallback(() => table.previousPage(), [table]);
527
- return useSliceSelector(useCallback((state) => ({
559
+ return {
528
560
  canNextPage: table.getCanNextPage(),
529
561
  canPrevPage: table.getCanPreviousPage(),
530
562
  nextPage,
@@ -535,20 +567,16 @@ function useDataTablePagination() {
535
567
  pageSize: state.pageSize,
536
568
  setPageSize: store.setPageSize,
537
569
  totalRows: state.filteredData.length
538
- }), [
539
- store,
540
- table,
541
- nextPage,
542
- prevPage
543
- ]));
570
+ };
544
571
  }
545
572
  function useDataTableRows() {
573
+ useRenderKey();
546
574
  const table = useTableInstance();
547
- return useSliceSelector(useCallback((_state) => ({
575
+ return {
548
576
  rows: table.getRowModel().rows,
549
577
  headerGroups: table.getHeaderGroups(),
550
578
  totalColumns: table.getAllColumns().length
551
- }), [table]));
579
+ };
552
580
  }
553
581
  function useDataTableLoading() {
554
582
  return useSliceSelector(useCallback((state) => ({
@@ -564,41 +592,6 @@ function useDataTableInlineContents() {
564
592
  unregisterInlineContent: store.unregisterInlineContent
565
593
  }), [store]));
566
594
  }
567
- function useDataTableContext() {
568
- const store = useDataTableStore();
569
- const table = useTableInstance();
570
- const state = useSyncExternalStore(store.subscribe, store.getSnapshot);
571
- return {
572
- table,
573
- mode: state.mode,
574
- sorting: state.sorting,
575
- filters: state.filters,
576
- search: state.search,
577
- rowSelection: state.rowSelection,
578
- isLoading: state.isLoading,
579
- setSorting: store.setSorting,
580
- setFilter: store.setFilter,
581
- clearFilter: store.clearFilter,
582
- clearAllFilters: store.clearAllFilters,
583
- setSearch: store.setSearch,
584
- clearSearch: store.clearSearch,
585
- setRowSelection: store.setRowSelection,
586
- pagination: {
587
- canNextPage: table.getCanNextPage(),
588
- canPrevPage: table.getCanPreviousPage(),
589
- nextPage: () => table.nextPage(),
590
- prevPage: () => table.previousPage(),
591
- pageIndex: state.pageIndex,
592
- pageCount: table.getPageCount(),
593
- setPageIndex: store.setPageIndex,
594
- pageSize: state.pageSize,
595
- setPageSize: store.setPageSize
596
- },
597
- inlineContents: state.inlineContents,
598
- registerInlineContent: store.registerInlineContent,
599
- unregisterInlineContent: store.unregisterInlineContent
600
- };
601
- }
602
595
 
603
596
  //#endregion
604
597
  //#region src/components/features/data-table/components/active-filters.tsx
@@ -621,14 +614,15 @@ function FilterGroup({ label, children, className }) {
621
614
  });
622
615
  }
623
616
  const EMPTY_LABELS = {};
624
- function ActiveFiltersInner({ label = "Selected Filters", filterLabels = EMPTY_LABELS, formatFilterValue: formatter, clearAll = "icon", clearAllLabel = "Clear all", className, groupClassName, badgeClassName }) {
617
+ function ActiveFiltersInner({ label = "Selected Filters", excludeFilters, filterLabels = EMPTY_LABELS, formatFilterValue: formatter, clearAll = "icon", clearAllLabel = "Clear all", className, groupClassName, badgeClassName }) {
625
618
  const { filters, setFilter, clearFilter, clearAllFilters } = useDataTableFilters();
626
619
  const { search, clearSearch } = useDataTableSearch();
627
- const activeFilterEntries = Object.entries(filters).filter(([, value]) => value != null && value !== "" && !(Array.isArray(value) && value.length === 0));
628
- const hasSearch = search.length > 0;
620
+ const excludeSet = useMemo(() => new Set(excludeFilters ?? []), [excludeFilters]);
621
+ const activeFilterEntries = Object.entries(filters).filter(([key, value]) => !excludeSet.has(key) && value != null && value !== "" && !(Array.isArray(value) && value.length === 0));
622
+ const showSearch = search.length > 0 && !excludeSet.has("search");
629
623
  const hasFilters = activeFilterEntries.length > 0;
630
- if (!hasSearch && !hasFilters) return null;
631
- const totalGroups = activeFilterEntries.length + (hasSearch ? 1 : 0);
624
+ if (!showSearch && !hasFilters) return null;
625
+ const totalGroups = activeFilterEntries.length + (showSearch ? 1 : 0);
632
626
  const removeArrayItem = (column, items, item) => {
633
627
  const remaining = items.filter((v) => v !== item);
634
628
  if (remaining.length > 0) setFilter(column, remaining);
@@ -636,7 +630,7 @@ function ActiveFiltersInner({ label = "Selected Filters", filterLabels = EMPTY_L
636
630
  };
637
631
  const handleClearAll = () => {
638
632
  clearAllFilters();
639
- if (hasSearch) clearSearch();
633
+ if (search.length > 0) clearSearch();
640
634
  };
641
635
  const badgeCn = cn("flex items-center gap-1.5 px-2 py-0.5 text-xs", badgeClassName);
642
636
  return /* @__PURE__ */ jsxs("div", {
@@ -649,7 +643,7 @@ function ActiveFiltersInner({ label = "Selected Filters", filterLabels = EMPTY_L
649
643
  "data-slot": "dt-active-filters-label",
650
644
  children: label
651
645
  }),
652
- hasSearch && /* @__PURE__ */ jsx(FilterGroup, {
646
+ showSearch && /* @__PURE__ */ jsx(FilterGroup, {
653
647
  label: "Search",
654
648
  className: groupClassName,
655
649
  children: /* @__PURE__ */ jsxs(Badge, {
@@ -914,19 +908,8 @@ function DataTableLoading({ rows = DEFAULT_LOADING_ROWS, columns = 4, className
914
908
  return /* @__PURE__ */ jsx("div", {
915
909
  className,
916
910
  "data-slot": "dt-loading",
917
- children: /* @__PURE__ */ jsxs("div", {
918
- className: "rounded-md border",
919
- children: [/* @__PURE__ */ jsx("div", {
920
- className: "border-b",
921
- children: /* @__PURE__ */ jsx("div", {
922
- className: "flex gap-4 p-4",
923
- children: Array.from({ length: columns }, (_, i) => /* @__PURE__ */ jsx(Skeleton, { className: "h-4 flex-1" }, i))
924
- })
925
- }), Array.from({ length: rows }, (_, rowIndex) => /* @__PURE__ */ jsx("div", {
926
- className: "flex gap-4 border-b p-4 last:border-b-0",
927
- children: Array.from({ length: columns }, (_, colIndex) => /* @__PURE__ */ jsx(Skeleton, { className: "h-4 flex-1" }, colIndex))
928
- }, rowIndex))]
929
- })
911
+ style: { overflowX: "auto" },
912
+ 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)) })] })
930
913
  });
931
914
  }
932
915
 
@@ -1025,9 +1008,9 @@ function DataTablePagination({ pageSizes = DEFAULT_PAGE_SIZES, className }) {
1025
1008
  return /* @__PURE__ */ jsx(Button, {
1026
1009
  theme: isActive ? "solid" : "outline",
1027
1010
  size: "small",
1028
- className: cn("h-8 min-w-8 px-2", isActive && "font-semibold"),
1011
+ className: cn("h-8 min-w-8 px-2", isActive && "pointer-events-none font-semibold"),
1029
1012
  onClick: () => setPageIndex(page - 1),
1030
- disabled: isActive,
1013
+ "aria-disabled": isActive || void 0,
1031
1014
  "aria-label": `Page ${page}`,
1032
1015
  "aria-current": isActive ? "page" : void 0,
1033
1016
  children: page
@@ -1091,7 +1074,7 @@ function DataTableRowActions({ row, actions, isLoading = false, className }) {
1091
1074
 
1092
1075
  //#endregion
1093
1076
  //#region src/components/features/data-table/components/search.tsx
1094
- function DataTableSearch({ placeholder = "Search...", debounceMs = DEFAULT_DEBOUNCE_MS, className }) {
1077
+ function DataTableSearch({ placeholder = "Search...", debounceMs = DEFAULT_DEBOUNCE_MS, className, disabled }) {
1095
1078
  const { search, setSearch } = useDataTableSearch();
1096
1079
  const [inputValue, setInputValue] = useState(search);
1097
1080
  useEffect(() => {
@@ -1113,68 +1096,345 @@ function DataTableSearch({ placeholder = "Search...", debounceMs = DEFAULT_DEBOU
1113
1096
  value: inputValue,
1114
1097
  onChange: (e) => setInputValue(e.target.value),
1115
1098
  className,
1099
+ disabled,
1116
1100
  "aria-label": placeholder,
1117
1101
  "data-slot": "dt-search"
1118
1102
  });
1119
1103
  }
1120
1104
 
1121
1105
  //#endregion
1122
- //#region src/components/features/data-table/core/client-provider.tsx
1123
- function ClientProvider({ store, table, className, children }) {
1124
- return /* @__PURE__ */ jsx(DataTableStoreContext, {
1125
- value: store,
1126
- children: /* @__PURE__ */ jsx(TableInstanceContext, {
1127
- value: table,
1128
- children: /* @__PURE__ */ jsx("div", {
1129
- className,
1130
- children
1131
- })
1132
- })
1106
+ //#region src/components/features/data-table/hooks/use-data-table-client.ts
1107
+ function useDataTableClient(options) {
1108
+ const { data, columns, pageSize, getRowId, enableRowSelection = false, defaultSort, defaultFilters, searchableColumns, searchFn, filterFns, stateAdapter } = options;
1109
+ const store = useMemo(() => createDataTableStore({
1110
+ data,
1111
+ mode: "client",
1112
+ defaultSort,
1113
+ defaultFilters,
1114
+ pageSize,
1115
+ searchableColumns,
1116
+ searchFn,
1117
+ filterFns
1118
+ }), []);
1119
+ const isInitialRender = useRef(true);
1120
+ useEffect(() => {
1121
+ if (isInitialRender.current) {
1122
+ isInitialRender.current = false;
1123
+ return;
1124
+ }
1125
+ store.setData(data);
1126
+ }, [data, store]);
1127
+ const resolvedColumns = useMemo(() => enableRowSelection ? withSelectionColumn(columns, typeof enableRowSelection === "object" ? enableRowSelection : {}) : columns, [columns, enableRowSelection]);
1128
+ const { filteredData, sorting, rowSelection, pageIndex, pageSize: storePageSize, filters, search } = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
1129
+ const table = useReactTable({
1130
+ data: filteredData,
1131
+ columns: resolvedColumns,
1132
+ state: {
1133
+ sorting,
1134
+ rowSelection,
1135
+ pagination: {
1136
+ pageIndex,
1137
+ pageSize: storePageSize
1138
+ }
1139
+ },
1140
+ onSortingChange: (updater) => {
1141
+ const next = typeof updater === "function" ? updater(sorting) : updater;
1142
+ store.setSorting(next);
1143
+ },
1144
+ onRowSelectionChange: (updater) => {
1145
+ const next = typeof updater === "function" ? updater(rowSelection) : updater;
1146
+ store.setRowSelection(next);
1147
+ },
1148
+ onPaginationChange: (updater) => {
1149
+ const next = typeof updater === "function" ? updater({
1150
+ pageIndex,
1151
+ pageSize: storePageSize
1152
+ }) : updater;
1153
+ store.setPagination(next.pageIndex, next.pageSize);
1154
+ },
1155
+ getCoreRowModel: getCoreRowModel(),
1156
+ getSortedRowModel: getSortedRowModel(),
1157
+ getPaginationRowModel: getPaginationRowModel(),
1158
+ getRowId,
1159
+ enableRowSelection: !!enableRowSelection
1133
1160
  });
1161
+ const hydratedRef = useRef(false);
1162
+ useEffect(() => {
1163
+ if (stateAdapter && !hydratedRef.current) {
1164
+ hydratedRef.current = true;
1165
+ const persisted = stateAdapter.read();
1166
+ if (persisted.sorting && persisted.sorting.length > 0) store.setSorting(persisted.sorting);
1167
+ if (persisted.filters) {
1168
+ for (const [key, value] of Object.entries(persisted.filters)) if (value != null) store.setFilter(key, value);
1169
+ }
1170
+ if (persisted.search) store.setSearch(persisted.search);
1171
+ if (persisted.pageIndex != null && persisted.pageIndex > 0) store.setPageIndex(persisted.pageIndex);
1172
+ if (persisted.pageSize != null) store.setPageSize(persisted.pageSize);
1173
+ }
1174
+ }, []);
1175
+ const isFirstWrite = useRef(true);
1176
+ useEffect(() => {
1177
+ if (!stateAdapter) return;
1178
+ if (isFirstWrite.current) {
1179
+ isFirstWrite.current = false;
1180
+ return;
1181
+ }
1182
+ stateAdapter.write({
1183
+ sorting,
1184
+ filters,
1185
+ search,
1186
+ pageIndex,
1187
+ pageSize: storePageSize
1188
+ });
1189
+ }, [
1190
+ sorting,
1191
+ filters,
1192
+ search,
1193
+ pageIndex,
1194
+ storePageSize,
1195
+ stateAdapter
1196
+ ]);
1197
+ return {
1198
+ store,
1199
+ table
1200
+ };
1134
1201
  }
1135
1202
 
1136
1203
  //#endregion
1137
- //#region src/components/features/data-table/core/server-provider.tsx
1138
- function ServerProvider({ store, table, className, children }) {
1204
+ //#region src/components/features/data-table/hooks/use-is-client.ts
1205
+ /**
1206
+ * Returns `false` during SSR and `true` after hydration.
1207
+ * Used to gate components that depend on client-only APIs
1208
+ * (e.g. TanStack Table's internal useSyncExternalStore call
1209
+ * which omits the getServerSnapshot argument).
1210
+ */
1211
+ function useIsClient() {
1212
+ const [isClient, setIsClient] = useState(false);
1213
+ useEffect(() => setIsClient(true), []);
1214
+ return isClient;
1215
+ }
1216
+
1217
+ //#endregion
1218
+ //#region src/components/features/data-table/core/client-provider.tsx
1219
+ /**
1220
+ * Inner component that calls useDataTableClient.
1221
+ * Only rendered on the client (gated by ClientProvider).
1222
+ */
1223
+ function ClientProviderInner({ className, children, ssrFallback: _ssrFallback, ...options }) {
1224
+ const { store, table } = useDataTableClient(options);
1139
1225
  return /* @__PURE__ */ jsx(DataTableStoreContext, {
1140
1226
  value: store,
1141
1227
  children: /* @__PURE__ */ jsx(TableInstanceContext, {
1142
1228
  value: table,
1143
- children: /* @__PURE__ */ jsx("div", {
1144
- className,
1145
- children
1229
+ children: /* @__PURE__ */ jsx(DataTableRenderKeyContext, {
1230
+ value: store.getSnapshot()._version,
1231
+ children: /* @__PURE__ */ jsx("div", {
1232
+ className,
1233
+ children
1234
+ })
1146
1235
  })
1147
1236
  })
1148
1237
  });
1149
1238
  }
1239
+ function ClientProvider(props) {
1240
+ if (!useIsClient()) return /* @__PURE__ */ jsx(Fragment$1, { children: props.ssrFallback === void 0 ? /* @__PURE__ */ jsx(DataTableLoading, { columns: props.columns.length }) : props.ssrFallback });
1241
+ return /* @__PURE__ */ jsx(ClientProviderInner, { ...props });
1242
+ }
1150
1243
 
1151
1244
  //#endregion
1152
- //#region src/components/features/data-table/filters/checkbox-filter.tsx
1153
- const MAX_VISIBLE_BADGES = 2;
1154
- function CheckboxFilter({ column, label, options, className, checkboxPopoverClassName }) {
1155
- const { filters, setFilter, clearFilter, registerFilter, unregisterFilter } = useDataTableFilters();
1156
- const [open, setOpen] = useState(false);
1245
+ //#region src/components/features/data-table/hooks/use-data-table-server.ts
1246
+ function useDataTableServer(options) {
1247
+ const { columns, fetchFn, transform, limit = 20, getRowId, enableRowSelection = false, defaultSort, defaultFilters, stateAdapter } = options;
1248
+ const fetchRef = useRef(fetchFn);
1249
+ const transformRef = useRef(transform);
1157
1250
  useEffect(() => {
1158
- registerFilter(column, "checkbox");
1159
- return () => unregisterFilter(column);
1251
+ fetchRef.current = fetchFn;
1252
+ }, [fetchFn]);
1253
+ useEffect(() => {
1254
+ transformRef.current = transform;
1255
+ }, [transform]);
1256
+ const cursorMapRef = useRef(/* @__PURE__ */ new Map());
1257
+ const hasNextPageRef = useRef(false);
1258
+ const store = useMemo(() => createDataTableStore({
1259
+ data: [],
1260
+ mode: "server",
1261
+ defaultSort,
1262
+ defaultFilters,
1263
+ pageSize: limit
1264
+ }), []);
1265
+ const { sorting, filters, search, rowSelection, pageSize, pageIndex } = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
1266
+ const prevQueryRef = useRef({
1267
+ sorting,
1268
+ filters,
1269
+ search,
1270
+ pageSize
1271
+ });
1272
+ useEffect(() => {
1273
+ const prev = prevQueryRef.current;
1274
+ if (prev.sorting !== sorting || prev.filters !== filters || prev.search !== search || prev.pageSize !== pageSize) {
1275
+ cursorMapRef.current = /* @__PURE__ */ new Map();
1276
+ hasNextPageRef.current = false;
1277
+ if (pageIndex !== 0) {
1278
+ prevQueryRef.current = {
1279
+ sorting,
1280
+ filters,
1281
+ search,
1282
+ pageSize
1283
+ };
1284
+ store.setPageIndex(0);
1285
+ return;
1286
+ }
1287
+ }
1288
+ prevQueryRef.current = {
1289
+ sorting,
1290
+ filters,
1291
+ search,
1292
+ pageSize
1293
+ };
1294
+ let cancelled = false;
1295
+ store.setLoading(true);
1296
+ const cursor = cursorMapRef.current.get(pageIndex);
1297
+ fetchRef.current({
1298
+ sorting,
1299
+ filters,
1300
+ search,
1301
+ cursor,
1302
+ limit: pageSize
1303
+ }).then((response) => {
1304
+ if (cancelled) return;
1305
+ const result = transformRef.current(response);
1306
+ store.setServerData(result.data);
1307
+ store.setError(null);
1308
+ if (result.cursor) cursorMapRef.current.set(pageIndex + 1, result.cursor);
1309
+ hasNextPageRef.current = result.hasNextPage;
1310
+ }).catch((error) => {
1311
+ if (cancelled) return;
1312
+ store.setServerData([]);
1313
+ store.setError(error instanceof Error ? error : new Error(String(error)));
1314
+ hasNextPageRef.current = false;
1315
+ }).finally(() => {
1316
+ if (!cancelled) store.setLoading(false);
1317
+ });
1318
+ return () => {
1319
+ cancelled = true;
1320
+ };
1160
1321
  }, [
1161
- column,
1162
- registerFilter,
1163
- unregisterFilter
1322
+ sorting,
1323
+ filters,
1324
+ search,
1325
+ pageSize,
1326
+ pageIndex,
1327
+ store
1164
1328
  ]);
1165
- const selectedValues = filters[column] ?? [];
1166
- const updateValues = (newValues) => {
1167
- if (newValues.length > 0) setFilter(column, newValues);
1168
- else clearFilter(column);
1169
- };
1170
- const handleToggle = (optionValue, checked) => {
1171
- updateValues(checked ? [...selectedValues, optionValue] : selectedValues.filter((v) => v !== optionValue));
1172
- };
1173
- const removeValue = (optionValue) => {
1174
- updateValues(selectedValues.filter((v) => v !== optionValue));
1175
- };
1176
- const visibleBadges = selectedValues.slice(0, MAX_VISIBLE_BADGES);
1177
- const remainingCount = selectedValues.length - MAX_VISIBLE_BADGES;
1329
+ const resolvedColumns = useMemo(() => enableRowSelection ? withSelectionColumn(columns, typeof enableRowSelection === "object" ? enableRowSelection : {}) : columns, [columns, enableRowSelection]);
1330
+ const table = useReactTable({
1331
+ data: store.getSnapshot().data,
1332
+ columns: resolvedColumns,
1333
+ state: {
1334
+ sorting,
1335
+ rowSelection,
1336
+ pagination: {
1337
+ pageIndex,
1338
+ pageSize
1339
+ }
1340
+ },
1341
+ manualPagination: true,
1342
+ manualSorting: true,
1343
+ manualFiltering: true,
1344
+ pageCount: hasNextPageRef.current ? pageIndex + 2 : pageIndex + 1,
1345
+ getCoreRowModel: getCoreRowModel(),
1346
+ getRowId,
1347
+ enableRowSelection: !!enableRowSelection,
1348
+ onSortingChange: (updater) => {
1349
+ const next = typeof updater === "function" ? updater(sorting) : updater;
1350
+ store.setSorting(next);
1351
+ },
1352
+ onRowSelectionChange: (updater) => {
1353
+ const next = typeof updater === "function" ? updater(rowSelection) : updater;
1354
+ store.setRowSelection(next);
1355
+ },
1356
+ onPaginationChange: (updater) => {
1357
+ const next = typeof updater === "function" ? updater({
1358
+ pageIndex,
1359
+ pageSize
1360
+ }) : updater;
1361
+ store.setPagination(next.pageIndex, next.pageSize);
1362
+ }
1363
+ });
1364
+ useEffect(() => {
1365
+ if (stateAdapter) stateAdapter.write({
1366
+ sorting,
1367
+ filters,
1368
+ search,
1369
+ pageSize
1370
+ });
1371
+ }, [
1372
+ sorting,
1373
+ filters,
1374
+ search,
1375
+ pageSize,
1376
+ stateAdapter
1377
+ ]);
1378
+ return {
1379
+ store,
1380
+ table
1381
+ };
1382
+ }
1383
+
1384
+ //#endregion
1385
+ //#region src/components/features/data-table/core/server-provider.tsx
1386
+ /**
1387
+ * Inner component that calls useDataTableServer.
1388
+ * Only rendered on the client (gated by ServerProvider).
1389
+ */
1390
+ function ServerProviderInner({ className, children, ssrFallback: _ssrFallback, ...options }) {
1391
+ const { store, table } = useDataTableServer(options);
1392
+ return /* @__PURE__ */ jsx(DataTableStoreContext, {
1393
+ value: store,
1394
+ children: /* @__PURE__ */ jsx(TableInstanceContext, {
1395
+ value: table,
1396
+ children: /* @__PURE__ */ jsx(DataTableRenderKeyContext, {
1397
+ value: store.getSnapshot()._version,
1398
+ children: /* @__PURE__ */ jsx("div", {
1399
+ className,
1400
+ children
1401
+ })
1402
+ })
1403
+ })
1404
+ });
1405
+ }
1406
+ function ServerProvider(props) {
1407
+ if (!useIsClient()) return /* @__PURE__ */ jsx(Fragment$1, { children: props.ssrFallback === void 0 ? /* @__PURE__ */ jsx(DataTableLoading, { columns: props.columns.length }) : props.ssrFallback });
1408
+ return /* @__PURE__ */ jsx(ServerProviderInner, { ...props });
1409
+ }
1410
+
1411
+ //#endregion
1412
+ //#region src/components/features/data-table/filters/checkbox-filter.tsx
1413
+ const MAX_VISIBLE_BADGES = 2;
1414
+ function CheckboxFilter({ column, label, options, className, checkboxPopoverClassName, disabled }) {
1415
+ const { filters, setFilter, clearFilter, registerFilter, unregisterFilter } = useDataTableFilters();
1416
+ const [open, setOpen] = useState(false);
1417
+ useEffect(() => {
1418
+ registerFilter(column, "checkbox");
1419
+ return () => unregisterFilter(column);
1420
+ }, [
1421
+ column,
1422
+ registerFilter,
1423
+ unregisterFilter
1424
+ ]);
1425
+ const selectedValues = filters[column] ?? [];
1426
+ const updateValues = (newValues) => {
1427
+ if (newValues.length > 0) setFilter(column, newValues);
1428
+ else clearFilter(column);
1429
+ };
1430
+ const handleToggle = (optionValue, checked) => {
1431
+ updateValues(checked ? [...selectedValues, optionValue] : selectedValues.filter((v) => v !== optionValue));
1432
+ };
1433
+ const removeValue = (optionValue) => {
1434
+ updateValues(selectedValues.filter((v) => v !== optionValue));
1435
+ };
1436
+ const visibleBadges = selectedValues.slice(0, MAX_VISIBLE_BADGES);
1437
+ const remainingCount = selectedValues.length - MAX_VISIBLE_BADGES;
1178
1438
  return /* @__PURE__ */ jsxs(Popover, {
1179
1439
  open,
1180
1440
  onOpenChange: setOpen,
@@ -1182,6 +1442,7 @@ function CheckboxFilter({ column, label, options, className, checkboxPopoverClas
1182
1442
  asChild: true,
1183
1443
  children: /* @__PURE__ */ jsxs(Button, {
1184
1444
  theme: "outline",
1445
+ disabled,
1185
1446
  className: cn("justify-between gap-1", className),
1186
1447
  "data-slot": "dt-filter",
1187
1448
  "data-testid": "dt-filter-trigger",
@@ -1261,7 +1522,7 @@ function CheckboxFilter({ column, label, options, className, checkboxPopoverClas
1261
1522
 
1262
1523
  //#endregion
1263
1524
  //#region src/components/features/data-table/filters/date-picker-filter.tsx
1264
- function DatePickerFilter({ column, label, className, datePickerPopoverClassName, disableFuture, disablePast, minDate, maxDate }) {
1525
+ function DatePickerFilter({ column, label, className, datePickerPopoverClassName, disableFuture, disablePast, minDate, maxDate, disabled }) {
1265
1526
  const { filters, setFilter, clearFilter, registerFilter, unregisterFilter } = useDataTableFilters();
1266
1527
  const rawValue = filters[column];
1267
1528
  useEffect(() => {
@@ -1288,6 +1549,7 @@ function DatePickerFilter({ column, label, className, datePickerPopoverClassName
1288
1549
  placeholder: label,
1289
1550
  triggerClassName: className,
1290
1551
  variant: "outline",
1552
+ disabled,
1291
1553
  disableFuture,
1292
1554
  disablePast,
1293
1555
  minDate,
@@ -1303,7 +1565,7 @@ function DatePickerFilter({ column, label, className, datePickerPopoverClassName
1303
1565
 
1304
1566
  //#endregion
1305
1567
  //#region src/components/features/data-table/filters/select-filter.tsx
1306
- function SelectFilter({ column, label, options, placeholder, searchable = true, className, selectPopoverClassName }) {
1568
+ function SelectFilter({ column, label, options, placeholder, searchable = true, className, selectPopoverClassName, disabled }) {
1307
1569
  const { filters, setFilter, clearFilter, registerFilter, unregisterFilter } = useDataTableFilters();
1308
1570
  const [open, setOpen] = useState(false);
1309
1571
  const value = filters[column];
@@ -1325,6 +1587,7 @@ function SelectFilter({ column, label, options, placeholder, searchable = true,
1325
1587
  theme: "outline",
1326
1588
  role: "combobox",
1327
1589
  "aria-expanded": open,
1590
+ disabled,
1328
1591
  className: cn("justify-between", className),
1329
1592
  "data-slot": "dt-filter",
1330
1593
  "data-testid": "dt-filter-trigger",
@@ -1388,242 +1651,4 @@ const DataTable = {
1388
1651
  };
1389
1652
 
1390
1653
  //#endregion
1391
- //#region src/components/features/data-table/hooks/use-data-table-client.ts
1392
- function useDataTableClient(options) {
1393
- const { data, columns, pageSize, getRowId, enableRowSelection = false, defaultSort, defaultFilters, searchableColumns, searchFn, filterFns, stateAdapter } = options;
1394
- const store = useMemo(() => createDataTableStore({
1395
- data,
1396
- mode: "client",
1397
- defaultSort,
1398
- defaultFilters,
1399
- pageSize,
1400
- searchableColumns,
1401
- searchFn,
1402
- filterFns
1403
- }), []);
1404
- const isInitialRender = useRef(true);
1405
- useEffect(() => {
1406
- if (isInitialRender.current) {
1407
- isInitialRender.current = false;
1408
- return;
1409
- }
1410
- store.setData(data);
1411
- }, [data, store]);
1412
- const resolvedColumns = useMemo(() => enableRowSelection ? withSelectionColumn(columns, typeof enableRowSelection === "object" ? enableRowSelection : {}) : columns, [columns, enableRowSelection]);
1413
- const { filteredData, sorting, rowSelection, pageIndex, pageSize: storePageSize, filters, search } = useSyncExternalStore(store.subscribe, store.getSnapshot);
1414
- const table = useReactTable({
1415
- data: filteredData,
1416
- columns: resolvedColumns,
1417
- state: {
1418
- sorting,
1419
- rowSelection,
1420
- pagination: {
1421
- pageIndex,
1422
- pageSize: storePageSize
1423
- }
1424
- },
1425
- onSortingChange: (updater) => {
1426
- const next = typeof updater === "function" ? updater(sorting) : updater;
1427
- store.setSorting(next);
1428
- },
1429
- onRowSelectionChange: (updater) => {
1430
- const next = typeof updater === "function" ? updater(rowSelection) : updater;
1431
- store.setRowSelection(next);
1432
- },
1433
- onPaginationChange: (updater) => {
1434
- const next = typeof updater === "function" ? updater({
1435
- pageIndex,
1436
- pageSize: storePageSize
1437
- }) : updater;
1438
- store.setPagination(next.pageIndex, next.pageSize);
1439
- },
1440
- getCoreRowModel: getCoreRowModel(),
1441
- getSortedRowModel: getSortedRowModel(),
1442
- getPaginationRowModel: getPaginationRowModel(),
1443
- getRowId,
1444
- enableRowSelection: !!enableRowSelection
1445
- });
1446
- const hydratedRef = useRef(false);
1447
- useEffect(() => {
1448
- if (stateAdapter && !hydratedRef.current) {
1449
- hydratedRef.current = true;
1450
- const persisted = stateAdapter.read();
1451
- if (persisted.sorting && persisted.sorting.length > 0) store.setSorting(persisted.sorting);
1452
- if (persisted.filters) {
1453
- for (const [key, value] of Object.entries(persisted.filters)) if (value != null) store.setFilter(key, value);
1454
- }
1455
- if (persisted.search) store.setSearch(persisted.search);
1456
- if (persisted.pageIndex != null && persisted.pageIndex > 0) store.setPageIndex(persisted.pageIndex);
1457
- if (persisted.pageSize != null) store.setPageSize(persisted.pageSize);
1458
- }
1459
- }, []);
1460
- const isFirstWrite = useRef(true);
1461
- useEffect(() => {
1462
- if (!stateAdapter) return;
1463
- if (isFirstWrite.current) {
1464
- isFirstWrite.current = false;
1465
- return;
1466
- }
1467
- stateAdapter.write({
1468
- sorting,
1469
- filters,
1470
- search,
1471
- pageIndex,
1472
- pageSize: storePageSize
1473
- });
1474
- }, [
1475
- sorting,
1476
- filters,
1477
- search,
1478
- pageIndex,
1479
- storePageSize,
1480
- stateAdapter
1481
- ]);
1482
- return {
1483
- store,
1484
- table
1485
- };
1486
- }
1487
-
1488
- //#endregion
1489
- //#region src/components/features/data-table/hooks/use-data-table-server.ts
1490
- function useDataTableServer(options) {
1491
- const { columns, fetchFn, transform, limit = 20, getRowId, enableRowSelection = false, defaultSort, defaultFilters, stateAdapter } = options;
1492
- const fetchRef = useRef(fetchFn);
1493
- const transformRef = useRef(transform);
1494
- useEffect(() => {
1495
- fetchRef.current = fetchFn;
1496
- }, [fetchFn]);
1497
- useEffect(() => {
1498
- transformRef.current = transform;
1499
- }, [transform]);
1500
- const cursorMapRef = useRef(/* @__PURE__ */ new Map());
1501
- const hasNextPageRef = useRef(false);
1502
- const store = useMemo(() => createDataTableStore({
1503
- data: [],
1504
- mode: "server",
1505
- defaultSort,
1506
- defaultFilters,
1507
- pageSize: limit
1508
- }), []);
1509
- const { sorting, filters, search, rowSelection, pageSize, pageIndex } = useSyncExternalStore(store.subscribe, store.getSnapshot);
1510
- const prevQueryRef = useRef({
1511
- sorting,
1512
- filters,
1513
- search,
1514
- pageSize
1515
- });
1516
- useEffect(() => {
1517
- const prev = prevQueryRef.current;
1518
- if (prev.sorting !== sorting || prev.filters !== filters || prev.search !== search || prev.pageSize !== pageSize) {
1519
- cursorMapRef.current = /* @__PURE__ */ new Map();
1520
- hasNextPageRef.current = false;
1521
- if (pageIndex !== 0) {
1522
- prevQueryRef.current = {
1523
- sorting,
1524
- filters,
1525
- search,
1526
- pageSize
1527
- };
1528
- store.setPageIndex(0);
1529
- return;
1530
- }
1531
- }
1532
- prevQueryRef.current = {
1533
- sorting,
1534
- filters,
1535
- search,
1536
- pageSize
1537
- };
1538
- let cancelled = false;
1539
- store.setLoading(true);
1540
- const cursor = cursorMapRef.current.get(pageIndex);
1541
- fetchRef.current({
1542
- sorting,
1543
- filters,
1544
- search,
1545
- cursor,
1546
- limit: pageSize
1547
- }).then((response) => {
1548
- if (cancelled) return;
1549
- const result = transformRef.current(response);
1550
- store.setServerData(result.data);
1551
- store.setError(null);
1552
- if (result.nextCursor) cursorMapRef.current.set(pageIndex + 1, result.nextCursor);
1553
- hasNextPageRef.current = result.hasNextPage;
1554
- }).catch((error) => {
1555
- if (cancelled) return;
1556
- store.setServerData([]);
1557
- store.setError(error instanceof Error ? error : new Error(String(error)));
1558
- hasNextPageRef.current = false;
1559
- }).finally(() => {
1560
- if (!cancelled) store.setLoading(false);
1561
- });
1562
- return () => {
1563
- cancelled = true;
1564
- };
1565
- }, [
1566
- sorting,
1567
- filters,
1568
- search,
1569
- pageSize,
1570
- pageIndex,
1571
- store
1572
- ]);
1573
- const resolvedColumns = useMemo(() => enableRowSelection ? withSelectionColumn(columns, typeof enableRowSelection === "object" ? enableRowSelection : {}) : columns, [columns, enableRowSelection]);
1574
- const table = useReactTable({
1575
- data: store.getSnapshot().data,
1576
- columns: resolvedColumns,
1577
- state: {
1578
- sorting,
1579
- rowSelection,
1580
- pagination: {
1581
- pageIndex,
1582
- pageSize
1583
- }
1584
- },
1585
- manualPagination: true,
1586
- manualSorting: true,
1587
- manualFiltering: true,
1588
- pageCount: hasNextPageRef.current ? pageIndex + 2 : pageIndex + 1,
1589
- getCoreRowModel: getCoreRowModel(),
1590
- getRowId,
1591
- enableRowSelection: !!enableRowSelection,
1592
- onSortingChange: (updater) => {
1593
- const next = typeof updater === "function" ? updater(sorting) : updater;
1594
- store.setSorting(next);
1595
- },
1596
- onRowSelectionChange: (updater) => {
1597
- const next = typeof updater === "function" ? updater(rowSelection) : updater;
1598
- store.setRowSelection(next);
1599
- },
1600
- onPaginationChange: (updater) => {
1601
- const next = typeof updater === "function" ? updater({
1602
- pageIndex,
1603
- pageSize
1604
- }) : updater;
1605
- store.setPagination(next.pageIndex, next.pageSize);
1606
- }
1607
- });
1608
- useEffect(() => {
1609
- if (stateAdapter) stateAdapter.write({
1610
- sorting,
1611
- filters,
1612
- search,
1613
- pageSize
1614
- });
1615
- }, [
1616
- sorting,
1617
- filters,
1618
- search,
1619
- pageSize,
1620
- stateAdapter
1621
- ]);
1622
- return {
1623
- store,
1624
- table
1625
- };
1626
- }
1627
-
1628
- //#endregion
1629
- export { DEFAULT_DEBOUNCE_MS, DEFAULT_LOADING_ROWS, DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZES, DataTable, createDataTableStore, createSelectionColumn, useDataTableClient, useDataTableContext, useDataTableFilters, useDataTableInlineContents, useDataTableLoading, useDataTablePagination, useDataTableRows, useDataTableSearch, useDataTableSelection, useDataTableServer, useDataTableSorting, useNuqsAdapter };
1654
+ 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 };