@startupjs-ui/number-input 0.1.3

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/CHANGELOG.md ADDED
@@ -0,0 +1,20 @@
1
+ # Change Log
2
+
3
+ All notable changes to this project will be documented in this file.
4
+ See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
+
6
+ ## [0.1.3](https://github.com/startupjs/startupjs-ui/compare/v0.1.2...v0.1.3) (2025-12-29)
7
+
8
+ **Note:** Version bump only for package @startupjs-ui/number-input
9
+
10
+
11
+
12
+
13
+
14
+ ## [0.1.2](https://github.com/startupjs/startupjs-ui/compare/v0.1.1...v0.1.2) (2025-12-29)
15
+
16
+
17
+ ### Features
18
+
19
+ * add mdx and docs packages. Refactor docs to get rid of any @startupjs/ui usage and use startupjs-ui instead ([703c926](https://github.com/startupjs/startupjs-ui/commit/703c92636efb0421ffd11783f692fc892b74018f))
20
+ * **number-input:** refactor NumberInput component ([61f01c3](https://github.com/startupjs/startupjs-ui/commit/61f01c370cb12ab7c9aa110e829c2e8a08ff3f2b))
package/README.mdx ADDED
@@ -0,0 +1,207 @@
1
+ import { useState } from 'react'
2
+ import NumberInput, { _PropsJsonSchema as NumberInputPropsJsonSchema } from './index'
3
+ import Div from '@startupjs-ui/div'
4
+ import Br from '@startupjs-ui/br'
5
+ import { Sandbox } from '@startupjs-ui/docs'
6
+
7
+ # NumberInput
8
+
9
+ NumberInput allows user to enter or edit number.
10
+
11
+ ```jsx
12
+ import { NumberInput } from 'startupjs-ui'
13
+ ```
14
+
15
+ ## Simple example
16
+
17
+ ```jsx example
18
+ const [value, setValue] = useState()
19
+ return (
20
+ <NumberInput
21
+ value={value}
22
+ onChangeNumber={setValue}
23
+ />
24
+ )
25
+ ```
26
+
27
+ ## Disabled
28
+
29
+ ```jsx example
30
+ return (
31
+ <NumberInput
32
+ disabled
33
+ value={10}
34
+ />
35
+ )
36
+ ```
37
+
38
+ ## Readonly
39
+
40
+ ```jsx example
41
+ return (
42
+ <NumberInput
43
+ readonly
44
+ value={10}
45
+ />
46
+ )
47
+ ```
48
+
49
+ ## Step
50
+
51
+ The step specifies how many decimal places can be entered, set by the numbers 1, 0.1, 0.01, etc. in the `step` property (1 by default).
52
+
53
+ ```jsx example
54
+ const [value, setValue] = useState()
55
+ return (
56
+ <Div>
57
+ <NumberInput
58
+ value={value}
59
+ step={0.001}
60
+ onChangeNumber={setValue}
61
+ />
62
+ </Div>
63
+ )
64
+ ```
65
+
66
+ ## Minimum and maximum value
67
+
68
+ The minimum and maximum values are set by the `min` and `max` properties
69
+
70
+ ```jsx example
71
+ const [value, setValue] = useState()
72
+ return (
73
+ <Div>
74
+ <NumberInput
75
+ value={value}
76
+ min={-30}
77
+ max={20}
78
+ onChangeNumber={setValue}
79
+ />
80
+ </Div>
81
+ )
82
+ ```
83
+
84
+ ## Buttons mode
85
+
86
+ Buttons allow user to change the number at the specified step. The position of the buttons can be changed by passing the `buttonsMode` property with the value `vertical` or `horizontal` (`vertical` by default) to the component or hidden by passing the `buttonsMode` property with the value `none` to the component.
87
+
88
+ ```jsx example
89
+ const [valueVertical, setValueVertical] = useState()
90
+ const [valueHorizontal, setValueHorizontal] = useState()
91
+ const [valueNone, setValueNone] = useState()
92
+ return (
93
+ <Div>
94
+ <NumberInput
95
+ buttonsMode='vertical'
96
+ value={valueVertical}
97
+ onChangeNumber={setValueVertical}
98
+ />
99
+ <Br />
100
+ <NumberInput
101
+ buttonsMode='horizontal'
102
+ value={valueHorizontal}
103
+ onChangeNumber={setValueHorizontal}
104
+ />
105
+ <Br />
106
+ <NumberInput
107
+ buttonsMode='none'
108
+ value={valueNone}
109
+ onChangeNumber={setValueNone}
110
+ />
111
+ </Div>
112
+ )
113
+ ```
114
+
115
+ ## Sizes
116
+
117
+ Size can be modified using the `size` prop. Default size is `'m'`.
118
+
119
+ ```jsx example
120
+ const [valueL, setValueL] = useState()
121
+ const [valueM, setValueM] = useState()
122
+ const [valueS, setValueS] = useState()
123
+ return (
124
+ <Div>
125
+ <NumberInput
126
+ size='s'
127
+ value={valueS}
128
+ onChangeNumber={setValueS}
129
+ />
130
+ <Br />
131
+ <NumberInput
132
+ size='m'
133
+ value={valueM}
134
+ onChangeNumber={setValueM}
135
+ />
136
+ <Br />
137
+ <NumberInput
138
+ size='l'
139
+ value={valueL}
140
+ onChangeNumber={setValueL}
141
+ />
142
+ </Div>
143
+ )
144
+ ```
145
+
146
+ ## Units
147
+
148
+ The `units` property displays the units of the input. By default, units are displayed to the left of the input. Use `unitsPosition='right'` to display the units on the right side of the input.
149
+
150
+ ```jsx example
151
+ const [value, setValue] = useState()
152
+ return (
153
+ <NumberInput
154
+ value={value}
155
+ units='$'
156
+ onChangeNumber={setValue}
157
+ />
158
+ )
159
+ ```
160
+
161
+ ## Stylization
162
+
163
+ For stylization can apply the properties:
164
+
165
+ - `style` - to style the root component
166
+ - `inputStyle` - to style the input
167
+ - `buttonStyle` - for styling buttons
168
+
169
+ ```jsx example
170
+ const [value, setValue] = useState()
171
+
172
+ return (
173
+ <NumberInput
174
+ style={{
175
+ width: 250
176
+ }}
177
+ buttonStyle={{
178
+ borderWidth: 0
179
+ }}
180
+ inputStyle={{
181
+ borderTopWidth: 0,
182
+ borderLeftWidth: 0,
183
+ borderRightWidth: 0,
184
+ borderRadius: 0
185
+ }}
186
+ placeholder='Enter number'
187
+ value={value}
188
+ onChangeNumber={setValue}
189
+ />
190
+ )
191
+ ```
192
+
193
+ ## Sandbox
194
+
195
+ <Sandbox
196
+ Component={NumberInput}
197
+ propsJsonSchema={NumberInputPropsJsonSchema}
198
+ extraParams={{
199
+ step: {
200
+ step: 0.0001
201
+ },
202
+ value: {
203
+ step: 0.0001
204
+ }
205
+ }}
206
+ block
207
+ />
package/buttons.tsx ADDED
@@ -0,0 +1,54 @@
1
+ import { type ReactNode } from 'react'
2
+ import { type StyleProp, type ViewStyle } from 'react-native'
3
+ import { pug, observer } from 'startupjs'
4
+ import { themed } from '@startupjs-ui/core'
5
+ import Button from '@startupjs-ui/button'
6
+ import { faAngleDown } from '@fortawesome/free-solid-svg-icons/faAngleDown'
7
+ import { faAngleUp } from '@fortawesome/free-solid-svg-icons/faAngleUp'
8
+ import { faMinus } from '@fortawesome/free-solid-svg-icons/faMinus'
9
+ import { faPlus } from '@fortawesome/free-solid-svg-icons/faPlus'
10
+ import './index.cssx.styl'
11
+
12
+ interface NumberInputButtonsProps {
13
+ buttonStyle?: StyleProp<ViewStyle>
14
+ mode?: 'none' | 'horizontal' | 'vertical'
15
+ size?: 'l' | 'm' | 's'
16
+ disabled?: boolean
17
+ onIncrement?: (value: number) => void
18
+ }
19
+
20
+ function NumberInputButtons ({
21
+ buttonStyle,
22
+ mode,
23
+ size,
24
+ disabled,
25
+ onIncrement
26
+ }: NumberInputButtonsProps): ReactNode {
27
+ const buttonStyleNames = [mode]
28
+
29
+ return pug`
30
+ if mode !== 'none'
31
+ Button.input-button.increase(
32
+ style=buttonStyle
33
+ styleName=buttonStyleNames
34
+ focusable=false
35
+ disabled=disabled
36
+ icon=mode === 'horizontal' ? faPlus : faAngleUp
37
+ size=size
38
+ variant='outlined'
39
+ onPress=() => onIncrement?.(1)
40
+ )
41
+ Button.input-button.decrease(
42
+ style=buttonStyle
43
+ styleName=buttonStyleNames
44
+ focusable=false
45
+ disabled=disabled
46
+ icon=mode === 'horizontal' ? faMinus : faAngleDown
47
+ size=size
48
+ variant='outlined'
49
+ onPress=() => onIncrement?.(-1)
50
+ )
51
+ `
52
+ }
53
+
54
+ export default observer(themed('NumberInput', NumberInputButtons))
@@ -0,0 +1,87 @@
1
+ $sizes = 'l' 'm' 's'
2
+ // border color should be taken from text input config
3
+ // because the input border and the button border overlap each other
4
+ $inputBorderColor = var(--color-border-main)
5
+ $padding = {
6
+ l: 6u,
7
+ m: 5u,
8
+ s: 4u
9
+ }
10
+
11
+ .input-wrapper
12
+ flex-grow 1
13
+
14
+ &.right
15
+ flex-direction row-reverse
16
+
17
+ &.readonly
18
+ align-self flex-start
19
+
20
+ .input-units
21
+ align-self center
22
+
23
+ &.l
24
+ font(body1)
25
+
26
+ &.readonly
27
+ &.left
28
+ margin-right 0.5u
29
+
30
+ &.right
31
+ margin-left 0.5u
32
+
33
+ .input-container
34
+ flex-grow 1
35
+ flex-shrink 1
36
+
37
+ &.left
38
+ margin-left 1u
39
+
40
+ &.right
41
+ margin-right 1u
42
+
43
+ .input-input
44
+ for size in $sizes
45
+ padding = $padding[size]
46
+
47
+ &.vertical.{size}
48
+ padding-right padding
49
+
50
+ &.horizontal.{size}
51
+ padding-left padding
52
+ padding-right @padding-left
53
+
54
+ .input-button
55
+ position absolute
56
+ border-color $inputBorderColor
57
+
58
+ &.vertical
59
+ right 0
60
+ height 50%
61
+
62
+ &.increase
63
+ top 0
64
+ border-top-left-radius 0
65
+ border-bottom-left-radius 0
66
+ border-bottom-right-radius 0
67
+
68
+ &.decrease
69
+ bottom 0
70
+ border-top-left-radius 0
71
+ border-bottom-left-radius 0
72
+ border-top-right-radius 0
73
+
74
+ &.horizontal
75
+ top 0
76
+ bottom 0
77
+ height auto
78
+
79
+ &.increase
80
+ right 0
81
+ border-top-left-radius 0
82
+ border-bottom-left-radius 0
83
+
84
+ &.decrease
85
+ left 0
86
+ border-top-right-radius 0
87
+ border-bottom-right-radius 0
package/index.d.ts ADDED
@@ -0,0 +1,48 @@
1
+ /* eslint-disable */
2
+ // DO NOT MODIFY THIS FILE - IT IS AUTOMATICALLY GENERATED ON COMMITS.
3
+
4
+ import { type RefObject } from 'react';
5
+ import { type StyleProp, type TextStyle, type ViewStyle } from 'react-native';
6
+ import './index.cssx.styl';
7
+ export declare const _PropsJsonSchema: {};
8
+ export interface NumberInputProps {
9
+ /** Custom styles for the wrapper */
10
+ style?: StyleProp<ViewStyle>;
11
+ /** Custom styles for increment and decrement buttons */
12
+ buttonStyle?: StyleProp<ViewStyle>;
13
+ /** Current numeric value */
14
+ value?: number;
15
+ /** Input size preset @default 'm' */
16
+ size?: 'l' | 'm' | 's';
17
+ /** Buttons layout @default 'vertical' */
18
+ buttonsMode?: 'none' | 'horizontal' | 'vertical';
19
+ /** Disable interactions @default false */
20
+ disabled?: boolean;
21
+ /** Render a non-editable value @default false */
22
+ readonly?: boolean;
23
+ /** Maximum allowed value */
24
+ max?: number;
25
+ /** Minimum allowed value */
26
+ min?: number;
27
+ /** Increment step @default 1 */
28
+ step?: number;
29
+ /** Units label displayed next to the value */
30
+ units?: string;
31
+ /** Units position @default 'left' */
32
+ unitsPosition?: 'left' | 'right';
33
+ /** Return key type for the keyboard @default 'done' */
34
+ returnKeyType?: string;
35
+ /** Handler triggered when numeric value changes */
36
+ onChangeNumber?: (value?: number) => void;
37
+ /** Ref to access the underlying TextInput */
38
+ ref?: RefObject<any>;
39
+ /** Custom styles for the input element */
40
+ inputStyle?: StyleProp<TextStyle>;
41
+ /** Placeholder text */
42
+ placeholder?: string | number;
43
+ /** Error flag @private */
44
+ _hasError?: boolean;
45
+ [key: string]: any;
46
+ }
47
+ declare const _default: import("react").ComponentType<NumberInputProps>;
48
+ export default _default;
package/index.tsx ADDED
@@ -0,0 +1,248 @@
1
+ import {
2
+ useEffect,
3
+ useMemo,
4
+ useState,
5
+ useRef,
6
+ type ReactNode,
7
+ type RefObject
8
+ } from 'react'
9
+ import {
10
+ Platform,
11
+ type StyleProp,
12
+ type TextStyle,
13
+ type ViewStyle
14
+ } from 'react-native'
15
+ import { pug, observer } from 'startupjs'
16
+ import { themed } from '@startupjs-ui/core'
17
+ import Div from '@startupjs-ui/div'
18
+ import Span from '@startupjs-ui/span'
19
+ import TextInput from '@startupjs-ui/text-input'
20
+ import Buttons from './buttons'
21
+ import './index.cssx.styl'
22
+
23
+ const IS_IOS = Platform.OS === 'ios'
24
+
25
+ export const _PropsJsonSchema = {/* NumberInputProps */}
26
+
27
+ export interface NumberInputProps {
28
+ /** Custom styles for the wrapper */
29
+ style?: StyleProp<ViewStyle>
30
+ /** Custom styles for increment and decrement buttons */
31
+ buttonStyle?: StyleProp<ViewStyle>
32
+ /** Current numeric value */
33
+ value?: number
34
+ /** Input size preset @default 'm' */
35
+ size?: 'l' | 'm' | 's'
36
+ /** Buttons layout @default 'vertical' */
37
+ buttonsMode?: 'none' | 'horizontal' | 'vertical'
38
+ /** Disable interactions @default false */
39
+ disabled?: boolean
40
+ /** Render a non-editable value @default false */
41
+ readonly?: boolean
42
+ /** Maximum allowed value */
43
+ max?: number
44
+ /** Minimum allowed value */
45
+ min?: number
46
+ /** Increment step @default 1 */
47
+ step?: number
48
+ /** Units label displayed next to the value */
49
+ units?: string
50
+ /** Units position @default 'left' */
51
+ unitsPosition?: 'left' | 'right'
52
+ /** Return key type for the keyboard @default 'done' */
53
+ returnKeyType?: string
54
+ /** Handler triggered when numeric value changes */
55
+ onChangeNumber?: (value?: number) => void
56
+ /** Ref to access the underlying TextInput */
57
+ ref?: RefObject<any>
58
+ /** Custom styles for the input element */
59
+ inputStyle?: StyleProp<TextStyle>
60
+ /** Placeholder text */
61
+ placeholder?: string | number
62
+ /** Error flag @private */
63
+ _hasError?: boolean
64
+ [key: string]: any
65
+ }
66
+
67
+ function NumberInput ({
68
+ style,
69
+ buttonStyle,
70
+ value,
71
+ size = 'm',
72
+ buttonsMode = 'vertical',
73
+ disabled = false,
74
+ readonly = false,
75
+ max,
76
+ min,
77
+ step = 1,
78
+ units,
79
+ unitsPosition = 'left',
80
+ onChangeNumber,
81
+ returnKeyType = 'done',
82
+ ref,
83
+ ...props
84
+ }: NumberInputProps): ReactNode {
85
+ const [inputValue, setInputValue] = useState<string | undefined>(undefined)
86
+ const inputValueRef = useRef<string | undefined>(undefined)
87
+
88
+ const precision = useMemo(() => {
89
+ return String(step).split('.')?.[1]?.length || 0
90
+ }, [step])
91
+
92
+ const regexp = useMemo(() => {
93
+ return precision > 0
94
+ ? new RegExp('^-?\\d*(\\.(\\d{0,' + precision + '})?)?$')
95
+ : /^-?\d*$/
96
+ }, [precision])
97
+
98
+ function updateInputValue (newValue: string | undefined) {
99
+ inputValueRef.current = newValue
100
+ setInputValue(newValue)
101
+ }
102
+
103
+ useEffect(() => {
104
+ if (value == null) {
105
+ updateInputValue('')
106
+ return
107
+ }
108
+
109
+ if (!isNaN(value) && Number(inputValueRef.current) !== value) {
110
+ let nextValue = value
111
+ if (min != null && nextValue < min) {
112
+ nextValue = min
113
+ } else if (max != null && nextValue > max) {
114
+ nextValue = max
115
+ }
116
+
117
+ nextValue = +nextValue.toFixed(precision)
118
+
119
+ updateInputValue(String(nextValue))
120
+ onChangeNumber && onChangeNumber(nextValue)
121
+ }
122
+ }, [value, min, max, precision, onChangeNumber])
123
+
124
+ function onChangeText (newValue: string) {
125
+ let formattedValue = newValue
126
+ // replace comma with dot for some locales
127
+ if (precision > 0) formattedValue = formattedValue.replace(/,/g, '.')
128
+
129
+ if (!regexp.test(formattedValue)) return
130
+
131
+ let updateValue: number | undefined
132
+ // check for an empty string and undefined
133
+ // and check for strings '-' or '.'
134
+ // to convert newValue to number
135
+ // otherwise should value should be undefined
136
+ if (formattedValue && !isNaN(Number(formattedValue))) {
137
+ const numericValue = Number(formattedValue)
138
+
139
+ if ((min != null && numericValue < min) || (max != null && numericValue > max)) {
140
+ // TODO: display tip?
141
+ return
142
+ }
143
+
144
+ updateValue = numericValue
145
+ }
146
+
147
+ updateInputValue(formattedValue)
148
+
149
+ // prevent update for the same values
150
+ // for example
151
+ // when add dot (values NUMBER and NUMBER. are the same)
152
+ // when add -
153
+ // when change value from -NUMBER to -
154
+ if (updateValue === value) return
155
+
156
+ onChangeNumber && onChangeNumber(updateValue)
157
+ }
158
+
159
+ function onIncrement (byNumber: number) {
160
+ const newValue = +((value ?? 0) + byNumber * step).toFixed(precision)
161
+ // we use string because this is the value for TextInput
162
+ onChangeText(String(newValue))
163
+ }
164
+
165
+ function getReturnKeyType (): string | undefined {
166
+ let res
167
+
168
+ if (IS_IOS && returnKeyType === 'none') {
169
+ res = 'default'
170
+ } else {
171
+ res = returnKeyType
172
+ }
173
+
174
+ return res
175
+ }
176
+
177
+ const extraStyleName: Record<string, any> = {}
178
+
179
+ if (units) {
180
+ extraStyleName[unitsPosition] = unitsPosition
181
+ }
182
+
183
+ const renderWrapper = (
184
+ { style: wrapperStyle }: { style?: StyleProp<ViewStyle> },
185
+ children: ReactNode
186
+ ): ReactNode => {
187
+ return pug`
188
+ Div(style=wrapperStyle)
189
+ Div.input-wrapper(
190
+ styleName=[extraStyleName, { readonly }]
191
+ row
192
+ )
193
+ if units
194
+ Span.input-units(
195
+ styleName=[size, extraStyleName, { readonly }]
196
+ )
197
+ = units
198
+ = children
199
+ `
200
+ }
201
+
202
+ if (readonly) {
203
+ return renderWrapper({
204
+ style: [style]
205
+ }, pug`
206
+ Span= value
207
+ `)
208
+ }
209
+
210
+ function renderInputWrapper (
211
+ wrapperProps: { style?: StyleProp<ViewStyle> },
212
+ children: ReactNode
213
+ ): ReactNode {
214
+ return renderWrapper(
215
+ wrapperProps,
216
+ pug`
217
+ Div.input-container(styleName=[extraStyleName])
218
+ Buttons(
219
+ buttonStyle=buttonStyle
220
+ mode=buttonsMode
221
+ size=size
222
+ disabled=disabled
223
+ onIncrement=onIncrement
224
+ )
225
+ = children
226
+ `)
227
+ }
228
+
229
+ return pug`
230
+ TextInput(
231
+ style=style
232
+ ref=ref
233
+ inputStyleName=['input-input', buttonsMode, size]
234
+ value=inputValue
235
+ size=size
236
+ disabled=disabled
237
+ keyboardType='numeric'
238
+ returnKeyType=getReturnKeyType()
239
+ onChangeText=onChangeText
240
+ _renderWrapper=renderInputWrapper
241
+ ...props
242
+ )
243
+ `
244
+ }
245
+
246
+ export default observer(
247
+ themed('NumberInput', NumberInput)
248
+ )
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@startupjs-ui/number-input",
3
+ "version": "0.1.3",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "main": "index.tsx",
8
+ "types": "index.d.ts",
9
+ "type": "module",
10
+ "dependencies": {
11
+ "@startupjs-ui/button": "^0.1.3",
12
+ "@startupjs-ui/core": "^0.1.3",
13
+ "@startupjs-ui/div": "^0.1.3",
14
+ "@startupjs-ui/span": "^0.1.3",
15
+ "@startupjs-ui/text-input": "^0.1.3"
16
+ },
17
+ "peerDependencies": {
18
+ "react": "*",
19
+ "react-native": "*",
20
+ "startupjs": "*"
21
+ },
22
+ "gitHead": "fd964ebc3892d3dd0a6c85438c0af619cc50c3f0"
23
+ }