@boost33/ui 0.8.0-next.3 → 0.8.0-next.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +74 -0
- package/README.md +49 -0
- package/dist/buttons.module.css.js +4 -4
- package/dist/components/AppShell/AppShell.d.ts +20 -6
- package/dist/components/AppShell/AppShell.js +52 -50
- package/dist/components/AppShell/AppShell.js.map +1 -1
- package/dist/components/AppShell/AppShell.module.css.js +21 -21
- package/dist/components/AppSwitcher/AppSwitcher.d.ts +6 -0
- package/dist/components/AppSwitcher/AppSwitcher.js.map +1 -1
- package/dist/components/NavItem/NavItem.d.ts +18 -1
- package/dist/components/NavItem/NavItem.js +17 -13
- package/dist/components/NavItem/NavItem.js.map +1 -1
- package/dist/components/NotificationButton/NotificationButton.d.ts +26 -2
- package/dist/components/NotificationButton/NotificationButton.js +27 -15
- package/dist/components/NotificationButton/NotificationButton.js.map +1 -1
- package/dist/components/NotificationButton/labels.d.ts +19 -2
- package/dist/components/NotificationButton/labels.js +11 -9
- package/dist/components/NotificationButton/labels.js.map +1 -1
- package/dist/components/TopBar/TopBar.module.css.js +8 -8
- package/dist/components/WalletChip/WalletChip.d.ts +6 -0
- package/dist/components/WalletChip/WalletChip.js +125 -104
- package/dist/components/WalletChip/WalletChip.js.map +1 -1
- package/dist/components/WalletChip/WalletChip.module.css.js +81 -81
- package/dist/components/WalletChip/format.d.ts +2 -2
- package/dist/components/WalletChip/format.js +23 -20
- package/dist/components/WalletChip/format.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/style.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 { 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
|
+
{"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 * **Pass it from a client component only.** This is a `'use client'` module,\n * and a Server Component cannot hand a function across that boundary: Next\n * throws and the render 500s (Passport). Render this component inside a\n * `'use client'` file of your own and pass that into AppShell's slot. See the\n * README, \"Server Components\".\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,4 +1,6 @@
|
|
|
1
1
|
import { ElementType, ReactNode } from 'react';
|
|
2
|
+
/** Pass-through attributes for a nav row: `data-*` only. */
|
|
3
|
+
export type NavItemAttrs = Record<`data-${string}`, string>;
|
|
2
4
|
export interface NavItemProps {
|
|
3
5
|
/** The nav label. Truncates with an ellipsis rather than wrapping. */
|
|
4
6
|
children: ReactNode;
|
|
@@ -29,6 +31,21 @@ export interface NavItemProps {
|
|
|
29
31
|
* thing.
|
|
30
32
|
*/
|
|
31
33
|
dot?: string;
|
|
34
|
+
/**
|
|
35
|
+
* What a screen reader hears for `count`, in the consumer's words: "3 to
|
|
36
|
+
* open", "3 à ouvrir" (0.8.0-next.5, Off Market). The bare number on its own
|
|
37
|
+
* says nothing about what is counted. Given, the visible figure is hidden from
|
|
38
|
+
* assistive tech and this is read in its place; omitted, the number is read as
|
|
39
|
+
* before. Ignored without `count`.
|
|
40
|
+
*/
|
|
41
|
+
countLabel?: string;
|
|
42
|
+
/**
|
|
43
|
+
* `data-*` attributes for the row's element, such as a product tour's hook
|
|
44
|
+
* (`{ 'data-tour': 'nav-accounts' }`, Outbound's driver.js). Only `data-*`, so
|
|
45
|
+
* nothing here can change the row's role, name or behaviour, and the row's
|
|
46
|
+
* own `data-active` and `data-collapsed` always win (0.8.0-next.5).
|
|
47
|
+
*/
|
|
48
|
+
attrs?: NavItemAttrs;
|
|
32
49
|
href?: string;
|
|
33
50
|
/**
|
|
34
51
|
* The element the linked form renders through. Defaults to a plain `a`; a
|
|
@@ -42,4 +59,4 @@ export interface NavItemProps {
|
|
|
42
59
|
linkComponent?: ElementType;
|
|
43
60
|
onClick?: () => void;
|
|
44
61
|
}
|
|
45
|
-
export declare function NavItem({ children, icon, active, collapsed, count, countUrgent, dot, href, linkComponent, onClick }: NavItemProps): import("react").JSX.Element;
|
|
62
|
+
export declare function NavItem({ children, icon, active, collapsed, count, countUrgent, countLabel, dot, attrs, href, linkComponent, onClick }: NavItemProps): import("react").JSX.Element;
|
|
@@ -1,19 +1,23 @@
|
|
|
1
|
-
import { jsx as
|
|
1
|
+
import { jsx as s, jsxs as n, Fragment as r } from "react/jsx-runtime";
|
|
2
2
|
import a from "./NavItem.module.css.js";
|
|
3
|
-
function b({ children:
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
"data-
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
/* @__PURE__ */
|
|
11
|
-
/* @__PURE__ */
|
|
12
|
-
|
|
13
|
-
/* @__PURE__ */
|
|
3
|
+
function b({ children: p, icon: t, active: l = !1, collapsed: N = !1, count: i, countUrgent: f = !1, countLabel: e, dot: c, attrs: g, href: d, linkComponent: v, onClick: o }) {
|
|
4
|
+
const h = {
|
|
5
|
+
...Object.fromEntries(Object.entries(g ?? {}).filter(([m]) => m.startsWith("data-"))),
|
|
6
|
+
"data-active": l ? "true" : void 0,
|
|
7
|
+
"data-collapsed": N ? "true" : void 0,
|
|
8
|
+
"aria-current": l ? "page" : void 0
|
|
9
|
+
}, u = /* @__PURE__ */ n(r, { children: [
|
|
10
|
+
t != null && /* @__PURE__ */ s("span", { className: a.icon, "aria-hidden": "true", children: t }),
|
|
11
|
+
/* @__PURE__ */ s("span", { className: a.label, children: p }),
|
|
12
|
+
/* @__PURE__ */ s("span", { className: a.trailingSlot, children: i != null ? /* @__PURE__ */ n(r, { children: [
|
|
13
|
+
/* @__PURE__ */ s("span", { className: f ? a.countUrgent : a.count, "aria-hidden": e != null ? "true" : void 0, children: i }),
|
|
14
|
+
e != null && /* @__PURE__ */ s("span", { className: a.srOnly, children: e })
|
|
15
|
+
] }) : c != null && /* @__PURE__ */ n(r, { children: [
|
|
16
|
+
/* @__PURE__ */ s("span", { className: a.dot, "aria-hidden": "true" }),
|
|
17
|
+
/* @__PURE__ */ s("span", { className: a.srOnly, children: c })
|
|
14
18
|
] }) })
|
|
15
19
|
] });
|
|
16
|
-
return
|
|
20
|
+
return d ? /* @__PURE__ */ s(v ?? "a", { className: a.row, href: d, onClick: o, ...h, children: u }) : /* @__PURE__ */ s("button", { type: "button", className: a.row, onClick: o, ...h, children: u });
|
|
17
21
|
}
|
|
18
22
|
export {
|
|
19
23
|
b as NavItem
|
|
@@ -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 /**\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}
|
|
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\n/** Pass-through attributes for a nav row: `data-*` only. */\nexport type NavItemAttrs = Record<`data-${string}`, string>\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 /**\n * What a screen reader hears for `count`, in the consumer's words: \"3 to\n * open\", \"3 à ouvrir\" (0.8.0-next.5, Off Market). The bare number on its own\n * says nothing about what is counted. Given, the visible figure is hidden from\n * assistive tech and this is read in its place; omitted, the number is read as\n * before. Ignored without `count`.\n */\n countLabel?: string\n /**\n * `data-*` attributes for the row's element, such as a product tour's hook\n * (`{ 'data-tour': 'nav-accounts' }`, Outbound's driver.js). Only `data-*`, so\n * nothing here can change the row's role, name or behaviour, and the row's\n * own `data-active` and `data-collapsed` always win (0.8.0-next.5).\n */\n attrs?: NavItemAttrs\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, countLabel, dot, attrs, href, linkComponent, onClick }: NavItemProps) {\n // The consumer's `data-*` first, so the row's own attributes below override\n // any it repeats. Filtered again at runtime for a caller that is not\n // type-checked.\n const passThrough = Object.fromEntries(Object.entries(attrs ?? {}).filter(([name]) => name.startsWith('data-')))\n const dataProps = {\n ...passThrough,\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 <>\n <span className={countUrgent ? styles.countUrgent : styles.count} aria-hidden={countLabel != null ? 'true' : undefined}>\n {count}\n </span>\n {countLabel != null && <span className={styles.srOnly}>{countLabel}</span>}\n </>\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","countLabel","dot","attrs","href","linkComponent","onClick","dataProps","name","content","jsxs","Fragment","styles","jsx"],"mappings":";;AAiEO,SAASA,EAAQ,EAAE,UAAAC,GAAU,MAAAC,GAAM,QAAAC,IAAS,IAAO,WAAAC,IAAY,IAAO,OAAAC,GAAO,aAAAC,IAAc,IAAO,YAAAC,GAAY,KAAAC,GAAK,OAAAC,GAAO,MAAAC,GAAM,eAAAC,GAAe,SAAAC,KAAyB;AAK7K,QAAMC,IAAY;AAAA,IAChB,GAFkB,OAAO,YAAY,OAAO,QAAQJ,KAAS,CAAA,CAAE,EAAE,OAAO,CAAC,CAACK,CAAI,MAAMA,EAAK,WAAW,OAAO,CAAC,CAAC;AAAA,IAG7G,eAAeX,IAAU,SAAmB;AAAA,IAC5C,kBAAkBC,IAAa,SAAmB;AAAA,IAClD,gBAAgBD,IAAU,SAAmB;AAAA,EAAA,GAGzCY,IACJ,gBAAAC,EAAAC,GAAA,EACG,UAAA;AAAA,IAAAf,KAAQ,0BACN,QAAA,EAAK,WAAWgB,EAAO,MAAM,eAAY,QACvC,UAAAhB,EAAA,CACH;AAAA,IAEF,gBAAAiB,EAAC,QAAA,EAAK,WAAWD,EAAO,OAAQ,UAAAjB,GAAS;AAAA,sBACxC,QAAA,EAAK,WAAWiB,EAAO,cACrB,UAAAb,KAAS,OACR,gBAAAW,EAAAC,GAAA,EACE,UAAA;AAAA,MAAA,gBAAAE,EAAC,QAAA,EAAK,WAAWb,IAAcY,EAAO,cAAcA,EAAO,OAAO,eAAaX,KAAc,OAAO,SAAS,QAC1G,UAAAF,GACH;AAAA,MACCE,KAAc,QAAQ,gBAAAY,EAAC,UAAK,WAAWD,EAAO,QAAS,UAAAX,EAAA,CAAW;AAAA,IAAA,EAAA,CACrE,IAEAC,KAAO,QACL,gBAAAQ,EAAAC,GAAA,EACE,UAAA;AAAA,MAAA,gBAAAE,EAAC,QAAA,EAAK,WAAWD,EAAO,KAAK,eAAY,QAAO;AAAA,MAChD,gBAAAC,EAAC,QAAA,EAAK,WAAWD,EAAO,QAAS,UAAAV,EAAA,CAAI;AAAA,IAAA,EAAA,CACvC,EAAA,CAGN;AAAA,EAAA,GACF;AAGF,SAAIE,IAGA,gBAAAS,EAFWR,KAAiB,OAEtB,WAAWO,EAAO,KAAK,MAAAR,GAAY,SAAAE,GAAmB,GAAGC,GAC5D,UAAAE,EAAA,CACH,IAKF,gBAAAI,EAAC,UAAA,EAAO,MAAK,UAAS,WAAWD,EAAO,KAAK,SAAAN,GAAmB,GAAGC,GAChE,UAAAE,EAAA,CACH;AAEJ;"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ElementType } from 'react';
|
|
1
|
+
import { ElementType, Ref } from 'react';
|
|
2
2
|
import { NotificationButtonLabels } from './labels';
|
|
3
3
|
export interface NotificationButtonProps {
|
|
4
4
|
/**
|
|
@@ -14,6 +14,30 @@ export interface NotificationButtonProps {
|
|
|
14
14
|
linkComponent?: ElementType;
|
|
15
15
|
/** The button's words. Each field defaults to English. */
|
|
16
16
|
labels?: Partial<NotificationButtonLabels>;
|
|
17
|
+
/**
|
|
18
|
+
* How many are unread (0.8.0-next.5, Content). It goes into the accessible
|
|
19
|
+
* name -- "Notifications, 3 unread" through `labels.unreadCount` -- so a
|
|
20
|
+
* screen reader hears how many, not only that there are some. Above 0 it also
|
|
21
|
+
* draws the dot, as `unread` does; 0 draws none. The figure is not drawn:
|
|
22
|
+
* board 20 draws a dot, not a badge, and a visible count wants a board first.
|
|
23
|
+
*/
|
|
24
|
+
unreadCount?: number;
|
|
25
|
+
/**
|
|
26
|
+
* Whether what the button opens is open. Set it when `onClick` toggles a
|
|
27
|
+
* panel, and the button carries `aria-expanded` (the disclosure pattern,
|
|
28
|
+
* ADR-0010). Ignored on the link form: a link navigates, it opens nothing.
|
|
29
|
+
*/
|
|
30
|
+
expanded?: boolean;
|
|
31
|
+
/** The id of the panel the button opens, as `aria-controls`. Button form only, like `expanded`. */
|
|
32
|
+
controls?: string;
|
|
33
|
+
/**
|
|
34
|
+
* The rendered `<button>` or link, for a consumer that anchors a panel to
|
|
35
|
+
* the bell or returns focus to it when the panel closes (0.8.0-next.5,
|
|
36
|
+
* Content). React 19 passes `ref` as an ordinary prop, so this is that and
|
|
37
|
+
* not `forwardRef`. On the link form it reaches whatever `linkComponent`
|
|
38
|
+
* renders, provided that component forwards it (next/link does).
|
|
39
|
+
*/
|
|
40
|
+
ref?: Ref<HTMLButtonElement | HTMLAnchorElement>;
|
|
17
41
|
}
|
|
18
42
|
/**
|
|
19
43
|
* The bell in the top bar, next to the avatar (B33-1712, board 20 of the 3 Nov
|
|
@@ -26,4 +50,4 @@ export interface NotificationButtonProps {
|
|
|
26
50
|
* `href` the bell would be a control that does nothing, so it does not render
|
|
27
51
|
* (the rule B33-1270 set for the wallet's top-up and the switcher's account row).
|
|
28
52
|
*/
|
|
29
|
-
export declare function NotificationButton({ unread, onClick, href, linkComponent, labels }: NotificationButtonProps): import("react").JSX.Element | null;
|
|
53
|
+
export declare function NotificationButton({ unread: unreadProp, unreadCount, onClick, href, linkComponent, labels, expanded, controls, ref }: NotificationButtonProps): import("react").JSX.Element | null;
|
|
@@ -1,23 +1,35 @@
|
|
|
1
|
-
import { jsx as
|
|
2
|
-
import { resolveNotificationButtonLabels as
|
|
1
|
+
import { jsx as n, jsxs as i, Fragment as h } from "react/jsx-runtime";
|
|
2
|
+
import { resolveNotificationButtonLabels as x } from "./labels.js";
|
|
3
3
|
import o from "./NotificationButton.module.css.js";
|
|
4
|
-
function
|
|
5
|
-
const
|
|
6
|
-
if (
|
|
7
|
-
const a = /* @__PURE__ */
|
|
8
|
-
/* @__PURE__ */
|
|
9
|
-
/* @__PURE__ */
|
|
10
|
-
/* @__PURE__ */
|
|
11
|
-
|
|
4
|
+
function y({ unread: l = !1, unreadCount: t, onClick: s, href: r, linkComponent: u, labels: m, expanded: p, controls: v, ref: d }) {
|
|
5
|
+
const a = x(m);
|
|
6
|
+
if (r === void 0 && s === void 0) return null;
|
|
7
|
+
const e = t !== void 0 ? t > 0 : l, f = t !== void 0 && t > 0 ? a.unreadCount(String(t)) : a.unread, c = /* @__PURE__ */ i(h, { children: [
|
|
8
|
+
/* @__PURE__ */ i("svg", { className: o.bell, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
|
|
9
|
+
/* @__PURE__ */ n("path", { d: "M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9" }),
|
|
10
|
+
/* @__PURE__ */ n("path", { d: "M10.3 21a1.94 1.94 0 0 0 3.4 0" }),
|
|
11
|
+
e && /* @__PURE__ */ n("circle", { className: o.dot, cx: "18", cy: "6.5", r: "3", stroke: "none" })
|
|
12
12
|
] }),
|
|
13
|
-
/* @__PURE__ */
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
/* @__PURE__ */ i("span", { className: o.srOnly, children: [
|
|
14
|
+
a.name,
|
|
15
|
+
e && `, ${f}`
|
|
16
16
|
] })
|
|
17
17
|
] });
|
|
18
|
-
return
|
|
18
|
+
return r !== void 0 ? /* @__PURE__ */ n(u ?? "a", { ref: d, className: o.button, href: r, "data-unread": e ? "true" : void 0, children: c }) : /* @__PURE__ */ n(
|
|
19
|
+
"button",
|
|
20
|
+
{
|
|
21
|
+
ref: d,
|
|
22
|
+
type: "button",
|
|
23
|
+
className: o.button,
|
|
24
|
+
onClick: s,
|
|
25
|
+
"aria-expanded": p,
|
|
26
|
+
"aria-controls": v,
|
|
27
|
+
"data-unread": e ? "true" : void 0,
|
|
28
|
+
children: c
|
|
29
|
+
}
|
|
30
|
+
);
|
|
19
31
|
}
|
|
20
32
|
export {
|
|
21
|
-
|
|
33
|
+
y as NotificationButton
|
|
22
34
|
};
|
|
23
35
|
//# sourceMappingURL=NotificationButton.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NotificationButton.js","sources":["../../../src/components/NotificationButton/NotificationButton.tsx"],"sourcesContent":["// Not a client module, on the same footing as NavItem: it holds no state and\n// calls no hook. A consumer passing `onClick` is already a client component; one\n// passing `href` can render this from the server (B33-846).\n\nimport type { ElementType } from 'react'\nimport { resolveNotificationButtonLabels, type NotificationButtonLabels } from './labels'\nimport styles from './NotificationButton.module.css'\n\nexport interface NotificationButtonProps {\n /**\n * Draws the unread dot, and tells a screen reader (`labels.unread`). The\n * consumer decides what counts as unread; the button fetches nothing.\n */\n unread?: boolean\n /** What pressing it does -- opening the product's notifications. Renders a `<button>`. */\n onClick?: () => void\n /** Where it goes, when it navigates rather than opens something. Renders a link. Wins over `onClick`. */\n href?: string\n /** The element the link form renders through (ADR-0011). Defaults to `a`. */\n linkComponent?: ElementType\n /** The button's words. Each field defaults to English. */\n labels?: Partial<NotificationButtonLabels>\n}\n\n/**\n * The bell in the top bar, next to the avatar (B33-1712, board 20 of the 3 Nov\n * 2026 redesign): a 32px square on the muted fill, a 17px bell, and an orange\n * dot when something is unread.\n *\n * It is the button and nothing more. NotificationCenter, the panel it would\n * open, is Stage A and not releasable (B33-494), so this opens whatever the\n * consumer wires to it -- or nothing is drawn at all: with neither `onClick` nor\n * `href` the bell would be a control that does nothing, so it does not render\n * (the rule B33-1270 set for the wallet's top-up and the switcher's account row).\n */\nexport function NotificationButton({ unread = false, onClick, href, linkComponent, labels }: NotificationButtonProps) {\n const t = resolveNotificationButtonLabels(labels)\n if (href === undefined && onClick === undefined) return null\n\n const content = (\n <>\n <svg className={styles.bell} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9\" />\n <path d=\"M10.3 21a1.94 1.94 0 0 0 3.4 0\" />\n {unread && <circle className={styles.dot} cx=\"18\" cy=\"6.5\" r=\"3\" stroke=\"none\" />}\n </svg>\n <span className={styles.srOnly}>\n {t.name}\n {unread && `, ${
|
|
1
|
+
{"version":3,"file":"NotificationButton.js","sources":["../../../src/components/NotificationButton/NotificationButton.tsx"],"sourcesContent":["// Not a client module, on the same footing as NavItem: it holds no state and\n// calls no hook. A consumer passing `onClick` is already a client component; one\n// passing `href` can render this from the server (B33-846).\n\nimport type { ElementType, Ref } from 'react'\nimport { resolveNotificationButtonLabels, type NotificationButtonLabels } from './labels'\nimport styles from './NotificationButton.module.css'\n\nexport interface NotificationButtonProps {\n /**\n * Draws the unread dot, and tells a screen reader (`labels.unread`). The\n * consumer decides what counts as unread; the button fetches nothing.\n */\n unread?: boolean\n /** What pressing it does -- opening the product's notifications. Renders a `<button>`. */\n onClick?: () => void\n /** Where it goes, when it navigates rather than opens something. Renders a link. Wins over `onClick`. */\n href?: string\n /** The element the link form renders through (ADR-0011). Defaults to `a`. */\n linkComponent?: ElementType\n /** The button's words. Each field defaults to English. */\n labels?: Partial<NotificationButtonLabels>\n /**\n * How many are unread (0.8.0-next.5, Content). It goes into the accessible\n * name -- \"Notifications, 3 unread\" through `labels.unreadCount` -- so a\n * screen reader hears how many, not only that there are some. Above 0 it also\n * draws the dot, as `unread` does; 0 draws none. The figure is not drawn:\n * board 20 draws a dot, not a badge, and a visible count wants a board first.\n */\n unreadCount?: number\n /**\n * Whether what the button opens is open. Set it when `onClick` toggles a\n * panel, and the button carries `aria-expanded` (the disclosure pattern,\n * ADR-0010). Ignored on the link form: a link navigates, it opens nothing.\n */\n expanded?: boolean\n /** The id of the panel the button opens, as `aria-controls`. Button form only, like `expanded`. */\n controls?: string\n /**\n * The rendered `<button>` or link, for a consumer that anchors a panel to\n * the bell or returns focus to it when the panel closes (0.8.0-next.5,\n * Content). React 19 passes `ref` as an ordinary prop, so this is that and\n * not `forwardRef`. On the link form it reaches whatever `linkComponent`\n * renders, provided that component forwards it (next/link does).\n */\n ref?: Ref<HTMLButtonElement | HTMLAnchorElement>\n}\n\n/**\n * The bell in the top bar, next to the avatar (B33-1712, board 20 of the 3 Nov\n * 2026 redesign): a 32px square on the muted fill, a 17px bell, and an orange\n * dot when something is unread.\n *\n * It is the button and nothing more. NotificationCenter, the panel it would\n * open, is Stage A and not releasable (B33-494), so this opens whatever the\n * consumer wires to it -- or nothing is drawn at all: with neither `onClick` nor\n * `href` the bell would be a control that does nothing, so it does not render\n * (the rule B33-1270 set for the wallet's top-up and the switcher's account row).\n */\nexport function NotificationButton({ unread: unreadProp = false, unreadCount, onClick, href, linkComponent, labels, expanded, controls, ref }: NotificationButtonProps) {\n const t = resolveNotificationButtonLabels(labels)\n if (href === undefined && onClick === undefined) return null\n // A count wins over the flag when both are given: it is the more precise\n // claim, and the two cannot then disagree about whether to draw the dot.\n const unread = unreadCount !== undefined ? unreadCount > 0 : unreadProp\n const unreadText = unreadCount !== undefined && unreadCount > 0 ? t.unreadCount(String(unreadCount)) : t.unread\n\n const content = (\n <>\n <svg className={styles.bell} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9\" />\n <path d=\"M10.3 21a1.94 1.94 0 0 0 3.4 0\" />\n {unread && <circle className={styles.dot} cx=\"18\" cy=\"6.5\" r=\"3\" stroke=\"none\" />}\n </svg>\n <span className={styles.srOnly}>\n {t.name}\n {unread && `, ${unreadText}`}\n </span>\n </>\n )\n\n if (href !== undefined) {\n const Link = linkComponent ?? 'a'\n return (\n <Link ref={ref} className={styles.button} href={href} data-unread={unread ? 'true' : undefined}>\n {content}\n </Link>\n )\n }\n\n return (\n <button\n ref={ref as Ref<HTMLButtonElement>}\n type=\"button\"\n className={styles.button}\n onClick={onClick}\n aria-expanded={expanded}\n aria-controls={controls}\n data-unread={unread ? 'true' : undefined}\n >\n {content}\n </button>\n )\n}\n"],"names":["NotificationButton","unreadProp","unreadCount","onClick","href","linkComponent","labels","expanded","controls","ref","t","resolveNotificationButtonLabels","unread","unreadText","content","jsxs","Fragment","styles","jsx"],"mappings":";;;AA2DO,SAASA,EAAmB,EAAE,QAAQC,IAAa,IAAO,aAAAC,GAAa,SAAAC,GAAS,MAAAC,GAAM,eAAAC,GAAe,QAAAC,GAAQ,UAAAC,GAAU,UAAAC,GAAU,KAAAC,KAAgC;AACtK,QAAMC,IAAIC,EAAgCL,CAAM;AAChD,MAAIF,MAAS,UAAaD,MAAY,OAAW,QAAO;AAGxD,QAAMS,IAASV,MAAgB,SAAYA,IAAc,IAAID,GACvDY,IAAaX,MAAgB,UAAaA,IAAc,IAAIQ,EAAE,YAAY,OAAOR,CAAW,CAAC,IAAIQ,EAAE,QAEnGI,IACJ,gBAAAC,EAAAC,GAAA,EACE,UAAA;AAAA,IAAA,gBAAAD,EAAC,SAAI,WAAWE,EAAO,MAAM,SAAQ,aAAY,MAAK,QAAO,QAAO,gBAAe,aAAY,KAAI,eAAc,SAAQ,gBAAe,SAAQ,eAAY,QAC1J,UAAA;AAAA,MAAA,gBAAAC,EAAC,QAAA,EAAK,GAAE,4CAAA,CAA4C;AAAA,MACpD,gBAAAA,EAAC,QAAA,EAAK,GAAE,iCAAA,CAAiC;AAAA,MACxCN,KAAU,gBAAAM,EAAC,UAAA,EAAO,WAAWD,EAAO,KAAK,IAAG,MAAK,IAAG,OAAM,GAAE,KAAI,QAAO,OAAA,CAAO;AAAA,IAAA,GACjF;AAAA,IACA,gBAAAF,EAAC,QAAA,EAAK,WAAWE,EAAO,QACrB,UAAA;AAAA,MAAAP,EAAE;AAAA,MACFE,KAAU,KAAKC,CAAU;AAAA,IAAA,EAAA,CAC5B;AAAA,EAAA,GACF;AAGF,SAAIT,MAAS,SAGT,gBAAAc,EAFWb,KAAiB,KAE3B,EAAK,KAAAI,GAAU,WAAWQ,EAAO,QAAQ,MAAAb,GAAY,eAAaQ,IAAS,SAAS,QAClF,UAAAE,EAAA,CACH,IAKF,gBAAAI;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,KAAAT;AAAA,MACA,MAAK;AAAA,MACL,WAAWQ,EAAO;AAAA,MAClB,SAAAd;AAAA,MACA,iBAAeI;AAAA,MACf,iBAAeC;AAAA,MACf,eAAaI,IAAS,SAAS;AAAA,MAE9B,UAAAE;AAAA,IAAA;AAAA,EAAA;AAGP;"}
|
|
@@ -6,7 +6,24 @@ export interface NotificationButtonLabels {
|
|
|
6
6
|
* colour alone. Default "unread".
|
|
7
7
|
*/
|
|
8
8
|
unread: string;
|
|
9
|
+
/**
|
|
10
|
+
* Appended instead of `unread` when the consumer passes `unreadCount`. A
|
|
11
|
+
* template: `%count%` is the number. Default `%count% unread`. Optional, so a
|
|
12
|
+
* catalogue written before 0.8.0-next.5 still type-checks; it falls back to
|
|
13
|
+
* English until translated.
|
|
14
|
+
*/
|
|
15
|
+
unreadCount?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare const defaultNotificationButtonLabels: {
|
|
18
|
+
name: string;
|
|
19
|
+
unread: string;
|
|
20
|
+
unreadCount: string;
|
|
21
|
+
};
|
|
22
|
+
/** The labels as the render consumes them, the template presented as a function. */
|
|
23
|
+
export interface ResolvedNotificationButtonLabels {
|
|
24
|
+
name: string;
|
|
25
|
+
unread: string;
|
|
26
|
+
unreadCount: (count: string) => string;
|
|
9
27
|
}
|
|
10
|
-
export declare const defaultNotificationButtonLabels: NotificationButtonLabels;
|
|
11
28
|
/** Merge partial labels onto the defaults, skipping explicit `undefined`s (B33-873). */
|
|
12
|
-
export declare function resolveNotificationButtonLabels(labels?: Partial<NotificationButtonLabels>):
|
|
29
|
+
export declare function resolveNotificationButtonLabels(labels?: Partial<NotificationButtonLabels>): ResolvedNotificationButtonLabels;
|
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
import { fillTemplate as i } from "../../fill-template.js";
|
|
2
|
+
const r = {
|
|
2
3
|
name: "Notifications",
|
|
3
|
-
unread: "unread"
|
|
4
|
+
unread: "unread",
|
|
5
|
+
unreadCount: "%count% unread"
|
|
4
6
|
};
|
|
5
|
-
function
|
|
6
|
-
const
|
|
7
|
-
Object.entries(
|
|
8
|
-
);
|
|
9
|
-
return { ...
|
|
7
|
+
function a(n) {
|
|
8
|
+
const o = Object.fromEntries(
|
|
9
|
+
Object.entries(n ?? {}).filter(([, t]) => t !== void 0)
|
|
10
|
+
), e = { ...r, ...o };
|
|
11
|
+
return { ...e, unreadCount: (t) => i(e.unreadCount, { count: t }) };
|
|
10
12
|
}
|
|
11
13
|
export {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
r as defaultNotificationButtonLabels,
|
|
15
|
+
a as resolveNotificationButtonLabels
|
|
14
16
|
};
|
|
15
17
|
//# sourceMappingURL=labels.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"labels.js","sources":["../../../src/components/NotificationButton/labels.ts"],"sourcesContent":["// The notification button's words, each a prop with an English default -- the\n// pattern every labels module here follows (B33-849).\n\nexport interface NotificationButtonLabels {\n /** The button's accessible name. It draws no text, so this is its whole name. Default \"Notifications\". */\n name: string\n /**\n * Appended for a screen reader when `unread` is set, because the unread dot is\n * colour alone. Default \"unread\".\n */\n unread: string\n}\n\nexport const defaultNotificationButtonLabels
|
|
1
|
+
{"version":3,"file":"labels.js","sources":["../../../src/components/NotificationButton/labels.ts"],"sourcesContent":["// The notification button's words, each a prop with an English default -- the\n// pattern every labels module here follows (B33-849).\n\nimport { fillTemplate } from '../../fill-template'\n\nexport interface NotificationButtonLabels {\n /** The button's accessible name. It draws no text, so this is its whole name. Default \"Notifications\". */\n name: string\n /**\n * Appended for a screen reader when `unread` is set, because the unread dot is\n * colour alone. Default \"unread\".\n */\n unread: string\n /**\n * Appended instead of `unread` when the consumer passes `unreadCount`. A\n * template: `%count%` is the number. Default `%count% unread`. Optional, so a\n * catalogue written before 0.8.0-next.5 still type-checks; it falls back to\n * English until translated.\n */\n unreadCount?: string\n}\n\nexport const defaultNotificationButtonLabels = {\n name: 'Notifications',\n unread: 'unread',\n unreadCount: '%count% unread',\n} satisfies Required<NotificationButtonLabels>\n\n/** The labels as the render consumes them, the template presented as a function. */\nexport interface ResolvedNotificationButtonLabels {\n name: string\n unread: string\n unreadCount: (count: string) => string\n}\n\n/** Merge partial labels onto the defaults, skipping explicit `undefined`s (B33-873). */\nexport function resolveNotificationButtonLabels(labels?: Partial<NotificationButtonLabels>): ResolvedNotificationButtonLabels {\n const supplied = Object.fromEntries(\n Object.entries(labels ?? {}).filter(([, value]) => value !== undefined),\n ) as Partial<NotificationButtonLabels>\n const merged = { ...defaultNotificationButtonLabels, ...supplied }\n return { ...merged, unreadCount: (count) => fillTemplate(merged.unreadCount, { count }) }\n}\n"],"names":["defaultNotificationButtonLabels","resolveNotificationButtonLabels","labels","supplied","value","merged","count","fillTemplate"],"mappings":";AAsBO,MAAMA,IAAkC;AAAA,EAC7C,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,aAAa;AACf;AAUO,SAASC,EAAgCC,GAA8E;AAC5H,QAAMC,IAAW,OAAO;AAAA,IACtB,OAAO,QAAQD,KAAU,EAAE,EAAE,OAAO,CAAC,CAAA,EAAGE,CAAK,MAAMA,MAAU,MAAS;AAAA,EAAA,GAElEC,IAAS,EAAE,GAAGL,GAAiC,GAAGG,EAAA;AACxD,SAAO,EAAE,GAAGE,GAAQ,aAAa,CAACC,MAAUC,EAAaF,EAAO,aAAa,EAAE,OAAAC,EAAA,CAAO,EAAA;AACxF;"}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
const t = "
|
|
1
|
+
const t = "_bar_1fqrh_9", e = "_start_1fqrh_40", r = "_end_1fqrh_41", _ = "_centre_1fqrh_60", n = "_label_1fqrh_69", s = {
|
|
2
2
|
bar: t,
|
|
3
3
|
start: e,
|
|
4
|
-
end:
|
|
5
|
-
centre:
|
|
6
|
-
label:
|
|
4
|
+
end: r,
|
|
5
|
+
centre: _,
|
|
6
|
+
label: n
|
|
7
7
|
};
|
|
8
8
|
export {
|
|
9
9
|
t as bar,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
_ as centre,
|
|
11
|
+
s as default,
|
|
12
|
+
r as end,
|
|
13
|
+
n as label,
|
|
14
14
|
e as start
|
|
15
15
|
};
|
|
16
16
|
//# sourceMappingURL=TopBar.module.css.js.map
|
|
@@ -27,6 +27,12 @@ interface WalletChipChromeProps {
|
|
|
27
27
|
/**
|
|
28
28
|
* The element the popover's "Open" link renders through, so it routes
|
|
29
29
|
* client-side with the locale prefix intact (ADR-0011). Defaults to `a`.
|
|
30
|
+
*
|
|
31
|
+
* **Pass it from a client component only.** This is a `'use client'` module,
|
|
32
|
+
* and a Server Component cannot hand a function across that boundary: Next
|
|
33
|
+
* throws and the render 500s (Passport). Render this component inside a
|
|
34
|
+
* `'use client'` file of your own and pass that into AppShell's slot. See the
|
|
35
|
+
* README, "Server Components". The same holds for `onTopUp`.
|
|
30
36
|
*/
|
|
31
37
|
linkComponent?: ElementType;
|
|
32
38
|
/** Render the top-bar popover open on first paint -- the specimen's open state. */
|