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.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