@eifi1/ui-kit 0.20.1 → 0.22.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 (286) hide show
  1. package/dist/chart.d.ts +10 -8
  2. package/dist/components/account-chips.d.ts +10 -3
  3. package/dist/components/account-chips.js +5 -2
  4. package/dist/components/account-chips.js.map +1 -1
  5. package/dist/components/account-settings.d.ts +33 -2
  6. package/dist/components/account-settings.js +43 -21
  7. package/dist/components/account-settings.js.map +1 -1
  8. package/dist/components/amount-input.d.ts +27 -8
  9. package/dist/components/amount-input.js +15 -28
  10. package/dist/components/amount-input.js.map +1 -1
  11. package/dist/components/autocomplete.d.ts +8 -0
  12. package/dist/components/autocomplete.js +9 -4
  13. package/dist/components/autocomplete.js.map +1 -1
  14. package/dist/components/button-group.d.ts +10 -8
  15. package/dist/components/calculator.d.ts +10 -8
  16. package/dist/components/checkbox-group.d.ts +104 -0
  17. package/dist/components/checkbox-group.js +111 -0
  18. package/dist/components/checkbox-group.js.map +1 -0
  19. package/dist/components/checkbox.d.ts +19 -0
  20. package/dist/components/checkbox.js +30 -7
  21. package/dist/components/checkbox.js.map +1 -1
  22. package/dist/components/chip.d.ts +18 -0
  23. package/dist/components/chip.js +47 -10
  24. package/dist/components/chip.js.map +1 -1
  25. package/dist/components/combobox.d.ts +13 -2
  26. package/dist/components/combobox.js +17 -8
  27. package/dist/components/combobox.js.map +1 -1
  28. package/dist/components/confirm-dialog.d.ts +66 -161
  29. package/dist/components/copy-button.d.ts +10 -8
  30. package/dist/components/country-select.d.ts +124 -0
  31. package/dist/components/country-select.js +237 -0
  32. package/dist/components/country-select.js.map +1 -0
  33. package/dist/components/currency-select.d.ts +17 -1
  34. package/dist/components/currency-select.js +56 -23
  35. package/dist/components/currency-select.js.map +1 -1
  36. package/dist/components/danger-confirm.d.ts +66 -166
  37. package/dist/components/danger-confirm.js +133 -49
  38. package/dist/components/danger-confirm.js.map +1 -1
  39. package/dist/components/data-table-cells.d.ts +1 -2
  40. package/dist/components/data-table-filter-popover.d.ts +1 -2
  41. package/dist/components/data-table-filters.d.ts +1 -2
  42. package/dist/components/data-table-labels.d.ts +4 -97
  43. package/dist/components/data-table-labels.js +9 -1
  44. package/dist/components/data-table-labels.js.map +1 -1
  45. package/dist/components/data-table-pagination.d.ts +3 -1
  46. package/dist/components/data-table.d.ts +1 -2
  47. package/dist/components/data-table.js +98 -3
  48. package/dist/components/data-table.js.map +1 -1
  49. package/dist/components/date-picker.d.ts +25 -5
  50. package/dist/components/date-picker.js +110 -59
  51. package/dist/components/date-picker.js.map +1 -1
  52. package/dist/components/entity-combobox.d.ts +8 -1
  53. package/dist/components/entity-combobox.js +9 -4
  54. package/dist/components/entity-combobox.js.map +1 -1
  55. package/dist/components/facing-pair.d.ts +10 -8
  56. package/dist/components/field-anatomy.d.ts +95 -0
  57. package/dist/components/field-anatomy.js +84 -0
  58. package/dist/components/field-anatomy.js.map +1 -0
  59. package/dist/components/field-parts.d.ts +114 -0
  60. package/dist/components/field-parts.js +72 -0
  61. package/dist/components/field-parts.js.map +1 -0
  62. package/dist/components/file-button.d.ts +10 -8
  63. package/dist/components/file-button.js +13 -3
  64. package/dist/components/file-button.js.map +1 -1
  65. package/dist/components/file-dropzone.d.ts +32 -9
  66. package/dist/components/file-dropzone.js +57 -34
  67. package/dist/components/file-dropzone.js.map +1 -1
  68. package/dist/components/form-actions.d.ts +10 -8
  69. package/dist/components/form-actions.js +61 -3
  70. package/dist/components/form-actions.js.map +1 -1
  71. package/dist/components/hover-menu.d.ts +3 -0
  72. package/dist/components/hover-menu.js +2 -2
  73. package/dist/components/hover-menu.js.map +1 -1
  74. package/dist/components/iban-input.d.ts +66 -0
  75. package/dist/components/iban-input.js +125 -0
  76. package/dist/components/iban-input.js.map +1 -0
  77. package/dist/components/icon-picker.d.ts +12 -2
  78. package/dist/components/icon-picker.js +9 -2
  79. package/dist/components/icon-picker.js.map +1 -1
  80. package/dist/components/inline-edit-field.d.ts +167 -0
  81. package/dist/components/inline-edit-field.js +293 -0
  82. package/dist/components/inline-edit-field.js.map +1 -0
  83. package/dist/components/language-select.d.ts +127 -0
  84. package/dist/components/language-select.js +18 -0
  85. package/dist/components/language-select.js.map +1 -0
  86. package/dist/components/line-items.d.ts +25 -3
  87. package/dist/components/line-items.js +55 -46
  88. package/dist/components/line-items.js.map +1 -1
  89. package/dist/components/list.d.ts +25 -0
  90. package/dist/components/list.js +16 -5
  91. package/dist/components/list.js.map +1 -1
  92. package/dist/components/money-field.d.ts +20 -8
  93. package/dist/components/money-field.js.map +1 -1
  94. package/dist/components/month-picker.d.ts +52 -4
  95. package/dist/components/month-picker.js +290 -86
  96. package/dist/components/month-picker.js.map +1 -1
  97. package/dist/components/multi-select.d.ts +8 -1
  98. package/dist/components/multi-select.js +9 -4
  99. package/dist/components/multi-select.js.map +1 -1
  100. package/dist/components/number-field.d.ts +25 -13
  101. package/dist/components/number-field.js +6 -20
  102. package/dist/components/number-field.js.map +1 -1
  103. package/dist/components/number-input.d.ts +70 -13
  104. package/dist/components/number-input.js +90 -38
  105. package/dist/components/number-input.js.map +1 -1
  106. package/dist/components/numpad-sheet.d.ts +10 -8
  107. package/dist/components/one-time-code-input.d.ts +100 -0
  108. package/dist/components/one-time-code-input.js +137 -0
  109. package/dist/components/one-time-code-input.js.map +1 -0
  110. package/dist/components/phone-input.d.ts +66 -0
  111. package/dist/components/phone-input.js +171 -0
  112. package/dist/components/phone-input.js.map +1 -0
  113. package/dist/components/series-chart.d.ts +10 -8
  114. package/dist/components/settings-fields.d.ts +10 -8
  115. package/dist/components/share-card.d.ts +8 -6
  116. package/dist/components/sign-chip.d.ts +66 -0
  117. package/dist/components/sign-chip.js +39 -0
  118. package/dist/components/sign-chip.js.map +1 -0
  119. package/dist/components/swatch-picker.d.ts +27 -2
  120. package/dist/components/swatch-picker.js +73 -31
  121. package/dist/components/swatch-picker.js.map +1 -1
  122. package/dist/components/switch.d.ts +19 -0
  123. package/dist/components/switch.js +24 -6
  124. package/dist/components/switch.js.map +1 -1
  125. package/dist/components/text-link.d.ts +10 -8
  126. package/dist/components/tile-radio.d.ts +68 -13
  127. package/dist/components/tile-radio.js +3 -3
  128. package/dist/components/tile-radio.js.map +1 -1
  129. package/dist/components/time-input.d.ts +15 -9
  130. package/dist/components/time-input.js.map +1 -1
  131. package/dist/components/toggle-group.d.ts +47 -1
  132. package/dist/components/toggle-group.js +110 -69
  133. package/dist/components/toggle-group.js.map +1 -1
  134. package/dist/components/ui.d.ts +10 -8
  135. package/dist/components/ui.js +280 -78
  136. package/dist/components/ui.js.map +1 -1
  137. package/dist/components/use-table-state.d.ts +1 -2
  138. package/dist/{data-table-3mi2R3so.d.ts → data-table-labels-B7OdnM0S.d.ts} +140 -3
  139. package/dist/data-table.d.ts +1 -2
  140. package/dist/data-table.js +2 -1
  141. package/dist/data-table.js.map +1 -1
  142. package/dist/feedback/feedback-attachment.d.ts +1 -1
  143. package/dist/feedback/feedback-attachment.js +210 -3
  144. package/dist/feedback/feedback-attachment.js.map +1 -1
  145. package/dist/feedback/feedback-dialog.d.ts +1 -1
  146. package/dist/feedback/feedback-dialog.js.map +1 -1
  147. package/dist/feedback/feedback-inbox.d.ts +1 -1
  148. package/dist/feedback/feedback-thread.d.ts +37 -3
  149. package/dist/feedback/feedback-thread.js +38 -9
  150. package/dist/feedback/feedback-thread.js.map +1 -1
  151. package/dist/{feedback-attachment-WFiXJ8We.d.ts → feedback-attachment-NhUm7Zga.d.ts} +70 -5
  152. package/dist/feedback.d.ts +2 -2
  153. package/dist/hooks/use-file-drop.d.ts +10 -8
  154. package/dist/i18n/defaults.d.ts +10 -8
  155. package/dist/i18n/defaults.js +17 -2
  156. package/dist/i18n/defaults.js.map +1 -1
  157. package/dist/i18n/german.d.ts +10 -8
  158. package/dist/i18n/german.js +50 -5
  159. package/dist/i18n/german.js.map +1 -1
  160. package/dist/i18n/kit-labels.d.ts +10 -8
  161. package/dist/i18n/kit-labels.js +8 -2
  162. package/dist/i18n/kit-labels.js.map +1 -1
  163. package/dist/i18n/languages.d.ts +10 -8
  164. package/dist/i18n/locales/de-CH.d.ts +10 -8
  165. package/dist/i18n/locales/en.d.ts +10 -8
  166. package/dist/i18n/locales/en.js +9 -0
  167. package/dist/i18n/locales/en.js.map +1 -1
  168. package/dist/i18n/locales/es.d.ts +10 -8
  169. package/dist/i18n/locales/es.js +48 -5
  170. package/dist/i18n/locales/es.js.map +1 -1
  171. package/dist/i18n/locales/fr.d.ts +10 -8
  172. package/dist/i18n/locales/fr.js +48 -5
  173. package/dist/i18n/locales/fr.js.map +1 -1
  174. package/dist/i18n/locales/hu.d.ts +10 -8
  175. package/dist/i18n/locales/hu.js +49 -5
  176. package/dist/i18n/locales/hu.js.map +1 -1
  177. package/dist/i18n/locales/it.d.ts +10 -8
  178. package/dist/i18n/locales/it.js +49 -5
  179. package/dist/i18n/locales/it.js.map +1 -1
  180. package/dist/i18n/locales/zh.d.ts +10 -8
  181. package/dist/i18n/locales/zh.js +48 -5
  182. package/dist/i18n/locales/zh.js.map +1 -1
  183. package/dist/i18n/review.d.ts +10 -8
  184. package/dist/i18n/review.js +8 -1
  185. package/dist/i18n/review.js.map +1 -1
  186. package/dist/index.d.ts +17 -10
  187. package/dist/index.js +57 -2
  188. package/dist/index.js.map +1 -1
  189. package/dist/{kit-labels-D1J2_jyD.d.ts → kit-labels-R8mIAc9N.d.ts} +811 -8
  190. package/dist/lib/countries.d.ts +43 -0
  191. package/dist/lib/countries.js +39 -0
  192. package/dist/lib/countries.js.map +1 -0
  193. package/dist/lib/decimal-marks.d.ts +46 -0
  194. package/dist/lib/decimal-marks.js +40 -0
  195. package/dist/lib/decimal-marks.js.map +1 -0
  196. package/dist/lib/iban.d.ts +98 -0
  197. package/dist/lib/iban.js +154 -0
  198. package/dist/lib/iban.js.map +1 -0
  199. package/dist/lib/isin.d.ts +30 -0
  200. package/dist/lib/isin.js +36 -0
  201. package/dist/lib/isin.js.map +1 -0
  202. package/dist/lib/phone.d.ts +69 -0
  203. package/dist/lib/phone.js +121 -0
  204. package/dist/lib/phone.js.map +1 -0
  205. package/dist/rhf/fields.d.ts +148 -11
  206. package/dist/rhf/fields.js +332 -3
  207. package/dist/rhf/fields.js.map +1 -1
  208. package/dist/rhf/form.d.ts +10 -8
  209. package/dist/rhf.d.ts +13 -9
  210. package/dist/rhf.js.map +1 -1
  211. package/dist/shell/app-shell.d.ts +10 -8
  212. package/dist/shell/top-bar-brand.d.ts +10 -8
  213. package/dist/shell/topbar-action-menu.d.ts +13 -1
  214. package/dist/shell/topbar-action-menu.js +31 -28
  215. package/dist/shell/topbar-action-menu.js.map +1 -1
  216. package/dist/shell.d.ts +10 -8
  217. package/dist/tour/tour.js +20 -24
  218. package/dist/tour/tour.js.map +1 -1
  219. package/dist/wizard/stepper-nav.d.ts +10 -8
  220. package/dist/wizard.d.ts +10 -8
  221. package/package.json +1 -1
  222. package/src/components/account-chips.tsx +17 -3
  223. package/src/components/account-settings.tsx +92 -26
  224. package/src/components/amount-input.tsx +49 -56
  225. package/src/components/autocomplete.tsx +133 -110
  226. package/src/components/checkbox-group.tsx +229 -0
  227. package/src/components/checkbox.tsx +72 -8
  228. package/src/components/chip.tsx +80 -10
  229. package/src/components/combobox.tsx +292 -253
  230. package/src/components/country-select.tsx +443 -0
  231. package/src/components/currency-select.tsx +109 -33
  232. package/src/components/danger-confirm.tsx +345 -77
  233. package/src/components/data-table-labels.ts +16 -1
  234. package/src/components/data-table.tsx +175 -6
  235. package/src/components/date-picker.tsx +173 -62
  236. package/src/components/entity-combobox.tsx +107 -87
  237. package/src/components/field-anatomy.tsx +190 -0
  238. package/src/components/field-parts.tsx +184 -0
  239. package/src/components/file-button.tsx +36 -3
  240. package/src/components/file-dropzone.tsx +106 -38
  241. package/src/components/form-actions.tsx +115 -2
  242. package/src/components/hover-menu.tsx +7 -4
  243. package/src/components/iban-input.tsx +231 -0
  244. package/src/components/icon-picker.tsx +26 -4
  245. package/src/components/inline-edit-field.tsx +533 -0
  246. package/src/components/language-select.tsx +78 -0
  247. package/src/components/line-items.tsx +35 -2
  248. package/src/components/list.tsx +55 -6
  249. package/src/components/money-field.tsx +10 -0
  250. package/src/components/month-picker.tsx +399 -63
  251. package/src/components/multi-select.tsx +80 -58
  252. package/src/components/number-field.tsx +28 -46
  253. package/src/components/number-input.tsx +210 -43
  254. package/src/components/one-time-code-input.tsx +288 -0
  255. package/src/components/phone-input.tsx +299 -0
  256. package/src/components/sign-chip.tsx +103 -0
  257. package/src/components/swatch-picker.tsx +118 -30
  258. package/src/components/switch.tsx +58 -7
  259. package/src/components/tile-radio.tsx +71 -16
  260. package/src/components/time-input.tsx +8 -1
  261. package/src/components/toggle-group.tsx +206 -73
  262. package/src/components/ui.tsx +472 -105
  263. package/src/data-table.ts +2 -1
  264. package/src/feedback/feedback-attachment.tsx +313 -9
  265. package/src/feedback/feedback-dialog.tsx +2 -0
  266. package/src/feedback/feedback-thread.tsx +66 -5
  267. package/src/i18n/defaults.ts +16 -1
  268. package/src/i18n/german.ts +47 -0
  269. package/src/i18n/kit-labels.tsx +78 -2
  270. package/src/i18n/locales/en.ts +10 -0
  271. package/src/i18n/locales/es.ts +46 -0
  272. package/src/i18n/locales/fr.ts +45 -0
  273. package/src/i18n/locales/hu.ts +46 -0
  274. package/src/i18n/locales/it.ts +47 -0
  275. package/src/i18n/locales/zh.ts +44 -0
  276. package/src/i18n/review.ts +7 -0
  277. package/src/index.ts +53 -3
  278. package/src/lib/countries.ts +91 -0
  279. package/src/lib/decimal-marks.ts +99 -0
  280. package/src/lib/iban.ts +171 -0
  281. package/src/lib/isin.ts +62 -0
  282. package/src/lib/phone.ts +211 -0
  283. package/src/rhf/fields.tsx +606 -9
  284. package/src/rhf.ts +2 -1
  285. package/src/shell/topbar-action-menu.tsx +53 -29
  286. package/src/tour/tour.tsx +66 -28
package/dist/wizard.d.ts CHANGED
@@ -6,8 +6,10 @@ export { WizardStep, WizardStepProps } from './wizard/wizard-step.js';
6
6
  export { WizardSummary } from './wizard/wizard-summary.js';
7
7
  export { StepperNav } from './wizard/stepper-nav.js';
8
8
  import 'react';
9
- import './kit-labels-D1J2_jyD.js';
10
- import './components/data-table-labels.js';
9
+ import './kit-labels-R8mIAc9N.js';
10
+ import './data-table-labels-B7OdnM0S.js';
11
+ import './components/data-table-sort.js';
12
+ import './components/swipeable-row.js';
11
13
  import './components/mini-calendar.js';
12
14
  import './components/calendar-heatmap.js';
13
15
  import './components/popover.js';
@@ -29,7 +31,6 @@ import './components/sparkline.js';
29
31
  import './components/stat-tile.js';
30
32
  import './components/signature-pad.js';
31
33
  import './components/password-strength.js';
32
- import './components/danger-confirm.js';
33
34
  import './components/swatch-picker.js';
34
35
  import './components/tile-radio.js';
35
36
  import './components/icon-picker.js';
@@ -37,11 +38,10 @@ import './components/dialog-frame.js';
37
38
  import './components/modal.js';
38
39
  import './hooks/use-search-param-state.js';
39
40
  import './components/measured-grid.js';
40
- import './feedback-attachment-WFiXJ8We.js';
41
+ import './feedback-attachment-NhUm7Zga.js';
41
42
  import './feedback/feedback-thread.js';
42
43
  import './feedback/feedback-inbox.js';
43
44
  import './components/account-settings-labels.js';
44
- import './components/confirm-dialog.js';
45
45
  import './components/floating-panel.js';
46
46
  import './components/bulk-action-bar.js';
47
47
  import './components/list.js';
@@ -60,11 +60,13 @@ import './components/write-lock.js';
60
60
  import './components/legal.js';
61
61
  import './components/account-chips.js';
62
62
  import './lib/format.js';
63
- import './data-table-3mi2R3so.js';
64
- import './components/data-table-sort.js';
65
- import './components/swipeable-row.js';
66
63
  import './components/reauth-dialog.js';
67
64
  import './components/server-wake.js';
68
65
  import './lib/server-wake.js';
69
66
  import './components/translation-review-labels.js';
70
67
  import './lib/translation-review.js';
68
+ import './components/country-select.js';
69
+ import './components/inline-edit-field.js';
70
+ import './lib/iban.js';
71
+ import './lib/phone.js';
72
+ import './components/sign-chip.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eifi1/ui-kit",
3
- "version": "0.20.1",
3
+ "version": "0.22.0",
4
4
  "type": "module",
5
5
  "description": "App-agnostic shared design system: domain-free React components, theme and tokens.",
6
6
  "_sideEffects": "The barrel (src/index.ts) re-exports EVERY component, including components/chart, which statically imports recharts. Without this field a bundler must assume each of those modules has side effects, so it cannot drop the unused ones: importing a Button from '@eifi1/ui-kit' dragged all 433KB of recharts into the consumer's ENTRY chunk, and Keksdose was preloading it on every page load, login included. Nothing in src/ imports for side effects (no bare imports, no CSS imports from JS, no module-level global writes) — the token stylesheet is its own './tokens.css' export — so this is safe. CSS is still listed so any future stylesheet survives. Re-checked when `flag-icons` left `dependencies` and tsup started emitting a `\"use client\"` banner: neither touches this. The flag stylesheet was never imported from JS in the first place (that was the finding), and a directive prologue is a string literal every bundler lifts out before it decides whether a module is inert.",
@@ -3,7 +3,8 @@ import type { LucideIcon } from "lucide-react";
3
3
  import { cn } from "../lib/cn";
4
4
  import { formatDate, formatRelativeTime, toDate, EMPTY_FORMATTED_VALUE } from "../lib/format";
5
5
  import type { DateInput, FormatDateStyle, FormatRelativeTimeOptions } from "../lib/format";
6
- import { useKitLabels, useKitLocale } from "../i18n/kit-labels";
6
+ import { useKitDateFormatter, useKitLabels, useKitLocale } from "../i18n/kit-labels";
7
+ import { toLocalIso } from "../lib/dates";
7
8
  import { Chip } from "./chip";
8
9
  import type { ChipShape, ChipSize, ChipTone, ChipVariant } from "./chip";
9
10
  import { Tooltip } from "./tooltip";
@@ -201,7 +202,8 @@ export interface DateMarkProps {
201
202
  */
202
203
  display?: "date" | "relative";
203
204
  /** Default `medium`. Under `"relative"`, the style of the tooltip's date (default
204
- * `dateTime`) and of the date `relative.absoluteAfterDays` switches to. */
205
+ * `dateTime`) and of the date `relative.absoluteAfterDays` switches to. Given, it wins
206
+ * over the provider's `formatDate`. */
205
207
  dateStyle?: FormatDateStyle | Intl.DateTimeFormatOptions;
206
208
  /** `formatRelativeTime` options: `numeric`, `style`, `absoluteAfterDays`, `now`. */
207
209
  relative?: Omit<FormatRelativeTimeOptions, "locale" | "empty" | "absoluteStyle">;
@@ -216,6 +218,13 @@ export interface DateMarkProps {
216
218
  /**
217
219
  * A date in a table cell: a `<time>` with a machine-readable `dateTime`, nowrap and
218
220
  * tabular figures, in the provider's locale — or `empty` when there is none.
221
+ *
222
+ * `display="date"` without a `dateStyle` is written by `<UiKitProvider formatDate>` when
223
+ * the app set one (keksdose K12: its `DateCell` exists to put every column's date in the
224
+ * app's date-format preference, weekday and all — dev#546), called with the LOCAL
225
+ * calendar day of the value as `"YYYY-MM-DD"` and `weekday: true`. Without one, or for
226
+ * an empty answer, it is `Intl`'s `medium` date as before. The relative display is not
227
+ * a calendar day and keeps `formatRelativeTime`.
219
228
  */
220
229
  export function DateMark({
221
230
  value,
@@ -227,13 +236,18 @@ export function DateMark({
227
236
  className,
228
237
  }: DateMarkProps) {
229
238
  const locale = useKitLocale(localeProp);
239
+ const fromProvider = useKitDateFormatter();
230
240
  const date = toDate(value);
231
241
  const cls = cn("whitespace-nowrap tabular-nums", className);
232
242
  if (!date) return <span className={cn(cls, "text-[var(--text-muted)]")}>{empty}</span>;
233
243
  if (display === "date") {
244
+ const own =
245
+ dateStyle === undefined && fromProvider
246
+ ? fromProvider(toLocalIso(date), { unit: "day", source: "dateMark", locale, weekday: true })
247
+ : "";
234
248
  return (
235
249
  <time dateTime={date.toISOString()} className={cls}>
236
- {formatDate(date, dateStyle ?? "medium", { locale })}
250
+ {own || formatDate(date, dateStyle ?? "medium", { locale })}
237
251
  </time>
238
252
  );
239
253
  }
@@ -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,6 +38,23 @@ 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;
39
58
  className?: string;
40
59
  id?: string;
41
60
  ariaLabel?: string;
@@ -224,51 +243,12 @@ function settleAmount(text: string, digits: number | undefined, min?: number, ma
224
243
  return Number(out) === Number(t) ? text : out;
225
244
  }
226
245
 
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
- }
246
+ // The locale's decimal mark and the grouping parse (kastlan 40, keksdose G1) live in
247
+ // `lib/decimal-marks.ts`, shared with NumberInput since 0.22 (keksdose K5).
242
248
 
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
- }
249
+ // The error line under a field — the type of `ui.tsx`'s (module-private) one, so an
250
+ // AmountInput's message is indistinguishable from an Input's.
251
+ const FIELD_ERROR_CLASS = "mt-1 text-[11px] leading-tight text-[var(--danger)]";
272
252
 
273
253
  // The consuming app's ONE money palette (`--money-expense` / `--money-income`),
274
254
  // not a bespoke rose/emerald pairing: a figure being typed has to wear the same
@@ -311,20 +291,23 @@ function isResultOf(previous: string, text: string, settle: (text: string) => st
311
291
  }
312
292
 
313
293
  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) => {
294
+ ({ value, onChange, currency, onCurrencyChange, placeholder, label, disabled, invalid: invalidProp, error, calculator = true, 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) => {
315
295
  const generatedId = useId();
316
296
  const fieldId = id ?? generatedId;
317
297
  const hintId = useId();
298
+ const errorId = useId();
318
299
  // Text is a caption under the field; a FieldHint rides the label line. See `hint`.
319
300
  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";
301
+ const hasError = hasMessage(error);
302
+ // The standing advice first, the news second: the caller's ids, the caption, the error.
303
+ const describedBy = mergeDescribedBy(ariaDescribedBy, textHint && hintId, hasError && errorId);
304
+ const invalid = Boolean(invalidProp) || hasError || ariaInvalid === true || ariaInvalid === "true";
322
305
  const editable = !!onCurrencyChange;
323
306
  // On phones we suppress the OS keyboard (inputMode="none" below) and show our
324
307
  // own calculator numpad, so the desktop popover trigger is hidden. The
325
308
  // end padding keys off showCalc, so it tightens up automatically.
326
309
  const isMobile = useMediaQuery(PHONE_QUERY, false);
327
- const showCalc = !disabled && !isMobile;
310
+ const showCalc = calculator && !disabled && !isMobile;
328
311
  const [focused, setFocused] = useState(false);
329
312
  // On mobile, focusing the field opens the numpad bottom sheet in place of the
330
313
  // native keyboard (feedback #334) — a full calculator keypad, not just the old
@@ -381,15 +364,17 @@ export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
381
364
  // decimals (keksdose live #356). Display only — `value` is the host's, untouched.
382
365
  const displayDigits = digitsProp ?? currencyMinorDigits(currency);
383
366
  const resting = focused ? shown : settleAmount(shown, displayDigits);
384
- const derived = mark === "," ? resting.replace(/\./g, ",") : resting;
385
- const draftAs = draft === null ? "" : sanitizeLive(normalizeTypedMarks(draft, mark));
367
+ const derived = localizeMark(resting, mark);
368
+ // The draft only while it says what the field holds AND holds nothing the live
369
+ // sanitising would drop — a letter, a second decimal point — which would otherwise
370
+ // stay on screen while the value said something else (see `showsTyped`).
386
371
  const display =
387
372
  draft === null
388
373
  ? derived
389
- : draftAs === shown
374
+ : showsTyped(draft, mark, shown)
390
375
  ? draft
391
376
  : // The caller owns the sign: the typed "12" is shown as the "-12" it is.
392
- signOwned && negative && `-${draftAs}` === shown
377
+ signOwned && negative && shown.startsWith("-") && showsTyped(draft, mark, shown.slice(1))
393
378
  ? `-${draft}`
394
379
  : derived;
395
380
  // Every route into the field — typing, the numpad sheet, the desktop
@@ -434,7 +419,7 @@ export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
434
419
  };
435
420
  const typed = (raw: string) => {
436
421
  setDraft(raw);
437
- handleText(normalizeTypedMarks(raw, mark));
422
+ handleText(readTyped(raw, mark));
438
423
  };
439
424
  const { open, setOpen, wrapperRef, panelRef, query, setQuery, inputRef } = useDropdownSearch();
440
425
  // The chip the currency list hangs off (Keksdose dev#548). It is portalled now, so
@@ -565,9 +550,10 @@ export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
565
550
  >
566
551
  {showCalc && (
567
552
  <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}
553
+ // The mark the field shows but not the typist's grouping: the evaluator
554
+ // reads either mark, and "1.234,5" is two of them. The result comes back
555
+ // dot-decimal.
556
+ value={derived}
571
557
  onChange={(result, expression) => {
572
558
  setDraft(null);
573
559
  const settled = settle(result);
@@ -660,6 +646,13 @@ export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
660
646
  {hint}
661
647
  </p>
662
648
  )}
649
+ {/* The news after the standing advice, as Select orders them. The root is
650
+ already a steady box, so the message coming and going remounts nothing. */}
651
+ {hasError && (
652
+ <p id={errorId} className={FIELD_ERROR_CLASS}>
653
+ {error}
654
+ </p>
655
+ )}
663
656
  {showNumpad && (
664
657
  <NumberPadSheet
665
658
  // Localised like the field it mirrors. Keys are typing, read by the