@dynostack/react-grid 0.1.0 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -40,6 +40,7 @@ A single `<DataTable />` component that gives you ag-grid–level functionality
40
40
  - [Tailwind setup](#tailwind-setup)
41
41
  - [Theme tokens](#theme-tokens)
42
42
  - [Quick start](#quick-start)
43
+ - [Data fetching](#data-fetching)
43
44
  - [Theming](#theming)
44
45
  - [Density](#density)
45
46
  - [Feature flags](#feature-flags)
@@ -167,6 +168,84 @@ That's it. You now have sort + filter + edit + add + delete + export + pin + res
167
168
 
168
169
  ---
169
170
 
171
+ ## Data fetching
172
+
173
+ `DataTable` supports two data ownership models.
174
+
175
+ ### 1. Controlled data from your page
176
+
177
+ Use this when your app already owns fetching with TanStack Query, SWR, Redux,
178
+ loader functions, or custom hooks. The table receives rows and loading flags as
179
+ props, and your app owns error/toast behavior.
180
+
181
+ ```tsx
182
+ const usersQuery = useQuery({
183
+ queryKey: ["users"],
184
+ queryFn: fetchUsers,
185
+ })
186
+
187
+ <DataTable<User>
188
+ data={usersQuery.data ?? []}
189
+ columns={columns}
190
+ isLoading={usersQuery.isLoading}
191
+ isFetching={usersQuery.isFetching}
192
+ onRefresh={() => usersQuery.refetch()}
193
+ totalRecords={usersQuery.data?.length ?? 0}
194
+ />
195
+ ```
196
+
197
+ For blocking load errors, render your own page-level error state or pass an empty
198
+ array. For background errors, show a toast from your query/mutation callbacks.
199
+
200
+ ### 2. Internal fetching with `dataSource`
201
+
202
+ Use this when you want the table to own fetch/loading/error/refresh state.
203
+ `fetchRows` receives the current table state and can return either an array or
204
+ `{ rows, totalRecords }`.
205
+
206
+ ```tsx
207
+ <DataTable<User>
208
+ columns={columns}
209
+ dataSource={{
210
+ fetchRows: async ({ pageIndex, pageSize, sorting, columnFilters, globalFilter }) => {
211
+ const res = await fetch("/api/users", {
212
+ method: "POST",
213
+ headers: { "content-type": "application/json" },
214
+ body: JSON.stringify({
215
+ pageIndex,
216
+ pageSize,
217
+ sorting,
218
+ columnFilters,
219
+ q: globalFilter,
220
+ }),
221
+ })
222
+
223
+ if (!res.ok) throw new Error("Failed to load users")
224
+ return res.json() as Promise<{ rows: User[]; totalRecords: number }>
225
+ },
226
+ mode: "server",
227
+ onError: (error, context) => {
228
+ toast.error(context.message)
229
+ console.error(error)
230
+ },
231
+ }}
232
+ />
233
+ ```
234
+
235
+ Internal mode behavior:
236
+
237
+ - Initial load shows the table skeleton.
238
+ - Initial load failure shows an inline `Could not load rows` state with `Retry`.
239
+ - Refresh failure keeps the last successful rows visible and calls `onError`.
240
+ - `mode: "client"` expects the full row array and lets the table sort/filter/page in memory.
241
+ - `mode: "server"` expects the current page and uses `totalRecords` for pagination.
242
+
243
+ Keep using `onCellEdit`, `onRowSave`, `onAddRow`, and `onBulkDelete` for mutations.
244
+ The table does not assume your write API; this lets you choose optimistic updates,
245
+ rollback, toast notifications, and validation.
246
+
247
+ ---
248
+
170
249
  ## Theming
171
250
 
172
251
  All shadcn tokens are supported plus `radius` and `fontFamily`. Anything you omit falls through to the consumer's `:root`.
@@ -454,7 +533,11 @@ Mark a column non-exportable via `meta.exportable: false`.
454
533
 
455
534
  ## Server-side data
456
535
 
457
- Provide a controlled global filter and refetch on change:
536
+ You can do server-side data in either mode.
537
+
538
+ ### Controlled server-side data
539
+
540
+ Own the API call in your page and pass the result into the table:
458
541
 
459
542
  ```tsx
460
543
  const [q, setQ] = useState("")
@@ -474,7 +557,38 @@ const usersQuery = useQuery({
474
557
  />
475
558
  ```
476
559
 
477
- Pair with TanStack Query's pagination/cursor utilities for cursor-based grids.
560
+ Pair this with TanStack Query's pagination/cursor utilities when you want query
561
+ caching and mutation orchestration outside the grid.
562
+
563
+ ### Built-in server-side data
564
+
565
+ Let the grid call your API by setting `dataSource.mode` to `"server"`:
566
+
567
+ ```tsx
568
+ <DataTable<User>
569
+ columns={columns}
570
+ dataSource={{
571
+ mode: "server",
572
+ fetchRows: async (state) => {
573
+ const res = await fetch("/api/users/grid", {
574
+ method: "POST",
575
+ headers: { "content-type": "application/json" },
576
+ body: JSON.stringify(state),
577
+ })
578
+
579
+ if (!res.ok) throw new Error("Users request failed")
580
+ return res.json() as Promise<{ rows: User[]; totalRecords: number }>
581
+ },
582
+ onError: (_error, context) => {
583
+ toast.error(context.message)
584
+ },
585
+ }}
586
+ />
587
+ ```
588
+
589
+ `state` contains `pageIndex`, `pageSize`, `sorting`, `columnFilters`, and
590
+ `globalFilter`. In server mode the table assumes the API already applied those
591
+ operations and only renders the returned page.
478
592
 
479
593
  ---
480
594
 
@@ -482,8 +596,9 @@ Pair with TanStack Query's pagination/cursor utilities for cursor-based grids.
482
596
 
483
597
  | Prop | Type | Default | Description |
484
598
  | ------------------------- | ---------------------------------------------------------- | ---------------------- | ------------------------------------------------------ |
485
- | `data` | `TData[]` | — | Row data. |
599
+ | `data` | `TData[]` | `[]` | Controlled row data. Use this when fetching outside the table. |
486
600
  | `columns` | `ColumnDef<TData>[]` | — | TanStack column definitions. |
601
+ | `dataSource` | `DataTableDataSource<TData>` | — | Optional internal fetcher for client/server data loading. |
487
602
  | `isLoading` | `boolean` | `false` | Initial skeleton state. |
488
603
  | `isFetching` | `boolean` | `false` | Background-refresh indicator. |
489
604
  | `onRefresh` | `() => void` | — | Refresh button handler. |
@@ -514,6 +629,26 @@ Pair with TanStack Query's pagination/cursor utilities for cursor-based grids.
514
629
 
515
630
  `TData` must extend `{ id: string \| number }`.
516
631
 
632
+ ```ts
633
+ type DataTableDataSource<TData> = {
634
+ fetchRows: (params: {
635
+ pageIndex: number
636
+ pageSize: number
637
+ sorting: SortingState
638
+ columnFilters: ColumnFiltersState
639
+ globalFilter: string
640
+ }) => Promise<TData[] | { rows: TData[]; totalRecords?: number }>
641
+ mode?: "client" | "server"
642
+ enabled?: boolean
643
+ initialData?: TData[]
644
+ deps?: readonly unknown[]
645
+ onError?: (
646
+ error: unknown,
647
+ context: { type: "load" | "refresh"; message: string }
648
+ ) => void
649
+ }
650
+ ```
651
+
517
652
  ---
518
653
 
519
654
  ## Compatibility
package/dist/index.cjs CHANGED
@@ -8,6 +8,7 @@ var tailwindMerge = require('tailwind-merge');
8
8
  var radixUi = require('radix-ui');
9
9
  var jsxRuntime = require('react/jsx-runtime');
10
10
  var classVarianceAuthority = require('class-variance-authority');
11
+ var reactDom = require('react-dom');
11
12
 
12
13
  function cn(...inputs) {
13
14
  return tailwindMerge.twMerge(clsx.clsx(inputs));
@@ -1717,14 +1718,34 @@ function DataTableRowActions({
1717
1718
  onCancelEdit
1718
1719
  }) {
1719
1720
  const [open, setOpen] = react.useState(false);
1721
+ const [menuPosition, setMenuPosition] = react.useState({ top: 0, left: 0 });
1722
+ const triggerRef = react.useRef(null);
1720
1723
  const menuRef = react.useRef(null);
1724
+ react.useLayoutEffect(() => {
1725
+ if (!open || !triggerRef.current) return;
1726
+ const rect = triggerRef.current.getBoundingClientRect();
1727
+ setMenuPosition({
1728
+ top: rect.bottom + 4,
1729
+ left: Math.max(8, rect.right - 160)
1730
+ });
1731
+ }, [open]);
1721
1732
  react.useEffect(() => {
1722
1733
  if (!open) return;
1723
1734
  const onPointerDown = (event) => {
1724
- if (!menuRef.current?.contains(event.target)) setOpen(false);
1735
+ const target = event.target;
1736
+ if (!menuRef.current?.contains(target) && !triggerRef.current?.contains(target)) {
1737
+ setOpen(false);
1738
+ }
1725
1739
  };
1740
+ const onDismiss = () => setOpen(false);
1726
1741
  document.addEventListener("pointerdown", onPointerDown);
1727
- return () => document.removeEventListener("pointerdown", onPointerDown);
1742
+ window.addEventListener("scroll", onDismiss, true);
1743
+ window.addEventListener("resize", onDismiss);
1744
+ return () => {
1745
+ document.removeEventListener("pointerdown", onPointerDown);
1746
+ window.removeEventListener("scroll", onDismiss, true);
1747
+ window.removeEventListener("resize", onDismiss);
1748
+ };
1728
1749
  }, [open]);
1729
1750
  if (isEditing) {
1730
1751
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-end gap-1", children: [
@@ -1755,6 +1776,7 @@ function DataTableRowActions({
1755
1776
  /* @__PURE__ */ jsxRuntime.jsx(
1756
1777
  Button,
1757
1778
  {
1779
+ ref: triggerRef,
1758
1780
  variant: "ghost",
1759
1781
  size: "icon-xs",
1760
1782
  "aria-label": "Open row actions",
@@ -1763,41 +1785,52 @@ function DataTableRowActions({
1763
1785
  children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MoreHorizontalIcon, {})
1764
1786
  }
1765
1787
  ),
1766
- open && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute top-7 right-0 z-50 w-40 rounded-md border bg-popover p-1 text-popover-foreground shadow-md", children: [
1767
- actions.map((a) => /* @__PURE__ */ jsxRuntime.jsxs(
1768
- RowActionButton,
1788
+ open && reactDom.createPortal(
1789
+ /* @__PURE__ */ jsxRuntime.jsxs(
1790
+ "div",
1769
1791
  {
1770
- danger: a === "delete",
1771
- onClick: () => {
1772
- setOpen(false);
1773
- onAction?.(a, row);
1774
- },
1792
+ ref: menuRef,
1793
+ className: "fixed z-[1000] w-40 rounded-md border bg-popover p-1 text-popover-foreground shadow-lg",
1794
+ style: { top: menuPosition.top, left: menuPosition.left },
1775
1795
  children: [
1776
- builtinIcons[a],
1777
- " ",
1778
- builtinLabels[a]
1779
- ]
1780
- },
1781
- a
1782
- )),
1783
- customActions.length > 0 && actions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "-mx-1 my-1 h-px bg-border" }),
1784
- customActions.filter((c) => c.show ? c.show(row) : true).map((c) => /* @__PURE__ */ jsxRuntime.jsxs(
1785
- RowActionButton,
1786
- {
1787
- danger: c.danger,
1788
- onClick: () => {
1789
- setOpen(false);
1790
- onAction?.(c.id, row);
1791
- },
1792
- children: [
1793
- c.icon,
1794
- " ",
1795
- c.label
1796
+ actions.map((a) => /* @__PURE__ */ jsxRuntime.jsxs(
1797
+ RowActionButton,
1798
+ {
1799
+ danger: a === "delete",
1800
+ onClick: () => {
1801
+ setOpen(false);
1802
+ onAction?.(a, row);
1803
+ },
1804
+ children: [
1805
+ builtinIcons[a],
1806
+ " ",
1807
+ builtinLabels[a]
1808
+ ]
1809
+ },
1810
+ a
1811
+ )),
1812
+ customActions.length > 0 && actions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "-mx-1 my-1 h-px bg-border" }),
1813
+ customActions.filter((c) => c.show ? c.show(row) : true).map((c) => /* @__PURE__ */ jsxRuntime.jsxs(
1814
+ RowActionButton,
1815
+ {
1816
+ danger: c.danger,
1817
+ onClick: () => {
1818
+ setOpen(false);
1819
+ onAction?.(c.id, row);
1820
+ },
1821
+ children: [
1822
+ c.icon,
1823
+ " ",
1824
+ c.label
1825
+ ]
1826
+ },
1827
+ c.id
1828
+ ))
1796
1829
  ]
1797
- },
1798
- c.id
1799
- ))
1800
- ] })
1830
+ }
1831
+ ),
1832
+ document.body
1833
+ )
1801
1834
  ] });
1802
1835
  }
1803
1836
  function RowActionButton({
@@ -2166,6 +2199,8 @@ var DEFAULT_LABELS = {
2166
2199
  total: "Total",
2167
2200
  noData: "No data.",
2168
2201
  noResults: "No rows match the current filters.",
2202
+ loadError: "Could not load rows.",
2203
+ retry: "Retry",
2169
2204
  refreshing: "Refreshing",
2170
2205
  rowsPerPage: "Rows per page",
2171
2206
  page: "Page",
@@ -2179,6 +2214,7 @@ var DENSITY_CLASS = {
2179
2214
  function DataTable({
2180
2215
  data,
2181
2216
  columns,
2217
+ dataSource,
2182
2218
  isLoading,
2183
2219
  isFetching,
2184
2220
  onRefresh,
@@ -2210,7 +2246,7 @@ function DataTable({
2210
2246
  const labels = { ...DEFAULT_LABELS, ...labelsProp };
2211
2247
  const f = {
2212
2248
  search: features?.search ?? true,
2213
- refresh: features?.refresh ?? !!onRefresh,
2249
+ refresh: features?.refresh ?? !!(onRefresh || dataSource),
2214
2250
  columnVisibility: features?.columnVisibility ?? true,
2215
2251
  export: features?.export ?? true,
2216
2252
  addRow: features?.addRow ?? !!onAddRow,
@@ -2254,14 +2290,80 @@ function DataTable({
2254
2290
  pageIndex: 0,
2255
2291
  pageSize: initialPageSize
2256
2292
  });
2293
+ const [sourceRows, setSourceRows] = react.useState(
2294
+ () => dataSource?.initialData ?? []
2295
+ );
2296
+ const [sourceTotalRecords, setSourceTotalRecords] = react.useState(void 0);
2297
+ const [sourceError, setSourceError] = react.useState(null);
2298
+ const [sourceLoading, setSourceLoading] = react.useState(false);
2299
+ const [sourceFetching, setSourceFetching] = react.useState(false);
2300
+ const [sourceRefreshKey, setSourceRefreshKey] = react.useState(0);
2257
2301
  const [editingCell, setEditingCell] = react.useState(null);
2258
2302
  const [editingRowId, setEditingRowId] = react.useState(null);
2259
2303
  const [rowDraft, setRowDraft] = react.useState({});
2304
+ const dataSourceEnabled = !!dataSource && dataSource.enabled !== false;
2305
+ const dataSourceMode = dataSource?.mode ?? "client";
2306
+ const isServerSideDataSource = dataSourceEnabled && dataSourceMode === "server";
2307
+ const dataSourceDeps = dataSource?.deps ?? [];
2308
+ react.useEffect(() => {
2309
+ if (!dataSourceEnabled || !dataSource) return;
2310
+ let cancelled = false;
2311
+ const isInitialLoad = sourceRows.length === 0;
2312
+ setSourceError(null);
2313
+ setSourceLoading(isInitialLoad);
2314
+ setSourceFetching(!isInitialLoad);
2315
+ dataSource.fetchRows({
2316
+ pageIndex: pagination.pageIndex,
2317
+ pageSize: pagination.pageSize,
2318
+ sorting,
2319
+ columnFilters,
2320
+ globalFilter
2321
+ }).then((result) => {
2322
+ if (cancelled) return;
2323
+ const rows2 = Array.isArray(result) ? result : result.rows;
2324
+ setSourceRows(rows2);
2325
+ setSourceTotalRecords(
2326
+ Array.isArray(result) ? rows2.length : result.totalRecords ?? rows2.length
2327
+ );
2328
+ }).catch((error) => {
2329
+ if (cancelled) return;
2330
+ setSourceError(error);
2331
+ dataSource.onError?.(error, {
2332
+ type: isInitialLoad ? "load" : "refresh",
2333
+ message: isInitialLoad ? labels.loadError : labels.refreshing
2334
+ });
2335
+ }).finally(() => {
2336
+ if (cancelled) return;
2337
+ setSourceLoading(false);
2338
+ setSourceFetching(false);
2339
+ });
2340
+ return () => {
2341
+ cancelled = true;
2342
+ };
2343
+ }, [
2344
+ dataSourceEnabled,
2345
+ dataSource,
2346
+ sourceRefreshKey,
2347
+ pagination.pageIndex,
2348
+ pagination.pageSize,
2349
+ sorting,
2350
+ columnFilters,
2351
+ globalFilter,
2352
+ labels.loadError,
2353
+ labels.refreshing,
2354
+ // eslint-disable-next-line react-hooks/exhaustive-deps
2355
+ ...dataSourceDeps
2356
+ ]);
2357
+ const baseData = dataSourceEnabled ? sourceRows : data ?? [];
2358
+ const effectiveIsLoading = dataSourceEnabled ? sourceLoading : isLoading;
2359
+ const effectiveIsFetching = dataSourceEnabled ? sourceFetching : isFetching;
2360
+ const effectiveOnRefresh = dataSourceEnabled ? () => setSourceRefreshKey((key) => key + 1) : onRefresh;
2260
2361
  const [extraRows, setExtraRows] = react.useState([]);
2261
2362
  const mergedData = react.useMemo(
2262
- () => extraRows.length ? [...extraRows, ...data] : data,
2263
- [extraRows, data]
2363
+ () => extraRows.length ? [...extraRows, ...baseData] : baseData,
2364
+ [extraRows, baseData]
2264
2365
  );
2366
+ const effectiveTotalRecords = totalRecords ?? (dataSourceEnabled ? sourceTotalRecords ?? mergedData.length : mergedData.length);
2265
2367
  const augmentedColumns = react.useMemo(() => {
2266
2368
  const wrap = (col) => {
2267
2369
  const meta = col.meta;
@@ -2411,6 +2513,10 @@ function DataTable({
2411
2513
  enableRowSelection: true,
2412
2514
  enableExpanding: !!(renderSubRow || getSubRows),
2413
2515
  enablePinning: f.pinning,
2516
+ manualPagination: isServerSideDataSource,
2517
+ manualSorting: isServerSideDataSource,
2518
+ manualFiltering: isServerSideDataSource,
2519
+ pageCount: isServerSideDataSource ? Math.max(1, Math.ceil(effectiveTotalRecords / pagination.pageSize)) : void 0,
2414
2520
  getRowId: (row) => String(row.id),
2415
2521
  getSubRows,
2416
2522
  getRowCanExpand: (row) => {
@@ -2435,27 +2541,56 @@ function DataTable({
2435
2541
  getExpandedRowModel: reactTable.getExpandedRowModel(),
2436
2542
  getPaginationRowModel: reactTable.getPaginationRowModel()
2437
2543
  });
2438
- const dragState = react.useRef({ src: null });
2439
- const onHeaderDragStart = (id, e) => {
2440
- dragState.current.src = id;
2441
- e.dataTransfer.effectAllowed = "move";
2442
- e.dataTransfer.setData("text/plain", id);
2544
+ const reorderState = react.useRef(null);
2545
+ const [activeReorderId, setActiveReorderId] = react.useState(null);
2546
+ const [dragOverColId, setDragOverColId] = react.useState(null);
2547
+ const isInteractiveTarget = (target) => {
2548
+ const el = target;
2549
+ if (!el || !el.closest) return false;
2550
+ return !!el.closest(
2551
+ 'button, a, input, select, textarea, [role="menuitem"], [data-no-drag]'
2552
+ );
2443
2553
  };
2444
- const onHeaderDragOver = (e) => {
2445
- e.preventDefault();
2446
- e.dataTransfer.dropEffect = "move";
2554
+ const onHeaderPointerDown = (id) => (e) => {
2555
+ if (e.button !== 0) return;
2556
+ if (isInteractiveTarget(e.target)) return;
2557
+ reorderState.current = { x: e.clientX, y: e.clientY, id };
2447
2558
  };
2448
- const onHeaderDrop = (targetId) => {
2449
- const src = dragState.current.src;
2450
- dragState.current.src = null;
2451
- if (!src || src === targetId) return;
2559
+ const onHeaderPointerMove = (e) => {
2560
+ const start = reorderState.current;
2561
+ if (!start) return;
2562
+ if (!activeReorderId) {
2563
+ const dx = e.clientX - start.x;
2564
+ const dy = e.clientY - start.y;
2565
+ if (Math.hypot(dx, dy) < 6) return;
2566
+ setActiveReorderId(start.id);
2567
+ e.currentTarget.setPointerCapture(e.pointerId);
2568
+ }
2569
+ const el = document.elementFromPoint(e.clientX, e.clientY);
2570
+ const th = el ? el.closest("th[data-col-id]") : null;
2571
+ const overId = th?.dataset.colId ?? null;
2572
+ const next = overId && overId !== start.id ? overId : null;
2573
+ if (next !== dragOverColId) setDragOverColId(next);
2574
+ };
2575
+ const onHeaderPointerUp = (e) => {
2576
+ const start = reorderState.current;
2577
+ reorderState.current = null;
2578
+ if (!activeReorderId) return;
2579
+ try {
2580
+ e.currentTarget.releasePointerCapture(e.pointerId);
2581
+ } catch {
2582
+ }
2583
+ const target = dragOverColId;
2584
+ setActiveReorderId(null);
2585
+ setDragOverColId(null);
2586
+ if (!start || !target || target === start.id) return;
2452
2587
  setColumnOrder((order) => {
2453
2588
  const next = [...order];
2454
- const from = next.indexOf(src);
2455
- const to = next.indexOf(targetId);
2589
+ const from = next.indexOf(start.id);
2590
+ const to = next.indexOf(target);
2456
2591
  if (from === -1 || to === -1) return order;
2457
2592
  next.splice(from, 1);
2458
- next.splice(to, 0, src);
2593
+ next.splice(to, 0, start.id);
2459
2594
  return next;
2460
2595
  });
2461
2596
  };
@@ -2512,9 +2647,9 @@ function DataTable({
2512
2647
  DataTableToolbar,
2513
2648
  {
2514
2649
  table,
2515
- totalRecords: totalRecords ?? mergedData.length,
2516
- isFetching,
2517
- onRefresh: f.refresh ? onRefresh : void 0,
2650
+ totalRecords: effectiveTotalRecords,
2651
+ isFetching: effectiveIsFetching,
2652
+ onRefresh: f.refresh ? effectiveOnRefresh : void 0,
2518
2653
  onAddRow: f.addRow ? handleAddRow : void 0,
2519
2654
  onBulkDelete,
2520
2655
  exportFileName,
@@ -2542,32 +2677,38 @@ function DataTable({
2542
2677
  const meta = header.column.columnDef.meta;
2543
2678
  const isUtility = header.column.id === "__select" || header.column.id === "__expand" || header.column.id === "__actions";
2544
2679
  const isPinned = !!header.column.getIsPinned();
2680
+ const canReorder = !isUtility && f.reordering;
2681
+ const isDropTarget = canReorder && dragOverColId === header.column.id;
2682
+ const isBeingDragged = canReorder && activeReorderId === header.column.id;
2545
2683
  return /* @__PURE__ */ jsxRuntime.jsxs(
2546
2684
  "th",
2547
2685
  {
2548
2686
  colSpan: header.colSpan,
2687
+ "data-col-id": header.column.id,
2688
+ onPointerDown: canReorder ? onHeaderPointerDown(header.column.id) : void 0,
2689
+ onPointerMove: canReorder ? onHeaderPointerMove : void 0,
2690
+ onPointerUp: canReorder ? onHeaderPointerUp : void 0,
2691
+ onPointerCancel: canReorder ? onHeaderPointerUp : void 0,
2549
2692
  style: {
2550
2693
  width: header.getSize(),
2551
2694
  ...getPinningStyles(header.column),
2552
2695
  ...isPinned ? { zIndex: 2 } : {}
2553
2696
  },
2554
2697
  className: cn(
2555
- "group/th relative h-9 px-2 text-left align-middle text-xs font-medium text-muted-foreground",
2698
+ "group/th relative h-9 px-2 text-left align-middle text-xs font-medium text-muted-foreground select-none",
2699
+ canReorder && "cursor-grab active:cursor-grabbing",
2556
2700
  isPinned && "bg-card",
2701
+ isBeingDragged && "opacity-50",
2702
+ isDropTarget && "bg-primary/10 ring-2 ring-inset ring-primary/40",
2557
2703
  meta?.headerClassName
2558
2704
  ),
2559
2705
  children: [
2560
2706
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
2561
- !isUtility && f.reordering && /* @__PURE__ */ jsxRuntime.jsx(
2562
- "span",
2707
+ canReorder && /* @__PURE__ */ jsxRuntime.jsx(
2708
+ lucideReact.GripVerticalIcon,
2563
2709
  {
2564
- draggable: true,
2565
- onDragStart: (e) => onHeaderDragStart(header.column.id, e),
2566
- onDragOver: onHeaderDragOver,
2567
- onDrop: () => onHeaderDrop(header.column.id),
2568
- className: "-ml-1 cursor-grab rounded p-0.5 text-muted-foreground/40 opacity-0 transition group-hover/th:opacity-100 hover:bg-muted hover:text-foreground active:cursor-grabbing",
2569
- title: "Drag to reorder",
2570
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.GripVerticalIcon, { className: "size-3" })
2710
+ className: "-ml-1 size-3 shrink-0 text-muted-foreground/40 opacity-0 transition group-hover/th:opacity-100",
2711
+ "aria-hidden": "true"
2571
2712
  }
2572
2713
  ),
2573
2714
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-w-0 flex-1", children: header.isPlaceholder ? null : reactTable.flexRender(header.column.columnDef.header, header.getContext()) })
@@ -2575,6 +2716,8 @@ function DataTable({
2575
2716
  header.column.getCanResize() && /* @__PURE__ */ jsxRuntime.jsx(
2576
2717
  "span",
2577
2718
  {
2719
+ "data-no-drag": true,
2720
+ onPointerDown: (e) => e.stopPropagation(),
2578
2721
  onMouseDown: header.getResizeHandler(),
2579
2722
  onTouchStart: header.getResizeHandler(),
2580
2723
  onDoubleClick: () => header.column.resetSize(),
@@ -2589,7 +2732,25 @@ function DataTable({
2589
2732
  header.id
2590
2733
  );
2591
2734
  }) }, hg.id)) }),
2592
- /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: isLoading ? renderSkeleton(visibleLeafCount, pagination.pageSize) : rows.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsx(
2735
+ /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: effectiveIsLoading ? renderSkeleton(visibleLeafCount, pagination.pageSize) : sourceError && rows.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsx(
2736
+ "td",
2737
+ {
2738
+ colSpan: visibleLeafCount,
2739
+ className: "h-32 text-center text-sm text-muted-foreground",
2740
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-2", children: [
2741
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: labels.loadError }),
2742
+ effectiveOnRefresh && /* @__PURE__ */ jsxRuntime.jsx(
2743
+ "button",
2744
+ {
2745
+ type: "button",
2746
+ onClick: effectiveOnRefresh,
2747
+ className: "rounded-md border px-2 py-1 text-xs text-foreground hover:bg-muted",
2748
+ children: labels.retry
2749
+ }
2750
+ )
2751
+ ] })
2752
+ }
2753
+ ) }) : rows.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsx(
2593
2754
  "td",
2594
2755
  {
2595
2756
  colSpan: visibleLeafCount,
@@ -2608,7 +2769,7 @@ function DataTable({
2608
2769
  ]
2609
2770
  }
2610
2771
  ),
2611
- isFetching && !isLoading && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pointer-events-none absolute top-1.5 right-2 flex items-center gap-1.5 rounded-md bg-background/80 px-2 py-1 text-xs text-muted-foreground shadow-sm backdrop-blur", children: [
2772
+ effectiveIsFetching && !effectiveIsLoading && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pointer-events-none absolute top-1.5 right-2 flex items-center gap-1.5 rounded-md bg-background/80 px-2 py-1 text-xs text-muted-foreground shadow-sm backdrop-blur", children: [
2612
2773
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2Icon, { className: "size-3 animate-spin" }),
2613
2774
  " ",
2614
2775
  labels.refreshing