@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.
- package/CHANGELOG.md +613 -585
- 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
|
-
###
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
86
|
-
no new design.
|
|
49
|
+
What exists today, and what does not:
|
|
87
50
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
126
|
-
|
|
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
|
-
|
|
130
|
-
|
|
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
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
|
216
|
-
does not collapse.
|
|
132
|
+
**The shape, so it is legible from here:**
|
|
217
133
|
|
|
218
|
-
|
|
219
|
-
the
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
225
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
###
|
|
213
|
+
### Added — `NotificationCenter`, Stage A only, **not releasable yet** (B33-494)
|
|
372
214
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
389
|
-
|
|
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
|
-
```
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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
|
|
411
|
-
|
|
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
|
-
|
|
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
|
-
`
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
-
|
|
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
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
-
|
|
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
|
|
473
|
-
|
|
474
|
-
the
|
|
475
|
-
|
|
476
|
-
|
|
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
|
-
|
|
479
|
-
|
|
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
|
-
|
|
482
|
-
|
|
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
|
-
|
|
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
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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
|
-
|
|
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
|
-
**
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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
|
-
|
|
511
|
+
The 56px rail keeps working, icon-only, and gets no new design. The top bar
|
|
512
|
+
does not collapse.
|
|
522
513
|
|
|
523
|
-
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
-
|
|
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
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
568
|
-
|
|
569
|
-
|
|
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
|
-
|
|
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
|
-
**
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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
|
-
###
|
|
667
|
+
### Note — in a Tailwind v4 app, `style.css` alone re-sizes the app's own utilities
|
|
603
668
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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
|
-
|
|
624
|
-
|
|
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
|
-
```
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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
|
|
673
|
-
|
|
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
|
-
|
|
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
|
-
|
|
688
|
-
|
|
689
|
-
|
|
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
|
|