@boost33/ui 0.8.0-next.4 → 0.8.0-next.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,19 +1,23 @@
1
- import { jsx as e, jsxs as d, Fragment as m } from "react/jsx-runtime";
1
+ import { jsx as s, jsxs as n, Fragment as r } from "react/jsx-runtime";
2
2
  import a from "./NavItem.module.css.js";
3
- function b({ children: u, icon: n, active: s = !1, collapsed: p = !1, count: l, countUrgent: h = !1, dot: r, href: t, linkComponent: N, onClick: c }) {
4
- const i = {
5
- "data-active": s ? "true" : void 0,
6
- "data-collapsed": p ? "true" : void 0,
7
- "aria-current": s ? "page" : void 0
8
- }, o = /* @__PURE__ */ d(m, { children: [
9
- n != null && /* @__PURE__ */ e("span", { className: a.icon, "aria-hidden": "true", children: n }),
10
- /* @__PURE__ */ e("span", { className: a.label, children: u }),
11
- /* @__PURE__ */ e("span", { className: a.trailingSlot, children: l != null ? /* @__PURE__ */ e("span", { className: h ? a.countUrgent : a.count, children: l }) : r != null && /* @__PURE__ */ d(m, { children: [
12
- /* @__PURE__ */ e("span", { className: a.dot, "aria-hidden": "true" }),
13
- /* @__PURE__ */ e("span", { className: a.srOnly, children: r })
3
+ function b({ children: p, icon: t, active: l = !1, collapsed: N = !1, count: i, countUrgent: f = !1, countLabel: e, dot: c, attrs: g, href: d, linkComponent: v, onClick: o }) {
4
+ const h = {
5
+ ...Object.fromEntries(Object.entries(g ?? {}).filter(([m]) => m.startsWith("data-"))),
6
+ "data-active": l ? "true" : void 0,
7
+ "data-collapsed": N ? "true" : void 0,
8
+ "aria-current": l ? "page" : void 0
9
+ }, u = /* @__PURE__ */ n(r, { children: [
10
+ t != null && /* @__PURE__ */ s("span", { className: a.icon, "aria-hidden": "true", children: t }),
11
+ /* @__PURE__ */ s("span", { className: a.label, children: p }),
12
+ /* @__PURE__ */ s("span", { className: a.trailingSlot, children: i != null ? /* @__PURE__ */ n(r, { children: [
13
+ /* @__PURE__ */ s("span", { className: f ? a.countUrgent : a.count, "aria-hidden": e != null ? "true" : void 0, children: i }),
14
+ e != null && /* @__PURE__ */ s("span", { className: a.srOnly, children: e })
15
+ ] }) : c != null && /* @__PURE__ */ n(r, { children: [
16
+ /* @__PURE__ */ s("span", { className: a.dot, "aria-hidden": "true" }),
17
+ /* @__PURE__ */ s("span", { className: a.srOnly, children: c })
14
18
  ] }) })
15
19
  ] });
16
- return t ? /* @__PURE__ */ e(N ?? "a", { className: a.row, href: t, onClick: c, ...i, children: o }) : /* @__PURE__ */ e("button", { type: "button", className: a.row, onClick: c, ...i, children: o });
20
+ return d ? /* @__PURE__ */ s(v ?? "a", { className: a.row, href: d, onClick: o, ...h, children: u }) : /* @__PURE__ */ s("button", { type: "button", className: a.row, onClick: o, ...h, children: u });
17
21
  }
18
22
  export {
19
23
  b as NavItem
@@ -1 +1 @@
1
- {"version":3,"file":"NavItem.js","sources":["../../../src/components/NavItem/NavItem.tsx"],"sourcesContent":["import type { ElementType, ReactNode } from 'react'\nimport styles from './NavItem.module.css'\n\nexport interface NavItemProps {\n /** The nav label. Truncates with an ellipsis rather than wrapping. */\n children: ReactNode\n /**\n * The leading icon. Decorative -- the label carries the name, so pass an\n * `aria-hidden` glyph. On the rail it is the only thing that shows, the\n * label having collapsed to the accessibility tree.\n */\n icon?: ReactNode\n /** Marks this row as the current page. Sets `aria-current=\"page\"`. */\n active?: boolean\n /** Sidebar-on-the-rail state. Hides the label and trailing slot visually; the label stays in the accessibility tree. */\n collapsed?: boolean\n /** Trailing slot content. Omit to leave the slot empty -- it still reserves its width so counts line up across rows. */\n count?: number\n /**\n * Draws `count` as a filled pill rather than muted text -- the \"needs you\"\n * count (board 20 of the 3 Nov 2026 redesign, B33-1711). Until 0.8 it drew the\n * count in --color-primary text, which was 3.45:1 on the grey sidebar and\n * failed AA at the row's size; the pill is white on --color-orange-600, 5.18:1.\n */\n countUrgent?: boolean\n /**\n * A 7px dot in the trailing slot, saying the row needs attention -- an\n * unfinished profile, say (board 20, B33-1711). The dot is colour alone, so\n * the string is what a screen reader hears in its place, in the consumer's\n * language (\"Incomplete\"). Ignored when `count` is set: the slot holds one\n * thing.\n */\n dot?: string\n href?: string\n /**\n * The element the linked form renders through. Defaults to a plain `a`; a\n * consumer whose router owns navigation (Next.js, next-intl's localised\n * `Link`) passes its own so the row routes client-side with the locale prefix\n * intact instead of a bare anchor that full-page-reloads (B33-847). Receives\n * `href`, `className`, `onClick` and the row's data/aria attributes unchanged,\n * so the markup is identical to the anchor's. Only used when `href` is set;\n * the action form stays a `button`. See docs/adr/0011.\n */\n linkComponent?: ElementType\n onClick?: () => void\n}\n\nexport function NavItem({ children, icon, active = false, collapsed = false, count, countUrgent = false, dot, href, linkComponent, onClick }: NavItemProps) {\n const dataProps = {\n 'data-active': active ? ('true' as const) : undefined,\n 'data-collapsed': collapsed ? ('true' as const) : undefined,\n 'aria-current': active ? ('page' as const) : undefined,\n }\n\n const content = (\n <>\n {icon != null && (\n <span className={styles.icon} aria-hidden=\"true\">\n {icon}\n </span>\n )}\n <span className={styles.label}>{children}</span>\n <span className={styles.trailingSlot}>\n {count != null ? (\n <span className={countUrgent ? styles.countUrgent : styles.count}>{count}</span>\n ) : (\n dot != null && (\n <>\n <span className={styles.dot} aria-hidden=\"true\" />\n <span className={styles.srOnly}>{dot}</span>\n </>\n )\n )}\n </span>\n </>\n )\n\n if (href) {\n const Link = linkComponent ?? 'a'\n return (\n <Link className={styles.row} href={href} onClick={onClick} {...dataProps}>\n {content}\n </Link>\n )\n }\n\n return (\n <button type=\"button\" className={styles.row} onClick={onClick} {...dataProps}>\n {content}\n </button>\n )\n}\n"],"names":["NavItem","children","icon","active","collapsed","count","countUrgent","dot","href","linkComponent","onClick","dataProps","content","jsxs","Fragment","styles","jsx"],"mappings":";;AA+CO,SAASA,EAAQ,EAAE,UAAAC,GAAU,MAAAC,GAAM,QAAAC,IAAS,IAAO,WAAAC,IAAY,IAAO,OAAAC,GAAO,aAAAC,IAAc,IAAO,KAAAC,GAAK,MAAAC,GAAM,eAAAC,GAAe,SAAAC,KAAyB;AAC1J,QAAMC,IAAY;AAAA,IAChB,eAAeR,IAAU,SAAmB;AAAA,IAC5C,kBAAkBC,IAAa,SAAmB;AAAA,IAClD,gBAAgBD,IAAU,SAAmB;AAAA,EAAA,GAGzCS,IACJ,gBAAAC,EAAAC,GAAA,EACG,UAAA;AAAA,IAAAZ,KAAQ,0BACN,QAAA,EAAK,WAAWa,EAAO,MAAM,eAAY,QACvC,UAAAb,EAAA,CACH;AAAA,IAEF,gBAAAc,EAAC,QAAA,EAAK,WAAWD,EAAO,OAAQ,UAAAd,GAAS;AAAA,sBACxC,QAAA,EAAK,WAAWc,EAAO,cACrB,UAAAV,KAAS,OACR,gBAAAW,EAAC,QAAA,EAAK,WAAWV,IAAcS,EAAO,cAAcA,EAAO,OAAQ,aAAM,IAEzER,KAAO,QACL,gBAAAM,EAAAC,GAAA,EACE,UAAA;AAAA,MAAA,gBAAAE,EAAC,QAAA,EAAK,WAAWD,EAAO,KAAK,eAAY,QAAO;AAAA,MAChD,gBAAAC,EAAC,QAAA,EAAK,WAAWD,EAAO,QAAS,UAAAR,EAAA,CAAI;AAAA,IAAA,EAAA,CACvC,EAAA,CAGN;AAAA,EAAA,GACF;AAGF,SAAIC,IAGA,gBAAAQ,EAFWP,KAAiB,OAEtB,WAAWM,EAAO,KAAK,MAAAP,GAAY,SAAAE,GAAmB,GAAGC,GAC5D,UAAAC,EAAA,CACH,IAKF,gBAAAI,EAAC,UAAA,EAAO,MAAK,UAAS,WAAWD,EAAO,KAAK,SAAAL,GAAmB,GAAGC,GAChE,UAAAC,EAAA,CACH;AAEJ;"}
1
+ {"version":3,"file":"NavItem.js","sources":["../../../src/components/NavItem/NavItem.tsx"],"sourcesContent":["import type { ElementType, ReactNode } from 'react'\nimport styles from './NavItem.module.css'\n\n/** Pass-through attributes for a nav row: `data-*` only. */\nexport type NavItemAttrs = Record<`data-${string}`, string>\n\nexport interface NavItemProps {\n /** The nav label. Truncates with an ellipsis rather than wrapping. */\n children: ReactNode\n /**\n * The leading icon. Decorative -- the label carries the name, so pass an\n * `aria-hidden` glyph. On the rail it is the only thing that shows, the\n * label having collapsed to the accessibility tree.\n */\n icon?: ReactNode\n /** Marks this row as the current page. Sets `aria-current=\"page\"`. */\n active?: boolean\n /** Sidebar-on-the-rail state. Hides the label and trailing slot visually; the label stays in the accessibility tree. */\n collapsed?: boolean\n /** Trailing slot content. Omit to leave the slot empty -- it still reserves its width so counts line up across rows. */\n count?: number\n /**\n * Draws `count` as a filled pill rather than muted text -- the \"needs you\"\n * count (board 20 of the 3 Nov 2026 redesign, B33-1711). Until 0.8 it drew the\n * count in --color-primary text, which was 3.45:1 on the grey sidebar and\n * failed AA at the row's size; the pill is white on --color-orange-600, 5.18:1.\n */\n countUrgent?: boolean\n /**\n * A 7px dot in the trailing slot, saying the row needs attention -- an\n * unfinished profile, say (board 20, B33-1711). The dot is colour alone, so\n * the string is what a screen reader hears in its place, in the consumer's\n * language (\"Incomplete\"). Ignored when `count` is set: the slot holds one\n * thing.\n */\n dot?: string\n /**\n * What a screen reader hears for `count`, in the consumer's words: \"3 to\n * open\", \"3 à ouvrir\" (0.8.0-next.5, Off Market). The bare number on its own\n * says nothing about what is counted. Given, the visible figure is hidden from\n * assistive tech and this is read in its place; omitted, the number is read as\n * before. Ignored without `count`.\n */\n countLabel?: string\n /**\n * `data-*` attributes for the row's element, such as a product tour's hook\n * (`{ 'data-tour': 'nav-accounts' }`, Outbound's driver.js). Only `data-*`, so\n * nothing here can change the row's role, name or behaviour, and the row's\n * own `data-active` and `data-collapsed` always win (0.8.0-next.5).\n */\n attrs?: NavItemAttrs\n href?: string\n /**\n * The element the linked form renders through. Defaults to a plain `a`; a\n * consumer whose router owns navigation (Next.js, next-intl's localised\n * `Link`) passes its own so the row routes client-side with the locale prefix\n * intact instead of a bare anchor that full-page-reloads (B33-847). Receives\n * `href`, `className`, `onClick` and the row's data/aria attributes unchanged,\n * so the markup is identical to the anchor's. Only used when `href` is set;\n * the action form stays a `button`. See docs/adr/0011.\n */\n linkComponent?: ElementType\n onClick?: () => void\n}\n\nexport function NavItem({ children, icon, active = false, collapsed = false, count, countUrgent = false, countLabel, dot, attrs, href, linkComponent, onClick }: NavItemProps) {\n // The consumer's `data-*` first, so the row's own attributes below override\n // any it repeats. Filtered again at runtime for a caller that is not\n // type-checked.\n const passThrough = Object.fromEntries(Object.entries(attrs ?? {}).filter(([name]) => name.startsWith('data-')))\n const dataProps = {\n ...passThrough,\n 'data-active': active ? ('true' as const) : undefined,\n 'data-collapsed': collapsed ? ('true' as const) : undefined,\n 'aria-current': active ? ('page' as const) : undefined,\n }\n\n const content = (\n <>\n {icon != null && (\n <span className={styles.icon} aria-hidden=\"true\">\n {icon}\n </span>\n )}\n <span className={styles.label}>{children}</span>\n <span className={styles.trailingSlot}>\n {count != null ? (\n <>\n <span className={countUrgent ? styles.countUrgent : styles.count} aria-hidden={countLabel != null ? 'true' : undefined}>\n {count}\n </span>\n {countLabel != null && <span className={styles.srOnly}>{countLabel}</span>}\n </>\n ) : (\n dot != null && (\n <>\n <span className={styles.dot} aria-hidden=\"true\" />\n <span className={styles.srOnly}>{dot}</span>\n </>\n )\n )}\n </span>\n </>\n )\n\n if (href) {\n const Link = linkComponent ?? 'a'\n return (\n <Link className={styles.row} href={href} onClick={onClick} {...dataProps}>\n {content}\n </Link>\n )\n }\n\n return (\n <button type=\"button\" className={styles.row} onClick={onClick} {...dataProps}>\n {content}\n </button>\n )\n}\n"],"names":["NavItem","children","icon","active","collapsed","count","countUrgent","countLabel","dot","attrs","href","linkComponent","onClick","dataProps","name","content","jsxs","Fragment","styles","jsx"],"mappings":";;AAiEO,SAASA,EAAQ,EAAE,UAAAC,GAAU,MAAAC,GAAM,QAAAC,IAAS,IAAO,WAAAC,IAAY,IAAO,OAAAC,GAAO,aAAAC,IAAc,IAAO,YAAAC,GAAY,KAAAC,GAAK,OAAAC,GAAO,MAAAC,GAAM,eAAAC,GAAe,SAAAC,KAAyB;AAK7K,QAAMC,IAAY;AAAA,IAChB,GAFkB,OAAO,YAAY,OAAO,QAAQJ,KAAS,CAAA,CAAE,EAAE,OAAO,CAAC,CAACK,CAAI,MAAMA,EAAK,WAAW,OAAO,CAAC,CAAC;AAAA,IAG7G,eAAeX,IAAU,SAAmB;AAAA,IAC5C,kBAAkBC,IAAa,SAAmB;AAAA,IAClD,gBAAgBD,IAAU,SAAmB;AAAA,EAAA,GAGzCY,IACJ,gBAAAC,EAAAC,GAAA,EACG,UAAA;AAAA,IAAAf,KAAQ,0BACN,QAAA,EAAK,WAAWgB,EAAO,MAAM,eAAY,QACvC,UAAAhB,EAAA,CACH;AAAA,IAEF,gBAAAiB,EAAC,QAAA,EAAK,WAAWD,EAAO,OAAQ,UAAAjB,GAAS;AAAA,sBACxC,QAAA,EAAK,WAAWiB,EAAO,cACrB,UAAAb,KAAS,OACR,gBAAAW,EAAAC,GAAA,EACE,UAAA;AAAA,MAAA,gBAAAE,EAAC,QAAA,EAAK,WAAWb,IAAcY,EAAO,cAAcA,EAAO,OAAO,eAAaX,KAAc,OAAO,SAAS,QAC1G,UAAAF,GACH;AAAA,MACCE,KAAc,QAAQ,gBAAAY,EAAC,UAAK,WAAWD,EAAO,QAAS,UAAAX,EAAA,CAAW;AAAA,IAAA,EAAA,CACrE,IAEAC,KAAO,QACL,gBAAAQ,EAAAC,GAAA,EACE,UAAA;AAAA,MAAA,gBAAAE,EAAC,QAAA,EAAK,WAAWD,EAAO,KAAK,eAAY,QAAO;AAAA,MAChD,gBAAAC,EAAC,QAAA,EAAK,WAAWD,EAAO,QAAS,UAAAV,EAAA,CAAI;AAAA,IAAA,EAAA,CACvC,EAAA,CAGN;AAAA,EAAA,GACF;AAGF,SAAIE,IAGA,gBAAAS,EAFWR,KAAiB,OAEtB,WAAWO,EAAO,KAAK,MAAAR,GAAY,SAAAE,GAAmB,GAAGC,GAC5D,UAAAE,EAAA,CACH,IAKF,gBAAAI,EAAC,UAAA,EAAO,MAAK,UAAS,WAAWD,EAAO,KAAK,SAAAN,GAAmB,GAAGC,GAChE,UAAAE,EAAA,CACH;AAEJ;"}
@@ -1,4 +1,4 @@
1
- import { ElementType } from 'react';
1
+ import { ElementType, Ref } from 'react';
2
2
  import { NotificationButtonLabels } from './labels';
3
3
  export interface NotificationButtonProps {
4
4
  /**
@@ -14,6 +14,30 @@ export interface NotificationButtonProps {
14
14
  linkComponent?: ElementType;
15
15
  /** The button's words. Each field defaults to English. */
16
16
  labels?: Partial<NotificationButtonLabels>;
17
+ /**
18
+ * How many are unread (0.8.0-next.5, Content). It goes into the accessible
19
+ * name -- "Notifications, 3 unread" through `labels.unreadCount` -- so a
20
+ * screen reader hears how many, not only that there are some. Above 0 it also
21
+ * draws the dot, as `unread` does; 0 draws none. The figure is not drawn:
22
+ * board 20 draws a dot, not a badge, and a visible count wants a board first.
23
+ */
24
+ unreadCount?: number;
25
+ /**
26
+ * Whether what the button opens is open. Set it when `onClick` toggles a
27
+ * panel, and the button carries `aria-expanded` (the disclosure pattern,
28
+ * ADR-0010). Ignored on the link form: a link navigates, it opens nothing.
29
+ */
30
+ expanded?: boolean;
31
+ /** The id of the panel the button opens, as `aria-controls`. Button form only, like `expanded`. */
32
+ controls?: string;
33
+ /**
34
+ * The rendered `<button>` or link, for a consumer that anchors a panel to
35
+ * the bell or returns focus to it when the panel closes (0.8.0-next.5,
36
+ * Content). React 19 passes `ref` as an ordinary prop, so this is that and
37
+ * not `forwardRef`. On the link form it reaches whatever `linkComponent`
38
+ * renders, provided that component forwards it (next/link does).
39
+ */
40
+ ref?: Ref<HTMLButtonElement | HTMLAnchorElement>;
17
41
  }
18
42
  /**
19
43
  * The bell in the top bar, next to the avatar (B33-1712, board 20 of the 3 Nov
@@ -26,4 +50,4 @@ export interface NotificationButtonProps {
26
50
  * `href` the bell would be a control that does nothing, so it does not render
27
51
  * (the rule B33-1270 set for the wallet's top-up and the switcher's account row).
28
52
  */
29
- export declare function NotificationButton({ unread, onClick, href, linkComponent, labels }: NotificationButtonProps): import("react").JSX.Element | null;
53
+ export declare function NotificationButton({ unread: unreadProp, unreadCount, onClick, href, linkComponent, labels, expanded, controls, ref }: NotificationButtonProps): import("react").JSX.Element | null;
@@ -1,23 +1,35 @@
1
- import { jsx as t, jsxs as r, Fragment as d } from "react/jsx-runtime";
2
- import { resolveNotificationButtonLabels as u } from "./labels.js";
1
+ import { jsx as n, jsxs as i, Fragment as h } from "react/jsx-runtime";
2
+ import { resolveNotificationButtonLabels as x } from "./labels.js";
3
3
  import o from "./NotificationButton.module.css.js";
4
- function h({ unread: n = !1, onClick: i, href: e, linkComponent: c, labels: l }) {
5
- const s = u(l);
6
- if (e === void 0 && i === void 0) return null;
7
- const a = /* @__PURE__ */ r(d, { children: [
8
- /* @__PURE__ */ r("svg", { className: o.bell, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
9
- /* @__PURE__ */ t("path", { d: "M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9" }),
10
- /* @__PURE__ */ t("path", { d: "M10.3 21a1.94 1.94 0 0 0 3.4 0" }),
11
- n && /* @__PURE__ */ t("circle", { className: o.dot, cx: "18", cy: "6.5", r: "3", stroke: "none" })
4
+ function y({ unread: l = !1, unreadCount: t, onClick: s, href: r, linkComponent: u, labels: m, expanded: p, controls: v, ref: d }) {
5
+ const a = x(m);
6
+ if (r === void 0 && s === void 0) return null;
7
+ const e = t !== void 0 ? t > 0 : l, f = t !== void 0 && t > 0 ? a.unreadCount(String(t)) : a.unread, c = /* @__PURE__ */ i(h, { children: [
8
+ /* @__PURE__ */ i("svg", { className: o.bell, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
9
+ /* @__PURE__ */ n("path", { d: "M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9" }),
10
+ /* @__PURE__ */ n("path", { d: "M10.3 21a1.94 1.94 0 0 0 3.4 0" }),
11
+ e && /* @__PURE__ */ n("circle", { className: o.dot, cx: "18", cy: "6.5", r: "3", stroke: "none" })
12
12
  ] }),
13
- /* @__PURE__ */ r("span", { className: o.srOnly, children: [
14
- s.name,
15
- n && `, ${s.unread}`
13
+ /* @__PURE__ */ i("span", { className: o.srOnly, children: [
14
+ a.name,
15
+ e && `, ${f}`
16
16
  ] })
17
17
  ] });
18
- return e !== void 0 ? /* @__PURE__ */ t(c ?? "a", { className: o.button, href: e, "data-unread": n ? "true" : void 0, children: a }) : /* @__PURE__ */ t("button", { type: "button", className: o.button, onClick: i, "data-unread": n ? "true" : void 0, children: a });
18
+ return r !== void 0 ? /* @__PURE__ */ n(u ?? "a", { ref: d, className: o.button, href: r, "data-unread": e ? "true" : void 0, children: c }) : /* @__PURE__ */ n(
19
+ "button",
20
+ {
21
+ ref: d,
22
+ type: "button",
23
+ className: o.button,
24
+ onClick: s,
25
+ "aria-expanded": p,
26
+ "aria-controls": v,
27
+ "data-unread": e ? "true" : void 0,
28
+ children: c
29
+ }
30
+ );
19
31
  }
20
32
  export {
21
- h as NotificationButton
33
+ y as NotificationButton
22
34
  };
23
35
  //# sourceMappingURL=NotificationButton.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"NotificationButton.js","sources":["../../../src/components/NotificationButton/NotificationButton.tsx"],"sourcesContent":["// Not a client module, on the same footing as NavItem: it holds no state and\n// calls no hook. A consumer passing `onClick` is already a client component; one\n// passing `href` can render this from the server (B33-846).\n\nimport type { ElementType } from 'react'\nimport { resolveNotificationButtonLabels, type NotificationButtonLabels } from './labels'\nimport styles from './NotificationButton.module.css'\n\nexport interface NotificationButtonProps {\n /**\n * Draws the unread dot, and tells a screen reader (`labels.unread`). The\n * consumer decides what counts as unread; the button fetches nothing.\n */\n unread?: boolean\n /** What pressing it does -- opening the product's notifications. Renders a `<button>`. */\n onClick?: () => void\n /** Where it goes, when it navigates rather than opens something. Renders a link. Wins over `onClick`. */\n href?: string\n /** The element the link form renders through (ADR-0011). Defaults to `a`. */\n linkComponent?: ElementType\n /** The button's words. Each field defaults to English. */\n labels?: Partial<NotificationButtonLabels>\n}\n\n/**\n * The bell in the top bar, next to the avatar (B33-1712, board 20 of the 3 Nov\n * 2026 redesign): a 32px square on the muted fill, a 17px bell, and an orange\n * dot when something is unread.\n *\n * It is the button and nothing more. NotificationCenter, the panel it would\n * open, is Stage A and not releasable (B33-494), so this opens whatever the\n * consumer wires to it -- or nothing is drawn at all: with neither `onClick` nor\n * `href` the bell would be a control that does nothing, so it does not render\n * (the rule B33-1270 set for the wallet's top-up and the switcher's account row).\n */\nexport function NotificationButton({ unread = false, onClick, href, linkComponent, labels }: NotificationButtonProps) {\n const t = resolveNotificationButtonLabels(labels)\n if (href === undefined && onClick === undefined) return null\n\n const content = (\n <>\n <svg className={styles.bell} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9\" />\n <path d=\"M10.3 21a1.94 1.94 0 0 0 3.4 0\" />\n {unread && <circle className={styles.dot} cx=\"18\" cy=\"6.5\" r=\"3\" stroke=\"none\" />}\n </svg>\n <span className={styles.srOnly}>\n {t.name}\n {unread && `, ${t.unread}`}\n </span>\n </>\n )\n\n if (href !== undefined) {\n const Link = linkComponent ?? 'a'\n return (\n <Link className={styles.button} href={href} data-unread={unread ? 'true' : undefined}>\n {content}\n </Link>\n )\n }\n\n return (\n <button type=\"button\" className={styles.button} onClick={onClick} data-unread={unread ? 'true' : undefined}>\n {content}\n </button>\n )\n}\n"],"names":["NotificationButton","unread","onClick","href","linkComponent","labels","t","resolveNotificationButtonLabels","content","jsxs","Fragment","styles","jsx"],"mappings":";;;AAmCO,SAASA,EAAmB,EAAE,QAAAC,IAAS,IAAO,SAAAC,GAAS,MAAAC,GAAM,eAAAC,GAAe,QAAAC,KAAmC;AACpH,QAAMC,IAAIC,EAAgCF,CAAM;AAChD,MAAIF,MAAS,UAAaD,MAAY,OAAW,QAAO;AAExD,QAAMM,IACJ,gBAAAC,EAAAC,GAAA,EACE,UAAA;AAAA,IAAA,gBAAAD,EAAC,SAAI,WAAWE,EAAO,MAAM,SAAQ,aAAY,MAAK,QAAO,QAAO,gBAAe,aAAY,KAAI,eAAc,SAAQ,gBAAe,SAAQ,eAAY,QAC1J,UAAA;AAAA,MAAA,gBAAAC,EAAC,QAAA,EAAK,GAAE,4CAAA,CAA4C;AAAA,MACpD,gBAAAA,EAAC,QAAA,EAAK,GAAE,iCAAA,CAAiC;AAAA,MACxCX,KAAU,gBAAAW,EAAC,UAAA,EAAO,WAAWD,EAAO,KAAK,IAAG,MAAK,IAAG,OAAM,GAAE,KAAI,QAAO,OAAA,CAAO;AAAA,IAAA,GACjF;AAAA,IACA,gBAAAF,EAAC,QAAA,EAAK,WAAWE,EAAO,QACrB,UAAA;AAAA,MAAAL,EAAE;AAAA,MACFL,KAAU,KAAKK,EAAE,MAAM;AAAA,IAAA,EAAA,CAC1B;AAAA,EAAA,GACF;AAGF,SAAIH,MAAS,SAGT,gBAAAS,EAFWR,KAAiB,KAE3B,EAAK,WAAWO,EAAO,QAAQ,MAAAR,GAAY,eAAaF,IAAS,SAAS,QACxE,UAAAO,EAAA,CACH,IAKF,gBAAAI,EAAC,UAAA,EAAO,MAAK,UAAS,WAAWD,EAAO,QAAQ,SAAAT,GAAkB,eAAaD,IAAS,SAAS,QAC9F,UAAAO,GACH;AAEJ;"}
1
+ {"version":3,"file":"NotificationButton.js","sources":["../../../src/components/NotificationButton/NotificationButton.tsx"],"sourcesContent":["// Not a client module, on the same footing as NavItem: it holds no state and\n// calls no hook. A consumer passing `onClick` is already a client component; one\n// passing `href` can render this from the server (B33-846).\n\nimport type { ElementType, Ref } from 'react'\nimport { resolveNotificationButtonLabels, type NotificationButtonLabels } from './labels'\nimport styles from './NotificationButton.module.css'\n\nexport interface NotificationButtonProps {\n /**\n * Draws the unread dot, and tells a screen reader (`labels.unread`). The\n * consumer decides what counts as unread; the button fetches nothing.\n */\n unread?: boolean\n /** What pressing it does -- opening the product's notifications. Renders a `<button>`. */\n onClick?: () => void\n /** Where it goes, when it navigates rather than opens something. Renders a link. Wins over `onClick`. */\n href?: string\n /** The element the link form renders through (ADR-0011). Defaults to `a`. */\n linkComponent?: ElementType\n /** The button's words. Each field defaults to English. */\n labels?: Partial<NotificationButtonLabels>\n /**\n * How many are unread (0.8.0-next.5, Content). It goes into the accessible\n * name -- \"Notifications, 3 unread\" through `labels.unreadCount` -- so a\n * screen reader hears how many, not only that there are some. Above 0 it also\n * draws the dot, as `unread` does; 0 draws none. The figure is not drawn:\n * board 20 draws a dot, not a badge, and a visible count wants a board first.\n */\n unreadCount?: number\n /**\n * Whether what the button opens is open. Set it when `onClick` toggles a\n * panel, and the button carries `aria-expanded` (the disclosure pattern,\n * ADR-0010). Ignored on the link form: a link navigates, it opens nothing.\n */\n expanded?: boolean\n /** The id of the panel the button opens, as `aria-controls`. Button form only, like `expanded`. */\n controls?: string\n /**\n * The rendered `<button>` or link, for a consumer that anchors a panel to\n * the bell or returns focus to it when the panel closes (0.8.0-next.5,\n * Content). React 19 passes `ref` as an ordinary prop, so this is that and\n * not `forwardRef`. On the link form it reaches whatever `linkComponent`\n * renders, provided that component forwards it (next/link does).\n */\n ref?: Ref<HTMLButtonElement | HTMLAnchorElement>\n}\n\n/**\n * The bell in the top bar, next to the avatar (B33-1712, board 20 of the 3 Nov\n * 2026 redesign): a 32px square on the muted fill, a 17px bell, and an orange\n * dot when something is unread.\n *\n * It is the button and nothing more. NotificationCenter, the panel it would\n * open, is Stage A and not releasable (B33-494), so this opens whatever the\n * consumer wires to it -- or nothing is drawn at all: with neither `onClick` nor\n * `href` the bell would be a control that does nothing, so it does not render\n * (the rule B33-1270 set for the wallet's top-up and the switcher's account row).\n */\nexport function NotificationButton({ unread: unreadProp = false, unreadCount, onClick, href, linkComponent, labels, expanded, controls, ref }: NotificationButtonProps) {\n const t = resolveNotificationButtonLabels(labels)\n if (href === undefined && onClick === undefined) return null\n // A count wins over the flag when both are given: it is the more precise\n // claim, and the two cannot then disagree about whether to draw the dot.\n const unread = unreadCount !== undefined ? unreadCount > 0 : unreadProp\n const unreadText = unreadCount !== undefined && unreadCount > 0 ? t.unreadCount(String(unreadCount)) : t.unread\n\n const content = (\n <>\n <svg className={styles.bell} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9\" />\n <path d=\"M10.3 21a1.94 1.94 0 0 0 3.4 0\" />\n {unread && <circle className={styles.dot} cx=\"18\" cy=\"6.5\" r=\"3\" stroke=\"none\" />}\n </svg>\n <span className={styles.srOnly}>\n {t.name}\n {unread && `, ${unreadText}`}\n </span>\n </>\n )\n\n if (href !== undefined) {\n const Link = linkComponent ?? 'a'\n return (\n <Link ref={ref} className={styles.button} href={href} data-unread={unread ? 'true' : undefined}>\n {content}\n </Link>\n )\n }\n\n return (\n <button\n ref={ref as Ref<HTMLButtonElement>}\n type=\"button\"\n className={styles.button}\n onClick={onClick}\n aria-expanded={expanded}\n aria-controls={controls}\n data-unread={unread ? 'true' : undefined}\n >\n {content}\n </button>\n )\n}\n"],"names":["NotificationButton","unreadProp","unreadCount","onClick","href","linkComponent","labels","expanded","controls","ref","t","resolveNotificationButtonLabels","unread","unreadText","content","jsxs","Fragment","styles","jsx"],"mappings":";;;AA2DO,SAASA,EAAmB,EAAE,QAAQC,IAAa,IAAO,aAAAC,GAAa,SAAAC,GAAS,MAAAC,GAAM,eAAAC,GAAe,QAAAC,GAAQ,UAAAC,GAAU,UAAAC,GAAU,KAAAC,KAAgC;AACtK,QAAMC,IAAIC,EAAgCL,CAAM;AAChD,MAAIF,MAAS,UAAaD,MAAY,OAAW,QAAO;AAGxD,QAAMS,IAASV,MAAgB,SAAYA,IAAc,IAAID,GACvDY,IAAaX,MAAgB,UAAaA,IAAc,IAAIQ,EAAE,YAAY,OAAOR,CAAW,CAAC,IAAIQ,EAAE,QAEnGI,IACJ,gBAAAC,EAAAC,GAAA,EACE,UAAA;AAAA,IAAA,gBAAAD,EAAC,SAAI,WAAWE,EAAO,MAAM,SAAQ,aAAY,MAAK,QAAO,QAAO,gBAAe,aAAY,KAAI,eAAc,SAAQ,gBAAe,SAAQ,eAAY,QAC1J,UAAA;AAAA,MAAA,gBAAAC,EAAC,QAAA,EAAK,GAAE,4CAAA,CAA4C;AAAA,MACpD,gBAAAA,EAAC,QAAA,EAAK,GAAE,iCAAA,CAAiC;AAAA,MACxCN,KAAU,gBAAAM,EAAC,UAAA,EAAO,WAAWD,EAAO,KAAK,IAAG,MAAK,IAAG,OAAM,GAAE,KAAI,QAAO,OAAA,CAAO;AAAA,IAAA,GACjF;AAAA,IACA,gBAAAF,EAAC,QAAA,EAAK,WAAWE,EAAO,QACrB,UAAA;AAAA,MAAAP,EAAE;AAAA,MACFE,KAAU,KAAKC,CAAU;AAAA,IAAA,EAAA,CAC5B;AAAA,EAAA,GACF;AAGF,SAAIT,MAAS,SAGT,gBAAAc,EAFWb,KAAiB,KAE3B,EAAK,KAAAI,GAAU,WAAWQ,EAAO,QAAQ,MAAAb,GAAY,eAAaQ,IAAS,SAAS,QAClF,UAAAE,EAAA,CACH,IAKF,gBAAAI;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,KAAAT;AAAA,MACA,MAAK;AAAA,MACL,WAAWQ,EAAO;AAAA,MAClB,SAAAd;AAAA,MACA,iBAAeI;AAAA,MACf,iBAAeC;AAAA,MACf,eAAaI,IAAS,SAAS;AAAA,MAE9B,UAAAE;AAAA,IAAA;AAAA,EAAA;AAGP;"}
@@ -6,7 +6,24 @@ export interface NotificationButtonLabels {
6
6
  * colour alone. Default "unread".
7
7
  */
8
8
  unread: string;
9
+ /**
10
+ * Appended instead of `unread` when the consumer passes `unreadCount`. A
11
+ * template: `%count%` is the number. Default `%count% unread`. Optional, so a
12
+ * catalogue written before 0.8.0-next.5 still type-checks; it falls back to
13
+ * English until translated.
14
+ */
15
+ unreadCount?: string;
16
+ }
17
+ export declare const defaultNotificationButtonLabels: {
18
+ name: string;
19
+ unread: string;
20
+ unreadCount: string;
21
+ };
22
+ /** The labels as the render consumes them, the template presented as a function. */
23
+ export interface ResolvedNotificationButtonLabels {
24
+ name: string;
25
+ unread: string;
26
+ unreadCount: (count: string) => string;
9
27
  }
10
- export declare const defaultNotificationButtonLabels: NotificationButtonLabels;
11
28
  /** Merge partial labels onto the defaults, skipping explicit `undefined`s (B33-873). */
12
- export declare function resolveNotificationButtonLabels(labels?: Partial<NotificationButtonLabels>): NotificationButtonLabels;
29
+ export declare function resolveNotificationButtonLabels(labels?: Partial<NotificationButtonLabels>): ResolvedNotificationButtonLabels;
@@ -1,15 +1,17 @@
1
- const n = {
1
+ import { fillTemplate as i } from "../../fill-template.js";
2
+ const r = {
2
3
  name: "Notifications",
3
- unread: "unread"
4
+ unread: "unread",
5
+ unreadCount: "%count% unread"
4
6
  };
5
- function o(t) {
6
- const e = Object.fromEntries(
7
- Object.entries(t ?? {}).filter(([, i]) => i !== void 0)
8
- );
9
- return { ...n, ...e };
7
+ function a(n) {
8
+ const o = Object.fromEntries(
9
+ Object.entries(n ?? {}).filter(([, t]) => t !== void 0)
10
+ ), e = { ...r, ...o };
11
+ return { ...e, unreadCount: (t) => i(e.unreadCount, { count: t }) };
10
12
  }
11
13
  export {
12
- n as defaultNotificationButtonLabels,
13
- o as resolveNotificationButtonLabels
14
+ r as defaultNotificationButtonLabels,
15
+ a as resolveNotificationButtonLabels
14
16
  };
15
17
  //# sourceMappingURL=labels.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"labels.js","sources":["../../../src/components/NotificationButton/labels.ts"],"sourcesContent":["// The notification button's words, each a prop with an English default -- the\n// pattern every labels module here follows (B33-849).\n\nexport interface NotificationButtonLabels {\n /** The button's accessible name. It draws no text, so this is its whole name. Default \"Notifications\". */\n name: string\n /**\n * Appended for a screen reader when `unread` is set, because the unread dot is\n * colour alone. Default \"unread\".\n */\n unread: string\n}\n\nexport const defaultNotificationButtonLabels: NotificationButtonLabels = {\n name: 'Notifications',\n unread: 'unread',\n}\n\n/** Merge partial labels onto the defaults, skipping explicit `undefined`s (B33-873). */\nexport function resolveNotificationButtonLabels(labels?: Partial<NotificationButtonLabels>): NotificationButtonLabels {\n const supplied = Object.fromEntries(\n Object.entries(labels ?? {}).filter(([, value]) => value !== undefined),\n ) as Partial<NotificationButtonLabels>\n return { ...defaultNotificationButtonLabels, ...supplied }\n}\n"],"names":["defaultNotificationButtonLabels","resolveNotificationButtonLabels","labels","supplied","value"],"mappings":"AAaO,MAAMA,IAA4D;AAAA,EACvE,MAAM;AAAA,EACN,QAAQ;AACV;AAGO,SAASC,EAAgCC,GAAsE;AACpH,QAAMC,IAAW,OAAO;AAAA,IACtB,OAAO,QAAQD,KAAU,EAAE,EAAE,OAAO,CAAC,CAAA,EAAGE,CAAK,MAAMA,MAAU,MAAS;AAAA,EAAA;AAExE,SAAO,EAAE,GAAGJ,GAAiC,GAAGG,EAAA;AAClD;"}
1
+ {"version":3,"file":"labels.js","sources":["../../../src/components/NotificationButton/labels.ts"],"sourcesContent":["// The notification button's words, each a prop with an English default -- the\n// pattern every labels module here follows (B33-849).\n\nimport { fillTemplate } from '../../fill-template'\n\nexport interface NotificationButtonLabels {\n /** The button's accessible name. It draws no text, so this is its whole name. Default \"Notifications\". */\n name: string\n /**\n * Appended for a screen reader when `unread` is set, because the unread dot is\n * colour alone. Default \"unread\".\n */\n unread: string\n /**\n * Appended instead of `unread` when the consumer passes `unreadCount`. A\n * template: `%count%` is the number. Default `%count% unread`. Optional, so a\n * catalogue written before 0.8.0-next.5 still type-checks; it falls back to\n * English until translated.\n */\n unreadCount?: string\n}\n\nexport const defaultNotificationButtonLabels = {\n name: 'Notifications',\n unread: 'unread',\n unreadCount: '%count% unread',\n} satisfies Required<NotificationButtonLabels>\n\n/** The labels as the render consumes them, the template presented as a function. */\nexport interface ResolvedNotificationButtonLabels {\n name: string\n unread: string\n unreadCount: (count: string) => string\n}\n\n/** Merge partial labels onto the defaults, skipping explicit `undefined`s (B33-873). */\nexport function resolveNotificationButtonLabels(labels?: Partial<NotificationButtonLabels>): ResolvedNotificationButtonLabels {\n const supplied = Object.fromEntries(\n Object.entries(labels ?? {}).filter(([, value]) => value !== undefined),\n ) as Partial<NotificationButtonLabels>\n const merged = { ...defaultNotificationButtonLabels, ...supplied }\n return { ...merged, unreadCount: (count) => fillTemplate(merged.unreadCount, { count }) }\n}\n"],"names":["defaultNotificationButtonLabels","resolveNotificationButtonLabels","labels","supplied","value","merged","count","fillTemplate"],"mappings":";AAsBO,MAAMA,IAAkC;AAAA,EAC7C,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,aAAa;AACf;AAUO,SAASC,EAAgCC,GAA8E;AAC5H,QAAMC,IAAW,OAAO;AAAA,IACtB,OAAO,QAAQD,KAAU,EAAE,EAAE,OAAO,CAAC,CAAA,EAAGE,CAAK,MAAMA,MAAU,MAAS;AAAA,EAAA,GAElEC,IAAS,EAAE,GAAGL,GAAiC,GAAGG,EAAA;AACxD,SAAO,EAAE,GAAGE,GAAQ,aAAa,CAACC,MAAUC,EAAaF,EAAO,aAAa,EAAE,OAAAC,EAAA,CAAO,EAAA;AACxF;"}
@@ -27,6 +27,12 @@ interface WalletChipChromeProps {
27
27
  /**
28
28
  * The element the popover's "Open" link renders through, so it routes
29
29
  * client-side with the locale prefix intact (ADR-0011). Defaults to `a`.
30
+ *
31
+ * **Pass it from a client component only.** This is a `'use client'` module,
32
+ * and a Server Component cannot hand a function across that boundary: Next
33
+ * throws and the render 500s (Passport). Render this component inside a
34
+ * `'use client'` file of your own and pass that into AppShell's slot. See the
35
+ * README, "Server Components". The same holds for `onTopUp`.
30
36
  */
31
37
  linkComponent?: ElementType;
32
38
  /** Render the top-bar popover open on first paint -- the specimen's open state. */
@@ -1 +1 @@
1
- {"version":3,"file":"WalletChip.js","sources":["../../../src/components/WalletChip/WalletChip.tsx"],"sourcesContent":["'use client'\n\n// A client module: the chip wires `onTopUp` to a button's onClick, which a\n// Server Component cannot carry. The rest of the shell stays on the server --\n// see Sidebar and B33-846.\n\nimport { useLayoutEffect, useRef, type ElementType, type RefObject } from 'react'\nimport { useAnchoredOverlay } from '../../anchored-overlay'\nimport { Chevron } from '../AppSwitcher/icons'\nimport buttons from '../../buttons.module.css'\nimport styles from './WalletChip.module.css'\nimport { DEFAULT_WALLET_LOCALE, formatPercent, formatTokens, thousandsFigure } from './format'\nimport { RetryMark, WalletMark } from './icons'\nimport { resolveWalletChipLabels, type WalletChipLabels } from './labels'\n\n// Past this fraction of the cycle's grant burned, the chip stops resting. The\n// spec's threshold, in one place so the derivation and the styling agree.\n//\n// It stays in the package and is not a prop, which is the whole reason\n// `burnedFraction` is a fraction rather than a `threshold?: boolean` (B33-483).\n// The consumer measures its own burn; 80% is where four products agree that\n// measurement means something, and a boolean would hand each of them the policy\n// and let it drift. It also happens to agree with the middle rung of Score's own\n// burndown alerts, so adopting it changed no product decision.\nconst THRESHOLD_BURN = 0.8\n\n// The floor under a non-zero bar segment, named once so the width expression\n// below reads as what it is. The value and its derivation live in the token --\n// see --spacing-wallet-segment-min in tokens.css.\nconst SEGMENT_MIN = 'var(--spacing-wallet-segment-min)'\n\n/**\n * A bar segment's CSS `width`: its honest share of everything the cycle started\n * with, floored so that a non-zero amount is never drawn below one visible mark.\n *\n * `max()` rather than a `min-width` on the segment classes, and the reason is the\n * zero case (B33-590). A stylesheet sees only the percentage, and no CSS length\n * can tell 0% from 0.3% -- a bare `min-width` would floor a wallet that holds\n * none of that kind of token into a phantom sliver, which is the same lie in the\n * other direction. The decision needs the amount itself, so it is made here where\n * the amount is, in one expression instead of a rule plus a signal from the\n * component to switch it off. `max()` also keeps the honest percentage wherever\n * the share clears the floor: the floor only ever wins when the truth is\n * unpaintable.\n *\n * Why it needs flooring at all: once a seller has bought tokens the grant is a\n * small share of the cycle, and a small share of a 240px bar is a fraction of a\n * pixel. Score measured 3,000 granted rendering at `width: 0.3%` -- 0.72px, which\n * paints nothing, while the ledger still held the grant. Both segments are\n * floored: purchased burns second, so it drains to zero through every small\n * number on the way, and 10 purchased on a spent 3,000 grant is 0.33% of the\n * cycle and the seller's entire remaining balance.\n *\n * This is the second time the rendering has been reported. Score fixed it in its\n * own wallet markup; B33-482 replaced that markup with this component, which\n * carried no floor. Its record of the first fix is still on disk there, as dead\n * code, at components/app-shell/wallet-bar.ts.\n */\nfunction segmentWidth(amount: number, cycleTotal: number): string {\n // A cycle of nothing has no shares in it. Guarded because the divisor is\n // reachable -- a known balance of zero on a cycle that granted zero rendered\n // `width: NaN%`, which a browser drops rather than reports.\n const share = cycleTotal > 0 ? (amount / cycleTotal) * 100 : 0\n if (amount <= 0) return `${share}%`\n return `max(${share}%, ${SEGMENT_MIN})`\n}\n\n/**\n * The props that say nothing about a balance, so both states take them\n * identically: where the chip is drawn, what it is wired to, and the words and\n * number formats it renders with.\n */\ninterface WalletChipChromeProps {\n /**\n * Where the chip is drawn. `'sidebar'`, the default, is the card it has always\n * been, pinned at the sidebar's foot. `'topbar'` is the 3 Nov 2026 redesign's\n * token chip (B33-1712, boards 20 and 21): a 32px pill in the top bar, drawn\n * like the app switcher, that opens a popover with the breakdown.\n *\n * One component in two placements rather than a second component, because the\n * figures, the bar's rules and every word are the same chip's (CLAUDE.md:\n * build once, add a prop). `collapsed` applies to the sidebar placement only;\n * the top bar has no rail.\n */\n placement?: 'sidebar' | 'topbar'\n /**\n * Where the popover's \"Open\" link goes -- the Wallet. `placement=\"topbar\"`\n * only. Omit it and the popover draws no link. The consumer supplies the URL;\n * Identity owns where the Wallet lives.\n */\n walletHref?: string\n /**\n * The element the popover's \"Open\" link renders through, so it routes\n * client-side with the locale prefix intact (ADR-0011). Defaults to `a`.\n */\n linkComponent?: ElementType\n /** Render the top-bar popover open on first paint -- the specimen's open state. */\n defaultOpen?: boolean\n /** The 56px rail. Abbreviates the balance and turns the split on its side. */\n collapsed?: boolean\n /**\n * Top up. Wiring for the one button the sidebar is allowed to have.\n *\n * **It is also what makes that button exist.** Omit it and the chip draws no\n * top-up affordance at all, in every state, and that is the fix B33-1270 made.\n * The render used to be gated on `!unknown` alone, so a consumer that passed\n * no handler got a visible control that did nothing and could not be removed\n * -- `.topUp`'s class name is CSS-Modules-hashed, so there is no selector to\n * reach it from outside the package.\n *\n * That was not hypothetical in either consumer. The Wallet passes no handler\n * and ships the dead button today\n * (`boostledger/src/app/(wallet)/WalletShell.tsx`), and Score wrote a whole\n * client module to arm it across a Server Component boundary rather than live\n * with it (`boostscore/components/app-shell/wallet-chip-top-up.tsx`, which\n * says so at length).\n *\n * Nothing renders in the button's place. It carries no information of its own\n * -- only its own affordance -- so a consumer with nowhere to send someone is\n * better served by the chip without it than by a disabled one they did not\n * ask for (ADR-0010, \"Not entitled is not disabled\", on why unavailable is not\n * the same as disabled). B33-1047 is the other half: an `href` form of the\n * same affordance, for a consumer that cannot pass a callback at all.\n */\n onTopUp?: () => void\n /**\n * The chip's user-facing strings. Every field has an English default, so a\n * consumer supplies only the ones it translates; the first two consumers are\n * French-default. The package takes no i18n dependency -- it renders what it\n * is given (B33-849).\n *\n * Every field is a plain string, so the whole object crosses the React Server\n * Component boundary and a Next App Router page can build it. The four labels\n * that carry a value mark its position with a `%placeholder%` (B33-1277). None\n * of them takes a function: that form only ever worked from a Client Component\n * and nothing in the type said so (B33-880, B33-1278).\n */\n labels?: Partial<WalletChipLabels>\n /**\n * The BCP-47 language tag the chip's figures are formatted in. Defaults to\n * `en-US`, which is what the chip rendered before this prop existed.\n *\n * `labels` gives the consumer the chip's words; this gives it their numbers.\n * French writes `4 600` where English writes `4,600`, and a label cannot fix\n * it -- the label functions receive the figures already formatted, so before\n * this prop there was no hook at all (B33-876). Grouping and the decimal\n * separator are CLDR facts about a locale rather than a property of the\n * consumer's sentence, so a tag is enough: `Intl.NumberFormat` knows the rest,\n * and a tag is a plain string, which a Server Component can pass where a\n * formatter function could not.\n *\n * It reaches the visible figures and, because the same strings are\n * interpolated into them, the `bar` and `railBalance` accessible names.\n *\n * The `k` after the rail's abbreviated balance is a word, not a number, so it\n * is `labels.railFigure` and not this.\n */\n locale?: string\n}\n\n/**\n * The chip with a balance the consumer stands behind. The two remaining figures\n * are required, because this is the state that renders them.\n */\nexport interface WalletChipKnownProps extends WalletChipChromeProps {\n /**\n * Absent or `false`. The discriminant: it is what makes the figures below\n * required here and forbidden in {@link WalletChipUnknownProps}.\n */\n unknown?: false\n /** Absent or `false`: see {@link WalletChipTotalOnlyProps}. */\n totalOnly?: false\n /** Remaining granted tokens. Granted burns first and expires. */\n granted: number\n /** Remaining purchased tokens. Purchased never expires. */\n purchased: number\n /**\n * What this cycle's grant started with, if the consumer knows. The bar's full\n * width alongside purchased, so the part already burned draws as empty track\n * and the filled portion shortens as the month runs.\n *\n * **Optional, and that is the fix B33-483 made.** It used to be required, and\n * no consumer could satisfy it honestly: it is the Ledger's\n * `grant_tokens_per_cycle`, and it cannot be reconstructed from a balance --\n * `cycleUsedPct` counts purchased-bucket spend, is not `(cycle total -\n * remaining)` once a credit, refund or void has touched the account, and is not\n * bounded at 100. So the Wallet passed `grantCycle = granted`, which pins\n * `burned` at 0 and held the threshold state unreachable in the reference\n * implementation, and Score passed a local per-plan belief instead.\n *\n * Omitted, the bar measures against `granted + purchased` -- the balance\n * itself. It then draws full and claims nothing about what burned, which is the\n * honest rendering when nobody can say.\n *\n * It no longer decides the threshold on its own: {@link burnedFraction} does\n * that, and this is only consulted when that is absent.\n */\n grantCycle?: number\n /**\n * How much of the cycle's grant has burned, as a fraction -- `0.88` for 88%.\n * Drives the threshold state, and is the figure the chip rounds into its pill.\n *\n * Passed in rather than derived, for exactly the reason {@link burnNote}\n * already is: the burn is the product's measurement to make, not the chip's.\n * The chip kept the arithmetic while taking the runway line as a prop, and fed\n * it from a denominator no product could supply -- so the measurement was the\n * chip's in name and nobody's in fact (B33-483).\n *\n * The *policy* stays here. `THRESHOLD_BURN` is 0.8 in this package, so four\n * products trip at the same place and cannot drift to four different numbers.\n * That is why this is a fraction and not a `threshold?: boolean`: a boolean\n * would move the 80% decision into each consumer, and it would leave the pill\n * with no figure to print.\n *\n * Supplied, it wins. Absent, the chip derives it from {@link grantCycle} as it\n * always did; absent with no `grantCycle` either, nothing is claimed and the\n * chip rests.\n *\n * It is not clamped. A consumer that passes a percentage where a fraction\n * belongs renders a visibly absurd pill rather than a plausible \"100%\", and a\n * wrong figure that looks wrong is the one that gets fixed.\n */\n burnedFraction?: number\n /**\n * When the current grant expires, preformatted for display, e.g. \"30 Sep\":\n * the day this month's grant runs out and the next one lands. The sidebar\n * card says it as an expiry (`labels.grantExpiry`), the top-bar popover as a\n * reset (`labels.grantedNote`); both are this one date.\n */\n grantExpires?: string\n /**\n * The threshold state's runway line, preformatted, e.g. \"About four days left\n * at your current burn.\" Passed in rather than derived: the burn estimate is\n * the product's to make, not the chip's -- the chip renders what it is given.\n */\n burnNote?: string\n}\n\n/**\n * The chip whose balance could not be confirmed. It renders an em dash, no bar\n * and no button, so **it takes no figures at all** -- each one below is\n * `?: never`.\n *\n * That is deliberate and it is the second half of B33-483. The figures were\n * required in every state, so both consumers manufactured zeroes to get past the\n * type and then apologised for them in a comment -- \"these zeroes are never\n * rendered ... but they are not a balance and must not be read as one\"\n * (`boostscore/components/app-shell/wallet-chip.tsx`), and the same again in\n * `wallet-slot.tsx` and `boostledger/src/app/(wallet)/layout.tsx`. A zero is a\n * confident claim about someone's money. The type now refuses it rather than\n * asking a comment to.\n *\n * `undefined` is still accepted, so a consumer that must build one props object\n * can write `{ unknown: true, granted: undefined }` instead of branching.\n */\nexport interface WalletChipUnknownProps extends WalletChipChromeProps {\n /** The balance could not be confirmed. Renders as unknown, never as zero. */\n unknown: true\n totalOnly?: false\n balance?: never\n granted?: never\n purchased?: never\n grantCycle?: never\n burnedFraction?: never\n /** Not rendered in this state: the footer only draws on a resting chip. */\n grantExpires?: never\n /** Not rendered in this state: the runway line only draws past the threshold. */\n burnNote?: never\n}\n\n/**\n * A discriminated union on `unknown`, and the discriminant has consequences for\n * how a consumer *types* its own code, not only what it passes:\n *\n * - A helper that returns props for both states needs an explicit\n * `: WalletChipProps` return annotation. Without one, TypeScript widens the\n * `unknown: true` in an object literal to `boolean`, which matches neither\n * member, and the spread at the call site stops compiling.\n * - `Omit<WalletChipProps, 'onTopUp'>` is not what it looks like. `Omit` over a\n * union flattens it into one object and loses the discrimination. Omit from\n * the members instead -- `Omit<WalletChipKnownProps, 'onTopUp'> |\n * Omit<WalletChipUnknownProps, 'onTopUp'>` -- which is why both are exported.\n */\nexport type WalletChipProps = WalletChipKnownProps | WalletChipUnknownProps | WalletChipTotalOnlyProps\n\n/**\n * The top-bar chip with a real total and no drawable split (B33-1712). Debits\n * draw the grant first, then purchased, and may run a set share past zero (the\n * Ledger's overdraft); credits add only to the bucket they name and never pay a\n * negative one off. So the Ledger can report a bucket below zero -- granted 0 /\n * purchased -40 (overdrawn, total -40), or granted 1000 / purchased -40 after\n * the month's grant lands (total 960, for the rest of the month). A two-segment\n * bar cannot draw a negative, and clamping it to zero would draw a split nobody\n * has: a split invented by the interface. So this state takes **the total and\n * no split**, and draws no bar and no granted / purchased legend.\n *\n * Below zero the figure is the distance past zero, positive, in --color-danger,\n * with the words \"past zero\" where a minus sign would be -- the Wallet's own\n * past-zero screen (boostledger, Paper 201 screen 03), followed rather than\n * inventing a third look. Nothing is broken and the copy does not say it is;\n * what happens next is the consumer's `burnNote`, because the overdraft limit is\n * the Ledger's. At zero or above it is the plain balance.\n *\n * **Top bar only.** The sidebar card has no drawing for this and the redesigned\n * shell does not render it, so the type requires `placement=\"topbar\"` rather\n * than leaving the card to guess. No board draws either form: they are new\n * undrawn states for the person at the gate to judge.\n */\nexport interface WalletChipTotalOnlyProps extends WalletChipChromeProps {\n placement: 'topbar'\n totalOnly: true\n unknown?: false\n /**\n * The balance, signed, in whole tokens: `granted + purchased` from the\n * Ledger's signed figures, which its single balance row guarantees is the\n * balance.\n */\n balance: number\n /**\n * What happens next, preformatted, e.g. \"Work carries on for another 930\n * tokens, then paid actions stop.\" The consumer's, because the overdraft limit\n * is the Ledger's, not the chip's.\n */\n burnNote?: string\n granted?: never\n purchased?: never\n grantCycle?: never\n burnedFraction?: never\n grantExpires?: never\n}\n\n/**\n * The granted/purchased bar. Granted is drawn first, because granted burns\n * first. The accessible label is passed in whole rather than assembled here:\n * word order is the consumer's, not English's (B33-849).\n */\nfunction Bar({ granted, purchased, cycleTotal, tone, label }: { granted: number; purchased: number; cycleTotal: number; tone: 'rest' | 'threshold'; label: string }) {\n return (\n <div className={styles.bar} data-tone={tone} role=\"img\" aria-label={label}>\n <span className={styles.segmentGranted} data-testid=\"segment-granted\" style={{ width: segmentWidth(granted, cycleTotal) }} />\n <span className={styles.segmentPurchased} data-testid=\"segment-purchased\" style={{ width: segmentWidth(purchased, cycleTotal) }} />\n </div>\n )\n}\n\nexport function WalletChip(props: WalletChipProps) {\n if (props.totalOnly === true) return <TotalOnlyChip {...props} />\n if (props.placement === 'topbar') return <TopBarChip {...props} />\n return <SidebarChip {...props} />\n}\n\n// The clear space the token popover keeps from the viewport's edges: the\n// 16px side gutter every Boost33 page keeps at phone width (--spacing-4). A\n// number because it is compared with getBoundingClientRect(), which reads px.\nconst POPOVER_GUTTER_PX = 16\n\n/**\n * Keeps the open token popover on screen (0.8.0-next.4). It hangs from the\n * pill's left edge and is 340px wide, so wherever the pill sits less than 340px\n * from the right of the viewport -- 1024px (8px over) and every phone width\n * (the right edge at 535px of 390) -- it ran off the edge and cut the\n * threshold line and the reset note. Measured after layout and before paint,\n * it slides left by exactly the overshoot, never past the left gutter; the\n * stylesheet caps its width at the viewport less both gutters. Re-measured on\n * resize while open. A layout effect, so the first frame is already in place.\n */\nfunction useClampToViewport(rootRef: RefObject<HTMLDivElement | null>, open: boolean) {\n useLayoutEffect(() => {\n if (!open) return\n const root = rootRef.current\n const panel = root?.querySelector<HTMLElement>('[data-token-popover]')\n if (!root || !panel) return\n const place = () => {\n panel.style.left = ''\n const rootLeft = root.getBoundingClientRect().left\n const rect = panel.getBoundingClientRect()\n const overshoot = rect.right - (document.documentElement.clientWidth - POPOVER_GUTTER_PX)\n if (overshoot <= 0) return\n const left = Math.max(rect.left - overshoot, POPOVER_GUTTER_PX)\n panel.style.left = `${left - rootLeft}px`\n }\n place()\n window.addEventListener('resize', place)\n return () => window.removeEventListener('resize', place)\n }, [rootRef, open])\n}\n\n/**\n * The top-bar token chip (B33-1712). The pill shows the balance and the\n * two-segment bar; the popover (board 21) shows the figure, the bar again at\n * width, each kind of token with what it means, the runway line, and the buy\n * button.\n *\n * Both segments are always drawn and a zero one is 0px wide with the gap kept\n * -- board 22, and B33-590's rule that a zero is never drawn as a sliver. That\n * overrides B33-1712's line about copying the hub's 4px floor, by Jamal's\n * decision of 3 Nov 2026. A non-zero segment keeps the existing floor\n * (`segmentWidth`).\n *\n * Every figure is the consumer's. Unknown renders an em dash, no bar and no\n * breakdown -- a made-up split in a token balance is a commercial fact invented\n * by the interface (B33-1712).\n *\n * A disclosure, like the switcher (ADR-0010): the trigger is a button, the\n * panel carries no widget role, and dismissal is the shared anchored-overlay\n * contract's (ADR-0009).\n */\nfunction TopBarChip({ granted = 0, purchased = 0, grantCycle, burnedFraction, grantExpires, burnNote, unknown = false, onTopUp, labels, locale = DEFAULT_WALLET_LOCALE, walletHref, linkComponent, defaultOpen }: WalletChipProps) {\n const t = resolveWalletChipLabels(labels)\n const overlay = useAnchoredOverlay({ defaultOpen })\n const rootRef = useRef<HTMLDivElement>(null)\n useClampToViewport(rootRef, overlay.open)\n const Link: ElementType = linkComponent ?? 'a'\n const balance = granted + purchased\n // The threshold, decided exactly as the sidebar card decides it: the\n // consumer's `burnedFraction` wins, `grantCycle` derives it otherwise, and\n // with neither nothing is claimed (B33-483). 0.8.0-next.0 dropped this from\n // the top bar and with it the only at-a-glance \"running low\" signal the\n // chip had (score-main, 4 Nov 2026); Jamal chose the sidebar's own drawing\n // to bring it back.\n const derivedBurn = grantCycle !== undefined && grantCycle > 0 ? (grantCycle - granted) / grantCycle : 0\n const burned = burnedFraction ?? derivedBurn\n const isThreshold = !unknown && burned >= THRESHOLD_BURN\n // At rest the bar draws the split of the balance itself, full width, with no\n // burned track (board 22): the top bar claims nothing about what the cycle\n // started with. Past the threshold, and only when `grantCycle` says what the\n // cycle started with, it measures against that instead -- the sidebar card's\n // drawing -- so the bar shortens and the burned share shows as the\n // warning-tinted track. With `burnedFraction` alone there is no honest\n // denominator, so the bar stays full and the popover line carries the warning.\n const total = isThreshold && grantCycle !== undefined ? grantCycle + purchased : balance\n const tone = isThreshold ? 'threshold' : undefined\n const figure = unknown ? '—' : formatTokens(balance, locale)\n const buildBarLabel = () => t.bar(formatTokens(granted, locale), formatTokens(purchased, locale), formatTokens(balance, locale))\n // Built only past the threshold, where it is drawn.\n const thresholdNote = isThreshold ? t.thresholdNote(formatPercent(burned, locale)) : undefined\n const triggerLabel = unknown ? t.railBalanceUnknown : thresholdNote ? `${t.railBalance(figure)}. ${thresholdNote}` : t.railBalance(figure)\n\n return (\n <div ref={rootRef} className={styles.topRoot} data-threshold={isThreshold ? 'true' : undefined}>\n <button type=\"button\" className={styles.topTrigger} aria-label={triggerLabel} onClick={overlay.toggle} {...overlay.triggerProps}>\n <span className={styles.topFigure} aria-hidden=\"true\">\n <span className={styles.topAmount}>{figure}</span>\n <span className={styles.topUnit}>{t.unit}</span>\n </span>\n {!unknown && <TopBar granted={granted} purchased={purchased} total={total} tone={tone} className={styles.topBar} />}\n <Chevron className={styles.topChevron} />\n </button>\n\n {overlay.open && (\n <div {...overlay.panelProps} className={styles.popover} data-token-popover=\"\" aria-label={t.popover}>\n <div className={styles.popHead}>\n <span className={styles.popFigure}>\n <span className={styles.popAmount}>{figure}</span>\n <span className={styles.popUnit}>{t.unit}</span>\n </span>\n {walletHref !== undefined && (\n <Link className={buttons.quiet} href={walletHref} onClick={overlay.close}>\n {t.open}\n </Link>\n )}\n </div>\n\n {unknown ? (\n <p className={styles.popNote}>{t.balanceUnknown}</p>\n ) : (\n <>\n <TopBar granted={granted} purchased={purchased} total={total} tone={tone} className={styles.popBar} label={buildBarLabel()} />\n <ul className={styles.legend}>\n <li className={styles.legendRow}>\n <span className={styles.swatchGranted} aria-hidden=\"true\" />\n <span className={styles.legendText}>\n <span className={styles.legendFigure}>{t.grantedFigure(formatTokens(granted, locale))}</span>\n <span className={styles.legendNote}>{grantExpires ? t.grantedNote(grantExpires) : t.grantedNoteUndated}</span>\n </span>\n </li>\n <li className={styles.legendRow}>\n <span className={styles.swatchPurchased} aria-hidden=\"true\" />\n <span className={styles.legendText}>\n <span className={styles.legendFigure}>{t.purchasedFigure(formatTokens(purchased, locale))}</span>\n <span className={styles.legendNote}>{t.purchasedNote}</span>\n </span>\n </li>\n </ul>\n {(thresholdNote || burnNote || onTopUp !== undefined) && (\n <div className={styles.popFoot}>\n {thresholdNote && <p className={styles.popWarning}>{thresholdNote}</p>}\n {burnNote && <p className={styles.popNote}>{burnNote}</p>}\n {onTopUp !== undefined && (\n <button type=\"button\" className={`${buttons.primary} ${styles.buy}`} data-tone=\"accent\" onClick={onTopUp}>\n {t.buyTokens}\n </button>\n )}\n </div>\n )}\n </>\n )}\n </div>\n )}\n </div>\n )\n}\n\n/**\n * The top-bar chip with no drawable split (`WalletChipTotalOnlyProps`): the same\n * pill and popover as {@link TopBarChip}, with the total alone -- the distance\n * past zero when it is below -- and no bar and no legend anywhere.\n */\nfunction TotalOnlyChip({ balance, burnNote, onTopUp, labels, locale = DEFAULT_WALLET_LOCALE, walletHref, linkComponent, defaultOpen }: WalletChipTotalOnlyProps) {\n const t = resolveWalletChipLabels(labels)\n const overlay = useAnchoredOverlay({ defaultOpen })\n const rootRef = useRef<HTMLDivElement>(null)\n useClampToViewport(rootRef, overlay.open)\n const Link: ElementType = linkComponent ?? 'a'\n const pastZero = balance < 0\n const figure = formatTokens(pastZero ? -balance : balance, locale)\n\n return (\n <div ref={rootRef} className={styles.topRoot} data-past-zero={pastZero ? 'true' : undefined}>\n <button type=\"button\" className={styles.topTrigger} aria-label={pastZero ? t.pastZeroBalance(figure) : t.railBalance(figure)} onClick={overlay.toggle} {...overlay.triggerProps}>\n <span className={styles.topFigure} aria-hidden=\"true\">\n <span className={styles.topAmount}>{figure}</span>\n <span className={styles.topUnit}>{pastZero ? t.pastZero : t.unit}</span>\n </span>\n <Chevron className={styles.topChevron} />\n </button>\n\n {overlay.open && (\n <div {...overlay.panelProps} className={styles.popover} data-token-popover=\"\" aria-label={t.popover}>\n <div className={styles.popHead}>\n <span className={styles.popFigure}>\n <span className={styles.popAmount}>{figure}</span>\n <span className={styles.popUnit}>{pastZero ? t.pastZeroLong : t.unit}</span>\n </span>\n {walletHref !== undefined && (\n <Link className={buttons.quiet} href={walletHref} onClick={overlay.close}>\n {t.open}\n </Link>\n )}\n </div>\n {(burnNote || onTopUp !== undefined) && (\n <div className={styles.popFoot}>\n {burnNote && <p className={styles.popNote}>{burnNote}</p>}\n {onTopUp !== undefined && (\n <button type=\"button\" className={`${buttons.primary} ${styles.buy}`} data-tone=\"accent\" onClick={onTopUp}>\n {t.buyTokens}\n </button>\n )}\n </div>\n )}\n </div>\n )}\n </div>\n )\n}\n\n/**\n * The top bar's two-segment bar, in the pill and in the popover. Granted first,\n * because it burns first; a fixed gap between the two that stays when a segment\n * is zero, so a two-part balance never reads as one lump (board 22).\n */\nfunction TopBar({ granted, purchased, total, tone, className, label }: { granted: number; purchased: number; total: number; tone?: 'threshold'; className?: string; label?: string }) {\n return (\n <span className={className} data-tone={tone} role={label ? 'img' : undefined} aria-label={label} aria-hidden={label ? undefined : 'true'}>\n <span className={styles.segmentGranted} data-segment=\"granted\" style={{ width: segmentWidth(granted, total) }} />\n <span className={styles.segmentPurchased} data-segment=\"purchased\" style={{ width: segmentWidth(purchased, total) }} />\n </span>\n )\n}\n\nfunction SidebarChip({ granted = 0, purchased = 0, grantCycle, burnedFraction, grantExpires, burnNote, unknown = false, collapsed = false, onTopUp, labels, locale = DEFAULT_WALLET_LOCALE }: WalletChipProps) {\n const t = resolveWalletChipLabels(labels)\n // Assembled lazily: the unknown state draws no bar, and a consumer's `bar`\n // implementation can throw on the zeroes the destructure above defaults the\n // degraded path's absent figures to. Calling it only where a bar renders keeps\n // that crash surface off the render that exists precisely because fetching the\n // balance already failed (B33-875). B33-483 stopped the consumer having to pass\n // those zeroes; the chip still has to pick something, so the laziness stays.\n const buildBarLabel = () =>\n t.bar(formatTokens(granted, locale), formatTokens(purchased, locale), formatTokens(granted + purchased, locale))\n const balance = granted + purchased\n // Everything the cycle started with: the grant plus the purchased balance\n // sitting alongside it. Measuring against this is what makes the bar shorten\n // as tokens burn instead of only re-proportioning to a full width.\n //\n // With no `grantCycle` the denominator falls back to `granted + purchased`, so\n // the bar fills and there is no burned track -- the honest drawing when nobody\n // can say what the cycle started with (B33-483). The zero-division guard inside\n // segmentWidth still covers the case where that balance is itself zero.\n const cycleTotal = (grantCycle ?? granted) + purchased\n // How much of the cycle's grant has burned. The consumer's own figure wins;\n // `grantCycle` derives it exactly as it always did; with neither, nothing is\n // claimed and the chip rests. `grantCycle` is tested against undefined rather\n // than leaned on for falsiness, because 0 and \"not supplied\" both resolve to a\n // burn of 0 and must keep doing so.\n const derivedBurn = grantCycle !== undefined && grantCycle > 0 ? (grantCycle - granted) / grantCycle : 0\n const burned = burnedFraction ?? derivedBurn\n const isThreshold = !unknown && burned >= THRESHOLD_BURN\n\n if (collapsed) {\n // The abbreviated balance, assembled once: it is both the rail's visible\n // text and the figure the group's accessible name is built around, and a\n // sighted user and a screen-reader user must not be told different numbers.\n // Built only when a balance is known, for the same reason buildBarLabel is\n // lazy -- the unknown rail shows no figure, and a consumer's railFigure\n // should not run on a degraded path it never renders on (B33-875).\n const railBalance = unknown ? undefined : t.railFigure(thousandsFigure(balance, locale))\n return (\n <div className={styles.rail} role=\"group\" aria-label={railBalance === undefined ? t.railBalanceUnknown : t.railBalance(railBalance)} tabIndex={0}>\n <WalletMark className={styles.railIcon} />\n <span className={styles.railFigure}>{railBalance ?? '—'}</span>\n {unknown ? <span className={styles.srOnly}>{t.railUnknown}</span> : <Bar granted={granted} purchased={purchased} cycleTotal={cycleTotal} tone={isThreshold ? 'threshold' : 'rest'} label={buildBarLabel()} />}\n </div>\n )\n }\n\n return (\n <div className={styles.chip} role=\"group\" aria-label={t.region} tabIndex={0} data-threshold={isThreshold ? 'true' : undefined} data-unknown={unknown ? 'true' : undefined}>\n <div className={styles.header}>\n <span className={styles.name}>{t.name}</span>\n {isThreshold && <span className={styles.pill}>{Math.round(burned * 100)}%</span>}\n </div>\n\n {unknown ? (\n <div className={styles.figure}>\n <span className={styles.amount} aria-hidden=\"true\">\n {'—'}\n </span>\n <RetryMark className={styles.retry} />\n <span className={styles.srOnly}>{t.balanceUnknown}</span>\n </div>\n ) : (\n <div className={styles.figure}>\n <span className={styles.amount}>{formatTokens(balance, locale)}</span>\n <span className={styles.unit}>{t.unit}</span>\n </div>\n )}\n\n {unknown ? (\n <div className={styles.bar} data-tone=\"unknown\" aria-hidden=\"true\" />\n ) : (\n <Bar granted={granted} purchased={purchased} cycleTotal={cycleTotal} tone={isThreshold ? 'threshold' : 'rest'} label={buildBarLabel()} />\n )}\n\n {!unknown && !isThreshold && (\n <div className={styles.details}>\n <dl className={styles.split}>\n <div className={styles.splitRow}>\n <dt className={styles.splitLabel}>\n <span className={styles.dotGranted} aria-hidden=\"true\" />\n {t.granted}\n </dt>\n <dd className={styles.splitAmount}>{formatTokens(granted, locale)}</dd>\n </div>\n <div className={styles.splitRow}>\n <dt className={styles.splitLabel}>\n <span className={styles.dotPurchased} aria-hidden=\"true\" />\n {t.purchased}\n </dt>\n <dd className={styles.splitAmount}>{formatTokens(purchased, locale)}</dd>\n </div>\n </dl>\n {grantExpires && <p className={styles.footer}>{t.grantExpiry(grantExpires)}</p>}\n </div>\n )}\n\n {isThreshold && burnNote && <p className={styles.burnNote}>{burnNote}</p>}\n\n {/* A handler is what makes the button exist, not only what it does when\n pressed (B33-1270). `!unknown` alone drew it for a consumer with\n nowhere to send anyone, and a hashed class name left them no way to\n take it back out -- see `onTopUp`. Tested against `undefined` rather\n than for truthiness so that `onTopUp={undefined}`, which is what a\n consumer's own absent value looks like, reads the same as omitting it. */}\n {!unknown && onTopUp !== undefined && (\n <button\n type=\"button\"\n // Two tiers for one button (B33-1713, ADR-0016): at rest it is a word\n // in the chip's header, the quiet tier; past the threshold it grows\n // into the chip's one call to action, the primary tier in the accent\n // tone. `.topUp` adds only where it sits.\n className={`${isThreshold ? buttons.primary : buttons.quiet} ${styles.topUp}`}\n data-tone={isThreshold ? 'accent' : undefined}\n data-grown={isThreshold ? 'true' : undefined}\n onClick={onTopUp}\n >\n {t.topUp}\n </button>\n )}\n </div>\n )\n}\n"],"names":["THRESHOLD_BURN","SEGMENT_MIN","segmentWidth","amount","cycleTotal","share","Bar","granted","purchased","tone","label","jsxs","styles","jsx","WalletChip","props","TotalOnlyChip","TopBarChip","SidebarChip","POPOVER_GUTTER_PX","useClampToViewport","rootRef","open","useLayoutEffect","root","panel","place","rootLeft","rect","overshoot","left","grantCycle","burnedFraction","grantExpires","burnNote","unknown","onTopUp","labels","locale","DEFAULT_WALLET_LOCALE","walletHref","linkComponent","defaultOpen","t","resolveWalletChipLabels","overlay","useAnchoredOverlay","useRef","Link","balance","derivedBurn","burned","isThreshold","total","figure","formatTokens","buildBarLabel","thresholdNote","formatPercent","triggerLabel","TopBar","Chevron","buttons","Fragment","pastZero","className","collapsed","railBalance","thousandsFigure","WalletMark","RetryMark"],"mappings":"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"}
1
+ {"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 { useLayoutEffect, useRef, type ElementType, type RefObject } from 'react'\nimport { useAnchoredOverlay } from '../../anchored-overlay'\nimport { Chevron } from '../AppSwitcher/icons'\nimport buttons from '../../buttons.module.css'\nimport styles from './WalletChip.module.css'\nimport { DEFAULT_WALLET_LOCALE, formatPercent, formatTokens, thousandsFigure } from './format'\nimport { RetryMark, WalletMark } from './icons'\nimport { resolveWalletChipLabels, type WalletChipLabels } from './labels'\n\n// Past this fraction of the cycle's grant burned, the chip stops resting. The\n// spec's threshold, in one place so the derivation and the styling agree.\n//\n// It stays in the package and is not a prop, which is the whole reason\n// `burnedFraction` is a fraction rather than a `threshold?: boolean` (B33-483).\n// The consumer measures its own burn; 80% is where four products agree that\n// measurement means something, and a boolean would hand each of them the policy\n// and let it drift. It also happens to agree with the middle rung of Score's own\n// burndown alerts, so adopting it changed no product decision.\nconst THRESHOLD_BURN = 0.8\n\n// The floor under a non-zero bar segment, named once so the width expression\n// below reads as what it is. The value and its derivation live in the token --\n// see --spacing-wallet-segment-min in tokens.css.\nconst SEGMENT_MIN = 'var(--spacing-wallet-segment-min)'\n\n/**\n * A bar segment's CSS `width`: its honest share of everything the cycle started\n * with, floored so that a non-zero amount is never drawn below one visible mark.\n *\n * `max()` rather than a `min-width` on the segment classes, and the reason is the\n * zero case (B33-590). A stylesheet sees only the percentage, and no CSS length\n * can tell 0% from 0.3% -- a bare `min-width` would floor a wallet that holds\n * none of that kind of token into a phantom sliver, which is the same lie in the\n * other direction. The decision needs the amount itself, so it is made here where\n * the amount is, in one expression instead of a rule plus a signal from the\n * component to switch it off. `max()` also keeps the honest percentage wherever\n * the share clears the floor: the floor only ever wins when the truth is\n * unpaintable.\n *\n * Why it needs flooring at all: once a seller has bought tokens the grant is a\n * small share of the cycle, and a small share of a 240px bar is a fraction of a\n * pixel. Score measured 3,000 granted rendering at `width: 0.3%` -- 0.72px, which\n * paints nothing, while the ledger still held the grant. Both segments are\n * floored: purchased burns second, so it drains to zero through every small\n * number on the way, and 10 purchased on a spent 3,000 grant is 0.33% of the\n * cycle and the seller's entire remaining balance.\n *\n * This is the second time the rendering has been reported. Score fixed it in its\n * own wallet markup; B33-482 replaced that markup with this component, which\n * carried no floor. Its record of the first fix is still on disk there, as dead\n * code, at components/app-shell/wallet-bar.ts.\n */\nfunction segmentWidth(amount: number, cycleTotal: number): string {\n // A cycle of nothing has no shares in it. Guarded because the divisor is\n // reachable -- a known balance of zero on a cycle that granted zero rendered\n // `width: NaN%`, which a browser drops rather than reports.\n const share = cycleTotal > 0 ? (amount / cycleTotal) * 100 : 0\n if (amount <= 0) return `${share}%`\n return `max(${share}%, ${SEGMENT_MIN})`\n}\n\n/**\n * The props that say nothing about a balance, so both states take them\n * identically: where the chip is drawn, what it is wired to, and the words and\n * number formats it renders with.\n */\ninterface WalletChipChromeProps {\n /**\n * Where the chip is drawn. `'sidebar'`, the default, is the card it has always\n * been, pinned at the sidebar's foot. `'topbar'` is the 3 Nov 2026 redesign's\n * token chip (B33-1712, boards 20 and 21): a 32px pill in the top bar, drawn\n * like the app switcher, that opens a popover with the breakdown.\n *\n * One component in two placements rather than a second component, because the\n * figures, the bar's rules and every word are the same chip's (CLAUDE.md:\n * build once, add a prop). `collapsed` applies to the sidebar placement only;\n * the top bar has no rail.\n */\n placement?: 'sidebar' | 'topbar'\n /**\n * Where the popover's \"Open\" link goes -- the Wallet. `placement=\"topbar\"`\n * only. Omit it and the popover draws no link. The consumer supplies the URL;\n * Identity owns where the Wallet lives.\n */\n walletHref?: string\n /**\n * The element the popover's \"Open\" link renders through, so it routes\n * client-side with the locale prefix intact (ADR-0011). Defaults to `a`.\n *\n * **Pass it from a client component only.** This is a `'use client'` module,\n * and a Server Component cannot hand a function across that boundary: Next\n * throws and the render 500s (Passport). Render this component inside a\n * `'use client'` file of your own and pass that into AppShell's slot. See the\n * README, \"Server Components\". The same holds for `onTopUp`.\n */\n linkComponent?: ElementType\n /** Render the top-bar popover open on first paint -- the specimen's open state. */\n defaultOpen?: boolean\n /** The 56px rail. Abbreviates the balance and turns the split on its side. */\n collapsed?: boolean\n /**\n * Top up. Wiring for the one button the sidebar is allowed to have.\n *\n * **It is also what makes that button exist.** Omit it and the chip draws no\n * top-up affordance at all, in every state, and that is the fix B33-1270 made.\n * The render used to be gated on `!unknown` alone, so a consumer that passed\n * no handler got a visible control that did nothing and could not be removed\n * -- `.topUp`'s class name is CSS-Modules-hashed, so there is no selector to\n * reach it from outside the package.\n *\n * That was not hypothetical in either consumer. The Wallet passes no handler\n * and ships the dead button today\n * (`boostledger/src/app/(wallet)/WalletShell.tsx`), and Score wrote a whole\n * client module to arm it across a Server Component boundary rather than live\n * with it (`boostscore/components/app-shell/wallet-chip-top-up.tsx`, which\n * says so at length).\n *\n * Nothing renders in the button's place. It carries no information of its own\n * -- only its own affordance -- so a consumer with nowhere to send someone is\n * better served by the chip without it than by a disabled one they did not\n * ask for (ADR-0010, \"Not entitled is not disabled\", on why unavailable is not\n * the same as disabled). B33-1047 is the other half: an `href` form of the\n * same affordance, for a consumer that cannot pass a callback at all.\n */\n onTopUp?: () => void\n /**\n * The chip's user-facing strings. Every field has an English default, so a\n * consumer supplies only the ones it translates; the first two consumers are\n * French-default. The package takes no i18n dependency -- it renders what it\n * is given (B33-849).\n *\n * Every field is a plain string, so the whole object crosses the React Server\n * Component boundary and a Next App Router page can build it. The four labels\n * that carry a value mark its position with a `%placeholder%` (B33-1277). None\n * of them takes a function: that form only ever worked from a Client Component\n * and nothing in the type said so (B33-880, B33-1278).\n */\n labels?: Partial<WalletChipLabels>\n /**\n * The BCP-47 language tag the chip's figures are formatted in. Defaults to\n * `en-US`, which is what the chip rendered before this prop existed.\n *\n * `labels` gives the consumer the chip's words; this gives it their numbers.\n * French writes `4 600` where English writes `4,600`, and a label cannot fix\n * it -- the label functions receive the figures already formatted, so before\n * this prop there was no hook at all (B33-876). Grouping and the decimal\n * separator are CLDR facts about a locale rather than a property of the\n * consumer's sentence, so a tag is enough: `Intl.NumberFormat` knows the rest,\n * and a tag is a plain string, which a Server Component can pass where a\n * formatter function could not.\n *\n * It reaches the visible figures and, because the same strings are\n * interpolated into them, the `bar` and `railBalance` accessible names.\n *\n * The `k` after the rail's abbreviated balance is a word, not a number, so it\n * is `labels.railFigure` and not this.\n */\n locale?: string\n}\n\n/**\n * The chip with a balance the consumer stands behind. The two remaining figures\n * are required, because this is the state that renders them.\n */\nexport interface WalletChipKnownProps extends WalletChipChromeProps {\n /**\n * Absent or `false`. The discriminant: it is what makes the figures below\n * required here and forbidden in {@link WalletChipUnknownProps}.\n */\n unknown?: false\n /** Absent or `false`: see {@link WalletChipTotalOnlyProps}. */\n totalOnly?: false\n /** Remaining granted tokens. Granted burns first and expires. */\n granted: number\n /** Remaining purchased tokens. Purchased never expires. */\n purchased: number\n /**\n * What this cycle's grant started with, if the consumer knows. The bar's full\n * width alongside purchased, so the part already burned draws as empty track\n * and the filled portion shortens as the month runs.\n *\n * **Optional, and that is the fix B33-483 made.** It used to be required, and\n * no consumer could satisfy it honestly: it is the Ledger's\n * `grant_tokens_per_cycle`, and it cannot be reconstructed from a balance --\n * `cycleUsedPct` counts purchased-bucket spend, is not `(cycle total -\n * remaining)` once a credit, refund or void has touched the account, and is not\n * bounded at 100. So the Wallet passed `grantCycle = granted`, which pins\n * `burned` at 0 and held the threshold state unreachable in the reference\n * implementation, and Score passed a local per-plan belief instead.\n *\n * Omitted, the bar measures against `granted + purchased` -- the balance\n * itself. It then draws full and claims nothing about what burned, which is the\n * honest rendering when nobody can say.\n *\n * It no longer decides the threshold on its own: {@link burnedFraction} does\n * that, and this is only consulted when that is absent.\n */\n grantCycle?: number\n /**\n * How much of the cycle's grant has burned, as a fraction -- `0.88` for 88%.\n * Drives the threshold state, and is the figure the chip rounds into its pill.\n *\n * Passed in rather than derived, for exactly the reason {@link burnNote}\n * already is: the burn is the product's measurement to make, not the chip's.\n * The chip kept the arithmetic while taking the runway line as a prop, and fed\n * it from a denominator no product could supply -- so the measurement was the\n * chip's in name and nobody's in fact (B33-483).\n *\n * The *policy* stays here. `THRESHOLD_BURN` is 0.8 in this package, so four\n * products trip at the same place and cannot drift to four different numbers.\n * That is why this is a fraction and not a `threshold?: boolean`: a boolean\n * would move the 80% decision into each consumer, and it would leave the pill\n * with no figure to print.\n *\n * Supplied, it wins. Absent, the chip derives it from {@link grantCycle} as it\n * always did; absent with no `grantCycle` either, nothing is claimed and the\n * chip rests.\n *\n * It is not clamped. A consumer that passes a percentage where a fraction\n * belongs renders a visibly absurd pill rather than a plausible \"100%\", and a\n * wrong figure that looks wrong is the one that gets fixed.\n */\n burnedFraction?: number\n /**\n * When the current grant expires, preformatted for display, e.g. \"30 Sep\":\n * the day this month's grant runs out and the next one lands. The sidebar\n * card says it as an expiry (`labels.grantExpiry`), the top-bar popover as a\n * reset (`labels.grantedNote`); both are this one date.\n */\n grantExpires?: string\n /**\n * The threshold state's runway line, preformatted, e.g. \"About four days left\n * at your current burn.\" Passed in rather than derived: the burn estimate is\n * the product's to make, not the chip's -- the chip renders what it is given.\n */\n burnNote?: string\n}\n\n/**\n * The chip whose balance could not be confirmed. It renders an em dash, no bar\n * and no button, so **it takes no figures at all** -- each one below is\n * `?: never`.\n *\n * That is deliberate and it is the second half of B33-483. The figures were\n * required in every state, so both consumers manufactured zeroes to get past the\n * type and then apologised for them in a comment -- \"these zeroes are never\n * rendered ... but they are not a balance and must not be read as one\"\n * (`boostscore/components/app-shell/wallet-chip.tsx`), and the same again in\n * `wallet-slot.tsx` and `boostledger/src/app/(wallet)/layout.tsx`. A zero is a\n * confident claim about someone's money. The type now refuses it rather than\n * asking a comment to.\n *\n * `undefined` is still accepted, so a consumer that must build one props object\n * can write `{ unknown: true, granted: undefined }` instead of branching.\n */\nexport interface WalletChipUnknownProps extends WalletChipChromeProps {\n /** The balance could not be confirmed. Renders as unknown, never as zero. */\n unknown: true\n totalOnly?: false\n balance?: never\n granted?: never\n purchased?: never\n grantCycle?: never\n burnedFraction?: never\n /** Not rendered in this state: the footer only draws on a resting chip. */\n grantExpires?: never\n /** Not rendered in this state: the runway line only draws past the threshold. */\n burnNote?: never\n}\n\n/**\n * A discriminated union on `unknown`, and the discriminant has consequences for\n * how a consumer *types* its own code, not only what it passes:\n *\n * - A helper that returns props for both states needs an explicit\n * `: WalletChipProps` return annotation. Without one, TypeScript widens the\n * `unknown: true` in an object literal to `boolean`, which matches neither\n * member, and the spread at the call site stops compiling.\n * - `Omit<WalletChipProps, 'onTopUp'>` is not what it looks like. `Omit` over a\n * union flattens it into one object and loses the discrimination. Omit from\n * the members instead -- `Omit<WalletChipKnownProps, 'onTopUp'> |\n * Omit<WalletChipUnknownProps, 'onTopUp'>` -- which is why both are exported.\n */\nexport type WalletChipProps = WalletChipKnownProps | WalletChipUnknownProps | WalletChipTotalOnlyProps\n\n/**\n * The top-bar chip with a real total and no drawable split (B33-1712). Debits\n * draw the grant first, then purchased, and may run a set share past zero (the\n * Ledger's overdraft); credits add only to the bucket they name and never pay a\n * negative one off. So the Ledger can report a bucket below zero -- granted 0 /\n * purchased -40 (overdrawn, total -40), or granted 1000 / purchased -40 after\n * the month's grant lands (total 960, for the rest of the month). A two-segment\n * bar cannot draw a negative, and clamping it to zero would draw a split nobody\n * has: a split invented by the interface. So this state takes **the total and\n * no split**, and draws no bar and no granted / purchased legend.\n *\n * Below zero the figure is the distance past zero, positive, in --color-danger,\n * with the words \"past zero\" where a minus sign would be -- the Wallet's own\n * past-zero screen (boostledger, Paper 201 screen 03), followed rather than\n * inventing a third look. Nothing is broken and the copy does not say it is;\n * what happens next is the consumer's `burnNote`, because the overdraft limit is\n * the Ledger's. At zero or above it is the plain balance.\n *\n * **Top bar only.** The sidebar card has no drawing for this and the redesigned\n * shell does not render it, so the type requires `placement=\"topbar\"` rather\n * than leaving the card to guess. No board draws either form: they are new\n * undrawn states for the person at the gate to judge.\n */\nexport interface WalletChipTotalOnlyProps extends WalletChipChromeProps {\n placement: 'topbar'\n totalOnly: true\n unknown?: false\n /**\n * The balance, signed, in whole tokens: `granted + purchased` from the\n * Ledger's signed figures, which its single balance row guarantees is the\n * balance.\n */\n balance: number\n /**\n * What happens next, preformatted, e.g. \"Work carries on for another 930\n * tokens, then paid actions stop.\" The consumer's, because the overdraft limit\n * is the Ledger's, not the chip's.\n */\n burnNote?: string\n granted?: never\n purchased?: never\n grantCycle?: never\n burnedFraction?: never\n grantExpires?: never\n}\n\n/**\n * The granted/purchased bar. Granted is drawn first, because granted burns\n * first. The accessible label is passed in whole rather than assembled here:\n * word order is the consumer's, not English's (B33-849).\n */\nfunction Bar({ granted, purchased, cycleTotal, tone, label }: { granted: number; purchased: number; cycleTotal: number; tone: 'rest' | 'threshold'; label: string }) {\n return (\n <div className={styles.bar} data-tone={tone} role=\"img\" aria-label={label}>\n <span className={styles.segmentGranted} data-testid=\"segment-granted\" style={{ width: segmentWidth(granted, cycleTotal) }} />\n <span className={styles.segmentPurchased} data-testid=\"segment-purchased\" style={{ width: segmentWidth(purchased, cycleTotal) }} />\n </div>\n )\n}\n\nexport function WalletChip(props: WalletChipProps) {\n if (props.totalOnly === true) return <TotalOnlyChip {...props} />\n if (props.placement === 'topbar') return <TopBarChip {...props} />\n return <SidebarChip {...props} />\n}\n\n// The clear space the token popover keeps from the viewport's edges: the\n// 16px side gutter every Boost33 page keeps at phone width (--spacing-4). A\n// number because it is compared with getBoundingClientRect(), which reads px.\nconst POPOVER_GUTTER_PX = 16\n\n/**\n * Keeps the open token popover on screen (0.8.0-next.4). It hangs from the\n * pill's left edge and is 340px wide, so wherever the pill sits less than 340px\n * from the right of the viewport -- 1024px (8px over) and every phone width\n * (the right edge at 535px of 390) -- it ran off the edge and cut the\n * threshold line and the reset note. Measured after layout and before paint,\n * it slides left by exactly the overshoot, never past the left gutter; the\n * stylesheet caps its width at the viewport less both gutters. Re-measured on\n * resize while open. A layout effect, so the first frame is already in place.\n */\nfunction useClampToViewport(rootRef: RefObject<HTMLDivElement | null>, open: boolean) {\n useLayoutEffect(() => {\n if (!open) return\n const root = rootRef.current\n const panel = root?.querySelector<HTMLElement>('[data-token-popover]')\n if (!root || !panel) return\n const place = () => {\n panel.style.left = ''\n const rootLeft = root.getBoundingClientRect().left\n const rect = panel.getBoundingClientRect()\n const overshoot = rect.right - (document.documentElement.clientWidth - POPOVER_GUTTER_PX)\n if (overshoot <= 0) return\n const left = Math.max(rect.left - overshoot, POPOVER_GUTTER_PX)\n panel.style.left = `${left - rootLeft}px`\n }\n place()\n window.addEventListener('resize', place)\n return () => window.removeEventListener('resize', place)\n }, [rootRef, open])\n}\n\n/**\n * The top-bar token chip (B33-1712). The pill shows the balance and the\n * two-segment bar; the popover (board 21) shows the figure, the bar again at\n * width, each kind of token with what it means, the runway line, and the buy\n * button.\n *\n * Both segments are always drawn and a zero one is 0px wide with the gap kept\n * -- board 22, and B33-590's rule that a zero is never drawn as a sliver. That\n * overrides B33-1712's line about copying the hub's 4px floor, by Jamal's\n * decision of 3 Nov 2026. A non-zero segment keeps the existing floor\n * (`segmentWidth`).\n *\n * Every figure is the consumer's. Unknown renders an em dash, no bar and no\n * breakdown -- a made-up split in a token balance is a commercial fact invented\n * by the interface (B33-1712).\n *\n * A disclosure, like the switcher (ADR-0010): the trigger is a button, the\n * panel carries no widget role, and dismissal is the shared anchored-overlay\n * contract's (ADR-0009).\n */\nfunction TopBarChip({ granted = 0, purchased = 0, grantCycle, burnedFraction, grantExpires, burnNote, unknown = false, onTopUp, labels, locale = DEFAULT_WALLET_LOCALE, walletHref, linkComponent, defaultOpen }: WalletChipProps) {\n const t = resolveWalletChipLabels(labels)\n const overlay = useAnchoredOverlay({ defaultOpen })\n const rootRef = useRef<HTMLDivElement>(null)\n useClampToViewport(rootRef, overlay.open)\n const Link: ElementType = linkComponent ?? 'a'\n const balance = granted + purchased\n // The threshold, decided exactly as the sidebar card decides it: the\n // consumer's `burnedFraction` wins, `grantCycle` derives it otherwise, and\n // with neither nothing is claimed (B33-483). 0.8.0-next.0 dropped this from\n // the top bar and with it the only at-a-glance \"running low\" signal the\n // chip had (score-main, 4 Nov 2026); Jamal chose the sidebar's own drawing\n // to bring it back.\n const derivedBurn = grantCycle !== undefined && grantCycle > 0 ? (grantCycle - granted) / grantCycle : 0\n const burned = burnedFraction ?? derivedBurn\n const isThreshold = !unknown && burned >= THRESHOLD_BURN\n // At rest the bar draws the split of the balance itself, full width, with no\n // burned track (board 22): the top bar claims nothing about what the cycle\n // started with. Past the threshold, and only when `grantCycle` says what the\n // cycle started with, it measures against that instead -- the sidebar card's\n // drawing -- so the bar shortens and the burned share shows as the\n // warning-tinted track. With `burnedFraction` alone there is no honest\n // denominator, so the bar stays full and the popover line carries the warning.\n const total = isThreshold && grantCycle !== undefined ? grantCycle + purchased : balance\n const tone = isThreshold ? 'threshold' : undefined\n const figure = unknown ? '—' : formatTokens(balance, locale)\n const buildBarLabel = () => t.bar(formatTokens(granted, locale), formatTokens(purchased, locale), formatTokens(balance, locale))\n // Built only past the threshold, where it is drawn.\n const thresholdNote = isThreshold ? t.thresholdNote(formatPercent(burned, locale)) : undefined\n const triggerLabel = unknown ? t.railBalanceUnknown : thresholdNote ? `${t.railBalance(figure)}. ${thresholdNote}` : t.railBalance(figure)\n\n return (\n <div ref={rootRef} className={styles.topRoot} data-threshold={isThreshold ? 'true' : undefined}>\n <button type=\"button\" className={styles.topTrigger} aria-label={triggerLabel} onClick={overlay.toggle} {...overlay.triggerProps}>\n <span className={styles.topFigure} aria-hidden=\"true\">\n <span className={styles.topAmount}>{figure}</span>\n <span className={styles.topUnit}>{t.unit}</span>\n </span>\n {!unknown && <TopBar granted={granted} purchased={purchased} total={total} tone={tone} className={styles.topBar} />}\n <Chevron className={styles.topChevron} />\n </button>\n\n {overlay.open && (\n <div {...overlay.panelProps} className={styles.popover} data-token-popover=\"\" aria-label={t.popover}>\n <div className={styles.popHead}>\n <span className={styles.popFigure}>\n <span className={styles.popAmount}>{figure}</span>\n <span className={styles.popUnit}>{t.unit}</span>\n </span>\n {walletHref !== undefined && (\n <Link className={buttons.quiet} href={walletHref} onClick={overlay.close}>\n {t.open}\n </Link>\n )}\n </div>\n\n {unknown ? (\n <p className={styles.popNote}>{t.balanceUnknown}</p>\n ) : (\n <>\n <TopBar granted={granted} purchased={purchased} total={total} tone={tone} className={styles.popBar} label={buildBarLabel()} />\n <ul className={styles.legend}>\n <li className={styles.legendRow}>\n <span className={styles.swatchGranted} aria-hidden=\"true\" />\n <span className={styles.legendText}>\n <span className={styles.legendFigure}>{t.grantedFigure(formatTokens(granted, locale))}</span>\n <span className={styles.legendNote}>{grantExpires ? t.grantedNote(grantExpires) : t.grantedNoteUndated}</span>\n </span>\n </li>\n <li className={styles.legendRow}>\n <span className={styles.swatchPurchased} aria-hidden=\"true\" />\n <span className={styles.legendText}>\n <span className={styles.legendFigure}>{t.purchasedFigure(formatTokens(purchased, locale))}</span>\n <span className={styles.legendNote}>{t.purchasedNote}</span>\n </span>\n </li>\n </ul>\n {(thresholdNote || burnNote || onTopUp !== undefined) && (\n <div className={styles.popFoot}>\n {thresholdNote && <p className={styles.popWarning}>{thresholdNote}</p>}\n {burnNote && <p className={styles.popNote}>{burnNote}</p>}\n {onTopUp !== undefined && (\n <button type=\"button\" className={`${buttons.primary} ${styles.buy}`} data-tone=\"accent\" onClick={onTopUp}>\n {t.buyTokens}\n </button>\n )}\n </div>\n )}\n </>\n )}\n </div>\n )}\n </div>\n )\n}\n\n/**\n * The top-bar chip with no drawable split (`WalletChipTotalOnlyProps`): the same\n * pill and popover as {@link TopBarChip}, with the total alone -- the distance\n * past zero when it is below -- and no bar and no legend anywhere.\n */\nfunction TotalOnlyChip({ balance, burnNote, onTopUp, labels, locale = DEFAULT_WALLET_LOCALE, walletHref, linkComponent, defaultOpen }: WalletChipTotalOnlyProps) {\n const t = resolveWalletChipLabels(labels)\n const overlay = useAnchoredOverlay({ defaultOpen })\n const rootRef = useRef<HTMLDivElement>(null)\n useClampToViewport(rootRef, overlay.open)\n const Link: ElementType = linkComponent ?? 'a'\n const pastZero = balance < 0\n const figure = formatTokens(pastZero ? -balance : balance, locale)\n\n return (\n <div ref={rootRef} className={styles.topRoot} data-past-zero={pastZero ? 'true' : undefined}>\n <button type=\"button\" className={styles.topTrigger} aria-label={pastZero ? t.pastZeroBalance(figure) : t.railBalance(figure)} onClick={overlay.toggle} {...overlay.triggerProps}>\n <span className={styles.topFigure} aria-hidden=\"true\">\n <span className={styles.topAmount}>{figure}</span>\n <span className={styles.topUnit}>{pastZero ? t.pastZero : t.unit}</span>\n </span>\n <Chevron className={styles.topChevron} />\n </button>\n\n {overlay.open && (\n <div {...overlay.panelProps} className={styles.popover} data-token-popover=\"\" aria-label={t.popover}>\n <div className={styles.popHead}>\n <span className={styles.popFigure}>\n <span className={styles.popAmount}>{figure}</span>\n <span className={styles.popUnit}>{pastZero ? t.pastZeroLong : t.unit}</span>\n </span>\n {walletHref !== undefined && (\n <Link className={buttons.quiet} href={walletHref} onClick={overlay.close}>\n {t.open}\n </Link>\n )}\n </div>\n {(burnNote || onTopUp !== undefined) && (\n <div className={styles.popFoot}>\n {burnNote && <p className={styles.popNote}>{burnNote}</p>}\n {onTopUp !== undefined && (\n <button type=\"button\" className={`${buttons.primary} ${styles.buy}`} data-tone=\"accent\" onClick={onTopUp}>\n {t.buyTokens}\n </button>\n )}\n </div>\n )}\n </div>\n )}\n </div>\n )\n}\n\n/**\n * The top bar's two-segment bar, in the pill and in the popover. Granted first,\n * because it burns first; a fixed gap between the two that stays when a segment\n * is zero, so a two-part balance never reads as one lump (board 22).\n */\nfunction TopBar({ granted, purchased, total, tone, className, label }: { granted: number; purchased: number; total: number; tone?: 'threshold'; className?: string; label?: string }) {\n return (\n <span className={className} data-tone={tone} role={label ? 'img' : undefined} aria-label={label} aria-hidden={label ? undefined : 'true'}>\n <span className={styles.segmentGranted} data-segment=\"granted\" style={{ width: segmentWidth(granted, total) }} />\n <span className={styles.segmentPurchased} data-segment=\"purchased\" style={{ width: segmentWidth(purchased, total) }} />\n </span>\n )\n}\n\nfunction SidebarChip({ granted = 0, purchased = 0, grantCycle, burnedFraction, grantExpires, burnNote, unknown = false, collapsed = false, onTopUp, labels, locale = DEFAULT_WALLET_LOCALE }: WalletChipProps) {\n const t = resolveWalletChipLabels(labels)\n // Assembled lazily: the unknown state draws no bar, and a consumer's `bar`\n // implementation can throw on the zeroes the destructure above defaults the\n // degraded path's absent figures to. Calling it only where a bar renders keeps\n // that crash surface off the render that exists precisely because fetching the\n // balance already failed (B33-875). B33-483 stopped the consumer having to pass\n // those zeroes; the chip still has to pick something, so the laziness stays.\n const buildBarLabel = () =>\n t.bar(formatTokens(granted, locale), formatTokens(purchased, locale), formatTokens(granted + purchased, locale))\n const balance = granted + purchased\n // Everything the cycle started with: the grant plus the purchased balance\n // sitting alongside it. Measuring against this is what makes the bar shorten\n // as tokens burn instead of only re-proportioning to a full width.\n //\n // With no `grantCycle` the denominator falls back to `granted + purchased`, so\n // the bar fills and there is no burned track -- the honest drawing when nobody\n // can say what the cycle started with (B33-483). The zero-division guard inside\n // segmentWidth still covers the case where that balance is itself zero.\n const cycleTotal = (grantCycle ?? granted) + purchased\n // How much of the cycle's grant has burned. The consumer's own figure wins;\n // `grantCycle` derives it exactly as it always did; with neither, nothing is\n // claimed and the chip rests. `grantCycle` is tested against undefined rather\n // than leaned on for falsiness, because 0 and \"not supplied\" both resolve to a\n // burn of 0 and must keep doing so.\n const derivedBurn = grantCycle !== undefined && grantCycle > 0 ? (grantCycle - granted) / grantCycle : 0\n const burned = burnedFraction ?? derivedBurn\n const isThreshold = !unknown && burned >= THRESHOLD_BURN\n\n if (collapsed) {\n // The abbreviated balance, assembled once: it is both the rail's visible\n // text and the figure the group's accessible name is built around, and a\n // sighted user and a screen-reader user must not be told different numbers.\n // Built only when a balance is known, for the same reason buildBarLabel is\n // lazy -- the unknown rail shows no figure, and a consumer's railFigure\n // should not run on a degraded path it never renders on (B33-875).\n const railBalance = unknown ? undefined : t.railFigure(thousandsFigure(balance, locale))\n return (\n <div className={styles.rail} role=\"group\" aria-label={railBalance === undefined ? t.railBalanceUnknown : t.railBalance(railBalance)} tabIndex={0}>\n <WalletMark className={styles.railIcon} />\n <span className={styles.railFigure}>{railBalance ?? '—'}</span>\n {unknown ? <span className={styles.srOnly}>{t.railUnknown}</span> : <Bar granted={granted} purchased={purchased} cycleTotal={cycleTotal} tone={isThreshold ? 'threshold' : 'rest'} label={buildBarLabel()} />}\n </div>\n )\n }\n\n return (\n <div className={styles.chip} role=\"group\" aria-label={t.region} tabIndex={0} data-threshold={isThreshold ? 'true' : undefined} data-unknown={unknown ? 'true' : undefined}>\n <div className={styles.header}>\n <span className={styles.name}>{t.name}</span>\n {isThreshold && <span className={styles.pill}>{Math.round(burned * 100)}%</span>}\n </div>\n\n {unknown ? (\n <div className={styles.figure}>\n <span className={styles.amount} aria-hidden=\"true\">\n {'—'}\n </span>\n <RetryMark className={styles.retry} />\n <span className={styles.srOnly}>{t.balanceUnknown}</span>\n </div>\n ) : (\n <div className={styles.figure}>\n <span className={styles.amount}>{formatTokens(balance, locale)}</span>\n <span className={styles.unit}>{t.unit}</span>\n </div>\n )}\n\n {unknown ? (\n <div className={styles.bar} data-tone=\"unknown\" aria-hidden=\"true\" />\n ) : (\n <Bar granted={granted} purchased={purchased} cycleTotal={cycleTotal} tone={isThreshold ? 'threshold' : 'rest'} label={buildBarLabel()} />\n )}\n\n {!unknown && !isThreshold && (\n <div className={styles.details}>\n <dl className={styles.split}>\n <div className={styles.splitRow}>\n <dt className={styles.splitLabel}>\n <span className={styles.dotGranted} aria-hidden=\"true\" />\n {t.granted}\n </dt>\n <dd className={styles.splitAmount}>{formatTokens(granted, locale)}</dd>\n </div>\n <div className={styles.splitRow}>\n <dt className={styles.splitLabel}>\n <span className={styles.dotPurchased} aria-hidden=\"true\" />\n {t.purchased}\n </dt>\n <dd className={styles.splitAmount}>{formatTokens(purchased, locale)}</dd>\n </div>\n </dl>\n {grantExpires && <p className={styles.footer}>{t.grantExpiry(grantExpires)}</p>}\n </div>\n )}\n\n {isThreshold && burnNote && <p className={styles.burnNote}>{burnNote}</p>}\n\n {/* A handler is what makes the button exist, not only what it does when\n pressed (B33-1270). `!unknown` alone drew it for a consumer with\n nowhere to send anyone, and a hashed class name left them no way to\n take it back out -- see `onTopUp`. Tested against `undefined` rather\n than for truthiness so that `onTopUp={undefined}`, which is what a\n consumer's own absent value looks like, reads the same as omitting it. */}\n {!unknown && onTopUp !== undefined && (\n <button\n type=\"button\"\n // Two tiers for one button (B33-1713, ADR-0016): at rest it is a word\n // in the chip's header, the quiet tier; past the threshold it grows\n // into the chip's one call to action, the primary tier in the accent\n // tone. `.topUp` adds only where it sits.\n className={`${isThreshold ? buttons.primary : buttons.quiet} ${styles.topUp}`}\n data-tone={isThreshold ? 'accent' : undefined}\n data-grown={isThreshold ? 'true' : undefined}\n onClick={onTopUp}\n >\n {t.topUp}\n </button>\n )}\n </div>\n )\n}\n"],"names":["THRESHOLD_BURN","SEGMENT_MIN","segmentWidth","amount","cycleTotal","share","Bar","granted","purchased","tone","label","jsxs","styles","jsx","WalletChip","props","TotalOnlyChip","TopBarChip","SidebarChip","POPOVER_GUTTER_PX","useClampToViewport","rootRef","open","useLayoutEffect","root","panel","place","rootLeft","rect","overshoot","left","grantCycle","burnedFraction","grantExpires","burnNote","unknown","onTopUp","labels","locale","DEFAULT_WALLET_LOCALE","walletHref","linkComponent","defaultOpen","t","resolveWalletChipLabels","overlay","useAnchoredOverlay","useRef","Link","balance","derivedBurn","burned","isThreshold","total","figure","formatTokens","buildBarLabel","thresholdNote","formatPercent","triggerLabel","TopBar","Chevron","buttons","Fragment","pastZero","className","collapsed","railBalance","thousandsFigure","WalletMark","RetryMark"],"mappings":"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"}
@@ -15,8 +15,8 @@ export declare const DEFAULT_WALLET_LOCALE = "en-US";
15
15
  export declare function formatTokens(value: number, locale?: string): string;
16
16
  /**
17
17
  * A burned share as a whole percentage in the locale's own form: 0.87 ->
18
- * `"87%"` in `en-US`, `"87 %"` in `fr-FR` (with the narrow no-break space CLDR
19
- * puts there). Used by the top-bar chip's threshold line.
18
+ * `"87%"` in `en-US`, `"87 %"` in `fr-FR` (with a no-break space; see
19
+ * `visibleSpaces`). Used by the top-bar chip's threshold line.
20
20
  */
21
21
  export declare function formatPercent(fraction: number, locale?: string): string;
22
22
  /**
@@ -1,34 +1,37 @@
1
- const r = "en-US", m = {
1
+ const e = "en-US", m = {
2
2
  grouped: {},
3
3
  oneDecimal: { minimumFractionDigits: 1, maximumFractionDigits: 1 },
4
4
  percent: { style: "percent", maximumFractionDigits: 0 }
5
- }, c = /* @__PURE__ */ new Map(), s = /* @__PURE__ */ new Set();
5
+ }, s = /* @__PURE__ */ new Map(), u = /* @__PURE__ */ new Set();
6
6
  function o(t, n) {
7
- const a = `${n}|${t}`, i = c.get(a);
8
- if (i) return i;
9
- let e;
7
+ const i = `${n}|${t}`, c = s.get(i);
8
+ if (c) return c;
9
+ let r;
10
10
  try {
11
- e = new Intl.NumberFormat(t, m[n]);
11
+ r = new Intl.NumberFormat(t, m[n]);
12
12
  } catch {
13
- s.has(t) || (s.add(t), console.warn(
14
- `@boost33/ui: WalletChip cannot format numbers in locale ${JSON.stringify(t)}; falling back to ${r}.`
15
- )), e = new Intl.NumberFormat(r, m[n]);
13
+ u.has(t) || (u.add(t), console.warn(
14
+ `@boost33/ui: WalletChip cannot format numbers in locale ${JSON.stringify(t)}; falling back to ${e}.`
15
+ )), r = new Intl.NumberFormat(e, m[n]);
16
16
  }
17
- return c.set(a, e), e;
17
+ return s.set(i, r), r;
18
18
  }
19
- function u(t, n = r) {
20
- return o(n, "grouped").format(t);
19
+ function f(t, n = e) {
20
+ return a(o(n, "grouped").format(t));
21
21
  }
22
- function f(t, n = r) {
23
- return o(n, "percent").format(t);
22
+ function a(t) {
23
+ return t.replace(/\u202f/g, " ");
24
24
  }
25
- function g(t, n = r) {
26
- return o(n, "oneDecimal").format(t / 1e3);
25
+ function g(t, n = e) {
26
+ return a(o(n, "percent").format(t));
27
+ }
28
+ function p(t, n = e) {
29
+ return a(o(n, "oneDecimal").format(t / 1e3));
27
30
  }
28
31
  export {
29
- r as DEFAULT_WALLET_LOCALE,
30
- f as formatPercent,
31
- u as formatTokens,
32
- g as thousandsFigure
32
+ e as DEFAULT_WALLET_LOCALE,
33
+ g as formatPercent,
34
+ f as formatTokens,
35
+ p as thousandsFigure
33
36
  };
34
37
  //# sourceMappingURL=format.js.map