@marktiderman/genesis-ui 2.8.0 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SKILL.md CHANGED
@@ -56,7 +56,7 @@ Reach for these before a slot. Each one replaced a slot workaround in a real app
56
56
  | You need | Prop |
57
57
  | --- | --- |
58
58
  | Every row reachable in `resource` mode | default `pagination="client"` fetches up to `clientRowLimit` (1000) and says so past it; `pagination="server"` pages through the provider (give it `searchFields`) |
59
- | Per-row rights in the peek | `canEditItem(item)` for both, or `canEdit(item)` / `canDelete(item)` separately (UI gate only; the server still decides) |
59
+ | Who may create, edit, delete | `can(action, item)` (UI gate only; the server still decides) |
60
60
  | A confirmed delete | `allowDelete` (built in, `resource` mode), or `onDelete` + `confirmDelete`; copy via `confirmDelete={{ title: (item) => … }}`, toast via `onDeleted` |
61
61
  | A computed peek subtitle | `getSubtitle(item)`; the peek stays open after an edit and shows the saved row |
62
62
  | A list inside a tab (no page title) | `header="toolbar"` (also on `DataPageShell`) |
@@ -68,6 +68,49 @@ Reach for these before a slot. Each one replaced a slot workaround in a real app
68
68
  honours `maxLength` on text fields, adds no placeholder unless the field sets one (or
69
69
  `autoPlaceholder`), and takes `prefillFromUrl={false}` when the page's query string is not form data.
70
70
 
71
+ `ResourceForm` also:
72
+
73
+ - puts a provider's field-level errors (`DataWriteError.fields`; the Supabase provider maps
74
+ unique, not-null and foreign-key violations) on their inputs, with the summary in the alert;
75
+ - takes `warnWhenUnsaved` (default off): Cancel / Esc / outside click ask before dropping
76
+ changes, and the tab's close or reload prompts;
77
+ - takes `mutationMode="optimistic" | "undoable"` (default `"pessimistic"`): the form closes at
78
+ once and the cached list updates; `undoable` shows an Undo toast and sends the save after
79
+ `undoDelayMs` (5000) unless undone. A failure puts the list back and is toasted. Needs a
80
+ mounted `<Toaster />`. `ResourcePage` passes its own `mutationMode` to its form and deletes.
81
+
82
+ ## Unsaved changes
83
+
84
+ Core never depends on a router. `useResourceForm` returns `isDirty` and `confirmLeave()`
85
+ (true when leaving is fine); `ResourceForm` reports both through `onDirtyChange`. For any
86
+ other state use `useUnsavedChangesGuard({ when })` from core. Wire `confirmLeave` to your
87
+ router's blocker:
88
+
89
+ ```tsx
90
+ // React Router (data router, v6.4+ / v7)
91
+ import { useBlocker } from "react-router"; // "react-router-dom" on v6
92
+
93
+ const { form, isDirty, confirmLeave } = useResourceForm({ …, warnWhenUnsaved: true });
94
+ const blocker = useBlocker(isDirty);
95
+ useEffect(() => {
96
+ if (blocker.state !== "blocked") return;
97
+ if (confirmLeave()) blocker.proceed();
98
+ else blocker.reset();
99
+ }, [blocker, confirmLeave]);
100
+ ```
101
+
102
+ ```tsx
103
+ // TanStack Router (v1)
104
+ import { useBlocker } from "@tanstack/react-router";
105
+
106
+ const { isDirty, confirmLeave } = useResourceForm({ …, warnWhenUnsaved: true });
107
+ // The hook already handles beforeunload, so the router's own is off.
108
+ useBlocker({ shouldBlockFn: () => !confirmLeave(), disabled: !isDirty, enableBeforeUnload: false });
109
+ ```
110
+
111
+ With `ResourceForm`, keep the pair from `onDirtyChange={(dirty, confirmLeave) => …}` in state
112
+ and use it the same way.
113
+
71
114
  ## The layer contract (docs/FRAMEWORK.md)
72
115
 
73
116
  1. Layers: tokens → primitives → patterns → layouts → data-bound; each depends downward only.
@@ -129,16 +172,27 @@ DataGrid and DetailPanel yourself. The parts apps used to hand-roll are props:
129
172
  - **Clickable cards:** with a custom `renderCard`, `openOnCardClick` makes the whole card one
130
173
  keyboard-reachable button that opens the detail. On a bare `DataGrid`, use `onItemClick` and
131
174
  `getItemLabel`. Never wrap a card in a `div role="button"`.
132
- - **Per-row rights:** `canEditItem(item)` hides Edit and Delete for rows the user can't change;
133
- `canEdit(item)` / `canDelete(item)` gate each one separately. `canEditItem` is checked first: a row it
134
- refuses gets neither button, whatever the pair says.
175
+ - **Permissions:** `can(action, item)` with `"create"`, `"edit"` or `"delete"` hides the New button or
176
+ a row's Edit / Delete (a UI gate; the server still decides). `canEditItem`, `canEdit` and `canDelete`
177
+ are deprecated aliases, still honoured.
135
178
  - **Names and empty state:** `getTitle` / `getSubtitle` when a record's name spans fields;
136
179
  `emptyIcon`, `emptyMessage`, `emptyDescription` and `emptyState` (its action) for the empty list.
137
180
  - **A list inside tabs:** `header="toolbar"` (on `ResourcePage` or `DataPageShell`) drops the title
138
181
  block and keeps the count, view toggle, filters and create button.
182
+ - **Shareable state:** `urlState={useReactRouterSearchParams()}` (or the TanStack / browser adapter) keeps
183
+ the open record (`?id=`), search, sort and, with `pagination="server"`, the page in the URL. Back
184
+ closes a record that was opened. A page that owns its own paging uses `useResourceUrlState`.
139
185
  - **Non-destructive confirm:** `<AlertDialogAction variant="default">` for publish/send; red stays
140
186
  the default for deletes.
141
187
 
188
+ ## Big tables
189
+
190
+ `DataTable` runs on TanStack Table. Sorting compares numbers, dates and booleans by value. For a long
191
+ list without pages, `pagination={false} virtualize` renders only the rows in view (`virtualize={{ height }}`
192
+ sets the scroll box). `pinnedColumns={{ left: ["name"], right: ["actions"] }}` keeps columns in view
193
+ when scrolling sideways. Users can reorder columns by dragging a header or with Alt+←/→, and the order
194
+ is kept per device with `columnOrderKey`.
195
+
142
196
  ## Full inventory
143
197
 
144
198
  `docs/component-reference.md` lists every export with its props and stability tag. It is
@@ -0,0 +1,58 @@
1
+ "use client";
2
+ import { useCallback, useMemo } from 'react';
3
+
4
+ // src/hooks/use-resource-url-state.ts
5
+ var DEFAULTS = { detail: "id", page: "page", sort: "sort", q: "q" };
6
+ function parseSort(raw) {
7
+ if (!raw) return null;
8
+ const at = raw.lastIndexOf(":");
9
+ const field = at > 0 ? raw.slice(0, at) : "";
10
+ const order = raw.slice(at + 1);
11
+ return field && (order === "asc" || order === "desc") ? { field, order } : null;
12
+ }
13
+ function useResourceUrlState(options) {
14
+ const adapter = options?.searchParams;
15
+ const names = { ...DEFAULTS, ...options?.params };
16
+ const sp = adapter?.searchParams;
17
+ const setSearchParams = adapter?.setSearchParams;
18
+ const detailId = sp?.get(names.detail) || null;
19
+ const rawPage = sp?.get(names.page);
20
+ const pageNum = rawPage && /^\d+$/.test(rawPage) ? Number(rawPage) : null;
21
+ const page = pageNum !== null && Number.isSafeInteger(pageNum) && pageNum > 0 ? pageNum : null;
22
+ const sortRaw = sp?.get(names.sort) ?? null;
23
+ const q = sp?.get(names.q) ?? "";
24
+ const setDetailId = useCallback(
25
+ (id) => {
26
+ if (!sp || !setSearchParams) return;
27
+ const next = new URLSearchParams(sp);
28
+ if (id) next.set(names.detail, id);
29
+ else next.delete(names.detail);
30
+ if (next.toString() === sp.toString()) return;
31
+ setSearchParams(next, { replace: !id });
32
+ },
33
+ [sp, setSearchParams, names.detail]
34
+ );
35
+ const write = useCallback(
36
+ (values) => {
37
+ if (!sp || !setSearchParams) return;
38
+ const next = new URLSearchParams(sp);
39
+ const put = (key, value) => {
40
+ if (value) next.set(key, value);
41
+ else next.delete(key);
42
+ };
43
+ if (values.page !== void 0) put(names.page, values.page && values.page > 1 ? String(values.page) : null);
44
+ if (values.sort !== void 0) put(names.sort, values.sort ? `${values.sort.field}:${values.sort.order}` : null);
45
+ if (values.q !== void 0) put(names.q, values.q || null);
46
+ if (next.toString() === sp.toString()) return;
47
+ setSearchParams(next, { replace: true });
48
+ },
49
+ [sp, setSearchParams, names.page, names.sort, names.q]
50
+ );
51
+ const sort = useMemo(() => parseSort(sortRaw), [sortRaw]);
52
+ if (!adapter) return void 0;
53
+ return { detailId, setDetailId, page, sort, q, write };
54
+ }
55
+
56
+ export { useResourceUrlState };
57
+ //# sourceMappingURL=chunk-DIV4BYXX.js.map
58
+ //# sourceMappingURL=chunk-DIV4BYXX.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/hooks/use-resource-url-state.ts"],"names":[],"mappings":"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","file":"chunk-DIV4BYXX.js","sourcesContent":["\"use client\";\n\nimport { useCallback, useMemo } from \"react\";\nimport type { SearchParamsAdapter } from \"../navigation\";\nimport type { SortParam } from \"@marktiderman/genesis-core/provider\";\n\n/**\n * The query-string names a list keeps its state under. Rename one when two\n * lists share a page, or when `id` already means something in your routes.\n *\n * @stability Beta\n */\nexport interface ResourceUrlParams {\n /** The open record's key. Default `\"id\"`. */\n detail?: string;\n /** The page number (from 1). Default `\"page\"`. */\n page?: string;\n /** `field:asc` or `field:desc`. Default `\"sort\"`. */\n sort?: string;\n /** The search text. Default `\"q\"`. */\n q?: string;\n}\n\n/**\n * Where a list's state lives in the URL: the router's search params, plus\n * optional names for each value.\n *\n * @stability Beta\n */\nexport interface ResourceUrlStateOptions {\n searchParams: SearchParamsAdapter;\n params?: ResourceUrlParams;\n}\n\n/** @stability Beta */\nexport interface ResourceUrlState {\n /** The open record's key, or null when no record is open. */\n detailId: string | null;\n /** Open a record (a new history entry, so Back closes it) or close it (null; replaces). */\n setDetailId: (id: string | null) => void;\n /** The page from the URL, or null when absent or not a positive integer. */\n page: number | null;\n /** The sort from the URL, or null. */\n sort: SortParam | null;\n /** The search text from the URL (\"\" when absent). */\n q: string;\n /**\n * Write page, sort and search. Replaces the history entry (typing a search\n * shouldn't fill Back with every keystroke). A default value (page 1, no\n * sort, empty search) is dropped from the URL; `undefined` leaves a value as is.\n */\n write: (next: { page?: number | null; sort?: SortParam | null; q?: string }) => void;\n}\n\nconst DEFAULTS: Required<ResourceUrlParams> = { detail: \"id\", page: \"page\", sort: \"sort\", q: \"q\" };\n\nfunction parseSort(raw: string | null): SortParam | null {\n if (!raw) return null;\n const at = raw.lastIndexOf(\":\");\n const field = at > 0 ? raw.slice(0, at) : \"\";\n const order = raw.slice(at + 1);\n return field && (order === \"asc\" || order === \"desc\") ? { field, order } : null;\n}\n\n/**\n * A list's peek, page, sort and search, kept in the URL through any\n * {@link SearchParamsAdapter} (the browser's, React Router's or TanStack\n * Router's). The URL is the source of truth: a copied link reopens the same\n * record on the same page, and Back closes a record that was opened.\n *\n * `ResourcePage` uses it when given `urlState`; a server-controlled page\n * that owns its own page/sort/search can call it directly.\n *\n * @stability Beta\n */\nexport function useResourceUrlState(options: ResourceUrlStateOptions): ResourceUrlState;\nexport function useResourceUrlState(options: ResourceUrlStateOptions | undefined): ResourceUrlState | undefined;\nexport function useResourceUrlState(options: ResourceUrlStateOptions | undefined): ResourceUrlState | undefined {\n const adapter = options?.searchParams;\n const names = { ...DEFAULTS, ...options?.params };\n const sp = adapter?.searchParams;\n const setSearchParams = adapter?.setSearchParams;\n\n const detailId = sp?.get(names.detail) || null;\n const rawPage = sp?.get(names.page);\n const pageNum = rawPage && /^\\d+$/.test(rawPage) ? Number(rawPage) : null;\n // A positive safe integer only: hundreds of digits parse to Infinity.\n const page = pageNum !== null && Number.isSafeInteger(pageNum) && pageNum > 0 ? pageNum : null;\n const sortRaw = sp?.get(names.sort) ?? null;\n const q = sp?.get(names.q) ?? \"\";\n\n const setDetailId = useCallback(\n (id: string | null) => {\n if (!sp || !setSearchParams) return;\n const next = new URLSearchParams(sp);\n if (id) next.set(names.detail, id);\n else next.delete(names.detail);\n if (next.toString() === sp.toString()) return;\n // Opening adds an entry so Back closes the record; closing replaces.\n setSearchParams(next, { replace: !id });\n },\n [sp, setSearchParams, names.detail],\n );\n\n const write = useCallback<ResourceUrlState[\"write\"]>(\n (values) => {\n if (!sp || !setSearchParams) return;\n const next = new URLSearchParams(sp);\n const put = (key: string, value: string | null) => {\n if (value) next.set(key, value);\n else next.delete(key);\n };\n if (values.page !== undefined) put(names.page, values.page && values.page > 1 ? String(values.page) : null);\n if (values.sort !== undefined) put(names.sort, values.sort ? `${values.sort.field}:${values.sort.order}` : null);\n if (values.q !== undefined) put(names.q, values.q || null);\n if (next.toString() === sp.toString()) return;\n setSearchParams(next, { replace: true });\n },\n [sp, setSearchParams, names.page, names.sort, names.q],\n );\n\n const sort = useMemo(() => parseSort(sortRaw), [sortRaw]);\n\n if (!adapter) return undefined;\n return { detailId, setDetailId, page, sort, q, write };\n}\n"]}
@@ -78,10 +78,11 @@ function useResourcePage(props) {
78
78
  subtitleField: subtitleFieldProp,
79
79
  initialFilters,
80
80
  onFiltersChange,
81
- controlled = false
81
+ controlled = false,
82
+ viewKey
82
83
  } = props;
83
84
  const [viewMode, setViewMode] = useViewPreference(
84
- resource ?? "resource-page",
85
+ viewKey ?? resource ?? "resource-page",
85
86
  defaultView
86
87
  );
87
88
  const initialSearch = useMemo(() => {
@@ -293,5 +294,5 @@ function useResourcePage(props) {
293
294
  }
294
295
 
295
296
  export { titleCase, useResourcePage };
296
- //# sourceMappingURL=chunk-DWCAC2E7.js.map
297
- //# sourceMappingURL=chunk-DWCAC2E7.js.map
297
+ //# sourceMappingURL=chunk-RKPS7ERS.js.map
298
+ //# sourceMappingURL=chunk-RKPS7ERS.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/hooks/use-resource-page.ts"],"names":[],"mappings":"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","file":"chunk-RKPS7ERS.js","sourcesContent":["\"use client\";\n\nimport { useState, useMemo, useCallback, useEffect, useRef } from \"react\";\nimport type { ReactNode } from \"react\";\nimport type { DataTableColumn } from \"../components/data/DataTable\";\nimport type { DataFilterConfig } from \"../components/data/DataFilters\";\nimport type { ViewMode } from \"../components/patterns/view-toggle\";\nimport type { Identifier, FilterParam, SortParam } from \"@marktiderman/genesis-core/provider\";\nimport { useViewPreference, type ViewModeDefault } from \"./use-view-preference\";\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/** Actions available inside column render functions. */\nexport interface ResourceActions<T> {\n update: (id: Identifier, data: Partial<T>) => Promise<T>;\n remove: (id: Identifier) => Promise<void>;\n refetch: () => void;\n}\n\n/** Column definition — full config form. */\nexport interface ResourceColumnDef<T> {\n key: string;\n header?: string;\n sortable?: boolean;\n hideBelow?: \"sm\" | \"md\" | \"lg\" | \"xl\";\n /** Starting width, passed straight through to `DataTable` (a number is px). */\n width?: DataTableColumn<T>[\"width\"];\n /** Smallest width the user can resize to, in px. Passed through to `DataTable`. */\n minWidth?: number;\n /** Whether the column shows before the user picks columns. Default true. */\n defaultVisible?: boolean;\n render?: (\n item: T,\n value: unknown,\n actions: ResourceActions<T>,\n ) => ReactNode;\n /** In-place cell editing, passed straight through to `DataTable`. */\n editable?: DataTableColumn<T>[\"editable\"];\n /**\n * The cell's value, when it isn't `item[key]`. Passed straight through to\n * `DataTable`, which also starts an edit from `String(accessor(item))`.\n */\n accessor?: (item: T) => unknown;\n}\n\nexport interface UseResourcePageProps<T> {\n /** Resource name — used for view-preference storage key. */\n resource?: string;\n /** The data to display (pre-fetched or from useResource). */\n data: T[];\n /** Total count of records (before client-side filtering). */\n total: number;\n /** Column definitions. Strings are expanded to `{ key, header, sortable }`. Omit to auto-generate from data keys. */\n columns?: Array<string | ResourceColumnDef<T>>;\n /** Status filter config. String = field name (options auto-derived). Object = explicit field + options. */\n statusFilter?: string | { field: string; options?: string[] };\n /** Fields to search across. Defaults to all string-valued fields in data. */\n searchFields?: string[];\n /** Initial filter params applied on mount. */\n initialFilters?: FilterParam[];\n /** Callback when filters change. */\n onFiltersChange?: (filters: FilterParam[]) => void;\n /**\n * Default view mode, used until the user picks one. Pass\n * `{ mobile, desktop }` for a viewport-dependent default (cards on phones,\n * a table on desktops); only an explicit pick is persisted.\n */\n defaultView?: ViewModeDefault;\n /**\n * Where the table/cards/list choice is remembered. Default: `resource`,\n * else one key shared by every keyless page — so pages without a\n * `resource` (rows passed in) should each set their own.\n */\n viewKey?: string;\n /** Detail interaction mode. */\n detail?: \"panel\" | \"modal\" | \"route\" | \"none\";\n /** Field used for the detail title. Defaults to first column key. */\n titleField?: string;\n /** Field used for the detail subtitle. Defaults to second column key. */\n subtitleField?: string;\n /**\n * Server-controlled mode. The `data` handed in has ALREADY been searched,\n * sorted and paginated by the server, so this hook must not do any of it a\n * second time — `data` is returned untouched and `total` is the server's\n * `total` rather than the length of a client-filtered slice.\n *\n * Search / sort / status state is still tracked (the filter UI stays\n * controlled and `onFiltersChange` still fires) — only the client-side\n * transform of `data` is skipped. Defaults to `false`, so every existing\n * caller keeps its current client-side behavior.\n */\n controlled?: boolean;\n}\n\nexport interface UseResourcePageReturn<T> {\n /** Filtered and sorted data for display. */\n data: T[];\n /** Total count (of the filtered set; the server's `total` when `controlled`). */\n total: number;\n /** Resolved column definitions. */\n columns: ResourceColumnDef<T>[];\n /** Columns mapped to DataTable format. */\n tableColumns: DataTableColumn<T>[];\n /** Ready-to-spread filter config for DataFilters. */\n filterConfig: {\n search: string;\n onSearchChange: (value: string) => void;\n sort: string;\n onSortChange: (sort: string) => void;\n sortOptions: Array<{ value: string; label: string }>;\n statusChips?: {\n options: string[];\n selected: string[];\n onChange: (values: string[]) => void;\n };\n };\n viewMode: ViewMode;\n setViewMode: (mode: ViewMode) => void;\n search: string;\n setSearch: (value: string) => void;\n sort: SortParam | null;\n setSort: (sort: SortParam | null) => void;\n statusValue: string[];\n setStatusValue: (values: string[]) => void;\n selectedItem: T | null;\n setSelectedItem: (item: T | null) => void;\n detailOpen: boolean;\n setDetailOpen: (open: boolean) => void;\n handleRowClick: (item: T) => void;\n titleField: string;\n subtitleField: string;\n statusOptions: string[];\n hasActiveFilters: boolean;\n clearFilters: () => void;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Convert a key string to title case.\n * \"created_at\" → \"Created At\"\n * \"firstName\" → \"First Name\"\n */\nexport function titleCase(str: string): string {\n // Split on underscores, hyphens, and camelCase boundaries\n const words = str\n .replace(/([a-z])([A-Z])/g, \"$1 $2\") // camelCase → camel Case\n .replace(/[_-]/g, \" \") // snake_case / kebab-case → spaces\n .split(/\\s+/)\n .filter(Boolean);\n\n return words\n .map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())\n .join(\" \");\n}\n\n/**\n * Resolve column shorthand into full ResourceColumnDef objects.\n * - undefined + data present → auto-generate from Object.keys(data[0]), skip \"id\"\n * - undefined + empty data → empty array\n * - string items → { key, header: titleCase(key), sortable: true }\n * - object items → defaults applied\n */\nfunction resolveColumns<T>(\n columns: Array<string | ResourceColumnDef<T>> | undefined,\n data: T[],\n): ResourceColumnDef<T>[] {\n if (columns === undefined) {\n if (data.length === 0) return [];\n const keys = Object.keys(data[0] as Record<string, unknown>).filter(\n (k) => k !== \"id\",\n );\n return keys.map((key) => ({\n key,\n header: titleCase(key),\n sortable: true,\n }));\n }\n\n return columns.map((col) => {\n if (typeof col === \"string\") {\n return { key: col, header: titleCase(col), sortable: true };\n }\n return {\n ...col,\n header: col.header ?? titleCase(col.key),\n sortable: col.sortable ?? true,\n };\n });\n}\n\n/**\n * Derive the list of string fields in a data set (for default search).\n */\nfunction deriveStringFields<T>(data: T[]): string[] {\n if (data.length === 0) return [];\n const first = data[0] as Record<string, unknown>;\n return Object.keys(first).filter((k) => typeof first[k] === \"string\");\n}\n\n/**\n * Derive unique status options from a data array.\n */\nfunction deriveStatusOptions<T>(data: T[], field: string): string[] {\n const values = new Set<string>();\n for (const item of data) {\n const val = (item as Record<string, unknown>)[field];\n if (val != null) values.add(String(val));\n }\n return Array.from(values).sort();\n}\n\n/**\n * Client-side text search across multiple fields.\n */\nfunction matchesSearch<T>(\n item: T,\n query: string,\n fields: string[],\n): boolean {\n if (!query) return true;\n const lower = query.toLowerCase();\n const record = item as Record<string, unknown>;\n return fields.some((f) => {\n const val = record[f];\n return val != null && String(val).toLowerCase().includes(lower);\n });\n}\n\n/**\n * Client-side sort comparator.\n */\nfunction compareItems<T>(a: T, b: T, sort: SortParam): number {\n const ra = a as Record<string, unknown>;\n const rb = b as Record<string, unknown>;\n const va = ra[sort.field];\n const vb = rb[sort.field];\n const sa = va == null ? \"\" : String(va);\n const sb = vb == null ? \"\" : String(vb);\n const cmp = sa.localeCompare(sb, undefined, {\n numeric: true,\n sensitivity: \"base\",\n });\n return sort.order === \"asc\" ? cmp : -cmp;\n}\n\n// ---------------------------------------------------------------------------\n// Hook\n// ---------------------------------------------------------------------------\n\nexport function useResourcePage<T = Record<string, unknown>>(\n props: UseResourcePageProps<T>,\n): UseResourcePageReturn<T> {\n const {\n resource,\n data: rawData,\n total: rawTotal,\n columns: columnsProp,\n statusFilter: statusFilterProp,\n searchFields: searchFieldsProp,\n defaultView = \"table\",\n detail = \"panel\",\n titleField: titleFieldProp,\n subtitleField: subtitleFieldProp,\n initialFilters,\n onFiltersChange,\n controlled = false,\n viewKey,\n } = props;\n\n // -- View mode --\n const [viewMode, setViewMode] = useViewPreference(\n viewKey ?? resource ?? \"resource-page\",\n defaultView,\n );\n\n // -- Derive initial values from initialFilters --\n const initialSearch = useMemo(() => {\n if (!initialFilters) return \"\";\n const searchFilter = initialFilters.find(\n (f) => f.operator === \"contains\" && f.field === \"__search__\",\n );\n return searchFilter && \"value\" in searchFilter ? String(searchFilter.value ?? \"\") : \"\";\n }, []); // eslint-disable-line react-hooks/exhaustive-deps -- only on mount\n\n const initialSort = useMemo(() => {\n if (!initialFilters) return null;\n const sortFilter = initialFilters.find(\n (f) => f.field === \"__sort__\",\n );\n if (!sortFilter || !(\"value\" in sortFilter) || typeof sortFilter.value !== \"string\") return null;\n const [field, order] = sortFilter.value.split(\":\");\n if (!field || (order !== \"asc\" && order !== \"desc\")) return null;\n return { field, order } as SortParam;\n }, []); // eslint-disable-line react-hooks/exhaustive-deps -- only on mount\n\n const initialStatus = useMemo(() => {\n if (!initialFilters) return [];\n const statusFilter = initialFilters.find(\n (f) => f.operator === \"in\" && f.field === \"__status__\",\n );\n if (!statusFilter || statusFilter.operator !== \"in\") return [];\n return statusFilter.value.map((v) => String(v));\n }, []); // eslint-disable-line react-hooks/exhaustive-deps -- only on mount\n\n // -- Search --\n const [search, setSearch] = useState(initialSearch);\n\n // -- Sort --\n const [sort, setSort] = useState<SortParam | null>(initialSort);\n\n // -- Status filter --\n const [statusValue, setStatusValue] = useState<string[]>(initialStatus);\n\n // -- Detail panel --\n const [selectedItem, setSelectedItem] = useState<T | null>(null);\n const [detailOpen, setDetailOpen] = useState(false);\n\n // -- Resolve columns --\n const resolvedColumns = useMemo(\n () => resolveColumns(columnsProp, rawData),\n [columnsProp, rawData],\n );\n\n // -- Title / subtitle fields --\n const titleField =\n titleFieldProp ?? resolvedColumns[0]?.key ?? \"name\";\n const subtitleField =\n subtitleFieldProp ?? resolvedColumns[1]?.key ?? \"\";\n\n // -- Status filter config --\n const statusFieldName = useMemo(() => {\n if (!statusFilterProp) return null;\n return typeof statusFilterProp === \"string\"\n ? statusFilterProp\n : statusFilterProp.field;\n }, [statusFilterProp]);\n\n const statusOptions = useMemo(() => {\n if (!statusFilterProp) return [];\n if (typeof statusFilterProp === \"object\" && statusFilterProp.options) {\n return statusFilterProp.options;\n }\n if (!statusFieldName) return [];\n return deriveStatusOptions(rawData, statusFieldName);\n }, [statusFilterProp, statusFieldName, rawData]);\n\n // -- Search fields --\n const searchFields = useMemo(\n () => searchFieldsProp ?? deriveStringFields(rawData),\n [searchFieldsProp, rawData],\n );\n\n // -- Client-side filtering + sorting --\n // Skipped entirely in controlled mode: the server already applied the\n // search, the sort and the page window, so re-running them here would\n // filter the current page against a query the server has already honoured\n // (and re-sort a slice of rows that is not the full result set).\n const filteredData = useMemo(() => {\n if (controlled) return rawData;\n\n let result = rawData;\n\n // Text search\n if (search) {\n result = result.filter((item) =>\n matchesSearch(item, search, searchFields),\n );\n }\n\n // Status filter\n if (statusFieldName && statusValue.length > 0) {\n result = result.filter((item) => {\n const val = (item as Record<string, unknown>)[statusFieldName];\n return val != null && statusValue.includes(String(val));\n });\n }\n\n // Sort\n if (sort) {\n result = [...result].sort((a, b) => compareItems(a, b, sort));\n }\n\n return result;\n }, [controlled, rawData, search, searchFields, statusFieldName, statusValue, sort]);\n\n // -- Sort options (built from resolved columns) --\n const sortOptions = useMemo(() => {\n const opts: Array<{ value: string; label: string }> = [];\n for (const col of resolvedColumns) {\n if (col.sortable !== false) {\n opts.push({\n value: `${col.key}:asc`,\n label: `${col.header ?? titleCase(col.key)} (A-Z)`,\n });\n opts.push({\n value: `${col.key}:desc`,\n label: `${col.header ?? titleCase(col.key)} (Z-A)`,\n });\n }\n }\n return opts;\n }, [resolvedColumns]);\n\n // -- Sort string for DataFilters select --\n const sortString = sort ? `${sort.field}:${sort.order}` : \"\";\n\n const handleSortChange = useCallback((value: string) => {\n if (!value) {\n setSort(null);\n return;\n }\n const [field, order] = value.split(\":\");\n setSort({ field, order: order as \"asc\" | \"desc\" });\n }, []);\n\n // -- Map to DataTableColumn --\n const tableColumns: DataTableColumn<T>[] = useMemo(\n () =>\n resolvedColumns.map((col) => ({\n key: col.key,\n header: col.header ?? titleCase(col.key),\n sortable: col.sortable ?? true,\n hideBelow: col.hideBelow,\n width: col.width,\n minWidth: col.minWidth,\n defaultVisible: col.defaultVisible,\n editable: col.editable,\n accessor: col.accessor,\n render: col.render\n ? (item: T, value: unknown) =>\n col.render!(item, value, {\n update: async () => {\n throw new Error(\n \"ResourceActions.update not available in server-data mode\",\n );\n },\n remove: async () => {\n throw new Error(\n \"ResourceActions.remove not available in server-data mode\",\n );\n },\n refetch: () => {\n /* no-op in server-data mode */\n },\n })\n : undefined,\n })),\n [resolvedColumns],\n );\n\n // -- Active filters check --\n const hasActiveFilters =\n search !== \"\" || statusValue.length > 0 || sort !== null;\n\n const clearFilters = useCallback(() => {\n setSearch(\"\");\n setStatusValue([]);\n setSort(null);\n }, []);\n\n // -- Notify parent of filter changes --\n const onFiltersChangeRef = useRef(onFiltersChange);\n onFiltersChangeRef.current = onFiltersChange;\n\n useEffect(() => {\n if (!onFiltersChangeRef.current) return;\n const filters: FilterParam[] = [];\n if (search) {\n filters.push({ field: \"__search__\", operator: \"contains\", value: search });\n }\n if (sort) {\n filters.push({ field: \"__sort__\", operator: \"eq\", value: `${sort.field}:${sort.order}` });\n }\n if (statusValue.length > 0) {\n filters.push({ field: \"__status__\", operator: \"in\", value: statusValue });\n }\n onFiltersChangeRef.current(filters);\n }, [search, sort, statusValue]);\n\n // -- Row click --\n const handleRowClick = useCallback(\n (item: T) => {\n if (detail === \"none\") return;\n setSelectedItem(item);\n setDetailOpen(true);\n },\n [detail],\n );\n\n // -- Filter config (ready to spread into DataFilters) --\n const filterConfig = useMemo(\n () => ({\n search,\n onSearchChange: setSearch,\n sort: sortString,\n onSortChange: handleSortChange,\n sortOptions,\n ...(statusFieldName && statusOptions.length > 0\n ? {\n statusChips: {\n options: statusOptions,\n selected: statusValue,\n onChange: setStatusValue,\n },\n }\n : {}),\n }),\n [\n search,\n sortString,\n handleSortChange,\n sortOptions,\n statusFieldName,\n statusOptions,\n statusValue,\n ],\n );\n\n return {\n data: filteredData,\n // Controlled mode: `filteredData` is one server page, so its length is\n // not the record count — the server's total is.\n total: controlled ? rawTotal : filteredData.length,\n columns: resolvedColumns,\n tableColumns,\n filterConfig,\n viewMode,\n setViewMode,\n search,\n setSearch,\n sort,\n setSort,\n statusValue,\n setStatusValue,\n selectedItem,\n setSelectedItem,\n detailOpen,\n setDetailOpen,\n handleRowClick,\n titleField,\n subtitleField,\n statusOptions,\n hasActiveFilters,\n clearFilters,\n };\n}\n"]}
@@ -6,20 +6,21 @@ import { V as ViewMode } from '../../view-toggle-CahQ87PW.js';
6
6
  export { a as ViewToggle } from '../../view-toggle-CahQ87PW.js';
7
7
  import { LucideIcon } from 'lucide-react';
8
8
  import { Density } from '../../hooks/use-view-settings.js';
9
- import { R as ResourceActions, a as ResourceColumnDef, D as DataTable } from '../../use-resource-page-D4byuwAs.js';
10
- export { c as DataTableColumn, d as DataTableProps, L as LinkCell, T as ThumbnailCell } from '../../use-resource-page-D4byuwAs.js';
9
+ import { R as ResourceActions, a as ResourceColumnDef, D as DataTable } from '../../use-resource-page-BDnnCssE.js';
10
+ export { c as DataTableColumn, d as DataTableProps, e as DataTableSort, L as LinkCell, T as ThumbnailCell } from '../../use-resource-page-BDnnCssE.js';
11
11
  export { EmptyState } from '../patterns/empty-state.js';
12
12
  export { V as ViewSettings } from '../../view-settings-kHKCHMjj.js';
13
- import { L as LinkComponent, a as NavigateFn } from '../../navigation-Bha0R3Fu.js';
13
+ import { L as LinkComponent, a as NavigateFn, S as SearchParamsAdapter } from '../../navigation-Bha0R3Fu.js';
14
14
  export { D as DefaultLink, G as GenesisLinkProps, I as IsPathActiveOptions, N as NavigationNode, R as ResolvedNavigation, d as defaultNavigate, i as isPathActive, r as resolveNavigation } from '../../navigation-Bha0R3Fu.js';
15
15
  import { UseFormReturn, Control, FieldErrors } from 'react-hook-form';
16
+ import { WizardStep, MutationMode, UndoableWrite, UseResourceOptions } from '@marktiderman/genesis-core/hooks';
16
17
  import { ResourceFormFieldDef } from '@marktiderman/genesis-core';
17
18
  export { FieldType, ResourceFormFieldDef, SelectOption } from '@marktiderman/genesis-core';
18
- import { WizardStep, UseResourceOptions } from '@marktiderman/genesis-core/hooks';
19
19
  import { b as Slots, a as SlotPropsFor } from '../../slots-tR0wf89f.js';
20
20
  export { S as SlotOverride } from '../../slots-tR0wf89f.js';
21
21
  import { ViewModeDefault } from '../../hooks/use-view-preference.js';
22
22
  export { ResponsiveViewMode } from '../../hooks/use-view-preference.js';
23
+ import { ResourceUrlStateOptions } from '../../hooks/use-resource-url-state.js';
23
24
  import { FilterParam, SortParam, BaseRecord, Identifier } from '@marktiderman/genesis-core/provider';
24
25
  export { CommandPalette, CommandPaletteItem, CommandPaletteProps, useCommandPaletteHotkey } from '../ui/command-palette.js';
25
26
  export { UseSavedViewsReturn, useSavedViews } from '../../hooks/use-saved-views.js';
@@ -465,6 +466,34 @@ interface ResourceFormProps {
465
466
  * always shows. (Select fields keep their "Select…" prompt either way.)
466
467
  */
467
468
  autoPlaceholder?: boolean;
469
+ /**
470
+ * Warn before unsaved changes are lost. Default false. While the form is
471
+ * dirty, Cancel / Esc / clicking outside asks first, and closing or
472
+ * reloading the tab shows the browser's prompt. Pass `{ message }` to
473
+ * change the question. For router navigation, see `onDirtyChange`.
474
+ */
475
+ warnWhenUnsaved?: boolean | {
476
+ message?: string;
477
+ };
478
+ /**
479
+ * Called when the form becomes dirty or clean, with a `confirmLeave` that
480
+ * asks when needed: the hook for a router blocker (SKILL.md "Unsaved
481
+ * changes").
482
+ */
483
+ onDirtyChange?: (isDirty: boolean, confirmLeave: () => boolean) => void;
484
+ /**
485
+ * When a save reaches the list. Default `"pessimistic"`: the form waits
486
+ * for the provider. `"optimistic"` closes at once and updates the cached
487
+ * list, putting it back (and toasting the error) if the save fails.
488
+ * `"undoable"` does the same and shows an Undo toast; the save is sent
489
+ * after `undoDelayMs` unless undone (or at once, undone by the provider,
490
+ * when it serves `undo`). Needs a mounted `<Toaster />`.
491
+ */
492
+ mutationMode?: MutationMode;
493
+ /** How long an `undoable` save waits for an undo, in ms. Default 5000. */
494
+ undoDelayMs?: number;
495
+ /** Replaces the undo toast. */
496
+ onUndoable?: (write: UndoableWrite) => void;
468
497
  }
469
498
  declare function ResourceForm(props: ResourceFormProps): React.JSX.Element | null;
470
499
 
@@ -484,7 +513,15 @@ interface ConfirmDeleteDialogProps {
484
513
  onConfirm: () => void | Promise<void>;
485
514
  /** Default "Delete this record?". */
486
515
  title?: string;
487
- /** Default "This can't be undone." */
516
+ /**
517
+ * What happens to the record: `"permanent"` (default) or `"recycle"` when
518
+ * a delete moves it to a recycle bin. Sets the default description.
519
+ */
520
+ kind?: "permanent" | "recycle";
521
+ /**
522
+ * Default by `kind`: "This can't be undone.", or "It moves to the recycle
523
+ * bin, where it can be restored."
524
+ */
488
525
  description?: ReactNode;
489
526
  /** Default "Delete". */
490
527
  confirmLabel?: string;
@@ -499,8 +536,14 @@ interface ConfirmDeleteDialogProps {
499
536
  *
500
537
  * @stability Beta
501
538
  */
502
- declare function ConfirmDeleteDialog({ open, onOpenChange, onConfirm, title, description, confirmLabel, cancelLabel, }: ConfirmDeleteDialogProps): React.JSX.Element;
539
+ declare function ConfirmDeleteDialog({ open, onOpenChange, onConfirm, title, kind, description, confirmLabel, cancelLabel, }: ConfirmDeleteDialogProps): React.JSX.Element;
503
540
 
541
+ /**
542
+ * What a user may do with a list's records, for ResourcePage's `can`.
543
+ *
544
+ * @stability Beta
545
+ */
546
+ type ResourceAction = "create" | "edit" | "delete";
504
547
  /**
505
548
  * Row-bound actions handed to `renderCard`, on top of the resource-wide
506
549
  * `ResourceActions`. `open` is per-row, which is why it lives here rather
@@ -847,28 +890,28 @@ interface ResourcePageProps<T extends Record<string, unknown> = Record<string, u
847
890
  */
848
891
  onDelete?: (item: T) => void | Promise<void>;
849
892
  /**
850
- * Whether the signed-in user may change this record at all: shorthand for
851
- * `canEdit` and `canDelete` together. When it returns false the peek
852
- * offers neither Edit nor Delete.
853
- *
854
- * Precedence: `canEditItem` is checked first. A row it refuses gets no Edit
855
- * and no Delete, whatever `canEdit` / `canDelete` say. A row it allows is
856
- * then gated by `canEdit` (Edit) and `canDelete` (Delete) when given. Use
857
- * `canEditItem` alone for one owner rule, or the pair for separate rules.
893
+ * What the signed-in user may do: `can("create")`, `can("edit", item)`,
894
+ * `can("delete", item)`. Return false to hide the New button, or a row's
895
+ * Edit or Delete (and the built-in edit form won't open for it). Omitted,
896
+ * everything the handlers allow is offered. A UI gate only: the server
897
+ * must still refuse the write.
898
+ */
899
+ can?: (action: ResourceAction, item?: T) => boolean;
900
+ /**
901
+ * @deprecated Use `can` — `can={(action, item) => !item || mayChange(item)}`.
902
+ * Removed in the next minor after one deprecation window. Still honoured:
903
+ * a row it refuses gets no Edit and no Delete.
858
904
  */
859
905
  canEditItem?: (item: T) => boolean;
860
906
  createLabel?: string;
861
907
  /**
862
- * Whether this row may be edited. The peek's Edit button is hidden for a
863
- * row that returns `false` (and the built-in edit form will not open for
864
- * it). Omitted, every row is editable, as before. A UI gate only: the
865
- * server must still refuse the write.
908
+ * @deprecated Use `can` — `can={(action, item) => action !== "edit" || !item || mayEdit(item)}`.
909
+ * Still honoured: a row it refuses gets no Edit.
866
910
  */
867
911
  canEdit?: (item: T) => boolean;
868
912
  /**
869
- * Whether this row may be deleted. The peek's Delete button is hidden for
870
- * a row that returns `false`. Omitted, every row is deletable. A UI gate
871
- * only: the server must still refuse the write.
913
+ * @deprecated Use `can` — `can={(action, item) => action !== "delete" || !item || mayDelete(item)}`.
914
+ * Still honoured: a row it refuses gets no Delete.
872
915
  */
873
916
  canDelete?: (item: T) => boolean;
874
917
  /**
@@ -906,6 +949,13 @@ interface ResourcePageProps<T extends Record<string, unknown> = Record<string, u
906
949
  formSteps?: WizardStep[];
907
950
  /** Layout mode for the built-in form container. If not set, defaults to sheet for edit, dialog for create. */
908
951
  formLayout?: "dialog" | "sheet" | "page";
952
+ /**
953
+ * When the built-in form's saves and deletes reach the list: `"pessimistic"`
954
+ * (default, wait for the provider), `"optimistic"` or `"undoable"` (an Undo
955
+ * toast). See ResourceForm `mutationMode`. `resourceOptions.mutationMode`
956
+ * wins for deletes.
957
+ */
958
+ mutationMode?: MutationMode;
909
959
  bulkActions?: Array<{
910
960
  label: string;
911
961
  icon?: LucideIcon;
@@ -975,9 +1025,10 @@ interface ResourcePageProps<T extends Record<string, unknown> = Record<string, u
975
1025
  * opt out.
976
1026
  *
977
1027
  * Default-on, and scoped so that being on by default stays safe:
978
- * - Only while the grid or list view is showing. Table view keeps its own
979
- * independent sort/pagination, so a flat focus index can't reliably map
980
- * onto its visible rows.
1028
+ * - The j/k cursor runs while the grid or list view is showing. Table view
1029
+ * keeps its own sort/pagination, so there `DataTable` handles the keys on
1030
+ * its focusable rows instead (↑/↓ or j/k, Home/End, Enter, x); this prop
1031
+ * turns that off too.
981
1032
  * - Cursor keys suspend while a detail panel/modal or the built-in form
982
1033
  * is open, so they can't drive the list behind the overlay; ⌫ stays
983
1034
  * live there so it can close it.
@@ -1010,6 +1061,18 @@ interface ResourcePageProps<T extends Record<string, unknown> = Record<string, u
1010
1061
  */
1011
1062
  onNavigate?: NavigateFn;
1012
1063
  resourceOptions?: UseResourceOptions<T>;
1064
+ /**
1065
+ * Keep the list's state in the URL: the open record (`?id=`), the search
1066
+ * (`q`), the sort (`sort=field:asc`) and, with `pagination="server"`, the
1067
+ * page. Pass your router's search-params adapter
1068
+ * (`useReactRouterSearchParams()`, `useTanStackRouterSearchParams()`, or
1069
+ * `useBrowserSearchParams()` with no router), or `{ searchParams, params }`
1070
+ * to rename the keys. A copied link reopens the same record, and Back
1071
+ * closes one that was opened. Search and sort are read when the page
1072
+ * mounts and written as they change. A server-controlled page owns its
1073
+ * own page, sort and search: use `useResourceUrlState` there.
1074
+ */
1075
+ urlState?: SearchParamsAdapter | ResourceUrlStateOptions;
1013
1076
  /**
1014
1077
  * The view shown until the user picks one with the view toggle. Default
1015
1078
  * `"table"`. Pass `{ mobile: "grid", desktop: "table" }` for cards on
@@ -1183,4 +1246,4 @@ declare const AppShell: typeof AppShell$1;
1183
1246
  */
1184
1247
  declare const PageHeader: typeof PageHeader$1;
1185
1248
 
1186
- export { AppShell, BadgeCell, type BulkAction, type ColumnStatus, ConfirmDeleteDialog, type ConfirmDeleteDialogProps, CurrencyCell, DataBulkBar, type DataFilterConfig, DataFilters, DataGrid, DataList, type DataListProps, DataPageShell, type DataPageShellProps, DataTable, DateCell, Density, DetailPanel, type DetailPanelProps, FieldArrayRenderer, FileUploadField, FilterCombobox, type FilterOption, KanbanBoard, type KanbanBoardProps, type KanbanColumn, LinkComponent, NavigateFn, NumberCell, PageHeader, PageHeaderOptions, RelationField, ResourceActions, type ResourceCardActions, ResourceColumnDef, type ResourceDeleteConfirmCopy, ResourceDetailPage, type ResourceDetailPageProps, ResourceForm, type ResourceFormProps, ResourcePage, type ResourcePageProps, type ResourcePageSlotMap, type ResourcePageSlotProps, type ResourcePageSlots, type SavedViewItem, SlotPropsFor, Slots, StatCard, type StatCardColor, type StatCardProps, type StatusChipOption, type StatusChipsVariant, ViewMode, ViewModeDefault, WizardForm };
1249
+ export { AppShell, BadgeCell, type BulkAction, type ColumnStatus, ConfirmDeleteDialog, type ConfirmDeleteDialogProps, CurrencyCell, DataBulkBar, type DataFilterConfig, DataFilters, DataGrid, DataList, type DataListProps, DataPageShell, type DataPageShellProps, DataTable, DateCell, Density, DetailPanel, type DetailPanelProps, FieldArrayRenderer, FileUploadField, FilterCombobox, type FilterOption, KanbanBoard, type KanbanBoardProps, type KanbanColumn, LinkComponent, NavigateFn, NumberCell, PageHeader, PageHeaderOptions, RelationField, type ResourceAction, ResourceActions, type ResourceCardActions, ResourceColumnDef, type ResourceDeleteConfirmCopy, ResourceDetailPage, type ResourceDetailPageProps, ResourceForm, type ResourceFormProps, ResourcePage, type ResourcePageProps, type ResourcePageSlotMap, type ResourcePageSlotProps, type ResourcePageSlots, type SavedViewItem, SlotPropsFor, Slots, StatCard, type StatCardColor, type StatCardProps, type StatusChipOption, type StatusChipsVariant, ViewMode, ViewModeDefault, WizardForm };