@boost33/ui 0.7.1 → 0.8.0-next.1

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 (68) hide show
  1. package/CHANGELOG.md +257 -0
  2. package/dist/buttons.module.css.js +10 -0
  3. package/dist/buttons.module.css.js.map +1 -0
  4. package/dist/components/AppShell/AppShell.d.ts +129 -17
  5. package/dist/components/AppShell/AppShell.js +78 -50
  6. package/dist/components/AppShell/AppShell.js.map +1 -1
  7. package/dist/components/AppShell/AppShell.module.css.js +22 -14
  8. package/dist/components/AppShell/AppShell.module.css.js.map +1 -1
  9. package/dist/components/AppSwitcher/AppSwitcher.d.ts +92 -45
  10. package/dist/components/AppSwitcher/AppSwitcher.js +76 -68
  11. package/dist/components/AppSwitcher/AppSwitcher.js.map +1 -1
  12. package/dist/components/AppSwitcher/AppSwitcher.module.css.js +42 -36
  13. package/dist/components/AppSwitcher/AppSwitcher.module.css.js.map +1 -1
  14. package/dist/components/AppSwitcher/icons.js +10 -10
  15. package/dist/components/AppSwitcher/icons.js.map +1 -1
  16. package/dist/components/AppSwitcher/labels.d.ts +15 -23
  17. package/dist/components/AppSwitcher/labels.js +3 -4
  18. package/dist/components/AppSwitcher/labels.js.map +1 -1
  19. package/dist/components/CommandPalette/CommandPalette.d.ts +8 -2
  20. package/dist/components/CommandPalette/CommandPalette.js.map +1 -1
  21. package/dist/components/NavItem/NavItem.d.ts +15 -2
  22. package/dist/components/NavItem/NavItem.js +14 -11
  23. package/dist/components/NavItem/NavItem.js.map +1 -1
  24. package/dist/components/NavItem/NavItem.module.css.js +6 -2
  25. package/dist/components/NavItem/NavItem.module.css.js.map +1 -1
  26. package/dist/components/NavSection/NavSection.module.css.js +5 -5
  27. package/dist/components/NotificationButton/NotificationButton.d.ts +29 -0
  28. package/dist/components/NotificationButton/NotificationButton.js +23 -0
  29. package/dist/components/NotificationButton/NotificationButton.js.map +1 -0
  30. package/dist/components/NotificationButton/NotificationButton.module.css.js +14 -0
  31. package/dist/components/NotificationButton/NotificationButton.module.css.js.map +1 -0
  32. package/dist/components/NotificationButton/labels.d.ts +12 -0
  33. package/dist/components/NotificationButton/labels.js +15 -0
  34. package/dist/components/NotificationButton/labels.js.map +1 -0
  35. package/dist/components/NotificationCenter/NotificationCenter.js +37 -36
  36. package/dist/components/NotificationCenter/NotificationCenter.js.map +1 -1
  37. package/dist/components/NotificationCenter/NotificationCenter.module.css.js +43 -43
  38. package/dist/components/Sidebar/NavHeightBudgetWarning.js +10 -10
  39. package/dist/components/Sidebar/NavHeightBudgetWarning.js.map +1 -1
  40. package/dist/components/Sidebar/Sidebar.module.css.js +1 -1
  41. package/dist/components/TopBar/TopBar.d.ts +55 -4
  42. package/dist/components/TopBar/TopBar.js +24 -7
  43. package/dist/components/TopBar/TopBar.js.map +1 -1
  44. package/dist/components/TopBar/TopBar.module.css.js +12 -8
  45. package/dist/components/TopBar/TopBar.module.css.js.map +1 -1
  46. package/dist/components/TopBar/labels.d.ts +20 -0
  47. package/dist/components/TopBar/labels.js +15 -0
  48. package/dist/components/TopBar/labels.js.map +1 -0
  49. package/dist/components/WalletChip/WalletChip.d.ts +77 -2
  50. package/dist/components/WalletChip/WalletChip.js +142 -50
  51. package/dist/components/WalletChip/WalletChip.js.map +1 -1
  52. package/dist/components/WalletChip/WalletChip.module.css.js +93 -47
  53. package/dist/components/WalletChip/WalletChip.module.css.js.map +1 -1
  54. package/dist/components/WalletChip/labels.d.ts +63 -1
  55. package/dist/components/WalletChip/labels.js +21 -6
  56. package/dist/components/WalletChip/labels.js.map +1 -1
  57. package/dist/deprecated-prop-warning.d.ts +5 -0
  58. package/dist/deprecated-prop-warning.js +11 -0
  59. package/dist/deprecated-prop-warning.js.map +1 -0
  60. package/dist/index.d.ts +9 -3
  61. package/dist/index.js +55 -49
  62. package/dist/index.js.map +1 -1
  63. package/dist/nav-height-budget.d.ts +12 -9
  64. package/dist/nav-height-budget.js +3 -3
  65. package/dist/nav-height-budget.js.map +1 -1
  66. package/dist/style.css +1 -1
  67. package/dist/theme.css +1 -1
  68. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"AppShell.js","sources":["../../../src/components/AppShell/AppShell.tsx"],"sourcesContent":["import type { ElementType, ReactNode } from 'react'\nimport { Sidebar } from '../Sidebar/Sidebar'\nimport { NavSection } from '../NavSection/NavSection'\nimport { NavItem } from '../NavItem/NavItem'\nimport { TopBar, type TopBarProps } from '../TopBar/TopBar'\nimport { Wordmark } from '../Wordmark/Wordmark'\nimport styles from './AppShell.module.css'\n\n/** One nav row, described as data. The consumer never hands the shell markup. */\nexport interface AppShellNavItem {\n /** The row's name. Truncates rather than wrapping; on the rail it collapses to the accessibility tree. */\n label: string\n /** The leading icon. Decorative -- the label carries the name. On the rail it is all that shows. */\n icon?: ReactNode\n /** Marks this row as the current page. Sets `aria-current=\"page\"`. */\n active?: boolean\n /** Trailing-slot count. Omit to leave the slot empty; it still reserves its width so counts line up. */\n count?: number\n /** Renders `count` in the primary color instead of muted. */\n countUrgent?: boolean\n /** Where the row goes. A nav row's destination is data; the shell renders it as a link. */\n href?: string\n}\n\n/** One nav section: an optional header over a run of rows. */\nexport interface AppShellNavSection {\n /** The section header. Omit for a headerless section (the nav's first section has none). On the rail every header drops. */\n label?: string\n /** The section's rows, as data. */\n items: AppShellNavItem[]\n}\n\nexport interface AppShellProps {\n /**\n * The accessible name of the sidebar landmark. A display string in the\n * consumer's own language -- usually the product's name -- that reaches\n * `aria-label` on the `<aside>` and is never rendered as text.\n *\n * **It is an accessible name, not product-awareness.** This prop was called\n * `product` and its comment claimed it was \"the whole of the shell's\n * product-awareness\" (B33-1272). Nothing branches on it, nothing is looked up\n * with it, and no artwork or copy changes because of it -- so the comment\n * described a mechanism that did not exist and sent the next reader looking\n * for the rest of it. The shell is product-aware through `nav`, `switcher`,\n * `wallet` and `footer`, which are the product's own data.\n *\n * **Not the Identity slug.** `AppSwitcher.currentSlug` takes that\n * (`\"wallet\"`, `\"boostscore\"`); this takes what a person would read\n * (\"Wallet\"). The shell carries two names for one product and nothing in the\n * types connects them, so check which kind of string each prop wants.\n */\n label: string\n /**\n * The accessible name of the navigation landmark inside the sidebar.\n * **Defaults to `label`, which is today's behaviour and not the fix.**\n *\n * Pass a distinct string here. Without one the `<aside>` and the `<nav>`\n * nested inside it carry the same accessible name, and a screen-reader user\n * moving by landmark hears \"Wallet complementary\" then \"Wallet navigation\" --\n * the name doing none of the distinguishing that is the one job a landmark\n * label has (B33-1272). Something like \"Wallet sections\" or \"Wallet pages\",\n * in your language, resolves it.\n *\n * **The package cannot default this to anything better, which is why the\n * fallback is the default.** A sensible default would have to compose a word\n * -- \"navigation\", \"sections\" -- and that word is user-facing text a screen\n * reader reads aloud, so it would have to be in the consumer's language. Both\n * consumers default to French. This package hardcodes no user-facing English\n * (`tests/no-hardcoded-strings.test.ts`), and a composed English default would\n * ship an untranslatable word into assistive output for every consumer that\n * never noticed the prop. Repeating `label` is at least a name the consumer\n * chose and wrote in their own language. So the duplicate name is only fully\n * resolved by a consumer passing this.\n */\n navLabel?: string\n /** The navigation, as data. No product hardcodes rows; the shell composes them. */\n nav: AppShellNavSection[]\n /** The top bar's content -- the breadcrumb and the shell actions on its right. */\n topBar: TopBarProps\n /** The Boost33 mark for the wordmark lockup. Passed in, the way Wordmark takes it; on the rail it is all that shows. Pass it unlinked -- `hubHref` links the whole lockup. */\n mark?: ReactNode\n /**\n * Where the hub lives. Set it and the wordmark in the sidebar's top-left\n * corner becomes a link home, rendered through `linkComponent`; omit it and\n * the wordmark is the plain lockup it always was (B33-1699). The consumer\n * supplies the URL -- Identity owns where the hub is and it differs per\n * environment, so the package never hardcodes it. See `Wordmark.hubHref`.\n */\n hubHref?: string\n /**\n * The product switcher, sitting under the wordmark. A slot: the registry it\n * draws is served by Identity (ADR-0003), not owned here. Render its own\n * collapsed form for the rail.\n */\n switcher?: ReactNode\n /**\n * The wallet chip, pinned above the footer. A slot: the balance comes from\n * `@boost33/ledger-client`, never read here. Render its own collapsed form\n * for the rail.\n */\n wallet?: ReactNode\n /** The sidebar footer, below the wallet chip. A slot -- the product's own. */\n footer?: ReactNode\n /**\n * The sidebar collapsed to the 56px rail. A state of the shell: the sidebar\n * narrows and every piece it owns collapses off this one flag. The slots\n * (switcher, wallet, footer) render their own collapsed form.\n */\n collapsed?: boolean\n /**\n * The element every linked nav row renders through. Defaults to a plain `a`;\n * a consumer whose router owns navigation passes its own here and the shell\n * threads it to every row of `nav`, and to the wordmark when `hubHref` links\n * it, so the whole nav goes client-side with the locale prefix intact rather\n * than full-page-reloading (B33-847). This is the shell's one nod to how a\n * consumer routes -- it renders the element, never knowing what it is. See\n * docs/adr/0011.\n *\n * **It reaches the rows of `nav` and the wordmark, and nothing else.** The\n * slots (`switcher`, `wallet`, `footer`, `topBar.actions`) are `ReactNode`s\n * the consumer has already created, so the shell cannot put a prop on what is\n * inside one. A\n * consumer composing a link-bearing component into a slot -- `AppSwitcher`,\n * `AccountMenu`, `NotificationCenter` -- passes `linkComponent` to that\n * component as well, or its rows stay bare anchors and reload the page. The\n * Wallet shipped exactly that defect (B33-1281) by reading this prop as the\n * single place, which is the 2026-10-06 correction on docs/adr/0011.\n */\n linkComponent?: ElementType\n /**\n * The page body. Sits on white, below the top bar, and the shell renders it\n * inside the page's `<main>` -- so a consumer adds no wrapper of its own.\n *\n * **The page's title goes here too.** A screen with a title renders\n * `PageHeader` as the first thing in its own body; a screen with none renders\n * nothing and the top bar's hairline sits straight on the content, which is\n * what the Wallet's overview does and what its 9 Sep baseline shows. The\n * shell used to take a `pageHeader` prop and draw the header itself, above\n * this; B33-1267 removed it, because no consumer ever passed it and Score\n * forbids it by test (the page below owns the sole `h1`).\n *\n * **Everything here scrolls.** This box is the shell's one scrolling element,\n * so the sidebar and top bar stay pinned and the page header goes up with the\n * body. The removed prop sat outside the scroll box and pinned the header\n * instead -- the one real difference between the two spellings, and the\n * reason the choice was worth collapsing to one rather than leaving to a\n * consumer to discover on a long page.\n */\n children: ReactNode\n}\n\n// The shell every authenticated Boost33 screen renders inside. It composes the\n// pieces built in the earlier tickets -- sidebar, wordmark, switcher, nav,\n// wallet chip, top bar -- into one thing a product imports instead of building a\n// sidebar. Everything that differs between products is the product's own data --\n// `nav`, `switcher`, `wallet`, `footer` -- and nothing above the nav (the Boost33\n// wordmark and brand) changes at all. `label` and `navLabel` name the two\n// landmarks and decide nothing; B33-1272 corrected the claim that `label` (then\n// `product`) was the whole of the shell's product-awareness.\n//\n// `PageHeader` is deliberately not in that list. The shell drew it from a\n// `pageHeader` prop until B33-1267; a page's title is the page's, rendered at\n// the top of `children`, which is what both consumers already did.\nexport function AppShell({\n label,\n navLabel,\n nav,\n topBar,\n mark,\n hubHref,\n switcher,\n wallet,\n footer,\n collapsed = false,\n linkComponent,\n children,\n}: AppShellProps) {\n return (\n <div className={styles.shell}>\n <Sidebar landmarkLabel={label} collapsed={collapsed}>\n <div className={styles.wordmark}>\n <Wordmark mark={mark} collapsed={collapsed} hubHref={hubHref} linkComponent={linkComponent} />\n </div>\n {switcher != null && <div className={styles.switcher}>{switcher}</div>}\n {/* Falls back to the sidebar's name rather than composing one. The\n composed string would be a word a screen reader reads aloud, so it\n would have to be in the consumer's language, and this package\n hardcodes no user-facing English. Falling back repeats a name the\n consumer wrote; it does not invent one. See `navLabel`. */}\n <nav aria-label={navLabel ?? label} className={styles.nav}>\n {nav.map((section, sectionIndex) => (\n // No room for a header at 56px, so on the rail every section renders\n // headerless -- its rows collapse to their icons and line up.\n <NavSection key={sectionIndex} label={collapsed ? undefined : section.label}>\n {section.items.map((item, itemIndex) => (\n <NavItem\n key={itemIndex}\n icon={item.icon}\n active={item.active}\n collapsed={collapsed}\n count={item.count}\n countUrgent={item.countUrgent}\n href={item.href}\n linkComponent={linkComponent}\n >\n {item.label}\n </NavItem>\n ))}\n </NavSection>\n ))}\n </nav>\n {(wallet != null || footer != null) && (\n <div className={styles.foot}>\n {wallet}\n {footer}\n </div>\n )}\n </Sidebar>\n {/* `.main` is the product's column -- the top bar over the body. The\n landmark is the body alone, one level down: jumping to main has to\n skip the chrome, and a <main> around the top bar would swallow the\n breadcrumb, the search box and the avatar. */}\n <div className={styles.main}>\n <TopBar {...topBar} />\n {/* The page's main landmark, and the shell's to supply. This was a plain\n div, so no route of any consumer had one unless that consumer\n remembered a wrapper of its own -- the Wallet did\n (`boostledger/src/app/(wallet)/layout.tsx`), and three products each\n having to remember it is three chances to ship without one. What the\n absence costs a screen-reader user is the first move they make on an\n unfamiliar screen: jump to main and start reading. Without one they\n land at the top of the document and walk the wordmark, the switcher,\n every nav row, the wallet chip, the footer links, the sidebar toggle,\n the search box and the avatar before the first word of the page -- on\n every navigation, because the shell is a layout and does not remount.\n\n The landmark is the body alone, which is the half that matters: a\n <main> drawn around `.main` would satisfy any \"has a landmark\" check\n and buy the reader nothing, because jumping to it would land them\n back on the breadcrumb.\n\n A consumer that already wraps `children` now has two, which is worse\n than none -- B33-1265 drops the Wallet's as it takes this version,\n and the CHANGELOG entry tells both consumers to.\n\n An element swap and nothing else: `main` and `div` are both\n `display: block`, the class and every declaration behind it are\n untouched, and all 27 gated screenshot states came back at the ratios\n they held before. */}\n <main className={styles.content}>{children}</main>\n </div>\n </div>\n )\n}\n"],"names":["AppShell","label","navLabel","nav","topBar","mark","hubHref","switcher","wallet","footer","collapsed","linkComponent","children","jsxs","styles","Sidebar","jsx","Wordmark","section","sectionIndex","NavSection","item","itemIndex","NavItem","TopBar"],"mappings":";;;;;;;AAmKO,SAASA,EAAS;AAAA,EACvB,OAAAC;AAAA,EACA,UAAAC;AAAA,EACA,KAAAC;AAAA,EACA,QAAAC;AAAA,EACA,MAAAC;AAAA,EACA,SAAAC;AAAA,EACA,UAAAC;AAAA,EACA,QAAAC;AAAA,EACA,QAAAC;AAAA,EACA,WAAAC,IAAY;AAAA,EACZ,eAAAC;AAAA,EACA,UAAAC;AACF,GAAkB;AAChB,SACE,gBAAAC,EAAC,OAAA,EAAI,WAAWC,EAAO,OACrB,UAAA;AAAA,IAAA,gBAAAD,EAACE,GAAA,EAAQ,eAAed,GAAO,WAAAS,GAC7B,UAAA;AAAA,MAAA,gBAAAM,EAAC,OAAA,EAAI,WAAWF,EAAO,UACrB,UAAA,gBAAAE,EAACC,KAAS,MAAAZ,GAAY,WAAAK,GAAsB,SAAAJ,GAAkB,eAAAK,EAAA,CAA8B,EAAA,CAC9F;AAAA,MACCJ,KAAY,QAAQ,gBAAAS,EAAC,SAAI,WAAWF,EAAO,UAAW,UAAAP,GAAS;AAAA,MAMhE,gBAAAS,EAAC,OAAA,EAAI,cAAYd,KAAYD,GAAO,WAAWa,EAAO,KACnD,UAAAX,EAAI,IAAI,CAACe,GAASC;AAAA;AAAA;AAAA,QAGjB,gBAAAH,EAACI,GAAA,EAA8B,OAAOV,IAAY,SAAYQ,EAAQ,OACnE,UAAAA,EAAQ,MAAM,IAAI,CAACG,GAAMC,MACxB,gBAAAN;AAAA,UAACO;AAAA,UAAA;AAAA,YAEC,MAAMF,EAAK;AAAA,YACX,QAAQA,EAAK;AAAA,YACb,WAAAX;AAAA,YACA,OAAOW,EAAK;AAAA,YACZ,aAAaA,EAAK;AAAA,YAClB,MAAMA,EAAK;AAAA,YACX,eAAAV;AAAA,YAEC,UAAAU,EAAK;AAAA,UAAA;AAAA,UATDC;AAAA,QAAA,CAWR,KAdcH,CAejB;AAAA,OACD,GACH;AAAA,OACEX,KAAU,QAAQC,KAAU,2BAC3B,OAAA,EAAI,WAAWK,EAAO,MACpB,UAAA;AAAA,QAAAN;AAAA,QACAC;AAAA,MAAA,EAAA,CACH;AAAA,IAAA,GAEJ;AAAA,IAKA,gBAAAI,EAAC,OAAA,EAAI,WAAWC,EAAO,MACrB,UAAA;AAAA,MAAA,gBAAAE,EAACQ,GAAA,EAAQ,GAAGpB,GAAQ;AAAA,MA0BpB,gBAAAY,EAAC,QAAA,EAAK,WAAWF,EAAO,SAAU,UAAAF,EAAA,CAAS;AAAA,IAAA,EAAA,CAC7C;AAAA,EAAA,GACF;AAEJ;"}
1
+ {"version":3,"file":"AppShell.js","sources":["../../../src/components/AppShell/AppShell.tsx"],"sourcesContent":["import type { ElementType, ReactNode } from 'react'\nimport { Sidebar } from '../Sidebar/Sidebar'\nimport { NavSection } from '../NavSection/NavSection'\nimport { NavItem } from '../NavItem/NavItem'\nimport { TopBar, type TopBarProps } from '../TopBar/TopBar'\nimport { Wordmark } from '../Wordmark/Wordmark'\nimport buttons from '../../buttons.module.css'\nimport styles from './AppShell.module.css'\n\n/** One nav row, described as data. The consumer never hands the shell markup. */\nexport interface AppShellNavItem {\n /** The row's name. Truncates rather than wrapping; on the rail it collapses to the accessibility tree. */\n label: string\n /** The leading icon. Decorative -- the label carries the name. On the rail it is all that shows. */\n icon?: ReactNode\n /** Marks this row as the current page. Sets `aria-current=\"page\"`. */\n active?: boolean\n /** Trailing-slot count. Omit to leave the slot empty; it still reserves its width so counts line up. */\n count?: number\n /** Draws `count` as the filled \"needs you\" pill instead of muted text. */\n countUrgent?: boolean\n /** A trailing attention dot; the string is what a screen reader hears. Ignored when `count` is set. */\n dot?: string\n /** Where the row goes. A nav row's destination is data; the shell renders it as a link. */\n href?: string\n}\n\n/** One nav section: an optional header over a run of rows. */\n/**\n * The product's main action, drawn as a button at the foot of the sidebar,\n * above the footer's divider (B33-1713, board 20's \"New scan\"). Data, like a\n * nav row: the shell renders it, in the primary tier's ink tone (ADR-0016).\n */\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":";;;;;;;;AAsRO,SAASA,EAAS;AAAA,EACvB,OAAAC;AAAA,EACA,SAAAC;AAAA,EACA,UAAAC;AAAA,EACA,KAAAC;AAAA,EACA,QAAAC;AAAA,EACA,MAAAC;AAAA,EACA,SAAAC;AAAA,EACA,KAAAC;AAAA,EACA,UAAAC;AAAA,EACA,QAAAC;AAAA,EACA,QAAAC;AAAA,EACA,QAAAC;AAAA,EACA,WAAAC,IAAY;AAAA,EACZ,YAAAC;AAAA,EACA,aAAAC;AAAA,EACA,eAAAC;AAAA,EACA,UAAAC;AACF,GAAkB;AAChB,2BACG,OAAA,EAAI,WAAWC,EAAO,OAAO,oBAAkBJ,GAC7C,UAAA;AAAA,IAAAA,KAAc,QAAQC,KAAe,QAAQ,gBAAAI,EAAC,SAAI,WAAWD,EAAO,aAAc,UAAAH,EAAA,CAAY;AAAA,IAK/F,gBAAAI;AAAA,MAACC;AAAA,MAAA;AAAA,QACE,GAAGf;AAAA,QACJ,OACE,gBAAAgB,EAAAC,GAAA,EAIE,UAAA;AAAA,UAAA,gBAAAH,EAACI,GAAA,EAAS,MAAAjB,GAAY,WAAS,IAAC,SAAAC,GAAkB,eAAAS,GAA8B;AAAA,UAC/ER;AAAA,QAAA,GACH;AAAA,QAEF,UAAAC;AAAA,QACA,QAAQC;AAAA,MAAA;AAAA,IAAA;AAAA,IAEV,gBAAAW,EAAC,OAAA,EAAI,WAAWH,EAAO,MACrB,UAAA;AAAA,MAAA,gBAAAG,EAACG,GAAA,EAAQ,eAAevB,GAAO,WAAAY,GAC5B,UAAA;AAAA,QAAAX,KAAW,QAAQ,CAACW,KAAa,gBAAAM,EAAC,OAAE,WAAWD,EAAO,SAAU,UAAAhB,EAAA,CAAQ;AAAA,QAMzE,gBAAAiB,EAAC,OAAA,EAAI,cAAYhB,KAAYF,GAAO,WAAWiB,EAAO,KACnD,UAAAd,EAAI,IAAI,CAACqB,GAASC;AAAA;AAAA;AAAA,UAGjB,gBAAAP,EAACQ,GAAA,EAA8B,OAAOd,IAAY,SAAYY,EAAQ,OACnE,UAAAA,EAAQ,MAAM,IAAI,CAACG,GAAMC,MACxB,gBAAAV;AAAA,YAACW;AAAA,YAAA;AAAA,cAEC,MAAMF,EAAK;AAAA,cACX,QAAQA,EAAK;AAAA,cACb,WAAAf;AAAA,cACA,OAAOe,EAAK;AAAA,cACZ,aAAaA,EAAK;AAAA,cAClB,KAAKA,EAAK;AAAA,cACV,MAAMA,EAAK;AAAA,cACX,eAAAZ;AAAA,cAEC,UAAAY,EAAK;AAAA,YAAA;AAAA,YAVDC;AAAA,UAAA,CAYR,KAfcH,CAgBjB;AAAA,SACD,GACH;AAAA,QACA,gBAAAP,EAACY,GAAA,EAAc,QAAApB,GAAgB,WAAAE,GAAsB,eAAAG,EAAA,CAA8B;AAAA,QAClFJ,KAAU,QAAQ,gBAAAO,EAAC,SAAI,WAAWD,EAAO,MAAO,UAAAN,EAAA,CAAO;AAAA,MAAA,GAC1D;AAAA,MAIA,gBAAAO,EAAC,OAAA,EAAI,WAAWD,EAAO,MA0BrB,UAAA,gBAAAC,EAAC,QAAA,EAAK,WAAWD,EAAO,SAAU,UAAAD,EAAA,CAAS,EAAA,CAC7C;AAAA,IAAA,EAAA,CACF;AAAA,EAAA,GACF;AAEJ;AAQA,SAASc,EAAc,EAAE,QAAApB,GAAQ,WAAAE,GAAW,eAAAG,KAAqH;AAE/J,MADIL,MAAW,UAAcA,EAAO,SAAS,UAAaA,EAAO,YAAY,UACzEE,KAAaF,EAAO,QAAQ,KAAM,QAAO;AAE7C,QAAMqB,IACJ,gBAAAX,EAAAC,GAAA,EACG,UAAA;AAAA,IAAAX,EAAO,QAAQ,QAAQ,gBAAAQ,EAAC,UAAK,eAAY,QAAQ,YAAO,KAAA,CAAK;AAAA,IAE9D,gBAAAA,EAAC,UAAK,WAAWN,IAAYK,EAAO,SAAS,QAAY,YAAO,MAAA,CAAM;AAAA,EAAA,GACxE,GAEIe,IAAY,GAAGC,EAAQ,OAAO,IAAIhB,EAAO,MAAM,IAC/CiB,IAAgBtB,IAAY,SAAS;AAE3C,MAAIF,EAAO,SAAS,QAAW;AAC7B,UAAMyB,IAAOpB,KAAiB;AAC9B,WACE,gBAAAG,EAAC,OAAA,EAAI,WAAWD,EAAO,YACrB,UAAA,gBAAAC,EAACiB,GAAA,EAAK,WAAAH,GAAsB,MAAMtB,EAAO,MAAM,kBAAgBwB,GAC5D,aACH,GACF;AAAA,EAEJ;AACA,2BACG,OAAA,EAAI,WAAWjB,EAAO,YACrB,4BAAC,UAAA,EAAO,MAAK,UAAS,WAAAe,GAAsB,SAAStB,EAAO,SAAS,kBAAgBwB,GAClF,aACH,GACF;AAEJ;"}
@@ -1,20 +1,28 @@
1
- const t = "_shell_10chf_10", c = "_wordmark_10chf_20", o = "_switcher_10chf_26", _ = "_foot_10chf_33", n = "_nav_10chf_45", s = "_main_10chf_54", h = "_content_10chf_75", e = {
2
- shell: t,
3
- wordmark: c,
4
- switcher: o,
1
+ const o = "_shell_8q83z_13", n = "_body_8q83z_24", t = "_appName_8q83z_39", _ = "_foot_8q83z_65", c = "_actionSlot_8q83z_79", s = "_action_8q83z_79", a = "_srOnly_8q83z_103", e = "_nav_8q83z_116", l = "_main_8q83z_125", q = "_content_8q83z_146", z = "_phoneChrome_8q83z_196", p = {
2
+ shell: o,
3
+ body: n,
4
+ appName: t,
5
5
  foot: _,
6
- nav: n,
7
- main: s,
8
- content: h
6
+ actionSlot: c,
7
+ action: s,
8
+ srOnly: a,
9
+ nav: e,
10
+ main: l,
11
+ content: q,
12
+ phoneChrome: z
9
13
  };
10
14
  export {
11
- h as content,
12
- e as default,
15
+ s as action,
16
+ c as actionSlot,
17
+ t as appName,
18
+ n as body,
19
+ q as content,
20
+ p as default,
13
21
  _ as foot,
14
- s as main,
15
- n as nav,
16
- t as shell,
17
- o as switcher,
18
- c as wordmark
22
+ l as main,
23
+ e as nav,
24
+ z as phoneChrome,
25
+ o as shell,
26
+ a as srOnly
19
27
  };
20
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":";;;;;;;;;;;;;"}
@@ -3,9 +3,13 @@ import { AppSwitcherLabels } from './labels';
3
3
  /**
4
4
  * One product the switcher can jump to. It is Identity's registry row, supplied
5
5
  * by the consuming app (ADR-0003) -- the switcher fetches nothing and hardcodes
6
- * no list. Every product is rendered, entitled or not and built or not: hiding
7
- * an unowned one means nobody learns the family exists (B33-479), and hiding an
8
- * unbuilt one means nobody learns it is coming.
6
+ * no list. Every product that exists is rendered, entitled or not: hiding an
7
+ * unowned one means nobody learns the family exists (B33-479), and it tells a
8
+ * client who could buy a product that the product is not there.
9
+ *
10
+ * A product that does not exist yet is not rendered (B33-1711). Until 0.8 it
11
+ * drew with a "Soon" tag; the navigation redesign of 3 Nov 2026 took unbuilt
12
+ * products off the switcher, the same way they came off the hub. See `live`.
9
13
  */
10
14
  export interface AppSwitcherProduct {
11
15
  /** Identity's product slug, which selects the ProductMark artwork. */
@@ -22,8 +26,23 @@ export interface AppSwitcherProduct {
22
26
  */
23
27
  entitled?: boolean;
24
28
  /**
25
- * Whether the product exists yet. Defaults to `true`. `false` dims the mark and
26
- * shows the "Soon" tag in place of "Not on plan".
29
+ * Whether the product exists yet. Defaults to `true`. `false` leaves the row
30
+ * out of the panel entirely (B33-1711), unless it is the product this shell is
31
+ * rendering for -- the current product is always listed, since you are in it.
32
+ *
33
+ * **Only this hides a row. `entitled: false` never does.** The two answer
34
+ * different questions, and the difference is commercial: a product the account
35
+ * could buy is real, and leaving it out tells a paying client it does not
36
+ * exist. That rule is the hub's as well (boost33-hub CLAUDE.md, `cardState`),
37
+ * and the switcher follows it.
38
+ *
39
+ * Until 0.8 an unbuilt row was drawn with a "Soon" tag. The 3 Nov 2026
40
+ * navigation redesign (B33-1710) removed unbuilt products from the switcher,
41
+ * which overrules the Vocabulary document's "the switcher shows every product
42
+ * regardless of state"; that document is amended alongside this change.
43
+ *
44
+ * What follows is why `live` is a second field rather than a third value of
45
+ * `entitled`, which still holds.
27
46
  *
28
47
  * **A different question from `entitled`, which is why it is a second field
29
48
  * and not a third value of the first.** `live` is global: the same answer for
@@ -38,14 +57,50 @@ export interface AppSwitcherProduct {
38
57
  * told a client they had not bought Boost Voice, when nobody has: the row read
39
58
  * "Not on plan" and pointed at a product with no code behind it.
40
59
  *
41
- * It stays a rendering hint like `entitled`. An unbuilt product's row is still
42
- * an ordinary focusable link -- the switcher does not decide what anyone may
43
- * open (ADR-0010), and `url` is served for every slug whether or not anything
44
- * answers on it, because `?app=` has to resolve for a campaign link.
60
+ * Hiding the row is a rendering decision, not an access one. The switcher
61
+ * still does not decide what anyone may open (ADR-0010), and `url` is still
62
+ * served for every slug, because `?app=` has to resolve for a campaign link.
45
63
  */
46
64
  live?: boolean;
65
+ /**
66
+ * The heading of the section this row sits under, in the consumer's language
67
+ * -- "Your tools", "Included with every account" (B33-1711, board 21). Rows
68
+ * are grouped by this label in the order the labels first appear, and each
69
+ * group gets a header. Leave it off every row and the panel is one plain list
70
+ * with no headers; rows without one, in a panel where others have one, form a
71
+ * headerless group of their own.
72
+ *
73
+ * Data the consumer is served, never decided here: which products are "your
74
+ * tools" is the account's grant and Identity's registry, and the package
75
+ * knows neither (CLAUDE.md -- no component knows what a product means).
76
+ */
77
+ group?: string;
78
+ /**
79
+ * One thing at the right of the row (B33-1711, board 21) -- a figure such as
80
+ * the Wallet's balance, or a dot saying there is something new. The consumer
81
+ * supplies it already formatted; the switcher fetches nothing and formats
82
+ * nothing. The current product's check takes the same slot, so a trailing on
83
+ * the current row is not drawn.
84
+ */
85
+ trailing?: AppSwitcherTrailing;
47
86
  }
48
- /** The "all products" row above the divider. Its blurb and URL are the consumer's. */
87
+ /**
88
+ * The right-hand slot of a product row, in its two shapes.
89
+ *
90
+ * - `value` -- a short preformatted figure, drawn muted in the mono face:
91
+ * `{ kind: 'value', value: '8,940' }`.
92
+ * - `dot` -- a small primary dot meaning "something new here". A dot is a
93
+ * colour and nothing else, so `label` is required: it is what a screen reader
94
+ * hears instead ("2 new"), in the consumer's language.
95
+ */
96
+ export type AppSwitcherTrailing = {
97
+ kind: 'value';
98
+ value: string;
99
+ } | {
100
+ kind: 'dot';
101
+ label: string;
102
+ };
103
+ /** The "all products" row at the top of the panel. Its blurb and URL are the consumer's. */
49
104
  export interface AppSwitcherHub {
50
105
  /** The row's name, e.g. "Hub". */
51
106
  name: string;
@@ -55,9 +110,15 @@ export interface AppSwitcherHub {
55
110
  href: string;
56
111
  }
57
112
  /**
58
- * The account/workspace row at the foot of the panel. A row that opens a second
59
- * panel (a later ticket owns that panel), so it is a button rather than a link;
60
- * this ticket renders the row and calls `onSelect` when it is chosen.
113
+ * The account/workspace row that sat at the foot of the panel until 0.8.0.
114
+ *
115
+ * **Not drawn since 0.8.0.** The 3 Nov 2026 redesign (B33-1710, board 21) took
116
+ * the account row off the switcher: the organisation now lives in the top bar's
117
+ * left-hand slot, as the consumer's own pill (B33-1712). The type stays so a
118
+ * consumer's existing `account` object still compiles; nothing reads it.
119
+ *
120
+ * @deprecated Rendered nowhere since 0.8.0 (B33-1711). Stop passing `account`;
121
+ * in development, passing it logs a console warning once.
61
122
  */
62
123
  export interface AppSwitcherAccount {
63
124
  /** The organisation's name, e.g. "Acme Coaching". */
@@ -66,35 +127,15 @@ export interface AppSwitcherAccount {
66
127
  initials: string;
67
128
  /** The line beneath the name, preformatted, e.g. "Admin · 2 workspaces". */
68
129
  description: string;
69
- /**
70
- * Chosen -- opens the workspace panel.
71
- *
72
- * **It is also what makes the row a control.** Supplied, the row is the
73
- * `<button>` it has always been, with the chevron pointing at the panel it
74
- * opens. Omitted, the row still draws -- the organisation and its workspace
75
- * count are facts about the account whether or not anything opens -- but as a
76
- * plain `<div>`: no chevron, no hover step, no tab stop (B33-1270).
77
- *
78
- * Both consumers ship it unarmed today, because the panel it would open is
79
- * B33-493's and unbuilt: see the comment on `account` in
80
- * `boostscore/components/app-shell/product-switcher.tsx` ("the Wallet's
81
- * migration left the same row inert for the same reason"). Before this gate
82
- * existed they got a button that swallowed a click and did nothing, and no way
83
- * to reach it -- `.accountRow`'s class name is CSS-Modules-hashed.
84
- *
85
- * The whole row is not removed, and that is deliberate. ADR-0010's "Not
86
- * entitled is not disabled" is the nearest settled reasoning: a row that
87
- * carries information is not the component's to withhold, and `aria-disabled`
88
- * does not belong on something nobody can act on anyway. So the information
89
- * stays and the affordances go. Nor is the button `disabled` -- that is still
90
- * a control a consumer cannot remove.
91
- */
130
+ /** Was: chosen -- opens the workspace panel. Never called since 0.8.0. */
92
131
  onSelect?: () => void;
93
132
  }
94
133
  export interface AppSwitcherProps {
95
134
  /**
96
- * The products, in the order Identity serves them. Rendered whole -- entitled
97
- * or not, built or not.
135
+ * The products, in the order Identity serves them. Pass the registry whole:
136
+ * the switcher leaves out the rows with `live: false` itself (B33-1711), so a
137
+ * consumer does not filter, and every product that exists is drawn whether
138
+ * the account holds it or not.
98
139
  */
99
140
  products: AppSwitcherProduct[];
100
141
  /**
@@ -109,8 +150,15 @@ export interface AppSwitcherProps {
109
150
  currentSlug: string;
110
151
  /** The "all products" row above the products. */
111
152
  hub: AppSwitcherHub;
112
- /** The account/workspace row below the products. */
113
- account: AppSwitcherAccount;
153
+ /**
154
+ * Was the account/workspace row below the products. Optional and unread since
155
+ * 0.8.0, when the redesign took that row off the panel (B33-1711).
156
+ *
157
+ * @deprecated Rendered nowhere since 0.8.0. The organisation goes in the top
158
+ * bar's slot instead (B33-1712). In development, passing it logs a console
159
+ * warning once.
160
+ */
161
+ account?: AppSwitcherAccount;
114
162
  /**
115
163
  * Every user-facing string that is not row data. Each field defaults to
116
164
  * English, and a field left out -- or supplied as `undefined`, which is what
@@ -120,8 +168,7 @@ export interface AppSwitcherProps {
120
168
  /**
121
169
  * The element the product and hub rows render through. Defaults to a plain
122
170
  * `a`; a router-owning consumer passes its own so the rows route client-side
123
- * with the locale prefix intact (ADR-0011, B33-847). The account row stays a
124
- * button -- it opens a panel, it does not navigate.
171
+ * with the locale prefix intact (ADR-0011, B33-847).
125
172
  */
126
173
  linkComponent?: ElementType;
127
174
  /** The 56px rail. The trigger collapses to the current product's mark alone. */
@@ -132,8 +179,8 @@ export interface AppSwitcherProps {
132
179
  onOpenChange?: (open: boolean) => void;
133
180
  }
134
181
  /**
135
- * The product switcher under the wordmark. It opens, lists every Boost33 product
136
- * with the current one checked, and takes you straight into the one you pick --
182
+ * The product switcher. It opens, lists every Boost33 product that exists with
183
+ * the current one checked, and takes you straight into the one you pick --
137
184
  * the real switcher the Wallet's chevron-that-opens-nothing never was (B33-479).
138
185
  *
139
186
  * A disclosure, not a menu (ADR-0010): the panel carries no widget role, the
@@ -142,4 +189,4 @@ export interface AppSwitcherProps {
142
189
  * panel on the first row. Dismissal, focus restoration and trigger ARIA are the
143
190
  * shared anchored-overlay contract's (ADR-0009); it never becomes a modal.
144
191
  */
145
- export declare function AppSwitcher({ products, currentSlug, hub, account, 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;
@@ -1,88 +1,96 @@
1
1
  "use client";
2
- import { jsxs as s, jsx as a, Fragment as f } from "react/jsx-runtime";
3
- import { useAnchoredOverlay as y } from "../../anchored-overlay.js";
4
- import { ProductMark as g } from "../ProductMark/ProductMark.js";
5
- import { Chevron as x, HubMark as P, Check as A, ChevronRight as S } from "./icons.js";
6
- import { resolveAppSwitcherLabels as L } from "./labels.js";
2
+ import { jsxs as n, jsx as a, Fragment as k } from "react/jsx-runtime";
3
+ import { useId as O } from "react";
4
+ import { useAnchoredOverlay as x } from "../../anchored-overlay.js";
5
+ import { warnDeprecatedProp as I } from "../../deprecated-prop-warning.js";
6
+ import { ProductMark as w } from "../ProductMark/ProductMark.js";
7
+ import { Chevron as L, HubMark as T, ChevronRight as j, Check as D } from "./icons.js";
8
+ import { resolveAppSwitcherLabels as E } from "./labels.js";
7
9
  import e from "./AppSwitcher.module.css.js";
8
- function M({ account: r }) {
9
- const l = /* @__PURE__ */ s(f, { children: [
10
- /* @__PURE__ */ a("span", { className: e.avatar, "aria-hidden": "true", children: r.initials }),
11
- /* @__PURE__ */ s("span", { className: e.rowText, children: [
12
- /* @__PURE__ */ a("span", { className: e.rowName, children: r.name }),
13
- /* @__PURE__ */ a("span", { className: e.rowSub, children: r.description })
14
- ] })
15
- ] });
16
- return r.onSelect === void 0 ? /* @__PURE__ */ a("div", { className: e.accountRow, "data-actionable": "false", children: l }) : /* @__PURE__ */ s("button", { type: "button", className: e.accountRow, onClick: r.onSelect, children: [
17
- l,
18
- /* @__PURE__ */ a(S, { className: e.chevronRight })
10
+ const S = 28;
11
+ function _(l) {
12
+ const r = [];
13
+ for (const i of l) {
14
+ const c = r[r.length - 1];
15
+ c !== void 0 && c.label === i.group ? c.products.push(i) : r.push({ label: i.group, products: [i] });
16
+ }
17
+ return r;
18
+ }
19
+ function $({ trailing: l }) {
20
+ return l === void 0 ? null : l.kind === "value" ? /* @__PURE__ */ a("span", { className: e.trailingValue, children: l.value }) : /* @__PURE__ */ n(k, { children: [
21
+ /* @__PURE__ */ a("span", { className: e.trailingDot, "aria-hidden": "true" }),
22
+ /* @__PURE__ */ a("span", { className: e.srOnly, children: l.label })
19
23
  ] });
20
24
  }
21
- function q({
22
- products: r,
23
- currentSlug: l,
24
- hub: o,
25
- account: w,
26
- labels: b,
27
- linkComponent: k,
28
- collapsed: c = !1,
29
- defaultOpen: C,
30
- onOpenChange: R
25
+ function B({
26
+ products: l,
27
+ currentSlug: r,
28
+ hub: i,
29
+ labels: c,
30
+ linkComponent: C,
31
+ collapsed: d = !1,
32
+ defaultOpen: y,
33
+ onOpenChange: P,
34
+ account: R
31
35
  }) {
32
- const i = y({ defaultOpen: C, onOpenChange: R }), t = L(b), p = k ?? "a", m = r.find((n) => n.slug === l), u = (m == null ? void 0 : m.name) ?? t.switchProduct;
33
- return /* @__PURE__ */ s("div", { className: e.root, children: [
34
- /* @__PURE__ */ s(
36
+ R != null && I("AppSwitcher.account");
37
+ const t = x({ defaultOpen: y, onOpenChange: P }), A = O(), m = E(c), g = C ?? "a", p = l.find((s) => s.slug === r), M = l.filter((s) => s.live !== !1 || s.slug === r), f = (p == null ? void 0 : p.name) ?? m.switchProduct;
38
+ return /* @__PURE__ */ n("div", { className: e.root, children: [
39
+ /* @__PURE__ */ n(
35
40
  "button",
36
41
  {
37
42
  type: "button",
38
- className: c ? e.railTrigger : e.trigger,
39
- onClick: i.toggle,
40
- ...i.triggerProps,
43
+ className: d ? e.railTrigger : e.trigger,
44
+ onClick: t.toggle,
45
+ ...t.triggerProps,
41
46
  children: [
42
- /* @__PURE__ */ a(g, { slug: l, title: c ? u : void 0 }),
43
- !c && /* @__PURE__ */ s(f, { children: [
44
- /* @__PURE__ */ a("span", { className: e.triggerName, children: u }),
45
- /* @__PURE__ */ a(x, { className: e.chevron })
47
+ /* @__PURE__ */ a(w, { slug: r, title: d ? f : void 0 }),
48
+ !d && /* @__PURE__ */ n(k, { children: [
49
+ /* @__PURE__ */ a("span", { className: e.triggerName, children: f }),
50
+ /* @__PURE__ */ a(L, { className: e.chevron })
46
51
  ] })
47
52
  ]
48
53
  }
49
54
  ),
50
- i.open && /* @__PURE__ */ s("div", { ...i.panelProps, className: e.panel, "aria-label": t.panel, children: [
51
- /* @__PURE__ */ s(p, { className: e.hubRow, href: o.href, onClick: i.close, children: [
52
- /* @__PURE__ */ a(P, { className: e.hubMark }),
53
- /* @__PURE__ */ s("span", { className: e.rowText, children: [
54
- /* @__PURE__ */ a("span", { className: e.rowName, children: o.name }),
55
- /* @__PURE__ */ a("span", { className: e.rowSub, children: o.description })
56
- ] })
55
+ t.open && /* @__PURE__ */ n("div", { ...t.panelProps, className: e.panel, "aria-label": m.panel, children: [
56
+ /* @__PURE__ */ n(g, { className: e.hubRow, href: i.href, onClick: t.close, children: [
57
+ /* @__PURE__ */ a(T, { className: e.hubMark }),
58
+ /* @__PURE__ */ n("span", { className: e.rowText, children: [
59
+ /* @__PURE__ */ a("span", { className: e.rowName, children: i.name }),
60
+ /* @__PURE__ */ a("span", { className: e.rowSub, children: i.description })
61
+ ] }),
62
+ /* @__PURE__ */ a(j, { className: e.hubChevron })
57
63
  ] }),
58
- /* @__PURE__ */ a("hr", { className: e.divider }),
59
- /* @__PURE__ */ a("ul", { className: e.products, children: r.map((n) => {
60
- const d = n.slug === l, h = n.entitled === !1, N = n.live === !1, v = N ? t.soon : h ? t.notOnPlan : null;
61
- return /* @__PURE__ */ a("li", { children: /* @__PURE__ */ s(
62
- p,
63
- {
64
- className: e.productRow,
65
- href: n.href,
66
- "aria-current": d ? "true" : void 0,
67
- "data-current": d ? "true" : void 0,
68
- "data-entitled": h ? "false" : void 0,
69
- "data-live": N ? "false" : void 0,
70
- onClick: i.close,
71
- children: [
72
- /* @__PURE__ */ a(g, { slug: n.slug, muted: h || N }),
73
- /* @__PURE__ */ a("span", { className: e.rowName, children: n.name }),
74
- v !== null && /* @__PURE__ */ a("span", { className: e.tag, children: v }),
75
- d && /* @__PURE__ */ a(A, { className: e.check })
76
- ]
77
- }
78
- ) }, n.slug);
79
- }) }),
80
- /* @__PURE__ */ a("hr", { className: e.divider }),
81
- /* @__PURE__ */ a(M, { account: w })
64
+ _(M).map((s, N) => {
65
+ const v = `${A}-group-${N}`;
66
+ return /* @__PURE__ */ n("div", { className: e.group, children: [
67
+ s.label !== void 0 && /* @__PURE__ */ a("p", { id: v, className: e.groupLabel, children: s.label }),
68
+ /* @__PURE__ */ a("ul", { className: e.products, "aria-labelledby": s.label !== void 0 ? v : void 0, children: s.products.map((o) => {
69
+ const h = o.slug === r, u = o.entitled === !1, b = u ? m.notOnPlan : null;
70
+ return /* @__PURE__ */ a("li", { children: /* @__PURE__ */ n(
71
+ g,
72
+ {
73
+ className: e.productRow,
74
+ href: o.href,
75
+ "aria-current": h ? "true" : void 0,
76
+ "data-current": h ? "true" : void 0,
77
+ "data-entitled": u ? "false" : void 0,
78
+ onClick: t.close,
79
+ children: [
80
+ /* @__PURE__ */ a(w, { slug: o.slug, size: S, muted: u }),
81
+ /* @__PURE__ */ a("span", { className: e.rowName, children: o.name }),
82
+ b !== null && /* @__PURE__ */ a("span", { className: e.tag, children: b }),
83
+ /* @__PURE__ */ a("span", { className: e.trailing, children: h ? /* @__PURE__ */ a(D, { className: e.check }) : /* @__PURE__ */ a($, { trailing: o.trailing }) })
84
+ ]
85
+ }
86
+ ) }, o.slug);
87
+ }) })
88
+ ] }, N);
89
+ })
82
90
  ] })
83
91
  ] });
84
92
  }
85
93
  export {
86
- q as AppSwitcher
94
+ B as AppSwitcher
87
95
  };
88
96
  //# sourceMappingURL=AppSwitcher.js.map