@boost33/ui 0.7.1 → 0.8.0-next.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.
- package/CHANGELOG.md +183 -0
- package/dist/buttons.module.css.js +10 -0
- package/dist/buttons.module.css.js.map +1 -0
- package/dist/components/AppShell/AppShell.d.ts +85 -17
- package/dist/components/AppShell/AppShell.js +75 -50
- package/dist/components/AppShell/AppShell.js.map +1 -1
- package/dist/components/AppShell/AppShell.module.css.js +22 -16
- package/dist/components/AppShell/AppShell.module.css.js.map +1 -1
- package/dist/components/AppSwitcher/AppSwitcher.d.ts +90 -45
- package/dist/components/AppSwitcher/AppSwitcher.js +71 -66
- package/dist/components/AppSwitcher/AppSwitcher.js.map +1 -1
- package/dist/components/AppSwitcher/AppSwitcher.module.css.js +42 -36
- package/dist/components/AppSwitcher/AppSwitcher.module.css.js.map +1 -1
- package/dist/components/AppSwitcher/icons.js +10 -10
- package/dist/components/AppSwitcher/icons.js.map +1 -1
- package/dist/components/AppSwitcher/labels.d.ts +11 -22
- package/dist/components/AppSwitcher/labels.js.map +1 -1
- package/dist/components/CommandPalette/CommandPalette.d.ts +8 -2
- package/dist/components/CommandPalette/CommandPalette.js.map +1 -1
- package/dist/components/NavItem/NavItem.d.ts +15 -2
- package/dist/components/NavItem/NavItem.js +14 -11
- package/dist/components/NavItem/NavItem.js.map +1 -1
- package/dist/components/NavItem/NavItem.module.css.js +6 -2
- package/dist/components/NavItem/NavItem.module.css.js.map +1 -1
- package/dist/components/NavSection/NavSection.module.css.js +5 -5
- package/dist/components/NotificationButton/NotificationButton.d.ts +29 -0
- package/dist/components/NotificationButton/NotificationButton.js +23 -0
- package/dist/components/NotificationButton/NotificationButton.js.map +1 -0
- package/dist/components/NotificationButton/NotificationButton.module.css.js +14 -0
- package/dist/components/NotificationButton/NotificationButton.module.css.js.map +1 -0
- package/dist/components/NotificationButton/labels.d.ts +12 -0
- package/dist/components/NotificationButton/labels.js +15 -0
- package/dist/components/NotificationButton/labels.js.map +1 -0
- package/dist/components/NotificationCenter/NotificationCenter.js +37 -36
- package/dist/components/NotificationCenter/NotificationCenter.js.map +1 -1
- package/dist/components/NotificationCenter/NotificationCenter.module.css.js +43 -43
- package/dist/components/Sidebar/NavHeightBudgetWarning.js +2 -2
- package/dist/components/Sidebar/NavHeightBudgetWarning.js.map +1 -1
- package/dist/components/Sidebar/Sidebar.module.css.js +1 -1
- package/dist/components/TopBar/TopBar.d.ts +54 -4
- package/dist/components/TopBar/TopBar.js +22 -7
- package/dist/components/TopBar/TopBar.js.map +1 -1
- package/dist/components/TopBar/TopBar.module.css.js +12 -8
- package/dist/components/TopBar/TopBar.module.css.js.map +1 -1
- package/dist/components/TopBar/labels.d.ts +20 -0
- package/dist/components/TopBar/labels.js +15 -0
- package/dist/components/TopBar/labels.js.map +1 -0
- package/dist/components/WalletChip/WalletChip.d.ts +77 -2
- package/dist/components/WalletChip/WalletChip.js +142 -50
- package/dist/components/WalletChip/WalletChip.js.map +1 -1
- package/dist/components/WalletChip/WalletChip.module.css.js +93 -47
- package/dist/components/WalletChip/WalletChip.module.css.js.map +1 -1
- package/dist/components/WalletChip/labels.d.ts +63 -1
- package/dist/components/WalletChip/labels.js +21 -6
- package/dist/components/WalletChip/labels.js.map +1 -1
- package/dist/index.d.ts +9 -3
- package/dist/index.js +55 -49
- package/dist/index.js.map +1 -1
- package/dist/nav-height-budget.d.ts +12 -9
- package/dist/nav-height-budget.js +3 -3
- package/dist/nav-height-budget.js.map +1 -1
- package/dist/style.css +1 -1
- package/dist/theme.css +1 -1
- package/package.json +1 -1
|
@@ -93,6 +93,53 @@ export interface WalletChipLabels {
|
|
|
93
93
|
* `%granted% granted and %purchased% purchased of %remaining% remaining`.
|
|
94
94
|
*/
|
|
95
95
|
bar: string;
|
|
96
|
+
/**
|
|
97
|
+
* The top-bar chip's popover, named for a screen reader: a disclosure panel
|
|
98
|
+
* carries no widget role, so it takes an `aria-label` (ADR-0010). Default
|
|
99
|
+
* "Token balance". `placement="topbar"` only (B33-1712).
|
|
100
|
+
*/
|
|
101
|
+
popover: string;
|
|
102
|
+
/**
|
|
103
|
+
* The popover's granted line, around the preformatted amount (board 21:
|
|
104
|
+
* "3,420 granted"). Placeholder `%amount%`. Default `%amount% granted`.
|
|
105
|
+
*/
|
|
106
|
+
grantedFigure: string;
|
|
107
|
+
/** The purchased line, on the same terms. Placeholder `%amount%`. Default `%amount% purchased`. */
|
|
108
|
+
purchasedFigure: string;
|
|
109
|
+
/**
|
|
110
|
+
* The note under the granted line when `grantExpires` is known (board 21).
|
|
111
|
+
* Placeholder `%date%`, the preformatted reset date. Default
|
|
112
|
+
* `Included each month. Resets %date%.`
|
|
113
|
+
*/
|
|
114
|
+
grantedNote: string;
|
|
115
|
+
/**
|
|
116
|
+
* The same note when `grantExpires` is not supplied, so the chip never prints
|
|
117
|
+
* a reset date nobody gave it. Default `Included each month.`
|
|
118
|
+
*/
|
|
119
|
+
grantedNoteUndated: string;
|
|
120
|
+
/** The note under the purchased line. Default `Yours to keep. These never expire.` */
|
|
121
|
+
purchasedNote: string;
|
|
122
|
+
/** The popover's link to the Wallet (`walletHref`). Default "Open". */
|
|
123
|
+
open: string;
|
|
124
|
+
/** The popover's buy button (`onTopUp`). Default "Buy tokens". */
|
|
125
|
+
buyTokens: string;
|
|
126
|
+
/**
|
|
127
|
+
* The top-bar chip's unit when the account is past zero (`totalOnly` with a negative `balance`,
|
|
128
|
+
* B33-1712), after the figure for how far past zero it is: "420 past zero".
|
|
129
|
+
* The Wallet's own past-zero screen says it in these words rather than with a
|
|
130
|
+
* minus sign, and the chip follows it. Default `past zero`.
|
|
131
|
+
*/
|
|
132
|
+
pastZero: string;
|
|
133
|
+
/**
|
|
134
|
+
* The same, in the popover, where there is room for the noun. Default
|
|
135
|
+
* `tokens past zero`.
|
|
136
|
+
*/
|
|
137
|
+
pastZeroLong: string;
|
|
138
|
+
/**
|
|
139
|
+
* The past-zero pill's accessible name. A template: `%amount%` is the
|
|
140
|
+
* formatted figure past zero. Default `Wallet balance %amount% tokens past zero`.
|
|
141
|
+
*/
|
|
142
|
+
pastZeroBalance: string;
|
|
96
143
|
}
|
|
97
144
|
/**
|
|
98
145
|
* The English defaults. A consumer that passes nothing renders exactly what the
|
|
@@ -128,6 +175,17 @@ export declare const defaultWalletChipLabels: Readonly<{
|
|
|
128
175
|
railBalanceUnknown: string;
|
|
129
176
|
railUnknown: string;
|
|
130
177
|
bar: string;
|
|
178
|
+
popover: string;
|
|
179
|
+
grantedFigure: string;
|
|
180
|
+
purchasedFigure: string;
|
|
181
|
+
grantedNote: string;
|
|
182
|
+
grantedNoteUndated: string;
|
|
183
|
+
purchasedNote: string;
|
|
184
|
+
open: string;
|
|
185
|
+
buyTokens: string;
|
|
186
|
+
pastZero: string;
|
|
187
|
+
pastZeroLong: string;
|
|
188
|
+
pastZeroBalance: string;
|
|
131
189
|
}>;
|
|
132
190
|
/**
|
|
133
191
|
* The labels as the render consumes them: the four templates presented as
|
|
@@ -140,11 +198,15 @@ export declare const defaultWalletChipLabels: Readonly<{
|
|
|
140
198
|
* of it; and the call is where B33-875's laziness lives, so a label for an element
|
|
141
199
|
* that does not render is not interpolated at all.
|
|
142
200
|
*/
|
|
143
|
-
export interface ResolvedWalletChipLabels extends Omit<WalletChipLabels, 'grantExpiry' | 'railFigure' | 'railBalance' | 'bar'> {
|
|
201
|
+
export interface ResolvedWalletChipLabels extends Omit<WalletChipLabels, 'grantExpiry' | 'railFigure' | 'railBalance' | 'bar' | 'grantedFigure' | 'purchasedFigure' | 'grantedNote' | 'pastZeroBalance'> {
|
|
144
202
|
grantExpiry: (date: string) => string;
|
|
203
|
+
grantedFigure: (amount: string) => string;
|
|
204
|
+
purchasedFigure: (amount: string) => string;
|
|
205
|
+
grantedNote: (date: string) => string;
|
|
145
206
|
railFigure: (figure: string) => string;
|
|
146
207
|
railBalance: (balance: string) => string;
|
|
147
208
|
bar: (granted: string, purchased: string, remaining: string) => string;
|
|
209
|
+
pastZeroBalance: (amount: string) => string;
|
|
148
210
|
}
|
|
149
211
|
/**
|
|
150
212
|
* Merge a consumer's partial `labels` onto the English defaults, treating an
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { fillTemplate as n } from "../../fill-template.js";
|
|
2
|
-
const
|
|
2
|
+
const i = Object.freeze({
|
|
3
3
|
region: "Wallet balance",
|
|
4
4
|
name: "Wallet",
|
|
5
5
|
unit: "tokens",
|
|
@@ -12,22 +12,37 @@ const c = Object.freeze({
|
|
|
12
12
|
railBalance: "Wallet balance %balance%",
|
|
13
13
|
railBalanceUnknown: "Wallet balance unknown",
|
|
14
14
|
railUnknown: "unknown",
|
|
15
|
-
bar: "%granted% granted and %purchased% purchased of %remaining% remaining"
|
|
15
|
+
bar: "%granted% granted and %purchased% purchased of %remaining% remaining",
|
|
16
|
+
popover: "Token balance",
|
|
17
|
+
grantedFigure: "%amount% granted",
|
|
18
|
+
purchasedFigure: "%amount% purchased",
|
|
19
|
+
grantedNote: "Included each month. Resets %date%.",
|
|
20
|
+
grantedNoteUndated: "Included each month.",
|
|
21
|
+
purchasedNote: "Yours to keep. These never expire.",
|
|
22
|
+
open: "Open",
|
|
23
|
+
buyTokens: "Buy tokens",
|
|
24
|
+
pastZero: "past zero",
|
|
25
|
+
pastZeroLong: "tokens past zero",
|
|
26
|
+
pastZeroBalance: "Wallet balance %amount% tokens past zero"
|
|
16
27
|
});
|
|
17
28
|
function p(r) {
|
|
18
|
-
const
|
|
29
|
+
const t = Object.fromEntries(
|
|
19
30
|
Object.entries(r ?? {}).filter(([, e]) => e !== void 0)
|
|
20
|
-
), a = { ...
|
|
31
|
+
), a = { ...i, ...t };
|
|
21
32
|
return {
|
|
22
33
|
...a,
|
|
23
34
|
grantExpiry: (e) => n(a.grantExpiry, { date: e }),
|
|
24
35
|
railFigure: (e) => n(a.railFigure, { figure: e }),
|
|
25
36
|
railBalance: (e) => n(a.railBalance, { balance: e }),
|
|
26
|
-
bar: (e,
|
|
37
|
+
bar: (e, o, l) => n(a.bar, { granted: e, purchased: o, remaining: l }),
|
|
38
|
+
grantedFigure: (e) => n(a.grantedFigure, { amount: e }),
|
|
39
|
+
purchasedFigure: (e) => n(a.purchasedFigure, { amount: e }),
|
|
40
|
+
grantedNote: (e) => n(a.grantedNote, { date: e }),
|
|
41
|
+
pastZeroBalance: (e) => n(a.pastZeroBalance, { amount: e })
|
|
27
42
|
};
|
|
28
43
|
}
|
|
29
44
|
export {
|
|
30
|
-
|
|
45
|
+
i as defaultWalletChipLabels,
|
|
31
46
|
p as resolveWalletChipLabels
|
|
32
47
|
};
|
|
33
48
|
//# sourceMappingURL=labels.js.map
|
|
@@ -1 +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. They\n// are supplied whole, as a template that spells the value's position with a\n// `%placeholder%`.\n//\n// ── Every field is a string, and that is the whole type (B33-1278) ───────────\n//\n// Each of those three was once `string | ((…) => string)`. The function half is\n// gone, and the reason is that it could not be used by the consumers this package\n// has. A function is the one thing that cannot be a prop of a Client Component\n// rendered from a React Server Component: React's Flight serializer walks the\n// whole prop graph and throws on any function it finds, nested or not, in\n// production as well as 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// `WalletChip` is a client module and `AppShell.wallet` is a ReactNode slot, so\n// it is the *consumer's page* that constructs `<WalletChip …/>` -- and under the\n// App Router that page is a Server Component by default, which is how both\n// consumers render it, because reading the balance is a server read. So the\n// function half was type-legal and fatal in production for the only way anyone\n// uses the component, and nothing in the type said so: `tsc` accepted it, the dev\n// server accepted it, and it threw at serialisation time (B33-880 for the first\n// half of this, B33-1278 for the rest). Score covered the hole from outside, with\n// a runtime `typeof raw === 'string'` guard and five tests walking the prop graph,\n// because -- in its own words -- neither `tsc` nor the dev server will.\n//\n// What the function form was *for* was logic a template cannot express: ICU\n// plurals, an `Intl.ListFormat` call. That capability is not lost, it moves to\n// where ICU already lives. A consumer selecting a plural form does the selection\n// in its own catalogue and passes this package the finished sentence, which is\n// what both consumers already do with next-intl. No default label in this package\n// is count-dependent, so no default needed it (see `unit` below).\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\nimport { fillTemplate } from '../../fill-template'\n\n/**\n * Every user-facing string the chip renders. Supplied whole -- the strings that\n * carry a value take a `%placeholder%` template rather than fragments, because\n * word order is not a property of English and cannot be reassembled on the\n * consumer's side.\n *\n * **Every field is a `string`.** `grantExpiry`, `railFigure`, `railBalance` and\n * `bar` are templates: plain strings marking each value's position with a\n * `%placeholder%` -- `'Granted expires %date% · burns first'`. So the whole\n * `labels` object is data all the way down, it survives the React Server Component\n * boundary, and a Next App Router page can build it. There is no function form:\n * it only ever worked from a Client Component and nothing in the type said so\n * (B33-1278). A sentence needing logic a template cannot express -- a plural rule,\n * an `Intl.ListFormat` call -- is computed in the consumer's own catalogue, where\n * ICU already lives, and passed here finished.\n *\n * In a template, a `%name%` the label does not define is left exactly as written\n * -- a locale whose copy genuinely pairs two percent signs 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 *\n * **The delimiter is `%name%`, not `{name}`** (B33-1277). A brace is ICU\n * MessageFormat argument syntax, so a consumer storing these templates in its own\n * next-intl catalogue could not read them back through its own formatter. It can\n * now. `src/fill-template.ts` carries the reasoning, including why `%{name}` and\n * `{{name}}` would both have kept the collision. A `{braced}` name in a template\n * is ordinary text here and is passed through untouched.\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 /**\n * The unit rendered beside the figure. Default \"tokens\".\n *\n * A single string, and so not count-dependent: a balance of one renders \"1\n * tokens\" in English. That has been true since B33-849 and B33-1278 did not\n * change it -- `unit` never had the function form, so dropping that form took\n * nothing away here. It is also a state the chip is unlikely to reach: a\n * one-token balance is an all-but-empty wallet. If a locale needs the unit to\n * agree with the figure, the shape to add is a count-aware field, which is a\n * decision and not a refactor; nothing in the package invents one meanwhile.\n */\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 */\n grantExpiry: 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`.\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\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%`.\n */\n railBalance: 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`.\n */\n bar: 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, and now by construction.** That is what makes\n * `labels={defaultWalletChipLabels}` and\n * `labels={{ ...defaultWalletChipLabels, name: 'Portefeuille' }}` -- the pattern\n * the specimen shows and this package's own advice recommends -- legal from a\n * Server Component (B33-880). It used to be a runtime fact pinned by a test,\n * because `WalletChipLabels` admitted functions on four fields and `satisfies`\n * could not tell the two apart (B33-1029). With the union gone the compiler\n * carries it: a function here is a type error at the `satisfies`, and\n * `tests/wallet-chip-labels.type-test.ts` asserts the all-strings shape of the\n * interface itself so the guarantee cannot be lost by widening one field.\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 templates presented as\n * functions of their values, so the component writes `t.bar(…)` and never handles\n * a template itself.\n *\n * This is internal -- it is not exported from `src/index.ts` -- and it stays a\n * function shape now that `WalletChipLabels` holds only strings, for two reasons.\n * The interpolation has to happen somewhere, and one site per label is the whole\n * of it; and the call is where B33-875's laziness lives, so a label for an element\n * that does not render is not interpolated at all.\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 * Merge a consumer's partial `labels` onto the English defaults, treating an\n * explicitly-`undefined` field as unsupplied, and present the four templates as\n * functions of their values.\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 hand\n * `fillTemplate` an `undefined` template (B33-873). A plain object spread copies\n * the `undefined` across; a 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) => fillTemplate(merged.grantExpiry, { date }),\n railFigure: (figure) => fillTemplate(merged.railFigure, { figure }),\n railBalance: (balance) => fillTemplate(merged.railBalance, { balance }),\n bar: (granted, purchased, remaining) => fillTemplate(merged.bar, { granted, purchased, remaining }),\n }\n}\n"],"names":["defaultWalletChipLabels","resolveWalletChipLabels","labels","supplied","value","merged","date","fillTemplate","figure","balance","granted","purchased","remaining"],"mappings":";AA0KO,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;AAiCrB,SAASC,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,GAAGL,GAAyB,GAAGG,EAAA;AAChD,SAAO;AAAA,IACL,GAAGE;AAAA,IACH,aAAa,CAACC,MAASC,EAAaF,EAAO,aAAa,EAAE,MAAAC,GAAM;AAAA,IAChE,YAAY,CAACE,MAAWD,EAAaF,EAAO,YAAY,EAAE,QAAAG,GAAQ;AAAA,IAClE,aAAa,CAACC,MAAYF,EAAaF,EAAO,aAAa,EAAE,SAAAI,GAAS;AAAA,IACtE,KAAK,CAACC,GAASC,GAAWC,MAAcL,EAAaF,EAAO,KAAK,EAAE,SAAAK,GAAS,WAAAC,GAAW,WAAAC,EAAA,CAAW;AAAA,EAAA;AAEtG;"}
|
|
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. They\n// are supplied whole, as a template that spells the value's position with a\n// `%placeholder%`.\n//\n// ── Every field is a string, and that is the whole type (B33-1278) ───────────\n//\n// Each of those three was once `string | ((…) => string)`. The function half is\n// gone, and the reason is that it could not be used by the consumers this package\n// has. A function is the one thing that cannot be a prop of a Client Component\n// rendered from a React Server Component: React's Flight serializer walks the\n// whole prop graph and throws on any function it finds, nested or not, in\n// production as well as 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// `WalletChip` is a client module and `AppShell.wallet` is a ReactNode slot, so\n// it is the *consumer's page* that constructs `<WalletChip …/>` -- and under the\n// App Router that page is a Server Component by default, which is how both\n// consumers render it, because reading the balance is a server read. So the\n// function half was type-legal and fatal in production for the only way anyone\n// uses the component, and nothing in the type said so: `tsc` accepted it, the dev\n// server accepted it, and it threw at serialisation time (B33-880 for the first\n// half of this, B33-1278 for the rest). Score covered the hole from outside, with\n// a runtime `typeof raw === 'string'` guard and five tests walking the prop graph,\n// because -- in its own words -- neither `tsc` nor the dev server will.\n//\n// What the function form was *for* was logic a template cannot express: ICU\n// plurals, an `Intl.ListFormat` call. That capability is not lost, it moves to\n// where ICU already lives. A consumer selecting a plural form does the selection\n// in its own catalogue and passes this package the finished sentence, which is\n// what both consumers already do with next-intl. No default label in this package\n// is count-dependent, so no default needed it (see `unit` below).\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\nimport { fillTemplate } from '../../fill-template'\n\n/**\n * Every user-facing string the chip renders. Supplied whole -- the strings that\n * carry a value take a `%placeholder%` template rather than fragments, because\n * word order is not a property of English and cannot be reassembled on the\n * consumer's side.\n *\n * **Every field is a `string`.** `grantExpiry`, `railFigure`, `railBalance` and\n * `bar` are templates: plain strings marking each value's position with a\n * `%placeholder%` -- `'Granted expires %date% · burns first'`. So the whole\n * `labels` object is data all the way down, it survives the React Server Component\n * boundary, and a Next App Router page can build it. There is no function form:\n * it only ever worked from a Client Component and nothing in the type said so\n * (B33-1278). A sentence needing logic a template cannot express -- a plural rule,\n * an `Intl.ListFormat` call -- is computed in the consumer's own catalogue, where\n * ICU already lives, and passed here finished.\n *\n * In a template, a `%name%` the label does not define is left exactly as written\n * -- a locale whose copy genuinely pairs two percent signs 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 *\n * **The delimiter is `%name%`, not `{name}`** (B33-1277). A brace is ICU\n * MessageFormat argument syntax, so a consumer storing these templates in its own\n * next-intl catalogue could not read them back through its own formatter. It can\n * now. `src/fill-template.ts` carries the reasoning, including why `%{name}` and\n * `{{name}}` would both have kept the collision. A `{braced}` name in a template\n * is ordinary text here and is passed through untouched.\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 /**\n * The unit rendered beside the figure. Default \"tokens\".\n *\n * A single string, and so not count-dependent: a balance of one renders \"1\n * tokens\" in English. That has been true since B33-849 and B33-1278 did not\n * change it -- `unit` never had the function form, so dropping that form took\n * nothing away here. It is also a state the chip is unlikely to reach: a\n * one-token balance is an all-but-empty wallet. If a locale needs the unit to\n * agree with the figure, the shape to add is a count-aware field, which is a\n * decision and not a refactor; nothing in the package invents one meanwhile.\n */\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 */\n grantExpiry: 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`.\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\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%`.\n */\n railBalance: 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`.\n */\n bar: string\n /**\n * The top-bar chip's popover, named for a screen reader: a disclosure panel\n * carries no widget role, so it takes an `aria-label` (ADR-0010). Default\n * \"Token balance\". `placement=\"topbar\"` only (B33-1712).\n */\n popover: string\n /**\n * The popover's granted line, around the preformatted amount (board 21:\n * \"3,420 granted\"). Placeholder `%amount%`. Default `%amount% granted`.\n */\n grantedFigure: string\n /** The purchased line, on the same terms. Placeholder `%amount%`. Default `%amount% purchased`. */\n purchasedFigure: string\n /**\n * The note under the granted line when `grantExpires` is known (board 21).\n * Placeholder `%date%`, the preformatted reset date. Default\n * `Included each month. Resets %date%.`\n */\n grantedNote: string\n /**\n * The same note when `grantExpires` is not supplied, so the chip never prints\n * a reset date nobody gave it. Default `Included each month.`\n */\n grantedNoteUndated: string\n /** The note under the purchased line. Default `Yours to keep. These never expire.` */\n purchasedNote: string\n /** The popover's link to the Wallet (`walletHref`). Default \"Open\". */\n open: string\n /** The popover's buy button (`onTopUp`). Default \"Buy tokens\". */\n buyTokens: string\n /**\n * The top-bar chip's unit when the account is past zero (`totalOnly` with a negative `balance`,\n * B33-1712), after the figure for how far past zero it is: \"420 past zero\".\n * The Wallet's own past-zero screen says it in these words rather than with a\n * minus sign, and the chip follows it. Default `past zero`.\n */\n pastZero: string\n /**\n * The same, in the popover, where there is room for the noun. Default\n * `tokens past zero`.\n */\n pastZeroLong: string\n /**\n * The past-zero pill's accessible name. A template: `%amount%` is the\n * formatted figure past zero. Default `Wallet balance %amount% tokens past zero`.\n */\n pastZeroBalance: 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, and now by construction.** That is what makes\n * `labels={defaultWalletChipLabels}` and\n * `labels={{ ...defaultWalletChipLabels, name: 'Portefeuille' }}` -- the pattern\n * the specimen shows and this package's own advice recommends -- legal from a\n * Server Component (B33-880). It used to be a runtime fact pinned by a test,\n * because `WalletChipLabels` admitted functions on four fields and `satisfies`\n * could not tell the two apart (B33-1029). With the union gone the compiler\n * carries it: a function here is a type error at the `satisfies`, and\n * `tests/wallet-chip-labels.type-test.ts` asserts the all-strings shape of the\n * interface itself so the guarantee cannot be lost by widening one field.\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 popover: 'Token balance',\n grantedFigure: '%amount% granted',\n purchasedFigure: '%amount% purchased',\n grantedNote: 'Included each month. Resets %date%.',\n grantedNoteUndated: 'Included each month.',\n purchasedNote: 'Yours to keep. These never expire.',\n open: 'Open',\n buyTokens: 'Buy tokens',\n pastZero: 'past zero',\n pastZeroLong: 'tokens past zero',\n pastZeroBalance: 'Wallet balance %amount% tokens past zero',\n} satisfies WalletChipLabels)\n\n/**\n * The labels as the render consumes them: the four templates presented as\n * functions of their values, so the component writes `t.bar(…)` and never handles\n * a template itself.\n *\n * This is internal -- it is not exported from `src/index.ts` -- and it stays a\n * function shape now that `WalletChipLabels` holds only strings, for two reasons.\n * The interpolation has to happen somewhere, and one site per label is the whole\n * of it; and the call is where B33-875's laziness lives, so a label for an element\n * that does not render is not interpolated at all.\n */\nexport interface ResolvedWalletChipLabels\n extends Omit<WalletChipLabels, 'grantExpiry' | 'railFigure' | 'railBalance' | 'bar' | 'grantedFigure' | 'purchasedFigure' | 'grantedNote' | 'pastZeroBalance'> {\n grantExpiry: (date: string) => string\n grantedFigure: (amount: string) => string\n purchasedFigure: (amount: string) => string\n grantedNote: (date: string) => string\n railFigure: (figure: string) => string\n railBalance: (balance: string) => string\n bar: (granted: string, purchased: string, remaining: string) => string\n pastZeroBalance: (amount: string) => string\n}\n\n/**\n * Merge a consumer's partial `labels` onto the English defaults, treating an\n * explicitly-`undefined` field as unsupplied, and present the four templates as\n * functions of their values.\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 hand\n * `fillTemplate` an `undefined` template (B33-873). A plain object spread copies\n * the `undefined` across; a 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) => fillTemplate(merged.grantExpiry, { date }),\n railFigure: (figure) => fillTemplate(merged.railFigure, { figure }),\n railBalance: (balance) => fillTemplate(merged.railBalance, { balance }),\n bar: (granted, purchased, remaining) => fillTemplate(merged.bar, { granted, purchased, remaining }),\n grantedFigure: (amount) => fillTemplate(merged.grantedFigure, { amount }),\n purchasedFigure: (amount) => fillTemplate(merged.purchasedFigure, { amount }),\n grantedNote: (date) => fillTemplate(merged.grantedNote, { date }),\n pastZeroBalance: (amount) => fillTemplate(merged.pastZeroBalance, { amount }),\n }\n}\n"],"names":["defaultWalletChipLabels","resolveWalletChipLabels","labels","supplied","value","merged","date","fillTemplate","figure","balance","granted","purchased","remaining","amount"],"mappings":";AAyNO,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;AAAA,EACL,SAAS;AAAA,EACT,eAAe;AAAA,EACf,iBAAiB;AAAA,EACjB,aAAa;AAAA,EACb,oBAAoB;AAAA,EACpB,eAAe;AAAA,EACf,MAAM;AAAA,EACN,WAAW;AAAA,EACX,UAAU;AAAA,EACV,cAAc;AAAA,EACd,iBAAiB;AACnB,CAA4B;AAqCrB,SAASC,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,GAAGL,GAAyB,GAAGG,EAAA;AAChD,SAAO;AAAA,IACL,GAAGE;AAAA,IACH,aAAa,CAACC,MAASC,EAAaF,EAAO,aAAa,EAAE,MAAAC,GAAM;AAAA,IAChE,YAAY,CAACE,MAAWD,EAAaF,EAAO,YAAY,EAAE,QAAAG,GAAQ;AAAA,IAClE,aAAa,CAACC,MAAYF,EAAaF,EAAO,aAAa,EAAE,SAAAI,GAAS;AAAA,IACtE,KAAK,CAACC,GAASC,GAAWC,MAAcL,EAAaF,EAAO,KAAK,EAAE,SAAAK,GAAS,WAAAC,GAAW,WAAAC,EAAA,CAAW;AAAA,IAClG,eAAe,CAACC,MAAWN,EAAaF,EAAO,eAAe,EAAE,QAAAQ,GAAQ;AAAA,IACxE,iBAAiB,CAACA,MAAWN,EAAaF,EAAO,iBAAiB,EAAE,QAAAQ,GAAQ;AAAA,IAC5E,aAAa,CAACP,MAASC,EAAaF,EAAO,aAAa,EAAE,MAAAC,GAAM;AAAA,IAChE,iBAAiB,CAACO,MAAWN,EAAaF,EAAO,iBAAiB,EAAE,QAAAQ,GAAQ;AAAA,EAAA;AAEhF;"}
|
package/dist/index.d.ts
CHANGED
|
@@ -11,17 +11,23 @@ export type { ProductMarkProps } from './components/ProductMark/ProductMark';
|
|
|
11
11
|
export { NavItem } from './components/NavItem/NavItem';
|
|
12
12
|
export type { NavItemProps } from './components/NavItem/NavItem';
|
|
13
13
|
export { WalletChip } from './components/WalletChip/WalletChip';
|
|
14
|
-
export type { WalletChipProps, WalletChipKnownProps, WalletChipUnknownProps } from './components/WalletChip/WalletChip';
|
|
14
|
+
export type { WalletChipProps, WalletChipKnownProps, WalletChipUnknownProps, WalletChipTotalOnlyProps } from './components/WalletChip/WalletChip';
|
|
15
15
|
export { defaultWalletChipLabels } from './components/WalletChip/labels';
|
|
16
16
|
export type { WalletChipLabels } from './components/WalletChip/labels';
|
|
17
17
|
export { TopBar } from './components/TopBar/TopBar';
|
|
18
18
|
export type { TopBarProps } from './components/TopBar/TopBar';
|
|
19
|
+
export { defaultTopBarLabels } from './components/TopBar/labels';
|
|
20
|
+
export type { TopBarLabels } from './components/TopBar/labels';
|
|
21
|
+
export { NotificationButton } from './components/NotificationButton/NotificationButton';
|
|
22
|
+
export type { NotificationButtonProps } from './components/NotificationButton/NotificationButton';
|
|
23
|
+
export { defaultNotificationButtonLabels } from './components/NotificationButton/labels';
|
|
24
|
+
export type { NotificationButtonLabels } from './components/NotificationButton/labels';
|
|
19
25
|
export { PageHeader } from './components/PageHeader/PageHeader';
|
|
20
26
|
export type { PageHeaderProps } from './components/PageHeader/PageHeader';
|
|
21
27
|
export { AppShell } from './components/AppShell/AppShell';
|
|
22
|
-
export type { AppShellProps, AppShellNavSection, AppShellNavItem } from './components/AppShell/AppShell';
|
|
28
|
+
export type { AppShellProps, AppShellAction, AppShellNavSection, AppShellNavItem } from './components/AppShell/AppShell';
|
|
23
29
|
export { AppSwitcher } from './components/AppSwitcher/AppSwitcher';
|
|
24
|
-
export type { AppSwitcherProps, AppSwitcherProduct, AppSwitcherHub, AppSwitcherAccount, } from './components/AppSwitcher/AppSwitcher';
|
|
30
|
+
export type { AppSwitcherProps, AppSwitcherProduct, AppSwitcherHub, AppSwitcherAccount, AppSwitcherTrailing, } from './components/AppSwitcher/AppSwitcher';
|
|
25
31
|
export { defaultAppSwitcherLabels } from './components/AppSwitcher/labels';
|
|
26
32
|
export type { AppSwitcherLabels } from './components/AppSwitcher/labels';
|
|
27
33
|
export { NotificationCenter } from './components/NotificationCenter/NotificationCenter';
|
package/dist/index.js
CHANGED
|
@@ -1,58 +1,64 @@
|
|
|
1
1
|
/* empty css */
|
|
2
|
-
import { Sidebar as
|
|
2
|
+
import { Sidebar as e } from "./components/Sidebar/Sidebar.js";
|
|
3
3
|
import { NavSection as p } from "./components/NavSection/NavSection.js";
|
|
4
|
-
import { Wordmark as
|
|
5
|
-
import { BOOST33_MARK_MIN_LEGIBLE_PX as
|
|
6
|
-
import { ProductMark as d, hasProductMark as
|
|
4
|
+
import { Wordmark as m } from "./components/Wordmark/Wordmark.js";
|
|
5
|
+
import { BOOST33_MARK_MIN_LEGIBLE_PX as i, Boost33Mark as l } from "./components/Boost33Mark/Boost33Mark.js";
|
|
6
|
+
import { ProductMark as d, hasProductMark as u } from "./components/ProductMark/ProductMark.js";
|
|
7
7
|
import { NavItem as s } from "./components/NavItem/NavItem.js";
|
|
8
|
-
import { WalletChip as
|
|
9
|
-
import { defaultWalletChipLabels as
|
|
10
|
-
import { TopBar as
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
8
|
+
import { WalletChip as h } from "./components/WalletChip/WalletChip.js";
|
|
9
|
+
import { defaultWalletChipLabels as b } from "./components/WalletChip/labels.js";
|
|
10
|
+
import { TopBar as M } from "./components/TopBar/TopBar.js";
|
|
11
|
+
import { defaultTopBarLabels as S } from "./components/TopBar/labels.js";
|
|
12
|
+
import { NotificationButton as k } from "./components/NotificationButton/NotificationButton.js";
|
|
13
|
+
import { defaultNotificationButtonLabels as P } from "./components/NotificationButton/labels.js";
|
|
14
|
+
import { PageHeader as _ } from "./components/PageHeader/PageHeader.js";
|
|
15
|
+
import { AppShell as F } from "./components/AppShell/AppShell.js";
|
|
16
|
+
import { AppSwitcher as I } from "./components/AppSwitcher/AppSwitcher.js";
|
|
17
|
+
import { defaultAppSwitcherLabels as W } from "./components/AppSwitcher/labels.js";
|
|
18
|
+
import { NotificationCenter as O } from "./components/NotificationCenter/NotificationCenter.js";
|
|
19
|
+
import { defaultNotificationCenterLabels as G } from "./components/NotificationCenter/labels.js";
|
|
20
|
+
import { AccountMenu as R } from "./components/AccountMenu/AccountMenu.js";
|
|
21
|
+
import { defaultAccountMenuLabels as X } from "./components/AccountMenu/labels.js";
|
|
22
|
+
import { CommandPalette as q } from "./components/CommandPalette/CommandPalette.js";
|
|
23
|
+
import { defaultCommandPaletteLabels as z } from "./components/CommandPalette/labels.js";
|
|
24
|
+
import { DotMatrixLoader as Q } from "./components/DotMatrixLoader/DotMatrixLoader.js";
|
|
25
|
+
import { LaunchLoader as Y } from "./components/LaunchLoader/LaunchLoader.js";
|
|
26
|
+
import { launchBackdropCss as $ } from "./launch-gradient.js";
|
|
27
|
+
import { SettingsFrame as ro } from "./components/SettingsFrame/SettingsFrame.js";
|
|
28
|
+
import { defaultSettingsFrameLabels as eo } from "./components/SettingsFrame/labels.js";
|
|
29
|
+
import { checkNavHeightBudget as po, navBudgetForViewport as fo, navContentHeight as mo } from "./nav-height-budget.js";
|
|
27
30
|
export {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
R as AccountMenu,
|
|
32
|
+
F as AppShell,
|
|
33
|
+
I as AppSwitcher,
|
|
34
|
+
i as BOOST33_MARK_MIN_LEGIBLE_PX,
|
|
35
|
+
l as Boost33Mark,
|
|
36
|
+
q as CommandPalette,
|
|
37
|
+
Q as DotMatrixLoader,
|
|
38
|
+
Y as LaunchLoader,
|
|
36
39
|
s as NavItem,
|
|
37
40
|
p as NavSection,
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
k as NotificationButton,
|
|
42
|
+
O as NotificationCenter,
|
|
43
|
+
_ as PageHeader,
|
|
40
44
|
d as ProductMark,
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
45
|
+
ro as SettingsFrame,
|
|
46
|
+
e as Sidebar,
|
|
47
|
+
M as TopBar,
|
|
48
|
+
h as WalletChip,
|
|
49
|
+
m as Wordmark,
|
|
50
|
+
po as checkNavHeightBudget,
|
|
51
|
+
X as defaultAccountMenuLabels,
|
|
52
|
+
W as defaultAppSwitcherLabels,
|
|
53
|
+
z as defaultCommandPaletteLabels,
|
|
54
|
+
P as defaultNotificationButtonLabels,
|
|
55
|
+
G as defaultNotificationCenterLabels,
|
|
56
|
+
eo as defaultSettingsFrameLabels,
|
|
57
|
+
S as defaultTopBarLabels,
|
|
58
|
+
b as defaultWalletChipLabels,
|
|
59
|
+
u as hasProductMark,
|
|
60
|
+
$ as launchBackdropCss,
|
|
61
|
+
fo as navBudgetForViewport,
|
|
62
|
+
mo as navContentHeight
|
|
57
63
|
};
|
|
58
64
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export declare const NAV_SHELL_PACKAGE_FIXED_PX =
|
|
2
|
-
export declare const NAV_SHELL_CONSUMER_SLOTS_PX =
|
|
1
|
+
export declare const NAV_SHELL_PACKAGE_FIXED_PX = 126;
|
|
2
|
+
export declare const NAV_SHELL_CONSUMER_SLOTS_PX = 44;
|
|
3
3
|
export declare const NAV_SHELL_FIXED_PX: number;
|
|
4
4
|
export declare const NAV_ITEM_PX = 33;
|
|
5
5
|
export declare const NAV_SECTION_HEADER_PX = 30;
|
|
@@ -50,14 +50,17 @@ export interface NavRowCosts {
|
|
|
50
50
|
export declare function navContentHeight(sections: NavSectionShape[], costs?: NavRowCosts): number;
|
|
51
51
|
/**
|
|
52
52
|
* The nav's budget at a given viewport: what is left once the fixed shell parts
|
|
53
|
-
* are taken.
|
|
53
|
+
* are taken. The viewport is the shell's whole height -- since B33-1712 that
|
|
54
|
+
* includes the top bar above the sidebar, which the package furniture counts.
|
|
54
55
|
*
|
|
55
|
-
* `consumerSlotsPx` is the
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* will over-count their column and can
|
|
56
|
+
* `consumerSlotsPx` is the height of the slot this package does not own in the
|
|
57
|
+
* sidebar -- the footer, as the consumer renders it. (The switcher and the
|
|
58
|
+
* wallet chip were sidebar slots too until B33-1712 moved them into the top
|
|
59
|
+
* bar.) It defaults to the stress specimen's 44px allowance, which is what
|
|
60
|
+
* NAV_SHELL_FIXED_PX assumes. A consumer whose footer is taller than that MUST
|
|
61
|
+
* pass their own measured height, or this will over-count their column and can
|
|
62
|
+
* report a fit that scrolls. A consumer passing AppShell's `action` adds 67px
|
|
63
|
+
* to it (B33-1713; see NAV_SHELL_CONSUMER_SLOTS_PX).
|
|
61
64
|
*/
|
|
62
65
|
export declare function navBudgetForViewport(viewportPx: number, consumerSlotsPx?: number): number;
|
|
63
66
|
export interface NavHeightBudget {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const P =
|
|
1
|
+
const P = 126, S = 44, H = 33, X = 30, T = 16, V = 8, N = c(900);
|
|
2
2
|
function i(_, t = {}) {
|
|
3
3
|
if (_.length === 0) return 0;
|
|
4
4
|
const e = t.itemPx ?? 33, E = t.sectionHeaderPx ?? 30, o = _.reduce(
|
|
@@ -7,8 +7,8 @@ function i(_, t = {}) {
|
|
|
7
7
|
), r = 16 * (_.length - 1);
|
|
8
8
|
return o + r + 8;
|
|
9
9
|
}
|
|
10
|
-
function c(_, t =
|
|
11
|
-
return _ -
|
|
10
|
+
function c(_, t = 44) {
|
|
11
|
+
return _ - 126 - t;
|
|
12
12
|
}
|
|
13
13
|
function a(_, t = N) {
|
|
14
14
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nav-height-budget.js","sources":["../src/nav-height-budget.ts"],"sourcesContent":["// A sidebar's nav has to fit above the wallet chip and footer without\n// scrolling. Boost Outbound's admin role has already blown past that once, so\n// this is a real check, not a hypothetical.\n//\n// Sidebar calls checkNavHeightBudget and measureNavHeight itself now, and warns\n// outside a production build when a nav does not fit the column it was given\n// (B33-500, ADR-0005). These stay exported for the other half of the job:\n// checking a nav's shape from its data, before it is ever rendered.\n//\n// WHAT OF THIS IS PUBLISHED (B33-483)\n//\n// src/index.ts re-exports three functions -- navContentHeight,\n// navBudgetForViewport, checkNavHeightBudget -- and three types:\n// NavHeightBudget, NavRowCosts, NavSectionShape. Everything else below is\n// module-internal: the eight NAV_*_PX constants and measureNavHeight are\n// exported from this file for the tests, for NavHeightBudgetWarning and for\n// scripts/nav-height-budget-check.mts, and they do not reach a consumer.\n//\n// That is deliberate and it is what the rest of this header argues for. Every\n// number here is provisional in a documented way -- four of them changed value\n// in a release with no note (B33-763), and the row costs' font dependency is\n// still unmeasured for Windows (the \">>> THE MEASUREMENT THIS MODEL IS STILL\n// MISSING <<<\" block below, B33-1009). Published, they become values a 1.0\n// cannot move without a major bump. Internal, they stay free to move with the\n// measurement that finally pins them. Do not re-add them to the barrel to save\n// a caller a line: the functions apply them as defaults and NavRowCosts takes\n// the override, so no caller needs to read one.\n//\n// ===========================================================================\n// WHAT THIS IS A MODEL OF (B33-762)\n// ===========================================================================\n//\n// Every number below was read off a real render. That much was settled in\n// B33-752. What B33-762 found is that the module never said *which* render --\n// which shell's furniture it measured, which nav state its row costs describe,\n// or whose font metrics those costs came from. Three silent assumptions, and a\n// consumer importing the constants could not see any of them.\n//\n// So they are named here, each with the measurement behind it and each with the\n// direction its error runs. A budget that is wrong and documented is\n// recoverable. A budget that is right and undocumented is what produced this\n// ticket.\n//\n// All three measurements were taken on the built specimen in Chromium at\n// 1440x900, deviceScaleFactor 2, reduced motion -- the screenshot harness's own\n// conditions -- and re-taken on 23 Sep 2026 for this pass.\n//\n// ---------------------------------------------------------------------------\n// Assumption 1: the column is the stress specimen's, and part of it is not ours\n// ---------------------------------------------------------------------------\n//\n// NAV_SHELL_FIXED_PX is not one number this package can be correct about. Most\n// of the column is the package's own furniture, but the switcher, the wallet\n// chip and the footer are *slots* -- the consumer's markup, whose height the\n// package neither sets nor knows. Measured across the two routes that render\n// the whole shell:\n//\n// stress (820px) Wallet (900px)\n// wordmark block 56 56\n// switcher wrapper padding 16 16\n// .foot padding-block-start + gap 32 32\n// nav's negative gutter margin -8 -8\n// WalletChip, expanded 89 89\n// -- package subtotal 185 185\n// switcher slot contents 42 42\n// footer slot contents 44 32\n// -- consumer subtotal 86 74\n// == fixed furniture 271 259\n//\n// The package half is identical on both routes, which is the point: 185px is a\n// number this package owns and can defend. The 12px of difference is entirely\n// the footer slot -- the stress route's avatar row is 8+28+8, the Wallet\n// route's link row is 12+16+4.\n//\n// The default therefore stays pinned to the stress route's 271, unchanged from\n// B33-752. Re-pinning it to the Wallet, as this ticket was originally filed to\n// do, would move a published constant 12px toward the false-\"fits\" direction on\n// the strength of one consumer's footer -- and a consumer with a taller footer\n// than either route would then be handed a budget that over-counts their\n// column, which is exactly the B33-752 defect returning by another door.\n//\n// A consumer whose slots differ passes their own measured total as the second\n// argument to navBudgetForViewport. That is the honest interface: the package\n// states what it owns, and the caller states what they own.\n//\n// ---------------------------------------------------------------------------\n// Assumption 2: the row costs describe the expanded nav, not the rail\n// ---------------------------------------------------------------------------\n//\n// NAV_ITEM_PX and NAV_SECTION_HEADER_PX are expanded-nav costs, and the rail is\n// deliberately out of this model's scope. On the rail the label goes out of\n// flow (position: absolute, clipped), so the 16px icon is the tallest in-flow\n// child and a row measures 8+16+8 = 32px, not 33; and AppShell passes\n// `label={collapsed ? undefined : section.label}`, so NavSection renders no\n// header element at all and a headed section costs 0, not 30.\n//\n// Modelling that would mean a NAV_RAIL_ITEM_PX and a `collapsed` flag on\n// NavSectionShape -- a public API change to a module four products import, to\n// describe a state that cannot overflow. The measurement says it cannot. Same\n// specimen, same 13-item shape, same 820px stage:\n//\n// expanded rail\n// nav scrollHeight 575 472\n// column left for the nav 549 587\n// verdict scrolls fits, 115px spare\n//\n// The rail is shorter on both sides at once: its rows lose the label's line\n// box, and its furniture shrinks too (the wallet chip collapses 89 -> 75, the\n// switcher 42 -> 34, the footer 44 -> 28). So checking a rail nav against the\n// expanded model over-counts its content by 103px and under-counts its column\n// by 38px -- 141px of slack, both errors in the safe direction. The expanded\n// model is a conservative bound on the rail, and scripts/nav-height-budget-\n// check.mts now drives the rail stage and asserts exactly that, so the claim\n// fails CI if it ever stops being true.\n//\n// The runtime half was never affected: Sidebar measures the real DOM, so its\n// dev warning is correct in either state.\n//\n// ---------------------------------------------------------------------------\n// Assumption 3: the row costs are Inter's line boxes, and the package ships no\n// font\n// ---------------------------------------------------------------------------\n//\n// Neither NavItem's row and label nor NavSection's header sets a line-height;\n// the computed value is `normal`, so half of each row cost is token-derived\n// padding and the rest is whatever line box the consumer's font draws. This\n// package ships no font -- there is no @font-face anywhere in src/, `files` is\n// [\"dist\"], and a guard test forbids src/ from reaching the specimen's font\n// file. The faces are the consumer's; the tokens carry only a fallback stack.\n//\n// Swapping --font-sans on the built specimen, same stage, same 13-item shape:\n//\n// family row header nav scrollHeight\n// Inter (the tokens' head) 33 30 575\n// system-ui 33 29 572\n// Helvetica 32 29 559\n// Arial 32 28 556\n// Georgia 32 28 556\n//\n// Every face reachable on macOS renders shorter than Inter, so the model\n// over-counts -- the safe direction. The constants therefore stay as they are\n// and this dependency is recorded rather than removed.\n//\n// Removing it would mean setting an explicit line-height from the --leading-*\n// tokens, the way PageHeader already does (its title state measures a flat\n// 156px across every face). That is the better model, and it is not done here\n// on purpose: --leading-tight at --text-2xs gives a 29px section header against\n// today's 30, which moves the sidebar-*, navsection-* and appshell-* renders by\n// a pixel. A baseline change is a human approving a design change (CLAUDE.md),\n// and the pixel-ratio diff would wave a 1px header through in silence anyway\n// (B33-548, B33-772). It wants its own ticket with a human at the fidelity gate.\n//\n// >>> THE MEASUREMENT THIS MODEL IS STILL MISSING <<<\n//\n// 'Segoe UI' sits in the fallback stack and is reported to carry a ~1.33 normal\n// line-height factor, which at 14px would give a ~19px line box and a 35px row\n// -- an UNDER-count, the one direction B33-752 exists to forbid. It is NOT\n// recorded above because it could not be measured: this pass ran on macOS,\n// where Segoe UI is not installed. Requesting it resolves to the fallback --\n// canvas measureText at 14px gives 'Segoe UI' 241.097px for the same string as\n// a deliberately nonsense family name, against 249.737px for Helvetica and\n// 268.837px for Inter -- so the 32/29 a naive swap reports is Helvetica wearing\n// Segoe UI's name, not Segoe UI.\n//\n// A human on Windows has to take it, and nothing here should be tuned to a\n// guessed number in the meantime. Until then the honest statement of this\n// module's scope is: these costs hold for a --font-sans whose `normal` line box\n// at 14px is 17px or less, which is every face measured. A Windows consumer is\n// outside that and is protected only by Sidebar's runtime warning, which\n// measures the real DOM and is right whatever the font does.\n//\n// The browser check cannot close this either -- the specimen bundles Inter, so\n// it can only ever confirm Inter's numbers.\n//\n// ===========================================================================\n//\n// NOTE for a human: boostledger/src/ui/shell/nav.ts carries the same constants\n// and the same navHeight arithmetic, inherited from the architecture doc's\n// §4.1, and has the same under-count/over-count. It is in another repo and\n// another GitHub org, so it is not corrected here; it wants the same fix\n// (B33-752).\n\n// The part of the fixed furniture this package draws itself, and is therefore\n// the only part it can be correct about: the wordmark block (56), the switcher\n// wrapper's padding (16), `.foot`'s padding-block-start and gap (32) and the\n// expanded WalletChip (89), less the 8px the nav takes back as negative margin\n// for its focus gutter. Measured identical on both shell routes -- see\n// \"Assumption 1\" above. The nav's own scroll gutter is NOT part of this: it\n// sits inside the nav's client box, so it lives in the budget (and in\n// navContentHeight), not in the furniture -- counting it here as well would\n// double it.\nexport const NAV_SHELL_PACKAGE_FIXED_PX = 185\n\n// The slots the consumer fills, as the stress specimen fills them: a 42px\n// switcher button and a 44px account footer. This is an ALLOWANCE, not a\n// package fact -- the Wallet route's own slots total 74px, and a consumer with\n// a taller footer than either exceeds it. A consumer who knows their own slot\n// heights passes the total to navBudgetForViewport instead of taking this.\nexport const NAV_SHELL_CONSUMER_SLOTS_PX = 86\n\n// The sidebar's fixed furniture above and below the nav, as the stress\n// specimen renders it: the package's own 185 plus that route's 86 of slots.\n// Equal to the column height less the nav's own client box (820 - 549), which\n// is how B33-752 measured it and why the value is unchanged from it.\nexport const NAV_SHELL_FIXED_PX = NAV_SHELL_PACKAGE_FIXED_PX + NAV_SHELL_CONSUMER_SLOTS_PX\n\n// The per-row costs of the EXPANDED nav, each the padding the CSS sets plus the\n// line box the text draws into. A nav item is --spacing-2 top and bottom (16px)\n// around a 14px label's 17px line box; a section header is the same 16px around\n// an 11px label's 14px line box. Measured in the specimen render, not derived\n// from Paper (see NavItem.module.css `.row` and NavSection.module.css\n// `.header`).\n//\n// Two things these numbers do not cover, both argued in full above. They are\n// the rail's costs only by accident -- a rail row is 32px and a rail section\n// header is nothing at all (assumption 2) -- and their line-box half belongs to\n// the consumer's font, not to this package (assumption 3). Both errors run in\n// the over-counting direction for every face that could be measured; the\n// Windows case is unmeasured and is the one candidate for the other direction.\nexport const NAV_ITEM_PX = 33\nexport const NAV_SECTION_HEADER_PX = 30\n\n// The gap the nav sets between its sections (--spacing-4, AppShell.module.css\n// `.nav`), counted once between each adjacent pair.\nexport const NAV_SECTION_GAP_PX = 16\n\n// The nav's own scroll gutter: --spacing-1 of padding top and bottom, the 4px\n// each way that keeps NavItem's focus ring off the scrollport edge (ADR-0005).\n// It is part of the nav's box, so it counts toward the height the nav takes.\nexport const NAV_FOCUS_GUTTER_PX = 8\n\n// The nav budget at the reference 900px viewport: what the column leaves once\n// the fixed furniture is taken. Derived from NAV_SHELL_FIXED_PX so the two\n// cannot disagree; used as the default budget and named in the dev warning.\nexport const NAV_HEIGHT_BUDGET_PX = navBudgetForViewport(900)\n\n/** One section of a nav, described by its shape rather than its rendered rows. */\nexport interface NavSectionShape {\n /** Whether the section carries a header label (its NAV_SECTION_HEADER_PX cost). */\n hasHeader: boolean\n /** How many nav items the section holds (NAV_ITEM_PX each). */\n itemCount: number\n}\n\n/**\n * The per-row costs to charge, for a consumer who has measured their own.\n *\n * The defaults are Inter's, and are right for every face this package could\n * measure. A consumer rendering in a font whose `normal` line box at 14px is\n * taller than 17px -- `Segoe UI` is the known candidate, unmeasured -- gets a\n * model that under-counts, and can correct it here rather than forking the\n * check. Measure a rendered row's `offsetHeight`, do not estimate it.\n *\n * The NAV_*_PX constants named below are this module's own and are not published\n * (B33-483); they are the defaults, not something a caller reads. Omitting a\n * field here takes the default, which is why there is nothing to import.\n */\nexport interface NavRowCosts {\n /** What one nav row costs. Defaults to NAV_ITEM_PX, the expanded row in Inter. */\n itemPx?: number\n /** What one section header costs. Defaults to NAV_SECTION_HEADER_PX. */\n sectionHeaderPx?: number\n}\n\n/**\n * The vertical space an EXPANDED nav takes, summed from its sections' shapes --\n * the rows and headers, the gaps the nav sets between sections, and the nav's\n * own scroll gutter. This is the whole box the nav occupies (its rendered\n * scrollHeight), so the budget can be checked from the nav's data alone, before\n * it is rendered into a sidebar that might not fit it -- Boost Outbound's admin\n * role has overflowed this once already.\n *\n * The rail is out of scope and needs no separate model: a collapsed nav is\n * shorter than the same nav expanded, in a column that is taller, so this is a\n * conservative bound on it (B33-762, and the check that pins it).\n *\n * Pass `costs` only if you have measured a rendered row in your own font; the\n * defaults are the package's, measured in Inter.\n */\nexport function navContentHeight(sections: NavSectionShape[], costs: NavRowCosts = {}): number {\n if (sections.length === 0) return 0\n const itemPx = costs.itemPx ?? NAV_ITEM_PX\n const sectionHeaderPx = costs.sectionHeaderPx ?? NAV_SECTION_HEADER_PX\n const rows = sections.reduce(\n (used, section) => used + (section.hasHeader ? sectionHeaderPx : 0) + section.itemCount * itemPx,\n 0,\n )\n const gaps = NAV_SECTION_GAP_PX * (sections.length - 1)\n return rows + gaps + NAV_FOCUS_GUTTER_PX\n}\n\n/**\n * The nav's budget at a given viewport: what is left once the fixed shell parts\n * are taken.\n *\n * `consumerSlotsPx` is the total height of the three slots this package does\n * not own -- the switcher, the wallet chip's neighbours and the sidebar footer\n * as the consumer renders them. It defaults to the stress specimen's 86px\n * allowance, which is what NAV_SHELL_FIXED_PX assumes. A consumer whose footer\n * or switcher is taller than that MUST pass their own measured total, or this\n * will over-count their column and can report a fit that scrolls.\n */\nexport function navBudgetForViewport(\n viewportPx: number,\n consumerSlotsPx: number = NAV_SHELL_CONSUMER_SLOTS_PX,\n): number {\n return viewportPx - NAV_SHELL_PACKAGE_FIXED_PX - consumerSlotsPx\n}\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":["NAV_SHELL_PACKAGE_FIXED_PX","NAV_SHELL_CONSUMER_SLOTS_PX","NAV_ITEM_PX","NAV_SECTION_HEADER_PX","NAV_SECTION_GAP_PX","NAV_FOCUS_GUTTER_PX","NAV_HEIGHT_BUDGET_PX","navBudgetForViewport","navContentHeight","sections","costs","itemPx","sectionHeaderPx","rows","used","section","gaps","viewportPx","consumerSlotsPx","checkNavHeightBudget","usedPx","budgetPx","measureNavHeight","nav"],"mappings":"AA+LO,MAAMA,IAA6B,KAO7BC,IAA8B,IAqB9BC,IAAc,IACdC,IAAwB,IAIxBC,IAAqB,IAKrBC,IAAsB,GAKtBC,IAAuBC,EAAqB,GAAG;AA6CrD,SAASC,EAAiBC,GAA6BC,IAAqB,IAAY;AAC7F,MAAID,EAAS,WAAW,EAAG,QAAO;AAClC,QAAME,IAASD,EAAM,UAAU,IACzBE,IAAkBF,EAAM,mBAAmB,IAC3CG,IAAOJ,EAAS;AAAA,IACpB,CAACK,GAAMC,MAAYD,KAAQC,EAAQ,YAAYH,IAAkB,KAAKG,EAAQ,YAAYJ;AAAA,IAC1F;AAAA,EAAA,GAEIK,IAAO,MAAsBP,EAAS,SAAS;AACrD,SAAOI,IAAOG,IAAO;AACvB;AAaO,SAAST,EACdU,GACAC,IAA0B,IAClB;AACR,SAAOD,IAAa,MAA6BC;AACnD;AASO,SAASC,EAAqBC,GAAgBC,IAAmBf,GAAuC;AAC7G,SAAO;AAAA,IACL,QAAAc;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":"nav-height-budget.js","sources":["../src/nav-height-budget.ts"],"sourcesContent":["// A sidebar's nav has to fit above the wallet chip and footer without\n// scrolling. Boost Outbound's admin role has already blown past that once, so\n// this is a real check, not a hypothetical.\n//\n// Sidebar calls checkNavHeightBudget and measureNavHeight itself now, and warns\n// outside a production build when a nav does not fit the column it was given\n// (B33-500, ADR-0005). These stay exported for the other half of the job:\n// checking a nav's shape from its data, before it is ever rendered.\n//\n// WHAT OF THIS IS PUBLISHED (B33-483)\n//\n// src/index.ts re-exports three functions -- navContentHeight,\n// navBudgetForViewport, checkNavHeightBudget -- and three types:\n// NavHeightBudget, NavRowCosts, NavSectionShape. Everything else below is\n// module-internal: the eight NAV_*_PX constants and measureNavHeight are\n// exported from this file for the tests, for NavHeightBudgetWarning and for\n// scripts/nav-height-budget-check.mts, and they do not reach a consumer.\n//\n// That is deliberate and it is what the rest of this header argues for. Every\n// number here is provisional in a documented way -- four of them changed value\n// in a release with no note (B33-763), and the row costs' font dependency is\n// still unmeasured for Windows (the \">>> THE MEASUREMENT THIS MODEL IS STILL\n// MISSING <<<\" block below, B33-1009). Published, they become values a 1.0\n// cannot move without a major bump. Internal, they stay free to move with the\n// measurement that finally pins them. Do not re-add them to the barrel to save\n// a caller a line: the functions apply them as defaults and NavRowCosts takes\n// the override, so no caller needs to read one.\n//\n// ===========================================================================\n// WHAT THIS IS A MODEL OF (B33-762)\n// ===========================================================================\n//\n// Every number below was read off a real render. That much was settled in\n// B33-752. What B33-762 found is that the module never said *which* render --\n// which shell's furniture it measured, which nav state its row costs describe,\n// or whose font metrics those costs came from. Three silent assumptions, and a\n// consumer importing the constants could not see any of them.\n//\n// So they are named here, each with the measurement behind it and each with the\n// direction its error runs. A budget that is wrong and documented is\n// recoverable. A budget that is right and undocumented is what produced this\n// ticket.\n//\n// All three measurements were taken on the built specimen in Chromium at\n// 1440x900, deviceScaleFactor 2, reduced motion -- the screenshot harness's own\n// conditions -- and re-taken on 23 Sep 2026 for this pass.\n//\n// ---------------------------------------------------------------------------\n// Assumption 1: the column is the stress specimen's, and part of it is not ours\n// ---------------------------------------------------------------------------\n//\n// NAV_SHELL_FIXED_PX is not one number this package can be correct about. Most\n// of it is the package's own furniture, but the footer is a *slot* -- the\n// consumer's markup, whose height the package neither sets nor knows.\n//\n// B33-1712 (the 3 Nov 2026 shell redesign) moved the wordmark, the switcher and\n// the wallet chip out of the sidebar and put a full-width top bar above it, so\n// the furniture changed shape. Measured across the two routes that render the\n// whole shell:\n//\n// stress (700px) Wallet (900px)\n// top bar, above the sidebar 48 48\n// sidebar padding, top and bottom 32 32\n// app name block (4 + 20 + 14) 38 (38)\n// .foot padding-block-start 16 16\n// nav's negative gutter margin -8 -8\n// -- package subtotal 126 126\n// footer slot contents 44 32\n// -- consumer subtotal 44 32\n// == fixed furniture 170 158\n//\n// The app name is drawn by the package but only when the consumer passes\n// `appName`. The Wallet route predates it and passes none, so its 38 is counted\n// here as the allowance every redesigned product will carry; a shell without\n// one is given 38px less budget than it has -- the safe direction.\n//\n// Until B33-1712 the package half was 185 (the wordmark block 56, the switcher\n// wrapper's padding 16, .foot's padding and gap 32, the expanded wallet chip\n// 89, less the 8px gutter) and the consumer half 86 (a 42px switcher and the\n// 44px footer). The total fell from 271 to 170, so the default budget at 900px\n// rose from 629 to 730: the redesign gave the nav 101px.\n//\n// The default consumer allowance stays pinned to the stress route's footer\n// (44) rather than the Wallet's (32), for the reason B33-752 gave: re-pinning it\n// to the shorter of the two would move a published behaviour toward the\n// false-\"fits\" direction on the strength of one consumer's footer.\n//\n// A consumer whose slots differ passes their own measured total as the second\n// argument to navBudgetForViewport. That is the honest interface: the package\n// states what it owns, and the caller states what they own.\n//\n// ---------------------------------------------------------------------------\n// Assumption 2: the row costs describe the expanded nav, not the rail\n// ---------------------------------------------------------------------------\n//\n// NAV_ITEM_PX and NAV_SECTION_HEADER_PX are expanded-nav costs, and the rail is\n// deliberately out of this model's scope. On the rail the label goes out of\n// flow (position: absolute, clipped), so the 16px icon is the tallest in-flow\n// child and a row measures 8+16+8 = 32px, not 33; and AppShell passes\n// `label={collapsed ? undefined : section.label}`, so NavSection renders no\n// header element at all and a headed section costs 0, not 30.\n//\n// Modelling that would mean a NAV_RAIL_ITEM_PX and a `collapsed` flag on\n// NavSectionShape -- a public API change to a module four products import, to\n// describe a state that cannot overflow. The measurement says it cannot. Same\n// specimen, same 13-item shape, same 820px stage:\n//\n// expanded rail\n// nav scrollHeight 575 472\n// column left for the nav 549 587\n// verdict scrolls fits, 115px spare\n//\n// The rail is shorter on both sides at once: its rows lose the label's line\n// box, and its furniture shrinks too (the wallet chip collapses 89 -> 75, the\n// switcher 42 -> 34, the footer 44 -> 28). So checking a rail nav against the\n// expanded model over-counts its content by 103px and under-counts its column\n// by 38px -- 141px of slack, both errors in the safe direction. The expanded\n// model is a conservative bound on the rail, and scripts/nav-height-budget-\n// check.mts now drives the rail stage and asserts exactly that, so the claim\n// fails CI if it ever stops being true.\n//\n// The runtime half was never affected: Sidebar measures the real DOM, so its\n// dev warning is correct in either state.\n//\n// ---------------------------------------------------------------------------\n// Assumption 3: the row costs are Inter's line boxes, and the package ships no\n// font\n// ---------------------------------------------------------------------------\n//\n// Neither NavItem's row and label nor NavSection's header sets a line-height;\n// the computed value is `normal`, so half of each row cost is token-derived\n// padding and the rest is whatever line box the consumer's font draws. This\n// package ships no font -- there is no @font-face anywhere in src/, `files` is\n// [\"dist\"], and a guard test forbids src/ from reaching the specimen's font\n// file. The faces are the consumer's; the tokens carry only a fallback stack.\n//\n// Swapping --font-sans on the built specimen, same stage, same 13-item shape:\n//\n// family row header nav scrollHeight\n// Inter (the tokens' head) 33 30 575\n// system-ui 33 29 572\n// Helvetica 32 29 559\n// Arial 32 28 556\n// Georgia 32 28 556\n//\n// Every face reachable on macOS renders shorter than Inter, so the model\n// over-counts -- the safe direction. The constants therefore stay as they are\n// and this dependency is recorded rather than removed.\n//\n// Removing it would mean setting an explicit line-height from the --leading-*\n// tokens, the way PageHeader already does (its title state measures a flat\n// 156px across every face). That is the better model, and it is not done here\n// on purpose: --leading-tight at --text-2xs gives a 29px section header against\n// today's 30, which moves the sidebar-*, navsection-* and appshell-* renders by\n// a pixel. A baseline change is a human approving a design change (CLAUDE.md),\n// and the pixel-ratio diff would wave a 1px header through in silence anyway\n// (B33-548, B33-772). It wants its own ticket with a human at the fidelity gate.\n//\n// >>> THE MEASUREMENT THIS MODEL IS STILL MISSING <<<\n//\n// 'Segoe UI' sits in the fallback stack and is reported to carry a ~1.33 normal\n// line-height factor, which at 14px would give a ~19px line box and a 35px row\n// -- an UNDER-count, the one direction B33-752 exists to forbid. It is NOT\n// recorded above because it could not be measured: this pass ran on macOS,\n// where Segoe UI is not installed. Requesting it resolves to the fallback --\n// canvas measureText at 14px gives 'Segoe UI' 241.097px for the same string as\n// a deliberately nonsense family name, against 249.737px for Helvetica and\n// 268.837px for Inter -- so the 32/29 a naive swap reports is Helvetica wearing\n// Segoe UI's name, not Segoe UI.\n//\n// A human on Windows has to take it, and nothing here should be tuned to a\n// guessed number in the meantime. Until then the honest statement of this\n// module's scope is: these costs hold for a --font-sans whose `normal` line box\n// at 14px is 17px or less, which is every face measured. A Windows consumer is\n// outside that and is protected only by Sidebar's runtime warning, which\n// measures the real DOM and is right whatever the font does.\n//\n// The browser check cannot close this either -- the specimen bundles Inter, so\n// it can only ever confirm Inter's numbers.\n//\n// ===========================================================================\n//\n// NOTE for a human: boostledger/src/ui/shell/nav.ts carries the same constants\n// and the same navHeight arithmetic, inherited from the architecture doc's\n// §4.1, and has the same under-count/over-count. It is in another repo and\n// another GitHub org, so it is not corrected here; it wants the same fix\n// (B33-752).\n\n// The part of the fixed furniture this package draws itself, and is therefore\n// the only part it can be correct about: the top bar above the sidebar (48), the\n// sidebar's own padding (32), the app name block (38), and `.foot`'s\n// padding-block-start (16), less the 8px the nav takes back as negative margin\n// for its focus gutter. 185 until B33-1712 moved the wordmark, the switcher and\n// the wallet chip out of the sidebar -- see \"Assumption 1\" above. The nav's own\n// scroll gutter is NOT part of this: it sits inside the nav's client box, so it\n// lives in the budget (and in navContentHeight), not in the furniture --\n// counting it here as well would double it.\nexport const NAV_SHELL_PACKAGE_FIXED_PX = 126\n\n// The slot the consumer fills, as the stress specimen fills it: a 44px account\n// footer. This is an ALLOWANCE, not a package fact -- the Wallet route's own\n// footer is 32px, and a consumer with a taller footer exceeds it. A consumer who\n// knows their own slot height passes it to navBudgetForViewport instead of\n// taking this. 86 until B33-1712, which took the 42px switcher out of the\n// sidebar.\n//\n// AppShell's `action` (B33-1713) is not in it: the stress specimen passes none,\n// and most screens of a product do not either. A consumer that passes one adds\n// 56px -- the 40px primary button and the 16px clear above it -- plus the 10px\n// and hairline the footer then takes above its rows, so 67px in all.\nexport const NAV_SHELL_CONSUMER_SLOTS_PX = 44\n\n// The fixed furniture above and below the nav, as the stress specimen renders\n// it: the package's own 126 plus that route's 44 of slot. Equal to the stage\n// height less the nav's own client box (700 - 530).\nexport const NAV_SHELL_FIXED_PX = NAV_SHELL_PACKAGE_FIXED_PX + NAV_SHELL_CONSUMER_SLOTS_PX\n\n// The per-row costs of the EXPANDED nav, each the padding the CSS sets plus the\n// line box the text draws into. A nav item is --spacing-2 top and bottom (16px)\n// around a 14px label's 17px line box; a section header is the same 16px around\n// an 11px label's 14px line box. Measured in the specimen render, not derived\n// from Paper (see NavItem.module.css `.row` and NavSection.module.css\n// `.header`).\n//\n// Two things these numbers do not cover, both argued in full above. They are\n// the rail's costs only by accident -- a rail row is 32px and a rail section\n// header is nothing at all (assumption 2) -- and their line-box half belongs to\n// the consumer's font, not to this package (assumption 3). Both errors run in\n// the over-counting direction for every face that could be measured; the\n// Windows case is unmeasured and is the one candidate for the other direction.\nexport const NAV_ITEM_PX = 33\nexport const NAV_SECTION_HEADER_PX = 30\n\n// The gap the nav sets between its sections (--spacing-4, AppShell.module.css\n// `.nav`), counted once between each adjacent pair.\nexport const NAV_SECTION_GAP_PX = 16\n\n// The nav's own scroll gutter: --spacing-1 of padding top and bottom, the 4px\n// each way that keeps NavItem's focus ring off the scrollport edge (ADR-0005).\n// It is part of the nav's box, so it counts toward the height the nav takes.\nexport const NAV_FOCUS_GUTTER_PX = 8\n\n// The nav budget at the reference 900px viewport: what the column leaves once\n// the fixed furniture is taken. Derived from NAV_SHELL_FIXED_PX so the two\n// cannot disagree; used as the default budget and named in the dev warning.\nexport const NAV_HEIGHT_BUDGET_PX = navBudgetForViewport(900)\n\n/** One section of a nav, described by its shape rather than its rendered rows. */\nexport interface NavSectionShape {\n /** Whether the section carries a header label (its NAV_SECTION_HEADER_PX cost). */\n hasHeader: boolean\n /** How many nav items the section holds (NAV_ITEM_PX each). */\n itemCount: number\n}\n\n/**\n * The per-row costs to charge, for a consumer who has measured their own.\n *\n * The defaults are Inter's, and are right for every face this package could\n * measure. A consumer rendering in a font whose `normal` line box at 14px is\n * taller than 17px -- `Segoe UI` is the known candidate, unmeasured -- gets a\n * model that under-counts, and can correct it here rather than forking the\n * check. Measure a rendered row's `offsetHeight`, do not estimate it.\n *\n * The NAV_*_PX constants named below are this module's own and are not published\n * (B33-483); they are the defaults, not something a caller reads. Omitting a\n * field here takes the default, which is why there is nothing to import.\n */\nexport interface NavRowCosts {\n /** What one nav row costs. Defaults to NAV_ITEM_PX, the expanded row in Inter. */\n itemPx?: number\n /** What one section header costs. Defaults to NAV_SECTION_HEADER_PX. */\n sectionHeaderPx?: number\n}\n\n/**\n * The vertical space an EXPANDED nav takes, summed from its sections' shapes --\n * the rows and headers, the gaps the nav sets between sections, and the nav's\n * own scroll gutter. This is the whole box the nav occupies (its rendered\n * scrollHeight), so the budget can be checked from the nav's data alone, before\n * it is rendered into a sidebar that might not fit it -- Boost Outbound's admin\n * role has overflowed this once already.\n *\n * The rail is out of scope and needs no separate model: a collapsed nav is\n * shorter than the same nav expanded, in a column that is taller, so this is a\n * conservative bound on it (B33-762, and the check that pins it).\n *\n * Pass `costs` only if you have measured a rendered row in your own font; the\n * defaults are the package's, measured in Inter.\n */\nexport function navContentHeight(sections: NavSectionShape[], costs: NavRowCosts = {}): number {\n if (sections.length === 0) return 0\n const itemPx = costs.itemPx ?? NAV_ITEM_PX\n const sectionHeaderPx = costs.sectionHeaderPx ?? NAV_SECTION_HEADER_PX\n const rows = sections.reduce(\n (used, section) => used + (section.hasHeader ? sectionHeaderPx : 0) + section.itemCount * itemPx,\n 0,\n )\n const gaps = NAV_SECTION_GAP_PX * (sections.length - 1)\n return rows + gaps + NAV_FOCUS_GUTTER_PX\n}\n\n/**\n * The nav's budget at a given viewport: what is left once the fixed shell parts\n * are taken. The viewport is the shell's whole height -- since B33-1712 that\n * includes the top bar above the sidebar, which the package furniture counts.\n *\n * `consumerSlotsPx` is the height of the slot this package does not own in the\n * sidebar -- the footer, as the consumer renders it. (The switcher and the\n * wallet chip were sidebar slots too until B33-1712 moved them into the top\n * bar.) It defaults to the stress specimen's 44px allowance, which is what\n * NAV_SHELL_FIXED_PX assumes. A consumer whose footer is taller than that MUST\n * pass their own measured height, or this will over-count their column and can\n * report a fit that scrolls. A consumer passing AppShell's `action` adds 67px\n * to it (B33-1713; see NAV_SHELL_CONSUMER_SLOTS_PX).\n */\nexport function navBudgetForViewport(\n viewportPx: number,\n consumerSlotsPx: number = NAV_SHELL_CONSUMER_SLOTS_PX,\n): number {\n return viewportPx - NAV_SHELL_PACKAGE_FIXED_PX - consumerSlotsPx\n}\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":["NAV_SHELL_PACKAGE_FIXED_PX","NAV_SHELL_CONSUMER_SLOTS_PX","NAV_ITEM_PX","NAV_SECTION_HEADER_PX","NAV_SECTION_GAP_PX","NAV_FOCUS_GUTTER_PX","NAV_HEIGHT_BUDGET_PX","navBudgetForViewport","navContentHeight","sections","costs","itemPx","sectionHeaderPx","rows","used","section","gaps","viewportPx","consumerSlotsPx","checkNavHeightBudget","usedPx","budgetPx","measureNavHeight","nav"],"mappings":"AAqMO,MAAMA,IAA6B,KAa7BC,IAA8B,IAoB9BC,IAAc,IACdC,IAAwB,IAIxBC,IAAqB,IAKrBC,IAAsB,GAKtBC,IAAuBC,EAAqB,GAAG;AA6CrD,SAASC,EAAiBC,GAA6BC,IAAqB,IAAY;AAC7F,MAAID,EAAS,WAAW,EAAG,QAAO;AAClC,QAAME,IAASD,EAAM,UAAU,IACzBE,IAAkBF,EAAM,mBAAmB,IAC3CG,IAAOJ,EAAS;AAAA,IACpB,CAACK,GAAMC,MAAYD,KAAQC,EAAQ,YAAYH,IAAkB,KAAKG,EAAQ,YAAYJ;AAAA,IAC1F;AAAA,EAAA,GAEIK,IAAO,MAAsBP,EAAS,SAAS;AACrD,SAAOI,IAAOG,IAAO;AACvB;AAgBO,SAAST,EACdU,GACAC,IAA0B,IAClB;AACR,SAAOD,IAAa,MAA6BC;AACnD;AASO,SAASC,EAAqBC,GAAgBC,IAAmBf,GAAuC;AAC7G,SAAO;AAAA,IACL,QAAAc;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;"}
|