@umituz/react-native-design-system 4.25.94 → 4.25.95
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/AtomicFab.tsx +28 -32
- package/src/atoms/AtomicInput.tsx +11 -8
- package/src/atoms/AtomicProgress.tsx +7 -7
- package/src/atoms/AtomicSwitch.tsx +11 -7
- package/src/atoms/AtomicTouchable.tsx +10 -10
- package/src/atoms/chip/AtomicChip.tsx +13 -8
- package/src/atoms/image/AtomicImage.tsx +14 -10
- package/src/atoms/picker/components/PickerIcons.tsx +8 -2
- package/src/device/infrastructure/services/DeviceFeatureService.ts +4 -2
- package/src/device/infrastructure/services/PersistentDeviceIdService.ts +8 -2
- package/src/device/presentation/hooks/useDeviceFeatures.ts +8 -4
- package/src/image/infrastructure/services/ImageEnhanceService.ts +8 -5
- package/src/molecules/SearchBar/SearchBar.tsx +3 -3
- package/src/molecules/avatar/AvatarGroup.tsx +1 -1
- package/src/molecules/calendar/infrastructure/services/CalendarService.ts +2 -2
- package/src/molecules/filter-group/FilterGroup.tsx +3 -4
- package/src/onboarding/presentation/hooks/useOnboardingGestures.ts +3 -10
- package/src/storage/presentation/hooks/usePersistentCache.ts +1 -3
- package/src/storage/presentation/hooks/useStorageState.ts +27 -15
- package/src/tanstack/infrastructure/monitoring/DevMonitor.ts +2 -0
- package/src/theme/infrastructure/stores/themeStore.ts +12 -3
- package/src/timezone/infrastructure/utils/SimpleCache.ts +5 -0
- package/src/utils/hooks/useAsyncOperation.ts +4 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-design-system",
|
|
3
|
-
"version": "4.25.
|
|
3
|
+
"version": "4.25.95",
|
|
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": "./dist/index.d.ts",
|
package/src/atoms/AtomicFab.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
2
|
import { TouchableOpacity, StyleSheet } from 'react-native';
|
|
3
3
|
import { useAppDesignTokens } from '../theme';
|
|
4
4
|
import { useResponsive } from '../responsive';
|
|
@@ -57,45 +57,41 @@ export const AtomicFab: React.FC<AtomicFabProps> = ({
|
|
|
57
57
|
}) => {
|
|
58
58
|
const tokens = useAppDesignTokens();
|
|
59
59
|
const responsive = useResponsive();
|
|
60
|
-
const isDisabled = disabled;
|
|
61
60
|
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
const variants = getFabVariants(tokens);
|
|
65
|
-
const variantConfig = variants[variant as 'primary' | 'secondary' | 'surface'];
|
|
66
|
-
const baseIconSize = getFabIconSize(size as 'sm' | 'md' | 'lg');
|
|
61
|
+
const variantConfig = useMemo(() => getFabVariants(tokens)[variant as 'primary' | 'secondary' | 'surface'], [tokens, variant]);
|
|
62
|
+
const iconSize = useMemo(() => getFabIconSize(size as 'sm' | 'md' | 'lg') * tokens.spacingMultiplier, [size, tokens.spacingMultiplier]);
|
|
67
63
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
64
|
+
const fabStyle = useMemo(() => {
|
|
65
|
+
const baseSizeConfig = FAB_SIZES[size as 'sm' | 'md' | 'lg'];
|
|
66
|
+
const sizeConfig = {
|
|
67
|
+
width: baseSizeConfig.width * tokens.spacingMultiplier,
|
|
68
|
+
height: baseSizeConfig.height * tokens.spacingMultiplier,
|
|
69
|
+
borderRadius: baseSizeConfig.borderRadius * tokens.spacingMultiplier,
|
|
70
|
+
};
|
|
75
71
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
]);
|
|
72
|
+
return StyleSheet.flatten([
|
|
73
|
+
{
|
|
74
|
+
position: 'absolute' as const,
|
|
75
|
+
bottom: responsive.fabPosition.bottom,
|
|
76
|
+
right: responsive.fabPosition.right,
|
|
77
|
+
width: sizeConfig.width,
|
|
78
|
+
height: sizeConfig.height,
|
|
79
|
+
borderRadius: sizeConfig.borderRadius,
|
|
80
|
+
backgroundColor: variantConfig.backgroundColor,
|
|
81
|
+
alignItems: 'center' as const,
|
|
82
|
+
justifyContent: 'center' as const,
|
|
83
|
+
},
|
|
84
|
+
getFabBorder(tokens),
|
|
85
|
+
disabled ? { opacity: tokens.opacity.disabled } : undefined,
|
|
86
|
+
style,
|
|
87
|
+
]);
|
|
88
|
+
}, [size, tokens, responsive.fabPosition, variantConfig, disabled, style]);
|
|
93
89
|
|
|
94
90
|
return (
|
|
95
91
|
<TouchableOpacity
|
|
96
92
|
style={fabStyle}
|
|
97
93
|
onPress={onPress}
|
|
98
|
-
disabled={
|
|
94
|
+
disabled={disabled}
|
|
99
95
|
activeOpacity={activeOpacity}
|
|
100
96
|
testID={testID}
|
|
101
97
|
accessibilityLabel={accessibilityLabel || `${icon} floating action button`}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useCallback, useMemo } from 'react';
|
|
2
2
|
import { View, TextInput, StyleProp, ViewStyle, TextStyle } from 'react-native';
|
|
3
3
|
import { useAppDesignTokens } from '../theme';
|
|
4
4
|
import { useInputState } from './input/hooks/useInputState';
|
|
@@ -69,7 +69,7 @@ export const AtomicInput = React.forwardRef<React.ElementRef<typeof TextInput>,
|
|
|
69
69
|
const iconColor = isDisabled ? tokens.colors.textDisabled : tokens.colors.textSecondary;
|
|
70
70
|
const iconPadding = sizeConfig.iconSize + 8;
|
|
71
71
|
|
|
72
|
-
const containerStyle
|
|
72
|
+
const containerStyle = useMemo<StyleProp<ViewStyle>>(() => [
|
|
73
73
|
inputStyles.container,
|
|
74
74
|
variantStyle,
|
|
75
75
|
{
|
|
@@ -77,13 +77,16 @@ export const AtomicInput = React.forwardRef<React.ElementRef<typeof TextInput>,
|
|
|
77
77
|
paddingBottom: sizeConfig.paddingVertical,
|
|
78
78
|
paddingHorizontal: sizeConfig.paddingHorizontal,
|
|
79
79
|
minHeight: sizeConfig.minHeight,
|
|
80
|
-
justifyContent: 'center',
|
|
80
|
+
justifyContent: 'center' as const,
|
|
81
81
|
opacity: isDisabled ? 0.5 : 1,
|
|
82
82
|
},
|
|
83
83
|
style,
|
|
84
|
-
];
|
|
84
|
+
], [variantStyle, sizeConfig, isDisabled, style]);
|
|
85
85
|
|
|
86
|
-
const
|
|
86
|
+
const handleBlur = useCallback(() => { setIsFocused(false); onBlur?.(); }, [setIsFocused, onBlur]);
|
|
87
|
+
const handleFocus = useCallback(() => { setIsFocused(true); onFocus?.(); }, [setIsFocused, onFocus]);
|
|
88
|
+
|
|
89
|
+
const textInputStyle = useMemo<StyleProp<TextStyle>>(() => [
|
|
87
90
|
inputStyles.input,
|
|
88
91
|
{
|
|
89
92
|
fontSize: sizeConfig.fontSize,
|
|
@@ -94,7 +97,7 @@ export const AtomicInput = React.forwardRef<React.ElementRef<typeof TextInput>,
|
|
|
94
97
|
paddingRight: (trailingIcon || showPasswordToggle) ? iconPadding : undefined,
|
|
95
98
|
},
|
|
96
99
|
inputStyle,
|
|
97
|
-
];
|
|
100
|
+
], [sizeConfig, textColor, leadingIcon, trailingIcon, showPasswordToggle, iconPadding, inputStyle]);
|
|
98
101
|
|
|
99
102
|
return (
|
|
100
103
|
<View testID={testID}>
|
|
@@ -123,8 +126,8 @@ export const AtomicInput = React.forwardRef<React.ElementRef<typeof TextInput>,
|
|
|
123
126
|
numberOfLines={numberOfLines}
|
|
124
127
|
textContentType={textContentType}
|
|
125
128
|
style={textInputStyle}
|
|
126
|
-
onBlur={
|
|
127
|
-
onFocus={
|
|
129
|
+
onBlur={handleBlur}
|
|
130
|
+
onFocus={handleFocus}
|
|
128
131
|
testID={testID ? `${testID}-input` : undefined}
|
|
129
132
|
/>
|
|
130
133
|
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* - Form completion
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import React from 'react';
|
|
17
|
+
import React, { useMemo } from 'react';
|
|
18
18
|
import { View, StyleSheet, ViewStyle, DimensionValue, Text } from 'react-native';
|
|
19
19
|
import { useAppDesignTokens } from '../theme';
|
|
20
20
|
|
|
@@ -82,27 +82,27 @@ export const AtomicProgress: React.FC<AtomicProgressProps> = ({
|
|
|
82
82
|
// Border radius based on shape
|
|
83
83
|
const progressBorderRadius = shape === 'rounded' ? scaledHeight / 2 : 0;
|
|
84
84
|
|
|
85
|
-
const containerStyle
|
|
85
|
+
const containerStyle = useMemo<ViewStyle>(() => ({
|
|
86
86
|
width: width as DimensionValue,
|
|
87
87
|
height: scaledHeight,
|
|
88
88
|
backgroundColor: progressBackground,
|
|
89
89
|
borderRadius: progressBorderRadius,
|
|
90
90
|
overflow: 'hidden',
|
|
91
|
-
};
|
|
91
|
+
}), [width, scaledHeight, progressBackground, progressBorderRadius]);
|
|
92
92
|
|
|
93
|
-
const progressStyle
|
|
93
|
+
const progressStyle = useMemo<ViewStyle>(() => ({
|
|
94
94
|
width: progressWidth as DimensionValue,
|
|
95
95
|
height: '100%' as DimensionValue,
|
|
96
96
|
backgroundColor: progressColor,
|
|
97
97
|
borderRadius: progressBorderRadius,
|
|
98
|
-
};
|
|
98
|
+
}), [progressWidth, progressColor, progressBorderRadius]);
|
|
99
99
|
|
|
100
|
-
const textStyle = {
|
|
100
|
+
const textStyle = useMemo(() => ({
|
|
101
101
|
fontSize: tokens.typography.bodySmall.responsiveFontSize,
|
|
102
102
|
fontWeight: tokens.typography.labelMedium.fontWeight,
|
|
103
103
|
color: progressTextColor,
|
|
104
104
|
textAlign: 'center' as const,
|
|
105
|
-
};
|
|
105
|
+
}), [tokens.typography.bodySmall.responsiveFontSize, tokens.typography.labelMedium.fontWeight, progressTextColor]);
|
|
106
106
|
|
|
107
107
|
return (
|
|
108
108
|
<View
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Purpose: Boolean toggle across all apps
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import React from 'react';
|
|
8
|
+
import React, { useMemo } from 'react';
|
|
9
9
|
import { View, Switch, StyleSheet, ViewStyle } from 'react-native';
|
|
10
10
|
import { useAppDesignTokens } from '../theme';
|
|
11
11
|
import { AtomicText } from './AtomicText';
|
|
@@ -31,6 +31,10 @@ export const AtomicSwitch: React.FC<AtomicSwitchProps> = ({
|
|
|
31
31
|
}) => {
|
|
32
32
|
const tokens = useAppDesignTokens();
|
|
33
33
|
|
|
34
|
+
const labelStyle = useMemo(() => ({ color: tokens.colors.textPrimary }), [tokens.colors.textPrimary]);
|
|
35
|
+
const descriptionStyle = useMemo(() => ({ color: tokens.colors.textSecondary, marginTop: 2 }), [tokens.colors.textSecondary]);
|
|
36
|
+
const trackColor = useMemo(() => ({ false: tokens.colors.border, true: tokens.colors.primary }), [tokens.colors.border, tokens.colors.primary]);
|
|
37
|
+
|
|
34
38
|
return (
|
|
35
39
|
<View style={[styles.container, style]} testID={testID}>
|
|
36
40
|
<View style={styles.row}>
|
|
@@ -38,14 +42,14 @@ export const AtomicSwitch: React.FC<AtomicSwitchProps> = ({
|
|
|
38
42
|
<View style={styles.labelContainer}>
|
|
39
43
|
<AtomicText
|
|
40
44
|
type="bodyMedium"
|
|
41
|
-
style={
|
|
45
|
+
style={labelStyle}
|
|
42
46
|
>
|
|
43
47
|
{label}
|
|
44
48
|
</AtomicText>
|
|
45
49
|
{description && (
|
|
46
50
|
<AtomicText
|
|
47
51
|
type="bodySmall"
|
|
48
|
-
style={
|
|
52
|
+
style={descriptionStyle}
|
|
49
53
|
>
|
|
50
54
|
{description}
|
|
51
55
|
</AtomicText>
|
|
@@ -56,11 +60,11 @@ export const AtomicSwitch: React.FC<AtomicSwitchProps> = ({
|
|
|
56
60
|
value={value}
|
|
57
61
|
onValueChange={onValueChange}
|
|
58
62
|
disabled={disabled}
|
|
59
|
-
trackColor={
|
|
60
|
-
false: tokens.colors.border,
|
|
61
|
-
true: tokens.colors.primary,
|
|
62
|
-
}}
|
|
63
|
+
trackColor={trackColor}
|
|
63
64
|
thumbColor={value ? tokens.colors.onPrimary : tokens.colors.surface}
|
|
65
|
+
accessibilityRole="switch"
|
|
66
|
+
accessibilityLabel={label}
|
|
67
|
+
accessibilityState={{ checked: value, disabled }}
|
|
64
68
|
/>
|
|
65
69
|
</View>
|
|
66
70
|
</View>
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Purpose: Touchable wrapper across all apps
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import React, { useRef } from 'react';
|
|
8
|
+
import React, { useRef, useCallback } from 'react';
|
|
9
9
|
import { TouchableOpacity, ViewStyle, StyleProp } from 'react-native';
|
|
10
10
|
|
|
11
11
|
const DEBOUNCE_MS = 300;
|
|
@@ -47,19 +47,19 @@ const AtomicTouchableComponent: React.FC<AtomicTouchableProps> = ({
|
|
|
47
47
|
accessible = true,
|
|
48
48
|
}) => {
|
|
49
49
|
const lastPressRef = useRef(0);
|
|
50
|
+
const onPressRef = useRef(onPress);
|
|
51
|
+
onPressRef.current = onPress;
|
|
50
52
|
|
|
51
|
-
const handlePress =
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
: undefined;
|
|
53
|
+
const handlePress = useCallback(() => {
|
|
54
|
+
const now = Date.now();
|
|
55
|
+
if (now - lastPressRef.current < DEBOUNCE_MS) return;
|
|
56
|
+
lastPressRef.current = now;
|
|
57
|
+
onPressRef.current?.();
|
|
58
|
+
}, []);
|
|
59
59
|
|
|
60
60
|
return (
|
|
61
61
|
<TouchableOpacity
|
|
62
|
-
onPress={handlePress}
|
|
62
|
+
onPress={onPress ? handlePress : undefined}
|
|
63
63
|
onLongPress={onLongPress}
|
|
64
64
|
disabled={disabled}
|
|
65
65
|
activeOpacity={activeOpacity}
|
|
@@ -47,7 +47,7 @@ export const AtomicChip: React.FC<AtomicChipProps> = React.memo(({
|
|
|
47
47
|
const isDisabled = disabled || (!clickable && !onPress);
|
|
48
48
|
const opacity = isDisabled ? 0.5 : 1;
|
|
49
49
|
|
|
50
|
-
const chipStyle
|
|
50
|
+
const chipStyle = useMemo<ViewStyle>(() => ({
|
|
51
51
|
flexDirection: 'row',
|
|
52
52
|
alignItems: 'center',
|
|
53
53
|
justifyContent: 'center',
|
|
@@ -58,23 +58,28 @@ export const AtomicChip: React.FC<AtomicChipProps> = React.memo(({
|
|
|
58
58
|
...borderStyle,
|
|
59
59
|
borderColor: finalBorderColor,
|
|
60
60
|
...selectedStyle,
|
|
61
|
-
};
|
|
61
|
+
}), [sizeConfig, finalBackgroundColor, opacity, borderStyle, finalBorderColor, selectedStyle]);
|
|
62
62
|
|
|
63
|
-
const textStyle = {
|
|
63
|
+
const textStyle = useMemo(() => ({
|
|
64
64
|
fontSize: sizeConfig.fontSize,
|
|
65
65
|
fontWeight: tokens.typography.medium,
|
|
66
|
-
};
|
|
66
|
+
}), [sizeConfig.fontSize, tokens.typography.medium]);
|
|
67
67
|
|
|
68
68
|
const iconColor = finalTextColor;
|
|
69
69
|
|
|
70
|
-
const
|
|
71
|
-
const
|
|
70
|
+
const isInteractive = clickable && onPress && !disabled;
|
|
71
|
+
const Component = isInteractive ? TouchableOpacity : View;
|
|
72
|
+
const componentProps = isInteractive ? { onPress, activeOpacity } : {};
|
|
73
|
+
const accessibilityProps = isInteractive
|
|
74
|
+
? { accessibilityRole: 'button' as const, accessibilityState: { selected, disabled } }
|
|
75
|
+
: {};
|
|
72
76
|
|
|
73
77
|
return (
|
|
74
|
-
<Component
|
|
75
|
-
style={[chipStyle, style]}
|
|
78
|
+
<Component
|
|
79
|
+
style={[chipStyle, style]}
|
|
76
80
|
testID={testID}
|
|
77
81
|
{...componentProps}
|
|
82
|
+
{...accessibilityProps}
|
|
78
83
|
>
|
|
79
84
|
{leadingIcon && (
|
|
80
85
|
<AtomicIcon
|
|
@@ -19,6 +19,16 @@ export type AtomicImageProps = {
|
|
|
19
19
|
[key: string]: any;
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
+
const RESIZE_MODE_MAP: Record<string, 'cover' | 'contain' | 'stretch' | 'center'> = {
|
|
23
|
+
cover: 'cover',
|
|
24
|
+
contain: 'contain',
|
|
25
|
+
fill: 'stretch',
|
|
26
|
+
none: 'center',
|
|
27
|
+
'scale-down': 'contain',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const ROUNDED_STYLE = { borderRadius: 9999 };
|
|
31
|
+
|
|
22
32
|
export const AtomicImage: React.FC<AtomicImageProps> = ({
|
|
23
33
|
style,
|
|
24
34
|
rounded,
|
|
@@ -26,7 +36,7 @@ export const AtomicImage: React.FC<AtomicImageProps> = ({
|
|
|
26
36
|
cachePolicy,
|
|
27
37
|
...props
|
|
28
38
|
}) => {
|
|
29
|
-
const roundedStyle = rounded ?
|
|
39
|
+
const roundedStyle = rounded ? ROUNDED_STYLE : undefined;
|
|
30
40
|
|
|
31
41
|
if (ExpoImage) {
|
|
32
42
|
return (
|
|
@@ -34,23 +44,17 @@ export const AtomicImage: React.FC<AtomicImageProps> = ({
|
|
|
34
44
|
style={[style, roundedStyle]}
|
|
35
45
|
contentFit={contentFit}
|
|
36
46
|
cachePolicy={cachePolicy}
|
|
47
|
+
accessibilityRole="image"
|
|
37
48
|
{...props}
|
|
38
49
|
/>
|
|
39
50
|
);
|
|
40
51
|
}
|
|
41
52
|
|
|
42
|
-
// Fallback: React Native Image
|
|
43
|
-
const resizeModeMap: Record<string, 'cover' | 'contain' | 'stretch' | 'center'> = {
|
|
44
|
-
cover: 'cover',
|
|
45
|
-
contain: 'contain',
|
|
46
|
-
fill: 'stretch',
|
|
47
|
-
none: 'center',
|
|
48
|
-
'scale-down': 'contain',
|
|
49
|
-
};
|
|
50
53
|
return (
|
|
51
54
|
<RNImage
|
|
52
55
|
style={[style as StyleProp<ImageStyle>, roundedStyle]}
|
|
53
|
-
resizeMode={
|
|
56
|
+
resizeMode={RESIZE_MODE_MAP[contentFit] ?? 'cover'}
|
|
57
|
+
accessibilityRole="image"
|
|
54
58
|
{...props}
|
|
55
59
|
/>
|
|
56
60
|
);
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Renders clear button and dropdown icon for AtomicPicker
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import React from 'react';
|
|
6
|
+
import React, { useMemo } from 'react';
|
|
7
7
|
import { TouchableOpacity, View } from 'react-native';
|
|
8
8
|
import { useAppDesignTokens } from '../../../theme';
|
|
9
9
|
import { AtomicIcon, useIconName } from '../../icon';
|
|
@@ -33,8 +33,14 @@ export const PickerIcons: React.FC<PickerIconsProps> = ({
|
|
|
33
33
|
const chevronUpIcon = useIconName('chevronUp');
|
|
34
34
|
const chevronDownIcon = useIconName('chevronDown');
|
|
35
35
|
|
|
36
|
+
const containerStyle = useMemo(() => ({
|
|
37
|
+
flexDirection: 'row' as const,
|
|
38
|
+
alignItems: 'center' as const,
|
|
39
|
+
gap: tokens.spacing.xs,
|
|
40
|
+
}), [tokens.spacing.xs]);
|
|
41
|
+
|
|
36
42
|
return (
|
|
37
|
-
<View style={
|
|
43
|
+
<View style={containerStyle}>
|
|
38
44
|
{/* Clear Button */}
|
|
39
45
|
{clearable && selectedOptionsCount > 0 && !disabled && (
|
|
40
46
|
<TouchableOpacity
|
|
@@ -112,8 +112,10 @@ export class DeviceFeatureService {
|
|
|
112
112
|
const key = this.getStorageKey(deviceId, featureName);
|
|
113
113
|
try {
|
|
114
114
|
await storageRepository.setItem(key, usage);
|
|
115
|
-
} catch {
|
|
116
|
-
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (__DEV__) {
|
|
117
|
+
console.warn(`[DesignSystem] DeviceFeatureService: Failed to persist usage for "${featureName}"`, error);
|
|
118
|
+
}
|
|
117
119
|
}
|
|
118
120
|
}
|
|
119
121
|
|
|
@@ -99,14 +99,20 @@ export class PersistentDeviceIdService {
|
|
|
99
99
|
*/
|
|
100
100
|
static async clearStoredId(): Promise<void> {
|
|
101
101
|
try {
|
|
102
|
+
// Wait for pending initialization to prevent race condition
|
|
103
|
+
if (initializationPromise) {
|
|
104
|
+
await initializationPromise.catch(() => {});
|
|
105
|
+
}
|
|
102
106
|
await this.secureRepo.remove();
|
|
103
107
|
cachedDeviceId = null;
|
|
104
108
|
initializationPromise = null;
|
|
105
109
|
if (__DEV__) {
|
|
106
110
|
console.log('[DesignSystem] Device ID: Stored ID cleared');
|
|
107
111
|
}
|
|
108
|
-
} catch {
|
|
109
|
-
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (__DEV__) {
|
|
114
|
+
console.warn('[DesignSystem] Device ID: Failed to clear stored ID', error);
|
|
115
|
+
}
|
|
110
116
|
}
|
|
111
117
|
}
|
|
112
118
|
|
|
@@ -32,8 +32,10 @@ export function useDeviceFeatures(
|
|
|
32
32
|
try {
|
|
33
33
|
const result = await DeviceFeatureService.checkFeatureAccess(featureName);
|
|
34
34
|
setAccess(result);
|
|
35
|
-
} catch {
|
|
36
|
-
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (__DEV__) {
|
|
37
|
+
console.warn(`[DesignSystem] useDeviceFeatures: Failed to check access for "${featureName}"`, error);
|
|
38
|
+
}
|
|
37
39
|
}
|
|
38
40
|
}, [featureName]);
|
|
39
41
|
|
|
@@ -41,8 +43,10 @@ export function useDeviceFeatures(
|
|
|
41
43
|
try {
|
|
42
44
|
await DeviceFeatureService.incrementFeatureUsage(featureName);
|
|
43
45
|
await checkAccess();
|
|
44
|
-
} catch {
|
|
45
|
-
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (__DEV__) {
|
|
48
|
+
console.warn(`[DesignSystem] useDeviceFeatures: Failed to increment usage for "${featureName}"`, error);
|
|
49
|
+
}
|
|
46
50
|
}
|
|
47
51
|
}, [featureName, checkAccess]);
|
|
48
52
|
|
|
@@ -36,12 +36,15 @@ export class ImageEnhanceService {
|
|
|
36
36
|
throw ImageErrorHandler.createError(uriValidation.error!, IMAGE_ERROR_CODES.INVALID_URI, 'analyzeImage');
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
if (__DEV__) {
|
|
40
|
+
console.warn('[DesignSystem] ImageEnhanceService.analyzeImage: Returning placeholder metrics. Real image analysis is not yet implemented.');
|
|
41
|
+
}
|
|
39
42
|
return {
|
|
40
|
-
sharpness:
|
|
41
|
-
brightness:
|
|
42
|
-
contrast:
|
|
43
|
-
colorfulness:
|
|
44
|
-
overallQuality:
|
|
43
|
+
sharpness: 50,
|
|
44
|
+
brightness: 50,
|
|
45
|
+
contrast: 50,
|
|
46
|
+
colorfulness: 50,
|
|
47
|
+
overallQuality: 50,
|
|
45
48
|
};
|
|
46
49
|
} catch (error) {
|
|
47
50
|
throw ImageErrorHandler.handleUnknownError(error, 'analyzeImage');
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useCallback } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
View,
|
|
4
4
|
TextInput,
|
|
@@ -28,10 +28,10 @@ export const SearchBar: React.FC<SearchBarProps> = ({
|
|
|
28
28
|
const searchIcon = useIconName('search');
|
|
29
29
|
const closeCircleIcon = useIconName('closeCircle');
|
|
30
30
|
|
|
31
|
-
const handleClear = () => {
|
|
31
|
+
const handleClear = useCallback(() => {
|
|
32
32
|
onChangeText('');
|
|
33
33
|
onClear?.();
|
|
34
|
-
};
|
|
34
|
+
}, [onChangeText, onClear]);
|
|
35
35
|
|
|
36
36
|
const showClear = value.length > 0 && !loading;
|
|
37
37
|
|
|
@@ -60,7 +60,7 @@ export const AvatarGroup: React.FC<AvatarGroupProps> = ({
|
|
|
60
60
|
<View style={[styles.container, style]}>
|
|
61
61
|
{visibleItems.map((item, index) => (
|
|
62
62
|
<View
|
|
63
|
-
key={item.uri || item.name || item.icon}
|
|
63
|
+
key={item.uri || item.name || item.icon || `avatar-${index}`}
|
|
64
64
|
style={[
|
|
65
65
|
styles.avatarWrapper,
|
|
66
66
|
index > 0 && { marginLeft: spacing },
|
|
@@ -96,12 +96,12 @@ export class CalendarService {
|
|
|
96
96
|
/**
|
|
97
97
|
* Get weekday names
|
|
98
98
|
*/
|
|
99
|
-
static getWeekdayNames(): string[] {
|
|
99
|
+
static getWeekdayNames(locale?: string): string[] {
|
|
100
100
|
const weekdays: string[] = [];
|
|
101
101
|
for (let i = 0; i < 7; i++) {
|
|
102
102
|
const date = new Date();
|
|
103
103
|
date.setDate(date.getDate() - date.getDay() + i);
|
|
104
|
-
weekdays.push(date.toLocaleDateString(
|
|
104
|
+
weekdays.push(date.toLocaleDateString(locale ?? undefined, { weekday: 'short' }));
|
|
105
105
|
}
|
|
106
106
|
return weekdays;
|
|
107
107
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
import React from 'react';
|
|
2
|
+
import React, { useMemo } from 'react';
|
|
3
3
|
import { ScrollView, StyleSheet } from 'react-native';
|
|
4
4
|
import { AtomicChip } from '../../atoms/chip/AtomicChip';
|
|
5
5
|
import { useAppDesignTokens } from '../../theme';
|
|
@@ -16,7 +16,7 @@ export function FilterGroup<T = string>({
|
|
|
16
16
|
}: FilterGroupProps<T>) {
|
|
17
17
|
const tokens = useAppDesignTokens();
|
|
18
18
|
|
|
19
|
-
const styles = StyleSheet.create({
|
|
19
|
+
const styles = useMemo(() => StyleSheet.create({
|
|
20
20
|
container: {
|
|
21
21
|
flexGrow: 0,
|
|
22
22
|
},
|
|
@@ -26,9 +26,8 @@ export function FilterGroup<T = string>({
|
|
|
26
26
|
alignItems: 'center',
|
|
27
27
|
},
|
|
28
28
|
item: {
|
|
29
|
-
// Default styles if needed, though AtomicChip handles most
|
|
30
29
|
},
|
|
31
|
-
});
|
|
30
|
+
}), [tokens.spacing.md]);
|
|
32
31
|
|
|
33
32
|
return (
|
|
34
33
|
<ScrollView
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Handles swipe gestures for onboarding navigation
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { useRef
|
|
6
|
+
import { useRef } from "react";
|
|
7
7
|
import { PanResponder } from "react-native";
|
|
8
8
|
|
|
9
9
|
interface UseOnboardingGesturesProps {
|
|
@@ -27,15 +27,8 @@ export const useOnboardingGestures = ({
|
|
|
27
27
|
onBack,
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
// Update
|
|
31
|
-
|
|
32
|
-
latestPropsRef.current = {
|
|
33
|
-
isFirstSlide,
|
|
34
|
-
isAnswerValid,
|
|
35
|
-
onNext,
|
|
36
|
-
onBack,
|
|
37
|
-
};
|
|
38
|
-
});
|
|
30
|
+
// Update ref on every render to ensure PanResponder has fresh values
|
|
31
|
+
latestPropsRef.current = { isFirstSlide, isAnswerValid, onNext, onBack };
|
|
39
32
|
|
|
40
33
|
const panResponder = useRef(
|
|
41
34
|
PanResponder.create({
|
|
@@ -101,9 +101,7 @@ export function usePersistentCache<T>(
|
|
|
101
101
|
|
|
102
102
|
// Stabilize actions to prevent circular dependency
|
|
103
103
|
const stableActionsRef = useRef(actions);
|
|
104
|
-
|
|
105
|
-
stableActionsRef.current = actions;
|
|
106
|
-
});
|
|
104
|
+
stableActionsRef.current = actions;
|
|
107
105
|
|
|
108
106
|
const loadFromStorage = useCallback(async () => {
|
|
109
107
|
const currentActions = stableActionsRef.current;
|
|
@@ -5,11 +5,10 @@
|
|
|
5
5
|
* Combines React state with automatic storage persistence
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { useState, useCallback } from 'react';
|
|
8
|
+
import { useState, useCallback, useEffect, useRef } from 'react';
|
|
9
9
|
import { storageRepository } from '../../infrastructure/repositories/AsyncStorageRepository';
|
|
10
10
|
import { unwrap } from '../../domain/entities/StorageResult';
|
|
11
11
|
import type { StorageKey } from '../../domain/value-objects/StorageKey';
|
|
12
|
-
import { useAsyncOperation } from '../../../utils/hooks';
|
|
13
12
|
|
|
14
13
|
/**
|
|
15
14
|
* Storage State Hook
|
|
@@ -27,20 +26,33 @@ export const useStorageState = <T>(
|
|
|
27
26
|
): [T, (value: T) => Promise<void>, boolean] => {
|
|
28
27
|
const keyString = typeof key === 'string' ? key : String(key);
|
|
29
28
|
const [state, setState] = useState<T>(defaultValue);
|
|
29
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
30
|
+
const isMountedRef = useRef(true);
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
isMountedRef.current = true;
|
|
34
|
+
setIsLoading(true);
|
|
35
|
+
|
|
36
|
+
storageRepository
|
|
37
|
+
.getItem<T>(keyString, defaultValue)
|
|
38
|
+
.then((result) => {
|
|
39
|
+
if (isMountedRef.current) {
|
|
40
|
+
setState(unwrap(result, defaultValue));
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
.catch(() => {
|
|
44
|
+
// Keep defaultValue on error
|
|
45
|
+
})
|
|
46
|
+
.finally(() => {
|
|
47
|
+
if (isMountedRef.current) {
|
|
48
|
+
setIsLoading(false);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return () => {
|
|
53
|
+
isMountedRef.current = false;
|
|
54
|
+
};
|
|
55
|
+
}, [keyString]);
|
|
44
56
|
|
|
45
57
|
// Update state and persist to storage
|
|
46
58
|
const updateState = useCallback(
|
|
@@ -103,9 +103,18 @@ export const useTheme = createStore<ThemeState, ThemeActions>({
|
|
|
103
103
|
},
|
|
104
104
|
|
|
105
105
|
setCustomColors: async (colors?: CustomThemeColors) => {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
106
|
+
const { _updateInProgress } = get();
|
|
107
|
+
if (_updateInProgress) return;
|
|
108
|
+
set({ _updateInProgress: true, customColors: colors });
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
await ThemeStorage.setCustomColors(colors);
|
|
112
|
+
useDesignSystemTheme.getState().setCustomColors(colors);
|
|
113
|
+
} catch {
|
|
114
|
+
// Silent failure
|
|
115
|
+
} finally {
|
|
116
|
+
set({ _updateInProgress: false });
|
|
117
|
+
}
|
|
109
118
|
},
|
|
110
119
|
|
|
111
120
|
setDefaultColors: (colors: CustomThemeColors) => {
|
|
@@ -14,6 +14,7 @@ export class SimpleCache<T> {
|
|
|
14
14
|
private cache = new Map<string, CacheEntry<T>>();
|
|
15
15
|
private defaultTTL: number;
|
|
16
16
|
private cleanupTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
17
|
+
private destroyed = false;
|
|
17
18
|
|
|
18
19
|
constructor(defaultTTL: number = 60000) {
|
|
19
20
|
this.defaultTTL = defaultTTL;
|
|
@@ -24,6 +25,7 @@ export class SimpleCache<T> {
|
|
|
24
25
|
* Destroy the cache and stop cleanup timer
|
|
25
26
|
*/
|
|
26
27
|
destroy(): void {
|
|
28
|
+
this.destroyed = true;
|
|
27
29
|
if (this.cleanupTimeout) {
|
|
28
30
|
clearTimeout(this.cleanupTimeout);
|
|
29
31
|
this.cleanupTimeout = null;
|
|
@@ -32,6 +34,7 @@ export class SimpleCache<T> {
|
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
set(key: string, value: T, ttl?: number): void {
|
|
37
|
+
if (this.destroyed) return;
|
|
35
38
|
const expires = Date.now() + (ttl ?? this.defaultTTL);
|
|
36
39
|
this.cache.set(key, { value, expires });
|
|
37
40
|
}
|
|
@@ -73,6 +76,8 @@ export class SimpleCache<T> {
|
|
|
73
76
|
}
|
|
74
77
|
|
|
75
78
|
private scheduleCleanup(): void {
|
|
79
|
+
if (this.destroyed) return;
|
|
80
|
+
|
|
76
81
|
if (this.cleanupTimeout) {
|
|
77
82
|
clearTimeout(this.cleanupTimeout);
|
|
78
83
|
}
|
|
@@ -79,13 +79,10 @@ export function useAsyncOperation<T, E = Error>(
|
|
|
79
79
|
const operationRef = useRef(operation);
|
|
80
80
|
const errorHandlerRef = useRef(errorHandler);
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}, [onSuccess, onError, onFinally]);
|
|
87
|
-
|
|
88
|
-
// Keep operation and errorHandler in refs so execute doesn't need them as deps
|
|
82
|
+
// Keep all callback refs in sync with latest values
|
|
83
|
+
onSuccessRef.current = onSuccess;
|
|
84
|
+
onErrorRef.current = onError;
|
|
85
|
+
onFinallyRef.current = onFinally;
|
|
89
86
|
operationRef.current = operation;
|
|
90
87
|
errorHandlerRef.current = errorHandler;
|
|
91
88
|
|