@eifi1/ui-kit 0.4.0 → 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 +91 -20
  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 +96 -20
  63. package/dist/components/dropdown.js +77 -33
  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 +38 -6
  75. package/dist/components/file-dropzone.js +34 -9
  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 +28 -2
  105. package/dist/components/number-input.js +12 -2
  106. package/dist/components/number-input.js.map +1 -1
  107. package/dist/components/numpad-sheet.d.ts +32 -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 +15 -4
  176. package/dist/components/user-avatar.js +5 -4
  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 +27 -5
  270. package/dist/theme/palette-presets.js +231 -33
  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 +67 -14
  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
@@ -1,9 +1,16 @@
1
+ "use client";
1
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
- import { useMemo } from "react";
3
+ import { useId, useMemo, useState } from "react";
3
4
  import { Check } from "lucide-react";
4
5
  import { FieldChevron, FieldLabel, FIELD_TRIGGER, FIELD_INVALID, FIELD_FLOATING_PAD } from "./ui.js";
5
6
  import { cn } from "../lib/cn.js";
6
7
  import { DropdownPanel, DropdownSearchHeader, useDropdownSearch } from "./dropdown.js";
8
+ import { useActiveOptionScroll } from "./combobox-core.js";
9
+ import {
10
+ DEFAULT_COMMON_LABELS,
11
+ DEFAULT_MULTI_SELECT_LABELS,
12
+ useKitLabels
13
+ } from "../i18n/kit-labels.js";
7
14
  function MultiSelect({
8
15
  options,
9
16
  values,
@@ -12,14 +19,27 @@ function MultiSelect({
12
19
  itemLabel,
13
20
  placeholder,
14
21
  label,
15
- searchLabel = "Search",
16
- selectAllLabel = "Select all",
17
- clearLabel = "Clear",
22
+ searchLabel,
23
+ selectAllLabel,
24
+ clearLabel,
18
25
  className,
19
26
  panelClassName,
20
- invalid
27
+ invalid,
28
+ "aria-label": ariaLabel,
29
+ ...rest
21
30
  }) {
22
- const { open, setOpen, wrapperRef, query, setQuery, inputRef } = useDropdownSearch();
31
+ const { open, setOpen, wrapperRef, triggerRef, closeToTrigger, query, setQuery, inputRef } = useDropdownSearch();
32
+ const labels = useKitLabels("multiSelect", DEFAULT_MULTI_SELECT_LABELS, {
33
+ search: searchLabel,
34
+ selectAll: selectAllLabel,
35
+ clear: clearLabel,
36
+ selectedCount: itemLabel
37
+ });
38
+ const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
39
+ const uid = useId();
40
+ const listboxId = `${uid}-listbox`;
41
+ const optionId = (index) => `${uid}-option-${index}`;
42
+ const [active, setActive] = useState(0);
23
43
  const filtered = useMemo(() => {
24
44
  const q = query.trim().toLowerCase();
25
45
  if (!q) return options;
@@ -31,9 +51,8 @@ function MultiSelect({
31
51
  const allChecked = options.length > 0 && options.every((o) => valueSet.has(o.value));
32
52
  const anyChecked = values.length > 0;
33
53
  const selectedSummary = (() => {
34
- if (!anyChecked) return allLabel ?? placeholder ?? "All";
35
- if (allChecked) return allLabel ?? placeholder ?? "All";
36
- return itemLabel ? itemLabel(values.length) : `${values.length}`;
54
+ if (!anyChecked || allChecked) return allLabel ?? placeholder ?? labels.all;
55
+ return labels.selectedCount(values.length);
37
56
  })();
38
57
  const toggle = (val) => {
39
58
  if (valueSet.has(val)) onChange(values.filter((v) => v !== val));
@@ -41,90 +60,148 @@ function MultiSelect({
41
60
  };
42
61
  const selectAll = () => onChange(options.map((o) => o.value));
43
62
  const clearAll = () => onChange([]);
44
- return /* @__PURE__ */ jsxs("div", { ref: wrapperRef, className: cn("relative", className), children: [
45
- label !== void 0 && /* @__PURE__ */ jsx(FieldLabel, { children: label }),
46
- /* @__PURE__ */ jsxs(
47
- "button",
48
- {
49
- type: "button",
50
- onClick: () => setOpen((v) => !v),
51
- "aria-invalid": invalid || void 0,
52
- className: cn(FIELD_TRIGGER, "pr-9", label !== void 0 && FIELD_FLOATING_PAD, invalid && FIELD_INVALID),
53
- children: [
54
- /* @__PURE__ */ jsx("span", { className: "truncate", children: selectedSummary }),
55
- /* @__PURE__ */ jsx(FieldChevron, {})
56
- ]
57
- }
58
- ),
59
- open && /* @__PURE__ */ jsx(
60
- DropdownPanel,
61
- {
62
- className: cn("w-64", panelClassName),
63
- empty: filtered.length === 0,
64
- header: /* @__PURE__ */ jsxs(Fragment, { children: [
65
- /* @__PURE__ */ jsx(
66
- DropdownSearchHeader,
67
- {
68
- query,
69
- onQueryChange: setQuery,
70
- inputRef,
71
- placeholder: searchLabel
63
+ const activeId = active >= 0 && active < filtered.length ? optionId(active) : void 0;
64
+ useActiveOptionScroll(activeId);
65
+ const onListKeyDown = (e) => {
66
+ if (e.key === "ArrowDown") {
67
+ e.preventDefault();
68
+ setActive((i) => Math.min(i + 1, filtered.length - 1));
69
+ } else if (e.key === "ArrowUp") {
70
+ e.preventDefault();
71
+ setActive((i) => Math.max(i - 1, 0));
72
+ } else if (e.key === "Home") {
73
+ e.preventDefault();
74
+ setActive(0);
75
+ } else if (e.key === "End") {
76
+ e.preventDefault();
77
+ setActive(filtered.length - 1);
78
+ } else if (e.key === "Enter") {
79
+ e.preventDefault();
80
+ if (filtered[active]) toggle(filtered[active].value);
81
+ } else if (e.key === "Tab") {
82
+ e.preventDefault();
83
+ closeToTrigger();
84
+ }
85
+ };
86
+ return (
87
+ // `rest` dresses the wrapper; the name belongs to the trigger below it, which is
88
+ // the thing with a role. Spread FIRST so the ARIA the trigger composes, and the
89
+ // handlers that open the list, cannot be replaced by accident from outside.
90
+ /* @__PURE__ */ jsxs("div", { ...rest, ref: wrapperRef, className: cn("relative", className), children: [
91
+ label !== void 0 && /* @__PURE__ */ jsx(FieldLabel, { children: label }),
92
+ /* @__PURE__ */ jsxs(
93
+ "button",
94
+ {
95
+ ref: triggerRef,
96
+ type: "button",
97
+ role: "combobox",
98
+ "aria-expanded": open,
99
+ "aria-controls": listboxId,
100
+ "aria-haspopup": "listbox",
101
+ "aria-label": ariaLabel ?? (typeof label === "string" ? common.fieldValue(label, selectedSummary) : void 0),
102
+ onClick: () => {
103
+ setOpen((v) => !v);
104
+ setActive(0);
105
+ },
106
+ onKeyDown: (e) => {
107
+ if (e.key === "ArrowDown" || e.key === "ArrowUp") {
108
+ e.preventDefault();
109
+ setOpen(true);
110
+ setActive(0);
72
111
  }
73
- ),
74
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 px-2 py-1.5 text-xs", children: [
112
+ },
113
+ "aria-invalid": invalid || void 0,
114
+ className: cn(FIELD_TRIGGER, "pr-9", label !== void 0 && FIELD_FLOATING_PAD, invalid && FIELD_INVALID),
115
+ children: [
116
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: selectedSummary }),
117
+ /* @__PURE__ */ jsx(FieldChevron, {})
118
+ ]
119
+ }
120
+ ),
121
+ open && /* @__PURE__ */ jsx(
122
+ DropdownPanel,
123
+ {
124
+ className: cn("w-64", panelClassName),
125
+ empty: filtered.length === 0,
126
+ listProps: { id: listboxId, role: "listbox", "aria-multiselectable": true },
127
+ header: /* @__PURE__ */ jsxs(Fragment, { children: [
75
128
  /* @__PURE__ */ jsx(
76
- "button",
129
+ DropdownSearchHeader,
77
130
  {
78
- type: "button",
79
- onClick: selectAll,
80
- disabled: allChecked,
81
- className: "rounded px-2 py-0.5 text-slate-600 hover:bg-slate-100 disabled:opacity-40 dark:text-slate-300 dark:hover:bg-slate-800",
82
- children: selectAllLabel
131
+ query,
132
+ onQueryChange: (v) => {
133
+ setQuery(v);
134
+ setActive(0);
135
+ },
136
+ inputRef,
137
+ placeholder: labels.search,
138
+ listboxId,
139
+ activeId,
140
+ onKeyDown: onListKeyDown
83
141
  }
84
142
  ),
85
- /* @__PURE__ */ jsx(
143
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 px-2 py-1.5 text-xs", children: [
144
+ /* @__PURE__ */ jsx(
145
+ "button",
146
+ {
147
+ type: "button",
148
+ onClick: selectAll,
149
+ disabled: allChecked,
150
+ className: "rounded px-2 py-0.5 text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] disabled:opacity-40",
151
+ children: labels.selectAll
152
+ }
153
+ ),
154
+ /* @__PURE__ */ jsx(
155
+ "button",
156
+ {
157
+ type: "button",
158
+ onClick: clearAll,
159
+ disabled: !anyChecked,
160
+ className: "rounded px-2 py-0.5 text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] disabled:opacity-40",
161
+ children: labels.clear
162
+ }
163
+ )
164
+ ] })
165
+ ] }),
166
+ children: filtered.map((o, i) => {
167
+ const checked = valueSet.has(o.value);
168
+ return /* @__PURE__ */ jsx("li", { role: "presentation", children: /* @__PURE__ */ jsx(
86
169
  "button",
87
170
  {
88
171
  type: "button",
89
- onClick: clearAll,
90
- disabled: !anyChecked,
91
- className: "rounded px-2 py-0.5 text-slate-600 hover:bg-slate-100 disabled:opacity-40 dark:text-slate-300 dark:hover:bg-slate-800",
92
- children: clearLabel
93
- }
94
- )
95
- ] })
96
- ] }),
97
- children: filtered.map((o) => {
98
- const checked = valueSet.has(o.value);
99
- return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
100
- "button",
101
- {
102
- type: "button",
103
- onClick: () => toggle(o.value),
104
- className: cn(
105
- "flex w-full items-center justify-between gap-2 px-3 py-1.5 text-sm text-left hover:bg-slate-50 dark:hover:bg-slate-800",
106
- checked && "bg-slate-100/60 dark:bg-slate-800/60"
107
- ),
108
- children: /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2 min-w-0", children: [
109
- /* @__PURE__ */ jsx(
110
- "span",
111
- {
112
- className: cn(
113
- "flex size-4 shrink-0 items-center justify-center rounded border",
114
- checked ? "border-slate-900 bg-slate-900 text-white dark:border-slate-100 dark:bg-slate-100 dark:text-slate-900" : "border-slate-300 bg-white dark:border-slate-600 dark:bg-slate-900"
115
- ),
116
- children: checked && /* @__PURE__ */ jsx(Check, { className: "size-3" })
117
- }
172
+ id: optionId(i),
173
+ role: "option",
174
+ "aria-selected": checked,
175
+ tabIndex: -1,
176
+ onClick: () => toggle(o.value),
177
+ onMouseEnter: () => setActive(i),
178
+ className: cn(
179
+ "flex w-full items-center justify-between gap-2 px-3 py-1.5 text-sm text-left hover:bg-[var(--bg-hover)]",
180
+ checked && "bg-[var(--bg-active)]",
181
+ i === active && "bg-[var(--bg-hover)]"
118
182
  ),
119
- /* @__PURE__ */ jsx("span", { className: "truncate text-slate-900 dark:text-slate-100", children: o.label }),
120
- o.hint && /* @__PURE__ */ jsx("span", { className: "truncate text-xs text-slate-400 dark:text-slate-500", children: o.hint })
121
- ] })
122
- }
123
- ) }, o.value);
124
- })
125
- }
126
- )
127
- ] });
183
+ children: /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2 min-w-0", children: [
184
+ /* @__PURE__ */ jsx(
185
+ "span",
186
+ {
187
+ "aria-hidden": true,
188
+ className: cn(
189
+ "flex size-4 shrink-0 items-center justify-center rounded border",
190
+ checked ? "border-[var(--bg-inverse)] bg-[var(--bg-inverse)] text-[var(--text-inverse)]" : "border-[var(--border-strong)] bg-[var(--bg-surface)]"
191
+ ),
192
+ children: checked && /* @__PURE__ */ jsx(Check, { className: "size-3" })
193
+ }
194
+ ),
195
+ /* @__PURE__ */ jsx("span", { className: "truncate text-[var(--text-primary)]", children: o.label }),
196
+ o.hint && /* @__PURE__ */ jsx("span", { className: "truncate text-xs text-[var(--text-placeholder)]", children: o.hint })
197
+ ] })
198
+ }
199
+ ) }, o.value);
200
+ })
201
+ }
202
+ )
203
+ ] })
204
+ );
128
205
  }
129
206
  export {
130
207
  MultiSelect
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/multi-select.tsx"],"sourcesContent":["import { useMemo } from \"react\";\nimport type { ReactNode } 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\";\n\nexport interface MultiSelectOption {\n value: string | number;\n label: string;\n hint?: string;\n}\n\ninterface Props {\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 \"All\"; pass a\n * translated string for i18n. */\n allLabel?: string;\n /** Singular/plural countable label, used when 1+ items are selected. */\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 \"Search\"). */\n searchLabel?: string;\n /** \"Select all\" action label (default \"Select all\"). */\n selectAllLabel?: string;\n /** \"Clear\" action label (default \"Clear\"). */\n clearLabel?: string;\n className?: 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}\n\nexport function MultiSelect({\n options,\n values,\n onChange,\n allLabel,\n itemLabel,\n placeholder,\n label,\n searchLabel = \"Search\",\n selectAllLabel = \"Select all\",\n clearLabel = \"Clear\",\n className,\n panelClassName,\n invalid,\n}: Props) {\n const { open, setOpen, wrapperRef, query, setQuery, inputRef } = useDropdownSearch();\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) return allLabel ?? placeholder ?? \"All\";\n if (allChecked) return allLabel ?? placeholder ?? \"All\";\n return itemLabel ? itemLabel(values.length) : `${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 return (\n <div ref={wrapperRef} className={cn(\"relative\", className)}>\n {label !== undefined && <FieldLabel>{label}</FieldLabel>}\n <button\n type=\"button\"\n onClick={() => setOpen((v) => !v)}\n aria-invalid={invalid || undefined}\n className={cn(FIELD_TRIGGER, \"pr-9\", label !== undefined && FIELD_FLOATING_PAD, invalid && FIELD_INVALID)}\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 <FieldChevron />\n </button>\n {open && (\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 header={\n <>\n <DropdownSearchHeader\n query={query}\n onQueryChange={setQuery}\n inputRef={inputRef}\n placeholder={searchLabel}\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-slate-600 hover:bg-slate-100 disabled:opacity-40 dark:text-slate-300 dark:hover:bg-slate-800\"\n >\n {selectAllLabel}\n </button>\n <button\n type=\"button\"\n onClick={clearAll}\n disabled={!anyChecked}\n className=\"rounded px-2 py-0.5 text-slate-600 hover:bg-slate-100 disabled:opacity-40 dark:text-slate-300 dark:hover:bg-slate-800\"\n >\n {clearLabel}\n </button>\n </div>\n </>\n }\n >\n {filtered.map((o) => {\n const checked = valueSet.has(o.value);\n return (\n <li key={o.value}>\n <button\n type=\"button\"\n onClick={() => toggle(o.value)}\n className={cn(\n \"flex w-full items-center justify-between gap-2 px-3 py-1.5 text-sm text-left hover:bg-slate-50 dark:hover:bg-slate-800\",\n checked && \"bg-slate-100/60 dark:bg-slate-800/60\",\n )}\n >\n <span className=\"flex items-center gap-2 min-w-0\">\n <span\n className={cn(\n \"flex size-4 shrink-0 items-center justify-center rounded border\",\n checked\n ? \"border-slate-900 bg-slate-900 text-white dark:border-slate-100 dark:bg-slate-100 dark:text-slate-900\"\n : \"border-slate-300 bg-white dark:border-slate-600 dark:bg-slate-900\",\n )}\n >\n {checked && <Check className=\"size-3\" />}\n </span>\n <span className=\"truncate text-slate-900 dark:text-slate-100\">{o.label}</span>\n {o.hint && (\n <span className=\"truncate text-xs text-slate-400 dark:text-slate-500\">\n {o.hint}\n </span>\n )}\n </span>\n </button>\n </li>\n );\n })}\n </DropdownPanel>\n )}\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 } 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}\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 \"aria-label\": ariaLabel,\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 // 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>{label}</FieldLabel>}\n <button\n ref={triggerRef}\n type=\"button\"\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={invalid || undefined}\n className={cn(FIELD_TRIGGER, \"pr-9\", label !== undefined && FIELD_FLOATING_PAD, invalid && FIELD_INVALID)}\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 <FieldChevron />\n </button>\n {open && (\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-left 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 </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,92 @@
1
+ import * as react from 'react';
2
+ import { ComponentProps, ReactNode } from 'react';
3
+ import { NumberInput } from './number-input.js';
4
+ import './calculator.js';
5
+ import '../i18n/kit-labels.js';
6
+ import './data-table-labels.js';
7
+ import './mini-calendar.js';
8
+ import './popover.js';
9
+ import './chip.js';
10
+ import 'lucide-react';
11
+ import './field-sync.js';
12
+ import './ui.js';
13
+ import '../wizard/types.js';
14
+ import '../tour/tour.js';
15
+ import '../search/command-palette.js';
16
+ import './month-picker.js';
17
+ import './page-contents.js';
18
+ import './series-chart-labels.js';
19
+ import './sparkline.js';
20
+ import './stat-tile.js';
21
+ import './signature-pad.js';
22
+ import './password-strength.js';
23
+ import './numpad-sheet.js';
24
+
25
+ type NumberInputProps = ComponentProps<typeof NumberInput>;
26
+ interface NumberFieldProps extends Omit<NumberInputProps, "value" | "onChange" | "onCommit" | "suffix" | "invalid"> {
27
+ /** The committed number. `null` is "no value" — shown as an empty field. */
28
+ value: number | null;
29
+ /**
30
+ * The parsed number, on blur or Enter — never per keystroke — and only when it
31
+ * differs from the last one committed. Already rounded to `digits` and clamped
32
+ * into `[min, max]`. `null` only when `nullable` and the field was emptied.
33
+ */
34
+ onCommit: (value: number | null) => void;
35
+ /** Round to this many decimals on commit, and show at most this many (trailing
36
+ * zeros are trimmed: `digits={2}` shows 12.5, not 12.50). Omit for no rounding. */
37
+ digits?: number;
38
+ /** Clamp the committed number into `[min, max]`. Applied after rounding. */
39
+ min?: number;
40
+ max?: number;
41
+ /** An emptied field commits `null`. Without it an empty field is not an answer
42
+ * and snaps back to the last value, like any other unparsable text. */
43
+ nullable?: boolean;
44
+ /** The unit — "mm", "%", "km/h". */
45
+ unit?: ReactNode;
46
+ /** Where `unit` goes: `"suffix"` (default) is NumberInput's read-out at the
47
+ * field's end edge; `"label"` appends it to the label as "Label (unit)", for rows
48
+ * of narrow fields where a suffix would eat the digits. */
49
+ unitPlacement?: "suffix" | "label";
50
+ /** BCP 47 tag for the decimal mark, else the `<UiKitProvider locale>`, else the
51
+ * runtime's. */
52
+ locale?: string;
53
+ /** See {@link Input}'s `invalid`. */
54
+ invalid?: boolean;
55
+ /** See {@link Input}'s `error`: shown under the field, attached via
56
+ * `aria-describedby`, implies `invalid`. */
57
+ error?: ReactNode;
58
+ /** Merged with the error's id. Accepted so a form library's control slot (which
59
+ * injects `id` / `aria-describedby` / `aria-invalid`) can wrap this directly. */
60
+ "aria-describedby"?: string;
61
+ "aria-invalid"?: boolean | "true" | "false";
62
+ }
63
+ /**
64
+ * A number field whose value IS a number: {@link NumberInput}'s text, calculator and
65
+ * numpad, with the parse/commit/revert loop every consumer was writing on top of it.
66
+ *
67
+ * WHY THE DRAFT. Half-typed values — "-", "1,", "12+" — are not numbers, and
68
+ * reformatting them mid-keystroke moves the caret and fights the typist; committing
69
+ * per keystroke would also fire whatever a commit fires (a recalculation, a save) for
70
+ * every meaningless intermediate. So the field keeps a draft STRING while it is being
71
+ * typed and resolves it on blur/Enter:
72
+ *
73
+ * - a number (or a calculation, "2700+50") → rounded to `digits`, clamped into
74
+ * `[min, max]`, committed, and the draft re-rendered from the result;
75
+ * - empty → `null` when `nullable`, otherwise the same as the next case — `Number("")`
76
+ * is 0, and clearing a wheelbase and tabbing away must not set it to zero;
77
+ * - anything else unparsable → the draft SNAPS BACK to the last committed value
78
+ * rather than sending NaN on for a server to reject.
79
+ *
80
+ * The draft follows `value` when it changes from outside (a reset, a loaded record);
81
+ * a change that came from here arrives back identical and is a no-op.
82
+ *
83
+ * LOCALE. The decimal mark is the locale's: a German user sees and types "1,5".
84
+ * NumberInput normalises every keystroke to a dot (so its evaluator reads one
85
+ * alphabet); this field maps the dot back before the draft is shown, which also
86
+ * means the numpad's "." key types the mark the field is displaying. There is no
87
+ * digit grouping — "1.000" would read as a thousand in German and as one in English,
88
+ * and a field that parses its own display back must not have that ambiguity.
89
+ */
90
+ declare function NumberField({ value, onCommit, digits, min, max, nullable, unit, unitPlacement, locale: localeProp, label, invalid, error, id, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid, ...rest }: NumberFieldProps): react.JSX.Element;
91
+
92
+ export { NumberField, type NumberFieldProps };
@@ -0,0 +1,111 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useId, useState } from "react";
4
+ import { NumberInput } from "./number-input.js";
5
+ import { formatResult } from "../lib/calc.js";
6
+ 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
+ function roundTo(n, digits) {
16
+ if (digits === void 0) return n;
17
+ const d = Math.min(Math.max(Math.trunc(digits), 0), 100);
18
+ const rounded = Number(n.toFixed(d));
19
+ return Number.isFinite(rounded) ? rounded : n;
20
+ }
21
+ const FIELD_ERROR_CLASS = "mt-1 text-[11px] leading-tight text-[var(--danger)]";
22
+ function NumberField({
23
+ value,
24
+ onCommit,
25
+ digits,
26
+ min,
27
+ max,
28
+ nullable = false,
29
+ unit,
30
+ unitPlacement = "suffix",
31
+ locale: localeProp,
32
+ label,
33
+ invalid,
34
+ error,
35
+ id,
36
+ "aria-describedby": ariaDescribedBy,
37
+ "aria-invalid": ariaInvalid,
38
+ ...rest
39
+ }) {
40
+ const locale = useKitLocale(localeProp);
41
+ const mark = decimalMark(locale);
42
+ const localize = (text) => mark === "," ? text.replace(/\./g, ",") : text;
43
+ const format = (n) => n === null || !Number.isFinite(n) ? "" : localize(formatResult(roundTo(n, digits)));
44
+ const [draft, setDraft] = useState(() => format(value));
45
+ const shownFor = `${value}|${mark}|${digits}`;
46
+ const [seen, setSeen] = useState(shownFor);
47
+ const [committed, setCommitted] = useState(value);
48
+ if (shownFor !== seen) {
49
+ setSeen(shownFor);
50
+ setDraft(format(value));
51
+ setCommitted(value);
52
+ }
53
+ const commit = (text) => {
54
+ const trimmed = text.trim();
55
+ let next;
56
+ if (trimmed === "") {
57
+ if (!nullable) return setDraft(format(committed));
58
+ next = null;
59
+ } else {
60
+ const parsed = Number(trimmed.replace(/,/g, "."));
61
+ if (!Number.isFinite(parsed)) return setDraft(format(committed));
62
+ next = roundTo(parsed, digits);
63
+ if (min !== void 0) next = Math.max(min, next);
64
+ if (max !== void 0) next = Math.min(max, next);
65
+ }
66
+ setDraft(format(next));
67
+ if (next !== committed) {
68
+ setCommitted(next);
69
+ onCommit(next);
70
+ }
71
+ };
72
+ const generatedId = useId();
73
+ const fieldId = id ?? generatedId;
74
+ const errorId = useId();
75
+ const hasError = error !== void 0 && error !== null && error !== false && error !== "";
76
+ const describedBy = [ariaDescribedBy, hasError ? errorId : void 0].filter(Boolean).join(" ");
77
+ useEffect(() => {
78
+ const input = document.getElementById(fieldId);
79
+ if (!input) return;
80
+ if (describedBy) input.setAttribute("aria-describedby", describedBy);
81
+ else input.removeAttribute("aria-describedby");
82
+ }, [fieldId, describedBy]);
83
+ const labelWithUnit = unit !== void 0 && unitPlacement === "label" ? label === void 0 || label === "" ? unit : typeof label === "string" && typeof unit === "string" ? `${label} (${unit})` : /* @__PURE__ */ jsxs(Fragment, { children: [
84
+ label,
85
+ " (",
86
+ unit,
87
+ ")"
88
+ ] }) : label;
89
+ const field = /* @__PURE__ */ jsx(
90
+ NumberInput,
91
+ {
92
+ ...rest,
93
+ id: fieldId,
94
+ label: labelWithUnit,
95
+ value: draft,
96
+ onChange: (text) => setDraft(localize(text)),
97
+ onCommit: commit,
98
+ suffix: unitPlacement === "suffix" ? unit : void 0,
99
+ invalid: invalid || hasError || ariaInvalid === true || ariaInvalid === "true"
100
+ }
101
+ );
102
+ if (!hasError) return field;
103
+ return /* @__PURE__ */ jsxs("div", { children: [
104
+ field,
105
+ /* @__PURE__ */ jsx("p", { id: errorId, className: FIELD_ERROR_CLASS, children: error })
106
+ ] });
107
+ }
108
+ export {
109
+ NumberField
110
+ };
111
+ //# sourceMappingURL=number-field.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/number-field.tsx"],"sourcesContent":["import { useEffect, useId, useState } from \"react\";\nimport type { ComponentProps, ReactNode } from \"react\";\nimport { NumberInput } from \"./number-input\";\nimport { 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 /** 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\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/** `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 * 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({\n value,\n onCommit,\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 ...rest\n}: NumberFieldProps) {\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 shownFor = `${value}|${mark}|${digits}`;\n const [seen, setSeen] = useState(shownFor);\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\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 (shownFor !== seen) {\n setSeen(shownFor);\n setDraft(format(value));\n setCommitted(value);\n }\n\n const commit = (text: string) => {\n const trimmed = text.trim();\n let next: number | null;\n if (trimmed === \"\") {\n if (!nullable) return setDraft(format(committed));\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 setDraft(format(committed));\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 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 // NumberInput takes no `aria-describedby` of its own, so the reference is set on\n // its <input> directly, found by the id this field hands it. React never renders\n // that attribute there, so nothing reconciles it away again.\n useEffect(() => {\n const input = document.getElementById(fieldId);\n if (!input) return;\n if (describedBy) input.setAttribute(\"aria-describedby\", describedBy);\n else input.removeAttribute(\"aria-describedby\");\n }, [fieldId, describedBy]);\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 label={labelWithUnit}\n value={draft}\n onChange={(text) => setDraft(localize(text))}\n onCommit={commit}\n suffix={unitPlacement === \"suffix\" ? unit : undefined}\n invalid={invalid || hasError || ariaInvalid === true || ariaInvalid === \"true\"}\n />\n );\n // Same shape as Input's FieldGroup: no wrapper at all without a message, so adding\n // `error` support changed nothing about the DOM a flex row lays out.\n if (!hasError) return field;\n return (\n <div>\n {field}\n <p id={errorId} className={FIELD_ERROR_CLASS}>\n {error}\n </p>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -1,6 +1,25 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
3
  import { CalculatorButtonLabels } from './calculator.js';
4
+ import { NumberPadSheetLabels } from './numpad-sheet.js';
5
+ import '../i18n/kit-labels.js';
6
+ import './data-table-labels.js';
7
+ import './mini-calendar.js';
8
+ import './popover.js';
9
+ import './chip.js';
10
+ import 'lucide-react';
11
+ import './field-sync.js';
12
+ import './ui.js';
13
+ import '../wizard/types.js';
14
+ import '../tour/tour.js';
15
+ import '../search/command-palette.js';
16
+ import './month-picker.js';
17
+ import './page-contents.js';
18
+ import './series-chart-labels.js';
19
+ import './sparkline.js';
20
+ import './stat-tile.js';
21
+ import './signature-pad.js';
22
+ import './password-strength.js';
4
23
 
5
24
  interface NumberInputProps {
6
25
  value: string;
@@ -13,11 +32,18 @@ interface NumberInputProps {
13
32
  label?: ReactNode;
14
33
  ariaLabel?: string;
15
34
  /** Names for the calculator this field renders — its trigger, and the controls
16
- * inside the popover. English defaults, so a host that does not translate is
17
- * unaffected. */
35
+ * inside the popover — and for the numpad sheet it opens on a phone. Overrides
36
+ * for THIS field only: both resolve the `calculator` namespace of
37
+ * `<UiKitProvider labels>` themselves, then fall back to English.
38
+ *
39
+ * `pad` is not optional plumbing: this field renders a {@link NumberPadSheet}
40
+ * exactly as {@link AmountInput} does, but had no way to pass it anything, so
41
+ * the SAME keypad announced itself in German when a money field opened it and in
42
+ * English when a goal target did. */
18
43
  labels?: {
19
44
  calculatorTrigger?: string;
20
45
  calculator?: CalculatorButtonLabels;
46
+ pad?: NumberPadSheetLabels;
21
47
  };
22
48
  placeholder?: string;
23
49
  disabled?: boolean;