@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-design-system",
3
- "version": "4.23.60",
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",
@@ -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, BASE_TOKENS } from '../theme';
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={[styles.container, style]} testID={testID}>
76
+ <View style={[themedStyles.container, style]} testID={testID}>
44
77
  {illustration ? (
45
78
  illustration
46
79
  ) : (
47
80
  <View
48
81
  style={[
49
- styles.iconContainer,
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={[styles.title, { textAlign: 'left' }]}
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={[styles.description, { textAlign: 'left' }]}
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
- styles.actionButton,
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: await this.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
  */
@@ -5,5 +5,3 @@
5
5
  */
6
6
 
7
7
  export * from "../exception";
8
-
9
- export { ErrorBoundary } from "../exception/presentation/components/ErrorBoundary";
@@ -1,6 +1 @@
1
1
  export * from "../tanstack";
2
-
3
- export {
4
- getGlobalQueryClient,
5
- hasGlobalQueryClient,
6
- } from "../tanstack/infrastructure/config/QueryClientSingleton";
@@ -5,5 +5,3 @@
5
5
  */
6
6
 
7
7
  export * from "../timezone";
8
-
9
- export { timezoneService } from "../timezone/infrastructure/services/TimezoneService";
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 styles = StyleSheet.create({
21
- container: {
22
- flexDirection: 'row',
23
- alignItems: 'center',
24
- paddingVertical: tokens.spacing.md,
25
- gap: tokens.spacing.md,
26
- },
27
- backButton: {
28
- width: 56,
29
- height: 56,
30
- borderRadius: tokens.borders.radius.lg,
31
- backgroundColor: tokens.colors.surface,
32
- justifyContent: 'center',
33
- alignItems: 'center',
34
- borderWidth: 1,
35
- borderColor: tokens.colors.outlineVariant,
36
- },
37
- actionButton: {
38
- flex: 1,
39
- height: 56,
40
- borderRadius: tokens.borders.radius.lg,
41
- overflow: 'hidden',
42
- },
43
- actionContent: {
44
- flex: 1,
45
- flexDirection: 'row',
46
- alignItems: 'center',
47
- justifyContent: 'flex-start',
48
- backgroundColor: tokens.colors.primary,
49
- gap: tokens.spacing.sm,
50
- paddingHorizontal: tokens.spacing.lg,
51
- },
52
- actionText: {
53
- color: tokens.colors.onPrimary,
54
- fontWeight: '800',
55
- fontSize: 18,
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={[styles.container, style]}>
64
+ <View style={[themedStyles.container, style]}>
61
65
  <TouchableOpacity
62
- style={styles.backButton}
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={styles.actionButton}
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={styles.actionContent}>
82
- <AtomicText style={styles.actionText}>{actionLabel}</AtomicText>
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 styles = StyleSheet.create({
19
- container: {
20
- width: '100%',
21
- height: height || 400,
22
- position: 'relative',
23
- backgroundColor: tokens.colors.surface,
24
- justifyContent: 'flex-start',
25
- alignItems: 'flex-start',
26
- overflow: 'hidden',
27
- },
28
- background: {
29
- ...StyleSheet.absoluteFillObject,
30
- backgroundColor: tokens.colors.surfaceVariant,
31
- },
32
- image: {
33
- ...StyleSheet.absoluteFillObject,
34
- width: '100%',
35
- height: '100%',
36
- },
37
- iconWrapper: {
38
- width: 120,
39
- height: 120,
40
- borderRadius: 60,
41
- backgroundColor: tokens.colors.surfaceVariant,
42
- justifyContent: 'flex-start',
43
- alignItems: 'flex-start',
44
- borderWidth: 2,
45
- borderColor: tokens.colors.outlineVariant,
46
- zIndex: 10,
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={[styles.container, 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={styles.background} />
57
+ <View style={themedStyles.background} />
79
58
  )}
80
59
 
81
60
  {/* Show icon only if no image */}
82
61
  {!source && icon && (
83
- <View style={styles.iconWrapper}>
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 = StyleSheet.create({
110
- container: {
111
- flex: 1,
112
- backgroundColor: tokens.colors.backgroundPrimary,
113
- },
114
- surface: {
115
- flex: 1,
116
- backgroundColor: tokens.colors.surface,
117
- borderWidth: showBorder ? 1 : 0,
118
- borderColor: tokens.colors.border,
119
- borderRadius: tokens.borders.radius.md,
120
- },
121
- scrollView: {
122
- flex: 1,
123
- },
124
- contentContainer: {
125
- flexGrow: 1,
126
- padding: tokens.spacing.lg,
127
- paddingTop: tokens.spacing.xl,
128
- paddingBottom: formBottomPadding + insets.bottom,
129
- maxWidth: formContentWidth,
130
- alignSelf: 'center',
131
- width: '100%',
132
- gap: formElementSpacing,
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
- loadValue().then(() => {
52
- if (!isMounted) {
53
- setValue(undefined);
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
- }, [loadValue]);
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
- // Prevent infinite loops by only running when key or enabled changes
141
+ // Load from storage when key, enabled, or version changes
142
142
  useEffect(() => {
143
143
  loadFromStorage();
144
- }, [key, enabled]);
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 ki sonsuz re-render olmasın
12
- const stableConfig = useMemo(() => config, [config.name, JSON.stringify(config)]);
13
- const store = useMemo(() => createStore(stableConfig), [stableConfig]);
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, useCallback } from 'react';
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
- const formatDate = useCallback((date: Date, options?: Intl.DateTimeFormatOptions) =>
55
- timezoneService.formatDate(date, locale, options), [locale]);
56
-
57
- const formatTime = useCallback((date: Date, options?: Intl.DateTimeFormatOptions) =>
58
- timezoneService.formatTime(date, locale, options), [locale]);
59
-
60
- const formatDateTime = useCallback((date: Date, options?: Intl.DateTimeFormatOptions) =>
61
- timezoneService.formatDateTime(date, locale, options), [locale]);
62
-
63
- const formatRelativeTime = useCallback((date: Date) =>
64
- timezoneService.formatRelativeTime(date, locale), [locale]);
65
-
66
- const fromNow = useCallback((date: Date) =>
67
- timezoneService.fromNow(date, locale), [locale]);
68
-
69
- const getCalendarDays = useCallback((year: number, month: number) =>
70
- timezoneService.getCalendarDays(year, month), []);
71
-
72
- const isToday = useCallback((date: Date) => timezoneService.isToday(date), []);
73
- const isSameDay = useCallback((date1: Date, date2: Date) => timezoneService.isSameDay(date1, date2), []);
74
- const addDays = useCallback((date: Date, days: number) => timezoneService.addDays(date, days), []);
75
- const startOfDay = useCallback((date: Date) => timezoneService.startOfDay(date), []);
76
- const endOfDay = useCallback((date: Date) => timezoneService.endOfDay(date), []);
77
- const formatDateToString = useCallback((date: Date) => timezoneService.formatDateToString(date), []);
78
- const getCurrentISOString = useCallback(() => timezoneService.getCurrentISOString(), []);
79
- const formatToISOString = useCallback((date: Date) => timezoneService.formatToISOString(date), []);
80
- const getTimezones = useCallback(() => timezoneService.getTimezones(), []);
81
- const isValid = useCallback((date: Date) => timezoneService.isValid(date), []);
82
- const getAge = useCallback((birthDate: Date) => timezoneService.getAge(birthDate), []);
83
- const isBetween = useCallback((date: Date, start: Date, end: Date) =>
84
- timezoneService.isBetween(date, start, end), []);
85
- const min = useCallback((dates: Date[]) => timezoneService.min(dates), []);
86
- const max = useCallback((dates: Date[]) => timezoneService.max(dates), []);
87
- const getWeek = useCallback((date: Date) => timezoneService.getWeek(date), []);
88
- const getQuarter = useCallback((date: Date) => timezoneService.getQuarter(date), []);
89
- const getTimezoneOffsetFor = useCallback((timezone: string, date?: Date) =>
90
- timezoneService.getTimezoneOffsetFor(timezone, date), []);
91
- const convertTimezone = useCallback((date: Date, fromTimezone: string, toTimezone: string) =>
92
- timezoneService.convertTimezone(date, fromTimezone, toTimezone), []);
93
- const formatDuration = useCallback((milliseconds: number) => timezoneService.formatDuration(milliseconds), []);
94
- const isWeekend = useCallback((date: Date) => timezoneService.isWeekend(date), []);
95
- const addBusinessDays = useCallback((date: Date, days: number) =>
96
- timezoneService.addBusinessDays(date, days), []);
97
- const isFirstDayOfMonth = useCallback((date: Date) => timezoneService.isFirstDayOfMonth(date), []);
98
- const isLastDayOfMonth = useCallback((date: Date) => timezoneService.isLastDayOfMonth(date), []);
99
- const getDaysInMonth = useCallback((date: Date) => timezoneService.getDaysInMonth(date), []);
100
- const getDateRange = useCallback((start: Date, end: Date) => timezoneService.getDateRange(start, end), []);
101
- const areRangesOverlapping = useCallback((start1: Date, end1: Date, start2: Date, end2: Date) =>
102
- timezoneService.areRangesOverlapping(start1, end1, start2, end2), []);
103
- const clampDate = useCallback((date: Date, min: Date, max: Date) =>
104
- timezoneService.clampDate(date, min, max), []);
105
- const areSameHour = useCallback((date1: Date, date2: Date) =>
106
- timezoneService.areSameHour(date1, date2), []);
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
  };