@teamofsilicons/silicon-ui 0.0.0-stage → 0.1.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 (352) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +63 -2
  3. package/components/accordion/accordion.d.ts +13 -0
  4. package/components/accordion/accordion.js +35 -0
  5. package/components/accordion/accordion.module.css +15 -0
  6. package/components/action-button/action-button.d.ts +11 -0
  7. package/components/action-button/action-button.js +57 -0
  8. package/components/action-button/action-button.module.css +15 -0
  9. package/components/activity-heatmap/activity-heatmap.d.ts +37 -0
  10. package/components/activity-heatmap/activity-heatmap.js +303 -0
  11. package/components/activity-heatmap/activity-heatmap.module.css +83 -0
  12. package/components/alert/alert.d.ts +13 -0
  13. package/components/alert/alert.js +79 -0
  14. package/components/alert/alert.module.css +15 -0
  15. package/components/animated-counter/animated-counter.d.ts +13 -0
  16. package/components/animated-counter/animated-counter.js +73 -0
  17. package/components/animated-counter/animated-counter.module.css +12 -0
  18. package/components/announcement-bar/announcement-bar.d.ts +53 -0
  19. package/components/announcement-bar/announcement-bar.js +187 -0
  20. package/components/announcement-bar/announcement-bar.module.css +60 -0
  21. package/components/avatar/avatar.d.ts +8 -0
  22. package/components/avatar/avatar.js +18 -0
  23. package/components/avatar/avatar.module.css +60 -0
  24. package/components/avatar-group/avatar-group.d.ts +13 -0
  25. package/components/avatar-group/avatar-group.js +24 -0
  26. package/components/avatar-group/avatar-group.module.css +27 -0
  27. package/components/badge/badge.d.ts +10 -0
  28. package/components/badge/badge.js +66 -0
  29. package/components/badge/badge.module.css +52 -0
  30. package/components/bar-chart/bar-chart.d.ts +32 -0
  31. package/components/bar-chart/bar-chart.js +327 -0
  32. package/components/bar-chart/bar-chart.module.css +49 -0
  33. package/components/billing-toggle/billing-toggle.d.ts +35 -0
  34. package/components/billing-toggle/billing-toggle.js +83 -0
  35. package/components/billing-toggle/billing-toggle.module.css +139 -0
  36. package/components/bottom-sheet/bottom-sheet.d.ts +27 -0
  37. package/components/bottom-sheet/bottom-sheet.js +386 -0
  38. package/components/bottom-sheet/bottom-sheet.module.css +69 -0
  39. package/components/breadcrumb/breadcrumb.d.ts +13 -0
  40. package/components/breadcrumb/breadcrumb.js +19 -0
  41. package/components/breadcrumb/breadcrumb.module.css +12 -0
  42. package/components/brush-chart/brush-chart.d.ts +45 -0
  43. package/components/brush-chart/brush-chart.js +426 -0
  44. package/components/brush-chart/brush-chart.module.css +69 -0
  45. package/components/button/button.d.ts +10 -0
  46. package/components/button/button.js +97 -0
  47. package/components/button/button.module.css +39 -0
  48. package/components/button-group/button-group.d.ts +59 -0
  49. package/components/button-group/button-group.js +280 -0
  50. package/components/button-group/button-group.module.css +87 -0
  51. package/components/button-link/button-link.d.ts +24 -0
  52. package/components/button-link/button-link.js +11 -0
  53. package/components/button-link/button-link.module.css +7 -0
  54. package/components/calendar/calendar.d.ts +22 -0
  55. package/components/calendar/calendar.js +253 -0
  56. package/components/calendar/calendar.module.css +51 -0
  57. package/components/card/card.d.ts +19 -0
  58. package/components/card/card.js +108 -0
  59. package/components/card/card.module.css +51 -0
  60. package/components/card-stack/card-stack.d.ts +34 -0
  61. package/components/card-stack/card-stack.js +291 -0
  62. package/components/card-stack/card-stack.module.css +63 -0
  63. package/components/carousel/carousel.d.ts +28 -0
  64. package/components/carousel/carousel.js +332 -0
  65. package/components/carousel/carousel.module.css +42 -0
  66. package/components/chat-thread/chat-thread.d.ts +135 -0
  67. package/components/chat-thread/chat-thread.js +406 -0
  68. package/components/chat-thread/chat-thread.module.css +242 -0
  69. package/components/checkbox/checkbox.d.ts +7 -0
  70. package/components/checkbox/checkbox.js +23 -0
  71. package/components/checkbox/checkbox.module.css +15 -0
  72. package/components/chip-group/chip-group.d.ts +24 -0
  73. package/components/chip-group/chip-group.js +196 -0
  74. package/components/chip-group/chip-group.module.css +38 -0
  75. package/components/code-block/code-block.d.ts +11 -0
  76. package/components/code-block/code-block.js +133 -0
  77. package/components/code-block/code-block.module.css +43 -0
  78. package/components/color-picker/color-picker.d.ts +40 -0
  79. package/components/color-picker/color-picker.js +449 -0
  80. package/components/color-picker/color-picker.module.css +111 -0
  81. package/components/combobox/combobox.d.ts +19 -0
  82. package/components/combobox/combobox.js +167 -0
  83. package/components/combobox/combobox.module.css +28 -0
  84. package/components/command-palette/command-palette.d.ts +20 -0
  85. package/components/command-palette/command-palette.js +151 -0
  86. package/components/command-palette/command-palette.module.css +94 -0
  87. package/components/comment-thread/comment-thread.d.ts +120 -0
  88. package/components/comment-thread/comment-thread.js +343 -0
  89. package/components/comment-thread/comment-thread.module.css +195 -0
  90. package/components/confirm-morph/confirm-morph.d.ts +65 -0
  91. package/components/confirm-morph/confirm-morph.js +355 -0
  92. package/components/confirm-morph/confirm-morph.module.css +146 -0
  93. package/components/context-menu/context-menu.d.ts +18 -0
  94. package/components/context-menu/context-menu.js +148 -0
  95. package/components/context-menu/context-menu.module.css +17 -0
  96. package/components/copy-button/copy-button.d.ts +10 -0
  97. package/components/copy-button/copy-button.js +69 -0
  98. package/components/copy-button/copy-button.module.css +21 -0
  99. package/components/countdown/countdown.d.ts +31 -0
  100. package/components/countdown/countdown.js +177 -0
  101. package/components/countdown/countdown.module.css +49 -0
  102. package/components/date-picker/date-picker.d.ts +17 -0
  103. package/components/date-picker/date-picker.js +100 -0
  104. package/components/date-picker/date-picker.module.css +34 -0
  105. package/components/date-range-picker/date-range-picker.d.ts +44 -0
  106. package/components/date-range-picker/date-range-picker.js +534 -0
  107. package/components/date-range-picker/date-range-picker.module.css +214 -0
  108. package/components/dialog/dialog.d.ts +11 -0
  109. package/components/dialog/dialog.js +52 -0
  110. package/components/dialog/dialog.module.css +36 -0
  111. package/components/donut-chart/donut-chart.d.ts +46 -0
  112. package/components/donut-chart/donut-chart.js +382 -0
  113. package/components/donut-chart/donut-chart.module.css +48 -0
  114. package/components/drawer/drawer.d.ts +20 -0
  115. package/components/drawer/drawer.js +111 -0
  116. package/components/drawer/drawer.module.css +97 -0
  117. package/components/dropdown-menu/dropdown-menu.d.ts +16 -0
  118. package/components/dropdown-menu/dropdown-menu.js +39 -0
  119. package/components/dropdown-menu/dropdown-menu.module.css +38 -0
  120. package/components/elastic-slider/elastic-slider.d.ts +39 -0
  121. package/components/elastic-slider/elastic-slider.js +261 -0
  122. package/components/elastic-slider/elastic-slider.module.css +40 -0
  123. package/components/empty-state/empty-state.d.ts +12 -0
  124. package/components/empty-state/empty-state.js +69 -0
  125. package/components/empty-state/empty-state.module.css +13 -0
  126. package/components/expandable-card/expandable-card.d.ts +13 -0
  127. package/components/expandable-card/expandable-card.js +89 -0
  128. package/components/expandable-card/expandable-card.module.css +22 -0
  129. package/components/expanding-button-group/expanding-button-group.d.ts +41 -0
  130. package/components/expanding-button-group/expanding-button-group.js +258 -0
  131. package/components/expanding-button-group/expanding-button-group.module.css +44 -0
  132. package/components/expanding-search/expanding-search.d.ts +38 -0
  133. package/components/expanding-search/expanding-search.js +242 -0
  134. package/components/expanding-search/expanding-search.module.css +52 -0
  135. package/components/file-dropzone/file-dropzone.d.ts +44 -0
  136. package/components/file-dropzone/file-dropzone.js +379 -0
  137. package/components/file-dropzone/file-dropzone.module.css +115 -0
  138. package/components/file-upload/file-upload.d.ts +22 -0
  139. package/components/file-upload/file-upload.js +192 -0
  140. package/components/file-upload/file-upload.module.css +49 -0
  141. package/components/filter-toolbar/filter-toolbar.d.ts +48 -0
  142. package/components/filter-toolbar/filter-toolbar.js +446 -0
  143. package/components/filter-toolbar/filter-toolbar.module.css +70 -0
  144. package/components/floating-button-group/floating-button-group.d.ts +51 -0
  145. package/components/floating-button-group/floating-button-group.js +215 -0
  146. package/components/floating-button-group/floating-button-group.module.css +74 -0
  147. package/components/gauge/gauge.d.ts +18 -0
  148. package/components/gauge/gauge.js +101 -0
  149. package/components/gauge/gauge.module.css +22 -0
  150. package/components/hold-to-confirm/hold-to-confirm.d.ts +25 -0
  151. package/components/hold-to-confirm/hold-to-confirm.js +165 -0
  152. package/components/hold-to-confirm/hold-to-confirm.module.css +20 -0
  153. package/components/hover-card/hover-card.d.ts +38 -0
  154. package/components/hover-card/hover-card.js +145 -0
  155. package/components/hover-card/hover-card.module.css +20 -0
  156. package/components/image-compare/image-compare.d.ts +27 -0
  157. package/components/image-compare/image-compare.js +267 -0
  158. package/components/image-compare/image-compare.module.css +25 -0
  159. package/components/in-view-title/in-view-title.d.ts +17 -0
  160. package/components/in-view-title/in-view-title.js +70 -0
  161. package/components/in-view-title/in-view-title.module.css +13 -0
  162. package/components/inline-edit/inline-edit.d.ts +27 -0
  163. package/components/inline-edit/inline-edit.js +315 -0
  164. package/components/inline-edit/inline-edit.module.css +73 -0
  165. package/components/input/input.d.ts +7 -0
  166. package/components/input/input.js +60 -0
  167. package/components/input/input.module.css +22 -0
  168. package/components/json-viewer/json-viewer.d.ts +48 -0
  169. package/components/json-viewer/json-viewer.js +463 -0
  170. package/components/json-viewer/json-viewer.module.css +92 -0
  171. package/components/line-chart/line-chart.d.ts +58 -0
  172. package/components/line-chart/line-chart.js +383 -0
  173. package/components/line-chart/line-chart.module.css +60 -0
  174. package/components/mention-input/mention-input.d.ts +72 -0
  175. package/components/mention-input/mention-input.js +417 -0
  176. package/components/mention-input/mention-input.module.css +134 -0
  177. package/components/metric-card/metric-card.d.ts +8 -0
  178. package/components/metric-card/metric-card.js +44 -0
  179. package/components/metric-card/metric-card.module.css +18 -0
  180. package/components/money-input/money-input.d.ts +48 -0
  181. package/components/money-input/money-input.js +503 -0
  182. package/components/money-input/money-input.module.css +77 -0
  183. package/components/morph-select/morph-select.d.ts +50 -0
  184. package/components/morph-select/morph-select.js +395 -0
  185. package/components/morph-select/morph-select.module.css +88 -0
  186. package/components/multi-select/multi-select.d.ts +18 -0
  187. package/components/multi-select/multi-select.js +105 -0
  188. package/components/multi-select/multi-select.module.css +37 -0
  189. package/components/notification-center/notification-center.d.ts +24 -0
  190. package/components/notification-center/notification-center.js +146 -0
  191. package/components/notification-center/notification-center.module.css +79 -0
  192. package/components/number-field/number-field.d.ts +41 -0
  193. package/components/number-field/number-field.js +455 -0
  194. package/components/number-field/number-field.module.css +71 -0
  195. package/components/otp-input/otp-input.d.ts +13 -0
  196. package/components/otp-input/otp-input.js +129 -0
  197. package/components/otp-input/otp-input.module.css +28 -0
  198. package/components/pagination/pagination.d.ts +7 -0
  199. package/components/pagination/pagination.js +60 -0
  200. package/components/pagination/pagination.module.css +16 -0
  201. package/components/password-field/password-field.d.ts +14 -0
  202. package/components/password-field/password-field.js +51 -0
  203. package/components/password-field/password-field.module.css +27 -0
  204. package/components/password-strength/password-strength.d.ts +107 -0
  205. package/components/password-strength/password-strength.js +161 -0
  206. package/components/password-strength/password-strength.module.css +57 -0
  207. package/components/phone-input/phone-input.d.ts +73 -0
  208. package/components/phone-input/phone-input.js +612 -0
  209. package/components/phone-input/phone-input.module.css +98 -0
  210. package/components/popover/popover.d.ts +6 -0
  211. package/components/popover/popover.js +19 -0
  212. package/components/popover/popover.module.css +32 -0
  213. package/components/progress/progress.d.ts +8 -0
  214. package/components/progress/progress.js +42 -0
  215. package/components/progress/progress.module.css +1 -0
  216. package/components/radio-cards/radio-cards.d.ts +35 -0
  217. package/components/radio-cards/radio-cards.js +110 -0
  218. package/components/radio-cards/radio-cards.module.css +84 -0
  219. package/components/radio-group/radio-group.d.ts +13 -0
  220. package/components/radio-group/radio-group.js +55 -0
  221. package/components/radio-group/radio-group.module.css +30 -0
  222. package/components/resizable-panels/resizable-panels.d.ts +37 -0
  223. package/components/resizable-panels/resizable-panels.js +338 -0
  224. package/components/resizable-panels/resizable-panels.module.css +40 -0
  225. package/components/rich-text-editor/rich-text-editor.d.ts +55 -0
  226. package/components/rich-text-editor/rich-text-editor.js +1733 -0
  227. package/components/rich-text-editor/rich-text-editor.module.css +201 -0
  228. package/components/ridgeline/ridgeline.d.ts +37 -0
  229. package/components/ridgeline/ridgeline.js +346 -0
  230. package/components/ridgeline/ridgeline.module.css +58 -0
  231. package/components/scroll-area/scroll-area.d.ts +40 -0
  232. package/components/scroll-area/scroll-area.js +153 -0
  233. package/components/scroll-area/scroll-area.module.css +79 -0
  234. package/components/search-field/search-field.d.ts +7 -0
  235. package/components/search-field/search-field.js +20 -0
  236. package/components/search-field/search-field.module.css +17 -0
  237. package/components/segmented-control/segmented-control.d.ts +16 -0
  238. package/components/segmented-control/segmented-control.js +56 -0
  239. package/components/segmented-control/segmented-control.module.css +15 -0
  240. package/components/select/select.d.ts +15 -0
  241. package/components/select/select.js +36 -0
  242. package/components/select/select.module.css +35 -0
  243. package/components/shortcut-recorder/shortcut-recorder.d.ts +108 -0
  244. package/components/shortcut-recorder/shortcut-recorder.js +380 -0
  245. package/components/shortcut-recorder/shortcut-recorder.module.css +85 -0
  246. package/components/signature-pad/signature-pad.d.ts +72 -0
  247. package/components/signature-pad/signature-pad.js +450 -0
  248. package/components/signature-pad/signature-pad.module.css +84 -0
  249. package/components/skeleton/skeleton.d.ts +13 -0
  250. package/components/skeleton/skeleton.js +53 -0
  251. package/components/skeleton/skeleton.module.css +11 -0
  252. package/components/slider/slider.d.ts +45 -0
  253. package/components/slider/slider.js +347 -0
  254. package/components/slider/slider.module.css +45 -0
  255. package/components/slope-chart/slope-chart.d.ts +38 -0
  256. package/components/slope-chart/slope-chart.js +183 -0
  257. package/components/slope-chart/slope-chart.module.css +51 -0
  258. package/components/slot-text/slot-text.d.ts +26 -0
  259. package/components/slot-text/slot-text.js +119 -0
  260. package/components/slot-text/slot-text.module.css +19 -0
  261. package/components/sortable-data-table/sortable-data-table.d.ts +38 -0
  262. package/components/sortable-data-table/sortable-data-table.js +240 -0
  263. package/components/sortable-data-table/sortable-data-table.module.css +116 -0
  264. package/components/sparkline/sparkline.d.ts +19 -0
  265. package/components/sparkline/sparkline.js +239 -0
  266. package/components/sparkline/sparkline.module.css +26 -0
  267. package/components/split-button/split-button.d.ts +19 -0
  268. package/components/split-button/split-button.js +33 -0
  269. package/components/split-button/split-button.module.css +51 -0
  270. package/components/stepper/stepper.d.ts +36 -0
  271. package/components/stepper/stepper.js +108 -0
  272. package/components/stepper/stepper.module.css +74 -0
  273. package/components/streamgraph/streamgraph.d.ts +51 -0
  274. package/components/streamgraph/streamgraph.js +376 -0
  275. package/components/streamgraph/streamgraph.module.css +55 -0
  276. package/components/swipe-actions/swipe-actions.d.ts +35 -0
  277. package/components/swipe-actions/swipe-actions.js +330 -0
  278. package/components/swipe-actions/swipe-actions.module.css +64 -0
  279. package/components/switch/switch.d.ts +7 -0
  280. package/components/switch/switch.js +45 -0
  281. package/components/switch/switch.module.css +12 -0
  282. package/components/tabs/tabs.d.ts +8 -0
  283. package/components/tabs/tabs.js +138 -0
  284. package/components/tabs/tabs.module.css +27 -0
  285. package/components/tag-input/tag-input.d.ts +15 -0
  286. package/components/tag-input/tag-input.js +150 -0
  287. package/components/tag-input/tag-input.module.css +34 -0
  288. package/components/text-morph/text-morph.d.ts +13 -0
  289. package/components/text-morph/text-morph.js +69 -0
  290. package/components/text-morph/text-morph.module.css +8 -0
  291. package/components/text-reveal/text-reveal.d.ts +14 -0
  292. package/components/text-reveal/text-reveal.js +20 -0
  293. package/components/text-reveal/text-reveal.module.css +13 -0
  294. package/components/text-shimmer/text-shimmer.d.ts +18 -0
  295. package/components/text-shimmer/text-shimmer.js +90 -0
  296. package/components/text-shimmer/text-shimmer.module.css +9 -0
  297. package/components/textarea/textarea.d.ts +7 -0
  298. package/components/textarea/textarea.js +56 -0
  299. package/components/textarea/textarea.module.css +22 -0
  300. package/components/theme-switch/theme-switch.d.ts +11 -0
  301. package/components/theme-switch/theme-switch.js +37 -0
  302. package/components/theme-switch/theme-switch.module.css +31 -0
  303. package/components/time-picker/time-picker.d.ts +13 -0
  304. package/components/time-picker/time-picker.js +95 -0
  305. package/components/time-picker/time-picker.module.css +23 -0
  306. package/components/timeline/timeline.d.ts +47 -0
  307. package/components/timeline/timeline.js +171 -0
  308. package/components/timeline/timeline.module.css +51 -0
  309. package/components/toast/toast.d.ts +8 -0
  310. package/components/toast/toast.js +118 -0
  311. package/components/toast/toast.module.css +100 -0
  312. package/components/toast-stack/toast-stack.d.ts +57 -0
  313. package/components/toast-stack/toast-stack.js +399 -0
  314. package/components/toast-stack/toast-stack.module.css +61 -0
  315. package/components/tooltip/tooltip.d.ts +8 -0
  316. package/components/tooltip/tooltip.js +62 -0
  317. package/components/tooltip/tooltip.module.css +18 -0
  318. package/components/tree-view/tree-view.d.ts +17 -0
  319. package/components/tree-view/tree-view.js +147 -0
  320. package/components/tree-view/tree-view.module.css +22 -0
  321. package/components/treemap/treemap.d.ts +35 -0
  322. package/components/treemap/treemap.js +375 -0
  323. package/components/treemap/treemap.module.css +59 -0
  324. package/components/usage-meter/usage-meter.d.ts +24 -0
  325. package/components/usage-meter/usage-meter.js +296 -0
  326. package/components/usage-meter/usage-meter.module.css +61 -0
  327. package/components/user-menu/user-menu.d.ts +62 -0
  328. package/components/user-menu/user-menu.js +316 -0
  329. package/components/user-menu/user-menu.module.css +97 -0
  330. package/components/waffle-chart/waffle-chart.d.ts +39 -0
  331. package/components/waffle-chart/waffle-chart.js +244 -0
  332. package/components/waffle-chart/waffle-chart.module.css +52 -0
  333. package/components.json +1330 -0
  334. package/lib/escape.d.ts +19 -0
  335. package/lib/escape.js +50 -0
  336. package/lib/media.d.ts +353 -0
  337. package/lib/media.js +65 -0
  338. package/lib/menu-highlight.d.ts +28 -0
  339. package/lib/menu-highlight.js +43 -0
  340. package/lib/motion-tokens.d.ts +60 -0
  341. package/lib/motion-tokens.js +25 -0
  342. package/lib/return-focus.d.ts +9 -0
  343. package/lib/return-focus.js +53 -0
  344. package/lib/silicon-provider.d.ts +60 -0
  345. package/lib/silicon-provider.js +47 -0
  346. package/lib/use-copy-feedback.d.ts +8 -0
  347. package/lib/use-copy-feedback.js +36 -0
  348. package/lib/use-today.d.ts +2 -0
  349. package/lib/use-today.js +29 -0
  350. package/package.json +66 -4
  351. package/styles.css +218 -0
  352. package/styles.css.d.ts +1 -0
@@ -0,0 +1,48 @@
1
+ import type { FocusEvent as ReactFocusEvent } from "react";
2
+ export interface MoneyInputDetails {
3
+ currency: string;
4
+ /** The amount in major units, such as 12.5 for $12.50. */
5
+ major: number | null;
6
+ /** The amount as a full currency string, such as "$12.50". */
7
+ formatted: string;
8
+ }
9
+ export interface MoneyInputProps {
10
+ label: string;
11
+ hideLabel?: boolean;
12
+ /** Amount in minor units (cents for USD, yen for JPY). `null` is empty. */
13
+ value?: number | null;
14
+ defaultValue?: number | null;
15
+ onValueChange?: (value: number | null, details: MoneyInputDetails) => void;
16
+ /** ISO 4217 code, such as "USD". */
17
+ currency?: string;
18
+ defaultCurrency?: string;
19
+ onCurrencyChange?: (currency: string) => void;
20
+ /** Codes offered by the currency menu. Pass one code to hide the menu. */
21
+ currencies?: string[];
22
+ /** Lowest amount in minor units. Checked when the field loses focus. */
23
+ min?: number;
24
+ /** Highest amount in minor units. Typing past it is refused with a nudge. */
25
+ max?: number;
26
+ /** Quick add chips in major units. Pass an empty array to hide them. */
27
+ quickAdd?: number[];
28
+ /** Arrow keys move by this many minor units. Defaults to one major unit; Shift moves ten times as far. */
29
+ step?: number;
30
+ /** Formatting locale. Fixed by default so server and client render the same digits. */
31
+ locale?: string;
32
+ description?: string;
33
+ /** Replaces the built-in range message. */
34
+ error?: string;
35
+ disabled?: boolean;
36
+ /** Adds a hidden input carrying the amount in minor units. */
37
+ name?: string;
38
+ id?: string;
39
+ className?: string;
40
+ onBlur?: (event: ReactFocusEvent<HTMLInputElement>) => void;
41
+ }
42
+ /**
43
+ * A currency amount field. Digits group as they are typed without moving the caret, the missing cents wait as faint ghosts
44
+ * so the amount never changes width when it settles, long amounts scale down to fit instead of scrolling, and quick add chips
45
+ * and arrow keys roll the digits. The value comes out in minor units, so $12.50 is 1250.
46
+ */
47
+ export declare const MoneyInput: import("react").ForwardRefExoticComponent<MoneyInputProps & import("react").RefAttributes<HTMLInputElement>>;
48
+ export default MoneyInput;
@@ -0,0 +1,503 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { forwardRef, useCallback, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useReducer, useRef, useState, useSyncExternalStore } from "react";
4
+ import { AnimatePresence, animate, motion, useMotionValue, useReducedMotion } from "motion/react";
5
+ import { Check, ChevronDown } from "lucide-react";
6
+ import { motionTokens } from "../../lib/motion-tokens.js";
7
+ import styles from "./money-input.module.css";
8
+ const enterEase = [...motionTokens.ease.enter];
9
+ const standardEase = [...motionTokens.ease.standard];
10
+ const { spring, blur, duration } = motionTokens;
11
+ const physical = (visualDuration, bounce) => {
12
+ const root = (2 * Math.PI) / (visualDuration * 1.2);
13
+ return { type: "spring", stiffness: root * root, damping: 2 * (1 - bounce) * root, mass: 1 };
14
+ };
15
+ const GLIDE = physical(.28, .08);
16
+ /** A sideways kick when a limit refuses the change: the amount strains toward the press and springs home. */
17
+ const KICK = physical(.42, .3);
18
+ const MAX_INTEGER_DIGITS = 12;
19
+ const subscribe = () => () => { };
20
+ function useReducedFlag() {
21
+ const hydrated = useSyncExternalStore(subscribe, () => true, () => false);
22
+ return !!useReducedMotion() && hydrated;
23
+ }
24
+ const infoCache = new Map();
25
+ function currencyInfo(code, locale) {
26
+ const cacheKey = `${locale}:${code}`;
27
+ const cached = infoCache.get(cacheKey);
28
+ if (cached)
29
+ return cached;
30
+ const full = new Intl.NumberFormat(locale, { style: "currency", currency: code, currencyDisplay: "narrowSymbol", numberingSystem: "latn" });
31
+ const parts = full.formatToParts(1);
32
+ const symbolAt = parts.findIndex(part => part.type === "currency"), numberAt = parts.findIndex(part => part.type === "integer");
33
+ const digits = full.resolvedOptions().maximumFractionDigits ?? 2;
34
+ let name = code;
35
+ try {
36
+ name = new Intl.DisplayNames([locale], { type: "currency" }).of(code) ?? code;
37
+ }
38
+ catch { /* older engines keep the code */ }
39
+ const info = {
40
+ code, symbol: parts[symbolAt]?.value ?? code, position: symbolAt < numberAt ? "prefix" : "suffix", digits, name: name.charAt(0).toUpperCase() + name.slice(1), full,
41
+ whole: new Intl.NumberFormat(locale, { style: "currency", currency: code, currencyDisplay: "narrowSymbol", maximumFractionDigits: 0, numberingSystem: "latn" }),
42
+ };
43
+ infoCache.set(cacheKey, info);
44
+ return info;
45
+ }
46
+ function localeSymbols(locale) {
47
+ const parts = new Intl.NumberFormat(locale, { numberingSystem: "latn" }).formatToParts(12345.6);
48
+ return { group: parts.find(part => part.type === "group")?.value ?? ",", decimal: parts.find(part => part.type === "decimal")?.value ?? "." };
49
+ }
50
+ /* A draft is the amount as typed in a canonical form: digits with an optional ".", such as "1234.5". */
51
+ const pow10 = (digits) => 10 ** digits;
52
+ function toMinor(draft, digits) {
53
+ if (!draft)
54
+ return null;
55
+ const [whole, fraction = ""] = draft.split(".");
56
+ return Number(whole || "0") * pow10(digits) + Number(fraction.padEnd(digits, "0").slice(0, digits) || "0");
57
+ }
58
+ function fromMinor(minor, digits) {
59
+ if (minor === null || minor === undefined || !Number.isFinite(minor))
60
+ return "";
61
+ const text = String(Math.round(Math.abs(minor))).padStart(digits + 1, "0");
62
+ return digits ? `${text.slice(0, text.length - digits)}.${text.slice(text.length - digits)}` : text;
63
+ }
64
+ /** Settles a draft the way it will be read back: fraction padded to the currency, leading zeros gone. */
65
+ function settle(draft, digits) { return draft ? fromMinor(toMinor(draft, digits), digits) : ""; }
66
+ /** Carries an amount across currencies with different minor units, keeping the major amount. */
67
+ function convertDraft(draft, digits) {
68
+ if (!draft)
69
+ return "";
70
+ const [whole = "", fraction] = draft.split(".");
71
+ if (!digits)
72
+ return whole || "0";
73
+ return fraction === undefined ? whole : `${whole}.${fraction.slice(0, digits)}`;
74
+ }
75
+ function groupInteger(whole, group) { return whole.replace(/\B(?=(\d{3})+(?!\d))/g, group); }
76
+ function displayOf(draft, symbols) {
77
+ if (!draft)
78
+ return "";
79
+ const [whole = "", fraction] = draft.split(".");
80
+ return groupInteger(whole, symbols.group) + (fraction !== undefined ? symbols.decimal + fraction : "");
81
+ }
82
+ /** Columns keyed by place value, so 9 → 10 keeps the ones column the ones column. Missing fraction digits are ghosts that hold their width. */
83
+ function partsOf(draft, digits, symbols) {
84
+ const [whole = "", fraction] = draft ? draft.split(".") : [];
85
+ const parts = [];
86
+ const intDigits = whole || "0";
87
+ const ghostInt = !whole;
88
+ [...intDigits].forEach((char, index) => {
89
+ const place = intDigits.length - 1 - index;
90
+ parts.push({ key: `i${place}`, char, ghost: ghostInt });
91
+ if (place > 0 && place % 3 === 0)
92
+ parts.push({ key: `g${place}`, char: symbols.group, ghost: ghostInt });
93
+ });
94
+ if (digits) {
95
+ parts.push({ key: "d", char: symbols.decimal, ghost: fraction === undefined });
96
+ for (let index = 0; index < digits; index++)
97
+ parts.push({ key: `f${index}`, char: fraction?.[index] ?? "0", ghost: fraction === undefined || index >= fraction.length });
98
+ }
99
+ return parts;
100
+ }
101
+ /* Columns open their width while the digit rises in the direction of change. Typing cuts straight to the result so the caret is never behind. */
102
+ const column = {
103
+ enter: { width: 0, opacity: 0 },
104
+ center: { width: "auto", opacity: 1, transition: { width: spring.morph, opacity: { duration: duration.fast } } },
105
+ exit: { width: 0, opacity: 0, transition: { width: spring.smooth, opacity: { duration: duration.instant } } },
106
+ };
107
+ const columnCut = { enter: { width: "auto", opacity: 1 }, center: { width: "auto", opacity: 1, transition: { duration: 0 } }, exit: { width: 0, opacity: 0, transition: { duration: 0 } } };
108
+ const glyph = {
109
+ enter: (dir) => ({ y: `${dir * .55}em`, opacity: 0, filter: `blur(${blur.soft}px)` }),
110
+ center: { y: 0, opacity: 1, filter: "blur(0px)", transitionEnd: { filter: "none" }, transition: { y: spring.snappy, opacity: { duration: duration.fast, ease: enterEase }, filter: { duration: duration.exit, ease: enterEase } } },
111
+ exit: (dir) => ({ y: `${dir * -.55}em`, opacity: 0, filter: `blur(${blur.subtle}px)`, transition: { duration: duration.instant, ease: standardEase } }),
112
+ };
113
+ const glyphCut = { enter: { y: 0, opacity: 1, filter: "none" }, center: { y: 0, opacity: 1, filter: "none", transition: { duration: 0 } }, exit: { opacity: 0, transition: { duration: 0 } } };
114
+ const glyphFade = { enter: { y: 0, opacity: 0, filter: "none" }, center: { y: 0, opacity: 1, filter: "none", transition: { duration: duration.instant } }, exit: { opacity: 0, transition: { duration: 0 } } };
115
+ function Glyphs({ parts, direction, instant, reduced }) {
116
+ const columnVariants = instant || reduced ? columnCut : column;
117
+ const glyphVariants = instant ? glyphCut : reduced ? glyphFade : glyph;
118
+ return _jsx(AnimatePresence, { initial: false, custom: direction, children: parts.map(part => _jsxs(motion.span, { className: styles.column, "data-ghost": part.ghost || undefined, custom: direction, variants: columnVariants, initial: "enter", animate: "center", exit: "exit", children: [_jsx("span", { className: styles.sizer, children: part.char }), _jsx(AnimatePresence, { initial: false, custom: direction, children: _jsx(motion.span, { className: styles.glyph, custom: direction, variants: glyphVariants, initial: "enter", animate: "center", exit: "exit", children: part.char }, part.char) })] }, part.key)) });
119
+ }
120
+ /** The currency symbol swaps with a short roll while its slot springs to the new width. */
121
+ function SymbolSlot({ symbol, reduced }) {
122
+ const sizerRef = useRef(null);
123
+ const width = useMotionValue("auto");
124
+ const measured = useRef(false);
125
+ useLayoutEffect(() => {
126
+ const node = sizerRef.current;
127
+ if (!node || typeof ResizeObserver === "undefined")
128
+ return;
129
+ const observer = new ResizeObserver(() => {
130
+ if (!measured.current || reduced)
131
+ width.jump(node.offsetWidth);
132
+ else
133
+ animate(width, node.offsetWidth, spring.morph);
134
+ measured.current = true;
135
+ });
136
+ observer.observe(node);
137
+ return () => observer.disconnect();
138
+ }, [reduced, width]);
139
+ return _jsxs(motion.span, { className: styles.symbol, style: { width }, "aria-hidden": "true", children: [_jsx("span", { ref: sizerRef, className: styles.symbolSizer, children: symbol }), _jsx(AnimatePresence, { initial: false, mode: "popLayout", children: _jsx(motion.span, { className: styles.symbolGlyph, initial: reduced ? { opacity: 0 } : { opacity: 0, y: ".4em", filter: `blur(${blur.soft}px)` }, animate: { opacity: 1, y: 0, filter: "blur(0px)" }, exit: { opacity: 0, y: reduced ? 0 : "-.3em", transition: { duration: duration.instant } }, transition: reduced ? { duration: duration.instant } : { y: spring.snappy, opacity: { duration: duration.fast }, filter: { duration: duration.exit } }, children: symbol }, symbol) })] });
140
+ }
141
+ const isSignificant = (char, decimal) => /\d/.test(char) || char === decimal;
142
+ function positionAfter(text, count, decimal) {
143
+ if (count <= 0)
144
+ return 0;
145
+ let seen = 0;
146
+ for (let index = 0; index < text.length; index++)
147
+ if (isSignificant(text[index], decimal) && ++seen === count)
148
+ return index + 1;
149
+ return text.length;
150
+ }
151
+ /**
152
+ * A currency amount field. Digits group as they are typed without moving the caret, the missing cents wait as faint ghosts
153
+ * so the amount never changes width when it settles, long amounts scale down to fit instead of scrolling, and quick add chips
154
+ * and arrow keys roll the digits. The value comes out in minor units, so $12.50 is 1250.
155
+ */
156
+ export const MoneyInput = forwardRef(function MoneyInput({ label, hideLabel = false, value, defaultValue = null, onValueChange, currency: currencyProp, defaultCurrency = "USD", onCurrencyChange, currencies = ["USD", "EUR", "GBP", "JPY", "CAD", "AUD", "CHF", "INR"], min, max, quickAdd = [10, 50, 100], step: stepProp, locale = "en-US", description, error, disabled = false, name, id, className, onBlur, }, forwardedRef) {
157
+ const reduced = useReducedFlag();
158
+ const uid = useId();
159
+ const inputId = id ?? `${uid}-amount`;
160
+ const hintId = `${inputId}-hint`, errorId = `${inputId}-error`, rangeId = `${inputId}-range`;
161
+ const [innerCurrency, setInnerCurrency] = useState(defaultCurrency);
162
+ const code = currencyProp ?? innerCurrency;
163
+ const info = useMemo(() => currencyInfo(code, locale), [code, locale]);
164
+ const symbols = useMemo(() => localeSymbols(locale), [locale]);
165
+ const step = stepProp && stepProp > 0 ? stepProp : pow10(info.digits);
166
+ const [draft, setDraft] = useState(() => fromMinor(value !== undefined ? value : defaultValue, info.digits));
167
+ const minor = toMinor(draft, info.digits);
168
+ const [seenValue, setSeenValue] = useState(value);
169
+ const [source, setSource] = useState("external");
170
+ const [direction, setDirection] = useState(1);
171
+ if (value !== seenValue) {
172
+ setSeenValue(value);
173
+ if (value !== undefined && value !== minor) {
174
+ setDraft(fromMinor(value, info.digits));
175
+ setSource("external");
176
+ setDirection((value ?? 0) >= (minor ?? 0) ? 1 : -1);
177
+ }
178
+ }
179
+ const [focused, setFocused] = useState(false);
180
+ const [touched, setTouched] = useState(false);
181
+ const [limitFlash, setLimitFlash] = useState(0);
182
+ const [announcement, setAnnouncement] = useState("");
183
+ const [, rerender] = useReducer((count) => count + 1, 0);
184
+ const inputRef = useRef(null);
185
+ useImperativeHandle(forwardedRef, () => inputRef.current);
186
+ const pendingCaret = useRef(null);
187
+ const latest = useRef({ minor, draft });
188
+ useLayoutEffect(() => { latest.current = { minor, draft }; });
189
+ const money = useCallback((amount) => amount === null ? "" : info.full.format(amount / pow10(info.digits)), [info]);
190
+ const display = displayOf(draft, symbols);
191
+ const parts = useMemo(() => partsOf(draft, info.digits, symbols), [draft, info.digits, symbols]);
192
+ /* ---------------------------------------------- Limits and feedback ---------------------------------------------- */
193
+ const bumpX = useMotionValue(0);
194
+ const flashTimer = useRef(undefined);
195
+ useEffect(() => () => window.clearTimeout(flashTimer.current), []);
196
+ function refuse(toward, limit) {
197
+ if (!reduced)
198
+ animate(bumpX, 0, { ...KICK, velocity: toward * 160 });
199
+ setLimitFlash(count => count + 1);
200
+ window.clearTimeout(flashTimer.current);
201
+ flashTimer.current = window.setTimeout(() => setLimitFlash(0), 1400);
202
+ setAnnouncement(`${toward > 0 ? "Maximum" : "Minimum"} is ${money(limit)}`);
203
+ }
204
+ function emit(nextMinor, nextInfo = info) {
205
+ onValueChange?.(nextMinor, { currency: nextInfo.code, major: nextMinor === null ? null : nextMinor / pow10(nextInfo.digits), formatted: nextMinor === null ? "" : nextInfo.full.format(nextMinor / pow10(nextInfo.digits)) });
206
+ }
207
+ function setAmount(nextDraft, nextSource) {
208
+ const nextMinor = toMinor(nextDraft, info.digits);
209
+ setDirection((nextMinor ?? 0) >= (latest.current.minor ?? 0) ? 1 : -1);
210
+ setSource(nextSource);
211
+ setDraft(nextDraft);
212
+ latest.current = { minor: nextMinor, draft: nextDraft };
213
+ if (nextMinor !== minor)
214
+ emit(nextMinor);
215
+ if (nextSource !== "type")
216
+ setAnnouncement(money(nextMinor));
217
+ }
218
+ /* ---------------------------------------------- Typing ---------------------------------------------- */
219
+ /** Reads what the input holds into a draft and counts the digits and decimal before the caret, so the caret can return to the same spot. */
220
+ function sanitize(raw, caret) {
221
+ let out = "", before = 0, dot = false;
222
+ for (let index = 0; index < raw.length; index++) {
223
+ const char = raw[index];
224
+ let kept = "";
225
+ if (/\d/.test(char))
226
+ kept = char;
227
+ else if ((char === symbols.decimal || (char === "." && symbols.group !== ".")) && info.digits > 0 && !dot) {
228
+ kept = ".";
229
+ dot = true;
230
+ }
231
+ if (!kept)
232
+ continue;
233
+ out += kept;
234
+ if (index < caret)
235
+ before++;
236
+ }
237
+ const pieces = out.split(".");
238
+ let whole = pieces[0];
239
+ const fraction = pieces[1];
240
+ const stripped = whole.length - (whole.replace(/^0+/, "").length);
241
+ whole = whole.replace(/^0+/, "");
242
+ let shift = -Math.min(stripped, before);
243
+ if (fraction !== undefined && !whole) {
244
+ whole = "0";
245
+ shift += 1;
246
+ }
247
+ if (fraction === undefined && !whole && stripped) {
248
+ whole = "0";
249
+ shift += 1;
250
+ }
251
+ if (fraction !== undefined && fraction.length > info.digits)
252
+ return null;
253
+ if (whole.length > MAX_INTEGER_DIGITS)
254
+ return null;
255
+ return { draft: fraction !== undefined ? `${whole}.${fraction}` : whole, before: Math.max(0, before + shift) };
256
+ }
257
+ function applyRaw(raw, caret) {
258
+ const result = sanitize(raw, caret);
259
+ // A refused keystroke keeps the caret where it was: one significant character before where the browser left it.
260
+ if (!result) {
261
+ pendingCaret.current = Math.max(0, [...raw.slice(0, caret)].filter(char => isSignificant(char, symbols.decimal) || char === ".").length - 1);
262
+ rerender();
263
+ return;
264
+ }
265
+ const nextMinor = toMinor(result.draft, info.digits);
266
+ if (max !== undefined && nextMinor !== null && nextMinor > max) {
267
+ pendingCaret.current = Math.max(0, result.before - 1);
268
+ refuse(1, max);
269
+ rerender();
270
+ return;
271
+ }
272
+ pendingCaret.current = result.before;
273
+ setAmount(result.draft, "type");
274
+ }
275
+ useLayoutEffect(() => {
276
+ const input = inputRef.current, count = pendingCaret.current;
277
+ pendingCaret.current = null;
278
+ if (!input || count === null || document.activeElement !== input)
279
+ return;
280
+ const position = positionAfter(input.value, count, symbols.decimal);
281
+ input.setSelectionRange(position, position);
282
+ });
283
+ function onKeyDown(event) {
284
+ const input = event.currentTarget;
285
+ const moves = { ArrowUp: 1, ArrowDown: -1, PageUp: 10, PageDown: -10 };
286
+ if (event.key in moves) {
287
+ event.preventDefault();
288
+ const amount = moves[event.key] * step * (event.shiftKey && Math.abs(moves[event.key]) === 1 ? 10 : 1);
289
+ nudge(amount, "key");
290
+ return;
291
+ }
292
+ if (event.key === "Enter") {
293
+ setDraft(settle(draft, info.digits));
294
+ pendingCaret.current = Number.MAX_SAFE_INTEGER;
295
+ return;
296
+ }
297
+ const start = input.selectionStart ?? 0, end = input.selectionEnd ?? 0;
298
+ if (start !== end || event.metaKey || event.ctrlKey || event.altKey)
299
+ return;
300
+ // Deleting a group separator deletes the digit before it instead of doing nothing.
301
+ if (event.key === "Backspace" && start > 1 && input.value[start - 1] === symbols.group) {
302
+ event.preventDefault();
303
+ applyRaw(input.value.slice(0, start - 2) + input.value.slice(start - 1), start - 2);
304
+ }
305
+ if (event.key === "Delete" && input.value[start] === symbols.group) {
306
+ event.preventDefault();
307
+ applyRaw(input.value.slice(0, start + 1) + input.value.slice(start + 2), start);
308
+ }
309
+ }
310
+ function onPaste(event) {
311
+ const text = event.clipboardData.getData("text").trim();
312
+ if (!text)
313
+ return;
314
+ event.preventDefault();
315
+ // "$1,234.56", "1.234,56 €", "1234.5": a final separator followed by one or two digits is the decimal; the rest are groups.
316
+ const match = /[.,](\d{1,2})\s*\D*$/.exec(text);
317
+ const whole = (match ? text.slice(0, match.index) : text).replace(/\D/g, "");
318
+ const pasted = match && info.digits ? `${whole}.${match[1]}` : whole;
319
+ if (!/\d/.test(pasted))
320
+ return;
321
+ const input = event.currentTarget;
322
+ const start = input.selectionStart ?? 0, end = input.selectionEnd ?? 0;
323
+ const inserted = pasted.replace(".", symbols.decimal);
324
+ const replaceAll = start === 0 && end === input.value.length || !input.value;
325
+ const raw = replaceAll ? inserted : input.value.slice(0, start) + inserted + input.value.slice(end);
326
+ applyRaw(raw, replaceAll ? raw.length : start + inserted.length);
327
+ }
328
+ /* ---------------------------------------------- Steps and chips ---------------------------------------------- */
329
+ function nudge(amount, nextSource) {
330
+ const from = latest.current.minor ?? 0;
331
+ const floor = min ?? 0, ceiling = max ?? Number.MAX_SAFE_INTEGER;
332
+ let next = from + amount;
333
+ if (next > ceiling) {
334
+ if (from >= ceiling) {
335
+ refuse(1, ceiling);
336
+ return;
337
+ }
338
+ next = ceiling;
339
+ refuse(1, ceiling);
340
+ }
341
+ if (next < floor) {
342
+ if (from <= floor && latest.current.minor !== null) {
343
+ refuse(-1, floor);
344
+ return;
345
+ }
346
+ next = floor;
347
+ }
348
+ pendingCaret.current = Number.MAX_SAFE_INTEGER;
349
+ setAmount(fromMinor(next, info.digits), nextSource);
350
+ }
351
+ /* ---------------------------------------------- Fit to width ---------------------------------------------- */
352
+ const wrapRef = useRef(null);
353
+ const groupRef = useRef(null);
354
+ const scale = useMotionValue(1);
355
+ useLayoutEffect(() => {
356
+ const wrap = wrapRef.current, group = groupRef.current;
357
+ if (!wrap || !group || typeof ResizeObserver === "undefined")
358
+ return;
359
+ let first = true;
360
+ const fit = () => {
361
+ const natural = group.offsetWidth, room = wrap.clientWidth;
362
+ const next = natural > 0 && room > 0 ? Math.min(1, room / natural) : 1;
363
+ if (first || reduced)
364
+ scale.jump(next);
365
+ else
366
+ animate(scale, next, spring.smooth);
367
+ first = false;
368
+ };
369
+ const observer = new ResizeObserver(fit);
370
+ observer.observe(wrap);
371
+ observer.observe(group);
372
+ return () => observer.disconnect();
373
+ }, [reduced, scale]);
374
+ /* ---------------------------------------------- Currency menu ---------------------------------------------- */
375
+ const [menuOpen, setMenuOpen] = useState(false);
376
+ const [activeCode, setActiveCode] = useState(code);
377
+ const menuRef = useRef(null);
378
+ const listRef = useRef(null);
379
+ const menuButtonRef = useRef(null);
380
+ const optionRefs = useRef(new Map());
381
+ const options = useMemo(() => currencies.map(entry => currencyInfo(entry, locale)), [currencies, locale]);
382
+ const canSwitch = options.length > 1;
383
+ const openMenu = () => { if (disabled)
384
+ return; setActiveCode(code); setMenuOpen(true); };
385
+ const closeMenu = useCallback((focusButton) => { setMenuOpen(false); if (focusButton)
386
+ menuButtonRef.current?.focus({ preventScroll: true }); }, []);
387
+ useLayoutEffect(() => { if (menuOpen)
388
+ listRef.current?.focus({ preventScroll: true }); }, [menuOpen]);
389
+ useEffect(() => {
390
+ if (!menuOpen)
391
+ return;
392
+ const down = (event) => { if (!menuRef.current?.contains(event.target))
393
+ closeMenu(false); };
394
+ document.addEventListener("pointerdown", down);
395
+ return () => document.removeEventListener("pointerdown", down);
396
+ }, [closeMenu, menuOpen]);
397
+ const hy = useMotionValue(0), hh = useMotionValue(0), ho = useMotionValue(0);
398
+ useLayoutEffect(() => {
399
+ const node = menuOpen ? optionRefs.current.get(activeCode) : undefined;
400
+ if (!node) {
401
+ ho.jump(0);
402
+ return;
403
+ }
404
+ if (ho.get() < .05 || reduced) {
405
+ hy.jump(node.offsetTop);
406
+ hh.jump(node.offsetHeight);
407
+ }
408
+ else {
409
+ animate(hy, node.offsetTop, GLIDE);
410
+ animate(hh, node.offsetHeight, GLIDE);
411
+ }
412
+ ho.jump(1);
413
+ node.scrollIntoView?.({ block: "nearest" });
414
+ }, [activeCode, hh, ho, hy, menuOpen, reduced]);
415
+ function chooseCurrency(next) {
416
+ closeMenu(true);
417
+ if (next.code === code)
418
+ return;
419
+ const nextDraft = focused ? convertDraft(draft, next.digits) : settle(convertDraft(draft, next.digits), next.digits);
420
+ const nextMinor = toMinor(nextDraft, next.digits);
421
+ if (currencyProp === undefined)
422
+ setInnerCurrency(next.code);
423
+ onCurrencyChange?.(next.code);
424
+ setSource("external");
425
+ setDraft(nextDraft);
426
+ latest.current = { minor: nextMinor, draft: nextDraft };
427
+ emit(nextMinor, next);
428
+ setAnnouncement(`${next.name}${nextMinor === null ? "" : `, ${next.full.format(nextMinor / pow10(next.digits))}`}`);
429
+ }
430
+ const typeahead = useRef({ buffer: "", at: 0 });
431
+ function onMenuKeyDown(event) {
432
+ const at = options.findIndex(option => option.code === activeCode);
433
+ const go = (index) => { event.preventDefault(); setActiveCode(options[Math.max(0, Math.min(options.length - 1, index))].code); };
434
+ switch (event.key) {
435
+ case "ArrowDown":
436
+ go(at + 1);
437
+ return;
438
+ case "ArrowUp":
439
+ go(at - 1);
440
+ return;
441
+ case "Home":
442
+ go(0);
443
+ return;
444
+ case "End":
445
+ go(options.length - 1);
446
+ return;
447
+ case "Enter":
448
+ case " ":
449
+ event.preventDefault();
450
+ if (options[at])
451
+ chooseCurrency(options[at]);
452
+ return;
453
+ case "Escape":
454
+ event.preventDefault();
455
+ event.stopPropagation();
456
+ closeMenu(true);
457
+ return;
458
+ case "Tab":
459
+ closeMenu(false);
460
+ return;
461
+ }
462
+ if (event.key.length === 1 && /\w/.test(event.key)) {
463
+ const now = performance.now(), state = typeahead.current;
464
+ state.buffer = now - state.at > 600 ? event.key.toLowerCase() : state.buffer + event.key.toLowerCase();
465
+ state.at = now;
466
+ const hit = options.find(option => option.code.toLowerCase().startsWith(state.buffer) || option.name.toLowerCase().startsWith(state.buffer));
467
+ if (hit) {
468
+ event.preventDefault();
469
+ setActiveCode(hit.code);
470
+ }
471
+ }
472
+ }
473
+ /* ---------------------------------------------- Messages ---------------------------------------------- */
474
+ const range = min !== undefined && max !== undefined ? `${money(min)} to ${money(max)}` : max !== undefined ? `Up to ${money(max)}` : min !== undefined ? `At least ${money(min)}` : "";
475
+ const outOfRange = minor !== null && ((min !== undefined && minor < min) || (max !== undefined && minor > max));
476
+ const belowMin = minor !== null && min !== undefined && minor < min;
477
+ const builtIn = touched && !focused && outOfRange ? belowMin ? `The minimum is ${money(min ?? 0)}` : `The maximum is ${money(max ?? 0)}` : undefined;
478
+ const message = error ?? builtIn;
479
+ const invalid = !!message;
480
+ const describedBy = [range ? rangeId : null, description ? hintId : null, message ? errorId : null].filter(Boolean).join(" ") || undefined;
481
+ const instant = source === "type";
482
+ const chips = quickAdd.filter(amount => amount > 0);
483
+ return _jsxs("div", { className: [styles.field, className].filter(Boolean).join(" "), "data-disabled": disabled || undefined, children: [_jsx("label", { htmlFor: inputId, className: hideLabel ? styles.srOnly : styles.label, children: label }), _jsxs("div", { className: styles.control, "data-invalid": invalid || undefined, "data-focused": focused || undefined, children: [_jsx("div", { ref: wrapRef, className: styles.amountWrap, onMouseDown: event => { if (event.target !== inputRef.current)
484
+ event.preventDefault(); }, onClick: event => { if (!disabled && event.target !== inputRef.current) {
485
+ const input = inputRef.current;
486
+ input?.focus();
487
+ input?.setSelectionRange(input.value.length, input.value.length);
488
+ } }, children: _jsxs(motion.div, { ref: groupRef, className: styles.group, style: { scale, x: bumpX }, children: [info.position === "prefix" && _jsx(SymbolSlot, { symbol: info.symbol, reduced: reduced }), _jsxs("span", { className: styles.number, children: [_jsx("span", { className: styles.mirror, "aria-hidden": "true", "data-empty": !draft || undefined, children: _jsx(Glyphs, { parts: parts, direction: direction, instant: instant, reduced: reduced }) }), _jsx("input", { ref: inputRef, id: inputId, className: styles.input, type: "text", role: "spinbutton", inputMode: info.digits ? "decimal" : "numeric", autoComplete: "off", spellCheck: false, value: display, disabled: disabled, "aria-invalid": invalid || undefined, "aria-describedby": describedBy, "aria-label": hideLabel ? label : undefined, "aria-valuenow": minor === null ? undefined : minor / pow10(info.digits), "aria-valuetext": minor === null ? "Empty" : money(minor), "aria-valuemin": min === undefined ? undefined : min / pow10(info.digits), "aria-valuemax": max === undefined ? undefined : max / pow10(info.digits), onChange: event => applyRaw(event.currentTarget.value, event.currentTarget.selectionStart ?? event.currentTarget.value.length), onKeyDown: onKeyDown, onPaste: onPaste, onFocus: () => setFocused(true), onBlur: event => { setFocused(false); setTouched(true); if (draft)
489
+ setDraft(settle(draft, info.digits)); onBlur?.(event); } })] }), info.position === "suffix" && _jsx(SymbolSlot, { symbol: info.symbol, reduced: reduced })] }) }), canSwitch ? _jsxs("div", { ref: menuRef, className: styles.menuAnchor, children: [_jsxs("button", { ref: menuButtonRef, type: "button", className: styles.currencyButton, disabled: disabled, "aria-haspopup": "listbox", "aria-expanded": menuOpen, "aria-controls": `${uid}-currencies`, "aria-label": `Currency, ${info.name}`, "data-open": menuOpen || undefined, onClick: () => (menuOpen ? closeMenu(true) : openMenu()), onKeyDown: event => { if (event.key === "ArrowDown" || event.key === "ArrowUp") {
490
+ event.preventDefault();
491
+ openMenu();
492
+ } }, children: [_jsx("span", { className: styles.code, children: code }), _jsx(motion.span, { className: styles.chevron, "aria-hidden": "true", initial: false, animate: { rotate: menuOpen ? 180 : 0 }, transition: reduced ? { duration: 0 } : GLIDE, children: _jsx(ChevronDown, { size: 16, strokeWidth: 1.75 }) })] }), _jsx(AnimatePresence, { children: menuOpen && _jsx(motion.div, { className: styles.menu, initial: reduced ? { opacity: 0 } : { opacity: 0, scale: .96, y: -4, filter: `blur(${blur.subtle}px)` }, animate: { opacity: 1, scale: 1, y: 0, filter: "blur(0px)" }, exit: reduced ? { opacity: 0, transition: { duration: duration.instant } } : { opacity: 0, scale: .97, y: -2, filter: `blur(${blur.subtle}px)`, transition: { duration: duration.instant, ease: standardEase } }, transition: reduced ? { duration: duration.instant } : { ...spring.snappy, opacity: { duration: duration.fast, ease: enterEase }, filter: { duration: duration.fast, ease: enterEase } }, children: _jsxs("div", { ref: listRef, id: `${uid}-currencies`, className: styles.list, role: "listbox", tabIndex: -1, "aria-label": "Currency", "aria-activedescendant": `${uid}-cur-${activeCode}`, onKeyDown: onMenuKeyDown, children: [_jsx(motion.span, { className: styles.highlight, style: { y: hy, height: hh, opacity: ho }, "aria-hidden": "true" }), options.map(option => _jsxs("div", { id: `${uid}-cur-${option.code}`, role: "option", "aria-selected": option.code === code, className: styles.option, "data-active": option.code === activeCode || undefined, ref: node => { if (node)
493
+ optionRefs.current.set(option.code, node);
494
+ else
495
+ optionRefs.current.delete(option.code); }, onPointerMove: event => { if (event.pointerType === "mouse" && option.code !== activeCode)
496
+ setActiveCode(option.code); }, onPointerDown: event => event.preventDefault(), onClick: () => chooseCurrency(option), children: [_jsx("span", { className: styles.optionSymbol, "aria-hidden": "true", children: option.symbol }), _jsx("span", { className: styles.optionCode, children: option.code }), _jsx("span", { className: styles.optionName, children: option.name }), _jsx("span", { className: styles.check, "data-on": option.code === code || undefined, "aria-hidden": "true", children: _jsx(Check, { size: 16, strokeWidth: 1.75 }) })] }, option.code))] }) }, "menu") })] }) : _jsx("span", { className: styles.codeStatic, children: code })] }), (chips.length > 0 || range) && _jsxs("div", { className: styles.toolbar, children: [chips.length > 0 && _jsx("div", { className: styles.chips, role: "group", "aria-label": "Quick add", children: chips.map(amount => {
497
+ const add = amount * pow10(info.digits);
498
+ const atMax = max !== undefined && (minor ?? 0) >= max;
499
+ return _jsxs("button", { type: "button", className: styles.chip, disabled: disabled, "aria-disabled": atMax || undefined, "aria-controls": inputId, "aria-label": `Add ${info.whole.format(amount)}`, onClick: () => nudge(add, "chip"), children: ["+", info.whole.format(amount)] }, amount);
500
+ }) }), range && _jsx("span", { id: rangeId, className: styles.range, "data-flash": limitFlash > 0 || undefined, children: range })] }), _jsx(AnimatePresence, { initial: false, children: (message || description) && _jsx(motion.span, { className: styles.messageSlot, initial: reduced ? false : { height: 0, opacity: 0 }, animate: { height: "auto", opacity: 1 }, exit: { height: 0, opacity: 0, transition: reduced ? { duration: 0 } : { height: spring.smooth, opacity: { duration: duration.instant } } }, transition: reduced ? { duration: 0 } : { height: spring.smooth, opacity: { duration: duration.fast } }, children: _jsx(AnimatePresence, { initial: false, mode: "popLayout", children: _jsx(motion.span, { id: message ? errorId : hintId, className: message ? styles.error : styles.hint, role: message ? "alert" : undefined, initial: reduced ? { opacity: 0 } : { opacity: 0, y: "0.35em", filter: `blur(${blur.soft}px)` }, animate: { opacity: 1, y: 0, filter: "blur(0px)" }, exit: { opacity: 0, transition: { duration: duration.instant } }, transition: { duration: reduced ? duration.instant : duration.standard, ease: enterEase }, children: message ?? description }, message ?? description) }) }, "message") }), name && _jsx("input", { type: "hidden", name: name, value: minor ?? "" }), _jsx("span", { className: styles.srOnly, role: "status", "aria-live": "polite", children: announcement })] });
501
+ });
502
+ MoneyInput.displayName = "MoneyInput";
503
+ export default MoneyInput;
@@ -0,0 +1,77 @@
1
+ /* No row gap: grid tracks clamp a negative margin at zero, so a closed message row would still pay the gap. Each row carries its own space instead. */
2
+ .field { display: grid; min-width: 0; font-family: var(--font-body); letter-spacing: var(--tracking-body); }
3
+ .label { justify-self: start; margin-bottom: var(--space-related); color: var(--foreground); font-size: var(--text-sm); font-weight: 500; line-height: var(--leading-body); }
4
+
5
+ /* One tall shell: the amount on the left, the currency on the right. Set --money-input-height to resize it. */
6
+ .control { position: relative; display: flex; min-width: 0; height: var(--money-input-height, 76px); box-sizing: border-box; align-items: center; gap: var(--space-related); padding: 0 8px 0 18px; border: 1px solid var(--border-strong); border-radius: var(--radius-container); corner-shape: var(--corner-shape); background: var(--surface); transition: border-color var(--duration-fast) var(--ease-standard); }
7
+ @media (hover: hover) and (pointer: fine) { .control:hover:not([data-invalid]) { border-color: var(--foreground); } }
8
+ .control[data-focused] { border-color: var(--foreground); }
9
+ .control[data-invalid] { border-color: var(--danger); }
10
+ .field[data-disabled] .control { opacity: .5; background: var(--surface-muted); }
11
+
12
+ /* The amount scales down to fit rather than scroll, so a long number is always read whole. */
13
+ .amountWrap { position: relative; display: flex; min-width: 0; height: 100%; flex: 1; align-items: center; overflow: hidden; cursor: text; }
14
+ .field[data-disabled] .amountWrap { cursor: not-allowed; }
15
+ .group { display: inline-flex; flex: none; align-items: baseline; transform-origin: 0 50%; white-space: nowrap; color: var(--foreground); font-family: var(--font-display); font-size: var(--text-3xl); font-weight: 500; line-height: 1.2; letter-spacing: var(--tracking-display); font-variant-numeric: tabular-nums; font-kerning: none; }
16
+
17
+ .symbol { position: relative; display: inline-block; margin-right: .06em; color: var(--text-secondary); white-space: pre; }
18
+ .group > .symbol:last-child { margin-right: 0; margin-left: .18em; }
19
+ .symbolSizer { position: absolute; top: 0; left: 0; visibility: hidden; white-space: pre; }
20
+ .symbolGlyph { display: inline-block; white-space: pre; }
21
+
22
+ /* The input only draws the caret and selection. Its text is always painted by the mirror, so settled, rolling, and typed amounts share one position. */
23
+ .number { position: relative; display: inline-flex; }
24
+ .mirror { display: inline-flex; }
25
+ .input { position: absolute; z-index: 1; top: 0; left: 0; width: calc(100% + .5em); height: 100%; margin: 0; padding: 0; border: 0; background: transparent; color: transparent; caret-color: var(--foreground); font: inherit; font-variant-numeric: inherit; font-kerning: none; letter-spacing: inherit; line-height: inherit; outline: none; }
26
+ .input::selection { background: color-mix(in oklab, var(--foreground) 16%, transparent); color: transparent; }
27
+ .input:disabled { cursor: not-allowed; }
28
+
29
+ /* Each character is a column: a hidden sizer holds its width and the glyph rolls inside, feathered at the top and bottom edge. */
30
+ .column { --feather: .18em; position: relative; display: inline-block; overflow-x: visible; overflow-y: clip; margin-block: calc(var(--feather) * -1); padding-block: var(--feather); mask-image: linear-gradient(to bottom, transparent, #000 calc(var(--feather) * 1.4), #000 calc(100% - var(--feather) * 1.4), transparent); transition: color var(--duration-standard) var(--ease-standard); }
31
+ /* Missing cents wait as ghosts, so settling the amount only changes their color. */
32
+ .column[data-ghost] { color: color-mix(in oklab, var(--text-muted) 55%, transparent); }
33
+ .sizer { visibility: hidden; white-space: pre; }
34
+ .glyph { position: absolute; inset: var(--feather) 0; text-align: center; white-space: pre; }
35
+
36
+ /* Currency switch: plain text on the shell, a quiet fill on hover and while open. It answers a press with color, since it anchors a menu. */
37
+ .menuAnchor { position: relative; flex: none; }
38
+ .currencyButton { display: inline-flex; height: 36px; align-items: center; gap: 4px; padding: 0 8px 0 12px; border: 0; border-radius: var(--radius-full); corner-shape: round; background: transparent; color: var(--foreground); cursor: pointer; font: inherit; font-size: var(--text-sm); font-weight: 500; -webkit-tap-highlight-color: transparent; transition: background-color var(--duration-fast) var(--ease-standard); }
39
+ @media (hover: hover) and (pointer: fine) { .currencyButton:hover:not(:disabled) { background: var(--surface-muted); } }
40
+ .currencyButton:active:not(:disabled), .currencyButton[data-open] { background: var(--surface-muted); }
41
+ .currencyButton:disabled { cursor: not-allowed; }
42
+ .code { font-variant-numeric: tabular-nums; }
43
+ .codeStatic { flex: none; padding: 0 10px; color: var(--text-secondary); font-size: var(--text-sm); font-weight: 500; }
44
+ .chevron { display: grid; width: 16px; height: 16px; place-items: center; color: var(--text-muted); }
45
+
46
+ /* The menu grows out of the currency button's corner and opens 8px below the shell, whatever its height. */
47
+ .menu { --drop: calc((var(--money-input-height, 76px) - 36px) / 2 + var(--space-related)); position: absolute; z-index: 30; top: calc(100% + var(--drop)); right: -8px; width: min(256px, calc(100vw - 2rem)); box-sizing: border-box; padding: 6px; border: 1px solid var(--border); border-radius: var(--radius-container); corner-shape: var(--corner-shape); background: var(--surface-raised); box-shadow: var(--shadow-floating); transform-origin: calc(100% - 44px) calc(var(--drop) * -1 - 18px); }
48
+ .list { position: relative; display: grid; max-height: 288px; gap: 2px; overflow-y: auto; overscroll-behavior: contain; outline: none; scrollbar-width: thin; }
49
+ .highlight { position: absolute; top: 0; right: 0; left: 0; border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: color-mix(in oklab, var(--foreground) 6.5%, transparent); pointer-events: none; }
50
+ .option { position: relative; display: flex; min-height: 36px; box-sizing: border-box; align-items: center; gap: 10px; padding: 0 10px; border-radius: var(--radius-control); corner-shape: var(--corner-shape); color: var(--text-secondary); cursor: pointer; font-size: var(--text-sm); user-select: none; -webkit-user-select: none; transition: color var(--duration-fast) var(--ease-standard); }
51
+ .option[data-active], .option[aria-selected="true"] { color: var(--foreground); }
52
+ .optionSymbol { width: 22px; flex: none; color: var(--text-muted); text-align: center; }
53
+ .optionCode { flex: none; font-weight: 500; font-variant-numeric: tabular-nums; }
54
+ .optionName { min-width: 0; flex: 1; overflow: hidden; color: var(--text-muted); font-size: var(--text-xs); text-overflow: ellipsis; white-space: nowrap; }
55
+ .check { display: grid; width: 16px; height: 16px; flex: none; place-items: center; color: var(--foreground); opacity: 0; transform: scale(.6); transition: opacity var(--duration-fast) var(--ease-standard), transform var(--duration-spring) var(--ease-spring); }
56
+ .check[data-on] { opacity: 1; transform: none; }
57
+
58
+ /* Quick add chips and the allowed range share one row under the shell. */
59
+ .toolbar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-related) var(--space-group); padding-top: var(--space-group); }
60
+ .chips { display: flex; flex-wrap: wrap; gap: 6px; }
61
+ .chip { height: 30px; padding: 0 12px; border: 1px solid var(--border); border-radius: var(--radius-full); corner-shape: round; background: var(--surface); color: var(--text-secondary); cursor: pointer; font: inherit; font-size: var(--text-xs); font-weight: 500; font-variant-numeric: tabular-nums; -webkit-tap-highlight-color: transparent; touch-action: manipulation; transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), border-color var(--duration-fast) var(--ease-standard), transform var(--duration-spring) var(--ease-spring); }
62
+ @media (hover: hover) and (pointer: fine) { .chip:hover:not(:disabled) { background: var(--surface-muted); color: var(--foreground); } }
63
+ .chip:active:not(:disabled) { transform: scale(.96); transition-duration: var(--duration-fast), var(--duration-fast), var(--duration-fast), 100ms; }
64
+ .chip[aria-disabled="true"] { opacity: .45; }
65
+ .chip:disabled { opacity: .45; cursor: not-allowed; }
66
+ .range { color: var(--text-muted); font-size: var(--text-xs); font-variant-numeric: tabular-nums; transition: color var(--duration-standard) var(--ease-standard); }
67
+ .range[data-flash] { color: var(--warning); transition-duration: var(--duration-instant); }
68
+
69
+ .messageSlot { position: relative; display: block; overflow: hidden; }
70
+ .hint, .error { display: block; padding-top: var(--space-related); font-size: var(--text-xs); line-height: var(--leading-body); }
71
+ .hint { color: var(--text-muted); }
72
+ .error { color: var(--danger); }
73
+
74
+ .srOnly { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }
75
+
76
+ @media (max-width: 420px) { .control { padding-left: 14px; } .group { font-size: var(--text-2xl); } }
77
+ @media (prefers-reduced-motion: reduce) { .control, .column, .check, .chip, .range { transition-duration: 0ms; } .chip:active:not(:disabled) { transform: none; } }