@boost33/ui 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +5 -2
  2. package/dist/anchored-overlay.d.ts +86 -0
  3. package/dist/anchored-overlay.js +125 -0
  4. package/dist/anchored-overlay.js.map +1 -0
  5. package/dist/components/AppShell/AppShell.d.ts +80 -0
  6. package/dist/components/AppShell/AppShell.js +59 -0
  7. package/dist/components/AppShell/AppShell.js.map +1 -0
  8. package/dist/components/AppShell/AppShell.module.css.js +20 -0
  9. package/dist/components/AppShell/AppShell.module.css.js.map +1 -0
  10. package/dist/components/AppSwitcher/AppSwitcher.d.ts +96 -0
  11. package/dist/components/AppSwitcher/AppSwitcher.js +81 -0
  12. package/dist/components/AppSwitcher/AppSwitcher.js.map +1 -0
  13. package/dist/components/AppSwitcher/AppSwitcher.module.css.js +44 -0
  14. package/dist/components/AppSwitcher/AppSwitcher.module.css.js.map +1 -0
  15. package/dist/components/AppSwitcher/icons.d.ts +20 -0
  16. package/dist/components/AppSwitcher/icons.js +25 -0
  17. package/dist/components/AppSwitcher/icons.js.map +1 -0
  18. package/dist/components/AppSwitcher/labels.d.ts +60 -0
  19. package/dist/components/AppSwitcher/labels.js +16 -0
  20. package/dist/components/AppSwitcher/labels.js.map +1 -0
  21. package/dist/components/Boost33Mark/Boost33Mark.d.ts +65 -0
  22. package/dist/components/Boost33Mark/Boost33Mark.js +62 -0
  23. package/dist/components/Boost33Mark/Boost33Mark.js.map +1 -0
  24. package/dist/components/Boost33Mark/Boost33Mark.module.css.js +12 -0
  25. package/dist/components/Boost33Mark/Boost33Mark.module.css.js.map +1 -0
  26. package/dist/components/NavItem/NavItem.d.ts +18 -2
  27. package/dist/components/NavItem/NavItem.js +18 -0
  28. package/dist/components/NavItem/NavItem.js.map +1 -0
  29. package/dist/components/NavItem/NavItem.module.css.js +18 -0
  30. package/dist/components/NavItem/NavItem.module.css.js.map +1 -0
  31. package/dist/components/NavSection/NavSection.d.ts +12 -0
  32. package/dist/components/NavSection/NavSection.js +14 -0
  33. package/dist/components/NavSection/NavSection.js.map +1 -0
  34. package/dist/components/NavSection/NavSection.module.css.js +10 -0
  35. package/dist/components/NavSection/NavSection.module.css.js.map +1 -0
  36. package/dist/components/PageHeader/PageHeader.d.ts +10 -0
  37. package/dist/components/PageHeader/PageHeader.js +15 -0
  38. package/dist/components/PageHeader/PageHeader.js.map +1 -0
  39. package/dist/components/PageHeader/PageHeader.module.css.js +16 -0
  40. package/dist/components/PageHeader/PageHeader.module.css.js.map +1 -0
  41. package/dist/components/ProductMark/ProductMark.d.ts +63 -0
  42. package/dist/components/ProductMark/ProductMark.js +73 -0
  43. package/dist/components/ProductMark/ProductMark.js.map +1 -0
  44. package/dist/components/ProductMark/ProductMark.module.css.js +10 -0
  45. package/dist/components/ProductMark/ProductMark.module.css.js.map +1 -0
  46. package/dist/components/Sidebar/NavHeightBudgetWarning.d.ts +3 -0
  47. package/dist/components/Sidebar/NavHeightBudgetWarning.js +28 -0
  48. package/dist/components/Sidebar/NavHeightBudgetWarning.js.map +1 -0
  49. package/dist/components/Sidebar/Sidebar.d.ts +19 -0
  50. package/dist/components/Sidebar/Sidebar.js +14 -0
  51. package/dist/components/Sidebar/Sidebar.js.map +1 -0
  52. package/dist/components/Sidebar/Sidebar.module.css.js +8 -0
  53. package/dist/components/Sidebar/Sidebar.module.css.js.map +1 -0
  54. package/dist/components/TopBar/TopBar.d.ts +8 -0
  55. package/dist/components/TopBar/TopBar.js +12 -0
  56. package/dist/components/TopBar/TopBar.js.map +1 -0
  57. package/dist/components/TopBar/TopBar.module.css.js +12 -0
  58. package/dist/components/TopBar/TopBar.module.css.js.map +1 -0
  59. package/dist/components/WalletChip/WalletChip.d.ts +60 -0
  60. package/dist/components/WalletChip/WalletChip.js +68 -0
  61. package/dist/components/WalletChip/WalletChip.js.map +1 -0
  62. package/dist/components/WalletChip/WalletChip.module.css.js +56 -0
  63. package/dist/components/WalletChip/WalletChip.module.css.js.map +1 -0
  64. package/dist/components/WalletChip/format.d.ts +47 -0
  65. package/dist/components/WalletChip/format.js +29 -0
  66. package/dist/components/WalletChip/format.js.map +1 -0
  67. package/dist/components/WalletChip/icons.d.ts +8 -0
  68. package/dist/components/WalletChip/icons.js +18 -0
  69. package/dist/components/WalletChip/icons.js.map +1 -0
  70. package/dist/components/WalletChip/labels.d.ts +143 -0
  71. package/dist/components/WalletChip/labels.js +38 -0
  72. package/dist/components/WalletChip/labels.js.map +1 -0
  73. package/dist/components/Wordmark/Wordmark.d.ts +12 -0
  74. package/dist/components/Wordmark/Wordmark.js +12 -0
  75. package/dist/components/Wordmark/Wordmark.js.map +1 -0
  76. package/dist/components/Wordmark/Wordmark.module.css.js +12 -0
  77. package/dist/components/Wordmark/Wordmark.module.css.js.map +1 -0
  78. package/dist/index.d.ts +26 -2
  79. package/dist/index.js +41 -35
  80. package/dist/index.js.map +1 -1
  81. package/dist/nav-height-budget.d.ts +58 -1
  82. package/dist/nav-height-budget.js +38 -0
  83. package/dist/nav-height-budget.js.map +1 -0
  84. package/dist/style.css +1 -1
  85. package/dist/theme.css +189 -0
  86. package/package.json +17 -4
@@ -0,0 +1,29 @@
1
+ const e = "en-US", i = {
2
+ grouped: {},
3
+ oneDecimal: { minimumFractionDigits: 1, maximumFractionDigits: 1 }
4
+ }, m = /* @__PURE__ */ new Map(), s = /* @__PURE__ */ new Set();
5
+ function c(t, n) {
6
+ const o = `${n}|${t}`, a = m.get(o);
7
+ if (a) return a;
8
+ let r;
9
+ try {
10
+ r = new Intl.NumberFormat(t, i[n]);
11
+ } catch {
12
+ s.has(t) || (s.add(t), console.warn(
13
+ `@boost33/ui: WalletChip cannot format numbers in locale ${JSON.stringify(t)}; falling back to ${e}.`
14
+ )), r = new Intl.NumberFormat(e, i[n]);
15
+ }
16
+ return m.set(o, r), r;
17
+ }
18
+ function u(t, n = e) {
19
+ return c(n, "grouped").format(t);
20
+ }
21
+ function f(t, n = e) {
22
+ return c(n, "oneDecimal").format(t / 1e3);
23
+ }
24
+ export {
25
+ e as DEFAULT_WALLET_LOCALE,
26
+ u as formatTokens,
27
+ f as thousandsFigure
28
+ };
29
+ //# sourceMappingURL=format.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.js","sources":["../../../src/components/WalletChip/format.ts"],"sourcesContent":["// The two numbers the chip says out loud. Kept separate from the component so\n// the rounding and the separator -- the parts that carry meaning rather than\n// look -- are unit-tested without a render.\n//\n// ---------------------------------------------------------------------------\n// B33-876: why a `locale` string, and not consumer-supplied formatters\n// ---------------------------------------------------------------------------\n//\n// This module used to hardcode `toLocaleString('en-US')` and a literal `k`. The\n// first two consumers are French-default, and French writes `4 600` where this\n// wrote `4,600`, and `4,6 k` where this wrote `4.6k` -- in the largest glyphs on\n// the chip, and inside the `bar` and `railBalance` accessible names. `labels`\n// could not reach any of it: the label functions receive the figures *already\n// formatted*, by documented intent, so a consumer had `\"4,600 jetons\"` and no\n// hook.\n//\n// The ticket named two candidate shapes and picked neither: a `locale` prop, or\n// `formatTokens` / `abbreviateThousands` supplied by the consumer. The shape\n// chosen is **a `locale` prop** -- one BCP-47 language tag, defaulting to\n// `en-US`, driving `Intl.NumberFormat`. Written down here, prominently, because\n// it is a published API decision reaching four products on a version bump and\n// reversing it later should be cheap.\n//\n// 1. **A formatter prop is a function, and a function cannot cross the React\n// Server Component boundary.** B33-880 had just finished taking the functions\n// *out* of `labels` for exactly this reason: `AppShell.wallet` is a\n// `ReactNode` slot, so it is the consumer's own page -- under the App Router,\n// a Server Component by default -- that constructs `<WalletChip …/>`, and\n// React's Flight serializer throws on any function anywhere in the prop\n// graph, in production as well as in development. An API of injected\n// formatters would reintroduce that bug the same week it was fixed, and\n// reintroduce it as the *recommended* path. A language tag is a string: data\n// all the way down, and it survives the boundary intact.\n//\n// 2. **Numbers are not words.** `labels` takes whole sentences because word\n// order is not a property of English and cannot be reassembled from\n// fragments -- that is a fact about a consumer's sentence, which no library\n// can know. Grouping and the decimal separator are the opposite: they are\n// CLDR facts about a locale, the platform ships them for every locale, and\n// asking four products to each re-implement French grouping is asking four\n// products to each get it slightly wrong. So \"the `labels` prop is already\n// the injected shape, be consistent\" reverses once you read *why* `labels`\n// took that shape.\n//\n// 3. **It is one string with a default**, and a consumer that passes nothing\n// renders character-for-character what the chip rendered before the prop\n// existed. The five `walletchip-*` baselines cannot move.\n//\n// What `locale` deliberately does not own is the `k`. See `thousandsFigure`.\n\n/**\n * The language tag the chip formats in when the consumer names none. Today's\n * rendering, so the default path is byte-identical to the one the fidelity\n * baselines were captured from.\n */\nexport const DEFAULT_WALLET_LOCALE = 'en-US'\n\n/**\n * The two shapes a figure is asked for: the full balance, grouped; and one\n * decimal of a thousand for the rail.\n */\ntype FormatterKind = 'grouped' | 'oneDecimal'\n\nconst FORMATTER_OPTIONS: Record<FormatterKind, Intl.NumberFormatOptions> = {\n grouped: {},\n oneDecimal: { minimumFractionDigits: 1, maximumFractionDigits: 1 },\n}\n\n/**\n * `Intl.NumberFormat` instances, keyed by kind and tag.\n *\n * Constructing one is not free and the chip formats up to four figures per\n * render, on a component that sits in the sidebar of every screen. The set of\n * locales an app uses is small and long-lived, so a plain module-level Map is\n * the whole of the caching this needs.\n */\nconst formatters = new Map<string, Intl.NumberFormat>()\n\n/**\n * The tags already warned about, so a bad one warns once rather than on every\n * render of every chip.\n */\nconst warnedLocales = new Set<string>()\n\n/**\n * An `Intl.NumberFormat` for this tag, falling back to English if the tag is\n * malformed.\n *\n * `Intl.NumberFormat` throws a `RangeError` on a structurally invalid language\n * tag (`'en_US'`, `'français'`), and this chip sits in the sidebar of four\n * products: one consumer's typo in a config file would take the whole shell down\n * rather than mis-space a number. A tag the platform refuses degrades to the\n * English default and warns the developer -- the same instinct as B33-873, where\n * a missing translation key falls back to English instead of blanking the\n * header. The warning is `console`-only: developer-facing, never rendered, never\n * translated.\n *\n * An unknown-but-well-formed tag (`'zz-ZZ'`) does not throw; the platform\n * resolves it to its own best match, which is its business and not this\n * package's.\n */\nfunction formatterFor(locale: string, kind: FormatterKind): Intl.NumberFormat {\n const key = `${kind}|${locale}`\n const cached = formatters.get(key)\n if (cached) return cached\n\n let formatter: Intl.NumberFormat\n try {\n formatter = new Intl.NumberFormat(locale, FORMATTER_OPTIONS[kind])\n } catch {\n if (!warnedLocales.has(locale)) {\n warnedLocales.add(locale)\n console.warn(\n `@boost33/ui: WalletChip cannot format numbers in locale ${JSON.stringify(locale)}; falling back to ${DEFAULT_WALLET_LOCALE}.`,\n )\n }\n formatter = new Intl.NumberFormat(DEFAULT_WALLET_LOCALE, FORMATTER_OPTIONS[kind])\n }\n formatters.set(key, formatter)\n return formatter\n}\n\n/**\n * The full balance, thousands grouped the way the locale groups them:\n * 4600 -> `\"4,600\"` in `en-US`, `\"4 600\"` in `fr-FR`, `\"4.600\"` in `de-DE`.\n *\n * The `en-US` default is what `toLocaleString('en-US')` produced, character for\n * character -- it is the same CLDR data reached through the constructor that can\n * be cached.\n */\nexport function formatTokens(value: number, locale: string = DEFAULT_WALLET_LOCALE): string {\n return formatterFor(locale, 'grouped').format(value)\n}\n\n/**\n * The rail's short form: one decimal thousand, always. At 56px there is no room\n * for four digits and a separator, and \"4.6k\" still answers \"am I fine?\".\n *\n * Returns **the figure alone** -- `\"4.6\"` in `en-US`, `\"4,6\"` in `fr-FR`. The\n * mark that follows it is a word rather than a number (\"k\" in English, \"Tsd.\" in\n * German, \"ألف\" in Arabic), and French puts a space in front of it where English\n * does not, so it lives with the chip's other words, as `labels.railFigure`\n * (B33-876).\n *\n * Not `Intl.NumberFormat`'s own `notation: 'compact'`, for two concrete reasons.\n * It renders `en-US` as `\"4.6K\"`, a capital the baselines do not have. And\n * German does not abbreviate four digits at all, so it renders `\"4600,0\"` -- six\n * characters and a decimal, in a 56px rail, longer than the number it was\n * supposed to shorten. One decimal thousand is this package's decision about a\n * fixed-width rail, not CLDR's about a locale; the separator inside it is CLDR's,\n * and that is the part `locale` fixes.\n *\n * Two default-path differences from the `(value / 1000).toFixed(1)` this\n * replaced, both narrower than any state the baselines hold and both left\n * deliberately, but written down because they reach four products (B33-876):\n *\n * - **A balance landing on an exact half rounds the other way.** `toFixed`\n * rounds the double's exact binary expansion, so 150 tokens printed `0.1`;\n * `Intl` rounds the shortest decimal the double stands for, so it prints\n * `0.2`. `Intl` agrees with the reader here, and the old answer was a float\n * artefact rather than a decision.\n * - **A balance over a million is grouped.** `toFixed` gave `1234.6`, this\n * gives `1,234.6`. Neither fits a 56px rail; that is a rail question, not a\n * formatting one.\n */\nexport function thousandsFigure(value: number, locale: string = DEFAULT_WALLET_LOCALE): string {\n return formatterFor(locale, 'oneDecimal').format(value / 1000)\n}\n"],"names":["DEFAULT_WALLET_LOCALE","FORMATTER_OPTIONS","formatters","warnedLocales","formatterFor","locale","kind","key","cached","formatter","formatTokens","value","thousandsFigure"],"mappings":"AAuDO,MAAMA,IAAwB,SAQ/BC,IAAqE;AAAA,EACzE,SAAS,CAAA;AAAA,EACT,YAAY,EAAE,uBAAuB,GAAG,uBAAuB,EAAA;AACjE,GAUMC,wBAAiB,IAAA,GAMjBC,wBAAoB,IAAA;AAmB1B,SAASC,EAAaC,GAAgBC,GAAwC;AAC5E,QAAMC,IAAM,GAAGD,CAAI,IAAID,CAAM,IACvBG,IAASN,EAAW,IAAIK,CAAG;AACjC,MAAIC,EAAQ,QAAOA;AAEnB,MAAIC;AACJ,MAAI;AACF,IAAAA,IAAY,IAAI,KAAK,aAAaJ,GAAQJ,EAAkBK,CAAI,CAAC;AAAA,EACnE,QAAQ;AACN,IAAKH,EAAc,IAAIE,CAAM,MAC3BF,EAAc,IAAIE,CAAM,GACxB,QAAQ;AAAA,MACN,2DAA2D,KAAK,UAAUA,CAAM,CAAC,qBAAqBL,CAAqB;AAAA,IAAA,IAG/HS,IAAY,IAAI,KAAK,aAAaT,GAAuBC,EAAkBK,CAAI,CAAC;AAAA,EAClF;AACA,SAAAJ,EAAW,IAAIK,GAAKE,CAAS,GACtBA;AACT;AAUO,SAASC,EAAaC,GAAeN,IAAiBL,GAA+B;AAC1F,SAAOI,EAAaC,GAAQ,SAAS,EAAE,OAAOM,CAAK;AACrD;AAiCO,SAASC,EAAgBD,GAAeN,IAAiBL,GAA+B;AAC7F,SAAOI,EAAaC,GAAQ,YAAY,EAAE,OAAOM,IAAQ,GAAI;AAC/D;"}
@@ -0,0 +1,8 @@
1
+ /** The card mark shown on the rail, where there is no room for the word "Wallet". */
2
+ export declare function WalletMark({ className }: {
3
+ className?: string;
4
+ }): import("react").JSX.Element;
5
+ /** The retry affordance shown when the balance could not be confirmed. */
6
+ export declare function RetryMark({ className }: {
7
+ className?: string;
8
+ }): import("react").JSX.Element;
@@ -0,0 +1,18 @@
1
+ import { jsxs as t, jsx as r } from "react/jsx-runtime";
2
+ function n({ className: e }) {
3
+ return /* @__PURE__ */ t("svg", { className: e, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
4
+ /* @__PURE__ */ r("rect", { x: "3", y: "6", width: "18", height: "12", rx: "2" }),
5
+ /* @__PURE__ */ r("path", { d: "M3 10h18" })
6
+ ] });
7
+ }
8
+ function i({ className: e }) {
9
+ return /* @__PURE__ */ t("svg", { className: e, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
10
+ /* @__PURE__ */ r("path", { d: "M21 12a9 9 0 1 1-2.64-6.36" }),
11
+ /* @__PURE__ */ r("path", { d: "M21 3v4h-4" })
12
+ ] });
13
+ }
14
+ export {
15
+ i as RetryMark,
16
+ n as WalletMark
17
+ };
18
+ //# sourceMappingURL=icons.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"icons.js","sources":["../../../src/components/WalletChip/icons.tsx"],"sourcesContent":["// Small monochrome glyphs the chip draws in `currentColor` -- no fill token, so\n// the surrounding text colour carries them and the no-hardcoded-hex guard stays\n// satisfied. Decorative: the balance text is what carries meaning, so both are\n// aria-hidden.\n\n/** The card mark shown on the rail, where there is no room for the word \"Wallet\". */\nexport function WalletMark({ className }: { className?: string }) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.75\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n <rect x=\"3\" y=\"6\" width=\"18\" height=\"12\" rx=\"2\" />\n <path d=\"M3 10h18\" />\n </svg>\n )\n}\n\n/** The retry affordance shown when the balance could not be confirmed. */\nexport function RetryMark({ className }: { className?: string }) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.75\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M21 12a9 9 0 1 1-2.64-6.36\" />\n <path d=\"M21 3v4h-4\" />\n </svg>\n )\n}\n"],"names":["WalletMark","className","jsx","RetryMark"],"mappings":";AAMO,SAASA,EAAW,EAAE,WAAAC,KAAqC;AAChE,2BACG,OAAA,EAAI,WAAAA,GAAsB,SAAQ,aAAY,MAAK,QAAO,QAAO,gBAAe,aAAY,QAAO,eAAc,SAAQ,gBAAe,SAAQ,eAAY,QAC3J,UAAA;AAAA,IAAA,gBAAAC,EAAC,QAAA,EAAK,GAAE,KAAI,GAAE,KAAI,OAAM,MAAK,QAAO,MAAK,IAAG,IAAA,CAAI;AAAA,IAChD,gBAAAA,EAAC,QAAA,EAAK,GAAE,WAAA,CAAW;AAAA,EAAA,GACrB;AAEJ;AAGO,SAASC,EAAU,EAAE,WAAAF,KAAqC;AAC/D,2BACG,OAAA,EAAI,WAAAA,GAAsB,SAAQ,aAAY,MAAK,QAAO,QAAO,gBAAe,aAAY,QAAO,eAAc,SAAQ,gBAAe,SAAQ,eAAY,QAC3J,UAAA;AAAA,IAAA,gBAAAC,EAAC,QAAA,EAAK,GAAE,6BAAA,CAA6B;AAAA,IACrC,gBAAAA,EAAC,QAAA,EAAK,GAAE,aAAA,CAAa;AAAA,EAAA,GACvB;AAEJ;"}
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Every user-facing string the chip renders. Supplied whole -- the strings that
3
+ * carry a value take a `{placeholder}` template (or a function) rather than
4
+ * fragments, because word order is not a property of English and cannot be
5
+ * reassembled on the consumer's side.
6
+ *
7
+ * `grantExpiry`, `railFigure`, `railBalance` and `bar` each accept two shapes.
8
+ *
9
+ * A **template** is a plain string marking the value's position with a
10
+ * `{placeholder}` -- `'Granted expires {date} · burns first'`. It is data all
11
+ * the way down, so the whole `labels` object survives the React Server Component
12
+ * boundary and a Next App Router page can pass it. **This is the recommended
13
+ * form**, and the one the defaults use.
14
+ *
15
+ * A **function** of the same values still works unchanged. It is never
16
+ * serializable, so it only reaches the chip from a Client Component, but it is
17
+ * the escape hatch when the sentence needs logic a template cannot express --
18
+ * ICU plurals, an `Intl.ListFormat` call.
19
+ *
20
+ * In a template, a `{name}` the label does not define is left exactly as written
21
+ * -- a locale whose copy genuinely contains braces is not mangled, and a
22
+ * misspelled placeholder shows up in the rendering rather than silently
23
+ * blanking. A placeholder a template omits is simply not rendered: a locale that
24
+ * does not need a figure is allowed not to name it.
25
+ */
26
+ export interface WalletChipLabels {
27
+ /** Accessible name for the expanded chip's status region. Default "Wallet balance". */
28
+ region: string;
29
+ /** The chip header. Default "Wallet". */
30
+ name: string;
31
+ /** The unit rendered beside the figure. Default "tokens". */
32
+ unit: string;
33
+ /** Screen-reader-only text in the figure when the balance is unknown. Default "Balance unknown". */
34
+ balanceUnknown: string;
35
+ /** The granted side of the split -- granted burns first. Default "Granted". */
36
+ granted: string;
37
+ /** The purchased side of the split -- purchased never expires. Default "Purchased". */
38
+ purchased: string;
39
+ /**
40
+ * The footer, around the preformatted expiry date. Supplied whole so the
41
+ * consumer owns word order.
42
+ *
43
+ * Placeholder: `{date}`. Default (en) `Granted expires {date} · burns first`.
44
+ * A function form `(date) => string` is also accepted.
45
+ */
46
+ grantExpiry: string | ((date: string) => string);
47
+ /** The top-up button. Default "Top up". */
48
+ topUp: string;
49
+ /**
50
+ * The rail's abbreviated balance, around the one-decimal-thousand figure
51
+ * (e.g. "4.6"). Both the visible text in the 56px rail and the figure that
52
+ * `railBalance` is given.
53
+ *
54
+ * Placeholder: `{figure}`. Default (en) `{figure}k`. A function form
55
+ * `(figure) => string` is also accepted.
56
+ *
57
+ * The figure itself is formatted for `locale` -- "4.6" in `en-US`, "4,6" in
58
+ * `fr-FR` -- but the mark after it is a word, and locales disagree about it:
59
+ * French writes "4,6 k" with a space, German abbreviates a thousand "Tsd.".
60
+ * So the mark is a label like every other word the chip renders, and a
61
+ * template rather than a bare suffix, because a locale is entitled to put the
62
+ * mark somewhere other than the end (B33-876).
63
+ */
64
+ railFigure: string | ((figure: string) => string);
65
+ /**
66
+ * The rail group's accessible name, around the abbreviated balance (e.g. "4.6k").
67
+ *
68
+ * Placeholder: `{balance}`. Default (en) `Wallet balance {balance}`. A function
69
+ * form `(balance) => string` is also accepted.
70
+ */
71
+ railBalance: string | ((balance: string) => string);
72
+ /** The rail group's accessible name when the balance is unknown. Default "Wallet balance unknown". */
73
+ railBalanceUnknown: string;
74
+ /** Screen-reader-only marker on the rail when the balance is unknown. Default "unknown". */
75
+ railUnknown: string;
76
+ /**
77
+ * The bar's accessible label, around the three preformatted figures. Assembled
78
+ * whole rather than concatenated from fragments -- the shape that cannot be
79
+ * translated at all otherwise.
80
+ *
81
+ * Placeholders: `{granted}`, `{purchased}`, `{remaining}`. Default (en)
82
+ * `{granted} granted and {purchased} purchased of {remaining} remaining`. A
83
+ * function form `(granted, purchased, remaining) => string` is also accepted.
84
+ */
85
+ bar: string | ((granted: string, purchased: string, remaining: string) => string);
86
+ }
87
+ /**
88
+ * The English defaults. A consumer that passes nothing renders exactly what the
89
+ * chip rendered before this prop existed, so the wallet baselines do not move.
90
+ *
91
+ * **Every field is a string.** That is what makes `labels={defaultWalletChipLabels}`
92
+ * and `labels={{ ...defaultWalletChipLabels, name: 'Portefeuille' }}` -- the
93
+ * pattern the specimen shows and this package's own advice recommends -- legal
94
+ * from a Server Component. The four value-carrying defaults are templates, and
95
+ * the rendered output is character-for-character what the function defaults
96
+ * produced (B33-880).
97
+ *
98
+ * Frozen because it is a public export reaching four products on a version bump.
99
+ * `const` protects the binding, not the contents: without the freeze, a consumer
100
+ * aliasing it by reference -- `const mine = defaultWalletChipLabels; mine.name =
101
+ * 'Portefeuille'` -- would silently rename every chip in the process (B33-877).
102
+ */
103
+ export declare const defaultWalletChipLabels: Readonly<{
104
+ region: string;
105
+ name: string;
106
+ unit: string;
107
+ balanceUnknown: string;
108
+ granted: string;
109
+ purchased: string;
110
+ grantExpiry: string;
111
+ topUp: string;
112
+ railFigure: string;
113
+ railBalance: string;
114
+ railBalanceUnknown: string;
115
+ railUnknown: string;
116
+ bar: string;
117
+ }>;
118
+ /**
119
+ * The labels as the render consumes them: the four value-carrying fields
120
+ * normalised to functions, whichever shape the consumer supplied. The component
121
+ * calls `t.bar(…)` either way and never learns which form it was given, so the
122
+ * union costs the render nothing -- and, in particular, the lazy call that keeps
123
+ * a consumer's `bar` off the degraded unknown path (B33-875) still works.
124
+ */
125
+ export interface ResolvedWalletChipLabels extends Omit<WalletChipLabels, 'grantExpiry' | 'railFigure' | 'railBalance' | 'bar'> {
126
+ grantExpiry: (date: string) => string;
127
+ railFigure: (figure: string) => string;
128
+ railBalance: (balance: string) => string;
129
+ bar: (granted: string, purchased: string, remaining: string) => string;
130
+ }
131
+ /**
132
+ * Merge a consumer's partial `labels` onto the English defaults, treating an
133
+ * explicitly-`undefined` field as unsupplied, and normalise the four
134
+ * value-carrying fields to functions.
135
+ *
136
+ * A next-intl `t('...')` returns `undefined` on a missed key, so
137
+ * `{ name: undefined }` is what one missing translation out of thirteen looks like
138
+ * at the call site -- it must fall back to English, not overwrite the default
139
+ * with `undefined` and blank the header, unname the button, or crash on
140
+ * `t.bar(...)` (B33-873). A plain object spread copies the `undefined` across; a
141
+ * per-field skip does not.
142
+ */
143
+ export declare function resolveWalletChipLabels(labels?: Partial<WalletChipLabels>): ResolvedWalletChipLabels;
@@ -0,0 +1,38 @@
1
+ const c = Object.freeze({
2
+ region: "Wallet balance",
3
+ name: "Wallet",
4
+ unit: "tokens",
5
+ balanceUnknown: "Balance unknown",
6
+ granted: "Granted",
7
+ purchased: "Purchased",
8
+ grantExpiry: "Granted expires {date} · burns first",
9
+ topUp: "Top up",
10
+ railFigure: "{figure}k",
11
+ railBalance: "Wallet balance {balance}",
12
+ railBalanceUnknown: "Wallet balance unknown",
13
+ railUnknown: "unknown",
14
+ bar: "{granted} granted and {purchased} purchased of {remaining} remaining"
15
+ });
16
+ function r(t, e) {
17
+ return t.replace(
18
+ /\{(\w+)\}/g,
19
+ (n, a) => Object.hasOwn(e, a) ? e[a] ?? n : n
20
+ );
21
+ }
22
+ function o(t) {
23
+ const e = Object.fromEntries(
24
+ Object.entries(t ?? {}).filter(([, a]) => a !== void 0)
25
+ ), n = { ...c, ...e };
26
+ return {
27
+ ...n,
28
+ grantExpiry: (a) => typeof n.grantExpiry == "function" ? n.grantExpiry(a) : r(n.grantExpiry, { date: a }),
29
+ railFigure: (a) => typeof n.railFigure == "function" ? n.railFigure(a) : r(n.railFigure, { figure: a }),
30
+ railBalance: (a) => typeof n.railBalance == "function" ? n.railBalance(a) : r(n.railBalance, { balance: a }),
31
+ bar: (a, l, i) => typeof n.bar == "function" ? n.bar(a, l, i) : r(n.bar, { granted: a, purchased: l, remaining: i })
32
+ };
33
+ }
34
+ export {
35
+ c as defaultWalletChipLabels,
36
+ o as resolveWalletChipLabels
37
+ };
38
+ //# sourceMappingURL=labels.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"labels.js","sources":["../../../src/components/WalletChip/labels.ts"],"sourcesContent":["// The chip is the only component in the package that renders words. Every one\n// of them lives here as a prop with an English default, so a French-default\n// consumer (the first two are) can translate the chip without the package\n// taking an i18n dependency -- CLAUDE.md, \"a component here renders what it is\n// given\". This is the same argument grantExpires and burnNote already made,\n// applied to the strings that were still English literals in the render.\n//\n// Three of those strings carry a value, and word order is not a property of\n// English: \"Granted expires 30 Sep\" puts the date where English puts it, and a\n// translation cannot be reassembled from fragments on the consumer's side. The\n// shape that solves that and nothing else is a function -- and a function is\n// the one thing that cannot be a prop of a Client Component rendered from a\n// React Server Component. React's Flight serializer walks the whole prop graph\n// and throws on any function it finds, nested or not, in production as well as\n// in development:\n//\n// Functions cannot be passed directly to Client Components unless you\n// explicitly expose it by marking it with \"use server\".\n//\n// `AppShell.wallet` is a ReactNode slot, so it is the *consumer's page* that\n// constructs `<WalletChip …/>`, and a Server Component page is the default way\n// the shell is meant to be used. A labels object that cannot cross that\n// boundary is a labels object most consumers cannot pass (B33-880).\n//\n// So each of the three takes either shape. The string form spells the value's\n// position with a `{placeholder}`, which is data all the way down and crosses\n// the boundary intact; it is the form the defaults use and the form the\n// specimen and the CHANGELOG demonstrate. The function form still works\n// unchanged, for a Client Component consumer that wants a real function --\n// next-intl's ICU plurals, an Intl.ListFormat call -- and that is a consumer\n// already on the client, where functions are ordinary props.\n//\n// B33-876 added a fourth of the same kind, `railFigure`. The rail's balance is\n// a number the `locale` prop formats -- \"4.6\" or \"4,6\" -- followed by a mark\n// that is a word and not a number: English \"k\", German \"Tsd.\", and French with\n// a space in front of it that English does not have. Intl cannot supply it (its\n// own compact notation renders German four-digit balances unabbreviated, which\n// does not fit a 56px rail -- see format.ts), so it belongs here, in the same\n// template shape, for the same reason: the consumer owns where the mark goes.\n\n/**\n * Every user-facing string the chip renders. Supplied whole -- the strings that\n * carry a value take a `{placeholder}` template (or a function) rather than\n * fragments, because word order is not a property of English and cannot be\n * reassembled on the consumer's side.\n *\n * `grantExpiry`, `railFigure`, `railBalance` and `bar` each accept two shapes.\n *\n * A **template** is a plain string marking the value's position with a\n * `{placeholder}` -- `'Granted expires {date} · burns first'`. It is data all\n * the way down, so the whole `labels` object survives the React Server Component\n * boundary and a Next App Router page can pass it. **This is the recommended\n * form**, and the one the defaults use.\n *\n * A **function** of the same values still works unchanged. It is never\n * serializable, so it only reaches the chip from a Client Component, but it is\n * the escape hatch when the sentence needs logic a template cannot express --\n * ICU plurals, an `Intl.ListFormat` call.\n *\n * In a template, a `{name}` the label does not define is left exactly as written\n * -- a locale whose copy genuinely contains braces is not mangled, and a\n * misspelled placeholder shows up in the rendering rather than silently\n * blanking. A placeholder a template omits is simply not rendered: a locale that\n * does not need a figure is allowed not to name it.\n */\nexport interface WalletChipLabels {\n /** Accessible name for the expanded chip's status region. Default \"Wallet balance\". */\n region: string\n /** The chip header. Default \"Wallet\". */\n name: string\n /** The unit rendered beside the figure. Default \"tokens\". */\n unit: string\n /** Screen-reader-only text in the figure when the balance is unknown. Default \"Balance unknown\". */\n balanceUnknown: string\n /** The granted side of the split -- granted burns first. Default \"Granted\". */\n granted: string\n /** The purchased side of the split -- purchased never expires. Default \"Purchased\". */\n purchased: string\n /**\n * The footer, around the preformatted expiry date. Supplied whole so the\n * consumer owns word order.\n *\n * Placeholder: `{date}`. Default (en) `Granted expires {date} · burns first`.\n * A function form `(date) => string` is also accepted.\n */\n grantExpiry: string | ((date: string) => string)\n /** The top-up button. Default \"Top up\". */\n topUp: string\n /**\n * The rail's abbreviated balance, around the one-decimal-thousand figure\n * (e.g. \"4.6\"). Both the visible text in the 56px rail and the figure that\n * `railBalance` is given.\n *\n * Placeholder: `{figure}`. Default (en) `{figure}k`. A function form\n * `(figure) => string` is also accepted.\n *\n * The figure itself is formatted for `locale` -- \"4.6\" in `en-US`, \"4,6\" in\n * `fr-FR` -- but the mark after it is a word, and locales disagree about it:\n * French writes \"4,6 k\" with a space, German abbreviates a thousand \"Tsd.\".\n * So the mark is a label like every other word the chip renders, and a\n * template rather than a bare suffix, because a locale is entitled to put the\n * mark somewhere other than the end (B33-876).\n */\n railFigure: string | ((figure: string) => string)\n /**\n * The rail group's accessible name, around the abbreviated balance (e.g. \"4.6k\").\n *\n * Placeholder: `{balance}`. Default (en) `Wallet balance {balance}`. A function\n * form `(balance) => string` is also accepted.\n */\n railBalance: string | ((balance: string) => string)\n /** The rail group's accessible name when the balance is unknown. Default \"Wallet balance unknown\". */\n railBalanceUnknown: string\n /** Screen-reader-only marker on the rail when the balance is unknown. Default \"unknown\". */\n railUnknown: string\n /**\n * The bar's accessible label, around the three preformatted figures. Assembled\n * whole rather than concatenated from fragments -- the shape that cannot be\n * translated at all otherwise.\n *\n * Placeholders: `{granted}`, `{purchased}`, `{remaining}`. Default (en)\n * `{granted} granted and {purchased} purchased of {remaining} remaining`. A\n * function form `(granted, purchased, remaining) => string` is also accepted.\n */\n bar: string | ((granted: string, purchased: string, remaining: string) => string)\n}\n\n/**\n * The English defaults. A consumer that passes nothing renders exactly what the\n * chip rendered before this prop existed, so the wallet baselines do not move.\n *\n * **Every field is a string.** That is what makes `labels={defaultWalletChipLabels}`\n * and `labels={{ ...defaultWalletChipLabels, name: 'Portefeuille' }}` -- the\n * pattern the specimen shows and this package's own advice recommends -- legal\n * from a Server Component. The four value-carrying defaults are templates, and\n * the rendered output is character-for-character what the function defaults\n * produced (B33-880).\n *\n * Frozen because it is a public export reaching four products on a version bump.\n * `const` protects the binding, not the contents: without the freeze, a consumer\n * aliasing it by reference -- `const mine = defaultWalletChipLabels; mine.name =\n * 'Portefeuille'` -- would silently rename every chip in the process (B33-877).\n */\nexport const defaultWalletChipLabels = Object.freeze({\n region: 'Wallet balance',\n name: 'Wallet',\n unit: 'tokens',\n balanceUnknown: 'Balance unknown',\n granted: 'Granted',\n purchased: 'Purchased',\n grantExpiry: 'Granted expires {date} · burns first',\n topUp: 'Top up',\n railFigure: '{figure}k',\n railBalance: 'Wallet balance {balance}',\n railBalanceUnknown: 'Wallet balance unknown',\n railUnknown: 'unknown',\n bar: '{granted} granted and {purchased} purchased of {remaining} remaining',\n} satisfies WalletChipLabels)\n\n/**\n * The labels as the render consumes them: the four value-carrying fields\n * normalised to functions, whichever shape the consumer supplied. The component\n * calls `t.bar(…)` either way and never learns which form it was given, so the\n * union costs the render nothing -- and, in particular, the lazy call that keeps\n * a consumer's `bar` off the degraded unknown path (B33-875) still works.\n */\nexport interface ResolvedWalletChipLabels\n extends Omit<WalletChipLabels, 'grantExpiry' | 'railFigure' | 'railBalance' | 'bar'> {\n grantExpiry: (date: string) => string\n railFigure: (figure: string) => string\n railBalance: (balance: string) => string\n bar: (granted: string, purchased: string, remaining: string) => string\n}\n\n/**\n * Fill a template's `{placeholder}`s from `values`.\n *\n * A name not in `values` is left verbatim, so misspelled or deliberate braces\n * survive to the rendering rather than disappearing. The replacement runs\n * through a callback, so a `$` in a formatted figure is a `$` and not a\n * `String.prototype.replace` substitution pattern.\n */\nfunction fillTemplate(template: string, values: Record<string, string>): string {\n return template.replace(/\\{(\\w+)\\}/g, (whole, key: string) =>\n Object.hasOwn(values, key) ? (values[key] ?? whole) : whole,\n )\n}\n\n/**\n * Merge a consumer's partial `labels` onto the English defaults, treating an\n * explicitly-`undefined` field as unsupplied, and normalise the four\n * value-carrying fields to functions.\n *\n * A next-intl `t('...')` returns `undefined` on a missed key, so\n * `{ name: undefined }` is what one missing translation out of thirteen looks like\n * at the call site -- it must fall back to English, not overwrite the default\n * with `undefined` and blank the header, unname the button, or crash on\n * `t.bar(...)` (B33-873). A plain object spread copies the `undefined` across; a\n * per-field skip does not.\n */\nexport function resolveWalletChipLabels(labels?: Partial<WalletChipLabels>): ResolvedWalletChipLabels {\n const supplied = Object.fromEntries(\n Object.entries(labels ?? {}).filter(([, value]) => value !== undefined),\n ) as Partial<WalletChipLabels>\n const merged = { ...defaultWalletChipLabels, ...supplied }\n return {\n ...merged,\n grantExpiry: (date) =>\n typeof merged.grantExpiry === 'function'\n ? merged.grantExpiry(date)\n : fillTemplate(merged.grantExpiry, { date }),\n railFigure: (figure) =>\n typeof merged.railFigure === 'function'\n ? merged.railFigure(figure)\n : fillTemplate(merged.railFigure, { figure }),\n railBalance: (balance) =>\n typeof merged.railBalance === 'function'\n ? merged.railBalance(balance)\n : fillTemplate(merged.railBalance, { balance }),\n bar: (granted, purchased, remaining) =>\n typeof merged.bar === 'function'\n ? merged.bar(granted, purchased, remaining)\n : fillTemplate(merged.bar, { granted, purchased, remaining }),\n }\n}\n"],"names":["defaultWalletChipLabels","fillTemplate","template","values","whole","key","resolveWalletChipLabels","labels","supplied","value","merged","date","figure","balance","granted","purchased","remaining"],"mappings":"AA+IO,MAAMA,IAA0B,OAAO,OAAO;AAAA,EACnD,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,MAAM;AAAA,EACN,gBAAgB;AAAA,EAChB,SAAS;AAAA,EACT,WAAW;AAAA,EACX,aAAa;AAAA,EACb,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,oBAAoB;AAAA,EACpB,aAAa;AAAA,EACb,KAAK;AACP,CAA4B;AAyB5B,SAASC,EAAaC,GAAkBC,GAAwC;AAC9E,SAAOD,EAAS;AAAA,IAAQ;AAAA,IAAc,CAACE,GAAOC,MAC5C,OAAO,OAAOF,GAAQE,CAAG,IAAKF,EAAOE,CAAG,KAAKD,IAASA;AAAA,EAAA;AAE1D;AAcO,SAASE,EAAwBC,GAA8D;AACpG,QAAMC,IAAW,OAAO;AAAA,IACtB,OAAO,QAAQD,KAAU,EAAE,EAAE,OAAO,CAAC,CAAA,EAAGE,CAAK,MAAMA,MAAU,MAAS;AAAA,EAAA,GAElEC,IAAS,EAAE,GAAGV,GAAyB,GAAGQ,EAAA;AAChD,SAAO;AAAA,IACL,GAAGE;AAAA,IACH,aAAa,CAACC,MACZ,OAAOD,EAAO,eAAgB,aAC1BA,EAAO,YAAYC,CAAI,IACvBV,EAAaS,EAAO,aAAa,EAAE,MAAAC,GAAM;AAAA,IAC/C,YAAY,CAACC,MACX,OAAOF,EAAO,cAAe,aACzBA,EAAO,WAAWE,CAAM,IACxBX,EAAaS,EAAO,YAAY,EAAE,QAAAE,GAAQ;AAAA,IAChD,aAAa,CAACC,MACZ,OAAOH,EAAO,eAAgB,aAC1BA,EAAO,YAAYG,CAAO,IAC1BZ,EAAaS,EAAO,aAAa,EAAE,SAAAG,GAAS;AAAA,IAClD,KAAK,CAACC,GAASC,GAAWC,MACxB,OAAON,EAAO,OAAQ,aAClBA,EAAO,IAAII,GAASC,GAAWC,CAAS,IACxCf,EAAaS,EAAO,KAAK,EAAE,SAAAI,GAAS,WAAAC,GAAW,WAAAC,EAAA,CAAW;AAAA,EAAA;AAEpE;"}
@@ -0,0 +1,12 @@
1
+ import { ReactNode } from 'react';
2
+ export interface WordmarkProps {
3
+ /**
4
+ * The Boost33 mark -- the logo artwork, owned by its own ticket. Passed in
5
+ * rather than hardcoded here so the wordmark does not carry a second copy of
6
+ * the mark. On the rail it is all that shows.
7
+ */
8
+ mark?: ReactNode;
9
+ /** Sidebar-on-the-rail state. Hides the "Boost33" name; the mark stays. */
10
+ collapsed?: boolean;
11
+ }
12
+ export declare function Wordmark({ mark, collapsed }: WordmarkProps): import("react").JSX.Element;
@@ -0,0 +1,12 @@
1
+ import { jsxs as l, jsx as s } from "react/jsx-runtime";
2
+ import a from "./Wordmark.module.css.js";
3
+ function n({ mark: r, collapsed: e = !1 }) {
4
+ return /* @__PURE__ */ l("div", { className: a.wordmark, "data-collapsed": e ? "true" : void 0, children: [
5
+ r != null && /* @__PURE__ */ s("span", { className: a.mark, "aria-hidden": "true", children: r }),
6
+ /* @__PURE__ */ s("span", { className: a.name, children: "Boost33" })
7
+ ] });
8
+ }
9
+ export {
10
+ n as Wordmark
11
+ };
12
+ //# sourceMappingURL=Wordmark.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Wordmark.js","sources":["../../../src/components/Wordmark/Wordmark.tsx"],"sourcesContent":["import type { ReactNode } from 'react'\nimport styles from './Wordmark.module.css'\n\nexport interface WordmarkProps {\n /**\n * The Boost33 mark -- the logo artwork, owned by its own ticket. Passed in\n * rather than hardcoded here so the wordmark does not carry a second copy of\n * the mark. On the rail it is all that shows.\n */\n mark?: ReactNode\n /** Sidebar-on-the-rail state. Hides the \"Boost33\" name; the mark stays. */\n collapsed?: boolean\n}\n\n// The lockup at the top of the sidebar: the Boost33 mark and the \"Boost33\"\n// name beside it. On the rail the name drops to the accessibility tree and the\n// mark stands alone.\nexport function Wordmark({ mark, collapsed = false }: WordmarkProps) {\n return (\n <div className={styles.wordmark} data-collapsed={collapsed ? 'true' : undefined}>\n {mark != null && (\n <span className={styles.mark} aria-hidden=\"true\">\n {mark}\n </span>\n )}\n <span className={styles.name}>Boost33</span>\n </div>\n )\n}\n"],"names":["Wordmark","mark","collapsed","jsxs","styles","jsx"],"mappings":";;AAiBO,SAASA,EAAS,EAAE,MAAAC,GAAM,WAAAC,IAAY,MAAwB;AACnE,SACE,gBAAAC,EAAC,SAAI,WAAWC,EAAO,UAAU,kBAAgBF,IAAY,SAAS,QACnE,UAAA;AAAA,IAAAD,KAAQ,0BACN,QAAA,EAAK,WAAWG,EAAO,MAAM,eAAY,QACvC,UAAAH,EAAA,CACH;AAAA,IAEF,gBAAAI,EAAC,QAAA,EAAK,WAAWD,EAAO,MAAM,UAAA,UAAA,CAAO;AAAA,EAAA,GACvC;AAEJ;"}
@@ -0,0 +1,12 @@
1
+ const r = "_wordmark_1k88r_4", a = "_mark_1k88r_17", k = "_name_1k88r_35", m = {
2
+ wordmark: r,
3
+ mark: a,
4
+ name: k
5
+ };
6
+ export {
7
+ m as default,
8
+ a as mark,
9
+ k as name,
10
+ r as wordmark
11
+ };
12
+ //# sourceMappingURL=Wordmark.module.css.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Wordmark.module.css.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;"}
package/dist/index.d.ts CHANGED
@@ -1,4 +1,28 @@
1
+ export { Sidebar } from './components/Sidebar/Sidebar';
2
+ export type { SidebarProps } from './components/Sidebar/Sidebar';
3
+ export { NavSection } from './components/NavSection/NavSection';
4
+ export type { NavSectionProps } from './components/NavSection/NavSection';
5
+ export { Wordmark } from './components/Wordmark/Wordmark';
6
+ export type { WordmarkProps } from './components/Wordmark/Wordmark';
7
+ export { Boost33Mark, BOOST33_MARK_MIN_LEGIBLE_PX } from './components/Boost33Mark/Boost33Mark';
8
+ export type { Boost33MarkProps } from './components/Boost33Mark/Boost33Mark';
9
+ export { ProductMark, PRODUCT_SLUGS } from './components/ProductMark/ProductMark';
10
+ export type { ProductMarkProps, ProductSlug } from './components/ProductMark/ProductMark';
1
11
  export { NavItem } from './components/NavItem/NavItem';
2
12
  export type { NavItemProps } from './components/NavItem/NavItem';
3
- export { NAV_HEIGHT_BUDGET_PX, checkNavHeightBudget, measureNavHeight } from './nav-height-budget';
4
- export type { NavHeightBudget } from './nav-height-budget';
13
+ export { WalletChip } from './components/WalletChip/WalletChip';
14
+ export type { WalletChipProps } from './components/WalletChip/WalletChip';
15
+ export { defaultWalletChipLabels } from './components/WalletChip/labels';
16
+ export type { WalletChipLabels } from './components/WalletChip/labels';
17
+ export { TopBar } from './components/TopBar/TopBar';
18
+ export type { TopBarProps } from './components/TopBar/TopBar';
19
+ export { PageHeader } from './components/PageHeader/PageHeader';
20
+ export type { PageHeaderProps } from './components/PageHeader/PageHeader';
21
+ export { AppShell } from './components/AppShell/AppShell';
22
+ export type { AppShellProps, AppShellNavSection, AppShellNavItem } from './components/AppShell/AppShell';
23
+ export { AppSwitcher } from './components/AppSwitcher/AppSwitcher';
24
+ export type { AppSwitcherProps, AppSwitcherProduct, AppSwitcherHub, AppSwitcherAccount, } from './components/AppSwitcher/AppSwitcher';
25
+ export { defaultAppSwitcherLabels } from './components/AppSwitcher/labels';
26
+ export type { AppSwitcherLabels } from './components/AppSwitcher/labels';
27
+ export { NAV_HEIGHT_BUDGET_PX, NAV_SECTION_HEADER_PX, NAV_ITEM_PX, NAV_SECTION_GAP_PX, NAV_FOCUS_GUTTER_PX, NAV_SHELL_FIXED_PX, NAV_SHELL_PACKAGE_FIXED_PX, NAV_SHELL_CONSUMER_SLOTS_PX, checkNavHeightBudget, measureNavHeight, navContentHeight, navBudgetForViewport, } from './nav-height-budget';
28
+ export type { NavHeightBudget, NavRowCosts, NavSectionShape } from './nav-height-budget';
package/dist/index.js CHANGED
@@ -1,38 +1,44 @@
1
- import { jsx as o, jsxs as _, Fragment as f } from "react/jsx-runtime";
2
- const g = "_row_5f12t_4", m = "_label_5f12t_36", d = "_trailingSlot_5f12t_62", h = "_count_5f12t_76", p = "_countUrgent_5f12t_80", a = {
3
- row: g,
4
- label: m,
5
- trailingSlot: d,
6
- count: h,
7
- countUrgent: p
8
- };
9
- function w({ children: t, active: n = !1, collapsed: i = !1, count: e, countUrgent: u = !1, href: r, onClick: l }) {
10
- const c = {
11
- "data-active": n ? "true" : void 0,
12
- "data-collapsed": i ? "true" : void 0,
13
- "aria-current": n ? "page" : void 0
14
- }, s = /* @__PURE__ */ _(f, { children: [
15
- /* @__PURE__ */ o("span", { className: a.label, children: t }),
16
- /* @__PURE__ */ o("span", { className: a.trailingSlot, children: e != null && /* @__PURE__ */ o("span", { className: u ? a.countUrgent : a.count, children: e }) })
17
- ] });
18
- return r ? /* @__PURE__ */ o("a", { className: a.row, href: r, onClick: l, ...c, children: s }) : /* @__PURE__ */ o("button", { type: "button", className: a.row, onClick: l, ...c, children: s });
19
- }
20
- const N = 700;
21
- function H(t, n = N) {
22
- return {
23
- usedPx: t,
24
- budgetPx: n,
25
- fits: t <= n,
26
- overBy: Math.max(0, t - n)
27
- };
28
- }
29
- function U(t) {
30
- return t.scrollHeight;
31
- }
1
+ import { Sidebar as o } from "./components/Sidebar/Sidebar.js";
2
+ import { NavSection as _ } from "./components/NavSection/NavSection.js";
3
+ import { Wordmark as a } from "./components/Wordmark/Wordmark.js";
4
+ import { BOOST33_MARK_MIN_LEGIBLE_PX as E, Boost33Mark as S } from "./components/Boost33Mark/Boost33Mark.js";
5
+ import { PRODUCT_SLUGS as A, ProductMark as N } from "./components/ProductMark/ProductMark.js";
6
+ import { NavItem as P } from "./components/NavItem/NavItem.js";
7
+ import { WalletChip as T } from "./components/WalletChip/WalletChip.js";
8
+ import { defaultWalletChipLabels as i } from "./components/WalletChip/labels.js";
9
+ import { TopBar as H } from "./components/TopBar/TopBar.js";
10
+ import { PageHeader as C } from "./components/PageHeader/PageHeader.js";
11
+ import { AppShell as V } from "./components/AppShell/AppShell.js";
12
+ import { AppSwitcher as O } from "./components/AppSwitcher/AppSwitcher.js";
13
+ import { defaultAppSwitcherLabels as G } from "./components/AppSwitcher/labels.js";
14
+ import { NAV_FOCUS_GUTTER_PX as g, NAV_HEIGHT_BUDGET_PX as u, NAV_ITEM_PX as v, NAV_SECTION_GAP_PX as M, NAV_SECTION_HEADER_PX as U, NAV_SHELL_CONSUMER_SLOTS_PX as n, NAV_SHELL_FIXED_PX as D, NAV_SHELL_PACKAGE_FIXED_PX as R, checkNavHeightBudget as k, measureNavHeight as s, navBudgetForViewport as F, navContentHeight as b } from "./nav-height-budget.js";
32
15
  export {
33
- N as NAV_HEIGHT_BUDGET_PX,
34
- w as NavItem,
35
- H as checkNavHeightBudget,
36
- U as measureNavHeight
16
+ V as AppShell,
17
+ O as AppSwitcher,
18
+ E as BOOST33_MARK_MIN_LEGIBLE_PX,
19
+ S as Boost33Mark,
20
+ g as NAV_FOCUS_GUTTER_PX,
21
+ u as NAV_HEIGHT_BUDGET_PX,
22
+ v as NAV_ITEM_PX,
23
+ M as NAV_SECTION_GAP_PX,
24
+ U as NAV_SECTION_HEADER_PX,
25
+ n as NAV_SHELL_CONSUMER_SLOTS_PX,
26
+ D as NAV_SHELL_FIXED_PX,
27
+ R as NAV_SHELL_PACKAGE_FIXED_PX,
28
+ P as NavItem,
29
+ _ as NavSection,
30
+ A as PRODUCT_SLUGS,
31
+ C as PageHeader,
32
+ N as ProductMark,
33
+ o as Sidebar,
34
+ H as TopBar,
35
+ T as WalletChip,
36
+ a as Wordmark,
37
+ k as checkNavHeightBudget,
38
+ G as defaultAppSwitcherLabels,
39
+ i as defaultWalletChipLabels,
40
+ s as measureNavHeight,
41
+ F as navBudgetForViewport,
42
+ b as navContentHeight
37
43
  };
38
44
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../src/components/NavItem/NavItem.tsx","../src/nav-height-budget.ts"],"sourcesContent":["import type { ReactNode } from 'react'\nimport styles from './NavItem.module.css'\n\nexport interface NavItemProps {\n /** The nav label. Truncates with an ellipsis rather than wrapping. */\n children: ReactNode\n /** Marks this row as the current page. Sets `aria-current=\"page\"`. */\n active?: boolean\n /** Sidebar-on-the-rail state. Hides the label and trailing slot visually; the label stays in the accessibility tree. */\n collapsed?: boolean\n /** Trailing slot content. Omit to leave the slot empty -- it still reserves its width so counts line up across rows. */\n count?: number\n /** Renders `count` in the primary color instead of muted. */\n countUrgent?: boolean\n href?: string\n onClick?: () => void\n}\n\nexport function NavItem({ children, active = false, collapsed = false, count, countUrgent = false, href, onClick }: NavItemProps) {\n const dataProps = {\n 'data-active': active ? ('true' as const) : undefined,\n 'data-collapsed': collapsed ? ('true' as const) : undefined,\n 'aria-current': active ? ('page' as const) : undefined,\n }\n\n const content = (\n <>\n <span className={styles.label}>{children}</span>\n <span className={styles.trailingSlot}>\n {count != null && <span className={countUrgent ? styles.countUrgent : styles.count}>{count}</span>}\n </span>\n </>\n )\n\n if (href) {\n return (\n <a className={styles.row} href={href} onClick={onClick} {...dataProps}>\n {content}\n </a>\n )\n }\n\n return (\n <button type=\"button\" className={styles.row} onClick={onClick} {...dataProps}>\n {content}\n </button>\n )\n}\n","// The spec's rough budget: a sidebar's nav has to fit above the wallet chip\n// and footer without scrolling. Boost Outbound's admin role has already blown\n// past it once, so this is a real check, not a hypothetical.\nexport const NAV_HEIGHT_BUDGET_PX = 700\n\nexport interface NavHeightBudget {\n usedPx: number\n budgetPx: number\n fits: boolean\n overBy: number\n}\n\nexport function checkNavHeightBudget(usedPx: number, budgetPx: number = NAV_HEIGHT_BUDGET_PX): NavHeightBudget {\n return {\n usedPx,\n budgetPx,\n fits: usedPx <= budgetPx,\n overBy: Math.max(0, usedPx - budgetPx),\n }\n}\n\nexport function measureNavHeight(nav: HTMLElement): number {\n return nav.scrollHeight\n}\n"],"names":["NavItem","children","active","collapsed","count","countUrgent","href","onClick","dataProps","content","jsxs","Fragment","jsx","styles","NAV_HEIGHT_BUDGET_PX","checkNavHeightBudget","usedPx","budgetPx","measureNavHeight","nav"],"mappings":";;;;;;;;AAkBO,SAASA,EAAQ,EAAE,UAAAC,GAAU,QAAAC,IAAS,IAAO,WAAAC,IAAY,IAAO,OAAAC,GAAO,aAAAC,IAAc,IAAO,MAAAC,GAAM,SAAAC,EAAA,GAAyB;AAChI,QAAMC,IAAY;AAAA,IAChB,eAAeN,IAAU,SAAmB;AAAA,IAC5C,kBAAkBC,IAAa,SAAmB;AAAA,IAClD,gBAAgBD,IAAU,SAAmB;AAAA,EAAA,GAGzCO,IACJ,gBAAAC,EAAAC,GAAA,EACE,UAAA;AAAA,IAAA,gBAAAC,EAAC,QAAA,EAAK,WAAWC,EAAO,OAAQ,UAAAZ,GAAS;AAAA,sBACxC,QAAA,EAAK,WAAWY,EAAO,cACrB,eAAS,QAAQ,gBAAAD,EAAC,QAAA,EAAK,WAAWP,IAAcQ,EAAO,cAAcA,EAAO,OAAQ,aAAM,EAAA,CAC7F;AAAA,EAAA,GACF;AAGF,SAAIP,IAEA,gBAAAM,EAAC,OAAE,WAAWC,EAAO,KAAK,MAAAP,GAAY,SAAAC,GAAmB,GAAGC,GACzD,UAAAC,EAAA,CACH,IAKF,gBAAAG,EAAC,UAAA,EAAO,MAAK,UAAS,WAAWC,EAAO,KAAK,SAAAN,GAAmB,GAAGC,GAChE,UAAAC,EAAA,CACH;AAEJ;AC5CO,MAAMK,IAAuB;AAS7B,SAASC,EAAqBC,GAAgBC,IAAmBH,GAAuC;AAC7G,SAAO;AAAA,IACL,QAAAE;AAAA,IACA,UAAAC;AAAA,IACA,MAAMD,KAAUC;AAAA,IAChB,QAAQ,KAAK,IAAI,GAAGD,IAASC,CAAQ;AAAA,EAAA;AAEzC;AAEO,SAASC,EAAiBC,GAA0B;AACzD,SAAOA,EAAI;AACb;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;"}
@@ -1,4 +1,61 @@
1
- export declare const NAV_HEIGHT_BUDGET_PX = 700;
1
+ export declare const NAV_SHELL_PACKAGE_FIXED_PX = 185;
2
+ export declare const NAV_SHELL_CONSUMER_SLOTS_PX = 86;
3
+ export declare const NAV_SHELL_FIXED_PX: number;
4
+ export declare const NAV_ITEM_PX = 33;
5
+ export declare const NAV_SECTION_HEADER_PX = 30;
6
+ export declare const NAV_SECTION_GAP_PX = 16;
7
+ export declare const NAV_FOCUS_GUTTER_PX = 8;
8
+ export declare const NAV_HEIGHT_BUDGET_PX: number;
9
+ /** One section of a nav, described by its shape rather than its rendered rows. */
10
+ export interface NavSectionShape {
11
+ /** Whether the section carries a header label (its NAV_SECTION_HEADER_PX cost). */
12
+ hasHeader: boolean;
13
+ /** How many nav items the section holds (NAV_ITEM_PX each). */
14
+ itemCount: number;
15
+ }
16
+ /**
17
+ * The per-row costs to charge, for a consumer who has measured their own.
18
+ *
19
+ * The defaults are Inter's, and are right for every face this package could
20
+ * measure. A consumer rendering in a font whose `normal` line box at 14px is
21
+ * taller than 17px -- `Segoe UI` is the known candidate, unmeasured -- gets a
22
+ * model that under-counts, and can correct it here rather than forking the
23
+ * check. Measure a rendered row's `offsetHeight`, do not estimate it.
24
+ */
25
+ export interface NavRowCosts {
26
+ /** What one nav row costs. Defaults to NAV_ITEM_PX, the expanded row in Inter. */
27
+ itemPx?: number;
28
+ /** What one section header costs. Defaults to NAV_SECTION_HEADER_PX. */
29
+ sectionHeaderPx?: number;
30
+ }
31
+ /**
32
+ * The vertical space an EXPANDED nav takes, summed from its sections' shapes --
33
+ * the rows and headers, the gaps the nav sets between sections, and the nav's
34
+ * own scroll gutter. This is the whole box the nav occupies (its rendered
35
+ * scrollHeight), so the budget can be checked from the nav's data alone, before
36
+ * it is rendered into a sidebar that might not fit it -- Boost Outbound's admin
37
+ * role has overflowed this once already.
38
+ *
39
+ * The rail is out of scope and needs no separate model: a collapsed nav is
40
+ * shorter than the same nav expanded, in a column that is taller, so this is a
41
+ * conservative bound on it (B33-762, and the check that pins it).
42
+ *
43
+ * Pass `costs` only if you have measured a rendered row in your own font; the
44
+ * defaults are the package's, measured in Inter.
45
+ */
46
+ export declare function navContentHeight(sections: NavSectionShape[], costs?: NavRowCosts): number;
47
+ /**
48
+ * The nav's budget at a given viewport: what is left once the fixed shell parts
49
+ * are taken.
50
+ *
51
+ * `consumerSlotsPx` is the total height of the three slots this package does
52
+ * not own -- the switcher, the wallet chip's neighbours and the sidebar footer
53
+ * as the consumer renders them. It defaults to the stress specimen's 86px
54
+ * allowance, which is what NAV_SHELL_FIXED_PX assumes. A consumer whose footer
55
+ * or switcher is taller than that MUST pass their own measured total, or this
56
+ * will over-count their column and can report a fit that scrolls.
57
+ */
58
+ export declare function navBudgetForViewport(viewportPx: number, consumerSlotsPx?: number): number;
2
59
  export interface NavHeightBudget {
3
60
  usedPx: number;
4
61
  budgetPx: number;
@@ -0,0 +1,38 @@
1
+ const P = 185, X = 86, c = 271, H = 33, L = 30, V = 16, T = 8, S = r(900);
2
+ function C(_, t = {}) {
3
+ if (_.length === 0) return 0;
4
+ const E = t.itemPx ?? 33, e = t.sectionHeaderPx ?? 30, A = _.reduce(
5
+ (o, n) => o + (n.hasHeader ? e : 0) + n.itemCount * E,
6
+ 0
7
+ ), N = 16 * (_.length - 1);
8
+ return A + N + 8;
9
+ }
10
+ function r(_, t = 86) {
11
+ return _ - 185 - t;
12
+ }
13
+ function i(_, t = S) {
14
+ return {
15
+ usedPx: _,
16
+ budgetPx: t,
17
+ fits: _ <= t,
18
+ overBy: Math.max(0, _ - t)
19
+ };
20
+ }
21
+ function a(_) {
22
+ return _.scrollHeight;
23
+ }
24
+ export {
25
+ T as NAV_FOCUS_GUTTER_PX,
26
+ S as NAV_HEIGHT_BUDGET_PX,
27
+ H as NAV_ITEM_PX,
28
+ V as NAV_SECTION_GAP_PX,
29
+ L as NAV_SECTION_HEADER_PX,
30
+ X as NAV_SHELL_CONSUMER_SLOTS_PX,
31
+ c as NAV_SHELL_FIXED_PX,
32
+ P as NAV_SHELL_PACKAGE_FIXED_PX,
33
+ i as checkNavHeightBudget,
34
+ a as measureNavHeight,
35
+ r as navBudgetForViewport,
36
+ C as navContentHeight
37
+ };
38
+ //# sourceMappingURL=nav-height-budget.js.map