@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
@@ -0,0 +1,225 @@
1
+ import { useEffect, useId, useState } from "react";
2
+ import type { ComponentProps, ReactNode } from "react";
3
+ import { NumberInput } from "./number-input";
4
+ import { formatResult } from "../lib/calc";
5
+ import { useKitLocale } from "../i18n/kit-labels";
6
+
7
+ type NumberInputProps = ComponentProps<typeof NumberInput>;
8
+
9
+ export interface NumberFieldProps
10
+ extends Omit<NumberInputProps, "value" | "onChange" | "onCommit" | "suffix" | "invalid"> {
11
+ /** The committed number. `null` is "no value" — shown as an empty field. */
12
+ value: number | null;
13
+ /**
14
+ * The parsed number, on blur or Enter — never per keystroke — and only when it
15
+ * differs from the last one committed. Already rounded to `digits` and clamped
16
+ * into `[min, max]`. `null` only when `nullable` and the field was emptied.
17
+ */
18
+ onCommit: (value: number | null) => void;
19
+ /** Round to this many decimals on commit, and show at most this many (trailing
20
+ * zeros are trimmed: `digits={2}` shows 12.5, not 12.50). Omit for no rounding. */
21
+ digits?: number;
22
+ /** Clamp the committed number into `[min, max]`. Applied after rounding. */
23
+ min?: number;
24
+ max?: number;
25
+ /** An emptied field commits `null`. Without it an empty field is not an answer
26
+ * and snaps back to the last value, like any other unparsable text. */
27
+ nullable?: boolean;
28
+ /** The unit — "mm", "%", "km/h". */
29
+ unit?: ReactNode;
30
+ /** Where `unit` goes: `"suffix"` (default) is NumberInput's read-out at the
31
+ * field's end edge; `"label"` appends it to the label as "Label (unit)", for rows
32
+ * of narrow fields where a suffix would eat the digits. */
33
+ unitPlacement?: "suffix" | "label";
34
+ /** BCP 47 tag for the decimal mark, else the `<UiKitProvider locale>`, else the
35
+ * runtime's. */
36
+ locale?: string;
37
+ /** See {@link Input}'s `invalid`. */
38
+ invalid?: boolean;
39
+ /** See {@link Input}'s `error`: shown under the field, attached via
40
+ * `aria-describedby`, implies `invalid`. */
41
+ error?: ReactNode;
42
+ /** Merged with the error's id. Accepted so a form library's control slot (which
43
+ * injects `id` / `aria-describedby` / `aria-invalid`) can wrap this directly. */
44
+ "aria-describedby"?: string;
45
+ "aria-invalid"?: boolean | "true" | "false";
46
+ }
47
+
48
+ /** The locale's decimal mark, as far as this field can type it: "," or ".". A mark
49
+ * outside those two (Arabic's "٫") is not something {@link sanitizeLive} lets
50
+ * through, so those locales keep the dot rather than get a field that eats keys. */
51
+ function decimalMark(locale: string | undefined): "," | "." {
52
+ try {
53
+ const part = new Intl.NumberFormat(locale).formatToParts(1.5).find((p) => p.type === "decimal");
54
+ return part?.value === "," ? "," : ".";
55
+ } catch {
56
+ // An invalid tag throws a RangeError; a typo in a locale prop must not take the
57
+ // form down with it.
58
+ return ".";
59
+ }
60
+ }
61
+
62
+ /** `toFixed` rounding rather than `Math.round(n * 10^d)`, which turns 1.005 into
63
+ * 1.00 by way of 100.49999999999999 — the same choice the lenkbank field made. */
64
+ function roundTo(n: number, digits: number | undefined): number {
65
+ if (digits === undefined) return n;
66
+ const d = Math.min(Math.max(Math.trunc(digits), 0), 100);
67
+ const rounded = Number(n.toFixed(d));
68
+ return Number.isFinite(rounded) ? rounded : n;
69
+ }
70
+
71
+ // The error line under a field — the same type as `ui.tsx`'s (module-private) one,
72
+ // so a NumberField's message is indistinguishable from an Input's.
73
+ const FIELD_ERROR_CLASS = "mt-1 text-[11px] leading-tight text-[var(--danger)]";
74
+
75
+ /**
76
+ * A number field whose value IS a number: {@link NumberInput}'s text, calculator and
77
+ * numpad, with the parse/commit/revert loop every consumer was writing on top of it.
78
+ *
79
+ * WHY THE DRAFT. Half-typed values — "-", "1,", "12+" — are not numbers, and
80
+ * reformatting them mid-keystroke moves the caret and fights the typist; committing
81
+ * per keystroke would also fire whatever a commit fires (a recalculation, a save) for
82
+ * every meaningless intermediate. So the field keeps a draft STRING while it is being
83
+ * typed and resolves it on blur/Enter:
84
+ *
85
+ * - a number (or a calculation, "2700+50") → rounded to `digits`, clamped into
86
+ * `[min, max]`, committed, and the draft re-rendered from the result;
87
+ * - empty → `null` when `nullable`, otherwise the same as the next case — `Number("")`
88
+ * is 0, and clearing a wheelbase and tabbing away must not set it to zero;
89
+ * - anything else unparsable → the draft SNAPS BACK to the last committed value
90
+ * rather than sending NaN on for a server to reject.
91
+ *
92
+ * The draft follows `value` when it changes from outside (a reset, a loaded record);
93
+ * a change that came from here arrives back identical and is a no-op.
94
+ *
95
+ * LOCALE. The decimal mark is the locale's: a German user sees and types "1,5".
96
+ * NumberInput normalises every keystroke to a dot (so its evaluator reads one
97
+ * alphabet); this field maps the dot back before the draft is shown, which also
98
+ * means the numpad's "." key types the mark the field is displaying. There is no
99
+ * digit grouping — "1.000" would read as a thousand in German and as one in English,
100
+ * and a field that parses its own display back must not have that ambiguity.
101
+ */
102
+ export function NumberField({
103
+ value,
104
+ onCommit,
105
+ digits,
106
+ min,
107
+ max,
108
+ nullable = false,
109
+ unit,
110
+ unitPlacement = "suffix",
111
+ locale: localeProp,
112
+ label,
113
+ invalid,
114
+ error,
115
+ id,
116
+ "aria-describedby": ariaDescribedBy,
117
+ "aria-invalid": ariaInvalid,
118
+ ...rest
119
+ }: NumberFieldProps) {
120
+ const locale = useKitLocale(localeProp);
121
+ const mark = decimalMark(locale);
122
+ const localize = (text: string) => (mark === "," ? text.replace(/\./g, ",") : text);
123
+ // `formatResult` for fixed notation: `String(1e-7)` is "1e-7", which the field
124
+ // would read back as 17 (see calc.ts).
125
+ const format = (n: number | null) =>
126
+ n === null || !Number.isFinite(n) ? "" : localize(formatResult(roundTo(n, digits)));
127
+
128
+ const [draft, setDraft] = useState(() => format(value));
129
+ // Everything the displayed text depends on, not just `value`: switching the
130
+ // provider's language or a field's `digits` has to re-render the draft too.
131
+ const shownFor = `${value}|${mark}|${digits}`;
132
+ const [seen, setSeen] = useState(shownFor);
133
+ // The last number handed to `onCommit`, so Enter-then-blur (both commit) and a
134
+ // parent that does not echo the value back cannot commit the same number twice.
135
+ // State rather than a ref because the external-change branch below resets it
136
+ // during render, and a ref may not be written there.
137
+ const [committed, setCommitted] = useState(value);
138
+
139
+ // Adjust state during render rather than in an effect: React re-renders at once
140
+ // with the new draft, before anything is painted, so there is no frame showing
141
+ // the old text.
142
+ if (shownFor !== seen) {
143
+ setSeen(shownFor);
144
+ setDraft(format(value));
145
+ setCommitted(value);
146
+ }
147
+
148
+ const commit = (text: string) => {
149
+ const trimmed = text.trim();
150
+ let next: number | null;
151
+ if (trimmed === "") {
152
+ if (!nullable) return setDraft(format(committed));
153
+ next = null;
154
+ } else {
155
+ // `text` is NumberInput's committed form: an evaluated calculation, dot-decimal.
156
+ // What survives its digits-only fallback can still be "-" or "." alone.
157
+ const parsed = Number(trimmed.replace(/,/g, "."));
158
+ if (!Number.isFinite(parsed)) return setDraft(format(committed));
159
+ next = roundTo(parsed, digits);
160
+ if (min !== undefined) next = Math.max(min, next);
161
+ if (max !== undefined) next = Math.min(max, next);
162
+ }
163
+ // Always re-render the draft from the result — "007" becomes "7", a clamped 500
164
+ // shows the 100 it was clamped to — even when there is nothing new to commit.
165
+ setDraft(format(next));
166
+ if (next !== committed) {
167
+ setCommitted(next);
168
+ onCommit(next);
169
+ }
170
+ };
171
+
172
+ const generatedId = useId();
173
+ const fieldId = id ?? generatedId;
174
+ const errorId = useId();
175
+ // `null`/`false`/`""` are what `touched && errors.x` evaluates to on the happy path.
176
+ const hasError = error !== undefined && error !== null && error !== false && error !== "";
177
+ const describedBy = [ariaDescribedBy, hasError ? errorId : undefined].filter(Boolean).join(" ");
178
+
179
+ // NumberInput takes no `aria-describedby` of its own, so the reference is set on
180
+ // its <input> directly, found by the id this field hands it. React never renders
181
+ // that attribute there, so nothing reconciles it away again.
182
+ useEffect(() => {
183
+ const input = document.getElementById(fieldId);
184
+ if (!input) return;
185
+ if (describedBy) input.setAttribute("aria-describedby", describedBy);
186
+ else input.removeAttribute("aria-describedby");
187
+ }, [fieldId, describedBy]);
188
+
189
+ const labelWithUnit =
190
+ unit !== undefined && unitPlacement === "label"
191
+ ? label === undefined || label === ""
192
+ ? unit
193
+ : typeof label === "string" && typeof unit === "string"
194
+ ? `${label} (${unit})`
195
+ : (
196
+ <>
197
+ {label} ({unit})
198
+ </>
199
+ )
200
+ : label;
201
+
202
+ const field = (
203
+ <NumberInput
204
+ {...rest}
205
+ id={fieldId}
206
+ label={labelWithUnit}
207
+ value={draft}
208
+ onChange={(text) => setDraft(localize(text))}
209
+ onCommit={commit}
210
+ suffix={unitPlacement === "suffix" ? unit : undefined}
211
+ invalid={invalid || hasError || ariaInvalid === true || ariaInvalid === "true"}
212
+ />
213
+ );
214
+ // Same shape as Input's FieldGroup: no wrapper at all without a message, so adding
215
+ // `error` support changed nothing about the DOM a flex row lays out.
216
+ if (!hasError) return field;
217
+ return (
218
+ <div>
219
+ {field}
220
+ <p id={errorId} className={FIELD_ERROR_CLASS}>
221
+ {error}
222
+ </p>
223
+ </div>
224
+ );
225
+ }
@@ -0,0 +1,211 @@
1
+ import { useId, useRef, useState } from "react";
2
+ import type { KeyboardEvent, ReactNode } from "react";
3
+ import { CalculatorButton, type CalculatorButtonLabels } from "./calculator";
4
+ import { NumberPadSheet, type NumberPadSheetLabels } from "./numpad-sheet";
5
+ import { FIELD_BASE, FIELD_DISPLAY, FIELD_INVALID, FLOATING_INPUT_CLASS, FloatingField, PHONE_QUERY } from "./ui";
6
+ import { cn } from "../lib/cn";
7
+ import { commitExpression, sanitizeLive } from "../lib/calc";
8
+ import { useMediaQuery } from "../hooks/use-media-query";
9
+
10
+ interface NumberInputProps {
11
+ value: string;
12
+ /** Fired on every keystroke with the raw text (comma → dot, operators kept so
13
+ * a typed expression survives). */
14
+ onChange: (value: string) => void;
15
+ /** Fired on blur/Enter with the *evaluated* value — use this for save-on-blur
16
+ * fields so the save always sees the resolved number, never "10+5". */
17
+ onCommit?: (value: string) => void;
18
+ label?: ReactNode;
19
+ ariaLabel?: string;
20
+ /** Names for the calculator this field renders — its trigger, and the controls
21
+ * inside the popover — and for the numpad sheet it opens on a phone. Overrides
22
+ * for THIS field only: both resolve the `calculator` namespace of
23
+ * `<UiKitProvider labels>` themselves, then fall back to English.
24
+ *
25
+ * `pad` is not optional plumbing: this field renders a {@link NumberPadSheet}
26
+ * exactly as {@link AmountInput} does, but had no way to pass it anything, so
27
+ * the SAME keypad announced itself in German when a money field opened it and in
28
+ * English when a goal target did. */
29
+ labels?: {
30
+ calculatorTrigger?: string;
31
+ calculator?: CalculatorButtonLabels;
32
+ pad?: NumberPadSheetLabels;
33
+ };
34
+ placeholder?: string;
35
+ disabled?: boolean;
36
+ autoFocus?: boolean;
37
+ /** Wrapper class. */
38
+ className?: string;
39
+ /** Overrides the input's own styling (merged after the field base). */
40
+ inputClassName?: string;
41
+ id?: string;
42
+ /** Show the calculator-popover trigger (default true). Set false for fields in
43
+ * an ephemeral close-on-blur editor, where clicking the icon would blur away
44
+ * and tear the editor down before the popover could open — inline typing
45
+ * (e.g. "200+50" → Enter) still evaluates there. */
46
+ calculator?: boolean;
47
+ /** {@link FIELD_DISPLAY} — on a phone, the figure at display size with the field
48
+ * chrome dropped. The currency-free half of the same treatment `AmountInput`
49
+ * carries, for a dialog whose whole point is the one number (a budget goal's
50
+ * target); not for a field among many, and never for the compact inline
51
+ * editors this control also serves. */
52
+ variant?: "field" | "display";
53
+ /** A {@link FieldHint} "?" on the label's own line, beside the label rather
54
+ * than at the far end of the strip — the far end is where the calculator
55
+ * lives, and a hint placed there landed on top of it (steering-design
56
+ * feedback #48). */
57
+ hint?: ReactNode;
58
+ /** Required and unanswered — {@link FIELD_INVALID}. See {@link Input}'s `invalid`. */
59
+ invalid?: boolean;
60
+ /**
61
+ * A static unit shown at the field's right edge — "%", "kg", "km/h".
62
+ *
63
+ * The read-out half of {@link AmountInput}'s currency chip, and it exists for the
64
+ * same reason that one does: a unit belongs to the FIELD, not to the text, so
65
+ * typing it is a way to get it into the value. Keksdose had a percentage spelled
66
+ * three different ways across three screens — `<Input type="number" step="0.01">`
67
+ * for a loan rate, `step="0.1"` for a tax rate, `inputMode="decimal"` for a VAT
68
+ * rate — and none of the three showed a "%" anywhere near the box, so what the
69
+ * digits meant was a question the label alone had to answer.
70
+ *
71
+ * `aria-hidden`, like the currency read-out: it is a property of the field that
72
+ * the label already names ("Interest rate (%)"), and announcing it again after
73
+ * every value reads as part of the number.
74
+ */
75
+ suffix?: ReactNode;
76
+ }
77
+
78
+ /**
79
+ * A numeric text field with a built-in calculator: type a calculation straight
80
+ * in (e.g. "12+5", evaluated on blur/Enter) or click the trailing calculator
81
+ * icon for a keypad. String `value`/`onChange` contract, mirroring
82
+ * {@link AmountInput} — the currency-free counterpart for budgets, goals,
83
+ * invoice totals and split lines.
84
+ */
85
+ export function NumberInput({
86
+ value,
87
+ onChange,
88
+ onCommit,
89
+ label,
90
+ ariaLabel,
91
+ labels,
92
+ placeholder,
93
+ disabled,
94
+ autoFocus,
95
+ className,
96
+ inputClassName,
97
+ id,
98
+ calculator = true,
99
+ variant = "field",
100
+ hint,
101
+ invalid,
102
+ suffix,
103
+ }: NumberInputProps) {
104
+ const generatedId = useId();
105
+ const fieldId = id ?? generatedId;
106
+ const labelled = label !== undefined;
107
+ // On phones we suppress the OS keyboard (inputMode="none" below) for our own
108
+ // calculator numpad, so the desktop popover trigger is hidden (feedback #334).
109
+ const isMobile = useMediaQuery(PHONE_QUERY, false);
110
+ const asDisplay = isMobile && variant === "display";
111
+ const showCalc = calculator && !disabled && !isMobile;
112
+ const [focused, setFocused] = useState(false);
113
+ // On mobile, focusing opens the numpad bottom sheet instead of the native
114
+ // keyboard (#334); the ref lets its "Done" blur → commit + close.
115
+ const showNumpad = isMobile && !disabled && focused;
116
+ const inputRef = useRef<HTMLInputElement>(null);
117
+
118
+ const commit = () => {
119
+ const next = commitExpression(value);
120
+ if (next !== value) onChange(next);
121
+ onCommit?.(next);
122
+ };
123
+
124
+ const onKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
125
+ if (e.key === "Enter") commit();
126
+ };
127
+
128
+ return (
129
+ <FloatingField
130
+ className={cn("w-full", className)}
131
+ htmlFor={fieldId}
132
+ label={label}
133
+ srOnlyLabel={asDisplay}
134
+ hint={hint}
135
+ >
136
+ <input
137
+ ref={inputRef}
138
+ id={fieldId}
139
+ aria-label={ariaLabel}
140
+ type="text"
141
+ // Mobile: suppress the OS keyboard so the numpad sheet owns entry (field
142
+ // keeps focus/caret); desktop keeps the native decimal keypad.
143
+ inputMode={isMobile ? "none" : "decimal"}
144
+ autoComplete="off"
145
+ autoFocus={autoFocus}
146
+ disabled={disabled}
147
+ // Display mode has no floating label to feed the blank-placeholder trick,
148
+ // and an empty borderless figure shows nothing — so it keeps whatever
149
+ // placeholder the caller gave, falling back to a zero to aim at.
150
+ placeholder={asDisplay ? (placeholder ?? "0") : labelled ? " " : placeholder}
151
+ value={value}
152
+ onChange={(e) => onChange(sanitizeLive(e.target.value))}
153
+ onFocus={() => setFocused(true)}
154
+ onBlur={() => {
155
+ commit();
156
+ setFocused(false);
157
+ }}
158
+ onKeyDown={onKeyDown}
159
+ aria-invalid={invalid || undefined}
160
+ // The right-padding sits last so it always wins over an inputClassName that
161
+ // sets its own px. Both trailing controls can be on at once, so the room
162
+ // they need is reserved together rather than by whichever happens to render:
163
+ // the calculator is ~36px and a short unit ~24px, and a field that reserved
164
+ // only one of them would let the digits run under the other.
165
+ className={cn(
166
+ asDisplay
167
+ ? cn(FIELD_DISPLAY, "text-4xl font-semibold leading-tight tracking-tight tabular-nums")
168
+ : labelled
169
+ ? FLOATING_INPUT_CLASS
170
+ : FIELD_BASE,
171
+ inputClassName,
172
+ showCalc && suffix !== undefined ? "pr-16" : showCalc ? "pr-9" : suffix !== undefined ? "pr-8" : undefined,
173
+ invalid && FIELD_INVALID,
174
+ )}
175
+ />
176
+ {(showCalc || suffix !== undefined) && (
177
+ // One flex track for both, so the unit and the calculator sit side by side
178
+ // instead of stacking on the same corner — AmountInput's arrangement, which
179
+ // has carried a chip and a calculator together since #430.
180
+ <div className="absolute inset-y-0 right-0 flex items-center">
181
+ {showCalc && (
182
+ <CalculatorButton
183
+ value={value}
184
+ onChange={onChange}
185
+ className="self-stretch px-2.5"
186
+ ariaLabel={labels?.calculatorTrigger}
187
+ labels={labels?.calculator}
188
+ />
189
+ )}
190
+ {suffix !== undefined && (
191
+ <span
192
+ aria-hidden
193
+ className="pointer-events-none pr-3 text-xs font-medium text-[var(--text-muted)]"
194
+ >
195
+ {suffix}
196
+ </span>
197
+ )}
198
+ </div>
199
+ )}
200
+ {showNumpad && (
201
+ <NumberPadSheet
202
+ value={value}
203
+ onChange={onChange}
204
+ onDone={() => inputRef.current?.blur()}
205
+ label={label}
206
+ labels={labels?.pad}
207
+ />
208
+ )}
209
+ </FloatingField>
210
+ );
211
+ }