@umituz/react-native-design-system 1.5.36 → 1.5.38
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/README.md +2 -2
- package/package.json +7 -5
- package/src/index.ts +29 -221
- package/src/presentation/organisms/AppHeader.tsx +3 -5
- package/src/presentation/tokens/commonStyles.ts +1 -1
- package/src/presentation/atoms/AtomicAvatar.tsx +0 -157
- package/src/presentation/atoms/AtomicAvatarGroup.tsx +0 -169
- package/src/presentation/atoms/AtomicBadge.tsx +0 -232
- package/src/presentation/atoms/AtomicButton.tsx +0 -236
- package/src/presentation/atoms/AtomicCard.tsx +0 -107
- package/src/presentation/atoms/AtomicChip.tsx +0 -223
- package/src/presentation/atoms/AtomicDatePicker.tsx +0 -347
- package/src/presentation/atoms/AtomicDivider.tsx +0 -114
- package/src/presentation/atoms/AtomicFab.tsx +0 -98
- package/src/presentation/atoms/AtomicFilter.tsx +0 -154
- package/src/presentation/atoms/AtomicFormError.tsx +0 -105
- package/src/presentation/atoms/AtomicIcon.tsx +0 -40
- package/src/presentation/atoms/AtomicImage.tsx +0 -149
- package/src/presentation/atoms/AtomicInput.tsx +0 -363
- package/src/presentation/atoms/AtomicNumberInput.tsx +0 -182
- package/src/presentation/atoms/AtomicPicker.tsx +0 -458
- package/src/presentation/atoms/AtomicProgress.tsx +0 -139
- package/src/presentation/atoms/AtomicSearchBar.tsx +0 -114
- package/src/presentation/atoms/AtomicSort.tsx +0 -145
- package/src/presentation/atoms/AtomicSwitch.tsx +0 -166
- package/src/presentation/atoms/AtomicText.tsx +0 -55
- package/src/presentation/atoms/AtomicTextArea.tsx +0 -313
- package/src/presentation/atoms/AtomicTouchable.tsx +0 -209
- package/src/presentation/atoms/fab/styles/fabStyles.ts +0 -69
- package/src/presentation/atoms/fab/types/index.ts +0 -82
- package/src/presentation/atoms/filter/styles/filterStyles.ts +0 -32
- package/src/presentation/atoms/filter/types/index.ts +0 -89
- package/src/presentation/atoms/index.ts +0 -366
- package/src/presentation/atoms/input/hooks/useInputState.ts +0 -15
- package/src/presentation/atoms/input/styles/inputStyles.ts +0 -66
- package/src/presentation/atoms/input/types/index.ts +0 -25
- package/src/presentation/atoms/picker/styles/pickerStyles.ts +0 -207
- package/src/presentation/atoms/picker/types/index.ts +0 -40
- package/src/presentation/atoms/touchable/styles/touchableStyles.ts +0 -62
- package/src/presentation/atoms/touchable/types/index.ts +0 -155
- package/src/presentation/hooks/useResponsive.ts +0 -180
- package/src/presentation/molecules/AtomicConfirmationModal.tsx +0 -243
- package/src/presentation/molecules/EmptyState.tsx +0 -130
- package/src/presentation/molecules/FormField.tsx +0 -128
- package/src/presentation/molecules/GridContainer.tsx +0 -124
- package/src/presentation/molecules/IconContainer.tsx +0 -94
- package/src/presentation/molecules/ListItem.tsx +0 -36
- package/src/presentation/molecules/ScreenHeader.tsx +0 -140
- package/src/presentation/molecules/SearchBar.tsx +0 -85
- package/src/presentation/molecules/SectionCard.tsx +0 -74
- package/src/presentation/molecules/SectionContainer.tsx +0 -106
- package/src/presentation/molecules/SectionHeader.tsx +0 -125
- package/src/presentation/molecules/confirmation-modal/styles/confirmationModalStyles.ts +0 -133
- package/src/presentation/molecules/confirmation-modal/types/index.ts +0 -105
- package/src/presentation/molecules/index.ts +0 -41
- package/src/presentation/molecules/listitem/styles/listItemStyles.ts +0 -19
- package/src/presentation/molecules/listitem/types/index.ts +0 -17
- package/src/presentation/organisms/FormContainer.tsx +0 -180
- package/src/presentation/organisms/ScreenLayout.tsx +0 -171
- package/src/presentation/organisms/index.ts +0 -25
- package/src/presentation/utils/platformConstants.ts +0 -124
- package/src/presentation/utils/responsive.ts +0 -516
|
@@ -1,363 +0,0 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
2
|
-
import { View, TextInput, Pressable, StyleSheet, StyleProp, ViewStyle, TextStyle } from 'react-native';
|
|
3
|
-
import { useAppDesignTokens } from '@umituz/react-native-theme';
|
|
4
|
-
import { AtomicIcon } from './AtomicIcon';
|
|
5
|
-
import { AtomicText } from './AtomicText';
|
|
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 - Pure React Native Text Input
|
|
65
|
-
*
|
|
66
|
-
* Features:
|
|
67
|
-
* - Pure React Native implementation (no Paper dependency)
|
|
68
|
-
* - Lucide icons for password toggle and custom icons
|
|
69
|
-
* - 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 [isFocused, setIsFocused] = useState(false);
|
|
104
|
-
const isDisabled = state === 'disabled' || disabled;
|
|
105
|
-
const characterCount = value?.toString().length || 0;
|
|
106
|
-
const hasError = state === 'error';
|
|
107
|
-
const hasSuccess = state === 'success';
|
|
108
|
-
|
|
109
|
-
// Size configuration
|
|
110
|
-
const sizeConfig = {
|
|
111
|
-
sm: {
|
|
112
|
-
paddingVertical: tokens.spacing.xs,
|
|
113
|
-
paddingHorizontal: tokens.spacing.sm,
|
|
114
|
-
fontSize: tokens.typography.bodySmall.fontSize,
|
|
115
|
-
iconSize: 16,
|
|
116
|
-
minHeight: 40,
|
|
117
|
-
},
|
|
118
|
-
md: {
|
|
119
|
-
paddingVertical: tokens.spacing.sm,
|
|
120
|
-
paddingHorizontal: tokens.spacing.md,
|
|
121
|
-
fontSize: tokens.typography.bodyMedium.fontSize,
|
|
122
|
-
iconSize: 20,
|
|
123
|
-
minHeight: 48,
|
|
124
|
-
},
|
|
125
|
-
lg: {
|
|
126
|
-
paddingVertical: tokens.spacing.md,
|
|
127
|
-
paddingHorizontal: tokens.spacing.lg,
|
|
128
|
-
fontSize: tokens.typography.bodyLarge.fontSize,
|
|
129
|
-
iconSize: 24,
|
|
130
|
-
minHeight: 56,
|
|
131
|
-
},
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
const config = sizeConfig[size] ?? sizeConfig.md;
|
|
135
|
-
|
|
136
|
-
// Ensure config is always defined with safe fallbacks
|
|
137
|
-
const fontSize = config?.fontSize ?? tokens.typography.bodyMedium?.fontSize ?? 16;
|
|
138
|
-
const iconSize = config?.iconSize ?? 20;
|
|
139
|
-
const paddingVertical = config?.paddingVertical ?? tokens.spacing?.sm ?? 8;
|
|
140
|
-
const paddingHorizontal = config?.paddingHorizontal ?? tokens.spacing?.md ?? 12;
|
|
141
|
-
const minHeight = config?.minHeight ?? 48;
|
|
142
|
-
|
|
143
|
-
// Get variant styles
|
|
144
|
-
const getVariantStyle = (): ViewStyle => {
|
|
145
|
-
const baseStyle: ViewStyle = {
|
|
146
|
-
backgroundColor: tokens.colors.surface,
|
|
147
|
-
borderRadius: tokens.borders.radius.md,
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
let borderColor = tokens.colors.border;
|
|
151
|
-
if (isFocused) borderColor = tokens.colors.primary;
|
|
152
|
-
if (hasError) borderColor = tokens.colors.error;
|
|
153
|
-
if (hasSuccess) borderColor = tokens.colors.success;
|
|
154
|
-
if (isDisabled) borderColor = tokens.colors.borderDisabled;
|
|
155
|
-
|
|
156
|
-
switch (variant) {
|
|
157
|
-
case 'outlined':
|
|
158
|
-
return {
|
|
159
|
-
...baseStyle,
|
|
160
|
-
borderWidth: isFocused ? 2 : 1,
|
|
161
|
-
borderColor,
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
case 'filled':
|
|
165
|
-
return {
|
|
166
|
-
...baseStyle,
|
|
167
|
-
backgroundColor: tokens.colors.surfaceSecondary,
|
|
168
|
-
borderWidth: 0,
|
|
169
|
-
borderBottomWidth: isFocused ? 2 : 1,
|
|
170
|
-
borderBottomColor: borderColor,
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
case 'flat':
|
|
174
|
-
return {
|
|
175
|
-
...baseStyle,
|
|
176
|
-
backgroundColor: 'transparent',
|
|
177
|
-
borderWidth: 0,
|
|
178
|
-
borderBottomWidth: 1,
|
|
179
|
-
borderBottomColor: borderColor,
|
|
180
|
-
borderRadius: 0,
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
default:
|
|
184
|
-
return baseStyle;
|
|
185
|
-
}
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
// Get text color based on state
|
|
189
|
-
const getTextColor = () => {
|
|
190
|
-
if (isDisabled) return tokens.colors.textDisabled;
|
|
191
|
-
if (hasError) return tokens.colors.error;
|
|
192
|
-
if (hasSuccess) return tokens.colors.success;
|
|
193
|
-
return tokens.colors.textPrimary;
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
const iconColor = isDisabled ? tokens.colors.textDisabled : tokens.colors.textSecondary;
|
|
197
|
-
|
|
198
|
-
const containerStyle: StyleProp<ViewStyle> = [
|
|
199
|
-
styles.container,
|
|
200
|
-
getVariantStyle(),
|
|
201
|
-
{
|
|
202
|
-
paddingTop: paddingVertical,
|
|
203
|
-
paddingBottom: paddingVertical,
|
|
204
|
-
paddingHorizontal: paddingHorizontal,
|
|
205
|
-
minHeight: minHeight,
|
|
206
|
-
justifyContent: 'center',
|
|
207
|
-
opacity: isDisabled ? 0.5 : 1,
|
|
208
|
-
},
|
|
209
|
-
style,
|
|
210
|
-
];
|
|
211
|
-
|
|
212
|
-
const textInputStyle: StyleProp<TextStyle> = [
|
|
213
|
-
styles.input,
|
|
214
|
-
{
|
|
215
|
-
fontSize: fontSize,
|
|
216
|
-
lineHeight: fontSize * 1.2, // Tighter lineHeight to prevent text clipping
|
|
217
|
-
color: getTextColor(),
|
|
218
|
-
paddingVertical: 0, // Remove vertical padding to prevent clipping
|
|
219
|
-
},
|
|
220
|
-
leadingIcon ? { paddingLeft: iconSize + 8 } : undefined,
|
|
221
|
-
(trailingIcon || showPasswordToggle) ? { paddingRight: iconSize + 8 } : undefined,
|
|
222
|
-
inputStyle,
|
|
223
|
-
];
|
|
224
|
-
|
|
225
|
-
return (
|
|
226
|
-
<View testID={testID}>
|
|
227
|
-
{label && (
|
|
228
|
-
<AtomicText
|
|
229
|
-
type="labelMedium"
|
|
230
|
-
color={hasError ? 'error' : hasSuccess ? 'success' : 'secondary'}
|
|
231
|
-
style={styles.label}
|
|
232
|
-
>
|
|
233
|
-
{label}
|
|
234
|
-
</AtomicText>
|
|
235
|
-
)}
|
|
236
|
-
|
|
237
|
-
<View style={containerStyle}>
|
|
238
|
-
{leadingIcon && (
|
|
239
|
-
<View style={styles.leadingIcon}>
|
|
240
|
-
<AtomicIcon
|
|
241
|
-
name={leadingIcon}
|
|
242
|
-
customSize={iconSize}
|
|
243
|
-
customColor={iconColor}
|
|
244
|
-
/>
|
|
245
|
-
</View>
|
|
246
|
-
)}
|
|
247
|
-
|
|
248
|
-
<TextInput
|
|
249
|
-
value={value}
|
|
250
|
-
onChangeText={onChangeText}
|
|
251
|
-
placeholder={placeholder}
|
|
252
|
-
placeholderTextColor={tokens.colors.textSecondary}
|
|
253
|
-
secureTextEntry={secureTextEntry && !isPasswordVisible}
|
|
254
|
-
maxLength={maxLength}
|
|
255
|
-
keyboardType={keyboardType}
|
|
256
|
-
autoCapitalize={autoCapitalize}
|
|
257
|
-
autoCorrect={autoCorrect}
|
|
258
|
-
editable={!isDisabled}
|
|
259
|
-
style={textInputStyle}
|
|
260
|
-
onBlur={() => {
|
|
261
|
-
setIsFocused(false);
|
|
262
|
-
onBlur?.();
|
|
263
|
-
}}
|
|
264
|
-
onFocus={() => {
|
|
265
|
-
setIsFocused(true);
|
|
266
|
-
onFocus?.();
|
|
267
|
-
}}
|
|
268
|
-
testID={testID ? `${testID}-input` : undefined}
|
|
269
|
-
/>
|
|
270
|
-
|
|
271
|
-
{(showPasswordToggle && secureTextEntry) && (
|
|
272
|
-
<Pressable
|
|
273
|
-
onPress={() => setIsPasswordVisible(!isPasswordVisible)}
|
|
274
|
-
style={styles.trailingIcon}
|
|
275
|
-
>
|
|
276
|
-
<AtomicIcon
|
|
277
|
-
name={isPasswordVisible ? "EyeOff" : "Eye"}
|
|
278
|
-
customSize={iconSize}
|
|
279
|
-
customColor={iconColor}
|
|
280
|
-
/>
|
|
281
|
-
</Pressable>
|
|
282
|
-
)}
|
|
283
|
-
|
|
284
|
-
{trailingIcon && !showPasswordToggle && (
|
|
285
|
-
<Pressable
|
|
286
|
-
onPress={onTrailingIconPress}
|
|
287
|
-
style={styles.trailingIcon}
|
|
288
|
-
disabled={!onTrailingIconPress}
|
|
289
|
-
>
|
|
290
|
-
<AtomicIcon
|
|
291
|
-
name={trailingIcon}
|
|
292
|
-
customSize={iconSize}
|
|
293
|
-
customColor={iconColor}
|
|
294
|
-
/>
|
|
295
|
-
</Pressable>
|
|
296
|
-
)}
|
|
297
|
-
</View>
|
|
298
|
-
|
|
299
|
-
{(helperText || showCharacterCount) && (
|
|
300
|
-
<View style={styles.helperRow}>
|
|
301
|
-
{helperText && (
|
|
302
|
-
<AtomicText
|
|
303
|
-
type="bodySmall"
|
|
304
|
-
color={hasError ? 'error' : 'secondary'}
|
|
305
|
-
style={styles.helperText}
|
|
306
|
-
testID={testID ? `${testID}-helper` : undefined}
|
|
307
|
-
>
|
|
308
|
-
{helperText}
|
|
309
|
-
</AtomicText>
|
|
310
|
-
)}
|
|
311
|
-
{showCharacterCount && maxLength && (
|
|
312
|
-
<AtomicText
|
|
313
|
-
type="bodySmall"
|
|
314
|
-
color="secondary"
|
|
315
|
-
style={styles.characterCount}
|
|
316
|
-
testID={testID ? `${testID}-count` : undefined}
|
|
317
|
-
>
|
|
318
|
-
{characterCount}/{maxLength}
|
|
319
|
-
</AtomicText>
|
|
320
|
-
)}
|
|
321
|
-
</View>
|
|
322
|
-
)}
|
|
323
|
-
</View>
|
|
324
|
-
);
|
|
325
|
-
};
|
|
326
|
-
|
|
327
|
-
const styles = StyleSheet.create({
|
|
328
|
-
container: {
|
|
329
|
-
flexDirection: 'row',
|
|
330
|
-
alignItems: 'center',
|
|
331
|
-
},
|
|
332
|
-
input: {
|
|
333
|
-
flex: 1,
|
|
334
|
-
margin: 0,
|
|
335
|
-
padding: 0,
|
|
336
|
-
},
|
|
337
|
-
label: {
|
|
338
|
-
marginBottom: 4,
|
|
339
|
-
},
|
|
340
|
-
leadingIcon: {
|
|
341
|
-
position: 'absolute',
|
|
342
|
-
left: 12,
|
|
343
|
-
zIndex: 1,
|
|
344
|
-
},
|
|
345
|
-
trailingIcon: {
|
|
346
|
-
position: 'absolute',
|
|
347
|
-
right: 12,
|
|
348
|
-
zIndex: 1,
|
|
349
|
-
},
|
|
350
|
-
helperRow: {
|
|
351
|
-
flexDirection: 'row',
|
|
352
|
-
justifyContent: 'space-between',
|
|
353
|
-
marginTop: 4,
|
|
354
|
-
},
|
|
355
|
-
helperText: {
|
|
356
|
-
flex: 1,
|
|
357
|
-
},
|
|
358
|
-
characterCount: {
|
|
359
|
-
marginLeft: 8,
|
|
360
|
-
},
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
export type { AtomicInputProps as InputProps };
|
|
@@ -1,182 +0,0 @@
|
|
|
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
|
-
};
|