@aglyn/shared-ui-jsx 1.0.0-beta.146 → 1.0.0-beta.149

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/shared-ui-jsx",
3
- "version": "1.0.0-beta.146",
3
+ "version": "1.0.0-beta.149",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,10 +25,10 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/shared-data-mdi": "1.0.0-beta.146",
29
- "@aglyn/shared-ui-theme": "1.0.0-beta.146",
30
- "@aglyn/shared-util-tools": "1.0.0-beta.146",
31
- "@aglyn/shared-util-vendor": "1.0.0-beta.146",
28
+ "@aglyn/shared-data-mdi": "1.0.0-beta.149",
29
+ "@aglyn/shared-ui-theme": "1.0.0-beta.149",
30
+ "@aglyn/shared-util-tools": "1.0.0-beta.149",
31
+ "@aglyn/shared-util-vendor": "1.0.0-beta.149",
32
32
  "@mdi/js": "^7.4.47",
33
33
  "@swc/helpers": "0.5.23",
34
34
  "change-case": "^5.4.4",
@@ -1,4 +1,24 @@
1
1
  import type { ReactNode } from 'react';
2
+ /**
3
+ * An average in currency, together with everything needed to say what it is
4
+ * an average OF (AGL-3080).
5
+ *
6
+ * {@link MeasuredRate}'s contract in money, and it goes wrong the same way:
7
+ * "$0.42 per recipient" is meaningless without the recipient count, and worse
8
+ * than meaningless when the reader assumes a different one from the writer.
9
+ * Minor units throughout, because the per-message figure is fractional by
10
+ * nature and rounding it at the model would lose the thing being measured.
11
+ */
12
+ export interface MeasuredMoney {
13
+ /** Minor units per unit of the denominator. Fractional by nature. */
14
+ cents: number;
15
+ numeratorCents: number;
16
+ denominator: number;
17
+ /** Reader-facing name of the denominator, e.g. `'delivered'`. */
18
+ denominatorLabel: string;
19
+ /** Lowercase ISO code as it was recorded, e.g. `'usd'`. */
20
+ currency: string;
21
+ }
2
22
  /**
3
23
  * A rate, together with everything needed to say what it is a rate OF.
4
24
  *
@@ -62,3 +82,43 @@ export declare function RateRow(props: {
62
82
  export declare namespace RateRow {
63
83
  var displayName: string;
64
84
  }
85
+ /**
86
+ * One amount of money, in the currency it was recorded in.
87
+ *
88
+ * NOT a figure primitive, and deliberately separate from {@link Figure}: it
89
+ * carries a currency, and every surface that shows these refuses to sum two
90
+ * of them into one number. A currency code the platform cannot format falls
91
+ * back to the amount beside the code, which is still true, rather than
92
+ * throwing inside a render.
93
+ */
94
+ export declare function money(cents: number, currency: string): string;
95
+ /**
96
+ * One money figure, with the population it is averaged over named.
97
+ *
98
+ * The rule {@link RateRow} enforces for a percentage, applied to an average.
99
+ * `null` draws the reason rather than a zero, for {@link Figure}'s reason.
100
+ */
101
+ export declare function MoneyPerUnitRow(props: {
102
+ label: string;
103
+ figure: MeasuredMoney | null;
104
+ }): import("react").JSX.Element;
105
+ export declare namespace MoneyPerUnitRow {
106
+ var displayName: string;
107
+ }
108
+ /**
109
+ * One money total, with what it counts named underneath.
110
+ *
111
+ * {@link Figure}'s contract in currency: `null` draws a dash and never a
112
+ * zero, because "no attribution has ever been recorded" and "this earned
113
+ * nothing" lead a merchant to opposite conclusions about whether to send
114
+ * another one.
115
+ */
116
+ export declare function MoneyFigure(props: {
117
+ label: string;
118
+ cents: number | null;
119
+ currency: string;
120
+ note: string;
121
+ }): import("react").JSX.Element;
122
+ export declare namespace MoneyFigure {
123
+ var displayName: string;
124
+ }
@@ -38,9 +38,15 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
38
38
  *
39
39
  * They live in one module because a second copy is a second chance to render
40
40
  * a percentage without its denominator — and the copy would be written by
41
- * whoever adds the next card, in a file nobody tests for it. The email
42
- * plugin's `report-figures` re-exports these rather than keeping its own,
43
- * which is what stopped the console's forms surface from becoming that copy.
41
+ * whoever adds the next card, in a file nobody tests for it.
42
+ *
43
+ * MONEY joined them (AGL-3080), from the campaign lib that had kept its own.
44
+ * It is not a fourth way of measuring — it is the same two, in currency: a
45
+ * total that draws a dash rather than a zero, and an average that names the
46
+ * population it is over. The rule they enforce does not become someone
47
+ * else's the moment the number has a currency on it, and the copy that was
48
+ * left behind sat in a library only the email surfaces could reach, so the
49
+ * next surface to show money would have written a third.
44
50
  */ import { Stack, Typography } from "@mui/material";
45
51
  /**
46
52
  * A rate, or `null` when one cannot honestly be taken.
@@ -157,5 +163,102 @@ Figure.displayName = 'Figure';
157
163
  });
158
164
  }
159
165
  RateRow.displayName = 'RateRow';
166
+ /**
167
+ * One amount of money, in the currency it was recorded in.
168
+ *
169
+ * NOT a figure primitive, and deliberately separate from {@link Figure}: it
170
+ * carries a currency, and every surface that shows these refuses to sum two
171
+ * of them into one number. A currency code the platform cannot format falls
172
+ * back to the amount beside the code, which is still true, rather than
173
+ * throwing inside a render.
174
+ */ export function money(cents, currency) {
175
+ const amount = cents / 100;
176
+ try {
177
+ return new Intl.NumberFormat(undefined, {
178
+ style: 'currency',
179
+ currency: currency.toUpperCase()
180
+ }).format(amount);
181
+ } catch (unused) {
182
+ return `${amount.toFixed(2)} ${currency.toUpperCase()}`;
183
+ }
184
+ }
185
+ /**
186
+ * One money figure, with the population it is averaged over named.
187
+ *
188
+ * The rule {@link RateRow} enforces for a percentage, applied to an average.
189
+ * `null` draws the reason rather than a zero, for {@link Figure}'s reason.
190
+ */ export function MoneyPerUnitRow(props) {
191
+ const { label, figure } = props;
192
+ return /*#__PURE__*/ _jsxs(Stack, {
193
+ direction: "row",
194
+ spacing: 2,
195
+ sx: {
196
+ justifyContent: 'space-between',
197
+ alignItems: 'baseline'
198
+ },
199
+ children: [
200
+ /*#__PURE__*/ _jsx(Typography, {
201
+ variant: "body2",
202
+ children: label
203
+ }),
204
+ figure ? /*#__PURE__*/ _jsxs(Stack, {
205
+ direction: "row",
206
+ spacing: 1,
207
+ sx: {
208
+ alignItems: 'baseline'
209
+ },
210
+ children: [
211
+ /*#__PURE__*/ _jsx(Typography, {
212
+ variant: "body2",
213
+ sx: {
214
+ fontWeight: 'bold'
215
+ },
216
+ children: money(figure.cents, figure.currency)
217
+ }),
218
+ /*#__PURE__*/ _jsx(Typography, {
219
+ variant: "caption",
220
+ color: "text.secondary",
221
+ children: `${money(figure.numeratorCents, figure.currency)} over ${figure.denominator.toLocaleString()} ${figure.denominatorLabel}`
222
+ })
223
+ ]
224
+ }) : /*#__PURE__*/ _jsx(Typography, {
225
+ variant: "caption",
226
+ color: "text.secondary",
227
+ children: '— not enough recorded to compute'
228
+ })
229
+ ]
230
+ });
231
+ }
232
+ MoneyPerUnitRow.displayName = 'MoneyPerUnitRow';
233
+ /**
234
+ * One money total, with what it counts named underneath.
235
+ *
236
+ * {@link Figure}'s contract in currency: `null` draws a dash and never a
237
+ * zero, because "no attribution has ever been recorded" and "this earned
238
+ * nothing" lead a merchant to opposite conclusions about whether to send
239
+ * another one.
240
+ */ export function MoneyFigure(props) {
241
+ return /*#__PURE__*/ _jsxs(Stack, {
242
+ sx: {
243
+ minWidth: 140
244
+ },
245
+ children: [
246
+ /*#__PURE__*/ _jsx(Typography, {
247
+ variant: "h6",
248
+ children: props.cents === null ? '—' : money(props.cents, props.currency)
249
+ }),
250
+ /*#__PURE__*/ _jsx(Typography, {
251
+ variant: "body2",
252
+ children: props.label
253
+ }),
254
+ /*#__PURE__*/ _jsx(Typography, {
255
+ variant: "caption",
256
+ color: "text.secondary",
257
+ children: props.cents === null ? 'not recorded' : props.note
258
+ })
259
+ ]
260
+ });
261
+ }
262
+ MoneyFigure.displayName = 'MoneyFigure';
160
263
 
161
264
  //# sourceMappingURL=measured-figures.component.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/components/measured-figures.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\n/**\n * THE THREE WAYS A MEASURED NUMBER IS ALLOWED TO REACH A SCREEN.\n *\n * Every reporting surface in this product renders the same three things — a\n * titled block, a count, and a rate — and each carries a rule that is easy to\n * state and easy to lose:\n *\n * - **A count that is `null` draws a dash, never a zero.** \"We have no\n * record of this\" and \"this happened zero times\" lead a reader to opposite\n * conclusions, and a zero renders the first as the second.\n * - **A rate prints its denominator on the same line as its percentage.**\n * An open rate over `sent` and one over `delivered` are different numbers\n * sharing a label; a reader comparing two rates must be able to see they\n * are over different populations without hovering anything. The same\n * ambiguity exists wherever a rate is quoted — a form's lead rate over\n * submissions and over submissions that carried an address differ by\n * however many people declined to give one.\n * - **A rate that cannot be divided draws a dash and no number.** There is\n * no branch here that substitutes a denominator to get something\n * printable.\n *\n * They live in one module because a second copy is a second chance to render\n * a percentage without its denominator — and the copy would be written by\n * whoever adds the next card, in a file nobody tests for it. The email\n * plugin's `report-figures` re-exports these rather than keeping its own,\n * which is what stopped the console's forms surface from becoming that copy.\n */\n\nimport { Stack, Typography } from '@mui/material'\nimport type { ReactNode } from 'react'\n\n/**\n * A rate, together with everything needed to say what it is a rate OF.\n *\n * The producer decides whether a rate can honestly be taken; a `null` in\n * place of one of these is how it says no. Nothing here formats — `value` is\n * 0–1 and the display multiplies — so a model can be tested on the number\n * without a renderer, and this file cannot invent a denominator.\n */\nexport interface MeasuredRate {\n /** 0–1. Multiply for display; a producer never formats. */\n value: number\n numerator: number\n denominator: number\n /** Reader-facing name of the denominator, e.g. `'delivered'`. */\n denominatorLabel: string\n}\n\n/**\n * A rate, or `null` when one cannot honestly be taken.\n *\n * `null` on a zero or unrecorded denominator. Those are not the same as 0%:\n * a rate over nothing is undefined, and printing it as zero states a measured\n * failure where there was no measurement.\n */\nexport function measuredRate(\n numerator: number | null | undefined,\n denominator: number | null | undefined,\n denominatorLabel: string,\n): MeasuredRate | null {\n if (numerator == null || denominator == null) return null\n const top = Number(numerator)\n const bottom = Number(denominator)\n if (!Number.isFinite(top) || !Number.isFinite(bottom) || bottom <= 0) {\n return null\n }\n return { value: top / bottom, numerator: top, denominator: bottom, denominatorLabel }\n}\n\n/** A titled block. */\nexport function Section(props: { title: string; children: ReactNode }) {\n return (\n <Stack spacing={1}>\n <Typography variant=\"overline\" color=\"text.secondary\">\n {props.title}\n </Typography>\n {props.children}\n </Stack>\n )\n}\nSection.displayName = 'Section'\n\n/**\n * One count, with the population it describes named underneath.\n *\n * `null` renders as an em dash and NOT as zero, which is the whole reason\n * this takes `number | null` rather than defaulting.\n */\nexport function Figure(props: {\n label: string\n value: number | null\n note: string\n}) {\n return (\n <Stack sx={{ minWidth: 140 }}>\n <Typography variant=\"h6\">\n {props.value === null ? '—' : props.value.toLocaleString()}\n </Typography>\n <Typography variant=\"body2\">{props.label}</Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {props.value === null ? 'not recorded' : props.note}\n </Typography>\n </Stack>\n )\n}\nFigure.displayName = 'Figure'\n\n/** A percentage to one decimal place. */\nexport function percent(value: number): string {\n return `${(value * 100).toFixed(1)}%`\n}\n\n/**\n * One rate, with its denominator spelled out on the same line.\n *\n * The denominator is rendered as `12 of 480 delivered` rather than as a\n * tooltip or a footnote, because a reader comparing two rates has to be able\n * to see they are over different populations without hovering anything. A\n * `null` rate draws the dash and no number at all.\n */\nexport function RateRow(props: { label: string; rate: MeasuredRate | null }) {\n const { label, rate } = props\n return (\n <Stack\n direction=\"row\"\n spacing={2}\n sx={{ justifyContent: 'space-between', alignItems: 'baseline' }}\n >\n <Typography variant=\"body2\">{label}</Typography>\n {rate ? (\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'baseline' }}>\n <Typography variant=\"body2\" sx={{ fontWeight: 'bold' }}>\n {percent(rate.value)}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`${rate.numerator.toLocaleString()} of ${rate.denominator.toLocaleString()} ${rate.denominatorLabel}`}\n </Typography>\n </Stack>\n ) : (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'— not enough recorded to compute'}\n </Typography>\n )}\n </Stack>\n )\n}\nRateRow.displayName = 'RateRow'\n"],"names":["Stack","Typography","measuredRate","numerator","denominator","denominatorLabel","top","Number","bottom","isFinite","value","Section","props","spacing","variant","color","title","children","displayName","Figure","sx","minWidth","toLocaleString","label","note","percent","toFixed","RateRow","rate","direction","justifyContent","alignItems","fontWeight"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;CA0BC,GAED,SAASA,KAAK,EAAEC,UAAU,QAAQ,gBAAe;AAoBjD;;;;;;CAMC,GACD,OAAO,SAASC,aACdC,SAAoC,EACpCC,WAAsC,EACtCC,gBAAwB;IAExB,IAAIF,aAAa,QAAQC,eAAe,MAAM,OAAO;IACrD,MAAME,MAAMC,OAAOJ;IACnB,MAAMK,SAASD,OAAOH;IACtB,IAAI,CAACG,OAAOE,QAAQ,CAACH,QAAQ,CAACC,OAAOE,QAAQ,CAACD,WAAWA,UAAU,GAAG;QACpE,OAAO;IACT;IACA,OAAO;QAAEE,OAAOJ,MAAME;QAAQL,WAAWG;QAAKF,aAAaI;QAAQH;IAAiB;AACtF;AAEA,oBAAoB,GACpB,OAAO,SAASM,QAAQC,KAA6C;IACnE,qBACE,MAACZ;QAAMa,SAAS;;0BACd,KAACZ;gBAAWa,SAAQ;gBAAWC,OAAM;0BAClCH,MAAMI,KAAK;;YAEbJ,MAAMK,QAAQ;;;AAGrB;AACAN,QAAQO,WAAW,GAAG;AAEtB;;;;;CAKC,GACD,OAAO,SAASC,OAAOP,KAItB;IACC,qBACE,MAACZ;QAAMoB,IAAI;YAAEC,UAAU;QAAI;;0BACzB,KAACpB;gBAAWa,SAAQ;0BACjBF,MAAMF,KAAK,KAAK,OAAO,MAAME,MAAMF,KAAK,CAACY,cAAc;;0BAE1D,KAACrB;gBAAWa,SAAQ;0BAASF,MAAMW,KAAK;;0BACxC,KAACtB;gBAAWa,SAAQ;gBAAUC,OAAM;0BACjCH,MAAMF,KAAK,KAAK,OAAO,iBAAiBE,MAAMY,IAAI;;;;AAI3D;AACAL,OAAOD,WAAW,GAAG;AAErB,uCAAuC,GACvC,OAAO,SAASO,QAAQf,KAAa;IACnC,OAAO,GAAG,AAACA,CAAAA,QAAQ,GAAE,EAAGgB,OAAO,CAAC,GAAG,CAAC,CAAC;AACvC;AAEA;;;;;;;CAOC,GACD,OAAO,SAASC,QAAQf,KAAmD;IACzE,MAAM,EAAEW,KAAK,EAAEK,IAAI,EAAE,GAAGhB;IACxB,qBACE,MAACZ;QACC6B,WAAU;QACVhB,SAAS;QACTO,IAAI;YAAEU,gBAAgB;YAAiBC,YAAY;QAAW;;0BAE9D,KAAC9B;gBAAWa,SAAQ;0BAASS;;YAC5BK,qBACC,MAAC5B;gBAAM6B,WAAU;gBAAMhB,SAAS;gBAAGO,IAAI;oBAAEW,YAAY;gBAAW;;kCAC9D,KAAC9B;wBAAWa,SAAQ;wBAAQM,IAAI;4BAAEY,YAAY;wBAAO;kCAClDP,QAAQG,KAAKlB,KAAK;;kCAErB,KAACT;wBAAWa,SAAQ;wBAAUC,OAAM;kCACjC,GAAGa,KAAKzB,SAAS,CAACmB,cAAc,GAAG,IAAI,EAAEM,KAAKxB,WAAW,CAACkB,cAAc,GAAG,CAAC,EAAEM,KAAKvB,gBAAgB,EAAE;;;+BAI1G,KAACJ;gBAAWa,SAAQ;gBAAUC,OAAM;0BACjC;;;;AAKX;AACAY,QAAQT,WAAW,GAAG"}
1
+ {"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/components/measured-figures.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\n/**\n * THE THREE WAYS A MEASURED NUMBER IS ALLOWED TO REACH A SCREEN.\n *\n * Every reporting surface in this product renders the same three things — a\n * titled block, a count, and a rate — and each carries a rule that is easy to\n * state and easy to lose:\n *\n * - **A count that is `null` draws a dash, never a zero.** \"We have no\n * record of this\" and \"this happened zero times\" lead a reader to opposite\n * conclusions, and a zero renders the first as the second.\n * - **A rate prints its denominator on the same line as its percentage.**\n * An open rate over `sent` and one over `delivered` are different numbers\n * sharing a label; a reader comparing two rates must be able to see they\n * are over different populations without hovering anything. The same\n * ambiguity exists wherever a rate is quoted — a form's lead rate over\n * submissions and over submissions that carried an address differ by\n * however many people declined to give one.\n * - **A rate that cannot be divided draws a dash and no number.** There is\n * no branch here that substitutes a denominator to get something\n * printable.\n *\n * They live in one module because a second copy is a second chance to render\n * a percentage without its denominator — and the copy would be written by\n * whoever adds the next card, in a file nobody tests for it.\n *\n * MONEY joined them (AGL-3080), from the campaign lib that had kept its own.\n * It is not a fourth way of measuring — it is the same two, in currency: a\n * total that draws a dash rather than a zero, and an average that names the\n * population it is over. The rule they enforce does not become someone\n * else's the moment the number has a currency on it, and the copy that was\n * left behind sat in a library only the email surfaces could reach, so the\n * next surface to show money would have written a third.\n */\n\nimport { Stack, Typography } from '@mui/material'\nimport type { ReactNode } from 'react'\n\n/**\n * An average in currency, together with everything needed to say what it is\n * an average OF (AGL-3080).\n *\n * {@link MeasuredRate}'s contract in money, and it goes wrong the same way:\n * \"$0.42 per recipient\" is meaningless without the recipient count, and worse\n * than meaningless when the reader assumes a different one from the writer.\n * Minor units throughout, because the per-message figure is fractional by\n * nature and rounding it at the model would lose the thing being measured.\n */\nexport interface MeasuredMoney {\n /** Minor units per unit of the denominator. Fractional by nature. */\n cents: number\n numeratorCents: number\n denominator: number\n /** Reader-facing name of the denominator, e.g. `'delivered'`. */\n denominatorLabel: string\n /** Lowercase ISO code as it was recorded, e.g. `'usd'`. */\n currency: string\n}\n\n/**\n * A rate, together with everything needed to say what it is a rate OF.\n *\n * The producer decides whether a rate can honestly be taken; a `null` in\n * place of one of these is how it says no. Nothing here formats — `value` is\n * 0–1 and the display multiplies — so a model can be tested on the number\n * without a renderer, and this file cannot invent a denominator.\n */\nexport interface MeasuredRate {\n /** 0–1. Multiply for display; a producer never formats. */\n value: number\n numerator: number\n denominator: number\n /** Reader-facing name of the denominator, e.g. `'delivered'`. */\n denominatorLabel: string\n}\n\n/**\n * A rate, or `null` when one cannot honestly be taken.\n *\n * `null` on a zero or unrecorded denominator. Those are not the same as 0%:\n * a rate over nothing is undefined, and printing it as zero states a measured\n * failure where there was no measurement.\n */\nexport function measuredRate(\n numerator: number | null | undefined,\n denominator: number | null | undefined,\n denominatorLabel: string,\n): MeasuredRate | null {\n if (numerator == null || denominator == null) return null\n const top = Number(numerator)\n const bottom = Number(denominator)\n if (!Number.isFinite(top) || !Number.isFinite(bottom) || bottom <= 0) {\n return null\n }\n return { value: top / bottom, numerator: top, denominator: bottom, denominatorLabel }\n}\n\n/** A titled block. */\nexport function Section(props: { title: string; children: ReactNode }) {\n return (\n <Stack spacing={1}>\n <Typography variant=\"overline\" color=\"text.secondary\">\n {props.title}\n </Typography>\n {props.children}\n </Stack>\n )\n}\nSection.displayName = 'Section'\n\n/**\n * One count, with the population it describes named underneath.\n *\n * `null` renders as an em dash and NOT as zero, which is the whole reason\n * this takes `number | null` rather than defaulting.\n */\nexport function Figure(props: {\n label: string\n value: number | null\n note: string\n}) {\n return (\n <Stack sx={{ minWidth: 140 }}>\n <Typography variant=\"h6\">\n {props.value === null ? '—' : props.value.toLocaleString()}\n </Typography>\n <Typography variant=\"body2\">{props.label}</Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {props.value === null ? 'not recorded' : props.note}\n </Typography>\n </Stack>\n )\n}\nFigure.displayName = 'Figure'\n\n/** A percentage to one decimal place. */\nexport function percent(value: number): string {\n return `${(value * 100).toFixed(1)}%`\n}\n\n/**\n * One rate, with its denominator spelled out on the same line.\n *\n * The denominator is rendered as `12 of 480 delivered` rather than as a\n * tooltip or a footnote, because a reader comparing two rates has to be able\n * to see they are over different populations without hovering anything. A\n * `null` rate draws the dash and no number at all.\n */\nexport function RateRow(props: { label: string; rate: MeasuredRate | null }) {\n const { label, rate } = props\n return (\n <Stack\n direction=\"row\"\n spacing={2}\n sx={{ justifyContent: 'space-between', alignItems: 'baseline' }}\n >\n <Typography variant=\"body2\">{label}</Typography>\n {rate ? (\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'baseline' }}>\n <Typography variant=\"body2\" sx={{ fontWeight: 'bold' }}>\n {percent(rate.value)}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`${rate.numerator.toLocaleString()} of ${rate.denominator.toLocaleString()} ${rate.denominatorLabel}`}\n </Typography>\n </Stack>\n ) : (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'— not enough recorded to compute'}\n </Typography>\n )}\n </Stack>\n )\n}\nRateRow.displayName = 'RateRow'\n\n/**\n * One amount of money, in the currency it was recorded in.\n *\n * NOT a figure primitive, and deliberately separate from {@link Figure}: it\n * carries a currency, and every surface that shows these refuses to sum two\n * of them into one number. A currency code the platform cannot format falls\n * back to the amount beside the code, which is still true, rather than\n * throwing inside a render.\n */\nexport function money(cents: number, currency: string): string {\n const amount = cents / 100\n try {\n return new Intl.NumberFormat(undefined, {\n style: 'currency',\n currency: currency.toUpperCase(),\n }).format(amount)\n } catch {\n return `${amount.toFixed(2)} ${currency.toUpperCase()}`\n }\n}\n\n/**\n * One money figure, with the population it is averaged over named.\n *\n * The rule {@link RateRow} enforces for a percentage, applied to an average.\n * `null` draws the reason rather than a zero, for {@link Figure}'s reason.\n */\nexport function MoneyPerUnitRow(props: {\n label: string\n figure: MeasuredMoney | null\n}) {\n const { label, figure } = props\n return (\n <Stack\n direction=\"row\"\n spacing={2}\n sx={{ justifyContent: 'space-between', alignItems: 'baseline' }}\n >\n <Typography variant=\"body2\">{label}</Typography>\n {figure ? (\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'baseline' }}>\n <Typography variant=\"body2\" sx={{ fontWeight: 'bold' }}>\n {money(figure.cents, figure.currency)}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`${money(figure.numeratorCents, figure.currency)} over ${figure.denominator.toLocaleString()} ${figure.denominatorLabel}`}\n </Typography>\n </Stack>\n ) : (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'— not enough recorded to compute'}\n </Typography>\n )}\n </Stack>\n )\n}\nMoneyPerUnitRow.displayName = 'MoneyPerUnitRow'\n\n/**\n * One money total, with what it counts named underneath.\n *\n * {@link Figure}'s contract in currency: `null` draws a dash and never a\n * zero, because \"no attribution has ever been recorded\" and \"this earned\n * nothing\" lead a merchant to opposite conclusions about whether to send\n * another one.\n */\nexport function MoneyFigure(props: {\n label: string\n cents: number | null\n currency: string\n note: string\n}) {\n return (\n <Stack sx={{ minWidth: 140 }}>\n <Typography variant=\"h6\">\n {props.cents === null ? '—' : money(props.cents, props.currency)}\n </Typography>\n <Typography variant=\"body2\">{props.label}</Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {props.cents === null ? 'not recorded' : props.note}\n </Typography>\n </Stack>\n )\n}\nMoneyFigure.displayName = 'MoneyFigure'\n"],"names":["Stack","Typography","measuredRate","numerator","denominator","denominatorLabel","top","Number","bottom","isFinite","value","Section","props","spacing","variant","color","title","children","displayName","Figure","sx","minWidth","toLocaleString","label","note","percent","toFixed","RateRow","rate","direction","justifyContent","alignItems","fontWeight","money","cents","currency","amount","Intl","NumberFormat","undefined","style","toUpperCase","format","MoneyPerUnitRow","figure","numeratorCents","MoneyFigure"],"mappings":"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"}