@umituz/react-native-design-system 2.3.14 → 2.3.15
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 +19 -2
- package/src/index.ts +91 -0
- package/src/layouts/ScreenLayout/ScreenLayout.example.tsx +2 -2
- package/src/layouts/ScreenLayout/ScreenLayout.tsx +1 -1
- package/src/molecules/animation/core/AnimationCore.ts +29 -0
- package/src/molecules/animation/domain/entities/Animation.ts +81 -0
- package/src/molecules/animation/domain/entities/Fireworks.ts +44 -0
- package/src/molecules/animation/domain/entities/Theme.ts +76 -0
- package/src/molecules/animation/index.ts +146 -0
- package/src/molecules/animation/infrastructure/services/AnimationConfigService.ts +35 -0
- package/src/molecules/animation/infrastructure/services/SpringAnimationConfigService.ts +67 -0
- package/src/molecules/animation/infrastructure/services/TimingAnimationConfigService.ts +57 -0
- package/src/molecules/animation/infrastructure/services/__tests__/SpringAnimationConfigService.test.ts +114 -0
- package/src/molecules/animation/infrastructure/services/__tests__/TimingAnimationConfigService.test.ts +105 -0
- package/src/molecules/animation/presentation/components/Fireworks.tsx +126 -0
- package/src/molecules/animation/presentation/components/__tests__/Fireworks.test.tsx +189 -0
- package/src/molecules/animation/presentation/hooks/__tests__/useAnimation.integration.test.ts +216 -0
- package/src/molecules/animation/presentation/hooks/__tests__/useFireworks.test.ts +242 -0
- package/src/molecules/animation/presentation/hooks/__tests__/useGesture.test.ts +111 -0
- package/src/molecules/animation/presentation/hooks/__tests__/useSpringAnimation.test.ts +131 -0
- package/src/molecules/animation/presentation/hooks/__tests__/useTimingAnimation.test.ts +175 -0
- package/src/molecules/animation/presentation/hooks/__tests__/useTransformAnimation.test.ts +137 -0
- package/src/molecules/animation/presentation/hooks/useAnimation.ts +77 -0
- package/src/molecules/animation/presentation/hooks/useFireworks.ts +141 -0
- package/src/molecules/animation/presentation/hooks/useGesture.ts +61 -0
- package/src/molecules/animation/presentation/hooks/useGestureCreators.ts +163 -0
- package/src/molecules/animation/presentation/hooks/useGestureState.ts +53 -0
- package/src/molecules/animation/presentation/hooks/useIconAnimations.ts +119 -0
- package/src/molecules/animation/presentation/hooks/useModalAnimations.ts +124 -0
- package/src/molecules/animation/presentation/hooks/useReanimatedReady.ts +60 -0
- package/src/molecules/animation/presentation/hooks/useSpringAnimation.ts +69 -0
- package/src/molecules/animation/presentation/hooks/useTimingAnimation.ts +111 -0
- package/src/molecules/animation/presentation/hooks/useTransformAnimation.ts +57 -0
- package/src/molecules/animation/presentation/providers/AnimationThemeProvider.tsx +62 -0
- package/src/molecules/animation/presentation/providers/__tests__/AnimationThemeProvider.test.tsx +165 -0
- package/src/molecules/animation/types/global.d.ts +97 -0
- package/src/molecules/celebration/domain/entities/CelebrationConfig.ts +17 -0
- package/src/molecules/celebration/domain/entities/FireworksConfig.ts +32 -0
- package/src/molecules/celebration/index.ts +93 -0
- package/src/molecules/celebration/infrastructure/services/FireworksConfigService.ts +49 -0
- package/src/molecules/celebration/presentation/components/CelebrationFireworksOverlay.tsx +33 -0
- package/src/molecules/celebration/presentation/components/CelebrationModal.tsx +78 -0
- package/src/molecules/celebration/presentation/components/CelebrationModalContent.tsx +90 -0
- package/src/molecules/celebration/presentation/hooks/useCelebrationModalAnimation.ts +49 -0
- package/src/molecules/celebration/presentation/hooks/useCelebrationState.ts +45 -0
- package/src/molecules/celebration/presentation/styles/CelebrationModalStyles.ts +65 -0
- package/src/molecules/countdown/components/Countdown.tsx +128 -0
- package/src/molecules/countdown/components/CountdownHeader.tsx +84 -0
- package/src/molecules/countdown/components/TimeUnit.tsx +73 -0
- package/src/molecules/countdown/hooks/useCountdown.ts +107 -0
- package/src/molecules/countdown/index.ts +25 -0
- package/src/molecules/countdown/types/CountdownTypes.ts +31 -0
- package/src/molecules/countdown/utils/TimeCalculator.ts +46 -0
- package/src/molecules/emoji/domain/entities/Emoji.ts +129 -0
- package/src/molecules/emoji/index.ts +177 -0
- package/src/molecules/emoji/presentation/components/EmojiPicker.tsx +102 -0
- package/src/molecules/emoji/presentation/hooks/useEmojiPicker.ts +171 -0
- package/src/molecules/index.ts +21 -0
- package/src/molecules/long-press-menu/domain/entities/MenuAction.ts +37 -0
- package/src/molecules/long-press-menu/index.ts +16 -0
- package/src/molecules/navigation/StackNavigator.tsx +75 -0
- package/src/molecules/navigation/TabsNavigator.tsx +94 -0
- package/src/molecules/navigation/components/FabButton.tsx +45 -0
- package/src/molecules/navigation/components/TabLabel.tsx +47 -0
- package/src/molecules/navigation/createStackNavigator.ts +20 -0
- package/src/molecules/navigation/createTabNavigator.ts +20 -0
- package/src/molecules/navigation/hooks/useTabBarStyles.ts +54 -0
- package/src/molecules/navigation/index.ts +37 -0
- package/src/molecules/navigation/types.ts +118 -0
- package/src/molecules/navigation/utils/AppNavigation.ts +101 -0
- package/src/molecules/navigation/utils/IconRenderer.ts +50 -0
- package/src/molecules/navigation/utils/LabelProcessor.ts +70 -0
- package/src/molecules/navigation/utils/NavigationCleanup.ts +62 -0
- package/src/molecules/navigation/utils/NavigationTheme.ts +21 -0
- package/src/molecules/navigation/utils/NavigationValidator.ts +61 -0
- package/src/molecules/navigation/utils/ScreenFactory.ts +115 -0
- package/src/molecules/navigation/utils/__tests__/IconRenderer.getIconName.test.ts +109 -0
- package/src/molecules/navigation/utils/__tests__/IconRenderer.renderIcon.test.ts +116 -0
- package/src/molecules/navigation/utils/__tests__/LabelProcessor.processLabel.test.ts +116 -0
- package/src/molecules/navigation/utils/__tests__/LabelProcessor.processTitle.test.ts +59 -0
- package/src/molecules/navigation/utils/__tests__/NavigationCleanup.test.ts +271 -0
- package/src/molecules/navigation/utils/__tests__/NavigationValidator.test.ts +252 -0
- package/src/molecules/swipe-actions/domain/entities/SwipeAction.ts +194 -0
- package/src/molecules/swipe-actions/index.ts +6 -0
- package/src/molecules/swipe-actions/presentation/components/SwipeActionButton.tsx +131 -0
- package/src/theme/hooks/useResponsiveDesignTokens.ts +1 -1
- package/src/utilities/clipboard/ClipboardUtils.ts +71 -0
- package/src/utilities/clipboard/index.ts +5 -0
- package/src/utilities/index.ts +6 -0
- package/src/utilities/sharing/domain/entities/Share.ts +210 -0
- package/src/utilities/sharing/index.ts +205 -0
- package/src/utilities/sharing/infrastructure/services/SharingService.ts +165 -0
- package/src/utilities/sharing/presentation/hooks/useSharing.ts +154 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import { Animated } from "../../../animation";
|
|
4
|
+
import { useAppDesignTokens } from "../../../../theme";
|
|
5
|
+
import { AtomicButton, AtomicText, AtomicIcon } from "../../../../atoms";
|
|
6
|
+
import type { CelebrationConfig } from "../../domain/entities/CelebrationConfig";
|
|
7
|
+
import type { ThemeColors } from "../../domain/entities/FireworksConfig";
|
|
8
|
+
import type { useCelebrationModalAnimation } from "../hooks/useCelebrationModalAnimation";
|
|
9
|
+
import { createCelebrationModalStyles } from "../styles/CelebrationModalStyles";
|
|
10
|
+
|
|
11
|
+
export interface CelebrationModalContentProps {
|
|
12
|
+
config: CelebrationConfig;
|
|
13
|
+
onClose: () => void;
|
|
14
|
+
themeColors?: ThemeColors;
|
|
15
|
+
iconStyle: ReturnType<typeof useCelebrationModalAnimation>["iconStyle"];
|
|
16
|
+
modalStyle: ReturnType<typeof useCelebrationModalAnimation>["modalStyle"];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const CelebrationModalContent: React.FC<CelebrationModalContentProps> = ({
|
|
20
|
+
config,
|
|
21
|
+
onClose,
|
|
22
|
+
themeColors,
|
|
23
|
+
iconStyle,
|
|
24
|
+
modalStyle,
|
|
25
|
+
}) => {
|
|
26
|
+
const tokens = useAppDesignTokens();
|
|
27
|
+
const styles = createCelebrationModalStyles();
|
|
28
|
+
|
|
29
|
+
const successColor = themeColors?.success || tokens.colors.success;
|
|
30
|
+
const primaryColor = themeColors?.primary || tokens.colors.primary;
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<Animated.View
|
|
34
|
+
style={[
|
|
35
|
+
styles.modal,
|
|
36
|
+
modalStyle,
|
|
37
|
+
{
|
|
38
|
+
backgroundColor: tokens.colors.surface,
|
|
39
|
+
borderColor: tokens.colors.surfaceVariant,
|
|
40
|
+
shadowColor: tokens.colors.onSurface,
|
|
41
|
+
shadowOffset: { width: 0, height: 8 },
|
|
42
|
+
shadowOpacity: 0.15,
|
|
43
|
+
shadowRadius: 24,
|
|
44
|
+
elevation: 8,
|
|
45
|
+
},
|
|
46
|
+
]}
|
|
47
|
+
>
|
|
48
|
+
<Animated.View style={[styles.iconContainer, iconStyle]}>
|
|
49
|
+
<View style={[styles.iconCircle, { backgroundColor: successColor }]}>
|
|
50
|
+
<AtomicIcon name="checkmark" size="xl" color="onPrimary" />
|
|
51
|
+
</View>
|
|
52
|
+
</Animated.View>
|
|
53
|
+
|
|
54
|
+
<AtomicText type="headlineSmall" style={[styles.title, { color: tokens.colors.onSurface }]}>
|
|
55
|
+
{config.title}
|
|
56
|
+
</AtomicText>
|
|
57
|
+
|
|
58
|
+
<AtomicText type="bodyLarge" style={[styles.message, { color: tokens.colors.onSurface }]}>
|
|
59
|
+
{config.message}
|
|
60
|
+
</AtomicText>
|
|
61
|
+
|
|
62
|
+
<View style={styles.actions}>
|
|
63
|
+
{config.primaryAction && (
|
|
64
|
+
<AtomicButton
|
|
65
|
+
title={config.primaryAction.label}
|
|
66
|
+
onPress={() => {
|
|
67
|
+
config.primaryAction?.onPress();
|
|
68
|
+
onClose();
|
|
69
|
+
}}
|
|
70
|
+
variant="primary"
|
|
71
|
+
style={{ backgroundColor: primaryColor }}
|
|
72
|
+
fullWidth
|
|
73
|
+
/>
|
|
74
|
+
)}
|
|
75
|
+
|
|
76
|
+
{config.secondaryAction && (
|
|
77
|
+
<AtomicButton
|
|
78
|
+
title={config.secondaryAction.label}
|
|
79
|
+
onPress={() => {
|
|
80
|
+
config.secondaryAction?.onPress();
|
|
81
|
+
onClose();
|
|
82
|
+
}}
|
|
83
|
+
variant="secondary"
|
|
84
|
+
fullWidth
|
|
85
|
+
/>
|
|
86
|
+
)}
|
|
87
|
+
</View>
|
|
88
|
+
</Animated.View>
|
|
89
|
+
);
|
|
90
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useCelebrationModalAnimation Hook
|
|
3
|
+
* Single Responsibility: Compose all celebration modal animations
|
|
4
|
+
* Uses ../../../animation directly
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
useModalAnimations,
|
|
9
|
+
useIconAnimations,
|
|
10
|
+
type ModalAnimationConfig,
|
|
11
|
+
type IconAnimationConfig,
|
|
12
|
+
} from "../../../animation";
|
|
13
|
+
|
|
14
|
+
export interface UseCelebrationModalAnimationReturn {
|
|
15
|
+
isReady: boolean;
|
|
16
|
+
overlayStyle: ReturnType<typeof useModalAnimations>["overlayStyle"];
|
|
17
|
+
modalStyle: ReturnType<typeof useModalAnimations>["modalStyle"];
|
|
18
|
+
iconStyle: ReturnType<typeof useIconAnimations>["iconStyle"];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Hook for managing all celebration modal animations
|
|
23
|
+
*/
|
|
24
|
+
export function useCelebrationModalAnimation(
|
|
25
|
+
visible: boolean,
|
|
26
|
+
animationConfig?: {
|
|
27
|
+
modal?: ModalAnimationConfig;
|
|
28
|
+
icon?: IconAnimationConfig;
|
|
29
|
+
},
|
|
30
|
+
): UseCelebrationModalAnimationReturn {
|
|
31
|
+
const { isReady, overlayStyle, modalStyle } = useModalAnimations(
|
|
32
|
+
visible,
|
|
33
|
+
animationConfig?.modal,
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const { iconStyle } = useIconAnimations(
|
|
37
|
+
visible,
|
|
38
|
+
isReady,
|
|
39
|
+
animationConfig?.icon,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
isReady,
|
|
44
|
+
overlayStyle,
|
|
45
|
+
modalStyle,
|
|
46
|
+
iconStyle,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useCelebrationState Hook
|
|
3
|
+
* Single Responsibility: Manage celebration modal state
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useCallback } from "react";
|
|
7
|
+
import type { CelebrationConfig } from "../../domain/entities/CelebrationConfig";
|
|
8
|
+
|
|
9
|
+
export interface UseCelebrationStateReturn {
|
|
10
|
+
visible: boolean;
|
|
11
|
+
config: CelebrationConfig | null;
|
|
12
|
+
show: (config: CelebrationConfig) => void;
|
|
13
|
+
hide: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const ANIMATION_CLEANUP_DELAY = 300;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Hook for managing celebration modal state
|
|
20
|
+
*/
|
|
21
|
+
export function useCelebrationState(): UseCelebrationStateReturn {
|
|
22
|
+
const [visible, setVisible] = useState(false);
|
|
23
|
+
const [config, setConfig] = useState<CelebrationConfig | null>(null);
|
|
24
|
+
|
|
25
|
+
const show = useCallback((celebrationConfig: CelebrationConfig) => {
|
|
26
|
+
setConfig(celebrationConfig);
|
|
27
|
+
setVisible(true);
|
|
28
|
+
}, []);
|
|
29
|
+
|
|
30
|
+
const hide = useCallback(() => {
|
|
31
|
+
setVisible(false);
|
|
32
|
+
// Clear config after animation completes
|
|
33
|
+
setTimeout(() => {
|
|
34
|
+
setConfig(null);
|
|
35
|
+
}, ANIMATION_CLEANUP_DELAY);
|
|
36
|
+
}, []);
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
visible,
|
|
40
|
+
config: config || null,
|
|
41
|
+
show,
|
|
42
|
+
hide,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { StyleSheet } from "react-native";
|
|
2
|
+
import { useAppDesignTokens } from "../../../../theme";
|
|
3
|
+
|
|
4
|
+
export const createCelebrationModalStyles = () => {
|
|
5
|
+
const tokens = useAppDesignTokens();
|
|
6
|
+
|
|
7
|
+
return StyleSheet.create({
|
|
8
|
+
overlay: {
|
|
9
|
+
flex: 1,
|
|
10
|
+
justifyContent: "center",
|
|
11
|
+
alignItems: "center",
|
|
12
|
+
padding: tokens.spacing.lg || 20,
|
|
13
|
+
},
|
|
14
|
+
modal: {
|
|
15
|
+
width: "100%",
|
|
16
|
+
maxWidth: 400,
|
|
17
|
+
borderRadius: tokens.borders.radius.xl || 20,
|
|
18
|
+
padding: tokens.spacing.xl || 28,
|
|
19
|
+
borderWidth: 1,
|
|
20
|
+
alignItems: "center",
|
|
21
|
+
},
|
|
22
|
+
iconContainer: {
|
|
23
|
+
marginBottom: tokens.spacing.xl || 24,
|
|
24
|
+
},
|
|
25
|
+
iconCircle: {
|
|
26
|
+
width: 80,
|
|
27
|
+
height: 80,
|
|
28
|
+
borderRadius: 40,
|
|
29
|
+
alignItems: "center",
|
|
30
|
+
justifyContent: "center",
|
|
31
|
+
},
|
|
32
|
+
iconText: {
|
|
33
|
+
fontSize: 40,
|
|
34
|
+
color: "#FFFFFF",
|
|
35
|
+
fontWeight: "bold",
|
|
36
|
+
},
|
|
37
|
+
title: {
|
|
38
|
+
fontSize: (tokens.typography.headlineSmall as any).fontSize || 22,
|
|
39
|
+
fontWeight: "700",
|
|
40
|
+
marginBottom: tokens.spacing.xs || 8,
|
|
41
|
+
textAlign: "center",
|
|
42
|
+
},
|
|
43
|
+
message: {
|
|
44
|
+
fontSize: (tokens.typography.bodyLarge as any).fontSize || 15,
|
|
45
|
+
marginBottom: tokens.spacing.xl || 24,
|
|
46
|
+
textAlign: "center",
|
|
47
|
+
lineHeight: 22,
|
|
48
|
+
},
|
|
49
|
+
actions: {
|
|
50
|
+
width: "100%",
|
|
51
|
+
gap: tokens.spacing.md || 12,
|
|
52
|
+
},
|
|
53
|
+
button: {
|
|
54
|
+
width: "100%",
|
|
55
|
+
paddingVertical: 14,
|
|
56
|
+
paddingHorizontal: 24,
|
|
57
|
+
borderRadius: tokens.borders.radius.lg || 12,
|
|
58
|
+
alignItems: "center",
|
|
59
|
+
},
|
|
60
|
+
buttonText: {
|
|
61
|
+
fontSize: 16,
|
|
62
|
+
fontWeight: "600",
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import { View, StyleSheet } from 'react-native';
|
|
3
|
+
import { useAppDesignTokens } from '../../../theme';
|
|
4
|
+
import { useCountdown } from '../hooks/useCountdown';
|
|
5
|
+
import { CountdownHeader } from './CountdownHeader';
|
|
6
|
+
import { TimeUnit } from './TimeUnit';
|
|
7
|
+
import type { CountdownTarget, CountdownDisplayConfig } from '../types/CountdownTypes';
|
|
8
|
+
import type { IconName } from '../../../atoms/AtomicIcon';
|
|
9
|
+
|
|
10
|
+
export interface CountdownProps {
|
|
11
|
+
target: CountdownTarget;
|
|
12
|
+
alternateTargets?: CountdownTarget[];
|
|
13
|
+
displayConfig?: CountdownDisplayConfig;
|
|
14
|
+
interval?: number;
|
|
15
|
+
onExpire?: () => void;
|
|
16
|
+
onTargetChange?: (target: CountdownTarget) => void;
|
|
17
|
+
formatLabel?: (unit: 'days' | 'hours' | 'minutes' | 'seconds', value: number) => string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const Countdown: React.FC<CountdownProps> = ({
|
|
21
|
+
target,
|
|
22
|
+
alternateTargets = [],
|
|
23
|
+
displayConfig = {},
|
|
24
|
+
interval = 1000,
|
|
25
|
+
onExpire,
|
|
26
|
+
onTargetChange,
|
|
27
|
+
formatLabel,
|
|
28
|
+
}) => {
|
|
29
|
+
const tokens = useAppDesignTokens();
|
|
30
|
+
const {
|
|
31
|
+
showIcon = true,
|
|
32
|
+
showLabel = true,
|
|
33
|
+
showToggle = alternateTargets.length > 0,
|
|
34
|
+
layout = 'grid',
|
|
35
|
+
size = 'medium',
|
|
36
|
+
} = displayConfig;
|
|
37
|
+
|
|
38
|
+
const [currentTargetIndex, setCurrentTargetIndex] = React.useState(0);
|
|
39
|
+
const allTargets = useMemo(
|
|
40
|
+
() => [target, ...alternateTargets],
|
|
41
|
+
[target, alternateTargets]
|
|
42
|
+
);
|
|
43
|
+
const currentTarget = allTargets[currentTargetIndex];
|
|
44
|
+
|
|
45
|
+
const { timeRemaining } = useCountdown(currentTarget, {
|
|
46
|
+
interval,
|
|
47
|
+
onExpire,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const handleToggle = () => {
|
|
51
|
+
const nextIndex = (currentTargetIndex + 1) % allTargets.length;
|
|
52
|
+
setCurrentTargetIndex(nextIndex);
|
|
53
|
+
if (onTargetChange) {
|
|
54
|
+
onTargetChange(allTargets[nextIndex]);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const defaultFormatLabel = (unit: 'days' | 'hours' | 'minutes' | 'seconds', value: number) => {
|
|
59
|
+
const labels = {
|
|
60
|
+
days: 'DAYS',
|
|
61
|
+
hours: 'HOURS',
|
|
62
|
+
minutes: 'MINUTES',
|
|
63
|
+
seconds: 'SECONDS',
|
|
64
|
+
};
|
|
65
|
+
return labels[unit];
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const labelFormatter = formatLabel || defaultFormatLabel;
|
|
69
|
+
|
|
70
|
+
const timeUnits = useMemo(() => {
|
|
71
|
+
const units = [];
|
|
72
|
+
|
|
73
|
+
if (timeRemaining.days > 0) {
|
|
74
|
+
units.push({
|
|
75
|
+
value: timeRemaining.days,
|
|
76
|
+
label: labelFormatter('days', timeRemaining.days)
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
units.push({
|
|
80
|
+
value: timeRemaining.hours,
|
|
81
|
+
label: labelFormatter('hours', timeRemaining.hours)
|
|
82
|
+
});
|
|
83
|
+
units.push({
|
|
84
|
+
value: timeRemaining.minutes,
|
|
85
|
+
label: labelFormatter('minutes', timeRemaining.minutes)
|
|
86
|
+
});
|
|
87
|
+
units.push({
|
|
88
|
+
value: timeRemaining.seconds,
|
|
89
|
+
label: labelFormatter('seconds', timeRemaining.seconds)
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return units;
|
|
93
|
+
}, [timeRemaining, labelFormatter]);
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<View style={styles.container}>
|
|
97
|
+
{showLabel && (
|
|
98
|
+
<CountdownHeader
|
|
99
|
+
title={currentTarget.label || 'Countdown'}
|
|
100
|
+
icon={currentTarget.icon as IconName}
|
|
101
|
+
showToggle={showToggle}
|
|
102
|
+
onToggle={handleToggle}
|
|
103
|
+
/>
|
|
104
|
+
)}
|
|
105
|
+
|
|
106
|
+
<View style={[styles.grid, { gap: tokens.spacing.sm }]}>
|
|
107
|
+
{timeUnits.map((unit, index) => (
|
|
108
|
+
<TimeUnit
|
|
109
|
+
key={index}
|
|
110
|
+
value={unit.value}
|
|
111
|
+
label={unit.label}
|
|
112
|
+
size={size}
|
|
113
|
+
/>
|
|
114
|
+
))}
|
|
115
|
+
</View>
|
|
116
|
+
</View>
|
|
117
|
+
);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const styles = StyleSheet.create({
|
|
121
|
+
container: {
|
|
122
|
+
width: '100%',
|
|
123
|
+
},
|
|
124
|
+
grid: {
|
|
125
|
+
flexDirection: 'row',
|
|
126
|
+
justifyContent: 'space-between',
|
|
127
|
+
},
|
|
128
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, StyleSheet, TouchableOpacity } from 'react-native';
|
|
3
|
+
import { AtomicText, AtomicIcon } from '../../../atoms';
|
|
4
|
+
import { useAppDesignTokens } from '../../../theme';
|
|
5
|
+
import type { IconName } from '../../../atoms/AtomicIcon';
|
|
6
|
+
|
|
7
|
+
export interface CountdownHeaderProps {
|
|
8
|
+
title: string;
|
|
9
|
+
icon?: IconName;
|
|
10
|
+
iconColor?: string;
|
|
11
|
+
showToggle?: boolean;
|
|
12
|
+
onToggle?: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const CountdownHeader: React.FC<CountdownHeaderProps> = ({
|
|
16
|
+
title,
|
|
17
|
+
icon,
|
|
18
|
+
iconColor = 'primary',
|
|
19
|
+
showToggle = false,
|
|
20
|
+
onToggle,
|
|
21
|
+
}) => {
|
|
22
|
+
const tokens = useAppDesignTokens();
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<View style={[styles.container, { marginBottom: tokens.spacing.md }]}>
|
|
26
|
+
<View style={[styles.titleRow, { gap: tokens.spacing.sm }]}>
|
|
27
|
+
{icon && (
|
|
28
|
+
<AtomicIcon
|
|
29
|
+
name={icon}
|
|
30
|
+
size="sm"
|
|
31
|
+
color={iconColor as any}
|
|
32
|
+
/>
|
|
33
|
+
)}
|
|
34
|
+
<AtomicText
|
|
35
|
+
type="titleMedium"
|
|
36
|
+
color="onSurface"
|
|
37
|
+
style={styles.title}
|
|
38
|
+
>
|
|
39
|
+
{title}
|
|
40
|
+
</AtomicText>
|
|
41
|
+
</View>
|
|
42
|
+
|
|
43
|
+
{showToggle && onToggle && (
|
|
44
|
+
<TouchableOpacity
|
|
45
|
+
style={[
|
|
46
|
+
styles.toggleButton,
|
|
47
|
+
{
|
|
48
|
+
backgroundColor: tokens.colors.surfaceSecondary,
|
|
49
|
+
width: 36,
|
|
50
|
+
height: 36,
|
|
51
|
+
borderRadius: 18,
|
|
52
|
+
},
|
|
53
|
+
]}
|
|
54
|
+
onPress={onToggle}
|
|
55
|
+
>
|
|
56
|
+
<AtomicIcon
|
|
57
|
+
name="swap-horizontal"
|
|
58
|
+
size="sm"
|
|
59
|
+
color="onSurface"
|
|
60
|
+
/>
|
|
61
|
+
</TouchableOpacity>
|
|
62
|
+
)}
|
|
63
|
+
</View>
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const styles = StyleSheet.create({
|
|
68
|
+
container: {
|
|
69
|
+
flexDirection: 'row',
|
|
70
|
+
justifyContent: 'space-between',
|
|
71
|
+
alignItems: 'center',
|
|
72
|
+
},
|
|
73
|
+
titleRow: {
|
|
74
|
+
flexDirection: 'row',
|
|
75
|
+
alignItems: 'center',
|
|
76
|
+
},
|
|
77
|
+
title: {
|
|
78
|
+
fontWeight: '700',
|
|
79
|
+
},
|
|
80
|
+
toggleButton: {
|
|
81
|
+
alignItems: 'center',
|
|
82
|
+
justifyContent: 'center',
|
|
83
|
+
},
|
|
84
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, StyleSheet } from 'react-native';
|
|
3
|
+
import { AtomicText } from '../../../atoms';
|
|
4
|
+
import { useAppDesignTokens } from '../../../theme';
|
|
5
|
+
|
|
6
|
+
export interface TimeUnitProps {
|
|
7
|
+
value: number;
|
|
8
|
+
label: string;
|
|
9
|
+
size?: 'small' | 'medium' | 'large';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const TimeUnit: React.FC<TimeUnitProps> = ({
|
|
13
|
+
value,
|
|
14
|
+
label,
|
|
15
|
+
size = 'medium',
|
|
16
|
+
}) => {
|
|
17
|
+
const tokens = useAppDesignTokens();
|
|
18
|
+
|
|
19
|
+
const sizeConfig = {
|
|
20
|
+
small: { fontSize: 24, padding: tokens.spacing.sm, minHeight: 70 },
|
|
21
|
+
medium: { fontSize: 32, padding: tokens.spacing.md, minHeight: 90 },
|
|
22
|
+
large: { fontSize: 40, padding: tokens.spacing.lg, minHeight: 110 },
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const config = sizeConfig[size];
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<View
|
|
29
|
+
style={[
|
|
30
|
+
styles.container,
|
|
31
|
+
{
|
|
32
|
+
backgroundColor: tokens.colors.surfaceSecondary,
|
|
33
|
+
borderRadius: tokens.borders.radius.lg,
|
|
34
|
+
paddingVertical: config.padding,
|
|
35
|
+
minHeight: config.minHeight,
|
|
36
|
+
},
|
|
37
|
+
]}
|
|
38
|
+
>
|
|
39
|
+
<AtomicText
|
|
40
|
+
type="displaySmall"
|
|
41
|
+
color="onSurface"
|
|
42
|
+
style={[styles.value, { fontSize: config.fontSize }]}
|
|
43
|
+
>
|
|
44
|
+
{String(value).padStart(2, '0')}
|
|
45
|
+
</AtomicText>
|
|
46
|
+
<AtomicText
|
|
47
|
+
type="labelSmall"
|
|
48
|
+
color="onSurface"
|
|
49
|
+
style={styles.label}
|
|
50
|
+
>
|
|
51
|
+
{label}
|
|
52
|
+
</AtomicText>
|
|
53
|
+
</View>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const styles = StyleSheet.create({
|
|
58
|
+
container: {
|
|
59
|
+
flex: 1,
|
|
60
|
+
alignItems: 'center',
|
|
61
|
+
justifyContent: 'center',
|
|
62
|
+
},
|
|
63
|
+
value: {
|
|
64
|
+
fontWeight: '700',
|
|
65
|
+
lineHeight: 38,
|
|
66
|
+
},
|
|
67
|
+
label: {
|
|
68
|
+
fontWeight: '600',
|
|
69
|
+
marginTop: 2,
|
|
70
|
+
letterSpacing: 1,
|
|
71
|
+
textTransform: 'uppercase',
|
|
72
|
+
},
|
|
73
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
2
|
+
import type { TimeRemaining, CountdownTarget } from '../types/CountdownTypes';
|
|
3
|
+
import { calculateTimeRemaining } from '../utils/TimeCalculator';
|
|
4
|
+
|
|
5
|
+
export interface UseCountdownOptions {
|
|
6
|
+
interval?: number;
|
|
7
|
+
autoStart?: boolean;
|
|
8
|
+
onExpire?: () => void;
|
|
9
|
+
onTick?: (timeRemaining: TimeRemaining) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface UseCountdownReturn {
|
|
13
|
+
timeRemaining: TimeRemaining;
|
|
14
|
+
isActive: boolean;
|
|
15
|
+
isExpired: boolean;
|
|
16
|
+
start: () => void;
|
|
17
|
+
stop: () => void;
|
|
18
|
+
reset: () => void;
|
|
19
|
+
setTarget: (target: CountdownTarget) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function useCountdown(
|
|
23
|
+
initialTarget: CountdownTarget | null,
|
|
24
|
+
options: UseCountdownOptions = {}
|
|
25
|
+
): UseCountdownReturn {
|
|
26
|
+
const {
|
|
27
|
+
interval = 1000,
|
|
28
|
+
autoStart = true,
|
|
29
|
+
onExpire,
|
|
30
|
+
onTick,
|
|
31
|
+
} = options;
|
|
32
|
+
|
|
33
|
+
const [target, setTargetState] = useState<CountdownTarget | null>(initialTarget);
|
|
34
|
+
const [isActive, setIsActive] = useState(autoStart);
|
|
35
|
+
const [timeRemaining, setTimeRemaining] = useState<TimeRemaining>(() =>
|
|
36
|
+
target ? calculateTimeRemaining(target.date) : {
|
|
37
|
+
days: 0,
|
|
38
|
+
hours: 0,
|
|
39
|
+
minutes: 0,
|
|
40
|
+
seconds: 0,
|
|
41
|
+
totalSeconds: 0,
|
|
42
|
+
isExpired: true,
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const expiredRef = useRef(false);
|
|
47
|
+
|
|
48
|
+
const updateTime = useCallback(() => {
|
|
49
|
+
if (!target) return;
|
|
50
|
+
|
|
51
|
+
const remaining = calculateTimeRemaining(target.date);
|
|
52
|
+
setTimeRemaining(remaining);
|
|
53
|
+
|
|
54
|
+
if (onTick) {
|
|
55
|
+
onTick(remaining);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (remaining.isExpired && !expiredRef.current) {
|
|
59
|
+
expiredRef.current = true;
|
|
60
|
+
setIsActive(false);
|
|
61
|
+
if (onExpire) {
|
|
62
|
+
onExpire();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}, [target, onTick, onExpire]);
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (!isActive || !target) return;
|
|
69
|
+
|
|
70
|
+
updateTime();
|
|
71
|
+
const intervalId = setInterval(updateTime, interval);
|
|
72
|
+
|
|
73
|
+
return () => clearInterval(intervalId);
|
|
74
|
+
}, [isActive, target, interval, updateTime]);
|
|
75
|
+
|
|
76
|
+
const start = useCallback(() => {
|
|
77
|
+
setIsActive(true);
|
|
78
|
+
}, []);
|
|
79
|
+
|
|
80
|
+
const stop = useCallback(() => {
|
|
81
|
+
setIsActive(false);
|
|
82
|
+
}, []);
|
|
83
|
+
|
|
84
|
+
const reset = useCallback(() => {
|
|
85
|
+
expiredRef.current = false;
|
|
86
|
+
setIsActive(autoStart);
|
|
87
|
+
if (target) {
|
|
88
|
+
updateTime();
|
|
89
|
+
}
|
|
90
|
+
}, [autoStart, target, updateTime]);
|
|
91
|
+
|
|
92
|
+
const setTarget = useCallback((newTarget: CountdownTarget) => {
|
|
93
|
+
setTargetState(newTarget);
|
|
94
|
+
expiredRef.current = false;
|
|
95
|
+
setIsActive(autoStart);
|
|
96
|
+
}, [autoStart]);
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
timeRemaining,
|
|
100
|
+
isActive,
|
|
101
|
+
isExpired: timeRemaining.isExpired,
|
|
102
|
+
start,
|
|
103
|
+
stop,
|
|
104
|
+
reset,
|
|
105
|
+
setTarget,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export { Countdown } from './components/Countdown';
|
|
2
|
+
export type { CountdownProps } from './components/Countdown';
|
|
3
|
+
|
|
4
|
+
export { TimeUnit } from './components/TimeUnit';
|
|
5
|
+
export type { TimeUnitProps } from './components/TimeUnit';
|
|
6
|
+
|
|
7
|
+
export { CountdownHeader } from './components/CountdownHeader';
|
|
8
|
+
export type { CountdownHeaderProps } from './components/CountdownHeader';
|
|
9
|
+
|
|
10
|
+
export { useCountdown } from './hooks/useCountdown';
|
|
11
|
+
export type { UseCountdownOptions, UseCountdownReturn } from './hooks/useCountdown';
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
calculateTimeRemaining,
|
|
15
|
+
padNumber,
|
|
16
|
+
getNextDayStart,
|
|
17
|
+
getNextYearStart,
|
|
18
|
+
} from './utils/TimeCalculator';
|
|
19
|
+
|
|
20
|
+
export type {
|
|
21
|
+
TimeRemaining,
|
|
22
|
+
CountdownTarget,
|
|
23
|
+
CountdownFormatOptions,
|
|
24
|
+
CountdownDisplayConfig,
|
|
25
|
+
} from './types/CountdownTypes';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface TimeRemaining {
|
|
2
|
+
days: number;
|
|
3
|
+
hours: number;
|
|
4
|
+
minutes: number;
|
|
5
|
+
seconds: number;
|
|
6
|
+
totalSeconds: number;
|
|
7
|
+
isExpired: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface CountdownTarget {
|
|
11
|
+
date: Date | string;
|
|
12
|
+
label?: string;
|
|
13
|
+
icon?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface CountdownFormatOptions {
|
|
17
|
+
showDays?: boolean;
|
|
18
|
+
showHours?: boolean;
|
|
19
|
+
showMinutes?: boolean;
|
|
20
|
+
showSeconds?: boolean;
|
|
21
|
+
showZeros?: boolean;
|
|
22
|
+
separator?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface CountdownDisplayConfig {
|
|
26
|
+
showIcon?: boolean;
|
|
27
|
+
showLabel?: boolean;
|
|
28
|
+
showToggle?: boolean;
|
|
29
|
+
layout?: 'grid' | 'inline' | 'compact';
|
|
30
|
+
size?: 'small' | 'medium' | 'large';
|
|
31
|
+
}
|