@papernote/ui 2.0.4 → 2.0.5

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.
@@ -1,16 +1,21 @@
1
-
2
- import React, { useState, useRef, useEffect, useCallback } from 'react';
3
- import { createPortal } from 'react-dom';
4
- import { ChevronDown, ChevronRight, MoreVertical, Edit, Trash } from 'lucide-react';
5
- import Menu, { MenuItem } from './Menu';
6
- import Pagination from './Pagination';
7
- import Select from './Select';
8
- import DataTableCardView, { CardViewConfig } from './DataTableCardView';
9
- import { useIsMobile } from '../hooks/useResponsive';
1
+ import React, { useState, useRef, useEffect, useCallback } from "react";
2
+ import { createPortal } from "react-dom";
3
+ import {
4
+ ChevronDown,
5
+ ChevronRight,
6
+ MoreVertical,
7
+ Edit,
8
+ Trash,
9
+ } from "lucide-react";
10
+ import Menu, { MenuItem } from "./Menu";
11
+ import Pagination from "./Pagination";
12
+ import Select from "./Select";
13
+ import DataTableCardView, { CardViewConfig } from "./DataTableCardView";
14
+ import { useIsMobile } from "../hooks/useResponsive";
10
15
 
11
16
  /**
12
17
  * Base data item interface - all data items must have an id
13
- *
18
+ *
14
19
  * All data passed to DataTable must implement this interface to ensure
15
20
  * proper row identification and selection handling.
16
21
  */
@@ -21,7 +26,7 @@ export interface BaseDataItem {
21
26
 
22
27
  /**
23
28
  * Column configuration for DataTable
24
- *
29
+ *
25
30
  * Defines how each column should be displayed, including width constraints,
26
31
  * custom rendering, sorting behavior, and alignment.
27
32
  */
@@ -42,26 +47,44 @@ export interface DataTableColumn<T> {
42
47
  render?: (item: T, value: any) => React.ReactNode;
43
48
  /** Secondary line content (smaller, muted text below primary) */
44
49
  renderSecondary?: (item: T, value: any) => React.ReactNode;
50
+ /**
51
+ * Header text for the secondary line. Surfaced as the cell's native
52
+ * tooltip on hover so users can identify the field even though the
53
+ * column header above only labels the primary row.
54
+ */
55
+ secondaryHeader?: string;
56
+ /**
57
+ * Custom tooltip resolver for the primary cell. Receives item + value;
58
+ * return a plain string. Falls back to `String(value)` when omitted so
59
+ * users hovering a truncated cell can still read the full value.
60
+ */
61
+ tooltip?: (item: T, value: any) => string | undefined;
62
+ /**
63
+ * Custom tooltip resolver for the secondary cell. When omitted, the
64
+ * tooltip becomes `${secondaryHeader}: ${value}` so the otherwise-
65
+ * unlabeled second row stays self-describing.
66
+ */
67
+ secondaryTooltip?: (item: T, value: any) => string | undefined;
45
68
  /** Enable sorting for this column */
46
69
  sortable?: boolean;
47
70
  /** Additional CSS classes for column cells */
48
71
  className?: string;
49
72
  /** Horizontal text alignment in column */
50
- align?: 'left' | 'center' | 'right';
73
+ align?: "left" | "center" | "right";
51
74
  /** Vertical alignment of cell content - useful when rows have varying heights */
52
- verticalAlign?: 'top' | 'middle' | 'bottom';
75
+ verticalAlign?: "top" | "middle" | "bottom";
53
76
  }
54
77
 
55
78
  /**
56
79
  * Sort configuration
57
- *
80
+ *
58
81
  * Describes the current sort state for the table.
59
82
  */
60
83
  export interface SortConfig {
61
84
  /** Column key being sorted */
62
85
  key: string;
63
86
  /** Sort direction */
64
- direction: 'asc' | 'desc';
87
+ direction: "asc" | "desc";
65
88
  /** Optional display label for sort indicator */
66
89
  label?: string;
67
90
  }
@@ -78,7 +101,7 @@ export interface DataTableAction<T> {
78
101
  /** Click handler receives the row item */
79
102
  onClick: (item: T) => void;
80
103
  /** Button styling variant */
81
- variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
104
+ variant?: "primary" | "secondary" | "ghost" | "danger";
82
105
  /** Optional conditional visibility */
83
106
  show?: (item: T) => boolean;
84
107
  /** Optional tooltip text */
@@ -88,7 +111,7 @@ export interface DataTableAction<T> {
88
111
  /**
89
112
  * Expansion mode types
90
113
  */
91
- export type ExpansionMode = 'edit' | 'details' | string; // string allows 'addRelated-[key]' | 'manageRelated-[key]'
114
+ export type ExpansionMode = "edit" | "details" | string; // string allows 'addRelated-[key]' | 'manageRelated-[key]'
92
115
 
93
116
  /**
94
117
  * Configuration for different expansion modes
@@ -96,12 +119,16 @@ export type ExpansionMode = 'edit' | 'details' | string; // string allows 'addRe
96
119
  export interface ExpandedRowConfig<T> {
97
120
  /** Edit mode - inline editing of the record */
98
121
  edit?: {
99
- render: (item: T, onSave: (updated: T) => Promise<void>, onCancel: () => void) => React.ReactNode;
122
+ render: (
123
+ item: T,
124
+ onSave: (updated: T) => Promise<void>,
125
+ onCancel: () => void,
126
+ ) => React.ReactNode;
100
127
  triggerOnDoubleClick?: boolean; // Default: true
101
128
  menuLabel?: string; // Default: 'Edit'
102
129
  menuIcon?: React.ComponentType<any>;
103
130
  };
104
-
131
+
105
132
  /** View details mode - read-only expanded view */
106
133
  details?: {
107
134
  render: (item: T) => React.ReactNode;
@@ -110,16 +137,20 @@ export interface ExpandedRowConfig<T> {
110
137
  menuLabel?: string; // Default: 'View Details'
111
138
  menuIcon?: React.ComponentType<any>;
112
139
  };
113
-
140
+
114
141
  /** Add related modes - creating related records */
115
142
  addRelated?: Array<{
116
143
  key: string; // Unique identifier for this add related mode
117
144
  label: string; // e.g., 'Add Line Item', 'Add Contact'
118
145
  icon?: React.ComponentType<any>;
119
- render: (parentItem: T, onSave: (newItem: any) => Promise<void>, onCancel: () => void) => React.ReactNode;
146
+ render: (
147
+ parentItem: T,
148
+ onSave: (newItem: any) => Promise<void>,
149
+ onCancel: () => void,
150
+ ) => React.ReactNode;
120
151
  showInMenu?: boolean; // Default: true
121
152
  }>;
122
-
153
+
123
154
  /** Manage related modes - viewing/editing related records */
124
155
  manageRelated?: Array<{
125
156
  key: string; // Unique identifier for this manage related mode
@@ -140,7 +171,7 @@ interface ExpansionState {
140
171
 
141
172
  /**
142
173
  * DataTable component props
143
- *
174
+ *
144
175
  * Feature-rich data table with sorting, filtering, selection, expansion,
145
176
  * row actions, and virtual scrolling support.
146
177
  */
@@ -196,11 +227,11 @@ interface DataTableProps<T extends BaseDataItem = BaseDataItem> {
196
227
 
197
228
  // Visual customization props
198
229
  /** Enable zebra striping - true for default, 'odd' or 'even' for specific rows */
199
- striped?: boolean | 'odd' | 'even';
230
+ striped?: boolean | "odd" | "even";
200
231
  /** Custom color for striped rows (Tailwind class like 'bg-primary-50' or 'bg-accent-50') */
201
232
  stripedColor?: string;
202
233
  /** Row density - affects padding and text size */
203
- density?: 'compact' | 'normal' | 'comfortable';
234
+ density?: "compact" | "normal" | "comfortable";
204
235
  /** Custom class name for rows - static string or function returning class per row */
205
236
  rowClassName?: string | ((item: T, index: number) => string);
206
237
  /** Conditional row highlighting - returns color class (e.g., 'bg-warning-50') */
@@ -258,11 +289,11 @@ interface DataTableProps<T extends BaseDataItem = BaseDataItem> {
258
289
 
259
290
  // Mobile view props
260
291
  /** Mobile view mode: 'auto' (detect viewport), 'card' (always cards), 'table' (always table) */
261
- mobileView?: 'auto' | 'card' | 'table';
292
+ mobileView?: "auto" | "card" | "table";
262
293
  /** Configuration for card view layout */
263
294
  cardConfig?: CardViewConfig<T>;
264
295
  /** Gap between cards in card view */
265
- cardGap?: 'sm' | 'md' | 'lg';
296
+ cardGap?: "sm" | "md" | "lg";
266
297
  /** Custom class name for cards */
267
298
  cardClassName?: string;
268
299
  }
@@ -315,34 +346,38 @@ function ActionMenu<T>({
315
346
  useEffect(() => {
316
347
  const handleClickOutside = (event: MouseEvent) => {
317
348
  if (
318
- menuRef.current && !menuRef.current.contains(event.target as Node) &&
319
- buttonRef.current && !buttonRef.current.contains(event.target as Node)
349
+ menuRef.current &&
350
+ !menuRef.current.contains(event.target as Node) &&
351
+ buttonRef.current &&
352
+ !buttonRef.current.contains(event.target as Node)
320
353
  ) {
321
354
  setIsOpen(false);
322
355
  }
323
356
  };
324
357
 
325
358
  if (isOpen) {
326
- document.addEventListener('mousedown', handleClickOutside);
359
+ document.addEventListener("mousedown", handleClickOutside);
327
360
  }
328
361
 
329
362
  return () => {
330
- document.removeEventListener('mousedown', handleClickOutside);
363
+ document.removeEventListener("mousedown", handleClickOutside);
331
364
  };
332
365
  }, [isOpen]);
333
366
 
334
- const visibleActions = actions.filter(action => !action.show || action.show(item));
367
+ const visibleActions = actions.filter(
368
+ (action) => !action.show || action.show(item),
369
+ );
335
370
 
336
371
  if (visibleActions.length === 0) return null;
337
372
 
338
373
  const dropdownContent = isOpen && (
339
- <div
374
+ <div
340
375
  ref={menuRef}
341
- className="fixed w-56 bg-white rounded-lg shadow-lg border border-paper-300 py-1"
376
+ className="fixed w-56 bg-white rounded-lg shadow-lg border border-paper-300 py-1"
342
377
  style={{
343
378
  zIndex: 999999,
344
379
  top: `${position.top}px`,
345
- left: `${position.left}px`
380
+ left: `${position.left}px`,
346
381
  }}
347
382
  >
348
383
  {visibleActions.map((action, idx) => {
@@ -351,10 +386,12 @@ function ActionMenu<T>({
351
386
  if (React.isValidElement(action.icon)) {
352
387
  iconElement = action.icon;
353
388
  } else {
354
- iconElement = React.createElement(action.icon as any, { className: 'h-4 w-4 flex-shrink-0' });
389
+ iconElement = React.createElement(action.icon as any, {
390
+ className: "h-4 w-4 flex-shrink-0",
391
+ });
355
392
  }
356
393
  }
357
-
394
+
358
395
  return (
359
396
  <button
360
397
  key={idx}
@@ -365,14 +402,14 @@ function ActionMenu<T>({
365
402
  try {
366
403
  await action.onClick(item);
367
404
  } catch (error) {
368
- console.error('DataTable action error:', error);
405
+ console.error("DataTable action error:", error);
369
406
  }
370
407
  setIsOpen(false);
371
408
  }}
372
409
  className={`w-full flex items-center gap-3 px-4 py-2.5 text-sm transition-colors ${
373
- action.variant === 'danger'
374
- ? 'text-error-600 hover:bg-error-50 hover:text-error-700'
375
- : 'text-ink-700 hover:bg-paper-50'
410
+ action.variant === "danger"
411
+ ? "text-error-600 hover:bg-error-50 hover:text-error-700"
412
+ : "text-ink-700 hover:bg-paper-50"
376
413
  }`}
377
414
  title={action.tooltip}
378
415
  >
@@ -406,44 +443,54 @@ function ActionMenu<T>({
406
443
  /**
407
444
  * Helper function to generate column styles from width configuration
408
445
  */
409
- function getColumnStyle<T>(column: DataTableColumn<T>, dynamicWidth?: number): React.CSSProperties {
446
+ function getColumnStyle<T>(
447
+ column: DataTableColumn<T>,
448
+ dynamicWidth?: number,
449
+ ): React.CSSProperties {
410
450
  const style: React.CSSProperties = {};
411
-
451
+
412
452
  // Use dynamic width if provided (from resizing)
413
453
  if (dynamicWidth !== undefined) {
414
454
  style.width = `${dynamicWidth}px`;
415
455
  } else if (column.width !== undefined) {
416
- style.width = typeof column.width === 'number' ? `${column.width}px` : column.width;
456
+ style.width =
457
+ typeof column.width === "number" ? `${column.width}px` : column.width;
417
458
  }
418
-
459
+
419
460
  if (column.minWidth !== undefined) {
420
- style.minWidth = typeof column.minWidth === 'number' ? `${column.minWidth}px` : column.minWidth;
461
+ style.minWidth =
462
+ typeof column.minWidth === "number"
463
+ ? `${column.minWidth}px`
464
+ : column.minWidth;
421
465
  }
422
-
466
+
423
467
  if (column.maxWidth !== undefined) {
424
- style.maxWidth = typeof column.maxWidth === 'number' ? `${column.maxWidth}px` : column.maxWidth;
468
+ style.maxWidth =
469
+ typeof column.maxWidth === "number"
470
+ ? `${column.maxWidth}px`
471
+ : column.maxWidth;
425
472
  }
426
-
473
+
427
474
  if (column.flex !== undefined) {
428
475
  style.flexGrow = column.flex;
429
476
  style.flexShrink = 1;
430
477
  style.flexBasis = 0;
431
478
  }
432
-
479
+
433
480
  if (column.align) {
434
481
  style.textAlign = column.align;
435
482
  }
436
-
483
+
437
484
  if (column.verticalAlign) {
438
485
  style.verticalAlign = column.verticalAlign;
439
486
  }
440
-
487
+
441
488
  return style;
442
489
  }
443
490
 
444
491
  /**
445
492
  * DataTable - Feature-rich data table component
446
- *
493
+ *
447
494
  * Features:
448
495
  * - Column sorting with visual indicators (3-state: asc → desc → none)
449
496
  * - Loading states (skeleton + overlay)
@@ -457,7 +504,7 @@ function getColumnStyle<T>(column: DataTableColumn<T>, dynamicWidth?: number): R
457
504
  * - Custom cell rendering
458
505
  * - Controlled or uncontrolled selection and expansion
459
506
  * - Column width configuration (width, minWidth, maxWidth, flex)
460
- *
507
+ *
461
508
  * @example
462
509
  * ```tsx
463
510
  * const columns: DataTableColumn<User>[] = [
@@ -465,12 +512,12 @@ function getColumnStyle<T>(column: DataTableColumn<T>, dynamicWidth?: number): R
465
512
  * { key: 'email', header: 'Email', sortable: true, width: '250px' },
466
513
  * { key: 'role', header: 'Role', width: '120px' },
467
514
  * ];
468
- *
515
+ *
469
516
  * const actions: DataTableAction<User>[] = [
470
517
  * { label: 'Edit', icon: Edit, onClick: handleEdit },
471
518
  * { label: 'Delete', icon: Trash, onClick: handleDelete, variant: 'danger' },
472
519
  * ];
473
- *
520
+ *
474
521
  * <DataTable
475
522
  * data={users}
476
523
  * columns={columns}
@@ -490,9 +537,9 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
490
537
  columns,
491
538
  loading = false,
492
539
  error = null,
493
- emptyMessage = 'No data available',
540
+ emptyMessage = "No data available",
494
541
  loadingRows = 5,
495
- className = '',
542
+ className = "",
496
543
  onSortChange,
497
544
  currentSort = null,
498
545
  onEdit,
@@ -513,24 +560,24 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
513
560
  // Visual customization props
514
561
  striped = false,
515
562
  stripedColor,
516
- density = 'normal',
563
+ density = "normal",
517
564
  rowClassName,
518
565
  rowHighlight,
519
566
  highlightedRowId,
520
567
  highlightedRows = [],
521
568
  highlightDuration = 2000,
522
569
  bordered = false,
523
- borderColor = 'border-paper-200',
570
+ borderColor = "border-paper-200",
524
571
  disableHover = false,
525
572
  hiddenColumns = [],
526
- headerClassName = '',
573
+ headerClassName = "",
527
574
  renderEmptyState: customRenderEmptyState,
528
575
  resizable = false,
529
576
  onColumnResize,
530
577
  reorderable = false,
531
578
  onColumnReorder,
532
579
  virtualized = false,
533
- virtualHeight = '600px',
580
+ virtualHeight = "600px",
534
581
  virtualRowHeight = 60,
535
582
  // Pagination props
536
583
  paginated = false,
@@ -542,9 +589,9 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
542
589
  onPageSizeChange,
543
590
  showPageSizeSelector = true,
544
591
  // Mobile view props
545
- mobileView = 'auto',
592
+ mobileView = "auto",
546
593
  cardConfig,
547
- cardGap = 'md',
594
+ cardGap = "md",
548
595
  cardClassName,
549
596
  }: DataTableProps<T>) {
550
597
  // Mobile detection for auto mode
@@ -568,8 +615,11 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
568
615
  const [hoveredRowKey, setHoveredRowKey] = useState<string | null>(null);
569
616
 
570
617
  // Keyboard navigation state
571
- const [focusedCell, setFocusedCell] = useState<{ row: number; col: number } | null>(null);
572
- const [announcement, setAnnouncement] = useState<string>('');
618
+ const [focusedCell, setFocusedCell] = useState<{
619
+ row: number;
620
+ col: number;
621
+ } | null>(null);
622
+ const [announcement, setAnnouncement] = useState<string>("");
573
623
  const tableBodyRef = useRef<HTMLTableSectionElement>(null);
574
624
 
575
625
  // Temporary row highlight state (for flash animation)
@@ -589,13 +639,13 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
589
639
 
590
640
  // Filter columns based on hiddenColumns
591
641
  const baseVisibleColumns = columns.filter(
592
- col => !hiddenColumns.includes(String(col.key))
642
+ (col) => !hiddenColumns.includes(String(col.key)),
593
643
  );
594
644
 
595
645
  // Initialize column order on mount or when columns change
596
646
  useEffect(() => {
597
647
  if (columnOrder.length === 0) {
598
- setColumnOrder(baseVisibleColumns.map(col => String(col.key)));
648
+ setColumnOrder(baseVisibleColumns.map((col) => String(col.key)));
599
649
  }
600
650
  }, [baseVisibleColumns, columnOrder.length]);
601
651
 
@@ -603,7 +653,7 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
603
653
  useEffect(() => {
604
654
  if (highlightedRows.length > 0) {
605
655
  // Add new highlighted rows to flashing set
606
- const newFlashingRows = new Set(highlightedRows.map(id => String(id)));
656
+ const newFlashingRows = new Set(highlightedRows.map((id) => String(id)));
607
657
  setFlashingRows(newFlashingRows);
608
658
 
609
659
  // Clear any existing timeout
@@ -625,28 +675,31 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
625
675
  }, [highlightedRows, highlightDuration]);
626
676
 
627
677
  // Apply column order
628
- const visibleColumns = reorderable && columnOrder.length > 0
629
- ? columnOrder
630
- .map(key => baseVisibleColumns.find(col => String(col.key) === key))
631
- .filter((col): col is DataTableColumn<T> => col !== undefined)
632
- : baseVisibleColumns;
678
+ const visibleColumns =
679
+ reorderable && columnOrder.length > 0
680
+ ? columnOrder
681
+ .map((key) =>
682
+ baseVisibleColumns.find((col) => String(col.key) === key),
683
+ )
684
+ .filter((col): col is DataTableColumn<T> => col !== undefined)
685
+ : baseVisibleColumns;
633
686
 
634
687
  // Density classes
635
688
  const densityClasses = {
636
689
  compact: {
637
- cell: 'px-3 py-1',
638
- text: 'text-xs',
639
- header: 'px-3 py-2',
690
+ cell: "px-3 py-1",
691
+ text: "text-xs",
692
+ header: "px-3 py-2",
640
693
  },
641
694
  normal: {
642
- cell: 'px-6 py-1.5',
643
- text: 'text-sm',
644
- header: 'px-6 py-3',
695
+ cell: "px-6 py-1.5",
696
+ text: "text-sm",
697
+ header: "px-6 py-3",
645
698
  },
646
699
  comfortable: {
647
- cell: 'px-6 py-3',
648
- text: 'text-base',
649
- header: 'px-6 py-4',
700
+ cell: "px-6 py-3",
701
+ text: "text-base",
702
+ header: "px-6 py-4",
650
703
  },
651
704
  };
652
705
 
@@ -657,9 +710,15 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
657
710
 
658
711
  // Calculate if there are any actions (for keyboard navigation column calculation)
659
712
  // This is computed early so it can be used in keyboard handlers
660
- const hasAnyActions = !!(onEdit || onDelete || actions.length > 0 ||
661
- expandedRowConfig?.edit || expandedRowConfig?.details ||
662
- expandedRowConfig?.addRelated?.length || expandedRowConfig?.manageRelated?.length);
713
+ const hasAnyActions = !!(
714
+ onEdit ||
715
+ onDelete ||
716
+ actions.length > 0 ||
717
+ expandedRowConfig?.edit ||
718
+ expandedRowConfig?.details ||
719
+ expandedRowConfig?.addRelated?.length ||
720
+ expandedRowConfig?.manageRelated?.length
721
+ );
663
722
 
664
723
  // Get row background class based on striping and highlighting
665
724
  const getRowBackgroundClass = (item: T, index: number): string => {
@@ -668,11 +727,14 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
668
727
 
669
728
  // Check for temporary flash highlight (takes priority)
670
729
  if (flashingRows.has(rowKey)) {
671
- classes.push('animate-row-flash');
730
+ classes.push("animate-row-flash");
672
731
  }
673
732
  // Check for highlighted row
674
- else if (highlightedRowId !== undefined && rowKey === String(highlightedRowId)) {
675
- classes.push('bg-accent-100');
733
+ else if (
734
+ highlightedRowId !== undefined &&
735
+ rowKey === String(highlightedRowId)
736
+ ) {
737
+ classes.push("bg-accent-100");
676
738
  }
677
739
  // Check for custom row highlight
678
740
  else if (rowHighlight) {
@@ -685,32 +747,41 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
685
747
  else if (striped) {
686
748
  const isOdd = index % 2 === 0; // 0-indexed, so even index = odd row
687
749
  const shouldStripe =
688
- striped === true ? isOdd :
689
- striped === 'odd' ? isOdd :
690
- striped === 'even' ? !isOdd :
691
- false;
750
+ striped === true
751
+ ? isOdd
752
+ : striped === "odd"
753
+ ? isOdd
754
+ : striped === "even"
755
+ ? !isOdd
756
+ : false;
692
757
 
693
758
  if (shouldStripe) {
694
- classes.push(stripedColor || 'bg-paper-50');
759
+ classes.push(stripedColor || "bg-paper-50");
695
760
  }
696
761
  }
697
762
 
698
763
  // Add custom row class
699
764
  if (rowClassName) {
700
- if (typeof rowClassName === 'string') {
765
+ if (typeof rowClassName === "string") {
701
766
  classes.push(rowClassName);
702
767
  } else {
703
768
  classes.push(rowClassName(item, index));
704
769
  }
705
770
  }
706
771
 
707
- return classes.join(' ');
772
+ return classes.join(" ");
708
773
  };
709
774
  // NEW: Expansion mode state management (for expandedRowConfig)
710
- const [expansionState, setExpansionState] = useState<ExpansionState | null>(null);
775
+ const [expansionState, setExpansionState] = useState<ExpansionState | null>(
776
+ null,
777
+ );
711
778
 
712
779
  // Column resize handlers
713
- const handleResizeStart = (e: React.MouseEvent, columnKey: string, currentWidth: number) => {
780
+ const handleResizeStart = (
781
+ e: React.MouseEvent,
782
+ columnKey: string,
783
+ currentWidth: number,
784
+ ) => {
714
785
  e.preventDefault();
715
786
  e.stopPropagation();
716
787
  setResizingColumn(columnKey);
@@ -721,13 +792,13 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
721
792
  // Column reorder handlers
722
793
  const handleDragStart = (e: React.DragEvent, columnKey: string) => {
723
794
  setDraggingColumn(columnKey);
724
- e.dataTransfer.effectAllowed = 'move';
725
- e.dataTransfer.setData('text/html', columnKey);
795
+ e.dataTransfer.effectAllowed = "move";
796
+ e.dataTransfer.setData("text/html", columnKey);
726
797
  };
727
798
 
728
799
  const handleDragOver = (e: React.DragEvent, columnKey: string) => {
729
800
  e.preventDefault();
730
- e.dataTransfer.dropEffect = 'move';
801
+ e.dataTransfer.dropEffect = "move";
731
802
  if (draggingColumn && draggingColumn !== columnKey) {
732
803
  setDragOverColumn(columnKey);
733
804
  }
@@ -763,7 +834,7 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
763
834
  const handleMouseMove = (e: MouseEvent) => {
764
835
  const delta = e.clientX - resizeStartX;
765
836
  const newWidth = Math.max(50, resizeStartWidth + delta); // Min width 50px
766
- setColumnWidths(prev => ({
837
+ setColumnWidths((prev) => ({
767
838
  ...prev,
768
839
  [resizingColumn]: newWidth,
769
840
  }));
@@ -776,62 +847,68 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
776
847
  setResizingColumn(null);
777
848
  };
778
849
 
779
- document.addEventListener('mousemove', handleMouseMove);
780
- document.addEventListener('mouseup', handleMouseUp);
850
+ document.addEventListener("mousemove", handleMouseMove);
851
+ document.addEventListener("mouseup", handleMouseUp);
781
852
 
782
853
  return () => {
783
- document.removeEventListener('mousemove', handleMouseMove);
784
- document.removeEventListener('mouseup', handleMouseUp);
854
+ document.removeEventListener("mousemove", handleMouseMove);
855
+ document.removeEventListener("mouseup", handleMouseUp);
785
856
  };
786
- }, [resizingColumn, resizeStartX, resizeStartWidth, columnWidths, onColumnResize]);
857
+ }, [
858
+ resizingColumn,
859
+ resizeStartX,
860
+ resizeStartWidth,
861
+ columnWidths,
862
+ onColumnResize,
863
+ ]);
787
864
 
788
865
  // Build combined actions: built-in edit/delete + custom actions + expansion mode actions
789
866
  const builtInActions: DataTableAction<T>[] = [];
790
-
867
+
791
868
  // Legacy onEdit (still supported)
792
869
  if (onEdit) {
793
870
  builtInActions.push({
794
- label: 'Edit',
871
+ label: "Edit",
795
872
  icon: Edit,
796
873
  onClick: onEdit,
797
- variant: 'secondary',
798
- tooltip: 'Edit item'
874
+ variant: "secondary",
875
+ tooltip: "Edit item",
799
876
  });
800
877
  }
801
-
878
+
802
879
  // NEW: Edit mode from expandedRowConfig
803
880
  if (expandedRowConfig?.edit && !onEdit) {
804
881
  const editConfig = expandedRowConfig.edit;
805
882
  builtInActions.push({
806
- label: editConfig.menuLabel || 'Edit',
883
+ label: editConfig.menuLabel || "Edit",
807
884
  icon: editConfig.menuIcon || Edit,
808
885
  onClick: (item: T) => {
809
886
  const rowKey = getRowKey(item);
810
- handleExpansionWithMode(rowKey, 'edit');
887
+ handleExpansionWithMode(rowKey, "edit");
811
888
  },
812
- variant: 'secondary',
813
- tooltip: 'Edit inline'
889
+ variant: "secondary",
890
+ tooltip: "Edit inline",
814
891
  });
815
892
  }
816
-
893
+
817
894
  // NEW: View details mode from expandedRowConfig
818
895
  if (expandedRowConfig?.details) {
819
896
  const detailsConfig = expandedRowConfig.details;
820
897
  builtInActions.push({
821
- label: detailsConfig.menuLabel || 'View Details',
898
+ label: detailsConfig.menuLabel || "View Details",
822
899
  icon: detailsConfig.menuIcon,
823
900
  onClick: (item: T) => {
824
901
  const rowKey = getRowKey(item);
825
- handleExpansionWithMode(rowKey, 'details');
902
+ handleExpansionWithMode(rowKey, "details");
826
903
  },
827
- variant: 'ghost',
828
- tooltip: 'View details'
904
+ variant: "ghost",
905
+ tooltip: "View details",
829
906
  });
830
907
  }
831
-
908
+
832
909
  // NEW: Add related modes from expandedRowConfig
833
910
  if (expandedRowConfig?.addRelated) {
834
- expandedRowConfig.addRelated.forEach(config => {
911
+ expandedRowConfig.addRelated.forEach((config) => {
835
912
  if (config.showInMenu !== false) {
836
913
  builtInActions.push({
837
914
  label: config.label,
@@ -840,16 +917,16 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
840
917
  const rowKey = getRowKey(item);
841
918
  handleExpansionWithMode(rowKey, `addRelated-${config.key}`);
842
919
  },
843
- variant: 'secondary',
844
- tooltip: config.label
920
+ variant: "secondary",
921
+ tooltip: config.label,
845
922
  });
846
923
  }
847
924
  });
848
925
  }
849
-
926
+
850
927
  // NEW: Manage related modes from expandedRowConfig
851
928
  if (expandedRowConfig?.manageRelated) {
852
- expandedRowConfig.manageRelated.forEach(config => {
929
+ expandedRowConfig.manageRelated.forEach((config) => {
853
930
  if (config.showInMenu !== false) {
854
931
  builtInActions.push({
855
932
  label: config.label,
@@ -858,26 +935,26 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
858
935
  const rowKey = getRowKey(item);
859
936
  handleExpansionWithMode(rowKey, `manageRelated-${config.key}`);
860
937
  },
861
- variant: 'ghost',
862
- tooltip: config.label
938
+ variant: "ghost",
939
+ tooltip: config.label,
863
940
  });
864
941
  }
865
942
  });
866
943
  }
867
-
944
+
868
945
  // Combine all actions: built-in first, then custom actions, then delete last
869
946
  // Delete is stored separately to ensure it's always last
870
947
  let deleteAction: DataTableAction<T> | null = null;
871
948
  if (onDelete) {
872
949
  deleteAction = {
873
- label: 'Delete',
950
+ label: "Delete",
874
951
  icon: Trash,
875
952
  onClick: onDelete,
876
- variant: 'danger',
877
- tooltip: 'Delete item'
953
+ variant: "danger",
954
+ tooltip: "Delete item",
878
955
  };
879
956
  }
880
-
957
+
881
958
  // Build final actions array with consistent ordering:
882
959
  // 1. Edit (first - most common action)
883
960
  // 2. View Details
@@ -888,12 +965,14 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
888
965
  const allActions: DataTableAction<T>[] = [
889
966
  ...builtInActions,
890
967
  ...actions,
891
- ...(deleteAction ? [deleteAction] : [])
968
+ ...(deleteAction ? [deleteAction] : []),
892
969
  ];
893
970
 
894
971
  // Convert actions to menu items for context menu
895
972
  const convertActionsToMenuItems = (item: T): MenuItem[] => {
896
- const visibleActions = allActions.filter(action => !action.show || action.show(item));
973
+ const visibleActions = allActions.filter(
974
+ (action) => !action.show || action.show(item),
975
+ );
897
976
 
898
977
  return visibleActions.map((action, idx) => {
899
978
  let iconElement: React.ReactNode = null;
@@ -901,7 +980,9 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
901
980
  if (React.isValidElement(action.icon)) {
902
981
  iconElement = action.icon;
903
982
  } else {
904
- iconElement = React.createElement(action.icon as any, { className: 'h-4 w-4' });
983
+ iconElement = React.createElement(action.icon as any, {
984
+ className: "h-4 w-4",
985
+ });
905
986
  }
906
987
  }
907
988
 
@@ -910,19 +991,26 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
910
991
  label: action.label,
911
992
  icon: iconElement,
912
993
  onClick: () => action.onClick(item),
913
- danger: action.variant === 'danger',
994
+ danger: action.variant === "danger",
914
995
  };
915
996
  });
916
997
  };
917
998
 
918
999
  // Selection state management
919
- const [internalSelectedRows, setInternalSelectedRows] = useState<Set<string>>(new Set());
920
-
1000
+ const [internalSelectedRows, setInternalSelectedRows] = useState<Set<string>>(
1001
+ new Set(),
1002
+ );
1003
+
921
1004
  // Expansion state management
922
- const [internalExpandedRows, setInternalExpandedRows] = useState<Set<string>>(new Set());
923
-
1005
+ const [internalExpandedRows, setInternalExpandedRows] = useState<Set<string>>(
1006
+ new Set(),
1007
+ );
1008
+
924
1009
  // Use external selection if provided, otherwise internal
925
- const selectedRowsSet = externalSelectedRows !== undefined ? externalSelectedRows : internalSelectedRows;
1010
+ const selectedRowsSet =
1011
+ externalSelectedRows !== undefined
1012
+ ? externalSelectedRows
1013
+ : internalSelectedRows;
926
1014
  const setSelectedRows = (newSet: Set<string>) => {
927
1015
  if (externalSelectedRows !== undefined) {
928
1016
  // Controlled component - notify parent
@@ -944,7 +1032,7 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
944
1032
  }
945
1033
  setSelectedRows(newSelected);
946
1034
  };
947
-
1035
+
948
1036
  // Handle select all
949
1037
  const handleSelectAll = () => {
950
1038
  if (selectedRowsSet.size === data.length && data.length > 0) {
@@ -954,9 +1042,12 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
954
1042
  setSelectedRows(allKeys);
955
1043
  }
956
1044
  };
957
-
1045
+
958
1046
  // Use external expansion if provided, otherwise internal
959
- const expandedRowsSet = externalExpandedRows !== undefined ? externalExpandedRows : internalExpandedRows;
1047
+ const expandedRowsSet =
1048
+ externalExpandedRows !== undefined
1049
+ ? externalExpandedRows
1050
+ : internalExpandedRows;
960
1051
  const setExpandedRows = (newSet: Set<string>) => {
961
1052
  if (externalExpandedRows !== undefined) {
962
1053
  // Controlled component - parent manages state
@@ -966,7 +1057,7 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
966
1057
  setInternalExpandedRows(newSet);
967
1058
  }
968
1059
  };
969
-
1060
+
970
1061
  // Handle row expansion
971
1062
  const handleRowExpand = (rowKey: string) => {
972
1063
  const newExpanded = new Set(expandedRowsSet);
@@ -995,219 +1086,272 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
995
1086
  };
996
1087
 
997
1088
  // Keyboard navigation handler
998
- const handleKeyboardNavigation = useCallback((e: React.KeyboardEvent<HTMLTableSectionElement>) => {
999
- if (!data.length) return;
1000
-
1001
- const totalRows = data.length;
1002
- const totalCols = visibleColumns.length;
1003
-
1004
- // If no cell is focused, focus first data cell on first arrow key
1005
- if (!focusedCell) {
1006
- if (['ArrowDown', 'ArrowUp', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
1007
- e.preventDefault();
1008
- setFocusedCell({ row: 0, col: 0 });
1009
- const colHeader = visibleColumns[0]?.header || 'first column';
1010
- setAnnouncement(`Row 1, ${colHeader}`);
1089
+ const handleKeyboardNavigation = useCallback(
1090
+ (e: React.KeyboardEvent<HTMLTableSectionElement>) => {
1091
+ if (!data.length) return;
1092
+
1093
+ const totalRows = data.length;
1094
+ const totalCols = visibleColumns.length;
1095
+
1096
+ // If no cell is focused, focus first data cell on first arrow key
1097
+ if (!focusedCell) {
1098
+ if (
1099
+ ["ArrowDown", "ArrowUp", "ArrowLeft", "ArrowRight"].includes(e.key)
1100
+ ) {
1101
+ e.preventDefault();
1102
+ setFocusedCell({ row: 0, col: 0 });
1103
+ const colHeader = visibleColumns[0]?.header || "first column";
1104
+ setAnnouncement(`Row 1, ${colHeader}`);
1105
+ return;
1106
+ }
1011
1107
  return;
1012
1108
  }
1013
- return;
1014
- }
1015
1109
 
1016
- const { row, col } = focusedCell;
1017
-
1018
- switch (e.key) {
1019
- case 'ArrowDown':
1020
- e.preventDefault();
1021
- if (row < totalRows - 1) {
1022
- const newRow = row + 1;
1023
- setFocusedCell({ row: newRow, col });
1024
- const rowItem = data[newRow];
1025
- const colHeader = visibleColumns[col]?.header || '';
1026
- const cellValue = rowItem[visibleColumns[col]?.key as keyof typeof rowItem];
1027
- setAnnouncement(`Row ${newRow + 1}, ${colHeader}: ${cellValue || 'empty'}`);
1028
- }
1029
- break;
1030
-
1031
- case 'ArrowUp':
1032
- e.preventDefault();
1033
- if (row > 0) {
1034
- const newRow = row - 1;
1035
- setFocusedCell({ row: newRow, col });
1036
- const rowItem = data[newRow];
1037
- const colHeader = visibleColumns[col]?.header || '';
1038
- const cellValue = rowItem[visibleColumns[col]?.key as keyof typeof rowItem];
1039
- setAnnouncement(`Row ${newRow + 1}, ${colHeader}: ${cellValue || 'empty'}`);
1040
- }
1041
- break;
1042
-
1043
- case 'ArrowRight':
1044
- e.preventDefault();
1045
- if (col < totalCols - 1) {
1046
- const newCol = col + 1;
1047
- setFocusedCell({ row, col: newCol });
1048
- const rowItem = data[row];
1049
- const colHeader = visibleColumns[newCol]?.header || '';
1050
- const cellValue = rowItem[visibleColumns[newCol]?.key as keyof typeof rowItem];
1051
- setAnnouncement(`${colHeader}: ${cellValue || 'empty'}`);
1052
- }
1053
- break;
1054
-
1055
- case 'ArrowLeft':
1056
- e.preventDefault();
1057
- if (col > 0) {
1058
- const newCol = col - 1;
1059
- setFocusedCell({ row, col: newCol });
1060
- const rowItem = data[row];
1061
- const colHeader = visibleColumns[newCol]?.header || '';
1062
- const cellValue = rowItem[visibleColumns[newCol]?.key as keyof typeof rowItem];
1063
- setAnnouncement(`${colHeader}: ${cellValue || 'empty'}`);
1064
- }
1065
- break;
1110
+ const { row, col } = focusedCell;
1066
1111
 
1067
- case 'Home':
1068
- e.preventDefault();
1069
- if (e.ctrlKey) {
1070
- // Ctrl+Home: Go to first cell
1071
- setFocusedCell({ row: 0, col: 0 });
1072
- setAnnouncement(`First cell, Row 1, ${visibleColumns[0]?.header || ''}`);
1073
- } else {
1074
- // Home: Go to first cell in current row
1075
- setFocusedCell({ row, col: 0 });
1076
- const rowItem = data[row];
1077
- const cellValue = rowItem[visibleColumns[0]?.key as keyof typeof rowItem];
1078
- setAnnouncement(`${visibleColumns[0]?.header || ''}: ${cellValue || 'empty'}`);
1079
- }
1080
- break;
1081
-
1082
- case 'End':
1083
- e.preventDefault();
1084
- if (e.ctrlKey) {
1085
- // Ctrl+End: Go to last cell
1086
- const lastRow = totalRows - 1;
1087
- const lastCol = totalCols - 1;
1088
- setFocusedCell({ row: lastRow, col: lastCol });
1089
- setAnnouncement(`Last cell, Row ${lastRow + 1}, ${visibleColumns[lastCol]?.header || ''}`);
1090
- } else {
1091
- // End: Go to last cell in current row
1092
- const lastCol = totalCols - 1;
1093
- setFocusedCell({ row, col: lastCol });
1094
- const rowItem = data[row];
1095
- const cellValue = rowItem[visibleColumns[lastCol]?.key as keyof typeof rowItem];
1096
- setAnnouncement(`${visibleColumns[lastCol]?.header || ''}: ${cellValue || 'empty'}`);
1097
- }
1098
- break;
1099
-
1100
- case 'Enter':
1101
- e.preventDefault();
1102
- {
1103
- const rowItem = data[row];
1104
- const rowKey = getRowKey(rowItem);
1105
-
1106
- // Priority: Edit mode > Details mode > Row double-click handler
1107
- if (onEdit) {
1108
- onEdit(rowItem);
1109
- setAnnouncement('Opening edit mode');
1110
- } else if (expandedRowConfig?.edit) {
1111
- handleExpansionWithMode(rowKey, 'edit');
1112
- setAnnouncement('Opening inline edit');
1113
- } else if (expandedRowConfig?.details) {
1114
- handleExpansionWithMode(rowKey, 'details');
1115
- setAnnouncement('Opening details view');
1116
- } else if (onRowDoubleClick) {
1117
- onRowDoubleClick(rowItem);
1118
- setAnnouncement('Activating row');
1119
- } else if (onRowClick) {
1120
- onRowClick(rowItem);
1121
- setAnnouncement('Row selected');
1112
+ switch (e.key) {
1113
+ case "ArrowDown":
1114
+ e.preventDefault();
1115
+ if (row < totalRows - 1) {
1116
+ const newRow = row + 1;
1117
+ setFocusedCell({ row: newRow, col });
1118
+ const rowItem = data[newRow];
1119
+ const colHeader = visibleColumns[col]?.header || "";
1120
+ const cellValue =
1121
+ rowItem[visibleColumns[col]?.key as keyof typeof rowItem];
1122
+ setAnnouncement(
1123
+ `Row ${newRow + 1}, ${colHeader}: ${cellValue || "empty"}`,
1124
+ );
1122
1125
  }
1123
- }
1124
- break;
1126
+ break;
1125
1127
 
1126
- case ' ':
1127
- // Space: Toggle selection if selectable
1128
- if (selectable) {
1128
+ case "ArrowUp":
1129
1129
  e.preventDefault();
1130
- const rowItem = data[row];
1131
- const rowKey = getRowKey(rowItem);
1132
- handleRowSelect(rowKey);
1133
- const isNowSelected = !selectedRowsSet.has(rowKey);
1134
- setAnnouncement(isNowSelected ? 'Row selected' : 'Row deselected');
1135
- }
1136
- break;
1137
-
1138
- case 'Escape':
1139
- e.preventDefault();
1140
- setFocusedCell(null);
1141
- setAnnouncement('Table navigation exited');
1142
- // Return focus to table container
1143
- tableBodyRef.current?.closest('table')?.focus();
1144
- break;
1145
-
1146
- case 'PageDown':
1147
- e.preventDefault();
1148
- {
1149
- const jumpSize = 10;
1150
- const newRow = Math.min(row + jumpSize, totalRows - 1);
1151
- setFocusedCell({ row: newRow, col });
1152
- const colHeader = visibleColumns[col]?.header || '';
1153
- setAnnouncement(`Row ${newRow + 1} of ${totalRows}, ${colHeader}`);
1154
- }
1155
- break;
1156
-
1157
- case 'PageUp':
1158
- e.preventDefault();
1159
- {
1160
- const jumpSize = 10;
1161
- const newRow = Math.max(row - jumpSize, 0);
1162
- setFocusedCell({ row: newRow, col });
1163
- const colHeader = visibleColumns[col]?.header || '';
1164
- setAnnouncement(`Row ${newRow + 1} of ${totalRows}, ${colHeader}`);
1165
- }
1166
- break;
1167
- }
1168
- }, [data, visibleColumns, focusedCell, selectable, expandedRowConfig, onEdit, onRowDoubleClick, onRowClick, getRowKey, handleExpansionWithMode, handleRowSelect, selectedRowsSet]);
1130
+ if (row > 0) {
1131
+ const newRow = row - 1;
1132
+ setFocusedCell({ row: newRow, col });
1133
+ const rowItem = data[newRow];
1134
+ const colHeader = visibleColumns[col]?.header || "";
1135
+ const cellValue =
1136
+ rowItem[visibleColumns[col]?.key as keyof typeof rowItem];
1137
+ setAnnouncement(
1138
+ `Row ${newRow + 1}, ${colHeader}: ${cellValue || "empty"}`,
1139
+ );
1140
+ }
1141
+ break;
1142
+
1143
+ case "ArrowRight":
1144
+ e.preventDefault();
1145
+ if (col < totalCols - 1) {
1146
+ const newCol = col + 1;
1147
+ setFocusedCell({ row, col: newCol });
1148
+ const rowItem = data[row];
1149
+ const colHeader = visibleColumns[newCol]?.header || "";
1150
+ const cellValue =
1151
+ rowItem[visibleColumns[newCol]?.key as keyof typeof rowItem];
1152
+ setAnnouncement(`${colHeader}: ${cellValue || "empty"}`);
1153
+ }
1154
+ break;
1155
+
1156
+ case "ArrowLeft":
1157
+ e.preventDefault();
1158
+ if (col > 0) {
1159
+ const newCol = col - 1;
1160
+ setFocusedCell({ row, col: newCol });
1161
+ const rowItem = data[row];
1162
+ const colHeader = visibleColumns[newCol]?.header || "";
1163
+ const cellValue =
1164
+ rowItem[visibleColumns[newCol]?.key as keyof typeof rowItem];
1165
+ setAnnouncement(`${colHeader}: ${cellValue || "empty"}`);
1166
+ }
1167
+ break;
1168
+
1169
+ case "Home":
1170
+ e.preventDefault();
1171
+ if (e.ctrlKey) {
1172
+ // Ctrl+Home: Go to first cell
1173
+ setFocusedCell({ row: 0, col: 0 });
1174
+ setAnnouncement(
1175
+ `First cell, Row 1, ${visibleColumns[0]?.header || ""}`,
1176
+ );
1177
+ } else {
1178
+ // Home: Go to first cell in current row
1179
+ setFocusedCell({ row, col: 0 });
1180
+ const rowItem = data[row];
1181
+ const cellValue =
1182
+ rowItem[visibleColumns[0]?.key as keyof typeof rowItem];
1183
+ setAnnouncement(
1184
+ `${visibleColumns[0]?.header || ""}: ${cellValue || "empty"}`,
1185
+ );
1186
+ }
1187
+ break;
1188
+
1189
+ case "End":
1190
+ e.preventDefault();
1191
+ if (e.ctrlKey) {
1192
+ // Ctrl+End: Go to last cell
1193
+ const lastRow = totalRows - 1;
1194
+ const lastCol = totalCols - 1;
1195
+ setFocusedCell({ row: lastRow, col: lastCol });
1196
+ setAnnouncement(
1197
+ `Last cell, Row ${lastRow + 1}, ${visibleColumns[lastCol]?.header || ""}`,
1198
+ );
1199
+ } else {
1200
+ // End: Go to last cell in current row
1201
+ const lastCol = totalCols - 1;
1202
+ setFocusedCell({ row, col: lastCol });
1203
+ const rowItem = data[row];
1204
+ const cellValue =
1205
+ rowItem[visibleColumns[lastCol]?.key as keyof typeof rowItem];
1206
+ setAnnouncement(
1207
+ `${visibleColumns[lastCol]?.header || ""}: ${cellValue || "empty"}`,
1208
+ );
1209
+ }
1210
+ break;
1211
+
1212
+ case "Enter":
1213
+ e.preventDefault();
1214
+ {
1215
+ const rowItem = data[row];
1216
+ const rowKey = getRowKey(rowItem);
1217
+
1218
+ // Priority: Edit mode > Details mode > Row double-click handler
1219
+ if (onEdit) {
1220
+ onEdit(rowItem);
1221
+ setAnnouncement("Opening edit mode");
1222
+ } else if (expandedRowConfig?.edit) {
1223
+ handleExpansionWithMode(rowKey, "edit");
1224
+ setAnnouncement("Opening inline edit");
1225
+ } else if (expandedRowConfig?.details) {
1226
+ handleExpansionWithMode(rowKey, "details");
1227
+ setAnnouncement("Opening details view");
1228
+ } else if (onRowDoubleClick) {
1229
+ onRowDoubleClick(rowItem);
1230
+ setAnnouncement("Activating row");
1231
+ } else if (onRowClick) {
1232
+ onRowClick(rowItem);
1233
+ setAnnouncement("Row selected");
1234
+ }
1235
+ }
1236
+ break;
1237
+
1238
+ case " ":
1239
+ // Space: Toggle selection if selectable
1240
+ if (selectable) {
1241
+ e.preventDefault();
1242
+ const rowItem = data[row];
1243
+ const rowKey = getRowKey(rowItem);
1244
+ handleRowSelect(rowKey);
1245
+ const isNowSelected = !selectedRowsSet.has(rowKey);
1246
+ setAnnouncement(isNowSelected ? "Row selected" : "Row deselected");
1247
+ }
1248
+ break;
1249
+
1250
+ case "Escape":
1251
+ e.preventDefault();
1252
+ setFocusedCell(null);
1253
+ setAnnouncement("Table navigation exited");
1254
+ // Return focus to table container
1255
+ tableBodyRef.current?.closest("table")?.focus();
1256
+ break;
1257
+
1258
+ case "PageDown":
1259
+ e.preventDefault();
1260
+ {
1261
+ const jumpSize = 10;
1262
+ const newRow = Math.min(row + jumpSize, totalRows - 1);
1263
+ setFocusedCell({ row: newRow, col });
1264
+ const colHeader = visibleColumns[col]?.header || "";
1265
+ setAnnouncement(`Row ${newRow + 1} of ${totalRows}, ${colHeader}`);
1266
+ }
1267
+ break;
1268
+
1269
+ case "PageUp":
1270
+ e.preventDefault();
1271
+ {
1272
+ const jumpSize = 10;
1273
+ const newRow = Math.max(row - jumpSize, 0);
1274
+ setFocusedCell({ row: newRow, col });
1275
+ const colHeader = visibleColumns[col]?.header || "";
1276
+ setAnnouncement(`Row ${newRow + 1} of ${totalRows}, ${colHeader}`);
1277
+ }
1278
+ break;
1279
+ }
1280
+ },
1281
+ [
1282
+ data,
1283
+ visibleColumns,
1284
+ focusedCell,
1285
+ selectable,
1286
+ expandedRowConfig,
1287
+ onEdit,
1288
+ onRowDoubleClick,
1289
+ onRowClick,
1290
+ getRowKey,
1291
+ handleExpansionWithMode,
1292
+ handleRowSelect,
1293
+ selectedRowsSet,
1294
+ ],
1295
+ );
1169
1296
 
1170
1297
  // Focus the appropriate cell when focusedCell changes
1171
1298
  useEffect(() => {
1172
1299
  if (focusedCell && tableBodyRef.current) {
1173
1300
  const { row, col } = focusedCell;
1174
- const rows = tableBodyRef.current.querySelectorAll('tr[data-row-index]');
1301
+ const rows = tableBodyRef.current.querySelectorAll("tr[data-row-index]");
1175
1302
  const targetRow = rows[row] as HTMLTableRowElement | undefined;
1176
1303
  if (targetRow) {
1177
1304
  // Calculate actual column index including extra columns
1178
1305
  const hasSelectionCol = selectable;
1179
- const hasExpandCol = (expandable || expandedRowConfig) && showExpandChevron;
1306
+ const hasExpandCol =
1307
+ (expandable || expandedRowConfig) && showExpandChevron;
1180
1308
  const hasActionsCol = hasAnyActions;
1181
- const extraColsBefore = (hasSelectionCol ? 1 : 0) + (hasExpandCol ? 1 : 0) + (hasActionsCol ? 1 : 0);
1182
-
1183
- const cells = targetRow.querySelectorAll('td');
1184
- const targetCell = cells[col + extraColsBefore] as HTMLTableCellElement | undefined;
1309
+ const extraColsBefore =
1310
+ (hasSelectionCol ? 1 : 0) +
1311
+ (hasExpandCol ? 1 : 0) +
1312
+ (hasActionsCol ? 1 : 0);
1313
+
1314
+ const cells = targetRow.querySelectorAll("td");
1315
+ const targetCell = cells[col + extraColsBefore] as
1316
+ | HTMLTableCellElement
1317
+ | undefined;
1185
1318
  if (targetCell) {
1186
1319
  targetCell.focus();
1187
1320
  // Scroll into view if needed
1188
- targetCell.scrollIntoView({ block: 'nearest', inline: 'nearest' });
1321
+ targetCell.scrollIntoView({ block: "nearest", inline: "nearest" });
1189
1322
  }
1190
1323
  }
1191
1324
  }
1192
- }, [focusedCell, selectable, expandable, expandedRowConfig, showExpandChevron, hasAnyActions]);
1325
+ }, [
1326
+ focusedCell,
1327
+ selectable,
1328
+ expandable,
1329
+ expandedRowConfig,
1330
+ showExpandChevron,
1331
+ hasAnyActions,
1332
+ ]);
1193
1333
 
1194
1334
  // Handle column header click for sorting
1195
1335
  const handleSort = (column: DataTableColumn<T>) => {
1196
1336
  if (!column.sortable || !onSortChange) return;
1197
1337
 
1198
1338
  const columnKey = String(column.key);
1199
-
1339
+
1200
1340
  // If clicking the same column, toggle direction
1201
1341
  if (currentSort?.key === columnKey) {
1202
- if (currentSort.direction === 'asc') {
1203
- onSortChange({ key: columnKey, direction: 'desc', label: column.header });
1342
+ if (currentSort.direction === "asc") {
1343
+ onSortChange({
1344
+ key: columnKey,
1345
+ direction: "desc",
1346
+ label: column.header,
1347
+ });
1204
1348
  } else {
1205
1349
  // Remove sort on third click
1206
1350
  onSortChange(null);
1207
1351
  }
1208
1352
  } else {
1209
1353
  // New column - start with ascending
1210
- onSortChange({ key: columnKey, direction: 'asc', label: column.header });
1354
+ onSortChange({ key: columnKey, direction: "asc", label: column.header });
1211
1355
  }
1212
1356
  };
1213
1357
 
@@ -1217,13 +1361,23 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
1217
1361
 
1218
1362
  const columnKey = String(column.key);
1219
1363
  const isActive = currentSort?.key === columnKey;
1220
- const isAscending = currentSort?.direction === 'asc';
1364
+ const isAscending = currentSort?.direction === "asc";
1221
1365
 
1222
1366
  // Inactive state - show neutral up/down arrows
1223
1367
  if (!isActive) {
1224
1368
  return (
1225
- <svg className="ml-2 w-4 h-4 text-ink-400 group-hover:text-ink-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1226
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4" />
1369
+ <svg
1370
+ className="ml-2 w-4 h-4 text-ink-400 group-hover:text-ink-700"
1371
+ fill="none"
1372
+ stroke="currentColor"
1373
+ viewBox="0 0 24 24"
1374
+ >
1375
+ <path
1376
+ strokeLinecap="round"
1377
+ strokeLinejoin="round"
1378
+ strokeWidth={2}
1379
+ d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4"
1380
+ />
1227
1381
  </svg>
1228
1382
  );
1229
1383
  }
@@ -1231,16 +1385,36 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
1231
1385
  // Active ascending state - show up arrow highlighted
1232
1386
  if (isAscending) {
1233
1387
  return (
1234
- <svg className="ml-2 w-4 h-4 text-accent-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1235
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 4h13M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4v12" />
1388
+ <svg
1389
+ className="ml-2 w-4 h-4 text-accent-600"
1390
+ fill="none"
1391
+ stroke="currentColor"
1392
+ viewBox="0 0 24 24"
1393
+ >
1394
+ <path
1395
+ strokeLinecap="round"
1396
+ strokeLinejoin="round"
1397
+ strokeWidth={2}
1398
+ d="M3 4h13M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4v12"
1399
+ />
1236
1400
  </svg>
1237
1401
  );
1238
1402
  }
1239
1403
 
1240
1404
  // Active descending state - show down arrow highlighted
1241
1405
  return (
1242
- <svg className="ml-2 w-4 h-4 text-accent-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1243
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 17l-4 4m0 0l-4-4m4 4V3" />
1406
+ <svg
1407
+ className="ml-2 w-4 h-4 text-accent-600"
1408
+ fill="none"
1409
+ stroke="currentColor"
1410
+ viewBox="0 0 24 24"
1411
+ >
1412
+ <path
1413
+ strokeLinecap="round"
1414
+ strokeLinejoin="round"
1415
+ strokeWidth={2}
1416
+ d="M16 17l-4 4m0 0l-4-4m4 4V3"
1417
+ />
1244
1418
  </svg>
1245
1419
  );
1246
1420
  };
@@ -1249,19 +1423,28 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
1249
1423
  const renderLoadingSkeleton = () => (
1250
1424
  <>
1251
1425
  {Array.from({ length: loadingRows }, (_, i) => (
1252
- <tr key={`loading-${i}`} className={`animate-pulse table-row-stable ${bordered ? `border-b ${borderColor}` : ''}`}>
1426
+ <tr
1427
+ key={`loading-${i}`}
1428
+ className={`animate-pulse table-row-stable ${bordered ? `border-b ${borderColor}` : ""}`}
1429
+ >
1253
1430
  {selectable && (
1254
- <td className={`sticky left-0 bg-white ${currentDensity.cell} border-b ${borderColor} z-10 align-middle`}>
1431
+ <td
1432
+ className={`sticky left-0 bg-white ${currentDensity.cell} border-b ${borderColor} z-10 align-middle`}
1433
+ >
1255
1434
  <div className="h-4 w-4 bg-paper-200 rounded"></div>
1256
1435
  </td>
1257
1436
  )}
1258
1437
  {expandable && (
1259
- <td className={`sticky left-0 bg-white px-2 ${currentDensity.cell} border-b ${borderColor} z-10`}>
1438
+ <td
1439
+ className={`sticky left-0 bg-white px-2 ${currentDensity.cell} border-b ${borderColor} z-10`}
1440
+ >
1260
1441
  <div className="h-4 w-4 bg-paper-200 rounded"></div>
1261
1442
  </td>
1262
1443
  )}
1263
1444
  {allActions.length > 0 && (
1264
- <td className={`sticky left-0 bg-white px-2 ${currentDensity.cell} border-b ${borderColor} z-10`}>
1445
+ <td
1446
+ className={`sticky left-0 bg-white px-2 ${currentDensity.cell} border-b ${borderColor} z-10`}
1447
+ >
1265
1448
  <div className="h-8 w-8 bg-paper-200 rounded"></div>
1266
1449
  </td>
1267
1450
  )}
@@ -1271,7 +1454,7 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
1271
1454
  return (
1272
1455
  <td
1273
1456
  key={`loading-${i}-${colIndex}`}
1274
- className={`${currentDensity.cell} whitespace-nowrap table-row-stable ${bordered ? `border ${borderColor}` : ''}`}
1457
+ className={`${currentDensity.cell} whitespace-nowrap table-row-stable ${bordered ? `border ${borderColor}` : ""}`}
1275
1458
  style={getColumnStyle(column, dynamicWidth)}
1276
1459
  >
1277
1460
  <div className="h-4 bg-paper-200 rounded mb-1"></div>
@@ -1289,7 +1472,14 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
1289
1472
  if (customRenderEmptyState) {
1290
1473
  return (
1291
1474
  <tr>
1292
- <td colSpan={visibleColumns.length + (allActions.length > 0 ? 1 : 0) + (selectable ? 1 : 0) + (expandable ? 1 : 0)}>
1475
+ <td
1476
+ colSpan={
1477
+ visibleColumns.length +
1478
+ (allActions.length > 0 ? 1 : 0) +
1479
+ (selectable ? 1 : 0) +
1480
+ (expandable ? 1 : 0)
1481
+ }
1482
+ >
1293
1483
  {customRenderEmptyState()}
1294
1484
  </td>
1295
1485
  </tr>
@@ -1297,7 +1487,15 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
1297
1487
  }
1298
1488
  return (
1299
1489
  <tr>
1300
- <td colSpan={visibleColumns.length + (allActions.length > 0 ? 1 : 0) + (selectable ? 1 : 0) + (expandable ? 1 : 0)} className={`${currentDensity.cell} py-8 text-center text-ink-500`}>
1490
+ <td
1491
+ colSpan={
1492
+ visibleColumns.length +
1493
+ (allActions.length > 0 ? 1 : 0) +
1494
+ (selectable ? 1 : 0) +
1495
+ (expandable ? 1 : 0)
1496
+ }
1497
+ className={`${currentDensity.cell} py-8 text-center text-ink-500`}
1498
+ >
1301
1499
  {error || emptyMessage}
1302
1500
  </td>
1303
1501
  </tr>
@@ -1307,12 +1505,15 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
1307
1505
  // Virtual scrolling calculations
1308
1506
  const getVisibleRange = () => {
1309
1507
  if (!virtualized) return { start: 0, end: data.length };
1310
-
1508
+
1311
1509
  const overscan = 5;
1312
- const start = Math.max(0, Math.floor(scrollTop / virtualRowHeight) - overscan);
1510
+ const start = Math.max(
1511
+ 0,
1512
+ Math.floor(scrollTop / virtualRowHeight) - overscan,
1513
+ );
1313
1514
  const visibleCount = Math.ceil(parseInt(virtualHeight) / virtualRowHeight);
1314
1515
  const end = Math.min(data.length, start + visibleCount + overscan * 2);
1315
-
1516
+
1316
1517
  return { start, end };
1317
1518
  };
1318
1519
 
@@ -1327,7 +1528,7 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
1327
1528
 
1328
1529
  // Render data rows
1329
1530
  const renderDataRows = () => {
1330
- const rowsToRender = virtualized
1531
+ const rowsToRender = virtualized
1331
1532
  ? data.slice(visibleStart, visibleEnd)
1332
1533
  : data;
1333
1534
 
@@ -1337,327 +1538,443 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
1337
1538
  const isSelected = selectedRowsSet.has(rowKey);
1338
1539
  const isExpanded = expandedRowsSet.has(rowKey);
1339
1540
  const rowBgClass = getRowBackgroundClass(item, index);
1340
- const borderClass = bordered ? `border-b ${borderColor}` : (!visibleColumns.some(col => !!col.renderSecondary) ? `border-b ${borderColor}` : '');
1341
- const hasSecondaryRow = visibleColumns.some(col => !!col.renderSecondary);
1541
+ const borderClass = bordered
1542
+ ? `border-b ${borderColor}`
1543
+ : !visibleColumns.some((col) => !!col.renderSecondary)
1544
+ ? `border-b ${borderColor}`
1545
+ : "";
1546
+ const hasSecondaryRow = visibleColumns.some(
1547
+ (col) => !!col.renderSecondary,
1548
+ );
1342
1549
 
1343
1550
  // Hover state for row pair (primary + secondary)
1344
1551
  const isHovered = hoveredRowKey === rowKey;
1345
- const hoverClass = disableHover ? '' : (isHovered ? 'bg-paper-100' : '');
1552
+ const hoverClass = disableHover ? "" : isHovered ? "bg-paper-100" : "";
1346
1553
 
1347
1554
  // Check if this row is keyboard-focused
1348
1555
  const isKeyboardFocused = focusedCell?.row === index;
1349
1556
 
1350
1557
  return (
1351
- <React.Fragment key={rowKey}>
1352
- <tr
1353
- data-row-index={index}
1354
- className={`table-row-stable ${onRowDoubleClick || onRowClick || onEdit || expandedRowConfig?.edit || expandedRowConfig?.details || expandedRowConfig?.addRelated?.length || expandedRowConfig?.manageRelated?.length ? 'cursor-pointer' : ''} ${isSelected ? 'bg-accent-50 border-l-2 border-accent-500' : hoverClass || rowBgClass} ${borderClass} ${isKeyboardFocused ? 'ring-2 ring-inset ring-accent-400' : ''}`}
1355
- onMouseEnter={() => !disableHover && setHoveredRowKey(rowKey)}
1356
- onMouseLeave={() => !disableHover && setHoveredRowKey(null)}
1357
- onClick={() => onRowClick?.(item)}
1358
- onContextMenu={(e) => {
1359
- if (enableContextMenu && allActions.length > 0) {
1360
- e.preventDefault();
1361
- e.stopPropagation();
1362
-
1363
- const x = e.clientX;
1364
- const y = e.clientY;
1365
-
1366
- setContextMenuState({
1367
- isOpen: true,
1368
- position: { x, y },
1369
- item,
1370
- });
1371
- }
1372
- }}
1373
- onDoubleClick={() => {
1374
- // Priority 1: If there's an onEdit handler (legacy), trigger it
1375
- if (onEdit) {
1376
- onEdit(item);
1377
- }
1378
- // Priority 2: If there's an expandable edit mode, trigger it
1379
- else if (expandedRowConfig?.edit) {
1380
- handleExpansionWithMode(rowKey, 'edit');
1381
- }
1382
- // Priority 3: If there's an expandable details mode, trigger it
1383
- else if (expandedRowConfig?.details) {
1384
- handleExpansionWithMode(rowKey, 'details');
1385
- }
1386
- // Priority 4: If there's any addRelated mode, trigger the first one
1387
- else if (expandedRowConfig?.addRelated && expandedRowConfig.addRelated.length > 0) {
1388
- handleExpansionWithMode(rowKey, `addRelated-${expandedRowConfig.addRelated[0].key}`);
1389
- }
1390
- // Priority 5: If there's any manageRelated mode, trigger the first one
1391
- else if (expandedRowConfig?.manageRelated && expandedRowConfig.manageRelated.length > 0) {
1392
- handleExpansionWithMode(rowKey, `manageRelated-${expandedRowConfig.manageRelated[0].key}`);
1393
- }
1394
- // Priority 6: Legacy onRowDoubleClick handler
1395
- else {
1396
- onRowDoubleClick?.(item);
1397
- }
1398
- }}
1399
- title={
1400
- onEdit ? 'Double-click to edit' :
1401
- expandedRowConfig?.edit ? 'Double-click to edit inline' :
1402
- expandedRowConfig?.details ? 'Double-click to view details' :
1403
- expandedRowConfig?.addRelated && expandedRowConfig.addRelated.length > 0 ? `Double-click to ${expandedRowConfig.addRelated[0].label}` :
1404
- expandedRowConfig?.manageRelated && expandedRowConfig.manageRelated.length > 0 ? `Double-click to ${expandedRowConfig.manageRelated[0].label}` :
1405
- onRowDoubleClick ? 'Double-click for details' :
1406
- onRowClick ? 'Click to select' :
1407
- undefined
1408
- }
1409
- >
1410
- {selectable && (
1411
- <td
1412
- className={`sticky left-0 z-10 ${bordered ? `border ${borderColor}` : ''}`}
1413
- style={{
1414
- backgroundColor: 'inherit',
1415
- verticalAlign: 'middle',
1416
- padding: '0.375rem 0.75rem',
1417
- textAlign: 'center'
1558
+ <React.Fragment key={rowKey}>
1559
+ <tr
1560
+ data-row-index={index}
1561
+ className={`table-row-stable ${onRowDoubleClick || onRowClick || onEdit || expandedRowConfig?.edit || expandedRowConfig?.details || expandedRowConfig?.addRelated?.length || expandedRowConfig?.manageRelated?.length ? "cursor-pointer" : ""} ${isSelected ? "bg-accent-50 border-l-2 border-accent-500" : hoverClass || rowBgClass} ${borderClass} ${isKeyboardFocused ? "ring-2 ring-inset ring-accent-400" : ""}`}
1562
+ onMouseEnter={() => !disableHover && setHoveredRowKey(rowKey)}
1563
+ onMouseLeave={() => !disableHover && setHoveredRowKey(null)}
1564
+ onClick={() => onRowClick?.(item)}
1565
+ onContextMenu={(e) => {
1566
+ if (enableContextMenu && allActions.length > 0) {
1567
+ e.preventDefault();
1568
+ e.stopPropagation();
1569
+
1570
+ const x = e.clientX;
1571
+ const y = e.clientY;
1572
+
1573
+ setContextMenuState({
1574
+ isOpen: true,
1575
+ position: { x, y },
1576
+ item,
1577
+ });
1578
+ }
1418
1579
  }}
1419
- rowSpan={hasSecondaryRow ? 2 : 1}
1580
+ onDoubleClick={() => {
1581
+ // Priority 1: If there's an onEdit handler (legacy), trigger it
1582
+ if (onEdit) {
1583
+ onEdit(item);
1584
+ }
1585
+ // Priority 2: If there's an expandable edit mode, trigger it
1586
+ else if (expandedRowConfig?.edit) {
1587
+ handleExpansionWithMode(rowKey, "edit");
1588
+ }
1589
+ // Priority 3: If there's an expandable details mode, trigger it
1590
+ else if (expandedRowConfig?.details) {
1591
+ handleExpansionWithMode(rowKey, "details");
1592
+ }
1593
+ // Priority 4: If there's any addRelated mode, trigger the first one
1594
+ else if (
1595
+ expandedRowConfig?.addRelated &&
1596
+ expandedRowConfig.addRelated.length > 0
1597
+ ) {
1598
+ handleExpansionWithMode(
1599
+ rowKey,
1600
+ `addRelated-${expandedRowConfig.addRelated[0].key}`,
1601
+ );
1602
+ }
1603
+ // Priority 5: If there's any manageRelated mode, trigger the first one
1604
+ else if (
1605
+ expandedRowConfig?.manageRelated &&
1606
+ expandedRowConfig.manageRelated.length > 0
1607
+ ) {
1608
+ handleExpansionWithMode(
1609
+ rowKey,
1610
+ `manageRelated-${expandedRowConfig.manageRelated[0].key}`,
1611
+ );
1612
+ }
1613
+ // Priority 6: Legacy onRowDoubleClick handler
1614
+ else {
1615
+ onRowDoubleClick?.(item);
1616
+ }
1617
+ }}
1618
+ title={
1619
+ onEdit
1620
+ ? "Double-click to edit"
1621
+ : expandedRowConfig?.edit
1622
+ ? "Double-click to edit inline"
1623
+ : expandedRowConfig?.details
1624
+ ? "Double-click to view details"
1625
+ : expandedRowConfig?.addRelated &&
1626
+ expandedRowConfig.addRelated.length > 0
1627
+ ? `Double-click to ${expandedRowConfig.addRelated[0].label}`
1628
+ : expandedRowConfig?.manageRelated &&
1629
+ expandedRowConfig.manageRelated.length > 0
1630
+ ? `Double-click to ${expandedRowConfig.manageRelated[0].label}`
1631
+ : onRowDoubleClick
1632
+ ? "Double-click for details"
1633
+ : onRowClick
1634
+ ? "Click to select"
1635
+ : undefined
1636
+ }
1420
1637
  >
1421
- <input
1422
- type="checkbox"
1423
- checked={isSelected}
1424
- onChange={() => handleRowSelect(rowKey)}
1425
- className="w-4 h-4 text-accent-600 border-paper-300 rounded focus:ring-accent-400"
1426
- aria-label={`Select row ${rowKey}`}
1427
- />
1428
- </td>
1429
- )}
1430
- {((expandable || expandedRowConfig) && showExpandChevron) && (
1431
- <td
1432
- className={`sticky left-0 px-2 ${currentDensity.cell} z-10 ${bordered ? `border ${borderColor}` : ''}`}
1433
- style={{ backgroundColor: 'inherit', verticalAlign: 'middle' }}
1434
- rowSpan={hasSecondaryRow ? 2 : 1}
1435
- >
1436
- <button
1437
- onClick={() => {
1438
- // NEW: Enhanced logic for expandedRowConfig
1439
- if (expandedRowConfig?.details && expandedRowConfig.details.triggerOnExpand !== false) {
1440
- // Trigger details mode if configured
1441
- handleExpansionWithMode(rowKey, 'details');
1442
- } else if (expandedRowConfig?.edit && expandedRowConfig.edit.triggerOnDoubleClick !== false) {
1443
- // Fallback to edit mode if no details but edit is available
1444
- handleExpansionWithMode(rowKey, 'edit');
1445
- } else {
1446
- // Legacy: use handleRowExpand
1447
- handleRowExpand(rowKey);
1638
+ {selectable && (
1639
+ <td
1640
+ className={`sticky left-0 z-10 ${bordered ? `border ${borderColor}` : ""}`}
1641
+ style={{
1642
+ backgroundColor: "inherit",
1643
+ verticalAlign: "middle",
1644
+ padding: "0.375rem 0.75rem",
1645
+ textAlign: "center",
1646
+ }}
1647
+ rowSpan={hasSecondaryRow ? 2 : 1}
1648
+ >
1649
+ <input
1650
+ type="checkbox"
1651
+ checked={isSelected}
1652
+ onChange={() => handleRowSelect(rowKey)}
1653
+ className="w-4 h-4 text-accent-600 border-paper-300 rounded focus:ring-accent-400"
1654
+ aria-label={`Select row ${rowKey}`}
1655
+ />
1656
+ </td>
1657
+ )}
1658
+ {(expandable || expandedRowConfig) && showExpandChevron && (
1659
+ <td
1660
+ className={`sticky left-0 px-2 ${currentDensity.cell} z-10 ${bordered ? `border ${borderColor}` : ""}`}
1661
+ style={{ backgroundColor: "inherit", verticalAlign: "middle" }}
1662
+ rowSpan={hasSecondaryRow ? 2 : 1}
1663
+ >
1664
+ <button
1665
+ onClick={() => {
1666
+ // NEW: Enhanced logic for expandedRowConfig
1667
+ if (
1668
+ expandedRowConfig?.details &&
1669
+ expandedRowConfig.details.triggerOnExpand !== false
1670
+ ) {
1671
+ // Trigger details mode if configured
1672
+ handleExpansionWithMode(rowKey, "details");
1673
+ } else if (
1674
+ expandedRowConfig?.edit &&
1675
+ expandedRowConfig.edit.triggerOnDoubleClick !== false
1676
+ ) {
1677
+ // Fallback to edit mode if no details but edit is available
1678
+ handleExpansionWithMode(rowKey, "edit");
1679
+ } else {
1680
+ // Legacy: use handleRowExpand
1681
+ handleRowExpand(rowKey);
1682
+ }
1683
+ }}
1684
+ className="text-ink-500 hover:text-ink-900 transition-colors"
1685
+ aria-label={
1686
+ isExpanded || expansionState?.rowKey === rowKey
1687
+ ? "Collapse row"
1688
+ : "Expand row"
1448
1689
  }
1690
+ >
1691
+ {isExpanded || expansionState?.rowKey === rowKey ? (
1692
+ <ChevronDown className="h-4 w-4" />
1693
+ ) : (
1694
+ <ChevronRight className="h-4 w-4" />
1695
+ )}
1696
+ </button>
1697
+ </td>
1698
+ )}
1699
+ {allActions.length > 0 && (
1700
+ <td
1701
+ className="sticky left-0 whitespace-nowrap shadow-[4px_0_6px_-2px_rgba(0,0,0,0.1)] z-10"
1702
+ style={{
1703
+ width: "28px",
1704
+ padding: "0",
1705
+ backgroundColor: "inherit",
1706
+ verticalAlign: "middle",
1449
1707
  }}
1450
- className="text-ink-500 hover:text-ink-900 transition-colors"
1451
- aria-label={isExpanded || (expansionState?.rowKey === rowKey) ? 'Collapse row' : 'Expand row'}
1708
+ onClick={(e) => e.stopPropagation()}
1709
+ rowSpan={hasSecondaryRow ? 2 : 1}
1452
1710
  >
1453
- {isExpanded || (expansionState?.rowKey === rowKey) ? (
1454
- <ChevronDown className="h-4 w-4" />
1455
- ) : (
1456
- <ChevronRight className="h-4 w-4" />
1457
- )}
1458
- </button>
1459
- </td>
1460
- )}
1461
- {allActions.length > 0 && (
1462
- <td
1463
- className="sticky left-0 whitespace-nowrap shadow-[4px_0_6px_-2px_rgba(0,0,0,0.1)] z-10"
1464
- style={{
1465
- width: '28px',
1466
- padding: '0',
1467
- backgroundColor: 'inherit',
1468
- verticalAlign: 'middle'
1469
- }}
1470
- onClick={(e) => e.stopPropagation()}
1471
- rowSpan={hasSecondaryRow ? 2 : 1}
1472
- >
1473
- <div style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: '28px' }}>
1474
- <ActionMenu actions={allActions} item={item} />
1475
- </div>
1476
- </td>
1477
- )}
1478
- {visibleColumns.map((column, colIdx) => {
1479
- const columnKey = String(column.key);
1480
- const dynamicWidth = columnWidths[columnKey];
1481
- const value = typeof column.key === 'string'
1482
- ? item[column.key as keyof T]
1483
- : item[column.key];
1484
-
1485
- const primaryContent = column.render ? column.render(item, value) : String(value || '');
1711
+ <div
1712
+ style={{
1713
+ display: "inline-flex",
1714
+ alignItems: "center",
1715
+ justifyContent: "center",
1716
+ width: "28px",
1717
+ }}
1718
+ >
1719
+ <ActionMenu actions={allActions} item={item} />
1720
+ </div>
1721
+ </td>
1722
+ )}
1723
+ {visibleColumns.map((column, colIdx) => {
1724
+ const columnKey = String(column.key);
1725
+ const dynamicWidth = columnWidths[columnKey];
1726
+ const value =
1727
+ typeof column.key === "string"
1728
+ ? item[column.key as keyof T]
1729
+ : item[column.key];
1730
+
1731
+ const primaryContent = column.render
1732
+ ? column.render(item, value)
1733
+ : String(value || "");
1734
+
1735
+ // Tooltip: caller-provided > raw value stringified. Empty
1736
+ // strings get dropped so we don't render an empty title
1737
+ // attribute (which the browser would still show as a
1738
+ // 0-width tooltip box on hover).
1739
+ const primaryTooltipText = column.tooltip
1740
+ ? column.tooltip(item, value)
1741
+ : value !== null && value !== undefined && value !== ""
1742
+ ? String(value)
1743
+ : undefined;
1744
+
1745
+ // Reduce left padding on first column when there are action buttons
1746
+ const isFirstColumn = colIdx === 0;
1747
+ const paddingClass =
1748
+ isFirstColumn && allActions.length > 0 ? "pl-3" : "";
1486
1749
 
1487
- // Reduce left padding on first column when there are action buttons
1488
- const isFirstColumn = colIdx === 0;
1489
- const paddingClass = isFirstColumn && allActions.length > 0 ? 'pl-3' : '';
1750
+ // Check if this cell is keyboard-focused
1751
+ const isCellFocused =
1752
+ focusedCell?.row === index && focusedCell?.col === colIdx;
1490
1753
 
1491
- // Check if this cell is keyboard-focused
1492
- const isCellFocused = focusedCell?.row === index && focusedCell?.col === colIdx;
1754
+ return (
1755
+ <td
1756
+ key={`${item.id}-${columnKey}`}
1757
+ className={`${currentDensity.cell} ${paddingClass} ${column.className || ""} ${bordered ? `border ${borderColor}` : ""} ${isCellFocused ? "outline outline-2 outline-accent-500 outline-offset-[-2px]" : ""}`}
1758
+ style={getColumnStyle(column, dynamicWidth)}
1759
+ tabIndex={isCellFocused ? 0 : -1}
1760
+ role="gridcell"
1761
+ aria-colindex={colIdx + 1}
1762
+ >
1763
+ <div
1764
+ className={`${currentDensity.text} leading-tight`}
1765
+ title={primaryTooltipText}
1766
+ >
1767
+ {primaryContent}
1768
+ </div>
1769
+ </td>
1770
+ );
1771
+ })}
1772
+ </tr>
1493
1773
 
1494
- return (
1495
- <td
1496
- key={`${item.id}-${columnKey}`}
1497
- className={`${currentDensity.cell} ${paddingClass} ${column.className || ''} ${bordered ? `border ${borderColor}` : ''} ${isCellFocused ? 'outline outline-2 outline-accent-500 outline-offset-[-2px]' : ''}`}
1498
- style={getColumnStyle(column, dynamicWidth)}
1499
- tabIndex={isCellFocused ? 0 : -1}
1500
- role="gridcell"
1501
- aria-colindex={colIdx + 1}
1774
+ {/* Secondary row - only render if any column has renderSecondary */}
1775
+ {hasSecondaryRow && (
1776
+ <tr
1777
+ className={`secondary-row ${isSelected ? "bg-accent-50 border-l-2 border-accent-500" : hoverClass || rowBgClass} border-b ${borderColor}`}
1778
+ onMouseEnter={() => !disableHover && setHoveredRowKey(rowKey)}
1779
+ onMouseLeave={() => !disableHover && setHoveredRowKey(null)}
1502
1780
  >
1503
- <div className={`${currentDensity.text} leading-tight`}>{primaryContent}</div>
1504
- </td>
1505
- );
1506
- })}
1507
- </tr>
1508
-
1509
- {/* Secondary row - only render if any column has renderSecondary */}
1510
- {hasSecondaryRow && (
1511
- <tr
1512
- className={`secondary-row ${isSelected ? 'bg-accent-50 border-l-2 border-accent-500' : hoverClass || rowBgClass} border-b ${borderColor}`}
1513
- onMouseEnter={() => !disableHover && setHoveredRowKey(rowKey)}
1514
- onMouseLeave={() => !disableHover && setHoveredRowKey(null)}
1515
- >
1516
- {/* Selectable checkbox uses rowspan from primary row, no cell needed here */}
1517
- {/* Expand chevron uses rowspan from primary row, no cell needed here */}
1518
- {/* Actions column uses rowspan from primary row, no cell needed here */}
1519
- {visibleColumns.map((column, colIdx) => {
1520
- const columnKey = String(column.key);
1521
- const dynamicWidth = columnWidths[columnKey];
1522
- const value = typeof column.key === 'string'
1781
+ {/* Selectable checkbox uses rowspan from primary row, no cell needed here */}
1782
+ {/* Expand chevron uses rowspan from primary row, no cell needed here */}
1783
+ {/* Actions column uses rowspan from primary row, no cell needed here */}
1784
+ {visibleColumns.map((column, colIdx) => {
1785
+ const columnKey = String(column.key);
1786
+ const dynamicWidth = columnWidths[columnKey];
1787
+ const value =
1788
+ typeof column.key === "string"
1523
1789
  ? item[column.key as keyof T]
1524
1790
  : item[column.key];
1525
- const secondaryContent = column.renderSecondary ? column.renderSecondary(item, value) : null;
1526
-
1527
- // Reduce left padding on first column when there are action buttons
1528
- const isFirstColumn = colIdx === 0;
1529
- const paddingClass = isFirstColumn && allActions.length > 0 ? 'pl-3' : '';
1791
+ const secondaryContent = column.renderSecondary
1792
+ ? column.renderSecondary(item, value)
1793
+ : null;
1794
+
1795
+ // Tooltip on the secondary row prefixes the field label when
1796
+ // available so the otherwise-unlabeled second row stays
1797
+ // self-describing on hover. Caller can override entirely
1798
+ // via `secondaryTooltip`.
1799
+ const hasSecondaryValue =
1800
+ value !== null && value !== undefined && value !== "";
1801
+ let secondaryTooltipText: string | undefined;
1802
+ if (column.secondaryTooltip) {
1803
+ secondaryTooltipText = column.secondaryTooltip(item, value);
1804
+ } else if (hasSecondaryValue) {
1805
+ secondaryTooltipText = column.secondaryHeader
1806
+ ? `${column.secondaryHeader}: ${value}`
1807
+ : String(value);
1808
+ } else if (column.secondaryHeader) {
1809
+ secondaryTooltipText = column.secondaryHeader;
1810
+ }
1530
1811
 
1531
- return (
1532
- <td
1533
- key={`${item.id}-${columnKey}-secondary`}
1534
- className={`${currentDensity.cell} py-0.5 ${paddingClass} ${column.className || ''} ${bordered ? `border ${borderColor}` : ''}`}
1535
- style={getColumnStyle(column, dynamicWidth)}
1812
+ // Reduce left padding on first column when there are action buttons
1813
+ const isFirstColumn = colIdx === 0;
1814
+ const paddingClass =
1815
+ isFirstColumn && allActions.length > 0 ? "pl-3" : "";
1816
+
1817
+ return (
1818
+ <td
1819
+ key={`${item.id}-${columnKey}-secondary`}
1820
+ className={`${currentDensity.cell} py-0.5 ${paddingClass} ${column.className || ""} ${bordered ? `border ${borderColor}` : ""}`}
1821
+ style={getColumnStyle(column, dynamicWidth)}
1822
+ >
1823
+ <div
1824
+ className="text-xs text-ink-500 leading-tight"
1825
+ title={secondaryTooltipText}
1536
1826
  >
1537
- <div className="text-xs text-ink-500 leading-tight">
1538
- {secondaryContent || <span className="invisible">—</span>}
1539
- </div>
1540
- </td>
1541
- ); })}
1542
- </tr>
1543
- )}
1544
-
1545
- {/* Expanded row content - Legacy mode */}
1546
- {expandable && isExpanded && renderExpandedRow && (
1547
- <tr>
1548
- <td
1549
- colSpan={
1550
- visibleColumns.length +
1551
- (selectable ? 1 : 0) +
1552
- (((expandable || expandedRowConfig) && showExpandChevron) ? 1 : 0) +
1553
- (allActions.length > 0 ? 1 : 0)
1554
- }
1555
- className={`${currentDensity.cell} py-4 bg-paper-50`}
1556
- >
1557
- {renderExpandedRow(item)}
1558
- </td>
1559
- </tr>
1560
- )}
1561
-
1562
- {/* Expanded row content - NEW: Multiple expansion modes */}
1563
- {expansionState && expansionState.rowKey === rowKey && expandedRowConfig && (() => {
1564
- const mode = expansionState.mode;
1565
- let content: React.ReactNode = null;
1566
- let bgColorClass = 'bg-paper-50'; // Default
1567
-
1568
- // Edit mode
1569
- if (mode === 'edit' && expandedRowConfig.edit) {
1570
- bgColorClass = 'bg-paper-100/80 border-t border-b border-paper-300/80';
1571
- content = expandedRowConfig.edit.render(
1572
- item,
1573
- async (_updated: T) => {
1574
- // Handle save
1575
- handleCollapseExpansion();
1576
- },
1577
- () => {
1578
- // Handle cancel
1579
- handleCollapseExpansion();
1580
- }
1581
- );
1582
- }
1583
-
1584
- // Details mode
1585
- else if (mode === 'details' && expandedRowConfig.details) {
1586
- bgColorClass = 'bg-primary-50/80 border-t border-b border-primary-200/80';
1587
- content = expandedRowConfig.details.render(item);
1588
- }
1589
-
1590
- // Add related modes
1591
- else if (mode.startsWith('addRelated-') && expandedRowConfig.addRelated) {
1592
- const key = mode.replace('addRelated-', '');
1593
- const config = expandedRowConfig.addRelated.find(c => c.key === key);
1594
- if (config) {
1595
- bgColorClass = 'bg-success-50/80 border-t border-b border-success-200/80';
1596
- content = config.render(
1597
- item,
1598
- async (_newItem: any) => {
1599
- // Handle save
1600
- handleCollapseExpansion();
1601
- },
1602
- () => {
1603
- // Handle cancel
1604
- handleCollapseExpansion();
1605
- }
1606
- );
1607
- }
1608
- }
1609
-
1610
- // Manage related modes
1611
- else if (mode.startsWith('manageRelated-') && expandedRowConfig.manageRelated) {
1612
- const key = mode.replace('manageRelated-', '');
1613
- const config = expandedRowConfig.manageRelated.find(c => c.key === key);
1614
- if (config) {
1615
- bgColorClass = 'bg-slate-50/80 border-t border-b border-slate-200/80';
1616
- const handleClose = () => setExpansionState(null);
1617
- content = config.render(item, handleClose);
1618
- }
1619
- }
1620
-
1621
- if (!content) return null;
1622
-
1623
- return (
1624
- <tr key={`expanded-${rowKey}`}>
1827
+ {secondaryContent || <span className="invisible">—</span>}
1828
+ </div>
1829
+ </td>
1830
+ );
1831
+ })}
1832
+ </tr>
1833
+ )}
1834
+
1835
+ {/* Expanded row content - Legacy mode */}
1836
+ {expandable && isExpanded && renderExpandedRow && (
1837
+ <tr>
1625
1838
  <td
1626
1839
  colSpan={
1627
1840
  visibleColumns.length +
1628
1841
  (selectable ? 1 : 0) +
1629
- (((expandable || expandedRowConfig) && showExpandChevron) ? 1 : 0) +
1842
+ ((expandable || expandedRowConfig) && showExpandChevron
1843
+ ? 1
1844
+ : 0) +
1630
1845
  (allActions.length > 0 ? 1 : 0)
1631
1846
  }
1632
- className={`${currentDensity.cell} py-4 ${bgColorClass} animate-expand`}
1847
+ className={`${currentDensity.cell} py-4 bg-paper-50`}
1633
1848
  >
1634
- {content}
1849
+ {renderExpandedRow(item)}
1635
1850
  </td>
1636
1851
  </tr>
1637
- );
1638
- })()}
1639
- </React.Fragment>
1852
+ )}
1853
+
1854
+ {/* Expanded row content - NEW: Multiple expansion modes */}
1855
+ {expansionState &&
1856
+ expansionState.rowKey === rowKey &&
1857
+ expandedRowConfig &&
1858
+ (() => {
1859
+ const mode = expansionState.mode;
1860
+ let content: React.ReactNode = null;
1861
+ let bgColorClass = "bg-paper-50"; // Default
1862
+
1863
+ // Edit mode
1864
+ if (mode === "edit" && expandedRowConfig.edit) {
1865
+ bgColorClass =
1866
+ "bg-paper-100/80 border-t border-b border-paper-300/80";
1867
+ content = expandedRowConfig.edit.render(
1868
+ item,
1869
+ async (_updated: T) => {
1870
+ // Handle save
1871
+ handleCollapseExpansion();
1872
+ },
1873
+ () => {
1874
+ // Handle cancel
1875
+ handleCollapseExpansion();
1876
+ },
1877
+ );
1878
+ }
1879
+
1880
+ // Details mode
1881
+ else if (mode === "details" && expandedRowConfig.details) {
1882
+ bgColorClass =
1883
+ "bg-primary-50/80 border-t border-b border-primary-200/80";
1884
+ content = expandedRowConfig.details.render(item);
1885
+ }
1886
+
1887
+ // Add related modes
1888
+ else if (
1889
+ mode.startsWith("addRelated-") &&
1890
+ expandedRowConfig.addRelated
1891
+ ) {
1892
+ const key = mode.replace("addRelated-", "");
1893
+ const config = expandedRowConfig.addRelated.find(
1894
+ (c) => c.key === key,
1895
+ );
1896
+ if (config) {
1897
+ bgColorClass =
1898
+ "bg-success-50/80 border-t border-b border-success-200/80";
1899
+ content = config.render(
1900
+ item,
1901
+ async (_newItem: any) => {
1902
+ // Handle save
1903
+ handleCollapseExpansion();
1904
+ },
1905
+ () => {
1906
+ // Handle cancel
1907
+ handleCollapseExpansion();
1908
+ },
1909
+ );
1910
+ }
1911
+ }
1912
+
1913
+ // Manage related modes
1914
+ else if (
1915
+ mode.startsWith("manageRelated-") &&
1916
+ expandedRowConfig.manageRelated
1917
+ ) {
1918
+ const key = mode.replace("manageRelated-", "");
1919
+ const config = expandedRowConfig.manageRelated.find(
1920
+ (c) => c.key === key,
1921
+ );
1922
+ if (config) {
1923
+ bgColorClass =
1924
+ "bg-slate-50/80 border-t border-b border-slate-200/80";
1925
+ const handleClose = () => setExpansionState(null);
1926
+ content = config.render(item, handleClose);
1927
+ }
1928
+ }
1929
+
1930
+ if (!content) return null;
1931
+
1932
+ return (
1933
+ <tr key={`expanded-${rowKey}`}>
1934
+ <td
1935
+ colSpan={
1936
+ visibleColumns.length +
1937
+ (selectable ? 1 : 0) +
1938
+ ((expandable || expandedRowConfig) && showExpandChevron
1939
+ ? 1
1940
+ : 0) +
1941
+ (allActions.length > 0 ? 1 : 0)
1942
+ }
1943
+ className={`${currentDensity.cell} py-4 ${bgColorClass} animate-expand`}
1944
+ >
1945
+ {content}
1946
+ </td>
1947
+ </tr>
1948
+ );
1949
+ })()}
1950
+ </React.Fragment>
1640
1951
  );
1641
1952
  });
1642
1953
  };
1643
1954
 
1644
1955
  const tableContent = (
1645
- <div className={`bg-white rounded-lg shadow border-2 ${borderColor} ${virtualized ? 'overflow-hidden' : 'overflow-x-auto overflow-y-visible'} ${className}`} style={{ position: 'relative' }}>
1956
+ <div
1957
+ className={`bg-white rounded-lg shadow border-2 ${borderColor} ${virtualized ? "overflow-hidden" : "overflow-x-auto overflow-y-visible"} ${className}`}
1958
+ style={{ position: "relative" }}
1959
+ >
1646
1960
  {/* Loading overlay for when data is being refreshed */}
1647
1961
  {loading && data.length > 0 && (
1648
1962
  <div
1649
1963
  className="absolute inset-0 bg-white/75 flex items-center justify-center z-20"
1650
- style={{ backdropFilter: 'blur(2px)' }}
1964
+ style={{ backdropFilter: "blur(2px)" }}
1651
1965
  >
1652
1966
  <div className="flex flex-col items-center gap-3">
1653
- <div className="loading-spinner" style={{ width: '32px', height: '32px', borderWidth: '3px' }}></div>
1967
+ <div
1968
+ className="loading-spinner"
1969
+ style={{ width: "32px", height: "32px", borderWidth: "3px" }}
1970
+ ></div>
1654
1971
  <span className="text-sm font-medium text-ink-600">Loading...</span>
1655
1972
  </div>
1656
1973
  </div>
1657
1974
  )}
1658
1975
 
1659
1976
  <table
1660
- className={`table-stable w-full ${bordered ? 'border-collapse' : ''}`}
1977
+ className={`table-stable w-full ${bordered ? "border-collapse" : ""}`}
1661
1978
  role="grid"
1662
1979
  aria-label="Data table"
1663
1980
  aria-rowcount={data.length}
@@ -1665,8 +1982,10 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
1665
1982
  >
1666
1983
  <colgroup>
1667
1984
  {selectable && <col className="w-12" />}
1668
- {((expandable || expandedRowConfig) && showExpandChevron) && <col className="w-10" />}
1669
- {allActions.length > 0 && <col style={{ width: '28px' }} />}
1985
+ {(expandable || expandedRowConfig) && showExpandChevron && (
1986
+ <col className="w-10" />
1987
+ )}
1988
+ {allActions.length > 0 && <col style={{ width: "28px" }} />}
1670
1989
  {visibleColumns.map((column, index) => {
1671
1990
  const columnKey = String(column.key);
1672
1991
  const dynamicWidth = columnWidths[columnKey];
@@ -1678,23 +1997,32 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
1678
1997
  <thead className={`bg-paper-100 sticky top-0 z-10 ${headerClassName}`}>
1679
1998
  <tr className="table-header-row">
1680
1999
  {selectable && (
1681
- <th className={`sticky left-0 bg-paper-100 ${currentDensity.header} border-b ${borderColor} z-20 w-12 ${bordered ? `border ${borderColor}` : ''}`}>
2000
+ <th
2001
+ className={`sticky left-0 bg-paper-100 ${currentDensity.header} border-b ${borderColor} z-20 w-12 ${bordered ? `border ${borderColor}` : ""}`}
2002
+ >
1682
2003
  <input
1683
2004
  type="checkbox"
1684
- checked={selectedRowsSet.size === data.length && data.length > 0}
2005
+ checked={
2006
+ selectedRowsSet.size === data.length && data.length > 0
2007
+ }
1685
2008
  onChange={handleSelectAll}
1686
2009
  className="w-4 h-4 text-accent-600 border-paper-300 rounded focus:ring-accent-400"
1687
2010
  aria-label="Select all rows"
1688
2011
  />
1689
2012
  </th>
1690
2013
  )}
1691
- {((expandable || expandedRowConfig) && showExpandChevron) && (
1692
- <th className={`sticky left-0 bg-paper-100 px-2 ${currentDensity.header} border-b ${borderColor} z-19 w-10 ${bordered ? `border ${borderColor}` : ''}`}>
2014
+ {(expandable || expandedRowConfig) && showExpandChevron && (
2015
+ <th
2016
+ className={`sticky left-0 bg-paper-100 px-2 ${currentDensity.header} border-b ${borderColor} z-19 w-10 ${bordered ? `border ${borderColor}` : ""}`}
2017
+ >
1693
2018
  {/* Empty header for expand column */}
1694
2019
  </th>
1695
2020
  )}
1696
2021
  {allActions.length > 0 && (
1697
- <th className={`sticky left-0 bg-paper-100 text-center text-xs font-medium text-ink-700 uppercase tracking-wider border-b ${borderColor} z-20 ${bordered ? `border ${borderColor}` : ''}`} style={{ width: '28px', padding: '0' }}>
2022
+ <th
2023
+ className={`sticky left-0 bg-paper-100 text-center text-xs font-medium text-ink-700 uppercase tracking-wider border-b ${borderColor} z-20 ${bordered ? `border ${borderColor}` : ""}`}
2024
+ style={{ width: "28px", padding: "0" }}
2025
+ >
1698
2026
  {/* Actions column header */}
1699
2027
  </th>
1700
2028
  )}
@@ -1707,22 +2035,27 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
1707
2035
 
1708
2036
  // Reduce left padding on first column when there are action buttons (match body cells)
1709
2037
  const isFirstColumn = colIdx === 0;
1710
- const headerPaddingClass = isFirstColumn && allActions.length > 0 ? 'pl-3' : '';
2038
+ const headerPaddingClass =
2039
+ isFirstColumn && allActions.length > 0 ? "pl-3" : "";
1711
2040
 
1712
2041
  return (
1713
2042
  <th
1714
2043
  key={columnKey}
1715
2044
  ref={thRef}
1716
2045
  draggable={reorderable}
1717
- onDragStart={(e) => reorderable && handleDragStart(e, columnKey)}
1718
- onDragOver={(e) => reorderable && handleDragOver(e, columnKey)}
2046
+ onDragStart={(e) =>
2047
+ reorderable && handleDragStart(e, columnKey)
2048
+ }
2049
+ onDragOver={(e) =>
2050
+ reorderable && handleDragOver(e, columnKey)
2051
+ }
1719
2052
  onDragEnd={handleDragEnd}
1720
2053
  onDrop={(e) => reorderable && handleDrop(e, columnKey)}
1721
2054
  className={`
1722
- ${currentDensity.header} ${headerPaddingClass} text-left border-b ${borderColor} ${bordered ? `border ${borderColor}` : ''} relative
1723
- ${reorderable ? 'cursor-move' : ''}
1724
- ${isDragging ? 'opacity-50' : ''}
1725
- ${isDragOver ? 'bg-accent-100' : ''}
2055
+ ${currentDensity.header} ${headerPaddingClass} text-left border-b ${borderColor} ${bordered ? `border ${borderColor}` : ""} relative
2056
+ ${reorderable ? "cursor-move" : ""}
2057
+ ${isDragging ? "opacity-50" : ""}
2058
+ ${isDragOver ? "bg-accent-100" : ""}
1726
2059
  `}
1727
2060
  style={getColumnStyle(column, dynamicWidth)}
1728
2061
  >
@@ -1763,13 +2096,11 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
1763
2096
  role="rowgroup"
1764
2097
  aria-label="Table data"
1765
2098
  >
1766
- {loading && data.length === 0 ? (
1767
- renderLoadingSkeleton()
1768
- ) : data.length === 0 ? (
1769
- renderEmptyStateContent()
1770
- ) : (
1771
- renderDataRows()
1772
- )}
2099
+ {loading && data.length === 0
2100
+ ? renderLoadingSkeleton()
2101
+ : data.length === 0
2102
+ ? renderEmptyStateContent()
2103
+ : renderDataRows()}
1773
2104
  </tbody>
1774
2105
  </table>
1775
2106
  </div>
@@ -1780,19 +2111,21 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
1780
2111
  <div
1781
2112
  ref={tableContainerRef}
1782
2113
  onScroll={handleScroll}
1783
- style={{ height: virtualHeight, overflow: 'auto' }}
2114
+ style={{ height: virtualHeight, overflow: "auto" }}
1784
2115
  className="rounded-lg"
1785
2116
  >
1786
2117
  {tableContent}
1787
2118
  </div>
1788
- ) : tableContent;
2119
+ ) : (
2120
+ tableContent
2121
+ );
1789
2122
 
1790
2123
  // Calculate pagination values
1791
2124
  const effectiveTotalItems = totalItems ?? data.length;
1792
2125
  const totalPages = Math.ceil(effectiveTotalItems / pageSize);
1793
2126
 
1794
2127
  // Page size selector options
1795
- const pageSizeSelectOptions = pageSizeOptions.map(size => ({
2128
+ const pageSizeSelectOptions = pageSizeOptions.map((size) => ({
1796
2129
  value: String(size),
1797
2130
  label: `${size} per page`,
1798
2131
  }));
@@ -1817,10 +2150,12 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
1817
2150
  <span className="text-sm text-ink-600">
1818
2151
  {effectiveTotalItems > 0 ? (
1819
2152
  <>
1820
- Showing {((currentPage - 1) * pageSize) + 1} - {Math.min(currentPage * pageSize, effectiveTotalItems)} of {effectiveTotalItems}
2153
+ Showing {(currentPage - 1) * pageSize + 1} -{" "}
2154
+ {Math.min(currentPage * pageSize, effectiveTotalItems)} of{" "}
2155
+ {effectiveTotalItems}
1821
2156
  </>
1822
2157
  ) : (
1823
- 'No items'
2158
+ "No items"
1824
2159
  )}
1825
2160
  </span>
1826
2161
  </div>
@@ -1836,9 +2171,8 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
1836
2171
  };
1837
2172
 
1838
2173
  // Determine if we should show card view
1839
- const shouldShowCardView =
1840
- mobileView === 'card' ||
1841
- (mobileView === 'auto' && isMobileViewport);
2174
+ const shouldShowCardView =
2175
+ mobileView === "card" || (mobileView === "auto" && isMobileViewport);
1842
2176
 
1843
2177
  // Card view content
1844
2178
  const cardViewContent = shouldShowCardView ? (
@@ -1853,8 +2187,14 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
1853
2187
  onCardLongPress={(item, event) => {
1854
2188
  if (enableContextMenu && allActions.length > 0) {
1855
2189
  event.preventDefault();
1856
- const clientX = 'touches' in event ? event.touches[0].clientX : (event as React.MouseEvent).clientX;
1857
- const clientY = 'touches' in event ? event.touches[0].clientY : (event as React.MouseEvent).clientY;
2190
+ const clientX =
2191
+ "touches" in event
2192
+ ? event.touches[0].clientX
2193
+ : (event as React.MouseEvent).clientX;
2194
+ const clientY =
2195
+ "touches" in event
2196
+ ? event.touches[0].clientY
2197
+ : (event as React.MouseEvent).clientY;
1858
2198
  setContextMenuState({
1859
2199
  isOpen: true,
1860
2200
  position: { x: clientX, y: clientY },
@@ -1893,7 +2233,13 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
1893
2233
  <Menu
1894
2234
  items={convertActionsToMenuItems(contextMenuState.item)}
1895
2235
  position={contextMenuState.position}
1896
- onClose={() => setContextMenuState({ isOpen: false, position: { x: 0, y: 0 }, item: null })}
2236
+ onClose={() =>
2237
+ setContextMenuState({
2238
+ isOpen: false,
2239
+ position: { x: 0, y: 0 },
2240
+ item: null,
2241
+ })
2242
+ }
1897
2243
  />
1898
2244
  )}
1899
2245
  </>