@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.
Files changed (26) hide show
  1. package/package.json +1 -1
  2. package/src/atoms/AtomicFab.tsx +28 -32
  3. package/src/atoms/AtomicInput.tsx +11 -8
  4. package/src/atoms/AtomicProgress.tsx +7 -7
  5. package/src/atoms/AtomicSwitch.tsx +11 -7
  6. package/src/atoms/AtomicTouchable.tsx +10 -10
  7. package/src/atoms/chip/AtomicChip.tsx +13 -8
  8. package/src/atoms/image/AtomicImage.tsx +14 -10
  9. package/src/atoms/picker/components/PickerIcons.tsx +8 -2
  10. package/src/device/infrastructure/services/DeviceFeatureService.ts +4 -2
  11. package/src/device/infrastructure/services/PersistentDeviceIdService.ts +8 -2
  12. package/src/device/presentation/hooks/useDeviceFeatures.ts +8 -4
  13. package/src/image/infrastructure/services/ImageEnhanceService.ts +8 -5
  14. package/src/molecules/SearchBar/SearchBar.tsx +3 -3
  15. package/src/molecules/avatar/AvatarGroup.tsx +1 -1
  16. package/src/molecules/calendar/infrastructure/services/CalendarService.ts +2 -2
  17. package/src/molecules/filter-group/FilterGroup.tsx +3 -4
  18. package/src/onboarding/presentation/hooks/useOnboardingGestures.ts +3 -10
  19. package/src/storage/presentation/hooks/usePersistentCache.ts +1 -3
  20. package/src/storage/presentation/hooks/useStorageState.ts +27 -15
  21. package/src/tanstack/infrastructure/monitoring/DevMonitor.ts +2 -0
  22. package/src/theme/core/colors/DarkColors.ts +93 -93
  23. package/src/theme/core/colors/LightColors.ts +95 -95
  24. package/src/theme/infrastructure/stores/themeStore.ts +12 -3
  25. package/src/timezone/infrastructure/utils/SimpleCache.ts +5 -0
  26. 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.93",
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",
@@ -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
- // Get configurations
63
- const baseSizeConfig = FAB_SIZES[size as 'sm' | 'md' | 'lg'];
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
- // Scale dimensions
69
- const sizeConfig = {
70
- width: baseSizeConfig.width * tokens.spacingMultiplier,
71
- height: baseSizeConfig.height * tokens.spacingMultiplier,
72
- borderRadius: baseSizeConfig.borderRadius * tokens.spacingMultiplier,
73
- };
74
- const iconSize = baseIconSize * tokens.spacingMultiplier;
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
- // Combine styles
77
- const fabStyle = StyleSheet.flatten([
78
- {
79
- position: 'absolute' as const,
80
- bottom: responsive.fabPosition.bottom,
81
- right: responsive.fabPosition.right,
82
- width: sizeConfig.width,
83
- height: sizeConfig.height,
84
- borderRadius: sizeConfig.borderRadius,
85
- backgroundColor: variantConfig.backgroundColor,
86
- alignItems: 'center' as const,
87
- justifyContent: 'center' as const,
88
- },
89
- getFabBorder(tokens),
90
- isDisabled ? { opacity: tokens.opacity.disabled } : undefined,
91
- style, // Custom style override
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={isDisabled}
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: StyleProp<ViewStyle> = [
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 textInputStyle: StyleProp<TextStyle> = [
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={() => { setIsFocused(false); onBlur?.(); }}
127
- onFocus={() => { setIsFocused(true); 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: ViewStyle = {
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: ViewStyle = {
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={{ color: tokens.colors.textPrimary }}
45
+ style={labelStyle}
42
46
  >
43
47
  {label}
44
48
  </AtomicText>
45
49
  {description && (
46
50
  <AtomicText
47
51
  type="bodySmall"
48
- style={{ color: tokens.colors.textSecondary, marginTop: 2 }}
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 = onPress
52
- ? () => {
53
- const now = Date.now();
54
- if (now - lastPressRef.current < DEBOUNCE_MS) return;
55
- lastPressRef.current = now;
56
- onPress();
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: ViewStyle = {
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 Component = (clickable && onPress && !disabled) ? TouchableOpacity : View;
71
- const componentProps = (clickable && onPress && !disabled) ? { onPress, activeOpacity } : {};
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 ? { borderRadius: 9999 } : undefined;
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={resizeModeMap[contentFit] ?? 'cover'}
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={{ flexDirection: 'row', alignItems: 'center', gap: tokens.spacing.xs }}>
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
- // Silent fail
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
- // Silent fail
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
- // Silent fail
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
- // Silent fail
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: Math.random() * 100,
41
- brightness: Math.random() * 100,
42
- contrast: Math.random() * 100,
43
- colorfulness: Math.random() * 100,
44
- overallQuality: Math.random() * 100,
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('en-US', { weekday: 'short' }));
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, useEffect } from "react";
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 refs on every render to ensure PanResponder has fresh values
31
- useEffect(() => {
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
- useEffect(() => {
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
- // Load initial value from storage
32
- const { data, isLoading } = useAsyncOperation<T, Error>(
33
- async () => {
34
- const result = await storageRepository.getItem(keyString, defaultValue);
35
- return unwrap(result, defaultValue);
36
- },
37
- {
38
- immediate: true,
39
- initialData: defaultValue,
40
- errorHandler: (err) => err as Error,
41
- onSuccess: (value) => setState(value),
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(
@@ -95,6 +95,8 @@ class DevMonitorClass {
95
95
  this.cacheSubscription = null;
96
96
  }
97
97
 
98
+ this.stopStatsLogging();
99
+ this.metrics.clear();
98
100
  this.queryClient = null;
99
101
  }
100
102
 
@@ -1,74 +1,74 @@
1
1
  /**
2
2
  * DARK THEME COLORS
3
- *
4
- * Dark theme color palette with warm orange harmony
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 - Warm Orange & Harmony Bloom (Dark Mode)
9
- primary: '#FF8C42', // Warm Orange for dark backgrounds
10
- primaryLight: '#FFA07A', // Light Salmon
11
- primaryDark: '#FF6B35', // Vibrant Orange
12
-
13
- secondary: '#FFCC99', // Light Peach for dark backgrounds
14
- secondaryLight: '#FFD4A3', // Warm Beige
15
- secondaryDark: '#FFB88C', // Soft Peach
16
-
17
- accent: '#FFB6C1', // Light Pink (Bloom) for dark backgrounds
18
- accentLight: '#FFA07A', // Light Salmon
19
- accentDark: '#FF8C69', // Salmon
20
-
21
- // MATERIAL DESIGN 3 - ON COLORS (Dark mode text colors)
22
- onPrimary: '#000000', // Dark text on light primary
23
- onSecondary: '#000000', // Dark text on light secondary
24
- onSuccess: '#000000', // Dark text on light success
25
- onError: '#FFFFFF', // Light text on dark error
26
- onWarning: '#000000', // Dark text on light warning
27
- onInfo: '#000000', // Dark text on light info
28
- onSurface: '#E2E8F0', // Light text on dark surface
29
- onBackground: '#F1F5F9', // Light text on dark background
30
- onSurfaceDisabled: '#64748B', // Disabled dark mode text
31
- onSurfaceVariant: '#CBD5E1', // Text on dark surface variant
32
-
33
- // MATERIAL DESIGN 3 - CONTAINER COLORS (Dark mode containers)
34
- primaryContainer: '#CC4A1F', // Dark orange container
35
- onPrimaryContainer: '#FFE4CD', // Light text on dark primary container
36
- secondaryContainer: '#CC8C5F', // Dark peach container
37
- onSecondaryContainer: '#FFF8DC', // Light text on dark secondary container
38
- errorContainer: '#7F1D1D', // Dark red container
39
- onErrorContainer: '#FEE2E2', // Light text on dark error container
40
-
41
- // MATERIAL DESIGN 3 - OUTLINE (Dark mode outlines)
42
- outline: '#475569', // Medium gray outline for dark mode
43
- outlineVariant: '#334155', // Darker outline variant
44
- outlineDisabled: '#334155', // Disabled outline
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: '#34D399', // Lighter green for dark mode
48
- successLight: '#34D399',
49
- successDark: '#059669',
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: '#F59E0B',
56
- warningLight: '#FBBF24',
57
- warningDark: '#D97706',
55
+ warning: '#FFC107',
56
+ warningLight: '#FFD54F',
57
+ warningDark: '#E6A800',
58
58
 
59
- info: '#FF8C42', // Warm Orange for info (dark mode)
60
- infoLight: '#FFA07A', // Light Salmon
61
- infoDark: '#FF6347', // Tomato
59
+ info: '#FF8C42',
60
+ infoLight: '#FFA06A',
61
+ infoDark: '#E57030',
62
62
 
63
- // SEMANTIC CONTAINER COLORS (Same as light mode for type consistency)
64
- successContainer: '#D1FAE5', // Same as light mode for type consistency
65
- onSuccessContainer: '#065F46', // Same as light mode for type consistency
66
- warningContainer: '#FEF3C7', // Same as light mode for type consistency
67
- onWarningContainer: '#92400E', // Same as light mode for type consistency
68
- infoContainer: '#FFE4CD', // Light orange container
69
- onInfoContainer: '#CC4A1F', // Text on info container
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 (Same as light mode for type consistency)
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 (dark mode - true dark backgrounds)
84
- backgroundPrimary: '#0F172A', // Slate 900 - Deep dark background
85
- backgroundSecondary: '#1E293B', // Slate 800 - Slightly lighter
83
+ // BACKGROUND COLORS MoveLog's signature dark palette
84
+ backgroundPrimary: '#1C1C1E', // MoveLog dark root
85
+ backgroundSecondary: '#2C2C2E', // MoveLog dark default
86
86
 
87
- surface: '#1E293B', // Slate 800 - Card/surface backgrounds
88
- surfaceVariant: '#334155', // Slate 700 - Variant surfaces
89
- surfaceSecondary: '#334155', // Alias for surfaceVariant
90
- surfaceDisabled: '#475569', // Slate 600 - Disabled surfaces
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 - light text on dark backgrounds)
93
- textPrimary: '#F1F5F9', // Slate 100 - Primary text (very light)
94
- textSecondary: '#CBD5E1', // Slate 300 - Secondary text
95
- textTertiary: '#94A3B8', // Slate 400 - Tertiary text
96
- textDisabled: '#64748B', // Slate 500 - Disabled text
97
- textInverse: '#0F172A', // Dark text for light backgrounds
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 - subtle borders)
100
- border: '#334155', // Slate 700 - Default border
101
- borderLight: '#475569', // Slate 600 - Light border
102
- borderMedium: '#64748B', // Slate 500 - Medium border
103
- borderFocus: '#60A5FA', // Blue 400 - Focus border (lighter)
104
- borderDisabled: '#475569', // Slate 600 - Disabled border
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 specific)
107
- buttonPrimary: '#FF8C42', // Warm Orange for dark mode
108
- buttonSecondary: '#FFCC99', // Light Peach for dark mode
106
+ // COMPONENT-SPECIFIC COLORS (dark mode)
107
+ buttonPrimary: '#5AAF7F',
108
+ buttonSecondary: '#FF8C42',
109
109
 
110
- inputBackground: '#1E293B', // Dark input background
111
- inputBorder: '#475569', // Subtle input border
110
+ inputBackground: '#2C2C2E',
111
+ inputBorder: '#48484A',
112
112
 
113
- cardBackground: '#1E293B', // Dark card background
113
+ cardBackground: '#2C2C2E',
114
114
 
115
115
  // COLOR ALIASES
116
- text: '#F1F5F9', // Alias for textPrimary
117
- background: '#0F172A', // Alias for backgroundPrimary
118
- card: '#1E293B', // Alias for cardBackground
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 (Same as light mode for type consistency)
126
- modalOverlay: 'rgba(0, 0, 0, 0.5)',
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.3)',
130
- overlayBackground: 'rgba(0, 0, 0, 0.05)',
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.5)',
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(255, 140, 66, 0.1)', // Orange background (dark mode)
139
+ primaryBackground: 'rgba(90, 175, 127, 0.15)', // Green overlay (dark mode)
140
140
 
141
- cardOverlay: 'rgba(0, 0, 0, 0.15)',
141
+ cardOverlay: 'rgba(0, 0, 0, 0.25)',
142
142
 
143
- inputBackground_RGBA: 'rgba(248, 250, 252, 0.9)',
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 with warm orange harmony
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 - Warm Orange & Harmony Bloom
9
- primary: '#FF6B35', // Vibrant Orange
10
- primaryLight: '#FF8C42', // Warm Orange
11
- primaryDark: '#FF4500', // Orange Red
12
-
13
- secondary: '#FFB88C', // Soft Peach
14
- secondaryLight: '#FFCC99', // Light Peach
15
- secondaryDark: '#FF8C69', // Salmon
16
-
17
- accent: '#FFB6C1', // Light Pink (Bloom)
18
- accentLight: '#FFA07A', // Light Salmon
19
- accentDark: '#FF8C69', // Salmon
20
-
21
- // MATERIAL DESIGN 3 - ON COLORS (Text on colored backgrounds)
22
- onPrimary: '#FFFFFF', // Text on primary background
23
- onSecondary: '#FFFFFF', // Text on secondary background
24
- onSuccess: '#FFFFFF', // Text on success background
25
- onError: '#FFFFFF', // Text on error background
26
- onWarning: '#000000', // Text on warning background
27
- onInfo: '#FFFFFF', // Text on info background
28
- onSurface: '#1E293B', // Text on surface
29
- onBackground: '#1E293B', // Text on background
30
- onSurfaceDisabled: '#CBD5E1', // Disabled text color
31
- onSurfaceVariant: '#64748B', // Text on surface variant
32
-
33
- // MATERIAL DESIGN 3 - CONTAINER COLORS (Lighter versions for containers)
34
- primaryContainer: '#FFE4CD', // Light orange container
35
- onPrimaryContainer: '#CC4A1F', // Text on primary container
36
- secondaryContainer: '#FFF8DC', // Light peach container
37
- onSecondaryContainer: '#CC8C5F', // Text on secondary container
38
- errorContainer: '#FEE2E2', // Light container using error
39
- onErrorContainer: '#991B1B', // Text on error container
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: '#CBD5E1', // Default outline color
43
- outlineVariant: '#E2E8F0', // Lighter outline variant
44
- outlineDisabled: '#E2E8F0', // Disabled outline color
42
+ outline: '#C7C7CC',
43
+ outlineVariant: '#E5E5EA',
44
+ outlineDisabled: '#E5E5EA',
45
45
 
46
46
  // SEMANTIC UI COLORS
47
- success: '#10B981',
48
- successLight: '#34D399',
49
- successDark: '#059669',
50
-
51
- error: '#EF4444',
52
- errorLight: '#F87171',
53
- errorDark: '#DC2626',
54
-
55
- warning: '#F59E0B',
56
- warningLight: '#FBBF24',
57
- warningDark: '#D97706',
58
-
59
- info: '#FF8C42', // Warm Orange for info
60
- infoLight: '#FFA07A', // Light Salmon
61
- infoDark: '#FF6347', // Tomato
62
-
63
- // SEMANTIC CONTAINER COLORS (Light mode)
64
- successContainer: '#D1FAE5', // Light container for success states
65
- onSuccessContainer: '#065F46', // Text on success container
66
- warningContainer: '#FEF3C7', // Light container for warning states
67
- onWarningContainer: '#92400E', // Text on warning container
68
- infoContainer: '#FFE4CD', // Light orange container for info states
69
- onInfoContainer: '#CC4A1F', // Text on info container
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: '#E4E4E7',
75
- gray300: '#D4D4D8',
76
- gray400: '#A1A1AA',
77
- gray500: '#71717A',
78
- gray600: '#52525B',
79
- gray700: '#3F3F46',
80
- gray800: '#27272A',
81
- gray900: '#18181B',
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: '#F8FAFC',
85
+ backgroundSecondary: '#F8F9FA',
86
86
 
87
87
  surface: '#FFFFFF',
88
- surfaceVariant: '#F1F5F9',
89
- surfaceSecondary: '#F1F5F9', // Alias
90
- surfaceDisabled: '#F4F4F5', // Disabled surface color
88
+ surfaceVariant: '#F2F2F7',
89
+ surfaceSecondary: '#F2F2F7',
90
+ surfaceDisabled: '#F4F4F4',
91
91
 
92
92
  // TEXT COLORS
93
- textPrimary: '#1E293B',
94
- textSecondary: '#64748B',
95
- textTertiary: '#94A3B8',
96
- textDisabled: '#CBD5E1',
93
+ textPrimary: '#1C1C1E',
94
+ textSecondary: '#6C757D',
95
+ textTertiary: '#98989D',
96
+ textDisabled: '#C7C7CC',
97
97
  textInverse: '#FFFFFF',
98
98
 
99
99
  // BORDER COLORS
100
- border: '#E2E8F0',
101
- borderLight: '#F1F5F9',
102
- borderMedium: '#CBD5E1',
103
- borderFocus: '#3B82F6',
104
- borderDisabled: '#F1F5F9',
100
+ border: '#E5E5EA',
101
+ borderLight: '#F2F2F7',
102
+ borderMedium: '#C7C7CC',
103
+ borderFocus: '#4A9B6F',
104
+ borderDisabled: '#F2F2F7',
105
105
 
106
106
  // COMPONENT-SPECIFIC COLORS
107
- buttonPrimary: '#FF6B35', // Vibrant Orange
108
- buttonSecondary: '#FFB88C', // Soft Peach
107
+ buttonPrimary: '#4A9B6F',
108
+ buttonSecondary: '#FF8C42',
109
109
 
110
110
  inputBackground: '#FFFFFF',
111
- inputBorder: '#E2E8F0',
111
+ inputBorder: '#E5E5EA',
112
112
 
113
113
  cardBackground: '#FFFFFF',
114
114
 
115
115
  // COLOR ALIASES
116
- text: '#1E293B', // Alias for textPrimary
117
- background: '#FFFFFF', // Alias for backgroundPrimary
118
- card: '#FFFFFF', // Alias for cardBackground
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 (for modals, cards, etc.)
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(239, 68, 68, 0.1)',
139
- primaryBackground: 'rgba(255, 107, 53, 0.1)', // Orange background
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.15)',
141
+ cardOverlay: 'rgba(0, 0, 0, 0.08)',
142
142
 
143
- inputBackground_RGBA: 'rgba(248, 250, 252, 0.9)',
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
- set({ customColors: colors });
107
- await ThemeStorage.setCustomColors(colors);
108
- useDesignSystemTheme.getState().setCustomColors(colors);
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
- useEffect(() => {
83
- onSuccessRef.current = onSuccess;
84
- onErrorRef.current = onError;
85
- onFinallyRef.current = onFinally;
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