@boost33/ui 0.1.0 → 0.3.0

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 (86) hide show
  1. package/README.md +5 -2
  2. package/dist/anchored-overlay.d.ts +86 -0
  3. package/dist/anchored-overlay.js +125 -0
  4. package/dist/anchored-overlay.js.map +1 -0
  5. package/dist/components/AppShell/AppShell.d.ts +80 -0
  6. package/dist/components/AppShell/AppShell.js +59 -0
  7. package/dist/components/AppShell/AppShell.js.map +1 -0
  8. package/dist/components/AppShell/AppShell.module.css.js +20 -0
  9. package/dist/components/AppShell/AppShell.module.css.js.map +1 -0
  10. package/dist/components/AppSwitcher/AppSwitcher.d.ts +96 -0
  11. package/dist/components/AppSwitcher/AppSwitcher.js +81 -0
  12. package/dist/components/AppSwitcher/AppSwitcher.js.map +1 -0
  13. package/dist/components/AppSwitcher/AppSwitcher.module.css.js +44 -0
  14. package/dist/components/AppSwitcher/AppSwitcher.module.css.js.map +1 -0
  15. package/dist/components/AppSwitcher/icons.d.ts +20 -0
  16. package/dist/components/AppSwitcher/icons.js +25 -0
  17. package/dist/components/AppSwitcher/icons.js.map +1 -0
  18. package/dist/components/AppSwitcher/labels.d.ts +60 -0
  19. package/dist/components/AppSwitcher/labels.js +16 -0
  20. package/dist/components/AppSwitcher/labels.js.map +1 -0
  21. package/dist/components/Boost33Mark/Boost33Mark.d.ts +65 -0
  22. package/dist/components/Boost33Mark/Boost33Mark.js +62 -0
  23. package/dist/components/Boost33Mark/Boost33Mark.js.map +1 -0
  24. package/dist/components/Boost33Mark/Boost33Mark.module.css.js +12 -0
  25. package/dist/components/Boost33Mark/Boost33Mark.module.css.js.map +1 -0
  26. package/dist/components/NavItem/NavItem.d.ts +18 -2
  27. package/dist/components/NavItem/NavItem.js +18 -0
  28. package/dist/components/NavItem/NavItem.js.map +1 -0
  29. package/dist/components/NavItem/NavItem.module.css.js +18 -0
  30. package/dist/components/NavItem/NavItem.module.css.js.map +1 -0
  31. package/dist/components/NavSection/NavSection.d.ts +12 -0
  32. package/dist/components/NavSection/NavSection.js +14 -0
  33. package/dist/components/NavSection/NavSection.js.map +1 -0
  34. package/dist/components/NavSection/NavSection.module.css.js +10 -0
  35. package/dist/components/NavSection/NavSection.module.css.js.map +1 -0
  36. package/dist/components/PageHeader/PageHeader.d.ts +10 -0
  37. package/dist/components/PageHeader/PageHeader.js +15 -0
  38. package/dist/components/PageHeader/PageHeader.js.map +1 -0
  39. package/dist/components/PageHeader/PageHeader.module.css.js +16 -0
  40. package/dist/components/PageHeader/PageHeader.module.css.js.map +1 -0
  41. package/dist/components/ProductMark/ProductMark.d.ts +63 -0
  42. package/dist/components/ProductMark/ProductMark.js +73 -0
  43. package/dist/components/ProductMark/ProductMark.js.map +1 -0
  44. package/dist/components/ProductMark/ProductMark.module.css.js +10 -0
  45. package/dist/components/ProductMark/ProductMark.module.css.js.map +1 -0
  46. package/dist/components/Sidebar/NavHeightBudgetWarning.d.ts +3 -0
  47. package/dist/components/Sidebar/NavHeightBudgetWarning.js +28 -0
  48. package/dist/components/Sidebar/NavHeightBudgetWarning.js.map +1 -0
  49. package/dist/components/Sidebar/Sidebar.d.ts +19 -0
  50. package/dist/components/Sidebar/Sidebar.js +14 -0
  51. package/dist/components/Sidebar/Sidebar.js.map +1 -0
  52. package/dist/components/Sidebar/Sidebar.module.css.js +8 -0
  53. package/dist/components/Sidebar/Sidebar.module.css.js.map +1 -0
  54. package/dist/components/TopBar/TopBar.d.ts +8 -0
  55. package/dist/components/TopBar/TopBar.js +12 -0
  56. package/dist/components/TopBar/TopBar.js.map +1 -0
  57. package/dist/components/TopBar/TopBar.module.css.js +12 -0
  58. package/dist/components/TopBar/TopBar.module.css.js.map +1 -0
  59. package/dist/components/WalletChip/WalletChip.d.ts +60 -0
  60. package/dist/components/WalletChip/WalletChip.js +68 -0
  61. package/dist/components/WalletChip/WalletChip.js.map +1 -0
  62. package/dist/components/WalletChip/WalletChip.module.css.js +56 -0
  63. package/dist/components/WalletChip/WalletChip.module.css.js.map +1 -0
  64. package/dist/components/WalletChip/format.d.ts +47 -0
  65. package/dist/components/WalletChip/format.js +29 -0
  66. package/dist/components/WalletChip/format.js.map +1 -0
  67. package/dist/components/WalletChip/icons.d.ts +8 -0
  68. package/dist/components/WalletChip/icons.js +18 -0
  69. package/dist/components/WalletChip/icons.js.map +1 -0
  70. package/dist/components/WalletChip/labels.d.ts +143 -0
  71. package/dist/components/WalletChip/labels.js +38 -0
  72. package/dist/components/WalletChip/labels.js.map +1 -0
  73. package/dist/components/Wordmark/Wordmark.d.ts +12 -0
  74. package/dist/components/Wordmark/Wordmark.js +12 -0
  75. package/dist/components/Wordmark/Wordmark.js.map +1 -0
  76. package/dist/components/Wordmark/Wordmark.module.css.js +12 -0
  77. package/dist/components/Wordmark/Wordmark.module.css.js.map +1 -0
  78. package/dist/index.d.ts +26 -2
  79. package/dist/index.js +41 -35
  80. package/dist/index.js.map +1 -1
  81. package/dist/nav-height-budget.d.ts +58 -1
  82. package/dist/nav-height-budget.js +38 -0
  83. package/dist/nav-height-budget.js.map +1 -0
  84. package/dist/style.css +1 -1
  85. package/dist/theme.css +189 -0
  86. package/package.json +17 -4
@@ -0,0 +1,63 @@
1
+ /**
2
+ * The four products Identity serves, as one list. Everything else keys off it:
3
+ * ProductSlug is derived from it, and GLYPHS is typed against it so adding a
4
+ * slug without artwork is a compile error rather than a mark that silently
5
+ * draws nothing. These are Identity's *slugs*, not display names -- the Wallet
6
+ * keys its own record on `'Boost Score' | ... | 'Wallet'`, which a rename
7
+ * breaks, and that is the exact trap this ordering avoids.
8
+ */
9
+ export declare const PRODUCT_SLUGS: readonly ["wallet", "score", "voice", "outbound"];
10
+ /** One of Identity's four product slugs. */
11
+ export type ProductSlug = (typeof PRODUCT_SLUGS)[number];
12
+ export interface ProductMarkProps {
13
+ /**
14
+ * Identity's product slug. An unknown one -- a product the served registry
15
+ * carries but this package has no artwork for yet -- draws the bare tile
16
+ * rather than throwing (ADR-0003's safe degrade), so the registry can run
17
+ * ahead of a mark release.
18
+ */
19
+ slug: string;
20
+ /** Rendered width and height in px. Defaults to 20, the Wallet's own default. */
21
+ size?: number;
22
+ /**
23
+ * Dims the tile for a product the account is not entitled to -- the switcher's
24
+ * unowned rows need it. It swaps the **ground only**, to --color-neutral-300;
25
+ * the glyph stays white. Fading the glyph too would cost the shape the one
26
+ * thing telling the four marks apart, and it is a prop rather than a wrapper
27
+ * `opacity-40 grayscale` filter because that lands on a mid-grey visibly
28
+ * darker than the grey Paper draws.
29
+ *
30
+ * **Only use this where the mark is decorative.** White on --color-neutral-300
31
+ * is 1.41:1, well under WCAG 1.4.11's 3:1, and the reason that is not a defect
32
+ * is that the criterion covers graphics *required to understand the content* --
33
+ * which this is not, while the mark is `aria-hidden`, the product's name sits
34
+ * beside it as text, and entitlement is additionally stated in words (the
35
+ * switcher's "Not on plan" tag). Combining `muted` with `title`, so the mark
36
+ * stands alone as the sole carrier of a product's identity, removes every one
37
+ * of those and makes the ratio bite. That combination is out of contract; no
38
+ * call site in this package uses it. See ADR-0012, which also records why the
39
+ * ground is not darkened and what to reach for if this is ever revisited.
40
+ */
41
+ muted?: boolean;
42
+ /**
43
+ * An accessible name. Omit it -- the default -- and the mark is decorative:
44
+ * `aria-hidden`, contributing nothing to the accessibility tree. That is the
45
+ * right answer in the switcher, where the product's name sits beside it and a
46
+ * name here would only make a screen reader say it twice. Pass one only where
47
+ * the mark stands alone as the sole carrier of meaning.
48
+ *
49
+ * Passing this is what takes the mark out of `muted`'s contract -- see the
50
+ * note there. A mark that carries meaning on its own has to be legible on its
51
+ * own, and the muted ground is not.
52
+ */
53
+ title?: string;
54
+ /** Escape hatch for a consumer that needs to position the mark. */
55
+ className?: string;
56
+ }
57
+ /**
58
+ * The tile-and-glyph mark for one Boost33 product -- a rounded --color-neutral-950
59
+ * square with the product's glyph in --color-primary-foreground. The four marks
60
+ * share one palette and are told apart by shape alone; there is zero brand
61
+ * orange in any of them.
62
+ */
63
+ export declare function ProductMark({ slug, size, muted, title, className }: ProductMarkProps): import("react").JSX.Element;
@@ -0,0 +1,73 @@
1
+ import { jsxs as o, jsx as r, Fragment as l } from "react/jsx-runtime";
2
+ import { useId as a } from "react";
3
+ import i from "./ProductMark.module.css.js";
4
+ const k = 20, x = ["wallet", "score", "voice", "outbound"], f = {
5
+ // A credit card with its magnetic stripe: an outline card (fill none, so the
6
+ // tile shows through) plus a filled stripe across it.
7
+ wallet: /* @__PURE__ */ o(l, { children: [
8
+ /* @__PURE__ */ r("rect", { x: "5", y: "10", width: "30", height: "20", rx: "4", fill: "none", stroke: "currentColor", strokeWidth: "3" }),
9
+ /* @__PURE__ */ r("rect", { x: "5", y: "16", width: "30", height: "5", fill: "currentColor" })
10
+ ] }),
11
+ // A gauge with the needle swung high: an arc, the needle, and the hub it
12
+ // pivots on. Arc and needle stroke at width 4 with round caps.
13
+ score: /* @__PURE__ */ o(l, { children: [
14
+ /* @__PURE__ */ r("path", { d: "M6 26 A14 14 0 0 1 34 26", fill: "none", stroke: "currentColor", strokeWidth: "4", strokeLinecap: "round" }),
15
+ /* @__PURE__ */ r("line", { x1: "20", y1: "26", x2: "28", y2: "14", stroke: "currentColor", strokeWidth: "4", strokeLinecap: "round" }),
16
+ /* @__PURE__ */ r("circle", { cx: "20", cy: "26", r: "3", fill: "currentColor" })
17
+ ] }),
18
+ // A soundwave, tall in the middle: three pill bars, all centred on y=20.
19
+ voice: /* @__PURE__ */ o(l, { children: [
20
+ /* @__PURE__ */ r("rect", { x: "6", y: "14", width: "7", height: "12", rx: "3.5", fill: "currentColor" }),
21
+ /* @__PURE__ */ r("rect", { x: "16.5", y: "4", width: "7", height: "32", rx: "3.5", fill: "currentColor" }),
22
+ /* @__PURE__ */ r("rect", { x: "27", y: "14", width: "7", height: "12", rx: "3.5", fill: "currentColor" })
23
+ ] }),
24
+ // An arrow leaving through the top-right: the shaft and the elbow of its head,
25
+ // both stroke width 4 with round caps and joins.
26
+ outbound: /* @__PURE__ */ o(l, { children: [
27
+ /* @__PURE__ */ r("line", { x1: "8", y1: "32", x2: "32", y2: "8", stroke: "currentColor", strokeWidth: "4", strokeLinecap: "round", strokeLinejoin: "round" }),
28
+ /* @__PURE__ */ r("path", { d: "M16 8 H32 V24", fill: "none", stroke: "currentColor", strokeWidth: "4", strokeLinecap: "round", strokeLinejoin: "round" })
29
+ ] })
30
+ };
31
+ function y(e) {
32
+ return x.includes(e);
33
+ }
34
+ function v({ slug: e, size: n = k, muted: u, title: c, className: d }) {
35
+ const s = a(), t = c == null, h = y(e) ? f[e] : null;
36
+ return /* @__PURE__ */ o(
37
+ "svg",
38
+ {
39
+ className: d != null ? `${i.mark} ${d}` : i.mark,
40
+ width: n,
41
+ height: n,
42
+ viewBox: "0 0 40 40",
43
+ role: t ? void 0 : "img",
44
+ "aria-hidden": t ? "true" : void 0,
45
+ "aria-labelledby": t ? void 0 : s,
46
+ children: [
47
+ !t && /* @__PURE__ */ r("title", { id: s, children: c }),
48
+ /* @__PURE__ */ r(
49
+ "rect",
50
+ {
51
+ className: i.ground,
52
+ width: "40",
53
+ height: "40",
54
+ style: { fill: u ? "var(--color-neutral-300)" : "var(--color-neutral-950)" }
55
+ }
56
+ ),
57
+ h && /* @__PURE__ */ r(
58
+ "g",
59
+ {
60
+ transform: "translate(10 10) scale(0.5)",
61
+ style: { color: "var(--color-primary-foreground)" },
62
+ children: h
63
+ }
64
+ )
65
+ ]
66
+ }
67
+ );
68
+ }
69
+ export {
70
+ x as PRODUCT_SLUGS,
71
+ v as ProductMark
72
+ };
73
+ //# sourceMappingURL=ProductMark.js.map
@@ -0,0 +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 four products Identity serves, as one list. Everything else keys off it:\n * ProductSlug is derived from it, and GLYPHS is typed against it so adding a\n * slug without artwork is a compile error rather than a mark that silently\n * draws nothing. These are Identity's *slugs*, not display names -- the Wallet\n * keys its own record on `'Boost Score' | ... | 'Wallet'`, which a rename\n * breaks, and that is the exact trap this ordering avoids.\n */\nexport const PRODUCT_SLUGS = ['wallet', 'score', 'voice', 'outbound'] as const\n\n/** One of Identity's four product slugs. */\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 score: (\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 voice: (\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 outbound: (\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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"}
@@ -0,0 +1,10 @@
1
+ const t = "_mark_1jt5g_6", o = "_ground_1jt5g_17", r = {
2
+ mark: t,
3
+ ground: o
4
+ };
5
+ export {
6
+ r as default,
7
+ o as ground,
8
+ t as mark
9
+ };
10
+ //# sourceMappingURL=ProductMark.module.css.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ProductMark.module.css.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;"}
@@ -0,0 +1,3 @@
1
+ export declare function NavHeightBudgetWarning({ label }: {
2
+ label: string;
3
+ }): import("react").JSX.Element | null;
@@ -0,0 +1,28 @@
1
+ "use client";
2
+ import { jsx as a } from "react/jsx-runtime";
3
+ import { useRef as s, useEffect as u } from "react";
4
+ import { checkNavHeightBudget as c, measureNavHeight as d, NAV_HEIGHT_BUDGET_PX as l } from "../../nav-height-budget.js";
5
+ function v({ label: r }) {
6
+ const o = s(null);
7
+ return u(() => {
8
+ var i;
9
+ if (process.env.NODE_ENV === "production") return;
10
+ const t = (i = o.current) == null ? void 0 : i.parentElement;
11
+ if (!t) return;
12
+ const n = t.querySelector(":scope > nav");
13
+ if (!(n instanceof HTMLElement)) {
14
+ t.querySelector("nav") && console.warn(
15
+ `@boost33/ui Sidebar ("${r}"): the nav is nested inside another element, so it is not the sidebar's scroll region. Make the <nav> a direct child of the Sidebar, or a nav longer than the column will paint past its bottom edge instead of scrolling. See ADR-0005.`
16
+ );
17
+ return;
18
+ }
19
+ const e = c(d(n), n.clientHeight);
20
+ e.fits || console.warn(
21
+ `@boost33/ui Sidebar ("${r}"): the nav needs ${e.usedPx}px and the column has ${e.budgetPx}px for it, so it is ${e.overBy}px over and scrolling. Nothing is lost -- the wallet chip and footer stay pinned -- but the shell's nav budget is about ${l}px at a 900px viewport, and a nav past it is a nav worth cutting. navContentHeight() and navBudgetForViewport(), exported from this package, check a nav's shape before it renders.`
22
+ );
23
+ }), process.env.NODE_ENV === "production" ? null : /* @__PURE__ */ a("span", { ref: o, hidden: !0, "aria-hidden": "true" });
24
+ }
25
+ export {
26
+ v as NavHeightBudgetWarning
27
+ };
28
+ //# sourceMappingURL=NavHeightBudgetWarning.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NavHeightBudgetWarning.js","sources":["../../../src/components/Sidebar/NavHeightBudgetWarning.tsx"],"sourcesContent":["'use client'\n\nimport { useEffect, useRef } from 'react'\nimport { NAV_HEIGHT_BUDGET_PX, checkNavHeightBudget, measureNavHeight } from '../../nav-height-budget'\n\n/*\n * The development-mode half of the nav budget, split out as the sidebar's one\n * client module.\n *\n * Sidebar.module.css stops an over-long nav from painting past the column, but\n * silently: the consumer sees a scrollbar and never learns their nav is too\n * long. The budget helpers in nav-height-budget.ts were written for exactly\n * that -- Boost Outbound's admin role has exceeded the budget once already --\n * and until this hook nothing in the shipped components ever called them, so a\n * consumer was protected only if they remembered to check by hand.\n *\n * It lives apart from Sidebar so Sidebar itself -- and the whole shell that\n * renders it -- can stay a Server Component (B33-846). A `console.warn` in\n * an effect is the only reason the sidebar column would otherwise be forced onto\n * the client, and a consumer should not ship the shell's static half to the\n * browser to get a development-only warning. So this is the boundary: it carries\n * \"use client\", and renders nothing a consumer sees. In production it is inert\n * and renders null; in development it renders one empty, hidden marker so the\n * effect has a handle on the sidebar element to measure from.\n *\n * `process.env.NODE_ENV` rather than an `import.meta` flag because every\n * bundler a consumer might use replaces that exact expression at build time\n * and drops the branch, which is how React itself ships its own dev warnings.\n *\n * Best effort by design: it re-measures on every render, after paint. Sidebar\n * re-renders when the consumer's stacked contents change, so a nav that grows\n * is re-checked; a nav that only overflows after a font swap or a window resize\n * is not caught, and does not need to be -- the case this exists for is a nav\n * that is too long for the column the moment it is rendered.\n */\nexport function NavHeightBudgetWarning({ label }: { label: string }) {\n // The marker sits as a direct child of the sidebar, so its parent is the\n // column and `:scope > nav` from there finds the same nav the CSS scroll\n // region rule selects.\n const marker = useRef<HTMLSpanElement>(null)\n\n useEffect(() => {\n if (process.env.NODE_ENV === 'production') return\n\n const element = marker.current?.parentElement\n if (!element) return\n\n const nav = element.querySelector(':scope > nav')\n if (!(nav instanceof HTMLElement)) {\n // Only worth saying when there is a nav and it is in the wrong place.\n // A sidebar that holds no nav at all is an ordinary thing to render.\n if (element.querySelector('nav')) {\n console.warn(\n `@boost33/ui Sidebar (\"${label}\"): the nav is nested inside another element, so it is not the sidebar's scroll ` +\n `region. Make the <nav> a direct child of the Sidebar, or a nav longer than the column will paint past its ` +\n `bottom edge instead of scrolling. See ADR-0005.`,\n )\n }\n return\n }\n\n const budget = checkNavHeightBudget(measureNavHeight(nav), nav.clientHeight)\n if (budget.fits) return\n\n console.warn(\n `@boost33/ui Sidebar (\"${label}\"): the nav needs ${budget.usedPx}px and the column has ${budget.budgetPx}px for it, ` +\n `so it is ${budget.overBy}px over and scrolling. Nothing is lost -- the wallet chip and footer stay pinned -- but ` +\n `the shell's nav budget is about ${NAV_HEIGHT_BUDGET_PX}px at a 900px viewport, and a nav past it is a nav worth ` +\n `cutting. navContentHeight() and navBudgetForViewport(), exported from this package, check a nav's shape before ` +\n `it renders.`,\n )\n })\n\n if (process.env.NODE_ENV === 'production') return null\n return <span ref={marker} hidden aria-hidden=\"true\" />\n}\n"],"names":["NavHeightBudgetWarning","label","marker","useRef","useEffect","element","_a","nav","budget","checkNavHeightBudget","measureNavHeight","NAV_HEIGHT_BUDGET_PX"],"mappings":";;;;AAmCO,SAASA,EAAuB,EAAE,OAAAC,KAA4B;AAInE,QAAMC,IAASC,EAAwB,IAAI;AAkC3C,SAhCAC,EAAU,MAAM;;AACd,QAAI,QAAQ,IAAI,aAAa,aAAc;AAE3C,UAAMC,KAAUC,IAAAJ,EAAO,YAAP,gBAAAI,EAAgB;AAChC,QAAI,CAACD,EAAS;AAEd,UAAME,IAAMF,EAAQ,cAAc,cAAc;AAChD,QAAI,EAAEE,aAAe,cAAc;AAGjC,MAAIF,EAAQ,cAAc,KAAK,KAC7B,QAAQ;AAAA,QACN,yBAAyBJ,CAAK;AAAA,MAAA;AAKlC;AAAA,IACF;AAEA,UAAMO,IAASC,EAAqBC,EAAiBH,CAAG,GAAGA,EAAI,YAAY;AAC3E,IAAIC,EAAO,QAEX,QAAQ;AAAA,MACN,yBAAyBP,CAAK,qBAAqBO,EAAO,MAAM,yBAAyBA,EAAO,QAAQ,uBAC1FA,EAAO,MAAM,2HACUG,CAAoB;AAAA,IAAA;AAAA,EAI7D,CAAC,GAEG,QAAQ,IAAI,aAAa,eAAqB,yBAC1C,QAAA,EAAK,KAAKT,GAAQ,QAAM,IAAC,eAAY,QAAO;AACtD;"}
@@ -0,0 +1,19 @@
1
+ import { ReactNode } from 'react';
2
+ export interface SidebarProps {
3
+ /** Names the sidebar landmark, so it stays distinct from other regions on the page. */
4
+ label: string;
5
+ /** Sidebar-on-the-rail state. Narrows to the 56px rail; the pieces inside collapse themselves. */
6
+ collapsed?: boolean;
7
+ /**
8
+ * What the sidebar holds -- wordmark, switcher, nav sections, wallet chip, footer. Passed in, never hardcoded.
9
+ *
10
+ * **Put the nav in a `<nav>` element that is a direct child of the sidebar.**
11
+ * That element is the sidebar's scroll region: a nav longer than the column
12
+ * scrolls inside itself, and everything above and below it -- the wordmark,
13
+ * the switcher, the wallet chip, the footer -- stays pinned. A nav nested
14
+ * inside another element does not get that, and says so in development. See
15
+ * ADR-0005.
16
+ */
17
+ children: ReactNode;
18
+ }
19
+ export declare function Sidebar({ label, collapsed, children }: SidebarProps): import("react").JSX.Element;
@@ -0,0 +1,14 @@
1
+ import { jsxs as i, jsx as t } from "react/jsx-runtime";
2
+ import "react";
3
+ import { NavHeightBudgetWarning as o } from "./NavHeightBudgetWarning.js";
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: [
7
+ /* @__PURE__ */ t(o, { label: r }),
8
+ e
9
+ ] });
10
+ }
11
+ export {
12
+ p as Sidebar
13
+ };
14
+ //# sourceMappingURL=Sidebar.js.map
@@ -0,0 +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;"}
@@ -0,0 +1,8 @@
1
+ const s = "_sidebar_17kel_7", e = {
2
+ sidebar: s
3
+ };
4
+ export {
5
+ e as default,
6
+ s as sidebar
7
+ };
8
+ //# sourceMappingURL=Sidebar.module.css.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Sidebar.module.css.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;"}
@@ -0,0 +1,8 @@
1
+ import { ReactNode } from 'react';
2
+ export interface TopBarProps {
3
+ /** The breadcrumb naming the current location, on the left of the bar. */
4
+ breadcrumb: ReactNode;
5
+ /** Shell actions on the right -- search, notifications, the account. Omit to leave the right empty. */
6
+ actions?: ReactNode;
7
+ }
8
+ export declare function TopBar({ breadcrumb, actions }: TopBarProps): import("react").JSX.Element;
@@ -0,0 +1,12 @@
1
+ import { jsxs as i, jsx as e } from "react/jsx-runtime";
2
+ import r from "./TopBar.module.css.js";
3
+ function m({ breadcrumb: a, actions: s }) {
4
+ return /* @__PURE__ */ i("div", { className: r.bar, children: [
5
+ /* @__PURE__ */ e("div", { className: r.breadcrumb, children: a }),
6
+ s != null && /* @__PURE__ */ e("div", { className: r.actions, children: s })
7
+ ] });
8
+ }
9
+ export {
10
+ m as TopBar
11
+ };
12
+ //# sourceMappingURL=TopBar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TopBar.js","sources":["../../../src/components/TopBar/TopBar.tsx"],"sourcesContent":["import type { ReactNode } from 'react'\nimport styles from './TopBar.module.css'\n\nexport interface TopBarProps {\n /** The breadcrumb naming the current location, on the left of the bar. */\n breadcrumb: ReactNode\n /** Shell actions on the right -- search, notifications, the account. Omit to leave the right empty. */\n actions?: ReactNode\n}\n\nexport function TopBar({ breadcrumb, actions }: TopBarProps) {\n return (\n <div className={styles.bar}>\n <div className={styles.breadcrumb}>{breadcrumb}</div>\n {actions != null && <div className={styles.actions}>{actions}</div>}\n </div>\n )\n}\n"],"names":["TopBar","breadcrumb","actions","jsxs","styles","jsx"],"mappings":";;AAUO,SAASA,EAAO,EAAE,YAAAC,GAAY,SAAAC,KAAwB;AAC3D,SACE,gBAAAC,EAAC,OAAA,EAAI,WAAWC,EAAO,KACrB,UAAA;AAAA,IAAA,gBAAAC,EAAC,OAAA,EAAI,WAAWD,EAAO,YAAa,UAAAH,GAAW;AAAA,IAC9CC,KAAW,QAAQ,gBAAAG,EAAC,SAAI,WAAWD,EAAO,SAAU,UAAAF,EAAA,CAAQ;AAAA,EAAA,GAC/D;AAEJ;"}
@@ -0,0 +1,12 @@
1
+ const r = "_bar_r1jjn_6", a = "_breadcrumb_r1jjn_19", c = "_actions_r1jjn_32", n = {
2
+ bar: r,
3
+ breadcrumb: a,
4
+ actions: c
5
+ };
6
+ export {
7
+ c as actions,
8
+ r as bar,
9
+ a as breadcrumb,
10
+ n as default
11
+ };
12
+ //# sourceMappingURL=TopBar.module.css.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TopBar.module.css.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;"}
@@ -0,0 +1,60 @@
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;
23
+ /** The 56px rail. Abbreviates the balance and turns the split on its side. */
24
+ collapsed?: boolean;
25
+ /** Top up. Wiring for the one button the sidebar is allowed to have. */
26
+ onTopUp?: () => void;
27
+ /**
28
+ * The chip's user-facing strings. Every field has an English default, so a
29
+ * consumer supplies only the ones it translates; the first two consumers are
30
+ * French-default. The package takes no i18n dependency -- it renders what it
31
+ * is given (B33-849).
32
+ *
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).
37
+ */
38
+ labels?: Partial<WalletChipLabels>;
39
+ /**
40
+ * The BCP-47 language tag the chip's figures are formatted in. Defaults to
41
+ * `en-US`, which is what the chip rendered before this prop existed.
42
+ *
43
+ * `labels` gives the consumer the chip's words; this gives it their numbers.
44
+ * French writes `4 600` where English writes `4,600`, and a label cannot fix
45
+ * it -- the label functions receive the figures already formatted, so before
46
+ * this prop there was no hook at all (B33-876). Grouping and the decimal
47
+ * separator are CLDR facts about a locale rather than a property of the
48
+ * consumer's sentence, so a tag is enough: `Intl.NumberFormat` knows the rest,
49
+ * and a tag is a plain string, which a Server Component can pass where a
50
+ * formatter function could not.
51
+ *
52
+ * It reaches the visible figures and, because the same strings are
53
+ * interpolated into them, the `bar` and `railBalance` accessible names.
54
+ *
55
+ * The `k` after the rail's abbreviated balance is a word, not a number, so it
56
+ * is `labels.railFigure` and not this.
57
+ */
58
+ locale?: string;
59
+ }
60
+ export declare function WalletChip({ granted, purchased, grantCycle, grantExpires, burnNote, unknown, collapsed, onTopUp, labels, locale }: WalletChipProps): import("react").JSX.Element;
@@ -0,0 +1,68 @@
1
+ "use client";
2
+ import { jsxs as l, jsx as e } from "react/jsx-runtime";
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) } })
13
+ ] });
14
+ }
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 }),
21
+ /* @__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() })
23
+ ] });
24
+ }
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: [
26
+ /* @__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
+ "%"
31
+ ] })
32
+ ] }),
33
+ i ? /* @__PURE__ */ l("div", { className: a.figure, children: [
34
+ /* @__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 })
37
+ ] }) : /* @__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 })
40
+ ] }),
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: [
43
+ /* @__PURE__ */ l("dl", { className: a.split, children: [
44
+ /* @__PURE__ */ l("div", { className: a.splitRow, children: [
45
+ /* @__PURE__ */ l("dt", { className: a.splitLabel, children: [
46
+ /* @__PURE__ */ e("span", { className: a.dotGranted, "aria-hidden": "true" }),
47
+ s.granted
48
+ ] }),
49
+ /* @__PURE__ */ e("dd", { className: a.splitAmount, children: o(r, n) })
50
+ ] }),
51
+ /* @__PURE__ */ l("div", { className: a.splitRow, children: [
52
+ /* @__PURE__ */ l("dt", { className: a.splitLabel, children: [
53
+ /* @__PURE__ */ e("span", { className: a.dotPurchased, "aria-hidden": "true" }),
54
+ s.purchased
55
+ ] }),
56
+ /* @__PURE__ */ e("dd", { className: a.splitAmount, children: o(t, n) })
57
+ ] })
58
+ ] }),
59
+ m && /* @__PURE__ */ e("p", { className: a.footer, children: s.grantExpiry(m) })
60
+ ] }),
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 })
63
+ ] });
64
+ }
65
+ export {
66
+ I as WalletChip
67
+ };
68
+ //# sourceMappingURL=WalletChip.js.map
@@ -0,0 +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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"}
@@ -0,0 +1,56 @@
1
+ const t = "_chip_xyt7t_37", _ = "_header_xyt7t_74", n = "_name_xyt7t_83", s = "_pill_xyt7t_162", e = "_figure_xyt7t_171", o = "_amount_xyt7t_186", r = "_unit_xyt7t_195", a = "_retry_xyt7t_200", i = "_bar_xyt7t_209", c = "_segmentGranted_xyt7t_230", l = "_segmentPurchased_xyt7t_231", y = "_details_xyt7t_251", x = "_split_xyt7t_260", d = "_splitRow_xyt7t_267", p = "_splitLabel_xyt7t_273", u = "_splitAmount_xyt7t_281", m = "_dotGranted_xyt7t_289", g = "_dotPurchased_xyt7t_290", h = "_footer_xyt7t_304", b = "_burnNote_xyt7t_312", f = "_topUp_xyt7t_87", G = "_rail_xyt7t_378", P = "_railIcon_xyt7t_392", w = "_railFigure_xyt7t_398", A = "_srOnly_xyt7t_411", F = {
2
+ chip: t,
3
+ header: _,
4
+ name: n,
5
+ pill: s,
6
+ figure: e,
7
+ amount: o,
8
+ unit: r,
9
+ retry: a,
10
+ bar: i,
11
+ segmentGranted: c,
12
+ segmentPurchased: l,
13
+ details: y,
14
+ split: x,
15
+ splitRow: d,
16
+ splitLabel: p,
17
+ splitAmount: u,
18
+ dotGranted: m,
19
+ dotPurchased: g,
20
+ footer: h,
21
+ burnNote: b,
22
+ topUp: f,
23
+ rail: G,
24
+ railIcon: P,
25
+ railFigure: w,
26
+ srOnly: A
27
+ };
28
+ export {
29
+ o as amount,
30
+ i as bar,
31
+ b as burnNote,
32
+ t as chip,
33
+ F as default,
34
+ y as details,
35
+ m as dotGranted,
36
+ g as dotPurchased,
37
+ e as figure,
38
+ h as footer,
39
+ _ as header,
40
+ n as name,
41
+ s as pill,
42
+ G as rail,
43
+ w as railFigure,
44
+ P as railIcon,
45
+ a as retry,
46
+ c as segmentGranted,
47
+ l as segmentPurchased,
48
+ x as split,
49
+ u as splitAmount,
50
+ p as splitLabel,
51
+ d as splitRow,
52
+ A as srOnly,
53
+ f as topUp,
54
+ r as unit
55
+ };
56
+ //# sourceMappingURL=WalletChip.module.css.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WalletChip.module.css.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The language tag the chip formats in when the consumer names none. Today's
3
+ * rendering, so the default path is byte-identical to the one the fidelity
4
+ * baselines were captured from.
5
+ */
6
+ export declare const DEFAULT_WALLET_LOCALE = "en-US";
7
+ /**
8
+ * The full balance, thousands grouped the way the locale groups them:
9
+ * 4600 -> `"4,600"` in `en-US`, `"4 600"` in `fr-FR`, `"4.600"` in `de-DE`.
10
+ *
11
+ * The `en-US` default is what `toLocaleString('en-US')` produced, character for
12
+ * character -- it is the same CLDR data reached through the constructor that can
13
+ * be cached.
14
+ */
15
+ export declare function formatTokens(value: number, locale?: string): string;
16
+ /**
17
+ * The rail's short form: one decimal thousand, always. At 56px there is no room
18
+ * for four digits and a separator, and "4.6k" still answers "am I fine?".
19
+ *
20
+ * Returns **the figure alone** -- `"4.6"` in `en-US`, `"4,6"` in `fr-FR`. The
21
+ * mark that follows it is a word rather than a number ("k" in English, "Tsd." in
22
+ * German, "ألف" in Arabic), and French puts a space in front of it where English
23
+ * does not, so it lives with the chip's other words, as `labels.railFigure`
24
+ * (B33-876).
25
+ *
26
+ * Not `Intl.NumberFormat`'s own `notation: 'compact'`, for two concrete reasons.
27
+ * It renders `en-US` as `"4.6K"`, a capital the baselines do not have. And
28
+ * German does not abbreviate four digits at all, so it renders `"4600,0"` -- six
29
+ * characters and a decimal, in a 56px rail, longer than the number it was
30
+ * supposed to shorten. One decimal thousand is this package's decision about a
31
+ * fixed-width rail, not CLDR's about a locale; the separator inside it is CLDR's,
32
+ * and that is the part `locale` fixes.
33
+ *
34
+ * Two default-path differences from the `(value / 1000).toFixed(1)` this
35
+ * replaced, both narrower than any state the baselines hold and both left
36
+ * deliberately, but written down because they reach four products (B33-876):
37
+ *
38
+ * - **A balance landing on an exact half rounds the other way.** `toFixed`
39
+ * rounds the double's exact binary expansion, so 150 tokens printed `0.1`;
40
+ * `Intl` rounds the shortest decimal the double stands for, so it prints
41
+ * `0.2`. `Intl` agrees with the reader here, and the old answer was a float
42
+ * artefact rather than a decision.
43
+ * - **A balance over a million is grouped.** `toFixed` gave `1234.6`, this
44
+ * gives `1,234.6`. Neither fits a 56px rail; that is a rail question, not a
45
+ * formatting one.
46
+ */
47
+ export declare function thousandsFigure(value: number, locale?: string): string;