@umituz/react-native-design-system 1.15.0 → 2.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.
Files changed (87) hide show
  1. package/package.json +26 -19
  2. package/src/atoms/AtomicAvatar.tsx +161 -0
  3. package/src/atoms/AtomicButton.tsx +241 -0
  4. package/src/atoms/AtomicChip.tsx +226 -0
  5. package/src/atoms/AtomicDatePicker.tsx +255 -0
  6. package/src/atoms/AtomicFab.tsx +99 -0
  7. package/src/atoms/AtomicIcon.tsx +149 -0
  8. package/src/atoms/AtomicInput.tsx +308 -0
  9. package/src/atoms/AtomicPicker.tsx +310 -0
  10. package/src/atoms/AtomicProgress.tsx +149 -0
  11. package/src/atoms/AtomicText.tsx +55 -0
  12. package/src/atoms/__tests__/AtomicButton.test.tsx +107 -0
  13. package/src/atoms/__tests__/AtomicIcon.test.tsx +110 -0
  14. package/src/atoms/__tests__/AtomicInput.test.tsx +195 -0
  15. package/src/atoms/datepicker/components/DatePickerButton.tsx +112 -0
  16. package/src/atoms/datepicker/components/DatePickerModal.tsx +143 -0
  17. package/src/atoms/fab/styles/fabStyles.ts +98 -0
  18. package/src/atoms/fab/types/index.ts +88 -0
  19. package/src/atoms/index.ts +70 -0
  20. package/src/atoms/input/hooks/useInputState.ts +63 -0
  21. package/src/atoms/input/styles/inputStylesHelper.ts +120 -0
  22. package/src/atoms/picker/components/PickerChips.tsx +57 -0
  23. package/src/atoms/picker/components/PickerModal.tsx +214 -0
  24. package/src/atoms/picker/styles/pickerStyles.ts +223 -0
  25. package/src/atoms/picker/types/index.ts +42 -0
  26. package/src/index.ts +133 -56
  27. package/src/molecules/ConfirmationModal.tsx +42 -0
  28. package/src/molecules/ConfirmationModalContent.tsx +87 -0
  29. package/src/molecules/ConfirmationModalMain.tsx +91 -0
  30. package/src/molecules/FormField.tsx +155 -0
  31. package/src/molecules/IconContainer.tsx +79 -0
  32. package/src/molecules/ListItem.tsx +35 -0
  33. package/src/molecules/ScreenHeader.tsx +171 -0
  34. package/src/molecules/SearchBar.tsx +198 -0
  35. package/src/molecules/confirmation-modal/components.tsx +94 -0
  36. package/src/molecules/confirmation-modal/index.ts +7 -0
  37. package/src/molecules/confirmation-modal/styles/confirmationModalStyles.ts +133 -0
  38. package/src/molecules/confirmation-modal/types/index.ts +41 -0
  39. package/src/molecules/confirmation-modal/useConfirmationModal.ts +50 -0
  40. package/src/molecules/index.ts +19 -0
  41. package/src/molecules/listitem/index.ts +6 -0
  42. package/src/molecules/listitem/styles/listItemStyles.ts +37 -0
  43. package/src/molecules/listitem/types/index.ts +21 -0
  44. package/src/organisms/AppHeader.tsx +136 -0
  45. package/src/organisms/FormContainer.tsx +169 -0
  46. package/src/organisms/ScreenLayout.tsx +183 -0
  47. package/src/organisms/index.ts +31 -0
  48. package/src/responsive/config.ts +139 -0
  49. package/src/responsive/deviceDetection.ts +155 -0
  50. package/src/responsive/gridUtils.ts +79 -0
  51. package/src/responsive/index.ts +52 -0
  52. package/src/responsive/platformConstants.ts +98 -0
  53. package/src/responsive/responsive.ts +61 -0
  54. package/src/responsive/responsiveLayout.ts +137 -0
  55. package/src/responsive/responsiveSizing.ts +134 -0
  56. package/src/responsive/useResponsive.ts +140 -0
  57. package/src/responsive/validation.ts +158 -0
  58. package/src/theme/core/BaseTokens.ts +42 -0
  59. package/src/theme/core/ColorPalette.ts +29 -0
  60. package/src/theme/core/CustomColors.ts +122 -0
  61. package/src/theme/core/NavigationTheme.ts +72 -0
  62. package/src/theme/core/TokenFactory.ts +103 -0
  63. package/src/theme/core/colors/ColorUtils.ts +53 -0
  64. package/src/theme/core/colors/DarkColors.ts +146 -0
  65. package/src/theme/core/colors/LightColors.ts +146 -0
  66. package/src/theme/core/constants/DesignConstants.ts +31 -0
  67. package/src/theme/core/themes.ts +118 -0
  68. package/src/theme/core/tokens/BaseTokens.ts +144 -0
  69. package/src/theme/core/tokens/Borders.ts +43 -0
  70. package/src/theme/core/tokens/Sizes.ts +51 -0
  71. package/src/theme/core/tokens/Spacing.ts +38 -0
  72. package/src/theme/core/tokens/Typography.ts +143 -0
  73. package/src/theme/hooks/useAppDesignTokens.ts +45 -0
  74. package/src/theme/hooks/useCommonStyles.ts +248 -0
  75. package/src/theme/hooks/useThemedStyles.ts +68 -0
  76. package/src/theme/index.ts +94 -0
  77. package/src/theme/infrastructure/globalThemeStore.ts +69 -0
  78. package/src/theme/infrastructure/storage/ThemeStorage.ts +93 -0
  79. package/src/theme/infrastructure/stores/themeStore.ts +109 -0
  80. package/src/typography/__tests__/colorValidationUtils.test.ts +180 -0
  81. package/src/typography/__tests__/textColorUtils.test.ts +185 -0
  82. package/src/typography/__tests__/textStyleUtils.test.ts +168 -0
  83. package/src/typography/domain/entities/TypographyTypes.ts +88 -0
  84. package/src/typography/index.ts +53 -0
  85. package/src/typography/presentation/utils/colorValidationUtils.ts +133 -0
  86. package/src/typography/presentation/utils/textColorUtils.ts +205 -0
  87. package/src/typography/presentation/utils/textStyleUtils.ts +159 -0
@@ -0,0 +1,88 @@
1
+ import { StyleProp, ViewStyle } from 'react-native';
2
+
3
+ /**
4
+ * FAB (Floating Action Button) size variants
5
+ * Based on Material Design 3 standards
6
+ */
7
+ export type FabSize = 'sm' | 'md' | 'lg';
8
+
9
+ /**
10
+ * FAB variant types
11
+ * - primary: Main action (uses primary color)
12
+ * - secondary: Secondary action (uses secondary color)
13
+ * - surface: Neutral action (uses surface color with border)
14
+ */
15
+ export type FabVariant = 'primary' | 'secondary' | 'surface';
16
+
17
+ /**
18
+ * FAB configuration for variant styling
19
+ */
20
+ export interface FabVariantConfig {
21
+ backgroundColor: string;
22
+ iconColor: string;
23
+ }
24
+
25
+ /**
26
+ * FAB configuration for size styling
27
+ */
28
+ export interface FabSizeConfig {
29
+ width: number;
30
+ height: number;
31
+ borderRadius: number;
32
+ }
33
+
34
+ /**
35
+ * AtomicFab component props
36
+ */
37
+ export interface AtomicFabProps {
38
+ /**
39
+ * Icon name to display (required)
40
+ * Any MaterialIcons name (see https://fonts.google.com/icons)
41
+ * Examples: 'add', 'edit', 'camera', etc.
42
+ */
43
+ icon: string;
44
+
45
+ /**
46
+ * Callback when FAB is pressed
47
+ */
48
+ onPress: () => void;
49
+
50
+ /**
51
+ * Visual variant of the FAB
52
+ * @default 'primary'
53
+ */
54
+ variant?: FabVariant;
55
+
56
+ /**
57
+ * Size of the FAB
58
+ * @default 'md'
59
+ */
60
+ size?: FabSize;
61
+
62
+ /**
63
+ * Whether the FAB is disabled
64
+ * @default false
65
+ */
66
+ disabled?: boolean;
67
+
68
+ /**
69
+ * Custom style for the FAB container
70
+ */
71
+ style?: StyleProp<ViewStyle>;
72
+
73
+ /**
74
+ * Test ID for testing
75
+ */
76
+ testID?: string;
77
+
78
+ /**
79
+ * Accessibility label
80
+ */
81
+ accessibilityLabel?: string;
82
+
83
+ /**
84
+ * Active opacity for touch feedback
85
+ * @default 0.7
86
+ */
87
+ activeOpacity?: number;
88
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Atoms - Primitive UI components
3
+ * Building blocks for molecules and organisms
4
+ */
5
+
6
+ // Button
7
+ export {
8
+ AtomicButton,
9
+ type AtomicButtonProps,
10
+ type ButtonVariant,
11
+ type ButtonSize,
12
+ } from './AtomicButton';
13
+
14
+ // Text
15
+ export { AtomicText, type AtomicTextProps } from './AtomicText';
16
+
17
+ // Card - Note: Card may not exist, check later
18
+ // export {
19
+ // AtomicCard,
20
+ // type AtomicCardProps,
21
+ // type AtomicCardVariant,
22
+ // type AtomicCardPadding,
23
+ // } from './AtomicCard';
24
+
25
+ // Input
26
+ export {
27
+ AtomicInput,
28
+ type AtomicInputProps,
29
+ type AtomicInputVariant,
30
+ type AtomicInputState,
31
+ type AtomicInputSize,
32
+ } from './AtomicInput';
33
+
34
+ // Icon
35
+ export {
36
+ AtomicIcon,
37
+ type AtomicIconProps,
38
+ type IconSize,
39
+ type IconColor,
40
+ type IconName,
41
+ } from './AtomicIcon';
42
+
43
+ // Avatar
44
+ export { AtomicAvatar, type AtomicAvatarProps } from './AtomicAvatar';
45
+
46
+ // Chip
47
+ export { AtomicChip, type AtomicChipProps } from './AtomicChip';
48
+
49
+ // Progress
50
+ export { AtomicProgress, type AtomicProgressProps } from './AtomicProgress';
51
+
52
+ // Fab
53
+ export {
54
+ AtomicFab,
55
+ type AtomicFabProps,
56
+ type FabSize,
57
+ type FabVariant,
58
+ getFabVariants,
59
+ } from './AtomicFab';
60
+
61
+ // Picker
62
+ export {
63
+ AtomicPicker,
64
+ type AtomicPickerProps,
65
+ type PickerOption,
66
+ type PickerSize,
67
+ } from './AtomicPicker';
68
+
69
+ // DatePicker
70
+ export { AtomicDatePicker, type AtomicDatePickerProps } from './AtomicDatePicker';
@@ -0,0 +1,63 @@
1
+ import { useState, useCallback } from 'react';
2
+
3
+ interface UseInputStateProps {
4
+ value?: string;
5
+ onChangeText?: (text: string) => void;
6
+ secureTextEntry?: boolean;
7
+ showPasswordToggle?: boolean;
8
+ maxLength?: number;
9
+ showCharacterCount?: boolean;
10
+ }
11
+
12
+ interface UseInputStateReturn {
13
+ localValue: string;
14
+ isFocused: boolean;
15
+ isPasswordVisible: boolean;
16
+ characterCount: number;
17
+ isAtMaxLength: boolean;
18
+ setIsFocused: (focused: boolean) => void;
19
+ handleTextChange: (text: string) => void;
20
+ togglePasswordVisibility: () => void;
21
+ }
22
+
23
+ export const useInputState = ({
24
+ value = '',
25
+ onChangeText,
26
+ secureTextEntry = false,
27
+ showPasswordToggle = false,
28
+ maxLength,
29
+ showCharacterCount = false,
30
+ }: UseInputStateProps = {}): UseInputStateReturn => {
31
+ const [localValue, setLocalValue] = useState(value);
32
+ const [isFocused, setIsFocused] = useState(false);
33
+ const [isPasswordVisible, setIsPasswordVisible] = useState(!secureTextEntry);
34
+
35
+ const handleTextChange = useCallback((text: string) => {
36
+ if (__DEV__) {
37
+ console.log('[useInputState] Text changed:', { text, length: text.length });
38
+ }
39
+ setLocalValue(text);
40
+ onChangeText?.(text);
41
+ }, [onChangeText]);
42
+
43
+ const togglePasswordVisibility = useCallback(() => {
44
+ if (__DEV__) {
45
+ console.log('[useInputState] Password visibility toggled');
46
+ }
47
+ setIsPasswordVisible((prev) => !prev);
48
+ }, []);
49
+
50
+ const characterCount = localValue.length;
51
+ const isAtMaxLength = maxLength ? characterCount >= maxLength : false;
52
+
53
+ return {
54
+ localValue,
55
+ isFocused,
56
+ isPasswordVisible,
57
+ characterCount,
58
+ isAtMaxLength,
59
+ setIsFocused,
60
+ handleTextChange,
61
+ togglePasswordVisibility,
62
+ };
63
+ };
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Input Styles Helper
3
+ *
4
+ * Helper functions for AtomicInput component styling.
5
+ * Extracted from AtomicInput for better separation of concerns.
6
+ */
7
+
8
+ import { ViewStyle, TextStyle } from 'react-native';
9
+ import { useAppDesignTokens } from '../../../theme';
10
+ import type { AtomicInputVariant, AtomicInputSize } from '../../AtomicInput';
11
+
12
+ interface GetVariantStyleParams {
13
+ variant: AtomicInputVariant;
14
+ isFocused: boolean;
15
+ hasError: boolean;
16
+ hasSuccess: boolean;
17
+ isDisabled: boolean;
18
+ tokens: ReturnType<typeof useAppDesignTokens>;
19
+ }
20
+
21
+ interface GetSizeConfigParams {
22
+ size: AtomicInputSize;
23
+ tokens: ReturnType<typeof useAppDesignTokens>;
24
+ }
25
+
26
+ export const getSizeConfig = ({ size, tokens }: GetSizeConfigParams) => {
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
+
51
+ return sizeConfig[size] ?? sizeConfig.md;
52
+ };
53
+
54
+ export const getVariantStyle = ({
55
+ variant,
56
+ isFocused,
57
+ hasError,
58
+ hasSuccess,
59
+ isDisabled,
60
+ tokens,
61
+ }: GetVariantStyleParams): ViewStyle => {
62
+ const baseStyle: ViewStyle = {
63
+ backgroundColor: tokens.colors.surface,
64
+ borderRadius: tokens.borders.radius.md,
65
+ };
66
+
67
+ let borderColor = tokens.colors.border;
68
+ if (isFocused) borderColor = tokens.colors.primary;
69
+ if (hasError) borderColor = tokens.colors.error;
70
+ if (hasSuccess) borderColor = tokens.colors.success;
71
+ if (isDisabled) borderColor = tokens.colors.borderDisabled;
72
+
73
+ switch (variant) {
74
+ case 'outlined':
75
+ return {
76
+ ...baseStyle,
77
+ borderWidth: isFocused ? 2 : 1,
78
+ borderColor,
79
+ };
80
+
81
+ case 'filled':
82
+ return {
83
+ ...baseStyle,
84
+ backgroundColor: tokens.colors.surfaceVariant,
85
+ borderWidth: 0,
86
+ borderBottomWidth: isFocused ? 2 : 1,
87
+ borderBottomColor: borderColor,
88
+ };
89
+
90
+ case 'flat':
91
+ return {
92
+ ...baseStyle,
93
+ backgroundColor: 'transparent',
94
+ borderWidth: 0,
95
+ borderBottomWidth: 1,
96
+ borderBottomColor: borderColor,
97
+ borderRadius: 0,
98
+ };
99
+
100
+ default:
101
+ return baseStyle;
102
+ }
103
+ };
104
+
105
+ export const getTextColor = ({
106
+ isDisabled,
107
+ hasError,
108
+ hasSuccess,
109
+ tokens,
110
+ }: {
111
+ isDisabled: boolean;
112
+ hasError: boolean;
113
+ hasSuccess: boolean;
114
+ tokens: ReturnType<typeof useAppDesignTokens>;
115
+ }): string => {
116
+ if (isDisabled) return tokens.colors.textDisabled;
117
+ if (hasError) return tokens.colors.error;
118
+ if (hasSuccess) return tokens.colors.success;
119
+ return tokens.colors.onSurface;
120
+ };
@@ -0,0 +1,57 @@
1
+ /**
2
+ * PickerChips Component
3
+ *
4
+ * Component for rendering selected chips in multi-select mode.
5
+ * Extracted from AtomicPicker for better separation of concerns.
6
+ */
7
+
8
+ import React from 'react';
9
+ import { View, TouchableOpacity } from 'react-native';
10
+ import { useAppDesignTokens } from '../../../theme';
11
+ import { PickerOption } from '../types';
12
+ import { AtomicIcon } from '../../AtomicIcon';
13
+ import { AtomicText } from '../../AtomicText';
14
+ import {
15
+ getChipContainerStyles,
16
+ getChipStyles,
17
+ getChipTextStyles,
18
+ } from '../styles/pickerStyles';
19
+
20
+ interface PickerChipsProps {
21
+ selectedOptions: PickerOption[];
22
+ onRemoveChip: (value: string) => void;
23
+ testID?: string;
24
+ }
25
+
26
+ export const PickerChips: React.FC<PickerChipsProps> = React.memo(({
27
+ selectedOptions,
28
+ onRemoveChip,
29
+ testID,
30
+ }) => {
31
+ const tokens = useAppDesignTokens();
32
+
33
+ const chipContainerStyles = getChipContainerStyles(tokens);
34
+ const chipStyles = getChipStyles(tokens);
35
+ const chipTextStyles = getChipTextStyles(tokens);
36
+
37
+ if (selectedOptions.length === 0) return null;
38
+
39
+ return (
40
+ <View style={chipContainerStyles}>
41
+ {selectedOptions.map((opt) => (
42
+ <View key={opt.value} style={chipStyles}>
43
+ <AtomicText style={chipTextStyles}>{opt.label}</AtomicText>
44
+ <TouchableOpacity
45
+ onPress={(e) => {
46
+ e.stopPropagation();
47
+ onRemoveChip(opt.value);
48
+ }}
49
+ hitSlop={{ top: 4, bottom: 4, left: 4, right: 4 }}
50
+ >
51
+ <AtomicIcon name="X" size="sm" color="primary" />
52
+ </TouchableOpacity>
53
+ </View>
54
+ ))}
55
+ </View>
56
+ );
57
+ });
@@ -0,0 +1,214 @@
1
+ /**
2
+ * PickerModal Component
3
+ *
4
+ * Modal component for AtomicPicker that handles the selection interface.
5
+ * Extracted from AtomicPicker to follow single responsibility principle.
6
+ *
7
+ * Features:
8
+ * - Search functionality
9
+ * - Option list rendering
10
+ * - Multi-select support
11
+ * - Empty state handling
12
+ */
13
+
14
+ import React from 'react';
15
+ import {
16
+ View,
17
+ Modal,
18
+ FlatList,
19
+ TextInput,
20
+ StyleSheet,
21
+ TouchableOpacity,
22
+ } from 'react-native';
23
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
24
+ import { useAppDesignTokens } from '../../../theme';
25
+ import { PickerOption } from '../types';
26
+ import { AtomicIcon } from '../../AtomicIcon';
27
+ import { AtomicText } from '../../AtomicText';
28
+ import {
29
+ getModalOverlayStyles,
30
+ getModalContainerStyles,
31
+ getModalHeaderStyles,
32
+ getModalTitleStyles,
33
+ getSearchContainerStyles,
34
+ getSearchInputStyles,
35
+ getOptionContainerStyles,
36
+ getOptionTextStyles,
37
+ getOptionDescriptionStyles,
38
+ getEmptyStateStyles,
39
+ getEmptyStateTextStyles,
40
+ } from '../styles/pickerStyles';
41
+
42
+ interface PickerModalProps {
43
+ visible: boolean;
44
+ onClose: () => void;
45
+ options: PickerOption[];
46
+ selectedValues: string[];
47
+ onSelect: (value: string) => void;
48
+ title?: string;
49
+ searchable?: boolean;
50
+ searchQuery: string;
51
+ onSearchChange: (query: string) => void;
52
+ filteredOptions: PickerOption[];
53
+ multiple?: boolean;
54
+ emptyMessage?: string;
55
+ searchPlaceholder?: string;
56
+ closeAccessibilityLabel?: string;
57
+ testID?: string;
58
+ }
59
+
60
+ export const PickerModal: React.FC<PickerModalProps> = React.memo(({
61
+ visible,
62
+ onClose,
63
+ options,
64
+ selectedValues,
65
+ onSelect,
66
+ title,
67
+ searchable = false,
68
+ searchQuery,
69
+ onSearchChange,
70
+ filteredOptions,
71
+ multiple = false,
72
+ emptyMessage = 'No options available',
73
+ searchPlaceholder = 'Search...',
74
+ closeAccessibilityLabel = 'Close picker',
75
+ testID,
76
+ }) => {
77
+ const tokens = useAppDesignTokens();
78
+ const insets = useSafeAreaInsets();
79
+
80
+ const modalOverlayStyles = getModalOverlayStyles(tokens);
81
+ const modalContainerStyles = getModalContainerStyles(tokens, 0);
82
+ const modalHeaderStyles = getModalHeaderStyles(tokens);
83
+ const modalTitleStyles = getModalTitleStyles(tokens);
84
+ const searchContainerStyles = getSearchContainerStyles(tokens);
85
+ const searchInputStyles = getSearchInputStyles(tokens);
86
+ const emptyStateStyles = getEmptyStateStyles(tokens);
87
+ const emptyStateTextStyles = getEmptyStateTextStyles(tokens);
88
+
89
+ const isSelected = (optionValue: string): boolean => {
90
+ return selectedValues.includes(optionValue);
91
+ };
92
+
93
+ const renderOption = ({ item }: { item: PickerOption }) => {
94
+ const selected = isSelected(item.value);
95
+ const itemDisabled = item.disabled || false;
96
+
97
+ const optionContainerStyle = getOptionContainerStyles(
98
+ tokens,
99
+ selected,
100
+ itemDisabled
101
+ );
102
+ const optionTextStyle = getOptionTextStyles(tokens, selected);
103
+ const optionDescriptionStyle = getOptionDescriptionStyles(tokens);
104
+
105
+ return (
106
+ <TouchableOpacity
107
+ onPress={() => !itemDisabled && onSelect(item.value)}
108
+ disabled={itemDisabled}
109
+ testID={item.testID || `${testID}-option-${item.value}`}
110
+ style={optionContainerStyle}
111
+ >
112
+ {/* Option Icon */}
113
+ {item.icon && (
114
+ <AtomicIcon
115
+ name={item.icon}
116
+ size="md"
117
+ color={selected ? 'primary' : 'secondary'}
118
+ />
119
+ )}
120
+
121
+ {/* Option Content */}
122
+ <View style={{ flex: 1 }}>
123
+ <AtomicText style={optionTextStyle}>{item.label}</AtomicText>
124
+ {item.description && (
125
+ <AtomicText style={optionDescriptionStyle}>
126
+ {item.description}
127
+ </AtomicText>
128
+ )}
129
+ </View>
130
+
131
+ {/* Selected Indicator */}
132
+ {selected && (
133
+ <AtomicIcon name="CircleCheck" size="md" color="primary" />
134
+ )}
135
+ </TouchableOpacity>
136
+ );
137
+ };
138
+
139
+ return (
140
+ <Modal
141
+ visible={visible}
142
+ animationType="slide"
143
+ transparent
144
+ onRequestClose={onClose}
145
+ testID={`${testID}-modal`}
146
+ >
147
+ <View style={modalOverlayStyles}>
148
+ <View
149
+ style={[
150
+ modalContainerStyles,
151
+ { paddingBottom: insets.bottom + tokens.spacing.md },
152
+ ]}
153
+ >
154
+ {/* Modal Header */}
155
+ <View style={modalHeaderStyles}>
156
+ {/* Title */}
157
+ <AtomicText style={modalTitleStyles}>
158
+ {title || 'Select'}
159
+ </AtomicText>
160
+
161
+ {/* Close Button */}
162
+ <TouchableOpacity
163
+ onPress={onClose}
164
+ hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
165
+ accessibilityRole="button"
166
+ accessibilityLabel={closeAccessibilityLabel}
167
+ testID={`${testID}-close`}
168
+ >
169
+ <AtomicIcon name="X" size="md" color="primary" />
170
+ </TouchableOpacity>
171
+ </View>
172
+
173
+ {/* Search Bar */}
174
+ {searchable && (
175
+ <View style={searchContainerStyles}>
176
+ <AtomicIcon name="Search" size="sm" color="secondary" />
177
+ <TextInput
178
+ value={searchQuery}
179
+ onChangeText={onSearchChange}
180
+ placeholder={searchPlaceholder}
181
+ placeholderTextColor={tokens.colors.textSecondary}
182
+ style={searchInputStyles}
183
+ testID={`${testID}-search`}
184
+ />
185
+ {searchQuery.length > 0 && (
186
+ <TouchableOpacity onPress={() => onSearchChange('')}>
187
+ <AtomicIcon name="X" size="sm" color="secondary" />
188
+ </TouchableOpacity>
189
+ )}
190
+ </View>
191
+ )}
192
+
193
+ {/* Options List */}
194
+ {filteredOptions.length > 0 ? (
195
+ <FlatList
196
+ data={filteredOptions}
197
+ keyExtractor={(item) => item.value}
198
+ renderItem={renderOption}
199
+ showsVerticalScrollIndicator
200
+ testID={`${testID}-list`}
201
+ />
202
+ ) : (
203
+ <View style={emptyStateStyles}>
204
+ <AtomicIcon name="Info" size="xl" color="secondary" />
205
+ <AtomicText style={emptyStateTextStyles}>
206
+ {emptyMessage}
207
+ </AtomicText>
208
+ </View>
209
+ )}
210
+ </View>
211
+ </View>
212
+ </Modal>
213
+ );
214
+ });