@eifi1/ui-kit 0.21.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (320) hide show
  1. package/README.md +42 -25
  2. package/dist/chart.d.ts +19 -14
  3. package/dist/components/account-chips.d.ts +10 -3
  4. package/dist/components/account-chips.js +5 -2
  5. package/dist/components/account-chips.js.map +1 -1
  6. package/dist/components/account-settings.d.ts +33 -2
  7. package/dist/components/account-settings.js +43 -21
  8. package/dist/components/account-settings.js.map +1 -1
  9. package/dist/components/amount-input.d.ts +53 -14
  10. package/dist/components/amount-input.js +17 -28
  11. package/dist/components/amount-input.js.map +1 -1
  12. package/dist/components/autocomplete.d.ts +29 -0
  13. package/dist/components/autocomplete.js +134 -104
  14. package/dist/components/autocomplete.js.map +1 -1
  15. package/dist/components/button-group.d.ts +19 -14
  16. package/dist/components/calculator.d.ts +19 -14
  17. package/dist/components/checkbox-group.d.ts +104 -0
  18. package/dist/components/checkbox-group.js +111 -0
  19. package/dist/components/checkbox-group.js.map +1 -0
  20. package/dist/components/checkbox.d.ts +19 -0
  21. package/dist/components/checkbox.js +30 -7
  22. package/dist/components/checkbox.js.map +1 -1
  23. package/dist/components/chip.d.ts +18 -0
  24. package/dist/components/chip.js +47 -10
  25. package/dist/components/chip.js.map +1 -1
  26. package/dist/components/column-mapper.d.ts +182 -0
  27. package/dist/components/column-mapper.js +375 -0
  28. package/dist/components/column-mapper.js.map +1 -0
  29. package/dist/components/combobox-core.d.ts +39 -2
  30. package/dist/components/combobox-core.js +29 -6
  31. package/dist/components/combobox-core.js.map +1 -1
  32. package/dist/components/combobox.d.ts +91 -2
  33. package/dist/components/combobox.js +454 -308
  34. package/dist/components/combobox.js.map +1 -1
  35. package/dist/components/confirm-dialog.d.ts +69 -161
  36. package/dist/components/confirm-dialog.js +7 -8
  37. package/dist/components/confirm-dialog.js.map +1 -1
  38. package/dist/components/copy-button.d.ts +16 -11
  39. package/dist/components/country-select.d.ts +167 -0
  40. package/dist/components/country-select.js +293 -0
  41. package/dist/components/country-select.js.map +1 -0
  42. package/dist/components/currency-select.d.ts +17 -1
  43. package/dist/components/currency-select.js +56 -23
  44. package/dist/components/currency-select.js.map +1 -1
  45. package/dist/components/danger-confirm.d.ts +69 -166
  46. package/dist/components/danger-confirm.js +144 -50
  47. package/dist/components/danger-confirm.js.map +1 -1
  48. package/dist/components/data-table-cells.d.ts +1 -2
  49. package/dist/components/data-table-filter-popover.d.ts +1 -2
  50. package/dist/components/data-table-filters.d.ts +1 -2
  51. package/dist/components/data-table-labels.d.ts +4 -97
  52. package/dist/components/data-table-labels.js +9 -1
  53. package/dist/components/data-table-labels.js.map +1 -1
  54. package/dist/components/data-table-pagination.d.ts +3 -1
  55. package/dist/components/data-table.d.ts +1 -2
  56. package/dist/components/data-table.js +98 -3
  57. package/dist/components/data-table.js.map +1 -1
  58. package/dist/components/date-picker.d.ts +66 -5
  59. package/dist/components/date-picker.js +378 -66
  60. package/dist/components/date-picker.js.map +1 -1
  61. package/dist/components/entity-combobox.d.ts +42 -1
  62. package/dist/components/entity-combobox.js +154 -113
  63. package/dist/components/entity-combobox.js.map +1 -1
  64. package/dist/components/facing-pair.d.ts +19 -14
  65. package/dist/components/field-parts.d.ts +224 -0
  66. package/dist/components/field-parts.js +156 -0
  67. package/dist/components/field-parts.js.map +1 -0
  68. package/dist/components/field-strip.d.ts +105 -0
  69. package/dist/components/field-strip.js +46 -0
  70. package/dist/components/field-strip.js.map +1 -0
  71. package/dist/components/file-button.d.ts +19 -14
  72. package/dist/components/file-button.js +13 -3
  73. package/dist/components/file-button.js.map +1 -1
  74. package/dist/components/file-dropzone.d.ts +41 -15
  75. package/dist/components/file-dropzone.js +57 -34
  76. package/dist/components/file-dropzone.js.map +1 -1
  77. package/dist/components/form-actions.d.ts +18 -13
  78. package/dist/components/form-actions.js +69 -5
  79. package/dist/components/form-actions.js.map +1 -1
  80. package/dist/components/iban-input.d.ts +69 -0
  81. package/dist/components/iban-input.js +125 -0
  82. package/dist/components/iban-input.js.map +1 -0
  83. package/dist/components/icon-picker.d.ts +21 -2
  84. package/dist/components/icon-picker.js +19 -4
  85. package/dist/components/icon-picker.js.map +1 -1
  86. package/dist/components/inline-edit-field.d.ts +167 -0
  87. package/dist/components/inline-edit-field.js +293 -0
  88. package/dist/components/inline-edit-field.js.map +1 -0
  89. package/dist/components/language-select.d.ts +130 -0
  90. package/dist/components/language-select.js +18 -0
  91. package/dist/components/language-select.js.map +1 -0
  92. package/dist/components/line-items.d.ts +25 -3
  93. package/dist/components/line-items.js +55 -46
  94. package/dist/components/line-items.js.map +1 -1
  95. package/dist/components/list.d.ts +25 -0
  96. package/dist/components/list.js +16 -5
  97. package/dist/components/list.js.map +1 -1
  98. package/dist/components/money-field.d.ts +32 -14
  99. package/dist/components/money-field.js.map +1 -1
  100. package/dist/components/month-picker.d.ts +52 -4
  101. package/dist/components/month-picker.js +291 -86
  102. package/dist/components/month-picker.js.map +1 -1
  103. package/dist/components/multi-entity-combobox.d.ts +26 -1
  104. package/dist/components/multi-entity-combobox.js +143 -102
  105. package/dist/components/multi-entity-combobox.js.map +1 -1
  106. package/dist/components/multi-select.d.ts +8 -1
  107. package/dist/components/multi-select.js +9 -4
  108. package/dist/components/multi-select.js.map +1 -1
  109. package/dist/components/number-field.d.ts +34 -19
  110. package/dist/components/number-field.js +6 -20
  111. package/dist/components/number-field.js.map +1 -1
  112. package/dist/components/number-input.d.ts +79 -19
  113. package/dist/components/number-input.js +90 -38
  114. package/dist/components/number-input.js.map +1 -1
  115. package/dist/components/numpad-sheet.d.ts +19 -14
  116. package/dist/components/one-time-code-input.d.ts +100 -0
  117. package/dist/components/one-time-code-input.js +137 -0
  118. package/dist/components/one-time-code-input.js.map +1 -0
  119. package/dist/components/phone-input.d.ts +69 -0
  120. package/dist/components/phone-input.js +171 -0
  121. package/dist/components/phone-input.js.map +1 -0
  122. package/dist/components/reauth-dialog.d.ts +2 -2
  123. package/dist/components/reauth-dialog.js +6 -8
  124. package/dist/components/reauth-dialog.js.map +1 -1
  125. package/dist/components/series-chart.d.ts +19 -14
  126. package/dist/components/settings-fields.d.ts +19 -14
  127. package/dist/components/share-card.d.ts +16 -11
  128. package/dist/components/sign-chip.d.ts +66 -0
  129. package/dist/components/sign-chip.js +39 -0
  130. package/dist/components/sign-chip.js.map +1 -0
  131. package/dist/components/swatch-picker.d.ts +43 -2
  132. package/dist/components/swatch-picker.js +80 -32
  133. package/dist/components/swatch-picker.js.map +1 -1
  134. package/dist/components/switch.d.ts +19 -0
  135. package/dist/components/switch.js +24 -6
  136. package/dist/components/switch.js.map +1 -1
  137. package/dist/components/text-link.d.ts +18 -13
  138. package/dist/components/tile-radio.d.ts +88 -13
  139. package/dist/components/tile-radio.js +28 -11
  140. package/dist/components/tile-radio.js.map +1 -1
  141. package/dist/components/time-input.d.ts +24 -15
  142. package/dist/components/time-input.js.map +1 -1
  143. package/dist/components/toggle-group.d.ts +47 -1
  144. package/dist/components/toggle-group.js +110 -69
  145. package/dist/components/toggle-group.js.map +1 -1
  146. package/dist/components/trigger-aria.d.ts +3 -0
  147. package/dist/components/trigger-aria.js +3 -1
  148. package/dist/components/trigger-aria.js.map +1 -1
  149. package/dist/components/ui.d.ts +16 -11
  150. package/dist/components/ui.js +284 -88
  151. package/dist/components/ui.js.map +1 -1
  152. package/dist/components/use-table-state.d.ts +1 -2
  153. package/dist/{data-table-3mi2R3so.d.ts → data-table-labels-B7OdnM0S.d.ts} +140 -3
  154. package/dist/data-table.d.ts +1 -2
  155. package/dist/data-table.js +2 -1
  156. package/dist/data-table.js.map +1 -1
  157. package/dist/feedback/feedback-attachment.d.ts +1 -1
  158. package/dist/feedback/feedback-attachment.js +348 -41
  159. package/dist/feedback/feedback-attachment.js.map +1 -1
  160. package/dist/feedback/feedback-dialog.d.ts +1 -1
  161. package/dist/feedback/feedback-dialog.js.map +1 -1
  162. package/dist/feedback/feedback-inbox.d.ts +3 -2
  163. package/dist/feedback/feedback-inbox.js.map +1 -1
  164. package/dist/feedback/feedback-thread.d.ts +68 -253
  165. package/dist/feedback/feedback-thread.js +46 -10
  166. package/dist/feedback/feedback-thread.js.map +1 -1
  167. package/dist/{kit-labels-D1J2_jyD.d.ts → feedback-BxeQVzwq.d.ts} +1206 -20
  168. package/dist/{feedback-attachment-WFiXJ8We.d.ts → feedback-attachment-fGAzZPf0.d.ts} +178 -11
  169. package/dist/feedback.d.ts +66 -2
  170. package/dist/hooks/use-file-drop.d.ts +19 -14
  171. package/dist/i18n/defaults.d.ts +19 -14
  172. package/dist/i18n/defaults.js +19 -2
  173. package/dist/i18n/defaults.js.map +1 -1
  174. package/dist/i18n/german.d.ts +19 -14
  175. package/dist/i18n/german.js +84 -5
  176. package/dist/i18n/german.js.map +1 -1
  177. package/dist/i18n/kit-labels.d.ts +15 -10
  178. package/dist/i18n/kit-labels.js +8 -2
  179. package/dist/i18n/kit-labels.js.map +1 -1
  180. package/dist/i18n/languages.d.ts +19 -14
  181. package/dist/i18n/locales/de-CH.d.ts +19 -14
  182. package/dist/i18n/locales/en.d.ts +19 -14
  183. package/dist/i18n/locales/en.js +16 -0
  184. package/dist/i18n/locales/en.js.map +1 -1
  185. package/dist/i18n/locales/es.d.ts +19 -14
  186. package/dist/i18n/locales/es.js +83 -5
  187. package/dist/i18n/locales/es.js.map +1 -1
  188. package/dist/i18n/locales/fr.d.ts +19 -14
  189. package/dist/i18n/locales/fr.js +82 -5
  190. package/dist/i18n/locales/fr.js.map +1 -1
  191. package/dist/i18n/locales/hu.d.ts +19 -14
  192. package/dist/i18n/locales/hu.js +85 -5
  193. package/dist/i18n/locales/hu.js.map +1 -1
  194. package/dist/i18n/locales/it.d.ts +19 -14
  195. package/dist/i18n/locales/it.js +84 -5
  196. package/dist/i18n/locales/it.js.map +1 -1
  197. package/dist/i18n/locales/zh.d.ts +19 -14
  198. package/dist/i18n/locales/zh.js +80 -5
  199. package/dist/i18n/locales/zh.js.map +1 -1
  200. package/dist/i18n/review.d.ts +19 -14
  201. package/dist/i18n/review.js +27 -1
  202. package/dist/i18n/review.js.map +1 -1
  203. package/dist/index.d.ts +21 -10
  204. package/dist/index.js +73 -2
  205. package/dist/index.js.map +1 -1
  206. package/dist/lib/column-mapping.d.ts +81 -0
  207. package/dist/lib/column-mapping.js +108 -0
  208. package/dist/lib/column-mapping.js.map +1 -0
  209. package/dist/lib/countries.d.ts +43 -0
  210. package/dist/lib/countries.js +39 -0
  211. package/dist/lib/countries.js.map +1 -0
  212. package/dist/lib/decimal-marks.d.ts +46 -0
  213. package/dist/lib/decimal-marks.js +40 -0
  214. package/dist/lib/decimal-marks.js.map +1 -0
  215. package/dist/lib/iban.d.ts +98 -0
  216. package/dist/lib/iban.js +154 -0
  217. package/dist/lib/iban.js.map +1 -0
  218. package/dist/lib/isin.d.ts +30 -0
  219. package/dist/lib/isin.js +36 -0
  220. package/dist/lib/isin.js.map +1 -0
  221. package/dist/lib/phone.d.ts +69 -0
  222. package/dist/lib/phone.js +121 -0
  223. package/dist/lib/phone.js.map +1 -0
  224. package/dist/lib/table-text.d.ts +109 -1
  225. package/dist/lib/table-text.js +122 -1
  226. package/dist/lib/table-text.js.map +1 -1
  227. package/dist/rhf/fields.d.ts +233 -18
  228. package/dist/rhf/fields.js +456 -3
  229. package/dist/rhf/fields.js.map +1 -1
  230. package/dist/rhf/form.d.ts +19 -14
  231. package/dist/rhf.d.ts +22 -15
  232. package/dist/rhf.js.map +1 -1
  233. package/dist/shell/app-shell.d.ts +18 -13
  234. package/dist/shell/top-bar-brand.d.ts +19 -14
  235. package/dist/shell/topbar-action-menu.d.ts +36 -3
  236. package/dist/shell/topbar-action-menu.js +74 -33
  237. package/dist/shell/topbar-action-menu.js.map +1 -1
  238. package/dist/shell.d.ts +18 -13
  239. package/dist/table-text.d.ts +1 -1
  240. package/dist/tour/tour.js +20 -24
  241. package/dist/tour/tour.js.map +1 -1
  242. package/dist/wizard/stepper-nav.d.ts +49 -16
  243. package/dist/wizard/stepper-nav.js +4 -0
  244. package/dist/wizard/stepper-nav.js.map +1 -1
  245. package/dist/wizard.d.ts +19 -14
  246. package/package.json +1 -1
  247. package/src/components/account-chips.tsx +17 -3
  248. package/src/components/account-settings.tsx +92 -26
  249. package/src/components/amount-input.tsx +68 -56
  250. package/src/components/autocomplete.tsx +198 -114
  251. package/src/components/checkbox-group.tsx +229 -0
  252. package/src/components/checkbox.tsx +72 -8
  253. package/src/components/chip.tsx +80 -10
  254. package/src/components/column-mapper.tsx +666 -0
  255. package/src/components/combobox-core.tsx +82 -9
  256. package/src/components/combobox.tsx +661 -340
  257. package/src/components/confirm-dialog.tsx +12 -8
  258. package/src/components/country-select.tsx +556 -0
  259. package/src/components/currency-select.tsx +109 -33
  260. package/src/components/danger-confirm.tsx +429 -82
  261. package/src/components/data-table-labels.ts +16 -1
  262. package/src/components/data-table.tsx +175 -6
  263. package/src/components/date-picker.tsx +595 -71
  264. package/src/components/entity-combobox.tsx +235 -124
  265. package/src/components/field-parts.tsx +403 -0
  266. package/src/components/field-strip.tsx +149 -0
  267. package/src/components/file-button.tsx +36 -3
  268. package/src/components/file-dropzone.tsx +106 -38
  269. package/src/components/form-actions.tsx +147 -6
  270. package/src/components/iban-input.tsx +231 -0
  271. package/src/components/icon-picker.tsx +47 -6
  272. package/src/components/inline-edit-field.tsx +533 -0
  273. package/src/components/language-select.tsx +78 -0
  274. package/src/components/line-items.tsx +35 -2
  275. package/src/components/list.tsx +55 -6
  276. package/src/components/money-field.tsx +13 -0
  277. package/src/components/month-picker.tsx +400 -63
  278. package/src/components/multi-entity-combobox.tsx +207 -120
  279. package/src/components/multi-select.tsx +80 -58
  280. package/src/components/number-field.tsx +28 -46
  281. package/src/components/number-input.tsx +210 -43
  282. package/src/components/one-time-code-input.tsx +288 -0
  283. package/src/components/phone-input.tsx +299 -0
  284. package/src/components/reauth-dialog.tsx +17 -18
  285. package/src/components/sign-chip.tsx +103 -0
  286. package/src/components/swatch-picker.tsx +143 -31
  287. package/src/components/switch.tsx +58 -7
  288. package/src/components/tile-radio.tsx +145 -29
  289. package/src/components/time-input.tsx +8 -1
  290. package/src/components/toggle-group.tsx +206 -73
  291. package/src/components/trigger-aria.ts +5 -0
  292. package/src/components/ui.tsx +479 -138
  293. package/src/data-table.ts +2 -1
  294. package/src/feedback/feedback-attachment.tsx +597 -46
  295. package/src/feedback/feedback-dialog.tsx +9 -3
  296. package/src/feedback/feedback-inbox.tsx +3 -2
  297. package/src/feedback/feedback-thread.tsx +113 -9
  298. package/src/i18n/defaults.ts +18 -1
  299. package/src/i18n/german.ts +89 -0
  300. package/src/i18n/kit-labels.tsx +82 -2
  301. package/src/i18n/locales/en.ts +36 -4
  302. package/src/i18n/locales/es.ts +87 -0
  303. package/src/i18n/locales/fr.ts +87 -0
  304. package/src/i18n/locales/hu.ts +83 -0
  305. package/src/i18n/locales/it.ts +88 -0
  306. package/src/i18n/locales/zh.ts +76 -0
  307. package/src/i18n/review.ts +26 -0
  308. package/src/index.ts +71 -3
  309. package/src/lib/column-mapping.ts +234 -0
  310. package/src/lib/countries.ts +91 -0
  311. package/src/lib/decimal-marks.ts +99 -0
  312. package/src/lib/iban.ts +171 -0
  313. package/src/lib/isin.ts +62 -0
  314. package/src/lib/phone.ts +211 -0
  315. package/src/lib/table-text.ts +271 -0
  316. package/src/rhf/fields.tsx +860 -9
  317. package/src/rhf.ts +3 -1
  318. package/src/shell/topbar-action-menu.tsx +134 -38
  319. package/src/tour/tour.tsx +66 -28
  320. package/src/wizard/stepper-nav.tsx +35 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/multi-entity-combobox.tsx"],"sourcesContent":["import { useId, useMemo, type ComponentPropsWithoutRef, type ReactNode } from \"react\";\nimport { X } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { FieldChevron, FieldLabel, FIELD_TRIGGER, FIELD_FLOATING_PAD, FIELD_INVALID } from \"./ui\";\nimport {\n ComboboxPanel,\n useComboboxCore,\n useComboboxFieldError,\n type ComboOption,\n} from \"./combobox-core\";\nimport {\n DEFAULT_COMBOBOX_LABELS,\n DEFAULT_COMMON_LABELS,\n useKitLabels,\n useKitLocale,\n} from \"../i18n/kit-labels\";\n\n/** `onChange` is the kit's — \"a selection was made\", carrying values — rather\n * than the div's form event, so the DOM's spelling of it is omitted. */\nexport interface MultiEntityComboboxProps<V extends string | number>\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\"> {\n /** Selected ids. */\n value: V[];\n onChange: (value: V[]) => void;\n /** Already-loaded options (client-side filtered); also resolves selected labels. */\n options?: ComboOption<V>[];\n /** Async option source, debounced + race-safe; a rejection shows\n * `loadErrorLabel`. When set, `options` resolves labels only. */\n loadOptions?: (query: string) => Promise<ComboOption<V>[]>;\n loading?: boolean;\n label?: ReactNode;\n placeholder?: string;\n searchPlaceholder?: string;\n emptyLabel?: string;\n /** Trigger summary when 1+ selected (default: the labels as a locale-formatted\n * list, or `combobox.selectedCount` when a label can't be resolved). */\n itemLabel?: (count: number) => string;\n clearable?: boolean;\n clearLabel?: string;\n /** The phone sheet's close button. Its own prop rather than a reuse of\n * `clearLabel`: \"clear the selection\" and \"close this screen\" are different\n * actions, and on a full-screen sheet the close button is the only way out —\n * so it is the one control here that MUST be in the reader's language. */\n closeLabel?: string;\n disabled?: boolean;\n /** When set, a \"create\" row appears for a non-empty query with no exact match.\n * The panel stays open (adding the new entity to `value` is the caller's job). */\n onCreate?: (query: string) => void;\n createLabel?: (query: string) => string;\n /** Required and unanswered — {@link FIELD_INVALID}. See {@link Input}'s `invalid`. */\n invalid?: boolean;\n /** What is wrong with the value, as {@link Input}'s `error`: rendered under the\n * field, on the trigger's `aria-describedby`, and implies `invalid`. */\n error?: ReactNode;\n /** Narrow `options` client-side by the query. Default `true`; `false` shows them\n * as given (a server-ranked list). */\n filter?: boolean;\n /** Offer nothing, and call no `loadOptions`, below this many characters. Default\n * `0`, i.e. the list loads as the panel opens. */\n minChars?: number;\n /** `loadOptions` debounce. Default 150 ms. */\n debounceMs?: number;\n /** Shown when `loadOptions` rejects. Default: `combobox.loadError`. */\n loadErrorLabel?: string;\n}\n\n/**\n * Multi-value sibling of {@link EntityCombobox}: pick several entities by search,\n * with the same async `loadOptions`/`onCreate` support. Rows show a checkbox and\n * toggle without closing the panel; the trigger summarises the selection.\n */\nexport function MultiEntityCombobox<V extends string | number>({\n value,\n onChange,\n options,\n loadOptions,\n loading,\n label,\n placeholder,\n searchPlaceholder,\n emptyLabel,\n itemLabel,\n clearable,\n clearLabel,\n closeLabel,\n disabled,\n onCreate,\n createLabel,\n className,\n invalid,\n error,\n filter,\n minChars,\n debounceMs,\n loadErrorLabel,\n \"aria-label\": ariaLabel,\n // The control's own wiring, taken off `rest` so it lands on the TRIGGER rather than\n // the wrapper: `Field`'s render-prop spreads `{ id, aria-describedby, aria-invalid,\n // aria-required }`, and on the role-less wrapper div the label's `htmlFor` named\n // nothing and the hint and required state described nothing — kastlan's\n // communication-page To/Cc/Bcc and lease-tenants-step.tsx read as unnamed pickers.\n id,\n \"aria-describedby\": ariaDescribedBy,\n \"aria-invalid\": ariaInvalid,\n \"aria-required\": ariaRequired,\n ...rest\n}: MultiEntityComboboxProps<V>) {\n const core = useComboboxCore<V>({\n options,\n loadOptions,\n loading,\n filter,\n minChars,\n debounceMs,\n });\n const field = useComboboxFieldError(\n error,\n invalid || ariaInvalid === true || ariaInvalid === \"true\",\n ariaDescribedBy,\n );\n // The props are the per-instance overrides, the provider the app-wide ones; a\n // prop left `undefined` falls through to the provider rather than masking it.\n const labels = useKitLabels(\"combobox\", DEFAULT_COMBOBOX_LABELS, {\n search: searchPlaceholder,\n noResults: emptyLabel,\n clear: clearLabel,\n create: createLabel,\n });\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const { open, results, resolve, setOpen, query, triggerRef } = core;\n // One id per instance, generated here rather than in the core: `aria-controls` on\n // the trigger has to name the list while the list is still closed, so the id\n // belongs to whoever renders both ends of it.\n const listboxId = useId();\n\n const valueSet = useMemo(() => new Set(value), [value]);\n // A list of NAMES, not a sentence, so the narrow conjunction: English gets\n // \"Food, Rent, Travel\" (no \"and\" squeezed into a truncating trigger), German and\n // French still get their \"und\"/\"et\" where the language insists on it, and Japanese\n // and Chinese get the ideographic comma \"、\". The `unit` type looked like the\n // natural fit for a summary, but in ja/zh it concatenates the names with nothing\n // between them at all.\n const locale = useKitLocale();\n const listFormat = useMemo(\n () => new Intl.ListFormat(locale, { type: \"conjunction\", style: \"narrow\" }),\n [locale],\n );\n const summary = (() => {\n if (value.length === 0) return placeholder ?? \"\";\n if (itemLabel) return itemLabel(value.length);\n const chosen = value.map(resolve);\n return chosen.every(Boolean)\n ? listFormat.format(chosen.map((o) => o!.label))\n : labels.selectedCount(value.length);\n })();\n\n const q = query.trim();\n const showCreate =\n Boolean(onCreate) && q.length > 0 && !results.some((o) => o.label.toLowerCase() === q.toLowerCase());\n const showClear = Boolean(clearable && value.length > 0 && !disabled);\n\n const toggle = (o: ComboOption<V>) => {\n core.rememberOption(o);\n if (valueSet.has(o.value)) onChange(value.filter((v) => v !== o.value));\n else onChange([...value, o.value]);\n // Stay open — multi-select keeps adding.\n };\n\n return (\n // `rest` dresses the wrapper, which has no role; the accessible NAME — and the\n // id, description, invalid and required state above — go on the trigger, which\n // has one. Spread FIRST so the trigger's ARIA and the\n // handlers that open the panel cannot be clobbered from outside.\n <div {...rest} className={cn(\"relative\", className)}>\n {label !== undefined && <FieldLabel>{label}</FieldLabel>}\n <button\n ref={triggerRef}\n id={id}\n type=\"button\"\n // A combobox, not a button. The distinction is not pedantry: this control\n // carried `aria-invalid`, which `button` does not support, so a required\n // field left empty painted a rose border and told a reader nothing at all —\n // and ESLint flagged it as exactly that (`role-supports-aria-props`). The\n // fix the audit asked for is the role that describes what this IS: a closed\n // choice that expands into the list named below. `combobox` supports\n // `aria-invalid`, so the border and the announcement finally agree.\n role=\"combobox\"\n aria-expanded={open}\n aria-controls={listboxId}\n aria-haspopup=\"listbox\"\n // The label is a floating <span>, not a <label for>, so without this the\n // trigger's accessible name is whatever value happens to be selected —\n // \"Checking\" with nothing saying it is the account. The label AND the\n // value, because `aria-label` replaces the content rather than adding to\n // it, and a control that announces only its name has lost the answer.\n // A caller's own name wins over the composition: two fields labelled\n // \"Account\" on a transfer form are the from and the to, and only the\n // caller knows which is which.\n aria-label={\n ariaLabel ??\n (typeof label === \"string\" ? common.fieldValue(label, summary) : undefined)\n }\n disabled={disabled}\n aria-invalid={field.isInvalid || undefined}\n aria-describedby={field.describedBy}\n aria-required={ariaRequired}\n onClick={() => !disabled && setOpen((o) => !o)}\n // Down/Up opens the list from the closed trigger, per the APG. Enter and\n // Space already do it through the button's own click.\n onKeyDown={(e) => {\n if (disabled) return;\n if (e.key === \"ArrowDown\" || e.key === \"ArrowUp\") {\n e.preventDefault();\n setOpen(true);\n }\n }}\n className={cn(\n FIELD_TRIGGER,\n \"pe-9\",\n label !== undefined && FIELD_FLOATING_PAD,\n disabled && \"cursor-not-allowed opacity-50\",\n field.isInvalid && FIELD_INVALID,\n )}\n >\n <span\n className={cn(\n \"min-w-0 truncate\",\n // Inherit FIELD_BASE's ink for a value, keep the placeholder tone\n // (`--text-placeholder`) — the same rule every field on a form row\n // follows (Keksdose dev#477).\n !value.length && \"text-[var(--text-placeholder)]\",\n )}\n >\n {summary}\n </span>\n {showClear ? (\n /* eslint-disable-next-line jsx-a11y/click-events-have-key-events -- tabIndex -1 inside\n the trigger <button>, so it never holds focus and a key handler here could\n never fire. A pointer shortcut; the keyboard path is unticking the options\n in the open list. */\n <span\n role=\"button\"\n tabIndex={-1}\n aria-label={labels.clear}\n onClick={(e) => {\n e.stopPropagation();\n onChange([]);\n }}\n className=\"absolute end-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-[var(--text-placeholder)] hover:text-[var(--text-secondary)]\"\n >\n <X className=\"size-4\" />\n </span>\n ) : (\n <FieldChevron />\n )}\n </button>\n <ComboboxPanel\n core={core}\n listboxId={listboxId}\n multi\n // On a phone the panel becomes a full-screen sheet, which needs the field's\n // own label to say what it is asking for (live #200).\n sheetTitle={label ?? placeholder}\n searchPlaceholder={labels.search}\n emptyLabel={labels.noResults}\n closeLabel={closeLabel}\n loadErrorLabel={loadErrorLabel}\n isSelected={(v) => valueSet.has(v)}\n onChoose={toggle}\n showCreate={showCreate}\n onCreate={() => {\n onCreate?.(q);\n core.setQuery(\"\");\n }}\n createContent={labels.create(q)}\n />\n {field.errorEl}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/multi-entity-combobox.tsx"],"sourcesContent":["import { useId, useMemo, type ComponentPropsWithoutRef, type ReactNode } from \"react\";\nimport { X } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { FieldChevron, FieldLabel, FIELD_TRIGGER, FIELD_FLOATING_PAD, FIELD_INVALID } from \"./ui\";\nimport {\n ComboboxPanel,\n endHintRowProps,\n offersCreate,\n useComboboxCore,\n useComboboxFieldError,\n type ComboOption,\n} from \"./combobox-core\";\nimport {\n DEFAULT_COMBOBOX_LABELS,\n DEFAULT_COMMON_LABELS,\n useKitLabels,\n useKitLocale,\n} from \"../i18n/kit-labels\";\nimport {\n EndHintRow,\n FieldCaption,\n LABEL_IN_ROW,\n LockedReason,\n StaticLabelRow,\n useFieldHint,\n useLockReason,\n} from \"./field-parts\";\nimport { mergeDescribedBy } from \"./choice-parts\";\nimport { useCommitReason } from \"./write-lock\";\n\n/** `onChange` is the kit's — \"a selection was made\", carrying values — rather\n * than the div's form event, so the DOM's spelling of it is omitted. */\nexport interface MultiEntityComboboxProps<V extends string | number>\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\"> {\n /** Selected ids. */\n value: V[];\n onChange: (value: V[]) => void;\n /** Already-loaded options (client-side filtered); also resolves selected labels. */\n options?: ComboOption<V>[];\n /** Async option source, debounced + race-safe; a rejection shows\n * `loadErrorLabel`. When set, `options` resolves labels only. */\n loadOptions?: (query: string) => Promise<ComboOption<V>[]>;\n loading?: boolean;\n label?: ReactNode;\n placeholder?: string;\n searchPlaceholder?: string;\n emptyLabel?: string;\n /** Trigger summary when 1+ selected (default: the labels as a locale-formatted\n * list, or `combobox.selectedCount` when a label can't be resolved). */\n itemLabel?: (count: number) => string;\n /** Offer a clear \"×\" while anything is chosen; it emits `[]`. A pointer shortcut out\n * of the tab order — the keyboard's way is Delete or Backspace on the CLOSED trigger,\n * or unticking rows in the open list. Not offered while `disabled` or locked. */\n clearable?: boolean;\n clearLabel?: string;\n /** The phone sheet's close button. Its own prop rather than a reuse of\n * `clearLabel`: \"clear the selection\" and \"close this screen\" are different\n * actions, and on a full-screen sheet the close button is the only way out —\n * so it is the one control here that MUST be in the reader's language. */\n closeLabel?: string;\n disabled?: boolean;\n /** Why the selection cannot be changed — {@link EntityCombobox}'s `disabledReason`:\n * focusable and `aria-disabled`, no list, no clear, no `onChange`, the reason in the\n * kit {@link Tooltip}. Wins over `disabled`. */\n disabledReason?: ReactNode;\n /** This picker COMMITS — toggling saves. Locked the `disabledReason` way under a\n * locked {@link WriteLockProvider}. See {@link EntityCombobox}'s `commit`. */\n commit?: boolean;\n /** When set, a \"create\" row appears for a non-empty query with no exact match.\n * The panel stays open (adding the new entity to `value` is the caller's job). */\n onCreate?: (query: string) => void;\n createLabel?: (query: string) => string;\n /** Offer the create row with nothing typed too, worded as this. See\n * {@link EntityCombobox}'s `createEmptyLabel`. */\n createEmptyLabel?: string;\n /** The create row is a write while toggling is not: locked on its own under a locked\n * {@link WriteLockProvider}. See {@link EntityCombobox}'s `createCommit`. */\n createCommit?: boolean;\n /** Required and unanswered — {@link FIELD_INVALID}. See {@link Input}'s `invalid`. */\n invalid?: boolean;\n /** What is wrong with the value, as {@link Input}'s `error`: rendered under the\n * field, on the trigger's `aria-describedby`, and implies `invalid`. */\n error?: ReactNode;\n /**\n * Standing advice — the rest of the Combobox family's `hint` since 0.22, which this\n * one was left without (keksdose K4): plain TEXT is a caption under the field on the\n * trigger's `aria-describedby`, before any error; a {@link FieldHint} \"?\" rides the\n * label line, or with no label sits at the trigger's end edge, outside the box.\n * Passed at all — even `undefined` — the field keeps one box, so a hint coming and\n * going never rebuilds the trigger under the cursor (the 0.22 rule).\n */\n hint?: ReactNode;\n /** Narrow `options` client-side by the query. Default `true`; `false` shows them\n * as given (a server-ranked list). */\n filter?: boolean;\n /** Offer nothing, and call no `loadOptions`, below this many characters. Default\n * `0`, i.e. the list loads as the panel opens. */\n minChars?: number;\n /** `loadOptions` debounce. Default 150 ms. */\n debounceMs?: number;\n /** Shown when `loadOptions` rejects. Default: `combobox.loadError`. */\n loadErrorLabel?: string;\n}\n\n/**\n * Multi-value sibling of {@link EntityCombobox}: pick several entities by search,\n * with the same async `loadOptions`/`onCreate` support. Rows show a checkbox and\n * toggle without closing the panel; the trigger summarises the selection.\n */\nexport function MultiEntityCombobox<V extends string | number>(props: MultiEntityComboboxProps<V>) {\n const {\n value,\n onChange,\n options,\n loadOptions,\n loading,\n label,\n placeholder,\n searchPlaceholder,\n emptyLabel,\n itemLabel,\n clearable,\n clearLabel,\n closeLabel,\n disabled,\n disabledReason,\n commit,\n onCreate,\n createLabel,\n createEmptyLabel,\n createCommit,\n className,\n invalid,\n error,\n hint,\n filter,\n minChars,\n debounceMs,\n loadErrorLabel,\n \"aria-label\": ariaLabel,\n // The control's own wiring, taken off `rest` so it lands on the TRIGGER rather than\n // the wrapper: `Field`'s render-prop spreads `{ id, aria-describedby, aria-invalid,\n // aria-required }`, and on the role-less wrapper div the label's `htmlFor` named\n // nothing and the hint and required state described nothing — kastlan's\n // communication-page To/Cc/Bcc and lease-tenants-step.tsx read as unnamed pickers.\n id,\n \"aria-describedby\": ariaDescribedBy,\n \"aria-invalid\": ariaInvalid,\n \"aria-required\": ariaRequired,\n ...rest\n } = props;\n const core = useComboboxCore<V>({\n options,\n loadOptions,\n loading,\n filter,\n minChars,\n debounceMs,\n });\n const hintParts = useFieldHint(hint, ariaDescribedBy);\n const field = useComboboxFieldError(\n error,\n invalid || ariaInvalid === true || ariaInvalid === \"true\",\n hintParts.describedBy,\n );\n // The props are the per-instance overrides, the provider the app-wide ones; a\n // prop left `undefined` falls through to the provider rather than masking it.\n const labels = useKitLabels(\"combobox\", DEFAULT_COMBOBOX_LABELS, {\n search: searchPlaceholder,\n noResults: emptyLabel,\n clear: clearLabel,\n create: createLabel,\n });\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const { open, results, resolve, setOpen, query, triggerRef } = core;\n // The lock — see EntityCombobox: the trigger stays reachable and says why.\n const lock = useLockReason(commit, disabledReason);\n const inert = lock.locked || Boolean(disabled);\n if (lock.locked && open) setOpen(false);\n const createReason = useCommitReason(createCommit, undefined);\n // One id per instance, generated here rather than in the core: `aria-controls` on\n // the trigger has to name the list while the list is still closed, so the id\n // belongs to whoever renders both ends of it.\n const listboxId = useId();\n\n const valueSet = useMemo(() => new Set(value), [value]);\n // A list of NAMES, not a sentence, so the narrow conjunction: English gets\n // \"Food, Rent, Travel\" (no \"and\" squeezed into a truncating trigger), German and\n // French still get their \"und\"/\"et\" where the language insists on it, and Japanese\n // and Chinese get the ideographic comma \"、\". The `unit` type looked like the\n // natural fit for a summary, but in ja/zh it concatenates the names with nothing\n // between them at all.\n const locale = useKitLocale();\n const listFormat = useMemo(\n () => new Intl.ListFormat(locale, { type: \"conjunction\", style: \"narrow\" }),\n [locale],\n );\n const summary = (() => {\n if (value.length === 0) return placeholder ?? \"\";\n if (itemLabel) return itemLabel(value.length);\n const chosen = value.map(resolve);\n return chosen.every(Boolean)\n ? listFormat.format(chosen.map((o) => o!.label))\n : labels.selectedCount(value.length);\n })();\n\n // While locked the trigger sits in the reason's Tooltip, as on EntityCombobox.\n const withLock = (trigger: ReactNode) =>\n lock.locked ? (\n <LockedReason lock={lock} className=\"block\">\n {trigger}\n </LockedReason>\n ) : (\n trigger\n );\n\n const q = query.trim();\n const showCreate = offersCreate(\n onCreate,\n q,\n results.map((o) => o.label),\n createEmptyLabel,\n );\n const showClear = Boolean(clearable && value.length > 0 && !inert);\n\n const toggle = (o: ComboOption<V>) => {\n if (lock.locked) return;\n core.rememberOption(o);\n if (valueSet.has(o.value)) onChange(value.filter((v) => v !== o.value));\n else onChange([...value, o.value]);\n // Stay open — multi-select keeps adding.\n };\n\n return (\n // `rest` dresses the wrapper, which has no role; the accessible NAME — and the\n // id, description, invalid and required state above — go on the trigger, which\n // has one. Spread FIRST so the trigger's ARIA and the\n // handlers that open the panel cannot be clobbered from outside.\n <div {...rest} className={cn(\"relative\", className)}>\n {label !== undefined && hintParts.labelHint === undefined && <FieldLabel>{label}</FieldLabel>}\n <EndHintRow {...endHintRowProps(\"hint\" in props, label !== undefined, hintParts.labelHint)}>\n {withLock(\n <button\n ref={triggerRef}\n id={id}\n type=\"button\"\n // A combobox, not a button. The distinction is not pedantry: this control\n // carried `aria-invalid`, which `button` does not support, so a required\n // field left empty painted a rose border and told a reader nothing at all —\n // and ESLint flagged it as exactly that (`role-supports-aria-props`). The\n // fix the audit asked for is the role that describes what this IS: a closed\n // choice that expands into the list named below. `combobox` supports\n // `aria-invalid`, so the border and the announcement finally agree.\n role=\"combobox\"\n aria-expanded={open}\n aria-controls={listboxId}\n aria-haspopup=\"listbox\"\n // The label is a floating <span>, not a <label for>, so without this the\n // trigger's accessible name is whatever value happens to be selected —\n // \"Checking\" with nothing saying it is the account. The label AND the\n // value, because `aria-label` replaces the content rather than adding to\n // it, and a control that announces only its name has lost the answer.\n // A caller's own name wins over the composition: two fields labelled\n // \"Account\" on a transfer form are the from and the to, and only the\n // caller knows which is which.\n aria-label={\n ariaLabel ??\n (typeof label === \"string\" ? common.fieldValue(label, summary) : undefined)\n }\n // Focusable while locked — see EntityCombobox.\n disabled={lock.locked ? undefined : disabled}\n aria-disabled={lock.locked || undefined}\n aria-invalid={field.isInvalid || undefined}\n aria-describedby={mergeDescribedBy(field.describedBy, lock.locked && lock.reasonId)}\n aria-required={ariaRequired}\n onClick={() => !inert && setOpen((o) => !o)}\n // Down/Up opens the list from the closed trigger, per the APG. Enter and\n // Space already do it through the button's own click.\n onKeyDown={(e) => {\n if (inert) return;\n if (e.key === \"ArrowDown\" || e.key === \"ArrowUp\") {\n e.preventDefault();\n setOpen(true);\n } else if (showClear && !open && (e.key === \"Delete\" || e.key === \"Backspace\")) {\n // The keyboard's clear: the \"×\" is a pointer shortcut out of the tab\n // order, so without this a cleared-by-mouse field was set-only from the\n // keyboard. The closed trigger only — CountrySelect's rule (0.23).\n e.preventDefault();\n onChange([]);\n }\n }}\n className={cn(\n FIELD_TRIGGER,\n \"pe-9\",\n label !== undefined && FIELD_FLOATING_PAD,\n inert && \"cursor-not-allowed opacity-50\",\n field.isInvalid && FIELD_INVALID,\n )}\n >\n <span\n className={cn(\n \"min-w-0 truncate\",\n // Inherit FIELD_BASE's ink for a value, keep the placeholder tone\n // (`--text-placeholder`) — the same rule every field on a form row\n // follows (Keksdose dev#477).\n !value.length && \"text-[var(--text-placeholder)]\",\n )}\n >\n {summary}\n </span>\n {showClear ? (\n /* eslint-disable-next-line jsx-a11y/click-events-have-key-events -- tabIndex -1 inside\n the trigger <button>, so it never holds focus and a key handler here could\n never fire. A pointer shortcut; the keyboard paths are Delete on the closed\n trigger and unticking the options in the open list. */\n <span\n role=\"button\"\n tabIndex={-1}\n aria-label={labels.clear}\n onClick={(e) => {\n e.stopPropagation();\n onChange([]);\n }}\n className=\"absolute end-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-[var(--text-placeholder)] hover:text-[var(--text-secondary)]\"\n >\n <X className=\"size-4\" />\n </span>\n ) : (\n <FieldChevron />\n )}\n </button>,\n )}\n </EndHintRow>\n {/* With a \"?\" the label shares the top strip with it — after the trigger, so the\n \"?\" follows the control in the tab order, as on EntityCombobox. */}\n {label !== undefined && hintParts.labelHint !== undefined && (\n <StaticLabelRow hint={hintParts.labelHint}>\n <FieldLabel className={LABEL_IN_ROW}>{label}</FieldLabel>\n </StaticLabelRow>\n )}\n <ComboboxPanel\n core={core}\n listboxId={listboxId}\n multi\n // On a phone the panel becomes a full-screen sheet, which needs the field's\n // own label to say what it is asking for (live #200).\n sheetTitle={label ?? placeholder}\n searchPlaceholder={labels.search}\n emptyLabel={labels.noResults}\n closeLabel={closeLabel}\n loadErrorLabel={loadErrorLabel}\n isSelected={(v) => valueSet.has(v)}\n onChoose={toggle}\n showCreate={showCreate}\n onCreate={() => {\n if (lock.locked) return;\n onCreate?.(q);\n core.setQuery(\"\");\n }}\n createContent={q ? labels.create(q) : createEmptyLabel}\n createDisabledReason={createReason}\n />\n <FieldCaption parts={hintParts} />\n {field.errorEl}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -40,10 +40,17 @@ interface MultiSelectProps extends Omit<ComponentPropsWithoutRef<"div">, "onChan
40
40
  * caller passed) and implies `invalid` — {@link Input}'s `error`, on the same
41
41
  * rules. */
42
42
  error?: ReactNode;
43
+ /**
44
+ * Standing advice for the field — {@link Combobox}'s `hint` (keksdose K4): plain TEXT
45
+ * is a caption under the field on the trigger's `aria-describedby`, before any error;
46
+ * a {@link FieldHint} "?" rides the label line, or with no label sits at the trigger's
47
+ * end edge, outside the box. Not an option's `hint`, which stays on its row.
48
+ */
49
+ hint?: ReactNode;
43
50
  /** The trigger cannot be opened: FIELD_BASE's settled grey, no chevron (as a
44
51
  * disabled {@link Select} drops its own), and a dimmed label. */
45
52
  disabled?: boolean;
46
53
  }
47
- declare function MultiSelect({ options, values, onChange, allLabel, itemLabel, placeholder, label, searchLabel, selectAllLabel, clearLabel, className, panelClassName, invalid, error, disabled, "aria-label": ariaLabel, "aria-describedby": ariaDescribedBy, ...rest }: MultiSelectProps): react.JSX.Element;
54
+ declare function MultiSelect({ options, values, onChange, allLabel, itemLabel, placeholder, label, searchLabel, selectAllLabel, clearLabel, className, panelClassName, invalid, error, hint, disabled, "aria-label": ariaLabel, "aria-describedby": ariaDescribedBy, ...rest }: MultiSelectProps): react.JSX.Element;
48
55
 
49
56
  export { MultiSelect, type MultiSelectOption, type MultiSelectProps };
@@ -6,6 +6,7 @@ import { FieldChevron, FieldLabel, FIELD_TRIGGER, FIELD_INVALID, FIELD_FLOATING_
6
6
  import { cn } from "../lib/cn.js";
7
7
  import { DropdownPanel, DropdownSearchHeader, useDropdownSearch } from "./dropdown.js";
8
8
  import { useActiveOptionScroll, useComboboxFieldError } from "./combobox-core.js";
9
+ import { EndHintRow, FieldCaption, LABEL_IN_ROW, StaticLabelRow, useFieldHint } from "./field-parts.js";
9
10
  import {
10
11
  DEFAULT_COMMON_LABELS,
11
12
  DEFAULT_MULTI_SELECT_LABELS,
@@ -26,6 +27,7 @@ function MultiSelect({
26
27
  panelClassName,
27
28
  invalid,
28
29
  error,
30
+ hint,
29
31
  disabled,
30
32
  "aria-label": ariaLabel,
31
33
  "aria-describedby": ariaDescribedBy,
@@ -39,7 +41,8 @@ function MultiSelect({
39
41
  selectedCount: itemLabel
40
42
  });
41
43
  const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
42
- const field = useComboboxFieldError(error, invalid, ariaDescribedBy);
44
+ const hintParts = useFieldHint(hint, ariaDescribedBy);
45
+ const field = useComboboxFieldError(error, invalid, hintParts.describedBy);
43
46
  const uid = useId();
44
47
  const listboxId = `${uid}-listbox`;
45
48
  const optionId = (index) => `${uid}-option-${index}`;
@@ -92,8 +95,8 @@ function MultiSelect({
92
95
  // the thing with a role. Spread FIRST so the ARIA the trigger composes, and the
93
96
  // handlers that open the list, cannot be replaced by accident from outside.
94
97
  /* @__PURE__ */ jsxs("div", { ...rest, ref: wrapperRef, className: cn("relative", className), children: [
95
- label !== void 0 && /* @__PURE__ */ jsx(FieldLabel, { className: disabled ? "opacity-50" : void 0, children: label }),
96
- /* @__PURE__ */ jsxs(
98
+ label !== void 0 && hintParts.labelHint === void 0 && /* @__PURE__ */ jsx(FieldLabel, { className: disabled ? "opacity-50" : void 0, children: label }),
99
+ /* @__PURE__ */ jsx(EndHintRow, { hint: label === void 0 ? hintParts.labelHint : void 0, children: /* @__PURE__ */ jsxs(
97
100
  "button",
98
101
  {
99
102
  ref: triggerRef,
@@ -133,7 +136,8 @@ function MultiSelect({
133
136
  !disabled && /* @__PURE__ */ jsx(FieldChevron, {})
134
137
  ]
135
138
  }
136
- ),
139
+ ) }),
140
+ label !== void 0 && hintParts.labelHint !== void 0 && /* @__PURE__ */ jsx(StaticLabelRow, { hint: hintParts.labelHint, children: /* @__PURE__ */ jsx(FieldLabel, { className: cn(LABEL_IN_ROW, disabled && "opacity-50"), children: label }) }),
137
141
  open && !disabled && /* @__PURE__ */ jsx(
138
142
  DropdownPanel,
139
143
  {
@@ -216,6 +220,7 @@ function MultiSelect({
216
220
  })
217
221
  }
218
222
  ),
223
+ /* @__PURE__ */ jsx(FieldCaption, { parts: hintParts }),
219
224
  field.errorEl
220
225
  ] })
221
226
  );
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/multi-select.tsx"],"sourcesContent":["import { useId, useMemo, useState } from \"react\";\nimport type {\n ComponentPropsWithoutRef,\n KeyboardEvent as ReactKeyboardEvent,\n ReactNode,\n} from \"react\";\nimport { Check } from \"lucide-react\";\nimport { FieldChevron, FieldLabel, FIELD_TRIGGER, FIELD_INVALID, FIELD_FLOATING_PAD } from \"./ui\";\nimport { cn } from \"../lib/cn\";\nimport { DropdownPanel, DropdownSearchHeader, useDropdownSearch } from \"./dropdown\";\nimport { useActiveOptionScroll, useComboboxFieldError } from \"./combobox-core\";\nimport {\n DEFAULT_COMMON_LABELS,\n DEFAULT_MULTI_SELECT_LABELS,\n useKitLabels,\n} from \"../i18n/kit-labels\";\n\nexport interface MultiSelectOption {\n value: string | number;\n label: string;\n hint?: string;\n}\n\n/** `onChange` is the kit's — \"the selection changed\", carrying the new values —\n * rather than the div's form event. */\nexport interface MultiSelectProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\"> {\n options: MultiSelectOption[];\n values: (string | number)[];\n onChange: (next: (string | number)[]) => void;\n /** Displayed when no value is selected (i.e. \"all\"). Defaults to `placeholder`,\n * then to `multiSelect.all` from the {@link UiKitProvider}, then \"All\". */\n allLabel?: string;\n /** Singular/plural countable label, used when 1+ items are selected. Default:\n * `multiSelect.selectedCount` from the provider — the bare count in English. */\n itemLabel?: (count: number) => string;\n placeholder?: string;\n /** Embedded top-boundary label, matching the native Input/Select fields. */\n label?: ReactNode;\n /** Search-box placeholder. Default: `multiSelect.search` from the provider, else \"Search\". */\n searchLabel?: string;\n /** \"Select all\" action label. Default: `multiSelect.selectAll`, else \"Select all\". */\n selectAllLabel?: string;\n /** \"Clear\" action label. Default: `multiSelect.clear`, else \"Clear\". */\n clearLabel?: string;\n /** Extra classes for the open dropdown PANEL — the way to widen it past its\n * 16rem default when the rows carry more than a label (Keksdose feedback #147). */\n panelClassName?: string;\n /** Required and unanswered — {@link FIELD_INVALID}, the same rose border the\n * native `Select` has worn since feedback #235. Every field-styled control in\n * this package carries it now, so a form can mark any of its fields rather than\n * only the one that happened to have it first. */\n invalid?: boolean;\n /** What is wrong with the selection, in the caller's words. Rendered under the\n * field, pointed at by the trigger's `aria-describedby` (merged with any the\n * caller passed) and implies `invalid` — {@link Input}'s `error`, on the same\n * rules. */\n error?: ReactNode;\n /** The trigger cannot be opened: FIELD_BASE's settled grey, no chevron (as a\n * disabled {@link Select} drops its own), and a dimmed label. */\n disabled?: boolean;\n}\n\nexport function MultiSelect({\n options,\n values,\n onChange,\n allLabel,\n itemLabel,\n placeholder,\n label,\n searchLabel,\n selectAllLabel,\n clearLabel,\n className,\n panelClassName,\n invalid,\n error,\n disabled,\n \"aria-label\": ariaLabel,\n \"aria-describedby\": ariaDescribedBy,\n ...rest\n}: MultiSelectProps) {\n const { open, setOpen, wrapperRef, triggerRef, closeToTrigger, query, setQuery, inputRef } =\n useDropdownSearch();\n // `allLabel` is deliberately NOT folded in here: between it and the provider's\n // `all` sits `placeholder`, which has always doubled as the \"nothing picked\" text,\n // and a caller who set it must keep seeing it.\n const labels = useKitLabels(\"multiSelect\", DEFAULT_MULTI_SELECT_LABELS, {\n search: searchLabel,\n selectAll: selectAllLabel,\n clear: clearLabel,\n selectedCount: itemLabel,\n });\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const field = useComboboxFieldError(error, invalid, ariaDescribedBy);\n // One id per instance: `aria-controls` on the trigger names the list while the\n // list is still closed.\n const uid = useId();\n const listboxId = `${uid}-listbox`;\n const optionId = (index: number) => `${uid}-option-${index}`;\n // Which row the arrow keys are on. Reset from the handlers that change what the\n // list contains rather than from an effect — a render pass is not where this\n // decision belongs, and the two callers that move the list are right here.\n const [active, setActive] = useState(0);\n\n const filtered = useMemo(() => {\n const q = query.trim().toLowerCase();\n if (!q) return options;\n return options.filter(\n (o) =>\n o.label.toLowerCase().includes(q) ||\n (o.hint ?? \"\").toLowerCase().includes(q),\n );\n }, [options, query]);\n\n const valueSet = useMemo(() => new Set(values), [values]);\n const allChecked = options.length > 0 && options.every((o) => valueSet.has(o.value));\n const anyChecked = values.length > 0;\n const selectedSummary = (() => {\n if (!anyChecked || allChecked) return allLabel ?? placeholder ?? labels.all;\n return labels.selectedCount(values.length);\n })();\n\n const toggle = (val: string | number) => {\n if (valueSet.has(val)) onChange(values.filter((v) => v !== val));\n else onChange([...values, val]);\n };\n\n const selectAll = () => onChange(options.map((o) => o.value));\n const clearAll = () => onChange([]);\n\n const activeId = active >= 0 && active < filtered.length ? optionId(active) : undefined;\n useActiveOptionScroll(activeId);\n\n // The WAI-ARIA APG's listbox keyboard, handled on the search box because that is\n // where focus is while the panel is up. Escape is `useDropdown`'s, on the document.\n const onListKeyDown = (e: ReactKeyboardEvent<HTMLInputElement>) => {\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n setActive((i) => Math.min(i + 1, filtered.length - 1));\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n setActive((i) => Math.max(i - 1, 0));\n } else if (e.key === \"Home\") {\n e.preventDefault();\n setActive(0);\n } else if (e.key === \"End\") {\n e.preventDefault();\n setActive(filtered.length - 1);\n } else if (e.key === \"Enter\") {\n e.preventDefault();\n // Toggling keeps the panel open, exactly as clicking a row does: this control\n // exists to answer \"which of these\", which is rarely one of them.\n if (filtered[active]) toggle(filtered[active].value);\n } else if (e.key === \"Tab\") {\n e.preventDefault();\n closeToTrigger();\n }\n };\n\n return (\n // `rest` dresses the wrapper; the name belongs to the trigger below it, which is\n // the thing with a role. Spread FIRST so the ARIA the trigger composes, and the\n // handlers that open the list, cannot be replaced by accident from outside.\n <div {...rest} ref={wrapperRef} className={cn(\"relative\", className)}>\n {label !== undefined && <FieldLabel className={disabled ? \"opacity-50\" : undefined}>{label}</FieldLabel>}\n <button\n ref={triggerRef}\n type=\"button\"\n disabled={disabled}\n // A combobox, not a bare button: this trigger carried `aria-invalid`, which\n // `button` does not support, so a required-and-empty filter painted its rose\n // border and announced nothing (ESLint's `role-supports-aria-props`, the\n // audit's §a11y). The role that describes it is the one that also supports\n // the attribute.\n role=\"combobox\"\n aria-expanded={open}\n aria-controls={listboxId}\n aria-haspopup=\"listbox\"\n // The label is a floating <span>, not a <label for>, so without this the\n // trigger announces only its summary — \"3 selected\", with nothing saying\n // three of WHAT. The label AND the summary, because `aria-label` replaces\n // the content rather than adding to it; the same composition its two\n // siblings use, so a form row of pickers reads the same way throughout.\n // A caller's own name wins: two filters labelled \"Accounts\" on one screen have\n // to be told apart, and only the caller knows by what.\n aria-label={\n ariaLabel ?? (typeof label === \"string\" ? common.fieldValue(label, selectedSummary) : undefined)\n }\n onClick={() => {\n setOpen((v) => !v);\n setActive(0);\n }}\n // Down/Up opens the list from the closed trigger, per the APG; Enter and\n // Space already do through the button's own click.\n onKeyDown={(e) => {\n if (e.key === \"ArrowDown\" || e.key === \"ArrowUp\") {\n e.preventDefault();\n setOpen(true);\n setActive(0);\n }\n }}\n aria-invalid={field.isInvalid || undefined}\n // On the TRIGGER, where focus lands — the `aria-describedby` a caller put on\n // the component is taken off the wrapper for the same reason.\n aria-describedby={field.describedBy}\n className={cn(\n FIELD_TRIGGER,\n \"pe-9\",\n label !== undefined && FIELD_FLOATING_PAD,\n // FIELD_TRIGGER's hover would still light a trigger nothing can open.\n \"disabled:hover:bg-[var(--bg-surface-2)]\",\n // Dimmed like EntityCombobox's and MultiEntityCombobox's disabled triggers;\n // only the label was, so a disabled MultiSelect read as live (showcase audit).\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n field.isInvalid && FIELD_INVALID,\n )}\n >\n {/* The summary IS the field's value, so it inherits FIELD_BASE's ink rather\n than restating a lighter one (Keksdose dev#477). */}\n <span className=\"truncate\">{selectedSummary}</span>\n {!disabled && <FieldChevron />}\n </button>\n {open && !disabled && (\n <DropdownPanel\n // 16rem by default; `panelClassName` is how a caller widens it (Keksdose\n // feedback #147: the market picker's rows carry a postcode, a town, a\n // distance and a receipt count, all of which were being truncated). The\n // override used to be applied from the OUTSIDE with a\n // `[&>div]:w-full` descendant selector, which worked and was a hack —\n // \"change the package\" was the right call.\n className={cn(\"w-64\", panelClassName)}\n empty={filtered.length === 0}\n // The rows below are options, so the list around them has to be a listbox\n // — and one that says several answers are allowed, which is the only thing\n // that made the trigger's \"3 selected\" make sense to a reader.\n listProps={{ id: listboxId, role: \"listbox\", \"aria-multiselectable\": true }}\n header={\n <>\n <DropdownSearchHeader\n query={query}\n onQueryChange={(v) => {\n setQuery(v);\n // The list under the highlight just changed; keeping the old index\n // would point `aria-activedescendant` at a different option than\n // the one the user was on.\n setActive(0);\n }}\n inputRef={inputRef}\n placeholder={labels.search}\n listboxId={listboxId}\n activeId={activeId}\n onKeyDown={onListKeyDown}\n />\n <div className=\"flex items-center justify-between gap-2 px-2 py-1.5 text-xs\">\n <button\n type=\"button\"\n onClick={selectAll}\n disabled={allChecked}\n className=\"rounded px-2 py-0.5 text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] disabled:opacity-40\"\n >\n {labels.selectAll}\n </button>\n <button\n type=\"button\"\n onClick={clearAll}\n disabled={!anyChecked}\n className=\"rounded px-2 py-0.5 text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] disabled:opacity-40\"\n >\n {labels.clear}\n </button>\n </div>\n </>\n }\n >\n {filtered.map((o, i) => {\n const checked = valueSet.has(o.value);\n return (\n <li key={o.value} role=\"presentation\">\n <button\n type=\"button\"\n id={optionId(i)}\n // The checked state used to exist ONLY as the decorative span\n // below (the audit's §a11y): the row said its label and nothing\n // else, so \"selected\" was imperceptible without sight. This is\n // the same state, in the one place a reader looks for it.\n role=\"option\"\n aria-selected={checked}\n // Focus stays in the search box — that is what\n // `aria-activedescendant` is for — so the rows are not tab stops.\n tabIndex={-1}\n onClick={() => toggle(o.value)}\n onMouseEnter={() => setActive(i)}\n className={cn(\n \"flex w-full items-center justify-between gap-2 px-3 py-1.5 text-sm text-start hover:bg-[var(--bg-hover)]\",\n checked && \"bg-[var(--bg-active)]\",\n i === active && \"bg-[var(--bg-hover)]\",\n )}\n >\n <span className=\"flex items-center gap-2 min-w-0\">\n <span\n // A picture of `aria-selected` above; a reader that met both\n // would be told twice.\n aria-hidden\n className={cn(\n \"flex size-4 shrink-0 items-center justify-center rounded border\",\n checked\n ? \"border-[var(--bg-inverse)] bg-[var(--bg-inverse)] text-[var(--text-inverse)]\"\n : \"border-[var(--border-strong)] bg-[var(--bg-surface)]\",\n )}\n >\n {checked && <Check className=\"size-3\" />}\n </span>\n <span className=\"truncate text-[var(--text-primary)]\">{o.label}</span>\n {o.hint && (\n <span className=\"truncate text-xs text-[var(--text-placeholder)]\">\n {o.hint}\n </span>\n )}\n </span>\n </button>\n </li>\n );\n })}\n </DropdownPanel>\n )}\n {/* After the panel, not before it: the panel is `absolute` with no `top`, so it\n opens at its place in the flow — under the message, had the message come\n first. */}\n {field.errorEl}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/multi-select.tsx"],"sourcesContent":["import { useId, useMemo, useState } from \"react\";\nimport type {\n ComponentPropsWithoutRef,\n KeyboardEvent as ReactKeyboardEvent,\n ReactNode,\n} from \"react\";\nimport { Check } from \"lucide-react\";\nimport { FieldChevron, FieldLabel, FIELD_TRIGGER, FIELD_INVALID, FIELD_FLOATING_PAD } from \"./ui\";\nimport { cn } from \"../lib/cn\";\nimport { DropdownPanel, DropdownSearchHeader, useDropdownSearch } from \"./dropdown\";\nimport { useActiveOptionScroll, useComboboxFieldError } from \"./combobox-core\";\nimport { EndHintRow, FieldCaption, LABEL_IN_ROW, StaticLabelRow, useFieldHint } from \"./field-parts\";\nimport {\n DEFAULT_COMMON_LABELS,\n DEFAULT_MULTI_SELECT_LABELS,\n useKitLabels,\n} from \"../i18n/kit-labels\";\n\nexport interface MultiSelectOption {\n value: string | number;\n label: string;\n hint?: string;\n}\n\n/** `onChange` is the kit's — \"the selection changed\", carrying the new values —\n * rather than the div's form event. */\nexport interface MultiSelectProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\"> {\n options: MultiSelectOption[];\n values: (string | number)[];\n onChange: (next: (string | number)[]) => void;\n /** Displayed when no value is selected (i.e. \"all\"). Defaults to `placeholder`,\n * then to `multiSelect.all` from the {@link UiKitProvider}, then \"All\". */\n allLabel?: string;\n /** Singular/plural countable label, used when 1+ items are selected. Default:\n * `multiSelect.selectedCount` from the provider — the bare count in English. */\n itemLabel?: (count: number) => string;\n placeholder?: string;\n /** Embedded top-boundary label, matching the native Input/Select fields. */\n label?: ReactNode;\n /** Search-box placeholder. Default: `multiSelect.search` from the provider, else \"Search\". */\n searchLabel?: string;\n /** \"Select all\" action label. Default: `multiSelect.selectAll`, else \"Select all\". */\n selectAllLabel?: string;\n /** \"Clear\" action label. Default: `multiSelect.clear`, else \"Clear\". */\n clearLabel?: string;\n /** Extra classes for the open dropdown PANEL — the way to widen it past its\n * 16rem default when the rows carry more than a label (Keksdose feedback #147). */\n panelClassName?: string;\n /** Required and unanswered — {@link FIELD_INVALID}, the same rose border the\n * native `Select` has worn since feedback #235. Every field-styled control in\n * this package carries it now, so a form can mark any of its fields rather than\n * only the one that happened to have it first. */\n invalid?: boolean;\n /** What is wrong with the selection, in the caller's words. Rendered under the\n * field, pointed at by the trigger's `aria-describedby` (merged with any the\n * caller passed) and implies `invalid` — {@link Input}'s `error`, on the same\n * rules. */\n error?: ReactNode;\n /**\n * Standing advice for the field — {@link Combobox}'s `hint` (keksdose K4): plain TEXT\n * is a caption under the field on the trigger's `aria-describedby`, before any error;\n * a {@link FieldHint} \"?\" rides the label line, or with no label sits at the trigger's\n * end edge, outside the box. Not an option's `hint`, which stays on its row.\n */\n hint?: ReactNode;\n /** The trigger cannot be opened: FIELD_BASE's settled grey, no chevron (as a\n * disabled {@link Select} drops its own), and a dimmed label. */\n disabled?: boolean;\n}\n\nexport function MultiSelect({\n options,\n values,\n onChange,\n allLabel,\n itemLabel,\n placeholder,\n label,\n searchLabel,\n selectAllLabel,\n clearLabel,\n className,\n panelClassName,\n invalid,\n error,\n hint,\n disabled,\n \"aria-label\": ariaLabel,\n \"aria-describedby\": ariaDescribedBy,\n ...rest\n}: MultiSelectProps) {\n const { open, setOpen, wrapperRef, triggerRef, closeToTrigger, query, setQuery, inputRef } =\n useDropdownSearch();\n // `allLabel` is deliberately NOT folded in here: between it and the provider's\n // `all` sits `placeholder`, which has always doubled as the \"nothing picked\" text,\n // and a caller who set it must keep seeing it.\n const labels = useKitLabels(\"multiSelect\", DEFAULT_MULTI_SELECT_LABELS, {\n search: searchLabel,\n selectAll: selectAllLabel,\n clear: clearLabel,\n selectedCount: itemLabel,\n });\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const hintParts = useFieldHint(hint, ariaDescribedBy);\n const field = useComboboxFieldError(error, invalid, hintParts.describedBy);\n // One id per instance: `aria-controls` on the trigger names the list while the\n // list is still closed.\n const uid = useId();\n const listboxId = `${uid}-listbox`;\n const optionId = (index: number) => `${uid}-option-${index}`;\n // Which row the arrow keys are on. Reset from the handlers that change what the\n // list contains rather than from an effect — a render pass is not where this\n // decision belongs, and the two callers that move the list are right here.\n const [active, setActive] = useState(0);\n\n const filtered = useMemo(() => {\n const q = query.trim().toLowerCase();\n if (!q) return options;\n return options.filter(\n (o) =>\n o.label.toLowerCase().includes(q) ||\n (o.hint ?? \"\").toLowerCase().includes(q),\n );\n }, [options, query]);\n\n const valueSet = useMemo(() => new Set(values), [values]);\n const allChecked = options.length > 0 && options.every((o) => valueSet.has(o.value));\n const anyChecked = values.length > 0;\n const selectedSummary = (() => {\n if (!anyChecked || allChecked) return allLabel ?? placeholder ?? labels.all;\n return labels.selectedCount(values.length);\n })();\n\n const toggle = (val: string | number) => {\n if (valueSet.has(val)) onChange(values.filter((v) => v !== val));\n else onChange([...values, val]);\n };\n\n const selectAll = () => onChange(options.map((o) => o.value));\n const clearAll = () => onChange([]);\n\n const activeId = active >= 0 && active < filtered.length ? optionId(active) : undefined;\n useActiveOptionScroll(activeId);\n\n // The WAI-ARIA APG's listbox keyboard, handled on the search box because that is\n // where focus is while the panel is up. Escape is `useDropdown`'s, on the document.\n const onListKeyDown = (e: ReactKeyboardEvent<HTMLInputElement>) => {\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n setActive((i) => Math.min(i + 1, filtered.length - 1));\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n setActive((i) => Math.max(i - 1, 0));\n } else if (e.key === \"Home\") {\n e.preventDefault();\n setActive(0);\n } else if (e.key === \"End\") {\n e.preventDefault();\n setActive(filtered.length - 1);\n } else if (e.key === \"Enter\") {\n e.preventDefault();\n // Toggling keeps the panel open, exactly as clicking a row does: this control\n // exists to answer \"which of these\", which is rarely one of them.\n if (filtered[active]) toggle(filtered[active].value);\n } else if (e.key === \"Tab\") {\n e.preventDefault();\n closeToTrigger();\n }\n };\n\n return (\n // `rest` dresses the wrapper; the name belongs to the trigger below it, which is\n // the thing with a role. Spread FIRST so the ARIA the trigger composes, and the\n // handlers that open the list, cannot be replaced by accident from outside.\n <div {...rest} ref={wrapperRef} className={cn(\"relative\", className)}>\n {label !== undefined && hintParts.labelHint === undefined && (\n <FieldLabel className={disabled ? \"opacity-50\" : undefined}>{label}</FieldLabel>\n )}\n <EndHintRow hint={label === undefined ? hintParts.labelHint : undefined}>\n <button\n ref={triggerRef}\n type=\"button\"\n disabled={disabled}\n // A combobox, not a bare button: this trigger carried `aria-invalid`, which\n // `button` does not support, so a required-and-empty filter painted its rose\n // border and announced nothing (ESLint's `role-supports-aria-props`, the\n // audit's §a11y). The role that describes it is the one that also supports\n // the attribute.\n role=\"combobox\"\n aria-expanded={open}\n aria-controls={listboxId}\n aria-haspopup=\"listbox\"\n // The label is a floating <span>, not a <label for>, so without this the\n // trigger announces only its summary — \"3 selected\", with nothing saying\n // three of WHAT. The label AND the summary, because `aria-label` replaces\n // the content rather than adding to it; the same composition its two\n // siblings use, so a form row of pickers reads the same way throughout.\n // A caller's own name wins: two filters labelled \"Accounts\" on one screen have\n // to be told apart, and only the caller knows by what.\n aria-label={\n ariaLabel ?? (typeof label === \"string\" ? common.fieldValue(label, selectedSummary) : undefined)\n }\n onClick={() => {\n setOpen((v) => !v);\n setActive(0);\n }}\n // Down/Up opens the list from the closed trigger, per the APG; Enter and\n // Space already do through the button's own click.\n onKeyDown={(e) => {\n if (e.key === \"ArrowDown\" || e.key === \"ArrowUp\") {\n e.preventDefault();\n setOpen(true);\n setActive(0);\n }\n }}\n aria-invalid={field.isInvalid || undefined}\n // On the TRIGGER, where focus lands — the `aria-describedby` a caller put on\n // the component is taken off the wrapper for the same reason.\n aria-describedby={field.describedBy}\n className={cn(\n FIELD_TRIGGER,\n \"pe-9\",\n label !== undefined && FIELD_FLOATING_PAD,\n // FIELD_TRIGGER's hover would still light a trigger nothing can open.\n \"disabled:hover:bg-[var(--bg-surface-2)]\",\n // Dimmed like EntityCombobox's and MultiEntityCombobox's disabled triggers;\n // only the label was, so a disabled MultiSelect read as live (showcase audit).\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n field.isInvalid && FIELD_INVALID,\n )}\n >\n {/* The summary IS the field's value, so it inherits FIELD_BASE's ink rather\n than restating a lighter one (Keksdose dev#477). */}\n <span className=\"truncate\">{selectedSummary}</span>\n {!disabled && <FieldChevron />}\n </button>\n </EndHintRow>\n {/* With a \"?\" the label shares the top strip with it — after the trigger, so the\n \"?\" follows the control in the tab order, as on Input and Select. */}\n {label !== undefined && hintParts.labelHint !== undefined && (\n <StaticLabelRow hint={hintParts.labelHint}>\n <FieldLabel className={cn(LABEL_IN_ROW, disabled && \"opacity-50\")}>{label}</FieldLabel>\n </StaticLabelRow>\n )}\n {open && !disabled && (\n <DropdownPanel\n // 16rem by default; `panelClassName` is how a caller widens it (Keksdose\n // feedback #147: the market picker's rows carry a postcode, a town, a\n // distance and a receipt count, all of which were being truncated). The\n // override used to be applied from the OUTSIDE with a\n // `[&>div]:w-full` descendant selector, which worked and was a hack —\n // \"change the package\" was the right call.\n className={cn(\"w-64\", panelClassName)}\n empty={filtered.length === 0}\n // The rows below are options, so the list around them has to be a listbox\n // — and one that says several answers are allowed, which is the only thing\n // that made the trigger's \"3 selected\" make sense to a reader.\n listProps={{ id: listboxId, role: \"listbox\", \"aria-multiselectable\": true }}\n header={\n <>\n <DropdownSearchHeader\n query={query}\n onQueryChange={(v) => {\n setQuery(v);\n // The list under the highlight just changed; keeping the old index\n // would point `aria-activedescendant` at a different option than\n // the one the user was on.\n setActive(0);\n }}\n inputRef={inputRef}\n placeholder={labels.search}\n listboxId={listboxId}\n activeId={activeId}\n onKeyDown={onListKeyDown}\n />\n <div className=\"flex items-center justify-between gap-2 px-2 py-1.5 text-xs\">\n <button\n type=\"button\"\n onClick={selectAll}\n disabled={allChecked}\n className=\"rounded px-2 py-0.5 text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] disabled:opacity-40\"\n >\n {labels.selectAll}\n </button>\n <button\n type=\"button\"\n onClick={clearAll}\n disabled={!anyChecked}\n className=\"rounded px-2 py-0.5 text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] disabled:opacity-40\"\n >\n {labels.clear}\n </button>\n </div>\n </>\n }\n >\n {filtered.map((o, i) => {\n const checked = valueSet.has(o.value);\n return (\n <li key={o.value} role=\"presentation\">\n <button\n type=\"button\"\n id={optionId(i)}\n // The checked state used to exist ONLY as the decorative span\n // below (the audit's §a11y): the row said its label and nothing\n // else, so \"selected\" was imperceptible without sight. This is\n // the same state, in the one place a reader looks for it.\n role=\"option\"\n aria-selected={checked}\n // Focus stays in the search box — that is what\n // `aria-activedescendant` is for — so the rows are not tab stops.\n tabIndex={-1}\n onClick={() => toggle(o.value)}\n onMouseEnter={() => setActive(i)}\n className={cn(\n \"flex w-full items-center justify-between gap-2 px-3 py-1.5 text-sm text-start hover:bg-[var(--bg-hover)]\",\n checked && \"bg-[var(--bg-active)]\",\n i === active && \"bg-[var(--bg-hover)]\",\n )}\n >\n <span className=\"flex items-center gap-2 min-w-0\">\n <span\n // A picture of `aria-selected` above; a reader that met both\n // would be told twice.\n aria-hidden\n className={cn(\n \"flex size-4 shrink-0 items-center justify-center rounded border\",\n checked\n ? \"border-[var(--bg-inverse)] bg-[var(--bg-inverse)] text-[var(--text-inverse)]\"\n : \"border-[var(--border-strong)] bg-[var(--bg-surface)]\",\n )}\n >\n {checked && <Check className=\"size-3\" />}\n </span>\n <span className=\"truncate text-[var(--text-primary)]\">{o.label}</span>\n {o.hint && (\n <span className=\"truncate text-xs text-[var(--text-placeholder)]\">\n {o.hint}\n </span>\n )}\n </span>\n </button>\n </li>\n );\n })}\n </DropdownPanel>\n )}\n {/* After the panel, not before it: the panel is `absolute` with no `top`, so it\n opens at its place in the flow — under the message, had the message come\n first. */}\n <FieldCaption parts={hintParts} />\n {field.errorEl}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -2,17 +2,21 @@ import * as react from 'react';
2
2
  import { ComponentProps, ReactNode } from 'react';
3
3
  import { NumberInput } from './number-input.js';
4
4
  import './calculator.js';
5
- import '../kit-labels-D1J2_jyD.js';
6
- import './data-table-labels.js';
5
+ import '../feedback-BxeQVzwq.js';
6
+ import '../feedback-attachment-fGAzZPf0.js';
7
+ import '../feedback/feedback-inbox.js';
8
+ import 'lucide-react';
9
+ import './tooltip.js';
10
+ import '../hooks/use-anchored-rect.js';
11
+ import '../lib/clipping.js';
12
+ import '../data-table-labels-B7OdnM0S.js';
13
+ import './data-table-sort.js';
14
+ import './swipeable-row.js';
7
15
  import './mini-calendar.js';
8
16
  import './calendar-heatmap.js';
9
17
  import './popover.js';
10
18
  import './chip.js';
11
- import 'lucide-react';
12
19
  import './field-sync.js';
13
- import './tooltip.js';
14
- import '../hooks/use-anchored-rect.js';
15
- import '../lib/clipping.js';
16
20
  import '../wizard/types.js';
17
21
  import '../tour/tour.js';
18
22
  import '../search/command-palette.js';
@@ -26,7 +30,6 @@ import './sparkline.js';
26
30
  import './stat-tile.js';
27
31
  import './signature-pad.js';
28
32
  import './password-strength.js';
29
- import './danger-confirm.js';
30
33
  import './swatch-picker.js';
31
34
  import './tile-radio.js';
32
35
  import './icon-picker.js';
@@ -34,11 +37,7 @@ import './dialog-frame.js';
34
37
  import './modal.js';
35
38
  import '../hooks/use-search-param-state.js';
36
39
  import './measured-grid.js';
37
- import '../feedback-attachment-WFiXJ8We.js';
38
- import '../feedback/feedback-thread.js';
39
- import '../feedback/feedback-inbox.js';
40
40
  import './account-settings-labels.js';
41
- import './confirm-dialog.js';
42
41
  import './floating-panel.js';
43
42
  import './bulk-action-bar.js';
44
43
  import './list.js';
@@ -57,18 +56,24 @@ import './write-lock.js';
57
56
  import './legal.js';
58
57
  import './account-chips.js';
59
58
  import '../lib/format.js';
60
- import '../data-table-3mi2R3so.js';
61
- import './data-table-sort.js';
62
- import './swipeable-row.js';
63
59
  import './reauth-dialog.js';
64
60
  import './server-wake.js';
65
61
  import '../lib/server-wake.js';
66
62
  import './translation-review-labels.js';
67
63
  import '../lib/translation-review.js';
64
+ import './country-select.js';
65
+ import './inline-edit-field.js';
66
+ import '../lib/iban.js';
67
+ import '../lib/phone.js';
68
+ import './sign-chip.js';
69
+ import './column-mapper.js';
70
+ import '../lib/column-mapping.js';
71
+ import '../lib/table-text.js';
72
+ import './field-parts.js';
68
73
  import './numpad-sheet.js';
69
74
 
70
75
  type NumberInputProps = ComponentProps<typeof NumberInput>;
71
- interface NumberFieldProps extends Omit<NumberInputProps, "value" | "onChange" | "onCommit" | "suffix" | "invalid"> {
76
+ interface NumberFieldProps extends Omit<NumberInputProps, "value" | "onChange" | "onCommit" | "suffix" | "invalid" | "error" | "locale"> {
72
77
  /** The committed number. `null` is "no value" — shown as an empty field. */
73
78
  value: number | null;
74
79
  /**
@@ -168,10 +173,20 @@ interface NumberFieldProps extends Omit<NumberInputProps, "value" | "onChange" |
168
173
  *
169
174
  * LOCALE. The decimal mark is the locale's: a German user sees and types "1,5".
170
175
  * NumberInput normalises every keystroke to a dot (so its evaluator reads one
171
- * alphabet); this field maps the dot back before the draft is shown, which also
172
- * means the numpad's "." key types the mark the field is displaying. There is no
173
- * digit grouping — "1.000" would read as a thousand in German and as one in English,
174
- * and a field that parses its own display back must not have that ambiguity.
176
+ * alphabet); this field maps the dot back before the draft is shown. The field never
177
+ * WRITES digit grouping — "1.000" would read as a thousand in German and as one in
178
+ * English, and a field that parses its own display back must not have that ambiguity —
179
+ * but since 0.22 it READS it, through NumberInput (keksdose K5): "1.234,56" typed by a
180
+ * German user commits 1234.56 rather than 1.23456, and "1,234.56" by an English one
181
+ * likewise. While a figure is typed the typist's own text stays on screen ("1.234,5",
182
+ * or a dot typed as the decimal in a comma locale); it is re-spelt in the locale's mark
183
+ * when it commits.
184
+ *
185
+ * NOT FOR MONEY. `digits={2}` rounds, but a currency is more than two decimals: JPY has
186
+ * none, a unit price has four, and the chip, the minor-unit default and `min`/`max` on
187
+ * the SIGNED figure are {@link MoneyField}'s (number value) and {@link AmountInput}'s
188
+ * (string value). A NumberField whose `unit` is a currency says so on the console in
189
+ * development — see NumberInput's money guard.
175
190
  */
176
191
  declare function NumberField(props: NumberFieldProps): react.JSX.Element;
177
192
  /**
@@ -3,15 +3,8 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useId, useState } from "react";
4
4
  import { NumberInput } from "./number-input.js";
5
5
  import { evaluateExpression, formatResult } from "../lib/calc.js";
6
+ import { decimalMark } from "../lib/decimal-marks.js";
6
7
  import { useKitLocale } from "../i18n/kit-labels.js";
7
- function decimalMark(locale) {
8
- try {
9
- const part = new Intl.NumberFormat(locale).formatToParts(1.5).find((p) => p.type === "decimal");
10
- return part?.value === "," ? "," : ".";
11
- } catch {
12
- return ".";
13
- }
14
- }
15
8
  function parseLive(text) {
16
9
  const t = text.trim().replace(/,/g, ".");
17
10
  if (t === "") return null;
@@ -28,7 +21,6 @@ function roundTo(n, digits) {
28
21
  const rounded = Number(n.toFixed(d));
29
22
  return Number.isFinite(rounded) ? rounded : n;
30
23
  }
31
- const FIELD_ERROR_CLASS = "mt-1 text-[11px] leading-tight text-[var(--danger)]";
32
24
  function NumberField(props) {
33
25
  const {
34
26
  value,
@@ -103,9 +95,6 @@ function NumberField(props) {
103
95
  };
104
96
  const generatedId = useId();
105
97
  const fieldId = id ?? generatedId;
106
- const errorId = useId();
107
- const hasError = error !== void 0 && error !== null && error !== false && error !== "";
108
- const describedBy = [ariaDescribedBy, hasError ? errorId : void 0].filter(Boolean).join(" ");
109
98
  const required = ariaRequired === true || ariaRequired === "true";
110
99
  const labelWithUnit = unit !== void 0 && unitPlacement === "label" ? label === void 0 || label === "" ? unit : typeof label === "string" && typeof unit === "string" ? `${label} (${unit})` : /* @__PURE__ */ jsxs(Fragment, { children: [
111
100
  label,
@@ -113,12 +102,12 @@ function NumberField(props) {
113
102
  unit,
114
103
  ")"
115
104
  ] }) : label;
116
- const field = /* @__PURE__ */ jsx(
105
+ return /* @__PURE__ */ jsx(
117
106
  NumberInput,
118
107
  {
119
108
  ...rest,
120
109
  id: fieldId,
121
- "aria-describedby": describedBy || void 0,
110
+ "aria-describedby": ariaDescribedBy,
122
111
  "aria-required": required || void 0,
123
112
  label: labelWithUnit,
124
113
  value: draft,
@@ -126,15 +115,12 @@ function NumberField(props) {
126
115
  onCommit: commit,
127
116
  min,
128
117
  max,
118
+ locale,
129
119
  suffix: unitPlacement === "suffix" ? unit : void 0,
130
- invalid: invalid || hasError || ariaInvalid === true || ariaInvalid === "true"
120
+ invalid: invalid || ariaInvalid === true || ariaInvalid === "true",
121
+ ..."error" in props ? { error } : {}
131
122
  }
132
123
  );
133
- if (!hasError && !("error" in props)) return field;
134
- return /* @__PURE__ */ jsxs("div", { children: [
135
- field,
136
- hasError && /* @__PURE__ */ jsx("p", { id: errorId, className: FIELD_ERROR_CLASS, children: error })
137
- ] });
138
124
  }
139
125
  function IntegerField(props) {
140
126
  return /* @__PURE__ */ jsx(NumberField, { ...props, digits: props.digits ?? 0, calculator: props.calculator ?? false });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/number-field.tsx"],"sourcesContent":["import { useId, useState } from \"react\";\nimport type { ComponentProps, ReactNode } from \"react\";\nimport { NumberInput } from \"./number-input\";\nimport { evaluateExpression, formatResult } from \"../lib/calc\";\nimport { useKitLocale } from \"../i18n/kit-labels\";\n\ntype NumberInputProps = ComponentProps<typeof NumberInput>;\n\nexport interface NumberFieldProps\n extends Omit<NumberInputProps, \"value\" | \"onChange\" | \"onCommit\" | \"suffix\" | \"invalid\"> {\n /** The committed number. `null` is \"no value\" — shown as an empty field. */\n value: number | null;\n /**\n * The parsed number, on blur or Enter — never per keystroke — and only when it\n * differs from the last one committed. Already rounded to `digits` and clamped\n * into `[min, max]`. `null` only when `nullable` and the field was emptied.\n */\n onCommit: (value: number | null) => void;\n /**\n * LIVE MODE: the number the text reads as, on every keystroke (and every step\n * key) — for a calculator that recomputes as the user types, while `onCommit`\n * stays the one to save on.\n *\n * What it is NOT, until the commit: rounded to `digits` or clamped into\n * `[min, max]` — a half-typed \"1\" on the way to \"150\" must not read as the\n * minimum of 100. A calculation reads as its result (\"12+5\" → 17); an empty field\n * as `null` (whether or not `nullable`); a draft that is no number yet (\"-\", \"12+\")\n * fires nothing, so the last number stands. On blur/Enter it fires once more with\n * the committed value if that differs from the last one it reported — the rounded,\n * clamped or snapped-back number — so a live consumer always ends where `onCommit`\n * does.\n *\n * Feeding it back into `value` is safe: a `value` equal to the number this field\n * last reported does not re-render the text, so \"1,\" stays \"1,\" while the rest of\n * the form already shows 1. Not called when `value` changes from outside.\n */\n onValueChange?: (value: number | null) => void;\n /** Round to this many decimals on commit, and show at most this many (trailing\n * zeros are trimmed: `digits={2}` shows 12.5, not 12.50). Omit for no rounding. */\n digits?: number;\n /** Clamp the committed number into `[min, max]`. Applied after rounding. */\n min?: number;\n max?: number;\n /** An emptied field commits `null`. Without it an empty field is not an answer\n * and snaps back to the last value, like any other unparsable text. */\n nullable?: boolean;\n /** The unit — \"mm\", \"%\", \"km/h\". */\n unit?: ReactNode;\n /** Where `unit` goes: `\"suffix\"` (default) is NumberInput's read-out at the\n * field's end edge; `\"label\"` appends it to the label as \"Label (unit)\", for rows\n * of narrow fields where a suffix would eat the digits. */\n unitPlacement?: \"suffix\" | \"label\";\n /** BCP 47 tag for the decimal mark, else the `<UiKitProvider locale>`, else the\n * runtime's. */\n locale?: string;\n /** See {@link Input}'s `invalid`. */\n invalid?: boolean;\n /** See {@link Input}'s `error`: shown under the field, attached via\n * `aria-describedby`, implies `invalid`. */\n error?: ReactNode;\n /** Merged with the error's id. Accepted so a form library's control slot (which\n * injects `id` / `aria-describedby` / `aria-invalid`) can wrap this directly. */\n \"aria-describedby\"?: string;\n \"aria-invalid\"?: boolean | \"true\" | \"false\";\n /**\n * Set on the `<input>`. Declared, with `id` (from NumberInput) and the two above, so\n * `Field`'s render-prop spread — `{(ids) => <NumberField {...ids} … />}`, kastlan's\n * wizard steps — is typed and reaches the input rather than being dropped: before\n * this, `aria-required` was not in the type and a required number was announced as\n * optional.\n */\n \"aria-required\"?: boolean | \"true\" | \"false\";\n}\n\n/** The locale's decimal mark, as far as this field can type it: \",\" or \".\". A mark\n * outside those two (Arabic's \"٫\") is not something {@link sanitizeLive} lets\n * through, so those locales keep the dot rather than get a field that eats keys. */\nfunction decimalMark(locale: string | undefined): \",\" | \".\" {\n try {\n const part = new Intl.NumberFormat(locale).formatToParts(1.5).find((p) => p.type === \"decimal\");\n return part?.value === \",\" ? \",\" : \".\";\n } catch {\n // An invalid tag throws a RangeError; a typo in a locale prop must not take the\n // form down with it.\n return \".\";\n }\n}\n\n/** What the draft reads as while it is being typed — see `onValueChange`. `undefined`\n * is \"not a number yet\", distinct from `null` (empty). */\nfunction parseLive(text: string): number | null | undefined {\n const t = text.trim().replace(/,/g, \".\");\n if (t === \"\") return null;\n if (/^-?\\d*\\.?\\d*$/.test(t)) {\n // \"-\", \".\" and \"-.\" pass the pattern and are no number yet.\n if (!/\\d/.test(t)) return undefined;\n const n = Number(t);\n return Number.isFinite(n) ? n : undefined;\n }\n return evaluateExpression(t) ?? undefined;\n}\n\n/** `toFixed` rounding rather than `Math.round(n * 10^d)`, which turns 1.005 into\n * 1.00 by way of 100.49999999999999 — the same choice the lenkbank field made. */\nfunction roundTo(n: number, digits: number | undefined): number {\n if (digits === undefined) return n;\n const d = Math.min(Math.max(Math.trunc(digits), 0), 100);\n const rounded = Number(n.toFixed(d));\n return Number.isFinite(rounded) ? rounded : n;\n}\n\n// The error line under a field — the same type as `ui.tsx`'s (module-private) one,\n// so a NumberField's message is indistinguishable from an Input's.\nconst FIELD_ERROR_CLASS = \"mt-1 text-[11px] leading-tight text-[var(--danger)]\";\n\n/**\n * A number field whose value IS a number: {@link NumberInput}'s text, calculator and\n * numpad, with the parse/commit/revert loop every consumer was writing on top of it.\n *\n * WHY THE DRAFT. Half-typed values — \"-\", \"1,\", \"12+\" — are not numbers, and\n * reformatting them mid-keystroke moves the caret and fights the typist; committing\n * per keystroke would also fire whatever a commit fires (a recalculation, a save) for\n * every meaningless intermediate. So the field keeps a draft STRING while it is being\n * typed and resolves it on blur/Enter:\n *\n * - a number (or a calculation, \"2700+50\") → rounded to `digits`, clamped into\n * `[min, max]`, committed, and the draft re-rendered from the result;\n * - empty → `null` when `nullable`, otherwise the same as the next case — `Number(\"\")`\n * is 0, and clearing a wheelbase and tabbing away must not set it to zero;\n * - anything else unparsable → the draft SNAPS BACK to the last committed value\n * rather than sending NaN on for a server to reject.\n *\n * The draft follows `value` when it changes from outside (a reset, a loaded record);\n * a change that came from here arrives back identical and is a no-op.\n *\n * LIVE MODE. A calculator that recomputes per keystroke used to have to pick: this\n * field (limits, rounding, but a number only on blur) or a bare NumberInput (every\n * keystroke, but a string and no limits). `onValueChange` is the per-keystroke\n * channel here, next to the commit rather than instead of it: live numbers for the\n * read-out, `onCommit` for the save. It lives on this field rather than as\n * `min`/`max`/`digits` on NumberInput because parsing, the locale's mark and the\n * limits are what this field already is — a second copy on the string field would be\n * a second, subtly different NumberField.\n *\n * STEPS. `step` (with `min`/`max` as bounds) turns on ArrowUp/ArrowDown and PageUp/\n * PageDown (×10) — see NumberInput's `step`. A step is a draft change like a\n * keystroke: `onValueChange` hears it at once, `onCommit` on blur/Enter. Pick a\n * `step` that `digits` can show (0.25 needs `digits={2}`), or the commit rounds it.\n *\n * LOCALE. The decimal mark is the locale's: a German user sees and types \"1,5\".\n * NumberInput normalises every keystroke to a dot (so its evaluator reads one\n * alphabet); this field maps the dot back before the draft is shown, which also\n * means the numpad's \".\" key types the mark the field is displaying. There is no\n * digit grouping — \"1.000\" would read as a thousand in German and as one in English,\n * and a field that parses its own display back must not have that ambiguity.\n */\nexport function NumberField(props: NumberFieldProps) {\n const {\n value,\n onCommit,\n onValueChange,\n digits,\n min,\n max,\n nullable = false,\n unit,\n unitPlacement = \"suffix\",\n locale: localeProp,\n label,\n invalid,\n error,\n id,\n \"aria-describedby\": ariaDescribedBy,\n \"aria-invalid\": ariaInvalid,\n \"aria-required\": ariaRequired,\n ...rest\n } = props;\n const locale = useKitLocale(localeProp);\n const mark = decimalMark(locale);\n const localize = (text: string) => (mark === \",\" ? text.replace(/\\./g, \",\") : text);\n // `formatResult` for fixed notation: `String(1e-7)` is \"1e-7\", which the field\n // would read back as 17 (see calc.ts).\n const format = (n: number | null) =>\n n === null || !Number.isFinite(n) ? \"\" : localize(formatResult(roundTo(n, digits)));\n\n const [draft, setDraft] = useState(() => format(value));\n // Everything the displayed text depends on, not just `value`: switching the\n // provider's language or a field's `digits` has to re-render the draft too.\n const [seen, setSeen] = useState({ value, mark, digits });\n // The last number handed to `onCommit`, so Enter-then-blur (both commit) and a\n // parent that does not echo the value back cannot commit the same number twice.\n // State rather than a ref because the external-change branch below resets it\n // during render, and a ref may not be written there.\n const [committed, setCommitted] = useState(value);\n // The last number handed to `onValueChange` — state for the same reason.\n const [reported, setReported] = useState(value);\n\n // Adjust state during render rather than in an effect: React re-renders at once\n // with the new draft, before anything is painted, so there is no frame showing\n // the old text.\n if (seen.value !== value || seen.mark !== mark || seen.digits !== digits) {\n setSeen({ value, mark, digits });\n // Live mode echoing back what it was just told: the draft already says it, in\n // the typist's own spelling (\"1,\" or \"12+5\"), and reformatting would fight them.\n // Only a changed `value` qualifies — a new mark or `digits` still re-renders.\n const echo =\n onValueChange !== undefined && value === reported && seen.mark === mark && seen.digits === digits;\n if (!echo) {\n setDraft(format(value));\n setCommitted(value);\n setReported(value);\n }\n }\n\n const report = (next: number | null) => {\n if (next === reported) return;\n setReported(next);\n onValueChange?.(next);\n };\n\n const onText = (text: string) => {\n setDraft(localize(text));\n const live = parseLive(text);\n if (live !== undefined) report(live);\n };\n\n const commit = (text: string) => {\n const trimmed = text.trim();\n let next: number | null;\n const snapBack = () => {\n setDraft(format(committed));\n report(committed);\n };\n if (trimmed === \"\") {\n if (!nullable) return snapBack();\n next = null;\n } else {\n // `text` is NumberInput's committed form: an evaluated calculation, dot-decimal.\n // What survives its digits-only fallback can still be \"-\" or \".\" alone.\n const parsed = Number(trimmed.replace(/,/g, \".\"));\n if (!Number.isFinite(parsed)) return snapBack();\n next = roundTo(parsed, digits);\n if (min !== undefined) next = Math.max(min, next);\n if (max !== undefined) next = Math.min(max, next);\n }\n // Always re-render the draft from the result — \"007\" becomes \"7\", a clamped 500\n // shows the 100 it was clamped to — even when there is nothing new to commit.\n setDraft(format(next));\n report(next);\n if (next !== committed) {\n setCommitted(next);\n onCommit(next);\n }\n };\n\n const generatedId = useId();\n const fieldId = id ?? generatedId;\n const errorId = useId();\n // `null`/`false`/`\"\"` are what `touched && errors.x` evaluates to on the happy path.\n const hasError = error !== undefined && error !== null && error !== false && error !== \"\";\n const describedBy = [ariaDescribedBy, hasError ? errorId : undefined].filter(Boolean).join(\" \");\n\n // Straight onto NumberInput's <input>, which declares both now. They used to be set\n // on the DOM node from an effect, because NumberInput had no props for them.\n const required = ariaRequired === true || ariaRequired === \"true\";\n\n const labelWithUnit =\n unit !== undefined && unitPlacement === \"label\"\n ? label === undefined || label === \"\"\n ? unit\n : typeof label === \"string\" && typeof unit === \"string\"\n ? `${label} (${unit})`\n : (\n <>\n {label} ({unit})\n </>\n )\n : label;\n\n const field = (\n <NumberInput\n {...rest}\n id={fieldId}\n aria-describedby={describedBy || undefined}\n aria-required={required || undefined}\n label={labelWithUnit}\n value={draft}\n onChange={onText}\n onCommit={commit}\n min={min}\n max={max}\n suffix={unitPlacement === \"suffix\" ? unit : undefined}\n invalid={invalid || hasError || ariaInvalid === true || ariaInvalid === \"true\"}\n />\n );\n // Input's FieldGroup, restated: no wrapper for a field that never takes `error`, so\n // the prop changed nothing about the DOM a flex row lays out — and a STEADY one for a\n // field that does, message or not. Wrapping only while a message showed swapped the\n // element above the <input>, which remounted it: focus, caret and the half-typed\n // draft's keystrokes were lost the moment the message came or went.\n if (!hasError && !(\"error\" in props)) return field;\n return (\n <div>\n {field}\n {hasError && (\n <p id={errorId} className={FIELD_ERROR_CLASS}>\n {error}\n </p>\n )}\n </div>\n );\n}\n\n/**\n * A {@link NumberField} for whole numbers: `digits={0}` and `calculator={false}` as\n * defaults, each overridable — the preset `RhfIntegerField` (`@eifi1/ui-kit/rhf`) applies to a\n * form-bound field, for the plain one (kastlan 51: a building's construction year in a\n * wizard's local state, an entity id in the document upload spelled the two props out\n * by hand).\n *\n * `??` rather than a spread over the defaults, so a wrapper forwarding its own optional\n * `digits={props.digits}` still gets the preset.\n */\nexport function IntegerField(props: NumberFieldProps) {\n return <NumberField {...props} digits={props.digits ?? 0} calculator={props.calculator ?? false} />;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/number-field.tsx"],"sourcesContent":["import { useId, useState } from \"react\";\nimport type { ComponentProps, ReactNode } from \"react\";\nimport { NumberInput } from \"./number-input\";\nimport { evaluateExpression, formatResult } from \"../lib/calc\";\nimport { decimalMark } from \"../lib/decimal-marks\";\nimport { useKitLocale } from \"../i18n/kit-labels\";\n\ntype NumberInputProps = ComponentProps<typeof NumberInput>;\n\nexport interface NumberFieldProps\n extends Omit<NumberInputProps, \"value\" | \"onChange\" | \"onCommit\" | \"suffix\" | \"invalid\" | \"error\" | \"locale\"> {\n /** The committed number. `null` is \"no value\" — shown as an empty field. */\n value: number | null;\n /**\n * The parsed number, on blur or Enter — never per keystroke — and only when it\n * differs from the last one committed. Already rounded to `digits` and clamped\n * into `[min, max]`. `null` only when `nullable` and the field was emptied.\n */\n onCommit: (value: number | null) => void;\n /**\n * LIVE MODE: the number the text reads as, on every keystroke (and every step\n * key) — for a calculator that recomputes as the user types, while `onCommit`\n * stays the one to save on.\n *\n * What it is NOT, until the commit: rounded to `digits` or clamped into\n * `[min, max]` — a half-typed \"1\" on the way to \"150\" must not read as the\n * minimum of 100. A calculation reads as its result (\"12+5\" → 17); an empty field\n * as `null` (whether or not `nullable`); a draft that is no number yet (\"-\", \"12+\")\n * fires nothing, so the last number stands. On blur/Enter it fires once more with\n * the committed value if that differs from the last one it reported — the rounded,\n * clamped or snapped-back number — so a live consumer always ends where `onCommit`\n * does.\n *\n * Feeding it back into `value` is safe: a `value` equal to the number this field\n * last reported does not re-render the text, so \"1,\" stays \"1,\" while the rest of\n * the form already shows 1. Not called when `value` changes from outside.\n */\n onValueChange?: (value: number | null) => void;\n /** Round to this many decimals on commit, and show at most this many (trailing\n * zeros are trimmed: `digits={2}` shows 12.5, not 12.50). Omit for no rounding. */\n digits?: number;\n /** Clamp the committed number into `[min, max]`. Applied after rounding. */\n min?: number;\n max?: number;\n /** An emptied field commits `null`. Without it an empty field is not an answer\n * and snaps back to the last value, like any other unparsable text. */\n nullable?: boolean;\n /** The unit — \"mm\", \"%\", \"km/h\". */\n unit?: ReactNode;\n /** Where `unit` goes: `\"suffix\"` (default) is NumberInput's read-out at the\n * field's end edge; `\"label\"` appends it to the label as \"Label (unit)\", for rows\n * of narrow fields where a suffix would eat the digits. */\n unitPlacement?: \"suffix\" | \"label\";\n /** BCP 47 tag for the decimal mark, else the `<UiKitProvider locale>`, else the\n * runtime's. */\n locale?: string;\n /** See {@link Input}'s `invalid`. */\n invalid?: boolean;\n /** See {@link Input}'s `error`: shown under the field, attached via\n * `aria-describedby`, implies `invalid`. */\n error?: ReactNode;\n /** Merged with the error's id. Accepted so a form library's control slot (which\n * injects `id` / `aria-describedby` / `aria-invalid`) can wrap this directly. */\n \"aria-describedby\"?: string;\n \"aria-invalid\"?: boolean | \"true\" | \"false\";\n /**\n * Set on the `<input>`. Declared, with `id` (from NumberInput) and the two above, so\n * `Field`'s render-prop spread — `{(ids) => <NumberField {...ids} … />}`, kastlan's\n * wizard steps — is typed and reaches the input rather than being dropped: before\n * this, `aria-required` was not in the type and a required number was announced as\n * optional.\n */\n \"aria-required\"?: boolean | \"true\" | \"false\";\n}\n\n/** What the draft reads as while it is being typed — see `onValueChange`. `undefined`\n * is \"not a number yet\", distinct from `null` (empty). */\nfunction parseLive(text: string): number | null | undefined {\n const t = text.trim().replace(/,/g, \".\");\n if (t === \"\") return null;\n if (/^-?\\d*\\.?\\d*$/.test(t)) {\n // \"-\", \".\" and \"-.\" pass the pattern and are no number yet.\n if (!/\\d/.test(t)) return undefined;\n const n = Number(t);\n return Number.isFinite(n) ? n : undefined;\n }\n return evaluateExpression(t) ?? undefined;\n}\n\n/** `toFixed` rounding rather than `Math.round(n * 10^d)`, which turns 1.005 into\n * 1.00 by way of 100.49999999999999 — the same choice the lenkbank field made. */\nfunction roundTo(n: number, digits: number | undefined): number {\n if (digits === undefined) return n;\n const d = Math.min(Math.max(Math.trunc(digits), 0), 100);\n const rounded = Number(n.toFixed(d));\n return Number.isFinite(rounded) ? rounded : n;\n}\n\n/**\n * A number field whose value IS a number: {@link NumberInput}'s text, calculator and\n * numpad, with the parse/commit/revert loop every consumer was writing on top of it.\n *\n * WHY THE DRAFT. Half-typed values — \"-\", \"1,\", \"12+\" — are not numbers, and\n * reformatting them mid-keystroke moves the caret and fights the typist; committing\n * per keystroke would also fire whatever a commit fires (a recalculation, a save) for\n * every meaningless intermediate. So the field keeps a draft STRING while it is being\n * typed and resolves it on blur/Enter:\n *\n * - a number (or a calculation, \"2700+50\") → rounded to `digits`, clamped into\n * `[min, max]`, committed, and the draft re-rendered from the result;\n * - empty → `null` when `nullable`, otherwise the same as the next case — `Number(\"\")`\n * is 0, and clearing a wheelbase and tabbing away must not set it to zero;\n * - anything else unparsable → the draft SNAPS BACK to the last committed value\n * rather than sending NaN on for a server to reject.\n *\n * The draft follows `value` when it changes from outside (a reset, a loaded record);\n * a change that came from here arrives back identical and is a no-op.\n *\n * LIVE MODE. A calculator that recomputes per keystroke used to have to pick: this\n * field (limits, rounding, but a number only on blur) or a bare NumberInput (every\n * keystroke, but a string and no limits). `onValueChange` is the per-keystroke\n * channel here, next to the commit rather than instead of it: live numbers for the\n * read-out, `onCommit` for the save. It lives on this field rather than as\n * `min`/`max`/`digits` on NumberInput because parsing, the locale's mark and the\n * limits are what this field already is — a second copy on the string field would be\n * a second, subtly different NumberField.\n *\n * STEPS. `step` (with `min`/`max` as bounds) turns on ArrowUp/ArrowDown and PageUp/\n * PageDown (×10) — see NumberInput's `step`. A step is a draft change like a\n * keystroke: `onValueChange` hears it at once, `onCommit` on blur/Enter. Pick a\n * `step` that `digits` can show (0.25 needs `digits={2}`), or the commit rounds it.\n *\n * LOCALE. The decimal mark is the locale's: a German user sees and types \"1,5\".\n * NumberInput normalises every keystroke to a dot (so its evaluator reads one\n * alphabet); this field maps the dot back before the draft is shown. The field never\n * WRITES digit grouping — \"1.000\" would read as a thousand in German and as one in\n * English, and a field that parses its own display back must not have that ambiguity —\n * but since 0.22 it READS it, through NumberInput (keksdose K5): \"1.234,56\" typed by a\n * German user commits 1234.56 rather than 1.23456, and \"1,234.56\" by an English one\n * likewise. While a figure is typed the typist's own text stays on screen (\"1.234,5\",\n * or a dot typed as the decimal in a comma locale); it is re-spelt in the locale's mark\n * when it commits.\n *\n * NOT FOR MONEY. `digits={2}` rounds, but a currency is more than two decimals: JPY has\n * none, a unit price has four, and the chip, the minor-unit default and `min`/`max` on\n * the SIGNED figure are {@link MoneyField}'s (number value) and {@link AmountInput}'s\n * (string value). A NumberField whose `unit` is a currency says so on the console in\n * development — see NumberInput's money guard.\n */\nexport function NumberField(props: NumberFieldProps) {\n const {\n value,\n onCommit,\n onValueChange,\n digits,\n min,\n max,\n nullable = false,\n unit,\n unitPlacement = \"suffix\",\n locale: localeProp,\n label,\n invalid,\n error,\n id,\n \"aria-describedby\": ariaDescribedBy,\n \"aria-invalid\": ariaInvalid,\n \"aria-required\": ariaRequired,\n ...rest\n } = props;\n const locale = useKitLocale(localeProp);\n const mark = decimalMark(locale);\n const localize = (text: string) => (mark === \",\" ? text.replace(/\\./g, \",\") : text);\n // `formatResult` for fixed notation: `String(1e-7)` is \"1e-7\", which the field\n // would read back as 17 (see calc.ts).\n const format = (n: number | null) =>\n n === null || !Number.isFinite(n) ? \"\" : localize(formatResult(roundTo(n, digits)));\n\n const [draft, setDraft] = useState(() => format(value));\n // Everything the displayed text depends on, not just `value`: switching the\n // provider's language or a field's `digits` has to re-render the draft too.\n const [seen, setSeen] = useState({ value, mark, digits });\n // The last number handed to `onCommit`, so Enter-then-blur (both commit) and a\n // parent that does not echo the value back cannot commit the same number twice.\n // State rather than a ref because the external-change branch below resets it\n // during render, and a ref may not be written there.\n const [committed, setCommitted] = useState(value);\n // The last number handed to `onValueChange` — state for the same reason.\n const [reported, setReported] = useState(value);\n\n // Adjust state during render rather than in an effect: React re-renders at once\n // with the new draft, before anything is painted, so there is no frame showing\n // the old text.\n if (seen.value !== value || seen.mark !== mark || seen.digits !== digits) {\n setSeen({ value, mark, digits });\n // Live mode echoing back what it was just told: the draft already says it, in\n // the typist's own spelling (\"1,\" or \"12+5\"), and reformatting would fight them.\n // Only a changed `value` qualifies — a new mark or `digits` still re-renders.\n const echo =\n onValueChange !== undefined && value === reported && seen.mark === mark && seen.digits === digits;\n if (!echo) {\n setDraft(format(value));\n setCommitted(value);\n setReported(value);\n }\n }\n\n const report = (next: number | null) => {\n if (next === reported) return;\n setReported(next);\n onValueChange?.(next);\n };\n\n const onText = (text: string) => {\n setDraft(localize(text));\n const live = parseLive(text);\n if (live !== undefined) report(live);\n };\n\n const commit = (text: string) => {\n const trimmed = text.trim();\n let next: number | null;\n const snapBack = () => {\n setDraft(format(committed));\n report(committed);\n };\n if (trimmed === \"\") {\n if (!nullable) return snapBack();\n next = null;\n } else {\n // `text` is NumberInput's committed form: an evaluated calculation, dot-decimal.\n // What survives its digits-only fallback can still be \"-\" or \".\" alone.\n const parsed = Number(trimmed.replace(/,/g, \".\"));\n if (!Number.isFinite(parsed)) return snapBack();\n next = roundTo(parsed, digits);\n if (min !== undefined) next = Math.max(min, next);\n if (max !== undefined) next = Math.min(max, next);\n }\n // Always re-render the draft from the result — \"007\" becomes \"7\", a clamped 500\n // shows the 100 it was clamped to — even when there is nothing new to commit.\n setDraft(format(next));\n report(next);\n if (next !== committed) {\n setCommitted(next);\n onCommit(next);\n }\n };\n\n const generatedId = useId();\n const fieldId = id ?? generatedId;\n\n // Straight onto NumberInput's <input>, which declares both now. They used to be set\n // on the DOM node from an effect, because NumberInput had no props for them.\n const required = ariaRequired === true || ariaRequired === \"true\";\n\n const labelWithUnit =\n unit !== undefined && unitPlacement === \"label\"\n ? label === undefined || label === \"\"\n ? unit\n : typeof label === \"string\" && typeof unit === \"string\"\n ? `${label} (${unit})`\n : (\n <>\n {label} ({unit})\n </>\n )\n : label;\n\n return (\n <NumberInput\n {...rest}\n id={fieldId}\n aria-describedby={ariaDescribedBy}\n aria-required={required || undefined}\n label={labelWithUnit}\n value={draft}\n onChange={onText}\n onCommit={commit}\n min={min}\n max={max}\n // The mark this field spells its draft in, so the two can never disagree when\n // this field's `locale` is not the provider's.\n locale={locale}\n suffix={unitPlacement === \"suffix\" ? unit : undefined}\n invalid={invalid || ariaInvalid === true || ariaInvalid === \"true\"}\n // NumberInput's `error` — the message, the description, the paint, and Input's\n // steady box: no wrapper for a field that never takes `error`, and the SAME one\n // for a field that does, message or not, so the message coming or going never\n // remounts the <input> mid-keystroke. Only forwarded when it was passed, since\n // passing the key is what asks for the box.\n {...(\"error\" in props ? { error } : {})}\n />\n );\n}\n\n/**\n * A {@link NumberField} for whole numbers: `digits={0}` and `calculator={false}` as\n * defaults, each overridable — the preset `RhfIntegerField` (`@eifi1/ui-kit/rhf`) applies to a\n * form-bound field, for the plain one (kastlan 51: a building's construction year in a\n * wizard's local state, an entity id in the document upload spelled the two props out\n * by hand).\n *\n * `??` rather than a spread over the defaults, so a wrapper forwarding its own optional\n * `digits={props.digits}` still gets the preset.\n */\nexport function IntegerField(props: NumberFieldProps) {\n return <NumberField {...props} digits={props.digits ?? 0} calculator={props.calculator ?? false} />;\n}\n"],"mappings":"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","names":[]}