@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
package/src/index.ts CHANGED
@@ -111,8 +111,27 @@ export * from "./components/disclosure";
111
111
  export * from "./components/dialog-frame";
112
112
  // Named, not `*`: danger-confirm also holds the password field and pending hook that
113
113
  // ReauthDialog shares with it, which are kit-internal.
114
- export { DangerConfirm, DEFAULT_DANGER_CONFIRM_LABELS, typedMatches } from "./components/danger-confirm";
115
- export type { DangerConfirmLabels, DangerConfirmProps, TypedMatch } from "./components/danger-confirm";
114
+ export {
115
+ DangerConfirm,
116
+ DEFAULT_DANGER_CONFIRM_LABELS,
117
+ typedMatches,
118
+ // 0.22.0: the typed-confirmation field on its own (keksdose K7) and the current-password
119
+ // field kastlan wants for sign-in and re-authentication (kastlan 1).
120
+ TypedConfirmField,
121
+ CurrentPasswordInput,
122
+ } from "./components/danger-confirm";
123
+ export type {
124
+ DangerConfirmLabels,
125
+ DangerConfirmProps,
126
+ TypedMatch,
127
+ DangerConsequence,
128
+ TypedConfirmFieldProps,
129
+ TypedConfirmFieldLabels,
130
+ CurrentPasswordInputProps,
131
+ CurrentPasswordInputLabels,
132
+ // 0.23.0: onConfirm's second argument — the guards' values (keksdose G4b).
133
+ DangerConfirmValues,
134
+ } from "./components/danger-confirm";
116
135
  // 0.18: re-authentication before a sensitive action (Kurvenschmiede 4).
117
136
  export { ReauthDialog, DEFAULT_REAUTH_DIALOG_LABELS } from "./components/reauth-dialog";
118
137
  export type { ReauthDialogProps, ReauthDialogLabels } from "./components/reauth-dialog";
@@ -259,8 +278,9 @@ export * from "./components/data-table-pagination";
259
278
  export { FilterPopover } from "./components/data-table-filter-popover";
260
279
  // data-table re-exports SortState internally; export the rest explicitly to
261
280
  // avoid a duplicate SortState star-export (it comes from data-table-sort).
262
- export { DataTable } from "./components/data-table";
281
+ export { DataTable, DEFAULT_DATA_TABLE_SORT_LABELS } from "./components/data-table";
263
282
  export type {
283
+ DataTableSortLabels,
264
284
  DataTableColumn,
265
285
  DataTableCellProps,
266
286
  DataTableHeadProps,
@@ -406,3 +426,51 @@ export type {
406
426
  // The legal pages' shell (0.19, H10): Imprint, Privacy Policy, Terms in every app.
407
427
  export { LegalLayout, LegalSection, LegalLinks, DEFAULT_LEGAL_LABELS } from "./components/legal";
408
428
  export type { LegalLabels, LegalLink, LegalLinksProps, LegalSectionProps, LegalLayoutProps } from "./components/legal";
429
+
430
+ // ── 0.22.0: the inputs round (kastlan's, keksdose's and Kurvenschmiede's audits) ──
431
+ export * from "./components/checkbox-group";
432
+ export * from "./components/one-time-code-input";
433
+ export * from "./components/language-select";
434
+ export * from "./components/tile-radio";
435
+ export * from "./components/inline-edit-field";
436
+ export * from "./components/sign-chip";
437
+ export * from "./components/country-select";
438
+ export { COUNTRY_CODES, countryName } from "./lib/countries";
439
+ export * from "./components/iban-input";
440
+ export {
441
+ IBAN_LENGTHS,
442
+ compactIban,
443
+ formatIban,
444
+ ibanCheckDigits,
445
+ ibanProblem,
446
+ isValidIban,
447
+ isQrIban,
448
+ } from "./lib/iban";
449
+ export type { IbanKind, IbanProblem } from "./lib/iban";
450
+ export { formatIsin, isinCheckDigit, isValidIsin } from "./lib/isin";
451
+ export * from "./components/phone-input";
452
+ export {
453
+ PHONE_DIAL_CODES,
454
+ PHONE_COUNTRIES,
455
+ parsePhone,
456
+ formatNationalPhone,
457
+ formatPhone,
458
+ isE164,
459
+ } from "./lib/phone";
460
+ export type { PhoneCountryCode, PhoneCountry, ParsedPhone } from "./lib/phone";
461
+
462
+ // ── 0.23.0: the apps' 0.22 adoption round (kastlan, keksdose, Kurvenschmiede) ──
463
+ // The label strip over custom content (keksdose G8).
464
+ export * from "./components/field-strip";
465
+ // Paste or drop a table, give each column a role (Kurvenschmiede's columns input,
466
+ // keksdose's import map step). The lexer itself stays in `@eifi1/ui-kit/table-text`.
467
+ export * from "./components/column-mapper";
468
+ export {
469
+ assignColumnRole,
470
+ guessMapping,
471
+ missingRoles,
472
+ readMappedTable,
473
+ readTextFile,
474
+ roleOfColumn,
475
+ } from "./lib/column-mapping";
476
+ export type { ColumnMapperResult, ColumnMapping, ColumnRole } from "./lib/column-mapping";
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Which column of a table plays which part — the headless half of {@link ColumnMapper}.
3
+ *
4
+ * Two apps built this by hand, the same way. Kurvenschmiede's columns input
5
+ * (`features/control/columns-input.tsx`) reads a recorder's export and asks which
6
+ * column is the time, the setpoint and the actual; keksdose's bank-file import
7
+ * (`import-map-step.tsx`, `use-file-import.ts`) shows a bank's CSV and asks which
8
+ * column is the booking date, the amount, the payee. Both then have the same four
9
+ * rules, and keksdose wrote them down after getting each one wrong once:
10
+ *
11
+ * * **A role sits on one column at most, and a column carries one role.** Picking a
12
+ * role for a column MOVES it there from wherever it was ({@link assignColumnRole});
13
+ * the column it left falls back to "ignore" in the same step. There is no error
14
+ * state, because there is nothing wrong to report.
15
+ * * **What is still missing is named**, not left to a disabled button
16
+ * ({@link missingRoles}). A required role can be one of a group: a bank export that
17
+ * splits debit and credit has no amount column and needs none, so `required` may
18
+ * name a group, satisfied by any one of its members (keksdose's "money").
19
+ * * **A mapping is indexes into ONE table.** When the column count changes, the
20
+ * indexes mean nothing any more and are guessed afresh; when it does not (the
21
+ * header line toggled), they still point where they did — keksdose's `keepRoles`.
22
+ * * **The rows a role cannot read are reported by line**, not dropped in silence
23
+ * ({@link readMappedTable}): a footer under a recording, a total under a statement.
24
+ *
25
+ * Pure functions over plain data, so a two-step wizard can hold the mapping in its own
26
+ * state between steps, and a table the server sniffed (keksdose's) can be mapped with
27
+ * the same rules as one parsed here.
28
+ */
29
+ import { tableNumber } from "./table-text";
30
+ import type { TableLine, TextTable } from "./table-text";
31
+
32
+ /** One part a column can play, defined by the caller. */
33
+ export interface ColumnRole<R extends string = string> {
34
+ /** The key the mapping is stored under. */
35
+ value: R;
36
+ /** What the role select shows — translated by the caller. */
37
+ label: string;
38
+ /**
39
+ * `true`: the table cannot be used until a column plays this role. A string names a
40
+ * GROUP: the table needs any one of the roles that share it — keksdose's amount,
41
+ * debit and credit, any of which carries the money.
42
+ */
43
+ required?: boolean | string;
44
+ /**
45
+ * Every cell this role reads must be a number in the table's decimal convention
46
+ * ({@link tableNumber}). A row where one is not — a footer, a units line, a total
47
+ * with a grouping mark — is moved to `unread` with its line number instead of being
48
+ * handed over. Kurvenschmiede's time, setpoint and actual. The cells stay strings:
49
+ * converting is the caller's, with `tableNumber(cell, result.decimalComma)`.
50
+ */
51
+ numeric?: boolean;
52
+ }
53
+
54
+ /** Role → column index. A role that is absent or `null` sits on no column — so a
55
+ * mapping with nullable fields (keksdose's `CsvMapping`) is one as it stands. */
56
+ export type ColumnMapping<R extends string = string> = Partial<Record<R, number | null>>;
57
+
58
+ /** What the table is, read through a mapping. */
59
+ export interface ColumnMapperResult<R extends string = string> extends TextTable {
60
+ mapping: ColumnMapping<R>;
61
+ /** What the table still needs before it can be used: one entry per gap, each the
62
+ * roles any of which would fill it (one role, or a required group). */
63
+ missing: R[][];
64
+ /** Every required role placed, and at least one row read. */
65
+ complete: boolean;
66
+ }
67
+
68
+ const placedAt = <R extends string>(mapping: ColumnMapping<R>, role: R, width?: number): number | null => {
69
+ const column = mapping[role];
70
+ if (typeof column !== "number") return null;
71
+ return width === undefined || column < width ? column : null;
72
+ };
73
+
74
+ /** The role on this column, or `null` for an ignored one. */
75
+ export function roleOfColumn<R extends string>(
76
+ roles: readonly ColumnRole<R>[],
77
+ mapping: ColumnMapping<R>,
78
+ column: number,
79
+ ): R | null {
80
+ return roles.find((role) => mapping[role.value] === column)?.value ?? null;
81
+ }
82
+
83
+ /**
84
+ * Put `role` on `column` — or, with `null`, take the column's role off it.
85
+ *
86
+ * Whatever this column played before stops, and the role moves here from any other
87
+ * column, in one new mapping (keksdose's `assignColumn`: "both halves are patches to
88
+ * the same object"). Other fields of the mapping are kept as they are.
89
+ */
90
+ export function assignColumnRole<R extends string, M extends ColumnMapping<R>>(
91
+ roles: readonly ColumnRole<R>[],
92
+ mapping: M,
93
+ column: number,
94
+ role: R | null,
95
+ ): M {
96
+ const next = { ...mapping } as Record<string, unknown>;
97
+ for (const { value } of roles) {
98
+ if (mapping[value] === column) next[value] = null;
99
+ }
100
+ if (role !== null) next[role] = column;
101
+ return next as M;
102
+ }
103
+
104
+ /**
105
+ * The gaps: each required role with no column, and each required group none of whose
106
+ * roles has one. In the order of `roles`. A column index past `width` is no column.
107
+ */
108
+ export function missingRoles<R extends string>(
109
+ roles: readonly ColumnRole<R>[],
110
+ mapping: ColumnMapping<R>,
111
+ width?: number,
112
+ ): R[][] {
113
+ const gaps: R[][] = [];
114
+ const groups = new Set<string>();
115
+ for (const role of roles) {
116
+ if (role.required === true) {
117
+ if (placedAt(mapping, role.value, width) === null) gaps.push([role.value]);
118
+ } else if (typeof role.required === "string" && !groups.has(role.required)) {
119
+ groups.add(role.required);
120
+ const members = roles.filter((other) => other.required === role.required);
121
+ if (members.every((member) => placedAt(mapping, member.value, width) === null)) {
122
+ gaps.push(members.map((member) => member.value));
123
+ }
124
+ }
125
+ }
126
+ return gaps;
127
+ }
128
+
129
+ const normalised = (text: string) => text.trim().toLowerCase();
130
+
131
+ /**
132
+ * A first mapping for a table nobody has mapped yet.
133
+ *
134
+ * A column whose header IS a role's label or key gets that role. The required roles
135
+ * left over then take the free columns in order — Kurvenschmiede's "time, setpoint,
136
+ * actual are the first three columns", which is how a recorder writes them. Optional
137
+ * roles are not guessed by position: in a bank
138
+ * export of twelve columns, "the fourth role is the fourth column" is a wrong answer
139
+ * presented as a suggestion, and an "ignore" is the honest one.
140
+ */
141
+ export function guessMapping<R extends string>(
142
+ roles: readonly ColumnRole<R>[],
143
+ header: readonly string[] | null,
144
+ width: number,
145
+ ): ColumnMapping<R> {
146
+ const mapping: ColumnMapping<R> = {};
147
+ const taken = new Set<number>();
148
+ if (header) {
149
+ for (const role of roles) {
150
+ const names = [normalised(role.label), normalised(role.value)];
151
+ const column = header.findIndex((name, index) => !taken.has(index) && names.includes(normalised(name)));
152
+ if (column >= 0) {
153
+ mapping[role.value] = column;
154
+ taken.add(column);
155
+ }
156
+ }
157
+ }
158
+ const free = Array.from({ length: width }, (_, index) => index).filter((index) => !taken.has(index));
159
+ for (const role of roles) {
160
+ if (role.required !== true || typeof mapping[role.value] === "number") continue;
161
+ const column = free.shift();
162
+ if (column === undefined) break;
163
+ mapping[role.value] = column;
164
+ }
165
+ return mapping;
166
+ }
167
+
168
+ /**
169
+ * The table read through the mapping: the rows every `numeric` role can read, the
170
+ * others moved to `unread` (in line order, with the parse's own), what is missing,
171
+ * and whether the table can be used. Column indexes past the table's width count as
172
+ * unplaced.
173
+ */
174
+ export function readMappedTable<R extends string>(
175
+ table: TextTable,
176
+ roles: readonly ColumnRole<R>[],
177
+ mapping: ColumnMapping<R>,
178
+ ): ColumnMapperResult<R> {
179
+ const numeric = roles
180
+ .filter((role) => role.numeric)
181
+ .map((role) => placedAt(mapping, role.value, table.width))
182
+ .filter((column): column is number => column !== null);
183
+ const rows: string[][] = [];
184
+ const lines: TableLine[] = [];
185
+ const refused: TableLine[] = [];
186
+ table.rows.forEach((row, index) => {
187
+ if (numeric.every((column) => Number.isFinite(tableNumber(row[column] ?? "", table.decimalComma)))) {
188
+ rows.push(row);
189
+ lines.push(table.lines[index]);
190
+ } else {
191
+ refused.push(table.lines[index]);
192
+ }
193
+ });
194
+ const unread = refused.length ? [...table.unread, ...refused].sort((a, b) => a.line - b.line) : table.unread;
195
+ const missing = missingRoles(roles, mapping, table.width);
196
+ return {
197
+ ...table,
198
+ rows,
199
+ lines,
200
+ unread,
201
+ mapping,
202
+ missing,
203
+ complete: missing.length === 0 && rows.length > 0,
204
+ };
205
+ }
206
+
207
+ /**
208
+ * A dropped or chosen file's text — UTF-8 when it is UTF-8, else Windows-1252.
209
+ *
210
+ * A German bank or bench export is very often written in Windows-1252, and reading it
211
+ * as UTF-8 turns every "ä" in a column name into "�" — no error, just a header nobody
212
+ * recognises. So the bytes are tried as strict UTF-8 first and read as Windows-1252
213
+ * when that fails (keksdose's server sniffs the encoding for the same reason; a file
214
+ * read in the browser had nothing doing it). Falls back to `FileReader` where a `File`
215
+ * has no `arrayBuffer` (Kurvenschmiede's `readText`).
216
+ */
217
+ export async function readTextFile(file: Blob): Promise<string> {
218
+ const bytes = await bytesOf(file);
219
+ try {
220
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
221
+ } catch {
222
+ return new TextDecoder("windows-1252").decode(bytes);
223
+ }
224
+ }
225
+
226
+ function bytesOf(file: Blob): Promise<ArrayBuffer> {
227
+ if (typeof file.arrayBuffer === "function") return file.arrayBuffer();
228
+ return new Promise((resolve, reject) => {
229
+ const reader = new FileReader();
230
+ reader.onload = () => resolve(reader.result as ArrayBuffer);
231
+ reader.onerror = () => reject(reader.error ?? new Error("read failed"));
232
+ reader.readAsArrayBuffer(file);
233
+ });
234
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Countries as ISO 3166-1 alpha-2 codes, named by the runtime — for {@link CountrySelect}
3
+ * and for an app that shows the code it stored ("CH") as a name somewhere else (a
4
+ * table cell, an address line).
5
+ *
6
+ * The kit ships CODES, not names. Every runtime the kit supports has
7
+ * `Intl.DisplayNames({ type: "region" })`, which names all of these in every locale
8
+ * the browser knows — so a table of 249 names per language would be a large file
9
+ * that is out of date the day a country is renamed (Türkiye, Czechia, Eswatini) and
10
+ * still covers fewer languages than the browser does. What is left to ship is the
11
+ * list of which codes ARE countries, and that is one string.
12
+ */
13
+
14
+ /**
15
+ * Every officially assigned ISO 3166-1 alpha-2 code — the 249 countries, territories
16
+ * and areas of the standard, upper-case, in code order.
17
+ *
18
+ * User-assigned codes are deliberately not in it: `XK` (Kosovo) is a widespread
19
+ * convention, not ISO, and whether to offer it is the app's decision. An app that
20
+ * wants it passes its own list to `CountrySelect`'s `countries` — the runtime names
21
+ * `XK` as well.
22
+ */
23
+ export const COUNTRY_CODES: readonly string[] = Object.freeze(
24
+ (
25
+ "AD AE AF AG AI AL AM AO AQ AR AS AT AU AW AX AZ BA BB BD BE BF BG BH BI BJ BL BM BN BO BQ " +
26
+ "BR BS BT BV BW BY BZ CA CC CD CF CG CH CI CK CL CM CN CO CR CU CV CW CX CY CZ DE DJ DK DM " +
27
+ "DO DZ EC EE EG EH ER ES ET FI FJ FK FM FO FR GA GB GD GE GF GG GH GI GL GM GN GP GQ GR GS " +
28
+ "GT GU GW GY HK HM HN HR HT HU ID IE IL IM IN IO IQ IR IS IT JE JM JO JP KE KG KH KI KM KN " +
29
+ "KP KR KW KY KZ LA LB LC LI LK LR LS LT LU LV LY MA MC MD ME MF MG MH MK ML MM MN MO MP MQ " +
30
+ "MR MS MT MU MV MW MX MY MZ NA NC NE NF NG NI NL NO NP NR NU NZ OM PA PE PF PG PH PK PL PM " +
31
+ "PN PR PS PT PW PY QA RE RO RS RU RW SA SB SC SD SE SG SH SI SJ SK SL SM SN SO SR SS ST SV " +
32
+ "SX SY SZ TC TD TF TG TH TJ TK TL TM TN TO TR TT TV TW TZ UA UG UM US UY UZ VA VC VE VG VI " +
33
+ "VN VU WF WS YE YT ZA ZM ZW"
34
+ ).split(" "),
35
+ );
36
+
37
+ /** Two ASCII letters — the shape of an alpha-2 code, whatever the case. */
38
+ const ALPHA_2 = /^[A-Za-z]{2}$/;
39
+
40
+ /** `code` upper-cased when it has the shape of an alpha-2 code, else `undefined`. Does
41
+ * not check that ISO assigned it — see {@link COUNTRY_CODES} for why `XK` must pass. */
42
+ export function normalizeCountryCode(code: string | null | undefined): string | undefined {
43
+ return code && ALPHA_2.test(code) ? code.toUpperCase() : undefined;
44
+ }
45
+
46
+ // One `DisplayNames` per locale, built on first use. `null` records a locale the
47
+ // runtime refused (a malformed tag) or a runtime without the API, so neither is
48
+ // retried for every row of a 249-row list.
49
+ const namers = new Map<string, Intl.DisplayNames | null>();
50
+
51
+ function namer(locale: string): Intl.DisplayNames | null {
52
+ let n = namers.get(locale);
53
+ if (n === undefined) {
54
+ try {
55
+ n = new Intl.DisplayNames([locale], { type: "region", fallback: "code" });
56
+ } catch {
57
+ n = null;
58
+ }
59
+ namers.set(locale, n);
60
+ }
61
+ return n;
62
+ }
63
+
64
+ /**
65
+ * The name of the country `code` stands for, in `locale` — `countryName("CH", "de-CH")`
66
+ * is "Schweiz". Falls back to the upper-cased code itself when the runtime has no name
67
+ * for it, rejects the locale, or has no `Intl.DisplayNames`: a code is still the right
68
+ * answer, where an empty cell would not be.
69
+ *
70
+ * `locale` defaults to English rather than to the runtime's language, the rule the
71
+ * kit's currency names follow (`currencyName`): an app that never set a locale reads
72
+ * the kit's English everywhere else, and a country list in the browser's language
73
+ * beside English labels — or in the test runner's — is two languages on one form.
74
+ *
75
+ * The code is upper-cased first. `DisplayNames` is case-sensitive about it in some
76
+ * runtimes (Node answers "ch" with "ch"), and the codes reach this from forms,
77
+ * URLs and databases in either case.
78
+ */
79
+ export function countryName(code: string, locale: string = "en"): string {
80
+ const upper = code.toUpperCase();
81
+ const n = namer(locale);
82
+ if (n) {
83
+ try {
84
+ const name = n.of(upper);
85
+ if (name) return name;
86
+ } catch {
87
+ // Not a well-formed region subtag ("C1", "") — the code is all there is.
88
+ }
89
+ }
90
+ return upper;
91
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * The locale's decimal mark and the reading of typed grouping marks, shared by the
3
+ * kit's numeric text fields: {@link AmountInput}, {@link NumberInput} and, through
4
+ * NumberInput, {@link NumberField} and {@link MoneyField}.
5
+ *
6
+ * Package-private — not re-exported from the barrel. It lives here rather than in
7
+ * `calc.ts` because that module is re-exported wholesale (`export * from
8
+ * "./lib/calc"`), and these are the fields' plumbing, not API.
9
+ *
10
+ * WHY ONE MODULE. AmountInput learned the locale's mark (kastlan 40) and the grouping
11
+ * parse (keksdose G1); NumberInput learned neither, so the same "1.234,56" a German
12
+ * user typed into a money field read as 1234.56 and, typed into the budget-goal field
13
+ * next to it, as 1.23456 (keksdose K5). Two private copies of the same rule are how
14
+ * that happened, so the rule now has one home.
15
+ */
16
+ import { sanitizeLive } from "./calc";
17
+
18
+ /**
19
+ * The locale's decimal mark, as far as a field can type it: "," or "." (kastlan 40).
20
+ * A mark outside those two (Arabic's "٫") is not one {@link sanitizeLive} lets
21
+ * through, so those locales keep the dot rather than get a field that eats the key it
22
+ * shows. `undefined` is the runtime's own locale, which is what `Intl` reads it as.
23
+ */
24
+ export function decimalMark(locale: string | undefined): "," | "." {
25
+ try {
26
+ const part = new Intl.NumberFormat(locale).formatToParts(1.5).find((p) => p.type === "decimal");
27
+ return part?.value === "," ? "," : ".";
28
+ } catch {
29
+ // An invalid tag throws a RangeError; a typo in a locale must not take the form down.
30
+ return ".";
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Which typed mark is the decimal one (keksdose G1). {@link sanitizeLive} folds every
36
+ * "," into ".", so on its own it read the German "1.234,56" as 1.23456 — and a money
37
+ * field's settle then made that 1.23, final. This decides per operand, from the
38
+ * typist's own text, before that fold:
39
+ *
40
+ * - both marks present: the one that is not the locale's is grouping, and goes;
41
+ * - in a "," locale, only "." present: grouping when it cannot be a decimal as a
42
+ * German reader writes it — twice ("1.234.567"), or in thousands shape ("1.234",
43
+ * "12.500") — otherwise a decimal ("1.5", typed by a dot-decimal habit);
44
+ * - in a "." locale, only "," present: grouping when it appears twice ("1,234,567");
45
+ * once it stays a decimal, because Swiss and German typists write "1,5" in a
46
+ * de-CH form (kastlan 40) and a unit price "1,789" is a decimal.
47
+ *
48
+ * Only a keystroke's text comes through here. A calculator result is dot-decimal
49
+ * already, and "1.234" from `100/81.03…` is never a thousand.
50
+ */
51
+ export function normalizeTypedMarks(raw: string, mark: "," | "."): string {
52
+ const other = mark === "," ? "." : ",";
53
+ return raw.replace(/[0-9.,]+/g, (operand) => {
54
+ const hasMark = operand.includes(mark);
55
+ const count = operand.split(other).length - 1;
56
+ if (count === 0) return operand;
57
+ const grouping = hasMark
58
+ ? true
59
+ : count > 1 || (mark === "," && /^\d{1,3}(\.\d{3})+$/.test(operand));
60
+ return grouping ? operand.split(other).join("") : operand;
61
+ });
62
+ }
63
+
64
+ /** What a keystroke's raw text reports: grouping read off, then the field's live
65
+ * sanitising (comma → dot, stray characters out, one decimal point per operand). */
66
+ export function readTyped(raw: string, mark: "," | "."): string {
67
+ return sanitizeLive(normalizeTypedMarks(raw, mark));
68
+ }
69
+
70
+ // Spacing a typist uses to group digits — a space, the no-break and narrow no-break
71
+ // spaces `Intl` itself writes for fr-CH ("1 234,5" with U+202F), and the Swiss
72
+ // apostrophes ("1'234.50"). The live sanitising drops them, so they never reach the
73
+ // value; the draft may still show them.
74
+ const GROUP_SPACING = /[\s'’\u00a0\u202f]/g;
75
+
76
+ /**
77
+ * Whether the field may keep SHOWING `raw` — the typist's own text — while it holds
78
+ * `canonical` (dot-decimal, as `onChange` reported it).
79
+ *
80
+ * Showing the typed text rather than re-spelling the value is what keeps the caret
81
+ * where the typist put it, and what keeps a grouping "." from being turned into a ","
82
+ * under their fingers — after which no rule could tell the two apart (keksdose G1).
83
+ * But only text that says the same as the value, and says it with nothing the field
84
+ * would have DROPPED: a letter, or a second decimal point in one operand ("1.2.3"),
85
+ * must not stay on screen while the value says "1.23" (Keksdose live #201 is the bug
86
+ * that showing it would bring back). Grouping marks and grouping spaces may stay.
87
+ */
88
+ export function showsTyped(raw: string, mark: "," | ".", canonical: string): boolean {
89
+ const read = normalizeTypedMarks(raw, mark).replace(GROUP_SPACING, "");
90
+ const sanitized = sanitizeLive(read);
91
+ // Nothing dropped: the read text is the sanitised one, but for which mark it uses.
92
+ return sanitized === read.replace(/,/g, ".") && sanitized === sanitizeLive(canonical);
93
+ }
94
+
95
+ /** A dot-decimal text spelt in `mark` — display only, one character for one, so a
96
+ * re-spelling never moves a caret. */
97
+ export function localizeMark(text: string, mark: "," | "."): string {
98
+ return mark === "," ? text.replace(/\./g, ",") : text;
99
+ }