@pk-design/react-tailwind 1.3.0 → 1.4.1

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.
@@ -224,7 +224,7 @@ const TypeClassMap = {
224
224
  const Alert = (props) => {
225
225
  const { type = 'info', className = '', message, children, removable = false, onRemove = () => { }, ...restProps } = props;
226
226
  const Icon = AlertTypeIconMap[type] || SvgInfo;
227
- return (jsxs("div", { className: buildClassName(className, 'flex items-center justify-between rounded-md p-3 text-sm ring-1 ring-inset gap-2', TypeClassMap[type]), ...restProps, children: [jsxs("span", { className: "inline-flex items-center gap-2", children: [jsx(Icon, { className: "size-5", "data-testid": `alert-${type}-icon` }), jsx("span", { className: "pr-2 inline-flex items-center", children: message || children })] }), removable && (jsx(Button, { iconOnly: true, "aria-label": "Dismiss alert", size: "xs", variant: "plain", theme: "secondary", className: "hover:bg-black/5 text-gray-600 dark:hover:bg-black/5 dark:text-gray-600", onClick: () => onRemove(), "data-testid": "alert-remove-btn", children: jsx(SvgX, { className: "size-4.5", "data-testid": "alert-remove-icon" }) }))] }));
227
+ return (jsxs("div", { className: buildClassName(className, 'flex items-center justify-between rounded-md p-3 text-sm ring-1 ring-inset gap-2', TypeClassMap[type]), ...restProps, children: [jsxs("span", { className: "inline-flex items-center gap-2", children: [jsx(Icon, { className: "size-5", "data-testid": `alert-${type}-icon` }), jsx("span", { className: "pr-2 inline-flex items-center", children: message || children })] }), removable && (jsx(Button, { iconOnly: true, "aria-label": "Dismiss alert", size: "xs", variant: "plain", theme: "secondary", className: "hover:bg-black/10 dark:hover:bg-white/10", onClick: () => onRemove(), "data-testid": "alert-remove-btn", children: jsx(SvgX, { className: "size-4.5", "data-testid": "alert-remove-icon" }) }))] }));
228
228
  };
229
229
  Alert.displayName = 'Alert';
230
230
 
@@ -237,7 +237,7 @@ const ThemeClassMap = {
237
237
  };
238
238
  const Badge = forwardRef((props, ref) => {
239
239
  const { theme = 'info', className = '', children, removable = false, rounded = false, onRemove = () => { }, ...restProps } = props;
240
- return (jsxs(TextContent, { ref: ref, className: buildClassName(className, 'inline-flex items-center pl-3 py-1 text-xs font-medium ring-1 ring-inset', ThemeClassMap[theme], rounded ? 'rounded-full' : 'rounded-md'), "data-testid": `badge-${theme}`, ...restProps, children: [jsx(TextContent, { className: "pr-3 inline-flex items-center", children: children }), removable ? (jsx(SvgX, { className: "size-5 px-0.5 mr-1 hover:bg-slate-200 dark:hover:bg-white/10 rounded-sm cursor-pointer text-gray-600 dark:text-gray-300", role: "button", tabIndex: 0, "aria-label": "Remove", onClick: () => onRemove(), onKeyDown: (e) => {
240
+ return (jsxs(TextContent, { ref: ref, className: buildClassName(className, 'inline-flex items-center pl-3 py-1 text-xs font-medium ring-1 ring-inset', ThemeClassMap[theme], rounded ? 'rounded-full' : 'rounded-md'), "data-testid": `badge-${theme}`, ...restProps, children: [jsx(TextContent, { className: "pr-3 inline-flex items-center", children: children }), removable ? (jsx(SvgX, { className: "size-5 px-0.5 mr-1 hover:bg-black/10 dark:hover:bg-white/10 rounded-sm cursor-pointer", role: "button", tabIndex: 0, "aria-label": "Remove", onClick: () => onRemove(), onKeyDown: (e) => {
241
241
  if (e.key === 'Enter' || e.key === ' ') {
242
242
  e.preventDefault();
243
243
  onRemove();
@@ -289,13 +289,11 @@ var SvgCircleCheck = function SvgCircleCheck(props) {
289
289
  };
290
290
 
291
291
  const sizeMap = {
292
- 4: `size-4`,
293
- 5: `size-5`,
294
- 6: `size-6`,
295
- 7: `size-7`,
296
- 8: `size-8`,
297
- 9: `size-9`,
298
- 10: `size-10`,
292
+ xs: 'size-4',
293
+ sm: 'size-5',
294
+ md: 'size-6',
295
+ lg: 'size-8',
296
+ xl: 'size-10',
299
297
  };
300
298
  const IconMap = {
301
299
  info: SvgInfo,
@@ -304,7 +302,7 @@ const IconMap = {
304
302
  warning: SvgTriangleAlert,
305
303
  };
306
304
  const Banner = forwardRef((props, ref) => {
307
- const { type = 'info', iconSize = 5, children, ...restProps } = props;
305
+ const { type = 'info', iconSize = 'sm', children, ...restProps } = props;
308
306
  const className = buildClassName('text-sm banner rounded-md flex w-full p-3 items-center', type === 'info' ? 'bg-blue-50 text-blue-700' : '', type === 'error' ? 'bg-red-50 text-red-700' : '', type === 'success' ? 'bg-green-50 text-green-700' : '', type === 'warning' ? 'bg-orange-50 text-orange-700' : '');
309
307
  const renderIcon = () => {
310
308
  const Icon = IconMap[type];
@@ -430,9 +428,7 @@ const TableRowLoader = ({ colSize, rowSize }) => (jsx(Fragment, { children: Arra
430
428
  /* ----------------------------------------------
431
429
  * Table
432
430
  * ---------------------------------------------- */
433
- const Table = ({ children, layout = 'auto', className = '' }) => {
434
- return (jsx("table", { className: buildClassName('border-collapse w-full divide-y divide-gray-200 dark:divide-gray-700', TABLE_LAYOUT[layout], className), children: children }));
435
- };
431
+ const Table = ({ children, layout = 'auto', className = '', caption, }) => (jsxs("table", { className: buildClassName('border-collapse w-full divide-y divide-gray-200 dark:divide-gray-700', TABLE_LAYOUT[layout], className), children: [caption && (jsx("caption", { className: "mb-2 text-sm text-left text-gray-500 dark:text-gray-400", children: caption })), children] }));
436
432
  /* ----------------------------------------------
437
433
  * Head
438
434
  * ---------------------------------------------- */
@@ -440,7 +436,7 @@ const TableHead = ({ children, className = '' }) => (jsx("thead", { className: b
440
436
  /* ----------------------------------------------
441
437
  * Body
442
438
  * ---------------------------------------------- */
443
- const TableBody = ({ children, loading, colSize, rowSize = 5, className, }) => (jsx("tbody", { className: buildClassName('text-sm text-gray-700 dark:text-white divide-y divide-gray-200 dark:divide-gray-700', className), children: loading ? jsx(TableRowLoader, { colSize: colSize, rowSize: rowSize }) : children }));
439
+ const TableBody = ({ children, loading, colSize = 1, rowSize = 5, className, }) => (jsx("tbody", { className: buildClassName('text-sm text-gray-700 dark:text-white divide-y divide-gray-200 dark:divide-gray-700', className), children: loading ? jsx(TableRowLoader, { colSize: colSize, rowSize: rowSize }) : children }));
444
440
  /* ----------------------------------------------
445
441
  * Foot
446
442
  * ---------------------------------------------- */
@@ -453,7 +449,7 @@ const TableRow = ({ children, hoverable = false, className, ...props }) => (jsx(
453
449
  * Cells
454
450
  * ---------------------------------------------- */
455
451
  const TableCell = ({ children, align = 'left', className, ...props }) => (jsx("td", { ...props, className: buildClassName('px-3 py-3 first:pl-4 last:pr-4 bg-white dark:bg-gray-900 group-hover:bg-gray-50 group-hover:dark:bg-gray-800', TEXT_ALIGN[align], className), children: jsx("div", { className: "leading-normal", children: children }) }));
456
- const TableHeaderCell = ({ children, align = 'left', className, ...props }) => (jsx("th", { ...props, className: buildClassName('px-3 py-3 first:pl-4 last:pr-4 tracking-wider whitespace-nowrap text-gray-800 dark:text-gray-300', TEXT_ALIGN[align], className), children: jsx(BodyText, { as: "span", className: "leading-none", children: children }) }));
452
+ const TableHeaderCell = ({ children, align = 'left', className, ...props }) => (jsx("th", { ...props, className: buildClassName('px-3 py-3 first:pl-4 last:pr-4 tracking-wider whitespace-nowrap text-gray-800 dark:text-gray-300', TEXT_ALIGN[align], className), children: children }));
457
453
  /* ----------------------------------------------
458
454
  * Empty Row
459
455
  * ---------------------------------------------- */
@@ -703,9 +699,11 @@ const SortIconMap = {
703
699
  };
704
700
  const getSortIcon = (sort) => sort ? (SortIconMap[sort]) : (jsx(SvgArrowUpDown, { className: "size-4 text-gray-400 shrink-0 group-hover:text-gray-800 dark:group-hover:text-white" }));
705
701
 
706
- /**
707
- * Smart auto-sort helper (optional usage)
708
- */
702
+ const DENSITY_CELL_CLASS$2 = {
703
+ compact: 'px-2 py-1.5',
704
+ default: '',
705
+ spacious: 'px-4 py-4',
706
+ };
709
707
  const autoSortItems = (items, column, sort) => {
710
708
  const path = column.name;
711
709
  return [...items].sort((a, b) => {
@@ -721,7 +719,6 @@ const autoSortItems = (items, column, sort) => {
721
719
  const dB = new Date(valueB).getTime();
722
720
  return sort === 'asc' || sort === '1' ? dA - dB : dB - dA;
723
721
  }
724
- // String fallback
725
722
  const sA = String(valueA).toLowerCase();
726
723
  const sB = String(valueB).toLowerCase();
727
724
  if (sA < sB)
@@ -731,7 +728,7 @@ const autoSortItems = (items, column, sort) => {
731
728
  return 0;
732
729
  });
733
730
  };
734
- const DataTable = ({ items, columns, sorting = {}, onSort, loading, pagination = null, setPagination = () => { }, emptyMessage = 'No data found', containerClass = '', wrapperClass = '', layout = 'auto', }) => {
731
+ function DataTable({ items, columns, sorting = {}, onSort, loading, pagination, setPagination = () => { }, emptyMessage = 'No data found', containerClass = '', wrapperClass = '', tableClass = '', headClass = '', footerRow, rowKey = 'id', onRowClick, rowClass, density = 'default', striped = false, layout = 'auto', }) {
735
732
  const internalSorting = useRef({});
736
733
  const [sortedItems, setSortedItems] = useState(items);
737
734
  useEffect(() => {
@@ -746,17 +743,38 @@ const DataTable = ({ items, columns, sorting = {}, onSort, loading, pagination =
746
743
  return;
747
744
  onSort ? onSort(column) : doAutoSort(column);
748
745
  };
749
- return (jsxs("div", { className: buildClassName('w-full rounded-lg relative', containerClass), children: [jsx("div", { className: buildClassName('overflow-x-auto overflow-y-hidden rounded-lg border border-gray-200 dark:border-gray-800', wrapperClass, pagination && 'rounded-b-none border-b-0'), children: jsxs(Table, { className: "relative w-full max-w-full h-fit", layout: layout, children: [jsx(TableHead, { children: columns.map((column) => (jsx(TableHeaderCell, { align: column.headerAlign, style: { width: column.width || 'auto' }, className: buildClassName('bg-gray-50 dark:bg-gray-800', column.sticky === 'left' &&
746
+ const getKey = (item, idx) => {
747
+ var _a;
748
+ if (typeof rowKey === 'function')
749
+ return rowKey(item);
750
+ return (_a = item[rowKey]) !== null && _a !== void 0 ? _a : idx;
751
+ };
752
+ const getRowClass = (item, idx) => {
753
+ if (!rowClass)
754
+ return '';
755
+ if (typeof rowClass === 'function')
756
+ return rowClass(item, idx);
757
+ return rowClass;
758
+ };
759
+ const getCellClass = (column, item) => {
760
+ if (!column.cellClass)
761
+ return '';
762
+ if (typeof column.cellClass === 'function')
763
+ return column.cellClass(item);
764
+ return column.cellClass;
765
+ };
766
+ const densityCellClass = DENSITY_CELL_CLASS$2[density];
767
+ return (jsxs("div", { className: buildClassName('w-full rounded-lg relative', containerClass), children: [jsx("div", { className: buildClassName('overflow-x-auto overflow-y-hidden rounded-lg border border-gray-200 dark:border-gray-800', wrapperClass, pagination && 'rounded-b-none border-b-0'), children: jsxs(Table, { className: buildClassName('relative w-full max-w-full h-fit', tableClass), layout: layout, children: [jsx(TableHead, { className: headClass, children: columns.map((column) => (jsx(TableHeaderCell, { align: column.headerAlign, style: { width: column.width || 'auto' }, className: buildClassName('bg-gray-50 dark:bg-gray-800', densityCellClass, column.sticky === 'left' &&
750
768
  'sticky left-0 z-10 shadow-[2px_0_6px_rgba(0,0,0,0.06)]', column.sticky === 'right' &&
751
- 'sticky right-0 z-10 shadow-[-2px_0_6px_rgba(0,0,0,0.06)]'), children: jsxs(TextContent, { role: column.sortable ? 'button' : undefined, className: buildClassName('group', column.sortable && 'inline-flex items-center gap-2 cursor-pointer select-none'), onClick: () => handleSort(column), children: [jsx(TextContent, { className: "shrink-0", children: column.label }), column.sortable ? getSortIcon(sorting[column.name]) : null] }) }, column.name))) }), jsx(TableBody, { loading: loading, colSize: columns.length, rowSize: (pagination === null || pagination === void 0 ? void 0 : pagination.limit) || 5, children: !loading && sortedItems.length === 0 ? (jsx(EmptyTableRow, { colSpan: columns.length, children: emptyMessage })) : (sortedItems.map((item) => (jsx(TableRow, { className: "group", children: columns.map((column) => {
769
+ 'sticky right-0 z-10 shadow-[-2px_0_6px_rgba(0,0,0,0.06)]', column.headerClass), children: jsxs(TextContent, { role: column.sortable ? 'button' : undefined, className: buildClassName('group', column.sortable && 'inline-flex items-center gap-2 cursor-pointer select-none'), onClick: () => handleSort(column), children: [jsx(TextContent, { className: "shrink-0", children: column.label }), column.sortable ? getSortIcon(sorting[column.name]) : null] }) }, column.name))) }), jsx(TableBody, { loading: loading, colSize: columns.length, rowSize: (pagination === null || pagination === void 0 ? void 0 : pagination.limit) || 5, children: !loading && sortedItems.length === 0 ? (jsx(EmptyTableRow, { colSpan: columns.length, children: emptyMessage })) : (sortedItems.map((item, idx) => (jsx(TableRow, { className: buildClassName('group', onRowClick && 'cursor-pointer', striped && idx % 2 === 1 && 'bg-gray-50 dark:bg-gray-800/40', getRowClass(item, idx)), onClick: onRowClick ? () => onRowClick(item, idx) : undefined, children: columns.map((column) => {
752
770
  var _a;
753
- return (jsx(TableCell, { align: column.align, className: buildClassName(column.sticky === 'left' &&
771
+ return (jsx(TableCell, { align: column.align, className: buildClassName(densityCellClass, striped && idx % 2 === 1 && 'bg-gray-50 dark:bg-gray-800/40', column.sticky === 'left' &&
754
772
  'sticky left-0 z-10 shadow-[2px_0_6px_rgba(0,0,0,0.06)]', column.sticky === 'right' &&
755
- 'sticky right-0 z-10 shadow-[-2px_0_6px_rgba(0,0,0,0.06)]'), children: column.render
773
+ 'sticky right-0 z-10 shadow-[-2px_0_6px_rgba(0,0,0,0.06)]', getCellClass(column, item)), children: column.render
756
774
  ? column.render(item)
757
775
  : ((_a = get(item, column.name)) !== null && _a !== void 0 ? _a : 'Not set') }, column.name));
758
- }) }, item.id)))) })] }) }), pagination && (jsx(Pagination, { ...pagination, className: "rounded-b-lg border border-t-0 border-gray-200 dark:border-gray-800", onChange: setPagination }))] }));
759
- };
776
+ }) }, getKey(item, idx))))) }), footerRow && jsx(TableFoot, { children: footerRow })] }) }), pagination && (jsx(Pagination, { ...pagination, className: "rounded-b-lg border border-t-0 border-gray-200 dark:border-gray-800", onChange: setPagination }))] }));
777
+ }
760
778
  DataTable.displayName = 'DataTable';
761
779
 
762
780
  const Title = ({ children, className }) => (jsx("h1", { className: buildClassName('text-3xl font-bold', className), children: children }));
@@ -1428,33 +1446,61 @@ const ToastProvider = ({ children, placement = 'top-center' }) => {
1428
1446
  return (jsxs(ToastContext.Provider, { value: { showToast, closeToast }, children: [children, jsx("div", { className: buildClassName('fixed z-50 flex flex-col gap-3 pointer-events-none', placementClass[placement]), children: toasts.map((toast, index) => (jsx("div", { className: "pointer-events-auto", onMouseEnter: () => toast.pauseOnHover && clearTimer(toast.id), onMouseLeave: () => toast.pauseOnHover && scheduleClose(toast), children: jsx(Toast, { ...toast, isTop: placement.startsWith('top'), onClose: () => closeToast(toast.id) }) }, toast.id))) })] }));
1429
1447
  };
1430
1448
 
1431
- const VirtualizedRow = ({ index, style, data }) => {
1432
- const { items, columns, gridTemplate } = data;
1449
+ const DENSITY_CELL_CLASS$1 = {
1450
+ compact: 'px-2 py-1.5',
1451
+ default: 'px-3 py-3',
1452
+ spacious: 'px-4 py-4',
1453
+ };
1454
+ const VirtualizedRow = ({ index, style, data, }) => {
1455
+ const { items, columns, gridTemplate, onRowClick, rowClass, density = 'default', striped } = data;
1433
1456
  const item = items[index];
1434
- return (jsx("div", { role: "row", className: "grid border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 group", style: {
1457
+ const isStriped = striped && index % 2 === 1;
1458
+ const resolvedRowClass = !rowClass
1459
+ ? ''
1460
+ : typeof rowClass === 'function'
1461
+ ? rowClass(item, index)
1462
+ : rowClass;
1463
+ const getCellClass = (col) => {
1464
+ if (!col.cellClass)
1465
+ return '';
1466
+ if (typeof col.cellClass === 'function')
1467
+ return col.cellClass(item);
1468
+ return col.cellClass;
1469
+ };
1470
+ const densityCellClass = DENSITY_CELL_CLASS$1[density];
1471
+ return (jsx("div", { role: "row", className: buildClassName('grid border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 group', onRowClick && 'cursor-pointer', isStriped && 'bg-gray-50 dark:bg-gray-800/40', resolvedRowClass), style: {
1435
1472
  ...style,
1436
1473
  display: 'grid',
1437
1474
  gridTemplateColumns: gridTemplate,
1438
1475
  minWidth: 'fit-content',
1439
- }, children: columns.map((col) => {
1476
+ }, onClick: onRowClick ? () => onRowClick(item, index) : undefined, children: columns.map((col) => {
1440
1477
  var _a;
1441
1478
  const content = col.render ? col.render(item) : ((_a = get(item, col.name)) !== null && _a !== void 0 ? _a : 'Not set');
1442
- return (jsx("div", { role: "cell", className: buildClassName('px-3 py-3 whitespace-nowrap border-r last:border-r-0 border-gray-200 dark:border-gray-700', col.align === 'center' && 'text-center', col.align === 'right' && 'text-right', col.sticky === 'left' &&
1479
+ return (jsx("div", { role: "cell", className: buildClassName('whitespace-nowrap border-r last:border-r-0 border-gray-200 dark:border-gray-700', densityCellClass, col.align === 'center' && 'text-center', col.align === 'right' && 'text-right', col.sticky === 'left' &&
1443
1480
  'sticky left-0 z-10 bg-white dark:bg-gray-900 shadow-[2px_0_4px_rgba(0,0,0,0.05)]', col.sticky === 'right' &&
1444
- 'sticky right-0 z-10 bg-white dark:bg-gray-900 shadow-[-2px_0_4px_rgba(0,0,0,0.05)]'), children: content }, col.name));
1481
+ 'sticky right-0 z-10 bg-white dark:bg-gray-900 shadow-[-2px_0_4px_rgba(0,0,0,0.05)]', isStriped && 'bg-gray-50 dark:bg-gray-800/40', getCellClass(col)), children: content }, col.name));
1445
1482
  }) }));
1446
1483
  };
1447
1484
 
1448
- /* -------------------------------------------------------
1449
- * Convert column definitions into CSS grid template
1450
- * ------------------------------------------------------- */
1451
- const useGridTemplate = (columns) => useMemo(() => columns.map((col) => (col.width ? `${col.width}px` : '1fr')).join(' '), [columns]);
1452
- const VirtualizedDataTable = ({ items, columns, sorting = {}, onSort, loading = false, emptyMessage = 'No data available', containerClass = '', wrapperClass = '', rowHeight = 48, maxHeight = 400, }) => {
1485
+ const DENSITY_CELL_CLASS = {
1486
+ compact: 'px-2 py-1.5',
1487
+ default: 'px-3 py-3',
1488
+ spacious: 'px-4 py-4',
1489
+ };
1490
+ const columnWidth = (col) => {
1491
+ if (!col.width)
1492
+ return '1fr';
1493
+ if (typeof col.width === 'number')
1494
+ return `${col.width}px`;
1495
+ return col.width;
1496
+ };
1497
+ function VirtualizedDataTable({ items, columns, sorting = {}, onSort, loading = false, emptyMessage = 'No data available', containerClass = '', wrapperClass = '', onRowClick, rowClass, density = 'default', striped = false, rowHeight = 48, maxHeight = 400, overscanCount = 3, }) {
1453
1498
  const scrollRef = useRef(null);
1454
1499
  const outerRef = useRef(null);
1455
1500
  const headerRef = useRef(null);
1456
- const gridTemplate = useGridTemplate(columns);
1457
- /** Sync horizontal scroll of the virtualized body → header */
1501
+ const gridTemplate = useMemo(() => columns.map((col) => columnWidth(col)).join(' '), [columns]);
1502
+ const densityCellClass = DENSITY_CELL_CLASS[density];
1503
+ const skeletonRows = Math.max(1, Math.floor(maxHeight / rowHeight));
1458
1504
  useEffect(() => {
1459
1505
  const el = outerRef.current;
1460
1506
  if (!el)
@@ -1470,35 +1516,31 @@ const VirtualizedDataTable = ({ items, columns, sorting = {}, onSort, loading =
1470
1516
  if (column.sortable)
1471
1517
  onSort === null || onSort === void 0 ? void 0 : onSort(column);
1472
1518
  }, [onSort]);
1519
+ const itemData = useMemo(() => ({ items, columns, gridTemplate, onRowClick, rowClass, density, striped }), [items, columns, gridTemplate, onRowClick, rowClass, density, striped]);
1473
1520
  /* -------------------------------------------------------
1474
- * RENDER: Header row (real DOM for accessibility)
1521
+ * Header
1475
1522
  * ------------------------------------------------------- */
1476
- const Header = (jsx("div", { ref: headerRef, className: "overflow-hidden w-full", children: jsx("div", { className: "grid bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700", style: {
1477
- gridTemplateColumns: gridTemplate,
1478
- minWidth: 'fit-content',
1479
- }, children: columns.map((col) => (jsx("div", { role: "columnheader", className: buildClassName('px-3 py-3 font-semibold text-sm whitespace-nowrap flex items-center', 'border-r last:border-r-0 border-gray-200 dark:border-gray-700', col.sticky === 'left' &&
1523
+ const Header = (jsx("div", { ref: headerRef, className: "overflow-hidden w-full", children: jsx("div", { className: "grid bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700", style: { gridTemplateColumns: gridTemplate, minWidth: 'fit-content' }, children: columns.map((col) => (jsx("div", { role: "columnheader", className: buildClassName('font-semibold text-sm whitespace-nowrap flex items-center', densityCellClass, 'border-r last:border-r-0 border-gray-200 dark:border-gray-700', col.sticky === 'left' &&
1480
1524
  'sticky left-0 z-20 bg-gray-50 dark:bg-gray-800 shadow-[2px_0_4px_rgba(0,0,0,0.05)]', col.sticky === 'right' &&
1481
- 'sticky right-0 z-20 bg-gray-50 dark:bg-gray-800 shadow-[-2px_0_4px_rgba(0,0,0,0.05)]'), children: jsxs(TextContent, { role: col.sortable ? 'button' : undefined, className: buildClassName(col.sortable && 'inline-flex items-center gap-2 cursor-pointer select-none'), onClick: () => handleSort(col), children: [col.label, col.sortable ? getSortIcon(sorting[col.name]) : null] }) }, col.name))) }) }));
1525
+ 'sticky right-0 z-20 bg-gray-50 dark:bg-gray-800 shadow-[-2px_0_4px_rgba(0,0,0,0.05)]', col.headerClass), children: jsxs(TextContent, { role: col.sortable ? 'button' : undefined, className: buildClassName(col.sortable && 'inline-flex items-center gap-2 cursor-pointer select-none'), onClick: () => handleSort(col), children: [col.label, col.sortable ? getSortIcon(sorting[col.name]) : null] }) }, col.name))) }) }));
1526
+ const wrapperCls = buildClassName('rounded-lg border border-gray-200 dark:border-gray-800 overflow-hidden', wrapperClass);
1482
1527
  /* -------------------------------------------------------
1483
- * LOADING STATE (non-virtualized placeholder)
1528
+ * Loading state
1484
1529
  * ------------------------------------------------------- */
1485
1530
  if (loading) {
1486
- return (jsx("div", { className: buildClassName('w-full rounded-lg relative', containerClass), children: jsxs("div", { className: buildClassName('rounded-lg border border-gray-200 dark:border-gray-800 overflow-hidden', wrapperClass), children: [Header, jsx("div", { style: { height: maxHeight }, className: "flex flex-col", children: [...Array(6)].map((_, idx) => (jsx("div", { "data-testid": "skeleton-row", className: "grid animate-pulse bg-white dark:bg-gray-900 border-b border-gray-100 dark:border-gray-800", style: {
1487
- gridTemplateColumns: gridTemplate,
1488
- height: rowHeight,
1489
- }, children: columns.map((col) => (jsx("div", { className: "px-3 py-3", children: jsx("div", { className: "h-4 w-3/4 bg-gray-300 dark:bg-gray-700 rounded" }) }, col.name))) }, idx))) })] }) }));
1531
+ return (jsx("div", { className: buildClassName('w-full rounded-lg relative', containerClass), children: jsxs("div", { className: wrapperCls, children: [Header, jsx("div", { style: { height: maxHeight }, className: "flex flex-col", children: Array.from({ length: skeletonRows }).map((_, idx) => (jsx("div", { "data-testid": "skeleton-row", className: "grid animate-pulse bg-white dark:bg-gray-900 border-b border-gray-100 dark:border-gray-800", style: { gridTemplateColumns: gridTemplate, height: rowHeight }, children: columns.map((col) => (jsx("div", { className: densityCellClass, children: jsx("div", { className: "h-4 w-3/4 bg-gray-300 dark:bg-gray-700 rounded" }) }, col.name))) }, idx))) })] }) }));
1490
1532
  }
1491
1533
  /* -------------------------------------------------------
1492
- * EMPTY STATE (non-virtualized → single row)
1534
+ * Empty state
1493
1535
  * ------------------------------------------------------- */
1494
- if (!loading && items.length === 0) {
1495
- return (jsx("div", { className: buildClassName('w-full rounded-lg relative', containerClass), children: jsxs("div", { className: buildClassName('rounded-lg border border-gray-200 dark:border-gray-800 overflow-hidden', wrapperClass), children: [Header, jsx("div", { className: "flex items-center justify-center text-gray-500 dark:text-gray-300", style: { height: maxHeight }, children: emptyMessage })] }) }));
1536
+ if (items.length === 0) {
1537
+ return (jsx("div", { className: buildClassName('w-full rounded-lg relative', containerClass), children: jsxs("div", { className: wrapperCls, children: [Header, jsx("div", { className: "flex items-center justify-center text-gray-500 dark:text-gray-300", style: { height: maxHeight }, children: emptyMessage })] }) }));
1496
1538
  }
1497
1539
  /* -------------------------------------------------------
1498
- * MAIN (virtualized body)
1540
+ * Virtualized body
1499
1541
  * ------------------------------------------------------- */
1500
- return (jsx("div", { className: buildClassName('w-full rounded-lg relative', containerClass), children: jsxs("div", { className: buildClassName('rounded-lg border border-gray-200 dark:border-gray-800 overflow-hidden', wrapperClass), children: [Header, jsx(FixedSizeList, { ref: scrollRef, outerRef: outerRef, height: maxHeight, width: "100%", itemCount: items.length, itemSize: rowHeight, itemData: { items, columns, gridTemplate }, children: VirtualizedRow })] }) }));
1501
- };
1542
+ return (jsx("div", { className: buildClassName('w-full rounded-lg relative', containerClass), children: jsxs("div", { className: wrapperCls, children: [Header, jsx(FixedSizeList, { ref: scrollRef, outerRef: outerRef, height: maxHeight, width: "100%", itemCount: items.length, itemSize: rowHeight, itemData: itemData, overscanCount: overscanCount, children: VirtualizedRow })] }) }));
1543
+ }
1502
1544
 
1503
1545
  const useIsMobile = () => {
1504
1546
  const [isMobile, setIsMobile] = useState(false);