@marktiderman/genesis-ui 2.8.0 → 2.10.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.
Files changed (33) hide show
  1. package/SKILL.md +75 -4
  2. package/dist/{AppShell-LXQykEqe.d.ts → AppShell-BVr4V0UK.d.ts} +1 -110
  3. package/dist/PageHeader-V9lVkZtF.d.ts +114 -0
  4. package/dist/{chunk-S7NXD37O.js → chunk-25XEXYGC.js} +4 -4
  5. package/dist/chunk-25XEXYGC.js.map +1 -0
  6. package/dist/chunk-C7PQ54JI.js +763 -0
  7. package/dist/chunk-C7PQ54JI.js.map +1 -0
  8. package/dist/chunk-DIV4BYXX.js +58 -0
  9. package/dist/chunk-DIV4BYXX.js.map +1 -0
  10. package/dist/chunk-I4NBE2KL.js +16 -0
  11. package/dist/chunk-I4NBE2KL.js.map +1 -0
  12. package/dist/{chunk-DWCAC2E7.js → chunk-RKPS7ERS.js} +5 -4
  13. package/dist/chunk-RKPS7ERS.js.map +1 -0
  14. package/dist/components/data/index.d.ts +42 -949
  15. package/dist/components/data/index.js +962 -777
  16. package/dist/components/data/index.js.map +1 -1
  17. package/dist/components/layout/index.d.ts +2 -1
  18. package/dist/components/layout/index.js +7 -7
  19. package/dist/components/patterns/index.js +2 -1
  20. package/dist/components/patterns/sortable-list.js +2 -1
  21. package/dist/hooks/index.d.ts +8 -1
  22. package/dist/hooks/index.js +4 -1
  23. package/dist/hooks/use-resource-page.d.ts +1 -1
  24. package/dist/hooks/use-resource-page.js +1 -1
  25. package/dist/hooks/use-resource-url-state.d.ts +68 -0
  26. package/dist/hooks/use-resource-url-state.js +3 -0
  27. package/dist/hooks/use-resource-url-state.js.map +1 -0
  28. package/dist/index.js +21 -20
  29. package/dist/{use-resource-page-D4byuwAs.d.ts → use-resource-page-DaJzuq3M.d.ts} +63 -2
  30. package/dist/use-resource-table-DCVf_lEG.d.ts +1113 -0
  31. package/package.json +4 -8
  32. package/dist/chunk-DWCAC2E7.js.map +0 -1
  33. package/dist/chunk-S7NXD37O.js.map +0 -1
@@ -2,51 +2,58 @@
2
2
  import { AppShell, PageHeader } from '../../chunk-UYI3GLFJ.js';
3
3
  export { ViewSettings, ViewToggle } from '../../chunk-46TZGMFW.js';
4
4
  import { renderSlot } from '../../chunk-7S7LKQBD.js';
5
+ import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from '../../chunk-N2ZFNOOT.js';
5
6
  import { Textarea } from '../../chunk-YETMISCE.js';
6
7
  import { Separator } from '../../chunk-SXSCHWZU.js';
7
8
  import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription } from '../../chunk-XCGZPFT7.js';
8
9
  import { Skeleton } from '../../chunk-QFM4Z2SS.js';
9
- import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from '../../chunk-N2ZFNOOT.js';
10
10
  import { Progress } from '../../chunk-W5Y3LG5K.js';
11
- import { Card, CardContent, CardHeader, CardTitle } from '../../chunk-FN3FZVUR.js';
12
11
  import { Checkbox } from '../../chunk-SEBU6JCK.js';
12
+ import { Card, CardContent, CardHeader, CardTitle } from '../../chunk-FN3FZVUR.js';
13
13
  import { AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction } from '../../chunk-4C4ESW6O.js';
14
+ import { PageLoading } from '../../chunk-YRYQXU3G.js';
15
+ import { Spinner } from '../../chunk-OVU3Z2MT.js';
16
+ import { useAnnouncer } from '../../chunk-I4NBE2KL.js';
14
17
  import { Input } from '../../chunk-QSXVL5UB.js';
15
18
  import '../../chunk-B2GIBUQD.js';
16
19
  import { Badge } from '../../chunk-NPNADZHM.js';
17
- import { DetailPage } from '../../chunk-AFZ76Q26.js';
18
- import '../../chunk-D2LIXA2Y.js';
19
- import '../../chunk-AVFZ4P4B.js';
20
- import { PageLoading } from '../../chunk-YRYQXU3G.js';
21
- import { Spinner } from '../../chunk-OVU3Z2MT.js';
22
20
  import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '../../chunk-FK42ZGGV.js';
23
21
  import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuCheckboxItem, DropdownMenuSeparator, DropdownMenuItem, DropdownMenuLabel } from '../../chunk-QJ6P2DW6.js';
24
22
  import { Switch } from '../../chunk-27ELYTGE.js';
25
23
  import { Popover, PopoverTrigger, PopoverContent } from '../../chunk-22XB3Q6H.js';
24
+ import { DetailPage } from '../../chunk-AFZ76Q26.js';
25
+ import '../../chunk-D2LIXA2Y.js';
26
+ import '../../chunk-AVFZ4P4B.js';
26
27
  import { EmptyState } from '../../chunk-65SPT7EA.js';
27
28
  export { EmptyState } from '../../chunk-65SPT7EA.js';
28
29
  import { Button } from '../../chunk-NWSCYLPT.js';
29
30
  import { FormField, FormLabel, FormError, FormDescription } from '../../chunk-R7QFP6EL.js';
30
31
  import '../../chunk-F47U5KNW.js';
31
- import { titleCase, useResourcePage } from '../../chunk-DWCAC2E7.js';
32
+ import { showUndoToast, showWriteErrorToast, useResourceTableModel } from '../../chunk-C7PQ54JI.js';
33
+ export { showUndoToast, showWriteErrorToast, useResourceTable } from '../../chunk-C7PQ54JI.js';
34
+ import { titleCase } from '../../chunk-RKPS7ERS.js';
35
+ import '../../chunk-DIV4BYXX.js';
32
36
  export { useSavedViews } from '../../chunk-GCQFVMPI.js';
33
37
  import '../../chunk-PKFDGGZV.js';
34
- import { useViewSettings } from '../../chunk-OZ7ZAYCS.js';
38
+ import '../../chunk-OZ7ZAYCS.js';
39
+ import '../../chunk-4RAZJ6LL.js';
35
40
  export { CommandPalette, useCommandPaletteHotkey } from '../../chunk-CNTWQSUT.js';
36
41
  import { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandSeparator } from '../../chunk-SEFK4NZV.js';
37
42
  import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from '../../chunk-7XMZVISM.js';
38
43
  import { cn } from '../../chunk-NZ5WBA4R.js';
39
- import { DefaultLink, defaultNavigate } from '../../chunk-WGMEVX3Y.js';
44
+ import { DefaultLink } from '../../chunk-WGMEVX3Y.js';
40
45
  export { DefaultLink, defaultNavigate, isPathActive, resolveNavigation } from '../../chunk-WGMEVX3Y.js';
41
- import { useKeyboardNavigation } from '../../chunk-YJ6MPS6U.js';
46
+ import '../../chunk-YJ6MPS6U.js';
42
47
  import { useIsMobile } from '../../chunk-VFO3I7PV.js';
43
- import { AlertTriangle, RefreshCw, ChevronDown, Check, Search, X, ArrowUpDown, Bookmark, Trash2, BookmarkPlus, Loader2, Columns3, ArrowUp, ArrowDown, ChevronLeft, ChevronRight, ExternalLink, ArrowRightLeft, Pencil, ChevronsUpDown, ImageIcon, Upload, AlertCircle, FileIcon, Plus } from 'lucide-react';
48
+ import { AlertTriangle, RefreshCw, ChevronDown, Check, Search, X, ArrowUpDown, Bookmark, Trash2, BookmarkPlus, Loader2, Columns3, ArrowUp, ArrowDown, ChevronLeft, ChevronRight, ExternalLink, GripVertical, ArrowRightLeft, Pencil, ChevronsUpDown, ImageIcon, Upload, AlertCircle, FileIcon, Plus } from 'lucide-react';
44
49
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
45
- import { useState, useRef, useEffect, useCallback, useMemo, useId, Fragment as Fragment$1 } from 'react';
50
+ import { useState, useRef, useEffect, useCallback, useMemo, useId, Fragment as Fragment$1, useLayoutEffect } from 'react';
51
+ import { useReactTable, getPaginationRowModel, getSortedRowModel, getCoreRowModel } from '@tanstack/react-table';
52
+ import { useVirtualizer } from '@tanstack/react-virtual';
46
53
  import { formatDistanceToNow, format } from 'date-fns';
47
54
  import { useFieldArray, Controller, useWatch } from 'react-hook-form';
48
55
  import { useResource, useWizard, useResourceForm, useOne, buildArrayItemDefaults } from '@marktiderman/genesis-core/hooks';
49
- import { useDataProvider } from '@marktiderman/genesis-core/provider';
56
+ import { useDataProvider, isDataReadError } from '@marktiderman/genesis-core/provider';
50
57
  import { useDropzone } from 'react-dropzone';
51
58
 
52
59
  function DataPageShell({
@@ -670,6 +677,50 @@ var DENSITY_HEAD = {
670
677
  comfortable: "h-10",
671
678
  spacious: "h-12 py-3"
672
679
  };
680
+ function storeSet(key, value) {
681
+ try {
682
+ localStorage.setItem(key, value);
683
+ } catch {
684
+ }
685
+ }
686
+ function storeRemove(key) {
687
+ try {
688
+ localStorage.removeItem(key);
689
+ } catch {
690
+ }
691
+ }
692
+ var ROW_HEIGHT = { compact: 33, comfortable: 41, spacious: 49 };
693
+ var PINNED_WIDTH = 160;
694
+ function compareValues(a, b) {
695
+ const emptyA = a == null || a === "";
696
+ const emptyB = b == null || b === "";
697
+ if (emptyA || emptyB) return emptyA === emptyB ? 0 : emptyA ? -1 : 1;
698
+ if (typeof a === "number" && typeof b === "number") return a - b;
699
+ if (typeof a === "bigint" && typeof b === "bigint") return a < b ? -1 : a > b ? 1 : 0;
700
+ if (a instanceof Date && b instanceof Date) return a.getTime() - b.getTime();
701
+ if (typeof a === "boolean" && typeof b === "boolean") return Number(a) - Number(b);
702
+ return String(a).localeCompare(String(b), void 0, { numeric: true, sensitivity: "base" });
703
+ }
704
+ var ROW_OPEN = "data-row-open";
705
+ function focusRowAt(body, index) {
706
+ const target = body?.querySelector(`[${ROW_OPEN}="${index}"]`);
707
+ target?.focus();
708
+ return !!target;
709
+ }
710
+ function nextSort(prev, key, shiftKey) {
711
+ const existing = prev.findIndex((s) => s.key === key);
712
+ if (existing >= 0) {
713
+ const entry = prev[existing];
714
+ if (entry.dir === "asc") {
715
+ const next = [...prev];
716
+ next[existing] = { key, dir: "desc" };
717
+ return next;
718
+ }
719
+ return prev.filter((_, i) => i !== existing);
720
+ }
721
+ if (shiftKey) return [...prev, { key, dir: "asc" }];
722
+ return [{ key, dir: "asc" }];
723
+ }
673
724
  function getCellClasses(col) {
674
725
  const hide = col.hideBelow ? col.hideBelow === "sm" ? "hidden sm:table-cell" : col.hideBelow === "md" ? "hidden md:table-cell" : col.hideBelow === "lg" ? "hidden lg:table-cell" : "hidden xl:table-cell" : "";
675
726
  return cn(typeof col.width === "string" && !PX_WIDTH.test(col.width) ? col.width : void 0, hide);
@@ -888,29 +939,29 @@ function DataTable({
888
939
  stickyHeader = true,
889
940
  resizeKey,
890
941
  stickyFirstColumn,
891
- density = "comfortable"
942
+ density = "comfortable",
943
+ keyboardNavigation = true,
944
+ openColumn,
945
+ virtualize,
946
+ pinnedColumns,
947
+ reorderableColumns = true,
948
+ columnOrderKey,
949
+ sort: sortProp,
950
+ onSortChange
892
951
  }) {
952
+ const rowKeys = keyboardNavigation && !!onRowClick;
953
+ const [activeRowKey, setActiveRowKey] = useState(null);
893
954
  const cellDensity = DENSITY_CELL[density];
894
955
  const headDensity = DENSITY_HEAD[density];
895
- const [sorts, setSorts] = useState([]);
956
+ const [ownSorts, setOwnSorts] = useState([]);
957
+ const sorts = sortProp ?? ownSorts;
896
958
  const handleSort = useCallback(
897
959
  (key, shiftKey) => {
898
- setSorts((prev) => {
899
- const existing = prev.findIndex((s) => s.key === key);
900
- if (existing >= 0) {
901
- const entry = prev[existing];
902
- if (entry.dir === "asc") {
903
- const next = [...prev];
904
- next[existing] = { key, dir: "desc" };
905
- return next;
906
- }
907
- return prev.filter((_, i) => i !== existing);
908
- }
909
- if (shiftKey) return [...prev, { key, dir: "asc" }];
910
- return [{ key, dir: "asc" }];
911
- });
960
+ const next = nextSort(sorts, key, shiftKey);
961
+ if (sortProp === void 0) setOwnSorts(next);
962
+ onSortChange?.(next);
912
963
  },
913
- []
964
+ [sorts, sortProp, onSortChange]
914
965
  );
915
966
  const [visibleCols, setVisibleCols] = useState(
916
967
  () => {
@@ -950,7 +1001,7 @@ function DataTable({
950
1001
  }, [columnVisibilityKey]);
951
1002
  useEffect(() => {
952
1003
  if (!visibilityHydrated || !columnVisibilityKey) return;
953
- localStorage.setItem(
1004
+ storeSet(
954
1005
  `dt-vis-${columnVisibilityKey}`,
955
1006
  JSON.stringify(visibleCols)
956
1007
  );
@@ -964,8 +1015,9 @@ function DataTable({
964
1015
  defaults[c.key] = c.defaultVisible !== false;
965
1016
  });
966
1017
  setVisibleCols(defaults);
1018
+ setColOrder(null);
967
1019
  if (columnVisibilityKey) {
968
- localStorage.removeItem(`dt-vis-${columnVisibilityKey}`);
1020
+ storeRemove(`dt-vis-${columnVisibilityKey}`);
969
1021
  }
970
1022
  }, [columns, columnVisibilityKey]);
971
1023
  useEffect(() => {
@@ -981,7 +1033,54 @@ function DataTable({
981
1033
  return changed ? updated : prev;
982
1034
  });
983
1035
  }, [columns]);
984
- const activeColumns = columnVisibility ? columns.filter((c) => visibleCols[c.key] !== false) : columns;
1036
+ const [colOrder, setColOrder] = useState(null);
1037
+ const [orderHydrated, setOrderHydrated] = useState(false);
1038
+ useEffect(() => {
1039
+ if (columnOrderKey) {
1040
+ try {
1041
+ const saved = localStorage.getItem(`dt-order-${columnOrderKey}`);
1042
+ const parsed = saved ? JSON.parse(saved) : null;
1043
+ if (Array.isArray(parsed) && parsed.every((k) => typeof k === "string")) {
1044
+ setColOrder(parsed);
1045
+ }
1046
+ } catch {
1047
+ }
1048
+ }
1049
+ setOrderHydrated(true);
1050
+ }, [columnOrderKey]);
1051
+ useEffect(() => {
1052
+ if (!orderHydrated || !columnOrderKey) return;
1053
+ if (colOrder) storeSet(`dt-order-${columnOrderKey}`, JSON.stringify(colOrder));
1054
+ else storeRemove(`dt-order-${columnOrderKey}`);
1055
+ }, [colOrder, columnOrderKey, orderHydrated]);
1056
+ const orderedColumns = useMemo(() => {
1057
+ if (!colOrder) return columns;
1058
+ const rank = new Map(colOrder.map((k, i) => [k, i]));
1059
+ return [...columns].sort(
1060
+ (a, b) => (rank.get(a.key) ?? colOrder.length) - (rank.get(b.key) ?? colOrder.length)
1061
+ );
1062
+ }, [columns, colOrder]);
1063
+ const moveColumn = useCallback(
1064
+ (key, to) => {
1065
+ const keys = orderedColumns.map((c) => c.key);
1066
+ const from = keys.indexOf(key);
1067
+ if (from < 0) return;
1068
+ keys.splice(from, 1);
1069
+ const at = "before" in to ? Math.max(0, keys.indexOf(to.before)) : keys.indexOf(to.after) + 1;
1070
+ keys.splice(at, 0, key);
1071
+ setColOrder(keys);
1072
+ },
1073
+ [orderedColumns]
1074
+ );
1075
+ const [dragKey, setDragKey] = useState(null);
1076
+ const visibleColumns = columnVisibility ? orderedColumns.filter((c) => visibleCols[c.key] !== false) : orderedColumns;
1077
+ const leftKeys = pinnedColumns?.left ?? (stickyFirstColumn && visibleColumns[0] ? [visibleColumns[0].key] : []);
1078
+ const rightKeys = pinnedColumns?.right ?? [];
1079
+ const activeColumns = [
1080
+ ...visibleColumns.filter((c) => leftKeys.includes(c.key)),
1081
+ ...visibleColumns.filter((c) => !leftKeys.includes(c.key) && !rightKeys.includes(c.key)),
1082
+ ...visibleColumns.filter((c) => rightKeys.includes(c.key))
1083
+ ];
985
1084
  const [colWidths, setColWidths] = useState(() => {
986
1085
  if (resizeKey) {
987
1086
  try {
@@ -1008,46 +1107,236 @@ function DataTable({
1008
1107
  );
1009
1108
  const handleResizeEnd = useCallback(() => {
1010
1109
  if (resizeKey) {
1011
- localStorage.setItem(`dt-resize-${resizeKey}`, JSON.stringify(colWidths));
1110
+ storeSet(`dt-resize-${resizeKey}`, JSON.stringify(colWidths));
1012
1111
  }
1013
1112
  }, [resizeKey, colWidths]);
1113
+ const pinWidth = (key) => colWidths[key] ?? PINNED_WIDTH;
1114
+ const pinned = {};
1115
+ {
1116
+ let left = selectable ? 40 : 0;
1117
+ for (const c of activeColumns) {
1118
+ if (!leftKeys.includes(c.key)) continue;
1119
+ pinned[c.key] = { side: "left", offset: left };
1120
+ left += pinWidth(c.key);
1121
+ }
1122
+ let right = renderActions ? 40 : 0;
1123
+ for (const c of [...activeColumns].reverse()) {
1124
+ if (!rightKeys.includes(c.key)) continue;
1125
+ pinned[c.key] = { side: "right", offset: right };
1126
+ right += pinWidth(c.key);
1127
+ }
1128
+ }
1129
+ const pinProps = (key, header) => {
1130
+ const z = header ? stickyHeader ? "z-[15]" : "z-[5]" : "z-[1]";
1131
+ if (key === "__select") {
1132
+ return leftKeys.length ? { className: cn("sticky left-0 bg-background", z), style: { left: 0 } } : null;
1133
+ }
1134
+ if (key === "__actions") {
1135
+ return rightKeys.length ? { className: cn("sticky right-0 bg-background", z), style: { right: 0 } } : null;
1136
+ }
1137
+ const p = pinned[key];
1138
+ return p ? { className: cn("sticky bg-background", z), style: { [p.side]: p.offset } } : null;
1139
+ };
1014
1140
  const [currentPage, setCurrentPage] = useState(0);
1015
1141
  const [currentPageSize, setCurrentPageSize] = useState(initialPageSize);
1016
1142
  useEffect(() => {
1017
1143
  setCurrentPage(0);
1018
1144
  }, [items.length, currentPageSize]);
1019
- const sorted = (() => {
1020
- if (sorts.length === 0) return items;
1021
- return [...items].sort((a, b) => {
1022
- for (const s of sorts) {
1023
- const col = columns.find((c) => c.key === s.key);
1024
- if (!col) continue;
1025
- const acc = col.accessor || ((item) => item[col.key]);
1026
- const va = acc(a);
1027
- const vb = acc(b);
1028
- const sa = va == null ? "" : String(va);
1029
- const sb = vb == null ? "" : String(vb);
1030
- const cmp = sa.localeCompare(sb, void 0, {
1031
- numeric: true,
1032
- sensitivity: "base"
1033
- });
1034
- if (cmp !== 0) return s.dir === "asc" ? cmp : -cmp;
1035
- }
1036
- return 0;
1037
- });
1038
- })();
1039
- const totalItems = sorted.length;
1145
+ const sorting = useMemo(
1146
+ () => sorts.filter((s) => columns.some((c) => c.key === s.key)).map((s) => ({ id: s.key, desc: s.dir === "desc" })),
1147
+ [sorts, columns]
1148
+ );
1149
+ const tanstackColumns = useMemo(
1150
+ () => columns.map((col) => ({
1151
+ id: col.key,
1152
+ // Blanks read as undefined here, so `sortUndefined` keeps them last
1153
+ // whichever way the column sorts.
1154
+ accessorFn: (row) => {
1155
+ const v = col.accessor ? col.accessor(row) : row[col.key];
1156
+ return v == null || v === "" ? void 0 : v;
1157
+ },
1158
+ sortUndefined: "last",
1159
+ sortingFn: (a, b, id) => compareValues(a.getValue(id), b.getValue(id))
1160
+ })),
1161
+ [columns]
1162
+ );
1163
+ const table = useReactTable({
1164
+ data: items,
1165
+ columns: tanstackColumns,
1166
+ getRowId: (row) => getKey(row),
1167
+ state: {
1168
+ sorting,
1169
+ pagination: { pageIndex: currentPage, pageSize: currentPageSize }
1170
+ },
1171
+ enableMultiSort: true,
1172
+ getCoreRowModel: getCoreRowModel(),
1173
+ getSortedRowModel: getSortedRowModel(),
1174
+ ...pagination ? { getPaginationRowModel: getPaginationRowModel() } : {}
1175
+ });
1176
+ const totalItems = table.getPrePaginationRowModel().rows.length;
1040
1177
  const totalPages = pagination ? Math.max(1, Math.ceil(totalItems / currentPageSize)) : 1;
1041
- const displayItems = pagination ? sorted.slice(
1042
- currentPage * currentPageSize,
1043
- (currentPage + 1) * currentPageSize
1044
- ) : sorted;
1178
+ const displayItems = table.getRowModel().rows.map((row) => row.original);
1179
+ const scrollRef = useRef(null);
1180
+ const bodyRef = useRef(null);
1181
+ const virtualizing = !!virtualize && !pagination;
1182
+ const virtualOptions = typeof virtualize === "object" ? virtualize : {};
1183
+ const viewportHeight = virtualOptions.height ?? 600;
1184
+ const rowVirtualizer = useVirtualizer({
1185
+ count: virtualizing ? displayItems.length : 0,
1186
+ getScrollElement: () => scrollRef.current,
1187
+ estimateSize: () => virtualOptions.estimateRowHeight ?? ROW_HEIGHT[density],
1188
+ overscan: 10,
1189
+ initialRect: { width: 0, height: viewportHeight }
1190
+ });
1191
+ const virtualRows = virtualizing ? rowVirtualizer.getVirtualItems() : null;
1192
+ const focusRow = (index) => {
1193
+ const last = displayItems.length - 1;
1194
+ const target = Math.max(0, Math.min(last, index));
1195
+ if (focusRowAt(bodyRef.current, target)) return;
1196
+ if (!virtualizing) return;
1197
+ rowVirtualizer.scrollToIndex(target);
1198
+ requestAnimationFrame(() => focusRowAt(bodyRef.current, target));
1199
+ };
1200
+ const openKey = !rowKeys ? null : openColumn !== void 0 ? openColumn : activeColumns.find((c) => !c.editable)?.key ?? null;
1201
+ const renderedItems = virtualRows ? virtualRows.map((vr) => displayItems[vr.index]).filter(Boolean) : displayItems;
1202
+ const scrollTop = rowVirtualizer.scrollOffset ?? 0;
1203
+ const firstInView = virtualRows ? virtualRows.find((vr) => vr.start >= scrollTop) ?? virtualRows.find((vr) => vr.end > scrollTop) ?? virtualRows[0] : void 0;
1204
+ const fallbackItem = firstInView ? displayItems[firstInView.index] : renderedItems[0];
1205
+ const tabStopKey = activeRowKey && renderedItems.some((it) => getKey(it) === activeRowKey) ? activeRowKey : fallbackItem ? getKey(fallbackItem) : null;
1206
+ const rowLabel = (item, id) => {
1207
+ const col = activeColumns.find((c) => c.key === (openKey ?? activeColumns[0]?.key));
1208
+ const value = col ? (col.accessor ?? ((it) => it[col.key]))(item) : void 0;
1209
+ return typeof value === "string" || typeof value === "number" ? String(value) : id;
1210
+ };
1045
1211
  const showingFrom = totalItems === 0 ? 0 : currentPage * currentPageSize + 1;
1046
1212
  const showingTo = Math.min(
1047
1213
  (currentPage + 1) * currentPageSize,
1048
1214
  totalItems
1049
1215
  );
1050
1216
  const totalCols = activeColumns.length + (selectable ? 1 : 0) + (renderActions ? 1 : 0);
1217
+ const renderRow = (item, index, measure) => {
1218
+ const id = getKey(item);
1219
+ return /* @__PURE__ */ jsxs(
1220
+ TableRow,
1221
+ {
1222
+ ref: measure,
1223
+ "data-index": index,
1224
+ "aria-rowindex": virtualizing ? index + 2 : void 0,
1225
+ className: cn(
1226
+ onRowClick && "cursor-pointer",
1227
+ "focus-within:bg-muted/40"
1228
+ ),
1229
+ "data-state": selected?.has(id) ? "selected" : void 0,
1230
+ onClick: onRowClick ? () => onRowClick(item) : void 0,
1231
+ children: [
1232
+ selectable && /* @__PURE__ */ jsx(
1233
+ TableCell,
1234
+ {
1235
+ className: cn(cellDensity, pinProps("__select", false)?.className),
1236
+ style: pinProps("__select", false)?.style,
1237
+ onClick: (e) => e.stopPropagation(),
1238
+ children: /* @__PURE__ */ jsx(
1239
+ Checkbox,
1240
+ {
1241
+ "aria-label": `Select ${rowLabel(item, id)}`,
1242
+ checked: selected?.has(id),
1243
+ onCheckedChange: () => onToggleSelect?.(id)
1244
+ }
1245
+ )
1246
+ }
1247
+ ),
1248
+ activeColumns.map((col, colIndex) => {
1249
+ const acc = col.accessor || ((it) => it[col.key]);
1250
+ const value = acc(item);
1251
+ const pin = pinProps(col.key, false);
1252
+ return /* @__PURE__ */ jsx(
1253
+ TableCell,
1254
+ {
1255
+ className: cn(
1256
+ getCellClasses(
1257
+ col
1258
+ ),
1259
+ cellDensity,
1260
+ pin?.className
1261
+ ),
1262
+ style: pin?.style,
1263
+ children: (() => {
1264
+ const content = col.editable ? /* @__PURE__ */ jsx(
1265
+ EditableCell,
1266
+ {
1267
+ item,
1268
+ column: col,
1269
+ value
1270
+ }
1271
+ ) : col.render ? col.render(item, value) : /* @__PURE__ */ jsx("span", { className: "truncate block max-w-[300px]", children: String(value ?? "\u2014") });
1272
+ if (col.key !== openKey) return content;
1273
+ const openProps = {
1274
+ type: "button",
1275
+ [ROW_OPEN]: String(index),
1276
+ tabIndex: id === tabStopKey ? 0 : -1,
1277
+ onFocus: () => setActiveRowKey(id),
1278
+ onClick: (e) => {
1279
+ e.stopPropagation();
1280
+ onRowClick?.(item);
1281
+ },
1282
+ onKeyDown: (e) => {
1283
+ if (e.target !== e.currentTarget) return;
1284
+ if (e.metaKey || e.ctrlKey || e.altKey) return;
1285
+ const move = (to) => {
1286
+ e.preventDefault();
1287
+ focusRow(to);
1288
+ };
1289
+ if (e.key === "ArrowDown" || e.key === "j") move(index + 1);
1290
+ else if (e.key === "ArrowUp" || e.key === "k") move(index - 1);
1291
+ else if (e.key === "Home") move(0);
1292
+ else if (e.key === "End") move(displayItems.length - 1);
1293
+ else if (e.key === "x" && selectable) {
1294
+ e.preventDefault();
1295
+ onToggleSelect?.(id);
1296
+ }
1297
+ }
1298
+ };
1299
+ if (!col.editable && !col.render) {
1300
+ return /* @__PURE__ */ jsx(
1301
+ "button",
1302
+ {
1303
+ ...openProps,
1304
+ className: "-mx-1 block w-[calc(100%+0.5rem)] min-w-0 rounded px-1 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring",
1305
+ children: content
1306
+ }
1307
+ );
1308
+ }
1309
+ return /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-1", children: [
1310
+ /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children: content }),
1311
+ /* @__PURE__ */ jsx(
1312
+ "button",
1313
+ {
1314
+ ...openProps,
1315
+ "aria-label": `Open ${rowLabel(item, id)}`,
1316
+ className: "sr-only shrink-0 rounded px-1 text-xs text-muted-foreground focus-visible:not-sr-only focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring",
1317
+ children: "Open"
1318
+ }
1319
+ )
1320
+ ] });
1321
+ })()
1322
+ },
1323
+ col.key
1324
+ );
1325
+ }),
1326
+ renderActions && /* @__PURE__ */ jsx(
1327
+ TableCell,
1328
+ {
1329
+ className: cn(cellDensity, pinProps("__actions", false)?.className),
1330
+ style: pinProps("__actions", false)?.style,
1331
+ onClick: (e) => e.stopPropagation(),
1332
+ children: renderActions(item)
1333
+ }
1334
+ )
1335
+ ]
1336
+ },
1337
+ id
1338
+ );
1339
+ };
1051
1340
  return /* @__PURE__ */ jsxs("div", { className: "border rounded-lg animate-fade-in flex flex-col", children: [
1052
1341
  columnVisibility && /* @__PURE__ */ jsx("div", { className: "flex items-center justify-end px-3 py-2 border-b bg-muted/30", children: /* @__PURE__ */ jsxs(DropdownMenu, { children: [
1053
1342
  /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
@@ -1085,151 +1374,181 @@ function DataTable({
1085
1374
  }
1086
1375
  )
1087
1376
  ] }) }),
1088
- /* @__PURE__ */ jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxs(Table, { style: { tableLayout: "fixed" }, children: [
1089
- /* @__PURE__ */ jsxs("colgroup", { children: [
1090
- selectable && /* @__PURE__ */ jsx("col", { style: { width: 40 } }),
1091
- activeColumns.map((col) => {
1092
- const w = colWidths[col.key];
1093
- return /* @__PURE__ */ jsx(
1094
- "col",
1095
- {
1096
- className: colHideClass(col.hideBelow),
1097
- style: w ? { width: w } : void 0
1098
- },
1099
- col.key
1100
- );
1101
- }),
1102
- renderActions && /* @__PURE__ */ jsx("col", { style: { width: 40 } })
1103
- ] }),
1104
- /* @__PURE__ */ jsx(
1105
- TableHeader,
1106
- {
1107
- className: stickyHeader ? "sticky top-0 z-10 bg-background" : "",
1108
- children: /* @__PURE__ */ jsxs(TableRow, { children: [
1109
- selectable && /* @__PURE__ */ jsx(TableHead, { className: cn("w-10", headDensity), children: /* @__PURE__ */ jsx(
1110
- Checkbox,
1111
- {
1112
- checked: items.length > 0 && selected?.size === items.length,
1113
- onCheckedChange: onToggleAll
1114
- }
1115
- ) }),
1116
- activeColumns.map((col, colIndex) => {
1117
- const sortIndex = sorts.findIndex((s) => s.key === col.key);
1118
- const sortEntry = sortIndex >= 0 ? sorts[sortIndex] : null;
1119
- const resizeWidth = colWidths[col.key];
1120
- const isFirstCol = colIndex === 0 && stickyFirstColumn;
1121
- return /* @__PURE__ */ jsxs(
1122
- TableHead,
1123
- {
1124
- className: cn(
1125
- getCellClasses(col),
1126
- headDensity,
1127
- col.sortable && "cursor-pointer select-none",
1128
- "overflow-hidden",
1129
- resizableColumns && "relative",
1130
- isFirstCol && "sticky left-0 bg-background",
1131
- isFirstCol && stickyHeader ? "z-[15]" : isFirstCol ? "z-[5]" : void 0
1132
- ),
1133
- style: resizeWidth ? { width: resizeWidth, minWidth: resizeWidth } : void 0,
1134
- onClick: col.sortable ? (e) => handleSort(col.key, e.shiftKey) : void 0,
1135
- children: [
1136
- /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 items-center gap-1", children: [
1137
- /* @__PURE__ */ jsx("span", { className: "truncate", title: col.header, children: col.header }),
1138
- col.sortable && (sortEntry ? /* @__PURE__ */ jsxs(Fragment, { children: [
1139
- sortEntry.dir === "asc" ? /* @__PURE__ */ jsx(ArrowUp, { className: "h-3 w-3 shrink-0 text-foreground" }) : /* @__PURE__ */ jsx(ArrowDown, { className: "h-3 w-3 shrink-0 text-foreground" }),
1140
- sorts.length > 1 && /* @__PURE__ */ jsx("span", { className: "text-[10px] text-muted-foreground font-mono", children: sortIndex + 1 })
1141
- ] }) : /* @__PURE__ */ jsx(ArrowUpDown, { className: "h-3 w-3 shrink-0 text-muted-foreground/50" }))
1142
- ] }),
1143
- resizableColumns && /* @__PURE__ */ jsx(
1144
- ResizeHandle,
1145
- {
1146
- onResize: (delta) => handleResize(
1147
- col.key,
1148
- delta,
1149
- col.minWidth ?? 60
1150
- ),
1151
- onResizeEnd: handleResizeEnd
1152
- }
1153
- )
1154
- ]
1155
- },
1156
- col.key
1157
- );
1158
- }),
1159
- renderActions && /* @__PURE__ */ jsx(TableHead, { className: cn("w-10", headDensity) })
1160
- ] })
1161
- }
1162
- ),
1163
- /* @__PURE__ */ jsx(TableBody, { children: displayItems.length === 0 ? /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(
1164
- TableCell,
1165
- {
1166
- colSpan: totalCols,
1167
- className: "text-center py-12 text-muted-foreground",
1168
- children: emptyMessage
1169
- }
1170
- ) }) : displayItems.map((item) => {
1171
- const id = getKey(item);
1172
- return /* @__PURE__ */ jsxs(
1173
- TableRow,
1377
+ /* @__PURE__ */ jsx(
1378
+ "div",
1379
+ {
1380
+ ref: scrollRef,
1381
+ className: cn("overflow-x-auto", virtualizing && "overflow-y-auto"),
1382
+ style: virtualizing ? { maxHeight: viewportHeight } : void 0,
1383
+ children: /* @__PURE__ */ jsxs(
1384
+ Table,
1174
1385
  {
1175
- className: cn(onRowClick && "cursor-pointer"),
1176
- "data-state": selected?.has(id) ? "selected" : void 0,
1177
- onClick: onRowClick ? () => onRowClick(item) : void 0,
1386
+ style: { tableLayout: "fixed" },
1387
+ "aria-rowcount": virtualizing ? displayItems.length + 1 : void 0,
1178
1388
  children: [
1179
- selectable && /* @__PURE__ */ jsx(
1180
- TableCell,
1181
- {
1182
- className: cellDensity,
1183
- onClick: (e) => e.stopPropagation(),
1184
- children: /* @__PURE__ */ jsx(
1185
- Checkbox,
1389
+ /* @__PURE__ */ jsxs("colgroup", { children: [
1390
+ selectable && /* @__PURE__ */ jsx("col", { style: { width: 40 } }),
1391
+ activeColumns.map((col) => {
1392
+ const w = colWidths[col.key] ?? (pinned[col.key] ? PINNED_WIDTH : void 0);
1393
+ return /* @__PURE__ */ jsx(
1394
+ "col",
1186
1395
  {
1187
- checked: selected?.has(id),
1188
- onCheckedChange: () => onToggleSelect?.(id)
1189
- }
1190
- )
1191
- }
1192
- ),
1193
- activeColumns.map((col, colIndex) => {
1194
- const acc = col.accessor || ((it) => it[col.key]);
1195
- const value = acc(item);
1196
- const isFirstCol = colIndex === 0 && stickyFirstColumn;
1197
- return /* @__PURE__ */ jsx(
1198
- TableCell,
1199
- {
1200
- className: cn(
1201
- getCellClasses(
1202
- col
1203
- ),
1204
- cellDensity,
1205
- isFirstCol && "sticky left-0 z-[1] bg-background"
1396
+ className: colHideClass(col.hideBelow),
1397
+ style: w ? { width: w } : void 0
1398
+ },
1399
+ col.key
1400
+ );
1401
+ }),
1402
+ renderActions && /* @__PURE__ */ jsx("col", { style: { width: 40 } })
1403
+ ] }),
1404
+ /* @__PURE__ */ jsx(
1405
+ TableHeader,
1406
+ {
1407
+ className: stickyHeader ? "sticky top-0 z-10 bg-background" : "",
1408
+ children: /* @__PURE__ */ jsxs(TableRow, { "aria-rowindex": virtualizing ? 1 : void 0, children: [
1409
+ selectable && /* @__PURE__ */ jsx(
1410
+ TableHead,
1411
+ {
1412
+ className: cn("w-10", headDensity, pinProps("__select", true)?.className),
1413
+ style: pinProps("__select", true)?.style,
1414
+ children: /* @__PURE__ */ jsx(
1415
+ Checkbox,
1416
+ {
1417
+ "aria-label": "Select all rows",
1418
+ checked: items.length > 0 && selected?.size === items.length,
1419
+ onCheckedChange: onToggleAll
1420
+ }
1421
+ )
1422
+ }
1206
1423
  ),
1207
- children: col.editable ? /* @__PURE__ */ jsx(
1208
- EditableCell,
1424
+ activeColumns.map((col, colIndex) => {
1425
+ const sortIndex = sorts.findIndex((s) => s.key === col.key);
1426
+ const sortEntry = sortIndex >= 0 ? sorts[sortIndex] : null;
1427
+ const resizeWidth = colWidths[col.key];
1428
+ const pin = pinProps(col.key, true);
1429
+ const draggable = reorderableColumns && !pin;
1430
+ return /* @__PURE__ */ jsxs(
1431
+ TableHead,
1432
+ {
1433
+ className: cn(
1434
+ getCellClasses(col),
1435
+ headDensity,
1436
+ col.sortable && "select-none",
1437
+ "overflow-hidden",
1438
+ resizableColumns && "relative",
1439
+ pin?.className
1440
+ ),
1441
+ style: {
1442
+ ...resizeWidth ? { width: resizeWidth, minWidth: resizeWidth } : {},
1443
+ ...pin?.style
1444
+ },
1445
+ draggable: draggable || void 0,
1446
+ onDragStart: draggable ? (e) => {
1447
+ setDragKey(col.key);
1448
+ e.dataTransfer.effectAllowed = "move";
1449
+ e.dataTransfer.setData("text/plain", col.key);
1450
+ } : void 0,
1451
+ onDragOver: draggable ? (e) => {
1452
+ if (dragKey && dragKey !== col.key) e.preventDefault();
1453
+ } : void 0,
1454
+ onDrop: draggable ? (e) => {
1455
+ e.preventDefault();
1456
+ if (dragKey && dragKey !== col.key) moveColumn(dragKey, { before: col.key });
1457
+ setDragKey(null);
1458
+ } : void 0,
1459
+ onDragEnd: draggable ? () => setDragKey(null) : void 0,
1460
+ "aria-sort": sortEntry && sortIndex === 0 ? sortEntry.dir === "asc" ? "ascending" : "descending" : void 0,
1461
+ children: [
1462
+ (() => {
1463
+ const label = /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 items-center gap-1", children: [
1464
+ /* @__PURE__ */ jsx("span", { className: "truncate", title: col.header, children: col.header }),
1465
+ col.sortable && (sortEntry ? /* @__PURE__ */ jsxs(Fragment, { children: [
1466
+ sortEntry.dir === "asc" ? /* @__PURE__ */ jsx(ArrowUp, { className: "h-3 w-3 shrink-0 text-foreground" }) : /* @__PURE__ */ jsx(ArrowDown, { className: "h-3 w-3 shrink-0 text-foreground" }),
1467
+ sorts.length > 1 && /* @__PURE__ */ jsx(
1468
+ "span",
1469
+ {
1470
+ "aria-hidden": "true",
1471
+ className: "text-[10px] text-muted-foreground font-mono",
1472
+ children: sortIndex + 1
1473
+ }
1474
+ )
1475
+ ] }) : /* @__PURE__ */ jsx(ArrowUpDown, { className: "h-3 w-3 shrink-0 text-muted-foreground/50" }))
1476
+ ] });
1477
+ return col.sortable ? /* @__PURE__ */ jsx(
1478
+ "button",
1479
+ {
1480
+ type: "button",
1481
+ className: "-mx-1 flex w-[calc(100%+0.5rem)] min-w-0 items-center rounded px-1 text-left cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
1482
+ onClick: (e) => handleSort(col.key, e.shiftKey),
1483
+ onKeyDown: (e) => {
1484
+ if (!draggable || !e.altKey) return;
1485
+ if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
1486
+ const movable = activeColumns.filter((c) => !pinned[c.key]);
1487
+ const i = movable.findIndex((c) => c.key === col.key);
1488
+ const neighbour = movable[e.key === "ArrowLeft" ? i - 1 : i + 1];
1489
+ if (!neighbour) return;
1490
+ e.preventDefault();
1491
+ moveColumn(col.key, e.key === "ArrowLeft" ? { before: neighbour.key } : { after: neighbour.key });
1492
+ },
1493
+ "aria-label": sortEntry && sortIndex > 0 ? `${col.header}, sorted ${sortEntry.dir === "asc" ? "ascending" : "descending"}, sort ${sortIndex + 1}` : void 0,
1494
+ children: label
1495
+ }
1496
+ ) : label;
1497
+ })(),
1498
+ resizableColumns && /* @__PURE__ */ jsx(
1499
+ ResizeHandle,
1500
+ {
1501
+ onResize: (delta) => handleResize(
1502
+ col.key,
1503
+ delta,
1504
+ col.minWidth ?? 60
1505
+ ),
1506
+ onResizeEnd: handleResizeEnd
1507
+ }
1508
+ )
1509
+ ]
1510
+ },
1511
+ col.key
1512
+ );
1513
+ }),
1514
+ renderActions && /* @__PURE__ */ jsx(
1515
+ TableHead,
1209
1516
  {
1210
- item,
1211
- column: col,
1212
- value
1517
+ className: cn("w-10", headDensity, pinProps("__actions", true)?.className),
1518
+ style: pinProps("__actions", true)?.style
1213
1519
  }
1214
- ) : col.render ? col.render(item, value) : /* @__PURE__ */ jsx("span", { className: "truncate block max-w-[300px]", children: String(value ?? "\u2014") })
1215
- },
1216
- col.key
1217
- );
1218
- }),
1219
- renderActions && /* @__PURE__ */ jsx(
1520
+ )
1521
+ ] })
1522
+ }
1523
+ ),
1524
+ /* @__PURE__ */ jsx(TableBody, { ref: bodyRef, children: displayItems.length === 0 ? /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(
1220
1525
  TableCell,
1221
1526
  {
1222
- className: cellDensity,
1223
- onClick: (e) => e.stopPropagation(),
1224
- children: renderActions(item)
1527
+ colSpan: totalCols,
1528
+ className: "text-center py-12 text-muted-foreground",
1529
+ children: emptyMessage
1225
1530
  }
1226
- )
1531
+ ) }) : virtualRows ? [
1532
+ /* @__PURE__ */ jsx("tr", { "aria-hidden": true, style: { height: virtualRows[0]?.start ?? 0 } }, "__top"),
1533
+ ...virtualRows.map(
1534
+ (vr) => renderRow(displayItems[vr.index], vr.index, rowVirtualizer.measureElement)
1535
+ ),
1536
+ /* @__PURE__ */ jsx(
1537
+ "tr",
1538
+ {
1539
+ "aria-hidden": true,
1540
+ style: {
1541
+ height: rowVirtualizer.getTotalSize() - (virtualRows[virtualRows.length - 1]?.end ?? 0)
1542
+ }
1543
+ },
1544
+ "__bottom"
1545
+ )
1546
+ ] : displayItems.map((item, index) => renderRow(item, index)) })
1227
1547
  ]
1228
- },
1229
- id
1230
- );
1231
- }) })
1232
- ] }) }),
1548
+ }
1549
+ )
1550
+ }
1551
+ ),
1233
1552
  pagination && /* @__PURE__ */ jsxs("div", { className: "sticky bottom-0 z-10 flex flex-col sm:flex-row items-center justify-between gap-2 px-4 py-3 border-t bg-muted/30 text-xs text-muted-foreground", children: [
1234
1553
  /* @__PURE__ */ jsx("span", { children: totalItems === 0 ? "No records" : `Showing ${showingFrom}\u2013${showingTo} of ${totalItems}` }),
1235
1554
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
@@ -1477,6 +1796,7 @@ var statusColorMap = {
1477
1796
  primary: { color: "text-primary", bgColor: "bg-primary/15" }
1478
1797
  };
1479
1798
  var DRAG_TYPE = "application/x-genesis-kanban-item";
1799
+ var useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
1480
1800
  function KanbanBoard({
1481
1801
  columns,
1482
1802
  renderCard,
@@ -1489,112 +1809,301 @@ function KanbanBoard({
1489
1809
  }) {
1490
1810
  const [dragging, setDragging] = useState(null);
1491
1811
  const [over, setOver] = useState(null);
1812
+ const [grabbed, setGrabbed] = useState(null);
1813
+ const { message: announcement, announce } = useAnnouncer();
1814
+ const handles = useRef(/* @__PURE__ */ new Map());
1815
+ const refocus = useRef(null);
1816
+ const instructionsId = useId();
1492
1817
  const keyOf = (item, index) => getKey ? getKey(item) : String(index);
1493
- const move = (item, to) => {
1494
- void Promise.resolve(onMoveItem?.(item, to)).catch(() => {
1495
- });
1818
+ const movable = Boolean(onMoveItem && getKey);
1819
+ const find = (key) => {
1820
+ if (!getKey) return null;
1821
+ for (const column of columns) {
1822
+ const item = column.items.find((i) => getKey(i) === key);
1823
+ if (item !== void 0) return { item, column };
1824
+ }
1825
+ return null;
1826
+ };
1827
+ const labelOf = (key) => {
1828
+ const found = find(key);
1829
+ return found && getItemLabel ? getItemLabel(found.item) : "Card";
1830
+ };
1831
+ const columnLabel = (key) => columns.find((c) => c.key === key)?.label ?? key;
1832
+ const move = (item, to, name) => {
1833
+ let result;
1834
+ try {
1835
+ result = onMoveItem?.(item, to);
1836
+ } catch (error) {
1837
+ result = Promise.reject(error);
1838
+ }
1839
+ void Promise.resolve(result).then(
1840
+ () => announce(`${name} moved to ${columnLabel(to)}.`),
1841
+ () => announce(`Couldn\u2019t move ${name}.`)
1842
+ );
1843
+ };
1844
+ const moveKey = (key, to) => {
1845
+ const found = find(key);
1846
+ if (!found || found.column.key === to) return;
1847
+ move(found.item, to, labelOf(key));
1496
1848
  };
1497
1849
  const onDrop = (event, to) => {
1498
1850
  event.preventDefault();
1499
1851
  setOver(null);
1500
1852
  const key = event.dataTransfer.getData(DRAG_TYPE) || dragging?.key;
1501
1853
  setDragging(null);
1502
- if (!key || !getKey) return;
1503
- for (const column of columns) {
1504
- const item = column.items.find((i) => getKey(i) === key);
1505
- if (item) {
1506
- if (column.key !== to) move(item, to);
1507
- return;
1854
+ if (key) moveKey(key, to);
1855
+ };
1856
+ const pickUp = (key, from) => {
1857
+ setGrabbed({ key, from, target: from });
1858
+ announce(
1859
+ `${labelOf(key)} picked up in ${columnLabel(from)}. Use the left and right arrow keys to choose a column, Space to drop, Escape to cancel.`
1860
+ );
1861
+ };
1862
+ const aim = (index) => {
1863
+ if (!grabbed) return;
1864
+ const column = columns[Math.max(0, Math.min(columns.length - 1, index))];
1865
+ if (!column || column.key === grabbed.target) return;
1866
+ setGrabbed({ ...grabbed, target: column.key });
1867
+ announce(`Over ${column.label}, column ${columns.indexOf(column) + 1} of ${columns.length}.`);
1868
+ };
1869
+ const drop = () => {
1870
+ if (!grabbed) return;
1871
+ setGrabbed(null);
1872
+ if (grabbed.target === grabbed.from) {
1873
+ announce(`${labelOf(grabbed.key)} dropped back in ${columnLabel(grabbed.from)}.`);
1874
+ return;
1875
+ }
1876
+ refocus.current = grabbed.key;
1877
+ moveKey(grabbed.key, grabbed.target);
1878
+ };
1879
+ const cancel = () => {
1880
+ if (!grabbed) return;
1881
+ setGrabbed(null);
1882
+ announce(`Move cancelled. ${labelOf(grabbed.key)} stays in ${columnLabel(grabbed.from)}.`);
1883
+ };
1884
+ useEffect(() => {
1885
+ if (!grabbed) return;
1886
+ const onKey = (event) => {
1887
+ if (event.key !== "Escape") return;
1888
+ event.preventDefault();
1889
+ event.stopPropagation();
1890
+ cancel();
1891
+ };
1892
+ window.addEventListener("keydown", onKey, true);
1893
+ return () => window.removeEventListener("keydown", onKey, true);
1894
+ });
1895
+ useEffect(() => {
1896
+ if (grabbed && (!movable || !find(grabbed.key))) setGrabbed(null);
1897
+ if (dragging && (!movable || !find(dragging.key))) {
1898
+ setDragging(null);
1899
+ setOver(null);
1900
+ }
1901
+ });
1902
+ useIsomorphicLayoutEffect(() => {
1903
+ const key = refocus.current;
1904
+ if (!key) return;
1905
+ const handle = handles.current.get(key);
1906
+ if (!handle) return;
1907
+ if (handle.isConnected && document.activeElement === handle) {
1908
+ refocus.current = null;
1909
+ return;
1910
+ }
1911
+ const active = document.activeElement;
1912
+ if (active === null || active === document.body) {
1913
+ handle.focus();
1914
+ refocus.current = null;
1915
+ }
1916
+ });
1917
+ const onHandleKeyDown = (event, key, from) => {
1918
+ const delta = event.key === "ArrowLeft" ? -1 : event.key === "ArrowRight" ? 1 : 0;
1919
+ if (grabbed?.key === key) {
1920
+ const index = columns.findIndex((c) => c.key === grabbed.target);
1921
+ if (delta) {
1922
+ event.preventDefault();
1923
+ aim(index + delta);
1924
+ } else if (event.key === "Home" || event.key === "End") {
1925
+ event.preventDefault();
1926
+ aim(event.key === "Home" ? 0 : columns.length - 1);
1927
+ }
1928
+ return;
1929
+ }
1930
+ if (delta && event.altKey && !grabbed) {
1931
+ event.preventDefault();
1932
+ const to = columns[columns.findIndex((c) => c.key === from) + delta];
1933
+ if (to) {
1934
+ refocus.current = key;
1935
+ moveKey(key, to.key);
1508
1936
  }
1509
1937
  }
1510
1938
  };
1511
- const movable = Boolean(onMoveItem && getKey);
1512
- return /* @__PURE__ */ jsx(
1513
- "div",
1514
- {
1515
- className: cn(
1516
- "grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4",
1517
- className
1518
- ),
1519
- children: columns.map((column) => {
1520
- const resolved = column.status ? statusColorMap[column.status] : { color: column.color ?? "", bgColor: column.bgColor ?? "" };
1521
- return /* @__PURE__ */ jsxs(
1522
- "section",
1523
- {
1524
- "aria-label": column.label,
1525
- "data-kanban-column": column.key,
1526
- className: cn(
1527
- "flex flex-col gap-3 rounded-lg",
1528
- movable && over === column.key && dragging?.from !== column.key && "bg-accent/40 ring-2 ring-primary/40"
1529
- ),
1530
- onDragOver: movable ? (e) => {
1531
- if (!dragging) return;
1532
- e.preventDefault();
1533
- e.dataTransfer.dropEffect = "move";
1534
- if (over !== column.key) setOver(column.key);
1535
- } : void 0,
1536
- onDragLeave: movable ? (e) => {
1537
- if (!e.currentTarget.contains(e.relatedTarget)) setOver(null);
1538
- } : void 0,
1539
- onDrop: movable ? (e) => onDrop(e, column.key) : void 0,
1540
- children: [
1541
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1542
- /* @__PURE__ */ jsx("span", { className: cn("text-sm font-semibold", resolved.color), children: column.label }),
1543
- /* @__PURE__ */ jsx(
1544
- Badge,
1545
- {
1546
- variant: "secondary",
1547
- className: cn("text-[10px] px-1.5 py-0", resolved.bgColor),
1548
- children: column.total ?? column.items.length
1549
- }
1550
- )
1551
- ] }),
1552
- /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2 min-h-[80px]", children: column.items.length === 0 ? /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center rounded-lg border border-dashed border-border p-6 text-xs text-muted-foreground", children: emptyMessage }) : column.items.map((item, i) => {
1553
- const key = keyOf(item, i);
1554
- return /* @__PURE__ */ jsxs(
1555
- "div",
1556
- {
1557
- className: cn("animate-fade-in", movable && "group relative", dragging?.key === key && "opacity-50"),
1558
- style: { animationDelay: `${i * 30}ms`, animationFillMode: "both" },
1559
- draggable: movable || void 0,
1560
- onDragStart: movable ? (e) => {
1561
- e.dataTransfer.setData(DRAG_TYPE, key);
1562
- e.dataTransfer.effectAllowed = "move";
1563
- setDragging({ key, from: column.key });
1564
- } : void 0,
1565
- onDragEnd: movable ? () => {
1566
- setDragging(null);
1567
- setOver(null);
1568
- } : void 0,
1569
- children: [
1570
- renderCard(item),
1571
- movable && columns.length > 1 && /* @__PURE__ */ jsxs(DropdownMenu, { children: [
1572
- /* @__PURE__ */ jsx(
1573
- DropdownMenuTrigger,
1939
+ const onHandleClick = (event, key, from) => {
1940
+ if (event.detail !== 0) return;
1941
+ if (grabbed?.key === key) drop();
1942
+ else if (!grabbed) pickUp(key, from);
1943
+ };
1944
+ const columnAt = (x, y) => (typeof document.elementFromPoint === "function" ? document.elementFromPoint(x, y) : null)?.closest("[data-kanban-column]")?.dataset.kanbanColumn ?? null;
1945
+ const onHandlePointerDown = (event, key, from) => {
1946
+ if (event.pointerType === "mouse" || grabbed || dragging) return;
1947
+ event.currentTarget.setPointerCapture?.(event.pointerId);
1948
+ setDragging({ key, from, pointer: event.pointerId });
1949
+ announce(`${labelOf(key)} picked up in ${columnLabel(from)}.`);
1950
+ };
1951
+ const onHandlePointerMove = (event) => {
1952
+ if (dragging?.pointer !== event.pointerId) return;
1953
+ const to = columnAt(event.clientX, event.clientY);
1954
+ if (to !== over) setOver(to);
1955
+ };
1956
+ const onHandlePointerUp = (event) => {
1957
+ if (dragging?.pointer !== event.pointerId) return;
1958
+ const to = columnAt(event.clientX, event.clientY) ?? over;
1959
+ const { key, from } = dragging;
1960
+ setDragging(null);
1961
+ setOver(null);
1962
+ if (to && to !== from) moveKey(key, to);
1963
+ else announce(`${labelOf(key)} dropped back in ${columnLabel(from)}.`);
1964
+ };
1965
+ const onHandlePointerCancel = (event) => {
1966
+ if (dragging?.pointer !== event.pointerId) return;
1967
+ setDragging(null);
1968
+ setOver(null);
1969
+ };
1970
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1971
+ /* @__PURE__ */ jsx(
1972
+ "div",
1973
+ {
1974
+ className: cn(
1975
+ "grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4",
1976
+ className
1977
+ ),
1978
+ children: columns.map((column) => {
1979
+ const resolved = column.status ? statusColorMap[column.status] : { color: column.color ?? "", bgColor: column.bgColor ?? "" };
1980
+ return /* @__PURE__ */ jsxs(
1981
+ "section",
1982
+ {
1983
+ "aria-label": column.label,
1984
+ "data-kanban-column": column.key,
1985
+ className: cn(
1986
+ "flex flex-col gap-3 rounded-lg",
1987
+ movable && (dragging && over === column.key && dragging.from !== column.key || grabbed && grabbed.target === column.key && grabbed.from !== column.key) && "bg-accent/40 ring-2 ring-primary/40"
1988
+ ),
1989
+ onDragOver: movable ? (e) => {
1990
+ if (!dragging || dragging.pointer !== void 0) return;
1991
+ e.preventDefault();
1992
+ e.dataTransfer.dropEffect = "move";
1993
+ if (over !== column.key) setOver(column.key);
1994
+ } : void 0,
1995
+ onDragLeave: movable ? (e) => {
1996
+ if (!e.currentTarget.contains(e.relatedTarget)) setOver(null);
1997
+ } : void 0,
1998
+ onDrop: movable ? (e) => onDrop(e, column.key) : void 0,
1999
+ children: [
2000
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
2001
+ /* @__PURE__ */ jsx("span", { className: cn("text-sm font-semibold", resolved.color), children: column.label }),
2002
+ /* @__PURE__ */ jsx(
2003
+ Badge,
2004
+ {
2005
+ variant: "secondary",
2006
+ className: cn("text-[10px] px-1.5 py-0", resolved.bgColor),
2007
+ children: column.total ?? column.items.length
2008
+ }
2009
+ )
2010
+ ] }),
2011
+ /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2 min-h-[80px]", children: column.items.length === 0 ? /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center rounded-lg border border-dashed border-border p-6 text-xs text-muted-foreground", children: emptyMessage }) : column.items.map((item, i) => {
2012
+ const key = keyOf(item, i);
2013
+ const name = getItemLabel?.(item) ?? "card";
2014
+ const isGrabbed = grabbed?.key === key;
2015
+ return /* @__PURE__ */ jsxs(
2016
+ "div",
2017
+ {
2018
+ className: cn(
2019
+ "animate-fade-in",
2020
+ movable && "group relative rounded-lg",
2021
+ dragging?.key === key && "opacity-50",
2022
+ isGrabbed && "ring-2 ring-primary/50"
2023
+ ),
2024
+ style: { animationDelay: `${i * 30}ms`, animationFillMode: "both" },
2025
+ draggable: movable || void 0,
2026
+ onDragStart: movable ? (e) => {
2027
+ e.dataTransfer.setData(DRAG_TYPE, key);
2028
+ e.dataTransfer.effectAllowed = "move";
2029
+ setDragging({ key, from: column.key });
2030
+ } : void 0,
2031
+ onDragEnd: movable ? () => {
2032
+ setDragging(null);
2033
+ setOver(null);
2034
+ } : void 0,
2035
+ children: [
2036
+ renderCard(item),
2037
+ movable && columns.length > 1 && /* @__PURE__ */ jsx(
2038
+ "button",
1574
2039
  {
1575
- "aria-label": `Move ${getItemLabel?.(item) ?? "card"}`,
1576
- className: "absolute right-1.5 top-1.5 rounded p-1 text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
1577
- children: /* @__PURE__ */ jsx(ArrowRightLeft, { className: "h-3.5 w-3.5", "aria-hidden": true })
2040
+ type: "button",
2041
+ ref: (node) => {
2042
+ if (node) handles.current.set(key, node);
2043
+ else handles.current.delete(key);
2044
+ },
2045
+ "aria-label": `Pick up ${name}`,
2046
+ "aria-describedby": instructionsId,
2047
+ "data-grabbed": isGrabbed || void 0,
2048
+ className: cn(
2049
+ "absolute right-8 top-1.5 cursor-grab touch-none rounded p-1 text-muted-foreground",
2050
+ "hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
2051
+ isGrabbed && "bg-accent text-foreground"
2052
+ ),
2053
+ onClick: (e) => onHandleClick(e, key, column.key),
2054
+ onKeyDown: (e) => onHandleKeyDown(e, key, column.key),
2055
+ onBlur: () => {
2056
+ if (isGrabbed) cancel();
2057
+ },
2058
+ onPointerDown: (e) => onHandlePointerDown(e, key, column.key),
2059
+ onPointerMove: onHandlePointerMove,
2060
+ onPointerUp: onHandlePointerUp,
2061
+ onPointerCancel: onHandlePointerCancel,
2062
+ children: /* @__PURE__ */ jsx(GripVertical, { className: "h-3.5 w-3.5", "aria-hidden": true })
1578
2063
  }
1579
2064
  ),
1580
- /* @__PURE__ */ jsxs(DropdownMenuContent, { align: "end", children: [
1581
- /* @__PURE__ */ jsx(DropdownMenuLabel, { children: "Move to" }),
1582
- columns.filter((c) => c.key !== column.key).map((c) => /* @__PURE__ */ jsx(DropdownMenuItem, { onSelect: () => move(item, c.key), children: c.label }, c.key))
2065
+ movable && columns.length > 1 && /* @__PURE__ */ jsxs(DropdownMenu, { children: [
2066
+ /* @__PURE__ */ jsx(
2067
+ DropdownMenuTrigger,
2068
+ {
2069
+ "aria-label": `Move ${name}`,
2070
+ className: "absolute right-1.5 top-1.5 rounded p-1 text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
2071
+ children: /* @__PURE__ */ jsx(ArrowRightLeft, { className: "h-3.5 w-3.5", "aria-hidden": true })
2072
+ }
2073
+ ),
2074
+ /* @__PURE__ */ jsxs(DropdownMenuContent, { align: "end", children: [
2075
+ /* @__PURE__ */ jsx(DropdownMenuLabel, { children: "Move to" }),
2076
+ columns.filter((c) => c.key !== column.key).map((c) => /* @__PURE__ */ jsx(
2077
+ DropdownMenuItem,
2078
+ {
2079
+ onSelect: () => {
2080
+ refocus.current = key;
2081
+ move(item, c.key, getItemLabel?.(item) ?? "Card");
2082
+ },
2083
+ children: c.label
2084
+ },
2085
+ c.key
2086
+ ))
2087
+ ] })
1583
2088
  ] })
1584
- ] })
1585
- ]
1586
- },
1587
- key
1588
- );
1589
- }) }),
1590
- renderColumnFooter?.(column)
1591
- ]
1592
- },
1593
- column.key
1594
- );
1595
- })
1596
- }
1597
- );
2089
+ ]
2090
+ },
2091
+ key
2092
+ );
2093
+ }) }),
2094
+ renderColumnFooter?.(column)
2095
+ ]
2096
+ },
2097
+ column.key
2098
+ );
2099
+ })
2100
+ }
2101
+ ),
2102
+ movable && /* @__PURE__ */ jsxs(Fragment, { children: [
2103
+ /* @__PURE__ */ jsx("p", { id: instructionsId, hidden: true, children: "Space or Enter picks the card up. Left and right arrows, Home and End choose a column. Space or Enter drops it, Escape puts it back. Alt with an arrow moves it one column." }),
2104
+ /* @__PURE__ */ jsx("div", { "aria-live": "polite", "aria-atomic": "true", className: "sr-only", children: announcement })
2105
+ ] })
2106
+ ] });
1598
2107
  }
1599
2108
  function DataList({
1600
2109
  items,
@@ -2713,7 +3222,12 @@ function ResourceForm(props) {
2713
3222
  steps,
2714
3223
  columns = 1,
2715
3224
  prefillFromUrl,
2716
- autoPlaceholder = false
3225
+ autoPlaceholder = false,
3226
+ warnWhenUnsaved = false,
3227
+ onDirtyChange,
3228
+ mutationMode = "pessimistic",
3229
+ undoDelayMs,
3230
+ onUndoable
2717
3231
  } = props;
2718
3232
  const isMobile = useIsMobile();
2719
3233
  const effectiveLayout = isMobile ? "sheet" : layoutProp;
@@ -2731,11 +3245,33 @@ function ResourceForm(props) {
2731
3245
  onOpenChange(false);
2732
3246
  onSuccess?.(data);
2733
3247
  },
2734
- onError,
3248
+ // The form is closed by the time an optimistic or undoable save fails,
3249
+ // so the error has to reach the user some other way.
3250
+ onError: onError ?? (mutationMode === "pessimistic" ? void 0 : showWriteErrorToast),
2735
3251
  prefillFromUrl,
2736
- autoPlaceholder
3252
+ autoPlaceholder,
3253
+ warnWhenUnsaved,
3254
+ // A closed dialog is not touched by a save that fails later.
3255
+ visible: open,
3256
+ mutationMode,
3257
+ undoDelayMs,
3258
+ onUndoable: onUndoable ?? showUndoToast
2737
3259
  };
2738
- const { form, resolvedFields, handleSave, isSaving, isLoading } = useResourceForm(hookOptions);
3260
+ const { form, resolvedFields, handleSave, isSaving, isLoading, isDirty, confirmLeave } = useResourceForm(hookOptions);
3261
+ const onDirtyChangeRef = useRef(onDirtyChange);
3262
+ useEffect(() => {
3263
+ onDirtyChangeRef.current = onDirtyChange;
3264
+ });
3265
+ useEffect(() => {
3266
+ onDirtyChangeRef.current?.(isDirty, confirmLeave);
3267
+ }, [isDirty, confirmLeave]);
3268
+ const requestOpenChange = useCallback(
3269
+ (next) => {
3270
+ if (!next && !confirmLeave()) return;
3271
+ onOpenChange(next);
3272
+ },
3273
+ [confirmLeave, onOpenChange]
3274
+ );
2739
3275
  const bodyProps = {
2740
3276
  form,
2741
3277
  resolvedFields,
@@ -2743,14 +3279,14 @@ function ResourceForm(props) {
2743
3279
  action,
2744
3280
  isSaving,
2745
3281
  isLoading,
2746
- onOpenChange,
3282
+ onOpenChange: requestOpenChange,
2747
3283
  handleSave,
2748
3284
  mode,
2749
3285
  steps,
2750
3286
  columns
2751
3287
  };
2752
3288
  if (effectiveLayout === "dialog") {
2753
- return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxs(DialogContent, { className: "sm:max-w-[560px]", children: [
3289
+ return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange: requestOpenChange, children: /* @__PURE__ */ jsxs(DialogContent, { className: "sm:max-w-[560px]", children: [
2754
3290
  /* @__PURE__ */ jsxs(DialogHeader, { children: [
2755
3291
  /* @__PURE__ */ jsx(DialogTitle, { children: formTitle }),
2756
3292
  /* @__PURE__ */ jsx(DialogDescription, { className: "sr-only", children: formDescription })
@@ -2759,7 +3295,7 @@ function ResourceForm(props) {
2759
3295
  ] }) });
2760
3296
  }
2761
3297
  if (effectiveLayout === "sheet") {
2762
- return /* @__PURE__ */ jsx(Sheet, { open, onOpenChange, children: /* @__PURE__ */ jsxs(SheetContent, { side: "right", className: "sm:max-w-[480px] overflow-y-auto", children: [
3298
+ return /* @__PURE__ */ jsx(Sheet, { open, onOpenChange: requestOpenChange, children: /* @__PURE__ */ jsxs(SheetContent, { side: "right", className: "sm:max-w-[480px] overflow-y-auto", children: [
2763
3299
  /* @__PURE__ */ jsxs(SheetHeader, { children: [
2764
3300
  /* @__PURE__ */ jsx(SheetTitle, { children: formTitle }),
2765
3301
  /* @__PURE__ */ jsx(SheetDescription, { className: "sr-only", children: formDescription })
@@ -2778,7 +3314,8 @@ function ConfirmDeleteDialog({
2778
3314
  onOpenChange,
2779
3315
  onConfirm,
2780
3316
  title = "Delete this record?",
2781
- description = "This can't be undone.",
3317
+ kind = "permanent",
3318
+ description = kind === "recycle" ? "It moves to the recycle bin, where it can be restored." : "This can't be undone.",
2782
3319
  confirmLabel = "Delete",
2783
3320
  cancelLabel = "Cancel"
2784
3321
  }) {
@@ -2847,499 +3384,131 @@ function ConfirmDeleteDialog({
2847
3384
  }
2848
3385
  function ResourcePage(props) {
2849
3386
  const {
2850
- resource: resourceName,
2851
- data: dataProp,
2852
- total: totalProp,
2853
- pagination = "client",
2854
- clientRowLimit = 1e3,
2855
- title,
2856
- subtitle,
2857
- icon,
2858
- columns: columnsProp,
2859
- getKey: getKeyProp,
2860
- detail = "panel",
2861
- detailWidth = "md",
2862
- titleField: titleFieldProp,
2863
- subtitleField: subtitleFieldProp,
2864
- getTitle,
2865
- getSubtitle,
2866
- renderDetail: renderDetailProp,
2867
- detailFields,
2868
- statusFilter,
2869
- searchFields,
2870
- searchPlaceholder,
2871
- initialFilters,
2872
- onFiltersChange,
2873
- filters: filtersProp,
2874
- page: pageProp,
2875
- perPage: perPageProp,
2876
- onPageChange,
2877
- onPerPageChange,
2878
- onSortChange,
2879
- onSearchChange,
2880
- loading: loadingProp,
2881
- error: errorProp,
2882
- errorMessage: errorMessage2,
2883
- onRetry: onRetryProp,
2884
- savedViews,
2885
- onLoadView,
2886
- onSaveView,
2887
- onDeleteView,
3387
+ bulkActions,
3388
+ can,
2888
3389
  canSaveViews,
3390
+ capabilities,
3391
+ className,
2889
3392
  columnVisibility,
2890
3393
  columnVisibilityKey,
2891
- resizableColumns,
2892
- resizeKey,
2893
- stickyHeader,
2894
- stickyFirstColumn,
2895
- pageSizeOptions,
2896
- onCreate,
2897
- onEdit,
2898
- onDelete,
2899
- canEditItem,
2900
3394
  createLabel = "New",
2901
- canEdit,
2902
- canDelete,
2903
- allowDelete,
2904
- confirmDelete,
2905
- onDeleted,
2906
- secondaryActions,
2907
- allowCreate,
2908
- allowEdit,
3395
+ detail = "panel",
3396
+ detailFields,
3397
+ detailWidth = "md",
3398
+ emptyDescription,
3399
+ emptyIcon,
3400
+ emptyMessage,
3401
+ emptyState,
3402
+ errorMessage: errorMessage2,
3403
+ filters: filtersProp,
2909
3404
  formFields,
2910
- renderForm: renderFormProp,
3405
+ formLayout: formLayoutProp,
2911
3406
  formMode,
2912
3407
  formSteps,
2913
- formLayout: formLayoutProp,
2914
- bulkActions,
2915
- renderCard: renderCardProp,
2916
- openOnCardClick = false,
2917
- renderTable: renderTableProp,
2918
3408
  gridCols: gridColsProp,
2919
- keyboardNavigation = true,
2920
- capabilities,
2921
- toolbar,
2922
- emptyState,
2923
- emptyIcon,
2924
- emptyMessage,
2925
- emptyDescription,
2926
3409
  header: headerMode = "page",
2927
- resourceOptions,
2928
- defaultView = "table",
2929
- onNavigate = defaultNavigate,
2930
- viewSettingsKey: viewSettingsKeyProp,
2931
- slots,
3410
+ icon,
3411
+ keyboardNavigation = true,
3412
+ mutationMode: mutationModeProp,
3413
+ onCreate,
3414
+ onDelete,
3415
+ onDeleteView,
3416
+ onEdit,
3417
+ onLoadView,
3418
+ onPageChange,
3419
+ onRetry: onRetryProp,
3420
+ onSaveView,
3421
+ onSearchChange,
3422
+ onSortChange,
3423
+ openOnCardClick = false,
3424
+ pageSizeOptions,
3425
+ pagination = "client",
3426
+ renderCard: renderCardProp,
3427
+ renderDetail: renderDetailProp,
3428
+ renderForm: renderFormProp,
3429
+ renderTable: renderTableProp,
3430
+ resizableColumns,
3431
+ resizeKey,
3432
+ resource: resourceName,
3433
+ savedViews,
3434
+ searchPlaceholder,
3435
+ secondaryActions,
2932
3436
  slotProps,
2933
- className
3437
+ slots,
3438
+ stickyFirstColumn,
3439
+ stickyHeader,
3440
+ subtitle,
3441
+ title,
3442
+ toolbar
2934
3443
  } = props;
2935
- const isClientMode = !!resourceName;
2936
- const resourceServerPaging = isClientMode && pagination === "server";
2937
- const isServerControlled = resourceServerPaging || pageProp !== void 0 || perPageProp !== void 0 || onPageChange !== void 0 || onPerPageChange !== void 0 || onSortChange !== void 0 || onSearchChange !== void 0;
2938
- const viewSettingsKey = viewSettingsKeyProp ?? resourceName ?? title.toLowerCase().replace(/\s+/g, "-");
2939
- const { settings, updateSetting } = useViewSettings(viewSettingsKey);
2940
- const serverSearchFields = resourceOptions?.searchFields ?? searchFields;
2941
- const resourceHook = useResource(
2942
- resourceName ?? "__noop__",
2943
- isClientMode ? {
2944
- ...resourceOptions,
2945
- // Client paging fetches everything it can in one request; server
2946
- // paging starts at the user's stored page size. An explicit
2947
- // `defaultPerPage` wins over both.
2948
- defaultPerPage: resourceOptions?.defaultPerPage ?? (resourceServerPaging ? perPageProp ?? settings.pageSize : clientRowLimit),
2949
- searchFields: resourceServerPaging ? serverSearchFields : resourceOptions?.searchFields,
2950
- // Paging, searching and sorting change the query key; keep the
2951
- // current page on screen while the next one loads.
2952
- keepPreviousData: resourceOptions?.keepPreviousData ?? resourceServerPaging
2953
- } : { enabled: false }
2954
- );
2955
- const effectivePageSize = resourceServerPaging ? resourceHook.params.pagination?.perPage ?? settings.pageSize : perPageProp ?? settings.pageSize;
2956
- const displayData = isClientMode ? resourceHook.list.data?.data ?? [] : dataProp ?? [];
2957
- const displayTotal = isClientMode ? resourceHook.list.data?.total ?? 0 : totalProp ?? 0;
2958
- const isLoading = isClientMode ? resourceHook.list.isLoading : loadingProp ?? false;
2959
- const hasError = isClientMode ? resourceHook.list.isError : errorProp ?? false;
2960
- const page = useResourcePage({
2961
- resource: resourceName,
2962
- data: displayData,
2963
- total: displayTotal,
2964
- columns: columnsProp,
2965
- statusFilter,
2966
- searchFields,
2967
- initialFilters,
2968
- onFiltersChange,
2969
- defaultView,
2970
- detail,
2971
- titleField: titleFieldProp,
2972
- subtitleField: subtitleFieldProp,
2973
- controlled: isServerControlled
2974
- });
2975
3444
  const {
2976
- setSearch: setResourceSearch,
2977
- setSort: setResourceSort,
2978
- setFilters: setResourceFilters
2979
- } = resourceHook;
2980
- const pushedSearchRef = useRef("");
2981
- const searchEffectRanRef = useRef(false);
2982
- useEffect(() => {
2983
- if (!resourceServerPaging) return;
2984
- const firstRun = !searchEffectRanRef.current;
2985
- searchEffectRanRef.current = true;
2986
- if (page.search === pushedSearchRef.current) return;
2987
- const push = () => {
2988
- pushedSearchRef.current = page.search;
2989
- setResourceSearch(page.search);
2990
- };
2991
- if (firstRun || page.search === "") {
2992
- push();
2993
- return;
2994
- }
2995
- const timer = setTimeout(push, 300);
2996
- return () => clearTimeout(timer);
2997
- }, [resourceServerPaging, page.search, setResourceSearch]);
2998
- const defaultSort = resourceOptions?.defaultSort;
2999
- const pushedSortRef = useRef("");
3000
- useEffect(() => {
3001
- if (!resourceServerPaging) return;
3002
- const key = page.sort ? `${page.sort.field}:${page.sort.order}` : "";
3003
- if (key === pushedSortRef.current) return;
3004
- pushedSortRef.current = key;
3005
- setResourceSort(page.sort ?? defaultSort);
3006
- }, [resourceServerPaging, page.sort, defaultSort, setResourceSort]);
3007
- const statusField = statusFilter ? typeof statusFilter === "string" ? statusFilter : statusFilter.field : void 0;
3008
- const defaultFilters = resourceOptions?.defaultFilters;
3009
- const pushedStatusRef = useRef("");
3010
- useEffect(() => {
3011
- if (!resourceServerPaging || !statusField) return;
3012
- const key = JSON.stringify([page.statusValue, defaultFilters ?? null]);
3013
- if (key === pushedStatusRef.current) return;
3014
- pushedStatusRef.current = key;
3015
- setResourceFilters([
3016
- ...defaultFilters ?? [],
3017
- ...page.statusValue.length > 0 ? [{ field: statusField, operator: "in", value: page.statusValue }] : []
3018
- ]);
3019
- }, [
3020
- resourceServerPaging,
3021
- statusField,
3022
- page.statusValue,
3023
- defaultFilters,
3024
- setResourceFilters
3025
- ]);
3026
- const clientTruncated = isClientMode && !resourceServerPaging && !isLoading && !hasError && displayTotal > displayData.length;
3027
- const warnedTruncatedRef = useRef(false);
3028
- useEffect(() => {
3029
- if (!clientTruncated || warnedTruncatedRef.current) return;
3030
- warnedTruncatedRef.current = true;
3031
- console.warn(
3032
- `ResourcePage: "${resourceName}" has ${displayTotal} rows but only ${displayData.length} were fetched. Pass \`pagination="server"\` to page through all of them, or raise \`clientRowLimit\`.`
3033
- );
3034
- }, [clientTruncated, resourceName, displayTotal, displayData.length]);
3035
- const actions = useMemo(() => {
3036
- if (isClientMode) {
3037
- return {
3038
- update: resourceHook.update,
3039
- remove: resourceHook.remove,
3040
- refetch: () => resourceHook.list.refetch()
3041
- };
3042
- }
3043
- return {
3044
- update: async () => {
3045
- throw new Error(
3046
- "ResourceActions.update not available in server-data mode"
3047
- );
3048
- },
3049
- remove: async () => {
3050
- throw new Error(
3051
- "ResourceActions.remove not available in server-data mode"
3052
- );
3053
- },
3054
- refetch: () => {
3055
- }
3056
- };
3057
- }, [
3058
- isClientMode,
3059
- resourceHook.update,
3060
- resourceHook.remove,
3061
- resourceHook.list
3062
- ]);
3063
- const keyOf = useCallback(
3064
- (item) => getKeyProp ? getKeyProp(item) : String(item.id ?? ""),
3065
- [getKeyProp]
3066
- );
3067
- const selectedKey = page.selectedItem ? keyOf(page.selectedItem) : null;
3068
- const selectedItem = !page.selectedItem ? null : selectedKey ? displayData.find((row) => keyOf(row) === selectedKey) ?? page.selectedItem : page.selectedItem;
3069
- const [selectedIds, setSelectedIds] = useState(/* @__PURE__ */ new Set());
3070
- const handleToggleSelect = useCallback((id) => {
3071
- setSelectedIds((prev) => {
3072
- const next = new Set(prev);
3073
- if (next.has(id)) next.delete(id);
3074
- else next.add(id);
3075
- return next;
3076
- });
3077
- }, []);
3078
- const handleToggleAll = useCallback(() => {
3079
- setSelectedIds((prev) => {
3080
- if (prev.size === page.data.length) return /* @__PURE__ */ new Set();
3081
- return new Set(page.data.map(keyOf));
3082
- });
3083
- }, [page.data, keyOf]);
3084
- const handleClearSelection = useCallback(() => {
3085
- setSelectedIds(/* @__PURE__ */ new Set());
3086
- }, []);
3087
- const [formOpen, setFormOpen] = useState(false);
3088
- const [formAction, setFormAction] = useState("create");
3089
- const [formItem, setFormItem] = useState(null);
3090
- const useBuiltInCreate = allowCreate && !onCreate && isClientMode;
3091
- const useBuiltInEdit = allowEdit && !onEdit && isClientMode;
3092
- const handleCreate = useCallback(() => {
3093
- if (useBuiltInCreate) {
3094
- setFormAction("create");
3095
- setFormItem(null);
3096
- setFormOpen(true);
3097
- } else if (onCreate) {
3098
- onCreate();
3099
- }
3100
- }, [useBuiltInCreate, onCreate]);
3101
- const handleEdit = useCallback(
3102
- (item) => {
3103
- if (canEdit && !canEdit(item)) return;
3104
- if (canEditItem && !canEditItem(item)) return;
3105
- if (useBuiltInEdit) {
3106
- setFormAction("edit");
3107
- setFormItem(item);
3108
- setFormOpen(true);
3109
- } else if (onEdit) {
3110
- onEdit(item);
3111
- }
3112
- },
3113
- [useBuiltInEdit, onEdit, canEdit, canEditItem]
3114
- );
3115
- const closeDetail = useCallback(() => {
3116
- page.setDetailOpen(false);
3117
- page.setSelectedItem(null);
3118
- }, [page]);
3119
- const useBuiltInDelete = !!allowDelete && !onDelete && isClientMode;
3120
- const deleteConfirmed = confirmDelete === void 0 ? useBuiltInDelete : !!confirmDelete;
3121
- const deleteCopy = typeof confirmDelete === "object" ? confirmDelete : void 0;
3122
- const [pendingDelete, setPendingDelete] = useState(null);
3123
- const [deleteOpen, setDeleteOpen] = useState(false);
3124
- const { remove: removeRecord } = resourceHook;
3125
- const performDelete = useCallback(
3126
- async (item) => {
3127
- if (useBuiltInDelete) {
3128
- await removeRecord(
3129
- getKeyProp ? keyOf(item) : item.id
3130
- );
3131
- } else if (onDelete) {
3132
- await onDelete(item);
3133
- }
3134
- closeDetail();
3135
- onDeleted?.(item);
3136
- },
3137
- [
3138
- useBuiltInDelete,
3139
- removeRecord,
3140
- getKeyProp,
3141
- keyOf,
3142
- onDelete,
3143
- closeDetail,
3144
- onDeleted
3145
- ]
3146
- );
3147
- const deleteHandler = !(useBuiltInDelete || onDelete) ? void 0 : deleteConfirmed ? (item) => {
3148
- setPendingDelete(item);
3149
- setDeleteOpen(true);
3150
- } : useBuiltInDelete ? (item) => {
3151
- performDelete(item).catch((err) => {
3152
- console.error("ResourcePage: delete failed", err);
3153
- });
3154
- } : (
3155
- // Unconfirmed `onDelete` keeps its original contract exactly:
3156
- // called straight from the peek, nothing closed for it.
3157
- (item) => {
3158
- void onDelete?.(item);
3159
- }
3160
- );
3161
- const mayChange = (item) => canEditItem ? canEditItem(item) : true;
3162
- const selectedEditable = !!selectedItem && mayChange(selectedItem) && (canEdit ? canEdit(selectedItem) : true);
3163
- const selectedDeletable = !!selectedItem && mayChange(selectedItem) && (canDelete ? canDelete(selectedItem) : true);
3164
- const tableColumns = useMemo(() => {
3165
- const base = isClientMode ? page.columns.map((col) => ({
3166
- key: col.key,
3167
- header: col.header ?? col.key,
3168
- sortable: col.sortable ?? true,
3169
- hideBelow: col.hideBelow,
3170
- editable: col.editable,
3171
- accessor: col.accessor,
3172
- render: col.render ? (item, value) => col.render(item, value, actions) : void 0
3173
- })) : page.tableColumns;
3174
- if (!isServerControlled) return base;
3175
- return base.map((col) => ({ ...col, sortable: false }));
3176
- }, [
3445
+ actions,
3446
+ allows,
3447
+ clientTruncated,
3448
+ closeDetail,
3449
+ currentPage,
3450
+ deleteConfirmed,
3451
+ deleteCopy,
3452
+ deleteHandler,
3453
+ deleteKind,
3454
+ deleteOpen,
3455
+ detailOpen,
3456
+ detailSubtitle,
3457
+ detailTitle,
3458
+ displayData,
3459
+ displayTotal,
3460
+ effectivePageSize,
3461
+ formAction,
3462
+ formItem,
3463
+ formOpen,
3464
+ handleClearFilters,
3465
+ handleClearSelection,
3466
+ handleCreate,
3467
+ handleEdit,
3468
+ handlePageSizeChange,
3469
+ handleRowClick,
3470
+ handleSearchChange,
3471
+ handleSortSelect,
3472
+ handleTableSort,
3473
+ handleToggleAll,
3474
+ handleToggleSelect,
3475
+ hasError,
3177
3476
  isClientMode,
3477
+ isLoading,
3178
3478
  isServerControlled,
3179
- page.columns,
3180
- page.tableColumns,
3181
- actions
3182
- ]);
3183
- const pagerPage = resourceServerPaging ? resourceHook.params.pagination?.page ?? 1 : pageProp;
3184
- const pagerPerPage = resourceServerPaging ? resourceHook.params.pagination?.perPage : perPageProp;
3185
- const pagerTotal = resourceServerPaging ? displayTotal : totalProp;
3186
- const pagerOnPageChange = resourceServerPaging ? resourceHook.setPage : onPageChange;
3187
- const currentPage = pagerPage ?? 1;
3188
- const handleSearchChange = useCallback(
3189
- (value) => {
3190
- page.filterConfig.onSearchChange(value);
3191
- if (!isServerControlled) return;
3192
- onSearchChange?.(value);
3193
- if (currentPage !== 1 && !resourceServerPaging) onPageChange?.(1);
3194
- },
3195
- [
3196
- page.filterConfig,
3197
- isServerControlled,
3198
- onSearchChange,
3199
- onPageChange,
3200
- resourceServerPaging,
3201
- currentPage
3202
- ]
3203
- );
3204
- const handleSortSelect = useCallback(
3205
- (value) => {
3206
- page.filterConfig.onSortChange(value);
3207
- if (!isServerControlled) return;
3208
- const [field, order] = value.split(":");
3209
- onSortChange?.(
3210
- field && (order === "asc" || order === "desc") ? { field, order } : null
3211
- );
3212
- if (!resourceServerPaging) onPageChange?.(1);
3213
- },
3214
- [
3215
- page.filterConfig,
3216
- isServerControlled,
3217
- onSortChange,
3218
- onPageChange,
3219
- resourceServerPaging
3220
- ]
3221
- );
3222
- const handleClearFilters = useCallback(() => {
3223
- page.clearFilters();
3224
- if (!isServerControlled) return;
3225
- onSearchChange?.("");
3226
- onSortChange?.(null);
3227
- if (!resourceServerPaging) onPageChange?.(1);
3228
- }, [
3479
+ keyOf,
3480
+ keyboardNav,
3481
+ linkMissing,
3482
+ linked,
3483
+ missingPagerProps,
3229
3484
  page,
3230
- isServerControlled,
3231
- onSearchChange,
3232
- onSortChange,
3233
- onPageChange,
3234
- resourceServerPaging
3235
- ]);
3236
- const handleStatusChange = useCallback(
3237
- (values) => {
3238
- page.filterConfig.statusChips?.onChange(values);
3239
- if (!isServerControlled) return;
3240
- if (currentPage !== 1 && !resourceServerPaging) onPageChange?.(1);
3241
- },
3242
- [
3243
- page.filterConfig,
3244
- isServerControlled,
3245
- onPageChange,
3246
- resourceServerPaging,
3247
- currentPage
3248
- ]
3249
- );
3250
- const statusChipsConfig = useMemo(() => {
3251
- const base = page.filterConfig.statusChips;
3252
- if (!base) return void 0;
3253
- return { ...base, onChange: handleStatusChange };
3254
- }, [page.filterConfig.statusChips, handleStatusChange]);
3255
- const handlePageSizeChange = useCallback(
3256
- (size) => {
3257
- updateSetting("pageSize", size);
3258
- onPerPageChange?.(size);
3259
- if (resourceServerPaging) resourceHook.setPerPage(size);
3260
- else if (isServerControlled) onPageChange?.(1);
3261
- },
3262
- [
3263
- updateSetting,
3264
- onPerPageChange,
3265
- resourceServerPaging,
3266
- resourceHook.setPerPage,
3267
- isServerControlled,
3268
- onPageChange
3269
- ]
3270
- );
3271
- const missingPerPage = isServerControlled && !!pagerOnPageChange && pagerPerPage === void 0;
3272
- const missingTotal = isServerControlled && !!pagerOnPageChange && pagerTotal === void 0;
3273
- const pagerMisconfigured = missingPerPage || missingTotal;
3274
- const missingPagerProps = [
3275
- ...missingPerPage ? ["perPage"] : [],
3276
- ...missingTotal ? ["total"] : []
3277
- ];
3278
- const loggedMissingPerPageRef = useRef(false);
3279
- useEffect(() => {
3280
- if (!pagerMisconfigured) return;
3281
- if (loggedMissingPerPageRef.current) return;
3282
- loggedMissingPerPageRef.current = true;
3283
- console.error(
3284
- `ResourcePage: ${missingPagerProps.map((name) => `\`${name}\``).join(" and ")} ${missingPagerProps.length > 1 ? "are" : "is"} required when \`onPageChange\` is supplied.`
3285
- );
3286
- }, [pagerMisconfigured]);
3287
- const pagerPageSize = pagerPerPage ?? effectivePageSize;
3288
- const totalPages = Math.max(
3289
- 1,
3290
- Math.ceil(displayTotal / Math.max(1, pagerPageSize))
3291
- );
3292
- const rangeFrom = page.data.length === 0 ? 0 : (currentPage - 1) * pagerPageSize + 1;
3293
- const rangeTo = rangeFrom === 0 ? 0 : rangeFrom + page.data.length - 1;
3294
- const { setPage: setResourcePage } = resourceHook;
3295
- const settledPage = !isLoading && !hasError && !resourceHook.list.isPlaceholderData;
3296
- useEffect(() => {
3297
- if (!resourceServerPaging || !settledPage) return;
3298
- if (currentPage > totalPages) setResourcePage(totalPages);
3299
- }, [resourceServerPaging, settledPage, currentPage, totalPages, setResourcePage]);
3300
- const titleOf = (item) => getTitle ? getTitle(item) : String(item[page.titleField] ?? "");
3301
- const detailTitle = selectedItem ? titleOf(selectedItem) : "";
3302
- const detailSubtitle = !selectedItem ? void 0 : getSubtitle ? getSubtitle(selectedItem) : page.subtitleField ? String(
3303
- selectedItem[page.subtitleField] ?? ""
3304
- ) : void 0;
3305
- const handleRowClick = (item) => {
3306
- if (detail === "route") {
3307
- const id = item.id;
3308
- if (resourceName && id) {
3309
- onNavigate(`/${resourceName}/${id}`);
3310
- }
3311
- return;
3312
- }
3313
- page.handleRowClick(item);
3314
- };
3315
- const hasBulkSelect = !!bulkActions && bulkActions.length > 0;
3316
- const overlayOpen = page.detailOpen || formOpen || deleteOpen;
3317
- const keyboardNav = useKeyboardNavigation({
3318
- itemCount: page.data.length,
3319
- enabled: keyboardNavigation && page.viewMode !== "table",
3320
- overlayOpen,
3321
- onOpen: (index) => {
3322
- const item = page.data[index];
3323
- if (item) handleRowClick(item);
3324
- },
3325
- onToggleSelect: hasBulkSelect ? (index) => {
3326
- const item = page.data[index];
3327
- if (item) {
3328
- handleToggleSelect(keyOf(item));
3329
- }
3330
- } : void 0,
3331
- // Supplied only while there is an open detail to close — the hook
3332
- // suppresses Backspace's default whenever this is present, so leaving it
3333
- // wired up permanently would swallow the browser/app's Backspace on
3334
- // every grid/list page (including detail="none" and detail="route",
3335
- // which never open one). `detailOpen` is only ever true for the panel
3336
- // and modal modes, so it is the precise precondition here.
3337
- onBack: page.detailOpen ? () => {
3338
- page.setDetailOpen(false);
3339
- page.setSelectedItem(null);
3340
- } : void 0
3341
- });
3342
- const showCreateButton = onCreate || useBuiltInCreate;
3485
+ pagerMisconfigured,
3486
+ pagerOnPageChange,
3487
+ pendingDelete,
3488
+ performDelete,
3489
+ rangeFrom,
3490
+ rangeTo,
3491
+ resourceHook,
3492
+ resourceServerPaging,
3493
+ selectedDeletable,
3494
+ selectedEditable,
3495
+ selectedIds,
3496
+ selectedItem,
3497
+ serverSearchFields,
3498
+ setDeleteOpen,
3499
+ setFormOpen,
3500
+ settings,
3501
+ statusChipsConfig,
3502
+ tableColumns,
3503
+ tableSort,
3504
+ titleOf,
3505
+ totalPages,
3506
+ updateSetting,
3507
+ useBuiltInCreate,
3508
+ useBuiltInDelete,
3509
+ useBuiltInEdit
3510
+ } = useResourceTableModel(props);
3511
+ const showCreateButton = (onCreate || useBuiltInCreate) && allows("create");
3343
3512
  const createButton = showCreateButton ? /* @__PURE__ */ jsxs(Button, { size: "sm", onClick: handleCreate, children: [
3344
3513
  /* @__PURE__ */ jsx(Plus, { className: "h-4 w-4 mr-1" }),
3345
3514
  createLabel
@@ -3439,7 +3608,8 @@ function ResourcePage(props) {
3439
3608
  isLoading,
3440
3609
  hasError,
3441
3610
  isEmpty,
3442
- errorMessage: errorMessage2,
3611
+ errorMessage: errorMessage2 ?? // A provider's read error carries a sentence users can be shown.
3612
+ (isClientMode && isDataReadError(resourceHook.list.error) ? resourceHook.list.error.message : void 0),
3443
3613
  onRetry: isClientMode ? () => resourceHook.list.refetch() : onRetryProp,
3444
3614
  viewMode: page.viewMode,
3445
3615
  onViewModeChange: page.setViewMode,
@@ -3534,9 +3704,13 @@ function ResourcePage(props) {
3534
3704
  resizeKey,
3535
3705
  stickyHeader,
3536
3706
  stickyFirstColumn,
3707
+ keyboardNavigation,
3537
3708
  // The server already cut the page window; DataTable's own
3538
3709
  // pagination would slice the slice.
3539
- pagination: isServerControlled ? false : void 0
3710
+ pagination: isServerControlled ? false : void 0,
3711
+ // Client mode: the header sort is the page's sort, so it
3712
+ // reaches the URL and a link restores what the table shows.
3713
+ ...isServerControlled ? {} : { sort: tableSort, onSortChange: handleTableSort }
3540
3714
  },
3541
3715
  slotProps?.table
3542
3716
  ),
@@ -3656,9 +3830,18 @@ function ResourcePage(props) {
3656
3830
  (detail === "panel" || detail === "modal") && renderSlot(
3657
3831
  DetailPanel,
3658
3832
  slots?.detail,
3659
- {
3833
+ linkMissing ? {
3834
+ // A linked record this user can't read, or that's gone.
3835
+ item: {},
3836
+ open: detailOpen,
3837
+ onClose: closeDetail,
3838
+ title: "Record not found",
3839
+ width: detailWidth,
3840
+ layout: detail === "modal" ? "dialog" : "sheet",
3841
+ children: () => /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", "data-testid": "resource-page-link-missing", children: isDataReadError(linked.error) && linked.error.outcome === "forbidden" ? linked.error.message : "This record doesn't exist, or you don't have access to it." })
3842
+ } : {
3660
3843
  item: selectedItem,
3661
- open: page.detailOpen,
3844
+ open: detailOpen,
3662
3845
  onClose: closeDetail,
3663
3846
  title: detailTitle,
3664
3847
  subtitle: detailSubtitle,
@@ -3680,6 +3863,7 @@ function ResourcePage(props) {
3680
3863
  layout: formLayoutProp ?? (formAction === "edit" ? "sheet" : "dialog"),
3681
3864
  mode: formMode,
3682
3865
  steps: formSteps,
3866
+ mutationMode: mutationModeProp,
3683
3867
  item: formAction === "edit" ? formItem ?? void 0 : page.data.length > 0 ? Object.fromEntries(
3684
3868
  Object.keys(page.data[0]).filter((k) => k !== "id").map((k) => [k, ""])
3685
3869
  ) : void 0,
@@ -3701,6 +3885,7 @@ function ResourcePage(props) {
3701
3885
  open: deleteOpen,
3702
3886
  onOpenChange: setDeleteOpen,
3703
3887
  onConfirm: () => pendingDelete ? performDelete(pendingDelete) : void 0,
3888
+ kind: deleteKind,
3704
3889
  title: typeof deleteCopy?.title === "function" ? pendingDelete ? deleteCopy.title(pendingDelete) : void 0 : deleteCopy?.title,
3705
3890
  description: typeof deleteCopy?.description === "function" ? pendingDelete ? deleteCopy.description(pendingDelete) : void 0 : deleteCopy?.description,
3706
3891
  confirmLabel: deleteCopy?.confirmLabel,