@boost33/ui 0.8.0-next.2 → 0.8.0-next.3

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.
@@ -1 +1 @@
1
- {"version":3,"file":"WalletChip.js","sources":["../../../src/components/WalletChip/WalletChip.tsx"],"sourcesContent":["'use client'\n\n// A client module: the chip wires `onTopUp` to a button's onClick, which a\n// Server Component cannot carry. The rest of the shell stays on the server --\n// see Sidebar and B33-846.\n\nimport type { ElementType } from 'react'\nimport { useAnchoredOverlay } from '../../anchored-overlay'\nimport { Chevron } from '../AppSwitcher/icons'\nimport buttons from '../../buttons.module.css'\nimport styles from './WalletChip.module.css'\nimport { DEFAULT_WALLET_LOCALE, formatTokens, thousandsFigure } from './format'\nimport { RetryMark, WalletMark } from './icons'\nimport { resolveWalletChipLabels, type WalletChipLabels } from './labels'\n\n// Past this fraction of the cycle's grant burned, the chip stops resting. The\n// spec's threshold, in one place so the derivation and the styling agree.\n//\n// It stays in the package and is not a prop, which is the whole reason\n// `burnedFraction` is a fraction rather than a `threshold?: boolean` (B33-483).\n// The consumer measures its own burn; 80% is where four products agree that\n// measurement means something, and a boolean would hand each of them the policy\n// and let it drift. It also happens to agree with the middle rung of Score's own\n// burndown alerts, so adopting it changed no product decision.\nconst THRESHOLD_BURN = 0.8\n\n// The floor under a non-zero bar segment, named once so the width expression\n// below reads as what it is. The value and its derivation live in the token --\n// see --spacing-wallet-segment-min in tokens.css.\nconst SEGMENT_MIN = 'var(--spacing-wallet-segment-min)'\n\n/**\n * A bar segment's CSS `width`: its honest share of everything the cycle started\n * with, floored so that a non-zero amount is never drawn below one visible mark.\n *\n * `max()` rather than a `min-width` on the segment classes, and the reason is the\n * zero case (B33-590). A stylesheet sees only the percentage, and no CSS length\n * can tell 0% from 0.3% -- a bare `min-width` would floor a wallet that holds\n * none of that kind of token into a phantom sliver, which is the same lie in the\n * other direction. The decision needs the amount itself, so it is made here where\n * the amount is, in one expression instead of a rule plus a signal from the\n * component to switch it off. `max()` also keeps the honest percentage wherever\n * the share clears the floor: the floor only ever wins when the truth is\n * unpaintable.\n *\n * Why it needs flooring at all: once a seller has bought tokens the grant is a\n * small share of the cycle, and a small share of a 240px bar is a fraction of a\n * pixel. Score measured 3,000 granted rendering at `width: 0.3%` -- 0.72px, which\n * paints nothing, while the ledger still held the grant. Both segments are\n * floored: purchased burns second, so it drains to zero through every small\n * number on the way, and 10 purchased on a spent 3,000 grant is 0.33% of the\n * cycle and the seller's entire remaining balance.\n *\n * This is the second time the rendering has been reported. Score fixed it in its\n * own wallet markup; B33-482 replaced that markup with this component, which\n * carried no floor. Its record of the first fix is still on disk there, as dead\n * code, at components/app-shell/wallet-bar.ts.\n */\nfunction segmentWidth(amount: number, cycleTotal: number): string {\n // A cycle of nothing has no shares in it. Guarded because the divisor is\n // reachable -- a known balance of zero on a cycle that granted zero rendered\n // `width: NaN%`, which a browser drops rather than reports.\n const share = cycleTotal > 0 ? (amount / cycleTotal) * 100 : 0\n if (amount <= 0) return `${share}%`\n return `max(${share}%, ${SEGMENT_MIN})`\n}\n\n/**\n * The props that say nothing about a balance, so both states take them\n * identically: where the chip is drawn, what it is wired to, and the words and\n * number formats it renders with.\n */\ninterface WalletChipChromeProps {\n /**\n * Where the chip is drawn. `'sidebar'`, the default, is the card it has always\n * been, pinned at the sidebar's foot. `'topbar'` is the 3 Nov 2026 redesign's\n * token chip (B33-1712, boards 20 and 21): a 32px pill in the top bar, drawn\n * like the app switcher, that opens a popover with the breakdown.\n *\n * One component in two placements rather than a second component, because the\n * figures, the bar's rules and every word are the same chip's (CLAUDE.md:\n * build once, add a prop). `collapsed` applies to the sidebar placement only;\n * the top bar has no rail.\n */\n placement?: 'sidebar' | 'topbar'\n /**\n * Where the popover's \"Open\" link goes -- the Wallet. `placement=\"topbar\"`\n * only. Omit it and the popover draws no link. The consumer supplies the URL;\n * Identity owns where the Wallet lives.\n */\n walletHref?: string\n /**\n * The element the popover's \"Open\" link renders through, so it routes\n * client-side with the locale prefix intact (ADR-0011). Defaults to `a`.\n */\n linkComponent?: ElementType\n /** Render the top-bar popover open on first paint -- the specimen's open state. */\n defaultOpen?: boolean\n /** The 56px rail. Abbreviates the balance and turns the split on its side. */\n collapsed?: boolean\n /**\n * Top up. Wiring for the one button the sidebar is allowed to have.\n *\n * **It is also what makes that button exist.** Omit it and the chip draws no\n * top-up affordance at all, in every state, and that is the fix B33-1270 made.\n * The render used to be gated on `!unknown` alone, so a consumer that passed\n * no handler got a visible control that did nothing and could not be removed\n * -- `.topUp`'s class name is CSS-Modules-hashed, so there is no selector to\n * reach it from outside the package.\n *\n * That was not hypothetical in either consumer. The Wallet passes no handler\n * and ships the dead button today\n * (`boostledger/src/app/(wallet)/WalletShell.tsx`), and Score wrote a whole\n * client module to arm it across a Server Component boundary rather than live\n * with it (`boostscore/components/app-shell/wallet-chip-top-up.tsx`, which\n * says so at length).\n *\n * Nothing renders in the button's place. It carries no information of its own\n * -- only its own affordance -- so a consumer with nowhere to send someone is\n * better served by the chip without it than by a disabled one they did not\n * ask for (ADR-0010, \"Not entitled is not disabled\", on why unavailable is not\n * the same as disabled). B33-1047 is the other half: an `href` form of the\n * same affordance, for a consumer that cannot pass a callback at all.\n */\n onTopUp?: () => void\n /**\n * The chip's user-facing strings. Every field has an English default, so a\n * consumer supplies only the ones it translates; the first two consumers are\n * French-default. The package takes no i18n dependency -- it renders what it\n * is given (B33-849).\n *\n * Every field is a plain string, so the whole object crosses the React Server\n * Component boundary and a Next App Router page can build it. The four labels\n * that carry a value mark its position with a `%placeholder%` (B33-1277). None\n * of them takes a function: that form only ever worked from a Client Component\n * and nothing in the type said so (B33-880, B33-1278).\n */\n labels?: Partial<WalletChipLabels>\n /**\n * The BCP-47 language tag the chip's figures are formatted in. Defaults to\n * `en-US`, which is what the chip rendered before this prop existed.\n *\n * `labels` gives the consumer the chip's words; this gives it their numbers.\n * French writes `4 600` where English writes `4,600`, and a label cannot fix\n * it -- the label functions receive the figures already formatted, so before\n * this prop there was no hook at all (B33-876). Grouping and the decimal\n * separator are CLDR facts about a locale rather than a property of the\n * consumer's sentence, so a tag is enough: `Intl.NumberFormat` knows the rest,\n * and a tag is a plain string, which a Server Component can pass where a\n * formatter function could not.\n *\n * It reaches the visible figures and, because the same strings are\n * interpolated into them, the `bar` and `railBalance` accessible names.\n *\n * The `k` after the rail's abbreviated balance is a word, not a number, so it\n * is `labels.railFigure` and not this.\n */\n locale?: string\n}\n\n/**\n * The chip with a balance the consumer stands behind. The two remaining figures\n * are required, because this is the state that renders them.\n */\nexport interface WalletChipKnownProps extends WalletChipChromeProps {\n /**\n * Absent or `false`. The discriminant: it is what makes the figures below\n * required here and forbidden in {@link WalletChipUnknownProps}.\n */\n unknown?: false\n /** Absent or `false`: see {@link WalletChipTotalOnlyProps}. */\n totalOnly?: false\n /** Remaining granted tokens. Granted burns first and expires. */\n granted: number\n /** Remaining purchased tokens. Purchased never expires. */\n purchased: number\n /**\n * What this cycle's grant started with, if the consumer knows. The bar's full\n * width alongside purchased, so the part already burned draws as empty track\n * and the filled portion shortens as the month runs.\n *\n * **Optional, and that is the fix B33-483 made.** It used to be required, and\n * no consumer could satisfy it honestly: it is the Ledger's\n * `grant_tokens_per_cycle`, and it cannot be reconstructed from a balance --\n * `cycleUsedPct` counts purchased-bucket spend, is not `(cycle total -\n * remaining)` once a credit, refund or void has touched the account, and is not\n * bounded at 100. So the Wallet passed `grantCycle = granted`, which pins\n * `burned` at 0 and held the threshold state unreachable in the reference\n * implementation, and Score passed a local per-plan belief instead.\n *\n * Omitted, the bar measures against `granted + purchased` -- the balance\n * itself. It then draws full and claims nothing about what burned, which is the\n * honest rendering when nobody can say.\n *\n * It no longer decides the threshold on its own: {@link burnedFraction} does\n * that, and this is only consulted when that is absent.\n */\n grantCycle?: number\n /**\n * How much of the cycle's grant has burned, as a fraction -- `0.88` for 88%.\n * Drives the threshold state, and is the figure the chip rounds into its pill.\n *\n * Passed in rather than derived, for exactly the reason {@link burnNote}\n * already is: the burn is the product's measurement to make, not the chip's.\n * The chip kept the arithmetic while taking the runway line as a prop, and fed\n * it from a denominator no product could supply -- so the measurement was the\n * chip's in name and nobody's in fact (B33-483).\n *\n * The *policy* stays here. `THRESHOLD_BURN` is 0.8 in this package, so four\n * products trip at the same place and cannot drift to four different numbers.\n * That is why this is a fraction and not a `threshold?: boolean`: a boolean\n * would move the 80% decision into each consumer, and it would leave the pill\n * with no figure to print.\n *\n * Supplied, it wins. Absent, the chip derives it from {@link grantCycle} as it\n * always did; absent with no `grantCycle` either, nothing is claimed and the\n * chip rests.\n *\n * It is not clamped. A consumer that passes a percentage where a fraction\n * belongs renders a visibly absurd pill rather than a plausible \"100%\", and a\n * wrong figure that looks wrong is the one that gets fixed.\n */\n burnedFraction?: number\n /** When the current grant expires, preformatted for display, e.g. \"30 Sep\". */\n grantExpires?: string\n /**\n * The threshold state's runway line, preformatted, e.g. \"About four days left\n * at your current burn.\" Passed in rather than derived: the burn estimate is\n * the product's to make, not the chip's -- the chip renders what it is given.\n */\n burnNote?: string\n}\n\n/**\n * The chip whose balance could not be confirmed. It renders an em dash, no bar\n * and no button, so **it takes no figures at all** -- each one below is\n * `?: never`.\n *\n * That is deliberate and it is the second half of B33-483. The figures were\n * required in every state, so both consumers manufactured zeroes to get past the\n * type and then apologised for them in a comment -- \"these zeroes are never\n * rendered ... but they are not a balance and must not be read as one\"\n * (`boostscore/components/app-shell/wallet-chip.tsx`), and the same again in\n * `wallet-slot.tsx` and `boostledger/src/app/(wallet)/layout.tsx`. A zero is a\n * confident claim about someone's money. The type now refuses it rather than\n * asking a comment to.\n *\n * `undefined` is still accepted, so a consumer that must build one props object\n * can write `{ unknown: true, granted: undefined }` instead of branching.\n */\nexport interface WalletChipUnknownProps extends WalletChipChromeProps {\n /** The balance could not be confirmed. Renders as unknown, never as zero. */\n unknown: true\n totalOnly?: false\n balance?: never\n granted?: never\n purchased?: never\n grantCycle?: never\n burnedFraction?: never\n /** Not rendered in this state: the footer only draws on a resting chip. */\n grantExpires?: never\n /** Not rendered in this state: the runway line only draws past the threshold. */\n burnNote?: never\n}\n\n/**\n * A discriminated union on `unknown`, and the discriminant has consequences for\n * how a consumer *types* its own code, not only what it passes:\n *\n * - A helper that returns props for both states needs an explicit\n * `: WalletChipProps` return annotation. Without one, TypeScript widens the\n * `unknown: true` in an object literal to `boolean`, which matches neither\n * member, and the spread at the call site stops compiling.\n * - `Omit<WalletChipProps, 'onTopUp'>` is not what it looks like. `Omit` over a\n * union flattens it into one object and loses the discrimination. Omit from\n * the members instead -- `Omit<WalletChipKnownProps, 'onTopUp'> |\n * Omit<WalletChipUnknownProps, 'onTopUp'>` -- which is why both are exported.\n */\nexport type WalletChipProps = WalletChipKnownProps | WalletChipUnknownProps | WalletChipTotalOnlyProps\n\n/**\n * The top-bar chip with a real total and no drawable split (B33-1712). Debits\n * draw the grant first, then purchased, and may run a set share past zero (the\n * Ledger's overdraft); credits add only to the bucket they name and never pay a\n * negative one off. So the Ledger can report a bucket below zero -- granted 0 /\n * purchased -40 (overdrawn, total -40), or granted 1000 / purchased -40 after\n * the month's grant lands (total 960, for the rest of the month). A two-segment\n * bar cannot draw a negative, and clamping it to zero would draw a split nobody\n * has: a split invented by the interface. So this state takes **the total and\n * no split**, and draws no bar and no granted / purchased legend.\n *\n * Below zero the figure is the distance past zero, positive, in --color-danger,\n * with the words \"past zero\" where a minus sign would be -- the Wallet's own\n * past-zero screen (boostledger, Paper 201 screen 03), followed rather than\n * inventing a third look. Nothing is broken and the copy does not say it is;\n * what happens next is the consumer's `burnNote`, because the overdraft limit is\n * the Ledger's. At zero or above it is the plain balance.\n *\n * **Top bar only.** The sidebar card has no drawing for this and the redesigned\n * shell does not render it, so the type requires `placement=\"topbar\"` rather\n * than leaving the card to guess. No board draws either form: they are new\n * undrawn states for the person at the gate to judge.\n */\nexport interface WalletChipTotalOnlyProps extends WalletChipChromeProps {\n placement: 'topbar'\n totalOnly: true\n unknown?: false\n /**\n * The balance, signed, in whole tokens: `granted + purchased` from the\n * Ledger's signed figures, which its single balance row guarantees is the\n * balance.\n */\n balance: number\n /**\n * What happens next, preformatted, e.g. \"Work carries on for another 930\n * tokens, then paid actions stop.\" The consumer's, because the overdraft limit\n * is the Ledger's, not the chip's.\n */\n burnNote?: string\n granted?: never\n purchased?: never\n grantCycle?: never\n burnedFraction?: never\n grantExpires?: never\n}\n\n/**\n * The granted/purchased bar. Granted is drawn first, because granted burns\n * first. The accessible label is passed in whole rather than assembled here:\n * word order is the consumer's, not English's (B33-849).\n */\nfunction Bar({ granted, purchased, cycleTotal, tone, label }: { granted: number; purchased: number; cycleTotal: number; tone: 'rest' | 'threshold'; label: string }) {\n return (\n <div className={styles.bar} data-tone={tone} role=\"img\" aria-label={label}>\n <span className={styles.segmentGranted} data-testid=\"segment-granted\" style={{ width: segmentWidth(granted, cycleTotal) }} />\n <span className={styles.segmentPurchased} data-testid=\"segment-purchased\" style={{ width: segmentWidth(purchased, cycleTotal) }} />\n </div>\n )\n}\n\nexport function WalletChip(props: WalletChipProps) {\n if (props.totalOnly === true) return <TotalOnlyChip {...props} />\n if (props.placement === 'topbar') return <TopBarChip {...props} />\n return <SidebarChip {...props} />\n}\n\n/**\n * The top-bar token chip (B33-1712). The pill shows the balance and the\n * two-segment bar; the popover (board 21) shows the figure, the bar again at\n * width, each kind of token with what it means, the runway line, and the buy\n * button.\n *\n * Both segments are always drawn and a zero one is 0px wide with the gap kept\n * -- board 22, and B33-590's rule that a zero is never drawn as a sliver. That\n * overrides B33-1712's line about copying the hub's 4px floor, by Jamal's\n * decision of 3 Nov 2026. A non-zero segment keeps the existing floor\n * (`segmentWidth`).\n *\n * Every figure is the consumer's. Unknown renders an em dash, no bar and no\n * breakdown -- a made-up split in a token balance is a commercial fact invented\n * by the interface (B33-1712).\n *\n * A disclosure, like the switcher (ADR-0010): the trigger is a button, the\n * panel carries no widget role, and dismissal is the shared anchored-overlay\n * contract's (ADR-0009).\n */\nfunction TopBarChip({ granted = 0, purchased = 0, grantExpires, burnNote, unknown = false, onTopUp, labels, locale = DEFAULT_WALLET_LOCALE, walletHref, linkComponent, defaultOpen }: WalletChipProps) {\n const t = resolveWalletChipLabels(labels)\n const overlay = useAnchoredOverlay({ defaultOpen })\n const Link: ElementType = linkComponent ?? 'a'\n const balance = granted + purchased\n // The pill and the popover draw the split of the balance itself: no burned\n // track, because the top bar claims nothing about what the cycle started\n // with. The sidebar card still does, from `grantCycle`.\n const total = balance\n const figure = unknown ? '—' : formatTokens(balance, locale)\n const buildBarLabel = () => t.bar(formatTokens(granted, locale), formatTokens(purchased, locale), formatTokens(balance, locale))\n\n return (\n <div className={styles.topRoot}>\n <button type=\"button\" className={styles.topTrigger} aria-label={unknown ? t.railBalanceUnknown : t.railBalance(figure)} onClick={overlay.toggle} {...overlay.triggerProps}>\n <span className={styles.topFigure} aria-hidden=\"true\">\n <span className={styles.topAmount}>{figure}</span>\n <span className={styles.topUnit}>{t.unit}</span>\n </span>\n {!unknown && <TopBar granted={granted} purchased={purchased} total={total} className={styles.topBar} />}\n <Chevron className={styles.topChevron} />\n </button>\n\n {overlay.open && (\n <div {...overlay.panelProps} className={styles.popover} aria-label={t.popover}>\n <div className={styles.popHead}>\n <span className={styles.popFigure}>\n <span className={styles.popAmount}>{figure}</span>\n <span className={styles.popUnit}>{t.unit}</span>\n </span>\n {walletHref !== undefined && (\n <Link className={buttons.quiet} href={walletHref} onClick={overlay.close}>\n {t.open}\n </Link>\n )}\n </div>\n\n {unknown ? (\n <p className={styles.popNote}>{t.balanceUnknown}</p>\n ) : (\n <>\n <TopBar granted={granted} purchased={purchased} total={total} className={styles.popBar} label={buildBarLabel()} />\n <ul className={styles.legend}>\n <li className={styles.legendRow}>\n <span className={styles.swatchGranted} aria-hidden=\"true\" />\n <span className={styles.legendText}>\n <span className={styles.legendFigure}>{t.grantedFigure(formatTokens(granted, locale))}</span>\n <span className={styles.legendNote}>{grantExpires ? t.grantedNote(grantExpires) : t.grantedNoteUndated}</span>\n </span>\n </li>\n <li className={styles.legendRow}>\n <span className={styles.swatchPurchased} aria-hidden=\"true\" />\n <span className={styles.legendText}>\n <span className={styles.legendFigure}>{t.purchasedFigure(formatTokens(purchased, locale))}</span>\n <span className={styles.legendNote}>{t.purchasedNote}</span>\n </span>\n </li>\n </ul>\n {(burnNote || onTopUp !== undefined) && (\n <div className={styles.popFoot}>\n {burnNote && <p className={styles.popNote}>{burnNote}</p>}\n {onTopUp !== undefined && (\n <button type=\"button\" className={`${buttons.primary} ${styles.buy}`} data-tone=\"accent\" onClick={onTopUp}>\n {t.buyTokens}\n </button>\n )}\n </div>\n )}\n </>\n )}\n </div>\n )}\n </div>\n )\n}\n\n/**\n * The top-bar chip with no drawable split (`WalletChipTotalOnlyProps`): the same\n * pill and popover as {@link TopBarChip}, with the total alone -- the distance\n * past zero when it is below -- and no bar and no legend anywhere.\n */\nfunction TotalOnlyChip({ balance, burnNote, onTopUp, labels, locale = DEFAULT_WALLET_LOCALE, walletHref, linkComponent, defaultOpen }: WalletChipTotalOnlyProps) {\n const t = resolveWalletChipLabels(labels)\n const overlay = useAnchoredOverlay({ defaultOpen })\n const Link: ElementType = linkComponent ?? 'a'\n const pastZero = balance < 0\n const figure = formatTokens(pastZero ? -balance : balance, locale)\n\n return (\n <div className={styles.topRoot} data-past-zero={pastZero ? 'true' : undefined}>\n <button type=\"button\" className={styles.topTrigger} aria-label={pastZero ? t.pastZeroBalance(figure) : t.railBalance(figure)} onClick={overlay.toggle} {...overlay.triggerProps}>\n <span className={styles.topFigure} aria-hidden=\"true\">\n <span className={styles.topAmount}>{figure}</span>\n <span className={styles.topUnit}>{pastZero ? t.pastZero : t.unit}</span>\n </span>\n <Chevron className={styles.topChevron} />\n </button>\n\n {overlay.open && (\n <div {...overlay.panelProps} className={styles.popover} aria-label={t.popover}>\n <div className={styles.popHead}>\n <span className={styles.popFigure}>\n <span className={styles.popAmount}>{figure}</span>\n <span className={styles.popUnit}>{pastZero ? t.pastZeroLong : t.unit}</span>\n </span>\n {walletHref !== undefined && (\n <Link className={buttons.quiet} href={walletHref} onClick={overlay.close}>\n {t.open}\n </Link>\n )}\n </div>\n {(burnNote || onTopUp !== undefined) && (\n <div className={styles.popFoot}>\n {burnNote && <p className={styles.popNote}>{burnNote}</p>}\n {onTopUp !== undefined && (\n <button type=\"button\" className={`${buttons.primary} ${styles.buy}`} data-tone=\"accent\" onClick={onTopUp}>\n {t.buyTokens}\n </button>\n )}\n </div>\n )}\n </div>\n )}\n </div>\n )\n}\n\n/**\n * The top bar's two-segment bar, in the pill and in the popover. Granted first,\n * because it burns first; a fixed gap between the two that stays when a segment\n * is zero, so a two-part balance never reads as one lump (board 22).\n */\nfunction TopBar({ granted, purchased, total, className, label }: { granted: number; purchased: number; total: number; className?: string; label?: string }) {\n return (\n <span className={className} role={label ? 'img' : undefined} aria-label={label} aria-hidden={label ? undefined : 'true'}>\n <span className={styles.segmentGranted} data-segment=\"granted\" style={{ width: segmentWidth(granted, total) }} />\n <span className={styles.segmentPurchased} data-segment=\"purchased\" style={{ width: segmentWidth(purchased, total) }} />\n </span>\n )\n}\n\nfunction SidebarChip({ granted = 0, purchased = 0, grantCycle, burnedFraction, grantExpires, burnNote, unknown = false, collapsed = false, onTopUp, labels, locale = DEFAULT_WALLET_LOCALE }: WalletChipProps) {\n const t = resolveWalletChipLabels(labels)\n // Assembled lazily: the unknown state draws no bar, and a consumer's `bar`\n // implementation can throw on the zeroes the destructure above defaults the\n // degraded path's absent figures to. Calling it only where a bar renders keeps\n // that crash surface off the render that exists precisely because fetching the\n // balance already failed (B33-875). B33-483 stopped the consumer having to pass\n // those zeroes; the chip still has to pick something, so the laziness stays.\n const buildBarLabel = () =>\n t.bar(formatTokens(granted, locale), formatTokens(purchased, locale), formatTokens(granted + purchased, locale))\n const balance = granted + purchased\n // Everything the cycle started with: the grant plus the purchased balance\n // sitting alongside it. Measuring against this is what makes the bar shorten\n // as tokens burn instead of only re-proportioning to a full width.\n //\n // With no `grantCycle` the denominator falls back to `granted + purchased`, so\n // the bar fills and there is no burned track -- the honest drawing when nobody\n // can say what the cycle started with (B33-483). The zero-division guard inside\n // segmentWidth still covers the case where that balance is itself zero.\n const cycleTotal = (grantCycle ?? granted) + purchased\n // How much of the cycle's grant has burned. The consumer's own figure wins;\n // `grantCycle` derives it exactly as it always did; with neither, nothing is\n // claimed and the chip rests. `grantCycle` is tested against undefined rather\n // than leaned on for falsiness, because 0 and \"not supplied\" both resolve to a\n // burn of 0 and must keep doing so.\n const derivedBurn = grantCycle !== undefined && grantCycle > 0 ? (grantCycle - granted) / grantCycle : 0\n const burned = burnedFraction ?? derivedBurn\n const isThreshold = !unknown && burned >= THRESHOLD_BURN\n\n if (collapsed) {\n // The abbreviated balance, assembled once: it is both the rail's visible\n // text and the figure the group's accessible name is built around, and a\n // sighted user and a screen-reader user must not be told different numbers.\n // Built only when a balance is known, for the same reason buildBarLabel is\n // lazy -- the unknown rail shows no figure, and a consumer's railFigure\n // should not run on a degraded path it never renders on (B33-875).\n const railBalance = unknown ? undefined : t.railFigure(thousandsFigure(balance, locale))\n return (\n <div className={styles.rail} role=\"group\" aria-label={railBalance === undefined ? t.railBalanceUnknown : t.railBalance(railBalance)} tabIndex={0}>\n <WalletMark className={styles.railIcon} />\n <span className={styles.railFigure}>{railBalance ?? '—'}</span>\n {unknown ? <span className={styles.srOnly}>{t.railUnknown}</span> : <Bar granted={granted} purchased={purchased} cycleTotal={cycleTotal} tone={isThreshold ? 'threshold' : 'rest'} label={buildBarLabel()} />}\n </div>\n )\n }\n\n return (\n <div className={styles.chip} role=\"group\" aria-label={t.region} tabIndex={0} data-threshold={isThreshold ? 'true' : undefined} data-unknown={unknown ? 'true' : undefined}>\n <div className={styles.header}>\n <span className={styles.name}>{t.name}</span>\n {isThreshold && <span className={styles.pill}>{Math.round(burned * 100)}%</span>}\n </div>\n\n {unknown ? (\n <div className={styles.figure}>\n <span className={styles.amount} aria-hidden=\"true\">\n {'—'}\n </span>\n <RetryMark className={styles.retry} />\n <span className={styles.srOnly}>{t.balanceUnknown}</span>\n </div>\n ) : (\n <div className={styles.figure}>\n <span className={styles.amount}>{formatTokens(balance, locale)}</span>\n <span className={styles.unit}>{t.unit}</span>\n </div>\n )}\n\n {unknown ? (\n <div className={styles.bar} data-tone=\"unknown\" aria-hidden=\"true\" />\n ) : (\n <Bar granted={granted} purchased={purchased} cycleTotal={cycleTotal} tone={isThreshold ? 'threshold' : 'rest'} label={buildBarLabel()} />\n )}\n\n {!unknown && !isThreshold && (\n <div className={styles.details}>\n <dl className={styles.split}>\n <div className={styles.splitRow}>\n <dt className={styles.splitLabel}>\n <span className={styles.dotGranted} aria-hidden=\"true\" />\n {t.granted}\n </dt>\n <dd className={styles.splitAmount}>{formatTokens(granted, locale)}</dd>\n </div>\n <div className={styles.splitRow}>\n <dt className={styles.splitLabel}>\n <span className={styles.dotPurchased} aria-hidden=\"true\" />\n {t.purchased}\n </dt>\n <dd className={styles.splitAmount}>{formatTokens(purchased, locale)}</dd>\n </div>\n </dl>\n {grantExpires && <p className={styles.footer}>{t.grantExpiry(grantExpires)}</p>}\n </div>\n )}\n\n {isThreshold && burnNote && <p className={styles.burnNote}>{burnNote}</p>}\n\n {/* A handler is what makes the button exist, not only what it does when\n pressed (B33-1270). `!unknown` alone drew it for a consumer with\n nowhere to send anyone, and a hashed class name left them no way to\n take it back out -- see `onTopUp`. Tested against `undefined` rather\n than for truthiness so that `onTopUp={undefined}`, which is what a\n consumer's own absent value looks like, reads the same as omitting it. */}\n {!unknown && onTopUp !== undefined && (\n <button\n type=\"button\"\n // Two tiers for one button (B33-1713, ADR-0016): at rest it is a word\n // in the chip's header, the quiet tier; past the threshold it grows\n // into the chip's one call to action, the primary tier in the accent\n // tone. `.topUp` adds only where it sits.\n className={`${isThreshold ? buttons.primary : buttons.quiet} ${styles.topUp}`}\n data-tone={isThreshold ? 'accent' : undefined}\n data-grown={isThreshold ? 'true' : undefined}\n onClick={onTopUp}\n >\n {t.topUp}\n </button>\n )}\n </div>\n )\n}\n"],"names":["THRESHOLD_BURN","SEGMENT_MIN","segmentWidth","amount","cycleTotal","share","Bar","granted","purchased","tone","label","jsxs","styles","jsx","WalletChip","props","TotalOnlyChip","TopBarChip","SidebarChip","grantExpires","burnNote","unknown","onTopUp","labels","locale","DEFAULT_WALLET_LOCALE","walletHref","linkComponent","defaultOpen","t","resolveWalletChipLabels","overlay","useAnchoredOverlay","Link","balance","total","figure","formatTokens","buildBarLabel","TopBar","Chevron","buttons","Fragment","pastZero","className","grantCycle","burnedFraction","collapsed","derivedBurn","burned","isThreshold","railBalance","thousandsFigure","WalletMark","RetryMark"],"mappings":"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"}
1
+ {"version":3,"file":"WalletChip.js","sources":["../../../src/components/WalletChip/WalletChip.tsx"],"sourcesContent":["'use client'\n\n// A client module: the chip wires `onTopUp` to a button's onClick, which a\n// Server Component cannot carry. The rest of the shell stays on the server --\n// see Sidebar and B33-846.\n\nimport type { ElementType } from 'react'\nimport { useAnchoredOverlay } from '../../anchored-overlay'\nimport { Chevron } from '../AppSwitcher/icons'\nimport buttons from '../../buttons.module.css'\nimport styles from './WalletChip.module.css'\nimport { DEFAULT_WALLET_LOCALE, formatPercent, formatTokens, thousandsFigure } from './format'\nimport { RetryMark, WalletMark } from './icons'\nimport { resolveWalletChipLabels, type WalletChipLabels } from './labels'\n\n// Past this fraction of the cycle's grant burned, the chip stops resting. The\n// spec's threshold, in one place so the derivation and the styling agree.\n//\n// It stays in the package and is not a prop, which is the whole reason\n// `burnedFraction` is a fraction rather than a `threshold?: boolean` (B33-483).\n// The consumer measures its own burn; 80% is where four products agree that\n// measurement means something, and a boolean would hand each of them the policy\n// and let it drift. It also happens to agree with the middle rung of Score's own\n// burndown alerts, so adopting it changed no product decision.\nconst THRESHOLD_BURN = 0.8\n\n// The floor under a non-zero bar segment, named once so the width expression\n// below reads as what it is. The value and its derivation live in the token --\n// see --spacing-wallet-segment-min in tokens.css.\nconst SEGMENT_MIN = 'var(--spacing-wallet-segment-min)'\n\n/**\n * A bar segment's CSS `width`: its honest share of everything the cycle started\n * with, floored so that a non-zero amount is never drawn below one visible mark.\n *\n * `max()` rather than a `min-width` on the segment classes, and the reason is the\n * zero case (B33-590). A stylesheet sees only the percentage, and no CSS length\n * can tell 0% from 0.3% -- a bare `min-width` would floor a wallet that holds\n * none of that kind of token into a phantom sliver, which is the same lie in the\n * other direction. The decision needs the amount itself, so it is made here where\n * the amount is, in one expression instead of a rule plus a signal from the\n * component to switch it off. `max()` also keeps the honest percentage wherever\n * the share clears the floor: the floor only ever wins when the truth is\n * unpaintable.\n *\n * Why it needs flooring at all: once a seller has bought tokens the grant is a\n * small share of the cycle, and a small share of a 240px bar is a fraction of a\n * pixel. Score measured 3,000 granted rendering at `width: 0.3%` -- 0.72px, which\n * paints nothing, while the ledger still held the grant. Both segments are\n * floored: purchased burns second, so it drains to zero through every small\n * number on the way, and 10 purchased on a spent 3,000 grant is 0.33% of the\n * cycle and the seller's entire remaining balance.\n *\n * This is the second time the rendering has been reported. Score fixed it in its\n * own wallet markup; B33-482 replaced that markup with this component, which\n * carried no floor. Its record of the first fix is still on disk there, as dead\n * code, at components/app-shell/wallet-bar.ts.\n */\nfunction segmentWidth(amount: number, cycleTotal: number): string {\n // A cycle of nothing has no shares in it. Guarded because the divisor is\n // reachable -- a known balance of zero on a cycle that granted zero rendered\n // `width: NaN%`, which a browser drops rather than reports.\n const share = cycleTotal > 0 ? (amount / cycleTotal) * 100 : 0\n if (amount <= 0) return `${share}%`\n return `max(${share}%, ${SEGMENT_MIN})`\n}\n\n/**\n * The props that say nothing about a balance, so both states take them\n * identically: where the chip is drawn, what it is wired to, and the words and\n * number formats it renders with.\n */\ninterface WalletChipChromeProps {\n /**\n * Where the chip is drawn. `'sidebar'`, the default, is the card it has always\n * been, pinned at the sidebar's foot. `'topbar'` is the 3 Nov 2026 redesign's\n * token chip (B33-1712, boards 20 and 21): a 32px pill in the top bar, drawn\n * like the app switcher, that opens a popover with the breakdown.\n *\n * One component in two placements rather than a second component, because the\n * figures, the bar's rules and every word are the same chip's (CLAUDE.md:\n * build once, add a prop). `collapsed` applies to the sidebar placement only;\n * the top bar has no rail.\n */\n placement?: 'sidebar' | 'topbar'\n /**\n * Where the popover's \"Open\" link goes -- the Wallet. `placement=\"topbar\"`\n * only. Omit it and the popover draws no link. The consumer supplies the URL;\n * Identity owns where the Wallet lives.\n */\n walletHref?: string\n /**\n * The element the popover's \"Open\" link renders through, so it routes\n * client-side with the locale prefix intact (ADR-0011). Defaults to `a`.\n */\n linkComponent?: ElementType\n /** Render the top-bar popover open on first paint -- the specimen's open state. */\n defaultOpen?: boolean\n /** The 56px rail. Abbreviates the balance and turns the split on its side. */\n collapsed?: boolean\n /**\n * Top up. Wiring for the one button the sidebar is allowed to have.\n *\n * **It is also what makes that button exist.** Omit it and the chip draws no\n * top-up affordance at all, in every state, and that is the fix B33-1270 made.\n * The render used to be gated on `!unknown` alone, so a consumer that passed\n * no handler got a visible control that did nothing and could not be removed\n * -- `.topUp`'s class name is CSS-Modules-hashed, so there is no selector to\n * reach it from outside the package.\n *\n * That was not hypothetical in either consumer. The Wallet passes no handler\n * and ships the dead button today\n * (`boostledger/src/app/(wallet)/WalletShell.tsx`), and Score wrote a whole\n * client module to arm it across a Server Component boundary rather than live\n * with it (`boostscore/components/app-shell/wallet-chip-top-up.tsx`, which\n * says so at length).\n *\n * Nothing renders in the button's place. It carries no information of its own\n * -- only its own affordance -- so a consumer with nowhere to send someone is\n * better served by the chip without it than by a disabled one they did not\n * ask for (ADR-0010, \"Not entitled is not disabled\", on why unavailable is not\n * the same as disabled). B33-1047 is the other half: an `href` form of the\n * same affordance, for a consumer that cannot pass a callback at all.\n */\n onTopUp?: () => void\n /**\n * The chip's user-facing strings. Every field has an English default, so a\n * consumer supplies only the ones it translates; the first two consumers are\n * French-default. The package takes no i18n dependency -- it renders what it\n * is given (B33-849).\n *\n * Every field is a plain string, so the whole object crosses the React Server\n * Component boundary and a Next App Router page can build it. The four labels\n * that carry a value mark its position with a `%placeholder%` (B33-1277). None\n * of them takes a function: that form only ever worked from a Client Component\n * and nothing in the type said so (B33-880, B33-1278).\n */\n labels?: Partial<WalletChipLabels>\n /**\n * The BCP-47 language tag the chip's figures are formatted in. Defaults to\n * `en-US`, which is what the chip rendered before this prop existed.\n *\n * `labels` gives the consumer the chip's words; this gives it their numbers.\n * French writes `4 600` where English writes `4,600`, and a label cannot fix\n * it -- the label functions receive the figures already formatted, so before\n * this prop there was no hook at all (B33-876). Grouping and the decimal\n * separator are CLDR facts about a locale rather than a property of the\n * consumer's sentence, so a tag is enough: `Intl.NumberFormat` knows the rest,\n * and a tag is a plain string, which a Server Component can pass where a\n * formatter function could not.\n *\n * It reaches the visible figures and, because the same strings are\n * interpolated into them, the `bar` and `railBalance` accessible names.\n *\n * The `k` after the rail's abbreviated balance is a word, not a number, so it\n * is `labels.railFigure` and not this.\n */\n locale?: string\n}\n\n/**\n * The chip with a balance the consumer stands behind. The two remaining figures\n * are required, because this is the state that renders them.\n */\nexport interface WalletChipKnownProps extends WalletChipChromeProps {\n /**\n * Absent or `false`. The discriminant: it is what makes the figures below\n * required here and forbidden in {@link WalletChipUnknownProps}.\n */\n unknown?: false\n /** Absent or `false`: see {@link WalletChipTotalOnlyProps}. */\n totalOnly?: false\n /** Remaining granted tokens. Granted burns first and expires. */\n granted: number\n /** Remaining purchased tokens. Purchased never expires. */\n purchased: number\n /**\n * What this cycle's grant started with, if the consumer knows. The bar's full\n * width alongside purchased, so the part already burned draws as empty track\n * and the filled portion shortens as the month runs.\n *\n * **Optional, and that is the fix B33-483 made.** It used to be required, and\n * no consumer could satisfy it honestly: it is the Ledger's\n * `grant_tokens_per_cycle`, and it cannot be reconstructed from a balance --\n * `cycleUsedPct` counts purchased-bucket spend, is not `(cycle total -\n * remaining)` once a credit, refund or void has touched the account, and is not\n * bounded at 100. So the Wallet passed `grantCycle = granted`, which pins\n * `burned` at 0 and held the threshold state unreachable in the reference\n * implementation, and Score passed a local per-plan belief instead.\n *\n * Omitted, the bar measures against `granted + purchased` -- the balance\n * itself. It then draws full and claims nothing about what burned, which is the\n * honest rendering when nobody can say.\n *\n * It no longer decides the threshold on its own: {@link burnedFraction} does\n * that, and this is only consulted when that is absent.\n */\n grantCycle?: number\n /**\n * How much of the cycle's grant has burned, as a fraction -- `0.88` for 88%.\n * Drives the threshold state, and is the figure the chip rounds into its pill.\n *\n * Passed in rather than derived, for exactly the reason {@link burnNote}\n * already is: the burn is the product's measurement to make, not the chip's.\n * The chip kept the arithmetic while taking the runway line as a prop, and fed\n * it from a denominator no product could supply -- so the measurement was the\n * chip's in name and nobody's in fact (B33-483).\n *\n * The *policy* stays here. `THRESHOLD_BURN` is 0.8 in this package, so four\n * products trip at the same place and cannot drift to four different numbers.\n * That is why this is a fraction and not a `threshold?: boolean`: a boolean\n * would move the 80% decision into each consumer, and it would leave the pill\n * with no figure to print.\n *\n * Supplied, it wins. Absent, the chip derives it from {@link grantCycle} as it\n * always did; absent with no `grantCycle` either, nothing is claimed and the\n * chip rests.\n *\n * It is not clamped. A consumer that passes a percentage where a fraction\n * belongs renders a visibly absurd pill rather than a plausible \"100%\", and a\n * wrong figure that looks wrong is the one that gets fixed.\n */\n burnedFraction?: number\n /**\n * When the current grant expires, preformatted for display, e.g. \"30 Sep\":\n * the day this month's grant runs out and the next one lands. The sidebar\n * card says it as an expiry (`labels.grantExpiry`), the top-bar popover as a\n * reset (`labels.grantedNote`); both are this one date.\n */\n grantExpires?: string\n /**\n * The threshold state's runway line, preformatted, e.g. \"About four days left\n * at your current burn.\" Passed in rather than derived: the burn estimate is\n * the product's to make, not the chip's -- the chip renders what it is given.\n */\n burnNote?: string\n}\n\n/**\n * The chip whose balance could not be confirmed. It renders an em dash, no bar\n * and no button, so **it takes no figures at all** -- each one below is\n * `?: never`.\n *\n * That is deliberate and it is the second half of B33-483. The figures were\n * required in every state, so both consumers manufactured zeroes to get past the\n * type and then apologised for them in a comment -- \"these zeroes are never\n * rendered ... but they are not a balance and must not be read as one\"\n * (`boostscore/components/app-shell/wallet-chip.tsx`), and the same again in\n * `wallet-slot.tsx` and `boostledger/src/app/(wallet)/layout.tsx`. A zero is a\n * confident claim about someone's money. The type now refuses it rather than\n * asking a comment to.\n *\n * `undefined` is still accepted, so a consumer that must build one props object\n * can write `{ unknown: true, granted: undefined }` instead of branching.\n */\nexport interface WalletChipUnknownProps extends WalletChipChromeProps {\n /** The balance could not be confirmed. Renders as unknown, never as zero. */\n unknown: true\n totalOnly?: false\n balance?: never\n granted?: never\n purchased?: never\n grantCycle?: never\n burnedFraction?: never\n /** Not rendered in this state: the footer only draws on a resting chip. */\n grantExpires?: never\n /** Not rendered in this state: the runway line only draws past the threshold. */\n burnNote?: never\n}\n\n/**\n * A discriminated union on `unknown`, and the discriminant has consequences for\n * how a consumer *types* its own code, not only what it passes:\n *\n * - A helper that returns props for both states needs an explicit\n * `: WalletChipProps` return annotation. Without one, TypeScript widens the\n * `unknown: true` in an object literal to `boolean`, which matches neither\n * member, and the spread at the call site stops compiling.\n * - `Omit<WalletChipProps, 'onTopUp'>` is not what it looks like. `Omit` over a\n * union flattens it into one object and loses the discrimination. Omit from\n * the members instead -- `Omit<WalletChipKnownProps, 'onTopUp'> |\n * Omit<WalletChipUnknownProps, 'onTopUp'>` -- which is why both are exported.\n */\nexport type WalletChipProps = WalletChipKnownProps | WalletChipUnknownProps | WalletChipTotalOnlyProps\n\n/**\n * The top-bar chip with a real total and no drawable split (B33-1712). Debits\n * draw the grant first, then purchased, and may run a set share past zero (the\n * Ledger's overdraft); credits add only to the bucket they name and never pay a\n * negative one off. So the Ledger can report a bucket below zero -- granted 0 /\n * purchased -40 (overdrawn, total -40), or granted 1000 / purchased -40 after\n * the month's grant lands (total 960, for the rest of the month). A two-segment\n * bar cannot draw a negative, and clamping it to zero would draw a split nobody\n * has: a split invented by the interface. So this state takes **the total and\n * no split**, and draws no bar and no granted / purchased legend.\n *\n * Below zero the figure is the distance past zero, positive, in --color-danger,\n * with the words \"past zero\" where a minus sign would be -- the Wallet's own\n * past-zero screen (boostledger, Paper 201 screen 03), followed rather than\n * inventing a third look. Nothing is broken and the copy does not say it is;\n * what happens next is the consumer's `burnNote`, because the overdraft limit is\n * the Ledger's. At zero or above it is the plain balance.\n *\n * **Top bar only.** The sidebar card has no drawing for this and the redesigned\n * shell does not render it, so the type requires `placement=\"topbar\"` rather\n * than leaving the card to guess. No board draws either form: they are new\n * undrawn states for the person at the gate to judge.\n */\nexport interface WalletChipTotalOnlyProps extends WalletChipChromeProps {\n placement: 'topbar'\n totalOnly: true\n unknown?: false\n /**\n * The balance, signed, in whole tokens: `granted + purchased` from the\n * Ledger's signed figures, which its single balance row guarantees is the\n * balance.\n */\n balance: number\n /**\n * What happens next, preformatted, e.g. \"Work carries on for another 930\n * tokens, then paid actions stop.\" The consumer's, because the overdraft limit\n * is the Ledger's, not the chip's.\n */\n burnNote?: string\n granted?: never\n purchased?: never\n grantCycle?: never\n burnedFraction?: never\n grantExpires?: never\n}\n\n/**\n * The granted/purchased bar. Granted is drawn first, because granted burns\n * first. The accessible label is passed in whole rather than assembled here:\n * word order is the consumer's, not English's (B33-849).\n */\nfunction Bar({ granted, purchased, cycleTotal, tone, label }: { granted: number; purchased: number; cycleTotal: number; tone: 'rest' | 'threshold'; label: string }) {\n return (\n <div className={styles.bar} data-tone={tone} role=\"img\" aria-label={label}>\n <span className={styles.segmentGranted} data-testid=\"segment-granted\" style={{ width: segmentWidth(granted, cycleTotal) }} />\n <span className={styles.segmentPurchased} data-testid=\"segment-purchased\" style={{ width: segmentWidth(purchased, cycleTotal) }} />\n </div>\n )\n}\n\nexport function WalletChip(props: WalletChipProps) {\n if (props.totalOnly === true) return <TotalOnlyChip {...props} />\n if (props.placement === 'topbar') return <TopBarChip {...props} />\n return <SidebarChip {...props} />\n}\n\n/**\n * The top-bar token chip (B33-1712). The pill shows the balance and the\n * two-segment bar; the popover (board 21) shows the figure, the bar again at\n * width, each kind of token with what it means, the runway line, and the buy\n * button.\n *\n * Both segments are always drawn and a zero one is 0px wide with the gap kept\n * -- board 22, and B33-590's rule that a zero is never drawn as a sliver. That\n * overrides B33-1712's line about copying the hub's 4px floor, by Jamal's\n * decision of 3 Nov 2026. A non-zero segment keeps the existing floor\n * (`segmentWidth`).\n *\n * Every figure is the consumer's. Unknown renders an em dash, no bar and no\n * breakdown -- a made-up split in a token balance is a commercial fact invented\n * by the interface (B33-1712).\n *\n * A disclosure, like the switcher (ADR-0010): the trigger is a button, the\n * panel carries no widget role, and dismissal is the shared anchored-overlay\n * contract's (ADR-0009).\n */\nfunction TopBarChip({ granted = 0, purchased = 0, grantCycle, burnedFraction, grantExpires, burnNote, unknown = false, onTopUp, labels, locale = DEFAULT_WALLET_LOCALE, walletHref, linkComponent, defaultOpen }: WalletChipProps) {\n const t = resolveWalletChipLabels(labels)\n const overlay = useAnchoredOverlay({ defaultOpen })\n const Link: ElementType = linkComponent ?? 'a'\n const balance = granted + purchased\n // The threshold, decided exactly as the sidebar card decides it: the\n // consumer's `burnedFraction` wins, `grantCycle` derives it otherwise, and\n // with neither nothing is claimed (B33-483). 0.8.0-next.0 dropped this from\n // the top bar and with it the only at-a-glance \"running low\" signal the\n // chip had (score-main, 4 Nov 2026); Jamal chose the sidebar's own drawing\n // to bring it back.\n const derivedBurn = grantCycle !== undefined && grantCycle > 0 ? (grantCycle - granted) / grantCycle : 0\n const burned = burnedFraction ?? derivedBurn\n const isThreshold = !unknown && burned >= THRESHOLD_BURN\n // At rest the bar draws the split of the balance itself, full width, with no\n // burned track (board 22): the top bar claims nothing about what the cycle\n // started with. Past the threshold, and only when `grantCycle` says what the\n // cycle started with, it measures against that instead -- the sidebar card's\n // drawing -- so the bar shortens and the burned share shows as the\n // warning-tinted track. With `burnedFraction` alone there is no honest\n // denominator, so the bar stays full and the popover line carries the warning.\n const total = isThreshold && grantCycle !== undefined ? grantCycle + purchased : balance\n const tone = isThreshold ? 'threshold' : undefined\n const figure = unknown ? '—' : formatTokens(balance, locale)\n const buildBarLabel = () => t.bar(formatTokens(granted, locale), formatTokens(purchased, locale), formatTokens(balance, locale))\n // Built only past the threshold, where it is drawn.\n const thresholdNote = isThreshold ? t.thresholdNote(formatPercent(burned, locale)) : undefined\n const triggerLabel = unknown ? t.railBalanceUnknown : thresholdNote ? `${t.railBalance(figure)}. ${thresholdNote}` : t.railBalance(figure)\n\n return (\n <div className={styles.topRoot} data-threshold={isThreshold ? 'true' : undefined}>\n <button type=\"button\" className={styles.topTrigger} aria-label={triggerLabel} onClick={overlay.toggle} {...overlay.triggerProps}>\n <span className={styles.topFigure} aria-hidden=\"true\">\n <span className={styles.topAmount}>{figure}</span>\n <span className={styles.topUnit}>{t.unit}</span>\n </span>\n {!unknown && <TopBar granted={granted} purchased={purchased} total={total} tone={tone} className={styles.topBar} />}\n <Chevron className={styles.topChevron} />\n </button>\n\n {overlay.open && (\n <div {...overlay.panelProps} className={styles.popover} aria-label={t.popover}>\n <div className={styles.popHead}>\n <span className={styles.popFigure}>\n <span className={styles.popAmount}>{figure}</span>\n <span className={styles.popUnit}>{t.unit}</span>\n </span>\n {walletHref !== undefined && (\n <Link className={buttons.quiet} href={walletHref} onClick={overlay.close}>\n {t.open}\n </Link>\n )}\n </div>\n\n {unknown ? (\n <p className={styles.popNote}>{t.balanceUnknown}</p>\n ) : (\n <>\n <TopBar granted={granted} purchased={purchased} total={total} tone={tone} className={styles.popBar} label={buildBarLabel()} />\n <ul className={styles.legend}>\n <li className={styles.legendRow}>\n <span className={styles.swatchGranted} aria-hidden=\"true\" />\n <span className={styles.legendText}>\n <span className={styles.legendFigure}>{t.grantedFigure(formatTokens(granted, locale))}</span>\n <span className={styles.legendNote}>{grantExpires ? t.grantedNote(grantExpires) : t.grantedNoteUndated}</span>\n </span>\n </li>\n <li className={styles.legendRow}>\n <span className={styles.swatchPurchased} aria-hidden=\"true\" />\n <span className={styles.legendText}>\n <span className={styles.legendFigure}>{t.purchasedFigure(formatTokens(purchased, locale))}</span>\n <span className={styles.legendNote}>{t.purchasedNote}</span>\n </span>\n </li>\n </ul>\n {(thresholdNote || burnNote || onTopUp !== undefined) && (\n <div className={styles.popFoot}>\n {thresholdNote && <p className={styles.popWarning}>{thresholdNote}</p>}\n {burnNote && <p className={styles.popNote}>{burnNote}</p>}\n {onTopUp !== undefined && (\n <button type=\"button\" className={`${buttons.primary} ${styles.buy}`} data-tone=\"accent\" onClick={onTopUp}>\n {t.buyTokens}\n </button>\n )}\n </div>\n )}\n </>\n )}\n </div>\n )}\n </div>\n )\n}\n\n/**\n * The top-bar chip with no drawable split (`WalletChipTotalOnlyProps`): the same\n * pill and popover as {@link TopBarChip}, with the total alone -- the distance\n * past zero when it is below -- and no bar and no legend anywhere.\n */\nfunction TotalOnlyChip({ balance, burnNote, onTopUp, labels, locale = DEFAULT_WALLET_LOCALE, walletHref, linkComponent, defaultOpen }: WalletChipTotalOnlyProps) {\n const t = resolveWalletChipLabels(labels)\n const overlay = useAnchoredOverlay({ defaultOpen })\n const Link: ElementType = linkComponent ?? 'a'\n const pastZero = balance < 0\n const figure = formatTokens(pastZero ? -balance : balance, locale)\n\n return (\n <div className={styles.topRoot} data-past-zero={pastZero ? 'true' : undefined}>\n <button type=\"button\" className={styles.topTrigger} aria-label={pastZero ? t.pastZeroBalance(figure) : t.railBalance(figure)} onClick={overlay.toggle} {...overlay.triggerProps}>\n <span className={styles.topFigure} aria-hidden=\"true\">\n <span className={styles.topAmount}>{figure}</span>\n <span className={styles.topUnit}>{pastZero ? t.pastZero : t.unit}</span>\n </span>\n <Chevron className={styles.topChevron} />\n </button>\n\n {overlay.open && (\n <div {...overlay.panelProps} className={styles.popover} aria-label={t.popover}>\n <div className={styles.popHead}>\n <span className={styles.popFigure}>\n <span className={styles.popAmount}>{figure}</span>\n <span className={styles.popUnit}>{pastZero ? t.pastZeroLong : t.unit}</span>\n </span>\n {walletHref !== undefined && (\n <Link className={buttons.quiet} href={walletHref} onClick={overlay.close}>\n {t.open}\n </Link>\n )}\n </div>\n {(burnNote || onTopUp !== undefined) && (\n <div className={styles.popFoot}>\n {burnNote && <p className={styles.popNote}>{burnNote}</p>}\n {onTopUp !== undefined && (\n <button type=\"button\" className={`${buttons.primary} ${styles.buy}`} data-tone=\"accent\" onClick={onTopUp}>\n {t.buyTokens}\n </button>\n )}\n </div>\n )}\n </div>\n )}\n </div>\n )\n}\n\n/**\n * The top bar's two-segment bar, in the pill and in the popover. Granted first,\n * because it burns first; a fixed gap between the two that stays when a segment\n * is zero, so a two-part balance never reads as one lump (board 22).\n */\nfunction TopBar({ granted, purchased, total, tone, className, label }: { granted: number; purchased: number; total: number; tone?: 'threshold'; className?: string; label?: string }) {\n return (\n <span className={className} data-tone={tone} role={label ? 'img' : undefined} aria-label={label} aria-hidden={label ? undefined : 'true'}>\n <span className={styles.segmentGranted} data-segment=\"granted\" style={{ width: segmentWidth(granted, total) }} />\n <span className={styles.segmentPurchased} data-segment=\"purchased\" style={{ width: segmentWidth(purchased, total) }} />\n </span>\n )\n}\n\nfunction SidebarChip({ granted = 0, purchased = 0, grantCycle, burnedFraction, grantExpires, burnNote, unknown = false, collapsed = false, onTopUp, labels, locale = DEFAULT_WALLET_LOCALE }: WalletChipProps) {\n const t = resolveWalletChipLabels(labels)\n // Assembled lazily: the unknown state draws no bar, and a consumer's `bar`\n // implementation can throw on the zeroes the destructure above defaults the\n // degraded path's absent figures to. Calling it only where a bar renders keeps\n // that crash surface off the render that exists precisely because fetching the\n // balance already failed (B33-875). B33-483 stopped the consumer having to pass\n // those zeroes; the chip still has to pick something, so the laziness stays.\n const buildBarLabel = () =>\n t.bar(formatTokens(granted, locale), formatTokens(purchased, locale), formatTokens(granted + purchased, locale))\n const balance = granted + purchased\n // Everything the cycle started with: the grant plus the purchased balance\n // sitting alongside it. Measuring against this is what makes the bar shorten\n // as tokens burn instead of only re-proportioning to a full width.\n //\n // With no `grantCycle` the denominator falls back to `granted + purchased`, so\n // the bar fills and there is no burned track -- the honest drawing when nobody\n // can say what the cycle started with (B33-483). The zero-division guard inside\n // segmentWidth still covers the case where that balance is itself zero.\n const cycleTotal = (grantCycle ?? granted) + purchased\n // How much of the cycle's grant has burned. The consumer's own figure wins;\n // `grantCycle` derives it exactly as it always did; with neither, nothing is\n // claimed and the chip rests. `grantCycle` is tested against undefined rather\n // than leaned on for falsiness, because 0 and \"not supplied\" both resolve to a\n // burn of 0 and must keep doing so.\n const derivedBurn = grantCycle !== undefined && grantCycle > 0 ? (grantCycle - granted) / grantCycle : 0\n const burned = burnedFraction ?? derivedBurn\n const isThreshold = !unknown && burned >= THRESHOLD_BURN\n\n if (collapsed) {\n // The abbreviated balance, assembled once: it is both the rail's visible\n // text and the figure the group's accessible name is built around, and a\n // sighted user and a screen-reader user must not be told different numbers.\n // Built only when a balance is known, for the same reason buildBarLabel is\n // lazy -- the unknown rail shows no figure, and a consumer's railFigure\n // should not run on a degraded path it never renders on (B33-875).\n const railBalance = unknown ? undefined : t.railFigure(thousandsFigure(balance, locale))\n return (\n <div className={styles.rail} role=\"group\" aria-label={railBalance === undefined ? t.railBalanceUnknown : t.railBalance(railBalance)} tabIndex={0}>\n <WalletMark className={styles.railIcon} />\n <span className={styles.railFigure}>{railBalance ?? '—'}</span>\n {unknown ? <span className={styles.srOnly}>{t.railUnknown}</span> : <Bar granted={granted} purchased={purchased} cycleTotal={cycleTotal} tone={isThreshold ? 'threshold' : 'rest'} label={buildBarLabel()} />}\n </div>\n )\n }\n\n return (\n <div className={styles.chip} role=\"group\" aria-label={t.region} tabIndex={0} data-threshold={isThreshold ? 'true' : undefined} data-unknown={unknown ? 'true' : undefined}>\n <div className={styles.header}>\n <span className={styles.name}>{t.name}</span>\n {isThreshold && <span className={styles.pill}>{Math.round(burned * 100)}%</span>}\n </div>\n\n {unknown ? (\n <div className={styles.figure}>\n <span className={styles.amount} aria-hidden=\"true\">\n {'—'}\n </span>\n <RetryMark className={styles.retry} />\n <span className={styles.srOnly}>{t.balanceUnknown}</span>\n </div>\n ) : (\n <div className={styles.figure}>\n <span className={styles.amount}>{formatTokens(balance, locale)}</span>\n <span className={styles.unit}>{t.unit}</span>\n </div>\n )}\n\n {unknown ? (\n <div className={styles.bar} data-tone=\"unknown\" aria-hidden=\"true\" />\n ) : (\n <Bar granted={granted} purchased={purchased} cycleTotal={cycleTotal} tone={isThreshold ? 'threshold' : 'rest'} label={buildBarLabel()} />\n )}\n\n {!unknown && !isThreshold && (\n <div className={styles.details}>\n <dl className={styles.split}>\n <div className={styles.splitRow}>\n <dt className={styles.splitLabel}>\n <span className={styles.dotGranted} aria-hidden=\"true\" />\n {t.granted}\n </dt>\n <dd className={styles.splitAmount}>{formatTokens(granted, locale)}</dd>\n </div>\n <div className={styles.splitRow}>\n <dt className={styles.splitLabel}>\n <span className={styles.dotPurchased} aria-hidden=\"true\" />\n {t.purchased}\n </dt>\n <dd className={styles.splitAmount}>{formatTokens(purchased, locale)}</dd>\n </div>\n </dl>\n {grantExpires && <p className={styles.footer}>{t.grantExpiry(grantExpires)}</p>}\n </div>\n )}\n\n {isThreshold && burnNote && <p className={styles.burnNote}>{burnNote}</p>}\n\n {/* A handler is what makes the button exist, not only what it does when\n pressed (B33-1270). `!unknown` alone drew it for a consumer with\n nowhere to send anyone, and a hashed class name left them no way to\n take it back out -- see `onTopUp`. Tested against `undefined` rather\n than for truthiness so that `onTopUp={undefined}`, which is what a\n consumer's own absent value looks like, reads the same as omitting it. */}\n {!unknown && onTopUp !== undefined && (\n <button\n type=\"button\"\n // Two tiers for one button (B33-1713, ADR-0016): at rest it is a word\n // in the chip's header, the quiet tier; past the threshold it grows\n // into the chip's one call to action, the primary tier in the accent\n // tone. `.topUp` adds only where it sits.\n className={`${isThreshold ? buttons.primary : buttons.quiet} ${styles.topUp}`}\n data-tone={isThreshold ? 'accent' : undefined}\n data-grown={isThreshold ? 'true' : undefined}\n onClick={onTopUp}\n >\n {t.topUp}\n </button>\n )}\n </div>\n )\n}\n"],"names":["THRESHOLD_BURN","SEGMENT_MIN","segmentWidth","amount","cycleTotal","share","Bar","granted","purchased","tone","label","jsxs","styles","jsx","WalletChip","props","TotalOnlyChip","TopBarChip","SidebarChip","grantCycle","burnedFraction","grantExpires","burnNote","unknown","onTopUp","labels","locale","DEFAULT_WALLET_LOCALE","walletHref","linkComponent","defaultOpen","t","resolveWalletChipLabels","overlay","useAnchoredOverlay","Link","balance","derivedBurn","burned","isThreshold","total","figure","formatTokens","buildBarLabel","thresholdNote","formatPercent","triggerLabel","TopBar","Chevron","buttons","Fragment","pastZero","className","collapsed","railBalance","thousandsFigure","WalletMark","RetryMark"],"mappings":"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"}
@@ -1,102 +1,104 @@
1
- const t = "_chip_qvtyk_37", o = "_header_qvtyk_74", _ = "_name_qvtyk_83", e = "_pill_qvtyk_162", n = "_figure_qvtyk_171", p = "_amount_qvtyk_186", s = "_unit_qvtyk_195", r = "_retry_qvtyk_200", c = "_bar_qvtyk_209", a = "_segmentGranted_qvtyk_230", y = "_segmentPurchased_qvtyk_231", i = "_details_qvtyk_275", v = "_split_qvtyk_284", l = "_splitRow_qvtyk_291", d = "_splitLabel_qvtyk_297", k = "_splitAmount_qvtyk_305", q = "_dotGranted_qvtyk_313", u = "_dotPurchased_qvtyk_314", g = "_footer_qvtyk_328", h = "_burnNote_qvtyk_336", m = "_topUp_qvtyk_87", F = "_rail_qvtyk_387", b = "_railIcon_qvtyk_401", w = "_railFigure_qvtyk_407", A = "_srOnly_qvtyk_420", G = "_topRoot_qvtyk_444", N = "_topTrigger_qvtyk_453", P = "_topFigure_qvtyk_482", R = "_topAmount_qvtyk_489", U = "_topUnit_qvtyk_497", f = "_topChevron_qvtyk_508", B = "_topBar_qvtyk_518", T = "_popBar_qvtyk_519", x = "_popover_qvtyk_534", C = "_popHead_qvtyk_551", H = "_popFigure_qvtyk_558", I = "_popAmount_qvtyk_566", L = "_popUnit_qvtyk_575", O = "_legend_qvtyk_589", j = "_legendText_qvtyk_598", z = "_legendRow_qvtyk_603", D = "_swatchGranted_qvtyk_609", E = "_swatchPurchased_qvtyk_610", J = "_legendFigure_qvtyk_626", K = "_legendNote_qvtyk_634", M = "_popFoot_qvtyk_641", Q = "_popNote_qvtyk_649", S = "_buy_qvtyk_658", V = {
1
+ const t = "_chip_d975i_37", o = "_header_d975i_74", _ = "_name_d975i_83", e = "_pill_d975i_162", n = "_figure_d975i_171", i = "_amount_d975i_186", p = "_unit_d975i_195", d = "_retry_d975i_200", s = "_bar_d975i_209", r = "_segmentGranted_d975i_230", c = "_segmentPurchased_d975i_231", a = "_details_d975i_275", l = "_split_d975i_284", u = "_splitRow_d975i_291", g = "_splitLabel_d975i_297", h = "_splitAmount_d975i_305", m = "_dotGranted_d975i_313", F = "_dotPurchased_d975i_314", b = "_footer_d975i_328", w = "_burnNote_d975i_336", y = "_topUp_d975i_87", A = "_rail_d975i_387", G = "_railIcon_d975i_401", N = "_railFigure_d975i_407", P = "_srOnly_d975i_420", R = "_topRoot_d975i_444", U = "_topTrigger_d975i_453", f = "_topFigure_d975i_482", v = "_topAmount_d975i_489", B = "_topUnit_d975i_497", T = "_topChevron_d975i_508", x = "_topBar_d975i_518", C = "_popBar_d975i_519", H = "_popover_d975i_534", I = "_popHead_d975i_551", L = "_popFigure_d975i_558", O = "_popAmount_d975i_566", W = "_popUnit_d975i_575", j = "_legend_d975i_589", k = "_legendText_d975i_598", q = "_legendRow_d975i_603", z = "_swatchGranted_d975i_609", D = "_swatchPurchased_d975i_610", E = "_legendFigure_d975i_626", J = "_legendNote_d975i_634", K = "_popFoot_d975i_641", M = "_popNote_d975i_649", Q = "_popWarning_d975i_659", S = "_buy_d975i_678", V = {
2
2
  chip: t,
3
3
  header: o,
4
4
  name: _,
5
5
  pill: e,
6
6
  figure: n,
7
- amount: p,
8
- unit: s,
9
- retry: r,
10
- bar: c,
11
- segmentGranted: a,
12
- segmentPurchased: y,
13
- details: i,
14
- split: v,
15
- splitRow: l,
16
- splitLabel: d,
17
- splitAmount: k,
18
- dotGranted: q,
19
- dotPurchased: u,
20
- footer: g,
21
- burnNote: h,
22
- topUp: m,
23
- rail: F,
24
- railIcon: b,
25
- railFigure: w,
26
- srOnly: A,
27
- topRoot: G,
28
- topTrigger: N,
29
- topFigure: P,
30
- topAmount: R,
31
- topUnit: U,
32
- topChevron: f,
33
- topBar: B,
34
- popBar: T,
35
- popover: x,
36
- popHead: C,
37
- popFigure: H,
38
- popAmount: I,
39
- popUnit: L,
40
- legend: O,
41
- legendText: j,
42
- legendRow: z,
43
- swatchGranted: D,
44
- swatchPurchased: E,
45
- legendFigure: J,
46
- legendNote: K,
47
- popFoot: M,
48
- popNote: Q,
7
+ amount: i,
8
+ unit: p,
9
+ retry: d,
10
+ bar: s,
11
+ segmentGranted: r,
12
+ segmentPurchased: c,
13
+ details: a,
14
+ split: l,
15
+ splitRow: u,
16
+ splitLabel: g,
17
+ splitAmount: h,
18
+ dotGranted: m,
19
+ dotPurchased: F,
20
+ footer: b,
21
+ burnNote: w,
22
+ topUp: y,
23
+ rail: A,
24
+ railIcon: G,
25
+ railFigure: N,
26
+ srOnly: P,
27
+ topRoot: R,
28
+ topTrigger: U,
29
+ topFigure: f,
30
+ topAmount: v,
31
+ topUnit: B,
32
+ topChevron: T,
33
+ topBar: x,
34
+ popBar: C,
35
+ popover: H,
36
+ popHead: I,
37
+ popFigure: L,
38
+ popAmount: O,
39
+ popUnit: W,
40
+ legend: j,
41
+ legendText: k,
42
+ legendRow: q,
43
+ swatchGranted: z,
44
+ swatchPurchased: D,
45
+ legendFigure: E,
46
+ legendNote: J,
47
+ popFoot: K,
48
+ popNote: M,
49
+ popWarning: Q,
49
50
  buy: S
50
51
  };
51
52
  export {
52
- p as amount,
53
- c as bar,
54
- h as burnNote,
53
+ i as amount,
54
+ s as bar,
55
+ w as burnNote,
55
56
  S as buy,
56
57
  t as chip,
57
58
  V as default,
58
- i as details,
59
- q as dotGranted,
60
- u as dotPurchased,
59
+ a as details,
60
+ m as dotGranted,
61
+ F as dotPurchased,
61
62
  n as figure,
62
- g as footer,
63
+ b as footer,
63
64
  o as header,
64
- O as legend,
65
- J as legendFigure,
66
- K as legendNote,
67
- z as legendRow,
68
- j as legendText,
65
+ j as legend,
66
+ E as legendFigure,
67
+ J as legendNote,
68
+ q as legendRow,
69
+ k as legendText,
69
70
  _ as name,
70
71
  e as pill,
71
- I as popAmount,
72
- T as popBar,
73
- H as popFigure,
74
- M as popFoot,
75
- C as popHead,
76
- Q as popNote,
77
- L as popUnit,
78
- x as popover,
79
- F as rail,
80
- w as railFigure,
81
- b as railIcon,
82
- r as retry,
83
- a as segmentGranted,
84
- y as segmentPurchased,
85
- v as split,
86
- k as splitAmount,
87
- d as splitLabel,
88
- l as splitRow,
89
- A as srOnly,
90
- D as swatchGranted,
91
- E as swatchPurchased,
92
- R as topAmount,
93
- B as topBar,
94
- f as topChevron,
95
- P as topFigure,
96
- G as topRoot,
97
- N as topTrigger,
98
- U as topUnit,
99
- m as topUp,
100
- s as unit
72
+ O as popAmount,
73
+ C as popBar,
74
+ L as popFigure,
75
+ K as popFoot,
76
+ I as popHead,
77
+ M as popNote,
78
+ W as popUnit,
79
+ Q as popWarning,
80
+ H as popover,
81
+ A as rail,
82
+ N as railFigure,
83
+ G as railIcon,
84
+ d as retry,
85
+ r as segmentGranted,
86
+ c as segmentPurchased,
87
+ l as split,
88
+ h as splitAmount,
89
+ g as splitLabel,
90
+ u as splitRow,
91
+ P as srOnly,
92
+ z as swatchGranted,
93
+ D as swatchPurchased,
94
+ v as topAmount,
95
+ x as topBar,
96
+ T as topChevron,
97
+ f as topFigure,
98
+ R as topRoot,
99
+ U as topTrigger,
100
+ B as topUnit,
101
+ y as topUp,
102
+ p as unit
101
103
  };
102
104
  //# sourceMappingURL=WalletChip.module.css.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"WalletChip.module.css.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"WalletChip.module.css.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -13,6 +13,12 @@ export declare const DEFAULT_WALLET_LOCALE = "en-US";
13
13
  * be cached.
14
14
  */
15
15
  export declare function formatTokens(value: number, locale?: string): string;
16
+ /**
17
+ * A burned share as a whole percentage in the locale's own form: 0.87 ->
18
+ * `"87%"` in `en-US`, `"87 %"` in `fr-FR` (with the narrow no-break space CLDR
19
+ * puts there). Used by the top-bar chip's threshold line.
20
+ */
21
+ export declare function formatPercent(fraction: number, locale?: string): string;
16
22
  /**
17
23
  * The rail's short form: one decimal thousand, always. At 56px there is no room
18
24
  * for four digits and a separator, and "4.6k" still answers "am I fine?".
@@ -1,29 +1,34 @@
1
- const e = "en-US", i = {
1
+ const r = "en-US", m = {
2
2
  grouped: {},
3
- oneDecimal: { minimumFractionDigits: 1, maximumFractionDigits: 1 }
4
- }, m = /* @__PURE__ */ new Map(), s = /* @__PURE__ */ new Set();
5
- function c(t, n) {
6
- const o = `${n}|${t}`, a = m.get(o);
7
- if (a) return a;
8
- let r;
3
+ oneDecimal: { minimumFractionDigits: 1, maximumFractionDigits: 1 },
4
+ percent: { style: "percent", maximumFractionDigits: 0 }
5
+ }, c = /* @__PURE__ */ new Map(), s = /* @__PURE__ */ new Set();
6
+ function o(t, n) {
7
+ const a = `${n}|${t}`, i = c.get(a);
8
+ if (i) return i;
9
+ let e;
9
10
  try {
10
- r = new Intl.NumberFormat(t, i[n]);
11
+ e = new Intl.NumberFormat(t, m[n]);
11
12
  } catch {
12
13
  s.has(t) || (s.add(t), console.warn(
13
- `@boost33/ui: WalletChip cannot format numbers in locale ${JSON.stringify(t)}; falling back to ${e}.`
14
- )), r = new Intl.NumberFormat(e, i[n]);
14
+ `@boost33/ui: WalletChip cannot format numbers in locale ${JSON.stringify(t)}; falling back to ${r}.`
15
+ )), e = new Intl.NumberFormat(r, m[n]);
15
16
  }
16
- return m.set(o, r), r;
17
+ return c.set(a, e), e;
17
18
  }
18
- function u(t, n = e) {
19
- return c(n, "grouped").format(t);
19
+ function u(t, n = r) {
20
+ return o(n, "grouped").format(t);
20
21
  }
21
- function f(t, n = e) {
22
- return c(n, "oneDecimal").format(t / 1e3);
22
+ function f(t, n = r) {
23
+ return o(n, "percent").format(t);
24
+ }
25
+ function g(t, n = r) {
26
+ return o(n, "oneDecimal").format(t / 1e3);
23
27
  }
24
28
  export {
25
- e as DEFAULT_WALLET_LOCALE,
29
+ r as DEFAULT_WALLET_LOCALE,
30
+ f as formatPercent,
26
31
  u as formatTokens,
27
- f as thousandsFigure
32
+ g as thousandsFigure
28
33
  };
29
34
  //# sourceMappingURL=format.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"format.js","sources":["../../../src/components/WalletChip/format.ts"],"sourcesContent":["// The two numbers the chip says out loud. Kept separate from the component so\n// the rounding and the separator -- the parts that carry meaning rather than\n// look -- are unit-tested without a render.\n//\n// ---------------------------------------------------------------------------\n// B33-876: why a `locale` string, and not consumer-supplied formatters\n// ---------------------------------------------------------------------------\n//\n// This module used to hardcode `toLocaleString('en-US')` and a literal `k`. The\n// first two consumers are French-default, and French writes `4 600` where this\n// wrote `4,600`, and `4,6 k` where this wrote `4.6k` -- in the largest glyphs on\n// the chip, and inside the `bar` and `railBalance` accessible names. `labels`\n// could not reach any of it: the label functions receive the figures *already\n// formatted*, by documented intent, so a consumer had `\"4,600 jetons\"` and no\n// hook.\n//\n// The ticket named two candidate shapes and picked neither: a `locale` prop, or\n// `formatTokens` / `abbreviateThousands` supplied by the consumer. The shape\n// chosen is **a `locale` prop** -- one BCP-47 language tag, defaulting to\n// `en-US`, driving `Intl.NumberFormat`. Written down here, prominently, because\n// it is a published API decision reaching four products on a version bump and\n// reversing it later should be cheap.\n//\n// 1. **A formatter prop is a function, and a function cannot cross the React\n// Server Component boundary.** B33-880 had just finished taking the functions\n// *out* of `labels` for exactly this reason: `AppShell.wallet` is a\n// `ReactNode` slot, so it is the consumer's own page -- under the App Router,\n// a Server Component by default -- that constructs `<WalletChip …/>`, and\n// React's Flight serializer throws on any function anywhere in the prop\n// graph, in production as well as in development. An API of injected\n// formatters would reintroduce that bug the same week it was fixed, and\n// reintroduce it as the *recommended* path. A language tag is a string: data\n// all the way down, and it survives the boundary intact.\n//\n// 2. **Numbers are not words.** `labels` takes whole sentences because word\n// order is not a property of English and cannot be reassembled from\n// fragments -- that is a fact about a consumer's sentence, which no library\n// can know. Grouping and the decimal separator are the opposite: they are\n// CLDR facts about a locale, the platform ships them for every locale, and\n// asking four products to each re-implement French grouping is asking four\n// products to each get it slightly wrong. So \"the `labels` prop is already\n// the injected shape, be consistent\" reverses once you read *why* `labels`\n// took that shape.\n//\n// 3. **It is one string with a default**, and a consumer that passes nothing\n// renders character-for-character what the chip rendered before the prop\n// existed. The five `walletchip-*` baselines cannot move.\n//\n// What `locale` deliberately does not own is the `k`. See `thousandsFigure`.\n\n/**\n * The language tag the chip formats in when the consumer names none. Today's\n * rendering, so the default path is byte-identical to the one the fidelity\n * baselines were captured from.\n */\nexport const DEFAULT_WALLET_LOCALE = 'en-US'\n\n/**\n * The two shapes a figure is asked for: the full balance, grouped; and one\n * decimal of a thousand for the rail.\n */\ntype FormatterKind = 'grouped' | 'oneDecimal'\n\nconst FORMATTER_OPTIONS: Record<FormatterKind, Intl.NumberFormatOptions> = {\n grouped: {},\n oneDecimal: { minimumFractionDigits: 1, maximumFractionDigits: 1 },\n}\n\n/**\n * `Intl.NumberFormat` instances, keyed by kind and tag.\n *\n * Constructing one is not free and the chip formats up to four figures per\n * render, on a component that sits in the sidebar of every screen. The set of\n * locales an app uses is small and long-lived, so a plain module-level Map is\n * the whole of the caching this needs.\n */\nconst formatters = new Map<string, Intl.NumberFormat>()\n\n/**\n * The tags already warned about, so a bad one warns once rather than on every\n * render of every chip.\n */\nconst warnedLocales = new Set<string>()\n\n/**\n * An `Intl.NumberFormat` for this tag, falling back to English if the tag is\n * malformed.\n *\n * `Intl.NumberFormat` throws a `RangeError` on a structurally invalid language\n * tag (`'en_US'`, `'français'`), and this chip sits in the sidebar of four\n * products: one consumer's typo in a config file would take the whole shell down\n * rather than mis-space a number. A tag the platform refuses degrades to the\n * English default and warns the developer -- the same instinct as B33-873, where\n * a missing translation key falls back to English instead of blanking the\n * header. The warning is `console`-only: developer-facing, never rendered, never\n * translated.\n *\n * An unknown-but-well-formed tag (`'zz-ZZ'`) does not throw; the platform\n * resolves it to its own best match, which is its business and not this\n * package's.\n */\nfunction formatterFor(locale: string, kind: FormatterKind): Intl.NumberFormat {\n const key = `${kind}|${locale}`\n const cached = formatters.get(key)\n if (cached) return cached\n\n let formatter: Intl.NumberFormat\n try {\n formatter = new Intl.NumberFormat(locale, FORMATTER_OPTIONS[kind])\n } catch {\n if (!warnedLocales.has(locale)) {\n warnedLocales.add(locale)\n console.warn(\n `@boost33/ui: WalletChip cannot format numbers in locale ${JSON.stringify(locale)}; falling back to ${DEFAULT_WALLET_LOCALE}.`,\n )\n }\n formatter = new Intl.NumberFormat(DEFAULT_WALLET_LOCALE, FORMATTER_OPTIONS[kind])\n }\n formatters.set(key, formatter)\n return formatter\n}\n\n/**\n * The full balance, thousands grouped the way the locale groups them:\n * 4600 -> `\"4,600\"` in `en-US`, `\"4 600\"` in `fr-FR`, `\"4.600\"` in `de-DE`.\n *\n * The `en-US` default is what `toLocaleString('en-US')` produced, character for\n * character -- it is the same CLDR data reached through the constructor that can\n * be cached.\n */\nexport function formatTokens(value: number, locale: string = DEFAULT_WALLET_LOCALE): string {\n return formatterFor(locale, 'grouped').format(value)\n}\n\n/**\n * The rail's short form: one decimal thousand, always. At 56px there is no room\n * for four digits and a separator, and \"4.6k\" still answers \"am I fine?\".\n *\n * Returns **the figure alone** -- `\"4.6\"` in `en-US`, `\"4,6\"` in `fr-FR`. The\n * mark that follows it is a word rather than a number (\"k\" in English, \"Tsd.\" in\n * German, \"ألف\" in Arabic), and French puts a space in front of it where English\n * does not, so it lives with the chip's other words, as `labels.railFigure`\n * (B33-876).\n *\n * Not `Intl.NumberFormat`'s own `notation: 'compact'`, for two concrete reasons.\n * It renders `en-US` as `\"4.6K\"`, a capital the baselines do not have. And\n * German does not abbreviate four digits at all, so it renders `\"4600,0\"` -- six\n * characters and a decimal, in a 56px rail, longer than the number it was\n * supposed to shorten. One decimal thousand is this package's decision about a\n * fixed-width rail, not CLDR's about a locale; the separator inside it is CLDR's,\n * and that is the part `locale` fixes.\n *\n * Two default-path differences from the `(value / 1000).toFixed(1)` this\n * replaced, both narrower than any state the baselines hold and both left\n * deliberately, but written down because they reach four products (B33-876):\n *\n * - **A balance landing on an exact half rounds the other way.** `toFixed`\n * rounds the double's exact binary expansion, so 150 tokens printed `0.1`;\n * `Intl` rounds the shortest decimal the double stands for, so it prints\n * `0.2`. `Intl` agrees with the reader here, and the old answer was a float\n * artefact rather than a decision.\n * - **A balance over a million is grouped.** `toFixed` gave `1234.6`, this\n * gives `1,234.6`. Neither fits a 56px rail; that is a rail question, not a\n * formatting one.\n */\nexport function thousandsFigure(value: number, locale: string = DEFAULT_WALLET_LOCALE): string {\n return formatterFor(locale, 'oneDecimal').format(value / 1000)\n}\n"],"names":["DEFAULT_WALLET_LOCALE","FORMATTER_OPTIONS","formatters","warnedLocales","formatterFor","locale","kind","key","cached","formatter","formatTokens","value","thousandsFigure"],"mappings":"AAuDO,MAAMA,IAAwB,SAQ/BC,IAAqE;AAAA,EACzE,SAAS,CAAA;AAAA,EACT,YAAY,EAAE,uBAAuB,GAAG,uBAAuB,EAAA;AACjE,GAUMC,wBAAiB,IAAA,GAMjBC,wBAAoB,IAAA;AAmB1B,SAASC,EAAaC,GAAgBC,GAAwC;AAC5E,QAAMC,IAAM,GAAGD,CAAI,IAAID,CAAM,IACvBG,IAASN,EAAW,IAAIK,CAAG;AACjC,MAAIC,EAAQ,QAAOA;AAEnB,MAAIC;AACJ,MAAI;AACF,IAAAA,IAAY,IAAI,KAAK,aAAaJ,GAAQJ,EAAkBK,CAAI,CAAC;AAAA,EACnE,QAAQ;AACN,IAAKH,EAAc,IAAIE,CAAM,MAC3BF,EAAc,IAAIE,CAAM,GACxB,QAAQ;AAAA,MACN,2DAA2D,KAAK,UAAUA,CAAM,CAAC,qBAAqBL,CAAqB;AAAA,IAAA,IAG/HS,IAAY,IAAI,KAAK,aAAaT,GAAuBC,EAAkBK,CAAI,CAAC;AAAA,EAClF;AACA,SAAAJ,EAAW,IAAIK,GAAKE,CAAS,GACtBA;AACT;AAUO,SAASC,EAAaC,GAAeN,IAAiBL,GAA+B;AAC1F,SAAOI,EAAaC,GAAQ,SAAS,EAAE,OAAOM,CAAK;AACrD;AAiCO,SAASC,EAAgBD,GAAeN,IAAiBL,GAA+B;AAC7F,SAAOI,EAAaC,GAAQ,YAAY,EAAE,OAAOM,IAAQ,GAAI;AAC/D;"}
1
+ {"version":3,"file":"format.js","sources":["../../../src/components/WalletChip/format.ts"],"sourcesContent":["// The two numbers the chip says out loud. Kept separate from the component so\n// the rounding and the separator -- the parts that carry meaning rather than\n// look -- are unit-tested without a render.\n//\n// ---------------------------------------------------------------------------\n// B33-876: why a `locale` string, and not consumer-supplied formatters\n// ---------------------------------------------------------------------------\n//\n// This module used to hardcode `toLocaleString('en-US')` and a literal `k`. The\n// first two consumers are French-default, and French writes `4 600` where this\n// wrote `4,600`, and `4,6 k` where this wrote `4.6k` -- in the largest glyphs on\n// the chip, and inside the `bar` and `railBalance` accessible names. `labels`\n// could not reach any of it: the label functions receive the figures *already\n// formatted*, by documented intent, so a consumer had `\"4,600 jetons\"` and no\n// hook.\n//\n// The ticket named two candidate shapes and picked neither: a `locale` prop, or\n// `formatTokens` / `abbreviateThousands` supplied by the consumer. The shape\n// chosen is **a `locale` prop** -- one BCP-47 language tag, defaulting to\n// `en-US`, driving `Intl.NumberFormat`. Written down here, prominently, because\n// it is a published API decision reaching four products on a version bump and\n// reversing it later should be cheap.\n//\n// 1. **A formatter prop is a function, and a function cannot cross the React\n// Server Component boundary.** B33-880 had just finished taking the functions\n// *out* of `labels` for exactly this reason: `AppShell.wallet` is a\n// `ReactNode` slot, so it is the consumer's own page -- under the App Router,\n// a Server Component by default -- that constructs `<WalletChip …/>`, and\n// React's Flight serializer throws on any function anywhere in the prop\n// graph, in production as well as in development. An API of injected\n// formatters would reintroduce that bug the same week it was fixed, and\n// reintroduce it as the *recommended* path. A language tag is a string: data\n// all the way down, and it survives the boundary intact.\n//\n// 2. **Numbers are not words.** `labels` takes whole sentences because word\n// order is not a property of English and cannot be reassembled from\n// fragments -- that is a fact about a consumer's sentence, which no library\n// can know. Grouping and the decimal separator are the opposite: they are\n// CLDR facts about a locale, the platform ships them for every locale, and\n// asking four products to each re-implement French grouping is asking four\n// products to each get it slightly wrong. So \"the `labels` prop is already\n// the injected shape, be consistent\" reverses once you read *why* `labels`\n// took that shape.\n//\n// 3. **It is one string with a default**, and a consumer that passes nothing\n// renders character-for-character what the chip rendered before the prop\n// existed. The five `walletchip-*` baselines cannot move.\n//\n// What `locale` deliberately does not own is the `k`. See `thousandsFigure`.\n\n/**\n * The language tag the chip formats in when the consumer names none. Today's\n * rendering, so the default path is byte-identical to the one the fidelity\n * baselines were captured from.\n */\nexport const DEFAULT_WALLET_LOCALE = 'en-US'\n\n/**\n * The two shapes a figure is asked for: the full balance, grouped; and one\n * decimal of a thousand for the rail.\n */\ntype FormatterKind = 'grouped' | 'oneDecimal' | 'percent'\n\nconst FORMATTER_OPTIONS: Record<FormatterKind, Intl.NumberFormatOptions> = {\n grouped: {},\n oneDecimal: { minimumFractionDigits: 1, maximumFractionDigits: 1 },\n percent: { style: 'percent', maximumFractionDigits: 0 },\n}\n\n/**\n * `Intl.NumberFormat` instances, keyed by kind and tag.\n *\n * Constructing one is not free and the chip formats up to four figures per\n * render, on a component that sits in the sidebar of every screen. The set of\n * locales an app uses is small and long-lived, so a plain module-level Map is\n * the whole of the caching this needs.\n */\nconst formatters = new Map<string, Intl.NumberFormat>()\n\n/**\n * The tags already warned about, so a bad one warns once rather than on every\n * render of every chip.\n */\nconst warnedLocales = new Set<string>()\n\n/**\n * An `Intl.NumberFormat` for this tag, falling back to English if the tag is\n * malformed.\n *\n * `Intl.NumberFormat` throws a `RangeError` on a structurally invalid language\n * tag (`'en_US'`, `'français'`), and this chip sits in the sidebar of four\n * products: one consumer's typo in a config file would take the whole shell down\n * rather than mis-space a number. A tag the platform refuses degrades to the\n * English default and warns the developer -- the same instinct as B33-873, where\n * a missing translation key falls back to English instead of blanking the\n * header. The warning is `console`-only: developer-facing, never rendered, never\n * translated.\n *\n * An unknown-but-well-formed tag (`'zz-ZZ'`) does not throw; the platform\n * resolves it to its own best match, which is its business and not this\n * package's.\n */\nfunction formatterFor(locale: string, kind: FormatterKind): Intl.NumberFormat {\n const key = `${kind}|${locale}`\n const cached = formatters.get(key)\n if (cached) return cached\n\n let formatter: Intl.NumberFormat\n try {\n formatter = new Intl.NumberFormat(locale, FORMATTER_OPTIONS[kind])\n } catch {\n if (!warnedLocales.has(locale)) {\n warnedLocales.add(locale)\n console.warn(\n `@boost33/ui: WalletChip cannot format numbers in locale ${JSON.stringify(locale)}; falling back to ${DEFAULT_WALLET_LOCALE}.`,\n )\n }\n formatter = new Intl.NumberFormat(DEFAULT_WALLET_LOCALE, FORMATTER_OPTIONS[kind])\n }\n formatters.set(key, formatter)\n return formatter\n}\n\n/**\n * The full balance, thousands grouped the way the locale groups them:\n * 4600 -> `\"4,600\"` in `en-US`, `\"4 600\"` in `fr-FR`, `\"4.600\"` in `de-DE`.\n *\n * The `en-US` default is what `toLocaleString('en-US')` produced, character for\n * character -- it is the same CLDR data reached through the constructor that can\n * be cached.\n */\nexport function formatTokens(value: number, locale: string = DEFAULT_WALLET_LOCALE): string {\n return formatterFor(locale, 'grouped').format(value)\n}\n\n/**\n * A burned share as a whole percentage in the locale's own form: 0.87 ->\n * `\"87%\"` in `en-US`, `\"87 %\"` in `fr-FR` (with the narrow no-break space CLDR\n * puts there). Used by the top-bar chip's threshold line.\n */\nexport function formatPercent(fraction: number, locale: string = DEFAULT_WALLET_LOCALE): string {\n return formatterFor(locale, 'percent').format(fraction)\n}\n\n/**\n * The rail's short form: one decimal thousand, always. At 56px there is no room\n * for four digits and a separator, and \"4.6k\" still answers \"am I fine?\".\n *\n * Returns **the figure alone** -- `\"4.6\"` in `en-US`, `\"4,6\"` in `fr-FR`. The\n * mark that follows it is a word rather than a number (\"k\" in English, \"Tsd.\" in\n * German, \"ألف\" in Arabic), and French puts a space in front of it where English\n * does not, so it lives with the chip's other words, as `labels.railFigure`\n * (B33-876).\n *\n * Not `Intl.NumberFormat`'s own `notation: 'compact'`, for two concrete reasons.\n * It renders `en-US` as `\"4.6K\"`, a capital the baselines do not have. And\n * German does not abbreviate four digits at all, so it renders `\"4600,0\"` -- six\n * characters and a decimal, in a 56px rail, longer than the number it was\n * supposed to shorten. One decimal thousand is this package's decision about a\n * fixed-width rail, not CLDR's about a locale; the separator inside it is CLDR's,\n * and that is the part `locale` fixes.\n *\n * Two default-path differences from the `(value / 1000).toFixed(1)` this\n * replaced, both narrower than any state the baselines hold and both left\n * deliberately, but written down because they reach four products (B33-876):\n *\n * - **A balance landing on an exact half rounds the other way.** `toFixed`\n * rounds the double's exact binary expansion, so 150 tokens printed `0.1`;\n * `Intl` rounds the shortest decimal the double stands for, so it prints\n * `0.2`. `Intl` agrees with the reader here, and the old answer was a float\n * artefact rather than a decision.\n * - **A balance over a million is grouped.** `toFixed` gave `1234.6`, this\n * gives `1,234.6`. Neither fits a 56px rail; that is a rail question, not a\n * formatting one.\n */\nexport function thousandsFigure(value: number, locale: string = DEFAULT_WALLET_LOCALE): string {\n return formatterFor(locale, 'oneDecimal').format(value / 1000)\n}\n"],"names":["DEFAULT_WALLET_LOCALE","FORMATTER_OPTIONS","formatters","warnedLocales","formatterFor","locale","kind","key","cached","formatter","formatTokens","value","formatPercent","fraction","thousandsFigure"],"mappings":"AAuDO,MAAMA,IAAwB,SAQ/BC,IAAqE;AAAA,EACzE,SAAS,CAAA;AAAA,EACT,YAAY,EAAE,uBAAuB,GAAG,uBAAuB,EAAA;AAAA,EAC/D,SAAS,EAAE,OAAO,WAAW,uBAAuB,EAAA;AACtD,GAUMC,wBAAiB,IAAA,GAMjBC,wBAAoB,IAAA;AAmB1B,SAASC,EAAaC,GAAgBC,GAAwC;AAC5E,QAAMC,IAAM,GAAGD,CAAI,IAAID,CAAM,IACvBG,IAASN,EAAW,IAAIK,CAAG;AACjC,MAAIC,EAAQ,QAAOA;AAEnB,MAAIC;AACJ,MAAI;AACF,IAAAA,IAAY,IAAI,KAAK,aAAaJ,GAAQJ,EAAkBK,CAAI,CAAC;AAAA,EACnE,QAAQ;AACN,IAAKH,EAAc,IAAIE,CAAM,MAC3BF,EAAc,IAAIE,CAAM,GACxB,QAAQ;AAAA,MACN,2DAA2D,KAAK,UAAUA,CAAM,CAAC,qBAAqBL,CAAqB;AAAA,IAAA,IAG/HS,IAAY,IAAI,KAAK,aAAaT,GAAuBC,EAAkBK,CAAI,CAAC;AAAA,EAClF;AACA,SAAAJ,EAAW,IAAIK,GAAKE,CAAS,GACtBA;AACT;AAUO,SAASC,EAAaC,GAAeN,IAAiBL,GAA+B;AAC1F,SAAOI,EAAaC,GAAQ,SAAS,EAAE,OAAOM,CAAK;AACrD;AAOO,SAASC,EAAcC,GAAkBR,IAAiBL,GAA+B;AAC9F,SAAOI,EAAaC,GAAQ,SAAS,EAAE,OAAOQ,CAAQ;AACxD;AAiCO,SAASC,EAAgBH,GAAeN,IAAiBL,GAA+B;AAC7F,SAAOI,EAAaC,GAAQ,YAAY,EAAE,OAAOM,IAAQ,GAAI;AAC/D;"}
@@ -108,8 +108,15 @@ export interface WalletChipLabels {
108
108
  purchasedFigure: string;
109
109
  /**
110
110
  * The note under the granted line when `grantExpires` is known (board 21).
111
- * Placeholder `%date%`, the preformatted reset date. Default
112
- * `Included each month. Resets %date%.`
111
+ * Placeholder `%date%`, the same date the sidebar card prints in
112
+ * `grantExpiry`: the day this month's grant expires and the next one lands.
113
+ * One date, said two ways -- "expires" where the card warns about burn order,
114
+ * "resets" where the popover explains what the grant is.
115
+ *
116
+ * Default `Included each month · resets %date%`, with nothing after the date.
117
+ * Until 0.8.0-next.3 it ended `Resets %date%.`, and a locale that abbreviates
118
+ * months with a full stop printed two ("30 sept..", Score). Keep a
119
+ * translation's punctuation off the end of `%date%` for the same reason.
113
120
  */
114
121
  grantedNote: string;
115
122
  /**
@@ -140,6 +147,15 @@ export interface WalletChipLabels {
140
147
  * formatted figure past zero. Default `Wallet balance %amount% tokens past zero`.
141
148
  */
142
149
  pastZeroBalance: string;
150
+ /**
151
+ * The top-bar popover's line once this cycle's grant is past the threshold
152
+ * (80% burned, B33-1710): the at-a-glance warning the sidebar card's pill
153
+ * carried, moved into the bar's popover. Also appended to the chip's
154
+ * accessible name, so the warning is not colour alone. A template:
155
+ * `%percent%` is the burned share, formatted for `locale` ("87%", "87 %").
156
+ * Default `You've used %percent% of this month's grant.`
157
+ */
158
+ thresholdNote: string;
143
159
  }
144
160
  /**
145
161
  * The English defaults. A consumer that passes nothing renders exactly what the
@@ -186,6 +202,7 @@ export declare const defaultWalletChipLabels: Readonly<{
186
202
  pastZero: string;
187
203
  pastZeroLong: string;
188
204
  pastZeroBalance: string;
205
+ thresholdNote: string;
189
206
  }>;
190
207
  /**
191
208
  * The labels as the render consumes them: the four templates presented as
@@ -198,7 +215,7 @@ export declare const defaultWalletChipLabels: Readonly<{
198
215
  * of it; and the call is where B33-875's laziness lives, so a label for an element
199
216
  * that does not render is not interpolated at all.
200
217
  */
201
- export interface ResolvedWalletChipLabels extends Omit<WalletChipLabels, 'grantExpiry' | 'railFigure' | 'railBalance' | 'bar' | 'grantedFigure' | 'purchasedFigure' | 'grantedNote' | 'pastZeroBalance'> {
218
+ export interface ResolvedWalletChipLabels extends Omit<WalletChipLabels, 'grantExpiry' | 'railFigure' | 'railBalance' | 'bar' | 'grantedFigure' | 'purchasedFigure' | 'grantedNote' | 'pastZeroBalance' | 'thresholdNote'> {
202
219
  grantExpiry: (date: string) => string;
203
220
  grantedFigure: (amount: string) => string;
204
221
  purchasedFigure: (amount: string) => string;
@@ -207,6 +224,7 @@ export interface ResolvedWalletChipLabels extends Omit<WalletChipLabels, 'grantE
207
224
  railBalance: (balance: string) => string;
208
225
  bar: (granted: string, purchased: string, remaining: string) => string;
209
226
  pastZeroBalance: (amount: string) => string;
227
+ thresholdNote: (percent: string) => string;
210
228
  }
211
229
  /**
212
230
  * Merge a consumer's partial `labels` onto the English defaults, treating an