@classytic/fluid 0.3.6 → 0.4.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/dist/client/core.d.mts +14 -18
- package/dist/client/core.mjs +105 -279
- package/dist/client/spreadsheet.d.mts +207 -0
- package/dist/client/spreadsheet.mjs +611 -0
- package/dist/client/table.d.mts +41 -5
- package/dist/client/table.mjs +54 -7
- package/dist/dropdown-wrapper-B86u9Fri.mjs +357 -0
- package/dist/forms.mjs +4 -4
- package/package.json +32 -15
- package/dist/api-pagination-DBTE0yk4.mjs +0 -190
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { memo, useCallback, useEffect, useMemo, useReducer, useRef } from "react";
|
|
5
|
+
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
6
|
+
|
|
7
|
+
//#region src/components/spreadsheet/spreadsheet-row.tsx
|
|
8
|
+
function SpreadsheetRowInner({ rowId, rowIndex, item, visibleColumns, dispatch, isReadOnly, rowActions, onCellKeyDown, cellAttr = "data-cell", isMobile = false, style, measureRef }) {
|
|
9
|
+
const handleChange = useCallback((fieldOrPatch, value) => {
|
|
10
|
+
if (typeof fieldOrPatch === "string") dispatch({
|
|
11
|
+
type: "UPDATE_CELL",
|
|
12
|
+
rowId,
|
|
13
|
+
field: fieldOrPatch,
|
|
14
|
+
value
|
|
15
|
+
});
|
|
16
|
+
else dispatch({
|
|
17
|
+
type: "UPDATE_ROW",
|
|
18
|
+
rowId,
|
|
19
|
+
patch: fieldOrPatch
|
|
20
|
+
});
|
|
21
|
+
}, [dispatch, rowId]);
|
|
22
|
+
if (isMobile) return /* @__PURE__ */ jsxs("div", {
|
|
23
|
+
className: "rounded-lg border bg-card p-3 space-y-2",
|
|
24
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
25
|
+
className: "grid grid-cols-12 gap-2",
|
|
26
|
+
children: visibleColumns.map((col) => {
|
|
27
|
+
const CellComponent = col.cell;
|
|
28
|
+
const field = col.field ?? col.id;
|
|
29
|
+
const value = item?.[field];
|
|
30
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
31
|
+
style: { gridColumn: `span ${col.mobileSpan ?? 12}` },
|
|
32
|
+
[cellAttr]: `${rowId}:${col.id}`,
|
|
33
|
+
children: [col.mobileLabel && /* @__PURE__ */ jsx("label", {
|
|
34
|
+
className: "text-xs text-muted-foreground mb-1 block",
|
|
35
|
+
children: col.mobileLabel
|
|
36
|
+
}), /* @__PURE__ */ jsx(CellComponent, {
|
|
37
|
+
value,
|
|
38
|
+
row: item,
|
|
39
|
+
rowId,
|
|
40
|
+
rowIndex,
|
|
41
|
+
column: col,
|
|
42
|
+
onChange: handleChange,
|
|
43
|
+
isReadOnly
|
|
44
|
+
})]
|
|
45
|
+
}, col.id);
|
|
46
|
+
})
|
|
47
|
+
}), rowActions && /* @__PURE__ */ jsx("div", {
|
|
48
|
+
className: "flex justify-end pt-1",
|
|
49
|
+
children: rowActions({
|
|
50
|
+
rowId,
|
|
51
|
+
rowIndex,
|
|
52
|
+
isReadOnly
|
|
53
|
+
})
|
|
54
|
+
})]
|
|
55
|
+
});
|
|
56
|
+
const isVirtual = !!style;
|
|
57
|
+
return /* @__PURE__ */ jsxs("tr", {
|
|
58
|
+
ref: measureRef,
|
|
59
|
+
"data-index": measureRef ? rowIndex : void 0,
|
|
60
|
+
className: "border-b transition-colors hover:bg-muted/30",
|
|
61
|
+
style,
|
|
62
|
+
children: [visibleColumns.map((col) => {
|
|
63
|
+
const CellComponent = col.cell;
|
|
64
|
+
const field = col.field ?? col.id;
|
|
65
|
+
const value = item?.[field];
|
|
66
|
+
return /* @__PURE__ */ jsx("td", {
|
|
67
|
+
className: `px-2 py-1 overflow-hidden ${col.align === "right" ? "text-right" : col.align === "center" ? "text-center" : "text-left"}`,
|
|
68
|
+
style: isVirtual ? {
|
|
69
|
+
flex: `0 0 ${col.width}`,
|
|
70
|
+
minWidth: 0
|
|
71
|
+
} : void 0,
|
|
72
|
+
[cellAttr]: `${rowId}:${col.id}`,
|
|
73
|
+
onKeyDown: onCellKeyDown ? (e) => onCellKeyDown(e, rowId, col.id) : void 0,
|
|
74
|
+
children: /* @__PURE__ */ jsx(CellComponent, {
|
|
75
|
+
value,
|
|
76
|
+
row: item,
|
|
77
|
+
rowId,
|
|
78
|
+
rowIndex,
|
|
79
|
+
column: col,
|
|
80
|
+
onChange: handleChange,
|
|
81
|
+
isReadOnly
|
|
82
|
+
})
|
|
83
|
+
}, col.id);
|
|
84
|
+
}), rowActions && /* @__PURE__ */ jsx("td", {
|
|
85
|
+
className: "px-2 py-1",
|
|
86
|
+
style: isVirtual ? {
|
|
87
|
+
flex: "0 0 8%",
|
|
88
|
+
minWidth: 0
|
|
89
|
+
} : { width: "8%" },
|
|
90
|
+
children: rowActions({
|
|
91
|
+
rowId,
|
|
92
|
+
rowIndex,
|
|
93
|
+
isReadOnly
|
|
94
|
+
})
|
|
95
|
+
})]
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
function rowPropsAreEqual(prev, next) {
|
|
99
|
+
return prev.rowId === next.rowId && prev.rowIndex === next.rowIndex && prev.item === next.item && prev.isReadOnly === next.isReadOnly && prev.isMobile === next.isMobile && prev.visibleColumns === next.visibleColumns && prev.measureRef === next.measureRef && prev.style?.transform === next.style?.transform;
|
|
100
|
+
}
|
|
101
|
+
const SpreadsheetRow = memo(SpreadsheetRowInner, rowPropsAreEqual);
|
|
102
|
+
|
|
103
|
+
//#endregion
|
|
104
|
+
//#region src/components/spreadsheet/use-spreadsheet-keyboard.ts
|
|
105
|
+
/**
|
|
106
|
+
* Data attribute used to mark focusable cells for keyboard navigation.
|
|
107
|
+
* Each cell should have: data-cell="rowId:colId"
|
|
108
|
+
*/
|
|
109
|
+
const CELL_ATTR = "data-cell";
|
|
110
|
+
function findCell(container, rowId, colId) {
|
|
111
|
+
return container.querySelector(`[${CELL_ATTR}="${rowId}:${colId}"]`);
|
|
112
|
+
}
|
|
113
|
+
function focusCell(cell) {
|
|
114
|
+
const focusable = cell.querySelector("input, select, textarea, button, [tabindex]:not([tabindex=\"-1\"])");
|
|
115
|
+
if (focusable) focusable.focus();
|
|
116
|
+
else cell.focus();
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Keyboard navigation for spreadsheet cells.
|
|
120
|
+
*
|
|
121
|
+
* Tab → next cell → wrap to next row
|
|
122
|
+
* Shift+Tab → previous cell → wrap to previous row
|
|
123
|
+
* Enter → commit + move down (same column, next row)
|
|
124
|
+
*/
|
|
125
|
+
function useSpreadsheetKeyboard({ orderedIds, columnIds, containerRef, onAddRow }) {
|
|
126
|
+
const orderedIdsRef = useRef(orderedIds);
|
|
127
|
+
const columnIdsRef = useRef(columnIds);
|
|
128
|
+
const onAddRowRef = useRef(onAddRow);
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
orderedIdsRef.current = orderedIds;
|
|
131
|
+
columnIdsRef.current = columnIds;
|
|
132
|
+
onAddRowRef.current = onAddRow;
|
|
133
|
+
}, [
|
|
134
|
+
orderedIds,
|
|
135
|
+
columnIds,
|
|
136
|
+
onAddRow
|
|
137
|
+
]);
|
|
138
|
+
return {
|
|
139
|
+
handleCellKeyDown: useCallback((e, rowId, colId) => {
|
|
140
|
+
const container = containerRef.current;
|
|
141
|
+
if (!container) return;
|
|
142
|
+
const currentOrderedIds = orderedIdsRef.current;
|
|
143
|
+
const currentColumnIds = columnIdsRef.current;
|
|
144
|
+
const rowIdx = currentOrderedIds.indexOf(rowId);
|
|
145
|
+
const colIdx = currentColumnIds.indexOf(colId);
|
|
146
|
+
if (rowIdx === -1 || colIdx === -1) return;
|
|
147
|
+
let targetRowIdx = rowIdx;
|
|
148
|
+
let targetColIdx = colIdx;
|
|
149
|
+
if (e.key === "Tab") {
|
|
150
|
+
e.preventDefault();
|
|
151
|
+
if (e.shiftKey) {
|
|
152
|
+
targetColIdx--;
|
|
153
|
+
if (targetColIdx < 0) {
|
|
154
|
+
targetRowIdx--;
|
|
155
|
+
targetColIdx = currentColumnIds.length - 1;
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
targetColIdx++;
|
|
159
|
+
if (targetColIdx >= currentColumnIds.length) {
|
|
160
|
+
targetRowIdx++;
|
|
161
|
+
targetColIdx = 0;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
} else if (e.key === "Enter" && !e.shiftKey) {
|
|
165
|
+
if (document.activeElement instanceof HTMLElement) document.activeElement.blur();
|
|
166
|
+
e.preventDefault();
|
|
167
|
+
targetRowIdx++;
|
|
168
|
+
} else if (e.key === "ArrowDown") {
|
|
169
|
+
e.preventDefault();
|
|
170
|
+
targetRowIdx++;
|
|
171
|
+
} else if (e.key === "ArrowUp") {
|
|
172
|
+
e.preventDefault();
|
|
173
|
+
targetRowIdx--;
|
|
174
|
+
} else if (e.key === "ArrowRight") {
|
|
175
|
+
const active = document.activeElement;
|
|
176
|
+
if (!active || active.tagName !== "INPUT" || active.selectionStart === active.value?.length) {
|
|
177
|
+
e.preventDefault();
|
|
178
|
+
targetColIdx++;
|
|
179
|
+
} else return;
|
|
180
|
+
} else if (e.key === "ArrowLeft") {
|
|
181
|
+
const active = document.activeElement;
|
|
182
|
+
if (!active || active.tagName !== "INPUT" || active.selectionEnd === 0) {
|
|
183
|
+
e.preventDefault();
|
|
184
|
+
targetColIdx--;
|
|
185
|
+
} else return;
|
|
186
|
+
} else return;
|
|
187
|
+
if (targetRowIdx >= currentOrderedIds.length) {
|
|
188
|
+
if (onAddRowRef.current) {
|
|
189
|
+
const lastRowId = currentOrderedIds[currentOrderedIds.length - 1];
|
|
190
|
+
onAddRowRef.current(lastRowId);
|
|
191
|
+
}
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (targetRowIdx < 0) return;
|
|
195
|
+
if (targetColIdx < 0 || targetColIdx >= currentColumnIds.length) return;
|
|
196
|
+
const targetRowId = currentOrderedIds[targetRowIdx];
|
|
197
|
+
const targetColId = currentColumnIds[targetColIdx];
|
|
198
|
+
const targetCell = findCell(container, targetRowId, targetColId);
|
|
199
|
+
if (targetCell) requestAnimationFrame(() => focusCell(targetCell));
|
|
200
|
+
}, [containerRef]),
|
|
201
|
+
CELL_ATTR
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
//#endregion
|
|
206
|
+
//#region src/components/spreadsheet/use-spreadsheet-clipboard.ts
|
|
207
|
+
/**
|
|
208
|
+
* Handles copy/pasting from Excel or other spreadsheets natively.
|
|
209
|
+
* Supports matrix pasting (multiple rows and columns at once) via BATCH updates.
|
|
210
|
+
*/
|
|
211
|
+
function useSpreadsheetClipboard({ containerRef, orderedIds, columns, dispatch, isReadOnly = false }) {
|
|
212
|
+
const orderedIdsRef = useRef(orderedIds);
|
|
213
|
+
const columnsRef = useRef(columns);
|
|
214
|
+
const isReadOnlyRef = useRef(isReadOnly);
|
|
215
|
+
useEffect(() => {
|
|
216
|
+
orderedIdsRef.current = orderedIds;
|
|
217
|
+
columnsRef.current = columns;
|
|
218
|
+
isReadOnlyRef.current = isReadOnly;
|
|
219
|
+
}, [
|
|
220
|
+
orderedIds,
|
|
221
|
+
columns,
|
|
222
|
+
isReadOnly
|
|
223
|
+
]);
|
|
224
|
+
useEffect(() => {
|
|
225
|
+
const container = containerRef.current;
|
|
226
|
+
if (!container) return;
|
|
227
|
+
function handlePaste(e) {
|
|
228
|
+
if (isReadOnlyRef.current) return;
|
|
229
|
+
const activeElement = document.activeElement;
|
|
230
|
+
if (!activeElement || !container?.contains(activeElement)) return;
|
|
231
|
+
const cellElement = activeElement.closest("[data-cell]");
|
|
232
|
+
if (!cellElement) return;
|
|
233
|
+
const cellCoord = cellElement.getAttribute("data-cell");
|
|
234
|
+
if (!cellCoord) return;
|
|
235
|
+
const [startRowId, startColId] = cellCoord.split(":");
|
|
236
|
+
const clipboardData = e.clipboardData;
|
|
237
|
+
if (!clipboardData) return;
|
|
238
|
+
const pastedText = clipboardData.getData("text/plain");
|
|
239
|
+
if (!pastedText) return;
|
|
240
|
+
if (!(pastedText.includes(" ") || pastedText.includes("\n"))) return;
|
|
241
|
+
e.preventDefault();
|
|
242
|
+
const currentOrderedIds = orderedIdsRef.current;
|
|
243
|
+
const visibleCols = columnsRef.current.filter((c) => !c.hiddenWhen?.({ isReadOnly: isReadOnlyRef.current }));
|
|
244
|
+
const startRowIdx = currentOrderedIds.indexOf(startRowId);
|
|
245
|
+
const startColIdx = visibleCols.findIndex((c) => c.id === startColId);
|
|
246
|
+
if (startRowIdx === -1 || startColIdx === -1) return;
|
|
247
|
+
const rows = pastedText.replace(/\r?\n$/, "").split(/\r?\n/);
|
|
248
|
+
const batchActions = [];
|
|
249
|
+
rows.forEach((row, rIdx) => {
|
|
250
|
+
const targetRowIdx = startRowIdx + rIdx;
|
|
251
|
+
if (targetRowIdx >= currentOrderedIds.length) return;
|
|
252
|
+
const targetRowId = currentOrderedIds[targetRowIdx];
|
|
253
|
+
row.split(" ").forEach((cellValue, cIdx) => {
|
|
254
|
+
const targetColIdx = startColIdx + cIdx;
|
|
255
|
+
if (targetColIdx >= visibleCols.length) return;
|
|
256
|
+
const col = visibleCols[targetColIdx];
|
|
257
|
+
const field = col.field ?? col.id;
|
|
258
|
+
let value = cellValue;
|
|
259
|
+
if (col.dataType === "number") {
|
|
260
|
+
const num = Number(cellValue);
|
|
261
|
+
value = isNaN(num) ? 0 : num;
|
|
262
|
+
}
|
|
263
|
+
batchActions.push({
|
|
264
|
+
type: "UPDATE_CELL",
|
|
265
|
+
rowId: targetRowId,
|
|
266
|
+
field,
|
|
267
|
+
value
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
if (batchActions.length > 0) dispatch({
|
|
272
|
+
type: "BATCH",
|
|
273
|
+
actions: batchActions
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
container.addEventListener("paste", handlePaste);
|
|
277
|
+
return () => container.removeEventListener("paste", handlePaste);
|
|
278
|
+
}, [containerRef, dispatch]);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
//#endregion
|
|
282
|
+
//#region src/components/spreadsheet/spreadsheet-table.tsx
|
|
283
|
+
const VIRTUAL_ROW_BASE = {
|
|
284
|
+
display: "flex",
|
|
285
|
+
alignItems: "stretch",
|
|
286
|
+
position: "absolute",
|
|
287
|
+
top: 0,
|
|
288
|
+
left: 0,
|
|
289
|
+
width: "100%",
|
|
290
|
+
willChange: "transform"
|
|
291
|
+
};
|
|
292
|
+
function SpreadsheetTable({ columns, items, orderedIds, dispatch, isReadOnly = false, footer, mobileFooter, rowActions, className = "", ariaLabel = "Spreadsheet", onAddRow, virtualize = false, estimateRowHeight = 48 }) {
|
|
293
|
+
const containerRef = useRef(null);
|
|
294
|
+
const tableContainerRef = useRef(null);
|
|
295
|
+
const mobileContainerRef = useRef(null);
|
|
296
|
+
const visibleColumns = useMemo(() => columns.filter((col) => !col.hiddenWhen?.({ isReadOnly })), [columns, isReadOnly]);
|
|
297
|
+
const { handleCellKeyDown, CELL_ATTR } = useSpreadsheetKeyboard({
|
|
298
|
+
orderedIds,
|
|
299
|
+
columnIds: useMemo(() => visibleColumns.map((col) => col.id), [visibleColumns]),
|
|
300
|
+
containerRef,
|
|
301
|
+
onAddRow
|
|
302
|
+
});
|
|
303
|
+
useSpreadsheetClipboard({
|
|
304
|
+
containerRef,
|
|
305
|
+
orderedIds,
|
|
306
|
+
columns,
|
|
307
|
+
dispatch,
|
|
308
|
+
isReadOnly
|
|
309
|
+
});
|
|
310
|
+
const getItemKey = useCallback((index) => orderedIds[index] ?? index, [orderedIds]);
|
|
311
|
+
const estimateSize = useCallback(() => estimateRowHeight, [estimateRowHeight]);
|
|
312
|
+
const desktopVirtualizer = useVirtualizer({
|
|
313
|
+
count: orderedIds.length,
|
|
314
|
+
getScrollElement: () => tableContainerRef.current,
|
|
315
|
+
estimateSize,
|
|
316
|
+
measureElement: virtualize ? (el) => el.getBoundingClientRect().height : void 0,
|
|
317
|
+
overscan: 5,
|
|
318
|
+
getItemKey,
|
|
319
|
+
enabled: virtualize
|
|
320
|
+
});
|
|
321
|
+
const mobileVirtualizer = useVirtualizer({
|
|
322
|
+
count: orderedIds.length,
|
|
323
|
+
getScrollElement: () => mobileContainerRef.current,
|
|
324
|
+
estimateSize,
|
|
325
|
+
measureElement: virtualize ? (el) => el.getBoundingClientRect().height : void 0,
|
|
326
|
+
overscan: 3,
|
|
327
|
+
getItemKey,
|
|
328
|
+
enabled: virtualize
|
|
329
|
+
});
|
|
330
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
331
|
+
ref: containerRef,
|
|
332
|
+
className,
|
|
333
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
334
|
+
ref: tableContainerRef,
|
|
335
|
+
className: "hidden md:block overflow-x-auto",
|
|
336
|
+
style: virtualize ? {
|
|
337
|
+
maxHeight: "600px",
|
|
338
|
+
overflowY: "auto"
|
|
339
|
+
} : void 0,
|
|
340
|
+
children: /* @__PURE__ */ jsxs("table", {
|
|
341
|
+
className: "w-full border-collapse table-fixed",
|
|
342
|
+
style: { minWidth: "800px" },
|
|
343
|
+
"aria-label": ariaLabel,
|
|
344
|
+
children: [
|
|
345
|
+
/* @__PURE__ */ jsx("thead", {
|
|
346
|
+
className: "sticky top-0 z-10 bg-background",
|
|
347
|
+
children: /* @__PURE__ */ jsxs("tr", {
|
|
348
|
+
className: "border-b bg-muted/50",
|
|
349
|
+
style: virtualize ? {
|
|
350
|
+
display: "flex",
|
|
351
|
+
width: "100%"
|
|
352
|
+
} : void 0,
|
|
353
|
+
children: [visibleColumns.map((col) => /* @__PURE__ */ jsx("th", {
|
|
354
|
+
className: `px-2 py-2 text-xs font-medium text-muted-foreground ${col.align === "right" ? "text-right" : col.align === "center" ? "text-center" : "text-left"}`,
|
|
355
|
+
style: virtualize ? {
|
|
356
|
+
width: col.width,
|
|
357
|
+
flex: `0 0 ${col.width}`,
|
|
358
|
+
minWidth: 0
|
|
359
|
+
} : { width: col.width },
|
|
360
|
+
children: col.header
|
|
361
|
+
}, col.id)), rowActions && /* @__PURE__ */ jsx("th", {
|
|
362
|
+
className: "px-2 py-2",
|
|
363
|
+
style: virtualize ? {
|
|
364
|
+
width: "8%",
|
|
365
|
+
flex: "0 0 8%",
|
|
366
|
+
minWidth: 0
|
|
367
|
+
} : { width: "8%" }
|
|
368
|
+
})]
|
|
369
|
+
})
|
|
370
|
+
}),
|
|
371
|
+
/* @__PURE__ */ jsx("tbody", {
|
|
372
|
+
style: virtualize ? {
|
|
373
|
+
height: `${desktopVirtualizer.getTotalSize()}px`,
|
|
374
|
+
position: "relative"
|
|
375
|
+
} : void 0,
|
|
376
|
+
children: virtualize ? desktopVirtualizer.getVirtualItems().map((virtualRow) => {
|
|
377
|
+
const rowId = orderedIds[virtualRow.index];
|
|
378
|
+
const item = items.get(rowId);
|
|
379
|
+
if (!item) return null;
|
|
380
|
+
return /* @__PURE__ */ jsx(SpreadsheetRow, {
|
|
381
|
+
rowId,
|
|
382
|
+
rowIndex: virtualRow.index,
|
|
383
|
+
item,
|
|
384
|
+
visibleColumns,
|
|
385
|
+
dispatch,
|
|
386
|
+
isReadOnly,
|
|
387
|
+
rowActions,
|
|
388
|
+
onCellKeyDown: handleCellKeyDown,
|
|
389
|
+
cellAttr: CELL_ATTR,
|
|
390
|
+
isMobile: false,
|
|
391
|
+
measureRef: desktopVirtualizer.measureElement,
|
|
392
|
+
style: {
|
|
393
|
+
...VIRTUAL_ROW_BASE,
|
|
394
|
+
transform: `translateY(${virtualRow.start}px)`
|
|
395
|
+
}
|
|
396
|
+
}, virtualRow.key);
|
|
397
|
+
}) : orderedIds.map((rowId, index) => {
|
|
398
|
+
const item = items.get(rowId);
|
|
399
|
+
if (!item) return null;
|
|
400
|
+
return /* @__PURE__ */ jsx(SpreadsheetRow, {
|
|
401
|
+
rowId,
|
|
402
|
+
rowIndex: index,
|
|
403
|
+
item,
|
|
404
|
+
visibleColumns,
|
|
405
|
+
dispatch,
|
|
406
|
+
isReadOnly,
|
|
407
|
+
rowActions,
|
|
408
|
+
onCellKeyDown: handleCellKeyDown,
|
|
409
|
+
cellAttr: CELL_ATTR,
|
|
410
|
+
isMobile: false
|
|
411
|
+
}, rowId);
|
|
412
|
+
})
|
|
413
|
+
}),
|
|
414
|
+
footer && /* @__PURE__ */ jsx("tfoot", { children: footer })
|
|
415
|
+
]
|
|
416
|
+
})
|
|
417
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
418
|
+
ref: mobileContainerRef,
|
|
419
|
+
className: "md:hidden space-y-3",
|
|
420
|
+
style: virtualize ? {
|
|
421
|
+
maxHeight: "600px",
|
|
422
|
+
overflowY: "auto"
|
|
423
|
+
} : void 0,
|
|
424
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
425
|
+
style: virtualize ? {
|
|
426
|
+
height: `${mobileVirtualizer.getTotalSize()}px`,
|
|
427
|
+
position: "relative"
|
|
428
|
+
} : void 0,
|
|
429
|
+
children: virtualize ? mobileVirtualizer.getVirtualItems().map((virtualRow) => {
|
|
430
|
+
const rowId = orderedIds[virtualRow.index];
|
|
431
|
+
const item = items.get(rowId);
|
|
432
|
+
if (!item) return null;
|
|
433
|
+
return /* @__PURE__ */ jsx(SpreadsheetRow, {
|
|
434
|
+
rowId,
|
|
435
|
+
rowIndex: virtualRow.index,
|
|
436
|
+
item,
|
|
437
|
+
visibleColumns,
|
|
438
|
+
dispatch,
|
|
439
|
+
isReadOnly,
|
|
440
|
+
rowActions,
|
|
441
|
+
cellAttr: CELL_ATTR,
|
|
442
|
+
isMobile: true,
|
|
443
|
+
measureRef: mobileVirtualizer.measureElement,
|
|
444
|
+
style: {
|
|
445
|
+
...VIRTUAL_ROW_BASE,
|
|
446
|
+
transform: `translateY(${virtualRow.start}px)`
|
|
447
|
+
}
|
|
448
|
+
}, virtualRow.key);
|
|
449
|
+
}) : orderedIds.map((rowId, index) => {
|
|
450
|
+
const item = items.get(rowId);
|
|
451
|
+
if (!item) return null;
|
|
452
|
+
return /* @__PURE__ */ jsx(SpreadsheetRow, {
|
|
453
|
+
rowId,
|
|
454
|
+
rowIndex: index,
|
|
455
|
+
item,
|
|
456
|
+
visibleColumns,
|
|
457
|
+
dispatch,
|
|
458
|
+
isReadOnly,
|
|
459
|
+
rowActions,
|
|
460
|
+
cellAttr: CELL_ATTR,
|
|
461
|
+
isMobile: true
|
|
462
|
+
}, rowId);
|
|
463
|
+
})
|
|
464
|
+
}), mobileFooter && /* @__PURE__ */ jsx("div", {
|
|
465
|
+
className: "rounded-lg border bg-card p-3",
|
|
466
|
+
children: mobileFooter
|
|
467
|
+
})]
|
|
468
|
+
})]
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
//#endregion
|
|
473
|
+
//#region src/components/spreadsheet/use-spreadsheet-store.ts
|
|
474
|
+
function defaultGetId(item) {
|
|
475
|
+
if (item && typeof item === "object") {
|
|
476
|
+
const obj = item;
|
|
477
|
+
const id = obj._id ?? obj.id;
|
|
478
|
+
if (typeof id === "string" && id) return id;
|
|
479
|
+
}
|
|
480
|
+
return crypto.randomUUID();
|
|
481
|
+
}
|
|
482
|
+
function spreadsheetReducer(state, action) {
|
|
483
|
+
switch (action.type) {
|
|
484
|
+
case "SET_ALL": {
|
|
485
|
+
const getId = action.getId ?? defaultGetId;
|
|
486
|
+
const items = /* @__PURE__ */ new Map();
|
|
487
|
+
const orderedIds = [];
|
|
488
|
+
for (const item of action.items) {
|
|
489
|
+
let id = getId(item);
|
|
490
|
+
while (items.has(id)) id = `${id}_${crypto.randomUUID().slice(0, 8)}`;
|
|
491
|
+
items.set(id, item);
|
|
492
|
+
orderedIds.push(id);
|
|
493
|
+
}
|
|
494
|
+
return {
|
|
495
|
+
items,
|
|
496
|
+
orderedIds
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
case "UPDATE_CELL": {
|
|
500
|
+
const existing = state.items.get(action.rowId);
|
|
501
|
+
if (!existing) return state;
|
|
502
|
+
const updated = {
|
|
503
|
+
...existing,
|
|
504
|
+
[action.field]: action.value
|
|
505
|
+
};
|
|
506
|
+
const newItems = new Map(state.items);
|
|
507
|
+
newItems.set(action.rowId, updated);
|
|
508
|
+
return {
|
|
509
|
+
...state,
|
|
510
|
+
items: newItems
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
case "UPDATE_ROW": {
|
|
514
|
+
const existing = state.items.get(action.rowId);
|
|
515
|
+
if (!existing) return state;
|
|
516
|
+
const updated = {
|
|
517
|
+
...existing,
|
|
518
|
+
...action.patch
|
|
519
|
+
};
|
|
520
|
+
const newItems = new Map(state.items);
|
|
521
|
+
newItems.set(action.rowId, updated);
|
|
522
|
+
return {
|
|
523
|
+
...state,
|
|
524
|
+
items: newItems
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
case "ADD_ROW": {
|
|
528
|
+
const newId = action.newId ?? crypto.randomUUID();
|
|
529
|
+
const newItems = new Map(state.items);
|
|
530
|
+
newItems.set(newId, action.item);
|
|
531
|
+
const idx = state.orderedIds.indexOf(action.afterRowId);
|
|
532
|
+
const insertAt = idx === -1 ? state.orderedIds.length : idx + 1;
|
|
533
|
+
const newIds = [...state.orderedIds];
|
|
534
|
+
newIds.splice(insertAt, 0, newId);
|
|
535
|
+
return {
|
|
536
|
+
items: newItems,
|
|
537
|
+
orderedIds: newIds
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
case "REMOVE_ROW": {
|
|
541
|
+
if (state.orderedIds.length <= 1) return state;
|
|
542
|
+
const newItems = new Map(state.items);
|
|
543
|
+
newItems.delete(action.rowId);
|
|
544
|
+
return {
|
|
545
|
+
items: newItems,
|
|
546
|
+
orderedIds: state.orderedIds.filter((id) => id !== action.rowId)
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
case "BATCH": {
|
|
550
|
+
if (action.actions.every((a) => a.type === "UPDATE_CELL")) {
|
|
551
|
+
const newItems = new Map(state.items);
|
|
552
|
+
for (const sub of action.actions) {
|
|
553
|
+
if (sub.type !== "UPDATE_CELL") continue;
|
|
554
|
+
const existing = newItems.get(sub.rowId);
|
|
555
|
+
if (!existing) continue;
|
|
556
|
+
newItems.set(sub.rowId, {
|
|
557
|
+
...existing,
|
|
558
|
+
[sub.field]: sub.value
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
return {
|
|
562
|
+
...state,
|
|
563
|
+
items: newItems
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
let current = state;
|
|
567
|
+
for (const sub of action.actions) current = spreadsheetReducer(current, sub);
|
|
568
|
+
return current;
|
|
569
|
+
}
|
|
570
|
+
default: return state;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
const EMPTY_STATE = {
|
|
574
|
+
items: /* @__PURE__ */ new Map(),
|
|
575
|
+
orderedIds: []
|
|
576
|
+
};
|
|
577
|
+
function useSpreadsheetStore(initialItems, getId) {
|
|
578
|
+
const [state, dispatch] = useReducer(spreadsheetReducer, initialItems, (items) => {
|
|
579
|
+
if (!items || items.length === 0) return EMPTY_STATE;
|
|
580
|
+
return spreadsheetReducer(EMPTY_STATE, {
|
|
581
|
+
type: "SET_ALL",
|
|
582
|
+
items,
|
|
583
|
+
getId
|
|
584
|
+
});
|
|
585
|
+
});
|
|
586
|
+
const stateRef = useRef(state);
|
|
587
|
+
stateRef.current = state;
|
|
588
|
+
/** Get items as an ordered array (for API submission) — stable reference */
|
|
589
|
+
const getOrderedItems = useCallback(() => {
|
|
590
|
+
const { orderedIds, items } = stateRef.current;
|
|
591
|
+
return orderedIds.map((id) => items.get(id)).filter(Boolean);
|
|
592
|
+
}, []);
|
|
593
|
+
/** Convenience: total row count */
|
|
594
|
+
const rowCount = state.orderedIds.length;
|
|
595
|
+
return useMemo(() => ({
|
|
596
|
+
items: state.items,
|
|
597
|
+
orderedIds: state.orderedIds,
|
|
598
|
+
dispatch,
|
|
599
|
+
getOrderedItems,
|
|
600
|
+
rowCount
|
|
601
|
+
}), [
|
|
602
|
+
state.items,
|
|
603
|
+
state.orderedIds,
|
|
604
|
+
dispatch,
|
|
605
|
+
getOrderedItems,
|
|
606
|
+
rowCount
|
|
607
|
+
]);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
//#endregion
|
|
611
|
+
export { SpreadsheetRow, SpreadsheetTable, useSpreadsheetKeyboard, useSpreadsheetStore };
|
package/dist/client/table.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { n as ApiPaginationData } from "../api-pagination-CJ0vR_w6.mjs";
|
|
2
2
|
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
3
|
-
import React, { ReactNode } from "react";
|
|
4
|
-
import { ColumnDef } from "@tanstack/react-table";
|
|
3
|
+
import React$1, { ReactNode } from "react";
|
|
4
|
+
import { ColumnDef, Table as TanstackTable, VisibilityState, VisibilityState as VisibilityState$1 } from "@tanstack/react-table";
|
|
5
5
|
|
|
6
6
|
//#region src/components/data-table.d.ts
|
|
7
7
|
interface DataTablePaginationProps extends Partial<ApiPaginationData> {
|
|
@@ -15,11 +15,15 @@ interface DataTableProps<TData, TValue> {
|
|
|
15
15
|
enableSorting?: boolean;
|
|
16
16
|
enableRowSelection?: boolean;
|
|
17
17
|
onRowSelectionChange?: (selectedRows: TData[]) => void;
|
|
18
|
+
/** Controlled column visibility state. Keys are column IDs, values are booleans. */
|
|
19
|
+
columnVisibility?: VisibilityState$1;
|
|
20
|
+
/** Callback when column visibility changes. */
|
|
21
|
+
onColumnVisibilityChange?: (visibility: VisibilityState$1) => void;
|
|
18
22
|
className?: string;
|
|
19
23
|
/** Custom loading state renderer */
|
|
20
|
-
loadingState?: React.ReactNode;
|
|
24
|
+
loadingState?: React$1.ReactNode;
|
|
21
25
|
/** Custom empty state renderer */
|
|
22
|
-
emptyState?: React.ReactNode;
|
|
26
|
+
emptyState?: React$1.ReactNode;
|
|
23
27
|
}
|
|
24
28
|
declare function DataTable<TData, TValue>({
|
|
25
29
|
columns,
|
|
@@ -29,6 +33,8 @@ declare function DataTable<TData, TValue>({
|
|
|
29
33
|
enableSorting,
|
|
30
34
|
enableRowSelection,
|
|
31
35
|
onRowSelectionChange,
|
|
36
|
+
columnVisibility: controlledVisibility,
|
|
37
|
+
onColumnVisibilityChange,
|
|
32
38
|
className,
|
|
33
39
|
loadingState: customLoadingState,
|
|
34
40
|
emptyState: customEmptyState
|
|
@@ -81,4 +87,34 @@ declare function DataTableToolbar({
|
|
|
81
87
|
showResultCount
|
|
82
88
|
}: DataTableToolbarProps): react_jsx_runtime0.JSX.Element;
|
|
83
89
|
//#endregion
|
|
84
|
-
|
|
90
|
+
//#region src/components/data-table-column-toggle.d.ts
|
|
91
|
+
interface ToggleableColumn {
|
|
92
|
+
/** Column ID (must match the column key used in VisibilityState) */
|
|
93
|
+
id: string;
|
|
94
|
+
/** Display name shown in the toggle list */
|
|
95
|
+
header: string;
|
|
96
|
+
}
|
|
97
|
+
interface DataTableColumnToggleProps {
|
|
98
|
+
/** List of columns that can be toggled. Derive from your ColumnDef array. */
|
|
99
|
+
columns: ToggleableColumn[];
|
|
100
|
+
/** Controlled visibility state. Keys are column IDs, values are booleans. */
|
|
101
|
+
columnVisibility: VisibilityState$1;
|
|
102
|
+
/** Callback when visibility changes. */
|
|
103
|
+
onColumnVisibilityChange: (visibility: VisibilityState$1) => void;
|
|
104
|
+
/** Custom trigger element. Defaults to a Settings2 icon button. */
|
|
105
|
+
trigger?: React.ReactNode;
|
|
106
|
+
/** Label shown at the top of the dropdown */
|
|
107
|
+
label?: string;
|
|
108
|
+
/** Additional className for the trigger button */
|
|
109
|
+
className?: string;
|
|
110
|
+
}
|
|
111
|
+
declare function DataTableColumnToggle({
|
|
112
|
+
columns,
|
|
113
|
+
columnVisibility,
|
|
114
|
+
onColumnVisibilityChange,
|
|
115
|
+
trigger,
|
|
116
|
+
label,
|
|
117
|
+
className
|
|
118
|
+
}: DataTableColumnToggleProps): react_jsx_runtime0.JSX.Element;
|
|
119
|
+
//#endregion
|
|
120
|
+
export { DataTable, DataTableColumnToggle, type DataTableColumnToggleProps, type DataTablePaginationProps, type DataTableProps, DataTableToolbar, type DataTableToolbarProps, type TanstackTable, type ToggleableColumn, type VisibilityState };
|