@boost33/ui 0.8.0-next.5 → 0.8.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.
Files changed (2) hide show
  1. package/CHANGELOG.md +613 -585
  2. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -31,662 +31,690 @@ Three components are built to Stage A on main and are not releasable until
31
31
  each passes its fidelity gate. Their entries wait here for the release that
32
32
  ships them.
33
33
 
34
- ### Changed — the shell redesign, part 1 of 3: the sidebar and the app switcher (B33-1711)
35
-
36
- The 3 Nov 2026 navigation redesign (B33-1710; Paper file Boost Products, page
37
- 101 Hub, row V0.7, boards 20 and 21). **This changes how every consumer's
38
- sidebar and switcher look, and every gated screenshot of them.** No new
39
- baseline has been captured: the person approving the redesign captures them
40
- (CLAUDE.md). Parts 2 and 3 (B33-1712, B33-1713) land in the same release.
41
-
42
- What a consumer sees, and what it has to do:
34
+ ### Added — `CommandPalette`, Stage A only. **Not releasable: it has not passed its fidelity gate** (B33-495)
43
35
 
44
- - **`AppShell` takes `appName`.** The product's name, drawn as plain text at
45
- the top of the sidebar with no icon. Optional: omit it and nothing draws
46
- there. Pass your product's display name, in your language. It names no
47
- landmark (`label` still does that).
48
- - **`AppSwitcher` no longer draws products that do not exist yet.** A row with
49
- `live: false` is left out, unless it is the current product. Keep passing the
50
- registry whole -- do not filter it yourself. A product the account does not
51
- hold (`entitled: false`) is still drawn, with its "Not on plan" tag: hiding it
52
- would tell a client their product does not exist. This overrules the
53
- Vocabulary document's "the switcher shows every product regardless of state",
54
- which is amended to match.
55
- - **The "Soon" tag is gone with them.** `labels.soon` stays in the type, now
56
- optional and deprecated, and is ignored: a labels object that sets it still
57
- compiles, and a catalogue typed as a full `AppSwitcherLabels` no longer has to
58
- carry it. Drop it when convenient.
59
- - **The switcher's account row is gone.** `account` is now optional and
60
- ignored, and `AppSwitcherAccount` is deprecated. The organisation moves to the
61
- top bar in part 2. Nothing breaks if you keep passing it; stop when
62
- convenient. Its `onSelect` is never called. In development, passing
63
- `account` logs a console warning once (`process.env.NODE_ENV` other than
64
- `production`); production builds are silent.
65
- - **New, optional, per product row:** `group` (a section header, e.g. "Your
66
- tools" -- rows are grouped in the order served; no groups means one plain
67
- list) and `trailing` (`{ kind: 'value', value: '8,940' }` or
68
- `{ kind: 'dot', label: '2 new' }` -- the dot's `label` is what a screen reader
69
- hears). Both are data you are served; the switcher fetches nothing.
70
- - **The switcher is redrawn to board 21.** A 32px tinted trigger with no border
71
- and the product name in the display face; a 360px borderless panel; a dark
72
- Hub row with a chevron; 28px product tiles; the current row semibold on a
73
- muted ground. The product marks are unchanged (the set approved 29 Oct 2026).
74
- - **Nav rows:** the active label is regular weight now, not semibold (the white
75
- row and full-ink colour carry "you are here"). Counts are 12px medium and
76
- right-aligned. `countUrgent` draws a filled pill instead of orange text --
77
- white on `--color-orange-600`, 5.18:1, because the old orange text was 3.45:1
78
- on the sidebar and failed AA. New `dot` on `NavItem` and `AppShellNavItem`: a
79
- 7px attention dot, its string read by a screen reader.
80
- - **Section labels** track at 0.09em (`--tracking-wider`), up from 0.05em.
81
- - **Three new component-private tokens** (in `style.css`, no Tailwind
82
- utility): `--container-app-switcher` (360px), `--spacing-2_75` (11px),
83
- `--spacing-1_75` (7px).
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.
84
48
 
85
- The 56px rail keeps working, icon-only. The redesign draws no rail, so it gets
86
- no new design.
49
+ What exists today, and what does not:
87
50
 
88
- Repo-only: the specimen's unarmed-switcher route is removed (the row it showed
89
- is gone), and a new "Shell redesign — B33-1710" route at the bottom of the
90
- specimen assembles boards 20 and 21 for review.
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 |
91
56
 
92
- ### Changed — the shell redesign, part 3 of 3: the sidebar action, and every button on three tiers (B33-1713)
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.
93
65
 
94
- Board 20 of the same redesign, and the consolidation its review asked for.
95
- **Every button the package draws changes look**; no new baseline has been
96
- captured (the person approving the redesign captures them).
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.
97
75
 
98
- What a consumer sees, and what it has to do:
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).
99
82
 
100
- - **New `AppShell.action`** (`AppShellAction`: `label`, optional `icon`, and
101
- `href` or `onClick`): your product's main action, drawn at the foot of the
102
- sidebar directly above the footer's divider -- board 20's "New scan". Near-black,
103
- full width, 40px. Not drawn with neither `href` nor `onClick`. On the rail it
104
- shows its icon alone, its label kept as its accessible name, and is not drawn
105
- if it has no icon. If you drew your own action button elsewhere in the shell,
106
- pass it here instead.
107
- - **The footer sits under a hairline when there is an action**, 10px above its
108
- rows, and its rows are 2px apart (the nav's own pitch), down from 16px.
109
- - **Three button tiers, written down in ADR-0016:** primary (filled, 40px, ink or
110
- accent), outlined (32px, white, 1px edge) and quiet (a word, no box). Every
111
- package button now takes one.
112
- - **`WalletChip`'s "Top up" is no longer orange text.** At rest it is the quiet
113
- tier in full ink; past the threshold it is the primary tier in the accent tone.
114
- The orange text was 3.69:1 on white and failed AA at 12px.
115
- - **The accent tone is `--color-orange-600`**, not the brand orange: white on the
116
- brand orange is 3.69:1. "Buy tokens" and the grown "Top up" both use it.
117
- Decided by Jamal on 4 Nov 2026.
118
- - **`NotificationCenter`'s "Mark all as read" and "View all"** are the quiet tier.
119
- The component is still Stage A and not releasable.
120
- - **If you pass `action`, add 67px to `consumerSlotsPx`** when you call
121
- `navBudgetForViewport` yourself: the 40px button, 16px clear above it, and the
122
- hairline and 10px the footer then takes. The development warning measures the
123
- real column and needs nothing.
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.
124
88
 
125
- Not done here, and why: board 20's Settings row carries an external-link arrow,
126
- which no ticket in Wave 4 asks for and `NavItem` has no slot for. It wants its
127
- own ticket.
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.
128
91
 
129
- Repo-only: `src/buttons.module.css` holds the tiers. The "Shell redesign —
130
- B33-1710" specimen route's shell gains "New scan" and a Settings / Help footer.
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 — `AccountMenu`, Stage A only, **not yet through its fidelity gate** (B33-493)
131
97
 
132
- ### Changed — the shell redesign, part 2 of 3: the top bar, the token chip, the bell (B33-1712)
98
+ > [!danger] Do not include this component in a release until a human has approved
99
+ > it. `AccountMenu` has **not passed its fidelity gate.** Nobody has yet looked at
100
+ > it beside a reference and said yes, no baseline exists for it, and no measurement
101
+ > it draws with has been written into the spec. `CLAUDE.md` is explicit about what
102
+ > that means for the three components the Wallet never rendered: "Before the gate,
103
+ > nothing is authoritative", and the gate is where the referent is created rather
104
+ > than checked. Until that session has happened — a person opening the specimen
105
+ > route, judging the component, capturing the first baseline themselves, and
106
+ > recording the approved measurements in the spec in the same pass — every number
107
+ > this component renders is provisional and may change in a way that moves pixels
108
+ > in your app. **If you are cutting a release, either hold this entry back or get
109
+ > the gate done first.**
133
110
 
134
- Boards 20, 21 and 22 of the same redesign. **This is a breaking layout change
135
- for every consumer of `AppShell`: the switcher and the wallet chip move out of
136
- the sidebar into the top bar, and the breadcrumb is no longer drawn.** Every
137
- gated screenshot of the shell, the top bar and the sidebar changes; no new
138
- baseline has been captured (the person approving the redesign captures them).
111
+ The twelfth component. The account menu's **panel**: the signed-in person, the
112
+ organisation they are acting in, and the rows into Settings and out of the
113
+ session. Exported as `AccountMenu`, with `AccountMenuProps`,
114
+ `AccountMenuPerson`, `AccountMenuOrg`, `AccountMenuGroup`, `AccountMenuItem`,
115
+ `AccountMenuIcon`, `AccountMenuLabels` and `defaultAccountMenuLabels`.
139
116
 
140
- What a consumer sees, and what it has to do:
117
+ **It is the panel and nothing else — no trigger, no open state, no dismissal.**
118
+ That is not an unfinished half of trigger-plus-panel; it is the part of this
119
+ component the two reference drawings agree about. Paper's 004 Components artboard
120
+ and its origin on 103 Identity are identical in every property of the panel, and
121
+ they disagree about the anchor above it and about where the panel sits relative to
122
+ that anchor. Choosing between them is the gate-holder's, so the component takes no
123
+ anchor, does no positioning, and leaves `TopBar` untouched. Nothing in this release
124
+ changes `TopBar`.
141
125
 
142
- - **The top bar has three regions.** Left: the Boost33 mark (linked home
143
- through `hubHref`, as the sidebar wordmark was) and the new `org` slot.
144
- Centre: "Switch Tools →" then the switcher, "Check Your Tokens →" then the
145
- token chip. Right: `topBar.actions` -- pass `NotificationButton` and your
146
- avatar there.
147
- - **`AppShell.switcher` and `AppShell.wallet` keep their names and move.**
148
- Both now render in the top bar's centre group, not the sidebar. Each label is
149
- drawn only when its control is passed. Pass `WalletChip` with
150
- `placement="topbar"` in `wallet`; the sidebar card is not designed for the
151
- bar.
152
- - **The sidebar no longer draws the wordmark, the switcher or the wallet
153
- chip.** It holds `appName`, the nav and `footer`, with 16px top and bottom
154
- padding of its own.
155
- - **New `AppShell.org`**: a slot for your organisation pill, beside the mark.
156
- The package draws no organisation menu (that is AccountMenu, B33-493, which
157
- has not passed its gate).
158
- - **`topBar` is optional, and `topBar.breadcrumb` is deprecated and not
159
- drawn.** It still compiles; stop passing it. `PageHeader` names the page.
160
- **If you put `SidebarToggle` in `topBar.breadcrumb` (the Wallet does), move it
161
- to the new `topBar.sidebarToggle` slot, or your rail loses its toggle.** The
162
- screenshot gate does not catch this: the bar passed its baseline at 0.19%
163
- with the toggle gone. So in development, passing `breadcrumb` -- to `TopBar`
164
- or through AppShell `topBar` -- logs a console warning once, naming where a
165
- toggle and an organisation switcher go instead; production builds are silent.
166
- - **New `topBar.sidebarToggle`** (and `TopBar.sidebarToggle`): your rail
167
- toggle, drawn at the far left of the bar before the mark. No board draws it;
168
- judged at the gate.
169
- `topBar.labels` (`TopBarLabels`, `defaultTopBarLabels`) translates the two
170
- centre labels.
171
- - **No search, and no `⌘K`.** Search was removed from the top bar on 3 Nov
172
- 2026 until it is built, and nothing replaces it. `CommandPalette` stays in the
173
- package, Stage A and unreleasable, with no trigger in the shell.
174
- - **New `WalletChip placement="topbar"`**: a 32px pill drawn like the
175
- switcher -- the balance in the mono face, a 44 x 6 two-segment bar, a
176
- chevron -- that opens a 340px popover with the balance, the bar at width,
177
- granted and purchased with what each means, your `burnNote`, an "Open" link
178
- (`walletHref`, `linkComponent`) and a "Buy tokens" button (`onTopUp`). A zero
179
- segment is drawn at 0px with the 2px gap kept, never as a sliver (B33-590).
180
- `unknown` shows an em dash, no bar and no split. The default
181
- `placement="sidebar"` card is unchanged.
182
- - **New `WalletChip totalOnly` with a signed `balance`** (top bar only, enforced
183
- by the type), for a balance the Ledger reports with a bucket below zero, which
184
- no two-segment bar can draw honestly. Below zero: the distance past zero in
185
- `--color-danger` with the words "past zero", as the Wallet's past-zero screen
186
- says it. At zero or above (a bucket below zero, a positive total -- decided by
187
- Jamal on 3 Nov 2026): the plain figure. Neither draws a bar or a
188
- granted / purchased legend. New labels: `pastZero`, `pastZeroLong`,
189
- `pastZeroBalance`. **No board draws this state; it is judged at the gate.**
190
- - **New labels for the popover:** `popover`, `grantedFigure`,
191
- `purchasedFigure`, `grantedNote`, `grantedNoteUndated`, `purchasedNote`,
192
- `open`, `buyTokens`. Each defaults to English.
193
- - **New `NotificationButton`**: the 32px bell for `topBar.actions`, with an
194
- `unread` dot. It fetches nothing and renders nothing without `onClick` or
195
- `href`. The NotificationCenter panel stays Stage A and is not wired.
196
- - **"Buy tokens" is white on `--color-orange-600`** (5.18:1), not the brand
197
- orange board 21 draws (3.69:1, fails AA at 13px). Decided by Jamal on 4 Nov
198
- 2026: orange that carries text is `--color-orange-600`.
199
- - **The top bar spans the full width,** above the sidebar and the product's
200
- column alike (board 20). Until now it sat over the content column only and
201
- the sidebar ran the full height.
202
- - **Two new component-private tokens:** `--container-token-popover` (340px),
203
- `--spacing-0_75` (3px).
204
- - **The nav height budget moves, because the sidebar's furniture moved.**
205
- `navBudgetForViewport(900)` and `checkNavHeightBudget`'s default budget rise
206
- from 629 to 730px: the top bar (48) is now counted above the sidebar, and the
207
- wordmark, switcher and wallet chip no longer take sidebar height. The default
208
- `consumerSlotsPx` is now 44 (the footer alone), down from 86 (switcher plus
209
- footer). If you pass your own `consumerSlotsPx`, drop your switcher's height
210
- from it. A 13-item nav that scrolled at 900px now fits; the dev warning in
211
- `Sidebar` follows the same numbers.
212
- - **The app name no longer shrinks** when the nav is long enough to scroll
213
- (it lost 2px of its 38).
126
+ **What a consumer does with it today: nothing.** Render it and you get a static
127
+ panel. It holds no state, so it cannot be opened or closed, and it wires no
128
+ handlers, so `Sign out` is a `<button>` that does nothing when pressed. Wiring it
129
+ to `useAnchoredOverlay`, giving the anchor its `aria-expanded`, and deciding what
130
+ each row does are Stage B, after the gate (`CLAUDE.md`, "Presentational-first").
214
131
 
215
- The 56px rail keeps working, icon-only, and gets no new design. The top bar
216
- does not collapse.
132
+ **The shape, so it is legible from here:**
217
133
 
218
- Repo-only: the specimen's "Shell redesign — B33-1710" route grows the top bar,
219
- the open token popover, board 22's nothing-purchased chip, and the unknown,
220
- past-zero and total-only chips.
221
-
222
- ### Added and fixed — the release candidate (0.8.0-next.5)
134
+ - `person` is `{ name, email, avatarUrl? }`. With no `avatarUrl` the circle draws
135
+ bare, which is what the drawings show; with one, the image is decorative and
136
+ takes an empty `alt`, because the person's name is on the line beside it.
137
+ - `org` is `{ name, initials, role? }`. The initials are decorative; omitting
138
+ `role` draws no pill rather than an empty one.
139
+ - `groups` is a list of `{ id, items }`, and **a hairline is drawn above every
140
+ group** — which is what separates the identity blocks from the first group as
141
+ well as each group from the last. Grouped rather than an `items` array plus a
142
+ named `footer`, because "footer" would be this package inventing a meaning for
143
+ the second group.
144
+ - an item is `{ id, label, description?, href?, icon? }`. **Whether a row
145
+ navigates is decided by `href` and nothing else** (ADR-0011): with an `href` the
146
+ row is a link and renders through `linkComponent`; without one it is an action
147
+ and stays a `<button type="button">`. So the package never has to know which of
148
+ Profile, Settings, Switch account and Sign out your product treats as a
149
+ navigation. `icon` names one of four glyphs the package draws
150
+ (`'person' | 'settings' | 'switch' | 'signOut'`); omit it and the glyph lane
151
+ stays, empty, so the row labels still line up in one column.
152
+ - `linkComponent` is the ADR-0011 escape hatch, unchanged from `NavItem` and
153
+ `AppSwitcher`: an `ElementType` defaulting to `'a'`, receiving `href`,
154
+ `className` and the row's `data-*`/`aria-*` through unchanged.
155
+ - `labels` is `Partial<AccountMenuLabels>` and holds **one** field, `panel`,
156
+ default `"Account"` — the panel's accessible name, which a disclosure panel has
157
+ no other way to get (ADR-0010). Everything else a person reads is row data you
158
+ supply, and therefore translates by construction.
223
159
 
224
- Every new prop and label key here is optional, so an app moving from next.4
225
- changes only its version pin.
160
+ **The label contract is the `WalletChip`'s, deliberately and in full.** `labels`
161
+ is a `Partial`, a field supplied as `undefined` (what a missed next-intl key looks
162
+ like) falls back to English rather than blanking the panel's name, and
163
+ `defaultAccountMenuLabels` is frozen with no type annotation so the freeze
164
+ survives into the emitted `.d.ts`. That is three of this repo's own fixed bugs
165
+ (B33-873, B33-877, B33-916) taken as the starting point rather than repeated.
166
+ B33-1007 records `AppSwitcher` still diverging on the last of the three; this
167
+ component does not add a fourth dialect, and the switcher's remaining divergence
168
+ is unchanged here and still that ticket's.
226
169
 
227
- - **French figures show their grouping space.** CLDR groups French thousands
228
- with a narrow no-break space (U+202F), and the kit's faces draw it at almost
229
- nothing: 1.9px in Inter at 14px, 4.1px in Outfit at 40px. So the token
230
- popover read "1500" while the chip read "1 500" (Brokers). Every figure the
231
- chip formats (`formatTokens`, the rail's short form, the threshold
232
- percentage) now uses an ordinary no-break space (U+00A0) instead. It is
233
- visible in every face and still never wraps. en-US output is unchanged.
234
- If your tests compare against `Intl.NumberFormat('fr-FR')` output, apply the
235
- same swap.
236
- - **`NotificationButton`** (Content):
237
- - `unreadCount?: number` goes into the accessible name ("Notifications,
238
- 3 unread", new optional label `unreadCount`, `%count%`). Above 0 it draws
239
- the dot. The figure itself is not drawn: board 20 draws a dot.
240
- - `expanded?: boolean` and `controls?: string` become `aria-expanded` and
241
- `aria-controls` when it opens a panel. Button form only.
242
- - `ref` reaches the rendered button or link (React 19 passes `ref` as a
243
- prop; the link form needs a `linkComponent` that forwards it, as next/link
244
- does).
245
- - **`AppShellAction.active?: boolean`** (Off Market): on the action's own page,
246
- the link form carries `aria-current="page"` and both forms keep the primary
247
- tier's pressed ground (`.primary[data-active='true']`, its hover colour, in
248
- either tone).
249
- - **`countLabel?: string`** on `NavItem` and `AppShellNavItem` (Off Market):
250
- what a screen reader hears for `count` ("3 à ouvrir"). Given, the visible
251
- number is hidden from assistive tech and this is read instead.
252
- - **`attrs?: NavItemAttrs`** on `NavItem` and `AppShellNavItem` (Outbound):
253
- `data-*` attributes for the row, such as a tour's `data-tour`. Only `data-*`
254
- names are accepted (also filtered at runtime), and the row's own
255
- `data-active`/`data-collapsed` win. New exported type `NavItemAttrs`.
256
- - **README: two consumer notes.**
257
- - Map the `--boost33-font-*` hooks on `:root`, with `next/font`'s variable
258
- classes on `<html>`. A mapping on `body` never reaches the kit (Outbound).
259
- - **Never pass a function from a Server Component to `AppSwitcher` or
260
- `WalletChip`**, the package's client components: `linkComponent`,
261
- `onTopUp`, `onOpenChange`. Next refuses it and every signed-in render 500s
262
- (Passport), and `renderToStaticMarkup` tests cannot see it. Render those
263
- two inside a `'use client'` file of your own. The `linkComponent` docs on
264
- both now say so.
265
- - No approved screenshot moves (see the commit for the comparison).
170
+ **A disclosure, not a menu** (ADR-0010): no widget role on the panel, no role on
171
+ the rows, every row the real `<a>` or `<button>` it already is, so Tab walks them
172
+ in the document's own order.
266
173
 
267
- ### Fixed — the top bar fits at 1024px; the token popover stays on screen (0.8.0-next.4)
174
+ **Three new tokens, all three read off an unapproved drawing.** They reach your
175
+ app through `./style.css` and `./theme.css` like every other token, and the
176
+ derivation comment on each says in as many words that the value is not settled:
268
177
 
269
- - **Below 80rem of bar, the centre group's two labels leave the screen.**
270
- next.3 stopped the regions overlapping, but the centre does not shrink, so at
271
- 1024px Content's bar still needed 1067px and clipped the avatar. Now
272
- "Switch Tools →" and "Check Your Tokens →" are hidden visually wherever the
273
- bar's content box is under 80rem (1280px; a full-width bar in a window under
274
- 1344px) and the centre gives back about 280px. They stay in the
275
- accessibility tree, before the control each introduces, so a screen reader
276
- hears the same thing at every width. A container query on the bar
277
- (`container: boost33-top-bar / inline-size`), so a bar narrower than the
278
- window is measured by its own width. Measured in Chromium at 1024px with a
279
- 256px start group and either a 70px or a 220px actions group: no overflow and
280
- no overlap. If you write container queries in the bar's slots, name your own
281
- container: the bar is now a container.
282
- - **The token popover stays on screen.** It hangs from the pill's left edge and
283
- is 340px wide. At 390px its right edge was at 535px, cutting the threshold
284
- line and the reset note, and at 1024px it overshot by 8px. It now slides left
285
- by exactly its overshoot to keep 16px from the right edge (never past 16px
286
- from the left), and is never wider than the viewport less 32px. Both the
287
- split chip and `totalOnly` do this. It is measured before paint and again on
288
- resize.
289
- - **Below `chromeFrom` the shell fills the screen**: `min-height: 100dvh`
290
- instead of next.3's `100%`, which resolved against app wrappers with no
291
- height and left short pages ending grey (Content).
292
- - **No approved screenshot moves**: all 40 gated states measure the same with
293
- and without these changes, each on a fresh specimen build.
178
+ - `--container-account-menu: 312px` — the panel's width.
179
+ - `--spacing-3_5: 14px` — the person block's block padding.
180
+ - `--spacing-9_5: 38px` — the person block's avatar, the largest in the package.
294
181
 
295
- ### Fixed — the top-bar token chip warns again past 80% of the grant; the top bar holds its layout (0.8.0-next.3)
182
+ Nothing else in the package reads any of the three, so no existing rendering
183
+ moves, and **no baseline moved**: all 27 gated states come back unchanged, because
184
+ every token here is additive and no shared value was touched.
296
185
 
297
- - **The 80% threshold is back in the top bar.** 0.8.0-next.0 moved the chip to
298
- the top bar without its threshold state, so a seller who had burned 87% of the
299
- month's grant saw the same chip as one at 10%, and nothing here said so
300
- (found by Score). `placement="topbar"` now reads `grantCycle` and
301
- `burnedFraction` exactly as the sidebar card does (the consumer's
302
- `burnedFraction` wins, otherwise it is derived from `grantCycle`, and with
303
- neither nothing is claimed). Past 80%:
304
- - with `grantCycle`, the bar in the chip and in the popover measures against
305
- what the cycle started with, so it shortens and the burned share shows as
306
- the warning-tinted track: the sidebar card's drawing (Jamal, 4 Nov 2026).
307
- With `burnedFraction` alone there is no honest denominator, so the bar
308
- stays full;
309
- - the popover shows a line above the runway note, and the chip's accessible
310
- name ends with it, so the warning is never colour alone. **New label
311
- `thresholdNote`**, a template with `%percent%`, formatted for `locale`
312
- ("87%", "87 %"). Default `You've used %percent% of this month's grant.`
313
- A catalogue typed as a full `WalletChipLabels` must add it; that is the
314
- typecheck catching a missing translation;
315
- - the root carries `data-threshold="true"`. Buy tokens is unchanged: the
316
- accent primary.
317
- Keep passing `grantCycle` (or `burnedFraction`) to the top-bar chip.
318
- - **The top bar keeps its regions apart at any width.** At 1024px the start
319
- group (mark and organisation pill) was given less than it held and painted
320
- across "Switch Tools →" (Content, Wallet, Activity, Outbound). The bar is now
321
- a three-column grid whose sides are never narrower than their contents: where
322
- everything fits, the centre group sits in the middle as before; where it does
323
- not, the centre moves off the middle instead of under the pill. Below that,
324
- on a phone with no `chromeFrom`, the bar runs off the right edge in order
325
- rather than piling up (Score). **No approved screenshot moves**: every gated
326
- state measures the same pixel count with and without the change.
327
- - **The top bar is a `<header>`**, the page's banner landmark, so assistive tech
328
- can jump to it and a test can scope to it (Score). `getByRole('banner')`
329
- finds it. The census in `scripts/border-proof.ts` records the bar's hairline on
330
- a `header` now; the pixels did not move.
331
- - **The popover's reset note ends on the date.** `grantedNote` defaults to
332
- `Included each month · resets %date%`, without the `Resets %date%.`
333
- full stop that printed "30 sept.." for a French date (Score). If you
334
- translate it, keep punctuation off the end of `%date%`. `grantExpires` is one
335
- date in both placements, the day this month's grant runs out and the next
336
- lands. The sidebar says it as an expiry, the popover as a reset.
337
- - **Below `chromeFrom` the shell still fills its root** (`min-height: 100%`),
338
- so a short page keeps the white ground to the bottom of the screen (Content).
339
- Replaced by `100dvh` in next.4.
186
+ **Four places where this does not reproduce its drawing**, each named at its rule
187
+ in `AccountMenu.module.css`, and each because the drawn value is not a step this
188
+ package's token set holds and the nearest step is within about a pixel — the trade
189
+ `PageHeader` already records for `--leading-tight` against Paper's 30px. The row's
190
+ glyph-to-text gap (11px drawn, `--spacing-2_5` here); the person block's
191
+ name-to-email gap (3px drawn, `--spacing-0_5`, which is what the rows draw for the
192
+ same relationship); two 10px type sizes (`--text-2xs`); and every line-height,
193
+ because the drawing sets them in px and this package holds only percentages.
340
194
 
341
- ### Added — `AppShell` gets out of the way on a phone: `chromeFrom` and `phoneChrome`
195
+ **One open design question, recorded rather than decided.** This panel picks a row
196
+ out with `--color-neutral-50`, where every other popover in the package uses
197
+ `--color-surface-muted`, one neutral step darker. Reproduced as drawn and raised
198
+ at the gate — the drawing was cloned from an Identity screen that predates the
199
+ shared token pass, which is why it is the only popover in its own file still bound
200
+ to literals.
342
201
 
343
- AppShell has no phone layout, and the 0.8 top bar runs off a 390px screen.
344
- Products keep their own phone chrome below a breakpoint, and until now the only
345
- ways to do that were a `matchMedia` switch (the desktop shell flashes on a phone
346
- before it swaps) or CSS reaching into the shell's DOM shape. Both options are opt-in;
347
- a shell that sets neither renders exactly as before.
202
+ #### Not in `dist/`, and so not your concern
348
203
 
349
- - **`chromeFrom?: 'md' | 'lg' | 'xl'`** (new type `AppShellBreakpoint`). Below
350
- that viewport width the shell hides its own top bar and sidebar, the page
351
- body takes the full width, and the document scrolls instead of the shell's
352
- content box. Done in the shell's own `@media` CSS, so the first paint is
353
- already right, with no flash and no second render of the page. The widths are
354
- Tailwind v4's defaults -- `md` 48rem (768px), `lg` 64rem (1024px), `xl` 80rem
355
- (1280px) -- so `lg` here and `lg:` in your own markup flip at the same pixel.
356
- It is a name and not a number because a media query cannot read a value
357
- passed at runtime.
358
- - **`phoneChrome?: ReactNode`**: your phone header and navigation. The shell
359
- draws it above the page body, outside `<main>`, and shows it only below
360
- `chromeFrom`. Ignored without `chromeFrom`. The shell's wrapper around it
361
- draws no box (`display: contents`), so a `position: sticky` header you pass
362
- stays pinned while the page scrolls (fixed after 0.8.0-next.1, where it
363
- scrolled away). Pass your phone chrome here and
364
- drop any `lg:hidden` and any selector that hid the kit's bar or `<aside>` by
365
- position: those depended on a DOM shape this package does not promise.
366
- - Not a mobile shell. Nothing is redesigned below the breakpoint; the shell only
367
- stops drawing its desktop chrome. A phone design needs a board first.
368
- - The sidebar's development-only nav-height warning no longer fires when the
369
- sidebar is hidden this way.
204
+ The specimen gains an `AccountMenu` route with two sections, neither screenshot-gated
205
+ and both recorded in `SPECIMEN_ONLY` with reasons: the panel, whose first baseline
206
+ is a *designed* baseline only the person at the fidelity gate may capture, and a
207
+ row rendered through a stand-in link component, which draws identically to the
208
+ default by design. `tests/account-menu.test.tsx` adds 28 cases covering the
209
+ structure, the row elements, the hairline-per-group rule, the label fallbacks and
210
+ the ADR-0011 pass-through — none of them asserting geometry, which jsdom cannot
211
+ see.
370
212
 
371
- ### Note — in a Tailwind v4 app, `style.css` alone re-sizes the app's own utilities
213
+ ### Added — `NotificationCenter`, Stage A only, **not releasable yet** (B33-494)
372
214
 
373
- **Importing `@boost33/ui/style.css` changes what a Tailwind v4 app already has
374
- on screen, with or without `theme.css`.** Off Market and Brokers measured it
375
- adopting 0.8.0-next.0. This is not new in 0.8, but 0.8 is when Tailwind v4
376
- apps first take the shell.
215
+ > [!danger] This component has not passed its fidelity gate and must not be
216
+ > included in a release until it has.
217
+ > `NotificationCenter` is exported from `src/index.ts`, which means it would be
218
+ > published by any version cut from this state. Do not cut one. A human has not
219
+ > yet opened the specimen route, judged the component, captured its first
220
+ > baseline or written its measurements into the spec — and until all three happen
221
+ > in one session, there is no referent for this component at all (`CLAUDE.md`,
222
+ > "The three components the Wallet never rendered"). Nothing it draws is settled,
223
+ > including the panel's 360px width and every space inside it.
224
+ >
225
+ > It is also only half built. Stage A is static: the panel renders and nothing
226
+ > responds. The bell does not open it, Escape does not close it, the filters do
227
+ > not filter and "Mark all as read" marks nothing. That is Stage B, and it lands
228
+ > after the approval, not before.
377
229
 
378
- Why: `style.css` carries `tokens.css`, which declares this package's tokens on
379
- `:root`, unlayered, under the names Tailwind v4 itself uses (`--text-*`,
380
- `--leading-*`, `--tracking-*`, `--radius-*`, `--font-*`, `--font-weight-*`,
381
- `--color-*`). Tailwind's default utilities read those names through `var()`
382
- (`.text-sm { font-size: var(--text-sm) }`), and an unlayered declaration beats
383
- Tailwind's `@layer theme`. So in the app, `text-sm` becomes 13px (Tailwind's
384
- 14px) with a 13px line height, `text-base` 15px (16px), and `rounded-sm`, `-md`,
385
- `-lg`, `-xl`, `-2xl` become 3/4/6/8/12px (4/6/8/12/16px). The `leading-*` and
386
- `tracking-*` steps move too.
230
+ The panel behind the top bar's bell: everything the four products did, in one
231
+ list, with the unread ones marked. Both states the design draws — populated and
232
+ empty — ship from this package and appear in the specimen app.
387
233
 
388
- To keep your own scale, tell Tailwind to write literal values into its
389
- utilities so they never read `:root`. Either import Tailwind's theme inline:
234
+ **It is presentational and it owns no feed.** The notifications arrive as a prop
235
+ and this package neither fetches, stores nor subscribes to anything. There is no
236
+ cross-product event source in any repo today, and a component that implied one
237
+ would make the shared shell the place four services integrate — the opposite of
238
+ the dependency direction every other component here has, and a straight
239
+ contradiction of `CLAUDE.md`'s "it renders what it is given". That was decided on
240
+ the ticket on 2026-10-06; the feed itself is separate work, outside 1.0.
390
241
 
391
- ```css
392
- @layer theme, base, components, utilities;
393
- @import "tailwindcss/theme.css" layer(theme) theme(inline);
394
- @import "tailwindcss/preflight.css" layer(base);
395
- @import "tailwindcss/utilities.css" layer(utilities);
396
- @import "@boost33/ui/style.css";
242
+ ```tsx
243
+ <NotificationCenter
244
+ items={[
245
+ {
246
+ id: 'n1',
247
+ slug: 'wallet', // optional: selects the ProductMark
248
+ title: 'Your granted tokens expire in 3 days',
249
+ source: 'Wallet', // the host's words, not a slug
250
+ time: '12 minutes ago', // preformatted by the host
251
+ dateTime: '2026-10-06T09:48:00Z', // optional: the <time> element's datetime
252
+ href: '/wallet/grants', // optional: a row with nowhere to go is not a link
253
+ unread: true, // defaults to false
254
+ },
255
+ ]}
256
+ viewAllHref="/notifications"
257
+ />
397
258
  ```
398
259
 
399
- or pin each colliding name you use under `@theme inline { … }` with Tailwind's
400
- value. Either way the kit's components keep reading `:root` and render as
401
- designed. Brokers measured identical computed styles on all 12 of its pages
402
- with the first recipe.
260
+ **The time arrives formatted, and that is a decision rather than an omission.**
261
+ The obvious move is the one `WalletChip` makes for numbers — take a `locale` and
262
+ format from a `Date` — and three things separate the two cases. A number is a
263
+ pure function of its value and a locale; a *relative* time needs a `now`, so the
264
+ component would read a clock and then either re-read it on a timer or go stale on
265
+ screen, which is state in a component whose contract is that it has none. It
266
+ would also make this component's own fidelity gate unreproducible: a designed
267
+ baseline showing "12 minutes ago" from `Date.now()` is a different image an hour
268
+ later. And the hard part is editorial — where a product cuts over from "12
269
+ minutes ago" to "Yesterday" to a date is a policy each product may differ on, and
270
+ `Intl.RelativeTimeFormat` does not decide it. The optional `dateTime` is how the
271
+ machine-readable instant still reaches the page without this package deciding how
272
+ to say it.
403
273
 
404
- `theme.css` is a separate, optional step on top of this. It adds utilities for
405
- names only this package has (`bg-surface`, `border-border`, …) and makes the
406
- kit's values the app's whole scale (ADR-0011, ADR-0012). Every kit component
407
- renders correctly without it (the specimen imports no `theme.css`). An app that
408
- used those utilities and drops `theme.css` loses them.
274
+ **Every string is a prop with an English default**, in the same shape
275
+ `WalletChipLabels` uses: `labels` is a `Partial`, a missing key falls back to
276
+ English rather than blanking the UI (B33-873), `defaultNotificationCenterLabels`
277
+ is frozen in the B33-916 spelling so writing to it is a compile error, and the
278
+ one value-carrying string — the row's `{source} · {time}` line — is a template or
279
+ a function, so a locale owns the word order and the separator. `AppSwitcher`'s
280
+ contract still differs on the freeze; B33-1007 closes that by bringing the
281
+ switcher to this shape, not by adding a fourth.
409
282
 
410
- The shell also sets `font-family: var(--font-sans)` (Inter) on its frame, so
411
- `children` inherit Inter unless the page sets its own family.
283
+ The panel is a disclosure, not a menu and not an ARIA tab set (ADR-0010): no
284
+ widget role, real links and buttons, and Tab is the document's own order, so the
285
+ header action, both filters, every row and the footer link are each their own tab
286
+ stop. Rows that navigate render through `linkComponent` (ADR-0011). The unread
287
+ state is announced in words as well as drawn as a dot, because a colour reaches
288
+ nobody on a screen reader.
412
289
 
413
- ### Changed — `CHANGELOG.md` now ships inside the package
290
+ Three further things it deliberately does not do: it does not sort, filter, page
291
+ or truncate `items`, because each is a decision about data it does not own; it
292
+ draws no "0" on the unread filter, because a zero pill reads as a state; and the
293
+ empty rendering drops the filters, the header action and the footer rather than
294
+ offering three controls over an empty list. The empty state is not an edge case —
295
+ a host with no feed wired yet has nothing to pass, which makes it the rendering
296
+ most consumers meet first.
414
297
 
415
- `package.json` `files` adds `CHANGELOG.md` next to `dist`, so this file is in
416
- the tarball at `node_modules/@boost33/ui/CHANGELOG.md`. Until now it reached a
417
- consumer only through the repository, which is a strange place to keep the one
418
- notice a consumer gets. Nothing in `dist/` changes. 0.5.0 and earlier were
419
- published without it, and a published version cannot be amended, so the first
420
- release to carry it is the one after 0.5.0. Repo-only otherwise: no import,
421
- prop or rendering changes.
298
+ **`TopBar` is unchanged.** Its bell's `unread` prop still defaults to `false`, it
299
+ still gains no `aria-haspopup`, and it still opens nothing. Connecting the two is
300
+ Stage B.
422
301
 
423
- ### Added — `CommandPalette`, Stage A only. **Not releasable: it has not passed its fidelity gate** (B33-495)
302
+ ## 0.8.0 — 2026-11-05
303
+
304
+ **Breaking. Read the B33-1712 entry before you bump.** The 3 November 2026
305
+ shell redesign (B33-1710) in three parts: the sidebar and the switcher
306
+ (B33-1711), the top bar, the token chip and the bell (B33-1712), and the
307
+ sidebar action with every button on three tiers (B33-1713). The switcher and
308
+ the wallet chip move from the sidebar into the top bar, the bar stops drawing
309
+ `breadcrumb`, and every button the package draws changes look. A minor under
310
+ `0.x`, where a breaking change is allowed in one; the entries below are what
311
+ tell you what to do.
312
+
313
+ **The redesign passed its fidelity gate on 5 November 2026.** Jamal judged the
314
+ specimen on 0.8.0-next.5 and approved it. The three part entries below say "no
315
+ new baseline has been captured"; that was true when each landed and is not any
316
+ more: eleven designed baselines (the two re-captured `appswitcher-*` states and
317
+ nine `shell-redesign-*` states) were captured from the specimen at the gate and
318
+ are now gated, and the measurements are in the spec, "Shell redesign, recorded
319
+ at its gate, 5 November 2026". Repo-only: no import, prop or rendering changes.
320
+
321
+ The entries for `0.8.0-next.1` to `next.5` are kept below as they were written
322
+ for each prerelease. An app moving from `next.5` changes only its version pin;
323
+ an app moving from `0.7.1` reads all of them.
324
+
325
+ **Held back, as in 0.6.0 to 0.7.1:** the Stage A entries under Unreleased
326
+ (`CommandPalette`, `AccountMenu`, `NotificationCenter`) have still not passed
327
+ their fidelity gate and are not part of this release, even though their exports
328
+ are present in `dist/`.
424
329
 
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.
330
+ ### Changed — the shell redesign, part 1 of 3: the sidebar and the app switcher (B33-1711)
437
331
 
438
- What exists today, and what does not:
332
+ The 3 Nov 2026 navigation redesign (B33-1710; Paper file Boost Products, page
333
+ 101 Hub, row V0.7, boards 20 and 21). **This changes how every consumer's
334
+ sidebar and switcher look, and every gated screenshot of them.** No new
335
+ baseline has been captured: the person approving the redesign captures them
336
+ (CLAUDE.md). Parts 2 and 3 (B33-1712, B33-1713) land in the same release.
439
337
 
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 |
338
+ What a consumer sees, and what it has to do:
339
+
340
+ - **`AppShell` takes `appName`.** The product's name, drawn as plain text at
341
+ the top of the sidebar with no icon. Optional: omit it and nothing draws
342
+ there. Pass your product's display name, in your language. It names no
343
+ landmark (`label` still does that).
344
+ - **`AppSwitcher` no longer draws products that do not exist yet.** A row with
345
+ `live: false` is left out, unless it is the current product. Keep passing the
346
+ registry whole -- do not filter it yourself. A product the account does not
347
+ hold (`entitled: false`) is still drawn, with its "Not on plan" tag: hiding it
348
+ would tell a client their product does not exist. This overrules the
349
+ Vocabulary document's "the switcher shows every product regardless of state",
350
+ which is amended to match.
351
+ - **The "Soon" tag is gone with them.** `labels.soon` stays in the type, now
352
+ optional and deprecated, and is ignored: a labels object that sets it still
353
+ compiles, and a catalogue typed as a full `AppSwitcherLabels` no longer has to
354
+ carry it. Drop it when convenient.
355
+ - **The switcher's account row is gone.** `account` is now optional and
356
+ ignored, and `AppSwitcherAccount` is deprecated. The organisation moves to the
357
+ top bar in part 2. Nothing breaks if you keep passing it; stop when
358
+ convenient. Its `onSelect` is never called. In development, passing
359
+ `account` logs a console warning once (`process.env.NODE_ENV` other than
360
+ `production`); production builds are silent.
361
+ - **New, optional, per product row:** `group` (a section header, e.g. "Your
362
+ tools" -- rows are grouped in the order served; no groups means one plain
363
+ list) and `trailing` (`{ kind: 'value', value: '8,940' }` or
364
+ `{ kind: 'dot', label: '2 new' }` -- the dot's `label` is what a screen reader
365
+ hears). Both are data you are served; the switcher fetches nothing.
366
+ - **The switcher is redrawn to board 21.** A 32px tinted trigger with no border
367
+ and the product name in the display face; a 360px borderless panel; a dark
368
+ Hub row with a chevron; 28px product tiles; the current row semibold on a
369
+ muted ground. The product marks are unchanged (the set approved 29 Oct 2026).
370
+ - **Nav rows:** the active label is regular weight now, not semibold (the white
371
+ row and full-ink colour carry "you are here"). Counts are 12px medium and
372
+ right-aligned. `countUrgent` draws a filled pill instead of orange text --
373
+ white on `--color-orange-600`, 5.18:1, because the old orange text was 3.45:1
374
+ on the sidebar and failed AA. New `dot` on `NavItem` and `AppShellNavItem`: a
375
+ 7px attention dot, its string read by a screen reader.
376
+ - **Section labels** track at 0.09em (`--tracking-wider`), up from 0.05em.
377
+ - **Three new component-private tokens** (in `style.css`, no Tailwind
378
+ utility): `--container-app-switcher` (360px), `--spacing-2_75` (11px),
379
+ `--spacing-1_75` (7px).
380
+
381
+ The 56px rail keeps working, icon-only. The redesign draws no rail, so it gets
382
+ no new design.
383
+
384
+ Repo-only: the specimen's unarmed-switcher route is removed (the row it showed
385
+ is gone), and a new "Shell redesign — B33-1710" route at the bottom of the
386
+ specimen assembles boards 20 and 21 for review.
445
387
 
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.
388
+ ### Changed — the shell redesign, part 3 of 3: the sidebar action, and every button on three tiers (B33-1713)
454
389
 
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.
390
+ Board 20 of the same redesign, and the consolidation its review asked for.
391
+ **Every button the package draws changes look**; no new baseline has been
392
+ captured (the person approving the redesign captures them).
464
393
 
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).
394
+ What a consumer sees, and what it has to do:
471
395
 
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.
396
+ - **New `AppShell.action`** (`AppShellAction`: `label`, optional `icon`, and
397
+ `href` or `onClick`): your product's main action, drawn at the foot of the
398
+ sidebar directly above the footer's divider -- board 20's "New scan". Near-black,
399
+ full width, 40px. Not drawn with neither `href` nor `onClick`. On the rail it
400
+ shows its icon alone, its label kept as its accessible name, and is not drawn
401
+ if it has no icon. If you drew your own action button elsewhere in the shell,
402
+ pass it here instead.
403
+ - **The footer sits under a hairline when there is an action**, 10px above its
404
+ rows, and its rows are 2px apart (the nav's own pitch), down from 16px.
405
+ - **Three button tiers, written down in ADR-0016:** primary (filled, 40px, ink or
406
+ accent), outlined (32px, white, 1px edge) and quiet (a word, no box). Every
407
+ package button now takes one.
408
+ - **`WalletChip`'s "Top up" is no longer orange text.** At rest it is the quiet
409
+ tier in full ink; past the threshold it is the primary tier in the accent tone.
410
+ The orange text was 3.69:1 on white and failed AA at 12px.
411
+ - **The accent tone is `--color-orange-600`**, not the brand orange: white on the
412
+ brand orange is 3.69:1. "Buy tokens" and the grown "Top up" both use it.
413
+ Decided by Jamal on 4 Nov 2026.
414
+ - **`NotificationCenter`'s "Mark all as read" and "View all"** are the quiet tier.
415
+ The component is still Stage A and not releasable.
416
+ - **If you pass `action`, add 67px to `consumerSlotsPx`** when you call
417
+ `navBudgetForViewport` yourself: the 40px button, 16px clear above it, and the
418
+ hairline and 10px the footer then takes. The development warning measures the
419
+ real column and needs nothing.
477
420
 
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.
421
+ Not done here, and why: board 20's Settings row carries an external-link arrow,
422
+ which no ticket in Wave 4 asks for and `NavItem` has no slot for. It wants its
423
+ own ticket.
480
424
 
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)
425
+ Repo-only: `src/buttons.module.css` holds the tiers. The "Shell redesign —
426
+ B33-1710" specimen route's shell gains "New scan" and a Settings / Help footer.
486
427
 
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.**
428
+ ### Changed — the shell redesign, part 2 of 3: the top bar, the token chip, the bell (B33-1712)
499
429
 
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`.
430
+ Boards 20, 21 and 22 of the same redesign. **This is a breaking layout change
431
+ for every consumer of `AppShell`: the switcher and the wallet chip move out of
432
+ the sidebar into the top bar, and the breadcrumb is no longer drawn.** Every
433
+ gated screenshot of the shell, the top bar and the sidebar changes; no new
434
+ baseline has been captured (the person approving the redesign captures them).
505
435
 
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`.
436
+ What a consumer sees, and what it has to do:
514
437
 
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").
438
+ - **The top bar has three regions.** Left: the Boost33 mark (linked home
439
+ through `hubHref`, as the sidebar wordmark was) and the new `org` slot.
440
+ Centre: "Switch Tools →" then the switcher, "Check Your Tokens →" then the
441
+ token chip. Right: `topBar.actions` -- pass `NotificationButton` and your
442
+ avatar there.
443
+ - **`AppShell.switcher` and `AppShell.wallet` keep their names and move.**
444
+ Both now render in the top bar's centre group, not the sidebar. Each label is
445
+ drawn only when its control is passed. Pass `WalletChip` with
446
+ `placement="topbar"` in `wallet`; the sidebar card is not designed for the
447
+ bar.
448
+ - **The sidebar no longer draws the wordmark, the switcher or the wallet
449
+ chip.** It holds `appName`, the nav and `footer`, with 16px top and bottom
450
+ padding of its own.
451
+ - **New `AppShell.org`**: a slot for your organisation pill, beside the mark.
452
+ The package draws no organisation menu (that is AccountMenu, B33-493, which
453
+ has not passed its gate).
454
+ - **`topBar` is optional, and `topBar.breadcrumb` is deprecated and not
455
+ drawn.** It still compiles; stop passing it. `PageHeader` names the page.
456
+ **If you put `SidebarToggle` in `topBar.breadcrumb` (the Wallet does), move it
457
+ to the new `topBar.sidebarToggle` slot, or your rail loses its toggle.** The
458
+ screenshot gate does not catch this: the bar passed its baseline at 0.19%
459
+ with the toggle gone. So in development, passing `breadcrumb` -- to `TopBar`
460
+ or through AppShell `topBar` -- logs a console warning once, naming where a
461
+ toggle and an organisation switcher go instead; production builds are silent.
462
+ - **New `topBar.sidebarToggle`** (and `TopBar.sidebarToggle`): your rail
463
+ toggle, drawn at the far left of the bar before the mark. No board draws it;
464
+ judged at the gate.
465
+ `topBar.labels` (`TopBarLabels`, `defaultTopBarLabels`) translates the two
466
+ centre labels.
467
+ - **No search, and no `⌘K`.** Search was removed from the top bar on 3 Nov
468
+ 2026 until it is built, and nothing replaces it. `CommandPalette` stays in the
469
+ package, Stage A and unreleasable, with no trigger in the shell.
470
+ - **New `WalletChip placement="topbar"`**: a 32px pill drawn like the
471
+ switcher -- the balance in the mono face, a 44 x 6 two-segment bar, a
472
+ chevron -- that opens a 340px popover with the balance, the bar at width,
473
+ granted and purchased with what each means, your `burnNote`, an "Open" link
474
+ (`walletHref`, `linkComponent`) and a "Buy tokens" button (`onTopUp`). A zero
475
+ segment is drawn at 0px with the 2px gap kept, never as a sliver (B33-590).
476
+ `unknown` shows an em dash, no bar and no split. The default
477
+ `placement="sidebar"` card is unchanged.
478
+ - **New `WalletChip totalOnly` with a signed `balance`** (top bar only, enforced
479
+ by the type), for a balance the Ledger reports with a bucket below zero, which
480
+ no two-segment bar can draw honestly. Below zero: the distance past zero in
481
+ `--color-danger` with the words "past zero", as the Wallet's past-zero screen
482
+ says it. At zero or above (a bucket below zero, a positive total -- decided by
483
+ Jamal on 3 Nov 2026): the plain figure. Neither draws a bar or a
484
+ granted / purchased legend. New labels: `pastZero`, `pastZeroLong`,
485
+ `pastZeroBalance`. **No board draws this state; it is judged at the gate.**
486
+ - **New labels for the popover:** `popover`, `grantedFigure`,
487
+ `purchasedFigure`, `grantedNote`, `grantedNoteUndated`, `purchasedNote`,
488
+ `open`, `buyTokens`. Each defaults to English.
489
+ - **New `NotificationButton`**: the 32px bell for `topBar.actions`, with an
490
+ `unread` dot. It fetches nothing and renders nothing without `onClick` or
491
+ `href`. The NotificationCenter panel stays Stage A and is not wired.
492
+ - **"Buy tokens" is white on `--color-orange-600`** (5.18:1), not the brand
493
+ orange board 21 draws (3.69:1, fails AA at 13px). Decided by Jamal on 4 Nov
494
+ 2026: orange that carries text is `--color-orange-600`.
495
+ - **The top bar spans the full width,** above the sidebar and the product's
496
+ column alike (board 20). Until now it sat over the content column only and
497
+ the sidebar ran the full height.
498
+ - **Two new component-private tokens:** `--container-token-popover` (340px),
499
+ `--spacing-0_75` (3px).
500
+ - **The nav height budget moves, because the sidebar's furniture moved.**
501
+ `navBudgetForViewport(900)` and `checkNavHeightBudget`'s default budget rise
502
+ from 629 to 730px: the top bar (48) is now counted above the sidebar, and the
503
+ wordmark, switcher and wallet chip no longer take sidebar height. The default
504
+ `consumerSlotsPx` is now 44 (the footer alone), down from 86 (switcher plus
505
+ footer). If you pass your own `consumerSlotsPx`, drop your switcher's height
506
+ from it. A 13-item nav that scrolled at 900px now fits; the dev warning in
507
+ `Sidebar` follows the same numbers.
508
+ - **The app name no longer shrinks** when the nav is long enough to scroll
509
+ (it lost 2px of its 38).
520
510
 
521
- **The shape, so it is legible from here:**
511
+ The 56px rail keeps working, icon-only, and gets no new design. The top bar
512
+ does not collapse.
522
513
 
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.
514
+ Repo-only: the specimen's "Shell redesign — B33-1710" route grows the top bar,
515
+ the open token popover, board 22's nothing-purchased chip, and the unknown,
516
+ past-zero and total-only chips.
548
517
 
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.
518
+ ### Added and fixed — the release candidate (0.8.0-next.5)
558
519
 
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.
520
+ Every new prop and label key here is optional, so an app moving from next.4
521
+ changes only its version pin.
522
+
523
+ - **French figures show their grouping space.** CLDR groups French thousands
524
+ with a narrow no-break space (U+202F), and the kit's faces draw it at almost
525
+ nothing: 1.9px in Inter at 14px, 4.1px in Outfit at 40px. So the token
526
+ popover read "1500" while the chip read "1 500" (Brokers). Every figure the
527
+ chip formats (`formatTokens`, the rail's short form, the threshold
528
+ percentage) now uses an ordinary no-break space (U+00A0) instead. It is
529
+ visible in every face and still never wraps. en-US output is unchanged.
530
+ If your tests compare against `Intl.NumberFormat('fr-FR')` output, apply the
531
+ same swap.
532
+ - **`NotificationButton`** (Content):
533
+ - `unreadCount?: number` goes into the accessible name ("Notifications,
534
+ 3 unread", new optional label `unreadCount`, `%count%`). Above 0 it draws
535
+ the dot. The figure itself is not drawn: board 20 draws a dot.
536
+ - `expanded?: boolean` and `controls?: string` become `aria-expanded` and
537
+ `aria-controls` when it opens a panel. Button form only.
538
+ - `ref` reaches the rendered button or link (React 19 passes `ref` as a
539
+ prop; the link form needs a `linkComponent` that forwards it, as next/link
540
+ does).
541
+ - **`AppShellAction.active?: boolean`** (Off Market): on the action's own page,
542
+ the link form carries `aria-current="page"` and both forms keep the primary
543
+ tier's pressed ground (`.primary[data-active='true']`, its hover colour, in
544
+ either tone).
545
+ - **`countLabel?: string`** on `NavItem` and `AppShellNavItem` (Off Market):
546
+ what a screen reader hears for `count` ("3 à ouvrir"). Given, the visible
547
+ number is hidden from assistive tech and this is read instead.
548
+ - **`attrs?: NavItemAttrs`** on `NavItem` and `AppShellNavItem` (Outbound):
549
+ `data-*` attributes for the row, such as a tour's `data-tour`. Only `data-*`
550
+ names are accepted (also filtered at runtime), and the row's own
551
+ `data-active`/`data-collapsed` win. New exported type `NavItemAttrs`.
552
+ - **README: two consumer notes.**
553
+ - Map the `--boost33-font-*` hooks on `:root`, with `next/font`'s variable
554
+ classes on `<html>`. A mapping on `body` never reaches the kit (Outbound).
555
+ - **Never pass a function from a Server Component to `AppSwitcher` or
556
+ `WalletChip`**, the package's client components: `linkComponent`,
557
+ `onTopUp`, `onOpenChange`. Next refuses it and every signed-in render 500s
558
+ (Passport), and `renderToStaticMarkup` tests cannot see it. Render those
559
+ two inside a `'use client'` file of your own. The `linkComponent` docs on
560
+ both now say so.
561
+ - No approved screenshot moves (see the commit for the comparison).
562
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:
563
+ ### Fixed — the top bar fits at 1024px; the token popover stays on screen (0.8.0-next.4)
566
564
 
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.
565
+ - **Below 80rem of bar, the centre group's two labels leave the screen.**
566
+ next.3 stopped the regions overlapping, but the centre does not shrink, so at
567
+ 1024px Content's bar still needed 1067px and clipped the avatar. Now
568
+ "Switch Tools →" and "Check Your Tokens →" are hidden visually wherever the
569
+ bar's content box is under 80rem (1280px; a full-width bar in a window under
570
+ 1344px) and the centre gives back about 280px. They stay in the
571
+ accessibility tree, before the control each introduces, so a screen reader
572
+ hears the same thing at every width. A container query on the bar
573
+ (`container: boost33-top-bar / inline-size`), so a bar narrower than the
574
+ window is measured by its own width. Measured in Chromium at 1024px with a
575
+ 256px start group and either a 70px or a 220px actions group: no overflow and
576
+ no overlap. If you write container queries in the bar's slots, name your own
577
+ container: the bar is now a container.
578
+ - **The token popover stays on screen.** It hangs from the pill's left edge and
579
+ is 340px wide. At 390px its right edge was at 535px, cutting the threshold
580
+ line and the reset note, and at 1024px it overshot by 8px. It now slides left
581
+ by exactly its overshoot to keep 16px from the right edge (never past 16px
582
+ from the left), and is never wider than the viewport less 32px. Both the
583
+ split chip and `totalOnly` do this. It is measured before paint and again on
584
+ resize.
585
+ - **Below `chromeFrom` the shell fills the screen**: `min-height: 100dvh`
586
+ instead of next.3's `100%`, which resolved against app wrappers with no
587
+ height and left short pages ending grey (Content).
588
+ - **No approved screenshot moves**: all 40 gated states measure the same with
589
+ and without these changes, each on a fresh specimen build.
570
590
 
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.
591
+ ### Fixed — the top-bar token chip warns again past 80% of the grant; the top bar holds its layout (0.8.0-next.3)
574
592
 
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.
593
+ - **The 80% threshold is back in the top bar.** 0.8.0-next.0 moved the chip to
594
+ the top bar without its threshold state, so a seller who had burned 87% of the
595
+ month's grant saw the same chip as one at 10%, and nothing here said so
596
+ (found by Score). `placement="topbar"` now reads `grantCycle` and
597
+ `burnedFraction` exactly as the sidebar card does (the consumer's
598
+ `burnedFraction` wins, otherwise it is derived from `grantCycle`, and with
599
+ neither nothing is claimed). Past 80%:
600
+ - with `grantCycle`, the bar in the chip and in the popover measures against
601
+ what the cycle started with, so it shortens and the burned share shows as
602
+ the warning-tinted track: the sidebar card's drawing (Jamal, 4 Nov 2026).
603
+ With `burnedFraction` alone there is no honest denominator, so the bar
604
+ stays full;
605
+ - the popover shows a line above the runway note, and the chip's accessible
606
+ name ends with it, so the warning is never colour alone. **New label
607
+ `thresholdNote`**, a template with `%percent%`, formatted for `locale`
608
+ ("87%", "87 %"). Default `You've used %percent% of this month's grant.`
609
+ A catalogue typed as a full `WalletChipLabels` must add it; that is the
610
+ typecheck catching a missing translation;
611
+ - the root carries `data-threshold="true"`. Buy tokens is unchanged: the
612
+ accent primary.
613
+ Keep passing `grantCycle` (or `burnedFraction`) to the top-bar chip.
614
+ - **The top bar keeps its regions apart at any width.** At 1024px the start
615
+ group (mark and organisation pill) was given less than it held and painted
616
+ across "Switch Tools →" (Content, Wallet, Activity, Outbound). The bar is now
617
+ a three-column grid whose sides are never narrower than their contents: where
618
+ everything fits, the centre group sits in the middle as before; where it does
619
+ not, the centre moves off the middle instead of under the pill. Below that,
620
+ on a phone with no `chromeFrom`, the bar runs off the right edge in order
621
+ rather than piling up (Score). **No approved screenshot moves**: every gated
622
+ state measures the same pixel count with and without the change.
623
+ - **The top bar is a `<header>`**, the page's banner landmark, so assistive tech
624
+ can jump to it and a test can scope to it (Score). `getByRole('banner')`
625
+ finds it. The census in `scripts/border-proof.ts` records the bar's hairline on
626
+ a `header` now; the pixels did not move.
627
+ - **The popover's reset note ends on the date.** `grantedNote` defaults to
628
+ `Included each month · resets %date%`, without the `Resets %date%.`
629
+ full stop that printed "30 sept.." for a French date (Score). If you
630
+ translate it, keep punctuation off the end of `%date%`. `grantExpires` is one
631
+ date in both placements, the day this month's grant runs out and the next
632
+ lands. The sidebar says it as an expiry, the popover as a reset.
633
+ - **Below `chromeFrom` the shell still fills its root** (`min-height: 100%`),
634
+ so a short page keeps the white ground to the bottom of the screen (Content).
635
+ Replaced by `100dvh` in next.4.
583
636
 
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.
637
+ ### Added — `AppShell` gets out of the way on a phone: `chromeFrom` and `phoneChrome`
590
638
 
591
- #### Not in `dist/`, and so not your concern
639
+ AppShell has no phone layout, and the 0.8 top bar runs off a 390px screen.
640
+ Products keep their own phone chrome below a breakpoint, and until now the only
641
+ ways to do that were a `matchMedia` switch (the desktop shell flashes on a phone
642
+ before it swaps) or CSS reaching into the shell's DOM shape. Both options are opt-in;
643
+ a shell that sets neither renders exactly as before.
592
644
 
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.
645
+ - **`chromeFrom?: 'md' | 'lg' | 'xl'`** (new type `AppShellBreakpoint`). Below
646
+ that viewport width the shell hides its own top bar and sidebar, the page
647
+ body takes the full width, and the document scrolls instead of the shell's
648
+ content box. Done in the shell's own `@media` CSS, so the first paint is
649
+ already right, with no flash and no second render of the page. The widths are
650
+ Tailwind v4's defaults -- `md` 48rem (768px), `lg` 64rem (1024px), `xl` 80rem
651
+ (1280px) -- so `lg` here and `lg:` in your own markup flip at the same pixel.
652
+ It is a name and not a number because a media query cannot read a value
653
+ passed at runtime.
654
+ - **`phoneChrome?: ReactNode`**: your phone header and navigation. The shell
655
+ draws it above the page body, outside `<main>`, and shows it only below
656
+ `chromeFrom`. Ignored without `chromeFrom`. The shell's wrapper around it
657
+ draws no box (`display: contents`), so a `position: sticky` header you pass
658
+ stays pinned while the page scrolls (fixed after 0.8.0-next.1, where it
659
+ scrolled away). Pass your phone chrome here and
660
+ drop any `lg:hidden` and any selector that hid the kit's bar or `<aside>` by
661
+ position: those depended on a DOM shape this package does not promise.
662
+ - Not a mobile shell. Nothing is redesigned below the breakpoint; the shell only
663
+ stops drawing its desktop chrome. A phone design needs a board first.
664
+ - The sidebar's development-only nav-height warning no longer fires when the
665
+ sidebar is hidden this way.
601
666
 
602
- ### Added — `NotificationCenter`, Stage A only, **not releasable yet** (B33-494)
667
+ ### Note — in a Tailwind v4 app, `style.css` alone re-sizes the app's own utilities
603
668
 
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.
669
+ **Importing `@boost33/ui/style.css` changes what a Tailwind v4 app already has
670
+ on screen, with or without `theme.css`.** Off Market and Brokers measured it
671
+ adopting 0.8.0-next.0. This is not new in 0.8, but 0.8 is when Tailwind v4
672
+ apps first take the shell.
618
673
 
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.
674
+ Why: `style.css` carries `tokens.css`, which declares this package's tokens on
675
+ `:root`, unlayered, under the names Tailwind v4 itself uses (`--text-*`,
676
+ `--leading-*`, `--tracking-*`, `--radius-*`, `--font-*`, `--font-weight-*`,
677
+ `--color-*`). Tailwind's default utilities read those names through `var()`
678
+ (`.text-sm { font-size: var(--text-sm) }`), and an unlayered declaration beats
679
+ Tailwind's `@layer theme`. So in the app, `text-sm` becomes 13px (Tailwind's
680
+ 14px) with a 13px line height, `text-base` 15px (16px), and `rounded-sm`, `-md`,
681
+ `-lg`, `-xl`, `-2xl` become 3/4/6/8/12px (4/6/8/12/16px). The `leading-*` and
682
+ `tracking-*` steps move too.
622
683
 
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.
684
+ To keep your own scale, tell Tailwind to write literal values into its
685
+ utilities so they never read `:root`. Either import Tailwind's theme inline:
630
686
 
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
- />
687
+ ```css
688
+ @layer theme, base, components, utilities;
689
+ @import "tailwindcss/theme.css" layer(theme) theme(inline);
690
+ @import "tailwindcss/preflight.css" layer(base);
691
+ @import "tailwindcss/utilities.css" layer(utilities);
692
+ @import "@boost33/ui/style.css";
647
693
  ```
648
694
 
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.
695
+ or pin each colliding name you use under `@theme inline { … }` with Tailwind's
696
+ value. Either way the kit's components keep reading `:root` and render as
697
+ designed. Brokers measured identical computed styles on all 12 of its pages
698
+ with the first recipe.
662
699
 
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.
700
+ `theme.css` is a separate, optional step on top of this. It adds utilities for
701
+ names only this package has (`bg-surface`, `border-border`, …) and makes the
702
+ kit's values the app's whole scale (ADR-0011, ADR-0012). Every kit component
703
+ renders correctly without it (the specimen imports no `theme.css`). An app that
704
+ used those utilities and drops `theme.css` loses them.
671
705
 
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.
706
+ The shell also sets `font-family: var(--font-sans)` (Inter) on its frame, so
707
+ `children` inherit Inter unless the page sets its own family.
678
708
 
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.
709
+ ### Changed — `CHANGELOG.md` now ships inside the package
686
710
 
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.
711
+ `package.json` `files` adds `CHANGELOG.md` next to `dist`, so this file is in
712
+ the tarball at `node_modules/@boost33/ui/CHANGELOG.md`. Until now it reached a
713
+ consumer only through the repository, which is a strange place to keep the one
714
+ notice a consumer gets. Nothing in `dist/` changes. 0.5.0 and earlier were
715
+ published without it, and a published version cannot be amended, so the first
716
+ release to carry it is the one after 0.5.0. Repo-only otherwise: no import,
717
+ prop or rendering changes.
690
718
 
691
719
  ## 0.7.1 — 2026-11-02
692
720