@umituz/react-native-design-system 1.14.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.
- package/package.json +26 -19
- package/src/atoms/AtomicAvatar.tsx +161 -0
- package/src/atoms/AtomicButton.tsx +241 -0
- package/src/atoms/AtomicChip.tsx +226 -0
- package/src/atoms/AtomicDatePicker.tsx +255 -0
- package/src/atoms/AtomicFab.tsx +99 -0
- package/src/atoms/AtomicIcon.tsx +149 -0
- package/src/atoms/AtomicInput.tsx +308 -0
- package/src/atoms/AtomicPicker.tsx +310 -0
- package/src/atoms/AtomicProgress.tsx +149 -0
- package/src/atoms/AtomicText.tsx +55 -0
- package/src/atoms/__tests__/AtomicButton.test.tsx +107 -0
- package/src/atoms/__tests__/AtomicIcon.test.tsx +110 -0
- package/src/atoms/__tests__/AtomicInput.test.tsx +195 -0
- package/src/atoms/datepicker/components/DatePickerButton.tsx +112 -0
- package/src/atoms/datepicker/components/DatePickerModal.tsx +143 -0
- package/src/atoms/fab/styles/fabStyles.ts +98 -0
- package/src/atoms/fab/types/index.ts +88 -0
- package/src/atoms/index.ts +70 -0
- package/src/atoms/input/hooks/useInputState.ts +63 -0
- package/src/atoms/input/styles/inputStylesHelper.ts +120 -0
- package/src/atoms/picker/components/PickerChips.tsx +57 -0
- package/src/atoms/picker/components/PickerModal.tsx +214 -0
- package/src/atoms/picker/styles/pickerStyles.ts +223 -0
- package/src/atoms/picker/types/index.ts +42 -0
- package/src/index.ts +148 -79
- package/src/molecules/ConfirmationModal.tsx +42 -0
- package/src/molecules/ConfirmationModalContent.tsx +87 -0
- package/src/molecules/ConfirmationModalMain.tsx +91 -0
- package/src/molecules/FormField.tsx +155 -0
- package/src/molecules/IconContainer.tsx +79 -0
- package/src/molecules/ListItem.tsx +35 -0
- package/src/molecules/ScreenHeader.tsx +171 -0
- package/src/molecules/SearchBar.tsx +198 -0
- package/src/molecules/confirmation-modal/components.tsx +94 -0
- package/src/molecules/confirmation-modal/index.ts +7 -0
- package/src/molecules/confirmation-modal/styles/confirmationModalStyles.ts +133 -0
- package/src/molecules/confirmation-modal/types/index.ts +41 -0
- package/src/molecules/confirmation-modal/useConfirmationModal.ts +50 -0
- package/src/molecules/index.ts +19 -0
- package/src/molecules/listitem/index.ts +6 -0
- package/src/molecules/listitem/styles/listItemStyles.ts +37 -0
- package/src/molecules/listitem/types/index.ts +21 -0
- package/src/organisms/AppHeader.tsx +136 -0
- package/src/organisms/FormContainer.tsx +169 -0
- package/src/organisms/ScreenLayout.tsx +183 -0
- package/src/organisms/index.ts +31 -0
- package/src/responsive/config.ts +139 -0
- package/src/responsive/deviceDetection.ts +155 -0
- package/src/responsive/gridUtils.ts +79 -0
- package/src/responsive/index.ts +52 -0
- package/src/responsive/platformConstants.ts +98 -0
- package/src/responsive/responsive.ts +61 -0
- package/src/responsive/responsiveLayout.ts +137 -0
- package/src/responsive/responsiveSizing.ts +134 -0
- package/src/responsive/useResponsive.ts +140 -0
- package/src/responsive/validation.ts +158 -0
- package/src/theme/core/BaseTokens.ts +42 -0
- package/src/theme/core/ColorPalette.ts +29 -0
- package/src/theme/core/CustomColors.ts +122 -0
- package/src/theme/core/NavigationTheme.ts +72 -0
- package/src/theme/core/TokenFactory.ts +103 -0
- package/src/theme/core/colors/ColorUtils.ts +53 -0
- package/src/theme/core/colors/DarkColors.ts +146 -0
- package/src/theme/core/colors/LightColors.ts +146 -0
- package/src/theme/core/constants/DesignConstants.ts +31 -0
- package/src/theme/core/themes.ts +118 -0
- package/src/theme/core/tokens/BaseTokens.ts +144 -0
- package/src/theme/core/tokens/Borders.ts +43 -0
- package/src/theme/core/tokens/Sizes.ts +51 -0
- package/src/theme/core/tokens/Spacing.ts +38 -0
- package/src/theme/core/tokens/Typography.ts +143 -0
- package/src/theme/hooks/useAppDesignTokens.ts +45 -0
- package/src/theme/hooks/useCommonStyles.ts +248 -0
- package/src/theme/hooks/useThemedStyles.ts +68 -0
- package/src/theme/index.ts +94 -0
- package/src/theme/infrastructure/globalThemeStore.ts +69 -0
- package/src/theme/infrastructure/storage/ThemeStorage.ts +93 -0
- package/src/theme/infrastructure/stores/themeStore.ts +109 -0
- package/src/typography/__tests__/colorValidationUtils.test.ts +180 -0
- package/src/typography/__tests__/textColorUtils.test.ts +185 -0
- package/src/typography/__tests__/textStyleUtils.test.ts +168 -0
- package/src/typography/domain/entities/TypographyTypes.ts +88 -0
- package/src/typography/index.ts +53 -0
- package/src/typography/presentation/utils/colorValidationUtils.ts +133 -0
- package/src/typography/presentation/utils/textColorUtils.ts +205 -0
- 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
|
+
});
|