@marktiderman/genesis-ui 2.7.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/EXTENDING.md +40 -1
- package/SKILL.md +198 -9
- package/dist/{AppShell-CX0M-dfc.d.ts → AppShell-LXQykEqe.d.ts} +47 -5
- package/dist/{chunk-M62CHO3N.js → chunk-4C4ESW6O.js} +9 -4
- package/dist/chunk-4C4ESW6O.js.map +1 -0
- package/dist/chunk-DIV4BYXX.js +58 -0
- package/dist/chunk-DIV4BYXX.js.map +1 -0
- package/dist/chunk-FUVWKTPL.js +19 -0
- package/dist/chunk-FUVWKTPL.js.map +1 -0
- package/dist/{chunk-PLIYF6KI.js → chunk-MPI33I4K.js} +2 -2
- package/dist/{chunk-PLIYF6KI.js.map → chunk-MPI33I4K.js.map} +1 -1
- package/dist/{chunk-JA3ZKULS.js → chunk-OITWCIG2.js} +3 -3
- package/dist/chunk-OITWCIG2.js.map +1 -0
- package/dist/chunk-PKFDGGZV.js +65 -0
- package/dist/chunk-PKFDGGZV.js.map +1 -0
- package/dist/{chunk-IXGAEECT.js → chunk-RKPS7ERS.js} +9 -5
- package/dist/chunk-RKPS7ERS.js.map +1 -0
- package/dist/{chunk-TXOZMKNW.js → chunk-S7NXD37O.js} +27 -26
- package/dist/chunk-S7NXD37O.js.map +1 -0
- package/dist/{chunk-ZPPLUGLA.js → chunk-UYI3GLFJ.js} +162 -19
- package/dist/chunk-UYI3GLFJ.js.map +1 -0
- package/dist/{chunk-CDYZRZ6U.js → chunk-WGMEVX3Y.js} +2 -2
- package/dist/chunk-WGMEVX3Y.js.map +1 -0
- package/dist/chunk-ZDHWNQTR.js +552 -0
- package/dist/chunk-ZDHWNQTR.js.map +1 -0
- package/dist/components/data/index.d.ts +306 -19
- package/dist/components/data/index.js +1177 -284
- package/dist/components/data/index.js.map +1 -1
- package/dist/components/layout/form-page.d.ts +1 -1
- package/dist/components/layout/form-page.js +1 -1
- package/dist/components/layout/index.d.ts +2 -2
- package/dist/components/layout/index.js +8 -8
- package/dist/components/patterns/index.d.ts +1 -1
- package/dist/components/patterns/index.js +5 -5
- package/dist/components/patterns/sortable-list.js +1 -1
- package/dist/components/rich-text/rich-text-editor.d.ts +194 -0
- package/dist/components/rich-text/rich-text-editor.js +833 -0
- package/dist/components/rich-text/rich-text-editor.js.map +1 -0
- package/dist/components/rich-text/rich-text.d.ts +51 -0
- package/dist/components/rich-text/rich-text.js +4 -0
- package/dist/components/rich-text/rich-text.js.map +1 -0
- package/dist/components/ui/alert-dialog.d.ts +12 -1
- package/dist/components/ui/alert-dialog.js +1 -1
- package/dist/hooks/index.d.ts +4 -3
- package/dist/hooks/index.js +5 -4
- package/dist/hooks/use-data-filters.d.ts +5 -3
- package/dist/hooks/use-data-filters.js +2 -2
- package/dist/hooks/use-resource-page.d.ts +2 -1
- package/dist/hooks/use-resource-page.js +3 -2
- 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/hooks/use-view-preference.d.ts +17 -2
- package/dist/hooks/use-view-preference.js +2 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +32 -32
- package/dist/{navigation-DBwcZ6vb.d.ts → navigation-Bha0R3Fu.d.ts} +6 -3
- package/dist/rich-text-doc-C54fME2C.d.ts +180 -0
- package/dist/router/react-router.d.ts +47 -0
- package/dist/router/react-router.js +53 -0
- package/dist/router/react-router.js.map +1 -0
- package/dist/router/tanstack-router.d.ts +47 -0
- package/dist/router/tanstack-router.js +105 -0
- package/dist/router/tanstack-router.js.map +1 -0
- package/dist/{use-resource-page-Dd2VUv30.d.ts → use-resource-page-BDnnCssE.d.ts} +83 -5
- package/package.json +61 -14
- package/dist/chunk-CDYZRZ6U.js.map +0 -1
- package/dist/chunk-FTTNGGLA.js +0 -20
- package/dist/chunk-FTTNGGLA.js.map +0 -1
- package/dist/chunk-IXGAEECT.js.map +0 -1
- package/dist/chunk-JA3ZKULS.js.map +0 -1
- package/dist/chunk-M62CHO3N.js.map +0 -1
- package/dist/chunk-TXOZMKNW.js.map +0 -1
- package/dist/chunk-ZPPLUGLA.js.map +0 -1
package/EXTENDING.md
CHANGED
|
@@ -48,7 +48,23 @@ already right, change the one part that was not.
|
|
|
48
48
|
Every interior part a pattern composes is named in its slot map
|
|
49
49
|
(`ResourcePageSlotMap`). That map is the contract: **a part added to the
|
|
50
50
|
render tree gets added to the map in the same change**, otherwise the next
|
|
51
|
-
surface that needs it forks the page instead.
|
|
51
|
+
surface that needs it forks the page instead. (The delete confirmation,
|
|
52
|
+
`deleteDialog`, joined the map with the built-in delete.)
|
|
53
|
+
|
|
54
|
+
Type your slots with the exported types rather than reaching into the props:
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
import type { ResourcePageSlots, ResourcePageSlotProps } from "@marktiderman/genesis-ui/data";
|
|
58
|
+
|
|
59
|
+
const slots: ResourcePageSlots<Practice> = { filters: null };
|
|
60
|
+
const slotProps: ResourcePageSlotProps<Practice> = { table: { stickyHeader: true } };
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Before writing a slot, check for the prop.** A slot that exists only to
|
|
64
|
+
compute a subtitle, gate Edit per row, confirm a delete, drop the title or
|
|
65
|
+
add an empty-state sentence is a slot genesis now owes you as a prop
|
|
66
|
+
(`getSubtitle`, `canEdit` / `canDelete`, `allowDelete` / `confirmDelete`,
|
|
67
|
+
`header="toolbar"`, `emptyDescription`) — and those exist.
|
|
52
68
|
|
|
53
69
|
### 3 · The primitives
|
|
54
70
|
|
|
@@ -102,3 +118,26 @@ from. Reach for rung 2 first.
|
|
|
102
118
|
5. **When a downstream app hand-builds something genesis nearly had, that is
|
|
103
119
|
the bug report.** `ListPage`, a hand-rolled bulk bar and a forked header
|
|
104
120
|
were each a missing prop away from not existing.
|
|
121
|
+
|
|
122
|
+
## Navigation: declare the tree, not the chrome
|
|
123
|
+
|
|
124
|
+
Rule 5 in practice. Apps kept re-deriving the same three things by hand: the
|
|
125
|
+
contextual nav for "where am I", the mobile tab bar for it, and a back row
|
|
126
|
+
placed inside page content. `AppShell` now derives all of them from one
|
|
127
|
+
`NavigationNode` tree, so the ladder for app navigation is:
|
|
128
|
+
|
|
129
|
+
1. **`navigation`** — pass the tree and `activePath`. The root's `items` are
|
|
130
|
+
the global sidebar; the current node's `items` become a contextual section
|
|
131
|
+
(and the mobile tab bar); its logical parent becomes the back link in the
|
|
132
|
+
content header. `$param` segments in paths and item `to`s are filled from
|
|
133
|
+
the URL, and a static segment beats a `$param` sibling.
|
|
134
|
+
2. **Override one part** — `navItems`, `mobileNavItems` or
|
|
135
|
+
`parentNavigation={{ href, label }}` win over what the tree derives;
|
|
136
|
+
`parentNavigation={null}` hides the back link.
|
|
137
|
+
3. **`resolveNavigation(tree, pathname)`** — the same resolver, for chrome
|
|
138
|
+
outside AppShell.
|
|
139
|
+
|
|
140
|
+
Do not copy `resolveNavigation` or `NavigationNode` into an app, and do not
|
|
141
|
+
put a back row in page content or call `history.back()`. genesis-lint
|
|
142
|
+
(`scripts/check-navigation-reimplementation.mjs`) fails a local declaration of
|
|
143
|
+
either.
|
package/SKILL.md
CHANGED
|
@@ -49,6 +49,68 @@ already right; that re-derivation is where forks come from. Reach for rung 2
|
|
|
49
49
|
first. When the app hand-builds something Genesis nearly had, that is a bug
|
|
50
50
|
report against Genesis (`packages/ui/EXTENDING.md`), not a reason to fork.
|
|
51
51
|
|
|
52
|
+
## ResourcePage knobs a real screen needs
|
|
53
|
+
|
|
54
|
+
Reach for these before a slot. Each one replaced a slot workaround in a real app.
|
|
55
|
+
|
|
56
|
+
| You need | Prop |
|
|
57
|
+
| --- | --- |
|
|
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
|
+
| Who may create, edit, delete | `can(action, item)` (UI gate only; the server still decides) |
|
|
60
|
+
| A confirmed delete | `allowDelete` (built in, `resource` mode), or `onDelete` + `confirmDelete`; copy via `confirmDelete={{ title: (item) => … }}`, toast via `onDeleted` |
|
|
61
|
+
| A computed peek subtitle | `getSubtitle(item)`; the peek stays open after an edit and shows the saved row |
|
|
62
|
+
| A list inside a tab (no page title) | `header="toolbar"` (also on `DataPageShell`) |
|
|
63
|
+
| Empty-state copy | `emptyMessage`, `emptyDescription`, `emptyIcon` |
|
|
64
|
+
| Cards on phones, table on desktop | `defaultView={{ mobile: "grid", desktop: "table" }}`; only an explicit toggle is stored |
|
|
65
|
+
| Typed slot maps | `ResourcePageSlots<T>`, `ResourcePageSlotProps<T>`, `ResourcePageSlotMap<T>` from `/data` |
|
|
66
|
+
|
|
67
|
+
`ResourceForm` ties labels to inputs (`getByLabelText` works), shows a failed save as an alert,
|
|
68
|
+
honours `maxLength` on text fields, adds no placeholder unless the field sets one (or
|
|
69
|
+
`autoPlaceholder`), and takes `prefillFromUrl={false}` when the page's query string is not form data.
|
|
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
|
+
|
|
52
114
|
## The layer contract (docs/FRAMEWORK.md)
|
|
53
115
|
|
|
54
116
|
1. Layers: tokens → primitives → patterns → layouts → data-bound; each depends downward only.
|
|
@@ -62,6 +124,7 @@ report against Genesis (`packages/ui/EXTENDING.md`), not a reason to fork.
|
|
|
62
124
|
| You need | Use | Import from |
|
|
63
125
|
| ------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
|
|
64
126
|
| App skeleton: nav, header, content | `AppShell` | `@marktiderman/genesis-ui/layout` |
|
|
127
|
+
| Sidebar, contextual nav, tab bar and back link from one tree | `AppShell navigation={tree}` (`NavigationNode`; `parentNavigation` to override) | `/layout` |
|
|
65
128
|
| Page title row with count, view toggle, options | `PageHeader` (+ `options.configurable` for the in-header options popover) | `/layout` |
|
|
66
129
|
| List page chrome: header, filters, loading/empty/error | `DataPageShell` (`stats`, `filters` slots) | `/data` |
|
|
67
130
|
| A whole CRUD list page over a resource | `ResourcePage` | `/data` |
|
|
@@ -76,7 +139,9 @@ report against Genesis (`packages/ui/EXTENDING.md`), not a reason to fork.
|
|
|
76
139
|
| Cmd-K palette | `CommandPalette`, `useCommandPaletteHotkey` | `/command-palette` |
|
|
77
140
|
| Arrow-key list navigation | `useKeyboardNavigation` | `/hooks/use-keyboard-navigation` |
|
|
78
141
|
| Reorderable list: drag, keyboard, row-menu moves | `SortableList` | `/sortable-list` |
|
|
142
|
+
| Formatted text (lessons, notes): editor and read-only view | `RichTextEditor`, `RichText` (+ `sanitizeRichTextDoc`, `richTextToHtml` on the server) | `/rich-text-editor`, `/rich-text` |
|
|
79
143
|
| Persisted page size / density | `useViewSettings` | `/hooks/use-view-settings` |
|
|
144
|
+
| Client-side links, navigate, URL filters on your router | `ReactRouterLink` / `TanStackRouterLink` and their `use…Navigate`, `use…SearchParams`, `use…ActivePath` hooks | `/router/react-router`, `/router/tanstack-router` |
|
|
80
145
|
|
|
81
146
|
## Import rule
|
|
82
147
|
|
|
@@ -97,13 +162,85 @@ wins over `@layer theme`. `tailwindFromBrand(brand).css` adds brand ramps
|
|
|
97
162
|
(`--color-neutral-500` and the like) on top. Components reference only semantic tokens
|
|
98
163
|
(`bg-primary`, `text-muted-foreground`), never raw colours.
|
|
99
164
|
|
|
165
|
+
## Data views without hand-rolling
|
|
166
|
+
|
|
167
|
+
Reach for `ResourcePage` (with `data` for rows you already have) before composing DataTable,
|
|
168
|
+
DataGrid and DetailPanel yourself. The parts apps used to hand-roll are props:
|
|
169
|
+
|
|
170
|
+
- **Column widths:** `width: 180` is px, through a `<colgroup>`; the other columns share the rest.
|
|
171
|
+
Long headers truncate and keep the full text as a tooltip.
|
|
172
|
+
- **Clickable cards:** with a custom `renderCard`, `openOnCardClick` makes the whole card one
|
|
173
|
+
keyboard-reachable button that opens the detail. On a bare `DataGrid`, use `onItemClick` and
|
|
174
|
+
`getItemLabel`. Never wrap a card in a `div role="button"`.
|
|
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.
|
|
178
|
+
- **Names and empty state:** `getTitle` / `getSubtitle` when a record's name spans fields;
|
|
179
|
+
`emptyIcon`, `emptyMessage`, `emptyDescription` and `emptyState` (its action) for the empty list.
|
|
180
|
+
- **A list inside tabs:** `header="toolbar"` (on `ResourcePage` or `DataPageShell`) drops the title
|
|
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`.
|
|
185
|
+
- **Non-destructive confirm:** `<AlertDialogAction variant="default">` for publish/send; red stays
|
|
186
|
+
the default for deletes.
|
|
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
|
+
|
|
100
196
|
## Full inventory
|
|
101
197
|
|
|
102
198
|
`docs/component-reference.md` lists every export with its props and stability tag. It is
|
|
103
199
|
generated (`pnpm gen:component-reference`); do not edit it by hand.
|
|
104
200
|
|
|
201
|
+
## Rich text
|
|
202
|
+
|
|
203
|
+
`RichTextEditor` (`/rich-text-editor`, Tiptap 3) needs the optional peers
|
|
204
|
+
`@tiptap/core @tiptap/pm @tiptap/react @tiptap/starter-kit @tiptap/extensions` (`^3.31`). `RichText` and the
|
|
205
|
+
helpers on `/rich-text` need nothing beyond React and run on a server.
|
|
206
|
+
|
|
207
|
+
- **Store JSON, derive HTML.** `onChange(doc, details)` gives the document JSON (canonical) and lazy
|
|
208
|
+
`details.html` / `details.text`. Persist `doc` (Postgres `jsonb`). If you also need HTML or text (email,
|
|
209
|
+
search, previews), derive them on the server; never trust HTML or JSON a client posts.
|
|
210
|
+
- **Sanitize on the server on every write, with limits:**
|
|
211
|
+
`const doc = sanitizeRichTextDoc(body.doc, { maxCharacters: 50_000, maxNodes: 5_000, onTooDeep: "throw" })`
|
|
212
|
+
keeps only the allow-listed nodes (paragraph, h2-h4, lists, blockquote, bold/italic/underline,
|
|
213
|
+
http(s)/mailto/tel/relative links, YouTube/Vimeo/Loom embeds by id). Over a limit, or nested more than 32
|
|
214
|
+
levels, it throws `RichTextLimitError` (`reason`: `"characters" | "nodes" | "depth"`), so answer 413/422
|
|
215
|
+
instead of storing it. Then `richTextToHtml(doc)` (`{ embedAs: "link" }` for email) and
|
|
216
|
+
`richTextToPlainText(doc)`. Both run without a DOM and escape every value.
|
|
217
|
+
- **Render with `<RichText value={doc} />`**, not `dangerouslySetInnerHTML`. It re-sanitizes and works in
|
|
218
|
+
server components. If you must inject stored HTML, inject only HTML you produced with `richTextToHtml`.
|
|
219
|
+
- The editor adds roughly 135 KB gzipped (ProseMirror + Tiptap), so load it only where people write:
|
|
220
|
+
`const RichTextEditor = lazy(() => import("@marktiderman/genesis-ui/rich-text-editor").then((m) => ({ default: m.RichTextEditor })))`.
|
|
221
|
+
Reading pages use `RichText`, which pulls in no editor code.
|
|
222
|
+
- Embeds are off by default: `<RichTextEditor embeds />`. A CSP needs `frame-src` for
|
|
223
|
+
`https://www.youtube-nocookie.com https://player.vimeo.com https://www.loom.com`. The editor injects no
|
|
224
|
+
`<style>` tag (Tiptap's base rules are classes), so `style-src` needs no `'unsafe-inline'` for it.
|
|
225
|
+
|
|
226
|
+
```tsx
|
|
227
|
+
<RichTextEditor aria-label="Lesson text" value={doc} onChange={setDoc} placeholder="Write the lesson…" embeds maxLength={20000} />
|
|
228
|
+
<RichText value={lesson.body} />
|
|
229
|
+
```
|
|
230
|
+
|
|
105
231
|
## Wiring into an app
|
|
106
232
|
|
|
233
|
+
**Data provider and query cache.** If the app already has a TanStack `QueryClientProvider`,
|
|
234
|
+
mount `DataProviderRoot` inside it (or pass `queryClient` to `GenesisProvider`). Plain
|
|
235
|
+
`GenesisProvider` creates its own `QueryClient`, a second cache that app invalidations never
|
|
236
|
+
reach. On Supabase with RLS, configure `createSupabaseProvider(client, { binTables, resources,
|
|
237
|
+
onWrite })` instead of writing a local adapter: zero-row updates/deletes and Postgres error codes
|
|
238
|
+
come back as typed `DataWriteError` outcomes (`forbidden`, `gone`, `invalid`,
|
|
239
|
+
`unauthenticated`), recycle-bin tables count "gone from view" as success, and per-resource
|
|
240
|
+
`prepare` / `create` / `update` / `toFormValues` / `invalidates` cover RPC writes, form shapes and
|
|
241
|
+
related query keys. Branch on `isDataWriteError(e) && e.outcome`, never on the message. Details:
|
|
242
|
+
`packages/core/README.md`.
|
|
243
|
+
|
|
107
244
|
**Tailwind class scanning.** Tailwind v4 skips git-ignored paths such as `node_modules`, so genesis-ui's classes
|
|
108
245
|
are missing unless the app points at them. Import the shipped source entry — never hardcode an
|
|
109
246
|
`@source` path into `node_modules`:
|
|
@@ -113,26 +250,78 @@ are missing unless the app points at them. Import the shipped source entry — n
|
|
|
113
250
|
@import "@marktiderman/genesis-ui/styles.css"; /* = @source over the package's dist/ */
|
|
114
251
|
```
|
|
115
252
|
|
|
253
|
+
**Router adapters.** genesis-ui never imports a router from its root or layer subpaths. Links default
|
|
254
|
+
to a plain `<a>` and navigation to a full page load until you inject an adapter. Take the shipped one
|
|
255
|
+
for your router; do not hand-write it. Each lives on its own subpath with its router as an optional
|
|
256
|
+
peer, so the other router is never installed or bundled.
|
|
257
|
+
|
|
258
|
+
```tsx
|
|
259
|
+
// React Router v7
|
|
260
|
+
import {
|
|
261
|
+
ReactRouterLink,
|
|
262
|
+
useReactRouterActivePath,
|
|
263
|
+
useReactRouterNavigate,
|
|
264
|
+
useReactRouterSearchParams,
|
|
265
|
+
} from "@marktiderman/genesis-ui/router/react-router";
|
|
266
|
+
// TanStack Router: the same four, named TanStackRouterLink / useTanStackRouter…,
|
|
267
|
+
// from "@marktiderman/genesis-ui/router/tanstack-router".
|
|
268
|
+
|
|
269
|
+
<AppShell linkComponent={ReactRouterLink} activePath={useReactRouterActivePath()} … />
|
|
270
|
+
<ResourcePage detail="route" onNavigate={useReactRouterNavigate()} … />
|
|
271
|
+
useDataFilters("orders", defs, { searchParams: useReactRouterSearchParams() });
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
The link takes Genesis's string `href` (`/admin?tab=users#top`) and keeps its query and hash; the
|
|
275
|
+
TanStack one parses the query with the router's own `parseSearch`, so `validateSearch` sees what a
|
|
276
|
+
typed `<Link search>` would give it. Genesis owns the active state: the adapters never add a
|
|
277
|
+
router's own `aria-current` or `active` class. Absolute URLs leave the app as plain anchors or a
|
|
278
|
+
document navigation. Next.js needs no adapter: pass `next/link` as `linkComponent`.
|
|
279
|
+
|
|
116
280
|
**Active nav items with `?query` / `#hash`.** Pass the full location as `activePath`
|
|
117
|
-
(`pathname + search + hash`). Plain items still match on the path; an item whose `to` carries a
|
|
281
|
+
(`pathname + search + hash`, which `use…ActivePath()` above returns). Plain items still match on the path; an item whose `to` carries a
|
|
118
282
|
query or hash (`/admin?tab=users`, `/settings#profile`) is active only when those match too.
|
|
119
283
|
Do not write a custom `isActive` for this. `isPathActive(to, activePath, { end: true })` is the
|
|
120
284
|
exact-path variant.
|
|
121
285
|
|
|
122
|
-
**Hierarchical
|
|
123
|
-
and
|
|
124
|
-
|
|
125
|
-
|
|
286
|
+
**Hierarchical navigation — the default.** Describe the logical hierarchy once as a
|
|
287
|
+
`NavigationNode<NavItem>` tree and hand it to `AppShell` with the current location. AppShell
|
|
288
|
+
derives the global sidebar (the root's `items`), a contextual section (the current node's
|
|
289
|
+
`items`, `$param`s filled from the URL), the mobile tab bar (contextual items, else global) and
|
|
290
|
+
a back-to-parent link in the content header. `navItems`, `mobileNavItems` and
|
|
291
|
+
`parentNavigation={{ href, label }}` override a derived part; `parentNavigation={null}` hides the
|
|
292
|
+
link. Never put a back row in page content, never `history.back()`, never copy the resolver.
|
|
293
|
+
|
|
294
|
+
```tsx
|
|
295
|
+
import { AppShell, type NavigationNode, type NavItem } from "@marktiderman/genesis-ui/layout";
|
|
296
|
+
|
|
297
|
+
const navigation: NavigationNode<NavItem> = {
|
|
298
|
+
id: "app", path: "/", label: "Home", items: globalItems, children: [
|
|
299
|
+
{ id: "projects", path: "/projects", label: "Projects", items: [], children: [
|
|
300
|
+
{ id: "project", path: "/projects/$projectId", label: "Project", items: [
|
|
301
|
+
{ to: "/projects/$projectId", icon: FileText, label: "Overview", end: true },
|
|
302
|
+
{ to: "/projects/$projectId/tasks", icon: CheckSquare, label: "Tasks" },
|
|
303
|
+
] },
|
|
304
|
+
] },
|
|
305
|
+
],
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
<AppShell appName="Acme" navigation={navigation} activePath={pathname + search + hash}
|
|
309
|
+
linkComponent={RouterLink}>{children}</AppShell>
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
For chrome outside AppShell, call the same resolver: `resolveNavigation(tree, pathname)` (from
|
|
313
|
+
`/layout`, `/data` or the root barrel) returns `{ current, parent, items, trail, params }` with
|
|
314
|
+
every path interpolated from the URL's params.
|
|
126
315
|
|
|
127
316
|
```ts
|
|
128
317
|
const nav = resolveNavigation(
|
|
129
318
|
{ id: "app", path: "/app", label: "Home", items: globalItems, children: [
|
|
130
|
-
{ id: "
|
|
131
|
-
{ id: "
|
|
319
|
+
{ id: "projects", path: "/dashboard", label: "Projects", items: [], children: [
|
|
320
|
+
{ id: "project", path: "/projects/$projectId", label: "Project", items: projectItems },
|
|
132
321
|
] },
|
|
133
322
|
] },
|
|
134
323
|
pathname,
|
|
135
324
|
);
|
|
136
|
-
// pathname "/
|
|
137
|
-
// nav.params {
|
|
325
|
+
// pathname "/projects/p1/tasks" → nav.current.id "project", nav.parent.path "/dashboard",
|
|
326
|
+
// nav.params { projectId: "p1" }
|
|
138
327
|
```
|
|
@@ -3,7 +3,7 @@ import { ReactNode } from 'react';
|
|
|
3
3
|
import { LucideIcon } from 'lucide-react';
|
|
4
4
|
import { V as ViewMode } from './view-toggle-CahQ87PW.js';
|
|
5
5
|
import { Density } from './hooks/use-view-settings.js';
|
|
6
|
-
import { L as LinkComponent } from './navigation-
|
|
6
|
+
import { N as NavigationNode, L as LinkComponent } from './navigation-Bha0R3Fu.js';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Declarative chrome for PageHeader — developer defaults + optional
|
|
@@ -12,6 +12,13 @@ import { L as LinkComponent } from './navigation-DBwcZ6vb.js';
|
|
|
12
12
|
* @stability Beta
|
|
13
13
|
*/
|
|
14
14
|
interface PageHeaderOptions {
|
|
15
|
+
/**
|
|
16
|
+
* The icon, title and subtitle block. Turn it off where the surrounding
|
|
17
|
+
* page already names the content (a list inside a tab), keeping only the
|
|
18
|
+
* actions row: count, view toggle, filters, create. Default true. A
|
|
19
|
+
* developer setting, not offered in the user's Header options.
|
|
20
|
+
*/
|
|
21
|
+
showTitle?: boolean;
|
|
15
22
|
showIcon?: boolean;
|
|
16
23
|
showSubtitle?: boolean;
|
|
17
24
|
showCount?: boolean;
|
|
@@ -119,6 +126,16 @@ interface NavItem {
|
|
|
119
126
|
*/
|
|
120
127
|
end?: boolean;
|
|
121
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* The "up" link AppShell renders above page content: a real link to the
|
|
131
|
+
* logical parent, never `history.back()`.
|
|
132
|
+
*
|
|
133
|
+
* @stability Beta
|
|
134
|
+
*/
|
|
135
|
+
interface ParentNavigation {
|
|
136
|
+
href: string;
|
|
137
|
+
label: string;
|
|
138
|
+
}
|
|
122
139
|
/**
|
|
123
140
|
* Declarative chrome for AppShell — developer defaults + optional
|
|
124
141
|
* in-sidebar user toggles (Notion/Linear “customize chrome” pattern).
|
|
@@ -171,8 +188,33 @@ interface AppShellProps {
|
|
|
171
188
|
appLogo?: string;
|
|
172
189
|
/** Custom logo node — preferred over `appLogo` letter tile */
|
|
173
190
|
logo?: ReactNode;
|
|
174
|
-
/**
|
|
175
|
-
|
|
191
|
+
/**
|
|
192
|
+
* Primary navigation items. Optional when `navigation` is set: the tree
|
|
193
|
+
* root's `items` are the global nav then.
|
|
194
|
+
*/
|
|
195
|
+
navItems?: NavItem[];
|
|
196
|
+
/**
|
|
197
|
+
* The app's logical navigation hierarchy. AppShell resolves it against
|
|
198
|
+
* `activePath` (see `resolveNavigation`) and derives:
|
|
199
|
+
* - the sidebar's global items from the root's `items` (unless `navItems`),
|
|
200
|
+
* - a contextual section from the current node's `items`, labelled with
|
|
201
|
+
* the node's `label` (`$param` segments in `to` filled from the URL),
|
|
202
|
+
* - the mobile tab bar: the contextual items when there are any, else the
|
|
203
|
+
* global top-level items (unless `mobileNavItems`),
|
|
204
|
+
* - a back link to the current node's logical parent (unless
|
|
205
|
+
* `parentNavigation`).
|
|
206
|
+
*
|
|
207
|
+
* @stability Beta
|
|
208
|
+
*/
|
|
209
|
+
navigation?: NavigationNode<NavItem>;
|
|
210
|
+
/**
|
|
211
|
+
* Back-to-parent link rendered in the content header, above `children`, on
|
|
212
|
+
* mobile and desktop. Derived from `navigation` when omitted; `null` hides
|
|
213
|
+
* it. Use this instead of placing a back row inside page content.
|
|
214
|
+
*
|
|
215
|
+
* @stability Beta
|
|
216
|
+
*/
|
|
217
|
+
parentNavigation?: ParentNavigation | null;
|
|
176
218
|
/** Secondary nav (e.g. “More”) — rendered below a divider when shown */
|
|
177
219
|
secondaryNavItems?: NavItem[];
|
|
178
220
|
/** Label above secondary nav (default “More”) */
|
|
@@ -236,6 +278,6 @@ interface AppShellProps {
|
|
|
236
278
|
*/
|
|
237
279
|
isActive?: (to: string, activePath?: string) => boolean;
|
|
238
280
|
}
|
|
239
|
-
declare function AppShell({ appName, appLogo, logo, navItems, secondaryNavItems, secondaryNavLabel, mobileNavItems, headerSlot, collapsedHeaderSlot, footer, userSlot, children, showThemeToggle: showThemeToggleProp, options: optionsProp, optionsStorageKey, collapsed: collapsedProp, onCollapsedChange, linkComponent, activePath, isActive, }: AppShellProps): React.JSX.Element;
|
|
281
|
+
declare function AppShell({ appName, appLogo, logo, navItems: navItemsProp, navigation, parentNavigation: parentNavigationProp, secondaryNavItems, secondaryNavLabel, mobileNavItems, headerSlot, collapsedHeaderSlot, footer, userSlot, children, showThemeToggle: showThemeToggleProp, options: optionsProp, optionsStorageKey, collapsed: collapsedProp, onCollapsedChange, linkComponent, activePath, isActive, }: AppShellProps): React.JSX.Element;
|
|
240
282
|
|
|
241
|
-
export { AppShell as A, type NavItem as N, PageHeader as P, type AppShellOptions as a, type AppShellProps as b, type PageHeaderOptions as c, type PageHeaderProps as d, type
|
|
283
|
+
export { AppShell as A, type NavItem as N, PageHeader as P, type AppShellOptions as a, type AppShellProps as b, type PageHeaderOptions as c, type PageHeaderProps as d, type ParentNavigation as e, type PageHeaderAction as f };
|
|
@@ -60,12 +60,17 @@ var AlertDialogDescription = React.forwardRef(({ className, ...props }, ref) =>
|
|
|
60
60
|
}
|
|
61
61
|
));
|
|
62
62
|
AlertDialogDescription.displayName = AlertDialog$1.Description.displayName;
|
|
63
|
-
var
|
|
63
|
+
var ALERT_ACTION_VARIANT = {
|
|
64
|
+
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
|
65
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/90"
|
|
66
|
+
};
|
|
67
|
+
var AlertDialogAction = React.forwardRef(({ className, variant = "destructive", ...props }, ref) => /* @__PURE__ */ jsx(
|
|
64
68
|
AlertDialog$1.Action,
|
|
65
69
|
{
|
|
66
70
|
ref,
|
|
67
71
|
className: cn(
|
|
68
|
-
"inline-flex h-10 items-center justify-center rounded-lg
|
|
72
|
+
"inline-flex h-10 items-center justify-center rounded-lg px-4 py-2 text-sm font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
|
73
|
+
ALERT_ACTION_VARIANT[variant],
|
|
69
74
|
className
|
|
70
75
|
),
|
|
71
76
|
...props
|
|
@@ -86,5 +91,5 @@ var AlertDialogCancel = React.forwardRef(({ className, ...props }, ref) => /* @_
|
|
|
86
91
|
AlertDialogCancel.displayName = AlertDialog$1.Cancel.displayName;
|
|
87
92
|
|
|
88
93
|
export { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger };
|
|
89
|
-
//# sourceMappingURL=chunk-
|
|
90
|
-
//# sourceMappingURL=chunk-
|
|
94
|
+
//# sourceMappingURL=chunk-4C4ESW6O.js.map
|
|
95
|
+
//# sourceMappingURL=chunk-4C4ESW6O.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/ui/alert-dialog.tsx"],"names":["AlertDialogPrimitive"],"mappings":"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","file":"chunk-4C4ESW6O.js","sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport { AlertDialog as AlertDialogPrimitive } from \"radix-ui\";\nimport { cn } from \"../../utils\";\n\nconst AlertDialog = AlertDialogPrimitive.Root;\nconst AlertDialogTrigger = AlertDialogPrimitive.Trigger;\nconst AlertDialogPortal = AlertDialogPrimitive.Portal;\n\nconst AlertDialogOverlay = React.forwardRef<\n React.ComponentRef<typeof AlertDialogPrimitive.Overlay>,\n React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n <AlertDialogPrimitive.Overlay\n className={cn(\n \"fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n className\n )}\n {...props}\n ref={ref}\n />\n));\nAlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;\n\nconst AlertDialogContent = React.forwardRef<\n React.ComponentRef<typeof AlertDialogPrimitive.Content>,\n React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>\n>(({ className, ...props }, ref) => (\n <AlertDialogPortal>\n <AlertDialogOverlay />\n <AlertDialogPrimitive.Content\n ref={ref}\n className={cn(\n \"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-border bg-card p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg\",\n className\n )}\n {...props}\n />\n </AlertDialogPortal>\n));\nAlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;\n\nconst AlertDialogHeader = ({\n className,\n ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n <div className={cn(\"flex flex-col space-y-2 text-center sm:text-left\", className)} {...props} />\n);\n\nconst AlertDialogFooter = ({\n className,\n ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n <div className={cn(\"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2\", className)} {...props} />\n);\n\nconst AlertDialogTitle = React.forwardRef<\n React.ComponentRef<typeof AlertDialogPrimitive.Title>,\n React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n <AlertDialogPrimitive.Title\n ref={ref}\n className={cn(\"text-lg font-semibold\", className)}\n {...props}\n />\n));\nAlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;\n\nconst AlertDialogDescription = React.forwardRef<\n React.ComponentRef<typeof AlertDialogPrimitive.Description>,\n React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n <AlertDialogPrimitive.Description\n ref={ref}\n className={cn(\"text-sm text-muted-foreground\", className)}\n {...props}\n />\n));\nAlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName;\n\nconst ALERT_ACTION_VARIANT = {\n destructive: \"bg-destructive text-destructive-foreground hover:bg-destructive/90\",\n default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n} as const;\n\n/**\n * The confirming button. `variant` defaults to `\"destructive\"` (red), the\n * right colour for deleting; pass `\"default\"` when the action is not\n * dangerous (publish, send, continue).\n */\nconst AlertDialogAction = React.forwardRef<\n React.ComponentRef<typeof AlertDialogPrimitive.Action>,\n React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action> & {\n variant?: keyof typeof ALERT_ACTION_VARIANT | undefined;\n }\n>(({ className, variant = \"destructive\", ...props }, ref) => (\n <AlertDialogPrimitive.Action\n ref={ref}\n className={cn(\n \"inline-flex h-10 items-center justify-center rounded-lg px-4 py-2 text-sm font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50\",\n ALERT_ACTION_VARIANT[variant],\n className\n )}\n {...props}\n />\n));\nAlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;\n\nconst AlertDialogCancel = React.forwardRef<\n React.ComponentRef<typeof AlertDialogPrimitive.Cancel>,\n React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>\n>(({ className, ...props }, ref) => (\n <AlertDialogPrimitive.Cancel\n ref={ref}\n className={cn(\n \"mt-2 inline-flex h-10 items-center justify-center rounded-lg border border-border bg-card px-4 py-2 text-sm font-medium transition-colors hover:bg-accent focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 sm:mt-0\",\n className\n )}\n {...props}\n />\n));\nAlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;\n\nexport {\n AlertDialog,\n AlertDialogPortal,\n AlertDialogOverlay,\n AlertDialogTrigger,\n AlertDialogContent,\n AlertDialogHeader,\n AlertDialogFooter,\n AlertDialogTitle,\n AlertDialogDescription,\n AlertDialogAction,\n AlertDialogCancel,\n};\n"]}
|
|
@@ -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"]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// src/router/utils.ts
|
|
2
|
+
function isExternalHref(href) {
|
|
3
|
+
return /^[a-z][a-z\d+.-]*:/i.test(href) || href.startsWith("//");
|
|
4
|
+
}
|
|
5
|
+
function splitNavigationHref(href) {
|
|
6
|
+
const hashAt = href.indexOf("#");
|
|
7
|
+
const hash = hashAt === -1 ? "" : href.slice(hashAt + 1);
|
|
8
|
+
const beforeHash = hashAt === -1 ? href : href.slice(0, hashAt);
|
|
9
|
+
const searchAt = beforeHash.indexOf("?");
|
|
10
|
+
return {
|
|
11
|
+
pathname: searchAt === -1 ? beforeHash : beforeHash.slice(0, searchAt),
|
|
12
|
+
search: searchAt === -1 ? "" : beforeHash.slice(searchAt + 1),
|
|
13
|
+
hash
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export { isExternalHref, splitNavigationHref };
|
|
18
|
+
//# sourceMappingURL=chunk-FUVWKTPL.js.map
|
|
19
|
+
//# sourceMappingURL=chunk-FUVWKTPL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/router/utils.ts"],"names":[],"mappings":";AAWO,SAAS,eAAe,IAAA,EAAuB;AACpD,EAAA,OAAO,sBAAsB,IAAA,CAAK,IAAI,CAAA,IAAK,IAAA,CAAK,WAAW,IAAI,CAAA;AACjE;AAQO,SAAS,oBAAoB,IAAA,EAIlC;AACA,EAAA,MAAM,MAAA,GAAS,IAAA,CAAK,OAAA,CAAQ,GAAG,CAAA;AAC/B,EAAA,MAAM,OAAO,MAAA,KAAW,EAAA,GAAK,KAAK,IAAA,CAAK,KAAA,CAAM,SAAS,CAAC,CAAA;AACvD,EAAA,MAAM,aAAa,MAAA,KAAW,EAAA,GAAK,OAAO,IAAA,CAAK,KAAA,CAAM,GAAG,MAAM,CAAA;AAC9D,EAAA,MAAM,QAAA,GAAW,UAAA,CAAW,OAAA,CAAQ,GAAG,CAAA;AACvC,EAAA,OAAO;AAAA,IACL,UAAU,QAAA,KAAa,EAAA,GAAK,aAAa,UAAA,CAAW,KAAA,CAAM,GAAG,QAAQ,CAAA;AAAA,IACrE,QAAQ,QAAA,KAAa,EAAA,GAAK,KAAK,UAAA,CAAW,KAAA,CAAM,WAAW,CAAC,CAAA;AAAA,IAC5D;AAAA,GACF;AACF","file":"chunk-FUVWKTPL.js","sourcesContent":["/**\n * Helpers shared by the router adapters. Router-free on purpose: nothing here\n * may import a router, so either adapter can use it without pulling in the\n * other's peer.\n */\n\n/**\n * True for a href that leaves the app: a scheme (`https:`, `mailto:`, `tel:`)\n * or a protocol-relative `//host/…`. Router adapters hand these to the browser\n * instead of resolving them as an in-app route.\n */\nexport function isExternalHref(href: string): boolean {\n return /^[a-z][a-z\\d+.-]*:/i.test(href) || href.startsWith(\"//\");\n}\n\n/**\n * Split an in-app href into its path, query string (no `?`) and hash (no `#`).\n * Unlike the active-path matcher's splitter this keeps the path exactly as\n * given (no trailing-slash normalisation): it is a navigation target, not a\n * comparison key.\n */\nexport function splitNavigationHref(href: string): {\n pathname: string;\n search: string;\n hash: string;\n} {\n const hashAt = href.indexOf(\"#\");\n const hash = hashAt === -1 ? \"\" : href.slice(hashAt + 1);\n const beforeHash = hashAt === -1 ? href : href.slice(0, hashAt);\n const searchAt = beforeHash.indexOf(\"?\");\n return {\n pathname: searchAt === -1 ? beforeHash : beforeHash.slice(0, searchAt),\n search: searchAt === -1 ? \"\" : beforeHash.slice(searchAt + 1),\n hash,\n };\n}\n"]}
|
|
@@ -48,5 +48,5 @@ var FormPage = React.forwardRef(
|
|
|
48
48
|
FormPage.displayName = "FormPage";
|
|
49
49
|
|
|
50
50
|
export { FormPage };
|
|
51
|
-
//# sourceMappingURL=chunk-
|
|
52
|
-
//# sourceMappingURL=chunk-
|
|
51
|
+
//# sourceMappingURL=chunk-MPI33I4K.js.map
|
|
52
|
+
//# sourceMappingURL=chunk-MPI33I4K.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/components/layout/form-page.tsx"],"names":[],"mappings":";;;;;AAqFO,IAAM,QAAA,GAAiB,KAAA,CAAA,UAAA;AAAA,EAC5B,CACE,EAAE,SAAA,EAAW,KAAA,EAAO,WAAA,EAAa,MAAA,EAAQ,OAAA,EAAS,MAAA,EAAQ,QAAA,EAAU,GAAG,KAAA,EAAM,EAC7E,GAAA,qBAEA,IAAA;AAAA,IAAC,MAAA;AAAA,IAAA;AAAA,MACC,GAAA;AAAA,MACA,WAAA,EAAU,WAAA;AAAA,MACV,aAAA,EAAa,MAAA;AAAA,MACb,SAAA,EAAW,EAAA,CAAG,qBAAA,EAAuB,SAAS,CAAA;AAAA,MAC7C,GAAG,KAAA;AAAA,MAEJ,QAAA,EAAA;AAAA,wBAAA,IAAA,CAAC,QAAA,EAAA,EAAO,WAAA,EAAU,kBAAA,EAAmB,SAAA,EAAU,6BAAA,EAC7C,QAAA,EAAA;AAAA,0BAAA,GAAA;AAAA,YAAC,IAAA;AAAA,YAAA;AAAA,cACC,WAAA,EAAU,iBAAA;AAAA,cACV,SAAA,EAAU,uDAAA;AAAA,cAET,QAAA,EAAA;AAAA;AAAA,WACH;AAAA,UACC,WAAA,mBACC,GAAA;AAAA,YAAC,GAAA;AAAA,YAAA;AAAA,cACC,WAAA,EAAU,uBAAA;AAAA,cACV,SAAA,EAAU,+BAAA;AAAA,cAET,QAAA,EAAA;AAAA;AAAA,WACH,GACE;AAAA,SAAA,EACN,CAAA;AAAA,QAEC,MAAA,mBACC,GAAA,CAAC,KAAA,EAAA,EAAM,OAAA,EAAQ,aAAA,EAAc,WAAA,EAAU,kBAAA,EACrC,QAAA,kBAAA,GAAA,CAAC,gBAAA,EAAA,EAAkB,QAAA,EAAA,MAAA,EAAO,CAAA,EAC5B,CAAA,GACE,IAAA;AAAA,4BAEH,KAAA,EAAA,EAAI,WAAA,EAAU,gBAAA,EAAiB,SAAA,EAAU,+BACvC,QAAA,EACH,CAAA;AAAA,QAEC,OAAA,mBACC,GAAA;AAAA,UAAC,KAAA;AAAA,UAAA;AAAA,YACC,WAAA,EAAU,mBAAA;AAAA,YACV,SAAA,EAAU,8GAAA;AAAA,YAET,QAAA,EAAA;AAAA;AAAA,SACH,GACE;AAAA;AAAA;AAAA;AAGV;AACA,QAAA,CAAS,WAAA,GAAc,UAAA","file":"chunk-
|
|
1
|
+
{"version":3,"sources":["../src/components/layout/form-page.tsx"],"names":[],"mappings":";;;;;AAqFO,IAAM,QAAA,GAAiB,KAAA,CAAA,UAAA;AAAA,EAC5B,CACE,EAAE,SAAA,EAAW,KAAA,EAAO,WAAA,EAAa,MAAA,EAAQ,OAAA,EAAS,MAAA,EAAQ,QAAA,EAAU,GAAG,KAAA,EAAM,EAC7E,GAAA,qBAEA,IAAA;AAAA,IAAC,MAAA;AAAA,IAAA;AAAA,MACC,GAAA;AAAA,MACA,WAAA,EAAU,WAAA;AAAA,MACV,aAAA,EAAa,MAAA;AAAA,MACb,SAAA,EAAW,EAAA,CAAG,qBAAA,EAAuB,SAAS,CAAA;AAAA,MAC7C,GAAG,KAAA;AAAA,MAEJ,QAAA,EAAA;AAAA,wBAAA,IAAA,CAAC,QAAA,EAAA,EAAO,WAAA,EAAU,kBAAA,EAAmB,SAAA,EAAU,6BAAA,EAC7C,QAAA,EAAA;AAAA,0BAAA,GAAA;AAAA,YAAC,IAAA;AAAA,YAAA;AAAA,cACC,WAAA,EAAU,iBAAA;AAAA,cACV,SAAA,EAAU,uDAAA;AAAA,cAET,QAAA,EAAA;AAAA;AAAA,WACH;AAAA,UACC,WAAA,mBACC,GAAA;AAAA,YAAC,GAAA;AAAA,YAAA;AAAA,cACC,WAAA,EAAU,uBAAA;AAAA,cACV,SAAA,EAAU,+BAAA;AAAA,cAET,QAAA,EAAA;AAAA;AAAA,WACH,GACE;AAAA,SAAA,EACN,CAAA;AAAA,QAEC,MAAA,mBACC,GAAA,CAAC,KAAA,EAAA,EAAM,OAAA,EAAQ,aAAA,EAAc,WAAA,EAAU,kBAAA,EACrC,QAAA,kBAAA,GAAA,CAAC,gBAAA,EAAA,EAAkB,QAAA,EAAA,MAAA,EAAO,CAAA,EAC5B,CAAA,GACE,IAAA;AAAA,4BAEH,KAAA,EAAA,EAAI,WAAA,EAAU,gBAAA,EAAiB,SAAA,EAAU,+BACvC,QAAA,EACH,CAAA;AAAA,QAEC,OAAA,mBACC,GAAA;AAAA,UAAC,KAAA;AAAA,UAAA;AAAA,YACC,WAAA,EAAU,mBAAA;AAAA,YACV,SAAA,EAAU,8GAAA;AAAA,YAET,QAAA,EAAA;AAAA;AAAA,SACH,GACE;AAAA;AAAA;AAAA;AAGV;AACA,QAAA,CAAS,WAAA,GAAc,UAAA","file":"chunk-MPI33I4K.js","sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils\";\nimport { Alert, AlertDescription } from \"../ui/alert\";\n\nexport interface FormPageProps\n // `FormHTMLAttributes` declares `title?: string` (the native tooltip\n // attribute); omit it before redeclaring the page title as a ReactNode.\n extends Omit<React.FormHTMLAttributes<HTMLFormElement>, \"title\"> {\n /** Page title. Rendered as the page's single `<h1>`. */\n title: React.ReactNode;\n /** Supporting copy under the title — what this form is for. */\n description?: React.ReactNode;\n /**\n * Validation summary. Rendered in a destructive `Alert` above the fields\n * when set, so the summary announces itself (`role=\"alert\"`) and sits\n * where a keyboard user lands after a failed submit. Pass a list, a\n * sentence, or your own markup — the copy is yours.\n */\n errors?: React.ReactNode;\n /**\n * Submit / cancel controls. Rendered in a bar that sticks to the bottom of\n * the scroll container, so the primary action stays reachable on a form\n * longer than the viewport.\n */\n actions?: React.ReactNode;\n /** Stable test/automation selector (emitted as `data-testid`). */\n testID?: string;\n}\n\n/**\n * FormPage — a titled form: header, validation summary, a body of sections,\n * and a sticky action bar.\n *\n * It renders a real `<form>` and forwards `onSubmit` (and every other form\n * attribute), so a plain `type=\"submit\"` button in `actions` submits it with\n * no `form=` wiring. That is what keeps it usable for a HAND-WRITTEN form —\n * `react-hook-form`, `useActionState`, or an uncontrolled `<form action>` —\n * rather than only for one of them.\n *\n * **Do not put a `ResourceForm` inside it.** `ResourceForm` renders its own\n * `<form>`, and nesting one form in another is invalid HTML: the browser\n * drops the inner element, SSR and hydration disagree about the tree, and a\n * submit fires both handlers. `ResourceForm` is the data-bound *alternative*\n * to this template — it already answers \"bind a form to a resource\", with a\n * `layout=\"page\"` mode of its own — not something this one wraps.\n *\n * ```tsx\n * <FormPage\n * title=\"New engagement\"\n * description=\"Both parties will be notified.\"\n * errors={submitFailed ? \"Fix the highlighted fields.\" : undefined}\n * onSubmit={handleSubmit(onValid)}\n * actions={\n * <>\n * <Button variant=\"ghost\" type=\"button\" testID=\"cancel\">Cancel</Button>\n * <Button type=\"submit\" testID=\"save\">Save</Button>\n * </>\n * }\n * >\n * <Section title=\"Basics\">\n * <FormField label=\"Name\">…</FormField>\n * </Section>\n * </FormPage>\n * ```\n *\n * Sections are just children — `Section` is the natural one, but any node\n * works. The body spaces them at `gap-8`, wider than the page's own `gap-6`,\n * so section boundaries read as boundaries rather than as more fields.\n *\n * The sticky bar needs a scrolling ancestor to stick to, which `AppShell`'s\n * `<main>` provides. It renders only when `actions` is set: an empty sticky\n * strip is a border floating over the content for no reason.\n *\n * This is a **layout** — it positions a form, it does not submit one. Binding\n * fields to a resource is `ResourceForm`'s job\n * (`@marktiderman/genesis-ui/data`), which is already the one home for that\n * question. The header row is markup, not a component, and deliberately not\n * `PageHeader` — see `./detail-page` for the measured reasons composing it\n * was rejected across all four templates (`title: string`, and a 6x jump in\n * the emitted per-component closure plus a new `radix-ui` runtime dependency\n * for chrome none of them render). Width is left to `Container` for the\n * one-home reason — wrap, don't duplicate.\n *\n * @stability Beta\n */\nexport const FormPage = React.forwardRef<HTMLFormElement, FormPageProps>(\n (\n { className, title, description, errors, actions, testID, children, ...props },\n ref\n ) => (\n <form\n ref={ref}\n data-slot=\"form-page\"\n data-testid={testID}\n className={cn(\"flex flex-col gap-6\", className)}\n {...props}\n >\n <header data-slot=\"form-page-header\" className=\"flex min-w-0 flex-col gap-1\">\n <h1\n data-slot=\"form-page-title\"\n className=\"truncate text-xl font-bold tracking-tight sm:text-2xl\"\n >\n {title}\n </h1>\n {description ? (\n <p\n data-slot=\"form-page-description\"\n className=\"text-sm text-muted-foreground\"\n >\n {description}\n </p>\n ) : null}\n </header>\n\n {errors ? (\n <Alert variant=\"destructive\" data-slot=\"form-page-errors\">\n <AlertDescription>{errors}</AlertDescription>\n </Alert>\n ) : null}\n\n <div data-slot=\"form-page-body\" className=\"flex min-w-0 flex-col gap-8\">\n {children}\n </div>\n\n {actions ? (\n <div\n data-slot=\"form-page-actions\"\n className=\"sticky bottom-0 z-10 flex flex-wrap items-center justify-end gap-2 border-t border-border bg-background py-3\"\n >\n {actions}\n </div>\n ) : null}\n </form>\n )\n);\nFormPage.displayName = \"FormPage\";\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { useBrowserSearchParams } from './chunk-
|
|
2
|
+
import { useBrowserSearchParams } from './chunk-WGMEVX3Y.js';
|
|
3
3
|
import { useRef, useCallback, useState, useEffect } from 'react';
|
|
4
4
|
import { useStorage } from '@marktiderman/genesis-core';
|
|
5
5
|
|
|
@@ -116,5 +116,5 @@ function useDataFilters(storageKey, defs, options) {
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
export { useDataFilters };
|
|
119
|
-
//# sourceMappingURL=chunk-
|
|
120
|
-
//# sourceMappingURL=chunk-
|
|
119
|
+
//# sourceMappingURL=chunk-OITWCIG2.js.map
|
|
120
|
+
//# sourceMappingURL=chunk-OITWCIG2.js.map
|