@boost33/ui 0.8.0-next.5 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/CHANGELOG.md +262 -271
  2. package/dist/components/AccountMenu/AccountMenu.d.ts +42 -32
  3. package/dist/components/AccountMenu/AccountMenu.js +66 -48
  4. package/dist/components/AccountMenu/AccountMenu.js.map +1 -1
  5. package/dist/components/AccountMenu/AccountMenu.module.css.js +44 -38
  6. package/dist/components/AccountMenu/AccountMenu.module.css.js.map +1 -1
  7. package/dist/components/AccountMenu/StandardAccountMenu.d.ts +87 -0
  8. package/dist/components/AccountMenu/StandardAccountMenu.js +86 -0
  9. package/dist/components/AccountMenu/StandardAccountMenu.js.map +1 -0
  10. package/dist/components/AccountMenu/icons.d.ts +2 -2
  11. package/dist/components/AccountMenu/icons.js +21 -8
  12. package/dist/components/AccountMenu/icons.js.map +1 -1
  13. package/dist/components/AccountMenu/initials.d.ts +11 -0
  14. package/dist/components/AccountMenu/initials.js +8 -0
  15. package/dist/components/AccountMenu/initials.js.map +1 -0
  16. package/dist/components/AccountMenu/labels.d.ts +64 -3
  17. package/dist/components/AccountMenu/labels.js +33 -5
  18. package/dist/components/AccountMenu/labels.js.map +1 -1
  19. package/dist/components/AccountMenu/use-menu.d.ts +34 -0
  20. package/dist/components/AccountMenu/use-menu.js +102 -0
  21. package/dist/components/AccountMenu/use-menu.js.map +1 -0
  22. package/dist/components/TopBar/TopBar.d.ts +8 -1
  23. package/dist/components/TopBar/TopBar.js +19 -16
  24. package/dist/components/TopBar/TopBar.js.map +1 -1
  25. package/dist/index.d.ts +4 -2
  26. package/dist/index.js +40 -36
  27. package/dist/index.js.map +1 -1
  28. package/dist/style.css +1 -1
  29. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -27,9 +27,268 @@ why the entry matters more than the number.
27
27
 
28
28
  ## Unreleased
29
29
 
30
- Three components are built to Stage A on main and are not releasable until
31
- each passes its fidelity gate. Their entries wait here for the release that
32
- ships them.
30
+ Two components are built to Stage A on main and are not releasable until each
31
+ passes its fidelity gate. Their entries wait here for the release that ships
32
+ them.
33
+
34
+ ### Added — `CommandPalette`, Stage A only. **Not releasable: it has not passed its fidelity gate** (B33-495)
35
+
36
+ **Do not include this component in a release.** `CommandPalette` is exported from
37
+ `src/index.ts` and will appear in `dist/`, and a consumer who finds it there and
38
+ renders it is rendering an unapproved view. It is Stage A under `CLAUDE.md`'s
39
+ presentational-first rule: static markup, reproduced from Paper's
40
+ "CommandPalette — search" artboard, with **no state, no handlers and no data**.
41
+ The Wallet never rendered a palette, so there is no captured referent and no
42
+ approved measurement anywhere — `CLAUDE.md` is explicit that before the gate
43
+ nothing is authoritative and Paper is only a starting drawing. It becomes
44
+ releasable when a person opens the specimen route, approves it, captures the
45
+ first *designed* baseline themselves, and writes the measurements they approved
46
+ into the spec document in the same session. Until all three have happened, cut
47
+ this out of the release or hold the release.
48
+
49
+ What exists today, and what does not:
50
+
51
+ | | |
52
+ | --- | --- |
53
+ | Draws | the search row, three result groups with their headings, the footer legend, and an empty state |
54
+ | Takes | `groups`, `query`, `activeResultId`, `labels`, `linkComponent` |
55
+ | Does **not** | open, close, trap focus, mark anything `inert`, register `⌘K`, dismiss on Escape or a backdrop click, or move a highlight |
56
+
57
+ **It takes grouped results as props and owns no search index.** Pages, Products
58
+ and Actions are all the host's, decided on B33-495 on 2026-10-06. There is no
59
+ cross-suite index in any repo and this component does not imply one: a palette
60
+ over one product's pages is a smaller promise than the artboard's description
61
+ ("across the whole suite") makes, and that is accepted so the component is not
62
+ held behind an architectural gap. Taking results as props is scope-agnostic —
63
+ four products' results render exactly as one product's do — so a real index later
64
+ changes what is passed in, not what is here.
65
+
66
+ A result that navigates renders through `linkComponent` (ADR-0011); one with no
67
+ `href` is an action and stays a `<button>`, with no handler until Stage B. Each
68
+ result's glyph is the host's `icon`, because the groups are the host's and
69
+ nothing here can map one to a glyph without learning what a host's group means.
70
+ The markup is the combobox-over-listbox ADR-0010 settled for this component —
71
+ `role="combobox"` on the input naming the highlighted row through
72
+ `aria-activedescendant`, `role="listbox"` on the list, `role="option"` on each
73
+ result, `role="group"` on each group — and that is markup, not traversal; what
74
+ the arrow keys do is Stage B's to implement.
75
+
76
+ Every user-facing string is a prop with an English default
77
+ (`defaultCommandPaletteLabels`, exported and frozen), resolved through the same
78
+ `Partial` + `undefined`-skipping merge `WalletChip` uses, so one missed
79
+ translation key falls back to English rather than leaving the panel's only
80
+ control unnamed (B33-873, B33-877, and the contract B33-1007 asks the package to
81
+ converge on).
82
+
83
+ **New token: `--container-command-palette: 560px`**, the panel's width, read off
84
+ the artboard and recorded in `data/paper-tokens.json` as **provisional** — it is
85
+ the drawing's number, not an approved measurement, and the person at the gate may
86
+ move it. It is the only token this change adds; everything else draws on tokens
87
+ that already existed.
88
+
89
+ `TopBar` is unchanged. There is still no `⌘K` badge in the bar and still no
90
+ `aria-haspopup` on the search button — both arrive with the panel, in Stage B.
91
+
92
+ Not in `dist/` and not a consumer's business: the specimen route showing the four
93
+ states, their `SPECIMEN_ONLY` records (none of them is screenshot-gated, because
94
+ a *designed* baseline is the gate's to capture and never an agent's), and
95
+ `tests/command-palette.test.tsx`.
96
+ ### Added — `NotificationCenter`, Stage A only, **not releasable yet** (B33-494)
97
+
98
+ > [!danger] This component has not passed its fidelity gate and must not be
99
+ > included in a release until it has.
100
+ > `NotificationCenter` is exported from `src/index.ts`, which means it would be
101
+ > published by any version cut from this state. Do not cut one. A human has not
102
+ > yet opened the specimen route, judged the component, captured its first
103
+ > baseline or written its measurements into the spec — and until all three happen
104
+ > in one session, there is no referent for this component at all (`CLAUDE.md`,
105
+ > "The three components the Wallet never rendered"). Nothing it draws is settled,
106
+ > including the panel's 360px width and every space inside it.
107
+ >
108
+ > It is also only half built. Stage A is static: the panel renders and nothing
109
+ > responds. The bell does not open it, Escape does not close it, the filters do
110
+ > not filter and "Mark all as read" marks nothing. That is Stage B, and it lands
111
+ > after the approval, not before.
112
+
113
+ The panel behind the top bar's bell: everything the four products did, in one
114
+ list, with the unread ones marked. Both states the design draws — populated and
115
+ empty — ship from this package and appear in the specimen app.
116
+
117
+ **It is presentational and it owns no feed.** The notifications arrive as a prop
118
+ and this package neither fetches, stores nor subscribes to anything. There is no
119
+ cross-product event source in any repo today, and a component that implied one
120
+ would make the shared shell the place four services integrate — the opposite of
121
+ the dependency direction every other component here has, and a straight
122
+ contradiction of `CLAUDE.md`'s "it renders what it is given". That was decided on
123
+ the ticket on 2026-10-06; the feed itself is separate work, outside 1.0.
124
+
125
+ ```tsx
126
+ <NotificationCenter
127
+ items={[
128
+ {
129
+ id: 'n1',
130
+ slug: 'wallet', // optional: selects the ProductMark
131
+ title: 'Your granted tokens expire in 3 days',
132
+ source: 'Wallet', // the host's words, not a slug
133
+ time: '12 minutes ago', // preformatted by the host
134
+ dateTime: '2026-10-06T09:48:00Z', // optional: the <time> element's datetime
135
+ href: '/wallet/grants', // optional: a row with nowhere to go is not a link
136
+ unread: true, // defaults to false
137
+ },
138
+ ]}
139
+ viewAllHref="/notifications"
140
+ />
141
+ ```
142
+
143
+ **The time arrives formatted, and that is a decision rather than an omission.**
144
+ The obvious move is the one `WalletChip` makes for numbers — take a `locale` and
145
+ format from a `Date` — and three things separate the two cases. A number is a
146
+ pure function of its value and a locale; a *relative* time needs a `now`, so the
147
+ component would read a clock and then either re-read it on a timer or go stale on
148
+ screen, which is state in a component whose contract is that it has none. It
149
+ would also make this component's own fidelity gate unreproducible: a designed
150
+ baseline showing "12 minutes ago" from `Date.now()` is a different image an hour
151
+ later. And the hard part is editorial — where a product cuts over from "12
152
+ minutes ago" to "Yesterday" to a date is a policy each product may differ on, and
153
+ `Intl.RelativeTimeFormat` does not decide it. The optional `dateTime` is how the
154
+ machine-readable instant still reaches the page without this package deciding how
155
+ to say it.
156
+
157
+ **Every string is a prop with an English default**, in the same shape
158
+ `WalletChipLabels` uses: `labels` is a `Partial`, a missing key falls back to
159
+ English rather than blanking the UI (B33-873), `defaultNotificationCenterLabels`
160
+ is frozen in the B33-916 spelling so writing to it is a compile error, and the
161
+ one value-carrying string — the row's `{source} · {time}` line — is a template or
162
+ a function, so a locale owns the word order and the separator. `AppSwitcher`'s
163
+ contract still differs on the freeze; B33-1007 closes that by bringing the
164
+ switcher to this shape, not by adding a fourth.
165
+
166
+ The panel is a disclosure, not a menu and not an ARIA tab set (ADR-0010): no
167
+ widget role, real links and buttons, and Tab is the document's own order, so the
168
+ header action, both filters, every row and the footer link are each their own tab
169
+ stop. Rows that navigate render through `linkComponent` (ADR-0011). The unread
170
+ state is announced in words as well as drawn as a dot, because a colour reaches
171
+ nobody on a screen reader.
172
+
173
+ Three further things it deliberately does not do: it does not sort, filter, page
174
+ or truncate `items`, because each is a decision about data it does not own; it
175
+ draws no "0" on the unread filter, because a zero pill reads as a state; and the
176
+ empty rendering drops the filters, the header action and the footer rather than
177
+ offering three controls over an empty list. The empty state is not an edge case —
178
+ a host with no feed wired yet has nothing to pass, which makes it the rendering
179
+ most consumers meet first.
180
+
181
+ **`TopBar` is unchanged.** Its bell's `unread` prop still defaults to `false`, it
182
+ still gains no `aria-haspopup`, and it still opens nothing. Connecting the two is
183
+ Stage B.
184
+
185
+ ## 0.8.1 — 2026-11-05
186
+
187
+ **Not breaking. Add one element to your top bar.** The account menu, the Hub's,
188
+ for every app: the avatar opens the same menu in all ten products (Jamal, 5 Nov
189
+ 2026). New exports only; an app that passes nothing new renders exactly what
190
+ 0.8.0 rendered.
191
+
192
+ **Held back, as before:** `CommandPalette` and `NotificationCenter` under
193
+ Unreleased have still not passed their fidelity gate.
194
+
195
+ ### Added — `StandardAccountMenu`, the account menu behind every app's avatar, and `topBar.account` (B33-493)
196
+
197
+ The Hub's working menu (boost33-hub `src/app/account-menu.tsx`), moved into the
198
+ kit so every app draws the same one. It passed its fidelity gate on 5 Nov 2026
199
+ against a render of the Hub's own component.
200
+
201
+ What a consumer does:
202
+
203
+ ```tsx
204
+ import { AppShell, StandardAccountMenu } from '@boost33/ui'
205
+
206
+ <AppShell
207
+ topBar={{
208
+ actions: <NotificationButton … />,
209
+ account: (
210
+ <StandardAccountMenu
211
+ person={{ name: claims.name, email: claims.email, avatarUrl }}
212
+ org={orgName ? { name: orgName } : undefined}
213
+ role={role} // 'admin' | 'member' | null
214
+ identityUrl="https://id.boost33.com"
215
+ hubUrl="https://app.boost33.com"
216
+ otherAccountsCount={claims.accounts.length - 1}
217
+ />
218
+ ),
219
+ }}
220
+ …
221
+ />
222
+ ```
223
+
224
+ - **Safe from a Server Component layout.** Every prop is plain data. You pass no
225
+ `linkComponent` and no callback, so nothing crosses the client boundary as a
226
+ function. `AccountMenu` is a client component inside the package;
227
+ `StandardAccountMenu` is not.
228
+ - **Remove your own avatar** from `topBar.actions`. `topBar.account` (new on
229
+ `TopBar` and `AppShell`) draws after `actions`, at the far right of the bar.
230
+ - **The rows, the Hub's, with absolute links**, so they work from any app and
231
+ from the Hub itself:
232
+ - Profile, to `{hubUrl}/settings/profile`
233
+ - Security, to `{identityUrl}/account/security`
234
+ - Settings, to `{hubUrl}/settings/org/general`, described by role
235
+ - Switch account, to `{identityUrl}/choose-account`, only when
236
+ `otherAccountsCount` is above 0, described by `otherAccountNames` when every
237
+ account is named, or else by the count
238
+ - Feedback (`mailto:support@boost33.com`) and Docs (`https://docs.boost33.com`)
239
+ - Sign out, to `{identityUrl}/signout`
240
+ - **The Hub's production URL is `https://app.boost33.com`** (Identity's
241
+ `HUB_URL`). `hub.boost33.com` does not resolve.
242
+ - **The trigger is the Hub's**: a 26px avatar with the person's initials, or their
243
+ photo from `person.avatarUrl`, named "Your account". The panel opens 10px below
244
+ it, aligned to its right edge.
245
+ - **Keyboard and focus are the Hub's `useMenu`**: a WAI-ARIA menu button.
246
+ - Opening moves focus to the first row (ArrowUp on the trigger: the last).
247
+ - The arrows wrap; Home and End jump.
248
+ - Escape closes and returns focus to the avatar; Tab leaves and closes.
249
+ - A press outside closes it, and choosing a row closes it.
250
+ - **Words**: `labels` takes the rows' words (`StandardAccountMenuLabels`, English
251
+ defaults the Hub's); `menuLabels` takes the menu's accessible names.
252
+ - **Lower level**: `AccountMenu` takes `groups` of rows of your own, and
253
+ `standardAccountMenuGroups(options)` returns the standard rows as data to
254
+ extend. Do not add per-app rows without asking: one menu everywhere is the
255
+ point.
256
+
257
+ Changed from the Stage A `AccountMenu` that 0.8.0 exported but did not release:
258
+
259
+ - It now draws its own trigger and opens a `role="menu"`, not a static panel.
260
+ - `person.name`, `org` and `org.initials` are optional.
261
+ - The default accessible name is "Your account", not "Account".
262
+ - Rows are `menuitem`s with `tabIndex={-1}`.
263
+ - `AccountMenuIcon` gains `security`, `feedback` and `docs`.
264
+
265
+ ## 0.8.0 — 2026-11-05
266
+
267
+ **Breaking. Read the B33-1712 entry before you bump.** The 3 November 2026
268
+ shell redesign (B33-1710) in three parts: the sidebar and the switcher
269
+ (B33-1711), the top bar, the token chip and the bell (B33-1712), and the
270
+ sidebar action with every button on three tiers (B33-1713). The switcher and
271
+ the wallet chip move from the sidebar into the top bar, the bar stops drawing
272
+ `breadcrumb`, and every button the package draws changes look. A minor under
273
+ `0.x`, where a breaking change is allowed in one; the entries below are what
274
+ tell you what to do.
275
+
276
+ **The redesign passed its fidelity gate on 5 November 2026.** Jamal judged the
277
+ specimen on 0.8.0-next.5 and approved it. The three part entries below say "no
278
+ new baseline has been captured"; that was true when each landed and is not any
279
+ more: eleven designed baselines (the two re-captured `appswitcher-*` states and
280
+ nine `shell-redesign-*` states) were captured from the specimen at the gate and
281
+ are now gated, and the measurements are in the spec, "Shell redesign, recorded
282
+ at its gate, 5 November 2026". Repo-only: no import, prop or rendering changes.
283
+
284
+ The entries for `0.8.0-next.1` to `next.5` are kept below as they were written
285
+ for each prerelease. An app moving from `next.5` changes only its version pin;
286
+ an app moving from `0.7.1` reads all of them.
287
+
288
+ **Held back, as in 0.6.0 to 0.7.1:** the Stage A entries under Unreleased
289
+ (`CommandPalette`, `AccountMenu`, `NotificationCenter`) have still not passed
290
+ their fidelity gate and are not part of this release, even though their exports
291
+ are present in `dist/`.
33
292
 
34
293
  ### Changed — the shell redesign, part 1 of 3: the sidebar and the app switcher (B33-1711)
35
294
 
@@ -420,274 +679,6 @@ published without it, and a published version cannot be amended, so the first
420
679
  release to carry it is the one after 0.5.0. Repo-only otherwise: no import,
421
680
  prop or rendering changes.
422
681
 
423
- ### Added — `CommandPalette`, Stage A only. **Not releasable: it has not passed its fidelity gate** (B33-495)
424
-
425
- **Do not include this component in a release.** `CommandPalette` is exported from
426
- `src/index.ts` and will appear in `dist/`, and a consumer who finds it there and
427
- renders it is rendering an unapproved view. It is Stage A under `CLAUDE.md`'s
428
- presentational-first rule: static markup, reproduced from Paper's
429
- "CommandPalette — search" artboard, with **no state, no handlers and no data**.
430
- The Wallet never rendered a palette, so there is no captured referent and no
431
- approved measurement anywhere — `CLAUDE.md` is explicit that before the gate
432
- nothing is authoritative and Paper is only a starting drawing. It becomes
433
- releasable when a person opens the specimen route, approves it, captures the
434
- first *designed* baseline themselves, and writes the measurements they approved
435
- into the spec document in the same session. Until all three have happened, cut
436
- this out of the release or hold the release.
437
-
438
- What exists today, and what does not:
439
-
440
- | | |
441
- | --- | --- |
442
- | Draws | the search row, three result groups with their headings, the footer legend, and an empty state |
443
- | Takes | `groups`, `query`, `activeResultId`, `labels`, `linkComponent` |
444
- | Does **not** | open, close, trap focus, mark anything `inert`, register `⌘K`, dismiss on Escape or a backdrop click, or move a highlight |
445
-
446
- **It takes grouped results as props and owns no search index.** Pages, Products
447
- and Actions are all the host's, decided on B33-495 on 2026-10-06. There is no
448
- cross-suite index in any repo and this component does not imply one: a palette
449
- over one product's pages is a smaller promise than the artboard's description
450
- ("across the whole suite") makes, and that is accepted so the component is not
451
- held behind an architectural gap. Taking results as props is scope-agnostic —
452
- four products' results render exactly as one product's do — so a real index later
453
- changes what is passed in, not what is here.
454
-
455
- A result that navigates renders through `linkComponent` (ADR-0011); one with no
456
- `href` is an action and stays a `<button>`, with no handler until Stage B. Each
457
- result's glyph is the host's `icon`, because the groups are the host's and
458
- nothing here can map one to a glyph without learning what a host's group means.
459
- The markup is the combobox-over-listbox ADR-0010 settled for this component —
460
- `role="combobox"` on the input naming the highlighted row through
461
- `aria-activedescendant`, `role="listbox"` on the list, `role="option"` on each
462
- result, `role="group"` on each group — and that is markup, not traversal; what
463
- the arrow keys do is Stage B's to implement.
464
-
465
- Every user-facing string is a prop with an English default
466
- (`defaultCommandPaletteLabels`, exported and frozen), resolved through the same
467
- `Partial` + `undefined`-skipping merge `WalletChip` uses, so one missed
468
- translation key falls back to English rather than leaving the panel's only
469
- control unnamed (B33-873, B33-877, and the contract B33-1007 asks the package to
470
- converge on).
471
-
472
- **New token: `--container-command-palette: 560px`**, the panel's width, read off
473
- the artboard and recorded in `data/paper-tokens.json` as **provisional** — it is
474
- the drawing's number, not an approved measurement, and the person at the gate may
475
- move it. It is the only token this change adds; everything else draws on tokens
476
- that already existed.
477
-
478
- `TopBar` is unchanged. There is still no `⌘K` badge in the bar and still no
479
- `aria-haspopup` on the search button — both arrive with the panel, in Stage B.
480
-
481
- Not in `dist/` and not a consumer's business: the specimen route showing the four
482
- states, their `SPECIMEN_ONLY` records (none of them is screenshot-gated, because
483
- a *designed* baseline is the gate's to capture and never an agent's), and
484
- `tests/command-palette.test.tsx`.
485
- ### Added — `AccountMenu`, Stage A only, **not yet through its fidelity gate** (B33-493)
486
-
487
- > [!danger] Do not include this component in a release until a human has approved
488
- > it. `AccountMenu` has **not passed its fidelity gate.** Nobody has yet looked at
489
- > it beside a reference and said yes, no baseline exists for it, and no measurement
490
- > it draws with has been written into the spec. `CLAUDE.md` is explicit about what
491
- > that means for the three components the Wallet never rendered: "Before the gate,
492
- > nothing is authoritative", and the gate is where the referent is created rather
493
- > than checked. Until that session has happened — a person opening the specimen
494
- > route, judging the component, capturing the first baseline themselves, and
495
- > recording the approved measurements in the spec in the same pass — every number
496
- > this component renders is provisional and may change in a way that moves pixels
497
- > in your app. **If you are cutting a release, either hold this entry back or get
498
- > the gate done first.**
499
-
500
- The twelfth component. The account menu's **panel**: the signed-in person, the
501
- organisation they are acting in, and the rows into Settings and out of the
502
- session. Exported as `AccountMenu`, with `AccountMenuProps`,
503
- `AccountMenuPerson`, `AccountMenuOrg`, `AccountMenuGroup`, `AccountMenuItem`,
504
- `AccountMenuIcon`, `AccountMenuLabels` and `defaultAccountMenuLabels`.
505
-
506
- **It is the panel and nothing else — no trigger, no open state, no dismissal.**
507
- That is not an unfinished half of trigger-plus-panel; it is the part of this
508
- component the two reference drawings agree about. Paper's 004 Components artboard
509
- and its origin on 103 Identity are identical in every property of the panel, and
510
- they disagree about the anchor above it and about where the panel sits relative to
511
- that anchor. Choosing between them is the gate-holder's, so the component takes no
512
- anchor, does no positioning, and leaves `TopBar` untouched. Nothing in this release
513
- changes `TopBar`.
514
-
515
- **What a consumer does with it today: nothing.** Render it and you get a static
516
- panel. It holds no state, so it cannot be opened or closed, and it wires no
517
- handlers, so `Sign out` is a `<button>` that does nothing when pressed. Wiring it
518
- to `useAnchoredOverlay`, giving the anchor its `aria-expanded`, and deciding what
519
- each row does are Stage B, after the gate (`CLAUDE.md`, "Presentational-first").
520
-
521
- **The shape, so it is legible from here:**
522
-
523
- - `person` is `{ name, email, avatarUrl? }`. With no `avatarUrl` the circle draws
524
- bare, which is what the drawings show; with one, the image is decorative and
525
- takes an empty `alt`, because the person's name is on the line beside it.
526
- - `org` is `{ name, initials, role? }`. The initials are decorative; omitting
527
- `role` draws no pill rather than an empty one.
528
- - `groups` is a list of `{ id, items }`, and **a hairline is drawn above every
529
- group** — which is what separates the identity blocks from the first group as
530
- well as each group from the last. Grouped rather than an `items` array plus a
531
- named `footer`, because "footer" would be this package inventing a meaning for
532
- the second group.
533
- - an item is `{ id, label, description?, href?, icon? }`. **Whether a row
534
- navigates is decided by `href` and nothing else** (ADR-0011): with an `href` the
535
- row is a link and renders through `linkComponent`; without one it is an action
536
- and stays a `<button type="button">`. So the package never has to know which of
537
- Profile, Settings, Switch account and Sign out your product treats as a
538
- navigation. `icon` names one of four glyphs the package draws
539
- (`'person' | 'settings' | 'switch' | 'signOut'`); omit it and the glyph lane
540
- stays, empty, so the row labels still line up in one column.
541
- - `linkComponent` is the ADR-0011 escape hatch, unchanged from `NavItem` and
542
- `AppSwitcher`: an `ElementType` defaulting to `'a'`, receiving `href`,
543
- `className` and the row's `data-*`/`aria-*` through unchanged.
544
- - `labels` is `Partial<AccountMenuLabels>` and holds **one** field, `panel`,
545
- default `"Account"` — the panel's accessible name, which a disclosure panel has
546
- no other way to get (ADR-0010). Everything else a person reads is row data you
547
- supply, and therefore translates by construction.
548
-
549
- **The label contract is the `WalletChip`'s, deliberately and in full.** `labels`
550
- is a `Partial`, a field supplied as `undefined` (what a missed next-intl key looks
551
- like) falls back to English rather than blanking the panel's name, and
552
- `defaultAccountMenuLabels` is frozen with no type annotation so the freeze
553
- survives into the emitted `.d.ts`. That is three of this repo's own fixed bugs
554
- (B33-873, B33-877, B33-916) taken as the starting point rather than repeated.
555
- B33-1007 records `AppSwitcher` still diverging on the last of the three; this
556
- component does not add a fourth dialect, and the switcher's remaining divergence
557
- is unchanged here and still that ticket's.
558
-
559
- **A disclosure, not a menu** (ADR-0010): no widget role on the panel, no role on
560
- the rows, every row the real `<a>` or `<button>` it already is, so Tab walks them
561
- in the document's own order.
562
-
563
- **Three new tokens, all three read off an unapproved drawing.** They reach your
564
- app through `./style.css` and `./theme.css` like every other token, and the
565
- derivation comment on each says in as many words that the value is not settled:
566
-
567
- - `--container-account-menu: 312px` — the panel's width.
568
- - `--spacing-3_5: 14px` — the person block's block padding.
569
- - `--spacing-9_5: 38px` — the person block's avatar, the largest in the package.
570
-
571
- Nothing else in the package reads any of the three, so no existing rendering
572
- moves, and **no baseline moved**: all 27 gated states come back unchanged, because
573
- every token here is additive and no shared value was touched.
574
-
575
- **Four places where this does not reproduce its drawing**, each named at its rule
576
- in `AccountMenu.module.css`, and each because the drawn value is not a step this
577
- package's token set holds and the nearest step is within about a pixel — the trade
578
- `PageHeader` already records for `--leading-tight` against Paper's 30px. The row's
579
- glyph-to-text gap (11px drawn, `--spacing-2_5` here); the person block's
580
- name-to-email gap (3px drawn, `--spacing-0_5`, which is what the rows draw for the
581
- same relationship); two 10px type sizes (`--text-2xs`); and every line-height,
582
- because the drawing sets them in px and this package holds only percentages.
583
-
584
- **One open design question, recorded rather than decided.** This panel picks a row
585
- out with `--color-neutral-50`, where every other popover in the package uses
586
- `--color-surface-muted`, one neutral step darker. Reproduced as drawn and raised
587
- at the gate — the drawing was cloned from an Identity screen that predates the
588
- shared token pass, which is why it is the only popover in its own file still bound
589
- to literals.
590
-
591
- #### Not in `dist/`, and so not your concern
592
-
593
- The specimen gains an `AccountMenu` route with two sections, neither screenshot-gated
594
- and both recorded in `SPECIMEN_ONLY` with reasons: the panel, whose first baseline
595
- is a *designed* baseline only the person at the fidelity gate may capture, and a
596
- row rendered through a stand-in link component, which draws identically to the
597
- default by design. `tests/account-menu.test.tsx` adds 28 cases covering the
598
- structure, the row elements, the hairline-per-group rule, the label fallbacks and
599
- the ADR-0011 pass-through — none of them asserting geometry, which jsdom cannot
600
- see.
601
-
602
- ### Added — `NotificationCenter`, Stage A only, **not releasable yet** (B33-494)
603
-
604
- > [!danger] This component has not passed its fidelity gate and must not be
605
- > included in a release until it has.
606
- > `NotificationCenter` is exported from `src/index.ts`, which means it would be
607
- > published by any version cut from this state. Do not cut one. A human has not
608
- > yet opened the specimen route, judged the component, captured its first
609
- > baseline or written its measurements into the spec — and until all three happen
610
- > in one session, there is no referent for this component at all (`CLAUDE.md`,
611
- > "The three components the Wallet never rendered"). Nothing it draws is settled,
612
- > including the panel's 360px width and every space inside it.
613
- >
614
- > It is also only half built. Stage A is static: the panel renders and nothing
615
- > responds. The bell does not open it, Escape does not close it, the filters do
616
- > not filter and "Mark all as read" marks nothing. That is Stage B, and it lands
617
- > after the approval, not before.
618
-
619
- The panel behind the top bar's bell: everything the four products did, in one
620
- list, with the unread ones marked. Both states the design draws — populated and
621
- empty — ship from this package and appear in the specimen app.
622
-
623
- **It is presentational and it owns no feed.** The notifications arrive as a prop
624
- and this package neither fetches, stores nor subscribes to anything. There is no
625
- cross-product event source in any repo today, and a component that implied one
626
- would make the shared shell the place four services integrate — the opposite of
627
- the dependency direction every other component here has, and a straight
628
- contradiction of `CLAUDE.md`'s "it renders what it is given". That was decided on
629
- the ticket on 2026-10-06; the feed itself is separate work, outside 1.0.
630
-
631
- ```tsx
632
- <NotificationCenter
633
- items={[
634
- {
635
- id: 'n1',
636
- slug: 'wallet', // optional: selects the ProductMark
637
- title: 'Your granted tokens expire in 3 days',
638
- source: 'Wallet', // the host's words, not a slug
639
- time: '12 minutes ago', // preformatted by the host
640
- dateTime: '2026-10-06T09:48:00Z', // optional: the <time> element's datetime
641
- href: '/wallet/grants', // optional: a row with nowhere to go is not a link
642
- unread: true, // defaults to false
643
- },
644
- ]}
645
- viewAllHref="/notifications"
646
- />
647
- ```
648
-
649
- **The time arrives formatted, and that is a decision rather than an omission.**
650
- The obvious move is the one `WalletChip` makes for numbers — take a `locale` and
651
- format from a `Date` — and three things separate the two cases. A number is a
652
- pure function of its value and a locale; a *relative* time needs a `now`, so the
653
- component would read a clock and then either re-read it on a timer or go stale on
654
- screen, which is state in a component whose contract is that it has none. It
655
- would also make this component's own fidelity gate unreproducible: a designed
656
- baseline showing "12 minutes ago" from `Date.now()` is a different image an hour
657
- later. And the hard part is editorial — where a product cuts over from "12
658
- minutes ago" to "Yesterday" to a date is a policy each product may differ on, and
659
- `Intl.RelativeTimeFormat` does not decide it. The optional `dateTime` is how the
660
- machine-readable instant still reaches the page without this package deciding how
661
- to say it.
662
-
663
- **Every string is a prop with an English default**, in the same shape
664
- `WalletChipLabels` uses: `labels` is a `Partial`, a missing key falls back to
665
- English rather than blanking the UI (B33-873), `defaultNotificationCenterLabels`
666
- is frozen in the B33-916 spelling so writing to it is a compile error, and the
667
- one value-carrying string — the row's `{source} · {time}` line — is a template or
668
- a function, so a locale owns the word order and the separator. `AppSwitcher`'s
669
- contract still differs on the freeze; B33-1007 closes that by bringing the
670
- switcher to this shape, not by adding a fourth.
671
-
672
- The panel is a disclosure, not a menu and not an ARIA tab set (ADR-0010): no
673
- widget role, real links and buttons, and Tab is the document's own order, so the
674
- header action, both filters, every row and the footer link are each their own tab
675
- stop. Rows that navigate render through `linkComponent` (ADR-0011). The unread
676
- state is announced in words as well as drawn as a dot, because a colour reaches
677
- nobody on a screen reader.
678
-
679
- Three further things it deliberately does not do: it does not sort, filter, page
680
- or truncate `items`, because each is a decision about data it does not own; it
681
- draws no "0" on the unread filter, because a zero pill reads as a state; and the
682
- empty rendering drops the filters, the header action and the footer rather than
683
- offering three controls over an empty list. The empty state is not an edge case —
684
- a host with no feed wired yet has nothing to pass, which makes it the rendering
685
- most consumers meet first.
686
-
687
- **`TopBar` is unchanged.** Its bell's `unread` prop still defaults to `false`, it
688
- still gains no `aria-haspopup`, and it still opens nothing. Connecting the two is
689
- Stage B.
690
-
691
682
  ## 0.7.1 — 2026-11-02
692
683
 
693
684
  **Not breaking. Nothing to do on upgrade unless you want the link home**, which
@@ -6,13 +6,17 @@ import { AccountMenuLabels } from './labels';
6
6
  * menu reads no session and fetches nothing (CLAUDE.md).
7
7
  */
8
8
  export interface AccountMenuPerson {
9
- /** Their name, e.g. "Sofia Marques". */
10
- name: string;
9
+ /**
10
+ * Their name, e.g. "Sofia Marques". Optional: Identity may hold none. Without
11
+ * it the name line reads "You" (`labels.unnamed`) and the initials come from
12
+ * the email, as the Hub's do.
13
+ */
14
+ name?: string;
11
15
  /** Their email, on the line beneath. */
12
16
  email: string;
13
17
  /**
14
- * An avatar image. Omit it -- the drawing does -- and the circle draws bare, in
15
- * `--color-border-strong`.
18
+ * An avatar image, on the trigger and in the person block. Omit it and both
19
+ * circles carry the person's initials on `--color-neutral-300`, as the Hub's do.
16
20
  *
17
21
  * The image is decorative when it renders: it takes an empty `alt`, because the
18
22
  * person's name is on the line beside it and a name here would make a screen
@@ -27,8 +31,11 @@ export interface AccountMenuPerson {
27
31
  export interface AccountMenuOrg {
28
32
  /** The organisation's name, e.g. "Acme Coaching". */
29
33
  name: string;
30
- /** The initials on the tile beside it, e.g. "AC". Decorative -- the name is next to it. */
31
- initials: string;
34
+ /**
35
+ * The initials on the tile beside it, e.g. "AC". Decorative -- the name is next
36
+ * to it. Omit them and they are taken from the name.
37
+ */
38
+ initials?: string;
32
39
  /**
33
40
  * The role pill at the right of the strip, e.g. "Admin". Omit it and no pill is
34
41
  * drawn -- it is Identity's word for what this person is in this organisation,
@@ -81,8 +88,13 @@ export interface AccountMenuGroup {
81
88
  export interface AccountMenuProps {
82
89
  /** The signed-in person. */
83
90
  person: AccountMenuPerson;
84
- /** The organisation this person is acting in. */
85
- org: AccountMenuOrg;
91
+ /**
92
+ * The organisation this person is acting in. Omit it and no strip is drawn: a
93
+ * person on no membership would otherwise get an empty dark tile where the
94
+ * company goes, which reads as a company whose name failed to load (the Hub's
95
+ * reasoning).
96
+ */
97
+ org?: AccountMenuOrg;
86
98
  /** The row groups, in order. A hairline is drawn above each one. */
87
99
  groups: AccountMenuGroup[];
88
100
  /**
@@ -92,6 +104,11 @@ export interface AccountMenuProps {
92
104
  * looks like), keeps its default.
93
105
  */
94
106
  labels?: Partial<AccountMenuLabels>;
107
+ /**
108
+ * Draw the menu open on first render, without moving focus. For the specimen
109
+ * and for screenshots; an app leaves it unset.
110
+ */
111
+ defaultOpen?: boolean;
95
112
  /**
96
113
  * The element a row with an `href` renders through. Defaults to a plain `a`; a
97
114
  * router-owning consumer passes its own so the rows route client-side with the
@@ -101,31 +118,24 @@ export interface AccountMenuProps {
101
118
  linkComponent?: ElementType;
102
119
  }
103
120
  /**
104
- * The account menu: who you are, which organisation you are in, and the way into
105
- * Settings and out of the session (B33-493).
121
+ * The account menu: the avatar button in the top bar's end group, and the menu it
122
+ * opens -- who you are, which organisation you are in, the way into Profile,
123
+ * Security and Settings, Feedback and Docs, and the way out (B33-493).
106
124
  *
107
- * **This is Stage A, and this component is the panel alone.** It takes no
108
- * trigger, holds no open state and wires nothing. That is not a half-built
109
- * version of trigger-plus-panel -- it is the part of this component that the two
110
- * reference drawings agree about. Paper's 004 "AccountMenu — popover" and
111
- * Identity's 103 · 11 Account menu are identical in every property of the panel,
112
- * and they disagree about the anchor (004 draws a bell on a tile, an unread dot
113
- * and a halo behind the avatar; 103 draws none of the three) and about where the
114
- * panel sits relative to it (004: 8px below, 16px right inset, overhanging the
115
- * avatar by 4px; 103: 10px below, 20px inset, flush). CLAUDE.md puts that choice
116
- * with the person at the fidelity gate, and says plainly that an agent picking
117
- * one has invented a referent. So the anchor is not here, `TopBar` is untouched,
118
- * and the specimen positions the panel in its own stylesheet.
125
+ * **Stage B, and the referent is the Hub's working menu** (boost33-hub
126
+ * src/app/account-menu.tsx), named by Jamal on 5 Nov 2026 so that every app's
127
+ * avatar opens the same menu. The panel is Stage A's, which was drawn from the
128
+ * same Paper artboard the Hub built from (103 · 11); the trigger and the placement
129
+ * are the Hub's: a 26px avatar, the panel 10px below it and aligned to its right
130
+ * edge.
119
131
  *
120
- * A disclosure, like the switcher (ADR-0010): no widget role on the panel, no
121
- * role on the rows, every row the real `<a>` or `<button>` it already is, and Tab
122
- * the document's own order. Dismissal, focus restoration and the trigger's
123
- * `aria-expanded` are the shared anchored-overlay contract's (ADR-0009) and
124
- * arrive with the trigger in Stage B.
132
+ * A menu button, not a disclosure: `role="menu"` on the panel and
133
+ * `role="menuitem"` on every row, with the Hub's keyboard model (./use-menu.ts).
134
+ * Choosing a row closes the menu, so a client-side navigation does not leave it
135
+ * open over the page it just opened.
125
136
  *
126
- * Nothing here is authoritative. Every measurement in the stylesheet is read off
127
- * a drawing that no human has approved, and the gate is where it becomes a
128
- * referent -- at which point the approved baseline and the measurements written
129
- * into the spec are what binds, not Paper.
137
+ * Most apps want `StandardAccountMenu`, which builds the standard rows from an
138
+ * identity URL, a Hub URL and the person's role. This is the lower level: any
139
+ * rows, as groups.
130
140
  */
131
- export declare function AccountMenu({ person, org, groups, labels, linkComponent }: AccountMenuProps): import("react").JSX.Element;
141
+ export declare function AccountMenu({ person, org, groups, labels, defaultOpen, linkComponent }: AccountMenuProps): import("react").JSX.Element;