@boost33/ui 0.4.1 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +26 -1
  2. package/dist/components/AccountMenu/AccountMenu.d.ts +131 -0
  3. package/dist/components/AccountMenu/icons.d.ts +11 -0
  4. package/dist/components/AccountMenu/labels.d.ts +39 -0
  5. package/dist/components/AppShell/AppShell.d.ts +71 -18
  6. package/dist/components/AppShell/AppShell.js +33 -35
  7. package/dist/components/AppShell/AppShell.js.map +1 -1
  8. package/dist/components/AppShell/AppShell.module.css.js +11 -11
  9. package/dist/components/AppSwitcher/AppSwitcher.d.ts +23 -1
  10. package/dist/components/AppSwitcher/AppSwitcher.js +49 -43
  11. package/dist/components/AppSwitcher/AppSwitcher.js.map +1 -1
  12. package/dist/components/AppSwitcher/AppSwitcher.module.css.js +36 -36
  13. package/dist/components/AppSwitcher/labels.js.map +1 -1
  14. package/dist/components/CommandPalette/CommandPalette.d.ts +127 -0
  15. package/dist/components/CommandPalette/icons.d.ts +8 -0
  16. package/dist/components/CommandPalette/labels.d.ts +94 -0
  17. package/dist/components/DotMatrixLoader/DotMatrixLoader.d.ts +21 -0
  18. package/dist/components/DotMatrixLoader/DotMatrixLoader.js +18 -0
  19. package/dist/components/DotMatrixLoader/DotMatrixLoader.js.map +1 -0
  20. package/dist/components/DotMatrixLoader/DotMatrixLoader.module.css.js +12 -0
  21. package/dist/components/DotMatrixLoader/DotMatrixLoader.module.css.js.map +1 -0
  22. package/dist/components/LaunchLoader/LaunchLoader.d.ts +54 -0
  23. package/dist/components/LaunchLoader/LaunchLoader.js +57 -0
  24. package/dist/components/LaunchLoader/LaunchLoader.js.map +1 -0
  25. package/dist/components/LaunchLoader/LaunchLoader.module.css.js +24 -0
  26. package/dist/components/LaunchLoader/LaunchLoader.module.css.js.map +1 -0
  27. package/dist/components/NotificationCenter/NotificationCenter.d.ts +162 -0
  28. package/dist/components/NotificationCenter/icons.d.ts +34 -0
  29. package/dist/components/NotificationCenter/labels.d.ts +112 -0
  30. package/dist/components/ProductMark/ProductMark.d.ts +54 -1
  31. package/dist/components/ProductMark/ProductMark.js +23 -19
  32. package/dist/components/ProductMark/ProductMark.js.map +1 -1
  33. package/dist/components/Sidebar/Sidebar.d.ts +17 -3
  34. package/dist/components/Sidebar/Sidebar.js +5 -5
  35. package/dist/components/Sidebar/Sidebar.js.map +1 -1
  36. package/dist/components/WalletChip/WalletChip.d.ts +150 -27
  37. package/dist/components/WalletChip/WalletChip.js +38 -35
  38. package/dist/components/WalletChip/WalletChip.js.map +1 -1
  39. package/dist/components/WalletChip/WalletChip.module.css.js +36 -36
  40. package/dist/components/WalletChip/labels.d.ts +65 -47
  41. package/dist/components/WalletChip/labels.js +15 -20
  42. package/dist/components/WalletChip/labels.js.map +1 -1
  43. package/dist/fill-template.d.ts +10 -0
  44. package/dist/fill-template.js +10 -0
  45. package/dist/fill-template.js.map +1 -0
  46. package/dist/index.d.ts +10 -4
  47. package/dist/index.js +36 -39
  48. package/dist/index.js.map +1 -1
  49. package/dist/launch-gradient.d.ts +36 -0
  50. package/dist/launch-gradient.js +8 -0
  51. package/dist/launch-gradient.js.map +1 -0
  52. package/dist/nav-height-budget.d.ts +4 -0
  53. package/dist/nav-height-budget.js +18 -19
  54. package/dist/nav-height-budget.js.map +1 -1
  55. package/dist/style.css +1 -1
  56. package/dist/theme.css +40 -14
  57. package/package.json +2 -1
@@ -1 +1 @@
1
- {"version":3,"file":"ProductMark.js","sources":["../../../src/components/ProductMark/ProductMark.tsx"],"sourcesContent":["import { useId, type ReactNode } from 'react'\nimport styles from './ProductMark.module.css'\n\n/**\n * The size the mark renders at unless a caller says otherwise. 20px is the\n * Wallet's own default at its call sites, and the size the four\n * tests/baselines/productmark-*.png were captured at: a 20px tile in the 24px\n * gutter the specimen crop uses either side -> 68px CSS -> 136 device px at\n * deviceScaleFactor 2. Changing it moves four apps.\n *\n * There is deliberately no minimum-legible floor here, unlike Boost33Mark. That\n * floor is the monogram's alone -- the bowls of the B close up below ~20px. The\n * product marks have the opposite property: each is one path on a 40-unit grid\n * with a quarter-side corner, chosen so it renders correctly from Paper's 96px\n * down to 20px. Do not generalise Boost33Mark's floor into a shared rule.\n */\nconst DEFAULT_SIZE = 20\n\n/**\n * The slugs this package draws a product mark for, as one list. Everything else\n * keys off it: ProductSlug is derived from it, and GLYPHS is typed against it so\n * adding a slug without artwork is a compile error rather than a mark that\n * silently draws nothing.\n *\n * **It is no longer the whole of Identity's registry, and the difference is\n * deliberate.** Identity serves five slugs: these four plus `brokers` (B33-1158,\n * \"Boost Brokers\"). This package has no `brokers` glyph, so that row draws the\n * bare tile through the safe degrade below -- see \"The fifth slug\" underneath.\n *\n * **Transcribed from `boost33-identity/src/lib/productRegistry.ts`** -- its\n * `REGISTRY_SLUGS`, which is `PRODUCTS` from `src/config/products.ts` plus\n * `WALLET_SLUG`. That file is the source of truth and this is a copy of it in\n * another repository, which is a thing that drifts. It already did: until\n * B33-1043 this list read `wallet | score | voice | outbound` while this comment\n * asserted it was Identity's, so three of the four rows of a switcher fed\n * straight from `GET /api/v1/products` drew the bare tile (see `slug` below) and\n * nothing anywhere failed.\n *\n * Identity is not the side that renames. ADR-0003 records the shorter slugs as\n * considered and rejected: these match the `?app=` keys live campaign URLs\n * already carry, so renaming them means those links grant nothing on sign-up.\n *\n * `tests/product-mark.test.tsx` pins this list against a second transcription of\n * the same source, so the two repositories cannot part company in silence again.\n * That pin is what the old comment's assertion was standing in for, and it now\n * pins the gap as well as the overlap: it names `brokers` as the one served slug\n * with no glyph here, so a *sixth* product, or artwork landing for this one,\n * fails rather than passing quietly.\n *\n * The *order* is this package's own and not Identity's -- `REGISTRY_SLUGS` ends\n * with `wallet` where this begins with it. Nothing reads a render order off this\n * list: a switcher draws the order its consumer was served (`AppSwitcher`), and\n * the only thing this order sets is the left-to-right run of frames in the\n * ProductMark specimen.\n *\n * ── The fifth slug ───────────────────────────────────────────────────────────\n *\n * **`brokers` is absent because no Brokers artwork exists to lift, not because\n * nobody got to it.** The four glyphs below were taken verbatim out of\n * `boostledger/src/ui/primitives/icons.tsx`, whose own `GLYPHS` is typed\n * `Record<ProductName, ReactNode>` over exactly four display names. There is no\n * Brokers mark in that file, none in `boost33-brand-hub`, and none in this\n * repository. The hub renders the product as a text quick-link and no mark at\n * all.\n *\n * So drawing one here would not be transcribing a mark, it would be inventing\n * one -- and the comment on GLYPHS below records what that costs: PR #4 redrew\n * all four from a written description, got all four wrong, and reintroduced the\n * retired one-orange-element construction. An invented fifth mark is worse than\n * that was, because it has nothing to be caught against. A human captures the\n * first baseline at the fidelity gate, which would make the invention the\n * referent, and it looks equally settled either way afterwards (CLAUDE.md, \"an\n * agent that picks one and builds it has invented a referent\").\n *\n * The degrade is also the mildest it will ever be. Identity serves `brokers` as\n * `live: false` -- the space is still being built -- so the switcher row carries\n * the \"Soon\" tag beside the bare tile, which reads as a product that has not\n * arrived rather than as a drawing that failed to load. That stops being true\n * the day Brokers ships.\n *\n * **To close it:** get the mark drawn where the other four live\n * (`boostledger/src/ui/primitives/icons.tsx`), lift it here verbatim, add\n * `brokers` to this list, and let a human approve `productmark-brokers.png` at a\n * gate. Not in that order optional: this list is what GLYPHS is typed against,\n * so adding the slug without the artwork stops the build, which is the one part\n * of this that cannot go wrong quietly.\n */\nexport const PRODUCT_SLUGS = ['wallet', 'boostscore', 'boostvoice', 'boostoutbound'] as const\n\n/** One of the slugs this package draws a mark for. Not every slug Identity serves -- see above. */\nexport type ProductSlug = (typeof PRODUCT_SLUGS)[number]\n\n/**\n * The four product glyphs, lifted verbatim from `boostledger`'s\n * `src/ui/primitives/icons.tsx` (the `GLYPHS` record, as of `837d85d`) rather\n * than redrawn from any written description. Redrawing them from\n * `docs/mintlify/system/product-marks.mdx` is how PR #4 got all four wrong and\n * reintroduced the retired one-orange-element construction; if any document\n * disagrees with that file, that file wins. See B33-562.\n *\n * Each glyph is drawn on its own full 40-unit grid and inset into the middle 20\n * of the tile by the `translate(10 10) scale(0.5)` group below. The stroke\n * widths are right on that grid -- do not \"correct\" them to compensate for the\n * scale, the scale is what puts the stroke at the weight Paper draws. Every\n * element paints in `currentColor` (the tile's white glyph ink) or `none`;\n * there is no colour of any kind in here, which is what tells the four marks\n * apart by shape and never by hue.\n *\n * Typing this `Record<ProductSlug, ReactNode>` is load-bearing: a fifth slug\n * added to PRODUCT_SLUGS without a glyph here stops the build.\n */\nconst GLYPHS: Record<ProductSlug, ReactNode> = {\n // A credit card with its magnetic stripe: an outline card (fill none, so the\n // tile shows through) plus a filled stripe across it.\n wallet: (\n <>\n <rect x=\"5\" y=\"10\" width=\"30\" height=\"20\" rx=\"4\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"3\" />\n <rect x=\"5\" y=\"16\" width=\"30\" height=\"5\" fill=\"currentColor\" />\n </>\n ),\n // A gauge with the needle swung high: an arc, the needle, and the hub it\n // pivots on. Arc and needle stroke at width 4 with round caps.\n boostscore: (\n <>\n <path d=\"M6 26 A14 14 0 0 1 34 26\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"4\" strokeLinecap=\"round\" />\n <line x1=\"20\" y1=\"26\" x2=\"28\" y2=\"14\" stroke=\"currentColor\" strokeWidth=\"4\" strokeLinecap=\"round\" />\n <circle cx=\"20\" cy=\"26\" r=\"3\" fill=\"currentColor\" />\n </>\n ),\n // A soundwave, tall in the middle: three pill bars, all centred on y=20.\n boostvoice: (\n <>\n <rect x=\"6\" y=\"14\" width=\"7\" height=\"12\" rx=\"3.5\" fill=\"currentColor\" />\n <rect x=\"16.5\" y=\"4\" width=\"7\" height=\"32\" rx=\"3.5\" fill=\"currentColor\" />\n <rect x=\"27\" y=\"14\" width=\"7\" height=\"12\" rx=\"3.5\" fill=\"currentColor\" />\n </>\n ),\n // An arrow leaving through the top-right: the shaft and the elbow of its head,\n // both stroke width 4 with round caps and joins.\n boostoutbound: (\n <>\n <line x1=\"8\" y1=\"32\" x2=\"32\" y2=\"8\" stroke=\"currentColor\" strokeWidth=\"4\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n <path d=\"M16 8 H32 V24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"4\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n </>\n ),\n}\n\nfunction isProductSlug(slug: string): slug is ProductSlug {\n return (PRODUCT_SLUGS as readonly string[]).includes(slug)\n}\n\nexport interface ProductMarkProps {\n /**\n * Identity's product slug. An unknown one -- a product the served registry\n * carries but this package has no artwork for yet -- draws the bare tile\n * rather than throwing (ADR-0003's safe degrade), so the registry can run\n * ahead of a mark release.\n */\n slug: string\n /** Rendered width and height in px. Defaults to 20, the Wallet's own default. */\n size?: number\n /**\n * Dims the tile for a product the account is not entitled to -- the switcher's\n * unowned rows need it. It swaps the **ground only**, to --color-neutral-300;\n * the glyph stays white. Fading the glyph too would cost the shape the one\n * thing telling the four marks apart, and it is a prop rather than a wrapper\n * `opacity-40 grayscale` filter because that lands on a mid-grey visibly\n * darker than the grey Paper draws.\n *\n * **Only use this where the mark is decorative.** White on --color-neutral-300\n * is 1.41:1, well under WCAG 1.4.11's 3:1, and the reason that is not a defect\n * is that the criterion covers graphics *required to understand the content* --\n * which this is not, while the mark is `aria-hidden`, the product's name sits\n * beside it as text, and entitlement is additionally stated in words (the\n * switcher's \"Not on plan\" tag). Combining `muted` with `title`, so the mark\n * stands alone as the sole carrier of a product's identity, removes every one\n * of those and makes the ratio bite. That combination is out of contract; no\n * call site in this package uses it. See ADR-0012, which also records why the\n * ground is not darkened and what to reach for if this is ever revisited.\n */\n muted?: boolean\n /**\n * An accessible name. Omit it -- the default -- and the mark is decorative:\n * `aria-hidden`, contributing nothing to the accessibility tree. That is the\n * right answer in the switcher, where the product's name sits beside it and a\n * name here would only make a screen reader say it twice. Pass one only where\n * the mark stands alone as the sole carrier of meaning.\n *\n * Passing this is what takes the mark out of `muted`'s contract -- see the\n * note there. A mark that carries meaning on its own has to be legible on its\n * own, and the muted ground is not.\n */\n title?: string\n /** Escape hatch for a consumer that needs to position the mark. */\n className?: string\n}\n\n/**\n * The tile-and-glyph mark for one Boost33 product -- a rounded --color-neutral-950\n * square with the product's glyph in --color-primary-foreground. The four marks\n * share one palette and are told apart by shape alone; there is zero brand\n * orange in any of them.\n */\nexport function ProductMark({ slug, size = DEFAULT_SIZE, muted, title, className }: ProductMarkProps) {\n const titleId = useId()\n const decorative = title == null\n const glyph = isProductSlug(slug) ? GLYPHS[slug] : null\n\n return (\n <svg\n className={className != null ? `${styles.mark} ${className}` : styles.mark}\n width={size}\n height={size}\n viewBox=\"0 0 40 40\"\n role={decorative ? undefined : 'img'}\n aria-hidden={decorative ? 'true' : undefined}\n aria-labelledby={decorative ? undefined : titleId}\n >\n {!decorative && <title id={titleId}>{title}</title>}\n {/* The tile. Fill set inline, next to the muted decision that chooses it;\n the corner radius comes from the class (see ProductMark.module.css). */}\n <rect\n className={styles.ground}\n width=\"40\"\n height=\"40\"\n style={{ fill: muted ? 'var(--color-neutral-300)' : 'var(--color-neutral-950)' }}\n />\n {glyph && (\n <g\n transform=\"translate(10 10) scale(0.5)\"\n style={{ color: 'var(--color-primary-foreground)' }}\n >\n {glyph}\n </g>\n )}\n </svg>\n )\n}\n"],"names":["DEFAULT_SIZE","PRODUCT_SLUGS","GLYPHS","jsxs","Fragment","jsx","isProductSlug","slug","ProductMark","size","muted","title","className","titleId","useId","decorative","glyph","styles"],"mappings":"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"}
1
+ {"version":3,"file":"ProductMark.js","sources":["../../../src/components/ProductMark/ProductMark.tsx"],"sourcesContent":["import { useId, type ReactNode } from 'react'\nimport styles from './ProductMark.module.css'\n\n/**\n * The size the mark renders at unless a caller says otherwise. 20px is the\n * Wallet's own default at its call sites, and the size the four\n * tests/baselines/productmark-*.png were captured at: a 20px tile in the 24px\n * gutter the specimen crop uses either side -> 68px CSS -> 136 device px at\n * deviceScaleFactor 2. Changing it moves four apps.\n *\n * There is deliberately no minimum-legible floor here, unlike Boost33Mark. That\n * floor is the monogram's alone -- the bowls of the B close up below ~20px. The\n * product marks have the opposite property: each is one path on a 40-unit grid\n * with a quarter-side corner, chosen so it renders correctly from Paper's 96px\n * down to 20px. Do not generalise Boost33Mark's floor into a shared rule.\n */\nconst DEFAULT_SIZE = 20\n\n/**\n * The slugs this package draws a product mark for, as one list. Everything else\n * keys off it: ProductSlug is derived from it, and GLYPHS is typed against it so\n * adding a slug without artwork is a compile error rather than a mark that\n * silently draws nothing.\n *\n * **Module-internal since B33-1269, and it has to stay that way.** `src/index.ts`\n * publishes `hasProductMark` below and neither this list nor `ProductSlug`. A\n * `const` assertion over four strings made an artwork inventory part of the\n * published type contract: a consumer could write `Record<ProductSlug, ...>` or\n * switch exhaustively over it, and then artwork landing for a fifth product --\n * the thing this package exists to ship -- would be a breaking change for them.\n * Nothing published is typed `ProductSlug` anyway; every slug prop is `string` on\n * purpose, so Identity's registry may run ahead of a mark release (ADR-0003, and\n * `slug` below). So the union's only real job was answering \"which slugs have\n * artwork\", and `hasProductMark` answers that without freezing the list.\n *\n * The tests and the specimen read both names by deep import, which is how\n * everything in this repo reaches into `src/`. A consumer cannot: `package.json`\n * exports `.`, `./style.css` and `./theme.css`, and nothing else.\n *\n * **It is no longer the whole of Identity's registry, and the difference is\n * deliberate.** Identity serves five slugs: these four plus `brokers` (B33-1158,\n * \"Boost Brokers\"). This package has no `brokers` glyph, so that row draws the\n * bare tile through the safe degrade below -- see \"The fifth slug\" underneath.\n *\n * **Transcribed from `boost33-identity/src/lib/productRegistry.ts`** -- its\n * `REGISTRY_SLUGS`, which is `PRODUCTS` from `src/config/products.ts` plus\n * `WALLET_SLUG`. That file is the source of truth and this is a copy of it in\n * another repository, which is a thing that drifts. It already did: until\n * B33-1043 this list read `wallet | score | voice | outbound` while this comment\n * asserted it was Identity's, so three of the four rows of a switcher fed\n * straight from `GET /api/v1/products` drew the bare tile (see `slug` below) and\n * nothing anywhere failed.\n *\n * Identity is not the side that renames. ADR-0003 records the shorter slugs as\n * considered and rejected: these match the `?app=` keys live campaign URLs\n * already carry, so renaming them means those links grant nothing on sign-up.\n *\n * `tests/product-mark.test.tsx` pins this list against a second transcription of\n * the same source, so the two repositories cannot part company in silence again.\n * That pin is what the old comment's assertion was standing in for, and it now\n * pins the gap as well as the overlap: it names `brokers` as the one served slug\n * with no glyph here, so a *sixth* product, or artwork landing for this one,\n * fails rather than passing quietly.\n *\n * The *order* is this package's own and not Identity's -- `REGISTRY_SLUGS` ends\n * with `wallet` where this begins with it. Nothing reads a render order off this\n * list: a switcher draws the order its consumer was served (`AppSwitcher`), and\n * the only thing this order sets is the left-to-right run of frames in the\n * ProductMark specimen.\n *\n * ── The fifth slug ───────────────────────────────────────────────────────────\n *\n * **`brokers` is absent because no Brokers artwork exists to lift, not because\n * nobody got to it.** The four glyphs below were taken verbatim out of\n * `boostledger/src/ui/primitives/icons.tsx`, whose own `GLYPHS` is typed\n * `Record<ProductName, ReactNode>` over exactly four display names. There is no\n * Brokers mark in that file, none in `boost33-brand-hub`, and none in this\n * repository. The hub renders the product as a text quick-link and no mark at\n * all.\n *\n * So drawing one here would not be transcribing a mark, it would be inventing\n * one -- and the comment on GLYPHS below records what that costs: PR #4 redrew\n * all four from a written description, got all four wrong, and reintroduced the\n * retired one-orange-element construction. An invented fifth mark is worse than\n * that was, because it has nothing to be caught against. A human captures the\n * first baseline at the fidelity gate, which would make the invention the\n * referent, and it looks equally settled either way afterwards (CLAUDE.md, \"an\n * agent that picks one and builds it has invented a referent\").\n *\n * The degrade is also the mildest it will ever be. Identity serves `brokers` as\n * `live: false` -- the space is still being built -- so the switcher row carries\n * the \"Soon\" tag beside the bare tile, which reads as a product that has not\n * arrived rather than as a drawing that failed to load. That stops being true\n * the day Brokers ships.\n *\n * **To close it:** get the mark drawn where the other four live\n * (`boostledger/src/ui/primitives/icons.tsx`), lift it here verbatim, add\n * `brokers` to this list, and let a human approve `productmark-brokers.png` at a\n * gate. Not in that order optional: this list is what GLYPHS is typed against,\n * so adding the slug without the artwork stops the build, which is the one part\n * of this that cannot go wrong quietly.\n *\n * **And since B33-1269 that is an ordinary addition rather than a breaking\n * change.** This list is not published, so widening it widens nothing a consumer\n * has typed against: a fifth entry adds a drawing where there was a bare tile and\n * flips `hasProductMark('brokers')` from false to true. Until then it was a\n * major-bump-shaped change to ship a picture, which is the wrong price for the\n * thing this package is for.\n */\nexport const PRODUCT_SLUGS = ['wallet', 'boostscore', 'boostvoice', 'boostoutbound'] as const\n\n/**\n * One of the slugs this package draws a mark for. Not every slug Identity serves\n * -- see above -- and not published either, for the reason recorded there. What\n * this type is for is typing GLYPHS, so the list and the artwork cannot part\n * company.\n */\nexport type ProductSlug = (typeof PRODUCT_SLUGS)[number]\n\n/**\n * The four product glyphs, lifted verbatim from `boostledger`'s\n * `src/ui/primitives/icons.tsx` (the `GLYPHS` record, as of `837d85d`) rather\n * than redrawn from any written description. Redrawing them from\n * `docs/mintlify/system/product-marks.mdx` is how PR #4 got all four wrong and\n * reintroduced the retired one-orange-element construction; if any document\n * disagrees with that file, that file wins. See B33-562.\n *\n * Each glyph is drawn on its own full 40-unit grid and inset into the middle 20\n * of the tile by the `translate(10 10) scale(0.5)` group below. The stroke\n * widths are right on that grid -- do not \"correct\" them to compensate for the\n * scale, the scale is what puts the stroke at the weight Paper draws. Every\n * element paints in `currentColor` (the tile's white glyph ink) or `none`;\n * there is no colour of any kind in here, which is what tells the four marks\n * apart by shape and never by hue.\n *\n * Typing this `Record<ProductSlug, ReactNode>` is load-bearing: a fifth slug\n * added to PRODUCT_SLUGS without a glyph here stops the build. That is why the\n * list stays a `const` assertion with a union derived from it even though\n * B33-1269 stopped publishing both -- the union moved off the package surface,\n * it did not stop existing, and `tsc --noEmit` is the check that a slug and its\n * drawing arrive together.\n */\nconst GLYPHS: Record<ProductSlug, ReactNode> = {\n // A credit card with its magnetic stripe: an outline card (fill none, so the\n // tile shows through) plus a filled stripe across it.\n wallet: (\n <>\n <rect x=\"5\" y=\"10\" width=\"30\" height=\"20\" rx=\"4\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"3\" />\n <rect x=\"5\" y=\"16\" width=\"30\" height=\"5\" fill=\"currentColor\" />\n </>\n ),\n // A gauge with the needle swung high: an arc, the needle, and the hub it\n // pivots on. Arc and needle stroke at width 4 with round caps.\n boostscore: (\n <>\n <path d=\"M6 26 A14 14 0 0 1 34 26\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"4\" strokeLinecap=\"round\" />\n <line x1=\"20\" y1=\"26\" x2=\"28\" y2=\"14\" stroke=\"currentColor\" strokeWidth=\"4\" strokeLinecap=\"round\" />\n <circle cx=\"20\" cy=\"26\" r=\"3\" fill=\"currentColor\" />\n </>\n ),\n // A soundwave, tall in the middle: three pill bars, all centred on y=20.\n boostvoice: (\n <>\n <rect x=\"6\" y=\"14\" width=\"7\" height=\"12\" rx=\"3.5\" fill=\"currentColor\" />\n <rect x=\"16.5\" y=\"4\" width=\"7\" height=\"32\" rx=\"3.5\" fill=\"currentColor\" />\n <rect x=\"27\" y=\"14\" width=\"7\" height=\"12\" rx=\"3.5\" fill=\"currentColor\" />\n </>\n ),\n // An arrow leaving through the top-right: the shaft and the elbow of its head,\n // both stroke width 4 with round caps and joins.\n boostoutbound: (\n <>\n <line x1=\"8\" y1=\"32\" x2=\"32\" y2=\"8\" stroke=\"currentColor\" strokeWidth=\"4\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n <path d=\"M16 8 H32 V24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"4\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n </>\n ),\n}\n\n/*\n * The narrowing half of the same question, deliberately separate from the\n * published `hasProductMark` below. A type predicate -- `slug is ProductSlug` --\n * is what lets `GLYPHS[slug]` index safely, and it is also a signature that\n * names `ProductSlug` in the emitted `.d.ts`. Publishing this one would put the\n * union back on the package surface through the back door, which is exactly what\n * B33-1269 took off it.\n */\nfunction isProductSlug(slug: string): slug is ProductSlug {\n return (PRODUCT_SLUGS as readonly string[]).includes(slug)\n}\n\n/**\n * Does this package draw a mark for `slug`? The supported way to ask, and the\n * only thing about the artwork inventory the package publishes (B33-1269).\n *\n * **Ask this rather than matching a slug against a union of known slugs.** The\n * set of slugs with artwork is this package's inventory, not a vocabulary:\n * Identity owns which products exist (ADR-0003) and this list grows whenever a\n * drawing lands. `brokers` is the one slug Identity serves today that there is no\n * glyph here for -- see \"The fifth slug\" above. A caller that pattern-matches a\n * union is pinned to whichever inventory it compiled against; a caller that asks\n * this gets the installed version's answer, and keeps working when the next mark\n * ships.\n *\n * **A `false` is not an error and needs no guard around it.** `ProductMark`\n * renders a slug it has no glyph for as the bare tile rather than throwing\n * (ADR-0003's safe degrade), and in a switcher that row is still named, still\n * linked and still clickable -- only the glyph is missing. So do not use this to\n * filter a served registry, to hide a product's row, or to decide to substitute a\n * drawing of your own: every one of those turns a missing glyph into a missing\n * product, and the registry running ahead of the artwork is the supported state.\n *\n * It is here for a caller that needs to *know* rather than to branch on\n * rendering -- a consumer test asserting its own hardcoded fixtures still draw\n * something, or a report of which served slugs have artwork yet.\n */\nexport function hasProductMark(slug: string): boolean {\n return isProductSlug(slug)\n}\n\nexport interface ProductMarkProps {\n /**\n * Identity's product slug. An unknown one -- a product the served registry\n * carries but this package has no artwork for yet -- draws the bare tile\n * rather than throwing (ADR-0003's safe degrade), so the registry can run\n * ahead of a mark release.\n */\n slug: string\n /** Rendered width and height in px. Defaults to 20, the Wallet's own default. */\n size?: number\n /**\n * Dims the tile for a product the account is not entitled to -- the switcher's\n * unowned rows need it. It swaps the **ground only**, to --color-neutral-300;\n * the glyph stays white. Fading the glyph too would cost the shape the one\n * thing telling the four marks apart, and it is a prop rather than a wrapper\n * `opacity-40 grayscale` filter because that lands on a mid-grey visibly\n * darker than the grey Paper draws.\n *\n * **Only use this where the mark is decorative.** White on --color-neutral-300\n * is 1.41:1, well under WCAG 1.4.11's 3:1, and the reason that is not a defect\n * is that the criterion covers graphics *required to understand the content* --\n * which this is not, while the mark is `aria-hidden`, the product's name sits\n * beside it as text, and entitlement is additionally stated in words (the\n * switcher's \"Not on plan\" tag). Combining `muted` with `title`, so the mark\n * stands alone as the sole carrier of a product's identity, removes every one\n * of those and makes the ratio bite. That combination is out of contract; no\n * call site in this package uses it. See ADR-0012, which also records why the\n * ground is not darkened and what to reach for if this is ever revisited.\n */\n muted?: boolean\n /**\n * An accessible name. Omit it -- the default -- and the mark is decorative:\n * `aria-hidden`, contributing nothing to the accessibility tree. That is the\n * right answer in the switcher, where the product's name sits beside it and a\n * name here would only make a screen reader say it twice. Pass one only where\n * the mark stands alone as the sole carrier of meaning.\n *\n * Passing this is what takes the mark out of `muted`'s contract -- see the\n * note there. A mark that carries meaning on its own has to be legible on its\n * own, and the muted ground is not.\n */\n title?: string\n /** Escape hatch for a consumer that needs to position the mark. */\n className?: string\n}\n\n/**\n * The tile-and-glyph mark for one Boost33 product -- a rounded --color-neutral-950\n * square with the product's glyph in --color-primary-foreground. The four marks\n * share one palette and are told apart by shape alone; there is zero brand\n * orange in any of them.\n */\nexport function ProductMark({ slug, size = DEFAULT_SIZE, muted, title, className }: ProductMarkProps) {\n const titleId = useId()\n const decorative = title == null\n const glyph = isProductSlug(slug) ? GLYPHS[slug] : null\n\n return (\n <svg\n className={className != null ? `${styles.mark} ${className}` : styles.mark}\n width={size}\n height={size}\n viewBox=\"0 0 40 40\"\n role={decorative ? undefined : 'img'}\n aria-hidden={decorative ? 'true' : undefined}\n aria-labelledby={decorative ? undefined : titleId}\n >\n {!decorative && <title id={titleId}>{title}</title>}\n {/* The tile. Fill set inline, next to the muted decision that chooses it;\n the corner radius comes from the class (see ProductMark.module.css). */}\n <rect\n className={styles.ground}\n width=\"40\"\n height=\"40\"\n style={{ fill: muted ? 'var(--color-neutral-300)' : 'var(--color-neutral-950)' }}\n />\n {glyph && (\n <g\n transform=\"translate(10 10) scale(0.5)\"\n style={{ color: 'var(--color-primary-foreground)' }}\n >\n {glyph}\n </g>\n )}\n </svg>\n )\n}\n"],"names":["DEFAULT_SIZE","PRODUCT_SLUGS","GLYPHS","jsxs","Fragment","jsx","isProductSlug","slug","hasProductMark","ProductMark","size","muted","title","className","titleId","useId","decorative","glyph","styles"],"mappings":"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"}
@@ -1,7 +1,21 @@
1
1
  import { ReactNode } from 'react';
2
2
  export interface SidebarProps {
3
- /** Names the sidebar landmark, so it stays distinct from other regions on the page. */
4
- label: string;
3
+ /**
4
+ * Names the sidebar landmark, so it stays distinct from other regions on the
5
+ * page. It reaches `aria-label` on the `<aside>` and is **never rendered as
6
+ * text** -- a screen reader announces it and nobody else hears it.
7
+ *
8
+ * Spelled `landmarkLabel` rather than `label` on purpose (B33-1271). It used
9
+ * to be `label`, which is also what `NavSection` calls its prop -- and
10
+ * `NavSection`'s is the opposite thing, the visible uppercase header above the
11
+ * rows. `AppShell` passes both, a few lines apart, so one word named a string
12
+ * a person reads and a string only a screen reader hears in the same tree. A
13
+ * consumer who learned the convention from one component and applied it to the
14
+ * other shipped a visible string nobody could see, or a screen-reader-only
15
+ * string onto the screen. `label` stays with the visible meaning, which is the
16
+ * one a reader's intuition matches.
17
+ */
18
+ landmarkLabel: string;
5
19
  /** Sidebar-on-the-rail state. Narrows to the 56px rail; the pieces inside collapse themselves. */
6
20
  collapsed?: boolean;
7
21
  /**
@@ -16,4 +30,4 @@ export interface SidebarProps {
16
30
  */
17
31
  children: ReactNode;
18
32
  }
19
- export declare function Sidebar({ label, collapsed, children }: SidebarProps): import("react").JSX.Element;
33
+ export declare function Sidebar({ landmarkLabel, collapsed, children }: SidebarProps): import("react").JSX.Element;
@@ -1,14 +1,14 @@
1
- import { jsxs as i, jsx as t } from "react/jsx-runtime";
1
+ import { jsxs as a, jsx as t } from "react/jsx-runtime";
2
2
  import "react";
3
3
  import { NavHeightBudgetWarning as o } from "./NavHeightBudgetWarning.js";
4
4
  import s from "./Sidebar.module.css.js";
5
- function p({ label: r, collapsed: a = !1, children: e }) {
6
- return /* @__PURE__ */ i("aside", { className: s.sidebar, "aria-label": r, "data-collapsed": a ? "true" : void 0, children: [
5
+ function f({ landmarkLabel: r, collapsed: e = !1, children: i }) {
6
+ return /* @__PURE__ */ a("aside", { className: s.sidebar, "aria-label": r, "data-collapsed": e ? "true" : void 0, children: [
7
7
  /* @__PURE__ */ t(o, { label: r }),
8
- e
8
+ i
9
9
  ] });
10
10
  }
11
11
  export {
12
- p as Sidebar
12
+ f as Sidebar
13
13
  };
14
14
  //# sourceMappingURL=Sidebar.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Sidebar.js","sources":["../../../src/components/Sidebar/Sidebar.tsx"],"sourcesContent":["import { type ReactNode } from 'react'\nimport { NavHeightBudgetWarning } from './NavHeightBudgetWarning'\nimport styles from './Sidebar.module.css'\n\nexport interface SidebarProps {\n /** Names the sidebar landmark, so it stays distinct from other regions on the page. */\n label: string\n /** Sidebar-on-the-rail state. Narrows to the 56px rail; the pieces inside collapse themselves. */\n collapsed?: boolean\n /**\n * What the sidebar holds -- wordmark, switcher, nav sections, wallet chip, footer. Passed in, never hardcoded.\n *\n * **Put the nav in a `<nav>` element that is a direct child of the sidebar.**\n * That element is the sidebar's scroll region: a nav longer than the column\n * scrolls inside itself, and everything above and below it -- the wordmark,\n * the switcher, the wallet chip, the footer -- stays pinned. A nav nested\n * inside another element does not get that, and says so in development. See\n * ADR-0005.\n */\n children: ReactNode\n}\n\n// The sidebar frame: a 264px column with a hairline down its right edge,\n// collapsing to a 56px rail. It is only the container -- what stacks inside it\n// is data the consumer passes, so a product never hardcodes its own rows here.\n//\n// A Server Component: it holds no state, so a Next App Router consumer renders\n// the shell's static half without shipping it to the browser (B33-846). The one\n// piece that must run in the browser -- the development-mode nav-budget warning\n// -- is its own \"use client\" child, NavHeightBudgetWarning, which renders\n// nothing a consumer sees.\nexport function Sidebar({ label, collapsed = false, children }: SidebarProps) {\n return (\n <aside className={styles.sidebar} aria-label={label} data-collapsed={collapsed ? 'true' : undefined}>\n <NavHeightBudgetWarning label={label} />\n {children}\n </aside>\n )\n}\n"],"names":["Sidebar","label","collapsed","children","jsxs","styles","jsx","NavHeightBudgetWarning"],"mappings":";;;;AA+BO,SAASA,EAAQ,EAAE,OAAAC,GAAO,WAAAC,IAAY,IAAO,UAAAC,KAA0B;AAC5E,SACE,gBAAAC,EAAC,SAAA,EAAM,WAAWC,EAAO,SAAS,cAAYJ,GAAO,kBAAgBC,IAAY,SAAS,QACxF,UAAA;AAAA,IAAA,gBAAAI,EAACC,KAAuB,OAAAN,GAAc;AAAA,IACrCE;AAAA,EAAA,GACH;AAEJ;"}
1
+ {"version":3,"file":"Sidebar.js","sources":["../../../src/components/Sidebar/Sidebar.tsx"],"sourcesContent":["import { type ReactNode } from 'react'\nimport { NavHeightBudgetWarning } from './NavHeightBudgetWarning'\nimport styles from './Sidebar.module.css'\n\nexport interface SidebarProps {\n /**\n * Names the sidebar landmark, so it stays distinct from other regions on the\n * page. It reaches `aria-label` on the `<aside>` and is **never rendered as\n * text** -- a screen reader announces it and nobody else hears it.\n *\n * Spelled `landmarkLabel` rather than `label` on purpose (B33-1271). It used\n * to be `label`, which is also what `NavSection` calls its prop -- and\n * `NavSection`'s is the opposite thing, the visible uppercase header above the\n * rows. `AppShell` passes both, a few lines apart, so one word named a string\n * a person reads and a string only a screen reader hears in the same tree. A\n * consumer who learned the convention from one component and applied it to the\n * other shipped a visible string nobody could see, or a screen-reader-only\n * string onto the screen. `label` stays with the visible meaning, which is the\n * one a reader's intuition matches.\n */\n landmarkLabel: string\n /** Sidebar-on-the-rail state. Narrows to the 56px rail; the pieces inside collapse themselves. */\n collapsed?: boolean\n /**\n * What the sidebar holds -- wordmark, switcher, nav sections, wallet chip, footer. Passed in, never hardcoded.\n *\n * **Put the nav in a `<nav>` element that is a direct child of the sidebar.**\n * That element is the sidebar's scroll region: a nav longer than the column\n * scrolls inside itself, and everything above and below it -- the wordmark,\n * the switcher, the wallet chip, the footer -- stays pinned. A nav nested\n * inside another element does not get that, and says so in development. See\n * ADR-0005.\n */\n children: ReactNode\n}\n\n// The sidebar frame: a 264px column with a hairline down its right edge,\n// collapsing to a 56px rail. It is only the container -- what stacks inside it\n// is data the consumer passes, so a product never hardcodes its own rows here.\n//\n// A Server Component: it holds no state, so a Next App Router consumer renders\n// the shell's static half without shipping it to the browser (B33-846). The one\n// piece that must run in the browser -- the development-mode nav-budget warning\n// -- is its own \"use client\" child, NavHeightBudgetWarning, which renders\n// nothing a consumer sees.\nexport function Sidebar({ landmarkLabel, collapsed = false, children }: SidebarProps) {\n return (\n <aside className={styles.sidebar} aria-label={landmarkLabel} data-collapsed={collapsed ? 'true' : undefined}>\n {/* The warning prints this to name *which* sidebar it is talking about, so\n its prop is `label` in the developer-facing sense and stays that. */}\n <NavHeightBudgetWarning label={landmarkLabel} />\n {children}\n </aside>\n )\n}\n"],"names":["Sidebar","landmarkLabel","collapsed","children","jsxs","styles","jsx","NavHeightBudgetWarning"],"mappings":";;;;AA6CO,SAASA,EAAQ,EAAE,eAAAC,GAAe,WAAAC,IAAY,IAAO,UAAAC,KAA0B;AACpF,SACE,gBAAAC,EAAC,SAAA,EAAM,WAAWC,EAAO,SAAS,cAAYJ,GAAe,kBAAgBC,IAAY,SAAS,QAGhG,UAAA;AAAA,IAAA,gBAAAI,EAACC,GAAA,EAAuB,OAAON,EAAA,CAAe;AAAA,IAC7CE;AAAA,EAAA,GACH;AAEJ;"}
@@ -1,28 +1,36 @@
1
1
  import { WalletChipLabels } from './labels';
2
- export interface WalletChipProps {
3
- /** Remaining granted tokens. Granted burns first and expires. */
4
- granted: number;
5
- /** Remaining purchased tokens. Purchased never expires. */
6
- purchased: number;
7
- /**
8
- * The grant issued for this cycle. The denominator for "how much has burned",
9
- * and so for the threshold; drawn as the bar's full width alongside purchased
10
- * so the filled portion shortens as the month runs.
11
- */
12
- grantCycle: number;
13
- /** When the current grant expires, preformatted for display, e.g. "30 Sep". */
14
- grantExpires?: string;
15
- /**
16
- * The threshold state's runway line, preformatted, e.g. "About four days left
17
- * at your current burn." Passed in rather than derived: the burn estimate is
18
- * the product's to make, not the chip's -- the chip renders what it is given.
19
- */
20
- burnNote?: string;
21
- /** The balance could not be confirmed. Renders as unknown, never as zero. */
22
- unknown?: boolean;
2
+ /**
3
+ * The props that say nothing about a balance, so both states take them
4
+ * identically: where the chip is drawn, what it is wired to, and the words and
5
+ * number formats it renders with.
6
+ */
7
+ interface WalletChipChromeProps {
23
8
  /** The 56px rail. Abbreviates the balance and turns the split on its side. */
24
9
  collapsed?: boolean;
25
- /** Top up. Wiring for the one button the sidebar is allowed to have. */
10
+ /**
11
+ * Top up. Wiring for the one button the sidebar is allowed to have.
12
+ *
13
+ * **It is also what makes that button exist.** Omit it and the chip draws no
14
+ * top-up affordance at all, in every state, and that is the fix B33-1270 made.
15
+ * The render used to be gated on `!unknown` alone, so a consumer that passed
16
+ * no handler got a visible control that did nothing and could not be removed
17
+ * -- `.topUp`'s class name is CSS-Modules-hashed, so there is no selector to
18
+ * reach it from outside the package.
19
+ *
20
+ * That was not hypothetical in either consumer. The Wallet passes no handler
21
+ * and ships the dead button today
22
+ * (`boostledger/src/app/(wallet)/WalletShell.tsx`), and Score wrote a whole
23
+ * client module to arm it across a Server Component boundary rather than live
24
+ * with it (`boostscore/components/app-shell/wallet-chip-top-up.tsx`, which
25
+ * says so at length).
26
+ *
27
+ * Nothing renders in the button's place. It carries no information of its own
28
+ * -- only its own affordance -- so a consumer with nowhere to send someone is
29
+ * better served by the chip without it than by a disabled one they did not
30
+ * ask for (ADR-0010, "Not entitled is not disabled", on why unavailable is not
31
+ * the same as disabled). B33-1047 is the other half: an `href` form of the
32
+ * same affordance, for a consumer that cannot pass a callback at all.
33
+ */
26
34
  onTopUp?: () => void;
27
35
  /**
28
36
  * The chip's user-facing strings. Every field has an English default, so a
@@ -30,10 +38,11 @@ export interface WalletChipProps {
30
38
  * French-default. The package takes no i18n dependency -- it renders what it
31
39
  * is given (B33-849).
32
40
  *
33
- * Every field can be a plain string, so the whole object crosses the React
34
- * Server Component boundary and a Next App Router page can build it. The four
35
- * labels that carry a value mark its position with a `{placeholder}`; each
36
- * also accepts a function, which only works from a Client Component (B33-880).
41
+ * Every field is a plain string, so the whole object crosses the React Server
42
+ * Component boundary and a Next App Router page can build it. The four labels
43
+ * that carry a value mark its position with a `%placeholder%` (B33-1277). None
44
+ * of them takes a function: that form only ever worked from a Client Component
45
+ * and nothing in the type said so (B33-880, B33-1278).
37
46
  */
38
47
  labels?: Partial<WalletChipLabels>;
39
48
  /**
@@ -57,4 +66,118 @@ export interface WalletChipProps {
57
66
  */
58
67
  locale?: string;
59
68
  }
60
- export declare function WalletChip({ granted, purchased, grantCycle, grantExpires, burnNote, unknown, collapsed, onTopUp, labels, locale }: WalletChipProps): import("react").JSX.Element;
69
+ /**
70
+ * The chip with a balance the consumer stands behind. The two remaining figures
71
+ * are required, because this is the state that renders them.
72
+ */
73
+ export interface WalletChipKnownProps extends WalletChipChromeProps {
74
+ /**
75
+ * Absent or `false`. The discriminant: it is what makes the figures below
76
+ * required here and forbidden in {@link WalletChipUnknownProps}.
77
+ */
78
+ unknown?: false;
79
+ /** Remaining granted tokens. Granted burns first and expires. */
80
+ granted: number;
81
+ /** Remaining purchased tokens. Purchased never expires. */
82
+ purchased: number;
83
+ /**
84
+ * What this cycle's grant started with, if the consumer knows. The bar's full
85
+ * width alongside purchased, so the part already burned draws as empty track
86
+ * and the filled portion shortens as the month runs.
87
+ *
88
+ * **Optional, and that is the fix B33-483 made.** It used to be required, and
89
+ * no consumer could satisfy it honestly: it is the Ledger's
90
+ * `grant_tokens_per_cycle`, and it cannot be reconstructed from a balance --
91
+ * `cycleUsedPct` counts purchased-bucket spend, is not `(cycle total -
92
+ * remaining)` once a credit, refund or void has touched the account, and is not
93
+ * bounded at 100. So the Wallet passed `grantCycle = granted`, which pins
94
+ * `burned` at 0 and held the threshold state unreachable in the reference
95
+ * implementation, and Score passed a local per-plan belief instead.
96
+ *
97
+ * Omitted, the bar measures against `granted + purchased` -- the balance
98
+ * itself. It then draws full and claims nothing about what burned, which is the
99
+ * honest rendering when nobody can say.
100
+ *
101
+ * It no longer decides the threshold on its own: {@link burnedFraction} does
102
+ * that, and this is only consulted when that is absent.
103
+ */
104
+ grantCycle?: number;
105
+ /**
106
+ * How much of the cycle's grant has burned, as a fraction -- `0.88` for 88%.
107
+ * Drives the threshold state, and is the figure the chip rounds into its pill.
108
+ *
109
+ * Passed in rather than derived, for exactly the reason {@link burnNote}
110
+ * already is: the burn is the product's measurement to make, not the chip's.
111
+ * The chip kept the arithmetic while taking the runway line as a prop, and fed
112
+ * it from a denominator no product could supply -- so the measurement was the
113
+ * chip's in name and nobody's in fact (B33-483).
114
+ *
115
+ * The *policy* stays here. `THRESHOLD_BURN` is 0.8 in this package, so four
116
+ * products trip at the same place and cannot drift to four different numbers.
117
+ * That is why this is a fraction and not a `threshold?: boolean`: a boolean
118
+ * would move the 80% decision into each consumer, and it would leave the pill
119
+ * with no figure to print.
120
+ *
121
+ * Supplied, it wins. Absent, the chip derives it from {@link grantCycle} as it
122
+ * always did; absent with no `grantCycle` either, nothing is claimed and the
123
+ * chip rests.
124
+ *
125
+ * It is not clamped. A consumer that passes a percentage where a fraction
126
+ * belongs renders a visibly absurd pill rather than a plausible "100%", and a
127
+ * wrong figure that looks wrong is the one that gets fixed.
128
+ */
129
+ burnedFraction?: number;
130
+ /** When the current grant expires, preformatted for display, e.g. "30 Sep". */
131
+ grantExpires?: string;
132
+ /**
133
+ * The threshold state's runway line, preformatted, e.g. "About four days left
134
+ * at your current burn." Passed in rather than derived: the burn estimate is
135
+ * the product's to make, not the chip's -- the chip renders what it is given.
136
+ */
137
+ burnNote?: string;
138
+ }
139
+ /**
140
+ * The chip whose balance could not be confirmed. It renders an em dash, no bar
141
+ * and no button, so **it takes no figures at all** -- each one below is
142
+ * `?: never`.
143
+ *
144
+ * That is deliberate and it is the second half of B33-483. The figures were
145
+ * required in every state, so both consumers manufactured zeroes to get past the
146
+ * type and then apologised for them in a comment -- "these zeroes are never
147
+ * rendered ... but they are not a balance and must not be read as one"
148
+ * (`boostscore/components/app-shell/wallet-chip.tsx`), and the same again in
149
+ * `wallet-slot.tsx` and `boostledger/src/app/(wallet)/layout.tsx`. A zero is a
150
+ * confident claim about someone's money. The type now refuses it rather than
151
+ * asking a comment to.
152
+ *
153
+ * `undefined` is still accepted, so a consumer that must build one props object
154
+ * can write `{ unknown: true, granted: undefined }` instead of branching.
155
+ */
156
+ export interface WalletChipUnknownProps extends WalletChipChromeProps {
157
+ /** The balance could not be confirmed. Renders as unknown, never as zero. */
158
+ unknown: true;
159
+ granted?: never;
160
+ purchased?: never;
161
+ grantCycle?: never;
162
+ burnedFraction?: never;
163
+ /** Not rendered in this state: the footer only draws on a resting chip. */
164
+ grantExpires?: never;
165
+ /** Not rendered in this state: the runway line only draws past the threshold. */
166
+ burnNote?: never;
167
+ }
168
+ /**
169
+ * A discriminated union on `unknown`, and the discriminant has consequences for
170
+ * how a consumer *types* its own code, not only what it passes:
171
+ *
172
+ * - A helper that returns props for both states needs an explicit
173
+ * `: WalletChipProps` return annotation. Without one, TypeScript widens the
174
+ * `unknown: true` in an object literal to `boolean`, which matches neither
175
+ * member, and the spread at the call site stops compiling.
176
+ * - `Omit<WalletChipProps, 'onTopUp'>` is not what it looks like. `Omit` over a
177
+ * union flattens it into one object and loses the discrimination. Omit from
178
+ * the members instead -- `Omit<WalletChipKnownProps, 'onTopUp'> |
179
+ * Omit<WalletChipUnknownProps, 'onTopUp'>` -- which is why both are exported.
180
+ */
181
+ export type WalletChipProps = WalletChipKnownProps | WalletChipUnknownProps;
182
+ export declare function WalletChip({ granted, purchased, grantCycle, burnedFraction, grantExpires, burnNote, unknown, collapsed, onTopUp, labels, locale }: WalletChipProps): import("react").JSX.Element;
183
+ export {};
@@ -1,68 +1,71 @@
1
1
  "use client";
2
2
  import { jsxs as l, jsx as e } from "react/jsx-runtime";
3
3
  import a from "./WalletChip.module.css.js";
4
- import { thousandsFigure as U, formatTokens as o, DEFAULT_WALLET_LOCALE as B } from "./format.js";
5
- import { WalletMark as T, RetryMark as x } from "./icons.js";
6
- import { resolveWalletChipLabels as A } from "./labels.js";
7
- const R = 0.8;
8
- function L({ granted: r, purchased: t, cycleTotal: c, tone: m, label: h }) {
9
- const i = (p) => `${p / c * 100}%`;
10
- return /* @__PURE__ */ l("div", { className: a.bar, "data-tone": m, role: "img", "aria-label": h, children: [
11
- /* @__PURE__ */ e("span", { className: a.segmentGranted, "data-testid": "segment-granted", style: { width: i(r) } }),
12
- /* @__PURE__ */ e("span", { className: a.segmentPurchased, "data-testid": "segment-purchased", style: { width: i(t) } })
4
+ import { thousandsFigure as A, formatTokens as m, DEFAULT_WALLET_LOCALE as M } from "./format.js";
5
+ import { WalletMark as R, RetryMark as W } from "./icons.js";
6
+ import { resolveWalletChipLabels as w } from "./labels.js";
7
+ const y = 0.8, F = "var(--spacing-wallet-segment-min)";
8
+ function g(s, r) {
9
+ const t = r > 0 ? s / r * 100 : 0;
10
+ return s <= 0 ? `${t}%` : `max(${t}%, ${F})`;
11
+ }
12
+ function B({ granted: s, purchased: r, cycleTotal: t, tone: h, label: o }) {
13
+ return /* @__PURE__ */ l("div", { className: a.bar, "data-tone": h, role: "img", "aria-label": o, children: [
14
+ /* @__PURE__ */ e("span", { className: a.segmentGranted, "data-testid": "segment-granted", style: { width: g(s, t) } }),
15
+ /* @__PURE__ */ e("span", { className: a.segmentPurchased, "data-testid": "segment-purchased", style: { width: g(r, t) } })
13
16
  ] });
14
17
  }
15
- function I({ granted: r, purchased: t, grantCycle: c, grantExpires: m, burnNote: h, unknown: i = !1, collapsed: p = !1, onTopUp: g, labels: y, locale: n = B }) {
16
- const s = A(y), u = () => s.bar(o(r, n), o(t, n), o(r + t, n)), b = r + t, v = c + t, f = c > 0 ? (c - r) / c : 0, d = !i && f >= R;
17
- if (p) {
18
- const N = i ? void 0 : s.railFigure(U(b, n));
19
- return /* @__PURE__ */ l("div", { className: a.rail, role: "group", "aria-label": N === void 0 ? s.railBalanceUnknown : s.railBalance(N), tabIndex: 0, children: [
20
- /* @__PURE__ */ e(T, { className: a.railIcon }),
18
+ function $({ granted: s = 0, purchased: r = 0, grantCycle: t, burnedFraction: h, grantExpires: o, burnNote: p, unknown: n = !1, collapsed: x = !1, onTopUp: u, labels: E, locale: d = M }) {
19
+ const i = w(E), b = () => i.bar(m(s, d), m(r, d), m(s + r, d)), v = s + r, f = (t ?? s) + r, U = t !== void 0 && t > 0 ? (t - s) / t : 0, L = h ?? U, c = !n && L >= y;
20
+ if (x) {
21
+ const N = n ? void 0 : i.railFigure(A(v, d));
22
+ return /* @__PURE__ */ l("div", { className: a.rail, role: "group", "aria-label": N === void 0 ? i.railBalanceUnknown : i.railBalance(N), tabIndex: 0, children: [
23
+ /* @__PURE__ */ e(R, { className: a.railIcon }),
21
24
  /* @__PURE__ */ e("span", { className: a.railFigure, children: N ?? "—" }),
22
- i ? /* @__PURE__ */ e("span", { className: a.srOnly, children: s.railUnknown }) : /* @__PURE__ */ e(L, { granted: r, purchased: t, cycleTotal: v, tone: d ? "threshold" : "rest", label: u() })
25
+ n ? /* @__PURE__ */ e("span", { className: a.srOnly, children: i.railUnknown }) : /* @__PURE__ */ e(B, { granted: s, purchased: r, cycleTotal: f, tone: c ? "threshold" : "rest", label: b() })
23
26
  ] });
24
27
  }
25
- return /* @__PURE__ */ l("div", { className: a.chip, role: "group", "aria-label": s.region, tabIndex: 0, "data-threshold": d ? "true" : void 0, "data-unknown": i ? "true" : void 0, children: [
28
+ return /* @__PURE__ */ l("div", { className: a.chip, role: "group", "aria-label": i.region, tabIndex: 0, "data-threshold": c ? "true" : void 0, "data-unknown": n ? "true" : void 0, children: [
26
29
  /* @__PURE__ */ l("div", { className: a.header, children: [
27
- /* @__PURE__ */ e("span", { className: a.name, children: s.name }),
28
- d && /* @__PURE__ */ l("span", { className: a.pill, children: [
29
- Math.round(f * 100),
30
+ /* @__PURE__ */ e("span", { className: a.name, children: i.name }),
31
+ c && /* @__PURE__ */ l("span", { className: a.pill, children: [
32
+ Math.round(L * 100),
30
33
  "%"
31
34
  ] })
32
35
  ] }),
33
- i ? /* @__PURE__ */ l("div", { className: a.figure, children: [
36
+ n ? /* @__PURE__ */ l("div", { className: a.figure, children: [
34
37
  /* @__PURE__ */ e("span", { className: a.amount, "aria-hidden": "true", children: "—" }),
35
- /* @__PURE__ */ e(x, { className: a.retry }),
36
- /* @__PURE__ */ e("span", { className: a.srOnly, children: s.balanceUnknown })
38
+ /* @__PURE__ */ e(W, { className: a.retry }),
39
+ /* @__PURE__ */ e("span", { className: a.srOnly, children: i.balanceUnknown })
37
40
  ] }) : /* @__PURE__ */ l("div", { className: a.figure, children: [
38
- /* @__PURE__ */ e("span", { className: a.amount, children: o(b, n) }),
39
- /* @__PURE__ */ e("span", { className: a.unit, children: s.unit })
41
+ /* @__PURE__ */ e("span", { className: a.amount, children: m(v, d) }),
42
+ /* @__PURE__ */ e("span", { className: a.unit, children: i.unit })
40
43
  ] }),
41
- i ? /* @__PURE__ */ e("div", { className: a.bar, "data-tone": "unknown", "aria-hidden": "true" }) : /* @__PURE__ */ e(L, { granted: r, purchased: t, cycleTotal: v, tone: d ? "threshold" : "rest", label: u() }),
42
- !i && !d && /* @__PURE__ */ l("div", { className: a.details, children: [
44
+ n ? /* @__PURE__ */ e("div", { className: a.bar, "data-tone": "unknown", "aria-hidden": "true" }) : /* @__PURE__ */ e(B, { granted: s, purchased: r, cycleTotal: f, tone: c ? "threshold" : "rest", label: b() }),
45
+ !n && !c && /* @__PURE__ */ l("div", { className: a.details, children: [
43
46
  /* @__PURE__ */ l("dl", { className: a.split, children: [
44
47
  /* @__PURE__ */ l("div", { className: a.splitRow, children: [
45
48
  /* @__PURE__ */ l("dt", { className: a.splitLabel, children: [
46
49
  /* @__PURE__ */ e("span", { className: a.dotGranted, "aria-hidden": "true" }),
47
- s.granted
50
+ i.granted
48
51
  ] }),
49
- /* @__PURE__ */ e("dd", { className: a.splitAmount, children: o(r, n) })
52
+ /* @__PURE__ */ e("dd", { className: a.splitAmount, children: m(s, d) })
50
53
  ] }),
51
54
  /* @__PURE__ */ l("div", { className: a.splitRow, children: [
52
55
  /* @__PURE__ */ l("dt", { className: a.splitLabel, children: [
53
56
  /* @__PURE__ */ e("span", { className: a.dotPurchased, "aria-hidden": "true" }),
54
- s.purchased
57
+ i.purchased
55
58
  ] }),
56
- /* @__PURE__ */ e("dd", { className: a.splitAmount, children: o(t, n) })
59
+ /* @__PURE__ */ e("dd", { className: a.splitAmount, children: m(r, d) })
57
60
  ] })
58
61
  ] }),
59
- m && /* @__PURE__ */ e("p", { className: a.footer, children: s.grantExpiry(m) })
62
+ o && /* @__PURE__ */ e("p", { className: a.footer, children: i.grantExpiry(o) })
60
63
  ] }),
61
- d && h && /* @__PURE__ */ e("p", { className: a.burnNote, children: h }),
62
- !i && /* @__PURE__ */ e("button", { type: "button", className: a.topUp, "data-grown": d ? "true" : void 0, onClick: g, children: s.topUp })
64
+ c && p && /* @__PURE__ */ e("p", { className: a.burnNote, children: p }),
65
+ !n && u !== void 0 && /* @__PURE__ */ e("button", { type: "button", className: a.topUp, "data-grown": c ? "true" : void 0, onClick: u, children: i.topUp })
63
66
  ] });
64
67
  }
65
68
  export {
66
- I as WalletChip
69
+ $ as WalletChip
67
70
  };
68
71
  //# sourceMappingURL=WalletChip.js.map
@@ -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 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.\nconst THRESHOLD_BURN = 0.8\n\nexport interface WalletChipProps {\n /** Remaining granted tokens. Granted burns first and expires. */\n granted: number\n /** Remaining purchased tokens. Purchased never expires. */\n purchased: number\n /**\n * The grant issued for this cycle. The denominator for \"how much has burned\",\n * and so for the threshold; drawn as the bar's full width alongside purchased\n * so the filled portion shortens as the month runs.\n */\n grantCycle: 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 /** The balance could not be confirmed. Renders as unknown, never as zero. */\n unknown?: boolean\n /** The 56px rail. Abbreviates the balance and turns the split on its side. */\n collapsed?: boolean\n /** Top up. Wiring for the one button the sidebar is allowed to have. */\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 can be a plain string, so the whole object crosses the React\n * Server Component boundary and a Next App Router page can build it. The four\n * labels that carry a value mark its position with a `{placeholder}`; each\n * also accepts a function, which only works from a Client Component (B33-880).\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 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 const width = (n: number) => `${(n / cycleTotal) * 100}%`\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: width(granted) }} />\n <span className={styles.segmentPurchased} data-testid=\"segment-purchased\" style={{ width: width(purchased) }} />\n </div>\n )\n}\n\nexport function WalletChip({ granted, purchased, grantCycle, 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 degraded path's 0,0,0 figures. Calling it\n // only where a bar renders keeps that crash surface off the render that exists\n // precisely because fetching the balance already failed (B33-875).\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 const cycleTotal = grantCycle + purchased\n const burned = grantCycle > 0 ? (grantCycle - granted) / grantCycle : 0\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 {!unknown && (\n <button type=\"button\" className={styles.topUp} data-grown={isThreshold ? 'true' : undefined} onClick={onTopUp}>\n {t.topUp}\n </button>\n )}\n </div>\n )\n}\n"],"names":["THRESHOLD_BURN","Bar","granted","purchased","cycleTotal","tone","label","width","n","jsxs","styles","jsx","WalletChip","grantCycle","grantExpires","burnNote","unknown","collapsed","onTopUp","labels","locale","DEFAULT_WALLET_LOCALE","t","resolveWalletChipLabels","buildBarLabel","formatTokens","balance","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 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 /** 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 /** 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 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\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({ 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 type=\"button\" className={styles.topUp} data-grown={isThreshold ? 'true' : undefined} onClick={onTopUp}>\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","grantCycle","burnedFraction","grantExpires","burnNote","unknown","collapsed","onTopUp","labels","locale","DEFAULT_WALLET_LOCALE","t","resolveWalletChipLabels","buildBarLabel","formatTokens","balance","derivedBurn","burned","isThreshold","railBalance","thousandsFigure","WalletMark","RetryMark"],"mappings":"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"}