@umituz/react-native-design-system 4.23.96 → 4.23.100

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 (36) hide show
  1. package/package.json +1 -1
  2. package/src/atoms/button/AtomicButton.tsx +7 -0
  3. package/src/atoms/button/types/index.ts +4 -0
  4. package/src/atoms/input/hooks/useInputState.ts +3 -7
  5. package/src/haptics/infrastructure/services/HapticService.ts +1 -1
  6. package/src/media/infrastructure/hooks/useGenericMediaGeneration.ts +170 -0
  7. package/src/media/presentation/hooks/useCardMediaGeneration.ts +9 -113
  8. package/src/media/presentation/hooks/useCardMediaUpload.ts +5 -5
  9. package/src/media/presentation/hooks/useCardMediaValidation.ts +4 -1
  10. package/src/media/presentation/hooks/useMediaGeneration.ts +4 -87
  11. package/src/molecules/navigation/components/NavigationHeader.tsx +3 -3
  12. package/src/molecules/navigation/utils/AppNavigation.ts +20 -1
  13. package/src/offline/index.ts +1 -0
  14. package/src/offline/infrastructure/storage/OfflineConfigStore.ts +34 -0
  15. package/src/offline/presentation/hooks/useOffline.ts +8 -4
  16. package/src/storage/domain/utils/devUtils.ts +0 -24
  17. package/src/storage/index.ts +1 -1
  18. package/src/storage/infrastructure/adapters/StorageService.ts +2 -7
  19. package/src/storage/infrastructure/repositories/BaseStorageOperations.ts +0 -3
  20. package/src/storage/presentation/hooks/CacheStorageOperations.ts +2 -8
  21. package/src/storage/presentation/hooks/useStore.ts +14 -5
  22. package/src/utilities/sharing/presentation/hooks/useSharing.ts +3 -3
  23. package/src/layouts/ScreenLayout/ScreenLayout.example.tsx +0 -92
  24. package/src/media/domain/entities/CardMultimedia.types.ts +0 -120
  25. package/src/media/infrastructure/services/CardMediaGenerationService.README.md +0 -99
  26. package/src/media/infrastructure/services/CardMediaGenerationService.ts +0 -101
  27. package/src/media/infrastructure/services/CardMediaOptimizerService.README.md +0 -167
  28. package/src/media/infrastructure/services/CardMediaOptimizerService.ts +0 -36
  29. package/src/media/infrastructure/services/CardMediaUploadService.README.md +0 -123
  30. package/src/media/infrastructure/services/CardMediaUploadService.ts +0 -62
  31. package/src/media/infrastructure/services/CardMediaValidationService.README.md +0 -134
  32. package/src/media/infrastructure/services/CardMediaValidationService.ts +0 -81
  33. package/src/media/presentation/hooks/useCardMediaGeneration.README.md +0 -164
  34. package/src/media/presentation/hooks/useCardMediaUpload.README.md +0 -153
  35. package/src/media/presentation/hooks/useCardMediaValidation.README.md +0 -176
  36. package/src/storage/domain/utils/__tests__/devUtils.test.ts +0 -97
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-design-system",
3
- "version": "4.23.96",
3
+ "version": "4.23.100",
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",
@@ -29,6 +29,9 @@ export const AtomicButton: React.FC<AtomicButtonProps> = React.memo(({
29
29
  textStyle,
30
30
  activeOpacity = 0.8,
31
31
  testID,
32
+ accessibilityLabel,
33
+ accessibilityHint,
34
+ accessibilityRole = 'button',
32
35
  }) => {
33
36
  const tokens = useAppDesignTokens();
34
37
 
@@ -80,6 +83,10 @@ export const AtomicButton: React.FC<AtomicButtonProps> = React.memo(({
80
83
  activeOpacity={activeOpacity}
81
84
  disabled={isDisabled}
82
85
  testID={testID}
86
+ accessibilityRole={accessibilityRole}
87
+ accessibilityLabel={accessibilityLabel || title || (typeof children === 'string' ? children : 'Button')}
88
+ accessibilityHint={accessibilityHint}
89
+ accessibilityState={{ disabled: isDisabled, busy: loading }}
83
90
  >
84
91
  {loading ? (
85
92
  <AtomicSpinner
@@ -24,6 +24,10 @@ export interface AtomicButtonProps {
24
24
  readonly textStyle?: StyleProp<TextStyle>;
25
25
  readonly activeOpacity?: number;
26
26
  readonly testID?: string;
27
+ // Accessibility props
28
+ readonly accessibilityLabel?: string;
29
+ readonly accessibilityHint?: string;
30
+ readonly accessibilityRole?: 'button' | 'link';
27
31
  }
28
32
 
29
33
  export interface ButtonSizeConfig {
@@ -1,12 +1,10 @@
1
- import { useState, useCallback, useEffect } from 'react';
1
+ import { useState, useCallback, useEffect, useMemo } from 'react';
2
2
 
3
3
  interface UseInputStateProps {
4
4
  value?: string;
5
5
  onChangeText?: (text: string) => void;
6
6
  secureTextEntry?: boolean;
7
- showPasswordToggle?: boolean;
8
7
  maxLength?: number;
9
- showCharacterCount?: boolean;
10
8
  }
11
9
 
12
10
  interface UseInputStateReturn {
@@ -24,9 +22,7 @@ export const useInputState = ({
24
22
  value = '',
25
23
  onChangeText,
26
24
  secureTextEntry = false,
27
- showPasswordToggle: _showPasswordToggle = false,
28
25
  maxLength,
29
- showCharacterCount: _showCharacterCount = false,
30
26
  }: UseInputStateProps = {}): UseInputStateReturn => {
31
27
  const [localValue, setLocalValue] = useState(value);
32
28
  const [isFocused, setIsFocused] = useState(false);
@@ -49,7 +45,7 @@ export const useInputState = ({
49
45
  const characterCount = localValue.length;
50
46
  const isAtMaxLength = maxLength ? characterCount >= maxLength : false;
51
47
 
52
- return {
48
+ return useMemo(() => ({
53
49
  localValue,
54
50
  isFocused,
55
51
  isPasswordVisible,
@@ -58,5 +54,5 @@ export const useInputState = ({
58
54
  setIsFocused,
59
55
  handleTextChange,
60
56
  togglePasswordVisibility,
61
- };
57
+ }), [localValue, isFocused, isPasswordVisible, characterCount, isAtMaxLength, handleTextChange, togglePasswordVisibility]);
62
58
  };
@@ -15,7 +15,7 @@ import type { ImpactStyle, NotificationType, HapticPattern } from '../../domain/
15
15
  * Log error in development mode only
16
16
  */
17
17
  function logError(method: string, error: unknown): void {
18
- if (process.env.NODE_ENV === 'development') {
18
+ if (__DEV__) {
19
19
  console.error(`[DesignSystem] HapticService.${method} error:`, error);
20
20
  }
21
21
  }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Generic Media Generation Hook
3
+ * Shared implementation for both Media and CardMedia generation
4
+ * Eliminates ~600 LOC duplication
5
+ */
6
+
7
+ import { useState, useCallback } from "react";
8
+
9
+ interface GenericMediaAttachment {
10
+ id: string;
11
+ type: string;
12
+ position: string;
13
+ url: string;
14
+ filename: string;
15
+ fileSize: number;
16
+ mimeType: string;
17
+ duration?: number;
18
+ thumbnailUrl?: string;
19
+ caption?: string;
20
+ isDownloaded: boolean;
21
+ createdAt: string;
22
+ }
23
+
24
+ interface GenericMediaGenerationRequest {
25
+ type: "text_to_image" | "text_to_audio" | "image_search";
26
+ input: {
27
+ text?: string;
28
+ prompt?: string;
29
+ language?: string;
30
+ voice?: "male" | "female" | "neutral";
31
+ style?: "realistic" | "cartoon" | "artistic";
32
+ };
33
+ options: {
34
+ maxResults?: number;
35
+ quality?: "low" | "medium" | "high";
36
+ format?: "jpeg" | "png" | "mp3" | "wav";
37
+ };
38
+ }
39
+
40
+ interface GenericMediaGenerationResult<TAttachment> {
41
+ success: boolean;
42
+ attachments: TAttachment[];
43
+ creditsUsed: number;
44
+ processingTime: number;
45
+ error?: string;
46
+ requestId: string;
47
+ }
48
+
49
+ export interface UseGenericMediaGenerationResult<TAttachment, TRequest> {
50
+ generateMedia: (request: TRequest) => Promise<GenericMediaGenerationResult<TAttachment>>;
51
+ isGenerating: boolean;
52
+ generationResult: GenericMediaGenerationResult<TAttachment> | null;
53
+ error: string | null;
54
+ }
55
+
56
+ /**
57
+ * Generic implementation of media generation logic
58
+ * Type-safe through attachment factory pattern
59
+ */
60
+ export function useGenericMediaGeneration<
61
+ TAttachment extends GenericMediaAttachment,
62
+ TRequest extends GenericMediaGenerationRequest
63
+ >(
64
+ attachmentFactory: (baseAttachment: GenericMediaAttachment) => TAttachment
65
+ ): UseGenericMediaGenerationResult<TAttachment, TRequest> {
66
+ const [isGenerating, setIsGenerating] = useState(false);
67
+ const [generationResult, setGenerationResult] =
68
+ useState<GenericMediaGenerationResult<TAttachment> | null>(null);
69
+ const [error, setError] = useState<string | null>(null);
70
+
71
+ const generateMedia = useCallback(
72
+ async (request: TRequest): Promise<GenericMediaGenerationResult<TAttachment>> => {
73
+ try {
74
+ setIsGenerating(true);
75
+ setError(null);
76
+
77
+ // Simulate generation
78
+ await new Promise((resolve) => setTimeout(resolve, 3000));
79
+
80
+ const baseAttachments: GenericMediaAttachment[] = [];
81
+
82
+ switch (request.type) {
83
+ case "text_to_image":
84
+ for (let i = 0; i < (request.options.maxResults || 1); i++) {
85
+ baseAttachments.push({
86
+ id: `ai_img_${Date.now()}_${i}`,
87
+ type: "image",
88
+ position: "both",
89
+ url: `https://picsum.photos/400/300?random=${Date.now() + i}`,
90
+ filename: `ai_generated_${i}.jpg`,
91
+ fileSize: 150000,
92
+ mimeType: "image/jpeg",
93
+ isDownloaded: false,
94
+ createdAt: new Date().toISOString(),
95
+ });
96
+ }
97
+ break;
98
+
99
+ case "text_to_audio":
100
+ baseAttachments.push({
101
+ id: `ai_audio_${Date.now()}`,
102
+ type: "audio",
103
+ position: "back",
104
+ url: `https://example.com/audio_${Date.now()}.mp3`,
105
+ filename: `ai_generated_${Date.now()}.mp3`,
106
+ fileSize: 80000,
107
+ mimeType: "audio/mp3",
108
+ duration: 10,
109
+ isDownloaded: false,
110
+ createdAt: new Date().toISOString(),
111
+ });
112
+ break;
113
+
114
+ case "image_search":
115
+ for (let i = 0; i < (request.options.maxResults || 5); i++) {
116
+ baseAttachments.push({
117
+ id: `search_img_${Date.now()}_${i}`,
118
+ type: "image",
119
+ position: "both",
120
+ url: `https://picsum.photos/400/300?random=${Date.now() + i}`,
121
+ filename: `search_result_${i}.jpg`,
122
+ fileSize: 120000,
123
+ mimeType: "image/jpeg",
124
+ isDownloaded: false,
125
+ createdAt: new Date().toISOString(),
126
+ });
127
+ }
128
+ break;
129
+ }
130
+
131
+ const attachments = baseAttachments.map(attachmentFactory);
132
+
133
+ const result: GenericMediaGenerationResult<TAttachment> = {
134
+ success: true,
135
+ attachments,
136
+ creditsUsed: request.type === "text_to_image" ? 5 : request.type === "text_to_audio" ? 3 : 2,
137
+ processingTime: 3000,
138
+ requestId: `req_${Date.now()}`,
139
+ };
140
+
141
+ setGenerationResult(result);
142
+ return result;
143
+ } catch (err) {
144
+ const errorMessage =
145
+ err instanceof Error ? err.message : "Generation failed";
146
+ setError(errorMessage);
147
+ setIsGenerating(false);
148
+
149
+ return {
150
+ success: false,
151
+ attachments: [],
152
+ creditsUsed: 0,
153
+ processingTime: 0,
154
+ error: errorMessage,
155
+ requestId: "",
156
+ };
157
+ } finally {
158
+ setIsGenerating(false);
159
+ }
160
+ },
161
+ [attachmentFactory],
162
+ );
163
+
164
+ return {
165
+ generateMedia,
166
+ isGenerating,
167
+ generationResult,
168
+ error,
169
+ };
170
+ }
@@ -1,124 +1,20 @@
1
1
  /**
2
2
  * Card Media Generation Hook
3
3
  * Hook for generating card media with AI
4
+ * Now a thin wrapper around useGenericMediaGeneration
5
+ *
6
+ * Note: CardMedia types are aliases of Media types for backward compatibility
4
7
  */
5
8
 
6
- import { useState, useCallback } from "react";
9
+ import { useGenericMediaGeneration } from "../../infrastructure/hooks/useGenericMediaGeneration";
7
10
  import type { UseCardMediaGenerationResult } from "./card-multimedia.types";
8
11
  import type {
9
- CardMediaAttachment,
10
- CardMediaGenerationRequest,
11
- CardMediaGenerationResult,
12
- } from "../../domain/entities/CardMultimedia.types";
12
+ MediaAttachment as CardMediaAttachment,
13
+ MediaGenerationRequest as CardMediaGenerationRequest,
14
+ } from "../../domain/entities/MultimediaFlashcardTypes";
13
15
 
14
16
  export const useCardMediaGeneration = (): UseCardMediaGenerationResult => {
15
- const [isGenerating, setIsGenerating] = useState(false);
16
- const [generationResult, setGenerationResult] =
17
- useState<CardMediaGenerationResult | null>(null);
18
- const [error, setError] = useState<string | null>(null);
19
-
20
- const generateMedia = useCallback(
21
- async (
22
- request: CardMediaGenerationRequest,
23
- ): Promise<CardMediaGenerationResult> => {
24
- try {
25
- setIsGenerating(true);
26
- setError(null);
27
-
28
- // Simulate generation
29
- await new Promise((resolve) => setTimeout(resolve, 3000));
30
-
31
- const attachments: CardMediaAttachment[] = [];
32
-
33
- switch (request.type) {
34
- case "text_to_image":
35
- for (let i = 0; i < (request.options.maxResults || 1); i++) {
36
- attachments.push({
37
- id: `ai_img_${Date.now()}_${i}`,
38
- type: "image",
39
- position: "both",
40
- url: `https://picsum.photos/400/300?random=${Date.now() + i}`,
41
- filename: `ai_generated_${i}.jpg`,
42
- fileSize: 150000, // 150KB
43
- mimeType: "image/jpeg",
44
- isDownloaded: false,
45
- createdAt: new Date().toISOString(),
46
- });
47
- }
48
- break;
49
-
50
- case "text_to_audio":
51
- attachments.push({
52
- id: `ai_audio_${Date.now()}`,
53
- type: "audio",
54
- position: "back",
55
- url: `https://example.com/audio_${Date.now()}.mp3`,
56
- filename: `ai_generated_${Date.now()}.mp3`,
57
- fileSize: 80000, // 80KB
58
- mimeType: "audio/mp3",
59
- duration: 10, // 10 seconds
60
- isDownloaded: false,
61
- createdAt: new Date().toISOString(),
62
- });
63
- break;
64
-
65
- case "image_search":
66
- for (let i = 0; i < (request.options.maxResults || 5); i++) {
67
- attachments.push({
68
- id: `search_img_${Date.now()}_${i}`,
69
- type: "image",
70
- position: "both",
71
- url: `https://picsum.photos/400/300?random=${Date.now() + i}`,
72
- filename: `search_result_${i}.jpg`,
73
- fileSize: 120000, // 120KB
74
- mimeType: "image/jpeg",
75
- isDownloaded: false,
76
- createdAt: new Date().toISOString(),
77
- });
78
- }
79
- break;
80
- }
81
-
82
- const result: CardMediaGenerationResult = {
83
- success: true,
84
- attachments,
85
- creditsUsed:
86
- request.type === "text_to_image"
87
- ? 5
88
- : request.type === "text_to_audio"
89
- ? 3
90
- : 2,
91
- processingTime: 3000,
92
- requestId: `req_${Date.now()}`,
93
- };
94
-
95
- setGenerationResult(result);
96
- return result;
97
- } catch (err) {
98
- const errorMessage =
99
- err instanceof Error ? err.message : "Generation failed";
100
- setError(errorMessage);
101
- setIsGenerating(false);
102
-
103
- return {
104
- success: false,
105
- attachments: [],
106
- creditsUsed: 0,
107
- processingTime: 0,
108
- error: errorMessage,
109
- requestId: "",
110
- };
111
- } finally {
112
- setIsGenerating(false);
113
- }
114
- },
115
- [],
17
+ return useGenericMediaGeneration<CardMediaAttachment, CardMediaGenerationRequest>(
18
+ (baseAttachment) => baseAttachment as CardMediaAttachment
116
19
  );
117
-
118
- return {
119
- generateMedia,
120
- isGenerating,
121
- generationResult,
122
- error,
123
- };
124
20
  };
@@ -8,11 +8,11 @@ import { generateThumbnail, getMediaDuration } from "../../infrastructure/utils/
8
8
  import { getMediaTypeFromMime } from "../../infrastructure/utils/mime-type-detector";
9
9
  import type { UseCardMediaUploadResult } from "./card-multimedia.types";
10
10
  import type {
11
- CardMediaAttachment,
12
- CardMediaCompressionOptions,
13
- CardMediaFile,
14
- CardMediaUploadProgress,
15
- } from "../../domain/entities/CardMultimedia.types";
11
+ MediaAttachment as CardMediaAttachment,
12
+ MediaCompressionOptions as CardMediaCompressionOptions,
13
+ MediaFile as CardMediaFile,
14
+ MediaUploadProgress as CardMediaUploadProgress,
15
+ } from "../../domain/entities/MultimediaFlashcardTypes";
16
16
 
17
17
  export const useCardMediaUpload = (): UseCardMediaUploadResult => {
18
18
  const [isUploading, setIsUploading] = useState(false);
@@ -6,7 +6,10 @@
6
6
  import { useState, useCallback } from "react";
7
7
  import { formatFileSize } from "../../infrastructure/utils/media-collection-utils";
8
8
  import type { UseCardMediaValidationResult } from "./card-multimedia.types";
9
- import type { CardMediaValidation, CardMediaFile } from "../../domain/entities/CardMultimedia.types";
9
+ import type {
10
+ MediaValidation as CardMediaValidation,
11
+ MediaFile as CardMediaFile,
12
+ } from "../../domain/entities/MultimediaFlashcardTypes";
10
13
 
11
14
  export const useCardMediaValidation = (): UseCardMediaValidationResult => {
12
15
  const [isValidating, setIsValidating] = useState(false);
@@ -1,101 +1,18 @@
1
1
  /**
2
2
  * Media Generation Hook
3
3
  * Hook for generating media with AI
4
+ * Now a thin wrapper around useGenericMediaGeneration
4
5
  */
5
6
 
6
- import { useState, useCallback } from "react";
7
+ import { useGenericMediaGeneration } from "../../infrastructure/hooks/useGenericMediaGeneration";
7
8
  import type { UseMediaGenerationResult } from "./multimedia.types";
8
9
  import type {
9
10
  MediaAttachment,
10
11
  MediaGenerationRequest,
11
- MediaGenerationResult,
12
12
  } from "../../domain/entities/MultimediaFlashcardTypes";
13
13
 
14
14
  export const useMediaGeneration = (): UseMediaGenerationResult => {
15
- const [isGenerating, setIsGenerating] = useState(false);
16
- const [generationResult, setGenerationResult] =
17
- useState<MediaGenerationResult | null>(null);
18
- const [error, setError] = useState<string | null>(null);
19
-
20
- const generateMedia = useCallback(
21
- async (request: MediaGenerationRequest): Promise<MediaGenerationResult> => {
22
- try {
23
- setIsGenerating(true);
24
- setError(null);
25
-
26
- // Simulate generation
27
- await new Promise((resolve) => setTimeout(resolve, 3000));
28
-
29
- const attachments: MediaAttachment[] = [];
30
-
31
- switch (request.type) {
32
- case "text_to_image":
33
- for (let i = 0; i < (request.options.maxResults || 1); i++) {
34
- attachments.push({
35
- id: `ai_img_${Date.now()}_${i}`,
36
- type: "image",
37
- position: "both",
38
- url: `https://picsum.photos/400/300?random=${Date.now() + i}`,
39
- filename: `ai_generated_${i}.jpg`,
40
- fileSize: 150000, // 150KB
41
- mimeType: "image/jpeg",
42
- isDownloaded: false,
43
- createdAt: new Date().toISOString(),
44
- });
45
- }
46
- break;
47
-
48
- case "text_to_audio":
49
- attachments.push({
50
- id: `ai_audio_${Date.now()}`,
51
- type: "audio",
52
- position: "back",
53
- url: `https://example.com/audio_${Date.now()}.mp3`,
54
- filename: `ai_generated_${Date.now()}.mp3`,
55
- fileSize: 80000, // 80KB
56
- mimeType: "audio/mp3",
57
- duration: 10, // 10 seconds
58
- isDownloaded: false,
59
- createdAt: new Date().toISOString(),
60
- });
61
- break;
62
- }
63
-
64
- const result: MediaGenerationResult = {
65
- success: true,
66
- attachments,
67
- creditsUsed: request.type === "text_to_image" ? 5 : 3,
68
- processingTime: 3000,
69
- requestId: `req_${Date.now()}`,
70
- };
71
-
72
- setGenerationResult(result);
73
- return result;
74
- } catch (err) {
75
- const errorMessage =
76
- err instanceof Error ? err.message : "Generation failed";
77
- setError(errorMessage);
78
- setIsGenerating(false);
79
-
80
- return {
81
- success: false,
82
- attachments: [],
83
- creditsUsed: 0,
84
- processingTime: 0,
85
- error: errorMessage,
86
- requestId: "",
87
- };
88
- } finally {
89
- setIsGenerating(false);
90
- }
91
- },
92
- [],
15
+ return useGenericMediaGeneration<MediaAttachment, MediaGenerationRequest>(
16
+ (baseAttachment) => baseAttachment as MediaAttachment
93
17
  );
94
-
95
- return {
96
- generateMedia,
97
- isGenerating,
98
- generationResult,
99
- error,
100
- };
101
18
  };
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import React, { useMemo } from 'react';
2
2
  import { View, StyleSheet, TouchableOpacity } from 'react-native';
3
3
  import { AtomicText } from '../../../atoms';
4
4
  import { AtomicIcon, useIconName } from '../../../atoms';
@@ -20,7 +20,7 @@ export const NavigationHeader: React.FC<NavigationHeaderProps> = ({
20
20
  const insets = useSafeAreaInsets();
21
21
  const arrowLeftIcon = useIconName('arrowLeft');
22
22
 
23
- const styles = StyleSheet.create({
23
+ const styles = useMemo(() => StyleSheet.create({
24
24
  container: {
25
25
  paddingTop: insets.top,
26
26
  paddingHorizontal: tokens.spacing.md,
@@ -45,7 +45,7 @@ export const NavigationHeader: React.FC<NavigationHeaderProps> = ({
45
45
  flex: 1,
46
46
  textAlign: 'left',
47
47
  },
48
- });
48
+ }), [tokens, insets]);
49
49
 
50
50
  return (
51
51
  <View style={styles.container}>
@@ -34,12 +34,30 @@ export const canGoBack = (): boolean => navigationRef?.canGoBack() ?? false;
34
34
 
35
35
  /**
36
36
  * Navigate to a specific route.
37
+ * Supports both simple and nested navigation.
37
38
  */
38
39
  export const navigate = (name: string, params?: object): void => {
39
40
  if (__DEV__) {
40
41
  }
41
42
  if (navigationRef?.isReady()) {
42
- navigationRef.navigate(name, params);
43
+ navigationRef.navigate(name as never, params as never);
44
+ }
45
+ };
46
+
47
+ /**
48
+ * Navigate to a nested screen in a specific navigator.
49
+ * @param navigatorName - The name of the parent navigator
50
+ * @param screenName - The name of the screen within that navigator
51
+ * @param params - Optional parameters to pass to the screen
52
+ */
53
+ export const navigateNested = (navigatorName: string, screenName: string, params?: object): void => {
54
+ if (__DEV__) {
55
+ }
56
+ if (navigationRef?.isReady()) {
57
+ navigationRef.navigate(navigatorName as never, {
58
+ screen: screenName,
59
+ params,
60
+ } as never);
43
61
  }
44
62
  };
45
63
 
@@ -130,6 +148,7 @@ export const AppNavigation = {
130
148
  isReady,
131
149
  canGoBack,
132
150
  navigate,
151
+ navigateNested,
133
152
  push,
134
153
  goBack,
135
154
  reset,
@@ -14,6 +14,7 @@ export type {
14
14
 
15
15
  // Store
16
16
  export { useOfflineStore } from './infrastructure/storage/OfflineStore';
17
+ export { useOfflineConfigStore } from './infrastructure/storage/OfflineConfigStore';
17
18
 
18
19
  // Hooks
19
20
  export { useOffline, configureOffline } from './presentation/hooks/useOffline';
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Offline Config Store
3
+ * Centralized configuration for offline functionality
4
+ * Replaces module-level mutable state with Zustand store
5
+ */
6
+
7
+ import { create } from 'zustand';
8
+ import type { OfflineConfig } from '../../types';
9
+
10
+ interface OfflineConfigStore {
11
+ config: OfflineConfig;
12
+ setConfig: (config: OfflineConfig) => void;
13
+ mergeConfig: (partialConfig: OfflineConfig) => void;
14
+ reset: () => void;
15
+ }
16
+
17
+ export const useOfflineConfigStore = create<OfflineConfigStore>((set) => ({
18
+ config: {},
19
+
20
+ setConfig: (config) => set({ config }),
21
+
22
+ mergeConfig: (partialConfig) => set((state) => ({
23
+ config: { ...state.config, ...partialConfig }
24
+ })),
25
+
26
+ reset: () => set({ config: {} }),
27
+ }));
28
+
29
+ /**
30
+ * Get current config (for non-React contexts)
31
+ */
32
+ export const getOfflineConfig = (): OfflineConfig => {
33
+ return useOfflineConfigStore.getState().config;
34
+ };
@@ -10,6 +10,7 @@ import type { NetworkState as ExpoNetworkState } from 'expo-network';
10
10
  import type { NetworkState, OfflineConfig } from '../../types';
11
11
  import { useOfflineStore } from '../../infrastructure/storage/OfflineStore';
12
12
  import { networkEvents } from '../../infrastructure/events/NetworkEvents';
13
+ import { useOfflineConfigStore } from '../../infrastructure/storage/OfflineConfigStore';
13
14
 
14
15
  /**
15
16
  * Convert expo-network state to our internal format
@@ -21,14 +22,17 @@ const toNetworkState = (state: ExpoNetworkState): NetworkState => ({
21
22
  details: null,
22
23
  });
23
24
 
24
- let globalConfig: OfflineConfig = {};
25
-
25
+ /**
26
+ * Configure offline settings globally
27
+ * This is a facade over the config store for backward compatibility
28
+ */
26
29
  export const configureOffline = (config: OfflineConfig): void => {
27
- globalConfig = config;
30
+ useOfflineConfigStore.getState().setConfig(config);
28
31
  };
29
32
 
30
33
  export const useOffline = (config?: OfflineConfig) => {
31
34
  const store = useOfflineStore();
35
+ const globalConfig = useOfflineConfigStore((state) => state.config);
32
36
  const isInitialized = useRef(false);
33
37
  const previousStateRef = useRef<NetworkState | null>(null);
34
38
  const isMountedRef = useRef(true);
@@ -36,7 +40,7 @@ export const useOffline = (config?: OfflineConfig) => {
36
40
  // Memoize merged config to prevent unnecessary effect re-runs
37
41
  const mergedConfig = useMemo(
38
42
  () => ({ ...globalConfig, ...config }),
39
- [config]
43
+ [globalConfig, config]
40
44
  );
41
45
 
42
46
  const handleNetworkStateChange = useCallback((state: ExpoNetworkState) => {