@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.
- package/CHANGELOG.md +257 -0
- package/dist/buttons.module.css.js +10 -0
- package/dist/buttons.module.css.js.map +1 -0
- package/dist/components/AppShell/AppShell.d.ts +129 -17
- package/dist/components/AppShell/AppShell.js +78 -50
- package/dist/components/AppShell/AppShell.js.map +1 -1
- package/dist/components/AppShell/AppShell.module.css.js +22 -14
- package/dist/components/AppShell/AppShell.module.css.js.map +1 -1
- package/dist/components/AppSwitcher/AppSwitcher.d.ts +92 -45
- package/dist/components/AppSwitcher/AppSwitcher.js +76 -68
- 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 +15 -23
- package/dist/components/AppSwitcher/labels.js +3 -4
- package/dist/components/AppSwitcher/labels.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 +10 -10
- 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 +55 -4
- package/dist/components/TopBar/TopBar.js +24 -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/deprecated-prop-warning.d.ts +5 -0
- package/dist/deprecated-prop-warning.js +11 -0
- package/dist/deprecated-prop-warning.js.map +1 -0
- 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":"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 { warnDeprecatedProp } from '../../deprecated-prop-warning'\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 * in development, passing it logs a console warning once.\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). In development, passing it logs a console\n * warning once.\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 account,\n}: AppSwitcherProps) {\n if (account != null) warnDeprecatedProp('AppSwitcher.account')\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","account","warnDeprecatedProp","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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"}
|
|
@@ -1,44 +1,50 @@
|
|
|
1
|
-
const o = "
|
|
2
|
-
root:
|
|
3
|
-
trigger:
|
|
4
|
-
railTrigger:
|
|
1
|
+
const r = "_root_18wu0_8", o = "_trigger_18wu0_17", _ = "_railTrigger_18wu0_52", t = "_triggerName_18wu0_62", u = "_chevron_18wu0_79", n = "_panel_18wu0_127", e = "_hubRow_18wu0_147", c = "_productRow_18wu0_148", w = "_rowText_18wu0_179", g = "_rowName_18wu0_184", a = "_rowSub_18wu0_191", s = "_hubMark_18wu0_197", i = "_hubChevron_18wu0_204", l = "_group_18wu0_250", h = "_groupLabel_18wu0_258", p = "_products_18wu0_269", b = "_tag_18wu0_305", d = "_trailing_18wu0_317", k = "_check_18wu0_325", m = "_trailingValue_18wu0_333", v = "_trailingDot_18wu0_343", N = "_srOnly_18wu0_342", R = {
|
|
2
|
+
root: r,
|
|
3
|
+
trigger: o,
|
|
4
|
+
railTrigger: _,
|
|
5
5
|
triggerName: t,
|
|
6
|
-
chevron:
|
|
7
|
-
panel:
|
|
8
|
-
hubRow:
|
|
9
|
-
productRow:
|
|
10
|
-
|
|
11
|
-
products: s,
|
|
6
|
+
chevron: u,
|
|
7
|
+
panel: n,
|
|
8
|
+
hubRow: e,
|
|
9
|
+
productRow: c,
|
|
10
|
+
rowText: w,
|
|
12
11
|
rowName: g,
|
|
13
|
-
rowSub:
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
12
|
+
rowSub: a,
|
|
13
|
+
hubMark: s,
|
|
14
|
+
hubChevron: i,
|
|
15
|
+
group: l,
|
|
16
|
+
groupLabel: h,
|
|
17
|
+
products: p,
|
|
18
|
+
tag: b,
|
|
19
|
+
trailing: d,
|
|
20
|
+
check: k,
|
|
21
|
+
trailingValue: m,
|
|
22
|
+
trailingDot: v,
|
|
23
|
+
srOnly: N
|
|
21
24
|
};
|
|
22
25
|
export {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
r as
|
|
36
|
-
o as root,
|
|
26
|
+
k as check,
|
|
27
|
+
u as chevron,
|
|
28
|
+
R as default,
|
|
29
|
+
l as group,
|
|
30
|
+
h as groupLabel,
|
|
31
|
+
i as hubChevron,
|
|
32
|
+
s as hubMark,
|
|
33
|
+
e as hubRow,
|
|
34
|
+
n as panel,
|
|
35
|
+
c as productRow,
|
|
36
|
+
p as products,
|
|
37
|
+
_ as railTrigger,
|
|
38
|
+
r as root,
|
|
37
39
|
g as rowName,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
a as rowSub,
|
|
41
|
+
w as rowText,
|
|
42
|
+
N as srOnly,
|
|
43
|
+
b as tag,
|
|
44
|
+
d as trailing,
|
|
45
|
+
v as trailingDot,
|
|
46
|
+
m as trailingValue,
|
|
47
|
+
o as trigger,
|
|
42
48
|
t as triggerName
|
|
43
49
|
};
|
|
44
50
|
//# sourceMappingURL=AppSwitcher.module.css.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AppSwitcher.module.css.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"AppSwitcher.module.css.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;"}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { jsx as r, jsxs as
|
|
2
|
-
function
|
|
3
|
-
return /* @__PURE__ */
|
|
4
|
-
/* @__PURE__ */ r("rect", { x: "3", y: "3", width: "
|
|
5
|
-
/* @__PURE__ */ r("rect", { x: "
|
|
6
|
-
/* @__PURE__ */ r("rect", { x: "3", y: "
|
|
7
|
-
/* @__PURE__ */ r("rect", { x: "
|
|
1
|
+
import { jsx as r, jsxs as o } from "react/jsx-runtime";
|
|
2
|
+
function i({ className: e }) {
|
|
3
|
+
return /* @__PURE__ */ o("svg", { className: e, viewBox: "0 0 24 24", "aria-hidden": "true", children: [
|
|
4
|
+
/* @__PURE__ */ r("rect", { x: "3", y: "3", width: "8", height: "8", rx: "2", fill: "currentColor" }),
|
|
5
|
+
/* @__PURE__ */ r("rect", { x: "13", y: "3", width: "8", height: "8", rx: "2", fill: "currentColor" }),
|
|
6
|
+
/* @__PURE__ */ r("rect", { x: "3", y: "13", width: "8", height: "8", rx: "2", fill: "currentColor" }),
|
|
7
|
+
/* @__PURE__ */ r("rect", { x: "13", y: "13", width: "8", height: "8", rx: "2", fill: "var(--color-primary)" })
|
|
8
8
|
] });
|
|
9
9
|
}
|
|
10
|
-
function
|
|
10
|
+
function n({ className: e }) {
|
|
11
11
|
return /* @__PURE__ */ r("svg", { className: e, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: /* @__PURE__ */ r("path", { d: "M6 9l6 6 6-6" }) });
|
|
12
12
|
}
|
|
13
13
|
function h({ className: e }) {
|
|
@@ -18,8 +18,8 @@ function d({ className: e }) {
|
|
|
18
18
|
}
|
|
19
19
|
export {
|
|
20
20
|
d as Check,
|
|
21
|
-
|
|
21
|
+
n as Chevron,
|
|
22
22
|
h as ChevronRight,
|
|
23
|
-
|
|
23
|
+
i as HubMark
|
|
24
24
|
};
|
|
25
25
|
//# sourceMappingURL=icons.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"icons.js","sources":["../../../src/components/AppSwitcher/icons.tsx"],"sourcesContent":["// The switcher's own small glyphs, drawn in `currentColor` so the surrounding\n// text colour carries them and the no-hardcoded-hex guard stays satisfied. All\n// decorative -- the row's own text carries meaning -- so every one is\n// aria-hidden. The four product marks are ProductMark's; these are only the\n// chrome ProductMark does not draw.\n\n/**\n * The 2x2 grid that stands for \"all Boost33 products\" on the Hub row. Not a\n * ProductMark: the Hub is not a product and takes no dark tile -- it is four\n * rounded squares in the row's own ink, the universal \"all apps\" glyph.\n */\nexport function HubMark({ className }: { className?: string }) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\"
|
|
1
|
+
{"version":3,"file":"icons.js","sources":["../../../src/components/AppSwitcher/icons.tsx"],"sourcesContent":["// The switcher's own small glyphs, drawn in `currentColor` so the surrounding\n// text colour carries them and the no-hardcoded-hex guard stays satisfied. All\n// decorative -- the row's own text carries meaning -- so every one is\n// aria-hidden. The four product marks are ProductMark's; these are only the\n// chrome ProductMark does not draw.\n\n/**\n * The 2x2 grid that stands for \"all Boost33 products\" on the Hub row. Not a\n * ProductMark: the Hub is not a product and takes no dark tile -- it is four\n * rounded squares in the row's own ink, the universal \"all apps\" glyph.\n */\nexport function HubMark({ className }: { className?: string }) {\n // Four filled squares, the last in the brand orange -- the hub mark as board 21\n // draws it on the panel's dark Hub row (B33-1711). It was four outlined\n // squares on the Wallet-era light row.\n return (\n <svg className={className} viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <rect x=\"3\" y=\"3\" width=\"8\" height=\"8\" rx=\"2\" fill=\"currentColor\" />\n <rect x=\"13\" y=\"3\" width=\"8\" height=\"8\" rx=\"2\" fill=\"currentColor\" />\n <rect x=\"3\" y=\"13\" width=\"8\" height=\"8\" rx=\"2\" fill=\"currentColor\" />\n <rect x=\"13\" y=\"13\" width=\"8\" height=\"8\" rx=\"2\" fill=\"var(--color-primary)\" />\n </svg>\n )\n}\n\n/** The chevron on the trigger. Points down when closed; the CSS flips it up when open. */\nexport function Chevron({ className }: { className?: string }) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M6 9l6 6 6-6\" />\n </svg>\n )\n}\n\n/** The chevron on the account row, pointing at the second panel it opens. */\nexport function ChevronRight({ className }: { className?: string }) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M9 6l6 6-6 6\" />\n </svg>\n )\n}\n\n/** The check marking the current product -- a check, not a colour (B33-479). */\nexport function Check({ className }: { className?: string }) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M5 12l5 5 9-10\" />\n </svg>\n )\n}\n"],"names":["HubMark","className","jsx","Chevron","ChevronRight","Check"],"mappings":";AAWO,SAASA,EAAQ,EAAE,WAAAC,KAAqC;AAI7D,2BACG,OAAA,EAAI,WAAAA,GAAsB,SAAQ,aAAY,eAAY,QACzD,UAAA;AAAA,IAAA,gBAAAC,EAAC,QAAA,EAAK,GAAE,KAAI,GAAE,KAAI,OAAM,KAAI,QAAO,KAAI,IAAG,KAAI,MAAK,gBAAe;AAAA,IAClE,gBAAAA,EAAC,QAAA,EAAK,GAAE,MAAK,GAAE,KAAI,OAAM,KAAI,QAAO,KAAI,IAAG,KAAI,MAAK,gBAAe;AAAA,IACnE,gBAAAA,EAAC,QAAA,EAAK,GAAE,KAAI,GAAE,MAAK,OAAM,KAAI,QAAO,KAAI,IAAG,KAAI,MAAK,gBAAe;AAAA,IACnE,gBAAAA,EAAC,QAAA,EAAK,GAAE,MAAK,GAAE,MAAK,OAAM,KAAI,QAAO,KAAI,IAAG,KAAI,MAAK,uBAAA,CAAuB;AAAA,EAAA,GAC9E;AAEJ;AAGO,SAASC,EAAQ,EAAE,WAAAF,KAAqC;AAC7D,SACE,gBAAAC,EAAC,SAAI,WAAAD,GAAsB,SAAQ,aAAY,MAAK,QAAO,QAAO,gBAAe,aAAY,KAAI,eAAc,SAAQ,gBAAe,SAAQ,eAAY,QACxJ,UAAA,gBAAAC,EAAC,QAAA,EAAK,GAAE,eAAA,CAAe,EAAA,CACzB;AAEJ;AAGO,SAASE,EAAa,EAAE,WAAAH,KAAqC;AAClE,SACE,gBAAAC,EAAC,SAAI,WAAAD,GAAsB,SAAQ,aAAY,MAAK,QAAO,QAAO,gBAAe,aAAY,KAAI,eAAc,SAAQ,gBAAe,SAAQ,eAAY,QACxJ,UAAA,gBAAAC,EAAC,QAAA,EAAK,GAAE,eAAA,CAAe,EAAA,CACzB;AAEJ;AAGO,SAASG,EAAM,EAAE,WAAAJ,KAAqC;AAC3D,SACE,gBAAAC,EAAC,SAAI,WAAAD,GAAsB,SAAQ,aAAY,MAAK,QAAO,QAAO,gBAAe,aAAY,OAAM,eAAc,SAAQ,gBAAe,SAAQ,eAAY,QAC1J,UAAA,gBAAAC,EAAC,QAAA,EAAK,GAAE,iBAAA,CAAiB,EAAA,CAC3B;AAEJ;"}
|
|
@@ -13,25 +13,22 @@ export interface AppSwitcherLabels {
|
|
|
13
13
|
*/
|
|
14
14
|
notOnPlan: string;
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* **Unused since 0.8.0, and kept so a consumer's labels object still
|
|
17
|
+
* compiles.** It was the tag on a product that does not exist yet -- a row
|
|
18
|
+
* carrying `live: false`. B33-1711 took those rows off the switcher, so no row
|
|
19
|
+
* draws it any more.
|
|
18
20
|
*
|
|
19
|
-
* It
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
21
|
+
* It stays in the type rather than going, because a consumer that passes
|
|
22
|
+
* `labels={{ soon: 'Bientôt' }}` as an object literal would otherwise fail its
|
|
23
|
+
* typecheck on an excess property, and that is a breaking change across four
|
|
24
|
+
* products for a word nothing renders. It is optional rather than required so
|
|
25
|
+
* a consumer that types its catalogue as a full `AppSwitcherLabels` -- the way
|
|
26
|
+
* to make tsc catch a missed translation -- is not forced to carry a dead key
|
|
27
|
+
* (found by Brokers, 4 Nov 2026). Drop it from your catalogue at leisure.
|
|
25
28
|
*
|
|
26
|
-
*
|
|
27
|
-
* prevent (B33-561). With only `notOnPlan`, a product nobody can buy yet
|
|
28
|
-
* renders as one this client has not bought, and the row invites them to go
|
|
29
|
-
* and ask about something that does not exist.
|
|
30
|
-
*
|
|
31
|
-
* Like every other label here it is a prop with an English default, so the two
|
|
32
|
-
* French-default consumers translate it rather than reading "Soon".
|
|
29
|
+
* @deprecated Rendered nowhere since 0.8.0 (B33-1711).
|
|
33
30
|
*/
|
|
34
|
-
soon
|
|
31
|
+
soon?: string;
|
|
35
32
|
/**
|
|
36
33
|
* The trigger's name when `currentSlug` matches no product in the supplied
|
|
37
34
|
* list, so there is no product name to put there. Default "Switch product".
|
|
@@ -57,13 +54,8 @@ export interface AppSwitcherLabels {
|
|
|
57
54
|
* `currentSlug` matches nothing, and all three baselines were captured with the
|
|
58
55
|
* Wallet current.
|
|
59
56
|
*
|
|
60
|
-
* `soon`
|
|
61
|
-
*
|
|
62
|
-
* the four rows the baselines were captured from still draw exactly the tag they
|
|
63
|
-
* drew before the field existed. The Wallet never rendered a "Soon" tag at all
|
|
64
|
-
* -- it had no registry to read `live` from -- so there is no captured baseline
|
|
65
|
-
* for this state and `scripts/screenshot-states.ts` records the specimen row
|
|
66
|
-
* that shows it as ungated, pending a human capture (CLAUDE.md).
|
|
57
|
+
* `soon` reaches the screen nowhere since 0.8.0 (B33-1711) and is optional, so
|
|
58
|
+
* it has no default. See the field.
|
|
67
59
|
*/
|
|
68
60
|
export declare const defaultAppSwitcherLabels: AppSwitcherLabels;
|
|
69
61
|
/**
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
const n = {
|
|
2
2
|
panel: "Boost33 products",
|
|
3
3
|
notOnPlan: "Not on plan",
|
|
4
|
-
soon: "Soon",
|
|
5
4
|
switchProduct: "Switch product"
|
|
6
5
|
};
|
|
7
6
|
function r(t) {
|
|
8
|
-
const
|
|
9
|
-
Object.entries(t ?? {}).filter(([,
|
|
7
|
+
const e = Object.fromEntries(
|
|
8
|
+
Object.entries(t ?? {}).filter(([, o]) => o !== void 0)
|
|
10
9
|
);
|
|
11
|
-
return { ...n, ...
|
|
10
|
+
return { ...n, ...e };
|
|
12
11
|
}
|
|
13
12
|
export {
|
|
14
13
|
n as defaultAppSwitcherLabels,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"labels.js","sources":["../../../src/components/AppSwitcher/labels.ts"],"sourcesContent":["// The switcher's own user-facing words, each a prop with an English default --\n// the same pattern WalletChipLabels follows, and for the same reason: the first\n// two consumers are French-default, and the package renders what it is given\n// rather than taking an i18n dependency (CLAUDE.md; B33-849). The product names,\n// the Hub's name and blurb, and the account row's name and subtitle are not here\n// -- they are Identity's data, supplied per row on `products`, `hub` and\n// `account`, not copy this package owns.\n//\n// No label here carries a value, so this module names no placeholder. The one a\n// future field would use is `%name%` and not `{name}`: a brace is ICU\n// MessageFormat argument syntax, and a consumer keeps these strings in an ICU\n// catalogue (B33-1277; the reasoning is in `src/fill-template.ts`). Written down\n// because B33-1007 is open about this module quietly diverging from the chip's\n// contract, and a delimiter is the easiest thing to re-decide wrongly.\n\n/** Every user-facing string the switcher renders that is not row data. */\nexport interface AppSwitcherLabels {\n /**\n * The panel's accessible name. A disclosure panel carries no widget role, so\n * it takes an `aria-label` to be named at all (ADR-0010). Default\n * \"Boost33 products\".\n */\n panel: string\n /**\n * The tag on a product the account is not entitled to. Not a gate -- the row\n * stays an ordinary link -- only a hint (ADR-0010: \"Not entitled is not\n * disabled\"). Default \"Not on plan\".\n */\n notOnPlan: string\n /**\n *
|
|
1
|
+
{"version":3,"file":"labels.js","sources":["../../../src/components/AppSwitcher/labels.ts"],"sourcesContent":["// The switcher's own user-facing words, each a prop with an English default --\n// the same pattern WalletChipLabels follows, and for the same reason: the first\n// two consumers are French-default, and the package renders what it is given\n// rather than taking an i18n dependency (CLAUDE.md; B33-849). The product names,\n// the Hub's name and blurb, and the account row's name and subtitle are not here\n// -- they are Identity's data, supplied per row on `products`, `hub` and\n// `account`, not copy this package owns.\n//\n// No label here carries a value, so this module names no placeholder. The one a\n// future field would use is `%name%` and not `{name}`: a brace is ICU\n// MessageFormat argument syntax, and a consumer keeps these strings in an ICU\n// catalogue (B33-1277; the reasoning is in `src/fill-template.ts`). Written down\n// because B33-1007 is open about this module quietly diverging from the chip's\n// contract, and a delimiter is the easiest thing to re-decide wrongly.\n\n/** Every user-facing string the switcher renders that is not row data. */\nexport interface AppSwitcherLabels {\n /**\n * The panel's accessible name. A disclosure panel carries no widget role, so\n * it takes an `aria-label` to be named at all (ADR-0010). Default\n * \"Boost33 products\".\n */\n panel: string\n /**\n * The tag on a product the account is not entitled to. Not a gate -- the row\n * stays an ordinary link -- only a hint (ADR-0010: \"Not entitled is not\n * disabled\"). Default \"Not on plan\".\n */\n notOnPlan: string\n /**\n * **Unused since 0.8.0, and kept so a consumer's labels object still\n * compiles.** It was the tag on a product that does not exist yet -- a row\n * carrying `live: false`. B33-1711 took those rows off the switcher, so no row\n * draws it any more.\n *\n * It stays in the type rather than going, because a consumer that passes\n * `labels={{ soon: 'Bientôt' }}` as an object literal would otherwise fail its\n * typecheck on an excess property, and that is a breaking change across four\n * products for a word nothing renders. It is optional rather than required so\n * a consumer that types its catalogue as a full `AppSwitcherLabels` -- the way\n * to make tsc catch a missed translation -- is not forced to carry a dead key\n * (found by Brokers, 4 Nov 2026). Drop it from your catalogue at leisure.\n *\n * @deprecated Rendered nowhere since 0.8.0 (B33-1711).\n */\n soon?: string\n /**\n * The trigger's name when `currentSlug` matches no product in the supplied\n * list, so there is no product name to put there. Default \"Switch product\".\n *\n * That case is reachable by design: `AppSwitcherProduct.slug` is typed\n * `string` rather than `ProductSlug` so an unknown slug degrades to a bare\n * tile instead of failing (ADR-0003 -- Identity owns the registry and this\n * package does not know which slugs exist). A fifth Identity product, or a\n * `currentSlug` left stale by a rename, lands there. Before B33-919 the\n * degradation took the button's accessible name with it and the only control\n * in the collapsed shell announced nothing at all.\n *\n * It names the control rather than describing the missing product, because\n * the product is precisely what this package does not know. \"Switch product\"\n * is true in every state; a guess at the product's name would not be.\n */\n switchProduct: string\n}\n\n/**\n * The English defaults. A consumer that passes nothing renders exactly what the\n * appswitcher-* baselines were captured showing, so those baselines do not move.\n * `switchProduct` is no exception: it only ever reaches the screen when\n * `currentSlug` matches nothing, and all three baselines were captured with the\n * Wallet current.\n *\n * `soon` reaches the screen nowhere since 0.8.0 (B33-1711) and is optional, so\n * it has no default. See the field.\n */\nexport const defaultAppSwitcherLabels: AppSwitcherLabels = {\n panel: 'Boost33 products',\n notOnPlan: 'Not on plan',\n switchProduct: 'Switch product',\n}\n\n/**\n * Merge a consumer's partial `labels` onto the English defaults, treating an\n * explicitly-`undefined` field as unsupplied -- the same resolution\n * `resolveWalletChipLabels` performs, for the same two reasons.\n *\n * The first is forward compatibility, and it is why this exists now. `labels`\n * used to be a whole `AppSwitcherLabels`, so a consumer wrote the object out in\n * full; adding `switchProduct` to a required-field interface would have failed\n * the typecheck of every app that had done so, turning an accessibility fix\n * into a breaking change across four products on a patch bump (CLAUDE.md,\n * \"Versioning\"). Taking a `Partial` means a consumer supplies the fields it\n * wants to translate and inherits the rest, today and at the next field.\n *\n * The second is that a missed translation key must fall back to English rather\n * than blank the string. A next-intl `t('...')` returns `undefined` on a miss,\n * so `{ panel: undefined }` is what one missing key looks like at the call\n * site. A plain object spread copies that `undefined` across and unnames the\n * panel; a per-field skip does not (B33-873).\n */\nexport function resolveAppSwitcherLabels(labels?: Partial<AppSwitcherLabels>): AppSwitcherLabels {\n const supplied = Object.fromEntries(\n Object.entries(labels ?? {}).filter(([, value]) => value !== undefined),\n ) as Partial<AppSwitcherLabels>\n return { ...defaultAppSwitcherLabels, ...supplied }\n}\n"],"names":["defaultAppSwitcherLabels","resolveAppSwitcherLabels","labels","supplied","value"],"mappings":"AA2EO,MAAMA,IAA8C;AAAA,EACzD,OAAO;AAAA,EACP,WAAW;AAAA,EACX,eAAe;AACjB;AAqBO,SAASC,EAAyBC,GAAwD;AAC/F,QAAMC,IAAW,OAAO;AAAA,IACtB,OAAO,QAAQD,KAAU,EAAE,EAAE,OAAO,CAAC,CAAA,EAAGE,CAAK,MAAMA,MAAU,MAAS;AAAA,EAAA;AAExE,SAAO,EAAE,GAAGJ,GAA0B,GAAGG,EAAA;AAC3C;"}
|
|
@@ -108,8 +108,14 @@ export interface CommandPaletteProps {
|
|
|
108
108
|
linkComponent?: ElementType;
|
|
109
109
|
}
|
|
110
110
|
/**
|
|
111
|
-
* The command palette: a search field over grouped results
|
|
112
|
-
*
|
|
111
|
+
* The command palette: a search field over grouped results (B33-495).
|
|
112
|
+
*
|
|
113
|
+
* **It has no trigger in the shell.** The top bar's search button it was drawn
|
|
114
|
+
* to open from was removed on 3 Nov 2026 (B33-1710), and the palette is left out
|
|
115
|
+
* of 0.8.0 entirely: the shell neither renders it nor registers `⌘K`, because a
|
|
116
|
+
* shortcut that opens a Stage A drawing does nothing a person can use. It
|
|
117
|
+
* returns, its top-bar trigger and `⌘K` together as one change, when search
|
|
118
|
+
* ships.
|
|
113
119
|
*
|
|
114
120
|
* **Stage A. Static, and not yet through its fidelity gate.** It holds no state,
|
|
115
121
|
* wires no handler and registers no shortcut. It is not the modal variant yet
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CommandPalette.js","sources":["../../../src/components/CommandPalette/CommandPalette.tsx"],"sourcesContent":["// No \"use client\" directive, and that is a decision rather than an omission.\n//\n// Stage A holds no state, wires no handler and attaches no listener, so there is\n// nothing here a Server Component cannot render -- and tests/guards.test.ts\n// asserts the built package carries the directive on exactly the three modules\n// that need it (B33-846). Adding a fourth now would make that guard say\n// something untrue about this file. `useId` is one of the four hooks React 19's\n// react-server build exports, so the id generation below is server-safe.\n//\n// Stage B changes that: the modal variant of useAnchoredOverlay holds open\n// state, traps focus and marks the background inert, none of which survives on\n// the server. The directive and the guard's expectation move together then.\n\nimport { useId, type ElementType, type ReactNode } from 'react'\nimport { SearchGlyph } from './icons'\nimport { resolveCommandPaletteLabels, type CommandPaletteLabels } from './labels'\nimport styles from './CommandPalette.module.css'\n\n/**\n * One row in the palette. The host owns everything about it: its words, where it\n * goes, and the glyph in its leading slot.\n *\n * There is no `kind` field and there will not be one. The artboard draws three\n * kinds of row -- a page, a product, an action -- but the groups they sit in are\n * the host's and arbitrary, so a `kind` would be this package deciding what a\n * host's group *means*, which is the product meaning CLAUDE.md keeps out. A row\n * that draws a product tile draws it because the host passed\n * `icon={<ProductMark slug=\"boostscore\" />}`, not because the palette recognised\n * anything.\n */\nexport interface CommandPaletteResult {\n /**\n * Stable within one palette. It is the row's React key and, composed with the\n * palette's own `useId` base, the DOM id `aria-activedescendant` points at --\n * so two results sharing an id would leave the highlight ambiguous.\n */\n id: string\n /** The row's visible text, e.g. \"Buy tokens\". The host's, never defaulted. */\n label: string\n /**\n * Where choosing this row goes. A result with an `href` navigates and renders\n * through `linkComponent`; one without is an action and renders as a button.\n *\n * The button has no handler in Stage A. Activation -- Enter in the input, or a\n * click -- is interaction, and interaction is Stage B.\n */\n href?: string\n /**\n * The glyph in the row's 22px leading slot. Supplied by the host, because what\n * a row *is* is the host's business (see the note on this interface).\n *\n * The slot is drawn whether or not a row has one, so labels line up in a\n * single lane down the panel -- the same reason NavItem's trailing slot is\n * fixed-width (CONTEXT.md, \"Trailing slot\").\n */\n icon?: ReactNode\n}\n\n/** One titled run of results -- the artboard's Pages, Products and Actions. */\nexport interface CommandPaletteGroup {\n /** Stable within one palette; the group's React key and its heading's DOM id. */\n id: string\n /**\n * The group's uppercase label, e.g. \"Pages\". The host's words, not this\n * package's: the palette owns the grouping chrome and not the grouping.\n *\n * **Omit it and the group draws its rows with no label at all.** That is the\n * state the artboard's annotation describes -- \"before a search starts, the\n * same list shows Recent pages instead of grouped results, same shell, no\n * group labels needed\" -- expressed as the absence of a heading rather than as\n * a second mode. One prop, not a variant (CLAUDE.md, \"build-once components\").\n */\n heading?: string\n /** The rows, in the order the host wants them read. An empty group is skipped. */\n results: CommandPaletteResult[]\n}\n\nexport interface CommandPaletteProps {\n /**\n * The grouped results, in the order they should appear.\n *\n * **The palette takes these as props and owns no index.** That is the decision\n * recorded on B33-495 on 2026-10-06: Pages, Products and Actions are all the\n * host's, there is no cross-suite search index in any repo, and this component\n * must not imply one. It is scope-agnostic by construction -- four products'\n * worth of results render exactly as one product's do -- so a real index later\n * changes what is passed in and not what is here.\n */\n groups: CommandPaletteGroup[]\n /**\n * What is in the search field. Rendered as the input's initial value.\n *\n * Uncontrolled on purpose for now: a `value` with no `onChange` is a React\n * warning and a field nobody can type in, and the handler is Stage B. The\n * element and its markup do not change when the wiring arrives.\n */\n query?: string\n /**\n * The `id` of the highlighted result, if any.\n *\n * Settable from day one for the same reason `useAnchoredOverlay` takes `open`:\n * it is what lets a specimen route draw the highlighted row statically, which\n * is Stage A rather than wiring (ADR-0009). What *moves* the highlight is\n * Stage B. That the palette has a highlight at all, rather than moving DOM\n * focus between rows, is settled -- ADR-0010 puts it on the combobox pattern\n * with `aria-activedescendant`, and B33-693 is closed.\n *\n * An id matching no result is a supported state and simply draws no highlight.\n */\n activeResultId?: string\n /**\n * Every user-facing string that is not result data. Each field defaults to\n * English, and a field left out -- or supplied as `undefined`, which is what a\n * missed next-intl key looks like -- keeps its default (B33-873).\n */\n labels?: Partial<CommandPaletteLabels>\n /**\n * The element a navigating result renders through. Defaults to a plain `a`; a\n * router-owning consumer passes its own so rows route client-side with the\n * locale prefix intact (ADR-0011, B33-847).\n *\n * Consulted only for the linked form, as ADR-0011 requires: a result with no\n * `href` is an action and stays a `<button>`, and `linkComponent` is ignored\n * on it rather than rendered.\n */\n linkComponent?: ElementType\n}\n\n/**\n * The command palette: a search field over grouped results, opened from the top\n * bar's search button or `⌘K` (B33-495).\n *\n * **Stage A. Static, and not yet through its fidelity gate.** It holds no state,\n * wires no handler and registers no shortcut. It is not the modal variant yet\n * either: ADR-0009 folds this component into the one dismissal contract behind a\n * `modal` flag and it is the only one of the four anchored overlays that sets\n * it, but modality *is* interaction -- the focus trap, the `inert` background\n * and backdrop dismissal all arrive with the wiring.\n *\n * A combobox over a listbox, not a disclosure (ADR-0010): the input carries\n * `role=\"combobox\"` and names the highlighted row through\n * `aria-activedescendant`, the list is a `listbox`, each result an `option` and\n * each group a `group` with its own label. That markup is what the ADR settles;\n * what the arrow keys do inside it is Stage B's to implement.\n */\nexport function CommandPalette({\n groups,\n query,\n activeResultId,\n labels,\n linkComponent,\n}: CommandPaletteProps) {\n const t = resolveCommandPaletteLabels(labels)\n const Link: ElementType = linkComponent ?? 'a'\n\n // One base per mounted palette, so the specimen can stack several on a page\n // without two listboxes sharing an id and leaving aria-controls ambiguous.\n const base = useId()\n const listId = `${base}-results`\n const optionId = (resultId: string) => `${base}-result-${resultId}`\n const headingId = (groupId: string) => `${base}-heading-${groupId}`\n\n // A group with no rows is not drawn -- a heading over nothing is a promise the\n // panel does not keep.\n const populated = groups.filter((group) => group.results.length > 0)\n\n // The highlight is only announced if it names a row that is actually on\n // screen. An `aria-activedescendant` pointing at a missing id is worse than\n // none: it tells a screen reader there is a selection and then gives it\n // nothing to read.\n const highlighted =\n activeResultId !== undefined &&\n populated.some((group) => group.results.some((result) => result.id === activeResultId))\n ? activeResultId\n : undefined\n\n return (\n <div className={styles.panel}>\n <div className={styles.searchRow}>\n <SearchGlyph className={styles.searchGlyph} />\n <input\n type=\"text\"\n className={styles.input}\n // ADR-0010. The input is the combobox; the list it controls is the\n // popup. `aria-expanded` describes that popup and nothing else, which\n // is why it is false when there is no list to point at -- the panel\n // being on screen is a separate fact, and in Stage B it is the dialog\n // that carries it.\n role=\"combobox\"\n aria-label={t.search}\n aria-expanded={populated.length > 0}\n aria-controls={populated.length > 0 ? listId : undefined}\n aria-activedescendant={highlighted === undefined ? undefined : optionId(highlighted)}\n aria-autocomplete=\"list\"\n placeholder={t.placeholder}\n defaultValue={query}\n />\n <span className={styles.dismissKey}>{t.dismissKey}</span>\n </div>\n\n {populated.length > 0 ? (\n <div id={listId} className={styles.results} role=\"listbox\" aria-label={t.results}>\n {populated.map((group) => (\n <div\n key={group.id}\n className={styles.group}\n role=\"group\"\n aria-labelledby={group.heading === undefined ? undefined : headingId(group.id)}\n >\n {group.heading !== undefined && (\n <p id={headingId(group.id)} className={styles.groupHeading}>\n {group.heading}\n </p>\n )}\n {group.results.map((result) => {\n const selected = result.id === highlighted\n // Children of an `option` are presentational in ARIA, so a\n // focusable descendant is a conflict rather than a convenience:\n // the row's own text is what gets announced either way. Taking\n // the row out of the tab sequence is what the role already\n // implies, not an anticipation of traversal -- focus stays in\n // the input under ADR-0010's combobox model.\n const rowProps = {\n id: optionId(result.id),\n className: styles.row,\n role: 'option',\n 'aria-selected': selected,\n tabIndex: -1,\n }\n const body = (\n <>\n <span className={styles.icon}>{result.icon}</span>\n <span className={styles.label}>{result.label}</span>\n </>\n )\n return result.href === undefined ? (\n <button key={result.id} type=\"button\" {...rowProps}>\n {body}\n </button>\n ) : (\n <Link key={result.id} href={result.href} {...rowProps}>\n {body}\n </Link>\n )\n })}\n </div>\n ))}\n </div>\n ) : (\n <p className={styles.empty}>{t.empty}</p>\n )}\n\n <div className={styles.footer}>\n <span className={styles.hints}>{t.footerHints}</span>\n </div>\n </div>\n )\n}\n"],"names":["CommandPalette","groups","query","activeResultId","labels","linkComponent","t","resolveCommandPaletteLabels","Link","base","useId","listId","optionId","resultId","headingId","groupId","populated","group","highlighted","result","jsxs","styles","jsx","SearchGlyph","selected","rowProps","body","Fragment"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CommandPalette.js","sources":["../../../src/components/CommandPalette/CommandPalette.tsx"],"sourcesContent":["// No \"use client\" directive, and that is a decision rather than an omission.\n//\n// Stage A holds no state, wires no handler and attaches no listener, so there is\n// nothing here a Server Component cannot render -- and tests/guards.test.ts\n// asserts the built package carries the directive on exactly the three modules\n// that need it (B33-846). Adding a fourth now would make that guard say\n// something untrue about this file. `useId` is one of the four hooks React 19's\n// react-server build exports, so the id generation below is server-safe.\n//\n// Stage B changes that: the modal variant of useAnchoredOverlay holds open\n// state, traps focus and marks the background inert, none of which survives on\n// the server. The directive and the guard's expectation move together then.\n\nimport { useId, type ElementType, type ReactNode } from 'react'\nimport { SearchGlyph } from './icons'\nimport { resolveCommandPaletteLabels, type CommandPaletteLabels } from './labels'\nimport styles from './CommandPalette.module.css'\n\n/**\n * One row in the palette. The host owns everything about it: its words, where it\n * goes, and the glyph in its leading slot.\n *\n * There is no `kind` field and there will not be one. The artboard draws three\n * kinds of row -- a page, a product, an action -- but the groups they sit in are\n * the host's and arbitrary, so a `kind` would be this package deciding what a\n * host's group *means*, which is the product meaning CLAUDE.md keeps out. A row\n * that draws a product tile draws it because the host passed\n * `icon={<ProductMark slug=\"boostscore\" />}`, not because the palette recognised\n * anything.\n */\nexport interface CommandPaletteResult {\n /**\n * Stable within one palette. It is the row's React key and, composed with the\n * palette's own `useId` base, the DOM id `aria-activedescendant` points at --\n * so two results sharing an id would leave the highlight ambiguous.\n */\n id: string\n /** The row's visible text, e.g. \"Buy tokens\". The host's, never defaulted. */\n label: string\n /**\n * Where choosing this row goes. A result with an `href` navigates and renders\n * through `linkComponent`; one without is an action and renders as a button.\n *\n * The button has no handler in Stage A. Activation -- Enter in the input, or a\n * click -- is interaction, and interaction is Stage B.\n */\n href?: string\n /**\n * The glyph in the row's 22px leading slot. Supplied by the host, because what\n * a row *is* is the host's business (see the note on this interface).\n *\n * The slot is drawn whether or not a row has one, so labels line up in a\n * single lane down the panel -- the same reason NavItem's trailing slot is\n * fixed-width (CONTEXT.md, \"Trailing slot\").\n */\n icon?: ReactNode\n}\n\n/** One titled run of results -- the artboard's Pages, Products and Actions. */\nexport interface CommandPaletteGroup {\n /** Stable within one palette; the group's React key and its heading's DOM id. */\n id: string\n /**\n * The group's uppercase label, e.g. \"Pages\". The host's words, not this\n * package's: the palette owns the grouping chrome and not the grouping.\n *\n * **Omit it and the group draws its rows with no label at all.** That is the\n * state the artboard's annotation describes -- \"before a search starts, the\n * same list shows Recent pages instead of grouped results, same shell, no\n * group labels needed\" -- expressed as the absence of a heading rather than as\n * a second mode. One prop, not a variant (CLAUDE.md, \"build-once components\").\n */\n heading?: string\n /** The rows, in the order the host wants them read. An empty group is skipped. */\n results: CommandPaletteResult[]\n}\n\nexport interface CommandPaletteProps {\n /**\n * The grouped results, in the order they should appear.\n *\n * **The palette takes these as props and owns no index.** That is the decision\n * recorded on B33-495 on 2026-10-06: Pages, Products and Actions are all the\n * host's, there is no cross-suite search index in any repo, and this component\n * must not imply one. It is scope-agnostic by construction -- four products'\n * worth of results render exactly as one product's do -- so a real index later\n * changes what is passed in and not what is here.\n */\n groups: CommandPaletteGroup[]\n /**\n * What is in the search field. Rendered as the input's initial value.\n *\n * Uncontrolled on purpose for now: a `value` with no `onChange` is a React\n * warning and a field nobody can type in, and the handler is Stage B. The\n * element and its markup do not change when the wiring arrives.\n */\n query?: string\n /**\n * The `id` of the highlighted result, if any.\n *\n * Settable from day one for the same reason `useAnchoredOverlay` takes `open`:\n * it is what lets a specimen route draw the highlighted row statically, which\n * is Stage A rather than wiring (ADR-0009). What *moves* the highlight is\n * Stage B. That the palette has a highlight at all, rather than moving DOM\n * focus between rows, is settled -- ADR-0010 puts it on the combobox pattern\n * with `aria-activedescendant`, and B33-693 is closed.\n *\n * An id matching no result is a supported state and simply draws no highlight.\n */\n activeResultId?: string\n /**\n * Every user-facing string that is not result data. Each field defaults to\n * English, and a field left out -- or supplied as `undefined`, which is what a\n * missed next-intl key looks like -- keeps its default (B33-873).\n */\n labels?: Partial<CommandPaletteLabels>\n /**\n * The element a navigating result renders through. Defaults to a plain `a`; a\n * router-owning consumer passes its own so rows route client-side with the\n * locale prefix intact (ADR-0011, B33-847).\n *\n * Consulted only for the linked form, as ADR-0011 requires: a result with no\n * `href` is an action and stays a `<button>`, and `linkComponent` is ignored\n * on it rather than rendered.\n */\n linkComponent?: ElementType\n}\n\n/**\n * The command palette: a search field over grouped results (B33-495).\n *\n * **It has no trigger in the shell.** The top bar's search button it was drawn\n * to open from was removed on 3 Nov 2026 (B33-1710), and the palette is left out\n * of 0.8.0 entirely: the shell neither renders it nor registers `⌘K`, because a\n * shortcut that opens a Stage A drawing does nothing a person can use. It\n * returns, its top-bar trigger and `⌘K` together as one change, when search\n * ships.\n *\n * **Stage A. Static, and not yet through its fidelity gate.** It holds no state,\n * wires no handler and registers no shortcut. It is not the modal variant yet\n * either: ADR-0009 folds this component into the one dismissal contract behind a\n * `modal` flag and it is the only one of the four anchored overlays that sets\n * it, but modality *is* interaction -- the focus trap, the `inert` background\n * and backdrop dismissal all arrive with the wiring.\n *\n * A combobox over a listbox, not a disclosure (ADR-0010): the input carries\n * `role=\"combobox\"` and names the highlighted row through\n * `aria-activedescendant`, the list is a `listbox`, each result an `option` and\n * each group a `group` with its own label. That markup is what the ADR settles;\n * what the arrow keys do inside it is Stage B's to implement.\n */\nexport function CommandPalette({\n groups,\n query,\n activeResultId,\n labels,\n linkComponent,\n}: CommandPaletteProps) {\n const t = resolveCommandPaletteLabels(labels)\n const Link: ElementType = linkComponent ?? 'a'\n\n // One base per mounted palette, so the specimen can stack several on a page\n // without two listboxes sharing an id and leaving aria-controls ambiguous.\n const base = useId()\n const listId = `${base}-results`\n const optionId = (resultId: string) => `${base}-result-${resultId}`\n const headingId = (groupId: string) => `${base}-heading-${groupId}`\n\n // A group with no rows is not drawn -- a heading over nothing is a promise the\n // panel does not keep.\n const populated = groups.filter((group) => group.results.length > 0)\n\n // The highlight is only announced if it names a row that is actually on\n // screen. An `aria-activedescendant` pointing at a missing id is worse than\n // none: it tells a screen reader there is a selection and then gives it\n // nothing to read.\n const highlighted =\n activeResultId !== undefined &&\n populated.some((group) => group.results.some((result) => result.id === activeResultId))\n ? activeResultId\n : undefined\n\n return (\n <div className={styles.panel}>\n <div className={styles.searchRow}>\n <SearchGlyph className={styles.searchGlyph} />\n <input\n type=\"text\"\n className={styles.input}\n // ADR-0010. The input is the combobox; the list it controls is the\n // popup. `aria-expanded` describes that popup and nothing else, which\n // is why it is false when there is no list to point at -- the panel\n // being on screen is a separate fact, and in Stage B it is the dialog\n // that carries it.\n role=\"combobox\"\n aria-label={t.search}\n aria-expanded={populated.length > 0}\n aria-controls={populated.length > 0 ? listId : undefined}\n aria-activedescendant={highlighted === undefined ? undefined : optionId(highlighted)}\n aria-autocomplete=\"list\"\n placeholder={t.placeholder}\n defaultValue={query}\n />\n <span className={styles.dismissKey}>{t.dismissKey}</span>\n </div>\n\n {populated.length > 0 ? (\n <div id={listId} className={styles.results} role=\"listbox\" aria-label={t.results}>\n {populated.map((group) => (\n <div\n key={group.id}\n className={styles.group}\n role=\"group\"\n aria-labelledby={group.heading === undefined ? undefined : headingId(group.id)}\n >\n {group.heading !== undefined && (\n <p id={headingId(group.id)} className={styles.groupHeading}>\n {group.heading}\n </p>\n )}\n {group.results.map((result) => {\n const selected = result.id === highlighted\n // Children of an `option` are presentational in ARIA, so a\n // focusable descendant is a conflict rather than a convenience:\n // the row's own text is what gets announced either way. Taking\n // the row out of the tab sequence is what the role already\n // implies, not an anticipation of traversal -- focus stays in\n // the input under ADR-0010's combobox model.\n const rowProps = {\n id: optionId(result.id),\n className: styles.row,\n role: 'option',\n 'aria-selected': selected,\n tabIndex: -1,\n }\n const body = (\n <>\n <span className={styles.icon}>{result.icon}</span>\n <span className={styles.label}>{result.label}</span>\n </>\n )\n return result.href === undefined ? (\n <button key={result.id} type=\"button\" {...rowProps}>\n {body}\n </button>\n ) : (\n <Link key={result.id} href={result.href} {...rowProps}>\n {body}\n </Link>\n )\n })}\n </div>\n ))}\n </div>\n ) : (\n <p className={styles.empty}>{t.empty}</p>\n )}\n\n <div className={styles.footer}>\n <span className={styles.hints}>{t.footerHints}</span>\n </div>\n </div>\n )\n}\n"],"names":["CommandPalette","groups","query","activeResultId","labels","linkComponent","t","resolveCommandPaletteLabels","Link","base","useId","listId","optionId","resultId","headingId","groupId","populated","group","highlighted","result","jsxs","styles","jsx","SearchGlyph","selected","rowProps","body","Fragment"],"mappings":"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"}
|
|
@@ -14,8 +14,21 @@ export interface NavItemProps {
|
|
|
14
14
|
collapsed?: boolean;
|
|
15
15
|
/** Trailing slot content. Omit to leave the slot empty -- it still reserves its width so counts line up across rows. */
|
|
16
16
|
count?: number;
|
|
17
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* Draws `count` as a filled pill rather than muted text -- the "needs you"
|
|
19
|
+
* count (board 20 of the 3 Nov 2026 redesign, B33-1711). Until 0.8 it drew the
|
|
20
|
+
* count in --color-primary text, which was 3.45:1 on the grey sidebar and
|
|
21
|
+
* failed AA at the row's size; the pill is white on --color-orange-600, 5.18:1.
|
|
22
|
+
*/
|
|
18
23
|
countUrgent?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* A 7px dot in the trailing slot, saying the row needs attention -- an
|
|
26
|
+
* unfinished profile, say (board 20, B33-1711). The dot is colour alone, so
|
|
27
|
+
* the string is what a screen reader hears in its place, in the consumer's
|
|
28
|
+
* language ("Incomplete"). Ignored when `count` is set: the slot holds one
|
|
29
|
+
* thing.
|
|
30
|
+
*/
|
|
31
|
+
dot?: string;
|
|
19
32
|
href?: string;
|
|
20
33
|
/**
|
|
21
34
|
* The element the linked form renders through. Defaults to a plain `a`; a
|
|
@@ -29,4 +42,4 @@ export interface NavItemProps {
|
|
|
29
42
|
linkComponent?: ElementType;
|
|
30
43
|
onClick?: () => void;
|
|
31
44
|
}
|
|
32
|
-
export declare function NavItem({ children, icon, active, collapsed, count, countUrgent, href, linkComponent, onClick }: NavItemProps): import("react").JSX.Element;
|
|
45
|
+
export declare function NavItem({ children, icon, active, collapsed, count, countUrgent, dot, href, linkComponent, onClick }: NavItemProps): import("react").JSX.Element;
|
|
@@ -1,18 +1,21 @@
|
|
|
1
|
-
import { jsx as e, jsxs as
|
|
1
|
+
import { jsx as e, jsxs as d, Fragment as m } from "react/jsx-runtime";
|
|
2
2
|
import a from "./NavItem.module.css.js";
|
|
3
|
-
function
|
|
4
|
-
const
|
|
5
|
-
"data-active":
|
|
6
|
-
"data-collapsed":
|
|
7
|
-
"aria-current":
|
|
8
|
-
},
|
|
3
|
+
function b({ children: u, icon: n, active: s = !1, collapsed: p = !1, count: l, countUrgent: h = !1, dot: r, href: t, linkComponent: N, onClick: c }) {
|
|
4
|
+
const i = {
|
|
5
|
+
"data-active": s ? "true" : void 0,
|
|
6
|
+
"data-collapsed": p ? "true" : void 0,
|
|
7
|
+
"aria-current": s ? "page" : void 0
|
|
8
|
+
}, o = /* @__PURE__ */ d(m, { children: [
|
|
9
9
|
n != null && /* @__PURE__ */ e("span", { className: a.icon, "aria-hidden": "true", children: n }),
|
|
10
|
-
/* @__PURE__ */ e("span", { className: a.label, children:
|
|
11
|
-
/* @__PURE__ */ e("span", { className: a.trailingSlot, children:
|
|
10
|
+
/* @__PURE__ */ e("span", { className: a.label, children: u }),
|
|
11
|
+
/* @__PURE__ */ e("span", { className: a.trailingSlot, children: l != null ? /* @__PURE__ */ e("span", { className: h ? a.countUrgent : a.count, children: l }) : r != null && /* @__PURE__ */ d(m, { children: [
|
|
12
|
+
/* @__PURE__ */ e("span", { className: a.dot, "aria-hidden": "true" }),
|
|
13
|
+
/* @__PURE__ */ e("span", { className: a.srOnly, children: r })
|
|
14
|
+
] }) })
|
|
12
15
|
] });
|
|
13
|
-
return
|
|
16
|
+
return t ? /* @__PURE__ */ e(N ?? "a", { className: a.row, href: t, onClick: c, ...i, children: o }) : /* @__PURE__ */ e("button", { type: "button", className: a.row, onClick: c, ...i, children: o });
|
|
14
17
|
}
|
|
15
18
|
export {
|
|
16
|
-
|
|
19
|
+
b as NavItem
|
|
17
20
|
};
|
|
18
21
|
//# sourceMappingURL=NavItem.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NavItem.js","sources":["../../../src/components/NavItem/NavItem.tsx"],"sourcesContent":["import type { ElementType, ReactNode } from 'react'\nimport styles from './NavItem.module.css'\n\nexport interface NavItemProps {\n /** The nav label. Truncates with an ellipsis rather than wrapping. */\n children: ReactNode\n /**\n * The leading icon. Decorative -- the label carries the name, so pass an\n * `aria-hidden` glyph. On the rail it is the only thing that shows, the\n * label having collapsed to the accessibility tree.\n */\n icon?: ReactNode\n /** Marks this row as the current page. Sets `aria-current=\"page\"`. */\n active?: boolean\n /** Sidebar-on-the-rail state. Hides the label and trailing slot visually; the label stays in the accessibility tree. */\n collapsed?: boolean\n /** Trailing slot content. Omit to leave the slot empty -- it still reserves its width so counts line up across rows. */\n count?: number\n
|
|
1
|
+
{"version":3,"file":"NavItem.js","sources":["../../../src/components/NavItem/NavItem.tsx"],"sourcesContent":["import type { ElementType, ReactNode } from 'react'\nimport styles from './NavItem.module.css'\n\nexport interface NavItemProps {\n /** The nav label. Truncates with an ellipsis rather than wrapping. */\n children: ReactNode\n /**\n * The leading icon. Decorative -- the label carries the name, so pass an\n * `aria-hidden` glyph. On the rail it is the only thing that shows, the\n * label having collapsed to the accessibility tree.\n */\n icon?: ReactNode\n /** Marks this row as the current page. Sets `aria-current=\"page\"`. */\n active?: boolean\n /** Sidebar-on-the-rail state. Hides the label and trailing slot visually; the label stays in the accessibility tree. */\n collapsed?: boolean\n /** Trailing slot content. Omit to leave the slot empty -- it still reserves its width so counts line up across rows. */\n count?: number\n /**\n * Draws `count` as a filled pill rather than muted text -- the \"needs you\"\n * count (board 20 of the 3 Nov 2026 redesign, B33-1711). Until 0.8 it drew the\n * count in --color-primary text, which was 3.45:1 on the grey sidebar and\n * failed AA at the row's size; the pill is white on --color-orange-600, 5.18:1.\n */\n countUrgent?: boolean\n /**\n * A 7px dot in the trailing slot, saying the row needs attention -- an\n * unfinished profile, say (board 20, B33-1711). The dot is colour alone, so\n * the string is what a screen reader hears in its place, in the consumer's\n * language (\"Incomplete\"). Ignored when `count` is set: the slot holds one\n * thing.\n */\n dot?: string\n href?: string\n /**\n * The element the linked form renders through. Defaults to a plain `a`; a\n * consumer whose router owns navigation (Next.js, next-intl's localised\n * `Link`) passes its own so the row routes client-side with the locale prefix\n * intact instead of a bare anchor that full-page-reloads (B33-847). Receives\n * `href`, `className`, `onClick` and the row's data/aria attributes unchanged,\n * so the markup is identical to the anchor's. Only used when `href` is set;\n * the action form stays a `button`. See docs/adr/0011.\n */\n linkComponent?: ElementType\n onClick?: () => void\n}\n\nexport function NavItem({ children, icon, active = false, collapsed = false, count, countUrgent = false, dot, href, linkComponent, onClick }: NavItemProps) {\n const dataProps = {\n 'data-active': active ? ('true' as const) : undefined,\n 'data-collapsed': collapsed ? ('true' as const) : undefined,\n 'aria-current': active ? ('page' as const) : undefined,\n }\n\n const content = (\n <>\n {icon != null && (\n <span className={styles.icon} aria-hidden=\"true\">\n {icon}\n </span>\n )}\n <span className={styles.label}>{children}</span>\n <span className={styles.trailingSlot}>\n {count != null ? (\n <span className={countUrgent ? styles.countUrgent : styles.count}>{count}</span>\n ) : (\n dot != null && (\n <>\n <span className={styles.dot} aria-hidden=\"true\" />\n <span className={styles.srOnly}>{dot}</span>\n </>\n )\n )}\n </span>\n </>\n )\n\n if (href) {\n const Link = linkComponent ?? 'a'\n return (\n <Link className={styles.row} href={href} onClick={onClick} {...dataProps}>\n {content}\n </Link>\n )\n }\n\n return (\n <button type=\"button\" className={styles.row} onClick={onClick} {...dataProps}>\n {content}\n </button>\n )\n}\n"],"names":["NavItem","children","icon","active","collapsed","count","countUrgent","dot","href","linkComponent","onClick","dataProps","content","jsxs","Fragment","styles","jsx"],"mappings":";;AA+CO,SAASA,EAAQ,EAAE,UAAAC,GAAU,MAAAC,GAAM,QAAAC,IAAS,IAAO,WAAAC,IAAY,IAAO,OAAAC,GAAO,aAAAC,IAAc,IAAO,KAAAC,GAAK,MAAAC,GAAM,eAAAC,GAAe,SAAAC,KAAyB;AAC1J,QAAMC,IAAY;AAAA,IAChB,eAAeR,IAAU,SAAmB;AAAA,IAC5C,kBAAkBC,IAAa,SAAmB;AAAA,IAClD,gBAAgBD,IAAU,SAAmB;AAAA,EAAA,GAGzCS,IACJ,gBAAAC,EAAAC,GAAA,EACG,UAAA;AAAA,IAAAZ,KAAQ,0BACN,QAAA,EAAK,WAAWa,EAAO,MAAM,eAAY,QACvC,UAAAb,EAAA,CACH;AAAA,IAEF,gBAAAc,EAAC,QAAA,EAAK,WAAWD,EAAO,OAAQ,UAAAd,GAAS;AAAA,sBACxC,QAAA,EAAK,WAAWc,EAAO,cACrB,UAAAV,KAAS,OACR,gBAAAW,EAAC,QAAA,EAAK,WAAWV,IAAcS,EAAO,cAAcA,EAAO,OAAQ,aAAM,IAEzER,KAAO,QACL,gBAAAM,EAAAC,GAAA,EACE,UAAA;AAAA,MAAA,gBAAAE,EAAC,QAAA,EAAK,WAAWD,EAAO,KAAK,eAAY,QAAO;AAAA,MAChD,gBAAAC,EAAC,QAAA,EAAK,WAAWD,EAAO,QAAS,UAAAR,EAAA,CAAI;AAAA,IAAA,EAAA,CACvC,EAAA,CAGN;AAAA,EAAA,GACF;AAGF,SAAIC,IAGA,gBAAAQ,EAFWP,KAAiB,OAEtB,WAAWM,EAAO,KAAK,MAAAP,GAAY,SAAAE,GAAmB,GAAGC,GAC5D,UAAAC,EAAA,CACH,IAKF,gBAAAI,EAAC,UAAA,EAAO,MAAK,UAAS,WAAWD,EAAO,KAAK,SAAAL,GAAmB,GAAGC,GAChE,UAAAC,EAAA,CACH;AAEJ;"}
|
|
@@ -1,18 +1,22 @@
|
|
|
1
|
-
const t = "
|
|
1
|
+
const t = "_row_1vu2t_4", o = "_icon_1vu2t_41", n = "_label_1vu2t_66", _ = "_trailingSlot_1vu2t_106", c = "_count_1vu2t_122", l = "_countUrgent_1vu2t_134", s = "_dot_1vu2t_152", u = "_srOnly_1vu2t_151", r = {
|
|
2
2
|
row: t,
|
|
3
3
|
icon: o,
|
|
4
4
|
label: n,
|
|
5
5
|
trailingSlot: _,
|
|
6
6
|
count: c,
|
|
7
|
-
countUrgent: l
|
|
7
|
+
countUrgent: l,
|
|
8
|
+
dot: s,
|
|
9
|
+
srOnly: u
|
|
8
10
|
};
|
|
9
11
|
export {
|
|
10
12
|
c as count,
|
|
11
13
|
l as countUrgent,
|
|
12
14
|
r as default,
|
|
15
|
+
s as dot,
|
|
13
16
|
o as icon,
|
|
14
17
|
n as label,
|
|
15
18
|
t as row,
|
|
19
|
+
u as srOnly,
|
|
16
20
|
_ as trailingSlot
|
|
17
21
|
};
|
|
18
22
|
//# sourceMappingURL=NavItem.module.css.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NavItem.module.css.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"NavItem.module.css.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;"}
|