@boost33/ui 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,8 +3,9 @@ import { AppSwitcherLabels } from './labels';
3
3
  /**
4
4
  * One product the switcher can jump to. It is Identity's registry row, supplied
5
5
  * by the consuming app (ADR-0003) -- the switcher fetches nothing and hardcodes
6
- * no list. Every product is rendered, entitled or not: hiding an unowned one
7
- * means nobody learns the family exists (B33-479).
6
+ * no list. Every product is rendered, entitled or not and built or not: hiding
7
+ * an unowned one means nobody learns the family exists (B33-479), and hiding an
8
+ * unbuilt one means nobody learns it is coming.
8
9
  */
9
10
  export interface AppSwitcherProduct {
10
11
  /** Identity's product slug, which selects the ProductMark artwork. */
@@ -20,6 +21,29 @@ export interface AppSwitcherProduct {
20
21
  * deciding what someone may open (ADR-0010, "Not entitled is not disabled").
21
22
  */
22
23
  entitled?: boolean;
24
+ /**
25
+ * Whether the product exists yet. Defaults to `true`. `false` dims the mark and
26
+ * shows the "Soon" tag in place of "Not on plan".
27
+ *
28
+ * **A different question from `entitled`, which is why it is a second field
29
+ * and not a third value of the first.** `live` is global: the same answer for
30
+ * every account, because either the product has shipped or it has not.
31
+ * `entitled` is per account, read from the grant. Identity serves both for
32
+ * exactly this reason (B33-561, and the `live` note in
33
+ * `boost33-identity/src/lib/productRegistry.ts`).
34
+ *
35
+ * Until 0.4 this interface had nowhere to put it, so both consumers read the
36
+ * field off the registry and dropped it on the floor -- each with a comment
37
+ * saying so and calling it worth a package ticket. The cost was a row that
38
+ * told a client they had not bought Boost Voice, when nobody has: the row read
39
+ * "Not on plan" and pointed at a product with no code behind it.
40
+ *
41
+ * It stays a rendering hint like `entitled`. An unbuilt product's row is still
42
+ * an ordinary focusable link -- the switcher does not decide what anyone may
43
+ * open (ADR-0010), and `url` is served for every slug whether or not anything
44
+ * answers on it, because `?app=` has to resolve for a campaign link.
45
+ */
46
+ live?: boolean;
23
47
  }
24
48
  /** The "all products" row above the divider. Its blurb and URL are the consumer's. */
25
49
  export interface AppSwitcherHub {
@@ -46,7 +70,10 @@ export interface AppSwitcherAccount {
46
70
  onSelect?: () => void;
47
71
  }
48
72
  export interface AppSwitcherProps {
49
- /** The products, in the order Identity serves them. Rendered whole, entitled or not. */
73
+ /**
74
+ * The products, in the order Identity serves them. Rendered whole -- entitled
75
+ * or not, built or not.
76
+ */
50
77
  products: AppSwitcherProduct[];
51
78
  /**
52
79
  * The slug of the product this shell is rendering for. It names the trigger
@@ -1,81 +1,82 @@
1
1
  "use client";
2
- import { jsxs as s, jsx as a, Fragment as b } from "react/jsx-runtime";
3
- import { useAnchoredOverlay as C } from "../../anchored-overlay.js";
4
- import { ProductMark as u } from "../ProductMark/ProductMark.js";
5
- import { Chevron as x, HubMark as y, Check as P, ChevronRight as R } from "./icons.js";
6
- import { resolveAppSwitcherLabels as A } from "./labels.js";
2
+ import { jsxs as r, jsx as a, Fragment as x } from "react/jsx-runtime";
3
+ import { useAnchoredOverlay as y } from "../../anchored-overlay.js";
4
+ import { ProductMark as f } from "../ProductMark/ProductMark.js";
5
+ import { Chevron as P, HubMark as R, Check as A, ChevronRight as L } from "./icons.js";
6
+ import { resolveAppSwitcherLabels as M } from "./labels.js";
7
7
  import e from "./AppSwitcher.module.css.js";
8
- function E({
9
- products: N,
10
- currentSlug: i,
11
- hub: t,
12
- account: l,
13
- labels: v,
14
- linkComponent: f,
8
+ function H({
9
+ products: p,
10
+ currentSlug: t,
11
+ hub: o,
12
+ account: n,
13
+ labels: w,
14
+ linkComponent: k,
15
15
  collapsed: c = !1,
16
- defaultOpen: w,
17
- onOpenChange: k
16
+ defaultOpen: b,
17
+ onOpenChange: C
18
18
  }) {
19
- const n = C({ defaultOpen: w, onOpenChange: k }), o = A(v), p = f ?? "a", m = N.find((r) => r.slug === i), g = (m == null ? void 0 : m.name) ?? o.switchProduct;
20
- return /* @__PURE__ */ s("div", { className: e.root, children: [
21
- /* @__PURE__ */ s(
19
+ const l = y({ defaultOpen: b, onOpenChange: C }), i = M(w), g = k ?? "a", m = p.find((s) => s.slug === t), v = (m == null ? void 0 : m.name) ?? i.switchProduct;
20
+ return /* @__PURE__ */ r("div", { className: e.root, children: [
21
+ /* @__PURE__ */ r(
22
22
  "button",
23
23
  {
24
24
  type: "button",
25
25
  className: c ? e.railTrigger : e.trigger,
26
- onClick: n.toggle,
27
- ...n.triggerProps,
26
+ onClick: l.toggle,
27
+ ...l.triggerProps,
28
28
  children: [
29
- /* @__PURE__ */ a(u, { slug: i, title: c ? g : void 0 }),
30
- !c && /* @__PURE__ */ s(b, { children: [
31
- /* @__PURE__ */ a("span", { className: e.triggerName, children: g }),
32
- /* @__PURE__ */ a(x, { className: e.chevron })
29
+ /* @__PURE__ */ a(f, { slug: t, title: c ? v : void 0 }),
30
+ !c && /* @__PURE__ */ r(x, { children: [
31
+ /* @__PURE__ */ a("span", { className: e.triggerName, children: v }),
32
+ /* @__PURE__ */ a(P, { className: e.chevron })
33
33
  ] })
34
34
  ]
35
35
  }
36
36
  ),
37
- n.open && /* @__PURE__ */ s("div", { ...n.panelProps, className: e.panel, "aria-label": o.panel, children: [
38
- /* @__PURE__ */ s(p, { className: e.hubRow, href: t.href, onClick: n.close, children: [
39
- /* @__PURE__ */ a(y, { className: e.hubMark }),
40
- /* @__PURE__ */ s("span", { className: e.rowText, children: [
41
- /* @__PURE__ */ a("span", { className: e.rowName, children: t.name }),
42
- /* @__PURE__ */ a("span", { className: e.rowSub, children: t.description })
37
+ l.open && /* @__PURE__ */ r("div", { ...l.panelProps, className: e.panel, "aria-label": i.panel, children: [
38
+ /* @__PURE__ */ r(g, { className: e.hubRow, href: o.href, onClick: l.close, children: [
39
+ /* @__PURE__ */ a(R, { className: e.hubMark }),
40
+ /* @__PURE__ */ r("span", { className: e.rowText, children: [
41
+ /* @__PURE__ */ a("span", { className: e.rowName, children: o.name }),
42
+ /* @__PURE__ */ a("span", { className: e.rowSub, children: o.description })
43
43
  ] })
44
44
  ] }),
45
45
  /* @__PURE__ */ a("hr", { className: e.divider }),
46
- /* @__PURE__ */ a("ul", { className: e.products, children: N.map((r) => {
47
- const h = r.slug === i, d = r.entitled === !1;
48
- return /* @__PURE__ */ a("li", { children: /* @__PURE__ */ s(
49
- p,
46
+ /* @__PURE__ */ a("ul", { className: e.products, children: p.map((s) => {
47
+ const d = s.slug === t, h = s.entitled === !1, N = s.live === !1, u = N ? i.soon : h ? i.notOnPlan : null;
48
+ return /* @__PURE__ */ a("li", { children: /* @__PURE__ */ r(
49
+ g,
50
50
  {
51
51
  className: e.productRow,
52
- href: r.href,
53
- "aria-current": h ? "true" : void 0,
54
- "data-current": h ? "true" : void 0,
55
- "data-entitled": d ? "false" : void 0,
56
- onClick: n.close,
52
+ href: s.href,
53
+ "aria-current": d ? "true" : void 0,
54
+ "data-current": d ? "true" : void 0,
55
+ "data-entitled": h ? "false" : void 0,
56
+ "data-live": N ? "false" : void 0,
57
+ onClick: l.close,
57
58
  children: [
58
- /* @__PURE__ */ a(u, { slug: r.slug, muted: d }),
59
- /* @__PURE__ */ a("span", { className: e.rowName, children: r.name }),
60
- d && /* @__PURE__ */ a("span", { className: e.tag, children: o.notOnPlan }),
61
- h && /* @__PURE__ */ a(P, { className: e.check })
59
+ /* @__PURE__ */ a(f, { slug: s.slug, muted: h || N }),
60
+ /* @__PURE__ */ a("span", { className: e.rowName, children: s.name }),
61
+ u !== null && /* @__PURE__ */ a("span", { className: e.tag, children: u }),
62
+ d && /* @__PURE__ */ a(A, { className: e.check })
62
63
  ]
63
64
  }
64
- ) }, r.slug);
65
+ ) }, s.slug);
65
66
  }) }),
66
67
  /* @__PURE__ */ a("hr", { className: e.divider }),
67
- /* @__PURE__ */ s("button", { type: "button", className: e.accountRow, onClick: l.onSelect, children: [
68
- /* @__PURE__ */ a("span", { className: e.avatar, "aria-hidden": "true", children: l.initials }),
69
- /* @__PURE__ */ s("span", { className: e.rowText, children: [
70
- /* @__PURE__ */ a("span", { className: e.rowName, children: l.name }),
71
- /* @__PURE__ */ a("span", { className: e.rowSub, children: l.description })
68
+ /* @__PURE__ */ r("button", { type: "button", className: e.accountRow, onClick: n.onSelect, children: [
69
+ /* @__PURE__ */ a("span", { className: e.avatar, "aria-hidden": "true", children: n.initials }),
70
+ /* @__PURE__ */ r("span", { className: e.rowText, children: [
71
+ /* @__PURE__ */ a("span", { className: e.rowName, children: n.name }),
72
+ /* @__PURE__ */ a("span", { className: e.rowSub, children: n.description })
72
73
  ] }),
73
- /* @__PURE__ */ a(R, { className: e.chevronRight })
74
+ /* @__PURE__ */ a(L, { className: e.chevronRight })
74
75
  ] })
75
76
  ] })
76
77
  ] });
77
78
  }
78
79
  export {
79
- E as AppSwitcher
80
+ H as AppSwitcher
80
81
  };
81
82
  //# sourceMappingURL=AppSwitcher.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"AppSwitcher.js","sources":["../../../src/components/AppSwitcher/AppSwitcher.tsx"],"sourcesContent":["'use client'\n\n// A client module: the switcher holds open/closed state and wires clicks, focus\n// and Escape through useAnchoredOverlay, none of which a Server Component can\n// carry. The rest of the shell stays on the server -- see WalletChip, Sidebar\n// and B33-846. AppShell renders this as a slot (`switcher`), so it imports no\n// client code by taking it.\n\nimport type { ElementType } from 'react'\nimport { useAnchoredOverlay } from '../../anchored-overlay'\nimport { ProductMark } from '../ProductMark/ProductMark'\nimport { Check, Chevron, ChevronRight, HubMark } from './icons'\nimport { resolveAppSwitcherLabels, type AppSwitcherLabels } from './labels'\nimport styles from './AppSwitcher.module.css'\n\n/**\n * One product the switcher can jump to. It is Identity's registry row, supplied\n * by the consuming app (ADR-0003) -- the switcher fetches nothing and hardcodes\n * no list. Every product is rendered, entitled or not: hiding an unowned one\n * means nobody learns the family exists (B33-479).\n */\nexport interface AppSwitcherProduct {\n /** Identity's product slug, which selects the ProductMark artwork. */\n slug: string\n /** The product's display name, e.g. \"Boost Score\". Identity's, not the slug. */\n name: string\n /** Where selecting this product goes -- the product's own URL, never the hub. */\n href: string\n /**\n * Whether the account holds this product. Defaults to `true`. `false` dims the\n * mark and shows the \"Not on plan\" tag, but the row stays an ordinary focusable\n * link: entitlement is a rendering hint, and the switcher is not the thing\n * deciding what someone may open (ADR-0010, \"Not entitled is not disabled\").\n */\n entitled?: boolean\n}\n\n/** The \"all products\" row above the divider. Its blurb and URL are the consumer's. */\nexport interface AppSwitcherHub {\n /** The row's name, e.g. \"Hub\". */\n name: string\n /** The line beneath it, e.g. \"All Boost33 products\". */\n description: string\n /** The hub URL. */\n href: string\n}\n\n/**\n * The account/workspace row at the foot of the panel. A row that opens a second\n * panel (a later ticket owns that panel), so it is a button rather than a link;\n * this ticket renders the row and calls `onSelect` when it is chosen.\n */\nexport interface AppSwitcherAccount {\n /** The organisation's name, e.g. \"Acme Coaching\". */\n name: string\n /** The avatar's initials, e.g. \"AC\". */\n initials: string\n /** The line beneath the name, preformatted, e.g. \"Admin · 2 workspaces\". */\n description: string\n /** Chosen -- opens the workspace panel. */\n onSelect?: () => void\n}\n\nexport interface AppSwitcherProps {\n /** The products, in the order Identity serves them. Rendered whole, entitled or not. */\n products: AppSwitcherProduct[]\n /**\n * The slug of the product this shell is rendering for. It names the trigger\n * and is the row that carries the check -- the product you are in.\n *\n * A slug matching nothing in `products` is a supported state, not a caller\n * error: the registry is Identity's and can run ahead of this package\n * (ADR-0003). The mark degrades to a bare tile and the trigger falls back to\n * `labels.switchProduct` for its name (B33-919).\n */\n currentSlug: string\n /** The \"all products\" row above the products. */\n hub: AppSwitcherHub\n /** The account/workspace row below the products. */\n account: AppSwitcherAccount\n /**\n * Every user-facing string that is not row data. Each field defaults to\n * English, and a field left out -- or supplied as `undefined`, which is what\n * a missed next-intl key looks like -- keeps its default (B33-873).\n */\n labels?: Partial<AppSwitcherLabels>\n /**\n * The element the product and hub rows render through. Defaults to a plain\n * `a`; a router-owning consumer passes its own so the rows route client-side\n * with the locale prefix intact (ADR-0011, B33-847). The account row stays a\n * button -- it opens a panel, it does not navigate.\n */\n linkComponent?: ElementType\n /** The 56px rail. The trigger collapses to the current product's mark alone. */\n collapsed?: boolean\n /** Render the panel open on first paint -- the specimen's Stage-A open state. */\n defaultOpen?: boolean\n /** Every open and close, so a consumer can un-clip an ancestor's overflow while the panel overhangs. */\n onOpenChange?: (open: boolean) => void\n}\n\n/**\n * The product switcher under the wordmark. It opens, lists every Boost33 product\n * with the current one checked, and takes you straight into the one you pick --\n * the real switcher the Wallet's chevron-that-opens-nothing never was (B33-479).\n *\n * A disclosure, not a menu (ADR-0010): the panel carries no widget role, the\n * rows are the real links and buttons they already are, and Tab is the\n * document's own tab order -- so tabbing through the products does not close the\n * panel on the first row. Dismissal, focus restoration and trigger ARIA are the\n * shared anchored-overlay contract's (ADR-0009); it never becomes a modal.\n */\nexport function AppSwitcher({\n products,\n currentSlug,\n hub,\n account,\n labels,\n linkComponent,\n collapsed = false,\n defaultOpen,\n onOpenChange,\n}: AppSwitcherProps) {\n const overlay = useAnchoredOverlay({ defaultOpen, onOpenChange })\n const t = resolveAppSwitcherLabels(labels)\n const Link: ElementType = linkComponent ?? 'a'\n const current = products.find((p) => p.slug === currentSlug)\n\n // The name the trigger carries, in both its shapes. `current` is `undefined`\n // whenever `currentSlug` matches nothing in the served registry -- a fifth\n // Identity product, or a slug left stale by a rename -- which ADR-0003\n // contemplates and `AppSwitcherProduct.slug` being `string` rather than\n // `ProductSlug` deliberately allows.\n //\n // Degrading the artwork to a bare tile there is right. Degrading the name is\n // not, and that is what happened: the mark is `aria-hidden` unless it is\n // given a title, so a collapsed trigger handed `title={undefined}` had no\n // accessible name at all. A screen reader reached the only control in the\n // 56px rail and announced \"button\" -- no product, no purpose, nothing to act\n // on. Expanded it was the same absence with a visible face: an empty\n // `.triggerName` between a blank tile and a chevron. CLAUDE.md puts labels in\n // the definition of done, so the fallback is a label prop rather than a\n // literal (B33-919, B33-849).\n const triggerName = current?.name ?? t.switchProduct\n\n return (\n <div className={styles.root}>\n <button\n type=\"button\"\n className={collapsed ? styles.railTrigger : styles.trigger}\n onClick={overlay.toggle}\n {...overlay.triggerProps}\n >\n {/* On the rail the mark is the button's only content, so it carries the\n accessible name (title); expanded, the name sits beside it and the\n mark is decorative, or a screen reader says the product twice.\n\n No `size`. ProductMark's default is 20px -- the Wallet's own size at\n its call sites, and the size all three appswitcher-* baselines were\n captured at. This passed 28 expanded and 32 collapsed until B33-479's\n fidelity pass: a mark 8px over budget in every row pushed the account\n row out of the open panel's frame entirely and truncated \"Boost\n Outbound\" to \"Boos...\". The rail is not a larger mark; it is the same\n mark with the label and chevron gone. */}\n <ProductMark slug={currentSlug} title={collapsed ? triggerName : undefined} />\n {!collapsed && (\n <>\n <span className={styles.triggerName}>{triggerName}</span>\n <Chevron className={styles.chevron} />\n </>\n )}\n </button>\n\n {overlay.open && (\n <div {...overlay.panelProps} className={styles.panel} aria-label={t.panel}>\n <Link className={styles.hubRow} href={hub.href} onClick={overlay.close}>\n <HubMark className={styles.hubMark} />\n <span className={styles.rowText}>\n <span className={styles.rowName}>{hub.name}</span>\n <span className={styles.rowSub}>{hub.description}</span>\n </span>\n </Link>\n\n <hr className={styles.divider} />\n\n <ul className={styles.products}>\n {products.map((product) => {\n const isCurrent = product.slug === currentSlug\n const notEntitled = product.entitled === false\n return (\n <li key={product.slug}>\n <Link\n className={styles.productRow}\n href={product.href}\n aria-current={isCurrent ? 'true' : undefined}\n data-current={isCurrent ? 'true' : undefined}\n data-entitled={notEntitled ? 'false' : undefined}\n onClick={overlay.close}\n >\n {/* Default 20px, as in the trigger above -- see the note there. */}\n <ProductMark slug={product.slug} muted={notEntitled} />\n <span className={styles.rowName}>{product.name}</span>\n {notEntitled && <span className={styles.tag}>{t.notOnPlan}</span>}\n {isCurrent && <Check className={styles.check} />}\n </Link>\n </li>\n )\n })}\n </ul>\n\n <hr className={styles.divider} />\n\n <button type=\"button\" className={styles.accountRow} onClick={account.onSelect}>\n <span className={styles.avatar} aria-hidden=\"true\">\n {account.initials}\n </span>\n <span className={styles.rowText}>\n <span className={styles.rowName}>{account.name}</span>\n <span className={styles.rowSub}>{account.description}</span>\n </span>\n <ChevronRight className={styles.chevronRight} />\n </button>\n </div>\n )}\n </div>\n )\n}\n"],"names":["AppSwitcher","products","currentSlug","hub","account","labels","linkComponent","collapsed","defaultOpen","onOpenChange","overlay","useAnchoredOverlay","t","resolveAppSwitcherLabels","Link","current","p","triggerName","jsxs","styles","jsx","ProductMark","Fragment","Chevron","HubMark","product","isCurrent","notEntitled","Check","ChevronRight"],"mappings":"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"}
1
+ {"version":3,"file":"AppSwitcher.js","sources":["../../../src/components/AppSwitcher/AppSwitcher.tsx"],"sourcesContent":["'use client'\n\n// A client module: the switcher holds open/closed state and wires clicks, focus\n// and Escape through useAnchoredOverlay, none of which a Server Component can\n// carry. The rest of the shell stays on the server -- see WalletChip, Sidebar\n// and B33-846. AppShell renders this as a slot (`switcher`), so it imports no\n// client code by taking it.\n\nimport type { ElementType } from 'react'\nimport { useAnchoredOverlay } from '../../anchored-overlay'\nimport { ProductMark } from '../ProductMark/ProductMark'\nimport { Check, Chevron, ChevronRight, HubMark } from './icons'\nimport { resolveAppSwitcherLabels, type AppSwitcherLabels } from './labels'\nimport styles from './AppSwitcher.module.css'\n\n/**\n * One product the switcher can jump to. It is Identity's registry row, supplied\n * by the consuming app (ADR-0003) -- the switcher fetches nothing and hardcodes\n * no list. Every product is rendered, entitled or not and built or not: hiding\n * an unowned one means nobody learns the family exists (B33-479), and hiding an\n * unbuilt one means nobody learns it is coming.\n */\nexport interface AppSwitcherProduct {\n /** Identity's product slug, which selects the ProductMark artwork. */\n slug: string\n /** The product's display name, e.g. \"Boost Score\". Identity's, not the slug. */\n name: string\n /** Where selecting this product goes -- the product's own URL, never the hub. */\n href: string\n /**\n * Whether the account holds this product. Defaults to `true`. `false` dims the\n * mark and shows the \"Not on plan\" tag, but the row stays an ordinary focusable\n * link: entitlement is a rendering hint, and the switcher is not the thing\n * deciding what someone may open (ADR-0010, \"Not entitled is not disabled\").\n */\n entitled?: boolean\n /**\n * Whether the product exists yet. Defaults to `true`. `false` dims the mark and\n * shows the \"Soon\" tag in place of \"Not on plan\".\n *\n * **A different question from `entitled`, which is why it is a second field\n * and not a third value of the first.** `live` is global: the same answer for\n * every account, because either the product has shipped or it has not.\n * `entitled` is per account, read from the grant. Identity serves both for\n * exactly this reason (B33-561, and the `live` note in\n * `boost33-identity/src/lib/productRegistry.ts`).\n *\n * Until 0.4 this interface had nowhere to put it, so both consumers read the\n * field off the registry and dropped it on the floor -- each with a comment\n * saying so and calling it worth a package ticket. The cost was a row that\n * told a client they had not bought Boost Voice, when nobody has: the row read\n * \"Not on plan\" and pointed at a product with no code behind it.\n *\n * It stays a rendering hint like `entitled`. An unbuilt product's row is still\n * an ordinary focusable link -- the switcher does not decide what anyone may\n * open (ADR-0010), and `url` is served for every slug whether or not anything\n * answers on it, because `?app=` has to resolve for a campaign link.\n */\n live?: boolean\n}\n\n/** The \"all products\" row above the divider. Its blurb and URL are the consumer's. */\nexport interface AppSwitcherHub {\n /** The row's name, e.g. \"Hub\". */\n name: string\n /** The line beneath it, e.g. \"All Boost33 products\". */\n description: string\n /** The hub URL. */\n href: string\n}\n\n/**\n * The account/workspace row at the foot of the panel. A row that opens a second\n * panel (a later ticket owns that panel), so it is a button rather than a link;\n * this ticket renders the row and calls `onSelect` when it is chosen.\n */\nexport interface AppSwitcherAccount {\n /** The organisation's name, e.g. \"Acme Coaching\". */\n name: string\n /** The avatar's initials, e.g. \"AC\". */\n initials: string\n /** The line beneath the name, preformatted, e.g. \"Admin · 2 workspaces\". */\n description: string\n /** Chosen -- opens the workspace panel. */\n onSelect?: () => void\n}\n\nexport interface AppSwitcherProps {\n /**\n * The products, in the order Identity serves them. Rendered whole -- entitled\n * or not, built or not.\n */\n products: AppSwitcherProduct[]\n /**\n * The slug of the product this shell is rendering for. It names the trigger\n * and is the row that carries the check -- the product you are in.\n *\n * A slug matching nothing in `products` is a supported state, not a caller\n * error: the registry is Identity's and can run ahead of this package\n * (ADR-0003). The mark degrades to a bare tile and the trigger falls back to\n * `labels.switchProduct` for its name (B33-919).\n */\n currentSlug: string\n /** The \"all products\" row above the products. */\n hub: AppSwitcherHub\n /** The account/workspace row below the products. */\n account: AppSwitcherAccount\n /**\n * Every user-facing string that is not row data. Each field defaults to\n * English, and a field left out -- or supplied as `undefined`, which is what\n * a missed next-intl key looks like -- keeps its default (B33-873).\n */\n labels?: Partial<AppSwitcherLabels>\n /**\n * The element the product and hub rows render through. Defaults to a plain\n * `a`; a router-owning consumer passes its own so the rows route client-side\n * with the locale prefix intact (ADR-0011, B33-847). The account row stays a\n * button -- it opens a panel, it does not navigate.\n */\n linkComponent?: ElementType\n /** The 56px rail. The trigger collapses to the current product's mark alone. */\n collapsed?: boolean\n /** Render the panel open on first paint -- the specimen's Stage-A open state. */\n defaultOpen?: boolean\n /** Every open and close, so a consumer can un-clip an ancestor's overflow while the panel overhangs. */\n onOpenChange?: (open: boolean) => void\n}\n\n/**\n * The product switcher under the wordmark. It opens, lists every Boost33 product\n * with the current one checked, and takes you straight into the one you pick --\n * the real switcher the Wallet's chevron-that-opens-nothing never was (B33-479).\n *\n * A disclosure, not a menu (ADR-0010): the panel carries no widget role, the\n * rows are the real links and buttons they already are, and Tab is the\n * document's own tab order -- so tabbing through the products does not close the\n * panel on the first row. Dismissal, focus restoration and trigger ARIA are the\n * shared anchored-overlay contract's (ADR-0009); it never becomes a modal.\n */\nexport function AppSwitcher({\n products,\n currentSlug,\n hub,\n account,\n labels,\n linkComponent,\n collapsed = false,\n defaultOpen,\n onOpenChange,\n}: AppSwitcherProps) {\n const overlay = useAnchoredOverlay({ defaultOpen, onOpenChange })\n const t = resolveAppSwitcherLabels(labels)\n const Link: ElementType = linkComponent ?? 'a'\n const current = products.find((p) => p.slug === currentSlug)\n\n // The name the trigger carries, in both its shapes. `current` is `undefined`\n // whenever `currentSlug` matches nothing in the served registry -- a fifth\n // Identity product, or a slug left stale by a rename -- which ADR-0003\n // contemplates and `AppSwitcherProduct.slug` being `string` rather than\n // `ProductSlug` deliberately allows.\n //\n // Degrading the artwork to a bare tile there is right. Degrading the name is\n // not, and that is what happened: the mark is `aria-hidden` unless it is\n // given a title, so a collapsed trigger handed `title={undefined}` had no\n // accessible name at all. A screen reader reached the only control in the\n // 56px rail and announced \"button\" -- no product, no purpose, nothing to act\n // on. Expanded it was the same absence with a visible face: an empty\n // `.triggerName` between a blank tile and a chevron. CLAUDE.md puts labels in\n // the definition of done, so the fallback is a label prop rather than a\n // literal (B33-919, B33-849).\n const triggerName = current?.name ?? t.switchProduct\n\n return (\n <div className={styles.root}>\n <button\n type=\"button\"\n className={collapsed ? styles.railTrigger : styles.trigger}\n onClick={overlay.toggle}\n {...overlay.triggerProps}\n >\n {/* On the rail the mark is the button's only content, so it carries the\n accessible name (title); expanded, the name sits beside it and the\n mark is decorative, or a screen reader says the product twice.\n\n No `size`. ProductMark's default is 20px -- the Wallet's own size at\n its call sites, and the size all three appswitcher-* baselines were\n captured at. This passed 28 expanded and 32 collapsed until B33-479's\n fidelity pass: a mark 8px over budget in every row pushed the account\n row out of the open panel's frame entirely and truncated \"Boost\n Outbound\" to \"Boos...\". The rail is not a larger mark; it is the same\n mark with the label and chevron gone. */}\n <ProductMark slug={currentSlug} title={collapsed ? triggerName : undefined} />\n {!collapsed && (\n <>\n <span className={styles.triggerName}>{triggerName}</span>\n <Chevron className={styles.chevron} />\n </>\n )}\n </button>\n\n {overlay.open && (\n <div {...overlay.panelProps} className={styles.panel} aria-label={t.panel}>\n <Link className={styles.hubRow} href={hub.href} onClick={overlay.close}>\n <HubMark className={styles.hubMark} />\n <span className={styles.rowText}>\n <span className={styles.rowName}>{hub.name}</span>\n <span className={styles.rowSub}>{hub.description}</span>\n </span>\n </Link>\n\n <hr className={styles.divider} />\n\n <ul className={styles.products}>\n {products.map((product) => {\n const isCurrent = product.slug === currentSlug\n const notEntitled = product.entitled === false\n const notLive = product.live === false\n\n // One tag slot, and the unbuilt word wins it.\n //\n // A product that has not shipped is held by nobody, so Identity\n // serves every `live: false` row as `entitled: false` too and both\n // conditions are true at once on the same row. Rendering both tags\n // would stack two pills on a row with one tag's worth of space --\n // and the second would be answering a question the first has\n // already closed: \"not on plan\" is trivially true of a product\n // that does not exist, and saying it sends the reader off to ask\n // about buying something unbuyable (B33-561).\n //\n // The dim is deliberately shared rather than split. `muted` has\n // one meaning on the mark -- this row is not somewhere you can go\n // today -- and that is equally true either way; the tag is the\n // channel carrying *which* reason, because it is the one a screen\n // reader reaches and the mark is decorative here (ADR-0012).\n const tag = notLive ? t.soon : notEntitled ? t.notOnPlan : null\n\n return (\n <li key={product.slug}>\n <Link\n className={styles.productRow}\n href={product.href}\n aria-current={isCurrent ? 'true' : undefined}\n data-current={isCurrent ? 'true' : undefined}\n data-entitled={notEntitled ? 'false' : undefined}\n data-live={notLive ? 'false' : undefined}\n onClick={overlay.close}\n >\n {/* Default 20px, as in the trigger above -- see the note there. */}\n <ProductMark slug={product.slug} muted={notEntitled || notLive} />\n <span className={styles.rowName}>{product.name}</span>\n {tag !== null && <span className={styles.tag}>{tag}</span>}\n {isCurrent && <Check className={styles.check} />}\n </Link>\n </li>\n )\n })}\n </ul>\n\n <hr className={styles.divider} />\n\n <button type=\"button\" className={styles.accountRow} onClick={account.onSelect}>\n <span className={styles.avatar} aria-hidden=\"true\">\n {account.initials}\n </span>\n <span className={styles.rowText}>\n <span className={styles.rowName}>{account.name}</span>\n <span className={styles.rowSub}>{account.description}</span>\n </span>\n <ChevronRight className={styles.chevronRight} />\n </button>\n </div>\n )}\n </div>\n )\n}\n"],"names":["AppSwitcher","products","currentSlug","hub","account","labels","linkComponent","collapsed","defaultOpen","onOpenChange","overlay","useAnchoredOverlay","t","resolveAppSwitcherLabels","Link","current","p","triggerName","jsxs","styles","jsx","ProductMark","Fragment","Chevron","HubMark","product","isCurrent","notEntitled","notLive","tag","Check","ChevronRight"],"mappings":"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"}
@@ -1,44 +1,44 @@
1
- const c = "_root_chh8a_8", o = "_trigger_chh8a_17", _ = "_railTrigger_chh8a_46", h = "_triggerName_chh8a_56", r = "_chevron_chh8a_67", t = "_panel_chh8a_109", a = "_hubRow_chh8a_135", e = "_productRow_chh8a_136", n = "_accountRow_chh8a_137", s = "_products_chh8a_144", g = "_rowName_chh8a_193", i = "_rowSub_chh8a_213", u = "_rowText_chh8a_217", w = "_hubMark_chh8a_260", d = "_tag_chh8a_269", v = "_check_chh8a_280", R = "_avatar_chh8a_293", p = "_chevronRight_chh8a_306", b = "_divider_chh8a_327", l = {
2
- root: c,
3
- trigger: o,
4
- railTrigger: _,
5
- triggerName: h,
6
- chevron: r,
7
- panel: t,
8
- hubRow: a,
1
+ const o = "_root_1ih3v_8", _ = "_trigger_1ih3v_17", r = "_railTrigger_1ih3v_46", t = "_triggerName_1ih3v_56", c = "_chevron_1ih3v_67", i = "_panel_1ih3v_109", h = "_hubRow_1ih3v_135", e = "_productRow_1ih3v_136", a = "_accountRow_1ih3v_137", n = "_products_1ih3v_144", v = "_rowName_1ih3v_193", s = "_rowSub_1ih3v_213", g = "_rowText_1ih3v_217", u = "_hubMark_1ih3v_260", w = "_tag_1ih3v_276", d = "_check_1ih3v_287", R = "_avatar_1ih3v_300", p = "_chevronRight_1ih3v_313", b = "_divider_1ih3v_334", l = {
2
+ root: o,
3
+ trigger: _,
4
+ railTrigger: r,
5
+ triggerName: t,
6
+ chevron: c,
7
+ panel: i,
8
+ hubRow: h,
9
9
  productRow: e,
10
- accountRow: n,
11
- products: s,
12
- rowName: g,
13
- rowSub: i,
14
- rowText: u,
15
- hubMark: w,
16
- tag: d,
17
- check: v,
10
+ accountRow: a,
11
+ products: n,
12
+ rowName: v,
13
+ rowSub: s,
14
+ rowText: g,
15
+ hubMark: u,
16
+ tag: w,
17
+ check: d,
18
18
  avatar: R,
19
19
  chevronRight: p,
20
20
  divider: b
21
21
  };
22
22
  export {
23
- n as accountRow,
23
+ a as accountRow,
24
24
  R as avatar,
25
- v as check,
26
- r as chevron,
25
+ d as check,
26
+ c as chevron,
27
27
  p as chevronRight,
28
28
  l as default,
29
29
  b as divider,
30
- w as hubMark,
31
- a as hubRow,
32
- t as panel,
30
+ u as hubMark,
31
+ h as hubRow,
32
+ i as panel,
33
33
  e as productRow,
34
- s as products,
35
- _ as railTrigger,
36
- c as root,
37
- g as rowName,
38
- i as rowSub,
39
- u as rowText,
40
- d as tag,
41
- o as trigger,
42
- h as triggerName
34
+ n as products,
35
+ r as railTrigger,
36
+ o as root,
37
+ v as rowName,
38
+ s as rowSub,
39
+ g as rowText,
40
+ w as tag,
41
+ _ as trigger,
42
+ t as triggerName
43
43
  };
44
44
  //# sourceMappingURL=AppSwitcher.module.css.js.map
@@ -12,6 +12,26 @@ export interface AppSwitcherLabels {
12
12
  * disabled"). Default "Not on plan".
13
13
  */
14
14
  notOnPlan: string;
15
+ /**
16
+ * The tag on a product that does not exist yet -- `live: false` on the row.
17
+ * Default "Soon".
18
+ *
19
+ * It takes the same slot as `notOnPlan` and never sits beside it, because the
20
+ * two answer different questions and only one of them is worth a person's
21
+ * attention at a time. `live` is whether the product exists at all, globally;
22
+ * `notOnPlan` is whether this account holds it. A product that has not shipped
23
+ * is held by nobody, so an unbuilt row is *always* also an unentitled one --
24
+ * which is why the unbuilt word wins the slot rather than stacking with it.
25
+ *
26
+ * Collapsing the two is the confusion Identity added the `live` field to
27
+ * prevent (B33-561). With only `notOnPlan`, a product nobody can buy yet
28
+ * renders as one this client has not bought, and the row invites them to go
29
+ * and ask about something that does not exist.
30
+ *
31
+ * Like every other label here it is a prop with an English default, so the two
32
+ * French-default consumers translate it rather than reading "Soon".
33
+ */
34
+ soon: string;
15
35
  /**
16
36
  * The trigger's name when `currentSlug` matches no product in the supplied
17
37
  * list, so there is no product name to put there. Default "Switch product".
@@ -36,6 +56,14 @@ export interface AppSwitcherLabels {
36
56
  * `switchProduct` is no exception: it only ever reaches the screen when
37
57
  * `currentSlug` matches nothing, and all three baselines were captured with the
38
58
  * Wallet current.
59
+ *
60
+ * `soon` is no exception either, for the same shape of reason: it reaches the
61
+ * screen only on a row carrying `live: false`, and `live` defaults to true, so
62
+ * the four rows the baselines were captured from still draw exactly the tag they
63
+ * drew before the field existed. The Wallet never rendered a "Soon" tag at all
64
+ * -- it had no registry to read `live` from -- so there is no captured baseline
65
+ * for this state and `scripts/screenshot-states.ts` records the specimen row
66
+ * that shows it as ungated, pending a human capture (CLAUDE.md).
39
67
  */
40
68
  export declare const defaultAppSwitcherLabels: AppSwitcherLabels;
41
69
  /**
@@ -1,13 +1,14 @@
1
1
  const n = {
2
2
  panel: "Boost33 products",
3
3
  notOnPlan: "Not on plan",
4
+ soon: "Soon",
4
5
  switchProduct: "Switch product"
5
6
  };
6
7
  function r(t) {
7
- const e = Object.fromEntries(
8
- Object.entries(t ?? {}).filter(([, o]) => o !== void 0)
8
+ const o = Object.fromEntries(
9
+ Object.entries(t ?? {}).filter(([, e]) => e !== void 0)
9
10
  );
10
- return { ...n, ...e };
11
+ return { ...n, ...o };
11
12
  }
12
13
  export {
13
14
  n as defaultAppSwitcherLabels,
@@ -1 +1 @@
1
- {"version":3,"file":"labels.js","sources":["../../../src/components/AppSwitcher/labels.ts"],"sourcesContent":["// The switcher's own user-facing words, each a prop with an English default --\n// the same pattern WalletChipLabels follows, and for the same reason: the first\n// two consumers are French-default, and the package renders what it is given\n// rather than taking an i18n dependency (CLAUDE.md; B33-849). The product names,\n// the Hub's name and blurb, and the account row's name and subtitle are not here\n// -- they are Identity's data, supplied per row on `products`, `hub` and\n// `account`, not copy this package owns.\n\n/** Every user-facing string the switcher renders that is not row data. */\nexport interface AppSwitcherLabels {\n /**\n * The panel's accessible name. A disclosure panel carries no widget role, so\n * it takes an `aria-label` to be named at all (ADR-0010). Default\n * \"Boost33 products\".\n */\n panel: string\n /**\n * The tag on a product the account is not entitled to. Not a gate -- the row\n * stays an ordinary link -- only a hint (ADR-0010: \"Not entitled is not\n * disabled\"). Default \"Not on plan\".\n */\n notOnPlan: string\n /**\n * The trigger's name when `currentSlug` matches no product in the supplied\n * list, so there is no product name to put there. Default \"Switch product\".\n *\n * That case is reachable by design: `AppSwitcherProduct.slug` is typed\n * `string` rather than `ProductSlug` so an unknown slug degrades to a bare\n * tile instead of failing (ADR-0003 -- Identity owns the registry and this\n * package does not know which slugs exist). A fifth Identity product, or a\n * `currentSlug` left stale by a rename, lands there. Before B33-919 the\n * degradation took the button's accessible name with it and the only control\n * in the collapsed shell announced nothing at all.\n *\n * It names the control rather than describing the missing product, because\n * the product is precisely what this package does not know. \"Switch product\"\n * is true in every state; a guess at the product's name would not be.\n */\n switchProduct: string\n}\n\n/**\n * The English defaults. A consumer that passes nothing renders exactly what the\n * appswitcher-* baselines were captured showing, so those baselines do not move.\n * `switchProduct` is no exception: it only ever reaches the screen when\n * `currentSlug` matches nothing, and all three baselines were captured with the\n * Wallet current.\n */\nexport const defaultAppSwitcherLabels: AppSwitcherLabels = {\n panel: 'Boost33 products',\n notOnPlan: 'Not on plan',\n switchProduct: 'Switch product',\n}\n\n/**\n * Merge a consumer's partial `labels` onto the English defaults, treating an\n * explicitly-`undefined` field as unsupplied -- the same resolution\n * `resolveWalletChipLabels` performs, for the same two reasons.\n *\n * The first is forward compatibility, and it is why this exists now. `labels`\n * used to be a whole `AppSwitcherLabels`, so a consumer wrote the object out in\n * full; adding `switchProduct` to a required-field interface would have failed\n * the typecheck of every app that had done so, turning an accessibility fix\n * into a breaking change across four products on a patch bump (CLAUDE.md,\n * \"Versioning\"). Taking a `Partial` means a consumer supplies the fields it\n * wants to translate and inherits the rest, today and at the next field.\n *\n * The second is that a missed translation key must fall back to English rather\n * than blank the string. A next-intl `t('...')` returns `undefined` on a miss,\n * so `{ panel: undefined }` is what one missing key looks like at the call\n * site. A plain object spread copies that `undefined` across and unnames the\n * panel; a per-field skip does not (B33-873).\n */\nexport function resolveAppSwitcherLabels(labels?: Partial<AppSwitcherLabels>): AppSwitcherLabels {\n const supplied = Object.fromEntries(\n Object.entries(labels ?? {}).filter(([, value]) => value !== undefined),\n ) as Partial<AppSwitcherLabels>\n return { ...defaultAppSwitcherLabels, ...supplied }\n}\n"],"names":["defaultAppSwitcherLabels","resolveAppSwitcherLabels","labels","supplied","value"],"mappings":"AAgDO,MAAMA,IAA8C;AAAA,EACzD,OAAO;AAAA,EACP,WAAW;AAAA,EACX,eAAe;AACjB;AAqBO,SAASC,EAAyBC,GAAwD;AAC/F,QAAMC,IAAW,OAAO;AAAA,IACtB,OAAO,QAAQD,KAAU,EAAE,EAAE,OAAO,CAAC,CAAA,EAAGE,CAAK,MAAMA,MAAU,MAAS;AAAA,EAAA;AAExE,SAAO,EAAE,GAAGJ,GAA0B,GAAGG,EAAA;AAC3C;"}
1
+ {"version":3,"file":"labels.js","sources":["../../../src/components/AppSwitcher/labels.ts"],"sourcesContent":["// The switcher's own user-facing words, each a prop with an English default --\n// the same pattern WalletChipLabels follows, and for the same reason: the first\n// two consumers are French-default, and the package renders what it is given\n// rather than taking an i18n dependency (CLAUDE.md; B33-849). The product names,\n// the Hub's name and blurb, and the account row's name and subtitle are not here\n// -- they are Identity's data, supplied per row on `products`, `hub` and\n// `account`, not copy this package owns.\n\n/** Every user-facing string the switcher renders that is not row data. */\nexport interface AppSwitcherLabels {\n /**\n * The panel's accessible name. A disclosure panel carries no widget role, so\n * it takes an `aria-label` to be named at all (ADR-0010). Default\n * \"Boost33 products\".\n */\n panel: string\n /**\n * The tag on a product the account is not entitled to. Not a gate -- the row\n * stays an ordinary link -- only a hint (ADR-0010: \"Not entitled is not\n * disabled\"). Default \"Not on plan\".\n */\n notOnPlan: string\n /**\n * The tag on a product that does not exist yet -- `live: false` on the row.\n * Default \"Soon\".\n *\n * It takes the same slot as `notOnPlan` and never sits beside it, because the\n * two answer different questions and only one of them is worth a person's\n * attention at a time. `live` is whether the product exists at all, globally;\n * `notOnPlan` is whether this account holds it. A product that has not shipped\n * is held by nobody, so an unbuilt row is *always* also an unentitled one --\n * which is why the unbuilt word wins the slot rather than stacking with it.\n *\n * Collapsing the two is the confusion Identity added the `live` field to\n * prevent (B33-561). With only `notOnPlan`, a product nobody can buy yet\n * renders as one this client has not bought, and the row invites them to go\n * and ask about something that does not exist.\n *\n * Like every other label here it is a prop with an English default, so the two\n * French-default consumers translate it rather than reading \"Soon\".\n */\n soon: string\n /**\n * The trigger's name when `currentSlug` matches no product in the supplied\n * list, so there is no product name to put there. Default \"Switch product\".\n *\n * That case is reachable by design: `AppSwitcherProduct.slug` is typed\n * `string` rather than `ProductSlug` so an unknown slug degrades to a bare\n * tile instead of failing (ADR-0003 -- Identity owns the registry and this\n * package does not know which slugs exist). A fifth Identity product, or a\n * `currentSlug` left stale by a rename, lands there. Before B33-919 the\n * degradation took the button's accessible name with it and the only control\n * in the collapsed shell announced nothing at all.\n *\n * It names the control rather than describing the missing product, because\n * the product is precisely what this package does not know. \"Switch product\"\n * is true in every state; a guess at the product's name would not be.\n */\n switchProduct: string\n}\n\n/**\n * The English defaults. A consumer that passes nothing renders exactly what the\n * appswitcher-* baselines were captured showing, so those baselines do not move.\n * `switchProduct` is no exception: it only ever reaches the screen when\n * `currentSlug` matches nothing, and all three baselines were captured with the\n * Wallet current.\n *\n * `soon` is no exception either, for the same shape of reason: it reaches the\n * screen only on a row carrying `live: false`, and `live` defaults to true, so\n * the four rows the baselines were captured from still draw exactly the tag they\n * drew before the field existed. The Wallet never rendered a \"Soon\" tag at all\n * -- it had no registry to read `live` from -- so there is no captured baseline\n * for this state and `scripts/screenshot-states.ts` records the specimen row\n * that shows it as ungated, pending a human capture (CLAUDE.md).\n */\nexport const defaultAppSwitcherLabels: AppSwitcherLabels = {\n panel: 'Boost33 products',\n notOnPlan: 'Not on plan',\n soon: 'Soon',\n switchProduct: 'Switch product',\n}\n\n/**\n * Merge a consumer's partial `labels` onto the English defaults, treating an\n * explicitly-`undefined` field as unsupplied -- the same resolution\n * `resolveWalletChipLabels` performs, for the same two reasons.\n *\n * The first is forward compatibility, and it is why this exists now. `labels`\n * used to be a whole `AppSwitcherLabels`, so a consumer wrote the object out in\n * full; adding `switchProduct` to a required-field interface would have failed\n * the typecheck of every app that had done so, turning an accessibility fix\n * into a breaking change across four products on a patch bump (CLAUDE.md,\n * \"Versioning\"). Taking a `Partial` means a consumer supplies the fields it\n * wants to translate and inherits the rest, today and at the next field.\n *\n * The second is that a missed translation key must fall back to English rather\n * than blank the string. A next-intl `t('...')` returns `undefined` on a miss,\n * so `{ panel: undefined }` is what one missing key looks like at the call\n * site. A plain object spread copies that `undefined` across and unnames the\n * panel; a per-field skip does not (B33-873).\n */\nexport function resolveAppSwitcherLabels(labels?: Partial<AppSwitcherLabels>): AppSwitcherLabels {\n const supplied = Object.fromEntries(\n Object.entries(labels ?? {}).filter(([, value]) => value !== undefined),\n ) as Partial<AppSwitcherLabels>\n return { ...defaultAppSwitcherLabels, ...supplied }\n}\n"],"names":["defaultAppSwitcherLabels","resolveAppSwitcherLabels","labels","supplied","value"],"mappings":"AA4EO,MAAMA,IAA8C;AAAA,EACzD,OAAO;AAAA,EACP,WAAW;AAAA,EACX,MAAM;AAAA,EACN,eAAe;AACjB;AAqBO,SAASC,EAAyBC,GAAwD;AAC/F,QAAMC,IAAW,OAAO;AAAA,IACtB,OAAO,QAAQD,KAAU,EAAE,EAAE,OAAO,CAAC,CAAA,EAAGE,CAAK,MAAMA,MAAU,MAAS;AAAA,EAAA;AAExE,SAAO,EAAE,GAAGJ,GAA0B,GAAGG,EAAA;AAC3C;"}
@@ -1,13 +1,74 @@
1
1
  /**
2
- * The four products Identity serves, as one list. Everything else keys off it:
3
- * ProductSlug is derived from it, and GLYPHS is typed against it so adding a
4
- * slug without artwork is a compile error rather than a mark that silently
5
- * draws nothing. These are Identity's *slugs*, not display names -- the Wallet
6
- * keys its own record on `'Boost Score' | ... | 'Wallet'`, which a rename
7
- * breaks, and that is the exact trap this ordering avoids.
2
+ * The slugs this package draws a product mark for, as one list. Everything else
3
+ * keys off it: ProductSlug is derived from it, and GLYPHS is typed against it so
4
+ * adding a slug without artwork is a compile error rather than a mark that
5
+ * silently draws nothing.
6
+ *
7
+ * **It is no longer the whole of Identity's registry, and the difference is
8
+ * deliberate.** Identity serves five slugs: these four plus `brokers` (B33-1158,
9
+ * "Boost Brokers"). This package has no `brokers` glyph, so that row draws the
10
+ * bare tile through the safe degrade below -- see "The fifth slug" underneath.
11
+ *
12
+ * **Transcribed from `boost33-identity/src/lib/productRegistry.ts`** -- its
13
+ * `REGISTRY_SLUGS`, which is `PRODUCTS` from `src/config/products.ts` plus
14
+ * `WALLET_SLUG`. That file is the source of truth and this is a copy of it in
15
+ * another repository, which is a thing that drifts. It already did: until
16
+ * B33-1043 this list read `wallet | score | voice | outbound` while this comment
17
+ * asserted it was Identity's, so three of the four rows of a switcher fed
18
+ * straight from `GET /api/v1/products` drew the bare tile (see `slug` below) and
19
+ * nothing anywhere failed.
20
+ *
21
+ * Identity is not the side that renames. ADR-0003 records the shorter slugs as
22
+ * considered and rejected: these match the `?app=` keys live campaign URLs
23
+ * already carry, so renaming them means those links grant nothing on sign-up.
24
+ *
25
+ * `tests/product-mark.test.tsx` pins this list against a second transcription of
26
+ * the same source, so the two repositories cannot part company in silence again.
27
+ * That pin is what the old comment's assertion was standing in for, and it now
28
+ * pins the gap as well as the overlap: it names `brokers` as the one served slug
29
+ * with no glyph here, so a *sixth* product, or artwork landing for this one,
30
+ * fails rather than passing quietly.
31
+ *
32
+ * The *order* is this package's own and not Identity's -- `REGISTRY_SLUGS` ends
33
+ * with `wallet` where this begins with it. Nothing reads a render order off this
34
+ * list: a switcher draws the order its consumer was served (`AppSwitcher`), and
35
+ * the only thing this order sets is the left-to-right run of frames in the
36
+ * ProductMark specimen.
37
+ *
38
+ * ── The fifth slug ───────────────────────────────────────────────────────────
39
+ *
40
+ * **`brokers` is absent because no Brokers artwork exists to lift, not because
41
+ * nobody got to it.** The four glyphs below were taken verbatim out of
42
+ * `boostledger/src/ui/primitives/icons.tsx`, whose own `GLYPHS` is typed
43
+ * `Record<ProductName, ReactNode>` over exactly four display names. There is no
44
+ * Brokers mark in that file, none in `boost33-brand-hub`, and none in this
45
+ * repository. The hub renders the product as a text quick-link and no mark at
46
+ * all.
47
+ *
48
+ * So drawing one here would not be transcribing a mark, it would be inventing
49
+ * one -- and the comment on GLYPHS below records what that costs: PR #4 redrew
50
+ * all four from a written description, got all four wrong, and reintroduced the
51
+ * retired one-orange-element construction. An invented fifth mark is worse than
52
+ * that was, because it has nothing to be caught against. A human captures the
53
+ * first baseline at the fidelity gate, which would make the invention the
54
+ * referent, and it looks equally settled either way afterwards (CLAUDE.md, "an
55
+ * agent that picks one and builds it has invented a referent").
56
+ *
57
+ * The degrade is also the mildest it will ever be. Identity serves `brokers` as
58
+ * `live: false` -- the space is still being built -- so the switcher row carries
59
+ * the "Soon" tag beside the bare tile, which reads as a product that has not
60
+ * arrived rather than as a drawing that failed to load. That stops being true
61
+ * the day Brokers ships.
62
+ *
63
+ * **To close it:** get the mark drawn where the other four live
64
+ * (`boostledger/src/ui/primitives/icons.tsx`), lift it here verbatim, add
65
+ * `brokers` to this list, and let a human approve `productmark-brokers.png` at a
66
+ * gate. Not in that order optional: this list is what GLYPHS is typed against,
67
+ * so adding the slug without the artwork stops the build, which is the one part
68
+ * of this that cannot go wrong quietly.
8
69
  */
9
- export declare const PRODUCT_SLUGS: readonly ["wallet", "score", "voice", "outbound"];
10
- /** One of Identity's four product slugs. */
70
+ export declare const PRODUCT_SLUGS: readonly ["wallet", "boostscore", "boostvoice", "boostoutbound"];
71
+ /** One of the slugs this package draws a mark for. Not every slug Identity serves -- see above. */
11
72
  export type ProductSlug = (typeof PRODUCT_SLUGS)[number];
12
73
  export interface ProductMarkProps {
13
74
  /**
@@ -1,7 +1,7 @@
1
1
  import { jsxs as o, jsx as r, Fragment as l } from "react/jsx-runtime";
2
2
  import { useId as a } from "react";
3
3
  import i from "./ProductMark.module.css.js";
4
- const k = 20, x = ["wallet", "score", "voice", "outbound"], f = {
4
+ const k = 20, x = ["wallet", "boostscore", "boostvoice", "boostoutbound"], f = {
5
5
  // A credit card with its magnetic stripe: an outline card (fill none, so the
6
6
  // tile shows through) plus a filled stripe across it.
7
7
  wallet: /* @__PURE__ */ o(l, { children: [
@@ -10,29 +10,29 @@ const k = 20, x = ["wallet", "score", "voice", "outbound"], f = {
10
10
  ] }),
11
11
  // A gauge with the needle swung high: an arc, the needle, and the hub it
12
12
  // pivots on. Arc and needle stroke at width 4 with round caps.
13
- score: /* @__PURE__ */ o(l, { children: [
13
+ boostscore: /* @__PURE__ */ o(l, { children: [
14
14
  /* @__PURE__ */ r("path", { d: "M6 26 A14 14 0 0 1 34 26", fill: "none", stroke: "currentColor", strokeWidth: "4", strokeLinecap: "round" }),
15
15
  /* @__PURE__ */ r("line", { x1: "20", y1: "26", x2: "28", y2: "14", stroke: "currentColor", strokeWidth: "4", strokeLinecap: "round" }),
16
16
  /* @__PURE__ */ r("circle", { cx: "20", cy: "26", r: "3", fill: "currentColor" })
17
17
  ] }),
18
18
  // A soundwave, tall in the middle: three pill bars, all centred on y=20.
19
- voice: /* @__PURE__ */ o(l, { children: [
19
+ boostvoice: /* @__PURE__ */ o(l, { children: [
20
20
  /* @__PURE__ */ r("rect", { x: "6", y: "14", width: "7", height: "12", rx: "3.5", fill: "currentColor" }),
21
21
  /* @__PURE__ */ r("rect", { x: "16.5", y: "4", width: "7", height: "32", rx: "3.5", fill: "currentColor" }),
22
22
  /* @__PURE__ */ r("rect", { x: "27", y: "14", width: "7", height: "12", rx: "3.5", fill: "currentColor" })
23
23
  ] }),
24
24
  // An arrow leaving through the top-right: the shaft and the elbow of its head,
25
25
  // both stroke width 4 with round caps and joins.
26
- outbound: /* @__PURE__ */ o(l, { children: [
26
+ boostoutbound: /* @__PURE__ */ o(l, { children: [
27
27
  /* @__PURE__ */ r("line", { x1: "8", y1: "32", x2: "32", y2: "8", stroke: "currentColor", strokeWidth: "4", strokeLinecap: "round", strokeLinejoin: "round" }),
28
28
  /* @__PURE__ */ r("path", { d: "M16 8 H32 V24", fill: "none", stroke: "currentColor", strokeWidth: "4", strokeLinecap: "round", strokeLinejoin: "round" })
29
29
  ] })
30
30
  };
31
- function y(e) {
32
- return x.includes(e);
31
+ function y(t) {
32
+ return x.includes(t);
33
33
  }
34
- function v({ slug: e, size: n = k, muted: u, title: c, className: d }) {
35
- const s = a(), t = c == null, h = y(e) ? f[e] : null;
34
+ function v({ slug: t, size: n = k, muted: u, title: c, className: d }) {
35
+ const s = a(), e = c == null, h = y(t) ? f[t] : null;
36
36
  return /* @__PURE__ */ o(
37
37
  "svg",
38
38
  {
@@ -40,11 +40,11 @@ function v({ slug: e, size: n = k, muted: u, title: c, className: d }) {
40
40
  width: n,
41
41
  height: n,
42
42
  viewBox: "0 0 40 40",
43
- role: t ? void 0 : "img",
44
- "aria-hidden": t ? "true" : void 0,
45
- "aria-labelledby": t ? void 0 : s,
43
+ role: e ? void 0 : "img",
44
+ "aria-hidden": e ? "true" : void 0,
45
+ "aria-labelledby": e ? void 0 : s,
46
46
  children: [
47
- !t && /* @__PURE__ */ r("title", { id: s, children: c }),
47
+ !e && /* @__PURE__ */ r("title", { id: s, children: c }),
48
48
  /* @__PURE__ */ r(
49
49
  "rect",
50
50
  {
@@ -1 +1 @@
1
- {"version":3,"file":"ProductMark.js","sources":["../../../src/components/ProductMark/ProductMark.tsx"],"sourcesContent":["import { useId, type ReactNode } from 'react'\nimport styles from './ProductMark.module.css'\n\n/**\n * The size the mark renders at unless a caller says otherwise. 20px is the\n * Wallet's own default at its call sites, and the size the four\n * tests/baselines/productmark-*.png were captured at: a 20px tile in the 24px\n * gutter the specimen crop uses either side -> 68px CSS -> 136 device px at\n * deviceScaleFactor 2. Changing it moves four apps.\n *\n * There is deliberately no minimum-legible floor here, unlike Boost33Mark. That\n * floor is the monogram's alone -- the bowls of the B close up below ~20px. The\n * product marks have the opposite property: each is one path on a 40-unit grid\n * with a quarter-side corner, chosen so it renders correctly from Paper's 96px\n * down to 20px. Do not generalise Boost33Mark's floor into a shared rule.\n */\nconst DEFAULT_SIZE = 20\n\n/**\n * The four products Identity serves, as one list. Everything else keys off it:\n * ProductSlug is derived from it, and GLYPHS is typed against it so adding a\n * slug without artwork is a compile error rather than a mark that silently\n * draws nothing. These are Identity's *slugs*, not display names -- the Wallet\n * keys its own record on `'Boost Score' | ... | 'Wallet'`, which a rename\n * breaks, and that is the exact trap this ordering avoids.\n */\nexport const PRODUCT_SLUGS = ['wallet', 'score', 'voice', 'outbound'] as const\n\n/** One of Identity's four product slugs. */\nexport type ProductSlug = (typeof PRODUCT_SLUGS)[number]\n\n/**\n * The four product glyphs, lifted verbatim from `boostledger`'s\n * `src/ui/primitives/icons.tsx` (the `GLYPHS` record, as of `837d85d`) rather\n * than redrawn from any written description. Redrawing them from\n * `docs/mintlify/system/product-marks.mdx` is how PR #4 got all four wrong and\n * reintroduced the retired one-orange-element construction; if any document\n * disagrees with that file, that file wins. See B33-562.\n *\n * Each glyph is drawn on its own full 40-unit grid and inset into the middle 20\n * of the tile by the `translate(10 10) scale(0.5)` group below. The stroke\n * widths are right on that grid -- do not \"correct\" them to compensate for the\n * scale, the scale is what puts the stroke at the weight Paper draws. Every\n * element paints in `currentColor` (the tile's white glyph ink) or `none`;\n * there is no colour of any kind in here, which is what tells the four marks\n * apart by shape and never by hue.\n *\n * Typing this `Record<ProductSlug, ReactNode>` is load-bearing: a fifth slug\n * added to PRODUCT_SLUGS without a glyph here stops the build.\n */\nconst GLYPHS: Record<ProductSlug, ReactNode> = {\n // A credit card with its magnetic stripe: an outline card (fill none, so the\n // tile shows through) plus a filled stripe across it.\n wallet: (\n <>\n <rect x=\"5\" y=\"10\" width=\"30\" height=\"20\" rx=\"4\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"3\" />\n <rect x=\"5\" y=\"16\" width=\"30\" height=\"5\" fill=\"currentColor\" />\n </>\n ),\n // A gauge with the needle swung high: an arc, the needle, and the hub it\n // pivots on. Arc and needle stroke at width 4 with round caps.\n score: (\n <>\n <path d=\"M6 26 A14 14 0 0 1 34 26\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"4\" strokeLinecap=\"round\" />\n <line x1=\"20\" y1=\"26\" x2=\"28\" y2=\"14\" stroke=\"currentColor\" strokeWidth=\"4\" strokeLinecap=\"round\" />\n <circle cx=\"20\" cy=\"26\" r=\"3\" fill=\"currentColor\" />\n </>\n ),\n // A soundwave, tall in the middle: three pill bars, all centred on y=20.\n voice: (\n <>\n <rect x=\"6\" y=\"14\" width=\"7\" height=\"12\" rx=\"3.5\" fill=\"currentColor\" />\n <rect x=\"16.5\" y=\"4\" width=\"7\" height=\"32\" rx=\"3.5\" fill=\"currentColor\" />\n <rect x=\"27\" y=\"14\" width=\"7\" height=\"12\" rx=\"3.5\" fill=\"currentColor\" />\n </>\n ),\n // An arrow leaving through the top-right: the shaft and the elbow of its head,\n // both stroke width 4 with round caps and joins.\n outbound: (\n <>\n <line x1=\"8\" y1=\"32\" x2=\"32\" y2=\"8\" stroke=\"currentColor\" strokeWidth=\"4\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n <path d=\"M16 8 H32 V24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"4\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n </>\n ),\n}\n\nfunction isProductSlug(slug: string): slug is ProductSlug {\n return (PRODUCT_SLUGS as readonly string[]).includes(slug)\n}\n\nexport interface ProductMarkProps {\n /**\n * Identity's product slug. An unknown one -- a product the served registry\n * carries but this package has no artwork for yet -- draws the bare tile\n * rather than throwing (ADR-0003's safe degrade), so the registry can run\n * ahead of a mark release.\n */\n slug: string\n /** Rendered width and height in px. Defaults to 20, the Wallet's own default. */\n size?: number\n /**\n * Dims the tile for a product the account is not entitled to -- the switcher's\n * unowned rows need it. It swaps the **ground only**, to --color-neutral-300;\n * the glyph stays white. Fading the glyph too would cost the shape the one\n * thing telling the four marks apart, and it is a prop rather than a wrapper\n * `opacity-40 grayscale` filter because that lands on a mid-grey visibly\n * darker than the grey Paper draws.\n *\n * **Only use this where the mark is decorative.** White on --color-neutral-300\n * is 1.41:1, well under WCAG 1.4.11's 3:1, and the reason that is not a defect\n * is that the criterion covers graphics *required to understand the content* --\n * which this is not, while the mark is `aria-hidden`, the product's name sits\n * beside it as text, and entitlement is additionally stated in words (the\n * switcher's \"Not on plan\" tag). Combining `muted` with `title`, so the mark\n * stands alone as the sole carrier of a product's identity, removes every one\n * of those and makes the ratio bite. That combination is out of contract; no\n * call site in this package uses it. See ADR-0012, which also records why the\n * ground is not darkened and what to reach for if this is ever revisited.\n */\n muted?: boolean\n /**\n * An accessible name. Omit it -- the default -- and the mark is decorative:\n * `aria-hidden`, contributing nothing to the accessibility tree. That is the\n * right answer in the switcher, where the product's name sits beside it and a\n * name here would only make a screen reader say it twice. Pass one only where\n * the mark stands alone as the sole carrier of meaning.\n *\n * Passing this is what takes the mark out of `muted`'s contract -- see the\n * note there. A mark that carries meaning on its own has to be legible on its\n * own, and the muted ground is not.\n */\n title?: string\n /** Escape hatch for a consumer that needs to position the mark. */\n className?: string\n}\n\n/**\n * The tile-and-glyph mark for one Boost33 product -- a rounded --color-neutral-950\n * square with the product's glyph in --color-primary-foreground. The four marks\n * share one palette and are told apart by shape alone; there is zero brand\n * orange in any of them.\n */\nexport function ProductMark({ slug, size = DEFAULT_SIZE, muted, title, className }: ProductMarkProps) {\n const titleId = useId()\n const decorative = title == null\n const glyph = isProductSlug(slug) ? GLYPHS[slug] : null\n\n return (\n <svg\n className={className != null ? `${styles.mark} ${className}` : styles.mark}\n width={size}\n height={size}\n viewBox=\"0 0 40 40\"\n role={decorative ? undefined : 'img'}\n aria-hidden={decorative ? 'true' : undefined}\n aria-labelledby={decorative ? undefined : titleId}\n >\n {!decorative && <title id={titleId}>{title}</title>}\n {/* The tile. Fill set inline, next to the muted decision that chooses it;\n the corner radius comes from the class (see ProductMark.module.css). */}\n <rect\n className={styles.ground}\n width=\"40\"\n height=\"40\"\n style={{ fill: muted ? 'var(--color-neutral-300)' : 'var(--color-neutral-950)' }}\n />\n {glyph && (\n <g\n transform=\"translate(10 10) scale(0.5)\"\n style={{ color: 'var(--color-primary-foreground)' }}\n >\n {glyph}\n </g>\n )}\n </svg>\n )\n}\n"],"names":["DEFAULT_SIZE","PRODUCT_SLUGS","GLYPHS","jsxs","Fragment","jsx","isProductSlug","slug","ProductMark","size","muted","title","className","titleId","useId","decorative","glyph","styles"],"mappings":"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"}
1
+ {"version":3,"file":"ProductMark.js","sources":["../../../src/components/ProductMark/ProductMark.tsx"],"sourcesContent":["import { useId, type ReactNode } from 'react'\nimport styles from './ProductMark.module.css'\n\n/**\n * The size the mark renders at unless a caller says otherwise. 20px is the\n * Wallet's own default at its call sites, and the size the four\n * tests/baselines/productmark-*.png were captured at: a 20px tile in the 24px\n * gutter the specimen crop uses either side -> 68px CSS -> 136 device px at\n * deviceScaleFactor 2. Changing it moves four apps.\n *\n * There is deliberately no minimum-legible floor here, unlike Boost33Mark. That\n * floor is the monogram's alone -- the bowls of the B close up below ~20px. The\n * product marks have the opposite property: each is one path on a 40-unit grid\n * with a quarter-side corner, chosen so it renders correctly from Paper's 96px\n * down to 20px. Do not generalise Boost33Mark's floor into a shared rule.\n */\nconst DEFAULT_SIZE = 20\n\n/**\n * The slugs this package draws a product mark for, as one list. Everything else\n * keys off it: ProductSlug is derived from it, and GLYPHS is typed against it so\n * adding a slug without artwork is a compile error rather than a mark that\n * silently draws nothing.\n *\n * **It is no longer the whole of Identity's registry, and the difference is\n * deliberate.** Identity serves five slugs: these four plus `brokers` (B33-1158,\n * \"Boost Brokers\"). This package has no `brokers` glyph, so that row draws the\n * bare tile through the safe degrade below -- see \"The fifth slug\" underneath.\n *\n * **Transcribed from `boost33-identity/src/lib/productRegistry.ts`** -- its\n * `REGISTRY_SLUGS`, which is `PRODUCTS` from `src/config/products.ts` plus\n * `WALLET_SLUG`. That file is the source of truth and this is a copy of it in\n * another repository, which is a thing that drifts. It already did: until\n * B33-1043 this list read `wallet | score | voice | outbound` while this comment\n * asserted it was Identity's, so three of the four rows of a switcher fed\n * straight from `GET /api/v1/products` drew the bare tile (see `slug` below) and\n * nothing anywhere failed.\n *\n * Identity is not the side that renames. ADR-0003 records the shorter slugs as\n * considered and rejected: these match the `?app=` keys live campaign URLs\n * already carry, so renaming them means those links grant nothing on sign-up.\n *\n * `tests/product-mark.test.tsx` pins this list against a second transcription of\n * the same source, so the two repositories cannot part company in silence again.\n * That pin is what the old comment's assertion was standing in for, and it now\n * pins the gap as well as the overlap: it names `brokers` as the one served slug\n * with no glyph here, so a *sixth* product, or artwork landing for this one,\n * fails rather than passing quietly.\n *\n * The *order* is this package's own and not Identity's -- `REGISTRY_SLUGS` ends\n * with `wallet` where this begins with it. Nothing reads a render order off this\n * list: a switcher draws the order its consumer was served (`AppSwitcher`), and\n * the only thing this order sets is the left-to-right run of frames in the\n * ProductMark specimen.\n *\n * ── The fifth slug ───────────────────────────────────────────────────────────\n *\n * **`brokers` is absent because no Brokers artwork exists to lift, not because\n * nobody got to it.** The four glyphs below were taken verbatim out of\n * `boostledger/src/ui/primitives/icons.tsx`, whose own `GLYPHS` is typed\n * `Record<ProductName, ReactNode>` over exactly four display names. There is no\n * Brokers mark in that file, none in `boost33-brand-hub`, and none in this\n * repository. The hub renders the product as a text quick-link and no mark at\n * all.\n *\n * So drawing one here would not be transcribing a mark, it would be inventing\n * one -- and the comment on GLYPHS below records what that costs: PR #4 redrew\n * all four from a written description, got all four wrong, and reintroduced the\n * retired one-orange-element construction. An invented fifth mark is worse than\n * that was, because it has nothing to be caught against. A human captures the\n * first baseline at the fidelity gate, which would make the invention the\n * referent, and it looks equally settled either way afterwards (CLAUDE.md, \"an\n * agent that picks one and builds it has invented a referent\").\n *\n * The degrade is also the mildest it will ever be. Identity serves `brokers` as\n * `live: false` -- the space is still being built -- so the switcher row carries\n * the \"Soon\" tag beside the bare tile, which reads as a product that has not\n * arrived rather than as a drawing that failed to load. That stops being true\n * the day Brokers ships.\n *\n * **To close it:** get the mark drawn where the other four live\n * (`boostledger/src/ui/primitives/icons.tsx`), lift it here verbatim, add\n * `brokers` to this list, and let a human approve `productmark-brokers.png` at a\n * gate. Not in that order optional: this list is what GLYPHS is typed against,\n * so adding the slug without the artwork stops the build, which is the one part\n * of this that cannot go wrong quietly.\n */\nexport const PRODUCT_SLUGS = ['wallet', 'boostscore', 'boostvoice', 'boostoutbound'] as const\n\n/** One of the slugs this package draws a mark for. Not every slug Identity serves -- see above. */\nexport type ProductSlug = (typeof PRODUCT_SLUGS)[number]\n\n/**\n * The four product glyphs, lifted verbatim from `boostledger`'s\n * `src/ui/primitives/icons.tsx` (the `GLYPHS` record, as of `837d85d`) rather\n * than redrawn from any written description. Redrawing them from\n * `docs/mintlify/system/product-marks.mdx` is how PR #4 got all four wrong and\n * reintroduced the retired one-orange-element construction; if any document\n * disagrees with that file, that file wins. See B33-562.\n *\n * Each glyph is drawn on its own full 40-unit grid and inset into the middle 20\n * of the tile by the `translate(10 10) scale(0.5)` group below. The stroke\n * widths are right on that grid -- do not \"correct\" them to compensate for the\n * scale, the scale is what puts the stroke at the weight Paper draws. Every\n * element paints in `currentColor` (the tile's white glyph ink) or `none`;\n * there is no colour of any kind in here, which is what tells the four marks\n * apart by shape and never by hue.\n *\n * Typing this `Record<ProductSlug, ReactNode>` is load-bearing: a fifth slug\n * added to PRODUCT_SLUGS without a glyph here stops the build.\n */\nconst GLYPHS: Record<ProductSlug, ReactNode> = {\n // A credit card with its magnetic stripe: an outline card (fill none, so the\n // tile shows through) plus a filled stripe across it.\n wallet: (\n <>\n <rect x=\"5\" y=\"10\" width=\"30\" height=\"20\" rx=\"4\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"3\" />\n <rect x=\"5\" y=\"16\" width=\"30\" height=\"5\" fill=\"currentColor\" />\n </>\n ),\n // A gauge with the needle swung high: an arc, the needle, and the hub it\n // pivots on. Arc and needle stroke at width 4 with round caps.\n boostscore: (\n <>\n <path d=\"M6 26 A14 14 0 0 1 34 26\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"4\" strokeLinecap=\"round\" />\n <line x1=\"20\" y1=\"26\" x2=\"28\" y2=\"14\" stroke=\"currentColor\" strokeWidth=\"4\" strokeLinecap=\"round\" />\n <circle cx=\"20\" cy=\"26\" r=\"3\" fill=\"currentColor\" />\n </>\n ),\n // A soundwave, tall in the middle: three pill bars, all centred on y=20.\n boostvoice: (\n <>\n <rect x=\"6\" y=\"14\" width=\"7\" height=\"12\" rx=\"3.5\" fill=\"currentColor\" />\n <rect x=\"16.5\" y=\"4\" width=\"7\" height=\"32\" rx=\"3.5\" fill=\"currentColor\" />\n <rect x=\"27\" y=\"14\" width=\"7\" height=\"12\" rx=\"3.5\" fill=\"currentColor\" />\n </>\n ),\n // An arrow leaving through the top-right: the shaft and the elbow of its head,\n // both stroke width 4 with round caps and joins.\n boostoutbound: (\n <>\n <line x1=\"8\" y1=\"32\" x2=\"32\" y2=\"8\" stroke=\"currentColor\" strokeWidth=\"4\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n <path d=\"M16 8 H32 V24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"4\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n </>\n ),\n}\n\nfunction isProductSlug(slug: string): slug is ProductSlug {\n return (PRODUCT_SLUGS as readonly string[]).includes(slug)\n}\n\nexport interface ProductMarkProps {\n /**\n * Identity's product slug. An unknown one -- a product the served registry\n * carries but this package has no artwork for yet -- draws the bare tile\n * rather than throwing (ADR-0003's safe degrade), so the registry can run\n * ahead of a mark release.\n */\n slug: string\n /** Rendered width and height in px. Defaults to 20, the Wallet's own default. */\n size?: number\n /**\n * Dims the tile for a product the account is not entitled to -- the switcher's\n * unowned rows need it. It swaps the **ground only**, to --color-neutral-300;\n * the glyph stays white. Fading the glyph too would cost the shape the one\n * thing telling the four marks apart, and it is a prop rather than a wrapper\n * `opacity-40 grayscale` filter because that lands on a mid-grey visibly\n * darker than the grey Paper draws.\n *\n * **Only use this where the mark is decorative.** White on --color-neutral-300\n * is 1.41:1, well under WCAG 1.4.11's 3:1, and the reason that is not a defect\n * is that the criterion covers graphics *required to understand the content* --\n * which this is not, while the mark is `aria-hidden`, the product's name sits\n * beside it as text, and entitlement is additionally stated in words (the\n * switcher's \"Not on plan\" tag). Combining `muted` with `title`, so the mark\n * stands alone as the sole carrier of a product's identity, removes every one\n * of those and makes the ratio bite. That combination is out of contract; no\n * call site in this package uses it. See ADR-0012, which also records why the\n * ground is not darkened and what to reach for if this is ever revisited.\n */\n muted?: boolean\n /**\n * An accessible name. Omit it -- the default -- and the mark is decorative:\n * `aria-hidden`, contributing nothing to the accessibility tree. That is the\n * right answer in the switcher, where the product's name sits beside it and a\n * name here would only make a screen reader say it twice. Pass one only where\n * the mark stands alone as the sole carrier of meaning.\n *\n * Passing this is what takes the mark out of `muted`'s contract -- see the\n * note there. A mark that carries meaning on its own has to be legible on its\n * own, and the muted ground is not.\n */\n title?: string\n /** Escape hatch for a consumer that needs to position the mark. */\n className?: string\n}\n\n/**\n * The tile-and-glyph mark for one Boost33 product -- a rounded --color-neutral-950\n * square with the product's glyph in --color-primary-foreground. The four marks\n * share one palette and are told apart by shape alone; there is zero brand\n * orange in any of them.\n */\nexport function ProductMark({ slug, size = DEFAULT_SIZE, muted, title, className }: ProductMarkProps) {\n const titleId = useId()\n const decorative = title == null\n const glyph = isProductSlug(slug) ? GLYPHS[slug] : null\n\n return (\n <svg\n className={className != null ? `${styles.mark} ${className}` : styles.mark}\n width={size}\n height={size}\n viewBox=\"0 0 40 40\"\n role={decorative ? undefined : 'img'}\n aria-hidden={decorative ? 'true' : undefined}\n aria-labelledby={decorative ? undefined : titleId}\n >\n {!decorative && <title id={titleId}>{title}</title>}\n {/* The tile. Fill set inline, next to the muted decision that chooses it;\n the corner radius comes from the class (see ProductMark.module.css). */}\n <rect\n className={styles.ground}\n width=\"40\"\n height=\"40\"\n style={{ fill: muted ? 'var(--color-neutral-300)' : 'var(--color-neutral-950)' }}\n />\n {glyph && (\n <g\n transform=\"translate(10 10) scale(0.5)\"\n style={{ color: 'var(--color-primary-foreground)' }}\n >\n {glyph}\n </g>\n )}\n </svg>\n )\n}\n"],"names":["DEFAULT_SIZE","PRODUCT_SLUGS","GLYPHS","jsxs","Fragment","jsx","isProductSlug","slug","ProductMark","size","muted","title","className","titleId","useId","decorative","glyph","styles"],"mappings":"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"}
package/dist/style.css CHANGED
@@ -1 +1 @@
1
- ._sidebar_17kel_7{display:flex;flex-direction:column;box-sizing:border-box;width:var(--container-sidebar);height:100%;padding-inline:var(--spacing-3);border-right:1px solid var(--color-border);background:var(--color-background);font-family:var(--font-sans)}._sidebar_17kel_7[data-collapsed=true]{width:var(--container-sidebar-rail)}._sidebar_17kel_7>nav{flex-shrink:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;margin:calc(-1 * var(--spacing-1));padding:var(--spacing-1);scroll-padding:var(--spacing-1)}@media(prefers-reduced-motion:reduce){._sidebar_17kel_7>nav{scroll-behavior:auto}}._section_3sxsg_5{display:flex;flex-direction:column}._header_3sxsg_13{margin:0;padding:var(--spacing-2) var(--spacing-2_5);color:var(--color-foreground-muted);font-family:var(--font-sans);font-size:var(--text-2xs);font-weight:var(--font-weight-semibold);letter-spacing:var(--tracking-caps);text-transform:uppercase}._wordmark_1k88r_4{display:flex;align-items:center;gap:var(--spacing-2_5);font-family:var(--font-display)}._mark_1k88r_17{display:inline-flex;flex:0 0 var(--spacing-6);align-items:center;justify-content:center;width:var(--spacing-6);height:var(--spacing-6)}._mark_1k88r_17 svg{width:100%;height:100%}._name_1k88r_35{color:var(--color-foreground);font-family:var(--font-display);font-size:var(--text-lg);font-weight:var(--font-weight-semibold);letter-spacing:var(--tracking-tight)}._wordmark_1k88r_4[data-collapsed=true] ._name_1k88r_35{position:absolute;overflow:hidden;clip-path:inset(50%);width:1px;height:1px;white-space:nowrap}._wordmark_1k88r_4[data-collapsed=true]{justify-content:center}._tile_5vhrh_12{fill:var(--color-neutral-950)}._ring_5vhrh_16{fill:var(--color-primary)}._mark_5vhrh_24{display:block}._mark_1jt5g_6{display:block}._ground_1jt5g_17{rx:var(--radius-product-tile)}._row_16hr7_4{display:flex;align-items:center;width:100%;gap:var(--spacing-2_5);padding:var(--spacing-2) var(--spacing-2_5);border:none;border-radius:var(--radius-lg);background:transparent;font-family:var(--font-sans);text-align:left;text-decoration:none;cursor:pointer;transition:background-color var(--duration-fast) var(--easing-standard)}._row_16hr7_4:hover{background:var(--color-surface-muted)}._row_16hr7_4[data-active=true]{background:var(--color-surface)}._row_16hr7_4:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}._icon_16hr7_41{display:inline-flex;flex:0 0 var(--spacing-4);align-items:center;justify-content:center;width:var(--spacing-4);height:var(--spacing-4);color:var(--color-foreground-secondary)}._icon_16hr7_41 svg{width:100%;height:100%}._row_16hr7_4[data-active=true] ._icon_16hr7_41{color:var(--color-foreground)}._row_16hr7_4[data-collapsed=true]{justify-content:center}._label_16hr7_66{flex:1 1 auto;min-width:0;overflow:hidden;color:var(--color-foreground-secondary);font-size:var(--text-nav-label);font-weight:var(--font-weight-regular);text-overflow:ellipsis;white-space:nowrap}._row_16hr7_4[data-active=true] ._label_16hr7_66{color:var(--color-foreground);font-weight:var(--font-weight-semibold)}._row_16hr7_4[data-collapsed=true] ._label_16hr7_66{position:absolute;overflow:hidden;clip-path:inset(50%);width:1px;height:1px;white-space:nowrap}._trailingSlot_16hr7_92{display:flex;flex:0 0 var(--spacing-5_5);align-items:center;justify-content:center;width:var(--spacing-5_5);font-family:var(--font-sans);font-size:var(--text-nav-label)}._row_16hr7_4[data-collapsed=true] ._trailingSlot_16hr7_92{display:none}._count_16hr7_106{color:var(--color-foreground-muted)}._countUrgent_16hr7_110{color:var(--color-primary)}@media(prefers-reduced-motion:reduce){._row_16hr7_4{transition:none}}._chip_xyt7t_37{--chip-lift: 0px;position:relative;box-sizing:border-box;width:100%;padding:calc(var(--spacing-3) + var(--chip-lift));border:1px solid var(--color-border);border-radius:var(--radius-2xl);background:transparent;font-family:var(--font-sans)}._chip_xyt7t_37:not([data-unknown]):hover,._chip_xyt7t_37:not([data-unknown]):focus-within,._chip_xyt7t_37[data-threshold]{--chip-lift: var(--spacing-0_5);box-shadow:var(--shadow-overlay)}._chip_xyt7t_37:not([data-threshold]):not([data-unknown]):hover,._chip_xyt7t_37:not([data-threshold]):not([data-unknown]):focus-within{background:var(--color-surface)}._chip_xyt7t_37:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}._header_xyt7t_74{display:flex;align-items:center;gap:var(--spacing-2)}._chip_xyt7t_37:not([data-threshold]):not([data-unknown]) ._header_xyt7t_74{padding-right:var(--spacing-topup-reserve)}._name_xyt7t_83{min-width:0;overflow:hidden;color:var(--color-foreground-muted);font-size:var(--text-xs);font-weight:var(--font-weight-medium);line-height:var(--leading-tight);text-overflow:ellipsis;white-space:nowrap}._pill_xyt7t_162{padding:var(--spacing-0_5) var(--spacing-2);border-radius:var(--radius-full);background:var(--color-warning-subtle);color:var(--color-warning);font-size:var(--text-2xs);font-weight:var(--font-weight-semibold)}._figure_xyt7t_171{display:flex;align-items:baseline;gap:var(--spacing-1_5);margin-top:calc(var(--spacing-2_5) + var(--chip-lift))}._amount_xyt7t_186{color:var(--color-foreground);font-family:var(--font-mono);font-size:var(--text-2xl);font-weight:var(--font-weight-bold);letter-spacing:var(--tracking-tight);line-height:var(--leading-none)}._unit_xyt7t_195{color:var(--color-foreground-muted);font-size:var(--text-xs)}._retry_xyt7t_200{width:var(--spacing-3);height:var(--spacing-3);color:var(--color-foreground-muted)}._bar_xyt7t_209{display:flex;overflow:hidden;width:100%;height:calc(var(--spacing-1_25) + var(--chip-lift));margin-top:calc(var(--spacing-2_5) + var(--chip-lift));border-radius:var(--radius-full)}._bar_xyt7t_209[data-tone=rest]{background:var(--color-surface-muted)}._bar_xyt7t_209[data-tone=threshold]{background:var(--color-warning-subtle)}._bar_xyt7t_209[data-tone=unknown]{background:var(--color-surface-muted)}._segmentGranted_xyt7t_230,._segmentPurchased_xyt7t_231{height:100%;transition:width var(--duration-fast) var(--easing-standard)}._segmentGranted_xyt7t_230{background:var(--color-grant)}._segmentPurchased_xyt7t_231{background:var(--color-purchased)}._details_xyt7t_251{display:none}._chip_xyt7t_37:hover ._details_xyt7t_251,._chip_xyt7t_37:focus-within ._details_xyt7t_251{display:block}._split_xyt7t_260{display:flex;flex-direction:column;gap:var(--spacing-1);margin:var(--spacing-2_5) 0 0}._splitRow_xyt7t_267{display:flex;align-items:center;justify-content:space-between}._splitLabel_xyt7t_273{display:flex;align-items:center;gap:var(--spacing-1_5);color:var(--color-foreground);font-size:var(--text-xs)}._splitAmount_xyt7t_281{margin:0;color:var(--color-foreground);font-family:var(--font-mono);font-size:var(--text-xs);font-variant-numeric:tabular-nums}._dotGranted_xyt7t_289,._dotPurchased_xyt7t_290{width:var(--spacing-1_5);height:var(--spacing-1_5);border-radius:var(--radius-full)}._dotGranted_xyt7t_289{background:var(--color-grant)}._dotPurchased_xyt7t_290{background:var(--color-purchased)}._footer_xyt7t_304{margin:var(--spacing-3) 0 0;padding-top:var(--spacing-1);border-top:1px solid var(--color-border);color:var(--color-foreground-muted);font-size:var(--text-2xs)}._burnNote_xyt7t_312{margin:var(--spacing-2_5) 0 0;color:var(--color-foreground-secondary);font-size:var(--text-2xs);line-height:var(--leading-snug)}._topUp_xyt7t_87{position:absolute;top:calc(var(--spacing-3) + var(--chip-lift));right:calc(var(--spacing-3) + var(--chip-lift));padding:0;border:none;background:transparent;color:var(--color-primary);font-family:var(--font-sans);font-size:var(--text-xs);font-weight:var(--font-weight-semibold);opacity:0;cursor:pointer;transition:opacity var(--duration-fast) var(--easing-standard)}._chip_xyt7t_37:hover ._topUp_xyt7t_87,._chip_xyt7t_37:focus-within ._topUp_xyt7t_87,._topUp_xyt7t_87:focus-visible{opacity:1}._topUp_xyt7t_87:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}._topUp_xyt7t_87[data-grown=true]{position:static;width:100%;margin-top:var(--spacing-2_5);padding:var(--spacing-2);border-radius:var(--radius-lg);background:var(--color-primary);color:var(--color-primary-foreground);font-size:var(--text-sm);opacity:1}._topUp_xyt7t_87[data-grown=true]:hover{background:var(--color-primary-hover)}._rail_xyt7t_378{--chip-lift: 0px;position:relative;display:flex;box-sizing:border-box;width:100%;flex-direction:column;align-items:center;gap:var(--spacing-2);padding:var(--spacing-2);font-family:var(--font-sans)}._railIcon_xyt7t_392{width:var(--spacing-6);height:var(--spacing-6);color:var(--color-foreground-secondary)}._railFigure_xyt7t_398{color:var(--color-foreground);font-family:var(--font-mono);font-size:var(--text-sm);font-weight:var(--font-weight-semibold);font-variant-numeric:tabular-nums}._rail_xyt7t_378 ._bar_xyt7t_209{height:var(--spacing-0_5);margin-top:0}._srOnly_xyt7t_411{position:absolute;overflow:hidden;clip-path:inset(50%);width:1px;height:1px;white-space:nowrap}@media(prefers-reduced-motion:reduce){._segmentGranted_xyt7t_230,._segmentPurchased_xyt7t_231,._topUp_xyt7t_87{transition:none}}._bar_r1jjn_6{display:flex;align-items:center;justify-content:space-between;gap:var(--spacing-4);box-sizing:border-box;height:var(--spacing-12);padding:0 var(--spacing-8);border-bottom:1px solid var(--color-border);background:var(--color-surface);font-family:var(--font-sans)}._breadcrumb_r1jjn_19{display:flex;min-width:0;align-items:center;gap:var(--spacing-2);overflow:hidden;color:var(--color-foreground);font-size:var(--text-sm);font-weight:var(--font-weight-medium);text-overflow:ellipsis;white-space:nowrap}._actions_r1jjn_32{display:flex;flex:0 0 auto;align-items:center;gap:var(--spacing-2)}._header_i17td_42{display:flex;align-items:center;justify-content:space-between;gap:var(--spacing-8);padding:var(--spacing-4_5) var(--spacing-8) var(--spacing-4);background:var(--color-surface);font-family:var(--font-sans)}._headings_i17td_52{display:flex;min-width:0;flex-direction:column;gap:var(--spacing-1)}._title_i17td_33{margin:0;color:var(--color-foreground);font-family:var(--font-display);font-size:var(--text-2xl);font-weight:var(--font-weight-semibold);letter-spacing:var(--tracking-tight);line-height:var(--leading-tight)}._subtitle_i17td_74{margin:0;color:var(--color-foreground-secondary);font-size:var(--text-base);font-weight:var(--font-weight-regular);line-height:var(--leading-snug)}._actions_i17td_82{display:flex;flex:0 0 auto;align-items:center;gap:var(--spacing-2)}._shell_19o9q_10{display:flex;height:100%;background:var(--color-surface);font-family:var(--font-sans)}._wordmark_19o9q_20{padding:var(--spacing-3) var(--spacing-2_5) var(--spacing-5)}._switcher_19o9q_26{padding-bottom:var(--spacing-4)}._foot_19o9q_33{display:flex;flex-direction:column;gap:var(--spacing-4);margin-block-start:auto;padding-block-start:var(--spacing-4)}._nav_19o9q_45{display:flex;flex-direction:column;gap:var(--spacing-4)}._main_19o9q_54{display:flex;min-width:0;flex:1;flex-direction:column}._content_19o9q_65{flex:1;min-height:0;overflow-y:auto;background:var(--color-surface)}:root{--color-neutral-0: #FFFFFF;--color-neutral-50: #F7F7F8;--color-neutral-100: #EFEFF1;--color-neutral-200: #E5E5EA;--color-neutral-300: #D9D9D9;--color-neutral-400: #C1C1C2;--color-neutral-500: #8E8E93;--color-neutral-600: #6B6B6B;--color-neutral-700: #525252;--color-neutral-800: #3A3A3C;--color-neutral-900: #292929;--color-neutral-950: #1D1D1B;--color-neutral-1000: #000000;--color-orange-50: #FDF3EE;--color-orange-100: #FBE2D5;--color-orange-200: #F7C3A8;--color-orange-300: #F0A173;--color-orange-400: #EB7147;--color-orange-500: #E9521E;--color-orange-600: #C2410C;--color-orange-700: #9A3412;--color-sand-100: #FBF3E4;--color-sand-300: #F3D6AC;--color-sand-600: #8A6510;--color-green-100: #DDF0E4;--color-green-600: #157F3D;--color-crimson-100: #F7DEE1;--color-crimson-600: #A3182B;--color-blue-100: #DEE6FA;--color-blue-600: #2A5BD7;--color-background: var(--color-neutral-50);--color-surface: var(--color-neutral-0);--color-surface-muted: var(--color-neutral-100);--color-border: var(--color-neutral-200);--color-border-strong: var(--color-neutral-300);--color-foreground: var(--color-neutral-950);--color-foreground-secondary: var(--color-neutral-700);--color-foreground-muted: var(--color-neutral-600);--color-foreground-disabled: var(--color-neutral-400);--color-primary: var(--color-orange-500);--color-primary-hover: var(--color-orange-600);--color-primary-foreground: var(--color-neutral-0);--color-primary-subtle: var(--color-orange-50);--color-primary-border: var(--color-orange-200);--color-success: var(--color-green-600);--color-success-subtle: var(--color-green-100);--color-warning: var(--color-sand-600);--color-warning-subtle: var(--color-sand-300);--color-danger: var(--color-crimson-600);--color-danger-subtle: var(--color-crimson-100);--color-info: var(--color-blue-600);--color-info-subtle: var(--color-blue-100);--color-grant: var(--color-sand-600);--color-purchased: var(--color-neutral-950);--shadow-overlay-color: rgb(29 29 27 / 20%);--font-display: var(--boost33-font-display, Outfit), ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;--font-sans: var(--boost33-font-sans, Inter), ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;--font-mono: var(--boost33-font-mono, "Geist Mono"), ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;--text-3xs: 9px;--text-2xs: 11px;--text-xs: 12px;--text-sm: 13px;--text-base: 15px;--text-md: 16px;--text-lg: 18px;--text-xl: 20px;--text-2xl: 24px;--text-3xl: 30px;--text-4xl: 36px;--text-5xl: 46px;--text-6xl: 60px;--text-7xl: 78px;--text-nav-label: 14px;--font-weight-light: 300;--font-weight-regular: 400;--font-weight-medium: 500;--font-weight-semibold: 600;--font-weight-bold: 700;--tracking-tighter: -.03em;--tracking-tight: -.02em;--tracking-normal: 0em;--tracking-wide: .04em;--tracking-wider: .09em;--tracking-caps: .05em;--leading-none: 100%;--leading-tight: 120%;--leading-snug: 135%;--leading-normal: 150%;--leading-relaxed: 170%;--breakpoint-mobile: 390px;--breakpoint-desktop: 1440px;--container-sidebar: 264px;--container-sidebar-rail: 56px;--container-content: 1440px;--spacing-1: 4px;--spacing-2: 8px;--spacing-3: 12px;--spacing-4: 16px;--spacing-4_5: 18px;--spacing-5: 20px;--spacing-6: 24px;--spacing-7: 28px;--spacing-8: 32px;--spacing-10: 40px;--spacing-12: 48px;--spacing-16: 64px;--spacing-20: 80px;--spacing-2_5: 10px;--spacing-5_5: 22px;--spacing-1_5: 6px;--spacing-1_25: 5px;--spacing-0_5: 2px;--spacing-2_25: 9px;--spacing-topup-reserve: 68px;--radius-none: 0px;--radius-xs: 2px;--radius-sm: 3px;--radius-md: 4px;--radius-lg: 6px;--radius-xl: 8px;--radius-2xl: 12px;--radius-full: 9999px;--radius-product-tile: 25%}:root{--duration-fast: .12s;--easing-standard: cubic-bezier(.2, 0, 0, 1)}:root{--shadow-overlay: 0px 16px 40px -10px var(--shadow-overlay-color)}._root_chh8a_8{position:relative;font-family:var(--font-sans)}._trigger_chh8a_17{display:flex;box-sizing:border-box;width:100%;align-items:center;gap:var(--spacing-2);padding:var(--spacing-2_25) var(--spacing-2);border:1px solid var(--color-border-strong);border-radius:var(--radius-lg);background:transparent;font-family:var(--font-sans);cursor:pointer}._trigger_chh8a_17[aria-expanded=true]{background:var(--color-surface-muted)}._trigger_chh8a_17:focus-visible,._railTrigger_chh8a_46:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}._triggerName_chh8a_56{overflow:hidden;flex:1;color:var(--color-foreground);font-size:var(--text-nav-label);font-weight:var(--font-weight-semibold);text-align:left;text-overflow:ellipsis;white-space:nowrap}._chevron_chh8a_67{width:var(--spacing-3);height:var(--spacing-3);flex-shrink:0;color:var(--color-foreground-muted);transition:transform var(--duration-fast) var(--easing-standard)}._trigger_chh8a_17[aria-expanded=true] ._chevron_chh8a_67{transform:rotate(180deg)}._railTrigger_chh8a_46{display:flex;box-sizing:border-box;width:100%;min-height:var(--spacing-6);align-items:flex-start;justify-content:center;padding:0;border:none;background:transparent;cursor:pointer}._panel_chh8a_109{position:absolute;z-index:1;top:100%;left:0;width:var(--container-sidebar);box-sizing:border-box;margin-top:var(--spacing-0_5);padding:var(--spacing-2);border:1px solid var(--color-border);border-radius:var(--radius-2xl);background:var(--color-surface);box-shadow:var(--shadow-overlay)}._hubRow_chh8a_135,._productRow_chh8a_136,._accountRow_chh8a_137{display:flex;box-sizing:border-box;width:100%;align-items:center;gap:var(--spacing-2_5);padding:var(--spacing-2);border:none;border-radius:var(--radius-lg);background:transparent;color:var(--color-foreground);font-family:var(--font-sans);text-align:left;text-decoration:none;cursor:pointer}._hubRow_chh8a_135:hover,._productRow_chh8a_136:hover,._accountRow_chh8a_137:hover{background:var(--color-surface-muted)}._hubRow_chh8a_135:focus-visible,._productRow_chh8a_136:focus-visible,._accountRow_chh8a_137:focus-visible{outline:2px solid var(--color-primary);outline-offset:-2px}._products_chh8a_144{display:flex;flex-direction:column;gap:var(--spacing-0_5);margin:0;padding:0;list-style:none}._productRow_chh8a_136[data-current=true]{background:var(--color-surface-muted)}._productRow_chh8a_136[data-entitled=false] ._rowName_chh8a_193{color:var(--color-foreground-muted)}._accountRow_chh8a_137{padding-inline:var(--spacing-3)}._accountRow_chh8a_137 ._rowName_chh8a_193{font-size:var(--text-sm)}._accountRow_chh8a_137 ._rowSub_chh8a_213{font-size:var(--text-2xs)}._rowText_chh8a_217{display:flex;min-width:0;flex-direction:column}._hubRow_chh8a_135 ._rowText_chh8a_217{gap:var(--spacing-0_5)}._rowName_chh8a_193{overflow:hidden;color:var(--color-foreground);font-size:var(--text-nav-label);font-weight:var(--font-weight-regular);text-overflow:ellipsis;white-space:nowrap}._productRow_chh8a_136 ._rowName_chh8a_193{flex:1}._rowSub_chh8a_213{overflow:hidden;color:var(--color-foreground-muted);font-size:var(--text-xs);text-overflow:ellipsis;white-space:nowrap}._hubMark_chh8a_260{width:var(--spacing-5);height:var(--spacing-5);flex-shrink:0;color:var(--color-foreground)}._tag_chh8a_269{flex-shrink:0;margin-left:auto;padding:var(--spacing-0_5) var(--spacing-1_5);border-radius:var(--radius-full);background:var(--color-surface-muted);color:var(--color-foreground-muted);font-size:var(--text-2xs);font-weight:var(--font-weight-medium)}._check_chh8a_280{width:var(--spacing-4);height:var(--spacing-4);flex-shrink:0;margin-left:auto;color:var(--color-foreground)}._avatar_chh8a_293{display:grid;width:var(--spacing-6);height:var(--spacing-6);flex-shrink:0;border-radius:var(--radius-lg);background:var(--color-surface-muted);color:var(--color-foreground-secondary);font-size:var(--text-3xs);font-weight:var(--font-weight-semibold);place-items:center}._chevronRight_chh8a_306{width:var(--spacing-4);height:var(--spacing-4);flex-shrink:0;margin-left:auto;color:var(--color-foreground-muted)}._divider_chh8a_327{margin:var(--spacing-2_25) 0 var(--spacing-2);border:none;border-top:1px solid var(--color-border)}._products_chh8a_144+._divider_chh8a_327{margin:var(--spacing-1_5) 0 var(--spacing-1)}@media(prefers-reduced-motion:reduce){._chevron_chh8a_67{transition:none}}
1
+ ._sidebar_17kel_7{display:flex;flex-direction:column;box-sizing:border-box;width:var(--container-sidebar);height:100%;padding-inline:var(--spacing-3);border-right:1px solid var(--color-border);background:var(--color-background);font-family:var(--font-sans)}._sidebar_17kel_7[data-collapsed=true]{width:var(--container-sidebar-rail)}._sidebar_17kel_7>nav{flex-shrink:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;margin:calc(-1 * var(--spacing-1));padding:var(--spacing-1);scroll-padding:var(--spacing-1)}@media(prefers-reduced-motion:reduce){._sidebar_17kel_7>nav{scroll-behavior:auto}}._section_3sxsg_5{display:flex;flex-direction:column}._header_3sxsg_13{margin:0;padding:var(--spacing-2) var(--spacing-2_5);color:var(--color-foreground-muted);font-family:var(--font-sans);font-size:var(--text-2xs);font-weight:var(--font-weight-semibold);letter-spacing:var(--tracking-caps);text-transform:uppercase}._wordmark_1k88r_4{display:flex;align-items:center;gap:var(--spacing-2_5);font-family:var(--font-display)}._mark_1k88r_17{display:inline-flex;flex:0 0 var(--spacing-6);align-items:center;justify-content:center;width:var(--spacing-6);height:var(--spacing-6)}._mark_1k88r_17 svg{width:100%;height:100%}._name_1k88r_35{color:var(--color-foreground);font-family:var(--font-display);font-size:var(--text-lg);font-weight:var(--font-weight-semibold);letter-spacing:var(--tracking-tight)}._wordmark_1k88r_4[data-collapsed=true] ._name_1k88r_35{position:absolute;overflow:hidden;clip-path:inset(50%);width:1px;height:1px;white-space:nowrap}._wordmark_1k88r_4[data-collapsed=true]{justify-content:center}._tile_5vhrh_12{fill:var(--color-neutral-950)}._ring_5vhrh_16{fill:var(--color-primary)}._mark_5vhrh_24{display:block}._mark_1jt5g_6{display:block}._ground_1jt5g_17{rx:var(--radius-product-tile)}._row_16hr7_4{display:flex;align-items:center;width:100%;gap:var(--spacing-2_5);padding:var(--spacing-2) var(--spacing-2_5);border:none;border-radius:var(--radius-lg);background:transparent;font-family:var(--font-sans);text-align:left;text-decoration:none;cursor:pointer;transition:background-color var(--duration-fast) var(--easing-standard)}._row_16hr7_4:hover{background:var(--color-surface-muted)}._row_16hr7_4[data-active=true]{background:var(--color-surface)}._row_16hr7_4:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}._icon_16hr7_41{display:inline-flex;flex:0 0 var(--spacing-4);align-items:center;justify-content:center;width:var(--spacing-4);height:var(--spacing-4);color:var(--color-foreground-secondary)}._icon_16hr7_41 svg{width:100%;height:100%}._row_16hr7_4[data-active=true] ._icon_16hr7_41{color:var(--color-foreground)}._row_16hr7_4[data-collapsed=true]{justify-content:center}._label_16hr7_66{flex:1 1 auto;min-width:0;overflow:hidden;color:var(--color-foreground-secondary);font-size:var(--text-nav-label);font-weight:var(--font-weight-regular);text-overflow:ellipsis;white-space:nowrap}._row_16hr7_4[data-active=true] ._label_16hr7_66{color:var(--color-foreground);font-weight:var(--font-weight-semibold)}._row_16hr7_4[data-collapsed=true] ._label_16hr7_66{position:absolute;overflow:hidden;clip-path:inset(50%);width:1px;height:1px;white-space:nowrap}._trailingSlot_16hr7_92{display:flex;flex:0 0 var(--spacing-5_5);align-items:center;justify-content:center;width:var(--spacing-5_5);font-family:var(--font-sans);font-size:var(--text-nav-label)}._row_16hr7_4[data-collapsed=true] ._trailingSlot_16hr7_92{display:none}._count_16hr7_106{color:var(--color-foreground-muted)}._countUrgent_16hr7_110{color:var(--color-primary)}@media(prefers-reduced-motion:reduce){._row_16hr7_4{transition:none}}._chip_xyt7t_37{--chip-lift: 0px;position:relative;box-sizing:border-box;width:100%;padding:calc(var(--spacing-3) + var(--chip-lift));border:1px solid var(--color-border);border-radius:var(--radius-2xl);background:transparent;font-family:var(--font-sans)}._chip_xyt7t_37:not([data-unknown]):hover,._chip_xyt7t_37:not([data-unknown]):focus-within,._chip_xyt7t_37[data-threshold]{--chip-lift: var(--spacing-0_5);box-shadow:var(--shadow-overlay)}._chip_xyt7t_37:not([data-threshold]):not([data-unknown]):hover,._chip_xyt7t_37:not([data-threshold]):not([data-unknown]):focus-within{background:var(--color-surface)}._chip_xyt7t_37:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}._header_xyt7t_74{display:flex;align-items:center;gap:var(--spacing-2)}._chip_xyt7t_37:not([data-threshold]):not([data-unknown]) ._header_xyt7t_74{padding-right:var(--spacing-topup-reserve)}._name_xyt7t_83{min-width:0;overflow:hidden;color:var(--color-foreground-muted);font-size:var(--text-xs);font-weight:var(--font-weight-medium);line-height:var(--leading-tight);text-overflow:ellipsis;white-space:nowrap}._pill_xyt7t_162{padding:var(--spacing-0_5) var(--spacing-2);border-radius:var(--radius-full);background:var(--color-warning-subtle);color:var(--color-warning);font-size:var(--text-2xs);font-weight:var(--font-weight-semibold)}._figure_xyt7t_171{display:flex;align-items:baseline;gap:var(--spacing-1_5);margin-top:calc(var(--spacing-2_5) + var(--chip-lift))}._amount_xyt7t_186{color:var(--color-foreground);font-family:var(--font-mono);font-size:var(--text-2xl);font-weight:var(--font-weight-bold);letter-spacing:var(--tracking-tight);line-height:var(--leading-none)}._unit_xyt7t_195{color:var(--color-foreground-muted);font-size:var(--text-xs)}._retry_xyt7t_200{width:var(--spacing-3);height:var(--spacing-3);color:var(--color-foreground-muted)}._bar_xyt7t_209{display:flex;overflow:hidden;width:100%;height:calc(var(--spacing-1_25) + var(--chip-lift));margin-top:calc(var(--spacing-2_5) + var(--chip-lift));border-radius:var(--radius-full)}._bar_xyt7t_209[data-tone=rest]{background:var(--color-surface-muted)}._bar_xyt7t_209[data-tone=threshold]{background:var(--color-warning-subtle)}._bar_xyt7t_209[data-tone=unknown]{background:var(--color-surface-muted)}._segmentGranted_xyt7t_230,._segmentPurchased_xyt7t_231{height:100%;transition:width var(--duration-fast) var(--easing-standard)}._segmentGranted_xyt7t_230{background:var(--color-grant)}._segmentPurchased_xyt7t_231{background:var(--color-purchased)}._details_xyt7t_251{display:none}._chip_xyt7t_37:hover ._details_xyt7t_251,._chip_xyt7t_37:focus-within ._details_xyt7t_251{display:block}._split_xyt7t_260{display:flex;flex-direction:column;gap:var(--spacing-1);margin:var(--spacing-2_5) 0 0}._splitRow_xyt7t_267{display:flex;align-items:center;justify-content:space-between}._splitLabel_xyt7t_273{display:flex;align-items:center;gap:var(--spacing-1_5);color:var(--color-foreground);font-size:var(--text-xs)}._splitAmount_xyt7t_281{margin:0;color:var(--color-foreground);font-family:var(--font-mono);font-size:var(--text-xs);font-variant-numeric:tabular-nums}._dotGranted_xyt7t_289,._dotPurchased_xyt7t_290{width:var(--spacing-1_5);height:var(--spacing-1_5);border-radius:var(--radius-full)}._dotGranted_xyt7t_289{background:var(--color-grant)}._dotPurchased_xyt7t_290{background:var(--color-purchased)}._footer_xyt7t_304{margin:var(--spacing-3) 0 0;padding-top:var(--spacing-1);border-top:1px solid var(--color-border);color:var(--color-foreground-muted);font-size:var(--text-2xs)}._burnNote_xyt7t_312{margin:var(--spacing-2_5) 0 0;color:var(--color-foreground-secondary);font-size:var(--text-2xs);line-height:var(--leading-snug)}._topUp_xyt7t_87{position:absolute;top:calc(var(--spacing-3) + var(--chip-lift));right:calc(var(--spacing-3) + var(--chip-lift));padding:0;border:none;background:transparent;color:var(--color-primary);font-family:var(--font-sans);font-size:var(--text-xs);font-weight:var(--font-weight-semibold);opacity:0;cursor:pointer;transition:opacity var(--duration-fast) var(--easing-standard)}._chip_xyt7t_37:hover ._topUp_xyt7t_87,._chip_xyt7t_37:focus-within ._topUp_xyt7t_87,._topUp_xyt7t_87:focus-visible{opacity:1}._topUp_xyt7t_87:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}._topUp_xyt7t_87[data-grown=true]{position:static;width:100%;margin-top:var(--spacing-2_5);padding:var(--spacing-2);border-radius:var(--radius-lg);background:var(--color-primary);color:var(--color-primary-foreground);font-size:var(--text-sm);opacity:1}._topUp_xyt7t_87[data-grown=true]:hover{background:var(--color-primary-hover)}._rail_xyt7t_378{--chip-lift: 0px;position:relative;display:flex;box-sizing:border-box;width:100%;flex-direction:column;align-items:center;gap:var(--spacing-2);padding:var(--spacing-2);font-family:var(--font-sans)}._railIcon_xyt7t_392{width:var(--spacing-6);height:var(--spacing-6);color:var(--color-foreground-secondary)}._railFigure_xyt7t_398{color:var(--color-foreground);font-family:var(--font-mono);font-size:var(--text-sm);font-weight:var(--font-weight-semibold);font-variant-numeric:tabular-nums}._rail_xyt7t_378 ._bar_xyt7t_209{height:var(--spacing-0_5);margin-top:0}._srOnly_xyt7t_411{position:absolute;overflow:hidden;clip-path:inset(50%);width:1px;height:1px;white-space:nowrap}@media(prefers-reduced-motion:reduce){._segmentGranted_xyt7t_230,._segmentPurchased_xyt7t_231,._topUp_xyt7t_87{transition:none}}._bar_r1jjn_6{display:flex;align-items:center;justify-content:space-between;gap:var(--spacing-4);box-sizing:border-box;height:var(--spacing-12);padding:0 var(--spacing-8);border-bottom:1px solid var(--color-border);background:var(--color-surface);font-family:var(--font-sans)}._breadcrumb_r1jjn_19{display:flex;min-width:0;align-items:center;gap:var(--spacing-2);overflow:hidden;color:var(--color-foreground);font-size:var(--text-sm);font-weight:var(--font-weight-medium);text-overflow:ellipsis;white-space:nowrap}._actions_r1jjn_32{display:flex;flex:0 0 auto;align-items:center;gap:var(--spacing-2)}._header_i17td_42{display:flex;align-items:center;justify-content:space-between;gap:var(--spacing-8);padding:var(--spacing-4_5) var(--spacing-8) var(--spacing-4);background:var(--color-surface);font-family:var(--font-sans)}._headings_i17td_52{display:flex;min-width:0;flex-direction:column;gap:var(--spacing-1)}._title_i17td_33{margin:0;color:var(--color-foreground);font-family:var(--font-display);font-size:var(--text-2xl);font-weight:var(--font-weight-semibold);letter-spacing:var(--tracking-tight);line-height:var(--leading-tight)}._subtitle_i17td_74{margin:0;color:var(--color-foreground-secondary);font-size:var(--text-base);font-weight:var(--font-weight-regular);line-height:var(--leading-snug)}._actions_i17td_82{display:flex;flex:0 0 auto;align-items:center;gap:var(--spacing-2)}._shell_19o9q_10{display:flex;height:100%;background:var(--color-surface);font-family:var(--font-sans)}._wordmark_19o9q_20{padding:var(--spacing-3) var(--spacing-2_5) var(--spacing-5)}._switcher_19o9q_26{padding-bottom:var(--spacing-4)}._foot_19o9q_33{display:flex;flex-direction:column;gap:var(--spacing-4);margin-block-start:auto;padding-block-start:var(--spacing-4)}._nav_19o9q_45{display:flex;flex-direction:column;gap:var(--spacing-4)}._main_19o9q_54{display:flex;min-width:0;flex:1;flex-direction:column}._content_19o9q_65{flex:1;min-height:0;overflow-y:auto;background:var(--color-surface)}:root{--color-neutral-0: #FFFFFF;--color-neutral-50: #F7F7F8;--color-neutral-100: #EFEFF1;--color-neutral-200: #E5E5EA;--color-neutral-300: #D9D9D9;--color-neutral-400: #C1C1C2;--color-neutral-500: #8E8E93;--color-neutral-600: #6B6B6B;--color-neutral-700: #525252;--color-neutral-800: #3A3A3C;--color-neutral-900: #292929;--color-neutral-950: #1D1D1B;--color-neutral-1000: #000000;--color-orange-50: #FDF3EE;--color-orange-100: #FBE2D5;--color-orange-200: #F7C3A8;--color-orange-300: #F0A173;--color-orange-400: #EB7147;--color-orange-500: #E9521E;--color-orange-600: #C2410C;--color-orange-700: #9A3412;--color-sand-100: #FBF3E4;--color-sand-300: #F3D6AC;--color-sand-600: #8A6510;--color-green-100: #DDF0E4;--color-green-600: #157F3D;--color-crimson-100: #F7DEE1;--color-crimson-600: #A3182B;--color-blue-100: #DEE6FA;--color-blue-600: #2A5BD7;--color-background: var(--color-neutral-50);--color-surface: var(--color-neutral-0);--color-surface-muted: var(--color-neutral-100);--color-border: var(--color-neutral-200);--color-border-strong: var(--color-neutral-300);--color-foreground: var(--color-neutral-950);--color-foreground-secondary: var(--color-neutral-700);--color-foreground-muted: var(--color-neutral-600);--color-foreground-disabled: var(--color-neutral-400);--color-primary: var(--color-orange-500);--color-primary-hover: var(--color-orange-600);--color-primary-foreground: var(--color-neutral-0);--color-primary-subtle: var(--color-orange-50);--color-primary-border: var(--color-orange-200);--color-success: var(--color-green-600);--color-success-subtle: var(--color-green-100);--color-warning: var(--color-sand-600);--color-warning-subtle: var(--color-sand-300);--color-danger: var(--color-crimson-600);--color-danger-subtle: var(--color-crimson-100);--color-info: var(--color-blue-600);--color-info-subtle: var(--color-blue-100);--color-grant: var(--color-sand-600);--color-purchased: var(--color-neutral-950);--shadow-overlay-color: rgb(29 29 27 / 20%);--font-display: var(--boost33-font-display, Outfit), ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;--font-sans: var(--boost33-font-sans, Inter), ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;--font-mono: var(--boost33-font-mono, "Geist Mono"), ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;--text-3xs: 9px;--text-2xs: 11px;--text-xs: 12px;--text-sm: 13px;--text-base: 15px;--text-md: 16px;--text-lg: 18px;--text-xl: 20px;--text-2xl: 24px;--text-3xl: 30px;--text-4xl: 36px;--text-5xl: 46px;--text-6xl: 60px;--text-7xl: 78px;--text-nav-label: 14px;--font-weight-light: 300;--font-weight-regular: 400;--font-weight-medium: 500;--font-weight-semibold: 600;--font-weight-bold: 700;--tracking-tighter: -.03em;--tracking-tight: -.02em;--tracking-normal: 0em;--tracking-wide: .04em;--tracking-wider: .09em;--tracking-caps: .05em;--leading-none: 100%;--leading-tight: 120%;--leading-snug: 135%;--leading-normal: 150%;--leading-relaxed: 170%;--breakpoint-mobile: 390px;--breakpoint-desktop: 1440px;--container-sidebar: 264px;--container-sidebar-rail: 56px;--container-content: 1440px;--spacing-1: 4px;--spacing-2: 8px;--spacing-3: 12px;--spacing-4: 16px;--spacing-4_5: 18px;--spacing-5: 20px;--spacing-6: 24px;--spacing-7: 28px;--spacing-8: 32px;--spacing-10: 40px;--spacing-12: 48px;--spacing-16: 64px;--spacing-20: 80px;--spacing-2_5: 10px;--spacing-5_5: 22px;--spacing-1_5: 6px;--spacing-1_25: 5px;--spacing-0_5: 2px;--spacing-2_25: 9px;--spacing-topup-reserve: 68px;--radius-none: 0px;--radius-xs: 2px;--radius-sm: 3px;--radius-md: 4px;--radius-lg: 6px;--radius-xl: 8px;--radius-2xl: 12px;--radius-full: 9999px;--radius-product-tile: 25%}:root{--duration-fast: .12s;--easing-standard: cubic-bezier(.2, 0, 0, 1)}:root{--shadow-overlay: 0px 16px 40px -10px var(--shadow-overlay-color)}._root_1ih3v_8{position:relative;font-family:var(--font-sans)}._trigger_1ih3v_17{display:flex;box-sizing:border-box;width:100%;align-items:center;gap:var(--spacing-2);padding:var(--spacing-2_25) var(--spacing-2);border:1px solid var(--color-border-strong);border-radius:var(--radius-lg);background:transparent;font-family:var(--font-sans);cursor:pointer}._trigger_1ih3v_17[aria-expanded=true]{background:var(--color-surface-muted)}._trigger_1ih3v_17:focus-visible,._railTrigger_1ih3v_46:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}._triggerName_1ih3v_56{overflow:hidden;flex:1;color:var(--color-foreground);font-size:var(--text-nav-label);font-weight:var(--font-weight-semibold);text-align:left;text-overflow:ellipsis;white-space:nowrap}._chevron_1ih3v_67{width:var(--spacing-3);height:var(--spacing-3);flex-shrink:0;color:var(--color-foreground-muted);transition:transform var(--duration-fast) var(--easing-standard)}._trigger_1ih3v_17[aria-expanded=true] ._chevron_1ih3v_67{transform:rotate(180deg)}._railTrigger_1ih3v_46{display:flex;box-sizing:border-box;width:100%;min-height:var(--spacing-6);align-items:flex-start;justify-content:center;padding:0;border:none;background:transparent;cursor:pointer}._panel_1ih3v_109{position:absolute;z-index:1;top:100%;left:0;width:var(--container-sidebar);box-sizing:border-box;margin-top:var(--spacing-0_5);padding:var(--spacing-2);border:1px solid var(--color-border);border-radius:var(--radius-2xl);background:var(--color-surface);box-shadow:var(--shadow-overlay)}._hubRow_1ih3v_135,._productRow_1ih3v_136,._accountRow_1ih3v_137{display:flex;box-sizing:border-box;width:100%;align-items:center;gap:var(--spacing-2_5);padding:var(--spacing-2);border:none;border-radius:var(--radius-lg);background:transparent;color:var(--color-foreground);font-family:var(--font-sans);text-align:left;text-decoration:none;cursor:pointer}._hubRow_1ih3v_135:hover,._productRow_1ih3v_136:hover,._accountRow_1ih3v_137:hover{background:var(--color-surface-muted)}._hubRow_1ih3v_135:focus-visible,._productRow_1ih3v_136:focus-visible,._accountRow_1ih3v_137:focus-visible{outline:2px solid var(--color-primary);outline-offset:-2px}._products_1ih3v_144{display:flex;flex-direction:column;gap:var(--spacing-0_5);margin:0;padding:0;list-style:none}._productRow_1ih3v_136[data-current=true]{background:var(--color-surface-muted)}._productRow_1ih3v_136[data-entitled=false] ._rowName_1ih3v_193{color:var(--color-foreground-muted)}._accountRow_1ih3v_137{padding-inline:var(--spacing-3)}._accountRow_1ih3v_137 ._rowName_1ih3v_193{font-size:var(--text-sm)}._accountRow_1ih3v_137 ._rowSub_1ih3v_213{font-size:var(--text-2xs)}._rowText_1ih3v_217{display:flex;min-width:0;flex-direction:column}._hubRow_1ih3v_135 ._rowText_1ih3v_217{gap:var(--spacing-0_5)}._rowName_1ih3v_193{overflow:hidden;color:var(--color-foreground);font-size:var(--text-nav-label);font-weight:var(--font-weight-regular);text-overflow:ellipsis;white-space:nowrap}._productRow_1ih3v_136 ._rowName_1ih3v_193{flex:1}._rowSub_1ih3v_213{overflow:hidden;color:var(--color-foreground-muted);font-size:var(--text-xs);text-overflow:ellipsis;white-space:nowrap}._hubMark_1ih3v_260{width:var(--spacing-5);height:var(--spacing-5);flex-shrink:0;color:var(--color-foreground)}._tag_1ih3v_276{flex-shrink:0;margin-left:auto;padding:var(--spacing-0_5) var(--spacing-1_5);border-radius:var(--radius-full);background:var(--color-surface-muted);color:var(--color-foreground-muted);font-size:var(--text-2xs);font-weight:var(--font-weight-medium)}._check_1ih3v_287{width:var(--spacing-4);height:var(--spacing-4);flex-shrink:0;margin-left:auto;color:var(--color-foreground)}._avatar_1ih3v_300{display:grid;width:var(--spacing-6);height:var(--spacing-6);flex-shrink:0;border-radius:var(--radius-lg);background:var(--color-surface-muted);color:var(--color-foreground-secondary);font-size:var(--text-3xs);font-weight:var(--font-weight-semibold);place-items:center}._chevronRight_1ih3v_313{width:var(--spacing-4);height:var(--spacing-4);flex-shrink:0;margin-left:auto;color:var(--color-foreground-muted)}._divider_1ih3v_334{margin:var(--spacing-2_25) 0 var(--spacing-2);border:none;border-top:1px solid var(--color-border)}._products_1ih3v_144+._divider_1ih3v_334{margin:var(--spacing-1_5) 0 var(--spacing-1)}@media(prefers-reduced-motion:reduce){._chevron_1ih3v_67{transition:none}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boost33/ui",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "The app shell every Boost33 product renders inside.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",