@umituz/react-native-loading 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.
@@ -0,0 +1,295 @@
1
+ /**
2
+ * Loading Domain - Entity Definitions
3
+ *
4
+ * Core types and interfaces for loading states and animations.
5
+ * Provides consistent loading UX across all apps.
6
+ *
7
+ * @domain loading
8
+ * @layer domain/entities
9
+ */
10
+
11
+ /**
12
+ * Loading animation type
13
+ */
14
+ export type LoadingType = 'pulse' | 'spinner' | 'dots' | 'skeleton';
15
+
16
+ /**
17
+ * Loading size preset
18
+ */
19
+ export type LoadingSize = 'small' | 'medium' | 'large';
20
+
21
+ /**
22
+ * Skeleton loader pattern
23
+ */
24
+ export type SkeletonPattern = 'list' | 'card' | 'profile' | 'text' | 'custom';
25
+
26
+ /**
27
+ * Loading configuration
28
+ */
29
+ export interface LoadingConfig {
30
+ /** Animation type */
31
+ type: LoadingType;
32
+ /** Size preset */
33
+ size: LoadingSize;
34
+ /** Loading emoji (customizable per app) */
35
+ emoji?: string;
36
+ /** Loading message */
37
+ message?: string;
38
+ /** Full screen mode */
39
+ fullScreen?: boolean;
40
+ }
41
+
42
+ /**
43
+ * Size configuration for each preset
44
+ */
45
+ export interface SizeConfig {
46
+ emojiSize: number;
47
+ showMessage: boolean;
48
+ spacing: number;
49
+ }
50
+
51
+ /**
52
+ * Skeleton loader configuration
53
+ */
54
+ export interface SkeletonConfig {
55
+ width?: number | string;
56
+ height?: number;
57
+ borderRadius?: number;
58
+ marginBottom?: number;
59
+ }
60
+
61
+ /**
62
+ * Animation timing configuration
63
+ */
64
+ export interface AnimationConfig {
65
+ duration: number;
66
+ toValue: number;
67
+ easing?: 'linear' | 'ease' | 'easeIn' | 'easeOut' | 'easeInOut';
68
+ }
69
+
70
+ /**
71
+ * Size configurations for loading states
72
+ */
73
+ export const SIZE_CONFIGS: Record<LoadingSize, SizeConfig> = {
74
+ small: {
75
+ emojiSize: 32,
76
+ showMessage: false,
77
+ spacing: 8,
78
+ },
79
+ medium: {
80
+ emojiSize: 48,
81
+ showMessage: true,
82
+ spacing: 12,
83
+ },
84
+ large: {
85
+ emojiSize: 64,
86
+ showMessage: true,
87
+ spacing: 16,
88
+ },
89
+ };
90
+
91
+ /**
92
+ * App-specific emoji presets
93
+ * Apps can override the default emoji based on their theme
94
+ */
95
+ export const LOADING_EMOJIS = {
96
+ meditation: '🧘',
97
+ fitness: '💪',
98
+ workout: '🏋️',
99
+ running: '🏃',
100
+ cycling: '🚴',
101
+ yoga: '🧘‍♀️',
102
+ health: '🏥',
103
+ nutrition: '🥗',
104
+ productivity: '⏳',
105
+ education: '📚',
106
+ reading: '📖',
107
+ music: '🎵',
108
+ art: '🎨',
109
+ travel: '✈️',
110
+ finance: '💰',
111
+ shopping: '🛍️',
112
+ cooking: '👨‍🍳',
113
+ gaming: '🎮',
114
+ default: '⌛',
115
+ } as const;
116
+
117
+ /**
118
+ * Animation configurations
119
+ */
120
+ export const ANIMATION_CONFIGS = {
121
+ pulse: {
122
+ duration: 1000,
123
+ toValue: 1.15,
124
+ easing: 'easeInOut' as const,
125
+ },
126
+ spinner: {
127
+ duration: 1000,
128
+ toValue: 360,
129
+ easing: 'linear' as const,
130
+ },
131
+ dots: {
132
+ duration: 500,
133
+ toValue: 1,
134
+ easing: 'easeInOut' as const,
135
+ },
136
+ skeleton: {
137
+ duration: 1200,
138
+ toValue: 1,
139
+ easing: 'linear' as const,
140
+ },
141
+ } as const;
142
+
143
+ /**
144
+ * Skeleton pattern configurations
145
+ */
146
+ export const SKELETON_PATTERNS: Record<SkeletonPattern, SkeletonConfig[]> = {
147
+ list: [
148
+ { width: '100%', height: 60, borderRadius: 8, marginBottom: 12 },
149
+ ],
150
+ card: [
151
+ { width: '100%', height: 200, borderRadius: 12, marginBottom: 16 },
152
+ { width: '80%', height: 20, borderRadius: 4, marginBottom: 8 },
153
+ { width: '60%', height: 16, borderRadius: 4, marginBottom: 0 },
154
+ ],
155
+ profile: [
156
+ { width: 80, height: 80, borderRadius: 40, marginBottom: 16 },
157
+ { width: '60%', height: 24, borderRadius: 4, marginBottom: 8 },
158
+ { width: '40%', height: 16, borderRadius: 4, marginBottom: 0 },
159
+ ],
160
+ text: [
161
+ { width: '100%', height: 16, borderRadius: 4, marginBottom: 8 },
162
+ { width: '90%', height: 16, borderRadius: 4, marginBottom: 8 },
163
+ { width: '95%', height: 16, borderRadius: 4, marginBottom: 0 },
164
+ ],
165
+ custom: [],
166
+ };
167
+
168
+ /**
169
+ * Loading utility class
170
+ */
171
+ export class LoadingUtils {
172
+ /**
173
+ * Get emoji for app category
174
+ */
175
+ static getEmojiForCategory(category: string): string {
176
+ const normalizedCategory = category.toLowerCase();
177
+
178
+ if (normalizedCategory.includes('meditation') || normalizedCategory.includes('mindfulness')) {
179
+ return LOADING_EMOJIS.meditation;
180
+ }
181
+ if (normalizedCategory.includes('fitness') || normalizedCategory.includes('gym')) {
182
+ return LOADING_EMOJIS.fitness;
183
+ }
184
+ if (normalizedCategory.includes('workout')) {
185
+ return LOADING_EMOJIS.workout;
186
+ }
187
+ if (normalizedCategory.includes('running') || normalizedCategory.includes('run')) {
188
+ return LOADING_EMOJIS.running;
189
+ }
190
+ if (normalizedCategory.includes('cycling') || normalizedCategory.includes('bike')) {
191
+ return LOADING_EMOJIS.cycling;
192
+ }
193
+ if (normalizedCategory.includes('yoga')) {
194
+ return LOADING_EMOJIS.yoga;
195
+ }
196
+ if (normalizedCategory.includes('health') || normalizedCategory.includes('medical')) {
197
+ return LOADING_EMOJIS.health;
198
+ }
199
+ if (normalizedCategory.includes('nutrition') || normalizedCategory.includes('diet')) {
200
+ return LOADING_EMOJIS.nutrition;
201
+ }
202
+ if (normalizedCategory.includes('productivity') || normalizedCategory.includes('task')) {
203
+ return LOADING_EMOJIS.productivity;
204
+ }
205
+ if (normalizedCategory.includes('education') || normalizedCategory.includes('learn')) {
206
+ return LOADING_EMOJIS.education;
207
+ }
208
+ if (normalizedCategory.includes('reading') || normalizedCategory.includes('book')) {
209
+ return LOADING_EMOJIS.reading;
210
+ }
211
+ if (normalizedCategory.includes('music') || normalizedCategory.includes('audio')) {
212
+ return LOADING_EMOJIS.music;
213
+ }
214
+ if (normalizedCategory.includes('art') || normalizedCategory.includes('creative')) {
215
+ return LOADING_EMOJIS.art;
216
+ }
217
+ if (normalizedCategory.includes('travel') || normalizedCategory.includes('trip')) {
218
+ return LOADING_EMOJIS.travel;
219
+ }
220
+ if (normalizedCategory.includes('finance') || normalizedCategory.includes('money')) {
221
+ return LOADING_EMOJIS.finance;
222
+ }
223
+ if (normalizedCategory.includes('shopping') || normalizedCategory.includes('shop')) {
224
+ return LOADING_EMOJIS.shopping;
225
+ }
226
+ if (normalizedCategory.includes('cooking') || normalizedCategory.includes('recipe')) {
227
+ return LOADING_EMOJIS.cooking;
228
+ }
229
+ if (normalizedCategory.includes('gaming') || normalizedCategory.includes('game')) {
230
+ return LOADING_EMOJIS.gaming;
231
+ }
232
+
233
+ return LOADING_EMOJIS.default;
234
+ }
235
+
236
+ /**
237
+ * Get default loading config
238
+ */
239
+ static getDefaultConfig(overrides?: Partial<LoadingConfig>): LoadingConfig {
240
+ return {
241
+ type: 'pulse',
242
+ size: 'large',
243
+ emoji: LOADING_EMOJIS.default,
244
+ fullScreen: false,
245
+ ...overrides,
246
+ };
247
+ }
248
+
249
+ /**
250
+ * Get size config
251
+ */
252
+ static getSizeConfig(size: LoadingSize): SizeConfig {
253
+ return SIZE_CONFIGS[size];
254
+ }
255
+
256
+ /**
257
+ * Get animation config
258
+ */
259
+ static getAnimationConfig(type: LoadingType): AnimationConfig {
260
+ return ANIMATION_CONFIGS[type];
261
+ }
262
+
263
+ /**
264
+ * Get skeleton pattern
265
+ */
266
+ static getSkeletonPattern(pattern: SkeletonPattern): SkeletonConfig[] {
267
+ return SKELETON_PATTERNS[pattern];
268
+ }
269
+
270
+ /**
271
+ * Validate loading config
272
+ */
273
+ static validateConfig(config: Partial<LoadingConfig>): LoadingConfig {
274
+ return {
275
+ type: config.type || 'pulse',
276
+ size: config.size || 'large',
277
+ emoji: config.emoji || LOADING_EMOJIS.default,
278
+ message: config.message,
279
+ fullScreen: config.fullScreen ?? false,
280
+ };
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Loading constants
286
+ */
287
+ export const LOADING_CONSTANTS = {
288
+ DEFAULT_TYPE: 'pulse' as LoadingType,
289
+ DEFAULT_SIZE: 'large' as LoadingSize,
290
+ DEFAULT_EMOJI: LOADING_EMOJIS.default,
291
+ BREATHING_CYCLE_DURATION: 2000, // 2 seconds (inhale + exhale)
292
+ SPINNER_ROTATION_DURATION: 1000, // 1 second
293
+ DOTS_WAVE_DURATION: 1500, // 1.5 seconds
294
+ SKELETON_SHIMMER_DURATION: 1200, // 1.2 seconds
295
+ } as const;
package/src/index.ts ADDED
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Loading Domain - Barrel Export
3
+ *
4
+ * Public API for the loading domain.
5
+ * Provides consistent loading states and animations across all apps.
6
+ *
7
+ * Features:
8
+ * - Breathing animation loading state (meditation-inspired)
9
+ * - Skeleton loaders with shimmer effect
10
+ * - Loading state management hooks
11
+ * - App-specific emoji presets
12
+ * - Configurable sizes and patterns
13
+ *
14
+ * Usage:
15
+ * ```tsx
16
+ * import {
17
+ * LoadingState,
18
+ * SkeletonLoader,
19
+ * useLoading,
20
+ * LOADING_EMOJIS,
21
+ * } from '@umituz/react-native-loading';
22
+ *
23
+ * // Basic loading state
24
+ * const MyScreen = () => {
25
+ * const { isLoading, startLoading, stopLoading } = useLoading();
26
+ *
27
+ * return (
28
+ * <View>
29
+ * {isLoading ? (
30
+ * <LoadingState message="Loading..." />
31
+ * ) : (
32
+ * <Content />
33
+ * )}
34
+ * </View>
35
+ * );
36
+ * };
37
+ *
38
+ * // Skeleton loader for lists
39
+ * const ListScreen = () => {
40
+ * const [data, setData] = useState([]);
41
+ * const { isLoading } = useLoading();
42
+ *
43
+ * return (
44
+ * <View>
45
+ * {isLoading ? (
46
+ * <SkeletonLoader pattern="list" count={5} />
47
+ * ) : (
48
+ * <FlatList data={data} ... />
49
+ * )}
50
+ * </View>
51
+ * );
52
+ * };
53
+ *
54
+ * // With async wrapper
55
+ * const DataScreen = () => {
56
+ * const { isLoading, loadingMessage, withLoading } = useLoading();
57
+ *
58
+ * const loadData = () => withLoading(
59
+ * fetchData(),
60
+ * 'Loading data...'
61
+ * );
62
+ *
63
+ * return (
64
+ * <View>
65
+ * {isLoading && <LoadingState message={loadingMessage} />}
66
+ * <Button onPress={loadData}>Load</Button>
67
+ * </View>
68
+ * );
69
+ * };
70
+ *
71
+ * // Custom emoji per app
72
+ * const FitnessLoadingScreen = () => (
73
+ * <LoadingState
74
+ * emoji={LOADING_EMOJIS.fitness}
75
+ * message="Loading workouts..."
76
+ * />
77
+ * );
78
+ * ```
79
+ */
80
+
81
+ // Domain Entities
82
+ export type {
83
+ LoadingType,
84
+ LoadingSize,
85
+ SkeletonPattern,
86
+ LoadingConfig,
87
+ SizeConfig,
88
+ SkeletonConfig,
89
+ AnimationConfig,
90
+ } from './domain/entities/Loading';
91
+
92
+ export {
93
+ SIZE_CONFIGS,
94
+ LOADING_EMOJIS,
95
+ ANIMATION_CONFIGS,
96
+ SKELETON_PATTERNS,
97
+ LoadingUtils,
98
+ LOADING_CONSTANTS,
99
+ } from './domain/entities/Loading';
100
+
101
+ // Presentation Components
102
+ export {
103
+ LoadingState,
104
+ type LoadingStateProps,
105
+ } from './presentation/components/LoadingState';
106
+
107
+ export {
108
+ SkeletonLoader,
109
+ type SkeletonLoaderProps,
110
+ } from './presentation/components/SkeletonLoader';
111
+
112
+ // Presentation Hooks
113
+ export {
114
+ useLoading,
115
+ useSimpleLoading,
116
+ type UseLoadingReturn,
117
+ } from './presentation/hooks/useLoading';
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Loading Domain - LoadingState Component
3
+ *
4
+ * Universal loading component with breathing animation.
5
+ * Provides consistent, calm loading UX across all apps.
6
+ *
7
+ * Adapted from meditation_timer app's LoadingState component.
8
+ *
9
+ * @domain loading
10
+ * @layer presentation/components
11
+ */
12
+
13
+ import React, { useEffect, useRef } from 'react';
14
+ import { View, StyleSheet, Animated, Easing, type StyleProp, type ViewStyle, type TextStyle } from 'react-native';
15
+ import { useAppDesignTokens, AtomicText } from '@umituz/react-native-design-system';
16
+ import type { LoadingSize } from '../../domain/entities/Loading';
17
+ import {
18
+ SIZE_CONFIGS,
19
+ LOADING_EMOJIS,
20
+ LOADING_CONSTANTS,
21
+ } from '../../domain/entities/Loading';
22
+
23
+ /**
24
+ * LoadingState component props
25
+ */
26
+ export interface LoadingStateProps {
27
+ /** Loading emoji - default: ⌛ (customizable per app) */
28
+ emoji?: string;
29
+ /** Loading message (optional) */
30
+ message?: string;
31
+ /** Size: small (inline), medium (section), large (full screen) */
32
+ size?: LoadingSize;
33
+ /** Full screen layout vs inline */
34
+ fullScreen?: boolean;
35
+ /** Custom container style */
36
+ style?: StyleProp<ViewStyle>;
37
+ /** Custom message style */
38
+ messageStyle?: StyleProp<TextStyle>;
39
+ }
40
+
41
+ /**
42
+ * LoadingState Component
43
+ *
44
+ * Universal loading indicator with breathing animation.
45
+ * Creates a calm, mindful loading experience.
46
+ *
47
+ * USAGE:
48
+ * ```typescript
49
+ * // Basic usage
50
+ * <LoadingState />
51
+ *
52
+ * // With message
53
+ * <LoadingState message="Loading data..." size="medium" />
54
+ *
55
+ * // Full screen
56
+ * <LoadingState fullScreen message="Please wait..." />
57
+ *
58
+ * // Custom emoji (per app theme)
59
+ * <LoadingState emoji="🧘" message="Loading meditations..." />
60
+ *
61
+ * // Inline loading
62
+ * <LoadingState size="small" />
63
+ * ```
64
+ */
65
+ export const LoadingState: React.FC<LoadingStateProps> = ({
66
+ emoji = LOADING_EMOJIS.default,
67
+ message,
68
+ size = 'large',
69
+ fullScreen = false,
70
+ style,
71
+ messageStyle,
72
+ }) => {
73
+ const tokens = useAppDesignTokens();
74
+ const config = SIZE_CONFIGS[size];
75
+
76
+ // Animated value for emoji pulse (breathing effect)
77
+ const scaleAnim = useRef(new Animated.Value(1)).current;
78
+
79
+ useEffect(() => {
80
+ // Meditation breathing animation: 2 second cycle (inhale/exhale)
81
+ const breathingAnimation = Animated.loop(
82
+ Animated.sequence([
83
+ // Inhale - expand
84
+ Animated.timing(scaleAnim, {
85
+ toValue: 1.15,
86
+ duration: 1000,
87
+ easing: Easing.inOut(Easing.ease),
88
+ useNativeDriver: true,
89
+ }),
90
+ // Exhale - contract
91
+ Animated.timing(scaleAnim, {
92
+ toValue: 1,
93
+ duration: 1000,
94
+ easing: Easing.inOut(Easing.ease),
95
+ useNativeDriver: true,
96
+ }),
97
+ ])
98
+ );
99
+
100
+ breathingAnimation.start();
101
+
102
+ return () => {
103
+ breathingAnimation.stop();
104
+ };
105
+ }, [scaleAnim]);
106
+
107
+ return (
108
+ <View
109
+ style={[
110
+ styles.container,
111
+ fullScreen && styles.fullScreen,
112
+ { gap: config.spacing },
113
+ style,
114
+ ]}
115
+ >
116
+ {/* Animated Emoji with breathing pulse */}
117
+ <Animated.Text
118
+ style={[
119
+ styles.emoji,
120
+ {
121
+ fontSize: config.emojiSize,
122
+ transform: [{ scale: scaleAnim }],
123
+ },
124
+ ]}
125
+ >
126
+ {emoji}
127
+ </Animated.Text>
128
+
129
+ {/* Optional Loading Message */}
130
+ {config.showMessage && message && (
131
+ <AtomicText
132
+ type="bodySmall"
133
+ color="secondary"
134
+ style={[styles.message, messageStyle]}
135
+ >
136
+ {message}
137
+ </AtomicText>
138
+ )}
139
+ </View>
140
+ );
141
+ };
142
+
143
+ const styles = StyleSheet.create({
144
+ container: {
145
+ justifyContent: 'center',
146
+ alignItems: 'center',
147
+ paddingVertical: 20,
148
+ },
149
+ fullScreen: {
150
+ flex: 1,
151
+ paddingVertical: 60,
152
+ },
153
+ emoji: {
154
+ fontFamily: 'System', // Emoji font
155
+ },
156
+ message: {
157
+ textAlign: 'center',
158
+ opacity: 0.7,
159
+ paddingHorizontal: 32,
160
+ },
161
+ });