@boost33/ui 0.8.0-next.0 → 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 +79 -5
- package/dist/components/AppShell/AppShell.d.ts +45 -1
- package/dist/components/AppShell/AppShell.js +49 -46
- package/dist/components/AppShell/AppShell.js.map +1 -1
- package/dist/components/AppShell/AppShell.module.css.js +19 -17
- package/dist/components/AppShell/AppShell.module.css.js.map +1 -1
- package/dist/components/AppSwitcher/AppSwitcher.d.ts +5 -3
- package/dist/components/AppSwitcher/AppSwitcher.js +37 -34
- package/dist/components/AppSwitcher/AppSwitcher.js.map +1 -1
- package/dist/components/AppSwitcher/labels.d.ts +7 -4
- package/dist/components/AppSwitcher/labels.js +3 -4
- package/dist/components/AppSwitcher/labels.js.map +1 -1
- package/dist/components/Sidebar/NavHeightBudgetWarning.js +10 -10
- package/dist/components/Sidebar/NavHeightBudgetWarning.js.map +1 -1
- package/dist/components/TopBar/TopBar.d.ts +3 -2
- package/dist/components/TopBar/TopBar.js +18 -16
- package/dist/components/TopBar/TopBar.js.map +1 -1
- package/dist/components/WalletChip/WalletChip.js +44 -44
- package/dist/components/WalletChip/WalletChip.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 +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 { ProductMark } from '../ProductMark/ProductMark'\nimport { Check, Chevron, ChevronRight, HubMark } from './icons'\nimport { resolveAppSwitcherLabels, type AppSwitcherLabels } from './labels'\nimport styles from './AppSwitcher.module.css'\n\n/**\n * One product the switcher can jump to. It is Identity's registry row, supplied\n * by the consuming app (ADR-0003) -- the switcher fetches nothing and hardcodes\n * no list. Every product that exists is rendered, entitled or not: hiding an\n * unowned one means nobody learns the family exists (B33-479), and it tells a\n * client who could buy a product that the product is not there.\n *\n * A product that does not exist yet is not rendered (B33-1711). Until 0.8 it\n * drew with a \"Soon\" tag; the navigation redesign of 3 Nov 2026 took unbuilt\n * products off the switcher, the same way they came off the hub. See `live`.\n */\nexport interface AppSwitcherProduct {\n /** Identity's product slug, which selects the ProductMark artwork. */\n slug: string\n /** The product's display name, e.g. \"Boost Score\". Identity's, not the slug. */\n name: string\n /** Where selecting this product goes -- the product's own URL, never the hub. */\n href: string\n /**\n * Whether the account holds this product. Defaults to `true`. `false` dims the\n * mark and shows the \"Not on plan\" tag, but the row stays an ordinary focusable\n * link: entitlement is a rendering hint, and the switcher is not the thing\n * deciding what someone may open (ADR-0010, \"Not entitled is not disabled\").\n */\n entitled?: boolean\n /**\n * Whether the product exists yet. Defaults to `true`. `false` leaves the row\n * out of the panel entirely (B33-1711), unless it is the product this shell is\n * rendering for -- the current product is always listed, since you are in it.\n *\n * **Only this hides a row. `entitled: false` never does.** The two answer\n * different questions, and the difference is commercial: a product the account\n * could buy is real, and leaving it out tells a paying client it does not\n * exist. That rule is the hub's as well (boost33-hub CLAUDE.md, `cardState`),\n * and the switcher follows it.\n *\n * Until 0.8 an unbuilt row was drawn with a \"Soon\" tag. The 3 Nov 2026\n * navigation redesign (B33-1710) removed unbuilt products from the switcher,\n * which overrules the Vocabulary document's \"the switcher shows every product\n * regardless of state\"; that document is amended alongside this change.\n *\n * What follows is why `live` is a second field rather than a third value of\n * `entitled`, which still holds.\n *\n * **A different question from `entitled`, which is why it is a second field\n * and not a third value of the first.** `live` is global: the same answer for\n * every account, because either the product has shipped or it has not.\n * `entitled` is per account, read from the grant. Identity serves both for\n * exactly this reason (B33-561, and the `live` note in\n * `boost33-identity/src/lib/productRegistry.ts`).\n *\n * Until 0.4 this interface had nowhere to put it, so both consumers read the\n * field off the registry and dropped it on the floor -- each with a comment\n * saying so and calling it worth a package ticket. The cost was a row that\n * told a client they had not bought Boost Voice, when nobody has: the row read\n * \"Not on plan\" and pointed at a product with no code behind it.\n *\n * Hiding the row is a rendering decision, not an access one. The switcher\n * still does not decide what anyone may open (ADR-0010), and `url` is still\n * served for every slug, because `?app=` has to resolve for a campaign link.\n */\n live?: boolean\n /**\n * The heading of the section this row sits under, in the consumer's language\n * -- \"Your tools\", \"Included with every account\" (B33-1711, board 21). Rows\n * are grouped by this label in the order the labels first appear, and each\n * group gets a header. Leave it off every row and the panel is one plain list\n * with no headers; rows without one, in a panel where others have one, form a\n * headerless group of their own.\n *\n * Data the consumer is served, never decided here: which products are \"your\n * tools\" is the account's grant and Identity's registry, and the package\n * knows neither (CLAUDE.md -- no component knows what a product means).\n */\n group?: string\n /**\n * One thing at the right of the row (B33-1711, board 21) -- a figure such as\n * the Wallet's balance, or a dot saying there is something new. The consumer\n * supplies it already formatted; the switcher fetches nothing and formats\n * nothing. The current product's check takes the same slot, so a trailing on\n * the current row is not drawn.\n */\n trailing?: AppSwitcherTrailing\n}\n\n/**\n * The right-hand slot of a product row, in its two shapes.\n *\n * - `value` -- a short preformatted figure, drawn muted in the mono face:\n * `{ kind: 'value', value: '8,940' }`.\n * - `dot` -- a small primary dot meaning \"something new here\". A dot is a\n * colour and nothing else, so `label` is required: it is what a screen reader\n * hears instead (\"2 new\"), in the consumer's language.\n */\nexport type AppSwitcherTrailing = { kind: 'value'; value: string } | { kind: 'dot'; label: string }\n\n/** The \"all products\" row at the top of the panel. Its blurb and URL are the consumer's. */\nexport interface AppSwitcherHub {\n /** The row's name, e.g. \"Hub\". */\n name: string\n /** The line beneath it, e.g. \"All Boost33 products\". */\n description: string\n /** The hub URL. */\n href: string\n}\n\n/**\n * The account/workspace row that sat at the foot of the panel until 0.8.0.\n *\n * **Not drawn since 0.8.0.** The 3 Nov 2026 redesign (B33-1710, board 21) took\n * the account row off the switcher: the organisation now lives in the top bar's\n * left-hand slot, as the consumer's own pill (B33-1712). The type stays so a\n * consumer's existing `account` object still compiles; nothing reads it.\n *\n * @deprecated Rendered nowhere since 0.8.0 (B33-1711). Stop passing `account`.\n */\nexport interface AppSwitcherAccount {\n /** The organisation's name, e.g. \"Acme Coaching\". */\n name: string\n /** The avatar's initials, e.g. \"AC\". */\n initials: string\n /** The line beneath the name, preformatted, e.g. \"Admin · 2 workspaces\". */\n description: string\n /** Was: chosen -- opens the workspace panel. Never called since 0.8.0. */\n onSelect?: () => void\n}\n\nexport interface AppSwitcherProps {\n /**\n * The products, in the order Identity serves them. Pass the registry whole:\n * the switcher leaves out the rows with `live: false` itself (B33-1711), so a\n * consumer does not filter, and every product that exists is drawn whether\n * the account holds it or not.\n */\n products: AppSwitcherProduct[]\n /**\n * The slug of the product this shell is rendering for. It names the trigger\n * and is the row that carries the check -- the product you are in.\n *\n * A slug matching nothing in `products` is a supported state, not a caller\n * error: the registry is Identity's and can run ahead of this package\n * (ADR-0003). The mark degrades to a bare tile and the trigger falls back to\n * `labels.switchProduct` for its name (B33-919).\n */\n currentSlug: string\n /** The \"all products\" row above the products. */\n hub: AppSwitcherHub\n /**\n * Was the account/workspace row below the products. Optional and unread since\n * 0.8.0, when the redesign took that row off the panel (B33-1711).\n *\n * @deprecated Rendered nowhere since 0.8.0. The organisation goes in the top\n * bar's slot instead (B33-1712).\n */\n account?: AppSwitcherAccount\n /**\n * Every user-facing string that is not row data. Each field defaults to\n * English, and a field left out -- or supplied as `undefined`, which is what\n * a missed next-intl key looks like -- keeps its default (B33-873).\n */\n labels?: Partial<AppSwitcherLabels>\n /**\n * The element the product and hub rows render through. Defaults to a plain\n * `a`; a router-owning consumer passes its own so the rows route client-side\n * with the locale prefix intact (ADR-0011, B33-847).\n */\n linkComponent?: ElementType\n /** The 56px rail. The trigger collapses to the current product's mark alone. */\n collapsed?: boolean\n /** Render the panel open on first paint -- the specimen's Stage-A open state. */\n defaultOpen?: boolean\n /** Every open and close, so a consumer can un-clip an ancestor's overflow while the panel overhangs. */\n onOpenChange?: (open: boolean) => void\n}\n\n/** The panel's product tile, in px: board 21's 28px row tile. */\nconst ROW_MARK_SIZE = 28\n\n/**\n * Consecutive rows that share a `group`, in the order the rows arrive. A run\n * whose rows carry no group has no label and draws no header, so a list with no\n * groups at all is one headerless run -- the plain list.\n *\n * Runs, not buckets: two rows with the same label separated by a different one\n * draw two sections with the same header. The order is Identity's, and\n * regrouping would silently reorder what the consumer was served.\n */\nfunction groupRuns(products: AppSwitcherProduct[]): { label: string | undefined; products: AppSwitcherProduct[] }[] {\n const runs: { label: string | undefined; products: AppSwitcherProduct[] }[] = []\n for (const product of products) {\n const last = runs[runs.length - 1]\n if (last !== undefined && last.label === product.group) last.products.push(product)\n else runs.push({ label: product.group, products: [product] })\n }\n return runs\n}\n\n/** A row's right-hand figure or dot. A dot carries its words for a screen reader. */\nfunction Trailing({ trailing }: { trailing: AppSwitcherTrailing | undefined }) {\n if (trailing === undefined) return null\n if (trailing.kind === 'value') return <span className={styles.trailingValue}>{trailing.value}</span>\n return (\n <>\n <span className={styles.trailingDot} aria-hidden=\"true\" />\n <span className={styles.srOnly}>{trailing.label}</span>\n </>\n )\n}\n\n/**\n * The product switcher. It opens, lists every Boost33 product that exists with\n * the current one checked, and takes you straight into the one you pick --\n * the real switcher the Wallet's chevron-that-opens-nothing never was (B33-479).\n *\n * A disclosure, not a menu (ADR-0010): the panel carries no widget role, the\n * rows are the real links and buttons they already are, and Tab is the\n * document's own tab order -- so tabbing through the products does not close the\n * panel on the first row. Dismissal, focus restoration and trigger ARIA are the\n * shared anchored-overlay contract's (ADR-0009); it never becomes a modal.\n */\nexport function AppSwitcher({\n products,\n currentSlug,\n hub,\n labels,\n linkComponent,\n collapsed = false,\n defaultOpen,\n onOpenChange,\n}: AppSwitcherProps) {\n const overlay = useAnchoredOverlay({ defaultOpen, onOpenChange })\n const panelId = useId()\n const t = resolveAppSwitcherLabels(labels)\n const Link: ElementType = linkComponent ?? 'a'\n const current = products.find((p) => p.slug === currentSlug)\n // Unbuilt products are not drawn (B33-1711) -- except the one you are in.\n // A shell rendering for a product Identity still marks `live: false` is a\n // product in a preview or a soft launch, and a switcher that left out the\n // place you are standing would have nothing to put its check on.\n const listed = products.filter((p) => p.live !== false || p.slug === currentSlug)\n\n // The name the trigger carries, in both its shapes. `current` is `undefined`\n // whenever `currentSlug` matches nothing in the served registry -- a fifth\n // Identity product, or a slug left stale by a rename -- which ADR-0003\n // contemplates and `AppSwitcherProduct.slug` being `string` rather than\n // `ProductSlug` deliberately allows.\n //\n // Degrading the artwork to a bare tile there is right. Degrading the name is\n // not, and that is what happened: the mark is `aria-hidden` unless it is\n // given a title, so a collapsed trigger handed `title={undefined}` had no\n // accessible name at all. A screen reader reached the only control in the\n // 56px rail and announced \"button\" -- no product, no purpose, nothing to act\n // on. Expanded it was the same absence with a visible face: an empty\n // `.triggerName` between a blank tile and a chevron. CLAUDE.md puts labels in\n // the definition of done, so the fallback is a label prop rather than a\n // literal (B33-919, B33-849).\n const triggerName = current?.name ?? t.switchProduct\n\n return (\n <div className={styles.root}>\n <button\n type=\"button\"\n className={collapsed ? styles.railTrigger : styles.trigger}\n onClick={overlay.toggle}\n {...overlay.triggerProps}\n >\n {/* On the rail the mark is the button's only content, so it carries the\n accessible name (title); expanded, the name sits beside it and the\n mark is decorative, or a screen reader says the product twice.\n\n No `size`. ProductMark's default is 20px -- the Wallet's own size at\n its call sites, and the size all three appswitcher-* baselines were\n captured at. This passed 28 expanded and 32 collapsed until B33-479's\n fidelity pass: a mark 8px over budget in every row pushed the account\n row out of the open panel's frame entirely and truncated \"Boost\n Outbound\" to \"Boos...\". The rail is not a larger mark; it is the same\n mark with the label and chevron gone. */}\n <ProductMark slug={currentSlug} title={collapsed ? triggerName : undefined} />\n {!collapsed && (\n <>\n <span className={styles.triggerName}>{triggerName}</span>\n <Chevron className={styles.chevron} />\n </>\n )}\n </button>\n\n {overlay.open && (\n <div {...overlay.panelProps} className={styles.panel} aria-label={t.panel}>\n {/* The way out to every tool, drawn as the panel's one dark row\n (board 21). Its chevron says it leaves this product. */}\n <Link className={styles.hubRow} href={hub.href} onClick={overlay.close}>\n <HubMark className={styles.hubMark} />\n <span className={styles.rowText}>\n <span className={styles.rowName}>{hub.name}</span>\n <span className={styles.rowSub}>{hub.description}</span>\n </span>\n <ChevronRight className={styles.hubChevron} />\n </Link>\n\n {groupRuns(listed).map((run, runIndex) => {\n const headerId = `${panelId}-group-${runIndex}`\n return (\n <div key={runIndex} className={styles.group}>\n {run.label !== undefined && (\n <p id={headerId} className={styles.groupLabel}>\n {run.label}\n </p>\n )}\n <ul className={styles.products} aria-labelledby={run.label !== undefined ? headerId : undefined}>\n {run.products.map((product) => {\n const isCurrent = product.slug === currentSlug\n const notEntitled = product.entitled === false\n // A product that has not shipped never reaches here (see\n // `listed`), so the one tag left is entitlement's. B33-561's\n // problem -- an unbuilt product reading \"Not on plan\" --\n // cannot come back: Identity serves every unbuilt row\n // `entitled: false`, and none of them is drawn.\n const tag = notEntitled ? t.notOnPlan : null\n\n return (\n <li key={product.slug}>\n <Link\n className={styles.productRow}\n href={product.href}\n aria-current={isCurrent ? 'true' : undefined}\n data-current={isCurrent ? 'true' : undefined}\n data-entitled={notEntitled ? 'false' : undefined}\n onClick={overlay.close}\n >\n {/* 28px in the panel (board 21) -- the tile the row is\n built around. The trigger keeps the 20px default. */}\n <ProductMark slug={product.slug} size={ROW_MARK_SIZE} muted={notEntitled} />\n <span className={styles.rowName}>{product.name}</span>\n {tag !== null && <span className={styles.tag}>{tag}</span>}\n <span className={styles.trailing}>\n {isCurrent ? <Check className={styles.check} /> : <Trailing trailing={product.trailing} />}\n </span>\n </Link>\n </li>\n )\n })}\n </ul>\n </div>\n )\n })}\n </div>\n )}\n </div>\n )\n}\n"],"names":["ROW_MARK_SIZE","groupRuns","products","runs","product","last","Trailing","trailing","jsx","styles","jsxs","Fragment","AppSwitcher","currentSlug","hub","labels","linkComponent","collapsed","defaultOpen","onOpenChange","overlay","useAnchoredOverlay","panelId","useId","t","resolveAppSwitcherLabels","Link","current","p","listed","triggerName","ProductMark","Chevron","HubMark","ChevronRight","run","runIndex","headerId","isCurrent","notEntitled","tag","Check"],"mappings":"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"}
|
|
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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"}
|
|
@@ -21,11 +21,14 @@ export interface AppSwitcherLabels {
|
|
|
21
21
|
* It stays in the type rather than going, because a consumer that passes
|
|
22
22
|
* `labels={{ soon: 'Bientôt' }}` as an object literal would otherwise fail its
|
|
23
23
|
* typecheck on an excess property, and that is a breaking change across four
|
|
24
|
-
* products for a word nothing renders.
|
|
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
29
|
* @deprecated Rendered nowhere since 0.8.0 (B33-1711).
|
|
27
30
|
*/
|
|
28
|
-
soon
|
|
31
|
+
soon?: string;
|
|
29
32
|
/**
|
|
30
33
|
* The trigger's name when `currentSlug` matches no product in the supplied
|
|
31
34
|
* list, so there is no product name to put there. Default "Switch product".
|
|
@@ -51,8 +54,8 @@ export interface AppSwitcherLabels {
|
|
|
51
54
|
* `currentSlug` matches nothing, and all three baselines were captured with the
|
|
52
55
|
* Wallet current.
|
|
53
56
|
*
|
|
54
|
-
* `soon` reaches the screen nowhere since 0.8.0 (B33-1711)
|
|
55
|
-
*
|
|
57
|
+
* `soon` reaches the screen nowhere since 0.8.0 (B33-1711) and is optional, so
|
|
58
|
+
* it has no default. See the field.
|
|
56
59
|
*/
|
|
57
60
|
export declare const defaultAppSwitcherLabels: AppSwitcherLabels;
|
|
58
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 * **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. Drop it from your catalogue at leisure.\n *\n * @deprecated Rendered nowhere since 0.8.0 (B33-1711).\n */\n soon
|
|
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;"}
|
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as a } from "react/jsx-runtime";
|
|
3
3
|
import { useRef as s, useEffect as u } from "react";
|
|
4
|
-
import { checkNavHeightBudget as
|
|
5
|
-
function
|
|
4
|
+
import { checkNavHeightBudget as d, measureNavHeight as c, NAV_HEIGHT_BUDGET_PX as l } from "../../nav-height-budget.js";
|
|
5
|
+
function g({ label: r }) {
|
|
6
6
|
const o = s(null);
|
|
7
7
|
return u(() => {
|
|
8
8
|
var i;
|
|
9
9
|
if (process.env.NODE_ENV === "production") return;
|
|
10
|
-
const
|
|
11
|
-
if (!
|
|
12
|
-
const n =
|
|
10
|
+
const e = (i = o.current) == null ? void 0 : i.parentElement;
|
|
11
|
+
if (!e || getComputedStyle(e).display === "none") return;
|
|
12
|
+
const n = e.querySelector(":scope > nav");
|
|
13
13
|
if (!(n instanceof HTMLElement)) {
|
|
14
|
-
|
|
14
|
+
e.querySelector("nav") && console.warn(
|
|
15
15
|
`@boost33/ui Sidebar ("${r}"): the nav is nested inside another element, so it is not the sidebar's scroll region. Make the <nav> a direct child of the Sidebar, or a nav longer than the column will paint past its bottom edge instead of scrolling. See ADR-0005.`
|
|
16
16
|
);
|
|
17
17
|
return;
|
|
18
18
|
}
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
`@boost33/ui Sidebar ("${r}"): the nav needs ${
|
|
19
|
+
const t = d(c(n), n.clientHeight);
|
|
20
|
+
t.fits || console.warn(
|
|
21
|
+
`@boost33/ui Sidebar ("${r}"): the nav needs ${t.usedPx}px and the column has ${t.budgetPx}px for it, so it is ${t.overBy}px over and scrolling. Nothing is lost -- the action and the footer stay pinned -- but the shell's nav budget is about ${l}px at a 900px viewport, and a nav past it is a nav worth cutting. navContentHeight() and navBudgetForViewport(), exported from this package, check a nav's shape before it renders.`
|
|
22
22
|
);
|
|
23
23
|
}), process.env.NODE_ENV === "production" ? null : /* @__PURE__ */ a("span", { ref: o, hidden: !0, "aria-hidden": "true" });
|
|
24
24
|
}
|
|
25
25
|
export {
|
|
26
|
-
|
|
26
|
+
g as NavHeightBudgetWarning
|
|
27
27
|
};
|
|
28
28
|
//# sourceMappingURL=NavHeightBudgetWarning.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NavHeightBudgetWarning.js","sources":["../../../src/components/Sidebar/NavHeightBudgetWarning.tsx"],"sourcesContent":["'use client'\n\nimport { useEffect, useRef } from 'react'\nimport { NAV_HEIGHT_BUDGET_PX, checkNavHeightBudget, measureNavHeight } from '../../nav-height-budget'\n\n/*\n * The development-mode half of the nav budget, split out as the sidebar's one\n * client module.\n *\n * Sidebar.module.css stops an over-long nav from painting past the column, but\n * silently: the consumer sees a scrollbar and never learns their nav is too\n * long. The budget helpers in nav-height-budget.ts were written for exactly\n * that -- Boost Outbound's admin role has exceeded the budget once already --\n * and until this hook nothing in the shipped components ever called them, so a\n * consumer was protected only if they remembered to check by hand.\n *\n * It lives apart from Sidebar so Sidebar itself -- and the whole shell that\n * renders it -- can stay a Server Component (B33-846). A `console.warn` in\n * an effect is the only reason the sidebar column would otherwise be forced onto\n * the client, and a consumer should not ship the shell's static half to the\n * browser to get a development-only warning. So this is the boundary: it carries\n * \"use client\", and renders nothing a consumer sees. In production it is inert\n * and renders null; in development it renders one empty, hidden marker so the\n * effect has a handle on the sidebar element to measure from.\n *\n * `process.env.NODE_ENV` rather than an `import.meta` flag because every\n * bundler a consumer might use replaces that exact expression at build time\n * and drops the branch, which is how React itself ships its own dev warnings.\n *\n * Best effort by design: it re-measures on every render, after paint. Sidebar\n * re-renders when the consumer's stacked contents change, so a nav that grows\n * is re-checked; a nav that only overflows after a font swap or a window resize\n * is not caught, and does not need to be -- the case this exists for is a nav\n * that is too long for the column the moment it is rendered.\n */\nexport function NavHeightBudgetWarning({ label }: { label: string }) {\n // The marker sits as a direct child of the sidebar, so its parent is the\n // column and `:scope > nav` from there finds the same nav the CSS scroll\n // region rule selects.\n const marker = useRef<HTMLSpanElement>(null)\n\n useEffect(() => {\n if (process.env.NODE_ENV === 'production') return\n\n const element = marker.current?.parentElement\n if (!element) return\n\n const nav = element.querySelector(':scope > nav')\n if (!(nav instanceof HTMLElement)) {\n // Only worth saying when there is a nav and it is in the wrong place.\n // A sidebar that holds no nav at all is an ordinary thing to render.\n if (element.querySelector('nav')) {\n console.warn(\n `@boost33/ui Sidebar (\"${label}\"): the nav is nested inside another element, so it is not the sidebar's scroll ` +\n `region. Make the <nav> a direct child of the Sidebar, or a nav longer than the column will paint past its ` +\n `bottom edge instead of scrolling. See ADR-0005.`,\n )\n }\n return\n }\n\n const budget = checkNavHeightBudget(measureNavHeight(nav), nav.clientHeight)\n if (budget.fits) return\n\n console.warn(\n `@boost33/ui Sidebar (\"${label}\"): the nav needs ${budget.usedPx}px and the column has ${budget.budgetPx}px for it, ` +\n `so it is ${budget.overBy}px over and scrolling. Nothing is lost -- the action and the footer stay pinned -- but ` +\n `the shell's nav budget is about ${NAV_HEIGHT_BUDGET_PX}px at a 900px viewport, and a nav past it is a nav worth ` +\n `cutting. navContentHeight() and navBudgetForViewport(), exported from this package, check a nav's shape before ` +\n `it renders.`,\n )\n })\n\n if (process.env.NODE_ENV === 'production') return null\n return <span ref={marker} hidden aria-hidden=\"true\" />\n}\n"],"names":["NavHeightBudgetWarning","label","marker","useRef","useEffect","element","_a","nav","budget","checkNavHeightBudget","measureNavHeight","NAV_HEIGHT_BUDGET_PX"],"mappings":";;;;AAmCO,SAASA,EAAuB,EAAE,OAAAC,KAA4B;AAInE,QAAMC,IAASC,EAAwB,IAAI;
|
|
1
|
+
{"version":3,"file":"NavHeightBudgetWarning.js","sources":["../../../src/components/Sidebar/NavHeightBudgetWarning.tsx"],"sourcesContent":["'use client'\n\nimport { useEffect, useRef } from 'react'\nimport { NAV_HEIGHT_BUDGET_PX, checkNavHeightBudget, measureNavHeight } from '../../nav-height-budget'\n\n/*\n * The development-mode half of the nav budget, split out as the sidebar's one\n * client module.\n *\n * Sidebar.module.css stops an over-long nav from painting past the column, but\n * silently: the consumer sees a scrollbar and never learns their nav is too\n * long. The budget helpers in nav-height-budget.ts were written for exactly\n * that -- Boost Outbound's admin role has exceeded the budget once already --\n * and until this hook nothing in the shipped components ever called them, so a\n * consumer was protected only if they remembered to check by hand.\n *\n * It lives apart from Sidebar so Sidebar itself -- and the whole shell that\n * renders it -- can stay a Server Component (B33-846). A `console.warn` in\n * an effect is the only reason the sidebar column would otherwise be forced onto\n * the client, and a consumer should not ship the shell's static half to the\n * browser to get a development-only warning. So this is the boundary: it carries\n * \"use client\", and renders nothing a consumer sees. In production it is inert\n * and renders null; in development it renders one empty, hidden marker so the\n * effect has a handle on the sidebar element to measure from.\n *\n * `process.env.NODE_ENV` rather than an `import.meta` flag because every\n * bundler a consumer might use replaces that exact expression at build time\n * and drops the branch, which is how React itself ships its own dev warnings.\n *\n * Best effort by design: it re-measures on every render, after paint. Sidebar\n * re-renders when the consumer's stacked contents change, so a nav that grows\n * is re-checked; a nav that only overflows after a font swap or a window resize\n * is not caught, and does not need to be -- the case this exists for is a nav\n * that is too long for the column the moment it is rendered.\n */\nexport function NavHeightBudgetWarning({ label }: { label: string }) {\n // The marker sits as a direct child of the sidebar, so its parent is the\n // column and `:scope > nav` from there finds the same nav the CSS scroll\n // region rule selects.\n const marker = useRef<HTMLSpanElement>(null)\n\n useEffect(() => {\n if (process.env.NODE_ENV === 'production') return\n\n const element = marker.current?.parentElement\n if (!element) return\n // A sidebar that is not drawn has no height to be over. AppShell's\n // `chromeFrom` hides the sidebar itself with `display: none` below a\n // breakpoint, where the nav measures 0px of column and would read as\n // hundreds of pixels over.\n if (getComputedStyle(element).display === 'none') return\n\n const nav = element.querySelector(':scope > nav')\n if (!(nav instanceof HTMLElement)) {\n // Only worth saying when there is a nav and it is in the wrong place.\n // A sidebar that holds no nav at all is an ordinary thing to render.\n if (element.querySelector('nav')) {\n console.warn(\n `@boost33/ui Sidebar (\"${label}\"): the nav is nested inside another element, so it is not the sidebar's scroll ` +\n `region. Make the <nav> a direct child of the Sidebar, or a nav longer than the column will paint past its ` +\n `bottom edge instead of scrolling. See ADR-0005.`,\n )\n }\n return\n }\n\n const budget = checkNavHeightBudget(measureNavHeight(nav), nav.clientHeight)\n if (budget.fits) return\n\n console.warn(\n `@boost33/ui Sidebar (\"${label}\"): the nav needs ${budget.usedPx}px and the column has ${budget.budgetPx}px for it, ` +\n `so it is ${budget.overBy}px over and scrolling. Nothing is lost -- the action and the footer stay pinned -- but ` +\n `the shell's nav budget is about ${NAV_HEIGHT_BUDGET_PX}px at a 900px viewport, and a nav past it is a nav worth ` +\n `cutting. navContentHeight() and navBudgetForViewport(), exported from this package, check a nav's shape before ` +\n `it renders.`,\n )\n })\n\n if (process.env.NODE_ENV === 'production') return null\n return <span ref={marker} hidden aria-hidden=\"true\" />\n}\n"],"names":["NavHeightBudgetWarning","label","marker","useRef","useEffect","element","_a","nav","budget","checkNavHeightBudget","measureNavHeight","NAV_HEIGHT_BUDGET_PX"],"mappings":";;;;AAmCO,SAASA,EAAuB,EAAE,OAAAC,KAA4B;AAInE,QAAMC,IAASC,EAAwB,IAAI;AAuC3C,SArCAC,EAAU,MAAM;;AACd,QAAI,QAAQ,IAAI,aAAa,aAAc;AAE3C,UAAMC,KAAUC,IAAAJ,EAAO,YAAP,gBAAAI,EAAgB;AAMhC,QALI,CAACD,KAKD,iBAAiBA,CAAO,EAAE,YAAY,OAAQ;AAElD,UAAME,IAAMF,EAAQ,cAAc,cAAc;AAChD,QAAI,EAAEE,aAAe,cAAc;AAGjC,MAAIF,EAAQ,cAAc,KAAK,KAC7B,QAAQ;AAAA,QACN,yBAAyBJ,CAAK;AAAA,MAAA;AAKlC;AAAA,IACF;AAEA,UAAMO,IAASC,EAAqBC,EAAiBH,CAAG,GAAGA,EAAI,YAAY;AAC3E,IAAIC,EAAO,QAEX,QAAQ;AAAA,MACN,yBAAyBP,CAAK,qBAAqBO,EAAO,MAAM,yBAAyBA,EAAO,QAAQ,uBAC1FA,EAAO,MAAM,0HACUG,CAAoB;AAAA,IAAA;AAAA,EAI7D,CAAC,GAEG,QAAQ,IAAI,aAAa,eAAqB,yBAC1C,QAAA,EAAK,KAAKT,GAAQ,QAAM,IAAC,eAAY,QAAO;AACtD;"}
|
|
@@ -51,8 +51,9 @@ export interface TopBarProps {
|
|
|
51
51
|
* `PageHeader` names where you are. Optional so an existing call site still
|
|
52
52
|
* compiles.
|
|
53
53
|
*
|
|
54
|
-
* @deprecated Rendered nowhere since 0.8.0 (B33-1712). Stop passing it
|
|
54
|
+
* @deprecated Rendered nowhere since 0.8.0 (B33-1712). Stop passing it; in
|
|
55
|
+
* development, passing it logs a console warning once.
|
|
55
56
|
*/
|
|
56
57
|
breadcrumb?: ReactNode;
|
|
57
58
|
}
|
|
58
|
-
export declare function TopBar({ sidebarToggle, start, switcher, tokens, actions, labels }: TopBarProps): import("react").JSX.Element;
|
|
59
|
+
export declare function TopBar({ sidebarToggle, start, switcher, tokens, actions, labels, breadcrumb }: TopBarProps): import("react").JSX.Element;
|
|
@@ -1,27 +1,29 @@
|
|
|
1
|
-
import { jsxs as
|
|
1
|
+
import { jsxs as r, Fragment as c, jsx as n } from "react/jsx-runtime";
|
|
2
|
+
import { warnDeprecatedProp as u } from "../../deprecated-prop-warning.js";
|
|
2
3
|
import { resolveTopBarLabels as h } from "./labels.js";
|
|
3
4
|
import l from "./TopBar.module.css.js";
|
|
4
|
-
function
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
function T({ sidebarToggle: o, start: i, switcher: e, tokens: a, actions: d, labels: t, breadcrumb: m }) {
|
|
6
|
+
m != null && u("TopBar.breadcrumb");
|
|
7
|
+
const s = h(t), p = e != null || a != null;
|
|
8
|
+
return /* @__PURE__ */ r("div", { className: l.bar, children: [
|
|
9
|
+
/* @__PURE__ */ r("div", { className: l.start, children: [
|
|
10
|
+
o,
|
|
11
|
+
i
|
|
10
12
|
] }),
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
/* @__PURE__ */
|
|
14
|
-
|
|
13
|
+
p && /* @__PURE__ */ r("div", { className: l.centre, children: [
|
|
14
|
+
e != null && /* @__PURE__ */ r(c, { children: [
|
|
15
|
+
/* @__PURE__ */ n("span", { className: l.label, children: s.switchTools }),
|
|
16
|
+
e
|
|
15
17
|
] }),
|
|
16
|
-
|
|
17
|
-
/* @__PURE__ */
|
|
18
|
-
|
|
18
|
+
a != null && /* @__PURE__ */ r(c, { children: [
|
|
19
|
+
/* @__PURE__ */ n("span", { className: l.label, "data-after-switcher": e != null ? "true" : void 0, children: s.checkTokens }),
|
|
20
|
+
a
|
|
19
21
|
] })
|
|
20
22
|
] }),
|
|
21
|
-
/* @__PURE__ */
|
|
23
|
+
/* @__PURE__ */ n("div", { className: l.end, children: d })
|
|
22
24
|
] });
|
|
23
25
|
}
|
|
24
26
|
export {
|
|
25
|
-
|
|
27
|
+
T as TopBar
|
|
26
28
|
};
|
|
27
29
|
//# sourceMappingURL=TopBar.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TopBar.js","sources":["../../../src/components/TopBar/TopBar.tsx"],"sourcesContent":["import type { ReactNode } from 'react'\nimport { resolveTopBarLabels, type TopBarLabels } from './labels'\nimport styles from './TopBar.module.css'\n\nexport interface TopBarProps {\n /**\n * The control that collapses the sidebar to its 56px rail and back, at the\n * far left of the bar, before the mark (B33-1712). A slot: the product's own\n * button, which owns the collapsed state.\n *\n * It has a slot of its own because the bar stopped drawing `breadcrumb` in\n * 0.8.0, and a consumer that kept its toggle inside the breadcrumb -- the\n * Wallet did -- would otherwise lose its only way into and out of the rail on\n * upgrade, silently: the pixel diff passed the bar at 0.19% with the toggle\n * gone. No board draws it; the rail keeps working with no new design.\n */\n sidebarToggle?: ReactNode\n /**\n * The left of the bar: the Boost33 mark and the organisation's pill, as the 3\n * Nov 2026 redesign draws it (B33-1712, board 20). A slot. Inside `AppShell`\n * the shell fills it from its own `mark`, `hubHref` and `org` props, so a\n * consumer of the shell does not set this.\n */\n start?: ReactNode\n /**\n * The app switcher, at the centre of the bar after its \"Switch Tools →\"\n * label. A slot: pass `AppSwitcher`. Omit it and neither it nor its label is\n * drawn. Inside `AppShell`, the shell's `switcher` prop lands here.\n */\n switcher?: ReactNode\n /**\n * The token chip, after the switcher and its \"Check Your Tokens →\" label. A\n * slot: pass `WalletChip` with `placement=\"topbar\"`. Omit it and neither it\n * nor its label is drawn. Inside `AppShell`, the shell's `wallet` prop lands\n * here.\n */\n tokens?: ReactNode\n /**\n * The right of the bar: the notification button and the account avatar\n * (board 20). A slot -- the product's own, or the package's\n * `NotificationButton` beside the consumer's avatar.\n *\n * Search is not here and nothing replaces it: Jamal removed it on 3 Nov 2026\n * until the feature is built (B33-1710), and the CommandPalette that would\n * open from it is left out of 0.8.0 with no trigger and no shortcut.\n */\n actions?: ReactNode\n /** The centre group's two labels. Each field defaults to English. */\n labels?: Partial<TopBarLabels>\n /**\n * Was the breadcrumb on the left of the bar. **Not drawn since 0.8.0**: the\n * redesign puts the mark and the organisation there, and the page's own\n * `PageHeader` names where you are. Optional so an existing call site still\n * compiles.\n *\n * @deprecated Rendered nowhere since 0.8.0 (B33-1712). Stop passing it.\n */\n breadcrumb?: ReactNode\n}\n\n// The bar across the top of every screen (B33-1712). Three regions: the mark\n// and organisation on the left, the switcher and token chip in a centred group,\n// and the product's actions on the right. Left and right take equal shares of\n// what the centre leaves, which is what holds the centre group in the middle of\n// the bar whatever either side holds.\n//\n// A Server Component: no state, no handlers. The two controls in the centre are\n// client components passed in as slots.\nexport function TopBar({ sidebarToggle, start, switcher, tokens, actions, labels }: TopBarProps) {\n const t = resolveTopBarLabels(labels)\n const hasCentre = switcher != null || tokens != null\n\n return (\n <div className={styles.bar}>\n <div className={styles.start}>\n {sidebarToggle}\n {start}\n </div>\n {hasCentre && (\n <div className={styles.centre}>\n {switcher != null && (\n <>\n <span className={styles.label}>{t.switchTools}</span>\n {switcher}\n </>\n )}\n {tokens != null && (\n <>\n {/* The second label keeps 32px clear of the switcher, so each\n label reads as belonging to the control after it, not the one\n before it (board 20). With no switcher there is nothing to\n keep clear of, and the padding goes. */}\n <span className={styles.label} data-after-switcher={switcher != null ? 'true' : undefined}>\n {t.checkTokens}\n </span>\n {tokens}\n </>\n )}\n </div>\n )}\n <div className={styles.end}>{actions}</div>\n </div>\n )\n}\n"],"names":["TopBar","sidebarToggle","start","switcher","tokens","actions","labels","t","resolveTopBarLabels","hasCentre","jsxs","styles","Fragment","jsx"],"mappings":"
|
|
1
|
+
{"version":3,"file":"TopBar.js","sources":["../../../src/components/TopBar/TopBar.tsx"],"sourcesContent":["import type { ReactNode } from 'react'\nimport { warnDeprecatedProp } from '../../deprecated-prop-warning'\nimport { resolveTopBarLabels, type TopBarLabels } from './labels'\nimport styles from './TopBar.module.css'\n\nexport interface TopBarProps {\n /**\n * The control that collapses the sidebar to its 56px rail and back, at the\n * far left of the bar, before the mark (B33-1712). A slot: the product's own\n * button, which owns the collapsed state.\n *\n * It has a slot of its own because the bar stopped drawing `breadcrumb` in\n * 0.8.0, and a consumer that kept its toggle inside the breadcrumb -- the\n * Wallet did -- would otherwise lose its only way into and out of the rail on\n * upgrade, silently: the pixel diff passed the bar at 0.19% with the toggle\n * gone. No board draws it; the rail keeps working with no new design.\n */\n sidebarToggle?: ReactNode\n /**\n * The left of the bar: the Boost33 mark and the organisation's pill, as the 3\n * Nov 2026 redesign draws it (B33-1712, board 20). A slot. Inside `AppShell`\n * the shell fills it from its own `mark`, `hubHref` and `org` props, so a\n * consumer of the shell does not set this.\n */\n start?: ReactNode\n /**\n * The app switcher, at the centre of the bar after its \"Switch Tools →\"\n * label. A slot: pass `AppSwitcher`. Omit it and neither it nor its label is\n * drawn. Inside `AppShell`, the shell's `switcher` prop lands here.\n */\n switcher?: ReactNode\n /**\n * The token chip, after the switcher and its \"Check Your Tokens →\" label. A\n * slot: pass `WalletChip` with `placement=\"topbar\"`. Omit it and neither it\n * nor its label is drawn. Inside `AppShell`, the shell's `wallet` prop lands\n * here.\n */\n tokens?: ReactNode\n /**\n * The right of the bar: the notification button and the account avatar\n * (board 20). A slot -- the product's own, or the package's\n * `NotificationButton` beside the consumer's avatar.\n *\n * Search is not here and nothing replaces it: Jamal removed it on 3 Nov 2026\n * until the feature is built (B33-1710), and the CommandPalette that would\n * open from it is left out of 0.8.0 with no trigger and no shortcut.\n */\n actions?: ReactNode\n /** The centre group's two labels. Each field defaults to English. */\n labels?: Partial<TopBarLabels>\n /**\n * Was the breadcrumb on the left of the bar. **Not drawn since 0.8.0**: the\n * redesign puts the mark and the organisation there, and the page's own\n * `PageHeader` names where you are. Optional so an existing call site still\n * compiles.\n *\n * @deprecated Rendered nowhere since 0.8.0 (B33-1712). Stop passing it; in\n * development, passing it logs a console warning once.\n */\n breadcrumb?: ReactNode\n}\n\n// The bar across the top of every screen (B33-1712). Three regions: the mark\n// and organisation on the left, the switcher and token chip in a centred group,\n// and the product's actions on the right. Left and right take equal shares of\n// what the centre leaves, which is what holds the centre group in the middle of\n// the bar whatever either side holds.\n//\n// A Server Component: no state, no handlers. The two controls in the centre are\n// client components passed in as slots.\nexport function TopBar({ sidebarToggle, start, switcher, tokens, actions, labels, breadcrumb }: TopBarProps) {\n if (breadcrumb != null) warnDeprecatedProp('TopBar.breadcrumb')\n const t = resolveTopBarLabels(labels)\n const hasCentre = switcher != null || tokens != null\n\n return (\n <div className={styles.bar}>\n <div className={styles.start}>\n {sidebarToggle}\n {start}\n </div>\n {hasCentre && (\n <div className={styles.centre}>\n {switcher != null && (\n <>\n <span className={styles.label}>{t.switchTools}</span>\n {switcher}\n </>\n )}\n {tokens != null && (\n <>\n {/* The second label keeps 32px clear of the switcher, so each\n label reads as belonging to the control after it, not the one\n before it (board 20). With no switcher there is nothing to\n keep clear of, and the padding goes. */}\n <span className={styles.label} data-after-switcher={switcher != null ? 'true' : undefined}>\n {t.checkTokens}\n </span>\n {tokens}\n </>\n )}\n </div>\n )}\n <div className={styles.end}>{actions}</div>\n </div>\n )\n}\n"],"names":["TopBar","sidebarToggle","start","switcher","tokens","actions","labels","breadcrumb","warnDeprecatedProp","t","resolveTopBarLabels","hasCentre","jsxs","styles","Fragment","jsx"],"mappings":";;;;AAsEO,SAASA,EAAO,EAAE,eAAAC,GAAe,OAAAC,GAAO,UAAAC,GAAU,QAAAC,GAAQ,SAAAC,GAAS,QAAAC,GAAQ,YAAAC,KAA2B;AAC3G,EAAIA,KAAc,QAAMC,EAAmB,mBAAmB;AAC9D,QAAMC,IAAIC,EAAoBJ,CAAM,GAC9BK,IAAYR,KAAY,QAAQC,KAAU;AAEhD,SACE,gBAAAQ,EAAC,OAAA,EAAI,WAAWC,EAAO,KACrB,UAAA;AAAA,IAAA,gBAAAD,EAAC,OAAA,EAAI,WAAWC,EAAO,OACpB,UAAA;AAAA,MAAAZ;AAAA,MACAC;AAAA,IAAA,GACH;AAAA,IACCS,KACC,gBAAAC,EAAC,OAAA,EAAI,WAAWC,EAAO,QACpB,UAAA;AAAA,MAAAV,KAAY,QACX,gBAAAS,EAAAE,GAAA,EACE,UAAA;AAAA,QAAA,gBAAAC,EAAC,QAAA,EAAK,WAAWF,EAAO,OAAQ,YAAE,aAAY;AAAA,QAC7CV;AAAA,MAAA,GACH;AAAA,MAEDC,KAAU,QACT,gBAAAQ,EAAAE,GAAA,EAKE,UAAA;AAAA,QAAA,gBAAAC,EAAC,QAAA,EAAK,WAAWF,EAAO,OAAO,uBAAqBV,KAAY,OAAO,SAAS,QAC7E,UAAAM,EAAE,YAAA,CACL;AAAA,QACCL;AAAA,MAAA,EAAA,CACH;AAAA,IAAA,GAEJ;AAAA,IAEF,gBAAAW,EAAC,OAAA,EAAI,WAAWF,EAAO,KAAM,UAAAR,EAAA,CAAQ;AAAA,EAAA,GACvC;AAEJ;"}
|
|
@@ -1,36 +1,36 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as a, jsxs as n, Fragment as P } from "react/jsx-runtime";
|
|
3
3
|
import { useAnchoredOverlay as U } from "../../anchored-overlay.js";
|
|
4
|
-
import { Chevron as
|
|
5
|
-
import
|
|
4
|
+
import { Chevron as $ } from "../AppSwitcher/icons.js";
|
|
5
|
+
import y from "../../buttons.module.css.js";
|
|
6
6
|
import e from "./WalletChip.module.css.js";
|
|
7
7
|
import { formatTokens as o, DEFAULT_WALLET_LOCALE as k, thousandsFigure as w } from "./format.js";
|
|
8
|
-
import { WalletMark as x, RetryMark as
|
|
8
|
+
import { WalletMark as x, RetryMark as O } from "./icons.js";
|
|
9
9
|
import { resolveWalletChipLabels as A } from "./labels.js";
|
|
10
10
|
const E = 0.8, G = "var(--spacing-wallet-segment-min)";
|
|
11
11
|
function L(i, l) {
|
|
12
|
-
const
|
|
13
|
-
return i <= 0 ? `${
|
|
12
|
+
const t = l > 0 ? i / l * 100 : 0;
|
|
13
|
+
return i <= 0 ? `${t}%` : `max(${t}%, ${G})`;
|
|
14
14
|
}
|
|
15
|
-
function R({ granted: i, purchased: l, cycleTotal:
|
|
16
|
-
return /* @__PURE__ */ n("div", { className: e.bar, "data-tone":
|
|
17
|
-
/* @__PURE__ */ a("span", { className: e.segmentGranted, "data-testid": "segment-granted", style: { width: L(i,
|
|
18
|
-
/* @__PURE__ */ a("span", { className: e.segmentPurchased, "data-testid": "segment-purchased", style: { width: L(l,
|
|
15
|
+
function R({ granted: i, purchased: l, cycleTotal: t, tone: p, label: r }) {
|
|
16
|
+
return /* @__PURE__ */ n("div", { className: e.bar, "data-tone": p, role: "img", "aria-label": r, children: [
|
|
17
|
+
/* @__PURE__ */ a("span", { className: e.segmentGranted, "data-testid": "segment-granted", style: { width: L(i, t) } }),
|
|
18
|
+
/* @__PURE__ */ a("span", { className: e.segmentPurchased, "data-testid": "segment-purchased", style: { width: L(l, t) } })
|
|
19
19
|
] });
|
|
20
20
|
}
|
|
21
21
|
function J(i) {
|
|
22
22
|
return i.totalOnly === !0 ? /* @__PURE__ */ a(W, { ...i }) : i.placement === "topbar" ? /* @__PURE__ */ a(M, { ...i }) : /* @__PURE__ */ a(I, { ...i });
|
|
23
23
|
}
|
|
24
|
-
function M({ granted: i = 0, purchased: l = 0, grantExpires:
|
|
25
|
-
const s = A(
|
|
24
|
+
function M({ granted: i = 0, purchased: l = 0, grantExpires: t, burnNote: p, unknown: r = !1, onTopUp: v, labels: m, locale: u = k, walletHref: c, linkComponent: g, defaultOpen: h }) {
|
|
25
|
+
const s = A(m), d = U({ defaultOpen: h }), B = g ?? "a", b = i + l, F = b, f = r ? "—" : o(b, u), N = () => s.bar(o(i, u), o(l, u), o(b, u));
|
|
26
26
|
return /* @__PURE__ */ n("div", { className: e.topRoot, children: [
|
|
27
|
-
/* @__PURE__ */ n("button", { type: "button", className: e.topTrigger, "aria-label":
|
|
27
|
+
/* @__PURE__ */ n("button", { type: "button", className: e.topTrigger, "aria-label": r ? s.railBalanceUnknown : s.railBalance(f), onClick: d.toggle, ...d.triggerProps, children: [
|
|
28
28
|
/* @__PURE__ */ n("span", { className: e.topFigure, "aria-hidden": "true", children: [
|
|
29
29
|
/* @__PURE__ */ a("span", { className: e.topAmount, children: f }),
|
|
30
30
|
/* @__PURE__ */ a("span", { className: e.topUnit, children: s.unit })
|
|
31
31
|
] }),
|
|
32
|
-
!
|
|
33
|
-
/* @__PURE__ */ a(
|
|
32
|
+
!r && /* @__PURE__ */ a(T, { granted: i, purchased: l, total: F, className: e.topBar }),
|
|
33
|
+
/* @__PURE__ */ a($, { className: e.topChevron })
|
|
34
34
|
] }),
|
|
35
35
|
d.open && /* @__PURE__ */ n("div", { ...d.panelProps, className: e.popover, "aria-label": s.popover, children: [
|
|
36
36
|
/* @__PURE__ */ n("div", { className: e.popHead, children: [
|
|
@@ -38,16 +38,16 @@ function M({ granted: i = 0, purchased: l = 0, grantExpires: r, burnNote: m, unk
|
|
|
38
38
|
/* @__PURE__ */ a("span", { className: e.popAmount, children: f }),
|
|
39
39
|
/* @__PURE__ */ a("span", { className: e.popUnit, children: s.unit })
|
|
40
40
|
] }),
|
|
41
|
-
c !== void 0 && /* @__PURE__ */ a(
|
|
41
|
+
c !== void 0 && /* @__PURE__ */ a(B, { className: y.quiet, href: c, onClick: d.close, children: s.open })
|
|
42
42
|
] }),
|
|
43
|
-
|
|
44
|
-
/* @__PURE__ */ a(T, { granted: i, purchased: l, total:
|
|
43
|
+
r ? /* @__PURE__ */ a("p", { className: e.popNote, children: s.balanceUnknown }) : /* @__PURE__ */ n(P, { children: [
|
|
44
|
+
/* @__PURE__ */ a(T, { granted: i, purchased: l, total: F, className: e.popBar, label: N() }),
|
|
45
45
|
/* @__PURE__ */ n("ul", { className: e.legend, children: [
|
|
46
46
|
/* @__PURE__ */ n("li", { className: e.legendRow, children: [
|
|
47
47
|
/* @__PURE__ */ a("span", { className: e.swatchGranted, "aria-hidden": "true" }),
|
|
48
48
|
/* @__PURE__ */ n("span", { className: e.legendText, children: [
|
|
49
49
|
/* @__PURE__ */ a("span", { className: e.legendFigure, children: s.grantedFigure(o(i, u)) }),
|
|
50
|
-
/* @__PURE__ */ a("span", { className: e.legendNote, children:
|
|
50
|
+
/* @__PURE__ */ a("span", { className: e.legendNote, children: t ? s.grantedNote(t) : s.grantedNoteUndated })
|
|
51
51
|
] })
|
|
52
52
|
] }),
|
|
53
53
|
/* @__PURE__ */ n("li", { className: e.legendRow, children: [
|
|
@@ -58,23 +58,23 @@ function M({ granted: i = 0, purchased: l = 0, grantExpires: r, burnNote: m, unk
|
|
|
58
58
|
] })
|
|
59
59
|
] })
|
|
60
60
|
] }),
|
|
61
|
-
(
|
|
62
|
-
|
|
63
|
-
v !== void 0 && /* @__PURE__ */ a("button", { type: "button", className: `${
|
|
61
|
+
(p || v !== void 0) && /* @__PURE__ */ n("div", { className: e.popFoot, children: [
|
|
62
|
+
p && /* @__PURE__ */ a("p", { className: e.popNote, children: p }),
|
|
63
|
+
v !== void 0 && /* @__PURE__ */ a("button", { type: "button", className: `${y.primary} ${e.buy}`, "data-tone": "accent", onClick: v, children: s.buyTokens })
|
|
64
64
|
] })
|
|
65
65
|
] })
|
|
66
66
|
] })
|
|
67
67
|
] });
|
|
68
68
|
}
|
|
69
|
-
function W({ balance: i, burnNote: l, onTopUp:
|
|
70
|
-
const c = A(
|
|
69
|
+
function W({ balance: i, burnNote: l, onTopUp: t, labels: p, locale: r = k, walletHref: v, linkComponent: m, defaultOpen: u }) {
|
|
70
|
+
const c = A(p), g = U({ defaultOpen: u }), h = m ?? "a", s = i < 0, d = o(s ? -i : i, r);
|
|
71
71
|
return /* @__PURE__ */ n("div", { className: e.topRoot, "data-past-zero": s ? "true" : void 0, children: [
|
|
72
72
|
/* @__PURE__ */ n("button", { type: "button", className: e.topTrigger, "aria-label": s ? c.pastZeroBalance(d) : c.railBalance(d), onClick: g.toggle, ...g.triggerProps, children: [
|
|
73
73
|
/* @__PURE__ */ n("span", { className: e.topFigure, "aria-hidden": "true", children: [
|
|
74
74
|
/* @__PURE__ */ a("span", { className: e.topAmount, children: d }),
|
|
75
75
|
/* @__PURE__ */ a("span", { className: e.topUnit, children: s ? c.pastZero : c.unit })
|
|
76
76
|
] }),
|
|
77
|
-
/* @__PURE__ */ a(
|
|
77
|
+
/* @__PURE__ */ a($, { className: e.topChevron })
|
|
78
78
|
] }),
|
|
79
79
|
g.open && /* @__PURE__ */ n("div", { ...g.panelProps, className: e.popover, "aria-label": c.popover, children: [
|
|
80
80
|
/* @__PURE__ */ n("div", { className: e.popHead, children: [
|
|
@@ -82,32 +82,32 @@ function W({ balance: i, burnNote: l, onTopUp: r, labels: m, locale: t = k, wall
|
|
|
82
82
|
/* @__PURE__ */ a("span", { className: e.popAmount, children: d }),
|
|
83
83
|
/* @__PURE__ */ a("span", { className: e.popUnit, children: s ? c.pastZeroLong : c.unit })
|
|
84
84
|
] }),
|
|
85
|
-
v !== void 0 && /* @__PURE__ */ a(h, { className:
|
|
85
|
+
v !== void 0 && /* @__PURE__ */ a(h, { className: y.quiet, href: v, onClick: g.close, children: c.open })
|
|
86
86
|
] }),
|
|
87
|
-
(l ||
|
|
87
|
+
(l || t !== void 0) && /* @__PURE__ */ n("div", { className: e.popFoot, children: [
|
|
88
88
|
l && /* @__PURE__ */ a("p", { className: e.popNote, children: l }),
|
|
89
|
-
|
|
89
|
+
t !== void 0 && /* @__PURE__ */ a("button", { type: "button", className: `${y.primary} ${e.buy}`, "data-tone": "accent", onClick: t, children: c.buyTokens })
|
|
90
90
|
] })
|
|
91
91
|
] })
|
|
92
92
|
] });
|
|
93
93
|
}
|
|
94
|
-
function T({ granted: i, purchased: l, total:
|
|
95
|
-
return /* @__PURE__ */ n("span", { className:
|
|
96
|
-
/* @__PURE__ */ a("span", { className: e.segmentGranted, "data-segment": "granted", style: { width: L(i,
|
|
97
|
-
/* @__PURE__ */ a("span", { className: e.segmentPurchased, "data-segment": "purchased", style: { width: L(l,
|
|
94
|
+
function T({ granted: i, purchased: l, total: t, className: p, label: r }) {
|
|
95
|
+
return /* @__PURE__ */ n("span", { className: p, role: r ? "img" : void 0, "aria-label": r, "aria-hidden": r ? void 0 : "true", children: [
|
|
96
|
+
/* @__PURE__ */ a("span", { className: e.segmentGranted, "data-segment": "granted", style: { width: L(i, t) } }),
|
|
97
|
+
/* @__PURE__ */ a("span", { className: e.segmentPurchased, "data-segment": "purchased", style: { width: L(l, t) } })
|
|
98
98
|
] });
|
|
99
99
|
}
|
|
100
|
-
function I({ granted: i = 0, purchased: l = 0, grantCycle:
|
|
101
|
-
const s = A(g), d = () => s.bar(o(i, h), o(l, h), o(i + l, h)),
|
|
100
|
+
function I({ granted: i = 0, purchased: l = 0, grantCycle: t, burnedFraction: p, grantExpires: r, burnNote: v, unknown: m = !1, collapsed: u = !1, onTopUp: c, labels: g, locale: h = k }) {
|
|
101
|
+
const s = A(g), d = () => s.bar(o(i, h), o(l, h), o(i + l, h)), B = i + l, b = (t ?? i) + l, F = t !== void 0 && t > 0 ? (t - i) / t : 0, f = p ?? F, N = !m && f >= E;
|
|
102
102
|
if (u) {
|
|
103
|
-
const C =
|
|
103
|
+
const C = m ? void 0 : s.railFigure(w(B, h));
|
|
104
104
|
return /* @__PURE__ */ n("div", { className: e.rail, role: "group", "aria-label": C === void 0 ? s.railBalanceUnknown : s.railBalance(C), tabIndex: 0, children: [
|
|
105
105
|
/* @__PURE__ */ a(x, { className: e.railIcon }),
|
|
106
106
|
/* @__PURE__ */ a("span", { className: e.railFigure, children: C ?? "—" }),
|
|
107
|
-
|
|
107
|
+
m ? /* @__PURE__ */ a("span", { className: e.srOnly, children: s.railUnknown }) : /* @__PURE__ */ a(R, { granted: i, purchased: l, cycleTotal: b, tone: N ? "threshold" : "rest", label: d() })
|
|
108
108
|
] });
|
|
109
109
|
}
|
|
110
|
-
return /* @__PURE__ */ n("div", { className: e.chip, role: "group", "aria-label": s.region, tabIndex: 0, "data-threshold": N ? "true" : void 0, "data-unknown":
|
|
110
|
+
return /* @__PURE__ */ n("div", { className: e.chip, role: "group", "aria-label": s.region, tabIndex: 0, "data-threshold": N ? "true" : void 0, "data-unknown": m ? "true" : void 0, children: [
|
|
111
111
|
/* @__PURE__ */ n("div", { className: e.header, children: [
|
|
112
112
|
/* @__PURE__ */ a("span", { className: e.name, children: s.name }),
|
|
113
113
|
N && /* @__PURE__ */ n("span", { className: e.pill, children: [
|
|
@@ -115,16 +115,16 @@ function I({ granted: i = 0, purchased: l = 0, grantCycle: r, burnedFraction: m,
|
|
|
115
115
|
"%"
|
|
116
116
|
] })
|
|
117
117
|
] }),
|
|
118
|
-
|
|
118
|
+
m ? /* @__PURE__ */ n("div", { className: e.figure, children: [
|
|
119
119
|
/* @__PURE__ */ a("span", { className: e.amount, "aria-hidden": "true", children: "—" }),
|
|
120
|
-
/* @__PURE__ */ a(
|
|
120
|
+
/* @__PURE__ */ a(O, { className: e.retry }),
|
|
121
121
|
/* @__PURE__ */ a("span", { className: e.srOnly, children: s.balanceUnknown })
|
|
122
122
|
] }) : /* @__PURE__ */ n("div", { className: e.figure, children: [
|
|
123
|
-
/* @__PURE__ */ a("span", { className: e.amount, children: o(
|
|
123
|
+
/* @__PURE__ */ a("span", { className: e.amount, children: o(B, h) }),
|
|
124
124
|
/* @__PURE__ */ a("span", { className: e.unit, children: s.unit })
|
|
125
125
|
] }),
|
|
126
|
-
|
|
127
|
-
!
|
|
126
|
+
m ? /* @__PURE__ */ a("div", { className: e.bar, "data-tone": "unknown", "aria-hidden": "true" }) : /* @__PURE__ */ a(R, { granted: i, purchased: l, cycleTotal: b, tone: N ? "threshold" : "rest", label: d() }),
|
|
127
|
+
!m && !N && /* @__PURE__ */ n("div", { className: e.details, children: [
|
|
128
128
|
/* @__PURE__ */ n("dl", { className: e.split, children: [
|
|
129
129
|
/* @__PURE__ */ n("div", { className: e.splitRow, children: [
|
|
130
130
|
/* @__PURE__ */ n("dt", { className: e.splitLabel, children: [
|
|
@@ -141,14 +141,14 @@ function I({ granted: i = 0, purchased: l = 0, grantCycle: r, burnedFraction: m,
|
|
|
141
141
|
/* @__PURE__ */ a("dd", { className: e.splitAmount, children: o(l, h) })
|
|
142
142
|
] })
|
|
143
143
|
] }),
|
|
144
|
-
|
|
144
|
+
r && /* @__PURE__ */ a("p", { className: e.footer, children: s.grantExpiry(r) })
|
|
145
145
|
] }),
|
|
146
146
|
N && v && /* @__PURE__ */ a("p", { className: e.burnNote, children: v }),
|
|
147
|
-
!
|
|
147
|
+
!m && c !== void 0 && /* @__PURE__ */ a(
|
|
148
148
|
"button",
|
|
149
149
|
{
|
|
150
150
|
type: "button",
|
|
151
|
-
className: `${N ?
|
|
151
|
+
className: `${N ? y.primary : y.quiet} ${e.topUp}`,
|
|
152
152
|
"data-tone": N ? "accent" : void 0,
|
|
153
153
|
"data-grown": N ? "true" : void 0,
|
|
154
154
|
onClick: c,
|