@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/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { RowData, ColumnDef, ColumnPinningState, VisibilityState, Column, Table, CellContext, FilterFn } from '@tanstack/react-table';
2
+ import { RowData, ColumnDef, SortingState, ColumnFiltersState, ColumnPinningState, VisibilityState, Column, Table, CellContext, FilterFn } from '@tanstack/react-table';
3
3
  import { CSSProperties } from 'react';
4
4
 
5
5
  type RowActionId = "view" | "edit" | "duplicate" | "delete" | string;
@@ -168,17 +168,54 @@ type DataTableLabels = {
168
168
  total?: string;
169
169
  noData?: string;
170
170
  noResults?: string;
171
+ loadError?: string;
172
+ retry?: string;
171
173
  refreshing?: string;
172
174
  rowsPerPage?: string;
173
175
  page?: string;
174
176
  of?: string;
175
177
  };
176
178
  type DataTableDensity = "compact" | "default" | "comfortable";
179
+ type DataTableFetchParams = {
180
+ pageIndex: number;
181
+ pageSize: number;
182
+ sorting: SortingState;
183
+ columnFilters: ColumnFiltersState;
184
+ globalFilter: string;
185
+ };
186
+ type DataTableDataSourceResult<TData> = TData[] | {
187
+ rows: TData[];
188
+ totalRecords?: number;
189
+ };
190
+ type DataTableErrorContext = {
191
+ type: "load" | "refresh";
192
+ message: string;
193
+ };
194
+ type DataTableDataSource<TData> = {
195
+ /**
196
+ * Fetch rows internally. In "client" mode, return the full data set and the
197
+ * table applies client-side sort/filter/pagination. In "server" mode, return
198
+ * the current page and optionally `totalRecords`.
199
+ */
200
+ fetchRows: (params: DataTableFetchParams) => Promise<DataTableDataSourceResult<TData>>;
201
+ /** "client" by default. Use "server" for API-side pagination/filter/sort. */
202
+ mode?: "client" | "server";
203
+ /** Set false to pause fetching. */
204
+ enabled?: boolean;
205
+ /** Optional rows to show before the first fetch resolves. */
206
+ initialData?: TData[];
207
+ /** Refetch when these external values change. Memoize this array. */
208
+ deps?: readonly unknown[];
209
+ /** Called for load/refresh errors so apps can show a toast. */
210
+ onError?: (error: unknown, context: DataTableErrorContext) => void;
211
+ };
177
212
  type DataTableProps<TData extends {
178
213
  id: string | number;
179
214
  }> = {
180
- data: TData[];
215
+ data?: TData[];
181
216
  columns: ColumnDef<TData>[];
217
+ /** Optional internal fetcher. Omit this when using controlled `data`. */
218
+ dataSource?: DataTableDataSource<TData>;
182
219
  isLoading?: boolean;
183
220
  isFetching?: boolean;
184
221
  onRefresh?: () => void;
@@ -221,7 +258,7 @@ type DataTableProps<TData extends {
221
258
  };
222
259
  declare function DataTable<TData extends {
223
260
  id: string | number;
224
- }>({ data, columns, isLoading, isFetching, onRefresh, totalRecords, exportFileName, enableSelection, renderSubRow, getSubRows, rowActions, customRowActions, onRowAction, onCellEdit, onRowSave, onAddRow, onBulkDelete, initialPageSize, pageSizeOptions, initialColumnPinning, initialColumnVisibility, globalFilter: globalFilterProp, onGlobalFilterChange, className, toolbarSlot, features, labels: labelsProp, density, theme, }: DataTableProps<TData>): react_jsx_runtime.JSX.Element;
261
+ }>({ data, columns, dataSource, isLoading, isFetching, onRefresh, totalRecords, exportFileName, enableSelection, renderSubRow, getSubRows, rowActions, customRowActions, onRowAction, onCellEdit, onRowSave, onAddRow, onBulkDelete, initialPageSize, pageSizeOptions, initialColumnPinning, initialColumnVisibility, globalFilter: globalFilterProp, onGlobalFilterChange, className, toolbarSlot, features, labels: labelsProp, density, theme, }: DataTableProps<TData>): react_jsx_runtime.JSX.Element;
225
262
 
226
263
  type Props$2<TData, TValue> = {
227
264
  column: Column<TData, TValue>;
@@ -313,4 +350,4 @@ declare function ColumnFilterPanel<TData, TValue>(props: CommonProps<TData, TVal
313
350
  declare function exportToCsv<TData>(table: Table<TData>, fileName?: string): void;
314
351
  declare function exportToExcel<TData>(table: Table<TData>, fileName?: string): void;
315
352
 
316
- export { type AdvFilter, type CellEditorType, ColumnFilterPanel, type Combine, type Condition, type CustomRowAction, DataTable, type DataTableColumn, DataTableColumnHeader, type DataTableDensity, type DataTableFeatures, type DataTableLabels, DataTablePagination, type DataTableProps, DataTableRowActions, type DataTableRowActionsProps, type DataTableTheme, type DataTableThemeName, DataTableToolbar, type DateOp, EditableCell, type FilterType, type NumberOp, type RowAction, type RowActionId, type SelectOption, type SetFilter, type TextOp, booleanFilterFn, dateFilterFn, exportToCsv, exportToExcel, isFilterActive, numberFilterFn, setFilterFn, textFilterFn, themePresets, themeToStyle };
353
+ export { type AdvFilter, type CellEditorType, ColumnFilterPanel, type Combine, type Condition, type CustomRowAction, DataTable, type DataTableColumn, DataTableColumnHeader, type DataTableDataSource, type DataTableDataSourceResult, type DataTableDensity, type DataTableErrorContext, type DataTableFeatures, type DataTableFetchParams, type DataTableLabels, DataTablePagination, type DataTableProps, DataTableRowActions, type DataTableRowActionsProps, type DataTableTheme, type DataTableThemeName, DataTableToolbar, type DateOp, EditableCell, type FilterType, type NumberOp, type RowAction, type RowActionId, type SelectOption, type SetFilter, type TextOp, booleanFilterFn, dateFilterFn, exportToCsv, exportToExcel, isFilterActive, numberFilterFn, setFilterFn, textFilterFn, themePresets, themeToStyle };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { RowData, ColumnDef, ColumnPinningState, VisibilityState, Column, Table, CellContext, FilterFn } from '@tanstack/react-table';
2
+ import { RowData, ColumnDef, SortingState, ColumnFiltersState, ColumnPinningState, VisibilityState, Column, Table, CellContext, FilterFn } from '@tanstack/react-table';
3
3
  import { CSSProperties } from 'react';
4
4
 
5
5
  type RowActionId = "view" | "edit" | "duplicate" | "delete" | string;
@@ -168,17 +168,54 @@ type DataTableLabels = {
168
168
  total?: string;
169
169
  noData?: string;
170
170
  noResults?: string;
171
+ loadError?: string;
172
+ retry?: string;
171
173
  refreshing?: string;
172
174
  rowsPerPage?: string;
173
175
  page?: string;
174
176
  of?: string;
175
177
  };
176
178
  type DataTableDensity = "compact" | "default" | "comfortable";
179
+ type DataTableFetchParams = {
180
+ pageIndex: number;
181
+ pageSize: number;
182
+ sorting: SortingState;
183
+ columnFilters: ColumnFiltersState;
184
+ globalFilter: string;
185
+ };
186
+ type DataTableDataSourceResult<TData> = TData[] | {
187
+ rows: TData[];
188
+ totalRecords?: number;
189
+ };
190
+ type DataTableErrorContext = {
191
+ type: "load" | "refresh";
192
+ message: string;
193
+ };
194
+ type DataTableDataSource<TData> = {
195
+ /**
196
+ * Fetch rows internally. In "client" mode, return the full data set and the
197
+ * table applies client-side sort/filter/pagination. In "server" mode, return
198
+ * the current page and optionally `totalRecords`.
199
+ */
200
+ fetchRows: (params: DataTableFetchParams) => Promise<DataTableDataSourceResult<TData>>;
201
+ /** "client" by default. Use "server" for API-side pagination/filter/sort. */
202
+ mode?: "client" | "server";
203
+ /** Set false to pause fetching. */
204
+ enabled?: boolean;
205
+ /** Optional rows to show before the first fetch resolves. */
206
+ initialData?: TData[];
207
+ /** Refetch when these external values change. Memoize this array. */
208
+ deps?: readonly unknown[];
209
+ /** Called for load/refresh errors so apps can show a toast. */
210
+ onError?: (error: unknown, context: DataTableErrorContext) => void;
211
+ };
177
212
  type DataTableProps<TData extends {
178
213
  id: string | number;
179
214
  }> = {
180
- data: TData[];
215
+ data?: TData[];
181
216
  columns: ColumnDef<TData>[];
217
+ /** Optional internal fetcher. Omit this when using controlled `data`. */
218
+ dataSource?: DataTableDataSource<TData>;
182
219
  isLoading?: boolean;
183
220
  isFetching?: boolean;
184
221
  onRefresh?: () => void;
@@ -221,7 +258,7 @@ type DataTableProps<TData extends {
221
258
  };
222
259
  declare function DataTable<TData extends {
223
260
  id: string | number;
224
- }>({ data, columns, isLoading, isFetching, onRefresh, totalRecords, exportFileName, enableSelection, renderSubRow, getSubRows, rowActions, customRowActions, onRowAction, onCellEdit, onRowSave, onAddRow, onBulkDelete, initialPageSize, pageSizeOptions, initialColumnPinning, initialColumnVisibility, globalFilter: globalFilterProp, onGlobalFilterChange, className, toolbarSlot, features, labels: labelsProp, density, theme, }: DataTableProps<TData>): react_jsx_runtime.JSX.Element;
261
+ }>({ data, columns, dataSource, isLoading, isFetching, onRefresh, totalRecords, exportFileName, enableSelection, renderSubRow, getSubRows, rowActions, customRowActions, onRowAction, onCellEdit, onRowSave, onAddRow, onBulkDelete, initialPageSize, pageSizeOptions, initialColumnPinning, initialColumnVisibility, globalFilter: globalFilterProp, onGlobalFilterChange, className, toolbarSlot, features, labels: labelsProp, density, theme, }: DataTableProps<TData>): react_jsx_runtime.JSX.Element;
225
262
 
226
263
  type Props$2<TData, TValue> = {
227
264
  column: Column<TData, TValue>;
@@ -313,4 +350,4 @@ declare function ColumnFilterPanel<TData, TValue>(props: CommonProps<TData, TVal
313
350
  declare function exportToCsv<TData>(table: Table<TData>, fileName?: string): void;
314
351
  declare function exportToExcel<TData>(table: Table<TData>, fileName?: string): void;
315
352
 
316
- export { type AdvFilter, type CellEditorType, ColumnFilterPanel, type Combine, type Condition, type CustomRowAction, DataTable, type DataTableColumn, DataTableColumnHeader, type DataTableDensity, type DataTableFeatures, type DataTableLabels, DataTablePagination, type DataTableProps, DataTableRowActions, type DataTableRowActionsProps, type DataTableTheme, type DataTableThemeName, DataTableToolbar, type DateOp, EditableCell, type FilterType, type NumberOp, type RowAction, type RowActionId, type SelectOption, type SetFilter, type TextOp, booleanFilterFn, dateFilterFn, exportToCsv, exportToExcel, isFilterActive, numberFilterFn, setFilterFn, textFilterFn, themePresets, themeToStyle };
353
+ export { type AdvFilter, type CellEditorType, ColumnFilterPanel, type Combine, type Condition, type CustomRowAction, DataTable, type DataTableColumn, DataTableColumnHeader, type DataTableDataSource, type DataTableDataSourceResult, type DataTableDensity, type DataTableErrorContext, type DataTableFeatures, type DataTableFetchParams, type DataTableLabels, DataTablePagination, type DataTableProps, DataTableRowActions, type DataTableRowActionsProps, type DataTableTheme, type DataTableThemeName, DataTableToolbar, type DateOp, EditableCell, type FilterType, type NumberOp, type RowAction, type RowActionId, type SelectOption, type SetFilter, type TextOp, booleanFilterFn, dateFilterFn, exportToCsv, exportToExcel, isFilterActive, numberFilterFn, setFilterFn, textFilterFn, themePresets, themeToStyle };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { createContext, useState, useRef, useEffect, useMemo, useContext } from 'react';
1
+ import { createContext, useState, useRef, useEffect, useMemo, useLayoutEffect, useContext } from 'react';
2
2
  import { useReactTable, getPaginationRowModel, getExpandedRowModel, getSortedRowModel, getFilteredRowModel, getCoreRowModel, flexRender } from '@tanstack/react-table';
3
3
  import { Trash2Icon, XIcon, SearchIcon, RefreshCwIcon, ColumnsIcon, DownloadIcon, FileTextIcon, FileSpreadsheetIcon, PlusIcon, ChevronsLeftIcon, ChevronLeftIcon, ChevronRightIcon, ChevronsRightIcon, ArrowDownIcon, ArrowUpIcon, ChevronsUpDownIcon, PinOffIcon, PinIcon, EyeOffIcon, FilterIcon, CheckIcon, MoreHorizontalIcon, ChevronDownIcon, GripVerticalIcon, Loader2Icon, MinusIcon, CopyIcon, PencilIcon, EyeIcon, ChevronUpIcon } from 'lucide-react';
4
4
  import { clsx } from 'clsx';
@@ -6,6 +6,7 @@ import { twMerge } from 'tailwind-merge';
6
6
  import { Slot, Separator as Separator$1, Popover as Popover$1, Checkbox as Checkbox$1, DropdownMenu as DropdownMenu$1, Select as Select$1 } from 'radix-ui';
7
7
  import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
8
8
  import { cva } from 'class-variance-authority';
9
+ import { createPortal } from 'react-dom';
9
10
 
10
11
  function cn(...inputs) {
11
12
  return twMerge(clsx(inputs));
@@ -1715,14 +1716,34 @@ function DataTableRowActions({
1715
1716
  onCancelEdit
1716
1717
  }) {
1717
1718
  const [open, setOpen] = useState(false);
1719
+ const [menuPosition, setMenuPosition] = useState({ top: 0, left: 0 });
1720
+ const triggerRef = useRef(null);
1718
1721
  const menuRef = useRef(null);
1722
+ useLayoutEffect(() => {
1723
+ if (!open || !triggerRef.current) return;
1724
+ const rect = triggerRef.current.getBoundingClientRect();
1725
+ setMenuPosition({
1726
+ top: rect.bottom + 4,
1727
+ left: Math.max(8, rect.right - 160)
1728
+ });
1729
+ }, [open]);
1719
1730
  useEffect(() => {
1720
1731
  if (!open) return;
1721
1732
  const onPointerDown = (event) => {
1722
- if (!menuRef.current?.contains(event.target)) setOpen(false);
1733
+ const target = event.target;
1734
+ if (!menuRef.current?.contains(target) && !triggerRef.current?.contains(target)) {
1735
+ setOpen(false);
1736
+ }
1723
1737
  };
1738
+ const onDismiss = () => setOpen(false);
1724
1739
  document.addEventListener("pointerdown", onPointerDown);
1725
- return () => document.removeEventListener("pointerdown", onPointerDown);
1740
+ window.addEventListener("scroll", onDismiss, true);
1741
+ window.addEventListener("resize", onDismiss);
1742
+ return () => {
1743
+ document.removeEventListener("pointerdown", onPointerDown);
1744
+ window.removeEventListener("scroll", onDismiss, true);
1745
+ window.removeEventListener("resize", onDismiss);
1746
+ };
1726
1747
  }, [open]);
1727
1748
  if (isEditing) {
1728
1749
  return /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-end gap-1", children: [
@@ -1753,6 +1774,7 @@ function DataTableRowActions({
1753
1774
  /* @__PURE__ */ jsx(
1754
1775
  Button,
1755
1776
  {
1777
+ ref: triggerRef,
1756
1778
  variant: "ghost",
1757
1779
  size: "icon-xs",
1758
1780
  "aria-label": "Open row actions",
@@ -1761,41 +1783,52 @@ function DataTableRowActions({
1761
1783
  children: /* @__PURE__ */ jsx(MoreHorizontalIcon, {})
1762
1784
  }
1763
1785
  ),
1764
- open && /* @__PURE__ */ 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: [
1765
- actions.map((a) => /* @__PURE__ */ jsxs(
1766
- RowActionButton,
1786
+ open && createPortal(
1787
+ /* @__PURE__ */ jsxs(
1788
+ "div",
1767
1789
  {
1768
- danger: a === "delete",
1769
- onClick: () => {
1770
- setOpen(false);
1771
- onAction?.(a, row);
1772
- },
1790
+ ref: menuRef,
1791
+ className: "fixed z-[1000] w-40 rounded-md border bg-popover p-1 text-popover-foreground shadow-lg",
1792
+ style: { top: menuPosition.top, left: menuPosition.left },
1773
1793
  children: [
1774
- builtinIcons[a],
1775
- " ",
1776
- builtinLabels[a]
1777
- ]
1778
- },
1779
- a
1780
- )),
1781
- customActions.length > 0 && actions.length > 0 && /* @__PURE__ */ jsx("div", { className: "-mx-1 my-1 h-px bg-border" }),
1782
- customActions.filter((c) => c.show ? c.show(row) : true).map((c) => /* @__PURE__ */ jsxs(
1783
- RowActionButton,
1784
- {
1785
- danger: c.danger,
1786
- onClick: () => {
1787
- setOpen(false);
1788
- onAction?.(c.id, row);
1789
- },
1790
- children: [
1791
- c.icon,
1792
- " ",
1793
- c.label
1794
+ actions.map((a) => /* @__PURE__ */ jsxs(
1795
+ RowActionButton,
1796
+ {
1797
+ danger: a === "delete",
1798
+ onClick: () => {
1799
+ setOpen(false);
1800
+ onAction?.(a, row);
1801
+ },
1802
+ children: [
1803
+ builtinIcons[a],
1804
+ " ",
1805
+ builtinLabels[a]
1806
+ ]
1807
+ },
1808
+ a
1809
+ )),
1810
+ customActions.length > 0 && actions.length > 0 && /* @__PURE__ */ jsx("div", { className: "-mx-1 my-1 h-px bg-border" }),
1811
+ customActions.filter((c) => c.show ? c.show(row) : true).map((c) => /* @__PURE__ */ jsxs(
1812
+ RowActionButton,
1813
+ {
1814
+ danger: c.danger,
1815
+ onClick: () => {
1816
+ setOpen(false);
1817
+ onAction?.(c.id, row);
1818
+ },
1819
+ children: [
1820
+ c.icon,
1821
+ " ",
1822
+ c.label
1823
+ ]
1824
+ },
1825
+ c.id
1826
+ ))
1794
1827
  ]
1795
- },
1796
- c.id
1797
- ))
1798
- ] })
1828
+ }
1829
+ ),
1830
+ document.body
1831
+ )
1799
1832
  ] });
1800
1833
  }
1801
1834
  function RowActionButton({
@@ -2164,6 +2197,8 @@ var DEFAULT_LABELS = {
2164
2197
  total: "Total",
2165
2198
  noData: "No data.",
2166
2199
  noResults: "No rows match the current filters.",
2200
+ loadError: "Could not load rows.",
2201
+ retry: "Retry",
2167
2202
  refreshing: "Refreshing",
2168
2203
  rowsPerPage: "Rows per page",
2169
2204
  page: "Page",
@@ -2177,6 +2212,7 @@ var DENSITY_CLASS = {
2177
2212
  function DataTable({
2178
2213
  data,
2179
2214
  columns,
2215
+ dataSource,
2180
2216
  isLoading,
2181
2217
  isFetching,
2182
2218
  onRefresh,
@@ -2208,7 +2244,7 @@ function DataTable({
2208
2244
  const labels = { ...DEFAULT_LABELS, ...labelsProp };
2209
2245
  const f = {
2210
2246
  search: features?.search ?? true,
2211
- refresh: features?.refresh ?? !!onRefresh,
2247
+ refresh: features?.refresh ?? !!(onRefresh || dataSource),
2212
2248
  columnVisibility: features?.columnVisibility ?? true,
2213
2249
  export: features?.export ?? true,
2214
2250
  addRow: features?.addRow ?? !!onAddRow,
@@ -2252,14 +2288,80 @@ function DataTable({
2252
2288
  pageIndex: 0,
2253
2289
  pageSize: initialPageSize
2254
2290
  });
2291
+ const [sourceRows, setSourceRows] = useState(
2292
+ () => dataSource?.initialData ?? []
2293
+ );
2294
+ const [sourceTotalRecords, setSourceTotalRecords] = useState(void 0);
2295
+ const [sourceError, setSourceError] = useState(null);
2296
+ const [sourceLoading, setSourceLoading] = useState(false);
2297
+ const [sourceFetching, setSourceFetching] = useState(false);
2298
+ const [sourceRefreshKey, setSourceRefreshKey] = useState(0);
2255
2299
  const [editingCell, setEditingCell] = useState(null);
2256
2300
  const [editingRowId, setEditingRowId] = useState(null);
2257
2301
  const [rowDraft, setRowDraft] = useState({});
2302
+ const dataSourceEnabled = !!dataSource && dataSource.enabled !== false;
2303
+ const dataSourceMode = dataSource?.mode ?? "client";
2304
+ const isServerSideDataSource = dataSourceEnabled && dataSourceMode === "server";
2305
+ const dataSourceDeps = dataSource?.deps ?? [];
2306
+ useEffect(() => {
2307
+ if (!dataSourceEnabled || !dataSource) return;
2308
+ let cancelled = false;
2309
+ const isInitialLoad = sourceRows.length === 0;
2310
+ setSourceError(null);
2311
+ setSourceLoading(isInitialLoad);
2312
+ setSourceFetching(!isInitialLoad);
2313
+ dataSource.fetchRows({
2314
+ pageIndex: pagination.pageIndex,
2315
+ pageSize: pagination.pageSize,
2316
+ sorting,
2317
+ columnFilters,
2318
+ globalFilter
2319
+ }).then((result) => {
2320
+ if (cancelled) return;
2321
+ const rows2 = Array.isArray(result) ? result : result.rows;
2322
+ setSourceRows(rows2);
2323
+ setSourceTotalRecords(
2324
+ Array.isArray(result) ? rows2.length : result.totalRecords ?? rows2.length
2325
+ );
2326
+ }).catch((error) => {
2327
+ if (cancelled) return;
2328
+ setSourceError(error);
2329
+ dataSource.onError?.(error, {
2330
+ type: isInitialLoad ? "load" : "refresh",
2331
+ message: isInitialLoad ? labels.loadError : labels.refreshing
2332
+ });
2333
+ }).finally(() => {
2334
+ if (cancelled) return;
2335
+ setSourceLoading(false);
2336
+ setSourceFetching(false);
2337
+ });
2338
+ return () => {
2339
+ cancelled = true;
2340
+ };
2341
+ }, [
2342
+ dataSourceEnabled,
2343
+ dataSource,
2344
+ sourceRefreshKey,
2345
+ pagination.pageIndex,
2346
+ pagination.pageSize,
2347
+ sorting,
2348
+ columnFilters,
2349
+ globalFilter,
2350
+ labels.loadError,
2351
+ labels.refreshing,
2352
+ // eslint-disable-next-line react-hooks/exhaustive-deps
2353
+ ...dataSourceDeps
2354
+ ]);
2355
+ const baseData = dataSourceEnabled ? sourceRows : data ?? [];
2356
+ const effectiveIsLoading = dataSourceEnabled ? sourceLoading : isLoading;
2357
+ const effectiveIsFetching = dataSourceEnabled ? sourceFetching : isFetching;
2358
+ const effectiveOnRefresh = dataSourceEnabled ? () => setSourceRefreshKey((key) => key + 1) : onRefresh;
2258
2359
  const [extraRows, setExtraRows] = useState([]);
2259
2360
  const mergedData = useMemo(
2260
- () => extraRows.length ? [...extraRows, ...data] : data,
2261
- [extraRows, data]
2361
+ () => extraRows.length ? [...extraRows, ...baseData] : baseData,
2362
+ [extraRows, baseData]
2262
2363
  );
2364
+ const effectiveTotalRecords = totalRecords ?? (dataSourceEnabled ? sourceTotalRecords ?? mergedData.length : mergedData.length);
2263
2365
  const augmentedColumns = useMemo(() => {
2264
2366
  const wrap = (col) => {
2265
2367
  const meta = col.meta;
@@ -2409,6 +2511,10 @@ function DataTable({
2409
2511
  enableRowSelection: true,
2410
2512
  enableExpanding: !!(renderSubRow || getSubRows),
2411
2513
  enablePinning: f.pinning,
2514
+ manualPagination: isServerSideDataSource,
2515
+ manualSorting: isServerSideDataSource,
2516
+ manualFiltering: isServerSideDataSource,
2517
+ pageCount: isServerSideDataSource ? Math.max(1, Math.ceil(effectiveTotalRecords / pagination.pageSize)) : void 0,
2412
2518
  getRowId: (row) => String(row.id),
2413
2519
  getSubRows,
2414
2520
  getRowCanExpand: (row) => {
@@ -2433,27 +2539,56 @@ function DataTable({
2433
2539
  getExpandedRowModel: getExpandedRowModel(),
2434
2540
  getPaginationRowModel: getPaginationRowModel()
2435
2541
  });
2436
- const dragState = useRef({ src: null });
2437
- const onHeaderDragStart = (id, e) => {
2438
- dragState.current.src = id;
2439
- e.dataTransfer.effectAllowed = "move";
2440
- e.dataTransfer.setData("text/plain", id);
2542
+ const reorderState = useRef(null);
2543
+ const [activeReorderId, setActiveReorderId] = useState(null);
2544
+ const [dragOverColId, setDragOverColId] = useState(null);
2545
+ const isInteractiveTarget = (target) => {
2546
+ const el = target;
2547
+ if (!el || !el.closest) return false;
2548
+ return !!el.closest(
2549
+ 'button, a, input, select, textarea, [role="menuitem"], [data-no-drag]'
2550
+ );
2441
2551
  };
2442
- const onHeaderDragOver = (e) => {
2443
- e.preventDefault();
2444
- e.dataTransfer.dropEffect = "move";
2552
+ const onHeaderPointerDown = (id) => (e) => {
2553
+ if (e.button !== 0) return;
2554
+ if (isInteractiveTarget(e.target)) return;
2555
+ reorderState.current = { x: e.clientX, y: e.clientY, id };
2445
2556
  };
2446
- const onHeaderDrop = (targetId) => {
2447
- const src = dragState.current.src;
2448
- dragState.current.src = null;
2449
- if (!src || src === targetId) return;
2557
+ const onHeaderPointerMove = (e) => {
2558
+ const start = reorderState.current;
2559
+ if (!start) return;
2560
+ if (!activeReorderId) {
2561
+ const dx = e.clientX - start.x;
2562
+ const dy = e.clientY - start.y;
2563
+ if (Math.hypot(dx, dy) < 6) return;
2564
+ setActiveReorderId(start.id);
2565
+ e.currentTarget.setPointerCapture(e.pointerId);
2566
+ }
2567
+ const el = document.elementFromPoint(e.clientX, e.clientY);
2568
+ const th = el ? el.closest("th[data-col-id]") : null;
2569
+ const overId = th?.dataset.colId ?? null;
2570
+ const next = overId && overId !== start.id ? overId : null;
2571
+ if (next !== dragOverColId) setDragOverColId(next);
2572
+ };
2573
+ const onHeaderPointerUp = (e) => {
2574
+ const start = reorderState.current;
2575
+ reorderState.current = null;
2576
+ if (!activeReorderId) return;
2577
+ try {
2578
+ e.currentTarget.releasePointerCapture(e.pointerId);
2579
+ } catch {
2580
+ }
2581
+ const target = dragOverColId;
2582
+ setActiveReorderId(null);
2583
+ setDragOverColId(null);
2584
+ if (!start || !target || target === start.id) return;
2450
2585
  setColumnOrder((order) => {
2451
2586
  const next = [...order];
2452
- const from = next.indexOf(src);
2453
- const to = next.indexOf(targetId);
2587
+ const from = next.indexOf(start.id);
2588
+ const to = next.indexOf(target);
2454
2589
  if (from === -1 || to === -1) return order;
2455
2590
  next.splice(from, 1);
2456
- next.splice(to, 0, src);
2591
+ next.splice(to, 0, start.id);
2457
2592
  return next;
2458
2593
  });
2459
2594
  };
@@ -2510,9 +2645,9 @@ function DataTable({
2510
2645
  DataTableToolbar,
2511
2646
  {
2512
2647
  table,
2513
- totalRecords: totalRecords ?? mergedData.length,
2514
- isFetching,
2515
- onRefresh: f.refresh ? onRefresh : void 0,
2648
+ totalRecords: effectiveTotalRecords,
2649
+ isFetching: effectiveIsFetching,
2650
+ onRefresh: f.refresh ? effectiveOnRefresh : void 0,
2516
2651
  onAddRow: f.addRow ? handleAddRow : void 0,
2517
2652
  onBulkDelete,
2518
2653
  exportFileName,
@@ -2540,32 +2675,38 @@ function DataTable({
2540
2675
  const meta = header.column.columnDef.meta;
2541
2676
  const isUtility = header.column.id === "__select" || header.column.id === "__expand" || header.column.id === "__actions";
2542
2677
  const isPinned = !!header.column.getIsPinned();
2678
+ const canReorder = !isUtility && f.reordering;
2679
+ const isDropTarget = canReorder && dragOverColId === header.column.id;
2680
+ const isBeingDragged = canReorder && activeReorderId === header.column.id;
2543
2681
  return /* @__PURE__ */ jsxs(
2544
2682
  "th",
2545
2683
  {
2546
2684
  colSpan: header.colSpan,
2685
+ "data-col-id": header.column.id,
2686
+ onPointerDown: canReorder ? onHeaderPointerDown(header.column.id) : void 0,
2687
+ onPointerMove: canReorder ? onHeaderPointerMove : void 0,
2688
+ onPointerUp: canReorder ? onHeaderPointerUp : void 0,
2689
+ onPointerCancel: canReorder ? onHeaderPointerUp : void 0,
2547
2690
  style: {
2548
2691
  width: header.getSize(),
2549
2692
  ...getPinningStyles(header.column),
2550
2693
  ...isPinned ? { zIndex: 2 } : {}
2551
2694
  },
2552
2695
  className: cn(
2553
- "group/th relative h-9 px-2 text-left align-middle text-xs font-medium text-muted-foreground",
2696
+ "group/th relative h-9 px-2 text-left align-middle text-xs font-medium text-muted-foreground select-none",
2697
+ canReorder && "cursor-grab active:cursor-grabbing",
2554
2698
  isPinned && "bg-card",
2699
+ isBeingDragged && "opacity-50",
2700
+ isDropTarget && "bg-primary/10 ring-2 ring-inset ring-primary/40",
2555
2701
  meta?.headerClassName
2556
2702
  ),
2557
2703
  children: [
2558
2704
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
2559
- !isUtility && f.reordering && /* @__PURE__ */ jsx(
2560
- "span",
2705
+ canReorder && /* @__PURE__ */ jsx(
2706
+ GripVerticalIcon,
2561
2707
  {
2562
- draggable: true,
2563
- onDragStart: (e) => onHeaderDragStart(header.column.id, e),
2564
- onDragOver: onHeaderDragOver,
2565
- onDrop: () => onHeaderDrop(header.column.id),
2566
- 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",
2567
- title: "Drag to reorder",
2568
- children: /* @__PURE__ */ jsx(GripVerticalIcon, { className: "size-3" })
2708
+ className: "-ml-1 size-3 shrink-0 text-muted-foreground/40 opacity-0 transition group-hover/th:opacity-100",
2709
+ "aria-hidden": "true"
2569
2710
  }
2570
2711
  ),
2571
2712
  /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children: header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext()) })
@@ -2573,6 +2714,8 @@ function DataTable({
2573
2714
  header.column.getCanResize() && /* @__PURE__ */ jsx(
2574
2715
  "span",
2575
2716
  {
2717
+ "data-no-drag": true,
2718
+ onPointerDown: (e) => e.stopPropagation(),
2576
2719
  onMouseDown: header.getResizeHandler(),
2577
2720
  onTouchStart: header.getResizeHandler(),
2578
2721
  onDoubleClick: () => header.column.resetSize(),
@@ -2587,7 +2730,25 @@ function DataTable({
2587
2730
  header.id
2588
2731
  );
2589
2732
  }) }, hg.id)) }),
2590
- /* @__PURE__ */ jsx("tbody", { children: isLoading ? renderSkeleton(visibleLeafCount, pagination.pageSize) : rows.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx(
2733
+ /* @__PURE__ */ jsx("tbody", { children: effectiveIsLoading ? renderSkeleton(visibleLeafCount, pagination.pageSize) : sourceError && rows.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx(
2734
+ "td",
2735
+ {
2736
+ colSpan: visibleLeafCount,
2737
+ className: "h-32 text-center text-sm text-muted-foreground",
2738
+ children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-2", children: [
2739
+ /* @__PURE__ */ jsx("span", { children: labels.loadError }),
2740
+ effectiveOnRefresh && /* @__PURE__ */ jsx(
2741
+ "button",
2742
+ {
2743
+ type: "button",
2744
+ onClick: effectiveOnRefresh,
2745
+ className: "rounded-md border px-2 py-1 text-xs text-foreground hover:bg-muted",
2746
+ children: labels.retry
2747
+ }
2748
+ )
2749
+ ] })
2750
+ }
2751
+ ) }) : rows.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx(
2591
2752
  "td",
2592
2753
  {
2593
2754
  colSpan: visibleLeafCount,
@@ -2606,7 +2767,7 @@ function DataTable({
2606
2767
  ]
2607
2768
  }
2608
2769
  ),
2609
- isFetching && !isLoading && /* @__PURE__ */ 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: [
2770
+ effectiveIsFetching && !effectiveIsLoading && /* @__PURE__ */ 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: [
2610
2771
  /* @__PURE__ */ jsx(Loader2Icon, { className: "size-3 animate-spin" }),
2611
2772
  " ",
2612
2773
  labels.refreshing