aplance-ui 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +29 -0
- package/dist/api.d.ts +175 -0
- package/dist/api.js +1954 -0
- package/dist/api.js.map +1 -0
- package/dist/frameworks/astro/astro.config.snippet.md +23 -0
- package/dist/frameworks/astro/postcss.config.mjs.template +20 -0
- package/dist/frameworks/astro/vite-inline.snippet.md +22 -0
- package/dist/frameworks/electron-vite/vite.config.snippet.md +44 -0
- package/dist/frameworks/next/layout-import.snippet.md +19 -0
- package/dist/frameworks/next/next.config.snippet.md +15 -0
- package/dist/frameworks/next/postcss.config.js.template +16 -0
- package/dist/frameworks/tanstack-start/postcss.config.ts.template +17 -0
- package/dist/frameworks/tanstack-start/vite-inline.snippet.md +22 -0
- package/dist/frameworks/vite-react/postcss.config.ts.template +16 -0
- package/dist/frameworks/vite-react/vite-inline.snippet.md +20 -0
- package/dist/frameworks/vite-react/vite.config.ts.template +48 -0
- package/dist/index.js +2047 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
- package/registry/components/primitives/icon/icon.module.css +63 -0
- package/registry/components/primitives/icon/icon.tsx +53 -0
- package/registry/components/primitives/modal/modal-context.ts +19 -0
- package/registry/components/primitives/modal/modal.module.css +415 -0
- package/registry/components/primitives/modal/modal.tsx +550 -0
- package/registry/components/primitives/popover/popover-context.ts +28 -0
- package/registry/components/primitives/popover/popover.module.css +28 -0
- package/registry/components/primitives/popover/popover.tsx +465 -0
- package/registry/components/ui/accordion/accordion.module.css +208 -0
- package/registry/components/ui/accordion/accordion.tsx +327 -0
- package/registry/components/ui/avatar/avatar.module.css +32 -0
- package/registry/components/ui/avatar/avatar.tsx +37 -0
- package/registry/components/ui/badge/badge.module.css +103 -0
- package/registry/components/ui/badge/badge.tsx +29 -0
- package/registry/components/ui/button/button.module.css +216 -0
- package/registry/components/ui/button/button.tsx +133 -0
- package/registry/components/ui/chart/chart.module.css +168 -0
- package/registry/components/ui/chart/chart.tsx +1293 -0
- package/registry/components/ui/chart/types.ts +210 -0
- package/registry/components/ui/chart/utils.ts +59 -0
- package/registry/components/ui/copy-button/copy-button.module.css +21 -0
- package/registry/components/ui/copy-button/copy-button.tsx +123 -0
- package/registry/components/ui/countdown/countdown.module.css +5 -0
- package/registry/components/ui/countdown/countdown.tsx +152 -0
- package/registry/components/ui/data-table/checkbox-column.tsx +59 -0
- package/registry/components/ui/data-table/data-table.module.css +594 -0
- package/registry/components/ui/data-table/data-table.tsx +1109 -0
- package/registry/components/ui/data-table/filter-bar.tsx +33 -0
- package/registry/components/ui/data-table/pagination-bar.tsx +125 -0
- package/registry/components/ui/data-table/types.ts +594 -0
- package/registry/components/ui/data-table/useDataTable.tsx +573 -0
- package/registry/components/ui/data-table/utils.ts +445 -0
- package/registry/components/ui/empty-state/empty-state.module.css +26 -0
- package/registry/components/ui/empty-state/empty-state.tsx +32 -0
- package/registry/components/ui/input-fields/color-picker/color-palette.module.css +102 -0
- package/registry/components/ui/input-fields/color-picker/color-palette.tsx +92 -0
- package/registry/components/ui/input-fields/color-picker/color-picker.module.css +29 -0
- package/registry/components/ui/input-fields/color-picker/color-picker.tsx +168 -0
- package/registry/components/ui/input-fields/common/field-message/field-message.module.css +28 -0
- package/registry/components/ui/input-fields/common/field-message/field-message.tsx +39 -0
- package/registry/components/ui/input-fields/common/input-field-common.module.css +318 -0
- package/registry/components/ui/input-fields/common/input-field-native.module.css +267 -0
- package/registry/components/ui/input-fields/common/picker-popover-common.module.css +40 -0
- package/registry/components/ui/input-fields/common/select-button/select-button.module.css +9 -0
- package/registry/components/ui/input-fields/common/select-button/select-button.tsx +171 -0
- package/registry/components/ui/input-fields/common/types.ts +38 -0
- package/registry/components/ui/input-fields/custom-select/custom-select.module.css +56 -0
- package/registry/components/ui/input-fields/custom-select/custom-select.tsx +403 -0
- package/registry/components/ui/input-fields/date-picker/calendar.module.css +311 -0
- package/registry/components/ui/input-fields/date-picker/calendar.tsx +186 -0
- package/registry/components/ui/input-fields/date-picker/date-picker.module.css +57 -0
- package/registry/components/ui/input-fields/date-picker/date-picker.tsx +945 -0
- package/registry/components/ui/input-fields/date-picker/types.ts +102 -0
- package/registry/components/ui/input-fields/date-picker/utils.ts +236 -0
- package/registry/components/ui/input-fields/emoji-picker/emoji-palette.module.css +90 -0
- package/registry/components/ui/input-fields/emoji-picker/emoji-palette.tsx +168 -0
- package/registry/components/ui/input-fields/emoji-picker/emoji-picker.module.css +8 -0
- package/registry/components/ui/input-fields/emoji-picker/emoji-picker.tsx +102 -0
- package/registry/components/ui/input-fields/emoji-picker/utils.ts +505 -0
- package/registry/components/ui/input-fields/field-group/field-group.module.css +86 -0
- package/registry/components/ui/input-fields/field-group/field-group.tsx +86 -0
- package/registry/components/ui/input-fields/input/input.tsx +319 -0
- package/registry/components/ui/input-fields/media-input/kind-icons.tsx +62 -0
- package/registry/components/ui/input-fields/media-input/media-input.module.css +294 -0
- package/registry/components/ui/input-fields/media-input/media-input.tsx +327 -0
- package/registry/components/ui/input-fields/media-input/media-preview.tsx +101 -0
- package/registry/components/ui/input-fields/media-input/types.ts +129 -0
- package/registry/components/ui/input-fields/media-input/useMediaUpload.tsx +302 -0
- package/registry/components/ui/input-fields/media-input/utils.ts +325 -0
- package/registry/components/ui/input-fields/otp-input/otp-input.module.css +24 -0
- package/registry/components/ui/input-fields/otp-input/otp-input.tsx +291 -0
- package/registry/components/ui/input-fields/select/select.tsx +118 -0
- package/registry/components/ui/input-fields/switch/switch.module.css +70 -0
- package/registry/components/ui/input-fields/switch/switch.tsx +59 -0
- package/registry/components/ui/input-fields/textarea/textarea.tsx +133 -0
- package/registry/components/ui/input-fields/time-picker/time-picker.module.css +57 -0
- package/registry/components/ui/input-fields/time-picker/time-picker.tsx +437 -0
- package/registry/components/ui/loader/loader.module.css +156 -0
- package/registry/components/ui/loader/loader.tsx +102 -0
- package/registry/components/ui/stepper/stepper.module.css +294 -0
- package/registry/components/ui/stepper/stepper.tsx +197 -0
- package/registry/components/ui/tabs/tabs.module.css +150 -0
- package/registry/components/ui/tabs/tabs.tsx +134 -0
- package/registry/components/ui/toast/toast.module.css +159 -0
- package/registry/components/ui/toast/toast.tsx +104 -0
- package/registry/components/ui/tooltip/tooltip.module.css +82 -0
- package/registry/components/ui/tooltip/tooltip.tsx +72 -0
- package/registry/global.d.ts +4 -0
- package/registry/hooks/useFocusOutside.tsx +33 -0
- package/registry/hooks/useResizeObserver.tsx +32 -0
- package/registry/lib/dom.ts +25 -0
- package/registry/lib/string.ts +23 -0
- package/registry/registry.json +719 -0
- package/registry/styles/app.css +240 -0
- package/registry/styles/components.css +78 -0
- package/registry/styles/fonts/geist-regular.woff2 +0 -0
- package/registry/styles/globals.css +7 -0
- package/registry/styles/media.css +25 -0
- package/registry/styles/utilities.css +90 -0
- package/registry/themes/colors/blue.css +37 -0
- package/registry/themes/colors/green.css +37 -0
- package/registry/themes/colors/rose.css +37 -0
- package/registry/themes/colors/slate.css +37 -0
- package/registry/themes/colors/stone.css +37 -0
- package/registry/themes/colors/violet.css +37 -0
- package/registry/themes/colors/zinc.css +37 -0
- package/registry/themes/radius/pill.css +8 -0
- package/registry/themes/radius/rounded.css +8 -0
- package/registry/themes/radius/sharp.css +8 -0
- package/registry/themes/spacing/compact.css +17 -0
- package/registry/themes/spacing/spacious.css +17 -0
- package/registry/tsconfig.json +25 -0
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
import { useState, useMemo, useCallback, useEffect, useRef } from "react";
|
|
2
|
+
import type {
|
|
3
|
+
ColumnOrderState,
|
|
4
|
+
ColumnPinningState,
|
|
5
|
+
ColumnSizingState,
|
|
6
|
+
ExpandedState,
|
|
7
|
+
GroupingState,
|
|
8
|
+
OnChangeFn,
|
|
9
|
+
RowSelectionState,
|
|
10
|
+
SortingState,
|
|
11
|
+
VisibilityState,
|
|
12
|
+
} from "@tanstack/react-table";
|
|
13
|
+
import type {
|
|
14
|
+
DataTableProps,
|
|
15
|
+
PaginationState,
|
|
16
|
+
PersistArgs,
|
|
17
|
+
PersistedState,
|
|
18
|
+
UseDataTableOptions,
|
|
19
|
+
UseDataTableReturn,
|
|
20
|
+
} from "./types";
|
|
21
|
+
import {
|
|
22
|
+
applyPersistedState,
|
|
23
|
+
readLocalStorage,
|
|
24
|
+
readSearchParams,
|
|
25
|
+
snapshotPersistedState,
|
|
26
|
+
writeLocalStorage,
|
|
27
|
+
writeSearchParams,
|
|
28
|
+
} from "./utils";
|
|
29
|
+
|
|
30
|
+
// Stable empty-object reference - avoids resetting useState's initializer on
|
|
31
|
+
// every render when the consumer doesn't pass `defaultFilterValues`.
|
|
32
|
+
const EMPTY_FILTERS: Record<string, string> = {};
|
|
33
|
+
|
|
34
|
+
// ─── Hook ─────────────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
export function useDataTable<TData extends { id: string }>({
|
|
37
|
+
data,
|
|
38
|
+
pageSize: initialPageSize = 10,
|
|
39
|
+
defaultFilterValues = EMPTY_FILTERS,
|
|
40
|
+
filterFn,
|
|
41
|
+
onFilterChange,
|
|
42
|
+
onPaginationChange: externalOnPaginationChange,
|
|
43
|
+
onSortingChange: externalOnSortingChange,
|
|
44
|
+
onRowSelectionChange: externalOnRowSelectionChange,
|
|
45
|
+
onDataChange,
|
|
46
|
+
clientSidePagination = true,
|
|
47
|
+
manualSorting = false,
|
|
48
|
+
totalCount: externalTotalCount,
|
|
49
|
+
totalPages: externalTotalPages,
|
|
50
|
+
defaultColumnOrder,
|
|
51
|
+
defaultColumnVisibility,
|
|
52
|
+
defaultColumnPinning,
|
|
53
|
+
defaultColumnSizing,
|
|
54
|
+
defaultGrouping,
|
|
55
|
+
defaultExpanded,
|
|
56
|
+
persistState,
|
|
57
|
+
}: UseDataTableOptions<TData>): UseDataTableReturn<TData> {
|
|
58
|
+
// ── Internal state ──────────────────────────────────────────────────────────
|
|
59
|
+
const [sorting, setSorting] = useState<SortingState>([]);
|
|
60
|
+
const [pagination, setPagination] = useState<PaginationState>({
|
|
61
|
+
pageIndex: 0,
|
|
62
|
+
pageSize: initialPageSize,
|
|
63
|
+
});
|
|
64
|
+
const [filterValues, setFilterValues] =
|
|
65
|
+
useState<Record<string, string>>(defaultFilterValues);
|
|
66
|
+
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
|
67
|
+
const [editingRowId, setEditingRowId] = useState<string | null>(null);
|
|
68
|
+
const [columnOrder, setColumnOrderState] = useState<ColumnOrderState>(
|
|
69
|
+
defaultColumnOrder ?? [],
|
|
70
|
+
);
|
|
71
|
+
const [columnVisibility, setColumnVisibilityState] =
|
|
72
|
+
useState<VisibilityState>(defaultColumnVisibility ?? {});
|
|
73
|
+
const [columnPinning, setColumnPinningState] = useState<ColumnPinningState>(
|
|
74
|
+
defaultColumnPinning ?? {},
|
|
75
|
+
);
|
|
76
|
+
const [columnSizing, setColumnSizingState] = useState<ColumnSizingState>(
|
|
77
|
+
defaultColumnSizing ?? {},
|
|
78
|
+
);
|
|
79
|
+
const [grouping, setGroupingState] = useState<GroupingState>(
|
|
80
|
+
defaultGrouping ?? [],
|
|
81
|
+
);
|
|
82
|
+
const [expanded, setExpandedState] = useState<ExpandedState>(
|
|
83
|
+
defaultExpanded ?? true,
|
|
84
|
+
);
|
|
85
|
+
// snapshot for cancel-edit (ref keeps callbacks stable, avoids extra re-renders)
|
|
86
|
+
const snapshotRef = useRef<TData | null>(null);
|
|
87
|
+
// mutable data copy for inline editing
|
|
88
|
+
const [localData, setLocalData] = useState<TData[]>(data);
|
|
89
|
+
const [prevData, setPrevData] = useState<TData[]>(data);
|
|
90
|
+
const localDataRef = useRef<TData[]>(data);
|
|
91
|
+
|
|
92
|
+
// Latest-value refs - let handlers compute `next` outside of the setState
|
|
93
|
+
// updater. Firing consumer callbacks inside an updater causes double-fires
|
|
94
|
+
// under React strict/concurrent rendering (updaters may run twice) which
|
|
95
|
+
// would trigger duplicate API calls for server-side sort/filter consumers.
|
|
96
|
+
const sortingRef = useRef(sorting);
|
|
97
|
+
const rowSelectionRef = useRef(rowSelection);
|
|
98
|
+
const filterValuesRef = useRef(filterValues);
|
|
99
|
+
const paginationRef = useRef(pagination);
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
sortingRef.current = sorting;
|
|
102
|
+
rowSelectionRef.current = rowSelection;
|
|
103
|
+
filterValuesRef.current = filterValues;
|
|
104
|
+
paginationRef.current = pagination;
|
|
105
|
+
}, [sorting, rowSelection, filterValues, pagination]);
|
|
106
|
+
|
|
107
|
+
// Keep localData in sync when the outer data prop changes (new fetch result
|
|
108
|
+
// arrives), but only when not editing - overwriting in-progress edits would
|
|
109
|
+
// discard the user's input. We hold off on advancing `prevData` while
|
|
110
|
+
// editing so a `data` change *during* an edit isn't forgotten: the next
|
|
111
|
+
// run after `editingRowId` flips back to null still sees prevData!==data and
|
|
112
|
+
// syncs.
|
|
113
|
+
// We do this during render (instead of useEffect) to prevent a micro-second
|
|
114
|
+
// jerk/overlap of old data when server-side fetch completes.
|
|
115
|
+
// A shallow array comparison prevents infinite re-render loops when consumers
|
|
116
|
+
// pass inline literal arrays (like `data={[]}`) which change reference every render.
|
|
117
|
+
if (editingRowId === null) {
|
|
118
|
+
let hasChanged = prevData !== data;
|
|
119
|
+
if (hasChanged && prevData?.length === data?.length) {
|
|
120
|
+
hasChanged = false;
|
|
121
|
+
for (let i = 0; i < (data?.length || 0); i++) {
|
|
122
|
+
if (prevData[i] !== data[i]) {
|
|
123
|
+
hasChanged = true;
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (hasChanged) {
|
|
129
|
+
setPrevData(data);
|
|
130
|
+
setLocalData(data);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
localDataRef.current = localData;
|
|
136
|
+
}, [localData]);
|
|
137
|
+
|
|
138
|
+
// ── Sorting handler ─────────────────────────────────────────────────────────
|
|
139
|
+
const handleSortingChange: OnChangeFn<SortingState> = useCallback(
|
|
140
|
+
(updater) => {
|
|
141
|
+
const next =
|
|
142
|
+
typeof updater === "function" ? updater(sortingRef.current) : updater;
|
|
143
|
+
setSorting(next);
|
|
144
|
+
externalOnSortingChange?.(next);
|
|
145
|
+
},
|
|
146
|
+
[externalOnSortingChange],
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// ── Pagination handler ──────────────────────────────────────────────────────
|
|
150
|
+
const handlePaginationChange = useCallback(
|
|
151
|
+
(next: PaginationState) => {
|
|
152
|
+
setPagination(next);
|
|
153
|
+
externalOnPaginationChange?.(next);
|
|
154
|
+
},
|
|
155
|
+
[externalOnPaginationChange],
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
// ── Row selection handler ───────────────────────────────────────────────────
|
|
159
|
+
const handleRowSelectionChange: OnChangeFn<RowSelectionState> = useCallback(
|
|
160
|
+
(updater) => {
|
|
161
|
+
const next =
|
|
162
|
+
typeof updater === "function"
|
|
163
|
+
? updater(rowSelectionRef.current)
|
|
164
|
+
: updater;
|
|
165
|
+
setRowSelection(next);
|
|
166
|
+
externalOnRowSelectionChange?.(next);
|
|
167
|
+
},
|
|
168
|
+
[externalOnRowSelectionChange],
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
// Column-state setters double as tanstack `OnChangeFn`s - React's
|
|
172
|
+
// `SetStateAction<T>` is structurally identical to tanstack's `Updater<T>`,
|
|
173
|
+
// so a no-op forwarding wrapper would just add allocations.
|
|
174
|
+
|
|
175
|
+
// ── Filter handler ──────────────────────────────────────────────────────────
|
|
176
|
+
const setFilter = useCallback(
|
|
177
|
+
(key: string, value: string) => {
|
|
178
|
+
const next = { ...filterValuesRef.current, [key]: value };
|
|
179
|
+
setFilterValues(next);
|
|
180
|
+
onFilterChange?.(key, value, next);
|
|
181
|
+
|
|
182
|
+
const p = paginationRef.current;
|
|
183
|
+
if (p.pageIndex !== 0) {
|
|
184
|
+
handlePaginationChange({ ...p, pageIndex: 0 });
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
[onFilterChange, handlePaginationChange],
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const setFilters = useCallback(
|
|
191
|
+
(values: Record<string, string>) => {
|
|
192
|
+
const next = { ...filterValuesRef.current, ...values };
|
|
193
|
+
setFilterValues(next);
|
|
194
|
+
for (const [k, v] of Object.entries(values)) {
|
|
195
|
+
onFilterChange?.(k, v, next);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const p = paginationRef.current;
|
|
199
|
+
if (p.pageIndex !== 0) {
|
|
200
|
+
handlePaginationChange({ ...p, pageIndex: 0 });
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
[onFilterChange, handlePaginationChange],
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
const resetFilters = useCallback(() => {
|
|
207
|
+
setFilterValues(defaultFilterValues);
|
|
208
|
+
|
|
209
|
+
const p = paginationRef.current;
|
|
210
|
+
if (p.pageIndex !== 0) {
|
|
211
|
+
handlePaginationChange({ ...p, pageIndex: 0 });
|
|
212
|
+
}
|
|
213
|
+
}, [defaultFilterValues, handlePaginationChange]);
|
|
214
|
+
|
|
215
|
+
// ── Inline editing actions ──────────────────────────────────────────────────
|
|
216
|
+
const startEdit = useCallback((rowId: string) => {
|
|
217
|
+
const row = localDataRef.current.find((r) => r.id === rowId);
|
|
218
|
+
snapshotRef.current = row ? { ...row } : null;
|
|
219
|
+
setEditingRowId(rowId);
|
|
220
|
+
}, []);
|
|
221
|
+
|
|
222
|
+
const cancelEdit = useCallback(() => {
|
|
223
|
+
const snapshot = snapshotRef.current;
|
|
224
|
+
if (snapshot) {
|
|
225
|
+
setLocalData((prev) =>
|
|
226
|
+
prev.map((r) => (r.id === snapshot.id ? snapshot : r)),
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
snapshotRef.current = null;
|
|
230
|
+
setEditingRowId(null);
|
|
231
|
+
}, []);
|
|
232
|
+
|
|
233
|
+
const confirmSave = useCallback(() => {
|
|
234
|
+
snapshotRef.current = null;
|
|
235
|
+
setEditingRowId(null);
|
|
236
|
+
}, []);
|
|
237
|
+
|
|
238
|
+
// Compute `next` from the latest-data ref and fire `onDataChange` outside
|
|
239
|
+
// the setState updater - see the latest-value-ref note above. The ref is
|
|
240
|
+
// advanced synchronously so back-to-back calls in the same tick compose
|
|
241
|
+
// correctly instead of clobbering each other.
|
|
242
|
+
const updateRowData = useCallback(
|
|
243
|
+
(rowId: string, columnId: string, value: unknown) => {
|
|
244
|
+
const next = localDataRef.current.map((r) =>
|
|
245
|
+
r.id === rowId ? ({ ...r, [columnId]: value } as TData) : r,
|
|
246
|
+
);
|
|
247
|
+
localDataRef.current = next;
|
|
248
|
+
setLocalData(next);
|
|
249
|
+
onDataChange?.(next);
|
|
250
|
+
},
|
|
251
|
+
[onDataChange],
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
const reorderRow = useCallback(
|
|
255
|
+
(draggedRow: TData, targetRow: TData) => {
|
|
256
|
+
const prev = localDataRef.current;
|
|
257
|
+
const draggedIdx = prev.indexOf(draggedRow);
|
|
258
|
+
const targetIdx = prev.indexOf(targetRow);
|
|
259
|
+
if (draggedIdx === -1 || targetIdx === -1 || draggedIdx === targetIdx) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const next = [...prev];
|
|
263
|
+
const [moved] = next.splice(draggedIdx, 1);
|
|
264
|
+
next.splice(targetIdx, 0, moved);
|
|
265
|
+
localDataRef.current = next;
|
|
266
|
+
setLocalData(next);
|
|
267
|
+
onDataChange?.(next);
|
|
268
|
+
},
|
|
269
|
+
[onDataChange],
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
// ── Page helpers ────────────────────────────────────────────────────────────
|
|
273
|
+
const goToPage = useCallback(
|
|
274
|
+
(index: number) =>
|
|
275
|
+
handlePaginationChange({ ...pagination, pageIndex: index }),
|
|
276
|
+
[handlePaginationChange, pagination],
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
const setPageSize = useCallback(
|
|
280
|
+
(size: number) => handlePaginationChange({ pageIndex: 0, pageSize: size }),
|
|
281
|
+
[handlePaginationChange],
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
const clearSelection = useCallback(() => {
|
|
285
|
+
setRowSelection({});
|
|
286
|
+
}, []);
|
|
287
|
+
|
|
288
|
+
// ── Column-state imperative helpers ────────────────────────────────────────
|
|
289
|
+
const toggleColumnVisibility = useCallback((columnId: string) => {
|
|
290
|
+
setColumnVisibilityState((prev) => ({
|
|
291
|
+
...prev,
|
|
292
|
+
[columnId]: prev[columnId] === false,
|
|
293
|
+
}));
|
|
294
|
+
}, []);
|
|
295
|
+
|
|
296
|
+
// React's useState setters are already stable - no useCallback wrapper needed.
|
|
297
|
+
const setColumnVisibility = setColumnVisibilityState;
|
|
298
|
+
const setColumnOrder = setColumnOrderState;
|
|
299
|
+
const setColumnPinning = setColumnPinningState;
|
|
300
|
+
const setColumnSizing = setColumnSizingState;
|
|
301
|
+
const setGrouping = setGroupingState;
|
|
302
|
+
const setExpanded = setExpandedState;
|
|
303
|
+
|
|
304
|
+
const toggleGrouping = useCallback((columnId: string) => {
|
|
305
|
+
setGroupingState((prev) =>
|
|
306
|
+
prev.includes(columnId)
|
|
307
|
+
? prev.filter((id) => id !== columnId)
|
|
308
|
+
: [...prev, columnId],
|
|
309
|
+
);
|
|
310
|
+
}, []);
|
|
311
|
+
|
|
312
|
+
// ── State persistence (URL / localStorage / manual) ─────────────────────────
|
|
313
|
+
// Lives here (not in <DataTable>) so consumers get a single hook that wires
|
|
314
|
+
// their state to the chosen destination. The persisted blob shape is mode-
|
|
315
|
+
// agnostic - switching modes preserves the same data.
|
|
316
|
+
usePersist({
|
|
317
|
+
config: persistState,
|
|
318
|
+
filterKeys: Object.keys(defaultFilterValues),
|
|
319
|
+
filterValues,
|
|
320
|
+
setFilterValues,
|
|
321
|
+
pagination,
|
|
322
|
+
setPagination,
|
|
323
|
+
sorting,
|
|
324
|
+
setSorting,
|
|
325
|
+
columnOrder,
|
|
326
|
+
setColumnOrder: setColumnOrderState,
|
|
327
|
+
columnVisibility,
|
|
328
|
+
setColumnVisibility: setColumnVisibilityState,
|
|
329
|
+
columnPinning,
|
|
330
|
+
setColumnPinning: setColumnPinningState,
|
|
331
|
+
columnSizing,
|
|
332
|
+
setColumnSizing: setColumnSizingState,
|
|
333
|
+
grouping,
|
|
334
|
+
setGrouping: setGroupingState,
|
|
335
|
+
expanded,
|
|
336
|
+
setExpanded: setExpandedState,
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// ── Derived data ────────────────────────────────────────────────────────────
|
|
340
|
+
const filteredData = useMemo<TData[]>(() => {
|
|
341
|
+
if (!filterFn) return localData;
|
|
342
|
+
return filterFn(localData, filterValues);
|
|
343
|
+
}, [localData, filterFn, filterValues]);
|
|
344
|
+
|
|
345
|
+
const pageData = useMemo<TData[]>(() => {
|
|
346
|
+
if (!clientSidePagination) return filteredData;
|
|
347
|
+
const { pageIndex, pageSize } = pagination;
|
|
348
|
+
return filteredData.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize);
|
|
349
|
+
}, [filteredData, pagination, clientSidePagination]);
|
|
350
|
+
|
|
351
|
+
const totalCount = externalTotalCount ?? filteredData.length;
|
|
352
|
+
const totalPages =
|
|
353
|
+
externalTotalPages ?? Math.ceil(filteredData.length / pagination.pageSize);
|
|
354
|
+
|
|
355
|
+
const selectedIds = useMemo(() => Object.keys(rowSelection), [rowSelection]);
|
|
356
|
+
const selectedRows = useMemo(
|
|
357
|
+
() => localData.filter((r) => rowSelection[r.id]),
|
|
358
|
+
[localData, rowSelection],
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
// ── Assemble DataTable props ─────────────────────────────────────────────────
|
|
362
|
+
const props: Omit<
|
|
363
|
+
DataTableProps<TData>,
|
|
364
|
+
"columns" | "filters" | "className"
|
|
365
|
+
> = useMemo(
|
|
366
|
+
() => ({
|
|
367
|
+
data: pageData,
|
|
368
|
+
sorting,
|
|
369
|
+
onSortingChange: handleSortingChange,
|
|
370
|
+
manualSorting,
|
|
371
|
+
filterValues,
|
|
372
|
+
onFilterChange: setFilter,
|
|
373
|
+
showPagination: true,
|
|
374
|
+
pagination,
|
|
375
|
+
onPaginationChange: handlePaginationChange,
|
|
376
|
+
totalPages,
|
|
377
|
+
totalCount,
|
|
378
|
+
rowSelection,
|
|
379
|
+
onRowSelectionChange: handleRowSelectionChange,
|
|
380
|
+
editingRowId,
|
|
381
|
+
onUpdateData: updateRowData,
|
|
382
|
+
columnOrder,
|
|
383
|
+
onColumnOrderChange: setColumnOrderState,
|
|
384
|
+
columnVisibility,
|
|
385
|
+
onColumnVisibilityChange: setColumnVisibilityState,
|
|
386
|
+
columnPinning,
|
|
387
|
+
onColumnPinningChange: setColumnPinningState,
|
|
388
|
+
defaultColumnPinning,
|
|
389
|
+
columnSizing,
|
|
390
|
+
onColumnSizingChange: setColumnSizingState,
|
|
391
|
+
grouping,
|
|
392
|
+
onGroupingChange: setGroupingState,
|
|
393
|
+
expanded,
|
|
394
|
+
onExpandedChange: setExpandedState,
|
|
395
|
+
onRowOrderChange: reorderRow,
|
|
396
|
+
}),
|
|
397
|
+
[
|
|
398
|
+
pageData,
|
|
399
|
+
sorting,
|
|
400
|
+
handleSortingChange,
|
|
401
|
+
manualSorting,
|
|
402
|
+
filterValues,
|
|
403
|
+
setFilter,
|
|
404
|
+
pagination,
|
|
405
|
+
handlePaginationChange,
|
|
406
|
+
totalPages,
|
|
407
|
+
totalCount,
|
|
408
|
+
rowSelection,
|
|
409
|
+
handleRowSelectionChange,
|
|
410
|
+
editingRowId,
|
|
411
|
+
updateRowData,
|
|
412
|
+
columnOrder,
|
|
413
|
+
columnVisibility,
|
|
414
|
+
columnPinning,
|
|
415
|
+
defaultColumnPinning,
|
|
416
|
+
columnSizing,
|
|
417
|
+
grouping,
|
|
418
|
+
expanded,
|
|
419
|
+
reorderRow,
|
|
420
|
+
],
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
const actions = useMemo(
|
|
424
|
+
() => ({
|
|
425
|
+
setFilter,
|
|
426
|
+
setFilters,
|
|
427
|
+
resetFilters,
|
|
428
|
+
goToPage,
|
|
429
|
+
setPageSize,
|
|
430
|
+
clearSelection,
|
|
431
|
+
startEdit,
|
|
432
|
+
cancelEdit,
|
|
433
|
+
confirmSave,
|
|
434
|
+
updateRowData,
|
|
435
|
+
reorderRow,
|
|
436
|
+
toggleColumnVisibility,
|
|
437
|
+
setColumnVisibility,
|
|
438
|
+
setColumnOrder,
|
|
439
|
+
setColumnPinning,
|
|
440
|
+
setColumnSizing,
|
|
441
|
+
toggleGrouping,
|
|
442
|
+
setGrouping,
|
|
443
|
+
setExpanded,
|
|
444
|
+
}),
|
|
445
|
+
[
|
|
446
|
+
setFilter,
|
|
447
|
+
setFilters,
|
|
448
|
+
resetFilters,
|
|
449
|
+
goToPage,
|
|
450
|
+
setPageSize,
|
|
451
|
+
clearSelection,
|
|
452
|
+
startEdit,
|
|
453
|
+
cancelEdit,
|
|
454
|
+
confirmSave,
|
|
455
|
+
updateRowData,
|
|
456
|
+
reorderRow,
|
|
457
|
+
toggleColumnVisibility,
|
|
458
|
+
setColumnVisibility,
|
|
459
|
+
setColumnOrder,
|
|
460
|
+
setColumnPinning,
|
|
461
|
+
setColumnSizing,
|
|
462
|
+
toggleGrouping,
|
|
463
|
+
setGrouping,
|
|
464
|
+
setExpanded,
|
|
465
|
+
],
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
return {
|
|
469
|
+
props,
|
|
470
|
+
state: {
|
|
471
|
+
filterValues,
|
|
472
|
+
pagination,
|
|
473
|
+
sorting,
|
|
474
|
+
rowSelection,
|
|
475
|
+
editingRowId,
|
|
476
|
+
columnOrder,
|
|
477
|
+
columnVisibility,
|
|
478
|
+
columnPinning,
|
|
479
|
+
columnSizing,
|
|
480
|
+
grouping,
|
|
481
|
+
expanded,
|
|
482
|
+
pageData,
|
|
483
|
+
filteredData,
|
|
484
|
+
selectedIds,
|
|
485
|
+
selectedRows,
|
|
486
|
+
},
|
|
487
|
+
actions,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// ─── Persist wiring ──────────────────────────────────────────────────────────
|
|
492
|
+
// Pure read/write/snapshot/apply helpers live in `utils.ts`. This hook just
|
|
493
|
+
// orchestrates them - hydrate once on mount, then sync state → destination
|
|
494
|
+
// whenever any persistable slice changes.
|
|
495
|
+
|
|
496
|
+
function usePersist(args: PersistArgs): void {
|
|
497
|
+
const enabled = args.config?.enabled ?? false;
|
|
498
|
+
const mode = args.config?.mode;
|
|
499
|
+
|
|
500
|
+
const hydrated = useRef(false);
|
|
501
|
+
// pageSize at first render - used by search-params mode to keep the URL
|
|
502
|
+
// clean (only writes ?pageSize=X when it differs from this default).
|
|
503
|
+
const defaultPageSizeRef = useRef(args.pagination.pageSize);
|
|
504
|
+
|
|
505
|
+
// Stable ref for args/config to avoid re-running effects when consumers
|
|
506
|
+
// pass fresh closures every render.
|
|
507
|
+
const argsRef = useRef(args);
|
|
508
|
+
useEffect(() => {
|
|
509
|
+
argsRef.current = args;
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
// ── Hydrate once on mount ──────────────────────────────────────────────────
|
|
513
|
+
useEffect(() => {
|
|
514
|
+
if (!enabled || hydrated.current) return;
|
|
515
|
+
hydrated.current = true;
|
|
516
|
+
|
|
517
|
+
const cfg = argsRef.current.config;
|
|
518
|
+
if (!cfg) return;
|
|
519
|
+
|
|
520
|
+
let initial: PersistedState = {};
|
|
521
|
+
if (cfg.mode === "search-params") {
|
|
522
|
+
initial = readSearchParams(
|
|
523
|
+
argsRef.current.filterKeys,
|
|
524
|
+
cfg.paramMap ?? {},
|
|
525
|
+
defaultPageSizeRef.current,
|
|
526
|
+
);
|
|
527
|
+
} else if (cfg.mode === "local-storage") {
|
|
528
|
+
initial = readLocalStorage(cfg.storageKey);
|
|
529
|
+
} else if (cfg.mode === "manual") {
|
|
530
|
+
initial = cfg.initialState ?? {};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
applyPersistedState(initial, argsRef.current);
|
|
534
|
+
}, [enabled, mode]);
|
|
535
|
+
|
|
536
|
+
// ── Sync state → destination on every change ──────────────────────────────
|
|
537
|
+
useEffect(() => {
|
|
538
|
+
if (!enabled || !hydrated.current) return;
|
|
539
|
+
const cfg = argsRef.current.config;
|
|
540
|
+
if (!cfg) return;
|
|
541
|
+
|
|
542
|
+
const snapshot = snapshotPersistedState({
|
|
543
|
+
...argsRef.current,
|
|
544
|
+
defaultPageSize: defaultPageSizeRef.current,
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
if (cfg.mode === "search-params") {
|
|
548
|
+
writeSearchParams(
|
|
549
|
+
snapshot,
|
|
550
|
+
argsRef.current.filterKeys,
|
|
551
|
+
cfg.paramMap ?? {},
|
|
552
|
+
defaultPageSizeRef.current,
|
|
553
|
+
);
|
|
554
|
+
} else if (cfg.mode === "local-storage") {
|
|
555
|
+
writeLocalStorage(cfg.storageKey, snapshot);
|
|
556
|
+
}
|
|
557
|
+
// Fired for every mode (manual mode's primary side-effect *is* this call;
|
|
558
|
+
// for URL/storage modes it doubles as a subscribe-to-change hook).
|
|
559
|
+
cfg.onChange?.(snapshot);
|
|
560
|
+
}, [
|
|
561
|
+
enabled,
|
|
562
|
+
mode,
|
|
563
|
+
args.filterValues,
|
|
564
|
+
args.pagination,
|
|
565
|
+
args.sorting,
|
|
566
|
+
args.columnOrder,
|
|
567
|
+
args.columnVisibility,
|
|
568
|
+
args.columnPinning,
|
|
569
|
+
args.columnSizing,
|
|
570
|
+
args.grouping,
|
|
571
|
+
args.expanded,
|
|
572
|
+
]);
|
|
573
|
+
}
|