@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.
- 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 +133 -56
- 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,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConfirmationModal Main Component
|
|
3
|
+
*
|
|
4
|
+
* Main confirmation modal component
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import { View, Modal, TouchableOpacity } from 'react-native';
|
|
9
|
+
import { useAppDesignTokens } from '../theme';
|
|
10
|
+
import { ConfirmationModalProps } from './confirmation-modal/types/';
|
|
11
|
+
import {
|
|
12
|
+
getModalOverlayStyle,
|
|
13
|
+
getBackdropStyle,
|
|
14
|
+
} from './confirmation-modal/styles/confirmationModalStyles';
|
|
15
|
+
import { ConfirmationModalContent } from './ConfirmationModalContent';
|
|
16
|
+
|
|
17
|
+
const useBackdropHandler = (backdropDismissible: boolean, onCancel: () => void) => {
|
|
18
|
+
return React.useCallback(() => {
|
|
19
|
+
if (backdropDismissible) {
|
|
20
|
+
onCancel();
|
|
21
|
+
}
|
|
22
|
+
}, [backdropDismissible, onCancel]);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const ConfirmationModalBackdrop: React.FC<{
|
|
26
|
+
showBackdrop: boolean;
|
|
27
|
+
onBackdropPress: () => void;
|
|
28
|
+
testID: string;
|
|
29
|
+
}> = ({ showBackdrop, onBackdropPress, testID }) => {
|
|
30
|
+
if (!showBackdrop) return null;
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<TouchableOpacity
|
|
34
|
+
style={getBackdropStyle()}
|
|
35
|
+
activeOpacity={1}
|
|
36
|
+
onPress={onBackdropPress}
|
|
37
|
+
testID={`${testID}-backdrop`}
|
|
38
|
+
/>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const ConfirmationModal: React.FC<ConfirmationModalProps> = ({
|
|
43
|
+
visible,
|
|
44
|
+
title,
|
|
45
|
+
message,
|
|
46
|
+
variant = 'default',
|
|
47
|
+
confirmText = 'Confirm',
|
|
48
|
+
cancelText = 'Cancel',
|
|
49
|
+
icon,
|
|
50
|
+
onConfirm,
|
|
51
|
+
onCancel,
|
|
52
|
+
showBackdrop = true,
|
|
53
|
+
backdropDismissible = true,
|
|
54
|
+
style,
|
|
55
|
+
testID = 'atomic-confirmation-modal',
|
|
56
|
+
}) => {
|
|
57
|
+
const tokens = useAppDesignTokens();
|
|
58
|
+
const handleBackdropPress = useBackdropHandler(backdropDismissible, onCancel);
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<Modal
|
|
62
|
+
visible={visible}
|
|
63
|
+
transparent
|
|
64
|
+
onRequestClose={onCancel}
|
|
65
|
+
statusBarTranslucent
|
|
66
|
+
testID={testID}
|
|
67
|
+
>
|
|
68
|
+
<View style={getModalOverlayStyle(tokens)}>
|
|
69
|
+
<ConfirmationModalBackdrop
|
|
70
|
+
showBackdrop={showBackdrop}
|
|
71
|
+
onBackdropPress={handleBackdropPress}
|
|
72
|
+
testID={testID}
|
|
73
|
+
/>
|
|
74
|
+
|
|
75
|
+
<ConfirmationModalContent
|
|
76
|
+
tokens={tokens}
|
|
77
|
+
variant={variant}
|
|
78
|
+
title={title}
|
|
79
|
+
message={message}
|
|
80
|
+
confirmText={confirmText}
|
|
81
|
+
cancelText={cancelText}
|
|
82
|
+
icon={icon}
|
|
83
|
+
onConfirm={onConfirm}
|
|
84
|
+
onCancel={onCancel}
|
|
85
|
+
style={style}
|
|
86
|
+
testID={testID}
|
|
87
|
+
/>
|
|
88
|
+
</View>
|
|
89
|
+
</Modal>
|
|
90
|
+
);
|
|
91
|
+
};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FormField Molecule - Complete Form Input with Label and Error
|
|
3
|
+
*
|
|
4
|
+
* Combines AtomicText (label/error) + AtomicInput (field)
|
|
5
|
+
*
|
|
6
|
+
* Atomic Design Level: MOLECULE
|
|
7
|
+
* Composition: AtomicText + AtomicInput
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React from 'react';
|
|
11
|
+
import { View, ViewStyle } from 'react-native';
|
|
12
|
+
import { useAppDesignTokens } from '../theme';
|
|
13
|
+
import { AtomicText, AtomicInput, type AtomicInputProps } from '../atoms';
|
|
14
|
+
|
|
15
|
+
// =============================================================================
|
|
16
|
+
// TYPE DEFINITIONS
|
|
17
|
+
// =============================================================================
|
|
18
|
+
|
|
19
|
+
export interface FormFieldProps extends Omit<AtomicInputProps, 'state' | 'label'> {
|
|
20
|
+
label?: string;
|
|
21
|
+
error?: string;
|
|
22
|
+
helperText?: string;
|
|
23
|
+
required?: boolean;
|
|
24
|
+
containerStyle?: ViewStyle;
|
|
25
|
+
style?: ViewStyle; // Alias for containerStyle (for convenience)
|
|
26
|
+
requiredIndicator?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// =============================================================================
|
|
30
|
+
// COMPONENT IMPLEMENTATION
|
|
31
|
+
// =============================================================================
|
|
32
|
+
|
|
33
|
+
const FormFieldLabel: React.FC<{
|
|
34
|
+
label?: string;
|
|
35
|
+
required?: boolean;
|
|
36
|
+
requiredIndicator?: string;
|
|
37
|
+
styles: ReturnType<typeof getStyles>;
|
|
38
|
+
}> = ({ label, required, requiredIndicator, styles }) => {
|
|
39
|
+
if (!label) return null;
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<View style={styles.labelContainer}>
|
|
43
|
+
<AtomicText type="labelMedium" color="primary" style={styles.label}>
|
|
44
|
+
{label}
|
|
45
|
+
</AtomicText>
|
|
46
|
+
{required && (
|
|
47
|
+
<AtomicText type="labelMedium" color="error">
|
|
48
|
+
{requiredIndicator}
|
|
49
|
+
</AtomicText>
|
|
50
|
+
)}
|
|
51
|
+
</View>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const FormFieldMessage: React.FC<{
|
|
56
|
+
error?: string;
|
|
57
|
+
helperText?: string;
|
|
58
|
+
styles: ReturnType<typeof getStyles>;
|
|
59
|
+
}> = ({ error, helperText, styles }) => {
|
|
60
|
+
if (error) {
|
|
61
|
+
return (
|
|
62
|
+
<AtomicText
|
|
63
|
+
type="bodySmall"
|
|
64
|
+
color="error"
|
|
65
|
+
style={styles.errorText}
|
|
66
|
+
>
|
|
67
|
+
{error}
|
|
68
|
+
</AtomicText>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (helperText) {
|
|
73
|
+
return (
|
|
74
|
+
<AtomicText
|
|
75
|
+
type="bodySmall"
|
|
76
|
+
color="secondary"
|
|
77
|
+
style={styles.helperText}
|
|
78
|
+
>
|
|
79
|
+
{helperText}
|
|
80
|
+
</AtomicText>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return null;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const FormField: React.FC<FormFieldProps> = ({
|
|
88
|
+
label,
|
|
89
|
+
error,
|
|
90
|
+
helperText,
|
|
91
|
+
required = false,
|
|
92
|
+
containerStyle,
|
|
93
|
+
style,
|
|
94
|
+
requiredIndicator = ' *',
|
|
95
|
+
...inputProps
|
|
96
|
+
}) => {
|
|
97
|
+
const tokens = useAppDesignTokens();
|
|
98
|
+
const inputState = error ? 'error' : 'default';
|
|
99
|
+
const styles = getStyles(tokens);
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<View style={[styles.container, containerStyle || style]}>
|
|
103
|
+
<FormFieldLabel
|
|
104
|
+
label={label}
|
|
105
|
+
required={required}
|
|
106
|
+
requiredIndicator={requiredIndicator}
|
|
107
|
+
styles={styles}
|
|
108
|
+
/>
|
|
109
|
+
|
|
110
|
+
<AtomicInput
|
|
111
|
+
{...inputProps}
|
|
112
|
+
label={label || ''}
|
|
113
|
+
state={inputState}
|
|
114
|
+
/>
|
|
115
|
+
|
|
116
|
+
<FormFieldMessage
|
|
117
|
+
error={error}
|
|
118
|
+
helperText={helperText}
|
|
119
|
+
styles={styles}
|
|
120
|
+
/>
|
|
121
|
+
</View>
|
|
122
|
+
);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// =============================================================================
|
|
126
|
+
// STYLES
|
|
127
|
+
// =============================================================================
|
|
128
|
+
|
|
129
|
+
const getStyles = (tokens: ReturnType<typeof useAppDesignTokens>) => ({
|
|
130
|
+
container: {
|
|
131
|
+
marginBottom: tokens.spacing.md,
|
|
132
|
+
} as ViewStyle,
|
|
133
|
+
labelContainer: {
|
|
134
|
+
flexDirection: 'row',
|
|
135
|
+
marginBottom: tokens.spacing.sm,
|
|
136
|
+
} as ViewStyle,
|
|
137
|
+
label: {
|
|
138
|
+
fontWeight: tokens.typography.labelMedium.fontWeight,
|
|
139
|
+
color: tokens.colors.textPrimary,
|
|
140
|
+
} as ViewStyle,
|
|
141
|
+
inputError: {
|
|
142
|
+
borderColor: tokens.colors.error,
|
|
143
|
+
} as ViewStyle,
|
|
144
|
+
errorText: {
|
|
145
|
+
marginTop: tokens.spacing.xs,
|
|
146
|
+
} as ViewStyle,
|
|
147
|
+
helperText: {
|
|
148
|
+
marginTop: tokens.spacing.xs,
|
|
149
|
+
} as ViewStyle,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// =============================================================================
|
|
153
|
+
// EXPORTS
|
|
154
|
+
// =============================================================================
|
|
155
|
+
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IconContainer Molecule Component
|
|
3
|
+
*
|
|
4
|
+
* Standardized icon container with consistent sizing and styling.
|
|
5
|
+
* Used throughout app for icon displays in lists, cards, and settings.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Consistent sizing system
|
|
9
|
+
* - Optional background circle
|
|
10
|
+
* - Optional gradient background
|
|
11
|
+
* - Theme-aware colors
|
|
12
|
+
* - Accessibility support
|
|
13
|
+
*
|
|
14
|
+
* Atomic Design: Molecule (View + Icon)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import React from 'react';
|
|
18
|
+
import { View, StyleSheet } from 'react-native';
|
|
19
|
+
import { useAppDesignTokens } from '../theme';
|
|
20
|
+
|
|
21
|
+
interface IconContainerProps {
|
|
22
|
+
icon: React.ReactNode;
|
|
23
|
+
size?: 'sm' | 'md' | 'lg' | 'xl';
|
|
24
|
+
backgroundColor?: string;
|
|
25
|
+
withBorder?: boolean;
|
|
26
|
+
borderColor?: string;
|
|
27
|
+
style?: object;
|
|
28
|
+
testID?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const getSizeMap = (tokens: ReturnType<typeof useAppDesignTokens>) => ({
|
|
32
|
+
sm: tokens.iconSizes.sm,
|
|
33
|
+
md: tokens.iconSizes.md,
|
|
34
|
+
lg: tokens.iconSizes.lg,
|
|
35
|
+
xl: tokens.iconSizes.xl,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export const IconContainer: React.FC<IconContainerProps> = ({
|
|
39
|
+
icon,
|
|
40
|
+
size = 'md',
|
|
41
|
+
backgroundColor,
|
|
42
|
+
withBorder = false,
|
|
43
|
+
borderColor,
|
|
44
|
+
style,
|
|
45
|
+
testID,
|
|
46
|
+
}) => {
|
|
47
|
+
const tokens = useAppDesignTokens();
|
|
48
|
+
const sizeMap = getSizeMap(tokens);
|
|
49
|
+
const containerSize = sizeMap[size];
|
|
50
|
+
const borderRadius = containerSize / 2;
|
|
51
|
+
|
|
52
|
+
const containerStyle = [
|
|
53
|
+
styles.container,
|
|
54
|
+
{
|
|
55
|
+
width: containerSize,
|
|
56
|
+
height: containerSize,
|
|
57
|
+
borderRadius,
|
|
58
|
+
backgroundColor: backgroundColor || tokens.colors.surfaceVariant,
|
|
59
|
+
},
|
|
60
|
+
withBorder && {
|
|
61
|
+
borderWidth: 1,
|
|
62
|
+
borderColor: borderColor || tokens.colors.border,
|
|
63
|
+
},
|
|
64
|
+
style,
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<View style={containerStyle} testID={testID}>
|
|
69
|
+
{icon}
|
|
70
|
+
</View>
|
|
71
|
+
);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const styles = StyleSheet.create({
|
|
75
|
+
container: {
|
|
76
|
+
alignItems: 'center',
|
|
77
|
+
justifyContent: 'center',
|
|
78
|
+
},
|
|
79
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { TouchableOpacity, View } from 'react-native';
|
|
3
|
+
import { useAppDesignTokens } from '../theme';
|
|
4
|
+
import { AtomicText, AtomicIcon } from '../atoms';
|
|
5
|
+
import { ListItemProps } from './listitem/types';
|
|
6
|
+
import { getListItemStyles } from './listitem/styles/listItemStyles';
|
|
7
|
+
|
|
8
|
+
export type { ListItemProps };
|
|
9
|
+
|
|
10
|
+
export const ListItem: React.FC<ListItemProps> = ({
|
|
11
|
+
title, subtitle, leftIcon, rightIcon, onPress, disabled = false, style,
|
|
12
|
+
}) => {
|
|
13
|
+
const tokens = useAppDesignTokens();
|
|
14
|
+
const listItemStyles = getListItemStyles(tokens);
|
|
15
|
+
const Component = onPress ? TouchableOpacity : View;
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<Component style={[listItemStyles.container, disabled ? listItemStyles.disabled : undefined, style]} onPress={onPress} disabled={disabled} activeOpacity={0.7}>
|
|
19
|
+
{leftIcon && (
|
|
20
|
+
<View style={listItemStyles.iconContainer}>
|
|
21
|
+
<AtomicIcon name={leftIcon} color={disabled ? 'surfaceVariant' : 'primary'} />
|
|
22
|
+
</View>
|
|
23
|
+
)}
|
|
24
|
+
<View style={listItemStyles.content}>
|
|
25
|
+
<AtomicText type="bodyLarge" color={disabled ? 'surfaceVariant' : 'onSurface'} numberOfLines={1}>{title}</AtomicText>
|
|
26
|
+
{subtitle && <AtomicText type="bodySmall" color="surfaceVariant" numberOfLines={2} style={listItemStyles.subtitle}>{subtitle}</AtomicText>}
|
|
27
|
+
</View>
|
|
28
|
+
{rightIcon && onPress && (
|
|
29
|
+
<View style={listItemStyles.iconContainer}>
|
|
30
|
+
<AtomicIcon name={rightIcon} color="surfaceVariant" />
|
|
31
|
+
</View>
|
|
32
|
+
)}
|
|
33
|
+
</Component>
|
|
34
|
+
);
|
|
35
|
+
};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ScreenHeader Component
|
|
3
|
+
*
|
|
4
|
+
* Reusable screen header with consistent back button placement
|
|
5
|
+
*
|
|
6
|
+
* Features:
|
|
7
|
+
* - Top-left back button (configurable icon)
|
|
8
|
+
* - Centered title text
|
|
9
|
+
* - Optional right action button
|
|
10
|
+
* - Consistent spacing and layout
|
|
11
|
+
* - Theme-aware styling
|
|
12
|
+
* - Fully configurable for general purpose use
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import React from 'react';
|
|
16
|
+
import { View, TouchableOpacity, ViewStyle } from 'react-native';
|
|
17
|
+
import { AtomicIcon, AtomicText } from '../atoms';
|
|
18
|
+
import { useAppDesignTokens } from '../theme';
|
|
19
|
+
|
|
20
|
+
export interface ScreenHeaderProps {
|
|
21
|
+
/** Screen title (centered) */
|
|
22
|
+
title: string;
|
|
23
|
+
|
|
24
|
+
/** Optional right action button */
|
|
25
|
+
rightAction?: React.ReactNode;
|
|
26
|
+
|
|
27
|
+
/** Custom back button action (required if back button is visible) */
|
|
28
|
+
onBackPress?: () => void;
|
|
29
|
+
|
|
30
|
+
/** Hide back button (rare cases only) */
|
|
31
|
+
hideBackButton?: boolean;
|
|
32
|
+
|
|
33
|
+
/** Additional header style */
|
|
34
|
+
style?: ViewStyle;
|
|
35
|
+
|
|
36
|
+
/** Test ID for E2E testing */
|
|
37
|
+
testID?: string;
|
|
38
|
+
|
|
39
|
+
/** Custom back button icon name */
|
|
40
|
+
backIconName?: string;
|
|
41
|
+
|
|
42
|
+
/** Custom back button icon color */
|
|
43
|
+
backIconColor: 'primary' | 'secondary' | 'error' | 'warning' | 'success' | 'surfaceVariant';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* ScreenHeader Component
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* // Basic usage (most common)
|
|
51
|
+
* <ScreenHeader title="Settings" />
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* // With right action
|
|
55
|
+
* <ScreenHeader
|
|
56
|
+
* title="Edit Profile"
|
|
57
|
+
* rightAction={<TouchableOpacity onPress={handleSave}><Text>Save</Text></TouchableOpacity>}
|
|
58
|
+
* />
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* // Custom back action
|
|
62
|
+
* <ScreenHeader
|
|
63
|
+
* title="Unsaved Changes"
|
|
64
|
+
* onBackPress={handleUnsavedChanges}
|
|
65
|
+
* />
|
|
66
|
+
*/
|
|
67
|
+
const ScreenHeaderBackButton: React.FC<{
|
|
68
|
+
hideBackButton?: boolean;
|
|
69
|
+
onBackPress?: () => void;
|
|
70
|
+
backIconName?: string;
|
|
71
|
+
backIconColor?: 'primary' | 'secondary' | 'error' | 'warning' | 'success' | 'surfaceVariant';
|
|
72
|
+
testID?: string;
|
|
73
|
+
}> = ({ hideBackButton, onBackPress, backIconName, backIconColor, testID }) => {
|
|
74
|
+
const handleBackPress = React.useCallback(() => {
|
|
75
|
+
if (onBackPress) {
|
|
76
|
+
onBackPress();
|
|
77
|
+
} else {
|
|
78
|
+
__DEV__ && console.warn('ScreenHeader: onBackPress is required when back button is visible');
|
|
79
|
+
}
|
|
80
|
+
}, [onBackPress]);
|
|
81
|
+
|
|
82
|
+
if (hideBackButton) return null;
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<View style={{ width: 40, alignItems: 'flex-start' }}>
|
|
86
|
+
<TouchableOpacity
|
|
87
|
+
onPress={handleBackPress}
|
|
88
|
+
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
|
89
|
+
testID={`${testID}-back-button`}
|
|
90
|
+
>
|
|
91
|
+
<AtomicIcon name={backIconName || 'ArrowLeft'} color={backIconColor} />
|
|
92
|
+
</TouchableOpacity>
|
|
93
|
+
</View>
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const ScreenHeaderTitle: React.FC<{
|
|
98
|
+
title: string;
|
|
99
|
+
tokens: ReturnType<typeof useAppDesignTokens>;
|
|
100
|
+
testID?: string;
|
|
101
|
+
}> = ({ title, tokens, testID }) => (
|
|
102
|
+
<View style={{ flex: 1, alignItems: 'center', paddingHorizontal: tokens.spacing.sm }}>
|
|
103
|
+
<AtomicText
|
|
104
|
+
type="headlineMedium"
|
|
105
|
+
style={[
|
|
106
|
+
{
|
|
107
|
+
fontWeight: tokens.typography.bold,
|
|
108
|
+
textAlign: 'center',
|
|
109
|
+
color: tokens.colors.textPrimary,
|
|
110
|
+
}
|
|
111
|
+
]}
|
|
112
|
+
numberOfLines={1}
|
|
113
|
+
testID={`${testID}-title`}
|
|
114
|
+
>
|
|
115
|
+
{title}
|
|
116
|
+
</AtomicText>
|
|
117
|
+
</View>
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const ScreenHeaderRightAction: React.FC<{
|
|
121
|
+
rightAction?: React.ReactNode;
|
|
122
|
+
}> = ({ rightAction }) => (
|
|
123
|
+
<View style={{ width: 40, alignItems: 'flex-start' }}>
|
|
124
|
+
{rightAction || <View style={{ width: 40 }} />}
|
|
125
|
+
</View>
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
export const ScreenHeader: React.FC<ScreenHeaderProps> = ({
|
|
129
|
+
title,
|
|
130
|
+
rightAction,
|
|
131
|
+
onBackPress,
|
|
132
|
+
hideBackButton = false,
|
|
133
|
+
style,
|
|
134
|
+
testID = 'screen-header',
|
|
135
|
+
backIconName = 'ArrowLeft',
|
|
136
|
+
backIconColor = 'primary',
|
|
137
|
+
}) => {
|
|
138
|
+
const tokens = useAppDesignTokens();
|
|
139
|
+
|
|
140
|
+
const headerStyle = React.useMemo(() => [
|
|
141
|
+
{
|
|
142
|
+
flexDirection: 'row' as const,
|
|
143
|
+
alignItems: 'center' as const,
|
|
144
|
+
justifyContent: 'space-between' as const,
|
|
145
|
+
paddingHorizontal: tokens.spacing.screenPadding,
|
|
146
|
+
paddingVertical: tokens.spacing.md,
|
|
147
|
+
borderBottomWidth: 0.5,
|
|
148
|
+
backgroundColor: tokens.colors.backgroundPrimary,
|
|
149
|
+
borderBottomColor: tokens.colors.border,
|
|
150
|
+
},
|
|
151
|
+
style
|
|
152
|
+
], [tokens, style]);
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<View style={headerStyle} testID={testID}>
|
|
156
|
+
<ScreenHeaderBackButton
|
|
157
|
+
hideBackButton={hideBackButton}
|
|
158
|
+
onBackPress={onBackPress}
|
|
159
|
+
backIconName={backIconName}
|
|
160
|
+
backIconColor={backIconColor}
|
|
161
|
+
testID={testID}
|
|
162
|
+
/>
|
|
163
|
+
|
|
164
|
+
<ScreenHeaderTitle title={title} tokens={tokens} testID={testID} />
|
|
165
|
+
|
|
166
|
+
<ScreenHeaderRightAction rightAction={rightAction} />
|
|
167
|
+
</View>
|
|
168
|
+
);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
export default ScreenHeader;
|