@pk-design/react-tailwind 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -430,9 +430,7 @@ const TableRowLoader = ({ colSize, rowSize }) => (jsx(Fragment, { children: Arra
430
430
  /* ----------------------------------------------
431
431
  * Table
432
432
  * ---------------------------------------------- */
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
- };
433
+ 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
434
  /* ----------------------------------------------
437
435
  * Head
438
436
  * ---------------------------------------------- */
@@ -440,7 +438,7 @@ const TableHead = ({ children, className = '' }) => (jsx("thead", { className: b
440
438
  /* ----------------------------------------------
441
439
  * Body
442
440
  * ---------------------------------------------- */
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 }));
441
+ 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
442
  /* ----------------------------------------------
445
443
  * Foot
446
444
  * ---------------------------------------------- */
@@ -453,7 +451,7 @@ const TableRow = ({ children, hoverable = false, className, ...props }) => (jsx(
453
451
  * Cells
454
452
  * ---------------------------------------------- */
455
453
  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 }) }));
454
+ 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
455
  /* ----------------------------------------------
458
456
  * Empty Row
459
457
  * ---------------------------------------------- */
@@ -703,9 +701,11 @@ const SortIconMap = {
703
701
  };
704
702
  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
703
 
706
- /**
707
- * Smart auto-sort helper (optional usage)
708
- */
704
+ const DENSITY_CELL_CLASS$2 = {
705
+ compact: 'px-2 py-1.5',
706
+ default: '',
707
+ spacious: 'px-4 py-4',
708
+ };
709
709
  const autoSortItems = (items, column, sort) => {
710
710
  const path = column.name;
711
711
  return [...items].sort((a, b) => {
@@ -721,7 +721,6 @@ const autoSortItems = (items, column, sort) => {
721
721
  const dB = new Date(valueB).getTime();
722
722
  return sort === 'asc' || sort === '1' ? dA - dB : dB - dA;
723
723
  }
724
- // String fallback
725
724
  const sA = String(valueA).toLowerCase();
726
725
  const sB = String(valueB).toLowerCase();
727
726
  if (sA < sB)
@@ -731,7 +730,7 @@ const autoSortItems = (items, column, sort) => {
731
730
  return 0;
732
731
  });
733
732
  };
734
- const DataTable = ({ items, columns, sorting = {}, onSort, loading, pagination = null, setPagination = () => { }, emptyMessage = 'No data found', containerClass = '', wrapperClass = '', layout = 'auto', }) => {
733
+ 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
734
  const internalSorting = useRef({});
736
735
  const [sortedItems, setSortedItems] = useState(items);
737
736
  useEffect(() => {
@@ -746,17 +745,38 @@ const DataTable = ({ items, columns, sorting = {}, onSort, loading, pagination =
746
745
  return;
747
746
  onSort ? onSort(column) : doAutoSort(column);
748
747
  };
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' &&
748
+ const getKey = (item, idx) => {
749
+ var _a;
750
+ if (typeof rowKey === 'function')
751
+ return rowKey(item);
752
+ return (_a = item[rowKey]) !== null && _a !== void 0 ? _a : idx;
753
+ };
754
+ const getRowClass = (item, idx) => {
755
+ if (!rowClass)
756
+ return '';
757
+ if (typeof rowClass === 'function')
758
+ return rowClass(item, idx);
759
+ return rowClass;
760
+ };
761
+ const getCellClass = (column, item) => {
762
+ if (!column.cellClass)
763
+ return '';
764
+ if (typeof column.cellClass === 'function')
765
+ return column.cellClass(item);
766
+ return column.cellClass;
767
+ };
768
+ const densityCellClass = DENSITY_CELL_CLASS$2[density];
769
+ 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
770
  '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) => {
771
+ '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
772
  var _a;
753
- return (jsx(TableCell, { align: column.align, className: buildClassName(column.sticky === 'left' &&
773
+ 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
774
  '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
775
+ 'sticky right-0 z-10 shadow-[-2px_0_6px_rgba(0,0,0,0.06)]', getCellClass(column, item)), children: column.render
756
776
  ? column.render(item)
757
777
  : ((_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
- };
778
+ }) }, 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 }))] }));
779
+ }
760
780
  DataTable.displayName = 'DataTable';
761
781
 
762
782
  const Title = ({ children, className }) => (jsx("h1", { className: buildClassName('text-3xl font-bold', className), children: children }));
@@ -1282,35 +1302,48 @@ const SelectBox = forwardRef((props, forwardedRef) => {
1282
1302
  }, "data-testid": "combobox-input" }), jsx(ComboboxButton, { className: "group absolute inset-y-0 right-0 px-2.5", "data-testid": "combobox-button", children: jsx(SvgChevronDown, { className: chevronIconClass }) })] })) : (jsxs("div", { className: "relative", ref: forwardedRef, children: [jsx(ComboboxInput, { "aria-label": name, displayValue: (option) => (option ? getDisplayValue(option) : null), autoComplete: "off", placeholder: placeholder, onChange: (event) => setQuery(event.target.value), className: inputClass, disabled: disabled, id: id, readOnly: !searchable, ref: inputRef, "data-testid": "combobox-input" }), allowClear && !isEmpty(selected) && (jsx(Button, { size: "xs", iconOnly: true, "aria-label": "Clear selection", theme: "secondary", variant: "plain", className: "group absolute inset-y-0 right-8 h-6 top-2", onClick: () => handleOnChangeInternal(multiple ? [] : null), disabled: disabled, "data-testid": "combobox-clear-button", noOutlineOnFocus: true, children: jsx(SvgX, { className: "size-4 stroke-gray-500" }) })), jsx(ComboboxButton, { className: "group absolute inset-y-0 right-0 px-2.5", "data-testid": "combobox-button", children: jsx(SvgChevronDown, { className: chevronIconClass }) })] })), jsx(ComboboxOptions, { anchor: { to: 'bottom', gap: 5 }, as: "div", className: buildClassName('w-[var(--input-width)] rounded-lg border border-gray-200 dark:border-white/5 bg-white dark:bg-gray-800 p-1 [--anchor-gap:var(--spacing-1)]', '[--anchor-max-height:20rem]', 'origin-top border transition duration-200 ease-[cubic-bezier(0.4, 0.0, 0.2, 1)] data-[closed]:translate-y-[-15%] data-[closed]:opacity-0', dropdownContainerClass), transition: true, "data-testid": "combobox-options", children: optionsLoading ? (jsx("div", { "data-testid": "options-loading", className: "flex items-center justify-center py-2 px-3 select-none", children: jsx(Loader, { size: "sm" }) })) : shouldShowTypeToSearch ? (jsx("div", { className: "flex items-center justify-between py-2 px-3 select-none", children: "Type to search..." })) : (jsxs("div", { children: [filteredOptions.map((option) => (jsx(ComboboxOption, { disabled: option.disabled, value: option, className: "group rounded-lg py-1.5 px-3 select-none data-[focus]:bg-black/3 dark:data-[focus]:bg-white/10 data-disabled:opacity-50 cursor-pointer data-disabled:pointer-events-none", children: jsxs("div", { className: "flex items-center justify-between gap-2 dark:text-white group-data-[selected]:text-blue-600", children: [jsx("span", { className: "text-sm/6", children: getDisplayValue(option) }), jsx(SvgCheck, { className: "invisible size-5 dark:stroke-gray-300 group-data-[selected]:visible" })] }) }, getValue(option)))), filteredOptions.length === 0 && query && !optionsLoading && (jsxs("div", { "data-testid": "no-result-found", className: "flex items-center gap-2 py-1.5 px-3 select-none text-gray-500 dark:text-gray-400", children: ["No results found for ", jsx("span", { className: "font-semibold", children: query })] })), allowAdd && (jsx(ComboboxOption, { value: query, className: "group rounded-lg py-1.5 px-3 cursor-pointer text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700", onClick: () => void handleAddAndSelect(query), children: jsxs("div", { className: "flex items-center gap-2", children: [jsx(SvgCirclePlus, { className: "size-4.5" }), jsxs(TextContent, { children: [addNewText, " \"", query, "\""] })] }) }))] })) })] }), showErrorMessage && error && (jsx("div", { className: "mt-1", children: jsx(TextContent, { error: true, small: true, children: error }) }))] }));
1283
1303
  });
1284
1304
 
1285
- const Tabs = ({ tabs, defaultIndex = 0, variant = 'underline', className = '', listClass = '', panelClass = '', }) => {
1286
- return (jsxs(TabGroup, { defaultIndex: defaultIndex, className: className, children: [jsx(TabList, { className: buildClassName('flex gap-2', listClass), children: tabs.map((tab, idx) => (jsx(Tab, { as: Fragment$1, children: ({ selected }) => (jsx("button", { className: buildTabClass({ selected, variant }), children: tab.label })) }, idx))) }), jsx(TabPanels, { className: buildClassName('mt-3', panelClass), children: tabs.map((tab, idx) => (jsx(TabPanel, { className: "rounded-lg bg-white dark:bg-gray-800 p-4 shadow-sm", children: tab.content }, idx))) })] }));
1305
+ const Tabs = ({ tabs, defaultIndex = 0, selectedIndex, onChange, variant = 'underline', orientation = 'horizontal', stretch = false, className = '', listClass = '', panelsClass = '', }) => {
1306
+ const isVertical = orientation === 'vertical';
1307
+ return (jsxs(TabGroup, { defaultIndex: defaultIndex, selectedIndex: selectedIndex, onChange: onChange, vertical: isVertical, className: buildClassName(isVertical && 'flex gap-4', stretch && !isVertical && 'w-full', className), children: [jsx(TabList, { className: buildClassName(isVertical ? 'flex flex-col gap-1' : 'flex gap-2', stretch && !isVertical && 'w-full', listClass), children: tabs.map((tab, idx) => {
1308
+ var _a;
1309
+ return (jsx(Tab, { disabled: tab.disabled, as: Fragment$1, children: ({ selected }) => (jsxs("button", { disabled: tab.disabled, className: buildTabClass({
1310
+ selected,
1311
+ variant,
1312
+ stretch,
1313
+ isVertical,
1314
+ tabClass: tab.tabClass,
1315
+ }), children: [tab.icon && jsx("span", { className: "inline-flex shrink-0", children: tab.icon }), tab.label] })) }, (_a = tab.id) !== null && _a !== void 0 ? _a : idx));
1316
+ }) }), jsx(TabPanels, { className: buildClassName(isVertical ? 'flex-1' : 'mt-3', panelsClass), children: tabs.map((tab, idx) => {
1317
+ var _a;
1318
+ return (jsx(TabPanel, { className: tab.panelClass !== undefined
1319
+ ? tab.panelClass
1320
+ : 'rounded-lg bg-white dark:bg-gray-800 p-4 shadow-sm', children: tab.content }, (_a = tab.id) !== null && _a !== void 0 ? _a : idx));
1321
+ }) })] }));
1287
1322
  };
1288
- /**
1289
- * Build tab styles based on variant + active state
1290
- */
1291
- function buildTabClass({ selected, variant, }) {
1323
+ function buildTabClass({ selected, variant, stretch, isVertical, tabClass, }) {
1324
+ const base = buildClassName('inline-flex items-center gap-1.5 text-sm font-medium transition focus-visible:outline-none', 'disabled:opacity-40 disabled:cursor-not-allowed disabled:pointer-events-none', stretch && !isVertical && 'flex-1 justify-center');
1325
+ let variantClass;
1292
1326
  switch (variant) {
1293
- /**
1294
- * UNDERLINE (Default)
1295
- */
1296
1327
  case 'underline':
1297
- return buildClassName('px-3 py-1.5 text-sm font-medium transition relative', 'text-gray-600 dark:text-gray-300', 'hover:text-gray-900 dark:hover:text-white', selected &&
1298
- 'text-gray-900 dark:text-white after:absolute after:left-0 after:right-0 after:-bottom-1 after:h-0.5 after:bg-[var(--ui-primary)]');
1299
- /**
1300
- * SEGMENTED
1301
- */
1328
+ variantClass = isVertical
1329
+ ? buildClassName('px-3 py-2 w-full text-left border-l-2', selected
1330
+ ? 'border-[var(--ui-primary)] text-gray-900 dark:text-white'
1331
+ : 'border-transparent text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white hover:border-gray-300 dark:hover:border-gray-600')
1332
+ : buildClassName('px-3 py-1.5 relative text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white', selected &&
1333
+ 'text-gray-900 dark:text-white after:absolute after:z-10 after:left-0 after:right-0 after:-bottom-1 after:h-0.5 after:bg-[var(--ui-primary)]');
1334
+ break;
1302
1335
  case 'segmented':
1303
- return buildClassName('px-3 py-1.5 text-sm font-medium rounded-lg transition', selected
1336
+ variantClass = buildClassName('px-3 py-1.5 rounded-lg', selected
1304
1337
  ? 'bg-[var(--ui-primary)] text-white'
1305
1338
  : 'text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700');
1306
- /**
1307
- * SOLID
1308
- */
1339
+ break;
1309
1340
  case 'solid':
1310
- return buildClassName('px-3 py-1.5 text-sm font-medium rounded-md transition', selected
1341
+ variantClass = buildClassName('px-3 py-1.5 rounded-md', selected
1311
1342
  ? 'bg-gray-900 dark:bg-gray-700 text-white'
1312
1343
  : 'bg-gray-200 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-700');
1344
+ break;
1313
1345
  }
1346
+ return buildClassName(base, variantClass, tabClass);
1314
1347
  }
1315
1348
 
1316
1349
  const Textarea = forwardRef((props, ref) => {
@@ -1415,33 +1448,61 @@ const ToastProvider = ({ children, placement = 'top-center' }) => {
1415
1448
  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))) })] }));
1416
1449
  };
1417
1450
 
1418
- const VirtualizedRow = ({ index, style, data }) => {
1419
- const { items, columns, gridTemplate } = data;
1451
+ const DENSITY_CELL_CLASS$1 = {
1452
+ compact: 'px-2 py-1.5',
1453
+ default: 'px-3 py-3',
1454
+ spacious: 'px-4 py-4',
1455
+ };
1456
+ const VirtualizedRow = ({ index, style, data, }) => {
1457
+ const { items, columns, gridTemplate, onRowClick, rowClass, density = 'default', striped } = data;
1420
1458
  const item = items[index];
1421
- return (jsx("div", { role: "row", className: "grid border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 group", style: {
1459
+ const isStriped = striped && index % 2 === 1;
1460
+ const resolvedRowClass = !rowClass
1461
+ ? ''
1462
+ : typeof rowClass === 'function'
1463
+ ? rowClass(item, index)
1464
+ : rowClass;
1465
+ const getCellClass = (col) => {
1466
+ if (!col.cellClass)
1467
+ return '';
1468
+ if (typeof col.cellClass === 'function')
1469
+ return col.cellClass(item);
1470
+ return col.cellClass;
1471
+ };
1472
+ const densityCellClass = DENSITY_CELL_CLASS$1[density];
1473
+ 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: {
1422
1474
  ...style,
1423
1475
  display: 'grid',
1424
1476
  gridTemplateColumns: gridTemplate,
1425
1477
  minWidth: 'fit-content',
1426
- }, children: columns.map((col) => {
1478
+ }, onClick: onRowClick ? () => onRowClick(item, index) : undefined, children: columns.map((col) => {
1427
1479
  var _a;
1428
1480
  const content = col.render ? col.render(item) : ((_a = get(item, col.name)) !== null && _a !== void 0 ? _a : 'Not set');
1429
- 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' &&
1481
+ 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' &&
1430
1482
  'sticky left-0 z-10 bg-white dark:bg-gray-900 shadow-[2px_0_4px_rgba(0,0,0,0.05)]', col.sticky === 'right' &&
1431
- '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));
1483
+ '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));
1432
1484
  }) }));
1433
1485
  };
1434
1486
 
1435
- /* -------------------------------------------------------
1436
- * Convert column definitions into CSS grid template
1437
- * ------------------------------------------------------- */
1438
- const useGridTemplate = (columns) => useMemo(() => columns.map((col) => (col.width ? `${col.width}px` : '1fr')).join(' '), [columns]);
1439
- const VirtualizedDataTable = ({ items, columns, sorting = {}, onSort, loading = false, emptyMessage = 'No data available', containerClass = '', wrapperClass = '', rowHeight = 48, maxHeight = 400, }) => {
1487
+ const DENSITY_CELL_CLASS = {
1488
+ compact: 'px-2 py-1.5',
1489
+ default: 'px-3 py-3',
1490
+ spacious: 'px-4 py-4',
1491
+ };
1492
+ const columnWidth = (col) => {
1493
+ if (!col.width)
1494
+ return '1fr';
1495
+ if (typeof col.width === 'number')
1496
+ return `${col.width}px`;
1497
+ return col.width;
1498
+ };
1499
+ 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, }) {
1440
1500
  const scrollRef = useRef(null);
1441
1501
  const outerRef = useRef(null);
1442
1502
  const headerRef = useRef(null);
1443
- const gridTemplate = useGridTemplate(columns);
1444
- /** Sync horizontal scroll of the virtualized body → header */
1503
+ const gridTemplate = useMemo(() => columns.map((col) => columnWidth(col)).join(' '), [columns]);
1504
+ const densityCellClass = DENSITY_CELL_CLASS[density];
1505
+ const skeletonRows = Math.max(1, Math.floor(maxHeight / rowHeight));
1445
1506
  useEffect(() => {
1446
1507
  const el = outerRef.current;
1447
1508
  if (!el)
@@ -1457,35 +1518,31 @@ const VirtualizedDataTable = ({ items, columns, sorting = {}, onSort, loading =
1457
1518
  if (column.sortable)
1458
1519
  onSort === null || onSort === void 0 ? void 0 : onSort(column);
1459
1520
  }, [onSort]);
1521
+ const itemData = useMemo(() => ({ items, columns, gridTemplate, onRowClick, rowClass, density, striped }), [items, columns, gridTemplate, onRowClick, rowClass, density, striped]);
1460
1522
  /* -------------------------------------------------------
1461
- * RENDER: Header row (real DOM for accessibility)
1523
+ * Header
1462
1524
  * ------------------------------------------------------- */
1463
- 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: {
1464
- gridTemplateColumns: gridTemplate,
1465
- minWidth: 'fit-content',
1466
- }, 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' &&
1525
+ 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' &&
1467
1526
  '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' &&
1468
- '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))) }) }));
1527
+ '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))) }) }));
1528
+ const wrapperCls = buildClassName('rounded-lg border border-gray-200 dark:border-gray-800 overflow-hidden', wrapperClass);
1469
1529
  /* -------------------------------------------------------
1470
- * LOADING STATE (non-virtualized placeholder)
1530
+ * Loading state
1471
1531
  * ------------------------------------------------------- */
1472
1532
  if (loading) {
1473
- 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: {
1474
- gridTemplateColumns: gridTemplate,
1475
- height: rowHeight,
1476
- }, 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))) })] }) }));
1533
+ 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))) })] }) }));
1477
1534
  }
1478
1535
  /* -------------------------------------------------------
1479
- * EMPTY STATE (non-virtualized → single row)
1536
+ * Empty state
1480
1537
  * ------------------------------------------------------- */
1481
- if (!loading && items.length === 0) {
1482
- 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 })] }) }));
1538
+ if (items.length === 0) {
1539
+ 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 })] }) }));
1483
1540
  }
1484
1541
  /* -------------------------------------------------------
1485
- * MAIN (virtualized body)
1542
+ * Virtualized body
1486
1543
  * ------------------------------------------------------- */
1487
- 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 })] }) }));
1488
- };
1544
+ 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 })] }) }));
1545
+ }
1489
1546
 
1490
1547
  const useIsMobile = () => {
1491
1548
  const [isMobile, setIsMobile] = useState(false);