@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
@@ -1,9 +1,16 @@
1
1
  import { useId, useState } from "react";
2
2
  import type { KeyboardEvent, ReactNode } from "react";
3
- import { Button, Card, FIELD_WRITABLE_LOOK, Input } from "./ui";
3
+ import { Button, Card, Input } from "./ui";
4
4
  import { UserAvatar } from "./user-avatar";
5
5
  import { CopyButton } from "./copy-button";
6
6
  import { QrCode } from "./qr-code";
7
+ import { OneTimeCodeInput } from "./one-time-code-input";
8
+ import {
9
+ DEFAULT_PASSWORD_STRENGTH_LABELS,
10
+ passwordByteLength,
11
+ PasswordStrengthMeter,
12
+ } from "./password-strength";
13
+ import type { PasswordStrengthMeterProps } from "./password-strength";
7
14
  import { DEFAULT_COMMON_LABELS, useKitLabels } from "../i18n/kit-labels";
8
15
  import { useAccountSettingsLabels } from "./account-settings-labels";
9
16
  import type {
@@ -95,30 +102,73 @@ export function ProfileSetting({
95
102
  );
96
103
  }
97
104
 
105
+ /**
106
+ * What {@link PasswordSetting}'s `strength` takes besides `true`: the meter's own options,
107
+ * minus the ones the card owns — `value` (the new password), `minLength` (the card's, so
108
+ * the checklist's length rule and the submit's check are one number) and `maxBytes`
109
+ * (the card's own prop, which the submit enforces as well).
110
+ */
111
+ export type PasswordSettingStrength = Pick<PasswordStrengthMeterProps, "score" | "showRequirements" | "labels">;
112
+
98
113
  export function PasswordSetting({
99
114
  onSubmit,
100
115
  pending,
101
116
  minLength = 8,
117
+ strength,
118
+ maxBytes,
102
119
  labels: labelsProp,
103
120
  }: {
104
121
  /** Called with the validated (current, new) pair; return a promise to auto-clear on success. */
105
122
  onSubmit: (currentPassword: string, newPassword: string) => void | Promise<unknown>;
106
123
  pending?: boolean;
124
+ /** The shortest new password the card submits, in characters as a person counts them
125
+ * (an emoji is one). Keep it in step with the server's own minimum. Default 8. */
107
126
  minLength?: number;
127
+ /**
128
+ * Show the {@link PasswordStrengthMeter} under the new-password field (0.22.0,
129
+ * kastlan's input audit): the bar, the level in words and the checklist, with the
130
+ * length rule drawn from `minLength`. `true` for the meter as it comes; an object for
131
+ * its `score` (zxcvbn's, say), `showRequirements` or `labels`.
132
+ *
133
+ * Off by default, so a card that never asked for it does not grow one. The meter is
134
+ * advisory, as everywhere in the kit: it blocks nothing — the card's own checks do
135
+ * (`minLength`, the confirmation, `maxBytes`).
136
+ */
137
+ strength?: boolean | PasswordSettingStrength;
138
+ /**
139
+ * A byte ceiling on the new password — bcrypt reads at most 72 BYTES and ignores the
140
+ * rest, which is kastlan's case: `maxBytes={72}`. A new password over it is refused
141
+ * on submit with the meter's own "too long" sentence (the `passwordStrength`
142
+ * namespace's `tooLong`), and the meter, when shown, warns while it is typed.
143
+ *
144
+ * Unset by default, and leave it unset for a secret with no ceiling: an invented one
145
+ * talks people out of the long passphrase they chose.
146
+ */
147
+ maxBytes?: number;
108
148
  /** Prop > `<UiKitProvider labels={{ accountSettings: { password } }}>` > English. */
109
149
  labels?: Partial<PasswordSettingLabels>;
110
150
  }) {
111
151
  const labels = useAccountSettingsLabels("password", labelsProp);
152
+ const strengthOptions = typeof strength === "object" ? strength : undefined;
153
+ // Only for `tooLong`: the sentence the meter shows is the one the refusal says.
154
+ const strengthLabels = useKitLabels("passwordStrength", DEFAULT_PASSWORD_STRENGTH_LABELS, strengthOptions?.labels);
112
155
  const [current, setCurrent] = useState("");
113
156
  const [next, setNext] = useState("");
114
157
  const [confirm, setConfirm] = useState("");
115
158
  const [error, setError] = useState<string | null>(null);
116
159
 
117
160
  const submit = async () => {
118
- if (next.length < minLength) {
161
+ // Code points, not UTF-16 units — the count the meter's checklist and a server's
162
+ // `len()` use. `next.length` counted an emoji as two, so four of them passed a
163
+ // minimum of eight that the meter beside the field said they missed.
164
+ if ([...next].length < minLength) {
119
165
  setError(labels.tooShort);
120
166
  return;
121
167
  }
168
+ if (maxBytes !== undefined && passwordByteLength(next) > maxBytes) {
169
+ setError(strengthLabels.tooLong(maxBytes));
170
+ return;
171
+ }
122
172
  if (next !== confirm) {
123
173
  setError(labels.mismatch);
124
174
  return;
@@ -134,11 +184,31 @@ export function PasswordSetting({
134
184
  }
135
185
  };
136
186
 
187
+ const nextField = (
188
+ <Input type="password" autoComplete="new-password" label={labels.next} value={next} onChange={(e) => setNext(e.target.value)} />
189
+ );
190
+
137
191
  return (
138
192
  <Card className="p-4 space-y-3">
139
193
  <div className="text-sm font-medium">{labels.title}</div>
140
194
  <Input type="password" autoComplete="current-password" label={labels.current} value={current} onChange={(e) => setCurrent(e.target.value)} />
141
- <Input type="password" autoComplete="new-password" label={labels.next} value={next} onChange={(e) => setNext(e.target.value)} />
195
+ {strength ? (
196
+ // One box for the field and its meter, so the card's `space-y-3` spaces the pair
197
+ // from its neighbours and the meter keeps its own small gap under the field.
198
+ <div>
199
+ {nextField}
200
+ <PasswordStrengthMeter
201
+ value={next}
202
+ minLength={minLength}
203
+ maxBytes={maxBytes}
204
+ score={strengthOptions?.score}
205
+ showRequirements={strengthOptions?.showRequirements}
206
+ labels={strengthOptions?.labels}
207
+ />
208
+ </div>
209
+ ) : (
210
+ nextField
211
+ )}
142
212
  <Input type="password" autoComplete="new-password" label={labels.confirm} value={confirm} onChange={(e) => setConfirm(e.target.value)} />
143
213
  {error && <p className="text-xs text-[var(--danger)]">{error}</p>}
144
214
  <Button onClick={() => void submit()} disabled={!current || !next || !confirm || pending}>
@@ -204,13 +274,17 @@ export function TwoFactorSetting({
204
274
  const labels = useAccountSettingsLabels("twoFactor", labelsProp);
205
275
  const [code, setCode] = useState("");
206
276
  const [password, setPassword] = useState("");
207
- // Browsers autofill one-time-code fields with the saved username; keep the
208
- // field readOnly until focus to block that. It therefore has to keep the LOOK of
209
- // an editable field (FIELD_WRITABLE_LOOK): this is the one place where readOnly
210
- // does not mean "you may not type here", and without the opt-out the field would
211
- // sit there greyed until the moment it is focused.
212
- const [otpReadonly, setOtpReadonly] = useState(true);
213
- const onOtpFocus = () => setOtpReadonly(false);
277
+ // Both code fields are a OneTimeCodeInput (0.22.0) — the field this card wrote by hand
278
+ // twice — and keep what they did:
279
+ // - `readOnlyUntilFocus`: browsers autofill a one-time-code field with the saved
280
+ // username (the disable form has a current-password field right above it), and a
281
+ // read-only field is never autofilled. The guard drops on focus, and the field
282
+ // keeps the look of an editable one meanwhile (FIELD_WRITABLE_LOOK) — see the prop.
283
+ // - `length={8}`: the `maxLength={8}` they had, so an authenticator issuing eight
284
+ // digits still fits. What changed is that they now take digits only: "123 456"
285
+ // pasted from an authenticator that groups its digits used to reach `onEnable`
286
+ // with the space in it.
287
+ // - one `code` state for both, as before.
214
288
  // Only for the "Status: Enabled" composition — the punctuation between a field's
215
289
  // name and its value is the language's (see `CommonLabels.fieldValue`).
216
290
  const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
@@ -272,16 +346,12 @@ export function TwoFactorSetting({
272
346
  </div>
273
347
  </div>
274
348
  )}
275
- <Input
276
- inputMode="numeric"
277
- autoComplete="one-time-code"
278
- readOnly={otpReadonly}
279
- inputClassName={FIELD_WRITABLE_LOOK}
280
- onFocus={onOtpFocus}
281
- maxLength={8}
349
+ <OneTimeCodeInput
350
+ readOnlyUntilFocus
351
+ length={8}
282
352
  label={labels.codeLabel}
283
353
  value={code}
284
- onChange={(e) => setCode(e.target.value)}
354
+ onChange={setCode}
285
355
  onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {
286
356
  if (e.key === "Enter" && code && !busy) {
287
357
  e.preventDefault();
@@ -299,16 +369,12 @@ export function TwoFactorSetting({
299
369
  <div className="space-y-3 border-t border-[var(--border)] pt-3">
300
370
  <div className="text-sm font-medium">{labels.disableSection}</div>
301
371
  <Input type="password" autoComplete="current-password" label={labels.password} value={password} onChange={(e) => setPassword(e.target.value)} />
302
- <Input
303
- inputMode="numeric"
304
- autoComplete="one-time-code"
305
- readOnly={otpReadonly}
306
- inputClassName={FIELD_WRITABLE_LOOK}
307
- onFocus={onOtpFocus}
308
- maxLength={8}
372
+ <OneTimeCodeInput
373
+ readOnlyUntilFocus
374
+ length={8}
309
375
  label={labels.codeLabel}
310
376
  value={code}
311
- onChange={(e) => setCode(e.target.value)}
377
+ onChange={setCode}
312
378
  onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {
313
379
  if (e.key === "Enter" && password && code && !busy) {
314
380
  e.preventDefault();
@@ -25,6 +25,8 @@ import {
25
25
  sanitizeLive,
26
26
  splitLeadingSign,
27
27
  } from "../lib/calc";
28
+ import { decimalMark, localizeMark, readTyped, showsTyped } from "../lib/decimal-marks";
29
+ import { hasMessage, mergeDescribedBy } from "./choice-parts";
28
30
 
29
31
  interface AmountInputProps {
30
32
  value: string;
@@ -36,7 +38,41 @@ interface AmountInputProps {
36
38
  disabled?: boolean;
37
39
  /** Required and unanswered — {@link FIELD_INVALID}. See {@link Input}'s `invalid`. */
38
40
  invalid?: boolean;
41
+ /**
42
+ * What is wrong with the amount, in the caller's own words — {@link Input}'s `error`
43
+ * (keksdose K4: "exceeds the outstanding balance" was a loose `<p>` beside the field
44
+ * that nothing pointed at). Rendered under the field, after a text `hint`; merged into
45
+ * `aria-describedby` after the caller's ids and the hint's; implies `invalid`, so the
46
+ * field paints as well as announces. `null`, `false` and `""` are no message.
47
+ * {@link MoneyField} takes it too.
48
+ */
49
+ error?: ReactNode;
50
+ /**
51
+ * Show the desktop calculator trigger (default `true`). `false` for an amount in an
52
+ * ephemeral close-on-blur editor — a budget's assigned cell, a VAT cell (keksdose K6)
53
+ * — where a click on the icon blurs the field and tears the editor down before the
54
+ * popover can open. Typing a calculation ("200+50", then Enter) still evaluates, and
55
+ * a phone keeps its numpad, exactly as {@link NumberInput}'s `calculator={false}`.
56
+ */
57
+ calculator?: boolean;
58
+ /** The field's WRAPPER — width, a margin, a flex share. The `<input>` is
59
+ * `inputClassName`'s. */
39
60
  className?: string;
61
+ /**
62
+ * Classes for the `<input>` itself — {@link NumberInput}'s and {@link Input}'s prop of
63
+ * the same name, merged the same way: after the field's own base classes, so it can
64
+ * restyle the figure (`text-end` for a money column whose decimal points line up,
65
+ * `font-medium`, a tighter `py-1` in a table cell), and before the end padding the
66
+ * calculator and the currency chip reserve and before the invalid border, so neither
67
+ * can be taken away by accident.
68
+ *
69
+ * keksdose G7: the invoice's VAT cell right-aligns its figure, and with only
70
+ * `className` (the wrapper) it reached the element through `[&_input]:text-end` — a
71
+ * descendant selector that depends on the field's inner markup, which the kit is free
72
+ * to change, and that a reader of the call site has to decode. {@link MoneyField}
73
+ * passes it through, and `RhfMoneyField` takes it too.
74
+ */
75
+ inputClassName?: string;
40
76
  id?: string;
41
77
  ariaLabel?: string;
42
78
  /**
@@ -224,51 +260,12 @@ function settleAmount(text: string, digits: number | undefined, min?: number, ma
224
260
  return Number(out) === Number(t) ? text : out;
225
261
  }
226
262
 
227
- /**
228
- * The locale's decimal mark, as far as this field can type it: "," or "." (kastlan 40).
229
- * The same answer `NumberField` gives, for the same reason: a mark outside those two
230
- * (Arabic's "٫") is not one {@link sanitizeLive} lets through, so those locales keep
231
- * the dot rather than get a field that eats the key it shows.
232
- */
233
- function decimalMark(locale: string | undefined): "," | "." {
234
- try {
235
- const part = new Intl.NumberFormat(locale).formatToParts(1.5).find((p) => p.type === "decimal");
236
- return part?.value === "," ? "," : ".";
237
- } catch {
238
- // An invalid tag throws a RangeError; a typo in a locale must not take the form down.
239
- return ".";
240
- }
241
- }
263
+ // The locale's decimal mark and the grouping parse (kastlan 40, keksdose G1) live in
264
+ // `lib/decimal-marks.ts`, shared with NumberInput since 0.22 (keksdose K5).
242
265
 
243
- /**
244
- * Which typed mark is the decimal one (keksdose G1). {@link sanitizeLive} folds every
245
- * "," into ".", so on its own it read the German "1.234,56" as 1.23456 — and the
246
- * settle then made that 1.23, final. This decides per operand, from the typist's own
247
- * text, before that fold:
248
- *
249
- * - both marks present: the one that is not the locale's is grouping, and goes;
250
- * - in a "," locale, only "." present: grouping when it cannot be a decimal as a
251
- * German reader writes it — twice ("1.234.567"), or in thousands shape ("1.234",
252
- * "12.500") — otherwise a decimal ("1.5", typed by a dot-decimal habit);
253
- * - in a "." locale, only "," present: grouping when it appears twice ("1,234,567");
254
- * once it stays a decimal, because Swiss and German typists write "1,5" in a
255
- * de-CH form (kastlan 40) and a unit price "1,789" is a decimal.
256
- *
257
- * Only a keystroke's text comes through here. A calculator result is dot-decimal
258
- * already, and "1.234" from `100/81.03…` is never a thousand.
259
- */
260
- function normalizeTypedMarks(raw: string, mark: "," | "."): string {
261
- const other = mark === "," ? "." : ",";
262
- return raw.replace(/[0-9.,]+/g, (operand) => {
263
- const hasMark = operand.includes(mark);
264
- const count = operand.split(other).length - 1;
265
- if (count === 0) return operand;
266
- const grouping = hasMark
267
- ? true
268
- : count > 1 || (mark === "," && /^\d{1,3}(\.\d{3})+$/.test(operand));
269
- return grouping ? operand.split(other).join("") : operand;
270
- });
271
- }
266
+ // The error line under a field — the type of `ui.tsx`'s (module-private) one, so an
267
+ // AmountInput's message is indistinguishable from an Input's.
268
+ const FIELD_ERROR_CLASS = "mt-1 text-[11px] leading-tight text-[var(--danger)]";
272
269
 
273
270
  // The consuming app's ONE money palette (`--money-expense` / `--money-income`),
274
271
  // not a bespoke rose/emerald pairing: a figure being typed has to wear the same
@@ -311,20 +308,23 @@ function isResultOf(previous: string, text: string, settle: (text: string) => st
311
308
  }
312
309
 
313
310
  export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
314
- ({ value, onChange, currency, onCurrencyChange, placeholder, label, disabled, invalid: invalidProp, className, id, ariaLabel, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid, "aria-required": ariaRequired, hint, autoFocus, tone = "neutral", negative = false, onNegativeChange, variant = "field", align = "start", labels, currencyNames, digits: digitsProp, min, max, onCommit }, ref) => {
311
+ ({ value, onChange, currency, onCurrencyChange, placeholder, label, disabled, invalid: invalidProp, error, calculator = true, className, inputClassName, id, ariaLabel, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid, "aria-required": ariaRequired, hint, autoFocus, tone = "neutral", negative = false, onNegativeChange, variant = "field", align = "start", labels, currencyNames, digits: digitsProp, min, max, onCommit }, ref) => {
315
312
  const generatedId = useId();
316
313
  const fieldId = id ?? generatedId;
317
314
  const hintId = useId();
315
+ const errorId = useId();
318
316
  // Text is a caption under the field; a FieldHint rides the label line. See `hint`.
319
317
  const textHint = (typeof hint === "string" && hint !== "") || typeof hint === "number";
320
- const describedBy = textHint ? (ariaDescribedBy ? `${ariaDescribedBy} ${hintId}` : hintId) : ariaDescribedBy;
321
- const invalid = Boolean(invalidProp) || ariaInvalid === true || ariaInvalid === "true";
318
+ const hasError = hasMessage(error);
319
+ // The standing advice first, the news second: the caller's ids, the caption, the error.
320
+ const describedBy = mergeDescribedBy(ariaDescribedBy, textHint && hintId, hasError && errorId);
321
+ const invalid = Boolean(invalidProp) || hasError || ariaInvalid === true || ariaInvalid === "true";
322
322
  const editable = !!onCurrencyChange;
323
323
  // On phones we suppress the OS keyboard (inputMode="none" below) and show our
324
324
  // own calculator numpad, so the desktop popover trigger is hidden. The
325
325
  // end padding keys off showCalc, so it tightens up automatically.
326
326
  const isMobile = useMediaQuery(PHONE_QUERY, false);
327
- const showCalc = !disabled && !isMobile;
327
+ const showCalc = calculator && !disabled && !isMobile;
328
328
  const [focused, setFocused] = useState(false);
329
329
  // On mobile, focusing the field opens the numpad bottom sheet in place of the
330
330
  // native keyboard (feedback #334) — a full calculator keypad, not just the old
@@ -381,15 +381,17 @@ export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
381
381
  // decimals (keksdose live #356). Display only — `value` is the host's, untouched.
382
382
  const displayDigits = digitsProp ?? currencyMinorDigits(currency);
383
383
  const resting = focused ? shown : settleAmount(shown, displayDigits);
384
- const derived = mark === "," ? resting.replace(/\./g, ",") : resting;
385
- const draftAs = draft === null ? "" : sanitizeLive(normalizeTypedMarks(draft, mark));
384
+ const derived = localizeMark(resting, mark);
385
+ // The draft only while it says what the field holds AND holds nothing the live
386
+ // sanitising would drop — a letter, a second decimal point — which would otherwise
387
+ // stay on screen while the value said something else (see `showsTyped`).
386
388
  const display =
387
389
  draft === null
388
390
  ? derived
389
- : draftAs === shown
391
+ : showsTyped(draft, mark, shown)
390
392
  ? draft
391
393
  : // The caller owns the sign: the typed "12" is shown as the "-12" it is.
392
- signOwned && negative && `-${draftAs}` === shown
394
+ signOwned && negative && shown.startsWith("-") && showsTyped(draft, mark, shown.slice(1))
393
395
  ? `-${draft}`
394
396
  : derived;
395
397
  // Every route into the field — typing, the numpad sheet, the desktop
@@ -434,7 +436,7 @@ export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
434
436
  };
435
437
  const typed = (raw: string) => {
436
438
  setDraft(raw);
437
- handleText(normalizeTypedMarks(raw, mark));
439
+ handleText(readTyped(raw, mark));
438
440
  };
439
441
  const { open, setOpen, wrapperRef, panelRef, query, setQuery, inputRef } = useDropdownSearch();
440
442
  // The chip the currency list hangs off (Keksdose dev#548). It is portalled now, so
@@ -519,6 +521,8 @@ export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
519
521
  // (feedback #314). FLOATING_INPUT_CLASS already bundles the peer +
520
522
  // transparent-placeholder bits.
521
523
  asDisplay ? DISPLAY_INPUT_CLASS : label !== undefined ? FLOATING_INPUT_CLASS : FIELD_BASE,
524
+ // The caller's, over the base and under everything below (see `inputClassName`).
525
+ inputClassName,
522
526
  // Room for the trailing controls. showCalc is always false on a phone,
523
527
  // so the display shape only ever has to clear the currency chip — and
524
528
  // it clears it by the chip's actual width (a text-sm code plus a
@@ -565,9 +569,10 @@ export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
565
569
  >
566
570
  {showCalc && (
567
571
  <CalculatorButton
568
- // The mark the field shows, as NumberInput hands its calculator; the
569
- // evaluator reads either, and its result comes back dot-decimal.
570
- value={display}
572
+ // The mark the field shows but not the typist's grouping: the evaluator
573
+ // reads either mark, and "1.234,5" is two of them. The result comes back
574
+ // dot-decimal.
575
+ value={derived}
571
576
  onChange={(result, expression) => {
572
577
  setDraft(null);
573
578
  const settled = settle(result);
@@ -660,6 +665,13 @@ export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
660
665
  {hint}
661
666
  </p>
662
667
  )}
668
+ {/* The news after the standing advice, as Select orders them. The root is
669
+ already a steady box, so the message coming and going remounts nothing. */}
670
+ {hasError && (
671
+ <p id={errorId} className={FIELD_ERROR_CLASS}>
672
+ {error}
673
+ </p>
674
+ )}
663
675
  {showNumpad && (
664
676
  <NumberPadSheet
665
677
  // Localised like the field it mirrors. Keys are typing, read by the