@umituz/react-native-design-system 2.3.14 → 2.3.16

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.
Files changed (93) hide show
  1. package/package.json +19 -2
  2. package/src/index.ts +105 -0
  3. package/src/layouts/ScreenLayout/ScreenLayout.example.tsx +2 -2
  4. package/src/layouts/ScreenLayout/ScreenLayout.tsx +1 -1
  5. package/src/molecules/animation/core/AnimationCore.ts +29 -0
  6. package/src/molecules/animation/domain/entities/Animation.ts +81 -0
  7. package/src/molecules/animation/domain/entities/Fireworks.ts +44 -0
  8. package/src/molecules/animation/domain/entities/Theme.ts +76 -0
  9. package/src/molecules/animation/index.ts +146 -0
  10. package/src/molecules/animation/infrastructure/services/AnimationConfigService.ts +35 -0
  11. package/src/molecules/animation/infrastructure/services/SpringAnimationConfigService.ts +67 -0
  12. package/src/molecules/animation/infrastructure/services/TimingAnimationConfigService.ts +57 -0
  13. package/src/molecules/animation/infrastructure/services/__tests__/SpringAnimationConfigService.test.ts +114 -0
  14. package/src/molecules/animation/infrastructure/services/__tests__/TimingAnimationConfigService.test.ts +105 -0
  15. package/src/molecules/animation/presentation/components/Fireworks.tsx +126 -0
  16. package/src/molecules/animation/presentation/components/__tests__/Fireworks.test.tsx +189 -0
  17. package/src/molecules/animation/presentation/hooks/__tests__/useAnimation.integration.test.ts +216 -0
  18. package/src/molecules/animation/presentation/hooks/__tests__/useFireworks.test.ts +242 -0
  19. package/src/molecules/animation/presentation/hooks/__tests__/useGesture.test.ts +111 -0
  20. package/src/molecules/animation/presentation/hooks/__tests__/useSpringAnimation.test.ts +131 -0
  21. package/src/molecules/animation/presentation/hooks/__tests__/useTimingAnimation.test.ts +175 -0
  22. package/src/molecules/animation/presentation/hooks/__tests__/useTransformAnimation.test.ts +137 -0
  23. package/src/molecules/animation/presentation/hooks/useAnimation.ts +77 -0
  24. package/src/molecules/animation/presentation/hooks/useFireworks.ts +141 -0
  25. package/src/molecules/animation/presentation/hooks/useGesture.ts +61 -0
  26. package/src/molecules/animation/presentation/hooks/useGestureCreators.ts +163 -0
  27. package/src/molecules/animation/presentation/hooks/useGestureState.ts +53 -0
  28. package/src/molecules/animation/presentation/hooks/useIconAnimations.ts +119 -0
  29. package/src/molecules/animation/presentation/hooks/useModalAnimations.ts +124 -0
  30. package/src/molecules/animation/presentation/hooks/useReanimatedReady.ts +60 -0
  31. package/src/molecules/animation/presentation/hooks/useSpringAnimation.ts +69 -0
  32. package/src/molecules/animation/presentation/hooks/useTimingAnimation.ts +111 -0
  33. package/src/molecules/animation/presentation/hooks/useTransformAnimation.ts +57 -0
  34. package/src/molecules/animation/presentation/providers/AnimationThemeProvider.tsx +62 -0
  35. package/src/molecules/animation/presentation/providers/__tests__/AnimationThemeProvider.test.tsx +165 -0
  36. package/src/molecules/animation/types/global.d.ts +97 -0
  37. package/src/molecules/celebration/domain/entities/CelebrationConfig.ts +17 -0
  38. package/src/molecules/celebration/domain/entities/FireworksConfig.ts +32 -0
  39. package/src/molecules/celebration/index.ts +93 -0
  40. package/src/molecules/celebration/infrastructure/services/FireworksConfigService.ts +49 -0
  41. package/src/molecules/celebration/presentation/components/CelebrationFireworksOverlay.tsx +33 -0
  42. package/src/molecules/celebration/presentation/components/CelebrationModal.tsx +78 -0
  43. package/src/molecules/celebration/presentation/components/CelebrationModalContent.tsx +90 -0
  44. package/src/molecules/celebration/presentation/hooks/useCelebrationModalAnimation.ts +49 -0
  45. package/src/molecules/celebration/presentation/hooks/useCelebrationState.ts +45 -0
  46. package/src/molecules/celebration/presentation/styles/CelebrationModalStyles.ts +65 -0
  47. package/src/molecules/countdown/components/Countdown.tsx +128 -0
  48. package/src/molecules/countdown/components/CountdownHeader.tsx +84 -0
  49. package/src/molecules/countdown/components/TimeUnit.tsx +73 -0
  50. package/src/molecules/countdown/hooks/useCountdown.ts +107 -0
  51. package/src/molecules/countdown/index.ts +25 -0
  52. package/src/molecules/countdown/types/CountdownTypes.ts +31 -0
  53. package/src/molecules/countdown/utils/TimeCalculator.ts +46 -0
  54. package/src/molecules/emoji/domain/entities/Emoji.ts +129 -0
  55. package/src/molecules/emoji/index.ts +177 -0
  56. package/src/molecules/emoji/presentation/components/EmojiPicker.tsx +102 -0
  57. package/src/molecules/emoji/presentation/hooks/useEmojiPicker.ts +171 -0
  58. package/src/molecules/index.ts +21 -0
  59. package/src/molecules/long-press-menu/domain/entities/MenuAction.ts +37 -0
  60. package/src/molecules/long-press-menu/index.ts +16 -0
  61. package/src/molecules/navigation/StackNavigator.tsx +75 -0
  62. package/src/molecules/navigation/TabsNavigator.tsx +94 -0
  63. package/src/molecules/navigation/components/FabButton.tsx +45 -0
  64. package/src/molecules/navigation/components/TabLabel.tsx +47 -0
  65. package/src/molecules/navigation/createStackNavigator.ts +20 -0
  66. package/src/molecules/navigation/createTabNavigator.ts +20 -0
  67. package/src/molecules/navigation/hooks/useTabBarStyles.ts +54 -0
  68. package/src/molecules/navigation/index.ts +37 -0
  69. package/src/molecules/navigation/types.ts +118 -0
  70. package/src/molecules/navigation/utils/AppNavigation.ts +101 -0
  71. package/src/molecules/navigation/utils/IconRenderer.ts +50 -0
  72. package/src/molecules/navigation/utils/LabelProcessor.ts +70 -0
  73. package/src/molecules/navigation/utils/NavigationCleanup.ts +62 -0
  74. package/src/molecules/navigation/utils/NavigationTheme.ts +21 -0
  75. package/src/molecules/navigation/utils/NavigationValidator.ts +61 -0
  76. package/src/molecules/navigation/utils/ScreenFactory.ts +115 -0
  77. package/src/molecules/navigation/utils/__tests__/IconRenderer.getIconName.test.ts +109 -0
  78. package/src/molecules/navigation/utils/__tests__/IconRenderer.renderIcon.test.ts +116 -0
  79. package/src/molecules/navigation/utils/__tests__/LabelProcessor.processLabel.test.ts +116 -0
  80. package/src/molecules/navigation/utils/__tests__/LabelProcessor.processTitle.test.ts +59 -0
  81. package/src/molecules/navigation/utils/__tests__/NavigationCleanup.test.ts +271 -0
  82. package/src/molecules/navigation/utils/__tests__/NavigationValidator.test.ts +252 -0
  83. package/src/molecules/swipe-actions/domain/entities/SwipeAction.ts +194 -0
  84. package/src/molecules/swipe-actions/index.ts +6 -0
  85. package/src/molecules/swipe-actions/presentation/components/SwipeActionButton.tsx +131 -0
  86. package/src/theme/hooks/useResponsiveDesignTokens.ts +1 -1
  87. package/src/utilities/clipboard/ClipboardUtils.ts +71 -0
  88. package/src/utilities/clipboard/index.ts +5 -0
  89. package/src/utilities/index.ts +6 -0
  90. package/src/utilities/sharing/domain/entities/Share.ts +210 -0
  91. package/src/utilities/sharing/index.ts +205 -0
  92. package/src/utilities/sharing/infrastructure/services/SharingService.ts +165 -0
  93. 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
+ }