@marktiderman/genesis-ui 2.7.0 → 2.8.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 (69) hide show
  1. package/EXTENDING.md +40 -1
  2. package/SKILL.md +144 -9
  3. package/dist/{AppShell-CX0M-dfc.d.ts → AppShell-LXQykEqe.d.ts} +47 -5
  4. package/dist/{chunk-M62CHO3N.js → chunk-4C4ESW6O.js} +9 -4
  5. package/dist/chunk-4C4ESW6O.js.map +1 -0
  6. package/dist/{chunk-IXGAEECT.js → chunk-DWCAC2E7.js} +6 -3
  7. package/dist/chunk-DWCAC2E7.js.map +1 -0
  8. package/dist/chunk-FUVWKTPL.js +19 -0
  9. package/dist/chunk-FUVWKTPL.js.map +1 -0
  10. package/dist/{chunk-PLIYF6KI.js → chunk-MPI33I4K.js} +2 -2
  11. package/dist/{chunk-PLIYF6KI.js.map → chunk-MPI33I4K.js.map} +1 -1
  12. package/dist/{chunk-JA3ZKULS.js → chunk-OITWCIG2.js} +3 -3
  13. package/dist/chunk-OITWCIG2.js.map +1 -0
  14. package/dist/chunk-PKFDGGZV.js +65 -0
  15. package/dist/chunk-PKFDGGZV.js.map +1 -0
  16. package/dist/{chunk-TXOZMKNW.js → chunk-S7NXD37O.js} +27 -26
  17. package/dist/chunk-S7NXD37O.js.map +1 -0
  18. package/dist/{chunk-ZPPLUGLA.js → chunk-UYI3GLFJ.js} +162 -19
  19. package/dist/chunk-UYI3GLFJ.js.map +1 -0
  20. package/dist/{chunk-CDYZRZ6U.js → chunk-WGMEVX3Y.js} +2 -2
  21. package/dist/chunk-WGMEVX3Y.js.map +1 -0
  22. package/dist/chunk-ZDHWNQTR.js +552 -0
  23. package/dist/chunk-ZDHWNQTR.js.map +1 -0
  24. package/dist/components/data/index.d.ts +239 -15
  25. package/dist/components/data/index.js +522 -107
  26. package/dist/components/data/index.js.map +1 -1
  27. package/dist/components/layout/form-page.d.ts +1 -1
  28. package/dist/components/layout/form-page.js +1 -1
  29. package/dist/components/layout/index.d.ts +2 -2
  30. package/dist/components/layout/index.js +8 -8
  31. package/dist/components/patterns/index.d.ts +1 -1
  32. package/dist/components/patterns/index.js +5 -5
  33. package/dist/components/patterns/sortable-list.js +1 -1
  34. package/dist/components/rich-text/rich-text-editor.d.ts +194 -0
  35. package/dist/components/rich-text/rich-text-editor.js +833 -0
  36. package/dist/components/rich-text/rich-text-editor.js.map +1 -0
  37. package/dist/components/rich-text/rich-text.d.ts +51 -0
  38. package/dist/components/rich-text/rich-text.js +4 -0
  39. package/dist/components/rich-text/rich-text.js.map +1 -0
  40. package/dist/components/ui/alert-dialog.d.ts +12 -1
  41. package/dist/components/ui/alert-dialog.js +1 -1
  42. package/dist/hooks/index.d.ts +3 -3
  43. package/dist/hooks/index.js +4 -4
  44. package/dist/hooks/use-data-filters.d.ts +5 -3
  45. package/dist/hooks/use-data-filters.js +2 -2
  46. package/dist/hooks/use-resource-page.d.ts +2 -1
  47. package/dist/hooks/use-resource-page.js +3 -2
  48. package/dist/hooks/use-view-preference.d.ts +17 -2
  49. package/dist/hooks/use-view-preference.js +2 -1
  50. package/dist/index.d.ts +2 -2
  51. package/dist/index.js +30 -30
  52. package/dist/{navigation-DBwcZ6vb.d.ts → navigation-Bha0R3Fu.d.ts} +6 -3
  53. package/dist/rich-text-doc-C54fME2C.d.ts +180 -0
  54. package/dist/router/react-router.d.ts +47 -0
  55. package/dist/router/react-router.js +53 -0
  56. package/dist/router/react-router.js.map +1 -0
  57. package/dist/router/tanstack-router.d.ts +47 -0
  58. package/dist/router/tanstack-router.js +105 -0
  59. package/dist/router/tanstack-router.js.map +1 -0
  60. package/dist/{use-resource-page-Dd2VUv30.d.ts → use-resource-page-D4byuwAs.d.ts} +20 -3
  61. package/package.json +61 -10
  62. package/dist/chunk-CDYZRZ6U.js.map +0 -1
  63. package/dist/chunk-FTTNGGLA.js +0 -20
  64. package/dist/chunk-FTTNGGLA.js.map +0 -1
  65. package/dist/chunk-IXGAEECT.js.map +0 -1
  66. package/dist/chunk-JA3ZKULS.js.map +0 -1
  67. package/dist/chunk-M62CHO3N.js.map +0 -1
  68. package/dist/chunk-TXOZMKNW.js.map +0 -1
  69. package/dist/chunk-ZPPLUGLA.js.map +0 -1
@@ -1,47 +1,48 @@
1
1
  "use client";
2
- import { AppShell, PageHeader } from '../../chunk-ZPPLUGLA.js';
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';
6
5
  import { Textarea } from '../../chunk-YETMISCE.js';
7
6
  import { Separator } from '../../chunk-SXSCHWZU.js';
8
7
  import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription } from '../../chunk-XCGZPFT7.js';
9
8
  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 { Input } from '../../chunk-QSXVL5UB.js';
12
11
  import { Card, CardContent, CardHeader, CardTitle } from '../../chunk-FN3FZVUR.js';
13
12
  import { Checkbox } from '../../chunk-SEBU6JCK.js';
13
+ import { AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction } from '../../chunk-4C4ESW6O.js';
14
+ import { Input } from '../../chunk-QSXVL5UB.js';
14
15
  import '../../chunk-B2GIBUQD.js';
15
16
  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';
16
22
  import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '../../chunk-FK42ZGGV.js';
17
- import { Popover, PopoverTrigger, PopoverContent } from '../../chunk-22XB3Q6H.js';
18
23
  import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuCheckboxItem, DropdownMenuSeparator, DropdownMenuItem, DropdownMenuLabel } from '../../chunk-QJ6P2DW6.js';
19
24
  import { Switch } from '../../chunk-27ELYTGE.js';
25
+ import { Popover, PopoverTrigger, PopoverContent } from '../../chunk-22XB3Q6H.js';
20
26
  import { EmptyState } from '../../chunk-65SPT7EA.js';
21
27
  export { EmptyState } from '../../chunk-65SPT7EA.js';
22
28
  import { Button } from '../../chunk-NWSCYLPT.js';
23
- import { PageLoading } from '../../chunk-YRYQXU3G.js';
24
- import { Spinner } from '../../chunk-OVU3Z2MT.js';
25
29
  import { FormField, FormLabel, FormError, FormDescription } from '../../chunk-R7QFP6EL.js';
26
30
  import '../../chunk-F47U5KNW.js';
27
- import { DetailPage } from '../../chunk-AFZ76Q26.js';
28
- import '../../chunk-D2LIXA2Y.js';
29
- import '../../chunk-AVFZ4P4B.js';
30
- import { titleCase, useResourcePage } from '../../chunk-IXGAEECT.js';
31
+ import { titleCase, useResourcePage } from '../../chunk-DWCAC2E7.js';
31
32
  export { useSavedViews } from '../../chunk-GCQFVMPI.js';
32
- import '../../chunk-FTTNGGLA.js';
33
+ import '../../chunk-PKFDGGZV.js';
33
34
  import { useViewSettings } from '../../chunk-OZ7ZAYCS.js';
34
35
  export { CommandPalette, useCommandPaletteHotkey } from '../../chunk-CNTWQSUT.js';
35
36
  import { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandSeparator } from '../../chunk-SEFK4NZV.js';
36
37
  import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from '../../chunk-7XMZVISM.js';
37
38
  import { cn } from '../../chunk-NZ5WBA4R.js';
38
- import { DefaultLink, defaultNavigate } from '../../chunk-CDYZRZ6U.js';
39
- export { DefaultLink, defaultNavigate, isPathActive, resolveNavigation } from '../../chunk-CDYZRZ6U.js';
39
+ import { DefaultLink, defaultNavigate } from '../../chunk-WGMEVX3Y.js';
40
+ export { DefaultLink, defaultNavigate, isPathActive, resolveNavigation } from '../../chunk-WGMEVX3Y.js';
40
41
  import { useKeyboardNavigation } from '../../chunk-YJ6MPS6U.js';
41
42
  import { useIsMobile } from '../../chunk-VFO3I7PV.js';
42
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';
43
44
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
44
- import { useState, useRef, useEffect, useCallback, useMemo, Fragment as Fragment$1 } from 'react';
45
+ import { useState, useRef, useEffect, useCallback, useMemo, useId, Fragment as Fragment$1 } from 'react';
45
46
  import { formatDistanceToNow, format } from 'date-fns';
46
47
  import { useFieldArray, Controller, useWatch } from 'react-hook-form';
47
48
  import { useResource, useWizard, useResourceForm, useOne, buildArrayItemDefaults } from '@marktiderman/genesis-core/hooks';
@@ -59,6 +60,7 @@ function DataPageShell({
59
60
  isEmpty = false,
60
61
  emptyIcon,
61
62
  emptyMessage,
63
+ emptyDescription,
62
64
  emptyAction,
63
65
  hasError = false,
64
66
  errorMessage: errorMessage2,
@@ -89,11 +91,13 @@ function DataPageShell({
89
91
  }) {
90
92
  const entity = entityName || title.toLowerCase();
91
93
  const collapseFilters = filtersCollapsible && filters != null && header !== null;
94
+ const toolbarOnly = header === "toolbar";
95
+ const baseHeaderOptions = toolbarOnly ? { showTitle: false, ...headerOptions } : headerOptions;
92
96
  const mergedOptions = {
93
97
  defaultDetailsOpen: defaultFiltersOpen,
94
98
  detailsLabel: filtersLabel,
95
99
  subtitleInDetails: collapseFilters ? true : void 0,
96
- ...headerOptions
100
+ ...baseHeaderOptions
97
101
  };
98
102
  const sharedHeaderProps = {
99
103
  title,
@@ -114,7 +118,7 @@ function DataPageShell({
114
118
  stats,
115
119
  optionsStorageKey: headerOptionsStorageKey
116
120
  };
117
- const Header = header === null ? (() => null) : header ?? PageHeader;
121
+ const Header = header === null ? () => null : header === "toolbar" || header === void 0 ? PageHeader : header;
118
122
  return /* @__PURE__ */ jsxs("div", { className: cn("space-y-6", className), children: [
119
123
  collapseFilters ? /* @__PURE__ */ jsx(
120
124
  Header,
@@ -128,7 +132,7 @@ function DataPageShell({
128
132
  options: mergedOptions
129
133
  }
130
134
  ) : /* @__PURE__ */ jsxs(Fragment, { children: [
131
- /* @__PURE__ */ jsx(Header, { ...sharedHeaderProps, options: headerOptions }),
135
+ /* @__PURE__ */ jsx(Header, { ...sharedHeaderProps, options: baseHeaderOptions }),
132
136
  filters
133
137
  ] }),
134
138
  isLoading ? /* @__PURE__ */ jsx(
@@ -158,6 +162,7 @@ function DataPageShell({
158
162
  {
159
163
  icon: emptyIcon,
160
164
  title: emptyMessage,
165
+ ...emptyDescription ? { description: emptyDescription } : {},
161
166
  action: emptyAction,
162
167
  hasFilters: hasActiveFilters,
163
168
  onClearFilters
@@ -540,6 +545,28 @@ function DataFilters({
540
545
  ] });
541
546
  }
542
547
  var DEFAULT_BATCH_SIZE = 48;
548
+ var NESTED_CONTROL = "a,button,input,select,textarea,label,[role=button],[role=link],[role=checkbox],[role=switch],[role=menuitem],[contenteditable=true]";
549
+ function activation(item, onItemClick, getItemLabel) {
550
+ if (!onItemClick) return {};
551
+ return {
552
+ role: "button",
553
+ tabIndex: 0,
554
+ "aria-label": getItemLabel?.(item),
555
+ onClick: (e) => {
556
+ const control = e.target.closest(NESTED_CONTROL);
557
+ if (control && control !== e.currentTarget) return;
558
+ onItemClick(item);
559
+ },
560
+ onKeyDown: (e) => {
561
+ if (e.target !== e.currentTarget) return;
562
+ if (e.key === "Enter" || e.key === " ") {
563
+ e.preventDefault();
564
+ onItemClick(item);
565
+ }
566
+ }
567
+ };
568
+ }
569
+ var ACTIVATABLE = "cursor-pointer outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background";
543
570
  function DataGrid({
544
571
  items,
545
572
  viewMode,
@@ -551,7 +578,9 @@ function DataGrid({
551
578
  staggerAnimation = true,
552
579
  getKey,
553
580
  focusedIndex = -1,
554
- keyboardContainerRef
581
+ keyboardContainerRef,
582
+ onItemClick,
583
+ getItemLabel
555
584
  }) {
556
585
  const [visibleCount, setVisibleCount] = useState(batchSize);
557
586
  const sentinelRef = useRef(null);
@@ -587,8 +616,10 @@ function DataGrid({
587
616
  "div",
588
617
  {
589
618
  "data-nav-item": true,
619
+ ...activation(item, onItemClick, getItemLabel),
590
620
  className: cn(
591
621
  "px-4 py-3 transition-colors hover:bg-accent/50",
622
+ onItemClick && ACTIVATABLE,
592
623
  focusedIndex === i && "ring-2 ring-inset ring-primary bg-accent/50"
593
624
  ),
594
625
  children: renderListItem(item, i)
@@ -605,8 +636,10 @@ function DataGrid({
605
636
  "div",
606
637
  {
607
638
  "data-nav-item": true,
639
+ ...activation(item, onItemClick, getItemLabel),
608
640
  className: cn(
609
641
  staggerAnimation && "animate-fade-in",
642
+ onItemClick && cn(ACTIVATABLE, "h-full rounded-xl"),
610
643
  focusedIndex === i && "ring-2 ring-primary ring-offset-2 ring-offset-background rounded-xl transition-shadow duration-150"
611
644
  ),
612
645
  style: staggerAnimation ? {
@@ -639,7 +672,20 @@ var DENSITY_HEAD = {
639
672
  };
640
673
  function getCellClasses(col) {
641
674
  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" : "";
642
- return cn(col.width, hide);
675
+ return cn(typeof col.width === "string" && !PX_WIDTH.test(col.width) ? col.width : void 0, hide);
676
+ }
677
+ var PX_WIDTH = /^\s*\d+(\.\d+)?(px)?\s*$/;
678
+ function pxWidth(width) {
679
+ if (typeof width === "number") return width > 0 ? width : void 0;
680
+ if (width && PX_WIDTH.test(width)) {
681
+ const px = parseFloat(width);
682
+ return px > 0 ? px : void 0;
683
+ }
684
+ return void 0;
685
+ }
686
+ function colHideClass(hideBelow) {
687
+ if (!hideBelow) return void 0;
688
+ return hideBelow === "sm" ? "hidden sm:table-column" : hideBelow === "md" ? "hidden md:table-column" : hideBelow === "lg" ? "hidden lg:table-column" : "hidden xl:table-column";
643
689
  }
644
690
  function errorMessage(error) {
645
691
  return error instanceof Error ? error.message : String(error);
@@ -946,10 +992,8 @@ function DataTable({
946
992
  }
947
993
  const initial = {};
948
994
  columns.forEach((c) => {
949
- if (c.width) {
950
- const px = parseInt(c.width, 10);
951
- if (!isNaN(px) && px > 0) initial[c.key] = px;
952
- }
995
+ const px = pxWidth(c.width);
996
+ if (px) initial[c.key] = px;
953
997
  });
954
998
  return initial;
955
999
  });
@@ -1042,6 +1086,21 @@ function DataTable({
1042
1086
  )
1043
1087
  ] }) }),
1044
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
+ ] }),
1045
1104
  /* @__PURE__ */ jsx(
1046
1105
  TableHeader,
1047
1106
  {
@@ -1066,6 +1125,7 @@ function DataTable({
1066
1125
  getCellClasses(col),
1067
1126
  headDensity,
1068
1127
  col.sortable && "cursor-pointer select-none",
1128
+ "overflow-hidden",
1069
1129
  resizableColumns && "relative",
1070
1130
  isFirstCol && "sticky left-0 bg-background",
1071
1131
  isFirstCol && stickyHeader ? "z-[15]" : isFirstCol ? "z-[5]" : void 0
@@ -1073,12 +1133,12 @@ function DataTable({
1073
1133
  style: resizeWidth ? { width: resizeWidth, minWidth: resizeWidth } : void 0,
1074
1134
  onClick: col.sortable ? (e) => handleSort(col.key, e.shiftKey) : void 0,
1075
1135
  children: [
1076
- /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1", children: [
1077
- col.header,
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 }),
1078
1138
  col.sortable && (sortEntry ? /* @__PURE__ */ jsxs(Fragment, { children: [
1079
- sortEntry.dir === "asc" ? /* @__PURE__ */ jsx(ArrowUp, { className: "h-3 w-3 text-foreground" }) : /* @__PURE__ */ jsx(ArrowDown, { className: "h-3 w-3 text-foreground" }),
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" }),
1080
1140
  sorts.length > 1 && /* @__PURE__ */ jsx("span", { className: "text-[10px] text-muted-foreground font-mono", children: sortIndex + 1 })
1081
- ] }) : /* @__PURE__ */ jsx(ArrowUpDown, { className: "h-3 w-3 text-muted-foreground/50" }))
1141
+ ] }) : /* @__PURE__ */ jsx(ArrowUpDown, { className: "h-3 w-3 shrink-0 text-muted-foreground/50" }))
1082
1142
  ] }),
1083
1143
  resizableColumns && /* @__PURE__ */ jsx(
1084
1144
  ResizeHandle,
@@ -1754,7 +1814,11 @@ function RelationField({
1754
1814
  field,
1755
1815
  value,
1756
1816
  onChange,
1757
- disabled
1817
+ disabled,
1818
+ id,
1819
+ "aria-describedby": ariaDescribedBy,
1820
+ "aria-invalid": ariaInvalid,
1821
+ "aria-required": ariaRequired
1758
1822
  }) {
1759
1823
  const [open, setOpen] = useState(false);
1760
1824
  const [createOpen, setCreateOpen] = useState(false);
@@ -1813,7 +1877,11 @@ function RelationField({
1813
1877
  {
1814
1878
  variant: "outline",
1815
1879
  role: "combobox",
1880
+ id,
1816
1881
  "aria-expanded": open,
1882
+ "aria-describedby": ariaDescribedBy,
1883
+ "aria-invalid": ariaInvalid,
1884
+ "aria-required": ariaRequired,
1817
1885
  className: "w-full justify-between font-normal",
1818
1886
  disabled: disabled || isLoading,
1819
1887
  children: [
@@ -1924,7 +1992,12 @@ function FileUploadField({
1924
1992
  field,
1925
1993
  value,
1926
1994
  onChange,
1927
- disabled
1995
+ disabled,
1996
+ id,
1997
+ "aria-labelledby": ariaLabelledBy,
1998
+ "aria-describedby": ariaDescribedBy,
1999
+ "aria-invalid": ariaInvalid,
2000
+ "aria-required": ariaRequired
1928
2001
  }) {
1929
2002
  const multiple = field.multiple ?? false;
1930
2003
  const maxSize = field.maxSize ?? DEFAULT_MAX_SIZE;
@@ -1997,7 +2070,16 @@ function FileUploadField({
1997
2070
  /* @__PURE__ */ jsxs(
1998
2071
  "div",
1999
2072
  {
2000
- ...getRootProps(),
2073
+ ...getRootProps({
2074
+ // The focusable drop zone carries the field's name, description
2075
+ // and state; react-dropzone's default `role="presentation"` would
2076
+ // leave a keyboard user on an unnamed tab stop.
2077
+ role: "button",
2078
+ "aria-labelledby": ariaLabelledBy,
2079
+ "aria-describedby": ariaDescribedBy,
2080
+ "aria-invalid": ariaInvalid,
2081
+ "aria-disabled": disabled || void 0
2082
+ }),
2001
2083
  className: cn(
2002
2084
  "flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed px-6 py-8 text-center transition-colors",
2003
2085
  isDragActive ? "border-primary bg-primary/5" : "border-muted-foreground/25 hover:border-muted-foreground/50",
@@ -2005,7 +2087,17 @@ function FileUploadField({
2005
2087
  hasFiles && "py-4"
2006
2088
  ),
2007
2089
  children: [
2008
- /* @__PURE__ */ jsx("input", { ...getInputProps() }),
2090
+ /* @__PURE__ */ jsx(
2091
+ "input",
2092
+ {
2093
+ ...getInputProps({
2094
+ id,
2095
+ "aria-describedby": ariaDescribedBy,
2096
+ "aria-invalid": ariaInvalid,
2097
+ "aria-required": ariaRequired
2098
+ })
2099
+ }
2100
+ ),
2009
2101
  isImage ? /* @__PURE__ */ jsx(ImageIcon, { className: "mb-2 h-8 w-8 text-muted-foreground" }) : /* @__PURE__ */ jsx(Upload, { className: "mb-2 h-8 w-8 text-muted-foreground" }),
2010
2102
  /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: isDragActive ? "Drop files here..." : "Drag & drop or click to upload" }),
2011
2103
  /* @__PURE__ */ jsxs("p", { className: "mt-1 text-xs text-muted-foreground/70", children: [
@@ -2182,6 +2274,11 @@ function ResourceFormFieldRenderer({
2182
2274
  watchedValues
2183
2275
  }) {
2184
2276
  const label = field.label ?? titleCase(field.key);
2277
+ const baseId = useId();
2278
+ const inputId = `${baseId}-input`;
2279
+ const errorId = `${baseId}-error`;
2280
+ const descId = `${baseId}-desc`;
2281
+ const labelId = `${baseId}-label`;
2185
2282
  const fieldType = field.type ?? "text";
2186
2283
  const isReadOnly = action === "edit" && field.readOnlyOnEdit;
2187
2284
  const isDisabled = isReadOnly || (watchedValues ? field.disabled?.(watchedValues) === true : false);
@@ -2213,14 +2310,20 @@ function ResourceFormFieldRenderer({
2213
2310
  control,
2214
2311
  render: ({ field: f, fieldState }) => {
2215
2312
  const errorMessage2 = fieldState.error?.message;
2313
+ const describedBy = [field.description ? descId : null, errorMessage2 ? errorId : null].filter(Boolean).join(" ") || void 0;
2216
2314
  const ariaProps = {
2315
+ id: inputId,
2217
2316
  "aria-invalid": !!errorMessage2 || void 0,
2218
- "aria-describedby": errorMessage2 ? `${field.key}-error` : field.description ? `${field.key}-desc` : void 0,
2317
+ "aria-describedby": describedBy,
2219
2318
  "aria-required": field.required || void 0
2220
2319
  };
2221
2320
  if (fieldType === "switch") {
2222
2321
  return /* @__PURE__ */ jsxs(FormField, { className: "flex flex-row items-center justify-between rounded-lg border p-4", children: [
2223
- /* @__PURE__ */ jsx("div", { className: "space-y-0.5", children: /* @__PURE__ */ jsx(FormLabel, { error: !!errorMessage2, children: label }) }),
2322
+ /* @__PURE__ */ jsxs("div", { className: "space-y-0.5", children: [
2323
+ /* @__PURE__ */ jsx(FormLabel, { htmlFor: inputId, error: !!errorMessage2, children: label }),
2324
+ field.description && /* @__PURE__ */ jsx(FormDescription, { id: descId, children: field.description }),
2325
+ /* @__PURE__ */ jsx(FormError, { id: errorId, children: errorMessage2 })
2326
+ ] }),
2224
2327
  /* @__PURE__ */ jsx(
2225
2328
  Switch,
2226
2329
  {
@@ -2244,14 +2347,15 @@ function ResourceFormFieldRenderer({
2244
2347
  }
2245
2348
  ),
2246
2349
  /* @__PURE__ */ jsxs("div", { className: "space-y-1 leading-none", children: [
2247
- /* @__PURE__ */ jsx(FormLabel, { error: !!errorMessage2, children: label }),
2248
- /* @__PURE__ */ jsx(FormError, { id: `${field.key}-error`, children: errorMessage2 })
2350
+ /* @__PURE__ */ jsx(FormLabel, { htmlFor: inputId, error: !!errorMessage2, children: label }),
2351
+ field.description && /* @__PURE__ */ jsx(FormDescription, { id: descId, children: field.description }),
2352
+ /* @__PURE__ */ jsx(FormError, { id: errorId, children: errorMessage2 })
2249
2353
  ] })
2250
2354
  ] });
2251
2355
  }
2252
2356
  if (fieldType === "select" && field.options) {
2253
2357
  return /* @__PURE__ */ jsxs(FormField, { children: [
2254
- /* @__PURE__ */ jsx(FormLabel, { error: !!errorMessage2, required: field.required, children: label }),
2358
+ /* @__PURE__ */ jsx(FormLabel, { htmlFor: inputId, error: !!errorMessage2, required: field.required, children: label }),
2255
2359
  /* @__PURE__ */ jsxs(
2256
2360
  Select,
2257
2361
  {
@@ -2270,49 +2374,53 @@ function ResourceFormFieldRenderer({
2270
2374
  ]
2271
2375
  }
2272
2376
  ),
2273
- field.description && /* @__PURE__ */ jsx(FormDescription, { id: `${field.key}-desc`, children: field.description }),
2274
- /* @__PURE__ */ jsx(FormError, { id: `${field.key}-error`, children: errorMessage2 })
2377
+ field.description && /* @__PURE__ */ jsx(FormDescription, { id: descId, children: field.description }),
2378
+ /* @__PURE__ */ jsx(FormError, { id: errorId, children: errorMessage2 })
2275
2379
  ] });
2276
2380
  }
2277
2381
  if (fieldType === "relation" && field.relation) {
2278
2382
  return /* @__PURE__ */ jsxs(FormField, { children: [
2279
- /* @__PURE__ */ jsx(FormLabel, { error: !!errorMessage2, required: field.required, children: label }),
2383
+ /* @__PURE__ */ jsx(FormLabel, { htmlFor: inputId, error: !!errorMessage2, required: field.required, children: label }),
2280
2384
  /* @__PURE__ */ jsx(
2281
2385
  RelationField,
2282
2386
  {
2283
2387
  field,
2284
2388
  value: f.value != null ? String(f.value) : "",
2285
2389
  onChange: f.onChange,
2286
- disabled: isDisabled
2390
+ disabled: isDisabled,
2391
+ ...ariaProps
2287
2392
  }
2288
2393
  ),
2289
- field.description && /* @__PURE__ */ jsx(FormDescription, { id: `${field.key}-desc`, children: field.description }),
2290
- /* @__PURE__ */ jsx(FormError, { id: `${field.key}-error`, children: errorMessage2 })
2394
+ field.description && /* @__PURE__ */ jsx(FormDescription, { id: descId, children: field.description }),
2395
+ /* @__PURE__ */ jsx(FormError, { id: errorId, children: errorMessage2 })
2291
2396
  ] });
2292
2397
  }
2293
2398
  if (fieldType === "file" || fieldType === "image") {
2294
2399
  return /* @__PURE__ */ jsxs(FormField, { children: [
2295
- /* @__PURE__ */ jsx(FormLabel, { error: !!errorMessage2, required: field.required, children: label }),
2400
+ /* @__PURE__ */ jsx(FormLabel, { id: labelId, htmlFor: inputId, error: !!errorMessage2, required: field.required, children: label }),
2296
2401
  /* @__PURE__ */ jsx(
2297
2402
  FileUploadField,
2298
2403
  {
2404
+ "aria-labelledby": labelId,
2299
2405
  field,
2300
2406
  value: f.value,
2301
2407
  onChange: f.onChange,
2302
- disabled: isDisabled
2408
+ disabled: isDisabled,
2409
+ ...ariaProps
2303
2410
  }
2304
2411
  ),
2305
- field.description && /* @__PURE__ */ jsx(FormDescription, { id: `${field.key}-desc`, children: field.description }),
2306
- /* @__PURE__ */ jsx(FormError, { id: `${field.key}-error`, children: errorMessage2 })
2412
+ field.description && /* @__PURE__ */ jsx(FormDescription, { id: descId, children: field.description }),
2413
+ /* @__PURE__ */ jsx(FormError, { id: errorId, children: errorMessage2 })
2307
2414
  ] });
2308
2415
  }
2309
2416
  if (fieldType === "textarea") {
2310
2417
  return /* @__PURE__ */ jsxs(FormField, { children: [
2311
- /* @__PURE__ */ jsx(FormLabel, { error: !!errorMessage2, required: field.required, children: label }),
2418
+ /* @__PURE__ */ jsx(FormLabel, { htmlFor: inputId, error: !!errorMessage2, required: field.required, children: label }),
2312
2419
  /* @__PURE__ */ jsx(
2313
2420
  Textarea,
2314
2421
  {
2315
2422
  placeholder: field.placeholder,
2423
+ maxLength: field.maxLength,
2316
2424
  rows: field.rows ?? 3,
2317
2425
  value: f.value != null ? String(f.value) : "",
2318
2426
  onChange: (e) => f.onChange(e.target.value),
@@ -2321,13 +2429,13 @@ function ResourceFormFieldRenderer({
2321
2429
  ...ariaProps
2322
2430
  }
2323
2431
  ),
2324
- field.description && /* @__PURE__ */ jsx(FormDescription, { id: `${field.key}-desc`, children: field.description }),
2325
- /* @__PURE__ */ jsx(FormError, { id: `${field.key}-error`, children: errorMessage2 })
2432
+ field.description && /* @__PURE__ */ jsx(FormDescription, { id: descId, children: field.description }),
2433
+ /* @__PURE__ */ jsx(FormError, { id: errorId, children: errorMessage2 })
2326
2434
  ] });
2327
2435
  }
2328
2436
  if (fieldType === "number") {
2329
2437
  return /* @__PURE__ */ jsxs(FormField, { children: [
2330
- /* @__PURE__ */ jsx(FormLabel, { error: !!errorMessage2, required: field.required, children: label }),
2438
+ /* @__PURE__ */ jsx(FormLabel, { htmlFor: inputId, error: !!errorMessage2, required: field.required, children: label }),
2331
2439
  /* @__PURE__ */ jsx(
2332
2440
  Input,
2333
2441
  {
@@ -2345,17 +2453,18 @@ function ResourceFormFieldRenderer({
2345
2453
  ...ariaProps
2346
2454
  }
2347
2455
  ),
2348
- field.description && /* @__PURE__ */ jsx(FormDescription, { id: `${field.key}-desc`, children: field.description }),
2349
- /* @__PURE__ */ jsx(FormError, { id: `${field.key}-error`, children: errorMessage2 })
2456
+ field.description && /* @__PURE__ */ jsx(FormDescription, { id: descId, children: field.description }),
2457
+ /* @__PURE__ */ jsx(FormError, { id: errorId, children: errorMessage2 })
2350
2458
  ] });
2351
2459
  }
2352
2460
  return /* @__PURE__ */ jsxs(FormField, { children: [
2353
- /* @__PURE__ */ jsx(FormLabel, { error: !!errorMessage2, required: field.required, children: label }),
2461
+ /* @__PURE__ */ jsx(FormLabel, { htmlFor: inputId, error: !!errorMessage2, required: field.required, children: label }),
2354
2462
  /* @__PURE__ */ jsx(
2355
2463
  Input,
2356
2464
  {
2357
2465
  type: fieldType,
2358
2466
  placeholder: field.placeholder,
2467
+ maxLength: field.maxLength,
2359
2468
  value: f.value != null ? String(f.value) : "",
2360
2469
  onChange: (e) => f.onChange(e.target.value),
2361
2470
  disabled: isDisabled,
@@ -2363,8 +2472,8 @@ function ResourceFormFieldRenderer({
2363
2472
  ...ariaProps
2364
2473
  }
2365
2474
  ),
2366
- field.description && /* @__PURE__ */ jsx(FormDescription, { id: `${field.key}-desc`, children: field.description }),
2367
- /* @__PURE__ */ jsx(FormError, { id: `${field.key}-error`, children: errorMessage2 })
2475
+ field.description && /* @__PURE__ */ jsx(FormDescription, { id: descId, children: field.description }),
2476
+ /* @__PURE__ */ jsx(FormError, { id: errorId, children: errorMessage2 })
2368
2477
  ] });
2369
2478
  }
2370
2479
  }
@@ -2476,6 +2585,19 @@ function FormBody({
2476
2585
  control: form.control,
2477
2586
  disabled: !hasConditionalFields
2478
2587
  });
2588
+ const rootError = form.formState.errors.root?.message;
2589
+ const rootErrorBanner = rootError ? /* @__PURE__ */ jsxs(
2590
+ "div",
2591
+ {
2592
+ role: "alert",
2593
+ "data-testid": "resource-form-error",
2594
+ className: "flex items-start gap-2 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive",
2595
+ children: [
2596
+ /* @__PURE__ */ jsx(AlertTriangle, { className: "mt-0.5 h-4 w-4 shrink-0", "aria-hidden": "true" }),
2597
+ /* @__PURE__ */ jsx("span", { children: rootError })
2598
+ ]
2599
+ }
2600
+ ) : null;
2479
2601
  const onInvalid = (errors) => {
2480
2602
  const firstErrorKey = Object.keys(errors)[0];
2481
2603
  if (firstErrorKey) form.setFocus(firstErrorKey);
@@ -2484,18 +2606,21 @@ function FormBody({
2484
2606
  return /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center py-8", children: /* @__PURE__ */ jsx(Spinner, {}) });
2485
2607
  }
2486
2608
  if (mode === "wizard" && steps) {
2487
- return /* @__PURE__ */ jsx(
2488
- WizardForm,
2489
- {
2490
- steps,
2491
- form,
2492
- fields: resolvedFields,
2493
- onSubmit: handleSave,
2494
- isSaving,
2495
- action,
2496
- onInvalid
2497
- }
2498
- );
2609
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
2610
+ rootErrorBanner,
2611
+ /* @__PURE__ */ jsx(
2612
+ WizardForm,
2613
+ {
2614
+ steps,
2615
+ form,
2616
+ fields: resolvedFields,
2617
+ onSubmit: handleSave,
2618
+ isSaving,
2619
+ action,
2620
+ onInvalid
2621
+ }
2622
+ )
2623
+ ] });
2499
2624
  }
2500
2625
  const visibleFields = resolvedFields.filter(
2501
2626
  (field) => field.visible?.(watchedValues) !== false
@@ -2535,6 +2660,7 @@ function FormBody({
2535
2660
  return /* @__PURE__ */ jsxs(
2536
2661
  "form",
2537
2662
  {
2663
+ noValidate: true,
2538
2664
  onSubmit: (e) => {
2539
2665
  e.preventDefault();
2540
2666
  void form.handleSubmit(handleSave, onInvalid)(e);
@@ -2548,6 +2674,7 @@ function FormBody({
2548
2674
  ] }),
2549
2675
  renderFields(group.fields)
2550
2676
  ] }, group.name ?? "__default")) }) }),
2677
+ rootErrorBanner,
2551
2678
  /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2", children: [
2552
2679
  /* @__PURE__ */ jsx(
2553
2680
  Button,
@@ -2584,7 +2711,9 @@ function ResourceForm(props) {
2584
2711
  layout: layoutProp = "dialog",
2585
2712
  mode = "standard",
2586
2713
  steps,
2587
- columns = 1
2714
+ columns = 1,
2715
+ prefillFromUrl,
2716
+ autoPlaceholder = false
2588
2717
  } = props;
2589
2718
  const isMobile = useIsMobile();
2590
2719
  const effectiveLayout = isMobile ? "sheet" : layoutProp;
@@ -2602,7 +2731,9 @@ function ResourceForm(props) {
2602
2731
  onOpenChange(false);
2603
2732
  onSuccess?.(data);
2604
2733
  },
2605
- onError
2734
+ onError,
2735
+ prefillFromUrl,
2736
+ autoPlaceholder
2606
2737
  };
2607
2738
  const { form, resolvedFields, handleSave, isSaving, isLoading } = useResourceForm(hookOptions);
2608
2739
  const bodyProps = {
@@ -2642,11 +2773,85 @@ function ResourceForm(props) {
2642
2773
  /* @__PURE__ */ jsx(CardContent, { children: /* @__PURE__ */ jsx(FormBody, { ...bodyProps }) })
2643
2774
  ] });
2644
2775
  }
2776
+ function ConfirmDeleteDialog({
2777
+ open,
2778
+ onOpenChange,
2779
+ onConfirm,
2780
+ title = "Delete this record?",
2781
+ description = "This can't be undone.",
2782
+ confirmLabel = "Delete",
2783
+ cancelLabel = "Cancel"
2784
+ }) {
2785
+ const [pending, setPending] = useState(false);
2786
+ const [error, setError] = useState(null);
2787
+ useEffect(() => {
2788
+ if (open) setError(null);
2789
+ }, [open]);
2790
+ const confirm = async () => {
2791
+ setPending(true);
2792
+ setError(null);
2793
+ try {
2794
+ await onConfirm();
2795
+ onOpenChange(false);
2796
+ } catch (err) {
2797
+ setError(err instanceof Error ? err.message : String(err));
2798
+ } finally {
2799
+ setPending(false);
2800
+ }
2801
+ };
2802
+ return /* @__PURE__ */ jsx(
2803
+ AlertDialog,
2804
+ {
2805
+ open,
2806
+ onOpenChange: (next) => {
2807
+ if (!pending) onOpenChange(next);
2808
+ },
2809
+ children: /* @__PURE__ */ jsxs(AlertDialogContent, { "data-testid": "confirm-delete-dialog", children: [
2810
+ /* @__PURE__ */ jsxs(AlertDialogHeader, { children: [
2811
+ /* @__PURE__ */ jsx(AlertDialogTitle, { children: title }),
2812
+ /* @__PURE__ */ jsx(AlertDialogDescription, { children: description })
2813
+ ] }),
2814
+ error ? /* @__PURE__ */ jsxs(
2815
+ "div",
2816
+ {
2817
+ role: "alert",
2818
+ "data-testid": "confirm-delete-error",
2819
+ className: "flex items-start gap-2 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive",
2820
+ children: [
2821
+ /* @__PURE__ */ jsx(AlertTriangle, { className: "mt-0.5 h-4 w-4 shrink-0", "aria-hidden": "true" }),
2822
+ /* @__PURE__ */ jsx("span", { children: error })
2823
+ ]
2824
+ }
2825
+ ) : null,
2826
+ /* @__PURE__ */ jsxs(AlertDialogFooter, { children: [
2827
+ /* @__PURE__ */ jsx(AlertDialogCancel, { disabled: pending, "data-testid": "confirm-delete-cancel", children: cancelLabel }),
2828
+ /* @__PURE__ */ jsxs(
2829
+ AlertDialogAction,
2830
+ {
2831
+ disabled: pending,
2832
+ "data-testid": "confirm-delete-confirm",
2833
+ onClick: (e) => {
2834
+ e.preventDefault();
2835
+ void confirm();
2836
+ },
2837
+ children: [
2838
+ pending ? /* @__PURE__ */ jsx(Spinner, { size: "sm", className: "mr-2" }) : null,
2839
+ confirmLabel
2840
+ ]
2841
+ }
2842
+ )
2843
+ ] })
2844
+ ] })
2845
+ }
2846
+ );
2847
+ }
2645
2848
  function ResourcePage(props) {
2646
2849
  const {
2647
2850
  resource: resourceName,
2648
2851
  data: dataProp,
2649
2852
  total: totalProp,
2853
+ pagination = "client",
2854
+ clientRowLimit = 1e3,
2650
2855
  title,
2651
2856
  subtitle,
2652
2857
  icon,
@@ -2656,6 +2861,8 @@ function ResourcePage(props) {
2656
2861
  detailWidth = "md",
2657
2862
  titleField: titleFieldProp,
2658
2863
  subtitleField: subtitleFieldProp,
2864
+ getTitle,
2865
+ getSubtitle,
2659
2866
  renderDetail: renderDetailProp,
2660
2867
  detailFields,
2661
2868
  statusFilter,
@@ -2689,7 +2896,13 @@ function ResourcePage(props) {
2689
2896
  onCreate,
2690
2897
  onEdit,
2691
2898
  onDelete,
2899
+ canEditItem,
2692
2900
  createLabel = "New",
2901
+ canEdit,
2902
+ canDelete,
2903
+ allowDelete,
2904
+ confirmDelete,
2905
+ onDeleted,
2693
2906
  secondaryActions,
2694
2907
  allowCreate,
2695
2908
  allowEdit,
@@ -2700,12 +2913,17 @@ function ResourcePage(props) {
2700
2913
  formLayout: formLayoutProp,
2701
2914
  bulkActions,
2702
2915
  renderCard: renderCardProp,
2916
+ openOnCardClick = false,
2703
2917
  renderTable: renderTableProp,
2704
2918
  gridCols: gridColsProp,
2705
2919
  keyboardNavigation = true,
2706
2920
  capabilities,
2707
2921
  toolbar,
2708
2922
  emptyState,
2923
+ emptyIcon,
2924
+ emptyMessage,
2925
+ emptyDescription,
2926
+ header: headerMode = "page",
2709
2927
  resourceOptions,
2710
2928
  defaultView = "table",
2711
2929
  onNavigate = defaultNavigate,
@@ -2715,14 +2933,26 @@ function ResourcePage(props) {
2715
2933
  className
2716
2934
  } = props;
2717
2935
  const isClientMode = !!resourceName;
2718
- const isServerControlled = pageProp !== void 0 || perPageProp !== void 0 || onPageChange !== void 0 || onPerPageChange !== void 0 || onSortChange !== void 0 || onSearchChange !== void 0;
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;
2719
2938
  const viewSettingsKey = viewSettingsKeyProp ?? resourceName ?? title.toLowerCase().replace(/\s+/g, "-");
2720
2939
  const { settings, updateSetting } = useViewSettings(viewSettingsKey);
2721
- const effectivePageSize = perPageProp ?? settings.pageSize;
2940
+ const serverSearchFields = resourceOptions?.searchFields ?? searchFields;
2722
2941
  const resourceHook = useResource(
2723
2942
  resourceName ?? "__noop__",
2724
- isClientMode ? resourceOptions : { enabled: false }
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 }
2725
2954
  );
2955
+ const effectivePageSize = resourceServerPaging ? resourceHook.params.pagination?.perPage ?? settings.pageSize : perPageProp ?? settings.pageSize;
2726
2956
  const displayData = isClientMode ? resourceHook.list.data?.data ?? [] : dataProp ?? [];
2727
2957
  const displayTotal = isClientMode ? resourceHook.list.data?.total ?? 0 : totalProp ?? 0;
2728
2958
  const isLoading = isClientMode ? resourceHook.list.isLoading : loadingProp ?? false;
@@ -2742,6 +2972,66 @@ function ResourcePage(props) {
2742
2972
  subtitleField: subtitleFieldProp,
2743
2973
  controlled: isServerControlled
2744
2974
  });
2975
+ 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]);
2745
3035
  const actions = useMemo(() => {
2746
3036
  if (isClientMode) {
2747
3037
  return {
@@ -2774,6 +3064,8 @@ function ResourcePage(props) {
2774
3064
  (item) => getKeyProp ? getKeyProp(item) : String(item.id ?? ""),
2775
3065
  [getKeyProp]
2776
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;
2777
3069
  const [selectedIds, setSelectedIds] = useState(/* @__PURE__ */ new Set());
2778
3070
  const handleToggleSelect = useCallback((id) => {
2779
3071
  setSelectedIds((prev) => {
@@ -2808,6 +3100,8 @@ function ResourcePage(props) {
2808
3100
  }, [useBuiltInCreate, onCreate]);
2809
3101
  const handleEdit = useCallback(
2810
3102
  (item) => {
3103
+ if (canEdit && !canEdit(item)) return;
3104
+ if (canEditItem && !canEditItem(item)) return;
2811
3105
  if (useBuiltInEdit) {
2812
3106
  setFormAction("edit");
2813
3107
  setFormItem(item);
@@ -2816,8 +3110,57 @@ function ResourcePage(props) {
2816
3110
  onEdit(item);
2817
3111
  }
2818
3112
  },
2819
- [useBuiltInEdit, onEdit]
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
+ }
2820
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);
2821
3164
  const tableColumns = useMemo(() => {
2822
3165
  const base = isClientMode ? page.columns.map((col) => ({
2823
3166
  key: col.key,
@@ -2837,19 +3180,24 @@ function ResourcePage(props) {
2837
3180
  page.tableColumns,
2838
3181
  actions
2839
3182
  ]);
2840
- const currentPage = pageProp ?? 1;
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;
2841
3188
  const handleSearchChange = useCallback(
2842
3189
  (value) => {
2843
3190
  page.filterConfig.onSearchChange(value);
2844
3191
  if (!isServerControlled) return;
2845
3192
  onSearchChange?.(value);
2846
- if (currentPage !== 1) onPageChange?.(1);
3193
+ if (currentPage !== 1 && !resourceServerPaging) onPageChange?.(1);
2847
3194
  },
2848
3195
  [
2849
3196
  page.filterConfig,
2850
3197
  isServerControlled,
2851
3198
  onSearchChange,
2852
3199
  onPageChange,
3200
+ resourceServerPaging,
2853
3201
  currentPage
2854
3202
  ]
2855
3203
  );
@@ -2861,24 +3209,43 @@ function ResourcePage(props) {
2861
3209
  onSortChange?.(
2862
3210
  field && (order === "asc" || order === "desc") ? { field, order } : null
2863
3211
  );
2864
- onPageChange?.(1);
3212
+ if (!resourceServerPaging) onPageChange?.(1);
2865
3213
  },
2866
- [page.filterConfig, isServerControlled, onSortChange, onPageChange]
3214
+ [
3215
+ page.filterConfig,
3216
+ isServerControlled,
3217
+ onSortChange,
3218
+ onPageChange,
3219
+ resourceServerPaging
3220
+ ]
2867
3221
  );
2868
3222
  const handleClearFilters = useCallback(() => {
2869
3223
  page.clearFilters();
2870
3224
  if (!isServerControlled) return;
2871
3225
  onSearchChange?.("");
2872
3226
  onSortChange?.(null);
2873
- onPageChange?.(1);
2874
- }, [page, isServerControlled, onSearchChange, onSortChange, onPageChange]);
3227
+ if (!resourceServerPaging) onPageChange?.(1);
3228
+ }, [
3229
+ page,
3230
+ isServerControlled,
3231
+ onSearchChange,
3232
+ onSortChange,
3233
+ onPageChange,
3234
+ resourceServerPaging
3235
+ ]);
2875
3236
  const handleStatusChange = useCallback(
2876
3237
  (values) => {
2877
3238
  page.filterConfig.statusChips?.onChange(values);
2878
3239
  if (!isServerControlled) return;
2879
- if (currentPage !== 1) onPageChange?.(1);
3240
+ if (currentPage !== 1 && !resourceServerPaging) onPageChange?.(1);
2880
3241
  },
2881
- [page.filterConfig, isServerControlled, onPageChange, currentPage]
3242
+ [
3243
+ page.filterConfig,
3244
+ isServerControlled,
3245
+ onPageChange,
3246
+ resourceServerPaging,
3247
+ currentPage
3248
+ ]
2882
3249
  );
2883
3250
  const statusChipsConfig = useMemo(() => {
2884
3251
  const base = page.filterConfig.statusChips;
@@ -2889,12 +3256,20 @@ function ResourcePage(props) {
2889
3256
  (size) => {
2890
3257
  updateSetting("pageSize", size);
2891
3258
  onPerPageChange?.(size);
2892
- if (isServerControlled) onPageChange?.(1);
3259
+ if (resourceServerPaging) resourceHook.setPerPage(size);
3260
+ else if (isServerControlled) onPageChange?.(1);
2893
3261
  },
2894
- [updateSetting, onPerPageChange, isServerControlled, onPageChange]
3262
+ [
3263
+ updateSetting,
3264
+ onPerPageChange,
3265
+ resourceServerPaging,
3266
+ resourceHook.setPerPage,
3267
+ isServerControlled,
3268
+ onPageChange
3269
+ ]
2895
3270
  );
2896
- const missingPerPage = isServerControlled && !!onPageChange && perPageProp === void 0;
2897
- const missingTotal = isServerControlled && !!onPageChange && totalProp === void 0;
3271
+ const missingPerPage = isServerControlled && !!pagerOnPageChange && pagerPerPage === void 0;
3272
+ const missingTotal = isServerControlled && !!pagerOnPageChange && pagerTotal === void 0;
2898
3273
  const pagerMisconfigured = missingPerPage || missingTotal;
2899
3274
  const missingPagerProps = [
2900
3275
  ...missingPerPage ? ["perPage"] : [],
@@ -2909,18 +3284,23 @@ function ResourcePage(props) {
2909
3284
  `ResourcePage: ${missingPagerProps.map((name) => `\`${name}\``).join(" and ")} ${missingPagerProps.length > 1 ? "are" : "is"} required when \`onPageChange\` is supplied.`
2910
3285
  );
2911
3286
  }, [pagerMisconfigured]);
2912
- const pagerPageSize = perPageProp ?? effectivePageSize;
3287
+ const pagerPageSize = pagerPerPage ?? effectivePageSize;
2913
3288
  const totalPages = Math.max(
2914
3289
  1,
2915
3290
  Math.ceil(displayTotal / Math.max(1, pagerPageSize))
2916
3291
  );
2917
3292
  const rangeFrom = page.data.length === 0 ? 0 : (currentPage - 1) * pagerPageSize + 1;
2918
3293
  const rangeTo = rangeFrom === 0 ? 0 : rangeFrom + page.data.length - 1;
2919
- const detailTitle = page.selectedItem ? String(
2920
- page.selectedItem[page.titleField] ?? ""
2921
- ) : "";
2922
- const detailSubtitle = page.selectedItem && page.subtitleField ? String(
2923
- page.selectedItem[page.subtitleField] ?? ""
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] ?? ""
2924
3304
  ) : void 0;
2925
3305
  const handleRowClick = (item) => {
2926
3306
  if (detail === "route") {
@@ -2933,7 +3313,7 @@ function ResourcePage(props) {
2933
3313
  page.handleRowClick(item);
2934
3314
  };
2935
3315
  const hasBulkSelect = !!bulkActions && bulkActions.length > 0;
2936
- const overlayOpen = page.detailOpen || formOpen;
3316
+ const overlayOpen = page.detailOpen || formOpen || deleteOpen;
2937
3317
  const keyboardNav = useKeyboardNavigation({
2938
3318
  itemCount: page.data.length,
2939
3319
  enabled: keyboardNavigation && page.viewMode !== "table",
@@ -3072,6 +3452,10 @@ function ResourcePage(props) {
3072
3452
  hasActiveFilters: page.hasActiveFilters,
3073
3453
  onClearFilters: handleClearFilters,
3074
3454
  emptyAction: emptyState,
3455
+ emptyIcon,
3456
+ emptyMessage,
3457
+ emptyDescription,
3458
+ ...headerMode === "toolbar" ? { header: "toolbar" } : {},
3075
3459
  filters: renderSlot(
3076
3460
  DataFilters,
3077
3461
  slots?.filters,
@@ -3090,7 +3474,7 @@ function ResourcePage(props) {
3090
3474
  onDeleteView,
3091
3475
  canSaveViews,
3092
3476
  onClearAll: page.hasActiveFilters ? handleClearFilters : void 0,
3093
- showSearch: capabilities?.search !== false,
3477
+ showSearch: capabilities?.search !== false && !(resourceServerPaging && !serverSearchFields?.length),
3094
3478
  showSort: capabilities?.sort !== false
3095
3479
  },
3096
3480
  slotProps?.filters
@@ -3156,12 +3540,32 @@ function ResourcePage(props) {
3156
3540
  },
3157
3541
  slotProps?.table
3158
3542
  ),
3543
+ ...openOnCardClick && renderCardProp && detail !== "none" ? {
3544
+ onItemClick: handleRowClick,
3545
+ getItemLabel: (item) => `Open ${titleOf(item)}`.trim()
3546
+ } : {},
3159
3547
  renderCard: (item, index) => renderCardProp ? renderCardProp(item, index, cardActionsFor(item)) : renderDefaultCard(item),
3160
3548
  renderListItem: (item, index) => renderCardProp ? renderCardProp(item, index, cardActionsFor(item)) : renderDefaultListItem(item)
3161
3549
  },
3162
3550
  slotProps?.grid
3163
3551
  ),
3164
- isServerControlled && onPageChange && pagerMisconfigured && /* @__PURE__ */ jsxs(
3552
+ clientTruncated && /* @__PURE__ */ jsxs(
3553
+ "p",
3554
+ {
3555
+ role: "status",
3556
+ className: "px-3 py-2 text-sm text-muted-foreground",
3557
+ "data-testid": "resource-page-truncated",
3558
+ children: [
3559
+ "Showing the first ",
3560
+ displayData.length.toLocaleString(),
3561
+ " of",
3562
+ " ",
3563
+ displayTotal.toLocaleString(),
3564
+ " records."
3565
+ ]
3566
+ }
3567
+ ),
3568
+ isServerControlled && pagerOnPageChange && pagerMisconfigured && /* @__PURE__ */ jsxs(
3165
3569
  "div",
3166
3570
  {
3167
3571
  className: "flex items-center gap-2 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive",
@@ -3186,10 +3590,11 @@ function ResourcePage(props) {
3186
3590
  ]
3187
3591
  }
3188
3592
  ),
3189
- isServerControlled && onPageChange && !pagerMisconfigured && /* @__PURE__ */ jsxs(
3593
+ isServerControlled && pagerOnPageChange && !pagerMisconfigured && /* @__PURE__ */ jsxs(
3190
3594
  "nav",
3191
3595
  {
3192
3596
  "aria-label": "Pagination",
3597
+ "aria-busy": resourceServerPaging && resourceHook.list.isPlaceholderData || void 0,
3193
3598
  className: "flex flex-col sm:flex-row items-center justify-between gap-2 px-3 py-2 text-sm",
3194
3599
  "data-testid": "resource-page-pagination",
3195
3600
  children: [
@@ -3209,7 +3614,7 @@ function ResourcePage(props) {
3209
3614
  variant: "outline",
3210
3615
  size: "sm",
3211
3616
  disabled: currentPage <= 1,
3212
- onClick: () => onPageChange(currentPage - 1),
3617
+ onClick: () => pagerOnPageChange(currentPage - 1),
3213
3618
  testID: "resource-page-prev",
3214
3619
  children: [
3215
3620
  /* @__PURE__ */ jsx(ChevronLeft, { className: "h-4 w-4 mr-1", "aria-hidden": "true" }),
@@ -3236,7 +3641,7 @@ function ResourcePage(props) {
3236
3641
  variant: "outline",
3237
3642
  size: "sm",
3238
3643
  disabled: currentPage >= totalPages,
3239
- onClick: () => onPageChange(currentPage + 1),
3644
+ onClick: () => pagerOnPageChange(currentPage + 1),
3240
3645
  testID: "resource-page-next",
3241
3646
  children: [
3242
3647
  "Next",
@@ -3252,16 +3657,13 @@ function ResourcePage(props) {
3252
3657
  DetailPanel,
3253
3658
  slots?.detail,
3254
3659
  {
3255
- item: page.selectedItem,
3660
+ item: selectedItem,
3256
3661
  open: page.detailOpen,
3257
- onClose: () => {
3258
- page.setDetailOpen(false);
3259
- page.setSelectedItem(null);
3260
- },
3662
+ onClose: closeDetail,
3261
3663
  title: detailTitle,
3262
3664
  subtitle: detailSubtitle,
3263
- onEdit: onEdit ?? (useBuiltInEdit ? handleEdit : void 0),
3264
- onDelete,
3665
+ onEdit: selectedEditable ? onEdit ?? (useBuiltInEdit ? handleEdit : void 0) : void 0,
3666
+ onDelete: selectedDeletable ? deleteHandler : void 0,
3265
3667
  fields: detailFields,
3266
3668
  width: detailWidth,
3267
3669
  layout: detail === "modal" ? "dialog" : "sheet",
@@ -3287,11 +3689,24 @@ function ResourcePage(props) {
3287
3689
  listData: page.data,
3288
3690
  renderForm: renderFormProp,
3289
3691
  onSuccess: () => {
3290
- page.setDetailOpen(false);
3291
- page.setSelectedItem(null);
3692
+ if (formAction === "create") closeDetail();
3292
3693
  }
3293
3694
  },
3294
3695
  slotProps?.form
3696
+ ),
3697
+ deleteConfirmed && (useBuiltInDelete || !!onDelete) && renderSlot(
3698
+ ConfirmDeleteDialog,
3699
+ slots?.deleteDialog,
3700
+ {
3701
+ open: deleteOpen,
3702
+ onOpenChange: setDeleteOpen,
3703
+ onConfirm: () => pendingDelete ? performDelete(pendingDelete) : void 0,
3704
+ title: typeof deleteCopy?.title === "function" ? pendingDelete ? deleteCopy.title(pendingDelete) : void 0 : deleteCopy?.title,
3705
+ description: typeof deleteCopy?.description === "function" ? pendingDelete ? deleteCopy.description(pendingDelete) : void 0 : deleteCopy?.description,
3706
+ confirmLabel: deleteCopy?.confirmLabel,
3707
+ cancelLabel: deleteCopy?.cancelLabel
3708
+ },
3709
+ slotProps?.deleteDialog
3295
3710
  )
3296
3711
  ]
3297
3712
  }
@@ -3371,6 +3786,6 @@ function ResourceDetailPage({
3371
3786
  var AppShell2 = AppShell;
3372
3787
  var PageHeader2 = PageHeader;
3373
3788
 
3374
- export { AppShell2 as AppShell, BadgeCell, CurrencyCell, DataBulkBar, DataFilters, DataGrid, DataList, DataPageShell, DataTable, DateCell, DetailPanel, FieldArrayRenderer, FileUploadField, FilterCombobox, KanbanBoard, LinkCell, NumberCell, PageHeader2 as PageHeader, RelationField, ResourceDetailPage, ResourceForm, ResourcePage, StatCard, ThumbnailCell, WizardForm };
3789
+ export { AppShell2 as AppShell, BadgeCell, ConfirmDeleteDialog, CurrencyCell, DataBulkBar, DataFilters, DataGrid, DataList, DataPageShell, DataTable, DateCell, DetailPanel, FieldArrayRenderer, FileUploadField, FilterCombobox, KanbanBoard, LinkCell, NumberCell, PageHeader2 as PageHeader, RelationField, ResourceDetailPage, ResourceForm, ResourcePage, StatCard, ThumbnailCell, WizardForm };
3375
3790
  //# sourceMappingURL=index.js.map
3376
3791
  //# sourceMappingURL=index.js.map