@boost33/ui 0.8.0-next.3 → 0.8.0-next.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/README.md +49 -0
  3. package/dist/buttons.module.css.js +4 -4
  4. package/dist/components/AppShell/AppShell.d.ts +20 -6
  5. package/dist/components/AppShell/AppShell.js +52 -50
  6. package/dist/components/AppShell/AppShell.js.map +1 -1
  7. package/dist/components/AppShell/AppShell.module.css.js +21 -21
  8. package/dist/components/AppSwitcher/AppSwitcher.d.ts +6 -0
  9. package/dist/components/AppSwitcher/AppSwitcher.js.map +1 -1
  10. package/dist/components/NavItem/NavItem.d.ts +18 -1
  11. package/dist/components/NavItem/NavItem.js +17 -13
  12. package/dist/components/NavItem/NavItem.js.map +1 -1
  13. package/dist/components/NotificationButton/NotificationButton.d.ts +26 -2
  14. package/dist/components/NotificationButton/NotificationButton.js +27 -15
  15. package/dist/components/NotificationButton/NotificationButton.js.map +1 -1
  16. package/dist/components/NotificationButton/labels.d.ts +19 -2
  17. package/dist/components/NotificationButton/labels.js +11 -9
  18. package/dist/components/NotificationButton/labels.js.map +1 -1
  19. package/dist/components/TopBar/TopBar.module.css.js +8 -8
  20. package/dist/components/WalletChip/WalletChip.d.ts +6 -0
  21. package/dist/components/WalletChip/WalletChip.js +125 -104
  22. package/dist/components/WalletChip/WalletChip.js.map +1 -1
  23. package/dist/components/WalletChip/WalletChip.module.css.js +81 -81
  24. package/dist/components/WalletChip/format.d.ts +2 -2
  25. package/dist/components/WalletChip/format.js +23 -20
  26. package/dist/components/WalletChip/format.js.map +1 -1
  27. package/dist/index.d.ts +1 -1
  28. package/dist/style.css +1 -1
  29. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -219,6 +219,79 @@ Repo-only: the specimen's "Shell redesign — B33-1710" route grows the top bar,
219
219
  the open token popover, board 22's nothing-purchased chip, and the unknown,
220
220
  past-zero and total-only chips.
221
221
 
222
+ ### Added and fixed — the release candidate (0.8.0-next.5)
223
+
224
+ Every new prop and label key here is optional, so an app moving from next.4
225
+ changes only its version pin.
226
+
227
+ - **French figures show their grouping space.** CLDR groups French thousands
228
+ with a narrow no-break space (U+202F), and the kit's faces draw it at almost
229
+ nothing: 1.9px in Inter at 14px, 4.1px in Outfit at 40px. So the token
230
+ popover read "1500" while the chip read "1 500" (Brokers). Every figure the
231
+ chip formats (`formatTokens`, the rail's short form, the threshold
232
+ percentage) now uses an ordinary no-break space (U+00A0) instead. It is
233
+ visible in every face and still never wraps. en-US output is unchanged.
234
+ If your tests compare against `Intl.NumberFormat('fr-FR')` output, apply the
235
+ same swap.
236
+ - **`NotificationButton`** (Content):
237
+ - `unreadCount?: number` goes into the accessible name ("Notifications,
238
+ 3 unread", new optional label `unreadCount`, `%count%`). Above 0 it draws
239
+ the dot. The figure itself is not drawn: board 20 draws a dot.
240
+ - `expanded?: boolean` and `controls?: string` become `aria-expanded` and
241
+ `aria-controls` when it opens a panel. Button form only.
242
+ - `ref` reaches the rendered button or link (React 19 passes `ref` as a
243
+ prop; the link form needs a `linkComponent` that forwards it, as next/link
244
+ does).
245
+ - **`AppShellAction.active?: boolean`** (Off Market): on the action's own page,
246
+ the link form carries `aria-current="page"` and both forms keep the primary
247
+ tier's pressed ground (`.primary[data-active='true']`, its hover colour, in
248
+ either tone).
249
+ - **`countLabel?: string`** on `NavItem` and `AppShellNavItem` (Off Market):
250
+ what a screen reader hears for `count` ("3 à ouvrir"). Given, the visible
251
+ number is hidden from assistive tech and this is read instead.
252
+ - **`attrs?: NavItemAttrs`** on `NavItem` and `AppShellNavItem` (Outbound):
253
+ `data-*` attributes for the row, such as a tour's `data-tour`. Only `data-*`
254
+ names are accepted (also filtered at runtime), and the row's own
255
+ `data-active`/`data-collapsed` win. New exported type `NavItemAttrs`.
256
+ - **README: two consumer notes.**
257
+ - Map the `--boost33-font-*` hooks on `:root`, with `next/font`'s variable
258
+ classes on `<html>`. A mapping on `body` never reaches the kit (Outbound).
259
+ - **Never pass a function from a Server Component to `AppSwitcher` or
260
+ `WalletChip`**, the package's client components: `linkComponent`,
261
+ `onTopUp`, `onOpenChange`. Next refuses it and every signed-in render 500s
262
+ (Passport), and `renderToStaticMarkup` tests cannot see it. Render those
263
+ two inside a `'use client'` file of your own. The `linkComponent` docs on
264
+ both now say so.
265
+ - No approved screenshot moves (see the commit for the comparison).
266
+
267
+ ### Fixed — the top bar fits at 1024px; the token popover stays on screen (0.8.0-next.4)
268
+
269
+ - **Below 80rem of bar, the centre group's two labels leave the screen.**
270
+ next.3 stopped the regions overlapping, but the centre does not shrink, so at
271
+ 1024px Content's bar still needed 1067px and clipped the avatar. Now
272
+ "Switch Tools →" and "Check Your Tokens →" are hidden visually wherever the
273
+ bar's content box is under 80rem (1280px; a full-width bar in a window under
274
+ 1344px) and the centre gives back about 280px. They stay in the
275
+ accessibility tree, before the control each introduces, so a screen reader
276
+ hears the same thing at every width. A container query on the bar
277
+ (`container: boost33-top-bar / inline-size`), so a bar narrower than the
278
+ window is measured by its own width. Measured in Chromium at 1024px with a
279
+ 256px start group and either a 70px or a 220px actions group: no overflow and
280
+ no overlap. If you write container queries in the bar's slots, name your own
281
+ container: the bar is now a container.
282
+ - **The token popover stays on screen.** It hangs from the pill's left edge and
283
+ is 340px wide. At 390px its right edge was at 535px, cutting the threshold
284
+ line and the reset note, and at 1024px it overshot by 8px. It now slides left
285
+ by exactly its overshoot to keep 16px from the right edge (never past 16px
286
+ from the left), and is never wider than the viewport less 32px. Both the
287
+ split chip and `totalOnly` do this. It is measured before paint and again on
288
+ resize.
289
+ - **Below `chromeFrom` the shell fills the screen**: `min-height: 100dvh`
290
+ instead of next.3's `100%`, which resolved against app wrappers with no
291
+ height and left short pages ending grey (Content).
292
+ - **No approved screenshot moves**: all 40 gated states measure the same with
293
+ and without these changes, each on a fresh specimen build.
294
+
222
295
  ### Fixed — the top-bar token chip warns again past 80% of the grant; the top bar holds its layout (0.8.0-next.3)
223
296
 
224
297
  - **The 80% threshold is back in the top bar.** 0.8.0-next.0 moved the chip to
@@ -263,6 +336,7 @@ past-zero and total-only chips.
263
336
  lands. The sidebar says it as an expiry, the popover as a reset.
264
337
  - **Below `chromeFrom` the shell still fills its root** (`min-height: 100%`),
265
338
  so a short page keeps the white ground to the bottom of the screen (Content).
339
+ Replaced by `100dvh` in next.4.
266
340
 
267
341
  ### Added — `AppShell` gets out of the way on a phone: `chromeFrom` and `phoneChrome`
268
342
 
package/README.md CHANGED
@@ -45,6 +45,55 @@ Watch for real test output in the log, not the agent's summary of it.
45
45
  The Wallet migrates first, Score second. **No 1.0 until both render from this
46
46
  package** — until then it has one consumer, which is a fork with a version number.
47
47
 
48
+ ### Fonts: map the hooks on `:root`, not `body`
49
+
50
+ The kit's faces read three hooks, `--boost33-font-display`, `--boost33-font-sans`
51
+ and `--boost33-font-mono`, and `tokens.css` resolves them **on `:root`**
52
+ (`--font-sans: var(--boost33-font-sans, Inter), …`). A custom property resolves
53
+ where it is declared, so a hook set only on `body` never reaches the kit, and
54
+ the kit silently falls back to Inter, Outfit and Geist Mono.
55
+
56
+ With `next/font`, put each font's `.variable` class on `<html>`, not `<body>`,
57
+ and map the hooks on `:root`:
58
+
59
+ ```css
60
+ :root {
61
+ --boost33-font-display: var(--font-outfit);
62
+ --boost33-font-sans: var(--font-inter);
63
+ }
64
+ ```
65
+
66
+ To check it, inspect any kit element (a nav row) and read its computed
67
+ `font-family`. One false alarm: on a dev run where Google Fonts cannot be
68
+ reached, `next/font` substitutes its own `'Inter Fallback'`, and the dev log says
69
+ ``Failed to download `Inter` ``. That is the network, not the mapping (found by
70
+ Outbound).
71
+
72
+ ### Server Components: never pass a function to the kit's client components
73
+
74
+ Three modules in this package are client components (`'use client'`):
75
+ **`AppSwitcher`**, **`WalletChip`** and the internal `NavHeightBudgetWarning`.
76
+ Everything else -- `AppShell`, `TopBar`, `Sidebar`, `NavItem`, `NavSection`,
77
+ `NotificationButton`, `PageHeader`, `Wordmark` and the marks -- renders on the
78
+ server.
79
+
80
+ A Server Component cannot pass a function or a component to a client component:
81
+ props cross that boundary serialised, and Next refuses anything that is not
82
+ plain data. So **from a Server Component, never pass `linkComponent`,
83
+ `onTopUp`, `onOpenChange` or any other function to `AppSwitcher` or
84
+ `WalletChip`.** Next throws ("Functions cannot be passed directly to Client
85
+ Components") and every signed-in render returns a 500 (found by Passport).
86
+ `renderToStaticMarkup` tests do not catch it: they render one tree with no
87
+ boundary.
88
+
89
+ Build the two instead inside one small `'use client'` file of your own, which
90
+ imports your router's `Link` and renders `<AppSwitcher linkComponent={Link} …/>`
91
+ and `<WalletChip onTopUp={…} …/>`, and pass that file's components into
92
+ AppShell's `switcher` and `wallet` slots. Serialisable props (`products`,
93
+ `labels`, `locale`, the balances) can still come from the server through it.
94
+ `AppShell` and `NavItem` take `linkComponent` safely: they are server modules,
95
+ so the function never crosses a boundary.
96
+
48
97
  ## The launch screen
49
98
 
50
99
  `LaunchLoader` is presentational. The page that renders it owns the timing and
@@ -1,10 +1,10 @@
1
- const t = "_primary_qxhao_26", a = "_quiet_qxhao_77", o = {
1
+ const t = "_primary_1nkom_26", o = "_quiet_1nkom_84", r = {
2
2
  primary: t,
3
- quiet: a
3
+ quiet: o
4
4
  };
5
5
  export {
6
- o as default,
6
+ r as default,
7
7
  t as primary,
8
- a as quiet
8
+ o as quiet
9
9
  };
10
10
  //# sourceMappingURL=buttons.module.css.js.map
@@ -1,4 +1,5 @@
1
1
  import { ElementType, ReactNode } from 'react';
2
+ import { NavItemAttrs } from '../NavItem/NavItem';
2
3
  import { TopBarProps } from '../TopBar/TopBar';
3
4
  /** One nav row, described as data. The consumer never hands the shell markup. */
4
5
  export interface AppShellNavItem {
@@ -14,20 +15,23 @@ export interface AppShellNavItem {
14
15
  countUrgent?: boolean;
15
16
  /** A trailing attention dot; the string is what a screen reader hears. Ignored when `count` is set. */
16
17
  dot?: string;
18
+ /** What a screen reader hears for `count`, e.g. "3 to open". See `NavItem.countLabel`. */
19
+ countLabel?: string;
20
+ /** `data-*` attributes for the row, e.g. a product tour's `data-tour`. See `NavItem.attrs`. */
21
+ attrs?: NavItemAttrs;
17
22
  /** Where the row goes. A nav row's destination is data; the shell renders it as a link. */
18
23
  href?: string;
19
24
  }
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
25
  /**
27
26
  * A named width for `AppShell.chromeFrom`, with Tailwind v4's default values:
28
27
  * `md` 48rem (768px), `lg` 64rem (1024px), `xl` 80rem (1280px).
29
28
  */
30
29
  export type AppShellBreakpoint = 'md' | 'lg' | 'xl';
30
+ /**
31
+ * The product's main action, drawn as a button at the foot of the sidebar,
32
+ * above the footer's divider (B33-1713, board 20's "New scan"). Data, like a
33
+ * nav row: the shell renders it, in the primary tier's ink tone (ADR-0016).
34
+ */
31
35
  export interface AppShellAction {
32
36
  /** The button's words, in the consumer's language -- "New scan". */
33
37
  label: string;
@@ -37,7 +41,17 @@ export interface AppShellAction {
37
41
  href?: string;
38
42
  /** What pressing it does. Renders a `<button>`. */
39
43
  onClick?: () => void;
44
+ /**
45
+ * The action's own page is the current one -- "New scan" while on the new
46
+ * scan screen (0.8.0-next.5, Off Market). It is not a nav row, so nothing
47
+ * else in the sidebar says "you are here" there. The link form carries
48
+ * `aria-current="page"`, and both forms take the tier's pressed ground
49
+ * (`--color-neutral-800`, the primary ink tone's hover) so it reads as
50
+ * already chosen.
51
+ */
52
+ active?: boolean;
40
53
  }
54
+ /** One nav section: an optional header over a run of rows. */
41
55
  export interface AppShellNavSection {
42
56
  /** The section header. Omit for a headerless section (the nav's first section has none). On the rail every header drops. */
43
57
  label?: string;
@@ -1,84 +1,86 @@
1
- import { jsxs as c, jsx as r, Fragment as N } from "react/jsx-runtime";
1
+ import { jsxs as i, jsx as r, Fragment as b } from "react/jsx-runtime";
2
2
  import { Sidebar as A } from "../Sidebar/Sidebar.js";
3
- import { NavSection as C } from "../NavSection/NavSection.js";
4
- import { NavItem as L } from "../NavItem/NavItem.js";
3
+ import { NavSection as j } from "../NavSection/NavSection.js";
4
+ import { NavItem as C } from "../NavItem/NavItem.js";
5
5
  import { TopBar as U } from "../TopBar/TopBar.js";
6
6
  import { Wordmark as $ } from "../Wordmark/Wordmark.js";
7
7
  import B from "../../buttons.module.css.js";
8
- import n from "./AppShell.module.css.js";
8
+ import e from "./AppShell.module.css.js";
9
9
  function F({
10
- label: l,
11
- appName: a,
12
- navLabel: s,
13
- nav: o,
14
- topBar: t,
15
- mark: d,
16
- hubHref: m,
17
- org: b,
10
+ label: a,
11
+ appName: n,
12
+ navLabel: u,
13
+ nav: c,
14
+ topBar: d,
15
+ mark: o,
16
+ hubHref: s,
17
+ org: m,
18
18
  switcher: S,
19
19
  wallet: k,
20
20
  action: y,
21
- footer: u,
22
- collapsed: i = !1,
21
+ footer: v,
22
+ collapsed: t = !1,
23
23
  chromeFrom: f,
24
- phoneChrome: v,
24
+ phoneChrome: p,
25
25
  linkComponent: h,
26
26
  children: g
27
27
  }) {
28
- return /* @__PURE__ */ c("div", { className: n.shell, "data-chrome-from": f, children: [
29
- f != null && v != null && /* @__PURE__ */ r("div", { className: n.phoneChrome, children: v }),
28
+ return /* @__PURE__ */ i("div", { className: e.shell, "data-chrome-from": f, children: [
29
+ f != null && p != null && /* @__PURE__ */ r("div", { className: e.phoneChrome, children: p }),
30
30
  /* @__PURE__ */ r(
31
31
  U,
32
32
  {
33
- ...t,
34
- start: /* @__PURE__ */ c(N, { children: [
35
- /* @__PURE__ */ r($, { mark: d, collapsed: !0, hubHref: m, linkComponent: h }),
36
- b
33
+ ...d,
34
+ start: /* @__PURE__ */ i(b, { children: [
35
+ /* @__PURE__ */ r($, { mark: o, collapsed: !0, hubHref: s, linkComponent: h }),
36
+ m
37
37
  ] }),
38
38
  switcher: S,
39
39
  tokens: k
40
40
  }
41
41
  ),
42
- /* @__PURE__ */ c("div", { className: n.body, children: [
43
- /* @__PURE__ */ c(A, { landmarkLabel: l, collapsed: i, children: [
44
- a != null && !i && /* @__PURE__ */ r("p", { className: n.appName, children: a }),
45
- /* @__PURE__ */ r("nav", { "aria-label": s ?? l, className: n.nav, children: o.map((p, x) => (
42
+ /* @__PURE__ */ i("div", { className: e.body, children: [
43
+ /* @__PURE__ */ i(A, { landmarkLabel: a, collapsed: t, children: [
44
+ n != null && !t && /* @__PURE__ */ r("p", { className: e.appName, children: n }),
45
+ /* @__PURE__ */ r("nav", { "aria-label": u ?? a, className: e.nav, children: c.map((N, L) => (
46
46
  // No room for a header at 56px, so on the rail every section renders
47
47
  // headerless -- its rows collapse to their icons and line up.
48
- /* @__PURE__ */ r(C, { label: i ? void 0 : p.label, children: p.items.map((e, j) => /* @__PURE__ */ r(
49
- L,
48
+ /* @__PURE__ */ r(j, { label: t ? void 0 : N.label, children: N.items.map((l, x) => /* @__PURE__ */ r(
49
+ C,
50
50
  {
51
- icon: e.icon,
52
- active: e.active,
53
- collapsed: i,
54
- count: e.count,
55
- countUrgent: e.countUrgent,
56
- dot: e.dot,
57
- href: e.href,
51
+ icon: l.icon,
52
+ active: l.active,
53
+ collapsed: t,
54
+ count: l.count,
55
+ countUrgent: l.countUrgent,
56
+ countLabel: l.countLabel,
57
+ dot: l.dot,
58
+ attrs: l.attrs,
59
+ href: l.href,
58
60
  linkComponent: h,
59
- children: e.label
61
+ children: l.label
60
62
  },
61
- j
62
- )) }, x)
63
+ x
64
+ )) }, L)
63
65
  )) }),
64
- /* @__PURE__ */ r(I, { action: y, collapsed: i, linkComponent: h }),
65
- u != null && /* @__PURE__ */ r("div", { className: n.foot, children: u })
66
+ /* @__PURE__ */ r(I, { action: y, collapsed: t, linkComponent: h }),
67
+ v != null && /* @__PURE__ */ r("div", { className: e.foot, children: v })
66
68
  ] }),
67
- /* @__PURE__ */ r("div", { className: n.main, children: /* @__PURE__ */ r("main", { className: n.content, children: g }) })
69
+ /* @__PURE__ */ r("div", { className: e.main, children: /* @__PURE__ */ r("main", { className: e.content, children: g }) })
68
70
  ] })
69
71
  ] });
70
72
  }
71
- function I({ action: l, collapsed: a, linkComponent: s }) {
72
- if (l === void 0 || l.href === void 0 && l.onClick === void 0 || a && l.icon == null) return null;
73
- const o = /* @__PURE__ */ c(N, { children: [
74
- l.icon != null && /* @__PURE__ */ r("span", { "aria-hidden": "true", children: l.icon }),
75
- /* @__PURE__ */ r("span", { className: a ? n.srOnly : void 0, children: l.label })
76
- ] }), t = `${B.primary} ${n.action}`, d = a ? "true" : void 0;
77
- if (l.href !== void 0) {
78
- const m = s ?? "a";
79
- return /* @__PURE__ */ r("div", { className: n.actionSlot, children: /* @__PURE__ */ r(m, { className: t, href: l.href, "data-collapsed": d, children: o }) });
73
+ function I({ action: a, collapsed: n, linkComponent: u }) {
74
+ if (a === void 0 || a.href === void 0 && a.onClick === void 0 || n && a.icon == null) return null;
75
+ const c = /* @__PURE__ */ i(b, { children: [
76
+ a.icon != null && /* @__PURE__ */ r("span", { "aria-hidden": "true", children: a.icon }),
77
+ /* @__PURE__ */ r("span", { className: n ? e.srOnly : void 0, children: a.label })
78
+ ] }), d = `${B.primary} ${e.action}`, o = n ? "true" : void 0, s = a.active ? "true" : void 0;
79
+ if (a.href !== void 0) {
80
+ const m = u ?? "a";
81
+ return /* @__PURE__ */ r("div", { className: e.actionSlot, children: /* @__PURE__ */ r(m, { className: d, href: a.href, "aria-current": a.active ? "page" : void 0, "data-collapsed": o, "data-active": s, children: c }) });
80
82
  }
81
- return /* @__PURE__ */ r("div", { className: n.actionSlot, children: /* @__PURE__ */ r("button", { type: "button", className: t, onClick: l.onClick, "data-collapsed": d, children: o }) });
83
+ return /* @__PURE__ */ r("div", { className: e.actionSlot, children: /* @__PURE__ */ r("button", { type: "button", className: d, onClick: a.onClick, "data-collapsed": o, "data-active": s, children: c }) });
82
84
  }
83
85
  export {
84
86
  F as AppShell
@@ -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 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 */\n/**\n * A named width for `AppShell.chromeFrom`, with Tailwind v4's default values:\n * `md` 48rem (768px), `lg` 64rem (1024px), `xl` 80rem (1280px).\n */\nexport type AppShellBreakpoint = 'md' | 'lg' | 'xl'\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 width from which the shell draws its own chrome -- the top bar and the\n * sidebar. Below it the shell hides both and the page body takes the full\n * width, so a product renders its own phone header and navigation around the\n * shell. Omit it and the chrome is drawn at every width, as before.\n *\n * Not a mobile shell: nothing is redesigned below the breakpoint, the shell\n * only gets out of the way. AppShell has no phone layout of its own, and the\n * 0.8 top bar runs off a 390px screen.\n *\n * Done in the shell's own CSS, `@media (width < …)`, so the first paint is\n * already right and a server-rendered page does not flash the desktop chrome\n * on a phone, which a `matchMedia` switch in the product does. That is also\n * why it is a name and not a number: a media query cannot read a value passed\n * at runtime. The names and widths are Tailwind v4's defaults, so `lg` here\n * and `lg:` in a product's own markup flip at the same pixel:\n * `md` 48rem (768px), `lg` 64rem (1024px), `xl` 80rem (1280px).\n *\n * Below the breakpoint the page scrolls the document instead of the shell's\n * content box, so a product header above the shell scrolls with the page. The\n * `<main>` landmark stays; the hidden chrome is `display: none`, so it leaves\n * the accessibility tree too.\n */\n chromeFrom?: AppShellBreakpoint\n /**\n * The product's own phone chrome -- its header, its navigation -- drawn by\n * the shell above the page body and shown only below `chromeFrom`, the\n * inverse of the top bar and sidebar. The shell's own CSS does both halves,\n * so a product passes its phone header here and writes no selector and no\n * `lg:hidden` of its own.\n *\n * Above `<main>`, not inside it: \"jump to main\" skips the chrome on a phone\n * the way it skips the top bar and sidebar on a desktop. A slot -- the\n * product's own markup; the shell draws no phone design.\n *\n * Ignored without `chromeFrom`: with no breakpoint there is no width at which\n * it would show.\n */\n phoneChrome?: ReactNode\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 chromeFrom,\n phoneChrome,\n linkComponent,\n children,\n}: AppShellProps) {\n return (\n <div className={styles.shell} data-chrome-from={chromeFrom}>\n {chromeFrom != null && phoneChrome != null && <div className={styles.phoneChrome}>{phoneChrome}</div>}\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","chromeFrom","phoneChrome","linkComponent","children","styles","jsx","TopBar","jsxs","Fragment","Wordmark","Sidebar","section","sectionIndex","NavSection","item","itemIndex","NavItem","SidebarAction","content","className","buttons","dataCollapsed","Link"],"mappings":"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"}
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, type NavItemAttrs } 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 /** What a screen reader hears for `count`, e.g. \"3 to open\". See `NavItem.countLabel`. */\n countLabel?: string\n /** `data-*` attributes for the row, e.g. a product tour's `data-tour`. See `NavItem.attrs`. */\n attrs?: NavItemAttrs\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/**\n * A named width for `AppShell.chromeFrom`, with Tailwind v4's default values:\n * `md` 48rem (768px), `lg` 64rem (1024px), `xl` 80rem (1280px).\n */\nexport type AppShellBreakpoint = 'md' | 'lg' | 'xl'\n\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 * The action's own page is the current one -- \"New scan\" while on the new\n * scan screen (0.8.0-next.5, Off Market). It is not a nav row, so nothing\n * else in the sidebar says \"you are here\" there. The link form carries\n * `aria-current=\"page\"`, and both forms take the tier's pressed ground\n * (`--color-neutral-800`, the primary ink tone's hover) so it reads as\n * already chosen.\n */\n active?: boolean\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 /**\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 width from which the shell draws its own chrome -- the top bar and the\n * sidebar. Below it the shell hides both and the page body takes the full\n * width, so a product renders its own phone header and navigation around the\n * shell. Omit it and the chrome is drawn at every width, as before.\n *\n * Not a mobile shell: nothing is redesigned below the breakpoint, the shell\n * only gets out of the way. AppShell has no phone layout of its own, and the\n * 0.8 top bar runs off a 390px screen.\n *\n * Done in the shell's own CSS, `@media (width < …)`, so the first paint is\n * already right and a server-rendered page does not flash the desktop chrome\n * on a phone, which a `matchMedia` switch in the product does. That is also\n * why it is a name and not a number: a media query cannot read a value passed\n * at runtime. The names and widths are Tailwind v4's defaults, so `lg` here\n * and `lg:` in a product's own markup flip at the same pixel:\n * `md` 48rem (768px), `lg` 64rem (1024px), `xl` 80rem (1280px).\n *\n * Below the breakpoint the page scrolls the document instead of the shell's\n * content box, so a product header above the shell scrolls with the page. The\n * `<main>` landmark stays; the hidden chrome is `display: none`, so it leaves\n * the accessibility tree too.\n */\n chromeFrom?: AppShellBreakpoint\n /**\n * The product's own phone chrome -- its header, its navigation -- drawn by\n * the shell above the page body and shown only below `chromeFrom`, the\n * inverse of the top bar and sidebar. The shell's own CSS does both halves,\n * so a product passes its phone header here and writes no selector and no\n * `lg:hidden` of its own.\n *\n * Above `<main>`, not inside it: \"jump to main\" skips the chrome on a phone\n * the way it skips the top bar and sidebar on a desktop. A slot -- the\n * product's own markup; the shell draws no phone design.\n *\n * Ignored without `chromeFrom`: with no breakpoint there is no width at which\n * it would show.\n */\n phoneChrome?: ReactNode\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 chromeFrom,\n phoneChrome,\n linkComponent,\n children,\n}: AppShellProps) {\n return (\n <div className={styles.shell} data-chrome-from={chromeFrom}>\n {chromeFrom != null && phoneChrome != null && <div className={styles.phoneChrome}>{phoneChrome}</div>}\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 countLabel={item.countLabel}\n dot={item.dot}\n attrs={item.attrs}\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 const dataActive = action.active ? '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} aria-current={action.active ? 'page' : undefined} data-collapsed={dataCollapsed} data-active={dataActive}>\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} data-active={dataActive}>\n {content}\n </button>\n </div>\n )\n}\n"],"names":["AppShell","label","appName","navLabel","nav","topBar","mark","hubHref","org","switcher","wallet","action","footer","collapsed","chromeFrom","phoneChrome","linkComponent","children","styles","jsx","TopBar","jsxs","Fragment","Wordmark","Sidebar","section","sectionIndex","NavSection","item","itemIndex","NavItem","SidebarAction","content","className","buttons","dataCollapsed","dataActive","Link"],"mappings":"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"}
@@ -1,28 +1,28 @@
1
- const t = "_shell_kt75f_13", o = "_body_kt75f_24", n = "_appName_kt75f_39", _ = "_foot_kt75f_65", c = "_actionSlot_kt75f_79", s = "_action_kt75f_79", a = "_srOnly_kt75f_103", e = "_nav_kt75f_116", f = "_main_kt75f_125", l = "_content_kt75f_146", k = "_phoneChrome_kt75f_203", p = {
2
- shell: t,
3
- body: o,
4
- appName: n,
1
+ const o = "_shell_l6zer_13", n = "_body_l6zer_24", t = "_appName_l6zer_39", _ = "_foot_l6zer_65", e = "_actionSlot_l6zer_79", l = "_action_l6zer_79", c = "_srOnly_l6zer_103", s = "_nav_l6zer_116", a = "_main_l6zer_125", r = "_content_l6zer_146", z = "_phoneChrome_l6zer_207", p = {
2
+ shell: o,
3
+ body: n,
4
+ appName: t,
5
5
  foot: _,
6
- actionSlot: c,
7
- action: s,
8
- srOnly: a,
9
- nav: e,
10
- main: f,
11
- content: l,
12
- phoneChrome: k
6
+ actionSlot: e,
7
+ action: l,
8
+ srOnly: c,
9
+ nav: s,
10
+ main: a,
11
+ content: r,
12
+ phoneChrome: z
13
13
  };
14
14
  export {
15
- s as action,
16
- c as actionSlot,
17
- n as appName,
18
- o as body,
19
- l as content,
15
+ l as action,
16
+ e as actionSlot,
17
+ t as appName,
18
+ n as body,
19
+ r as content,
20
20
  p as default,
21
21
  _ as foot,
22
- f as main,
23
- e as nav,
24
- k as phoneChrome,
25
- t as shell,
26
- a as srOnly
22
+ a as main,
23
+ s as nav,
24
+ z as phoneChrome,
25
+ o as shell,
26
+ c as srOnly
27
27
  };
28
28
  //# sourceMappingURL=AppShell.module.css.js.map
@@ -169,6 +169,12 @@ export interface AppSwitcherProps {
169
169
  * The element the product and hub rows render through. Defaults to a plain
170
170
  * `a`; a router-owning consumer passes its own so the rows route client-side
171
171
  * with the locale prefix intact (ADR-0011, B33-847).
172
+ *
173
+ * **Pass it from a client component only.** This is a `'use client'` module,
174
+ * and a Server Component cannot hand a function across that boundary: Next
175
+ * throws and the render 500s (Passport). Render this component inside a
176
+ * `'use client'` file of your own and pass that into AppShell's slot. See the
177
+ * README, "Server Components".
172
178
  */
173
179
  linkComponent?: ElementType;
174
180
  /** The 56px rail. The trigger collapses to the current product's mark alone. */