@eifi1/ui-kit 0.35.0 → 0.36.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 (265) hide show
  1. package/README.md +35 -5
  2. package/dist/admin/role-select.d.ts +3 -2
  3. package/dist/admin/role-select.js +2 -0
  4. package/dist/admin/role-select.js.map +1 -1
  5. package/dist/components/amount-input.d.ts +2 -2
  6. package/dist/components/amount-input.js.map +1 -1
  7. package/dist/components/autocomplete.d.ts +2 -2
  8. package/dist/components/autocomplete.js +2 -1
  9. package/dist/components/autocomplete.js.map +1 -1
  10. package/dist/components/button-look.d.ts +1 -1
  11. package/dist/components/button-look.js +9 -4
  12. package/dist/components/button-look.js.map +1 -1
  13. package/dist/components/checkbox-group.d.ts +5 -2
  14. package/dist/components/checkbox-group.js +15 -4
  15. package/dist/components/checkbox-group.js.map +1 -1
  16. package/dist/components/column-mapper.d.ts +5 -3
  17. package/dist/components/column-mapper.js +2 -0
  18. package/dist/components/column-mapper.js.map +1 -1
  19. package/dist/components/combobox.d.ts +2 -2
  20. package/dist/components/combobox.js +2 -1
  21. package/dist/components/combobox.js.map +1 -1
  22. package/dist/components/country-select.d.ts +3 -3
  23. package/dist/components/country-select.js +3 -0
  24. package/dist/components/country-select.js.map +1 -1
  25. package/dist/components/currency-select.d.ts +3 -2
  26. package/dist/components/currency-select.js +10 -1
  27. package/dist/components/currency-select.js.map +1 -1
  28. package/dist/components/date-field.d.ts +2 -1
  29. package/dist/components/date-field.js.map +1 -1
  30. package/dist/components/date-picker.js +3 -0
  31. package/dist/components/date-picker.js.map +1 -1
  32. package/dist/components/date-range-picker.js +3 -0
  33. package/dist/components/date-range-picker.js.map +1 -1
  34. package/dist/components/entity-combobox.d.ts +2 -2
  35. package/dist/components/entity-combobox.js +2 -1
  36. package/dist/components/entity-combobox.js.map +1 -1
  37. package/dist/components/field-parts.d.ts +66 -11
  38. package/dist/components/field-parts.js +32 -10
  39. package/dist/components/field-parts.js.map +1 -1
  40. package/dist/components/field-strip.d.ts +3 -2
  41. package/dist/components/field-strip.js +2 -1
  42. package/dist/components/field-strip.js.map +1 -1
  43. package/dist/components/field.d.ts +3 -2
  44. package/dist/components/field.js +24 -15
  45. package/dist/components/field.js.map +1 -1
  46. package/dist/components/help-popover.d.ts +9 -7
  47. package/dist/components/help-popover.js.map +1 -1
  48. package/dist/components/icon-picker.d.ts +3 -2
  49. package/dist/components/icon-picker.js +3 -0
  50. package/dist/components/icon-picker.js.map +1 -1
  51. package/dist/components/inline-entity-combobox.d.ts +2 -2
  52. package/dist/components/inline-entity-combobox.js +2 -1
  53. package/dist/components/inline-entity-combobox.js.map +1 -1
  54. package/dist/components/input.d.ts +2 -2
  55. package/dist/components/input.js +2 -1
  56. package/dist/components/input.js.map +1 -1
  57. package/dist/components/month-picker.d.ts +2 -1
  58. package/dist/components/month-picker.js +8 -1
  59. package/dist/components/month-picker.js.map +1 -1
  60. package/dist/components/multi-entity-combobox.d.ts +2 -2
  61. package/dist/components/multi-entity-combobox.js +2 -1
  62. package/dist/components/multi-entity-combobox.js.map +1 -1
  63. package/dist/components/multi-select.d.ts +2 -2
  64. package/dist/components/multi-select.js +3 -0
  65. package/dist/components/multi-select.js.map +1 -1
  66. package/dist/components/number-input.d.ts +2 -2
  67. package/dist/components/number-input.js.map +1 -1
  68. package/dist/components/numeric-field.d.ts +5 -2
  69. package/dist/components/numeric-field.js +4 -1
  70. package/dist/components/numeric-field.js.map +1 -1
  71. package/dist/components/one-time-code-input.d.ts +2 -1
  72. package/dist/components/one-time-code-input.js +2 -1
  73. package/dist/components/one-time-code-input.js.map +1 -1
  74. package/dist/components/phone-input.d.ts +1 -1
  75. package/dist/components/phone-input.js +4 -0
  76. package/dist/components/phone-input.js.map +1 -1
  77. package/dist/components/popover.js +1 -1
  78. package/dist/components/popover.js.map +1 -1
  79. package/dist/components/select.d.ts +4 -3
  80. package/dist/components/select.js +2 -1
  81. package/dist/components/select.js.map +1 -1
  82. package/dist/components/slider.d.ts +3 -2
  83. package/dist/components/slider.js +7 -1
  84. package/dist/components/slider.js.map +1 -1
  85. package/dist/components/stat-tile.js +2 -11
  86. package/dist/components/stat-tile.js.map +1 -1
  87. package/dist/components/swatch-picker.d.ts +3 -2
  88. package/dist/components/swatch-picker.js +3 -0
  89. package/dist/components/swatch-picker.js.map +1 -1
  90. package/dist/components/text-link.js +3 -1
  91. package/dist/components/text-link.js.map +1 -1
  92. package/dist/components/textarea.d.ts +2 -1
  93. package/dist/components/textarea.js +9 -3
  94. package/dist/components/textarea.js.map +1 -1
  95. package/dist/components/toggle-group.d.ts +2 -2
  96. package/dist/components/toggle-group.js +7 -3
  97. package/dist/components/toggle-group.js.map +1 -1
  98. package/dist/eslint/index.js +1 -1
  99. package/dist/i18n/defaults.js +5 -1
  100. package/dist/i18n/defaults.js.map +1 -1
  101. package/dist/i18n/german.js +97 -1
  102. package/dist/i18n/german.js.map +1 -1
  103. package/dist/i18n/kit-labels.d.ts +15 -0
  104. package/dist/i18n/kit-labels.js +21 -1
  105. package/dist/i18n/kit-labels.js.map +1 -1
  106. package/dist/i18n/locales/en.js +103 -2
  107. package/dist/i18n/locales/en.js.map +3 -3
  108. package/dist/i18n/locales/es.js +95 -1
  109. package/dist/i18n/locales/es.js.map +1 -1
  110. package/dist/i18n/locales/fr.js +96 -1
  111. package/dist/i18n/locales/fr.js.map +1 -1
  112. package/dist/i18n/locales/hu.js +95 -1
  113. package/dist/i18n/locales/hu.js.map +1 -1
  114. package/dist/i18n/locales/it.js +96 -1
  115. package/dist/i18n/locales/it.js.map +1 -1
  116. package/dist/i18n/locales/zh.js +95 -1
  117. package/dist/i18n/locales/zh.js.map +1 -1
  118. package/dist/i18n/review.js +13 -0
  119. package/dist/i18n/review.js.map +1 -1
  120. package/dist/i18n/shared-labels.js +3 -1
  121. package/dist/i18n/shared-labels.js.map +1 -1
  122. package/dist/index.d.ts +21 -0
  123. package/dist/index.js +38 -0
  124. package/dist/index.js.map +1 -1
  125. package/dist/lib/device-settings.d.ts +72 -0
  126. package/dist/lib/device-settings.js +64 -0
  127. package/dist/lib/device-settings.js.map +1 -0
  128. package/dist/lib/plain-text.d.ts +11 -0
  129. package/dist/lib/plain-text.js +16 -0
  130. package/dist/lib/plain-text.js.map +1 -0
  131. package/dist/lib/safe-storage.d.ts +6 -0
  132. package/dist/lib/safe-storage.js +11 -0
  133. package/dist/lib/safe-storage.js.map +1 -1
  134. package/dist/notifications/notification-parts.d.ts +39 -0
  135. package/dist/notifications/notification-parts.js +47 -0
  136. package/dist/notifications/notification-parts.js.map +1 -0
  137. package/dist/notifications/notification-types-setting.d.ts +62 -0
  138. package/dist/notifications/notification-types-setting.js +192 -0
  139. package/dist/notifications/notification-types-setting.js.map +1 -0
  140. package/dist/notifications/notification-types.d.ts +75 -0
  141. package/dist/notifications/notification-types.js +2 -0
  142. package/dist/notifications/notification-types.js.map +1 -0
  143. package/dist/notifications/notifications-labels.d.ts +131 -0
  144. package/dist/notifications/notifications-labels.js +83 -0
  145. package/dist/notifications/notifications-labels.js.map +1 -0
  146. package/dist/notifications/push-device-setting.d.ts +37 -0
  147. package/dist/notifications/push-device-setting.js +164 -0
  148. package/dist/notifications/push-device-setting.js.map +1 -0
  149. package/dist/notifications/push-devices-setting.d.ts +44 -0
  150. package/dist/notifications/push-devices-setting.js +203 -0
  151. package/dist/notifications/push-devices-setting.js.map +1 -0
  152. package/dist/notifications/quiet-hours-setting.d.ts +47 -0
  153. package/dist/notifications/quiet-hours-setting.js +131 -0
  154. package/dist/notifications/quiet-hours-setting.js.map +1 -0
  155. package/dist/notifications/time-zone-select.d.ts +50 -0
  156. package/dist/notifications/time-zone-select.js +76 -0
  157. package/dist/notifications/time-zone-select.js.map +1 -0
  158. package/dist/notifications/unsubscribe-panel.d.ts +53 -0
  159. package/dist/notifications/unsubscribe-panel.js +139 -0
  160. package/dist/notifications/unsubscribe-panel.js.map +1 -0
  161. package/dist/notifications/use-push-device.d.ts +122 -0
  162. package/dist/notifications/use-push-device.js +230 -0
  163. package/dist/notifications/use-push-device.js.map +1 -0
  164. package/dist/rhf/fields.d.ts +26 -19
  165. package/dist/rhf/fields.js +51 -22
  166. package/dist/rhf/fields.js.map +1 -1
  167. package/dist/settings/device-reset-setting-labels.d.ts +3 -0
  168. package/dist/settings/device-reset-setting-labels.js +15 -0
  169. package/dist/settings/device-reset-setting-labels.js.map +1 -0
  170. package/dist/settings/device-reset-setting.d.ts +58 -0
  171. package/dist/settings/device-reset-setting.js +51 -0
  172. package/dist/settings/device-reset-setting.js.map +1 -0
  173. package/dist/shell/app-shell.js +5 -0
  174. package/dist/shell/app-shell.js.map +1 -1
  175. package/dist/theme/contrast-tokens.d.ts +42 -4
  176. package/dist/theme/contrast-tokens.js +56 -9
  177. package/dist/theme/contrast-tokens.js.map +1 -1
  178. package/dist/theme/contrast.d.ts +4 -2
  179. package/dist/theme/contrast.js +13 -0
  180. package/dist/theme/contrast.js.map +1 -1
  181. package/dist/theme/palette-derive.d.ts +15 -1
  182. package/dist/theme/palette-derive.js +37 -8
  183. package/dist/theme/palette-derive.js.map +1 -1
  184. package/dist/theme/palette-presets.d.ts +2 -1
  185. package/dist/theme/palette-presets.js +5 -5
  186. package/dist/theme/palette-presets.js.map +1 -1
  187. package/dist/theme/palette-store.js +6 -0
  188. package/dist/theme/palette-store.js.map +1 -1
  189. package/dist/theme/text-size.js +6 -0
  190. package/dist/theme/text-size.js.map +1 -1
  191. package/dist/theme/theme-store.js +6 -0
  192. package/dist/theme/theme-store.js.map +1 -1
  193. package/package.json +1 -1
  194. package/src/admin/role-select.tsx +4 -1
  195. package/src/components/amount-input.tsx +2 -1
  196. package/src/components/autocomplete.tsx +5 -2
  197. package/src/components/button-look.ts +15 -5
  198. package/src/components/checkbox-group.tsx +22 -4
  199. package/src/components/column-mapper.tsx +6 -2
  200. package/src/components/combobox.tsx +5 -2
  201. package/src/components/country-select.tsx +6 -1
  202. package/src/components/currency-select.tsx +12 -3
  203. package/src/components/date-field.tsx +2 -2
  204. package/src/components/date-picker.tsx +3 -0
  205. package/src/components/date-range-picker.tsx +3 -0
  206. package/src/components/entity-combobox.tsx +5 -2
  207. package/src/components/field-parts.tsx +107 -19
  208. package/src/components/field-strip.tsx +4 -3
  209. package/src/components/field.tsx +28 -15
  210. package/src/components/help-popover.tsx +9 -7
  211. package/src/components/icon-picker.tsx +8 -3
  212. package/src/components/inline-entity-combobox.tsx +5 -2
  213. package/src/components/input.tsx +4 -2
  214. package/src/components/month-picker.tsx +10 -3
  215. package/src/components/multi-entity-combobox.tsx +5 -2
  216. package/src/components/multi-select.tsx +6 -1
  217. package/src/components/number-input.tsx +2 -2
  218. package/src/components/numeric-field.ts +10 -2
  219. package/src/components/one-time-code-input.tsx +5 -3
  220. package/src/components/phone-input.tsx +4 -0
  221. package/src/components/popover.tsx +3 -1
  222. package/src/components/select.tsx +6 -3
  223. package/src/components/slider.tsx +10 -3
  224. package/src/components/stat-tile.tsx +3 -18
  225. package/src/components/swatch-picker.tsx +9 -4
  226. package/src/components/text-link.tsx +3 -1
  227. package/src/components/textarea.tsx +11 -4
  228. package/src/components/toggle-group.tsx +21 -5
  229. package/src/i18n/defaults.ts +4 -0
  230. package/src/i18n/german.ts +113 -0
  231. package/src/i18n/kit-labels.tsx +51 -1
  232. package/src/i18n/locales/en.ts +6 -0
  233. package/src/i18n/locales/es.ts +106 -0
  234. package/src/i18n/locales/fr.ts +106 -0
  235. package/src/i18n/locales/hu.ts +104 -0
  236. package/src/i18n/locales/it.ts +105 -0
  237. package/src/i18n/locales/zh.ts +96 -0
  238. package/src/i18n/review.ts +13 -0
  239. package/src/i18n/shared-labels.ts +2 -0
  240. package/src/index.ts +50 -0
  241. package/src/lib/device-settings.ts +146 -0
  242. package/src/lib/plain-text.ts +22 -0
  243. package/src/lib/safe-storage.ts +16 -0
  244. package/src/notifications/notification-parts.tsx +83 -0
  245. package/src/notifications/notification-types-setting.tsx +309 -0
  246. package/src/notifications/notification-types.ts +80 -0
  247. package/src/notifications/notifications-labels.ts +238 -0
  248. package/src/notifications/push-device-setting.tsx +224 -0
  249. package/src/notifications/push-devices-setting.tsx +306 -0
  250. package/src/notifications/quiet-hours-setting.tsx +185 -0
  251. package/src/notifications/time-zone-select.tsx +126 -0
  252. package/src/notifications/unsubscribe-panel.tsx +203 -0
  253. package/src/notifications/use-push-device.ts +392 -0
  254. package/src/rhf/fields.tsx +77 -29
  255. package/src/settings/device-reset-setting-labels.ts +20 -0
  256. package/src/settings/device-reset-setting.tsx +119 -0
  257. package/src/shell/app-shell.tsx +8 -0
  258. package/src/theme/contrast-tokens.ts +106 -12
  259. package/src/theme/contrast.ts +30 -4
  260. package/src/theme/palette-derive.ts +68 -7
  261. package/src/theme/palette-presets.ts +9 -6
  262. package/src/theme/palette-store.ts +12 -0
  263. package/src/theme/text-size.ts +8 -0
  264. package/src/theme/theme-store.ts +10 -0
  265. package/tokens.css +38 -4
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/column-mapper.tsx"],"sourcesContent":["import { useEffect, useEffectEvent, useState } from \"react\";\nimport type { ChangeEvent, DragEvent, HTMLAttributes, ReactNode } from \"react\";\nimport { Upload } from \"lucide-react\";\n\nimport { useKitLabels, useKitLocale, type LabelOverride } 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 } from \"./select\";\nimport { Textarea } from \"./textarea\";\nimport { DEFAULT_COLUMN_MAPPER_LABELS, markedShort } from \"./column-mapper-labels\";\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 — only while some role is NOT required: when\n * every one is, the mark tells no role from another (0.24, Kurvenschmiede). Since\n * 0.25 this is what a screen reader says for the option (its `aria-label`); the\n * option SHOWS {@link requiredRoleShort}. */\n requiredRole: (role: string) => string;\n /** The same required role as the select shows it (0.25): the role's name and a short\n * mark, so the mark does not cut the name short in a phone-width column — \"Booking\n * date (re…\" was all keksdose's closed select had room for. Spoken as\n * {@link requiredRole}. Optional, so a complete `UiKitLabels` typed before 0.25\n * still compiles; the asterisk needs no translation in most languages. */\n requiredRoleShort?: (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\n// Defined in ./column-mapper-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_COLUMN_MAPPER_LABELS };\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\n/** A `data-*` attribute, typed so `{ \"data-private\": \"\" }` needs no cast. */\ntype DataAttributes = { [key: `data-${string}`]: string | number | boolean | undefined };\n\n/**\n * What {@link ColumnRoleTableProps.bodyProps} puts on the preview's `<tbody>`: `data-*`\n * hooks, an `id`, a class, a style.\n */\nexport type ColumnRoleTableBodyProps = Omit<\n HTMLAttributes<HTMLTableSectionElement>,\n \"children\" | \"dangerouslySetInnerHTML\"\n> &\n DataAttributes;\n\n/**\n * What {@link ColumnRoleTableProps.rowProps} returns for one preview row's `<tr>` —\n * the same shape as LineItems' `LineItemsRowProps`, on a table row.\n */\nexport type ColumnRoleTableRowProps = Omit<\n HTMLAttributes<HTMLTableRowElement>,\n \"children\" | \"dangerouslySetInnerHTML\"\n> &\n DataAttributes;\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 /**\n * Extra attributes for the preview's `<tbody>` (0.24) — keksdose: the sample rows are\n * the user's own bank data (payees, memos, amounts), and its demo mode blurs exactly\n * that through `.demo-mode [data-private]` (#336). With nowhere to put the attribute,\n * its map step reached into the table with a `useLayoutEffect` that set it on the\n * `<tbody>` after every commit; `bodyProps={{ \"data-private\": \"\" }}` declares it.\n * There is deliberately no head counterpart: the column names and the role selects\n * are what such a mode is there to show.\n *\n * Spread on the kit `TableBody`; a `className` is added to its own.\n */\n bodyProps?: ColumnRoleTableBodyProps;\n /**\n * Extra attributes for one preview row's `<tr>`, from its cells and its index in the\n * preview (0.24) — the contract of LineItems' `rowProps` (keksdose K20), on a table:\n * `data-*` hooks (a per-row `data-private`, a test id), an `id`, a class. Called for\n * the rows drawn, the first {@link previewRows}. The row has no attributes of its own\n * to protect; a `className` is added after its classes.\n */\n rowProps?: (row: readonly string[], index: number) => ColumnRoleTableRowProps | undefined;\n labels?: LabelOverride<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}). What the\n * table still needs is named under it — a required group as \"either Amount, Debit or\n * Credit\". Required roles are marked in the list only where some role is not\n * (0.24): Kurvenschmiede's roles are all required, so the mark told no option from\n * another, and on a phone it cut the role's own name short (\"Zeit (s) (erforderl…\")\n * — the part of the option the reader has to read. The \"Still needed\" line already\n * says what is missing; a mark that distinguishes nothing is only noise.\n *\n * Where the mark does tell roles apart, it is short (0.25): keksdose's bank roles, on\n * a phone, still showed \"Booking date (re…\" in the closed select. The option shows\n * \"Booking date *\" ({@link ColumnMapperLabels.requiredRoleShort}) and is NAMED \"Booking\n * date (required)\" ({@link ColumnMapperLabels.requiredRole}, its `aria-label`), so a\n * screen reader hears the word rather than \"star\". And no role is cut at all: each\n * column is at least as wide as its select's longest option — the select's own width,\n * which a table cell otherwise ignores for a `w-full` control — up to 14rem, the width\n * the column's name and values already stop at. A long role name (\"Date de\n * comptabilisation\", \"Betrag (mit Vorzeichen)\") widens the columns, and the edge fade\n * says there are more of them to scroll to.\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. An edge with columns behind it fades out (0.24, Kurvenschmiede:\n * on a phone the fourth column's select sat off-screen and nothing said there was more\n * to the right; phone scrollbars are overlays that show only while you drag). Since\n * 0.25 that is the kit Table's own `edgeFade`, not a box of this table's round it:\n * measured, so a table that fits is painted as before; both ends once scrolled into\n * the middle; the reading direction's own in RTL; never animated; a `mask-image`,\n * which changes no layout and needs no background colour to fade to. A role select\n * focused behind the fade is scrolled clear of it, cell and all, and the Table's\n * wrapper says which ends are cut in `data-overflow`.\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 bodyProps,\n rowProps,\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 // The required mark only where it tells one option from another.\n const markRequired = roles.some((role) => role.required !== true);\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 edgeFade 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-caption font-normal text-muted\">\n {name}\n </span>\n <Select\n size=\"sm\"\n className=\"w-full\"\n // Its own width (the longest option) rather than the cell's: a cell\n // takes no width from a `w-full` select, so the role's name was\n // cut at the column's 9rem minimum. Stretched to the cell when that\n // is wider; capped where the column's name and values stop.\n selectClassName=\"w-auto min-w-full max-w-56\"\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 const marked = option.required === true && markRequired;\n return (\n <option\n key={option.value}\n value={option.value}\n // The short mark on screen, the word for a screen reader.\n aria-label={\n marked ? labels.requiredRole(option.label) : undefined\n }\n >\n {marked\n ? (labels.requiredRoleShort ?? markedShort)(option.label)\n : option.label}\n </option>\n );\n })}\n </Select>\n </TableHeaderCell>\n );\n })}\n </TableRow>\n </TableHead>\n <TableBody {...bodyProps}>\n {shown.map((row, index) => (\n <TableRow {...rowProps?.(row, index)} 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-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-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 /** The preview table's `<tbody>` attributes — {@link ColumnRoleTableProps.bodyProps}\n * (0.24): `{ \"data-private\": \"\" }` for a table of someone's own data. */\n bodyProps?: ColumnRoleTableBodyProps;\n /** One preview row's `<tr>` attributes — {@link ColumnRoleTableProps.rowProps}\n * (0.24). `row` is the row as read, `index` its place in the preview. */\n rowProps?: (row: readonly string[], index: number) => ColumnRoleTableRowProps | undefined;\n labels?: LabelOverride<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 bodyProps,\n rowProps,\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-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-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 bodyProps={bodyProps}\n rowProps={rowProps}\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-secondary\">\n {labels.unreadLine(line.line)}\n </span>\n <code className=\"block truncate font-mono text-muted\">\n {line.text}\n </code>\n </li>\n ))}\n {unread.length > UNREAD_LISTED && (\n <li className=\"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"]}
1
+ {"version":3,"sources":["../../src/components/column-mapper.tsx"],"sourcesContent":["import { useEffect, useEffectEvent, useState } from \"react\";\nimport type { ChangeEvent, DragEvent, HTMLAttributes, ReactNode } from \"react\";\nimport { Upload } from \"lucide-react\";\n\nimport { useKitLabels, useKitLocale, type LabelOverride } 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 type { FieldHelpProps } from \"./field-parts\";\nimport { Select } from \"./select\";\nimport { Textarea } from \"./textarea\";\nimport { DEFAULT_COLUMN_MAPPER_LABELS, markedShort } from \"./column-mapper-labels\";\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 — only while some role is NOT required: when\n * every one is, the mark tells no role from another (0.24, Kurvenschmiede). Since\n * 0.25 this is what a screen reader says for the option (its `aria-label`); the\n * option SHOWS {@link requiredRoleShort}. */\n requiredRole: (role: string) => string;\n /** The same required role as the select shows it (0.25): the role's name and a short\n * mark, so the mark does not cut the name short in a phone-width column — \"Booking\n * date (re…\" was all keksdose's closed select had room for. Spoken as\n * {@link requiredRole}. Optional, so a complete `UiKitLabels` typed before 0.25\n * still compiles; the asterisk needs no translation in most languages. */\n requiredRoleShort?: (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\n// Defined in ./column-mapper-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_COLUMN_MAPPER_LABELS };\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\n/** A `data-*` attribute, typed so `{ \"data-private\": \"\" }` needs no cast. */\ntype DataAttributes = { [key: `data-${string}`]: string | number | boolean | undefined };\n\n/**\n * What {@link ColumnRoleTableProps.bodyProps} puts on the preview's `<tbody>`: `data-*`\n * hooks, an `id`, a class, a style.\n */\nexport type ColumnRoleTableBodyProps = Omit<\n HTMLAttributes<HTMLTableSectionElement>,\n \"children\" | \"dangerouslySetInnerHTML\"\n> &\n DataAttributes;\n\n/**\n * What {@link ColumnRoleTableProps.rowProps} returns for one preview row's `<tr>` —\n * the same shape as LineItems' `LineItemsRowProps`, on a table row.\n */\nexport type ColumnRoleTableRowProps = Omit<\n HTMLAttributes<HTMLTableRowElement>,\n \"children\" | \"dangerouslySetInnerHTML\"\n> &\n DataAttributes;\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 /**\n * Extra attributes for the preview's `<tbody>` (0.24) — keksdose: the sample rows are\n * the user's own bank data (payees, memos, amounts), and its demo mode blurs exactly\n * that through `.demo-mode [data-private]` (#336). With nowhere to put the attribute,\n * its map step reached into the table with a `useLayoutEffect` that set it on the\n * `<tbody>` after every commit; `bodyProps={{ \"data-private\": \"\" }}` declares it.\n * There is deliberately no head counterpart: the column names and the role selects\n * are what such a mode is there to show.\n *\n * Spread on the kit `TableBody`; a `className` is added to its own.\n */\n bodyProps?: ColumnRoleTableBodyProps;\n /**\n * Extra attributes for one preview row's `<tr>`, from its cells and its index in the\n * preview (0.24) — the contract of LineItems' `rowProps` (keksdose K20), on a table:\n * `data-*` hooks (a per-row `data-private`, a test id), an `id`, a class. Called for\n * the rows drawn, the first {@link previewRows}. The row has no attributes of its own\n * to protect; a `className` is added after its classes.\n */\n rowProps?: (row: readonly string[], index: number) => ColumnRoleTableRowProps | undefined;\n labels?: LabelOverride<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}). What the\n * table still needs is named under it — a required group as \"either Amount, Debit or\n * Credit\". Required roles are marked in the list only where some role is not\n * (0.24): Kurvenschmiede's roles are all required, so the mark told no option from\n * another, and on a phone it cut the role's own name short (\"Zeit (s) (erforderl…\")\n * — the part of the option the reader has to read. The \"Still needed\" line already\n * says what is missing; a mark that distinguishes nothing is only noise.\n *\n * Where the mark does tell roles apart, it is short (0.25): keksdose's bank roles, on\n * a phone, still showed \"Booking date (re…\" in the closed select. The option shows\n * \"Booking date *\" ({@link ColumnMapperLabels.requiredRoleShort}) and is NAMED \"Booking\n * date (required)\" ({@link ColumnMapperLabels.requiredRole}, its `aria-label`), so a\n * screen reader hears the word rather than \"star\". And no role is cut at all: each\n * column is at least as wide as its select's longest option — the select's own width,\n * which a table cell otherwise ignores for a `w-full` control — up to 14rem, the width\n * the column's name and values already stop at. A long role name (\"Date de\n * comptabilisation\", \"Betrag (mit Vorzeichen)\") widens the columns, and the edge fade\n * says there are more of them to scroll to.\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. An edge with columns behind it fades out (0.24, Kurvenschmiede:\n * on a phone the fourth column's select sat off-screen and nothing said there was more\n * to the right; phone scrollbars are overlays that show only while you drag). Since\n * 0.25 that is the kit Table's own `edgeFade`, not a box of this table's round it:\n * measured, so a table that fits is painted as before; both ends once scrolled into\n * the middle; the reading direction's own in RTL; never animated; a `mask-image`,\n * which changes no layout and needs no background colour to fade to. A role select\n * focused behind the fade is scrolled clear of it, cell and all, and the Table's\n * wrapper says which ends are cut in `data-overflow`.\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 bodyProps,\n rowProps,\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 // The required mark only where it tells one option from another.\n const markRequired = roles.some((role) => role.required !== true);\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 edgeFade 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-caption font-normal text-muted\">\n {name}\n </span>\n <Select\n size=\"sm\"\n className=\"w-full\"\n // Its own width (the longest option) rather than the cell's: a cell\n // takes no width from a `w-full` select, so the role's name was\n // cut at the column's 9rem minimum. Stretched to the cell when that\n // is wider; capped where the column's name and values stop.\n selectClassName=\"w-auto min-w-full max-w-56\"\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 const marked = option.required === true && markRequired;\n return (\n <option\n key={option.value}\n value={option.value}\n // The short mark on screen, the word for a screen reader.\n aria-label={\n marked ? labels.requiredRole(option.label) : undefined\n }\n >\n {marked\n ? (labels.requiredRoleShort ?? markedShort)(option.label)\n : option.label}\n </option>\n );\n })}\n </Select>\n </TableHeaderCell>\n );\n })}\n </TableRow>\n </TableHead>\n <TableBody {...bodyProps}>\n {shown.map((row, index) => (\n <TableRow {...rowProps?.(row, index)} 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-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-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> extends FieldHelpProps {\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`. The paste box's\n * `help` (0.36) is `help`. */\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 /** The preview table's `<tbody>` attributes — {@link ColumnRoleTableProps.bodyProps}\n * (0.24): `{ \"data-private\": \"\" }` for a table of someone's own data. */\n bodyProps?: ColumnRoleTableBodyProps;\n /** One preview row's `<tr>` attributes — {@link ColumnRoleTableProps.rowProps}\n * (0.24). `row` is the row as read, `index` its place in the preview. */\n rowProps?: (row: readonly string[], index: number) => ColumnRoleTableRowProps | undefined;\n labels?: LabelOverride<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 help,\n accept = DEFAULT_ACCEPT,\n maxSize,\n defaultText = \"\",\n previewRows = 5,\n guess,\n disabled = false,\n bodyProps,\n rowProps,\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 help={help}\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-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-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 bodyProps={bodyProps}\n rowProps={rowProps}\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-secondary\">\n {labels.unreadLine(line.line)}\n </span>\n <code className=\"block truncate font-mono text-muted\">\n {line.text}\n </code>\n </li>\n ))}\n {unread.length > UNREAD_LISTED && (\n <li className=\"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"]}
@@ -1,5 +1,5 @@
1
1
  import type { ComponentPropsWithoutRef, ReactNode, Ref } from "react";
2
- import { type DisabledReasonDisplay } from "./field-parts.js";
2
+ import { type DisabledReasonDisplay, type FieldHelpProps } from "./field-parts.js";
3
3
  import type { CommitScope } from "./write-lock.js";
4
4
  export { InlineEntityCombobox } from "./inline-entity-combobox.js";
5
5
  export type { InlineEntityComboboxProps } from "./inline-entity-combobox.js";
@@ -10,7 +10,7 @@ export type { InlineEntityComboboxProps } from "./inline-entity-combobox.js";
10
10
  * are the field's — a value, a departure, and "I mean what I typed" — rather than
11
11
  * the DOM events of the wrapper. Everything else a wrapper needs reaches the root.
12
12
  */
13
- export interface ComboboxProps extends Omit<ComponentPropsWithoutRef<"div">, "id" | "onChange" | "onBlur" | "onSubmit"> {
13
+ export interface ComboboxProps extends Omit<ComponentPropsWithoutRef<"div">, "id" | "onChange" | "onBlur" | "onSubmit">, FieldHelpProps {
14
14
  value: string;
15
15
  onChange: (v: string) => void;
16
16
  /** Suggestion pool (e.g. existing payee names). */
@@ -43,6 +43,7 @@ function Combobox(props) {
43
43
  invalid,
44
44
  error,
45
45
  hint,
46
+ help,
46
47
  disabled,
47
48
  disabledReason,
48
49
  disabledReasonDisplay,
@@ -61,7 +62,7 @@ function Combobox(props) {
61
62
  "aria-required": ariaRequired,
62
63
  ...rest
63
64
  } = props;
64
- const hintParts = useFieldHint(hint, ariaDescribedBy);
65
+ const hintParts = useFieldHint(hint, ariaDescribedBy, { help, name: label ?? ariaLabel });
65
66
  const field = useFieldError(error, invalid, hintParts.describedBy, ariaInvalid);
66
67
  const generated = useId();
67
68
  const fieldId = id ?? generated;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/combobox.tsx"],"sourcesContent":["import { Fragment, useId, useMemo, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode, Ref } from \"react\";\nimport { ChevronDown } from \"lucide-react\";\nimport { FIELD_BASE, FIELD_FLOATING_PAD, FIELD_INVALID } from \"./field-base\";\nimport { cn } from \"../lib/cn\";\nimport { useDropdown } from \"./dropdown\";\nimport { takeEscape, useDismissLayer } from \"../hooks/use-dismiss\";\nimport { isImeEnter } from \"../lib/ime\";\nimport { usePhoneLayout } from \"../hooks/use-breakpoint\";\nimport { PickerSheet, SHEET_ROW_CLASS } from \"./picker-sheet\";\nimport { ComboboxFieldLabel, endHintRowProps, useActiveOptionScroll } from \"./combobox-core\";\nimport { useKitLocale } from \"../i18n/kit-labels\";\nimport { rankMatch } from \"../lib/text-match\";\nimport {\n EndHintRow,\n FieldCaption,\n LABEL_IN_ROW,\n LockedReason,\n mergeDescribedBy,\n StaticLabelRow,\n useFieldError,\n useFieldHint,\n useLockReason,\n type DisabledReasonDisplay,\n} from \"./field-parts\";\nimport type { CommitScope } from \"./write-lock\";\nimport { lockedKeyDown, rowClass, SuggestionList, usePrimaryPressOnly } from \"./combobox-suggestions\";\n// The id-keyed sibling lives in inline-entity-combobox.tsx (MOD-9), the list both share\n// in combobox-suggestions.tsx; its public names are still this module's.\nexport { InlineEntityCombobox } from \"./inline-entity-combobox\";\nexport type { InlineEntityComboboxProps } from \"./inline-entity-combobox\";\n\n/**\n * Four of the div's own attributes are omitted because this component already owns\n * the name, with a different meaning: `id` is the INPUT's (a caller labels or\n * automates the field, not the box around it), and `onChange`/`onBlur`/`onSubmit`\n * are the field's — a value, a departure, and \"I mean what I typed\" — rather than\n * the DOM events of the wrapper. Everything else a wrapper needs reaches the root.\n */\nexport interface ComboboxProps\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"id\" | \"onChange\" | \"onBlur\" | \"onSubmit\"> {\n value: string;\n onChange: (v: string) => void;\n /** Suggestion pool (e.g. existing payee names). */\n options: string[];\n /** The visible label, a `<label for>` above the field: a ReactNode, as on every kit\n * field (a string until 0.34, refactor audit 2026-10, API-9). Without one, name the\n * field with `aria-label`. */\n label?: ReactNode;\n id?: string;\n placeholder?: string;\n className?: string;\n /** Required and unanswered — see {@link Input}'s `invalid`. Set on the `<input>`\n * itself rather than on the wrapper, which is what let Keksdose's account picker\n * delete the `[&_input]:…` copy of {@link FIELD_INVALID} it had been carrying\n * because this component had no `invalid` of its own. */\n invalid?: boolean;\n /** What is wrong with the value, as {@link Input}'s `error`: rendered under the\n * field, on the `<input>`'s `aria-describedby`, and implies `invalid` (lenkbank's\n * scope fields, which moved here off `<Input list>` + `<datalist>`). */\n error?: ReactNode;\n /**\n * Standing advice for the field — {@link Input}'s `hint`, so the combobox family\n * reads like every other field (keksdose K4: the family had `error` and no `hint`, so\n * the app's pickers carried their captions outside the component, attached to\n * nothing). Plain TEXT is a caption under the field on the input's\n * `aria-describedby`, before any error; a {@link FieldHint} \"?\" rides the label line,\n * or with no label sits at the field's end edge, outside the box.\n */\n hint?: ReactNode;\n /** As `<input disabled>`: no focus, no list, the field's settled look. The chevron\n * stops toggling with it — it is a mouse target the input's own `disabled` does\n * not reach. */\n disabled?: boolean;\n /**\n * Why the value cannot be changed — {@link Button}'s `disabledReason`, for a field\n * that SAVES itself (an inline editor whose blur writes). The input stays focusable\n * and `aria-disabled`, is `readOnly`, opens no list and calls neither `onChange` nor\n * `onSubmit`; the reason is in the kit {@link Tooltip} and on its `aria-describedby`.\n * Wins over `disabled`. The family's lock (keksdose G2) — see {@link InlineEntityCombobox}.\n */\n disabledReason?: ReactNode;\n /** This field COMMITS. Under a locked {@link WriteLockProvider} it is locked the\n * `disabledReason` way with the lock's reason; inside a form with its own Save, leave\n * it off and put `commit` on the Save. No provider, or an unlocked one: no effect. */\n commit?: CommitScope;\n /** Where the reason shows — {@link Button}'s `disabledReasonDisplay`: a line under the\n * field at Large and on a touch screen by default, the tooltip otherwise (DUP-3). */\n disabledReasonDisplay?: DisabledReasonDisplay;\n /** Heading an option belongs under. Supplying it makes this list read exactly\n * like {@link InlineEntityCombobox}'s — one heading per group with its rows\n * indented beneath — instead of a flat list (feedback #136 rework: the payee\n * field sat next to the newly-grouped category field and no longer matched).\n * Omit for a plain list. */\n groupBy?: (option: string) => string;\n /**\n * Something to show at the far end of an option's row — a badge saying what the\n * option IS, as distinct from what it is called.\n *\n * Keksdose's category-name field is the case: its proposals carry per-locale\n * names, so picking one stores a category that follows the UI language, and\n * picking a look-alike custom name does not. That difference is invisible in the\n * label and decides what the row DOES, so the row has to show it. Returning\n * `null` for an option renders nothing and costs no layout.\n *\n * Not rendered in the create row, which by definition names nothing in the pool.\n */\n optionAdornment?: (option: string) => ReactNode;\n /** Focus on mount, the way `<input autoFocus>` does — and, through `onFocus`, open\n * the list with it. A click-the-value inline editor needs it: the cell the user\n * clicked names the field the caret should land in. {@link InlineEntityCombobox}\n * has had it since live #218. */\n autoFocus?: boolean;\n /** Focus genuinely LEFT the field. Not fired by picking a row — the rows suppress\n * `mousedown`, so the input never blurs — which is what makes it usable as a\n * \"close the inline editor\" signal. */\n onBlur?: () => void;\n /** Enter pressed with no row highlighted, i.e. \"I mean what I typed\". The list\n * closes either way; this is for a caller whose Enter also submits a row editor. */\n onSubmit?: () => void;\n /** Rows to offer when the field is empty. 8 on a dropdown, where that is all\n * that fits; the phone sheet asks for more because it has a screen. */\n maxSuggestions?: number;\n searchPlaceholder?: string;\n closeLabel?: string;\n /**\n * The phone sheet's heading — and, as a string, the dialog's accessible name — for a\n * field whose label is drawn by someone else: a bound field's label above it,\n * which `RhfTextCombobox` hands over here (0.24). Default: `label`. Without either, the\n * full-screen sheet opened untitled and unnamed, the one screen on a phone that has to\n * say what it is asking for (live #200).\n */\n sheetTitle?: ReactNode;\n /**\n * Label for the row that COMMITS a value the list does not contain — e.g.\n * `(v) => \\`Add \"${v}\" as payee\\``. Supplying it turns the free text into\n * something you confirm rather than something you leave behind.\n *\n * Keksdose live #212: *\"For the payee input have an apply button, when input is\n * placed like 'add xxx as payee' to confirm. Now it can be put in and closed with\n * the right top X, which seems not intuitive.\"* On the phone sheet the search box\n * IS the value, so typing a brand-new name and dismissing the sheet did commit it —\n * but the only control on offer was the close X, which reads as \"discard\". A named\n * affordance says what the typing did.\n *\n * Omit and the row is not rendered, which is the right default for a list whose\n * values are all supposed to already exist.\n */\n createLabel?: (value: string) => string;\n /**\n * The `<input>` — the control a reader types in and the one focus belongs on — as a\n * React 19 ref prop, the way {@link CountrySelect} takes its trigger's (0.24, kastlan:\n * react-hook-form's focus-on-error calls `focus()` on whatever `field.ref` is handed,\n * and a combobox with no ref inside a hand-wired `RhfField` gave it nothing, so a\n * failed submit could not take the user to it). Not the wrapper: a ref is for focus,\n * measurement and selection, and all three are the input's. `RhfTextCombobox` passes it.\n */\n ref?: Ref<HTMLInputElement>;\n}\n\n/**\n * Free-text combobox that looks like the shared {@link Select} (same FIELD_BASE\n * styling, chevron and static floating label) but lets the user type a value\n * that isn't in the list — e.g. naming a brand-new payee. Picking from the\n * filtered suggestion list fills the value; typing keeps whatever was entered.\n *\n * Use this instead of an `<Input list=\"…\">` + `<datalist>` so the control is\n * visually and behaviourally consistent with the other dropdowns (feedback\n * #230 — the payee field looked/behaved differently from every other select).\n */\nexport function Combobox(props: ComboboxProps) {\n const {\n value,\n onChange,\n options,\n label,\n id,\n placeholder,\n className,\n groupBy,\n maxSuggestions,\n searchPlaceholder,\n closeLabel,\n sheetTitle,\n createLabel,\n invalid,\n error,\n hint,\n disabled,\n disabledReason,\n disabledReasonDisplay,\n commit,\n optionAdornment,\n autoFocus,\n onBlur,\n onSubmit,\n ref,\n \"aria-label\": ariaLabel,\n // Off `rest` and onto the <input>, which is the combobox a reader meets: `Field`'s\n // render-prop spreads `{ id, aria-describedby, aria-invalid, aria-required }`, and\n // on the wrapper div the hint and required state described nothing.\n \"aria-describedby\": ariaDescribedBy,\n \"aria-invalid\": ariaInvalid,\n \"aria-required\": ariaRequired,\n ...rest\n } = props;\n const hintParts = useFieldHint(hint, ariaDescribedBy);\n const field = useFieldError(error, invalid, hintParts.describedBy, ariaInvalid);\n const generated = useId();\n const fieldId = id ?? generated;\n // Derived from the GENERATED id, never from `id`: a caller's id is theirs to\n // collide with, and `aria-controls` has to resolve.\n const listboxId = `${generated}-listbox`;\n const optionId = (index: number) => `${generated}-option-${index}`;\n // `backCloses` is the desktop half of live #309: the phone's list IS a\n // {@link PickerSheet}, which registers its own history entry, so registering a\n // second one here would cost two Back presses to dismiss one sheet. Its Escape\n // layer is this field's own, below — the list can be open with nothing to show.\n const isPhone = usePhoneLayout();\n const { open, setOpen, wrapperRef, panelRef } = useDropdown({ backCloses: !isPhone, escapeLayer: false });\n const primaryOnly = usePrimaryPressOnly();\n const [active, setActive] = useState(-1);\n // The lock (see `lockedKeyDown`): a lock arriving while the list is up closes it,\n // adjusted while rendering so no frame offers a row that can no longer be taken.\n const lock = useLockReason(commit, disabledReason);\n const locked = lock.locked;\n const inert = locked || Boolean(disabled);\n if (locked && open) setOpen(false);\n // A phone opens the list as a full-screen sheet with its own input, the way a\n // native <select> does — live #200: \"Paid as full screen dialog with input.\n // Similar to the account select that already appears as full screen.\" The\n // anchored list stays for pointer devices, where it is the better shape.\n const sheetInputRef = useRef<HTMLInputElement | null>(null);\n // The box the portalled list hangs off — the inner wrapper, which hugs the input,\n // not the outer one (a grid item that can be taller than the field).\n const fieldRef = useRef<HTMLDivElement>(null);\n\n /**\n * Is the text in the field a QUERY, or just what was picked last time?\n *\n * Keksdose dev#549, filed against the category-name field: *\"Currently it acts kind\n * of a filter and clicking again does not open anything since the previous text is\n * still there likely filtering all other options out.\"* A combobox whose filter is\n * its own value can only ever re-offer the answer it already has — after picking\n * \"REWE\" the list reopens holding one row, the one you are looking at, and changing\n * your mind means clearing the field by hand first.\n *\n * So the text filters only while it is being TYPED; opening the list, by focus or\n * by clicking the field again, puts the whole pool back. {@link InlineEntityCombobox}\n * below has had this since it was written (see its `typedQuery`) and the app's\n * category-name field grew its own copy to get it — this one, the payee field, was\n * the one left with the bug.\n *\n * Not derived from \"does the text exactly match an option\": a custom value that\n * happens to collide with one would then behave differently from every other, and\n * what is being tracked is what the user just did, which is not a property of the\n * string.\n */\n const [typing, setTyping] = useState(false);\n\n const query = typing ? value.trim() : \"\";\n // Folded as every kit list folds (DUP-5): \"zurich\" finds Zürich, by the locale's case.\n const locale = useKitLocale();\n const matches = useMemo(() => {\n const seen = new Set<string>();\n const uniq = options.filter((o) => o && !seen.has(o) && seen.add(o));\n let ranked: string[];\n const cap = maxSuggestions ?? (isPhone ? 50 : 8);\n if (!query) {\n ranked = uniq.slice(0, cap);\n } else {\n // The whole name, then prefix matches, then substring matches — most relevant on\n // top (`rankMatch`; a stable sort keeps the pool's order within a rank).\n ranked = uniq\n .map((o) => ({ o, rank: rankMatch(o, query, { locale }) }))\n .filter((x) => x.rank >= 0)\n .sort((a, b) => a.rank - b.rank)\n .map((x) => x.o)\n .slice(0, cap);\n }\n if (!groupBy) return ranked;\n // Keep each group contiguous so a heading appears once instead of every time\n // the ranking interleaves two groups — while preserving \"best match first\":\n // a Map keeps insertion order, so groups come out ordered by their best-ranked\n // member and members keep their rank order inside the group.\n const byGroup = new Map<string, string[]>();\n for (const o of ranked) {\n const key = groupBy(o);\n if (!byGroup.has(key)) byGroup.set(key, []);\n byGroup.get(key)!.push(o);\n }\n return [...byGroup.values()].flat();\n }, [options, query, groupBy, maxSuggestions, isPhone, locale]);\n\n /**\n * Which rows OPEN a group, and with what heading — computed once for both lists.\n *\n * The desktop list had this as `matches[i - 1]`, which is free; the sheet had it as\n * `matches[matches.indexOf(o) - 1]`, which is a scan of the list per row and another\n * per heading test. That is quadratic in the sheet's own cap of 50, on the branch\n * that runs on the slowest hardware we ship to — and it answers the wrong question\n * besides: `indexOf` finds the FIRST row with that string, so a duplicated option\n * would have compared against a neighbour it is not next to. (`matches` de-duplicates\n * today; that is a property of the memo above, not of the row being rendered.)\n */\n const groupHeadings = useMemo(() => {\n if (!groupBy) return null;\n const heads = new Map<number, string>();\n let prev: string | undefined;\n matches.forEach((o, i) => {\n const group = groupBy(o);\n if (group != null && group !== prev) heads.set(i, group);\n prev = group;\n });\n return heads;\n }, [matches, groupBy]);\n\n const take = (v: string) => {\n if (locked) return;\n onChange(v);\n setOpen(false);\n setActive(-1);\n // What is in the field is now an ANSWER, not a query — so reopening offers the\n // whole pool again rather than a one-row filter of the value just chosen.\n setTyping(false);\n };\n\n // Offered only when the typed text is genuinely NOT in the pool: an exact\n // (case-insensitive) match is an existing entry the list is already showing, and a\n // second way to pick it would be noise. Compared against `options`, not `matches` —\n // `matches` is capped, so a pool of 300 payees would otherwise offer to \"add\" one\n // that exists but fell off the end of the list.\n // What the field claims the keyboard is on. Only on a pointer device: the phone\n // renders its rows in a {@link PickerSheet} whose own search box owns focus, and a\n // field pointing at an id that is not there is worse than one pointing nowhere.\n const activeId = !isPhone && active >= 0 && active < matches.length ? optionId(active) : undefined;\n useActiveOptionScroll(activeId);\n\n // While locked the field sits in the reason's Tooltip — in a fragment with a hidden\n // copy of the reason, Button's anatomy; `block` so a full-width field stays full width.\n const withLock = (box: ReactNode) =>\n locked ? (\n <LockedReason lock={lock} display={disabledReasonDisplay} className=\"block\">\n {box}\n </LockedReason>\n ) : (\n box\n );\n\n const typed = value.trim();\n const createRow =\n createLabel && typed.length > 0 && !options.some((o) => o.toLowerCase() === typed.toLowerCase())\n ? createLabel(typed)\n : null;\n\n // The list is a layer on the Escape stack while it SHOWS (Wave 2, DUP-1): an Escape\n // over an empty \"open\" list is the dialog's, as it always was. On a phone the sheet\n // is the layer.\n const showing = !isPhone && open && !inert && (matches.length > 0 || Boolean(createRow));\n useDismissLayer(showing, () => {\n setOpen(false);\n setActive(-1);\n });\n\n return (\n // `rest` dresses the outer box — a `data-tour` anchor, a test id. Not the NAME,\n // description, invalid or required state: those belong on the <input> below,\n // which is the combobox a reader meets. Spread FIRST, so the field's ARIA and the\n // handlers carrying live #309 and dev#549 cannot be replaced from outside.\n <div {...rest} ref={wrapperRef} className={cn(\"relative\", className)}>\n {/* A real <label for> since lenkbank's tests met a field `getByLabelText` could\n not find: it used to be a <span>, with the text copied onto `aria-label`. The\n name a reader hears is the same words either way. With a \"?\" it moves into a\n row after the field, below. */}\n {label !== undefined && hintParts.labelHint === undefined && (\n <ComboboxFieldLabel htmlFor={fieldId} className={inert ? \"opacity-50\" : undefined}>\n {label}\n </ComboboxFieldLabel>\n )}\n <EndHintRow {...endHintRowProps(\"hint\" in props, label !== undefined, hintParts.labelHint)}>\n {withLock(\n /* The chevron centers against this inner wrapper, which hugs the input.\n The outer div can be taller than the input (as a grid item it\n stretches to the row height, e.g. next to the editor's category cell\n with its split button), which used to drag a top-1/2 chevron down to\n the input's bottom edge (feedback #248).\n Dimmed as a whole when disabled (or locked), the way EntityCombobox dims its\n trigger. FIELD_BASE's grey alone left a disabled picker looking like a\n filled-in one beside the pickers that do dim (the label is not the input's\n `peer`, so it is dimmed by hand above). */\n <div ref={fieldRef} className={cn(\"relative\", inert && \"cursor-not-allowed opacity-50\")}>\n <input\n ref={ref}\n id={fieldId}\n value={value}\n placeholder={placeholder}\n // The caller's name, over the <label for> above — which is what names the\n // field otherwise (dev#477: a field with neither announces only its text).\n aria-label={ariaLabel}\n role=\"combobox\"\n aria-expanded={open}\n // The list this field is the mouth of. Required by the role, and the half\n // that was missing: the field said it was expanded and never said what it\n // had expanded, so a reader had no way from the box to the options\n // (ESLint's `role-has-required-aria-props`, the audit's §a11y).\n aria-controls={listboxId}\n aria-activedescendant={activeId}\n aria-autocomplete=\"list\"\n aria-invalid={field.isInvalid || undefined}\n aria-describedby={mergeDescribedBy(field.describedBy, locked && lock.reasonId)}\n aria-required={ariaRequired}\n autoComplete=\"off\"\n // Locked: focusable, `readOnly`, `aria-disabled` — see `lockedKeyDown`.\n disabled={locked ? undefined : disabled}\n readOnly={locked || undefined}\n aria-disabled={locked || undefined}\n // eslint-disable-next-line jsx-a11y/no-autofocus -- a documented prop the caller opts into (off by default); the field never takes focus on its own.\n autoFocus={autoFocus}\n onBlur={onBlur}\n onMouseDown={primaryOnly.onMouseDown}\n onFocus={() => {\n // A back/forward mouse button focuses this field on its way to\n // navigating; it is not a request to open anything (live #309 rework).\n // A locked field takes focus to say why, and opens nothing.\n if (primaryOnly.fromAuxButton() || locked) return;\n setOpen(true);\n setTyping(false);\n // The sheet carries its own input, so the field behind it must not also\n // pull up the keyboard and scroll the page under the dialog.\n if (isPhone) sheetInputRef.current?.focus();\n }}\n // A CLICK as well as focus — the other half of dev#549. Picking a suggestion\n // closes the list without moving focus (the rows suppress `mousedown` on\n // purpose, so the input never blurred), which means clicking the field again\n // fires no `focus` event at all and the list stayed shut. \"Does not open\n // anything\" was literally true.\n onClick={() => {\n if (locked) return;\n setOpen(true);\n setTyping(false);\n }}\n // `inputMode=\"none\"` rather than readOnly: the field must not look\n // uneditable (FIELD_BASE greys a read-only field since dev#468) and must\n // still take focus — it just has no keyboard of its own, the same trick\n // the amount field uses for the numpad.\n inputMode={isPhone ? \"none\" : undefined}\n onChange={(e) => {\n if (locked) return;\n onChange(e.target.value);\n setOpen(true);\n setActive(-1);\n setTyping(true);\n }}\n onKeyDown={(e) => {\n if (locked) return lockedKeyDown(e);\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n setOpen(true);\n setActive((i) => Math.min(i + 1, matches.length - 1));\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n setActive((i) => Math.max(i - 1, 0));\n } else if (e.key === \"Enter\") {\n // The Enter that confirms an IME composition is the IME's: it picks no\n // row and submits nothing (BUG-9).\n if (isImeEnter(e)) return;\n if (open && active >= 0 && active < matches.length) {\n e.preventDefault();\n take(matches[active]);\n } else {\n // No row highlighted: the typed text is the answer. `preventDefault`\n // only when a caller is taking Enter, so a plain form submit is\n // otherwise left alone.\n if (onSubmit) e.preventDefault();\n setOpen(false);\n onSubmit?.();\n }\n } else if (e.key === \"Escape\") {\n // A list that is SHOWING is the top of the Escape stack, and the key\n // closes it and stops here: a Modal or FullBleedDialog around the field\n // otherwise closed with it, form and all (round 0.34 BUG-1). With no\n // list on screen the key goes on to whoever wants it.\n if (showing && takeEscape(e)) return;\n setOpen(false);\n setActive(-1);\n } else if (e.key === \"Tab\") {\n // Closes, and lets the browser take the Tab: focus is in THIS input,\n // which is staying, so there is nothing to catch — unlike the pickers\n // whose focus sits inside a portalled panel.\n setOpen(false);\n setActive(-1);\n }\n // Home/End are deliberately absent. The APG gives them to the list only\n // where the combobox is not editable; here the text IS the value, a\n // payee name is long enough to want the caret moved to its start, and\n // the desktop list is capped at 8 rows — so jumping it would be worth\n // almost nothing and would cost the one gesture that field is used with.\n }}\n className={cn(\n FIELD_BASE,\n label !== undefined && FIELD_FLOATING_PAD,\n \"pe-9\",\n field.isInvalid && FIELD_INVALID,\n )}\n />\n <ChevronDown\n aria-hidden\n onMouseDown={(e) => {\n // Toggle on the chevron without stealing focus from the input.\n e.preventDefault();\n if (!inert) setOpen((o) => !o);\n }}\n className={cn(\n \"absolute end-2.5 top-1/2 size-4 -translate-y-1/2 text-placeholder\",\n // The box above does the dimming; dimming here too would halve it again.\n !inert && \"cursor-pointer\",\n )}\n />\n </div>,\n )}\n </EndHintRow>\n {/* The label and its \"?\" share the top strip — after the field in the DOM, so the\n \"?\" follows the control in the tab order, as it does on Input and Select. */}\n {label !== undefined && hintParts.labelHint !== undefined && (\n <StaticLabelRow hint={hintParts.labelHint}>\n <ComboboxFieldLabel htmlFor={fieldId} className={cn(LABEL_IN_ROW, inert && \"opacity-50\")}>\n {label}\n </ComboboxFieldLabel>\n </StaticLabelRow>\n )}\n <FieldCaption parts={hintParts} />\n {field.errorEl}\n {isPhone && (\n <PickerSheet\n open={open && !inert}\n onClose={() => setOpen(false)}\n title={sheetTitle ?? label}\n // The sheet's input IS the field: this is a free-text control, so what is\n // typed here is the value (a brand-new payee is just a name nothing\n // matches), and the list below narrows as it changes.\n query={value}\n onQueryChange={(v) => {\n onChange(v);\n setTyping(true);\n }}\n searchPlaceholder={searchPlaceholder ?? placeholder}\n inputRef={sheetInputRef}\n closeLabel={closeLabel}\n >\n <ul id={listboxId} role=\"listbox\">\n {createRow && (\n // `role=\"option\"` rides the BUTTON, not the <li> around it: an option\n // may not contain a separately focusable control, and the button is\n // what a pointer presses. Same shape `CommandPalette` has always had.\n <li role=\"presentation\">\n <button\n type=\"button\"\n role=\"option\"\n aria-selected={false}\n onClick={() => take(typed)}\n className={cn(SHEET_ROW_CLASS, \"font-medium text-brand\")}\n >\n {createRow}\n </button>\n </li>\n )}\n {matches.map((o, i) => {\n const group = groupHeadings?.get(i);\n return (\n <Fragment key={o}>\n {group !== undefined && (\n <li\n role=\"presentation\"\n className=\"px-4 pb-0.5 pt-3 text-caption font-semibold uppercase tracking-wide text-muted\"\n >\n {group}\n </li>\n )}\n <li role=\"presentation\">\n <button\n type=\"button\"\n role=\"option\"\n aria-selected={o === value}\n onClick={() => take(o)}\n className={cn(\n SHEET_ROW_CLASS,\n optionAdornment && \"flex items-center justify-between gap-2\",\n )}\n >\n {optionAdornment ? <span className=\"truncate-until-large\">{o}</span> : o}\n {optionAdornment?.(o)}\n </button>\n </li>\n </Fragment>\n );\n })}\n </ul>\n </PickerSheet>\n )}\n {showing && (\n <SuggestionList id={listboxId} anchorRef={fieldRef} panelRef={panelRef}>\n {createRow && (\n <li role=\"presentation\">\n <button\n type=\"button\"\n role=\"option\"\n aria-selected={false}\n tabIndex={-1}\n onMouseDown={(e) => {\n // mousedown, like the rows below: the input's blur would otherwise\n // close the list before the click landed.\n e.preventDefault();\n take(typed);\n }}\n className={cn(rowClass(false), \"font-medium text-brand\")}\n >\n {createRow}\n </button>\n </li>\n )}\n {matches.map((o, i) => {\n // Heading at each group boundary only — `matches` is group-contiguous, so\n // the boundaries are a property of the list rather than of the branch\n // drawing it. See `groupHeadings`.\n const group = groupHeadings?.get(i);\n return (\n <Fragment key={o}>\n {group !== undefined && (\n <li\n role=\"presentation\"\n className=\"px-3 pb-0.5 pt-2 text-caption font-semibold uppercase tracking-wide text-muted first:pt-1\"\n >\n {group}\n </li>\n )}\n <li role=\"presentation\">\n <button\n type=\"button\"\n id={optionId(i)}\n role=\"option\"\n // CHOSEN, not highlighted. The row the arrows are on is named by\n // the field's `aria-activedescendant`; saying \"selected\" for it\n // would tell a reader that the row they are passing over is\n // already the answer.\n aria-selected={o === value}\n // Focus stays in the field — that is what\n // `aria-activedescendant` is for — so the rows are not tab stops.\n tabIndex={-1}\n onMouseDown={(e) => {\n // mousedown (not click) so the blur from the input firing first\n // doesn't close the list before the selection registers.\n e.preventDefault();\n take(o);\n }}\n onMouseEnter={() => setActive(i)}\n className={cn(\n rowClass(i === active),\n group != null && \"ps-6\",\n // A row with a badge is a flex row so the label truncates and the\n // badge keeps its width; without one it stays the plain block the\n // other lists render, so nothing shifts for callers that pass none.\n // From Large up the label wraps instead (`truncate-until-large`,\n // text size §10.17, round 0.34 T1), as the badge-less row does.\n optionAdornment && \"flex items-center justify-between gap-2\",\n )}\n >\n {optionAdornment ? <span className=\"truncate-until-large\">{o}</span> : o}\n {optionAdornment?.(o)}\n </button>\n </li>\n </Fragment>\n );\n })}\n </SuggestionList>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/combobox.tsx"],"sourcesContent":["import { Fragment, useId, useMemo, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode, Ref } from \"react\";\nimport { ChevronDown } from \"lucide-react\";\nimport { FIELD_BASE, FIELD_FLOATING_PAD, FIELD_INVALID } from \"./field-base\";\nimport { cn } from \"../lib/cn\";\nimport { useDropdown } from \"./dropdown\";\nimport { takeEscape, useDismissLayer } from \"../hooks/use-dismiss\";\nimport { isImeEnter } from \"../lib/ime\";\nimport { usePhoneLayout } from \"../hooks/use-breakpoint\";\nimport { PickerSheet, SHEET_ROW_CLASS } from \"./picker-sheet\";\nimport { ComboboxFieldLabel, endHintRowProps, useActiveOptionScroll } from \"./combobox-core\";\nimport { useKitLocale } from \"../i18n/kit-labels\";\nimport { rankMatch } from \"../lib/text-match\";\nimport {\n EndHintRow,\n FieldCaption,\n LABEL_IN_ROW,\n LockedReason,\n mergeDescribedBy,\n StaticLabelRow,\n useFieldError,\n useFieldHint,\n useLockReason,\n type DisabledReasonDisplay,\n type FieldHelpProps,\n} from \"./field-parts\";\nimport type { CommitScope } from \"./write-lock\";\nimport { lockedKeyDown, rowClass, SuggestionList, usePrimaryPressOnly } from \"./combobox-suggestions\";\n// The id-keyed sibling lives in inline-entity-combobox.tsx (MOD-9), the list both share\n// in combobox-suggestions.tsx; its public names are still this module's.\nexport { InlineEntityCombobox } from \"./inline-entity-combobox\";\nexport type { InlineEntityComboboxProps } from \"./inline-entity-combobox\";\n\n/**\n * Four of the div's own attributes are omitted because this component already owns\n * the name, with a different meaning: `id` is the INPUT's (a caller labels or\n * automates the field, not the box around it), and `onChange`/`onBlur`/`onSubmit`\n * are the field's — a value, a departure, and \"I mean what I typed\" — rather than\n * the DOM events of the wrapper. Everything else a wrapper needs reaches the root.\n */\nexport interface ComboboxProps\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"id\" | \"onChange\" | \"onBlur\" | \"onSubmit\">,\n FieldHelpProps {\n value: string;\n onChange: (v: string) => void;\n /** Suggestion pool (e.g. existing payee names). */\n options: string[];\n /** The visible label, a `<label for>` above the field: a ReactNode, as on every kit\n * field (a string until 0.34, refactor audit 2026-10, API-9). Without one, name the\n * field with `aria-label`. */\n label?: ReactNode;\n id?: string;\n placeholder?: string;\n className?: string;\n /** Required and unanswered — see {@link Input}'s `invalid`. Set on the `<input>`\n * itself rather than on the wrapper, which is what let Keksdose's account picker\n * delete the `[&_input]:…` copy of {@link FIELD_INVALID} it had been carrying\n * because this component had no `invalid` of its own. */\n invalid?: boolean;\n /** What is wrong with the value, as {@link Input}'s `error`: rendered under the\n * field, on the `<input>`'s `aria-describedby`, and implies `invalid` (lenkbank's\n * scope fields, which moved here off `<Input list>` + `<datalist>`). */\n error?: ReactNode;\n /**\n * Standing advice for the field — {@link Input}'s `hint`, so the combobox family\n * reads like every other field (keksdose K4: the family had `error` and no `hint`, so\n * the app's pickers carried their captions outside the component, attached to\n * nothing). Plain TEXT is a caption under the field on the input's\n * `aria-describedby`, before any error; a {@link FieldHint} \"?\" rides the label line,\n * or with no label sits at the field's end edge, outside the box.\n */\n hint?: ReactNode;\n /** As `<input disabled>`: no focus, no list, the field's settled look. The chevron\n * stops toggling with it — it is a mouse target the input's own `disabled` does\n * not reach. */\n disabled?: boolean;\n /**\n * Why the value cannot be changed — {@link Button}'s `disabledReason`, for a field\n * that SAVES itself (an inline editor whose blur writes). The input stays focusable\n * and `aria-disabled`, is `readOnly`, opens no list and calls neither `onChange` nor\n * `onSubmit`; the reason is in the kit {@link Tooltip} and on its `aria-describedby`.\n * Wins over `disabled`. The family's lock (keksdose G2) — see {@link InlineEntityCombobox}.\n */\n disabledReason?: ReactNode;\n /** This field COMMITS. Under a locked {@link WriteLockProvider} it is locked the\n * `disabledReason` way with the lock's reason; inside a form with its own Save, leave\n * it off and put `commit` on the Save. No provider, or an unlocked one: no effect. */\n commit?: CommitScope;\n /** Where the reason shows — {@link Button}'s `disabledReasonDisplay`: a line under the\n * field at Large and on a touch screen by default, the tooltip otherwise (DUP-3). */\n disabledReasonDisplay?: DisabledReasonDisplay;\n /** Heading an option belongs under. Supplying it makes this list read exactly\n * like {@link InlineEntityCombobox}'s — one heading per group with its rows\n * indented beneath — instead of a flat list (feedback #136 rework: the payee\n * field sat next to the newly-grouped category field and no longer matched).\n * Omit for a plain list. */\n groupBy?: (option: string) => string;\n /**\n * Something to show at the far end of an option's row — a badge saying what the\n * option IS, as distinct from what it is called.\n *\n * Keksdose's category-name field is the case: its proposals carry per-locale\n * names, so picking one stores a category that follows the UI language, and\n * picking a look-alike custom name does not. That difference is invisible in the\n * label and decides what the row DOES, so the row has to show it. Returning\n * `null` for an option renders nothing and costs no layout.\n *\n * Not rendered in the create row, which by definition names nothing in the pool.\n */\n optionAdornment?: (option: string) => ReactNode;\n /** Focus on mount, the way `<input autoFocus>` does — and, through `onFocus`, open\n * the list with it. A click-the-value inline editor needs it: the cell the user\n * clicked names the field the caret should land in. {@link InlineEntityCombobox}\n * has had it since live #218. */\n autoFocus?: boolean;\n /** Focus genuinely LEFT the field. Not fired by picking a row — the rows suppress\n * `mousedown`, so the input never blurs — which is what makes it usable as a\n * \"close the inline editor\" signal. */\n onBlur?: () => void;\n /** Enter pressed with no row highlighted, i.e. \"I mean what I typed\". The list\n * closes either way; this is for a caller whose Enter also submits a row editor. */\n onSubmit?: () => void;\n /** Rows to offer when the field is empty. 8 on a dropdown, where that is all\n * that fits; the phone sheet asks for more because it has a screen. */\n maxSuggestions?: number;\n searchPlaceholder?: string;\n closeLabel?: string;\n /**\n * The phone sheet's heading — and, as a string, the dialog's accessible name — for a\n * field whose label is drawn by someone else: a bound field's label above it,\n * which `RhfTextCombobox` hands over here (0.24). Default: `label`. Without either, the\n * full-screen sheet opened untitled and unnamed, the one screen on a phone that has to\n * say what it is asking for (live #200).\n */\n sheetTitle?: ReactNode;\n /**\n * Label for the row that COMMITS a value the list does not contain — e.g.\n * `(v) => \\`Add \"${v}\" as payee\\``. Supplying it turns the free text into\n * something you confirm rather than something you leave behind.\n *\n * Keksdose live #212: *\"For the payee input have an apply button, when input is\n * placed like 'add xxx as payee' to confirm. Now it can be put in and closed with\n * the right top X, which seems not intuitive.\"* On the phone sheet the search box\n * IS the value, so typing a brand-new name and dismissing the sheet did commit it —\n * but the only control on offer was the close X, which reads as \"discard\". A named\n * affordance says what the typing did.\n *\n * Omit and the row is not rendered, which is the right default for a list whose\n * values are all supposed to already exist.\n */\n createLabel?: (value: string) => string;\n /**\n * The `<input>` — the control a reader types in and the one focus belongs on — as a\n * React 19 ref prop, the way {@link CountrySelect} takes its trigger's (0.24, kastlan:\n * react-hook-form's focus-on-error calls `focus()` on whatever `field.ref` is handed,\n * and a combobox with no ref inside a hand-wired `RhfField` gave it nothing, so a\n * failed submit could not take the user to it). Not the wrapper: a ref is for focus,\n * measurement and selection, and all three are the input's. `RhfTextCombobox` passes it.\n */\n ref?: Ref<HTMLInputElement>;\n}\n\n/**\n * Free-text combobox that looks like the shared {@link Select} (same FIELD_BASE\n * styling, chevron and static floating label) but lets the user type a value\n * that isn't in the list — e.g. naming a brand-new payee. Picking from the\n * filtered suggestion list fills the value; typing keeps whatever was entered.\n *\n * Use this instead of an `<Input list=\"…\">` + `<datalist>` so the control is\n * visually and behaviourally consistent with the other dropdowns (feedback\n * #230 — the payee field looked/behaved differently from every other select).\n */\nexport function Combobox(props: ComboboxProps) {\n const {\n value,\n onChange,\n options,\n label,\n id,\n placeholder,\n className,\n groupBy,\n maxSuggestions,\n searchPlaceholder,\n closeLabel,\n sheetTitle,\n createLabel,\n invalid,\n error,\n hint,\n help,\n disabled,\n disabledReason,\n disabledReasonDisplay,\n commit,\n optionAdornment,\n autoFocus,\n onBlur,\n onSubmit,\n ref,\n \"aria-label\": ariaLabel,\n // Off `rest` and onto the <input>, which is the combobox a reader meets: `Field`'s\n // render-prop spreads `{ id, aria-describedby, aria-invalid, aria-required }`, and\n // on the wrapper div the hint and required state described nothing.\n \"aria-describedby\": ariaDescribedBy,\n \"aria-invalid\": ariaInvalid,\n \"aria-required\": ariaRequired,\n ...rest\n } = props;\n const hintParts = useFieldHint(hint, ariaDescribedBy, { help, name: label ?? ariaLabel });\n const field = useFieldError(error, invalid, hintParts.describedBy, ariaInvalid);\n const generated = useId();\n const fieldId = id ?? generated;\n // Derived from the GENERATED id, never from `id`: a caller's id is theirs to\n // collide with, and `aria-controls` has to resolve.\n const listboxId = `${generated}-listbox`;\n const optionId = (index: number) => `${generated}-option-${index}`;\n // `backCloses` is the desktop half of live #309: the phone's list IS a\n // {@link PickerSheet}, which registers its own history entry, so registering a\n // second one here would cost two Back presses to dismiss one sheet. Its Escape\n // layer is this field's own, below — the list can be open with nothing to show.\n const isPhone = usePhoneLayout();\n const { open, setOpen, wrapperRef, panelRef } = useDropdown({ backCloses: !isPhone, escapeLayer: false });\n const primaryOnly = usePrimaryPressOnly();\n const [active, setActive] = useState(-1);\n // The lock (see `lockedKeyDown`): a lock arriving while the list is up closes it,\n // adjusted while rendering so no frame offers a row that can no longer be taken.\n const lock = useLockReason(commit, disabledReason);\n const locked = lock.locked;\n const inert = locked || Boolean(disabled);\n if (locked && open) setOpen(false);\n // A phone opens the list as a full-screen sheet with its own input, the way a\n // native <select> does — live #200: \"Paid as full screen dialog with input.\n // Similar to the account select that already appears as full screen.\" The\n // anchored list stays for pointer devices, where it is the better shape.\n const sheetInputRef = useRef<HTMLInputElement | null>(null);\n // The box the portalled list hangs off — the inner wrapper, which hugs the input,\n // not the outer one (a grid item that can be taller than the field).\n const fieldRef = useRef<HTMLDivElement>(null);\n\n /**\n * Is the text in the field a QUERY, or just what was picked last time?\n *\n * Keksdose dev#549, filed against the category-name field: *\"Currently it acts kind\n * of a filter and clicking again does not open anything since the previous text is\n * still there likely filtering all other options out.\"* A combobox whose filter is\n * its own value can only ever re-offer the answer it already has — after picking\n * \"REWE\" the list reopens holding one row, the one you are looking at, and changing\n * your mind means clearing the field by hand first.\n *\n * So the text filters only while it is being TYPED; opening the list, by focus or\n * by clicking the field again, puts the whole pool back. {@link InlineEntityCombobox}\n * below has had this since it was written (see its `typedQuery`) and the app's\n * category-name field grew its own copy to get it — this one, the payee field, was\n * the one left with the bug.\n *\n * Not derived from \"does the text exactly match an option\": a custom value that\n * happens to collide with one would then behave differently from every other, and\n * what is being tracked is what the user just did, which is not a property of the\n * string.\n */\n const [typing, setTyping] = useState(false);\n\n const query = typing ? value.trim() : \"\";\n // Folded as every kit list folds (DUP-5): \"zurich\" finds Zürich, by the locale's case.\n const locale = useKitLocale();\n const matches = useMemo(() => {\n const seen = new Set<string>();\n const uniq = options.filter((o) => o && !seen.has(o) && seen.add(o));\n let ranked: string[];\n const cap = maxSuggestions ?? (isPhone ? 50 : 8);\n if (!query) {\n ranked = uniq.slice(0, cap);\n } else {\n // The whole name, then prefix matches, then substring matches — most relevant on\n // top (`rankMatch`; a stable sort keeps the pool's order within a rank).\n ranked = uniq\n .map((o) => ({ o, rank: rankMatch(o, query, { locale }) }))\n .filter((x) => x.rank >= 0)\n .sort((a, b) => a.rank - b.rank)\n .map((x) => x.o)\n .slice(0, cap);\n }\n if (!groupBy) return ranked;\n // Keep each group contiguous so a heading appears once instead of every time\n // the ranking interleaves two groups — while preserving \"best match first\":\n // a Map keeps insertion order, so groups come out ordered by their best-ranked\n // member and members keep their rank order inside the group.\n const byGroup = new Map<string, string[]>();\n for (const o of ranked) {\n const key = groupBy(o);\n if (!byGroup.has(key)) byGroup.set(key, []);\n byGroup.get(key)!.push(o);\n }\n return [...byGroup.values()].flat();\n }, [options, query, groupBy, maxSuggestions, isPhone, locale]);\n\n /**\n * Which rows OPEN a group, and with what heading — computed once for both lists.\n *\n * The desktop list had this as `matches[i - 1]`, which is free; the sheet had it as\n * `matches[matches.indexOf(o) - 1]`, which is a scan of the list per row and another\n * per heading test. That is quadratic in the sheet's own cap of 50, on the branch\n * that runs on the slowest hardware we ship to — and it answers the wrong question\n * besides: `indexOf` finds the FIRST row with that string, so a duplicated option\n * would have compared against a neighbour it is not next to. (`matches` de-duplicates\n * today; that is a property of the memo above, not of the row being rendered.)\n */\n const groupHeadings = useMemo(() => {\n if (!groupBy) return null;\n const heads = new Map<number, string>();\n let prev: string | undefined;\n matches.forEach((o, i) => {\n const group = groupBy(o);\n if (group != null && group !== prev) heads.set(i, group);\n prev = group;\n });\n return heads;\n }, [matches, groupBy]);\n\n const take = (v: string) => {\n if (locked) return;\n onChange(v);\n setOpen(false);\n setActive(-1);\n // What is in the field is now an ANSWER, not a query — so reopening offers the\n // whole pool again rather than a one-row filter of the value just chosen.\n setTyping(false);\n };\n\n // Offered only when the typed text is genuinely NOT in the pool: an exact\n // (case-insensitive) match is an existing entry the list is already showing, and a\n // second way to pick it would be noise. Compared against `options`, not `matches` —\n // `matches` is capped, so a pool of 300 payees would otherwise offer to \"add\" one\n // that exists but fell off the end of the list.\n // What the field claims the keyboard is on. Only on a pointer device: the phone\n // renders its rows in a {@link PickerSheet} whose own search box owns focus, and a\n // field pointing at an id that is not there is worse than one pointing nowhere.\n const activeId = !isPhone && active >= 0 && active < matches.length ? optionId(active) : undefined;\n useActiveOptionScroll(activeId);\n\n // While locked the field sits in the reason's Tooltip — in a fragment with a hidden\n // copy of the reason, Button's anatomy; `block` so a full-width field stays full width.\n const withLock = (box: ReactNode) =>\n locked ? (\n <LockedReason lock={lock} display={disabledReasonDisplay} className=\"block\">\n {box}\n </LockedReason>\n ) : (\n box\n );\n\n const typed = value.trim();\n const createRow =\n createLabel && typed.length > 0 && !options.some((o) => o.toLowerCase() === typed.toLowerCase())\n ? createLabel(typed)\n : null;\n\n // The list is a layer on the Escape stack while it SHOWS (Wave 2, DUP-1): an Escape\n // over an empty \"open\" list is the dialog's, as it always was. On a phone the sheet\n // is the layer.\n const showing = !isPhone && open && !inert && (matches.length > 0 || Boolean(createRow));\n useDismissLayer(showing, () => {\n setOpen(false);\n setActive(-1);\n });\n\n return (\n // `rest` dresses the outer box — a `data-tour` anchor, a test id. Not the NAME,\n // description, invalid or required state: those belong on the <input> below,\n // which is the combobox a reader meets. Spread FIRST, so the field's ARIA and the\n // handlers carrying live #309 and dev#549 cannot be replaced from outside.\n <div {...rest} ref={wrapperRef} className={cn(\"relative\", className)}>\n {/* A real <label for> since lenkbank's tests met a field `getByLabelText` could\n not find: it used to be a <span>, with the text copied onto `aria-label`. The\n name a reader hears is the same words either way. With a \"?\" it moves into a\n row after the field, below. */}\n {label !== undefined && hintParts.labelHint === undefined && (\n <ComboboxFieldLabel htmlFor={fieldId} className={inert ? \"opacity-50\" : undefined}>\n {label}\n </ComboboxFieldLabel>\n )}\n <EndHintRow {...endHintRowProps(\"hint\" in props, label !== undefined, hintParts.labelHint)}>\n {withLock(\n /* The chevron centers against this inner wrapper, which hugs the input.\n The outer div can be taller than the input (as a grid item it\n stretches to the row height, e.g. next to the editor's category cell\n with its split button), which used to drag a top-1/2 chevron down to\n the input's bottom edge (feedback #248).\n Dimmed as a whole when disabled (or locked), the way EntityCombobox dims its\n trigger. FIELD_BASE's grey alone left a disabled picker looking like a\n filled-in one beside the pickers that do dim (the label is not the input's\n `peer`, so it is dimmed by hand above). */\n <div ref={fieldRef} className={cn(\"relative\", inert && \"cursor-not-allowed opacity-50\")}>\n <input\n ref={ref}\n id={fieldId}\n value={value}\n placeholder={placeholder}\n // The caller's name, over the <label for> above — which is what names the\n // field otherwise (dev#477: a field with neither announces only its text).\n aria-label={ariaLabel}\n role=\"combobox\"\n aria-expanded={open}\n // The list this field is the mouth of. Required by the role, and the half\n // that was missing: the field said it was expanded and never said what it\n // had expanded, so a reader had no way from the box to the options\n // (ESLint's `role-has-required-aria-props`, the audit's §a11y).\n aria-controls={listboxId}\n aria-activedescendant={activeId}\n aria-autocomplete=\"list\"\n aria-invalid={field.isInvalid || undefined}\n aria-describedby={mergeDescribedBy(field.describedBy, locked && lock.reasonId)}\n aria-required={ariaRequired}\n autoComplete=\"off\"\n // Locked: focusable, `readOnly`, `aria-disabled` — see `lockedKeyDown`.\n disabled={locked ? undefined : disabled}\n readOnly={locked || undefined}\n aria-disabled={locked || undefined}\n // eslint-disable-next-line jsx-a11y/no-autofocus -- a documented prop the caller opts into (off by default); the field never takes focus on its own.\n autoFocus={autoFocus}\n onBlur={onBlur}\n onMouseDown={primaryOnly.onMouseDown}\n onFocus={() => {\n // A back/forward mouse button focuses this field on its way to\n // navigating; it is not a request to open anything (live #309 rework).\n // A locked field takes focus to say why, and opens nothing.\n if (primaryOnly.fromAuxButton() || locked) return;\n setOpen(true);\n setTyping(false);\n // The sheet carries its own input, so the field behind it must not also\n // pull up the keyboard and scroll the page under the dialog.\n if (isPhone) sheetInputRef.current?.focus();\n }}\n // A CLICK as well as focus — the other half of dev#549. Picking a suggestion\n // closes the list without moving focus (the rows suppress `mousedown` on\n // purpose, so the input never blurred), which means clicking the field again\n // fires no `focus` event at all and the list stayed shut. \"Does not open\n // anything\" was literally true.\n onClick={() => {\n if (locked) return;\n setOpen(true);\n setTyping(false);\n }}\n // `inputMode=\"none\"` rather than readOnly: the field must not look\n // uneditable (FIELD_BASE greys a read-only field since dev#468) and must\n // still take focus — it just has no keyboard of its own, the same trick\n // the amount field uses for the numpad.\n inputMode={isPhone ? \"none\" : undefined}\n onChange={(e) => {\n if (locked) return;\n onChange(e.target.value);\n setOpen(true);\n setActive(-1);\n setTyping(true);\n }}\n onKeyDown={(e) => {\n if (locked) return lockedKeyDown(e);\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n setOpen(true);\n setActive((i) => Math.min(i + 1, matches.length - 1));\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n setActive((i) => Math.max(i - 1, 0));\n } else if (e.key === \"Enter\") {\n // The Enter that confirms an IME composition is the IME's: it picks no\n // row and submits nothing (BUG-9).\n if (isImeEnter(e)) return;\n if (open && active >= 0 && active < matches.length) {\n e.preventDefault();\n take(matches[active]);\n } else {\n // No row highlighted: the typed text is the answer. `preventDefault`\n // only when a caller is taking Enter, so a plain form submit is\n // otherwise left alone.\n if (onSubmit) e.preventDefault();\n setOpen(false);\n onSubmit?.();\n }\n } else if (e.key === \"Escape\") {\n // A list that is SHOWING is the top of the Escape stack, and the key\n // closes it and stops here: a Modal or FullBleedDialog around the field\n // otherwise closed with it, form and all (round 0.34 BUG-1). With no\n // list on screen the key goes on to whoever wants it.\n if (showing && takeEscape(e)) return;\n setOpen(false);\n setActive(-1);\n } else if (e.key === \"Tab\") {\n // Closes, and lets the browser take the Tab: focus is in THIS input,\n // which is staying, so there is nothing to catch — unlike the pickers\n // whose focus sits inside a portalled panel.\n setOpen(false);\n setActive(-1);\n }\n // Home/End are deliberately absent. The APG gives them to the list only\n // where the combobox is not editable; here the text IS the value, a\n // payee name is long enough to want the caret moved to its start, and\n // the desktop list is capped at 8 rows — so jumping it would be worth\n // almost nothing and would cost the one gesture that field is used with.\n }}\n className={cn(\n FIELD_BASE,\n label !== undefined && FIELD_FLOATING_PAD,\n \"pe-9\",\n field.isInvalid && FIELD_INVALID,\n )}\n />\n <ChevronDown\n aria-hidden\n onMouseDown={(e) => {\n // Toggle on the chevron without stealing focus from the input.\n e.preventDefault();\n if (!inert) setOpen((o) => !o);\n }}\n className={cn(\n \"absolute end-2.5 top-1/2 size-4 -translate-y-1/2 text-placeholder\",\n // The box above does the dimming; dimming here too would halve it again.\n !inert && \"cursor-pointer\",\n )}\n />\n </div>,\n )}\n </EndHintRow>\n {/* The label and its \"?\" share the top strip — after the field in the DOM, so the\n \"?\" follows the control in the tab order, as it does on Input and Select. */}\n {label !== undefined && hintParts.labelHint !== undefined && (\n <StaticLabelRow hint={hintParts.labelHint}>\n <ComboboxFieldLabel htmlFor={fieldId} className={cn(LABEL_IN_ROW, inert && \"opacity-50\")}>\n {label}\n </ComboboxFieldLabel>\n </StaticLabelRow>\n )}\n <FieldCaption parts={hintParts} />\n {field.errorEl}\n {isPhone && (\n <PickerSheet\n open={open && !inert}\n onClose={() => setOpen(false)}\n title={sheetTitle ?? label}\n // The sheet's input IS the field: this is a free-text control, so what is\n // typed here is the value (a brand-new payee is just a name nothing\n // matches), and the list below narrows as it changes.\n query={value}\n onQueryChange={(v) => {\n onChange(v);\n setTyping(true);\n }}\n searchPlaceholder={searchPlaceholder ?? placeholder}\n inputRef={sheetInputRef}\n closeLabel={closeLabel}\n >\n <ul id={listboxId} role=\"listbox\">\n {createRow && (\n // `role=\"option\"` rides the BUTTON, not the <li> around it: an option\n // may not contain a separately focusable control, and the button is\n // what a pointer presses. Same shape `CommandPalette` has always had.\n <li role=\"presentation\">\n <button\n type=\"button\"\n role=\"option\"\n aria-selected={false}\n onClick={() => take(typed)}\n className={cn(SHEET_ROW_CLASS, \"font-medium text-brand\")}\n >\n {createRow}\n </button>\n </li>\n )}\n {matches.map((o, i) => {\n const group = groupHeadings?.get(i);\n return (\n <Fragment key={o}>\n {group !== undefined && (\n <li\n role=\"presentation\"\n className=\"px-4 pb-0.5 pt-3 text-caption font-semibold uppercase tracking-wide text-muted\"\n >\n {group}\n </li>\n )}\n <li role=\"presentation\">\n <button\n type=\"button\"\n role=\"option\"\n aria-selected={o === value}\n onClick={() => take(o)}\n className={cn(\n SHEET_ROW_CLASS,\n optionAdornment && \"flex items-center justify-between gap-2\",\n )}\n >\n {optionAdornment ? <span className=\"truncate-until-large\">{o}</span> : o}\n {optionAdornment?.(o)}\n </button>\n </li>\n </Fragment>\n );\n })}\n </ul>\n </PickerSheet>\n )}\n {showing && (\n <SuggestionList id={listboxId} anchorRef={fieldRef} panelRef={panelRef}>\n {createRow && (\n <li role=\"presentation\">\n <button\n type=\"button\"\n role=\"option\"\n aria-selected={false}\n tabIndex={-1}\n onMouseDown={(e) => {\n // mousedown, like the rows below: the input's blur would otherwise\n // close the list before the click landed.\n e.preventDefault();\n take(typed);\n }}\n className={cn(rowClass(false), \"font-medium text-brand\")}\n >\n {createRow}\n </button>\n </li>\n )}\n {matches.map((o, i) => {\n // Heading at each group boundary only — `matches` is group-contiguous, so\n // the boundaries are a property of the list rather than of the branch\n // drawing it. See `groupHeadings`.\n const group = groupHeadings?.get(i);\n return (\n <Fragment key={o}>\n {group !== undefined && (\n <li\n role=\"presentation\"\n className=\"px-3 pb-0.5 pt-2 text-caption font-semibold uppercase tracking-wide text-muted first:pt-1\"\n >\n {group}\n </li>\n )}\n <li role=\"presentation\">\n <button\n type=\"button\"\n id={optionId(i)}\n role=\"option\"\n // CHOSEN, not highlighted. The row the arrows are on is named by\n // the field's `aria-activedescendant`; saying \"selected\" for it\n // would tell a reader that the row they are passing over is\n // already the answer.\n aria-selected={o === value}\n // Focus stays in the field — that is what\n // `aria-activedescendant` is for — so the rows are not tab stops.\n tabIndex={-1}\n onMouseDown={(e) => {\n // mousedown (not click) so the blur from the input firing first\n // doesn't close the list before the selection registers.\n e.preventDefault();\n take(o);\n }}\n onMouseEnter={() => setActive(i)}\n className={cn(\n rowClass(i === active),\n group != null && \"ps-6\",\n // A row with a badge is a flex row so the label truncates and the\n // badge keeps its width; without one it stays the plain block the\n // other lists render, so nothing shifts for callers that pass none.\n // From Large up the label wraps instead (`truncate-until-large`,\n // text size §10.17, round 0.34 T1), as the badge-less row does.\n optionAdornment && \"flex items-center justify-between gap-2\",\n )}\n >\n {optionAdornment ? <span className=\"truncate-until-large\">{o}</span> : o}\n {optionAdornment?.(o)}\n </button>\n </li>\n </Fragment>\n );\n })}\n </SuggestionList>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -1,5 +1,5 @@
1
1
  import type { ComponentPropsWithoutRef, ReactNode, Ref } from "react";
2
- import { type DisabledReasonDisplay } from "./field-parts.js";
2
+ import { type DisabledReasonDisplay, type FieldHelpProps } from "./field-parts.js";
3
3
  import type { CommitScope } from "./write-lock.js";
4
4
  import { type LabelOverride } from "../i18n/kit-labels.js";
5
5
  import { DEFAULT_COUNTRY_SELECT_LABELS } from "./country-select-labels.js";
@@ -29,7 +29,7 @@ export { DEFAULT_COUNTRY_SELECT_LABELS };
29
29
  * type-checks), and `(code: string | null) => void` once `clearable` is set — the
30
30
  * `null` of a clear cannot reach a handler that was never told about it.
31
31
  */
32
- export interface CountrySelectProps<Clearable extends boolean = false> extends Omit<ComponentPropsWithoutRef<"div">, "onChange" | "defaultValue"> {
32
+ export interface CountrySelectProps<Clearable extends boolean = false> extends Omit<ComponentPropsWithoutRef<"div">, "onChange" | "defaultValue">, FieldHelpProps {
33
33
  /** The chosen country as an ISO 3166-1 alpha-2 code ("CH"), in either case;
34
34
  * `null`, `undefined` or `""` when none is. A code outside the list still shows
35
35
  * its name — the list restricts what can be PICKED, not what can be shown. */
@@ -168,4 +168,4 @@ export interface CountrySelectProps<Clearable extends boolean = false> extends O
168
168
  * Since 0.23 it takes a `ref` to the trigger (react-hook-form's focus-on-error, kastlan)
169
169
  * and `clearable`, which hands `onChange` `null`.
170
170
  */
171
- export declare function CountrySelect<Clearable extends boolean = false>({ value, onChange, countries, preferred, flags, label, hint, error, invalid, placeholder, disabled, disabledReason: ownReason, disabledReasonDisplay, commit, clearable, clearLabel, locale: localeProp, labels: labelsProp, className, id, ref, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid, "aria-required": ariaRequired, ...rest }: CountrySelectProps<Clearable>): import("react").JSX.Element;
171
+ export declare function CountrySelect<Clearable extends boolean = false>({ value, onChange, countries, preferred, flags, label, hint, help, error, invalid, placeholder, disabled, disabledReason: ownReason, disabledReasonDisplay, commit, clearable, clearLabel, locale: localeProp, labels: labelsProp, className, id, ref, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid, "aria-required": ariaRequired, ...rest }: CountrySelectProps<Clearable>): import("react").JSX.Element;
@@ -55,6 +55,7 @@ function CountrySelect({
55
55
  flags = false,
56
56
  label,
57
57
  hint,
58
+ help,
58
59
  error,
59
60
  invalid,
60
61
  placeholder,
@@ -135,6 +136,8 @@ function CountrySelect({
135
136
  const listboxId = `${generated}-listbox`;
136
137
  const field = useFieldMessages({
137
138
  hint,
139
+ help,
140
+ name: label ?? ariaLabel,
138
141
  error,
139
142
  invalid,
140
143
  describedBy: ariaDescribedBy,