@umituz/react-native-design-system 1.5.33 → 1.5.35
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/package.json +4 -1
- package/src/presentation/atoms/AtomicDatePicker.tsx +7 -3
- package/src/presentation/atoms/AtomicInput.tsx +19 -12
- package/src/index.js +0 -100
- package/src/presentation/atoms/AtomicAvatar.js +0 -84
- package/src/presentation/atoms/AtomicAvatarGroup.js +0 -82
- package/src/presentation/atoms/AtomicBadge.js +0 -167
- package/src/presentation/atoms/AtomicButton.js +0 -171
- package/src/presentation/atoms/AtomicCard.js +0 -69
- package/src/presentation/atoms/AtomicChip.js +0 -130
- package/src/presentation/atoms/AtomicDatePicker.js +0 -245
- package/src/presentation/atoms/AtomicDivider.js +0 -57
- package/src/presentation/atoms/AtomicFab.js +0 -67
- package/src/presentation/atoms/AtomicFilter.js +0 -103
- package/src/presentation/atoms/AtomicFormError.js +0 -63
- package/src/presentation/atoms/AtomicIcon.js +0 -29
- package/src/presentation/atoms/AtomicImage.js +0 -91
- package/src/presentation/atoms/AtomicInput.js +0 -201
- package/src/presentation/atoms/AtomicNumberInput.js +0 -124
- package/src/presentation/atoms/AtomicPicker.js +0 -298
- package/src/presentation/atoms/AtomicProgress.js +0 -79
- package/src/presentation/atoms/AtomicSearchBar.js +0 -45
- package/src/presentation/atoms/AtomicSort.js +0 -76
- package/src/presentation/atoms/AtomicSwitch.js +0 -103
- package/src/presentation/atoms/AtomicText.js +0 -22
- package/src/presentation/atoms/AtomicTextArea.js +0 -195
- package/src/presentation/atoms/AtomicTouchable.js +0 -137
- package/src/presentation/atoms/fab/styles/fabStyles.js +0 -62
- package/src/presentation/atoms/fab/types/index.js +0 -1
- package/src/presentation/atoms/filter/styles/filterStyles.js +0 -28
- package/src/presentation/atoms/filter/types/index.js +0 -1
- package/src/presentation/atoms/index.js +0 -145
- package/src/presentation/atoms/input/hooks/useInputState.js +0 -12
- package/src/presentation/atoms/input/styles/inputStyles.js +0 -58
- package/src/presentation/atoms/input/types/index.js +0 -1
- package/src/presentation/atoms/picker/styles/pickerStyles.js +0 -176
- package/src/presentation/atoms/picker/types/index.js +0 -1
- package/src/presentation/atoms/touchable/styles/touchableStyles.js +0 -53
- package/src/presentation/atoms/touchable/types/index.js +0 -1
- package/src/presentation/hooks/useResponsive.js +0 -81
- package/src/presentation/molecules/AtomicConfirmationModal.js +0 -153
- package/src/presentation/molecules/EmptyState.js +0 -67
- package/src/presentation/molecules/FormField.js +0 -75
- package/src/presentation/molecules/GridContainer.js +0 -76
- package/src/presentation/molecules/IconContainer.js +0 -59
- package/src/presentation/molecules/ListItem.js +0 -23
- package/src/presentation/molecules/ScreenHeader.js +0 -93
- package/src/presentation/molecules/SearchBar.js +0 -46
- package/src/presentation/molecules/SectionCard.js +0 -46
- package/src/presentation/molecules/SectionContainer.js +0 -63
- package/src/presentation/molecules/SectionHeader.js +0 -72
- package/src/presentation/molecules/confirmation-modal/styles/confirmationModalStyles.js +0 -114
- package/src/presentation/molecules/confirmation-modal/types/index.js +0 -6
- package/src/presentation/molecules/index.js +0 -16
- package/src/presentation/molecules/listitem/styles/listItemStyles.js +0 -14
- package/src/presentation/molecules/listitem/types/index.js +0 -1
- package/src/presentation/organisms/AppHeader.js +0 -77
- package/src/presentation/organisms/FormContainer.js +0 -126
- package/src/presentation/organisms/ScreenLayout.js +0 -68
- package/src/presentation/organisms/index.js +0 -13
- package/src/presentation/tokens/commonStyles.js +0 -219
- package/src/presentation/utils/platformConstants.js +0 -113
- package/src/presentation/utils/responsive.js +0 -451
- package/src/presentation/utils/variants/compound.js +0 -15
- package/src/presentation/utils/variants/core.js +0 -22
- package/src/presentation/utils/variants/helpers.js +0 -9
- package/src/presentation/utils/variants.js +0 -3
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { TouchableOpacity, StyleSheet } from 'react-native';
|
|
3
|
-
import { useAppDesignTokens } from '@umituz/react-native-theme';
|
|
4
|
-
import { useResponsive } from '../hooks/useResponsive';
|
|
5
|
-
import { AtomicIcon } from './AtomicIcon';
|
|
6
|
-
import { FAB_SIZES, getFabVariants, getFabIconSize, getFabBorder, } from './fab/styles/fabStyles';
|
|
7
|
-
export { FAB_SIZES, getFabVariants, getFabIconSize, getFabBorder };
|
|
8
|
-
/**
|
|
9
|
-
* AtomicFab - Floating Action Button Component
|
|
10
|
-
*
|
|
11
|
-
* A Material Design 3 compliant FAB component for primary actions.
|
|
12
|
-
* Follows CLAUDE.md standards for responsive positioning.
|
|
13
|
-
*
|
|
14
|
-
* @example
|
|
15
|
-
* ```tsx
|
|
16
|
-
* // IMPORTANT: FAB must be used at screen level, NOT inside ScrollView
|
|
17
|
-
* <ScreenLayout>
|
|
18
|
-
* <ScrollView>
|
|
19
|
-
* {/* Your content *\/}
|
|
20
|
-
* </ScrollView>
|
|
21
|
-
* <AtomicFab
|
|
22
|
-
* icon="add"
|
|
23
|
-
* onPress={handleAddItem}
|
|
24
|
-
* variant="primary"
|
|
25
|
-
* size="md"
|
|
26
|
-
* />
|
|
27
|
-
* </ScreenLayout>
|
|
28
|
-
* ```
|
|
29
|
-
*
|
|
30
|
-
* Features:
|
|
31
|
-
* - Material Design 3 sizes (sm: 40px, md: 56px, lg: 72px)
|
|
32
|
-
* - Three variants: primary, secondary, surface
|
|
33
|
-
* - Responsive positioning (above tab bar, safe area aware)
|
|
34
|
-
* - Disabled state with opacity
|
|
35
|
-
* - Theme-aware colors from design tokens
|
|
36
|
-
* - Border for depth (no shadows per CLAUDE.md)
|
|
37
|
-
*/
|
|
38
|
-
export const AtomicFab = ({ icon, onPress, variant = 'primary', size = 'md', disabled = false, style, testID, accessibilityLabel, }) => {
|
|
39
|
-
const tokens = useAppDesignTokens();
|
|
40
|
-
const responsive = useResponsive();
|
|
41
|
-
const isDisabled = disabled;
|
|
42
|
-
// Get configurations
|
|
43
|
-
const sizeConfig = FAB_SIZES[size];
|
|
44
|
-
const variants = getFabVariants(tokens);
|
|
45
|
-
const variantConfig = variants[variant];
|
|
46
|
-
const iconSize = getFabIconSize(size);
|
|
47
|
-
// Combine styles
|
|
48
|
-
const fabStyle = StyleSheet.flatten([
|
|
49
|
-
{
|
|
50
|
-
position: 'absolute',
|
|
51
|
-
bottom: responsive.fabPosition.bottom,
|
|
52
|
-
right: responsive.fabPosition.right,
|
|
53
|
-
width: sizeConfig.width,
|
|
54
|
-
height: sizeConfig.height,
|
|
55
|
-
borderRadius: sizeConfig.borderRadius,
|
|
56
|
-
backgroundColor: variantConfig.backgroundColor,
|
|
57
|
-
alignItems: 'center',
|
|
58
|
-
justifyContent: 'center',
|
|
59
|
-
},
|
|
60
|
-
getFabBorder(tokens),
|
|
61
|
-
isDisabled ? { opacity: tokens.opacity.disabled } : undefined,
|
|
62
|
-
style, // Custom style override
|
|
63
|
-
]);
|
|
64
|
-
return (<TouchableOpacity style={fabStyle} onPress={onPress} disabled={isDisabled} activeOpacity={0.7} testID={testID} accessibilityLabel={accessibilityLabel || `${icon} button`} accessibilityRole="button">
|
|
65
|
-
<AtomicIcon name={icon} size={iconSize} customColor={variantConfig.iconColor}/>
|
|
66
|
-
</TouchableOpacity>);
|
|
67
|
-
};
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { ScrollView, View, TouchableOpacity } from 'react-native';
|
|
3
|
-
import { useAppDesignTokens } from '@umituz/react-native-theme';
|
|
4
|
-
import { AtomicChip } from './AtomicChip';
|
|
5
|
-
import { AtomicText } from './AtomicText';
|
|
6
|
-
import { AtomicIcon } from './AtomicIcon';
|
|
7
|
-
import { getFilterContainerStyle, getClearAllContainerStyle, getScrollContentContainerStyle, } from './filter/styles/filterStyles';
|
|
8
|
-
export { getFilterContainerStyle, getClearAllContainerStyle, getScrollContentContainerStyle, } from './filter/styles/filterStyles';
|
|
9
|
-
/**
|
|
10
|
-
* AtomicFilter - Horizontal Filter Chip Component
|
|
11
|
-
*
|
|
12
|
-
* A Material Design 3 compliant filter component using chip selection.
|
|
13
|
-
* Supports single and multi-select modes with "Clear All" functionality.
|
|
14
|
-
*
|
|
15
|
-
* @example
|
|
16
|
-
* ```tsx
|
|
17
|
-
* const [selectedFilters, setSelectedFilters] = useState<string[]>([]);
|
|
18
|
-
*
|
|
19
|
-
* <AtomicFilter
|
|
20
|
-
* options={[
|
|
21
|
-
* { id: 'active', label: 'Active', icon: 'check-circle' },
|
|
22
|
-
* { id: 'completed', label: 'Completed', icon: 'check' },
|
|
23
|
-
* { id: 'pending', label: 'Pending', icon: 'clock' },
|
|
24
|
-
* ]}
|
|
25
|
-
* selectedIds={selectedFilters}
|
|
26
|
-
* onSelectionChange={setSelectedFilters}
|
|
27
|
-
* multiSelect={true}
|
|
28
|
-
* showClearAll={true}
|
|
29
|
-
* />
|
|
30
|
-
* ```
|
|
31
|
-
*
|
|
32
|
-
* Features:
|
|
33
|
-
* - Horizontal scrollable filter chips
|
|
34
|
-
* - Single/Multi-select modes
|
|
35
|
-
* - Clear all button (when filters active)
|
|
36
|
-
* - Theme-aware colors from design tokens
|
|
37
|
-
* - Icon support per filter option
|
|
38
|
-
* - Fully controlled component
|
|
39
|
-
*/
|
|
40
|
-
export const AtomicFilter = ({ options, selectedIds, onSelectionChange, multiSelect = true, showClearAll = true, variant = 'outlined', color = 'primary', size = 'md', style, testID, }) => {
|
|
41
|
-
const tokens = useAppDesignTokens();
|
|
42
|
-
/**
|
|
43
|
-
* Handle filter chip press
|
|
44
|
-
*/
|
|
45
|
-
const handleFilterPress = (optionId) => {
|
|
46
|
-
if (multiSelect) {
|
|
47
|
-
// Multi-select mode: Toggle selection
|
|
48
|
-
if (selectedIds.includes(optionId)) {
|
|
49
|
-
// Deselect
|
|
50
|
-
onSelectionChange(selectedIds.filter(id => id !== optionId));
|
|
51
|
-
}
|
|
52
|
-
else {
|
|
53
|
-
// Select
|
|
54
|
-
onSelectionChange([...selectedIds, optionId]);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
else {
|
|
58
|
-
// Single-select mode: Replace selection
|
|
59
|
-
if (selectedIds.includes(optionId)) {
|
|
60
|
-
// Deselect (clear selection)
|
|
61
|
-
onSelectionChange([]);
|
|
62
|
-
}
|
|
63
|
-
else {
|
|
64
|
-
// Select (only this one)
|
|
65
|
-
onSelectionChange([optionId]);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
/**
|
|
70
|
-
* Handle clear all button press
|
|
71
|
-
*/
|
|
72
|
-
const handleClearAll = () => {
|
|
73
|
-
onSelectionChange([]);
|
|
74
|
-
};
|
|
75
|
-
const hasActiveFilters = selectedIds.length > 0;
|
|
76
|
-
return (<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={getScrollContentContainerStyle()} style={[style]} testID={testID}>
|
|
77
|
-
<View style={getFilterContainerStyle()}>
|
|
78
|
-
{options.map((option) => {
|
|
79
|
-
const isSelected = selectedIds.includes(option.id);
|
|
80
|
-
return (<AtomicChip key={option.id} variant={isSelected ? 'filled' : variant} color={color} size={size} leadingIcon={option.icon} selected={isSelected} clickable={true} onPress={() => handleFilterPress(option.id)} testID={`filter-chip-${option.id}`}>
|
|
81
|
-
{option.label}
|
|
82
|
-
</AtomicChip>);
|
|
83
|
-
})}
|
|
84
|
-
|
|
85
|
-
{/* Clear All Button */}
|
|
86
|
-
{showClearAll && hasActiveFilters && (<TouchableOpacity onPress={handleClearAll} style={[
|
|
87
|
-
getClearAllContainerStyle(),
|
|
88
|
-
{
|
|
89
|
-
backgroundColor: tokens.colors.surfaceVariant,
|
|
90
|
-
borderWidth: 1,
|
|
91
|
-
borderColor: tokens.colors.outline,
|
|
92
|
-
}
|
|
93
|
-
]} testID="clear-all-button">
|
|
94
|
-
<View style={{ flexDirection: 'row', alignItems: 'center', gap: tokens.spacing.xs }}>
|
|
95
|
-
<AtomicIcon name="X" size="xs" color="surfaceVariant"/>
|
|
96
|
-
<AtomicText type="labelSmall" style={{ color: tokens.colors.textSecondary }}>
|
|
97
|
-
Clear All
|
|
98
|
-
</AtomicText>
|
|
99
|
-
</View>
|
|
100
|
-
</TouchableOpacity>)}
|
|
101
|
-
</View>
|
|
102
|
-
</ScrollView>);
|
|
103
|
-
};
|
|
@@ -1,63 +0,0 @@
|
|
|
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;
|
|
@@ -1,29 +0,0 @@
|
|
|
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
|
-
};
|
|
@@ -1,91 +0,0 @@
|
|
|
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;
|
|
@@ -1,201 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,124 +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
|
-
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
|
-
};
|