@object-ui/layout 17.5.0 → 17.6.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 CHANGED
@@ -1,5 +1,264 @@
1
1
  # @object-ui/layout
2
2
 
3
+ ## 17.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 88085e3: Consume the declared nav `runAction` slot; retire the private `?runAction=` string convention
8
+
9
+ An `object` navigation item can now declare `runAction: '<actionName>'` and the shell will run that action once on arrival at the object's list surface, through the ordinary execute path — so param dialogs, confirms and entitlement gates all still apply. The slot is `@objectstack/spec`'s `ObjectNavItemSchema.runAction`; objectui now reads it instead of a private convention.
10
+
11
+ - `NavigationItem` declares `runAction` (derived from the spec's object-nav variant), and objectui's own nav schema stops stripping it — `objectui validate` previously discarded the key, so an entry carrying a deep link validated clean with the deep link thrown away.
12
+ - `resolveHref` encodes it onto the list href as the reserved `?runAction=` param, on the list landings only. A `recordId` entry resolves to a record page, which has no list toolbar to answer it, so the slot is not encoded there.
13
+ - The deep link is honoured on **every** object list, not just the environments list. The action is armed only when it is actually present at `list_toolbar`; a name no action answers to runs nothing and deliberately leaves the URL untouched, so a later mount with fresher metadata can still honour it. Undefined references are rejected upstream at authoring time by `defineStack`.
14
+ - The param name now has exactly one definition (`NAV_RUN_ACTION_PARAM`, exported from `@object-ui/layout`) and is registered in the console's reserved-param collision check. It was previously a bare literal hand-written at both the producing and consuming ends, declared by no schema and listed in no registry.
15
+ - `CloudOnboardingNext` takes an optional `properties.createAction` (defaulting to `create_environment`) instead of hand-concatenating its deep link.
16
+ - 86f633f: Remove the published optional key `logo` from `AppShellBranding` (`@object-ui/layout`).
17
+
18
+ The key was declared but never read. `useAppShellBranding` applies only
19
+ `primaryColor`, `accentColor`, `favicon` and `title`, and `AppShell` installs no
20
+ context provider at all — so its doc comment, "Logo URL — passed to sidebar/navbar
21
+ via context", described a mechanism that did not exist. Three call sites were
22
+ feeding the key a real value that was silently discarded, and all three are removed
23
+ with it: `AppSchemaRenderer`, `ConsoleLayout` and the console's `useBranding` hook.
24
+
25
+ The real logo entry point is unchanged and is where it always was: the app schema's
26
+ own `branding.logo`, read directly by `AppSidebar` in `@object-ui/app-shell`, plus
27
+ the app schema's top-level `logo`, rendered directly by `AppSchemaRenderer`'s
28
+ default sidebar header. Neither path went through `AppShellBranding`, so nothing
29
+ rendering-visible changes.
30
+
31
+ Migration: a consumer that passes `logo` inside an `AppShellBranding` object literal
32
+ now gets a compile error. Delete the key — it never reached a renderer. To show a
33
+ logo, set it on the app schema's `branding.logo` instead.
34
+ - 29167d5: **Breaking (shipped as `minor`, following the `page-header` `description` retirement):**
35
+ `app-shell` is no longer a component key. `registerLayout()` registered `AppShell` under
36
+ that key with no `inputs`; the registration is gone (objectui#4841, ADR-0049
37
+ enforce-or-remove, remove side, maintainer ruling 2026-08-16).
38
+
39
+ **What it could never do.** Four of `AppShellProps`' seven keys are `React.ReactNode`
40
+ slots — `sidebar`, `navbar`, `children`, `rightRail` — and a JSON document can fill none
41
+ of them, so `{ "type": "app-shell" }` resolved to a component that had exactly two
42
+ outcomes, neither of them a shell. `children` was dropped in silence: `SchemaRenderer`
43
+ strips `children` (and `body`) before spreading a node's keys as props, and `AppShell`
44
+ reads its `children` prop, never `schema.children`, so the `<main>` element rendered
45
+ empty with nothing logged. A schema written into `sidebar` / `navbar` / `rightRail`
46
+ arrived as a plain object React refuses to render, replacing the node with an error box.
47
+ Only `className`, `defaultOpen` and `branding` ever survived the JSON path, i.e. the best
48
+ result JSON could reach was a shell with no navigation, no top bar and an empty content
49
+ area. With no `inputs` declared, `sdui-parser` had no declaration face to compare a node
50
+ against either, so neither outcome was diagnosed.
51
+
52
+ **What changes for an author.** The middle state — parses, resolves, renders nothing — is
53
+ replaced by a named refusal. `SchemaRenderer` now shows its `Unknown component type:
54
+ app-shell` panel (`OBJUI-001`) and `sdui-parser` reports an `error`-severity
55
+ `unknown-component` diagnostic before render.
56
+
57
+ **FROM → TO.** There is no in-place rewrite, because the node never produced a shell.
58
+ Schema authors who want the whole shell from metadata use `app-schema-renderer`
59
+ (`AppSchemaRenderer`), which declares its `inputs` and builds branding and sidebar
60
+ navigation from an `AppSchema` document: `{ "type": "app-shell", … }` →
61
+ `{ "type": "app-schema-renderer", "schema": { … } }`. Everyone else composes in React —
62
+ `AppShell` is **unchanged and still exported** from `@object-ui/layout`, and remains the
63
+ way to build a shell and render JSON pages inside it.
64
+
65
+ Repo-wide scan before removal found no `"type": "app-shell"` node anywhere in
66
+ `objectstack-ai/objectui` or `objectstack-ai/objectstack` at `origin/main` — no example,
67
+ catalog schema, fixture or template authored one. `content/docs/guide/layout.md` is
68
+ updated to state the new fact, and
69
+ `packages/layout/src/__tests__/app-shell-not-a-component-key.test.tsx` pins it on both
70
+ faces (source and live registry) plus the rendered diagnostic.
71
+ - 45f7dcc: **Breaking (shipped as `minor`, following the `app-shell` component-key deregistration):**
72
+ `app-schema-renderer`'s `mobileNavMode` is now declared as the vocabulary the renderer
73
+ implements — `enum: ['drawer', 'bottom_nav']` — instead of free-text `string`, and the
74
+ third member of `MobileNavMode` is retired (objectui#3985, ADR-0049 enforce-or-remove,
75
+ maintainer ruling 2026-08-10).
76
+
77
+ **What used to happen.** The registration declared `{ name: 'mobileNavMode', type:
78
+ 'string' }`, so `sdui-parser`'s `checkType` asked only whether the value was a string.
79
+ `mobileNavMode="bottom-nav"` — the hyphenated spelling of the underscored value, and the
80
+ likeliest typo on this key — passed the manifest gate, passed the parser, reached the
81
+ renderer, missed its one equality test, and rendered the drawer. Nothing reported
82
+ anything, at any layer. The declaration was WIDER than the implementation, which is why
83
+ the `invalid-enum` that should have fired never could.
84
+
85
+ **What happens now.** A value outside the vocabulary is an **error**-level `invalid-enum`
86
+ from `validateTree` / `compile`, so a schema-driven author — very often an AI author — is
87
+ stopped at the typo instead of debugging a mode that silently did nothing. The generated
88
+ `sdui-intrinsics.d.ts` narrows from `mobileNavMode?: string` to the two-value union for
89
+ the same reason.
90
+
91
+ **Retirement.** `MobileNavMode` had a third member documented as "collapsed sidebar" with
92
+ **zero read points** in `AppSchemaRenderer`: the only two reads are the `drawer` default
93
+ and the `=== 'bottom_nav'` comparison that gates the bottom bar, so the value was
94
+ behaviourally identical to the default while the union, the JSDoc and `ROADMAP.md`
95
+ presented it as a capability. It is gone from the union and from the published
96
+ vocabulary. Making it real behaviour is an implementation card first — the value comes
97
+ back together with a renderer read point, never as a declaration on its own.
98
+
99
+ **Migration.** Both implemented modes are unchanged; no runtime behaviour moves. A
100
+ TypeScript caller passing the retired literal now fails to compile (it previously
101
+ compiled and rendered the drawer), and a JSON/JSX author writing it — or any other
102
+ out-of-vocabulary value — now gets a validation error where they previously got silence.
103
+ In both cases the value that reproduces the old behaviour exactly is `drawer`.
104
+ - f923b7c: **Breaking (shipped as `minor`, see below):** `@object-ui/layout`'s `<PageHeader>` retires the legacy `description` prop. `subtitle` is now the only spelling for the secondary line.
105
+
106
+ `PageHeader` read `subtitle ?? description`. The alias was not tolerance for a sloppy author — it was the only thing standing between an externally authored `description` and a page that renders its title and silently drops its second line, which is why objectui#3226 refused to delete it: "no in-repo author writes `description`" (true, verified) says nothing about the stored metadata of out-of-repo consumers.
107
+
108
+ **FROM → TO.** JSX call sites: `<PageHeader title="X" description="Y" />` → `<PageHeader title="X" subtitle="Y" />`. Schema authors: `{ "type": "page-header", "description": "Y" }` → `{ "type": "page-header", "subtitle": "Y" }`. Stored stack metadata needs no hand edit — `os migrate meta` rewrites it, and a protocol 17 loader rewrites it on the way in.
109
+
110
+ **Why the read could go now.** The normalization moved upstream, where a contract belongs: protocol 17's ADR-0087 D2 conversion `page-header-subtitle-alias` rewrites a header node's `properties.description` to `properties.subtitle` as the stack loads. The gate on this side was a measurement, not a date — every position a `page-header` node can occupy had to be shown to pass through that rewrite. It did not, at first: measured 2026-08-08 against `@objectstack/spec@17.0.0-rc.5`, the conversion walk reached `pages[].regions[].components[]` and stopped, leaving seven spec-valid header positions unconverted — including `slots.header` on a `kind: 'slotted'` record page, the shape this repo's own slotted-pages guide prescribes (objectstack#6775). With the walk widened by objectstack#6776 (slots) and objectstack PR #7034 (containers nested to any depth: `properties.children`, `items[].children`, `body`, `footer`), all seven convert. Re-measured against `@objectstack/spec@17.0.0-rc.6`, which this package depends on, before the read was deleted.
111
+
112
+ The measurement is now a standing pin rather than a one-off: `packages/layout/src/__tests__/page-header-subtitle-conversion-coverage.test.ts` re-runs all seven positions against the resolved spec build on every test run. If a future spec narrows that reach — or a dependency is pinned back below `17.0.0-rc.6` — it fails there, at the seam, instead of turning into a second line that quietly stops rendering in a consumer's app. The two tests in `page-header-authorable-keys.test.tsx` that pinned the fallback's continued existence were deleted in this same change, as their own SEQUENCING note instructed, and replaced with the opposite assertion: a lone `description` now renders no secondary line.
113
+
114
+ **Not affected, despite the shared word:** the `page` renderer's own `description` prop (the page's prose under the page title) is a live declared key of a different node, untouched here. `@object-ui/app-shell`'s separate `<PageHeader>` uses `description` as its only secondary-line prop and is likewise unchanged.
115
+
116
+ `minor` rather than `major` is deliberate and follows the repo's retirement precedent (PR #3793): all 39 publishable packages sit in one `fixed` group, so a `major` here would carry the whole family to 18.0.0 against an `@objectstack` still on 17 and break the "same major ⇒ compatible" pin between the two repos (AGENTS.md §版本号策略, enforced by `scripts/check-changeset-no-major.mjs`). objectui's own breaking changes ship as `minor` with the break spelled out in the body — which is what the FROM → TO above is.
117
+ - 9c60144: **Breaking (published API):** `NavigationRenderer` no longer accepts `resolveGroupLabel` or `resolveItemLabel`. If your build just broke on one of these props, delete the prop — it never did anything.
118
+
119
+ Both were id-keyed label resolvers on `NavigationRendererProps` (`@object-ui/layout`), typically wired to `useObjectLabel().navGroupLabel` from `@object-ui/i18n` to translate sidebar group and leaf labels from a client translation pack keyed `{ns}.apps.{appName}.navigation.{nodeId}.label`.
120
+
121
+ **They could never fire.** The renderer guards convention-based resolution with `isCustomized` — a case-insensitive "the authored label differs from this branch's comparison target" test. On the `object` / `dashboard` branches the target is the object or dashboard name, and the test is meaningful: it protects an author's custom label (`Projects`) from being overwritten by an `objects.project.label` translation. On the two id-keyed branches the target was the nav node's own **`id`** — `grp_workspace`, `group_sales` — while the label was its text — `Workspace`, `Sales`. Those never compare equal, so the guard was true for every real navigation entry and the resolver beneath it was unreachable. The only node that could have reached it is one whose label is literally its own id.
122
+
123
+ Nothing regresses when they go, because nothing was using them to begin with: **app-navigation localization is owned solely by the server-side `/meta` boundary.** `translateApp` (`@objectstack/spec`, `src/system/i18n-resolver.ts`) rewrites every navigation node's `label` by id, and `@objectstack/rest` applies it before the metadata reaches the client — so nav labels arrive already localized and the client-side path was never the one answering. One owner, not two.
124
+
125
+ **If you wired these hooks to localize navigation, your labels were never being localized by them.** Move the translation to the server side: add it to the app's i18n bundle that `translateApp` reads, keyed by navigation node id. A client-side pack keyed `apps.*.navigation.*.label` changes nothing in the sidebar.
126
+
127
+ Also in this change:
128
+
129
+ - `useObjectLabel().navGroupLabel` (`@object-ui/i18n`) is **kept**, but its docstring no longer promises `"Sales" → "销售"` for sidebar groups — that promise was false, and a live docstring describing an unreachable path is how an agent or a developer ends up wiring a translation pack and receiving silent non-localization. It is now documented for what it is: a plain reader for `{ns}.apps.{appName}.navigation.{groupId}.label` with no first-party caller, pointing at the server boundary for anything nav-related.
130
+ - `UnifiedSidebar` (`@object-ui/app-shell`) drops the two prop wirings, including a `studio` carve-out that existed only to stop `resolveItemLabel` from re-translating an already-translated "Package management" label.
131
+ - The `object` / `dashboard` / `viewName` branches — `resolveObjectLabel`, `resolveDashboardLabel`, `resolveViewLabel` — are untouched and keep both their resolvers and the `isCustomized` guard.
132
+
133
+ ### Patch Changes
134
+
135
+ - d442795: AppShell: drop the unused `Sidebar` import. `AppShell` renders the node the caller passes
136
+ in the `sidebar` prop and never constructs a `Sidebar` itself, so the import was dead
137
+ (tree-shaken out of every bundle) and only suggested otherwise to readers. No runtime
138
+ behaviour changes.
139
+ - 8a9dece: `SidebarNav`'s README example teaches the shape the component actually reads.
140
+
141
+ `packages/layout/README.md` — the package's npm landing page, shipped in `files` —
142
+ spelled every nav item `{ label, path, icon: 'home' }`. `NavItem` declares none of
143
+ those three keys: the label is `title`, the target is `href`, and `icon` is a
144
+ `React.ComponentType` rendered as `<item.icon />`, not an icon name. Copied
145
+ verbatim the example produced a sidebar whose every row was unlabelled
146
+ (`<span>{item.title}</span>` reading `undefined`), a `NavLink` with
147
+ `to={undefined}`, and the string `'home'` handed to React as an unknown lowercase
148
+ tag. Both README examples now use `title` / `href` / imported Lucide components,
149
+ and annotate the array as `NavItem[]` so the same class of typo becomes a compile
150
+ error where it is written instead of a blank sidebar at runtime.
151
+
152
+ Adds the props tables the README never carried — `SidebarNavProps`, `NavItem`
153
+ (`badge`, `badgeVariant` and `children` included) and `NavGroup`, plus a grouped
154
+ and nested example — and pins all of it rather than leaving a second surface free
155
+ to rot the same way: the examples are real, type-checked code in
156
+ `readme-sidebar-nav-example.test.ts` asserted to appear verbatim in the README, and
157
+ the tables are compared against the interface keys read out of `SidebarNav.tsx` on
158
+ every run. Also corrects a comment in `side-effects-manifest.test.ts` that named
159
+ `SidebarNav` as the component behind the `navigation-renderer` registration; that
160
+ key belongs to a different component, and `SidebarNav` is registered under no key
161
+ at all.
162
+
163
+ No runtime change — `SidebarNav.tsx` is untouched. This is `patch` rather than a
164
+ no-release declaration because the corrected landing page only reaches npm through
165
+ a publish.
166
+ - 183d09b: Studio 页面设计器不再为 canonical `page:header` 提供 `icon` 编辑框(objectui#3829)
167
+
168
+ `PageHeaderProps.icon` 已在 `@objectstack/spec` 17.0.0 随 ADR-0087 D2 退役
169
+ (objectstack#6946 / PR objectstack#7115):canonical `page:header` 渲染器从未读过它,
170
+ 表头的身份由 record chrome(`recordChrome`)与每个 action 自带的 `icon` 承担。退役前
171
+ 作者填入的值被静默丢弃,退役后平台按名拒绝整个节点 —— 而设计器仍在提供那个输入框,
172
+ 等于教作者写出解析失败的元数据。本次移除该字段与它此时已成孤儿的两条 i18n 键
173
+ (en / zh 同一次改动,两张表的键集保持一致),并把「不得回潮」钉在
174
+ `previews/__tests__/block-config.test.ts`。
175
+
176
+ `@object-ui/layout` 的 `page-header` / `layout:page-header` 别名**保留** `icon` 输入,
177
+ 行为不变:那是另一个渲染器,它真读真画(`PageHeader.tsx`),文档与本仓唯一的活 demo
178
+ 都写它。变的只是这条声明的**依据** —— 从 spec parity 改述为 renderer-read 事实,并让
179
+ `page-header-authorable-keys` 守卫在派生 spec 键集时跳过墓碑成员,不再因为
180
+ `Object.keys(shape)` 仍列着已退役的 `icon` 而假绿。
181
+ - Updated dependencies [88085e3]
182
+ - Updated dependencies [516663d]
183
+ - Updated dependencies [460c4d0]
184
+ - Updated dependencies [0ae27f7]
185
+ - Updated dependencies [2533ec5]
186
+ - Updated dependencies [78c0f9a]
187
+ - Updated dependencies [bbe8b86]
188
+ - Updated dependencies [8477be5]
189
+ - Updated dependencies [279fb13]
190
+ - Updated dependencies [2e82ab2]
191
+ - Updated dependencies [ad07b65]
192
+ - Updated dependencies [41f498b]
193
+ - Updated dependencies [e1d4251]
194
+ - Updated dependencies [40d3a33]
195
+ - Updated dependencies [8b9dc62]
196
+ - Updated dependencies [1184192]
197
+ - Updated dependencies [a2a9747]
198
+ - Updated dependencies [a1609a6]
199
+ - Updated dependencies [53f23bc]
200
+ - Updated dependencies [c4533dc]
201
+ - Updated dependencies [be60815]
202
+ - Updated dependencies [37f6844]
203
+ - Updated dependencies [93de4f6]
204
+ - Updated dependencies [2b50261]
205
+ - Updated dependencies [384f30d]
206
+ - Updated dependencies [ac600e5]
207
+ - Updated dependencies [97fba31]
208
+ - Updated dependencies [232f61a]
209
+ - Updated dependencies [d374caf]
210
+ - Updated dependencies [5673576]
211
+ - Updated dependencies [c1ef923]
212
+ - Updated dependencies [911ceaa]
213
+ - Updated dependencies [98eab36]
214
+ - Updated dependencies [af5e292]
215
+ - Updated dependencies [3fbbea1]
216
+ - Updated dependencies [7f96b10]
217
+ - Updated dependencies [167ec42]
218
+ - Updated dependencies [616a2a5]
219
+ - Updated dependencies [0046d8f]
220
+ - Updated dependencies [f1d4748]
221
+ - Updated dependencies [b1119ec]
222
+ - Updated dependencies [9f23d2b]
223
+ - Updated dependencies [578e025]
224
+ - Updated dependencies [af025ee]
225
+ - Updated dependencies [598c89a]
226
+ - Updated dependencies [4a0bd17]
227
+ - Updated dependencies [b8b9af4]
228
+ - Updated dependencies [31676be]
229
+ - Updated dependencies [8c0d52e]
230
+ - Updated dependencies [aff10e2]
231
+ - Updated dependencies [70a774b]
232
+ - Updated dependencies [9ce096f]
233
+ - Updated dependencies [e05db88]
234
+ - Updated dependencies [7458a41]
235
+ - Updated dependencies [5ffcc14]
236
+ - Updated dependencies [d971e51]
237
+ - Updated dependencies [97abb24]
238
+ - Updated dependencies [deb157a]
239
+ - Updated dependencies [d2ce342]
240
+ - Updated dependencies [9695da7]
241
+ - Updated dependencies [75444e3]
242
+ - Updated dependencies [58b8346]
243
+ - Updated dependencies [2d0bd16]
244
+ - Updated dependencies [dad51e5]
245
+ - Updated dependencies [1c9c342]
246
+ - Updated dependencies [787c738]
247
+ - Updated dependencies [8396656]
248
+ - Updated dependencies [dbbd38a]
249
+ - Updated dependencies [93fe362]
250
+ - Updated dependencies [dfc6975]
251
+ - Updated dependencies [3cf4de0]
252
+ - Updated dependencies [c9dc811]
253
+ - Updated dependencies [144ef9b]
254
+ - Updated dependencies [138ab04]
255
+ - Updated dependencies [a0b9e91]
256
+ - Updated dependencies [99bd015]
257
+ - @object-ui/types@17.6.0
258
+ - @object-ui/react@17.6.0
259
+ - @object-ui/components@17.6.0
260
+ - @object-ui/core@17.6.0
261
+
3
262
  ## 17.5.0
4
263
 
5
264
  ### Minor Changes
package/README.md CHANGED
@@ -24,9 +24,9 @@ pnpm add @object-ui/layout
24
24
  ## Registration
25
25
 
26
26
  Importing this package registers its component keys (`page-header`, `page:card`,
27
- `app-shell`, `responsive-grid`, `navigation-renderer`, `app-schema-renderer`) on
28
- the `ComponentRegistry` as a module load side effect, so the side-effect-only
29
- import is enough:
27
+ `responsive-grid`, `navigation-renderer`, `app-schema-renderer`) on the
28
+ `ComponentRegistry` as a module load side effect, so the side-effect-only import
29
+ is enough:
30
30
 
31
31
  ```typescript
32
32
  import '@object-ui/layout';
@@ -43,30 +43,61 @@ prefer to register explicitly.
43
43
 
44
44
  ### AppShell
45
45
 
46
- Complete application shell with header, sidebar, and main content area.
46
+ Complete application shell with a top navbar, a sidebar, and a main content area.
47
47
 
48
48
  ```typescript
49
49
  import { AppShell } from '@object-ui/layout';
50
50
 
51
51
  <AppShell
52
- header={<div>Header Content</div>}
52
+ navbar={<div>Navbar Content</div>}
53
53
  sidebar={<div>Sidebar Content</div>}
54
54
  >
55
55
  <div>Main Content</div>
56
56
  </AppShell>
57
57
  ```
58
58
 
59
+ The top bar's content goes in `navbar`. `AppShell` renders the `<header>`
60
+ element itself (`src/AppShell.tsx:248`) and `{navbar}` is the only thing that
61
+ fills it (`:249`) — there is no `header` prop. This example used to pass one
62
+ (objectui#4817), and because the component destructures a fixed key list with
63
+ **no rest element** (`:233-241`), the node was built and then dropped on the
64
+ floor: copied verbatim, the snippet rendered an empty top bar and said nothing.
65
+
66
+ Like `SidebarNav` below, `AppShell` is composed in JSX and is **not** on the
67
+ `ComponentRegistry`: four of its seven props are `React.ReactNode` slots that no
68
+ JSON document can fill, so the `app-shell` key was retired in objectui#4841 and
69
+ `{ "type": "app-shell" }` now reports `Unknown component type`. When the whole
70
+ shell has to come from metadata, the key for that is `app-schema-renderer`.
71
+
72
+ #### `AppShellProps`
73
+
74
+ | Prop | Type | Default | Description |
75
+ | --- | --- | --- | --- |
76
+ | `children` | `React.ReactNode` | — (required) | Main content, rendered inside the `<main>` element. |
77
+ | `navbar` | `React.ReactNode` | — | Top bar content. `AppShell` supplies the sticky `<header>` around it, so pass only what goes inside. |
78
+ | `sidebar` | `React.ReactNode` | — | Left sidebar node, rendered as a flex sibling of the content. Pass `SidebarNav`, or your own node. |
79
+ | `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. |
80
+ | `className` | `string` | — | Tailwind overrides for the `<main>` content element — **not** for the outer container. |
81
+ | `defaultOpen` | `boolean` | `true` | Initial open state of the underlying Shadcn `SidebarProvider`. |
82
+ | `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`. |
83
+
59
84
  ### PageHeader
60
85
 
61
- Page title block with an optional description, used at the top of a page's
86
+ Page title block with an optional subtitle, used at the top of a page's
62
87
  content area.
63
88
 
64
89
  ```typescript
65
90
  import { PageHeader } from '@object-ui/layout';
66
91
 
67
- <PageHeader title="Dashboard" description="View your metrics" />
92
+ <PageHeader title="Dashboard" subtitle="View your metrics" />
68
93
  ```
69
94
 
95
+ `subtitle` is the only spelling for the secondary line — it is the key
96
+ `@objectstack/spec/ui`'s `PageHeaderProps` declares. The legacy `description`
97
+ alias this component used to read as well was retired in objectui#3789; stored
98
+ metadata still carrying it is rewritten to `subtitle` at load time by the
99
+ ADR-0087 D2 conversion `page-header-subtitle-alias`.
100
+
70
101
  > **Rendering a whole `page` node?** That belongs to `PageRenderer` in
71
102
  > `@object-ui/components`, which is what the `page` component key resolves to —
72
103
  > it handles page types (record/home/app/utility), named regions and page
@@ -78,17 +109,91 @@ import { PageHeader } from '@object-ui/layout';
78
109
  Navigation sidebar component with React Router integration.
79
110
 
80
111
  ```typescript
81
- import { SidebarNav } from '@object-ui/layout';
112
+ import { SidebarNav, type NavItem } from '@object-ui/layout';
113
+ import { Home, Settings, Users } from 'lucide-react';
82
114
 
83
- const navItems = [
84
- { label: 'Dashboard', path: '/dashboard', icon: 'home' },
85
- { label: 'Users', path: '/users', icon: 'users' },
86
- { label: 'Settings', path: '/settings', icon: 'settings' }
115
+ const navItems: NavItem[] = [
116
+ { title: 'Dashboard', href: '/dashboard', icon: Home },
117
+ { title: 'Users', href: '/users', icon: Users },
118
+ { title: 'Settings', href: '/settings', icon: Settings },
87
119
  ];
88
120
 
89
121
  <SidebarNav items={navItems} />
90
122
  ```
91
123
 
124
+ An item's label is `title` and its target is `href` — and `icon` is a **component**,
125
+ not an icon name: it is rendered as `<item.icon />` (`src/SidebarNav.tsx:60`, `:109`),
126
+ so pass the imported Lucide component itself. This example used to be written with
127
+ `label` / `path` / `icon: 'home'`, none of which `NavItem` declares (objectui#3999);
128
+ copied as-is it produced rows with no label at all, a `NavLink` whose `to` was
129
+ `undefined`, and the string `'home'` handed to React as an unknown lowercase tag.
130
+ Annotating the array as `NavItem[]` is what turns that whole class of typo back into
131
+ a compile error where it is written, instead of a blank sidebar at runtime.
132
+
133
+ #### `SidebarNavProps`
134
+
135
+ | Prop | Type | Default | Description |
136
+ | --- | --- | --- | --- |
137
+ | `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. |
138
+ | `title` | `string` | `'Application'` | Section label shown above a flat `NavItem[]`. Ignored when `items` is a `NavGroup[]` — each group prints its own `label`. |
139
+ | `className` | `string` | — | Tailwind overrides, forwarded to the root `Sidebar`. |
140
+ | `collapsible` | `'offcanvas' \| 'icon' \| 'none'` | `'icon'` | Collapse behaviour of the underlying Shadcn `Sidebar`. |
141
+ | `searchEnabled` | `boolean` | `false` | Renders a search box that filters items by `title` (an item also survives when one of its `children` matches). |
142
+ | `searchPlaceholder` | `string` | `'Search…'` | Placeholder for that search box. |
143
+
144
+ #### `NavItem`
145
+
146
+ | Key | Type | Default | Description |
147
+ | --- | --- | --- | --- |
148
+ | `title` | `string` | — (required) | The visible label, and what search matches against. |
149
+ | `href` | `string` | — (required) | `NavLink` target; also the React key, so keep it unique within its list. Active state is `pathname === href`. |
150
+ | `icon` | `React.ComponentType<{ className?: string }>` | — | The icon **component** (e.g. `Home` from `lucide-react`), not its name. |
151
+ | `badge` | `string \| number` | — | Trailing badge content. Rendered whenever it is not `null`/`undefined`, so `0` shows. |
152
+ | `badgeVariant` | `'default' \| 'destructive' \| 'outline'` | `'default'` | Badge styling. |
153
+ | `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. |
154
+
155
+ #### `NavGroup`
156
+
157
+ | Key | Type | Description |
158
+ | --- | --- | --- |
159
+ | `label` | `string` | Section heading printed above the group. |
160
+ | `items` | `NavItem[]` | The group's items — same `NavItem` shape as above, nesting included. |
161
+
162
+ ```typescript
163
+ import { SidebarNav, type NavGroup } from '@object-ui/layout';
164
+ import { FolderOpen, Home, Settings } from 'lucide-react';
165
+
166
+ const navGroups: NavGroup[] = [
167
+ {
168
+ label: 'Workspace',
169
+ items: [
170
+ { title: 'Dashboard', href: '/dashboard', icon: Home },
171
+ {
172
+ title: 'Projects',
173
+ href: '/projects',
174
+ icon: FolderOpen,
175
+ badge: 3,
176
+ children: [
177
+ { title: 'Active', href: '/projects/active' },
178
+ { title: 'Archived', href: '/projects/archived', badge: 'WIP', badgeVariant: 'outline' },
179
+ ],
180
+ },
181
+ ],
182
+ },
183
+ {
184
+ label: 'System',
185
+ items: [{ title: 'Settings', href: '/settings', icon: Settings }],
186
+ },
187
+ ];
188
+
189
+ <SidebarNav items={navGroups} searchEnabled />
190
+ ```
191
+
192
+ Note that `SidebarNav` is a plain React component: unlike the keys listed under
193
+ [Registration](#registration) it is **not** on the `ComponentRegistry`, so it is
194
+ composed in JSX rather than authored as a JSON node. Full guide:
195
+ [SidebarNav docs](https://www.objectui.org/docs/layout/sidebar-nav).
196
+
92
197
  ## Usage with React Router
93
198
 
94
199
  The layout components are designed to work seamlessly with React Router:
@@ -96,17 +201,18 @@ The layout components are designed to work seamlessly with React Router:
96
201
  ```typescript
97
202
  import { BrowserRouter, Routes, Route } from 'react-router-dom';
98
203
  import { AppShell, SidebarNav } from '@object-ui/layout';
204
+ import { Home, Users } from 'lucide-react';
99
205
 
100
206
  function App() {
101
207
  return (
102
208
  <BrowserRouter>
103
209
  <AppShell
104
- header={<div className="p-4">My App</div>}
210
+ navbar={<div className="p-4">My App</div>}
105
211
  sidebar={
106
212
  <SidebarNav
107
213
  items={[
108
- { label: 'Dashboard', path: '/', icon: 'home' },
109
- { label: 'Users', path: '/users', icon: 'users' }
214
+ { title: 'Dashboard', href: '/', icon: Home },
215
+ { title: 'Users', href: '/users', icon: Users },
110
216
  ]}
111
217
  />
112
218
  }
@@ -123,15 +229,18 @@ function App() {
123
229
 
124
230
  ## Customization
125
231
 
126
- All components accept `className` prop for Tailwind customization:
232
+ `AppShell` takes a single `className`, and it lands on the `<main>` content
233
+ element (`src/AppShell.tsx:256`) rather than on the outer container. There is no
234
+ per-slot `headerClassName` / `sidebarClassName` — the navbar and the sidebar are
235
+ nodes **you** build, so style them where you build them:
127
236
 
128
237
  ```typescript
129
238
  <AppShell
130
239
  className="bg-gray-50"
131
- headerClassName="border-b"
132
- sidebarClassName="bg-white shadow-lg"
240
+ navbar={<div className="border-b px-4">My App</div>}
241
+ sidebar={<div className="bg-white shadow-lg">Sidebar Content</div>}
133
242
  >
134
- {children}
243
+ <div>Main Content</div>
135
244
  </AppShell>
136
245
  ```
137
246
 
@@ -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
- /** Mobile navigation display mode */
5
- export type MobileNavMode = 'drawer' | 'bottom_nav' | 'hamburger';
3
+ import { VisibilityEvaluator, PermissionChecker, CapabilityChecker } 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;
@@ -52,8 +70,8 @@ export interface AppSchemaRendererProps {
52
70
  * - Area switcher when multiple areas are VISIBLE — area visibility is
53
71
  * derived from the items inside, not authored (objectui#3311): an area
54
72
  * whose items are all gated away is hidden and never auto-activated
55
- * - Mobile modes: `drawer` (sheet overlay, default), `bottom_nav` (fixed
56
- * bottom bar), `hamburger` (collapsed sidebar)
73
+ * - Mobile modes: `drawer` (sheet overlay, default) and `bottom_nav` (fixed
74
+ * bottom bar) — see `MobileNavMode` for why that list is two long
57
75
  * - Evaluates `visible` expressions and `requiredPermissions` on every item
58
76
  *
59
77
  * @example
@@ -10,8 +10,6 @@ export interface AppShellBranding {
10
10
  accentColor?: string;
11
11
  /** Favicon URL — replaces the <link rel="icon"> href */
12
12
  favicon?: string;
13
- /** Logo URL — passed to sidebar/navbar via context */
14
- logo?: string;
15
13
  /** Page title suffix (sets document.title) */
16
14
  title?: string;
17
15
  }
@@ -100,22 +100,6 @@ export interface NavigationRendererProps {
100
100
  * as two `商机` rows where one is the list and the other is a Kanban view).
101
101
  */
102
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`.
109
- */
110
- resolveGroupLabel?: (groupId: string, fallbackLabel: string) => string;
111
- /**
112
- * Optional label resolver for non-object/non-dashboard/non-group nav
113
- * items (url, page, report, custom). Called with `(itemId, fallbackLabel)`
114
- * for items that have a plain-string label and a stable `id`.
115
- * Convention: `{ns}.apps.{appName}.navigation.{itemId}.label` — same
116
- * key shape as `resolveGroupLabel` so a single i18n helper covers both.
117
- */
118
- resolveItemLabel?: (itemId: string, fallbackLabel: string) => string;
119
103
  /**
120
104
  * Optional i18n translation function for resolving I18nLabel objects
121
105
  * (`{ key, defaultValue }`). When provided, labels are translated
@@ -213,6 +197,25 @@ export interface NavTemplateContext {
213
197
  */
214
198
  contextValues?: Record<string, string | null | undefined>;
215
199
  }
200
+ /**
201
+ * The wire encoding of the declared `runAction` nav slot — ONE definition,
202
+ * sited with `resolveHref` because that is the only thing that writes it.
203
+ *
204
+ * `ObjectNavItemSchema.runAction` (objectstack#7253) declares WHICH action an
205
+ * object nav entry auto-runs on arrival; the URL is how that declaration
206
+ * survives the navigation, because the list surface mounts in a fresh render
207
+ * tree with nothing but the location to read. Naming the param after the slot
208
+ * is deliberate: the slot owns the name, so producer and consumer cannot drift.
209
+ *
210
+ * Before this constant existed, the name was a bare `'runAction'` literal
211
+ * hand-written at both ends — a private convention no schema declared, so
212
+ * `objectui validate` could not see it, `RESERVED_URL_PARAMS` did not list it,
213
+ * and a page could have repurposed the name with nothing to catch the
214
+ * collision. Import this; never re-spell it. `@object-ui/app-shell`'s
215
+ * `urlParams` re-exports it into the reserved registry rather than restating
216
+ * it, so there is still exactly one definition.
217
+ */
218
+ export declare const NAV_RUN_ACTION_PARAM = "runAction";
216
219
  /**
217
220
  * Resolve a NavigationItem to an absolute href (relative to `basePath`).
218
221
  *
@@ -268,4 +271,4 @@ export declare function filterNavigationItems(items: NavigationItem[], query: st
268
271
  * />
269
272
  * ```
270
273
  */
271
- export declare function NavigationRenderer({ items, basePath, evaluateVisibility: evalVis, checkPermission: checkPerm, checkCapability: checkCap, onAction, searchQuery, enablePinning, onPinToggle, enableReorder, onReorder, resolveObjectLabel, resolveDashboardLabel, resolveGroupLabel, resolveViewLabel, resolveItemLabel, t: tProp, templateContext, }: NavigationRendererProps): React.JSX.Element;
274
+ export declare function NavigationRenderer({ items, basePath, evaluateVisibility: evalVis, checkPermission: checkPerm, checkCapability: checkCap, onAction, searchQuery, enablePinning, onPinToggle, enableReorder, onReorder, resolveObjectLabel, resolveDashboardLabel, resolveViewLabel, t: tProp, templateContext, }: NavigationRendererProps): React.JSX.Element;
@@ -24,12 +24,19 @@ import { default as React } from 'react';
24
24
  export interface PageHeaderComponentProps extends React.HTMLAttributes<HTMLDivElement> {
25
25
  title: string;
26
26
  /**
27
- * Optional secondary line under the title. Spec schemas use `subtitle`,
28
- * the legacy console pages use `description` — both are supported and
29
- * resolve {field.path} tokens against the current record context.
27
+ * Optional secondary line under the title. `subtitle` is the only spelling:
28
+ * it is the key `@objectstack/spec/ui`'s `PageHeaderProps` declares, and it
29
+ * resolves {field.path} tokens against the current record context.
30
+ *
31
+ * The legacy `description` alias this component used to read alongside it
32
+ * was retired in objectui#3789, once the ADR-0087 D2 conversion
33
+ * `page-header-subtitle-alias` was measured to reach EVERY spec-valid header
34
+ * position on the load path (regions, slots, and containers nested to any
35
+ * depth — objectstack#6775 / #6776 / PR #7034). Stored metadata authored with
36
+ * `description` is rewritten to `subtitle` before it ever reaches a renderer,
37
+ * so the second line survives without this side holding a second dialect open.
30
38
  */
31
39
  subtitle?: string;
32
- description?: string;
33
40
  /**
34
41
  * Optional icon for the header chip. Accepts either a string Lucide icon
35
42
  * name (resolved via `LazyIcon`, the standard ObjectUI helper) or a
@@ -66,4 +73,4 @@ export interface PageHeaderComponentProps extends React.HTMLAttributes<HTMLDivEl
66
73
  };
67
74
  };
68
75
  }
69
- export declare function PageHeader({ title, subtitle, description, icon, action, actions, showBack, schema, className, children, ...props }: PageHeaderComponentProps): React.JSX.Element;
76
+ export declare function PageHeader({ title, subtitle, icon, action, actions, showBack, schema, className, children, ...props }: PageHeaderComponentProps): React.JSX.Element;
package/dist/index.d.ts CHANGED
@@ -2,11 +2,11 @@
2
2
  * ObjectUI Layout
3
3
  * Copyright (c) 2024-present ObjectStack Inc.
4
4
  */
5
- export * from './PageHeader';
6
- export * from './AppShell';
7
- export * from './PageCard';
8
- export * from './SidebarNav';
9
- export * from './ResponsiveGrid';
10
- export * from './NavigationRenderer';
11
- export * from './AppSchemaRenderer';
5
+ export * from './PageHeader.js';
6
+ export * from './AppShell.js';
7
+ export * from './PageCard.js';
8
+ export * from './SidebarNav.js';
9
+ export * from './ResponsiveGrid.js';
10
+ export * from './NavigationRenderer.js';
11
+ export * from './AppSchemaRenderer.js';
12
12
  export declare function registerLayout(): void;