@input-kit/number 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Input Kit
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,161 @@
1
+ # @input-kit/number
2
+
3
+ Headless number input component for React with locale support, currency formatting, and full TypeScript support.
4
+
5
+ ## Features
6
+
7
+ - **Headless** - Bring your own styles
8
+ - **Locale-aware** - Automatic number formatting for any locale
9
+ - **Currency support** - Built-in currency formatting
10
+ - **Keyboard navigation** - Arrow keys and Home/End support
11
+ - **Accessible** - ARIA attributes and keyboard support
12
+ - **Tiny** - Tree-shakeable, minimal dependencies
13
+ - **TypeScript** - Full type safety
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install @input-kit/number
19
+ # or
20
+ yarn add @input-kit/number
21
+ # or
22
+ pnpm add @input-kit/number
23
+ ```
24
+
25
+ ## Quick Start
26
+
27
+ ### Using the Hook (Recommended)
28
+
29
+ ```tsx
30
+ import { useNumberInput } from '@input-kit/number';
31
+
32
+ function MyComponent() {
33
+ const { inputProps, value, increment, decrement } = useNumberInput({
34
+ defaultValue: 100,
35
+ min: 0,
36
+ max: 1000,
37
+ step: 10,
38
+ });
39
+
40
+ return (
41
+ <div>
42
+ <input {...inputProps} className="my-input" />
43
+ <button onClick={decrement}>-</button>
44
+ <span>{value}</span>
45
+ <button onClick={increment}>+</button>
46
+ </div>
47
+ );
48
+ }
49
+ ```
50
+
51
+ ### Using the Component
52
+
53
+ ```tsx
54
+ import { NumberInput } from '@input-kit/number';
55
+
56
+ function MyComponent() {
57
+ const [value, setValue] = useState<number | null>(100);
58
+
59
+ return (
60
+ <NumberInput
61
+ value={value}
62
+ onChange={setValue}
63
+ min={0}
64
+ max={1000}
65
+ step={10}
66
+ className="my-input"
67
+ />
68
+ );
69
+ }
70
+ ```
71
+
72
+ ## Currency Formatting
73
+
74
+ ```tsx
75
+ const { inputProps, formattedValue } = useNumberInput({
76
+ format: 'currency',
77
+ currency: 'EUR',
78
+ locale: 'de-DE',
79
+ });
80
+
81
+ // Displays: 1.234,56 €
82
+ ```
83
+
84
+ ## Percentage Formatting
85
+
86
+ ```tsx
87
+ const { inputProps } = useNumberInput({
88
+ format: 'percent',
89
+ decimals: 1,
90
+ });
91
+
92
+ // Displays: 15.5%
93
+ ```
94
+
95
+ ## API Reference
96
+
97
+ ### `useNumberInput(options)`
98
+
99
+ #### Options
100
+
101
+ | Option | Type | Default | Description |
102
+ |--------|------|---------|-------------|
103
+ | `value` | `number \| null` | - | Controlled value |
104
+ | `defaultValue` | `number \| null` | `null` | Default value (uncontrolled) |
105
+ | `onChange` | `(value: number \| null) => void` | - | Change handler |
106
+ | `min` | `number` | - | Minimum value |
107
+ | `max` | `number` | - | Maximum value |
108
+ | `step` | `number` | `1` | Step increment |
109
+ | `decimals` | `number` | - | Number of decimal places |
110
+ | `format` | `'decimal' \| 'currency' \| 'percent'` | `'decimal'` | Format style |
111
+ | `currency` | `string` | `'USD'` | Currency code (for currency format) |
112
+ | `currencyDisplay` | `'symbol' \| 'narrowSymbol' \| 'code' \| 'name'` | `'symbol'` | Currency display style |
113
+ | `locale` | `string \| string[]` | Browser locale | Locale for formatting |
114
+ | `allowNegative` | `boolean` | `true` | Allow negative numbers |
115
+ | `allowEmpty` | `boolean` | `true` | Allow empty value |
116
+ | `formatter` | `(value: number \| null) => string` | - | Custom formatter |
117
+ | `parser` | `(value: string) => number \| null` | - | Custom parser |
118
+
119
+ #### Returns
120
+
121
+ | Property | Type | Description |
122
+ |----------|------|-------------|
123
+ | `value` | `number \| null` | Current numeric value |
124
+ | `formattedValue` | `string` | Formatted display value |
125
+ | `inputValue` | `string` | Raw input value |
126
+ | `isFocused` | `boolean` | Whether input is focused |
127
+ | `isValid` | `boolean` | Whether value is valid |
128
+ | `error` | `string \| null` | Validation error message |
129
+ | `setValue` | `(value: number \| null) => void` | Set value directly |
130
+ | `increment` | `() => void` | Increment by step |
131
+ | `decrement` | `() => void` | Decrement by step |
132
+ | `clear` | `() => void` | Clear the input |
133
+ | `focus` | `() => void` | Focus the input |
134
+ | `blur` | `() => void` | Blur the input |
135
+ | `inputProps` | `object` | Props to spread on input element |
136
+
137
+ ### `NumberInput` Component
138
+
139
+ All hook options plus standard input props:
140
+
141
+ - `className`
142
+ - `placeholder`
143
+ - `disabled`
144
+ - `readOnly`
145
+ - `name`
146
+ - `id`
147
+ - `aria-label`
148
+ - `aria-labelledby`
149
+
150
+ ## Keyboard Shortcuts
151
+
152
+ | Key | Action |
153
+ |-----|--------|
154
+ | `↑` | Increment by step |
155
+ | `↓` | Decrement by step |
156
+ | `Home` | Set to minimum (if defined) |
157
+ | `End` | Set to maximum (if defined) |
158
+
159
+ ## License
160
+
161
+ MIT © Input Kit
package/dist/index.cjs ADDED
@@ -0,0 +1,2 @@
1
+ 'use strict';var react=require('react'),jsxRuntime=require('react/jsx-runtime');function v(){return typeof navigator<"u"&&navigator.language||"en-US"}function T(e,u=v()){if(!e||e.trim()==="")return null;let o=new Intl.NumberFormat(u).formatToParts(1234.5).find(y=>y.type==="decimal")?.value||".",l=e.trim(),t=l.includes("-")||l.includes("(")&&l.includes(")"),n=o.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),m=new RegExp(`[^\\d${n}-]`,"g");l=l.replace(m,""),o!=="."&&(l=l.replace(o,"."));let f=l.split(".");f.length>2&&(l=f[0]+"."+f.slice(1).join(""));let i=parseFloat(l);return isNaN(i)?null:t?-Math.abs(i):i}function D(e,u={}){if(e===null||isNaN(e))return "";let{format:r="decimal",locale:c=v(),decimals:o,currency:l="USD",currencyDisplay:t="symbol"}=u,n={};return r==="currency"?(n.style="currency",n.currency=l,n.currencyDisplay=t):r==="percent"?n.style="percent":r==="decimal"&&(n.style="decimal"),o!==void 0&&(n.minimumFractionDigits=o,n.maximumFractionDigits=o),new Intl.NumberFormat(c,n).format(e)}function U(e,u,r){return u!==void 0&&(e=Math.max(e,u)),r!==void 0&&(e=Math.min(e,r)),e}function B(e,u){let r=Math.pow(10,u);return Math.round(e*r)/r}function H(e,u=1,r,c,o){let t=(e??r??0)+u;return t=U(t,r,c),o!==void 0&&(t=B(t,o)),t}function $(e,u=1,r,c,o){let t=(e??c??0)-u;return t=U(t,r,c),o!==void 0&&(t=B(t,o)),t}function A(e,u,r,c=true,o=true){return e===null?{isValid:o,error:o?null:"Value is required"}:isNaN(e)?{isValid:false,error:"Invalid number"}:!c&&e<0?{isValid:false,error:"Negative numbers are not allowed"}:u!==void 0&&e<u?{isValid:false,error:`Minimum value is ${u}`}:r!==void 0&&e>r?{isValid:false,error:`Maximum value is ${r}`}:{isValid:true,error:null}}function z(e={}){let{value:u,defaultValue:r=null,onChange:c,onBlur:o,onFocus:l,locale:t=v(),min:n,max:m,step:f=1,decimals:i,format:y,currency:M,currencyDisplay:w,allowNegative:F=true,allowEmpty:N=true,formatter:d,parser:g}=e,V=u!==void 0,h=react.useRef(null),[L,P]=react.useState(r),[x,R]=react.useState(false),[C,E]=react.useState(""),s=V?u:L,I=react.useMemo(()=>({format:y,locale:t,decimals:i,currency:M,currencyDisplay:w}),[y,t,i,M,w]);react.useEffect(()=>{if(!x){let a=d?d(s):D(s,I);E(a);}},[s,x,d,I]);let{isValid:_,error:J}=react.useMemo(()=>A(s,n,m,F,N),[s,n,m,F,N]),p=react.useCallback(a=>{V||P(a),c?.(a);},[V,c]),k=react.useCallback(()=>{let a=H(s,f,n,m,i);p(a);},[s,f,n,m,i,p]),S=react.useCallback(()=>{let a=$(s,f,n,m,i);p(a);},[s,f,n,m,i,p]),Q=react.useCallback(()=>{p(N?null:n??0),E("");},[p,N,n]),W=react.useCallback(()=>{h.current?.focus();},[]),X=react.useCallback(()=>{h.current?.blur();},[]),Y=react.useCallback(a=>{let O=a.target.value;E(O);let j=g?g(O):T(O,t);if(j!==null){let G=Math.min(Math.max(j,n??-1/0),m??1/0),te=i!==void 0?Math.round(G*Math.pow(10,i))/Math.pow(10,i):G;p(te);}else N&&O===""&&p(null);},[t,g,n,m,i,N,p]),Z=react.useCallback(a=>{R(true),a.target.select(),l?.();},[l]),ee=react.useCallback(()=>{R(false);let a=d?d(s):D(s,I);E(a),o?.();},[s,d,I,o]),ne=react.useCallback(a=>{switch(a.key){case "ArrowUp":a.preventDefault(),k();break;case "ArrowDown":a.preventDefault(),S();break;case "Home":n!==void 0&&(a.preventDefault(),p(n));break;case "End":m!==void 0&&(a.preventDefault(),p(m));break}},[k,S,n,m,p]),re=react.useMemo(()=>d?d(s):D(s,I),[s,d,I]);return {value:s,formattedValue:re,inputValue:C,isFocused:x,isValid:_,error:J,setValue:p,increment:k,decrement:S,clear:Q,focus:W,blur:X,inputProps:{ref:h,value:C,onChange:Y,onFocus:Z,onBlur:ee,onKeyDown:ne,type:"text",inputMode:"decimal",autoComplete:"off",autoCorrect:"off",spellCheck:false,"aria-invalid":_?void 0:true}}}var ie=react.forwardRef(function(u,r){let{value:c,defaultValue:o,onChange:l,onBlur:t,onFocus:n,className:m,placeholder:f,disabled:i,readOnly:y,name:M,id:w,"aria-label":F,"aria-labelledby":N,...d}=u,{inputProps:g,increment:V,decrement:h,clear:L,focus:P,blur:x,isValid:R,error:C}=z({value:c,defaultValue:o,onChange:l,onBlur:t,onFocus:n,...d});return react.useImperativeHandle(r,()=>({focus:P,blur:x,clear:L,increment:V,decrement:h})),jsxRuntime.jsx("input",{...g,id:w,name:M,className:m,placeholder:f,disabled:i,readOnly:y,"aria-label":F,"aria-labelledby":N,"data-valid":R,"data-error":C??void 0})});exports.NumberInput=ie;exports.clamp=U;exports.decrementValue=$;exports.formatNumber=D;exports.getDefaultLocale=v;exports.incrementValue=H;exports.parseNumber=T;exports.roundToDecimals=B;exports.useNumberInput=z;exports.validateNumber=A;//# sourceMappingURL=index.cjs.map
2
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils.ts","../src/useNumberInput.ts","../src/NumberInput.tsx"],"names":["getDefaultLocale","parseNumber","value","locale","decimalSeparator","p","normalized","isNegative","escapedSeparator","regex","parts_split","num","formatNumber","options","format","decimals","currency","currencyDisplay","formatOptions","clamp","min","max","roundToDecimals","multiplier","incrementValue","step","next","decrementValue","validateNumber","allowNegative","allowEmpty","useNumberInput","controlledValue","defaultValue","onChange","onBlur","onFocus","formatter","parser","isControlled","inputRef","useRef","internalValue","setInternalValue","useState","isFocused","setIsFocused","inputValue","setInputValue","useMemo","useEffect","formatted","isValid","error","setValue","useCallback","newValue","increment","decrement","clear","focus","blur","handleInputChange","event","rawValue","parsed","clamped","finalValue","handleFocus","handleBlur","handleKeyDown","formattedValue","NumberInput","forwardRef","props","ref","className","placeholder","disabled","readOnly","name","id","ariaLabel","ariaLabelledBy","inputProps","useImperativeHandle","jsx"],"mappings":"gFAKO,SAASA,CAAAA,EAA2B,CACzC,OAAO,OAAO,SAAA,CAAc,GAAA,EACxB,SAAA,CAAU,QAAA,EAAY,OAE5B,CAKO,SAASC,CAAAA,CACdC,CAAAA,CACAC,CAAAA,CAA4BH,CAAAA,EAAiB,CAC9B,CACf,GAAI,CAACE,CAAAA,EAASA,CAAAA,CAAM,IAAA,EAAK,GAAM,EAAA,CAAI,OAAO,IAAA,CAK1C,IAAME,CAAAA,CAFQ,IAAI,IAAA,CAAK,YAAA,CAAaD,CAAM,CAAA,CAAE,aAAA,CAAc,MAAM,CAAA,CACtC,IAAA,CAAKE,CAAAA,EAAKA,CAAAA,CAAE,IAAA,GAAS,SAAS,CAAA,EAClB,OAAS,GAAA,CAG3CC,CAAAA,CAAaJ,CAAAA,CAAM,IAAA,EAAK,CAGtBK,CAAAA,CAAaD,CAAAA,CAAW,QAAA,CAAS,GAAG,CAAA,EACvCA,CAAAA,CAAW,QAAA,CAAS,GAAG,CAAA,EAAKA,CAAAA,CAAW,QAAA,CAAS,GAAG,CAAA,CAGhDE,CAAAA,CAAmBJ,CAAAA,CAAiB,OAAA,CAAQ,qBAAA,CAAuB,MAAM,CAAA,CACzEK,CAAAA,CAAQ,IAAI,MAAA,CAAO,CAAA,KAAA,EAAQD,CAAgB,CAAA,EAAA,CAAA,CAAM,GAAG,CAAA,CAC1DF,EAAaA,CAAAA,CAAW,OAAA,CAAQG,CAAAA,CAAO,EAAE,CAAA,CAGrCL,CAAAA,GAAqB,GAAA,GACvBE,CAAAA,CAAaA,CAAAA,CAAW,OAAA,CAAQF,CAAAA,CAAkB,GAAG,CAAA,CAAA,CAIvD,IAAMM,CAAAA,CAAcJ,CAAAA,CAAW,KAAA,CAAM,GAAG,CAAA,CACpCI,CAAAA,CAAY,MAAA,CAAS,CAAA,GACvBJ,CAAAA,CAAaI,CAAAA,CAAY,CAAC,CAAA,CAAI,GAAA,CAAMA,CAAAA,CAAY,KAAA,CAAM,CAAC,CAAA,CAAE,IAAA,CAAK,EAAE,CAAA,CAAA,CAGlE,IAAMC,CAAAA,CAAM,UAAA,CAAWL,CAAU,CAAA,CAEjC,OAAI,KAAA,CAAMK,CAAG,CAAA,CAAU,IAAA,CAEhBJ,CAAAA,CAAa,CAAC,IAAA,CAAK,GAAA,CAAII,CAAG,CAAA,CAAIA,CACvC,CAKO,SAASC,CAAAA,CACdV,CAAAA,CACAW,CAAAA,CAA8B,EAAC,CACvB,CACR,GAAIX,CAAAA,GAAU,IAAA,EAAQ,KAAA,CAAMA,CAAK,CAAA,CAAG,OAAO,EAAA,CAE3C,GAAM,CACJ,MAAA,CAAAY,CAAAA,CAAS,SAAA,CACT,MAAA,CAAAX,CAAAA,CAASH,CAAAA,EAAiB,CAC1B,QAAA,CAAAe,CAAAA,CACA,QAAA,CAAAC,CAAAA,CAAW,KAAA,CACX,eAAA,CAAAC,CAAAA,CAAkB,QACpB,CAAA,CAAIJ,CAAAA,CAEEK,CAAAA,CAA0C,EAAC,CAEjD,OAAIJ,CAAAA,GAAW,UAAA,EACbI,CAAAA,CAAc,KAAA,CAAQ,UAAA,CACtBA,CAAAA,CAAc,QAAA,CAAWF,EACzBE,CAAAA,CAAc,eAAA,CAAkBD,CAAAA,EACvBH,CAAAA,GAAW,SAAA,CACpBI,CAAAA,CAAc,KAAA,CAAQ,SAAA,CACbJ,CAAAA,GAAW,SAAA,GACpBI,CAAAA,CAAc,KAAA,CAAQ,SAAA,CAAA,CAGpBH,CAAAA,GAAa,MAAA,GACfG,CAAAA,CAAc,qBAAA,CAAwBH,CAAAA,CACtCG,CAAAA,CAAc,qBAAA,CAAwBH,CAAAA,CAAAA,CAGjC,IAAI,IAAA,CAAK,YAAA,CAAaZ,CAAAA,CAAQe,CAAa,CAAA,CAAE,MAAA,CAAOhB,CAAK,CAClE,CAKO,SAASiB,EAAMjB,CAAAA,CAAekB,CAAAA,CAAcC,CAAAA,CAAsB,CACvE,OAAID,CAAAA,GAAQ,MAAA,GAAWlB,CAAAA,CAAQ,IAAA,CAAK,GAAA,CAAIA,CAAAA,CAAOkB,CAAG,CAAA,CAAA,CAC9CC,CAAAA,GAAQ,MAAA,GAAWnB,CAAAA,CAAQ,IAAA,CAAK,GAAA,CAAIA,CAAAA,CAAOmB,CAAG,CAAA,CAAA,CAC3CnB,CACT,CAKO,SAASoB,CAAAA,CAAgBpB,CAAAA,CAAea,CAAAA,CAA0B,CACvE,IAAMQ,CAAAA,CAAa,IAAA,CAAK,GAAA,CAAI,GAAIR,CAAQ,CAAA,CACxC,OAAO,IAAA,CAAK,KAAA,CAAMb,CAAAA,CAAQqB,CAAU,CAAA,CAAIA,CAC1C,CAKO,SAASC,CAAAA,CACdtB,CAAAA,CACAuB,CAAAA,CAAe,CAAA,CACfL,CAAAA,CACAC,CAAAA,CACAN,CAAAA,CACQ,CAER,IAAIW,CAAAA,CAAAA,CADYxB,CAAAA,EAASkB,CAAAA,EAAO,CAAA,EACXK,CAAAA,CACrB,OAAAC,CAAAA,CAAOP,CAAAA,CAAMO,CAAAA,CAAMN,CAAAA,CAAKC,CAAG,CAAA,CACvBN,CAAAA,GAAa,MAAA,GACfW,CAAAA,CAAOJ,CAAAA,CAAgBI,CAAAA,CAAMX,CAAQ,CAAA,CAAA,CAEhCW,CACT,CAKO,SAASC,CAAAA,CACdzB,CAAAA,CACAuB,CAAAA,CAAe,CAAA,CACfL,CAAAA,CACAC,CAAAA,CACAN,CAAAA,CACQ,CAER,IAAIW,CAAAA,CAAAA,CADYxB,CAAAA,EAASmB,CAAAA,EAAO,CAAA,EACXI,CAAAA,CACrB,OAAAC,CAAAA,CAAOP,CAAAA,CAAMO,CAAAA,CAAMN,CAAAA,CAAKC,CAAG,CAAA,CACvBN,CAAAA,GAAa,SACfW,CAAAA,CAAOJ,CAAAA,CAAgBI,CAAAA,CAAMX,CAAQ,CAAA,CAAA,CAEhCW,CACT,CAKO,SAASE,CAAAA,CACd1B,CAAAA,CACAkB,CAAAA,CACAC,CAAAA,CACAQ,CAAAA,CAAyB,IAAA,CACzBC,CAAAA,CAAsB,IAAA,CACsB,CAC5C,OAAI5B,CAAAA,GAAU,IAAA,CACL,CACL,OAAA,CAAS4B,CAAAA,CACT,KAAA,CAAOA,CAAAA,CAAa,IAAA,CAAO,mBAC7B,CAAA,CAGE,KAAA,CAAM5B,CAAK,CAAA,CACN,CAAE,QAAS,KAAA,CAAO,KAAA,CAAO,gBAAiB,CAAA,CAG/C,CAAC2B,CAAAA,EAAiB3B,CAAAA,CAAQ,CAAA,CACrB,CAAE,OAAA,CAAS,KAAA,CAAO,KAAA,CAAO,kCAAmC,CAAA,CAGjEkB,CAAAA,GAAQ,MAAA,EAAalB,CAAAA,CAAQkB,CAAAA,CACxB,CAAE,OAAA,CAAS,KAAA,CAAO,KAAA,CAAO,CAAA,iBAAA,EAAoBA,CAAG,CAAA,CAAG,CAAA,CAGxDC,CAAAA,GAAQ,MAAA,EAAanB,CAAAA,CAAQmB,CAAAA,CACxB,CAAE,OAAA,CAAS,MAAO,KAAA,CAAO,CAAA,iBAAA,EAAoBA,CAAG,CAAA,CAAG,CAAA,CAGrD,CAAE,OAAA,CAAS,IAAA,CAAM,KAAA,CAAO,IAAK,CACtC,CC1KO,SAASU,CAAAA,CACdlB,CAAAA,CAMI,EAAC,CACiB,CACtB,GAAM,CACJ,KAAA,CAAOmB,CAAAA,CACP,YAAA,CAAAC,CAAAA,CAAe,IAAA,CACf,QAAA,CAAAC,CAAAA,CACA,MAAA,CAAAC,CAAAA,CACA,OAAA,CAAAC,CAAAA,CACA,MAAA,CAAAjC,EAASH,CAAAA,EAAiB,CAC1B,GAAA,CAAAoB,CAAAA,CACA,GAAA,CAAAC,CAAAA,CACA,IAAA,CAAAI,CAAAA,CAAO,CAAA,CACP,QAAA,CAAAV,CAAAA,CACA,MAAA,CAAAD,CAAAA,CACA,QAAA,CAAAE,CAAAA,CACA,eAAA,CAAAC,CAAAA,CACA,aAAA,CAAAY,CAAAA,CAAgB,IAAA,CAChB,UAAA,CAAAC,CAAAA,CAAa,IAAA,CACb,SAAA,CAAAO,CAAAA,CACA,MAAA,CAAAC,CACF,CAAA,CAAIzB,CAAAA,CAEE0B,CAAAA,CAAeP,CAAAA,GAAoB,MAAA,CACnCQ,EAAWC,YAAAA,CAAyB,IAAI,CAAA,CAGxC,CAACC,CAAAA,CAAeC,CAAgB,CAAA,CAAIC,cAAAA,CAAwBX,CAAY,CAAA,CACxE,CAACY,CAAAA,CAAWC,CAAY,CAAA,CAAIF,cAAAA,CAAS,KAAK,CAAA,CAC1C,CAACG,CAAAA,CAAYC,CAAa,CAAA,CAAIJ,cAAAA,CAAS,EAAE,CAAA,CAGzC1C,CAAAA,CAAQqC,CAAAA,CAAeP,CAAAA,CAAkBU,CAAAA,CAGzCxB,CAAAA,CAAgB+B,aAAAA,CACpB,KAAO,CACL,OAAAnC,CAAAA,CACA,MAAA,CAAAX,CAAAA,CACA,QAAA,CAAAY,CAAAA,CACA,QAAA,CAAAC,CAAAA,CACA,eAAA,CAAAC,CACF,CAAA,CAAA,CACA,CAACH,CAAAA,CAAQX,CAAAA,CAAQY,CAAAA,CAAUC,CAAAA,CAAUC,CAAe,CACtD,CAAA,CAGAiC,eAAAA,CAAU,IAAM,CACd,GAAI,CAACL,CAAAA,CAAW,CACd,IAAMM,CAAAA,CAAYd,CAAAA,CACdA,CAAAA,CAAUnC,CAAK,CAAA,CACfU,CAAAA,CAAaV,EAAOgB,CAAa,CAAA,CACrC8B,CAAAA,CAAcG,CAAS,EACzB,CACF,CAAA,CAAG,CAACjD,CAAAA,CAAO2C,CAAAA,CAAWR,CAAAA,CAAWnB,CAAa,CAAC,CAAA,CAG/C,GAAM,CAAE,OAAA,CAAAkC,CAAAA,CAAS,KAAA,CAAAC,CAAM,CAAA,CAAIJ,aAAAA,CACzB,IAAMrB,CAAAA,CAAe1B,CAAAA,CAAOkB,CAAAA,CAAKC,CAAAA,CAAKQ,CAAAA,CAAeC,CAAU,CAAA,CAC/D,CAAC5B,CAAAA,CAAOkB,CAAAA,CAAKC,CAAAA,CAAKQ,CAAAA,CAAeC,CAAU,CAC7C,CAAA,CAGMwB,CAAAA,CAAWC,iBAAAA,CACdC,CAAAA,EAA4B,CACtBjB,CAAAA,EACHI,CAAAA,CAAiBa,CAAQ,CAAA,CAE3BtB,CAAAA,GAAWsB,CAAQ,EACrB,CAAA,CACA,CAACjB,CAAAA,CAAcL,CAAQ,CACzB,CAAA,CAGMuB,CAAAA,CAAYF,iBAAAA,CAAY,IAAM,CAClC,IAAMC,CAAAA,CAAWhC,CAAAA,CAAetB,CAAAA,CAAOuB,CAAAA,CAAML,EAAKC,CAAAA,CAAKN,CAAQ,CAAA,CAC/DuC,CAAAA,CAASE,CAAQ,EACnB,CAAA,CAAG,CAACtD,CAAAA,CAAOuB,CAAAA,CAAML,CAAAA,CAAKC,CAAAA,CAAKN,CAAAA,CAAUuC,CAAQ,CAAC,CAAA,CAGxCI,CAAAA,CAAYH,iBAAAA,CAAY,IAAM,CAClC,IAAMC,CAAAA,CAAW7B,CAAAA,CAAezB,CAAAA,CAAOuB,CAAAA,CAAML,CAAAA,CAAKC,CAAAA,CAAKN,CAAQ,CAAA,CAC/DuC,CAAAA,CAASE,CAAQ,EACnB,EAAG,CAACtD,CAAAA,CAAOuB,CAAAA,CAAML,CAAAA,CAAKC,CAAAA,CAAKN,CAAAA,CAAUuC,CAAQ,CAAC,CAAA,CAGxCK,CAAAA,CAAQJ,iBAAAA,CAAY,IAAM,CAC9BD,CAAAA,CAASxB,CAAAA,CAAa,IAAA,CAAOV,CAAAA,EAAO,CAAC,CAAA,CACrC4B,CAAAA,CAAc,EAAE,EAClB,CAAA,CAAG,CAACM,CAAAA,CAAUxB,CAAAA,CAAYV,CAAG,CAAC,CAAA,CAGxBwC,CAAAA,CAAQL,iBAAAA,CAAY,IAAM,CAC9Bf,CAAAA,CAAS,OAAA,EAAS,KAAA,GACpB,CAAA,CAAG,EAAE,CAAA,CAGCqB,CAAAA,CAAON,iBAAAA,CAAY,IAAM,CAC7Bf,CAAAA,CAAS,OAAA,EAAS,IAAA,GACpB,CAAA,CAAG,EAAE,CAAA,CAGCsB,CAAAA,CAAoBP,iBAAAA,CACvBQ,CAAAA,EAA+C,CAC9C,IAAMC,CAAAA,CAAWD,CAAAA,CAAM,MAAA,CAAO,KAAA,CAC9Bf,CAAAA,CAAcgB,CAAQ,CAAA,CAEtB,IAAMC,CAAAA,CAAS3B,CAAAA,CAASA,CAAAA,CAAO0B,CAAQ,CAAA,CAAI/D,CAAAA,CAAY+D,CAAAA,CAAU7D,CAAM,CAAA,CAEvE,GAAI8D,CAAAA,GAAW,IAAA,CAAM,CACnB,IAAMC,CAAAA,CAAU,IAAA,CAAK,GAAA,CAAI,IAAA,CAAK,GAAA,CAAID,CAAAA,CAAQ7C,CAAAA,EAAO,EAAA,CAAA,CAAS,CAAA,CAAGC,CAAAA,EAAO,CAAA,CAAA,CAAQ,CAAA,CACtE8C,EAAAA,CAAapD,CAAAA,GAAa,MAAA,CAC5B,IAAA,CAAK,KAAA,CAAMmD,CAAAA,CAAU,KAAK,GAAA,CAAI,EAAA,CAAInD,CAAQ,CAAC,CAAA,CAAI,IAAA,CAAK,GAAA,CAAI,EAAA,CAAIA,CAAQ,CAAA,CACpEmD,CAAAA,CACJZ,CAAAA,CAASa,EAAU,EACrB,CAAA,KAAWrC,CAAAA,EAAckC,CAAAA,GAAa,EAAA,EACpCV,CAAAA,CAAS,IAAI,EAEjB,CAAA,CACA,CAACnD,CAAAA,CAAQmC,CAAAA,CAAQlB,CAAAA,CAAKC,CAAAA,CAAKN,CAAAA,CAAUe,CAAAA,CAAYwB,CAAQ,CAC3D,CAAA,CAGMc,EAAcb,iBAAAA,CACjBQ,CAAAA,EAA8C,CAC7CjB,CAAAA,CAAa,IAAI,CAAA,CAEjBiB,CAAAA,CAAM,MAAA,CAAO,MAAA,EAAO,CACpB3B,CAAAA,KACF,CAAA,CACA,CAACA,CAAO,CACV,CAAA,CAGMiC,EAAAA,CAAad,iBAAAA,CAAY,IAAM,CACnCT,CAAAA,CAAa,KAAK,CAAA,CAElB,IAAMK,CAAAA,CAAYd,CAAAA,CACdA,CAAAA,CAAUnC,CAAK,CAAA,CACfU,CAAAA,CAAaV,CAAAA,CAAOgB,CAAa,CAAA,CACrC8B,CAAAA,CAAcG,CAAS,CAAA,CACvBhB,CAAAA,KACF,CAAA,CAAG,CAACjC,CAAAA,CAAOmC,CAAAA,CAAWnB,CAAAA,CAAeiB,CAAM,CAAC,CAAA,CAGtCmC,EAAAA,CAAgBf,iBAAAA,CACnBQ,CAAAA,EAAiD,CAChD,OAAQA,CAAAA,CAAM,GAAA,EACZ,KAAK,SAAA,CACHA,CAAAA,CAAM,cAAA,EAAe,CACrBN,CAAAA,EAAU,CACV,MACF,KAAK,WAAA,CACHM,CAAAA,CAAM,cAAA,EAAe,CACrBL,CAAAA,EAAU,CACV,MACF,KAAK,MAAA,CACCtC,CAAAA,GAAQ,MAAA,GACV2C,CAAAA,CAAM,cAAA,EAAe,CACrBT,CAAAA,CAASlC,CAAG,CAAA,CAAA,CAEd,MACF,KAAK,KAAA,CACCC,CAAAA,GAAQ,MAAA,GACV0C,CAAAA,CAAM,cAAA,EAAe,CACrBT,CAAAA,CAASjC,CAAG,CAAA,CAAA,CAEd,KACJ,CACF,CAAA,CACA,CAACoC,CAAAA,CAAWC,EAAWtC,CAAAA,CAAKC,CAAAA,CAAKiC,CAAQ,CAC3C,CAAA,CAGMiB,EAAAA,CAAiBtB,aAAAA,CACrB,IAAOZ,CAAAA,CAAYA,CAAAA,CAAUnC,CAAK,CAAA,CAAIU,CAAAA,CAAaV,CAAAA,CAAOgB,CAAa,CAAA,CACvE,CAAChB,CAAAA,CAAOmC,CAAAA,CAAWnB,CAAa,CAClC,CAAA,CAEA,OAAO,CAEL,KAAA,CAAAhB,CAAAA,CACA,cAAA,CAAAqE,EAAAA,CACA,UAAA,CAAAxB,CAAAA,CACA,SAAA,CAAAF,CAAAA,CACA,QAAAO,CAAAA,CACA,KAAA,CAAAC,CAAAA,CAEA,QAAA,CAAAC,CAAAA,CACA,SAAA,CAAAG,CAAAA,CACA,SAAA,CAAAC,CAAAA,CACA,KAAA,CAAAC,CAAAA,CACA,KAAA,CAAAC,CAAAA,CACA,IAAA,CAAAC,CAAAA,CAEA,UAAA,CAAY,CACV,GAAA,CAAKrB,CAAAA,CACL,KAAA,CAAOO,CAAAA,CACP,QAAA,CAAUe,CAAAA,CACV,OAAA,CAASM,CAAAA,CACT,MAAA,CAAQC,EAAAA,CACR,SAAA,CAAWC,EAAAA,CACX,IAAA,CAAM,MAAA,CACN,SAAA,CAAW,UACX,YAAA,CAAc,KAAA,CACd,WAAA,CAAa,KAAA,CACb,UAAA,CAAY,KAAA,CACZ,cAAA,CAAgBlB,CAAAA,CAAU,MAAA,CAAY,IACxC,CACF,CACF,CCpMO,IAAMoB,EAAAA,CAAcC,gBAAAA,CACzB,SAAqBC,CAAAA,CAAOC,CAAAA,CAAK,CAC/B,GAAM,CACJ,KAAA,CAAAzE,CAAAA,CACA,YAAA,CAAA+B,CAAAA,CACA,QAAA,CAAAC,CAAAA,CACA,MAAA,CAAAC,EACA,OAAA,CAAAC,CAAAA,CACA,SAAA,CAAAwC,CAAAA,CACA,WAAA,CAAAC,CAAAA,CACA,QAAA,CAAAC,CAAAA,CACA,QAAA,CAAAC,CAAAA,CACA,IAAA,CAAAC,CAAAA,CACA,EAAA,CAAAC,CAAAA,CACA,YAAA,CAAcC,CAAAA,CACd,iBAAA,CAAmBC,CAAAA,CACnB,GAAGtE,CACL,CAAA,CAAI6D,CAAAA,CAEE,CACJ,UAAA,CAAAU,CAAAA,CACA,SAAA,CAAA3B,CAAAA,CACA,SAAA,CAAAC,CAAAA,CACA,KAAA,CAAAC,CAAAA,CACA,KAAA,CAAAC,EACA,IAAA,CAAAC,CAAAA,CACA,OAAA,CAAAT,CAAAA,CACA,KAAA,CAAAC,CACF,CAAA,CAAItB,CAAAA,CAAe,CACjB,KAAA,CAAA7B,CAAAA,CACA,YAAA,CAAA+B,CAAAA,CACA,QAAA,CAAAC,CAAAA,CACA,MAAA,CAAAC,CAAAA,CACA,OAAA,CAAAC,CAAAA,CACA,GAAGvB,CACL,CAAC,CAAA,CAGD,OAAAwE,yBAAAA,CAAoBV,CAAAA,CAAK,KAAO,CAC9B,KAAA,CAAAf,CAAAA,CACA,IAAA,CAAAC,CAAAA,CACA,MAAAF,CAAAA,CACA,SAAA,CAAAF,CAAAA,CACA,SAAA,CAAAC,CACF,CAAA,CAAE,CAAA,CAGA4B,cAAAA,CAAC,OAAA,CAAA,CACE,GAAGF,CAAAA,CACJ,EAAA,CAAIH,CAAAA,CACJ,IAAA,CAAMD,CAAAA,CACN,SAAA,CAAWJ,CAAAA,CACX,WAAA,CAAaC,CAAAA,CACb,QAAA,CAAUC,CAAAA,CACV,QAAA,CAAUC,CAAAA,CACV,YAAA,CAAYG,CAAAA,CACZ,iBAAA,CAAiBC,CAAAA,CACjB,YAAA,CAAY/B,CAAAA,CACZ,YAAA,CAAYC,CAAAA,EAAS,MAAA,CACvB,CAEJ,CACF","file":"index.cjs","sourcesContent":["import type { NumberInputOptions } from './types';\r\n\r\n/**\r\n * Get the browser locale\r\n */\r\nexport function getDefaultLocale(): string {\r\n return typeof navigator !== 'undefined' \r\n ? navigator.language || 'en-US'\r\n : 'en-US';\r\n}\r\n\r\n/**\r\n * Parse a formatted number string to a number\r\n */\r\nexport function parseNumber(\r\n value: string,\r\n locale: string | string[] = getDefaultLocale()\r\n): number | null {\r\n if (!value || value.trim() === '') return null;\r\n \r\n // Remove currency symbols and non-numeric characters except decimal separator\r\n const parts = new Intl.NumberFormat(locale).formatToParts(1234.5);\r\n const decimalPart = parts.find(p => p.type === 'decimal');\r\n const decimalSeparator = decimalPart?.value || '.';\r\n \r\n // Normalize the string\r\n let normalized = value.trim();\r\n \r\n // Handle negative numbers\r\n const isNegative = normalized.includes('-') || \r\n (normalized.includes('(') && normalized.includes(')'));\r\n \r\n // Remove all non-digit characters except the decimal separator\r\n const escapedSeparator = decimalSeparator.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\r\n const regex = new RegExp(`[^\\\\d${escapedSeparator}-]`, 'g');\r\n normalized = normalized.replace(regex, '');\r\n \r\n // Replace locale decimal separator with standard dot\r\n if (decimalSeparator !== '.') {\r\n normalized = normalized.replace(decimalSeparator, '.');\r\n }\r\n \r\n // Handle multiple decimal separators (keep first)\r\n const parts_split = normalized.split('.');\r\n if (parts_split.length > 2) {\r\n normalized = parts_split[0] + '.' + parts_split.slice(1).join('');\r\n }\r\n \r\n const num = parseFloat(normalized);\r\n \r\n if (isNaN(num)) return null;\r\n \r\n return isNegative ? -Math.abs(num) : num;\r\n}\r\n\r\n/**\r\n * Format a number according to locale and options\r\n */\r\nexport function formatNumber(\r\n value: number | null,\r\n options: NumberInputOptions = {}\r\n): string {\r\n if (value === null || isNaN(value)) return '';\r\n \r\n const {\r\n format = 'decimal',\r\n locale = getDefaultLocale(),\r\n decimals,\r\n currency = 'USD',\r\n currencyDisplay = 'symbol',\r\n } = options;\r\n \r\n const formatOptions: Intl.NumberFormatOptions = {};\r\n \r\n if (format === 'currency') {\r\n formatOptions.style = 'currency';\r\n formatOptions.currency = currency;\r\n formatOptions.currencyDisplay = currencyDisplay;\r\n } else if (format === 'percent') {\r\n formatOptions.style = 'percent';\r\n } else if (format === 'decimal') {\r\n formatOptions.style = 'decimal';\r\n }\r\n \r\n if (decimals !== undefined) {\r\n formatOptions.minimumFractionDigits = decimals;\r\n formatOptions.maximumFractionDigits = decimals;\r\n }\r\n \r\n return new Intl.NumberFormat(locale, formatOptions).format(value);\r\n}\r\n\r\n/**\r\n * Clamp a number between min and max\r\n */\r\nexport function clamp(value: number, min?: number, max?: number): number {\r\n if (min !== undefined) value = Math.max(value, min);\r\n if (max !== undefined) value = Math.min(value, max);\r\n return value;\r\n}\r\n\r\n/**\r\n * Round a number to specified decimal places\r\n */\r\nexport function roundToDecimals(value: number, decimals: number): number {\r\n const multiplier = Math.pow(10, decimals);\r\n return Math.round(value * multiplier) / multiplier;\r\n}\r\n\r\n/**\r\n * Increment a value by step\r\n */\r\nexport function incrementValue(\r\n value: number | null,\r\n step: number = 1,\r\n min?: number,\r\n max?: number,\r\n decimals?: number\r\n): number {\r\n const current = value ?? min ?? 0;\r\n let next = current + step;\r\n next = clamp(next, min, max);\r\n if (decimals !== undefined) {\r\n next = roundToDecimals(next, decimals);\r\n }\r\n return next;\r\n}\r\n\r\n/**\r\n * Decrement a value by step\r\n */\r\nexport function decrementValue(\r\n value: number | null,\r\n step: number = 1,\r\n min?: number,\r\n max?: number,\r\n decimals?: number\r\n): number {\r\n const current = value ?? max ?? 0;\r\n let next = current - step;\r\n next = clamp(next, min, max);\r\n if (decimals !== undefined) {\r\n next = roundToDecimals(next, decimals);\r\n }\r\n return next;\r\n}\r\n\r\n/**\r\n * Validate a number value\r\n */\r\nexport function validateNumber(\r\n value: number | null,\r\n min?: number,\r\n max?: number,\r\n allowNegative: boolean = true,\r\n allowEmpty: boolean = true\r\n): { isValid: boolean; error: string | null } {\r\n if (value === null) {\r\n return {\r\n isValid: allowEmpty,\r\n error: allowEmpty ? null : 'Value is required',\r\n };\r\n }\r\n \r\n if (isNaN(value)) {\r\n return { isValid: false, error: 'Invalid number' };\r\n }\r\n \r\n if (!allowNegative && value < 0) {\r\n return { isValid: false, error: 'Negative numbers are not allowed' };\r\n }\r\n \r\n if (min !== undefined && value < min) {\r\n return { isValid: false, error: `Minimum value is ${min}` };\r\n }\r\n \r\n if (max !== undefined && value > max) {\r\n return { isValid: false, error: `Maximum value is ${max}` };\r\n }\r\n \r\n return { isValid: true, error: null };\r\n}\r\n","import { useCallback, useMemo, useRef, useState, useEffect } from 'react';\r\nimport type { NumberInputOptions, UseNumberInputReturn } from './types';\r\nimport {\r\n getDefaultLocale,\r\n parseNumber,\r\n formatNumber,\r\n incrementValue,\r\n decrementValue,\r\n validateNumber,\r\n} from './utils';\r\n\r\nexport function useNumberInput(\r\n options: NumberInputOptions & {\r\n value?: number | null;\r\n defaultValue?: number | null;\r\n onChange?: (value: number | null) => void;\r\n onBlur?: () => void;\r\n onFocus?: () => void;\r\n } = {}\r\n): UseNumberInputReturn {\r\n const {\r\n value: controlledValue,\r\n defaultValue = null,\r\n onChange,\r\n onBlur,\r\n onFocus,\r\n locale = getDefaultLocale(),\r\n min,\r\n max,\r\n step = 1,\r\n decimals,\r\n format,\r\n currency,\r\n currencyDisplay,\r\n allowNegative = true,\r\n allowEmpty = true,\r\n formatter,\r\n parser,\r\n } = options;\r\n\r\n const isControlled = controlledValue !== undefined;\r\n const inputRef = useRef<HTMLInputElement>(null);\r\n \r\n // Internal state for uncontrolled mode\r\n const [internalValue, setInternalValue] = useState<number | null>(defaultValue);\r\n const [isFocused, setIsFocused] = useState(false);\r\n const [inputValue, setInputValue] = useState('');\r\n\r\n // Current value (controlled or uncontrolled)\r\n const value = isControlled ? controlledValue : internalValue;\r\n\r\n // Format options for display\r\n const formatOptions = useMemo(\r\n () => ({\r\n format,\r\n locale,\r\n decimals,\r\n currency,\r\n currencyDisplay,\r\n }),\r\n [format, locale, decimals, currency, currencyDisplay]\r\n );\r\n\r\n // Update input value when value changes (only when not focused)\r\n useEffect(() => {\r\n if (!isFocused) {\r\n const formatted = formatter\r\n ? formatter(value)\r\n : formatNumber(value, formatOptions);\r\n setInputValue(formatted);\r\n }\r\n }, [value, isFocused, formatter, formatOptions]);\r\n\r\n // Validation\r\n const { isValid, error } = useMemo(\r\n () => validateNumber(value, min, max, allowNegative, allowEmpty),\r\n [value, min, max, allowNegative, allowEmpty]\r\n );\r\n\r\n // Set value handler\r\n const setValue = useCallback(\r\n (newValue: number | null) => {\r\n if (!isControlled) {\r\n setInternalValue(newValue);\r\n }\r\n onChange?.(newValue);\r\n },\r\n [isControlled, onChange]\r\n );\r\n\r\n // Increment handler\r\n const increment = useCallback(() => {\r\n const newValue = incrementValue(value, step, min, max, decimals);\r\n setValue(newValue);\r\n }, [value, step, min, max, decimals, setValue]);\r\n\r\n // Decrement handler\r\n const decrement = useCallback(() => {\r\n const newValue = decrementValue(value, step, min, max, decimals);\r\n setValue(newValue);\r\n }, [value, step, min, max, decimals, setValue]);\r\n\r\n // Clear handler\r\n const clear = useCallback(() => {\r\n setValue(allowEmpty ? null : min ?? 0);\r\n setInputValue('');\r\n }, [setValue, allowEmpty, min]);\r\n\r\n // Focus handler\r\n const focus = useCallback(() => {\r\n inputRef.current?.focus();\r\n }, []);\r\n\r\n // Blur handler\r\n const blur = useCallback(() => {\r\n inputRef.current?.blur();\r\n }, []);\r\n\r\n // Handle input change\r\n const handleInputChange = useCallback(\r\n (event: React.ChangeEvent<HTMLInputElement>) => {\r\n const rawValue = event.target.value;\r\n setInputValue(rawValue);\r\n\r\n const parsed = parser ? parser(rawValue) : parseNumber(rawValue, locale);\r\n \r\n if (parsed !== null) {\r\n const clamped = Math.min(Math.max(parsed, min ?? -Infinity), max ?? Infinity);\r\n const finalValue = decimals !== undefined \r\n ? Math.round(clamped * Math.pow(10, decimals)) / Math.pow(10, decimals)\r\n : clamped;\r\n setValue(finalValue);\r\n } else if (allowEmpty && rawValue === '') {\r\n setValue(null);\r\n }\r\n },\r\n [locale, parser, min, max, decimals, allowEmpty, setValue]\r\n );\r\n\r\n // Handle focus\r\n const handleFocus = useCallback(\r\n (event: React.FocusEvent<HTMLInputElement>) => {\r\n setIsFocused(true);\r\n // Select all text on focus for easy editing\r\n event.target.select();\r\n onFocus?.();\r\n },\r\n [onFocus]\r\n );\r\n\r\n // Handle blur\r\n const handleBlur = useCallback(() => {\r\n setIsFocused(false);\r\n // Reformat on blur\r\n const formatted = formatter\r\n ? formatter(value)\r\n : formatNumber(value, formatOptions);\r\n setInputValue(formatted);\r\n onBlur?.();\r\n }, [value, formatter, formatOptions, onBlur]);\r\n\r\n // Handle keyboard\r\n const handleKeyDown = useCallback(\r\n (event: React.KeyboardEvent<HTMLInputElement>) => {\r\n switch (event.key) {\r\n case 'ArrowUp':\r\n event.preventDefault();\r\n increment();\r\n break;\r\n case 'ArrowDown':\r\n event.preventDefault();\r\n decrement();\r\n break;\r\n case 'Home':\r\n if (min !== undefined) {\r\n event.preventDefault();\r\n setValue(min);\r\n }\r\n break;\r\n case 'End':\r\n if (max !== undefined) {\r\n event.preventDefault();\r\n setValue(max);\r\n }\r\n break;\r\n }\r\n },\r\n [increment, decrement, min, max, setValue]\r\n );\r\n\r\n // Formatted display value\r\n const formattedValue = useMemo(\r\n () => (formatter ? formatter(value) : formatNumber(value, formatOptions)),\r\n [value, formatter, formatOptions]\r\n );\r\n\r\n return {\r\n // State\r\n value,\r\n formattedValue,\r\n inputValue,\r\n isFocused,\r\n isValid,\r\n error,\r\n // Actions\r\n setValue,\r\n increment,\r\n decrement,\r\n clear,\r\n focus,\r\n blur,\r\n // Input props\r\n inputProps: {\r\n ref: inputRef,\r\n value: inputValue,\r\n onChange: handleInputChange,\r\n onFocus: handleFocus,\r\n onBlur: handleBlur,\r\n onKeyDown: handleKeyDown,\r\n type: 'text',\r\n inputMode: 'decimal',\r\n autoComplete: 'off',\r\n autoCorrect: 'off',\r\n spellCheck: false,\r\n 'aria-invalid': isValid ? undefined : true,\r\n },\r\n };\r\n}\r\n","import { forwardRef, useImperativeHandle } from 'react';\r\nimport { useNumberInput } from './useNumberInput';\r\nimport type { NumberInputProps } from './types';\r\n\r\n\r\nexport interface NumberInputRef {\r\n /** Focus the input */\r\n focus: () => void;\r\n /** Blur the input */\r\n blur: () => void;\r\n /** Clear the input */\r\n clear: () => void;\r\n /** Increment the value */\r\n increment: () => void;\r\n /** Decrement the value */\r\n decrement: () => void;\r\n}\r\n\r\n/**\r\n * NumberInput component - headless number input with formatting\r\n * \r\n * @example\r\n * ```tsx\r\n * <NumberInput\r\n * value={value}\r\n * onChange={setValue}\r\n * format=\"currency\"\r\n * currency=\"USD\"\r\n * />\r\n * ```\r\n */\r\nexport const NumberInput = forwardRef<NumberInputRef, NumberInputProps>(\r\n function NumberInput(props, ref) {\r\n const {\r\n value,\r\n defaultValue,\r\n onChange,\r\n onBlur,\r\n onFocus,\r\n className,\r\n placeholder,\r\n disabled,\r\n readOnly,\r\n name,\r\n id,\r\n 'aria-label': ariaLabel,\r\n 'aria-labelledby': ariaLabelledBy,\r\n ...options\r\n } = props;\r\n\r\n const {\r\n inputProps,\r\n increment,\r\n decrement,\r\n clear,\r\n focus,\r\n blur,\r\n isValid,\r\n error,\r\n } = useNumberInput({\r\n value,\r\n defaultValue,\r\n onChange,\r\n onBlur,\r\n onFocus,\r\n ...options,\r\n });\r\n\r\n // Expose imperative methods\r\n useImperativeHandle(ref, () => ({\r\n focus,\r\n blur,\r\n clear,\r\n increment,\r\n decrement,\r\n }));\r\n\r\n return (\r\n <input\r\n {...inputProps}\r\n id={id}\r\n name={name}\r\n className={className}\r\n placeholder={placeholder}\r\n disabled={disabled}\r\n readOnly={readOnly}\r\n aria-label={ariaLabel}\r\n aria-labelledby={ariaLabelledBy}\r\n data-valid={isValid}\r\n data-error={error ?? undefined}\r\n />\r\n );\r\n }\r\n);\r\n\r\nexport default NumberInput;\r\n"]}
@@ -0,0 +1,85 @@
1
+ import * as react from 'react';
2
+ import { InputHTMLAttributes, Ref } from 'react';
3
+
4
+ type NumberFormat = 'decimal' | 'currency' | 'percent' | 'unit';
5
+ interface NumberInputOptions {
6
+ format?: NumberFormat;
7
+ locale?: string | string[];
8
+ min?: number;
9
+ max?: number;
10
+ step?: number;
11
+ decimals?: number;
12
+ currency?: string;
13
+ currencyDisplay?: 'symbol' | 'narrowSymbol' | 'code' | 'name';
14
+ allowNegative?: boolean;
15
+ allowEmpty?: boolean;
16
+ formatter?: (value: number | null) => string;
17
+ parser?: (value: string) => number | null;
18
+ }
19
+ interface NumberInputState {
20
+ value: number | null;
21
+ formattedValue: string;
22
+ inputValue: string;
23
+ isFocused: boolean;
24
+ isValid: boolean;
25
+ error: string | null;
26
+ }
27
+ interface NumberInputActions {
28
+ setValue: (value: number | null) => void;
29
+ increment: () => void;
30
+ decrement: () => void;
31
+ clear: () => void;
32
+ focus: () => void;
33
+ blur: () => void;
34
+ }
35
+ interface UseNumberInputReturn extends NumberInputState, NumberInputActions {
36
+ inputProps: InputHTMLAttributes<HTMLInputElement> & {
37
+ ref: Ref<HTMLInputElement>;
38
+ };
39
+ }
40
+ interface NumberInputProps extends NumberInputOptions {
41
+ value?: number | null;
42
+ defaultValue?: number | null;
43
+ onChange?: (value: number | null) => void;
44
+ onBlur?: () => void;
45
+ onFocus?: () => void;
46
+ className?: string;
47
+ placeholder?: string;
48
+ disabled?: boolean;
49
+ readOnly?: boolean;
50
+ name?: string;
51
+ id?: string;
52
+ 'aria-label'?: string;
53
+ 'aria-labelledby'?: string;
54
+ }
55
+
56
+ interface NumberInputRef {
57
+ focus: () => void;
58
+ blur: () => void;
59
+ clear: () => void;
60
+ increment: () => void;
61
+ decrement: () => void;
62
+ }
63
+ declare const NumberInput: react.ForwardRefExoticComponent<NumberInputProps & react.RefAttributes<NumberInputRef>>;
64
+
65
+ declare function useNumberInput(options?: NumberInputOptions & {
66
+ value?: number | null;
67
+ defaultValue?: number | null;
68
+ onChange?: (value: number | null) => void;
69
+ onBlur?: () => void;
70
+ onFocus?: () => void;
71
+ }): UseNumberInputReturn;
72
+
73
+ declare function getDefaultLocale(): string;
74
+ declare function parseNumber(value: string, locale?: string | string[]): number | null;
75
+ declare function formatNumber(value: number | null, options?: NumberInputOptions): string;
76
+ declare function clamp(value: number, min?: number, max?: number): number;
77
+ declare function roundToDecimals(value: number, decimals: number): number;
78
+ declare function incrementValue(value: number | null, step?: number, min?: number, max?: number, decimals?: number): number;
79
+ declare function decrementValue(value: number | null, step?: number, min?: number, max?: number, decimals?: number): number;
80
+ declare function validateNumber(value: number | null, min?: number, max?: number, allowNegative?: boolean, allowEmpty?: boolean): {
81
+ isValid: boolean;
82
+ error: string | null;
83
+ };
84
+
85
+ export { type NumberFormat, NumberInput, type NumberInputActions, type NumberInputOptions, type NumberInputProps, type NumberInputRef, type NumberInputState, type UseNumberInputReturn, clamp, decrementValue, formatNumber, getDefaultLocale, incrementValue, parseNumber, roundToDecimals, useNumberInput, validateNumber };
@@ -0,0 +1,85 @@
1
+ import * as react from 'react';
2
+ import { InputHTMLAttributes, Ref } from 'react';
3
+
4
+ type NumberFormat = 'decimal' | 'currency' | 'percent' | 'unit';
5
+ interface NumberInputOptions {
6
+ format?: NumberFormat;
7
+ locale?: string | string[];
8
+ min?: number;
9
+ max?: number;
10
+ step?: number;
11
+ decimals?: number;
12
+ currency?: string;
13
+ currencyDisplay?: 'symbol' | 'narrowSymbol' | 'code' | 'name';
14
+ allowNegative?: boolean;
15
+ allowEmpty?: boolean;
16
+ formatter?: (value: number | null) => string;
17
+ parser?: (value: string) => number | null;
18
+ }
19
+ interface NumberInputState {
20
+ value: number | null;
21
+ formattedValue: string;
22
+ inputValue: string;
23
+ isFocused: boolean;
24
+ isValid: boolean;
25
+ error: string | null;
26
+ }
27
+ interface NumberInputActions {
28
+ setValue: (value: number | null) => void;
29
+ increment: () => void;
30
+ decrement: () => void;
31
+ clear: () => void;
32
+ focus: () => void;
33
+ blur: () => void;
34
+ }
35
+ interface UseNumberInputReturn extends NumberInputState, NumberInputActions {
36
+ inputProps: InputHTMLAttributes<HTMLInputElement> & {
37
+ ref: Ref<HTMLInputElement>;
38
+ };
39
+ }
40
+ interface NumberInputProps extends NumberInputOptions {
41
+ value?: number | null;
42
+ defaultValue?: number | null;
43
+ onChange?: (value: number | null) => void;
44
+ onBlur?: () => void;
45
+ onFocus?: () => void;
46
+ className?: string;
47
+ placeholder?: string;
48
+ disabled?: boolean;
49
+ readOnly?: boolean;
50
+ name?: string;
51
+ id?: string;
52
+ 'aria-label'?: string;
53
+ 'aria-labelledby'?: string;
54
+ }
55
+
56
+ interface NumberInputRef {
57
+ focus: () => void;
58
+ blur: () => void;
59
+ clear: () => void;
60
+ increment: () => void;
61
+ decrement: () => void;
62
+ }
63
+ declare const NumberInput: react.ForwardRefExoticComponent<NumberInputProps & react.RefAttributes<NumberInputRef>>;
64
+
65
+ declare function useNumberInput(options?: NumberInputOptions & {
66
+ value?: number | null;
67
+ defaultValue?: number | null;
68
+ onChange?: (value: number | null) => void;
69
+ onBlur?: () => void;
70
+ onFocus?: () => void;
71
+ }): UseNumberInputReturn;
72
+
73
+ declare function getDefaultLocale(): string;
74
+ declare function parseNumber(value: string, locale?: string | string[]): number | null;
75
+ declare function formatNumber(value: number | null, options?: NumberInputOptions): string;
76
+ declare function clamp(value: number, min?: number, max?: number): number;
77
+ declare function roundToDecimals(value: number, decimals: number): number;
78
+ declare function incrementValue(value: number | null, step?: number, min?: number, max?: number, decimals?: number): number;
79
+ declare function decrementValue(value: number | null, step?: number, min?: number, max?: number, decimals?: number): number;
80
+ declare function validateNumber(value: number | null, min?: number, max?: number, allowNegative?: boolean, allowEmpty?: boolean): {
81
+ isValid: boolean;
82
+ error: string | null;
83
+ };
84
+
85
+ export { type NumberFormat, NumberInput, type NumberInputActions, type NumberInputOptions, type NumberInputProps, type NumberInputRef, type NumberInputState, type UseNumberInputReturn, clamp, decrementValue, formatNumber, getDefaultLocale, incrementValue, parseNumber, roundToDecimals, useNumberInput, validateNumber };
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ import {forwardRef,useImperativeHandle,useRef,useState,useMemo,useEffect,useCallback}from'react';import {jsx}from'react/jsx-runtime';function v(){return typeof navigator<"u"&&navigator.language||"en-US"}function T(e,u=v()){if(!e||e.trim()==="")return null;let o=new Intl.NumberFormat(u).formatToParts(1234.5).find(y=>y.type==="decimal")?.value||".",l=e.trim(),t=l.includes("-")||l.includes("(")&&l.includes(")"),n=o.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),m=new RegExp(`[^\\d${n}-]`,"g");l=l.replace(m,""),o!=="."&&(l=l.replace(o,"."));let f=l.split(".");f.length>2&&(l=f[0]+"."+f.slice(1).join(""));let i=parseFloat(l);return isNaN(i)?null:t?-Math.abs(i):i}function D(e,u={}){if(e===null||isNaN(e))return "";let{format:r="decimal",locale:c=v(),decimals:o,currency:l="USD",currencyDisplay:t="symbol"}=u,n={};return r==="currency"?(n.style="currency",n.currency=l,n.currencyDisplay=t):r==="percent"?n.style="percent":r==="decimal"&&(n.style="decimal"),o!==void 0&&(n.minimumFractionDigits=o,n.maximumFractionDigits=o),new Intl.NumberFormat(c,n).format(e)}function U(e,u,r){return u!==void 0&&(e=Math.max(e,u)),r!==void 0&&(e=Math.min(e,r)),e}function B(e,u){let r=Math.pow(10,u);return Math.round(e*r)/r}function H(e,u=1,r,c,o){let t=(e??r??0)+u;return t=U(t,r,c),o!==void 0&&(t=B(t,o)),t}function $(e,u=1,r,c,o){let t=(e??c??0)-u;return t=U(t,r,c),o!==void 0&&(t=B(t,o)),t}function A(e,u,r,c=true,o=true){return e===null?{isValid:o,error:o?null:"Value is required"}:isNaN(e)?{isValid:false,error:"Invalid number"}:!c&&e<0?{isValid:false,error:"Negative numbers are not allowed"}:u!==void 0&&e<u?{isValid:false,error:`Minimum value is ${u}`}:r!==void 0&&e>r?{isValid:false,error:`Maximum value is ${r}`}:{isValid:true,error:null}}function z(e={}){let{value:u,defaultValue:r=null,onChange:c,onBlur:o,onFocus:l,locale:t=v(),min:n,max:m,step:f=1,decimals:i,format:y,currency:M,currencyDisplay:w,allowNegative:F=true,allowEmpty:N=true,formatter:d,parser:g}=e,V=u!==void 0,h=useRef(null),[L,P]=useState(r),[x,R]=useState(false),[C,E]=useState(""),s=V?u:L,I=useMemo(()=>({format:y,locale:t,decimals:i,currency:M,currencyDisplay:w}),[y,t,i,M,w]);useEffect(()=>{if(!x){let a=d?d(s):D(s,I);E(a);}},[s,x,d,I]);let{isValid:_,error:J}=useMemo(()=>A(s,n,m,F,N),[s,n,m,F,N]),p=useCallback(a=>{V||P(a),c?.(a);},[V,c]),k=useCallback(()=>{let a=H(s,f,n,m,i);p(a);},[s,f,n,m,i,p]),S=useCallback(()=>{let a=$(s,f,n,m,i);p(a);},[s,f,n,m,i,p]),Q=useCallback(()=>{p(N?null:n??0),E("");},[p,N,n]),W=useCallback(()=>{h.current?.focus();},[]),X=useCallback(()=>{h.current?.blur();},[]),Y=useCallback(a=>{let O=a.target.value;E(O);let j=g?g(O):T(O,t);if(j!==null){let G=Math.min(Math.max(j,n??-1/0),m??1/0),te=i!==void 0?Math.round(G*Math.pow(10,i))/Math.pow(10,i):G;p(te);}else N&&O===""&&p(null);},[t,g,n,m,i,N,p]),Z=useCallback(a=>{R(true),a.target.select(),l?.();},[l]),ee=useCallback(()=>{R(false);let a=d?d(s):D(s,I);E(a),o?.();},[s,d,I,o]),ne=useCallback(a=>{switch(a.key){case "ArrowUp":a.preventDefault(),k();break;case "ArrowDown":a.preventDefault(),S();break;case "Home":n!==void 0&&(a.preventDefault(),p(n));break;case "End":m!==void 0&&(a.preventDefault(),p(m));break}},[k,S,n,m,p]),re=useMemo(()=>d?d(s):D(s,I),[s,d,I]);return {value:s,formattedValue:re,inputValue:C,isFocused:x,isValid:_,error:J,setValue:p,increment:k,decrement:S,clear:Q,focus:W,blur:X,inputProps:{ref:h,value:C,onChange:Y,onFocus:Z,onBlur:ee,onKeyDown:ne,type:"text",inputMode:"decimal",autoComplete:"off",autoCorrect:"off",spellCheck:false,"aria-invalid":_?void 0:true}}}var ie=forwardRef(function(u,r){let{value:c,defaultValue:o,onChange:l,onBlur:t,onFocus:n,className:m,placeholder:f,disabled:i,readOnly:y,name:M,id:w,"aria-label":F,"aria-labelledby":N,...d}=u,{inputProps:g,increment:V,decrement:h,clear:L,focus:P,blur:x,isValid:R,error:C}=z({value:c,defaultValue:o,onChange:l,onBlur:t,onFocus:n,...d});return useImperativeHandle(r,()=>({focus:P,blur:x,clear:L,increment:V,decrement:h})),jsx("input",{...g,id:w,name:M,className:m,placeholder:f,disabled:i,readOnly:y,"aria-label":F,"aria-labelledby":N,"data-valid":R,"data-error":C??void 0})});export{ie as NumberInput,U as clamp,$ as decrementValue,D as formatNumber,v as getDefaultLocale,H as incrementValue,T as parseNumber,B as roundToDecimals,z as useNumberInput,A as validateNumber};//# sourceMappingURL=index.js.map
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils.ts","../src/useNumberInput.ts","../src/NumberInput.tsx"],"names":["getDefaultLocale","parseNumber","value","locale","decimalSeparator","p","normalized","isNegative","escapedSeparator","regex","parts_split","num","formatNumber","options","format","decimals","currency","currencyDisplay","formatOptions","clamp","min","max","roundToDecimals","multiplier","incrementValue","step","next","decrementValue","validateNumber","allowNegative","allowEmpty","useNumberInput","controlledValue","defaultValue","onChange","onBlur","onFocus","formatter","parser","isControlled","inputRef","useRef","internalValue","setInternalValue","useState","isFocused","setIsFocused","inputValue","setInputValue","useMemo","useEffect","formatted","isValid","error","setValue","useCallback","newValue","increment","decrement","clear","focus","blur","handleInputChange","event","rawValue","parsed","clamped","finalValue","handleFocus","handleBlur","handleKeyDown","formattedValue","NumberInput","forwardRef","props","ref","className","placeholder","disabled","readOnly","name","id","ariaLabel","ariaLabelledBy","inputProps","useImperativeHandle","jsx"],"mappings":"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","file":"index.js","sourcesContent":["import type { NumberInputOptions } from './types';\r\n\r\n/**\r\n * Get the browser locale\r\n */\r\nexport function getDefaultLocale(): string {\r\n return typeof navigator !== 'undefined' \r\n ? navigator.language || 'en-US'\r\n : 'en-US';\r\n}\r\n\r\n/**\r\n * Parse a formatted number string to a number\r\n */\r\nexport function parseNumber(\r\n value: string,\r\n locale: string | string[] = getDefaultLocale()\r\n): number | null {\r\n if (!value || value.trim() === '') return null;\r\n \r\n // Remove currency symbols and non-numeric characters except decimal separator\r\n const parts = new Intl.NumberFormat(locale).formatToParts(1234.5);\r\n const decimalPart = parts.find(p => p.type === 'decimal');\r\n const decimalSeparator = decimalPart?.value || '.';\r\n \r\n // Normalize the string\r\n let normalized = value.trim();\r\n \r\n // Handle negative numbers\r\n const isNegative = normalized.includes('-') || \r\n (normalized.includes('(') && normalized.includes(')'));\r\n \r\n // Remove all non-digit characters except the decimal separator\r\n const escapedSeparator = decimalSeparator.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\r\n const regex = new RegExp(`[^\\\\d${escapedSeparator}-]`, 'g');\r\n normalized = normalized.replace(regex, '');\r\n \r\n // Replace locale decimal separator with standard dot\r\n if (decimalSeparator !== '.') {\r\n normalized = normalized.replace(decimalSeparator, '.');\r\n }\r\n \r\n // Handle multiple decimal separators (keep first)\r\n const parts_split = normalized.split('.');\r\n if (parts_split.length > 2) {\r\n normalized = parts_split[0] + '.' + parts_split.slice(1).join('');\r\n }\r\n \r\n const num = parseFloat(normalized);\r\n \r\n if (isNaN(num)) return null;\r\n \r\n return isNegative ? -Math.abs(num) : num;\r\n}\r\n\r\n/**\r\n * Format a number according to locale and options\r\n */\r\nexport function formatNumber(\r\n value: number | null,\r\n options: NumberInputOptions = {}\r\n): string {\r\n if (value === null || isNaN(value)) return '';\r\n \r\n const {\r\n format = 'decimal',\r\n locale = getDefaultLocale(),\r\n decimals,\r\n currency = 'USD',\r\n currencyDisplay = 'symbol',\r\n } = options;\r\n \r\n const formatOptions: Intl.NumberFormatOptions = {};\r\n \r\n if (format === 'currency') {\r\n formatOptions.style = 'currency';\r\n formatOptions.currency = currency;\r\n formatOptions.currencyDisplay = currencyDisplay;\r\n } else if (format === 'percent') {\r\n formatOptions.style = 'percent';\r\n } else if (format === 'decimal') {\r\n formatOptions.style = 'decimal';\r\n }\r\n \r\n if (decimals !== undefined) {\r\n formatOptions.minimumFractionDigits = decimals;\r\n formatOptions.maximumFractionDigits = decimals;\r\n }\r\n \r\n return new Intl.NumberFormat(locale, formatOptions).format(value);\r\n}\r\n\r\n/**\r\n * Clamp a number between min and max\r\n */\r\nexport function clamp(value: number, min?: number, max?: number): number {\r\n if (min !== undefined) value = Math.max(value, min);\r\n if (max !== undefined) value = Math.min(value, max);\r\n return value;\r\n}\r\n\r\n/**\r\n * Round a number to specified decimal places\r\n */\r\nexport function roundToDecimals(value: number, decimals: number): number {\r\n const multiplier = Math.pow(10, decimals);\r\n return Math.round(value * multiplier) / multiplier;\r\n}\r\n\r\n/**\r\n * Increment a value by step\r\n */\r\nexport function incrementValue(\r\n value: number | null,\r\n step: number = 1,\r\n min?: number,\r\n max?: number,\r\n decimals?: number\r\n): number {\r\n const current = value ?? min ?? 0;\r\n let next = current + step;\r\n next = clamp(next, min, max);\r\n if (decimals !== undefined) {\r\n next = roundToDecimals(next, decimals);\r\n }\r\n return next;\r\n}\r\n\r\n/**\r\n * Decrement a value by step\r\n */\r\nexport function decrementValue(\r\n value: number | null,\r\n step: number = 1,\r\n min?: number,\r\n max?: number,\r\n decimals?: number\r\n): number {\r\n const current = value ?? max ?? 0;\r\n let next = current - step;\r\n next = clamp(next, min, max);\r\n if (decimals !== undefined) {\r\n next = roundToDecimals(next, decimals);\r\n }\r\n return next;\r\n}\r\n\r\n/**\r\n * Validate a number value\r\n */\r\nexport function validateNumber(\r\n value: number | null,\r\n min?: number,\r\n max?: number,\r\n allowNegative: boolean = true,\r\n allowEmpty: boolean = true\r\n): { isValid: boolean; error: string | null } {\r\n if (value === null) {\r\n return {\r\n isValid: allowEmpty,\r\n error: allowEmpty ? null : 'Value is required',\r\n };\r\n }\r\n \r\n if (isNaN(value)) {\r\n return { isValid: false, error: 'Invalid number' };\r\n }\r\n \r\n if (!allowNegative && value < 0) {\r\n return { isValid: false, error: 'Negative numbers are not allowed' };\r\n }\r\n \r\n if (min !== undefined && value < min) {\r\n return { isValid: false, error: `Minimum value is ${min}` };\r\n }\r\n \r\n if (max !== undefined && value > max) {\r\n return { isValid: false, error: `Maximum value is ${max}` };\r\n }\r\n \r\n return { isValid: true, error: null };\r\n}\r\n","import { useCallback, useMemo, useRef, useState, useEffect } from 'react';\r\nimport type { NumberInputOptions, UseNumberInputReturn } from './types';\r\nimport {\r\n getDefaultLocale,\r\n parseNumber,\r\n formatNumber,\r\n incrementValue,\r\n decrementValue,\r\n validateNumber,\r\n} from './utils';\r\n\r\nexport function useNumberInput(\r\n options: NumberInputOptions & {\r\n value?: number | null;\r\n defaultValue?: number | null;\r\n onChange?: (value: number | null) => void;\r\n onBlur?: () => void;\r\n onFocus?: () => void;\r\n } = {}\r\n): UseNumberInputReturn {\r\n const {\r\n value: controlledValue,\r\n defaultValue = null,\r\n onChange,\r\n onBlur,\r\n onFocus,\r\n locale = getDefaultLocale(),\r\n min,\r\n max,\r\n step = 1,\r\n decimals,\r\n format,\r\n currency,\r\n currencyDisplay,\r\n allowNegative = true,\r\n allowEmpty = true,\r\n formatter,\r\n parser,\r\n } = options;\r\n\r\n const isControlled = controlledValue !== undefined;\r\n const inputRef = useRef<HTMLInputElement>(null);\r\n \r\n // Internal state for uncontrolled mode\r\n const [internalValue, setInternalValue] = useState<number | null>(defaultValue);\r\n const [isFocused, setIsFocused] = useState(false);\r\n const [inputValue, setInputValue] = useState('');\r\n\r\n // Current value (controlled or uncontrolled)\r\n const value = isControlled ? controlledValue : internalValue;\r\n\r\n // Format options for display\r\n const formatOptions = useMemo(\r\n () => ({\r\n format,\r\n locale,\r\n decimals,\r\n currency,\r\n currencyDisplay,\r\n }),\r\n [format, locale, decimals, currency, currencyDisplay]\r\n );\r\n\r\n // Update input value when value changes (only when not focused)\r\n useEffect(() => {\r\n if (!isFocused) {\r\n const formatted = formatter\r\n ? formatter(value)\r\n : formatNumber(value, formatOptions);\r\n setInputValue(formatted);\r\n }\r\n }, [value, isFocused, formatter, formatOptions]);\r\n\r\n // Validation\r\n const { isValid, error } = useMemo(\r\n () => validateNumber(value, min, max, allowNegative, allowEmpty),\r\n [value, min, max, allowNegative, allowEmpty]\r\n );\r\n\r\n // Set value handler\r\n const setValue = useCallback(\r\n (newValue: number | null) => {\r\n if (!isControlled) {\r\n setInternalValue(newValue);\r\n }\r\n onChange?.(newValue);\r\n },\r\n [isControlled, onChange]\r\n );\r\n\r\n // Increment handler\r\n const increment = useCallback(() => {\r\n const newValue = incrementValue(value, step, min, max, decimals);\r\n setValue(newValue);\r\n }, [value, step, min, max, decimals, setValue]);\r\n\r\n // Decrement handler\r\n const decrement = useCallback(() => {\r\n const newValue = decrementValue(value, step, min, max, decimals);\r\n setValue(newValue);\r\n }, [value, step, min, max, decimals, setValue]);\r\n\r\n // Clear handler\r\n const clear = useCallback(() => {\r\n setValue(allowEmpty ? null : min ?? 0);\r\n setInputValue('');\r\n }, [setValue, allowEmpty, min]);\r\n\r\n // Focus handler\r\n const focus = useCallback(() => {\r\n inputRef.current?.focus();\r\n }, []);\r\n\r\n // Blur handler\r\n const blur = useCallback(() => {\r\n inputRef.current?.blur();\r\n }, []);\r\n\r\n // Handle input change\r\n const handleInputChange = useCallback(\r\n (event: React.ChangeEvent<HTMLInputElement>) => {\r\n const rawValue = event.target.value;\r\n setInputValue(rawValue);\r\n\r\n const parsed = parser ? parser(rawValue) : parseNumber(rawValue, locale);\r\n \r\n if (parsed !== null) {\r\n const clamped = Math.min(Math.max(parsed, min ?? -Infinity), max ?? Infinity);\r\n const finalValue = decimals !== undefined \r\n ? Math.round(clamped * Math.pow(10, decimals)) / Math.pow(10, decimals)\r\n : clamped;\r\n setValue(finalValue);\r\n } else if (allowEmpty && rawValue === '') {\r\n setValue(null);\r\n }\r\n },\r\n [locale, parser, min, max, decimals, allowEmpty, setValue]\r\n );\r\n\r\n // Handle focus\r\n const handleFocus = useCallback(\r\n (event: React.FocusEvent<HTMLInputElement>) => {\r\n setIsFocused(true);\r\n // Select all text on focus for easy editing\r\n event.target.select();\r\n onFocus?.();\r\n },\r\n [onFocus]\r\n );\r\n\r\n // Handle blur\r\n const handleBlur = useCallback(() => {\r\n setIsFocused(false);\r\n // Reformat on blur\r\n const formatted = formatter\r\n ? formatter(value)\r\n : formatNumber(value, formatOptions);\r\n setInputValue(formatted);\r\n onBlur?.();\r\n }, [value, formatter, formatOptions, onBlur]);\r\n\r\n // Handle keyboard\r\n const handleKeyDown = useCallback(\r\n (event: React.KeyboardEvent<HTMLInputElement>) => {\r\n switch (event.key) {\r\n case 'ArrowUp':\r\n event.preventDefault();\r\n increment();\r\n break;\r\n case 'ArrowDown':\r\n event.preventDefault();\r\n decrement();\r\n break;\r\n case 'Home':\r\n if (min !== undefined) {\r\n event.preventDefault();\r\n setValue(min);\r\n }\r\n break;\r\n case 'End':\r\n if (max !== undefined) {\r\n event.preventDefault();\r\n setValue(max);\r\n }\r\n break;\r\n }\r\n },\r\n [increment, decrement, min, max, setValue]\r\n );\r\n\r\n // Formatted display value\r\n const formattedValue = useMemo(\r\n () => (formatter ? formatter(value) : formatNumber(value, formatOptions)),\r\n [value, formatter, formatOptions]\r\n );\r\n\r\n return {\r\n // State\r\n value,\r\n formattedValue,\r\n inputValue,\r\n isFocused,\r\n isValid,\r\n error,\r\n // Actions\r\n setValue,\r\n increment,\r\n decrement,\r\n clear,\r\n focus,\r\n blur,\r\n // Input props\r\n inputProps: {\r\n ref: inputRef,\r\n value: inputValue,\r\n onChange: handleInputChange,\r\n onFocus: handleFocus,\r\n onBlur: handleBlur,\r\n onKeyDown: handleKeyDown,\r\n type: 'text',\r\n inputMode: 'decimal',\r\n autoComplete: 'off',\r\n autoCorrect: 'off',\r\n spellCheck: false,\r\n 'aria-invalid': isValid ? undefined : true,\r\n },\r\n };\r\n}\r\n","import { forwardRef, useImperativeHandle } from 'react';\r\nimport { useNumberInput } from './useNumberInput';\r\nimport type { NumberInputProps } from './types';\r\n\r\n\r\nexport interface NumberInputRef {\r\n /** Focus the input */\r\n focus: () => void;\r\n /** Blur the input */\r\n blur: () => void;\r\n /** Clear the input */\r\n clear: () => void;\r\n /** Increment the value */\r\n increment: () => void;\r\n /** Decrement the value */\r\n decrement: () => void;\r\n}\r\n\r\n/**\r\n * NumberInput component - headless number input with formatting\r\n * \r\n * @example\r\n * ```tsx\r\n * <NumberInput\r\n * value={value}\r\n * onChange={setValue}\r\n * format=\"currency\"\r\n * currency=\"USD\"\r\n * />\r\n * ```\r\n */\r\nexport const NumberInput = forwardRef<NumberInputRef, NumberInputProps>(\r\n function NumberInput(props, ref) {\r\n const {\r\n value,\r\n defaultValue,\r\n onChange,\r\n onBlur,\r\n onFocus,\r\n className,\r\n placeholder,\r\n disabled,\r\n readOnly,\r\n name,\r\n id,\r\n 'aria-label': ariaLabel,\r\n 'aria-labelledby': ariaLabelledBy,\r\n ...options\r\n } = props;\r\n\r\n const {\r\n inputProps,\r\n increment,\r\n decrement,\r\n clear,\r\n focus,\r\n blur,\r\n isValid,\r\n error,\r\n } = useNumberInput({\r\n value,\r\n defaultValue,\r\n onChange,\r\n onBlur,\r\n onFocus,\r\n ...options,\r\n });\r\n\r\n // Expose imperative methods\r\n useImperativeHandle(ref, () => ({\r\n focus,\r\n blur,\r\n clear,\r\n increment,\r\n decrement,\r\n }));\r\n\r\n return (\r\n <input\r\n {...inputProps}\r\n id={id}\r\n name={name}\r\n className={className}\r\n placeholder={placeholder}\r\n disabled={disabled}\r\n readOnly={readOnly}\r\n aria-label={ariaLabel}\r\n aria-labelledby={ariaLabelledBy}\r\n data-valid={isValid}\r\n data-error={error ?? undefined}\r\n />\r\n );\r\n }\r\n);\r\n\r\nexport default NumberInput;\r\n"]}
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@input-kit/number",
3
+ "version": "0.1.0",
4
+ "description": "Headless number input component with locale support, currency modes, and TypeScript",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "sideEffects": false,
22
+ "scripts": {
23
+ "build": "tsup",
24
+ "dev": "tsup --watch",
25
+ "test": "vitest run",
26
+ "test:watch": "vitest",
27
+ "typecheck": "tsc --noEmit",
28
+ "lint": "eslint src --ext .ts,.tsx",
29
+ "prepublishOnly": "npm run build"
30
+ },
31
+ "keywords": [
32
+ "react",
33
+ "number",
34
+ "input",
35
+ "currency",
36
+ "locale",
37
+ "typescript",
38
+ "headless",
39
+ "unstyled",
40
+ "accessible"
41
+ ],
42
+ "author": "Harshit",
43
+ "license": "MIT",
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "git+https://github.com/yourusername/input-kit.git",
47
+ "directory": "packages/number"
48
+ },
49
+ "bugs": {
50
+ "url": "https://github.com/yourusername/input-kit/issues"
51
+ },
52
+ "homepage": "https://github.com/yourusername/input-kit/tree/main/packages/number#readme",
53
+ "engines": {
54
+ "node": ">=18.0.0"
55
+ },
56
+ "peerDependencies": {
57
+ "react": "^18.0.0 || ^19.0.0",
58
+ "react-dom": "^18.0.0 || ^19.0.0"
59
+ },
60
+ "devDependencies": {
61
+ "@testing-library/react": "^16.3.2",
62
+ "@types/react": "^18.3.18",
63
+ "@types/react-dom": "^18.3.5",
64
+ "@vitejs/plugin-react": "^4.3.4",
65
+ "jsdom": "^26.0.0",
66
+ "react": "^18.3.1",
67
+ "react-dom": "^18.3.1",
68
+ "tsup": "^8.3.5",
69
+ "typescript": "^5.7.3",
70
+ "vitest": "^3.0.4"
71
+ }
72
+ }