bolt-table 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +501 -0
- package/dist/index.d.mts +1542 -0
- package/dist/index.d.ts +1542 -0
- package/dist/index.js +2051 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2043 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +38 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2043 @@
|
|
|
1
|
+
// src/BoltTable.tsx
|
|
2
|
+
import {
|
|
3
|
+
closestCenter,
|
|
4
|
+
DndContext,
|
|
5
|
+
DragOverlay,
|
|
6
|
+
PointerSensor,
|
|
7
|
+
useSensor,
|
|
8
|
+
useSensors
|
|
9
|
+
} from "@dnd-kit/core";
|
|
10
|
+
import {
|
|
11
|
+
arrayMove,
|
|
12
|
+
horizontalListSortingStrategy,
|
|
13
|
+
SortableContext
|
|
14
|
+
} from "@dnd-kit/sortable";
|
|
15
|
+
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
16
|
+
import {
|
|
17
|
+
ChevronDown,
|
|
18
|
+
ChevronLeft,
|
|
19
|
+
ChevronRight,
|
|
20
|
+
ChevronsLeft,
|
|
21
|
+
ChevronsRight,
|
|
22
|
+
GripVertical as GripVertical2
|
|
23
|
+
} from "lucide-react";
|
|
24
|
+
import React4, {
|
|
25
|
+
useCallback,
|
|
26
|
+
useMemo as useMemo2,
|
|
27
|
+
useRef as useRef4,
|
|
28
|
+
useState as useState2
|
|
29
|
+
} from "react";
|
|
30
|
+
|
|
31
|
+
// src/DraggableHeader.tsx
|
|
32
|
+
import { useSortable } from "@dnd-kit/sortable";
|
|
33
|
+
import {
|
|
34
|
+
ArrowDownAZ,
|
|
35
|
+
ArrowUpAZ,
|
|
36
|
+
EyeOff,
|
|
37
|
+
Filter,
|
|
38
|
+
FilterX,
|
|
39
|
+
GripVertical,
|
|
40
|
+
Pin,
|
|
41
|
+
PinOff
|
|
42
|
+
} from "lucide-react";
|
|
43
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
44
|
+
import { createPortal } from "react-dom";
|
|
45
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
46
|
+
function isColumnSortable(col) {
|
|
47
|
+
return col.sortable !== false;
|
|
48
|
+
}
|
|
49
|
+
function isColumnFilterable(col) {
|
|
50
|
+
return col.filterable !== false;
|
|
51
|
+
}
|
|
52
|
+
var DraggableHeader = React.memo(
|
|
53
|
+
({
|
|
54
|
+
column,
|
|
55
|
+
visualIndex,
|
|
56
|
+
accentColor,
|
|
57
|
+
onResizeStart,
|
|
58
|
+
styles,
|
|
59
|
+
classNames,
|
|
60
|
+
hideGripIcon = false,
|
|
61
|
+
gripIcon,
|
|
62
|
+
stickyOffset,
|
|
63
|
+
onTogglePin,
|
|
64
|
+
onToggleHide,
|
|
65
|
+
isLastColumn = false,
|
|
66
|
+
sortDirection,
|
|
67
|
+
onSort,
|
|
68
|
+
filterValue = "",
|
|
69
|
+
onFilter,
|
|
70
|
+
onClearFilter,
|
|
71
|
+
customContextMenuItems
|
|
72
|
+
}) => {
|
|
73
|
+
const effectivelySortable = isColumnSortable(column);
|
|
74
|
+
const effectivelyFilterable = isColumnFilterable(column);
|
|
75
|
+
const [contextMenu, setContextMenu] = useState(null);
|
|
76
|
+
const [showFilterInput, setShowFilterInput] = useState(false);
|
|
77
|
+
const filterInputRef = useRef(null);
|
|
78
|
+
const menuRef = useRef(null);
|
|
79
|
+
const {
|
|
80
|
+
attributes,
|
|
81
|
+
listeners,
|
|
82
|
+
setNodeRef,
|
|
83
|
+
transition,
|
|
84
|
+
isDragging,
|
|
85
|
+
// true while this header is being dragged
|
|
86
|
+
isOver
|
|
87
|
+
// true while another header is being dragged over this one
|
|
88
|
+
} = useSortable({
|
|
89
|
+
id: column.key,
|
|
90
|
+
// Pinned columns cannot be dragged (their position is fixed by pinning)
|
|
91
|
+
disabled: Boolean(column.pinned)
|
|
92
|
+
});
|
|
93
|
+
const theme = typeof document !== "undefined" && document.documentElement.classList.contains("dark") ? "dark" : "light";
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
const handleClickOutside = (e) => {
|
|
96
|
+
if (menuRef.current && !menuRef.current.contains(e.target)) {
|
|
97
|
+
setContextMenu(null);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
if (contextMenu) {
|
|
101
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
102
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
103
|
+
}
|
|
104
|
+
}, [contextMenu]);
|
|
105
|
+
const handleContextMenu = (e) => {
|
|
106
|
+
e.preventDefault();
|
|
107
|
+
e.stopPropagation();
|
|
108
|
+
const menuWidth = 160;
|
|
109
|
+
const menuHeight = 180;
|
|
110
|
+
let x = e.clientX;
|
|
111
|
+
let y = e.clientY;
|
|
112
|
+
if (x + menuWidth > window.innerWidth) {
|
|
113
|
+
x = window.innerWidth - menuWidth - 10;
|
|
114
|
+
}
|
|
115
|
+
if (y + menuHeight > window.innerHeight) {
|
|
116
|
+
y = window.innerHeight - menuHeight - 10;
|
|
117
|
+
}
|
|
118
|
+
setContextMenu({ x, y });
|
|
119
|
+
};
|
|
120
|
+
const handleResizeStart = (e) => {
|
|
121
|
+
e.preventDefault();
|
|
122
|
+
e.stopPropagation();
|
|
123
|
+
if (column.pinned) return;
|
|
124
|
+
onResizeStart?.(column.key, e);
|
|
125
|
+
};
|
|
126
|
+
const columnWidth = column.width ?? 150;
|
|
127
|
+
const widthPx = `${columnWidth}px`;
|
|
128
|
+
const isPinned = Boolean(column.pinned);
|
|
129
|
+
const zIndex = isDragging ? 5 : isPinned ? 12 : 10;
|
|
130
|
+
const style = {
|
|
131
|
+
position: "sticky",
|
|
132
|
+
top: 0,
|
|
133
|
+
zIndex,
|
|
134
|
+
// Last column stretches to fill remaining space; all others are fixed width
|
|
135
|
+
width: isLastColumn ? "100%" : widthPx,
|
|
136
|
+
minWidth: widthPx,
|
|
137
|
+
...isLastColumn ? {} : { maxWidth: widthPx },
|
|
138
|
+
gridColumn: visualIndex + 1,
|
|
139
|
+
gridRow: 1,
|
|
140
|
+
// Fade out slightly while being dragged
|
|
141
|
+
opacity: isDragging ? 0.3 : 1,
|
|
142
|
+
transition,
|
|
143
|
+
borderWidth: "1px",
|
|
144
|
+
// Show a dashed accent-colored border when another column is dragged over this one
|
|
145
|
+
borderStyle: isOver ? "dashed" : "solid",
|
|
146
|
+
...isOver ? { borderColor: accentColor || "#1788ff" } : { borderLeftColor: "transparent" },
|
|
147
|
+
// Sticky positioning for pinned columns
|
|
148
|
+
...column.pinned === "left" && stickyOffset !== void 0 ? { left: `${stickyOffset}px`, position: "sticky" } : {},
|
|
149
|
+
...column.pinned === "right" && stickyOffset !== void 0 ? { right: `${stickyOffset}px`, position: "sticky" } : {},
|
|
150
|
+
// Pinned columns get a semi-transparent background so they visually
|
|
151
|
+
// separate from scrolling content behind them
|
|
152
|
+
...isPinned ? {
|
|
153
|
+
backgroundColor: styles?.pinnedBg ?? theme === "dark" ? "#10182890" : "#f9fafb90",
|
|
154
|
+
...styles?.pinnedHeader
|
|
155
|
+
} : {},
|
|
156
|
+
// Column-level style overrides applied last (highest specificity)
|
|
157
|
+
...column.style,
|
|
158
|
+
...styles?.header
|
|
159
|
+
};
|
|
160
|
+
const baseClasses = "bg-muted/40 group relative truncate flex h-9 items-center overflow-hidden backdrop-blur ";
|
|
161
|
+
const className = `${baseClasses} ${column.className ?? ""} ${classNames?.header ?? ""} ${classNames?.pinnedHeader ?? ""} `;
|
|
162
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
163
|
+
/* @__PURE__ */ jsxs(
|
|
164
|
+
"div",
|
|
165
|
+
{
|
|
166
|
+
ref: setNodeRef,
|
|
167
|
+
"data-column-key": column.key,
|
|
168
|
+
style,
|
|
169
|
+
className,
|
|
170
|
+
onContextMenu: handleContextMenu,
|
|
171
|
+
children: [
|
|
172
|
+
/* @__PURE__ */ jsxs(
|
|
173
|
+
"div",
|
|
174
|
+
{
|
|
175
|
+
...isPinned ? {} : attributes,
|
|
176
|
+
...isPinned ? {} : listeners,
|
|
177
|
+
className: `group relative z-10 flex h-full flex-1 touch-none items-center gap-1 truncate overflow-hidden px-2 font-medium ${isPinned ? "cursor-default" : "cursor-grab active:cursor-grabbing"}`,
|
|
178
|
+
"aria-label": isPinned ? `${column.key} column (pinned)` : `Drag ${column.key} column`,
|
|
179
|
+
children: [
|
|
180
|
+
hideGripIcon || isPinned ? null : gripIcon ?? /* @__PURE__ */ jsx(GripVertical, { className: "h-3 w-3 shrink-0 opacity-35 group-hover:opacity-80" }),
|
|
181
|
+
/* @__PURE__ */ jsxs(
|
|
182
|
+
"div",
|
|
183
|
+
{
|
|
184
|
+
className: `flex min-w-0 items-center gap-1 truncate overflow-hidden text-left select-none`,
|
|
185
|
+
children: [
|
|
186
|
+
column.title,
|
|
187
|
+
sortDirection === "asc" && /* @__PURE__ */ jsx(
|
|
188
|
+
ArrowUpAZ,
|
|
189
|
+
{
|
|
190
|
+
className: "h-3 w-3 shrink-0",
|
|
191
|
+
style: { color: accentColor }
|
|
192
|
+
}
|
|
193
|
+
),
|
|
194
|
+
sortDirection === "desc" && /* @__PURE__ */ jsx(
|
|
195
|
+
ArrowDownAZ,
|
|
196
|
+
{
|
|
197
|
+
className: "h-3 w-3 shrink-0",
|
|
198
|
+
style: { color: accentColor }
|
|
199
|
+
}
|
|
200
|
+
),
|
|
201
|
+
filterValue && /* @__PURE__ */ jsx(
|
|
202
|
+
Filter,
|
|
203
|
+
{
|
|
204
|
+
className: "h-2.5 w-2.5 shrink-0",
|
|
205
|
+
style: { color: accentColor }
|
|
206
|
+
}
|
|
207
|
+
)
|
|
208
|
+
]
|
|
209
|
+
}
|
|
210
|
+
)
|
|
211
|
+
]
|
|
212
|
+
}
|
|
213
|
+
),
|
|
214
|
+
isPinned && /* @__PURE__ */ jsx(
|
|
215
|
+
"button",
|
|
216
|
+
{
|
|
217
|
+
className: "group/unpin relative h-full w-6 shrink-0 cursor-pointer border-0 bg-transparent p-0",
|
|
218
|
+
onClick: (e) => {
|
|
219
|
+
e.preventDefault();
|
|
220
|
+
e.stopPropagation();
|
|
221
|
+
onTogglePin?.(column.key, false);
|
|
222
|
+
},
|
|
223
|
+
"aria-label": `Unpin ${column.key} column`,
|
|
224
|
+
title: "Unpin column",
|
|
225
|
+
style: { color: accentColor || "1788ff" },
|
|
226
|
+
children: /* @__PURE__ */ jsx(PinOff, { className: "mx-auto h-3 w-3" })
|
|
227
|
+
}
|
|
228
|
+
),
|
|
229
|
+
!isPinned && /* @__PURE__ */ jsx(
|
|
230
|
+
"button",
|
|
231
|
+
{
|
|
232
|
+
className: "group/resize relative h-full w-3 shrink-0 cursor-col-resize border-0 bg-transparent p-0",
|
|
233
|
+
onMouseDown: handleResizeStart,
|
|
234
|
+
"aria-label": `Resize ${column.key} column`,
|
|
235
|
+
children: /* @__PURE__ */ jsx(
|
|
236
|
+
"div",
|
|
237
|
+
{
|
|
238
|
+
className: "absolute top-0 right-0 h-full w-0.5 opacity-0 transition-opacity group-hover/resize:opacity-100",
|
|
239
|
+
style: {
|
|
240
|
+
backgroundColor: accentColor || "#1788ff"
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
)
|
|
246
|
+
]
|
|
247
|
+
}
|
|
248
|
+
),
|
|
249
|
+
contextMenu && typeof document !== "undefined" && createPortal(
|
|
250
|
+
/* @__PURE__ */ jsxs(
|
|
251
|
+
"div",
|
|
252
|
+
{
|
|
253
|
+
ref: menuRef,
|
|
254
|
+
className: "text-xxs fixed z-[9999] min-w-40 rounded-md border py-1 shadow-lg backdrop-blur",
|
|
255
|
+
style: {
|
|
256
|
+
left: `${contextMenu.x}px`,
|
|
257
|
+
top: `${contextMenu.y}px`,
|
|
258
|
+
position: "fixed"
|
|
259
|
+
},
|
|
260
|
+
role: "menu",
|
|
261
|
+
children: [
|
|
262
|
+
effectivelySortable && onSort && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
263
|
+
/* @__PURE__ */ jsxs(
|
|
264
|
+
"button",
|
|
265
|
+
{
|
|
266
|
+
className: `cusror-pointer flex w-full items-center gap-2 px-3 py-1.5 text-left ${sortDirection === "asc" ? "font-semibold" : ""}`,
|
|
267
|
+
style: sortDirection === "asc" ? { color: accentColor } : void 0,
|
|
268
|
+
onClick: () => {
|
|
269
|
+
onSort(column.key, "asc");
|
|
270
|
+
setContextMenu(null);
|
|
271
|
+
},
|
|
272
|
+
children: [
|
|
273
|
+
/* @__PURE__ */ jsx(ArrowUpAZ, { className: "h-3 w-3" }),
|
|
274
|
+
"Sort Ascending"
|
|
275
|
+
]
|
|
276
|
+
}
|
|
277
|
+
),
|
|
278
|
+
/* @__PURE__ */ jsxs(
|
|
279
|
+
"button",
|
|
280
|
+
{
|
|
281
|
+
className: `cusror-pointer flex w-full items-center gap-2 px-3 py-1.5 text-left ${sortDirection === "desc" ? "font-semibold" : ""}`,
|
|
282
|
+
style: sortDirection === "desc" ? { color: accentColor } : void 0,
|
|
283
|
+
onClick: () => {
|
|
284
|
+
onSort(column.key, "desc");
|
|
285
|
+
setContextMenu(null);
|
|
286
|
+
},
|
|
287
|
+
children: [
|
|
288
|
+
/* @__PURE__ */ jsx(ArrowDownAZ, { className: "h-3 w-3" }),
|
|
289
|
+
"Sort Descending"
|
|
290
|
+
]
|
|
291
|
+
}
|
|
292
|
+
),
|
|
293
|
+
/* @__PURE__ */ jsx("div", { className: "my-1 border-t dark:border-gray-700" })
|
|
294
|
+
] }),
|
|
295
|
+
effectivelyFilterable && onFilter && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
296
|
+
showFilterInput ? (
|
|
297
|
+
// Inline text input — pressing Enter applies the filter,
|
|
298
|
+
// pressing Escape cancels and returns to the button
|
|
299
|
+
/* @__PURE__ */ jsx("div", { className: "flex items-center gap-1 px-2 py-1.5", children: /* @__PURE__ */ jsx(
|
|
300
|
+
"input",
|
|
301
|
+
{
|
|
302
|
+
ref: filterInputRef,
|
|
303
|
+
type: "text",
|
|
304
|
+
autoFocus: true,
|
|
305
|
+
defaultValue: filterValue,
|
|
306
|
+
placeholder: "Filter...",
|
|
307
|
+
className: "bg-background text-foreground w-full rounded border px-1.5 py-0.5 text-xs outline-none focus:border-blue-400",
|
|
308
|
+
onKeyDown: (e) => {
|
|
309
|
+
if (e.key === "Enter") {
|
|
310
|
+
onFilter(
|
|
311
|
+
column.key,
|
|
312
|
+
e.target.value
|
|
313
|
+
);
|
|
314
|
+
setShowFilterInput(false);
|
|
315
|
+
setContextMenu(null);
|
|
316
|
+
}
|
|
317
|
+
if (e.key === "Escape") {
|
|
318
|
+
setShowFilterInput(false);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
) })
|
|
323
|
+
) : /* @__PURE__ */ jsxs(
|
|
324
|
+
"button",
|
|
325
|
+
{
|
|
326
|
+
className: "cusror-pointer flex w-full items-center gap-2 px-3 py-1.5 text-left",
|
|
327
|
+
onClick: () => {
|
|
328
|
+
setShowFilterInput(true);
|
|
329
|
+
},
|
|
330
|
+
children: [
|
|
331
|
+
/* @__PURE__ */ jsx(Filter, { className: "h-3 w-3" }),
|
|
332
|
+
filterValue ? `Filtered: "${filterValue}"` : "Filter Column"
|
|
333
|
+
]
|
|
334
|
+
}
|
|
335
|
+
),
|
|
336
|
+
filterValue && /* @__PURE__ */ jsxs(
|
|
337
|
+
"button",
|
|
338
|
+
{
|
|
339
|
+
className: "cusror-pointer flex w-full items-center gap-2 px-3 py-1.5 text-left text-red-500",
|
|
340
|
+
onClick: () => {
|
|
341
|
+
onClearFilter?.(column.key);
|
|
342
|
+
setShowFilterInput(false);
|
|
343
|
+
setContextMenu(null);
|
|
344
|
+
},
|
|
345
|
+
children: [
|
|
346
|
+
/* @__PURE__ */ jsx(FilterX, { className: "h-3 w-3" }),
|
|
347
|
+
"Clear Filter"
|
|
348
|
+
]
|
|
349
|
+
}
|
|
350
|
+
),
|
|
351
|
+
/* @__PURE__ */ jsx("div", { className: "my-1 border-t dark:border-gray-700" })
|
|
352
|
+
] }),
|
|
353
|
+
/* @__PURE__ */ jsxs(
|
|
354
|
+
"button",
|
|
355
|
+
{
|
|
356
|
+
className: "cusror-pointer flex w-full items-center gap-2 px-3 py-1.5 text-left",
|
|
357
|
+
onClick: () => {
|
|
358
|
+
onTogglePin?.(
|
|
359
|
+
column.key,
|
|
360
|
+
column.pinned === "left" ? false : "left"
|
|
361
|
+
);
|
|
362
|
+
setContextMenu(null);
|
|
363
|
+
},
|
|
364
|
+
children: [
|
|
365
|
+
column.pinned === "left" ? /* @__PURE__ */ jsx(PinOff, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx(Pin, { className: "h-3 w-3" }),
|
|
366
|
+
column.pinned === "left" ? "Unpin Left" : "Pin Left"
|
|
367
|
+
]
|
|
368
|
+
}
|
|
369
|
+
),
|
|
370
|
+
/* @__PURE__ */ jsxs(
|
|
371
|
+
"button",
|
|
372
|
+
{
|
|
373
|
+
className: "cusror-pointer flex w-full items-center gap-2 px-3 py-1.5 text-left",
|
|
374
|
+
onClick: () => {
|
|
375
|
+
onTogglePin?.(
|
|
376
|
+
column.key,
|
|
377
|
+
column.pinned === "right" ? false : "right"
|
|
378
|
+
);
|
|
379
|
+
setContextMenu(null);
|
|
380
|
+
},
|
|
381
|
+
children: [
|
|
382
|
+
column.pinned === "right" ? /* @__PURE__ */ jsx(PinOff, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx(Pin, { className: "h-3 w-3" }),
|
|
383
|
+
column.pinned === "right" ? "Unpin Right" : "Pin Right"
|
|
384
|
+
]
|
|
385
|
+
}
|
|
386
|
+
),
|
|
387
|
+
!isPinned && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
388
|
+
/* @__PURE__ */ jsx("div", { className: "my-1 border-t dark:border-gray-700" }),
|
|
389
|
+
/* @__PURE__ */ jsxs(
|
|
390
|
+
"button",
|
|
391
|
+
{
|
|
392
|
+
className: "cusror-pointer flex w-full items-center gap-2 px-3 py-1.5 text-left",
|
|
393
|
+
onClick: () => {
|
|
394
|
+
onToggleHide?.(column.key);
|
|
395
|
+
setContextMenu(null);
|
|
396
|
+
},
|
|
397
|
+
children: [
|
|
398
|
+
/* @__PURE__ */ jsx(EyeOff, { className: "h-3 w-3" }),
|
|
399
|
+
"Hide Column"
|
|
400
|
+
]
|
|
401
|
+
}
|
|
402
|
+
)
|
|
403
|
+
] }),
|
|
404
|
+
customContextMenuItems && customContextMenuItems.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
405
|
+
/* @__PURE__ */ jsx("div", { className: "my-1 border-t dark:border-gray-700" }),
|
|
406
|
+
customContextMenuItems.map((item) => /* @__PURE__ */ jsxs(
|
|
407
|
+
"button",
|
|
408
|
+
{
|
|
409
|
+
disabled: item.disabled,
|
|
410
|
+
className: `flex w-full items-center gap-2 px-3 py-1.5 text-left ${item.disabled ? "cursor-not-allowed opacity-50" : "cusror-pointer"} ${item.danger ? "text-red-500" : ""}`,
|
|
411
|
+
onClick: () => {
|
|
412
|
+
item.onClick(column.key);
|
|
413
|
+
setContextMenu(null);
|
|
414
|
+
},
|
|
415
|
+
children: [
|
|
416
|
+
item.icon && /* @__PURE__ */ jsx("span", { className: "flex h-3 w-3 items-center justify-center", children: item.icon }),
|
|
417
|
+
item.label
|
|
418
|
+
]
|
|
419
|
+
},
|
|
420
|
+
item.key
|
|
421
|
+
))
|
|
422
|
+
] })
|
|
423
|
+
]
|
|
424
|
+
}
|
|
425
|
+
),
|
|
426
|
+
document.body
|
|
427
|
+
)
|
|
428
|
+
] });
|
|
429
|
+
},
|
|
430
|
+
// ── Custom memo comparator ─────────────────────────────────────────────────
|
|
431
|
+
// Only re-render when props that actually affect this header's output change.
|
|
432
|
+
// This prevents a sort/filter change on one column from re-rendering all others.
|
|
433
|
+
(prevProps, nextProps) => {
|
|
434
|
+
return prevProps.column.width === nextProps.column.width && prevProps.column.key === nextProps.column.key && prevProps.column.pinned === nextProps.column.pinned && prevProps.column.sortable === nextProps.column.sortable && prevProps.column.filterable === nextProps.column.filterable && prevProps.column.sorter === nextProps.column.sorter && prevProps.column.filterFn === nextProps.column.filterFn && prevProps.visualIndex === nextProps.visualIndex && prevProps.stickyOffset === nextProps.stickyOffset && prevProps.isLastColumn === nextProps.isLastColumn && prevProps.sortDirection === nextProps.sortDirection && prevProps.filterValue === nextProps.filterValue;
|
|
435
|
+
}
|
|
436
|
+
);
|
|
437
|
+
DraggableHeader.displayName = "DraggableHeader";
|
|
438
|
+
var DraggableHeader_default = DraggableHeader;
|
|
439
|
+
|
|
440
|
+
// src/ResizeOverlay.tsx
|
|
441
|
+
import { forwardRef, useImperativeHandle, useRef as useRef2 } from "react";
|
|
442
|
+
import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
443
|
+
var ResizeOverlay = forwardRef(
|
|
444
|
+
({ accentColor = "#1778ff" }, ref) => {
|
|
445
|
+
const lineRef = useRef2(null);
|
|
446
|
+
const labelRef = useRef2(null);
|
|
447
|
+
const stateRef = useRef2({
|
|
448
|
+
headerLeftLocal: 0,
|
|
449
|
+
minSize: 40,
|
|
450
|
+
areaLeft: 0,
|
|
451
|
+
areaWidth: 0,
|
|
452
|
+
labelWidth: 80,
|
|
453
|
+
scrollLeft: 0
|
|
454
|
+
});
|
|
455
|
+
useImperativeHandle(
|
|
456
|
+
ref,
|
|
457
|
+
() => ({
|
|
458
|
+
show(_viewportX, columnName, areaRect, headerLeftLocal, minSize, scrollTop, scrollLeft, initialLineX) {
|
|
459
|
+
const line = lineRef.current;
|
|
460
|
+
const label = labelRef.current;
|
|
461
|
+
if (!line || !label) return;
|
|
462
|
+
line.style.top = `${scrollTop}px`;
|
|
463
|
+
line.style.height = `${areaRect.height}px`;
|
|
464
|
+
line.style.left = `${initialLineX - 2.5}px`;
|
|
465
|
+
line.style.display = "block";
|
|
466
|
+
label.textContent = `${columnName}`;
|
|
467
|
+
label.style.top = `${scrollTop + 8}px`;
|
|
468
|
+
label.style.left = `${initialLineX + 6}px`;
|
|
469
|
+
label.style.display = "block";
|
|
470
|
+
const labelWidth = label.offsetWidth ?? 80;
|
|
471
|
+
stateRef.current = {
|
|
472
|
+
headerLeftLocal,
|
|
473
|
+
minSize,
|
|
474
|
+
areaLeft: areaRect.left,
|
|
475
|
+
areaWidth: areaRect.width,
|
|
476
|
+
labelWidth,
|
|
477
|
+
scrollLeft
|
|
478
|
+
};
|
|
479
|
+
},
|
|
480
|
+
move(viewportX) {
|
|
481
|
+
const line = lineRef.current;
|
|
482
|
+
const label = labelRef.current;
|
|
483
|
+
if (!line || !label) return;
|
|
484
|
+
const {
|
|
485
|
+
headerLeftLocal,
|
|
486
|
+
minSize,
|
|
487
|
+
areaLeft,
|
|
488
|
+
areaWidth,
|
|
489
|
+
labelWidth,
|
|
490
|
+
scrollLeft
|
|
491
|
+
} = stateRef.current;
|
|
492
|
+
const localX = viewportX - areaLeft + scrollLeft;
|
|
493
|
+
const clampedLocalX = Math.max(localX, headerLeftLocal + minSize);
|
|
494
|
+
line.style.left = `${clampedLocalX}px`;
|
|
495
|
+
const currentText = label.textContent || "";
|
|
496
|
+
const colonIndex = currentText.indexOf(":");
|
|
497
|
+
if (colonIndex !== -1) {
|
|
498
|
+
label.textContent = `${currentText.substring(0, colonIndex)}`;
|
|
499
|
+
}
|
|
500
|
+
const labelViewportX = clampedLocalX - scrollLeft;
|
|
501
|
+
const FLIP_MARGIN = 20;
|
|
502
|
+
if (labelViewportX + labelWidth + FLIP_MARGIN > areaWidth) {
|
|
503
|
+
label.style.left = `${clampedLocalX - labelWidth - 10}px`;
|
|
504
|
+
} else {
|
|
505
|
+
label.style.left = `${clampedLocalX + 6}px`;
|
|
506
|
+
}
|
|
507
|
+
},
|
|
508
|
+
hide() {
|
|
509
|
+
const line = lineRef.current;
|
|
510
|
+
const label = labelRef.current;
|
|
511
|
+
if (line) line.style.display = "none";
|
|
512
|
+
if (label) label.style.display = "none";
|
|
513
|
+
}
|
|
514
|
+
}),
|
|
515
|
+
[]
|
|
516
|
+
);
|
|
517
|
+
const hexToRgba = (hex, opacity) => {
|
|
518
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
519
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
520
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
521
|
+
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
|
522
|
+
};
|
|
523
|
+
return /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
524
|
+
/* @__PURE__ */ jsx2(
|
|
525
|
+
"div",
|
|
526
|
+
{
|
|
527
|
+
ref: lineRef,
|
|
528
|
+
"aria-hidden": "true",
|
|
529
|
+
style: {
|
|
530
|
+
display: "none",
|
|
531
|
+
position: "absolute",
|
|
532
|
+
top: 0,
|
|
533
|
+
height: "0px",
|
|
534
|
+
left: 0,
|
|
535
|
+
width: "2px",
|
|
536
|
+
zIndex: 30,
|
|
537
|
+
pointerEvents: "none",
|
|
538
|
+
backgroundColor: accentColor,
|
|
539
|
+
boxShadow: `0 0 4px ${hexToRgba(accentColor, 0.5)}`,
|
|
540
|
+
willChange: "left"
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
),
|
|
544
|
+
/* @__PURE__ */ jsx2(
|
|
545
|
+
"div",
|
|
546
|
+
{
|
|
547
|
+
ref: labelRef,
|
|
548
|
+
"aria-hidden": "true",
|
|
549
|
+
style: {
|
|
550
|
+
display: "none",
|
|
551
|
+
position: "absolute",
|
|
552
|
+
top: "8px",
|
|
553
|
+
left: 0,
|
|
554
|
+
zIndex: 31,
|
|
555
|
+
pointerEvents: "none",
|
|
556
|
+
backgroundColor: accentColor,
|
|
557
|
+
color: "white",
|
|
558
|
+
fontSize: "11px",
|
|
559
|
+
fontWeight: 600,
|
|
560
|
+
lineHeight: 1,
|
|
561
|
+
padding: "4px 8px",
|
|
562
|
+
borderRadius: "5px",
|
|
563
|
+
whiteSpace: "nowrap",
|
|
564
|
+
boxShadow: "0 2px 10px rgba(0,0,0,0.2)",
|
|
565
|
+
userSelect: "none",
|
|
566
|
+
willChange: "left"
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
)
|
|
570
|
+
] });
|
|
571
|
+
}
|
|
572
|
+
);
|
|
573
|
+
ResizeOverlay.displayName = "ResizeOverlay";
|
|
574
|
+
var ResizeOverlay_default = ResizeOverlay;
|
|
575
|
+
|
|
576
|
+
// src/TableBody.tsx
|
|
577
|
+
import React3, { useEffect as useEffect2, useMemo, useRef as useRef3 } from "react";
|
|
578
|
+
import { Fragment as Fragment3, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
579
|
+
var SHIMMER_WIDTHS = [55, 70, 45, 80, 60, 50, 75, 65];
|
|
580
|
+
var Cell = React3.memo(
|
|
581
|
+
({
|
|
582
|
+
value,
|
|
583
|
+
record,
|
|
584
|
+
column,
|
|
585
|
+
rowIndex,
|
|
586
|
+
classNames,
|
|
587
|
+
styles,
|
|
588
|
+
isSelected,
|
|
589
|
+
rowSelection,
|
|
590
|
+
rowKey,
|
|
591
|
+
allData,
|
|
592
|
+
getRowKey,
|
|
593
|
+
accentColor,
|
|
594
|
+
isLoading
|
|
595
|
+
}) => {
|
|
596
|
+
const justifyClass = column.key === "__select__" || column.key === "__expand__" ? "justify-center" : "";
|
|
597
|
+
const isPinned = Boolean(column.pinned);
|
|
598
|
+
if (isLoading && column.key !== "__select__" && column.key !== "__expand__") {
|
|
599
|
+
const shimmerContent = column.shimmerRender ? column.shimmerRender() : /* @__PURE__ */ jsx3(
|
|
600
|
+
"div",
|
|
601
|
+
{
|
|
602
|
+
className: "bg-muted-foreground/15 animate-pulse rounded",
|
|
603
|
+
style: {
|
|
604
|
+
// Vary widths across cells so skeletons look more natural
|
|
605
|
+
width: `${SHIMMER_WIDTHS[(rowIndex + column.key.length) % SHIMMER_WIDTHS.length]}%`,
|
|
606
|
+
height: 14
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
);
|
|
610
|
+
return /* @__PURE__ */ jsx3(
|
|
611
|
+
"div",
|
|
612
|
+
{
|
|
613
|
+
className: `flex items-center overflow-hidden border-b px-2 ${column.className ?? ""} ${classNames?.cell ?? ""} ${isPinned ? classNames?.pinnedCell ?? "" : ""}`,
|
|
614
|
+
style: {
|
|
615
|
+
height: "100%",
|
|
616
|
+
...column.style,
|
|
617
|
+
...isPinned ? styles?.pinnedCell : void 0
|
|
618
|
+
},
|
|
619
|
+
children: shimmerContent
|
|
620
|
+
}
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
if (column.key === "__select__" && rowSelection && rowKey !== void 0) {
|
|
624
|
+
const checkboxProps = rowSelection.getCheckboxProps?.(record) ?? {
|
|
625
|
+
disabled: false
|
|
626
|
+
};
|
|
627
|
+
const content2 = rowSelection.type === "radio" ? /* @__PURE__ */ jsx3(
|
|
628
|
+
"input",
|
|
629
|
+
{
|
|
630
|
+
type: "radio",
|
|
631
|
+
checked: !!isSelected,
|
|
632
|
+
disabled: checkboxProps.disabled,
|
|
633
|
+
onChange: (e) => {
|
|
634
|
+
e.stopPropagation();
|
|
635
|
+
rowSelection.onSelect?.(record, true, [record], e.nativeEvent);
|
|
636
|
+
rowSelection.onChange?.([rowKey], [record], { type: "single" });
|
|
637
|
+
},
|
|
638
|
+
className: "cursor-pointer",
|
|
639
|
+
style: { accentColor }
|
|
640
|
+
}
|
|
641
|
+
) : /* @__PURE__ */ jsx3(
|
|
642
|
+
"input",
|
|
643
|
+
{
|
|
644
|
+
type: "checkbox",
|
|
645
|
+
checked: !!isSelected,
|
|
646
|
+
disabled: checkboxProps.disabled,
|
|
647
|
+
onChange: (e) => {
|
|
648
|
+
e.stopPropagation();
|
|
649
|
+
const currentKeys = (rowSelection.selectedRowKeys ?? []).map(
|
|
650
|
+
(k) => String(k)
|
|
651
|
+
);
|
|
652
|
+
const newSelected = isSelected ? currentKeys.filter((k) => k !== rowKey) : [...currentKeys, rowKey];
|
|
653
|
+
const newSelectedRows = (allData ?? []).filter(
|
|
654
|
+
(row, idx) => newSelected.includes(
|
|
655
|
+
getRowKey ? getRowKey(row, idx) : String(idx)
|
|
656
|
+
)
|
|
657
|
+
);
|
|
658
|
+
rowSelection.onSelect?.(
|
|
659
|
+
record,
|
|
660
|
+
!isSelected,
|
|
661
|
+
newSelectedRows,
|
|
662
|
+
e.nativeEvent
|
|
663
|
+
);
|
|
664
|
+
rowSelection.onChange?.(newSelected, newSelectedRows, {
|
|
665
|
+
type: "multiple"
|
|
666
|
+
});
|
|
667
|
+
},
|
|
668
|
+
className: "cursor-pointer",
|
|
669
|
+
style: { accentColor }
|
|
670
|
+
}
|
|
671
|
+
);
|
|
672
|
+
return /* @__PURE__ */ jsx3(
|
|
673
|
+
"div",
|
|
674
|
+
{
|
|
675
|
+
className: `flex items-center overflow-hidden border-b px-2 ${justifyClass} ${column.className ?? ""} ${classNames?.cell ?? ""} `,
|
|
676
|
+
style: {
|
|
677
|
+
height: "100%",
|
|
678
|
+
...column.style,
|
|
679
|
+
...isPinned ? styles?.pinnedCell : void 0
|
|
680
|
+
},
|
|
681
|
+
children: content2
|
|
682
|
+
}
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
const content = column.render ? column.render(value, record, rowIndex) : value ?? "";
|
|
686
|
+
return /* @__PURE__ */ jsx3(
|
|
687
|
+
"div",
|
|
688
|
+
{
|
|
689
|
+
className: `flex items-center truncate overflow-hidden border-b px-2 ${justifyClass} ${column.className ?? ""} ${classNames?.cell ?? ""} `,
|
|
690
|
+
style: {
|
|
691
|
+
height: "100%",
|
|
692
|
+
...column.style,
|
|
693
|
+
...isPinned ? styles?.pinnedCell : void 0
|
|
694
|
+
},
|
|
695
|
+
children: content
|
|
696
|
+
}
|
|
697
|
+
);
|
|
698
|
+
},
|
|
699
|
+
// ── Custom memo comparator ─────────────────────────────────────────────────
|
|
700
|
+
// Minimizes re-renders:
|
|
701
|
+
// - __select__ cells: re-render only when selection changes
|
|
702
|
+
// - __expand__ cells: re-render only when expand state changes
|
|
703
|
+
// - Normal cells: re-render only when value or rowIndex changes
|
|
704
|
+
(prev, next) => {
|
|
705
|
+
if (prev.isLoading !== next.isLoading) return false;
|
|
706
|
+
if (prev.column.key === "__select__") {
|
|
707
|
+
return prev.isSelected === next.isSelected && prev.normalizedSelectedKeys === next.normalizedSelectedKeys;
|
|
708
|
+
}
|
|
709
|
+
if (prev.column.key === "__expand__") {
|
|
710
|
+
return prev.isExpanded === next.isExpanded;
|
|
711
|
+
}
|
|
712
|
+
return prev.value === next.value && prev.rowIndex === next.rowIndex && prev.column.key === next.column.key;
|
|
713
|
+
}
|
|
714
|
+
);
|
|
715
|
+
Cell.displayName = "Cell";
|
|
716
|
+
var MeasuredExpandedRow = React3.memo(
|
|
717
|
+
({
|
|
718
|
+
rowKey,
|
|
719
|
+
onResize,
|
|
720
|
+
children
|
|
721
|
+
}) => {
|
|
722
|
+
const ref = useRef3(null);
|
|
723
|
+
const onResizeRef = useRef3(onResize);
|
|
724
|
+
useEffect2(() => {
|
|
725
|
+
onResizeRef.current = onResize;
|
|
726
|
+
}, [onResize]);
|
|
727
|
+
useEffect2(() => {
|
|
728
|
+
const el = ref.current;
|
|
729
|
+
if (!el) return;
|
|
730
|
+
const observer = new ResizeObserver((entries) => {
|
|
731
|
+
const height = entries[0]?.borderBoxSize?.[0]?.blockSize;
|
|
732
|
+
if (height != null && height > 0) {
|
|
733
|
+
onResizeRef.current(rowKey, height);
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
observer.observe(el);
|
|
737
|
+
return () => observer.disconnect();
|
|
738
|
+
}, [rowKey]);
|
|
739
|
+
return /* @__PURE__ */ jsx3("div", { ref, children });
|
|
740
|
+
}
|
|
741
|
+
);
|
|
742
|
+
MeasuredExpandedRow.displayName = "MeasuredExpandedRow";
|
|
743
|
+
var TableBody = ({
|
|
744
|
+
data,
|
|
745
|
+
orderedColumns,
|
|
746
|
+
rowVirtualizer,
|
|
747
|
+
columnOffsets,
|
|
748
|
+
styles,
|
|
749
|
+
classNames,
|
|
750
|
+
rowSelection,
|
|
751
|
+
normalizedSelectedKeys = [],
|
|
752
|
+
getRowKey,
|
|
753
|
+
expandable,
|
|
754
|
+
resolvedExpandedKeys,
|
|
755
|
+
rowHeight = 40,
|
|
756
|
+
scrollAreaWidth,
|
|
757
|
+
accentColor,
|
|
758
|
+
isLoading = false,
|
|
759
|
+
onExpandedRowResize,
|
|
760
|
+
maxExpandedRowHeight
|
|
761
|
+
}) => {
|
|
762
|
+
const virtualItems = rowVirtualizer.getVirtualItems();
|
|
763
|
+
const totalSize = rowVirtualizer.getTotalSize();
|
|
764
|
+
const theme = typeof document !== "undefined" && document.documentElement.classList.contains("dark") ? "dark" : "light";
|
|
765
|
+
const columnStyles = useMemo(() => {
|
|
766
|
+
return orderedColumns.map((col, colIndex) => {
|
|
767
|
+
const stickyOffset = columnOffsets.get(col.key);
|
|
768
|
+
const isPinned = Boolean(col.pinned);
|
|
769
|
+
let zIndex = 0;
|
|
770
|
+
if (col.key === "__select__" || col.key === "__expand__") zIndex = 11;
|
|
771
|
+
else if (isPinned) zIndex = 2;
|
|
772
|
+
const style = {
|
|
773
|
+
gridColumn: colIndex + 1,
|
|
774
|
+
gridRow: 2,
|
|
775
|
+
height: `${totalSize}px`,
|
|
776
|
+
position: isPinned ? "sticky" : "relative",
|
|
777
|
+
zIndex
|
|
778
|
+
};
|
|
779
|
+
if (col.pinned === "left" && stickyOffset !== void 0)
|
|
780
|
+
style.left = `${stickyOffset}px`;
|
|
781
|
+
else if (col.pinned === "right" && stickyOffset !== void 0)
|
|
782
|
+
style.right = `${stickyOffset}px`;
|
|
783
|
+
if (isPinned) {
|
|
784
|
+
style.backdropFilter = "blur(14px)";
|
|
785
|
+
style.backgroundColor = styles?.pinnedBg ?? theme === "dark" ? "#10182890" : "#f9fafb90";
|
|
786
|
+
if (styles?.pinnedCell) Object.assign(style, styles.pinnedCell);
|
|
787
|
+
}
|
|
788
|
+
return { key: col.key, style, isPinned };
|
|
789
|
+
});
|
|
790
|
+
}, [orderedColumns, columnOffsets, totalSize, styles]);
|
|
791
|
+
return /* @__PURE__ */ jsxs3(Fragment3, { children: [
|
|
792
|
+
columnStyles.map((colStyle, colIndex) => {
|
|
793
|
+
const col = orderedColumns[colIndex];
|
|
794
|
+
return /* @__PURE__ */ jsx3(
|
|
795
|
+
"div",
|
|
796
|
+
{
|
|
797
|
+
className: "truncate",
|
|
798
|
+
style: colStyle.style,
|
|
799
|
+
children: virtualItems.map((virtualRow) => {
|
|
800
|
+
const row = data[virtualRow.index];
|
|
801
|
+
const rowKey = getRowKey ? getRowKey(row, virtualRow.index) : String(virtualRow.index);
|
|
802
|
+
const isSelected = normalizedSelectedKeys.includes(rowKey);
|
|
803
|
+
const isExpanded = resolvedExpandedKeys?.has(rowKey) ?? false;
|
|
804
|
+
const cellValue = row[col.dataIndex];
|
|
805
|
+
const isRowShimmer = isLoading || rowKey.startsWith("__shimmer_");
|
|
806
|
+
return (
|
|
807
|
+
/*
|
|
808
|
+
* Row wrapper div:
|
|
809
|
+
* - data-row-key: used by BoltTable's DOM-based hover system
|
|
810
|
+
* (mouseover reads this attribute to apply hover styles
|
|
811
|
+
* across all column divs for the same row simultaneously)
|
|
812
|
+
* - data-selected: presence/absence attribute consumed by the
|
|
813
|
+
* CSS injected by BoltTable for selected row background
|
|
814
|
+
* - Absolute positioned at virtualRow.start for virtualization
|
|
815
|
+
* - Height = virtualRow.size (includes expanded row height)
|
|
816
|
+
*/
|
|
817
|
+
/* @__PURE__ */ jsx3(
|
|
818
|
+
"div",
|
|
819
|
+
{
|
|
820
|
+
"data-row-key": rowKey,
|
|
821
|
+
"data-selected": isSelected || void 0,
|
|
822
|
+
style: {
|
|
823
|
+
position: "absolute",
|
|
824
|
+
top: `${virtualRow.start}px`,
|
|
825
|
+
left: 0,
|
|
826
|
+
right: 0,
|
|
827
|
+
height: `${virtualRow.size}px`
|
|
828
|
+
},
|
|
829
|
+
className: "truncate",
|
|
830
|
+
children: /* @__PURE__ */ jsx3(
|
|
831
|
+
"div",
|
|
832
|
+
{
|
|
833
|
+
style: { height: `${rowHeight}px`, position: "relative" },
|
|
834
|
+
className: "truncate",
|
|
835
|
+
children: /* @__PURE__ */ jsx3(
|
|
836
|
+
Cell,
|
|
837
|
+
{
|
|
838
|
+
value: cellValue,
|
|
839
|
+
record: row,
|
|
840
|
+
column: col,
|
|
841
|
+
rowIndex: virtualRow.index,
|
|
842
|
+
classNames,
|
|
843
|
+
styles,
|
|
844
|
+
isSelected,
|
|
845
|
+
isExpanded,
|
|
846
|
+
rowSelection,
|
|
847
|
+
normalizedSelectedKeys,
|
|
848
|
+
rowKey,
|
|
849
|
+
allData: data,
|
|
850
|
+
getRowKey,
|
|
851
|
+
accentColor,
|
|
852
|
+
isLoading: isRowShimmer
|
|
853
|
+
}
|
|
854
|
+
)
|
|
855
|
+
}
|
|
856
|
+
)
|
|
857
|
+
},
|
|
858
|
+
`${rowKey}-${col.key}`
|
|
859
|
+
)
|
|
860
|
+
);
|
|
861
|
+
})
|
|
862
|
+
},
|
|
863
|
+
`spacer-${colStyle.key}`
|
|
864
|
+
);
|
|
865
|
+
}),
|
|
866
|
+
expandable && /* @__PURE__ */ jsx3(
|
|
867
|
+
"div",
|
|
868
|
+
{
|
|
869
|
+
style: {
|
|
870
|
+
gridColumn: "1 / -1",
|
|
871
|
+
gridRow: 2,
|
|
872
|
+
height: `${totalSize}px`,
|
|
873
|
+
position: "relative",
|
|
874
|
+
zIndex: 15,
|
|
875
|
+
// pointerEvents: none on the overlay so hover/click pass through
|
|
876
|
+
// to the cells below for rows that are NOT expanded
|
|
877
|
+
pointerEvents: "none"
|
|
878
|
+
},
|
|
879
|
+
children: virtualItems.map((virtualRow) => {
|
|
880
|
+
const row = data[virtualRow.index];
|
|
881
|
+
const rk = getRowKey ? getRowKey(row, virtualRow.index) : String(virtualRow.index);
|
|
882
|
+
if (!(resolvedExpandedKeys?.has(rk) ?? false)) return null;
|
|
883
|
+
const expandedContent = /* @__PURE__ */ jsx3(
|
|
884
|
+
"div",
|
|
885
|
+
{
|
|
886
|
+
className: `${classNames?.expandedRow ?? ""}`,
|
|
887
|
+
style: {
|
|
888
|
+
// Sticky left:0 + fixed width = viewport-locked panel
|
|
889
|
+
// regardless of how far the user has scrolled horizontally
|
|
890
|
+
position: "sticky",
|
|
891
|
+
left: 0,
|
|
892
|
+
zIndex: 5,
|
|
893
|
+
width: scrollAreaWidth && scrollAreaWidth > 0 ? `${scrollAreaWidth}px` : "100%",
|
|
894
|
+
overflow: "auto",
|
|
895
|
+
// Restore pointer events so the expanded content is interactive
|
|
896
|
+
pointerEvents: "auto",
|
|
897
|
+
borderBottom: "1px solid hsl(var(--border))",
|
|
898
|
+
backgroundColor: "hsl(var(--muted)/0.4)",
|
|
899
|
+
padding: 20,
|
|
900
|
+
// Optional max height — makes the panel scrollable for tall content
|
|
901
|
+
...maxExpandedRowHeight ? { maxHeight: `${maxExpandedRowHeight}px` } : void 0,
|
|
902
|
+
...styles?.expandedRow
|
|
903
|
+
},
|
|
904
|
+
children: expandable.expandedRowRender(row, virtualRow.index, 0, true)
|
|
905
|
+
}
|
|
906
|
+
);
|
|
907
|
+
return /* @__PURE__ */ jsx3(
|
|
908
|
+
"div",
|
|
909
|
+
{
|
|
910
|
+
style: {
|
|
911
|
+
position: "absolute",
|
|
912
|
+
// Position immediately below the row's base height
|
|
913
|
+
top: virtualRow.start + rowHeight,
|
|
914
|
+
left: 0,
|
|
915
|
+
right: 0
|
|
916
|
+
},
|
|
917
|
+
children: onExpandedRowResize ? /* @__PURE__ */ jsx3(
|
|
918
|
+
MeasuredExpandedRow,
|
|
919
|
+
{
|
|
920
|
+
rowKey: rk,
|
|
921
|
+
onResize: onExpandedRowResize,
|
|
922
|
+
children: expandedContent
|
|
923
|
+
}
|
|
924
|
+
) : expandedContent
|
|
925
|
+
},
|
|
926
|
+
`expanded-${rk}`
|
|
927
|
+
);
|
|
928
|
+
})
|
|
929
|
+
}
|
|
930
|
+
)
|
|
931
|
+
] });
|
|
932
|
+
};
|
|
933
|
+
TableBody.displayName = "TableBody";
|
|
934
|
+
var TableBody_default = TableBody;
|
|
935
|
+
|
|
936
|
+
// src/BoltTable.tsx
|
|
937
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
938
|
+
var SHIMMER_WIDTHS2 = [55, 70, 45, 80, 60, 50, 75, 65, 40, 72];
|
|
939
|
+
function BoltTable({
|
|
940
|
+
columns: initialColumns,
|
|
941
|
+
data,
|
|
942
|
+
rowHeight = 40,
|
|
943
|
+
expandedRowHeight = 200,
|
|
944
|
+
maxExpandedRowHeight,
|
|
945
|
+
accentColor = "#1890ff",
|
|
946
|
+
className = "",
|
|
947
|
+
classNames = {},
|
|
948
|
+
styles = {},
|
|
949
|
+
gripIcon,
|
|
950
|
+
hideGripIcon,
|
|
951
|
+
pagination,
|
|
952
|
+
onPaginationChange,
|
|
953
|
+
onColumnResize,
|
|
954
|
+
onColumnOrderChange,
|
|
955
|
+
onColumnPin,
|
|
956
|
+
onColumnHide,
|
|
957
|
+
rowSelection,
|
|
958
|
+
expandable,
|
|
959
|
+
rowKey = "id",
|
|
960
|
+
onEndReached,
|
|
961
|
+
onEndReachedThreshold = 5,
|
|
962
|
+
isLoading = false,
|
|
963
|
+
onSortChange,
|
|
964
|
+
onFilterChange,
|
|
965
|
+
columnContextMenuItems,
|
|
966
|
+
autoHeight = true,
|
|
967
|
+
layoutLoading,
|
|
968
|
+
emptyRenderer
|
|
969
|
+
}) {
|
|
970
|
+
const [columns, setColumns] = useState2(initialColumns);
|
|
971
|
+
const [columnOrder, setColumnOrder] = useState2(
|
|
972
|
+
() => initialColumns.map((c) => c.key)
|
|
973
|
+
);
|
|
974
|
+
const [activeId, setActiveId] = useState2(null);
|
|
975
|
+
const columnsFingerprintRef = useRef4("");
|
|
976
|
+
const newFingerprint = initialColumns.map((c) => {
|
|
977
|
+
const w = typeof c.width === "number" ? Math.round(c.width) : c.width ?? "";
|
|
978
|
+
return `${c.key}:${!!c.hidden}:${c.pinned || ""}:${w}`;
|
|
979
|
+
}).join("|");
|
|
980
|
+
const initialColumnsRef = useRef4(initialColumns);
|
|
981
|
+
initialColumnsRef.current = initialColumns;
|
|
982
|
+
React4.useEffect(() => {
|
|
983
|
+
if (columnsFingerprintRef.current === newFingerprint) return;
|
|
984
|
+
columnsFingerprintRef.current = newFingerprint;
|
|
985
|
+
setColumns(initialColumnsRef.current);
|
|
986
|
+
setColumnOrder(initialColumnsRef.current.map((c) => c.key));
|
|
987
|
+
}, [newFingerprint]);
|
|
988
|
+
const safeWidth = (w, fallback = 150) => typeof w === "number" && Number.isFinite(w) ? w : fallback;
|
|
989
|
+
const [columnWidths, setColumnWidths] = useState2(
|
|
990
|
+
() => /* @__PURE__ */ new Map()
|
|
991
|
+
);
|
|
992
|
+
const manuallyResizedRef = useRef4(/* @__PURE__ */ new Set());
|
|
993
|
+
const columnsWithPersistedWidths = useMemo2(
|
|
994
|
+
() => columns.map((col) => ({
|
|
995
|
+
...col,
|
|
996
|
+
width: safeWidth(columnWidths.get(col.key) ?? col.width)
|
|
997
|
+
})),
|
|
998
|
+
[columns, columnWidths]
|
|
999
|
+
);
|
|
1000
|
+
const [internalExpandedKeys, setInternalExpandedKeys] = useState2(() => {
|
|
1001
|
+
if (expandable?.defaultExpandAllRows) {
|
|
1002
|
+
return new Set(
|
|
1003
|
+
data.map((row, idx) => {
|
|
1004
|
+
if (typeof rowKey === "function") return rowKey(row);
|
|
1005
|
+
if (typeof rowKey === "string")
|
|
1006
|
+
return row[rowKey] ?? idx;
|
|
1007
|
+
return idx;
|
|
1008
|
+
})
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
return new Set(expandable?.defaultExpandedRowKeys ?? []);
|
|
1012
|
+
});
|
|
1013
|
+
const expandedKeysFingerprint = expandable?.expandedRowKeys?.map(String).join("|");
|
|
1014
|
+
const resolvedExpandedKeys = useMemo2(() => {
|
|
1015
|
+
if (expandable?.expandedRowKeys !== void 0)
|
|
1016
|
+
return new Set(expandable.expandedRowKeys);
|
|
1017
|
+
return internalExpandedKeys;
|
|
1018
|
+
}, [expandedKeysFingerprint, internalExpandedKeys]);
|
|
1019
|
+
const expandableRef = useRef4(expandable);
|
|
1020
|
+
expandableRef.current = expandable;
|
|
1021
|
+
const toggleExpand = useCallback((key) => {
|
|
1022
|
+
const exp = expandableRef.current;
|
|
1023
|
+
if (exp?.expandedRowKeys !== void 0) {
|
|
1024
|
+
const next = new Set(exp.expandedRowKeys);
|
|
1025
|
+
next.has(key) ? next.delete(key) : next.add(key);
|
|
1026
|
+
exp.onExpandedRowsChange?.(Array.from(next));
|
|
1027
|
+
} else {
|
|
1028
|
+
setInternalExpandedKeys((prev) => {
|
|
1029
|
+
const next = new Set(prev);
|
|
1030
|
+
next.has(key) ? next.delete(key) : next.add(key);
|
|
1031
|
+
exp?.onExpandedRowsChange?.(Array.from(next));
|
|
1032
|
+
return next;
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
}, []);
|
|
1036
|
+
const getRowKey = useCallback(
|
|
1037
|
+
(record, index) => {
|
|
1038
|
+
if (typeof rowKey === "function") return String(rowKey(record));
|
|
1039
|
+
if (typeof rowKey === "string") {
|
|
1040
|
+
const val = record[rowKey];
|
|
1041
|
+
return val !== void 0 && val !== null ? String(val) : String(index);
|
|
1042
|
+
}
|
|
1043
|
+
return String(index);
|
|
1044
|
+
},
|
|
1045
|
+
[rowKey]
|
|
1046
|
+
);
|
|
1047
|
+
const normalizedSelectedKeys = useMemo2(
|
|
1048
|
+
() => (rowSelection?.selectedRowKeys ?? []).map((k) => String(k)),
|
|
1049
|
+
[rowSelection?.selectedRowKeys]
|
|
1050
|
+
);
|
|
1051
|
+
const columnsWithExpand = useMemo2(() => {
|
|
1052
|
+
if (!expandable?.rowExpandable) return columnsWithPersistedWidths;
|
|
1053
|
+
const expandColumn = {
|
|
1054
|
+
key: "__expand__",
|
|
1055
|
+
dataIndex: "__expand__",
|
|
1056
|
+
title: "",
|
|
1057
|
+
width: 40,
|
|
1058
|
+
pinned: "left",
|
|
1059
|
+
hidden: false,
|
|
1060
|
+
render: (_, record, index) => {
|
|
1061
|
+
const key = getRowKey(record, index);
|
|
1062
|
+
const canExpand = expandable.rowExpandable?.(record) ?? true;
|
|
1063
|
+
const isExpanded = resolvedExpandedKeys.has(key);
|
|
1064
|
+
if (!canExpand)
|
|
1065
|
+
return /* @__PURE__ */ jsx4("span", { style: { display: "inline-block", width: 16 } });
|
|
1066
|
+
if (typeof expandable.expandIcon === "function") {
|
|
1067
|
+
return expandable.expandIcon({
|
|
1068
|
+
expanded: isExpanded,
|
|
1069
|
+
onExpand: (_2, e) => {
|
|
1070
|
+
e.stopPropagation();
|
|
1071
|
+
toggleExpand(key);
|
|
1072
|
+
},
|
|
1073
|
+
record
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
return /* @__PURE__ */ jsx4(
|
|
1077
|
+
"button",
|
|
1078
|
+
{
|
|
1079
|
+
onClick: (e) => {
|
|
1080
|
+
e.stopPropagation();
|
|
1081
|
+
toggleExpand(key);
|
|
1082
|
+
},
|
|
1083
|
+
style: {
|
|
1084
|
+
display: "flex",
|
|
1085
|
+
alignItems: "center",
|
|
1086
|
+
justifyContent: "center",
|
|
1087
|
+
background: "none",
|
|
1088
|
+
border: "none",
|
|
1089
|
+
cursor: "pointer",
|
|
1090
|
+
padding: "2px",
|
|
1091
|
+
borderRadius: "3px",
|
|
1092
|
+
color: accentColor
|
|
1093
|
+
},
|
|
1094
|
+
children: isExpanded ? /* @__PURE__ */ jsx4(ChevronDown, { style: { width: 14, height: 14 } }) : /* @__PURE__ */ jsx4(ChevronRight, { style: { width: 14, height: 14 } })
|
|
1095
|
+
}
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
1099
|
+
return [expandColumn, ...columnsWithPersistedWidths];
|
|
1100
|
+
}, [
|
|
1101
|
+
expandable,
|
|
1102
|
+
columnsWithPersistedWidths,
|
|
1103
|
+
getRowKey,
|
|
1104
|
+
resolvedExpandedKeys,
|
|
1105
|
+
toggleExpand,
|
|
1106
|
+
accentColor
|
|
1107
|
+
]);
|
|
1108
|
+
const columnsWithSelection = useMemo2(() => {
|
|
1109
|
+
if (!rowSelection) return columnsWithExpand;
|
|
1110
|
+
const selectionColumn = {
|
|
1111
|
+
key: "__select__",
|
|
1112
|
+
dataIndex: "__select__",
|
|
1113
|
+
title: "",
|
|
1114
|
+
width: 48,
|
|
1115
|
+
pinned: "left",
|
|
1116
|
+
hidden: false,
|
|
1117
|
+
render: () => null
|
|
1118
|
+
};
|
|
1119
|
+
return [selectionColumn, ...columnsWithExpand];
|
|
1120
|
+
}, [rowSelection, columnsWithExpand]);
|
|
1121
|
+
const resizeOverlayRef = useRef4(null);
|
|
1122
|
+
const tableAreaRef = useRef4(null);
|
|
1123
|
+
const [scrollAreaWidth, setScrollAreaWidth] = useState2(0);
|
|
1124
|
+
const prevScrollAreaWidthRef = useRef4(0);
|
|
1125
|
+
const roRef = useRef4(null);
|
|
1126
|
+
const rafRef = useRef4(null);
|
|
1127
|
+
const tableAreaCallbackRef = useCallback((el) => {
|
|
1128
|
+
roRef.current?.disconnect();
|
|
1129
|
+
roRef.current = null;
|
|
1130
|
+
if (rafRef.current !== null) {
|
|
1131
|
+
cancelAnimationFrame(rafRef.current);
|
|
1132
|
+
rafRef.current = null;
|
|
1133
|
+
}
|
|
1134
|
+
tableAreaRef.current = el;
|
|
1135
|
+
if (!el) return;
|
|
1136
|
+
const measure = () => {
|
|
1137
|
+
rafRef.current = null;
|
|
1138
|
+
const w = el.clientWidth;
|
|
1139
|
+
if (w !== prevScrollAreaWidthRef.current) {
|
|
1140
|
+
prevScrollAreaWidthRef.current = w;
|
|
1141
|
+
setScrollAreaWidth(w);
|
|
1142
|
+
}
|
|
1143
|
+
};
|
|
1144
|
+
measure();
|
|
1145
|
+
const ro = new ResizeObserver(() => {
|
|
1146
|
+
if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
|
|
1147
|
+
rafRef.current = requestAnimationFrame(measure);
|
|
1148
|
+
});
|
|
1149
|
+
ro.observe(el);
|
|
1150
|
+
roRef.current = ro;
|
|
1151
|
+
}, []);
|
|
1152
|
+
const hoveredRowRef = useRef4(null);
|
|
1153
|
+
React4.useEffect(() => {
|
|
1154
|
+
const el = tableAreaRef.current;
|
|
1155
|
+
if (!el) return;
|
|
1156
|
+
const setHover = (key) => {
|
|
1157
|
+
if (hoveredRowRef.current === key) return;
|
|
1158
|
+
if (hoveredRowRef.current) {
|
|
1159
|
+
el.querySelectorAll(
|
|
1160
|
+
`[data-row-key="${hoveredRowRef.current}"]`
|
|
1161
|
+
).forEach((n) => n.removeAttribute("data-hover"));
|
|
1162
|
+
}
|
|
1163
|
+
hoveredRowRef.current = key;
|
|
1164
|
+
if (key) {
|
|
1165
|
+
el.querySelectorAll(`[data-row-key="${key}"]`).forEach(
|
|
1166
|
+
(n) => n.setAttribute("data-hover", "")
|
|
1167
|
+
);
|
|
1168
|
+
}
|
|
1169
|
+
};
|
|
1170
|
+
const onOver = (e) => {
|
|
1171
|
+
const target = e.target.closest(
|
|
1172
|
+
"[data-row-key]"
|
|
1173
|
+
);
|
|
1174
|
+
setHover(target?.dataset.rowKey ?? null);
|
|
1175
|
+
};
|
|
1176
|
+
const onLeave = () => setHover(null);
|
|
1177
|
+
el.addEventListener("mouseover", onOver, { passive: true });
|
|
1178
|
+
el.addEventListener("mouseleave", onLeave, { passive: true });
|
|
1179
|
+
return () => {
|
|
1180
|
+
el.removeEventListener("mouseover", onOver);
|
|
1181
|
+
el.removeEventListener("mouseleave", onLeave);
|
|
1182
|
+
};
|
|
1183
|
+
}, []);
|
|
1184
|
+
const resizeStateRef = useRef4(null);
|
|
1185
|
+
const sensors = useSensors(useSensor(PointerSensor));
|
|
1186
|
+
const handleDragStart = (event) => {
|
|
1187
|
+
if (event.active.id === "__select__" || event.active.id === "__expand__")
|
|
1188
|
+
return;
|
|
1189
|
+
setActiveId(event.active.id);
|
|
1190
|
+
};
|
|
1191
|
+
const handleDragEnd = (event) => {
|
|
1192
|
+
const { active, over } = event;
|
|
1193
|
+
if (over && active.id !== over.id) {
|
|
1194
|
+
setColumnOrder((items) => {
|
|
1195
|
+
const oldIndex = items.indexOf(active.id);
|
|
1196
|
+
const newIndex = items.indexOf(over.id);
|
|
1197
|
+
const newOrder = arrayMove(items, oldIndex, newIndex);
|
|
1198
|
+
setTimeout(() => onColumnOrderChange?.(newOrder), 0);
|
|
1199
|
+
return newOrder;
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
setActiveId(null);
|
|
1203
|
+
};
|
|
1204
|
+
const handleResizeStart = (columnKey, e) => {
|
|
1205
|
+
e.preventDefault();
|
|
1206
|
+
e.stopPropagation();
|
|
1207
|
+
if (columnKey === "__select__" || columnKey === "__expand__") return;
|
|
1208
|
+
const columnIndex = columnsWithSelection.findIndex(
|
|
1209
|
+
(col) => col.key === columnKey
|
|
1210
|
+
);
|
|
1211
|
+
if (columnIndex === -1) return;
|
|
1212
|
+
if (columnsWithSelection[columnIndex].pinned) return;
|
|
1213
|
+
const column = columnsWithSelection[columnIndex];
|
|
1214
|
+
const startWidth = column.width ?? 150;
|
|
1215
|
+
resizeStateRef.current = {
|
|
1216
|
+
columnKey,
|
|
1217
|
+
startX: e.clientX,
|
|
1218
|
+
startWidth,
|
|
1219
|
+
columnIndex,
|
|
1220
|
+
currentX: e.clientX
|
|
1221
|
+
};
|
|
1222
|
+
if (tableAreaRef.current) {
|
|
1223
|
+
const headerElement = tableAreaRef.current.querySelector(
|
|
1224
|
+
`[data-column-key="${columnKey}"]`
|
|
1225
|
+
);
|
|
1226
|
+
if (headerElement) {
|
|
1227
|
+
const areaRect = tableAreaRef.current.getBoundingClientRect();
|
|
1228
|
+
const headerRect = headerElement.getBoundingClientRect();
|
|
1229
|
+
const scrollTop = tableAreaRef.current.scrollTop;
|
|
1230
|
+
const scrollLeft = tableAreaRef.current.scrollLeft;
|
|
1231
|
+
const headerLeftInContent = headerRect.left - areaRect.left + scrollLeft;
|
|
1232
|
+
resizeOverlayRef.current?.show(
|
|
1233
|
+
headerRect.right,
|
|
1234
|
+
typeof column.title === "string" ? column.title : String(column.key),
|
|
1235
|
+
areaRect,
|
|
1236
|
+
headerLeftInContent,
|
|
1237
|
+
40,
|
|
1238
|
+
// minimum column width
|
|
1239
|
+
scrollTop,
|
|
1240
|
+
scrollLeft,
|
|
1241
|
+
headerLeftInContent + startWidth
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
document.addEventListener("mousemove", handleResizeMove);
|
|
1246
|
+
document.addEventListener("mouseup", handleResizeEnd);
|
|
1247
|
+
};
|
|
1248
|
+
const handleResizeMove = (e) => {
|
|
1249
|
+
if (!resizeStateRef.current) return;
|
|
1250
|
+
resizeStateRef.current.currentX = e.clientX;
|
|
1251
|
+
resizeOverlayRef.current?.move(e.clientX);
|
|
1252
|
+
};
|
|
1253
|
+
const handleResizeEnd = React4.useCallback(() => {
|
|
1254
|
+
if (!resizeStateRef.current) return;
|
|
1255
|
+
const { startX, startWidth, currentX, columnKey } = resizeStateRef.current;
|
|
1256
|
+
const finalWidth = Math.max(40, startWidth + (currentX - startX));
|
|
1257
|
+
manuallyResizedRef.current.add(columnKey);
|
|
1258
|
+
setColumnWidths((prev) => {
|
|
1259
|
+
const next = new Map(prev);
|
|
1260
|
+
next.set(columnKey, finalWidth);
|
|
1261
|
+
return next;
|
|
1262
|
+
});
|
|
1263
|
+
onColumnResize?.(columnKey, finalWidth);
|
|
1264
|
+
resizeOverlayRef.current?.hide();
|
|
1265
|
+
resizeStateRef.current = null;
|
|
1266
|
+
document.removeEventListener("mousemove", handleResizeMove);
|
|
1267
|
+
document.removeEventListener("mouseup", handleResizeEnd);
|
|
1268
|
+
}, [onColumnResize]);
|
|
1269
|
+
const { leftPinned, unpinned, rightPinned } = useMemo2(() => {
|
|
1270
|
+
const columnMap = new Map(columnsWithSelection.map((c) => [c.key, c]));
|
|
1271
|
+
const systemKeys = [
|
|
1272
|
+
...rowSelection ? ["__select__"] : [],
|
|
1273
|
+
...expandable ? ["__expand__"] : []
|
|
1274
|
+
];
|
|
1275
|
+
const visibleColumns = [...systemKeys, ...columnOrder].map((key) => columnMap.get(key)).filter((col) => col !== void 0 && !col.hidden);
|
|
1276
|
+
const left = [], center = [], right = [];
|
|
1277
|
+
visibleColumns.forEach((col) => {
|
|
1278
|
+
if (col.pinned === "left") left.push(col);
|
|
1279
|
+
else if (col.pinned === "right") right.push(col);
|
|
1280
|
+
else center.push(col);
|
|
1281
|
+
});
|
|
1282
|
+
return { leftPinned: left, unpinned: center, rightPinned: right };
|
|
1283
|
+
}, [columnOrder, columnsWithSelection, rowSelection, expandable]);
|
|
1284
|
+
const orderedColumns = useMemo2(
|
|
1285
|
+
() => [...leftPinned, ...unpinned, ...rightPinned],
|
|
1286
|
+
[leftPinned, unpinned, rightPinned]
|
|
1287
|
+
);
|
|
1288
|
+
const totalTableWidth = useMemo2(
|
|
1289
|
+
() => orderedColumns.slice(0, -1).reduce((sum, col) => sum + (col.width ?? 150), 0) + (orderedColumns.at(-1)?.width ?? 150),
|
|
1290
|
+
[orderedColumns]
|
|
1291
|
+
);
|
|
1292
|
+
const gridTemplateColumns = useMemo2(() => {
|
|
1293
|
+
if (orderedColumns.length === 0) return "";
|
|
1294
|
+
return orderedColumns.map((col, i) => {
|
|
1295
|
+
const w = col.width ?? 150;
|
|
1296
|
+
return i === orderedColumns.length - 1 ? `minmax(${w}px, 1fr)` : `${w}px`;
|
|
1297
|
+
}).join(" ");
|
|
1298
|
+
}, [orderedColumns]);
|
|
1299
|
+
const columnOffsets = useMemo2(() => {
|
|
1300
|
+
const offsets = /* @__PURE__ */ new Map();
|
|
1301
|
+
let lo = 0;
|
|
1302
|
+
leftPinned.forEach((col) => {
|
|
1303
|
+
offsets.set(col.key, lo);
|
|
1304
|
+
lo += col.width ?? 150;
|
|
1305
|
+
});
|
|
1306
|
+
let ro = 0;
|
|
1307
|
+
for (let i = rightPinned.length - 1; i >= 0; i--) {
|
|
1308
|
+
const col = rightPinned[i];
|
|
1309
|
+
offsets.set(col.key, ro);
|
|
1310
|
+
ro += col.width ?? 150;
|
|
1311
|
+
}
|
|
1312
|
+
return offsets;
|
|
1313
|
+
}, [leftPinned, rightPinned]);
|
|
1314
|
+
const handleTogglePin = (columnKey, pinned) => {
|
|
1315
|
+
setColumns(
|
|
1316
|
+
(prev) => prev.map((col) => col.key === columnKey ? { ...col, pinned } : col)
|
|
1317
|
+
);
|
|
1318
|
+
onColumnPin?.(columnKey, pinned);
|
|
1319
|
+
};
|
|
1320
|
+
const handleToggleHide = (columnKey) => {
|
|
1321
|
+
setColumns(
|
|
1322
|
+
(prev) => prev.map((col) => {
|
|
1323
|
+
if (col.key !== columnKey || col.pinned) return col;
|
|
1324
|
+
return { ...col, hidden: !col.hidden };
|
|
1325
|
+
})
|
|
1326
|
+
);
|
|
1327
|
+
const column = columns.find((col) => col.key === columnKey);
|
|
1328
|
+
if (column && !column.pinned) onColumnHide?.(columnKey, !column.hidden);
|
|
1329
|
+
};
|
|
1330
|
+
const onSortChangeRef = useRef4(onSortChange);
|
|
1331
|
+
onSortChangeRef.current = onSortChange;
|
|
1332
|
+
const [sortState, setSortState] = useState2({ key: "", direction: null });
|
|
1333
|
+
const handleSort = useCallback(
|
|
1334
|
+
(columnKey, direction) => {
|
|
1335
|
+
setSortState((prev) => {
|
|
1336
|
+
let next;
|
|
1337
|
+
if (direction !== void 0) {
|
|
1338
|
+
next = prev.key === columnKey && prev.direction === direction ? null : direction;
|
|
1339
|
+
} else {
|
|
1340
|
+
next = prev.key !== columnKey ? "asc" : prev.direction === "asc" ? "desc" : prev.direction === "desc" ? null : "asc";
|
|
1341
|
+
}
|
|
1342
|
+
const state = { key: next ? columnKey : "", direction: next };
|
|
1343
|
+
onSortChangeRef.current?.(columnKey, next);
|
|
1344
|
+
return state;
|
|
1345
|
+
});
|
|
1346
|
+
},
|
|
1347
|
+
[]
|
|
1348
|
+
);
|
|
1349
|
+
const [columnFilters, setColumnFilters] = useState2(
|
|
1350
|
+
{}
|
|
1351
|
+
);
|
|
1352
|
+
const handleColumnFilter = useCallback(
|
|
1353
|
+
(columnKey, value) => {
|
|
1354
|
+
setColumnFilters((prev) => {
|
|
1355
|
+
const next = { ...prev };
|
|
1356
|
+
if (value) next[columnKey] = value;
|
|
1357
|
+
else delete next[columnKey];
|
|
1358
|
+
onFilterChange?.(next);
|
|
1359
|
+
return next;
|
|
1360
|
+
});
|
|
1361
|
+
},
|
|
1362
|
+
[onFilterChange]
|
|
1363
|
+
);
|
|
1364
|
+
const handleClearFilter = useCallback(
|
|
1365
|
+
(columnKey) => {
|
|
1366
|
+
handleColumnFilter(columnKey, "");
|
|
1367
|
+
},
|
|
1368
|
+
[handleColumnFilter]
|
|
1369
|
+
);
|
|
1370
|
+
const onFilterChangeRef = useRef4(onFilterChange);
|
|
1371
|
+
onFilterChangeRef.current = onFilterChange;
|
|
1372
|
+
const columnsLookupRef = useRef4(initialColumns);
|
|
1373
|
+
columnsLookupRef.current = initialColumns;
|
|
1374
|
+
const processedData = useMemo2(() => {
|
|
1375
|
+
let result = data;
|
|
1376
|
+
if (!onFilterChangeRef.current) {
|
|
1377
|
+
const filterKeys = Object.keys(columnFilters);
|
|
1378
|
+
if (filterKeys.length > 0) {
|
|
1379
|
+
result = result.filter(
|
|
1380
|
+
(row) => filterKeys.every((key) => {
|
|
1381
|
+
const col = columnsLookupRef.current.find((c) => c.key === key);
|
|
1382
|
+
if (typeof col?.filterFn === "function") {
|
|
1383
|
+
return col.filterFn(columnFilters[key], row, col.dataIndex);
|
|
1384
|
+
}
|
|
1385
|
+
const cellVal = String(row[key] ?? "").toLowerCase();
|
|
1386
|
+
return cellVal.includes(columnFilters[key].toLowerCase());
|
|
1387
|
+
})
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
if (!onSortChangeRef.current && sortState.key && sortState.direction) {
|
|
1392
|
+
const dir = sortState.direction === "asc" ? 1 : -1;
|
|
1393
|
+
const key = sortState.key;
|
|
1394
|
+
const col = columnsLookupRef.current.find((c) => c.key === key);
|
|
1395
|
+
if (typeof col?.sorter === "function") {
|
|
1396
|
+
const sorterFn = col.sorter;
|
|
1397
|
+
result = [...result].sort((a, b) => sorterFn(a, b) * dir);
|
|
1398
|
+
} else {
|
|
1399
|
+
result = [...result].sort((a, b) => {
|
|
1400
|
+
const aVal = a[key];
|
|
1401
|
+
const bVal = b[key];
|
|
1402
|
+
if (aVal == null && bVal == null) return 0;
|
|
1403
|
+
if (aVal == null) return 1;
|
|
1404
|
+
if (bVal == null) return -1;
|
|
1405
|
+
if (typeof aVal === "number" && typeof bVal === "number")
|
|
1406
|
+
return (aVal - bVal) * dir;
|
|
1407
|
+
return String(aVal).localeCompare(String(bVal)) * dir;
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
return result;
|
|
1412
|
+
}, [data, sortState, columnFilters]);
|
|
1413
|
+
const columnFiltersKey = Object.keys(columnFilters).sort().map((k) => `${k}:${columnFilters[k]}`).join("|");
|
|
1414
|
+
React4.useEffect(() => {
|
|
1415
|
+
tableAreaRef.current?.scrollTo({ top: 0 });
|
|
1416
|
+
}, [columnFiltersKey]);
|
|
1417
|
+
const pgEnabled = pagination !== false && !!pagination;
|
|
1418
|
+
const pgSize = pgEnabled ? pagination.pageSize ?? 10 : 10;
|
|
1419
|
+
const pgCurrent = pgEnabled ? Number(pagination.current ?? 1) : 1;
|
|
1420
|
+
const needsClientPagination = pgEnabled && processedData.length > pgSize;
|
|
1421
|
+
const paginatedData = useMemo2(() => {
|
|
1422
|
+
if (!needsClientPagination) return processedData;
|
|
1423
|
+
const start = (pgCurrent - 1) * pgSize;
|
|
1424
|
+
return processedData.slice(start, start + pgSize);
|
|
1425
|
+
}, [processedData, needsClientPagination, pgCurrent, pgSize]);
|
|
1426
|
+
const shimmerCount = pgEnabled ? pgSize : 15;
|
|
1427
|
+
const showShimmer = isLoading && processedData.length === 0;
|
|
1428
|
+
const shimmerData = useMemo2(() => {
|
|
1429
|
+
if (!showShimmer) return null;
|
|
1430
|
+
return Array.from(
|
|
1431
|
+
{ length: shimmerCount },
|
|
1432
|
+
(_, i) => ({
|
|
1433
|
+
[typeof rowKey === "string" ? rowKey : "id"]: `__shimmer_${i}__`
|
|
1434
|
+
})
|
|
1435
|
+
);
|
|
1436
|
+
}, [showShimmer, shimmerCount, rowKey]);
|
|
1437
|
+
const INFINITE_SHIMMER_COUNT = 5;
|
|
1438
|
+
const infiniteLoadingShimmer = useMemo2(() => {
|
|
1439
|
+
if (!isLoading || paginatedData.length === 0 || showShimmer) return null;
|
|
1440
|
+
if (pgEnabled) return null;
|
|
1441
|
+
return Array.from(
|
|
1442
|
+
{ length: INFINITE_SHIMMER_COUNT },
|
|
1443
|
+
(_, i) => ({
|
|
1444
|
+
[typeof rowKey === "string" ? rowKey : "id"]: `__shimmer_${i}__`
|
|
1445
|
+
})
|
|
1446
|
+
);
|
|
1447
|
+
}, [isLoading, paginatedData.length, showShimmer, pgEnabled, rowKey]);
|
|
1448
|
+
const displayData = useMemo2(() => {
|
|
1449
|
+
if (shimmerData) return shimmerData;
|
|
1450
|
+
if (infiniteLoadingShimmer)
|
|
1451
|
+
return [...paginatedData, ...infiniteLoadingShimmer];
|
|
1452
|
+
return paginatedData;
|
|
1453
|
+
}, [shimmerData, infiniteLoadingShimmer, paginatedData]);
|
|
1454
|
+
const measuredExpandedHeights = useRef4(/* @__PURE__ */ new Map());
|
|
1455
|
+
const expandedRowMeasureRafRef = useRef4(null);
|
|
1456
|
+
const handleExpandedRowResize = useCallback(
|
|
1457
|
+
(rk, contentHeight) => {
|
|
1458
|
+
const prev = measuredExpandedHeights.current.get(rk);
|
|
1459
|
+
const rounded = Math.round(contentHeight);
|
|
1460
|
+
if (prev === rounded) return;
|
|
1461
|
+
measuredExpandedHeights.current.set(rk, rounded);
|
|
1462
|
+
if (expandedRowMeasureRafRef.current !== null) {
|
|
1463
|
+
cancelAnimationFrame(expandedRowMeasureRafRef.current);
|
|
1464
|
+
}
|
|
1465
|
+
expandedRowMeasureRafRef.current = requestAnimationFrame(() => {
|
|
1466
|
+
expandedRowMeasureRafRef.current = null;
|
|
1467
|
+
rowVirtualizerRef.current?.measure();
|
|
1468
|
+
});
|
|
1469
|
+
},
|
|
1470
|
+
[]
|
|
1471
|
+
);
|
|
1472
|
+
const rowVirtualizer = useVirtualizer({
|
|
1473
|
+
count: displayData.length,
|
|
1474
|
+
getScrollElement: () => tableAreaRef.current,
|
|
1475
|
+
estimateSize: (index) => {
|
|
1476
|
+
if (shimmerData) return rowHeight;
|
|
1477
|
+
const key = getRowKey(displayData[index], index);
|
|
1478
|
+
if (!resolvedExpandedKeys.has(key)) return rowHeight;
|
|
1479
|
+
const cached = measuredExpandedHeights.current.get(key);
|
|
1480
|
+
return cached ? rowHeight + cached : rowHeight + expandedRowHeight;
|
|
1481
|
+
},
|
|
1482
|
+
overscan: 5,
|
|
1483
|
+
// Render 5 extra rows above and below the visible window
|
|
1484
|
+
getItemKey: (index) => shimmerData ? `__shimmer_${index}__` : getRowKey(displayData[index], index)
|
|
1485
|
+
});
|
|
1486
|
+
const rowVirtualizerRef = useRef4(rowVirtualizer);
|
|
1487
|
+
rowVirtualizerRef.current = rowVirtualizer;
|
|
1488
|
+
const resolvedExpandedKeysFingerprint = Array.from(resolvedExpandedKeys).sort().join(",");
|
|
1489
|
+
React4.useLayoutEffect(() => {
|
|
1490
|
+
rowVirtualizer.measure();
|
|
1491
|
+
}, [resolvedExpandedKeysFingerprint]);
|
|
1492
|
+
const endReachedFiredRef = useRef4(false);
|
|
1493
|
+
const onEndReachedRef = useRef4(onEndReached);
|
|
1494
|
+
onEndReachedRef.current = onEndReached;
|
|
1495
|
+
const isLoadingRef = useRef4(isLoading);
|
|
1496
|
+
isLoadingRef.current = isLoading;
|
|
1497
|
+
React4.useEffect(() => {
|
|
1498
|
+
const timer = setTimeout(() => {
|
|
1499
|
+
endReachedFiredRef.current = false;
|
|
1500
|
+
}, 200);
|
|
1501
|
+
return () => clearTimeout(timer);
|
|
1502
|
+
}, [data.length, isLoading]);
|
|
1503
|
+
React4.useEffect(() => {
|
|
1504
|
+
const el = tableAreaRef.current;
|
|
1505
|
+
if (!el) return;
|
|
1506
|
+
const checkEndReached = () => {
|
|
1507
|
+
if (!onEndReachedRef.current || displayData.length === 0 || endReachedFiredRef.current || isLoadingRef.current)
|
|
1508
|
+
return;
|
|
1509
|
+
const virtualItems = rowVirtualizer.getVirtualItems();
|
|
1510
|
+
if (virtualItems.length === 0) return;
|
|
1511
|
+
const lastVisibleIndex = virtualItems[virtualItems.length - 1].index;
|
|
1512
|
+
const distanceFromEnd = displayData.length - 1 - lastVisibleIndex;
|
|
1513
|
+
if (distanceFromEnd <= onEndReachedThreshold) {
|
|
1514
|
+
endReachedFiredRef.current = true;
|
|
1515
|
+
onEndReachedRef.current();
|
|
1516
|
+
}
|
|
1517
|
+
};
|
|
1518
|
+
el.addEventListener("scroll", checkEndReached, { passive: true });
|
|
1519
|
+
return () => el.removeEventListener("scroll", checkEndReached);
|
|
1520
|
+
}, [displayData.length, onEndReachedThreshold]);
|
|
1521
|
+
const activeColumn = activeId ? orderedColumns.find((col) => col.key === activeId) : null;
|
|
1522
|
+
const currentPage = pgCurrent;
|
|
1523
|
+
const pageSize = pgSize;
|
|
1524
|
+
const rawTotal = pgEnabled ? pagination.total ?? (needsClientPagination ? processedData.length : data.length) : data.length;
|
|
1525
|
+
const lastKnownTotalRef = useRef4(0);
|
|
1526
|
+
if (!isLoading || rawTotal > 0) {
|
|
1527
|
+
lastKnownTotalRef.current = rawTotal;
|
|
1528
|
+
}
|
|
1529
|
+
const total = isLoading && lastKnownTotalRef.current > 0 ? lastKnownTotalRef.current : rawTotal;
|
|
1530
|
+
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
|
1531
|
+
const handlePageChange = (p) => {
|
|
1532
|
+
if (p >= 1 && p <= totalPages) onPaginationChange?.(p, pageSize);
|
|
1533
|
+
};
|
|
1534
|
+
const handlePageSizeChange = (s) => onPaginationChange?.(1, s);
|
|
1535
|
+
React4.useEffect(() => {
|
|
1536
|
+
if (needsClientPagination) {
|
|
1537
|
+
tableAreaRef.current?.scrollTo({ top: 0 });
|
|
1538
|
+
}
|
|
1539
|
+
}, [pgCurrent, needsClientPagination]);
|
|
1540
|
+
const getPageNumbers = () => {
|
|
1541
|
+
if (totalPages <= 7)
|
|
1542
|
+
return Array.from({ length: totalPages }, (_, i) => i + 1);
|
|
1543
|
+
const leftSibling = Math.max(currentPage - 1, 2);
|
|
1544
|
+
const showLeftEllipsis = leftSibling > 2;
|
|
1545
|
+
const rightSibling = Math.min(currentPage + 1, totalPages - 1);
|
|
1546
|
+
const showRightEllipsis = currentPage < totalPages - 3;
|
|
1547
|
+
if (!showLeftEllipsis && showRightEllipsis)
|
|
1548
|
+
return [1, 2, 3, 4, 5, "ellipsis-right", totalPages];
|
|
1549
|
+
if (showLeftEllipsis && !showRightEllipsis)
|
|
1550
|
+
return [
|
|
1551
|
+
1,
|
|
1552
|
+
"ellipsis-left",
|
|
1553
|
+
...Array.from({ length: 5 }, (_, i) => totalPages - 4 + i)
|
|
1554
|
+
];
|
|
1555
|
+
return [
|
|
1556
|
+
1,
|
|
1557
|
+
"ellipsis-left",
|
|
1558
|
+
leftSibling,
|
|
1559
|
+
currentPage,
|
|
1560
|
+
rightSibling,
|
|
1561
|
+
"ellipsis-right",
|
|
1562
|
+
totalPages
|
|
1563
|
+
];
|
|
1564
|
+
};
|
|
1565
|
+
const HEADER_HEIGHT = 36;
|
|
1566
|
+
const MAX_AUTO_ROWS = 10;
|
|
1567
|
+
const naturalContentHeight = rowVirtualizer.getTotalSize() + HEADER_HEIGHT;
|
|
1568
|
+
const maxAutoHeight = MAX_AUTO_ROWS * rowHeight + HEADER_HEIGHT;
|
|
1569
|
+
const isEmpty = displayData.length === 0 && !showShimmer;
|
|
1570
|
+
const emptyMinHeight = 4 * rowHeight + HEADER_HEIGHT;
|
|
1571
|
+
const clampedAutoHeight = isEmpty ? emptyMinHeight : Math.min(naturalContentHeight, maxAutoHeight);
|
|
1572
|
+
return /* @__PURE__ */ jsxs4(
|
|
1573
|
+
DndContext,
|
|
1574
|
+
{
|
|
1575
|
+
sensors,
|
|
1576
|
+
collisionDetection: closestCenter,
|
|
1577
|
+
onDragStart: handleDragStart,
|
|
1578
|
+
onDragEnd: handleDragEnd,
|
|
1579
|
+
children: [
|
|
1580
|
+
/* @__PURE__ */ jsxs4(
|
|
1581
|
+
"div",
|
|
1582
|
+
{
|
|
1583
|
+
className: `flex ${autoHeight ? "max-h-full" : "h-full"} w-full flex-col ${className}`,
|
|
1584
|
+
children: [
|
|
1585
|
+
/* @__PURE__ */ jsx4("style", { children: `
|
|
1586
|
+
[data-row-key][data-hover] > div {
|
|
1587
|
+
background-color: ${styles.rowHover?.backgroundColor ?? `hsl(var(--muted) / 0.5)`};
|
|
1588
|
+
}
|
|
1589
|
+
[data-row-key][data-selected] > div {
|
|
1590
|
+
background-color: ${styles.rowSelected?.backgroundColor ?? `${accentColor}15`};
|
|
1591
|
+
}
|
|
1592
|
+
[data-row-key][data-selected][data-hover] > div {
|
|
1593
|
+
background-color: ${styles.rowSelected?.backgroundColor ?? `${accentColor}25`};
|
|
1594
|
+
}
|
|
1595
|
+
` }),
|
|
1596
|
+
/* @__PURE__ */ jsx4(
|
|
1597
|
+
"div",
|
|
1598
|
+
{
|
|
1599
|
+
className: `relative ${autoHeight ? "" : "flex-1"}`,
|
|
1600
|
+
style: autoHeight ? {
|
|
1601
|
+
height: `${clampedAutoHeight}px`,
|
|
1602
|
+
maxHeight: `${clampedAutoHeight}px`,
|
|
1603
|
+
flexShrink: 1,
|
|
1604
|
+
flexGrow: 0
|
|
1605
|
+
} : void 0,
|
|
1606
|
+
children: layoutLoading ? (
|
|
1607
|
+
/*
|
|
1608
|
+
* ── Layout loading skeleton ──────────────────────────────────
|
|
1609
|
+
* Shown when layoutLoading=true. Renders real column headers
|
|
1610
|
+
* (based on orderedColumns) alongside shimmer body rows.
|
|
1611
|
+
* Used for initial page load when column widths are not yet known.
|
|
1612
|
+
*/
|
|
1613
|
+
/* @__PURE__ */ jsx4(
|
|
1614
|
+
"div",
|
|
1615
|
+
{
|
|
1616
|
+
className: "absolute inset-0 overflow-auto",
|
|
1617
|
+
style: { contain: "layout paint" },
|
|
1618
|
+
children: /* @__PURE__ */ jsxs4(
|
|
1619
|
+
"div",
|
|
1620
|
+
{
|
|
1621
|
+
style: {
|
|
1622
|
+
display: "grid",
|
|
1623
|
+
gridTemplateColumns,
|
|
1624
|
+
gridTemplateRows: "36px auto",
|
|
1625
|
+
minWidth: `${totalTableWidth}px`,
|
|
1626
|
+
width: "100%",
|
|
1627
|
+
position: "relative"
|
|
1628
|
+
},
|
|
1629
|
+
children: [
|
|
1630
|
+
orderedColumns.map((column) => {
|
|
1631
|
+
const isPinned = !!column.pinned;
|
|
1632
|
+
const offset = columnOffsets.get(column.key);
|
|
1633
|
+
const isSystem = column.key === "__select__" || column.key === "__expand__";
|
|
1634
|
+
return /* @__PURE__ */ jsx4(
|
|
1635
|
+
"div",
|
|
1636
|
+
{
|
|
1637
|
+
className: `flex h-9 items-center truncate border-t border-b ${isPinned ? `bg-background backdrop-blur ${classNames.pinnedHeader ?? ""}` : `bg-muted/40 backdrop-blur ${classNames.header ?? ""}`}`,
|
|
1638
|
+
style: {
|
|
1639
|
+
position: "sticky",
|
|
1640
|
+
top: 0,
|
|
1641
|
+
zIndex: isPinned ? 13 : 10,
|
|
1642
|
+
...isPinned ? {
|
|
1643
|
+
[column.pinned]: offset ?? 0,
|
|
1644
|
+
...styles.pinnedHeader
|
|
1645
|
+
} : styles.header,
|
|
1646
|
+
paddingLeft: isSystem ? 0 : 8,
|
|
1647
|
+
paddingRight: isSystem ? 0 : 8
|
|
1648
|
+
}
|
|
1649
|
+
},
|
|
1650
|
+
column.key
|
|
1651
|
+
);
|
|
1652
|
+
}),
|
|
1653
|
+
/* @__PURE__ */ jsx4("div", { style: { gridColumn: "1 / -1" }, children: Array.from({ length: shimmerCount }).map((_, rowIndex) => /* @__PURE__ */ jsx4(
|
|
1654
|
+
"div",
|
|
1655
|
+
{
|
|
1656
|
+
style: {
|
|
1657
|
+
display: "grid",
|
|
1658
|
+
gridTemplateColumns,
|
|
1659
|
+
height: rowHeight
|
|
1660
|
+
},
|
|
1661
|
+
children: orderedColumns.map((column, colIndex) => {
|
|
1662
|
+
const isPinned = !!column.pinned;
|
|
1663
|
+
const offset = columnOffsets.get(column.key);
|
|
1664
|
+
const isSystem = column.key === "__select__" || column.key === "__expand__";
|
|
1665
|
+
const widthPercent = SHIMMER_WIDTHS2[(rowIndex * 7 + colIndex) % SHIMMER_WIDTHS2.length];
|
|
1666
|
+
return /* @__PURE__ */ jsx4(
|
|
1667
|
+
"div",
|
|
1668
|
+
{
|
|
1669
|
+
className: `flex items-center border-b ${isPinned ? `bg-background ${classNames.pinnedCell ?? ""}` : ""}`,
|
|
1670
|
+
style: {
|
|
1671
|
+
...isPinned ? {
|
|
1672
|
+
position: "sticky",
|
|
1673
|
+
[column.pinned]: offset ?? 0,
|
|
1674
|
+
zIndex: 5,
|
|
1675
|
+
...styles.pinnedCell
|
|
1676
|
+
} : {},
|
|
1677
|
+
paddingLeft: isSystem ? 0 : 8,
|
|
1678
|
+
paddingRight: isSystem ? 0 : 8,
|
|
1679
|
+
justifyContent: isSystem ? "center" : void 0
|
|
1680
|
+
},
|
|
1681
|
+
children: /* @__PURE__ */ jsx4(
|
|
1682
|
+
"div",
|
|
1683
|
+
{
|
|
1684
|
+
className: "bg-muted-foreground/15 animate-pulse rounded",
|
|
1685
|
+
style: {
|
|
1686
|
+
height: isSystem ? 16 : 14,
|
|
1687
|
+
width: isSystem ? 16 : `${widthPercent}%`,
|
|
1688
|
+
borderRadius: isSystem ? 3 : 4,
|
|
1689
|
+
animationDelay: `${(rowIndex * 7 + colIndex) * 50}ms`
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
)
|
|
1693
|
+
},
|
|
1694
|
+
column.key
|
|
1695
|
+
);
|
|
1696
|
+
})
|
|
1697
|
+
},
|
|
1698
|
+
rowIndex
|
|
1699
|
+
)) })
|
|
1700
|
+
]
|
|
1701
|
+
}
|
|
1702
|
+
)
|
|
1703
|
+
}
|
|
1704
|
+
)
|
|
1705
|
+
) : (
|
|
1706
|
+
/*
|
|
1707
|
+
* ── Main scroll container ────────────────────────────────────
|
|
1708
|
+
* absolute inset-0 so it fills whatever height the wrapper resolves to.
|
|
1709
|
+
* contain: layout paint — browser optimization hint that this element
|
|
1710
|
+
* is a layout and paint boundary (improves compositing performance).
|
|
1711
|
+
*/
|
|
1712
|
+
/* @__PURE__ */ jsxs4(
|
|
1713
|
+
"div",
|
|
1714
|
+
{
|
|
1715
|
+
ref: tableAreaCallbackRef,
|
|
1716
|
+
className: "absolute inset-0 overflow-auto",
|
|
1717
|
+
style: { contain: "layout paint" },
|
|
1718
|
+
children: [
|
|
1719
|
+
/* @__PURE__ */ jsx4(ResizeOverlay_default, { ref: resizeOverlayRef, accentColor }),
|
|
1720
|
+
/* @__PURE__ */ jsxs4(
|
|
1721
|
+
"div",
|
|
1722
|
+
{
|
|
1723
|
+
style: {
|
|
1724
|
+
display: "grid",
|
|
1725
|
+
gridTemplateColumns,
|
|
1726
|
+
gridTemplateRows: "36px 1fr",
|
|
1727
|
+
minWidth: `${totalTableWidth}px`,
|
|
1728
|
+
height: "100%",
|
|
1729
|
+
width: "100%",
|
|
1730
|
+
position: "relative"
|
|
1731
|
+
},
|
|
1732
|
+
children: [
|
|
1733
|
+
/* @__PURE__ */ jsx4(
|
|
1734
|
+
SortableContext,
|
|
1735
|
+
{
|
|
1736
|
+
items: columnOrder,
|
|
1737
|
+
strategy: horizontalListSortingStrategy,
|
|
1738
|
+
children: orderedColumns.map((column, visualIndex) => {
|
|
1739
|
+
if (column.key === "__select__" && rowSelection) {
|
|
1740
|
+
return /* @__PURE__ */ jsx4(
|
|
1741
|
+
"div",
|
|
1742
|
+
{
|
|
1743
|
+
className: `bg-muted/40 sticky flex h-9 items-center justify-center truncate border-t border-b backdrop-blur ${classNames.header ?? ""} ${classNames.pinnedHeader ?? ""} `,
|
|
1744
|
+
style: {
|
|
1745
|
+
position: "sticky",
|
|
1746
|
+
left: columnOffsets.get("__select__") ?? 0,
|
|
1747
|
+
top: 0,
|
|
1748
|
+
zIndex: 13,
|
|
1749
|
+
width: "48px",
|
|
1750
|
+
...styles.header,
|
|
1751
|
+
...styles.pinnedHeader
|
|
1752
|
+
},
|
|
1753
|
+
children: rowSelection.type !== "radio" && !rowSelection.hideSelectAll && /* @__PURE__ */ jsx4(
|
|
1754
|
+
"input",
|
|
1755
|
+
{
|
|
1756
|
+
type: "checkbox",
|
|
1757
|
+
checked: data.length > 0 && normalizedSelectedKeys.length === data.length,
|
|
1758
|
+
ref: (input) => {
|
|
1759
|
+
if (input) {
|
|
1760
|
+
input.indeterminate = normalizedSelectedKeys.length > 0 && normalizedSelectedKeys.length < data.length;
|
|
1761
|
+
}
|
|
1762
|
+
},
|
|
1763
|
+
onChange: (e) => {
|
|
1764
|
+
if (e.target.checked) {
|
|
1765
|
+
const allKeys = data.map(
|
|
1766
|
+
(row, idx) => getRowKey(row, idx)
|
|
1767
|
+
);
|
|
1768
|
+
rowSelection.onSelectAll?.(
|
|
1769
|
+
true,
|
|
1770
|
+
data,
|
|
1771
|
+
data
|
|
1772
|
+
);
|
|
1773
|
+
rowSelection.onChange?.(allKeys, data, {
|
|
1774
|
+
type: "all"
|
|
1775
|
+
});
|
|
1776
|
+
} else {
|
|
1777
|
+
rowSelection.onSelectAll?.(false, [], data);
|
|
1778
|
+
rowSelection.onChange?.([], [], {
|
|
1779
|
+
type: "all"
|
|
1780
|
+
});
|
|
1781
|
+
}
|
|
1782
|
+
},
|
|
1783
|
+
className: "cursor-pointer",
|
|
1784
|
+
style: { accentColor }
|
|
1785
|
+
}
|
|
1786
|
+
)
|
|
1787
|
+
},
|
|
1788
|
+
"__select__"
|
|
1789
|
+
);
|
|
1790
|
+
}
|
|
1791
|
+
if (column.key === "__expand__") {
|
|
1792
|
+
return /* @__PURE__ */ jsx4(
|
|
1793
|
+
"div",
|
|
1794
|
+
{
|
|
1795
|
+
className: `bg-muted/40 sticky flex h-9 items-center justify-center truncate border-t border-b backdrop-blur ${classNames.header ?? ""} ${classNames.pinnedHeader ?? ""}`,
|
|
1796
|
+
style: {
|
|
1797
|
+
position: "sticky",
|
|
1798
|
+
left: columnOffsets.get("__expand__") ?? 0,
|
|
1799
|
+
top: 0,
|
|
1800
|
+
zIndex: 13,
|
|
1801
|
+
width: "40px",
|
|
1802
|
+
...styles.header,
|
|
1803
|
+
...styles.pinnedHeader
|
|
1804
|
+
}
|
|
1805
|
+
},
|
|
1806
|
+
"__expand__"
|
|
1807
|
+
);
|
|
1808
|
+
}
|
|
1809
|
+
return /* @__PURE__ */ jsx4(
|
|
1810
|
+
DraggableHeader_default,
|
|
1811
|
+
{
|
|
1812
|
+
column,
|
|
1813
|
+
accentColor,
|
|
1814
|
+
visualIndex,
|
|
1815
|
+
onResizeStart: handleResizeStart,
|
|
1816
|
+
styles,
|
|
1817
|
+
classNames,
|
|
1818
|
+
gripIcon,
|
|
1819
|
+
hideGripIcon,
|
|
1820
|
+
stickyOffset: columnOffsets.get(column.key),
|
|
1821
|
+
onTogglePin: handleTogglePin,
|
|
1822
|
+
onToggleHide: handleToggleHide,
|
|
1823
|
+
isLastColumn: visualIndex === orderedColumns.length - 1,
|
|
1824
|
+
sortDirection: sortState.key === column.key ? sortState.direction : null,
|
|
1825
|
+
onSort: handleSort,
|
|
1826
|
+
filterValue: columnFilters[column.key] ?? "",
|
|
1827
|
+
onFilter: handleColumnFilter,
|
|
1828
|
+
onClearFilter: handleClearFilter,
|
|
1829
|
+
customContextMenuItems: columnContextMenuItems
|
|
1830
|
+
},
|
|
1831
|
+
column.key
|
|
1832
|
+
);
|
|
1833
|
+
})
|
|
1834
|
+
}
|
|
1835
|
+
),
|
|
1836
|
+
isEmpty ? (
|
|
1837
|
+
/*
|
|
1838
|
+
* ── Empty state ────────────────────────────────────────
|
|
1839
|
+
* col-span-full + height:100% fills the 1fr body grid row.
|
|
1840
|
+
*
|
|
1841
|
+
* The inner div uses `position: sticky; left: 0` with a fixed
|
|
1842
|
+
* width (scrollAreaWidth) to viewport-lock the empty state panel.
|
|
1843
|
+
* Without this, the empty message would scroll horizontally
|
|
1844
|
+
* with the grid content when there are many columns.
|
|
1845
|
+
*/
|
|
1846
|
+
/* @__PURE__ */ jsx4(
|
|
1847
|
+
"div",
|
|
1848
|
+
{
|
|
1849
|
+
className: "col-span-full",
|
|
1850
|
+
style: { height: "100%", position: "relative" },
|
|
1851
|
+
children: /* @__PURE__ */ jsx4(
|
|
1852
|
+
"div",
|
|
1853
|
+
{
|
|
1854
|
+
style: {
|
|
1855
|
+
position: "sticky",
|
|
1856
|
+
left: 0,
|
|
1857
|
+
width: scrollAreaWidth > 0 ? `${scrollAreaWidth}px` : "100%",
|
|
1858
|
+
height: "100%",
|
|
1859
|
+
display: "flex",
|
|
1860
|
+
alignItems: "center",
|
|
1861
|
+
justifyContent: "center"
|
|
1862
|
+
},
|
|
1863
|
+
children: emptyRenderer ?? /* @__PURE__ */ jsx4("div", { className: "text-muted-foreground flex flex-col items-center gap-2 py-8", children: /* @__PURE__ */ jsx4("span", { className: "text-sm", children: "No data" }) })
|
|
1864
|
+
}
|
|
1865
|
+
)
|
|
1866
|
+
}
|
|
1867
|
+
)
|
|
1868
|
+
) : (
|
|
1869
|
+
/* ── Virtualized table body ─────────────────────────── */
|
|
1870
|
+
/* @__PURE__ */ jsx4(
|
|
1871
|
+
TableBody_default,
|
|
1872
|
+
{
|
|
1873
|
+
data: displayData,
|
|
1874
|
+
orderedColumns,
|
|
1875
|
+
rowVirtualizer,
|
|
1876
|
+
columnOffsets,
|
|
1877
|
+
styles,
|
|
1878
|
+
classNames,
|
|
1879
|
+
rowSelection: !showShimmer ? rowSelection : void 0,
|
|
1880
|
+
normalizedSelectedKeys,
|
|
1881
|
+
getRowKey,
|
|
1882
|
+
expandable: !showShimmer ? expandable : void 0,
|
|
1883
|
+
resolvedExpandedKeys,
|
|
1884
|
+
rowHeight,
|
|
1885
|
+
totalTableWidth,
|
|
1886
|
+
scrollAreaWidth,
|
|
1887
|
+
accentColor,
|
|
1888
|
+
scrollContainerRef: tableAreaRef,
|
|
1889
|
+
isLoading: showShimmer,
|
|
1890
|
+
onExpandedRowResize: handleExpandedRowResize,
|
|
1891
|
+
maxExpandedRowHeight
|
|
1892
|
+
}
|
|
1893
|
+
)
|
|
1894
|
+
)
|
|
1895
|
+
]
|
|
1896
|
+
}
|
|
1897
|
+
)
|
|
1898
|
+
]
|
|
1899
|
+
}
|
|
1900
|
+
)
|
|
1901
|
+
)
|
|
1902
|
+
}
|
|
1903
|
+
),
|
|
1904
|
+
pagination !== false && /* @__PURE__ */ jsxs4(
|
|
1905
|
+
"div",
|
|
1906
|
+
{
|
|
1907
|
+
className: "flex h-9 items-center justify-between border-t px-3 text-xs backdrop-blur",
|
|
1908
|
+
style: {
|
|
1909
|
+
backgroundColor: "hsl(var(--background)/0.4)",
|
|
1910
|
+
gap: "12px"
|
|
1911
|
+
},
|
|
1912
|
+
children: [
|
|
1913
|
+
/* @__PURE__ */ jsx4("div", { className: "flex flex-1 items-center", children: (() => {
|
|
1914
|
+
const rangeStart = total > 0 ? (currentPage - 1) * pageSize + 1 : 0;
|
|
1915
|
+
const rangeEnd = Math.min(currentPage * pageSize, total);
|
|
1916
|
+
return pagination?.showTotal ? /* @__PURE__ */ jsxs4("span", { className: "text-muted-foreground text-xs", children: [
|
|
1917
|
+
"Showing",
|
|
1918
|
+
" ",
|
|
1919
|
+
pagination.showTotal(total, [rangeStart, rangeEnd]),
|
|
1920
|
+
" of",
|
|
1921
|
+
" ",
|
|
1922
|
+
total,
|
|
1923
|
+
" items"
|
|
1924
|
+
] }) : /* @__PURE__ */ jsxs4("span", { className: "text-muted-foreground text-xs", children: [
|
|
1925
|
+
rangeStart,
|
|
1926
|
+
"\u2013",
|
|
1927
|
+
rangeEnd,
|
|
1928
|
+
" of ",
|
|
1929
|
+
total
|
|
1930
|
+
] });
|
|
1931
|
+
})() }),
|
|
1932
|
+
/* @__PURE__ */ jsxs4("div", { className: "flex flex-1 items-center justify-center gap-1", children: [
|
|
1933
|
+
/* @__PURE__ */ jsx4(
|
|
1934
|
+
"button",
|
|
1935
|
+
{
|
|
1936
|
+
onClick: () => handlePageChange(1),
|
|
1937
|
+
disabled: currentPage === 1,
|
|
1938
|
+
className: "inline-flex h-6 w-6 cursor-pointer items-center justify-center text-xs transition-colors disabled:cursor-not-allowed disabled:opacity-30",
|
|
1939
|
+
title: "First page",
|
|
1940
|
+
children: /* @__PURE__ */ jsx4(ChevronsLeft, { className: "h-3 w-3" })
|
|
1941
|
+
}
|
|
1942
|
+
),
|
|
1943
|
+
/* @__PURE__ */ jsx4(
|
|
1944
|
+
"button",
|
|
1945
|
+
{
|
|
1946
|
+
onClick: () => handlePageChange(currentPage - 1),
|
|
1947
|
+
disabled: currentPage === 1,
|
|
1948
|
+
className: "inline-flex h-6 w-6 cursor-pointer items-center justify-center text-xs transition-colors disabled:cursor-not-allowed disabled:opacity-30",
|
|
1949
|
+
title: "Previous page",
|
|
1950
|
+
children: /* @__PURE__ */ jsx4(ChevronLeft, { className: "h-3 w-3" })
|
|
1951
|
+
}
|
|
1952
|
+
),
|
|
1953
|
+
getPageNumbers().map((page) => {
|
|
1954
|
+
if (page === "ellipsis-left" || page === "ellipsis-right") {
|
|
1955
|
+
return /* @__PURE__ */ jsx4(
|
|
1956
|
+
"span",
|
|
1957
|
+
{
|
|
1958
|
+
className: "text-muted-foreground px-1 text-xs select-none",
|
|
1959
|
+
children: "..."
|
|
1960
|
+
},
|
|
1961
|
+
page
|
|
1962
|
+
);
|
|
1963
|
+
}
|
|
1964
|
+
return /* @__PURE__ */ jsx4(
|
|
1965
|
+
"button",
|
|
1966
|
+
{
|
|
1967
|
+
style: {
|
|
1968
|
+
color: page === currentPage ? accentColor : void 0
|
|
1969
|
+
},
|
|
1970
|
+
onClick: () => handlePageChange(page),
|
|
1971
|
+
className: "inline-flex h-6 min-w-6 cursor-pointer items-center justify-center rounded px-1.5 text-xs transition-colors",
|
|
1972
|
+
children: page
|
|
1973
|
+
},
|
|
1974
|
+
page
|
|
1975
|
+
);
|
|
1976
|
+
}),
|
|
1977
|
+
/* @__PURE__ */ jsx4(
|
|
1978
|
+
"button",
|
|
1979
|
+
{
|
|
1980
|
+
onClick: () => handlePageChange(currentPage + 1),
|
|
1981
|
+
disabled: currentPage === totalPages,
|
|
1982
|
+
className: "inline-flex h-6 w-6 cursor-pointer items-center justify-center text-xs transition-colors disabled:cursor-not-allowed disabled:opacity-30",
|
|
1983
|
+
title: "Next page",
|
|
1984
|
+
children: /* @__PURE__ */ jsx4(ChevronRight, { className: "h-3 w-3" })
|
|
1985
|
+
}
|
|
1986
|
+
),
|
|
1987
|
+
/* @__PURE__ */ jsx4(
|
|
1988
|
+
"button",
|
|
1989
|
+
{
|
|
1990
|
+
onClick: () => handlePageChange(totalPages),
|
|
1991
|
+
disabled: currentPage === totalPages,
|
|
1992
|
+
className: "inline-flex h-6 w-6 cursor-pointer items-center justify-center text-xs transition-colors disabled:cursor-not-allowed disabled:opacity-30",
|
|
1993
|
+
title: "Last page",
|
|
1994
|
+
children: /* @__PURE__ */ jsx4(ChevronsRight, { className: "h-3 w-3" })
|
|
1995
|
+
}
|
|
1996
|
+
)
|
|
1997
|
+
] }),
|
|
1998
|
+
/* @__PURE__ */ jsx4("div", { className: "flex flex-1 items-center justify-end gap-2", children: /* @__PURE__ */ jsx4(
|
|
1999
|
+
"select",
|
|
2000
|
+
{
|
|
2001
|
+
value: pageSize,
|
|
2002
|
+
onChange: (e) => handlePageSizeChange(Number(e.target.value)),
|
|
2003
|
+
className: "bg-background text-foreground hover:border-primary cursor-pointer rounded border px-1.5 py-0.5 text-xs",
|
|
2004
|
+
style: { height: "24px" },
|
|
2005
|
+
children: [10, 15, 20, 25, 50, 100].map((size) => /* @__PURE__ */ jsxs4("option", { value: size, children: [
|
|
2006
|
+
size,
|
|
2007
|
+
" / page"
|
|
2008
|
+
] }, size))
|
|
2009
|
+
}
|
|
2010
|
+
) })
|
|
2011
|
+
]
|
|
2012
|
+
}
|
|
2013
|
+
)
|
|
2014
|
+
]
|
|
2015
|
+
}
|
|
2016
|
+
),
|
|
2017
|
+
/* @__PURE__ */ jsx4(DragOverlay, { children: activeColumn ? /* @__PURE__ */ jsx4(
|
|
2018
|
+
"div",
|
|
2019
|
+
{
|
|
2020
|
+
className: `flex h-9 items-center truncate overflow-hidden border border-dashed shadow-md backdrop-blur ${classNames.header ?? ""} ${classNames.dragHeader ?? ""}`,
|
|
2021
|
+
style: {
|
|
2022
|
+
width: `${activeColumn.width ?? 150}px`,
|
|
2023
|
+
cursor: "grabbing",
|
|
2024
|
+
...styles.header,
|
|
2025
|
+
...styles.dragHeader
|
|
2026
|
+
},
|
|
2027
|
+
children: /* @__PURE__ */ jsxs4("div", { className: "relative z-10 flex h-full flex-1 items-center gap-1 truncate overflow-hidden px-2 font-medium", children: [
|
|
2028
|
+
/* @__PURE__ */ jsx4(GripVertical2, { className: "h-3 w-3 shrink-0" }),
|
|
2029
|
+
/* @__PURE__ */ jsx4("div", { className: "min-w-0 truncate overflow-hidden text-left text-ellipsis whitespace-nowrap select-none", children: typeof activeColumn.title === "string" ? activeColumn.title : activeColumn.key })
|
|
2030
|
+
] })
|
|
2031
|
+
}
|
|
2032
|
+
) : null })
|
|
2033
|
+
]
|
|
2034
|
+
}
|
|
2035
|
+
);
|
|
2036
|
+
}
|
|
2037
|
+
export {
|
|
2038
|
+
BoltTable,
|
|
2039
|
+
DraggableHeader_default as DraggableHeader,
|
|
2040
|
+
ResizeOverlay_default as ResizeOverlay,
|
|
2041
|
+
TableBody_default as TableBody
|
|
2042
|
+
};
|
|
2043
|
+
//# sourceMappingURL=index.mjs.map
|