@umituz/react-native-design-system 1.4.0 → 1.4.1
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/README.md +2 -1
- package/lib/index.d.ts +3 -2
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +8 -2
- package/lib/index.js.map +1 -1
- package/lib/presentation/atoms/AtomicButton.d.ts +1 -0
- package/lib/presentation/atoms/AtomicButton.d.ts.map +1 -1
- package/lib/presentation/atoms/AtomicButton.js +9 -6
- package/lib/presentation/atoms/AtomicButton.js.map +1 -1
- package/lib/presentation/atoms/AtomicDatePicker.js +1 -1
- package/lib/presentation/atoms/AtomicDatePicker.js.map +1 -1
- package/lib/presentation/atoms/AtomicFab.d.ts +1 -0
- package/lib/presentation/atoms/AtomicFab.d.ts.map +1 -1
- package/lib/presentation/atoms/AtomicFab.js +5 -4
- package/lib/presentation/atoms/AtomicFab.js.map +1 -1
- package/lib/presentation/atoms/AtomicPicker.js +1 -1
- package/lib/presentation/atoms/AtomicPicker.js.map +1 -1
- package/lib/presentation/atoms/AtomicProgress.d.ts +3 -0
- package/lib/presentation/atoms/AtomicProgress.d.ts.map +1 -1
- package/lib/presentation/atoms/AtomicProgress.js +2 -1
- package/lib/presentation/atoms/AtomicProgress.js.map +1 -1
- package/lib/presentation/atoms/AtomicSkeleton.d.ts +4 -4
- package/lib/presentation/atoms/AtomicSkeleton.js +4 -4
- package/lib/presentation/atoms/AtomicTouchable.d.ts +11 -1
- package/lib/presentation/atoms/AtomicTouchable.d.ts.map +1 -1
- package/lib/presentation/atoms/AtomicTouchable.js +19 -6
- package/lib/presentation/atoms/AtomicTouchable.js.map +1 -1
- package/lib/presentation/atoms/fab/types/index.d.ts +5 -0
- package/lib/presentation/atoms/fab/types/index.d.ts.map +1 -1
- package/lib/presentation/atoms/touchable/styles/touchableStyles.d.ts +5 -0
- package/lib/presentation/atoms/touchable/styles/touchableStyles.d.ts.map +1 -1
- package/lib/presentation/atoms/touchable/styles/touchableStyles.js +8 -0
- package/lib/presentation/atoms/touchable/styles/touchableStyles.js.map +1 -1
- package/lib/presentation/atoms/touchable/types/index.d.ts +7 -1
- package/lib/presentation/atoms/touchable/types/index.d.ts.map +1 -1
- package/lib/presentation/loading/presentation/components/LoadingState.d.ts +39 -0
- package/lib/presentation/loading/presentation/components/LoadingState.d.ts.map +1 -0
- package/lib/presentation/loading/presentation/components/LoadingState.js +123 -0
- package/lib/presentation/loading/presentation/components/LoadingState.js.map +1 -0
- package/lib/presentation/molecules/AtomicConfirmationModal.js +1 -1
- package/lib/presentation/organisms/ScreenLayout.d.ts +15 -0
- package/lib/presentation/organisms/ScreenLayout.d.ts.map +1 -1
- package/lib/presentation/organisms/ScreenLayout.js +10 -1
- package/lib/presentation/organisms/ScreenLayout.js.map +1 -1
- package/lib/presentation/tokens/AppDesignTokens.d.ts +1 -1
- package/lib/presentation/tokens/AppDesignTokens.d.ts.map +1 -1
- package/lib/presentation/tokens/AppDesignTokens.js +1 -1
- package/lib/presentation/tokens/AppDesignTokens.js.map +1 -1
- package/lib/presentation/tokens/commonStyles.d.ts +1 -1
- package/lib/presentation/tokens/commonStyles.js +1 -1
- package/lib/presentation/tokens/core/BaseTokens.d.ts +25 -0
- package/lib/presentation/tokens/core/BaseTokens.d.ts.map +1 -1
- package/lib/presentation/tokens/core/BaseTokens.js +18 -0
- package/lib/presentation/tokens/core/BaseTokens.js.map +1 -1
- package/lib/presentation/tokens/core/TokenFactory.d.ts +15 -2
- package/lib/presentation/tokens/core/TokenFactory.d.ts.map +1 -1
- package/lib/presentation/tokens/core/TokenFactory.js +2 -1
- package/lib/presentation/tokens/core/TokenFactory.js.map +1 -1
- package/package.json +3 -4
- package/src/index.ts +10 -1
- package/src/presentation/atoms/AtomicButton.tsx +16 -5
- package/src/presentation/atoms/AtomicDatePicker.tsx +1 -1
- package/src/presentation/atoms/AtomicFab.tsx +9 -3
- package/src/presentation/atoms/AtomicPicker.tsx +1 -1
- package/src/presentation/atoms/AtomicProgress.tsx +4 -0
- package/src/presentation/atoms/AtomicSkeleton.tsx +4 -4
- package/src/presentation/atoms/AtomicTouchable.tsx +27 -3
- package/src/presentation/atoms/fab/types/index.ts +6 -0
- package/src/presentation/atoms/touchable/styles/touchableStyles.ts +9 -0
- package/src/presentation/atoms/touchable/types/index.ts +8 -1
- package/src/presentation/loading/presentation/components/LoadingState.tsx +200 -0
- package/src/presentation/molecules/AtomicConfirmationModal.tsx +1 -1
- package/src/presentation/organisms/ScreenLayout.tsx +40 -0
- package/src/presentation/tokens/AppDesignTokens.ts +2 -0
- package/src/presentation/tokens/commonStyles.ts +1 -1
- package/src/presentation/tokens/core/BaseTokens.ts +22 -0
- package/src/presentation/tokens/core/TokenFactory.ts +4 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-design-system",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"description": "Universal design system for React Native apps - Domain-Driven Design architecture with Material Design 3 components",
|
|
5
5
|
"main": "./lib/index.js",
|
|
6
6
|
"types": "./lib/index.d.ts",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"peerDependencies": {
|
|
34
34
|
"react": ">=18.2.0",
|
|
35
35
|
"react-native": ">=0.74.0",
|
|
36
|
+
"react-native-reanimated": "~3.10.1",
|
|
36
37
|
"react-native-svg": ">=13.0.0",
|
|
37
38
|
"@react-native-community/datetimepicker": "8.0.1",
|
|
38
39
|
"@expo/vector-icons": "^14.0.0",
|
|
@@ -43,9 +44,6 @@
|
|
|
43
44
|
"@umituz/react-native-localization": "^1.0.0"
|
|
44
45
|
},
|
|
45
46
|
"peerDependenciesMeta": {
|
|
46
|
-
"@react-native-community/datetimepicker": {
|
|
47
|
-
"optional": true
|
|
48
|
-
},
|
|
49
47
|
"expo-linear-gradient": {
|
|
50
48
|
"optional": true
|
|
51
49
|
},
|
|
@@ -62,6 +60,7 @@
|
|
|
62
60
|
"@types/react-native": "^0.73.0",
|
|
63
61
|
"react": ">=18.2.0",
|
|
64
62
|
"react-native": ">=0.74.0",
|
|
63
|
+
"react-native-reanimated": "~3.10.1",
|
|
65
64
|
"react-native-svg": "^15.0.0",
|
|
66
65
|
"@expo/vector-icons": "^14.0.0",
|
|
67
66
|
"lucide-react-native": "^0.468.0",
|
package/src/index.ts
CHANGED
|
@@ -11,11 +11,12 @@
|
|
|
11
11
|
* - presentation/atoms: Primitive UI components (AtomicButton, AtomicText, etc.)
|
|
12
12
|
* - presentation/molecules: Composite components (SearchBar, ListItem, etc.)
|
|
13
13
|
* - presentation/organisms: Complex patterns (ScreenLayout, AppHeader, FormContainer)
|
|
14
|
+
* - presentation/loading: Loading states (LoadingState, LoadingSpinner)
|
|
14
15
|
* - presentation/tokens: Design tokens (colors, typography, spacing, etc.)
|
|
15
16
|
* - presentation/utils: Utility functions and helpers
|
|
16
17
|
*
|
|
17
18
|
* Usage:
|
|
18
|
-
* import { AtomicButton, AtomicFilter, AtomicTouchable, SearchBar, STATIC_TOKENS } from '@umituz/react-native-design-system';
|
|
19
|
+
* import { AtomicButton, AtomicFilter, AtomicTouchable, SearchBar, LoadingState, STATIC_TOKENS } from '@umituz/react-native-design-system';
|
|
19
20
|
*/
|
|
20
21
|
|
|
21
22
|
// =============================================================================
|
|
@@ -227,6 +228,12 @@ export {
|
|
|
227
228
|
// Note: FeedbackModal moved to @domains/feedback
|
|
228
229
|
// Import from feedback domain: import { FeedbackModal } from '@domains/feedback';
|
|
229
230
|
|
|
231
|
+
// =============================================================================
|
|
232
|
+
// LOADING - Loading States
|
|
233
|
+
// =============================================================================
|
|
234
|
+
// Loading components moved to separate package
|
|
235
|
+
// Import from @umituz/react-native-loading when available
|
|
236
|
+
|
|
230
237
|
// =============================================================================
|
|
231
238
|
// TOKENS - Design Tokens (Refactored with ZERO duplication)
|
|
232
239
|
// =============================================================================
|
|
@@ -248,6 +255,7 @@ export {
|
|
|
248
255
|
// Individual base tokens
|
|
249
256
|
spacing,
|
|
250
257
|
typography,
|
|
258
|
+
animations,
|
|
251
259
|
borders,
|
|
252
260
|
|
|
253
261
|
// Type exports
|
|
@@ -256,6 +264,7 @@ export {
|
|
|
256
264
|
type ColorPalette,
|
|
257
265
|
type Spacing,
|
|
258
266
|
type Typography,
|
|
267
|
+
type Animations,
|
|
259
268
|
type Borders,
|
|
260
269
|
type BaseTokens,
|
|
261
270
|
} from './presentation/tokens/AppDesignTokens';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { StyleSheet, StyleProp, ViewStyle, TextStyle, TouchableOpacity, View } from 'react-native';
|
|
2
|
+
import { StyleSheet, StyleProp, ViewStyle, TextStyle, TouchableOpacity, ActivityIndicator, View } from 'react-native';
|
|
3
3
|
import { AtomicText } from './AtomicText';
|
|
4
4
|
import { Icon } from '../../domains/icons/presentation/components/Icon';
|
|
5
5
|
import { useAppDesignTokens } from '../hooks/useAppDesignTokens';
|
|
@@ -15,6 +15,7 @@ export interface AtomicButtonProps {
|
|
|
15
15
|
variant?: ButtonVariant;
|
|
16
16
|
size?: ButtonSize;
|
|
17
17
|
disabled?: boolean;
|
|
18
|
+
loading?: boolean;
|
|
18
19
|
icon?: IconName;
|
|
19
20
|
fullWidth?: boolean;
|
|
20
21
|
style?: StyleProp<ViewStyle>;
|
|
@@ -29,6 +30,7 @@ export const AtomicButton: React.FC<AtomicButtonProps> = ({
|
|
|
29
30
|
variant = 'primary',
|
|
30
31
|
size = 'md',
|
|
31
32
|
disabled = false,
|
|
33
|
+
loading = false,
|
|
32
34
|
icon,
|
|
33
35
|
fullWidth = false,
|
|
34
36
|
style,
|
|
@@ -38,7 +40,7 @@ export const AtomicButton: React.FC<AtomicButtonProps> = ({
|
|
|
38
40
|
const tokens = useAppDesignTokens();
|
|
39
41
|
|
|
40
42
|
const handlePress = () => {
|
|
41
|
-
if (!disabled) {
|
|
43
|
+
if (!disabled && !loading) {
|
|
42
44
|
onPress();
|
|
43
45
|
}
|
|
44
46
|
};
|
|
@@ -179,7 +181,7 @@ export const AtomicButton: React.FC<AtomicButtonProps> = ({
|
|
|
179
181
|
];
|
|
180
182
|
|
|
181
183
|
const buttonText = title || children;
|
|
182
|
-
const showIcon =
|
|
184
|
+
const showIcon = icon && !loading;
|
|
183
185
|
const iconColor = variantStyles.text.color;
|
|
184
186
|
|
|
185
187
|
return (
|
|
@@ -187,11 +189,17 @@ export const AtomicButton: React.FC<AtomicButtonProps> = ({
|
|
|
187
189
|
style={containerStyle}
|
|
188
190
|
onPress={handlePress}
|
|
189
191
|
activeOpacity={0.8}
|
|
190
|
-
disabled={disabled}
|
|
192
|
+
disabled={disabled || loading}
|
|
191
193
|
testID={testID}
|
|
192
194
|
>
|
|
193
195
|
<View style={styles.content}>
|
|
194
|
-
{
|
|
196
|
+
{loading ? (
|
|
197
|
+
<ActivityIndicator
|
|
198
|
+
size="small"
|
|
199
|
+
color={variantStyles.text.color}
|
|
200
|
+
style={styles.loader}
|
|
201
|
+
/>
|
|
202
|
+
) : showIcon ? (
|
|
195
203
|
<Icon
|
|
196
204
|
name={icon}
|
|
197
205
|
customSize={config.iconSize}
|
|
@@ -231,6 +239,9 @@ const styles = StyleSheet.create({
|
|
|
231
239
|
icon: {
|
|
232
240
|
marginRight: 8,
|
|
233
241
|
},
|
|
242
|
+
loader: {
|
|
243
|
+
marginRight: 8,
|
|
244
|
+
},
|
|
234
245
|
});
|
|
235
246
|
|
|
236
247
|
export type { AtomicButtonProps as ButtonProps };
|
|
@@ -215,7 +215,7 @@ export const AtomicDatePicker: React.FC<AtomicDatePickerProps> = ({
|
|
|
215
215
|
{show && (
|
|
216
216
|
<Modal
|
|
217
217
|
transparent
|
|
218
|
-
animationType=
|
|
218
|
+
animationType={isTabletDevice ? 'fade' : 'slide'}
|
|
219
219
|
visible={show}
|
|
220
220
|
onRequestClose={() => setShow(false)}
|
|
221
221
|
>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { TouchableOpacity, StyleSheet } from 'react-native';
|
|
2
|
+
import { TouchableOpacity, ActivityIndicator, StyleSheet } from 'react-native';
|
|
3
3
|
import { useAppDesignTokens } from '../hooks/useAppDesignTokens';
|
|
4
4
|
import { useResponsive } from '../hooks/useResponsive';
|
|
5
5
|
import { AtomicIcon } from './AtomicIcon';
|
|
@@ -40,6 +40,7 @@ export { FAB_SIZES, getFabVariants, getFabIconSize, getFabBorder };
|
|
|
40
40
|
* - Material Design 3 sizes (sm: 40px, md: 56px, lg: 72px)
|
|
41
41
|
* - Three variants: primary, secondary, surface
|
|
42
42
|
* - Responsive positioning (above tab bar, safe area aware)
|
|
43
|
+
* - Loading state with spinner
|
|
43
44
|
* - Disabled state with opacity
|
|
44
45
|
* - Theme-aware colors from design tokens
|
|
45
46
|
* - Border for depth (no shadows per CLAUDE.md)
|
|
@@ -50,13 +51,14 @@ export const AtomicFab: React.FC<AtomicFabProps> = ({
|
|
|
50
51
|
variant = 'primary',
|
|
51
52
|
size = 'md',
|
|
52
53
|
disabled = false,
|
|
54
|
+
loading = false,
|
|
53
55
|
style,
|
|
54
56
|
testID,
|
|
55
57
|
accessibilityLabel,
|
|
56
58
|
}) => {
|
|
57
59
|
const tokens = useAppDesignTokens();
|
|
58
60
|
const responsive = useResponsive();
|
|
59
|
-
const isDisabled = disabled;
|
|
61
|
+
const isDisabled = disabled || loading;
|
|
60
62
|
|
|
61
63
|
// Get configurations
|
|
62
64
|
const sizeConfig = FAB_SIZES[size as 'sm' | 'md' | 'lg'];
|
|
@@ -92,7 +94,11 @@ export const AtomicFab: React.FC<AtomicFabProps> = ({
|
|
|
92
94
|
accessibilityLabel={accessibilityLabel || `${icon} button`}
|
|
93
95
|
accessibilityRole="button"
|
|
94
96
|
>
|
|
95
|
-
|
|
97
|
+
{loading ? (
|
|
98
|
+
<ActivityIndicator size="small" color={variantConfig.iconColor} />
|
|
99
|
+
) : (
|
|
100
|
+
<AtomicIcon name={icon} size={iconSize} customColor={variantConfig.iconColor} />
|
|
101
|
+
)}
|
|
96
102
|
</TouchableOpacity>
|
|
97
103
|
);
|
|
98
104
|
};
|
|
@@ -382,7 +382,7 @@ export const AtomicPicker: React.FC<AtomicPickerProps> = ({
|
|
|
382
382
|
{/* Selection Modal */}
|
|
383
383
|
<Modal
|
|
384
384
|
visible={modalVisible}
|
|
385
|
-
animationType="
|
|
385
|
+
animationType="slide"
|
|
386
386
|
transparent
|
|
387
387
|
onRequestClose={closeModal}
|
|
388
388
|
testID={`${testID}-modal`}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* Usage:
|
|
11
11
|
* - File upload progress
|
|
12
12
|
* - Task completion progress
|
|
13
|
+
* - Loading progress
|
|
13
14
|
* - Achievement progress
|
|
14
15
|
* - Form completion
|
|
15
16
|
*/
|
|
@@ -41,6 +42,8 @@ export interface AtomicProgressProps {
|
|
|
41
42
|
showValue?: boolean;
|
|
42
43
|
/** Custom text color */
|
|
43
44
|
textColor?: string;
|
|
45
|
+
/** Animation duration in milliseconds */
|
|
46
|
+
animationDuration?: number;
|
|
44
47
|
/** Style overrides */
|
|
45
48
|
style?: ViewStyle | ViewStyle[];
|
|
46
49
|
/** Test ID for testing */
|
|
@@ -61,6 +64,7 @@ export const AtomicProgress: React.FC<AtomicProgressProps> = ({
|
|
|
61
64
|
showPercentage = false,
|
|
62
65
|
showValue = false,
|
|
63
66
|
textColor,
|
|
67
|
+
animationDuration = 300,
|
|
64
68
|
style,
|
|
65
69
|
testID,
|
|
66
70
|
}) => {
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* AtomicSkeleton - Universal Skeleton
|
|
2
|
+
* AtomicSkeleton - Universal Skeleton Loading Component
|
|
3
3
|
*
|
|
4
|
-
* Displays skeleton placeholders for
|
|
4
|
+
* Displays skeleton placeholders for loading states
|
|
5
5
|
* Theme: {{THEME_NAME}} ({{CATEGORY}} category)
|
|
6
6
|
*
|
|
7
7
|
* Atomic Design Level: ATOM
|
|
8
|
-
* Purpose:
|
|
8
|
+
* Purpose: Loading state placeholder
|
|
9
9
|
*
|
|
10
10
|
* Usage:
|
|
11
|
-
* - Content placeholders
|
|
11
|
+
* - Content loading placeholders
|
|
12
12
|
* - List item skeletons
|
|
13
13
|
* - Card skeletons
|
|
14
14
|
* - Text line skeletons
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { Pressable, StyleSheet } from 'react-native';
|
|
2
|
+
import { Pressable, View, ActivityIndicator, StyleSheet } from 'react-native';
|
|
3
3
|
import { useAppDesignTokens } from '../hooks/useAppDesignTokens';
|
|
4
4
|
import { AtomicTouchableProps, TouchableFeedback, FeedbackStrength } from './touchable/types';
|
|
5
5
|
import {
|
|
6
6
|
getOpacityValue,
|
|
7
7
|
getTouchableContainerStyle,
|
|
8
8
|
getDisabledStyle,
|
|
9
|
+
getLoadingContainerStyle,
|
|
9
10
|
normalizeHitSlop,
|
|
10
11
|
} from './touchable/styles/touchableStyles';
|
|
11
12
|
|
|
@@ -20,6 +21,7 @@ export {
|
|
|
20
21
|
getOpacityValue,
|
|
21
22
|
getTouchableContainerStyle,
|
|
22
23
|
getDisabledStyle,
|
|
24
|
+
getLoadingContainerStyle,
|
|
23
25
|
normalizeHitSlop,
|
|
24
26
|
} from './touchable/styles/touchableStyles';
|
|
25
27
|
|
|
@@ -32,6 +34,7 @@ export {
|
|
|
32
34
|
* Features:
|
|
33
35
|
* - Multiple feedback variants (opacity, highlight, ripple, none)
|
|
34
36
|
* - Configurable feedback strength (subtle, normal, strong)
|
|
37
|
+
* - Loading state with indicator
|
|
35
38
|
* - Disabled state with visual feedback
|
|
36
39
|
* - Hit slop customization for small touch targets
|
|
37
40
|
* - Minimum 48x48 touch target (iOS HIG compliance)
|
|
@@ -45,6 +48,15 @@ export {
|
|
|
45
48
|
* <AtomicText>Press Me</AtomicText>
|
|
46
49
|
* </AtomicTouchable>
|
|
47
50
|
*
|
|
51
|
+
* // With loading state
|
|
52
|
+
* <AtomicTouchable
|
|
53
|
+
* onPress={handleSubmit}
|
|
54
|
+
* loading={isSubmitting}
|
|
55
|
+
* feedback="highlight"
|
|
56
|
+
* >
|
|
57
|
+
* <AtomicText>Submit</AtomicText>
|
|
58
|
+
* </AtomicTouchable>
|
|
59
|
+
*
|
|
48
60
|
* // With custom hit slop (extends touch area)
|
|
49
61
|
* <AtomicTouchable
|
|
50
62
|
* onPress={handlePress}
|
|
@@ -64,6 +76,7 @@ export const AtomicTouchable: React.FC<AtomicTouchableProps> = ({
|
|
|
64
76
|
feedback = 'opacity',
|
|
65
77
|
strength = 'normal',
|
|
66
78
|
disabled = false,
|
|
79
|
+
loading = false,
|
|
67
80
|
hitSlop,
|
|
68
81
|
style,
|
|
69
82
|
pressedStyle,
|
|
@@ -79,7 +92,7 @@ export const AtomicTouchable: React.FC<AtomicTouchableProps> = ({
|
|
|
79
92
|
const tokens = useAppDesignTokens();
|
|
80
93
|
|
|
81
94
|
// Determine if touchable should be disabled
|
|
82
|
-
const isDisabled = disabled;
|
|
95
|
+
const isDisabled = disabled || loading;
|
|
83
96
|
|
|
84
97
|
// Get opacity value based on strength
|
|
85
98
|
const opacityValue = getOpacityValue(strength as 'subtle' | 'normal' | 'strong');
|
|
@@ -156,10 +169,21 @@ export const AtomicTouchable: React.FC<AtomicTouchableProps> = ({
|
|
|
156
169
|
accessibilityRole={accessibilityRole}
|
|
157
170
|
accessibilityState={{
|
|
158
171
|
disabled: isDisabled,
|
|
172
|
+
busy: loading,
|
|
159
173
|
}}
|
|
160
174
|
testID={testID}
|
|
161
175
|
>
|
|
162
|
-
{
|
|
176
|
+
{loading ? (
|
|
177
|
+
<View style={getLoadingContainerStyle()}>
|
|
178
|
+
<ActivityIndicator
|
|
179
|
+
size="small"
|
|
180
|
+
color={tokens.colors.primary}
|
|
181
|
+
testID={`${testID}-loading`}
|
|
182
|
+
/>
|
|
183
|
+
</View>
|
|
184
|
+
) : (
|
|
185
|
+
children
|
|
186
|
+
)}
|
|
163
187
|
</Pressable>
|
|
164
188
|
);
|
|
165
189
|
};
|
|
@@ -35,6 +35,15 @@ export const getDisabledStyle = (): ViewStyle => ({
|
|
|
35
35
|
opacity: 0.5,
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Get loading container style
|
|
40
|
+
* Centers the loading indicator
|
|
41
|
+
*/
|
|
42
|
+
export const getLoadingContainerStyle = (): ViewStyle => ({
|
|
43
|
+
justifyContent: 'center',
|
|
44
|
+
alignItems: 'center',
|
|
45
|
+
});
|
|
46
|
+
|
|
38
47
|
/**
|
|
39
48
|
* Convert number to HitSlop object
|
|
40
49
|
* If hitSlop is a number, apply it to all sides
|
|
@@ -40,7 +40,8 @@ export interface HitSlop {
|
|
|
40
40
|
* onPress={handlePress}
|
|
41
41
|
* feedback="opacity"
|
|
42
42
|
* strength="normal"
|
|
43
|
-
* disabled={
|
|
43
|
+
* disabled={isLoading}
|
|
44
|
+
* loading={isLoading}
|
|
44
45
|
* hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
|
45
46
|
* style={styles.touchable}
|
|
46
47
|
* >
|
|
@@ -92,6 +93,12 @@ export interface AtomicTouchableProps {
|
|
|
92
93
|
*/
|
|
93
94
|
disabled?: boolean;
|
|
94
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Show loading indicator (disables touch)
|
|
98
|
+
* @default false
|
|
99
|
+
*/
|
|
100
|
+
loading?: boolean;
|
|
101
|
+
|
|
95
102
|
/**
|
|
96
103
|
* Hit slop - extends touchable area
|
|
97
104
|
* Useful for small touch targets
|
|
@@ -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;
|
|
@@ -117,7 +117,7 @@ export const AtomicConfirmationModal: React.FC<AtomicConfirmationModalProps> = (
|
|
|
117
117
|
<Modal
|
|
118
118
|
visible={visible}
|
|
119
119
|
transparent
|
|
120
|
-
animationType="
|
|
120
|
+
animationType="fade"
|
|
121
121
|
onRequestClose={onCancel}
|
|
122
122
|
statusBarTranslucent
|
|
123
123
|
testID={testID}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Provides consistent layout structure for all screens:
|
|
5
5
|
* - SafeAreaView with configurable edges
|
|
6
6
|
* - Optional ScrollView for content
|
|
7
|
+
* - Loading state support
|
|
7
8
|
* - Theme-aware background colors
|
|
8
9
|
* - Optional header/footer slots
|
|
9
10
|
* - Consistent spacing and padding
|
|
@@ -16,6 +17,7 @@
|
|
|
16
17
|
* Advanced:
|
|
17
18
|
* <ScreenLayout
|
|
18
19
|
* scrollable={false}
|
|
20
|
+
* loading={isLoading}
|
|
19
21
|
* edges={['top', 'bottom']}
|
|
20
22
|
* header={<CustomHeader />}
|
|
21
23
|
* >
|
|
@@ -27,6 +29,7 @@ import React, { useMemo } from 'react';
|
|
|
27
29
|
import { View, ScrollView, StyleSheet, ViewStyle } from 'react-native';
|
|
28
30
|
import { SafeAreaView, Edge } from 'react-native-safe-area-context';
|
|
29
31
|
import { useAppDesignTokens } from '../hooks/useAppDesignTokens';
|
|
32
|
+
import { LoadingState } from '../loading/presentation/components/LoadingState';
|
|
30
33
|
|
|
31
34
|
export interface ScreenLayoutProps {
|
|
32
35
|
/**
|
|
@@ -49,6 +52,22 @@ export interface ScreenLayoutProps {
|
|
|
49
52
|
*/
|
|
50
53
|
edges?: Edge[];
|
|
51
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Show loading state
|
|
57
|
+
* When true, displays LoadingState component
|
|
58
|
+
*/
|
|
59
|
+
loading?: boolean;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Loading icon name (default: 'settings')
|
|
63
|
+
*/
|
|
64
|
+
loadingIcon?: string;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Loading message (default: 'Loading...')
|
|
68
|
+
*/
|
|
69
|
+
loadingMessage?: string;
|
|
70
|
+
|
|
52
71
|
/**
|
|
53
72
|
* Optional header component
|
|
54
73
|
* Rendered above scrollable content
|
|
@@ -99,6 +118,9 @@ export const ScreenLayout: React.FC<ScreenLayoutProps> = ({
|
|
|
99
118
|
children,
|
|
100
119
|
scrollable = true,
|
|
101
120
|
edges = ['top'],
|
|
121
|
+
loading = false,
|
|
122
|
+
loadingIcon = 'settings',
|
|
123
|
+
loadingMessage = 'Loading...',
|
|
102
124
|
header,
|
|
103
125
|
footer,
|
|
104
126
|
backgroundColor,
|
|
@@ -114,6 +136,24 @@ export const ScreenLayout: React.FC<ScreenLayoutProps> = ({
|
|
|
114
136
|
|
|
115
137
|
const bgColor = backgroundColor || tokens.colors.backgroundPrimary;
|
|
116
138
|
|
|
139
|
+
// Show loading state
|
|
140
|
+
if (loading) {
|
|
141
|
+
return (
|
|
142
|
+
<SafeAreaView
|
|
143
|
+
style={[styles.container, { backgroundColor: bgColor }, containerStyle]}
|
|
144
|
+
edges={edges}
|
|
145
|
+
testID={testID}
|
|
146
|
+
>
|
|
147
|
+
<LoadingState
|
|
148
|
+
icon={loadingIcon}
|
|
149
|
+
message={loadingMessage}
|
|
150
|
+
size="large"
|
|
151
|
+
fullScreen
|
|
152
|
+
/>
|
|
153
|
+
</SafeAreaView>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
117
157
|
// Non-scrollable layout
|
|
118
158
|
if (!scrollable) {
|
|
119
159
|
return (
|