@boost33/ui 0.8.0-next.0 → 0.8.0-next.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -52,13 +52,16 @@ What a consumer sees, and what it has to do:
52
52
  would tell a client their product does not exist. This overrules the
53
53
  Vocabulary document's "the switcher shows every product regardless of state",
54
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.
55
+ - **The "Soon" tag is gone with them.** `labels.soon` stays in the type, now
56
+ optional and deprecated, and is ignored: a labels object that sets it still
57
+ compiles, and a catalogue typed as a full `AppSwitcherLabels` no longer has to
58
+ carry it. Drop it when convenient.
58
59
  - **The switcher's account row is gone.** `account` is now optional and
59
60
  ignored, and `AppSwitcherAccount` is deprecated. The organisation moves to the
60
61
  top bar in part 2. Nothing breaks if you keep passing it; stop when
61
- convenient. Its `onSelect` is never called.
62
+ convenient. Its `onSelect` is never called. In development, passing
63
+ `account` logs a console warning once (`process.env.NODE_ENV` other than
64
+ `production`); production builds are silent.
62
65
  - **New, optional, per product row:** `group` (a section header, e.g. "Your
63
66
  tools" -- rows are grouped in the order served; no groups means one plain
64
67
  list) and `trailing` (`{ kind: 'value', value: '8,940' }` or
@@ -157,7 +160,9 @@ What a consumer sees, and what it has to do:
157
160
  **If you put `SidebarToggle` in `topBar.breadcrumb` (the Wallet does), move it
158
161
  to the new `topBar.sidebarToggle` slot, or your rail loses its toggle.** The
159
162
  screenshot gate does not catch this: the bar passed its baseline at 0.19%
160
- with the toggle gone.
163
+ with the toggle gone. So in development, passing `breadcrumb` -- to `TopBar`
164
+ or through AppShell `topBar` -- logs a console warning once, naming where a
165
+ toggle and an organisation switcher go instead; production builds are silent.
161
166
  - **New `topBar.sidebarToggle`** (and `TopBar.sidebarToggle`): your rail
162
167
  toggle, drawn at the far left of the bar before the mark. No board draws it;
163
168
  judged at the gate.
@@ -214,6 +219,78 @@ Repo-only: the specimen's "Shell redesign — B33-1710" route grows the top bar,
214
219
  the open token popover, board 22's nothing-purchased chip, and the unknown,
215
220
  past-zero and total-only chips.
216
221
 
222
+ ### Added — `AppShell` gets out of the way on a phone: `chromeFrom` and `phoneChrome`
223
+
224
+ AppShell has no phone layout, and the 0.8 top bar runs off a 390px screen.
225
+ Products keep their own phone chrome below a breakpoint, and until now the only
226
+ ways to do that were a `matchMedia` switch (the desktop shell flashes on a phone
227
+ before it swaps) or CSS reaching into the shell's DOM shape. Both options are opt-in;
228
+ a shell that sets neither renders exactly as before.
229
+
230
+ - **`chromeFrom?: 'md' | 'lg' | 'xl'`** (new type `AppShellBreakpoint`). Below
231
+ that viewport width the shell hides its own top bar and sidebar, the page
232
+ body takes the full width, and the document scrolls instead of the shell's
233
+ content box. Done in the shell's own `@media` CSS, so the first paint is
234
+ already right, with no flash and no second render of the page. The widths are
235
+ Tailwind v4's defaults -- `md` 48rem (768px), `lg` 64rem (1024px), `xl` 80rem
236
+ (1280px) -- so `lg` here and `lg:` in your own markup flip at the same pixel.
237
+ It is a name and not a number because a media query cannot read a value
238
+ passed at runtime.
239
+ - **`phoneChrome?: ReactNode`**: your phone header and navigation. The shell
240
+ draws it above the page body, outside `<main>`, and shows it only below
241
+ `chromeFrom`. Ignored without `chromeFrom`. The shell's wrapper around it
242
+ draws no box (`display: contents`), so a `position: sticky` header you pass
243
+ stays pinned while the page scrolls (fixed after 0.8.0-next.1, where it
244
+ scrolled away). Pass your phone chrome here and
245
+ drop any `lg:hidden` and any selector that hid the kit's bar or `<aside>` by
246
+ position: those depended on a DOM shape this package does not promise.
247
+ - Not a mobile shell. Nothing is redesigned below the breakpoint; the shell only
248
+ stops drawing its desktop chrome. A phone design needs a board first.
249
+ - The sidebar's development-only nav-height warning no longer fires when the
250
+ sidebar is hidden this way.
251
+
252
+ ### Note — in a Tailwind v4 app, `style.css` alone re-sizes the app's own utilities
253
+
254
+ **Importing `@boost33/ui/style.css` changes what a Tailwind v4 app already has
255
+ on screen, with or without `theme.css`.** Off Market and Brokers measured it
256
+ adopting 0.8.0-next.0. This is not new in 0.8, but 0.8 is when Tailwind v4
257
+ apps first take the shell.
258
+
259
+ Why: `style.css` carries `tokens.css`, which declares this package's tokens on
260
+ `:root`, unlayered, under the names Tailwind v4 itself uses (`--text-*`,
261
+ `--leading-*`, `--tracking-*`, `--radius-*`, `--font-*`, `--font-weight-*`,
262
+ `--color-*`). Tailwind's default utilities read those names through `var()`
263
+ (`.text-sm { font-size: var(--text-sm) }`), and an unlayered declaration beats
264
+ Tailwind's `@layer theme`. So in the app, `text-sm` becomes 13px (Tailwind's
265
+ 14px) with a 13px line height, `text-base` 15px (16px), and `rounded-sm`, `-md`,
266
+ `-lg`, `-xl`, `-2xl` become 3/4/6/8/12px (4/6/8/12/16px). The `leading-*` and
267
+ `tracking-*` steps move too.
268
+
269
+ To keep your own scale, tell Tailwind to write literal values into its
270
+ utilities so they never read `:root`. Either import Tailwind's theme inline:
271
+
272
+ ```css
273
+ @layer theme, base, components, utilities;
274
+ @import "tailwindcss/theme.css" layer(theme) theme(inline);
275
+ @import "tailwindcss/preflight.css" layer(base);
276
+ @import "tailwindcss/utilities.css" layer(utilities);
277
+ @import "@boost33/ui/style.css";
278
+ ```
279
+
280
+ or pin each colliding name you use under `@theme inline { … }` with Tailwind's
281
+ value. Either way the kit's components keep reading `:root` and render as
282
+ designed. Brokers measured identical computed styles on all 12 of its pages
283
+ with the first recipe.
284
+
285
+ `theme.css` is a separate, optional step on top of this. It adds utilities for
286
+ names only this package has (`bg-surface`, `border-border`, …) and makes the
287
+ kit's values the app's whole scale (ADR-0011, ADR-0012). Every kit component
288
+ renders correctly without it (the specimen imports no `theme.css`). An app that
289
+ used those utilities and drops `theme.css` loses them.
290
+
291
+ The shell also sets `font-family: var(--font-sans)` (Inter) on its frame, so
292
+ `children` inherit Inter unless the page sets its own family.
293
+
217
294
  ### Changed — `CHANGELOG.md` now ships inside the package
218
295
 
219
296
  `package.json` `files` adds `CHANGELOG.md` next to `dist`, so this file is in
@@ -23,6 +23,11 @@ export interface AppShellNavItem {
23
23
  * above the footer's divider (B33-1713, board 20's "New scan"). Data, like a
24
24
  * nav row: the shell renders it, in the primary tier's ink tone (ADR-0016).
25
25
  */
26
+ /**
27
+ * A named width for `AppShell.chromeFrom`, with Tailwind v4's default values:
28
+ * `md` 48rem (768px), `lg` 64rem (1024px), `xl` 80rem (1280px).
29
+ */
30
+ export type AppShellBreakpoint = 'md' | 'lg' | 'xl';
26
31
  export interface AppShellAction {
27
32
  /** The button's words, in the consumer's language -- "New scan". */
28
33
  label: string;
@@ -167,6 +172,45 @@ export interface AppShellProps {
167
172
  * the rail has no redesign of its own and keeps working as it did.
168
173
  */
169
174
  collapsed?: boolean;
175
+ /**
176
+ * The width from which the shell draws its own chrome -- the top bar and the
177
+ * sidebar. Below it the shell hides both and the page body takes the full
178
+ * width, so a product renders its own phone header and navigation around the
179
+ * shell. Omit it and the chrome is drawn at every width, as before.
180
+ *
181
+ * Not a mobile shell: nothing is redesigned below the breakpoint, the shell
182
+ * only gets out of the way. AppShell has no phone layout of its own, and the
183
+ * 0.8 top bar runs off a 390px screen.
184
+ *
185
+ * Done in the shell's own CSS, `@media (width < …)`, so the first paint is
186
+ * already right and a server-rendered page does not flash the desktop chrome
187
+ * on a phone, which a `matchMedia` switch in the product does. That is also
188
+ * why it is a name and not a number: a media query cannot read a value passed
189
+ * at runtime. The names and widths are Tailwind v4's defaults, so `lg` here
190
+ * and `lg:` in a product's own markup flip at the same pixel:
191
+ * `md` 48rem (768px), `lg` 64rem (1024px), `xl` 80rem (1280px).
192
+ *
193
+ * Below the breakpoint the page scrolls the document instead of the shell's
194
+ * content box, so a product header above the shell scrolls with the page. The
195
+ * `<main>` landmark stays; the hidden chrome is `display: none`, so it leaves
196
+ * the accessibility tree too.
197
+ */
198
+ chromeFrom?: AppShellBreakpoint;
199
+ /**
200
+ * The product's own phone chrome -- its header, its navigation -- drawn by
201
+ * the shell above the page body and shown only below `chromeFrom`, the
202
+ * inverse of the top bar and sidebar. The shell's own CSS does both halves,
203
+ * so a product passes its phone header here and writes no selector and no
204
+ * `lg:hidden` of its own.
205
+ *
206
+ * Above `<main>`, not inside it: "jump to main" skips the chrome on a phone
207
+ * the way it skips the top bar and sidebar on a desktop. A slot -- the
208
+ * product's own markup; the shell draws no phone design.
209
+ *
210
+ * Ignored without `chromeFrom`: with no breakpoint there is no width at which
211
+ * it would show.
212
+ */
213
+ phoneChrome?: ReactNode;
170
214
  /**
171
215
  * The element every linked nav row renders through. Defaults to a plain `a`;
172
216
  * a consumer whose router owns navigation passes its own here and the shell
@@ -208,4 +252,4 @@ export interface AppShellProps {
208
252
  */
209
253
  children: ReactNode;
210
254
  }
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;
255
+ export declare function AppShell({ label, appName, navLabel, nav, topBar, mark, hubHref, org, switcher, wallet, action, footer, collapsed, chromeFrom, phoneChrome, linkComponent, children, }: AppShellProps): import("react").JSX.Element;
@@ -1,49 +1,52 @@
1
- import { jsxs as o, jsx as r, Fragment as v } from "react/jsx-runtime";
2
- import { Sidebar as x } from "../Sidebar/Sidebar.js";
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,
1
+ import { jsxs as c, jsx as r, Fragment as N } from "react/jsx-runtime";
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";
5
+ import { TopBar as U } from "../TopBar/TopBar.js";
6
+ import { Wordmark as $ } from "../Wordmark/Wordmark.js";
7
+ import B from "../../buttons.module.css.js";
8
+ import n from "./AppShell.module.css.js";
9
+ function F({
10
+ label: l,
11
11
  appName: a,
12
12
  navLabel: s,
13
- nav: t,
14
- topBar: c,
13
+ nav: o,
14
+ topBar: t,
15
15
  mark: d,
16
16
  hubHref: m,
17
- org: p,
18
- switcher: N,
19
- wallet: b,
20
- action: S,
17
+ org: b,
18
+ switcher: S,
19
+ wallet: k,
20
+ action: y,
21
21
  footer: u,
22
22
  collapsed: i = !1,
23
+ chromeFrom: f,
24
+ phoneChrome: v,
23
25
  linkComponent: h,
24
- children: k
26
+ children: g
25
27
  }) {
26
- return /* @__PURE__ */ o("div", { className: l.shell, children: [
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 }),
27
30
  /* @__PURE__ */ r(
28
- C,
31
+ U,
29
32
  {
30
- ...c,
31
- start: /* @__PURE__ */ o(v, { children: [
32
- /* @__PURE__ */ r(L, { mark: d, collapsed: !0, hubHref: m, linkComponent: h }),
33
- p
33
+ ...t,
34
+ start: /* @__PURE__ */ c(N, { children: [
35
+ /* @__PURE__ */ r($, { mark: d, collapsed: !0, hubHref: m, linkComponent: h }),
36
+ b
34
37
  ] }),
35
- switcher: N,
36
- tokens: b
38
+ switcher: S,
39
+ tokens: k
37
40
  }
38
41
  ),
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) => (
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) => (
43
46
  // No room for a header at 56px, so on the rail every section renders
44
47
  // 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,
48
+ /* @__PURE__ */ r(C, { label: i ? void 0 : p.label, children: p.items.map((e, j) => /* @__PURE__ */ r(
49
+ L,
47
50
  {
48
51
  icon: e.icon,
49
52
  active: e.active,
@@ -55,29 +58,29 @@ function z({
55
58
  linkComponent: h,
56
59
  children: e.label
57
60
  },
58
- g
59
- )) }, y)
61
+ j
62
+ )) }, x)
60
63
  )) }),
61
- /* @__PURE__ */ r($, { action: S, collapsed: i, linkComponent: h }),
62
- u != null && /* @__PURE__ */ r("div", { className: l.foot, children: u })
64
+ /* @__PURE__ */ r(I, { action: y, collapsed: i, linkComponent: h }),
65
+ u != null && /* @__PURE__ */ r("div", { className: n.foot, children: u })
63
66
  ] }),
64
- /* @__PURE__ */ r("div", { className: l.main, children: /* @__PURE__ */ r("main", { className: l.content, children: k }) })
67
+ /* @__PURE__ */ r("div", { className: n.main, children: /* @__PURE__ */ r("main", { className: n.content, children: g }) })
65
68
  ] })
66
69
  ] });
67
70
  }
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) {
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) {
75
78
  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 }) });
79
+ return /* @__PURE__ */ r("div", { className: n.actionSlot, children: /* @__PURE__ */ r(m, { className: t, href: l.href, "data-collapsed": d, children: o }) });
77
80
  }
78
- return /* @__PURE__ */ r("div", { className: l.actionSlot, children: /* @__PURE__ */ r("button", { type: "button", className: c, onClick: n.onClick, "data-collapsed": d, children: t }) });
81
+ return /* @__PURE__ */ r("div", { className: n.actionSlot, children: /* @__PURE__ */ r("button", { type: "button", className: t, onClick: l.onClick, "data-collapsed": d, children: o }) });
79
82
  }
80
83
  export {
81
- z as AppShell
84
+ F as AppShell
82
85
  };
83
86
  //# 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 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":"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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 } 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,26 +1,28 @@
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 = {
1
+ const o = "_shell_1rvaz_13", n = "_body_1rvaz_24", t = "_appName_1rvaz_39", _ = "_foot_1rvaz_65", a = "_actionSlot_1rvaz_79", c = "_action_1rvaz_79", s = "_srOnly_1rvaz_103", e = "_nav_1rvaz_116", r = "_main_1rvaz_125", l = "_content_1rvaz_146", v = "_phoneChrome_1rvaz_201", z = {
2
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
3
+ body: n,
4
+ appName: t,
5
+ foot: _,
6
+ actionSlot: a,
7
+ action: c,
8
+ srOnly: s,
9
+ nav: e,
10
+ main: r,
11
+ content: l,
12
+ phoneChrome: v
12
13
  };
13
14
  export {
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,
15
+ c as action,
16
+ a as actionSlot,
17
+ t as appName,
18
+ n as body,
19
+ l as content,
20
+ z as default,
21
+ _ as foot,
22
+ r as main,
23
+ e as nav,
24
+ v as phoneChrome,
23
25
  o as shell,
24
- c as srOnly
26
+ s as srOnly
25
27
  };
26
28
  //# 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":";;;;;;;;;;;;;"}
@@ -117,7 +117,8 @@ export interface AppSwitcherHub {
117
117
  * left-hand slot, as the consumer's own pill (B33-1712). The type stays so a
118
118
  * consumer's existing `account` object still compiles; nothing reads it.
119
119
  *
120
- * @deprecated Rendered nowhere since 0.8.0 (B33-1711). Stop passing `account`.
120
+ * @deprecated Rendered nowhere since 0.8.0 (B33-1711). Stop passing `account`;
121
+ * in development, passing it logs a console warning once.
121
122
  */
122
123
  export interface AppSwitcherAccount {
123
124
  /** The organisation's name, e.g. "Acme Coaching". */
@@ -154,7 +155,8 @@ export interface AppSwitcherProps {
154
155
  * 0.8.0, when the redesign took that row off the panel (B33-1711).
155
156
  *
156
157
  * @deprecated Rendered nowhere since 0.8.0. The organisation goes in the top
157
- * bar's slot instead (B33-1712).
158
+ * bar's slot instead (B33-1712). In development, passing it logs a console
159
+ * warning once.
158
160
  */
159
161
  account?: AppSwitcherAccount;
160
162
  /**
@@ -187,4 +189,4 @@ export interface AppSwitcherProps {
187
189
  * panel on the first row. Dismissal, focus restoration and trigger ARIA are the
188
190
  * shared anchored-overlay contract's (ADR-0009); it never becomes a modal.
189
191
  */
190
- export declare function AppSwitcher({ products, currentSlug, hub, labels, linkComponent, collapsed, defaultOpen, onOpenChange, }: AppSwitcherProps): import("react").JSX.Element;
192
+ export declare function AppSwitcher({ products, currentSlug, hub, labels, linkComponent, collapsed, defaultOpen, onOpenChange, account, }: AppSwitcherProps): import("react").JSX.Element;