@marktiderman/genesis-ui 2.8.0 → 2.10.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 +75 -4
- package/dist/{AppShell-LXQykEqe.d.ts → AppShell-BVr4V0UK.d.ts} +1 -110
- package/dist/PageHeader-V9lVkZtF.d.ts +114 -0
- package/dist/{chunk-S7NXD37O.js → chunk-25XEXYGC.js} +4 -4
- package/dist/chunk-25XEXYGC.js.map +1 -0
- package/dist/chunk-C7PQ54JI.js +763 -0
- package/dist/chunk-C7PQ54JI.js.map +1 -0
- package/dist/chunk-DIV4BYXX.js +58 -0
- package/dist/chunk-DIV4BYXX.js.map +1 -0
- package/dist/chunk-I4NBE2KL.js +16 -0
- package/dist/chunk-I4NBE2KL.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 +42 -949
- package/dist/components/data/index.js +962 -777
- package/dist/components/data/index.js.map +1 -1
- package/dist/components/layout/index.d.ts +2 -1
- package/dist/components/layout/index.js +7 -7
- package/dist/components/patterns/index.js +2 -1
- package/dist/components/patterns/sortable-list.js +2 -1
- package/dist/hooks/index.d.ts +8 -1
- package/dist/hooks/index.js +4 -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 +21 -20
- package/dist/{use-resource-page-D4byuwAs.d.ts → use-resource-page-DaJzuq3M.d.ts} +63 -2
- package/dist/use-resource-table-DCVf_lEG.d.ts +1113 -0
- package/package.json +4 -8
- package/dist/chunk-DWCAC2E7.js.map +0 -1
- package/dist/chunk-S7NXD37O.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,52 @@ 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
|
+
The undo still works after the dialog closes. For a direct
|
|
82
|
+
`useResource({ mutationMode: "undoable" })`, pass `onUndoable: showUndoToast` and
|
|
83
|
+
`onError: showWriteErrorToast` (both exported from `/data`).
|
|
84
|
+
|
|
85
|
+
## Unsaved changes
|
|
86
|
+
|
|
87
|
+
Core never depends on a router. `useResourceForm` returns `isDirty` and `confirmLeave()`
|
|
88
|
+
(true when leaving is fine); `ResourceForm` reports both through `onDirtyChange`. For any
|
|
89
|
+
other state use `useUnsavedChangesGuard({ when })` from core. Wire `confirmLeave` to your
|
|
90
|
+
router's blocker:
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
// React Router (data router, v6.4+ / v7)
|
|
94
|
+
import { useBlocker } from "react-router"; // "react-router-dom" on v6
|
|
95
|
+
|
|
96
|
+
const { form, isDirty, confirmLeave } = useResourceForm({ …, warnWhenUnsaved: true });
|
|
97
|
+
const blocker = useBlocker(isDirty);
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (blocker.state !== "blocked") return;
|
|
100
|
+
if (confirmLeave()) blocker.proceed();
|
|
101
|
+
else blocker.reset();
|
|
102
|
+
}, [blocker, confirmLeave]);
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
// TanStack Router (v1)
|
|
107
|
+
import { useBlocker } from "@tanstack/react-router";
|
|
108
|
+
|
|
109
|
+
const { isDirty, confirmLeave } = useResourceForm({ …, warnWhenUnsaved: true });
|
|
110
|
+
// The hook already handles beforeunload, so the router's own is off.
|
|
111
|
+
useBlocker({ shouldBlockFn: () => !confirmLeave(), disabled: !isDirty, enableBeforeUnload: false });
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
With `ResourceForm`, keep the pair from `onDirtyChange={(dirty, confirmLeave) => …}` in state
|
|
115
|
+
and use it the same way.
|
|
116
|
+
|
|
71
117
|
## The layer contract (docs/FRAMEWORK.md)
|
|
72
118
|
|
|
73
119
|
1. Layers: tokens → primitives → patterns → layouts → data-bound; each depends downward only.
|
|
@@ -129,16 +175,41 @@ DataGrid and DetailPanel yourself. The parts apps used to hand-roll are props:
|
|
|
129
175
|
- **Clickable cards:** with a custom `renderCard`, `openOnCardClick` makes the whole card one
|
|
130
176
|
keyboard-reachable button that opens the detail. On a bare `DataGrid`, use `onItemClick` and
|
|
131
177
|
`getItemLabel`. Never wrap a card in a `div role="button"`.
|
|
132
|
-
- **
|
|
133
|
-
|
|
134
|
-
|
|
178
|
+
- **Permissions:** `can(action, item)` with `"create"`, `"edit"` or `"delete"` hides the New button or
|
|
179
|
+
a row's Edit / Delete (a UI gate; the server still decides). `canEditItem`, `canEdit` and `canDelete`
|
|
180
|
+
are deprecated aliases, still honoured.
|
|
135
181
|
- **Names and empty state:** `getTitle` / `getSubtitle` when a record's name spans fields;
|
|
136
182
|
`emptyIcon`, `emptyMessage`, `emptyDescription` and `emptyState` (its action) for the empty list.
|
|
137
183
|
- **A list inside tabs:** `header="toolbar"` (on `ResourcePage` or `DataPageShell`) drops the title
|
|
138
184
|
block and keeps the count, view toggle, filters and create button.
|
|
185
|
+
- **Shareable state:** `urlState={useReactRouterSearchParams()}` (or the TanStack / browser adapter) keeps
|
|
186
|
+
the open record (`?id=`), search, sort and, with `pagination="server"`, the page in the URL. Back
|
|
187
|
+
closes a record that was opened. A page that owns its own paging uses `useResourceUrlState`.
|
|
139
188
|
- **Non-destructive confirm:** `<AlertDialogAction variant="default">` for publish/send; red stays
|
|
140
189
|
the default for deletes.
|
|
141
190
|
|
|
191
|
+
## A different layout over the same list
|
|
192
|
+
|
|
193
|
+
`useResourceTable(options)` (from `/data` or `/hooks`) is ResourcePage without its markup. It takes the
|
|
194
|
+
same options and returns:
|
|
195
|
+
|
|
196
|
+
- the rows (`page.data`) and paging (`currentPage`, `totalPages`);
|
|
197
|
+
- search, sort and filters (`handleSearchChange`, `handleSortSelect`);
|
|
198
|
+
- selection (`selectedIds`, `handleToggleSelect`);
|
|
199
|
+
- the open record (`handleRowClick`, `detailOpen`, `selectedItem`, `closeDetail`);
|
|
200
|
+
- the built-in form and delete state.
|
|
201
|
+
|
|
202
|
+
Build a board, a calendar or a custom card wall on it instead of re-deriving that wiring.
|
|
203
|
+
`ResourcePage` is this hook plus Genesis's markup.
|
|
204
|
+
|
|
205
|
+
## Big tables
|
|
206
|
+
|
|
207
|
+
`DataTable` runs on TanStack Table. Sorting compares numbers, dates and booleans by value. For a long
|
|
208
|
+
list without pages, `pagination={false} virtualize` renders only the rows in view (`virtualize={{ height }}`
|
|
209
|
+
sets the scroll box). `pinnedColumns={{ left: ["name"], right: ["actions"] }}` keeps columns in view
|
|
210
|
+
when scrolling sideways. Users can reorder columns by dragging a header or with Alt+←/→, and the order
|
|
211
|
+
is kept per device with `columnOrderKey`.
|
|
212
|
+
|
|
142
213
|
## Full inventory
|
|
143
214
|
|
|
144
215
|
`docs/component-reference.md` lists every export with its props and stability tag. It is
|
|
@@ -1,117 +1,8 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
3
|
import { LucideIcon } from 'lucide-react';
|
|
4
|
-
import { V as ViewMode } from './view-toggle-CahQ87PW.js';
|
|
5
|
-
import { Density } from './hooks/use-view-settings.js';
|
|
6
4
|
import { N as NavigationNode, L as LinkComponent } from './navigation-Bha0R3Fu.js';
|
|
7
5
|
|
|
8
|
-
/**
|
|
9
|
-
* Declarative chrome for PageHeader — developer defaults + optional
|
|
10
|
-
* in-header user toggles (Notion/Linear “customize view” pattern).
|
|
11
|
-
*
|
|
12
|
-
* @stability Beta
|
|
13
|
-
*/
|
|
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;
|
|
22
|
-
showIcon?: boolean;
|
|
23
|
-
showSubtitle?: boolean;
|
|
24
|
-
showCount?: boolean;
|
|
25
|
-
showViewToggle?: boolean;
|
|
26
|
-
showViewSettings?: boolean;
|
|
27
|
-
showCreate?: boolean;
|
|
28
|
-
/**
|
|
29
|
-
* Stat cards row under the title block. Off by default — a header that
|
|
30
|
-
* receives `stats` still hides them until the developer or the user turns
|
|
31
|
-
* this on, so an existing page gains no new chrome by upgrading.
|
|
32
|
-
*/
|
|
33
|
-
showStats?: boolean;
|
|
34
|
-
/** Ghost ListFilter control in the actions cluster */
|
|
35
|
-
showDetailsToggle?: boolean;
|
|
36
|
-
/** Quiet hint beside actions while details are collapsed */
|
|
37
|
-
showDetailsHint?: boolean;
|
|
38
|
-
/** Move subtitle into the details panel (default true when details exist) */
|
|
39
|
-
subtitleInDetails?: boolean;
|
|
40
|
-
defaultDetailsOpen?: boolean;
|
|
41
|
-
detailsLabel?: string;
|
|
42
|
-
/**
|
|
43
|
-
* Show the in-header Header options popover so users can customize chrome.
|
|
44
|
-
* Default true when the header has more than one chrome affordance.
|
|
45
|
-
*/
|
|
46
|
-
configurable?: boolean;
|
|
47
|
-
}
|
|
48
|
-
/** One entry in {@link PageHeaderProps.secondaryActions}. */
|
|
49
|
-
interface PageHeaderAction {
|
|
50
|
-
label: string;
|
|
51
|
-
onClick: () => void;
|
|
52
|
-
icon?: LucideIcon;
|
|
53
|
-
/** `data-testid` on the menu item, so e2e can drive it. */
|
|
54
|
-
testId?: string;
|
|
55
|
-
disabled?: boolean;
|
|
56
|
-
/** Omits the action entirely — for one gated on data. */
|
|
57
|
-
hidden?: boolean;
|
|
58
|
-
}
|
|
59
|
-
interface PageHeaderProps {
|
|
60
|
-
title: string;
|
|
61
|
-
subtitle?: string;
|
|
62
|
-
icon?: LucideIcon;
|
|
63
|
-
/**
|
|
64
|
-
* Header chrome config (developer defaults). Merged under user picks from
|
|
65
|
-
* the in-header options menu when `options.configurable` / storage is on.
|
|
66
|
-
*/
|
|
67
|
-
options?: PageHeaderOptions;
|
|
68
|
-
/** Persist user header-option overrides (localStorage) */
|
|
69
|
-
optionsStorageKey?: string;
|
|
70
|
-
/** Extra trailing actions (after create); prefer first-class slots below */
|
|
71
|
-
actions?: ReactNode;
|
|
72
|
-
/**
|
|
73
|
-
* Stat cards row, full width, directly under the title block and above
|
|
74
|
-
* `details`. Layout-only: pass `<StatCard>`s (or anything else) and the
|
|
75
|
-
* header positions them. Rendered only when `options.showStats` resolves
|
|
76
|
-
* true — see {@link PageHeaderOptions.showStats}.
|
|
77
|
-
*/
|
|
78
|
-
stats?: ReactNode;
|
|
79
|
-
/** Collapsible details region below the title row */
|
|
80
|
-
details?: ReactNode;
|
|
81
|
-
detailsOpen?: boolean;
|
|
82
|
-
onDetailsOpenChange?: (open: boolean) => void;
|
|
83
|
-
detailsActive?: boolean;
|
|
84
|
-
detailsHint?: ReactNode;
|
|
85
|
-
/** Count badge */
|
|
86
|
-
count?: number;
|
|
87
|
-
totalCount?: number;
|
|
88
|
-
/** Singular entity for “N rows” copy when unfiltered */
|
|
89
|
-
entityName?: string;
|
|
90
|
-
countActive?: boolean;
|
|
91
|
-
viewMode?: ViewMode;
|
|
92
|
-
onViewModeChange?: (mode: ViewMode) => void;
|
|
93
|
-
pageSize?: number;
|
|
94
|
-
onPageSizeChange?: (size: number) => void;
|
|
95
|
-
density?: Density;
|
|
96
|
-
onDensityChange?: (density: Density) => void;
|
|
97
|
-
createAction?: ReactNode;
|
|
98
|
-
/**
|
|
99
|
-
* Additional actions, rendered in ONE overflow menu — never as more
|
|
100
|
-
* side-by-side buttons.
|
|
101
|
-
*
|
|
102
|
-
* The budget is the point. A header with three top-level buttons squeezes
|
|
103
|
-
* its own title to an ellipsis at real viewport widths, so a surface that
|
|
104
|
-
* genuinely has four actions needs somewhere to put the other three. Before
|
|
105
|
-
* this existed the only place was a fork of the header, which is exactly
|
|
106
|
-
* what one consumer did.
|
|
107
|
-
*
|
|
108
|
-
* Pass `ReactNode` via `actions` instead when you need arbitrary chrome;
|
|
109
|
-
* this prop is for the ordinary case, and it keeps the budget for you.
|
|
110
|
-
*/
|
|
111
|
-
secondaryActions?: PageHeaderAction[];
|
|
112
|
-
}
|
|
113
|
-
declare function PageHeader({ title, subtitle, icon: Icon, options: optionsProp, optionsStorageKey, actions, stats, details, detailsOpen, onDetailsOpenChange, detailsActive, detailsHint, count, totalCount, entityName, countActive, viewMode, onViewModeChange, pageSize, onPageSizeChange, density, onDensityChange, createAction, secondaryActions, }: PageHeaderProps): React.JSX.Element;
|
|
114
|
-
|
|
115
6
|
interface NavItem {
|
|
116
7
|
to: string;
|
|
117
8
|
icon: LucideIcon;
|
|
@@ -280,4 +171,4 @@ interface AppShellProps {
|
|
|
280
171
|
}
|
|
281
172
|
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;
|
|
282
173
|
|
|
283
|
-
export { AppShell as A, type NavItem as N,
|
|
174
|
+
export { AppShell as A, type NavItem as N, type ParentNavigation as P, type AppShellOptions as a, type AppShellProps as b };
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { LucideIcon } from 'lucide-react';
|
|
4
|
+
import { V as ViewMode } from './view-toggle-CahQ87PW.js';
|
|
5
|
+
import { Density } from './hooks/use-view-settings.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Declarative chrome for PageHeader — developer defaults + optional
|
|
9
|
+
* in-header user toggles (Notion/Linear “customize view” pattern).
|
|
10
|
+
*
|
|
11
|
+
* @stability Beta
|
|
12
|
+
*/
|
|
13
|
+
interface PageHeaderOptions {
|
|
14
|
+
/**
|
|
15
|
+
* The icon, title and subtitle block. Turn it off where the surrounding
|
|
16
|
+
* page already names the content (a list inside a tab), keeping only the
|
|
17
|
+
* actions row: count, view toggle, filters, create. Default true. A
|
|
18
|
+
* developer setting, not offered in the user's Header options.
|
|
19
|
+
*/
|
|
20
|
+
showTitle?: boolean;
|
|
21
|
+
showIcon?: boolean;
|
|
22
|
+
showSubtitle?: boolean;
|
|
23
|
+
showCount?: boolean;
|
|
24
|
+
showViewToggle?: boolean;
|
|
25
|
+
showViewSettings?: boolean;
|
|
26
|
+
showCreate?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Stat cards row under the title block. Off by default — a header that
|
|
29
|
+
* receives `stats` still hides them until the developer or the user turns
|
|
30
|
+
* this on, so an existing page gains no new chrome by upgrading.
|
|
31
|
+
*/
|
|
32
|
+
showStats?: boolean;
|
|
33
|
+
/** Ghost ListFilter control in the actions cluster */
|
|
34
|
+
showDetailsToggle?: boolean;
|
|
35
|
+
/** Quiet hint beside actions while details are collapsed */
|
|
36
|
+
showDetailsHint?: boolean;
|
|
37
|
+
/** Move subtitle into the details panel (default true when details exist) */
|
|
38
|
+
subtitleInDetails?: boolean;
|
|
39
|
+
defaultDetailsOpen?: boolean;
|
|
40
|
+
detailsLabel?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Show the in-header Header options popover so users can customize chrome.
|
|
43
|
+
* Default true when the header has more than one chrome affordance.
|
|
44
|
+
*/
|
|
45
|
+
configurable?: boolean;
|
|
46
|
+
}
|
|
47
|
+
/** One entry in {@link PageHeaderProps.secondaryActions}. */
|
|
48
|
+
interface PageHeaderAction {
|
|
49
|
+
label: string;
|
|
50
|
+
onClick: () => void;
|
|
51
|
+
icon?: LucideIcon;
|
|
52
|
+
/** `data-testid` on the menu item, so e2e can drive it. */
|
|
53
|
+
testId?: string;
|
|
54
|
+
disabled?: boolean;
|
|
55
|
+
/** Omits the action entirely — for one gated on data. */
|
|
56
|
+
hidden?: boolean;
|
|
57
|
+
}
|
|
58
|
+
interface PageHeaderProps {
|
|
59
|
+
title: string;
|
|
60
|
+
subtitle?: string;
|
|
61
|
+
icon?: LucideIcon;
|
|
62
|
+
/**
|
|
63
|
+
* Header chrome config (developer defaults). Merged under user picks from
|
|
64
|
+
* the in-header options menu when `options.configurable` / storage is on.
|
|
65
|
+
*/
|
|
66
|
+
options?: PageHeaderOptions;
|
|
67
|
+
/** Persist user header-option overrides (localStorage) */
|
|
68
|
+
optionsStorageKey?: string;
|
|
69
|
+
/** Extra trailing actions (after create); prefer first-class slots below */
|
|
70
|
+
actions?: ReactNode;
|
|
71
|
+
/**
|
|
72
|
+
* Stat cards row, full width, directly under the title block and above
|
|
73
|
+
* `details`. Layout-only: pass `<StatCard>`s (or anything else) and the
|
|
74
|
+
* header positions them. Rendered only when `options.showStats` resolves
|
|
75
|
+
* true — see {@link PageHeaderOptions.showStats}.
|
|
76
|
+
*/
|
|
77
|
+
stats?: ReactNode;
|
|
78
|
+
/** Collapsible details region below the title row */
|
|
79
|
+
details?: ReactNode;
|
|
80
|
+
detailsOpen?: boolean;
|
|
81
|
+
onDetailsOpenChange?: (open: boolean) => void;
|
|
82
|
+
detailsActive?: boolean;
|
|
83
|
+
detailsHint?: ReactNode;
|
|
84
|
+
/** Count badge */
|
|
85
|
+
count?: number;
|
|
86
|
+
totalCount?: number;
|
|
87
|
+
/** Singular entity for “N rows” copy when unfiltered */
|
|
88
|
+
entityName?: string;
|
|
89
|
+
countActive?: boolean;
|
|
90
|
+
viewMode?: ViewMode;
|
|
91
|
+
onViewModeChange?: (mode: ViewMode) => void;
|
|
92
|
+
pageSize?: number;
|
|
93
|
+
onPageSizeChange?: (size: number) => void;
|
|
94
|
+
density?: Density;
|
|
95
|
+
onDensityChange?: (density: Density) => void;
|
|
96
|
+
createAction?: ReactNode;
|
|
97
|
+
/**
|
|
98
|
+
* Additional actions, rendered in ONE overflow menu — never as more
|
|
99
|
+
* side-by-side buttons.
|
|
100
|
+
*
|
|
101
|
+
* The budget is the point. A header with three top-level buttons squeezes
|
|
102
|
+
* its own title to an ellipsis at real viewport widths, so a surface that
|
|
103
|
+
* genuinely has four actions needs somewhere to put the other three. Before
|
|
104
|
+
* this existed the only place was a fork of the header, which is exactly
|
|
105
|
+
* what one consumer did.
|
|
106
|
+
*
|
|
107
|
+
* Pass `ReactNode` via `actions` instead when you need arbitrary chrome;
|
|
108
|
+
* this prop is for the ordinary case, and it keeps the budget for you.
|
|
109
|
+
*/
|
|
110
|
+
secondaryActions?: PageHeaderAction[];
|
|
111
|
+
}
|
|
112
|
+
declare function PageHeader({ title, subtitle, icon: Icon, options: optionsProp, optionsStorageKey, actions, stats, details, detailsOpen, onDetailsOpenChange, detailsActive, detailsHint, count, totalCount, entityName, countActive, viewMode, onViewModeChange, pageSize, onPageSizeChange, density, onDensityChange, createAction, secondaryActions, }: PageHeaderProps): React.JSX.Element;
|
|
113
|
+
|
|
114
|
+
export { PageHeader as P, type PageHeaderOptions as a, type PageHeaderProps as b, type PageHeaderAction as c };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import { useAnnouncer } from './chunk-I4NBE2KL.js';
|
|
2
3
|
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from './chunk-QJ6P2DW6.js';
|
|
3
4
|
import { cn } from './chunk-NZ5WBA4R.js';
|
|
4
5
|
import * as React from 'react';
|
|
@@ -32,7 +33,7 @@ function SortableList({
|
|
|
32
33
|
const [preview, setPreview] = React.useState(null);
|
|
33
34
|
const [pending, setPending] = React.useState(false);
|
|
34
35
|
const pendingRef = React.useRef(false);
|
|
35
|
-
const
|
|
36
|
+
const { message: announcement, announce } = useAnnouncer();
|
|
36
37
|
const handles = React.useRef(/* @__PURE__ */ new Map());
|
|
37
38
|
const restoreFocus = React.useRef(null);
|
|
38
39
|
const instructionsId = React.useId();
|
|
@@ -44,7 +45,6 @@ function SortableList({
|
|
|
44
45
|
const item = byKey.get(key);
|
|
45
46
|
return item === void 0 ? "Item" : getItemLabel(item);
|
|
46
47
|
};
|
|
47
|
-
const announce = (message) => setAnnouncement((previous) => previous === message ? `${message}\xA0` : message);
|
|
48
48
|
const keepFocus = () => {
|
|
49
49
|
const active = typeof document === "undefined" ? null : document.activeElement;
|
|
50
50
|
restoreFocus.current = active instanceof HTMLElement && active !== document.body ? active : null;
|
|
@@ -317,5 +317,5 @@ function SortableList({
|
|
|
317
317
|
}
|
|
318
318
|
|
|
319
319
|
export { SortableList };
|
|
320
|
-
//# sourceMappingURL=chunk-
|
|
321
|
-
//# sourceMappingURL=chunk-
|
|
320
|
+
//# sourceMappingURL=chunk-25XEXYGC.js.map
|
|
321
|
+
//# sourceMappingURL=chunk-25XEXYGC.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/patterns/sortable-list.tsx"],"names":[],"mappings":"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","file":"chunk-25XEXYGC.js","sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport { GripVertical, MoreVertical } from \"lucide-react\";\nimport { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from \"../ui/dropdown-menu\";\nimport { cn } from \"../../utils\";\nimport { useAnnouncer } from \"./use-announcer\";\n\nexport interface SortableListItemContext {\n /** The item's position as shown, from 0. */\n index: number;\n /**\n * The drag handle: drag it with a pointer, or focus it and press Space or\n * Enter to pick the item up, the arrow keys to move it and Space or Enter\n * again to drop it. Render it somewhere in the row.\n */\n handle: React.ReactNode;\n /** True while this item is being dragged with a pointer. */\n isDragging: boolean;\n /** True while this item is picked up from the keyboard. */\n isGrabbed: boolean;\n /** Move this item without dragging, for a row menu of your own. */\n moveUp: () => void;\n moveDown: () => void;\n moveTo: (index: number) => void;\n}\n\nexport interface SortableListReorder {\n /** Key of the item that moved. */\n key: string;\n}\n\nexport interface SortableListProps<T> {\n items: readonly T[];\n /** A stable key per item. Required: dragging finds the item again by key. */\n getKey: (item: T) => string;\n /** A name for an item, for the handle's label and announcements (\"Week 1\"). */\n getItemLabel: (item: T) => string;\n renderItem: (item: T, context: SortableListItemContext) => React.ReactNode;\n /**\n * Move the item at `from` to `to` (both from 0, `to` counted after the item\n * is taken out). Each finished move calls this once: a keyboard pick-up\n * that wanders and comes back calls nothing.\n *\n * The list shows the new order straight away and keeps showing it until the\n * returned promise settles; by then `items` must be in the new order. A\n * synchronous handler must update `items` in the same tick. A rejected\n * promise puts the old order back and announces that the move failed. Other\n * moves wait until this one settles.\n */\n onReorder: (from: number, to: number, move: SortableListReorder) => void | Promise<void>;\n /** Accessible name of the list (\"Course steps\"). */\n label: string;\n /** Turns off dragging and every kind of move. */\n disabled?: boolean | undefined;\n /**\n * A \"Move\" menu on each row (up, down, to top, to bottom), on by default so\n * touch and screen-reader users always have a way to move an item. Turn it\n * off only when the row offers `moveUp` / `moveDown` / `moveTo` itself.\n */\n moveMenu?: boolean | undefined;\n className?: string | undefined;\n itemClassName?: string | undefined;\n}\n\ntype DropAt = { key: string; edge: \"before\" | \"after\" };\n\nconst DRAG_TYPE = \"application/x-genesis-sortable-item\";\n\n// useLayoutEffect warns during server rendering; nothing needs refocusing there.\nconst useIsomorphicLayoutEffect = typeof window === \"undefined\" ? React.useEffect : React.useLayoutEffect;\n\nfunction moved(keys: readonly string[], from: number, to: number) {\n const next = keys.slice();\n const [key] = next.splice(from, 1);\n next.splice(to, 0, key!);\n return next;\n}\n\n/**\n * A vertical list the user can rearrange: course steps, lesson blocks, form\n * fields.\n *\n * Every way to move an item is equivalent, so dragging is never the only one\n * (the same stance as KanbanBoard's \"Move to…\" menu):\n * - drag the handle with a pointer;\n * - focus the handle, Space or Enter to pick up, ↑/↓ and Home/End to move,\n * Space or Enter to drop, Escape to put it back (tabbing away also puts it\n * back); Alt+↑/↓ moves one step without picking up;\n * - the row's \"Move\" menu, or `moveUp` / `moveDown` / `moveTo` from the render\n * context.\n *\n * Moves are announced in a polite live region once they succeed. Native drag\n * and drop needs no dependency and Radix has none to offer (docs/FRAMEWORK.md\n * rule 3).\n *\n * @stability Beta\n */\nexport function SortableList<T>({\n items,\n getKey,\n getItemLabel,\n renderItem,\n onReorder,\n label,\n disabled,\n moveMenu = true,\n className,\n itemClassName,\n}: SortableListProps<T>) {\n const [armed, setArmed] = React.useState<string | null>(null);\n const [dragging, setDragging] = React.useState<string | null>(null);\n const [dropAt, setDropAt] = React.useState<DropAt | null>(null);\n // While a key is grabbed, `preview` is the order on screen; nothing is\n // written until the drop.\n const [grabbed, setGrabbed] = React.useState<{ key: string; from: number } | null>(null);\n // The order to show instead of `items`: during a keyboard pick-up, and from\n // a committed move until its promise settles.\n const [preview, setPreview] = React.useState<string[] | null>(null);\n const [pending, setPending] = React.useState(false);\n // The same, readable within one tick: two moves fired back to back must\n // not both start from the old order.\n const pendingRef = React.useRef(false);\n const { message: announcement, announce } = useAnnouncer();\n const handles = React.useRef(new Map<string, HTMLButtonElement>());\n // The element that had focus when a move started, to put focus back on if\n // the DOM move drops it to the body. Cleared after every commit.\n const restoreFocus = React.useRef<HTMLElement | null>(null);\n const instructionsId = React.useId();\n\n const byKey = new Map(items.map((item) => [getKey(item), item] as const));\n const itemKeys = items.map(getKey);\n const keys = preview && preview.length === itemKeys.length && preview.every((k) => byKey.has(k)) ? preview : itemKeys;\n const total = keys.length;\n const nameOf = (key: string) => {\n const item = byKey.get(key);\n return item === undefined ? \"Item\" : getItemLabel(item);\n };\n\n // Moving a focused element in the DOM drops focus to the body in real\n // browsers. Remember what had focus, the handle or a row's own button, and\n // put it back after the commit.\n const keepFocus = () => {\n const active = typeof document === \"undefined\" ? null : document.activeElement;\n restoreFocus.current = active instanceof HTMLElement && active !== document.body ? active : null;\n };\n useIsomorphicLayoutEffect(() => {\n const element = restoreFocus.current;\n restoreFocus.current = null;\n if (!element || !element.isConnected) return;\n const active = document.activeElement;\n if (active !== element && (active === null || active === document.body)) element.focus();\n });\n\n // An item that disappears mid-gesture, or the list turning disabled, ends\n // the gesture.\n React.useEffect(() => {\n if (disabled && (grabbed || armed || dragging)) {\n setGrabbed(null);\n setArmed(null);\n setDragging(null);\n setDropAt(null);\n if (!pendingRef.current) setPreview(null);\n return;\n }\n if (dragging && !byKey.has(dragging)) {\n setDragging(null);\n setDropAt(null);\n setArmed(null);\n }\n if (grabbed && !byKey.has(grabbed.key)) {\n setGrabbed(null);\n setPreview(null);\n }\n });\n\n const commit = (key: string, from: number, to: number, message: string) => {\n if (from === to) return;\n pendingRef.current = true;\n setPending(true);\n setPreview(moved(itemKeys, from, to));\n // Called synchronously, so a synchronous handler updates `items` in the\n // same tick as the preview.\n let result: void | Promise<void>;\n try {\n result = onReorder(from, to, { key });\n } catch (error) {\n result = Promise.reject(error);\n }\n Promise.resolve(result)\n .then(\n () => announce(message),\n () => announce(`Couldn’t move ${nameOf(key)}.`),\n )\n .finally(() => {\n // A rejected move reverts the order, which moves rows again.\n keepFocus();\n pendingRef.current = false;\n setPending(false);\n setPreview(null);\n });\n };\n\n const move = (key: string, to: number) => {\n if (disabled || pendingRef.current || grabbed) return;\n if (!Number.isFinite(to)) return;\n const from = itemKeys.indexOf(key);\n const target = Math.max(0, Math.min(total - 1, Math.round(to)));\n if (from < 0 || target === from) return;\n keepFocus();\n commit(key, from, target, `${nameOf(key)} moved to position ${target + 1} of ${total}.`);\n };\n\n const pickUp = (key: string) => {\n if (disabled || pendingRef.current) return;\n const from = itemKeys.indexOf(key);\n if (from < 0) return;\n setGrabbed({ key, from });\n setPreview(itemKeys);\n announce(\n `${nameOf(key)} picked up, position ${from + 1} of ${total}. Use the up and down arrow keys to move, Space to drop, Escape to cancel.`,\n );\n };\n\n const drop = (key: string) => {\n if (!grabbed) return;\n const to = keys.indexOf(key);\n const from = grabbed.from;\n setGrabbed(null);\n if (to === from) {\n setPreview(null);\n announce(`${nameOf(key)} dropped at position ${to + 1} of ${total}.`);\n return;\n }\n keepFocus();\n commit(key, from, to, `${nameOf(key)} dropped at position ${to + 1} of ${total}.`);\n };\n\n const cancel = (key: string) => {\n if (!grabbed) return;\n const from = grabbed.from;\n keepFocus();\n setGrabbed(null);\n setPreview(null);\n announce(`Move cancelled. ${nameOf(key)} is back at position ${from + 1} of ${total}.`);\n };\n\n const step = (key: string, to: number) => {\n const index = keys.indexOf(key);\n const target = Math.max(0, Math.min(total - 1, to));\n if (index < 0 || target === index) return;\n keepFocus();\n setPreview(moved(keys, index, target));\n announce(`${nameOf(key)} moved to position ${target + 1} of ${total}.`);\n };\n\n // Escape while an item is picked up must not also close a surrounding\n // Dialog or Sheet, which listen before the handle does.\n React.useEffect(() => {\n if (!grabbed) return;\n const onKey = (event: KeyboardEvent) => {\n if (event.key !== \"Escape\") return;\n event.preventDefault();\n event.stopPropagation();\n cancel(grabbed.key);\n };\n window.addEventListener(\"keydown\", onKey, true);\n return () => window.removeEventListener(\"keydown\", onKey, true);\n });\n\n // A press on the handle arms its row for native dragging; any release or\n // cancel, wherever it lands, disarms it.\n React.useEffect(() => {\n if (!armed || dragging) return;\n const disarm = () => setArmed(null);\n // A context menu or a release outside the window never sends pointerup.\n const events = [\"pointerup\", \"pointercancel\", \"contextmenu\", \"blur\"] as const;\n for (const type of events) window.addEventListener(type, disarm, { once: true });\n return () => {\n for (const type of events) window.removeEventListener(type, disarm);\n };\n }, [armed, dragging]);\n\n const onHandleKeyDown = (event: React.KeyboardEvent, key: string) => {\n if (disabled) return;\n const isGrabbed = grabbed?.key === key;\n const index = keys.indexOf(key);\n const delta = event.key === \"ArrowUp\" ? -1 : event.key === \"ArrowDown\" ? 1 : 0;\n if (isGrabbed) {\n if (delta) {\n event.preventDefault();\n step(key, index + delta);\n } else if (event.key === \"Home\" || event.key === \"End\") {\n event.preventDefault();\n step(key, event.key === \"Home\" ? 0 : total - 1);\n }\n return;\n }\n if (delta && event.altKey) {\n event.preventDefault();\n move(key, index + delta);\n }\n };\n\n // Space and Enter reach the handle as a click with `detail === 0`, as do\n // screen readers' activations; a pointer click is a press for dragging.\n const onHandleClick = (event: React.MouseEvent, key: string) => {\n if (disabled || event.detail !== 0) return;\n if (grabbed?.key === key) drop(key);\n else if (!grabbed) pickUp(key);\n };\n\n const onDragOver = (event: React.DragEvent<HTMLLIElement>, key: string) => {\n if (!dragging) return;\n event.preventDefault();\n event.dataTransfer.dropEffect = \"move\";\n const rect = event.currentTarget.getBoundingClientRect();\n const edge = event.clientY < rect.top + rect.height / 2 ? \"before\" : \"after\";\n if (dropAt?.key !== key || dropAt.edge !== edge) setDropAt({ key, edge });\n };\n\n const endDrag = () => {\n setDragging(null);\n setDropAt(null);\n setArmed(null);\n };\n\n const onDrop = (event: React.DragEvent) => {\n event.preventDefault();\n const key = event.dataTransfer.getData(DRAG_TYPE) || dragging;\n const target = dropAt;\n endDrag();\n if (!key || !target || pendingRef.current || grabbed) return;\n const from = itemKeys.indexOf(key);\n const over = itemKeys.indexOf(target.key);\n if (from < 0 || over < 0) return;\n // `to` counts positions after the dragged item is taken out.\n let to = target.edge === \"before\" ? over : over + 1;\n if (from < to) to -= 1;\n commit(key, from, to, `${nameOf(key)} dropped at position ${to + 1} of ${total}.`);\n };\n\n return (\n <>\n <ul aria-label={label} aria-busy={pending || undefined} className={cn(\"flex flex-col gap-1\", className)}>\n {keys.map((key, index) => {\n const item = byKey.get(key)!;\n const itemLabel = getItemLabel(item);\n const isGrabbed = grabbed?.key === key;\n const isDragging = dragging === key;\n const handle = disabled ? null : (\n <button\n type=\"button\"\n ref={(node) => {\n if (node) handles.current.set(key, node);\n else handles.current.delete(key);\n }}\n aria-label={`Reorder ${itemLabel}`}\n aria-describedby={instructionsId}\n data-grabbed={isGrabbed || undefined}\n className={cn(\n \"inline-flex h-7 w-5 shrink-0 cursor-grab items-center justify-center rounded text-muted-foreground\",\n \"hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n isGrabbed && \"bg-accent text-foreground\",\n )}\n onPointerDown={() => {\n if (!pendingRef.current && !grabbed) setArmed(key);\n }}\n onPointerCancel={() => setArmed(null)}\n onClick={(event) => onHandleClick(event, key)}\n onKeyDown={(event) => onHandleKeyDown(event, key)}\n onBlur={() => {\n // A DOM move during the pick-up blurs the handle for a moment;\n // that is not the user leaving.\n if (isGrabbed && restoreFocus.current !== handles.current.get(key)) cancel(key);\n }}\n >\n <GripVertical className=\"h-4 w-4\" aria-hidden />\n </button>\n );\n return (\n <li\n key={key}\n data-sortable-key={key}\n draggable={(!disabled && armed === key) || undefined}\n className={cn(\n \"relative rounded-md\",\n moveMenu && !disabled && \"pr-8\",\n isDragging && \"opacity-50\",\n isGrabbed && \"ring-2 ring-primary/50\",\n itemClassName,\n )}\n onDragStart={(event) => {\n if (disabled || armed !== key) return;\n event.dataTransfer.setData(DRAG_TYPE, key);\n event.dataTransfer.effectAllowed = \"move\";\n setDragging(key);\n }}\n onDragEnd={endDrag}\n onDragOver={(event) => onDragOver(event, key)}\n onDrop={onDrop}\n >\n {dropAt?.key === key && dragging !== key && (\n <span\n aria-hidden\n className={cn(\n \"pointer-events-none absolute inset-x-0 h-0.5 rounded bg-primary\",\n dropAt.edge === \"before\" ? \"-top-0.5\" : \"-bottom-0.5\",\n )}\n />\n )}\n {renderItem(item, {\n index,\n handle,\n isDragging,\n isGrabbed,\n moveUp: () => move(key, index - 1),\n moveDown: () => move(key, index + 1),\n moveTo: (to) => move(key, to),\n })}\n {moveMenu && !disabled && total > 1 && (\n <DropdownMenu>\n <DropdownMenuTrigger\n aria-label={`Move ${itemLabel}`}\n className=\"absolute right-1 top-1 rounded p-1 text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n >\n <MoreVertical className=\"h-4 w-4\" aria-hidden />\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n <DropdownMenuItem disabled={index === 0} onSelect={() => move(key, index - 1)}>\n Move up\n </DropdownMenuItem>\n <DropdownMenuItem disabled={index === total - 1} onSelect={() => move(key, index + 1)}>\n Move down\n </DropdownMenuItem>\n <DropdownMenuItem disabled={index === 0} onSelect={() => move(key, 0)}>\n Move to top\n </DropdownMenuItem>\n <DropdownMenuItem disabled={index === total - 1} onSelect={() => move(key, total - 1)}>\n Move to bottom\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n )}\n </li>\n );\n })}\n </ul>\n <p id={instructionsId} hidden>\n Space or Enter picks the item up. Up and down arrows, Home and End move it. Space or Enter drops it, Escape puts\n it back. Alt with an arrow moves it one step.\n </p>\n <div aria-live=\"polite\" aria-atomic=\"true\" className=\"sr-only\">\n {announcement}\n </div>\n </>\n );\n}\n"]}
|