@eifi1/ui-kit 0.21.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (320) hide show
  1. package/README.md +42 -25
  2. package/dist/chart.d.ts +19 -14
  3. package/dist/components/account-chips.d.ts +10 -3
  4. package/dist/components/account-chips.js +5 -2
  5. package/dist/components/account-chips.js.map +1 -1
  6. package/dist/components/account-settings.d.ts +33 -2
  7. package/dist/components/account-settings.js +43 -21
  8. package/dist/components/account-settings.js.map +1 -1
  9. package/dist/components/amount-input.d.ts +53 -14
  10. package/dist/components/amount-input.js +17 -28
  11. package/dist/components/amount-input.js.map +1 -1
  12. package/dist/components/autocomplete.d.ts +29 -0
  13. package/dist/components/autocomplete.js +134 -104
  14. package/dist/components/autocomplete.js.map +1 -1
  15. package/dist/components/button-group.d.ts +19 -14
  16. package/dist/components/calculator.d.ts +19 -14
  17. package/dist/components/checkbox-group.d.ts +104 -0
  18. package/dist/components/checkbox-group.js +111 -0
  19. package/dist/components/checkbox-group.js.map +1 -0
  20. package/dist/components/checkbox.d.ts +19 -0
  21. package/dist/components/checkbox.js +30 -7
  22. package/dist/components/checkbox.js.map +1 -1
  23. package/dist/components/chip.d.ts +18 -0
  24. package/dist/components/chip.js +47 -10
  25. package/dist/components/chip.js.map +1 -1
  26. package/dist/components/column-mapper.d.ts +182 -0
  27. package/dist/components/column-mapper.js +375 -0
  28. package/dist/components/column-mapper.js.map +1 -0
  29. package/dist/components/combobox-core.d.ts +39 -2
  30. package/dist/components/combobox-core.js +29 -6
  31. package/dist/components/combobox-core.js.map +1 -1
  32. package/dist/components/combobox.d.ts +91 -2
  33. package/dist/components/combobox.js +454 -308
  34. package/dist/components/combobox.js.map +1 -1
  35. package/dist/components/confirm-dialog.d.ts +69 -161
  36. package/dist/components/confirm-dialog.js +7 -8
  37. package/dist/components/confirm-dialog.js.map +1 -1
  38. package/dist/components/copy-button.d.ts +16 -11
  39. package/dist/components/country-select.d.ts +167 -0
  40. package/dist/components/country-select.js +293 -0
  41. package/dist/components/country-select.js.map +1 -0
  42. package/dist/components/currency-select.d.ts +17 -1
  43. package/dist/components/currency-select.js +56 -23
  44. package/dist/components/currency-select.js.map +1 -1
  45. package/dist/components/danger-confirm.d.ts +69 -166
  46. package/dist/components/danger-confirm.js +144 -50
  47. package/dist/components/danger-confirm.js.map +1 -1
  48. package/dist/components/data-table-cells.d.ts +1 -2
  49. package/dist/components/data-table-filter-popover.d.ts +1 -2
  50. package/dist/components/data-table-filters.d.ts +1 -2
  51. package/dist/components/data-table-labels.d.ts +4 -97
  52. package/dist/components/data-table-labels.js +9 -1
  53. package/dist/components/data-table-labels.js.map +1 -1
  54. package/dist/components/data-table-pagination.d.ts +3 -1
  55. package/dist/components/data-table.d.ts +1 -2
  56. package/dist/components/data-table.js +98 -3
  57. package/dist/components/data-table.js.map +1 -1
  58. package/dist/components/date-picker.d.ts +66 -5
  59. package/dist/components/date-picker.js +378 -66
  60. package/dist/components/date-picker.js.map +1 -1
  61. package/dist/components/entity-combobox.d.ts +42 -1
  62. package/dist/components/entity-combobox.js +154 -113
  63. package/dist/components/entity-combobox.js.map +1 -1
  64. package/dist/components/facing-pair.d.ts +19 -14
  65. package/dist/components/field-parts.d.ts +224 -0
  66. package/dist/components/field-parts.js +156 -0
  67. package/dist/components/field-parts.js.map +1 -0
  68. package/dist/components/field-strip.d.ts +105 -0
  69. package/dist/components/field-strip.js +46 -0
  70. package/dist/components/field-strip.js.map +1 -0
  71. package/dist/components/file-button.d.ts +19 -14
  72. package/dist/components/file-button.js +13 -3
  73. package/dist/components/file-button.js.map +1 -1
  74. package/dist/components/file-dropzone.d.ts +41 -15
  75. package/dist/components/file-dropzone.js +57 -34
  76. package/dist/components/file-dropzone.js.map +1 -1
  77. package/dist/components/form-actions.d.ts +18 -13
  78. package/dist/components/form-actions.js +69 -5
  79. package/dist/components/form-actions.js.map +1 -1
  80. package/dist/components/iban-input.d.ts +69 -0
  81. package/dist/components/iban-input.js +125 -0
  82. package/dist/components/iban-input.js.map +1 -0
  83. package/dist/components/icon-picker.d.ts +21 -2
  84. package/dist/components/icon-picker.js +19 -4
  85. package/dist/components/icon-picker.js.map +1 -1
  86. package/dist/components/inline-edit-field.d.ts +167 -0
  87. package/dist/components/inline-edit-field.js +293 -0
  88. package/dist/components/inline-edit-field.js.map +1 -0
  89. package/dist/components/language-select.d.ts +130 -0
  90. package/dist/components/language-select.js +18 -0
  91. package/dist/components/language-select.js.map +1 -0
  92. package/dist/components/line-items.d.ts +25 -3
  93. package/dist/components/line-items.js +55 -46
  94. package/dist/components/line-items.js.map +1 -1
  95. package/dist/components/list.d.ts +25 -0
  96. package/dist/components/list.js +16 -5
  97. package/dist/components/list.js.map +1 -1
  98. package/dist/components/money-field.d.ts +32 -14
  99. package/dist/components/money-field.js.map +1 -1
  100. package/dist/components/month-picker.d.ts +52 -4
  101. package/dist/components/month-picker.js +291 -86
  102. package/dist/components/month-picker.js.map +1 -1
  103. package/dist/components/multi-entity-combobox.d.ts +26 -1
  104. package/dist/components/multi-entity-combobox.js +143 -102
  105. package/dist/components/multi-entity-combobox.js.map +1 -1
  106. package/dist/components/multi-select.d.ts +8 -1
  107. package/dist/components/multi-select.js +9 -4
  108. package/dist/components/multi-select.js.map +1 -1
  109. package/dist/components/number-field.d.ts +34 -19
  110. package/dist/components/number-field.js +6 -20
  111. package/dist/components/number-field.js.map +1 -1
  112. package/dist/components/number-input.d.ts +79 -19
  113. package/dist/components/number-input.js +90 -38
  114. package/dist/components/number-input.js.map +1 -1
  115. package/dist/components/numpad-sheet.d.ts +19 -14
  116. package/dist/components/one-time-code-input.d.ts +100 -0
  117. package/dist/components/one-time-code-input.js +137 -0
  118. package/dist/components/one-time-code-input.js.map +1 -0
  119. package/dist/components/phone-input.d.ts +69 -0
  120. package/dist/components/phone-input.js +171 -0
  121. package/dist/components/phone-input.js.map +1 -0
  122. package/dist/components/reauth-dialog.d.ts +2 -2
  123. package/dist/components/reauth-dialog.js +6 -8
  124. package/dist/components/reauth-dialog.js.map +1 -1
  125. package/dist/components/series-chart.d.ts +19 -14
  126. package/dist/components/settings-fields.d.ts +19 -14
  127. package/dist/components/share-card.d.ts +16 -11
  128. package/dist/components/sign-chip.d.ts +66 -0
  129. package/dist/components/sign-chip.js +39 -0
  130. package/dist/components/sign-chip.js.map +1 -0
  131. package/dist/components/swatch-picker.d.ts +43 -2
  132. package/dist/components/swatch-picker.js +80 -32
  133. package/dist/components/swatch-picker.js.map +1 -1
  134. package/dist/components/switch.d.ts +19 -0
  135. package/dist/components/switch.js +24 -6
  136. package/dist/components/switch.js.map +1 -1
  137. package/dist/components/text-link.d.ts +18 -13
  138. package/dist/components/tile-radio.d.ts +88 -13
  139. package/dist/components/tile-radio.js +28 -11
  140. package/dist/components/tile-radio.js.map +1 -1
  141. package/dist/components/time-input.d.ts +24 -15
  142. package/dist/components/time-input.js.map +1 -1
  143. package/dist/components/toggle-group.d.ts +47 -1
  144. package/dist/components/toggle-group.js +110 -69
  145. package/dist/components/toggle-group.js.map +1 -1
  146. package/dist/components/trigger-aria.d.ts +3 -0
  147. package/dist/components/trigger-aria.js +3 -1
  148. package/dist/components/trigger-aria.js.map +1 -1
  149. package/dist/components/ui.d.ts +16 -11
  150. package/dist/components/ui.js +284 -88
  151. package/dist/components/ui.js.map +1 -1
  152. package/dist/components/use-table-state.d.ts +1 -2
  153. package/dist/{data-table-3mi2R3so.d.ts → data-table-labels-B7OdnM0S.d.ts} +140 -3
  154. package/dist/data-table.d.ts +1 -2
  155. package/dist/data-table.js +2 -1
  156. package/dist/data-table.js.map +1 -1
  157. package/dist/feedback/feedback-attachment.d.ts +1 -1
  158. package/dist/feedback/feedback-attachment.js +348 -41
  159. package/dist/feedback/feedback-attachment.js.map +1 -1
  160. package/dist/feedback/feedback-dialog.d.ts +1 -1
  161. package/dist/feedback/feedback-dialog.js.map +1 -1
  162. package/dist/feedback/feedback-inbox.d.ts +3 -2
  163. package/dist/feedback/feedback-inbox.js.map +1 -1
  164. package/dist/feedback/feedback-thread.d.ts +68 -253
  165. package/dist/feedback/feedback-thread.js +46 -10
  166. package/dist/feedback/feedback-thread.js.map +1 -1
  167. package/dist/{kit-labels-D1J2_jyD.d.ts → feedback-BxeQVzwq.d.ts} +1206 -20
  168. package/dist/{feedback-attachment-WFiXJ8We.d.ts → feedback-attachment-fGAzZPf0.d.ts} +178 -11
  169. package/dist/feedback.d.ts +66 -2
  170. package/dist/hooks/use-file-drop.d.ts +19 -14
  171. package/dist/i18n/defaults.d.ts +19 -14
  172. package/dist/i18n/defaults.js +19 -2
  173. package/dist/i18n/defaults.js.map +1 -1
  174. package/dist/i18n/german.d.ts +19 -14
  175. package/dist/i18n/german.js +84 -5
  176. package/dist/i18n/german.js.map +1 -1
  177. package/dist/i18n/kit-labels.d.ts +15 -10
  178. package/dist/i18n/kit-labels.js +8 -2
  179. package/dist/i18n/kit-labels.js.map +1 -1
  180. package/dist/i18n/languages.d.ts +19 -14
  181. package/dist/i18n/locales/de-CH.d.ts +19 -14
  182. package/dist/i18n/locales/en.d.ts +19 -14
  183. package/dist/i18n/locales/en.js +16 -0
  184. package/dist/i18n/locales/en.js.map +1 -1
  185. package/dist/i18n/locales/es.d.ts +19 -14
  186. package/dist/i18n/locales/es.js +83 -5
  187. package/dist/i18n/locales/es.js.map +1 -1
  188. package/dist/i18n/locales/fr.d.ts +19 -14
  189. package/dist/i18n/locales/fr.js +82 -5
  190. package/dist/i18n/locales/fr.js.map +1 -1
  191. package/dist/i18n/locales/hu.d.ts +19 -14
  192. package/dist/i18n/locales/hu.js +85 -5
  193. package/dist/i18n/locales/hu.js.map +1 -1
  194. package/dist/i18n/locales/it.d.ts +19 -14
  195. package/dist/i18n/locales/it.js +84 -5
  196. package/dist/i18n/locales/it.js.map +1 -1
  197. package/dist/i18n/locales/zh.d.ts +19 -14
  198. package/dist/i18n/locales/zh.js +80 -5
  199. package/dist/i18n/locales/zh.js.map +1 -1
  200. package/dist/i18n/review.d.ts +19 -14
  201. package/dist/i18n/review.js +27 -1
  202. package/dist/i18n/review.js.map +1 -1
  203. package/dist/index.d.ts +21 -10
  204. package/dist/index.js +73 -2
  205. package/dist/index.js.map +1 -1
  206. package/dist/lib/column-mapping.d.ts +81 -0
  207. package/dist/lib/column-mapping.js +108 -0
  208. package/dist/lib/column-mapping.js.map +1 -0
  209. package/dist/lib/countries.d.ts +43 -0
  210. package/dist/lib/countries.js +39 -0
  211. package/dist/lib/countries.js.map +1 -0
  212. package/dist/lib/decimal-marks.d.ts +46 -0
  213. package/dist/lib/decimal-marks.js +40 -0
  214. package/dist/lib/decimal-marks.js.map +1 -0
  215. package/dist/lib/iban.d.ts +98 -0
  216. package/dist/lib/iban.js +154 -0
  217. package/dist/lib/iban.js.map +1 -0
  218. package/dist/lib/isin.d.ts +30 -0
  219. package/dist/lib/isin.js +36 -0
  220. package/dist/lib/isin.js.map +1 -0
  221. package/dist/lib/phone.d.ts +69 -0
  222. package/dist/lib/phone.js +121 -0
  223. package/dist/lib/phone.js.map +1 -0
  224. package/dist/lib/table-text.d.ts +109 -1
  225. package/dist/lib/table-text.js +122 -1
  226. package/dist/lib/table-text.js.map +1 -1
  227. package/dist/rhf/fields.d.ts +233 -18
  228. package/dist/rhf/fields.js +456 -3
  229. package/dist/rhf/fields.js.map +1 -1
  230. package/dist/rhf/form.d.ts +19 -14
  231. package/dist/rhf.d.ts +22 -15
  232. package/dist/rhf.js.map +1 -1
  233. package/dist/shell/app-shell.d.ts +18 -13
  234. package/dist/shell/top-bar-brand.d.ts +19 -14
  235. package/dist/shell/topbar-action-menu.d.ts +36 -3
  236. package/dist/shell/topbar-action-menu.js +74 -33
  237. package/dist/shell/topbar-action-menu.js.map +1 -1
  238. package/dist/shell.d.ts +18 -13
  239. package/dist/table-text.d.ts +1 -1
  240. package/dist/tour/tour.js +20 -24
  241. package/dist/tour/tour.js.map +1 -1
  242. package/dist/wizard/stepper-nav.d.ts +49 -16
  243. package/dist/wizard/stepper-nav.js +4 -0
  244. package/dist/wizard/stepper-nav.js.map +1 -1
  245. package/dist/wizard.d.ts +19 -14
  246. package/package.json +1 -1
  247. package/src/components/account-chips.tsx +17 -3
  248. package/src/components/account-settings.tsx +92 -26
  249. package/src/components/amount-input.tsx +68 -56
  250. package/src/components/autocomplete.tsx +198 -114
  251. package/src/components/checkbox-group.tsx +229 -0
  252. package/src/components/checkbox.tsx +72 -8
  253. package/src/components/chip.tsx +80 -10
  254. package/src/components/column-mapper.tsx +666 -0
  255. package/src/components/combobox-core.tsx +82 -9
  256. package/src/components/combobox.tsx +661 -340
  257. package/src/components/confirm-dialog.tsx +12 -8
  258. package/src/components/country-select.tsx +556 -0
  259. package/src/components/currency-select.tsx +109 -33
  260. package/src/components/danger-confirm.tsx +429 -82
  261. package/src/components/data-table-labels.ts +16 -1
  262. package/src/components/data-table.tsx +175 -6
  263. package/src/components/date-picker.tsx +595 -71
  264. package/src/components/entity-combobox.tsx +235 -124
  265. package/src/components/field-parts.tsx +403 -0
  266. package/src/components/field-strip.tsx +149 -0
  267. package/src/components/file-button.tsx +36 -3
  268. package/src/components/file-dropzone.tsx +106 -38
  269. package/src/components/form-actions.tsx +147 -6
  270. package/src/components/iban-input.tsx +231 -0
  271. package/src/components/icon-picker.tsx +47 -6
  272. package/src/components/inline-edit-field.tsx +533 -0
  273. package/src/components/language-select.tsx +78 -0
  274. package/src/components/line-items.tsx +35 -2
  275. package/src/components/list.tsx +55 -6
  276. package/src/components/money-field.tsx +13 -0
  277. package/src/components/month-picker.tsx +400 -63
  278. package/src/components/multi-entity-combobox.tsx +207 -120
  279. package/src/components/multi-select.tsx +80 -58
  280. package/src/components/number-field.tsx +28 -46
  281. package/src/components/number-input.tsx +210 -43
  282. package/src/components/one-time-code-input.tsx +288 -0
  283. package/src/components/phone-input.tsx +299 -0
  284. package/src/components/reauth-dialog.tsx +17 -18
  285. package/src/components/sign-chip.tsx +103 -0
  286. package/src/components/swatch-picker.tsx +143 -31
  287. package/src/components/switch.tsx +58 -7
  288. package/src/components/tile-radio.tsx +145 -29
  289. package/src/components/time-input.tsx +8 -1
  290. package/src/components/toggle-group.tsx +206 -73
  291. package/src/components/trigger-aria.ts +5 -0
  292. package/src/components/ui.tsx +479 -138
  293. package/src/data-table.ts +2 -1
  294. package/src/feedback/feedback-attachment.tsx +597 -46
  295. package/src/feedback/feedback-dialog.tsx +9 -3
  296. package/src/feedback/feedback-inbox.tsx +3 -2
  297. package/src/feedback/feedback-thread.tsx +113 -9
  298. package/src/i18n/defaults.ts +18 -1
  299. package/src/i18n/german.ts +89 -0
  300. package/src/i18n/kit-labels.tsx +82 -2
  301. package/src/i18n/locales/en.ts +36 -4
  302. package/src/i18n/locales/es.ts +87 -0
  303. package/src/i18n/locales/fr.ts +87 -0
  304. package/src/i18n/locales/hu.ts +83 -0
  305. package/src/i18n/locales/it.ts +88 -0
  306. package/src/i18n/locales/zh.ts +76 -0
  307. package/src/i18n/review.ts +26 -0
  308. package/src/index.ts +71 -3
  309. package/src/lib/column-mapping.ts +234 -0
  310. package/src/lib/countries.ts +91 -0
  311. package/src/lib/decimal-marks.ts +99 -0
  312. package/src/lib/iban.ts +171 -0
  313. package/src/lib/isin.ts +62 -0
  314. package/src/lib/phone.ts +211 -0
  315. package/src/lib/table-text.ts +271 -0
  316. package/src/rhf/fields.tsx +860 -9
  317. package/src/rhf.ts +3 -1
  318. package/src/shell/topbar-action-menu.tsx +134 -38
  319. package/src/tour/tour.tsx +66 -28
  320. package/src/wizard/stepper-nav.tsx +35 -1
@@ -0,0 +1,375 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useEffectEvent, useState } from "react";
4
+ import { Upload } from "lucide-react";
5
+ import { useKitLabels, useKitLocale } from "../i18n/kit-labels.js";
6
+ import { cn } from "../lib/cn.js";
7
+ import {
8
+ assignColumnRole,
9
+ guessMapping,
10
+ missingRoles,
11
+ readMappedTable,
12
+ readTextFile,
13
+ roleOfColumn
14
+ } from "../lib/column-mapping.js";
15
+ import { parseTextTable } from "../lib/table-text.js";
16
+ import { AlertBanner } from "./alert-banner.js";
17
+ import { Checkbox } from "./checkbox.js";
18
+ import { Disclosure } from "./disclosure.js";
19
+ import { FileButton, useFilePicker } from "./file-button.js";
20
+ import {
21
+ Table,
22
+ TableBody,
23
+ TableCell,
24
+ TableHead,
25
+ TableHeaderCell,
26
+ TableRow
27
+ } from "./table.js";
28
+ import { Select, Textarea } from "./ui.js";
29
+ const DEFAULT_COLUMN_MAPPER_LABELS = {
30
+ paste: "Paste a table",
31
+ pasteHint: "Copy the rows out of a spreadsheet and paste them here, or drop a CSV or text file.",
32
+ chooseFile: "Choose a file",
33
+ readError: (name) => `\u201C${name}\u201D could not be read`,
34
+ headerRow: "The first line names the columns",
35
+ summary: (columns, rows) => `${columns} ${columns === 1 ? "column" : "columns"}, ${rows} ${rows === 1 ? "row" : "rows"}`,
36
+ separatorSemicolon: "separated by semicolons",
37
+ separatorComma: "separated by commas",
38
+ separatorTab: "separated by tabs",
39
+ separatorSpace: "separated by spaces",
40
+ decimalComma: "decimal comma (1,5)",
41
+ decimalPoint: "decimal point (1.5)",
42
+ unreadCount: (count) => count === 1 ? "1 line could not be read" : `${count} lines could not be read`,
43
+ unreadLine: (line) => `Line ${line} could not be read`,
44
+ unreadMore: (count) => `\u2026and ${count} more`,
45
+ noRows: "No line of this text reads as a row of the table.",
46
+ table: "Columns and their roles",
47
+ columnN: (n) => `Column ${n}`,
48
+ roleOf: (column) => `What does column \u201C${column}\u201D hold?`,
49
+ ignore: "Ignore",
50
+ requiredRole: (role) => `${role} (required)`,
51
+ previewOf: (shown, total) => shown === 1 ? `The first of ${total} rows` : `The first ${shown} of ${total} rows`,
52
+ oneOf: (roles) => `either ${roles}`,
53
+ missing: (roles) => `Still needed: ${roles}.`
54
+ };
55
+ const SEPARATOR_LABEL = {
56
+ ";": "separatorSemicolon",
57
+ ",": "separatorComma",
58
+ " ": "separatorTab",
59
+ " ": "separatorSpace"
60
+ };
61
+ const UNREAD_LISTED = 100;
62
+ const DEFAULT_ACCEPT = ".csv,.tsv,.txt,.dat,.asc";
63
+ function formatList(items, type, locale) {
64
+ try {
65
+ return new Intl.ListFormat(locale, { type, style: "long" }).format(items);
66
+ } catch {
67
+ return items.join(", ");
68
+ }
69
+ }
70
+ function ColumnRoleTable({
71
+ header,
72
+ rows,
73
+ width: widthProp,
74
+ roles,
75
+ mapping,
76
+ onMappingChange,
77
+ previewRows = 5,
78
+ totalRows,
79
+ disabled,
80
+ labels: labelsProp,
81
+ className
82
+ }) {
83
+ const labels = useKitLabels(
84
+ "columnMapper",
85
+ DEFAULT_COLUMN_MAPPER_LABELS,
86
+ labelsProp
87
+ );
88
+ const locale = useKitLocale();
89
+ const shown = rows.slice(0, previewRows);
90
+ const width = widthProp ?? shown.reduce(
91
+ (widest, row) => Math.max(widest, row.length),
92
+ header?.length ?? 0
93
+ );
94
+ const columns = Array.from({ length: width }, (_, index) => index);
95
+ const total = totalRows ?? rows.length;
96
+ const nameOf = (column) => header?.[column] || labels.columnN(column + 1);
97
+ const labelOf = (value) => roles.find((role) => role.value === value)?.label ?? value;
98
+ const gaps = missingRoles(roles, mapping, width).map(
99
+ (gap) => gap.length === 1 ? labelOf(gap[0]) : labels.oneOf(formatList(gap.map(labelOf), "disjunction", locale))
100
+ );
101
+ return /* @__PURE__ */ jsxs(
102
+ "div",
103
+ {
104
+ className: cn("min-w-0 space-y-1.5", className),
105
+ "data-slot": "column-role-table",
106
+ children: [
107
+ /* @__PURE__ */ jsxs(Table, { density: "compact", framed: true, "aria-label": labels.table, children: [
108
+ /* @__PURE__ */ jsx(TableHead, { children: /* @__PURE__ */ jsx(TableRow, { children: columns.map((column) => {
109
+ const role = roleOfColumn(roles, mapping, column);
110
+ const name = nameOf(column);
111
+ return /* @__PURE__ */ jsxs(
112
+ TableHeaderCell,
113
+ {
114
+ scope: "col",
115
+ className: "min-w-36 py-2 align-top",
116
+ children: [
117
+ /* @__PURE__ */ jsx("span", { className: "block max-w-56 truncate pb-1 text-[11px] font-normal text-[var(--text-muted)]", children: name }),
118
+ /* @__PURE__ */ jsxs(
119
+ Select,
120
+ {
121
+ size: "sm",
122
+ className: "w-full",
123
+ "aria-label": labels.roleOf(name),
124
+ value: role ?? "",
125
+ disabled,
126
+ onChange: (event) => onMappingChange(
127
+ assignColumnRole(
128
+ roles,
129
+ mapping,
130
+ column,
131
+ event.target.value === "" ? null : event.target.value
132
+ )
133
+ ),
134
+ children: [
135
+ /* @__PURE__ */ jsx("option", { value: "", children: labels.ignore }),
136
+ roles.map((option) => /* @__PURE__ */ jsx("option", { value: option.value, children: option.required === true ? labels.requiredRole(option.label) : option.label }, option.value))
137
+ ]
138
+ }
139
+ )
140
+ ]
141
+ },
142
+ column
143
+ );
144
+ }) }) }),
145
+ /* @__PURE__ */ jsx(TableBody, { children: shown.map((row, index) => /* @__PURE__ */ jsx(TableRow, { children: columns.map((column) => /* @__PURE__ */ jsx(
146
+ TableCell,
147
+ {
148
+ "data-ignored": roleOfColumn(roles, mapping, column) === null || void 0,
149
+ className: cn(
150
+ "font-mono whitespace-nowrap",
151
+ // Dimmed, not hidden: its values are what tell you whether ignoring
152
+ // it was right.
153
+ roleOfColumn(roles, mapping, column) === null && "text-[var(--text-muted)]"
154
+ ),
155
+ children: /* @__PURE__ */ jsx("span", { className: "block max-w-56 truncate", children: row[column] ?? "" })
156
+ },
157
+ column
158
+ )) }, index)) })
159
+ ] }),
160
+ total > shown.length && /* @__PURE__ */ jsx("p", { className: "text-xs text-[var(--text-muted)]", children: labels.previewOf(shown.length, total) }),
161
+ gaps.length > 0 && /* @__PURE__ */ jsx(AlertBanner, { tone: "warning", variant: "inline", size: "sm", block: true, children: labels.missing(formatList(gaps, "conjunction", locale)) })
162
+ ]
163
+ }
164
+ );
165
+ }
166
+ const hasFiles = (event) => Array.from(event.dataTransfer?.types ?? []).includes("Files");
167
+ function ColumnMapper({
168
+ roles,
169
+ onChange,
170
+ label,
171
+ hint,
172
+ accept = DEFAULT_ACCEPT,
173
+ maxSize,
174
+ defaultText = "",
175
+ previewRows = 5,
176
+ guess,
177
+ disabled = false,
178
+ labels: labelsProp,
179
+ className
180
+ }) {
181
+ const labels = useKitLabels(
182
+ "columnMapper",
183
+ DEFAULT_COLUMN_MAPPER_LABELS,
184
+ labelsProp
185
+ );
186
+ const firstMapping = (table2) => guess ? guess(table2) : guessMapping(roles, table2.header, table2.width);
187
+ const [state, setState] = useState(() => {
188
+ const table2 = parseTextTable(defaultText);
189
+ return {
190
+ text: defaultText,
191
+ header: null,
192
+ table: table2,
193
+ mapping: firstMapping(table2)
194
+ };
195
+ });
196
+ const [reading, setReading] = useState(false);
197
+ const [refusal, setRefusal] = useState(null);
198
+ const [dragOver, setDragOver] = useState(false);
199
+ const result = readMappedTable(state.table, roles, state.mapping);
200
+ const commit = (next) => {
201
+ setState(next);
202
+ onChange(
203
+ next.table.width === 0 ? null : readMappedTable(next.table, roles, next.mapping)
204
+ );
205
+ };
206
+ const take = (text, fresh) => {
207
+ const header = fresh ? null : state.header;
208
+ const table2 = parseTextTable(text, header === null ? {} : { header });
209
+ const keep = !fresh && table2.width === state.table.width && state.table.width > 0;
210
+ commit({
211
+ text,
212
+ header,
213
+ table: table2,
214
+ mapping: keep ? state.mapping : firstMapping(table2)
215
+ });
216
+ };
217
+ const load = (file) => {
218
+ setRefusal(null);
219
+ setReading(true);
220
+ void readTextFile(file).then(
221
+ (text) => take(text, true),
222
+ // Only the read: an error thrown by the caller's onChange is not the file's.
223
+ () => setRefusal(labels.readError(file.name))
224
+ ).finally(() => setReading(false));
225
+ };
226
+ const refuse = (rejections) => setRefusal(rejections[0]?.message ?? null);
227
+ const drop = useFilePicker({
228
+ accept,
229
+ maxSize,
230
+ disabled,
231
+ onFiles: ([file]) => load(file),
232
+ onReject: refuse
233
+ });
234
+ const seeded = useEffectEvent(() => {
235
+ if (state.table.width > 0) onChange(result);
236
+ });
237
+ useEffect(() => {
238
+ seeded();
239
+ }, []);
240
+ const { table } = state;
241
+ const separator = labels[SEPARATOR_LABEL[table.separator]];
242
+ const unread = result.unread;
243
+ return /* @__PURE__ */ jsxs(
244
+ "div",
245
+ {
246
+ className: cn("min-w-0 space-y-3", className),
247
+ "data-slot": "column-mapper",
248
+ children: [
249
+ /* @__PURE__ */ jsx(
250
+ Textarea,
251
+ {
252
+ label: label ?? labels.paste,
253
+ hint: hint ?? labels.pasteHint,
254
+ rows: 5,
255
+ wrap: "off",
256
+ spellCheck: false,
257
+ autoCapitalize: "off",
258
+ autoCorrect: "off",
259
+ value: state.text,
260
+ disabled,
261
+ className: cn(
262
+ "font-mono text-xs",
263
+ dragOver && "rounded-md ring-2 ring-[var(--brand)]"
264
+ ),
265
+ "data-drag-over": dragOver || void 0,
266
+ onChange: (event) => {
267
+ setRefusal(null);
268
+ take(event.target.value, false);
269
+ },
270
+ onDragEnter: (event) => {
271
+ if (!hasFiles(event)) return;
272
+ event.preventDefault();
273
+ if (!disabled) setDragOver(true);
274
+ },
275
+ onDragOver: (event) => {
276
+ if (!hasFiles(event)) return;
277
+ event.preventDefault();
278
+ event.dataTransfer.dropEffect = disabled ? "none" : "copy";
279
+ },
280
+ onDragLeave: () => setDragOver(false),
281
+ onDrop: (event) => {
282
+ if (!hasFiles(event)) return;
283
+ event.preventDefault();
284
+ setDragOver(false);
285
+ drop.take(event.dataTransfer.files);
286
+ }
287
+ }
288
+ ),
289
+ drop.element,
290
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-x-4 gap-y-2", children: [
291
+ /* @__PURE__ */ jsxs(
292
+ FileButton,
293
+ {
294
+ variant: "secondary",
295
+ accept,
296
+ maxSize,
297
+ droppable: true,
298
+ pending: reading,
299
+ disabled,
300
+ onFiles: ([file]) => load(file),
301
+ onReject: refuse,
302
+ children: [
303
+ /* @__PURE__ */ jsx(Upload, { className: "size-4", "aria-hidden": true }),
304
+ labels.chooseFile
305
+ ]
306
+ }
307
+ ),
308
+ table.width > 0 && /* @__PURE__ */ jsx(
309
+ Checkbox,
310
+ {
311
+ label: labels.headerRow,
312
+ checked: table.header !== null,
313
+ disabled,
314
+ onCheckedChange: (checked) => {
315
+ const next = parseTextTable(state.text, { header: checked });
316
+ commit({ ...state, header: checked, table: next });
317
+ }
318
+ }
319
+ )
320
+ ] }),
321
+ refusal && /* @__PURE__ */ jsx(AlertBanner, { tone: "danger", variant: "inline", size: "sm", block: true, children: refusal }),
322
+ table.width > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
323
+ /* @__PURE__ */ jsx(
324
+ "p",
325
+ {
326
+ className: "text-xs text-[var(--text-secondary)]",
327
+ "data-slot": "column-mapper-summary",
328
+ children: [
329
+ labels.summary(table.width, result.rows.length),
330
+ separator,
331
+ table.decimalComma ? labels.decimalComma : labels.decimalPoint
332
+ ].join(" \xB7 ")
333
+ }
334
+ ),
335
+ result.rows.length === 0 && /* @__PURE__ */ jsx(AlertBanner, { tone: "warning", variant: "inline", size: "sm", block: true, children: labels.noRows }),
336
+ /* @__PURE__ */ jsx(
337
+ ColumnRoleTable,
338
+ {
339
+ header: table.header,
340
+ rows: result.rows,
341
+ width: table.width,
342
+ roles,
343
+ mapping: state.mapping,
344
+ onMappingChange: (mapping) => commit({ ...state, mapping }),
345
+ previewRows,
346
+ disabled,
347
+ labels: labelsProp
348
+ }
349
+ ),
350
+ unread.length > 0 && /* @__PURE__ */ jsx(
351
+ Disclosure,
352
+ {
353
+ variant: "bare",
354
+ title: labels.unreadCount(unread.length),
355
+ "data-slot": "column-mapper-unread",
356
+ children: /* @__PURE__ */ jsxs("ul", { className: "space-y-1 text-xs", children: [
357
+ unread.slice(0, UNREAD_LISTED).map((line) => /* @__PURE__ */ jsxs("li", { className: "min-w-0", children: [
358
+ /* @__PURE__ */ jsx("span", { className: "block text-[var(--text-secondary)]", children: labels.unreadLine(line.line) }),
359
+ /* @__PURE__ */ jsx("code", { className: "block truncate font-mono text-[var(--text-muted)]", children: line.text })
360
+ ] }, line.line)),
361
+ unread.length > UNREAD_LISTED && /* @__PURE__ */ jsx("li", { className: "text-[var(--text-muted)]", children: labels.unreadMore(unread.length - UNREAD_LISTED) })
362
+ ] })
363
+ }
364
+ )
365
+ ] })
366
+ ]
367
+ }
368
+ );
369
+ }
370
+ export {
371
+ ColumnMapper,
372
+ ColumnRoleTable,
373
+ DEFAULT_COLUMN_MAPPER_LABELS
374
+ };
375
+ //# sourceMappingURL=column-mapper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/column-mapper.tsx"],"sourcesContent":["import { useEffect, useEffectEvent, useState } from \"react\";\nimport type { ChangeEvent, DragEvent, ReactNode } from \"react\";\nimport { Upload } from \"lucide-react\";\n\nimport { useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport {\n assignColumnRole,\n guessMapping,\n missingRoles,\n readMappedTable,\n readTextFile,\n roleOfColumn,\n} from \"../lib/column-mapping\";\nimport type {\n ColumnMapperResult,\n ColumnMapping,\n ColumnRole,\n} from \"../lib/column-mapping\";\nimport { parseTextTable } from \"../lib/table-text\";\nimport type { TableSeparator, TextTable } from \"../lib/table-text\";\nimport { AlertBanner } from \"./alert-banner\";\nimport { Checkbox } from \"./checkbox\";\nimport { Disclosure } from \"./disclosure\";\nimport { FileButton, useFilePicker } from \"./file-button\";\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeaderCell,\n TableRow,\n} from \"./table\";\nimport { Select, Textarea } from \"./ui\";\n\n/**\n * A table pasted, dropped or chosen as a file, and which of its columns plays which\n * part — Kurvenschmiede's columns input and keksdose's bank-file mapping step, once.\n *\n * Kurvenschmiede (`features/control/columns-input.tsx` + `csv.ts`) reads a recorder's\n * export into a playground signal (time, value) and a comparison (time, setpoint,\n * actual): a textarea, a file button, a parse, a select per role, and a line that says\n * which decimal convention was assumed and how many lines were skipped. keksdose\n * (`import-map-step.tsx`) shows a bank's CSV as a table with a role select on every\n * column's header, the file's real values under it, ignored columns dimmed, and what\n * the import still needs named under it. Same problem, two hand-built answers.\n *\n * Two layers, so both shapes fit:\n *\n * * {@link ColumnMapper} — the whole single panel: paste, drop or choose → parse\n * ({@link parseTextTable}: separator, decimal convention, header line, all stated\n * back) → preview with a role per column → `onChange(result)`. Kurvenschmiede's.\n * * {@link ColumnRoleTable} — the preview with the role selects alone, controlled, over\n * rows the caller already has. keksdose's map step, whose rows come from its server's\n * sniff (encodings, quoted multi-line cells, skipped title rows) and whose mapping\n * lives in the wizard's state between steps.\n *\n * The rules under both are headless in `lib/column-mapping.ts`: a role moves rather\n * than doubling up, what is missing is named, and the rows a numeric role cannot read\n * are reported by line number rather than dropped.\n */\n\n// ── Labels ────────────────────────────────────────────────────────────────────\n\nexport interface ColumnMapperLabels {\n /** The paste box's label. */\n paste: string;\n /** The caption under the paste box. */\n pasteHint: string;\n /** The file button. */\n chooseFile: string;\n /** A chosen or dropped file whose bytes could not be read at all. */\n readError: (name: string) => string;\n /** The checkbox that says whether the first line names the columns. */\n headerRow: string;\n /** What was read: so many columns and rows. */\n summary: (columns: number, rows: number) => string;\n /** The separator that was assumed. */\n separatorSemicolon: string;\n separatorComma: string;\n separatorTab: string;\n separatorSpace: string;\n /** The decimal convention that was assumed — with an example, because \"decimal\n * comma\" alone is a term, and `1,5` is something a reader recognises. */\n decimalComma: string;\n decimalPoint: string;\n /** The title over the lines that could not be read. */\n unreadCount: (count: number) => string;\n /** One of them. */\n unreadLine: (line: number) => string;\n /** Past the first hundred listed. */\n unreadMore: (count: number) => string;\n /** The text has content, and none of it is a row. */\n noRows: string;\n /** The preview table's name. */\n table: string;\n /** A column the table names no header for. */\n columnN: (n: number) => string;\n /** The role select on a column's header — its accessible name. */\n roleOf: (column: string) => string;\n /** The role select's \"no role\" option. */\n ignore: string;\n /** A required role in the select. */\n requiredRole: (role: string) => string;\n /** Under the preview, when it shows fewer rows than were read. */\n previewOf: (shown: number, total: number) => string;\n /** A required group's roles, already joined as a list with \"or\". */\n oneOf: (roles: string) => string;\n /** What the table still needs, already joined as a list with \"and\". */\n missing: (roles: string) => string;\n}\n\nexport const DEFAULT_COLUMN_MAPPER_LABELS: ColumnMapperLabels = {\n paste: \"Paste a table\",\n pasteHint:\n \"Copy the rows out of a spreadsheet and paste them here, or drop a CSV or text file.\",\n chooseFile: \"Choose a file\",\n readError: (name) => `“${name}” could not be read`,\n headerRow: \"The first line names the columns\",\n summary: (columns, rows) =>\n `${columns} ${columns === 1 ? \"column\" : \"columns\"}, ${rows} ${rows === 1 ? \"row\" : \"rows\"}`,\n separatorSemicolon: \"separated by semicolons\",\n separatorComma: \"separated by commas\",\n separatorTab: \"separated by tabs\",\n separatorSpace: \"separated by spaces\",\n decimalComma: \"decimal comma (1,5)\",\n decimalPoint: \"decimal point (1.5)\",\n unreadCount: (count) =>\n count === 1\n ? \"1 line could not be read\"\n : `${count} lines could not be read`,\n unreadLine: (line) => `Line ${line} could not be read`,\n unreadMore: (count) => `…and ${count} more`,\n noRows: \"No line of this text reads as a row of the table.\",\n table: \"Columns and their roles\",\n columnN: (n) => `Column ${n}`,\n roleOf: (column) => `What does column “${column}” hold?`,\n ignore: \"Ignore\",\n requiredRole: (role) => `${role} (required)`,\n previewOf: (shown, total) =>\n shown === 1\n ? `The first of ${total} rows`\n : `The first ${shown} of ${total} rows`,\n oneOf: (roles) => `either ${roles}`,\n missing: (roles) => `Still needed: ${roles}.`,\n};\n\nconst SEPARATOR_LABEL: Record<\n TableSeparator,\n \"separatorSemicolon\" | \"separatorComma\" | \"separatorTab\" | \"separatorSpace\"\n> = {\n \";\": \"separatorSemicolon\",\n \",\": \"separatorComma\",\n \"\\t\": \"separatorTab\",\n \" \": \"separatorSpace\",\n};\n\n/** How many unread lines are listed before \"…and N more\". */\nconst UNREAD_LISTED = 100;\n\n/** A file a table is likely to be in. Extensions only, so a refusal reads \"Only .csv,\n * .tsv, … files\" rather than a list of MIME types; the parse is the real check. */\nconst DEFAULT_ACCEPT = \".csv,.tsv,.txt,.dat,.asc\";\n\nfunction formatList(\n items: string[],\n type: \"conjunction\" | \"disjunction\",\n locale: string | undefined,\n): string {\n try {\n return new Intl.ListFormat(locale, { type, style: \"long\" }).format(items);\n } catch {\n return items.join(\", \");\n }\n}\n\n// ── ColumnRoleTable ───────────────────────────────────────────────────────────\n\nexport interface ColumnRoleTableProps<\n R extends string,\n M extends ColumnMapping<R> = ColumnMapping<R>,\n> {\n /** The columns' names — the file's header line — or `null` for a table without one\n * (each column is then \"Column 1\", \"Column 2\", …). */\n header: readonly string[] | null;\n /** The rows to preview, as text. Only the first {@link previewRows} are drawn. */\n rows: readonly (readonly string[])[];\n /** How many columns. Default: the header's length or the widest previewed row. */\n width?: number;\n /** The parts a column can play. */\n roles: readonly ColumnRole<R>[];\n /** Role → column. Any object with the roles as nullable number fields — keksdose's\n * `CsvMapping` as it stands; the other fields are handed back untouched. */\n mapping: M;\n /** The next mapping, with the picked role moved onto its column. */\n onMappingChange: (next: M) => void;\n /** Default 5. */\n previewRows?: number;\n /** How many rows the table has in all, for \"The first 5 of 120 rows\". Default:\n * `rows.length`. */\n totalRows?: number;\n /** Every role select disabled — keksdose locks them while a re-sniff replaces the\n * table they point into. */\n disabled?: boolean;\n labels?: Partial<ColumnMapperLabels>;\n className?: string;\n}\n\n/**\n * A table's first rows, with a role select on every column's header.\n *\n * keksdose's mapping step (dev#462: \"make it more table like, where the table columns\n * can be chosen from the input and the column data changes with the chosen column\n * header\"): the file IS the form. The file's own column name sits on top, verbatim —\n * it is how the reader recognises the column; under it the role, picked in place;\n * under that the column's real values, so a column that is one off shows up here and\n * not after the import. A column with no role says \"Ignore\" and its values are dimmed:\n * the state that most needs to be visible in a twelve-column bank export.\n *\n * Picking a role another column holds MOVES it ({@link assignColumnRole}). Required\n * roles say so in the list, and what the table still needs is named under it — a\n * required group as \"either Amount, Debit or Credit\".\n *\n * The table scrolls sideways inside its own box (the kit {@link Table}'s wrapper, a\n * named, keyboard-reachable region while it overflows), so a wide export never widens\n * the page at 390px.\n */\nexport function ColumnRoleTable<\n R extends string,\n M extends ColumnMapping<R> = ColumnMapping<R>,\n>({\n header,\n rows,\n width: widthProp,\n roles,\n mapping,\n onMappingChange,\n previewRows = 5,\n totalRows,\n disabled,\n labels: labelsProp,\n className,\n}: ColumnRoleTableProps<R, M>) {\n const labels: ColumnMapperLabels = useKitLabels(\n \"columnMapper\",\n DEFAULT_COLUMN_MAPPER_LABELS,\n labelsProp,\n );\n const locale = useKitLocale();\n const shown = rows.slice(0, previewRows);\n const width =\n widthProp ??\n shown.reduce(\n (widest, row) => Math.max(widest, row.length),\n header?.length ?? 0,\n );\n const columns = Array.from({ length: width }, (_, index) => index);\n const total = totalRows ?? rows.length;\n const nameOf = (column: number) =>\n header?.[column] || labels.columnN(column + 1);\n const labelOf = (value: R) =>\n roles.find((role) => role.value === value)?.label ?? value;\n const gaps = missingRoles(roles, mapping, width).map((gap) =>\n gap.length === 1\n ? labelOf(gap[0])\n : labels.oneOf(formatList(gap.map(labelOf), \"disjunction\", locale)),\n );\n\n return (\n <div\n className={cn(\"min-w-0 space-y-1.5\", className)}\n data-slot=\"column-role-table\"\n >\n <Table density=\"compact\" framed aria-label={labels.table}>\n <TableHead>\n <TableRow>\n {columns.map((column) => {\n const role = roleOfColumn(roles, mapping, column);\n const name = nameOf(column);\n return (\n <TableHeaderCell\n key={column}\n scope=\"col\"\n className=\"min-w-36 py-2 align-top\"\n >\n <span className=\"block max-w-56 truncate pb-1 text-[11px] font-normal text-[var(--text-muted)]\">\n {name}\n </span>\n <Select\n size=\"sm\"\n className=\"w-full\"\n aria-label={labels.roleOf(name)}\n value={role ?? \"\"}\n disabled={disabled}\n onChange={(event) =>\n onMappingChange(\n assignColumnRole(\n roles,\n mapping,\n column,\n event.target.value === \"\"\n ? null\n : (event.target.value as R),\n ),\n )\n }\n >\n <option value=\"\">{labels.ignore}</option>\n {roles.map((option) => (\n <option key={option.value} value={option.value}>\n {option.required === true\n ? labels.requiredRole(option.label)\n : option.label}\n </option>\n ))}\n </Select>\n </TableHeaderCell>\n );\n })}\n </TableRow>\n </TableHead>\n <TableBody>\n {shown.map((row, index) => (\n <TableRow key={index}>\n {columns.map((column) => (\n <TableCell\n key={column}\n data-ignored={\n roleOfColumn(roles, mapping, column) === null || undefined\n }\n className={cn(\n \"font-mono whitespace-nowrap\",\n // Dimmed, not hidden: its values are what tell you whether ignoring\n // it was right.\n roleOfColumn(roles, mapping, column) === null &&\n \"text-[var(--text-muted)]\",\n )}\n >\n <span className=\"block max-w-56 truncate\">\n {row[column] ?? \"\"}\n </span>\n </TableCell>\n ))}\n </TableRow>\n ))}\n </TableBody>\n </Table>\n {total > shown.length && (\n <p className=\"text-xs text-[var(--text-muted)]\">\n {labels.previewOf(shown.length, total)}\n </p>\n )}\n {gaps.length > 0 && (\n <AlertBanner tone=\"warning\" variant=\"inline\" size=\"sm\" block>\n {labels.missing(formatList(gaps, \"conjunction\", locale))}\n </AlertBanner>\n )}\n </div>\n );\n}\n\n// ── ColumnMapper ──────────────────────────────────────────────────────────────\n\nexport interface ColumnMapperProps<R extends string> {\n /** The parts the columns play — translated labels, which are required, which must\n * be numbers. See {@link ColumnRole}. */\n roles: readonly ColumnRole<R>[];\n /**\n * The table read through the current mapping, on every change: the text, the header\n * checkbox, a role. `null` while there is no table at all (an empty box). Use it when\n * `result.complete`; until then `result.missing` says what is not placed yet.\n *\n * The rows are strings. A numeric role's cells are guaranteed to read with\n * `tableNumber(cell, result.decimalComma)`; anything else is the caller's to convert.\n */\n onChange: (result: ColumnMapperResult<R> | null) => void;\n /** The paste box's label. Default: `labels.paste`. */\n label?: ReactNode;\n /** The caption under the paste box. Default: `labels.pasteHint`. */\n hint?: ReactNode;\n /** What the file dialog offers, and what a drop is checked against. Default:\n * `.csv,.tsv,.txt,.dat,.asc`. */\n accept?: string;\n /** The largest file taken, in bytes. Default: no limit. */\n maxSize?: number;\n /** Text to start with. `onChange` is called with its result once, on mount. */\n defaultText?: string;\n /** Rows in the preview. Default 5. */\n previewRows?: number;\n /** The first mapping for a new table. Default {@link guessMapping}: a column named\n * like a role gets it, and the required roles left over take the free columns in\n * order. */\n guess?: (table: TextTable) => ColumnMapping<R>;\n disabled?: boolean;\n labels?: Partial<ColumnMapperLabels>;\n className?: string;\n}\n\ninterface MapperState<R extends string> {\n text: string;\n /** The reader's answer to \"does the first line name the columns\", or `null` for\n * the parse's own. */\n header: boolean | null;\n table: TextTable;\n mapping: ColumnMapping<R>;\n}\n\nconst hasFiles = (event: DragEvent) =>\n Array.from(event.dataTransfer?.types ?? []).includes(\"Files\");\n\n/**\n * Paste, drop or choose a table, see what it was read as, and say which column is\n * which — Kurvenschmiede's playground signal and comparison input as one kit part.\n *\n * Everything the parse assumed is stated, because a guess the reader cannot see is a\n * guess they cannot correct: the shape it found, the separator, the decimal\n * convention (\"decimal comma (1,5)\"), and every line it could not read, by number and\n * with its text. The header line is a checkbox, since the detection is a guess either\n * way and toggling it does not move a column (the mapping is kept, keksdose's\n * `keepRoles`). A new file is a new table and is mapped afresh; typing that changes the\n * column count re-guesses, because the old indexes point at nothing.\n *\n * A file can be chosen, dropped on the button, or dropped on the paste box — which,\n * left to the browser, would open the file in place of the page. It is read as UTF-8,\n * or as Windows-1252 when it is not UTF-8 ({@link readTextFile}).\n *\n * A two-step wizard that keeps the drop and the mapping apart (keksdose's) uses the\n * parts: {@link parseTextTable} or its own server's sniff, then {@link ColumnRoleTable},\n * then {@link readMappedTable}.\n */\nexport function ColumnMapper<R extends string>({\n roles,\n onChange,\n label,\n hint,\n accept = DEFAULT_ACCEPT,\n maxSize,\n defaultText = \"\",\n previewRows = 5,\n guess,\n disabled = false,\n labels: labelsProp,\n className,\n}: ColumnMapperProps<R>) {\n const labels: ColumnMapperLabels = useKitLabels(\n \"columnMapper\",\n DEFAULT_COLUMN_MAPPER_LABELS,\n labelsProp,\n );\n const firstMapping = (table: TextTable) =>\n guess ? guess(table) : guessMapping(roles, table.header, table.width);\n const [state, setState] = useState<MapperState<R>>(() => {\n const table = parseTextTable(defaultText);\n return {\n text: defaultText,\n header: null,\n table,\n mapping: firstMapping(table),\n };\n });\n const [reading, setReading] = useState(false);\n const [refusal, setRefusal] = useState<string | null>(null);\n const [dragOver, setDragOver] = useState(false);\n\n // What is drawn and what is handed over are the same computation. Not memoised: a\n // caller writes `roles={[…]}` inline, so a memo keyed on them would miss every time,\n // and the read is one pass over rows that are already split.\n const result = readMappedTable(state.table, roles, state.mapping);\n\n const commit = (next: MapperState<R>) => {\n setState(next);\n onChange(\n next.table.width === 0\n ? null\n : readMappedTable(next.table, roles, next.mapping),\n );\n };\n\n /** New text. `fresh` (a file): the reader's header answer and the mapping start over. */\n const take = (text: string, fresh: boolean) => {\n const header = fresh ? null : state.header;\n const table = parseTextTable(text, header === null ? {} : { header });\n const keep =\n !fresh && table.width === state.table.width && state.table.width > 0;\n commit({\n text,\n header,\n table,\n mapping: keep ? state.mapping : firstMapping(table),\n });\n };\n\n const load = (file: File) => {\n setRefusal(null);\n setReading(true);\n void readTextFile(file)\n .then(\n (text) => take(text, true),\n // Only the read: an error thrown by the caller's onChange is not the file's.\n () => setRefusal(labels.readError(file.name)),\n )\n .finally(() => setReading(false));\n };\n\n const refuse = (rejections: { message: string }[]) =>\n setRefusal(rejections[0]?.message ?? null);\n\n // A file dropped on the paste box. The same screening as the button's.\n const drop = useFilePicker({\n accept,\n maxSize,\n disabled,\n onFiles: ([file]) => load(file),\n onReject: refuse,\n });\n\n const seeded = useEffectEvent(() => {\n if (state.table.width > 0) onChange(result);\n });\n useEffect(() => {\n seeded();\n }, []);\n\n const { table } = state;\n const separator = labels[SEPARATOR_LABEL[table.separator]];\n const unread = result.unread;\n\n return (\n <div\n className={cn(\"min-w-0 space-y-3\", className)}\n data-slot=\"column-mapper\"\n >\n <Textarea\n label={label ?? labels.paste}\n hint={hint ?? labels.pasteHint}\n rows={5}\n wrap=\"off\"\n spellCheck={false}\n autoCapitalize=\"off\"\n autoCorrect=\"off\"\n value={state.text}\n disabled={disabled}\n className={cn(\n \"font-mono text-xs\",\n dragOver && \"rounded-md ring-2 ring-[var(--brand)]\",\n )}\n data-drag-over={dragOver || undefined}\n onChange={(event: ChangeEvent<HTMLTextAreaElement>) => {\n setRefusal(null);\n take(event.target.value, false);\n }}\n onDragEnter={(event) => {\n if (!hasFiles(event)) return;\n event.preventDefault();\n if (!disabled) setDragOver(true);\n }}\n onDragOver={(event) => {\n if (!hasFiles(event)) return;\n // Cancelled for every file drag: an uncancelled one makes the browser open\n // the file in place of the page.\n event.preventDefault();\n event.dataTransfer.dropEffect = disabled ? \"none\" : \"copy\";\n }}\n onDragLeave={() => setDragOver(false)}\n onDrop={(event) => {\n if (!hasFiles(event)) return;\n event.preventDefault();\n setDragOver(false);\n drop.take(event.dataTransfer.files);\n }}\n />\n {drop.element}\n\n <div className=\"flex flex-wrap items-center gap-x-4 gap-y-2\">\n <FileButton\n variant=\"secondary\"\n accept={accept}\n maxSize={maxSize}\n droppable\n pending={reading}\n disabled={disabled}\n onFiles={([file]) => load(file)}\n onReject={refuse}\n >\n <Upload className=\"size-4\" aria-hidden />\n {labels.chooseFile}\n </FileButton>\n {table.width > 0 && (\n <Checkbox\n label={labels.headerRow}\n checked={table.header !== null}\n disabled={disabled}\n onCheckedChange={(checked) => {\n const next = parseTextTable(state.text, { header: checked });\n // Same text, same split: the columns are where they were, and so are the\n // roles on them.\n commit({ ...state, header: checked, table: next });\n }}\n />\n )}\n </div>\n\n {refusal && (\n <AlertBanner tone=\"danger\" variant=\"inline\" size=\"sm\" block>\n {refusal}\n </AlertBanner>\n )}\n\n {table.width > 0 && (\n <>\n <p\n className=\"text-xs text-[var(--text-secondary)]\"\n data-slot=\"column-mapper-summary\"\n >\n {[\n labels.summary(table.width, result.rows.length),\n separator,\n table.decimalComma ? labels.decimalComma : labels.decimalPoint,\n ].join(\" · \")}\n </p>\n {result.rows.length === 0 && (\n <AlertBanner tone=\"warning\" variant=\"inline\" size=\"sm\" block>\n {labels.noRows}\n </AlertBanner>\n )}\n <ColumnRoleTable\n header={table.header}\n rows={result.rows}\n width={table.width}\n roles={roles}\n mapping={state.mapping}\n onMappingChange={(mapping) => commit({ ...state, mapping })}\n previewRows={previewRows}\n disabled={disabled}\n labels={labelsProp}\n />\n {unread.length > 0 && (\n <Disclosure\n variant=\"bare\"\n title={labels.unreadCount(unread.length)}\n data-slot=\"column-mapper-unread\"\n >\n <ul className=\"space-y-1 text-xs\">\n {unread.slice(0, UNREAD_LISTED).map((line) => (\n <li key={line.line} className=\"min-w-0\">\n <span className=\"block text-[var(--text-secondary)]\">\n {labels.unreadLine(line.line)}\n </span>\n <code className=\"block truncate font-mono text-[var(--text-muted)]\">\n {line.text}\n </code>\n </li>\n ))}\n {unread.length > UNREAD_LISTED && (\n <li className=\"text-[var(--text-muted)]\">\n {labels.unreadMore(unread.length - UNREAD_LISTED)}\n </li>\n )}\n </ul>\n </Disclosure>\n )}\n </>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["table"]}
@@ -65,6 +65,36 @@ declare function settleEnabled(rows: readonly {
65
65
  /** Classes a disabled row adds over its normal ones — dimmed, no hover, the
66
66
  * kit's soft-disabled look (`DangerConfirm`, `SignaturePad`). */
67
67
  declare const DISABLED_ROW_CLASS = "cursor-not-allowed opacity-50 hover:bg-transparent";
68
+ /**
69
+ * Whether an entity picker offers its create row — one rule for
70
+ * {@link EntityCombobox}, {@link MultiEntityCombobox} and {@link InlineEntityCombobox}.
71
+ *
72
+ * With a query: while the trimmed query names no option exactly (case-insensitive) —
73
+ * an exact hit is a record the list is already showing, and a second way to "make" it
74
+ * would mint a duplicate. With NO query: only when the caller named the row for that
75
+ * case (`createEmptyLabel`) — a row reading `Create “”` says nothing, and the kit cannot
76
+ * know the noun ("Create cash account") that would make it say something (keksdose G9).
77
+ *
78
+ * `names` is what the query is compared against: the loaded rows for a panel picker
79
+ * (its async list is all it knows), every option for the inline one.
80
+ */
81
+ declare function offersCreate(onCreate: unknown, query: string, names: readonly string[], createEmptyLabel: string | undefined): boolean;
82
+ /**
83
+ * {@link EndHintRow}'s `hint` and `className` for a combobox-family field, keeping the
84
+ * 0.22 rule (keksdose): a field that was passed `hint` at all — even `undefined` — keeps
85
+ * ONE box, so a hint coming and going never rebuilds the control under the cursor.
86
+ *
87
+ * A text hint is a caption after the control and never moved it; the case this covers
88
+ * is the "?" (a {@link FieldHint}) on a field with no label line, which sits at the end
89
+ * edge in a row of its own. Without this the row existed only while the "?" did, and
90
+ * the control was re-parented — remounted, focus and caret lost — each time it came or
91
+ * went. With `hint` passed the row stays, its end slot empty and its gap closed, so
92
+ * nothing on screen moves either.
93
+ */
94
+ declare function endHintRowProps(hintPassed: boolean, hasLabel: boolean, labelHint: ReactNode | undefined): {
95
+ hint: ReactNode | undefined;
96
+ className?: string;
97
+ };
68
98
  /**
69
99
  * The family's floating label, as a real `<label for>` (lenkbank): the input is then
70
100
  * named the way {@link Input}'s is, so `getByLabelText` and any helper that walks
@@ -213,6 +243,13 @@ interface ComboboxPanelProps<V extends string | number> extends ComponentPropsWi
213
243
  showCreate: boolean;
214
244
  onCreate: () => void;
215
245
  createContent: ReactNode;
246
+ /**
247
+ * The create row is shown but not takeable, and this says why — the lock's reason
248
+ * when the picker's `createCommit` meets a locked {@link WriteLockProvider}. Rendered
249
+ * as the row's second line rather than a tooltip, as {@link ComboOption.disabled}
250
+ * asks of every disabled row: a hover is the one explanation a phone cannot show.
251
+ */
252
+ createDisabledReason?: ReactNode;
216
253
  /** What the sheet calls itself on a phone — the field's label. Ignored on
217
254
  * desktop, where the panel sits under the field that already says it. */
218
255
  sheetTitle?: ReactNode;
@@ -246,6 +283,6 @@ interface ComboboxPanelProps<V extends string | number> extends ComponentPropsWi
246
283
  * `tabIndex={-1}` because focus never leaves the search box — that is what
247
284
  * `aria-activedescendant` is FOR.
248
285
  */
249
- declare function ComboboxPanel<V extends string | number>({ core, listboxId, searchPlaceholder, emptyLabel, multi, isSelected, onChoose, showCreate, onCreate, createContent, sheetTitle, closeLabel, loadErrorLabel, className, style, ...rest }: ComboboxPanelProps<V>): react.JSX.Element | null;
286
+ declare function ComboboxPanel<V extends string | number>({ core, listboxId, searchPlaceholder, emptyLabel, multi, isSelected, onChoose, showCreate, onCreate, createContent, createDisabledReason, sheetTitle, closeLabel, loadErrorLabel, className, style, ...rest }: ComboboxPanelProps<V>): react.JSX.Element | null;
250
287
 
251
- export { type ComboClearValue, type ComboOption, type ComboboxCore, type ComboboxCoreOptions, ComboboxFieldLabel, ComboboxPanel, type ComboboxPanelProps, DISABLED_ROW_CLASS, type OptionSource, type OptionSourceOptions, SUGGESTION_LIST_CLASS, isOptionEnabled, settleEnabled, stepEnabled, suggestionRowClass, useActiveOptionScroll, useComboboxCore, useComboboxFieldError, useOptionSource };
288
+ export { type ComboClearValue, type ComboOption, type ComboboxCore, type ComboboxCoreOptions, ComboboxFieldLabel, ComboboxPanel, type ComboboxPanelProps, DISABLED_ROW_CLASS, type OptionSource, type OptionSourceOptions, SUGGESTION_LIST_CLASS, endHintRowProps, isOptionEnabled, offersCreate, settleEnabled, stepEnabled, suggestionRowClass, useActiveOptionScroll, useComboboxCore, useComboboxFieldError, useOptionSource };
@@ -30,6 +30,17 @@ function settleEnabled(rows, i) {
30
30
  return before !== i ? before : -1;
31
31
  }
32
32
  const DISABLED_ROW_CLASS = "cursor-not-allowed opacity-50 hover:bg-transparent";
33
+ function offersCreate(onCreate, query, names, createEmptyLabel) {
34
+ if (!onCreate) return false;
35
+ if (!query) return Boolean(createEmptyLabel);
36
+ const q = query.toLowerCase();
37
+ return !names.some((n) => n.toLowerCase() === q);
38
+ }
39
+ function endHintRowProps(hintPassed, hasLabel, labelHint) {
40
+ if (hasLabel) return { hint: void 0 };
41
+ if (labelHint !== void 0) return { hint: labelHint };
42
+ return hintPassed ? { hint: null, className: "gap-0" } : { hint: void 0 };
43
+ }
33
44
  function ComboboxFieldLabel({
34
45
  htmlFor,
35
46
  className,
@@ -195,6 +206,7 @@ function ComboboxPanel({
195
206
  showCreate,
196
207
  onCreate,
197
208
  createContent,
209
+ createDisabledReason,
198
210
  sheetTitle,
199
211
  closeLabel,
200
212
  loadErrorLabel,
@@ -202,6 +214,7 @@ function ComboboxPanel({
202
214
  style,
203
215
  ...rest
204
216
  }) {
217
+ const createLocked = hasMessage(createDisabledReason);
205
218
  const {
206
219
  rect,
207
220
  placement,
@@ -218,7 +231,7 @@ function ComboboxPanel({
218
231
  closeToTrigger
219
232
  } = core;
220
233
  const isPhone = useMediaQuery(PHONE_QUERY, false);
221
- const rows = showCreate ? [...results, {}] : results;
234
+ const rows = showCreate ? [...results, { disabled: createLocked }] : results;
222
235
  const rowCount = rows.length;
223
236
  const optionId = (index) => `${listboxId}-option-${index}`;
224
237
  const current = settleEnabled(rows, active);
@@ -243,7 +256,7 @@ function ComboboxPanel({
243
256
  e.preventDefault();
244
257
  if (current < 0) return;
245
258
  if (current < results.length) onChoose(results[current]);
246
- else if (showCreate) onCreate();
259
+ else if (showCreate && !createLocked) onCreate();
247
260
  } else if (e.key === "Tab") {
248
261
  e.preventDefault();
249
262
  closeToTrigger();
@@ -363,20 +376,28 @@ function ComboboxPanel({
363
376
  id: optionId(results.length),
364
377
  role: "option",
365
378
  "aria-selected": false,
379
+ "aria-disabled": createLocked || void 0,
366
380
  tabIndex: -1,
367
381
  onMouseDown: (e) => {
368
382
  e.preventDefault();
369
- onCreate();
383
+ if (!createLocked) onCreate();
384
+ },
385
+ onMouseEnter: () => {
386
+ if (!createLocked) setActive(results.length);
370
387
  },
371
- onMouseEnter: () => setActive(results.length),
372
388
  className: cn(
373
389
  isPhone ? SHEET_ROW_CLASS : "flex w-full items-center gap-2 px-3 py-1.5 text-start text-sm",
374
390
  "text-[var(--text-secondary)]",
375
- current === results.length && !isPhone ? "bg-[var(--bg-active)]" : !isPhone && "hover:bg-[var(--bg-hover)]"
391
+ createLocked ? DISABLED_ROW_CLASS : current === results.length && !isPhone ? "bg-[var(--bg-active)]" : !isPhone && "hover:bg-[var(--bg-hover)]",
392
+ // After SHEET_ROW_CLASS, so its hover is the one cancelled.
393
+ createLocked && isPhone && DISABLED_ROW_CLASS
376
394
  ),
377
395
  children: [
378
396
  /* @__PURE__ */ jsx(Plus, { className: "size-4 shrink-0" }),
379
- /* @__PURE__ */ jsx("span", { className: "truncate", children: createContent })
397
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
398
+ /* @__PURE__ */ jsx("span", { className: "block truncate", children: createContent }),
399
+ createLocked && /* @__PURE__ */ jsx("span", { className: "block truncate text-xs text-[var(--text-placeholder)]", children: createDisabledReason })
400
+ ] })
380
401
  ]
381
402
  }
382
403
  ) })
@@ -456,7 +477,9 @@ export {
456
477
  ComboboxPanel,
457
478
  DISABLED_ROW_CLASS,
458
479
  SUGGESTION_LIST_CLASS,
480
+ endHintRowProps,
459
481
  isOptionEnabled,
482
+ offersCreate,
460
483
  settleEnabled,
461
484
  stepEnabled,
462
485
  suggestionRowClass,