@dynostack/react-grid 0.1.0 → 0.2.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.
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, useId, 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,
1767
- {
1768
- danger: a === "delete",
1769
- onClick: () => {
1770
- setOpen(false);
1771
- onAction?.(a, row);
1772
- },
1773
- 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,
1786
+ open && createPortal(
1787
+ /* @__PURE__ */ jsxs(
1788
+ "div",
1784
1789
  {
1785
- danger: c.danger,
1786
- onClick: () => {
1787
- setOpen(false);
1788
- onAction?.(c.id, row);
1789
- },
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 },
1790
1793
  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({
@@ -1991,57 +2024,148 @@ var TOKEN_TO_VAR = {
1991
2024
  radius: "--radius",
1992
2025
  fontFamily: "--font-family"
1993
2026
  };
1994
- function themeToStyle(theme) {
1995
- if (!theme) return {};
2027
+ function isModed(theme) {
2028
+ if (!theme) return false;
2029
+ return "light" in theme || "dark" in theme;
2030
+ }
2031
+ function tokensToStyle(tokens) {
2032
+ if (!tokens) return {};
1996
2033
  const out = {};
1997
- for (const [k, v] of Object.entries(theme)) {
2034
+ for (const [k, v] of Object.entries(tokens)) {
1998
2035
  if (v == null) continue;
1999
2036
  const cssVar = TOKEN_TO_VAR[k];
2000
2037
  if (cssVar) out[cssVar] = v;
2001
2038
  }
2002
2039
  return out;
2003
2040
  }
2004
- var themePresets = {
2041
+ function themeToStyle(theme) {
2042
+ if (!theme) return {};
2043
+ if (isModed(theme)) return tokensToStyle(theme.light);
2044
+ return tokensToStyle(theme);
2045
+ }
2046
+ function splitTheme(theme) {
2047
+ if (!theme) return { light: void 0, dark: void 0 };
2048
+ if (isModed(theme)) return { light: theme.light, dark: theme.dark };
2049
+ return { light: theme, dark: void 0 };
2050
+ }
2051
+ function buildPreset(hue, chroma = 0.03) {
2052
+ const c = chroma;
2053
+ return {
2054
+ light: {
2055
+ background: `oklch(0.99 ${c * 0.2} ${hue})`,
2056
+ foreground: `oklch(0.18 ${c * 1.2} ${hue})`,
2057
+ card: `oklch(1 0 0)`,
2058
+ cardForeground: `oklch(0.18 ${c * 1.2} ${hue})`,
2059
+ popover: `oklch(1 0 0)`,
2060
+ popoverForeground: `oklch(0.18 ${c * 1.2} ${hue})`,
2061
+ primary: `oklch(0.55 0.2 ${hue})`,
2062
+ primaryForeground: `oklch(0.99 0 0)`,
2063
+ secondary: `oklch(0.96 ${c} ${hue})`,
2064
+ secondaryForeground: `oklch(0.25 ${c * 2} ${hue})`,
2065
+ muted: `oklch(0.96 ${c} ${hue})`,
2066
+ mutedForeground: `oklch(0.5 ${c * 1.5} ${hue})`,
2067
+ accent: `oklch(0.94 ${c * 1.2} ${hue})`,
2068
+ accentForeground: `oklch(0.3 ${c * 3} ${hue})`,
2069
+ destructive: `oklch(0.577 0.245 27.325)`,
2070
+ destructiveForeground: `oklch(0.985 0 0)`,
2071
+ border: `oklch(0.9 ${c * 0.8} ${hue})`,
2072
+ input: `oklch(0.9 ${c * 0.8} ${hue})`,
2073
+ ring: `oklch(0.55 0.2 ${hue} / 0.5)`,
2074
+ radius: "0.625rem"
2075
+ },
2076
+ dark: {
2077
+ background: `oklch(0.16 ${c * 0.6} ${hue})`,
2078
+ foreground: `oklch(0.97 ${c * 0.2} ${hue})`,
2079
+ card: `oklch(0.21 ${c * 0.8} ${hue})`,
2080
+ cardForeground: `oklch(0.97 ${c * 0.2} ${hue})`,
2081
+ popover: `oklch(0.21 ${c * 0.8} ${hue})`,
2082
+ popoverForeground: `oklch(0.97 ${c * 0.2} ${hue})`,
2083
+ primary: `oklch(0.7 0.18 ${hue})`,
2084
+ primaryForeground: `oklch(0.16 ${c * 0.6} ${hue})`,
2085
+ secondary: `oklch(0.27 ${c * 0.8} ${hue})`,
2086
+ secondaryForeground: `oklch(0.97 ${c * 0.2} ${hue})`,
2087
+ muted: `oklch(0.27 ${c * 0.8} ${hue})`,
2088
+ mutedForeground: `oklch(0.7 ${c * 0.5} ${hue})`,
2089
+ accent: `oklch(0.3 ${c * 1.5} ${hue})`,
2090
+ accentForeground: `oklch(0.97 ${c * 0.2} ${hue})`,
2091
+ destructive: `oklch(0.577 0.245 27.325 / 0.6)`,
2092
+ destructiveForeground: `oklch(0.985 0 0)`,
2093
+ border: `oklch(0.27 ${c * 0.8} ${hue})`,
2094
+ input: `oklch(0.27 ${c * 0.8} ${hue})`,
2095
+ ring: `oklch(0.7 0.18 ${hue} / 0.5)`,
2096
+ radius: "0.625rem"
2097
+ }
2098
+ };
2099
+ }
2100
+ function tokensToCssBlock(tokens) {
2101
+ if (!tokens) return "";
2102
+ const parts = [];
2103
+ for (const [k, v] of Object.entries(tokens)) {
2104
+ if (v == null) continue;
2105
+ const cssVar = TOKEN_TO_VAR[k];
2106
+ if (cssVar) parts.push(`${cssVar}:${v}`);
2107
+ }
2108
+ return parts.join(";");
2109
+ }
2110
+ var neutral = {
2005
2111
  light: {
2006
2112
  background: "oklch(1 0 0)",
2007
2113
  foreground: "oklch(0.145 0 0)",
2008
2114
  card: "oklch(1 0 0)",
2009
2115
  cardForeground: "oklch(0.145 0 0)",
2116
+ popover: "oklch(1 0 0)",
2117
+ popoverForeground: "oklch(0.145 0 0)",
2010
2118
  primary: "oklch(0.205 0 0)",
2011
2119
  primaryForeground: "oklch(0.985 0 0)",
2012
- border: "oklch(0.922 0 0)"
2120
+ secondary: "oklch(0.97 0 0)",
2121
+ secondaryForeground: "oklch(0.205 0 0)",
2122
+ muted: "oklch(0.97 0 0)",
2123
+ mutedForeground: "oklch(0.556 0 0)",
2124
+ accent: "oklch(0.97 0 0)",
2125
+ accentForeground: "oklch(0.205 0 0)",
2126
+ destructive: "oklch(0.577 0.245 27.325)",
2127
+ destructiveForeground: "oklch(0.985 0 0)",
2128
+ border: "oklch(0.922 0 0)",
2129
+ input: "oklch(0.922 0 0)",
2130
+ ring: "oklch(0.708 0 0)",
2131
+ radius: "0.625rem"
2013
2132
  },
2014
2133
  dark: {
2015
2134
  background: "oklch(0.145 0 0)",
2016
2135
  foreground: "oklch(0.985 0 0)",
2017
2136
  card: "oklch(0.205 0 0)",
2018
2137
  cardForeground: "oklch(0.985 0 0)",
2138
+ popover: "oklch(0.205 0 0)",
2139
+ popoverForeground: "oklch(0.985 0 0)",
2019
2140
  primary: "oklch(0.985 0 0)",
2020
2141
  primaryForeground: "oklch(0.205 0 0)",
2021
- border: "oklch(0.269 0 0)"
2022
- },
2023
- emerald: {
2024
- primary: "oklch(0.66 0.15 162)",
2025
- primaryForeground: "oklch(1 0 0)",
2026
- accent: "oklch(0.94 0.04 162)",
2027
- accentForeground: "oklch(0.3 0.1 162)",
2028
- ring: "oklch(0.66 0.15 162 / 0.5)"
2029
- },
2030
- violet: {
2031
- primary: "oklch(0.55 0.22 285)",
2032
- primaryForeground: "oklch(1 0 0)",
2033
- accent: "oklch(0.94 0.04 285)",
2034
- accentForeground: "oklch(0.3 0.12 285)",
2035
- ring: "oklch(0.55 0.22 285 / 0.5)"
2036
- },
2037
- amber: {
2038
- primary: "oklch(0.7 0.18 65)",
2039
- primaryForeground: "oklch(0.2 0 0)",
2040
- accent: "oklch(0.94 0.05 65)",
2041
- accentForeground: "oklch(0.3 0.12 65)",
2042
- ring: "oklch(0.7 0.18 65 / 0.5)"
2142
+ secondary: "oklch(0.269 0 0)",
2143
+ secondaryForeground: "oklch(0.985 0 0)",
2144
+ muted: "oklch(0.269 0 0)",
2145
+ mutedForeground: "oklch(0.708 0 0)",
2146
+ accent: "oklch(0.269 0 0)",
2147
+ accentForeground: "oklch(0.985 0 0)",
2148
+ destructive: "oklch(0.577 0.245 27.325 / 0.6)",
2149
+ destructiveForeground: "oklch(0.985 0 0)",
2150
+ border: "oklch(0.269 0 0)",
2151
+ input: "oklch(0.269 0 0)",
2152
+ ring: "oklch(0.439 0 0)",
2153
+ radius: "0.625rem"
2043
2154
  }
2044
2155
  };
2156
+ var themePresets = {
2157
+ neutral,
2158
+ light: { light: neutral.light },
2159
+ dark: { light: neutral.dark },
2160
+ violet: buildPreset(285),
2161
+ emerald: buildPreset(162),
2162
+ amber: buildPreset(65),
2163
+ rose: buildPreset(15),
2164
+ sky: buildPreset(235),
2165
+ slate: buildPreset(240, 0.015)
2166
+ };
2167
+ var ISOLATE_LIGHT_TOKENS = neutral.light;
2168
+ var ISOLATE_DARK_TOKENS = neutral.dark;
2045
2169
  function getPinningStyles(column) {
2046
2170
  const isPinned = column.getIsPinned();
2047
2171
  if (!isPinned) return {};
@@ -2164,6 +2288,8 @@ var DEFAULT_LABELS = {
2164
2288
  total: "Total",
2165
2289
  noData: "No data.",
2166
2290
  noResults: "No rows match the current filters.",
2291
+ loadError: "Could not load rows.",
2292
+ retry: "Retry",
2167
2293
  refreshing: "Refreshing",
2168
2294
  rowsPerPage: "Rows per page",
2169
2295
  page: "Page",
@@ -2177,6 +2303,7 @@ var DENSITY_CLASS = {
2177
2303
  function DataTable({
2178
2304
  data,
2179
2305
  columns,
2306
+ dataSource,
2180
2307
  isLoading,
2181
2308
  isFetching,
2182
2309
  onRefresh,
@@ -2203,12 +2330,13 @@ function DataTable({
2203
2330
  features,
2204
2331
  labels: labelsProp,
2205
2332
  density = "default",
2206
- theme
2333
+ theme,
2334
+ isolate = false
2207
2335
  }) {
2208
2336
  const labels = { ...DEFAULT_LABELS, ...labelsProp };
2209
2337
  const f = {
2210
2338
  search: features?.search ?? true,
2211
- refresh: features?.refresh ?? !!onRefresh,
2339
+ refresh: features?.refresh ?? !!(onRefresh || dataSource),
2212
2340
  columnVisibility: features?.columnVisibility ?? true,
2213
2341
  export: features?.export ?? true,
2214
2342
  addRow: features?.addRow ?? !!onAddRow,
@@ -2219,7 +2347,20 @@ function DataTable({
2219
2347
  reordering: features?.reordering ?? true,
2220
2348
  pinning: features?.pinning ?? true
2221
2349
  };
2222
- const themeStyle = themeToStyle(theme);
2350
+ const rawInstanceId = useId();
2351
+ const instanceId = useMemo(
2352
+ () => `dyno-grid-${rawInstanceId.replace(/[^a-zA-Z0-9]/g, "")}`,
2353
+ [rawInstanceId]
2354
+ );
2355
+ const { lightStyle, darkCss } = useMemo(() => {
2356
+ const { light, dark } = splitTheme(theme);
2357
+ const lightTokens = isolate ? { ...ISOLATE_LIGHT_TOKENS, ...light ?? {} } : light;
2358
+ const darkTokens = isolate ? { ...ISOLATE_DARK_TOKENS, ...dark ?? light ?? {} } : dark;
2359
+ return {
2360
+ lightStyle: tokensToStyle(lightTokens),
2361
+ darkCss: darkTokens ? tokensToCssBlock(darkTokens) : null
2362
+ };
2363
+ }, [theme, isolate]);
2223
2364
  const [sorting, setSorting] = useState([]);
2224
2365
  const [columnFilters, setColumnFilters] = useState([]);
2225
2366
  const [internalGlobalFilter, setInternalGlobalFilter] = useState("");
@@ -2252,14 +2393,80 @@ function DataTable({
2252
2393
  pageIndex: 0,
2253
2394
  pageSize: initialPageSize
2254
2395
  });
2396
+ const [sourceRows, setSourceRows] = useState(
2397
+ () => dataSource?.initialData ?? []
2398
+ );
2399
+ const [sourceTotalRecords, setSourceTotalRecords] = useState(void 0);
2400
+ const [sourceError, setSourceError] = useState(null);
2401
+ const [sourceLoading, setSourceLoading] = useState(false);
2402
+ const [sourceFetching, setSourceFetching] = useState(false);
2403
+ const [sourceRefreshKey, setSourceRefreshKey] = useState(0);
2255
2404
  const [editingCell, setEditingCell] = useState(null);
2256
2405
  const [editingRowId, setEditingRowId] = useState(null);
2257
2406
  const [rowDraft, setRowDraft] = useState({});
2407
+ const dataSourceEnabled = !!dataSource && dataSource.enabled !== false;
2408
+ const dataSourceMode = dataSource?.mode ?? "client";
2409
+ const isServerSideDataSource = dataSourceEnabled && dataSourceMode === "server";
2410
+ const dataSourceDeps = dataSource?.deps ?? [];
2411
+ useEffect(() => {
2412
+ if (!dataSourceEnabled || !dataSource) return;
2413
+ let cancelled = false;
2414
+ const isInitialLoad = sourceRows.length === 0;
2415
+ setSourceError(null);
2416
+ setSourceLoading(isInitialLoad);
2417
+ setSourceFetching(!isInitialLoad);
2418
+ dataSource.fetchRows({
2419
+ pageIndex: pagination.pageIndex,
2420
+ pageSize: pagination.pageSize,
2421
+ sorting,
2422
+ columnFilters,
2423
+ globalFilter
2424
+ }).then((result) => {
2425
+ if (cancelled) return;
2426
+ const rows2 = Array.isArray(result) ? result : result.rows;
2427
+ setSourceRows(rows2);
2428
+ setSourceTotalRecords(
2429
+ Array.isArray(result) ? rows2.length : result.totalRecords ?? rows2.length
2430
+ );
2431
+ }).catch((error) => {
2432
+ if (cancelled) return;
2433
+ setSourceError(error);
2434
+ dataSource.onError?.(error, {
2435
+ type: isInitialLoad ? "load" : "refresh",
2436
+ message: isInitialLoad ? labels.loadError : labels.refreshing
2437
+ });
2438
+ }).finally(() => {
2439
+ if (cancelled) return;
2440
+ setSourceLoading(false);
2441
+ setSourceFetching(false);
2442
+ });
2443
+ return () => {
2444
+ cancelled = true;
2445
+ };
2446
+ }, [
2447
+ dataSourceEnabled,
2448
+ dataSource,
2449
+ sourceRefreshKey,
2450
+ pagination.pageIndex,
2451
+ pagination.pageSize,
2452
+ sorting,
2453
+ columnFilters,
2454
+ globalFilter,
2455
+ labels.loadError,
2456
+ labels.refreshing,
2457
+ // eslint-disable-next-line react-hooks/exhaustive-deps
2458
+ ...dataSourceDeps
2459
+ ]);
2460
+ const baseData = dataSourceEnabled ? sourceRows : data ?? [];
2461
+ const effectiveIsLoading = dataSourceEnabled ? sourceLoading : isLoading;
2462
+ const effectiveIsFetching = dataSourceEnabled ? sourceFetching : isFetching;
2463
+ const effectiveOnRefresh = dataSourceEnabled ? () => setSourceRefreshKey((key) => key + 1) : onRefresh;
2258
2464
  const [extraRows, setExtraRows] = useState([]);
2259
2465
  const mergedData = useMemo(
2260
- () => extraRows.length ? [...extraRows, ...data] : data,
2261
- [extraRows, data]
2466
+ () => extraRows.length ? [...extraRows, ...baseData] : baseData,
2467
+ [extraRows, baseData]
2262
2468
  );
2469
+ const effectiveTotalRecords = totalRecords ?? (dataSourceEnabled ? sourceTotalRecords ?? mergedData.length : mergedData.length);
2263
2470
  const augmentedColumns = useMemo(() => {
2264
2471
  const wrap = (col) => {
2265
2472
  const meta = col.meta;
@@ -2409,6 +2616,10 @@ function DataTable({
2409
2616
  enableRowSelection: true,
2410
2617
  enableExpanding: !!(renderSubRow || getSubRows),
2411
2618
  enablePinning: f.pinning,
2619
+ manualPagination: isServerSideDataSource,
2620
+ manualSorting: isServerSideDataSource,
2621
+ manualFiltering: isServerSideDataSource,
2622
+ pageCount: isServerSideDataSource ? Math.max(1, Math.ceil(effectiveTotalRecords / pagination.pageSize)) : void 0,
2412
2623
  getRowId: (row) => String(row.id),
2413
2624
  getSubRows,
2414
2625
  getRowCanExpand: (row) => {
@@ -2433,27 +2644,56 @@ function DataTable({
2433
2644
  getExpandedRowModel: getExpandedRowModel(),
2434
2645
  getPaginationRowModel: getPaginationRowModel()
2435
2646
  });
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);
2647
+ const reorderState = useRef(null);
2648
+ const [activeReorderId, setActiveReorderId] = useState(null);
2649
+ const [dragOverColId, setDragOverColId] = useState(null);
2650
+ const isInteractiveTarget = (target) => {
2651
+ const el = target;
2652
+ if (!el || !el.closest) return false;
2653
+ return !!el.closest(
2654
+ 'button, a, input, select, textarea, [role="menuitem"], [data-no-drag]'
2655
+ );
2441
2656
  };
2442
- const onHeaderDragOver = (e) => {
2443
- e.preventDefault();
2444
- e.dataTransfer.dropEffect = "move";
2657
+ const onHeaderPointerDown = (id) => (e) => {
2658
+ if (e.button !== 0) return;
2659
+ if (isInteractiveTarget(e.target)) return;
2660
+ reorderState.current = { x: e.clientX, y: e.clientY, id };
2445
2661
  };
2446
- const onHeaderDrop = (targetId) => {
2447
- const src = dragState.current.src;
2448
- dragState.current.src = null;
2449
- if (!src || src === targetId) return;
2662
+ const onHeaderPointerMove = (e) => {
2663
+ const start = reorderState.current;
2664
+ if (!start) return;
2665
+ if (!activeReorderId) {
2666
+ const dx = e.clientX - start.x;
2667
+ const dy = e.clientY - start.y;
2668
+ if (Math.hypot(dx, dy) < 6) return;
2669
+ setActiveReorderId(start.id);
2670
+ e.currentTarget.setPointerCapture(e.pointerId);
2671
+ }
2672
+ const el = document.elementFromPoint(e.clientX, e.clientY);
2673
+ const th = el ? el.closest("th[data-col-id]") : null;
2674
+ const overId = th?.dataset.colId ?? null;
2675
+ const next = overId && overId !== start.id ? overId : null;
2676
+ if (next !== dragOverColId) setDragOverColId(next);
2677
+ };
2678
+ const onHeaderPointerUp = (e) => {
2679
+ const start = reorderState.current;
2680
+ reorderState.current = null;
2681
+ if (!activeReorderId) return;
2682
+ try {
2683
+ e.currentTarget.releasePointerCapture(e.pointerId);
2684
+ } catch {
2685
+ }
2686
+ const target = dragOverColId;
2687
+ setActiveReorderId(null);
2688
+ setDragOverColId(null);
2689
+ if (!start || !target || target === start.id) return;
2450
2690
  setColumnOrder((order) => {
2451
2691
  const next = [...order];
2452
- const from = next.indexOf(src);
2453
- const to = next.indexOf(targetId);
2692
+ const from = next.indexOf(start.id);
2693
+ const to = next.indexOf(target);
2454
2694
  if (from === -1 || to === -1) return order;
2455
2695
  next.splice(from, 1);
2456
- next.splice(to, 0, src);
2696
+ next.splice(to, 0, start.id);
2457
2697
  return next;
2458
2698
  });
2459
2699
  };
@@ -2499,20 +2739,26 @@ function DataTable({
2499
2739
  children: /* @__PURE__ */ jsxs(
2500
2740
  "div",
2501
2741
  {
2742
+ "data-dyno-grid": instanceId,
2502
2743
  className: cn(
2503
2744
  "flex flex-col rounded-lg border bg-card text-card-foreground shadow-sm",
2504
2745
  DENSITY_CLASS[density],
2505
2746
  className
2506
2747
  ),
2507
- style: themeStyle,
2748
+ style: lightStyle,
2508
2749
  children: [
2750
+ darkCss && /* @__PURE__ */ jsxs("style", { children: [
2751
+ `@media (prefers-color-scheme: dark){[data-dyno-grid="${instanceId}"]{${darkCss}}}`,
2752
+ `.dark [data-dyno-grid="${instanceId}"]{${darkCss}}`,
2753
+ `[data-dyno-grid="${instanceId}"].dark{${darkCss}}`
2754
+ ] }),
2509
2755
  /* @__PURE__ */ jsx(
2510
2756
  DataTableToolbar,
2511
2757
  {
2512
2758
  table,
2513
- totalRecords: totalRecords ?? mergedData.length,
2514
- isFetching,
2515
- onRefresh: f.refresh ? onRefresh : void 0,
2759
+ totalRecords: effectiveTotalRecords,
2760
+ isFetching: effectiveIsFetching,
2761
+ onRefresh: f.refresh ? effectiveOnRefresh : void 0,
2516
2762
  onAddRow: f.addRow ? handleAddRow : void 0,
2517
2763
  onBulkDelete,
2518
2764
  exportFileName,
@@ -2540,32 +2786,38 @@ function DataTable({
2540
2786
  const meta = header.column.columnDef.meta;
2541
2787
  const isUtility = header.column.id === "__select" || header.column.id === "__expand" || header.column.id === "__actions";
2542
2788
  const isPinned = !!header.column.getIsPinned();
2789
+ const canReorder = !isUtility && f.reordering;
2790
+ const isDropTarget = canReorder && dragOverColId === header.column.id;
2791
+ const isBeingDragged = canReorder && activeReorderId === header.column.id;
2543
2792
  return /* @__PURE__ */ jsxs(
2544
2793
  "th",
2545
2794
  {
2546
2795
  colSpan: header.colSpan,
2796
+ "data-col-id": header.column.id,
2797
+ onPointerDown: canReorder ? onHeaderPointerDown(header.column.id) : void 0,
2798
+ onPointerMove: canReorder ? onHeaderPointerMove : void 0,
2799
+ onPointerUp: canReorder ? onHeaderPointerUp : void 0,
2800
+ onPointerCancel: canReorder ? onHeaderPointerUp : void 0,
2547
2801
  style: {
2548
2802
  width: header.getSize(),
2549
2803
  ...getPinningStyles(header.column),
2550
2804
  ...isPinned ? { zIndex: 2 } : {}
2551
2805
  },
2552
2806
  className: cn(
2553
- "group/th relative h-9 px-2 text-left align-middle text-xs font-medium text-muted-foreground",
2807
+ "group/th relative h-9 px-2 text-left align-middle text-xs font-medium text-muted-foreground select-none",
2808
+ canReorder && "cursor-grab active:cursor-grabbing",
2554
2809
  isPinned && "bg-card",
2810
+ isBeingDragged && "opacity-50",
2811
+ isDropTarget && "bg-primary/10 ring-2 ring-inset ring-primary/40",
2555
2812
  meta?.headerClassName
2556
2813
  ),
2557
2814
  children: [
2558
2815
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
2559
- !isUtility && f.reordering && /* @__PURE__ */ jsx(
2560
- "span",
2816
+ canReorder && /* @__PURE__ */ jsx(
2817
+ GripVerticalIcon,
2561
2818
  {
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" })
2819
+ className: "-ml-1 size-3 shrink-0 text-muted-foreground/40 opacity-0 transition group-hover/th:opacity-100",
2820
+ "aria-hidden": "true"
2569
2821
  }
2570
2822
  ),
2571
2823
  /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children: header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext()) })
@@ -2573,6 +2825,8 @@ function DataTable({
2573
2825
  header.column.getCanResize() && /* @__PURE__ */ jsx(
2574
2826
  "span",
2575
2827
  {
2828
+ "data-no-drag": true,
2829
+ onPointerDown: (e) => e.stopPropagation(),
2576
2830
  onMouseDown: header.getResizeHandler(),
2577
2831
  onTouchStart: header.getResizeHandler(),
2578
2832
  onDoubleClick: () => header.column.resetSize(),
@@ -2587,7 +2841,25 @@ function DataTable({
2587
2841
  header.id
2588
2842
  );
2589
2843
  }) }, hg.id)) }),
2590
- /* @__PURE__ */ jsx("tbody", { children: isLoading ? renderSkeleton(visibleLeafCount, pagination.pageSize) : rows.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx(
2844
+ /* @__PURE__ */ jsx("tbody", { children: effectiveIsLoading ? renderSkeleton(visibleLeafCount, pagination.pageSize) : sourceError && rows.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx(
2845
+ "td",
2846
+ {
2847
+ colSpan: visibleLeafCount,
2848
+ className: "h-32 text-center text-sm text-muted-foreground",
2849
+ children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-2", children: [
2850
+ /* @__PURE__ */ jsx("span", { children: labels.loadError }),
2851
+ effectiveOnRefresh && /* @__PURE__ */ jsx(
2852
+ "button",
2853
+ {
2854
+ type: "button",
2855
+ onClick: effectiveOnRefresh,
2856
+ className: "rounded-md border px-2 py-1 text-xs text-foreground hover:bg-muted",
2857
+ children: labels.retry
2858
+ }
2859
+ )
2860
+ ] })
2861
+ }
2862
+ ) }) : rows.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx(
2591
2863
  "td",
2592
2864
  {
2593
2865
  colSpan: visibleLeafCount,
@@ -2606,7 +2878,7 @@ function DataTable({
2606
2878
  ]
2607
2879
  }
2608
2880
  ),
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: [
2881
+ 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
2882
  /* @__PURE__ */ jsx(Loader2Icon, { className: "size-3 animate-spin" }),
2611
2883
  " ",
2612
2884
  labels.refreshing
@@ -2666,6 +2938,6 @@ function renderSkeleton(cols, rows) {
2666
2938
  return Array.from({ length: rows }).map((_, r) => /* @__PURE__ */ jsx("tr", { className: "border-b", children: Array.from({ length: cols }).map((__, c) => /* @__PURE__ */ jsx("td", { className: "px-2 py-2", children: /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-full" }) }, c)) }, r));
2667
2939
  }
2668
2940
 
2669
- export { ColumnFilterPanel, DataTable, DataTableColumnHeader, DataTablePagination, DataTableRowActions, DataTableToolbar, EditableCell, booleanFilterFn, dateFilterFn, exportToCsv, exportToExcel, isFilterActive, numberFilterFn, setFilterFn, textFilterFn, themePresets, themeToStyle };
2941
+ export { ColumnFilterPanel, DataTable, DataTableColumnHeader, DataTablePagination, DataTableRowActions, DataTableToolbar, EditableCell, ISOLATE_DARK_TOKENS, ISOLATE_LIGHT_TOKENS, booleanFilterFn, buildPreset, dateFilterFn, exportToCsv, exportToExcel, isFilterActive, numberFilterFn, setFilterFn, splitTheme, textFilterFn, themePresets, themeToStyle, tokensToCssBlock, tokensToStyle };
2670
2942
  //# sourceMappingURL=index.js.map
2671
2943
  //# sourceMappingURL=index.js.map