chordia-ui 3.7.2 → 3.7.4

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.
Files changed (36) hide show
  1. package/dist/CustomFilterChips.cjs.js +1 -1
  2. package/dist/CustomFilterChips.cjs.js.map +1 -1
  3. package/dist/CustomFilterChips.es.js +239 -125
  4. package/dist/CustomFilterChips.es.js.map +1 -1
  5. package/dist/DataTable2.cjs.js +2 -0
  6. package/dist/DataTable2.cjs.js.map +1 -0
  7. package/dist/DataTable2.es.js +1863 -0
  8. package/dist/DataTable2.es.js.map +1 -0
  9. package/dist/components/UpdatedInteractionDetails.cjs.js +2 -2
  10. package/dist/components/UpdatedInteractionDetails.cjs.js.map +1 -1
  11. package/dist/components/UpdatedInteractionDetails.es.js +14 -13
  12. package/dist/components/UpdatedInteractionDetails.es.js.map +1 -1
  13. package/dist/components/data.cjs.js +1 -1
  14. package/dist/components/data.cjs.js.map +1 -1
  15. package/dist/components/data.es.js +157 -153
  16. package/dist/components/data.es.js.map +1 -1
  17. package/dist/components/performance.cjs.js +1 -1
  18. package/dist/components/performance.cjs.js.map +1 -1
  19. package/dist/components/performance.es.js +1900 -480
  20. package/dist/components/performance.es.js.map +1 -1
  21. package/dist/index.cjs.js +1 -1
  22. package/dist/index.es.js +94 -89
  23. package/dist/index.es.js.map +1 -1
  24. package/package.json +1 -1
  25. package/src/components/UpdatedInteractionDetails/UpdatedInteractionDetails.jsx +13 -13
  26. package/src/components/UpdatedInteractionDetails/UpdatedThreads.jsx +1 -0
  27. package/src/components/common/CustomFilterChips.jsx +5 -1
  28. package/src/components/common/Pagination.jsx +152 -39
  29. package/src/components/data/DataTable2.jsx +2449 -0
  30. package/src/components/data/DataTableFilters2.jsx +186 -0
  31. package/src/components/data/index.js +2 -0
  32. package/src/components/index.js +2 -2
  33. package/src/components/performance/PerformanceDetailsPage.jsx +940 -0
  34. package/src/components/performance/PerformancePanel.jsx +423 -297
  35. package/src/components/performance/SupervisorSelect.jsx +386 -0
  36. package/src/components/performance/index.js +3 -1
@@ -0,0 +1,2449 @@
1
+ "use client";
2
+
3
+ import React, { useState, useMemo, useRef, useEffect, useCallback } from "react";
4
+ import {
5
+ DndContext,
6
+ closestCenter,
7
+ KeyboardSensor,
8
+ PointerSensor,
9
+ useSensor,
10
+ useSensors,
11
+ DragOverlay,
12
+ } from "@dnd-kit/core";
13
+ import {
14
+ arrayMove,
15
+ SortableContext,
16
+ sortableKeyboardCoordinates,
17
+ useSortable,
18
+ horizontalListSortingStrategy,
19
+ } from "@dnd-kit/sortable";
20
+ import { CSS } from "@dnd-kit/utilities";
21
+ import { GripVertical, ChevronUp, ChevronDown, ListFilter, ArrowUp, ArrowDown, Settings2, Search, Check, X, ArrowUpDown } from "lucide-react";
22
+ import { createPortal } from "react-dom";
23
+ import Pagination from "../common/Pagination.jsx";
24
+ // TODO: surface column limit errors via callback prop (onMaxColumnsError)
25
+ // TODO: replace with framework-agnostic component
26
+ function OpenCloseArrow({ isOpen, iconSize }) { return null; }
27
+ // TODO: replace with framework-agnostic component
28
+ function Separator() { return <div style={{ height: 1, background: "rgba(52,58,64,0.08)" }} />; }
29
+
30
+ // ─── Typed-filter helpers ──────────────────────────────────────────────────
31
+ function parseNumberLike(s) {
32
+ if (s === null || s === undefined) return NaN;
33
+ const cleaned = String(s).replace(/[,$%]|pp/gi, "").trim();
34
+ const n = parseFloat(cleaned);
35
+ return Number.isFinite(n) ? n : NaN;
36
+ }
37
+
38
+ function parseDurationToSeconds(s) {
39
+ if (s === null || s === undefined) return NaN;
40
+ const str = String(s).trim();
41
+ // HH:MM:SS or MM:SS
42
+ const colon = str.match(/^(\d+):(\d+)(?::(\d+))?$/);
43
+ if (colon) {
44
+ const a = +colon[1], b = +colon[2], c = colon[3] != null ? +colon[3] : null;
45
+ return c != null ? a * 3600 + b * 60 + c : a * 60 + b;
46
+ }
47
+ // 1d 2h 3m 4s
48
+ let total = 0;
49
+ let matched = false;
50
+ const dM = str.match(/(\d+)\s*d\b/i);
51
+ const hM = str.match(/(\d+)\s*h\b/i);
52
+ const mM = str.match(/(\d+)\s*m(?!s)\b/i);
53
+ const sM = str.match(/(\d+)\s*s\b/i);
54
+ if (dM) { total += +dM[1] * 86400; matched = true; }
55
+ if (hM) { total += +hM[1] * 3600; matched = true; }
56
+ if (mM) { total += +mM[1] * 60; matched = true; }
57
+ if (sM) { total += +sM[1]; matched = true; }
58
+ return matched ? total : NaN;
59
+ }
60
+
61
+ function parseDateLike(s) {
62
+ if (!s) return null;
63
+ const str = String(s).trim();
64
+ // MM/DD/YYYY or M/D/YY
65
+ const slash = str.match(/^(\d{1,2})\/(\d{1,2})\/(\d{2,4})$/);
66
+ if (slash) {
67
+ const yyyy = slash[3].length === 2 ? 2000 + +slash[3] : +slash[3];
68
+ const d = new Date(yyyy, +slash[1] - 1, +slash[2]);
69
+ return isNaN(d.getTime()) ? null : d;
70
+ }
71
+ const d = new Date(str);
72
+ return isNaN(d.getTime()) ? null : d;
73
+ }
74
+
75
+ function detectFilterType(values) {
76
+ if (!values || values.length === 0) return "text";
77
+ const sample = values.slice(0, 25);
78
+ const allMatch = (test) => sample.every(test);
79
+ if (allMatch((v) => /^\d{1,2}\/\d{1,2}\/\d{2,4}$/.test(v) || /^\d{4}-\d{2}-\d{2}/.test(v))) return "date";
80
+ if (allMatch((v) => /^\d+\s*[dhms]\b/i.test(v) || /^\d+:\d+(:\d+)?$/.test(v))) return "duration";
81
+ if (allMatch((v) => /^-?[\d,.]+(%|pp)?$/i.test(v))) return "number";
82
+ return "text";
83
+ }
84
+
85
+ function isFilterActive(filter) {
86
+ if (!filter) return false;
87
+ if (Array.isArray(filter)) return filter.length > 0;
88
+ if (filter.type === "range") return filter.min !== "" && filter.min !== undefined && filter.min !== null
89
+ || filter.max !== "" && filter.max !== undefined && filter.max !== null;
90
+ if (filter.type === "durationRange") return (filter.minSec !== "" && filter.minSec != null) || (filter.maxSec !== "" && filter.maxSec != null);
91
+ if (filter.type === "dateRange") return Boolean(filter.from || filter.to);
92
+ return false;
93
+ }
94
+
95
+ function secondsToHMS(sec) {
96
+ if (sec == null || sec === "" || isNaN(sec)) return "";
97
+ const n = Number(sec);
98
+ const h = Math.floor(n / 3600);
99
+ const m = Math.floor((n % 3600) / 60);
100
+ const s = Math.floor(n % 60);
101
+ if (h > 0) return `${h}h ${m}m`;
102
+ if (m > 0) return `${m}m ${s}s`;
103
+ return `${s}s`;
104
+ }
105
+
106
+ function chipSummary(filter) {
107
+ if (!filter) return "";
108
+ if (Array.isArray(filter)) return filter.length === 0 ? "" : String(filter[0]);
109
+ if (filter.type === "range") {
110
+ const lo = filter.min !== "" && filter.min != null ? filter.min : null;
111
+ const hi = filter.max !== "" && filter.max != null ? filter.max : null;
112
+ if (lo != null && hi != null) return `${lo} – ${hi}`;
113
+ if (lo != null) return `≥ ${lo}`;
114
+ if (hi != null) return `≤ ${hi}`;
115
+ return "";
116
+ }
117
+ if (filter.type === "durationRange") {
118
+ const lo = filter.minSec !== "" && filter.minSec != null ? secondsToHMS(filter.minSec) : null;
119
+ const hi = filter.maxSec !== "" && filter.maxSec != null ? secondsToHMS(filter.maxSec) : null;
120
+ if (lo && hi) return `${lo} – ${hi}`;
121
+ if (lo) return `≥ ${lo}`;
122
+ if (hi) return `≤ ${hi}`;
123
+ return "";
124
+ }
125
+ if (filter.type === "dateRange") {
126
+ if (filter.from && filter.to) return `${filter.from} – ${filter.to}`;
127
+ if (filter.from) return `from ${filter.from}`;
128
+ if (filter.to) return `to ${filter.to}`;
129
+ return "";
130
+ }
131
+ return "";
132
+ }
133
+
134
+ function chipExtraCount(filter) {
135
+ if (Array.isArray(filter)) return Math.max(0, filter.length - 1);
136
+ return 0;
137
+ }
138
+
139
+ function chipExtraValues(filter) {
140
+ if (Array.isArray(filter)) return filter.slice(1).map(String).join(", ");
141
+ return "";
142
+ }
143
+
144
+ function ExtraValuesBadge({ count, values }) {
145
+ const [hover, setHover] = useState(false);
146
+ return (
147
+ <span
148
+ onMouseEnter={() => setHover(true)}
149
+ onMouseLeave={() => setHover(false)}
150
+ style={{
151
+ position: "relative",
152
+ display: "inline-flex",
153
+ alignItems: "center",
154
+ justifyContent: "center",
155
+ height: 22,
156
+ minWidth: 22,
157
+ padding: "0 6px",
158
+ background: "#D8D8D8",
159
+ color: "#0B0B0B",
160
+ borderRadius: 14,
161
+ fontSize: 12,
162
+ fontWeight: 500,
163
+ lineHeight: 1,
164
+ cursor: "pointer",
165
+ }}
166
+ >
167
+ +{count}
168
+ {hover && (
169
+ <span
170
+ role="tooltip"
171
+ style={{
172
+ position: "absolute",
173
+ bottom: "calc(100% + 6px)",
174
+ left: "50%",
175
+ transform: "translateX(-50%)",
176
+ background: "#1E2125",
177
+ color: "#FFFFFF",
178
+ fontFamily: "var(--font-sans)",
179
+ fontSize: 12,
180
+ fontWeight: 400,
181
+ lineHeight: "20px",
182
+ padding: "6px 10px",
183
+ borderRadius: 8,
184
+ whiteSpace: "nowrap",
185
+ pointerEvents: "none",
186
+ zIndex: 100,
187
+ boxShadow: "0 6px 16px rgba(11, 11, 11, 0.18)",
188
+ }}
189
+ >
190
+ {values}
191
+ </span>
192
+ )}
193
+ </span>
194
+ );
195
+ }
196
+
197
+ function RangeInputs({ leftLabel, rightLabel, leftValue, rightValue, onLeftChange, onRightChange, type = "number", placeholderLeft = "Min", placeholderRight = "Max" }) {
198
+ const fieldStyle = {
199
+ width: "100%",
200
+ height: 32,
201
+ borderRadius: 8,
202
+ border: "1px solid #D9D9D9",
203
+ background: "#FFFFFF",
204
+ padding: "0 10px",
205
+ fontFamily: "var(--font-sans)",
206
+ fontSize: 14,
207
+ color: "#0B0B0B",
208
+ outline: "none",
209
+ boxSizing: "border-box",
210
+ };
211
+ const labelStyle = { fontSize: 12, color: "#676767", marginBottom: 4, fontFamily: "var(--font-sans)" };
212
+ return (
213
+ <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
214
+ <div style={{ display: "flex", flexDirection: "column" }}>
215
+ <span style={labelStyle}>{leftLabel}</span>
216
+ <input
217
+ type={type}
218
+ value={leftValue}
219
+ onChange={(e) => onLeftChange(e.target.value)}
220
+ placeholder={placeholderLeft}
221
+ style={fieldStyle}
222
+ />
223
+ </div>
224
+ <div style={{ display: "flex", flexDirection: "column" }}>
225
+ <span style={labelStyle}>{rightLabel}</span>
226
+ <input
227
+ type={type}
228
+ value={rightValue}
229
+ onChange={(e) => onRightChange(e.target.value)}
230
+ placeholder={placeholderRight}
231
+ style={fieldStyle}
232
+ />
233
+ </div>
234
+ </div>
235
+ );
236
+ }
237
+
238
+ function FilterValuePicker({ column, columnId, filterType = "text", position, popoverRef, distinctValues, initialValue, onApply, onCancel }) {
239
+ const isText = filterType === "text";
240
+ const isNumber = filterType === "number";
241
+ const isDate = filterType === "date";
242
+ const isDuration = filterType === "duration";
243
+
244
+ // ── State per filterType ────────────────────────────────────────────
245
+ const [staged, setStaged] = useState(() =>
246
+ isText ? new Set(Array.isArray(initialValue) ? initialValue : []) : new Set()
247
+ );
248
+ const [query, setQuery] = useState("");
249
+ const [rangeMin, setRangeMin] = useState(() => (isNumber && initialValue?.type === "range" && initialValue.min != null ? initialValue.min : ""));
250
+ const [rangeMax, setRangeMax] = useState(() => (isNumber && initialValue?.type === "range" && initialValue.max != null ? initialValue.max : ""));
251
+ const [durMin, setDurMin] = useState(() => (isDuration && initialValue?.type === "durationRange" && initialValue.minSec != null ? initialValue.minSec : ""));
252
+ const [durMax, setDurMax] = useState(() => (isDuration && initialValue?.type === "durationRange" && initialValue.maxSec != null ? initialValue.maxSec : ""));
253
+ const [dateFrom, setDateFrom] = useState(() => (isDate && initialValue?.type === "dateRange" ? initialValue.from || "" : ""));
254
+ const [dateTo, setDateTo] = useState(() => (isDate && initialValue?.type === "dateRange" ? initialValue.to || "" : ""));
255
+
256
+ useEffect(() => {
257
+ if (isText) setStaged(new Set(Array.isArray(initialValue) ? initialValue : []));
258
+ }, [columnId, isText]);
259
+
260
+ const filteredValues = useMemo(() => {
261
+ const q = query.trim().toLowerCase();
262
+ if (!q) return distinctValues;
263
+ return distinctValues.filter((v) => String(v).toLowerCase().includes(q));
264
+ }, [distinctValues, query]);
265
+
266
+ const toggle = (value) => {
267
+ setStaged((prev) => {
268
+ const next = new Set(prev);
269
+ if (next.has(value)) next.delete(value); else next.add(value);
270
+ return next;
271
+ });
272
+ };
273
+
274
+ const reset = () => {
275
+ if (isText) setStaged(new Set());
276
+ if (isNumber) { setRangeMin(""); setRangeMax(""); }
277
+ if (isDuration) { setDurMin(""); setDurMax(""); }
278
+ if (isDate) { setDateFrom(""); setDateTo(""); }
279
+ setQuery("");
280
+ };
281
+
282
+ const apply = () => {
283
+ if (isText) return onApply(Array.from(staged));
284
+ if (isNumber) return onApply({ type: "range", min: rangeMin === "" ? "" : Number(rangeMin), max: rangeMax === "" ? "" : Number(rangeMax) });
285
+ if (isDuration) return onApply({ type: "durationRange", minSec: durMin === "" ? "" : Number(durMin), maxSec: durMax === "" ? "" : Number(durMax) });
286
+ if (isDate) return onApply({ type: "dateRange", from: dateFrom, to: dateTo });
287
+ };
288
+
289
+ return (
290
+ <div
291
+ ref={popoverRef}
292
+ role="dialog"
293
+ aria-label={`Filter ${column.label || columnId}`}
294
+ style={{
295
+ position: "fixed",
296
+ top: position.top,
297
+ left: position.left,
298
+ zIndex: 50,
299
+ width: isText ? 240 : 280,
300
+ background: "#FFFFFF",
301
+ border: "1px solid #E6E6E6",
302
+ borderRadius: 10,
303
+ padding: 12,
304
+ display: "flex",
305
+ flexDirection: "column",
306
+ gap: 10,
307
+ boxShadow: "0 8px 24px rgba(11, 11, 11, 0.10)",
308
+ fontFamily: "var(--font-sans)",
309
+ }}
310
+ >
311
+ {isText && (
312
+ <>
313
+ <div
314
+ style={{
315
+ background: "#D8D8D8",
316
+ height: 32,
317
+ borderRadius: 10,
318
+ padding: 1,
319
+ display: "flex",
320
+ alignItems: "center",
321
+ }}
322
+ >
323
+ <div
324
+ style={{
325
+ background: "#FFFFFF",
326
+ flex: 1,
327
+ height: 30,
328
+ borderRadius: 10,
329
+ display: "flex",
330
+ alignItems: "center",
331
+ gap: 8,
332
+ padding: "0 8px",
333
+ }}
334
+ >
335
+ <Search size={16} color="#989898" strokeWidth={2} />
336
+ <input
337
+ autoFocus
338
+ type="text"
339
+ value={query}
340
+ onChange={(e) => setQuery(e.target.value)}
341
+ placeholder="Search"
342
+ style={{
343
+ flex: 1,
344
+ border: "none",
345
+ outline: "none",
346
+ background: "transparent",
347
+ fontSize: 14,
348
+ color: "#0B0B0B",
349
+ minWidth: 0,
350
+ fontFamily: "var(--font-sans)",
351
+ }}
352
+ />
353
+ </div>
354
+ </div>
355
+
356
+ <div style={{ maxHeight: 220, overflowY: "auto", display: "flex", flexDirection: "column", gap: 4 }}>
357
+ {filteredValues.length === 0 ? (
358
+ <div style={{ fontSize: 13, color: "#676767", padding: "8px 4px", textAlign: "center" }}>
359
+ No values
360
+ </div>
361
+ ) : (
362
+ filteredValues.map((value) => {
363
+ const checked = staged.has(value);
364
+ return (
365
+ <label
366
+ key={value}
367
+ style={{
368
+ display: "flex",
369
+ alignItems: "center",
370
+ gap: 12,
371
+ padding: "6px 4px",
372
+ cursor: "pointer",
373
+ borderRadius: 6,
374
+ }}
375
+ >
376
+ <span
377
+ style={{
378
+ width: 20,
379
+ height: 20,
380
+ borderRadius: 4,
381
+ display: "inline-flex",
382
+ alignItems: "center",
383
+ justifyContent: "center",
384
+ background: checked ? "#0B0B0B" : "#FFFFFF",
385
+ border: `1px solid ${checked ? "#0B0B0B" : "#676767"}`,
386
+ flexShrink: 0,
387
+ }}
388
+ >
389
+ {checked && <Check size={14} color="#FFFFFF" strokeWidth={3} />}
390
+ </span>
391
+ <input
392
+ type="checkbox"
393
+ checked={checked}
394
+ onChange={() => toggle(value)}
395
+ style={{ position: "absolute", opacity: 0, pointerEvents: "none" }}
396
+ tabIndex={-1}
397
+ />
398
+ <span
399
+ onClick={(e) => { e.preventDefault(); toggle(value); }}
400
+ style={{
401
+ fontSize: 14,
402
+ lineHeight: "20px",
403
+ color: "#0B0B0B",
404
+ flex: 1,
405
+ overflow: "hidden",
406
+ textOverflow: "ellipsis",
407
+ whiteSpace: "nowrap",
408
+ }}
409
+ >
410
+ {value}
411
+ </span>
412
+ </label>
413
+ );
414
+ })
415
+ )}
416
+ </div>
417
+ </>
418
+ )}
419
+
420
+ {isNumber && (
421
+ <RangeInputs
422
+ leftLabel="Min"
423
+ rightLabel="Max"
424
+ leftValue={rangeMin}
425
+ rightValue={rangeMax}
426
+ onLeftChange={setRangeMin}
427
+ onRightChange={setRangeMax}
428
+ type="number"
429
+ />
430
+ )}
431
+
432
+ {isDuration && (
433
+ <>
434
+ <div style={{ fontSize: 12, color: "#676767" }}>Duration in seconds</div>
435
+ <RangeInputs
436
+ leftLabel="Min (sec)"
437
+ rightLabel="Max (sec)"
438
+ leftValue={durMin}
439
+ rightValue={durMax}
440
+ onLeftChange={setDurMin}
441
+ onRightChange={setDurMax}
442
+ type="number"
443
+ placeholderLeft="0"
444
+ placeholderRight="3600"
445
+ />
446
+ </>
447
+ )}
448
+
449
+ {isDate && (
450
+ <RangeInputs
451
+ leftLabel="From"
452
+ rightLabel="To"
453
+ leftValue={dateFrom}
454
+ rightValue={dateTo}
455
+ onLeftChange={setDateFrom}
456
+ onRightChange={setDateTo}
457
+ type="date"
458
+ placeholderLeft=""
459
+ placeholderRight=""
460
+ />
461
+ )}
462
+
463
+ <div style={{ height: 1, background: "#D9D9D9", margin: "0 -12px" }} />
464
+ <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
465
+ <button
466
+ type="button"
467
+ onClick={reset}
468
+ style={{
469
+ background: "transparent",
470
+ border: "none",
471
+ padding: 0,
472
+ cursor: "pointer",
473
+ fontSize: 14,
474
+ fontWeight: 500,
475
+ color: "#323232",
476
+ fontFamily: "var(--font-sans)",
477
+ }}
478
+ >
479
+ Reset
480
+ </button>
481
+ <button
482
+ type="button"
483
+ onClick={apply}
484
+ style={{
485
+ background: "#0B0B0B",
486
+ color: "#FFFFFF",
487
+ border: "none",
488
+ borderRadius: 10,
489
+ height: 28,
490
+ padding: "0 14px",
491
+ fontSize: 14,
492
+ fontWeight: 600,
493
+ cursor: "pointer",
494
+ fontFamily: "var(--font-sans)",
495
+ }}
496
+ >
497
+ Apply
498
+ </button>
499
+ </div>
500
+ </div>
501
+ );
502
+ }
503
+ function ResizeColumnsIcon({ size = 10, color = "currentColor" }) {
504
+ return (
505
+ <svg
506
+ width={size}
507
+ height={size}
508
+ viewBox="0 0 16 16"
509
+ fill="none"
510
+ xmlns="http://www.w3.org/2000/svg"
511
+ style={{ display: "block" }}
512
+ aria-hidden="true"
513
+ >
514
+ <path d="M3.5 8H12.5" stroke={color} strokeWidth="1.4" strokeLinecap="round" />
515
+ <path d="M5 6.5L3.5 8L5 9.5" stroke={color} strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
516
+ <path d="M11 6.5L12.5 8L11 9.5" stroke={color} strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
517
+ <path d="M6.8 3V13" stroke={color} strokeWidth="1.2" strokeLinecap="round" />
518
+ <path d="M9.2 3V13" stroke={color} strokeWidth="1.2" strokeLinecap="round" />
519
+ </svg>
520
+ );
521
+ }
522
+ // TODO: replace with framework-agnostic context
523
+ const useUserContext = () => ({ userData: { email: "anonymous" } });
524
+ // TODO: replace with framework-agnostic image
525
+ const Image = (props) => <img {...props} />;
526
+ import DataTableFilters from "./DataTableFilters";
527
+ // TODO: replace with framework-agnostic tooltip
528
+ function HoverBalloon({
529
+ children,
530
+ content,
531
+ direction = "top",
532
+ align = "center",
533
+ styling,
534
+ indicatorColor,
535
+ }) {
536
+ const tooltipText =
537
+ typeof content === "string" ? content : content != null ? String(content) : "";
538
+ const resolvedArrowColor =
539
+ typeof indicatorColor === "string" &&
540
+ (indicatorColor.startsWith("#") ||
541
+ indicatorColor.startsWith("rgb") ||
542
+ indicatorColor.startsWith("hsl") ||
543
+ indicatorColor.startsWith("var("))
544
+ ? indicatorColor
545
+ : "var(--Grey-Strong, #2E3236)";
546
+ const [isVisible, setIsVisible] = useState(false);
547
+ const [mounted, setMounted] = useState(false);
548
+ const [position, setPosition] = useState({ top: 0, left: 0, arrowLeft: 0, placement: "top" });
549
+ const triggerRef = useRef(null);
550
+ const tooltipRef = useRef(null);
551
+
552
+ if (!tooltipText) return children;
553
+
554
+ useEffect(() => {
555
+ setMounted(true);
556
+ }, []);
557
+
558
+ const updatePosition = useCallback(() => {
559
+ if (!triggerRef.current || !tooltipRef.current || typeof window === "undefined") return;
560
+
561
+ const triggerRect = triggerRef.current.getBoundingClientRect();
562
+ const tooltipRect = tooltipRef.current.getBoundingClientRect();
563
+ const viewportWidth = window.innerWidth;
564
+ const viewportHeight = window.innerHeight;
565
+
566
+ const viewportPadding = 8;
567
+ const gap = 10;
568
+ const tooltipWidth = tooltipRect.width;
569
+ const tooltipHeight = tooltipRect.height;
570
+
571
+ const spaceAbove = triggerRect.top;
572
+ const spaceBelow = viewportHeight - triggerRect.bottom;
573
+ const preferredPlacement = direction === "bottom" ? "bottom" : "top";
574
+
575
+ let placement = preferredPlacement;
576
+ if (preferredPlacement === "top" && spaceAbove < tooltipHeight + gap + viewportPadding && spaceBelow > spaceAbove) {
577
+ placement = "bottom";
578
+ } else if (preferredPlacement === "bottom" && spaceBelow < tooltipHeight + gap + viewportPadding && spaceAbove > spaceBelow) {
579
+ placement = "top";
580
+ }
581
+
582
+ let anchorX = triggerRect.left + triggerRect.width / 2;
583
+ if (align === "start") anchorX = triggerRect.left;
584
+ if (align === "end") anchorX = triggerRect.right;
585
+
586
+ let top = placement === "top"
587
+ ? triggerRect.top - tooltipHeight - gap
588
+ : triggerRect.bottom + gap;
589
+ let left = align === "start"
590
+ ? anchorX
591
+ : align === "end"
592
+ ? anchorX - tooltipWidth
593
+ : anchorX - tooltipWidth / 2;
594
+
595
+ top = Math.max(viewportPadding, Math.min(top, viewportHeight - tooltipHeight - viewportPadding));
596
+ left = Math.max(viewportPadding, Math.min(left, viewportWidth - tooltipWidth - viewportPadding));
597
+
598
+ const arrowLeft = Math.max(10, Math.min(anchorX - left, tooltipWidth - 10));
599
+ setPosition({ top, left, arrowLeft, placement });
600
+ }, [align, direction]);
601
+
602
+ useEffect(() => {
603
+ if (!isVisible || !mounted) return;
604
+
605
+ updatePosition();
606
+
607
+ const handleReposition = () => updatePosition();
608
+ window.addEventListener("resize", handleReposition);
609
+ window.addEventListener("scroll", handleReposition, true);
610
+
611
+ return () => {
612
+ window.removeEventListener("resize", handleReposition);
613
+ window.removeEventListener("scroll", handleReposition, true);
614
+ };
615
+ }, [isVisible, mounted, updatePosition]);
616
+
617
+ return (
618
+ <span
619
+ ref={triggerRef}
620
+ style={{ display: "block", width: "100%" }}
621
+ onMouseEnter={() => setIsVisible(true)}
622
+ onMouseLeave={() => setIsVisible(false)}
623
+ >
624
+ {children}
625
+ {isVisible && mounted && createPortal(
626
+ <span
627
+ ref={tooltipRef}
628
+ style={{
629
+ position: "fixed",
630
+ top: `${position.top}px`,
631
+ left: `${position.left}px`,
632
+ background: "var(--Grey-Strong, #2E3236)",
633
+ color: "var(--Grey-White, #FFF)",
634
+ fontSize: "12px",
635
+ lineHeight: 1.3,
636
+ fontWeight: 500,
637
+ padding: "8px 10px",
638
+ borderRadius: "6px",
639
+ whiteSpace: "normal",
640
+ maxWidth: "320px",
641
+ width: "max-content",
642
+ boxShadow: "0 6px 18px rgba(0, 0, 0, 0.2)",
643
+ zIndex: 99999,
644
+ pointerEvents: "none",
645
+ ...(typeof styling === "object" ? styling : {}),
646
+ }}
647
+ >
648
+ {tooltipText}
649
+ <span
650
+ style={{
651
+ position: "absolute",
652
+ left: `${position.arrowLeft}px`,
653
+ transform: "translateX(-50%)",
654
+ width: 0,
655
+ height: 0,
656
+ borderLeft: "6px solid transparent",
657
+ borderRight: "6px solid transparent",
658
+ ...(position.placement === "top"
659
+ ? {
660
+ top: "100%",
661
+ borderTop: `7px solid ${resolvedArrowColor}`,
662
+ }
663
+ : {
664
+ bottom: "100%",
665
+ borderBottom: `7px solid ${resolvedArrowColor}`,
666
+ }),
667
+ }}
668
+ />
669
+ </span>,
670
+ document.body
671
+ )}
672
+ </span>
673
+ );
674
+ }
675
+
676
+ /**
677
+ * DataTable Component
678
+ * Interactive table with column reordering, filtering, sorting, sticky headers, and pagination
679
+ * Follows "No Judgment" principle - scores and counts displayed uniformly
680
+ */
681
+ export default function DataTable2({
682
+ data = [],
683
+ columns = [],
684
+ initialPageSize = 10,
685
+ onRowClick,
686
+ // Server-side pagination props (optional)
687
+ totalCount = null, // Total count from server (null = use client-side pagination)
688
+ page: controlledPage = null, // Controlled page (null = use internal state)
689
+ pageSize: controlledPageSize = null, // Controlled pageSize (null = use internal state)
690
+ onPageChange = null, // Callback for page changes (null = use internal state)
691
+ onPageSizeChange = null, // Callback for pageSize changes (null = use internal state)
692
+ onFilterChange = null, // Callback for filter changes (null = use client-side filtering)
693
+ columnFilters = null, // Controlled inline header filters (columnId -> value). Used to rehydrate inputs in server-side filtering mode.
694
+ // Optional callback to surface column limit errors to host app
695
+ onMaxColumnsError = null,
696
+ // Server-side sorting props (optional)
697
+ onSort = null, // Callback for sort changes (null = use client-side sorting)
698
+ sortFields = [], // Array of [field, direction] tuples from parent (null = use internal state)
699
+ tableId = null, // Optional unique identifier for localStorage persistence (e.g., "history", "agents")
700
+ isLoading = false, // Loading state to show overlay inside table
701
+ // Filter props (optional)
702
+ filtersConfig = null, // Top toolbar filter config (chips/date/export). Note: this is different from `columnFilters` for inline header inputs.
703
+ // Column resize (optional) — min/max in px; columns may override with column.minWidth / column.maxWidth
704
+ columnResizeMinWidth = 100,
705
+ columnResizeMaxWidth = 280,
706
+ }) {
707
+ // Get user email for user-specific storage
708
+ const { userData } = useUserContext();
709
+ const userId = userData?.email || 'anonymous';
710
+
711
+ // Component to show tooltip only when text is actually truncated
712
+ const TruncatedCell = React.memo(({ children, content, className = "" }) => {
713
+ const textRef = useRef(null);
714
+ const [isTruncated, setIsTruncated] = useState(false);
715
+
716
+ useEffect(() => {
717
+ const checkTruncation = () => {
718
+ if (textRef.current) {
719
+ const isOverflowing = textRef.current.scrollWidth > textRef.current.clientWidth;
720
+ setIsTruncated(isOverflowing);
721
+ }
722
+ };
723
+
724
+ checkTruncation();
725
+ const timeoutId = setTimeout(checkTruncation, 0);
726
+
727
+ window.addEventListener('resize', checkTruncation);
728
+ return () => {
729
+ clearTimeout(timeoutId);
730
+ window.removeEventListener('resize', checkTruncation);
731
+ };
732
+ }, [children, content]);
733
+
734
+ const textElement = (
735
+ <span className={`truncate block ${className}`} ref={textRef}>
736
+ {children}
737
+ </span>
738
+ );
739
+
740
+ if (isTruncated && content) {
741
+ return (
742
+ <HoverBalloon
743
+ content={content}
744
+ styling="bg-green-2 text-gray-900 px-3 py-2 text-sm rounded-lg shadow-md border border-gray-300 max-w-xs whitespace-normal"
745
+ indicatorColor="bg-green-2"
746
+ direction="top"
747
+ >
748
+ {textElement}
749
+ </HoverBalloon>
750
+ );
751
+ }
752
+
753
+ return textElement;
754
+ });
755
+ TruncatedCell.displayName = 'TruncatedCell';
756
+
757
+ // Use controlled pagination if props provided, otherwise use internal state
758
+ const isServerSidePagination = totalCount !== null && onPageChange !== null;
759
+ // Use server-side filtering if onFilterChange is provided
760
+ const isServerSideFiltering = onFilterChange !== null;
761
+ // Use server-side sorting if onSort is provided
762
+ const isServerSideSorting = onSort !== null;
763
+ const [internalPage, setInternalPage] = useState(1);
764
+ const [internalPageSize, setInternalPageSize] = useState(initialPageSize);
765
+
766
+ const page = controlledPage !== null ? controlledPage : internalPage;
767
+ const pageSize = controlledPageSize !== null ? controlledPageSize : internalPageSize;
768
+
769
+ const setPage = (newPage) => {
770
+ if (onPageChange) {
771
+ onPageChange(newPage);
772
+ } else {
773
+ setInternalPage(newPage);
774
+ }
775
+ };
776
+
777
+ const setPageSize = (newSize) => {
778
+ if (onPageSizeChange) {
779
+ onPageSizeChange(newSize);
780
+ } else {
781
+ setInternalPageSize(newSize);
782
+ }
783
+ };
784
+
785
+ const MAX_COLUMNS = 50;
786
+
787
+ const ACTION_COLUMN_ID = 'action';
788
+
789
+ // Refs for localStorage persistence
790
+ const userHasManuallyChangedColumns = useRef(false);
791
+
792
+ // Generate unique storage key for this table (includes userId)
793
+ const storageKey = useMemo(() => {
794
+ if (tableId) {
795
+ return `dataTable_columns_${tableId}:${userId}`;
796
+ }
797
+ // Fallback: use column keys to create unique identifier
798
+ const columnKeys = columns.map(col => (col.id || col.key)).sort().join('_');
799
+ return `dataTable_columns_${columnKeys}:${userId}`;
800
+ }, [tableId, columns, userId]);
801
+
802
+ const manualChangeFlagKey = useMemo(() => {
803
+ if (tableId) {
804
+ return `dataTable_manual_change_${tableId}:${userId}`;
805
+ }
806
+ return null;
807
+ }, [tableId, userId]);
808
+
809
+ // Load saved column visibility from localStorage
810
+ const loadSavedColumns = useCallback((key, cols) => {
811
+ if (typeof window === 'undefined' || !key || !cols || cols.length === 0) return null;
812
+ try {
813
+ const saved = localStorage.getItem(key);
814
+ if (saved) {
815
+ const savedColumns = JSON.parse(saved);
816
+ // Validate that saved columns still exist in current columns
817
+ const validColumns = savedColumns.filter(colId =>
818
+ cols.some(col => (col.id || col.key) === colId)
819
+ );
820
+ if (validColumns.length > 0) {
821
+ return validColumns;
822
+ }
823
+ }
824
+ } catch (error) {
825
+ console.warn('Failed to load saved columns from localStorage:', error);
826
+ }
827
+ return null;
828
+ }, []);
829
+
830
+ // filters: { [columnId]: string[] } — array of selected values per column
831
+ const [filters, setFilters] = useState({});
832
+ // Free-text global search applied to all visible string-cells
833
+ const [globalQuery, setGlobalQuery] = useState("");
834
+ // Which column header has its filter popover open (columnId or null)
835
+ const [openFilterColumnId, setOpenFilterColumnId] = useState(null);
836
+ const [filterPopoverPosition, setFilterPopoverPosition] = useState({ top: 0, left: 0 });
837
+ const filterPopoverRef = useRef(null);
838
+ const filterIconRefs = useRef({});
839
+ // Internal sorting state (only used for client-side sorting)
840
+ const [internalSortField, setInternalSortField] = useState(null);
841
+ const [internalSortDirection, setInternalSortDirection] = useState("asc");
842
+
843
+ const setColumnFilter = useCallback((columnId, value) => {
844
+ const active = isFilterActive(value);
845
+ setFilters((prev) => {
846
+ const next = { ...prev };
847
+ if (!active) delete next[columnId];
848
+ else next[columnId] = value;
849
+ return next;
850
+ });
851
+ if (onPageChange) onPageChange(1); else setInternalPage(1);
852
+ if (isServerSideFiltering && onFilterChange) {
853
+ onFilterChange(
854
+ active
855
+ ? { ...filters, [columnId]: value }
856
+ : Object.fromEntries(Object.entries(filters).filter(([k]) => k !== columnId))
857
+ );
858
+ }
859
+ }, [filters, onFilterChange, isServerSideFiltering, onPageChange]);
860
+
861
+ const clearAllFilters = useCallback(() => {
862
+ setFilters({});
863
+ setGlobalQuery("");
864
+ if (onPageChange) onPageChange(1); else setInternalPage(1);
865
+ if (isServerSideFiltering && onFilterChange) onFilterChange({});
866
+ }, [onFilterChange, isServerSideFiltering, onPageChange]);
867
+
868
+ const getDistinctValuesForColumn = useCallback((columnId) => {
869
+ if (!data || data.length === 0) return [];
870
+ const seen = new Set();
871
+ const out = [];
872
+ for (const row of data) {
873
+ const v = row[columnId];
874
+ if (v === null || v === undefined) continue;
875
+ const s = typeof v === "object" ? JSON.stringify(v) : String(v);
876
+ if (!seen.has(s)) {
877
+ seen.add(s);
878
+ out.push(s);
879
+ }
880
+ }
881
+ return out.sort((a, b) => a.localeCompare(b));
882
+ }, [data]);
883
+
884
+ const getColumnFilterType = useCallback((columnId) => {
885
+ const col = columns.find((c) => getColumnId(c) === columnId);
886
+ if (col && col.filterType) return col.filterType;
887
+ return detectFilterType(getDistinctValuesForColumn(columnId));
888
+ }, [columns, getDistinctValuesForColumn]);
889
+
890
+ const openFilterPopoverFor = useCallback((columnId, anchorEl) => {
891
+ if (!anchorEl) {
892
+ setOpenFilterColumnId(columnId);
893
+ return;
894
+ }
895
+ const rect = anchorEl.getBoundingClientRect();
896
+ setFilterPopoverPosition({ top: rect.bottom + 6, left: rect.left });
897
+ setOpenFilterColumnId((prev) => (prev === columnId ? null : columnId));
898
+ }, []);
899
+
900
+ // Outside click closes filter popover
901
+ useEffect(() => {
902
+ if (!openFilterColumnId) return;
903
+ const onMouseDown = (e) => {
904
+ if (filterPopoverRef.current && filterPopoverRef.current.contains(e.target)) return;
905
+ const anchor = filterIconRefs.current[openFilterColumnId];
906
+ if (anchor && anchor.contains(e.target)) return;
907
+ setOpenFilterColumnId(null);
908
+ };
909
+ document.addEventListener("mousedown", onMouseDown);
910
+ return () => document.removeEventListener("mousedown", onMouseDown);
911
+ }, [openFilterColumnId]);
912
+
913
+ // Normalize inline filters by removing empty/null values.
914
+ // Example usage (server-side mode):
915
+ // <DataTable onFilterChange={handleFilterChange} columnFilters={dataTableFilters} />
916
+ const normalizeColumnFilters = useCallback((incomingFilters) => {
917
+ if (!incomingFilters || typeof incomingFilters !== "object") return {};
918
+ return Object.fromEntries(
919
+ Object.entries(incomingFilters).filter(([, value]) => {
920
+ if (value === null || value === undefined) return false;
921
+ if (typeof value === "string") return value.trim() !== "";
922
+ return true; // allow numbers/booleans
923
+ })
924
+ );
925
+ }, []);
926
+
927
+ const areFiltersEqual = useCallback((a, b) => {
928
+ const aKeys = Object.keys(a || {});
929
+ const bKeys = Object.keys(b || {});
930
+ if (aKeys.length !== bKeys.length) return false;
931
+ return aKeys.every((key) => a[key] === b[key]);
932
+ }, []);
933
+
934
+ // Sync internal inline filter input UI from controlled prop in server-side mode.
935
+ // This does not trigger onFilterChange and therefore does not create callback loops.
936
+ useEffect(() => {
937
+ if (!isServerSideFiltering || columnFilters === null) return;
938
+ const normalizedFilters = normalizeColumnFilters(columnFilters);
939
+ setFilters((prev) => (areFiltersEqual(prev, normalizedFilters) ? prev : normalizedFilters));
940
+ }, [isServerSideFiltering, columnFilters, normalizeColumnFilters, areFiltersEqual]);
941
+
942
+ // Get current sort state (from props if server-side, otherwise from internal state)
943
+ // For server-side sorting, we need to map the sortField back to the column key
944
+ // because History.js's handleSort maps some keys (e.g., "evaluation.csat_score" -> "csat_score")
945
+ // but we need to compare against the original column key
946
+ const getSortFieldForComparison = useCallback((sortFieldFromProps, columnId) => {
947
+ // Reverse mapping: if sortField is "csat_score", it should match column "evaluation.csat_score"
948
+ if (sortFieldFromProps === "csat_score" && columnId === "evaluation.csat_score") {
949
+ return columnId;
950
+ }
951
+ // For other fields, use as-is (they should match)
952
+ return sortFieldFromProps;
953
+ }, []);
954
+
955
+ const sortField = isServerSideSorting && sortFields.length > 0 ? sortFields[0][0] : internalSortField;
956
+ const sortDirection = isServerSideSorting && sortFields.length > 0 ? sortFields[0][1] : internalSortDirection;
957
+ // Support both 'id' and 'key' for column identifiers
958
+ const [columnOrder, setColumnOrder] = useState(() =>
959
+ columns.map((col) => col.id || col.key)
960
+ );
961
+ const enforceActionPosition = useCallback((cols) => {
962
+ if (!cols || cols.length === 0) return cols;
963
+
964
+ // De-duplicate and enforce max column limit first
965
+ let result = Array.from(new Set(cols));
966
+ if (result.length > MAX_COLUMNS) {
967
+ result = result.slice(0, MAX_COLUMNS);
968
+ }
969
+
970
+ const allColumnIds = columns.map((col) => col.id || col.key);
971
+ const tableHasActionColumn = allColumnIds.includes(ACTION_COLUMN_ID);
972
+ const hasAction = result.includes(ACTION_COLUMN_ID);
973
+
974
+ // If the table defines an action column, always include it in the visible set,
975
+ // counting toward the MAX_COLUMNS cap.
976
+ if (!hasAction && tableHasActionColumn) {
977
+ if (result.length >= MAX_COLUMNS) {
978
+ // Replace the last non-action entry with the action column
979
+ result = [...result.slice(0, MAX_COLUMNS - 1), ACTION_COLUMN_ID];
980
+ } else {
981
+ result = [...result, ACTION_COLUMN_ID];
982
+ }
983
+ }
984
+
985
+ // Ensure the action column is always rendered last
986
+ const withoutAction = result.filter((id) => id !== ACTION_COLUMN_ID);
987
+ return [...withoutAction, ACTION_COLUMN_ID];
988
+ }, [columns]);
989
+
990
+ // Column selection state - load persisted columns if they exist, otherwise default to first 9
991
+ const [visibleColumns, setVisibleColumns] = useState(() => {
992
+ // Try to load persisted columns immediately on mount
993
+ if (typeof window !== 'undefined' && tableId) {
994
+ try {
995
+ // Check if user has manually changed columns before
996
+ const hasManualChange = localStorage.getItem(`dataTable_manual_change_${tableId}:${userId}`) === 'true';
997
+
998
+ if (hasManualChange) {
999
+ // User has manually changed columns, load persisted selection immediately
1000
+ const saved = localStorage.getItem(`dataTable_columns_${tableId}:${userId}`);
1001
+ if (saved) {
1002
+ const savedColumns = JSON.parse(saved);
1003
+ if (Array.isArray(savedColumns) && savedColumns.length > 0) {
1004
+ userHasManuallyChangedColumns.current = true;
1005
+ // Validate saved columns exist in current columns
1006
+ const validColumns = savedColumns.filter(colId =>
1007
+ columns.some(col => (col.id || col.key) === colId)
1008
+ );
1009
+ if (validColumns.length > 0) {
1010
+ return validColumns.slice(0, MAX_COLUMNS);
1011
+ }
1012
+ }
1013
+ }
1014
+ }
1015
+ } catch (error) {
1016
+ console.warn('Failed to load saved columns from localStorage:', error);
1017
+ }
1018
+ }
1019
+
1020
+ // If no persisted columns, default to first 9 columns
1021
+ const columnIds = columns.map((col) => col.id || col.key);
1022
+ const initial = columnIds.slice(0, MAX_COLUMNS);
1023
+ return enforceActionPosition(initial);
1024
+ });
1025
+ const [isColumnDropdownOpen, setIsColumnDropdownOpen] = useState(false);
1026
+ const [columnDropdownPosition, setColumnDropdownPosition] = useState({ top: 0, left: 0, width: 0 });
1027
+ const [pendingSelection, setPendingSelection] = useState(new Set());
1028
+ const [pendingDeselection, setPendingDeselection] = useState(new Set());
1029
+ // Snapshot of the columns visible on first mount; used as the Reset target
1030
+ const initialVisibleColumnsRef = useRef(null);
1031
+ if (initialVisibleColumnsRef.current === null) {
1032
+ initialVisibleColumnsRef.current = [...visibleColumns];
1033
+ }
1034
+ const [draggedColumnIndex, setDraggedColumnIndex] = useState(null);
1035
+ const [dragOverColumnIndex, setDragOverColumnIndex] = useState(null);
1036
+ const columnDropdownRef = useRef(null);
1037
+ const columnDropdownMenuRef = useRef(null);
1038
+ const toastTimeoutRef = useRef(null);
1039
+ const [activeId, setActiveId] = useState(null);
1040
+ const [columnWidths, setColumnWidths] = useState({});
1041
+ const tableRef = useRef(null);
1042
+ const resizeStateRef = useRef(null);
1043
+
1044
+ // Helper to get column identifier (id or key)
1045
+ const getColumnId = (col) => col.id || col.key;
1046
+ const toPixelWidth = useCallback((value) => {
1047
+ if (typeof value === "number" && Number.isFinite(value)) return value;
1048
+ if (typeof value === "string") {
1049
+ const trimmed = value.trim();
1050
+ if (trimmed.endsWith("px")) {
1051
+ const parsed = parseFloat(trimmed);
1052
+ return Number.isFinite(parsed) ? parsed : null;
1053
+ }
1054
+ const parsed = parseFloat(trimmed);
1055
+ return Number.isFinite(parsed) && `${parsed}` === trimmed ? parsed : null;
1056
+ }
1057
+ return null;
1058
+ }, []);
1059
+ const getResolvedColumnWidth = useCallback((columnId, fallbackWidth) => {
1060
+ if (columnWidths[columnId] != null) return `${columnWidths[columnId]}px`;
1061
+ if (typeof fallbackWidth === "number") return `${fallbackWidth}px`;
1062
+ return fallbackWidth || "auto";
1063
+ }, [columnWidths]);
1064
+
1065
+ const handleResizeMove = useCallback((event) => {
1066
+ if (!resizeStateRef.current) return;
1067
+ const { columnId, startX, startWidth, minWidth, maxWidth } = resizeStateRef.current;
1068
+ const delta = event.clientX - startX;
1069
+ const nextWidth = Math.round(startWidth + delta);
1070
+ const clamped = Math.min(maxWidth, Math.max(minWidth, nextWidth));
1071
+ setColumnWidths((prev) => ({ ...prev, [columnId]: clamped }));
1072
+ }, []);
1073
+
1074
+ const stopResize = useCallback(() => {
1075
+ resizeStateRef.current = null;
1076
+ document.body.style.cursor = "";
1077
+ document.body.style.userSelect = "";
1078
+ document.removeEventListener("mousemove", handleResizeMove);
1079
+ document.removeEventListener("mouseup", stopResize);
1080
+ }, [handleResizeMove]);
1081
+
1082
+ const startResize = useCallback((event, column, currentWidth) => {
1083
+ event.preventDefault();
1084
+ event.stopPropagation();
1085
+ const columnId = column.id || column.key;
1086
+ const colMin = toPixelWidth(column.minWidth) ?? columnResizeMinWidth;
1087
+ const colMax = toPixelWidth(column.maxWidth) ?? columnResizeMaxWidth;
1088
+ const minWidth = Math.min(colMin, colMax);
1089
+ const maxWidth = Math.max(colMin, colMax);
1090
+ const widthFromDom = event.currentTarget.closest("th")?.getBoundingClientRect().width || 0;
1091
+ const parsed = toPixelWidth(currentWidth);
1092
+ const base =
1093
+ parsed != null
1094
+ ? parsed
1095
+ : Math.max(minWidth, Math.round(widthFromDom) || minWidth);
1096
+ const startWidth = Math.min(maxWidth, Math.max(minWidth, base));
1097
+ resizeStateRef.current = { columnId, startX: event.clientX, startWidth, minWidth, maxWidth };
1098
+ document.body.style.cursor = "col-resize";
1099
+ document.body.style.userSelect = "none";
1100
+ document.addEventListener("mousemove", handleResizeMove);
1101
+ document.addEventListener("mouseup", stopResize);
1102
+ }, [columnResizeMaxWidth, columnResizeMinWidth, handleResizeMove, stopResize, toPixelWidth]);
1103
+
1104
+ useEffect(() => () => stopResize(), [stopResize]);
1105
+
1106
+ const sensors = useSensors(
1107
+ useSensor(PointerSensor),
1108
+ useSensor(KeyboardSensor, {
1109
+ coordinateGetter: sortableKeyboardCoordinates,
1110
+ })
1111
+ );
1112
+
1113
+ // Handle column drag end (table-level drag and drop)
1114
+ const handleDragEnd = (event) => {
1115
+ const { active, over } = event;
1116
+ if (over && active.id !== over.id) {
1117
+ if (active.id === ACTION_COLUMN_ID || over.id === ACTION_COLUMN_ID) {
1118
+ showMaxColumnsError("Cannot drop columns on the Action column");
1119
+ setActiveId(null);
1120
+ return;
1121
+ }
1122
+ // Mark that user has manually changed columns
1123
+ userHasManuallyChangedColumns.current = true;
1124
+
1125
+ // Update visibleColumns order when dragging in table header
1126
+ setVisibleColumns((prev) => {
1127
+ const oldIndex = prev.indexOf(active.id);
1128
+ const newIndex = prev.indexOf(over.id);
1129
+ if (oldIndex !== -1 && newIndex !== -1) {
1130
+ const reordered = arrayMove(prev, oldIndex, newIndex);
1131
+ const constrained = enforceActionPosition(reordered);
1132
+ // Save immediately after drag
1133
+ saveColumnsToStorage(constrained);
1134
+ return constrained;
1135
+ }
1136
+ return prev;
1137
+ });
1138
+
1139
+ // Also update columnOrder for consistency
1140
+ setColumnOrder((items) => {
1141
+ const oldIndex = items.indexOf(active.id);
1142
+ const newIndex = items.indexOf(over.id);
1143
+ return arrayMove(items, oldIndex, newIndex);
1144
+ });
1145
+ }
1146
+ setActiveId(null);
1147
+ };
1148
+
1149
+ // Handle column drag start
1150
+ const handleDragStart = (event) => {
1151
+ setActiveId(event.active.id);
1152
+ };
1153
+
1154
+ // Filter data based on filter inputs - supports multiple keywords
1155
+ // For server-side filtering, skip client-side filtering (data is already filtered by server)
1156
+ const filteredData = useMemo(() => {
1157
+ if (isServerSideFiltering) {
1158
+ // Server-side filtering: data is already filtered, use as-is
1159
+ return data || [];
1160
+ }
1161
+
1162
+ // Client-side filtering: filter the data locally
1163
+ if (!data || data.length === 0) return [];
1164
+
1165
+ const globalQ = globalQuery.trim().toLowerCase();
1166
+ const filterEntries = Object.entries(filters).filter(([, f]) => isFilterActive(f));
1167
+
1168
+ return data.filter((row) => {
1169
+ for (const [key, filter] of filterEntries) {
1170
+ const cellValue = row[key];
1171
+ if (cellValue === null || cellValue === undefined) return false;
1172
+ // Text-values filter (legacy / default)
1173
+ if (Array.isArray(filter)) {
1174
+ const cellStr = String(cellValue);
1175
+ if (!filter.some((v) => String(v) === cellStr)) return false;
1176
+ continue;
1177
+ }
1178
+ if (filter.type === "range") {
1179
+ const n = parseNumberLike(cellValue);
1180
+ if (isNaN(n)) return false;
1181
+ if (filter.min !== "" && filter.min != null && n < Number(filter.min)) return false;
1182
+ if (filter.max !== "" && filter.max != null && n > Number(filter.max)) return false;
1183
+ continue;
1184
+ }
1185
+ if (filter.type === "durationRange") {
1186
+ const sec = parseDurationToSeconds(cellValue);
1187
+ if (isNaN(sec)) return false;
1188
+ if (filter.minSec !== "" && filter.minSec != null && sec < Number(filter.minSec)) return false;
1189
+ if (filter.maxSec !== "" && filter.maxSec != null && sec > Number(filter.maxSec)) return false;
1190
+ continue;
1191
+ }
1192
+ if (filter.type === "dateRange") {
1193
+ const d = parseDateLike(cellValue);
1194
+ if (!d) return false;
1195
+ if (filter.from) {
1196
+ const from = new Date(filter.from + "T00:00:00");
1197
+ if (d < from) return false;
1198
+ }
1199
+ if (filter.to) {
1200
+ const to = new Date(filter.to + "T23:59:59");
1201
+ if (d > to) return false;
1202
+ }
1203
+ continue;
1204
+ }
1205
+ }
1206
+ // Global search — substring match across any cell value
1207
+ if (globalQ) {
1208
+ const anyMatch = Object.values(row).some((v) => {
1209
+ if (v === null || v === undefined) return false;
1210
+ return String(v).toLowerCase().includes(globalQ);
1211
+ });
1212
+ if (!anyMatch) return false;
1213
+ }
1214
+ return true;
1215
+ });
1216
+ }, [data, filters, globalQuery, isServerSideFiltering]);
1217
+
1218
+ // Sort filtered data
1219
+ // For server-side sorting, skip client-side sorting (data is already sorted by server)
1220
+ const sortedData = useMemo(() => {
1221
+ if (isServerSideSorting) {
1222
+ // Server-side sorting: data is already sorted, use as-is
1223
+ return filteredData;
1224
+ }
1225
+
1226
+ // Client-side sorting: sort the filtered data locally
1227
+ if (!sortField) return filteredData;
1228
+
1229
+ return [...filteredData].sort((a, b) => {
1230
+ const aVal = a[sortField];
1231
+ const bVal = b[sortField];
1232
+
1233
+ // Handle null/undefined
1234
+ if (aVal === null || aVal === undefined) return 1;
1235
+ if (bVal === null || bVal === undefined) return -1;
1236
+
1237
+ // Handle numbers
1238
+ if (typeof aVal === "number" && typeof bVal === "number") {
1239
+ return sortDirection === "asc" ? aVal - bVal : bVal - aVal;
1240
+ }
1241
+
1242
+ // Handle strings
1243
+ const aStr = String(aVal).toLowerCase();
1244
+ const bStr = String(bVal).toLowerCase();
1245
+
1246
+ if (sortDirection === "asc") {
1247
+ return aStr.localeCompare(bStr);
1248
+ } else {
1249
+ return bStr.localeCompare(aStr);
1250
+ }
1251
+ });
1252
+ }, [filteredData, sortField, sortDirection, isServerSideSorting]);
1253
+
1254
+ // Paginate sorted data
1255
+ // For server-side pagination, use data as-is (already paginated by server)
1256
+ // For client-side pagination, paginate the sorted data
1257
+ const paginatedData = useMemo(() => {
1258
+ if (isServerSidePagination) {
1259
+ // Server-side: data is already paginated, use as-is
1260
+ // Use data directly (not sortedData) since server handles sorting
1261
+ return data || [];
1262
+ } else {
1263
+ // Client-side: paginate the sorted data
1264
+ const startIndex = (page - 1) * pageSize;
1265
+ const endIndex = startIndex + pageSize;
1266
+ return sortedData.slice(startIndex, endIndex);
1267
+ }
1268
+ }, [data, sortedData, page, pageSize, isServerSidePagination]);
1269
+
1270
+ // Handle header click for sorting
1271
+ const handleSort = (field) => {
1272
+ if (isServerSideSorting && onSort) {
1273
+ // Server-side sorting: call parent's onSort callback
1274
+ onSort(field);
1275
+ // Reset to first page on sort (if pagination is controlled)
1276
+ if (onPageChange) {
1277
+ onPageChange(1);
1278
+ } else {
1279
+ setInternalPage(1);
1280
+ }
1281
+ } else {
1282
+ // Client-side sorting: update internal state
1283
+ if (internalSortField === field) {
1284
+ setInternalSortDirection(internalSortDirection === "asc" ? "desc" : "asc");
1285
+ } else {
1286
+ setInternalSortField(field);
1287
+ setInternalSortDirection("asc");
1288
+ }
1289
+ setPage(1); // Reset to first page on sort
1290
+ }
1291
+ };
1292
+
1293
+ // Legacy handler kept for any callers that still pass a string value;
1294
+ // it splits on whitespace and treats the resulting tokens as the selected values.
1295
+ const handleFilterChange = (id, value) => {
1296
+ const tokens = (value || "")
1297
+ .split(/\s+/)
1298
+ .map((t) => t.trim())
1299
+ .filter(Boolean);
1300
+ setColumnFilter(id, tokens);
1301
+ };
1302
+
1303
+ // Surface max columns limit error via callback, throttled to once per 3s
1304
+ const showMaxColumnsError = (message) => {
1305
+ if (toastTimeoutRef.current) return;
1306
+ if (typeof onMaxColumnsError === 'function') {
1307
+ onMaxColumnsError(message);
1308
+ }
1309
+ toastTimeoutRef.current = setTimeout(() => {
1310
+ toastTimeoutRef.current = null;
1311
+ }, 3000);
1312
+ };
1313
+
1314
+ // Save columns to localStorage
1315
+ const saveColumnsToStorage = useCallback((cols) => {
1316
+ if (typeof window !== 'undefined' && storageKey && cols.length > 0 && userHasManuallyChangedColumns.current) {
1317
+ try {
1318
+ localStorage.setItem(storageKey, JSON.stringify(cols));
1319
+ if (manualChangeFlagKey) {
1320
+ localStorage.setItem(manualChangeFlagKey, 'true');
1321
+ }
1322
+ } catch (error) {
1323
+ console.warn('Failed to save columns to localStorage:', error);
1324
+ }
1325
+ }
1326
+ }, [storageKey, manualChangeFlagKey]);
1327
+
1328
+ // Toggle column selection with smooth transitions
1329
+ const toggleColumnSelection = (columnId) => {
1330
+ // Prevent toggling the action column visibility; it should always be shown
1331
+ if (columnId === ACTION_COLUMN_ID) {
1332
+ return;
1333
+ }
1334
+ // Mark that user has manually changed columns
1335
+ userHasManuallyChangedColumns.current = true;
1336
+
1337
+ setVisibleColumns(prev => {
1338
+ // Unselecting: show unchecked state first, then remove from selected list
1339
+ if (prev.includes(columnId)) {
1340
+ // Clear any pending throttling timeout when unselecting
1341
+ if (toastTimeoutRef.current) {
1342
+ clearTimeout(toastTimeoutRef.current);
1343
+ toastTimeoutRef.current = null;
1344
+ }
1345
+ // toast.dismiss("max-columns-toast");
1346
+
1347
+ // Don't allow deselecting all columns
1348
+ if (prev.length === 1) return prev;
1349
+
1350
+ // Show unchecked state first, then remove after delay
1351
+ setPendingDeselection(prevSet => new Set(prevSet).add(columnId));
1352
+ setTimeout(() => {
1353
+ setVisibleColumns(current => {
1354
+ const updated = current.filter(id => id !== columnId);
1355
+ const constrained = enforceActionPosition(updated);
1356
+ // Save immediately after user change
1357
+ saveColumnsToStorage(constrained);
1358
+ return constrained;
1359
+ });
1360
+ setPendingDeselection(prevSet => {
1361
+ const next = new Set(prevSet);
1362
+ next.delete(columnId);
1363
+ return next;
1364
+ });
1365
+ }, 200); // 200ms delay for smooth transition
1366
+
1367
+ return prev; // Return current state, actual removal happens in setTimeout
1368
+ }
1369
+
1370
+ // Selecting: check if we're at the maximum limit
1371
+ if (prev.length >= MAX_COLUMNS) {
1372
+ showMaxColumnsError("Maximum 9 column selection allowed");
1373
+ return prev;
1374
+ }
1375
+
1376
+ // Show checked state first, then add to selected list after delay
1377
+ setPendingSelection(prevSet => new Set(prevSet).add(columnId));
1378
+ setTimeout(() => {
1379
+ setVisibleColumns(current => {
1380
+ const updated = !current.includes(columnId) ? [...current, columnId] : current;
1381
+ const constrained = enforceActionPosition(updated);
1382
+ // Save immediately after user change
1383
+ saveColumnsToStorage(constrained);
1384
+ return constrained;
1385
+ });
1386
+ setPendingSelection(prevSet => {
1387
+ const next = new Set(prevSet);
1388
+ next.delete(columnId);
1389
+ return next;
1390
+ });
1391
+ }, 200); // 200ms delay for smooth transition
1392
+
1393
+ return prev; // Return current state, actual addition happens in setTimeout
1394
+ });
1395
+ };
1396
+
1397
+ // Column drag handlers for reordering in dropdown
1398
+ const handleColumnDragStart = (e, index) => {
1399
+ if (visibleColumns[index] === ACTION_COLUMN_ID) return;
1400
+ setDraggedColumnIndex(index);
1401
+ };
1402
+
1403
+ const handleColumnDragEnd = () => {
1404
+ setDraggedColumnIndex(null);
1405
+ setDragOverColumnIndex(null);
1406
+ };
1407
+
1408
+ const handleColumnDragOver = (e, index) => {
1409
+ e.preventDefault();
1410
+ if (draggedColumnIndex !== null && draggedColumnIndex !== index) {
1411
+ setDragOverColumnIndex(index);
1412
+ }
1413
+ };
1414
+
1415
+ const handleColumnDragLeave = () => {
1416
+ setDragOverColumnIndex(null);
1417
+ };
1418
+
1419
+ const handleColumnDrop = (e, dropIndex) => {
1420
+ e.preventDefault();
1421
+ if (
1422
+ draggedColumnIndex !== null &&
1423
+ draggedColumnIndex !== dropIndex &&
1424
+ visibleColumns[dropIndex] !== ACTION_COLUMN_ID &&
1425
+ visibleColumns[draggedColumnIndex] !== ACTION_COLUMN_ID
1426
+ ) {
1427
+ // Mark that user has manually changed columns
1428
+ userHasManuallyChangedColumns.current = true;
1429
+
1430
+ setVisibleColumns(prev => {
1431
+ const reordered = arrayMove(prev, draggedColumnIndex, dropIndex);
1432
+ const constrained = enforceActionPosition(reordered);
1433
+ // Save immediately after drag
1434
+ saveColumnsToStorage(constrained);
1435
+ return constrained;
1436
+ });
1437
+ }
1438
+ setDraggedColumnIndex(null);
1439
+ setDragOverColumnIndex(null);
1440
+ };
1441
+
1442
+ // Update columnOrder when visibleColumns changes
1443
+ useEffect(() => {
1444
+ setColumnOrder(prev => {
1445
+ // Keep visible columns in their current order, append any new ones
1446
+ const visibleOrder = visibleColumns.filter(id => prev.includes(id));
1447
+ const newColumns = visibleColumns.filter(id => !prev.includes(id));
1448
+ const hiddenColumns = prev.filter(id => !visibleColumns.includes(id));
1449
+ return [...visibleOrder, ...newColumns, ...hiddenColumns];
1450
+ });
1451
+ }, [visibleColumns]);
1452
+
1453
+ // Save to localStorage whenever visibleColumns changes (only if user has manually changed columns)
1454
+ useEffect(() => {
1455
+ if (typeof window !== 'undefined' && storageKey && visibleColumns.length > 0 && userHasManuallyChangedColumns.current) {
1456
+ try {
1457
+ const constrained = enforceActionPosition(visibleColumns);
1458
+ localStorage.setItem(storageKey, JSON.stringify(constrained));
1459
+ if (manualChangeFlagKey) {
1460
+ localStorage.setItem(manualChangeFlagKey, 'true');
1461
+ }
1462
+ } catch (error) {
1463
+ console.warn('Failed to save columns to localStorage:', error);
1464
+ }
1465
+ }
1466
+ }, [visibleColumns, storageKey, manualChangeFlagKey]);
1467
+
1468
+ // Initialize visibleColumns when columns become available (if not already initialized)
1469
+ useEffect(() => {
1470
+ // Only initialize if columns are available and visibleColumns is empty
1471
+ if (columns.length > 0 && visibleColumns.length === 0) {
1472
+ // Check if user has manually changed columns before
1473
+ if (typeof window !== 'undefined' && tableId) {
1474
+ try {
1475
+ const hasManualChange = localStorage.getItem(`dataTable_manual_change_${tableId}:${userId}`) === 'true';
1476
+ if (hasManualChange) {
1477
+ const saved = localStorage.getItem(`dataTable_columns_${tableId}:${userId}`);
1478
+ if (saved) {
1479
+ const savedColumns = JSON.parse(saved);
1480
+ if (Array.isArray(savedColumns) && savedColumns.length > 0) {
1481
+ // Validate saved columns exist in current columns
1482
+ const validColumns = savedColumns.filter(colId =>
1483
+ columns.some(col => (col.id || col.key) === colId)
1484
+ );
1485
+ if (validColumns.length > 0) {
1486
+ userHasManuallyChangedColumns.current = true;
1487
+ setVisibleColumns(validColumns.slice(0, MAX_COLUMNS));
1488
+ return;
1489
+ }
1490
+ }
1491
+ }
1492
+ }
1493
+ } catch (error) {
1494
+ console.warn('Failed to load saved columns from localStorage:', error);
1495
+ }
1496
+ }
1497
+
1498
+ // Default to first 9 columns if no saved columns
1499
+ const columnIds = columns.map((col) => col.id || col.key);
1500
+ setVisibleColumns(enforceActionPosition(columnIds.slice(0, MAX_COLUMNS)));
1501
+ }
1502
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1503
+ }, [columns, tableId, userId, enforceActionPosition]);
1504
+
1505
+ // Get ordered columns based on visibleColumns and columnOrder
1506
+ const orderedColumns = useMemo(() => {
1507
+ // Filter to only show visible columns, maintaining their order
1508
+ const cols = visibleColumns
1509
+ .map((id) => columns.find((col) => getColumnId(col) === id))
1510
+ .filter(Boolean);
1511
+ return enforceActionPosition(cols.map(getColumnId))
1512
+ .map((id) => columns.find((col) => getColumnId(col) === id))
1513
+ .filter(Boolean);
1514
+ }, [columns, visibleColumns]);
1515
+
1516
+ // Check if any columns are filterable
1517
+ const hasFilterableColumns = useMemo(() => {
1518
+ return orderedColumns.some((col) => col.filterable === true);
1519
+ }, [orderedColumns]);
1520
+
1521
+ // Column dropdown position management
1522
+ useEffect(() => {
1523
+ if (!isColumnDropdownOpen || !columnDropdownRef.current) return;
1524
+
1525
+ const updatePosition = () => {
1526
+ if (columnDropdownRef.current) {
1527
+ const rect = columnDropdownRef.current.getBoundingClientRect();
1528
+ // For position: fixed, use viewport coordinates directly (no scroll offset needed)
1529
+ // This ensures the dropdown scrolls with the button
1530
+ // Connect menu directly to button (no gap)
1531
+ setColumnDropdownPosition({
1532
+ top: rect.bottom, // No gap - connect directly
1533
+ left: rect.left,
1534
+ width: Math.max(rect.width || 220, 220),
1535
+ });
1536
+ }
1537
+ };
1538
+
1539
+ updatePosition();
1540
+
1541
+ // Use simple scroll listeners like Dropdown component for smooth scrolling
1542
+ window.addEventListener("scroll", updatePosition, true);
1543
+ window.addEventListener("resize", updatePosition);
1544
+
1545
+ return () => {
1546
+ window.removeEventListener("scroll", updatePosition, true);
1547
+ window.removeEventListener("resize", updatePosition);
1548
+ };
1549
+ }, [isColumnDropdownOpen]);
1550
+
1551
+ // Close dropdown when clicking outside
1552
+ useEffect(() => {
1553
+ const handleClickOutside = (event) => {
1554
+ if (
1555
+ isColumnDropdownOpen &&
1556
+ columnDropdownRef.current &&
1557
+ !columnDropdownRef.current.contains(event.target) &&
1558
+ columnDropdownMenuRef.current &&
1559
+ !columnDropdownMenuRef.current.contains(event.target)
1560
+ ) {
1561
+ setIsColumnDropdownOpen(false);
1562
+ }
1563
+ };
1564
+
1565
+ if (isColumnDropdownOpen) {
1566
+ document.addEventListener("mousedown", handleClickOutside);
1567
+ return () => {
1568
+ document.removeEventListener("mousedown", handleClickOutside);
1569
+ };
1570
+ }
1571
+ }, [isColumnDropdownOpen]);
1572
+
1573
+ // Display text for dropdown button
1574
+ const displayText = visibleColumns.length === 1
1575
+ ? columns.find(col => (col.id || col.key) === visibleColumns[0])?.label || "1 selected"
1576
+ : `${visibleColumns.length} selected`;
1577
+
1578
+ // Sortable header cell component
1579
+ function SortableHeader({ column, isHeaderRow = true }) {
1580
+ const columnId = getColumnId(column);
1581
+ const isActionColumn = columnId === ACTION_COLUMN_ID;
1582
+ const resolvedWidth = getResolvedColumnWidth(columnId, column.width);
1583
+ const [isHeaderHovered, setIsHeaderHovered] = useState(false);
1584
+ const {
1585
+ attributes,
1586
+ listeners,
1587
+ setNodeRef,
1588
+ transform,
1589
+ transition,
1590
+ isDragging,
1591
+ } = useSortable({ id: columnId });
1592
+
1593
+ const style = {
1594
+ transform: CSS.Transform.toString(transform),
1595
+ transition,
1596
+ opacity: isDragging ? 0.5 : 1,
1597
+ width: resolvedWidth,
1598
+ minWidth: resolvedWidth,
1599
+ };
1600
+
1601
+ // For server-side sorting, check if the sortField matches this column
1602
+ // Handle reverse mapping (e.g., "csat_score" matches "evaluation.csat_score")
1603
+ let isSorted = false;
1604
+ if (isServerSideSorting && sortFields.length > 0) {
1605
+ const sortFieldFromProps = sortFields[0][0];
1606
+ // Direct match
1607
+ if (sortFieldFromProps === columnId) {
1608
+ isSorted = true;
1609
+ }
1610
+ // Reverse mapping: "csat_score" matches "evaluation.csat_score"
1611
+ else if (sortFieldFromProps === "csat_score" && columnId === "evaluation.csat_score") {
1612
+ isSorted = true;
1613
+ }
1614
+ } else {
1615
+ // Client-side sorting: direct comparison
1616
+ isSorted = sortField === columnId;
1617
+ }
1618
+
1619
+ const isAsc = sortDirection === "asc";
1620
+ const isFilterable = column.filterable === true; // Only filterable if explicitly set to true
1621
+
1622
+ // Filter row
1623
+ if (!isHeaderRow) {
1624
+ return (
1625
+ <th
1626
+ style={{
1627
+ width: resolvedWidth,
1628
+ minWidth: resolvedWidth,
1629
+ backgroundColor: "var(--primary-foreground)",
1630
+ }}
1631
+ className="sticky top-[57px] z-10 border-b border-[var(--border-strong)] px-4 py-2 text-left"
1632
+ >
1633
+ {isFilterable ? (
1634
+ <div className="relative">
1635
+ <Filter
1636
+ size={12}
1637
+ className="absolute left-2 top-1/2 -translate-y-1/2 text-[var(--text-faint)] pointer-events-none"
1638
+ />
1639
+ <input
1640
+ type="text"
1641
+ placeholder="Filter..."
1642
+ value={filters[columnId] || ""}
1643
+ onChange={(e) => {
1644
+ e.stopPropagation();
1645
+ handleFilterChange(columnId, e.target.value);
1646
+ }}
1647
+ onKeyDown={(e) => e.stopPropagation()}
1648
+ onMouseDown={(e) => e.stopPropagation()}
1649
+ className="w-full pl-7 pr-2 py-1.5 text-xs border border-[var(--border)] rounded text-[var(--text-ink)] placeholder:text-[var(--text-faint)] focus:outline-none focus:border-[var(--border-hover)] focus:ring-1 focus:ring-[var(--focus)] transition-colors"
1650
+ style={{ fontFamily: 'var(--font-sans)', backgroundColor: "var(--primary-foreground)" }}
1651
+ autoComplete="off"
1652
+ />
1653
+ </div>
1654
+ ) : (
1655
+ <div className="h-[34px]"></div>
1656
+ )}
1657
+ </th>
1658
+ );
1659
+ }
1660
+
1661
+ // Header row
1662
+ const filterPopoverOpen = openFilterColumnId === columnId;
1663
+ const hasActiveFilter = isFilterActive(filters[columnId]);
1664
+ const showSortBtn = column.sortable !== false && (isHeaderHovered || isSorted);
1665
+ const showFilterBtn = column.filterable !== false && !isActionColumn && (isHeaderHovered || filterPopoverOpen || hasActiveFilter);
1666
+ return (
1667
+ <th
1668
+ ref={setNodeRef}
1669
+ onMouseEnter={() => setIsHeaderHovered(true)}
1670
+ onMouseLeave={() => setIsHeaderHovered(false)}
1671
+ style={{
1672
+ ...style,
1673
+ padding: '0 12px',
1674
+ height: 36,
1675
+ textAlign: isActionColumn ? 'center' : 'left',
1676
+ fontWeight: 500,
1677
+ fontSize: '14px',
1678
+ letterSpacing: 'normal',
1679
+ textTransform: 'none',
1680
+ color: '#0b0b0b',
1681
+ cursor: column.sortable !== false ? 'pointer' : 'default',
1682
+ width: resolvedWidth,
1683
+ borderRight: 'none',
1684
+ userSelect: 'none',
1685
+ background: isHeaderHovered || filterPopoverOpen ? '#e6e6e6' : '#f2f2f0',
1686
+ borderBottom: '1px solid #e6e6e6',
1687
+ position: 'sticky',
1688
+ top: 0,
1689
+ zIndex: 10,
1690
+ fontFamily: 'var(--font-sans)',
1691
+ transition: 'background 100ms ease',
1692
+ }}
1693
+ >
1694
+ {/* Left edge separator (1px × 16px, vertically centered) */}
1695
+ {!isActionColumn && (
1696
+ <span
1697
+ aria-hidden="true"
1698
+ style={{
1699
+ position: "absolute",
1700
+ left: 0,
1701
+ top: "50%",
1702
+ transform: "translateY(-50%)",
1703
+ width: 1,
1704
+ height: 16,
1705
+ background: "#e6e6e6",
1706
+ pointerEvents: "none",
1707
+ }}
1708
+ />
1709
+ )}
1710
+ <div
1711
+ style={{
1712
+ display: 'flex',
1713
+ alignItems: 'center',
1714
+ gap: '4px',
1715
+ justifyContent: isActionColumn ? 'center' : 'flex-start',
1716
+ position: 'relative',
1717
+ minHeight: 20,
1718
+ }}
1719
+ >
1720
+ {!isActionColumn && (
1721
+ <div
1722
+ {...attributes}
1723
+ {...listeners}
1724
+ style={{ cursor: 'grab', color: 'rgba(11, 11, 11, 0.36)', display: 'inline-flex', alignItems: 'center' }}
1725
+ >
1726
+ <GripVertical size={14} />
1727
+ </div>
1728
+ )}
1729
+ <span
1730
+ onClick={() => column.sortable !== false && handleSort(columnId)}
1731
+ style={{ cursor: column.sortable !== false ? 'pointer' : 'default', flex: 1, lineHeight: '20px' }}
1732
+ >
1733
+ {column.label}
1734
+ </span>
1735
+ {showSortBtn && (
1736
+ <button
1737
+ type="button"
1738
+ onClick={(e) => { e.stopPropagation(); handleSort(columnId); }}
1739
+ title="Sort"
1740
+ style={{
1741
+ background: "transparent",
1742
+ border: "none",
1743
+ padding: 4,
1744
+ borderRadius: 4,
1745
+ cursor: "pointer",
1746
+ display: "inline-flex",
1747
+ alignItems: "center",
1748
+ justifyContent: "center",
1749
+ color: isSorted ? "#0b0b0b" : "rgba(11, 11, 11, 0.55)",
1750
+ }}
1751
+ >
1752
+ {isSorted
1753
+ ? (isAsc ? <ArrowUp size={14} strokeWidth={1.75} /> : <ArrowDown size={14} strokeWidth={1.75} />)
1754
+ : <ArrowUpDown size={14} strokeWidth={1.75} />}
1755
+ </button>
1756
+ )}
1757
+ {showFilterBtn && (
1758
+ <button
1759
+ type="button"
1760
+ ref={(el) => { filterIconRefs.current[columnId] = el; }}
1761
+ onClick={(e) => { e.stopPropagation(); openFilterPopoverFor(columnId, e.currentTarget); }}
1762
+ title="Filter"
1763
+ style={{
1764
+ position: "relative",
1765
+ background: filterPopoverOpen || hasActiveFilter ? "#D8D8D8" : "transparent",
1766
+ border: "none",
1767
+ padding: 4,
1768
+ borderRadius: 4,
1769
+ cursor: "pointer",
1770
+ display: "inline-flex",
1771
+ alignItems: "center",
1772
+ justifyContent: "center",
1773
+ color: hasActiveFilter ? "#0b0b0b" : "rgba(11, 11, 11, 0.7)",
1774
+ }}
1775
+ >
1776
+ <ListFilter size={14} strokeWidth={1.75} />
1777
+ {hasActiveFilter && (
1778
+ <span
1779
+ aria-hidden="true"
1780
+ style={{
1781
+ position: "absolute",
1782
+ top: 0,
1783
+ right: 0,
1784
+ width: 6,
1785
+ height: 6,
1786
+ borderRadius: "50%",
1787
+ background: "#0B0B0B",
1788
+ }}
1789
+ />
1790
+ )}
1791
+ </button>
1792
+ )}
1793
+ </div>
1794
+ {/* Right edge separator (1px × 16px, vertically centered, on the cell's right edge) */}
1795
+ {!isActionColumn && (
1796
+ <span
1797
+ aria-hidden="true"
1798
+ style={{
1799
+ position: "absolute",
1800
+ right: 0,
1801
+ top: "50%",
1802
+ transform: "translateY(-50%)",
1803
+ width: 1,
1804
+ height: 16,
1805
+ background: "#e6e6e6",
1806
+ pointerEvents: "none",
1807
+ }}
1808
+ />
1809
+ )}
1810
+ {/* Resize handle — sits right at the cell's right edge, away from the filter icon */}
1811
+ {!isActionColumn && (
1812
+ <div
1813
+ role="separator"
1814
+ aria-orientation="vertical"
1815
+ aria-label={`Resize ${column.label || columnId} column`}
1816
+ title="Drag to resize column"
1817
+ onMouseDown={(e) => startResize(e, column, resolvedWidth)}
1818
+ onClick={(e) => e.stopPropagation()}
1819
+ onMouseEnter={(e) => {
1820
+ e.currentTarget.style.background = "rgba(30, 33, 37, 0.18)";
1821
+ }}
1822
+ onMouseLeave={(e) => {
1823
+ e.currentTarget.style.background = "transparent";
1824
+ }}
1825
+ style={{
1826
+ position: "absolute",
1827
+ top: 0,
1828
+ right: -2,
1829
+ width: 4,
1830
+ height: "100%",
1831
+ cursor: "col-resize",
1832
+ zIndex: 4,
1833
+ background: "transparent",
1834
+ }}
1835
+ />
1836
+ )}
1837
+ </th>
1838
+ );
1839
+ }
1840
+
1841
+ // Drag overlay for column header
1842
+ const activeColumn = activeId ? columns.find((col) => getColumnId(col) === activeId) : null;
1843
+
1844
+ // Column dropdown menu
1845
+ const renderColumnRow = ({ col, columnId, isSelected, draggable, isAction, actualIndex }) => {
1846
+ const isDragging = draggable && draggedColumnIndex === actualIndex;
1847
+ const isDragOver = draggable && dragOverColumnIndex === actualIndex && draggedColumnIndex !== null && draggedColumnIndex !== actualIndex;
1848
+ const isPending = isSelected ? pendingDeselection.has(columnId) : pendingSelection.has(columnId);
1849
+ // Visual checked state: selected and not pending deselect, OR unselected and pending select
1850
+ const checked = isSelected ? !pendingDeselection.has(columnId) : pendingSelection.has(columnId);
1851
+ return (
1852
+ <div
1853
+ draggable={draggable}
1854
+ onDragStart={draggable ? (e) => handleColumnDragStart(e, actualIndex) : undefined}
1855
+ onDragEnd={draggable ? handleColumnDragEnd : undefined}
1856
+ onDragOver={draggable ? (e) => handleColumnDragOver(e, actualIndex) : undefined}
1857
+ onDragLeave={draggable ? handleColumnDragLeave : undefined}
1858
+ onDrop={draggable ? (e) => handleColumnDrop(e, actualIndex) : undefined}
1859
+ onClick={() => { if (!isAction) toggleColumnSelection(columnId); }}
1860
+ style={{
1861
+ display: "flex",
1862
+ alignItems: "center",
1863
+ gap: 4,
1864
+ padding: "2px 0",
1865
+ cursor: isAction ? "default" : (draggable ? "grab" : "pointer"),
1866
+ opacity: isDragging ? 0.5 : 1,
1867
+ background: isDragOver ? "#F2F2F0" : "transparent",
1868
+ borderRadius: 6,
1869
+ }}
1870
+ >
1871
+ {/* Drag handle — visible only for selected (sortable) items */}
1872
+ <span
1873
+ style={{
1874
+ width: 16,
1875
+ height: 16,
1876
+ display: "inline-flex",
1877
+ alignItems: "center",
1878
+ justifyContent: "center",
1879
+ color: "#989898",
1880
+ visibility: draggable ? "visible" : "hidden",
1881
+ flexShrink: 0,
1882
+ }}
1883
+ >
1884
+ <GripVertical size={14} strokeWidth={1.75} />
1885
+ </span>
1886
+ {/* Checkbox 20x20 */}
1887
+ <span
1888
+ style={{
1889
+ width: 20,
1890
+ height: 20,
1891
+ borderRadius: 4,
1892
+ display: "inline-flex",
1893
+ alignItems: "center",
1894
+ justifyContent: "center",
1895
+ background: checked ? "#0B0B0B" : "#FFFFFF",
1896
+ border: `1px solid ${checked ? "#0B0B0B" : "#676767"}`,
1897
+ flexShrink: 0,
1898
+ marginLeft: 8,
1899
+ }}
1900
+ >
1901
+ {checked && <Check size={14} color="#FFFFFF" strokeWidth={3} />}
1902
+ </span>
1903
+ <span
1904
+ style={{
1905
+ flex: 1,
1906
+ fontFamily: "var(--font-sans)",
1907
+ fontSize: 14,
1908
+ lineHeight: "20px",
1909
+ color: "#0B0B0B",
1910
+ marginLeft: 8,
1911
+ }}
1912
+ >
1913
+ {col.label}
1914
+ </span>
1915
+ </div>
1916
+ );
1917
+ };
1918
+
1919
+ const columnDropdownMenu = isColumnDropdownOpen ? (
1920
+ <div
1921
+ ref={columnDropdownMenuRef}
1922
+ style={{
1923
+ position: "fixed",
1924
+ top: `${columnDropdownPosition.top}px`,
1925
+ left: `${columnDropdownPosition.left}px`,
1926
+ width: 217,
1927
+ maxHeight: 400,
1928
+ overflow: "auto",
1929
+ zIndex: 49,
1930
+ background: "#FFFFFF",
1931
+ border: "1px solid #E6E6E6",
1932
+ borderRadius: 10,
1933
+ padding: 12,
1934
+ display: "flex",
1935
+ flexDirection: "column",
1936
+ gap: 12,
1937
+ fontFamily: "var(--font-sans)",
1938
+ boxShadow: "0 8px 24px rgba(11, 11, 11, 0.08)",
1939
+ }}
1940
+ >
1941
+ {/* Selected columns — draggable */}
1942
+ {visibleColumns.map((columnId, index) => {
1943
+ const col = columns.find((c) => getColumnId(c) === columnId);
1944
+ if (!col) return null;
1945
+ const isAction = columnId === ACTION_COLUMN_ID;
1946
+ return (
1947
+ <React.Fragment key={`selected-${columnId}-${index}`}>
1948
+ {renderColumnRow({
1949
+ col,
1950
+ columnId,
1951
+ isSelected: true,
1952
+ draggable: !isAction,
1953
+ isAction,
1954
+ actualIndex: index,
1955
+ })}
1956
+ </React.Fragment>
1957
+ );
1958
+ })}
1959
+
1960
+ {/* Unselected columns */}
1961
+ {columns
1962
+ .filter((col) => !visibleColumns.includes(getColumnId(col)))
1963
+ .map((col) => {
1964
+ const columnId = getColumnId(col);
1965
+ return (
1966
+ <React.Fragment key={`unselected-${columnId}`}>
1967
+ {renderColumnRow({
1968
+ col,
1969
+ columnId,
1970
+ isSelected: false,
1971
+ draggable: false,
1972
+ isAction: false,
1973
+ actualIndex: -1,
1974
+ })}
1975
+ </React.Fragment>
1976
+ );
1977
+ })}
1978
+
1979
+ {/* Footer — divider + Reset + Apply */}
1980
+ <div style={{ height: 1, background: "#D9D9D9", margin: "0 -12px" }} />
1981
+ <div
1982
+ style={{
1983
+ display: "flex",
1984
+ alignItems: "center",
1985
+ justifyContent: "space-between",
1986
+ height: 30,
1987
+ }}
1988
+ >
1989
+ <button
1990
+ type="button"
1991
+ onClick={(e) => {
1992
+ e.preventDefault();
1993
+ e.stopPropagation();
1994
+ // Restore visible columns to the initial snapshot, drop staged toggles
1995
+ const snapshot = initialVisibleColumnsRef.current
1996
+ ? [...initialVisibleColumnsRef.current]
1997
+ : columns.map(getColumnId).slice(0, MAX_COLUMNS);
1998
+ userHasManuallyChangedColumns.current = false;
1999
+ setVisibleColumns(enforceActionPosition(snapshot));
2000
+ setPendingSelection(new Set());
2001
+ setPendingDeselection(new Set());
2002
+ if (typeof window !== "undefined" && storageKey) {
2003
+ try { localStorage.removeItem(storageKey); } catch (_) {}
2004
+ }
2005
+ }}
2006
+ style={{
2007
+ background: "transparent",
2008
+ border: "none",
2009
+ padding: 0,
2010
+ cursor: "pointer",
2011
+ fontFamily: "var(--font-sans)",
2012
+ fontSize: 14,
2013
+ fontWeight: 500,
2014
+ color: "#323232",
2015
+ }}
2016
+ >
2017
+ Reset
2018
+ </button>
2019
+ <button
2020
+ type="button"
2021
+ onClick={(e) => {
2022
+ e.preventDefault();
2023
+ e.stopPropagation();
2024
+ setIsColumnDropdownOpen(false);
2025
+ }}
2026
+ style={{
2027
+ background: "#2E3236",
2028
+ color: "#FFFFFF",
2029
+ border: "none",
2030
+ borderRadius: 10,
2031
+ height: 28,
2032
+ padding: "0 14px",
2033
+ fontFamily: "var(--font-sans)",
2034
+ fontSize: 14,
2035
+ fontWeight: 600,
2036
+ cursor: "pointer",
2037
+ }}
2038
+ >
2039
+ Apply
2040
+ </button>
2041
+ </div>
2042
+ </div>
2043
+ ) : null;
2044
+
2045
+ const openFilterColumn = openFilterColumnId
2046
+ ? columns.find((c) => getColumnId(c) === openFilterColumnId)
2047
+ : null;
2048
+ const filterPopoverNode = openFilterColumn
2049
+ ? (
2050
+ <FilterValuePicker
2051
+ key={openFilterColumnId}
2052
+ column={openFilterColumn}
2053
+ columnId={openFilterColumnId}
2054
+ filterType={getColumnFilterType(openFilterColumnId)}
2055
+ position={filterPopoverPosition}
2056
+ popoverRef={filterPopoverRef}
2057
+ distinctValues={getDistinctValuesForColumn(openFilterColumnId)}
2058
+ initialValue={filters[openFilterColumnId]}
2059
+ onApply={(value) => {
2060
+ setColumnFilter(openFilterColumnId, value);
2061
+ setOpenFilterColumnId(null);
2062
+ }}
2063
+ onCancel={() => setOpenFilterColumnId(null)}
2064
+ />
2065
+ )
2066
+ : null;
2067
+
2068
+ return (
2069
+ <div className="w-full" style={{top:'130px', position: 'sticky', zIndex: 35, backgroundColor: "var(--primary-foreground)"}}>
2070
+ {/* Filter popover (anchored under the active column header) */}
2071
+ {filterPopoverNode && createPortal(filterPopoverNode, document.body)}
2072
+ {/* Toolbar: Columns dropdown + Search */}
2073
+ {columns.length > 0 && (
2074
+ <div
2075
+ className="pt-2 pb-1"
2076
+ style={{
2077
+ position: "sticky",
2078
+ zIndex: 35,
2079
+ backgroundColor: "var(--primary-foreground)",
2080
+ }}
2081
+ >
2082
+ <div
2083
+ style={{
2084
+ display: "flex",
2085
+ alignItems: "center",
2086
+ justifyContent: "flex-end",
2087
+ gap: 16,
2088
+ flexWrap: "wrap",
2089
+ }}
2090
+ >
2091
+ {filtersConfig && (
2092
+ <div style={{ marginRight: "auto" }}>
2093
+ <DataTableFilters
2094
+ dateRangePicker={filtersConfig.dateRangePicker}
2095
+ onWeekToDate={filtersConfig.onWeekToDate}
2096
+ exportConfig={filtersConfig.exportConfig}
2097
+ filterChipsConfig={filtersConfig.filterChipsConfig}
2098
+ trailingActions={filtersConfig.trailingActions}
2099
+ />
2100
+ </div>
2101
+ )}
2102
+
2103
+ {/* Columns button — compact */}
2104
+ <div style={{ position: "relative" }}>
2105
+ <button
2106
+ ref={columnDropdownRef}
2107
+ type="button"
2108
+ onClick={() => setIsColumnDropdownOpen(!isColumnDropdownOpen)}
2109
+ style={{
2110
+ display: "inline-flex",
2111
+ alignItems: "center",
2112
+ gap: 10,
2113
+ height: 32,
2114
+ padding: "0 16px",
2115
+ background: "#FFFFFF",
2116
+ border: "1px solid #D9D9D9",
2117
+ borderRadius: 10,
2118
+ cursor: "pointer",
2119
+ fontFamily: "var(--font-sans)",
2120
+ fontSize: 14,
2121
+ fontWeight: 600,
2122
+ color: "#2E3236",
2123
+ whiteSpace: "nowrap",
2124
+ }}
2125
+ >
2126
+ <Settings2 size={20} strokeWidth={1.75} />
2127
+ Columns
2128
+ </button>
2129
+ {isColumnDropdownOpen && createPortal(columnDropdownMenu, document.body)}
2130
+ </div>
2131
+
2132
+ {/* Search input */}
2133
+ <div
2134
+ style={{
2135
+ background: "#D8D8D8",
2136
+ width: 260,
2137
+ height: 32,
2138
+ borderRadius: 11,
2139
+ padding: 1,
2140
+ display: "flex",
2141
+ alignItems: "center",
2142
+ }}
2143
+ >
2144
+ <div
2145
+ style={{
2146
+ background: "#FFFFFF",
2147
+ flex: 1,
2148
+ height: 30,
2149
+ borderRadius: 10,
2150
+ display: "flex",
2151
+ alignItems: "center",
2152
+ gap: 8,
2153
+ padding: "0 8px",
2154
+ }}
2155
+ >
2156
+ <Search size={18} color="#989898" strokeWidth={1.75} />
2157
+ <input
2158
+ type="text"
2159
+ placeholder="Search"
2160
+ value={globalQuery}
2161
+ onChange={(e) => {
2162
+ setGlobalQuery(e.target.value);
2163
+ if (onPageChange) onPageChange(1); else setInternalPage(1);
2164
+ }}
2165
+ style={{
2166
+ flex: 1,
2167
+ border: "none",
2168
+ outline: "none",
2169
+ background: "transparent",
2170
+ fontFamily: "var(--font-sans)",
2171
+ fontSize: 14,
2172
+ color: "#0B0B0B",
2173
+ minWidth: 0,
2174
+ }}
2175
+ />
2176
+ </div>
2177
+ </div>
2178
+ </div>
2179
+ </div>
2180
+ )}
2181
+ {/* Filter chip strip — appears below the toolbar when any filter is active */}
2182
+ {Object.keys(filters).length > 0 && (
2183
+ <div
2184
+ style={{
2185
+ display: "flex",
2186
+ alignItems: "center",
2187
+ gap: 8,
2188
+ flexWrap: "wrap",
2189
+ padding: "8px 0",
2190
+ }}
2191
+ >
2192
+ {Object.entries(filters).map(([colId, filter]) => {
2193
+ const col = columns.find((c) => getColumnId(c) === colId);
2194
+ if (!col || !isFilterActive(filter)) return null;
2195
+ const first = chipSummary(filter);
2196
+ const extra = chipExtraCount(filter);
2197
+ const extraValues = chipExtraValues(filter);
2198
+ return (
2199
+ <span
2200
+ key={`chip-${colId}`}
2201
+ style={{
2202
+ display: "inline-flex",
2203
+ alignItems: "center",
2204
+ gap: 8,
2205
+ height: 28,
2206
+ padding: "0 12px",
2207
+ borderRadius: 20,
2208
+ border: "1px solid #B2B2B0",
2209
+ background: "#F2F2F0",
2210
+ fontFamily: "var(--font-sans)",
2211
+ fontSize: 14,
2212
+ lineHeight: "20px",
2213
+ color: "#0B0B0B",
2214
+ whiteSpace: "nowrap",
2215
+ }}
2216
+ >
2217
+ <span>
2218
+ <span style={{ color: "#676767" }}>{col.label}: </span>
2219
+ <span>{first}</span>
2220
+ </span>
2221
+ {extra > 0 && (
2222
+ <ExtraValuesBadge count={extra} values={extraValues} />
2223
+ )}
2224
+ <button
2225
+ type="button"
2226
+ onClick={() => setColumnFilter(colId, [])}
2227
+ aria-label={`Remove ${col.label} filter`}
2228
+ style={{
2229
+ background: "transparent",
2230
+ border: "none",
2231
+ padding: 0,
2232
+ cursor: "pointer",
2233
+ display: "inline-flex",
2234
+ color: "#676767",
2235
+ }}
2236
+ >
2237
+ <X size={14} strokeWidth={2} />
2238
+ </button>
2239
+ </span>
2240
+ );
2241
+ })}
2242
+ <button
2243
+ type="button"
2244
+ onClick={clearAllFilters}
2245
+ style={{
2246
+ marginLeft: "auto",
2247
+ background: "transparent",
2248
+ border: "none",
2249
+ padding: "0 4px",
2250
+ cursor: "pointer",
2251
+ fontFamily: "var(--font-sans)",
2252
+ fontSize: 14,
2253
+ fontWeight: 500,
2254
+ color: "#323232",
2255
+ }}
2256
+ >
2257
+ Reset
2258
+ </button>
2259
+ </div>
2260
+ )}
2261
+
2262
+ {/* Only render table if we have columns */}
2263
+ {columns.length > 0 && orderedColumns.length > 0 ? (
2264
+ <DndContext
2265
+ sensors={sensors}
2266
+ collisionDetection={closestCenter}
2267
+ onDragStart={handleDragStart}
2268
+ onDragEnd={handleDragEnd}
2269
+ >
2270
+ <div style={{
2271
+ borderRadius: 8,
2272
+ border: '1px solid #E6E6E6',
2273
+ overflow: 'hidden',
2274
+ background: 'var(--primary-foreground)',
2275
+ position: 'relative',
2276
+ top: '0.5rem',
2277
+ }}>
2278
+ {/* Loading overlay */}
2279
+ {isLoading && (
2280
+ <div style={{
2281
+ position: 'absolute',
2282
+ inset: 0,
2283
+ background: 'rgba(255, 255, 255, 0.9)',
2284
+ backdropFilter: 'blur(2px)',
2285
+ zIndex: 20,
2286
+ display: 'flex',
2287
+ flexDirection: 'column',
2288
+ alignItems: 'center',
2289
+ justifyContent: 'center',
2290
+ borderRadius: '14px'
2291
+ }}>
2292
+ <div style={{
2293
+ display: 'flex',
2294
+ flexDirection: 'column',
2295
+ alignItems: 'center',
2296
+ gap: '12px'
2297
+ }}>
2298
+ <p style={{
2299
+ fontSize: '13px',
2300
+ color: 'rgba(30, 33, 37, 0.6)',
2301
+ margin: 0
2302
+ }}>Loading...</p>
2303
+ <Image
2304
+ src="/BrandLoading.gif"
2305
+ alt="Loading"
2306
+ width={50}
2307
+ height={50}
2308
+ unoptimized
2309
+ className="mt-1"
2310
+ />
2311
+ </div>
2312
+ </div>
2313
+ )}
2314
+ <div className="overflow-x-auto custom-thin-scrollbar-hidden" ref={tableRef} style={{ maxHeight: '500px', overflowY: 'auto', position: 'relative', zIndex: 1 }}>
2315
+ <table style={{
2316
+ width: 'auto',
2317
+ minWidth: '100%',
2318
+ borderCollapse: 'collapse',
2319
+ fontSize: '13px',
2320
+ fontFamily: 'var(--font-sans)'
2321
+ }}>
2322
+ <thead>
2323
+ {/* Header row with column labels */}
2324
+ <tr>
2325
+ <SortableContext
2326
+ items={visibleColumns}
2327
+ strategy={horizontalListSortingStrategy}
2328
+ >
2329
+ {orderedColumns.map((column) => (
2330
+ <SortableHeader key={`header-${getColumnId(column)}`} column={column} isHeaderRow={true} />
2331
+ ))}
2332
+ </SortableContext>
2333
+ </tr>
2334
+ </thead>
2335
+ <tbody>
2336
+ {!isLoading && paginatedData.length === 0 ? (
2337
+ <tr>
2338
+ <td
2339
+ colSpan={orderedColumns.length}
2340
+ style={{
2341
+ padding: '32px',
2342
+ textAlign: 'center',
2343
+ color: 'rgba(30, 33, 37, 0.42)',
2344
+ fontSize: '12px',
2345
+ fontFamily: 'var(--font-sans)',
2346
+ }}
2347
+ >
2348
+ No results found
2349
+ </td>
2350
+ </tr>
2351
+ ) : !isLoading && paginatedData.length > 0 ? (
2352
+ paginatedData.map((row, rowIndex) => (
2353
+ <tr
2354
+ key={rowIndex}
2355
+ data-row-id={row.id || rowIndex}
2356
+ style={{
2357
+ borderBottom: '1px solid #e6e6e6',
2358
+ cursor: onRowClick ? 'pointer' : 'default',
2359
+ transition: 'background 0.15s ease'
2360
+ }}
2361
+ onClick={onRowClick ? (e) => {
2362
+ e.stopPropagation();
2363
+ onRowClick(row, rowIndex);
2364
+ } : undefined}
2365
+ onMouseEnter={(e) => {
2366
+ e.currentTarget.style.background = 'rgba(231, 212, 162, 0.12)';
2367
+ }}
2368
+ onMouseLeave={(e) => {
2369
+ e.currentTarget.style.background = 'transparent';
2370
+ }}
2371
+ >
2372
+ {orderedColumns.map((column) => {
2373
+ const columnId = getColumnId(column);
2374
+ const resolvedWidth = getResolvedColumnWidth(columnId, column.width);
2375
+ const cellValue = row[columnId];
2376
+ const renderValue = column.render
2377
+ ? column.render(cellValue, row)
2378
+ : cellValue;
2379
+
2380
+ // Only show tooltip for string values that might be truncated
2381
+ const hasWidthConstraint = resolvedWidth && resolvedWidth !== 'auto';
2382
+ const isStringValue = typeof renderValue === 'string' && renderValue.trim() !== '';
2383
+ const shouldUseTruncatedCell = hasWidthConstraint && isStringValue;
2384
+
2385
+ const displayValue = shouldUseTruncatedCell ? (
2386
+ <TruncatedCell content={renderValue}>
2387
+ {renderValue}
2388
+ </TruncatedCell>
2389
+ ) : renderValue;
2390
+
2391
+ return (
2392
+ <td
2393
+ key={columnId}
2394
+ style={{
2395
+ padding: '10px 14px',
2396
+ color: 'rgba(30, 33, 37, 0.78)',
2397
+ fontSize: '13px',
2398
+ borderRight: '1px solid rgba(52, 58, 64, 0.04)',
2399
+ width: resolvedWidth,
2400
+ fontFamily: 'var(--font-sans)',
2401
+ ...(shouldUseTruncatedCell && { maxWidth: resolvedWidth || '200px' }),
2402
+ }}
2403
+ >
2404
+ {displayValue}
2405
+ </td>
2406
+ );
2407
+ })}
2408
+ </tr>
2409
+ ))
2410
+ ) : null}
2411
+ </tbody>
2412
+ </table>
2413
+ </div>
2414
+ </div>
2415
+
2416
+ <DragOverlay>
2417
+ {activeColumn ? (
2418
+ <div className="bg-[var(--paper-high)] border border-[var(--border-strong)] rounded px-4 py-2 shadow-lg">
2419
+ <div className="flex items-center gap-2">
2420
+ <GripVertical size={14} className="text-[var(--text-faint)]" />
2421
+ <span className="text-sm font-semibold text-[var(--text-ink)]">
2422
+ {activeColumn.label}
2423
+ </span>
2424
+ </div>
2425
+ </div>
2426
+ ) : null}
2427
+ </DragOverlay>
2428
+ </DndContext>
2429
+ ) : null}
2430
+
2431
+ {(isServerSidePagination ? totalCount > 0 : sortedData.length > 0) && (
2432
+ <Pagination
2433
+ page={page}
2434
+ pageSizeOptions={[5, 10, 20, 50, 100]}
2435
+ pageSize={pageSize}
2436
+ totalCount={isServerSidePagination ? totalCount : sortedData.length}
2437
+ currentDataLength={paginatedData.length}
2438
+ onPageChange={setPage}
2439
+ onPageSizeChange={(newSize) => {
2440
+ setPageSize(newSize);
2441
+ setPage(1);
2442
+ }}
2443
+ showPageSizeSelector={true}
2444
+ />
2445
+ )}
2446
+ </div>
2447
+ );
2448
+ }
2449
+