@eifi1/ui-kit 0.4.1 → 0.5.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 (410) hide show
  1. package/LICENSE +32 -0
  2. package/README.md +498 -57
  3. package/dist/chart.d.ts +10 -0
  4. package/dist/chart.js +10 -0
  5. package/dist/chart.js.map +1 -0
  6. package/dist/components/account-settings.d.ts +11 -0
  7. package/dist/components/account-settings.js +19 -12
  8. package/dist/components/account-settings.js.map +1 -1
  9. package/dist/components/alert-banner.d.ts +10 -6
  10. package/dist/components/alert-banner.js +20 -16
  11. package/dist/components/alert-banner.js.map +1 -1
  12. package/dist/components/amount-input.d.ts +30 -4
  13. package/dist/components/amount-input.js +36 -14
  14. package/dist/components/amount-input.js.map +1 -1
  15. package/dist/components/calculator.d.ts +24 -7
  16. package/dist/components/calculator.js +37 -20
  17. package/dist/components/calculator.js.map +1 -1
  18. package/dist/components/chart-zoom.d.ts +156 -0
  19. package/dist/components/chart-zoom.js +262 -0
  20. package/dist/components/chart-zoom.js.map +1 -0
  21. package/dist/components/chart.d.ts +28 -3
  22. package/dist/components/chart.js +71 -32
  23. package/dist/components/chart.js.map +1 -1
  24. package/dist/components/checkbox.d.ts +51 -0
  25. package/dist/components/checkbox.js +123 -0
  26. package/dist/components/checkbox.js.map +1 -0
  27. package/dist/components/chip.d.ts +128 -0
  28. package/dist/components/chip.js +357 -0
  29. package/dist/components/chip.js.map +1 -0
  30. package/dist/components/choice-parts.d.ts +24 -0
  31. package/dist/components/choice-parts.js +18 -0
  32. package/dist/components/choice-parts.js.map +1 -0
  33. package/dist/components/combobox-core.d.ts +52 -9
  34. package/dist/components/combobox-core.js +89 -19
  35. package/dist/components/combobox-core.js.map +1 -1
  36. package/dist/components/combobox.d.ts +43 -33
  37. package/dist/components/combobox.js +409 -340
  38. package/dist/components/combobox.js.map +1 -1
  39. package/dist/components/currency-select.d.ts +48 -8
  40. package/dist/components/currency-select.js +107 -65
  41. package/dist/components/currency-select.js.map +1 -1
  42. package/dist/components/data-table-filter-popover.d.ts +15 -3
  43. package/dist/components/data-table-filter-popover.js +23 -17
  44. package/dist/components/data-table-filter-popover.js.map +1 -1
  45. package/dist/components/data-table-filters.d.ts +1 -1
  46. package/dist/components/data-table-filters.js +3 -2
  47. package/dist/components/data-table-filters.js.map +1 -1
  48. package/dist/components/data-table-labels.d.ts +53 -2
  49. package/dist/components/data-table-labels.js +31 -2
  50. package/dist/components/data-table-labels.js.map +1 -1
  51. package/dist/components/data-table-pagination.d.ts +3 -1
  52. package/dist/components/data-table-pagination.js +16 -10
  53. package/dist/components/data-table-pagination.js.map +1 -1
  54. package/dist/components/data-table-sort.js +1 -0
  55. package/dist/components/data-table-sort.js.map +1 -1
  56. package/dist/components/data-table.d.ts +1 -1
  57. package/dist/components/data-table.js +201 -85
  58. package/dist/components/data-table.js.map +1 -1
  59. package/dist/components/date-picker.d.ts +25 -9
  60. package/dist/components/date-picker.js +232 -94
  61. package/dist/components/date-picker.js.map +1 -1
  62. package/dist/components/dropdown.d.ts +74 -19
  63. package/dist/components/dropdown.js +72 -22
  64. package/dist/components/dropdown.js.map +1 -1
  65. package/dist/components/entity-combobox.d.ts +5 -4
  66. package/dist/components/entity-combobox.js +107 -76
  67. package/dist/components/entity-combobox.js.map +1 -1
  68. package/dist/components/facing-pair.d.ts +36 -0
  69. package/dist/components/facing-pair.js +40 -0
  70. package/dist/components/facing-pair.js.map +1 -0
  71. package/dist/components/field-sync.d.ts +164 -0
  72. package/dist/components/field-sync.js +264 -0
  73. package/dist/components/field-sync.js.map +1 -0
  74. package/dist/components/file-dropzone.d.ts +21 -7
  75. package/dist/components/file-dropzone.js +28 -7
  76. package/dist/components/file-dropzone.js.map +1 -1
  77. package/dist/components/full-bleed-dialog.d.ts +37 -26
  78. package/dist/components/full-bleed-dialog.js +20 -5
  79. package/dist/components/full-bleed-dialog.js.map +1 -1
  80. package/dist/components/grouped-picker.d.ts +21 -10
  81. package/dist/components/grouped-picker.js +66 -52
  82. package/dist/components/grouped-picker.js.map +1 -1
  83. package/dist/components/hover-menu.d.ts +20 -4
  84. package/dist/components/hover-menu.js +15 -4
  85. package/dist/components/hover-menu.js.map +1 -1
  86. package/dist/components/mini-calendar.d.ts +102 -14
  87. package/dist/components/mini-calendar.js +256 -70
  88. package/dist/components/mini-calendar.js.map +1 -1
  89. package/dist/components/modal.d.ts +9 -3
  90. package/dist/components/modal.js +34 -45
  91. package/dist/components/modal.js.map +1 -1
  92. package/dist/components/month-picker.d.ts +79 -0
  93. package/dist/components/month-picker.js +342 -0
  94. package/dist/components/month-picker.js.map +1 -0
  95. package/dist/components/multi-entity-combobox.d.ts +7 -6
  96. package/dist/components/multi-entity-combobox.js +112 -72
  97. package/dist/components/multi-entity-combobox.js.map +1 -1
  98. package/dist/components/multi-select.d.ts +13 -11
  99. package/dist/components/multi-select.js +161 -84
  100. package/dist/components/multi-select.js.map +1 -1
  101. package/dist/components/number-field.d.ts +92 -0
  102. package/dist/components/number-field.js +111 -0
  103. package/dist/components/number-field.js.map +1 -0
  104. package/dist/components/number-input.d.ts +21 -2
  105. package/dist/components/number-input.js +2 -1
  106. package/dist/components/number-input.js.map +1 -1
  107. package/dist/components/numpad-sheet.d.ts +25 -4
  108. package/dist/components/numpad-sheet.js +30 -13
  109. package/dist/components/numpad-sheet.js.map +1 -1
  110. package/dist/components/page-contents.d.ts +90 -0
  111. package/dist/components/page-contents.js +170 -0
  112. package/dist/components/page-contents.js.map +1 -0
  113. package/dist/components/password-strength.d.ts +106 -0
  114. package/dist/components/password-strength.js +132 -0
  115. package/dist/components/password-strength.js.map +1 -0
  116. package/dist/components/picker-sheet.d.ts +23 -15
  117. package/dist/components/picker-sheet.js +34 -14
  118. package/dist/components/picker-sheet.js.map +1 -1
  119. package/dist/components/popover.d.ts +62 -4
  120. package/dist/components/popover.js +52 -2
  121. package/dist/components/popover.js.map +1 -1
  122. package/dist/components/search-field.d.ts +25 -7
  123. package/dist/components/search-field.js +7 -5
  124. package/dist/components/search-field.js.map +1 -1
  125. package/dist/components/series-chart-labels.d.ts +14 -0
  126. package/dist/components/series-chart-labels.js +11 -0
  127. package/dist/components/series-chart-labels.js.map +1 -0
  128. package/dist/components/series-chart.d.ts +207 -0
  129. package/dist/components/series-chart.js +223 -0
  130. package/dist/components/series-chart.js.map +1 -0
  131. package/dist/components/settings-fields.d.ts +14 -4
  132. package/dist/components/settings-fields.js +19 -8
  133. package/dist/components/settings-fields.js.map +1 -1
  134. package/dist/components/signature-pad.d.ts +119 -0
  135. package/dist/components/signature-pad.js +405 -0
  136. package/dist/components/signature-pad.js.map +1 -0
  137. package/dist/components/slider.d.ts +80 -0
  138. package/dist/components/slider.js +155 -0
  139. package/dist/components/slider.js.map +1 -0
  140. package/dist/components/sparkline.d.ts +93 -0
  141. package/dist/components/sparkline.js +205 -0
  142. package/dist/components/sparkline.js.map +1 -0
  143. package/dist/components/stat-tile.d.ts +151 -0
  144. package/dist/components/stat-tile.js +236 -0
  145. package/dist/components/stat-tile.js.map +1 -0
  146. package/dist/components/swipeable-row.d.ts +25 -2
  147. package/dist/components/swipeable-row.js +31 -0
  148. package/dist/components/swipeable-row.js.map +1 -1
  149. package/dist/components/switch.d.ts +60 -0
  150. package/dist/components/switch.js +81 -0
  151. package/dist/components/switch.js.map +1 -0
  152. package/dist/components/time-input.d.ts +58 -0
  153. package/dist/components/time-input.js +50 -0
  154. package/dist/components/time-input.js.map +1 -0
  155. package/dist/components/toggle-group.d.ts +18 -3
  156. package/dist/components/toggle-group.js +10 -6
  157. package/dist/components/toggle-group.js.map +1 -1
  158. package/dist/components/toggle-legend.d.ts +115 -0
  159. package/dist/components/toggle-legend.js +114 -0
  160. package/dist/components/toggle-legend.js.map +1 -0
  161. package/dist/components/tooltip.d.ts +40 -11
  162. package/dist/components/tooltip.js +77 -20
  163. package/dist/components/tooltip.js.map +1 -1
  164. package/dist/components/treemap.d.ts +132 -0
  165. package/dist/components/treemap.js +179 -0
  166. package/dist/components/treemap.js.map +1 -0
  167. package/dist/components/ui.d.ts +159 -46
  168. package/dist/components/ui.js +220 -115
  169. package/dist/components/ui.js.map +1 -1
  170. package/dist/components/use-mobile-reveal.js +1 -0
  171. package/dist/components/use-mobile-reveal.js.map +1 -1
  172. package/dist/components/use-table-state.d.ts +1 -1
  173. package/dist/components/use-table-state.js +38 -17
  174. package/dist/components/use-table-state.js.map +1 -1
  175. package/dist/components/user-avatar.d.ts +5 -3
  176. package/dist/components/user-avatar.js +4 -2
  177. package/dist/components/user-avatar.js.map +1 -1
  178. package/dist/components/wizard-stepper.js +3 -2
  179. package/dist/components/wizard-stepper.js.map +1 -1
  180. package/dist/{data-table-filters-aXyf0Xub.d.ts → data-table-filters-DM1auUfH.d.ts} +27 -5
  181. package/dist/data-table.d.ts +7 -0
  182. package/dist/data-table.js +12 -0
  183. package/dist/data-table.js.map +1 -0
  184. package/dist/feedback/feedback-attachment.js +12 -11
  185. package/dist/feedback/feedback-attachment.js.map +1 -1
  186. package/dist/feedback/feedback-dialog.js +2 -1
  187. package/dist/feedback/feedback-dialog.js.map +1 -1
  188. package/dist/feedback/feedback-inbox.d.ts +33 -7
  189. package/dist/feedback/feedback-inbox.js +49 -37
  190. package/dist/feedback/feedback-inbox.js.map +1 -1
  191. package/dist/feedback.d.ts +5 -0
  192. package/dist/feedback.js +5 -0
  193. package/dist/feedback.js.map +1 -0
  194. package/dist/hooks/use-anchored-panel.js +1 -0
  195. package/dist/hooks/use-anchored-panel.js.map +1 -1
  196. package/dist/hooks/use-anchored-rect.js +1 -0
  197. package/dist/hooks/use-anchored-rect.js.map +1 -1
  198. package/dist/hooks/use-announce.d.ts +46 -0
  199. package/dist/hooks/use-announce.js +37 -0
  200. package/dist/hooks/use-announce.js.map +1 -0
  201. package/dist/hooks/use-body-scroll-lock.js +1 -0
  202. package/dist/hooks/use-body-scroll-lock.js.map +1 -1
  203. package/dist/hooks/use-close-transition.js +1 -0
  204. package/dist/hooks/use-close-transition.js.map +1 -1
  205. package/dist/hooks/use-dismiss.js +1 -0
  206. package/dist/hooks/use-dismiss.js.map +1 -1
  207. package/dist/hooks/use-focus-trap.d.ts +16 -0
  208. package/dist/hooks/use-focus-trap.js +67 -0
  209. package/dist/hooks/use-focus-trap.js.map +1 -0
  210. package/dist/hooks/use-media-query.js +1 -0
  211. package/dist/hooks/use-media-query.js.map +1 -1
  212. package/dist/hooks/use-overlay-history.js +27 -1
  213. package/dist/hooks/use-overlay-history.js.map +1 -1
  214. package/dist/hooks/use-row-swipe.js +1 -0
  215. package/dist/hooks/use-row-swipe.js.map +1 -1
  216. package/dist/i18n/defaults.d.ts +31 -0
  217. package/dist/i18n/defaults.js +63 -0
  218. package/dist/i18n/defaults.js.map +1 -0
  219. package/dist/i18n/kit-labels.d.ts +250 -0
  220. package/dist/i18n/kit-labels.js +184 -0
  221. package/dist/i18n/kit-labels.js.map +1 -0
  222. package/dist/index.d.ts +47 -22
  223. package/dist/index.js +28 -0
  224. package/dist/index.js.map +1 -1
  225. package/dist/lib/calc.js +1 -0
  226. package/dist/lib/calc.js.map +1 -1
  227. package/dist/lib/cn.js +1 -0
  228. package/dist/lib/cn.js.map +1 -1
  229. package/dist/lib/dates.js +1 -0
  230. package/dist/lib/dates.js.map +1 -1
  231. package/dist/lib/logger.js +1 -0
  232. package/dist/lib/logger.js.map +1 -1
  233. package/dist/lib/safe-storage.js +1 -0
  234. package/dist/lib/safe-storage.js.map +1 -1
  235. package/dist/search/command-palette.d.ts +11 -1
  236. package/dist/search/command-palette.js +21 -15
  237. package/dist/search/command-palette.js.map +1 -1
  238. package/dist/search.d.ts +2 -0
  239. package/dist/search.js +3 -0
  240. package/dist/search.js.map +1 -0
  241. package/dist/shell/app-shell.d.ts +33 -4
  242. package/dist/shell/app-shell.js +275 -78
  243. package/dist/shell/app-shell.js.map +1 -1
  244. package/dist/shell/option-switcher-menu.js +3 -2
  245. package/dist/shell/option-switcher-menu.js.map +1 -1
  246. package/dist/shell/role-switcher.d.ts +6 -0
  247. package/dist/shell/role-switcher.js +7 -4
  248. package/dist/shell/role-switcher.js.map +1 -1
  249. package/dist/shell/top-bar.d.ts +15 -7
  250. package/dist/shell/top-bar.js +3 -5
  251. package/dist/shell/top-bar.js.map +1 -1
  252. package/dist/shell/topbar-action-menu.js +3 -2
  253. package/dist/shell/topbar-action-menu.js.map +1 -1
  254. package/dist/shell/topbar-controls.d.ts +5 -2
  255. package/dist/shell/topbar-controls.js +20 -15
  256. package/dist/shell/topbar-controls.js.map +1 -1
  257. package/dist/shell.d.ts +13 -0
  258. package/dist/shell.js +8 -0
  259. package/dist/shell.js.map +1 -0
  260. package/dist/theme/chart-palette.d.ts +19 -12
  261. package/dist/theme/chart-palette.js +24 -30
  262. package/dist/theme/chart-palette.js.map +1 -1
  263. package/dist/theme/color.d.ts +78 -0
  264. package/dist/theme/color.js +148 -0
  265. package/dist/theme/color.js.map +1 -0
  266. package/dist/theme/palette-derive.d.ts +140 -0
  267. package/dist/theme/palette-derive.js +376 -0
  268. package/dist/theme/palette-derive.js.map +1 -0
  269. package/dist/theme/palette-presets.d.ts +14 -1
  270. package/dist/theme/palette-presets.js +231 -3
  271. package/dist/theme/palette-presets.js.map +1 -1
  272. package/dist/theme/palette-store.js +1 -0
  273. package/dist/theme/palette-store.js.map +1 -1
  274. package/dist/theme/theme-store.js +1 -0
  275. package/dist/theme/theme-store.js.map +1 -1
  276. package/dist/tour/tour.d.ts +2 -1
  277. package/dist/tour/tour.js +15 -9
  278. package/dist/tour/tour.js.map +1 -1
  279. package/dist/tour.d.ts +2 -0
  280. package/dist/tour.js +3 -0
  281. package/dist/tour.js.map +1 -0
  282. package/dist/wizard/stepper-nav.js +10 -8
  283. package/dist/wizard/stepper-nav.js.map +1 -1
  284. package/dist/wizard/types.d.ts +18 -2
  285. package/dist/wizard/types.js +4 -1
  286. package/dist/wizard/types.js.map +1 -1
  287. package/dist/wizard/use-wizard.js +12 -4
  288. package/dist/wizard/use-wizard.js.map +1 -1
  289. package/dist/wizard/validation.js +1 -0
  290. package/dist/wizard/validation.js.map +1 -1
  291. package/dist/wizard/wizard-context.js +1 -0
  292. package/dist/wizard/wizard-context.js.map +1 -1
  293. package/dist/wizard/wizard-step.js +1 -0
  294. package/dist/wizard/wizard-step.js.map +1 -1
  295. package/dist/wizard/wizard-summary.js +5 -3
  296. package/dist/wizard/wizard-summary.js.map +1 -1
  297. package/dist/wizard.d.ts +8 -0
  298. package/dist/wizard.js +9 -0
  299. package/dist/wizard.js.map +1 -0
  300. package/package.json +66 -13
  301. package/src/chart.ts +22 -0
  302. package/src/components/account-settings.tsx +275 -0
  303. package/src/components/alert-banner.tsx +75 -0
  304. package/src/components/amount-input.tsx +471 -0
  305. package/src/components/calculator.tsx +252 -0
  306. package/src/components/chart-zoom.tsx +499 -0
  307. package/src/components/chart.tsx +363 -0
  308. package/src/components/checkbox.tsx +222 -0
  309. package/src/components/chip.tsx +570 -0
  310. package/src/components/choice-parts.ts +33 -0
  311. package/src/components/combobox-core.tsx +540 -0
  312. package/src/components/combobox.tsx +1026 -0
  313. package/src/components/currency-select.tsx +269 -0
  314. package/src/components/data-table-filter-popover.tsx +252 -0
  315. package/src/components/data-table-filters.ts +176 -0
  316. package/src/components/data-table-labels.ts +190 -0
  317. package/src/components/data-table-pagination.tsx +126 -0
  318. package/src/components/data-table-sort.ts +70 -0
  319. package/src/components/data-table.tsx +1740 -0
  320. package/src/components/date-picker.tsx +631 -0
  321. package/src/components/dropdown.tsx +341 -0
  322. package/src/components/entity-combobox.tsx +215 -0
  323. package/src/components/facing-pair.ts +86 -0
  324. package/src/components/field-sync.tsx +472 -0
  325. package/src/components/file-dropzone.tsx +181 -0
  326. package/src/components/full-bleed-dialog.tsx +179 -0
  327. package/src/components/grouped-picker.tsx +157 -0
  328. package/src/components/hover-menu.tsx +183 -0
  329. package/src/components/mini-calendar.tsx +554 -0
  330. package/src/components/modal.tsx +287 -0
  331. package/src/components/month-picker.tsx +549 -0
  332. package/src/components/multi-entity-combobox.tsx +226 -0
  333. package/src/components/multi-select.tsx +303 -0
  334. package/src/components/number-field.tsx +225 -0
  335. package/src/components/number-input.tsx +211 -0
  336. package/src/components/numpad-sheet.tsx +264 -0
  337. package/src/components/page-contents.tsx +289 -0
  338. package/src/components/password-strength.tsx +258 -0
  339. package/src/components/picker-sheet.tsx +251 -0
  340. package/src/components/popover.tsx +195 -0
  341. package/src/components/search-field.tsx +146 -0
  342. package/src/components/series-chart-labels.ts +26 -0
  343. package/src/components/series-chart.tsx +481 -0
  344. package/src/components/settings-fields.tsx +68 -0
  345. package/src/components/signature-pad.tsx +630 -0
  346. package/src/components/slider.tsx +313 -0
  347. package/src/components/sparkline.tsx +329 -0
  348. package/src/components/stat-tile.tsx +448 -0
  349. package/src/components/swipeable-row.tsx +271 -0
  350. package/src/components/switch.tsx +170 -0
  351. package/src/components/time-input.tsx +107 -0
  352. package/src/components/toggle-group.tsx +153 -0
  353. package/src/components/toggle-legend.tsx +229 -0
  354. package/src/components/tooltip.tsx +477 -0
  355. package/src/components/treemap.tsx +390 -0
  356. package/src/components/ui.tsx +1284 -0
  357. package/src/components/use-mobile-reveal.ts +54 -0
  358. package/src/components/use-table-state.ts +295 -0
  359. package/src/components/user-avatar.tsx +61 -0
  360. package/src/components/wizard-stepper.tsx +37 -0
  361. package/src/data-table.ts +19 -0
  362. package/src/feedback/feedback-attachment.tsx +246 -0
  363. package/src/feedback/feedback-dialog.tsx +209 -0
  364. package/src/feedback/feedback-inbox.tsx +615 -0
  365. package/src/feedback.ts +8 -0
  366. package/src/hooks/use-anchored-panel.ts +169 -0
  367. package/src/hooks/use-anchored-rect.ts +78 -0
  368. package/src/hooks/use-announce.ts +85 -0
  369. package/src/hooks/use-body-scroll-lock.ts +94 -0
  370. package/src/hooks/use-close-transition.ts +99 -0
  371. package/src/hooks/use-dismiss.ts +77 -0
  372. package/src/hooks/use-focus-trap.ts +160 -0
  373. package/src/hooks/use-media-query.ts +23 -0
  374. package/src/hooks/use-overlay-history.ts +345 -0
  375. package/src/hooks/use-row-swipe.ts +212 -0
  376. package/src/i18n/defaults.ts +68 -0
  377. package/src/i18n/kit-labels.tsx +447 -0
  378. package/src/index.ts +169 -0
  379. package/src/lib/calc.ts +309 -0
  380. package/src/lib/cn.ts +5 -0
  381. package/src/lib/dates.ts +187 -0
  382. package/src/lib/logger.ts +73 -0
  383. package/src/lib/safe-storage.ts +37 -0
  384. package/src/search/command-palette.tsx +342 -0
  385. package/src/search.ts +3 -0
  386. package/src/shell/app-shell.tsx +772 -0
  387. package/src/shell/option-switcher-menu.tsx +72 -0
  388. package/src/shell/role-switcher.tsx +46 -0
  389. package/src/shell/top-bar.tsx +39 -0
  390. package/src/shell/topbar-action-menu.tsx +91 -0
  391. package/src/shell/topbar-controls.tsx +229 -0
  392. package/src/shell.ts +10 -0
  393. package/src/theme/chart-palette.ts +145 -0
  394. package/src/theme/color.ts +266 -0
  395. package/src/theme/palette-derive.ts +681 -0
  396. package/src/theme/palette-presets.ts +760 -0
  397. package/src/theme/palette-store.ts +94 -0
  398. package/src/theme/theme-store.ts +145 -0
  399. package/src/tour/tour.tsx +586 -0
  400. package/src/tour.ts +3 -0
  401. package/src/vite-env.d.ts +26 -0
  402. package/src/wizard/stepper-nav.tsx +221 -0
  403. package/src/wizard/types.ts +178 -0
  404. package/src/wizard/use-wizard.ts +345 -0
  405. package/src/wizard/validation.ts +37 -0
  406. package/src/wizard/wizard-context.tsx +29 -0
  407. package/src/wizard/wizard-step.tsx +14 -0
  408. package/src/wizard/wizard-summary.tsx +69 -0
  409. package/src/wizard.ts +14 -0
  410. package/tokens.css +222 -20
@@ -0,0 +1,226 @@
1
+ import { useId, useMemo, type ComponentPropsWithoutRef, type ReactNode } from "react";
2
+ import { X } from "lucide-react";
3
+ import { cn } from "../lib/cn";
4
+ import { FieldChevron, FieldLabel, FIELD_TRIGGER, FIELD_FLOATING_PAD, FIELD_INVALID } from "./ui";
5
+ import { ComboboxPanel, useComboboxCore, type ComboOption } from "./combobox-core";
6
+ import {
7
+ DEFAULT_COMBOBOX_LABELS,
8
+ DEFAULT_COMMON_LABELS,
9
+ useKitLabels,
10
+ useKitLocale,
11
+ } from "../i18n/kit-labels";
12
+
13
+ /** `onChange` is the kit's — "a selection was made", carrying values — rather
14
+ * than the div's form event, so the DOM's spelling of it is omitted. */
15
+ export interface MultiEntityComboboxProps<V extends string | number>
16
+ extends Omit<ComponentPropsWithoutRef<"div">, "onChange"> {
17
+ /** Selected ids. */
18
+ value: V[];
19
+ onChange: (value: V[]) => void;
20
+ /** Already-loaded options (client-side filtered); also resolves selected labels. */
21
+ options?: ComboOption<V>[];
22
+ /** Async option source, debounced + race-safe. When set, `options` resolves
23
+ * labels only. */
24
+ loadOptions?: (query: string) => Promise<ComboOption<V>[]>;
25
+ loading?: boolean;
26
+ label?: ReactNode;
27
+ placeholder?: string;
28
+ searchPlaceholder?: string;
29
+ emptyLabel?: string;
30
+ /** Trigger summary when 1+ selected (default: the labels as a locale-formatted
31
+ * list, or `combobox.selectedCount` when a label can't be resolved). */
32
+ itemLabel?: (count: number) => string;
33
+ clearable?: boolean;
34
+ clearLabel?: string;
35
+ /** The phone sheet's close button. Its own prop rather than a reuse of
36
+ * `clearLabel`: "clear the selection" and "close this screen" are different
37
+ * actions, and on a full-screen sheet the close button is the only way out —
38
+ * so it is the one control here that MUST be in the reader's language. */
39
+ closeLabel?: string;
40
+ disabled?: boolean;
41
+ /** When set, a "create" row appears for a non-empty query with no exact match.
42
+ * The panel stays open (adding the new entity to `value` is the caller's job). */
43
+ onCreate?: (query: string) => void;
44
+ createLabel?: (query: string) => string;
45
+ /** Required and unanswered — {@link FIELD_INVALID}. See {@link Input}'s `invalid`. */
46
+ invalid?: boolean;
47
+ }
48
+
49
+ /**
50
+ * Multi-value sibling of {@link EntityCombobox}: pick several entities by search,
51
+ * with the same async `loadOptions`/`onCreate` support. Rows show a checkbox and
52
+ * toggle without closing the panel; the trigger summarises the selection.
53
+ */
54
+ export function MultiEntityCombobox<V extends string | number>({
55
+ value,
56
+ onChange,
57
+ options,
58
+ loadOptions,
59
+ loading,
60
+ label,
61
+ placeholder,
62
+ searchPlaceholder,
63
+ emptyLabel,
64
+ itemLabel,
65
+ clearable,
66
+ clearLabel,
67
+ closeLabel,
68
+ disabled,
69
+ onCreate,
70
+ createLabel,
71
+ className,
72
+ invalid,
73
+ "aria-label": ariaLabel,
74
+ ...rest
75
+ }: MultiEntityComboboxProps<V>) {
76
+ const core = useComboboxCore<V>({ options, loadOptions, loading });
77
+ // The props are the per-instance overrides, the provider the app-wide ones; a
78
+ // prop left `undefined` falls through to the provider rather than masking it.
79
+ const labels = useKitLabels("combobox", DEFAULT_COMBOBOX_LABELS, {
80
+ search: searchPlaceholder,
81
+ noResults: emptyLabel,
82
+ clear: clearLabel,
83
+ create: createLabel,
84
+ });
85
+ const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
86
+ const { open, results, resolve, setOpen } = core;
87
+ // One id per instance, generated here rather than in the core: `aria-controls` on
88
+ // the trigger has to name the list while the list is still closed, so the id
89
+ // belongs to whoever renders both ends of it.
90
+ const listboxId = useId();
91
+
92
+ const valueSet = useMemo(() => new Set(value), [value]);
93
+ // A list of NAMES, not a sentence, so the narrow conjunction: English gets
94
+ // "Food, Rent, Travel" (no "and" squeezed into a truncating trigger), German and
95
+ // French still get their "und"/"et" where the language insists on it, and Japanese
96
+ // and Chinese get the ideographic comma "、". The `unit` type looked like the
97
+ // natural fit for a summary, but in ja/zh it concatenates the names with nothing
98
+ // between them at all.
99
+ const locale = useKitLocale();
100
+ const listFormat = useMemo(
101
+ () => new Intl.ListFormat(locale, { type: "conjunction", style: "narrow" }),
102
+ [locale],
103
+ );
104
+ const summary = (() => {
105
+ if (value.length === 0) return placeholder ?? "";
106
+ if (itemLabel) return itemLabel(value.length);
107
+ const chosen = value.map(resolve);
108
+ return chosen.every(Boolean)
109
+ ? listFormat.format(chosen.map((o) => o!.label))
110
+ : labels.selectedCount(value.length);
111
+ })();
112
+
113
+ const q = core.query.trim();
114
+ const showCreate =
115
+ Boolean(onCreate) && q.length > 0 && !results.some((o) => o.label.toLowerCase() === q.toLowerCase());
116
+ const showClear = Boolean(clearable && value.length > 0 && !disabled);
117
+
118
+ const toggle = (o: ComboOption<V>) => {
119
+ core.cacheRef.current.set(o.value, o);
120
+ if (valueSet.has(o.value)) onChange(value.filter((v) => v !== o.value));
121
+ else onChange([...value, o.value]);
122
+ // Stay open — multi-select keeps adding.
123
+ };
124
+
125
+ return (
126
+ // `rest` dresses the wrapper, which has no role; the accessible NAME goes on
127
+ // the trigger, which has one. Spread FIRST so the trigger's ARIA and the
128
+ // handlers that open the panel cannot be clobbered from outside.
129
+ <div {...rest} className={cn("relative", className)}>
130
+ {label !== undefined && <FieldLabel>{label}</FieldLabel>}
131
+ <button
132
+ ref={core.triggerRef}
133
+ type="button"
134
+ // A combobox, not a button. The distinction is not pedantry: this control
135
+ // carried `aria-invalid`, which `button` does not support, so a required
136
+ // field left empty painted a rose border and told a reader nothing at all —
137
+ // and ESLint flagged it as exactly that (`role-supports-aria-props`). The
138
+ // fix the audit asked for is the role that describes what this IS: a closed
139
+ // choice that expands into the list named below. `combobox` supports
140
+ // `aria-invalid`, so the border and the announcement finally agree.
141
+ role="combobox"
142
+ aria-expanded={open}
143
+ aria-controls={listboxId}
144
+ aria-haspopup="listbox"
145
+ // The label is a floating <span>, not a <label for>, so without this the
146
+ // trigger's accessible name is whatever value happens to be selected —
147
+ // "Checking" with nothing saying it is the account. The label AND the
148
+ // value, because `aria-label` replaces the content rather than adding to
149
+ // it, and a control that announces only its name has lost the answer.
150
+ // A caller's own name wins over the composition: two fields labelled
151
+ // "Account" on a transfer form are the from and the to, and only the
152
+ // caller knows which is which.
153
+ aria-label={
154
+ ariaLabel ??
155
+ (typeof label === "string" ? common.fieldValue(label, summary) : undefined)
156
+ }
157
+ disabled={disabled}
158
+ aria-invalid={invalid || undefined}
159
+ onClick={() => !disabled && setOpen((o) => !o)}
160
+ // Down/Up opens the list from the closed trigger, per the APG. Enter and
161
+ // Space already do it through the button's own click.
162
+ onKeyDown={(e) => {
163
+ if (disabled) return;
164
+ if (e.key === "ArrowDown" || e.key === "ArrowUp") {
165
+ e.preventDefault();
166
+ setOpen(true);
167
+ }
168
+ }}
169
+ className={cn(
170
+ FIELD_TRIGGER,
171
+ "pr-9",
172
+ label !== undefined && FIELD_FLOATING_PAD,
173
+ disabled && "cursor-not-allowed opacity-50",
174
+ invalid && FIELD_INVALID,
175
+ )}
176
+ >
177
+ <span
178
+ className={cn(
179
+ "min-w-0 truncate",
180
+ // Inherit FIELD_BASE's ink for a value, keep the placeholder tone
181
+ // (`--text-placeholder`) — the same rule every field on a form row
182
+ // follows (Keksdose dev#477).
183
+ !value.length && "text-[var(--text-placeholder)]",
184
+ )}
185
+ >
186
+ {summary}
187
+ </span>
188
+ {showClear ? (
189
+ <span
190
+ role="button"
191
+ tabIndex={-1}
192
+ aria-label={labels.clear}
193
+ onClick={(e) => {
194
+ e.stopPropagation();
195
+ onChange([]);
196
+ }}
197
+ className="absolute right-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-[var(--text-placeholder)] hover:text-[var(--text-secondary)]"
198
+ >
199
+ <X className="size-4" />
200
+ </span>
201
+ ) : (
202
+ <FieldChevron />
203
+ )}
204
+ </button>
205
+ <ComboboxPanel
206
+ core={core}
207
+ listboxId={listboxId}
208
+ multi
209
+ // On a phone the panel becomes a full-screen sheet, which needs the field's
210
+ // own label to say what it is asking for (live #200).
211
+ sheetTitle={label ?? placeholder}
212
+ searchPlaceholder={labels.search}
213
+ emptyLabel={labels.noResults}
214
+ closeLabel={closeLabel}
215
+ isSelected={(v) => valueSet.has(v)}
216
+ onChoose={toggle}
217
+ showCreate={showCreate}
218
+ onCreate={() => {
219
+ onCreate?.(q);
220
+ core.setQuery("");
221
+ }}
222
+ createContent={labels.create(q)}
223
+ />
224
+ </div>
225
+ );
226
+ }
@@ -0,0 +1,303 @@
1
+ import { useId, useMemo, useState } from "react";
2
+ import type {
3
+ ComponentPropsWithoutRef,
4
+ KeyboardEvent as ReactKeyboardEvent,
5
+ ReactNode,
6
+ } from "react";
7
+ import { Check } from "lucide-react";
8
+ import { FieldChevron, FieldLabel, FIELD_TRIGGER, FIELD_INVALID, FIELD_FLOATING_PAD } from "./ui";
9
+ import { cn } from "../lib/cn";
10
+ import { DropdownPanel, DropdownSearchHeader, useDropdownSearch } from "./dropdown";
11
+ import { useActiveOptionScroll } from "./combobox-core";
12
+ import {
13
+ DEFAULT_COMMON_LABELS,
14
+ DEFAULT_MULTI_SELECT_LABELS,
15
+ useKitLabels,
16
+ } from "../i18n/kit-labels";
17
+
18
+ export interface MultiSelectOption {
19
+ value: string | number;
20
+ label: string;
21
+ hint?: string;
22
+ }
23
+
24
+ /** `onChange` is the kit's — "the selection changed", carrying the new values —
25
+ * rather than the div's form event. */
26
+ export interface MultiSelectProps extends Omit<ComponentPropsWithoutRef<"div">, "onChange"> {
27
+ options: MultiSelectOption[];
28
+ values: (string | number)[];
29
+ onChange: (next: (string | number)[]) => void;
30
+ /** Displayed when no value is selected (i.e. "all"). Defaults to `placeholder`,
31
+ * then to `multiSelect.all` from the {@link UiKitProvider}, then "All". */
32
+ allLabel?: string;
33
+ /** Singular/plural countable label, used when 1+ items are selected. Default:
34
+ * `multiSelect.selectedCount` from the provider — the bare count in English. */
35
+ itemLabel?: (count: number) => string;
36
+ placeholder?: string;
37
+ /** Embedded top-boundary label, matching the native Input/Select fields. */
38
+ label?: ReactNode;
39
+ /** Search-box placeholder. Default: `multiSelect.search` from the provider, else "Search". */
40
+ searchLabel?: string;
41
+ /** "Select all" action label. Default: `multiSelect.selectAll`, else "Select all". */
42
+ selectAllLabel?: string;
43
+ /** "Clear" action label. Default: `multiSelect.clear`, else "Clear". */
44
+ clearLabel?: string;
45
+ /** Extra classes for the open dropdown PANEL — the way to widen it past its
46
+ * 16rem default when the rows carry more than a label (Keksdose feedback #147). */
47
+ panelClassName?: string;
48
+ /** Required and unanswered — {@link FIELD_INVALID}, the same rose border the
49
+ * native `Select` has worn since feedback #235. Every field-styled control in
50
+ * this package carries it now, so a form can mark any of its fields rather than
51
+ * only the one that happened to have it first. */
52
+ invalid?: boolean;
53
+ }
54
+
55
+ export function MultiSelect({
56
+ options,
57
+ values,
58
+ onChange,
59
+ allLabel,
60
+ itemLabel,
61
+ placeholder,
62
+ label,
63
+ searchLabel,
64
+ selectAllLabel,
65
+ clearLabel,
66
+ className,
67
+ panelClassName,
68
+ invalid,
69
+ "aria-label": ariaLabel,
70
+ ...rest
71
+ }: MultiSelectProps) {
72
+ const { open, setOpen, wrapperRef, triggerRef, closeToTrigger, query, setQuery, inputRef } =
73
+ useDropdownSearch();
74
+ // `allLabel` is deliberately NOT folded in here: between it and the provider's
75
+ // `all` sits `placeholder`, which has always doubled as the "nothing picked" text,
76
+ // and a caller who set it must keep seeing it.
77
+ const labels = useKitLabels("multiSelect", DEFAULT_MULTI_SELECT_LABELS, {
78
+ search: searchLabel,
79
+ selectAll: selectAllLabel,
80
+ clear: clearLabel,
81
+ selectedCount: itemLabel,
82
+ });
83
+ const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
84
+ // One id per instance: `aria-controls` on the trigger names the list while the
85
+ // list is still closed.
86
+ const uid = useId();
87
+ const listboxId = `${uid}-listbox`;
88
+ const optionId = (index: number) => `${uid}-option-${index}`;
89
+ // Which row the arrow keys are on. Reset from the handlers that change what the
90
+ // list contains rather than from an effect — a render pass is not where this
91
+ // decision belongs, and the two callers that move the list are right here.
92
+ const [active, setActive] = useState(0);
93
+
94
+ const filtered = useMemo(() => {
95
+ const q = query.trim().toLowerCase();
96
+ if (!q) return options;
97
+ return options.filter(
98
+ (o) =>
99
+ o.label.toLowerCase().includes(q) ||
100
+ (o.hint ?? "").toLowerCase().includes(q),
101
+ );
102
+ }, [options, query]);
103
+
104
+ const valueSet = useMemo(() => new Set(values), [values]);
105
+ const allChecked = options.length > 0 && options.every((o) => valueSet.has(o.value));
106
+ const anyChecked = values.length > 0;
107
+ const selectedSummary = (() => {
108
+ if (!anyChecked || allChecked) return allLabel ?? placeholder ?? labels.all;
109
+ return labels.selectedCount(values.length);
110
+ })();
111
+
112
+ const toggle = (val: string | number) => {
113
+ if (valueSet.has(val)) onChange(values.filter((v) => v !== val));
114
+ else onChange([...values, val]);
115
+ };
116
+
117
+ const selectAll = () => onChange(options.map((o) => o.value));
118
+ const clearAll = () => onChange([]);
119
+
120
+ const activeId = active >= 0 && active < filtered.length ? optionId(active) : undefined;
121
+ useActiveOptionScroll(activeId);
122
+
123
+ // The WAI-ARIA APG's listbox keyboard, handled on the search box because that is
124
+ // where focus is while the panel is up. Escape is `useDropdown`'s, on the document.
125
+ const onListKeyDown = (e: ReactKeyboardEvent<HTMLInputElement>) => {
126
+ if (e.key === "ArrowDown") {
127
+ e.preventDefault();
128
+ setActive((i) => Math.min(i + 1, filtered.length - 1));
129
+ } else if (e.key === "ArrowUp") {
130
+ e.preventDefault();
131
+ setActive((i) => Math.max(i - 1, 0));
132
+ } else if (e.key === "Home") {
133
+ e.preventDefault();
134
+ setActive(0);
135
+ } else if (e.key === "End") {
136
+ e.preventDefault();
137
+ setActive(filtered.length - 1);
138
+ } else if (e.key === "Enter") {
139
+ e.preventDefault();
140
+ // Toggling keeps the panel open, exactly as clicking a row does: this control
141
+ // exists to answer "which of these", which is rarely one of them.
142
+ if (filtered[active]) toggle(filtered[active].value);
143
+ } else if (e.key === "Tab") {
144
+ e.preventDefault();
145
+ closeToTrigger();
146
+ }
147
+ };
148
+
149
+ return (
150
+ // `rest` dresses the wrapper; the name belongs to the trigger below it, which is
151
+ // the thing with a role. Spread FIRST so the ARIA the trigger composes, and the
152
+ // handlers that open the list, cannot be replaced by accident from outside.
153
+ <div {...rest} ref={wrapperRef} className={cn("relative", className)}>
154
+ {label !== undefined && <FieldLabel>{label}</FieldLabel>}
155
+ <button
156
+ ref={triggerRef}
157
+ type="button"
158
+ // A combobox, not a bare button: this trigger carried `aria-invalid`, which
159
+ // `button` does not support, so a required-and-empty filter painted its rose
160
+ // border and announced nothing (ESLint's `role-supports-aria-props`, the
161
+ // audit's §a11y). The role that describes it is the one that also supports
162
+ // the attribute.
163
+ role="combobox"
164
+ aria-expanded={open}
165
+ aria-controls={listboxId}
166
+ aria-haspopup="listbox"
167
+ // The label is a floating <span>, not a <label for>, so without this the
168
+ // trigger announces only its summary — "3 selected", with nothing saying
169
+ // three of WHAT. The label AND the summary, because `aria-label` replaces
170
+ // the content rather than adding to it; the same composition its two
171
+ // siblings use, so a form row of pickers reads the same way throughout.
172
+ // A caller's own name wins: two filters labelled "Accounts" on one screen have
173
+ // to be told apart, and only the caller knows by what.
174
+ aria-label={
175
+ ariaLabel ?? (typeof label === "string" ? common.fieldValue(label, selectedSummary) : undefined)
176
+ }
177
+ onClick={() => {
178
+ setOpen((v) => !v);
179
+ setActive(0);
180
+ }}
181
+ // Down/Up opens the list from the closed trigger, per the APG; Enter and
182
+ // Space already do through the button's own click.
183
+ onKeyDown={(e) => {
184
+ if (e.key === "ArrowDown" || e.key === "ArrowUp") {
185
+ e.preventDefault();
186
+ setOpen(true);
187
+ setActive(0);
188
+ }
189
+ }}
190
+ aria-invalid={invalid || undefined}
191
+ className={cn(FIELD_TRIGGER, "pr-9", label !== undefined && FIELD_FLOATING_PAD, invalid && FIELD_INVALID)}
192
+ >
193
+ {/* The summary IS the field's value, so it inherits FIELD_BASE's ink rather
194
+ than restating a lighter one (Keksdose dev#477). */}
195
+ <span className="truncate">{selectedSummary}</span>
196
+ <FieldChevron />
197
+ </button>
198
+ {open && (
199
+ <DropdownPanel
200
+ // 16rem by default; `panelClassName` is how a caller widens it (Keksdose
201
+ // feedback #147: the market picker's rows carry a postcode, a town, a
202
+ // distance and a receipt count, all of which were being truncated). The
203
+ // override used to be applied from the OUTSIDE with a
204
+ // `[&>div]:w-full` descendant selector, which worked and was a hack —
205
+ // "change the package" was the right call.
206
+ className={cn("w-64", panelClassName)}
207
+ empty={filtered.length === 0}
208
+ // The rows below are options, so the list around them has to be a listbox
209
+ // — and one that says several answers are allowed, which is the only thing
210
+ // that made the trigger's "3 selected" make sense to a reader.
211
+ listProps={{ id: listboxId, role: "listbox", "aria-multiselectable": true }}
212
+ header={
213
+ <>
214
+ <DropdownSearchHeader
215
+ query={query}
216
+ onQueryChange={(v) => {
217
+ setQuery(v);
218
+ // The list under the highlight just changed; keeping the old index
219
+ // would point `aria-activedescendant` at a different option than
220
+ // the one the user was on.
221
+ setActive(0);
222
+ }}
223
+ inputRef={inputRef}
224
+ placeholder={labels.search}
225
+ listboxId={listboxId}
226
+ activeId={activeId}
227
+ onKeyDown={onListKeyDown}
228
+ />
229
+ <div className="flex items-center justify-between gap-2 px-2 py-1.5 text-xs">
230
+ <button
231
+ type="button"
232
+ onClick={selectAll}
233
+ disabled={allChecked}
234
+ className="rounded px-2 py-0.5 text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] disabled:opacity-40"
235
+ >
236
+ {labels.selectAll}
237
+ </button>
238
+ <button
239
+ type="button"
240
+ onClick={clearAll}
241
+ disabled={!anyChecked}
242
+ className="rounded px-2 py-0.5 text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] disabled:opacity-40"
243
+ >
244
+ {labels.clear}
245
+ </button>
246
+ </div>
247
+ </>
248
+ }
249
+ >
250
+ {filtered.map((o, i) => {
251
+ const checked = valueSet.has(o.value);
252
+ return (
253
+ <li key={o.value} role="presentation">
254
+ <button
255
+ type="button"
256
+ id={optionId(i)}
257
+ // The checked state used to exist ONLY as the decorative span
258
+ // below (the audit's §a11y): the row said its label and nothing
259
+ // else, so "selected" was imperceptible without sight. This is
260
+ // the same state, in the one place a reader looks for it.
261
+ role="option"
262
+ aria-selected={checked}
263
+ // Focus stays in the search box — that is what
264
+ // `aria-activedescendant` is for — so the rows are not tab stops.
265
+ tabIndex={-1}
266
+ onClick={() => toggle(o.value)}
267
+ onMouseEnter={() => setActive(i)}
268
+ className={cn(
269
+ "flex w-full items-center justify-between gap-2 px-3 py-1.5 text-sm text-left hover:bg-[var(--bg-hover)]",
270
+ checked && "bg-[var(--bg-active)]",
271
+ i === active && "bg-[var(--bg-hover)]",
272
+ )}
273
+ >
274
+ <span className="flex items-center gap-2 min-w-0">
275
+ <span
276
+ // A picture of `aria-selected` above; a reader that met both
277
+ // would be told twice.
278
+ aria-hidden
279
+ className={cn(
280
+ "flex size-4 shrink-0 items-center justify-center rounded border",
281
+ checked
282
+ ? "border-[var(--bg-inverse)] bg-[var(--bg-inverse)] text-[var(--text-inverse)]"
283
+ : "border-[var(--border-strong)] bg-[var(--bg-surface)]",
284
+ )}
285
+ >
286
+ {checked && <Check className="size-3" />}
287
+ </span>
288
+ <span className="truncate text-[var(--text-primary)]">{o.label}</span>
289
+ {o.hint && (
290
+ <span className="truncate text-xs text-[var(--text-placeholder)]">
291
+ {o.hint}
292
+ </span>
293
+ )}
294
+ </span>
295
+ </button>
296
+ </li>
297
+ );
298
+ })}
299
+ </DropdownPanel>
300
+ )}
301
+ </div>
302
+ );
303
+ }