@codefast/ui 0.3.16-canary.2 → 0.4.0-canary.4

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 (289) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/README.md +28 -17
  3. package/dist/components/accordion.d.mts +7 -22
  4. package/dist/components/accordion.mjs +26 -29
  5. package/dist/components/alert-dialog.d.mts +27 -26
  6. package/dist/components/alert-dialog.mjs +53 -45
  7. package/dist/components/alert.d.mts +14 -14
  8. package/dist/components/alert.mjs +17 -28
  9. package/dist/components/aspect-ratio.d.mts +2 -2
  10. package/dist/components/aspect-ratio.mjs +2 -3
  11. package/dist/components/avatar.d.mts +41 -5
  12. package/dist/components/avatar.mjs +40 -10
  13. package/dist/components/badge.d.mts +3 -15
  14. package/dist/components/badge.mjs +6 -48
  15. package/dist/components/breadcrumb.d.mts +1 -0
  16. package/dist/components/breadcrumb.mjs +11 -10
  17. package/dist/components/button-group.d.mts +3 -13
  18. package/dist/components/button-group.mjs +9 -31
  19. package/dist/components/button.d.mts +3 -26
  20. package/dist/components/button.mjs +9 -79
  21. package/dist/components/calendar.d.mts +6 -2
  22. package/dist/components/calendar.mjs +41 -44
  23. package/dist/components/card.d.mts +4 -2
  24. package/dist/components/card.mjs +9 -9
  25. package/dist/components/carousel.d.mts +16 -5
  26. package/dist/components/carousel.mjs +24 -11
  27. package/dist/components/chart.d.mts +9 -6
  28. package/dist/components/chart.mjs +21 -15
  29. package/dist/components/checkbox-cards.mjs +4 -4
  30. package/dist/components/checkbox-group.mjs +3 -4
  31. package/dist/components/checkbox.d.mts +2 -2
  32. package/dist/components/checkbox.mjs +6 -7
  33. package/dist/components/collapsible.d.mts +4 -4
  34. package/dist/components/collapsible.mjs +4 -5
  35. package/dist/components/command.d.mts +11 -1
  36. package/dist/components/command.mjs +35 -32
  37. package/dist/components/context-menu.d.mts +22 -15
  38. package/dist/components/context-menu.mjs +44 -39
  39. package/dist/components/dialog.d.mts +19 -23
  40. package/dist/components/dialog.mjs +48 -47
  41. package/dist/components/direction.d.mts +24 -0
  42. package/dist/components/direction.mjs +18 -0
  43. package/dist/components/drawer.d.mts +3 -21
  44. package/dist/components/drawer.mjs +19 -27
  45. package/dist/components/dropdown-menu.d.mts +22 -15
  46. package/dist/components/dropdown-menu.mjs +41 -37
  47. package/dist/components/empty.d.mts +3 -13
  48. package/dist/components/empty.mjs +8 -23
  49. package/dist/components/field.d.mts +3 -14
  50. package/dist/components/field.mjs +14 -44
  51. package/dist/components/form.d.mts +7 -10
  52. package/dist/components/form.mjs +6 -7
  53. package/dist/components/hover-card.d.mts +5 -5
  54. package/dist/components/hover-card.mjs +14 -12
  55. package/dist/components/input-group.d.mts +4 -31
  56. package/dist/components/input-group.mjs +14 -96
  57. package/dist/components/input-number.d.mts +3 -1
  58. package/dist/components/input-number.mjs +50 -28
  59. package/dist/components/input-otp.mjs +9 -7
  60. package/dist/components/input-password.mjs +1 -4
  61. package/dist/components/input-search.mjs +3 -5
  62. package/dist/components/input.mjs +1 -2
  63. package/dist/components/item.d.mts +4 -29
  64. package/dist/components/item.mjs +12 -65
  65. package/dist/components/kbd.mjs +1 -1
  66. package/dist/components/label.d.mts +2 -2
  67. package/dist/components/label.mjs +3 -4
  68. package/dist/components/menubar.d.mts +22 -16
  69. package/dist/components/menubar.mjs +54 -47
  70. package/dist/components/native-select.d.mts +5 -1
  71. package/dist/components/native-select.mjs +9 -6
  72. package/dist/components/navigation-menu.d.mts +30 -13
  73. package/dist/components/navigation-menu.mjs +35 -32
  74. package/dist/components/pagination.d.mts +7 -1
  75. package/dist/components/pagination.mjs +27 -12
  76. package/dist/components/popover.d.mts +40 -7
  77. package/dist/components/popover.mjs +46 -14
  78. package/dist/components/progress-circle.d.mts +3 -47
  79. package/dist/components/progress-circle.mjs +2 -48
  80. package/dist/components/progress.d.mts +2 -2
  81. package/dist/components/progress.mjs +5 -6
  82. package/dist/components/radio-cards.d.mts +3 -3
  83. package/dist/components/radio-cards.mjs +11 -11
  84. package/dist/components/radio-group.d.mts +3 -3
  85. package/dist/components/radio-group.mjs +9 -9
  86. package/dist/components/radio.mjs +2 -3
  87. package/dist/components/resizable.mjs +3 -8
  88. package/dist/components/scroll-area.d.mts +8 -24
  89. package/dist/components/scroll-area.mjs +16 -70
  90. package/dist/components/select.d.mts +14 -14
  91. package/dist/components/select.mjs +47 -47
  92. package/dist/components/separator.d.mts +4 -19
  93. package/dist/components/separator.mjs +6 -27
  94. package/dist/components/sheet.d.mts +18 -31
  95. package/dist/components/sheet.mjs +46 -87
  96. package/dist/components/sidebar.d.mts +3 -19
  97. package/dist/components/sidebar.mjs +48 -84
  98. package/dist/components/skeleton.mjs +1 -1
  99. package/dist/components/slider.d.mts +2 -2
  100. package/dist/components/slider.mjs +9 -11
  101. package/dist/components/sonner.mjs +11 -3
  102. package/dist/components/spinner.mjs +6 -7
  103. package/dist/components/switch.d.mts +5 -2
  104. package/dist/components/switch.mjs +7 -7
  105. package/dist/components/table.mjs +10 -10
  106. package/dist/components/tabs.d.mts +8 -5
  107. package/dist/components/tabs.mjs +18 -12
  108. package/dist/components/textarea.mjs +1 -1
  109. package/dist/components/toggle-group.d.mts +11 -7
  110. package/dist/components/toggle-group.mjs +20 -21
  111. package/dist/components/toggle.d.mts +4 -24
  112. package/dist/components/toggle.mjs +6 -45
  113. package/dist/components/tooltip.d.mts +7 -6
  114. package/dist/components/tooltip.mjs +19 -17
  115. package/dist/hooks/use-animated-value.mjs +0 -1
  116. package/dist/hooks/use-copy-to-clipboard.mjs +0 -1
  117. package/dist/hooks/use-is-mobile.mjs +0 -1
  118. package/dist/hooks/use-media-query.mjs +0 -1
  119. package/dist/hooks/use-mutation-observer.mjs +0 -1
  120. package/dist/hooks/use-pagination.mjs +0 -1
  121. package/dist/index.d.mts +38 -21
  122. package/dist/index.mjs +40 -23
  123. package/dist/lib/utils.d.mts +1 -12
  124. package/dist/lib/utils.mjs +1 -9
  125. package/dist/primitives/checkbox-group.d.mts +9 -11
  126. package/dist/primitives/checkbox-group.mjs +14 -19
  127. package/dist/primitives/input-number.d.mts +3 -4
  128. package/dist/primitives/input-number.mjs +3 -5
  129. package/dist/primitives/input.d.mts +4 -5
  130. package/dist/primitives/input.mjs +2 -3
  131. package/dist/primitives/progress-circle.d.mts +3 -4
  132. package/dist/primitives/progress-circle.mjs +2 -3
  133. package/dist/variants/alert.d.mts +18 -0
  134. package/dist/variants/alert.mjs +15 -0
  135. package/dist/variants/badge.d.mts +22 -0
  136. package/dist/variants/badge.mjs +19 -0
  137. package/dist/variants/button-group.d.mts +18 -0
  138. package/dist/variants/button-group.mjs +15 -0
  139. package/dist/variants/button.d.mts +32 -0
  140. package/dist/variants/button.mjs +34 -0
  141. package/dist/variants/empty.d.mts +18 -0
  142. package/dist/variants/empty.mjs +15 -0
  143. package/dist/variants/field.d.mts +19 -0
  144. package/dist/variants/field.mjs +16 -0
  145. package/dist/variants/input-group.d.mts +43 -0
  146. package/dist/variants/input-group.mjs +34 -0
  147. package/dist/variants/input-number.d.mts +45 -0
  148. package/dist/variants/input-number.mjs +40 -0
  149. package/dist/variants/item.d.mts +38 -0
  150. package/dist/variants/item.mjs +38 -0
  151. package/dist/variants/navigation-menu.d.mts +13 -0
  152. package/dist/variants/navigation-menu.mjs +8 -0
  153. package/dist/variants/progress-circle.d.mts +52 -0
  154. package/dist/variants/progress-circle.mjs +45 -0
  155. package/dist/variants/scroll-area.d.mts +24 -0
  156. package/dist/variants/scroll-area.mjs +58 -0
  157. package/dist/variants/separator.d.mts +23 -0
  158. package/dist/variants/separator.mjs +25 -0
  159. package/dist/variants/sheet.d.mts +20 -0
  160. package/dist/variants/sheet.mjs +17 -0
  161. package/dist/variants/sidebar.d.mts +23 -0
  162. package/dist/variants/sidebar.mjs +25 -0
  163. package/dist/variants/tabs.d.mts +18 -0
  164. package/dist/variants/tabs.mjs +15 -0
  165. package/dist/variants/toggle.d.mts +23 -0
  166. package/dist/variants/toggle.mjs +25 -0
  167. package/package.json +186 -55
  168. package/src/components/accordion.tsx +114 -0
  169. package/src/components/alert-dialog.tsx +298 -0
  170. package/src/components/alert.tsx +94 -0
  171. package/src/components/aspect-ratio.tsx +25 -0
  172. package/src/components/avatar.tsx +171 -0
  173. package/src/components/badge.tsx +35 -0
  174. package/src/components/breadcrumb.tsx +191 -0
  175. package/src/components/button-group.tsx +97 -0
  176. package/src/components/button.tsx +55 -0
  177. package/src/components/calendar.tsx +222 -0
  178. package/src/components/card.tsx +169 -0
  179. package/src/components/carousel.tsx +349 -0
  180. package/src/components/chart.tsx +536 -0
  181. package/src/components/checkbox-cards.tsx +72 -0
  182. package/src/components/checkbox-group.tsx +60 -0
  183. package/src/components/checkbox.tsx +44 -0
  184. package/src/components/collapsible.tsx +57 -0
  185. package/src/components/command.tsx +298 -0
  186. package/src/components/context-menu.tsx +410 -0
  187. package/src/components/dialog.tsx +243 -0
  188. package/src/components/direction.tsx +32 -0
  189. package/src/components/drawer.tsx +209 -0
  190. package/src/components/dropdown-menu.tsx +419 -0
  191. package/src/components/empty.tsx +155 -0
  192. package/src/components/field.tsx +329 -0
  193. package/src/components/form.tsx +258 -0
  194. package/src/components/hover-card.tsx +93 -0
  195. package/src/components/input-group.tsx +185 -0
  196. package/src/components/input-number.tsx +141 -0
  197. package/src/components/input-otp.tsx +132 -0
  198. package/src/components/input-password.tsx +50 -0
  199. package/src/components/input-search.tsx +81 -0
  200. package/src/components/input.tsx +36 -0
  201. package/src/components/item.tsx +266 -0
  202. package/src/components/kbd.tsx +47 -0
  203. package/src/components/label.tsx +36 -0
  204. package/src/components/menubar.tsx +440 -0
  205. package/src/components/native-select.tsx +87 -0
  206. package/src/components/navigation-menu.tsx +235 -0
  207. package/src/components/pagination.tsx +198 -0
  208. package/src/components/popover.tsx +170 -0
  209. package/src/components/progress-circle.tsx +185 -0
  210. package/src/components/progress.tsx +41 -0
  211. package/src/components/radio-cards.tsx +66 -0
  212. package/src/components/radio-group.tsx +59 -0
  213. package/src/components/radio.tsx +40 -0
  214. package/src/components/resizable.tsx +78 -0
  215. package/src/components/scroll-area.tsx +95 -0
  216. package/src/components/select.tsx +296 -0
  217. package/src/components/separator.tsx +60 -0
  218. package/src/components/sheet.tsx +241 -0
  219. package/src/components/sidebar.tsx +926 -0
  220. package/src/components/skeleton.tsx +35 -0
  221. package/src/components/slider.tsx +66 -0
  222. package/src/components/sonner.tsx +57 -0
  223. package/src/components/spinner.tsx +66 -0
  224. package/src/components/switch.tsx +44 -0
  225. package/src/components/table.tsx +183 -0
  226. package/src/components/tabs.tsx +110 -0
  227. package/src/components/textarea.tsx +35 -0
  228. package/src/components/toggle-group.tsx +137 -0
  229. package/src/components/toggle.tsx +30 -0
  230. package/src/components/tooltip.tsx +115 -0
  231. package/src/css/foundation/base.css +50 -0
  232. package/src/css/foundation/motion.css +36 -0
  233. package/src/css/foundation/source.css +3 -0
  234. package/src/css/foundation/tokens.css +71 -0
  235. package/src/css/foundation/variants.css +113 -0
  236. package/src/css/preset.css +5 -195
  237. package/src/css/style.css +1 -1
  238. package/src/css/{amber.css → themes/amber.css} +59 -22
  239. package/src/css/{blue.css → themes/blue.css} +59 -22
  240. package/src/css/{cyan.css → themes/cyan.css} +59 -22
  241. package/src/css/{emerald.css → themes/emerald.css} +59 -22
  242. package/src/css/{fuchsia.css → themes/fuchsia.css} +59 -22
  243. package/src/css/{gray.css → themes/gray.css} +59 -22
  244. package/src/css/{green.css → themes/green.css} +59 -22
  245. package/src/css/{indigo.css → themes/indigo.css} +59 -22
  246. package/src/css/{lime.css → themes/lime.css} +59 -22
  247. package/src/css/{neutral.css → themes/neutral.css} +59 -22
  248. package/src/css/{orange.css → themes/orange.css} +59 -22
  249. package/src/css/{pink.css → themes/pink.css} +59 -22
  250. package/src/css/{purple.css → themes/purple.css} +59 -22
  251. package/src/css/{red.css → themes/red.css} +59 -22
  252. package/src/css/{rose.css → themes/rose.css} +59 -22
  253. package/src/css/{sky.css → themes/sky.css} +59 -22
  254. package/src/css/{slate.css → themes/slate.css} +59 -22
  255. package/src/css/{stone.css → themes/stone.css} +59 -22
  256. package/src/css/{teal.css → themes/teal.css} +59 -22
  257. package/src/css/{violet.css → themes/violet.css} +59 -22
  258. package/src/css/{yellow.css → themes/yellow.css} +59 -22
  259. package/src/css/{zinc.css → themes/zinc.css} +59 -22
  260. package/src/hooks/use-animated-value.ts +91 -0
  261. package/src/hooks/use-copy-to-clipboard.ts +58 -0
  262. package/src/hooks/use-is-mobile.ts +25 -0
  263. package/src/hooks/use-media-query.ts +69 -0
  264. package/src/hooks/use-mutation-observer.ts +51 -0
  265. package/src/hooks/use-pagination.ts +164 -0
  266. package/src/index.ts +679 -0
  267. package/src/lib/utils.ts +5 -0
  268. package/src/primitives/checkbox-group.tsx +346 -0
  269. package/src/primitives/input-number.tsx +967 -0
  270. package/src/primitives/input.tsx +227 -0
  271. package/src/primitives/progress-circle.tsx +507 -0
  272. package/src/variants/alert.ts +34 -0
  273. package/src/variants/badge.ts +39 -0
  274. package/src/variants/button-group.ts +36 -0
  275. package/src/variants/button.ts +56 -0
  276. package/src/variants/empty.ts +34 -0
  277. package/src/variants/field.ts +37 -0
  278. package/src/variants/input-group.ts +80 -0
  279. package/src/variants/input-number.ts +65 -0
  280. package/src/variants/item.ts +68 -0
  281. package/src/variants/navigation-menu.ts +25 -0
  282. package/src/variants/progress-circle.ts +46 -0
  283. package/src/variants/scroll-area.ts +73 -0
  284. package/src/variants/separator.ts +40 -0
  285. package/src/variants/sheet.ts +37 -0
  286. package/src/variants/sidebar.ts +41 -0
  287. package/src/variants/tabs.ts +34 -0
  288. package/src/variants/toggle.ts +40 -0
  289. package/dist/node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/clsx.d.mts +0 -6
@@ -0,0 +1,967 @@
1
+ import { composeEventHandlers } from "radix-ui/internal";
2
+ import { Context } from "radix-ui/internal";
3
+ import { useControllableState } from "radix-ui/internal";
4
+ import type {
5
+ ComponentProps,
6
+ FocusEventHandler,
7
+ JSX,
8
+ KeyboardEvent,
9
+ KeyboardEventHandler,
10
+ MouseEventHandler,
11
+ PointerEventHandler,
12
+ RefObject,
13
+ } from "react";
14
+ import { useCallback, useEffect, useMemo, useRef } from "react";
15
+
16
+ import * as InputPrimitive from "#/primitives/input";
17
+ import { createInputScope } from "#/primitives/input";
18
+
19
+ /* -----------------------------------------------------------------------------
20
+ * Context: InputNumber
21
+ * -------------------------------------------------------------------------- */
22
+
23
+ /**
24
+ * The name of the InputNumber component constant.
25
+ */
26
+ const NUMBER_INPUT_NAME = "InputNumber";
27
+
28
+ /**
29
+ * Props that include an optional scope for the InputNumber component.
30
+ */
31
+ type ScopedProps<P> = P & {
32
+ /**
33
+ * Optional scope for the InputNumber component context
34
+ */
35
+ __scopeInputNumber?: Context.Scope;
36
+ };
37
+
38
+ const [createInputNumberContext, createInputNumberScope] = Context.createContextScope(NUMBER_INPUT_NAME, [
39
+ createInputScope,
40
+ ]);
41
+ const useInputScope = createInputScope();
42
+
43
+ /**
44
+ * Context value for the InputNumber component.
45
+ */
46
+ interface InputNumberContextValue {
47
+ /**
48
+ * Formatting options for displaying the number value
49
+ */
50
+ formatOptions: Intl.NumberFormatOptions;
51
+
52
+ /**
53
+ * Function to format a number value as a string
54
+ */
55
+ formatValue: (value?: number) => string;
56
+
57
+ /**
58
+ * Reference to the input element
59
+ */
60
+ inputRef: RefObject<HTMLInputElement | null>;
61
+
62
+ /**
63
+ * Handler for when the value changes
64
+ */
65
+ onChange: (value?: number) => void;
66
+
67
+ /**
68
+ * Handler to decrement the value
69
+ */
70
+ onDecrement: () => void;
71
+
72
+ /**
73
+ * Handler to decrement the value to the minimum allowed
74
+ */
75
+ onDecrementToMin: () => void;
76
+
77
+ /**
78
+ * Handler to increment the value
79
+ */
80
+ onIncrement: () => void;
81
+
82
+ /**
83
+ * Handler to increment the value to the maximum allowed
84
+ */
85
+ onIncrementToMax: () => void;
86
+
87
+ /**
88
+ * Function to parse a value into a number
89
+ */
90
+ parseValue: (value: number | ReadonlyArray<string> | string | undefined) => number;
91
+
92
+ /**
93
+ * Accessible label for the decrement button
94
+ */
95
+ ariaDecrementLabel?: string;
96
+
97
+ /**
98
+ * Accessible label for the increment button
99
+ */
100
+ ariaIncrementLabel?: string;
101
+
102
+ /**
103
+ * Initial value used when the input is uncontrolled
104
+ */
105
+ defaultValue?: number;
106
+
107
+ /**
108
+ * Whether the input is disabled
109
+ */
110
+ disabled?: boolean;
111
+
112
+ /**
113
+ * Unique identifier for the input
114
+ */
115
+ id?: string;
116
+
117
+ /**
118
+ * Maximum allowed value
119
+ */
120
+ max?: number;
121
+
122
+ /**
123
+ * Minimum allowed value
124
+ */
125
+ min?: number;
126
+
127
+ /**
128
+ * Whether the input is read-only
129
+ */
130
+ readOnly?: boolean;
131
+
132
+ /**
133
+ * Step value for increments/decrements
134
+ */
135
+ step?: number;
136
+
137
+ /**
138
+ * Current value of the input
139
+ */
140
+ value?: number;
141
+ }
142
+
143
+ const [InputNumberContextProvider, useInputNumberContext] =
144
+ createInputNumberContext<InputNumberContextValue>(NUMBER_INPUT_NAME);
145
+
146
+ /* -----------------------------------------------------------------------------
147
+ * Component: InputNumber
148
+ * -------------------------------------------------------------------------- */
149
+
150
+ /**
151
+ * Props for the main InputNumber component.
152
+ *
153
+ * @since 0.3.16-canary.0
154
+ */
155
+ interface InputNumberProps extends ComponentProps<typeof InputPrimitive.Root> {
156
+ /**
157
+ * Accessible label for the decrement button
158
+ */
159
+ ariaDecrementLabel?: string;
160
+
161
+ /**
162
+ * Accessible label for the increment button
163
+ */
164
+ ariaIncrementLabel?: string;
165
+
166
+ /**
167
+ * Initial value when uncontrolled
168
+ */
169
+ defaultValue?: number;
170
+
171
+ /**
172
+ * Options for number formatting
173
+ */
174
+ formatOptions?: Intl.NumberFormatOptions;
175
+
176
+ /**
177
+ * Unique identifier for the input
178
+ */
179
+ id?: string;
180
+
181
+ /**
182
+ * Locale used for number formatting
183
+ */
184
+ locale?: string;
185
+
186
+ /**
187
+ * Maximum allowed value
188
+ */
189
+ max?: number;
190
+
191
+ /**
192
+ * Minimum allowed value
193
+ */
194
+ min?: number;
195
+
196
+ /**
197
+ * Handler called when the value changes
198
+ */
199
+ onChange?: (value?: number) => void;
200
+
201
+ /**
202
+ * Step value for increments/decrements
203
+ */
204
+ step?: number;
205
+
206
+ /**
207
+ * Current value when controlled
208
+ */
209
+ value?: number;
210
+ }
211
+
212
+ /**
213
+ * @since 0.3.16-canary.0
214
+ */
215
+ function InputNumber(numberInputProps: ScopedProps<InputNumberProps>): JSX.Element {
216
+ const {
217
+ __scopeInputNumber,
218
+ ariaDecrementLabel,
219
+ ariaIncrementLabel,
220
+ defaultValue,
221
+ formatOptions = { minimumFractionDigits: 0, style: "decimal" },
222
+ id,
223
+ locale,
224
+ max,
225
+ min,
226
+ onChange,
227
+ step = 1,
228
+ value: valueProperty,
229
+ ...props
230
+ } = numberInputProps;
231
+
232
+ /**
233
+ * Context.Scope for the input component
234
+ */
235
+ const inputScope = useInputScope(__scopeInputNumber);
236
+
237
+ /**
238
+ * Reference to the input element
239
+ */
240
+ const inputRef = useRef<HTMLInputElement>(null);
241
+
242
+ /**
243
+ * Controlled or uncontrolled value state
244
+ */
245
+ const [value, setValue] = useControllableState<number | undefined>({
246
+ defaultProp: defaultValue,
247
+ onChange,
248
+ prop: valueProperty,
249
+ });
250
+
251
+ /**
252
+ * Separators used for number formatting based on locale
253
+ */
254
+ const { decimalSeparator, thousandSeparator } = useMemo(() => getNumberFormatSeparators(locale), [locale]);
255
+
256
+ /**
257
+ * Formats a number value into a string representation
258
+ * @param inputValue - The number to format
259
+ * @returns A formatted string representation of the number
260
+ */
261
+ const formatValue = useCallback(
262
+ (inputValue?: number): string => {
263
+ if (inputValue === undefined || Number.isNaN(inputValue)) {
264
+ return "";
265
+ }
266
+
267
+ return new Intl.NumberFormat(locale, formatOptions).format(inputValue);
268
+ },
269
+ [formatOptions, locale],
270
+ );
271
+
272
+ /**
273
+ * Parses a string or number input into a normalized number value
274
+ * @param inputValue - The value to parse
275
+ * @returns The parsed number value, clamped between min and max
276
+ */
277
+ const parseValue = useCallback(
278
+ (inputValue: number | ReadonlyArray<string> | string | undefined): number => {
279
+ if (typeof inputValue === "number") {
280
+ return clamp(inputValue, min, max);
281
+ }
282
+
283
+ if (typeof inputValue !== "string") {
284
+ return Number.NaN;
285
+ }
286
+
287
+ const cleanedValue = inputValue.trim().replaceAll(/[^\d.,\-()]/g, "");
288
+
289
+ if (cleanedValue === "") {
290
+ return Number.NaN;
291
+ }
292
+
293
+ const normalizedValue = normalizeInputValue(cleanedValue, thousandSeparator, decimalSeparator);
294
+ let parsedValue = Number.parseFloat(normalizedValue);
295
+
296
+ if (formatOptions.style === "percent") {
297
+ parsedValue /= 100;
298
+ }
299
+
300
+ return Number.isNaN(parsedValue) ? 0 : clamp(parsedValue, min, max);
301
+ },
302
+ [decimalSeparator, formatOptions.style, max, min, thousandSeparator],
303
+ );
304
+
305
+ /**
306
+ * Changes the current value based on a provided operation
307
+ * @param operation - Function that takes the current value and returns a new value
308
+ */
309
+ const changeNumberValue = useCallback(
310
+ (operation: (number: number) => number) => {
311
+ const inputElement = inputRef.current;
312
+
313
+ if (!inputElement || props.disabled || props.readOnly) {
314
+ return;
315
+ }
316
+
317
+ const currentValue = parseValue(inputElement.value) || 0;
318
+ const newValue = clamp(operation(currentValue), min, max);
319
+
320
+ inputElement.value = formatValue(newValue);
321
+ setValue(newValue);
322
+ },
323
+ [props.disabled, formatValue, max, min, parseValue, props.readOnly, setValue],
324
+ );
325
+
326
+ /**
327
+ * Increments the current value by the step amount
328
+ */
329
+ const handleIncrement = useCallback(() => {
330
+ changeNumberValue((number) => number + step);
331
+ }, [changeNumberValue, step]);
332
+
333
+ /**
334
+ * Decrements the current value by the step amount
335
+ */
336
+ const handleDecrement = useCallback(() => {
337
+ changeNumberValue((number) => number - step);
338
+ }, [changeNumberValue, step]);
339
+
340
+ /**
341
+ * Sets the value to the maximum allowed
342
+ */
343
+ const handleIncrementToMax = useCallback(() => {
344
+ changeNumberValue((number) => max ?? number + step);
345
+ }, [changeNumberValue, max, step]);
346
+
347
+ /**
348
+ * Sets the value to the minimum allowed
349
+ */
350
+ const handleDecrementToMin = useCallback(() => {
351
+ changeNumberValue((number) => min ?? number - step);
352
+ }, [changeNumberValue, min, step]);
353
+
354
+ return (
355
+ <InputNumberContextProvider
356
+ ariaDecrementLabel={ariaDecrementLabel}
357
+ ariaIncrementLabel={ariaIncrementLabel}
358
+ defaultValue={defaultValue}
359
+ disabled={props.disabled}
360
+ formatOptions={formatOptions}
361
+ formatValue={formatValue}
362
+ id={id}
363
+ inputRef={inputRef}
364
+ max={max}
365
+ min={min}
366
+ parseValue={parseValue}
367
+ readOnly={props.readOnly}
368
+ scope={__scopeInputNumber}
369
+ value={value}
370
+ onChange={setValue}
371
+ onDecrement={handleDecrement}
372
+ onDecrementToMin={handleDecrementToMin}
373
+ onIncrement={handleIncrement}
374
+ onIncrementToMax={handleIncrementToMax}
375
+ >
376
+ <InputPrimitive.Root {...inputScope} {...props} />
377
+ </InputNumberContextProvider>
378
+ );
379
+ }
380
+
381
+ /* -----------------------------------------------------------------------------
382
+ * Component: InputNumberField
383
+ * -------------------------------------------------------------------------- */
384
+
385
+ /**
386
+ * The name of the InputNumberField component constant.
387
+ */
388
+ const NUMBER_INPUT_FIELD_NAME = "InputNumberField";
389
+
390
+ /**
391
+ * Defines the props for the `InputNumberField` component.
392
+ *
393
+ * @since 0.3.16-canary.0
394
+ */
395
+ type InputNumberFieldProps = Omit<
396
+ ComponentProps<typeof InputPrimitive.Field>,
397
+ "defaultValue" | "disabled" | "id" | "max" | "min" | "onChange" | "prefix" | "readOnly" | "step" | "value"
398
+ >;
399
+
400
+ /**
401
+ * @since 0.3.16-canary.0
402
+ */
403
+ function InputNumberField({
404
+ __scopeInputNumber,
405
+ onBlur,
406
+ onKeyDown,
407
+ ...props
408
+ }: ScopedProps<InputNumberFieldProps>): JSX.Element {
409
+ // Retrieve input number context and input scope
410
+ const inputScope = useInputScope(__scopeInputNumber);
411
+ const {
412
+ defaultValue,
413
+ disabled,
414
+ formatValue,
415
+ id,
416
+ inputRef,
417
+ max,
418
+ min,
419
+ onChange,
420
+ onDecrement,
421
+ onDecrementToMin,
422
+ onIncrement,
423
+ onIncrementToMax,
424
+ parseValue,
425
+ readOnly,
426
+ step,
427
+ value,
428
+ } = useInputNumberContext(NUMBER_INPUT_FIELD_NAME, __scopeInputNumber);
429
+
430
+ /**
431
+ * Handles the blur event to format the value of the input.
432
+ *
433
+ * @param event - The blur event triggered when the input loses focus.
434
+ */
435
+ const handleBlur = useCallback<FocusEventHandler<HTMLInputElement>>(
436
+ (event) => {
437
+ const numericValue = parseValue(event.target.value);
438
+ const formattedValue = formatValue(numericValue);
439
+
440
+ if (formattedValue !== event.target.value) {
441
+ event.target.value = formattedValue;
442
+ }
443
+
444
+ onChange(numericValue);
445
+ },
446
+ [formatValue, onChange, parseValue],
447
+ );
448
+
449
+ /**
450
+ * Handles keydown events to increment, decrement, or perform other actions.
451
+ *
452
+ * @param event - The keyboard event triggered by key presses.
453
+ */
454
+ const handleKeyDown = useCallback<KeyboardEventHandler<HTMLInputElement>>(
455
+ (event) => {
456
+ switch (event.key) {
457
+ case "ArrowUp": {
458
+ onIncrement();
459
+ event.preventDefault();
460
+ break;
461
+ }
462
+
463
+ case "PageUp": {
464
+ onIncrementToMax();
465
+ event.preventDefault();
466
+ break;
467
+ }
468
+
469
+ case "ArrowDown": {
470
+ onDecrement();
471
+ event.preventDefault();
472
+ break;
473
+ }
474
+
475
+ case "PageDown": {
476
+ onDecrementToMin();
477
+ event.preventDefault();
478
+ break;
479
+ }
480
+
481
+ default: {
482
+ break;
483
+ }
484
+ }
485
+ },
486
+ [onIncrement, onIncrementToMax, onDecrement, onDecrementToMin],
487
+ );
488
+
489
+ /**
490
+ * Prevents invalid keyboard input for the numeric input field.
491
+ *
492
+ * @param event - The keyboard event to handle.
493
+ */
494
+ const handleKeyDownPrevent = useCallback<KeyboardEventHandler<HTMLInputElement>>((event) => {
495
+ switch (event.key) {
496
+ case "ArrowUp":
497
+
498
+ case "ArrowDown":
499
+
500
+ case "ArrowLeft":
501
+
502
+ case "ArrowRight":
503
+
504
+ case "PageUp":
505
+
506
+ case "PageDown":
507
+
508
+ case "Tab":
509
+
510
+ case "Escape":
511
+
512
+ case "Enter":
513
+
514
+ case "Backspace":
515
+
516
+ case "Delete":
517
+
518
+ case "Home":
519
+
520
+ case "End":
521
+
522
+ case ".":
523
+
524
+ case ",":
525
+
526
+ case "-":
527
+
528
+ case "%": {
529
+ return;
530
+ }
531
+
532
+ default: {
533
+ if (isNumberKey(event.key) || isModifierKey(event) || isFunctionKey(event.key)) {
534
+ return;
535
+ }
536
+
537
+ event.preventDefault();
538
+ }
539
+ }
540
+ }, []);
541
+
542
+ /**
543
+ * Handles the Enter key to format the value of the input.
544
+ *
545
+ * @param event - The keyboard event triggered by pressing Enter.
546
+ */
547
+ const handleKeyDownEnter = useCallback<KeyboardEventHandler<HTMLInputElement>>(
548
+ (event) => {
549
+ const inputElement = inputRef.current;
550
+
551
+ if (event.key !== "Enter" || !inputElement) {
552
+ return;
553
+ }
554
+
555
+ const numericValue = parseValue(inputElement.value);
556
+ const formattedValue = formatValue(numericValue);
557
+
558
+ if (formattedValue !== inputElement.value) {
559
+ inputElement.value = formattedValue;
560
+ }
561
+
562
+ onChange(numericValue);
563
+ },
564
+ [formatValue, inputRef, onChange, parseValue],
565
+ );
566
+
567
+ /**
568
+ * Creates a combined keydown event handler that processes keyboard events in sequence.
569
+ *
570
+ * The handler chain executes in the following order:
571
+ * 1. User-provided onKeyDown handler (if any)
572
+ * 2. handleKeyDownPrevent - Prevents non-numeric input
573
+ * 3. handleKeyDown - Handles arrow keys and page up/down for value adjustments
574
+ * 4. handleKeyDownEnter - Handles Enter key press to format and update the value
575
+ *
576
+ * @returns A composed event handler function for the onKeyDown event
577
+ */
578
+ const combinedKeyDownHandler = useCallback<KeyboardEventHandler<HTMLInputElement>>(
579
+ (event) => {
580
+ composeEventHandlers(onKeyDown, chain(handleKeyDownPrevent, handleKeyDown, handleKeyDownEnter))(event);
581
+ },
582
+ [onKeyDown, handleKeyDown, handleKeyDownEnter, handleKeyDownPrevent],
583
+ );
584
+
585
+ /**
586
+ * Adds a listener to handle wheel events for incrementing or decrementing the value.
587
+ */
588
+ useEffect(() => {
589
+ const handleWheel = (event: WheelEvent): void => {
590
+ const inputElement = inputRef.current;
591
+
592
+ if (!inputElement || disabled || readOnly || document.activeElement !== inputElement) {
593
+ return;
594
+ }
595
+
596
+ event.preventDefault();
597
+
598
+ if (event.deltaY > 0) {
599
+ onIncrement();
600
+ } else {
601
+ onDecrement();
602
+ }
603
+ };
604
+
605
+ const inputElement = inputRef.current;
606
+
607
+ inputElement?.addEventListener("wheel", handleWheel);
608
+
609
+ return (): void => {
610
+ inputElement?.removeEventListener("wheel", handleWheel);
611
+ };
612
+ }, [onIncrement, onDecrement, inputRef, disabled, readOnly]);
613
+
614
+ /**
615
+ * Updates the input field's value when it changes in the context.
616
+ */
617
+ useEffect(() => {
618
+ const inputElement = inputRef.current;
619
+
620
+ if (inputElement && inputElement !== document.activeElement) {
621
+ inputElement.value = formatValue(value);
622
+ }
623
+ }, [formatValue, inputRef, value]);
624
+
625
+ /**
626
+ * Adds a listener to handle form reset events by clearing the input value.
627
+ */
628
+ useEffect(() => {
629
+ const inputElement = inputRef.current;
630
+
631
+ if (!inputElement) {
632
+ return;
633
+ }
634
+
635
+ const handleReset = (): void => {
636
+ onChange(parseValue(defaultValue));
637
+ };
638
+
639
+ const form = inputElement.form;
640
+
641
+ form?.addEventListener("reset", handleReset);
642
+
643
+ return (): void => {
644
+ form?.removeEventListener("reset", handleReset);
645
+ };
646
+ }, [defaultValue, inputRef, onChange, parseValue]);
647
+
648
+ return (
649
+ <InputPrimitive.Field
650
+ ref={inputRef}
651
+ defaultValue={formatValue(value)}
652
+ disabled={disabled}
653
+ id={id}
654
+ inputMode="decimal"
655
+ max={max}
656
+ min={min}
657
+ readOnly={readOnly}
658
+ step={step}
659
+ onBlur={composeEventHandlers(onBlur, handleBlur)}
660
+ onKeyDown={combinedKeyDownHandler}
661
+ {...inputScope}
662
+ {...props}
663
+ />
664
+ );
665
+ }
666
+
667
+ /* -----------------------------------------------------------------------------
668
+ * Component: NumberStepperButton
669
+ * -------------------------------------------------------------------------- */
670
+
671
+ /**
672
+ * The name of the NumberStepperButton component constant.
673
+ */
674
+ const NUMBER_STEPPER_BUTTON_NAME = "NumberStepperButton";
675
+
676
+ /**
677
+ * Props for the NumberStepperButton component.
678
+ */
679
+ interface NumberStepperButtonProps extends ComponentProps<"button"> {
680
+ /**
681
+ * The operation to perform when the button is pressed.
682
+ * - `'increment'`: Increases the value.
683
+ * - `'decrement'`: Decreases the value.
684
+ */
685
+ operation: "decrement" | "increment";
686
+ }
687
+
688
+ function NumberStepperButton({
689
+ __scopeInputNumber,
690
+ operation,
691
+ ...props
692
+ }: ScopedProps<NumberStepperButtonProps>): JSX.Element {
693
+ // Destructures relevant context values for the button functionality.
694
+ const { ariaDecrementLabel, ariaIncrementLabel, disabled, id, max, min, onDecrement, onIncrement, value } =
695
+ useInputNumberContext(NUMBER_STEPPER_BUTTON_NAME, __scopeInputNumber);
696
+
697
+ const isDisabled = useMemo(() => {
698
+ const atMin = min !== undefined && value !== undefined && value <= min;
699
+ const atMax = max !== undefined && value !== undefined && value >= max;
700
+
701
+ return (disabled ?? atMin) || atMax;
702
+ }, [min, max, value, disabled]);
703
+
704
+ /**
705
+ * Ref to store a timeout ID for managing repeated button actions.
706
+ */
707
+ const timeoutIdRef = useRef<ReturnType<typeof setTimeout>>(null);
708
+
709
+ /**
710
+ * Starts a repeated action at a regular interval.
711
+ * The action begins immediately and then continues with a delay.
712
+ *
713
+ * @param callback - The callback function to execute repeatedly.
714
+ */
715
+ const startActionInterval = useCallback((callback: () => void) => {
716
+ // Time between repeated actions (in milliseconds).
717
+ const interval = 100;
718
+
719
+ // Function to perform the action and set the next interval.
720
+ const repeatAction = (): void => {
721
+ callback();
722
+ timeoutIdRef.current = setTimeout(repeatAction, interval);
723
+ };
724
+
725
+ callback();
726
+ timeoutIdRef.current = setTimeout(repeatAction, interval * 2);
727
+ }, []);
728
+
729
+ /**
730
+ * Clears any ongoing action intervals.
731
+ */
732
+ const clearActionInterval = useCallback(() => {
733
+ if (timeoutIdRef.current) {
734
+ clearTimeout(timeoutIdRef.current);
735
+ timeoutIdRef.current = null;
736
+ }
737
+ }, []);
738
+
739
+ /**
740
+ * Handles pointer down events and triggers the appropriate action
741
+ * (`increment` or `decrement`).
742
+ */
743
+ const handlePointerDown = useCallback<PointerEventHandler<HTMLButtonElement>>(() => {
744
+ const action = operation === "increment" ? onIncrement : onDecrement;
745
+
746
+ startActionInterval(action);
747
+ }, [onDecrement, onIncrement, operation, startActionInterval]);
748
+
749
+ /**
750
+ * Prevents the context menu from displaying when the button is right-clicked.
751
+ *
752
+ * @param event - The mouse event triggered by the right-click.
753
+ */
754
+ const handleContextMenu = useCallback<MouseEventHandler<HTMLButtonElement>>((event) => {
755
+ event.preventDefault();
756
+ }, []);
757
+
758
+ /**
759
+ * Handles keyboard events to support activation of the button using
760
+ * keyboard navigation (Enter or Space).
761
+ *
762
+ * @param event - The keyboard event with the triggered key.
763
+ */
764
+ const handleKeyDown = useCallback<KeyboardEventHandler<HTMLButtonElement>>(
765
+ (event) => {
766
+ if (event.key === "Enter" || event.key === " ") {
767
+ event.preventDefault();
768
+
769
+ const action = operation === "increment" ? onIncrement : onDecrement;
770
+
771
+ action();
772
+ }
773
+ },
774
+ [onDecrement, onIncrement, operation],
775
+ );
776
+
777
+ return (
778
+ <button
779
+ aria-controls={id}
780
+ aria-label={operation === "increment" ? ariaIncrementLabel : ariaDecrementLabel}
781
+ aria-live="polite"
782
+ disabled={isDisabled}
783
+ type="button"
784
+ onContextMenu={handleContextMenu}
785
+ onKeyDown={handleKeyDown}
786
+ onPointerCancel={clearActionInterval}
787
+ onPointerDown={handlePointerDown}
788
+ onPointerLeave={clearActionInterval}
789
+ onPointerUp={clearActionInterval}
790
+ {...props}
791
+ />
792
+ );
793
+ }
794
+
795
+ /* -----------------------------------------------------------------------------
796
+ * Component: InputNumberIncrementButton
797
+ * -------------------------------------------------------------------------- */
798
+
799
+ /**
800
+ * @since 0.3.16-canary.0
801
+ */
802
+ type InputNumberIncrementButtonProps = Omit<ComponentProps<typeof NumberStepperButton>, "operation">;
803
+
804
+ /**
805
+ * @since 0.3.16-canary.0
806
+ */
807
+ function InputNumberIncrementButton(props: InputNumberIncrementButtonProps): JSX.Element {
808
+ return <NumberStepperButton operation="increment" {...props} />;
809
+ }
810
+
811
+ /* -----------------------------------------------------------------------------
812
+ * Component: InputNumberDecrementButton
813
+ * -------------------------------------------------------------------------- */
814
+
815
+ /**
816
+ * @since 0.3.16-canary.0
817
+ */
818
+ type InputNumberDecrementButtonProps = Omit<ComponentProps<typeof NumberStepperButton>, "operation">;
819
+
820
+ /**
821
+ * @since 0.3.16-canary.0
822
+ */
823
+ function InputNumberDecrementButton(props: InputNumberDecrementButtonProps): JSX.Element {
824
+ return <NumberStepperButton operation="decrement" {...props} />;
825
+ }
826
+
827
+ /* -----------------------------------------------------------------------------
828
+ * Utility Functions
829
+ * -------------------------------------------------------------------------- */
830
+
831
+ /**
832
+ * Chains multiple callbacks into a single function
833
+ *
834
+ * @param callbacks - Array of callback functions that will be executed in order
835
+ * @returns A single function that executes all callbacks
836
+ */
837
+ function chain<T extends Array<unknown>>(...callbacks: Array<(...args: T) => void>): (...args: T) => void {
838
+ return (...args: T) => {
839
+ for (const callback of callbacks) {
840
+ callback(...args);
841
+ }
842
+ };
843
+ }
844
+
845
+ /**
846
+ * Interface for number formatting separators
847
+ */
848
+ interface NumberFormatSeparators {
849
+ /**
850
+ * The character used to separate decimal part (e.g., "." or ",")
851
+ */
852
+ decimalSeparator: string;
853
+ /**
854
+ * The character used to separate thousands (e.g., "," or ".")
855
+ */
856
+ thousandSeparator: string;
857
+ }
858
+
859
+ /**
860
+ * Extracts decimal and a thousand separators from a given locale's number format
861
+ *
862
+ * @param locale - The locale string to use for number formatting (e.g., 'en-US', 'de-DE')
863
+ * @returns Object containing decimal and a thousand separators
864
+ */
865
+ function getNumberFormatSeparators(locale?: string): NumberFormatSeparators {
866
+ const numberFormat = new Intl.NumberFormat(locale);
867
+ const parts = numberFormat.formatToParts(12_345.6);
868
+ let thousandSeparator = "";
869
+ let decimalSeparator = "";
870
+
871
+ for (const part of parts) {
872
+ if (part.type === "group") {
873
+ thousandSeparator = part.value;
874
+ }
875
+
876
+ if (part.type === "decimal") {
877
+ decimalSeparator = part.value;
878
+ }
879
+
880
+ // Stop early if you've found enough.
881
+ if (thousandSeparator && decimalSeparator) {
882
+ break;
883
+ }
884
+ }
885
+
886
+ return { decimalSeparator, thousandSeparator };
887
+ }
888
+
889
+ /**
890
+ * Normalizes an input value by removing formatting characters
891
+ *
892
+ * @param value - The input string to normalize
893
+ * @param thousandSeparator - The thousand-separator character to remove
894
+ * @param decimalSeparator - The decimal separator to convert to standard dot notation
895
+ * @returns Normalized string value ready for numeric conversion
896
+ */
897
+ function normalizeInputValue(value: string, thousandSeparator: string, decimalSeparator: string): string {
898
+ return value
899
+ .replaceAll(new RegExp(`\\${thousandSeparator}`, "g"), "")
900
+ .replace(new RegExp(`\\${decimalSeparator}`), ".")
901
+ .replaceAll(/[()]/g, "-");
902
+ }
903
+
904
+ /**
905
+ * Checks if a keyboard event includes modifier keys (Ctrl, Alt, Meta, Shift)
906
+ *
907
+ * @param event - The keyboard event to check
908
+ * @returns True if any modifier key is pressed
909
+ */
910
+ function isModifierKey(event: KeyboardEvent<HTMLInputElement>): boolean {
911
+ return event.ctrlKey || event.altKey || event.metaKey || event.shiftKey;
912
+ }
913
+
914
+ /**
915
+ * Determines if a key is a function key (F1-F12)
916
+ *
917
+ * @param key - The key name to check
918
+ * @returns True if the key is a function key
919
+ */
920
+ function isFunctionKey(key: string): boolean {
921
+ return key.startsWith("F") && key.length > 1;
922
+ }
923
+
924
+ /**
925
+ * Checks if a key represents a number (0-9)
926
+ *
927
+ * @param key - The key name to check
928
+ * @returns True if the key represents a number
929
+ */
930
+ function isNumberKey(key: string): boolean {
931
+ return !Number.isNaN(Number(key));
932
+ }
933
+
934
+ /**
935
+ * Clamps a numeric value between a minimum and maximum
936
+ *
937
+ * @param value - The value to clamp
938
+ * @param min - The minimum allowed value (defaults to \-Infinity)
939
+ * @param max - The maximum allowed value (defaults to Infinity)
940
+ * @returns The clamped value
941
+ */
942
+ function clamp(value: number, min = Number.NEGATIVE_INFINITY, max = Number.POSITIVE_INFINITY): number {
943
+ return Math.min(Math.max(value, min), max);
944
+ }
945
+
946
+ /* -----------------------------------------------------------------------------
947
+ * Exports
948
+ * -------------------------------------------------------------------------- */
949
+
950
+ export {
951
+ createInputNumberScope,
952
+ InputNumberDecrementButton as DecrementButton,
953
+ InputNumberField as Field,
954
+ InputNumberIncrementButton as IncrementButton,
955
+ InputNumber,
956
+ InputNumberDecrementButton,
957
+ InputNumberField,
958
+ InputNumberIncrementButton,
959
+ InputNumber as Root,
960
+ };
961
+
962
+ export type {
963
+ InputNumberDecrementButtonProps,
964
+ InputNumberFieldProps,
965
+ InputNumberIncrementButtonProps,
966
+ InputNumberProps,
967
+ };