Skip to content

Commit 21a13f1

Browse files
spec(ui): a navigation group that says nothing opens — GroupNavItem expanded defaults to true (#26427)
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} --> _Requested by **Yibo Wang** · [project thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwBWwk9BkRgZKVzh5DadYMEZV)_ Fixes #26370 Clause-②: yes Metadata-protocol: semantic Skills-docs: none — no skill under `skills/**` states or implies the old default: `skills/objectstack-ui/SKILL.md` lists `expanded` on the group row with no default, and `rules/navigation.md` writes `expanded: true` out, so both read true under the flip, and no `objectstack-ui` eval exercises the key. The docs half is in this diff: `content/docs/ui/apps.mdx` (Group Navigation) now says a group opens unless it declares `expanded: false`, and the generated `content/docs/references/ui/app.mdx` carries the new describe text. Ruling-ref: 6431302164 (batch #405 item 1, letter A, maintainer 「同意」) ## 维护者速读 - 改了什么:导航分组 `expanded` 的默认值从 `false` 改成 `true`,不写就展开;想折叠要写 `expanded: false`。附一条 ADR-0087 D3 语义迁移条目、默认值变更声明和 changeset。 - 为什么改:按裁决 A,协议跟上控制台已经裁定的行为。实测:经 `defineStack` 解析的应用(showcase)以前给 8 个没写 `expanded` 的分组都下发了 `expanded: false`,控制台照此折叠;平台自带的 Setup/Account 不经解析,没写就不下发,控制台展开。两种来源结果相反,现在统一为展开。 - 风险与回滚:已编译的旧 `objectstack.json` 里写死了 `false`,那些分组仍会折叠,重新编译即可。回滚就是还原这一行默认值和迁移条目。 - 先看:`packages/spec/src/ui/app.zod.ts`(默认值与描述)、`packages/spec/src/migrations/entries/semantic/18.ui-nav-group-expanded-default-open.ts`、`packages/spec/scripts/lib/default-changes.ts`。 - 两分钟验证:`pnpm --filter @objectstack/spec exec vitest run src/ui/app.test.ts`,预期 GroupNavItemSchema 四个用例全绿:不写解析为 `true`,写 `false` 仍为 `false`。 - 需要你做的:无,席位落地。 ## What changes - `GroupNavItemSchema.expanded` (`packages/spec/src/ui/app.zod.ts`) is now `z.boolean().default(true)`. Its describe says what each value does: omitted opens, `false` keeps the group closed until the user opens it, and a group holding the current route opens whatever it declares. - ADR-0087 D3 semantic entry `ui-nav-group-expanded-default-open` (major 18). It carries the change to `os migrate meta` and the upgrade guide. It is the same class as `list-view-edit-inline-default-on` and `view-pagination-page-size-default-50`: no key removed, no tombstone, no D2 conversion. - `DEFAULT_CHANGES_BY_MAJOR[18]` declares `ui/GroupNavItem:expanded` false → true. The spec build refused the flip until it was declared. - Generated: `packages/spec/authorable-defaults/ui.json` (one line) and `content/docs/references/ui/app.mdx` (the describe text, five tables). Hand-written: one sentence in `content/docs/ui/apps.mdx`. - Changeset: `@objectstack/spec` minor, with the two declaration lines. - No renderer change. `ui/packages/layout/src/NavigationRenderer.tsx` (the `explicitOpen` block, about lines 1538-1550) opens on `true` and on an absent key and closes on `false`. After the flip the served shape is `true` or absent for every silent group, so the served shape and the renderer agree. ## Why The maintainer ruled letter A on #26370 (6431302164): a navigation group that says nothing opens, and an author who wants it closed writes `expanded: false`, as `setup.app.ts:116` and `account.app.ts:186` already do. ### Triage P3, measured (the hypothesis was half true) The served navigation DOES apply the spec default, for every app that goes through a parse. It does not for apps registered raw. | app | producer | served groups on `origin/main` (e931621) | served groups on this branch (2f3905e; the merge adds nothing under `packages/spec/src/ui`) | |:--|:--|:--|:--| | `showcase_app` | `defineStack` (parsed; the compiled `dist/objectstack.json` carries the same) | all 8 groups `expanded: false`, none authored | all 8 groups `expanded: true` | | `setup` | raw `App` object, registered without a parse | 8 groups with no key, `group_advanced` `false` (authored) | unchanged | | `account` | raw `App` object | inbox `true`, security `true`, developer `false` (all authored) | unchanged | The registry's `validate()` parse is a diagnostic only and stores the raw item (`packages/objectql/src/registry.ts`, the spec-conformance block), so a raw app never gains the default. `defineStack` in strict mode returns the parsed stack, so a parsed app always does. On main this made the two kinds of app behave oppositely in the console: the showcase's groups arrived with a written `false` and collapsed, and Setup's arrived with no key and opened. After the flip, both open. ## Tests - `packages/spec/src/ui/app.test.ts`: the silent-group case now expects `true`. New cases: an authored `false` stays `false`; a non-boolean is refused at path `expanded`; a silent group in a parsed app opens while an authored nested `false` stays closed. ## Acceptance notes - The migration entry and the default-change reason name the compiled-artifact case. A document that serialised an earlier parse carries a written `false` and its groups stay closed until it is recompiled or the key is deleted. - `docs/qa/platform-checklist/areas/platform-core.json` step 2 says "QA run found Setup's Configuration group collapsed as well". That records an earlier run and stays true as history. Setup's Configuration group declares no `expanded` and is served without the key, so it opens on main and on this branch. Not edited here. - Browser rendering was not re-measured. The open or closed reading follows from the served shape above and the renderer's `explicitOpen` block. ## Verification <details><summary>Served navigation, before and after (GET /api/v1/meta/app, showcase dev server, fresh DB, signed in as the seeded admin)</summary> ``` origin/main e931621: showcase_app | grp_workspace=false grp_data=false grp_slices=false grp_analytics=false grp_auth_structured=false grp_auth_viz=false grp_auth_html=false grp_auth_react=false setup | group_overview=absent group_apps=absent group_people_org=absent group_access_control=absent group_approvals=absent group_configuration=absent group_diagnostics=absent group_integrations=absent group_advanced=false account | grp_account_inbox=true grp_account_security=true grp_account_developer=false branch 2f3905e: showcase_app | grp_workspace=true grp_data=true grp_slices=true grp_analytics=true grp_auth_structured=true grp_auth_viz=true grp_auth_html=true grp_auth_react=true setup | (identical to main) account | (identical to main) ``` </details> <details><summary>Local runs at head 061f21a (the merge of origin/main e677770; no commit since)</summary> ``` pnpm --filter @objectstack/spec typecheck exit 0 pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 exit 0 · Test Files 806 passed (806) · Tests 23291 passed | 1 todo pnpm --filter @objectstack/spec build && check:generated exit 0 · All 14 generated artifacts are up to date node scripts/pm/dispatch-gates.mjs --commands 116 commands, each run once, exit code captured before any pipe: 116 x exit 0 node scripts/pm/dispatch-gates.mjs --ran ran.list exit 0 · 116 derived, 116 run, 0 NOT-MEASURED, 0 UNRUN ``` Before the merge, at 2f3905e: the same spec suite (805 files, 23266 tests) and the same 116 gates were green, after two precondition refusals were cleared (a fixture commit the shallow clone lacked; a spec dist rebuilt mid-read). The spec build refused the flip once until `DEFAULT_CHANGES_BY_MAJOR` declared it (`ui/GroupNavItem:expanded: false -> true`), and `check:generated --fix` regenerated `check:docs` only. `packages/cli` `migrate-meta-engine-guidance` (integration tier; reads every semantic entry for tracker ids) is declared to CI's Test Core. The new entry's printed fields carry 0 tracker ids (scanned). </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. - [x] 2. Every name I added uses the casing for its kind. - [ ] 3. No formatter ran. `pnpm lint` was not run locally: the repo-wide scan is CI's (`Lint & Repo Gates`). - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. - [ ] The *Before Submitting* list in CONTRIBUTING.md: not walked item by item; the local runs below are what was done. --- _Generated by [Claude Code](https://claude.ai/code/session_01WGufzgtoAxAMDnavXSKysG)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent d0a7e74 commit 21a13f1

9 files changed

Lines changed: 143 additions & 9 deletions

File tree

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
spec(ui): a navigation group that says nothing opens — `GroupNavItem.expanded` defaults to `true`
6+
7+
Clause-②: yes
8+
Metadata-protocol: semantic
9+
10+
<!-- adr-0087: registered ui-nav-group-expanded-default-open -->
11+
12+
**BREAKING** — a declared default moves, shipped as `minor` under the launch-window convention. The author who relied on the documented default (`false`, "a group starts collapsed unless the author opens it") for a group meant to stay out of the daily user's sight — an advanced, developer or diagnostics section — now sees it open on first view wherever the app is parsed (`defineStack`, `defineApp`, a compiled `objectstack.json`). The remedy is one key on that group: `expanded: false`.
13+
14+
- **Before.** `GroupNavItemSchema.expanded` defaulted to `false`. An app parsed through `defineStack` or `defineApp` (and so every compiled `objectstack.json`) carried a written `expanded: false` on every group whose author said nothing. The console honours a written `false` as an explicit close, so those groups started collapsed. An app registered without a parse (the platform's Setup and Account apps) arrived without the key, and the console opened its groups. The protocol and the console disagreed about what a silent group means.
15+
- **Now.** The default is `true`. A group that says nothing parses to `expanded: true` and opens on first view, the same answer the console already gave a group without the key. A group holding the current route still opens whatever it declares.
16+
- **To keep a group closed**, write `expanded: false`. Setup's Advanced group and Account's Developer group already do, and they still collapse. Every authored `expanded` parses exactly as before, and a non-boolean is still refused.
17+
- **Compiled artifacts.** A document that serialised an earlier parse (an `os compile` artifact) carries the old written `false`, and its groups stay closed. Recompile it, or delete the key.
18+
- **Upgrade path.** The ADR-0087 D3 semantic entry `ui-nav-group-expanded-default-open` carries the change to `os migrate meta` and the upgrade guide. The default change is declared in the build's default-change ledger. No key is added or removed.

‎content/docs/references/ui/app.mdx‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ Documentation entry on the app menu (ADR-0046). Targets a `book` and/or a `doc`
286286
| **requiresObject** | `string` | optional | Hide/disable this entry unless the named object is registered in the runtime |
287287
| **requiresService** | `string` | optional | Hide/disable this entry unless the named kernel service is registered |
288288
| **type** | `'group'` | ✅ | |
289-
| **expanded** | `boolean` | optional (default: `false`) | Default expansion state in sidebar |
289+
| **expanded** | `boolean` | optional (default: `true`) | Whether the group starts open in the sidebar. Omitted, it opens; write `false` to keep it closed until the user opens it. A group holding the current route opens whatever it declares. |
290290
| **children** | `({ id: string; label?: string \| Record<string, string>; icon?: string; order?: number; … } \| { type: 'separator'; id?: string; order?: number } \| … +8 more)[]` | ✅ | Child navigation items |
291291

292292
### Nested Shape: `App.areas[number]`
@@ -475,7 +475,7 @@ Documentation entry on the app menu (ADR-0046). Targets a `book` and/or a `doc`
475475
| **requiresObject** | `string` | optional | Hide/disable this entry unless the named object is registered in the runtime |
476476
| **requiresService** | `string` | optional | Hide/disable this entry unless the named kernel service is registered |
477477
| **type** | `'group'` | ✅ | |
478-
| **expanded** | `boolean` | optional (default: `false`) | Default expansion state in sidebar |
478+
| **expanded** | `boolean` | optional (default: `true`) | Whether the group starts open in the sidebar. Omitted, it opens; write `false` to keep it closed until the user opens it. A group holding the current route opens whatever it declares. |
479479

480480
### Nested Shape: `GroupNavItem.dataSource`
481481

@@ -680,7 +680,7 @@ Documentation entry on the app menu (ADR-0046). Targets a `book` and/or a `doc`
680680
| **requiresObject** | `string` | optional | Hide/disable this entry unless the named object is registered in the runtime |
681681
| **requiresService** | `string` | optional | Hide/disable this entry unless the named kernel service is registered |
682682
| **type** | `'group'` | ✅ | |
683-
| **expanded** | `boolean` | optional (default: `false`) | Default expansion state in sidebar |
683+
| **expanded** | `boolean` | optional (default: `true`) | Whether the group starts open in the sidebar. Omitted, it opens; write `false` to keep it closed until the user opens it. A group holding the current route opens whatever it declares. |
684684
| **children** | `({ id: string; label?: string \| Record<string, string>; icon?: string; order?: number; … } \| { type: 'separator'; id?: string; order?: number } \| … +8 more)[]` | ✅ | Child navigation items |
685685

686686

@@ -879,7 +879,7 @@ Documentation entry on the app menu (ADR-0046). Targets a `book` and/or a `doc`
879879
| **requiresObject** | `string` | optional | Hide/disable this entry unless the named object is registered in the runtime |
880880
| **requiresService** | `string` | optional | Hide/disable this entry unless the named kernel service is registered |
881881
| **type** | `'group'` | ✅ | |
882-
| **expanded** | `boolean` | optional (default: `false`) | Default expansion state in sidebar |
882+
| **expanded** | `boolean` | optional (default: `true`) | Whether the group starts open in the sidebar. Omitted, it opens; write `false` to keep it closed until the user opens it. A group holding the current route opens whatever it declares. |
883883
| **children** | `({ id: string; label?: string \| Record<string, string>; icon?: string; order?: number; … } \| { type: 'separator'; id?: string; order?: number } \| … +8 more)[]` | ✅ | Child navigation items |
884884

885885

@@ -1200,7 +1200,7 @@ Documentation entry on the app menu (ADR-0046). Targets a `book` and/or a `doc`
12001200
| **requiresObject** | `string` | optional | Hide/disable this entry unless the named object is registered in the runtime |
12011201
| **requiresService** | `string` | optional | Hide/disable this entry unless the named kernel service is registered |
12021202
| **type** | `'group'` | ✅ | |
1203-
| **expanded** | `boolean` | optional (default: `false`) | Default expansion state in sidebar |
1203+
| **expanded** | `boolean` | optional (default: `true`) | Whether the group starts open in the sidebar. Omitted, it opens; write `false` to keep it closed until the user opens it. A group holding the current route opens whatever it declares. |
12041204
| **children** | `[NavigationItem](#navigationitem)[]` | ✅ | Child navigation items |
12051205

12061206
### Nested Shape: `NavigationItem[type='group'].dataSource`

‎content/docs/ui/apps.mdx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,9 @@ Links to an external URL:
143143

144144
### Group Navigation
145145

146-
Groups items into collapsible sections with children:
146+
Groups items into collapsible sections with children. A group opens on first view unless it
147+
declares `expanded: false`, which keeps it closed until the user opens it; a group that holds the
148+
current route opens whatever it declares.
147149

148150
```typescript
149151
{

‎packages/spec/authorable-defaults/ui.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
"ui/GalleryConfig:cardSize = \"medium\"",
4848
"ui/GalleryConfig:coverFit = \"cover\"",
4949
"ui/GlobalFilter:scope = \"dashboard\"",
50-
"ui/GroupNavItem:expanded = false",
50+
"ui/GroupNavItem:expanded = true",
5151
"ui/GroupingField:collapsed = false",
5252
"ui/GroupingField:order = \"asc\"",
5353
"ui/HttpRequest:method = \"GET\"",

‎packages/spec/scripts/lib/default-changes.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,5 +667,19 @@ export const DEFAULT_CHANGES_BY_MAJOR: Readonly<Record<number, readonly Declared
667667
+ 'sections would re-create exactly the reading the ruling removed, and every declared value '
668668
+ 'keeps its meaning.',
669669
},
670+
{
671+
key: 'ui/GroupNavItem:expanded',
672+
from: 'false',
673+
to: 'true',
674+
reason:
675+
'A RULED default change (maintainer ruling record 6431302164, letter A): a navigation group '
676+
+ 'that says nothing opens. The console already opened a group that arrived without the key; '
677+
+ 'what moves is the PARSED value. An app parsed through `defineStack` / `defineApp` used to '
678+
+ 'carry a written `expanded: false` on every silent group, which the console honoured as an '
679+
+ 'explicit close, and now carries `true`, so those groups open on first view. To keep a group '
680+
+ 'closed until the user opens it, write `expanded: false`. The `semantic` entry '
681+
+ '`ui-nav-group-expanded-default-open` carries the change to `os migrate meta` and the '
682+
+ 'upgrade guide.',
683+
},
670684
],
671685
};
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import type { SemanticMigration } from '../../types.js';
4+
5+
// The declared default of `GroupNavItemSchema.expanded` moved from `false` to
6+
// `true` (maintainer ruling 6431302164 on objectstack#26370, letter A). The
7+
// product behaviour was ruled twice before the protocol followed it:
8+
// objectui#13336 (letter A — a group that does not say `expanded: false`
9+
// opens, and the "8 or more children collapse" rule is gone) and
10+
// hotcrm#2194 (letter B). The console's NavigationRenderer already opens an
11+
// undeclared group, so the protocol now states what the console does. A
12+
// default move reaches every silent document with no parse error and nothing
13+
// in the author's diff, so the upgrade path carries it as a TODO: only the
14+
// deployment can say which group that never declared the key was relying on
15+
// starting closed. The same class as `list-view-edit-inline-default-on` and
16+
// `view-pagination-page-size-default-50`: no key is removed, so there is no
17+
// tombstone and no RETIRED_KEYS_BY_MAJOR row, and no D2 conversion exists.
18+
// The citations live here, not in the printed guidance, which carries no
19+
// tracker number.
20+
export const entry: SemanticMigration = {
21+
id: 'ui-nav-group-expanded-default-open',
22+
// No backticks in `surface` — build-upgrade-guide.ts renders it inside a code
23+
// span AND a table cell.
24+
surface: 'ui.GroupNavItem.expanded — an OMITTED expanded on an app navigation group',
25+
replacement: 'nothing, to take the platform default: the group opens on first view. To keep a '
26+
+ 'group closed until the user opens it — an advanced or developer section the daily user '
27+
+ 'should not see first — write it: `expanded: false`',
28+
reason:
29+
'A RULED behaviour change on a default, so there is nothing to rewrite and nothing to '
30+
+ 'refuse: the maintainer ruled that a navigation group which says nothing opens, and the '
31+
+ 'declared default of `GroupNavItemSchema.expanded` moved from `false` to `true`. The console '
32+
+ 'already behaved this way for a group that arrived without the key — its length does not '
33+
+ 'decide, and a group holding the current route opens whatever it declares — so the protocol '
34+
+ 'now states the behaviour instead of contradicting it. What changes is the parsed and served '
35+
+ 'shape: a group parsed through the app schema (`defineStack`, `defineApp`) used to carry a '
36+
+ 'written `expanded: false` that the console honoured as an explicit close, and now carries '
37+
+ '`expanded: true`. A group registered without a parse still arrives without the key and still '
38+
+ 'opens. Not losslessly convertible because the question is intent, not text: a mechanical pass '
39+
+ 'that wrote `expanded: false` into every silent group would preserve the old posture and defeat '
40+
+ 'the ruling, and one that wrote `true` would add nothing the default does not already do. Only '
41+
+ 'the deployment knows which silent groups were meant to start closed. The accept set is '
42+
+ 'unchanged — a boolean — and every authored `expanded` parses exactly as before. A document that '
43+
+ 'serialised an earlier parse (an `os compile` artifact) carries a written `false` and its groups '
44+
+ 'stay closed; recompile it, or delete the key.',
45+
acceptanceCriteria:
46+
'A group navigation item that omits `expanded` parses to `expanded: true`, and an app whose '
47+
+ 'navigation holds such a group parses with `navigation[].expanded` true; an authored '
48+
+ '`expanded: false` still parses to `false` and an authored `true` to `true`; a non-boolean is '
49+
+ 'still refused. In the console, a group that declares nothing is open on first view, and a '
50+
+ 'group that declares `expanded: false` is closed unless it holds the current route. Every '
51+
+ 'group that must start closed declares `expanded: false` and shows its children only after the '
52+
+ 'user opens it.',
53+
};

‎packages/spec/src/ui/app.nav-type-assertions.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ export const objectWithoutChildren: NavigationItem = {
7979

8080
/**
8181
* `expanded` is REQUIRED on the output side: `GroupNavItemSchema` declares it
82-
* `.default(false)`, so a parsed group always carries it. If this suppression
82+
* `.default(true)`, so a parsed group always carries it. If this suppression
8383
* goes unused the default was dropped from the schema — and every consumer
8484
* reading `item.expanded` off a parsed app silently started getting
8585
* `undefined`.

‎packages/spec/src/ui/app.test.ts‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,10 +208,52 @@ describe('GroupNavItemSchema', () => {
208208
type: 'group' as const,
209209
};
210210

211+
const result = GroupNavItemSchema.parse(navItem);
212+
// ADR-0087 D3 `ui-nav-group-expanded-default-open`: a group that says nothing opens.
213+
expect(result.expanded).toBe(true);
214+
});
215+
216+
it('keeps an authored expanded: false closed', () => {
217+
const navItem = {
218+
id: 'nav_advanced',
219+
label: 'Advanced',
220+
type: 'group' as const,
221+
expanded: false,
222+
};
223+
211224
const result = GroupNavItemSchema.parse(navItem);
212225
expect(result.expanded).toBe(false);
213226
});
214227

228+
it('refuses a non-boolean expanded', () => {
229+
const result = GroupNavItemSchema.safeParse({
230+
id: 'nav_sales',
231+
label: 'Sales',
232+
type: 'group' as const,
233+
expanded: 'yes',
234+
});
235+
expect(result.success).toBe(false);
236+
expect(result.error?.issues.map((i) => i.path.join('.'))).toContain('expanded');
237+
});
238+
239+
it('opens a silent group nested in a parsed app', () => {
240+
const app = AppSchema.parse({
241+
name: 'nav_probe',
242+
label: 'Nav Probe',
243+
navigation: [
244+
{
245+
id: 'grp_outer',
246+
type: 'group',
247+
label: 'Outer',
248+
children: [{ id: 'grp_inner', type: 'group', label: 'Inner', expanded: false, children: [] }],
249+
},
250+
],
251+
});
252+
const outer = app.navigation?.[0] as { expanded?: boolean; children?: Array<{ expanded?: boolean }> };
253+
expect(outer.expanded).toBe(true);
254+
expect(outer.children?.[0]?.expanded).toBe(false);
255+
});
256+
215257
it('should accept group with expanded state', () => {
216258
const navItem = {
217259
id: 'nav_admin',

‎packages/spec/src/ui/app.zod.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -808,7 +808,12 @@ export const DocNavItemSchema = lazySchema(() => strictObject(navItemSurface('do
808808
export const GroupNavItemSchema = lazySchema(() => strictObject(navItemSurface('group'), {
809809
...BaseNavItemSchema.shape,
810810
type: z.literal('group'),
811-
expanded: z.boolean().default(false).describe('Default expansion state in sidebar'),
811+
// ADR-0087 D3 `ui-nav-group-expanded-default-open`: a group that says
812+
// nothing opens; `expanded: false` is how an author keeps one closed.
813+
expanded: z.boolean().default(true).describe(
814+
'Whether the group starts open in the sidebar. Omitted, it opens; write `false` to keep it closed '
815+
+ 'until the user opens it. A group holding the current route opens whatever it declares.',
816+
),
812817
// children property is added in the recursive definition below
813818
}));
814819

0 commit comments

Comments
 (0)