@boost33/ui 0.7.1 → 0.8.0-next.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 (64) hide show
  1. package/CHANGELOG.md +183 -0
  2. package/dist/buttons.module.css.js +10 -0
  3. package/dist/buttons.module.css.js.map +1 -0
  4. package/dist/components/AppShell/AppShell.d.ts +85 -17
  5. package/dist/components/AppShell/AppShell.js +75 -50
  6. package/dist/components/AppShell/AppShell.js.map +1 -1
  7. package/dist/components/AppShell/AppShell.module.css.js +22 -16
  8. package/dist/components/AppShell/AppShell.module.css.js.map +1 -1
  9. package/dist/components/AppSwitcher/AppSwitcher.d.ts +90 -45
  10. package/dist/components/AppSwitcher/AppSwitcher.js +71 -66
  11. package/dist/components/AppSwitcher/AppSwitcher.js.map +1 -1
  12. package/dist/components/AppSwitcher/AppSwitcher.module.css.js +42 -36
  13. package/dist/components/AppSwitcher/AppSwitcher.module.css.js.map +1 -1
  14. package/dist/components/AppSwitcher/icons.js +10 -10
  15. package/dist/components/AppSwitcher/icons.js.map +1 -1
  16. package/dist/components/AppSwitcher/labels.d.ts +11 -22
  17. package/dist/components/AppSwitcher/labels.js.map +1 -1
  18. package/dist/components/CommandPalette/CommandPalette.d.ts +8 -2
  19. package/dist/components/CommandPalette/CommandPalette.js.map +1 -1
  20. package/dist/components/NavItem/NavItem.d.ts +15 -2
  21. package/dist/components/NavItem/NavItem.js +14 -11
  22. package/dist/components/NavItem/NavItem.js.map +1 -1
  23. package/dist/components/NavItem/NavItem.module.css.js +6 -2
  24. package/dist/components/NavItem/NavItem.module.css.js.map +1 -1
  25. package/dist/components/NavSection/NavSection.module.css.js +5 -5
  26. package/dist/components/NotificationButton/NotificationButton.d.ts +29 -0
  27. package/dist/components/NotificationButton/NotificationButton.js +23 -0
  28. package/dist/components/NotificationButton/NotificationButton.js.map +1 -0
  29. package/dist/components/NotificationButton/NotificationButton.module.css.js +14 -0
  30. package/dist/components/NotificationButton/NotificationButton.module.css.js.map +1 -0
  31. package/dist/components/NotificationButton/labels.d.ts +12 -0
  32. package/dist/components/NotificationButton/labels.js +15 -0
  33. package/dist/components/NotificationButton/labels.js.map +1 -0
  34. package/dist/components/NotificationCenter/NotificationCenter.js +37 -36
  35. package/dist/components/NotificationCenter/NotificationCenter.js.map +1 -1
  36. package/dist/components/NotificationCenter/NotificationCenter.module.css.js +43 -43
  37. package/dist/components/Sidebar/NavHeightBudgetWarning.js +2 -2
  38. package/dist/components/Sidebar/NavHeightBudgetWarning.js.map +1 -1
  39. package/dist/components/Sidebar/Sidebar.module.css.js +1 -1
  40. package/dist/components/TopBar/TopBar.d.ts +54 -4
  41. package/dist/components/TopBar/TopBar.js +22 -7
  42. package/dist/components/TopBar/TopBar.js.map +1 -1
  43. package/dist/components/TopBar/TopBar.module.css.js +12 -8
  44. package/dist/components/TopBar/TopBar.module.css.js.map +1 -1
  45. package/dist/components/TopBar/labels.d.ts +20 -0
  46. package/dist/components/TopBar/labels.js +15 -0
  47. package/dist/components/TopBar/labels.js.map +1 -0
  48. package/dist/components/WalletChip/WalletChip.d.ts +77 -2
  49. package/dist/components/WalletChip/WalletChip.js +142 -50
  50. package/dist/components/WalletChip/WalletChip.js.map +1 -1
  51. package/dist/components/WalletChip/WalletChip.module.css.js +93 -47
  52. package/dist/components/WalletChip/WalletChip.module.css.js.map +1 -1
  53. package/dist/components/WalletChip/labels.d.ts +63 -1
  54. package/dist/components/WalletChip/labels.js +21 -6
  55. package/dist/components/WalletChip/labels.js.map +1 -1
  56. package/dist/index.d.ts +9 -3
  57. package/dist/index.js +55 -49
  58. package/dist/index.js.map +1 -1
  59. package/dist/nav-height-budget.d.ts +12 -9
  60. package/dist/nav-height-budget.js +3 -3
  61. package/dist/nav-height-budget.js.map +1 -1
  62. package/dist/style.css +1 -1
  63. package/dist/theme.css +1 -1
  64. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -31,6 +31,189 @@ 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:
43
+
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 and is
56
+ ignored, so a labels object that sets it still compiles. Drop it when
57
+ convenient.
58
+ - **The switcher's account row is gone.** `account` is now optional and
59
+ ignored, and `AppSwitcherAccount` is deprecated. The organisation moves to the
60
+ top bar in part 2. Nothing breaks if you keep passing it; stop when
61
+ convenient. Its `onSelect` is never called.
62
+ - **New, optional, per product row:** `group` (a section header, e.g. "Your
63
+ tools" -- rows are grouped in the order served; no groups means one plain
64
+ list) and `trailing` (`{ kind: 'value', value: '8,940' }` or
65
+ `{ kind: 'dot', label: '2 new' }` -- the dot's `label` is what a screen reader
66
+ hears). Both are data you are served; the switcher fetches nothing.
67
+ - **The switcher is redrawn to board 21.** A 32px tinted trigger with no border
68
+ and the product name in the display face; a 360px borderless panel; a dark
69
+ Hub row with a chevron; 28px product tiles; the current row semibold on a
70
+ muted ground. The product marks are unchanged (the set approved 29 Oct 2026).
71
+ - **Nav rows:** the active label is regular weight now, not semibold (the white
72
+ row and full-ink colour carry "you are here"). Counts are 12px medium and
73
+ right-aligned. `countUrgent` draws a filled pill instead of orange text --
74
+ white on `--color-orange-600`, 5.18:1, because the old orange text was 3.45:1
75
+ on the sidebar and failed AA. New `dot` on `NavItem` and `AppShellNavItem`: a
76
+ 7px attention dot, its string read by a screen reader.
77
+ - **Section labels** track at 0.09em (`--tracking-wider`), up from 0.05em.
78
+ - **Three new component-private tokens** (in `style.css`, no Tailwind
79
+ utility): `--container-app-switcher` (360px), `--spacing-2_75` (11px),
80
+ `--spacing-1_75` (7px).
81
+
82
+ The 56px rail keeps working, icon-only. The redesign draws no rail, so it gets
83
+ no new design.
84
+
85
+ Repo-only: the specimen's unarmed-switcher route is removed (the row it showed
86
+ is gone), and a new "Shell redesign — B33-1710" route at the bottom of the
87
+ specimen assembles boards 20 and 21 for review.
88
+
89
+ ### Changed — the shell redesign, part 3 of 3: the sidebar action, and every button on three tiers (B33-1713)
90
+
91
+ Board 20 of the same redesign, and the consolidation its review asked for.
92
+ **Every button the package draws changes look**; no new baseline has been
93
+ captured (the person approving the redesign captures them).
94
+
95
+ What a consumer sees, and what it has to do:
96
+
97
+ - **New `AppShell.action`** (`AppShellAction`: `label`, optional `icon`, and
98
+ `href` or `onClick`): your product's main action, drawn at the foot of the
99
+ sidebar directly above the footer's divider -- board 20's "New scan". Near-black,
100
+ full width, 40px. Not drawn with neither `href` nor `onClick`. On the rail it
101
+ shows its icon alone, its label kept as its accessible name, and is not drawn
102
+ if it has no icon. If you drew your own action button elsewhere in the shell,
103
+ pass it here instead.
104
+ - **The footer sits under a hairline when there is an action**, 10px above its
105
+ rows, and its rows are 2px apart (the nav's own pitch), down from 16px.
106
+ - **Three button tiers, written down in ADR-0016:** primary (filled, 40px, ink or
107
+ accent), outlined (32px, white, 1px edge) and quiet (a word, no box). Every
108
+ package button now takes one.
109
+ - **`WalletChip`'s "Top up" is no longer orange text.** At rest it is the quiet
110
+ tier in full ink; past the threshold it is the primary tier in the accent tone.
111
+ The orange text was 3.69:1 on white and failed AA at 12px.
112
+ - **The accent tone is `--color-orange-600`**, not the brand orange: white on the
113
+ brand orange is 3.69:1. "Buy tokens" and the grown "Top up" both use it.
114
+ Decided by Jamal on 4 Nov 2026.
115
+ - **`NotificationCenter`'s "Mark all as read" and "View all"** are the quiet tier.
116
+ The component is still Stage A and not releasable.
117
+ - **If you pass `action`, add 67px to `consumerSlotsPx`** when you call
118
+ `navBudgetForViewport` yourself: the 40px button, 16px clear above it, and the
119
+ hairline and 10px the footer then takes. The development warning measures the
120
+ real column and needs nothing.
121
+
122
+ Not done here, and why: board 20's Settings row carries an external-link arrow,
123
+ which no ticket in Wave 4 asks for and `NavItem` has no slot for. It wants its
124
+ own ticket.
125
+
126
+ Repo-only: `src/buttons.module.css` holds the tiers. The "Shell redesign —
127
+ B33-1710" specimen route's shell gains "New scan" and a Settings / Help footer.
128
+
129
+ ### Changed — the shell redesign, part 2 of 3: the top bar, the token chip, the bell (B33-1712)
130
+
131
+ Boards 20, 21 and 22 of the same redesign. **This is a breaking layout change
132
+ for every consumer of `AppShell`: the switcher and the wallet chip move out of
133
+ the sidebar into the top bar, and the breadcrumb is no longer drawn.** Every
134
+ gated screenshot of the shell, the top bar and the sidebar changes; no new
135
+ baseline has been captured (the person approving the redesign captures them).
136
+
137
+ What a consumer sees, and what it has to do:
138
+
139
+ - **The top bar has three regions.** Left: the Boost33 mark (linked home
140
+ through `hubHref`, as the sidebar wordmark was) and the new `org` slot.
141
+ Centre: "Switch Tools →" then the switcher, "Check Your Tokens →" then the
142
+ token chip. Right: `topBar.actions` -- pass `NotificationButton` and your
143
+ avatar there.
144
+ - **`AppShell.switcher` and `AppShell.wallet` keep their names and move.**
145
+ Both now render in the top bar's centre group, not the sidebar. Each label is
146
+ drawn only when its control is passed. Pass `WalletChip` with
147
+ `placement="topbar"` in `wallet`; the sidebar card is not designed for the
148
+ bar.
149
+ - **The sidebar no longer draws the wordmark, the switcher or the wallet
150
+ chip.** It holds `appName`, the nav and `footer`, with 16px top and bottom
151
+ padding of its own.
152
+ - **New `AppShell.org`**: a slot for your organisation pill, beside the mark.
153
+ The package draws no organisation menu (that is AccountMenu, B33-493, which
154
+ has not passed its gate).
155
+ - **`topBar` is optional, and `topBar.breadcrumb` is deprecated and not
156
+ drawn.** It still compiles; stop passing it. `PageHeader` names the page.
157
+ **If you put `SidebarToggle` in `topBar.breadcrumb` (the Wallet does), move it
158
+ to the new `topBar.sidebarToggle` slot, or your rail loses its toggle.** The
159
+ screenshot gate does not catch this: the bar passed its baseline at 0.19%
160
+ with the toggle gone.
161
+ - **New `topBar.sidebarToggle`** (and `TopBar.sidebarToggle`): your rail
162
+ toggle, drawn at the far left of the bar before the mark. No board draws it;
163
+ judged at the gate.
164
+ `topBar.labels` (`TopBarLabels`, `defaultTopBarLabels`) translates the two
165
+ centre labels.
166
+ - **No search, and no `⌘K`.** Search was removed from the top bar on 3 Nov
167
+ 2026 until it is built, and nothing replaces it. `CommandPalette` stays in the
168
+ package, Stage A and unreleasable, with no trigger in the shell.
169
+ - **New `WalletChip placement="topbar"`**: a 32px pill drawn like the
170
+ switcher -- the balance in the mono face, a 44 x 6 two-segment bar, a
171
+ chevron -- that opens a 340px popover with the balance, the bar at width,
172
+ granted and purchased with what each means, your `burnNote`, an "Open" link
173
+ (`walletHref`, `linkComponent`) and a "Buy tokens" button (`onTopUp`). A zero
174
+ segment is drawn at 0px with the 2px gap kept, never as a sliver (B33-590).
175
+ `unknown` shows an em dash, no bar and no split. The default
176
+ `placement="sidebar"` card is unchanged.
177
+ - **New `WalletChip totalOnly` with a signed `balance`** (top bar only, enforced
178
+ by the type), for a balance the Ledger reports with a bucket below zero, which
179
+ no two-segment bar can draw honestly. Below zero: the distance past zero in
180
+ `--color-danger` with the words "past zero", as the Wallet's past-zero screen
181
+ says it. At zero or above (a bucket below zero, a positive total -- decided by
182
+ Jamal on 3 Nov 2026): the plain figure. Neither draws a bar or a
183
+ granted / purchased legend. New labels: `pastZero`, `pastZeroLong`,
184
+ `pastZeroBalance`. **No board draws this state; it is judged at the gate.**
185
+ - **New labels for the popover:** `popover`, `grantedFigure`,
186
+ `purchasedFigure`, `grantedNote`, `grantedNoteUndated`, `purchasedNote`,
187
+ `open`, `buyTokens`. Each defaults to English.
188
+ - **New `NotificationButton`**: the 32px bell for `topBar.actions`, with an
189
+ `unread` dot. It fetches nothing and renders nothing without `onClick` or
190
+ `href`. The NotificationCenter panel stays Stage A and is not wired.
191
+ - **"Buy tokens" is white on `--color-orange-600`** (5.18:1), not the brand
192
+ orange board 21 draws (3.69:1, fails AA at 13px). Decided by Jamal on 4 Nov
193
+ 2026: orange that carries text is `--color-orange-600`.
194
+ - **The top bar spans the full width,** above the sidebar and the product's
195
+ column alike (board 20). Until now it sat over the content column only and
196
+ the sidebar ran the full height.
197
+ - **Two new component-private tokens:** `--container-token-popover` (340px),
198
+ `--spacing-0_75` (3px).
199
+ - **The nav height budget moves, because the sidebar's furniture moved.**
200
+ `navBudgetForViewport(900)` and `checkNavHeightBudget`'s default budget rise
201
+ from 629 to 730px: the top bar (48) is now counted above the sidebar, and the
202
+ wordmark, switcher and wallet chip no longer take sidebar height. The default
203
+ `consumerSlotsPx` is now 44 (the footer alone), down from 86 (switcher plus
204
+ footer). If you pass your own `consumerSlotsPx`, drop your switcher's height
205
+ from it. A 13-item nav that scrolled at 900px now fits; the dev warning in
206
+ `Sidebar` follows the same numbers.
207
+ - **The app name no longer shrinks** when the nav is long enough to scroll
208
+ (it lost 2px of its 38).
209
+
210
+ The 56px rail keeps working, icon-only, and gets no new design. The top bar
211
+ does not collapse.
212
+
213
+ Repo-only: the specimen's "Shell redesign — B33-1710" route grows the top bar,
214
+ the open token popover, board 22's nothing-purchased chip, and the unknown,
215
+ past-zero and total-only chips.
216
+
34
217
  ### Changed — `CHANGELOG.md` now ships inside the package
35
218
 
36
219
  `package.json` `files` adds `CHANGELOG.md` next to `dist`, so this file is in
@@ -0,0 +1,10 @@
1
+ const t = "_primary_qxhao_26", a = "_quiet_qxhao_77", o = {
2
+ primary: t,
3
+ quiet: a
4
+ };
5
+ export {
6
+ o as default,
7
+ t as primary,
8
+ a as quiet
9
+ };
10
+ //# sourceMappingURL=buttons.module.css.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"buttons.module.css.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;"}
@@ -10,12 +10,29 @@ export interface AppShellNavItem {
10
10
  active?: boolean;
11
11
  /** Trailing-slot count. Omit to leave the slot empty; it still reserves its width so counts line up. */
12
12
  count?: number;
13
- /** Renders `count` in the primary color instead of muted. */
13
+ /** Draws `count` as the filled "needs you" pill instead of muted text. */
14
14
  countUrgent?: boolean;
15
+ /** A trailing attention dot; the string is what a screen reader hears. Ignored when `count` is set. */
16
+ dot?: string;
15
17
  /** Where the row goes. A nav row's destination is data; the shell renders it as a link. */
16
18
  href?: string;
17
19
  }
18
20
  /** One nav section: an optional header over a run of rows. */
21
+ /**
22
+ * The product's main action, drawn as a button at the foot of the sidebar,
23
+ * above the footer's divider (B33-1713, board 20's "New scan"). Data, like a
24
+ * nav row: the shell renders it, in the primary tier's ink tone (ADR-0016).
25
+ */
26
+ export interface AppShellAction {
27
+ /** The button's words, in the consumer's language -- "New scan". */
28
+ label: string;
29
+ /** A leading icon, decorative. On the 56px rail it is all that shows. */
30
+ icon?: ReactNode;
31
+ /** Where it goes. Renders a link through `linkComponent`. Wins over `onClick`. */
32
+ href?: string;
33
+ /** What pressing it does. Renders a `<button>`. */
34
+ onClick?: () => void;
35
+ }
19
36
  export interface AppShellNavSection {
20
37
  /** The section header. Omit for a headerless section (the nav's first section has none). On the rail every header drops. */
21
38
  label?: string;
@@ -65,38 +82,89 @@ export interface AppShellProps {
65
82
  * resolved by a consumer passing this.
66
83
  */
67
84
  navLabel?: string;
85
+ /**
86
+ * The product's name, drawn as plain text at the top of the sidebar (B33-1711).
87
+ * The product name only -- "Boost Score", in the consumer's language -- with
88
+ * no icon beside it: the product mark belongs in the app switcher, not next to
89
+ * the name. Omit it and nothing is drawn there.
90
+ *
91
+ * It is visible text and nothing else. It names no landmark (that is `label`)
92
+ * and nothing branches on it. Often the same string as `label`; it is a
93
+ * separate prop because one is read aloud as a landmark's name and the other
94
+ * is read on screen, and a consumer may want them to differ.
95
+ *
96
+ * Hidden on the 56px rail, where there is no room for a word.
97
+ */
98
+ appName?: string;
68
99
  /** The navigation, as data. No product hardcodes rows; the shell composes them. */
69
100
  nav: AppShellNavSection[];
70
- /** The top bar's content -- the breadcrumb and the shell actions on its right. */
71
- topBar: TopBarProps;
72
- /** The Boost33 mark for the wordmark lockup. Passed in, the way Wordmark takes it; on the rail it is all that shows. Pass it unlinked -- `hubHref` links the whole lockup. */
101
+ /**
102
+ * The top bar's own content: its `actions` on the right (the notification
103
+ * button and the avatar) and its `labels`. The shell fills the rest of the bar
104
+ * itself -- the mark and `org` on the left, `switcher` and `wallet` in the
105
+ * centre -- so those are props of the shell, not of this object.
106
+ *
107
+ * `breadcrumb` is still accepted and no longer drawn (B33-1712): the redesign
108
+ * puts the mark and the organisation where it was. A sidebar toggle kept
109
+ * inside it goes in `topBar.sidebarToggle` instead, at the far left of the
110
+ * bar, or the rail loses its toggle.
111
+ */
112
+ topBar?: Omit<TopBarProps, 'start' | 'switcher' | 'tokens'>;
113
+ /**
114
+ * The Boost33 mark, at the left of the top bar (B33-1712; it was the
115
+ * sidebar's wordmark lockup until 0.8.0). Passed in, the way Wordmark takes
116
+ * it. Pass it unlinked -- `hubHref` links it. The word "Boost33" is not drawn
117
+ * beside it; it stays as the link's accessible name.
118
+ */
73
119
  mark?: ReactNode;
74
120
  /**
75
- * Where the hub lives. Set it and the wordmark in the sidebar's top-left
76
- * corner becomes a link home, rendered through `linkComponent`; omit it and
77
- * the wordmark is the plain lockup it always was (B33-1699). The consumer
121
+ * Where the hub lives. Set it and the mark at the left of the top bar becomes
122
+ * a link home, rendered through `linkComponent` (B33-1699). The consumer
78
123
  * supplies the URL -- Identity owns where the hub is and it differs per
79
124
  * environment, so the package never hardcodes it. See `Wordmark.hubHref`.
80
125
  */
81
126
  hubHref?: string;
82
127
  /**
83
- * The product switcher, sitting under the wordmark. A slot: the registry it
84
- * draws is served by Identity (ADR-0003), not owned here. Render its own
85
- * collapsed form for the rail.
128
+ * The organisation, beside the mark at the left of the top bar (B33-1712,
129
+ * board 20: "AC Acme Coaching ⌄"). A slot -- the product's own pill. The
130
+ * package draws no organisation menu: that is AccountMenu (B33-493), which has
131
+ * not passed its gate.
132
+ */
133
+ org?: ReactNode;
134
+ /**
135
+ * The app switcher, at the centre of the top bar after its "Switch Tools →"
136
+ * label (B33-1712; it sat under the wordmark in the sidebar until 0.8.0). A
137
+ * slot: pass `AppSwitcher`. The registry it draws is served by Identity
138
+ * (ADR-0003), not owned here. It no longer has a rail form to render -- the
139
+ * top bar does not collapse.
86
140
  */
87
141
  switcher?: ReactNode;
88
142
  /**
89
- * The wallet chip, pinned above the footer. A slot: the balance comes from
90
- * `@boost33/ledger-client`, never read here. Render its own collapsed form
91
- * for the rail.
143
+ * The token chip, at the centre of the top bar after the switcher and its
144
+ * "Check Your Tokens →" label (B33-1712; it was pinned above the sidebar's
145
+ * footer until 0.8.0). A slot: pass `WalletChip` with `placement="topbar"`.
146
+ * The balance comes from `@boost33/ledger-client`, never read here.
92
147
  */
93
148
  wallet?: ReactNode;
94
- /** The sidebar footer, below the wallet chip. A slot -- the product's own. */
149
+ /**
150
+ * The product's main action, at the foot of the sidebar directly above the
151
+ * footer's divider (B33-1713). Omit it and nothing is drawn. Given neither
152
+ * `href` nor `onClick`, it is not drawn either: a button that does nothing is
153
+ * a control that lies (B33-1270). On the rail it shows its icon alone, or
154
+ * nothing if it has none.
155
+ */
156
+ action?: AppShellAction;
157
+ /**
158
+ * The sidebar footer, at the foot of the sidebar, under a divider -- Settings
159
+ * and Help on board 20. A slot -- the product's own.
160
+ */
95
161
  footer?: ReactNode;
96
162
  /**
97
163
  * The sidebar collapsed to the 56px rail. A state of the shell: the sidebar
98
- * narrows and every piece it owns collapses off this one flag. The slots
99
- * (switcher, wallet, footer) render their own collapsed form.
164
+ * narrows to icon-only nav and every piece it owns collapses off this one
165
+ * flag. The footer slot renders its own collapsed form. The switcher and the
166
+ * token chip live in the top bar since 0.8.0 (B33-1712) and do not collapse;
167
+ * the rail has no redesign of its own and keeps working as it did.
100
168
  */
101
169
  collapsed?: boolean;
102
170
  /**
@@ -140,4 +208,4 @@ export interface AppShellProps {
140
208
  */
141
209
  children: ReactNode;
142
210
  }
143
- export declare function AppShell({ label, navLabel, nav, topBar, mark, hubHref, switcher, wallet, footer, collapsed, linkComponent, children, }: AppShellProps): import("react").JSX.Element;
211
+ export declare function AppShell({ label, appName, navLabel, nav, topBar, mark, hubHref, org, switcher, wallet, action, footer, collapsed, linkComponent, children, }: AppShellProps): import("react").JSX.Element;
@@ -1,58 +1,83 @@
1
- import { jsxs as i, jsx as r } from "react/jsx-runtime";
1
+ import { jsxs as o, jsx as r, Fragment as v } from "react/jsx-runtime";
2
2
  import { Sidebar as x } from "../Sidebar/Sidebar.js";
3
- import { NavSection as S } from "../NavSection/NavSection.js";
4
- import { NavItem as g } from "../NavItem/NavItem.js";
5
- import { TopBar as j } from "../TopBar/TopBar.js";
6
- import { Wordmark as k } from "../Wordmark/Wordmark.js";
7
- import a from "./AppShell.module.css.js";
8
- function W({
9
- label: e,
10
- navLabel: t,
11
- nav: h,
12
- topBar: v,
13
- mark: f,
14
- hubHref: p,
15
- switcher: m,
16
- wallet: c,
17
- footer: o,
18
- collapsed: n = !1,
19
- linkComponent: s,
20
- children: u
3
+ import { NavSection as j } from "../NavSection/NavSection.js";
4
+ import { NavItem as A } from "../NavItem/NavItem.js";
5
+ import { TopBar as C } from "../TopBar/TopBar.js";
6
+ import { Wordmark as L } from "../Wordmark/Wordmark.js";
7
+ import U from "../../buttons.module.css.js";
8
+ import l from "./AppShell.module.css.js";
9
+ function z({
10
+ label: n,
11
+ appName: a,
12
+ navLabel: s,
13
+ nav: t,
14
+ topBar: c,
15
+ mark: d,
16
+ hubHref: m,
17
+ org: p,
18
+ switcher: N,
19
+ wallet: b,
20
+ action: S,
21
+ footer: u,
22
+ collapsed: i = !1,
23
+ linkComponent: h,
24
+ children: k
21
25
  }) {
22
- return /* @__PURE__ */ i("div", { className: a.shell, children: [
23
- /* @__PURE__ */ i(x, { landmarkLabel: e, collapsed: n, children: [
24
- /* @__PURE__ */ r("div", { className: a.wordmark, children: /* @__PURE__ */ r(k, { mark: f, collapsed: n, hubHref: p, linkComponent: s }) }),
25
- m != null && /* @__PURE__ */ r("div", { className: a.switcher, children: m }),
26
- /* @__PURE__ */ r("nav", { "aria-label": t ?? e, className: a.nav, children: h.map((d, N) => (
27
- // No room for a header at 56px, so on the rail every section renders
28
- // headerless -- its rows collapse to their icons and line up.
29
- /* @__PURE__ */ r(S, { label: n ? void 0 : d.label, children: d.items.map((l, b) => /* @__PURE__ */ r(
30
- g,
31
- {
32
- icon: l.icon,
33
- active: l.active,
34
- collapsed: n,
35
- count: l.count,
36
- countUrgent: l.countUrgent,
37
- href: l.href,
38
- linkComponent: s,
39
- children: l.label
40
- },
41
- b
42
- )) }, N)
43
- )) }),
44
- (c != null || o != null) && /* @__PURE__ */ i("div", { className: a.foot, children: [
45
- c,
46
- o
47
- ] })
48
- ] }),
49
- /* @__PURE__ */ i("div", { className: a.main, children: [
50
- /* @__PURE__ */ r(j, { ...v }),
51
- /* @__PURE__ */ r("main", { className: a.content, children: u })
26
+ return /* @__PURE__ */ o("div", { className: l.shell, children: [
27
+ /* @__PURE__ */ r(
28
+ C,
29
+ {
30
+ ...c,
31
+ start: /* @__PURE__ */ o(v, { children: [
32
+ /* @__PURE__ */ r(L, { mark: d, collapsed: !0, hubHref: m, linkComponent: h }),
33
+ p
34
+ ] }),
35
+ switcher: N,
36
+ tokens: b
37
+ }
38
+ ),
39
+ /* @__PURE__ */ o("div", { className: l.body, children: [
40
+ /* @__PURE__ */ o(x, { landmarkLabel: n, collapsed: i, children: [
41
+ a != null && !i && /* @__PURE__ */ r("p", { className: l.appName, children: a }),
42
+ /* @__PURE__ */ r("nav", { "aria-label": s ?? n, className: l.nav, children: t.map((f, y) => (
43
+ // No room for a header at 56px, so on the rail every section renders
44
+ // headerless -- its rows collapse to their icons and line up.
45
+ /* @__PURE__ */ r(j, { label: i ? void 0 : f.label, children: f.items.map((e, g) => /* @__PURE__ */ r(
46
+ A,
47
+ {
48
+ icon: e.icon,
49
+ active: e.active,
50
+ collapsed: i,
51
+ count: e.count,
52
+ countUrgent: e.countUrgent,
53
+ dot: e.dot,
54
+ href: e.href,
55
+ linkComponent: h,
56
+ children: e.label
57
+ },
58
+ g
59
+ )) }, y)
60
+ )) }),
61
+ /* @__PURE__ */ r($, { action: S, collapsed: i, linkComponent: h }),
62
+ u != null && /* @__PURE__ */ r("div", { className: l.foot, children: u })
63
+ ] }),
64
+ /* @__PURE__ */ r("div", { className: l.main, children: /* @__PURE__ */ r("main", { className: l.content, children: k }) })
52
65
  ] })
53
66
  ] });
54
67
  }
68
+ function $({ action: n, collapsed: a, linkComponent: s }) {
69
+ if (n === void 0 || n.href === void 0 && n.onClick === void 0 || a && n.icon == null) return null;
70
+ const t = /* @__PURE__ */ o(v, { children: [
71
+ n.icon != null && /* @__PURE__ */ r("span", { "aria-hidden": "true", children: n.icon }),
72
+ /* @__PURE__ */ r("span", { className: a ? l.srOnly : void 0, children: n.label })
73
+ ] }), c = `${U.primary} ${l.action}`, d = a ? "true" : void 0;
74
+ if (n.href !== void 0) {
75
+ const m = s ?? "a";
76
+ return /* @__PURE__ */ r("div", { className: l.actionSlot, children: /* @__PURE__ */ r(m, { className: c, href: n.href, "data-collapsed": d, children: t }) });
77
+ }
78
+ return /* @__PURE__ */ r("div", { className: l.actionSlot, children: /* @__PURE__ */ r("button", { type: "button", className: c, onClick: n.onClick, "data-collapsed": d, children: t }) });
79
+ }
55
80
  export {
56
- W as AppShell
81
+ z as AppShell
57
82
  };
58
83
  //# sourceMappingURL=AppShell.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"AppShell.js","sources":["../../../src/components/AppShell/AppShell.tsx"],"sourcesContent":["import type { ElementType, ReactNode } from 'react'\nimport { Sidebar } from '../Sidebar/Sidebar'\nimport { NavSection } from '../NavSection/NavSection'\nimport { NavItem } from '../NavItem/NavItem'\nimport { TopBar, type TopBarProps } from '../TopBar/TopBar'\nimport { Wordmark } from '../Wordmark/Wordmark'\nimport styles from './AppShell.module.css'\n\n/** One nav row, described as data. The consumer never hands the shell markup. */\nexport interface AppShellNavItem {\n /** The row's name. Truncates rather than wrapping; on the rail it collapses to the accessibility tree. */\n label: string\n /** The leading icon. Decorative -- the label carries the name. On the rail it is all that shows. */\n icon?: ReactNode\n /** Marks this row as the current page. Sets `aria-current=\"page\"`. */\n active?: boolean\n /** Trailing-slot count. Omit to leave the slot empty; it still reserves its width so counts line up. */\n count?: number\n /** Renders `count` in the primary color instead of muted. */\n countUrgent?: boolean\n /** Where the row goes. A nav row's destination is data; the shell renders it as a link. */\n href?: string\n}\n\n/** One nav section: an optional header over a run of rows. */\nexport interface AppShellNavSection {\n /** The section header. Omit for a headerless section (the nav's first section has none). On the rail every header drops. */\n label?: string\n /** The section's rows, as data. */\n items: AppShellNavItem[]\n}\n\nexport interface AppShellProps {\n /**\n * The accessible name of the sidebar landmark. A display string in the\n * consumer's own language -- usually the product's name -- that reaches\n * `aria-label` on the `<aside>` and is never rendered as text.\n *\n * **It is an accessible name, not product-awareness.** This prop was called\n * `product` and its comment claimed it was \"the whole of the shell's\n * product-awareness\" (B33-1272). Nothing branches on it, nothing is looked up\n * with it, and no artwork or copy changes because of it -- so the comment\n * described a mechanism that did not exist and sent the next reader looking\n * for the rest of it. The shell is product-aware through `nav`, `switcher`,\n * `wallet` and `footer`, which are the product's own data.\n *\n * **Not the Identity slug.** `AppSwitcher.currentSlug` takes that\n * (`\"wallet\"`, `\"boostscore\"`); this takes what a person would read\n * (\"Wallet\"). The shell carries two names for one product and nothing in the\n * types connects them, so check which kind of string each prop wants.\n */\n label: string\n /**\n * The accessible name of the navigation landmark inside the sidebar.\n * **Defaults to `label`, which is today's behaviour and not the fix.**\n *\n * Pass a distinct string here. Without one the `<aside>` and the `<nav>`\n * nested inside it carry the same accessible name, and a screen-reader user\n * moving by landmark hears \"Wallet complementary\" then \"Wallet navigation\" --\n * the name doing none of the distinguishing that is the one job a landmark\n * label has (B33-1272). Something like \"Wallet sections\" or \"Wallet pages\",\n * in your language, resolves it.\n *\n * **The package cannot default this to anything better, which is why the\n * fallback is the default.** A sensible default would have to compose a word\n * -- \"navigation\", \"sections\" -- and that word is user-facing text a screen\n * reader reads aloud, so it would have to be in the consumer's language. Both\n * consumers default to French. This package hardcodes no user-facing English\n * (`tests/no-hardcoded-strings.test.ts`), and a composed English default would\n * ship an untranslatable word into assistive output for every consumer that\n * never noticed the prop. Repeating `label` is at least a name the consumer\n * chose and wrote in their own language. So the duplicate name is only fully\n * resolved by a consumer passing this.\n */\n navLabel?: string\n /** The navigation, as data. No product hardcodes rows; the shell composes them. */\n nav: AppShellNavSection[]\n /** The top bar's content -- the breadcrumb and the shell actions on its right. */\n topBar: TopBarProps\n /** The Boost33 mark for the wordmark lockup. Passed in, the way Wordmark takes it; on the rail it is all that shows. Pass it unlinked -- `hubHref` links the whole lockup. */\n mark?: ReactNode\n /**\n * Where the hub lives. Set it and the wordmark in the sidebar's top-left\n * corner becomes a link home, rendered through `linkComponent`; omit it and\n * the wordmark is the plain lockup it always was (B33-1699). The consumer\n * supplies the URL -- Identity owns where the hub is and it differs per\n * environment, so the package never hardcodes it. See `Wordmark.hubHref`.\n */\n hubHref?: string\n /**\n * The product switcher, sitting under the wordmark. A slot: the registry it\n * draws is served by Identity (ADR-0003), not owned here. Render its own\n * collapsed form for the rail.\n */\n switcher?: ReactNode\n /**\n * The wallet chip, pinned above the footer. A slot: the balance comes from\n * `@boost33/ledger-client`, never read here. Render its own collapsed form\n * for the rail.\n */\n wallet?: ReactNode\n /** The sidebar footer, below the wallet chip. A slot -- the product's own. */\n footer?: ReactNode\n /**\n * The sidebar collapsed to the 56px rail. A state of the shell: the sidebar\n * narrows and every piece it owns collapses off this one flag. The slots\n * (switcher, wallet, footer) render their own collapsed form.\n */\n collapsed?: boolean\n /**\n * The element every linked nav row renders through. Defaults to a plain `a`;\n * a consumer whose router owns navigation passes its own here and the shell\n * threads it to every row of `nav`, and to the wordmark when `hubHref` links\n * it, so the whole nav goes client-side with the locale prefix intact rather\n * than full-page-reloading (B33-847). This is the shell's one nod to how a\n * consumer routes -- it renders the element, never knowing what it is. See\n * docs/adr/0011.\n *\n * **It reaches the rows of `nav` and the wordmark, and nothing else.** The\n * slots (`switcher`, `wallet`, `footer`, `topBar.actions`) are `ReactNode`s\n * the consumer has already created, so the shell cannot put a prop on what is\n * inside one. A\n * consumer composing a link-bearing component into a slot -- `AppSwitcher`,\n * `AccountMenu`, `NotificationCenter` -- passes `linkComponent` to that\n * component as well, or its rows stay bare anchors and reload the page. The\n * Wallet shipped exactly that defect (B33-1281) by reading this prop as the\n * single place, which is the 2026-10-06 correction on docs/adr/0011.\n */\n linkComponent?: ElementType\n /**\n * The page body. Sits on white, below the top bar, and the shell renders it\n * inside the page's `<main>` -- so a consumer adds no wrapper of its own.\n *\n * **The page's title goes here too.** A screen with a title renders\n * `PageHeader` as the first thing in its own body; a screen with none renders\n * nothing and the top bar's hairline sits straight on the content, which is\n * what the Wallet's overview does and what its 9 Sep baseline shows. The\n * shell used to take a `pageHeader` prop and draw the header itself, above\n * this; B33-1267 removed it, because no consumer ever passed it and Score\n * forbids it by test (the page below owns the sole `h1`).\n *\n * **Everything here scrolls.** This box is the shell's one scrolling element,\n * so the sidebar and top bar stay pinned and the page header goes up with the\n * body. The removed prop sat outside the scroll box and pinned the header\n * instead -- the one real difference between the two spellings, and the\n * reason the choice was worth collapsing to one rather than leaving to a\n * consumer to discover on a long page.\n */\n children: ReactNode\n}\n\n// The shell every authenticated Boost33 screen renders inside. It composes the\n// pieces built in the earlier tickets -- sidebar, wordmark, switcher, nav,\n// wallet chip, top bar -- into one thing a product imports instead of building a\n// sidebar. Everything that differs between products is the product's own data --\n// `nav`, `switcher`, `wallet`, `footer` -- and nothing above the nav (the Boost33\n// wordmark and brand) changes at all. `label` and `navLabel` name the two\n// landmarks and decide nothing; B33-1272 corrected the claim that `label` (then\n// `product`) was the whole of the shell's product-awareness.\n//\n// `PageHeader` is deliberately not in that list. The shell drew it from a\n// `pageHeader` prop until B33-1267; a page's title is the page's, rendered at\n// the top of `children`, which is what both consumers already did.\nexport function AppShell({\n label,\n navLabel,\n nav,\n topBar,\n mark,\n hubHref,\n switcher,\n wallet,\n footer,\n collapsed = false,\n linkComponent,\n children,\n}: AppShellProps) {\n return (\n <div className={styles.shell}>\n <Sidebar landmarkLabel={label} collapsed={collapsed}>\n <div className={styles.wordmark}>\n <Wordmark mark={mark} collapsed={collapsed} hubHref={hubHref} linkComponent={linkComponent} />\n </div>\n {switcher != null && <div className={styles.switcher}>{switcher}</div>}\n {/* Falls back to the sidebar's name rather than composing one. The\n composed string would be a word a screen reader reads aloud, so it\n would have to be in the consumer's language, and this package\n hardcodes no user-facing English. Falling back repeats a name the\n consumer wrote; it does not invent one. See `navLabel`. */}\n <nav aria-label={navLabel ?? label} className={styles.nav}>\n {nav.map((section, sectionIndex) => (\n // No room for a header at 56px, so on the rail every section renders\n // headerless -- its rows collapse to their icons and line up.\n <NavSection key={sectionIndex} label={collapsed ? undefined : section.label}>\n {section.items.map((item, itemIndex) => (\n <NavItem\n key={itemIndex}\n icon={item.icon}\n active={item.active}\n collapsed={collapsed}\n count={item.count}\n countUrgent={item.countUrgent}\n href={item.href}\n linkComponent={linkComponent}\n >\n {item.label}\n </NavItem>\n ))}\n </NavSection>\n ))}\n </nav>\n {(wallet != null || footer != null) && (\n <div className={styles.foot}>\n {wallet}\n {footer}\n </div>\n )}\n </Sidebar>\n {/* `.main` is the product's column -- the top bar over the body. The\n landmark is the body alone, one level down: jumping to main has to\n skip the chrome, and a <main> around the top bar would swallow the\n breadcrumb, the search box and the avatar. */}\n <div className={styles.main}>\n <TopBar {...topBar} />\n {/* The page's main landmark, and the shell's to supply. This was a plain\n div, so no route of any consumer had one unless that consumer\n remembered a wrapper of its own -- the Wallet did\n (`boostledger/src/app/(wallet)/layout.tsx`), and three products each\n having to remember it is three chances to ship without one. What the\n absence costs a screen-reader user is the first move they make on an\n unfamiliar screen: jump to main and start reading. Without one they\n land at the top of the document and walk the wordmark, the switcher,\n every nav row, the wallet chip, the footer links, the sidebar toggle,\n the search box and the avatar before the first word of the page -- on\n every navigation, because the shell is a layout and does not remount.\n\n The landmark is the body alone, which is the half that matters: a\n <main> drawn around `.main` would satisfy any \"has a landmark\" check\n and buy the reader nothing, because jumping to it would land them\n back on the breadcrumb.\n\n A consumer that already wraps `children` now has two, which is worse\n than none -- B33-1265 drops the Wallet's as it takes this version,\n and the CHANGELOG entry tells both consumers to.\n\n An element swap and nothing else: `main` and `div` are both\n `display: block`, the class and every declaration behind it are\n untouched, and all 27 gated screenshot states came back at the ratios\n they held before. */}\n <main className={styles.content}>{children}</main>\n </div>\n </div>\n )\n}\n"],"names":["AppShell","label","navLabel","nav","topBar","mark","hubHref","switcher","wallet","footer","collapsed","linkComponent","children","jsxs","styles","Sidebar","jsx","Wordmark","section","sectionIndex","NavSection","item","itemIndex","NavItem","TopBar"],"mappings":";;;;;;;AAmKO,SAASA,EAAS;AAAA,EACvB,OAAAC;AAAA,EACA,UAAAC;AAAA,EACA,KAAAC;AAAA,EACA,QAAAC;AAAA,EACA,MAAAC;AAAA,EACA,SAAAC;AAAA,EACA,UAAAC;AAAA,EACA,QAAAC;AAAA,EACA,QAAAC;AAAA,EACA,WAAAC,IAAY;AAAA,EACZ,eAAAC;AAAA,EACA,UAAAC;AACF,GAAkB;AAChB,SACE,gBAAAC,EAAC,OAAA,EAAI,WAAWC,EAAO,OACrB,UAAA;AAAA,IAAA,gBAAAD,EAACE,GAAA,EAAQ,eAAed,GAAO,WAAAS,GAC7B,UAAA;AAAA,MAAA,gBAAAM,EAAC,OAAA,EAAI,WAAWF,EAAO,UACrB,UAAA,gBAAAE,EAACC,KAAS,MAAAZ,GAAY,WAAAK,GAAsB,SAAAJ,GAAkB,eAAAK,EAAA,CAA8B,EAAA,CAC9F;AAAA,MACCJ,KAAY,QAAQ,gBAAAS,EAAC,SAAI,WAAWF,EAAO,UAAW,UAAAP,GAAS;AAAA,MAMhE,gBAAAS,EAAC,OAAA,EAAI,cAAYd,KAAYD,GAAO,WAAWa,EAAO,KACnD,UAAAX,EAAI,IAAI,CAACe,GAASC;AAAA;AAAA;AAAA,QAGjB,gBAAAH,EAACI,GAAA,EAA8B,OAAOV,IAAY,SAAYQ,EAAQ,OACnE,UAAAA,EAAQ,MAAM,IAAI,CAACG,GAAMC,MACxB,gBAAAN;AAAA,UAACO;AAAA,UAAA;AAAA,YAEC,MAAMF,EAAK;AAAA,YACX,QAAQA,EAAK;AAAA,YACb,WAAAX;AAAA,YACA,OAAOW,EAAK;AAAA,YACZ,aAAaA,EAAK;AAAA,YAClB,MAAMA,EAAK;AAAA,YACX,eAAAV;AAAA,YAEC,UAAAU,EAAK;AAAA,UAAA;AAAA,UATDC;AAAA,QAAA,CAWR,KAdcH,CAejB;AAAA,OACD,GACH;AAAA,OACEX,KAAU,QAAQC,KAAU,2BAC3B,OAAA,EAAI,WAAWK,EAAO,MACpB,UAAA;AAAA,QAAAN;AAAA,QACAC;AAAA,MAAA,EAAA,CACH;AAAA,IAAA,GAEJ;AAAA,IAKA,gBAAAI,EAAC,OAAA,EAAI,WAAWC,EAAO,MACrB,UAAA;AAAA,MAAA,gBAAAE,EAACQ,GAAA,EAAQ,GAAGpB,GAAQ;AAAA,MA0BpB,gBAAAY,EAAC,QAAA,EAAK,WAAWF,EAAO,SAAU,UAAAF,EAAA,CAAS;AAAA,IAAA,EAAA,CAC7C;AAAA,EAAA,GACF;AAEJ;"}
1
+ {"version":3,"file":"AppShell.js","sources":["../../../src/components/AppShell/AppShell.tsx"],"sourcesContent":["import type { ElementType, ReactNode } from 'react'\nimport { Sidebar } from '../Sidebar/Sidebar'\nimport { NavSection } from '../NavSection/NavSection'\nimport { NavItem } from '../NavItem/NavItem'\nimport { TopBar, type TopBarProps } from '../TopBar/TopBar'\nimport { Wordmark } from '../Wordmark/Wordmark'\nimport buttons from '../../buttons.module.css'\nimport styles from './AppShell.module.css'\n\n/** One nav row, described as data. The consumer never hands the shell markup. */\nexport interface AppShellNavItem {\n /** The row's name. Truncates rather than wrapping; on the rail it collapses to the accessibility tree. */\n label: string\n /** The leading icon. Decorative -- the label carries the name. On the rail it is all that shows. */\n icon?: ReactNode\n /** Marks this row as the current page. Sets `aria-current=\"page\"`. */\n active?: boolean\n /** Trailing-slot count. Omit to leave the slot empty; it still reserves its width so counts line up. */\n count?: number\n /** Draws `count` as the filled \"needs you\" pill instead of muted text. */\n countUrgent?: boolean\n /** A trailing attention dot; the string is what a screen reader hears. Ignored when `count` is set. */\n dot?: string\n /** Where the row goes. A nav row's destination is data; the shell renders it as a link. */\n href?: string\n}\n\n/** One nav section: an optional header over a run of rows. */\n/**\n * The product's main action, drawn as a button at the foot of the sidebar,\n * above the footer's divider (B33-1713, board 20's \"New scan\"). Data, like a\n * nav row: the shell renders it, in the primary tier's ink tone (ADR-0016).\n */\nexport interface AppShellAction {\n /** The button's words, in the consumer's language -- \"New scan\". */\n label: string\n /** A leading icon, decorative. On the 56px rail it is all that shows. */\n icon?: ReactNode\n /** Where it goes. Renders a link through `linkComponent`. Wins over `onClick`. */\n href?: string\n /** What pressing it does. Renders a `<button>`. */\n onClick?: () => void\n}\n\nexport interface AppShellNavSection {\n /** The section header. Omit for a headerless section (the nav's first section has none). On the rail every header drops. */\n label?: string\n /** The section's rows, as data. */\n items: AppShellNavItem[]\n}\n\nexport interface AppShellProps {\n /**\n * The accessible name of the sidebar landmark. A display string in the\n * consumer's own language -- usually the product's name -- that reaches\n * `aria-label` on the `<aside>` and is never rendered as text.\n *\n * **It is an accessible name, not product-awareness.** This prop was called\n * `product` and its comment claimed it was \"the whole of the shell's\n * product-awareness\" (B33-1272). Nothing branches on it, nothing is looked up\n * with it, and no artwork or copy changes because of it -- so the comment\n * described a mechanism that did not exist and sent the next reader looking\n * for the rest of it. The shell is product-aware through `nav`, `switcher`,\n * `wallet` and `footer`, which are the product's own data.\n *\n * **Not the Identity slug.** `AppSwitcher.currentSlug` takes that\n * (`\"wallet\"`, `\"boostscore\"`); this takes what a person would read\n * (\"Wallet\"). The shell carries two names for one product and nothing in the\n * types connects them, so check which kind of string each prop wants.\n */\n label: string\n /**\n * The accessible name of the navigation landmark inside the sidebar.\n * **Defaults to `label`, which is today's behaviour and not the fix.**\n *\n * Pass a distinct string here. Without one the `<aside>` and the `<nav>`\n * nested inside it carry the same accessible name, and a screen-reader user\n * moving by landmark hears \"Wallet complementary\" then \"Wallet navigation\" --\n * the name doing none of the distinguishing that is the one job a landmark\n * label has (B33-1272). Something like \"Wallet sections\" or \"Wallet pages\",\n * in your language, resolves it.\n *\n * **The package cannot default this to anything better, which is why the\n * fallback is the default.** A sensible default would have to compose a word\n * -- \"navigation\", \"sections\" -- and that word is user-facing text a screen\n * reader reads aloud, so it would have to be in the consumer's language. Both\n * consumers default to French. This package hardcodes no user-facing English\n * (`tests/no-hardcoded-strings.test.ts`), and a composed English default would\n * ship an untranslatable word into assistive output for every consumer that\n * never noticed the prop. Repeating `label` is at least a name the consumer\n * chose and wrote in their own language. So the duplicate name is only fully\n * resolved by a consumer passing this.\n */\n navLabel?: string\n /**\n * The product's name, drawn as plain text at the top of the sidebar (B33-1711).\n * The product name only -- \"Boost Score\", in the consumer's language -- with\n * no icon beside it: the product mark belongs in the app switcher, not next to\n * the name. Omit it and nothing is drawn there.\n *\n * It is visible text and nothing else. It names no landmark (that is `label`)\n * and nothing branches on it. Often the same string as `label`; it is a\n * separate prop because one is read aloud as a landmark's name and the other\n * is read on screen, and a consumer may want them to differ.\n *\n * Hidden on the 56px rail, where there is no room for a word.\n */\n appName?: string\n /** The navigation, as data. No product hardcodes rows; the shell composes them. */\n nav: AppShellNavSection[]\n /**\n * The top bar's own content: its `actions` on the right (the notification\n * button and the avatar) and its `labels`. The shell fills the rest of the bar\n * itself -- the mark and `org` on the left, `switcher` and `wallet` in the\n * centre -- so those are props of the shell, not of this object.\n *\n * `breadcrumb` is still accepted and no longer drawn (B33-1712): the redesign\n * puts the mark and the organisation where it was. A sidebar toggle kept\n * inside it goes in `topBar.sidebarToggle` instead, at the far left of the\n * bar, or the rail loses its toggle.\n */\n topBar?: Omit<TopBarProps, 'start' | 'switcher' | 'tokens'>\n /**\n * The Boost33 mark, at the left of the top bar (B33-1712; it was the\n * sidebar's wordmark lockup until 0.8.0). Passed in, the way Wordmark takes\n * it. Pass it unlinked -- `hubHref` links it. The word \"Boost33\" is not drawn\n * beside it; it stays as the link's accessible name.\n */\n mark?: ReactNode\n /**\n * Where the hub lives. Set it and the mark at the left of the top bar becomes\n * a link home, rendered through `linkComponent` (B33-1699). The consumer\n * supplies the URL -- Identity owns where the hub is and it differs per\n * environment, so the package never hardcodes it. See `Wordmark.hubHref`.\n */\n hubHref?: string\n /**\n * The organisation, beside the mark at the left of the top bar (B33-1712,\n * board 20: \"AC Acme Coaching ⌄\"). A slot -- the product's own pill. The\n * package draws no organisation menu: that is AccountMenu (B33-493), which has\n * not passed its gate.\n */\n org?: ReactNode\n /**\n * The app switcher, at the centre of the top bar after its \"Switch Tools →\"\n * label (B33-1712; it sat under the wordmark in the sidebar until 0.8.0). A\n * slot: pass `AppSwitcher`. The registry it draws is served by Identity\n * (ADR-0003), not owned here. It no longer has a rail form to render -- the\n * top bar does not collapse.\n */\n switcher?: ReactNode\n /**\n * The token chip, at the centre of the top bar after the switcher and its\n * \"Check Your Tokens →\" label (B33-1712; it was pinned above the sidebar's\n * footer until 0.8.0). A slot: pass `WalletChip` with `placement=\"topbar\"`.\n * The balance comes from `@boost33/ledger-client`, never read here.\n */\n wallet?: ReactNode\n /**\n * The product's main action, at the foot of the sidebar directly above the\n * footer's divider (B33-1713). Omit it and nothing is drawn. Given neither\n * `href` nor `onClick`, it is not drawn either: a button that does nothing is\n * a control that lies (B33-1270). On the rail it shows its icon alone, or\n * nothing if it has none.\n */\n action?: AppShellAction\n /**\n * The sidebar footer, at the foot of the sidebar, under a divider -- Settings\n * and Help on board 20. A slot -- the product's own.\n */\n footer?: ReactNode\n /**\n * The sidebar collapsed to the 56px rail. A state of the shell: the sidebar\n * narrows to icon-only nav and every piece it owns collapses off this one\n * flag. The footer slot renders its own collapsed form. The switcher and the\n * token chip live in the top bar since 0.8.0 (B33-1712) and do not collapse;\n * the rail has no redesign of its own and keeps working as it did.\n */\n collapsed?: boolean\n /**\n * The element every linked nav row renders through. Defaults to a plain `a`;\n * a consumer whose router owns navigation passes its own here and the shell\n * threads it to every row of `nav`, and to the wordmark when `hubHref` links\n * it, so the whole nav goes client-side with the locale prefix intact rather\n * than full-page-reloading (B33-847). This is the shell's one nod to how a\n * consumer routes -- it renders the element, never knowing what it is. See\n * docs/adr/0011.\n *\n * **It reaches the rows of `nav` and the wordmark, and nothing else.** The\n * slots (`switcher`, `wallet`, `footer`, `topBar.actions`) are `ReactNode`s\n * the consumer has already created, so the shell cannot put a prop on what is\n * inside one. A\n * consumer composing a link-bearing component into a slot -- `AppSwitcher`,\n * `AccountMenu`, `NotificationCenter` -- passes `linkComponent` to that\n * component as well, or its rows stay bare anchors and reload the page. The\n * Wallet shipped exactly that defect (B33-1281) by reading this prop as the\n * single place, which is the 2026-10-06 correction on docs/adr/0011.\n */\n linkComponent?: ElementType\n /**\n * The page body. Sits on white, below the top bar, and the shell renders it\n * inside the page's `<main>` -- so a consumer adds no wrapper of its own.\n *\n * **The page's title goes here too.** A screen with a title renders\n * `PageHeader` as the first thing in its own body; a screen with none renders\n * nothing and the top bar's hairline sits straight on the content, which is\n * what the Wallet's overview does and what its 9 Sep baseline shows. The\n * shell used to take a `pageHeader` prop and draw the header itself, above\n * this; B33-1267 removed it, because no consumer ever passed it and Score\n * forbids it by test (the page below owns the sole `h1`).\n *\n * **Everything here scrolls.** This box is the shell's one scrolling element,\n * so the sidebar and top bar stay pinned and the page header goes up with the\n * body. The removed prop sat outside the scroll box and pinned the header\n * instead -- the one real difference between the two spellings, and the\n * reason the choice was worth collapsing to one rather than leaving to a\n * consumer to discover on a long page.\n */\n children: ReactNode\n}\n\n// The shell every authenticated Boost33 screen renders inside. It composes the\n// pieces built in the earlier tickets -- sidebar, wordmark, switcher, nav,\n// wallet chip, top bar -- into one thing a product imports instead of building a\n// sidebar. Everything that differs between products is the product's own data --\n// `nav`, `switcher`, `wallet`, `footer` -- and nothing above the nav (the Boost33\n// wordmark and brand) changes at all. `label` and `navLabel` name the two\n// landmarks and decide nothing; B33-1272 corrected the claim that `label` (then\n// `product`) was the whole of the shell's product-awareness.\n//\n// `PageHeader` is deliberately not in that list. The shell drew it from a\n// `pageHeader` prop until B33-1267; a page's title is the page's, rendered at\n// the top of `children`, which is what both consumers already did.\nexport function AppShell({\n label,\n appName,\n navLabel,\n nav,\n topBar,\n mark,\n hubHref,\n org,\n switcher,\n wallet,\n action,\n footer,\n collapsed = false,\n linkComponent,\n children,\n}: AppShellProps) {\n return (\n <div className={styles.shell}>\n {/* The bar across the full width, above the sidebar and the product's\n column alike (board 20). First in the DOM as it is first on screen, so\n the tab order runs mark, switcher, token chip, bell, avatar, then the\n nav, then the page. */}\n <TopBar\n {...topBar}\n start={\n <>\n {/* The mark alone, as board 20 draws it: Wordmark's collapsed form\n keeps \"Boost33\" in the accessibility tree, so the link home is\n named, and draws only the mark. */}\n <Wordmark mark={mark} collapsed hubHref={hubHref} linkComponent={linkComponent} />\n {org}\n </>\n }\n switcher={switcher}\n tokens={wallet}\n />\n <div className={styles.body}>\n <Sidebar landmarkLabel={label} collapsed={collapsed}>\n {appName != null && !collapsed && <p className={styles.appName}>{appName}</p>}\n {/* Falls back to the sidebar's name rather than composing one. The\n composed string would be a word a screen reader reads aloud, so it\n would have to be in the consumer's language, and this package\n hardcodes no user-facing English. Falling back repeats a name the\n consumer wrote; it does not invent one. See `navLabel`. */}\n <nav aria-label={navLabel ?? label} className={styles.nav}>\n {nav.map((section, sectionIndex) => (\n // No room for a header at 56px, so on the rail every section renders\n // headerless -- its rows collapse to their icons and line up.\n <NavSection key={sectionIndex} label={collapsed ? undefined : section.label}>\n {section.items.map((item, itemIndex) => (\n <NavItem\n key={itemIndex}\n icon={item.icon}\n active={item.active}\n collapsed={collapsed}\n count={item.count}\n countUrgent={item.countUrgent}\n dot={item.dot}\n href={item.href}\n linkComponent={linkComponent}\n >\n {item.label}\n </NavItem>\n ))}\n </NavSection>\n ))}\n </nav>\n <SidebarAction action={action} collapsed={collapsed} linkComponent={linkComponent} />\n {footer != null && <div className={styles.foot}>{footer}</div>}\n </Sidebar>\n {/* `.main` is the product's column, under the bar and beside the\n sidebar. The landmark is the page body inside it: jumping to main\n has to skip the chrome. */}\n <div className={styles.main}>\n {/* The page's main landmark, and the shell's to supply. This was a plain\n div, so no route of any consumer had one unless that consumer\n remembered a wrapper of its own -- the Wallet did\n (`boostledger/src/app/(wallet)/layout.tsx`), and three products each\n having to remember it is three chances to ship without one. What the\n absence costs a screen-reader user is the first move they make on an\n unfamiliar screen: jump to main and start reading. Without one they\n land at the top of the document and walk the mark, the switcher, the\n token chip, the bell, the avatar, every nav row, the footer links and\n the sidebar toggle before the first word of the page -- on\n every navigation, because the shell is a layout and does not remount.\n\n The landmark is the body alone, which is the half that matters: a\n <main> drawn around the whole shell would satisfy any \"has a\n landmark\" check and buy the reader nothing, because jumping to it\n would land them back on the top bar.\n\n A consumer that already wraps `children` now has two, which is worse\n than none -- B33-1265 drops the Wallet's as it takes this version,\n and the CHANGELOG entry tells both consumers to.\n\n An element swap and nothing else: `main` and `div` are both\n `display: block`, the class and every declaration behind it are\n untouched, and all 27 gated screenshot states came back at the ratios\n they held before. */}\n <main className={styles.content}>{children}</main>\n </div>\n </div>\n </div>\n )\n}\n\n/**\n * The sidebar's action button (B33-1713). The primary tier in its ink tone,\n * full width -- board 20's near-black \"New scan\", not the brand orange, which\n * the package keeps for the money moment (ADR-0016). Pushed to the foot of the\n * column with the footer: the nav above takes the slack.\n */\nfunction SidebarAction({ action, collapsed, linkComponent }: { action: AppShellAction | undefined; collapsed: boolean; linkComponent: ElementType | undefined }) {\n if (action === undefined || (action.href === undefined && action.onClick === undefined)) return null\n if (collapsed && action.icon == null) return null\n\n const content = (\n <>\n {action.icon != null && <span aria-hidden=\"true\">{action.icon}</span>}\n {/* On the rail the label leaves the screen and stays the button's name. */}\n <span className={collapsed ? styles.srOnly : undefined}>{action.label}</span>\n </>\n )\n const className = `${buttons.primary} ${styles.action}`\n const dataCollapsed = collapsed ? 'true' : undefined\n\n if (action.href !== undefined) {\n const Link = linkComponent ?? 'a'\n return (\n <div className={styles.actionSlot}>\n <Link className={className} href={action.href} data-collapsed={dataCollapsed}>\n {content}\n </Link>\n </div>\n )\n }\n return (\n <div className={styles.actionSlot}>\n <button type=\"button\" className={className} onClick={action.onClick} data-collapsed={dataCollapsed}>\n {content}\n </button>\n </div>\n )\n}\n"],"names":["AppShell","label","appName","navLabel","nav","topBar","mark","hubHref","org","switcher","wallet","action","footer","collapsed","linkComponent","children","jsxs","styles","jsx","TopBar","Fragment","Wordmark","Sidebar","section","sectionIndex","NavSection","item","itemIndex","NavItem","SidebarAction","content","className","buttons","dataCollapsed","Link"],"mappings":";;;;;;;;AAyOO,SAASA,EAAS;AAAA,EACvB,OAAAC;AAAA,EACA,SAAAC;AAAA,EACA,UAAAC;AAAA,EACA,KAAAC;AAAA,EACA,QAAAC;AAAA,EACA,MAAAC;AAAA,EACA,SAAAC;AAAA,EACA,KAAAC;AAAA,EACA,UAAAC;AAAA,EACA,QAAAC;AAAA,EACA,QAAAC;AAAA,EACA,QAAAC;AAAA,EACA,WAAAC,IAAY;AAAA,EACZ,eAAAC;AAAA,EACA,UAAAC;AACF,GAAkB;AAChB,SACE,gBAAAC,EAAC,OAAA,EAAI,WAAWC,EAAO,OAKrB,UAAA;AAAA,IAAA,gBAAAC;AAAA,MAACC;AAAA,MAAA;AAAA,QACE,GAAGd;AAAA,QACJ,OACE,gBAAAW,EAAAI,GAAA,EAIE,UAAA;AAAA,UAAA,gBAAAF,EAACG,GAAA,EAAS,MAAAf,GAAY,WAAS,IAAC,SAAAC,GAAkB,eAAAO,GAA8B;AAAA,UAC/EN;AAAA,QAAA,GACH;AAAA,QAEF,UAAAC;AAAA,QACA,QAAQC;AAAA,MAAA;AAAA,IAAA;AAAA,IAEV,gBAAAM,EAAC,OAAA,EAAI,WAAWC,EAAO,MACrB,UAAA;AAAA,MAAA,gBAAAD,EAACM,GAAA,EAAQ,eAAerB,GAAO,WAAAY,GAC5B,UAAA;AAAA,QAAAX,KAAW,QAAQ,CAACW,KAAa,gBAAAK,EAAC,OAAE,WAAWD,EAAO,SAAU,UAAAf,EAAA,CAAQ;AAAA,QAMzE,gBAAAgB,EAAC,OAAA,EAAI,cAAYf,KAAYF,GAAO,WAAWgB,EAAO,KACnD,UAAAb,EAAI,IAAI,CAACmB,GAASC;AAAA;AAAA;AAAA,UAGjB,gBAAAN,EAACO,GAAA,EAA8B,OAAOZ,IAAY,SAAYU,EAAQ,OACnE,UAAAA,EAAQ,MAAM,IAAI,CAACG,GAAMC,MACxB,gBAAAT;AAAA,YAACU;AAAA,YAAA;AAAA,cAEC,MAAMF,EAAK;AAAA,cACX,QAAQA,EAAK;AAAA,cACb,WAAAb;AAAA,cACA,OAAOa,EAAK;AAAA,cACZ,aAAaA,EAAK;AAAA,cAClB,KAAKA,EAAK;AAAA,cACV,MAAMA,EAAK;AAAA,cACX,eAAAZ;AAAA,cAEC,UAAAY,EAAK;AAAA,YAAA;AAAA,YAVDC;AAAA,UAAA,CAYR,KAfcH,CAgBjB;AAAA,SACD,GACH;AAAA,QACA,gBAAAN,EAACW,GAAA,EAAc,QAAAlB,GAAgB,WAAAE,GAAsB,eAAAC,EAAA,CAA8B;AAAA,QAClFF,KAAU,QAAQ,gBAAAM,EAAC,SAAI,WAAWD,EAAO,MAAO,UAAAL,EAAA,CAAO;AAAA,MAAA,GAC1D;AAAA,MAIA,gBAAAM,EAAC,OAAA,EAAI,WAAWD,EAAO,MA0BrB,UAAA,gBAAAC,EAAC,QAAA,EAAK,WAAWD,EAAO,SAAU,UAAAF,EAAA,CAAS,EAAA,CAC7C;AAAA,IAAA,EAAA,CACF;AAAA,EAAA,GACF;AAEJ;AAQA,SAASc,EAAc,EAAE,QAAAlB,GAAQ,WAAAE,GAAW,eAAAC,KAAqH;AAE/J,MADIH,MAAW,UAAcA,EAAO,SAAS,UAAaA,EAAO,YAAY,UACzEE,KAAaF,EAAO,QAAQ,KAAM,QAAO;AAE7C,QAAMmB,IACJ,gBAAAd,EAAAI,GAAA,EACG,UAAA;AAAA,IAAAT,EAAO,QAAQ,QAAQ,gBAAAO,EAAC,UAAK,eAAY,QAAQ,YAAO,KAAA,CAAK;AAAA,IAE9D,gBAAAA,EAAC,UAAK,WAAWL,IAAYI,EAAO,SAAS,QAAY,YAAO,MAAA,CAAM;AAAA,EAAA,GACxE,GAEIc,IAAY,GAAGC,EAAQ,OAAO,IAAIf,EAAO,MAAM,IAC/CgB,IAAgBpB,IAAY,SAAS;AAE3C,MAAIF,EAAO,SAAS,QAAW;AAC7B,UAAMuB,IAAOpB,KAAiB;AAC9B,WACE,gBAAAI,EAAC,OAAA,EAAI,WAAWD,EAAO,YACrB,UAAA,gBAAAC,EAACgB,GAAA,EAAK,WAAAH,GAAsB,MAAMpB,EAAO,MAAM,kBAAgBsB,GAC5D,aACH,GACF;AAAA,EAEJ;AACA,2BACG,OAAA,EAAI,WAAWhB,EAAO,YACrB,4BAAC,UAAA,EAAO,MAAK,UAAS,WAAAc,GAAsB,SAASpB,EAAO,SAAS,kBAAgBsB,GAClF,aACH,GACF;AAEJ;"}
@@ -1,20 +1,26 @@
1
- const t = "_shell_10chf_10", c = "_wordmark_10chf_20", o = "_switcher_10chf_26", _ = "_foot_10chf_33", n = "_nav_10chf_45", s = "_main_10chf_54", h = "_content_10chf_75", e = {
2
- shell: t,
3
- wordmark: c,
4
- switcher: o,
5
- foot: _,
6
- nav: n,
7
- main: s,
8
- content: h
1
+ const o = "_shell_1e11e_13", e = "_body_1e11e_24", n = "_appName_1e11e_39", t = "_foot_1e11e_65", _ = "_actionSlot_1e11e_79", a = "_action_1e11e_79", c = "_srOnly_1e11e_103", s = "_nav_1e11e_116", l = "_main_1e11e_125", i = "_content_1e11e_146", p = {
2
+ shell: o,
3
+ body: e,
4
+ appName: n,
5
+ foot: t,
6
+ actionSlot: _,
7
+ action: a,
8
+ srOnly: c,
9
+ nav: s,
10
+ main: l,
11
+ content: i
9
12
  };
10
13
  export {
11
- h as content,
12
- e as default,
13
- _ as foot,
14
- s as main,
15
- n as nav,
16
- t as shell,
17
- o as switcher,
18
- c as wordmark
14
+ a as action,
15
+ _ as actionSlot,
16
+ n as appName,
17
+ e as body,
18
+ i as content,
19
+ p as default,
20
+ t as foot,
21
+ l as main,
22
+ s as nav,
23
+ o as shell,
24
+ c as srOnly
19
25
  };
20
26
  //# sourceMappingURL=AppShell.module.css.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"AppShell.module.css.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;"}
1
+ {"version":3,"file":"AppShell.module.css.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;"}