@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.
- package/LICENSE +22 -0
- package/README.md +111 -0
- package/lib/domain/entities/Loading.d.ts +161 -0
- package/lib/domain/entities/Loading.d.ts.map +1 -0
- package/lib/domain/entities/Loading.js +224 -0
- package/lib/domain/entities/Loading.js.map +1 -0
- package/lib/index.d.ts +85 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +86 -0
- package/lib/index.js.map +1 -0
- package/lib/presentation/components/LoadingState.d.ts +57 -0
- package/lib/presentation/components/LoadingState.d.ts.map +1 -0
- package/lib/presentation/components/LoadingState.js +110 -0
- package/lib/presentation/components/LoadingState.js.map +1 -0
- package/lib/presentation/components/SkeletonLoader.d.ts +59 -0
- package/lib/presentation/components/SkeletonLoader.d.ts.map +1 -0
- package/lib/presentation/components/SkeletonLoader.js +112 -0
- package/lib/presentation/components/SkeletonLoader.js.map +1 -0
- package/lib/presentation/hooks/useLoading.d.ts +67 -0
- package/lib/presentation/hooks/useLoading.d.ts.map +1 -0
- package/lib/presentation/hooks/useLoading.js +124 -0
- package/lib/presentation/hooks/useLoading.js.map +1 -0
- package/package.json +55 -0
- package/src/USAGE_EXAMPLES.md +429 -0
- package/src/domain/entities/Loading.ts +295 -0
- package/src/index.ts +117 -0
- package/src/presentation/components/LoadingState.tsx +161 -0
- package/src/presentation/components/SkeletonLoader.tsx +159 -0
- package/src/presentation/hooks/useLoading.ts +170 -0
|
@@ -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
|
+
});
|