@kanso-labs/kanso-ui 0.40.5 → 0.40.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/dist/button/hooks.js +37 -1
  2. package/dist/button/hooks.js.map +1 -1
  3. package/dist/button/index.d.ts +57 -1
  4. package/dist/button/index.js +153 -96
  5. package/dist/button/index.js.map +1 -1
  6. package/dist/button/toggle.js +17 -0
  7. package/dist/button/toggle.js.map +1 -0
  8. package/dist/calendar/index.js +69 -58
  9. package/dist/calendar/index.js.map +1 -1
  10. package/dist/calendar/styles.js +29 -6
  11. package/dist/calendar/styles.js.map +1 -1
  12. package/dist/chip/index.js +3 -4
  13. package/dist/chip/index.js.map +1 -1
  14. package/dist/chip/styles.js +2 -6
  15. package/dist/chip/styles.js.map +1 -1
  16. package/dist/collection/empty.js +7 -0
  17. package/dist/collection/empty.js.map +1 -0
  18. package/dist/collection/index.js +125 -2
  19. package/dist/collection/index.js.map +1 -1
  20. package/dist/collection/styles.js +14 -0
  21. package/dist/collection/styles.js.map +1 -1
  22. package/dist/components/app-bar/index.d.ts +26 -23
  23. package/dist/components/app-bar/index.js +89 -72
  24. package/dist/components/app-bar/index.js.map +1 -1
  25. package/dist/components/autocomplete/index.d.ts +6 -4
  26. package/dist/components/autocomplete/index.js +23 -14
  27. package/dist/components/autocomplete/index.js.map +1 -1
  28. package/dist/components/button/index.d.ts +15 -35
  29. package/dist/components/button/index.js +102 -74
  30. package/dist/components/button/index.js.map +1 -1
  31. package/dist/components/calendar/index.d.ts +9 -3
  32. package/dist/components/calendar/index.js +51 -42
  33. package/dist/components/calendar/index.js.map +1 -1
  34. package/dist/components/card/index.js +43 -27
  35. package/dist/components/card/index.js.map +1 -1
  36. package/dist/components/carousel/index.js +1 -1
  37. package/dist/components/checkbox/index.d.ts +9 -0
  38. package/dist/components/checkbox/index.js +47 -35
  39. package/dist/components/checkbox/index.js.map +1 -1
  40. package/dist/components/checkbox-group/index.js +1 -1
  41. package/dist/components/chip/index.d.ts +9 -4
  42. package/dist/components/chip/index.js.map +1 -1
  43. package/dist/components/chip-group/index.js +51 -37
  44. package/dist/components/chip-group/index.js.map +1 -1
  45. package/dist/components/copy-field/index.d.ts +28 -6
  46. package/dist/components/copy-field/index.js +140 -54
  47. package/dist/components/copy-field/index.js.map +1 -1
  48. package/dist/components/currency/index.d.ts +6 -2
  49. package/dist/components/currency/index.js +74 -46
  50. package/dist/components/currency/index.js.map +1 -1
  51. package/dist/components/dialog/index.js +13 -13
  52. package/dist/components/disclosure/index.js +1 -1
  53. package/dist/components/icon-button/index.d.ts +6 -31
  54. package/dist/components/icon-button/index.js +64 -52
  55. package/dist/components/icon-button/index.js.map +1 -1
  56. package/dist/components/index.d.ts +2 -1
  57. package/dist/components/list/index.js +2 -2
  58. package/dist/components/list-box/index.d.ts +14 -2
  59. package/dist/components/list-box/index.js +16 -8
  60. package/dist/components/list-box/index.js.map +1 -1
  61. package/dist/components/list-item/index.js +1 -1
  62. package/dist/components/menu/index.d.ts +14 -1
  63. package/dist/components/menu/index.js +32 -17
  64. package/dist/components/menu/index.js.map +1 -1
  65. package/dist/components/navigation-tree/index.js +12 -12
  66. package/dist/components/radio-group/index.d.ts +4 -0
  67. package/dist/components/radio-group/index.js +12 -12
  68. package/dist/components/radio-group/index.js.map +1 -1
  69. package/dist/components/range-calendar/index.d.ts +9 -3
  70. package/dist/components/range-calendar/index.js +51 -42
  71. package/dist/components/range-calendar/index.js.map +1 -1
  72. package/dist/components/search-view/index.js +1 -1
  73. package/dist/components/segmented-button/index.d.ts +5 -4
  74. package/dist/components/segmented-button/index.js +3 -3
  75. package/dist/components/segmented-button/index.js.map +1 -1
  76. package/dist/components/sheet/index.js +12 -12
  77. package/dist/components/split-button/index.d.ts +1 -1
  78. package/dist/components/split-button/index.js.map +1 -1
  79. package/dist/components/switch/index.d.ts +9 -2
  80. package/dist/components/switch/index.js +2 -2
  81. package/dist/components/switch/index.js.map +1 -1
  82. package/dist/components/table/index.js +3 -3
  83. package/dist/components/time-picker/index.js +1 -1
  84. package/dist/components/tree/index.js +8 -8
  85. package/dist/control/index.js +55 -17
  86. package/dist/control/index.js.map +1 -1
  87. package/dist/field/index.js +59 -51
  88. package/dist/field/index.js.map +1 -1
  89. package/dist/hooks/useAppBarScroll.d.ts +52 -0
  90. package/dist/hooks/useAppBarScroll.js +75 -0
  91. package/dist/hooks/useAppBarScroll.js.map +1 -0
  92. package/dist/i18n/index.js +2 -0
  93. package/dist/i18n/index.js.map +1 -1
  94. package/dist/i18n/messages.js +68 -0
  95. package/dist/i18n/messages.js.map +1 -1
  96. package/dist/index.d.ts +2 -1
  97. package/dist/index.js +3 -2
  98. package/dist/render/name.d.ts +22 -0
  99. package/dist/styles.css +83 -24
  100. package/package.json +5 -5
@@ -2,9 +2,8 @@
2
2
  import { mergeStyles } from "../../styles/merge.js";
3
3
  import { useMessages } from "../../i18n/index.js";
4
4
  import Button from "../button/index.js";
5
- import Code from "../code/index.js";
6
5
  import { c } from "react/compiler-runtime";
7
- import { useEffect, useRef, useState } from "react";
6
+ import { useEffect, useId, useRef, useState } from "react";
8
7
  import { jsx, jsxs } from "react/jsx-runtime";
9
8
  //#region src/components/copy-field/index.tsx
10
9
  const COPIED_RESET_MS = 2e3;
@@ -14,17 +13,34 @@ const COPIED_RESET_MS = 2e3;
14
13
  * on the button itself.
15
14
  *
16
15
  * A write refused — outside a secure context, or where the permission is
17
- * denied — leaves the button at rest rather than confirming, and calls
18
- * `onCopyFailed`.
16
+ * denied — does not leave the reader with nothing. The field selects the
17
+ * value and tries the older copy command, which some of those places still
18
+ * allow; failing that, the selection is left ready for the keyboard's own
19
+ * copy, the field announces so, and `onCopyFailed` is called. One click on
20
+ * the value selects exactly the value too.
19
21
  */
20
22
  function CopyField(t0) {
21
- const $ = c(32);
22
- const { copiedLabel: copiedLabelProp, copyLabel: copyLabelProp, onCopied, onCopyFailed, value, ...props } = t0;
23
+ const $ = c(41);
24
+ const { copiedLabel: copiedLabelProp, copyLabel: copyLabelProp, label, onCopied, onCopyFailed, selectedLabel: selectedLabelProp, value, ...props } = t0;
23
25
  const messages = useMessages();
24
26
  const copiedLabel = copiedLabelProp ?? messages.copied;
25
27
  const copyLabel = copyLabelProp ?? messages.copy;
26
- const [copied, setCopied] = useState(false);
28
+ const selectedLabel = selectedLabelProp ?? messages.copySelected;
29
+ const [copy, setCopy] = useState(null);
30
+ const shown = copy?.text === value ? copy.kind : "copy";
31
+ const copied = shown === "copied";
27
32
  const timer = useRef(void 0);
33
+ const code = useRef(null);
34
+ const generatedId = useId();
35
+ const rootId = props.id ?? generatedId;
36
+ const copyId = useId();
37
+ const copiedId = useId();
38
+ const named = label === void 0 ? void 0 : {
39
+ "aria-label": label,
40
+ id: rootId,
41
+ role: "group"
42
+ };
43
+ const buttonName = label === void 0 ? void 0 : `${copied ? copiedId : copyId} ${rootId}`;
28
44
  let t1;
29
45
  let t2;
30
46
  if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
@@ -42,21 +58,33 @@ function CopyField(t0) {
42
58
  let t3;
43
59
  if ($[2] !== onCopied || $[3] !== onCopyFailed || $[4] !== value) {
44
60
  t3 = () => {
61
+ const settle = (kind) => {
62
+ clearTimeout(timer.current);
63
+ timer.current = setTimeout(() => {
64
+ setCopy(null);
65
+ }, COPIED_RESET_MS);
66
+ setCopy((current) => ({
67
+ count: (current?.count ?? 0) + 1,
68
+ kind,
69
+ text: value
70
+ }));
71
+ };
45
72
  const write = async () => {
46
73
  try {
47
74
  await navigator.clipboard.writeText(value);
48
75
  } catch (t4) {
49
76
  const error = t4;
50
- setCopied(false);
77
+ if (selectAndCopy(code.current)) {
78
+ settle("copied");
79
+ reportCopied(onCopied, value);
80
+ return;
81
+ }
82
+ settle("selected");
51
83
  onCopyFailed?.(error);
52
84
  return;
53
85
  }
54
- setCopied(true);
55
- onCopied?.(value);
56
- clearTimeout(timer.current);
57
- timer.current = setTimeout(() => {
58
- setCopied(false);
59
- }, COPIED_RESET_MS);
86
+ settle("copied");
87
+ reportCopied(onCopied, value);
60
88
  };
61
89
  write();
62
90
  };
@@ -70,8 +98,13 @@ function CopyField(t0) {
70
98
  let t5;
71
99
  if ($[6] !== value) {
72
100
  t5 = /* @__PURE__ */ jsx("span", {
73
- className: "x139gbkf x1iyjqo2 x1b35g6e xeuugli",
74
- children: /* @__PURE__ */ jsx(Code, { children: value })
101
+ className: "x139gbkf x1iyjqo2 x1b35g6e xdb2kus x9d6959 xwth0xh xybisxh xeuugli x6mezaz xvrbanz x126k92a",
102
+ children: /* @__PURE__ */ jsx("code", {
103
+ dir: "ltr",
104
+ ref: code,
105
+ className: "x9f619 xbppi91 x1qlqyl8 xj0a0fe x4ej8vm",
106
+ children: value
107
+ })
75
108
  });
76
109
  $[6] = value;
77
110
  $[7] = t5;
@@ -87,75 +120,90 @@ function CopyField(t0) {
87
120
  } else t7 = $[8];
88
121
  const t8 = t7[!!copied << 0];
89
122
  let t9;
90
- if ($[9] !== copyLabel || $[10] !== t6 || $[11] !== t8) {
123
+ if ($[9] !== copyId || $[10] !== copyLabel || $[11] !== t6 || $[12] !== t8) {
91
124
  t9 = /* @__PURE__ */ jsx("span", {
92
125
  "aria-hidden": t6,
126
+ id: copyId,
93
127
  ...t8,
94
128
  children: copyLabel
95
129
  });
96
- $[9] = copyLabel;
97
- $[10] = t6;
98
- $[11] = t8;
99
- $[12] = t9;
100
- } else t9 = $[12];
130
+ $[9] = copyId;
131
+ $[10] = copyLabel;
132
+ $[11] = t6;
133
+ $[12] = t8;
134
+ $[13] = t9;
135
+ } else t9 = $[13];
101
136
  const t10 = !copied || void 0;
102
137
  let t11;
103
- if ($[13] === Symbol.for("react.memo_cache_sentinel")) {
138
+ if ($[14] === Symbol.for("react.memo_cache_sentinel")) {
104
139
  t11 = {
105
140
  0: { className: "x15r89dc" },
106
141
  1: { className: "x15r89dc xlshs6z" }
107
142
  };
108
- $[13] = t11;
109
- } else t11 = $[13];
143
+ $[14] = t11;
144
+ } else t11 = $[14];
110
145
  const t12 = t11[!!!copied << 0];
111
146
  let t13;
112
- if ($[14] !== copiedLabel || $[15] !== t10 || $[16] !== t12) {
147
+ if ($[15] !== copiedId || $[16] !== copiedLabel || $[17] !== t10 || $[18] !== t12) {
113
148
  t13 = /* @__PURE__ */ jsx("span", {
114
149
  "aria-hidden": t10,
150
+ id: copiedId,
115
151
  ...t12,
116
152
  children: copiedLabel
117
153
  });
118
- $[14] = copiedLabel;
119
- $[15] = t10;
120
- $[16] = t12;
121
- $[17] = t13;
122
- } else t13 = $[17];
154
+ $[15] = copiedId;
155
+ $[16] = copiedLabel;
156
+ $[17] = t10;
157
+ $[18] = t12;
158
+ $[19] = t13;
159
+ } else t13 = $[19];
123
160
  let t14;
124
- if ($[18] !== t13 || $[19] !== t9) {
161
+ if ($[20] !== t13 || $[21] !== t9) {
125
162
  t14 = /* @__PURE__ */ jsxs("span", {
126
163
  className: "xrvj5dj x1o2pa38",
127
164
  children: [t9, t13]
128
165
  });
129
- $[18] = t13;
130
- $[19] = t9;
131
- $[20] = t14;
132
- } else t14 = $[20];
166
+ $[20] = t13;
167
+ $[21] = t9;
168
+ $[22] = t14;
169
+ } else t14 = $[22];
133
170
  let t15;
134
- if ($[21] !== handleCopy || $[22] !== t14) {
171
+ if ($[23] !== buttonName || $[24] !== handleCopy || $[25] !== t14) {
135
172
  t15 = /* @__PURE__ */ jsx(Button, {
173
+ "aria-labelledby": buttonName,
136
174
  onClick: handleCopy,
137
175
  size: "xs",
138
176
  variant: "text",
139
177
  children: t14
140
178
  });
141
- $[21] = handleCopy;
142
- $[22] = t14;
143
- $[23] = t15;
144
- } else t15 = $[23];
145
- const t16 = copied ? copiedLabel : "";
179
+ $[23] = buttonName;
180
+ $[24] = handleCopy;
181
+ $[25] = t14;
182
+ $[26] = t15;
183
+ } else t15 = $[26];
184
+ let t16;
185
+ if ($[27] !== copiedLabel || $[28] !== copy || $[29] !== selectedLabel || $[30] !== shown) {
186
+ t16 = copy !== null && shown !== "copy" ? /* @__PURE__ */ jsx("span", { children: shown === "copied" ? copiedLabel : selectedLabel }, copy.count) : null;
187
+ $[27] = copiedLabel;
188
+ $[28] = copy;
189
+ $[29] = selectedLabel;
190
+ $[30] = shown;
191
+ $[31] = t16;
192
+ } else t16 = $[31];
146
193
  let t17;
147
- if ($[24] !== t16) {
194
+ if ($[32] !== t16) {
148
195
  t17 = /* @__PURE__ */ jsx("output", {
149
196
  className: "xjm9jq1 x1hyvwdk x1i1rx1s xb3r6kr x10l6tqk xuxw1ft",
150
197
  children: t16
151
198
  });
152
- $[24] = t16;
153
- $[25] = t17;
154
- } else t17 = $[25];
199
+ $[32] = t16;
200
+ $[33] = t17;
201
+ } else t17 = $[33];
155
202
  let t18;
156
- if ($[26] !== props || $[27] !== t15 || $[28] !== t17 || $[29] !== t4 || $[30] !== t5) {
203
+ if ($[34] !== named || $[35] !== props || $[36] !== t15 || $[37] !== t17 || $[38] !== t4 || $[39] !== t5) {
157
204
  t18 = /* @__PURE__ */ jsxs("div", {
158
205
  ...props,
206
+ ...named,
159
207
  ...t4,
160
208
  children: [
161
209
  t5,
@@ -163,15 +211,53 @@ function CopyField(t0) {
163
211
  t17
164
212
  ]
165
213
  });
166
- $[26] = props;
167
- $[27] = t15;
168
- $[28] = t17;
169
- $[29] = t4;
170
- $[30] = t5;
171
- $[31] = t18;
172
- } else t18 = $[31];
214
+ $[34] = named;
215
+ $[35] = props;
216
+ $[36] = t15;
217
+ $[37] = t17;
218
+ $[38] = t4;
219
+ $[39] = t5;
220
+ $[40] = t18;
221
+ } else t18 = $[40];
173
222
  return t18;
174
223
  }
224
+ /**
225
+ * Hands the call site the value it copied. What its handler throws is raised
226
+ * on its own, outside the copy: it still reaches the page's error reporting,
227
+ * where a rejection of the copy's own promise reached nothing but an
228
+ * unhandled-rejection warning. Outside the component, since the React
229
+ * Compiler compiles neither optional chaining nor a captured catch binding
230
+ * inside a `try`.
231
+ */
232
+ function reportCopied(onCopied, value) {
233
+ if (onCopied === void 0) return;
234
+ try {
235
+ onCopied(value);
236
+ } catch (thrown) {
237
+ queueMicrotask(() => {
238
+ throw thrown;
239
+ });
240
+ }
241
+ }
242
+ /**
243
+ * Selects the value's text, which is what the keyboard's own copy acts on,
244
+ * and tries the older copy command on that selection, saying whether it
245
+ * copied. The command is deprecated, and still the one way to the clipboard
246
+ * on an insecure origin, which is where the newer one is refused.
247
+ */
248
+ function selectAndCopy(element) {
249
+ const selection = window.getSelection();
250
+ if (element === null || selection === null) return false;
251
+ const range = document.createRange();
252
+ range.selectNodeContents(element);
253
+ selection.removeAllRanges();
254
+ selection.addRange(range);
255
+ try {
256
+ return document.execCommand("copy");
257
+ } catch {
258
+ return false;
259
+ }
260
+ }
175
261
  //#endregion
176
262
  export { CopyField as default };
177
263
 
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["c","_c","HTMLAttributes","RefAttributes","stylex","useCallback","useEffect","useRef","useState","useMessages","mergeStyles","colors","radii","spacing","typography","Button","Code","COPIED_RESET_MS","CopyFieldProps","copiedLabel","copyLabel","onCopied","value","onCopyFailed","error","Omit","HTMLDivElement","CopyField","t0","$","copiedLabelProp","copyLabelProp","props","messages","copied","copy","setCopied","timer","undefined","t1","t2","Symbol","for","clearTimeout","current","t3","write","navigator","clipboard","writeText","t4","setTimeout","handleCopy","className","t5","t6","t7","0","1","t8","t9","t10","t11","t12","t13","t14","t15","t16","t17","t18","_c"],"sources":["../../../src/components/copy-field/index.tsx"],"sourcesContent":["'use client';\n\nimport type { HTMLAttributes, RefAttributes } from 'react';\nimport * as stylex from '@stylexjs/stylex';\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { useMessages } from '../../i18n';\nimport { mergeStyles } from '../../styles/merge';\nimport '../../tokens/design.tokens.stylex';\nimport { colors, radii, spacing, typography } from '../../tokens/design.tokens.stylex';\nimport Button from '../button';\nimport Code from '../code';\n\n// How long the confirmation stands before the control returns to offering the\n// copy again. Not a motion token: those are transition durations, measured in\n// hundreds of milliseconds, and this is a dwell — long enough to be read\n// without being long enough to leave the button lying about what it will do.\nconst COPIED_RESET_MS = 2000;\n\n// No Material Design page draws a read-only value with a copy control. It\n// follows the outlined text field: the 1dp rule in the outline role, the\n// surface behind it, and the control at its trailing end. The value is\n// body-small beside a 32dp text button, so the box is shorter than the\n// field's 56, which suits a value that is read rather than edited, and its\n// corner is the shape scale's small step for the same reason.\n\ntype CopyFieldProps = {\n /**\n * What the button says once the value has been copied, and what is\n * announced. It reverts to `copyLabel` on its own after a couple of\n * seconds. Left out, it is the word for it in the I18nProvider's locale —\n * \"Copied\" in English.\n */\n copiedLabel?: string;\n /**\n * What the button says at rest. Left out, it is the word for it in the\n * I18nProvider's locale — \"Copy\" in English.\n */\n copyLabel?: string;\n /**\n * Called with the value after it reaches the clipboard, never before.\n *\n * Named `onCopied` rather than `onCopy` because the root is a `<div>`, which\n * already has an `onCopy` of its own — React's handler for the native copy\n * event, fired when the reader presses the copy shortcut inside the element.\n * The two are different moments, and a prop that quietly shadowed the\n * built-in would be a trap rather than a convenience.\n */\n onCopied?: (value: string) => void;\n /**\n * Called when the write is refused, with whatever was thrown. The button\n * stays at rest, so this is the only signal an app gets — reach for it to\n * fall back, by selecting the value or saying so in a snackbar.\n *\n * Takes the error where `onCopied` takes the value, because the value is\n * the one thing the call site already has and the reason is the one thing\n * it does not. The two refusals throw differently: outside a secure\n * context `navigator.clipboard` is not there at all, and where the\n * permission is denied it is a `DOMException`.\n */\n onCopyFailed?: (error: unknown) => void;\n /** The text shown, and the text copied. */\n value: string;\n} & Omit<HTMLAttributes<HTMLDivElement>, 'children'>;\n\n/**\n * A value to be read and taken away — a repository URL, a token, a command.\n * It shows the value in the mono face and copies it on request, confirming\n * on the button itself.\n *\n * A write refused — outside a secure context, or where the permission is\n * denied — leaves the button at rest rather than confirming, and calls\n * `onCopyFailed`.\n */\nfunction CopyField({\n copiedLabel: copiedLabelProp,\n copyLabel: copyLabelProp,\n onCopied,\n onCopyFailed,\n value,\n ...props\n}: CopyFieldProps & RefAttributes<HTMLDivElement>) {\n const messages = useMessages();\n const copiedLabel = copiedLabelProp ?? messages.copied;\n const copyLabel = copyLabelProp ?? messages.copy;\n const [copied, setCopied] = useState(false);\n const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n\n // Without this, a field unmounted inside the dwell leaves a timer holding a\n // setState for a tree that is gone.\n useEffect(() => () => {\n clearTimeout(timer.current);\n }, []);\n const handleCopy = useCallback(() => {\n const write = async () => {\n try {\n await navigator.clipboard.writeText(value);\n } catch (error) {\n // A clipboard write is refused outside a secure context and wherever\n // the permission is denied, and neither is something the call site can\n // fix. Staying at rest is the honest report: the value is not on the\n // clipboard, so the control must not claim it is.\n //\n // It is reported rather than swallowed, since a control that does\n // nothing is indistinguishable from one that is broken. Nothing is\n // logged — the library writes to the console nowhere, and a warning\n // an app cannot turn off is not its to emit.\n setCopied(false);\n onCopyFailed?.(error);\n return;\n }\n setCopied(true);\n onCopied?.(value);\n clearTimeout(timer.current);\n timer.current = setTimeout(() => {\n setCopied(false);\n }, COPIED_RESET_MS);\n };\n void write();\n }, [onCopied, onCopyFailed, value]);\n return <div {...props} {...mergeStyles({\n className: \"x6s0dn4 x14s9m32 x1fwvt7v x1xdvaa9 x1y0btm7 xmkeg23 x9f619 x78zum5 x126z3so xmqoko8 x18fpw6u\"\n }, props)}>\n <span className=\"x139gbkf x1iyjqo2 x1b35g6e xeuugli\">\n <Code>{value}</Code>\n </span>\n <Button onClick={handleCopy} size=\"xs\" variant=\"text\">\n <span className=\"xrvj5dj x1o2pa38\">\n <span aria-hidden={copied || undefined} {...{\n 0: {\n className: \"x15r89dc\"\n },\n 1: {\n className: \"x15r89dc xlshs6z\"\n }\n }[!!copied << 0]}>\n {copyLabel}\n </span>\n <span aria-hidden={!copied || undefined} {...{\n 0: {\n className: \"x15r89dc\"\n },\n 1: {\n className: \"x15r89dc xlshs6z\"\n }\n }[!!!copied << 0]}>\n {copiedLabel}\n </span>\n </span>\n </Button>\n {/* The button's own label changes, but a label changing under a screen\n reader is not reliably announced. This is. `<output>` carries an\n implicit role of status, so it needs no role attribute of its own. */}\n <output className=\"xjm9jq1 x1hyvwdk x1i1rx1s xb3r6kr x10l6tqk xuxw1ft\">\n {copied ? copiedLabel : ''}\n </output>\n </div>;\n}\nexport type { CopyFieldProps };\nexport default CopyField;"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["c","_c","HTMLAttributes","RefAttributes","stylex","useCallback","useEffect","useId","useRef","useState","useMessages","mergeStyles","colors","radii","spacing","typography","Button","COPIED_RESET_MS","CopyFieldProps","copiedLabel","copyLabel","label","onCopied","value","onCopyFailed","error","selectedLabel","Omit","HTMLDivElement","CopyField","t0","$","copiedLabelProp","copyLabelProp","selectedLabelProp","props","messages","copied","copy","copySelected","setCopy","shown","text","kind","timer","undefined","code","generatedId","rootId","id","copyId","copiedId","named","role","buttonName","t1","t2","Symbol","for","clearTimeout","current","t3","settle","setTimeout","count","write","navigator","clipboard","writeText","t4","selectAndCopy","reportCopied","handleCopy","className","t5","t6","t7","0","1","t8","t9","t10","t11","t12","t13","t14","t15","t16","t17","t18","thrown","queueMicrotask","element","HTMLElement","selection","window","getSelection","range","document","createRange","selectNodeContents","removeAllRanges","addRange","execCommand","_c"],"sources":["../../../src/components/copy-field/index.tsx"],"sourcesContent":["'use client';\n\nimport type { HTMLAttributes, RefAttributes } from 'react';\nimport * as stylex from '@stylexjs/stylex';\nimport { useCallback, useEffect, useId, useRef, useState } from 'react';\nimport { useMessages } from '../../i18n';\nimport { mergeStyles } from '../../styles/merge';\nimport '../../tokens/design.tokens.stylex';\nimport { colors, radii, spacing, typography } from '../../tokens/design.tokens.stylex';\nimport Button from '../button';\n\n// How long the confirmation stands before the control returns to offering the\n// copy again. Not a motion token: those are transition durations, measured in\n// hundreds of milliseconds, and this is a dwell — long enough to be read\n// without being long enough to leave the button lying about what it will do.\nconst COPIED_RESET_MS = 2000;\n\n// No Material Design page draws a read-only value with a copy control. It\n// follows the outlined text field: the 1dp rule in the outline role, the\n// surface behind it, and the control at its trailing end. The value is\n// body-small beside a 32dp text button, so the box is shorter than the\n// field's 56, which suits a value that is read rather than edited, and its\n// corner is the shape scale's small step for the same reason.\n\ntype CopyFieldProps = {\n /**\n * What the button says once the value has been copied, and what is\n * announced. It reverts to `copyLabel` on its own after a couple of\n * seconds. Left out, it is the word for it in the I18nProvider's locale —\n * \"Copied\" in English.\n */\n copiedLabel?: string;\n /**\n * What the button says at rest. Left out, it is the word for it in the\n * I18nProvider's locale — \"Copy\" in English.\n */\n copyLabel?: string;\n /**\n * What the value is — \"Repository URL\", \"Access token\" — for a page with\n * more than one field. It names the field as a group and joins the button's\n * name, so a screen reader hears \"Copy Repository URL\" rather than one\n * \"Copy\" after another; it is not shown. The value itself stays out of the\n * button's name, since a token would be read aloud. Left out, the field is\n * unnamed and its button is \"Copy\" alone, as it always was.\n */\n label?: string;\n /**\n * Called with the value after it reaches the clipboard, never before.\n *\n * Named `onCopied` rather than `onCopy` because the root is a `<div>`, which\n * already has an `onCopy` of its own — React's handler for the native copy\n * event, fired when the reader presses the copy shortcut inside the element.\n * The two are different moments, and a prop that quietly shadowed the\n * built-in would be a trap rather than a convenience.\n */\n onCopied?: (value: string) => void;\n /**\n * Called when the write is refused, with whatever was thrown, and the\n * field's own fallback has not copied the value either. By then the value\n * is selected and the field says so — see `selectedLabel` — so this is for\n * an app that wants to say more, in a snackbar say.\n *\n * Takes the error where `onCopied` takes the value, because the value is\n * the one thing the call site already has and the reason is the one thing\n * it does not. The two refusals throw differently: outside a secure\n * context `navigator.clipboard` is not there at all, and where the\n * permission is denied it is a `DOMException`.\n */\n onCopyFailed?: (error: unknown) => void;\n /**\n * What is announced when the clipboard refused the write and the value has\n * been selected for the reader to copy themselves. The button keeps saying\n * `copyLabel`, since a label slot wide enough for this would double the\n * button's width at rest; the selection itself is what shows. Left out, it\n * is the phrase for it in the I18nProvider's locale — \"Selected to copy\"\n * in English.\n */\n selectedLabel?: string;\n /** The text shown, and the text copied. */\n value: string;\n} & Omit<HTMLAttributes<HTMLDivElement>, 'children'>;\n\n/**\n * A value to be read and taken away — a repository URL, a token, a command.\n * It shows the value in the mono face and copies it on request, confirming\n * on the button itself.\n *\n * A write refused — outside a secure context, or where the permission is\n * denied — does not leave the reader with nothing. The field selects the\n * value and tries the older copy command, which some of those places still\n * allow; failing that, the selection is left ready for the keyboard's own\n * copy, the field announces so, and `onCopyFailed` is called. One click on\n * the value selects exactly the value too.\n */\nfunction CopyField({\n copiedLabel: copiedLabelProp,\n copyLabel: copyLabelProp,\n label,\n onCopied,\n onCopyFailed,\n selectedLabel: selectedLabelProp,\n value,\n ...props\n}: CopyFieldProps & RefAttributes<HTMLDivElement>) {\n const messages = useMessages();\n const copiedLabel = copiedLabelProp ?? messages.copied;\n const copyLabel = copyLabelProp ?? messages.copy;\n const selectedLabel = selectedLabelProp ?? messages.copySelected;\n // What the last write put on the clipboard, and how many writes there have\n // been. The confirmation is that text still being the value shown, so a\n // value that changes inside the dwell — a token regenerated, a selection\n // swapped — takes the label back to Copy at once rather than confirming a\n // copy of something else. The count is what lets a second press be\n // announced, below. `kind` is whether the text reached the clipboard or\n // was only selected for the reader to copy, after a refusal.\n const [copy, setCopy] = useState<null | {\n count: number;\n kind: 'copied' | 'selected';\n text: string;\n }>(null);\n const shown: 'copied' | 'copy' | 'selected' = copy?.text === value ? copy.kind : 'copy';\n const copied = shown === 'copied';\n const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n const code = useRef<HTMLElement>(null);\n const generatedId = useId();\n const rootId = props.id ?? generatedId;\n const copyId = useId();\n const copiedId = useId();\n\n // With a label, the field is a group it names, and the button is named by\n // the label slot on show and then by the group: \"Copy Repository URL\",\n // then \"Copied Repository URL\". The slot on show rather than both, since\n // the two swap `aria-hidden` and a name taking both would say both.\n const named = label === undefined ? undefined : {\n 'aria-label': label,\n id: rootId,\n role: 'group'\n };\n const buttonName = label === undefined ? undefined : `${copied ? copiedId : copyId} ${rootId}`;\n\n // Without this, a field unmounted inside the dwell leaves a timer holding a\n // setState for a tree that is gone.\n useEffect(() => () => {\n clearTimeout(timer.current);\n }, []);\n const handleCopy = useCallback(() => {\n // The component's own state first, the dwell included, and the call\n // site's handler after it: a handler that threw used to skip the timer,\n // leaving the button on Copied for good. Its error is still the call\n // site's to see, raised on its own rather than swallowed.\n const settle = (kind: 'copied' | 'selected') => {\n clearTimeout(timer.current);\n timer.current = setTimeout(() => {\n setCopy(null);\n }, COPIED_RESET_MS);\n setCopy(current => ({\n count: (current?.count ?? 0) + 1,\n kind,\n text: value\n }));\n };\n const write = async () => {\n try {\n await navigator.clipboard.writeText(value);\n } catch (error) {\n // A clipboard write is refused outside a secure context and wherever\n // the permission is denied, neither of which the call site can fix,\n // and a button that did nothing there left the reader with nothing.\n // So the value is selected, which is what the keyboard's own copy\n // acts on, and the older copy command is tried on that selection,\n // since some of those places still allow it. Only where that fails\n // too does the field announce that the value is selected — not that\n // it is copied, which it is not — and report the refusal.\n //\n // Nothing is logged — the library writes to the console nowhere, and\n // a warning an app cannot turn off is not its to emit.\n if (selectAndCopy(code.current)) {\n settle('copied');\n reportCopied(onCopied, value);\n return;\n }\n settle('selected');\n onCopyFailed?.(error);\n return;\n }\n settle('copied');\n reportCopied(onCopied, value);\n };\n void write();\n }, [onCopied, onCopyFailed, value]);\n return <div {...props} {...named} {...mergeStyles({\n className: \"x6s0dn4 x14s9m32 x1fwvt7v x1xdvaa9 x1y0btm7 xmkeg23 x9f619 x78zum5 x126z3so xmqoko8 x18fpw6u\"\n }, props)}>\n <span className=\"x139gbkf x1iyjqo2 x1b35g6e xdb2kus x9d6959 xwth0xh xybisxh xeuugli x6mezaz xvrbanz x126k92a\">\n <code dir=\"ltr\" ref={code} className=\"x9f619 xbppi91 x1qlqyl8 xj0a0fe x4ej8vm\">\n {value}\n </code>\n </span>\n <Button aria-labelledby={buttonName} onClick={handleCopy} size=\"xs\" variant=\"text\">\n <span className=\"xrvj5dj x1o2pa38\">\n <span aria-hidden={copied || undefined} id={copyId} {...{\n 0: {\n className: \"x15r89dc\"\n },\n 1: {\n className: \"x15r89dc xlshs6z\"\n }\n }[!!copied << 0]}>\n {copyLabel}\n </span>\n <span aria-hidden={!copied || undefined} id={copiedId} {...{\n 0: {\n className: \"x15r89dc\"\n },\n 1: {\n className: \"x15r89dc xlshs6z\"\n }\n }[!!!copied << 0]}>\n {copiedLabel}\n </span>\n </span>\n </Button>\n {/* The button's own label changes, but a label changing under a screen\n reader is not reliably announced. This is. `<output>` carries an\n implicit role of status, so it needs no role attribute of its own.\n Each write puts a new node in it, keyed by the count, since a second\n press inside the dwell otherwise left the text as it was and the\n region had nothing to announce. */}\n <output className=\"xjm9jq1 x1hyvwdk x1i1rx1s xb3r6kr x10l6tqk xuxw1ft\">\n {copy !== null && shown !== 'copy' ? <span key={copy.count}>\n {shown === 'copied' ? copiedLabel : selectedLabel}\n </span> : null}\n </output>\n </div>;\n}\n\n/**\n * Hands the call site the value it copied. What its handler throws is raised\n * on its own, outside the copy: it still reaches the page's error reporting,\n * where a rejection of the copy's own promise reached nothing but an\n * unhandled-rejection warning. Outside the component, since the React\n * Compiler compiles neither optional chaining nor a captured catch binding\n * inside a `try`.\n */\nfunction reportCopied(onCopied: ((value: string) => void) | undefined, value: string) {\n if (onCopied === undefined) {\n return;\n }\n try {\n onCopied(value);\n } catch (thrown) {\n queueMicrotask(() => {\n throw thrown;\n });\n }\n}\n\n/**\n * Selects the value's text, which is what the keyboard's own copy acts on,\n * and tries the older copy command on that selection, saying whether it\n * copied. The command is deprecated, and still the one way to the clipboard\n * on an insecure origin, which is where the newer one is refused.\n */\nfunction selectAndCopy(element: HTMLElement | null) {\n const selection = window.getSelection();\n if (element === null || selection === null) {\n return false;\n }\n const range = document.createRange();\n range.selectNodeContents(element);\n selection.removeAllRanges();\n selection.addRange(range);\n try {\n // oxlint-disable-next-line typescript/no-deprecated -- the one fallback an insecure origin still allows\n return document.execCommand('copy');\n } catch {\n return false;\n }\n}\nexport type { CopyFieldProps };\nexport default CopyField;"],"mappings":"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"}
@@ -17,12 +17,16 @@ type CurrencyProps = Omit<RenderComponentProps<'span'>, 'children'> & {
17
17
  * prefixes positives with a plus, and `never` shows neither — which leaves
18
18
  * colour as the only thing separating a credit from a debt, so reach for it
19
19
  * only where the direction is already stated some other way.
20
+ *
21
+ * An amount that shows as zero carries no sign under any of them, since
22
+ * there is nothing owed or owing: `-0.004` reads `$0.00`, not `−$0.00`.
20
23
  * @default 'auto'
21
24
  */
22
25
  sign?: CurrencySignDisplay;
23
26
  /**
24
- * Which colour role to render in. `auto` follows the sign of `value`, with
25
- * zero reading as neutral.
27
+ * Which colour role to render in. `auto` follows the sign of the amount as
28
+ * it is shown, so one that rounds to zero — float residue, or a fraction
29
+ * of a cent — reads as neutral rather than as a gain or a debt.
26
30
  *
27
31
  * The positive and negative roles are guaranteed legible on the surface
28
32
  * family, which is what the library's own tokens and every demo scheme are
@@ -44,71 +44,99 @@ const MINUS_SIGN = "−";
44
44
  * amount.
45
45
  */
46
46
  function Currency(t0) {
47
- const $ = c(12);
47
+ const $ = c(7);
48
48
  const { currency: t1, locale, render, sign: t2, tone: t3, value, ...props$1 } = t0;
49
49
  const currency = t1 === void 0 ? "USD" : t1;
50
50
  const sign = t2 === void 0 ? "auto" : t2;
51
51
  const tone = t3 === void 0 ? "auto" : t3;
52
52
  const provided = useLocale().locale;
53
+ const amount = formatCurrency(value, {
54
+ currency,
55
+ fallbackLocale: provided,
56
+ locale: locale === void 0 || locale === "" ? provided : locale,
57
+ sign
58
+ });
53
59
  const t4 = "span";
54
- const t5 = locale ?? provided;
55
- let t6;
56
- if ($[0] !== currency || $[1] !== sign || $[2] !== t5 || $[3] !== value) {
57
- t6 = formatCurrency(value, {
58
- currency,
59
- locale: t5,
60
- sign
61
- });
62
- $[0] = currency;
63
- $[1] = sign;
64
- $[2] = t5;
65
- $[3] = value;
66
- $[4] = t6;
67
- } else t6 = $[4];
68
- const t7 = mergeStyles(props(styles.base, tones[resolveTone(value, tone)]), props$1);
69
- let t8;
70
- if ($[5] !== props$1 || $[6] !== t6 || $[7] !== t7) {
71
- t8 = {
60
+ const t5 = amount.text;
61
+ const t6 = mergeStyles(props(styles.base, tones[resolveTone(value, tone, amount.shownAsZero)]), props$1);
62
+ let t7;
63
+ if ($[0] !== amount || $[1] !== props$1 || $[2] !== t6) {
64
+ t7 = {
72
65
  ...props$1,
73
- children: t6,
74
- ...t7
66
+ children: t5,
67
+ ...t6
75
68
  };
76
- $[5] = props$1;
77
- $[6] = t6;
78
- $[7] = t7;
79
- $[8] = t8;
80
- } else t8 = $[8];
81
- let t9;
82
- if ($[9] !== render || $[10] !== t8) {
83
- t9 = {
69
+ $[0] = amount;
70
+ $[1] = props$1;
71
+ $[2] = t6;
72
+ $[3] = t7;
73
+ } else t7 = $[3];
74
+ let t8;
75
+ if ($[4] !== render || $[5] !== t7) {
76
+ t8 = {
84
77
  defaultTagName: t4,
85
- props: t8,
78
+ props: t7,
86
79
  render
87
80
  };
88
- $[9] = render;
89
- $[10] = t8;
90
- $[11] = t9;
91
- } else t9 = $[11];
92
- return useRender(t9);
81
+ $[4] = render;
82
+ $[5] = t7;
83
+ $[6] = t8;
84
+ } else t8 = $[6];
85
+ return useRender(t8);
93
86
  }
94
87
  const formatters = /* @__PURE__ */ new Map();
95
88
  function formatCurrency(value, options) {
96
- return formatterFor(options.locale, options.currency, options.sign).formatToParts(value).map((part) => part.type === "minusSign" ? MINUS_SIGN : part.value).join("");
89
+ const { code, formatter } = formatterFor(options.locale, options.currency, options.sign, options.fallbackLocale);
90
+ const parts = formatter.formatToParts(value);
91
+ const amount = parts.map((part) => part.type === "minusSign" ? MINUS_SIGN : part.value).join("");
92
+ return {
93
+ shownAsZero: parts.every((part) => part.type !== "integer" && part.type !== "fraction" || /^0+$/.test(part.value)),
94
+ text: code === null || code === "" ? amount : `${amount} ${code}`
95
+ };
97
96
  }
98
- function formatterFor(locale, currency, sign) {
99
- const key = `${locale}|${currency}|${sign}`;
97
+ const SIGN_DISPLAY = {
98
+ always: "exceptZero",
99
+ auto: "negative",
100
+ never: "never"
101
+ };
102
+ function formatterFor(locale, currency, sign, fallbackLocale) {
103
+ const key = `${locale}|${currency}|${sign}|${fallbackLocale}`;
100
104
  const cached = formatters.get(key);
101
105
  if (cached) return cached;
102
- const formatter = new Intl.NumberFormat(locale, {
103
- currency,
104
- signDisplay: sign,
105
- style: "currency"
106
- });
107
- formatters.set(key, formatter);
108
- return formatter;
106
+ const usable = [locale, fallbackLocale].find(isLocale) ?? "en-US";
107
+ let resolved;
108
+ try {
109
+ resolved = {
110
+ code: null,
111
+ formatter: new Intl.NumberFormat(usable, {
112
+ currency,
113
+ signDisplay: SIGN_DISPLAY[sign],
114
+ style: "currency"
115
+ })
116
+ };
117
+ } catch {
118
+ resolved = {
119
+ code: currency,
120
+ formatter: new Intl.NumberFormat(usable, {
121
+ maximumFractionDigits: 2,
122
+ minimumFractionDigits: 2,
123
+ signDisplay: SIGN_DISPLAY[sign]
124
+ })
125
+ };
126
+ }
127
+ formatters.set(key, resolved);
128
+ return resolved;
129
+ }
130
+ function isLocale(tag) {
131
+ try {
132
+ return Intl.NumberFormat.supportedLocalesOf(tag).length >= 0;
133
+ } catch {
134
+ return false;
135
+ }
109
136
  }
110
- function resolveTone(value, tone) {
137
+ function resolveTone(value, tone, shownAsZero) {
111
138
  if (tone !== "auto") return tone;
139
+ if (shownAsZero) return "neutral";
112
140
  if (value > 0) return "positive";
113
141
  if (value < 0) return "negative";
114
142
  return "neutral";
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["c","_c","stylex","useLocale","RenderComponentProps","useRender","mergeStyles","colors","typography","styles","base","kB7OPa","kMv6JI","kcqcaj","k63SB2","$$css","tones","negative","kMwMTN","neutral","positive","MINUS_SIGN","CurrencyProps","Omit","currency","locale","sign","CurrencySignDisplay","tone","CurrencyTone","value","Currency","t0","$","t1","render","t2","t3","props","undefined","provided","t4","t5","t6","formatCurrency","t7","resolveTone","t8","children","t9","defaultTagName","formatters","Map","Intl","NumberFormat","options","formatterFor","formatToParts","map","part","type","join","key","cached","get","formatter","signDisplay","style","set","_c","props","stylex.props"],"sources":["../../../src/components/currency/index.tsx"],"sourcesContent":["'use client';\n\nimport * as stylex from '@stylexjs/stylex';\nimport { useLocale } from 'react-aria-components';\nimport type { RenderComponentProps } from '../../render/useRender';\nimport { useRender } from '../../render/useRender';\nimport { mergeStyles } from '../../styles/merge';\nimport '../../tokens/design.tokens.stylex';\nimport { colors, typography } from '../../tokens/design.tokens.stylex';\n\n// Deliberately sets no font-size. The same value turns up at several sizes —\n// inside a list row, as a heading-scale figure, as a total — differing in\n// nothing else, so the size is the container's decision. What must never vary\n// is here: the mono face, tabular figures so a column of them lines up on the\n// decimal point, and the medium weight that separates a figure from the prose\n// around it.\n//\n// No Material Design page draws an amount. It is type alone: the mono face\n// and medium weight from the type tokens, tabular figures, and the on surface\n// role for the neutral tone. Positive and negative are the library's own\n// roles, the pair Tag draws from as well.\nconst styles = {\n base: {\n kB7OPa: \"x9f619\",\n kMv6JI: \"xbppi91\",\n kcqcaj: \"xss6m8b\",\n k63SB2: \"x1bf3uqz\",\n $$css: true\n }\n};\nconst tones = {\n negative: {\n kMwMTN: \"x12gp0t\",\n $$css: true\n },\n neutral: {\n kMwMTN: \"x139gbkf\",\n $$css: true\n },\n positive: {\n kMwMTN: \"x4no3js\",\n $$css: true\n }\n};\n\n// U+2212 MINUS SIGN, not the U+002D hyphen Intl actually emits. The hyphen is\n// a typographic stand-in: it is narrower than the plus it pairs with and does\n// not sit on the same optical centre as the digits, which shows up as a\n// wobbling column the moment amounts are stacked.\nconst MINUS_SIGN = '−';\ntype CurrencyProps = Omit<RenderComponentProps<'span'>, 'children'> & {\n /**\n * ISO 4217 code for the currency to format in.\n * @default 'USD'\n */\n currency?: string;\n /**\n * BCP 47 locale to format for. Defaults to React Aria's `I18nProvider`, as\n * the date and number fields do, so an amount reads in the locale the app\n * set — and to the browser's language where there is no provider.\n */\n locale?: string;\n /**\n * When to show the sign. `auto` shows one only on negatives, `always` also\n * prefixes positives with a plus, and `never` shows neither — which leaves\n * colour as the only thing separating a credit from a debt, so reach for it\n * only where the direction is already stated some other way.\n * @default 'auto'\n */\n sign?: CurrencySignDisplay;\n /**\n * Which colour role to render in. `auto` follows the sign of `value`, with\n * zero reading as neutral.\n *\n * The positive and negative roles are guaranteed legible on the surface\n * family, which is what the library's own tokens and every demo scheme are\n * held to. Over a container of its own — a selected row, a filled card —\n * the pair is one colour family on top of another and is not guaranteed, so\n * a custom theme placing one there has to check that pair itself.\n * @default 'auto'\n */\n tone?: CurrencyTone;\n /** The amount, in the currency's major units. */\n value: number;\n};\ntype CurrencySignDisplay = 'always' | 'auto' | 'never';\ntype CurrencyTone = 'auto' | 'negative' | 'neutral' | 'positive';\n\n/**\n * An amount of money, formatted for the reader's locale in the mono face with\n * tabular figures, so a column of amounts lines up on the decimal point.\n * Negatives carry a true minus sign, and the tone follows the sign unless\n * `tone` names one.\n *\n * ```tsx\n * <Currency value={-42.5} currency=\"EUR\" />\n * ```\n *\n * It sets no size of its own, so it takes the type around it: wrap it in a\n * Text, or put it in a heading, for a larger figure.\n *\n * The call site's `className` and `style` land on the element holding the\n * amount.\n */\nfunction Currency({\n currency = 'USD',\n locale,\n render,\n sign = 'auto',\n tone = 'auto',\n value,\n ...props\n}: CurrencyProps) {\n // React Aria holds the locale steady through hydration, where the runtime's\n // default differs between the server and the browser and the amount's text\n // would not match.\n const provided = useLocale().locale;\n return useRender({\n defaultTagName: 'span',\n props: {\n ...props,\n children: formatCurrency(value, {\n currency,\n locale: locale ?? provided,\n sign\n }),\n ...mergeStyles(stylex.props(styles.base, tones[resolveTone(value, tone)]), props)\n },\n render\n });\n}\n\n// Constructing an Intl.NumberFormat resolves the locale, loads the currency's\n// data and builds the pattern; formatToParts on one already built skips all\n// three, which measures about ten times cheaper per value. An instance is\n// stateless once constructed, so one can be shared across every component\n// asking for the same three options.\n//\n// What this saves is a mount and a changing amount, not an idle re-render:\n// the React Compiler already memoises the call below on the four things it\n// reads, so a component re-rendered with the same props never reaches here.\n// That cache belongs to one component instance and counts `value` among its\n// keys, which leaves two cases it cannot cover — a column of rows, where every\n// instance has a cache of its own and each builds a formatter, and a single\n// amount that ticks, where each new value misses.\n//\n// Unbounded on purpose. The keys are the option sets an app actually uses —\n// its locales times its currencies times the three sign settings — so the map\n// settles at that size rather than growing with renders or with values.\nconst formatters = new Map<string, Intl.NumberFormat>();\n\n// formatToParts rather than a replace over the formatted string, so only the\n// sign is substituted. A blind replace would also hit a hyphen inside a\n// locale's currency name or grouping, and locales that wrap negatives in\n// parentheses have no minus to swap at all — both of which this leaves alone.\nfunction formatCurrency(value: number, options: {\n currency: string;\n locale: string;\n sign: CurrencySignDisplay;\n}) {\n return formatterFor(options.locale, options.currency, options.sign).formatToParts(value).map(part => part.type === 'minusSign' ? MINUS_SIGN : part.value).join('');\n}\nfunction formatterFor(locale: string, currency: string, sign: CurrencySignDisplay) {\n // Neither a BCP 47 locale nor an ISO 4217 code can contain a vertical bar\n // and `sign` is one of three known words, so joining on one cannot make two\n // different option sets share a key.\n const key = `${locale}|${currency}|${sign}`;\n const cached = formatters.get(key);\n if (cached) {\n return cached;\n }\n const formatter = new Intl.NumberFormat(locale, {\n currency,\n signDisplay: sign,\n style: 'currency'\n });\n formatters.set(key, formatter);\n return formatter;\n}\n\n// Zero is neither owed nor owing, so it takes the neutral role rather than\n// being forced into one of the two. Negative zero lands here too, which is\n// what a rounded-away debt should read as.\nfunction resolveTone(value: number, tone: CurrencyTone) {\n if (tone !== 'auto') {\n return tone;\n }\n if (value > 0) {\n return 'positive';\n }\n if (value < 0) {\n return 'negative';\n }\n return 'neutral';\n}\nexport type { CurrencyProps, CurrencySignDisplay, CurrencyTone };\nexport default Currency;"],"mappings":";;;;;;;AAqBA,MAAMS,SAAS,EACbC,MAAM;CACJC,QAAQ;CACRC,QAAQ;CACRC,QAAQ;CACRC,QAAQ;CACRC,OAAO;AACT,EACF;AACA,MAAMC,QAAQ;CACZC,UAAU;EACRC,QAAQ;EACRH,OAAO;CACT;CACAI,SAAS;EACPD,QAAQ;EACRH,OAAO;CACT;CACAK,UAAU;EACRF,QAAQ;EACRH,OAAO;CACT;AACF;AAMA,MAAMM,aAAa;;;;;;;;;;;;;;;;;AAuDnB,SAAAU,SAAAC,IAAA;CAAA,MAAAC,IAAAhC,EAAA,EAAA;CAAkB,MAAA,EAAAuB,UAAAU,IAAAT,QAAAU,QAAAT,MAAAU,IAAAR,MAAAS,IAAAP,OAAA,GAAAQ,YAAAN;CAChB,MAAAR,WAAAU,OAAAK,KAAAA,IAAA,QAAAL;CAGA,MAAAR,OAAAU,OAAAG,KAAAA,IAAA,SAAAH;CACA,MAAAR,OAAAS,OAAAE,KAAAA,IAAA,SAAAF;CAOA,MAAAG,WAAiBrC,UAAU,CAAC,CAAAsB;CAEV,MAAAgB,KAAA;CAKJ,MAAAC,KAAAjB,UAAAe;CAAkB,IAAAG;CAAA,IAAAV,EAAA,OAAAT,YAAAS,EAAA,OAAAP,QAAAO,EAAA,OAAAS,MAAAT,EAAA,OAAAH,OAAA;EAFlBa,KAAAC,eAAed,OAAO;GAAAN;GAAAC,QAEtBiB;GAAkBhB;EAE5B,CAAC;EAACO,EAAA,KAAAT;EAAAS,EAAA,KAAAP;EAAAO,EAAA,KAAAS;EAAAT,EAAA,KAAAH;EAAAG,EAAA,KAAAU;CAAA,OAAAA,KAAAV,EAAA;CACC,MAAAY,KAAAvC,YAAYJ,MAAaO,OAAMC,MAAOM,MAAM8B,YAAYhB,OAAOF,IAAI,EAAE,GAAGU,OAAK;CAAC,IAAAS;CAAA,IAAAd,EAAA,OAAAK,WAAAL,EAAA,OAAAU,MAAAV,EAAA,OAAAY,IAAA;EAP5EE,KAAA;GAAA,GACFT;GAAKU,UACEL;GAIR,GACCE;EACL;EAACZ,EAAA,KAAAK;EAAAL,EAAA,KAAAU;EAAAV,EAAA,KAAAY;EAAAZ,EAAA,KAAAc;CAAA,OAAAA,KAAAd,EAAA;CAAA,IAAAgB;CAAA,IAAAhB,EAAA,OAAAE,UAAAF,EAAA,QAAAc,IAAA;EAVcE,KAAA;GAAAC,gBACCT;GAAMH,OACfS;GAQNZ;EAEH;EAACF,EAAA,KAAAE;EAAAF,EAAA,MAAAc;EAAAd,EAAA,MAAAgB;CAAA,OAAAA,KAAAhB,EAAA;CAAA,OAZM5B,UAAU4C,EAYhB;AAAC;AAoBJ,MAAME,6BAAa,IAAIC,IAA+B;AAMtD,SAASR,eAAed,OAAeyB,SAIpC;CACD,OAAOC,aAAaD,QAAQ9B,QAAQ8B,QAAQ/B,UAAU+B,QAAQ7B,IAAI,CAAC,CAAC+B,cAAc3B,KAAK,CAAC,CAAC4B,KAAIC,SAAQA,KAAKC,SAAS,cAAcvC,aAAasC,KAAK7B,KAAK,CAAC,CAAC+B,KAAK,EAAE;AACnK;AACA,SAASL,aAAa/B,QAAgBD,UAAkBE,MAA2B;CAIjF,MAAMoC,MAAM,GAAGrC,OAAM,GAAID,SAAQ,GAAIE;CACrC,MAAMqC,SAASZ,WAAWa,IAAIF,GAAG;CACjC,IAAIC,QACF,OAAOA;CAET,MAAME,YAAY,IAAIZ,KAAKC,aAAa7B,QAAQ;EAC9CD;EACA0C,aAAaxC;EACbyC,OAAO;CACT,CAAC;CACDhB,WAAWiB,IAAIN,KAAKG,SAAS;CAC7B,OAAOA;AACT;AAKA,SAASnB,YAAYhB,OAAeF,MAAoB;CACtD,IAAIA,SAAS,QACX,OAAOA;CAET,IAAIE,QAAQ,GACV,OAAO;CAET,IAAIA,QAAQ,GACV,OAAO;CAET,OAAO;AACT"}
1
+ {"version":3,"file":"index.js","names":["c","_c","stylex","useLocale","RenderComponentProps","useRender","mergeStyles","colors","typography","styles","base","kB7OPa","kMv6JI","kcqcaj","k63SB2","$$css","tones","negative","kMwMTN","neutral","positive","MINUS_SIGN","CurrencyProps","Omit","currency","locale","sign","CurrencySignDisplay","tone","CurrencyTone","value","Currency","t0","$","t1","render","t2","t3","props","undefined","provided","amount","formatCurrency","fallbackLocale","t4","t5","text","t6","resolveTone","shownAsZero","t7","children","t8","defaultTagName","formatters","Map","ResolvedFormat","code","formatter","Intl","NumberFormat","options","formatterFor","parts","formatToParts","map","part","type","join","every","test","SIGN_DISPLAY","always","auto","never","const","Record","NumberFormatOptions","key","cached","get","usable","find","isLocale","resolved","signDisplay","style","maximumFractionDigits","minimumFractionDigits","set","tag","supportedLocalesOf","length","_c","props","stylex.props"],"sources":["../../../src/components/currency/index.tsx"],"sourcesContent":["'use client';\n\nimport * as stylex from '@stylexjs/stylex';\nimport { useLocale } from 'react-aria-components';\nimport type { RenderComponentProps } from '../../render/useRender';\nimport { useRender } from '../../render/useRender';\nimport { mergeStyles } from '../../styles/merge';\nimport '../../tokens/design.tokens.stylex';\nimport { colors, typography } from '../../tokens/design.tokens.stylex';\n\n// Deliberately sets no font-size. The same value turns up at several sizes —\n// inside a list row, as a heading-scale figure, as a total — differing in\n// nothing else, so the size is the container's decision. What must never vary\n// is here: the mono face, tabular figures so a column of them lines up on the\n// decimal point, and the medium weight that separates a figure from the prose\n// around it.\n//\n// No Material Design page draws an amount. It is type alone: the mono face\n// and medium weight from the type tokens, tabular figures, and the on surface\n// role for the neutral tone. Positive and negative are the library's own\n// roles, the pair Tag draws from as well.\nconst styles = {\n base: {\n kB7OPa: \"x9f619\",\n kMv6JI: \"xbppi91\",\n kcqcaj: \"xss6m8b\",\n k63SB2: \"x1bf3uqz\",\n $$css: true\n }\n};\nconst tones = {\n negative: {\n kMwMTN: \"x12gp0t\",\n $$css: true\n },\n neutral: {\n kMwMTN: \"x139gbkf\",\n $$css: true\n },\n positive: {\n kMwMTN: \"x4no3js\",\n $$css: true\n }\n};\n\n// U+2212 MINUS SIGN, not the U+002D hyphen Intl actually emits. The hyphen is\n// a typographic stand-in: it is narrower than the plus it pairs with and does\n// not sit on the same optical centre as the digits, which shows up as a\n// wobbling column the moment amounts are stacked.\nconst MINUS_SIGN = '−';\ntype CurrencyProps = Omit<RenderComponentProps<'span'>, 'children'> & {\n /**\n * ISO 4217 code for the currency to format in.\n * @default 'USD'\n */\n currency?: string;\n /**\n * BCP 47 locale to format for. Defaults to React Aria's `I18nProvider`, as\n * the date and number fields do, so an amount reads in the locale the app\n * set — and to the browser's language where there is no provider.\n */\n locale?: string;\n /**\n * When to show the sign. `auto` shows one only on negatives, `always` also\n * prefixes positives with a plus, and `never` shows neither — which leaves\n * colour as the only thing separating a credit from a debt, so reach for it\n * only where the direction is already stated some other way.\n *\n * An amount that shows as zero carries no sign under any of them, since\n * there is nothing owed or owing: `-0.004` reads `$0.00`, not `−$0.00`.\n * @default 'auto'\n */\n sign?: CurrencySignDisplay;\n /**\n * Which colour role to render in. `auto` follows the sign of the amount as\n * it is shown, so one that rounds to zero — float residue, or a fraction\n * of a cent — reads as neutral rather than as a gain or a debt.\n *\n * The positive and negative roles are guaranteed legible on the surface\n * family, which is what the library's own tokens and every demo scheme are\n * held to. Over a container of its own — a selected row, a filled card —\n * the pair is one colour family on top of another and is not guaranteed, so\n * a custom theme placing one there has to check that pair itself.\n * @default 'auto'\n */\n tone?: CurrencyTone;\n /** The amount, in the currency's major units. */\n value: number;\n};\ntype CurrencySignDisplay = 'always' | 'auto' | 'never';\ntype CurrencyTone = 'auto' | 'negative' | 'neutral' | 'positive';\n\n/**\n * An amount of money, formatted for the reader's locale in the mono face with\n * tabular figures, so a column of amounts lines up on the decimal point.\n * Negatives carry a true minus sign, and the tone follows the sign unless\n * `tone` names one.\n *\n * ```tsx\n * <Currency value={-42.5} currency=\"EUR\" />\n * ```\n *\n * It sets no size of its own, so it takes the type around it: wrap it in a\n * Text, or put it in a heading, for a larger figure.\n *\n * The call site's `className` and `style` land on the element holding the\n * amount.\n */\nfunction Currency({\n currency = 'USD',\n locale,\n render,\n sign = 'auto',\n tone = 'auto',\n value,\n ...props\n}: CurrencyProps) {\n // React Aria holds the locale steady through hydration, where the runtime's\n // default differs between the server and the browser and the amount's text\n // would not match.\n const provided = useLocale().locale;\n const amount = formatCurrency(value, {\n currency,\n fallbackLocale: provided,\n // An empty string asks for the provider's locale as `undefined` does —\n // it is what clearing a locale control leaves — rather than reaching the\n // formatter as a tag it rejects.\n locale: locale === undefined || locale === '' ? provided : locale,\n sign\n });\n return useRender({\n defaultTagName: 'span',\n props: {\n ...props,\n children: amount.text,\n ...mergeStyles(stylex.props(styles.base, tones[resolveTone(value, tone, amount.shownAsZero)]), props)\n },\n render\n });\n}\n\n// Constructing an Intl.NumberFormat resolves the locale, loads the currency's\n// data and builds the pattern; formatToParts on one already built skips all\n// three, which measures about ten times cheaper per value. An instance is\n// stateless once constructed, so one can be shared across every component\n// asking for the same three options.\n//\n// What this saves is a mount and a changing amount, not an idle re-render:\n// the React Compiler already memoises the call below on the four things it\n// reads, so a component re-rendered with the same props never reaches here.\n// That cache belongs to one component instance and counts `value` among its\n// keys, which leaves two cases it cannot cover — a column of rows, where every\n// instance has a cache of its own and each builds a formatter, and a single\n// amount that ticks, where each new value misses.\n//\n// Unbounded on purpose. The keys are the option sets an app actually uses —\n// its locales times its currencies times the three sign settings — so the map\n// settles at that size rather than growing with renders or with values.\n//\n// A request the runtime rejects is cached too, under its own key, as the\n// fallback it resolved to, so a bad pair costs one failed construction rather\n// than one a render.\nconst formatters = new Map<string, ResolvedFormat>();\n\n/**\n * A formatter, and the currency code written beside its number when it is\n * the decimal fallback for a code the runtime rejected.\n */\ntype ResolvedFormat = {\n code: null | string;\n formatter: Intl.NumberFormat;\n};\n\n// formatToParts rather than a replace over the formatted string, so only the\n// sign is substituted. A blind replace would also hit a hyphen inside a\n// locale's currency name or grouping, and locales that wrap negatives in\n// parentheses have no minus to swap at all — both of which this leaves alone.\nfunction formatCurrency(value: number, options: {\n currency: string;\n fallbackLocale: string;\n locale: string;\n sign: CurrencySignDisplay;\n}) {\n const {\n code,\n formatter\n } = formatterFor(options.locale, options.currency, options.sign, options.fallbackLocale);\n const parts = formatter.formatToParts(value);\n const amount = parts.map(part => part.type === 'minusSign' ? MINUS_SIGN : part.value).join('');\n return {\n // Whether every digit shown is a zero, which is what the tone reads\n // rather than the raw value: the two never saw the same number before.\n shownAsZero: parts.every(part => part.type !== 'integer' && part.type !== 'fraction' || /^0+$/.test(part.value)),\n text: code === null || code === '' ? amount : `${amount} ${code}`\n };\n}\n\n// The sign each setting asks Intl for. `negative` and `exceptZero` are the\n// forms that leave a zero bare, which `auto` and `always` do not: they put a\n// minus on any negative input, and a plus on any positive one, including\n// those that round to a displayed zero.\nconst SIGN_DISPLAY = {\n always: 'exceptZero',\n auto: 'negative',\n never: 'never'\n} as const satisfies Record<CurrencySignDisplay, Intl.NumberFormatOptions['signDisplay']>;\n\n// A component that only shows an amount must not take the page down with it,\n// and `new Intl.NumberFormat` throws for a malformed locale (`xx_YY`, an\n// empty string) and for a currency code that is not one (`xx`, `US`). Valid\n// input takes exactly the path it always did; the rest falls back. A locale\n// the runtime rejects gives way to the provider's, and a currency code it\n// rejects to the plain number in that locale with the code written after it,\n// so the amount still reads and what was wrong with it stays visible.\n//\n// Nothing is logged: the library writes to the console nowhere, and a\n// warning an app cannot turn off is not its to emit.\nfunction formatterFor(locale: string, currency: string, sign: CurrencySignDisplay, fallbackLocale: string) {\n // Neither a BCP 47 locale nor an ISO 4217 code can contain a vertical bar\n // and `sign` is one of three known words, so joining on one cannot make two\n // different option sets share a key.\n const key = `${locale}|${currency}|${sign}|${fallbackLocale}`;\n const cached = formatters.get(key);\n if (cached) {\n return cached;\n }\n const usable = [locale, fallbackLocale].find(isLocale) ?? 'en-US';\n let resolved: ResolvedFormat;\n try {\n resolved = {\n code: null,\n formatter: new Intl.NumberFormat(usable, {\n currency,\n signDisplay: SIGN_DISPLAY[sign],\n style: 'currency'\n })\n };\n } catch {\n resolved = {\n code: currency,\n formatter: new Intl.NumberFormat(usable, {\n maximumFractionDigits: 2,\n minimumFractionDigits: 2,\n signDisplay: SIGN_DISPLAY[sign]\n })\n };\n }\n formatters.set(key, resolved);\n return resolved;\n}\n\n// Whether the runtime takes `tag` as a locale at all — one it parses, whether\n// or not it has data for it, which it then fills in from the nearest it has.\nfunction isLocale(tag: string) {\n try {\n return Intl.NumberFormat.supportedLocalesOf(tag).length >= 0;\n } catch {\n return false;\n }\n}\n\n// An amount shown as zero is neither owed nor owing, so it takes the neutral\n// role whatever the raw value's sign: negative zero, a rounded-away debt and\n// float residue all read as settled. Otherwise the sign decides.\nfunction resolveTone(value: number, tone: CurrencyTone, shownAsZero: boolean) {\n if (tone !== 'auto') {\n return tone;\n }\n if (shownAsZero) {\n return 'neutral';\n }\n if (value > 0) {\n return 'positive';\n }\n if (value < 0) {\n return 'negative';\n }\n return 'neutral';\n}\nexport type { CurrencyProps, CurrencySignDisplay, CurrencyTone };\nexport default Currency;"],"mappings":"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"}