@boost33/ui 0.2.0 → 0.3.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 (79) hide show
  1. package/README.md +5 -2
  2. package/dist/anchored-overlay.d.ts +28 -11
  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 +19 -4
  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.js +62 -0
  22. package/dist/components/Boost33Mark/Boost33Mark.js.map +1 -0
  23. package/dist/components/Boost33Mark/Boost33Mark.module.css.js +12 -0
  24. package/dist/components/Boost33Mark/Boost33Mark.module.css.js.map +1 -0
  25. package/dist/components/NavItem/NavItem.d.ts +12 -2
  26. package/dist/components/NavItem/NavItem.js +18 -0
  27. package/dist/components/NavItem/NavItem.js.map +1 -0
  28. package/dist/components/NavItem/NavItem.module.css.js +18 -0
  29. package/dist/components/NavItem/NavItem.module.css.js.map +1 -0
  30. package/dist/components/NavSection/NavSection.js +14 -0
  31. package/dist/components/NavSection/NavSection.js.map +1 -0
  32. package/dist/components/NavSection/NavSection.module.css.js +10 -0
  33. package/dist/components/NavSection/NavSection.module.css.js.map +1 -0
  34. package/dist/components/PageHeader/PageHeader.js +15 -0
  35. package/dist/components/PageHeader/PageHeader.js.map +1 -0
  36. package/dist/components/PageHeader/PageHeader.module.css.js +16 -0
  37. package/dist/components/PageHeader/PageHeader.module.css.js.map +1 -0
  38. package/dist/components/ProductMark/ProductMark.d.ts +15 -0
  39. package/dist/components/ProductMark/ProductMark.js +73 -0
  40. package/dist/components/ProductMark/ProductMark.js.map +1 -0
  41. package/dist/components/ProductMark/ProductMark.module.css.js +10 -0
  42. package/dist/components/ProductMark/ProductMark.module.css.js.map +1 -0
  43. package/dist/components/Sidebar/NavHeightBudgetWarning.d.ts +3 -0
  44. package/dist/components/Sidebar/NavHeightBudgetWarning.js +28 -0
  45. package/dist/components/Sidebar/NavHeightBudgetWarning.js.map +1 -0
  46. package/dist/components/Sidebar/Sidebar.js +14 -0
  47. package/dist/components/Sidebar/Sidebar.js.map +1 -0
  48. package/dist/components/Sidebar/Sidebar.module.css.js +8 -0
  49. package/dist/components/Sidebar/Sidebar.module.css.js.map +1 -0
  50. package/dist/components/TopBar/TopBar.js +12 -0
  51. package/dist/components/TopBar/TopBar.js.map +1 -0
  52. package/dist/components/TopBar/TopBar.module.css.js +12 -0
  53. package/dist/components/TopBar/TopBar.module.css.js.map +1 -0
  54. package/dist/components/WalletChip/WalletChip.d.ts +34 -1
  55. package/dist/components/WalletChip/WalletChip.js +68 -0
  56. package/dist/components/WalletChip/WalletChip.js.map +1 -0
  57. package/dist/components/WalletChip/WalletChip.module.css.js +56 -0
  58. package/dist/components/WalletChip/WalletChip.module.css.js.map +1 -0
  59. package/dist/components/WalletChip/format.d.ts +45 -5
  60. package/dist/components/WalletChip/format.js +29 -0
  61. package/dist/components/WalletChip/format.js.map +1 -0
  62. package/dist/components/WalletChip/icons.js +18 -0
  63. package/dist/components/WalletChip/icons.js.map +1 -0
  64. package/dist/components/WalletChip/labels.d.ts +143 -0
  65. package/dist/components/WalletChip/labels.js +38 -0
  66. package/dist/components/WalletChip/labels.js.map +1 -0
  67. package/dist/components/Wordmark/Wordmark.js +12 -0
  68. package/dist/components/Wordmark/Wordmark.js.map +1 -0
  69. package/dist/components/Wordmark/Wordmark.module.css.js +12 -0
  70. package/dist/components/Wordmark/Wordmark.module.css.js.map +1 -0
  71. package/dist/index.d.ts +8 -2
  72. package/dist/index.js +41 -420
  73. package/dist/index.js.map +1 -1
  74. package/dist/nav-height-budget.d.ts +51 -13
  75. package/dist/nav-height-budget.js +38 -0
  76. package/dist/nav-height-budget.js.map +1 -0
  77. package/dist/style.css +1 -1
  78. package/dist/theme.css +190 -0
  79. package/package.json +12 -4
@@ -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,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,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":";;;;;"}
@@ -1,3 +1,4 @@
1
+ import { WalletChipLabels } from './labels';
1
2
  export interface WalletChipProps {
2
3
  /** Remaining granted tokens. Granted burns first and expires. */
3
4
  granted: number;
@@ -23,5 +24,37 @@ export interface WalletChipProps {
23
24
  collapsed?: boolean;
24
25
  /** Top up. Wiring for the one button the sidebar is allowed to have. */
25
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;
26
59
  }
27
- export declare function WalletChip({ granted, purchased, grantCycle, grantExpires, burnNote, unknown, collapsed, onTopUp }: WalletChipProps): import("react").JSX.Element;
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":";;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -1,7 +1,47 @@
1
- /** The full balance, thousands grouped: 4600 -> "4,600". */
2
- export declare function formatTokens(value: number): string;
3
1
  /**
4
- * The rail's short form: one decimal thousand, always. At 56px there is no
5
- * room for four digits and a separator, and "4.6k" still answers "am I fine?".
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.
6
5
  */
7
- export declare function abbreviateThousands(value: number): string;
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;
@@ -0,0 +1,29 @@
1
+ const e = "en-US", i = {
2
+ grouped: {},
3
+ oneDecimal: { minimumFractionDigits: 1, maximumFractionDigits: 1 }
4
+ }, m = /* @__PURE__ */ new Map(), s = /* @__PURE__ */ new Set();
5
+ function c(t, n) {
6
+ const o = `${n}|${t}`, a = m.get(o);
7
+ if (a) return a;
8
+ let r;
9
+ try {
10
+ r = new Intl.NumberFormat(t, i[n]);
11
+ } catch {
12
+ s.has(t) || (s.add(t), console.warn(
13
+ `@boost33/ui: WalletChip cannot format numbers in locale ${JSON.stringify(t)}; falling back to ${e}.`
14
+ )), r = new Intl.NumberFormat(e, i[n]);
15
+ }
16
+ return m.set(o, r), r;
17
+ }
18
+ function u(t, n = e) {
19
+ return c(n, "grouped").format(t);
20
+ }
21
+ function f(t, n = e) {
22
+ return c(n, "oneDecimal").format(t / 1e3);
23
+ }
24
+ export {
25
+ e as DEFAULT_WALLET_LOCALE,
26
+ u as formatTokens,
27
+ f as thousandsFigure
28
+ };
29
+ //# sourceMappingURL=format.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.js","sources":["../../../src/components/WalletChip/format.ts"],"sourcesContent":["// The two numbers the chip says out loud. Kept separate from the component so\n// the rounding and the separator -- the parts that carry meaning rather than\n// look -- are unit-tested without a render.\n//\n// ---------------------------------------------------------------------------\n// B33-876: why a `locale` string, and not consumer-supplied formatters\n// ---------------------------------------------------------------------------\n//\n// This module used to hardcode `toLocaleString('en-US')` and a literal `k`. The\n// first two consumers are French-default, and French writes `4 600` where this\n// wrote `4,600`, and `4,6 k` where this wrote `4.6k` -- in the largest glyphs on\n// the chip, and inside the `bar` and `railBalance` accessible names. `labels`\n// could not reach any of it: the label functions receive the figures *already\n// formatted*, by documented intent, so a consumer had `\"4,600 jetons\"` and no\n// hook.\n//\n// The ticket named two candidate shapes and picked neither: a `locale` prop, or\n// `formatTokens` / `abbreviateThousands` supplied by the consumer. The shape\n// chosen is **a `locale` prop** -- one BCP-47 language tag, defaulting to\n// `en-US`, driving `Intl.NumberFormat`. Written down here, prominently, because\n// it is a published API decision reaching four products on a version bump and\n// reversing it later should be cheap.\n//\n// 1. **A formatter prop is a function, and a function cannot cross the React\n// Server Component boundary.** B33-880 had just finished taking the functions\n// *out* of `labels` for exactly this reason: `AppShell.wallet` is a\n// `ReactNode` slot, so it is the consumer's own page -- under the App Router,\n// a Server Component by default -- that constructs `<WalletChip …/>`, and\n// React's Flight serializer throws on any function anywhere in the prop\n// graph, in production as well as in development. An API of injected\n// formatters would reintroduce that bug the same week it was fixed, and\n// reintroduce it as the *recommended* path. A language tag is a string: data\n// all the way down, and it survives the boundary intact.\n//\n// 2. **Numbers are not words.** `labels` takes whole sentences because word\n// order is not a property of English and cannot be reassembled from\n// fragments -- that is a fact about a consumer's sentence, which no library\n// can know. Grouping and the decimal separator are the opposite: they are\n// CLDR facts about a locale, the platform ships them for every locale, and\n// asking four products to each re-implement French grouping is asking four\n// products to each get it slightly wrong. So \"the `labels` prop is already\n// the injected shape, be consistent\" reverses once you read *why* `labels`\n// took that shape.\n//\n// 3. **It is one string with a default**, and a consumer that passes nothing\n// renders character-for-character what the chip rendered before the prop\n// existed. The five `walletchip-*` baselines cannot move.\n//\n// What `locale` deliberately does not own is the `k`. See `thousandsFigure`.\n\n/**\n * The language tag the chip formats in when the consumer names none. Today's\n * rendering, so the default path is byte-identical to the one the fidelity\n * baselines were captured from.\n */\nexport const DEFAULT_WALLET_LOCALE = 'en-US'\n\n/**\n * The two shapes a figure is asked for: the full balance, grouped; and one\n * decimal of a thousand for the rail.\n */\ntype FormatterKind = 'grouped' | 'oneDecimal'\n\nconst FORMATTER_OPTIONS: Record<FormatterKind, Intl.NumberFormatOptions> = {\n grouped: {},\n oneDecimal: { minimumFractionDigits: 1, maximumFractionDigits: 1 },\n}\n\n/**\n * `Intl.NumberFormat` instances, keyed by kind and tag.\n *\n * Constructing one is not free and the chip formats up to four figures per\n * render, on a component that sits in the sidebar of every screen. The set of\n * locales an app uses is small and long-lived, so a plain module-level Map is\n * the whole of the caching this needs.\n */\nconst formatters = new Map<string, Intl.NumberFormat>()\n\n/**\n * The tags already warned about, so a bad one warns once rather than on every\n * render of every chip.\n */\nconst warnedLocales = new Set<string>()\n\n/**\n * An `Intl.NumberFormat` for this tag, falling back to English if the tag is\n * malformed.\n *\n * `Intl.NumberFormat` throws a `RangeError` on a structurally invalid language\n * tag (`'en_US'`, `'français'`), and this chip sits in the sidebar of four\n * products: one consumer's typo in a config file would take the whole shell down\n * rather than mis-space a number. A tag the platform refuses degrades to the\n * English default and warns the developer -- the same instinct as B33-873, where\n * a missing translation key falls back to English instead of blanking the\n * header. The warning is `console`-only: developer-facing, never rendered, never\n * translated.\n *\n * An unknown-but-well-formed tag (`'zz-ZZ'`) does not throw; the platform\n * resolves it to its own best match, which is its business and not this\n * package's.\n */\nfunction formatterFor(locale: string, kind: FormatterKind): Intl.NumberFormat {\n const key = `${kind}|${locale}`\n const cached = formatters.get(key)\n if (cached) return cached\n\n let formatter: Intl.NumberFormat\n try {\n formatter = new Intl.NumberFormat(locale, FORMATTER_OPTIONS[kind])\n } catch {\n if (!warnedLocales.has(locale)) {\n warnedLocales.add(locale)\n console.warn(\n `@boost33/ui: WalletChip cannot format numbers in locale ${JSON.stringify(locale)}; falling back to ${DEFAULT_WALLET_LOCALE}.`,\n )\n }\n formatter = new Intl.NumberFormat(DEFAULT_WALLET_LOCALE, FORMATTER_OPTIONS[kind])\n }\n formatters.set(key, formatter)\n return formatter\n}\n\n/**\n * The full balance, thousands grouped the way the locale groups them:\n * 4600 -> `\"4,600\"` in `en-US`, `\"4 600\"` in `fr-FR`, `\"4.600\"` in `de-DE`.\n *\n * The `en-US` default is what `toLocaleString('en-US')` produced, character for\n * character -- it is the same CLDR data reached through the constructor that can\n * be cached.\n */\nexport function formatTokens(value: number, locale: string = DEFAULT_WALLET_LOCALE): string {\n return formatterFor(locale, 'grouped').format(value)\n}\n\n/**\n * The rail's short form: one decimal thousand, always. At 56px there is no room\n * for four digits and a separator, and \"4.6k\" still answers \"am I fine?\".\n *\n * Returns **the figure alone** -- `\"4.6\"` in `en-US`, `\"4,6\"` in `fr-FR`. The\n * mark that follows it is a word rather than a number (\"k\" in English, \"Tsd.\" in\n * German, \"ألف\" in Arabic), and French puts a space in front of it where English\n * does not, so it lives with the chip's other words, as `labels.railFigure`\n * (B33-876).\n *\n * Not `Intl.NumberFormat`'s own `notation: 'compact'`, for two concrete reasons.\n * It renders `en-US` as `\"4.6K\"`, a capital the baselines do not have. And\n * German does not abbreviate four digits at all, so it renders `\"4600,0\"` -- six\n * characters and a decimal, in a 56px rail, longer than the number it was\n * supposed to shorten. One decimal thousand is this package's decision about a\n * fixed-width rail, not CLDR's about a locale; the separator inside it is CLDR's,\n * and that is the part `locale` fixes.\n *\n * Two default-path differences from the `(value / 1000).toFixed(1)` this\n * replaced, both narrower than any state the baselines hold and both left\n * deliberately, but written down because they reach four products (B33-876):\n *\n * - **A balance landing on an exact half rounds the other way.** `toFixed`\n * rounds the double's exact binary expansion, so 150 tokens printed `0.1`;\n * `Intl` rounds the shortest decimal the double stands for, so it prints\n * `0.2`. `Intl` agrees with the reader here, and the old answer was a float\n * artefact rather than a decision.\n * - **A balance over a million is grouped.** `toFixed` gave `1234.6`, this\n * gives `1,234.6`. Neither fits a 56px rail; that is a rail question, not a\n * formatting one.\n */\nexport function thousandsFigure(value: number, locale: string = DEFAULT_WALLET_LOCALE): string {\n return formatterFor(locale, 'oneDecimal').format(value / 1000)\n}\n"],"names":["DEFAULT_WALLET_LOCALE","FORMATTER_OPTIONS","formatters","warnedLocales","formatterFor","locale","kind","key","cached","formatter","formatTokens","value","thousandsFigure"],"mappings":"AAuDO,MAAMA,IAAwB,SAQ/BC,IAAqE;AAAA,EACzE,SAAS,CAAA;AAAA,EACT,YAAY,EAAE,uBAAuB,GAAG,uBAAuB,EAAA;AACjE,GAUMC,wBAAiB,IAAA,GAMjBC,wBAAoB,IAAA;AAmB1B,SAASC,EAAaC,GAAgBC,GAAwC;AAC5E,QAAMC,IAAM,GAAGD,CAAI,IAAID,CAAM,IACvBG,IAASN,EAAW,IAAIK,CAAG;AACjC,MAAIC,EAAQ,QAAOA;AAEnB,MAAIC;AACJ,MAAI;AACF,IAAAA,IAAY,IAAI,KAAK,aAAaJ,GAAQJ,EAAkBK,CAAI,CAAC;AAAA,EACnE,QAAQ;AACN,IAAKH,EAAc,IAAIE,CAAM,MAC3BF,EAAc,IAAIE,CAAM,GACxB,QAAQ;AAAA,MACN,2DAA2D,KAAK,UAAUA,CAAM,CAAC,qBAAqBL,CAAqB;AAAA,IAAA,IAG/HS,IAAY,IAAI,KAAK,aAAaT,GAAuBC,EAAkBK,CAAI,CAAC;AAAA,EAClF;AACA,SAAAJ,EAAW,IAAIK,GAAKE,CAAS,GACtBA;AACT;AAUO,SAASC,EAAaC,GAAeN,IAAiBL,GAA+B;AAC1F,SAAOI,EAAaC,GAAQ,SAAS,EAAE,OAAOM,CAAK;AACrD;AAiCO,SAASC,EAAgBD,GAAeN,IAAiBL,GAA+B;AAC7F,SAAOI,EAAaC,GAAQ,YAAY,EAAE,OAAOM,IAAQ,GAAI;AAC/D;"}
@@ -0,0 +1,18 @@
1
+ import { jsxs as t, jsx as r } from "react/jsx-runtime";
2
+ function n({ className: e }) {
3
+ return /* @__PURE__ */ t("svg", { className: e, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
4
+ /* @__PURE__ */ r("rect", { x: "3", y: "6", width: "18", height: "12", rx: "2" }),
5
+ /* @__PURE__ */ r("path", { d: "M3 10h18" })
6
+ ] });
7
+ }
8
+ function i({ className: e }) {
9
+ return /* @__PURE__ */ t("svg", { className: e, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
10
+ /* @__PURE__ */ r("path", { d: "M21 12a9 9 0 1 1-2.64-6.36" }),
11
+ /* @__PURE__ */ r("path", { d: "M21 3v4h-4" })
12
+ ] });
13
+ }
14
+ export {
15
+ i as RetryMark,
16
+ n as WalletMark
17
+ };
18
+ //# sourceMappingURL=icons.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"icons.js","sources":["../../../src/components/WalletChip/icons.tsx"],"sourcesContent":["// Small monochrome glyphs the chip draws in `currentColor` -- no fill token, so\n// the surrounding text colour carries them and the no-hardcoded-hex guard stays\n// satisfied. Decorative: the balance text is what carries meaning, so both are\n// aria-hidden.\n\n/** The card mark shown on the rail, where there is no room for the word \"Wallet\". */\nexport function WalletMark({ className }: { className?: string }) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.75\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n <rect x=\"3\" y=\"6\" width=\"18\" height=\"12\" rx=\"2\" />\n <path d=\"M3 10h18\" />\n </svg>\n )\n}\n\n/** The retry affordance shown when the balance could not be confirmed. */\nexport function RetryMark({ className }: { className?: string }) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.75\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M21 12a9 9 0 1 1-2.64-6.36\" />\n <path d=\"M21 3v4h-4\" />\n </svg>\n )\n}\n"],"names":["WalletMark","className","jsx","RetryMark"],"mappings":";AAMO,SAASA,EAAW,EAAE,WAAAC,KAAqC;AAChE,2BACG,OAAA,EAAI,WAAAA,GAAsB,SAAQ,aAAY,MAAK,QAAO,QAAO,gBAAe,aAAY,QAAO,eAAc,SAAQ,gBAAe,SAAQ,eAAY,QAC3J,UAAA;AAAA,IAAA,gBAAAC,EAAC,QAAA,EAAK,GAAE,KAAI,GAAE,KAAI,OAAM,MAAK,QAAO,MAAK,IAAG,IAAA,CAAI;AAAA,IAChD,gBAAAA,EAAC,QAAA,EAAK,GAAE,WAAA,CAAW;AAAA,EAAA,GACrB;AAEJ;AAGO,SAASC,EAAU,EAAE,WAAAF,KAAqC;AAC/D,2BACG,OAAA,EAAI,WAAAA,GAAsB,SAAQ,aAAY,MAAK,QAAO,QAAO,gBAAe,aAAY,QAAO,eAAc,SAAQ,gBAAe,SAAQ,eAAY,QAC3J,UAAA;AAAA,IAAA,gBAAAC,EAAC,QAAA,EAAK,GAAE,6BAAA,CAA6B;AAAA,IACrC,gBAAAA,EAAC,QAAA,EAAK,GAAE,aAAA,CAAa;AAAA,EAAA,GACvB;AAEJ;"}
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Every user-facing string the chip renders. Supplied whole -- the strings that
3
+ * carry a value take a `{placeholder}` template (or a function) rather than
4
+ * fragments, because word order is not a property of English and cannot be
5
+ * reassembled on the consumer's side.
6
+ *
7
+ * `grantExpiry`, `railFigure`, `railBalance` and `bar` each accept two shapes.
8
+ *
9
+ * A **template** is a plain string marking the value's position with a
10
+ * `{placeholder}` -- `'Granted expires {date} · burns first'`. It is data all
11
+ * the way down, so the whole `labels` object survives the React Server Component
12
+ * boundary and a Next App Router page can pass it. **This is the recommended
13
+ * form**, and the one the defaults use.
14
+ *
15
+ * A **function** of the same values still works unchanged. It is never
16
+ * serializable, so it only reaches the chip from a Client Component, but it is
17
+ * the escape hatch when the sentence needs logic a template cannot express --
18
+ * ICU plurals, an `Intl.ListFormat` call.
19
+ *
20
+ * In a template, a `{name}` the label does not define is left exactly as written
21
+ * -- a locale whose copy genuinely contains braces is not mangled, and a
22
+ * misspelled placeholder shows up in the rendering rather than silently
23
+ * blanking. A placeholder a template omits is simply not rendered: a locale that
24
+ * does not need a figure is allowed not to name it.
25
+ */
26
+ export interface WalletChipLabels {
27
+ /** Accessible name for the expanded chip's status region. Default "Wallet balance". */
28
+ region: string;
29
+ /** The chip header. Default "Wallet". */
30
+ name: string;
31
+ /** The unit rendered beside the figure. Default "tokens". */
32
+ unit: string;
33
+ /** Screen-reader-only text in the figure when the balance is unknown. Default "Balance unknown". */
34
+ balanceUnknown: string;
35
+ /** The granted side of the split -- granted burns first. Default "Granted". */
36
+ granted: string;
37
+ /** The purchased side of the split -- purchased never expires. Default "Purchased". */
38
+ purchased: string;
39
+ /**
40
+ * The footer, around the preformatted expiry date. Supplied whole so the
41
+ * consumer owns word order.
42
+ *
43
+ * Placeholder: `{date}`. Default (en) `Granted expires {date} · burns first`.
44
+ * A function form `(date) => string` is also accepted.
45
+ */
46
+ grantExpiry: string | ((date: string) => string);
47
+ /** The top-up button. Default "Top up". */
48
+ topUp: string;
49
+ /**
50
+ * The rail's abbreviated balance, around the one-decimal-thousand figure
51
+ * (e.g. "4.6"). Both the visible text in the 56px rail and the figure that
52
+ * `railBalance` is given.
53
+ *
54
+ * Placeholder: `{figure}`. Default (en) `{figure}k`. A function form
55
+ * `(figure) => string` is also accepted.
56
+ *
57
+ * The figure itself is formatted for `locale` -- "4.6" in `en-US`, "4,6" in
58
+ * `fr-FR` -- but the mark after it is a word, and locales disagree about it:
59
+ * French writes "4,6 k" with a space, German abbreviates a thousand "Tsd.".
60
+ * So the mark is a label like every other word the chip renders, and a
61
+ * template rather than a bare suffix, because a locale is entitled to put the
62
+ * mark somewhere other than the end (B33-876).
63
+ */
64
+ railFigure: string | ((figure: string) => string);
65
+ /**
66
+ * The rail group's accessible name, around the abbreviated balance (e.g. "4.6k").
67
+ *
68
+ * Placeholder: `{balance}`. Default (en) `Wallet balance {balance}`. A function
69
+ * form `(balance) => string` is also accepted.
70
+ */
71
+ railBalance: string | ((balance: string) => string);
72
+ /** The rail group's accessible name when the balance is unknown. Default "Wallet balance unknown". */
73
+ railBalanceUnknown: string;
74
+ /** Screen-reader-only marker on the rail when the balance is unknown. Default "unknown". */
75
+ railUnknown: string;
76
+ /**
77
+ * The bar's accessible label, around the three preformatted figures. Assembled
78
+ * whole rather than concatenated from fragments -- the shape that cannot be
79
+ * translated at all otherwise.
80
+ *
81
+ * Placeholders: `{granted}`, `{purchased}`, `{remaining}`. Default (en)
82
+ * `{granted} granted and {purchased} purchased of {remaining} remaining`. A
83
+ * function form `(granted, purchased, remaining) => string` is also accepted.
84
+ */
85
+ bar: string | ((granted: string, purchased: string, remaining: string) => string);
86
+ }
87
+ /**
88
+ * The English defaults. A consumer that passes nothing renders exactly what the
89
+ * chip rendered before this prop existed, so the wallet baselines do not move.
90
+ *
91
+ * **Every field is a string.** That is what makes `labels={defaultWalletChipLabels}`
92
+ * and `labels={{ ...defaultWalletChipLabels, name: 'Portefeuille' }}` -- the
93
+ * pattern the specimen shows and this package's own advice recommends -- legal
94
+ * from a Server Component. The four value-carrying defaults are templates, and
95
+ * the rendered output is character-for-character what the function defaults
96
+ * produced (B33-880).
97
+ *
98
+ * Frozen because it is a public export reaching four products on a version bump.
99
+ * `const` protects the binding, not the contents: without the freeze, a consumer
100
+ * aliasing it by reference -- `const mine = defaultWalletChipLabels; mine.name =
101
+ * 'Portefeuille'` -- would silently rename every chip in the process (B33-877).
102
+ */
103
+ export declare const defaultWalletChipLabels: Readonly<{
104
+ region: string;
105
+ name: string;
106
+ unit: string;
107
+ balanceUnknown: string;
108
+ granted: string;
109
+ purchased: string;
110
+ grantExpiry: string;
111
+ topUp: string;
112
+ railFigure: string;
113
+ railBalance: string;
114
+ railBalanceUnknown: string;
115
+ railUnknown: string;
116
+ bar: string;
117
+ }>;
118
+ /**
119
+ * The labels as the render consumes them: the four value-carrying fields
120
+ * normalised to functions, whichever shape the consumer supplied. The component
121
+ * calls `t.bar(…)` either way and never learns which form it was given, so the
122
+ * union costs the render nothing -- and, in particular, the lazy call that keeps
123
+ * a consumer's `bar` off the degraded unknown path (B33-875) still works.
124
+ */
125
+ export interface ResolvedWalletChipLabels extends Omit<WalletChipLabels, 'grantExpiry' | 'railFigure' | 'railBalance' | 'bar'> {
126
+ grantExpiry: (date: string) => string;
127
+ railFigure: (figure: string) => string;
128
+ railBalance: (balance: string) => string;
129
+ bar: (granted: string, purchased: string, remaining: string) => string;
130
+ }
131
+ /**
132
+ * Merge a consumer's partial `labels` onto the English defaults, treating an
133
+ * explicitly-`undefined` field as unsupplied, and normalise the four
134
+ * value-carrying fields to functions.
135
+ *
136
+ * A next-intl `t('...')` returns `undefined` on a missed key, so
137
+ * `{ name: undefined }` is what one missing translation out of thirteen looks like
138
+ * at the call site -- it must fall back to English, not overwrite the default
139
+ * with `undefined` and blank the header, unname the button, or crash on
140
+ * `t.bar(...)` (B33-873). A plain object spread copies the `undefined` across; a
141
+ * per-field skip does not.
142
+ */
143
+ export declare function resolveWalletChipLabels(labels?: Partial<WalletChipLabels>): ResolvedWalletChipLabels;