@umituz/react-native-design-system 1.0.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 +21 -0
- package/README.md +157 -0
- package/package.json +43 -0
- package/src/index.ts +345 -0
- package/src/presentation/atoms/AtomicAvatar.tsx +157 -0
- package/src/presentation/atoms/AtomicAvatarGroup.tsx +169 -0
- package/src/presentation/atoms/AtomicBadge.tsx +232 -0
- package/src/presentation/atoms/AtomicButton.tsx +124 -0
- package/src/presentation/atoms/AtomicCard.tsx +112 -0
- package/src/presentation/atoms/AtomicChip.tsx +223 -0
- package/src/presentation/atoms/AtomicDatePicker.tsx +347 -0
- package/src/presentation/atoms/AtomicDivider.tsx +114 -0
- package/src/presentation/atoms/AtomicFab.tsx +104 -0
- package/src/presentation/atoms/AtomicFilter.tsx +154 -0
- package/src/presentation/atoms/AtomicFormError.tsx +105 -0
- package/src/presentation/atoms/AtomicIcon.tsx +29 -0
- package/src/presentation/atoms/AtomicImage.tsx +149 -0
- package/src/presentation/atoms/AtomicInput.tsx +232 -0
- package/src/presentation/atoms/AtomicNumberInput.tsx +182 -0
- package/src/presentation/atoms/AtomicPicker.tsx +458 -0
- package/src/presentation/atoms/AtomicProgress.tsx +143 -0
- package/src/presentation/atoms/AtomicSearchBar.tsx +114 -0
- package/src/presentation/atoms/AtomicSkeleton.tsx +146 -0
- package/src/presentation/atoms/AtomicSort.tsx +145 -0
- package/src/presentation/atoms/AtomicSwitch.tsx +166 -0
- package/src/presentation/atoms/AtomicText.tsx +50 -0
- package/src/presentation/atoms/AtomicTextArea.tsx +198 -0
- package/src/presentation/atoms/AtomicTouchable.tsx +233 -0
- package/src/presentation/atoms/fab/styles/fabStyles.ts +69 -0
- package/src/presentation/atoms/fab/types/index.ts +88 -0
- package/src/presentation/atoms/filter/styles/filterStyles.ts +32 -0
- package/src/presentation/atoms/filter/types/index.ts +89 -0
- package/src/presentation/atoms/index.ts +378 -0
- package/src/presentation/atoms/input/hooks/useInputState.ts +15 -0
- package/src/presentation/atoms/input/styles/inputStyles.ts +66 -0
- package/src/presentation/atoms/input/types/index.ts +25 -0
- package/src/presentation/atoms/picker/styles/pickerStyles.ts +200 -0
- package/src/presentation/atoms/picker/types/index.ts +40 -0
- package/src/presentation/atoms/touchable/styles/touchableStyles.ts +71 -0
- package/src/presentation/atoms/touchable/types/index.ts +162 -0
- package/src/presentation/hooks/useAppDesignTokens.ts +78 -0
- package/src/presentation/hooks/useResponsive.ts +180 -0
- package/src/presentation/loading/index.ts +40 -0
- package/src/presentation/loading/presentation/components/LoadingSpinner.tsx +116 -0
- package/src/presentation/loading/presentation/components/LoadingState.tsx +200 -0
- package/src/presentation/loading/presentation/hooks/useLoading.ts +100 -0
- package/src/presentation/molecules/AtomicConfirmationModal.tsx +263 -0
- package/src/presentation/molecules/EmptyState.tsx +130 -0
- package/src/presentation/molecules/FormField.tsx +128 -0
- package/src/presentation/molecules/GridContainer.tsx +124 -0
- package/src/presentation/molecules/IconContainer.tsx +94 -0
- package/src/presentation/molecules/LanguageSwitcher.tsx +42 -0
- package/src/presentation/molecules/ListItem.tsx +36 -0
- package/src/presentation/molecules/ScreenHeader.tsx +140 -0
- package/src/presentation/molecules/SearchBar.tsx +85 -0
- package/src/presentation/molecules/SectionCard.tsx +74 -0
- package/src/presentation/molecules/SectionContainer.tsx +106 -0
- package/src/presentation/molecules/SectionHeader.tsx +125 -0
- package/src/presentation/molecules/confirmation-modal/styles/confirmationModalStyles.ts +133 -0
- package/src/presentation/molecules/confirmation-modal/types/index.ts +107 -0
- package/src/presentation/molecules/index.ts +42 -0
- package/src/presentation/molecules/languageswitcher/config/languageSwitcherConfig.ts +5 -0
- package/src/presentation/molecules/languageswitcher/hooks/useLanguageNavigation.ts +15 -0
- package/src/presentation/molecules/listitem/styles/listItemStyles.ts +19 -0
- package/src/presentation/molecules/listitem/types/index.ts +17 -0
- package/src/presentation/organisms/AppHeader.tsx +136 -0
- package/src/presentation/organisms/FormContainer.tsx +180 -0
- package/src/presentation/organisms/ScreenLayout.tsx +209 -0
- package/src/presentation/organisms/index.ts +25 -0
- package/src/presentation/tokens/AppDesignTokens.ts +57 -0
- package/src/presentation/tokens/commonStyles.ts +253 -0
- package/src/presentation/tokens/core/BaseTokens.ts +394 -0
- package/src/presentation/tokens/core/ColorPalette.ts +398 -0
- package/src/presentation/tokens/core/TokenFactory.ts +120 -0
- package/src/presentation/utils/platformConstants.ts +124 -0
- package/src/presentation/utils/responsive.ts +516 -0
- package/src/presentation/utils/variants/compound.ts +29 -0
- package/src/presentation/utils/variants/core.ts +39 -0
- package/src/presentation/utils/variants/helpers.ts +13 -0
- package/src/presentation/utils/variants.ts +3 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { View, Pressable, StyleProp, ViewStyle, TextStyle } from 'react-native';
|
|
3
|
+
import { TextInput, HelperText } from 'react-native-paper';
|
|
4
|
+
import { useAppDesignTokens } from '../hooks/useAppDesignTokens';
|
|
5
|
+
import { AtomicIcon } from './AtomicIcon';
|
|
6
|
+
import type { AtomicIconName, AtomicIconSize } from './AtomicIcon';
|
|
7
|
+
|
|
8
|
+
export type AtomicInputVariant = 'outlined' | 'filled' | 'flat';
|
|
9
|
+
export type AtomicInputState = 'default' | 'error' | 'success' | 'disabled';
|
|
10
|
+
export type AtomicInputSize = 'sm' | 'md' | 'lg';
|
|
11
|
+
|
|
12
|
+
export interface AtomicInputProps {
|
|
13
|
+
/** Input label */
|
|
14
|
+
label?: string;
|
|
15
|
+
/** Current input value */
|
|
16
|
+
value?: string;
|
|
17
|
+
/** Value change callback */
|
|
18
|
+
onChangeText?: (text: string) => void;
|
|
19
|
+
/** Input variant (outlined, filled, flat) */
|
|
20
|
+
variant?: AtomicInputVariant;
|
|
21
|
+
/** Input state (default, error, success, disabled) */
|
|
22
|
+
state?: AtomicInputState;
|
|
23
|
+
/** Input size (sm, md, lg) */
|
|
24
|
+
size?: AtomicInputSize;
|
|
25
|
+
/** Placeholder text */
|
|
26
|
+
placeholder?: string;
|
|
27
|
+
/** Helper text below input */
|
|
28
|
+
helperText?: string;
|
|
29
|
+
/** Leading icon (Lucide icon name) */
|
|
30
|
+
leadingIcon?: AtomicIconName;
|
|
31
|
+
/** Trailing icon (Lucide icon name) */
|
|
32
|
+
trailingIcon?: AtomicIconName;
|
|
33
|
+
/** Callback when trailing icon is pressed */
|
|
34
|
+
onTrailingIconPress?: () => void;
|
|
35
|
+
/** Show password toggle for secure inputs */
|
|
36
|
+
showPasswordToggle?: boolean;
|
|
37
|
+
/** Secure text entry (password field) */
|
|
38
|
+
secureTextEntry?: boolean;
|
|
39
|
+
/** Maximum character length */
|
|
40
|
+
maxLength?: number;
|
|
41
|
+
/** Show character counter */
|
|
42
|
+
showCharacterCount?: boolean;
|
|
43
|
+
/** Keyboard type */
|
|
44
|
+
keyboardType?: 'default' | 'email-address' | 'numeric' | 'phone-pad' | 'url' | 'number-pad' | 'decimal-pad';
|
|
45
|
+
/** Auto-capitalize */
|
|
46
|
+
autoCapitalize?: 'none' | 'sentences' | 'words' | 'characters';
|
|
47
|
+
/** Auto-correct */
|
|
48
|
+
autoCorrect?: boolean;
|
|
49
|
+
/** Disabled state */
|
|
50
|
+
disabled?: boolean;
|
|
51
|
+
/** Container style */
|
|
52
|
+
style?: StyleProp<ViewStyle>;
|
|
53
|
+
/** Input text style */
|
|
54
|
+
inputStyle?: StyleProp<TextStyle>;
|
|
55
|
+
/** Test ID for E2E testing */
|
|
56
|
+
testID?: string;
|
|
57
|
+
/** Blur callback */
|
|
58
|
+
onBlur?: () => void;
|
|
59
|
+
/** Focus callback */
|
|
60
|
+
onFocus?: () => void;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* AtomicInput - Material Design 3 Text Input
|
|
65
|
+
*
|
|
66
|
+
* Features:
|
|
67
|
+
* - React Native Paper TextInput integration
|
|
68
|
+
* - Lucide icons for password toggle and custom icons
|
|
69
|
+
* - Material Design 3 outlined/filled/flat variants
|
|
70
|
+
* - Error, success, disabled states
|
|
71
|
+
* - Character counter
|
|
72
|
+
* - Responsive sizing
|
|
73
|
+
* - Full accessibility support
|
|
74
|
+
*/
|
|
75
|
+
export const AtomicInput: React.FC<AtomicInputProps> = ({
|
|
76
|
+
variant = 'outlined',
|
|
77
|
+
state = 'default',
|
|
78
|
+
size = 'md',
|
|
79
|
+
label,
|
|
80
|
+
value = '',
|
|
81
|
+
onChangeText,
|
|
82
|
+
placeholder,
|
|
83
|
+
helperText,
|
|
84
|
+
leadingIcon,
|
|
85
|
+
trailingIcon,
|
|
86
|
+
onTrailingIconPress,
|
|
87
|
+
showPasswordToggle = false,
|
|
88
|
+
secureTextEntry = false,
|
|
89
|
+
maxLength,
|
|
90
|
+
showCharacterCount = false,
|
|
91
|
+
keyboardType = 'default',
|
|
92
|
+
autoCapitalize = 'sentences',
|
|
93
|
+
autoCorrect = true,
|
|
94
|
+
disabled = false,
|
|
95
|
+
style,
|
|
96
|
+
inputStyle,
|
|
97
|
+
testID,
|
|
98
|
+
onBlur,
|
|
99
|
+
onFocus,
|
|
100
|
+
}) => {
|
|
101
|
+
const tokens = useAppDesignTokens();
|
|
102
|
+
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
|
|
103
|
+
const isDisabled = state === 'disabled' || disabled;
|
|
104
|
+
const characterCount = value?.toString().length || 0;
|
|
105
|
+
|
|
106
|
+
// Map variant to Paper mode
|
|
107
|
+
const getPaperMode = (): 'outlined' | 'flat' => {
|
|
108
|
+
if (variant === 'outlined') return 'outlined';
|
|
109
|
+
return 'flat'; // filled and flat both use 'flat' mode
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// Map state to Paper error prop
|
|
113
|
+
const hasError = state === 'error';
|
|
114
|
+
|
|
115
|
+
// Get icon size based on input size
|
|
116
|
+
const getIconSize = (): AtomicIconSize => {
|
|
117
|
+
switch (size) {
|
|
118
|
+
case 'sm': return 'xs';
|
|
119
|
+
case 'lg': return 'md';
|
|
120
|
+
default: return 'sm';
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const iconSizeName = getIconSize();
|
|
125
|
+
const iconColor = isDisabled
|
|
126
|
+
? tokens.colors.onSurfaceDisabled
|
|
127
|
+
: tokens.colors.surfaceVariant;
|
|
128
|
+
|
|
129
|
+
// Render leading icon
|
|
130
|
+
const renderLeadingIcon = leadingIcon ? () => (
|
|
131
|
+
<AtomicIcon
|
|
132
|
+
name={leadingIcon}
|
|
133
|
+
size={iconSizeName}
|
|
134
|
+
customColor={iconColor}
|
|
135
|
+
/>
|
|
136
|
+
) : undefined;
|
|
137
|
+
|
|
138
|
+
// Render trailing icon or password toggle
|
|
139
|
+
const renderTrailingIcon = () => {
|
|
140
|
+
if (showPasswordToggle && secureTextEntry) {
|
|
141
|
+
return (
|
|
142
|
+
<Pressable onPress={() => setIsPasswordVisible(!isPasswordVisible)}>
|
|
143
|
+
<AtomicIcon
|
|
144
|
+
name={isPasswordVisible ? "EyeOff" : "Eye"}
|
|
145
|
+
size={iconSizeName}
|
|
146
|
+
customColor={iconColor}
|
|
147
|
+
/>
|
|
148
|
+
</Pressable>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (trailingIcon) {
|
|
153
|
+
const icon = (
|
|
154
|
+
<AtomicIcon
|
|
155
|
+
name={trailingIcon}
|
|
156
|
+
size={iconSizeName}
|
|
157
|
+
customColor={iconColor}
|
|
158
|
+
/>
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
return onTrailingIconPress ? (
|
|
162
|
+
<Pressable onPress={onTrailingIconPress}>{icon}</Pressable>
|
|
163
|
+
) : icon;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return undefined;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// Get text color based on state
|
|
170
|
+
const getTextColor = () => {
|
|
171
|
+
if (state === 'error') return tokens.colors.error;
|
|
172
|
+
if (state === 'success') return tokens.colors.success;
|
|
173
|
+
return tokens.colors.onSurface;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<View style={style} testID={testID}>
|
|
178
|
+
<TextInput
|
|
179
|
+
mode={getPaperMode()}
|
|
180
|
+
label={label}
|
|
181
|
+
value={value}
|
|
182
|
+
onChangeText={onChangeText}
|
|
183
|
+
placeholder={placeholder}
|
|
184
|
+
error={hasError}
|
|
185
|
+
disabled={isDisabled}
|
|
186
|
+
secureTextEntry={secureTextEntry && !isPasswordVisible}
|
|
187
|
+
maxLength={maxLength}
|
|
188
|
+
keyboardType={keyboardType}
|
|
189
|
+
autoCapitalize={autoCapitalize}
|
|
190
|
+
autoCorrect={autoCorrect}
|
|
191
|
+
left={renderLeadingIcon ? <TextInput.Icon icon={renderLeadingIcon} /> : undefined}
|
|
192
|
+
right={renderTrailingIcon() ? <TextInput.Icon icon={renderTrailingIcon} /> : undefined}
|
|
193
|
+
style={inputStyle}
|
|
194
|
+
textColor={getTextColor()}
|
|
195
|
+
onBlur={onBlur}
|
|
196
|
+
onFocus={onFocus}
|
|
197
|
+
testID={testID ? `${testID}-input` : undefined}
|
|
198
|
+
/>
|
|
199
|
+
|
|
200
|
+
{(helperText || showCharacterCount) && (
|
|
201
|
+
<View style={{
|
|
202
|
+
flexDirection: 'row',
|
|
203
|
+
justifyContent: 'space-between',
|
|
204
|
+
marginTop: tokens.spacing.xs,
|
|
205
|
+
}}>
|
|
206
|
+
{helperText && (
|
|
207
|
+
<HelperText
|
|
208
|
+
type={hasError ? 'error' : 'info'}
|
|
209
|
+
visible={Boolean(helperText)}
|
|
210
|
+
style={{ flex: 1 }}
|
|
211
|
+
testID={testID ? `${testID}-helper` : undefined}
|
|
212
|
+
>
|
|
213
|
+
{helperText}
|
|
214
|
+
</HelperText>
|
|
215
|
+
)}
|
|
216
|
+
{showCharacterCount && maxLength && (
|
|
217
|
+
<HelperText
|
|
218
|
+
type="info"
|
|
219
|
+
visible={true}
|
|
220
|
+
style={{ marginLeft: tokens.spacing.xs }}
|
|
221
|
+
testID={testID ? `${testID}-count` : undefined}
|
|
222
|
+
>
|
|
223
|
+
{characterCount}/{maxLength}
|
|
224
|
+
</HelperText>
|
|
225
|
+
)}
|
|
226
|
+
</View>
|
|
227
|
+
)}
|
|
228
|
+
</View>
|
|
229
|
+
);
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
export type { AtomicInputProps as InputProps };
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AtomicNumberInput Component
|
|
3
|
+
*
|
|
4
|
+
* A specialized number input component that wraps AtomicInput with
|
|
5
|
+
* number-specific validation and keyboard handling.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Numeric keyboard (integer or decimal)
|
|
9
|
+
* - Min/max validation
|
|
10
|
+
* - Step increment support
|
|
11
|
+
* - Automatic error states for invalid numbers
|
|
12
|
+
* - Parsed number callback (onValueChange)
|
|
13
|
+
* - Consistent styling with AtomicInput
|
|
14
|
+
* - All AtomicInput features (variants, states, sizes)
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* ```tsx
|
|
18
|
+
* const [age, setAge] = useState<number | null>(null);
|
|
19
|
+
*
|
|
20
|
+
* <AtomicNumberInput
|
|
21
|
+
* value={age?.toString() || ''}
|
|
22
|
+
* onValueChange={setAge}
|
|
23
|
+
* label="Age"
|
|
24
|
+
* min={0}
|
|
25
|
+
* max={150}
|
|
26
|
+
* helperText="Enter your age"
|
|
27
|
+
* />
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* Why This Component:
|
|
31
|
+
* - Separation of concerns (text vs number input)
|
|
32
|
+
* - Built-in number validation
|
|
33
|
+
* - Type-safe number callbacks
|
|
34
|
+
* - Prevents non-numeric input via keyboard
|
|
35
|
+
* - Consistent with AtomicInput styling
|
|
36
|
+
*
|
|
37
|
+
* @module AtomicNumberInput
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import React, { useState, useEffect } from 'react';
|
|
41
|
+
import { AtomicInput, AtomicInputProps } from './AtomicInput';
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Props for AtomicNumberInput component
|
|
45
|
+
* Extends AtomicInput but removes text-specific props
|
|
46
|
+
*/
|
|
47
|
+
export interface AtomicNumberInputProps
|
|
48
|
+
extends Omit<
|
|
49
|
+
AtomicInputProps,
|
|
50
|
+
'keyboardType' | 'secureTextEntry' | 'showPasswordToggle' | 'onChangeText'
|
|
51
|
+
> {
|
|
52
|
+
/** Minimum allowed value */
|
|
53
|
+
min?: number;
|
|
54
|
+
/** Maximum allowed value */
|
|
55
|
+
max?: number;
|
|
56
|
+
/** Step increment (for spinners, future feature) */
|
|
57
|
+
step?: number;
|
|
58
|
+
/** Allow decimal numbers (default: false for integers only) */
|
|
59
|
+
allowDecimal?: boolean;
|
|
60
|
+
/** Callback when valid number is entered (null if invalid/empty) */
|
|
61
|
+
onValueChange?: (value: number | null) => void;
|
|
62
|
+
/** Callback when raw text changes (optional) */
|
|
63
|
+
onTextChange?: (text: string) => void;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* AtomicNumberInput - Specialized numeric input component
|
|
68
|
+
*
|
|
69
|
+
* Wraps AtomicInput with:
|
|
70
|
+
* - Numeric keyboard
|
|
71
|
+
* - Number validation (min, max, format)
|
|
72
|
+
* - Parsed number callbacks
|
|
73
|
+
* - Automatic error states
|
|
74
|
+
*/
|
|
75
|
+
export const AtomicNumberInput: React.FC<AtomicNumberInputProps> = ({
|
|
76
|
+
min,
|
|
77
|
+
max,
|
|
78
|
+
step = 1,
|
|
79
|
+
allowDecimal = false,
|
|
80
|
+
onValueChange,
|
|
81
|
+
onTextChange,
|
|
82
|
+
value = '',
|
|
83
|
+
state: externalState,
|
|
84
|
+
helperText: externalHelperText,
|
|
85
|
+
...props
|
|
86
|
+
}) => {
|
|
87
|
+
const [internalError, setInternalError] = useState<string | undefined>(undefined);
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Validate number and return error message if invalid
|
|
91
|
+
*/
|
|
92
|
+
const validateNumber = (text: string): string | undefined => {
|
|
93
|
+
// Empty is valid (null value)
|
|
94
|
+
if (!text || text === '' || text === '-' || text === '.') {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Parse number
|
|
99
|
+
const num = parseFloat(text);
|
|
100
|
+
|
|
101
|
+
// Check if valid number
|
|
102
|
+
if (isNaN(num)) {
|
|
103
|
+
return 'Invalid number';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Check min constraint
|
|
107
|
+
if (min !== undefined && num < min) {
|
|
108
|
+
return `Minimum value is ${min}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Check max constraint
|
|
112
|
+
if (max !== undefined && num > max) {
|
|
113
|
+
return `Maximum value is ${max}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return undefined;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Handle text change with validation
|
|
121
|
+
*/
|
|
122
|
+
const handleChangeText = (text: string) => {
|
|
123
|
+
// Allow empty, minus sign, and decimal point during typing
|
|
124
|
+
if (text === '' || text === '-' || (allowDecimal && text === '.')) {
|
|
125
|
+
setInternalError(undefined);
|
|
126
|
+
onTextChange?.(text);
|
|
127
|
+
onValueChange?.(null);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Validate format
|
|
132
|
+
const decimalRegex = allowDecimal ? /^-?\d*\.?\d*$/ : /^-?\d*$/;
|
|
133
|
+
if (!decimalRegex.test(text)) {
|
|
134
|
+
// Invalid format, don't update
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Validate number
|
|
139
|
+
const error = validateNumber(text);
|
|
140
|
+
setInternalError(error);
|
|
141
|
+
|
|
142
|
+
// Call text callback
|
|
143
|
+
onTextChange?.(text);
|
|
144
|
+
|
|
145
|
+
// Call value callback with parsed number
|
|
146
|
+
if (!error && text !== '' && text !== '-' && text !== '.') {
|
|
147
|
+
const num = parseFloat(text);
|
|
148
|
+
onValueChange?.(isNaN(num) ? null : num);
|
|
149
|
+
} else {
|
|
150
|
+
onValueChange?.(null);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Validate on mount and when value/constraints change
|
|
156
|
+
*/
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
if (value) {
|
|
159
|
+
const error = validateNumber(value.toString());
|
|
160
|
+
setInternalError(error);
|
|
161
|
+
} else {
|
|
162
|
+
setInternalError(undefined);
|
|
163
|
+
}
|
|
164
|
+
}, [value, min, max]);
|
|
165
|
+
|
|
166
|
+
// Determine final state (external state overrides internal error)
|
|
167
|
+
const finalState = externalState || (internalError ? 'error' : 'default');
|
|
168
|
+
|
|
169
|
+
// Determine final helper text (internal error overrides external helper)
|
|
170
|
+
const finalHelperText = internalError || externalHelperText;
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<AtomicInput
|
|
174
|
+
{...props}
|
|
175
|
+
value={value}
|
|
176
|
+
onChangeText={handleChangeText}
|
|
177
|
+
keyboardType={allowDecimal ? 'decimal-pad' : 'numeric'}
|
|
178
|
+
state={finalState}
|
|
179
|
+
helperText={finalHelperText}
|
|
180
|
+
/>
|
|
181
|
+
);
|
|
182
|
+
};
|