@umituz/react-native-design-system 1.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/LICENSE +21 -0
- package/README.md +157 -0
- package/package.json +43 -0
- package/src/index.ts +345 -0
- package/src/presentation/atoms/AtomicAvatar.tsx +157 -0
- package/src/presentation/atoms/AtomicAvatarGroup.tsx +169 -0
- package/src/presentation/atoms/AtomicBadge.tsx +232 -0
- package/src/presentation/atoms/AtomicButton.tsx +124 -0
- package/src/presentation/atoms/AtomicCard.tsx +112 -0
- package/src/presentation/atoms/AtomicChip.tsx +223 -0
- package/src/presentation/atoms/AtomicDatePicker.tsx +347 -0
- package/src/presentation/atoms/AtomicDivider.tsx +114 -0
- package/src/presentation/atoms/AtomicFab.tsx +104 -0
- package/src/presentation/atoms/AtomicFilter.tsx +154 -0
- package/src/presentation/atoms/AtomicFormError.tsx +105 -0
- package/src/presentation/atoms/AtomicIcon.tsx +29 -0
- package/src/presentation/atoms/AtomicImage.tsx +149 -0
- package/src/presentation/atoms/AtomicInput.tsx +232 -0
- package/src/presentation/atoms/AtomicNumberInput.tsx +182 -0
- package/src/presentation/atoms/AtomicPicker.tsx +458 -0
- package/src/presentation/atoms/AtomicProgress.tsx +143 -0
- package/src/presentation/atoms/AtomicSearchBar.tsx +114 -0
- package/src/presentation/atoms/AtomicSkeleton.tsx +146 -0
- package/src/presentation/atoms/AtomicSort.tsx +145 -0
- package/src/presentation/atoms/AtomicSwitch.tsx +166 -0
- package/src/presentation/atoms/AtomicText.tsx +50 -0
- package/src/presentation/atoms/AtomicTextArea.tsx +198 -0
- package/src/presentation/atoms/AtomicTouchable.tsx +233 -0
- package/src/presentation/atoms/fab/styles/fabStyles.ts +69 -0
- package/src/presentation/atoms/fab/types/index.ts +88 -0
- package/src/presentation/atoms/filter/styles/filterStyles.ts +32 -0
- package/src/presentation/atoms/filter/types/index.ts +89 -0
- package/src/presentation/atoms/index.ts +378 -0
- package/src/presentation/atoms/input/hooks/useInputState.ts +15 -0
- package/src/presentation/atoms/input/styles/inputStyles.ts +66 -0
- package/src/presentation/atoms/input/types/index.ts +25 -0
- package/src/presentation/atoms/picker/styles/pickerStyles.ts +200 -0
- package/src/presentation/atoms/picker/types/index.ts +40 -0
- package/src/presentation/atoms/touchable/styles/touchableStyles.ts +71 -0
- package/src/presentation/atoms/touchable/types/index.ts +162 -0
- package/src/presentation/hooks/useAppDesignTokens.ts +78 -0
- package/src/presentation/hooks/useResponsive.ts +180 -0
- package/src/presentation/loading/index.ts +40 -0
- package/src/presentation/loading/presentation/components/LoadingSpinner.tsx +116 -0
- package/src/presentation/loading/presentation/components/LoadingState.tsx +200 -0
- package/src/presentation/loading/presentation/hooks/useLoading.ts +100 -0
- package/src/presentation/molecules/AtomicConfirmationModal.tsx +263 -0
- package/src/presentation/molecules/EmptyState.tsx +130 -0
- package/src/presentation/molecules/FormField.tsx +128 -0
- package/src/presentation/molecules/GridContainer.tsx +124 -0
- package/src/presentation/molecules/IconContainer.tsx +94 -0
- package/src/presentation/molecules/LanguageSwitcher.tsx +42 -0
- package/src/presentation/molecules/ListItem.tsx +36 -0
- package/src/presentation/molecules/ScreenHeader.tsx +140 -0
- package/src/presentation/molecules/SearchBar.tsx +85 -0
- package/src/presentation/molecules/SectionCard.tsx +74 -0
- package/src/presentation/molecules/SectionContainer.tsx +106 -0
- package/src/presentation/molecules/SectionHeader.tsx +125 -0
- package/src/presentation/molecules/confirmation-modal/styles/confirmationModalStyles.ts +133 -0
- package/src/presentation/molecules/confirmation-modal/types/index.ts +107 -0
- package/src/presentation/molecules/index.ts +42 -0
- package/src/presentation/molecules/languageswitcher/config/languageSwitcherConfig.ts +5 -0
- package/src/presentation/molecules/languageswitcher/hooks/useLanguageNavigation.ts +15 -0
- package/src/presentation/molecules/listitem/styles/listItemStyles.ts +19 -0
- package/src/presentation/molecules/listitem/types/index.ts +17 -0
- package/src/presentation/organisms/AppHeader.tsx +136 -0
- package/src/presentation/organisms/FormContainer.tsx +180 -0
- package/src/presentation/organisms/ScreenLayout.tsx +209 -0
- package/src/presentation/organisms/index.ts +25 -0
- package/src/presentation/tokens/AppDesignTokens.ts +57 -0
- package/src/presentation/tokens/commonStyles.ts +253 -0
- package/src/presentation/tokens/core/BaseTokens.ts +394 -0
- package/src/presentation/tokens/core/ColorPalette.ts +398 -0
- package/src/presentation/tokens/core/TokenFactory.ts +120 -0
- package/src/presentation/utils/platformConstants.ts +124 -0
- package/src/presentation/utils/responsive.ts +516 -0
- package/src/presentation/utils/variants/compound.ts +29 -0
- package/src/presentation/utils/variants/core.ts +39 -0
- package/src/presentation/utils/variants/helpers.ts +13 -0
- package/src/presentation/utils/variants.ts +3 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LoadingState - Dynamic Icon-Based Loading Component
|
|
3
|
+
*
|
|
4
|
+
* Universal loading component with configurable emoji/icon support
|
|
5
|
+
* Inspired by meditation_timer's breathing animation pattern
|
|
6
|
+
* Theme: {{THEME_NAME}} ({{CATEGORY}} category)
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - ✅ Dynamic emoji/icon per screen (🏠 Home, ⚙️ Settings, 💪 Workout, etc.)
|
|
10
|
+
* - ✅ Breathing animation effect (scale 1 → 1.15 → 1)
|
|
11
|
+
* - ✅ Size variants (small, medium, large)
|
|
12
|
+
* - ✅ Full screen or inline modes
|
|
13
|
+
* - ✅ Optional loading message
|
|
14
|
+
* - ✅ Theme-aware styling
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import React, { useRef, useEffect, useMemo } from 'react';
|
|
18
|
+
import {
|
|
19
|
+
View,
|
|
20
|
+
StyleSheet,
|
|
21
|
+
Animated,
|
|
22
|
+
Easing,
|
|
23
|
+
} from 'react-native';
|
|
24
|
+
import { useAppDesignTokens } from '../../../hooks/useAppDesignTokens';
|
|
25
|
+
import { STATIC_TOKENS } from '../../../tokens/AppDesignTokens';
|
|
26
|
+
import { AtomicText } from '../../../atoms/AtomicText';
|
|
27
|
+
|
|
28
|
+
// =============================================================================
|
|
29
|
+
// TYPE DEFINITIONS
|
|
30
|
+
// =============================================================================
|
|
31
|
+
|
|
32
|
+
export type LoadingStateSize = 'small' | 'medium' | 'large';
|
|
33
|
+
|
|
34
|
+
export interface LoadingStateProps {
|
|
35
|
+
/**
|
|
36
|
+
* Emoji/icon to display (changes per screen context)
|
|
37
|
+
* Examples: 🏠 Home, ⚙️ Settings, 💪 Workout, 🧘 Meditation, 📊 Analytics
|
|
38
|
+
*/
|
|
39
|
+
icon?: string;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Optional loading message
|
|
43
|
+
*/
|
|
44
|
+
message?: string;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Size variant
|
|
48
|
+
*/
|
|
49
|
+
size?: LoadingStateSize;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Full screen overlay mode
|
|
53
|
+
*/
|
|
54
|
+
fullScreen?: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// =============================================================================
|
|
58
|
+
// SIZE CONFIGURATION
|
|
59
|
+
// =============================================================================
|
|
60
|
+
|
|
61
|
+
interface SizeConfig {
|
|
62
|
+
iconSize: number;
|
|
63
|
+
showMessage: boolean;
|
|
64
|
+
containerPadding: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const SIZE_CONFIG: Record<LoadingStateSize, SizeConfig> = {
|
|
68
|
+
small: {
|
|
69
|
+
iconSize: 32,
|
|
70
|
+
showMessage: false,
|
|
71
|
+
containerPadding: 16,
|
|
72
|
+
},
|
|
73
|
+
medium: {
|
|
74
|
+
iconSize: 48,
|
|
75
|
+
showMessage: true,
|
|
76
|
+
containerPadding: 24,
|
|
77
|
+
},
|
|
78
|
+
large: {
|
|
79
|
+
iconSize: 64,
|
|
80
|
+
showMessage: true,
|
|
81
|
+
containerPadding: 32,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// =============================================================================
|
|
86
|
+
// COMPONENT IMPLEMENTATION
|
|
87
|
+
// =============================================================================
|
|
88
|
+
|
|
89
|
+
export const LoadingState: React.FC<LoadingStateProps> = ({
|
|
90
|
+
icon = '⏳', // Default hourglass icon
|
|
91
|
+
message,
|
|
92
|
+
size = 'large',
|
|
93
|
+
fullScreen = false,
|
|
94
|
+
}) => {
|
|
95
|
+
// ✅ Dynamic theme tokens
|
|
96
|
+
const tokens = useAppDesignTokens();
|
|
97
|
+
|
|
98
|
+
// Animation ref for breathing effect
|
|
99
|
+
const scaleAnim = useRef(new Animated.Value(1)).current;
|
|
100
|
+
|
|
101
|
+
// Size configuration
|
|
102
|
+
const config = SIZE_CONFIG[size];
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Breathing Animation Effect
|
|
106
|
+
* Smoothly scales icon from 1 → 1.15 → 1 in continuous loop
|
|
107
|
+
* Creates calming, natural breathing sensation
|
|
108
|
+
*/
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
const breathingAnimation = Animated.loop(
|
|
111
|
+
Animated.sequence([
|
|
112
|
+
// Expand (inhale)
|
|
113
|
+
Animated.timing(scaleAnim, {
|
|
114
|
+
toValue: 1.15,
|
|
115
|
+
duration: tokens.animations.slowest,
|
|
116
|
+
easing: Easing.inOut(Easing.ease),
|
|
117
|
+
useNativeDriver: true,
|
|
118
|
+
}),
|
|
119
|
+
// Contract (exhale)
|
|
120
|
+
Animated.timing(scaleAnim, {
|
|
121
|
+
toValue: 1,
|
|
122
|
+
duration: tokens.animations.slowest,
|
|
123
|
+
easing: Easing.inOut(Easing.ease),
|
|
124
|
+
useNativeDriver: true,
|
|
125
|
+
}),
|
|
126
|
+
])
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
breathingAnimation.start();
|
|
130
|
+
|
|
131
|
+
return () => {
|
|
132
|
+
breathingAnimation.stop();
|
|
133
|
+
};
|
|
134
|
+
}, [scaleAnim]);
|
|
135
|
+
|
|
136
|
+
// Dynamic styles based on theme
|
|
137
|
+
const styles = useMemo(() => getStyles(tokens, config, fullScreen), [tokens, config, fullScreen]);
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<View style={styles.container}>
|
|
141
|
+
{/* Animated Icon/Emoji */}
|
|
142
|
+
<Animated.Text
|
|
143
|
+
style={[
|
|
144
|
+
styles.icon,
|
|
145
|
+
{
|
|
146
|
+
fontSize: config.iconSize,
|
|
147
|
+
transform: [{ scale: scaleAnim }],
|
|
148
|
+
},
|
|
149
|
+
]}
|
|
150
|
+
>
|
|
151
|
+
{icon}
|
|
152
|
+
</Animated.Text>
|
|
153
|
+
|
|
154
|
+
{/* Optional Loading Message */}
|
|
155
|
+
{config.showMessage && message && (
|
|
156
|
+
<AtomicText
|
|
157
|
+
type="bodyMedium"
|
|
158
|
+
style={styles.message}
|
|
159
|
+
>
|
|
160
|
+
{message}
|
|
161
|
+
</AtomicText>
|
|
162
|
+
)}
|
|
163
|
+
</View>
|
|
164
|
+
);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// =============================================================================
|
|
168
|
+
// STYLES
|
|
169
|
+
// =============================================================================
|
|
170
|
+
|
|
171
|
+
const getStyles = (
|
|
172
|
+
tokens: ReturnType<typeof useAppDesignTokens>,
|
|
173
|
+
config: SizeConfig,
|
|
174
|
+
fullScreen: boolean
|
|
175
|
+
) => StyleSheet.create({
|
|
176
|
+
container: {
|
|
177
|
+
...(fullScreen ? {
|
|
178
|
+
flex: 1,
|
|
179
|
+
justifyContent: 'center',
|
|
180
|
+
alignItems: 'center',
|
|
181
|
+
backgroundColor: tokens.colors.backgroundPrimary,
|
|
182
|
+
} : {
|
|
183
|
+
justifyContent: 'center',
|
|
184
|
+
alignItems: 'center',
|
|
185
|
+
padding: config.containerPadding,
|
|
186
|
+
}),
|
|
187
|
+
},
|
|
188
|
+
icon: {
|
|
189
|
+
textAlign: 'center',
|
|
190
|
+
marginBottom: tokens.spacing.md,
|
|
191
|
+
},
|
|
192
|
+
message: {
|
|
193
|
+
color: tokens.colors.textSecondary,
|
|
194
|
+
textAlign: 'center',
|
|
195
|
+
marginTop: tokens.spacing.sm,
|
|
196
|
+
maxWidth: 300,
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
export default LoadingState;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useLoading - Loading State Management Hook
|
|
3
|
+
*
|
|
4
|
+
* Centralized hook for managing loading states across the application
|
|
5
|
+
* Theme: {{THEME_NAME}} ({{CATEGORY}} category)
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - ✅ Simple boolean loading state
|
|
9
|
+
* - ✅ Message management
|
|
10
|
+
* - ✅ Icon configuration per context
|
|
11
|
+
* - ✅ Type-safe loading control
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { useState, useCallback } from 'react';
|
|
15
|
+
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// TYPE DEFINITIONS
|
|
18
|
+
// =============================================================================
|
|
19
|
+
|
|
20
|
+
export interface LoadingConfig {
|
|
21
|
+
isLoading: boolean;
|
|
22
|
+
message?: string;
|
|
23
|
+
icon?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface UseLoadingReturn {
|
|
27
|
+
/**
|
|
28
|
+
* Current loading state
|
|
29
|
+
*/
|
|
30
|
+
isLoading: boolean;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Current loading message
|
|
34
|
+
*/
|
|
35
|
+
message: string | undefined;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Current loading icon
|
|
39
|
+
*/
|
|
40
|
+
icon: string | undefined;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Start loading with optional message and icon
|
|
44
|
+
*/
|
|
45
|
+
startLoading: (message?: string, icon?: string) => void;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Stop loading and clear message
|
|
49
|
+
*/
|
|
50
|
+
stopLoading: () => void;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Update loading message without affecting state
|
|
54
|
+
*/
|
|
55
|
+
setMessage: (message: string | undefined) => void;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Update loading icon without affecting state
|
|
59
|
+
*/
|
|
60
|
+
setIcon: (icon: string | undefined) => void;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// =============================================================================
|
|
64
|
+
// HOOK IMPLEMENTATION
|
|
65
|
+
// =============================================================================
|
|
66
|
+
|
|
67
|
+
export const useLoading = (initialConfig?: LoadingConfig): UseLoadingReturn => {
|
|
68
|
+
const [isLoading, setIsLoading] = useState(initialConfig?.isLoading ?? false);
|
|
69
|
+
const [message, setMessage] = useState<string | undefined>(initialConfig?.message);
|
|
70
|
+
const [icon, setIcon] = useState<string | undefined>(initialConfig?.icon);
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Start loading with optional message and icon
|
|
74
|
+
*/
|
|
75
|
+
const startLoading = useCallback((msg?: string, ico?: string) => {
|
|
76
|
+
setIsLoading(true);
|
|
77
|
+
if (msg !== undefined) setMessage(msg);
|
|
78
|
+
if (ico !== undefined) setIcon(ico);
|
|
79
|
+
}, []);
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Stop loading and clear message
|
|
83
|
+
*/
|
|
84
|
+
const stopLoading = useCallback(() => {
|
|
85
|
+
setIsLoading(false);
|
|
86
|
+
setMessage(undefined);
|
|
87
|
+
}, []);
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
isLoading,
|
|
91
|
+
message,
|
|
92
|
+
icon,
|
|
93
|
+
startLoading,
|
|
94
|
+
stopLoading,
|
|
95
|
+
setMessage,
|
|
96
|
+
setIcon,
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export default useLoading;
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AtomicConfirmationModal - Universal Confirmation Dialog
|
|
3
|
+
*
|
|
4
|
+
* A reusable confirmation modal for destructive and important actions.
|
|
5
|
+
* Follows Material Design 3 dialog patterns and accessibility guidelines.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Multiple variants (default, destructive, warning, success)
|
|
9
|
+
* - Configurable text and icons
|
|
10
|
+
* - Backdrop dismissal
|
|
11
|
+
* - Full keyboard and screen reader support
|
|
12
|
+
* - Theme-aware styling
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```tsx
|
|
16
|
+
* // Destructive confirmation (delete)
|
|
17
|
+
* <AtomicConfirmationModal
|
|
18
|
+
* visible={showDeleteModal}
|
|
19
|
+
* variant="destructive"
|
|
20
|
+
* title="Delete Item?"
|
|
21
|
+
* message="This action cannot be undone. All data will be permanently deleted."
|
|
22
|
+
* confirmText="Delete"
|
|
23
|
+
* cancelText="Cancel"
|
|
24
|
+
* onConfirm={handleDelete}
|
|
25
|
+
* onCancel={() => setShowDeleteModal(false)}
|
|
26
|
+
* />
|
|
27
|
+
*
|
|
28
|
+
* // Generic confirmation
|
|
29
|
+
* <AtomicConfirmationModal
|
|
30
|
+
* visible={showConfirmModal}
|
|
31
|
+
* variant="default"
|
|
32
|
+
* title="Confirm Action"
|
|
33
|
+
* message="Are you sure you want to proceed?"
|
|
34
|
+
* onConfirm={handleConfirm}
|
|
35
|
+
* onCancel={() => setShowConfirmModal(false)}
|
|
36
|
+
* />
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import React from 'react';
|
|
41
|
+
import { View, Modal, TouchableOpacity } from 'react-native';
|
|
42
|
+
import { useAppDesignTokens } from '../hooks/useAppDesignTokens';
|
|
43
|
+
import { useLocalization } from '@domains/localization';
|
|
44
|
+
import { AtomicText } from '../atoms/AtomicText';
|
|
45
|
+
import { AtomicButton } from '../atoms/AtomicButton';
|
|
46
|
+
import { AtomicIcon } from '../atoms/AtomicIcon';
|
|
47
|
+
import {
|
|
48
|
+
AtomicConfirmationModalProps,
|
|
49
|
+
ConfirmationModalVariant,
|
|
50
|
+
} from './confirmation-modal/types';
|
|
51
|
+
import {
|
|
52
|
+
getVariantConfig,
|
|
53
|
+
getModalOverlayStyle,
|
|
54
|
+
getBackdropStyle,
|
|
55
|
+
getModalContainerStyle,
|
|
56
|
+
getIconContainerStyle,
|
|
57
|
+
getTitleContainerStyle,
|
|
58
|
+
getMessageContainerStyle,
|
|
59
|
+
getButtonContainerStyle,
|
|
60
|
+
getButtonStyle,
|
|
61
|
+
} from './confirmation-modal/styles/confirmationModalStyles';
|
|
62
|
+
|
|
63
|
+
export type { AtomicConfirmationModalProps };
|
|
64
|
+
export type { ConfirmationModalVariant };
|
|
65
|
+
|
|
66
|
+
export const AtomicConfirmationModal: React.FC<AtomicConfirmationModalProps> = ({
|
|
67
|
+
visible,
|
|
68
|
+
title,
|
|
69
|
+
message,
|
|
70
|
+
variant = 'default',
|
|
71
|
+
confirmText,
|
|
72
|
+
cancelText,
|
|
73
|
+
icon,
|
|
74
|
+
onConfirm,
|
|
75
|
+
onCancel,
|
|
76
|
+
showBackdrop = true,
|
|
77
|
+
backdropDismissible = true,
|
|
78
|
+
style,
|
|
79
|
+
testID = 'atomic-confirmation-modal',
|
|
80
|
+
}) => {
|
|
81
|
+
const tokens = useAppDesignTokens();
|
|
82
|
+
const { t } = useLocalization();
|
|
83
|
+
|
|
84
|
+
// Get variant-specific configuration (icon and color only)
|
|
85
|
+
const variantConfig = getVariantConfig(variant, tokens);
|
|
86
|
+
|
|
87
|
+
// Get locale-aware default text based on variant
|
|
88
|
+
const getDefaultConfirmText = (): string => {
|
|
89
|
+
switch (variant) {
|
|
90
|
+
case 'destructive':
|
|
91
|
+
return t('general.delete');
|
|
92
|
+
case 'warning':
|
|
93
|
+
return t('general.continue');
|
|
94
|
+
case 'success':
|
|
95
|
+
case 'default':
|
|
96
|
+
default:
|
|
97
|
+
return t('general.confirm');
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Determine final text values
|
|
102
|
+
const finalConfirmText = confirmText || getDefaultConfirmText();
|
|
103
|
+
const finalCancelText = cancelText || t('general.cancel');
|
|
104
|
+
|
|
105
|
+
// Determine final icon
|
|
106
|
+
const finalIcon = icon || variantConfig.icon;
|
|
107
|
+
|
|
108
|
+
// Handle backdrop press
|
|
109
|
+
const handleBackdropPress = () => {
|
|
110
|
+
if (backdropDismissible) {
|
|
111
|
+
onCancel();
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<Modal
|
|
117
|
+
visible={visible}
|
|
118
|
+
transparent
|
|
119
|
+
animationType="fade"
|
|
120
|
+
onRequestClose={onCancel}
|
|
121
|
+
statusBarTranslucent
|
|
122
|
+
testID={testID}
|
|
123
|
+
>
|
|
124
|
+
<View style={getModalOverlayStyle(tokens)}>
|
|
125
|
+
{/* Backdrop - Tap to dismiss if enabled */}
|
|
126
|
+
{showBackdrop && (
|
|
127
|
+
<TouchableOpacity
|
|
128
|
+
style={getBackdropStyle()}
|
|
129
|
+
activeOpacity={1}
|
|
130
|
+
onPress={handleBackdropPress}
|
|
131
|
+
testID={`${testID}-backdrop`}
|
|
132
|
+
/>
|
|
133
|
+
)}
|
|
134
|
+
|
|
135
|
+
{/* Modal Container */}
|
|
136
|
+
<View style={[getModalContainerStyle(tokens), style]}>
|
|
137
|
+
{/* Icon */}
|
|
138
|
+
<View style={getIconContainerStyle(tokens)}>
|
|
139
|
+
<AtomicIcon
|
|
140
|
+
name={finalIcon}
|
|
141
|
+
size="xl"
|
|
142
|
+
color={variantConfig.iconColor}
|
|
143
|
+
testID={`${testID}-icon`}
|
|
144
|
+
/>
|
|
145
|
+
</View>
|
|
146
|
+
|
|
147
|
+
{/* Title */}
|
|
148
|
+
<View style={getTitleContainerStyle(tokens)}>
|
|
149
|
+
<AtomicText
|
|
150
|
+
type="titleLarge"
|
|
151
|
+
style={{
|
|
152
|
+
color: tokens.colors.textPrimary,
|
|
153
|
+
textAlign: 'center',
|
|
154
|
+
fontWeight: tokens.typography.bold,
|
|
155
|
+
}}
|
|
156
|
+
testID={`${testID}-title`}
|
|
157
|
+
>
|
|
158
|
+
{title}
|
|
159
|
+
</AtomicText>
|
|
160
|
+
</View>
|
|
161
|
+
|
|
162
|
+
{/* Message */}
|
|
163
|
+
<View style={getMessageContainerStyle(tokens)}>
|
|
164
|
+
<AtomicText
|
|
165
|
+
type="bodyMedium"
|
|
166
|
+
style={{
|
|
167
|
+
color: tokens.colors.textSecondary,
|
|
168
|
+
textAlign: 'center',
|
|
169
|
+
lineHeight: tokens.typography.bodyMedium.lineHeight,
|
|
170
|
+
}}
|
|
171
|
+
testID={`${testID}-message`}
|
|
172
|
+
>
|
|
173
|
+
{message}
|
|
174
|
+
</AtomicText>
|
|
175
|
+
</View>
|
|
176
|
+
|
|
177
|
+
{/* Action Buttons */}
|
|
178
|
+
<View style={getButtonContainerStyle(tokens)}>
|
|
179
|
+
{/* Cancel Button */}
|
|
180
|
+
<AtomicButton
|
|
181
|
+
variant="outline"
|
|
182
|
+
size="md"
|
|
183
|
+
onPress={onCancel}
|
|
184
|
+
style={getButtonStyle()}
|
|
185
|
+
testID={`${testID}-cancel-button`}
|
|
186
|
+
>
|
|
187
|
+
{finalCancelText}
|
|
188
|
+
</AtomicButton>
|
|
189
|
+
|
|
190
|
+
{/* Confirm Button */}
|
|
191
|
+
<AtomicButton
|
|
192
|
+
variant={variant === 'destructive' ? 'primary' : 'primary'}
|
|
193
|
+
size="md"
|
|
194
|
+
onPress={onConfirm}
|
|
195
|
+
style={[
|
|
196
|
+
getButtonStyle(),
|
|
197
|
+
...(variant === 'destructive' ? [{ backgroundColor: tokens.colors.error }] : []),
|
|
198
|
+
...(variant === 'warning' ? [{ backgroundColor: tokens.colors.warning }] : []),
|
|
199
|
+
...(variant === 'success' ? [{ backgroundColor: tokens.colors.success }] : []),
|
|
200
|
+
]}
|
|
201
|
+
testID={`${testID}-confirm-button`}
|
|
202
|
+
>
|
|
203
|
+
{finalConfirmText}
|
|
204
|
+
</AtomicButton>
|
|
205
|
+
</View>
|
|
206
|
+
</View>
|
|
207
|
+
</View>
|
|
208
|
+
</Modal>
|
|
209
|
+
);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Hook for managing confirmation modal state
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* ```tsx
|
|
217
|
+
* const { showConfirmation, confirmationProps } = useConfirmationModal({
|
|
218
|
+
* title: 'Delete Item?',
|
|
219
|
+
* message: 'This cannot be undone',
|
|
220
|
+
* variant: 'destructive',
|
|
221
|
+
* onConfirm: handleDelete,
|
|
222
|
+
* });
|
|
223
|
+
*
|
|
224
|
+
* // In JSX
|
|
225
|
+
* <AtomicConfirmationModal {...confirmationProps} />
|
|
226
|
+
* <Button onPress={showConfirmation}>Delete</Button>
|
|
227
|
+
* ```
|
|
228
|
+
*/
|
|
229
|
+
export const useConfirmationModal = (config: {
|
|
230
|
+
title: string;
|
|
231
|
+
message: string;
|
|
232
|
+
variant?: ConfirmationModalVariant;
|
|
233
|
+
confirmText?: string;
|
|
234
|
+
cancelText?: string;
|
|
235
|
+
onConfirm: () => void;
|
|
236
|
+
}) => {
|
|
237
|
+
const [visible, setVisible] = React.useState(false);
|
|
238
|
+
|
|
239
|
+
const showConfirmation = () => setVisible(true);
|
|
240
|
+
const hideConfirmation = () => setVisible(false);
|
|
241
|
+
|
|
242
|
+
const handleConfirm = () => {
|
|
243
|
+
config.onConfirm();
|
|
244
|
+
hideConfirmation();
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const confirmationProps: AtomicConfirmationModalProps = {
|
|
248
|
+
visible,
|
|
249
|
+
title: config.title,
|
|
250
|
+
message: config.message,
|
|
251
|
+
variant: config.variant || 'default',
|
|
252
|
+
confirmText: config.confirmText,
|
|
253
|
+
cancelText: config.cancelText,
|
|
254
|
+
onConfirm: handleConfirm,
|
|
255
|
+
onCancel: hideConfirmation,
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
showConfirmation,
|
|
260
|
+
hideConfirmation,
|
|
261
|
+
confirmationProps,
|
|
262
|
+
};
|
|
263
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EmptyState Molecule - Universal Empty State Display
|
|
3
|
+
*
|
|
4
|
+
* Displays icon, title, and subtitle for empty data scenarios
|
|
5
|
+
* Theme: {{THEME_NAME}} ({{CATEGORY}} category)
|
|
6
|
+
*
|
|
7
|
+
* Atomic Design Level: MOLECULE
|
|
8
|
+
* Composition: Icon + AtomicText + Layout
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* - Empty lists
|
|
12
|
+
* - Empty grids
|
|
13
|
+
* - No search results
|
|
14
|
+
* - No data states
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import React from 'react';
|
|
18
|
+
import { View, StyleSheet, ViewStyle, TextStyle } from 'react-native';
|
|
19
|
+
import { AtomicText, AtomicIcon } from '../atoms';
|
|
20
|
+
import { useAppDesignTokens } from '../hooks/useAppDesignTokens';
|
|
21
|
+
|
|
22
|
+
// =============================================================================
|
|
23
|
+
// TYPE DEFINITIONS
|
|
24
|
+
// =============================================================================
|
|
25
|
+
|
|
26
|
+
export interface EmptyStateProps {
|
|
27
|
+
/** Material icon name */
|
|
28
|
+
icon: string;
|
|
29
|
+
/** Icon size (default: xl) */
|
|
30
|
+
iconSize?: 'sm' | 'md' | 'lg' | 'xl' | 'xxl';
|
|
31
|
+
/** Main heading text */
|
|
32
|
+
title: string;
|
|
33
|
+
/** Descriptive subtitle text */
|
|
34
|
+
subtitle?: string;
|
|
35
|
+
/** Custom icon color (default: textTertiary) */
|
|
36
|
+
iconColor?: string;
|
|
37
|
+
/** Custom title color (default: textPrimary) */
|
|
38
|
+
titleColor?: string;
|
|
39
|
+
/** Custom subtitle color (default: textSecondary) */
|
|
40
|
+
subtitleColor?: string;
|
|
41
|
+
/** Container style override */
|
|
42
|
+
style?: ViewStyle;
|
|
43
|
+
/** Title style override */
|
|
44
|
+
titleStyle?: TextStyle;
|
|
45
|
+
/** Subtitle style override */
|
|
46
|
+
subtitleStyle?: TextStyle;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// =============================================================================
|
|
50
|
+
// COMPONENT IMPLEMENTATION
|
|
51
|
+
// =============================================================================
|
|
52
|
+
|
|
53
|
+
export const EmptyState: React.FC<EmptyStateProps> = ({
|
|
54
|
+
icon,
|
|
55
|
+
iconSize = 'xl',
|
|
56
|
+
title,
|
|
57
|
+
subtitle,
|
|
58
|
+
iconColor,
|
|
59
|
+
titleColor,
|
|
60
|
+
subtitleColor,
|
|
61
|
+
style,
|
|
62
|
+
titleStyle,
|
|
63
|
+
subtitleStyle,
|
|
64
|
+
}) => {
|
|
65
|
+
const tokens = useAppDesignTokens();
|
|
66
|
+
const styles = getStyles(tokens);
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<View style={[styles.container, style]}>
|
|
70
|
+
<AtomicIcon
|
|
71
|
+
name={icon}
|
|
72
|
+
size={iconSize}
|
|
73
|
+
customColor={iconColor || tokens.colors.textSecondary}
|
|
74
|
+
/>
|
|
75
|
+
<AtomicText
|
|
76
|
+
type="headlineMedium"
|
|
77
|
+
color={titleColor || tokens.colors.textPrimary}
|
|
78
|
+
style={StyleSheet.flatten([
|
|
79
|
+
styles.title,
|
|
80
|
+
titleStyle,
|
|
81
|
+
])}
|
|
82
|
+
>
|
|
83
|
+
{title}
|
|
84
|
+
</AtomicText>
|
|
85
|
+
{subtitle && (
|
|
86
|
+
<AtomicText
|
|
87
|
+
type="bodyMedium"
|
|
88
|
+
color={subtitleColor || tokens.colors.textSecondary}
|
|
89
|
+
style={StyleSheet.flatten([
|
|
90
|
+
styles.subtitle,
|
|
91
|
+
subtitleStyle,
|
|
92
|
+
])}
|
|
93
|
+
>
|
|
94
|
+
{subtitle}
|
|
95
|
+
</AtomicText>
|
|
96
|
+
)}
|
|
97
|
+
</View>
|
|
98
|
+
);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// =============================================================================
|
|
102
|
+
// STYLES
|
|
103
|
+
// =============================================================================
|
|
104
|
+
|
|
105
|
+
const getStyles = (tokens: ReturnType<typeof useAppDesignTokens>) =>
|
|
106
|
+
StyleSheet.create({
|
|
107
|
+
container: {
|
|
108
|
+
flex: 1,
|
|
109
|
+
justifyContent: 'center',
|
|
110
|
+
alignItems: 'center',
|
|
111
|
+
paddingVertical: tokens.spacing.xl,
|
|
112
|
+
paddingHorizontal: tokens.spacing.lg,
|
|
113
|
+
},
|
|
114
|
+
title: {
|
|
115
|
+
fontSize: tokens.typography.headingMedium.fontSize,
|
|
116
|
+
fontWeight: tokens.typography.headingMedium.fontWeight,
|
|
117
|
+
marginTop: tokens.spacing.md,
|
|
118
|
+
textAlign: 'center',
|
|
119
|
+
},
|
|
120
|
+
subtitle: {
|
|
121
|
+
fontSize: tokens.typography.bodySmall.fontSize,
|
|
122
|
+
marginTop: tokens.spacing.xs,
|
|
123
|
+
textAlign: 'center',
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// =============================================================================
|
|
128
|
+
// EXPORTS
|
|
129
|
+
// =============================================================================
|
|
130
|
+
|