@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 +58 -4
- package/dist/chunk-DIV4BYXX.js +58 -0
- package/dist/chunk-DIV4BYXX.js.map +1 -0
- package/dist/{chunk-DWCAC2E7.js → chunk-RKPS7ERS.js} +5 -4
- package/dist/chunk-RKPS7ERS.js.map +1 -0
- package/dist/components/data/index.d.ts +88 -25
- package/dist/components/data/index.js +702 -224
- package/dist/components/data/index.js.map +1 -1
- package/dist/components/layout/index.js +7 -7
- package/dist/hooks/index.d.ts +2 -1
- package/dist/hooks/index.js +2 -1
- package/dist/hooks/use-resource-page.d.ts +1 -1
- package/dist/hooks/use-resource-page.js +1 -1
- package/dist/hooks/use-resource-url-state.d.ts +68 -0
- package/dist/hooks/use-resource-url-state.js +3 -0
- package/dist/hooks/use-resource-url-state.js.map +1 -0
- package/dist/index.js +19 -19
- package/dist/{use-resource-page-D4byuwAs.d.ts → use-resource-page-BDnnCssE.d.ts} +63 -2
- package/package.json +4 -8
- package/dist/chunk-DWCAC2E7.js.map +0 -1
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
|
-
|
|
|
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
|
-
- **
|
|
133
|
-
|
|
134
|
-
|
|
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-
|
|
297
|
-
//# sourceMappingURL=chunk-
|
|
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-
|
|
10
|
-
export { c as DataTableColumn, d as DataTableProps, L as LinkCell, T as ThumbnailCell } from '../../use-resource-page-
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
851
|
-
* `
|
|
852
|
-
*
|
|
853
|
-
*
|
|
854
|
-
*
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
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
|
-
*
|
|
863
|
-
*
|
|
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
|
-
*
|
|
870
|
-
* a row
|
|
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
|
-
* -
|
|
979
|
-
*
|
|
980
|
-
*
|
|
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 };
|