@umituz/react-native-design-system 1.5.17 → 1.5.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/package.json +9 -8
  2. package/src/index.js +100 -0
  3. package/src/presentation/atoms/AtomicAvatar.js +84 -0
  4. package/src/presentation/atoms/AtomicAvatarGroup.js +82 -0
  5. package/src/presentation/atoms/AtomicBadge.js +167 -0
  6. package/src/presentation/atoms/AtomicButton.js +171 -0
  7. package/src/presentation/atoms/AtomicCard.js +69 -0
  8. package/src/presentation/atoms/AtomicChip.js +130 -0
  9. package/src/presentation/atoms/AtomicDatePicker.js +245 -0
  10. package/src/presentation/atoms/AtomicDivider.js +57 -0
  11. package/src/presentation/atoms/AtomicFab.js +67 -0
  12. package/src/presentation/atoms/AtomicFilter.js +103 -0
  13. package/src/presentation/atoms/AtomicFormError.js +63 -0
  14. package/src/presentation/atoms/AtomicIcon.js +29 -0
  15. package/src/presentation/atoms/AtomicImage.js +91 -0
  16. package/src/presentation/atoms/AtomicInput.js +201 -0
  17. package/src/presentation/atoms/AtomicNumberInput.js +124 -0
  18. package/src/presentation/atoms/AtomicPicker.js +298 -0
  19. package/src/presentation/atoms/AtomicProgress.js +79 -0
  20. package/src/presentation/atoms/AtomicSearchBar.js +45 -0
  21. package/src/presentation/atoms/AtomicSort.js +76 -0
  22. package/src/presentation/atoms/AtomicSwitch.js +103 -0
  23. package/src/presentation/atoms/AtomicText.js +58 -0
  24. package/src/presentation/atoms/AtomicTextArea.js +195 -0
  25. package/src/presentation/atoms/AtomicTouchable.js +137 -0
  26. package/src/presentation/atoms/fab/styles/fabStyles.js +62 -0
  27. package/src/presentation/atoms/fab/types/index.js +1 -0
  28. package/src/presentation/atoms/filter/styles/filterStyles.js +28 -0
  29. package/src/presentation/atoms/filter/types/index.js +1 -0
  30. package/src/presentation/atoms/index.js +145 -0
  31. package/src/presentation/atoms/input/hooks/useInputState.js +12 -0
  32. package/src/presentation/atoms/input/styles/inputStyles.js +58 -0
  33. package/src/presentation/atoms/input/types/index.js +1 -0
  34. package/src/presentation/atoms/picker/styles/pickerStyles.js +176 -0
  35. package/src/presentation/atoms/picker/types/index.js +1 -0
  36. package/src/presentation/atoms/touchable/styles/touchableStyles.js +53 -0
  37. package/src/presentation/atoms/touchable/types/index.js +1 -0
  38. package/src/presentation/hooks/useResponsive.js +81 -0
  39. package/src/presentation/molecules/AtomicConfirmationModal.js +153 -0
  40. package/src/presentation/molecules/EmptyState.js +67 -0
  41. package/src/presentation/molecules/FormField.js +75 -0
  42. package/src/presentation/molecules/GridContainer.js +76 -0
  43. package/src/presentation/molecules/IconContainer.js +59 -0
  44. package/src/presentation/molecules/ListItem.js +23 -0
  45. package/src/presentation/molecules/ScreenHeader.js +93 -0
  46. package/src/presentation/molecules/SearchBar.js +46 -0
  47. package/src/presentation/molecules/SectionCard.js +46 -0
  48. package/src/presentation/molecules/SectionContainer.js +63 -0
  49. package/src/presentation/molecules/SectionHeader.js +72 -0
  50. package/src/presentation/molecules/confirmation-modal/styles/confirmationModalStyles.js +114 -0
  51. package/src/presentation/molecules/confirmation-modal/types/index.js +6 -0
  52. package/src/presentation/molecules/index.js +16 -0
  53. package/src/presentation/molecules/listitem/styles/listItemStyles.js +14 -0
  54. package/src/presentation/molecules/listitem/types/index.js +1 -0
  55. package/src/presentation/organisms/AppHeader.js +77 -0
  56. package/src/presentation/organisms/FormContainer.js +126 -0
  57. package/src/presentation/organisms/ScreenLayout.js +68 -0
  58. package/src/presentation/organisms/index.js +13 -0
  59. package/src/presentation/tokens/commonStyles.js +219 -0
  60. package/src/presentation/utils/platformConstants.js +113 -0
  61. package/src/presentation/utils/responsive.js +451 -0
  62. package/src/presentation/utils/variants/compound.js +15 -0
  63. package/src/presentation/utils/variants/core.js +22 -0
  64. package/src/presentation/utils/variants/helpers.js +9 -0
  65. package/src/presentation/utils/variants.js +3 -0
@@ -0,0 +1,63 @@
1
+ /**
2
+ * AtomicFormError - Universal Form Error Component
3
+ *
4
+ * Provides consistent error message display for forms
5
+ * Theme: {{THEME_NAME}} ({{CATEGORY}} category)
6
+ *
7
+ * Atomic Design Level: ATOM
8
+ * Purpose: Display validation error messages
9
+ *
10
+ * Usage:
11
+ * - Form field validation errors
12
+ * - Global form error messages
13
+ * - API error display
14
+ * - Input validation feedback
15
+ */
16
+ import React from 'react';
17
+ import { View, StyleSheet } from 'react-native';
18
+ import { AtomicText } from './AtomicText';
19
+ import { useAppDesignTokens } from '@umituz/react-native-theme';
20
+ import { withAlpha } from '@umituz/react-native-theme';
21
+ // =============================================================================
22
+ // COMPONENT IMPLEMENTATION
23
+ // =============================================================================
24
+ export const AtomicFormError = ({ message, variant = 'field', style, textStyle, }) => {
25
+ const tokens = useAppDesignTokens();
26
+ if (!message) {
27
+ return null;
28
+ }
29
+ if (variant === 'global') {
30
+ return (<View style={[
31
+ {
32
+ padding: tokens.spacing.md,
33
+ borderRadius: tokens.borders.radius.md,
34
+ marginBottom: tokens.spacing.sm,
35
+ backgroundColor: withAlpha(tokens.colors.error, 0.15),
36
+ },
37
+ style,
38
+ ]}>
39
+ <AtomicText type="bodySmall" color="error" style={StyleSheet.flatten([
40
+ {
41
+ textAlign: 'center',
42
+ fontWeight: tokens.typography.medium,
43
+ },
44
+ textStyle,
45
+ ])}>
46
+ {message}
47
+ </AtomicText>
48
+ </View>);
49
+ }
50
+ return (<AtomicText type="bodySmall" color="error" style={StyleSheet.flatten([
51
+ {
52
+ marginTop: tokens.spacing.xs,
53
+ marginLeft: tokens.spacing.xs,
54
+ },
55
+ textStyle,
56
+ ])}>
57
+ {message}
58
+ </AtomicText>);
59
+ };
60
+ // =============================================================================
61
+ // EXPORTS
62
+ // =============================================================================
63
+ export default AtomicFormError;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * AtomicIcon - Atomic Design System Icon Component
3
+ *
4
+ * Wrapper for the universal Icon component from @domains/icons
5
+ * Provides backward compatibility with AtomicIcon naming convention
6
+ * while leveraging the full power of the icons domain architecture.
7
+ */
8
+ import React from 'react';
9
+ import { Icon } from '@umituz/react-native-icon';
10
+ /**
11
+ * AtomicIcon Component
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * import { AtomicIcon } from '@umituz/react-native-design-system';
16
+ *
17
+ * // Basic usage
18
+ * <AtomicIcon name="Settings" size="md" color="primary" />
19
+ *
20
+ * // Custom size and color
21
+ * <AtomicIcon name="Heart" customSize={32} customColor="#FF0000" />
22
+ *
23
+ * // With background
24
+ * <AtomicIcon name="Info" size="lg" withBackground backgroundColor="#667eea" />
25
+ * ```
26
+ */
27
+ export const AtomicIcon = (props) => {
28
+ return <Icon {...props}/>;
29
+ };
@@ -0,0 +1,91 @@
1
+ /**
2
+ * AtomicImage - Universal Image Component
3
+ *
4
+ * Provides consistent image handling across the app with theme integration
5
+ * Theme: {{THEME_NAME}} ({{CATEGORY}} category)
6
+ *
7
+ * Atomic Design Level: ATOM
8
+ * Purpose: Basic image display with consistent styling
9
+ *
10
+ * Usage:
11
+ * - Profile pictures
12
+ * - Product images
13
+ * - Icons and illustrations
14
+ * - Background images
15
+ */
16
+ import React from 'react';
17
+ import { Image, StyleSheet } from 'react-native';
18
+ import { useAppDesignTokens } from '@umituz/react-native-theme';
19
+ // =============================================================================
20
+ // SIZE CONFIGURATION
21
+ // =============================================================================
22
+ const SIZE_CONFIG = {
23
+ xs: 24,
24
+ sm: 32,
25
+ md: 48,
26
+ lg: 64,
27
+ xl: 96,
28
+ xxl: 128,
29
+ };
30
+ // =============================================================================
31
+ // COMPONENT IMPLEMENTATION
32
+ // =============================================================================
33
+ export const AtomicImage = ({ source, size = 'md', shape = 'rounded', borderRadius, style, imageStyle, backgroundColor, borderColor, borderWidth = 0, ...props }) => {
34
+ const tokens = useAppDesignTokens();
35
+ const styles = getStyles(tokens);
36
+ const imageSize = SIZE_CONFIG[size];
37
+ const calculatedBorderRadius = borderRadius ?? getBorderRadius(shape, imageSize, tokens);
38
+ const containerStyle = [
39
+ styles.container,
40
+ {
41
+ width: imageSize,
42
+ height: imageSize,
43
+ borderRadius: calculatedBorderRadius,
44
+ backgroundColor: backgroundColor || tokens.colors.surface,
45
+ borderColor: borderColor || tokens.colors.border,
46
+ borderWidth,
47
+ },
48
+ style,
49
+ ];
50
+ const finalImageStyle = [
51
+ styles.image,
52
+ {
53
+ borderRadius: calculatedBorderRadius,
54
+ },
55
+ imageStyle,
56
+ ];
57
+ return (<Image source={source} style={finalImageStyle} {...props}/>);
58
+ };
59
+ // =============================================================================
60
+ // HELPER FUNCTIONS
61
+ // =============================================================================
62
+ const getBorderRadius = (shape, size, tokens) => {
63
+ switch (shape) {
64
+ case 'circle':
65
+ return size / 2;
66
+ case 'square':
67
+ return 0;
68
+ case 'rounded':
69
+ default:
70
+ return tokens.borders.radius.md;
71
+ }
72
+ };
73
+ // =============================================================================
74
+ // STYLES
75
+ // =============================================================================
76
+ const getStyles = (tokens) => StyleSheet.create({
77
+ container: {
78
+ overflow: 'hidden',
79
+ justifyContent: 'center',
80
+ alignItems: 'center',
81
+ },
82
+ image: {
83
+ width: '100%',
84
+ height: '100%',
85
+ resizeMode: 'cover',
86
+ },
87
+ });
88
+ // =============================================================================
89
+ // EXPORTS
90
+ // =============================================================================
91
+ export default AtomicImage;
@@ -0,0 +1,201 @@
1
+ import React, { useState } from 'react';
2
+ import { View, TextInput, Pressable, StyleSheet, Platform } from 'react-native';
3
+ import { useAppDesignTokens } from '@umituz/react-native-theme';
4
+ import { AtomicIcon } from './AtomicIcon';
5
+ import { AtomicText } from './AtomicText';
6
+ /**
7
+ * AtomicInput - Pure React Native Text Input
8
+ *
9
+ * Features:
10
+ * - Pure React Native implementation (no Paper dependency)
11
+ * - Lucide icons for password toggle and custom icons
12
+ * - Outlined/filled/flat variants
13
+ * - Error, success, disabled states
14
+ * - Character counter
15
+ * - Responsive sizing
16
+ * - Full accessibility support
17
+ */
18
+ export const AtomicInput = ({ variant = 'outlined', state = 'default', size = 'md', label, value = '', onChangeText, placeholder, helperText, leadingIcon, trailingIcon, onTrailingIconPress, showPasswordToggle = false, secureTextEntry = false, maxLength, showCharacterCount = false, keyboardType = 'default', autoCapitalize = 'sentences', autoCorrect = true, disabled = false, style, inputStyle, testID, onBlur, onFocus, }) => {
19
+ const tokens = useAppDesignTokens();
20
+ const [isPasswordVisible, setIsPasswordVisible] = useState(false);
21
+ const [isFocused, setIsFocused] = useState(false);
22
+ const isDisabled = state === 'disabled' || disabled;
23
+ const characterCount = value?.toString().length || 0;
24
+ const hasError = state === 'error';
25
+ const hasSuccess = state === 'success';
26
+ // Size configuration
27
+ const sizeConfig = {
28
+ sm: {
29
+ paddingVertical: tokens.spacing.xs,
30
+ paddingHorizontal: tokens.spacing.sm,
31
+ fontSize: tokens.typography.bodySmall.fontSize,
32
+ iconSize: 16,
33
+ minHeight: 40,
34
+ },
35
+ md: {
36
+ paddingVertical: tokens.spacing.sm,
37
+ paddingHorizontal: tokens.spacing.md,
38
+ fontSize: tokens.typography.bodyMedium.fontSize,
39
+ iconSize: 20,
40
+ minHeight: 48,
41
+ },
42
+ lg: {
43
+ paddingVertical: tokens.spacing.md,
44
+ paddingHorizontal: tokens.spacing.lg,
45
+ fontSize: tokens.typography.bodyLarge.fontSize,
46
+ iconSize: 24,
47
+ minHeight: 56,
48
+ },
49
+ };
50
+ const config = sizeConfig[size];
51
+ // Get variant styles
52
+ const getVariantStyle = () => {
53
+ const baseStyle = {
54
+ backgroundColor: tokens.colors.surface,
55
+ borderRadius: tokens.borders.radius.md,
56
+ };
57
+ let borderColor = tokens.colors.border;
58
+ if (isFocused)
59
+ borderColor = tokens.colors.primary;
60
+ if (hasError)
61
+ borderColor = tokens.colors.error;
62
+ if (hasSuccess)
63
+ borderColor = tokens.colors.success;
64
+ if (isDisabled)
65
+ borderColor = tokens.colors.borderDisabled;
66
+ switch (variant) {
67
+ case 'outlined':
68
+ return {
69
+ ...baseStyle,
70
+ borderWidth: isFocused ? 2 : 1,
71
+ borderColor,
72
+ };
73
+ case 'filled':
74
+ return {
75
+ ...baseStyle,
76
+ backgroundColor: tokens.colors.surfaceSecondary,
77
+ borderWidth: 0,
78
+ borderBottomWidth: isFocused ? 2 : 1,
79
+ borderBottomColor: borderColor,
80
+ };
81
+ case 'flat':
82
+ return {
83
+ ...baseStyle,
84
+ backgroundColor: 'transparent',
85
+ borderWidth: 0,
86
+ borderBottomWidth: 1,
87
+ borderBottomColor: borderColor,
88
+ borderRadius: 0,
89
+ };
90
+ default:
91
+ return baseStyle;
92
+ }
93
+ };
94
+ // Get text color based on state
95
+ const getTextColor = () => {
96
+ if (isDisabled)
97
+ return tokens.colors.textDisabled;
98
+ if (hasError)
99
+ return tokens.colors.error;
100
+ if (hasSuccess)
101
+ return tokens.colors.success;
102
+ return tokens.colors.textPrimary;
103
+ };
104
+ const iconColor = isDisabled ? tokens.colors.textDisabled : tokens.colors.textSecondary;
105
+ const containerStyle = [
106
+ styles.container,
107
+ getVariantStyle(),
108
+ {
109
+ paddingTop: config.paddingVertical,
110
+ paddingBottom: config.paddingVertical,
111
+ paddingHorizontal: config.paddingHorizontal,
112
+ minHeight: config.minHeight,
113
+ justifyContent: 'center',
114
+ opacity: isDisabled ? 0.5 : 1,
115
+ },
116
+ style,
117
+ ];
118
+ const textInputStyle = [
119
+ styles.input,
120
+ {
121
+ fontSize: config.fontSize || tokens.typography.bodyMedium.fontSize || 16,
122
+ lineHeight: (config.fontSize || tokens.typography.bodyMedium.fontSize || 16) * 1.5, // Ensure text is fully visible
123
+ color: getTextColor(),
124
+ paddingVertical: 0, // Remove vertical padding to prevent clipping
125
+ },
126
+ leadingIcon ? { paddingLeft: config.iconSize + 8 } : undefined,
127
+ (trailingIcon || showPasswordToggle) ? { paddingRight: config.iconSize + 8 } : undefined,
128
+ inputStyle,
129
+ ];
130
+ return (<View testID={testID}>
131
+ {label && (<AtomicText type="labelMedium" color={hasError ? 'error' : hasSuccess ? 'success' : 'secondary'} style={styles.label}>
132
+ {label}
133
+ </AtomicText>)}
134
+
135
+ <View style={containerStyle}>
136
+ {leadingIcon && (<View style={styles.leadingIcon}>
137
+ <AtomicIcon name={leadingIcon} customSize={config.iconSize} customColor={iconColor}/>
138
+ </View>)}
139
+
140
+ <TextInput value={value} onChangeText={onChangeText} placeholder={placeholder} placeholderTextColor={tokens.colors.textSecondary} secureTextEntry={secureTextEntry && !isPasswordVisible} maxLength={maxLength} keyboardType={keyboardType} autoCapitalize={autoCapitalize} autoCorrect={autoCorrect} editable={!isDisabled} style={textInputStyle} textAlignVertical="center" {...(Platform.OS === 'android' && { includeFontPadding: false })} onBlur={() => {
141
+ setIsFocused(false);
142
+ onBlur?.();
143
+ }} onFocus={() => {
144
+ setIsFocused(true);
145
+ onFocus?.();
146
+ }} testID={testID ? `${testID}-input` : undefined}/>
147
+
148
+ {(showPasswordToggle && secureTextEntry) && (<Pressable onPress={() => setIsPasswordVisible(!isPasswordVisible)} style={styles.trailingIcon}>
149
+ <AtomicIcon name={isPasswordVisible ? "EyeOff" : "Eye"} customSize={config.iconSize} customColor={iconColor}/>
150
+ </Pressable>)}
151
+
152
+ {trailingIcon && !showPasswordToggle && (<Pressable onPress={onTrailingIconPress} style={styles.trailingIcon} disabled={!onTrailingIconPress}>
153
+ <AtomicIcon name={trailingIcon} customSize={config.iconSize} customColor={iconColor}/>
154
+ </Pressable>)}
155
+ </View>
156
+
157
+ {(helperText || showCharacterCount) && (<View style={styles.helperRow}>
158
+ {helperText && (<AtomicText type="bodySmall" color={hasError ? 'error' : 'secondary'} style={styles.helperText} testID={testID ? `${testID}-helper` : undefined}>
159
+ {helperText}
160
+ </AtomicText>)}
161
+ {showCharacterCount && maxLength && (<AtomicText type="bodySmall" color="secondary" style={styles.characterCount} testID={testID ? `${testID}-count` : undefined}>
162
+ {characterCount}/{maxLength}
163
+ </AtomicText>)}
164
+ </View>)}
165
+ </View>);
166
+ };
167
+ const styles = StyleSheet.create({
168
+ container: {
169
+ flexDirection: 'row',
170
+ alignItems: 'center',
171
+ },
172
+ input: {
173
+ flex: 1,
174
+ margin: 0,
175
+ padding: 0,
176
+ },
177
+ label: {
178
+ marginBottom: 4,
179
+ },
180
+ leadingIcon: {
181
+ position: 'absolute',
182
+ left: 12,
183
+ zIndex: 1,
184
+ },
185
+ trailingIcon: {
186
+ position: 'absolute',
187
+ right: 12,
188
+ zIndex: 1,
189
+ },
190
+ helperRow: {
191
+ flexDirection: 'row',
192
+ justifyContent: 'space-between',
193
+ marginTop: 4,
194
+ },
195
+ helperText: {
196
+ flex: 1,
197
+ },
198
+ characterCount: {
199
+ marginLeft: 8,
200
+ },
201
+ });
@@ -0,0 +1,124 @@
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
+ import React, { useState, useEffect } from 'react';
40
+ import { AtomicInput } from './AtomicInput';
41
+ /**
42
+ * AtomicNumberInput - Specialized numeric input component
43
+ *
44
+ * Wraps AtomicInput with:
45
+ * - Numeric keyboard
46
+ * - Number validation (min, max, format)
47
+ * - Parsed number callbacks
48
+ * - Automatic error states
49
+ */
50
+ export const AtomicNumberInput = ({ min, max, step = 1, allowDecimal = false, onValueChange, onTextChange, value = '', state: externalState, helperText: externalHelperText, ...props }) => {
51
+ const [internalError, setInternalError] = useState(undefined);
52
+ /**
53
+ * Validate number and return error message if invalid
54
+ */
55
+ const validateNumber = (text) => {
56
+ // Empty is valid (null value)
57
+ if (!text || text === '' || text === '-' || text === '.') {
58
+ return undefined;
59
+ }
60
+ // Parse number
61
+ const num = parseFloat(text);
62
+ // Check if valid number
63
+ if (isNaN(num)) {
64
+ return 'Invalid number';
65
+ }
66
+ // Check min constraint
67
+ if (min !== undefined && num < min) {
68
+ return `Minimum value is ${min}`;
69
+ }
70
+ // Check max constraint
71
+ if (max !== undefined && num > max) {
72
+ return `Maximum value is ${max}`;
73
+ }
74
+ return undefined;
75
+ };
76
+ /**
77
+ * Handle text change with validation
78
+ */
79
+ const handleChangeText = (text) => {
80
+ // Allow empty, minus sign, and decimal point during typing
81
+ if (text === '' || text === '-' || (allowDecimal && text === '.')) {
82
+ setInternalError(undefined);
83
+ onTextChange?.(text);
84
+ onValueChange?.(null);
85
+ return;
86
+ }
87
+ // Validate format
88
+ const decimalRegex = allowDecimal ? /^-?\d*\.?\d*$/ : /^-?\d*$/;
89
+ if (!decimalRegex.test(text)) {
90
+ // Invalid format, don't update
91
+ return;
92
+ }
93
+ // Validate number
94
+ const error = validateNumber(text);
95
+ setInternalError(error);
96
+ // Call text callback
97
+ onTextChange?.(text);
98
+ // Call value callback with parsed number
99
+ if (!error && text !== '' && text !== '-' && text !== '.') {
100
+ const num = parseFloat(text);
101
+ onValueChange?.(isNaN(num) ? null : num);
102
+ }
103
+ else {
104
+ onValueChange?.(null);
105
+ }
106
+ };
107
+ /**
108
+ * Validate on mount and when value/constraints change
109
+ */
110
+ useEffect(() => {
111
+ if (value) {
112
+ const error = validateNumber(value.toString());
113
+ setInternalError(error);
114
+ }
115
+ else {
116
+ setInternalError(undefined);
117
+ }
118
+ }, [value, min, max]);
119
+ // Determine final state (external state overrides internal error)
120
+ const finalState = externalState || (internalError ? 'error' : 'default');
121
+ // Determine final helper text (internal error overrides external helper)
122
+ const finalHelperText = internalError || externalHelperText;
123
+ return (<AtomicInput {...props} value={value} onChangeText={handleChangeText} keyboardType={allowDecimal ? 'decimal-pad' : 'numeric'} state={finalState} helperText={finalHelperText}/>);
124
+ };