@umituz/react-native-loading 1.1.0 โ 1.2.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/package.json +6 -4
- package/src/domain/entities/Loading.ts +3 -210
- package/src/index.ts +10 -76
- package/src/presentation/components/LoadingState.tsx +7 -52
- package/src/presentation/components/SkeletonLoader.tsx +6 -50
- package/src/presentation/hooks/useLoading.ts +9 -97
- package/src/USAGE_EXAMPLES.md +0 -429
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-loading",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Loading states and animations for React Native apps with breathing animations, skeleton loaders, and state management hooks",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"animation",
|
|
23
23
|
"spinner"
|
|
24
24
|
],
|
|
25
|
-
"author": "
|
|
25
|
+
"author": "รmit UZ <umit@umituz.com>",
|
|
26
26
|
"license": "MIT",
|
|
27
27
|
"repository": {
|
|
28
28
|
"type": "git",
|
|
@@ -36,10 +36,12 @@
|
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/react": "^18.2.45",
|
|
38
38
|
"@types/react-native": "^0.73.0",
|
|
39
|
-
"@umituz/react-native-design-system": "^1.5.
|
|
39
|
+
"@umituz/react-native-design-system": "^1.5.28",
|
|
40
|
+
"@umituz/react-native-design-system-theme": "latest",
|
|
40
41
|
"react": "^18.2.0",
|
|
41
42
|
"react-native": "^0.74.0",
|
|
42
|
-
"typescript": "^5.3.3"
|
|
43
|
+
"typescript": "^5.3.3",
|
|
44
|
+
"zustand": "^5.0.2"
|
|
43
45
|
},
|
|
44
46
|
"publishConfig": {
|
|
45
47
|
"access": "public"
|
|
@@ -1,18 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Loading Domain - Entity Definitions
|
|
3
3
|
*
|
|
4
|
-
* Core types and
|
|
5
|
-
* Provides consistent loading UX across all apps.
|
|
4
|
+
* Core types and configurations for loading states.
|
|
6
5
|
*
|
|
7
6
|
* @domain loading
|
|
8
7
|
* @layer domain/entities
|
|
9
8
|
*/
|
|
10
9
|
|
|
11
|
-
/**
|
|
12
|
-
* Loading animation type
|
|
13
|
-
*/
|
|
14
|
-
export type LoadingType = 'pulse' | 'spinner' | 'dots' | 'skeleton';
|
|
15
|
-
|
|
16
10
|
/**
|
|
17
11
|
* Loading size preset
|
|
18
12
|
*/
|
|
@@ -23,22 +17,6 @@ export type LoadingSize = 'small' | 'medium' | 'large';
|
|
|
23
17
|
*/
|
|
24
18
|
export type SkeletonPattern = 'list' | 'card' | 'profile' | 'text' | 'custom';
|
|
25
19
|
|
|
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
20
|
/**
|
|
43
21
|
* Size configuration for each preset
|
|
44
22
|
*/
|
|
@@ -58,15 +36,6 @@ export interface SkeletonConfig {
|
|
|
58
36
|
marginBottom?: number;
|
|
59
37
|
}
|
|
60
38
|
|
|
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
39
|
/**
|
|
71
40
|
* Size configurations for loading states
|
|
72
41
|
*/
|
|
@@ -88,58 +57,6 @@ export const SIZE_CONFIGS: Record<LoadingSize, SizeConfig> = {
|
|
|
88
57
|
},
|
|
89
58
|
};
|
|
90
59
|
|
|
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
60
|
/**
|
|
144
61
|
* Skeleton pattern configurations
|
|
145
62
|
*/
|
|
@@ -166,130 +83,6 @@ export const SKELETON_PATTERNS: Record<SkeletonPattern, SkeletonConfig[]> = {
|
|
|
166
83
|
};
|
|
167
84
|
|
|
168
85
|
/**
|
|
169
|
-
*
|
|
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
|
|
86
|
+
* Default emoji for loading state
|
|
286
87
|
*/
|
|
287
|
-
export const
|
|
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;
|
|
88
|
+
export const DEFAULT_LOADING_EMOJI = 'โณ';
|
package/src/index.ts
CHANGED
|
@@ -1,101 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* @umituz/react-native-loading
|
|
3
3
|
*
|
|
4
|
-
*
|
|
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
|
|
4
|
+
* Loading states and skeleton loaders for React Native apps.
|
|
13
5
|
*
|
|
14
6
|
* Usage:
|
|
15
7
|
* ```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();
|
|
8
|
+
* import { LoadingState, SkeletonLoader, useLoading } from '@umituz/react-native-loading';
|
|
57
9
|
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* 'Loading data...'
|
|
61
|
-
* );
|
|
10
|
+
* // Full screen loading with custom emoji
|
|
11
|
+
* <LoadingState emoji="๐ฌ" fullScreen />
|
|
62
12
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* {isLoading && <LoadingState message={loadingMessage} />}
|
|
66
|
-
* <Button onPress={loadData}>Load</Button>
|
|
67
|
-
* </View>
|
|
68
|
-
* );
|
|
69
|
-
* };
|
|
13
|
+
* // With message
|
|
14
|
+
* <LoadingState emoji="โณ" message="Loading..." />
|
|
70
15
|
*
|
|
71
|
-
* //
|
|
72
|
-
*
|
|
73
|
-
* <LoadingState
|
|
74
|
-
* emoji={LOADING_EMOJIS.fitness}
|
|
75
|
-
* message="Loading workouts..."
|
|
76
|
-
* />
|
|
77
|
-
* );
|
|
16
|
+
* // Skeleton loader
|
|
17
|
+
* <SkeletonLoader pattern="list" count={5} />
|
|
78
18
|
* ```
|
|
79
19
|
*/
|
|
80
20
|
|
|
81
21
|
// Domain Entities
|
|
82
22
|
export type {
|
|
83
|
-
LoadingType,
|
|
84
23
|
LoadingSize,
|
|
85
24
|
SkeletonPattern,
|
|
86
|
-
LoadingConfig,
|
|
87
25
|
SizeConfig,
|
|
88
26
|
SkeletonConfig,
|
|
89
|
-
AnimationConfig,
|
|
90
27
|
} from './domain/entities/Loading';
|
|
91
28
|
|
|
92
29
|
export {
|
|
93
30
|
SIZE_CONFIGS,
|
|
94
|
-
LOADING_EMOJIS,
|
|
95
|
-
ANIMATION_CONFIGS,
|
|
96
31
|
SKELETON_PATTERNS,
|
|
97
|
-
|
|
98
|
-
LOADING_CONSTANTS,
|
|
32
|
+
DEFAULT_LOADING_EMOJI,
|
|
99
33
|
} from './domain/entities/Loading';
|
|
100
34
|
|
|
101
35
|
// Presentation Components
|
|
@@ -1,30 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Loading
|
|
2
|
+
* Loading State Component
|
|
3
3
|
*
|
|
4
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
5
|
*/
|
|
12
6
|
|
|
13
7
|
import React, { useEffect, useRef } from 'react';
|
|
14
8
|
import { View, StyleSheet, Animated, Easing, type StyleProp, type ViewStyle, type TextStyle } from 'react-native';
|
|
15
|
-
import {
|
|
9
|
+
import { AtomicText } from '@umituz/react-native-design-system';
|
|
16
10
|
import type { LoadingSize } from '../../domain/entities/Loading';
|
|
17
|
-
import {
|
|
18
|
-
SIZE_CONFIGS,
|
|
19
|
-
LOADING_EMOJIS,
|
|
20
|
-
LOADING_CONSTANTS,
|
|
21
|
-
} from '../../domain/entities/Loading';
|
|
11
|
+
import { SIZE_CONFIGS, DEFAULT_LOADING_EMOJI } from '../../domain/entities/Loading';
|
|
22
12
|
|
|
23
|
-
/**
|
|
24
|
-
* LoadingState component props
|
|
25
|
-
*/
|
|
26
13
|
export interface LoadingStateProps {
|
|
27
|
-
/** Loading emoji -
|
|
14
|
+
/** Loading emoji - pass any emoji dynamically */
|
|
28
15
|
emoji?: string;
|
|
29
16
|
/** Loading message (optional) */
|
|
30
17
|
message?: string;
|
|
@@ -38,56 +25,26 @@ export interface LoadingStateProps {
|
|
|
38
25
|
messageStyle?: StyleProp<TextStyle>;
|
|
39
26
|
}
|
|
40
27
|
|
|
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
28
|
export const LoadingState: React.FC<LoadingStateProps> = ({
|
|
66
|
-
emoji =
|
|
29
|
+
emoji = DEFAULT_LOADING_EMOJI,
|
|
67
30
|
message,
|
|
68
31
|
size = 'large',
|
|
69
32
|
fullScreen = false,
|
|
70
33
|
style,
|
|
71
34
|
messageStyle,
|
|
72
35
|
}) => {
|
|
73
|
-
const tokens = useAppDesignTokens();
|
|
74
36
|
const config = SIZE_CONFIGS[size];
|
|
75
|
-
|
|
76
|
-
// Animated value for emoji pulse (breathing effect)
|
|
77
37
|
const scaleAnim = useRef(new Animated.Value(1)).current;
|
|
78
38
|
|
|
79
39
|
useEffect(() => {
|
|
80
|
-
// Meditation breathing animation: 2 second cycle (inhale/exhale)
|
|
81
40
|
const breathingAnimation = Animated.loop(
|
|
82
41
|
Animated.sequence([
|
|
83
|
-
// Inhale - expand
|
|
84
42
|
Animated.timing(scaleAnim, {
|
|
85
43
|
toValue: 1.15,
|
|
86
44
|
duration: 1000,
|
|
87
45
|
easing: Easing.inOut(Easing.ease),
|
|
88
46
|
useNativeDriver: true,
|
|
89
47
|
}),
|
|
90
|
-
// Exhale - contract
|
|
91
48
|
Animated.timing(scaleAnim, {
|
|
92
49
|
toValue: 1,
|
|
93
50
|
duration: 1000,
|
|
@@ -113,7 +70,6 @@ export const LoadingState: React.FC<LoadingStateProps> = ({
|
|
|
113
70
|
style,
|
|
114
71
|
]}
|
|
115
72
|
>
|
|
116
|
-
{/* Animated Emoji with breathing pulse */}
|
|
117
73
|
<Animated.Text
|
|
118
74
|
style={[
|
|
119
75
|
styles.emoji,
|
|
@@ -126,11 +82,10 @@ export const LoadingState: React.FC<LoadingStateProps> = ({
|
|
|
126
82
|
{emoji}
|
|
127
83
|
</Animated.Text>
|
|
128
84
|
|
|
129
|
-
{/* Optional Loading Message */}
|
|
130
85
|
{config.showMessage && message && (
|
|
131
86
|
<AtomicText
|
|
132
87
|
type="bodySmall"
|
|
133
|
-
color="
|
|
88
|
+
color="secondary"
|
|
134
89
|
style={[styles.message, messageStyle]}
|
|
135
90
|
>
|
|
136
91
|
{message}
|
|
@@ -151,7 +106,7 @@ const styles = StyleSheet.create({
|
|
|
151
106
|
paddingVertical: 60,
|
|
152
107
|
},
|
|
153
108
|
emoji: {
|
|
154
|
-
fontFamily: 'System',
|
|
109
|
+
fontFamily: 'System',
|
|
155
110
|
},
|
|
156
111
|
message: {
|
|
157
112
|
textAlign: 'center',
|
|
@@ -1,25 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Skeleton Loader Component
|
|
3
3
|
*
|
|
4
4
|
* Skeleton placeholder loader with shimmer animation.
|
|
5
|
-
* Provides visual feedback during content loading.
|
|
6
|
-
*
|
|
7
|
-
* @domain loading
|
|
8
|
-
* @layer presentation/components
|
|
9
5
|
*/
|
|
10
6
|
|
|
11
7
|
import React, { useEffect, useRef } from 'react';
|
|
12
8
|
import { View, StyleSheet, Animated, type StyleProp, type ViewStyle } from 'react-native';
|
|
13
9
|
import { useAppDesignTokens } from '@umituz/react-native-design-system';
|
|
14
10
|
import type { SkeletonPattern, SkeletonConfig } from '../../domain/entities/Loading';
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
} from '../../domain/entities/Loading';
|
|
11
|
+
import { SKELETON_PATTERNS } from '../../domain/entities/Loading';
|
|
12
|
+
|
|
13
|
+
const SHIMMER_DURATION = 1200;
|
|
19
14
|
|
|
20
|
-
/**
|
|
21
|
-
* SkeletonLoader component props
|
|
22
|
-
*/
|
|
23
15
|
export interface SkeletonLoaderProps {
|
|
24
16
|
/** Skeleton pattern preset */
|
|
25
17
|
pattern?: SkeletonPattern;
|
|
@@ -33,36 +25,6 @@ export interface SkeletonLoaderProps {
|
|
|
33
25
|
disableAnimation?: boolean;
|
|
34
26
|
}
|
|
35
27
|
|
|
36
|
-
/**
|
|
37
|
-
* SkeletonLoader Component
|
|
38
|
-
*
|
|
39
|
-
* Renders skeleton placeholders with shimmer animation.
|
|
40
|
-
* Provides visual feedback while content is loading.
|
|
41
|
-
*
|
|
42
|
-
* USAGE:
|
|
43
|
-
* ```typescript
|
|
44
|
-
* // List skeleton (default)
|
|
45
|
-
* <SkeletonLoader pattern="list" count={5} />
|
|
46
|
-
*
|
|
47
|
-
* // Card skeleton
|
|
48
|
-
* <SkeletonLoader pattern="card" count={3} />
|
|
49
|
-
*
|
|
50
|
-
* // Profile skeleton
|
|
51
|
-
* <SkeletonLoader pattern="profile" />
|
|
52
|
-
*
|
|
53
|
-
* // Text skeleton
|
|
54
|
-
* <SkeletonLoader pattern="text" count={3} />
|
|
55
|
-
*
|
|
56
|
-
* // Custom skeleton
|
|
57
|
-
* <SkeletonLoader
|
|
58
|
-
* pattern="custom"
|
|
59
|
-
* custom={[
|
|
60
|
-
* { width: 100, height: 100, borderRadius: 50 },
|
|
61
|
-
* { width: '80%', height: 20, borderRadius: 4 },
|
|
62
|
-
* ]}
|
|
63
|
-
* />
|
|
64
|
-
* ```
|
|
65
|
-
*/
|
|
66
28
|
export const SkeletonLoader: React.FC<SkeletonLoaderProps> = ({
|
|
67
29
|
pattern = 'list',
|
|
68
30
|
custom,
|
|
@@ -71,25 +33,21 @@ export const SkeletonLoader: React.FC<SkeletonLoaderProps> = ({
|
|
|
71
33
|
disableAnimation = false,
|
|
72
34
|
}) => {
|
|
73
35
|
const tokens = useAppDesignTokens();
|
|
74
|
-
|
|
75
|
-
// Get skeleton config
|
|
76
36
|
const skeletonConfigs = pattern === 'custom' && custom
|
|
77
37
|
? custom
|
|
78
38
|
: SKELETON_PATTERNS[pattern];
|
|
79
39
|
|
|
80
|
-
// Animated value for shimmer effect
|
|
81
40
|
const shimmerAnim = useRef(new Animated.Value(0)).current;
|
|
82
41
|
|
|
83
42
|
useEffect(() => {
|
|
84
43
|
if (disableAnimation) return;
|
|
85
44
|
|
|
86
|
-
// Shimmer animation: 1.2 second cycle
|
|
87
45
|
const shimmerAnimation = Animated.loop(
|
|
88
46
|
Animated.sequence([
|
|
89
47
|
Animated.timing(shimmerAnim, {
|
|
90
48
|
toValue: 1,
|
|
91
|
-
duration:
|
|
92
|
-
useNativeDriver: false,
|
|
49
|
+
duration: SHIMMER_DURATION,
|
|
50
|
+
useNativeDriver: false,
|
|
93
51
|
}),
|
|
94
52
|
Animated.timing(shimmerAnim, {
|
|
95
53
|
toValue: 0,
|
|
@@ -106,7 +64,6 @@ export const SkeletonLoader: React.FC<SkeletonLoaderProps> = ({
|
|
|
106
64
|
};
|
|
107
65
|
}, [shimmerAnim, disableAnimation]);
|
|
108
66
|
|
|
109
|
-
// Interpolate shimmer animation to background color
|
|
110
67
|
const backgroundColor = shimmerAnim.interpolate({
|
|
111
68
|
inputRange: [0, 0.5, 1],
|
|
112
69
|
outputRange: [
|
|
@@ -116,7 +73,6 @@ export const SkeletonLoader: React.FC<SkeletonLoaderProps> = ({
|
|
|
116
73
|
],
|
|
117
74
|
});
|
|
118
75
|
|
|
119
|
-
// Render skeleton items
|
|
120
76
|
const renderSkeletonItem = (index: number) => (
|
|
121
77
|
<View key={`skeleton-group-${index}`} style={styles.skeletonGroup}>
|
|
122
78
|
{skeletonConfigs.map((config, configIndex) => (
|
|
@@ -1,122 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Loading
|
|
2
|
+
* Loading Hook
|
|
3
3
|
*
|
|
4
4
|
* React hook for managing loading states.
|
|
5
|
-
* Provides consistent loading state management across components.
|
|
6
|
-
*
|
|
7
|
-
* @domain loading
|
|
8
|
-
* @layer presentation/hooks
|
|
9
5
|
*/
|
|
10
6
|
|
|
11
7
|
import { useState, useCallback } from 'react';
|
|
12
|
-
import type { LoadingType } from '../../domain/entities/Loading';
|
|
13
8
|
|
|
14
|
-
/**
|
|
15
|
-
* useLoading hook return type
|
|
16
|
-
*/
|
|
17
9
|
export interface UseLoadingReturn {
|
|
18
|
-
// Loading state
|
|
19
10
|
isLoading: boolean;
|
|
20
11
|
loadingMessage: string | null;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
// Actions
|
|
24
|
-
startLoading: (message?: string, type?: LoadingType) => void;
|
|
12
|
+
startLoading: (message?: string) => void;
|
|
25
13
|
stopLoading: () => void;
|
|
26
14
|
setLoadingMessage: (message: string | null) => void;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
// Async wrapper
|
|
30
|
-
withLoading: <T>(
|
|
31
|
-
asyncFn: Promise<T>,
|
|
32
|
-
message?: string,
|
|
33
|
-
type?: LoadingType
|
|
34
|
-
) => Promise<T>;
|
|
15
|
+
withLoading: <T>(asyncFn: Promise<T>, message?: string) => Promise<T>;
|
|
35
16
|
}
|
|
36
17
|
|
|
37
|
-
/**
|
|
38
|
-
* useLoading hook for loading state management
|
|
39
|
-
*
|
|
40
|
-
* USAGE:
|
|
41
|
-
* ```typescript
|
|
42
|
-
* const {
|
|
43
|
-
* isLoading,
|
|
44
|
-
* loadingMessage,
|
|
45
|
-
* startLoading,
|
|
46
|
-
* stopLoading,
|
|
47
|
-
* withLoading,
|
|
48
|
-
* } = useLoading();
|
|
49
|
-
*
|
|
50
|
-
* // Manual control
|
|
51
|
-
* const handleSave = async () => {
|
|
52
|
-
* startLoading('Saving data...');
|
|
53
|
-
* try {
|
|
54
|
-
* await saveData();
|
|
55
|
-
* } finally {
|
|
56
|
-
* stopLoading();
|
|
57
|
-
* }
|
|
58
|
-
* };
|
|
59
|
-
*
|
|
60
|
-
* // Automatic wrapper
|
|
61
|
-
* const handleLoad = () => withLoading(
|
|
62
|
-
* loadData(),
|
|
63
|
-
* 'Loading data...'
|
|
64
|
-
* );
|
|
65
|
-
*
|
|
66
|
-
* // In render
|
|
67
|
-
* {isLoading && <LoadingState message={loadingMessage} />}
|
|
68
|
-
* ```
|
|
69
|
-
*/
|
|
70
18
|
export const useLoading = (): UseLoadingReturn => {
|
|
71
19
|
const [isLoading, setIsLoading] = useState(false);
|
|
72
20
|
const [loadingMessage, setLoadingMessage] = useState<string | null>(null);
|
|
73
|
-
const [loadingType, setLoadingType] = useState<LoadingType>('pulse');
|
|
74
21
|
|
|
75
|
-
|
|
76
|
-
* Start loading state
|
|
77
|
-
*/
|
|
78
|
-
const startLoading = useCallback((message?: string, type: LoadingType = 'pulse') => {
|
|
22
|
+
const startLoading = useCallback((message?: string) => {
|
|
79
23
|
setIsLoading(true);
|
|
80
24
|
setLoadingMessage(message || null);
|
|
81
|
-
setLoadingType(type);
|
|
82
25
|
}, []);
|
|
83
26
|
|
|
84
|
-
/**
|
|
85
|
-
* Stop loading state
|
|
86
|
-
*/
|
|
87
27
|
const stopLoading = useCallback(() => {
|
|
88
28
|
setIsLoading(false);
|
|
89
29
|
setLoadingMessage(null);
|
|
90
30
|
}, []);
|
|
91
31
|
|
|
92
|
-
/**
|
|
93
|
-
* Update loading message
|
|
94
|
-
*/
|
|
95
32
|
const updateLoadingMessage = useCallback((message: string | null) => {
|
|
96
33
|
setLoadingMessage(message);
|
|
97
34
|
}, []);
|
|
98
35
|
|
|
99
|
-
/**
|
|
100
|
-
* Update loading type
|
|
101
|
-
*/
|
|
102
|
-
const updateLoadingType = useCallback((type: LoadingType) => {
|
|
103
|
-
setLoadingType(type);
|
|
104
|
-
}, []);
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Async wrapper that automatically manages loading state
|
|
108
|
-
*/
|
|
109
36
|
const withLoading = useCallback(
|
|
110
|
-
async <T,>(
|
|
111
|
-
|
|
112
|
-
message?: string,
|
|
113
|
-
type: LoadingType = 'pulse'
|
|
114
|
-
): Promise<T> => {
|
|
115
|
-
startLoading(message, type);
|
|
116
|
-
|
|
37
|
+
async <T,>(asyncFn: Promise<T>, message?: string): Promise<T> => {
|
|
38
|
+
startLoading(message);
|
|
117
39
|
try {
|
|
118
|
-
|
|
119
|
-
return result;
|
|
40
|
+
return await asyncFn;
|
|
120
41
|
} finally {
|
|
121
42
|
stopLoading();
|
|
122
43
|
}
|
|
@@ -125,24 +46,17 @@ export const useLoading = (): UseLoadingReturn => {
|
|
|
125
46
|
);
|
|
126
47
|
|
|
127
48
|
return {
|
|
128
|
-
// Loading state
|
|
129
49
|
isLoading,
|
|
130
50
|
loadingMessage,
|
|
131
|
-
loadingType,
|
|
132
|
-
|
|
133
|
-
// Actions
|
|
134
51
|
startLoading,
|
|
135
52
|
stopLoading,
|
|
136
53
|
setLoadingMessage: updateLoadingMessage,
|
|
137
|
-
setLoadingType: updateLoadingType,
|
|
138
|
-
|
|
139
|
-
// Async wrapper
|
|
140
54
|
withLoading,
|
|
141
55
|
};
|
|
142
56
|
};
|
|
143
57
|
|
|
144
58
|
/**
|
|
145
|
-
*
|
|
59
|
+
* Simple loading hook (no message)
|
|
146
60
|
*/
|
|
147
61
|
export const useSimpleLoading = () => {
|
|
148
62
|
const [isLoading, setIsLoading] = useState(false);
|
|
@@ -150,10 +64,8 @@ export const useSimpleLoading = () => {
|
|
|
150
64
|
const withLoading = useCallback(
|
|
151
65
|
async <T,>(asyncFn: Promise<T>): Promise<T> => {
|
|
152
66
|
setIsLoading(true);
|
|
153
|
-
|
|
154
67
|
try {
|
|
155
|
-
|
|
156
|
-
return result;
|
|
68
|
+
return await asyncFn;
|
|
157
69
|
} finally {
|
|
158
70
|
setIsLoading(false);
|
|
159
71
|
}
|
package/src/USAGE_EXAMPLES.md
DELETED
|
@@ -1,429 +0,0 @@
|
|
|
1
|
-
# Loading Domain - Usage Examples
|
|
2
|
-
|
|
3
|
-
This file provides comprehensive examples for using the loading domain across all generated apps.
|
|
4
|
-
|
|
5
|
-
## ๐ Table of Contents
|
|
6
|
-
|
|
7
|
-
1. [Basic Loading State](#basic-loading-state)
|
|
8
|
-
2. [Skeleton Loaders](#skeleton-loaders)
|
|
9
|
-
3. [Loading State Hook](#loading-state-hook)
|
|
10
|
-
4. [Full Screen Loading](#full-screen-loading)
|
|
11
|
-
5. [List Screens with Skeleton](#list-screens-with-skeleton)
|
|
12
|
-
6. [Custom Emoji per App](#custom-emoji-per-app)
|
|
13
|
-
|
|
14
|
-
---
|
|
15
|
-
|
|
16
|
-
## Basic Loading State
|
|
17
|
-
|
|
18
|
-
### Simple Loading Indicator
|
|
19
|
-
|
|
20
|
-
```typescript
|
|
21
|
-
import { LoadingState } from '@domains/loading';
|
|
22
|
-
|
|
23
|
-
const MyScreen = () => {
|
|
24
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
25
|
-
|
|
26
|
-
return (
|
|
27
|
-
<View>
|
|
28
|
-
{isLoading ? (
|
|
29
|
-
<LoadingState message="Loading data..." />
|
|
30
|
-
) : (
|
|
31
|
-
<Content />
|
|
32
|
-
)}
|
|
33
|
-
</View>
|
|
34
|
-
);
|
|
35
|
-
};
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
### Inline Loading (Small Size)
|
|
39
|
-
|
|
40
|
-
```typescript
|
|
41
|
-
<LoadingState size="small" />
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
### Section Loading (Medium Size)
|
|
45
|
-
|
|
46
|
-
```typescript
|
|
47
|
-
<LoadingState size="medium" message="Fetching settings..." />
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
---
|
|
51
|
-
|
|
52
|
-
## Skeleton Loaders
|
|
53
|
-
|
|
54
|
-
### List Skeleton
|
|
55
|
-
|
|
56
|
-
```typescript
|
|
57
|
-
import { SkeletonLoader } from '@domains/loading';
|
|
58
|
-
|
|
59
|
-
const ListScreen = () => {
|
|
60
|
-
const [data, setData] = useState([]);
|
|
61
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
62
|
-
|
|
63
|
-
return (
|
|
64
|
-
<View>
|
|
65
|
-
{isLoading ? (
|
|
66
|
-
<SkeletonLoader pattern="list" count={5} />
|
|
67
|
-
) : (
|
|
68
|
-
<FlatList data={data} ... />
|
|
69
|
-
)}
|
|
70
|
-
</View>
|
|
71
|
-
);
|
|
72
|
-
};
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
### Card Skeleton
|
|
76
|
-
|
|
77
|
-
```typescript
|
|
78
|
-
<SkeletonLoader pattern="card" count={3} />
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
### Profile Skeleton
|
|
82
|
-
|
|
83
|
-
```typescript
|
|
84
|
-
<SkeletonLoader pattern="profile" />
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
### Text Skeleton
|
|
88
|
-
|
|
89
|
-
```typescript
|
|
90
|
-
<SkeletonLoader pattern="text" count={3} />
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
### Custom Skeleton
|
|
94
|
-
|
|
95
|
-
```typescript
|
|
96
|
-
<SkeletonLoader
|
|
97
|
-
pattern="custom"
|
|
98
|
-
custom={[
|
|
99
|
-
{ width: 100, height: 100, borderRadius: 50 },
|
|
100
|
-
{ width: '80%', height: 20, borderRadius: 4, marginBottom: 8 },
|
|
101
|
-
{ width: '60%', height: 16, borderRadius: 4 },
|
|
102
|
-
]}
|
|
103
|
-
/>
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
---
|
|
107
|
-
|
|
108
|
-
## Loading State Hook
|
|
109
|
-
|
|
110
|
-
### Manual Control
|
|
111
|
-
|
|
112
|
-
```typescript
|
|
113
|
-
import { useLoading } from '@domains/loading';
|
|
114
|
-
|
|
115
|
-
const DataScreen = () => {
|
|
116
|
-
const { isLoading, loadingMessage, startLoading, stopLoading } = useLoading();
|
|
117
|
-
|
|
118
|
-
const handleSave = async () => {
|
|
119
|
-
startLoading('Saving changes...');
|
|
120
|
-
try {
|
|
121
|
-
await saveData();
|
|
122
|
-
} finally {
|
|
123
|
-
stopLoading();
|
|
124
|
-
}
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
return (
|
|
128
|
-
<View>
|
|
129
|
-
{isLoading && <LoadingState message={loadingMessage} />}
|
|
130
|
-
<Button onPress={handleSave}>Save</Button>
|
|
131
|
-
</View>
|
|
132
|
-
);
|
|
133
|
-
};
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
### Async Wrapper (Automatic)
|
|
137
|
-
|
|
138
|
-
```typescript
|
|
139
|
-
const DataScreen = () => {
|
|
140
|
-
const { isLoading, loadingMessage, withLoading } = useLoading();
|
|
141
|
-
|
|
142
|
-
const loadData = () => withLoading(
|
|
143
|
-
fetchData(),
|
|
144
|
-
'Loading data...'
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
useEffect(() => {
|
|
148
|
-
loadData();
|
|
149
|
-
}, []);
|
|
150
|
-
|
|
151
|
-
return (
|
|
152
|
-
<View>
|
|
153
|
-
{isLoading ? (
|
|
154
|
-
<LoadingState message={loadingMessage} />
|
|
155
|
-
) : (
|
|
156
|
-
<Content />
|
|
157
|
-
)}
|
|
158
|
-
</View>
|
|
159
|
-
);
|
|
160
|
-
};
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
### Simple Loading Hook
|
|
164
|
-
|
|
165
|
-
```typescript
|
|
166
|
-
import { useSimpleLoading } from '@domains/loading';
|
|
167
|
-
|
|
168
|
-
const SimpleScreen = () => {
|
|
169
|
-
const { isLoading, withLoading } = useSimpleLoading();
|
|
170
|
-
|
|
171
|
-
const handleAction = () => withLoading(performAction());
|
|
172
|
-
|
|
173
|
-
return (
|
|
174
|
-
<View>
|
|
175
|
-
{isLoading && <LoadingState />}
|
|
176
|
-
<Button onPress={handleAction}>Do Action</Button>
|
|
177
|
-
</View>
|
|
178
|
-
);
|
|
179
|
-
};
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
---
|
|
183
|
-
|
|
184
|
-
## Full Screen Loading
|
|
185
|
-
|
|
186
|
-
### With Message
|
|
187
|
-
|
|
188
|
-
```typescript
|
|
189
|
-
<LoadingState
|
|
190
|
-
fullScreen
|
|
191
|
-
message="Please wait..."
|
|
192
|
-
size="large"
|
|
193
|
-
/>
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
### Initial App Load
|
|
197
|
-
|
|
198
|
-
```typescript
|
|
199
|
-
const App = () => {
|
|
200
|
-
const [isReady, setIsReady] = useState(false);
|
|
201
|
-
|
|
202
|
-
useEffect(() => {
|
|
203
|
-
const prepare = async () => {
|
|
204
|
-
await loadAssets();
|
|
205
|
-
await loadData();
|
|
206
|
-
setIsReady(true);
|
|
207
|
-
};
|
|
208
|
-
prepare();
|
|
209
|
-
}, []);
|
|
210
|
-
|
|
211
|
-
if (!isReady) {
|
|
212
|
-
return <LoadingState fullScreen message="Initializing app..." />;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
return <MainApp />;
|
|
216
|
-
};
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
---
|
|
220
|
-
|
|
221
|
-
## List Screens with Skeleton
|
|
222
|
-
|
|
223
|
-
### Complete Example
|
|
224
|
-
|
|
225
|
-
```typescript
|
|
226
|
-
import { SkeletonLoader, useLoading } from '@domains/loading';
|
|
227
|
-
|
|
228
|
-
const WorkoutsScreen = () => {
|
|
229
|
-
const [workouts, setWorkouts] = useState([]);
|
|
230
|
-
const { isLoading, withLoading } = useLoading();
|
|
231
|
-
|
|
232
|
-
const loadWorkouts = () => withLoading(
|
|
233
|
-
fetchWorkouts(),
|
|
234
|
-
'Loading workouts...'
|
|
235
|
-
);
|
|
236
|
-
|
|
237
|
-
useEffect(() => {
|
|
238
|
-
loadWorkouts();
|
|
239
|
-
}, []);
|
|
240
|
-
|
|
241
|
-
return (
|
|
242
|
-
<ScreenLayout>
|
|
243
|
-
<ScreenHeader title="Workouts" />
|
|
244
|
-
|
|
245
|
-
{isLoading ? (
|
|
246
|
-
<SkeletonLoader pattern="list" count={8} />
|
|
247
|
-
) : workouts.length === 0 ? (
|
|
248
|
-
<EmptyState message="No workouts found" />
|
|
249
|
-
) : (
|
|
250
|
-
<FlatList
|
|
251
|
-
data={workouts}
|
|
252
|
-
renderItem={({ item }) => <WorkoutCard workout={item} />}
|
|
253
|
-
/>
|
|
254
|
-
)}
|
|
255
|
-
</ScreenLayout>
|
|
256
|
-
);
|
|
257
|
-
};
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
---
|
|
261
|
-
|
|
262
|
-
## Custom Emoji per App
|
|
263
|
-
|
|
264
|
-
### Meditation App
|
|
265
|
-
|
|
266
|
-
```typescript
|
|
267
|
-
import { LoadingState, LOADING_EMOJIS } from '@domains/loading';
|
|
268
|
-
|
|
269
|
-
<LoadingState
|
|
270
|
-
emoji={LOADING_EMOJIS.meditation}
|
|
271
|
-
message="Loading meditations..."
|
|
272
|
-
/>
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
### Fitness App
|
|
276
|
-
|
|
277
|
-
```typescript
|
|
278
|
-
<LoadingState
|
|
279
|
-
emoji={LOADING_EMOJIS.fitness}
|
|
280
|
-
message="Loading workouts..."
|
|
281
|
-
/>
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
### Productivity App
|
|
285
|
-
|
|
286
|
-
```typescript
|
|
287
|
-
<LoadingState
|
|
288
|
-
emoji={LOADING_EMOJIS.productivity}
|
|
289
|
-
message="Loading tasks..."
|
|
290
|
-
/>
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
### Available Emojis
|
|
294
|
-
|
|
295
|
-
```typescript
|
|
296
|
-
LOADING_EMOJIS = {
|
|
297
|
-
meditation: '๐ง',
|
|
298
|
-
fitness: '๐ช',
|
|
299
|
-
workout: '๐๏ธ',
|
|
300
|
-
running: '๐',
|
|
301
|
-
cycling: '๐ด',
|
|
302
|
-
yoga: '๐งโโ๏ธ',
|
|
303
|
-
health: '๐ฅ',
|
|
304
|
-
nutrition: '๐ฅ',
|
|
305
|
-
productivity: 'โณ',
|
|
306
|
-
education: '๐',
|
|
307
|
-
reading: '๐',
|
|
308
|
-
music: '๐ต',
|
|
309
|
-
art: '๐จ',
|
|
310
|
-
travel: 'โ๏ธ',
|
|
311
|
-
finance: '๐ฐ',
|
|
312
|
-
shopping: '๐๏ธ',
|
|
313
|
-
cooking: '๐จโ๐ณ',
|
|
314
|
-
gaming: '๐ฎ',
|
|
315
|
-
default: 'โ',
|
|
316
|
-
}
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
---
|
|
320
|
-
|
|
321
|
-
## Settings Screen Example
|
|
322
|
-
|
|
323
|
-
```typescript
|
|
324
|
-
import { LoadingState, useLoading } from '@domains/loading';
|
|
325
|
-
|
|
326
|
-
const SettingsScreen = () => {
|
|
327
|
-
const { theme } = useTheme();
|
|
328
|
-
const { language, setLanguage } = useLocalization();
|
|
329
|
-
const { isLoading, withLoading } = useLoading();
|
|
330
|
-
|
|
331
|
-
const handleLanguageChange = (newLanguage: string) => withLoading(
|
|
332
|
-
setLanguage(newLanguage),
|
|
333
|
-
'Changing language...'
|
|
334
|
-
);
|
|
335
|
-
|
|
336
|
-
if (isLoading) {
|
|
337
|
-
return <LoadingState fullScreen message="Applying changes..." />;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
return (
|
|
341
|
-
<ScreenLayout>
|
|
342
|
-
<ScreenHeader title="Settings" />
|
|
343
|
-
<ScrollView>
|
|
344
|
-
{/* Settings content */}
|
|
345
|
-
</ScrollView>
|
|
346
|
-
</ScreenLayout>
|
|
347
|
-
);
|
|
348
|
-
};
|
|
349
|
-
```
|
|
350
|
-
|
|
351
|
-
---
|
|
352
|
-
|
|
353
|
-
## Onboarding Screen Example
|
|
354
|
-
|
|
355
|
-
```typescript
|
|
356
|
-
import { LoadingState, useLoading } from '@domains/loading';
|
|
357
|
-
|
|
358
|
-
const OnboardingScreen = () => {
|
|
359
|
-
const { completeOnboarding } = useOnboarding();
|
|
360
|
-
const { isLoading, withLoading } = useLoading();
|
|
361
|
-
|
|
362
|
-
const handleComplete = () => withLoading(
|
|
363
|
-
completeOnboarding(),
|
|
364
|
-
'Setting up your account...'
|
|
365
|
-
);
|
|
366
|
-
|
|
367
|
-
return (
|
|
368
|
-
<ScreenLayout>
|
|
369
|
-
{isLoading ? (
|
|
370
|
-
<LoadingState fullScreen message="Almost there..." />
|
|
371
|
-
) : (
|
|
372
|
-
<OnboardingContent onComplete={handleComplete} />
|
|
373
|
-
)}
|
|
374
|
-
</ScreenLayout>
|
|
375
|
-
);
|
|
376
|
-
};
|
|
377
|
-
```
|
|
378
|
-
|
|
379
|
-
---
|
|
380
|
-
|
|
381
|
-
## Best Practices
|
|
382
|
-
|
|
383
|
-
1. **Use skeleton loaders for lists** - Better UX than full-screen loading
|
|
384
|
-
2. **Match emoji to app theme** - Use LOADING_EMOJIS for consistency
|
|
385
|
-
3. **Show meaningful messages** - "Loading workouts..." not "Please wait..."
|
|
386
|
-
4. **Use full screen sparingly** - Only for critical app-wide operations
|
|
387
|
-
5. **Prefer useLoading hook** - Centralized state management
|
|
388
|
-
6. **Use withLoading wrapper** - Cleaner async code
|
|
389
|
-
7. **Test loading states** - Ensure they work on slow connections
|
|
390
|
-
|
|
391
|
-
---
|
|
392
|
-
|
|
393
|
-
## Migration from Old Loading Patterns
|
|
394
|
-
|
|
395
|
-
### Before (ActivityIndicator)
|
|
396
|
-
|
|
397
|
-
```typescript
|
|
398
|
-
// โ OLD - Inconsistent
|
|
399
|
-
{isLoading && <ActivityIndicator />}
|
|
400
|
-
{isLoading && <Text>Loading...</Text>}
|
|
401
|
-
```
|
|
402
|
-
|
|
403
|
-
### After (LoadingState)
|
|
404
|
-
|
|
405
|
-
```typescript
|
|
406
|
-
// โ
NEW - Consistent, themed
|
|
407
|
-
{isLoading && <LoadingState message="Loading..." />}
|
|
408
|
-
```
|
|
409
|
-
|
|
410
|
-
### Before (Custom Spinner)
|
|
411
|
-
|
|
412
|
-
```typescript
|
|
413
|
-
// โ OLD - Duplicated code
|
|
414
|
-
<View style={styles.loadingContainer}>
|
|
415
|
-
<ActivityIndicator size="large" />
|
|
416
|
-
<Text style={styles.loadingText}>Please wait...</Text>
|
|
417
|
-
</View>
|
|
418
|
-
```
|
|
419
|
-
|
|
420
|
-
### After (LoadingState)
|
|
421
|
-
|
|
422
|
-
```typescript
|
|
423
|
-
// โ
NEW - One line, consistent
|
|
424
|
-
<LoadingState size="large" message="Please wait..." />
|
|
425
|
-
```
|
|
426
|
-
|
|
427
|
-
---
|
|
428
|
-
|
|
429
|
-
This loading domain ensures **consistent, beautiful, calming loading experiences** across all 100+ generated apps! ๐ง
|