@umituz/react-native-design-system 4.23.83 → 4.23.85
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 +1 -1
- package/src/atoms/input/hooks/useInputState.ts +2 -4
- package/src/image/presentation/components/editor/text-editor/TextTransformTab.tsx +40 -152
- package/src/image/presentation/components/editor/text-editor/components/TransformButtonRow.tsx +124 -0
- package/src/media/domain/utils/FileValidator.ts +156 -0
- package/src/media/infrastructure/services/MediaPickerService.ts +18 -57
- package/src/media/infrastructure/utils/PermissionManager.ts +92 -0
- package/src/media/presentation/hooks/useMedia.ts +5 -4
- package/src/molecules/alerts/AlertBanner.tsx +9 -25
- package/src/molecules/alerts/AlertInline.tsx +4 -23
- package/src/molecules/alerts/AlertModal.tsx +4 -11
- package/src/molecules/alerts/AlertToast.tsx +14 -13
- package/src/molecules/alerts/utils/alertUtils.ts +133 -0
- package/src/molecules/calendar/infrastructure/storage/CalendarStore.ts +65 -25
- package/src/molecules/countdown/hooks/useCountdown.ts +13 -5
- package/src/molecules/swipe-actions/domain/entities/SwipeAction.ts +15 -123
- package/src/molecules/swipe-actions/domain/utils/swipeActionHelpers.ts +109 -0
- package/src/molecules/swipe-actions/domain/utils/swipeActionValidator.ts +54 -0
- package/src/molecules/swipe-actions/presentation/components/SwipeActionButton.tsx +10 -5
- package/src/tanstack/domain/utils/MetricsCalculator.ts +103 -0
- package/src/tanstack/infrastructure/monitoring/DevMonitor.ts +35 -29
- package/src/timezone/infrastructure/utils/SimpleCache.ts +24 -2
- package/src/molecules/alerts/utils/alertToastHelpers.ts +0 -70
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permission Manager
|
|
3
|
+
*
|
|
4
|
+
* Centralized permission handling for media operations.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as ImagePicker from "expo-image-picker";
|
|
8
|
+
import { MediaLibraryPermission } from "../../domain/entities/Media";
|
|
9
|
+
import { mapPermissionStatus } from "./mediaPickerMappers";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Permission type for media operations
|
|
13
|
+
*/
|
|
14
|
+
export type PermissionType = 'camera' | 'mediaLibrary';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Permission manager for media operations
|
|
18
|
+
*/
|
|
19
|
+
export class PermissionManager {
|
|
20
|
+
/**
|
|
21
|
+
* Requests camera permission
|
|
22
|
+
*/
|
|
23
|
+
static async requestCameraPermission(): Promise<MediaLibraryPermission> {
|
|
24
|
+
try {
|
|
25
|
+
const { status } = await ImagePicker.requestCameraPermissionsAsync();
|
|
26
|
+
return mapPermissionStatus(status);
|
|
27
|
+
} catch {
|
|
28
|
+
return MediaLibraryPermission.DENIED;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Requests media library permission
|
|
34
|
+
*/
|
|
35
|
+
static async requestMediaLibraryPermission(): Promise<MediaLibraryPermission> {
|
|
36
|
+
try {
|
|
37
|
+
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
|
38
|
+
return mapPermissionStatus(status);
|
|
39
|
+
} catch {
|
|
40
|
+
return MediaLibraryPermission.DENIED;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Gets current camera permission status
|
|
46
|
+
*/
|
|
47
|
+
static async getCameraPermissionStatus(): Promise<MediaLibraryPermission> {
|
|
48
|
+
try {
|
|
49
|
+
const { status } = await ImagePicker.getCameraPermissionsAsync();
|
|
50
|
+
return mapPermissionStatus(status);
|
|
51
|
+
} catch {
|
|
52
|
+
return MediaLibraryPermission.DENIED;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Gets current media library permission status
|
|
58
|
+
*/
|
|
59
|
+
static async getMediaLibraryPermissionStatus(): Promise<MediaLibraryPermission> {
|
|
60
|
+
try {
|
|
61
|
+
const { status } = await ImagePicker.getMediaLibraryPermissionsAsync();
|
|
62
|
+
return mapPermissionStatus(status);
|
|
63
|
+
} catch {
|
|
64
|
+
return MediaLibraryPermission.DENIED;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Generic permission request based on type
|
|
70
|
+
*/
|
|
71
|
+
static async requestPermission(type: PermissionType): Promise<MediaLibraryPermission> {
|
|
72
|
+
return type === 'camera'
|
|
73
|
+
? this.requestCameraPermission()
|
|
74
|
+
: this.requestMediaLibraryPermission();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Generic permission status check based on type
|
|
79
|
+
*/
|
|
80
|
+
static async getPermissionStatus(type: PermissionType): Promise<MediaLibraryPermission> {
|
|
81
|
+
return type === 'camera'
|
|
82
|
+
? this.getCameraPermissionStatus()
|
|
83
|
+
: this.getMediaLibraryPermissionStatus();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Checks if permission is granted
|
|
88
|
+
*/
|
|
89
|
+
static isPermissionGranted(status: MediaLibraryPermission): boolean {
|
|
90
|
+
return status === MediaLibraryPermission.GRANTED || status === MediaLibraryPermission.LIMITED;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { useState, useCallback } from "react";
|
|
9
9
|
import { MediaPickerService } from "../../infrastructure/services/MediaPickerService";
|
|
10
|
+
import { PermissionManager } from "../../infrastructure/utils/PermissionManager";
|
|
10
11
|
import type {
|
|
11
12
|
MediaPickerOptions,
|
|
12
13
|
MediaPickerResult,
|
|
@@ -140,7 +141,7 @@ export const useMedia = () => {
|
|
|
140
141
|
const requestCameraPermission =
|
|
141
142
|
useCallback(async (): Promise<MediaLibraryPermission> => {
|
|
142
143
|
try {
|
|
143
|
-
return await
|
|
144
|
+
return await PermissionManager.requestCameraPermission();
|
|
144
145
|
} catch {
|
|
145
146
|
return MediaLibraryPermission.DENIED;
|
|
146
147
|
}
|
|
@@ -149,7 +150,7 @@ export const useMedia = () => {
|
|
|
149
150
|
const requestMediaLibraryPermission =
|
|
150
151
|
useCallback(async (): Promise<MediaLibraryPermission> => {
|
|
151
152
|
try {
|
|
152
|
-
return await
|
|
153
|
+
return await PermissionManager.requestMediaLibraryPermission();
|
|
153
154
|
} catch {
|
|
154
155
|
return MediaLibraryPermission.DENIED;
|
|
155
156
|
}
|
|
@@ -158,7 +159,7 @@ export const useMedia = () => {
|
|
|
158
159
|
const getCameraPermissionStatus =
|
|
159
160
|
useCallback(async (): Promise<MediaLibraryPermission> => {
|
|
160
161
|
try {
|
|
161
|
-
return await
|
|
162
|
+
return await PermissionManager.getCameraPermissionStatus();
|
|
162
163
|
} catch {
|
|
163
164
|
return MediaLibraryPermission.DENIED;
|
|
164
165
|
}
|
|
@@ -167,7 +168,7 @@ export const useMedia = () => {
|
|
|
167
168
|
const getMediaLibraryPermissionStatus =
|
|
168
169
|
useCallback(async (): Promise<MediaLibraryPermission> => {
|
|
169
170
|
try {
|
|
170
|
-
return await
|
|
171
|
+
return await PermissionManager.getMediaLibraryPermissionStatus();
|
|
171
172
|
} catch {
|
|
172
173
|
return MediaLibraryPermission.DENIED;
|
|
173
174
|
}
|
|
@@ -6,15 +6,14 @@
|
|
|
6
6
|
* Auto-dismisses after duration (default 3 seconds).
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import React, { useEffect } from 'react';
|
|
9
|
+
import React, { useEffect, useCallback } from 'react';
|
|
10
10
|
import { StyleSheet, View, Pressable } from 'react-native';
|
|
11
11
|
import { useSafeAreaInsets } from '../../safe-area';
|
|
12
12
|
import { AtomicText, AtomicIcon, useIconName } from '../../atoms';
|
|
13
13
|
import { useAppDesignTokens } from '../../theme';
|
|
14
|
-
import { Alert,
|
|
14
|
+
import { Alert, AlertPosition } from './AlertTypes';
|
|
15
15
|
import { useAlertStore } from './AlertStore';
|
|
16
|
-
|
|
17
|
-
const DEFAULT_DURATION = 3000;
|
|
16
|
+
import { getAlertBackgroundColor, getAlertTextColor, DEFAULT_ALERT_DURATION } from './utils/alertUtils';
|
|
18
17
|
|
|
19
18
|
interface AlertBannerProps {
|
|
20
19
|
alert: Alert;
|
|
@@ -26,37 +25,22 @@ export function AlertBanner({ alert }: AlertBannerProps) {
|
|
|
26
25
|
const tokens = useAppDesignTokens();
|
|
27
26
|
const closeIcon = useIconName('close');
|
|
28
27
|
|
|
29
|
-
const handleDismiss = () => {
|
|
28
|
+
const handleDismiss = useCallback(() => {
|
|
30
29
|
dismissAlert(alert.id);
|
|
31
30
|
alert.onDismiss?.();
|
|
32
|
-
};
|
|
31
|
+
}, [alert.id, dismissAlert, alert.onDismiss]);
|
|
33
32
|
|
|
34
33
|
// Auto-dismiss after duration
|
|
35
34
|
useEffect(() => {
|
|
36
|
-
const duration = alert.duration ??
|
|
35
|
+
const duration = alert.duration ?? DEFAULT_ALERT_DURATION;
|
|
37
36
|
if (duration <= 0) return;
|
|
38
37
|
|
|
39
38
|
const timer = setTimeout(handleDismiss, duration);
|
|
40
39
|
return () => clearTimeout(timer);
|
|
41
|
-
}, [alert.
|
|
42
|
-
|
|
43
|
-
const getBackgroundColor = (type: AlertType): string => {
|
|
44
|
-
switch (type) {
|
|
45
|
-
case AlertType.SUCCESS:
|
|
46
|
-
return tokens.colors.success;
|
|
47
|
-
case AlertType.ERROR:
|
|
48
|
-
return tokens.colors.error;
|
|
49
|
-
case AlertType.WARNING:
|
|
50
|
-
return tokens.colors.warning;
|
|
51
|
-
case AlertType.INFO:
|
|
52
|
-
return tokens.colors.info;
|
|
53
|
-
default:
|
|
54
|
-
return tokens.colors.backgroundSecondary;
|
|
55
|
-
}
|
|
56
|
-
};
|
|
40
|
+
}, [alert.duration, handleDismiss]);
|
|
57
41
|
|
|
58
|
-
const backgroundColor =
|
|
59
|
-
const textColor = tokens
|
|
42
|
+
const backgroundColor = getAlertBackgroundColor(alert.type, tokens);
|
|
43
|
+
const textColor = getAlertTextColor(tokens);
|
|
60
44
|
const isTop = alert.position === AlertPosition.TOP;
|
|
61
45
|
|
|
62
46
|
return (
|
|
@@ -6,7 +6,8 @@ import React from 'react';
|
|
|
6
6
|
import { StyleSheet, View } from 'react-native';
|
|
7
7
|
import { AtomicText } from '../../atoms';
|
|
8
8
|
import { useAppDesignTokens } from '../../theme';
|
|
9
|
-
import { Alert
|
|
9
|
+
import { Alert } from './AlertTypes';
|
|
10
|
+
import { getAlertBorderColor, getAlertBackgroundColorInline } from './utils/alertUtils';
|
|
10
11
|
|
|
11
12
|
interface AlertInlineProps {
|
|
12
13
|
alert: Alert;
|
|
@@ -15,32 +16,12 @@ interface AlertInlineProps {
|
|
|
15
16
|
export const AlertInline: React.FC<AlertInlineProps> = ({ alert }) => {
|
|
16
17
|
const tokens = useAppDesignTokens();
|
|
17
18
|
|
|
18
|
-
const getBorderColor = () => {
|
|
19
|
-
switch (alert.type) {
|
|
20
|
-
case AlertType.SUCCESS: return tokens.colors.success;
|
|
21
|
-
case AlertType.ERROR: return tokens.colors.error;
|
|
22
|
-
case AlertType.WARNING: return tokens.colors.warning;
|
|
23
|
-
case AlertType.INFO: return tokens.colors.info;
|
|
24
|
-
default: return tokens.colors.border;
|
|
25
|
-
}
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
const getBackgroundColor = () => {
|
|
29
|
-
switch (alert.type) {
|
|
30
|
-
case AlertType.SUCCESS: return tokens.colors.success + '15';
|
|
31
|
-
case AlertType.ERROR: return tokens.colors.error + '15';
|
|
32
|
-
case AlertType.WARNING: return tokens.colors.warning + '15';
|
|
33
|
-
case AlertType.INFO: return tokens.colors.info + '15';
|
|
34
|
-
default: return tokens.colors.backgroundSecondary;
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
|
|
38
19
|
return (
|
|
39
20
|
<View style={[
|
|
40
21
|
styles.container,
|
|
41
22
|
{
|
|
42
|
-
borderColor:
|
|
43
|
-
backgroundColor:
|
|
23
|
+
borderColor: getAlertBorderColor(alert.type, tokens),
|
|
24
|
+
backgroundColor: getAlertBackgroundColorInline(alert.type, tokens),
|
|
44
25
|
borderRadius: tokens.borders.radius.sm,
|
|
45
26
|
padding: tokens.spacing.md,
|
|
46
27
|
marginVertical: tokens.spacing.sm,
|
|
@@ -6,8 +6,9 @@ import React from 'react';
|
|
|
6
6
|
import { StyleSheet, View, Modal, Pressable } from 'react-native';
|
|
7
7
|
import { AtomicText, AtomicButton } from '../../atoms';
|
|
8
8
|
import { useAppDesignTokens } from '../../theme';
|
|
9
|
-
import { Alert
|
|
9
|
+
import { Alert } from './AlertTypes';
|
|
10
10
|
import { useAlertStore } from './AlertStore';
|
|
11
|
+
import { getAlertBackgroundColor } from './utils/alertUtils';
|
|
11
12
|
|
|
12
13
|
interface AlertModalProps {
|
|
13
14
|
alert: Alert;
|
|
@@ -22,15 +23,7 @@ export const AlertModal: React.FC<AlertModalProps> = ({ alert }) => {
|
|
|
22
23
|
alert.onDismiss?.();
|
|
23
24
|
};
|
|
24
25
|
|
|
25
|
-
const
|
|
26
|
-
switch (alert.type) {
|
|
27
|
-
case AlertType.SUCCESS: return tokens.colors.success;
|
|
28
|
-
case AlertType.ERROR: return tokens.colors.error;
|
|
29
|
-
case AlertType.WARNING: return tokens.colors.warning;
|
|
30
|
-
case AlertType.INFO: return tokens.colors.info;
|
|
31
|
-
default: return tokens.colors.primary;
|
|
32
|
-
}
|
|
33
|
-
};
|
|
26
|
+
const headerColor = getAlertBackgroundColor(alert.type, tokens);
|
|
34
27
|
|
|
35
28
|
return (
|
|
36
29
|
<Modal
|
|
@@ -53,7 +46,7 @@ export const AlertModal: React.FC<AlertModalProps> = ({ alert }) => {
|
|
|
53
46
|
borderColor: tokens.colors.border,
|
|
54
47
|
}
|
|
55
48
|
]}>
|
|
56
|
-
<View style={[styles.header, { backgroundColor:
|
|
49
|
+
<View style={[styles.header, { backgroundColor: headerColor }]}>
|
|
57
50
|
<AtomicText type="titleLarge" style={{ color: tokens.colors.textInverse }}>
|
|
58
51
|
{alert.title}
|
|
59
52
|
</AtomicText>
|
|
@@ -5,18 +5,19 @@
|
|
|
5
5
|
* Floats on top of content.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import React, { useEffect } from 'react';
|
|
8
|
+
import React, { useEffect, useCallback } from 'react';
|
|
9
9
|
import { StyleSheet, View, Pressable } from 'react-native';
|
|
10
10
|
import { AtomicText, AtomicIcon, useIconName } from '../../atoms';
|
|
11
11
|
import { useAppDesignTokens } from '../../theme';
|
|
12
12
|
import { Alert } from './AlertTypes';
|
|
13
13
|
import { useAlertStore } from './AlertStore';
|
|
14
14
|
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
getAlertBackgroundColor,
|
|
16
|
+
getAlertTextColor,
|
|
17
|
+
getActionButtonStyle,
|
|
18
|
+
getActionTextColor,
|
|
19
|
+
DEFAULT_ALERT_DURATION,
|
|
20
|
+
} from './utils/alertUtils';
|
|
20
21
|
|
|
21
22
|
interface AlertToastProps {
|
|
22
23
|
alert: Alert;
|
|
@@ -27,28 +28,28 @@ export function AlertToast({ alert }: AlertToastProps) {
|
|
|
27
28
|
const tokens = useAppDesignTokens();
|
|
28
29
|
const closeIcon = useIconName('close');
|
|
29
30
|
|
|
30
|
-
const dismiss = () => {
|
|
31
|
+
const dismiss = useCallback(() => {
|
|
31
32
|
dismissAlert(alert.id);
|
|
32
33
|
alert.onDismiss?.();
|
|
33
|
-
};
|
|
34
|
+
}, [alert.id, dismissAlert, alert.onDismiss]);
|
|
34
35
|
|
|
35
|
-
const handleDismiss = () => {
|
|
36
|
+
const handleDismiss = useCallback(() => {
|
|
36
37
|
if (alert.dismissible) {
|
|
37
38
|
dismiss();
|
|
38
39
|
}
|
|
39
|
-
};
|
|
40
|
+
}, [alert.dismissible, dismiss]);
|
|
40
41
|
|
|
41
42
|
// Auto-dismiss after duration
|
|
42
43
|
useEffect(() => {
|
|
43
|
-
const duration = alert.duration ??
|
|
44
|
+
const duration = alert.duration ?? DEFAULT_ALERT_DURATION;
|
|
44
45
|
if (duration <= 0) return;
|
|
45
46
|
|
|
46
47
|
const timer = setTimeout(dismiss, duration);
|
|
47
48
|
return () => clearTimeout(timer);
|
|
48
|
-
}, [alert.
|
|
49
|
+
}, [alert.duration, dismiss]);
|
|
49
50
|
|
|
50
51
|
const backgroundColor = getAlertBackgroundColor(alert.type, tokens);
|
|
51
|
-
const textColor = tokens
|
|
52
|
+
const textColor = getAlertTextColor(tokens);
|
|
52
53
|
|
|
53
54
|
return (
|
|
54
55
|
<View
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Alert Utilities
|
|
3
|
+
*
|
|
4
|
+
* Helper functions for alert component styling and behavior.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { DesignTokens } from '../../../theme';
|
|
8
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
9
|
+
import { AlertType } from '../AlertTypes';
|
|
10
|
+
import type { AlertAction } from '../AlertTypes';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Gets background color for alert type
|
|
14
|
+
*
|
|
15
|
+
* @param type - Alert type
|
|
16
|
+
* @param tokens - Design tokens containing color definitions
|
|
17
|
+
* @returns Color string for the alert type
|
|
18
|
+
*/
|
|
19
|
+
export function getAlertBackgroundColor(type: AlertType, tokens: DesignTokens): string {
|
|
20
|
+
const colors = {
|
|
21
|
+
[AlertType.SUCCESS]: tokens.colors.success,
|
|
22
|
+
[AlertType.ERROR]: tokens.colors.error,
|
|
23
|
+
[AlertType.WARNING]: tokens.colors.warning,
|
|
24
|
+
[AlertType.INFO]: tokens.colors.info,
|
|
25
|
+
};
|
|
26
|
+
return colors[type] || tokens.colors.backgroundSecondary;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Gets border color for alert type
|
|
31
|
+
*
|
|
32
|
+
* @param type - Alert type
|
|
33
|
+
* @param tokens - Design tokens containing color definitions
|
|
34
|
+
* @returns Border color string for the alert type
|
|
35
|
+
*/
|
|
36
|
+
export function getAlertBorderColor(type: AlertType, tokens: DesignTokens): string {
|
|
37
|
+
const colors = {
|
|
38
|
+
[AlertType.SUCCESS]: tokens.colors.success,
|
|
39
|
+
[AlertType.ERROR]: tokens.colors.error,
|
|
40
|
+
[AlertType.WARNING]: tokens.colors.warning,
|
|
41
|
+
[AlertType.INFO]: tokens.colors.info,
|
|
42
|
+
};
|
|
43
|
+
return colors[type] || tokens.colors.border;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Gets background color with opacity for inline alerts
|
|
48
|
+
*
|
|
49
|
+
* @param type - Alert type
|
|
50
|
+
* @param tokens - Design tokens containing color definitions
|
|
51
|
+
* @param opacity - Opacity value (0-255 hex), default '15' for ~8%
|
|
52
|
+
* @returns Background color string with opacity
|
|
53
|
+
*/
|
|
54
|
+
export function getAlertBackgroundColorInline(type: AlertType, tokens: DesignTokens, opacity: string = '15'): string {
|
|
55
|
+
const baseColor = getAlertBackgroundColor(type, tokens);
|
|
56
|
+
return baseColor + opacity;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Gets text color for alert type
|
|
61
|
+
*
|
|
62
|
+
* @param tokens - Design tokens containing color definitions
|
|
63
|
+
* @returns Text color string (typically inverse for alerts)
|
|
64
|
+
*/
|
|
65
|
+
export function getAlertTextColor(tokens: DesignTokens): string {
|
|
66
|
+
return tokens.colors.textInverse;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Gets default icon for alert type
|
|
71
|
+
*
|
|
72
|
+
* @param type - Alert type
|
|
73
|
+
* @returns Icon name or undefined
|
|
74
|
+
*/
|
|
75
|
+
export function getAlertIcon(type: AlertType): string | undefined {
|
|
76
|
+
switch (type) {
|
|
77
|
+
case AlertType.SUCCESS:
|
|
78
|
+
return 'CheckCircle';
|
|
79
|
+
case AlertType.ERROR:
|
|
80
|
+
return 'AlertCircle';
|
|
81
|
+
case AlertType.WARNING:
|
|
82
|
+
return 'AlertTriangle';
|
|
83
|
+
case AlertType.INFO:
|
|
84
|
+
return 'Info';
|
|
85
|
+
default:
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Gets action button style
|
|
92
|
+
*
|
|
93
|
+
* @param actionStyle - Button style type
|
|
94
|
+
* @param tokens - Design tokens
|
|
95
|
+
* @returns Style object
|
|
96
|
+
*/
|
|
97
|
+
export function getActionButtonStyle(
|
|
98
|
+
actionStyle: AlertAction['style'],
|
|
99
|
+
tokens: DesignTokens
|
|
100
|
+
): StyleProp<ViewStyle> {
|
|
101
|
+
if (actionStyle === 'secondary') {
|
|
102
|
+
return {
|
|
103
|
+
backgroundColor: undefined,
|
|
104
|
+
borderWidth: 1,
|
|
105
|
+
borderColor: tokens.colors.textInverse,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const colors = {
|
|
110
|
+
primary: tokens.colors.backgroundPrimary,
|
|
111
|
+
destructive: tokens.colors.error,
|
|
112
|
+
};
|
|
113
|
+
return { backgroundColor: colors[actionStyle as keyof typeof colors] || tokens.colors.backgroundSecondary };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Gets action text color
|
|
118
|
+
*
|
|
119
|
+
* @param actionStyle - Button style type
|
|
120
|
+
* @param tokens - Design tokens
|
|
121
|
+
* @returns Text color string
|
|
122
|
+
*/
|
|
123
|
+
export function getActionTextColor(
|
|
124
|
+
actionStyle: AlertAction['style'],
|
|
125
|
+
tokens: DesignTokens
|
|
126
|
+
): string {
|
|
127
|
+
return actionStyle === 'primary' ? tokens.colors.textPrimary : tokens.colors.textInverse;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Default alert duration in milliseconds
|
|
132
|
+
*/
|
|
133
|
+
export const DEFAULT_ALERT_DURATION = 3000;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Convenience hook that combines all calendar stores
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { useMemo } from 'react';
|
|
6
|
+
import { useMemo, useCallback } from 'react';
|
|
7
7
|
import { useCalendarEvents } from '../stores/useCalendarEvents';
|
|
8
8
|
import { useCalendarNavigation } from '../stores/useCalendarNavigation';
|
|
9
9
|
import { useCalendarView } from '../stores/useCalendarView';
|
|
@@ -26,22 +26,22 @@ export const useCalendar = () => {
|
|
|
26
26
|
const navigation = useCalendarNavigation();
|
|
27
27
|
const view = useCalendarView();
|
|
28
28
|
|
|
29
|
-
// Utility functions
|
|
30
|
-
const getEventsForDate = (date: Date) => {
|
|
29
|
+
// Utility functions - memoized to prevent recreating on every render
|
|
30
|
+
const getEventsForDate = useCallback((date: Date) => {
|
|
31
31
|
return events.events.filter(event => {
|
|
32
32
|
const eventDate = new Date(event.date);
|
|
33
33
|
return eventDate.toDateString() === date.toDateString();
|
|
34
34
|
});
|
|
35
|
-
};
|
|
35
|
+
}, [events.events]);
|
|
36
36
|
|
|
37
|
-
const getEventsForMonth = (year: number, month: number) => {
|
|
37
|
+
const getEventsForMonth = useCallback((year: number, month: number) => {
|
|
38
38
|
return events.events.filter(event => {
|
|
39
39
|
const eventDate = new Date(event.date);
|
|
40
40
|
return eventDate.getFullYear() === year && eventDate.getMonth() === month;
|
|
41
41
|
});
|
|
42
|
-
};
|
|
42
|
+
}, [events.events]);
|
|
43
43
|
|
|
44
|
-
return {
|
|
44
|
+
return useMemo(() => ({
|
|
45
45
|
// Events state and actions
|
|
46
46
|
events: events.events,
|
|
47
47
|
isLoading: events.isLoading,
|
|
@@ -70,7 +70,29 @@ export const useCalendar = () => {
|
|
|
70
70
|
// Utility functions
|
|
71
71
|
getEventsForDate,
|
|
72
72
|
getEventsForMonth,
|
|
73
|
-
}
|
|
73
|
+
}), [
|
|
74
|
+
events.events,
|
|
75
|
+
events.isLoading,
|
|
76
|
+
events.error,
|
|
77
|
+
events.loadEvents,
|
|
78
|
+
events.addEvent,
|
|
79
|
+
events.updateEvent,
|
|
80
|
+
events.deleteEvent,
|
|
81
|
+
events.completeEvent,
|
|
82
|
+
events.uncompleteEvent,
|
|
83
|
+
events.clearError,
|
|
84
|
+
events.clearAllEvents,
|
|
85
|
+
navigation.selectedDate,
|
|
86
|
+
navigation.currentMonth,
|
|
87
|
+
navigation.setSelectedDate,
|
|
88
|
+
navigation.goToToday,
|
|
89
|
+
navigation.navigateMonth,
|
|
90
|
+
navigation.setCurrentMonth,
|
|
91
|
+
view.viewMode,
|
|
92
|
+
view.setViewMode,
|
|
93
|
+
getEventsForDate,
|
|
94
|
+
getEventsForMonth,
|
|
95
|
+
]);
|
|
74
96
|
};
|
|
75
97
|
|
|
76
98
|
/**
|
|
@@ -85,6 +107,40 @@ export const useCalendarStore = () => {
|
|
|
85
107
|
return CalendarService.getMonthDays(year, month, calendar.events);
|
|
86
108
|
}, [calendar.currentMonth, calendar.events]);
|
|
87
109
|
|
|
110
|
+
const actions = useMemo(() => ({
|
|
111
|
+
loadEvents: calendar.loadEvents,
|
|
112
|
+
addEvent: calendar.addEvent,
|
|
113
|
+
updateEvent: calendar.updateEvent,
|
|
114
|
+
deleteEvent: calendar.deleteEvent,
|
|
115
|
+
completeEvent: calendar.completeEvent,
|
|
116
|
+
uncompleteEvent: calendar.uncompleteEvent,
|
|
117
|
+
setSelectedDate: calendar.setSelectedDate,
|
|
118
|
+
goToToday: calendar.goToToday,
|
|
119
|
+
navigateMonth: calendar.navigateMonth,
|
|
120
|
+
setCurrentMonth: calendar.setCurrentMonth,
|
|
121
|
+
setViewMode: calendar.setViewMode,
|
|
122
|
+
getEventsForDate: calendar.getEventsForDate,
|
|
123
|
+
getEventsForMonth: calendar.getEventsForMonth,
|
|
124
|
+
clearError: calendar.clearError,
|
|
125
|
+
clearAllEvents: calendar.clearAllEvents,
|
|
126
|
+
}), [
|
|
127
|
+
calendar.loadEvents,
|
|
128
|
+
calendar.addEvent,
|
|
129
|
+
calendar.updateEvent,
|
|
130
|
+
calendar.deleteEvent,
|
|
131
|
+
calendar.completeEvent,
|
|
132
|
+
calendar.uncompleteEvent,
|
|
133
|
+
calendar.setSelectedDate,
|
|
134
|
+
calendar.goToToday,
|
|
135
|
+
calendar.navigateMonth,
|
|
136
|
+
calendar.setCurrentMonth,
|
|
137
|
+
calendar.setViewMode,
|
|
138
|
+
calendar.getEventsForDate,
|
|
139
|
+
calendar.getEventsForMonth,
|
|
140
|
+
calendar.clearError,
|
|
141
|
+
calendar.clearAllEvents,
|
|
142
|
+
]);
|
|
143
|
+
|
|
88
144
|
return {
|
|
89
145
|
events: calendar.events,
|
|
90
146
|
selectedDate: calendar.selectedDate,
|
|
@@ -92,23 +148,7 @@ export const useCalendarStore = () => {
|
|
|
92
148
|
viewMode: calendar.viewMode,
|
|
93
149
|
isLoading: calendar.isLoading,
|
|
94
150
|
error: calendar.error,
|
|
95
|
-
actions
|
|
96
|
-
loadEvents: calendar.loadEvents,
|
|
97
|
-
addEvent: calendar.addEvent,
|
|
98
|
-
updateEvent: calendar.updateEvent,
|
|
99
|
-
deleteEvent: calendar.deleteEvent,
|
|
100
|
-
completeEvent: calendar.completeEvent,
|
|
101
|
-
uncompleteEvent: calendar.uncompleteEvent,
|
|
102
|
-
setSelectedDate: calendar.setSelectedDate,
|
|
103
|
-
goToToday: calendar.goToToday,
|
|
104
|
-
navigateMonth: calendar.navigateMonth,
|
|
105
|
-
setCurrentMonth: calendar.setCurrentMonth,
|
|
106
|
-
setViewMode: calendar.setViewMode,
|
|
107
|
-
getEventsForDate: calendar.getEventsForDate,
|
|
108
|
-
getEventsForMonth: calendar.getEventsForMonth,
|
|
109
|
-
clearError: calendar.clearError,
|
|
110
|
-
clearAllEvents: calendar.clearAllEvents,
|
|
111
|
-
},
|
|
151
|
+
actions,
|
|
112
152
|
days,
|
|
113
153
|
};
|
|
114
154
|
};
|
|
@@ -44,6 +44,14 @@ export function useCountdown(
|
|
|
44
44
|
);
|
|
45
45
|
|
|
46
46
|
const expiredRef = useRef(false);
|
|
47
|
+
const onExpireRef = useRef(onExpire);
|
|
48
|
+
const onTickRef = useRef(onTick);
|
|
49
|
+
|
|
50
|
+
// Keep refs in sync with latest callbacks
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
onExpireRef.current = onExpire;
|
|
53
|
+
onTickRef.current = onTick;
|
|
54
|
+
}, [onExpire, onTick]);
|
|
47
55
|
|
|
48
56
|
const updateTime = useCallback(() => {
|
|
49
57
|
if (!target) return;
|
|
@@ -51,18 +59,18 @@ export function useCountdown(
|
|
|
51
59
|
const remaining = calculateTimeRemaining(target.date);
|
|
52
60
|
setTimeRemaining(remaining);
|
|
53
61
|
|
|
54
|
-
if (
|
|
55
|
-
|
|
62
|
+
if (onTickRef.current) {
|
|
63
|
+
onTickRef.current(remaining);
|
|
56
64
|
}
|
|
57
65
|
|
|
58
66
|
if (remaining.isExpired && !expiredRef.current) {
|
|
59
67
|
expiredRef.current = true;
|
|
60
68
|
setIsActive(false);
|
|
61
|
-
if (
|
|
62
|
-
|
|
69
|
+
if (onExpireRef.current) {
|
|
70
|
+
onExpireRef.current();
|
|
63
71
|
}
|
|
64
72
|
}
|
|
65
|
-
}, [target
|
|
73
|
+
}, [target]);
|
|
66
74
|
|
|
67
75
|
useEffect(() => {
|
|
68
76
|
if (!isActive || !target) return;
|