@marktiderman/genesis-ui 2.10.1 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/SKILL.md +59 -1
  2. package/dist/AppShell-DpQdcLE-.d.ts +347 -0
  3. package/dist/{chunk-C7PQ54JI.js → chunk-3TWCZYZY.js} +43 -13
  4. package/dist/chunk-3TWCZYZY.js.map +1 -0
  5. package/dist/{chunk-WGMEVX3Y.js → chunk-KC43CNMA.js} +2 -2
  6. package/dist/{chunk-WGMEVX3Y.js.map → chunk-KC43CNMA.js.map} +1 -1
  7. package/dist/{chunk-UYI3GLFJ.js → chunk-SZMXNC5Q.js} +722 -159
  8. package/dist/chunk-SZMXNC5Q.js.map +1 -0
  9. package/dist/{chunk-OITWCIG2.js → chunk-XGG5RUDE.js} +3 -3
  10. package/dist/{chunk-OITWCIG2.js.map → chunk-XGG5RUDE.js.map} +1 -1
  11. package/dist/components/data/index.d.ts +5 -5
  12. package/dist/components/data/index.js +32 -6
  13. package/dist/components/data/index.js.map +1 -1
  14. package/dist/components/layout/index.d.ts +2 -2
  15. package/dist/components/layout/index.js +3 -2
  16. package/dist/components/rich-text/rich-text-editor.js +1 -1
  17. package/dist/hooks/index.d.ts +2 -2
  18. package/dist/hooks/index.js +3 -3
  19. package/dist/hooks/use-data-filters.d.ts +1 -1
  20. package/dist/hooks/use-data-filters.js +2 -2
  21. package/dist/hooks/use-resource-url-state.d.ts +1 -1
  22. package/dist/index.d.ts +1 -1
  23. package/dist/index.js +2 -2
  24. package/dist/{navigation-Bha0R3Fu.d.ts → navigation-By1DrzaM.d.ts} +24 -0
  25. package/dist/router/react-router.d.ts +1 -1
  26. package/dist/router/tanstack-router.d.ts +1 -1
  27. package/dist/router/tanstack-router.js +1 -1
  28. package/dist/{use-resource-table-DCVf_lEG.d.ts → use-resource-table-DrTkD5O1.d.ts} +32 -3
  29. package/package.json +4 -3
  30. package/dist/AppShell-BVr4V0UK.d.ts +0 -174
  31. package/dist/chunk-C7PQ54JI.js.map +0 -1
  32. package/dist/chunk-UYI3GLFJ.js.map +0 -1
package/SKILL.md CHANGED
@@ -55,13 +55,14 @@ Reach for these before a slot. Each one replaced a slot workaround in a real app
55
55
 
56
56
  | You need | Prop |
57
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`) |
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`); `pagination="cursor"` loads `resourceOptions.defaultPerPage` rows with a "Load more" button (keyset where the provider serves it; toolbar search and sort act on loaded rows) |
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`) |
63
63
  | Empty-state copy | `emptyMessage`, `emptyDescription`, `emptyIcon` |
64
64
  | Cards on phones, table on desktop | `defaultView={{ mobile: "grid", desktop: "table" }}`; only an explicit toggle is stored |
65
+ | A list that updates when others change it | `live` (needs a provider whose `subscribe` serves the resource; Supabase: `resources[r].realtime` plus the table in the `supabase_realtime` publication, see core's README) |
65
66
  | Typed slot maps | `ResourcePageSlots<T>`, `ResourcePageSlotProps<T>`, `ResourcePageSlotMap<T>` from `/data` |
66
67
 
67
68
  `ResourceForm` ties labels to inputs (`getByLabelText` works), shows a failed save as an alert,
@@ -326,6 +327,63 @@ const navigation: NavigationNode<NavItem> = {
326
327
  linkComponent={RouterLink}>{children}</AppShell>
327
328
  ```
328
329
 
330
+ **Collapsible sidebar.** On desktop the sidebar collapses to an icon rail (56px;
331
+ `options.collapsedRailWidth: 64` for the wider one): the logo mark, one icon per item (name in a
332
+ tooltip on hover and focus), nested groups in a popover, and the expand button at the bottom.
333
+ The panel button beside the logo toggles it; `options.collapseShortcut: "["` adds an opt-in key
334
+ (skipped while typing, in a listbox/combobox/menu, or with a dialog open). Pass
335
+ `optionsStorageKey` (app-scoped, e.g. `"acme:app-shell"`) to remember the user's choice under
336
+ `${key}:collapsed`. A page asks to open collapsed with `defaultCollapsed: true` on its
337
+ `NavigationNode` (the nearest node in the trail that sets it wins) or `defaultCollapsed` on
338
+ `AppShell`; the user's own toggle always wins once they have used it, and "Reset to defaults" in
339
+ the sidebar options hands it back. `options.defaultCollapsed` is the app-wide default, and
340
+ `collapsed` +
341
+ `onCollapsedChange` make it controlled. The mobile drawer is unaffected.
342
+
343
+ ```tsx
344
+ { id: "builder", path: "/forms/$formId/edit", label: "Form builder", items: [], defaultCollapsed: true }
345
+ ```
346
+
347
+ **Sub-navigation (a page's children).** A node's `items` are the current page's child items: a
348
+ form's Questions / Logic and endings / Responses / Settings. A node's items are `NavItem`s,
349
+ so each needs an `icon`. The hook (`useAppShellSubNav`) and the exported `SubNavTabs` take
350
+ `SubNavItem`s instead: a `NavItem` whose `icon` is optional. Without one, the sidebar and mobile
351
+ navigation show the label's first letter, and `SubNavTabs` shows the label alone. Each has `label`, `to` (a route, or a `?search` / `#hash`; active state follows the URL,
352
+ most specific sibling wins) and an optional `badge` (a count or short status, shown as a chip). Gate by role the same way as other items: filter the list before handing
353
+ it over.
354
+ For live data (badge counts) the page calls `useAppShellSubNav(items, { mode? })`, which
355
+ replaces the node's items while it is mounted, and puts a host chip at the end of the tab row with
356
+ `<AppShellSubNavEnd>…</AppShellSubNavEnd>`. The page's resource header (breadcrumb, title,
357
+ status, actions) goes in `<AppShellPageHeader>…</AppShellPageHeader>`, which the shell renders
358
+ above the tab row (on phones too; at the top of the content when there is no row). Both are
359
+ portals; when a layout and its page both mount one, the page's (innermost) renders, and the same
360
+ rule picks between `useAppShellSubNav` callers. Shell sub-navigation is opt-in through
361
+ `options.subNav`:
362
+
363
+ - `"section"` (default): exactly the pre-sub-nav shell. The children sit in a labelled section
364
+ below the global sidebar items and replace the phone bar, with no back item.
365
+ - `"tabs"`: one row of navigation links across the top of the content, badges as chips, the
366
+ current tab underlined in the brand colour, `aria-current="page"`.
367
+ - `"sidebar"`: nested under the active global item ("Forms"); a flyout from it on the rail.
368
+ - `"off"`: nothing, desktop or phone; draw your own (`SubNavTabs` is exported for that).
369
+
370
+ With `"tabs"` or `"sidebar"`, a page with children swaps the phone bar to a back item to the
371
+ logical parent ("Forms") plus the children and their badges; leaving the page brings the main
372
+ bar back. A node (`subNav` on `NavigationNode`, nearest in the trail wins), the `subNav` prop and
373
+ the page's hook override the option, in that order of strength (hook strongest), so one subtree
374
+ can opt in. With it on, a page's children also win over an app-wide `mobileNavItems` on that page;
375
+ in `"section"` / `"off"`, or on pages without children, `mobileNavItems` applies as before.
376
+
377
+ ```tsx
378
+ useAppShellSubNav([
379
+ { to: `/forms/${id}/edit`, label: "Questions", icon: ListChecks, badge: questions.length, end: true },
380
+ { to: `/forms/${id}/edit/logic`, label: "Logic and endings", icon: GitBranch, badge: rules.length },
381
+ { to: `/forms/${id}/edit/responses`, label: "Responses", icon: Inbox, badge: responseCount },
382
+ { to: `/forms/${id}/edit/settings`, label: "Settings", icon: Settings },
383
+ ]);
384
+ <AppShellSubNavEnd><Badge variant="success">Ready to publish</Badge></AppShellSubNavEnd>
385
+ ```
386
+
329
387
  For chrome outside AppShell, call the same resolver: `resolveNavigation(tree, pathname)` (from
330
388
  `/layout`, `/data` or the root barrel) returns `{ current, parent, items, trail, params }` with
331
389
  every path interpolated from the URL's params.
@@ -0,0 +1,347 @@
1
+ import * as React from 'react';
2
+ import { ReactNode } from 'react';
3
+ import { LucideIcon } from 'lucide-react';
4
+ import { L as LinkComponent, N as NavigationNode } from './navigation-By1DrzaM.js';
5
+
6
+ /**
7
+ * How AppShell shows the current page's child items. Sub-navigation is
8
+ * opt-in: the default, `section`, is the shell as it always was.
9
+ * - `section` (default): a labelled section below the global sidebar
10
+ * items; on phones the children replace the bottom bar (no back item).
11
+ * - `tabs`: one row of tabs across the top of the page content.
12
+ * - `sidebar`: nested under the active item in the sidebar; a flyout from
13
+ * that item on the collapsed rail.
14
+ * - `off`: nothing, on desktop or phones; the app draws its own
15
+ * (`SubNavTabs` is exported for it).
16
+ *
17
+ * With `tabs` or `sidebar`, phones switch the bottom bar to a back item to
18
+ * the logical parent plus the children, with their badges.
19
+ *
20
+ * @stability Beta
21
+ */
22
+ type SubNavMode = "section" | "tabs" | "sidebar" | "off";
23
+ /**
24
+ * A sub-navigation item: a `NavItem` whose `icon` is optional (the tab row,
25
+ * the phone bar and the rail show the label's first letter without one).
26
+ * Every `NavItem` is a `SubNavItem`; `NavItem` itself keeps `icon` required.
27
+ *
28
+ * @stability Beta
29
+ */
30
+ interface SubNavItem extends Omit<NavItem, "icon" | "children"> {
31
+ icon?: LucideIcon;
32
+ children?: SubNavItem[];
33
+ }
34
+ /**
35
+ * Props for {@link SubNavTabs}.
36
+ *
37
+ * @stability Beta
38
+ */
39
+ interface SubNavTabsProps {
40
+ items: SubNavItem[];
41
+ /** Current location (`pathname + search + hash`); drives the active tab. */
42
+ activePath?: string;
43
+ linkComponent: LinkComponent;
44
+ isActive?: (to: string, activePath?: string) => boolean;
45
+ /** Right-aligned slot in the row, e.g. a "Ready to publish" chip. */
46
+ end?: ReactNode;
47
+ /** Accessible name of the `<nav>` (default "Page sections"). */
48
+ label?: string;
49
+ className?: string;
50
+ }
51
+ /**
52
+ * A row of navigation tabs: real links (deep links and middle-click work),
53
+ * `aria-current="page"` on the active one, badges as chips, the active tab
54
+ * underlined in the brand colour. AppShell renders it in `tabs` mode; an app
55
+ * using `subNav: "off"` can place it itself.
56
+ *
57
+ * @stability Beta
58
+ */
59
+ declare function SubNavTabs({ items, activePath, linkComponent: LinkComp, isActive, end, label, className, }: SubNavTabsProps): React.JSX.Element;
60
+ /**
61
+ * Options for {@link useAppShellSubNav}.
62
+ *
63
+ * @stability Beta
64
+ */
65
+ interface UseAppShellSubNavOptions {
66
+ /** This page's `subNav` mode; wins over the shell's and the node's. */
67
+ mode?: SubNavMode;
68
+ }
69
+ /**
70
+ * Give the shell this page's child items (sub-navigation) while the page is
71
+ * mounted. They replace the current `NavigationNode`'s `items`; pass
72
+ * `null` / `undefined` to fall back to them (a `mode` still applies). When
73
+ * several components call it (a layout and its page), the innermost wins,
74
+ * for items and for mode separately. Use it when children depend on
75
+ * page data, such as live badge counts. A fresh array each render is fine:
76
+ * the shell only updates when a `to`, label, badge, icon or flag changes.
77
+ *
78
+ * ```tsx
79
+ * useAppShellSubNav([
80
+ * { to: `/forms/${id}/edit`, label: "Questions", icon: ListChecks, badge: questions.length, end: true },
81
+ * { to: `/forms/${id}/edit/logic`, label: "Logic and endings", icon: GitBranch, badge: rules.length },
82
+ * { to: `/forms/${id}/edit/responses`, label: "Responses", icon: Inbox, badge: responses },
83
+ * { to: `/forms/${id}/edit/settings`, label: "Settings", icon: Settings },
84
+ * ]);
85
+ * ```
86
+ *
87
+ * Does nothing outside an `AppShell`.
88
+ *
89
+ * @stability Beta
90
+ */
91
+ declare function useAppShellSubNav(items: SubNavItem[] | null | undefined, options?: UseAppShellSubNavOptions): void;
92
+ /**
93
+ * Render `children` in the right-hand slot of the shell's sub-navigation
94
+ * tab row, e.g. a "Ready to publish" status chip. Rendered through a
95
+ * portal, so it keeps the page's context and state. Renders nothing when
96
+ * the shell shows no tab row (other modes, phones, no children). When
97
+ * several are mounted (a layout's and a page's), the innermost renders.
98
+ *
99
+ * @stability Beta
100
+ */
101
+ declare function AppShellSubNavEnd({ children }: {
102
+ children: ReactNode;
103
+ }): ReactNode;
104
+ /**
105
+ * Render the page's resource header (breadcrumb, title, status, actions) at
106
+ * the top of the shell's content, ABOVE the sub-navigation tab row, where
107
+ * the design puts it. A portal like `AppShellSubNavEnd`, so it keeps the
108
+ * page's state; the innermost mounted one renders. On phones it stays at
109
+ * the top of the content while the bottom bar carries the children. With
110
+ * no tab row (the default `"section"` mode, or a page without children) it
111
+ * simply sits at the top of the content. Mount none and the layout is
112
+ * unchanged.
113
+ *
114
+ * @stability Beta
115
+ */
116
+ declare function AppShellPageHeader({ children }: {
117
+ children: ReactNode;
118
+ }): ReactNode;
119
+
120
+ interface NavItem {
121
+ /** Route, optionally with `?search` / `#hash` (see `isPathActive`). */
122
+ to: string;
123
+ icon: LucideIcon;
124
+ label: string;
125
+ /** Nested items — rendered as a collapsible group when the rail is expanded */
126
+ children?: NavItem[];
127
+ /**
128
+ * A count (`3`) or a short status (`"New"`), shown as a chip: in the
129
+ * sidebar, the sub-navigation tabs and the mobile bar; as a dot on the
130
+ * collapsed rail. It is part of the item's accessible name where the label
131
+ * is hidden.
132
+ */
133
+ badge?: string | number;
134
+ disabled?: boolean;
135
+ /**
136
+ * Exact-match active state (e.g. overview `/`). Exact on the path only, so
137
+ * an `activePath` carrying `?search#hash` still matches.
138
+ */
139
+ end?: boolean;
140
+ }
141
+ /**
142
+ * The "up" link AppShell renders above page content: a real link to the
143
+ * logical parent, never `history.back()`.
144
+ *
145
+ * @stability Beta
146
+ */
147
+ interface ParentNavigation {
148
+ href: string;
149
+ label: string;
150
+ }
151
+ /**
152
+ * Declarative chrome for AppShell — developer defaults + optional
153
+ * in-sidebar user toggles (Notion/Linear “customize chrome” pattern).
154
+ *
155
+ * @stability Beta
156
+ */
157
+ interface AppShellOptions {
158
+ showLogo?: boolean;
159
+ showAppName?: boolean;
160
+ showThemeToggle?: boolean;
161
+ /** Desktop rail collapse control */
162
+ showCollapse?: boolean;
163
+ showUserSlot?: boolean;
164
+ showFooter?: boolean;
165
+ showSecondaryNav?: boolean;
166
+ /** Mobile fixed bottom tab bar */
167
+ showMobileBottomNav?: boolean;
168
+ /**
169
+ * Open the desktop sidebar collapsed (an icon rail, `collapsedRailWidth` wide) when the user hasn't
170
+ * chosen. The app-wide default; a page asks for its own through
171
+ * `AppShellProps.defaultCollapsed` or `NavigationNode.defaultCollapsed`.
172
+ */
173
+ defaultCollapsed?: boolean;
174
+ /**
175
+ * Opt-in key that toggles the desktop sidebar, e.g. `"["` as in Linear and
176
+ * Notion. Off (`false`) by default. It never fires while typing in a field,
177
+ * while a listbox, combobox or menu has focus (Select typeahead), while a
178
+ * dialog is open, with Ctrl/Cmd/Alt held, or when `showCollapse` is off.
179
+ */
180
+ collapseShortcut?: string | false;
181
+ /**
182
+ * Width of the collapsed icon rail in px: `56` (default, as before) or
183
+ * `64` (the Breakthrough design).
184
+ */
185
+ collapsedRailWidth?: 56 | 64;
186
+ /**
187
+ * Shell sub-navigation, opt-in. `"section"` (default) renders a page's
188
+ * children exactly as AppShell always has; `"tabs"` or `"sidebar"` turn
189
+ * sub-navigation on (see `SubNavMode`), and `"off"` hides them. A
190
+ * navigation node, the `subNav` prop or the page
191
+ * (`useAppShellSubNav(items, { mode })`) can override it.
192
+ */
193
+ subNav?: SubNavMode;
194
+ /**
195
+ * Show the in-sidebar Sidebar options popover so users can customize chrome.
196
+ * Default true when collapse / secondary / theme / slots are available.
197
+ */
198
+ configurable?: boolean;
199
+ /**
200
+ * Toggle keys the developer has decided on and users may not override —
201
+ * e.g. hiding `showThemeToggle` for a brand that manages theme elsewhere.
202
+ * Locked keys are dropped from the options popover and any stored user
203
+ * override for them is ignored on resolve.
204
+ */
205
+ lockedOptions?: (keyof AppShellOptions)[];
206
+ /** Comfortable (default) vs compact row padding */
207
+ density?: "comfortable" | "compact";
208
+ /** Expanded rail width in rem-ish tokens: narrow ≈ 13rem, wide ≈ 16rem */
209
+ railWidth?: "narrow" | "wide";
210
+ /**
211
+ * Where page content scrolls.
212
+ * - `container` (default): main panel scrolls — classic app-shell chrome.
213
+ * - `window`: document/window scrolls with a sticky rail so React Router
214
+ * `ScrollRestoration` keeps working (dashboard dogfood).
215
+ */
216
+ scrollRoot?: "container" | "window";
217
+ }
218
+ /**
219
+ * @stability Beta
220
+ */
221
+ interface AppShellProps {
222
+ /** App name shown in sidebar header */
223
+ appName: string;
224
+ /** App logo letter or short text (ignored when `logo` is set) */
225
+ appLogo?: string;
226
+ /** Custom logo node — preferred over `appLogo` letter tile */
227
+ logo?: ReactNode;
228
+ /**
229
+ * Primary navigation items. Optional when `navigation` is set: the tree
230
+ * root's `items` are the global nav then.
231
+ */
232
+ navItems?: NavItem[];
233
+ /**
234
+ * The app's logical navigation hierarchy. AppShell resolves it against
235
+ * `activePath` (see `resolveNavigation`) and derives:
236
+ * - the sidebar's global items from the root's `items` (unless `navItems`),
237
+ * - a contextual section from the current node's `items`, labelled with
238
+ * the node's `label` (`$param` segments in `to` filled from the URL),
239
+ * - the mobile tab bar: the contextual items when there are any, else the
240
+ * global top-level items (unless `mobileNavItems`),
241
+ * - a back link to the current node's logical parent (unless
242
+ * `parentNavigation`).
243
+ *
244
+ * @stability Beta
245
+ */
246
+ navigation?: NavigationNode<NavItem>;
247
+ /**
248
+ * Back-to-parent link rendered in the content header, above `children`, on
249
+ * mobile and desktop. Derived from `navigation` when omitted; `null` hides
250
+ * it. Use this instead of placing a back row inside page content.
251
+ *
252
+ * @stability Beta
253
+ */
254
+ parentNavigation?: ParentNavigation | null;
255
+ /** Secondary nav (e.g. “More”) — rendered below a divider when shown */
256
+ secondaryNavItems?: NavItem[];
257
+ /** Label above secondary nav (default “More”) */
258
+ secondaryNavLabel?: string;
259
+ /**
260
+ * Items for the mobile bottom tab bar. Defaults to top-level `navItems`
261
+ * (no children). Pass a short list for apps with large sidebars; pass `[]`
262
+ * and set `options.showMobileBottomNav: false` to hide.
263
+ */
264
+ mobileNavItems?: NavItem[];
265
+ /** Slot below the logo row (product switcher, workspace picker, …) */
266
+ headerSlot?: ReactNode;
267
+ /**
268
+ * Compact slot shown in the **collapsed** desktop rail, where `headerSlot`
269
+ * is hidden for space. Use it to keep a core affordance reachable without
270
+ * expanding — e.g. an icon-only product switcher. Omit to hide the header
271
+ * region entirely while collapsed.
272
+ */
273
+ collapsedHeaderSlot?: ReactNode;
274
+ /** Footer region above the user slot (version, links) */
275
+ footer?: ReactNode;
276
+ /**
277
+ * User / account block at the bottom of the sidebar. When the rail is
278
+ * collapsed, AppShell hides any descendant carrying the
279
+ * `data-shell-user-detail` attribute (e.g. name/email text) so only a
280
+ * compact element (avatar, initial) remains — tag the detail wrapper you
281
+ * want hidden in the collapsed rail with that attribute.
282
+ */
283
+ userSlot?: ReactNode;
284
+ /** Page content */
285
+ children: ReactNode;
286
+ /**
287
+ * @deprecated Prefer `options.showThemeToggle`. Kept for sample-app
288
+ * back-compat; merges into resolved options when `options` omits it.
289
+ */
290
+ showThemeToggle?: boolean;
291
+ /** Chrome config (developer defaults + optional in-sidebar menu) */
292
+ options?: AppShellOptions;
293
+ /** Persist user sidebar-option overrides + collapse state (localStorage) */
294
+ optionsStorageKey?: string;
295
+ /**
296
+ * Controlled collapse (desktop rail). Overrides the stored user choice and
297
+ * every default.
298
+ */
299
+ collapsed?: boolean;
300
+ /** Called with the new state whenever the user toggles the sidebar. */
301
+ onCollapsedChange?: (collapsed: boolean) => void;
302
+ /**
303
+ * This page's default: open the desktop sidebar collapsed (`true`) or
304
+ * expanded (`false`) until the user toggles it. Reactive, so a layout can
305
+ * pass it per route. Wins over `navigation`'s `defaultCollapsed` and
306
+ * `options.defaultCollapsed`; the user's own choice (persisted under
307
+ * `optionsStorageKey`) wins over all of them.
308
+ *
309
+ * @stability Beta
310
+ */
311
+ defaultCollapsed?: boolean;
312
+ /**
313
+ * This page's sub-navigation mode (see `AppShellOptions.subNav`). Wins
314
+ * over `navigation`'s `subNav` and `options.subNav`; the page's own
315
+ * `useAppShellSubNav(items, { mode })` wins over it.
316
+ *
317
+ * @stability Beta
318
+ */
319
+ subNav?: SubNavMode;
320
+ /**
321
+ * Accessible name of the sub-navigation tab row. Defaults to the current
322
+ * navigation node's label, else "Page sections".
323
+ */
324
+ subNavLabel?: string;
325
+ /**
326
+ * Injectable link component. Defaults to a plain `<a>` (no router). Pass a
327
+ * router adapter — e.g. React Router `({ href, ...p }) => <Link to={href} {...p} />`
328
+ * or Next.js `next/link` — to get client-side navigation.
329
+ */
330
+ linkComponent?: LinkComponent;
331
+ /**
332
+ * Current location used to highlight the active nav item. Pass the full
333
+ * location, `pathname + search + hash` (React Router: `useLocation()`), so
334
+ * items whose `to` names a query or hash (`/admin?tab=users`) can become
335
+ * active. A pathname alone still works for path-only items, `end` included.
336
+ * Omit to disable active highlighting.
337
+ */
338
+ activePath?: string;
339
+ /**
340
+ * Optional custom active-state matcher. Defaults to a prefix match (with `/`
341
+ * treated as exact), mirroring React Router `NavLink`.
342
+ */
343
+ isActive?: (to: string, activePath?: string) => boolean;
344
+ }
345
+ 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, defaultCollapsed: defaultCollapsedProp, subNav: subNavProp, subNavLabel, linkComponent, activePath, isActive, }: AppShellProps): React.JSX.Element;
346
+
347
+ export { AppShell as A, type NavItem as N, type ParentNavigation as P, type SubNavItem as S, type UseAppShellSubNavOptions as U, type AppShellOptions as a, AppShellPageHeader as b, type AppShellProps as c, AppShellSubNavEnd as d, type SubNavMode as e, SubNavTabs as f, type SubNavTabsProps as g, useAppShellSubNav as u };
@@ -3,10 +3,10 @@ import { useResourcePage } from './chunk-RKPS7ERS.js';
3
3
  import { useResourceUrlState } from './chunk-DIV4BYXX.js';
4
4
  import { useViewSettings } from './chunk-OZ7ZAYCS.js';
5
5
  import { toast } from './chunk-4RAZJ6LL.js';
6
- import { defaultNavigate } from './chunk-WGMEVX3Y.js';
6
+ import { defaultNavigate } from './chunk-KC43CNMA.js';
7
7
  import { useKeyboardNavigation } from './chunk-YJ6MPS6U.js';
8
8
  import { useState, useRef, useEffect, useMemo, useCallback } from 'react';
9
- import { useResource, useOne, useCapabilities } from '@marktiderman/genesis-core/hooks';
9
+ import { useResource, useInfiniteResource, useOne, useCapabilities } from '@marktiderman/genesis-core/hooks';
10
10
 
11
11
  // src/components/data/undo-toast.ts
12
12
  var DONE = {
@@ -122,6 +122,7 @@ function useResourceTableModel(props) {
122
122
  emptyDescription,
123
123
  header: headerMode = "page",
124
124
  resourceOptions,
125
+ live,
125
126
  urlState: urlStateProp,
126
127
  defaultView = "table",
127
128
  onNavigate = defaultNavigate,
@@ -132,6 +133,7 @@ function useResourceTableModel(props) {
132
133
  } = props;
133
134
  const isClientMode = !!resourceName;
134
135
  const resourceServerPaging = isClientMode && pagination === "server";
136
+ const resourceCursorPaging = isClientMode && pagination === "cursor";
135
137
  const url = useResourceUrlState(
136
138
  urlStateProp ? "setSearchParams" in urlStateProp ? { searchParams: urlStateProp } : urlStateProp : void 0
137
139
  );
@@ -167,14 +169,26 @@ function useResourceTableModel(props) {
167
169
  ...urlSeed.page ? { defaultPage: urlSeed.page } : {},
168
170
  ...urlSeed.sort ? { defaultSort: urlSeed.sort } : {}
169
171
  } : {},
170
- ...writeModeOptions(resourceOptions, mutationModeProp)
172
+ ...writeModeOptions(resourceOptions, mutationModeProp),
173
+ // Cursor paging reads through useInfiniteResource below; this hook
174
+ // only writes.
175
+ ...resourceCursorPaging ? { enabled: false } : {},
176
+ live: resourceOptions?.live ?? live
171
177
  } : { enabled: false }
172
178
  );
179
+ const infinite = useInfiniteResource(resourceName ?? "__noop__", {
180
+ enabled: resourceCursorPaging && (resourceOptions?.enabled ?? true),
181
+ perPage: resourceOptions?.defaultPerPage ?? perPageProp ?? settings.pageSize,
182
+ defaultSort: resourceOptions?.defaultSort,
183
+ defaultFilters: resourceOptions?.defaultFilters,
184
+ select: resourceOptions?.select,
185
+ live: resourceOptions?.live ?? live
186
+ });
173
187
  const effectivePageSize = resourceServerPaging ? resourceHook.params.pagination?.perPage ?? settings.pageSize : perPageProp ?? settings.pageSize;
174
- const displayData = isClientMode ? resourceHook.list.data?.data ?? [] : dataProp ?? [];
175
- const displayTotal = isClientMode ? resourceHook.list.data?.total ?? 0 : totalProp ?? 0;
176
- const isLoading = isClientMode ? resourceHook.list.isLoading : loadingProp ?? false;
177
- const hasError = isClientMode ? resourceHook.list.isError : errorProp ?? false;
188
+ const displayData = resourceCursorPaging ? infinite.rows : isClientMode ? resourceHook.list.data?.data ?? [] : dataProp ?? [];
189
+ const displayTotal = resourceCursorPaging ? infinite.total ?? infinite.rows.length : isClientMode ? resourceHook.list.data?.total ?? 0 : totalProp ?? 0;
190
+ const isLoading = resourceCursorPaging ? infinite.query.isLoading : isClientMode ? resourceHook.list.isLoading : loadingProp ?? false;
191
+ const hasError = resourceCursorPaging ? infinite.query.isError : isClientMode ? resourceHook.list.isError : errorProp ?? false;
178
192
  const page = useResourcePage({
179
193
  resource: resourceName,
180
194
  data: displayData,
@@ -247,7 +261,7 @@ function useResourceTableModel(props) {
247
261
  defaultFilters,
248
262
  setResourceFilters
249
263
  ]);
250
- const clientTruncated = isClientMode && !resourceServerPaging && !isLoading && !hasError && displayTotal > displayData.length;
264
+ const clientTruncated = isClientMode && !resourceServerPaging && !resourceCursorPaging && !isLoading && !hasError && displayTotal > displayData.length;
251
265
  const warnedTruncatedRef = useRef(false);
252
266
  useEffect(() => {
253
267
  if (!clientTruncated || warnedTruncatedRef.current) return;
@@ -256,12 +270,14 @@ function useResourceTableModel(props) {
256
270
  `ResourcePage: "${resourceName}" has ${displayTotal} rows but only ${displayData.length} were fetched. Pass \`pagination="server"\` to page through all of them, or raise \`clientRowLimit\` (no higher than the provider's \`maxPerPage\`, which refuses a larger page).`
257
271
  );
258
272
  }, [clientTruncated, resourceName, displayTotal, displayData.length]);
273
+ const refetchList = resourceHook.list.refetch;
274
+ const refetchInfinite = infinite.query.refetch;
259
275
  const actions = useMemo(() => {
260
276
  if (isClientMode) {
261
277
  return {
262
278
  update: resourceHook.update,
263
279
  remove: resourceHook.remove,
264
- refetch: () => resourceHook.list.refetch()
280
+ refetch: () => resourceCursorPaging ? refetchInfinite() : refetchList()
265
281
  };
266
282
  }
267
283
  return {
@@ -282,7 +298,9 @@ function useResourceTableModel(props) {
282
298
  isClientMode,
283
299
  resourceHook.update,
284
300
  resourceHook.remove,
285
- resourceHook.list
301
+ refetchList,
302
+ resourceCursorPaging,
303
+ refetchInfinite
286
304
  ]);
287
305
  const keyOf = useCallback(
288
306
  (item) => getKeyProp ? getKeyProp(item) : String(item.id ?? ""),
@@ -629,10 +647,20 @@ function useResourceTableModel(props) {
629
647
  // and modal modes, so it is the precise precondition here.
630
648
  onBack: detailOpen ? closeDetail : void 0
631
649
  });
650
+ const infinitePages = infinite.query.data?.pages;
651
+ const loadMore = resourceCursorPaging ? {
652
+ hasMore: infinite.hasNextPage,
653
+ loading: infinite.isFetchingNextPage,
654
+ load: infinite.fetchNextPage,
655
+ loaded: infinite.rows.length,
656
+ // Rows the latest "Load more" added; 0 before the first one.
657
+ lastLoaded: infinitePages && infinitePages.length > 1 ? infinitePages[infinitePages.length - 1].data.length : 0
658
+ } : void 0;
632
659
  return {
633
660
  actions,
634
661
  allows,
635
662
  clientTruncated,
663
+ loadMore,
636
664
  closeDetail,
637
665
  currentPage,
638
666
  deleteConfirmed,
@@ -706,10 +734,12 @@ function useResourceTable(props) {
706
734
  );
707
735
  return {
708
736
  rows: m.page.data,
709
- total: m.page.total,
737
+ total: m.loadMore && m.page.search === "" && m.page.statusValue.length === 0 ? m.displayTotal : m.page.total,
710
738
  loading: m.isLoading,
711
739
  error: m.hasError,
712
740
  truncated: m.clientTruncated,
741
+ // Present only with `pagination="cursor"`.
742
+ ...m.loadMore ? { loadMore: m.loadMore } : {},
713
743
  columns: m.tableColumns,
714
744
  actions: m.actions,
715
745
  getKey: m.keyOf,
@@ -759,5 +789,5 @@ function useResourceTable(props) {
759
789
  }
760
790
 
761
791
  export { showUndoToast, showWriteErrorToast, useResourceTable, useResourceTableModel };
762
- //# sourceMappingURL=chunk-C7PQ54JI.js.map
763
- //# sourceMappingURL=chunk-C7PQ54JI.js.map
792
+ //# sourceMappingURL=chunk-3TWCZYZY.js.map
793
+ //# sourceMappingURL=chunk-3TWCZYZY.js.map