@eifi1/ui-kit 0.21.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (320) hide show
  1. package/README.md +42 -25
  2. package/dist/chart.d.ts +19 -14
  3. package/dist/components/account-chips.d.ts +10 -3
  4. package/dist/components/account-chips.js +5 -2
  5. package/dist/components/account-chips.js.map +1 -1
  6. package/dist/components/account-settings.d.ts +33 -2
  7. package/dist/components/account-settings.js +43 -21
  8. package/dist/components/account-settings.js.map +1 -1
  9. package/dist/components/amount-input.d.ts +53 -14
  10. package/dist/components/amount-input.js +17 -28
  11. package/dist/components/amount-input.js.map +1 -1
  12. package/dist/components/autocomplete.d.ts +29 -0
  13. package/dist/components/autocomplete.js +134 -104
  14. package/dist/components/autocomplete.js.map +1 -1
  15. package/dist/components/button-group.d.ts +19 -14
  16. package/dist/components/calculator.d.ts +19 -14
  17. package/dist/components/checkbox-group.d.ts +104 -0
  18. package/dist/components/checkbox-group.js +111 -0
  19. package/dist/components/checkbox-group.js.map +1 -0
  20. package/dist/components/checkbox.d.ts +19 -0
  21. package/dist/components/checkbox.js +30 -7
  22. package/dist/components/checkbox.js.map +1 -1
  23. package/dist/components/chip.d.ts +18 -0
  24. package/dist/components/chip.js +47 -10
  25. package/dist/components/chip.js.map +1 -1
  26. package/dist/components/column-mapper.d.ts +182 -0
  27. package/dist/components/column-mapper.js +375 -0
  28. package/dist/components/column-mapper.js.map +1 -0
  29. package/dist/components/combobox-core.d.ts +39 -2
  30. package/dist/components/combobox-core.js +29 -6
  31. package/dist/components/combobox-core.js.map +1 -1
  32. package/dist/components/combobox.d.ts +91 -2
  33. package/dist/components/combobox.js +454 -308
  34. package/dist/components/combobox.js.map +1 -1
  35. package/dist/components/confirm-dialog.d.ts +69 -161
  36. package/dist/components/confirm-dialog.js +7 -8
  37. package/dist/components/confirm-dialog.js.map +1 -1
  38. package/dist/components/copy-button.d.ts +16 -11
  39. package/dist/components/country-select.d.ts +167 -0
  40. package/dist/components/country-select.js +293 -0
  41. package/dist/components/country-select.js.map +1 -0
  42. package/dist/components/currency-select.d.ts +17 -1
  43. package/dist/components/currency-select.js +56 -23
  44. package/dist/components/currency-select.js.map +1 -1
  45. package/dist/components/danger-confirm.d.ts +69 -166
  46. package/dist/components/danger-confirm.js +144 -50
  47. package/dist/components/danger-confirm.js.map +1 -1
  48. package/dist/components/data-table-cells.d.ts +1 -2
  49. package/dist/components/data-table-filter-popover.d.ts +1 -2
  50. package/dist/components/data-table-filters.d.ts +1 -2
  51. package/dist/components/data-table-labels.d.ts +4 -97
  52. package/dist/components/data-table-labels.js +9 -1
  53. package/dist/components/data-table-labels.js.map +1 -1
  54. package/dist/components/data-table-pagination.d.ts +3 -1
  55. package/dist/components/data-table.d.ts +1 -2
  56. package/dist/components/data-table.js +98 -3
  57. package/dist/components/data-table.js.map +1 -1
  58. package/dist/components/date-picker.d.ts +66 -5
  59. package/dist/components/date-picker.js +378 -66
  60. package/dist/components/date-picker.js.map +1 -1
  61. package/dist/components/entity-combobox.d.ts +42 -1
  62. package/dist/components/entity-combobox.js +154 -113
  63. package/dist/components/entity-combobox.js.map +1 -1
  64. package/dist/components/facing-pair.d.ts +19 -14
  65. package/dist/components/field-parts.d.ts +224 -0
  66. package/dist/components/field-parts.js +156 -0
  67. package/dist/components/field-parts.js.map +1 -0
  68. package/dist/components/field-strip.d.ts +105 -0
  69. package/dist/components/field-strip.js +46 -0
  70. package/dist/components/field-strip.js.map +1 -0
  71. package/dist/components/file-button.d.ts +19 -14
  72. package/dist/components/file-button.js +13 -3
  73. package/dist/components/file-button.js.map +1 -1
  74. package/dist/components/file-dropzone.d.ts +41 -15
  75. package/dist/components/file-dropzone.js +57 -34
  76. package/dist/components/file-dropzone.js.map +1 -1
  77. package/dist/components/form-actions.d.ts +18 -13
  78. package/dist/components/form-actions.js +69 -5
  79. package/dist/components/form-actions.js.map +1 -1
  80. package/dist/components/iban-input.d.ts +69 -0
  81. package/dist/components/iban-input.js +125 -0
  82. package/dist/components/iban-input.js.map +1 -0
  83. package/dist/components/icon-picker.d.ts +21 -2
  84. package/dist/components/icon-picker.js +19 -4
  85. package/dist/components/icon-picker.js.map +1 -1
  86. package/dist/components/inline-edit-field.d.ts +167 -0
  87. package/dist/components/inline-edit-field.js +293 -0
  88. package/dist/components/inline-edit-field.js.map +1 -0
  89. package/dist/components/language-select.d.ts +130 -0
  90. package/dist/components/language-select.js +18 -0
  91. package/dist/components/language-select.js.map +1 -0
  92. package/dist/components/line-items.d.ts +25 -3
  93. package/dist/components/line-items.js +55 -46
  94. package/dist/components/line-items.js.map +1 -1
  95. package/dist/components/list.d.ts +25 -0
  96. package/dist/components/list.js +16 -5
  97. package/dist/components/list.js.map +1 -1
  98. package/dist/components/money-field.d.ts +32 -14
  99. package/dist/components/money-field.js.map +1 -1
  100. package/dist/components/month-picker.d.ts +52 -4
  101. package/dist/components/month-picker.js +291 -86
  102. package/dist/components/month-picker.js.map +1 -1
  103. package/dist/components/multi-entity-combobox.d.ts +26 -1
  104. package/dist/components/multi-entity-combobox.js +143 -102
  105. package/dist/components/multi-entity-combobox.js.map +1 -1
  106. package/dist/components/multi-select.d.ts +8 -1
  107. package/dist/components/multi-select.js +9 -4
  108. package/dist/components/multi-select.js.map +1 -1
  109. package/dist/components/number-field.d.ts +34 -19
  110. package/dist/components/number-field.js +6 -20
  111. package/dist/components/number-field.js.map +1 -1
  112. package/dist/components/number-input.d.ts +79 -19
  113. package/dist/components/number-input.js +90 -38
  114. package/dist/components/number-input.js.map +1 -1
  115. package/dist/components/numpad-sheet.d.ts +19 -14
  116. package/dist/components/one-time-code-input.d.ts +100 -0
  117. package/dist/components/one-time-code-input.js +137 -0
  118. package/dist/components/one-time-code-input.js.map +1 -0
  119. package/dist/components/phone-input.d.ts +69 -0
  120. package/dist/components/phone-input.js +171 -0
  121. package/dist/components/phone-input.js.map +1 -0
  122. package/dist/components/reauth-dialog.d.ts +2 -2
  123. package/dist/components/reauth-dialog.js +6 -8
  124. package/dist/components/reauth-dialog.js.map +1 -1
  125. package/dist/components/series-chart.d.ts +19 -14
  126. package/dist/components/settings-fields.d.ts +19 -14
  127. package/dist/components/share-card.d.ts +16 -11
  128. package/dist/components/sign-chip.d.ts +66 -0
  129. package/dist/components/sign-chip.js +39 -0
  130. package/dist/components/sign-chip.js.map +1 -0
  131. package/dist/components/swatch-picker.d.ts +43 -2
  132. package/dist/components/swatch-picker.js +80 -32
  133. package/dist/components/swatch-picker.js.map +1 -1
  134. package/dist/components/switch.d.ts +19 -0
  135. package/dist/components/switch.js +24 -6
  136. package/dist/components/switch.js.map +1 -1
  137. package/dist/components/text-link.d.ts +18 -13
  138. package/dist/components/tile-radio.d.ts +88 -13
  139. package/dist/components/tile-radio.js +28 -11
  140. package/dist/components/tile-radio.js.map +1 -1
  141. package/dist/components/time-input.d.ts +24 -15
  142. package/dist/components/time-input.js.map +1 -1
  143. package/dist/components/toggle-group.d.ts +47 -1
  144. package/dist/components/toggle-group.js +110 -69
  145. package/dist/components/toggle-group.js.map +1 -1
  146. package/dist/components/trigger-aria.d.ts +3 -0
  147. package/dist/components/trigger-aria.js +3 -1
  148. package/dist/components/trigger-aria.js.map +1 -1
  149. package/dist/components/ui.d.ts +16 -11
  150. package/dist/components/ui.js +284 -88
  151. package/dist/components/ui.js.map +1 -1
  152. package/dist/components/use-table-state.d.ts +1 -2
  153. package/dist/{data-table-3mi2R3so.d.ts → data-table-labels-B7OdnM0S.d.ts} +140 -3
  154. package/dist/data-table.d.ts +1 -2
  155. package/dist/data-table.js +2 -1
  156. package/dist/data-table.js.map +1 -1
  157. package/dist/feedback/feedback-attachment.d.ts +1 -1
  158. package/dist/feedback/feedback-attachment.js +348 -41
  159. package/dist/feedback/feedback-attachment.js.map +1 -1
  160. package/dist/feedback/feedback-dialog.d.ts +1 -1
  161. package/dist/feedback/feedback-dialog.js.map +1 -1
  162. package/dist/feedback/feedback-inbox.d.ts +3 -2
  163. package/dist/feedback/feedback-inbox.js.map +1 -1
  164. package/dist/feedback/feedback-thread.d.ts +68 -253
  165. package/dist/feedback/feedback-thread.js +46 -10
  166. package/dist/feedback/feedback-thread.js.map +1 -1
  167. package/dist/{kit-labels-D1J2_jyD.d.ts → feedback-BxeQVzwq.d.ts} +1206 -20
  168. package/dist/{feedback-attachment-WFiXJ8We.d.ts → feedback-attachment-fGAzZPf0.d.ts} +178 -11
  169. package/dist/feedback.d.ts +66 -2
  170. package/dist/hooks/use-file-drop.d.ts +19 -14
  171. package/dist/i18n/defaults.d.ts +19 -14
  172. package/dist/i18n/defaults.js +19 -2
  173. package/dist/i18n/defaults.js.map +1 -1
  174. package/dist/i18n/german.d.ts +19 -14
  175. package/dist/i18n/german.js +84 -5
  176. package/dist/i18n/german.js.map +1 -1
  177. package/dist/i18n/kit-labels.d.ts +15 -10
  178. package/dist/i18n/kit-labels.js +8 -2
  179. package/dist/i18n/kit-labels.js.map +1 -1
  180. package/dist/i18n/languages.d.ts +19 -14
  181. package/dist/i18n/locales/de-CH.d.ts +19 -14
  182. package/dist/i18n/locales/en.d.ts +19 -14
  183. package/dist/i18n/locales/en.js +16 -0
  184. package/dist/i18n/locales/en.js.map +1 -1
  185. package/dist/i18n/locales/es.d.ts +19 -14
  186. package/dist/i18n/locales/es.js +83 -5
  187. package/dist/i18n/locales/es.js.map +1 -1
  188. package/dist/i18n/locales/fr.d.ts +19 -14
  189. package/dist/i18n/locales/fr.js +82 -5
  190. package/dist/i18n/locales/fr.js.map +1 -1
  191. package/dist/i18n/locales/hu.d.ts +19 -14
  192. package/dist/i18n/locales/hu.js +85 -5
  193. package/dist/i18n/locales/hu.js.map +1 -1
  194. package/dist/i18n/locales/it.d.ts +19 -14
  195. package/dist/i18n/locales/it.js +84 -5
  196. package/dist/i18n/locales/it.js.map +1 -1
  197. package/dist/i18n/locales/zh.d.ts +19 -14
  198. package/dist/i18n/locales/zh.js +80 -5
  199. package/dist/i18n/locales/zh.js.map +1 -1
  200. package/dist/i18n/review.d.ts +19 -14
  201. package/dist/i18n/review.js +27 -1
  202. package/dist/i18n/review.js.map +1 -1
  203. package/dist/index.d.ts +21 -10
  204. package/dist/index.js +73 -2
  205. package/dist/index.js.map +1 -1
  206. package/dist/lib/column-mapping.d.ts +81 -0
  207. package/dist/lib/column-mapping.js +108 -0
  208. package/dist/lib/column-mapping.js.map +1 -0
  209. package/dist/lib/countries.d.ts +43 -0
  210. package/dist/lib/countries.js +39 -0
  211. package/dist/lib/countries.js.map +1 -0
  212. package/dist/lib/decimal-marks.d.ts +46 -0
  213. package/dist/lib/decimal-marks.js +40 -0
  214. package/dist/lib/decimal-marks.js.map +1 -0
  215. package/dist/lib/iban.d.ts +98 -0
  216. package/dist/lib/iban.js +154 -0
  217. package/dist/lib/iban.js.map +1 -0
  218. package/dist/lib/isin.d.ts +30 -0
  219. package/dist/lib/isin.js +36 -0
  220. package/dist/lib/isin.js.map +1 -0
  221. package/dist/lib/phone.d.ts +69 -0
  222. package/dist/lib/phone.js +121 -0
  223. package/dist/lib/phone.js.map +1 -0
  224. package/dist/lib/table-text.d.ts +109 -1
  225. package/dist/lib/table-text.js +122 -1
  226. package/dist/lib/table-text.js.map +1 -1
  227. package/dist/rhf/fields.d.ts +233 -18
  228. package/dist/rhf/fields.js +456 -3
  229. package/dist/rhf/fields.js.map +1 -1
  230. package/dist/rhf/form.d.ts +19 -14
  231. package/dist/rhf.d.ts +22 -15
  232. package/dist/rhf.js.map +1 -1
  233. package/dist/shell/app-shell.d.ts +18 -13
  234. package/dist/shell/top-bar-brand.d.ts +19 -14
  235. package/dist/shell/topbar-action-menu.d.ts +36 -3
  236. package/dist/shell/topbar-action-menu.js +74 -33
  237. package/dist/shell/topbar-action-menu.js.map +1 -1
  238. package/dist/shell.d.ts +18 -13
  239. package/dist/table-text.d.ts +1 -1
  240. package/dist/tour/tour.js +20 -24
  241. package/dist/tour/tour.js.map +1 -1
  242. package/dist/wizard/stepper-nav.d.ts +49 -16
  243. package/dist/wizard/stepper-nav.js +4 -0
  244. package/dist/wizard/stepper-nav.js.map +1 -1
  245. package/dist/wizard.d.ts +19 -14
  246. package/package.json +1 -1
  247. package/src/components/account-chips.tsx +17 -3
  248. package/src/components/account-settings.tsx +92 -26
  249. package/src/components/amount-input.tsx +68 -56
  250. package/src/components/autocomplete.tsx +198 -114
  251. package/src/components/checkbox-group.tsx +229 -0
  252. package/src/components/checkbox.tsx +72 -8
  253. package/src/components/chip.tsx +80 -10
  254. package/src/components/column-mapper.tsx +666 -0
  255. package/src/components/combobox-core.tsx +82 -9
  256. package/src/components/combobox.tsx +661 -340
  257. package/src/components/confirm-dialog.tsx +12 -8
  258. package/src/components/country-select.tsx +556 -0
  259. package/src/components/currency-select.tsx +109 -33
  260. package/src/components/danger-confirm.tsx +429 -82
  261. package/src/components/data-table-labels.ts +16 -1
  262. package/src/components/data-table.tsx +175 -6
  263. package/src/components/date-picker.tsx +595 -71
  264. package/src/components/entity-combobox.tsx +235 -124
  265. package/src/components/field-parts.tsx +403 -0
  266. package/src/components/field-strip.tsx +149 -0
  267. package/src/components/file-button.tsx +36 -3
  268. package/src/components/file-dropzone.tsx +106 -38
  269. package/src/components/form-actions.tsx +147 -6
  270. package/src/components/iban-input.tsx +231 -0
  271. package/src/components/icon-picker.tsx +47 -6
  272. package/src/components/inline-edit-field.tsx +533 -0
  273. package/src/components/language-select.tsx +78 -0
  274. package/src/components/line-items.tsx +35 -2
  275. package/src/components/list.tsx +55 -6
  276. package/src/components/money-field.tsx +13 -0
  277. package/src/components/month-picker.tsx +400 -63
  278. package/src/components/multi-entity-combobox.tsx +207 -120
  279. package/src/components/multi-select.tsx +80 -58
  280. package/src/components/number-field.tsx +28 -46
  281. package/src/components/number-input.tsx +210 -43
  282. package/src/components/one-time-code-input.tsx +288 -0
  283. package/src/components/phone-input.tsx +299 -0
  284. package/src/components/reauth-dialog.tsx +17 -18
  285. package/src/components/sign-chip.tsx +103 -0
  286. package/src/components/swatch-picker.tsx +143 -31
  287. package/src/components/switch.tsx +58 -7
  288. package/src/components/tile-radio.tsx +145 -29
  289. package/src/components/time-input.tsx +8 -1
  290. package/src/components/toggle-group.tsx +206 -73
  291. package/src/components/trigger-aria.ts +5 -0
  292. package/src/components/ui.tsx +479 -138
  293. package/src/data-table.ts +2 -1
  294. package/src/feedback/feedback-attachment.tsx +597 -46
  295. package/src/feedback/feedback-dialog.tsx +9 -3
  296. package/src/feedback/feedback-inbox.tsx +3 -2
  297. package/src/feedback/feedback-thread.tsx +113 -9
  298. package/src/i18n/defaults.ts +18 -1
  299. package/src/i18n/german.ts +89 -0
  300. package/src/i18n/kit-labels.tsx +82 -2
  301. package/src/i18n/locales/en.ts +36 -4
  302. package/src/i18n/locales/es.ts +87 -0
  303. package/src/i18n/locales/fr.ts +87 -0
  304. package/src/i18n/locales/hu.ts +83 -0
  305. package/src/i18n/locales/it.ts +88 -0
  306. package/src/i18n/locales/zh.ts +76 -0
  307. package/src/i18n/review.ts +26 -0
  308. package/src/index.ts +71 -3
  309. package/src/lib/column-mapping.ts +234 -0
  310. package/src/lib/countries.ts +91 -0
  311. package/src/lib/decimal-marks.ts +99 -0
  312. package/src/lib/iban.ts +171 -0
  313. package/src/lib/isin.ts +62 -0
  314. package/src/lib/phone.ts +211 -0
  315. package/src/lib/table-text.ts +271 -0
  316. package/src/rhf/fields.tsx +860 -9
  317. package/src/rhf.ts +3 -1
  318. package/src/shell/topbar-action-menu.tsx +134 -38
  319. package/src/tour/tour.tsx +66 -28
  320. package/src/wizard/stepper-nav.tsx +35 -1
@@ -0,0 +1,293 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useCallback, useId, useMemo } from "react";
4
+ import { X } from "lucide-react";
5
+ import { cn } from "../lib/cn.js";
6
+ import { COUNTRY_CODES, countryName, normalizeCountryCode } from "../lib/countries.js";
7
+ import { FieldChevron, FIELD_FLOATING_PAD, FIELD_INVALID, FIELD_TRIGGER, FloatingField } from "./ui.js";
8
+ import { Tooltip } from "./tooltip.js";
9
+ import { CurrencyFlag } from "./currency-select.js";
10
+ import {
11
+ ComboboxPanel,
12
+ useComboboxCore,
13
+ useComboboxFieldError
14
+ } from "./combobox-core.js";
15
+ import { assignRef, hasMessage, mergeDescribedBy } from "./choice-parts.js";
16
+ import { useCommitReason } from "./write-lock.js";
17
+ import {
18
+ DEFAULT_COMBOBOX_LABELS,
19
+ DEFAULT_COMMON_LABELS,
20
+ useKitLabels,
21
+ useKitLocale
22
+ } from "../i18n/kit-labels.js";
23
+ const DEFAULT_COUNTRY_SELECT_LABELS = {
24
+ country: "Country",
25
+ search: "Search country",
26
+ others: "Other countries"
27
+ };
28
+ function fold(s) {
29
+ return s.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase();
30
+ }
31
+ function rank(e, q) {
32
+ if (e.code.toLowerCase() === q) return 0;
33
+ if (e.folded.startsWith(q)) return 1;
34
+ return e.folded.includes(q) ? 2 : -1;
35
+ }
36
+ function search(entries, q) {
37
+ return entries.map((e) => ({ e, r: rank(e, q) })).filter((x) => x.r >= 0).sort((a, b) => a.r - b.r).map((x) => x.e);
38
+ }
39
+ function toOption(e, flags, group) {
40
+ return {
41
+ value: e.code,
42
+ label: e.name,
43
+ icon: flags ? /* @__PURE__ */ jsx(CurrencyFlag, { country: e.code.toLowerCase() }) : void 0,
44
+ group
45
+ };
46
+ }
47
+ function collator(locale) {
48
+ try {
49
+ return new Intl.Collator(locale);
50
+ } catch {
51
+ return new Intl.Collator();
52
+ }
53
+ }
54
+ function CountrySelect({
55
+ value,
56
+ onChange,
57
+ countries,
58
+ preferred,
59
+ flags = false,
60
+ label,
61
+ hint,
62
+ error,
63
+ invalid,
64
+ placeholder,
65
+ disabled,
66
+ disabledReason: ownReason,
67
+ commit,
68
+ clearable,
69
+ clearLabel,
70
+ locale: localeProp,
71
+ labels: labelsProp,
72
+ className,
73
+ id,
74
+ ref,
75
+ "aria-label": ariaLabel,
76
+ "aria-labelledby": ariaLabelledBy,
77
+ "aria-describedby": ariaDescribedBy,
78
+ "aria-invalid": ariaInvalid,
79
+ "aria-required": ariaRequired,
80
+ ...rest
81
+ }) {
82
+ const text = useKitLabels("countrySelect", DEFAULT_COUNTRY_SELECT_LABELS, labelsProp);
83
+ const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
84
+ const comboText = useKitLabels("combobox", DEFAULT_COMBOBOX_LABELS, { clear: clearLabel });
85
+ const locale = useKitLocale(localeProp) ?? "en";
86
+ const countriesKey = countries?.join(",");
87
+ const preferredKey = preferred?.join(",");
88
+ const { top, rest: others } = useMemo(() => {
89
+ const seen = /* @__PURE__ */ new Set();
90
+ const pool = [];
91
+ for (const raw of countriesKey !== void 0 ? countriesKey.split(",") : COUNTRY_CODES) {
92
+ const code2 = normalizeCountryCode(raw);
93
+ if (code2 && !seen.has(code2)) {
94
+ seen.add(code2);
95
+ pool.push(code2);
96
+ }
97
+ }
98
+ const entry = (code2) => {
99
+ const name2 = countryName(code2, locale);
100
+ return { code: code2, name: name2, folded: fold(name2) };
101
+ };
102
+ const firsts = [];
103
+ const picked = /* @__PURE__ */ new Set();
104
+ for (const raw of preferredKey ? preferredKey.split(",") : []) {
105
+ const code2 = normalizeCountryCode(raw);
106
+ if (code2 && seen.has(code2) && !picked.has(code2)) {
107
+ picked.add(code2);
108
+ firsts.push(entry(code2));
109
+ }
110
+ }
111
+ const compare = collator(locale).compare;
112
+ const sorted = pool.filter((c) => !picked.has(c)).map(entry).sort((a, b) => compare(a.name, b.name));
113
+ return { top: firsts, rest: sorted };
114
+ }, [countriesKey, preferredKey, locale]);
115
+ const allOptions = useMemo(
116
+ () => [...top, ...others].map((e) => toOption(e, flags)),
117
+ [top, others, flags]
118
+ );
119
+ const core = useComboboxCore({ options: allOptions, filter: false });
120
+ const { open, setOpen, query, triggerRef, closeToTrigger } = core;
121
+ const setTrigger = useCallback(
122
+ (el) => {
123
+ triggerRef.current = el;
124
+ assignRef(ref, el);
125
+ },
126
+ [ref, triggerRef]
127
+ );
128
+ const q = fold(query.trim());
129
+ const results = useMemo(() => {
130
+ const firsts = q ? search(top, q) : top;
131
+ const below = q ? search(others, q) : others;
132
+ const heading = firsts.length > 0 ? text.others : void 0;
133
+ return [...firsts.map((e) => toOption(e, flags)), ...below.map((e) => toOption(e, flags, heading))];
134
+ }, [top, others, q, text.others, flags]);
135
+ const reason = useCommitReason(commit, ownReason);
136
+ const locked = hasMessage(reason);
137
+ const inert = locked || Boolean(disabled);
138
+ if (inert && open) setOpen(false);
139
+ const generated = useId();
140
+ const triggerId = id ?? generated;
141
+ const listboxId = `${generated}-listbox`;
142
+ const hintId = useId();
143
+ const reasonId = useId();
144
+ const textHint = typeof hint === "string" && hint !== "" || typeof hint === "number";
145
+ const field = useComboboxFieldError(
146
+ error,
147
+ invalid || ariaInvalid === true || ariaInvalid === "true",
148
+ // The caller's, then the standing advice, then (inside) the error — the order every
149
+ // field keeps — and the lock's reason last, while there is one.
150
+ mergeDescribedBy(ariaDescribedBy, textHint && hintId)
151
+ );
152
+ const code = normalizeCountryCode(value);
153
+ const selectedName = code ? countryName(code, locale) : void 0;
154
+ const emptyText = placeholder ?? (label === void 0 && !ariaLabelledBy ? text.country : "");
155
+ const fieldName = typeof label === "string" ? label : text.country;
156
+ const valueId = `${generated}-value`;
157
+ const labelledBy = ariaLabel === void 0 && ariaLabelledBy ? mergeDescribedBy(ariaLabelledBy, (selectedName !== void 0 || Boolean(placeholder)) && valueId) : void 0;
158
+ const name = labelledBy !== void 0 ? void 0 : ariaLabel ?? (selectedName !== void 0 ? common.fieldValue(fieldName, selectedName) : placeholder ? common.fieldValue(fieldName, placeholder) : fieldName);
159
+ const showClear = Boolean(clearable && code && !inert);
160
+ const emit = onChange;
161
+ const clear = () => {
162
+ if (!showClear) return;
163
+ emit(null);
164
+ };
165
+ const choose = (o) => {
166
+ if (inert) return;
167
+ emit(o.value);
168
+ closeToTrigger();
169
+ };
170
+ const trigger = /* @__PURE__ */ jsxs(
171
+ "button",
172
+ {
173
+ ref: setTrigger,
174
+ id: triggerId,
175
+ type: "button",
176
+ role: "combobox",
177
+ "aria-haspopup": "listbox",
178
+ "aria-controls": listboxId,
179
+ "aria-expanded": open,
180
+ "aria-label": name,
181
+ "aria-labelledby": labelledBy,
182
+ "aria-invalid": field.isInvalid || void 0,
183
+ "aria-required": ariaRequired,
184
+ "aria-describedby": mergeDescribedBy(field.describedBy, locked && reasonId),
185
+ disabled: locked ? void 0 : disabled,
186
+ "aria-disabled": locked || void 0,
187
+ onClick: (e) => {
188
+ if (inert) {
189
+ e.preventDefault();
190
+ return;
191
+ }
192
+ setOpen((o) => !o);
193
+ },
194
+ onKeyDown: (e) => {
195
+ if (inert) return;
196
+ if (e.key === "ArrowDown" || e.key === "ArrowUp") {
197
+ e.preventDefault();
198
+ setOpen(true);
199
+ } else if (showClear && !open && (e.key === "Delete" || e.key === "Backspace")) {
200
+ e.preventDefault();
201
+ clear();
202
+ }
203
+ },
204
+ className: cn(
205
+ FIELD_TRIGGER,
206
+ // `peer` dims the floating label with a natively disabled trigger, as Select's.
207
+ "peer pe-9",
208
+ label !== void 0 && FIELD_FLOATING_PAD,
209
+ inert && "cursor-not-allowed opacity-50",
210
+ field.isInvalid && FIELD_INVALID
211
+ ),
212
+ children: [
213
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 items-center gap-2", children: [
214
+ flags && code && /* @__PURE__ */ jsx(CurrencyFlag, { country: code.toLowerCase() }),
215
+ /* @__PURE__ */ jsx(
216
+ "span",
217
+ {
218
+ id: valueId,
219
+ className: cn("truncate", selectedName === void 0 && "text-[var(--text-placeholder)]"),
220
+ children: selectedName ?? emptyText
221
+ }
222
+ )
223
+ ] }),
224
+ showClear ? (
225
+ /* eslint-disable-next-line jsx-a11y/click-events-have-key-events -- tabIndex -1 inside
226
+ the trigger <button>, so it never holds focus and a key handler here could never
227
+ fire; the keys go to the trigger, where Delete and Backspace clear. A pointer
228
+ shortcut, as on EntityCombobox. */
229
+ /* @__PURE__ */ jsx(
230
+ "span",
231
+ {
232
+ role: "button",
233
+ tabIndex: -1,
234
+ "aria-label": comboText.clear,
235
+ onClick: (e) => {
236
+ e.stopPropagation();
237
+ clear();
238
+ },
239
+ className: "absolute end-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-[var(--text-placeholder)] hover:text-[var(--text-secondary)]",
240
+ children: /* @__PURE__ */ jsx(X, { "aria-hidden": true, className: "size-4" })
241
+ }
242
+ )
243
+ ) : /* @__PURE__ */ jsx(FieldChevron, {})
244
+ ]
245
+ }
246
+ );
247
+ const control = locked ? (
248
+ // In a FRAGMENT, as Button does, so the Tooltip does not clone its bubble into the
249
+ // description as well — the hidden copy is what `aria-describedby` points at.
250
+ /* @__PURE__ */ jsx(Tooltip, { label: reason, className: "flex w-full", children: /* @__PURE__ */ jsxs(Fragment, { children: [
251
+ trigger,
252
+ /* @__PURE__ */ jsx("span", { id: reasonId, hidden: true, children: reason })
253
+ ] }) })
254
+ ) : trigger;
255
+ const labelHint = textHint ? void 0 : hasMessage(hint) ? hint : void 0;
256
+ const box = label !== void 0 ? /* @__PURE__ */ jsx(FloatingField, { htmlFor: triggerId, label, staticLabel: true, hint: labelHint, children: control }) : labelHint !== void 0 ? (
257
+ // No label line to ride: the "?" goes at the field's end, outside the box, as on
258
+ // an unlabelled Select.
259
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
260
+ /* @__PURE__ */ jsx("div", { className: "relative min-w-0 flex-1", children: control }),
261
+ /* @__PURE__ */ jsx("span", { className: "flex shrink-0 items-center", children: labelHint })
262
+ ] })
263
+ ) : /* @__PURE__ */ jsx("div", { className: "relative", children: control });
264
+ return (
265
+ // `rest` dresses the wrapper — a `data-tour` anchor, a test id. The name, the
266
+ // description and the invalid state are on the trigger above.
267
+ /* @__PURE__ */ jsxs("div", { ...rest, className, children: [
268
+ box,
269
+ textHint && /* @__PURE__ */ jsx("p", { id: hintId, className: "mt-1 text-[11px] leading-tight text-[var(--text-muted)]", children: hint }),
270
+ field.errorEl,
271
+ /* @__PURE__ */ jsx(
272
+ ComboboxPanel,
273
+ {
274
+ core: { ...core, results },
275
+ listboxId,
276
+ sheetTitle: label ?? text.country,
277
+ searchPlaceholder: text.search,
278
+ isSelected: (v) => v === code,
279
+ onChoose: choose,
280
+ showCreate: false,
281
+ onCreate: () => {
282
+ },
283
+ createContent: null
284
+ }
285
+ )
286
+ ] })
287
+ );
288
+ }
289
+ export {
290
+ CountrySelect,
291
+ DEFAULT_COUNTRY_SELECT_LABELS
292
+ };
293
+ //# sourceMappingURL=country-select.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/country-select.tsx"],"sourcesContent":["import { useCallback, useId, useMemo } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode, Ref } from \"react\";\nimport { X } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { COUNTRY_CODES, countryName, normalizeCountryCode } from \"../lib/countries\";\nimport { FieldChevron, FIELD_FLOATING_PAD, FIELD_INVALID, FIELD_TRIGGER, FloatingField } from \"./ui\";\nimport { Tooltip } from \"./tooltip\";\nimport { CurrencyFlag } from \"./currency-select\";\nimport {\n ComboboxPanel,\n useComboboxCore,\n useComboboxFieldError,\n type ComboOption,\n} from \"./combobox-core\";\nimport { assignRef, hasMessage, mergeDescribedBy } from \"./choice-parts\";\nimport { useCommitReason } from \"./write-lock\";\nimport {\n DEFAULT_COMBOBOX_LABELS,\n DEFAULT_COMMON_LABELS,\n useKitLabels,\n useKitLocale,\n} from \"../i18n/kit-labels\";\n\n// ── Labels ────────────────────────────────────────────────────────────────────\n\n/** The words {@link CountrySelect} renders on its own behalf. The list's \"no results\",\n * \"loading\" and result-count announcements are the shared `combobox` namespace's,\n * as on every other searchable picker. */\nexport interface CountrySelectLabels {\n /** What the field is, when the caller gives no `label`: the first half of the\n * trigger's accessible name, the phone sheet's title, and the empty trigger's\n * text. */\n country: string;\n /** The search box in the open list. */\n search: string;\n /** The heading between the `preferred` countries and all the others. Shown only\n * when `preferred` puts something above it. */\n others: string;\n}\n\nexport const DEFAULT_COUNTRY_SELECT_LABELS: CountrySelectLabels = {\n country: \"Country\",\n search: \"Search country\",\n others: \"Other countries\",\n};\n\n// ── Props ─────────────────────────────────────────────────────────────────────\n\n/**\n * `onChange` is the kit's — \"a country was picked\", carrying its code — rather than\n * the div's form event. `id` and the `aria-*` wiring below go to the TRIGGER, which is\n * the control a reader meets; everything else in `rest` dresses the wrapper.\n *\n * `Clearable` follows `clearable`, the way {@link EntityCombobox}'s `C` follows its\n * `clearValue`: it is inferred from the prop, so `onChange` is `(code: string) => void`\n * for the field as it always was (`onChange={setCountry}` over a `string` state still\n * type-checks), and `(code: string | null) => void` once `clearable` is set — the\n * `null` of a clear cannot reach a handler that was never told about it.\n */\nexport interface CountrySelectProps<Clearable extends boolean = false>\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\" | \"defaultValue\"> {\n /** The chosen country as an ISO 3166-1 alpha-2 code (\"CH\"), in either case;\n * `null`, `undefined` or `\"\"` when none is. A code outside the list still shows\n * its name — the list restricts what can be PICKED, not what can be shown. */\n value: string | null | undefined;\n /** A country was picked: its alpha-2 code, always upper-case. With `clearable`,\n * also `null`: the \"×\" (or Delete on the trigger) emptied the field. */\n onChange: (code: Clearable extends true ? string | null : string) => void;\n /**\n * Offer a clear \"×\" in place of the chevron while a country is chosen; it hands\n * `onChange` `null` (the kit's later list: an optional country — a contact's\n * nationality, a bank filter's \"any country\" — could be set but never unset again).\n *\n * The combobox family's clear, as {@link EntityCombobox} draws it: a pointer\n * shortcut inside the trigger, out of the tab order so it is not a second stop\n * between this field and the next, named by `clearLabel` (default: the\n * `combobox.clear` label every picker in the family shares). The keyboard way is\n * Delete or Backspace on the closed trigger. Not offered while `disabled` or locked\n * (`commit` / `disabledReason`): a clear is a change like any other.\n */\n clearable?: Clearable;\n /** The clear \"×\"'s accessible name. Wins over `combobox.clear` from the\n * {@link UiKitProvider}, as {@link EntityCombobox}'s `clearLabel` does. */\n clearLabel?: string;\n /**\n * The TRIGGER — the focusable `<button>` a reader meets — as a React 19 ref prop,\n * the way {@link MoneyField} takes its input's (kastlan: react-hook-form's\n * focus-on-error calls `focus()` on whatever `field.ref` was handed, and without a\n * ref the first field in error on an address form was never focused). See\n * `RhfCountrySelect`, which passes it.\n */\n ref?: Ref<HTMLButtonElement>;\n /**\n * Offer only these codes (any case; duplicates and malformed entries dropped).\n * Default: every ISO 3166-1 country, {@link COUNTRY_CODES}. Sorted by name either\n * way — the order given is not kept, because the reader looks a country up by its\n * name in their own language, and any other order is one they cannot predict.\n *\n * keksdose's bank picker offers the fifteen countries its banks are in\n * (DE AT CH FR IT ES NL BE PT PL SE DK FI GB IE). A code that is not in ISO's list\n * is allowed here on purpose: `XK` (Kosovo) is the app's decision, not the kit's.\n */\n countries?: readonly string[];\n /**\n * A short group shown FIRST, in the order given, above the alphabetical rest —\n * kastlan's CH, LI, DE, AT, FR, IT, the six countries most of its customers live\n * in, out of a list of 249. Each code appears once: a preferred country is not\n * repeated in the rest. The rest gets a heading (`labels.others`) so the boundary\n * is visible. Search keeps the split: matching preferred countries stay on top.\n * Codes not in `countries` are ignored — `countries` decides what can be picked.\n */\n preferred?: readonly string[];\n /**\n * Draw each country's flag as a flag-icons span (`fi fi-ch`), in the rows and on\n * the trigger. Default `false`, because the kit ships no flag stylesheet — the same\n * convention as `LanguageMenu`'s `country` and `CurrencySelect`: the host imports\n * `flag-icons/css/flag-icons.min.css`, and without it a flag is an empty box.\n */\n flags?: boolean;\n /** The floating label, as on every field. A string also names the trigger\n * (\"Country: Switzerland\"). */\n label?: ReactNode;\n /** As {@link Select}'s `hint`: plain TEXT is a caption under the field, attached\n * through `aria-describedby`; a {@link FieldHint} rides the label line (or the\n * field's end, when there is no label). */\n hint?: ReactNode;\n /** As {@link Input}'s `error`: rendered under the field, on the trigger's\n * `aria-describedby`, and implies `invalid`. */\n error?: ReactNode;\n /** Required and unanswered — {@link FIELD_INVALID}, as on every field. */\n invalid?: boolean;\n /** The empty trigger's text. Default: nothing when the field has a `label` or an\n * `aria-labelledby` (the label already says what goes here), else `labels.country`. */\n placeholder?: string;\n /** As on the other pickers: no focus, no list, the settled look. */\n disabled?: boolean;\n /**\n * Why the country cannot be changed — {@link Button}'s `disabledReason`, for a field\n * that saves itself. The trigger stays focusable and `aria-disabled`, the list does\n * not open and `onChange` never fires; the reason is in the kit {@link Tooltip} and\n * on the trigger's `aria-describedby`. Wins over `disabled`.\n */\n disabledReason?: ReactNode;\n /**\n * This select SAVES on change (a settings row with no Save button). Under a locked\n * {@link WriteLockProvider} it is locked the `disabledReason` way, with the lock's\n * reason. A country field inside a form that has its own Save stays editable under\n * the lock — leave this off there, and put `commit` on the Save.\n */\n commit?: boolean;\n /** The language the names are in. Default: the `<UiKitProvider locale>`, else\n * English (see {@link countryName}). */\n locale?: string;\n /** Per-instance overrides of {@link CountrySelectLabels}, over the provider's\n * `countrySelect` namespace, over English. */\n labels?: Partial<CountrySelectLabels>;\n /**\n * Name the trigger by an element of the caller's — a form's own label above the\n * field, which is how `RhfCountrySelect` names it. The trigger is then named by\n * that element AND the chosen country (two references, as {@link DatePicker}'s\n * trigger is named, so \"Country Switzerland\" needs no punctuation to translate),\n * instead of the composed \"label: country\". An `aria-label` still wins. Before\n * 0.23 this landed on the wrapper `<div>`, which has no role to be named.\n */\n \"aria-labelledby\"?: string;\n}\n\n/** Fold a name for search: no case, no accents — \"osterreich\" finds Österreich, \"cote\"\n * finds Côte d'Ivoire. A keyboard without the letter must still reach the country. */\nfunction fold(s: string): string {\n return s.normalize(\"NFD\").replace(/\\p{M}/gu, \"\").toLowerCase();\n}\n\ninterface Entry {\n code: string;\n name: string;\n folded: string;\n}\n\n/** How well `e` answers `q` (already folded): the code itself (a two-letter query\n * \"ch\" means Switzerland before it means Chad), then the start of the name, then\n * anywhere in it. `-1` for no match. */\nfunction rank(e: Entry, q: string): number {\n if (e.code.toLowerCase() === q) return 0;\n if (e.folded.startsWith(q)) return 1;\n return e.folded.includes(q) ? 2 : -1;\n}\n\n/** Best rank first; a stable sort, so equal ranks keep the collated (or preferred)\n * order they came in. */\nfunction search(entries: Entry[], q: string): Entry[] {\n return entries\n .map((e) => ({ e, r: rank(e, q) }))\n .filter((x) => x.r >= 0)\n .sort((a, b) => a.r - b.r)\n .map((x) => x.e);\n}\n\nfunction toOption(e: Entry, flags: boolean, group?: string): ComboOption<string> {\n return {\n value: e.code,\n label: e.name,\n icon: flags ? <CurrencyFlag country={e.code.toLowerCase()} /> : undefined,\n group,\n };\n}\n\nfunction collator(locale: string): Intl.Collator {\n try {\n return new Intl.Collator(locale);\n } catch {\n return new Intl.Collator();\n }\n}\n\n/**\n * A country picker: the value is the ISO 3166-1 alpha-2 code (\"CH\"), the names are the\n * runtime's own (`Intl.DisplayNames`) in the provider's locale, sorted by that\n * locale's collation. Requested by kastlan (customer and handover addresses, with its\n * six neighbours on top) and keksdose (the bank picker's fifteen countries) — both\n * apps carried a hand-written list of names in two languages.\n *\n * ## Built on the kit's searchable combobox\n *\n * 249 countries is not a menu, it is a lookup, and the list has to work on a phone. So\n * this is the entity pickers' machinery — `useComboboxCore` and `ComboboxPanel`, which\n * {@link EntityCombobox} is also made of — not a native `<select>`: the anchored panel\n * with a search box on a pointer device, the full-screen sheet with its own search box\n * on a phone (live #200), the same keyboard (APG listbox), the same announcements, the\n * same group headings. Only the trigger is this component's own, because the trigger\n * is where the write lock lives: `EntityCombobox`'s is a native `disabled` button,\n * and a locked field has to stay focusable to say why.\n *\n * The search is this component's own too: it folds accents and case (\"osterreich\"\n * finds Österreich), matches the code (\"ch\" ranks Switzerland first), and ranks\n * name-starts above name-contains. The panel is handed those rows in place of the\n * core's plain substring filter.\n *\n * Field anatomy as on every field: floating `label`, `hint`, `error`, `invalid`; the\n * trigger is named \"label: value\" as the other trigger pickers are.\n *\n * Since 0.23 it takes a `ref` to the trigger (react-hook-form's focus-on-error, kastlan)\n * and `clearable`, which hands `onChange` `null`.\n */\nexport function CountrySelect<Clearable extends boolean = false>({\n value,\n onChange,\n countries,\n preferred,\n flags = false,\n label,\n hint,\n error,\n invalid,\n placeholder,\n disabled,\n disabledReason: ownReason,\n commit,\n clearable,\n clearLabel,\n locale: localeProp,\n labels: labelsProp,\n className,\n id,\n ref,\n \"aria-label\": ariaLabel,\n \"aria-labelledby\": ariaLabelledBy,\n \"aria-describedby\": ariaDescribedBy,\n \"aria-invalid\": ariaInvalid,\n \"aria-required\": ariaRequired,\n ...rest\n}: CountrySelectProps<Clearable>) {\n const text = useKitLabels(\"countrySelect\", DEFAULT_COUNTRY_SELECT_LABELS, labelsProp);\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n // The clear's name is the family's (`combobox.clear`), as on EntityCombobox; a prop\n // left `undefined` falls through to the provider rather than masking it.\n const comboText = useKitLabels(\"combobox\", DEFAULT_COMBOBOX_LABELS, { clear: clearLabel });\n // English without a locale — see `countryName`.\n const locale = useKitLocale(localeProp) ?? \"en\";\n\n // ── The list ──────────────────────────────────────────────────────────────\n // Keyed on the arrays' CONTENTS: `countries={[\"DE\", \"AT\"]}` written inline is a new\n // array every render, and resorting 249 names on every keystroke elsewhere in the\n // form is work for nothing.\n const countriesKey = countries?.join(\",\");\n const preferredKey = preferred?.join(\",\");\n const { top, rest: others } = useMemo(() => {\n const seen = new Set<string>();\n const pool: string[] = [];\n for (const raw of countriesKey !== undefined ? countriesKey.split(\",\") : COUNTRY_CODES) {\n const code = normalizeCountryCode(raw);\n if (code && !seen.has(code)) {\n seen.add(code);\n pool.push(code);\n }\n }\n const entry = (code: string): Entry => {\n const name = countryName(code, locale);\n return { code, name, folded: fold(name) };\n };\n const firsts: Entry[] = [];\n const picked = new Set<string>();\n for (const raw of preferredKey ? preferredKey.split(\",\") : []) {\n const code = normalizeCountryCode(raw);\n if (code && seen.has(code) && !picked.has(code)) {\n picked.add(code);\n firsts.push(entry(code));\n }\n }\n const compare = collator(locale).compare;\n const sorted = pool\n .filter((c) => !picked.has(c))\n .map(entry)\n .sort((a, b) => compare(a.name, b.name));\n return { top: firsts, rest: sorted };\n }, [countriesKey, preferredKey, locale]);\n\n // `filter: false` — the panel is handed the rows `search` ranks below instead of\n // the core's substring filter; the core still owns open, query, the highlighted row,\n // anchoring and dismissal.\n const allOptions = useMemo(\n () => [...top, ...others].map((e) => toOption(e, flags)),\n [top, others, flags],\n );\n const core = useComboboxCore<string>({ options: allOptions, filter: false });\n const { open, setOpen, query, triggerRef, closeToTrigger } = core;\n // The core anchors the panel to the trigger and hands focus back to it; the caller's\n // `ref` wants the same element. One callback feeds both.\n const setTrigger = useCallback(\n (el: HTMLButtonElement | null) => {\n triggerRef.current = el;\n assignRef(ref, el);\n },\n [ref, triggerRef],\n );\n\n const q = fold(query.trim());\n const results = useMemo(() => {\n const firsts = q ? search(top, q) : top;\n const below = q ? search(others, q) : others;\n // The heading only where it separates something: with no preferred country in\n // view it would head the whole list and divide nothing.\n const heading = firsts.length > 0 ? text.others : undefined;\n return [...firsts.map((e) => toOption(e, flags)), ...below.map((e) => toOption(e, flags, heading))];\n }, [top, others, q, text.others, flags]);\n\n // ── The lock ──────────────────────────────────────────────────────────────\n const reason = useCommitReason(commit, ownReason);\n const locked = hasMessage(reason);\n const inert = locked || Boolean(disabled);\n // A lock (or `disabled`) arriving while the list is up closes it — adjusted while\n // rendering, as the core adjusts its own query, so no frame shows a list that can\n // no longer be used.\n if (inert && open) setOpen(false);\n\n // ── The field ─────────────────────────────────────────────────────────────\n const generated = useId();\n const triggerId = id ?? generated;\n const listboxId = `${generated}-listbox`;\n const hintId = useId();\n const reasonId = useId();\n // Text is a caption under the field; a FieldHint rides the label line. See `hint`.\n const textHint = (typeof hint === \"string\" && hint !== \"\") || typeof hint === \"number\";\n const field = useComboboxFieldError(\n error,\n invalid || ariaInvalid === true || ariaInvalid === \"true\",\n // The caller's, then the standing advice, then (inside) the error — the order every\n // field keeps — and the lock's reason last, while there is one.\n mergeDescribedBy(ariaDescribedBy, textHint && hintId),\n );\n\n const code = normalizeCountryCode(value);\n const selectedName = code ? countryName(code, locale) : undefined;\n // A field named by a label of the caller's (`aria-labelledby`) is a labelled field too:\n // the label already says what goes here.\n const emptyText = placeholder ?? (label === undefined && !ariaLabelledBy ? text.country : \"\");\n const fieldName = typeof label === \"string\" ? label : text.country;\n const valueId = `${generated}-value`;\n // Named by the caller's element (a form's label) — then the shown text is the second\n // reference, as the composed name below has the value second; but only when it IS a\n // value or the caller's placeholder, never the `country` fallback word.\n const labelledBy =\n ariaLabel === undefined && ariaLabelledBy\n ? mergeDescribedBy(ariaLabelledBy, (selectedName !== undefined || Boolean(placeholder)) && valueId)\n : undefined;\n const name =\n labelledBy !== undefined\n ? undefined\n : (ariaLabel ??\n (selectedName !== undefined\n ? common.fieldValue(fieldName, selectedName)\n : placeholder\n ? common.fieldValue(fieldName, placeholder)\n : fieldName));\n\n const showClear = Boolean(clearable && code && !inert);\n // `onChange` widened to take the clear's `null` — its type says so once `clearable`\n // is set, and the clear is only ever offered then.\n const emit = onChange as (code: string | null) => void;\n const clear = () => {\n if (!showClear) return;\n emit(null);\n };\n\n const choose = (o: ComboOption<string>) => {\n if (inert) return;\n emit(o.value);\n // Back to the trigger: the panel holding focus is about to unmount, and a keyboard\n // user who has answered this field should stand on it, ready to Tab on.\n closeToTrigger();\n };\n\n const trigger = (\n <button\n ref={setTrigger}\n id={triggerId}\n type=\"button\"\n // A closed choice that expands into the list below — the role every trigger\n // picker in the kit wears (see EntityCombobox), and the one that supports\n // `aria-invalid`.\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n aria-controls={listboxId}\n aria-expanded={open}\n aria-label={name}\n aria-labelledby={labelledBy}\n aria-invalid={field.isInvalid || undefined}\n aria-required={ariaRequired}\n aria-describedby={mergeDescribedBy(field.describedBy, locked && reasonId)}\n // The focusable kind of disabled while locked (Button's rule): native `disabled`\n // would take the field out of the tab order, and the reason with it.\n disabled={locked ? undefined : disabled}\n aria-disabled={locked || undefined}\n onClick={(e) => {\n if (inert) {\n e.preventDefault();\n return;\n }\n setOpen((o) => !o);\n }}\n // Down/Up opens the list from the closed trigger, per the APG; Enter and Space\n // already do through the button's own click.\n onKeyDown={(e) => {\n if (inert) return;\n if (e.key === \"ArrowDown\" || e.key === \"ArrowUp\") {\n e.preventDefault();\n setOpen(true);\n } else if (showClear && !open && (e.key === \"Delete\" || e.key === \"Backspace\")) {\n // The keyboard's clear: the \"×\" is out of the tab order (see `clearable`).\n e.preventDefault();\n clear();\n }\n }}\n className={cn(\n FIELD_TRIGGER,\n // `peer` dims the floating label with a natively disabled trigger, as Select's.\n \"peer pe-9\",\n label !== undefined && FIELD_FLOATING_PAD,\n inert && \"cursor-not-allowed opacity-50\",\n field.isInvalid && FIELD_INVALID,\n )}\n >\n <span className=\"flex min-w-0 items-center gap-2\">\n {flags && code && <CurrencyFlag country={code.toLowerCase()} />}\n {/* A long name (\"Saint Vincent and the Grenadines\") truncates rather than\n widening the field: the kit is checked at 390px. */}\n <span\n id={valueId}\n className={cn(\"truncate\", selectedName === undefined && \"text-[var(--text-placeholder)]\")}\n >\n {selectedName ?? emptyText}\n </span>\n </span>\n {showClear ? (\n /* eslint-disable-next-line jsx-a11y/click-events-have-key-events -- tabIndex -1 inside\n the trigger <button>, so it never holds focus and a key handler here could never\n fire; the keys go to the trigger, where Delete and Backspace clear. A pointer\n shortcut, as on EntityCombobox. */\n <span\n role=\"button\"\n tabIndex={-1}\n aria-label={comboText.clear}\n onClick={(e) => {\n e.stopPropagation();\n clear();\n }}\n className=\"absolute end-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-[var(--text-placeholder)] hover:text-[var(--text-secondary)]\"\n >\n <X aria-hidden className=\"size-4\" />\n </span>\n ) : (\n <FieldChevron />\n )}\n </button>\n );\n\n const control = locked ? (\n // In a FRAGMENT, as Button does, so the Tooltip does not clone its bubble into the\n // description as well — the hidden copy is what `aria-describedby` points at.\n <Tooltip label={reason} className=\"flex w-full\">\n <>\n {trigger}\n <span id={reasonId} hidden>\n {reason}\n </span>\n </>\n </Tooltip>\n ) : (\n trigger\n );\n\n const labelHint = textHint ? undefined : hasMessage(hint) ? hint : undefined;\n const box =\n label !== undefined ? (\n <FloatingField htmlFor={triggerId} label={label} staticLabel hint={labelHint}>\n {control}\n </FloatingField>\n ) : labelHint !== undefined ? (\n // No label line to ride: the \"?\" goes at the field's end, outside the box, as on\n // an unlabelled Select.\n <div className=\"flex items-center gap-1.5\">\n <div className=\"relative min-w-0 flex-1\">{control}</div>\n <span className=\"flex shrink-0 items-center\">{labelHint}</span>\n </div>\n ) : (\n <div className=\"relative\">{control}</div>\n );\n\n return (\n // `rest` dresses the wrapper — a `data-tour` anchor, a test id. The name, the\n // description and the invalid state are on the trigger above.\n <div {...rest} className={className}>\n {box}\n {textHint && (\n <p id={hintId} className=\"mt-1 text-[11px] leading-tight text-[var(--text-muted)]\">\n {hint}\n </p>\n )}\n {field.errorEl}\n <ComboboxPanel\n // The core with the rows ranked above in place of its own filter's.\n core={{ ...core, results }}\n listboxId={listboxId}\n // On a phone the list is a full-screen sheet, which needs the field's own\n // name to say what it is asking for (live #200).\n sheetTitle={label ?? text.country}\n searchPlaceholder={text.search}\n isSelected={(v) => v === code}\n onChoose={choose}\n showCreate={false}\n onCreate={() => {}}\n createContent={null}\n />\n </div>\n );\n}\n"],"mappings":";AA0MkB,SA0SZ,UA1SY,KAoQZ,YApQY;AA1MlB,SAAS,aAAa,OAAO,eAAe;AAE5C,SAAS,SAAS;AAClB,SAAS,UAAU;AACnB,SAAS,eAAe,aAAa,4BAA4B;AACjE,SAAS,cAAc,oBAAoB,eAAe,eAAe,qBAAqB;AAC9F,SAAS,eAAe;AACxB,SAAS,oBAAoB;AAC7B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP,SAAS,WAAW,YAAY,wBAAwB;AACxD,SAAS,uBAAuB;AAChC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAmBA,MAAM,gCAAqD;AAAA,EAChE,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,QAAQ;AACV;AA6HA,SAAS,KAAK,GAAmB;AAC/B,SAAO,EAAE,UAAU,KAAK,EAAE,QAAQ,WAAW,EAAE,EAAE,YAAY;AAC/D;AAWA,SAAS,KAAK,GAAU,GAAmB;AACzC,MAAI,EAAE,KAAK,YAAY,MAAM,EAAG,QAAO;AACvC,MAAI,EAAE,OAAO,WAAW,CAAC,EAAG,QAAO;AACnC,SAAO,EAAE,OAAO,SAAS,CAAC,IAAI,IAAI;AACpC;AAIA,SAAS,OAAO,SAAkB,GAAoB;AACpD,SAAO,QACJ,IAAI,CAAC,OAAO,EAAE,GAAG,GAAG,KAAK,GAAG,CAAC,EAAE,EAAE,EACjC,OAAO,CAAC,MAAM,EAAE,KAAK,CAAC,EACtB,KAAK,CAAC,GAAG,MAAM,EAAE,IAAI,EAAE,CAAC,EACxB,IAAI,CAAC,MAAM,EAAE,CAAC;AACnB;AAEA,SAAS,SAAS,GAAU,OAAgB,OAAqC;AAC/E,SAAO;AAAA,IACL,OAAO,EAAE;AAAA,IACT,OAAO,EAAE;AAAA,IACT,MAAM,QAAQ,oBAAC,gBAAa,SAAS,EAAE,KAAK,YAAY,GAAG,IAAK;AAAA,IAChE;AAAA,EACF;AACF;AAEA,SAAS,SAAS,QAA+B;AAC/C,MAAI;AACF,WAAO,IAAI,KAAK,SAAS,MAAM;AAAA,EACjC,QAAQ;AACN,WAAO,IAAI,KAAK,SAAS;AAAA,EAC3B;AACF;AA+BO,SAAS,cAAiD;AAAA,EAC/D;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,gBAAgB;AAAA,EAChB;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,mBAAmB;AAAA,EACnB,oBAAoB;AAAA,EACpB,gBAAgB;AAAA,EAChB,iBAAiB;AAAA,EACjB,GAAG;AACL,GAAkC;AAChC,QAAM,OAAO,aAAa,iBAAiB,+BAA+B,UAAU;AACpF,QAAM,SAAS,aAAa,UAAU,qBAAqB;AAG3D,QAAM,YAAY,aAAa,YAAY,yBAAyB,EAAE,OAAO,WAAW,CAAC;AAEzF,QAAM,SAAS,aAAa,UAAU,KAAK;AAM3C,QAAM,eAAe,WAAW,KAAK,GAAG;AACxC,QAAM,eAAe,WAAW,KAAK,GAAG;AACxC,QAAM,EAAE,KAAK,MAAM,OAAO,IAAI,QAAQ,MAAM;AAC1C,UAAM,OAAO,oBAAI,IAAY;AAC7B,UAAM,OAAiB,CAAC;AACxB,eAAW,OAAO,iBAAiB,SAAY,aAAa,MAAM,GAAG,IAAI,eAAe;AACtF,YAAMA,QAAO,qBAAqB,GAAG;AACrC,UAAIA,SAAQ,CAAC,KAAK,IAAIA,KAAI,GAAG;AAC3B,aAAK,IAAIA,KAAI;AACb,aAAK,KAAKA,KAAI;AAAA,MAChB;AAAA,IACF;AACA,UAAM,QAAQ,CAACA,UAAwB;AACrC,YAAMC,QAAO,YAAYD,OAAM,MAAM;AACrC,aAAO,EAAE,MAAAA,OAAM,MAAAC,OAAM,QAAQ,KAAKA,KAAI,EAAE;AAAA,IAC1C;AACA,UAAM,SAAkB,CAAC;AACzB,UAAM,SAAS,oBAAI,IAAY;AAC/B,eAAW,OAAO,eAAe,aAAa,MAAM,GAAG,IAAI,CAAC,GAAG;AAC7D,YAAMD,QAAO,qBAAqB,GAAG;AACrC,UAAIA,SAAQ,KAAK,IAAIA,KAAI,KAAK,CAAC,OAAO,IAAIA,KAAI,GAAG;AAC/C,eAAO,IAAIA,KAAI;AACf,eAAO,KAAK,MAAMA,KAAI,CAAC;AAAA,MACzB;AAAA,IACF;AACA,UAAM,UAAU,SAAS,MAAM,EAAE;AACjC,UAAM,SAAS,KACZ,OAAO,CAAC,MAAM,CAAC,OAAO,IAAI,CAAC,CAAC,EAC5B,IAAI,KAAK,EACT,KAAK,CAAC,GAAG,MAAM,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC;AACzC,WAAO,EAAE,KAAK,QAAQ,MAAM,OAAO;AAAA,EACrC,GAAG,CAAC,cAAc,cAAc,MAAM,CAAC;AAKvC,QAAM,aAAa;AAAA,IACjB,MAAM,CAAC,GAAG,KAAK,GAAG,MAAM,EAAE,IAAI,CAAC,MAAM,SAAS,GAAG,KAAK,CAAC;AAAA,IACvD,CAAC,KAAK,QAAQ,KAAK;AAAA,EACrB;AACA,QAAM,OAAO,gBAAwB,EAAE,SAAS,YAAY,QAAQ,MAAM,CAAC;AAC3E,QAAM,EAAE,MAAM,SAAS,OAAO,YAAY,eAAe,IAAI;AAG7D,QAAM,aAAa;AAAA,IACjB,CAAC,OAAiC;AAChC,iBAAW,UAAU;AACrB,gBAAU,KAAK,EAAE;AAAA,IACnB;AAAA,IACA,CAAC,KAAK,UAAU;AAAA,EAClB;AAEA,QAAM,IAAI,KAAK,MAAM,KAAK,CAAC;AAC3B,QAAM,UAAU,QAAQ,MAAM;AAC5B,UAAM,SAAS,IAAI,OAAO,KAAK,CAAC,IAAI;AACpC,UAAM,QAAQ,IAAI,OAAO,QAAQ,CAAC,IAAI;AAGtC,UAAM,UAAU,OAAO,SAAS,IAAI,KAAK,SAAS;AAClD,WAAO,CAAC,GAAG,OAAO,IAAI,CAAC,MAAM,SAAS,GAAG,KAAK,CAAC,GAAG,GAAG,MAAM,IAAI,CAAC,MAAM,SAAS,GAAG,OAAO,OAAO,CAAC,CAAC;AAAA,EACpG,GAAG,CAAC,KAAK,QAAQ,GAAG,KAAK,QAAQ,KAAK,CAAC;AAGvC,QAAM,SAAS,gBAAgB,QAAQ,SAAS;AAChD,QAAM,SAAS,WAAW,MAAM;AAChC,QAAM,QAAQ,UAAU,QAAQ,QAAQ;AAIxC,MAAI,SAAS,KAAM,SAAQ,KAAK;AAGhC,QAAM,YAAY,MAAM;AACxB,QAAM,YAAY,MAAM;AACxB,QAAM,YAAY,GAAG,SAAS;AAC9B,QAAM,SAAS,MAAM;AACrB,QAAM,WAAW,MAAM;AAEvB,QAAM,WAAY,OAAO,SAAS,YAAY,SAAS,MAAO,OAAO,SAAS;AAC9E,QAAM,QAAQ;AAAA,IACZ;AAAA,IACA,WAAW,gBAAgB,QAAQ,gBAAgB;AAAA;AAAA;AAAA,IAGnD,iBAAiB,iBAAiB,YAAY,MAAM;AAAA,EACtD;AAEA,QAAM,OAAO,qBAAqB,KAAK;AACvC,QAAM,eAAe,OAAO,YAAY,MAAM,MAAM,IAAI;AAGxD,QAAM,YAAY,gBAAgB,UAAU,UAAa,CAAC,iBAAiB,KAAK,UAAU;AAC1F,QAAM,YAAY,OAAO,UAAU,WAAW,QAAQ,KAAK;AAC3D,QAAM,UAAU,GAAG,SAAS;AAI5B,QAAM,aACJ,cAAc,UAAa,iBACvB,iBAAiB,iBAAiB,iBAAiB,UAAa,QAAQ,WAAW,MAAM,OAAO,IAChG;AACN,QAAM,OACJ,eAAe,SACX,SACC,cACA,iBAAiB,SACd,OAAO,WAAW,WAAW,YAAY,IACzC,cACE,OAAO,WAAW,WAAW,WAAW,IACxC;AAEZ,QAAM,YAAY,QAAQ,aAAa,QAAQ,CAAC,KAAK;AAGrD,QAAM,OAAO;AACb,QAAM,QAAQ,MAAM;AAClB,QAAI,CAAC,UAAW;AAChB,SAAK,IAAI;AAAA,EACX;AAEA,QAAM,SAAS,CAAC,MAA2B;AACzC,QAAI,MAAO;AACX,SAAK,EAAE,KAAK;AAGZ,mBAAe;AAAA,EACjB;AAEA,QAAM,UACJ;AAAA,IAAC;AAAA;AAAA,MACC,KAAK;AAAA,MACL,IAAI;AAAA,MACJ,MAAK;AAAA,MAIL,MAAK;AAAA,MACL,iBAAc;AAAA,MACd,iBAAe;AAAA,MACf,iBAAe;AAAA,MACf,cAAY;AAAA,MACZ,mBAAiB;AAAA,MACjB,gBAAc,MAAM,aAAa;AAAA,MACjC,iBAAe;AAAA,MACf,oBAAkB,iBAAiB,MAAM,aAAa,UAAU,QAAQ;AAAA,MAGxE,UAAU,SAAS,SAAY;AAAA,MAC/B,iBAAe,UAAU;AAAA,MACzB,SAAS,CAAC,MAAM;AACd,YAAI,OAAO;AACT,YAAE,eAAe;AACjB;AAAA,QACF;AACA,gBAAQ,CAAC,MAAM,CAAC,CAAC;AAAA,MACnB;AAAA,MAGA,WAAW,CAAC,MAAM;AAChB,YAAI,MAAO;AACX,YAAI,EAAE,QAAQ,eAAe,EAAE,QAAQ,WAAW;AAChD,YAAE,eAAe;AACjB,kBAAQ,IAAI;AAAA,QACd,WAAW,aAAa,CAAC,SAAS,EAAE,QAAQ,YAAY,EAAE,QAAQ,cAAc;AAE9E,YAAE,eAAe;AACjB,gBAAM;AAAA,QACR;AAAA,MACF;AAAA,MACA,WAAW;AAAA,QACT;AAAA;AAAA,QAEA;AAAA,QACA,UAAU,UAAa;AAAA,QACvB,SAAS;AAAA,QACT,MAAM,aAAa;AAAA,MACrB;AAAA,MAEA;AAAA,6BAAC,UAAK,WAAU,mCACb;AAAA,mBAAS,QAAQ,oBAAC,gBAAa,SAAS,KAAK,YAAY,GAAG;AAAA,UAG7D;AAAA,YAAC;AAAA;AAAA,cACC,IAAI;AAAA,cACJ,WAAW,GAAG,YAAY,iBAAiB,UAAa,gCAAgC;AAAA,cAEvF,0BAAgB;AAAA;AAAA,UACnB;AAAA,WACF;AAAA,QACC;AAAA;AAAA;AAAA;AAAA;AAAA,UAKC;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,UAAU;AAAA,cACV,cAAY,UAAU;AAAA,cACtB,SAAS,CAAC,MAAM;AACd,kBAAE,gBAAgB;AAClB,sBAAM;AAAA,cACR;AAAA,cACA,WAAU;AAAA,cAEV,8BAAC,KAAE,eAAW,MAAC,WAAU,UAAS;AAAA;AAAA,UACpC;AAAA,YAEA,oBAAC,gBAAa;AAAA;AAAA;AAAA,EAElB;AAGF,QAAM,UAAU;AAAA;AAAA;AAAA,IAGd,oBAAC,WAAQ,OAAO,QAAQ,WAAU,eAChC,2CACG;AAAA;AAAA,MACD,oBAAC,UAAK,IAAI,UAAU,QAAM,MACvB,kBACH;AAAA,OACF,GACF;AAAA,MAEA;AAGF,QAAM,YAAY,WAAW,SAAY,WAAW,IAAI,IAAI,OAAO;AACnE,QAAM,MACJ,UAAU,SACR,oBAAC,iBAAc,SAAS,WAAW,OAAc,aAAW,MAAC,MAAM,WAChE,mBACH,IACE,cAAc;AAAA;AAAA;AAAA,IAGhB,qBAAC,SAAI,WAAU,6BACb;AAAA,0BAAC,SAAI,WAAU,2BAA2B,mBAAQ;AAAA,MAClD,oBAAC,UAAK,WAAU,8BAA8B,qBAAU;AAAA,OAC1D;AAAA,MAEA,oBAAC,SAAI,WAAU,YAAY,mBAAQ;AAGvC;AAAA;AAAA;AAAA,IAGE,qBAAC,SAAK,GAAG,MAAM,WACZ;AAAA;AAAA,MACA,YACC,oBAAC,OAAE,IAAI,QAAQ,WAAU,2DACtB,gBACH;AAAA,MAED,MAAM;AAAA,MACP;AAAA,QAAC;AAAA;AAAA,UAEC,MAAM,EAAE,GAAG,MAAM,QAAQ;AAAA,UACzB;AAAA,UAGA,YAAY,SAAS,KAAK;AAAA,UAC1B,mBAAmB,KAAK;AAAA,UACxB,YAAY,CAAC,MAAM,MAAM;AAAA,UACzB,UAAU;AAAA,UACV,YAAY;AAAA,UACZ,UAAU,MAAM;AAAA,UAAC;AAAA,UACjB,eAAe;AAAA;AAAA,MACjB;AAAA,OACF;AAAA;AAEJ;","names":["code","name"]}
@@ -42,6 +42,22 @@ interface CurrencySelectProps extends Omit<ComponentPropsWithoutRef<"div">, "onC
42
42
  * this package carries it now, so a form can mark any of its fields rather than
43
43
  * only the one that happened to have it first. */
44
44
  invalid?: boolean;
45
+ /**
46
+ * What is wrong with the choice, in the caller's own words — {@link Input}'s `error`
47
+ * (keksdose K4: of the kit's pickers this one had neither a message nor a caption, so
48
+ * "the account is in EUR" sat in a loose `<p>` beside it that nothing pointed at).
49
+ * Rendered under the field, after a text `hint`; merged into the trigger's
50
+ * `aria-describedby`; implies `invalid`. `null`, `false` and `""` are no message.
51
+ */
52
+ error?: ReactNode;
53
+ /**
54
+ * {@link Select}'s `hint`, the same rule (keksdose K4). A {@link FieldHint} "?" rides
55
+ * the label's line; on an unlabelled picker it sits at the field's end edge, outside
56
+ * the box. Plain TEXT (a string or a number) is a caption UNDER the field, attached
57
+ * to the trigger through `aria-describedby` — the label line has no room for a
58
+ * sentence.
59
+ */
60
+ hint?: ReactNode;
45
61
  /** Every user-facing string this control owns, shaped like `AmountInput`'s
46
62
  * `labels` so the two currency pickers in the kit are configured the same way.
47
63
  *
@@ -72,6 +88,6 @@ interface CurrencySelectProps extends Omit<ComponentPropsWithoutRef<"div">, "onC
72
88
  */
73
89
  currencyNames?: Record<string, string>;
74
90
  }
75
- declare function CurrencySelect({ value, onChange, placeholder, className, label, options, invalid, labels, currencyNames, "aria-label": ariaLabel, ...rest }: CurrencySelectProps): react.JSX.Element;
91
+ declare function CurrencySelect({ value, onChange, placeholder, className, label, options, invalid, error, hint, labels, currencyNames, "aria-label": ariaLabel, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid, ...rest }: CurrencySelectProps): react.JSX.Element;
76
92
 
77
93
  export { CURRENCIES, CurrencyFlag, type CurrencyFlagProps, type CurrencyOption, CurrencySelect, type CurrencySelectProps, currencyName, getCurrency };
@@ -3,6 +3,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useMemo, useId } from "react";
4
4
  import { Check } from "lucide-react";
5
5
  import { FieldChevron, FieldLabel, FIELD_TRIGGER, FIELD_INVALID, FIELD_FLOATING_PAD } from "./ui.js";
6
+ import { hasMessage, mergeDescribedBy } from "./choice-parts.js";
6
7
  import { cn } from "../lib/cn.js";
7
8
  import { DropdownPanel, DropdownSearchHeader, useDropdownSearch } from "./dropdown.js";
8
9
  import {
@@ -74,9 +75,13 @@ function CurrencySelect({
74
75
  label,
75
76
  options,
76
77
  invalid,
78
+ error,
79
+ hint,
77
80
  labels,
78
81
  currencyNames,
79
82
  "aria-label": ariaLabel,
83
+ "aria-describedby": ariaDescribedBy,
84
+ "aria-invalid": ariaInvalid,
80
85
  ...rest
81
86
  }) {
82
87
  const { open, setOpen, wrapperRef, query, setQuery, inputRef } = useDropdownSearch();
@@ -96,35 +101,60 @@ function CurrencySelect({
96
101
  );
97
102
  }, [query, pool, currencyNames, locale]);
98
103
  const listboxId = `${useId()}-listbox`;
104
+ const hintId = useId();
105
+ const errorId = useId();
106
+ const textHint = typeof hint === "string" && hint !== "" || typeof hint === "number";
107
+ const labelHint = !textHint && hasMessage(hint) ? hint : void 0;
108
+ const hasError = hasMessage(error);
109
+ const describedBy = mergeDescribedBy(ariaDescribedBy, textHint && hintId, hasError && errorId);
110
+ const isInvalid = Boolean(invalid) || hasError || ariaInvalid === true || ariaInvalid === "true";
99
111
  const fieldName = typeof label === "string" ? label : text.currency;
100
112
  const name = ariaLabel ?? common.fieldValue(fieldName, selected ? selected.code : placeholder ?? text.currency);
113
+ const trigger = /* @__PURE__ */ jsxs(
114
+ "button",
115
+ {
116
+ type: "button",
117
+ "aria-label": name,
118
+ onClick: () => setOpen((v) => !v),
119
+ role: "combobox",
120
+ "aria-haspopup": "listbox",
121
+ "aria-controls": listboxId,
122
+ "aria-expanded": open,
123
+ "aria-invalid": isInvalid || void 0,
124
+ "aria-describedby": describedBy,
125
+ className: cn(FIELD_TRIGGER, "pe-9", label !== void 0 && FIELD_FLOATING_PAD, isInvalid && FIELD_INVALID),
126
+ children: [
127
+ /* @__PURE__ */ jsx("span", { className: cn("flex items-center gap-2 min-w-0", selected ? "text-[var(--text-primary)]" : "text-[var(--text-placeholder)]"), children: selected ? /* @__PURE__ */ jsxs(Fragment, { children: [
128
+ /* @__PURE__ */ jsx(CurrencyFlag, { country: selected.country }),
129
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: selected.code })
130
+ ] }) : placeholder ?? text.currency }),
131
+ /* @__PURE__ */ jsx(FieldChevron, {})
132
+ ]
133
+ }
134
+ );
101
135
  return (
102
136
  // The wrapper takes `rest`; the trigger takes the name. This field's label is a
103
137
  // floating <span> rather than a <label for>, so a caller who wants the control to
104
138
  // announce more than the code inside it has nowhere else to put it.
105
139
  /* @__PURE__ */ jsxs("div", { ...rest, ref: wrapperRef, className: cn("relative", className), children: [
106
- label !== void 0 && /* @__PURE__ */ jsx(FieldLabel, { children: label }),
107
- /* @__PURE__ */ jsxs(
108
- "button",
109
- {
110
- type: "button",
111
- "aria-label": name,
112
- onClick: () => setOpen((v) => !v),
113
- role: "combobox",
114
- "aria-haspopup": "listbox",
115
- "aria-controls": listboxId,
116
- "aria-expanded": open,
117
- "aria-invalid": invalid || void 0,
118
- className: cn(FIELD_TRIGGER, "pe-9", label !== void 0 && FIELD_FLOATING_PAD, invalid && FIELD_INVALID),
119
- children: [
120
- /* @__PURE__ */ jsx("span", { className: cn("flex items-center gap-2 min-w-0", selected ? "text-[var(--text-primary)]" : "text-[var(--text-placeholder)]"), children: selected ? /* @__PURE__ */ jsxs(Fragment, { children: [
121
- /* @__PURE__ */ jsx(CurrencyFlag, { country: selected.country }),
122
- /* @__PURE__ */ jsx("span", { className: "font-medium", children: selected.code })
123
- ] }) : placeholder ?? text.currency }),
124
- /* @__PURE__ */ jsx(FieldChevron, {})
125
- ]
126
- }
127
- ),
140
+ label !== void 0 && (labelHint === void 0 ? /* @__PURE__ */ jsx(FieldLabel, { children: label }) : (
141
+ // The label and its "?" as one row on the label's line, as FloatingField lays
142
+ // out a static label with a hint: the row places both, the label truncates
143
+ // and the hint keeps its width. `z-10` like FieldLabel's own: the trigger is
144
+ // `relative` and comes later, so it would paint over the strip.
145
+ /* @__PURE__ */ jsxs("div", { className: "pointer-events-none absolute inset-x-3 top-1 z-10 flex items-center gap-1", children: [
146
+ /* @__PURE__ */ jsx(FieldLabel, { className: "static z-auto min-w-0 max-w-none", children: label }),
147
+ /* @__PURE__ */ jsx("span", { className: "pointer-events-auto flex shrink-0 items-center", children: labelHint })
148
+ ] })
149
+ )),
150
+ label === void 0 && labelHint !== void 0 ? (
151
+ // No label line to ride: at the field's end edge, outside the box, as an
152
+ // unlabelled Select places its FieldHint.
153
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
154
+ /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children: trigger }),
155
+ /* @__PURE__ */ jsx("span", { className: "flex shrink-0 items-center", children: labelHint })
156
+ ] })
157
+ ) : trigger,
128
158
  open && /* @__PURE__ */ jsx(
129
159
  DropdownPanel,
130
160
  {
@@ -167,10 +197,13 @@ function CurrencySelect({
167
197
  ) }, c.code);
168
198
  })
169
199
  }
170
- )
200
+ ),
201
+ textHint && /* @__PURE__ */ jsx("p", { id: hintId, className: "mt-1 text-[11px] leading-tight text-[var(--text-muted)]", children: hint }),
202
+ hasError && /* @__PURE__ */ jsx("p", { id: errorId, className: FIELD_ERROR_CLASS, children: error })
171
203
  ] })
172
204
  );
173
205
  }
206
+ const FIELD_ERROR_CLASS = "mt-1 text-[11px] leading-tight text-[var(--danger)]";
174
207
  export {
175
208
  CURRENCIES,
176
209
  CurrencyFlag,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/currency-select.tsx"],"sourcesContent":["import { useMemo, useId } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport { Check } from \"lucide-react\";\nimport { FieldChevron, FieldLabel, FIELD_TRIGGER, FIELD_INVALID, FIELD_FLOATING_PAD } from \"./ui\";\nimport { cn } from \"../lib/cn\";\nimport { DropdownPanel, DropdownSearchHeader, useDropdownSearch } from \"./dropdown\";\nimport {\n DEFAULT_COMMON_LABELS,\n DEFAULT_CURRENCY_LABELS,\n useKitLabels,\n useKitLocale,\n} from \"../i18n/kit-labels\";\n\nexport interface CurrencyOption {\n code: string;\n name: string;\n symbol: string;\n /** ISO 3166-1 alpha-2 country code (lowercase) used by flag-icons. */\n country: string;\n}\n\nexport const CURRENCIES: CurrencyOption[] = [\n { code: \"EUR\", name: \"Euro\", symbol: \"€\", country: \"eu\" },\n { code: \"USD\", name: \"US Dollar\", symbol: \"$\", country: \"us\" },\n { code: \"GBP\", name: \"British Pound\", symbol: \"£\", country: \"gb\" },\n { code: \"CHF\", name: \"Swiss Franc\", symbol: \"CHF\", country: \"ch\" },\n { code: \"JPY\", name: \"Japanese Yen\", symbol: \"¥\", country: \"jp\" },\n { code: \"CAD\", name: \"Canadian Dollar\", symbol: \"CA$\", country: \"ca\" },\n { code: \"AUD\", name: \"Australian Dollar\", symbol: \"A$\", country: \"au\" },\n { code: \"NZD\", name: \"New Zealand Dollar\", symbol: \"NZ$\", country: \"nz\" },\n { code: \"SEK\", name: \"Swedish Krona\", symbol: \"kr\", country: \"se\" },\n { code: \"NOK\", name: \"Norwegian Krone\", symbol: \"kr\", country: \"no\" },\n { code: \"DKK\", name: \"Danish Krone\", symbol: \"kr\", country: \"dk\" },\n { code: \"PLN\", name: \"Polish Zloty\", symbol: \"zł\", country: \"pl\" },\n { code: \"CZK\", name: \"Czech Koruna\", symbol: \"Kč\", country: \"cz\" },\n { code: \"HUF\", name: \"Hungarian Forint\", symbol: \"Ft\", country: \"hu\" },\n { code: \"RON\", name: \"Romanian Leu\", symbol: \"lei\", country: \"ro\" },\n { code: \"BGN\", name: \"Bulgarian Lev\", symbol: \"лв\", country: \"bg\" },\n { code: \"TRY\", name: \"Turkish Lira\", symbol: \"₺\", country: \"tr\" },\n { code: \"RUB\", name: \"Russian Ruble\", symbol: \"₽\", country: \"ru\" },\n { code: \"CNY\", name: \"Chinese Yuan\", symbol: \"¥\", country: \"cn\" },\n { code: \"HKD\", name: \"Hong Kong Dollar\", symbol: \"HK$\", country: \"hk\" },\n { code: \"SGD\", name: \"Singapore Dollar\", symbol: \"S$\", country: \"sg\" },\n { code: \"INR\", name: \"Indian Rupee\", symbol: \"₹\", country: \"in\" },\n { code: \"BRL\", name: \"Brazilian Real\", symbol: \"R$\", country: \"br\" },\n { code: \"MXN\", name: \"Mexican Peso\", symbol: \"MX$\", country: \"mx\" },\n { code: \"ZAR\", name: \"South African Rand\", symbol: \"R\", country: \"za\" },\n { code: \"ILS\", name: \"Israeli Shekel\", symbol: \"₪\", country: \"il\" },\n { code: \"AED\", name: \"UAE Dirham\", symbol: \"د.إ\", country: \"ae\" },\n];\n\nexport interface CurrencyFlagProps extends ComponentPropsWithoutRef<\"span\"> {\n /** ISO 3166-1 alpha-2, lowercase — the class flag-icons keys its sprite by. */\n country: string;\n}\n\nexport function CurrencyFlag({ country, className, ...rest }: CurrencyFlagProps) {\n return (\n <span\n {...rest}\n // Stays `aria-hidden` whatever a caller passes: the flag is a picture of the\n // code printed next to it, and a reader that met both would hear the currency\n // twice.\n aria-hidden\n className={cn(\"fi inline-block w-5 h-[15px] rounded-sm shadow-[0_0_0_1px_rgba(0,0,0,0.08)] shrink-0\", `fi-${country}`, className)}\n />\n );\n}\n\nconst CURRENCY_BY_CODE = new Map(CURRENCIES.map((c) => [c.code, c]));\n\nexport function getCurrency(code: string | null | undefined): CurrencyOption | undefined {\n return code ? CURRENCY_BY_CODE.get(code.toUpperCase()) : undefined;\n}\n\n/** The name to SHOW for a currency: the caller's translation if they have one for\n * that code; else, when a `locale` is known, the name `Intl.DisplayNames` has for\n * it in that locale; else the English name {@link CURRENCIES} carries. Shared with\n * AmountInput, which renders the same list from the same data.\n *\n * `Intl` only when a locale was actually GIVEN (by prop or `<UiKitProvider>`): an\n * app that never set one keeps the shipped English names it has always shown,\n * rather than whatever language the test runner or the browser happens to be in. */\nexport function currencyName(\n c: CurrencyOption,\n names?: Record<string, string>,\n locale?: string,\n): string {\n const own = names?.[c.code];\n if (own !== undefined) return own;\n if (locale) {\n try {\n const shown = new Intl.DisplayNames([locale], { type: \"currency\" }).of(c.code);\n if (shown && shown !== c.code) return shown;\n } catch {\n // A malformed tag, or a runtime without `DisplayNames`: the shipped name.\n }\n }\n return c.name;\n}\n\n/** `onChange` is the kit's — \"a currency was picked\", carrying the code — rather\n * than the div's form event. */\nexport interface CurrencySelectProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\"> {\n value: string;\n onChange: (code: string) => void;\n placeholder?: string;\n label?: ReactNode;\n /**\n * Restrict the pickable currencies to these ISO codes (e.g. only the\n * currencies actually present in a budget). Order is preserved. Defaults to\n * the full {@link CURRENCIES} list.\n */\n options?: string[];\n /** Required and unanswered — {@link FIELD_INVALID}, the same rose border the\n * native `Select` has worn since feedback #235. Every field-styled control in\n * this package carries it now, so a form can mark any of its fields rather than\n * only the one that happened to have it first. */\n invalid?: boolean;\n /** Every user-facing string this control owns, shaped like `AmountInput`'s\n * `labels` so the two currency pickers in the kit are configured the same way.\n *\n * `search` exists because `placeholder` used to do BOTH jobs: it named the empty\n * field and, for want of anything else, it was also handed to the search box —\n * so a caller who wrote \"Currency\" got \"Currency\" as the prompt for typing a\n * filter, and a caller who wanted to name the filter had to rename the field.\n * `placeholder` still wins for the trigger, so nothing moves for the callers\n * that set it; what changes is that the search box no longer borrows it. */\n labels?: {\n /** The empty trigger, when `placeholder` is not given. */\n currency?: string;\n /** The filter box inside the open panel. */\n search?: string;\n };\n /**\n * Translated currency names, keyed by ISO code — `{ CHF: \"Schweizer Franken\" }`.\n *\n * {@link CURRENCIES} is data the package ships, so its 27 names were the one\n * string in the row a consumer could not translate: the code and the symbol are\n * language-neutral, and the name beside them stayed English in a German UI. This\n * is a sparse OVERRIDE rather than a replacement list, so an app translates the\n * eight currencies it actually uses and keeps the rest; forking `CURRENCIES` to\n * get there would also fork the flags and symbols, which nobody wants to maintain.\n *\n * The search matches what is SHOWN — type \"Franken\" and you find CHF — because a\n * filter that only matches the hidden English name is worse than no filter.\n */\n currencyNames?: Record<string, string>;\n}\n\nexport function CurrencySelect({\n value,\n onChange,\n placeholder,\n className,\n label,\n options,\n invalid,\n labels,\n currencyNames,\n \"aria-label\": ariaLabel,\n ...rest\n}: CurrencySelectProps) {\n const { open, setOpen, wrapperRef, query, setQuery, inputRef } = useDropdownSearch();\n // `labels` > `currency` from the provider > English.\n const text = useKitLabels(\"currency\", DEFAULT_CURRENCY_LABELS, labels);\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const locale = useKitLocale();\n\n const selected = getCurrency(value);\n const pool = useMemo(() => {\n if (!options) return CURRENCIES;\n // Keep the caller's order; drop codes we don't have a definition for.\n return options.map((code) => getCurrency(code)).filter((c): c is CurrencyOption => !!c);\n }, [options]);\n const filtered = useMemo(() => {\n const q = query.trim().toLocaleLowerCase(locale);\n if (!q) return pool;\n return pool.filter(\n (c) =>\n c.code.toLowerCase().includes(q) ||\n currencyName(c, currencyNames, locale).toLocaleLowerCase(locale).includes(q) ||\n c.symbol.toLowerCase().includes(q),\n );\n }, [query, pool, currencyNames, locale]);\n\n const listboxId = `${useId()}-listbox`;\n\n // The trigger's NAME. A combobox does not take its name from its content, and the\n // floating label is a <span>, not a <label for> — so without `aria-label` from the\n // caller this trigger had no name at all (keksdose's test suite, 0.5.0). Composed the\n // way MultiSelect and the entity comboboxes compose theirs, \"label: value\", with the\n // `currency` word standing in for a missing label so it is never nameless.\n const fieldName = typeof label === \"string\" ? label : text.currency;\n const name = ariaLabel ?? common.fieldValue(fieldName, selected ? selected.code : (placeholder ?? text.currency));\n\n return (\n // The wrapper takes `rest`; the trigger takes the name. This field's label is a\n // floating <span> rather than a <label for>, so a caller who wants the control to\n // announce more than the code inside it has nowhere else to put it.\n <div {...rest} ref={wrapperRef} className={cn(\"relative\", className)}>\n {label !== undefined && <FieldLabel>{label}</FieldLabel>}\n <button\n type=\"button\"\n aria-label={name}\n onClick={() => setOpen((v) => !v)}\n // The implicit `button` role supports neither `aria-expanded` nor\n // `aria-invalid`, so this trigger previously wore a red border and announced\n // nothing about being invalid or about there being a list behind it. The role\n // that DESCRIBES it is the one that also supports the attributes — the same\n // treatment its siblings (MultiSelect, the entity comboboxes) now carry, so a\n // form row of pickers reads the same way throughout.\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n aria-controls={listboxId}\n aria-expanded={open}\n aria-invalid={invalid || undefined}\n className={cn(FIELD_TRIGGER, \"pe-9\", label !== undefined && FIELD_FLOATING_PAD, invalid && FIELD_INVALID)}\n >\n {/* The trigger stays COMPACT — flag + code only — so it never clips in a\n narrow field; the full names live in the (wider) popup (feedback\n #308). */}\n <span className={cn(\"flex items-center gap-2 min-w-0\", selected ? \"text-[var(--text-primary)]\" : \"text-[var(--text-placeholder)]\")}>\n {selected ? (\n <>\n <CurrencyFlag country={selected.country} />\n <span className=\"font-medium\">{selected.code}</span>\n </>\n ) : (\n placeholder ?? text.currency\n )}\n </span>\n <FieldChevron />\n </button>\n {open && (\n <DropdownPanel\n // At least as wide as the trigger, but grows to fit the full currency\n // names and caps so it never runs off-screen. END-aligned so it grows\n // toward the start instead of pushing the page width when there's content\n // after the field (feedback #308).\n className=\"end-0 min-w-full w-max max-w-[min(20rem,calc(100vw-2rem))]\"\n empty={filtered.length === 0}\n listProps={{ id: listboxId }}\n header={\n <DropdownSearchHeader\n query={query}\n onQueryChange={setQuery}\n inputRef={inputRef}\n placeholder={text.search}\n />\n }\n >\n {filtered.map((c) => {\n const active = c.code === selected?.code;\n return (\n <li key={c.code}>\n <button\n type=\"button\"\n onClick={() => {\n onChange(c.code);\n setOpen(false);\n }}\n className={cn(\n \"flex w-full items-center justify-between gap-2 px-3 py-2 text-sm text-start hover:bg-[var(--bg-hover)]\",\n active && \"bg-[var(--bg-active)]\",\n )}\n >\n <span className=\"flex items-center gap-2 min-w-0\">\n <CurrencyFlag country={c.country} />\n <span className=\"font-mono text-[var(--text-muted)] w-8 shrink-0\">{c.symbol}</span>\n <span className=\"font-medium text-[var(--text-primary)]\">{c.code}</span>\n <span className=\"text-[var(--text-muted)] truncate\">{currencyName(c, currencyNames, locale)}</span>\n </span>\n {active && <Check className=\"size-4 text-[var(--text-secondary)] shrink-0\" />}\n </button>\n </li>\n );\n })}\n </DropdownPanel>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/currency-select.tsx"],"sourcesContent":["import { useMemo, useId } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport { Check } from \"lucide-react\";\nimport { FieldChevron, FieldLabel, FIELD_TRIGGER, FIELD_INVALID, FIELD_FLOATING_PAD } from \"./ui\";\nimport { hasMessage, mergeDescribedBy } from \"./choice-parts\";\nimport { cn } from \"../lib/cn\";\nimport { DropdownPanel, DropdownSearchHeader, useDropdownSearch } from \"./dropdown\";\nimport {\n DEFAULT_COMMON_LABELS,\n DEFAULT_CURRENCY_LABELS,\n useKitLabels,\n useKitLocale,\n} from \"../i18n/kit-labels\";\n\nexport interface CurrencyOption {\n code: string;\n name: string;\n symbol: string;\n /** ISO 3166-1 alpha-2 country code (lowercase) used by flag-icons. */\n country: string;\n}\n\nexport const CURRENCIES: CurrencyOption[] = [\n { code: \"EUR\", name: \"Euro\", symbol: \"€\", country: \"eu\" },\n { code: \"USD\", name: \"US Dollar\", symbol: \"$\", country: \"us\" },\n { code: \"GBP\", name: \"British Pound\", symbol: \"£\", country: \"gb\" },\n { code: \"CHF\", name: \"Swiss Franc\", symbol: \"CHF\", country: \"ch\" },\n { code: \"JPY\", name: \"Japanese Yen\", symbol: \"¥\", country: \"jp\" },\n { code: \"CAD\", name: \"Canadian Dollar\", symbol: \"CA$\", country: \"ca\" },\n { code: \"AUD\", name: \"Australian Dollar\", symbol: \"A$\", country: \"au\" },\n { code: \"NZD\", name: \"New Zealand Dollar\", symbol: \"NZ$\", country: \"nz\" },\n { code: \"SEK\", name: \"Swedish Krona\", symbol: \"kr\", country: \"se\" },\n { code: \"NOK\", name: \"Norwegian Krone\", symbol: \"kr\", country: \"no\" },\n { code: \"DKK\", name: \"Danish Krone\", symbol: \"kr\", country: \"dk\" },\n { code: \"PLN\", name: \"Polish Zloty\", symbol: \"zł\", country: \"pl\" },\n { code: \"CZK\", name: \"Czech Koruna\", symbol: \"Kč\", country: \"cz\" },\n { code: \"HUF\", name: \"Hungarian Forint\", symbol: \"Ft\", country: \"hu\" },\n { code: \"RON\", name: \"Romanian Leu\", symbol: \"lei\", country: \"ro\" },\n { code: \"BGN\", name: \"Bulgarian Lev\", symbol: \"лв\", country: \"bg\" },\n { code: \"TRY\", name: \"Turkish Lira\", symbol: \"₺\", country: \"tr\" },\n { code: \"RUB\", name: \"Russian Ruble\", symbol: \"₽\", country: \"ru\" },\n { code: \"CNY\", name: \"Chinese Yuan\", symbol: \"¥\", country: \"cn\" },\n { code: \"HKD\", name: \"Hong Kong Dollar\", symbol: \"HK$\", country: \"hk\" },\n { code: \"SGD\", name: \"Singapore Dollar\", symbol: \"S$\", country: \"sg\" },\n { code: \"INR\", name: \"Indian Rupee\", symbol: \"₹\", country: \"in\" },\n { code: \"BRL\", name: \"Brazilian Real\", symbol: \"R$\", country: \"br\" },\n { code: \"MXN\", name: \"Mexican Peso\", symbol: \"MX$\", country: \"mx\" },\n { code: \"ZAR\", name: \"South African Rand\", symbol: \"R\", country: \"za\" },\n { code: \"ILS\", name: \"Israeli Shekel\", symbol: \"₪\", country: \"il\" },\n { code: \"AED\", name: \"UAE Dirham\", symbol: \"د.إ\", country: \"ae\" },\n];\n\nexport interface CurrencyFlagProps extends ComponentPropsWithoutRef<\"span\"> {\n /** ISO 3166-1 alpha-2, lowercase — the class flag-icons keys its sprite by. */\n country: string;\n}\n\nexport function CurrencyFlag({ country, className, ...rest }: CurrencyFlagProps) {\n return (\n <span\n {...rest}\n // Stays `aria-hidden` whatever a caller passes: the flag is a picture of the\n // code printed next to it, and a reader that met both would hear the currency\n // twice.\n aria-hidden\n className={cn(\"fi inline-block w-5 h-[15px] rounded-sm shadow-[0_0_0_1px_rgba(0,0,0,0.08)] shrink-0\", `fi-${country}`, className)}\n />\n );\n}\n\nconst CURRENCY_BY_CODE = new Map(CURRENCIES.map((c) => [c.code, c]));\n\nexport function getCurrency(code: string | null | undefined): CurrencyOption | undefined {\n return code ? CURRENCY_BY_CODE.get(code.toUpperCase()) : undefined;\n}\n\n/** The name to SHOW for a currency: the caller's translation if they have one for\n * that code; else, when a `locale` is known, the name `Intl.DisplayNames` has for\n * it in that locale; else the English name {@link CURRENCIES} carries. Shared with\n * AmountInput, which renders the same list from the same data.\n *\n * `Intl` only when a locale was actually GIVEN (by prop or `<UiKitProvider>`): an\n * app that never set one keeps the shipped English names it has always shown,\n * rather than whatever language the test runner or the browser happens to be in. */\nexport function currencyName(\n c: CurrencyOption,\n names?: Record<string, string>,\n locale?: string,\n): string {\n const own = names?.[c.code];\n if (own !== undefined) return own;\n if (locale) {\n try {\n const shown = new Intl.DisplayNames([locale], { type: \"currency\" }).of(c.code);\n if (shown && shown !== c.code) return shown;\n } catch {\n // A malformed tag, or a runtime without `DisplayNames`: the shipped name.\n }\n }\n return c.name;\n}\n\n/** `onChange` is the kit's — \"a currency was picked\", carrying the code — rather\n * than the div's form event. */\nexport interface CurrencySelectProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\"> {\n value: string;\n onChange: (code: string) => void;\n placeholder?: string;\n label?: ReactNode;\n /**\n * Restrict the pickable currencies to these ISO codes (e.g. only the\n * currencies actually present in a budget). Order is preserved. Defaults to\n * the full {@link CURRENCIES} list.\n */\n options?: string[];\n /** Required and unanswered — {@link FIELD_INVALID}, the same rose border the\n * native `Select` has worn since feedback #235. Every field-styled control in\n * this package carries it now, so a form can mark any of its fields rather than\n * only the one that happened to have it first. */\n invalid?: boolean;\n /**\n * What is wrong with the choice, in the caller's own words — {@link Input}'s `error`\n * (keksdose K4: of the kit's pickers this one had neither a message nor a caption, so\n * \"the account is in EUR\" sat in a loose `<p>` beside it that nothing pointed at).\n * Rendered under the field, after a text `hint`; merged into the trigger's\n * `aria-describedby`; implies `invalid`. `null`, `false` and `\"\"` are no message.\n */\n error?: ReactNode;\n /**\n * {@link Select}'s `hint`, the same rule (keksdose K4). A {@link FieldHint} \"?\" rides\n * the label's line; on an unlabelled picker it sits at the field's end edge, outside\n * the box. Plain TEXT (a string or a number) is a caption UNDER the field, attached\n * to the trigger through `aria-describedby` — the label line has no room for a\n * sentence.\n */\n hint?: ReactNode;\n /** Every user-facing string this control owns, shaped like `AmountInput`'s\n * `labels` so the two currency pickers in the kit are configured the same way.\n *\n * `search` exists because `placeholder` used to do BOTH jobs: it named the empty\n * field and, for want of anything else, it was also handed to the search box —\n * so a caller who wrote \"Currency\" got \"Currency\" as the prompt for typing a\n * filter, and a caller who wanted to name the filter had to rename the field.\n * `placeholder` still wins for the trigger, so nothing moves for the callers\n * that set it; what changes is that the search box no longer borrows it. */\n labels?: {\n /** The empty trigger, when `placeholder` is not given. */\n currency?: string;\n /** The filter box inside the open panel. */\n search?: string;\n };\n /**\n * Translated currency names, keyed by ISO code — `{ CHF: \"Schweizer Franken\" }`.\n *\n * {@link CURRENCIES} is data the package ships, so its 27 names were the one\n * string in the row a consumer could not translate: the code and the symbol are\n * language-neutral, and the name beside them stayed English in a German UI. This\n * is a sparse OVERRIDE rather than a replacement list, so an app translates the\n * eight currencies it actually uses and keeps the rest; forking `CURRENCIES` to\n * get there would also fork the flags and symbols, which nobody wants to maintain.\n *\n * The search matches what is SHOWN — type \"Franken\" and you find CHF — because a\n * filter that only matches the hidden English name is worse than no filter.\n */\n currencyNames?: Record<string, string>;\n}\n\nexport function CurrencySelect({\n value,\n onChange,\n placeholder,\n className,\n label,\n options,\n invalid,\n error,\n hint,\n labels,\n currencyNames,\n \"aria-label\": ariaLabel,\n \"aria-describedby\": ariaDescribedBy,\n \"aria-invalid\": ariaInvalid,\n ...rest\n}: CurrencySelectProps) {\n const { open, setOpen, wrapperRef, query, setQuery, inputRef } = useDropdownSearch();\n // `labels` > `currency` from the provider > English.\n const text = useKitLabels(\"currency\", DEFAULT_CURRENCY_LABELS, labels);\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const locale = useKitLocale();\n\n const selected = getCurrency(value);\n const pool = useMemo(() => {\n if (!options) return CURRENCIES;\n // Keep the caller's order; drop codes we don't have a definition for.\n return options.map((code) => getCurrency(code)).filter((c): c is CurrencyOption => !!c);\n }, [options]);\n const filtered = useMemo(() => {\n const q = query.trim().toLocaleLowerCase(locale);\n if (!q) return pool;\n return pool.filter(\n (c) =>\n c.code.toLowerCase().includes(q) ||\n currencyName(c, currencyNames, locale).toLocaleLowerCase(locale).includes(q) ||\n c.symbol.toLowerCase().includes(q),\n );\n }, [query, pool, currencyNames, locale]);\n\n const listboxId = `${useId()}-listbox`;\n const hintId = useId();\n const errorId = useId();\n // Text is a caption under the field; anything else (a FieldHint) rides the label\n // line. See `hint`.\n const textHint = (typeof hint === \"string\" && hint !== \"\") || typeof hint === \"number\";\n const labelHint = !textHint && hasMessage(hint) ? hint : undefined;\n const hasError = hasMessage(error);\n // On the TRIGGER, which is the control with a role: a caller's description used to\n // land on the wrapper `<div>` through the spread, where nothing reads it. The\n // caller's ids first, then the caption, then the error — Select's order.\n const describedBy = mergeDescribedBy(ariaDescribedBy, textHint && hintId, hasError && errorId);\n const isInvalid = Boolean(invalid) || hasError || ariaInvalid === true || ariaInvalid === \"true\";\n\n // The trigger's NAME. A combobox does not take its name from its content, and the\n // floating label is a <span>, not a <label for> — so without `aria-label` from the\n // caller this trigger had no name at all (keksdose's test suite, 0.5.0). Composed the\n // way MultiSelect and the entity comboboxes compose theirs, \"label: value\", with the\n // `currency` word standing in for a missing label so it is never nameless.\n const fieldName = typeof label === \"string\" ? label : text.currency;\n const name = ariaLabel ?? common.fieldValue(fieldName, selected ? selected.code : (placeholder ?? text.currency));\n\n const trigger = (\n <button\n type=\"button\"\n aria-label={name}\n onClick={() => setOpen((v) => !v)}\n // The implicit `button` role supports neither `aria-expanded` nor\n // `aria-invalid`, so this trigger previously wore a red border and announced\n // nothing about being invalid or about there being a list behind it. The role\n // that DESCRIBES it is the one that also supports the attributes — the same\n // treatment its siblings (MultiSelect, the entity comboboxes) now carry, so a\n // form row of pickers reads the same way throughout.\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n aria-controls={listboxId}\n aria-expanded={open}\n aria-invalid={isInvalid || undefined}\n aria-describedby={describedBy}\n className={cn(FIELD_TRIGGER, \"pe-9\", label !== undefined && FIELD_FLOATING_PAD, isInvalid && FIELD_INVALID)}\n >\n {/* The trigger stays COMPACT — flag + code only — so it never clips in a\n narrow field; the full names live in the (wider) popup (feedback\n #308). */}\n <span className={cn(\"flex items-center gap-2 min-w-0\", selected ? \"text-[var(--text-primary)]\" : \"text-[var(--text-placeholder)]\")}>\n {selected ? (\n <>\n <CurrencyFlag country={selected.country} />\n <span className=\"font-medium\">{selected.code}</span>\n </>\n ) : (\n placeholder ?? text.currency\n )}\n </span>\n <FieldChevron />\n </button>\n );\n\n return (\n // The wrapper takes `rest`; the trigger takes the name. This field's label is a\n // floating <span> rather than a <label for>, so a caller who wants the control to\n // announce more than the code inside it has nowhere else to put it.\n <div {...rest} ref={wrapperRef} className={cn(\"relative\", className)}>\n {label !== undefined &&\n (labelHint === undefined ? (\n <FieldLabel>{label}</FieldLabel>\n ) : (\n // The label and its \"?\" as one row on the label's line, as FloatingField lays\n // out a static label with a hint: the row places both, the label truncates\n // and the hint keeps its width. `z-10` like FieldLabel's own: the trigger is\n // `relative` and comes later, so it would paint over the strip.\n <div className=\"pointer-events-none absolute inset-x-3 top-1 z-10 flex items-center gap-1\">\n <FieldLabel className=\"static z-auto min-w-0 max-w-none\">{label}</FieldLabel>\n <span className=\"pointer-events-auto flex shrink-0 items-center\">{labelHint}</span>\n </div>\n ))}\n {label === undefined && labelHint !== undefined ? (\n // No label line to ride: at the field's end edge, outside the box, as an\n // unlabelled Select places its FieldHint.\n <div className=\"flex items-center gap-1.5\">\n <div className=\"min-w-0 flex-1\">{trigger}</div>\n <span className=\"flex shrink-0 items-center\">{labelHint}</span>\n </div>\n ) : (\n trigger\n )}\n {open && (\n <DropdownPanel\n // At least as wide as the trigger, but grows to fit the full currency\n // names and caps so it never runs off-screen. END-aligned so it grows\n // toward the start instead of pushing the page width when there's content\n // after the field (feedback #308).\n className=\"end-0 min-w-full w-max max-w-[min(20rem,calc(100vw-2rem))]\"\n empty={filtered.length === 0}\n listProps={{ id: listboxId }}\n header={\n <DropdownSearchHeader\n query={query}\n onQueryChange={setQuery}\n inputRef={inputRef}\n placeholder={text.search}\n />\n }\n >\n {filtered.map((c) => {\n const active = c.code === selected?.code;\n return (\n <li key={c.code}>\n <button\n type=\"button\"\n onClick={() => {\n onChange(c.code);\n setOpen(false);\n }}\n className={cn(\n \"flex w-full items-center justify-between gap-2 px-3 py-2 text-sm text-start hover:bg-[var(--bg-hover)]\",\n active && \"bg-[var(--bg-active)]\",\n )}\n >\n <span className=\"flex items-center gap-2 min-w-0\">\n <CurrencyFlag country={c.country} />\n <span className=\"font-mono text-[var(--text-muted)] w-8 shrink-0\">{c.symbol}</span>\n <span className=\"font-medium text-[var(--text-primary)]\">{c.code}</span>\n <span className=\"text-[var(--text-muted)] truncate\">{currencyName(c, currencyNames, locale)}</span>\n </span>\n {active && <Check className=\"size-4 text-[var(--text-secondary)] shrink-0\" />}\n </button>\n </li>\n );\n })}\n </DropdownPanel>\n )}\n {/* After the panel, not before it, as MultiSelect places its message: the panel\n is `absolute` with no `top`, so it opens at its place in the flow — under the\n caption, had the caption come first. */}\n {textHint && (\n <p id={hintId} className=\"mt-1 text-[11px] leading-tight text-[var(--text-muted)]\">\n {hint}\n </p>\n )}\n {hasError && (\n <p id={errorId} className={FIELD_ERROR_CLASS}>\n {error}\n </p>\n )}\n </div>\n );\n}\n\n// The error line under a field — the type of `ui.tsx`'s (module-private) one, so a\n// CurrencySelect's message is indistinguishable from an Input's.\nconst FIELD_ERROR_CLASS = \"mt-1 text-[11px] leading-tight text-[var(--danger)]\";\n"],"mappings":"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","names":[]}