@boost33/ui 0.7.0 → 0.8.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +248 -3
- package/dist/buttons.module.css.js +10 -0
- package/dist/buttons.module.css.js.map +1 -0
- package/dist/components/AppShell/AppShell.d.ts +99 -21
- package/dist/components/AppShell/AppShell.js +76 -50
- package/dist/components/AppShell/AppShell.js.map +1 -1
- package/dist/components/AppShell/AppShell.module.css.js +22 -16
- package/dist/components/AppShell/AppShell.module.css.js.map +1 -1
- package/dist/components/AppSwitcher/AppSwitcher.d.ts +90 -45
- package/dist/components/AppSwitcher/AppSwitcher.js +71 -66
- package/dist/components/AppSwitcher/AppSwitcher.js.map +1 -1
- package/dist/components/AppSwitcher/AppSwitcher.module.css.js +42 -36
- package/dist/components/AppSwitcher/AppSwitcher.module.css.js.map +1 -1
- package/dist/components/AppSwitcher/icons.js +10 -10
- package/dist/components/AppSwitcher/icons.js.map +1 -1
- package/dist/components/AppSwitcher/labels.d.ts +11 -22
- package/dist/components/AppSwitcher/labels.js.map +1 -1
- package/dist/components/Boost33Mark/Boost33Mark.d.ts +37 -4
- package/dist/components/Boost33Mark/Boost33Mark.js +15 -15
- package/dist/components/Boost33Mark/Boost33Mark.js.map +1 -1
- package/dist/components/Boost33Mark/Boost33Mark.module.css.js +10 -8
- package/dist/components/Boost33Mark/Boost33Mark.module.css.js.map +1 -1
- package/dist/components/CommandPalette/CommandPalette.d.ts +8 -2
- package/dist/components/CommandPalette/CommandPalette.js.map +1 -1
- package/dist/components/NavItem/NavItem.d.ts +15 -2
- package/dist/components/NavItem/NavItem.js +14 -11
- package/dist/components/NavItem/NavItem.js.map +1 -1
- package/dist/components/NavItem/NavItem.module.css.js +6 -2
- package/dist/components/NavItem/NavItem.module.css.js.map +1 -1
- package/dist/components/NavSection/NavSection.module.css.js +5 -5
- package/dist/components/NotificationButton/NotificationButton.d.ts +29 -0
- package/dist/components/NotificationButton/NotificationButton.js +23 -0
- package/dist/components/NotificationButton/NotificationButton.js.map +1 -0
- package/dist/components/NotificationButton/NotificationButton.module.css.js +14 -0
- package/dist/components/NotificationButton/NotificationButton.module.css.js.map +1 -0
- package/dist/components/NotificationButton/labels.d.ts +12 -0
- package/dist/components/NotificationButton/labels.js +15 -0
- package/dist/components/NotificationButton/labels.js.map +1 -0
- package/dist/components/NotificationCenter/NotificationCenter.js +37 -36
- package/dist/components/NotificationCenter/NotificationCenter.js.map +1 -1
- package/dist/components/NotificationCenter/NotificationCenter.module.css.js +43 -43
- package/dist/components/Sidebar/NavHeightBudgetWarning.js +2 -2
- package/dist/components/Sidebar/NavHeightBudgetWarning.js.map +1 -1
- package/dist/components/Sidebar/Sidebar.module.css.js +1 -1
- package/dist/components/TopBar/TopBar.d.ts +54 -4
- package/dist/components/TopBar/TopBar.js +22 -7
- package/dist/components/TopBar/TopBar.js.map +1 -1
- package/dist/components/TopBar/TopBar.module.css.js +12 -8
- package/dist/components/TopBar/TopBar.module.css.js.map +1 -1
- package/dist/components/TopBar/labels.d.ts +20 -0
- package/dist/components/TopBar/labels.js +15 -0
- package/dist/components/TopBar/labels.js.map +1 -0
- package/dist/components/WalletChip/WalletChip.d.ts +77 -2
- package/dist/components/WalletChip/WalletChip.js +142 -50
- package/dist/components/WalletChip/WalletChip.js.map +1 -1
- package/dist/components/WalletChip/WalletChip.module.css.js +93 -47
- package/dist/components/WalletChip/WalletChip.module.css.js.map +1 -1
- package/dist/components/WalletChip/labels.d.ts +63 -1
- package/dist/components/WalletChip/labels.js +21 -6
- package/dist/components/WalletChip/labels.js.map +1 -1
- package/dist/components/Wordmark/Wordmark.d.ts +29 -2
- package/dist/components/Wordmark/Wordmark.js +9 -8
- package/dist/components/Wordmark/Wordmark.js.map +1 -1
- package/dist/components/Wordmark/Wordmark.module.css.js +7 -7
- package/dist/index.d.ts +9 -3
- package/dist/index.js +55 -49
- package/dist/index.js.map +1 -1
- package/dist/nav-height-budget.d.ts +12 -9
- package/dist/nav-height-budget.js +3 -3
- package/dist/nav-height-budget.js.map +1 -1
- package/dist/style.css +1 -1
- package/dist/theme.css +1 -1
- 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. */\n mark?: ReactNode\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`, so the whole nav goes client-side with the\n * locale prefix intact rather than full-page-reloading (B33-847). This is the\n * shell's one nod to how a consumer routes -- it renders the element, never\n * knowing what it is. See docs/adr/0011.\n *\n * **It reaches the rows of `nav` and nothing else.** The slots (`switcher`,\n * `wallet`, `footer`, `topBar.actions`) are `ReactNode`s the consumer has\n * already created, so the shell cannot put a prop on what is 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 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} />\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","switcher","wallet","footer","collapsed","linkComponent","children","jsxs","styles","Sidebar","jsx","Wordmark","section","sectionIndex","NavSection","item","itemIndex","NavItem","TopBar"],"mappings":";;;;;;;AAyJO,SAASA,EAAS;AAAA,EACvB,OAAAC;AAAA,EACA,UAAAC;AAAA,EACA,KAAAC;AAAA,EACA,QAAAC;AAAA,EACA,MAAAC;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,eAAeb,GAAO,WAAAQ,GAC7B,UAAA;AAAA,MAAA,gBAAAM,EAAC,OAAA,EAAI,WAAWF,EAAO,UACrB,4BAACG,GAAA,EAAS,MAAAX,GAAY,WAAAI,GAAsB,EAAA,CAC9C;AAAA,MACCH,KAAY,QAAQ,gBAAAS,EAAC,SAAI,WAAWF,EAAO,UAAW,UAAAP,GAAS;AAAA,MAMhE,gBAAAS,EAAC,OAAA,EAAI,cAAYb,KAAYD,GAAO,WAAWY,EAAO,KACnD,UAAAV,EAAI,IAAI,CAACc,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,GAAGnB,GAAQ;AAAA,MA0BpB,gBAAAW,EAAC,QAAA,EAAK,WAAWF,EAAO,SAAU,UAAAF,EAAA,CAAS;AAAA,IAAA,EAAA,CAC7C;AAAA,EAAA,GACF;AAEJ;"}
|
|
1
|
+
{"version":3,"file":"AppShell.js","sources":["../../../src/components/AppShell/AppShell.tsx"],"sourcesContent":["import type { ElementType, ReactNode } from 'react'\nimport { Sidebar } from '../Sidebar/Sidebar'\nimport { NavSection } from '../NavSection/NavSection'\nimport { NavItem } from '../NavItem/NavItem'\nimport { TopBar, type TopBarProps } from '../TopBar/TopBar'\nimport { Wordmark } from '../Wordmark/Wordmark'\nimport buttons from '../../buttons.module.css'\nimport styles from './AppShell.module.css'\n\n/** One nav row, described as data. The consumer never hands the shell markup. */\nexport interface AppShellNavItem {\n /** The row's name. Truncates rather than wrapping; on the rail it collapses to the accessibility tree. */\n label: string\n /** The leading icon. Decorative -- the label carries the name. On the rail it is all that shows. */\n icon?: ReactNode\n /** Marks this row as the current page. Sets `aria-current=\"page\"`. */\n active?: boolean\n /** Trailing-slot count. Omit to leave the slot empty; it still reserves its width so counts line up. */\n count?: number\n /** Draws `count` as the filled \"needs you\" pill instead of muted text. */\n countUrgent?: boolean\n /** A trailing attention dot; the string is what a screen reader hears. Ignored when `count` is set. */\n dot?: string\n /** Where the row goes. A nav row's destination is data; the shell renders it as a link. */\n href?: string\n}\n\n/** One nav section: an optional header over a run of rows. */\n/**\n * The product's main action, drawn as a button at the foot of the sidebar,\n * above the footer's divider (B33-1713, board 20's \"New scan\"). Data, like a\n * nav row: the shell renders it, in the primary tier's ink tone (ADR-0016).\n */\nexport interface AppShellAction {\n /** The button's words, in the consumer's language -- \"New scan\". */\n label: string\n /** A leading icon, decorative. On the 56px rail it is all that shows. */\n icon?: ReactNode\n /** Where it goes. Renders a link through `linkComponent`. Wins over `onClick`. */\n href?: string\n /** What pressing it does. Renders a `<button>`. */\n onClick?: () => void\n}\n\nexport interface AppShellNavSection {\n /** The section header. Omit for a headerless section (the nav's first section has none). On the rail every header drops. */\n label?: string\n /** The section's rows, as data. */\n items: AppShellNavItem[]\n}\n\nexport interface AppShellProps {\n /**\n * The accessible name of the sidebar landmark. A display string in the\n * consumer's own language -- usually the product's name -- that reaches\n * `aria-label` on the `<aside>` and is never rendered as text.\n *\n * **It is an accessible name, not product-awareness.** This prop was called\n * `product` and its comment claimed it was \"the whole of the shell's\n * product-awareness\" (B33-1272). Nothing branches on it, nothing is looked up\n * with it, and no artwork or copy changes because of it -- so the comment\n * described a mechanism that did not exist and sent the next reader looking\n * for the rest of it. The shell is product-aware through `nav`, `switcher`,\n * `wallet` and `footer`, which are the product's own data.\n *\n * **Not the Identity slug.** `AppSwitcher.currentSlug` takes that\n * (`\"wallet\"`, `\"boostscore\"`); this takes what a person would read\n * (\"Wallet\"). The shell carries two names for one product and nothing in the\n * types connects them, so check which kind of string each prop wants.\n */\n label: string\n /**\n * The accessible name of the navigation landmark inside the sidebar.\n * **Defaults to `label`, which is today's behaviour and not the fix.**\n *\n * Pass a distinct string here. Without one the `<aside>` and the `<nav>`\n * nested inside it carry the same accessible name, and a screen-reader user\n * moving by landmark hears \"Wallet complementary\" then \"Wallet navigation\" --\n * the name doing none of the distinguishing that is the one job a landmark\n * label has (B33-1272). Something like \"Wallet sections\" or \"Wallet pages\",\n * in your language, resolves it.\n *\n * **The package cannot default this to anything better, which is why the\n * fallback is the default.** A sensible default would have to compose a word\n * -- \"navigation\", \"sections\" -- and that word is user-facing text a screen\n * reader reads aloud, so it would have to be in the consumer's language. Both\n * consumers default to French. This package hardcodes no user-facing English\n * (`tests/no-hardcoded-strings.test.ts`), and a composed English default would\n * ship an untranslatable word into assistive output for every consumer that\n * never noticed the prop. Repeating `label` is at least a name the consumer\n * chose and wrote in their own language. So the duplicate name is only fully\n * resolved by a consumer passing this.\n */\n navLabel?: string\n /**\n * The product's name, drawn as plain text at the top of the sidebar (B33-1711).\n * The product name only -- \"Boost Score\", in the consumer's language -- with\n * no icon beside it: the product mark belongs in the app switcher, not next to\n * the name. Omit it and nothing is drawn there.\n *\n * It is visible text and nothing else. It names no landmark (that is `label`)\n * and nothing branches on it. Often the same string as `label`; it is a\n * separate prop because one is read aloud as a landmark's name and the other\n * is read on screen, and a consumer may want them to differ.\n *\n * Hidden on the 56px rail, where there is no room for a word.\n */\n appName?: string\n /** The navigation, as data. No product hardcodes rows; the shell composes them. */\n nav: AppShellNavSection[]\n /**\n * The top bar's own content: its `actions` on the right (the notification\n * button and the avatar) and its `labels`. The shell fills the rest of the bar\n * itself -- the mark and `org` on the left, `switcher` and `wallet` in the\n * centre -- so those are props of the shell, not of this object.\n *\n * `breadcrumb` is still accepted and no longer drawn (B33-1712): the redesign\n * puts the mark and the organisation where it was. A sidebar toggle kept\n * inside it goes in `topBar.sidebarToggle` instead, at the far left of the\n * bar, or the rail loses its toggle.\n */\n topBar?: Omit<TopBarProps, 'start' | 'switcher' | 'tokens'>\n /**\n * The Boost33 mark, at the left of the top bar (B33-1712; it was the\n * sidebar's wordmark lockup until 0.8.0). Passed in, the way Wordmark takes\n * it. Pass it unlinked -- `hubHref` links it. The word \"Boost33\" is not drawn\n * beside it; it stays as the link's accessible name.\n */\n mark?: ReactNode\n /**\n * Where the hub lives. Set it and the mark at the left of the top bar becomes\n * a link home, rendered through `linkComponent` (B33-1699). The consumer\n * supplies the URL -- Identity owns where the hub is and it differs per\n * environment, so the package never hardcodes it. See `Wordmark.hubHref`.\n */\n hubHref?: string\n /**\n * The organisation, beside the mark at the left of the top bar (B33-1712,\n * board 20: \"AC Acme Coaching ⌄\"). A slot -- the product's own pill. The\n * package draws no organisation menu: that is AccountMenu (B33-493), which has\n * not passed its gate.\n */\n org?: ReactNode\n /**\n * The app switcher, at the centre of the top bar after its \"Switch Tools →\"\n * label (B33-1712; it sat under the wordmark in the sidebar until 0.8.0). A\n * slot: pass `AppSwitcher`. The registry it draws is served by Identity\n * (ADR-0003), not owned here. It no longer has a rail form to render -- the\n * top bar does not collapse.\n */\n switcher?: ReactNode\n /**\n * The token chip, at the centre of the top bar after the switcher and its\n * \"Check Your Tokens →\" label (B33-1712; it was pinned above the sidebar's\n * footer until 0.8.0). A slot: pass `WalletChip` with `placement=\"topbar\"`.\n * The balance comes from `@boost33/ledger-client`, never read here.\n */\n wallet?: ReactNode\n /**\n * The product's main action, at the foot of the sidebar directly above the\n * footer's divider (B33-1713). Omit it and nothing is drawn. Given neither\n * `href` nor `onClick`, it is not drawn either: a button that does nothing is\n * a control that lies (B33-1270). On the rail it shows its icon alone, or\n * nothing if it has none.\n */\n action?: AppShellAction\n /**\n * The sidebar footer, at the foot of the sidebar, under a divider -- Settings\n * and Help on board 20. A slot -- the product's own.\n */\n footer?: ReactNode\n /**\n * The sidebar collapsed to the 56px rail. A state of the shell: the sidebar\n * narrows to icon-only nav and every piece it owns collapses off this one\n * flag. The footer slot renders its own collapsed form. The switcher and the\n * token chip live in the top bar since 0.8.0 (B33-1712) and do not collapse;\n * the rail has no redesign of its own and keeps working as it did.\n */\n collapsed?: boolean\n /**\n * The element every linked nav row renders through. Defaults to a plain `a`;\n * a consumer whose router owns navigation passes its own here and the shell\n * threads it to every row of `nav`, and to the wordmark when `hubHref` links\n * it, so the whole nav goes client-side with the locale prefix intact rather\n * than full-page-reloading (B33-847). This is the shell's one nod to how a\n * consumer routes -- it renders the element, never knowing what it is. See\n * docs/adr/0011.\n *\n * **It reaches the rows of `nav` and the wordmark, and nothing else.** The\n * slots (`switcher`, `wallet`, `footer`, `topBar.actions`) are `ReactNode`s\n * the consumer has already created, so the shell cannot put a prop on what is\n * inside one. A\n * consumer composing a link-bearing component into a slot -- `AppSwitcher`,\n * `AccountMenu`, `NotificationCenter` -- passes `linkComponent` to that\n * component as well, or its rows stay bare anchors and reload the page. The\n * Wallet shipped exactly that defect (B33-1281) by reading this prop as the\n * single place, which is the 2026-10-06 correction on docs/adr/0011.\n */\n linkComponent?: ElementType\n /**\n * The page body. Sits on white, below the top bar, and the shell renders it\n * inside the page's `<main>` -- so a consumer adds no wrapper of its own.\n *\n * **The page's title goes here too.** A screen with a title renders\n * `PageHeader` as the first thing in its own body; a screen with none renders\n * nothing and the top bar's hairline sits straight on the content, which is\n * what the Wallet's overview does and what its 9 Sep baseline shows. The\n * shell used to take a `pageHeader` prop and draw the header itself, above\n * this; B33-1267 removed it, because no consumer ever passed it and Score\n * forbids it by test (the page below owns the sole `h1`).\n *\n * **Everything here scrolls.** This box is the shell's one scrolling element,\n * so the sidebar and top bar stay pinned and the page header goes up with the\n * body. The removed prop sat outside the scroll box and pinned the header\n * instead -- the one real difference between the two spellings, and the\n * reason the choice was worth collapsing to one rather than leaving to a\n * consumer to discover on a long page.\n */\n children: ReactNode\n}\n\n// The shell every authenticated Boost33 screen renders inside. It composes the\n// pieces built in the earlier tickets -- sidebar, wordmark, switcher, nav,\n// wallet chip, top bar -- into one thing a product imports instead of building a\n// sidebar. Everything that differs between products is the product's own data --\n// `nav`, `switcher`, `wallet`, `footer` -- and nothing above the nav (the Boost33\n// wordmark and brand) changes at all. `label` and `navLabel` name the two\n// landmarks and decide nothing; B33-1272 corrected the claim that `label` (then\n// `product`) was the whole of the shell's product-awareness.\n//\n// `PageHeader` is deliberately not in that list. The shell drew it from a\n// `pageHeader` prop until B33-1267; a page's title is the page's, rendered at\n// the top of `children`, which is what both consumers already did.\nexport function AppShell({\n label,\n appName,\n navLabel,\n nav,\n topBar,\n mark,\n hubHref,\n org,\n switcher,\n wallet,\n action,\n footer,\n collapsed = false,\n linkComponent,\n children,\n}: AppShellProps) {\n return (\n <div className={styles.shell}>\n {/* The bar across the full width, above the sidebar and the product's\n column alike (board 20). First in the DOM as it is first on screen, so\n the tab order runs mark, switcher, token chip, bell, avatar, then the\n nav, then the page. */}\n <TopBar\n {...topBar}\n start={\n <>\n {/* The mark alone, as board 20 draws it: Wordmark's collapsed form\n keeps \"Boost33\" in the accessibility tree, so the link home is\n named, and draws only the mark. */}\n <Wordmark mark={mark} collapsed hubHref={hubHref} linkComponent={linkComponent} />\n {org}\n </>\n }\n switcher={switcher}\n tokens={wallet}\n />\n <div className={styles.body}>\n <Sidebar landmarkLabel={label} collapsed={collapsed}>\n {appName != null && !collapsed && <p className={styles.appName}>{appName}</p>}\n {/* Falls back to the sidebar's name rather than composing one. The\n composed string would be a word a screen reader reads aloud, so it\n would have to be in the consumer's language, and this package\n hardcodes no user-facing English. Falling back repeats a name the\n consumer wrote; it does not invent one. See `navLabel`. */}\n <nav aria-label={navLabel ?? label} className={styles.nav}>\n {nav.map((section, sectionIndex) => (\n // No room for a header at 56px, so on the rail every section renders\n // headerless -- its rows collapse to their icons and line up.\n <NavSection key={sectionIndex} label={collapsed ? undefined : section.label}>\n {section.items.map((item, itemIndex) => (\n <NavItem\n key={itemIndex}\n icon={item.icon}\n active={item.active}\n collapsed={collapsed}\n count={item.count}\n countUrgent={item.countUrgent}\n dot={item.dot}\n href={item.href}\n linkComponent={linkComponent}\n >\n {item.label}\n </NavItem>\n ))}\n </NavSection>\n ))}\n </nav>\n <SidebarAction action={action} collapsed={collapsed} linkComponent={linkComponent} />\n {footer != null && <div className={styles.foot}>{footer}</div>}\n </Sidebar>\n {/* `.main` is the product's column, under the bar and beside the\n sidebar. The landmark is the page body inside it: jumping to main\n has to skip the chrome. */}\n <div className={styles.main}>\n {/* The page's main landmark, and the shell's to supply. This was a plain\n div, so no route of any consumer had one unless that consumer\n remembered a wrapper of its own -- the Wallet did\n (`boostledger/src/app/(wallet)/layout.tsx`), and three products each\n having to remember it is three chances to ship without one. What the\n absence costs a screen-reader user is the first move they make on an\n unfamiliar screen: jump to main and start reading. Without one they\n land at the top of the document and walk the mark, the switcher, the\n token chip, the bell, the avatar, every nav row, the footer links and\n the sidebar toggle before the first word of the page -- on\n every navigation, because the shell is a layout and does not remount.\n\n The landmark is the body alone, which is the half that matters: a\n <main> drawn around the whole shell would satisfy any \"has a\n landmark\" check and buy the reader nothing, because jumping to it\n would land them back on the top bar.\n\n A consumer that already wraps `children` now has two, which is worse\n than none -- B33-1265 drops the Wallet's as it takes this version,\n and the CHANGELOG entry tells both consumers to.\n\n An element swap and nothing else: `main` and `div` are both\n `display: block`, the class and every declaration behind it are\n untouched, and all 27 gated screenshot states came back at the ratios\n they held before. */}\n <main className={styles.content}>{children}</main>\n </div>\n </div>\n </div>\n )\n}\n\n/**\n * The sidebar's action button (B33-1713). The primary tier in its ink tone,\n * full width -- board 20's near-black \"New scan\", not the brand orange, which\n * the package keeps for the money moment (ADR-0016). Pushed to the foot of the\n * column with the footer: the nav above takes the slack.\n */\nfunction SidebarAction({ action, collapsed, linkComponent }: { action: AppShellAction | undefined; collapsed: boolean; linkComponent: ElementType | undefined }) {\n if (action === undefined || (action.href === undefined && action.onClick === undefined)) return null\n if (collapsed && action.icon == null) return null\n\n const content = (\n <>\n {action.icon != null && <span aria-hidden=\"true\">{action.icon}</span>}\n {/* On the rail the label leaves the screen and stays the button's name. */}\n <span className={collapsed ? styles.srOnly : undefined}>{action.label}</span>\n </>\n )\n const className = `${buttons.primary} ${styles.action}`\n const dataCollapsed = collapsed ? 'true' : undefined\n\n if (action.href !== undefined) {\n const Link = linkComponent ?? 'a'\n return (\n <div className={styles.actionSlot}>\n <Link className={className} href={action.href} data-collapsed={dataCollapsed}>\n {content}\n </Link>\n </div>\n )\n }\n return (\n <div className={styles.actionSlot}>\n <button type=\"button\" className={className} onClick={action.onClick} data-collapsed={dataCollapsed}>\n {content}\n </button>\n </div>\n )\n}\n"],"names":["AppShell","label","appName","navLabel","nav","topBar","mark","hubHref","org","switcher","wallet","action","footer","collapsed","linkComponent","children","jsxs","styles","jsx","TopBar","Fragment","Wordmark","Sidebar","section","sectionIndex","NavSection","item","itemIndex","NavItem","SidebarAction","content","className","buttons","dataCollapsed","Link"],"mappings":"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"}
|
|
@@ -1,20 +1,26 @@
|
|
|
1
|
-
const
|
|
2
|
-
shell:
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
foot:
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
const o = "_shell_1e11e_13", e = "_body_1e11e_24", n = "_appName_1e11e_39", t = "_foot_1e11e_65", _ = "_actionSlot_1e11e_79", a = "_action_1e11e_79", c = "_srOnly_1e11e_103", s = "_nav_1e11e_116", l = "_main_1e11e_125", i = "_content_1e11e_146", p = {
|
|
2
|
+
shell: o,
|
|
3
|
+
body: e,
|
|
4
|
+
appName: n,
|
|
5
|
+
foot: t,
|
|
6
|
+
actionSlot: _,
|
|
7
|
+
action: a,
|
|
8
|
+
srOnly: c,
|
|
9
|
+
nav: s,
|
|
10
|
+
main: l,
|
|
11
|
+
content: i
|
|
9
12
|
};
|
|
10
13
|
export {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
a as action,
|
|
15
|
+
_ as actionSlot,
|
|
16
|
+
n as appName,
|
|
17
|
+
e as body,
|
|
18
|
+
i as content,
|
|
19
|
+
p as default,
|
|
20
|
+
t as foot,
|
|
21
|
+
l as main,
|
|
22
|
+
s as nav,
|
|
23
|
+
o as shell,
|
|
24
|
+
c as srOnly
|
|
19
25
|
};
|
|
20
26
|
//# sourceMappingURL=AppShell.module.css.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AppShell.module.css.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"AppShell.module.css.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;"}
|
|
@@ -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
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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`
|
|
26
|
-
*
|
|
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
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
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
|
-
/**
|
|
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,14 @@ export interface AppSwitcherHub {
|
|
|
55
110
|
href: string;
|
|
56
111
|
}
|
|
57
112
|
/**
|
|
58
|
-
* The account/workspace row at the foot of the panel
|
|
59
|
-
*
|
|
60
|
-
*
|
|
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`.
|
|
61
121
|
*/
|
|
62
122
|
export interface AppSwitcherAccount {
|
|
63
123
|
/** The organisation's name, e.g. "Acme Coaching". */
|
|
@@ -66,35 +126,15 @@ export interface AppSwitcherAccount {
|
|
|
66
126
|
initials: string;
|
|
67
127
|
/** The line beneath the name, preformatted, e.g. "Admin · 2 workspaces". */
|
|
68
128
|
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
|
-
*/
|
|
129
|
+
/** Was: chosen -- opens the workspace panel. Never called since 0.8.0. */
|
|
92
130
|
onSelect?: () => void;
|
|
93
131
|
}
|
|
94
132
|
export interface AppSwitcherProps {
|
|
95
133
|
/**
|
|
96
|
-
* The products, in the order Identity serves them.
|
|
97
|
-
*
|
|
134
|
+
* The products, in the order Identity serves them. Pass the registry whole:
|
|
135
|
+
* the switcher leaves out the rows with `live: false` itself (B33-1711), so a
|
|
136
|
+
* consumer does not filter, and every product that exists is drawn whether
|
|
137
|
+
* the account holds it or not.
|
|
98
138
|
*/
|
|
99
139
|
products: AppSwitcherProduct[];
|
|
100
140
|
/**
|
|
@@ -109,8 +149,14 @@ export interface AppSwitcherProps {
|
|
|
109
149
|
currentSlug: string;
|
|
110
150
|
/** The "all products" row above the products. */
|
|
111
151
|
hub: AppSwitcherHub;
|
|
112
|
-
/**
|
|
113
|
-
|
|
152
|
+
/**
|
|
153
|
+
* Was the account/workspace row below the products. Optional and unread since
|
|
154
|
+
* 0.8.0, when the redesign took that row off the panel (B33-1711).
|
|
155
|
+
*
|
|
156
|
+
* @deprecated Rendered nowhere since 0.8.0. The organisation goes in the top
|
|
157
|
+
* bar's slot instead (B33-1712).
|
|
158
|
+
*/
|
|
159
|
+
account?: AppSwitcherAccount;
|
|
114
160
|
/**
|
|
115
161
|
* Every user-facing string that is not row data. Each field defaults to
|
|
116
162
|
* English, and a field left out -- or supplied as `undefined`, which is what
|
|
@@ -120,8 +166,7 @@ export interface AppSwitcherProps {
|
|
|
120
166
|
/**
|
|
121
167
|
* The element the product and hub rows render through. Defaults to a plain
|
|
122
168
|
* `a`; a router-owning consumer passes its own so the rows route client-side
|
|
123
|
-
* with the locale prefix intact (ADR-0011, B33-847).
|
|
124
|
-
* button -- it opens a panel, it does not navigate.
|
|
169
|
+
* with the locale prefix intact (ADR-0011, B33-847).
|
|
125
170
|
*/
|
|
126
171
|
linkComponent?: ElementType;
|
|
127
172
|
/** The 56px rail. The trigger collapses to the current product's mark alone. */
|
|
@@ -132,8 +177,8 @@ export interface AppSwitcherProps {
|
|
|
132
177
|
onOpenChange?: (open: boolean) => void;
|
|
133
178
|
}
|
|
134
179
|
/**
|
|
135
|
-
* The product switcher
|
|
136
|
-
*
|
|
180
|
+
* The product switcher. It opens, lists every Boost33 product that exists with
|
|
181
|
+
* the current one checked, and takes you straight into the one you pick --
|
|
137
182
|
* the real switcher the Wallet's chevron-that-opens-nothing never was (B33-479).
|
|
138
183
|
*
|
|
139
184
|
* A disclosure, not a menu (ADR-0010): the panel carries no widget role, the
|
|
@@ -142,4 +187,4 @@ export interface AppSwitcherProps {
|
|
|
142
187
|
* panel on the first row. Dismissal, focus restoration and trigger ARIA are the
|
|
143
188
|
* shared anchored-overlay contract's (ADR-0009); it never becomes a modal.
|
|
144
189
|
*/
|
|
145
|
-
export declare function AppSwitcher({ products, currentSlug, hub,
|
|
190
|
+
export declare function AppSwitcher({ products, currentSlug, hub, labels, linkComponent, collapsed, defaultOpen, onOpenChange, }: AppSwitcherProps): import("react").JSX.Element;
|
|
@@ -1,88 +1,93 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsxs as
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
2
|
+
import { jsxs as n, jsx as a, Fragment as w } from "react/jsx-runtime";
|
|
3
|
+
import { useId as M } from "react";
|
|
4
|
+
import { useAnchoredOverlay as O } from "../../anchored-overlay.js";
|
|
5
|
+
import { ProductMark as k } from "../ProductMark/ProductMark.js";
|
|
6
|
+
import { Chevron as x, HubMark as I, ChevronRight as L, Check as T } from "./icons.js";
|
|
7
|
+
import { resolveAppSwitcherLabels as j } from "./labels.js";
|
|
7
8
|
import e from "./AppSwitcher.module.css.js";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
9
|
+
const E = 28;
|
|
10
|
+
function _(l) {
|
|
11
|
+
const s = [];
|
|
12
|
+
for (const i of l) {
|
|
13
|
+
const c = s[s.length - 1];
|
|
14
|
+
c !== void 0 && c.label === i.group ? c.products.push(i) : s.push({ label: i.group, products: [i] });
|
|
15
|
+
}
|
|
16
|
+
return s;
|
|
17
|
+
}
|
|
18
|
+
function $({ trailing: l }) {
|
|
19
|
+
return l === void 0 ? null : l.kind === "value" ? /* @__PURE__ */ a("span", { className: e.trailingValue, children: l.value }) : /* @__PURE__ */ n(w, { children: [
|
|
20
|
+
/* @__PURE__ */ a("span", { className: e.trailingDot, "aria-hidden": "true" }),
|
|
21
|
+
/* @__PURE__ */ a("span", { className: e.srOnly, children: l.label })
|
|
19
22
|
] });
|
|
20
23
|
}
|
|
21
|
-
function
|
|
22
|
-
products:
|
|
23
|
-
currentSlug:
|
|
24
|
-
hub:
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
defaultOpen: C,
|
|
24
|
+
function W({
|
|
25
|
+
products: l,
|
|
26
|
+
currentSlug: s,
|
|
27
|
+
hub: i,
|
|
28
|
+
labels: c,
|
|
29
|
+
linkComponent: C,
|
|
30
|
+
collapsed: d = !1,
|
|
31
|
+
defaultOpen: y,
|
|
30
32
|
onOpenChange: R
|
|
31
33
|
}) {
|
|
32
|
-
const
|
|
33
|
-
return /* @__PURE__ */
|
|
34
|
-
/* @__PURE__ */
|
|
34
|
+
const t = O({ defaultOpen: y, onOpenChange: R }), P = M(), m = j(c), g = C ?? "a", h = l.find((r) => r.slug === s), A = l.filter((r) => r.live !== !1 || r.slug === s), N = (h == null ? void 0 : h.name) ?? m.switchProduct;
|
|
35
|
+
return /* @__PURE__ */ n("div", { className: e.root, children: [
|
|
36
|
+
/* @__PURE__ */ n(
|
|
35
37
|
"button",
|
|
36
38
|
{
|
|
37
39
|
type: "button",
|
|
38
|
-
className:
|
|
39
|
-
onClick:
|
|
40
|
-
...
|
|
40
|
+
className: d ? e.railTrigger : e.trigger,
|
|
41
|
+
onClick: t.toggle,
|
|
42
|
+
...t.triggerProps,
|
|
41
43
|
children: [
|
|
42
|
-
/* @__PURE__ */ a(
|
|
43
|
-
!
|
|
44
|
-
/* @__PURE__ */ a("span", { className: e.triggerName, children:
|
|
44
|
+
/* @__PURE__ */ a(k, { slug: s, title: d ? N : void 0 }),
|
|
45
|
+
!d && /* @__PURE__ */ n(w, { children: [
|
|
46
|
+
/* @__PURE__ */ a("span", { className: e.triggerName, children: N }),
|
|
45
47
|
/* @__PURE__ */ a(x, { className: e.chevron })
|
|
46
48
|
] })
|
|
47
49
|
]
|
|
48
50
|
}
|
|
49
51
|
),
|
|
50
|
-
|
|
51
|
-
/* @__PURE__ */
|
|
52
|
-
/* @__PURE__ */ a(
|
|
53
|
-
/* @__PURE__ */
|
|
54
|
-
/* @__PURE__ */ a("span", { className: e.rowName, children:
|
|
55
|
-
/* @__PURE__ */ a("span", { className: e.rowSub, children:
|
|
56
|
-
] })
|
|
52
|
+
t.open && /* @__PURE__ */ n("div", { ...t.panelProps, className: e.panel, "aria-label": m.panel, children: [
|
|
53
|
+
/* @__PURE__ */ n(g, { className: e.hubRow, href: i.href, onClick: t.close, children: [
|
|
54
|
+
/* @__PURE__ */ a(I, { className: e.hubMark }),
|
|
55
|
+
/* @__PURE__ */ n("span", { className: e.rowText, children: [
|
|
56
|
+
/* @__PURE__ */ a("span", { className: e.rowName, children: i.name }),
|
|
57
|
+
/* @__PURE__ */ a("span", { className: e.rowSub, children: i.description })
|
|
58
|
+
] }),
|
|
59
|
+
/* @__PURE__ */ a(L, { className: e.hubChevron })
|
|
57
60
|
] }),
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
61
|
+
_(A).map((r, f) => {
|
|
62
|
+
const v = `${P}-group-${f}`;
|
|
63
|
+
return /* @__PURE__ */ n("div", { className: e.group, children: [
|
|
64
|
+
r.label !== void 0 && /* @__PURE__ */ a("p", { id: v, className: e.groupLabel, children: r.label }),
|
|
65
|
+
/* @__PURE__ */ a("ul", { className: e.products, "aria-labelledby": r.label !== void 0 ? v : void 0, children: r.products.map((o) => {
|
|
66
|
+
const u = o.slug === s, p = o.entitled === !1, b = p ? m.notOnPlan : null;
|
|
67
|
+
return /* @__PURE__ */ a("li", { children: /* @__PURE__ */ n(
|
|
68
|
+
g,
|
|
69
|
+
{
|
|
70
|
+
className: e.productRow,
|
|
71
|
+
href: o.href,
|
|
72
|
+
"aria-current": u ? "true" : void 0,
|
|
73
|
+
"data-current": u ? "true" : void 0,
|
|
74
|
+
"data-entitled": p ? "false" : void 0,
|
|
75
|
+
onClick: t.close,
|
|
76
|
+
children: [
|
|
77
|
+
/* @__PURE__ */ a(k, { slug: o.slug, size: E, muted: p }),
|
|
78
|
+
/* @__PURE__ */ a("span", { className: e.rowName, children: o.name }),
|
|
79
|
+
b !== null && /* @__PURE__ */ a("span", { className: e.tag, children: b }),
|
|
80
|
+
/* @__PURE__ */ a("span", { className: e.trailing, children: u ? /* @__PURE__ */ a(T, { className: e.check }) : /* @__PURE__ */ a($, { trailing: o.trailing }) })
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
) }, o.slug);
|
|
84
|
+
}) })
|
|
85
|
+
] }, f);
|
|
86
|
+
})
|
|
82
87
|
] })
|
|
83
88
|
] });
|
|
84
89
|
}
|
|
85
90
|
export {
|
|
86
|
-
|
|
91
|
+
W as AppSwitcher
|
|
87
92
|
};
|
|
88
93
|
//# sourceMappingURL=AppSwitcher.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AppSwitcher.js","sources":["../../../src/components/AppSwitcher/AppSwitcher.tsx"],"sourcesContent":["'use client'\n\n// A client module: the switcher holds open/closed state and wires clicks, focus\n// and Escape through useAnchoredOverlay, none of which a Server Component can\n// carry. The rest of the shell stays on the server -- see WalletChip, Sidebar\n// and B33-846. AppShell renders this as a slot (`switcher`), so it imports no\n// client code by taking it.\n\nimport type { ElementType } from 'react'\nimport { useAnchoredOverlay } from '../../anchored-overlay'\nimport { ProductMark } from '../ProductMark/ProductMark'\nimport { Check, Chevron, ChevronRight, HubMark } from './icons'\nimport { resolveAppSwitcherLabels, type AppSwitcherLabels } from './labels'\nimport styles from './AppSwitcher.module.css'\n\n/**\n * One product the switcher can jump to. It is Identity's registry row, supplied\n * by the consuming app (ADR-0003) -- the switcher fetches nothing and hardcodes\n * no list. Every product is rendered, entitled or not and built or not: hiding\n * an unowned one means nobody learns the family exists (B33-479), and hiding an\n * unbuilt one means nobody learns it is coming.\n */\nexport interface AppSwitcherProduct {\n /** Identity's product slug, which selects the ProductMark artwork. */\n slug: string\n /** The product's display name, e.g. \"Boost Score\". Identity's, not the slug. */\n name: string\n /** Where selecting this product goes -- the product's own URL, never the hub. */\n href: string\n /**\n * Whether the account holds this product. Defaults to `true`. `false` dims the\n * mark and shows the \"Not on plan\" tag, but the row stays an ordinary focusable\n * link: entitlement is a rendering hint, and the switcher is not the thing\n * deciding what someone may open (ADR-0010, \"Not entitled is not disabled\").\n */\n entitled?: boolean\n /**\n * Whether the product exists yet. Defaults to `true`. `false` dims the mark and\n * shows the \"Soon\" tag in place of \"Not on plan\".\n *\n * **A different question from `entitled`, which is why it is a second field\n * and not a third value of the first.** `live` is global: the same answer for\n * every account, because either the product has shipped or it has not.\n * `entitled` is per account, read from the grant. Identity serves both for\n * exactly this reason (B33-561, and the `live` note in\n * `boost33-identity/src/lib/productRegistry.ts`).\n *\n * Until 0.4 this interface had nowhere to put it, so both consumers read the\n * field off the registry and dropped it on the floor -- each with a comment\n * saying so and calling it worth a package ticket. The cost was a row that\n * told a client they had not bought Boost Voice, when nobody has: the row read\n * \"Not on plan\" and pointed at a product with no code behind it.\n *\n * It stays a rendering hint like `entitled`. An unbuilt product's row is still\n * an ordinary focusable link -- the switcher does not decide what anyone may\n * open (ADR-0010), and `url` is served for every slug whether or not anything\n * answers on it, because `?app=` has to resolve for a campaign link.\n */\n live?: boolean\n}\n\n/** The \"all products\" row above the divider. Its blurb and URL are the consumer's. */\nexport interface AppSwitcherHub {\n /** The row's name, e.g. \"Hub\". */\n name: string\n /** The line beneath it, e.g. \"All Boost33 products\". */\n description: string\n /** The hub URL. */\n href: string\n}\n\n/**\n * The account/workspace row at the foot of the panel. A row that opens a second\n * panel (a later ticket owns that panel), so it is a button rather than a link;\n * this ticket renders the row and calls `onSelect` when it is chosen.\n */\nexport interface AppSwitcherAccount {\n /** The organisation's name, e.g. \"Acme Coaching\". */\n name: string\n /** The avatar's initials, e.g. \"AC\". */\n initials: string\n /** The line beneath the name, preformatted, e.g. \"Admin · 2 workspaces\". */\n description: string\n /**\n * Chosen -- opens the workspace panel.\n *\n * **It is also what makes the row a control.** Supplied, the row is the\n * `<button>` it has always been, with the chevron pointing at the panel it\n * opens. Omitted, the row still draws -- the organisation and its workspace\n * count are facts about the account whether or not anything opens -- but as a\n * plain `<div>`: no chevron, no hover step, no tab stop (B33-1270).\n *\n * Both consumers ship it unarmed today, because the panel it would open is\n * B33-493's and unbuilt: see the comment on `account` in\n * `boostscore/components/app-shell/product-switcher.tsx` (\"the Wallet's\n * migration left the same row inert for the same reason\"). Before this gate\n * existed they got a button that swallowed a click and did nothing, and no way\n * to reach it -- `.accountRow`'s class name is CSS-Modules-hashed.\n *\n * The whole row is not removed, and that is deliberate. ADR-0010's \"Not\n * entitled is not disabled\" is the nearest settled reasoning: a row that\n * carries information is not the component's to withhold, and `aria-disabled`\n * does not belong on something nobody can act on anyway. So the information\n * stays and the affordances go. Nor is the button `disabled` -- that is still\n * a control a consumer cannot remove.\n */\n onSelect?: () => void\n}\n\nexport interface AppSwitcherProps {\n /**\n * The products, in the order Identity serves them. Rendered whole -- entitled\n * or not, built or not.\n */\n products: AppSwitcherProduct[]\n /**\n * The slug of the product this shell is rendering for. It names the trigger\n * and is the row that carries the check -- the product you are in.\n *\n * A slug matching nothing in `products` is a supported state, not a caller\n * error: the registry is Identity's and can run ahead of this package\n * (ADR-0003). The mark degrades to a bare tile and the trigger falls back to\n * `labels.switchProduct` for its name (B33-919).\n */\n currentSlug: string\n /** The \"all products\" row above the products. */\n hub: AppSwitcherHub\n /** The account/workspace row below the products. */\n account: AppSwitcherAccount\n /**\n * Every user-facing string that is not row data. Each field defaults to\n * English, and a field left out -- or supplied as `undefined`, which is what\n * a missed next-intl key looks like -- keeps its default (B33-873).\n */\n labels?: Partial<AppSwitcherLabels>\n /**\n * The element the product and hub rows render through. Defaults to a plain\n * `a`; a router-owning consumer passes its own so the rows route client-side\n * with the locale prefix intact (ADR-0011, B33-847). The account row stays a\n * button -- it opens a panel, it does not navigate.\n */\n linkComponent?: ElementType\n /** The 56px rail. The trigger collapses to the current product's mark alone. */\n collapsed?: boolean\n /** Render the panel open on first paint -- the specimen's Stage-A open state. */\n defaultOpen?: boolean\n /** Every open and close, so a consumer can un-clip an ancestor's overflow while the panel overhangs. */\n onOpenChange?: (open: boolean) => void\n}\n\n/**\n * The panel's foot, in its two shapes. The information is identical in both and\n * the affordances are not: a row with an `onSelect` is the button it always was,\n * and a row without one keeps the organisation and its workspace count while\n * losing everything that promises a second panel (B33-1270).\n *\n * The chevron goes with the handler rather than with the row, because it is the\n * part of the row that makes a promise: it points at the workspace panel, and\n * there is nothing to point at when nobody is listening. The avatar stays\n * `aria-hidden` either way -- the initials restate the name beside them.\n *\n * In the unarmed shape the accessible name is nothing at all, which is correct:\n * a `<div>` is not a control, so it has no name to get wrong, and a screen\n * reader reads its two lines of text in document order like any other text in\n * the panel. The armed shape's name is unchanged -- the button's content, which\n * is the organisation and its description.\n */\nfunction AccountRow({ account }: { account: AppSwitcherAccount }) {\n const body = (\n <>\n <span className={styles.avatar} aria-hidden=\"true\">\n {account.initials}\n </span>\n <span className={styles.rowText}>\n <span className={styles.rowName}>{account.name}</span>\n <span className={styles.rowSub}>{account.description}</span>\n </span>\n </>\n )\n\n // Tested against `undefined` rather than for truthiness, so a consumer's own\n // absent value -- `onSelect: undefined`, which is what an unwired branch\n // produces -- reads the same as leaving the field out.\n if (account.onSelect === undefined) {\n return (\n <div className={styles.accountRow} data-actionable=\"false\">\n {body}\n </div>\n )\n }\n\n return (\n <button type=\"button\" className={styles.accountRow} onClick={account.onSelect}>\n {body}\n <ChevronRight className={styles.chevronRight} />\n </button>\n )\n}\n\n/**\n * The product switcher under the wordmark. It opens, lists every Boost33 product\n * with the current one checked, and takes you straight into the one you pick --\n * the real switcher the Wallet's chevron-that-opens-nothing never was (B33-479).\n *\n * A disclosure, not a menu (ADR-0010): the panel carries no widget role, the\n * rows are the real links and buttons they already are, and Tab is the\n * document's own tab order -- so tabbing through the products does not close the\n * panel on the first row. Dismissal, focus restoration and trigger ARIA are the\n * shared anchored-overlay contract's (ADR-0009); it never becomes a modal.\n */\nexport function AppSwitcher({\n products,\n currentSlug,\n hub,\n account,\n labels,\n linkComponent,\n collapsed = false,\n defaultOpen,\n onOpenChange,\n}: AppSwitcherProps) {\n const overlay = useAnchoredOverlay({ defaultOpen, onOpenChange })\n const t = resolveAppSwitcherLabels(labels)\n const Link: ElementType = linkComponent ?? 'a'\n const current = products.find((p) => p.slug === currentSlug)\n\n // The name the trigger carries, in both its shapes. `current` is `undefined`\n // whenever `currentSlug` matches nothing in the served registry -- a fifth\n // Identity product, or a slug left stale by a rename -- which ADR-0003\n // contemplates and `AppSwitcherProduct.slug` being `string` rather than\n // `ProductSlug` deliberately allows.\n //\n // Degrading the artwork to a bare tile there is right. Degrading the name is\n // not, and that is what happened: the mark is `aria-hidden` unless it is\n // given a title, so a collapsed trigger handed `title={undefined}` had no\n // accessible name at all. A screen reader reached the only control in the\n // 56px rail and announced \"button\" -- no product, no purpose, nothing to act\n // on. Expanded it was the same absence with a visible face: an empty\n // `.triggerName` between a blank tile and a chevron. CLAUDE.md puts labels in\n // the definition of done, so the fallback is a label prop rather than a\n // literal (B33-919, B33-849).\n const triggerName = current?.name ?? t.switchProduct\n\n return (\n <div className={styles.root}>\n <button\n type=\"button\"\n className={collapsed ? styles.railTrigger : styles.trigger}\n onClick={overlay.toggle}\n {...overlay.triggerProps}\n >\n {/* On the rail the mark is the button's only content, so it carries the\n accessible name (title); expanded, the name sits beside it and the\n mark is decorative, or a screen reader says the product twice.\n\n No `size`. ProductMark's default is 20px -- the Wallet's own size at\n its call sites, and the size all three appswitcher-* baselines were\n captured at. This passed 28 expanded and 32 collapsed until B33-479's\n fidelity pass: a mark 8px over budget in every row pushed the account\n row out of the open panel's frame entirely and truncated \"Boost\n Outbound\" to \"Boos...\". The rail is not a larger mark; it is the same\n mark with the label and chevron gone. */}\n <ProductMark slug={currentSlug} title={collapsed ? triggerName : undefined} />\n {!collapsed && (\n <>\n <span className={styles.triggerName}>{triggerName}</span>\n <Chevron className={styles.chevron} />\n </>\n )}\n </button>\n\n {overlay.open && (\n <div {...overlay.panelProps} className={styles.panel} aria-label={t.panel}>\n <Link className={styles.hubRow} href={hub.href} onClick={overlay.close}>\n <HubMark className={styles.hubMark} />\n <span className={styles.rowText}>\n <span className={styles.rowName}>{hub.name}</span>\n <span className={styles.rowSub}>{hub.description}</span>\n </span>\n </Link>\n\n <hr className={styles.divider} />\n\n <ul className={styles.products}>\n {products.map((product) => {\n const isCurrent = product.slug === currentSlug\n const notEntitled = product.entitled === false\n const notLive = product.live === false\n\n // One tag slot, and the unbuilt word wins it.\n //\n // A product that has not shipped is held by nobody, so Identity\n // serves every `live: false` row as `entitled: false` too and both\n // conditions are true at once on the same row. Rendering both tags\n // would stack two pills on a row with one tag's worth of space --\n // and the second would be answering a question the first has\n // already closed: \"not on plan\" is trivially true of a product\n // that does not exist, and saying it sends the reader off to ask\n // about buying something unbuyable (B33-561).\n //\n // The dim is deliberately shared rather than split. `muted` has\n // one meaning on the mark -- this row is not somewhere you can go\n // today -- and that is equally true either way; the tag is the\n // channel carrying *which* reason, because it is the one a screen\n // reader reaches and the mark is decorative here (ADR-0012).\n const tag = notLive ? t.soon : notEntitled ? t.notOnPlan : null\n\n return (\n <li key={product.slug}>\n <Link\n className={styles.productRow}\n href={product.href}\n aria-current={isCurrent ? 'true' : undefined}\n data-current={isCurrent ? 'true' : undefined}\n data-entitled={notEntitled ? 'false' : undefined}\n data-live={notLive ? 'false' : undefined}\n onClick={overlay.close}\n >\n {/* Default 20px, as in the trigger above -- see the note there. */}\n <ProductMark slug={product.slug} muted={notEntitled || notLive} />\n <span className={styles.rowName}>{product.name}</span>\n {tag !== null && <span className={styles.tag}>{tag}</span>}\n {isCurrent && <Check className={styles.check} />}\n </Link>\n </li>\n )\n })}\n </ul>\n\n <hr className={styles.divider} />\n\n <AccountRow account={account} />\n </div>\n )}\n </div>\n )\n}\n"],"names":["AccountRow","account","body","jsxs","Fragment","jsx","styles","ChevronRight","AppSwitcher","products","currentSlug","hub","labels","linkComponent","collapsed","defaultOpen","onOpenChange","overlay","useAnchoredOverlay","resolveAppSwitcherLabels","Link","current","p","triggerName","ProductMark","Chevron","HubMark","product","isCurrent","notEntitled","notLive","tag","Check"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AppSwitcher.js","sources":["../../../src/components/AppSwitcher/AppSwitcher.tsx"],"sourcesContent":["'use client'\n\n// A client module: the switcher holds open/closed state and wires clicks, focus\n// and Escape through useAnchoredOverlay, none of which a Server Component can\n// carry. The rest of the shell stays on the server -- see WalletChip, Sidebar\n// and B33-846. AppShell renders this as a slot (`switcher`), so it imports no\n// client code by taking it.\n\nimport { useId, type ElementType } from 'react'\nimport { useAnchoredOverlay } from '../../anchored-overlay'\nimport { ProductMark } from '../ProductMark/ProductMark'\nimport { Check, Chevron, ChevronRight, HubMark } from './icons'\nimport { resolveAppSwitcherLabels, type AppSwitcherLabels } from './labels'\nimport styles from './AppSwitcher.module.css'\n\n/**\n * One product the switcher can jump to. It is Identity's registry row, supplied\n * by the consuming app (ADR-0003) -- the switcher fetches nothing and hardcodes\n * no list. Every product that exists is rendered, entitled or not: hiding an\n * unowned one means nobody learns the family exists (B33-479), and it tells a\n * client who could buy a product that the product is not there.\n *\n * A product that does not exist yet is not rendered (B33-1711). Until 0.8 it\n * drew with a \"Soon\" tag; the navigation redesign of 3 Nov 2026 took unbuilt\n * products off the switcher, the same way they came off the hub. See `live`.\n */\nexport interface AppSwitcherProduct {\n /** Identity's product slug, which selects the ProductMark artwork. */\n slug: string\n /** The product's display name, e.g. \"Boost Score\". Identity's, not the slug. */\n name: string\n /** Where selecting this product goes -- the product's own URL, never the hub. */\n href: string\n /**\n * Whether the account holds this product. Defaults to `true`. `false` dims the\n * mark and shows the \"Not on plan\" tag, but the row stays an ordinary focusable\n * link: entitlement is a rendering hint, and the switcher is not the thing\n * deciding what someone may open (ADR-0010, \"Not entitled is not disabled\").\n */\n entitled?: boolean\n /**\n * Whether the product exists yet. Defaults to `true`. `false` leaves the row\n * out of the panel entirely (B33-1711), unless it is the product this shell is\n * rendering for -- the current product is always listed, since you are in it.\n *\n * **Only this hides a row. `entitled: false` never does.** The two answer\n * different questions, and the difference is commercial: a product the account\n * could buy is real, and leaving it out tells a paying client it does not\n * exist. That rule is the hub's as well (boost33-hub CLAUDE.md, `cardState`),\n * and the switcher follows it.\n *\n * Until 0.8 an unbuilt row was drawn with a \"Soon\" tag. The 3 Nov 2026\n * navigation redesign (B33-1710) removed unbuilt products from the switcher,\n * which overrules the Vocabulary document's \"the switcher shows every product\n * regardless of state\"; that document is amended alongside this change.\n *\n * What follows is why `live` is a second field rather than a third value of\n * `entitled`, which still holds.\n *\n * **A different question from `entitled`, which is why it is a second field\n * and not a third value of the first.** `live` is global: the same answer for\n * every account, because either the product has shipped or it has not.\n * `entitled` is per account, read from the grant. Identity serves both for\n * exactly this reason (B33-561, and the `live` note in\n * `boost33-identity/src/lib/productRegistry.ts`).\n *\n * Until 0.4 this interface had nowhere to put it, so both consumers read the\n * field off the registry and dropped it on the floor -- each with a comment\n * saying so and calling it worth a package ticket. The cost was a row that\n * told a client they had not bought Boost Voice, when nobody has: the row read\n * \"Not on plan\" and pointed at a product with no code behind it.\n *\n * Hiding the row is a rendering decision, not an access one. The switcher\n * still does not decide what anyone may open (ADR-0010), and `url` is still\n * served for every slug, because `?app=` has to resolve for a campaign link.\n */\n live?: boolean\n /**\n * The heading of the section this row sits under, in the consumer's language\n * -- \"Your tools\", \"Included with every account\" (B33-1711, board 21). Rows\n * are grouped by this label in the order the labels first appear, and each\n * group gets a header. Leave it off every row and the panel is one plain list\n * with no headers; rows without one, in a panel where others have one, form a\n * headerless group of their own.\n *\n * Data the consumer is served, never decided here: which products are \"your\n * tools\" is the account's grant and Identity's registry, and the package\n * knows neither (CLAUDE.md -- no component knows what a product means).\n */\n group?: string\n /**\n * One thing at the right of the row (B33-1711, board 21) -- a figure such as\n * the Wallet's balance, or a dot saying there is something new. The consumer\n * supplies it already formatted; the switcher fetches nothing and formats\n * nothing. The current product's check takes the same slot, so a trailing on\n * the current row is not drawn.\n */\n trailing?: AppSwitcherTrailing\n}\n\n/**\n * The right-hand slot of a product row, in its two shapes.\n *\n * - `value` -- a short preformatted figure, drawn muted in the mono face:\n * `{ kind: 'value', value: '8,940' }`.\n * - `dot` -- a small primary dot meaning \"something new here\". A dot is a\n * colour and nothing else, so `label` is required: it is what a screen reader\n * hears instead (\"2 new\"), in the consumer's language.\n */\nexport type AppSwitcherTrailing = { kind: 'value'; value: string } | { kind: 'dot'; label: string }\n\n/** The \"all products\" row at the top of the panel. Its blurb and URL are the consumer's. */\nexport interface AppSwitcherHub {\n /** The row's name, e.g. \"Hub\". */\n name: string\n /** The line beneath it, e.g. \"All Boost33 products\". */\n description: string\n /** The hub URL. */\n href: string\n}\n\n/**\n * The account/workspace row that sat at the foot of the panel until 0.8.0.\n *\n * **Not drawn since 0.8.0.** The 3 Nov 2026 redesign (B33-1710, board 21) took\n * the account row off the switcher: the organisation now lives in the top bar's\n * left-hand slot, as the consumer's own pill (B33-1712). The type stays so a\n * consumer's existing `account` object still compiles; nothing reads it.\n *\n * @deprecated Rendered nowhere since 0.8.0 (B33-1711). Stop passing `account`.\n */\nexport interface AppSwitcherAccount {\n /** The organisation's name, e.g. \"Acme Coaching\". */\n name: string\n /** The avatar's initials, e.g. \"AC\". */\n initials: string\n /** The line beneath the name, preformatted, e.g. \"Admin · 2 workspaces\". */\n description: string\n /** Was: chosen -- opens the workspace panel. Never called since 0.8.0. */\n onSelect?: () => void\n}\n\nexport interface AppSwitcherProps {\n /**\n * The products, in the order Identity serves them. Pass the registry whole:\n * the switcher leaves out the rows with `live: false` itself (B33-1711), so a\n * consumer does not filter, and every product that exists is drawn whether\n * the account holds it or not.\n */\n products: AppSwitcherProduct[]\n /**\n * The slug of the product this shell is rendering for. It names the trigger\n * and is the row that carries the check -- the product you are in.\n *\n * A slug matching nothing in `products` is a supported state, not a caller\n * error: the registry is Identity's and can run ahead of this package\n * (ADR-0003). The mark degrades to a bare tile and the trigger falls back to\n * `labels.switchProduct` for its name (B33-919).\n */\n currentSlug: string\n /** The \"all products\" row above the products. */\n hub: AppSwitcherHub\n /**\n * Was the account/workspace row below the products. Optional and unread since\n * 0.8.0, when the redesign took that row off the panel (B33-1711).\n *\n * @deprecated Rendered nowhere since 0.8.0. The organisation goes in the top\n * bar's slot instead (B33-1712).\n */\n account?: AppSwitcherAccount\n /**\n * Every user-facing string that is not row data. Each field defaults to\n * English, and a field left out -- or supplied as `undefined`, which is what\n * a missed next-intl key looks like -- keeps its default (B33-873).\n */\n labels?: Partial<AppSwitcherLabels>\n /**\n * The element the product and hub rows render through. Defaults to a plain\n * `a`; a router-owning consumer passes its own so the rows route client-side\n * with the locale prefix intact (ADR-0011, B33-847).\n */\n linkComponent?: ElementType\n /** The 56px rail. The trigger collapses to the current product's mark alone. */\n collapsed?: boolean\n /** Render the panel open on first paint -- the specimen's Stage-A open state. */\n defaultOpen?: boolean\n /** Every open and close, so a consumer can un-clip an ancestor's overflow while the panel overhangs. */\n onOpenChange?: (open: boolean) => void\n}\n\n/** The panel's product tile, in px: board 21's 28px row tile. */\nconst ROW_MARK_SIZE = 28\n\n/**\n * Consecutive rows that share a `group`, in the order the rows arrive. A run\n * whose rows carry no group has no label and draws no header, so a list with no\n * groups at all is one headerless run -- the plain list.\n *\n * Runs, not buckets: two rows with the same label separated by a different one\n * draw two sections with the same header. The order is Identity's, and\n * regrouping would silently reorder what the consumer was served.\n */\nfunction groupRuns(products: AppSwitcherProduct[]): { label: string | undefined; products: AppSwitcherProduct[] }[] {\n const runs: { label: string | undefined; products: AppSwitcherProduct[] }[] = []\n for (const product of products) {\n const last = runs[runs.length - 1]\n if (last !== undefined && last.label === product.group) last.products.push(product)\n else runs.push({ label: product.group, products: [product] })\n }\n return runs\n}\n\n/** A row's right-hand figure or dot. A dot carries its words for a screen reader. */\nfunction Trailing({ trailing }: { trailing: AppSwitcherTrailing | undefined }) {\n if (trailing === undefined) return null\n if (trailing.kind === 'value') return <span className={styles.trailingValue}>{trailing.value}</span>\n return (\n <>\n <span className={styles.trailingDot} aria-hidden=\"true\" />\n <span className={styles.srOnly}>{trailing.label}</span>\n </>\n )\n}\n\n/**\n * The product switcher. It opens, lists every Boost33 product that exists with\n * the current one checked, and takes you straight into the one you pick --\n * the real switcher the Wallet's chevron-that-opens-nothing never was (B33-479).\n *\n * A disclosure, not a menu (ADR-0010): the panel carries no widget role, the\n * rows are the real links and buttons they already are, and Tab is the\n * document's own tab order -- so tabbing through the products does not close the\n * panel on the first row. Dismissal, focus restoration and trigger ARIA are the\n * shared anchored-overlay contract's (ADR-0009); it never becomes a modal.\n */\nexport function AppSwitcher({\n products,\n currentSlug,\n hub,\n labels,\n linkComponent,\n collapsed = false,\n defaultOpen,\n onOpenChange,\n}: AppSwitcherProps) {\n const overlay = useAnchoredOverlay({ defaultOpen, onOpenChange })\n const panelId = useId()\n const t = resolveAppSwitcherLabels(labels)\n const Link: ElementType = linkComponent ?? 'a'\n const current = products.find((p) => p.slug === currentSlug)\n // Unbuilt products are not drawn (B33-1711) -- except the one you are in.\n // A shell rendering for a product Identity still marks `live: false` is a\n // product in a preview or a soft launch, and a switcher that left out the\n // place you are standing would have nothing to put its check on.\n const listed = products.filter((p) => p.live !== false || p.slug === currentSlug)\n\n // The name the trigger carries, in both its shapes. `current` is `undefined`\n // whenever `currentSlug` matches nothing in the served registry -- a fifth\n // Identity product, or a slug left stale by a rename -- which ADR-0003\n // contemplates and `AppSwitcherProduct.slug` being `string` rather than\n // `ProductSlug` deliberately allows.\n //\n // Degrading the artwork to a bare tile there is right. Degrading the name is\n // not, and that is what happened: the mark is `aria-hidden` unless it is\n // given a title, so a collapsed trigger handed `title={undefined}` had no\n // accessible name at all. A screen reader reached the only control in the\n // 56px rail and announced \"button\" -- no product, no purpose, nothing to act\n // on. Expanded it was the same absence with a visible face: an empty\n // `.triggerName` between a blank tile and a chevron. CLAUDE.md puts labels in\n // the definition of done, so the fallback is a label prop rather than a\n // literal (B33-919, B33-849).\n const triggerName = current?.name ?? t.switchProduct\n\n return (\n <div className={styles.root}>\n <button\n type=\"button\"\n className={collapsed ? styles.railTrigger : styles.trigger}\n onClick={overlay.toggle}\n {...overlay.triggerProps}\n >\n {/* On the rail the mark is the button's only content, so it carries the\n accessible name (title); expanded, the name sits beside it and the\n mark is decorative, or a screen reader says the product twice.\n\n No `size`. ProductMark's default is 20px -- the Wallet's own size at\n its call sites, and the size all three appswitcher-* baselines were\n captured at. This passed 28 expanded and 32 collapsed until B33-479's\n fidelity pass: a mark 8px over budget in every row pushed the account\n row out of the open panel's frame entirely and truncated \"Boost\n Outbound\" to \"Boos...\". The rail is not a larger mark; it is the same\n mark with the label and chevron gone. */}\n <ProductMark slug={currentSlug} title={collapsed ? triggerName : undefined} />\n {!collapsed && (\n <>\n <span className={styles.triggerName}>{triggerName}</span>\n <Chevron className={styles.chevron} />\n </>\n )}\n </button>\n\n {overlay.open && (\n <div {...overlay.panelProps} className={styles.panel} aria-label={t.panel}>\n {/* The way out to every tool, drawn as the panel's one dark row\n (board 21). Its chevron says it leaves this product. */}\n <Link className={styles.hubRow} href={hub.href} onClick={overlay.close}>\n <HubMark className={styles.hubMark} />\n <span className={styles.rowText}>\n <span className={styles.rowName}>{hub.name}</span>\n <span className={styles.rowSub}>{hub.description}</span>\n </span>\n <ChevronRight className={styles.hubChevron} />\n </Link>\n\n {groupRuns(listed).map((run, runIndex) => {\n const headerId = `${panelId}-group-${runIndex}`\n return (\n <div key={runIndex} className={styles.group}>\n {run.label !== undefined && (\n <p id={headerId} className={styles.groupLabel}>\n {run.label}\n </p>\n )}\n <ul className={styles.products} aria-labelledby={run.label !== undefined ? headerId : undefined}>\n {run.products.map((product) => {\n const isCurrent = product.slug === currentSlug\n const notEntitled = product.entitled === false\n // A product that has not shipped never reaches here (see\n // `listed`), so the one tag left is entitlement's. B33-561's\n // problem -- an unbuilt product reading \"Not on plan\" --\n // cannot come back: Identity serves every unbuilt row\n // `entitled: false`, and none of them is drawn.\n const tag = notEntitled ? t.notOnPlan : null\n\n return (\n <li key={product.slug}>\n <Link\n className={styles.productRow}\n href={product.href}\n aria-current={isCurrent ? 'true' : undefined}\n data-current={isCurrent ? 'true' : undefined}\n data-entitled={notEntitled ? 'false' : undefined}\n onClick={overlay.close}\n >\n {/* 28px in the panel (board 21) -- the tile the row is\n built around. The trigger keeps the 20px default. */}\n <ProductMark slug={product.slug} size={ROW_MARK_SIZE} muted={notEntitled} />\n <span className={styles.rowName}>{product.name}</span>\n {tag !== null && <span className={styles.tag}>{tag}</span>}\n <span className={styles.trailing}>\n {isCurrent ? <Check className={styles.check} /> : <Trailing trailing={product.trailing} />}\n </span>\n </Link>\n </li>\n )\n })}\n </ul>\n </div>\n )\n })}\n </div>\n )}\n </div>\n )\n}\n"],"names":["ROW_MARK_SIZE","groupRuns","products","runs","product","last","Trailing","trailing","jsx","styles","jsxs","Fragment","AppSwitcher","currentSlug","hub","labels","linkComponent","collapsed","defaultOpen","onOpenChange","overlay","useAnchoredOverlay","panelId","useId","t","resolveAppSwitcherLabels","Link","current","p","listed","triggerName","ProductMark","Chevron","HubMark","ChevronRight","run","runIndex","headerId","isCurrent","notEntitled","tag","Check"],"mappings":"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"}
|