@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,40 @@
|
|
|
1
|
+
import { ViewStyle, TextStyle } from 'react-native';
|
|
2
|
+
import { AtomicIconColor } from '../../AtomicIcon';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Picker option item
|
|
6
|
+
*
|
|
7
|
+
* icon: Any MaterialIcons name
|
|
8
|
+
* @see https://fonts.google.com/icons
|
|
9
|
+
*/
|
|
10
|
+
export interface PickerOption {
|
|
11
|
+
label: string;
|
|
12
|
+
value: string;
|
|
13
|
+
icon?: string; // MaterialIcons name
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
description?: string;
|
|
16
|
+
testID?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type PickerSize = 'sm' | 'md' | 'lg';
|
|
20
|
+
|
|
21
|
+
export interface AtomicPickerProps {
|
|
22
|
+
value: string | string[];
|
|
23
|
+
onChange: (value: string | string[]) => void;
|
|
24
|
+
options: PickerOption[];
|
|
25
|
+
label?: string;
|
|
26
|
+
placeholder?: string;
|
|
27
|
+
error?: string;
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
multiple?: boolean;
|
|
30
|
+
searchable?: boolean;
|
|
31
|
+
clearable?: boolean;
|
|
32
|
+
autoClose?: boolean;
|
|
33
|
+
color?: AtomicIconColor;
|
|
34
|
+
size?: PickerSize;
|
|
35
|
+
modalTitle?: string;
|
|
36
|
+
emptyMessage?: string;
|
|
37
|
+
style?: ViewStyle | ViewStyle[];
|
|
38
|
+
labelStyle?: TextStyle | TextStyle[];
|
|
39
|
+
testID?: string;
|
|
40
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { ViewStyle } from 'react-native';
|
|
2
|
+
import { FeedbackStrength } from '../types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Get opacity value based on feedback strength
|
|
6
|
+
*/
|
|
7
|
+
export const getOpacityValue = (strength: FeedbackStrength): number => {
|
|
8
|
+
switch (strength) {
|
|
9
|
+
case 'subtle':
|
|
10
|
+
return 0.8;
|
|
11
|
+
case 'normal':
|
|
12
|
+
return 0.6;
|
|
13
|
+
case 'strong':
|
|
14
|
+
return 0.4;
|
|
15
|
+
default:
|
|
16
|
+
return 0.6;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get base touchable container style
|
|
22
|
+
* Ensures minimum touch target size (iOS HIG: 48x48)
|
|
23
|
+
*/
|
|
24
|
+
export const getTouchableContainerStyle = (): ViewStyle => ({
|
|
25
|
+
minWidth: 48,
|
|
26
|
+
minHeight: 48,
|
|
27
|
+
justifyContent: 'center',
|
|
28
|
+
alignItems: 'center',
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get disabled touchable style
|
|
33
|
+
*/
|
|
34
|
+
export const getDisabledStyle = (): ViewStyle => ({
|
|
35
|
+
opacity: 0.5,
|
|
36
|
+
});
|
|
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
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Convert number to HitSlop object
|
|
49
|
+
* If hitSlop is a number, apply it to all sides
|
|
50
|
+
*/
|
|
51
|
+
export const normalizeHitSlop = (
|
|
52
|
+
hitSlop: number | { top?: number; bottom?: number; left?: number; right?: number } | undefined
|
|
53
|
+
): { top: number; bottom: number; left: number; right: number } | undefined => {
|
|
54
|
+
if (hitSlop === undefined) return undefined;
|
|
55
|
+
|
|
56
|
+
if (typeof hitSlop === 'number') {
|
|
57
|
+
return {
|
|
58
|
+
top: hitSlop,
|
|
59
|
+
bottom: hitSlop,
|
|
60
|
+
left: hitSlop,
|
|
61
|
+
right: hitSlop,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
top: hitSlop.top || 0,
|
|
67
|
+
bottom: hitSlop.bottom || 0,
|
|
68
|
+
left: hitSlop.left || 0,
|
|
69
|
+
right: hitSlop.right || 0,
|
|
70
|
+
};
|
|
71
|
+
};
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { StyleProp, ViewStyle, GestureResponderEvent } from 'react-native';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Touchable feedback variant
|
|
5
|
+
* - opacity: iOS-style opacity feedback (default)
|
|
6
|
+
* - highlight: Android-style highlight feedback
|
|
7
|
+
* - ripple: Material Design ripple effect (Android only)
|
|
8
|
+
* - none: No visual feedback
|
|
9
|
+
*/
|
|
10
|
+
export type TouchableFeedback = 'opacity' | 'highlight' | 'ripple' | 'none';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Feedback strength for visual feedback
|
|
14
|
+
* - subtle: Light feedback (0.8 opacity)
|
|
15
|
+
* - normal: Standard feedback (0.6 opacity)
|
|
16
|
+
* - strong: Strong feedback (0.4 opacity)
|
|
17
|
+
*/
|
|
18
|
+
export type FeedbackStrength = 'subtle' | 'normal' | 'strong';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Hit slop configuration
|
|
22
|
+
* Extends the touchable area beyond the component's bounds
|
|
23
|
+
*/
|
|
24
|
+
export interface HitSlop {
|
|
25
|
+
top?: number;
|
|
26
|
+
bottom?: number;
|
|
27
|
+
left?: number;
|
|
28
|
+
right?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* AtomicTouchable component props
|
|
33
|
+
*
|
|
34
|
+
* A unified touchable wrapper with consistent behavior across platforms.
|
|
35
|
+
* Uses React Native's Pressable API for modern, accessible touch handling.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```tsx
|
|
39
|
+
* <AtomicTouchable
|
|
40
|
+
* onPress={handlePress}
|
|
41
|
+
* feedback="opacity"
|
|
42
|
+
* strength="normal"
|
|
43
|
+
* disabled={isLoading}
|
|
44
|
+
* loading={isLoading}
|
|
45
|
+
* hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
|
46
|
+
* style={styles.touchable}
|
|
47
|
+
* >
|
|
48
|
+
* <AtomicText>Press Me</AtomicText>
|
|
49
|
+
* </AtomicTouchable>
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export interface AtomicTouchableProps {
|
|
53
|
+
/**
|
|
54
|
+
* Content to render inside the touchable
|
|
55
|
+
*/
|
|
56
|
+
children: React.ReactNode;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Callback fired when the touchable is pressed
|
|
60
|
+
*/
|
|
61
|
+
onPress?: (event: GestureResponderEvent) => void;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Callback fired when press starts (finger down)
|
|
65
|
+
*/
|
|
66
|
+
onPressIn?: (event: GestureResponderEvent) => void;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Callback fired when press ends (finger up)
|
|
70
|
+
*/
|
|
71
|
+
onPressOut?: (event: GestureResponderEvent) => void;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Callback fired on long press (500ms default)
|
|
75
|
+
*/
|
|
76
|
+
onLongPress?: (event: GestureResponderEvent) => void;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Feedback variant
|
|
80
|
+
* @default 'opacity'
|
|
81
|
+
*/
|
|
82
|
+
feedback?: TouchableFeedback;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Feedback strength
|
|
86
|
+
* @default 'normal'
|
|
87
|
+
*/
|
|
88
|
+
strength?: FeedbackStrength;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Disable the touchable
|
|
92
|
+
* @default false
|
|
93
|
+
*/
|
|
94
|
+
disabled?: boolean;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Show loading indicator (disables touch)
|
|
98
|
+
* @default false
|
|
99
|
+
*/
|
|
100
|
+
loading?: boolean;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Hit slop - extends touchable area
|
|
104
|
+
* Useful for small touch targets
|
|
105
|
+
* @default undefined
|
|
106
|
+
*/
|
|
107
|
+
hitSlop?: HitSlop | number;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Custom style for the touchable container
|
|
111
|
+
*/
|
|
112
|
+
style?: StyleProp<ViewStyle>;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Style applied when pressed
|
|
116
|
+
*/
|
|
117
|
+
pressedStyle?: StyleProp<ViewStyle>;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Style applied when disabled
|
|
121
|
+
*/
|
|
122
|
+
disabledStyle?: StyleProp<ViewStyle>;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Accessibility label for screen readers
|
|
126
|
+
*/
|
|
127
|
+
accessibilityLabel?: string;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Accessibility hint for screen readers
|
|
131
|
+
*/
|
|
132
|
+
accessibilityHint?: string;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Accessibility role
|
|
136
|
+
* @default 'button'
|
|
137
|
+
*/
|
|
138
|
+
accessibilityRole?: 'button' | 'link' | 'none';
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Test ID for E2E testing
|
|
142
|
+
*/
|
|
143
|
+
testID?: string;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Delay before onLongPress is triggered (ms)
|
|
147
|
+
* @default 500
|
|
148
|
+
*/
|
|
149
|
+
delayLongPress?: number;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Ripple color (Android only, for 'ripple' feedback)
|
|
153
|
+
* @default theme primary color with alpha
|
|
154
|
+
*/
|
|
155
|
+
rippleColor?: string;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Border radius for ripple effect (Android only)
|
|
159
|
+
* @default 0
|
|
160
|
+
*/
|
|
161
|
+
rippleRadius?: number;
|
|
162
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useAppDesignTokens Hook - Dynamic Theme-Aware Design Tokens
|
|
3
|
+
*
|
|
4
|
+
* ✅ ZERO DUPLICATION - Uses TokenFactory (Single Source of Truth)
|
|
5
|
+
* ✅ DYNAMIC theme switching (light/dark)
|
|
6
|
+
* ✅ Type-safe design tokens
|
|
7
|
+
* ✅ Automatic re-render on theme change
|
|
8
|
+
* ✅ Graceful fallback to light theme
|
|
9
|
+
* ✅ NO CIRCULAR DEPENDENCY - Relative imports break barrel export cycle
|
|
10
|
+
*
|
|
11
|
+
* CRITICAL: Uses RELATIVE imports to break circular dependency!
|
|
12
|
+
* - Relative import for useTheme (not barrel '@domains/theme')
|
|
13
|
+
* - Relative import for TokenFactory (not barrel through AppDesignTokens)
|
|
14
|
+
* - NOT exported from AppDesignTokens.ts (only from design-system/index.ts)
|
|
15
|
+
*
|
|
16
|
+
* This architecture prevents cycle:
|
|
17
|
+
* - useAppDesignTokens → useTheme (relative, not through barrel)
|
|
18
|
+
* - theme → design-system (through barrel, but useAppDesignTokens not in token barrel)
|
|
19
|
+
* - No cycle detected!
|
|
20
|
+
*
|
|
21
|
+
* @module useAppDesignTokens
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { useMemo } from 'react';
|
|
25
|
+
import { useTheme } from '../../../theme/infrastructure/stores/themeStore';
|
|
26
|
+
import { createDesignTokens, STATIC_DESIGN_TOKENS, type ThemeMode } from '../tokens/core/TokenFactory';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 🎯 DYNAMIC DESIGN TOKENS HOOK
|
|
30
|
+
*
|
|
31
|
+
* USE THIS HOOK in all components for theme-aware design tokens!
|
|
32
|
+
*
|
|
33
|
+
* ✅ Colors are DYNAMIC (update when theme changes)
|
|
34
|
+
* ✅ Typography, spacing, etc. are STATIC (performance optimization)
|
|
35
|
+
* ✅ Automatic re-render on theme change
|
|
36
|
+
* ✅ Zero duplication (uses TokenFactory)
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```typescript
|
|
40
|
+
* import { useAppDesignTokens } from '@domains/design-system';
|
|
41
|
+
*
|
|
42
|
+
* const MyComponent = () => {
|
|
43
|
+
* const tokens = useAppDesignTokens();
|
|
44
|
+
* return (
|
|
45
|
+
* <View style={{
|
|
46
|
+
* backgroundColor: tokens.colors.primary,
|
|
47
|
+
* padding: tokens.spacing.md,
|
|
48
|
+
* borderRadius: tokens.borders.radius.md
|
|
49
|
+
* }}>
|
|
50
|
+
* <Text style={tokens.typography.bodyLarge}>Hello!</Text>
|
|
51
|
+
* </View>
|
|
52
|
+
* );
|
|
53
|
+
* };
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export const useAppDesignTokens = () => {
|
|
57
|
+
// ✅ Hooks must be called unconditionally at the top level
|
|
58
|
+
const { theme, themeMode: mode } = useTheme();
|
|
59
|
+
|
|
60
|
+
return useMemo(() => {
|
|
61
|
+
try {
|
|
62
|
+
// Validate theme mode
|
|
63
|
+
const themeMode: ThemeMode = mode === 'dark' ? 'dark' : 'light';
|
|
64
|
+
|
|
65
|
+
// Validate theme colors exist
|
|
66
|
+
if (!theme?.colors || typeof theme.colors !== 'object' || !theme.colors.primary) {
|
|
67
|
+
console.warn('useAppDesignTokens: Invalid theme, using light theme fallback');
|
|
68
|
+
return STATIC_DESIGN_TOKENS; // Fallback to light theme
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ✅ Create tokens using TokenFactory (ZERO duplication!)
|
|
72
|
+
return createDesignTokens(themeMode);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.warn('useAppDesignTokens: Error accessing theme, using fallback:', error);
|
|
75
|
+
return STATIC_DESIGN_TOKENS; // Fallback to light theme
|
|
76
|
+
}
|
|
77
|
+
}, [theme?.colors, mode]); // Re-compute when colors or mode changes
|
|
78
|
+
};
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useResponsive Hook
|
|
3
|
+
*
|
|
4
|
+
* React Hook for accessing responsive utilities with real-time dimension updates
|
|
5
|
+
* and safe area insets integration.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* ```tsx
|
|
9
|
+
* const { logoSize, inputHeight, fabPosition, isSmallDevice } = useResponsive();
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { useWindowDimensions } from 'react-native';
|
|
14
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
15
|
+
import {
|
|
16
|
+
getResponsiveLogoSize,
|
|
17
|
+
getResponsiveInputHeight,
|
|
18
|
+
getResponsiveHorizontalPadding,
|
|
19
|
+
getResponsiveBottomPosition,
|
|
20
|
+
getResponsiveFABPosition,
|
|
21
|
+
getResponsiveModalMaxHeight,
|
|
22
|
+
getResponsiveMinModalHeight,
|
|
23
|
+
getResponsiveIconContainerSize,
|
|
24
|
+
getResponsiveGridColumns,
|
|
25
|
+
getResponsiveMaxWidth,
|
|
26
|
+
getResponsiveFontSize,
|
|
27
|
+
isSmallPhone,
|
|
28
|
+
isTablet,
|
|
29
|
+
isLandscape,
|
|
30
|
+
getDeviceType,
|
|
31
|
+
getMinTouchTargetSize,
|
|
32
|
+
getSpacingMultiplier,
|
|
33
|
+
getOnboardingIconMarginTop,
|
|
34
|
+
getOnboardingIconMarginBottom,
|
|
35
|
+
getOnboardingTitleMarginBottom,
|
|
36
|
+
getOnboardingTextPadding,
|
|
37
|
+
getOnboardingDescriptionMarginTop,
|
|
38
|
+
getOnboardingIconSize,
|
|
39
|
+
getFormBottomPadding,
|
|
40
|
+
getInputIconSize,
|
|
41
|
+
getFormContentWidth,
|
|
42
|
+
getFormElementSpacing,
|
|
43
|
+
DeviceType,
|
|
44
|
+
} from '../utils/responsive';
|
|
45
|
+
|
|
46
|
+
export interface UseResponsiveReturn {
|
|
47
|
+
// Device info
|
|
48
|
+
width: number;
|
|
49
|
+
height: number;
|
|
50
|
+
isSmallDevice: boolean;
|
|
51
|
+
isTabletDevice: boolean;
|
|
52
|
+
isLandscapeMode: boolean;
|
|
53
|
+
deviceType: DeviceType;
|
|
54
|
+
|
|
55
|
+
// Safe area insets
|
|
56
|
+
insets: {
|
|
57
|
+
top: number;
|
|
58
|
+
bottom: number;
|
|
59
|
+
left: number;
|
|
60
|
+
right: number;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Responsive sizes
|
|
64
|
+
logoSize: number;
|
|
65
|
+
inputHeight: number;
|
|
66
|
+
iconContainerSize: number;
|
|
67
|
+
maxContentWidth: number;
|
|
68
|
+
minTouchTarget: number;
|
|
69
|
+
|
|
70
|
+
// Responsive positioning
|
|
71
|
+
horizontalPadding: number;
|
|
72
|
+
bottomPosition: number;
|
|
73
|
+
fabPosition: { bottom: number; right: number };
|
|
74
|
+
|
|
75
|
+
// Responsive layout
|
|
76
|
+
modalMaxHeight: string;
|
|
77
|
+
modalMinHeight: number;
|
|
78
|
+
gridColumns: number;
|
|
79
|
+
spacingMultiplier: number;
|
|
80
|
+
|
|
81
|
+
// Onboarding-specific spacing (pre-calculated, device-aware)
|
|
82
|
+
onboardingIconMarginTop: number;
|
|
83
|
+
onboardingIconMarginBottom: number;
|
|
84
|
+
onboardingIconSize: number;
|
|
85
|
+
onboardingTitleMarginBottom: number;
|
|
86
|
+
onboardingTextPadding: number;
|
|
87
|
+
onboardingDescriptionMarginTop: number;
|
|
88
|
+
|
|
89
|
+
// Form-specific spacing (pre-calculated, universal)
|
|
90
|
+
formBottomPadding: number;
|
|
91
|
+
inputIconSize: number;
|
|
92
|
+
formContentWidth: number | undefined;
|
|
93
|
+
formElementSpacing: number;
|
|
94
|
+
|
|
95
|
+
// Utility functions
|
|
96
|
+
getLogoSize: (baseSize?: number) => number;
|
|
97
|
+
getInputHeight: (baseHeight?: number) => number;
|
|
98
|
+
getIconSize: (baseSize?: number) => number;
|
|
99
|
+
getMaxWidth: (baseWidth?: number) => number;
|
|
100
|
+
getFontSize: (baseFontSize: number) => number;
|
|
101
|
+
getGridCols: (mobile?: number, tablet?: number) => number;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Hook for responsive design utilities
|
|
106
|
+
* Automatically updates when screen dimensions or orientation changes
|
|
107
|
+
*/
|
|
108
|
+
export const useResponsive = (): UseResponsiveReturn => {
|
|
109
|
+
const { width, height } = useWindowDimensions();
|
|
110
|
+
const insets = useSafeAreaInsets();
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
// Device info
|
|
114
|
+
width,
|
|
115
|
+
height,
|
|
116
|
+
isSmallDevice: isSmallPhone(),
|
|
117
|
+
isTabletDevice: isTablet(),
|
|
118
|
+
isLandscapeMode: isLandscape(),
|
|
119
|
+
deviceType: getDeviceType(),
|
|
120
|
+
|
|
121
|
+
// Safe area insets
|
|
122
|
+
insets,
|
|
123
|
+
|
|
124
|
+
// Responsive sizes (with default values)
|
|
125
|
+
logoSize: getResponsiveLogoSize(),
|
|
126
|
+
inputHeight: getResponsiveInputHeight(),
|
|
127
|
+
iconContainerSize: getResponsiveIconContainerSize(),
|
|
128
|
+
maxContentWidth: getResponsiveMaxWidth(),
|
|
129
|
+
minTouchTarget: getMinTouchTargetSize(),
|
|
130
|
+
|
|
131
|
+
// Responsive positioning
|
|
132
|
+
horizontalPadding: getResponsiveHorizontalPadding(16, insets),
|
|
133
|
+
bottomPosition: getResponsiveBottomPosition(32, insets),
|
|
134
|
+
fabPosition: getResponsiveFABPosition(insets),
|
|
135
|
+
|
|
136
|
+
// Responsive layout
|
|
137
|
+
modalMaxHeight: getResponsiveModalMaxHeight(),
|
|
138
|
+
modalMinHeight: getResponsiveMinModalHeight(),
|
|
139
|
+
gridColumns: getResponsiveGridColumns(),
|
|
140
|
+
spacingMultiplier: getSpacingMultiplier(),
|
|
141
|
+
|
|
142
|
+
// Onboarding-specific spacing (pre-calculated, no component calculations)
|
|
143
|
+
onboardingIconMarginTop: getOnboardingIconMarginTop(),
|
|
144
|
+
onboardingIconMarginBottom: getOnboardingIconMarginBottom(),
|
|
145
|
+
onboardingIconSize: getOnboardingIconSize(),
|
|
146
|
+
onboardingTitleMarginBottom: getOnboardingTitleMarginBottom(),
|
|
147
|
+
onboardingTextPadding: getOnboardingTextPadding(),
|
|
148
|
+
onboardingDescriptionMarginTop: getOnboardingDescriptionMarginTop(),
|
|
149
|
+
|
|
150
|
+
// Form-specific spacing (pre-calculated, universal)
|
|
151
|
+
formBottomPadding: getFormBottomPadding(insets.bottom),
|
|
152
|
+
inputIconSize: getInputIconSize(),
|
|
153
|
+
formContentWidth: getFormContentWidth(),
|
|
154
|
+
formElementSpacing: getFormElementSpacing(),
|
|
155
|
+
|
|
156
|
+
// Utility functions (allow custom base values)
|
|
157
|
+
getLogoSize: (baseSize) => getResponsiveLogoSize(baseSize),
|
|
158
|
+
getInputHeight: (baseHeight) => getResponsiveInputHeight(baseHeight),
|
|
159
|
+
getIconSize: (baseSize) => getResponsiveIconContainerSize(baseSize),
|
|
160
|
+
getMaxWidth: (baseWidth) => getResponsiveMaxWidth(baseWidth),
|
|
161
|
+
getFontSize: (baseFontSize) => getResponsiveFontSize(baseFontSize),
|
|
162
|
+
getGridCols: (mobile, tablet) => getResponsiveGridColumns(mobile, tablet),
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Shorthand hook for just responsive sizes
|
|
168
|
+
*/
|
|
169
|
+
export const useResponsiveSizes = () => {
|
|
170
|
+
const { logoSize, inputHeight, iconContainerSize, maxContentWidth } = useResponsive();
|
|
171
|
+
return { logoSize, inputHeight, iconContainerSize, maxContentWidth };
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Shorthand hook for just device type checks
|
|
176
|
+
*/
|
|
177
|
+
export const useDeviceType = () => {
|
|
178
|
+
const { isSmallDevice, isTabletDevice, deviceType } = useResponsive();
|
|
179
|
+
return { isSmallDevice, isTabletDevice, deviceType };
|
|
180
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loading Domain - Public API
|
|
3
|
+
*
|
|
4
|
+
* Domain-Driven Design (DDD) Architecture
|
|
5
|
+
* Theme: {{THEME_NAME}} ({{CATEGORY}} category)
|
|
6
|
+
*
|
|
7
|
+
* This is the SINGLE SOURCE OF TRUTH for the Loading domain.
|
|
8
|
+
* ALL imports from this domain MUST go through this file.
|
|
9
|
+
*
|
|
10
|
+
* Architecture:
|
|
11
|
+
* - presentation/components: UI components (LoadingState, LoadingSpinner)
|
|
12
|
+
* - presentation/hooks: React hooks (useLoading)
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* import { LoadingState, LoadingSpinner, useLoading } from '@domains/design-system';
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// =============================================================================
|
|
19
|
+
// PRESENTATION LAYER - Components
|
|
20
|
+
// =============================================================================
|
|
21
|
+
|
|
22
|
+
export { LoadingState } from './presentation/components/LoadingState';
|
|
23
|
+
export type { LoadingStateProps, LoadingStateSize } from './presentation/components/LoadingState';
|
|
24
|
+
|
|
25
|
+
export { LoadingSpinner } from './presentation/components/LoadingSpinner';
|
|
26
|
+
export type {
|
|
27
|
+
LoadingSpinnerProps,
|
|
28
|
+
LoadingSpinnerSize,
|
|
29
|
+
LoadingSpinnerColor,
|
|
30
|
+
} from './presentation/components/LoadingSpinner';
|
|
31
|
+
|
|
32
|
+
// =============================================================================
|
|
33
|
+
// PRESENTATION LAYER - Hooks
|
|
34
|
+
// =============================================================================
|
|
35
|
+
|
|
36
|
+
export { useLoading } from './presentation/hooks/useLoading';
|
|
37
|
+
export type {
|
|
38
|
+
LoadingConfig,
|
|
39
|
+
UseLoadingReturn,
|
|
40
|
+
} from './presentation/hooks/useLoading';
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LoadingSpinner - Theme-Aware Activity Indicator
|
|
3
|
+
*
|
|
4
|
+
* Refactored from AtomicLoadingSpinner - now part of Loading domain
|
|
5
|
+
* Uses central useAppDesignTokens() hook for automatic theme switching
|
|
6
|
+
* Theme: {{THEME_NAME}} ({{CATEGORY}} category)
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - ✅ AUTOMATIC theme switching via useAppDesignTokens()
|
|
10
|
+
* - ✅ Multiple size variants (small, medium, large)
|
|
11
|
+
* - ✅ Dynamic color customization
|
|
12
|
+
* - ✅ Overlay support
|
|
13
|
+
* - ✅ Message display
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import React from 'react';
|
|
17
|
+
import { View, ActivityIndicator, ViewStyle } from 'react-native';
|
|
18
|
+
import { useAppDesignTokens } from '../../../hooks/useAppDesignTokens';
|
|
19
|
+
import { withAlpha } from '../../../tokens/AppDesignTokens';
|
|
20
|
+
import { AtomicText } from '../../../atoms/AtomicText';
|
|
21
|
+
|
|
22
|
+
// =============================================================================
|
|
23
|
+
// TYPE DEFINITIONS
|
|
24
|
+
// =============================================================================
|
|
25
|
+
|
|
26
|
+
export type LoadingSpinnerSize = 'small' | 'medium' | 'large';
|
|
27
|
+
export type LoadingSpinnerColor = 'primary' | 'secondary' | 'white';
|
|
28
|
+
|
|
29
|
+
export interface LoadingSpinnerProps {
|
|
30
|
+
size?: LoadingSpinnerSize;
|
|
31
|
+
color?: LoadingSpinnerColor;
|
|
32
|
+
message?: string;
|
|
33
|
+
overlay?: boolean;
|
|
34
|
+
style?: ViewStyle;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// =============================================================================
|
|
38
|
+
// SIZE VARIANTS
|
|
39
|
+
// =============================================================================
|
|
40
|
+
|
|
41
|
+
const sizeVariants: Record<LoadingSpinnerSize, 'small' | 'large'> = {
|
|
42
|
+
small: 'small',
|
|
43
|
+
medium: 'large',
|
|
44
|
+
large: 'large',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// =============================================================================
|
|
48
|
+
// COMPONENT IMPLEMENTATION
|
|
49
|
+
// =============================================================================
|
|
50
|
+
|
|
51
|
+
export const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({
|
|
52
|
+
size = 'medium',
|
|
53
|
+
color = 'primary',
|
|
54
|
+
message,
|
|
55
|
+
overlay = false,
|
|
56
|
+
style,
|
|
57
|
+
}) => {
|
|
58
|
+
// ✅ DYNAMIC tokens from central hook
|
|
59
|
+
const tokens = useAppDesignTokens();
|
|
60
|
+
|
|
61
|
+
const spinnerSize = sizeVariants[size];
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get spinner color from dynamic theme
|
|
65
|
+
* ✅ Automatically updates when theme changes
|
|
66
|
+
*/
|
|
67
|
+
const getSpinnerColor = (): string => {
|
|
68
|
+
switch (color) {
|
|
69
|
+
case 'primary':
|
|
70
|
+
return tokens.colors.primary;
|
|
71
|
+
case 'secondary':
|
|
72
|
+
return tokens.colors.secondary;
|
|
73
|
+
case 'white':
|
|
74
|
+
return tokens.colors.textInverse;
|
|
75
|
+
default:
|
|
76
|
+
return tokens.colors.primary;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const spinnerColor = getSpinnerColor();
|
|
81
|
+
|
|
82
|
+
const containerStyle: ViewStyle = overlay
|
|
83
|
+
? {
|
|
84
|
+
position: 'absolute',
|
|
85
|
+
top: 0,
|
|
86
|
+
left: 0,
|
|
87
|
+
right: 0,
|
|
88
|
+
bottom: 0,
|
|
89
|
+
backgroundColor: withAlpha(tokens.colors.black, 0.5),
|
|
90
|
+
justifyContent: 'center',
|
|
91
|
+
alignItems: 'center',
|
|
92
|
+
zIndex: 9999,
|
|
93
|
+
}
|
|
94
|
+
: {
|
|
95
|
+
justifyContent: 'center',
|
|
96
|
+
alignItems: 'center',
|
|
97
|
+
padding: tokens.spacing.lg,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<View style={[containerStyle, style]}>
|
|
102
|
+
<ActivityIndicator size={spinnerSize} color={spinnerColor} />
|
|
103
|
+
{message && (
|
|
104
|
+
<AtomicText
|
|
105
|
+
type="bodyMedium"
|
|
106
|
+
color={overlay ? 'inverse' : 'primary'}
|
|
107
|
+
style={{ marginTop: tokens.spacing.md, textAlign: 'center' }}
|
|
108
|
+
>
|
|
109
|
+
{message}
|
|
110
|
+
</AtomicText>
|
|
111
|
+
)}
|
|
112
|
+
</View>
|
|
113
|
+
);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export default LoadingSpinner;
|