- Add useLoadingState hook with data/loading/error state management - Add useSimpleLoading hook for basic boolean loading state - Add useMultipleLoadingStates hook for tracking multiple items - Create Skeleton component with shimmer animation for placeholders - Create specialized skeletons: SkeletonText, SkeletonAvatar, SkeletonCard, SkeletonListItem, SkeletonBeneficiaryCard, SkeletonSensorCard - Create LoadingOverlay components: modal, inline, and card variants - Create ScreenLoading wrapper with loading/error/content states - Create RefreshableScreen with built-in pull-to-refresh - Create EmptyState and LoadingButtonState utility components - Add comprehensive tests for all components and hooks (61 tests passing) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
131 lines
2.9 KiB
TypeScript
131 lines
2.9 KiB
TypeScript
import React from 'react';
|
|
import {
|
|
View,
|
|
Text,
|
|
StyleSheet,
|
|
ActivityIndicator,
|
|
Modal,
|
|
ViewStyle,
|
|
} from 'react-native';
|
|
import { AppColors, BorderRadius, FontSizes, Spacing, Shadows } from '@/constants/theme';
|
|
|
|
interface LoadingOverlayProps {
|
|
visible: boolean;
|
|
message?: string;
|
|
transparent?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Full-screen modal loading overlay
|
|
* Use for operations that block the entire UI
|
|
*/
|
|
export function LoadingOverlay({
|
|
visible,
|
|
message = 'Loading...',
|
|
transparent = true,
|
|
}: LoadingOverlayProps) {
|
|
if (!visible) return null;
|
|
|
|
return (
|
|
<Modal
|
|
visible={visible}
|
|
transparent={transparent}
|
|
animationType="fade"
|
|
statusBarTranslucent
|
|
>
|
|
<View style={styles.overlay}>
|
|
<View style={styles.container}>
|
|
<ActivityIndicator size="large" color={AppColors.primary} />
|
|
{message && <Text style={styles.message}>{message}</Text>}
|
|
</View>
|
|
</View>
|
|
</Modal>
|
|
);
|
|
}
|
|
|
|
interface InlineLoadingOverlayProps {
|
|
visible: boolean;
|
|
message?: string;
|
|
style?: ViewStyle;
|
|
}
|
|
|
|
/**
|
|
* Inline loading overlay that covers its parent container
|
|
* Use for loading states within a specific section
|
|
*/
|
|
export function InlineLoadingOverlay({
|
|
visible,
|
|
message,
|
|
style,
|
|
}: InlineLoadingOverlayProps) {
|
|
if (!visible) return null;
|
|
|
|
return (
|
|
<View style={[styles.inlineOverlay, style]}>
|
|
<ActivityIndicator size="large" color={AppColors.primary} />
|
|
{message && <Text style={styles.inlineMessage}>{message}</Text>}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
interface LoadingCardOverlayProps {
|
|
visible: boolean;
|
|
}
|
|
|
|
/**
|
|
* Card-style loading overlay
|
|
* Use for overlay on cards or smaller components
|
|
*/
|
|
export function LoadingCardOverlay({ visible }: LoadingCardOverlayProps) {
|
|
if (!visible) return null;
|
|
|
|
return (
|
|
<View style={styles.cardOverlay}>
|
|
<ActivityIndicator size="small" color={AppColors.primary} />
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
overlay: {
|
|
flex: 1,
|
|
backgroundColor: AppColors.overlay,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
container: {
|
|
backgroundColor: AppColors.surface,
|
|
borderRadius: BorderRadius.xl,
|
|
padding: Spacing.xl,
|
|
alignItems: 'center',
|
|
minWidth: 150,
|
|
...Shadows.lg,
|
|
},
|
|
message: {
|
|
marginTop: Spacing.md,
|
|
fontSize: FontSizes.base,
|
|
color: AppColors.textSecondary,
|
|
textAlign: 'center',
|
|
},
|
|
inlineOverlay: {
|
|
...StyleSheet.absoluteFillObject,
|
|
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
borderRadius: BorderRadius.lg,
|
|
},
|
|
inlineMessage: {
|
|
marginTop: Spacing.sm,
|
|
fontSize: FontSizes.sm,
|
|
color: AppColors.textSecondary,
|
|
textAlign: 'center',
|
|
},
|
|
cardOverlay: {
|
|
...StyleSheet.absoluteFillObject,
|
|
backgroundColor: 'rgba(255, 255, 255, 0.8)',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
borderRadius: BorderRadius.lg,
|
|
},
|
|
});
|