@umituz/react-native-design-system 4.26.13 → 4.27.1
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/dist/molecules/ListItem.d.ts +1 -1
- package/dist/molecules/action-footer/ActionFooter.d.ts +1 -1
- package/dist/molecules/icon-grid/IconGrid.d.ts +1 -1
- package/dist/molecules/navigation/index.d.ts +78 -0
- package/dist/onboarding/presentation/components/OnboardingFooter.d.ts +1 -1
- package/dist/onboarding/presentation/components/OnboardingHeader.d.ts +1 -1
- package/dist/utils/math/CalculationUtils.d.ts +69 -0
- package/dist/utils/math/OpacityUtils.d.ts +31 -0
- package/dist/utils/math/ProgressUtils.d.ts +37 -0
- package/dist/utils/math/index.d.ts +6 -0
- package/package.json +1 -1
- package/src/device/detection/deviceDetection.ts +3 -1
- package/src/device/presentation/hooks/useAnonymousUser.ts +8 -13
- package/src/molecules/calendar/infrastructure/storage/CalendarStore.ts +14 -1
- package/src/molecules/navigation/hooks/useAppNavigation.ts +8 -3
- package/src/molecules/navigation/index.ts +85 -1
- package/src/onboarding/presentation/components/BackgroundImageCollage.tsx +3 -3
- package/src/onboarding/presentation/components/OnboardingFooter.tsx +1 -1
- package/src/storage/cache/domain/CacheManager.ts +5 -3
- package/src/storage/cache/presentation/useCachedValue.ts +6 -1
- package/src/storage/presentation/hooks/usePersistentCache.ts +17 -2
- package/src/storage/presentation/hooks/useStorageState.ts +13 -2
- package/src/tanstack/infrastructure/monitoring/DevMonitor.ts +0 -2
- package/src/theme/infrastructure/providers/DesignSystemProvider.tsx +2 -1
- package/src/theme/infrastructure/stores/themeStore.ts +22 -2
- package/src/timezone/infrastructure/utils/SimpleCache.ts +10 -4
- package/src/typography/presentation/utils/textColorUtils.ts +4 -1
- package/src/typography/presentation/utils/textStyleUtils.ts +2 -1
- package/src/utils/async/retryWithBackoff.ts +7 -5
- package/src/utils/constants/TimeConstants.ts +34 -0
- package/src/utils/errors/DesignSystemError.ts +3 -2
- package/src/utils/logger.ts +10 -4
|
@@ -8,14 +8,92 @@ export type { BottomTabScreenProps, BottomTabNavigationOptions, } from "@react-n
|
|
|
8
8
|
export { DEFAULT_FAB_CONFIG } from "./types";
|
|
9
9
|
export { NavigationCleanupManager } from "./utils/NavigationCleanup";
|
|
10
10
|
export type { NavigationCleanup } from "./utils/NavigationCleanup";
|
|
11
|
+
/**
|
|
12
|
+
* AppNavigation - Global navigation utility for programmatic navigation outside React components.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* import { AppNavigation } from '@umituz/react-native-design-system/molecules';
|
|
17
|
+
*
|
|
18
|
+
* // Navigate from a non-React context
|
|
19
|
+
* AppNavigation.navigate('ScreenName', { param: 'value' });
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
11
22
|
export { AppNavigation } from "./utils/AppNavigation";
|
|
12
23
|
export { TabLabel, type TabLabelProps } from "./components/TabLabel";
|
|
13
24
|
export * from "./components/NavigationHeader";
|
|
14
25
|
export { useTabBarStyles, type TabBarConfig } from "./hooks/useTabBarStyles";
|
|
15
26
|
export { useTabConfig, type UseTabConfigProps } from "./hooks/useTabConfig";
|
|
27
|
+
/**
|
|
28
|
+
* useAppNavigation - Standard navigation hook for all React Native packages.
|
|
29
|
+
*
|
|
30
|
+
* Provides a clean, type-safe navigation API that wraps React Navigation.
|
|
31
|
+
* Use this hook instead of @react-navigation/native's useNavigation for consistency.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* import { useAppNavigation } from '@umituz/react-native-design-system/molecules';
|
|
36
|
+
*
|
|
37
|
+
* function MyScreen() {
|
|
38
|
+
* const navigation = useAppNavigation();
|
|
39
|
+
*
|
|
40
|
+
* return (
|
|
41
|
+
* <Button onPress={() => navigation.navigate('Details', { id: 123 })}>
|
|
42
|
+
* Go to Details
|
|
43
|
+
* </Button>
|
|
44
|
+
* );
|
|
45
|
+
* }
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
16
48
|
export { useAppNavigation } from "./hooks/useAppNavigation";
|
|
49
|
+
export type { AppNavigationResult } from "./hooks/useAppNavigation";
|
|
50
|
+
/**
|
|
51
|
+
* useAppRoute - Hook to access current route parameters.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```typescript
|
|
55
|
+
* import { useAppRoute } from '@umituz/react-native-design-system/molecules';
|
|
56
|
+
*
|
|
57
|
+
* function DetailsScreen() {
|
|
58
|
+
* const route = useAppRoute<{ id: number }>();
|
|
59
|
+
* const id = route.params?.id;
|
|
60
|
+
* }
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
17
63
|
export { useAppRoute, type RouteProp } from "./hooks/useAppRoute";
|
|
64
|
+
/**
|
|
65
|
+
* useAppFocusEffect - Run effects when screen comes into focus.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```typescript
|
|
69
|
+
* import { useAppFocusEffect } from '@umituz/react-native-design-system/molecules';
|
|
70
|
+
* import { useCallback } from 'react';
|
|
71
|
+
*
|
|
72
|
+
* function ProfileScreen() {
|
|
73
|
+
* useAppFocusEffect(
|
|
74
|
+
* useCallback(() => {
|
|
75
|
+
* console.log('Screen focused');
|
|
76
|
+
* return () => console.log('Screen unfocused');
|
|
77
|
+
* }, [])
|
|
78
|
+
* );
|
|
79
|
+
* }
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
18
82
|
export { useAppFocusEffect } from "./hooks/useAppFocusEffect";
|
|
83
|
+
/**
|
|
84
|
+
* useAppIsFocused - Check if current screen is focused.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```typescript
|
|
88
|
+
* import { useAppIsFocused } from '@umituz/react-native-design-system/molecules';
|
|
89
|
+
*
|
|
90
|
+
* function VideoPlayer() {
|
|
91
|
+
* const isFocused = useAppIsFocused();
|
|
92
|
+
*
|
|
93
|
+
* return <Video playing={isFocused} />;
|
|
94
|
+
* }
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
19
97
|
export { useAppIsFocused } from "./hooks/useAppIsFocused";
|
|
20
98
|
export { createScreenOptions } from "./utils/createScreenOptions";
|
|
21
99
|
export type { ScreenOptionsParams } from "./utils/createScreenOptions";
|
|
@@ -9,4 +9,4 @@ export interface OnboardingFooterProps {
|
|
|
9
9
|
showProgressText?: boolean;
|
|
10
10
|
disabled?: boolean;
|
|
11
11
|
}
|
|
12
|
-
export declare const OnboardingFooter:
|
|
12
|
+
export declare const OnboardingFooter: React.NamedExoticComponent<OnboardingFooterProps>;
|
|
@@ -7,4 +7,4 @@ export interface OnboardingHeaderProps {
|
|
|
7
7
|
showSkipButton?: boolean;
|
|
8
8
|
skipButtonText?: string;
|
|
9
9
|
}
|
|
10
|
-
export declare const OnboardingHeader:
|
|
10
|
+
export declare const OnboardingHeader: React.NamedExoticComponent<OnboardingHeaderProps>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calculation Utilities
|
|
3
|
+
*
|
|
4
|
+
* Common mathematical calculations used throughout the app
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Clamps a number between a minimum and maximum value
|
|
8
|
+
* @param value - The value to clamp
|
|
9
|
+
* @param min - Minimum allowed value (default: 0)
|
|
10
|
+
* @param max - Maximum allowed value (default: 100)
|
|
11
|
+
* @returns Clamped value
|
|
12
|
+
*/
|
|
13
|
+
export declare function clamp(value: number, min?: number, max?: number): number;
|
|
14
|
+
/**
|
|
15
|
+
* Calculates progress percentage
|
|
16
|
+
* @param current - Current value
|
|
17
|
+
* @param total - Total value
|
|
18
|
+
* @returns Percentage (0-100)
|
|
19
|
+
*/
|
|
20
|
+
export declare function calculatePercentage(current: number, total: number): number;
|
|
21
|
+
/**
|
|
22
|
+
* Rounds a number to specified decimal places
|
|
23
|
+
* @param value - The value to round
|
|
24
|
+
* @param decimals - Number of decimal places (default: 0)
|
|
25
|
+
* @returns Rounded value
|
|
26
|
+
*/
|
|
27
|
+
export declare function roundTo(value: number, decimals?: number): number;
|
|
28
|
+
/**
|
|
29
|
+
* Converts intensity (0-100) to opacity (0-1)
|
|
30
|
+
* @param intensity - Intensity value (0-100)
|
|
31
|
+
* @param minOpacity - Minimum opacity (default: 0.05)
|
|
32
|
+
* @param maxOpacity - Maximum opacity (default: 0.95)
|
|
33
|
+
* @returns Opacity value (0-1)
|
|
34
|
+
*/
|
|
35
|
+
export declare function intensityToOpacity(intensity: number, minOpacity?: number, maxOpacity?: number): number;
|
|
36
|
+
/**
|
|
37
|
+
* Calculates grid item width based on container width and columns
|
|
38
|
+
* @param containerWidth - Total container width
|
|
39
|
+
* @param columns - Number of columns
|
|
40
|
+
* @param gap - Gap between items in pixels
|
|
41
|
+
* @returns Item width in pixels
|
|
42
|
+
*/
|
|
43
|
+
export declare function calculateGridItemWidth(containerWidth: number, columns: number, gap: number): number;
|
|
44
|
+
/**
|
|
45
|
+
* Checks if a value is within a range (inclusive)
|
|
46
|
+
* @param value - Value to check
|
|
47
|
+
* @param min - Range minimum
|
|
48
|
+
* @param max - Range maximum
|
|
49
|
+
* @returns True if value is in range
|
|
50
|
+
*/
|
|
51
|
+
export declare function isInRange(value: number, min: number, max: number): boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Linear interpolation between two values
|
|
54
|
+
* @param start - Start value
|
|
55
|
+
* @param end - End value
|
|
56
|
+
* @param progress - Progress (0-1)
|
|
57
|
+
* @returns Interpolated value
|
|
58
|
+
*/
|
|
59
|
+
export declare function lerp(start: number, end: number, progress: number): number;
|
|
60
|
+
/**
|
|
61
|
+
* Maps a value from one range to another
|
|
62
|
+
* @param value - Value to map
|
|
63
|
+
* @param inMin - Input range minimum
|
|
64
|
+
* @param inMax - Input range maximum
|
|
65
|
+
* @param outMin - Output range minimum
|
|
66
|
+
* @param outMax - Output range maximum
|
|
67
|
+
* @returns Mapped value
|
|
68
|
+
*/
|
|
69
|
+
export declare function mapRange(value: number, inMin: number, inMax: number, outMin: number, outMax: number): number;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opacity Utilities
|
|
3
|
+
*
|
|
4
|
+
* Helper functions for opacity-related calculations
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Converts intensity value to opacity for glassmorphism effects
|
|
8
|
+
* @param intensity - Intensity value (0-100)
|
|
9
|
+
* @param options - Configuration options
|
|
10
|
+
* @returns Opacity value (0-1)
|
|
11
|
+
*/
|
|
12
|
+
export declare function intensityToOpacity(intensity: number, options?: {
|
|
13
|
+
minOpacity?: number;
|
|
14
|
+
maxOpacity?: number;
|
|
15
|
+
invert?: boolean;
|
|
16
|
+
}): number;
|
|
17
|
+
/**
|
|
18
|
+
* Creates an RGBA color string with specified opacity
|
|
19
|
+
* @param rgb - RGB color as array [r, g, b]
|
|
20
|
+
* @param opacity - Opacity value (0-1)
|
|
21
|
+
* @returns RGBA color string
|
|
22
|
+
*/
|
|
23
|
+
export declare function createRgbaColor(rgb: [number, number, number], opacity: number): string;
|
|
24
|
+
/**
|
|
25
|
+
* Calculates opacity based on a ratio (0-1)
|
|
26
|
+
* @param ratio - Ratio value (0-1)
|
|
27
|
+
* @param minOpacity - Minimum opacity (default: 0.1)
|
|
28
|
+
* @param maxOpacity - Maximum opacity (default: 1)
|
|
29
|
+
* @returns Opacity value (0-1)
|
|
30
|
+
*/
|
|
31
|
+
export declare function ratioToOpacity(ratio: number, minOpacity?: number, maxOpacity?: number): number;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Progress Utilities
|
|
3
|
+
*
|
|
4
|
+
* Helper functions for progress-related calculations
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Validates and clamps a progress value to ensure it's within valid range
|
|
8
|
+
* @param value - Raw progress value
|
|
9
|
+
* @returns Clamped progress value (0-100)
|
|
10
|
+
*/
|
|
11
|
+
export declare function normalizeProgress(value: number): number;
|
|
12
|
+
/**
|
|
13
|
+
* Formats a progress value as a percentage string
|
|
14
|
+
* @param value - Progress value (0-100)
|
|
15
|
+
* @param decimals - Number of decimal places (default: 0)
|
|
16
|
+
* @returns Formatted percentage string
|
|
17
|
+
*/
|
|
18
|
+
export declare function formatPercentage(value: number, decimals?: number): string;
|
|
19
|
+
/**
|
|
20
|
+
* Calculates the percentage completed of a multi-step process
|
|
21
|
+
* @param currentStep - Current step (1-indexed)
|
|
22
|
+
* @param totalSteps - Total number of steps
|
|
23
|
+
* @returns Percentage (0-100)
|
|
24
|
+
*/
|
|
25
|
+
export declare function calculateStepProgress(currentStep: number, totalSteps: number): number;
|
|
26
|
+
/**
|
|
27
|
+
* Checks if progress is complete
|
|
28
|
+
* @param value - Progress value (0-100)
|
|
29
|
+
* @returns True if progress is 100%
|
|
30
|
+
*/
|
|
31
|
+
export declare function isComplete(value: number): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Checks if progress has started
|
|
34
|
+
* @param value - Progress value (0-100)
|
|
35
|
+
* @returns True if progress > 0%
|
|
36
|
+
*/
|
|
37
|
+
export declare function hasStarted(value: number): boolean;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Math Utilities Index
|
|
3
|
+
*/
|
|
4
|
+
export { clamp, calculatePercentage, roundTo, calculateGridItemWidth, isInRange, lerp, mapRange, } from './CalculationUtils';
|
|
5
|
+
export { normalizeProgress, formatPercentage, calculateStepProgress, isComplete, hasStarted, } from './ProgressUtils';
|
|
6
|
+
export { intensityToOpacity, createRgbaColor, ratioToOpacity, } from './OpacityUtils';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-design-system",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.27.1",
|
|
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 - TanStack persistence and expo-image-manipulator now lazy loaded",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -46,7 +46,9 @@ export const isTablet = (): boolean => {
|
|
|
46
46
|
return Device.deviceType === Device.DeviceType.TABLET;
|
|
47
47
|
}
|
|
48
48
|
// Fallback: Platform.isPad (iOS) or screen width >= 600dp (Android)
|
|
49
|
-
|
|
49
|
+
// Platform.isPad is not in React Native types but exists on iOS
|
|
50
|
+
const platformWithIsPad = Platform as { isPad?: boolean };
|
|
51
|
+
if (Platform.OS === 'ios' && platformWithIsPad.isPad) return true;
|
|
50
52
|
const { width, height } = getScreenDimensions();
|
|
51
53
|
return Math.min(width, height) >= 600;
|
|
52
54
|
};
|
|
@@ -46,7 +46,6 @@ export const useAnonymousUser = (
|
|
|
46
46
|
): UseAnonymousUserResult => {
|
|
47
47
|
const {
|
|
48
48
|
anonymousDisplayName = 'Anonymous',
|
|
49
|
-
fallbackUserId = 'anonymous_fallback',
|
|
50
49
|
} = options || {};
|
|
51
50
|
|
|
52
51
|
const { data: anonymousUser, isLoading, error, execute } = useAsyncOperation<AnonymousUser, string>(
|
|
@@ -56,9 +55,14 @@ export const useAnonymousUser = (
|
|
|
56
55
|
DeviceService.getUserFriendlyId(),
|
|
57
56
|
]);
|
|
58
57
|
|
|
58
|
+
// No fallback - if we can't get ID, let it error
|
|
59
|
+
if (!userId) {
|
|
60
|
+
throw new Error('Failed to generate device ID');
|
|
61
|
+
}
|
|
62
|
+
|
|
59
63
|
return {
|
|
60
|
-
userId
|
|
61
|
-
deviceName: deviceName
|
|
64
|
+
userId,
|
|
65
|
+
deviceName: deviceName ?? 'Device',
|
|
62
66
|
displayName: anonymousDisplayName,
|
|
63
67
|
isAnonymous: true,
|
|
64
68
|
};
|
|
@@ -66,16 +70,7 @@ export const useAnonymousUser = (
|
|
|
66
70
|
{
|
|
67
71
|
immediate: true,
|
|
68
72
|
initialData: null,
|
|
69
|
-
errorHandler: () =>
|
|
70
|
-
onError: () => {
|
|
71
|
-
// Fallback on error - set default anonymous user
|
|
72
|
-
return {
|
|
73
|
-
userId: fallbackUserId,
|
|
74
|
-
deviceName: 'Unknown Device',
|
|
75
|
-
displayName: anonymousDisplayName,
|
|
76
|
-
isAnonymous: true,
|
|
77
|
-
};
|
|
78
|
-
},
|
|
73
|
+
errorHandler: (err) => `Failed to generate device ID: ${err instanceof Error ? err.message : String(err)}`,
|
|
79
74
|
}
|
|
80
75
|
);
|
|
81
76
|
|
|
@@ -27,7 +27,13 @@ export const useCalendar = () => {
|
|
|
27
27
|
const view = useCalendarView();
|
|
28
28
|
|
|
29
29
|
// Utility functions - memoized to prevent recreating on every render
|
|
30
|
-
const getEventsForDate = useCallback((date: Date) => {
|
|
30
|
+
const getEventsForDate = useCallback((date: Date | null | undefined) => {
|
|
31
|
+
if (!date || !(date instanceof Date) || isNaN(date.getTime())) {
|
|
32
|
+
if (__DEV__) {
|
|
33
|
+
console.warn('[CalendarStore] getEventsForDate called with invalid date:', date);
|
|
34
|
+
}
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
31
37
|
return events.events.filter(event => {
|
|
32
38
|
const eventDate = new Date(event.date);
|
|
33
39
|
return eventDate.toDateString() === date.toDateString();
|
|
@@ -35,6 +41,13 @@ export const useCalendar = () => {
|
|
|
35
41
|
}, [events.events]);
|
|
36
42
|
|
|
37
43
|
const getEventsForMonth = useCallback((year: number, month: number) => {
|
|
44
|
+
if (typeof year !== 'number' || typeof month !== 'number' ||
|
|
45
|
+
isNaN(year) || isNaN(month) || month < 0 || month > 11) {
|
|
46
|
+
if (__DEV__) {
|
|
47
|
+
console.warn('[CalendarStore] getEventsForMonth called with invalid year/month:', { year, month });
|
|
48
|
+
}
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
38
51
|
return events.events.filter(event => {
|
|
39
52
|
const eventDate = new Date(event.date);
|
|
40
53
|
return eventDate.getFullYear() === year && eventDate.getMonth() === month;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useNavigation, StackActions } from "@react-navigation/native";
|
|
1
|
+
import { useNavigation, StackActions, CommonActions } from "@react-navigation/native";
|
|
2
2
|
import type { NavigationProp, ParamListBase } from "@react-navigation/native";
|
|
3
3
|
import { useCallback, useMemo } from "react";
|
|
4
4
|
|
|
@@ -31,8 +31,13 @@ export function useAppNavigation(): AppNavigationResult {
|
|
|
31
31
|
|
|
32
32
|
const navigate = useCallback(
|
|
33
33
|
(screen: string, params?: Record<string, unknown>) => {
|
|
34
|
-
// Dynamic navigation:
|
|
35
|
-
|
|
34
|
+
// Dynamic navigation: use CommonActions for type-safe arbitrary screen navigation
|
|
35
|
+
navigation.dispatch(
|
|
36
|
+
CommonActions.navigate({
|
|
37
|
+
name: screen,
|
|
38
|
+
params,
|
|
39
|
+
})
|
|
40
|
+
);
|
|
36
41
|
},
|
|
37
42
|
[navigation]
|
|
38
43
|
);
|
|
@@ -31,14 +31,98 @@ export { NavigationCleanupManager } from "./utils/NavigationCleanup";
|
|
|
31
31
|
export type { NavigationCleanup } from "./utils/NavigationCleanup";
|
|
32
32
|
|
|
33
33
|
// Navigation Utilities
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* AppNavigation - Global navigation utility for programmatic navigation outside React components.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```typescript
|
|
40
|
+
* import { AppNavigation } from '@umituz/react-native-design-system/molecules';
|
|
41
|
+
*
|
|
42
|
+
* // Navigate from a non-React context
|
|
43
|
+
* AppNavigation.navigate('ScreenName', { param: 'value' });
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
34
46
|
export { AppNavigation } from "./utils/AppNavigation";
|
|
47
|
+
|
|
35
48
|
export { TabLabel, type TabLabelProps } from "./components/TabLabel";
|
|
36
|
-
export
|
|
49
|
+
export { NavigationHeader, type NavigationHeaderProps } from "./components/NavigationHeader";
|
|
37
50
|
export { useTabBarStyles, type TabBarConfig } from "./hooks/useTabBarStyles";
|
|
38
51
|
export { useTabConfig, type UseTabConfigProps } from "./hooks/useTabConfig";
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* useAppNavigation - Standard navigation hook for all React Native packages.
|
|
55
|
+
*
|
|
56
|
+
* Provides a clean, type-safe navigation API that wraps React Navigation.
|
|
57
|
+
* Use this hook instead of @react-navigation/native's useNavigation for consistency.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```typescript
|
|
61
|
+
* import { useAppNavigation } from '@umituz/react-native-design-system/molecules';
|
|
62
|
+
*
|
|
63
|
+
* function MyScreen() {
|
|
64
|
+
* const navigation = useAppNavigation();
|
|
65
|
+
*
|
|
66
|
+
* return (
|
|
67
|
+
* <Button onPress={() => navigation.navigate('Details', { id: 123 })}>
|
|
68
|
+
* Go to Details
|
|
69
|
+
* </Button>
|
|
70
|
+
* );
|
|
71
|
+
* }
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
39
74
|
export { useAppNavigation } from "./hooks/useAppNavigation";
|
|
75
|
+
export type { AppNavigationResult } from "./hooks/useAppNavigation";
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* useAppRoute - Hook to access current route parameters.
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```typescript
|
|
82
|
+
* import { useAppRoute } from '@umituz/react-native-design-system/molecules';
|
|
83
|
+
*
|
|
84
|
+
* function DetailsScreen() {
|
|
85
|
+
* const route = useAppRoute<{ id: number }>();
|
|
86
|
+
* const id = route.params?.id;
|
|
87
|
+
* }
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
40
90
|
export { useAppRoute, type RouteProp } from "./hooks/useAppRoute";
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* useAppFocusEffect - Run effects when screen comes into focus.
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* ```typescript
|
|
97
|
+
* import { useAppFocusEffect } from '@umituz/react-native-design-system/molecules';
|
|
98
|
+
* import { useCallback } from 'react';
|
|
99
|
+
*
|
|
100
|
+
* function ProfileScreen() {
|
|
101
|
+
* useAppFocusEffect(
|
|
102
|
+
* useCallback(() => {
|
|
103
|
+
* console.log('Screen focused');
|
|
104
|
+
* return () => console.log('Screen unfocused');
|
|
105
|
+
* }, [])
|
|
106
|
+
* );
|
|
107
|
+
* }
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
41
110
|
export { useAppFocusEffect } from "./hooks/useAppFocusEffect";
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* useAppIsFocused - Check if current screen is focused.
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```typescript
|
|
117
|
+
* import { useAppIsFocused } from '@umituz/react-native-design-system/molecules';
|
|
118
|
+
*
|
|
119
|
+
* function VideoPlayer() {
|
|
120
|
+
* const isFocused = useAppIsFocused();
|
|
121
|
+
*
|
|
122
|
+
* return <Video playing={isFocused} />;
|
|
123
|
+
* }
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
42
126
|
export { useAppIsFocused } from "./hooks/useAppIsFocused";
|
|
43
127
|
|
|
44
128
|
// Screen Options
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import React, { useMemo } from "react";
|
|
8
|
-
import { View, Image as RNImage, StyleSheet } from "react-native";
|
|
8
|
+
import { View, Image as RNImage, StyleSheet, type ImageURISource, type ImageStyle } from "react-native";
|
|
9
9
|
import { useSafeAreaInsets } from "../../../safe-area/hooks/useSafeAreaInsets";
|
|
10
10
|
import {
|
|
11
11
|
generateGridLayout,
|
|
@@ -93,8 +93,8 @@ export const BackgroundImageCollage: React.FC<BackgroundImageCollageProps> = ({
|
|
|
93
93
|
return (
|
|
94
94
|
<RNImage
|
|
95
95
|
key={String(item.source)}
|
|
96
|
-
source={item.source as
|
|
97
|
-
style={item.style as
|
|
96
|
+
source={item.source as ImageURISource | number}
|
|
97
|
+
style={item.style as ImageStyle}
|
|
98
98
|
resizeMode="cover"
|
|
99
99
|
/>
|
|
100
100
|
);
|
|
@@ -52,7 +52,7 @@ export const OnboardingFooter = React.memo<OnboardingFooterProps>(({
|
|
|
52
52
|
const progressFillStyle = useMemo(
|
|
53
53
|
() => ({
|
|
54
54
|
...styles.progressFill,
|
|
55
|
-
width: `${progressPercent}%` as
|
|
55
|
+
width: `${progressPercent}%` as `${number}%`,
|
|
56
56
|
backgroundColor: colors.progressFillColor,
|
|
57
57
|
}),
|
|
58
58
|
[progressPercent, colors.progressFillColor]
|
|
@@ -20,10 +20,12 @@ export class CacheManager {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
getCache<T>(name: string, config?: CacheConfig): Cache<T> {
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
let cache = this.caches.get(name);
|
|
24
|
+
if (!cache) {
|
|
25
|
+
cache = new Cache<T>(config);
|
|
26
|
+
this.caches.set(name, cache);
|
|
25
27
|
}
|
|
26
|
-
return
|
|
28
|
+
return cache;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
deleteCache(name: string): boolean {
|
|
@@ -25,7 +25,12 @@ export function useCachedValue<T>(
|
|
|
25
25
|
return cached;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
const
|
|
28
|
+
const fetcherFn = fetcherRef.current;
|
|
29
|
+
if (!fetcherFn) {
|
|
30
|
+
throw new Error('Fetcher function is not defined');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const data = await fetcherFn();
|
|
29
34
|
cache.set(key, data, configRef.current?.ttl);
|
|
30
35
|
return data;
|
|
31
36
|
},
|
|
@@ -142,10 +142,25 @@ export function usePersistentCache<T>(
|
|
|
142
142
|
|
|
143
143
|
const setData = useCallback(
|
|
144
144
|
async (value: T) => {
|
|
145
|
-
|
|
145
|
+
// Optimistic update
|
|
146
|
+
const previousData = state.data;
|
|
146
147
|
stableActionsRef.current.setData(value);
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
await cacheOps.saveToStorage(key, value, { ttl, version, enabled });
|
|
151
|
+
} catch (error) {
|
|
152
|
+
// Rollback on error
|
|
153
|
+
if (previousData !== null) {
|
|
154
|
+
stableActionsRef.current.setData(previousData);
|
|
155
|
+
} else {
|
|
156
|
+
stableActionsRef.current.clearData();
|
|
157
|
+
}
|
|
158
|
+
if (__DEV__) {
|
|
159
|
+
console.warn('[usePersistentCache] Failed to save to storage, rolling back:', error);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
147
162
|
},
|
|
148
|
-
[key, ttl, version, enabled, cacheOps],
|
|
163
|
+
[key, ttl, version, enabled, cacheOps, state.data],
|
|
149
164
|
);
|
|
150
165
|
|
|
151
166
|
const clearData = useCallback(async () => {
|
|
@@ -57,10 +57,21 @@ export const useStorageState = <T>(
|
|
|
57
57
|
// Update state and persist to storage
|
|
58
58
|
const updateState = useCallback(
|
|
59
59
|
async (value: T) => {
|
|
60
|
+
// Optimistic update
|
|
61
|
+
const previousValue = state;
|
|
60
62
|
setState(value);
|
|
61
|
-
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
await storageRepository.setItem(keyString, value);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
// Rollback on error
|
|
68
|
+
setState(previousValue);
|
|
69
|
+
if (__DEV__) {
|
|
70
|
+
console.warn('[useStorageState] Failed to persist state, rolling back:', error);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
62
73
|
},
|
|
63
|
-
[keyString]
|
|
74
|
+
[keyString, state]
|
|
64
75
|
);
|
|
65
76
|
|
|
66
77
|
return [state, updateState, isLoading];
|
|
@@ -9,6 +9,7 @@ import type { CustomThemeColors } from '../../core/CustomColors';
|
|
|
9
9
|
import type { SplashScreenProps } from '../../../molecules/splash/types';
|
|
10
10
|
import { useIconStore } from '../../../atoms/icon/iconStore';
|
|
11
11
|
import type { IconRenderer, IconNames } from '../../../atoms/icon/iconStore';
|
|
12
|
+
import { FIVE_SECONDS_MS } from '../../../utils/constants/TimeConstants';
|
|
12
13
|
|
|
13
14
|
// Lazy load SplashScreen to avoid circular dependency
|
|
14
15
|
const SplashScreen = lazy(() => import('../../../molecules/splash').then(m => ({ default: m.SplashScreen })));
|
|
@@ -82,7 +83,7 @@ export const DesignSystemProvider: React.FC<DesignSystemProviderProps> = ({
|
|
|
82
83
|
if (!prev) onError?.(new Error('DesignSystemProvider initialization timed out'));
|
|
83
84
|
return true;
|
|
84
85
|
});
|
|
85
|
-
},
|
|
86
|
+
}, FIVE_SECONDS_MS);
|
|
86
87
|
|
|
87
88
|
initialize()
|
|
88
89
|
.then(() => {
|
|
@@ -12,6 +12,26 @@ import { useDesignSystemTheme } from '../globalThemeStore';
|
|
|
12
12
|
import type { ThemeMode } from '../../core/ColorPalette';
|
|
13
13
|
import type { CustomThemeColors } from '../../core/CustomColors';
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Shallow equality check for CustomThemeColors
|
|
17
|
+
* Compares all defined properties without deep object traversal
|
|
18
|
+
*/
|
|
19
|
+
function areCustomColorsEqual(a?: CustomThemeColors, b?: CustomThemeColors): boolean {
|
|
20
|
+
if (a === b) return true;
|
|
21
|
+
if (!a || !b) return false;
|
|
22
|
+
|
|
23
|
+
const keysA = Object.keys(a) as (keyof CustomThemeColors)[];
|
|
24
|
+
const keysB = Object.keys(b) as (keyof CustomThemeColors)[];
|
|
25
|
+
|
|
26
|
+
if (keysA.length !== keysB.length) return false;
|
|
27
|
+
|
|
28
|
+
for (const key of keysA) {
|
|
29
|
+
if (a[key] !== b[key]) return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
15
35
|
interface ThemeState {
|
|
16
36
|
theme: Theme;
|
|
17
37
|
themeMode: ThemeMode;
|
|
@@ -118,8 +138,8 @@ export const useTheme = createStore<ThemeState, ThemeActions>({
|
|
|
118
138
|
const { _updateInProgress, customColors: currentColors } = get();
|
|
119
139
|
if (_updateInProgress) return;
|
|
120
140
|
|
|
121
|
-
//
|
|
122
|
-
if (
|
|
141
|
+
// Shallow comparison to avoid redundant updates from new object references
|
|
142
|
+
if (areCustomColorsEqual(colors, currentColors)) return;
|
|
123
143
|
|
|
124
144
|
const updateId = Date.now();
|
|
125
145
|
set({ _updateInProgress: true, _lastUpdateId: updateId, customColors: colors });
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* No external dependencies - pure TypeScript implementation
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { ONE_MINUTE_MS } from '../../../utils/constants/TimeConstants';
|
|
9
|
+
|
|
8
10
|
interface CacheEntry<T> {
|
|
9
11
|
value: T;
|
|
10
12
|
expires: number;
|
|
@@ -17,7 +19,7 @@ export class SimpleCache<T> {
|
|
|
17
19
|
private destroyed = false;
|
|
18
20
|
private cleanupScheduleLock = false;
|
|
19
21
|
|
|
20
|
-
constructor(defaultTTL: number =
|
|
22
|
+
constructor(defaultTTL: number = ONE_MINUTE_MS) {
|
|
21
23
|
this.defaultTTL = defaultTTL;
|
|
22
24
|
this.scheduleCleanup();
|
|
23
25
|
}
|
|
@@ -91,9 +93,13 @@ export class SimpleCache<T> {
|
|
|
91
93
|
|
|
92
94
|
if (!this.destroyed) {
|
|
93
95
|
this.cleanupTimeout = setTimeout(() => {
|
|
94
|
-
this.
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
if (!this.destroyed) {
|
|
97
|
+
this.cleanupScheduleLock = false;
|
|
98
|
+
this.scheduleCleanup();
|
|
99
|
+
}
|
|
100
|
+
}, ONE_MINUTE_MS);
|
|
101
|
+
} else {
|
|
102
|
+
this.cleanupScheduleLock = false;
|
|
97
103
|
}
|
|
98
104
|
} catch (error) {
|
|
99
105
|
this.cleanupScheduleLock = false;
|
|
@@ -53,7 +53,10 @@ export function getTextColor(
|
|
|
53
53
|
|
|
54
54
|
const cacheKey = `${color}_${Object.keys(tokens.colors).length}_${tokens.colors.textPrimary}`;
|
|
55
55
|
|
|
56
|
-
if (colorCache.has(cacheKey))
|
|
56
|
+
if (colorCache.has(cacheKey)) {
|
|
57
|
+
const cached = colorCache.get(cacheKey);
|
|
58
|
+
if (cached) return cached;
|
|
59
|
+
}
|
|
57
60
|
|
|
58
61
|
const colorKey = COLOR_MAP[color as ColorVariant] ?? 'textPrimary';
|
|
59
62
|
const resolvedColor = tokens.colors[colorKey];
|
|
@@ -112,7 +112,8 @@ export function getTextStyle(
|
|
|
112
112
|
|
|
113
113
|
// Check cache first
|
|
114
114
|
if (typographyCache.has(cacheKey)) {
|
|
115
|
-
|
|
115
|
+
const cached = typographyCache.get(cacheKey);
|
|
116
|
+
if (cached) return cached;
|
|
116
117
|
}
|
|
117
118
|
|
|
118
119
|
// Resolve style and cache it
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* Useful for network requests, file operations, etc.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { DEFAULT_LONG_TIMEOUT_MS, DEFAULT_TIMEOUT_MS, ONE_MINUTE_MS, ONE_SECOND_MS, TEN_SECONDS_MS } from '../constants/TimeConstants';
|
|
9
|
+
|
|
8
10
|
export interface RetryOptions {
|
|
9
11
|
/**
|
|
10
12
|
* Maximum number of retry attempts
|
|
@@ -59,14 +61,14 @@ export async function retryWithBackoff<T>(
|
|
|
59
61
|
): Promise<T> {
|
|
60
62
|
const {
|
|
61
63
|
maxRetries = 3,
|
|
62
|
-
baseDelay =
|
|
63
|
-
maxDelay =
|
|
64
|
+
baseDelay = ONE_SECOND_MS,
|
|
65
|
+
maxDelay = TEN_SECONDS_MS,
|
|
64
66
|
backoffMultiplier = 2,
|
|
65
67
|
shouldRetry = () => true,
|
|
66
68
|
onRetry,
|
|
67
69
|
} = options;
|
|
68
70
|
|
|
69
|
-
let lastError: Error;
|
|
71
|
+
let lastError: Error | undefined;
|
|
70
72
|
|
|
71
73
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
72
74
|
try {
|
|
@@ -106,7 +108,7 @@ export async function retryWithBackoff<T>(
|
|
|
106
108
|
}
|
|
107
109
|
|
|
108
110
|
// This should never be reached, but TypeScript needs it
|
|
109
|
-
throw lastError
|
|
111
|
+
throw lastError ?? new Error('Retry operation failed with unknown error');
|
|
110
112
|
}
|
|
111
113
|
|
|
112
114
|
/**
|
|
@@ -125,7 +127,7 @@ export async function retryWithTimeout<T>(
|
|
|
125
127
|
fn: () => Promise<T>,
|
|
126
128
|
options: RetryOptions & { timeout?: number } = {}
|
|
127
129
|
): Promise<T> {
|
|
128
|
-
const { timeout =
|
|
130
|
+
const { timeout = DEFAULT_LONG_TIMEOUT_MS, ...retryOptions } = options;
|
|
129
131
|
|
|
130
132
|
return retryWithBackoff(
|
|
131
133
|
() => withTimeout(fn(), timeout),
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Time Constants
|
|
3
|
+
*
|
|
4
|
+
* Centralized time-related constants to replace magic numbers
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const MILLISECONDS_PER_SECOND = 1000;
|
|
8
|
+
export const MILLISECONDS_PER_MINUTE = 60 * 1000; // 60000
|
|
9
|
+
export const MILLISECONDS_PER_HOUR = 60 * 60 * 1000; // 3600000
|
|
10
|
+
export const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000; // 86400000
|
|
11
|
+
|
|
12
|
+
export const SECONDS_PER_MINUTE = 60;
|
|
13
|
+
export const SECONDS_PER_HOUR = 60 * 60;
|
|
14
|
+
export const SECONDS_PER_DAY = 24 * 60 * 60;
|
|
15
|
+
|
|
16
|
+
export const MINUTES_PER_HOUR = 60;
|
|
17
|
+
export const MINUTES_PER_DAY = 24 * 60;
|
|
18
|
+
|
|
19
|
+
// Common time intervals
|
|
20
|
+
export const ONE_SECOND_MS = MILLISECONDS_PER_SECOND;
|
|
21
|
+
export const FIVE_SECONDS_MS = 5 * MILLISECONDS_PER_SECOND;
|
|
22
|
+
export const TEN_SECONDS_MS = 10 * MILLISECONDS_PER_SECOND;
|
|
23
|
+
export const THIRTY_SECONDS_MS = 30 * MILLISECONDS_PER_SECOND;
|
|
24
|
+
export const ONE_MINUTE_MS = MILLISECONDS_PER_MINUTE;
|
|
25
|
+
export const FIVE_MINUTES_MS = 5 * MILLISECONDS_PER_MINUTE;
|
|
26
|
+
export const TEN_MINUTES_MS = 10 * MILLISECONDS_PER_MINUTE;
|
|
27
|
+
export const THIRTY_MINUTES_MS = 30 * MILLISECONDS_PER_MINUTE;
|
|
28
|
+
export const ONE_HOUR_MS = MILLISECONDS_PER_HOUR;
|
|
29
|
+
export const ONE_DAY_MS = MILLISECONDS_PER_DAY;
|
|
30
|
+
|
|
31
|
+
// Default timeouts
|
|
32
|
+
export const DEFAULT_TIMEOUT_MS = FIVE_SECONDS_MS;
|
|
33
|
+
export const DEFAULT_LONG_TIMEOUT_MS = THIRTY_SECONDS_MS;
|
|
34
|
+
export const DEFAULT_CACHE_TTL_MS = ONE_MINUTE_MS;
|
|
@@ -60,8 +60,9 @@ export class DesignSystemError extends Error {
|
|
|
60
60
|
this.retryable = metadata?.retryable ?? false;
|
|
61
61
|
|
|
62
62
|
// Maintains proper stack trace for where our error was thrown (only available on V8)
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
const ErrorConstructor = Error as { captureStackTrace?: (error: Error, constructor: typeof DesignSystemError) => void };
|
|
64
|
+
if (typeof ErrorConstructor.captureStackTrace === 'function') {
|
|
65
|
+
ErrorConstructor.captureStackTrace(this, DesignSystemError);
|
|
65
66
|
}
|
|
66
67
|
}
|
|
67
68
|
|
package/src/utils/logger.ts
CHANGED
|
@@ -100,8 +100,11 @@ class Logger {
|
|
|
100
100
|
* Use for performance measurements
|
|
101
101
|
*/
|
|
102
102
|
time(label: string): void {
|
|
103
|
-
if (this.isDev
|
|
104
|
-
|
|
103
|
+
if (this.isDev) {
|
|
104
|
+
const consoleWithTime = console as { time?: (label: string) => void };
|
|
105
|
+
if (typeof consoleWithTime.time === 'function') {
|
|
106
|
+
consoleWithTime.time(label);
|
|
107
|
+
}
|
|
105
108
|
}
|
|
106
109
|
}
|
|
107
110
|
|
|
@@ -109,8 +112,11 @@ class Logger {
|
|
|
109
112
|
* End time measurement - only in development
|
|
110
113
|
*/
|
|
111
114
|
timeEnd(label: string): void {
|
|
112
|
-
if (this.isDev
|
|
113
|
-
|
|
115
|
+
if (this.isDev) {
|
|
116
|
+
const consoleWithTimeEnd = console as { timeEnd?: (label: string) => void };
|
|
117
|
+
if (typeof consoleWithTimeEnd.timeEnd === 'function') {
|
|
118
|
+
consoleWithTimeEnd.timeEnd(label);
|
|
119
|
+
}
|
|
114
120
|
}
|
|
115
121
|
}
|
|
116
122
|
|