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