@umituz/react-native-design-system 4.25.93 → 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/core/colors/DarkColors.ts +93 -93
- package/src/theme/core/colors/LightColors.ts +95 -95
- 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(
|
|
@@ -1,74 +1,74 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* DARK THEME COLORS
|
|
3
|
-
*
|
|
4
|
-
* Dark theme color palette
|
|
3
|
+
*
|
|
4
|
+
* Dark theme color palette — Forest Green & Warm Orange (MoveLog theme)
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
export const darkColors = {
|
|
8
|
-
// PRIMARY BRAND COLORS -
|
|
9
|
-
primary: '#
|
|
10
|
-
primaryLight: '#
|
|
11
|
-
primaryDark: '#
|
|
12
|
-
|
|
13
|
-
secondary: '#
|
|
14
|
-
secondaryLight: '#
|
|
15
|
-
secondaryDark: '#
|
|
16
|
-
|
|
17
|
-
accent: '#
|
|
18
|
-
accentLight: '#
|
|
19
|
-
accentDark: '#
|
|
20
|
-
|
|
21
|
-
// MATERIAL DESIGN 3 - ON COLORS (Dark mode
|
|
22
|
-
onPrimary: '#000000',
|
|
23
|
-
onSecondary: '#000000',
|
|
24
|
-
onSuccess: '#000000',
|
|
25
|
-
onError: '#FFFFFF',
|
|
26
|
-
onWarning: '#000000',
|
|
27
|
-
onInfo: '#000000',
|
|
28
|
-
onSurface: '#E2E8F0',
|
|
29
|
-
onBackground: '#F1F5F9',
|
|
30
|
-
onSurfaceDisabled: '#
|
|
31
|
-
onSurfaceVariant: '#
|
|
32
|
-
|
|
33
|
-
// MATERIAL DESIGN 3 - CONTAINER COLORS (Dark mode
|
|
34
|
-
primaryContainer: '#
|
|
35
|
-
onPrimaryContainer: '#
|
|
36
|
-
secondaryContainer: '#
|
|
37
|
-
onSecondaryContainer: '#
|
|
38
|
-
errorContainer: '#7F1D1D',
|
|
39
|
-
onErrorContainer: '#FEE2E2',
|
|
40
|
-
|
|
41
|
-
// MATERIAL DESIGN 3 - OUTLINE (Dark mode
|
|
42
|
-
outline: '#
|
|
43
|
-
outlineVariant: '#
|
|
44
|
-
outlineDisabled: '#
|
|
8
|
+
// PRIMARY BRAND COLORS - Forest Green (lighter for dark backgrounds)
|
|
9
|
+
primary: '#5AAF7F', // Lighter Forest Green for dark mode
|
|
10
|
+
primaryLight: '#6BC48F', // Light Green
|
|
11
|
+
primaryDark: '#4A9B6F', // Forest Green
|
|
12
|
+
|
|
13
|
+
secondary: '#FF8C42', // Warm Orange
|
|
14
|
+
secondaryLight: '#FFA06A', // Light Orange
|
|
15
|
+
secondaryDark: '#E57030', // Dark Orange
|
|
16
|
+
|
|
17
|
+
accent: '#FF8C42', // Warm Orange
|
|
18
|
+
accentLight: '#FFA06A', // Light Orange
|
|
19
|
+
accentDark: '#E57030', // Dark Orange
|
|
20
|
+
|
|
21
|
+
// MATERIAL DESIGN 3 - ON COLORS (Dark mode)
|
|
22
|
+
onPrimary: '#000000',
|
|
23
|
+
onSecondary: '#000000',
|
|
24
|
+
onSuccess: '#000000',
|
|
25
|
+
onError: '#FFFFFF',
|
|
26
|
+
onWarning: '#000000',
|
|
27
|
+
onInfo: '#000000',
|
|
28
|
+
onSurface: '#E2E8F0',
|
|
29
|
+
onBackground: '#F1F5F9',
|
|
30
|
+
onSurfaceDisabled: '#6C757D',
|
|
31
|
+
onSurfaceVariant: '#98989D',
|
|
32
|
+
|
|
33
|
+
// MATERIAL DESIGN 3 - CONTAINER COLORS (Dark mode)
|
|
34
|
+
primaryContainer: '#2D6B4A', // Dark green container
|
|
35
|
+
onPrimaryContainer: '#D1F5E3', // Light green text
|
|
36
|
+
secondaryContainer: '#CC5500', // Dark orange container
|
|
37
|
+
onSecondaryContainer: '#FFE8D4', // Light text
|
|
38
|
+
errorContainer: '#7F1D1D',
|
|
39
|
+
onErrorContainer: '#FEE2E2',
|
|
40
|
+
|
|
41
|
+
// MATERIAL DESIGN 3 - OUTLINE (Dark mode)
|
|
42
|
+
outline: '#48484A',
|
|
43
|
+
outlineVariant: '#3A3A3C',
|
|
44
|
+
outlineDisabled: '#3A3A3C',
|
|
45
45
|
|
|
46
46
|
// SEMANTIC UI COLORS (slightly lighter for dark backgrounds)
|
|
47
|
-
success: '#
|
|
48
|
-
successLight: '#
|
|
49
|
-
successDark: '#
|
|
47
|
+
success: '#5AAF7F', // Lighter green for dark mode
|
|
48
|
+
successLight: '#6BC48F',
|
|
49
|
+
successDark: '#4A9B6F',
|
|
50
50
|
|
|
51
51
|
error: '#EF4444',
|
|
52
52
|
errorLight: '#F87171',
|
|
53
53
|
errorDark: '#DC2626',
|
|
54
54
|
|
|
55
|
-
warning: '#
|
|
56
|
-
warningLight: '#
|
|
57
|
-
warningDark: '#
|
|
55
|
+
warning: '#FFC107',
|
|
56
|
+
warningLight: '#FFD54F',
|
|
57
|
+
warningDark: '#E6A800',
|
|
58
58
|
|
|
59
|
-
info: '#FF8C42',
|
|
60
|
-
infoLight: '#
|
|
61
|
-
infoDark: '#
|
|
59
|
+
info: '#FF8C42',
|
|
60
|
+
infoLight: '#FFA06A',
|
|
61
|
+
infoDark: '#E57030',
|
|
62
62
|
|
|
63
|
-
// SEMANTIC CONTAINER COLORS
|
|
64
|
-
successContainer: '#
|
|
65
|
-
onSuccessContainer: '#
|
|
66
|
-
warningContainer: '#FEF3C7',
|
|
67
|
-
onWarningContainer: '#92400E',
|
|
68
|
-
infoContainer: '#
|
|
69
|
-
onInfoContainer: '#
|
|
63
|
+
// SEMANTIC CONTAINER COLORS
|
|
64
|
+
successContainer: '#D1F5E3',
|
|
65
|
+
onSuccessContainer: '#2D6B4A',
|
|
66
|
+
warningContainer: '#FEF3C7',
|
|
67
|
+
onWarningContainer: '#92400E',
|
|
68
|
+
infoContainer: '#FFE8D4',
|
|
69
|
+
onInfoContainer: '#CC5500',
|
|
70
70
|
|
|
71
|
-
// GRAYSCALE PALETTE
|
|
71
|
+
// GRAYSCALE PALETTE
|
|
72
72
|
gray50: '#FAFAFA',
|
|
73
73
|
gray100: '#F4F4F5',
|
|
74
74
|
gray200: '#E4E4E7',
|
|
@@ -80,65 +80,65 @@ export const darkColors = {
|
|
|
80
80
|
gray800: '#27272A',
|
|
81
81
|
gray900: '#18181B',
|
|
82
82
|
|
|
83
|
-
// BACKGROUND COLORS
|
|
84
|
-
backgroundPrimary: '#
|
|
85
|
-
backgroundSecondary: '#
|
|
83
|
+
// BACKGROUND COLORS — MoveLog's signature dark palette
|
|
84
|
+
backgroundPrimary: '#1C1C1E', // MoveLog dark root
|
|
85
|
+
backgroundSecondary: '#2C2C2E', // MoveLog dark default
|
|
86
86
|
|
|
87
|
-
surface: '#
|
|
88
|
-
surfaceVariant: '#
|
|
89
|
-
surfaceSecondary: '#
|
|
90
|
-
surfaceDisabled: '#
|
|
87
|
+
surface: '#2C2C2E', // MoveLog backgroundDefault
|
|
88
|
+
surfaceVariant: '#3A3A3C', // MoveLog backgroundSecondary
|
|
89
|
+
surfaceSecondary: '#3A3A3C',
|
|
90
|
+
surfaceDisabled: '#48484A', // MoveLog backgroundTertiary
|
|
91
91
|
|
|
92
|
-
// TEXT COLORS (dark mode
|
|
93
|
-
textPrimary: '#
|
|
94
|
-
textSecondary: '#
|
|
95
|
-
textTertiary: '#
|
|
96
|
-
textDisabled: '#
|
|
97
|
-
textInverse: '#
|
|
92
|
+
// TEXT COLORS (dark mode)
|
|
93
|
+
textPrimary: '#FFFFFF', // MoveLog dark text
|
|
94
|
+
textSecondary: '#98989D', // MoveLog dark textSecondary
|
|
95
|
+
textTertiary: '#6C757D',
|
|
96
|
+
textDisabled: '#48484A',
|
|
97
|
+
textInverse: '#1C1C1E',
|
|
98
98
|
|
|
99
|
-
// BORDER COLORS (dark mode
|
|
100
|
-
border: '#
|
|
101
|
-
borderLight: '#
|
|
102
|
-
borderMedium: '#
|
|
103
|
-
borderFocus: '#
|
|
104
|
-
borderDisabled: '#
|
|
99
|
+
// BORDER COLORS (dark mode)
|
|
100
|
+
border: '#48484A', // MoveLog dark border
|
|
101
|
+
borderLight: '#3A3A3C',
|
|
102
|
+
borderMedium: '#636366',
|
|
103
|
+
borderFocus: '#5AAF7F', // Green focus
|
|
104
|
+
borderDisabled: '#3A3A3C',
|
|
105
105
|
|
|
106
|
-
// COMPONENT-SPECIFIC COLORS (dark mode
|
|
107
|
-
buttonPrimary: '#
|
|
108
|
-
buttonSecondary: '#
|
|
106
|
+
// COMPONENT-SPECIFIC COLORS (dark mode)
|
|
107
|
+
buttonPrimary: '#5AAF7F',
|
|
108
|
+
buttonSecondary: '#FF8C42',
|
|
109
109
|
|
|
110
|
-
inputBackground: '#
|
|
111
|
-
inputBorder: '#
|
|
110
|
+
inputBackground: '#2C2C2E',
|
|
111
|
+
inputBorder: '#48484A',
|
|
112
112
|
|
|
113
|
-
cardBackground: '#
|
|
113
|
+
cardBackground: '#2C2C2E',
|
|
114
114
|
|
|
115
115
|
// COLOR ALIASES
|
|
116
|
-
text: '#
|
|
117
|
-
background: '#
|
|
118
|
-
card: '#
|
|
116
|
+
text: '#FFFFFF',
|
|
117
|
+
background: '#1C1C1E',
|
|
118
|
+
card: '#2C2C2E',
|
|
119
119
|
|
|
120
120
|
// SPECIAL COLORS
|
|
121
121
|
transparent: 'transparent',
|
|
122
122
|
black: '#000000',
|
|
123
123
|
white: '#FFFFFF',
|
|
124
124
|
|
|
125
|
-
// RGBA OVERLAY COLORS
|
|
126
|
-
modalOverlay: 'rgba(0, 0, 0, 0.
|
|
125
|
+
// RGBA OVERLAY COLORS
|
|
126
|
+
modalOverlay: 'rgba(0, 0, 0, 0.7)',
|
|
127
127
|
overlaySubtle: 'rgba(0, 0, 0, 0.05)',
|
|
128
128
|
overlayLight: 'rgba(0, 0, 0, 0.1)',
|
|
129
|
-
overlayMedium: 'rgba(0, 0, 0, 0.
|
|
130
|
-
overlayBackground: 'rgba(0, 0, 0, 0.
|
|
129
|
+
overlayMedium: 'rgba(0, 0, 0, 0.4)',
|
|
130
|
+
overlayBackground: 'rgba(0, 0, 0, 0.1)',
|
|
131
131
|
|
|
132
132
|
whiteOverlay: 'rgba(255, 255, 255, 0.2)',
|
|
133
133
|
whiteOverlayStrong: 'rgba(255, 255, 255, 0.95)',
|
|
134
|
-
whiteOverlayBorder: 'rgba(255, 255, 255, 0.
|
|
134
|
+
whiteOverlayBorder: 'rgba(255, 255, 255, 0.3)',
|
|
135
135
|
|
|
136
136
|
textWhiteOpacity: 'rgba(255, 255, 255, 0.8)',
|
|
137
137
|
|
|
138
138
|
errorBackground: 'rgba(239, 68, 68, 0.1)',
|
|
139
|
-
primaryBackground: 'rgba(
|
|
139
|
+
primaryBackground: 'rgba(90, 175, 127, 0.15)', // Green overlay (dark mode)
|
|
140
140
|
|
|
141
|
-
cardOverlay: 'rgba(0, 0, 0, 0.
|
|
141
|
+
cardOverlay: 'rgba(0, 0, 0, 0.25)',
|
|
142
142
|
|
|
143
|
-
inputBackground_RGBA: 'rgba(
|
|
144
|
-
};
|
|
143
|
+
inputBackground_RGBA: 'rgba(44, 44, 46, 0.9)',
|
|
144
|
+
};
|
|
@@ -1,128 +1,128 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* LIGHT THEME COLORS
|
|
3
|
-
*
|
|
4
|
-
* Light theme color palette
|
|
3
|
+
*
|
|
4
|
+
* Light theme color palette — Forest Green & Warm Orange (MoveLog theme)
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
export const lightColors = {
|
|
8
|
-
// PRIMARY BRAND COLORS -
|
|
9
|
-
primary: '#
|
|
10
|
-
primaryLight: '#
|
|
11
|
-
primaryDark: '#
|
|
12
|
-
|
|
13
|
-
secondary: '#
|
|
14
|
-
secondaryLight: '#
|
|
15
|
-
secondaryDark: '#
|
|
16
|
-
|
|
17
|
-
accent: '#
|
|
18
|
-
accentLight: '#
|
|
19
|
-
accentDark: '#
|
|
20
|
-
|
|
21
|
-
// MATERIAL DESIGN 3 - ON COLORS
|
|
22
|
-
onPrimary: '#FFFFFF',
|
|
23
|
-
onSecondary: '#FFFFFF',
|
|
24
|
-
onSuccess: '#FFFFFF',
|
|
25
|
-
onError: '#FFFFFF',
|
|
26
|
-
onWarning: '#000000',
|
|
27
|
-
onInfo: '#FFFFFF',
|
|
28
|
-
onSurface: '#
|
|
29
|
-
onBackground: '#
|
|
30
|
-
onSurfaceDisabled: '#
|
|
31
|
-
onSurfaceVariant: '#
|
|
32
|
-
|
|
33
|
-
// MATERIAL DESIGN 3 - CONTAINER COLORS
|
|
34
|
-
primaryContainer: '#
|
|
35
|
-
onPrimaryContainer: '#
|
|
36
|
-
secondaryContainer: '#
|
|
37
|
-
onSecondaryContainer: '#
|
|
38
|
-
errorContainer: '#FEE2E2',
|
|
39
|
-
onErrorContainer: '#991B1B',
|
|
8
|
+
// PRIMARY BRAND COLORS - Forest Green (MoveLog)
|
|
9
|
+
primary: '#4A9B6F', // Forest Green
|
|
10
|
+
primaryLight: '#6BC48F', // Light Green
|
|
11
|
+
primaryDark: '#3A7A5A', // Deep Green
|
|
12
|
+
|
|
13
|
+
secondary: '#FF8C42', // Warm Orange (accent)
|
|
14
|
+
secondaryLight: '#FFA06A', // Light Orange
|
|
15
|
+
secondaryDark: '#E57030', // Dark Orange
|
|
16
|
+
|
|
17
|
+
accent: '#FF8C42', // Warm Orange
|
|
18
|
+
accentLight: '#FFA06A', // Light Orange
|
|
19
|
+
accentDark: '#E57030', // Dark Orange
|
|
20
|
+
|
|
21
|
+
// MATERIAL DESIGN 3 - ON COLORS
|
|
22
|
+
onPrimary: '#FFFFFF',
|
|
23
|
+
onSecondary: '#FFFFFF',
|
|
24
|
+
onSuccess: '#FFFFFF',
|
|
25
|
+
onError: '#FFFFFF',
|
|
26
|
+
onWarning: '#000000',
|
|
27
|
+
onInfo: '#FFFFFF',
|
|
28
|
+
onSurface: '#1C1C1E',
|
|
29
|
+
onBackground: '#1C1C1E',
|
|
30
|
+
onSurfaceDisabled: '#C7C7CC',
|
|
31
|
+
onSurfaceVariant: '#6C757D',
|
|
32
|
+
|
|
33
|
+
// MATERIAL DESIGN 3 - CONTAINER COLORS
|
|
34
|
+
primaryContainer: '#D1F5E3', // Light green container
|
|
35
|
+
onPrimaryContainer: '#2D6B4A', // Dark green text
|
|
36
|
+
secondaryContainer: '#FFE8D4', // Light orange container
|
|
37
|
+
onSecondaryContainer: '#CC5500', // Dark orange text
|
|
38
|
+
errorContainer: '#FEE2E2',
|
|
39
|
+
onErrorContainer: '#991B1B',
|
|
40
40
|
|
|
41
41
|
// MATERIAL DESIGN 3 - OUTLINE
|
|
42
|
-
outline: '#
|
|
43
|
-
outlineVariant: '#
|
|
44
|
-
outlineDisabled: '#
|
|
42
|
+
outline: '#C7C7CC',
|
|
43
|
+
outlineVariant: '#E5E5EA',
|
|
44
|
+
outlineDisabled: '#E5E5EA',
|
|
45
45
|
|
|
46
46
|
// SEMANTIC UI COLORS
|
|
47
|
-
success: '#
|
|
48
|
-
successLight: '#
|
|
49
|
-
successDark: '#
|
|
50
|
-
|
|
51
|
-
error: '#
|
|
52
|
-
errorLight: '#
|
|
53
|
-
errorDark: '#
|
|
54
|
-
|
|
55
|
-
warning: '#
|
|
56
|
-
warningLight: '#
|
|
57
|
-
warningDark: '#
|
|
58
|
-
|
|
59
|
-
info: '#FF8C42',
|
|
60
|
-
infoLight: '#
|
|
61
|
-
infoDark: '#
|
|
62
|
-
|
|
63
|
-
// SEMANTIC CONTAINER COLORS
|
|
64
|
-
successContainer: '#
|
|
65
|
-
onSuccessContainer: '#
|
|
66
|
-
warningContainer: '#
|
|
67
|
-
onWarningContainer: '#92400E',
|
|
68
|
-
infoContainer: '#
|
|
69
|
-
onInfoContainer: '#
|
|
47
|
+
success: '#4A9B6F', // Forest Green (same as primary)
|
|
48
|
+
successLight: '#6BC48F',
|
|
49
|
+
successDark: '#3A7A5A',
|
|
50
|
+
|
|
51
|
+
error: '#DC3545',
|
|
52
|
+
errorLight: '#E87070',
|
|
53
|
+
errorDark: '#B02A37',
|
|
54
|
+
|
|
55
|
+
warning: '#FFC107',
|
|
56
|
+
warningLight: '#FFD54F',
|
|
57
|
+
warningDark: '#E6A800',
|
|
58
|
+
|
|
59
|
+
info: '#FF8C42', // Warm Orange for info
|
|
60
|
+
infoLight: '#FFA06A',
|
|
61
|
+
infoDark: '#E57030',
|
|
62
|
+
|
|
63
|
+
// SEMANTIC CONTAINER COLORS
|
|
64
|
+
successContainer: '#D1F5E3',
|
|
65
|
+
onSuccessContainer: '#2D6B4A',
|
|
66
|
+
warningContainer: '#FFF8DC',
|
|
67
|
+
onWarningContainer: '#92400E',
|
|
68
|
+
infoContainer: '#FFE8D4',
|
|
69
|
+
onInfoContainer: '#CC5500',
|
|
70
70
|
|
|
71
71
|
// GRAYSCALE PALETTE
|
|
72
72
|
gray50: '#FAFAFA',
|
|
73
73
|
gray100: '#F4F4F5',
|
|
74
|
-
gray200: '#
|
|
75
|
-
gray300: '#
|
|
76
|
-
gray400: '#
|
|
77
|
-
gray500: '#
|
|
78
|
-
gray600: '#
|
|
79
|
-
gray700: '#
|
|
80
|
-
gray800: '#
|
|
81
|
-
gray900: '#
|
|
82
|
-
|
|
83
|
-
// BACKGROUND COLORS
|
|
74
|
+
gray200: '#E5E5EA',
|
|
75
|
+
gray300: '#D1D1D6',
|
|
76
|
+
gray400: '#AEAEB2',
|
|
77
|
+
gray500: '#8E8E93',
|
|
78
|
+
gray600: '#636366',
|
|
79
|
+
gray700: '#48484A',
|
|
80
|
+
gray800: '#3A3A3C',
|
|
81
|
+
gray900: '#1C1C1E',
|
|
82
|
+
|
|
83
|
+
// BACKGROUND COLORS — clean iOS-style whites
|
|
84
84
|
backgroundPrimary: '#FFFFFF',
|
|
85
|
-
backgroundSecondary: '#
|
|
85
|
+
backgroundSecondary: '#F8F9FA',
|
|
86
86
|
|
|
87
87
|
surface: '#FFFFFF',
|
|
88
|
-
surfaceVariant: '#
|
|
89
|
-
surfaceSecondary: '#
|
|
90
|
-
surfaceDisabled: '#
|
|
88
|
+
surfaceVariant: '#F2F2F7',
|
|
89
|
+
surfaceSecondary: '#F2F2F7',
|
|
90
|
+
surfaceDisabled: '#F4F4F4',
|
|
91
91
|
|
|
92
92
|
// TEXT COLORS
|
|
93
|
-
textPrimary: '#
|
|
94
|
-
textSecondary: '#
|
|
95
|
-
textTertiary: '#
|
|
96
|
-
textDisabled: '#
|
|
93
|
+
textPrimary: '#1C1C1E',
|
|
94
|
+
textSecondary: '#6C757D',
|
|
95
|
+
textTertiary: '#98989D',
|
|
96
|
+
textDisabled: '#C7C7CC',
|
|
97
97
|
textInverse: '#FFFFFF',
|
|
98
98
|
|
|
99
99
|
// BORDER COLORS
|
|
100
|
-
border: '#
|
|
101
|
-
borderLight: '#
|
|
102
|
-
borderMedium: '#
|
|
103
|
-
borderFocus: '#
|
|
104
|
-
borderDisabled: '#
|
|
100
|
+
border: '#E5E5EA',
|
|
101
|
+
borderLight: '#F2F2F7',
|
|
102
|
+
borderMedium: '#C7C7CC',
|
|
103
|
+
borderFocus: '#4A9B6F',
|
|
104
|
+
borderDisabled: '#F2F2F7',
|
|
105
105
|
|
|
106
106
|
// COMPONENT-SPECIFIC COLORS
|
|
107
|
-
buttonPrimary: '#
|
|
108
|
-
buttonSecondary: '#
|
|
107
|
+
buttonPrimary: '#4A9B6F',
|
|
108
|
+
buttonSecondary: '#FF8C42',
|
|
109
109
|
|
|
110
110
|
inputBackground: '#FFFFFF',
|
|
111
|
-
inputBorder: '#
|
|
111
|
+
inputBorder: '#E5E5EA',
|
|
112
112
|
|
|
113
113
|
cardBackground: '#FFFFFF',
|
|
114
114
|
|
|
115
115
|
// COLOR ALIASES
|
|
116
|
-
text: '#
|
|
117
|
-
background: '#FFFFFF',
|
|
118
|
-
card: '#FFFFFF',
|
|
116
|
+
text: '#1C1C1E',
|
|
117
|
+
background: '#FFFFFF',
|
|
118
|
+
card: '#FFFFFF',
|
|
119
119
|
|
|
120
120
|
// SPECIAL COLORS
|
|
121
121
|
transparent: 'transparent',
|
|
122
122
|
black: '#000000',
|
|
123
123
|
white: '#FFFFFF',
|
|
124
124
|
|
|
125
|
-
// RGBA OVERLAY COLORS
|
|
125
|
+
// RGBA OVERLAY COLORS
|
|
126
126
|
modalOverlay: 'rgba(0, 0, 0, 0.5)',
|
|
127
127
|
overlaySubtle: 'rgba(0, 0, 0, 0.05)',
|
|
128
128
|
overlayLight: 'rgba(0, 0, 0, 0.1)',
|
|
@@ -135,10 +135,10 @@ export const lightColors = {
|
|
|
135
135
|
|
|
136
136
|
textWhiteOpacity: 'rgba(255, 255, 255, 0.8)',
|
|
137
137
|
|
|
138
|
-
errorBackground: 'rgba(
|
|
139
|
-
primaryBackground: 'rgba(
|
|
138
|
+
errorBackground: 'rgba(220, 53, 69, 0.1)',
|
|
139
|
+
primaryBackground: 'rgba(74, 155, 111, 0.1)', // Green background
|
|
140
140
|
|
|
141
|
-
cardOverlay: 'rgba(0, 0, 0, 0.
|
|
141
|
+
cardOverlay: 'rgba(0, 0, 0, 0.08)',
|
|
142
142
|
|
|
143
|
-
inputBackground_RGBA: 'rgba(248,
|
|
144
|
-
};
|
|
143
|
+
inputBackground_RGBA: 'rgba(248, 249, 250, 0.9)',
|
|
144
|
+
};
|
|
@@ -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
|
|