@umituz/react-native-design-system 4.23.60 → 4.23.62
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/EmptyState.tsx +39 -34
- package/src/atoms/input/hooks/useInputState.ts +6 -1
- package/src/device/infrastructure/services/DeviceCapabilityService.ts +16 -1
- package/src/exports/exception.ts +0 -2
- package/src/exports/tanstack.ts +0 -5
- package/src/exports/timezone.ts +0 -2
- package/src/index.ts +0 -7
- package/src/molecules/action-footer/ActionFooter.tsx +48 -44
- package/src/molecules/hero-section/HeroSection.tsx +54 -54
- package/src/organisms/FormContainer.tsx +31 -27
- package/src/storage/cache/presentation/useCachedValue.ts +26 -5
- package/src/storage/presentation/hooks/usePersistentCache.ts +2 -2
- package/src/storage/presentation/hooks/useStore.ts +5 -4
- package/src/tanstack/presentation/hooks/usePrefetch.ts +4 -4
- package/src/timezone/presentation/hooks/useTimezone.ts +54 -98
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-design-system",
|
|
3
|
-
"version": "4.23.
|
|
3
|
+
"version": "4.23.62",
|
|
4
4
|
"description": "Universal design system for React Native apps - Consolidated package with atoms, molecules, organisms, theme, typography, responsive, safe area, exception, infinite scroll, UUID, image, timezone, offline, onboarding, and loading utilities",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
package/src/atoms/EmptyState.tsx
CHANGED
|
@@ -11,7 +11,7 @@ import React from 'react';
|
|
|
11
11
|
import { View, StyleSheet, TouchableOpacity, ViewStyle } from 'react-native';
|
|
12
12
|
import { AtomicIcon } from './icon';
|
|
13
13
|
import { AtomicText } from './AtomicText';
|
|
14
|
-
import { useAppDesignTokens
|
|
14
|
+
import { useAppDesignTokens } from '../theme';
|
|
15
15
|
|
|
16
16
|
export interface EmptyStateProps {
|
|
17
17
|
icon?: string;
|
|
@@ -39,14 +39,47 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
|
|
|
39
39
|
const tokens = useAppDesignTokens();
|
|
40
40
|
const displayDescription = description || subtitle;
|
|
41
41
|
|
|
42
|
+
const themedStyles = React.useMemo(
|
|
43
|
+
() =>
|
|
44
|
+
StyleSheet.create({
|
|
45
|
+
container: {
|
|
46
|
+
flex: 1,
|
|
47
|
+
alignItems: 'flex-start',
|
|
48
|
+
justifyContent: 'flex-start',
|
|
49
|
+
padding: tokens.spacing.xl,
|
|
50
|
+
},
|
|
51
|
+
iconContainer: {
|
|
52
|
+
width: 120,
|
|
53
|
+
height: 120,
|
|
54
|
+
borderRadius: 60,
|
|
55
|
+
alignItems: 'flex-start',
|
|
56
|
+
justifyContent: 'flex-start',
|
|
57
|
+
marginBottom: tokens.spacing.lg,
|
|
58
|
+
},
|
|
59
|
+
title: {
|
|
60
|
+
marginBottom: tokens.spacing.sm,
|
|
61
|
+
},
|
|
62
|
+
description: {
|
|
63
|
+
marginBottom: tokens.spacing.lg,
|
|
64
|
+
},
|
|
65
|
+
actionButton: {
|
|
66
|
+
paddingHorizontal: tokens.spacing.lg,
|
|
67
|
+
paddingVertical: tokens.spacing.md,
|
|
68
|
+
borderRadius: tokens.borders.radius.md,
|
|
69
|
+
marginTop: tokens.spacing.sm,
|
|
70
|
+
},
|
|
71
|
+
}),
|
|
72
|
+
[tokens],
|
|
73
|
+
);
|
|
74
|
+
|
|
42
75
|
return (
|
|
43
|
-
<View style={[
|
|
76
|
+
<View style={[themedStyles.container, style]} testID={testID}>
|
|
44
77
|
{illustration ? (
|
|
45
78
|
illustration
|
|
46
79
|
) : (
|
|
47
80
|
<View
|
|
48
81
|
style={[
|
|
49
|
-
|
|
82
|
+
themedStyles.iconContainer,
|
|
50
83
|
{ backgroundColor: tokens.colors.surface },
|
|
51
84
|
]}
|
|
52
85
|
>
|
|
@@ -57,7 +90,7 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
|
|
|
57
90
|
<AtomicText
|
|
58
91
|
type="headlineSmall"
|
|
59
92
|
color="primary"
|
|
60
|
-
style={[
|
|
93
|
+
style={[themedStyles.title, { textAlign: 'left' }]}
|
|
61
94
|
>
|
|
62
95
|
{title}
|
|
63
96
|
</AtomicText>
|
|
@@ -66,7 +99,7 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
|
|
|
66
99
|
<AtomicText
|
|
67
100
|
type="bodyMedium"
|
|
68
101
|
color="secondary"
|
|
69
|
-
style={[
|
|
102
|
+
style={[themedStyles.description, { textAlign: 'left' }]}
|
|
70
103
|
>
|
|
71
104
|
{displayDescription}
|
|
72
105
|
</AtomicText>
|
|
@@ -75,7 +108,7 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
|
|
|
75
108
|
{actionLabel && onAction && (
|
|
76
109
|
<TouchableOpacity
|
|
77
110
|
style={[
|
|
78
|
-
|
|
111
|
+
themedStyles.actionButton,
|
|
79
112
|
{ backgroundColor: tokens.colors.primary },
|
|
80
113
|
]}
|
|
81
114
|
onPress={onAction}
|
|
@@ -90,31 +123,3 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
|
|
|
90
123
|
);
|
|
91
124
|
};
|
|
92
125
|
|
|
93
|
-
const styles = StyleSheet.create({
|
|
94
|
-
container: {
|
|
95
|
-
flex: 1,
|
|
96
|
-
alignItems: 'flex-start',
|
|
97
|
-
justifyContent: 'flex-start',
|
|
98
|
-
padding: BASE_TOKENS.spacing.xl,
|
|
99
|
-
},
|
|
100
|
-
iconContainer: {
|
|
101
|
-
width: 120,
|
|
102
|
-
height: 120,
|
|
103
|
-
borderRadius: 60,
|
|
104
|
-
alignItems: 'flex-start',
|
|
105
|
-
justifyContent: 'flex-start',
|
|
106
|
-
marginBottom: BASE_TOKENS.spacing.lg,
|
|
107
|
-
},
|
|
108
|
-
title: {
|
|
109
|
-
marginBottom: BASE_TOKENS.spacing.sm,
|
|
110
|
-
},
|
|
111
|
-
description: {
|
|
112
|
-
marginBottom: BASE_TOKENS.spacing.lg,
|
|
113
|
-
},
|
|
114
|
-
actionButton: {
|
|
115
|
-
paddingHorizontal: BASE_TOKENS.spacing.lg,
|
|
116
|
-
paddingVertical: BASE_TOKENS.spacing.md,
|
|
117
|
-
borderRadius: BASE_TOKENS.borders.radius.md,
|
|
118
|
-
marginTop: BASE_TOKENS.spacing.sm,
|
|
119
|
-
},
|
|
120
|
-
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useCallback } from 'react';
|
|
1
|
+
import { useState, useCallback, useEffect } from 'react';
|
|
2
2
|
|
|
3
3
|
interface UseInputStateProps {
|
|
4
4
|
value?: string;
|
|
@@ -34,6 +34,11 @@ export const useInputState = ({
|
|
|
34
34
|
const [isFocused, setIsFocused] = useState(false);
|
|
35
35
|
const [isPasswordVisible, setIsPasswordVisible] = useState(!secureTextEntry);
|
|
36
36
|
|
|
37
|
+
// Sync localValue when controlled value prop changes externally
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
setLocalValue(value);
|
|
40
|
+
}, [value]);
|
|
41
|
+
|
|
37
42
|
const handleTextChange = useCallback((text: string) => {
|
|
38
43
|
setLocalValue(text);
|
|
39
44
|
onChangeText?.(text);
|
|
@@ -27,13 +27,28 @@ export class DeviceCapabilityService {
|
|
|
27
27
|
return {
|
|
28
28
|
isDevice: info.isDevice,
|
|
29
29
|
isTablet: info.deviceType === DeviceType.TABLET,
|
|
30
|
-
hasNotch:
|
|
30
|
+
hasNotch: this.hasNotchFromInfo(info),
|
|
31
31
|
totalMemoryGB: info.totalMemory
|
|
32
32
|
? info.totalMemory / (1024 * 1024 * 1024)
|
|
33
33
|
: null,
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Check if device has notch/dynamic island from existing info
|
|
39
|
+
*/
|
|
40
|
+
private static hasNotchFromInfo(info: { modelName?: string | null }): boolean {
|
|
41
|
+
if (Platform.OS !== 'ios') {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
const modelName = info.modelName?.toLowerCase() ?? '';
|
|
45
|
+
return (
|
|
46
|
+
modelName.includes('iphone x') ||
|
|
47
|
+
modelName.includes('iphone 1') ||
|
|
48
|
+
modelName.includes('pro')
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
37
52
|
/**
|
|
38
53
|
* Check if device has notch/dynamic island
|
|
39
54
|
*/
|
package/src/exports/exception.ts
CHANGED
package/src/exports/tanstack.ts
CHANGED
package/src/exports/timezone.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -61,7 +61,6 @@ export * from "./exports/safe-area";
|
|
|
61
61
|
// EXCEPTION EXPORTS
|
|
62
62
|
// =============================================================================
|
|
63
63
|
export * from "./exports/exception";
|
|
64
|
-
export { ErrorBoundary } from "./exception/presentation/components/ErrorBoundary";
|
|
65
64
|
|
|
66
65
|
// =============================================================================
|
|
67
66
|
// INFINITE SCROLL EXPORTS
|
|
@@ -82,7 +81,6 @@ export * from "./exports/timezone";
|
|
|
82
81
|
// OFFLINE EXPORTS
|
|
83
82
|
// =============================================================================
|
|
84
83
|
export * from "./exports/offline";
|
|
85
|
-
export { NetworkProvider, useOffline, OfflineBanner } from "./offline";
|
|
86
84
|
|
|
87
85
|
// =============================================================================
|
|
88
86
|
// IMAGE EXPORTS
|
|
@@ -109,10 +107,6 @@ export * from "./exports/utilities";
|
|
|
109
107
|
// =============================================================================
|
|
110
108
|
export * from "./exports/storage";
|
|
111
109
|
|
|
112
|
-
// =============================================================================
|
|
113
|
-
// STORAGE STATE EXPORTS
|
|
114
|
-
// =============================================================================
|
|
115
|
-
export * from "./storage/presentation/hooks/useStorageState";
|
|
116
110
|
|
|
117
111
|
// =============================================================================
|
|
118
112
|
// ONBOARDING EXPORTS
|
|
@@ -132,7 +126,6 @@ export * from "./exports/media";
|
|
|
132
126
|
// TANSTACK EXPORTS
|
|
133
127
|
// =============================================================================
|
|
134
128
|
export * from "./exports/tanstack";
|
|
135
|
-
export { TanstackProvider } from "./tanstack";
|
|
136
129
|
|
|
137
130
|
// =============================================================================
|
|
138
131
|
// LOADING EXPORTS
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
import React from 'react';
|
|
2
|
+
import React, { useMemo } from 'react';
|
|
3
3
|
import { View, StyleSheet, TouchableOpacity } from 'react-native';
|
|
4
4
|
import { AtomicText } from '../../atoms/AtomicText';
|
|
5
5
|
import { AtomicIcon } from '../../atoms';
|
|
@@ -17,49 +17,53 @@ export const ActionFooter: React.FC<ActionFooterProps> = ({
|
|
|
17
17
|
}) => {
|
|
18
18
|
const tokens = useAppDesignTokens();
|
|
19
19
|
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
20
|
+
const themedStyles = useMemo(
|
|
21
|
+
() =>
|
|
22
|
+
StyleSheet.create({
|
|
23
|
+
container: {
|
|
24
|
+
flexDirection: 'row',
|
|
25
|
+
alignItems: 'center',
|
|
26
|
+
paddingVertical: tokens.spacing.md,
|
|
27
|
+
gap: tokens.spacing.md,
|
|
28
|
+
},
|
|
29
|
+
backButton: {
|
|
30
|
+
width: 56,
|
|
31
|
+
height: 56,
|
|
32
|
+
borderRadius: tokens.borders.radius.lg,
|
|
33
|
+
backgroundColor: tokens.colors.surface,
|
|
34
|
+
justifyContent: 'center',
|
|
35
|
+
alignItems: 'center',
|
|
36
|
+
borderWidth: 1,
|
|
37
|
+
borderColor: tokens.colors.outlineVariant,
|
|
38
|
+
},
|
|
39
|
+
actionButton: {
|
|
40
|
+
flex: 1,
|
|
41
|
+
height: 56,
|
|
42
|
+
borderRadius: tokens.borders.radius.lg,
|
|
43
|
+
overflow: 'hidden',
|
|
44
|
+
},
|
|
45
|
+
actionContent: {
|
|
46
|
+
flex: 1,
|
|
47
|
+
flexDirection: 'row',
|
|
48
|
+
alignItems: 'center',
|
|
49
|
+
justifyContent: 'flex-start',
|
|
50
|
+
backgroundColor: tokens.colors.primary,
|
|
51
|
+
gap: tokens.spacing.sm,
|
|
52
|
+
paddingHorizontal: tokens.spacing.lg,
|
|
53
|
+
},
|
|
54
|
+
actionText: {
|
|
55
|
+
color: tokens.colors.onPrimary,
|
|
56
|
+
fontWeight: '800',
|
|
57
|
+
fontSize: 18,
|
|
58
|
+
},
|
|
59
|
+
}),
|
|
60
|
+
[tokens],
|
|
61
|
+
);
|
|
58
62
|
|
|
59
63
|
return (
|
|
60
|
-
<View style={[
|
|
64
|
+
<View style={[themedStyles.container, style]}>
|
|
61
65
|
<TouchableOpacity
|
|
62
|
-
style={
|
|
66
|
+
style={themedStyles.backButton}
|
|
63
67
|
onPress={onBack}
|
|
64
68
|
activeOpacity={0.7}
|
|
65
69
|
testID="action-footer-back"
|
|
@@ -72,14 +76,14 @@ export const ActionFooter: React.FC<ActionFooterProps> = ({
|
|
|
72
76
|
</TouchableOpacity>
|
|
73
77
|
|
|
74
78
|
<TouchableOpacity
|
|
75
|
-
style={
|
|
79
|
+
style={themedStyles.actionButton}
|
|
76
80
|
onPress={onAction}
|
|
77
81
|
activeOpacity={0.9}
|
|
78
82
|
disabled={loading}
|
|
79
83
|
testID="action-footer-action"
|
|
80
84
|
>
|
|
81
|
-
<View style={
|
|
82
|
-
<AtomicText style={
|
|
85
|
+
<View style={themedStyles.actionContent}>
|
|
86
|
+
<AtomicText style={themedStyles.actionText}>{actionLabel}</AtomicText>
|
|
83
87
|
<AtomicIcon
|
|
84
88
|
name={actionIcon}
|
|
85
89
|
size="sm"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
import React from 'react';
|
|
2
|
+
import React, { useMemo } from 'react';
|
|
3
3
|
import { View, StyleSheet, Text, Image } from 'react-native';
|
|
4
4
|
import { useAppDesignTokens } from '../../theme';
|
|
5
5
|
import type { HeroSectionProps } from './types';
|
|
@@ -12,62 +12,41 @@ export const HeroSection: React.FC<HeroSectionProps> = ({
|
|
|
12
12
|
style,
|
|
13
13
|
}) => {
|
|
14
14
|
const tokens = useAppDesignTokens();
|
|
15
|
-
// Default height 50% of screen if not provided? No, components shouldn't guess screen height directly if possible, or use prop.
|
|
16
|
-
// We'll leave height control to the parent or style, but provide a sensible default if passed as prop.
|
|
17
15
|
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
},
|
|
48
|
-
emoji: {
|
|
49
|
-
fontSize: 64,
|
|
50
|
-
textAlign: 'left',
|
|
51
|
-
includeFontPadding: false,
|
|
52
|
-
},
|
|
53
|
-
fadeOverlay: {
|
|
54
|
-
position: 'absolute',
|
|
55
|
-
bottom: -1,
|
|
56
|
-
left: 0,
|
|
57
|
-
right: 0,
|
|
58
|
-
height: 100,
|
|
59
|
-
// We can't use LinearGradient here per rules.
|
|
60
|
-
// Use a solid color with opacity or a series of views if really needed, but user said NO GRADIENT.
|
|
61
|
-
// The original code used backgroundColor: "rgba(15, 5, 10, 0.7)".
|
|
62
|
-
// We'll use a semi-transparent overlay at the bottom.
|
|
63
|
-
backgroundColor: 'rgba(0,0,0,0.5)', // Generic dark overlay
|
|
64
|
-
},
|
|
65
|
-
});
|
|
16
|
+
const themedStyles = useMemo(
|
|
17
|
+
() => ({
|
|
18
|
+
container: {
|
|
19
|
+
width: '100%' as const,
|
|
20
|
+
height: height || 400,
|
|
21
|
+
position: 'relative' as const,
|
|
22
|
+
backgroundColor: tokens.colors.surface,
|
|
23
|
+
justifyContent: 'flex-start' as const,
|
|
24
|
+
alignItems: 'flex-start' as const,
|
|
25
|
+
overflow: 'hidden' as const,
|
|
26
|
+
},
|
|
27
|
+
background: {
|
|
28
|
+
...StyleSheet.absoluteFillObject,
|
|
29
|
+
backgroundColor: tokens.colors.surfaceVariant,
|
|
30
|
+
},
|
|
31
|
+
iconWrapper: {
|
|
32
|
+
width: 120,
|
|
33
|
+
height: 120,
|
|
34
|
+
borderRadius: 60,
|
|
35
|
+
backgroundColor: tokens.colors.surfaceVariant,
|
|
36
|
+
justifyContent: 'flex-start' as const,
|
|
37
|
+
alignItems: 'flex-start' as const,
|
|
38
|
+
borderWidth: 2,
|
|
39
|
+
borderColor: tokens.colors.outlineVariant,
|
|
40
|
+
zIndex: 10,
|
|
41
|
+
},
|
|
42
|
+
}),
|
|
43
|
+
[tokens, height],
|
|
44
|
+
);
|
|
66
45
|
|
|
67
46
|
const source = imageUrl ? { uri: imageUrl } : imageSource;
|
|
68
47
|
|
|
69
48
|
return (
|
|
70
|
-
<View style={[
|
|
49
|
+
<View style={[themedStyles.container, style]}>
|
|
71
50
|
{source ? (
|
|
72
51
|
<Image
|
|
73
52
|
source={source}
|
|
@@ -75,12 +54,12 @@ export const HeroSection: React.FC<HeroSectionProps> = ({
|
|
|
75
54
|
resizeMode="cover"
|
|
76
55
|
/>
|
|
77
56
|
) : (
|
|
78
|
-
<View style={
|
|
57
|
+
<View style={themedStyles.background} />
|
|
79
58
|
)}
|
|
80
59
|
|
|
81
60
|
{/* Show icon only if no image */}
|
|
82
61
|
{!source && icon && (
|
|
83
|
-
<View style={
|
|
62
|
+
<View style={themedStyles.iconWrapper}>
|
|
84
63
|
<Text style={styles.emoji}>{icon}</Text>
|
|
85
64
|
</View>
|
|
86
65
|
)}
|
|
@@ -89,3 +68,24 @@ export const HeroSection: React.FC<HeroSectionProps> = ({
|
|
|
89
68
|
</View>
|
|
90
69
|
);
|
|
91
70
|
};
|
|
71
|
+
|
|
72
|
+
const styles = StyleSheet.create({
|
|
73
|
+
image: {
|
|
74
|
+
...StyleSheet.absoluteFillObject,
|
|
75
|
+
width: '100%',
|
|
76
|
+
height: '100%',
|
|
77
|
+
},
|
|
78
|
+
emoji: {
|
|
79
|
+
fontSize: 64,
|
|
80
|
+
textAlign: 'left',
|
|
81
|
+
includeFontPadding: false,
|
|
82
|
+
},
|
|
83
|
+
fadeOverlay: {
|
|
84
|
+
position: 'absolute',
|
|
85
|
+
bottom: -1,
|
|
86
|
+
left: 0,
|
|
87
|
+
right: 0,
|
|
88
|
+
height: 100,
|
|
89
|
+
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
90
|
+
},
|
|
91
|
+
});
|
|
@@ -105,33 +105,37 @@ export const FormContainer: React.FC<FormContainerProps> = ({
|
|
|
105
105
|
const formBottomPadding = bottomPosition;
|
|
106
106
|
const formElementSpacing = tokens.spacing.lg;
|
|
107
107
|
|
|
108
|
-
// Create styles for form container
|
|
109
|
-
const styles =
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
108
|
+
// Create styles for form container (memoized to avoid re-creation on every render)
|
|
109
|
+
const styles = React.useMemo(
|
|
110
|
+
() =>
|
|
111
|
+
StyleSheet.create({
|
|
112
|
+
container: {
|
|
113
|
+
flex: 1,
|
|
114
|
+
backgroundColor: tokens.colors.backgroundPrimary,
|
|
115
|
+
},
|
|
116
|
+
surface: {
|
|
117
|
+
flex: 1,
|
|
118
|
+
backgroundColor: tokens.colors.surface,
|
|
119
|
+
borderWidth: showBorder ? 1 : 0,
|
|
120
|
+
borderColor: tokens.colors.border,
|
|
121
|
+
borderRadius: tokens.borders.radius.md,
|
|
122
|
+
},
|
|
123
|
+
scrollView: {
|
|
124
|
+
flex: 1,
|
|
125
|
+
},
|
|
126
|
+
contentContainer: {
|
|
127
|
+
flexGrow: 1,
|
|
128
|
+
padding: tokens.spacing.lg,
|
|
129
|
+
paddingTop: tokens.spacing.xl,
|
|
130
|
+
paddingBottom: formBottomPadding + insets.bottom,
|
|
131
|
+
maxWidth: formContentWidth,
|
|
132
|
+
alignSelf: 'center',
|
|
133
|
+
width: '100%',
|
|
134
|
+
gap: formElementSpacing,
|
|
135
|
+
},
|
|
136
|
+
}),
|
|
137
|
+
[tokens, showBorder, formBottomPadding, insets.bottom, formContentWidth, formElementSpacing],
|
|
138
|
+
);
|
|
135
139
|
|
|
136
140
|
return (
|
|
137
141
|
<View style={[styles.container, containerStyle]} testID={testID}>
|
|
@@ -48,16 +48,37 @@ export function useCachedValue<T>(
|
|
|
48
48
|
useEffect(() => {
|
|
49
49
|
let isMounted = true;
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
const doLoad = async () => {
|
|
52
|
+
const cache = cacheManager.getCache<T>(cacheName, configRef.current);
|
|
53
|
+
const cached = cache.get(key);
|
|
54
|
+
|
|
55
|
+
if (cached !== undefined) {
|
|
56
|
+
if (isMounted) setValue(cached);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (isMounted) {
|
|
61
|
+
setIsLoading(true);
|
|
62
|
+
setError(null);
|
|
54
63
|
}
|
|
55
|
-
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const data = await fetcherRef.current!();
|
|
67
|
+
cache.set(key, data, configRef.current?.ttl);
|
|
68
|
+
if (isMounted) setValue(data);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
if (isMounted) setError(err as Error);
|
|
71
|
+
} finally {
|
|
72
|
+
if (isMounted) setIsLoading(false);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
doLoad();
|
|
56
77
|
|
|
57
78
|
return () => {
|
|
58
79
|
isMounted = false;
|
|
59
80
|
};
|
|
60
|
-
}, [
|
|
81
|
+
}, [cacheName, key]);
|
|
61
82
|
|
|
62
83
|
const invalidate = useCallback(() => {
|
|
63
84
|
const cache = cacheManager.getCache<T>(cacheName);
|
|
@@ -138,10 +138,10 @@ export function usePersistentCache<T>(
|
|
|
138
138
|
await loadFromStorage();
|
|
139
139
|
}, [loadFromStorage]);
|
|
140
140
|
|
|
141
|
-
//
|
|
141
|
+
// Load from storage when key, enabled, or version changes
|
|
142
142
|
useEffect(() => {
|
|
143
143
|
loadFromStorage();
|
|
144
|
-
}, [
|
|
144
|
+
}, [loadFromStorage]);
|
|
145
145
|
|
|
146
146
|
return {
|
|
147
147
|
data: state.data,
|
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
* Helper for creating stores in components
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { useMemo } from 'react';
|
|
6
|
+
import { useMemo, useRef } from 'react';
|
|
7
7
|
import { createStore } from '../../domain/factories/StoreFactory';
|
|
8
8
|
import type { StoreConfig } from '../../domain/types/Store';
|
|
9
9
|
|
|
10
10
|
export function useStore<T extends object>(config: StoreConfig<T>) {
|
|
11
|
-
// Config objesini stabilize et
|
|
12
|
-
const
|
|
13
|
-
|
|
11
|
+
// Config objesini stabilize et - sadece name değiştiğinde yeni store oluştur
|
|
12
|
+
const configRef = useRef(config);
|
|
13
|
+
configRef.current = config;
|
|
14
|
+
const store = useMemo(() => createStore(configRef.current), [config.name]);
|
|
14
15
|
return store;
|
|
15
16
|
}
|
|
@@ -56,15 +56,15 @@ export function usePrefetchQuery<
|
|
|
56
56
|
options: PrefetchOptions = {},
|
|
57
57
|
) {
|
|
58
58
|
const queryClient = useQueryClient();
|
|
59
|
-
const prefetchingRef = new Set<TVariables>();
|
|
59
|
+
const prefetchingRef = useRef(new Set<TVariables>());
|
|
60
60
|
|
|
61
61
|
const prefetch = useCallback(
|
|
62
62
|
async (variables: TVariables) => {
|
|
63
|
-
if (prefetchingRef.has(variables)) {
|
|
63
|
+
if (prefetchingRef.current.has(variables)) {
|
|
64
64
|
return;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
prefetchingRef.add(variables);
|
|
67
|
+
prefetchingRef.current.add(variables);
|
|
68
68
|
|
|
69
69
|
try {
|
|
70
70
|
await queryClient.prefetchQuery({
|
|
@@ -79,7 +79,7 @@ export function usePrefetchQuery<
|
|
|
79
79
|
console.log('[TanStack Query] Prefetched:', [...queryKey, variables]);
|
|
80
80
|
}
|
|
81
81
|
} finally {
|
|
82
|
-
prefetchingRef.delete(variables);
|
|
82
|
+
prefetchingRef.current.delete(variables);
|
|
83
83
|
}
|
|
84
84
|
},
|
|
85
85
|
[queryClient, queryKey, queryFn, options.staleTime, options.gcTime],
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useMemo
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
2
|
import { timezoneService } from '../../infrastructure/services/TimezoneService';
|
|
3
3
|
import type { TimezoneInfo, TimezoneCalendarDay } from '../../domain/entities/Timezone';
|
|
4
4
|
|
|
@@ -51,101 +51,57 @@ export const useTimezone = (options?: UseTimezoneOptions): UseTimezoneReturn =>
|
|
|
51
51
|
const locale = options?.locale ?? 'en';
|
|
52
52
|
const timezoneInfo = useMemo(() => timezoneService.getTimezoneInfo(), []);
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const areSameMinute = useCallback((date1: Date, date2: Date) =>
|
|
108
|
-
timezoneService.areSameMinute(date1, date2), []);
|
|
109
|
-
const getMiddleOfDay = useCallback((date: Date) => timezoneService.getMiddleOfDay(date), []);
|
|
110
|
-
|
|
111
|
-
return {
|
|
112
|
-
timezone: timezoneInfo.timezone || '',
|
|
113
|
-
timezoneInfo,
|
|
114
|
-
formatDate,
|
|
115
|
-
formatTime,
|
|
116
|
-
getCalendarDays,
|
|
117
|
-
isToday,
|
|
118
|
-
isSameDay,
|
|
119
|
-
addDays,
|
|
120
|
-
startOfDay,
|
|
121
|
-
endOfDay,
|
|
122
|
-
formatDateToString,
|
|
123
|
-
getCurrentISOString,
|
|
124
|
-
formatToISOString,
|
|
125
|
-
formatRelativeTime,
|
|
126
|
-
formatDateTime,
|
|
127
|
-
getTimezones,
|
|
128
|
-
isValid,
|
|
129
|
-
getAge,
|
|
130
|
-
isBetween,
|
|
131
|
-
min,
|
|
132
|
-
max,
|
|
133
|
-
getWeek,
|
|
134
|
-
getQuarter,
|
|
135
|
-
getTimezoneOffsetFor,
|
|
136
|
-
convertTimezone,
|
|
137
|
-
formatDuration,
|
|
138
|
-
isWeekend,
|
|
139
|
-
addBusinessDays,
|
|
140
|
-
isFirstDayOfMonth,
|
|
141
|
-
isLastDayOfMonth,
|
|
142
|
-
getDaysInMonth,
|
|
143
|
-
getDateRange,
|
|
144
|
-
areRangesOverlapping,
|
|
145
|
-
clampDate,
|
|
146
|
-
areSameHour,
|
|
147
|
-
areSameMinute,
|
|
148
|
-
getMiddleOfDay,
|
|
149
|
-
fromNow,
|
|
150
|
-
};
|
|
54
|
+
// timezoneService is a singleton with pure functions - no need for individual useCallbacks.
|
|
55
|
+
// Only locale-dependent functions need to be memoized when locale changes.
|
|
56
|
+
return useMemo(
|
|
57
|
+
() => ({
|
|
58
|
+
timezone: timezoneInfo.timezone || '',
|
|
59
|
+
timezoneInfo,
|
|
60
|
+
// Locale-dependent formatters
|
|
61
|
+
formatDate: (date: Date, opts?: Intl.DateTimeFormatOptions) =>
|
|
62
|
+
timezoneService.formatDate(date, locale, opts),
|
|
63
|
+
formatTime: (date: Date, opts?: Intl.DateTimeFormatOptions) =>
|
|
64
|
+
timezoneService.formatTime(date, locale, opts),
|
|
65
|
+
formatDateTime: (date: Date, opts?: Intl.DateTimeFormatOptions) =>
|
|
66
|
+
timezoneService.formatDateTime(date, locale, opts),
|
|
67
|
+
formatRelativeTime: (date: Date) =>
|
|
68
|
+
timezoneService.formatRelativeTime(date, locale),
|
|
69
|
+
fromNow: (date: Date) =>
|
|
70
|
+
timezoneService.fromNow(date, locale),
|
|
71
|
+
// Locale-independent delegates
|
|
72
|
+
getCalendarDays: (year: number, month: number) => timezoneService.getCalendarDays(year, month),
|
|
73
|
+
isToday: (date: Date) => timezoneService.isToday(date),
|
|
74
|
+
isSameDay: (date1: Date, date2: Date) => timezoneService.isSameDay(date1, date2),
|
|
75
|
+
addDays: (date: Date, days: number) => timezoneService.addDays(date, days),
|
|
76
|
+
startOfDay: (date: Date) => timezoneService.startOfDay(date),
|
|
77
|
+
endOfDay: (date: Date) => timezoneService.endOfDay(date),
|
|
78
|
+
formatDateToString: (date: Date) => timezoneService.formatDateToString(date),
|
|
79
|
+
getCurrentISOString: () => timezoneService.getCurrentISOString(),
|
|
80
|
+
formatToISOString: (date: Date) => timezoneService.formatToISOString(date),
|
|
81
|
+
getTimezones: () => timezoneService.getTimezones(),
|
|
82
|
+
isValid: (date: Date) => timezoneService.isValid(date),
|
|
83
|
+
getAge: (birthDate: Date) => timezoneService.getAge(birthDate),
|
|
84
|
+
isBetween: (date: Date, start: Date, end: Date) => timezoneService.isBetween(date, start, end),
|
|
85
|
+
min: (dates: Date[]) => timezoneService.min(dates),
|
|
86
|
+
max: (dates: Date[]) => timezoneService.max(dates),
|
|
87
|
+
getWeek: (date: Date) => timezoneService.getWeek(date),
|
|
88
|
+
getQuarter: (date: Date) => timezoneService.getQuarter(date),
|
|
89
|
+
getTimezoneOffsetFor: (tz: string, date?: Date) => timezoneService.getTimezoneOffsetFor(tz, date),
|
|
90
|
+
convertTimezone: (date: Date, from: string, to: string) => timezoneService.convertTimezone(date, from, to),
|
|
91
|
+
formatDuration: (ms: number) => timezoneService.formatDuration(ms),
|
|
92
|
+
isWeekend: (date: Date) => timezoneService.isWeekend(date),
|
|
93
|
+
addBusinessDays: (date: Date, days: number) => timezoneService.addBusinessDays(date, days),
|
|
94
|
+
isFirstDayOfMonth: (date: Date) => timezoneService.isFirstDayOfMonth(date),
|
|
95
|
+
isLastDayOfMonth: (date: Date) => timezoneService.isLastDayOfMonth(date),
|
|
96
|
+
getDaysInMonth: (date: Date) => timezoneService.getDaysInMonth(date),
|
|
97
|
+
getDateRange: (start: Date, end: Date) => timezoneService.getDateRange(start, end),
|
|
98
|
+
areRangesOverlapping: (s1: Date, e1: Date, s2: Date, e2: Date) =>
|
|
99
|
+
timezoneService.areRangesOverlapping(s1, e1, s2, e2),
|
|
100
|
+
clampDate: (date: Date, mn: Date, mx: Date) => timezoneService.clampDate(date, mn, mx),
|
|
101
|
+
areSameHour: (d1: Date, d2: Date) => timezoneService.areSameHour(d1, d2),
|
|
102
|
+
areSameMinute: (d1: Date, d2: Date) => timezoneService.areSameMinute(d1, d2),
|
|
103
|
+
getMiddleOfDay: (date: Date) => timezoneService.getMiddleOfDay(date),
|
|
104
|
+
}),
|
|
105
|
+
[locale, timezoneInfo],
|
|
106
|
+
);
|
|
151
107
|
};
|