@object-ui/layout 17.5.0 → 17.7.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/CHANGELOG.md +1946 -0
- package/README.md +160 -22
- package/dist/AppSchemaRenderer.d.ts +27 -7
- package/dist/AppShell.d.ts +28 -3
- package/dist/NavigationRenderer.d.ts +148 -46
- package/dist/PageHeader.d.ts +12 -5
- package/dist/ResponsiveGrid.d.ts +53 -18
- package/dist/index.d.ts +7 -7
- package/dist/index.js +655 -619
- package/dist/index.umd.cjs +1 -1
- package/dist/navOrder.d.ts +18 -0
- package/package.json +12 -12
package/README.md
CHANGED
|
@@ -23,10 +23,9 @@ pnpm add @object-ui/layout
|
|
|
23
23
|
|
|
24
24
|
## Registration
|
|
25
25
|
|
|
26
|
-
Importing this package registers its component keys (`page
|
|
27
|
-
`app-
|
|
28
|
-
|
|
29
|
-
import is enough:
|
|
26
|
+
Importing this package registers its component keys (`page:card`,
|
|
27
|
+
`app-schema-renderer`) on the `ComponentRegistry` as a module load side effect,
|
|
28
|
+
so the side-effect-only import is enough:
|
|
30
29
|
|
|
31
30
|
```typescript
|
|
32
31
|
import '@object-ui/layout';
|
|
@@ -39,34 +38,75 @@ used to say `"sideEffects": false`, and a bundler honouring it dropped the
|
|
|
39
38
|
registration silently). `registerLayout()` is also exported for hosts that
|
|
40
39
|
prefer to register explicitly.
|
|
41
40
|
|
|
41
|
+
The `page-header` key is retired (objectui#10859): author `page:header`, which
|
|
42
|
+
`@object-ui/components` registers, with the secondary line as `subtitle`. `PageHeader`
|
|
43
|
+
stays an export for JSX composition.
|
|
44
|
+
|
|
45
|
+
The `responsive-grid` and `navigation-renderer` keys are retired too (objectui#11441).
|
|
46
|
+
A breakpoint grid is the `grid` node with a breakpoint `columns` object
|
|
47
|
+
(`{ "type": "grid", "columns": { "xs": 1, "md": 2 }, "gap": 4 }`), and navigation is
|
|
48
|
+
application metadata: the app's `navigation` items, drawn by the shell.
|
|
49
|
+
`ResponsiveGrid` and `NavigationRenderer` stay exports for JSX composition.
|
|
50
|
+
|
|
42
51
|
## Components
|
|
43
52
|
|
|
44
53
|
### AppShell
|
|
45
54
|
|
|
46
|
-
Complete application shell with
|
|
55
|
+
Complete application shell with a top navbar, a sidebar, and a main content area.
|
|
47
56
|
|
|
48
57
|
```typescript
|
|
49
58
|
import { AppShell } from '@object-ui/layout';
|
|
50
59
|
|
|
51
60
|
<AppShell
|
|
52
|
-
|
|
61
|
+
navbar={<div>Navbar Content</div>}
|
|
53
62
|
sidebar={<div>Sidebar Content</div>}
|
|
54
63
|
>
|
|
55
64
|
<div>Main Content</div>
|
|
56
65
|
</AppShell>
|
|
57
66
|
```
|
|
58
67
|
|
|
68
|
+
The top bar's content goes in `navbar`. `AppShell` renders the `<header>`
|
|
69
|
+
element itself (`src/AppShell.tsx:248`) and `{navbar}` is the only thing that
|
|
70
|
+
fills it (`:249`) — there is no `header` prop. This example used to pass one
|
|
71
|
+
(objectui#4817), and because the component destructures a fixed key list with
|
|
72
|
+
**no rest element** (`:233-241`), the node was built and then dropped on the
|
|
73
|
+
floor: copied verbatim, the snippet rendered an empty top bar and said nothing.
|
|
74
|
+
|
|
75
|
+
Like `SidebarNav` below, `AppShell` is composed in JSX and is **not** on the
|
|
76
|
+
`ComponentRegistry`: four of its seven props are `React.ReactNode` slots that no
|
|
77
|
+
JSON document can fill, so the `app-shell` key was retired in objectui#4841 and
|
|
78
|
+
`{ "type": "app-shell" }` now reports `Unknown component type`. When the whole
|
|
79
|
+
shell has to come from metadata, the key for that is `app-schema-renderer`.
|
|
80
|
+
|
|
81
|
+
#### `AppShellProps`
|
|
82
|
+
|
|
83
|
+
| Prop | Type | Default | Description |
|
|
84
|
+
| --- | --- | --- | --- |
|
|
85
|
+
| `children` | `React.ReactNode` | — (required) | Main content, rendered inside the `<main>` element. |
|
|
86
|
+
| `navbar` | `React.ReactNode` | — | Top bar content. `AppShell` supplies the sticky `<header>` around it, so pass only what goes inside. |
|
|
87
|
+
| `sidebar` | `React.ReactNode` | — | Left sidebar node, rendered as a flex sibling of the content. Pass `SidebarNav`, or your own node. |
|
|
88
|
+
| `rightRail` | `React.ReactNode` | — | Optional right-side rail. It reflows the content beside it rather than overlaying it (ADR-0057 P3a); absent → unchanged single-pane layout. |
|
|
89
|
+
| `className` | `string` | — | Tailwind overrides for the `<main>` content element — **not** for the outer container. |
|
|
90
|
+
| `defaultOpen` | `boolean` | `true` | Initial open state of the underlying Shadcn `SidebarProvider`. |
|
|
91
|
+
| `branding` | `AppShellBranding` | — | App branding, applied by `useAppShellBranding`: `primaryColor` / `accentColor` become CSS custom properties on the document root (re-derived for dark mode), `favicon` sets the icon link's `href`, and `title` sets `document.title`. |
|
|
92
|
+
|
|
59
93
|
### PageHeader
|
|
60
94
|
|
|
61
|
-
Page title block with an optional
|
|
95
|
+
Page title block with an optional subtitle, used at the top of a page's
|
|
62
96
|
content area.
|
|
63
97
|
|
|
64
98
|
```typescript
|
|
65
99
|
import { PageHeader } from '@object-ui/layout';
|
|
66
100
|
|
|
67
|
-
<PageHeader title="Dashboard"
|
|
101
|
+
<PageHeader title="Dashboard" subtitle="View your metrics" />
|
|
68
102
|
```
|
|
69
103
|
|
|
104
|
+
`subtitle` is the only spelling for the secondary line — it is the key
|
|
105
|
+
`@objectstack/spec/ui`'s `PageHeaderProps` declares. The legacy `description`
|
|
106
|
+
alias this component used to read as well was retired in objectui#3789; stored
|
|
107
|
+
metadata still carrying it is rewritten to `subtitle` at load time by the
|
|
108
|
+
ADR-0087 D2 conversion `page-header-subtitle-alias`.
|
|
109
|
+
|
|
70
110
|
> **Rendering a whole `page` node?** That belongs to `PageRenderer` in
|
|
71
111
|
> `@object-ui/components`, which is what the `page` component key resolves to —
|
|
72
112
|
> it handles page types (record/home/app/utility), named regions and page
|
|
@@ -78,42 +118,129 @@ import { PageHeader } from '@object-ui/layout';
|
|
|
78
118
|
Navigation sidebar component with React Router integration.
|
|
79
119
|
|
|
80
120
|
```typescript
|
|
81
|
-
import { SidebarNav } from '@object-ui/layout';
|
|
121
|
+
import { SidebarNav, type NavItem } from '@object-ui/layout';
|
|
122
|
+
import { Home, Settings, Users } from 'lucide-react';
|
|
82
123
|
|
|
83
|
-
const navItems = [
|
|
84
|
-
{
|
|
85
|
-
{
|
|
86
|
-
{
|
|
124
|
+
const navItems: NavItem[] = [
|
|
125
|
+
{ title: 'Dashboard', href: '/dashboard', icon: Home },
|
|
126
|
+
{ title: 'Users', href: '/users', icon: Users },
|
|
127
|
+
{ title: 'Settings', href: '/settings', icon: Settings },
|
|
87
128
|
];
|
|
88
129
|
|
|
89
130
|
<SidebarNav items={navItems} />
|
|
90
131
|
```
|
|
91
132
|
|
|
133
|
+
An item's label is `title` and its target is `href` — and `icon` is a **component**,
|
|
134
|
+
not an icon name: it is rendered as `<item.icon />` (`src/SidebarNav.tsx:60`, `:109`),
|
|
135
|
+
so pass the imported Lucide component itself. This example used to be written with
|
|
136
|
+
`label` / `path` / `icon: 'home'`, none of which `NavItem` declares (objectui#3999);
|
|
137
|
+
copied as-is it produced rows with no label at all, a `NavLink` whose `to` was
|
|
138
|
+
`undefined`, and the string `'home'` handed to React as an unknown lowercase tag.
|
|
139
|
+
Annotating the array as `NavItem[]` is what turns that whole class of typo back into
|
|
140
|
+
a compile error where it is written, instead of a blank sidebar at runtime.
|
|
141
|
+
|
|
142
|
+
#### `SidebarNavProps`
|
|
143
|
+
|
|
144
|
+
| Prop | Type | Default | Description |
|
|
145
|
+
| --- | --- | --- | --- |
|
|
146
|
+
| `items` | `NavItem[] \| NavGroup[]` | — (required) | Flat item list, or grouped sections. The array must be homogeneous: only the **first** element is probed to decide which of the two it is. |
|
|
147
|
+
| `title` | `string` | `'Application'` | Section label shown above a flat `NavItem[]`. Ignored when `items` is a `NavGroup[]` — each group prints its own `label`. |
|
|
148
|
+
| `className` | `string` | — | Tailwind overrides, forwarded to the root `Sidebar`. |
|
|
149
|
+
| `collapsible` | `'offcanvas' \| 'icon' \| 'none'` | `'icon'` | Collapse behaviour of the underlying Shadcn `Sidebar`. |
|
|
150
|
+
| `searchEnabled` | `boolean` | `false` | Renders a search box that filters items by `title` (an item also survives when one of its `children` matches). |
|
|
151
|
+
| `searchPlaceholder` | `string` | `'Search…'` | Placeholder for that search box. |
|
|
152
|
+
|
|
153
|
+
#### `NavItem`
|
|
154
|
+
|
|
155
|
+
| Key | Type | Default | Description |
|
|
156
|
+
| --- | --- | --- | --- |
|
|
157
|
+
| `title` | `string` | — (required) | The visible label, and what search matches against. |
|
|
158
|
+
| `href` | `string` | — (required) | `NavLink` target; also the React key, so keep it unique within its list. Active state is `pathname === href`. |
|
|
159
|
+
| `icon` | `React.ComponentType<{ className?: string }>` | — | The icon **component** (e.g. `Home` from `lucide-react`), not its name. |
|
|
160
|
+
| `badge` | `string \| number` | — | Trailing badge content. Rendered whenever it is not `null`/`undefined`, so `0` shows. |
|
|
161
|
+
| `badgeVariant` | `'default' \| 'destructive' \| 'outline'` | `'default'` | Badge styling. |
|
|
162
|
+
| `children` | `NavItem[]` | — | Nested sub-items. A non-empty list turns the row into a collapsible group: the parent's own `href` is then no longer a link, only the children are. |
|
|
163
|
+
|
|
164
|
+
#### `NavGroup`
|
|
165
|
+
|
|
166
|
+
| Key | Type | Description |
|
|
167
|
+
| --- | --- | --- |
|
|
168
|
+
| `label` | `string` | Section heading printed above the group. |
|
|
169
|
+
| `items` | `NavItem[]` | The group's items — same `NavItem` shape as above, nesting included. |
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
import { SidebarNav, type NavGroup } from '@object-ui/layout';
|
|
173
|
+
import { FolderOpen, Home, Settings } from 'lucide-react';
|
|
174
|
+
|
|
175
|
+
const navGroups: NavGroup[] = [
|
|
176
|
+
{
|
|
177
|
+
label: 'Workspace',
|
|
178
|
+
items: [
|
|
179
|
+
{ title: 'Dashboard', href: '/dashboard', icon: Home },
|
|
180
|
+
{
|
|
181
|
+
title: 'Projects',
|
|
182
|
+
href: '/projects',
|
|
183
|
+
icon: FolderOpen,
|
|
184
|
+
badge: 3,
|
|
185
|
+
children: [
|
|
186
|
+
{ title: 'Active', href: '/projects/active' },
|
|
187
|
+
{ title: 'Archived', href: '/projects/archived', badge: 'WIP', badgeVariant: 'outline' },
|
|
188
|
+
],
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
label: 'System',
|
|
194
|
+
items: [{ title: 'Settings', href: '/settings', icon: Settings }],
|
|
195
|
+
},
|
|
196
|
+
];
|
|
197
|
+
|
|
198
|
+
<SidebarNav items={navGroups} searchEnabled />
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Note that `SidebarNav` is a plain React component: unlike the keys listed under
|
|
202
|
+
[Registration](#registration) it is **not** on the `ComponentRegistry`, so it is
|
|
203
|
+
composed in JSX rather than authored as a JSON node. Full guide:
|
|
204
|
+
[SidebarNav docs](https://www.objectui.org/docs/layout/sidebar-nav).
|
|
205
|
+
|
|
92
206
|
## Usage with React Router
|
|
93
207
|
|
|
94
208
|
The layout components are designed to work seamlessly with React Router:
|
|
95
209
|
|
|
96
210
|
```typescript
|
|
97
|
-
import {
|
|
211
|
+
import type { ComponentType, ReactNode } from 'react';
|
|
98
212
|
import { AppShell, SidebarNav } from '@object-ui/layout';
|
|
213
|
+
import { Home, Users } from 'lucide-react';
|
|
214
|
+
|
|
215
|
+
// `react-router-dom` is a PEER dependency (see Installation above): your app
|
|
216
|
+
// installs it, this package does not. These three stand in for what you would
|
|
217
|
+
// import from it, so the composition below is still checked against the shipped
|
|
218
|
+
// `@object-ui/layout` types.
|
|
219
|
+
declare const BrowserRouter: ComponentType<{ children?: ReactNode }>;
|
|
220
|
+
declare const Routes: ComponentType<{ children?: ReactNode }>;
|
|
221
|
+
declare const Route: ComponentType<{ path: string; element: ReactNode }>;
|
|
222
|
+
|
|
223
|
+
// Your own page components, one per route.
|
|
224
|
+
declare const Dashboard: ComponentType;
|
|
225
|
+
declare const UsersPage: ComponentType;
|
|
99
226
|
|
|
100
227
|
function App() {
|
|
101
228
|
return (
|
|
102
229
|
<BrowserRouter>
|
|
103
230
|
<AppShell
|
|
104
|
-
|
|
231
|
+
navbar={<div className="p-4">My App</div>}
|
|
105
232
|
sidebar={
|
|
106
233
|
<SidebarNav
|
|
107
234
|
items={[
|
|
108
|
-
{
|
|
109
|
-
{
|
|
235
|
+
{ title: 'Dashboard', href: '/', icon: Home },
|
|
236
|
+
{ title: 'Users', href: '/users', icon: Users },
|
|
110
237
|
]}
|
|
111
238
|
/>
|
|
112
239
|
}
|
|
113
240
|
>
|
|
114
241
|
<Routes>
|
|
115
242
|
<Route path="/" element={<Dashboard />} />
|
|
116
|
-
<Route path="/users" element={<
|
|
243
|
+
<Route path="/users" element={<UsersPage />} />
|
|
117
244
|
</Routes>
|
|
118
245
|
</AppShell>
|
|
119
246
|
</BrowserRouter>
|
|
@@ -121,17 +248,28 @@ function App() {
|
|
|
121
248
|
}
|
|
122
249
|
```
|
|
123
250
|
|
|
251
|
+
A route's `element` takes **your page component**, never the `lucide-react` icon
|
|
252
|
+
of the same name. This example used to route `/users` to `Users` — the icon it
|
|
253
|
+
imports for the sidebar — so the page rendered a 24-pixel glyph where its content
|
|
254
|
+
belonged. Nothing catches that for you: an icon is a valid component, so the
|
|
255
|
+
route type-checks either way, which is why the two are named apart here.
|
|
256
|
+
|
|
124
257
|
## Customization
|
|
125
258
|
|
|
126
|
-
|
|
259
|
+
`AppShell` takes a single `className`, and it lands on the `<main>` content
|
|
260
|
+
element (`src/AppShell.tsx:256`) rather than on the outer container. There is no
|
|
261
|
+
per-slot `headerClassName` / `sidebarClassName` — the navbar and the sidebar are
|
|
262
|
+
nodes **you** build, so style them where you build them:
|
|
127
263
|
|
|
128
264
|
```typescript
|
|
265
|
+
import { AppShell } from '@object-ui/layout';
|
|
266
|
+
|
|
129
267
|
<AppShell
|
|
130
268
|
className="bg-gray-50"
|
|
131
|
-
|
|
132
|
-
|
|
269
|
+
navbar={<div className="border-b px-4">My App</div>}
|
|
270
|
+
sidebar={<div className="bg-white shadow-lg">Sidebar Content</div>}
|
|
133
271
|
>
|
|
134
|
-
|
|
272
|
+
<div>Main Content</div>
|
|
135
273
|
</AppShell>
|
|
136
274
|
```
|
|
137
275
|
|
|
@@ -1,8 +1,26 @@
|
|
|
1
1
|
import { default as React } from 'react';
|
|
2
2
|
import { AppComponentSchema, NavigationItem } from '@object-ui/types';
|
|
3
|
-
import { VisibilityEvaluator, PermissionChecker, CapabilityChecker } from './NavigationRenderer';
|
|
4
|
-
/**
|
|
5
|
-
|
|
3
|
+
import { VisibilityEvaluator, PermissionChecker, CapabilityChecker, DocTargetChecker } from './NavigationRenderer.js';
|
|
4
|
+
/**
|
|
5
|
+
* Mobile navigation display mode — the two modes this renderer implements.
|
|
6
|
+
*
|
|
7
|
+
* `drawer` is the default (the sidebar rides in the AppShell's mobile sheet
|
|
8
|
+
* overlay); `bottom_nav` additionally renders the fixed bottom bar. Those are
|
|
9
|
+
* the renderer's only two read points for the prop — the default below and the
|
|
10
|
+
* `=== 'bottom_nav'` comparison that gates the bottom bar — so every value that
|
|
11
|
+
* is not `bottom_nav` produces drawer behaviour.
|
|
12
|
+
*
|
|
13
|
+
* A third member (spelled after the collapsed-sidebar menu button) rode in this
|
|
14
|
+
* union with a JSDoc line promising "collapsed sidebar" and NO read point
|
|
15
|
+
* anywhere, which made it behaviourally identical to `drawer` while reading as
|
|
16
|
+
* a capability. Retired in objectui#3985 under ADR-0049 enforce-or-remove
|
|
17
|
+
* (maintainer ruling 2026-08-10): it never was behaviour, so it is gone from
|
|
18
|
+
* this union and from the published vocabulary rather than being documented as
|
|
19
|
+
* a synonym for the default. Wanting a collapsed-sidebar mode is an
|
|
20
|
+
* implementation card first, and the value comes back with a read point — never
|
|
21
|
+
* as a declaration on its own.
|
|
22
|
+
*/
|
|
23
|
+
export type MobileNavMode = 'drawer' | 'bottom_nav';
|
|
6
24
|
export interface AppSchemaRendererProps {
|
|
7
25
|
/** The AppSchema JSON to render */
|
|
8
26
|
schema: AppComponentSchema;
|
|
@@ -16,6 +34,8 @@ export interface AppSchemaRendererProps {
|
|
|
16
34
|
checkPermission?: PermissionChecker;
|
|
17
35
|
/** Optional capability checker passed to NavigationRenderer (gates `requiresObject` / `requiresService`) */
|
|
18
36
|
checkCapability?: CapabilityChecker;
|
|
37
|
+
/** Optional member-readability checker for `doc` entries, passed to NavigationRenderer (objectui#10188) */
|
|
38
|
+
checkDocTarget?: DocTargetChecker;
|
|
19
39
|
/** Called when an action-type navigation item is clicked */
|
|
20
40
|
onAction?: (item: NavigationItem) => void;
|
|
21
41
|
/** Slot: top navbar content (rendered beside the sidebar trigger) */
|
|
@@ -47,13 +67,13 @@ export interface AppSchemaRendererProps {
|
|
|
47
67
|
* Renders a complete application shell from an `AppSchema` JSON document.
|
|
48
68
|
*
|
|
49
69
|
* Responsibilities:
|
|
50
|
-
* - Reads `name`, `title`, `description`, `logo`, `favicon` for branding
|
|
70
|
+
* - Reads `name`, `title`, `description`, `icon`, `branding.logo`, `branding.favicon` for branding
|
|
51
71
|
* - Renders sidebar navigation from `navigation` or `areas[].navigation`
|
|
52
72
|
* - Area switcher when multiple areas are VISIBLE — area visibility is
|
|
53
73
|
* derived from the items inside, not authored (objectui#3311): an area
|
|
54
74
|
* whose items are all gated away is hidden and never auto-activated
|
|
55
|
-
* - Mobile modes: `drawer` (sheet overlay, default)
|
|
56
|
-
* bottom bar)
|
|
75
|
+
* - Mobile modes: `drawer` (sheet overlay, default) and `bottom_nav` (fixed
|
|
76
|
+
* bottom bar) — see `MobileNavMode` for why that list is two long
|
|
57
77
|
* - Evaluates `visible` expressions and `requiredPermissions` on every item
|
|
58
78
|
*
|
|
59
79
|
* @example
|
|
@@ -69,4 +89,4 @@ export interface AppSchemaRendererProps {
|
|
|
69
89
|
* </AppSchemaRenderer>
|
|
70
90
|
* ```
|
|
71
91
|
*/
|
|
72
|
-
export declare function AppSchemaRenderer({ schema, basePath, mobileNavMode, evaluateVisibility: evalVisProp, checkPermission: checkPermProp, checkCapability: checkCapProp, onAction, navbar, sidebarHeader, sidebarFooter, sidebarExtra, children, className, defaultOpen, enableSearch, enablePinning, onPinToggle, enableReorder, onReorder, }: AppSchemaRendererProps): React.JSX.Element;
|
|
92
|
+
export declare function AppSchemaRenderer({ schema, basePath, mobileNavMode, evaluateVisibility: evalVisProp, checkPermission: checkPermProp, checkCapability: checkCapProp, checkDocTarget, onAction, navbar, sidebarHeader, sidebarFooter, sidebarExtra, children, className, defaultOpen, enableSearch, enablePinning, onPinToggle, enableReorder, onReorder, }: AppSchemaRendererProps): React.JSX.Element;
|
package/dist/AppShell.d.ts
CHANGED
|
@@ -10,9 +10,11 @@ export interface AppShellBranding {
|
|
|
10
10
|
accentColor?: string;
|
|
11
11
|
/** Favicon URL — replaces the <link rel="icon"> href */
|
|
12
12
|
favicon?: string;
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Assigned to `document.title` as given. It is the whole title, not a suffix:
|
|
15
|
+
* the caller composes the string it wants (the console passes
|
|
16
|
+
* `"App label — Product name"`).
|
|
17
|
+
*/
|
|
16
18
|
title?: string;
|
|
17
19
|
}
|
|
18
20
|
export interface AppShellProps {
|
|
@@ -34,6 +36,29 @@ export interface AppShellProps {
|
|
|
34
36
|
/**
|
|
35
37
|
* Apply branding CSS custom properties to the document root.
|
|
36
38
|
* This is extracted as a standalone hook so it can be re-used independently.
|
|
39
|
+
*
|
|
40
|
+
* It is also the ONE writer of `document.title` for as long as a shell is
|
|
41
|
+
* mounted (objectui#8637). Ownership is scoped, not permanent: the hook
|
|
42
|
+
* captures whatever the tab already said, writes `title` over it, and puts the
|
|
43
|
+
* captured string back when the shell unmounts or `title` changes. So a host
|
|
44
|
+
* that mounts a shell for part of its route tree gets the specific title while
|
|
45
|
+
* it is there and its previous title back when it leaves, without a second
|
|
46
|
+
* writer keyed on navigation — which is what the console used to do, and what
|
|
47
|
+
* reverted the composed title to the bare product name on every in-app
|
|
48
|
+
* navigation.
|
|
49
|
+
*
|
|
50
|
+
* ⚠️ The capture is a read of the live `document.title`, so a host that lets
|
|
51
|
+
* something else write the tab title WHILE a shell is mounted hands this hook a
|
|
52
|
+
* value it did not put there, and that value is what comes back on unmount.
|
|
53
|
+
* Nesting a second title-writing surface inside a mounted shell is the shape to
|
|
54
|
+
* avoid; the console's own auth surfaces sit outside the shell for this reason.
|
|
55
|
+
*
|
|
56
|
+
* The favicon is scoped the same way (objectui#10040): when `branding.favicon`
|
|
57
|
+
* is set and the page has an icon link, the hook captures that link's `href`
|
|
58
|
+
* attribute, writes the branded URL, and puts the captured attribute back (or
|
|
59
|
+
* removes it, if there was none) when the shell unmounts or the effect re-runs
|
|
60
|
+
* on changed inputs. The same replay caveat applies — an icon written by something else
|
|
61
|
+
* while the shell is mounted is overwritten by the restore.
|
|
37
62
|
*/
|
|
38
63
|
export declare function useAppShellBranding(branding?: AppShellBranding, title?: string): void;
|
|
39
64
|
export declare function AppShell({ sidebar, navbar, children, className, defaultOpen, branding, rightRail, }: AppShellProps): React.JSX.Element;
|
|
@@ -27,6 +27,59 @@ export type PermissionChecker = (permissions: string[]) => boolean;
|
|
|
27
27
|
* runtime capability probe yet.
|
|
28
28
|
*/
|
|
29
29
|
export type CapabilityChecker = (kind: 'object' | 'service', name: string) => boolean;
|
|
30
|
+
/** What a `type: 'doc'` navigation entry opens: a book, a page, or that page in that book. */
|
|
31
|
+
export interface DocNavTarget {
|
|
32
|
+
book?: string;
|
|
33
|
+
doc?: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Answers whether the signed-in member may read what a `type: 'doc'` entry
|
|
37
|
+
* opens (objectui#10188). `false` hides the entry.
|
|
38
|
+
*
|
|
39
|
+
* The server is the enforcer (ADR-0046 §6.7): its app read already drops a
|
|
40
|
+
* `doc` entry the caller may not read (objectstack#19790), and this checker is
|
|
41
|
+
* the renderer's defence in depth behind it (that ruling's point 2). This layer
|
|
42
|
+
* holds no audience rules, so the host answers from the member's own doc / book
|
|
43
|
+
* reads and never re-derives an audience. Kept apart from
|
|
44
|
+
* {@link CapabilityChecker} on purpose: that one asks whether the RUNTIME has a
|
|
45
|
+
* target, this one whether the MEMBER may read it.
|
|
46
|
+
*
|
|
47
|
+
* When not provided, `doc` entries pass — the server's answer stands.
|
|
48
|
+
*/
|
|
49
|
+
export type DocTargetChecker = (target: DocNavTarget) => boolean;
|
|
50
|
+
/**
|
|
51
|
+
* What an UNLABELLED navigation entry inherits its text from (objectui#9868).
|
|
52
|
+
*
|
|
53
|
+
* `@objectstack/spec` 17.5.0 made a nav entry's `label` optional with a
|
|
54
|
+
* declared semantic: absent ⇒ the entry shows, at RENDER time, the CURRENT label
|
|
55
|
+
* of what it opens — the view's label when it names a view and that view is
|
|
56
|
+
* labelled, else the object's / dashboard's label. These are the three targets
|
|
57
|
+
* that sentence names; `resolveNavItemLabel` walks them in that order and asks a
|
|
58
|
+
* {@link NavTargetLabelResolver} about each one.
|
|
59
|
+
*/
|
|
60
|
+
export type NavLabelTarget = {
|
|
61
|
+
kind: 'view';
|
|
62
|
+
objectName: string;
|
|
63
|
+
viewName: string;
|
|
64
|
+
} | {
|
|
65
|
+
kind: 'object';
|
|
66
|
+
objectName: string;
|
|
67
|
+
} | {
|
|
68
|
+
kind: 'dashboard';
|
|
69
|
+
dashboardName: string;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Answers the CURRENT display label of a nav target from the host's metadata —
|
|
73
|
+
* the object schema's `label`, the named view's `label`, the dashboard's
|
|
74
|
+
* `label` — or `undefined` when the target carries none (or is not loaded).
|
|
75
|
+
*
|
|
76
|
+
* This layer holds no metadata, so the host supplies it: the console shell
|
|
77
|
+
* reads the metadata cache it already loads (`useNavTargetLabel` in
|
|
78
|
+
* `@object-ui/app-shell`). Because it is asked on every render and nothing is
|
|
79
|
+
* stored, a renamed target shows its new name on the next render. Without it,
|
|
80
|
+
* an unlabelled entry falls through to its target's machine name.
|
|
81
|
+
*/
|
|
82
|
+
export type NavTargetLabelResolver = (target: NavLabelTarget) => string | undefined;
|
|
30
83
|
export interface NavigationRendererProps {
|
|
31
84
|
/** Navigation items to render */
|
|
32
85
|
items: NavigationItem[];
|
|
@@ -41,6 +94,8 @@ export interface NavigationRendererProps {
|
|
|
41
94
|
checkPermission?: PermissionChecker;
|
|
42
95
|
/** Optional runtime-capability checker for `requiresObject` / `requiresService` */
|
|
43
96
|
checkCapability?: CapabilityChecker;
|
|
97
|
+
/** Optional member-readability checker for `doc` entries — see {@link DocTargetChecker} */
|
|
98
|
+
checkDocTarget?: DocTargetChecker;
|
|
44
99
|
/**
|
|
45
100
|
* Called when an `action`-type item is clicked.
|
|
46
101
|
*
|
|
@@ -72,54 +127,35 @@ export interface NavigationRendererProps {
|
|
|
72
127
|
/** Called when navigation items are reordered via drag */
|
|
73
128
|
onReorder?: (reorderedItems: NavigationItem[]) => void;
|
|
74
129
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* the
|
|
80
|
-
|
|
81
|
-
resolveObjectLabel?: (objectName: string, fallbackLabel: string) => string;
|
|
82
|
-
/**
|
|
83
|
-
* Optional label resolver for dashboard-type navigation items.
|
|
84
|
-
* Called with `(dashboardName, fallbackLabel)` for items where
|
|
85
|
-
* `item.type === 'dashboard'` and `item.label` is a plain string.
|
|
86
|
-
* Mirrors `resolveObjectLabel` for the convention-based i18n hook
|
|
87
|
-
* `useObjectLabel().dashboardLabel`.
|
|
88
|
-
*/
|
|
89
|
-
resolveDashboardLabel?: (dashboardName: string, fallbackLabel: string) => string;
|
|
90
|
-
/**
|
|
91
|
-
* Optional label resolver for object-type navigation items that target a
|
|
92
|
-
* specific view (i.e. `viewName` is set). Called with
|
|
93
|
-
* `(objectName, viewName, fallbackLabel)`. Mirrors
|
|
94
|
-
* `useObjectLabel().viewLabel` and resolves
|
|
95
|
-
* `{ns}.objects.{objectName}._views.{viewName}.label`.
|
|
130
|
+
* The viewer's locale (a BCP-47 tag such as `zh-CN`), for a PRESENT label
|
|
131
|
+
* written as an inline locale map (`{ en: 'Accounts', 'zh-CN': '客户' }`,
|
|
132
|
+
* the spec's `I18nLabel`): the map renders this locale's entry, through the
|
|
133
|
+
* spec's own `resolveI18nLabel`. When the map has no entry for this locale,
|
|
134
|
+
* the fallback order is that resolver's and is not restated here
|
|
135
|
+
* (objectui#11299).
|
|
96
136
|
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
|
|
102
|
-
resolveViewLabel?: (objectName: string, viewName: string, fallbackLabel: string) => string;
|
|
103
|
-
/**
|
|
104
|
-
* Optional label resolver for navigation group items.
|
|
105
|
-
* Called with `(groupId, fallbackLabel)` for items where
|
|
106
|
-
* `item.type === 'group'` and `item.label` is a plain string.
|
|
107
|
-
* Enables convention-based i18n via
|
|
108
|
-
* `{ns}.apps.{appName}.navigation.{groupId}.label`.
|
|
137
|
+
* Injected, like `t`: this layer carries no i18n dependency, so the host that
|
|
138
|
+
* knows the language passes it (the console passes its active UI language,
|
|
139
|
+
* the same value its sidebar resolves area labels in). Omitted ⇒ the
|
|
140
|
+
* resolver's documented no-locale default, `en`. A plain-string label and an
|
|
141
|
+
* absent one are unaffected.
|
|
109
142
|
*/
|
|
110
|
-
|
|
143
|
+
locale?: string;
|
|
111
144
|
/**
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
145
|
+
* Resolver for the text an entry with NO `label` inherits (objectui#9868):
|
|
146
|
+
* the current label of its view / object / dashboard, read from the host's
|
|
147
|
+
* metadata at render time. Consulted ONLY for an absent `label` — an authored
|
|
148
|
+
* label is never replaced by the target's metadata label, not even one
|
|
149
|
+
* spelled like the target's machine name (the ruling refused that sentinel).
|
|
150
|
+
* See {@link NavTargetLabelResolver} and {@link resolveNavItemLabel}.
|
|
117
151
|
*/
|
|
118
|
-
|
|
152
|
+
resolveTargetLabel?: NavTargetLabelResolver;
|
|
119
153
|
/**
|
|
120
|
-
* Optional i18n translation function for resolving
|
|
121
|
-
* (`{ key, defaultValue }
|
|
122
|
-
* through i18next; otherwise falls back to `defaultValue`.
|
|
154
|
+
* Optional i18n translation function for resolving KEYED label objects
|
|
155
|
+
* (`{ key, defaultValue }`, see {@link resolveLabel}). When provided, labels
|
|
156
|
+
* are translated through i18next; otherwise falls back to `defaultValue`.
|
|
157
|
+
* An inline locale map (`{ en, 'zh-CN' }`) is not keyed and never reaches
|
|
158
|
+
* it — {@link resolveNavItemLabel} reads a map itself, in {@link locale}.
|
|
123
159
|
*/
|
|
124
160
|
t?: (key: string, options?: any) => string;
|
|
125
161
|
/**
|
|
@@ -153,6 +189,45 @@ export declare function resolveIcon(name?: string): React.ComponentType<any>;
|
|
|
153
189
|
* wrongly for each other's input, silently (objectui#4167).
|
|
154
190
|
*/
|
|
155
191
|
export declare function resolveLabel(label: string | KeyedI18nLabel, t?: (key: string, options?: any) => string): string;
|
|
192
|
+
/**
|
|
193
|
+
* Resolve a navigation item's display text, in the spec's one order
|
|
194
|
+
* (`@objectstack/spec` `BaseNavItemSchema.label`, objectstack#20849):
|
|
195
|
+
*
|
|
196
|
+
* 1. The id-keyed bundle entry `apps.APP.navigation.ID.label`. That rung runs
|
|
197
|
+
* UPSTREAM of this function, at the server-side `/meta` boundary:
|
|
198
|
+
* `translateApp` in `@objectstack/spec` (`src/system/i18n-resolver.ts`)
|
|
199
|
+
* rewrites a node's `label` by its `id` before the metadata reaches this
|
|
200
|
+
* renderer. App-navigation localization has that one owner — localize nav
|
|
201
|
+
* labels there, never here.
|
|
202
|
+
* 2. A PRESENT label, as authored ({@link presentNavItemLabel}): an inline
|
|
203
|
+
* locale map reads the entry for `locale` (the viewer's), and a string
|
|
204
|
+
* renders verbatim.
|
|
205
|
+
* 3. An ABSENT label inherits its target's CURRENT label (objectui#9868 —
|
|
206
|
+
* `@objectstack/spec` 17.5.0 made it optional, cloud#2021 letter-A ruling),
|
|
207
|
+
* resolved here at render time and never written back — see
|
|
208
|
+
* {@link inheritedNavItemLabel} for the ladder. That arm is keyed on
|
|
209
|
+
* ABSENCE alone, so an authored label is never swapped for the target's
|
|
210
|
+
* metadata label, however it is spelled.
|
|
211
|
+
*
|
|
212
|
+
* ⛔ Nothing here matches a label's text against a name (objectui#11201,
|
|
213
|
+
* ruling B). A present label equal to its target's machine name (`account`)
|
|
214
|
+
* renders `account` in every locale: text cannot tell a deliberate `account`
|
|
215
|
+
* from a machine-written one, so translation is keyed on identity (rung 1) or
|
|
216
|
+
* comes through inheritance (rung 3). The three convention resolvers that
|
|
217
|
+
* the retired rule consulted, and the three arguments that carried them, are
|
|
218
|
+
* gone (objectui#11299).
|
|
219
|
+
*
|
|
220
|
+
* EXPORTED since `969ba84f4`, for the same reason {@link resolveHref} is: a
|
|
221
|
+
* second surface now renders the same `NavigationItem[]`. `nav:menu` is the
|
|
222
|
+
* app's navigation tree as PAGE CONTENT (`app-shell/src/views/nav-menu-renderer.tsx`),
|
|
223
|
+
* and it cannot mount `NavigationRenderer` itself — that renders through
|
|
224
|
+
* `SidebarMenuButton`, whose `useSidebar()` throws outside the shell's
|
|
225
|
+
* `SidebarProvider`. Re-deriving the label rules there would put one nav entry
|
|
226
|
+
* under two names on two surfaces of one app, which is exactly the drift the
|
|
227
|
+
* "single source of truth" note on `resolveHref` exists to prevent. Nothing
|
|
228
|
+
* about the behaviour changed with the keyword.
|
|
229
|
+
*/
|
|
230
|
+
export declare function resolveNavItemLabel(item: NavigationItem, t?: (key: string, options?: any) => string, targetLabel?: NavTargetLabelResolver, locale?: string): string;
|
|
156
231
|
/** Guard callbacks for {@link hasVisibleNavigationItems}. */
|
|
157
232
|
export interface NavigationVisibilityOptions {
|
|
158
233
|
/** Evaluator for item `visible` expressions. Defaults to always-visible. */
|
|
@@ -161,6 +236,8 @@ export interface NavigationVisibilityOptions {
|
|
|
161
236
|
checkPermission?: PermissionChecker;
|
|
162
237
|
/** Checker for `requiresObject` / `requiresService`. Defaults to pass. */
|
|
163
238
|
checkCapability?: CapabilityChecker;
|
|
239
|
+
/** Checker for what a `doc` entry opens ({@link DocTargetChecker}). Defaults to pass. */
|
|
240
|
+
checkDocTarget?: DocTargetChecker;
|
|
164
241
|
/**
|
|
165
242
|
* Whether the host wires an `onAction` dispatcher. Without one, `action`
|
|
166
243
|
* items are not rendered at all (framework#4509 — a nav entry that looks
|
|
@@ -213,6 +290,25 @@ export interface NavTemplateContext {
|
|
|
213
290
|
*/
|
|
214
291
|
contextValues?: Record<string, string | null | undefined>;
|
|
215
292
|
}
|
|
293
|
+
/**
|
|
294
|
+
* The wire encoding of the declared `runAction` nav slot — ONE definition,
|
|
295
|
+
* sited with `resolveHref` because that is the only thing that writes it.
|
|
296
|
+
*
|
|
297
|
+
* `ObjectNavItemSchema.runAction` (objectstack#7253) declares WHICH action an
|
|
298
|
+
* object nav entry auto-runs on arrival; the URL is how that declaration
|
|
299
|
+
* survives the navigation, because the list surface mounts in a fresh render
|
|
300
|
+
* tree with nothing but the location to read. Naming the param after the slot
|
|
301
|
+
* is deliberate: the slot owns the name, so producer and consumer cannot drift.
|
|
302
|
+
*
|
|
303
|
+
* Before this constant existed, the name was a bare `'runAction'` literal
|
|
304
|
+
* hand-written at both ends — a private convention no schema declared, so
|
|
305
|
+
* `objectui validate` could not see it, `RESERVED_URL_PARAMS` did not list it,
|
|
306
|
+
* and a page could have repurposed the name with nothing to catch the
|
|
307
|
+
* collision. Import this; never re-spell it. `@object-ui/app-shell`'s
|
|
308
|
+
* `urlParams` re-exports it into the reserved registry rather than restating
|
|
309
|
+
* it, so there is still exactly one definition.
|
|
310
|
+
*/
|
|
311
|
+
export declare const NAV_RUN_ACTION_PARAM = "runAction";
|
|
216
312
|
/**
|
|
217
313
|
* Resolve a NavigationItem to an absolute href (relative to `basePath`).
|
|
218
314
|
*
|
|
@@ -237,13 +333,19 @@ export declare function resolveActiveNavItem(items: NavigationItem[], pathname:
|
|
|
237
333
|
/**
|
|
238
334
|
* Recursively filter navigation items by search query (case-insensitive label match).
|
|
239
335
|
* Groups are kept if any child matches, with non-matching children pruned.
|
|
336
|
+
*
|
|
337
|
+
* Matches the text the row SHOWS: `labelOf` defaults to
|
|
338
|
+
* {@link resolveNavItemLabel} with no resolvers, and `NavigationRenderer` passes
|
|
339
|
+
* its own resolvers and locale so an unlabelled entry is found by the label it
|
|
340
|
+
* inherits (objectui#9868), not by a `label` it does not have, and a map-valued
|
|
341
|
+
* one by its entry in the viewer's locale (objectui#11299).
|
|
240
342
|
*/
|
|
241
|
-
export declare function filterNavigationItems(items: NavigationItem[], query: string): NavigationItem[];
|
|
343
|
+
export declare function filterNavigationItems(items: NavigationItem[], query: string, labelOf?: (item: NavigationItem) => string): NavigationItem[];
|
|
242
344
|
/**
|
|
243
345
|
* Renders a `NavigationItem[]` tree into Shadcn Sidebar components.
|
|
244
346
|
*
|
|
245
347
|
* Features:
|
|
246
|
-
* -
|
|
348
|
+
* - Every navigation item type + separators
|
|
247
349
|
* - Nested collapsible groups
|
|
248
350
|
* - Badge indicators
|
|
249
351
|
* - Visibility expression evaluation
|
|
@@ -268,4 +370,4 @@ export declare function filterNavigationItems(items: NavigationItem[], query: st
|
|
|
268
370
|
* />
|
|
269
371
|
* ```
|
|
270
372
|
*/
|
|
271
|
-
export declare function NavigationRenderer({ items, basePath, evaluateVisibility: evalVis, checkPermission: checkPerm, checkCapability: checkCap, onAction, searchQuery, enablePinning, onPinToggle, enableReorder, onReorder,
|
|
373
|
+
export declare function NavigationRenderer({ items, basePath, evaluateVisibility: evalVis, checkPermission: checkPerm, checkCapability: checkCap, checkDocTarget, onAction, searchQuery, enablePinning, onPinToggle, enableReorder, onReorder, resolveTargetLabel, locale, t: tProp, templateContext, }: NavigationRendererProps): React.JSX.Element;
|