@umituz/react-native-design-system 4.28.11 → 4.28.13

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 (49) hide show
  1. package/package.json +31 -8
  2. package/src/atoms/AtomicAvatar.tsx +69 -40
  3. package/src/atoms/AtomicDatePicker.tsx +6 -6
  4. package/src/atoms/AtomicSpinner.tsx +24 -22
  5. package/src/atoms/AtomicText.tsx +32 -27
  6. package/src/atoms/AtomicTextArea.tsx +17 -15
  7. package/src/atoms/EmptyState.tsx +44 -41
  8. package/src/atoms/button/AtomicButton.tsx +8 -9
  9. package/src/atoms/card/AtomicCard.tsx +26 -8
  10. package/src/atoms/datepicker/components/DatePickerButton.tsx +8 -8
  11. package/src/atoms/datepicker/components/DatePickerModal.tsx +7 -7
  12. package/src/atoms/fab/styles/fabStyles.ts +0 -21
  13. package/src/atoms/icon/index.ts +6 -20
  14. package/src/atoms/picker/components/PickerModal.tsx +24 -4
  15. package/src/atoms/skeleton/AtomicSkeleton.tsx +9 -11
  16. package/src/carousel/Carousel.tsx +43 -20
  17. package/src/carousel/carouselCalculations.ts +12 -9
  18. package/src/carousel/index.ts +0 -1
  19. package/src/device/detection/iPadDetection.ts +5 -14
  20. package/src/device/infrastructure/services/DeviceFeatureService.ts +89 -9
  21. package/src/device/infrastructure/services/DeviceInfoService.ts +33 -0
  22. package/src/device/infrastructure/services/UserFriendlyIdService.ts +8 -6
  23. package/src/device/infrastructure/utils/__tests__/stringUtils.test.ts +56 -20
  24. package/src/device/infrastructure/utils/nativeModuleUtils.ts +16 -2
  25. package/src/device/infrastructure/utils/stringUtils.ts +51 -5
  26. package/src/filesystem/domain/utils/FileUtils.ts +5 -1
  27. package/src/image/domain/utils/ImageUtils.ts +6 -0
  28. package/src/layouts/AppHeader/AppHeader.tsx +13 -3
  29. package/src/layouts/Container/Container.tsx +19 -1
  30. package/src/layouts/FormLayout/FormLayout.tsx +20 -1
  31. package/src/layouts/Grid/Grid.tsx +34 -4
  32. package/src/layouts/ScreenHeader/ScreenHeader.tsx +4 -0
  33. package/src/layouts/ScreenLayout/ScreenLayout.tsx +42 -3
  34. package/src/molecules/SearchBar/SearchBar.tsx +27 -23
  35. package/src/molecules/action-footer/ActionFooter.tsx +32 -31
  36. package/src/molecules/alerts/AlertService.ts +60 -15
  37. package/src/molecules/avatar/Avatar.tsx +3 -3
  38. package/src/molecules/avatar/AvatarGroup.tsx +7 -7
  39. package/src/molecules/bottom-sheet/components/BottomSheet.tsx +3 -3
  40. package/src/molecules/calendar/infrastructure/utils/DateUtilities.ts +12 -1
  41. package/src/molecules/calendar/presentation/components/CalendarDayCell.tsx +48 -32
  42. package/src/molecules/info-grid/InfoGrid.tsx +5 -3
  43. package/src/organisms/FormContainer.tsx +11 -1
  44. package/src/tanstack/domain/utils/ErrorHelpers.ts +2 -2
  45. package/src/tanstack/domain/utils/MetricsCalculator.ts +6 -1
  46. package/src/theme/core/colors/ColorUtils.ts +7 -4
  47. package/src/utils/formatters/stringFormatter.ts +18 -3
  48. package/src/utils/index.ts +6 -4
  49. package/src/utils/math/CalculationUtils.ts +10 -1
@@ -5,7 +5,7 @@
5
5
  * Uses design system responsive utilities
6
6
  */
7
7
 
8
- import React, { useMemo } from 'react';
8
+ import React, { useMemo, useId } from 'react';
9
9
  import { View, StyleSheet, type StyleProp, type ViewStyle } from 'react-native';
10
10
  import { useResponsive } from '../../responsive';
11
11
  import { useAppDesignTokens } from '../../theme';
@@ -28,6 +28,12 @@ export interface GridProps {
28
28
 
29
29
  /** Test ID for testing */
30
30
  testID?: string;
31
+
32
+ /** Accessibility label for the grid */
33
+ accessibilityLabel?: string;
34
+
35
+ /** Whether the grid is accessible */
36
+ accessible?: boolean;
31
37
  }
32
38
 
33
39
  /**
@@ -49,9 +55,12 @@ export const Grid: React.FC<GridProps> = ({
49
55
  gap,
50
56
  style,
51
57
  testID,
58
+ accessibilityLabel,
59
+ accessible,
52
60
  }) => {
53
61
  const { gridColumns, spacingMultiplier } = useResponsive();
54
62
  const tokens = useAppDesignTokens();
63
+ const generatedIdPrefix = useId();
55
64
 
56
65
  // Calculate responsive columns
57
66
  const columns = gridColumns || (mobileColumns && tabletColumns
@@ -82,12 +91,33 @@ export const Grid: React.FC<GridProps> = ({
82
91
  const childArray = React.Children.toArray(children);
83
92
 
84
93
  return (
85
- <View style={[styles.container, style]} testID={testID}>
94
+ <View
95
+ style={[styles.container, style]}
96
+ testID={testID}
97
+ accessibilityLabel={accessibilityLabel}
98
+ accessibilityRole={"grid" as any}
99
+ accessible={accessible !== false}
100
+ >
86
101
  {childArray.map((child, index) => {
87
- const key = (child as React.ReactElement).key || `grid-item-${index}`;
102
+ const childKey = (child as React.ReactElement).key;
103
+
104
+ // Warn in development if child is missing key
105
+ if (__DEV__ && !childKey) {
106
+ console.warn(
107
+ `[Grid] Child at index ${index} is missing a "key" prop. ` +
108
+ `This may cause issues with React reconciliation. ` +
109
+ `Please ensure all grid children have unique keys.`
110
+ );
111
+ }
112
+
113
+ const key = childKey || `${generatedIdPrefix}-grid-item-${index}`;
88
114
 
89
115
  return (
90
- <View key={key} style={itemStyle}>
116
+ <View
117
+ key={key}
118
+ style={itemStyle}
119
+ accessibilityRole={"gridcell" as any}
120
+ >
91
121
  {child}
92
122
  </View>
93
123
  );
@@ -86,6 +86,10 @@ const ScreenHeaderBackButton: React.FC<{
86
86
  onPress={handleBackPress}
87
87
  hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
88
88
  testID={`${testID}-back-button`}
89
+ accessibilityRole="button"
90
+ accessibilityLabel="Go back"
91
+ accessibilityState={{ disabled: !onBackPress }}
92
+ accessible
89
93
  >
90
94
  <AtomicIcon name={backIconName || 'arrow-back'} color={backIconColor} />
91
95
  </TouchableOpacity>
@@ -3,13 +3,34 @@
3
3
  */
4
4
 
5
5
  import React, { useMemo } from 'react';
6
- import { View, ScrollView, KeyboardAvoidingView, Platform } from 'react-native';
6
+ import { View, ScrollView, KeyboardAvoidingView, Platform, type StyleProp, type ViewStyle } from 'react-native';
7
7
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
8
8
  import { useAppDesignTokens } from '../../theme';
9
9
  import { getScreenLayoutConfig } from '../../responsive/responsiveLayout';
10
10
  import { getScreenLayoutStyles } from './styles/screenLayoutStyles';
11
11
  import type { ScreenLayoutProps } from './types';
12
12
 
13
+ export interface ScreenLayoutProps {
14
+ children: React.ReactNode;
15
+ scrollable?: boolean;
16
+ edges?: ('top' | 'bottom' | 'left' | 'right')[];
17
+ header?: React.ReactNode;
18
+ footer?: React.ReactNode;
19
+ backgroundColor?: string;
20
+ containerStyle?: StyleProp<ViewStyle>;
21
+ contentContainerStyle?: StyleProp<ViewStyle>;
22
+ testID?: string;
23
+ hideScrollIndicator?: boolean;
24
+ keyboardAvoiding?: boolean;
25
+ keyboardVerticalOffset?: number;
26
+ maxWidth?: number;
27
+ fullWidth?: boolean;
28
+ refreshControl?: React.ReactElement;
29
+ accessibilityLabel?: string;
30
+ accessibilityRole?: 'window' | 'region' | 'section';
31
+ accessible?: boolean;
32
+ }
33
+
13
34
  // Lazy-load react-native-keyboard-controller (optional peer dep).
14
35
  // Falls back to React Native's built-in components when not installed.
15
36
  let KCKeyboardAvoidingView: React.ComponentType<any> | null = null;
@@ -40,6 +61,9 @@ export const ScreenLayout: React.FC<ScreenLayoutProps> = (props: ScreenLayoutPro
40
61
  maxWidth,
41
62
  fullWidth = false,
42
63
  refreshControl,
64
+ accessibilityLabel,
65
+ accessibilityRole = 'region',
66
+ accessible,
43
67
  } = props;
44
68
 
45
69
  const tokens = useAppDesignTokens();
@@ -80,6 +104,9 @@ export const ScreenLayout: React.FC<ScreenLayoutProps> = (props: ScreenLayoutPro
80
104
  paddingRight,
81
105
  },
82
106
  ]}
107
+ accessibilityLabel={accessibilityLabel}
108
+ accessibilityRole={accessibilityRole}
109
+ accessible={accessible !== false}
83
110
  >
84
111
  {header}
85
112
  {scrollable ? (
@@ -109,7 +136,13 @@ export const ScreenLayout: React.FC<ScreenLayoutProps> = (props: ScreenLayoutPro
109
136
  if (keyboardAvoiding) {
110
137
  const KAV = KCKeyboardAvoidingView ?? KeyboardAvoidingView;
111
138
  return (
112
- <View style={[styles.container, { backgroundColor: bgColor }, containerStyle]} testID={testID}>
139
+ <View
140
+ style={[styles.container, { backgroundColor: bgColor }, containerStyle]}
141
+ testID={testID}
142
+ accessibilityLabel={accessibilityLabel}
143
+ accessibilityRole={accessibilityRole}
144
+ accessible={accessible !== false}
145
+ >
113
146
  <KAV
114
147
  style={styles.keyboardAvoidingView}
115
148
  behavior={Platform.OS === 'ios' ? 'padding' : undefined}
@@ -122,7 +155,13 @@ export const ScreenLayout: React.FC<ScreenLayoutProps> = (props: ScreenLayoutPro
122
155
  }
123
156
 
124
157
  return (
125
- <View style={[styles.container, { backgroundColor: bgColor }, containerStyle]} testID={testID}>
158
+ <View
159
+ style={[styles.container, { backgroundColor: bgColor }, containerStyle]}
160
+ testID={testID}
161
+ accessibilityLabel={accessibilityLabel}
162
+ accessibilityRole={accessibilityRole}
163
+ accessible={accessible !== false}
164
+ >
126
165
  <View style={styles.keyboardAvoidingView}>{content}</View>
127
166
  </View>
128
167
  );
@@ -12,7 +12,7 @@ import type { SearchBarProps } from './types';
12
12
  import { calculateResponsiveSize } from '../../responsive';
13
13
  import { MISC_SIZES } from '../../constants';
14
14
 
15
- export const SearchBar: React.FC<SearchBarProps> = ({
15
+ export const SearchBar: React.FC<SearchBarProps> = React.memo(({
16
16
  value,
17
17
  onChangeText,
18
18
  onSubmit,
@@ -22,8 +22,8 @@ export const SearchBar: React.FC<SearchBarProps> = ({
22
22
  placeholder = 'Search...',
23
23
  loading = false,
24
24
  disabled = false,
25
- containerStyle,
26
- inputStyle,
25
+ containerStyle: propContainerStyle,
26
+ inputStyle: propInputStyle,
27
27
  testID,
28
28
  }) => {
29
29
  const tokens = useAppDesignTokens();
@@ -42,19 +42,30 @@ export const SearchBar: React.FC<SearchBarProps> = ({
42
42
 
43
43
  const spacingMultiplier = tokens.spacingMultiplier;
44
44
 
45
+ const containerStyle = useMemo(() => [
46
+ styles.container,
47
+ {
48
+ backgroundColor: tokens.colors.surfaceVariant,
49
+ borderColor: tokens.colors.border,
50
+ height: calculateResponsiveSize(MISC_SIZES.searchInputHeight, spacingMultiplier),
51
+ paddingHorizontal: calculateResponsiveSize(tokens.spacing.md, spacingMultiplier),
52
+ borderRadius: calculateResponsiveSize(MISC_SIZES.searchBorderRadius, spacingMultiplier),
53
+ },
54
+ propContainerStyle,
55
+ ], [tokens.colors.surfaceVariant, tokens.colors.border, spacingMultiplier, propContainerStyle]);
56
+
57
+ const inputTextStyle = useMemo(() => [
58
+ styles.input,
59
+ {
60
+ color: tokens.colors.textPrimary,
61
+ fontSize: tokens.typography.bodyMedium.responsiveFontSize,
62
+ },
63
+ propInputStyle,
64
+ ], [styles.input, tokens.colors.textPrimary, tokens.typography.bodyMedium.responsiveFontSize, propInputStyle]);
65
+
45
66
  return (
46
67
  <View
47
- style={[
48
- styles.container,
49
- {
50
- backgroundColor: tokens.colors.surfaceVariant,
51
- borderColor: tokens.colors.border,
52
- height: calculateResponsiveSize(MISC_SIZES.searchInputHeight, spacingMultiplier),
53
- paddingHorizontal: calculateResponsiveSize(tokens.spacing.md, spacingMultiplier),
54
- borderRadius: calculateResponsiveSize(MISC_SIZES.searchBorderRadius, spacingMultiplier),
55
- },
56
- containerStyle,
57
- ]}
68
+ style={containerStyle}
58
69
  testID={testID}
59
70
  >
60
71
  <View style={styles.searchIcon}>
@@ -77,14 +88,7 @@ export const SearchBar: React.FC<SearchBarProps> = ({
77
88
  returnKeyType="search"
78
89
  autoCapitalize="none"
79
90
  autoCorrect={false}
80
- style={[
81
- styles.input,
82
- {
83
- color: tokens.colors.textPrimary,
84
- fontSize: tokens.typography.bodyMedium.responsiveFontSize,
85
- },
86
- inputStyle,
87
- ]}
91
+ style={inputTextStyle}
88
92
  />
89
93
 
90
94
  {(loading || showClear) && (
@@ -116,7 +120,7 @@ export const SearchBar: React.FC<SearchBarProps> = ({
116
120
  )}
117
121
  </View>
118
122
  );
119
- };
123
+ });
120
124
 
121
125
  const styles = StyleSheet.create({
122
126
  container: {
@@ -58,38 +58,39 @@ export const ActionFooter = React.memo<ActionFooterProps>(({
58
58
  const tokens = useAppDesignTokens();
59
59
  const spacingMultiplier = tokens.spacingMultiplier;
60
60
 
61
- const baseStyles = createStyles(spacingMultiplier);
62
-
63
61
  const themedStyles = useMemo(
64
- () => ({
65
- container: {
66
- ...baseStyles.container,
67
- paddingVertical: tokens.spacing.md,
68
- gap: tokens.spacing.md,
69
- },
70
- backButton: {
71
- ...baseStyles.backButton,
72
- borderRadius: tokens.borders.radius.lg,
73
- backgroundColor: tokens.colors.surface,
74
- borderColor: tokens.colors.outlineVariant,
75
- },
76
- actionButton: {
77
- ...baseStyles.actionButton,
78
- borderRadius: tokens.borders.radius.lg,
79
- },
80
- actionContent: {
81
- ...baseStyles.actionContent,
82
- backgroundColor: tokens.colors.primary,
83
- gap: tokens.spacing.sm,
84
- paddingHorizontal: tokens.spacing.lg,
85
- },
86
- actionText: {
87
- ...baseStyles.actionText,
88
- color: tokens.colors.onPrimary,
89
- fontSize: calculateResponsiveSize(18, spacingMultiplier),
90
- },
91
- }),
92
- [baseStyles, tokens, spacingMultiplier],
62
+ () => {
63
+ const baseStyles = createStyles(spacingMultiplier);
64
+ return {
65
+ container: {
66
+ ...baseStyles.container,
67
+ paddingVertical: tokens.spacing.md,
68
+ gap: tokens.spacing.md,
69
+ },
70
+ backButton: {
71
+ ...baseStyles.backButton,
72
+ borderRadius: tokens.borders.radius.lg,
73
+ backgroundColor: tokens.colors.surface,
74
+ borderColor: tokens.colors.outlineVariant,
75
+ },
76
+ actionButton: {
77
+ ...baseStyles.actionButton,
78
+ borderRadius: tokens.borders.radius.lg,
79
+ },
80
+ actionContent: {
81
+ ...baseStyles.actionContent,
82
+ backgroundColor: tokens.colors.primary,
83
+ gap: tokens.spacing.sm,
84
+ paddingHorizontal: tokens.spacing.lg,
85
+ },
86
+ actionText: {
87
+ ...baseStyles.actionText,
88
+ color: tokens.colors.onPrimary,
89
+ fontSize: calculateResponsiveSize(18, spacingMultiplier),
90
+ },
91
+ };
92
+ },
93
+ [tokens, spacingMultiplier],
93
94
  );
94
95
 
95
96
  const handleBackPress = useCallback(() => {
@@ -7,6 +7,12 @@ import { Alert, AlertType, AlertMode, AlertOptions, AlertPosition } from './Aler
7
7
  import { useAlertStore } from './AlertStore';
8
8
 
9
9
  export class AlertService {
10
+ // Debouncing state
11
+ private static lastAlertTime = 0;
12
+ private static debounceDelay = 300; // ms
13
+ private static pendingAlertTimeout: ReturnType<typeof setTimeout> | null = null;
14
+ private static pendingAlert: { type: AlertType; mode: AlertMode; title: string; message?: string; options?: AlertOptions } | null = null;
15
+
10
16
  /**
11
17
  * Creates a base Alert object with defaults
12
18
  */
@@ -59,38 +65,71 @@ export class AlertService {
59
65
  return this.createAlert(AlertType.INFO, AlertMode.TOAST, title, message, options);
60
66
  }
61
67
 
68
+ /**
69
+ * Add alert with debouncing to prevent spam
70
+ */
71
+ private static addAlertDebounced(type: AlertType, mode: AlertMode, title: string, message?: string, options?: AlertOptions): string {
72
+ const now = Date.now();
73
+ const timeSinceLastAlert = now - this.lastAlertTime;
74
+
75
+ // Clear any pending alert
76
+ if (this.pendingAlertTimeout) {
77
+ clearTimeout(this.pendingAlertTimeout);
78
+ this.pendingAlertTimeout = null;
79
+ }
80
+
81
+ // If enough time has passed, show immediately
82
+ if (timeSinceLastAlert >= this.debounceDelay) {
83
+ const alert = this.createAlert(type, mode, title, message, options);
84
+ useAlertStore.getState().addAlert(alert);
85
+ this.lastAlertTime = now;
86
+ return alert.id;
87
+ }
88
+
89
+ // Otherwise, debounce and show the latest alert after delay
90
+ this.pendingAlert = { type, mode, title, message, options };
91
+ this.pendingAlertTimeout = setTimeout(() => {
92
+ if (this.pendingAlert) {
93
+ const alert = this.createAlert(
94
+ this.pendingAlert.type,
95
+ this.pendingAlert.mode,
96
+ this.pendingAlert.title,
97
+ this.pendingAlert.message,
98
+ this.pendingAlert.options
99
+ );
100
+ useAlertStore.getState().addAlert(alert);
101
+ this.lastAlertTime = Date.now();
102
+ this.pendingAlert = null;
103
+ this.pendingAlertTimeout = null;
104
+ }
105
+ }, this.debounceDelay);
106
+
107
+ // Return a placeholder ID (the real alert will be shown after debounce)
108
+ return `pending-${Date.now()}`;
109
+ }
110
+
62
111
  /**
63
112
  * Convenience methods to show alerts directly from outside React components
64
113
  * These access the Zustand store directly without requiring hooks
65
114
  */
66
115
  static success(title: string, message?: string, options?: AlertOptions): string {
67
- const alert = this.createSuccessAlert(title, message, options);
68
- useAlertStore.getState().addAlert(alert);
69
- return alert.id;
116
+ return this.addAlertDebounced(AlertType.SUCCESS, AlertMode.TOAST, title, message, options);
70
117
  }
71
118
 
72
119
  static error(title: string, message?: string, options?: AlertOptions): string {
73
- const alert = this.createErrorAlert(title, message, options);
74
- useAlertStore.getState().addAlert(alert);
75
- return alert.id;
120
+ return this.addAlertDebounced(AlertType.ERROR, AlertMode.TOAST, title, message, options);
76
121
  }
77
122
 
78
123
  static warning(title: string, message?: string, options?: AlertOptions): string {
79
- const alert = this.createWarningAlert(title, message, options);
80
- useAlertStore.getState().addAlert(alert);
81
- return alert.id;
124
+ return this.addAlertDebounced(AlertType.WARNING, AlertMode.TOAST, title, message, options);
82
125
  }
83
126
 
84
127
  static info(title: string, message?: string, options?: AlertOptions): string {
85
- const alert = this.createInfoAlert(title, message, options);
86
- useAlertStore.getState().addAlert(alert);
87
- return alert.id;
128
+ return this.addAlertDebounced(AlertType.INFO, AlertMode.TOAST, title, message, options);
88
129
  }
89
130
 
90
131
  static show(type: AlertType, mode: AlertMode, title: string, message?: string, options?: AlertOptions): string {
91
- const alert = this.createAlert(type, mode, title, message, options);
92
- useAlertStore.getState().addAlert(alert);
93
- return alert.id;
132
+ return this.addAlertDebounced(type, mode, title, message, options);
94
133
  }
95
134
 
96
135
  static dismiss(id: string): void {
@@ -98,6 +137,12 @@ export class AlertService {
98
137
  }
99
138
 
100
139
  static clear(): void {
140
+ // Clear any pending alert
141
+ if (this.pendingAlertTimeout) {
142
+ clearTimeout(this.pendingAlertTimeout);
143
+ this.pendingAlertTimeout = null;
144
+ this.pendingAlert = null;
145
+ }
101
146
  useAlertStore.getState().clearAlerts();
102
147
  }
103
148
  }
@@ -48,7 +48,7 @@ const AvatarContent: React.FC<AvatarContentProps> = React.memo(({
48
48
  icon,
49
49
  config,
50
50
  borderRadius,
51
- imageStyle,
51
+ imageStyle: propImageStyle,
52
52
  }) => {
53
53
  const tokens = useAppDesignTokens();
54
54
 
@@ -59,8 +59,8 @@ const AvatarContent: React.FC<AvatarContentProps> = React.memo(({
59
59
  height: config.size,
60
60
  borderRadius,
61
61
  },
62
- imageStyle,
63
- ], [config.size, borderRadius, imageStyle]);
62
+ propImageStyle,
63
+ ], [config.size, borderRadius, propImageStyle]);
64
64
 
65
65
  const initialsStyle = useMemo(() => [
66
66
  styles.initials,
@@ -58,13 +58,13 @@ const AvatarItem = React.memo<{
58
58
  spacing: number;
59
59
  avatarStyle: any;
60
60
  }>(({ item, index, size, shape, spacing, avatarStyle }) => {
61
- const wrapperStyle = useMemo(
62
- () => [
63
- styles.avatarWrapper,
64
- index > 0 && { marginLeft: spacing },
65
- ],
66
- [index, spacing]
67
- );
61
+ const wrapperStyle = useMemo(() => {
62
+ const baseStyle = [styles.avatarWrapper];
63
+ if (index > 0) {
64
+ baseStyle.push({ marginLeft: spacing });
65
+ }
66
+ return baseStyle;
67
+ }, [index, spacing]);
68
68
 
69
69
  return (
70
70
  <View style={wrapperStyle}>
@@ -69,11 +69,11 @@ export const BottomSheet = forwardRef<BottomSheetRef, BottomSheetProps>((props,
69
69
 
70
70
  const spacingMultiplier = tokens.spacingMultiplier;
71
71
 
72
- const styles = useMemo(() => StyleSheet.create({
72
+ const styles = useMemo(() => ({
73
73
  overlay: {
74
74
  flex: 1,
75
75
  backgroundColor: tokens.colors.modalOverlay,
76
- justifyContent: 'flex-end',
76
+ justifyContent: 'flex-end' as const,
77
77
  },
78
78
  container: {
79
79
  height: sheetHeight,
@@ -87,7 +87,7 @@ export const BottomSheet = forwardRef<BottomSheetRef, BottomSheetProps>((props,
87
87
  height: calculateResponsiveSize(BOTTOM_SHEET_HANDLE.height, spacingMultiplier),
88
88
  backgroundColor: tokens.colors.border,
89
89
  borderRadius: calculateResponsiveSize(BOTTOM_SHEET_HANDLE.borderRadius, spacingMultiplier),
90
- alignSelf: 'center',
90
+ alignSelf: 'center' as const,
91
91
  marginTop: tokens.spacing.md,
92
92
  marginBottom: tokens.spacing.sm,
93
93
  },
@@ -96,7 +96,18 @@ export class DateUtilities {
96
96
  const year = parts[0] ?? 0;
97
97
  const month = parts[1] ?? 1;
98
98
  const day = parts[2] ?? 1;
99
- return new Date(year, month - 1, day);
99
+
100
+ const date = new Date(year, month - 1, day);
101
+
102
+ // Validate the date is not invalid
103
+ if (isNaN(date.getTime())) {
104
+ if (__DEV__) {
105
+ console.warn(`[DateUtilities] Invalid date string: ${dateString}`);
106
+ }
107
+ throw new Error(`Invalid date string: ${dateString}`);
108
+ }
109
+
110
+ return date;
100
111
  }
101
112
 
102
113
  /**
@@ -2,7 +2,7 @@
2
2
  * Calendar Day Cell Component
3
3
  */
4
4
 
5
- import React from 'react';
5
+ import React, { useMemo } from 'react';
6
6
  import { TouchableOpacity, View, StyleProp, ViewStyle } from 'react-native';
7
7
  import { AtomicText } from '../../../../atoms';
8
8
  import { useAppDesignTokens } from '../../../../theme/hooks/useAppDesignTokens';
@@ -38,22 +38,34 @@ export const CalendarDayCell: React.FC<CalendarDayCellProps> = React.memo(({
38
38
  const visibleEvents = day.events.slice(0, maxEventIndicators);
39
39
  const hiddenEventCount = Math.max(0, eventCount - maxEventIndicators);
40
40
 
41
+ const cellStyle = useMemo(() => [
42
+ calendarStyles.dayCell,
43
+ {
44
+ backgroundColor: isSelected ? tokens.colors.primary : 'transparent',
45
+ borderColor: isSelected
46
+ ? tokens.colors.primary
47
+ : day.isToday
48
+ ? tokens.colors.primary
49
+ : tokens.colors.border,
50
+ borderWidth: isSelected ? 2 : day.isToday ? 2 : 1,
51
+ opacity: day.isDisabled ? 0.4 : 1,
52
+ },
53
+ dayStyle,
54
+ ], [isSelected, day.isToday, day.isDisabled, tokens.colors.primary, tokens.colors.border, dayStyle]);
55
+
56
+ const dayTextStyle = useMemo(() => [
57
+ calendarStyles.dayText,
58
+ day.isToday && !isSelected && { fontWeight: 'bold' as const },
59
+ ], [day.isToday, isSelected]);
60
+
61
+ const todayDotStyle = useMemo(() => [
62
+ calendarStyles.eventDot,
63
+ { backgroundColor: tokens.colors.success },
64
+ ], [calendarStyles.eventDot, tokens.colors.success]);
65
+
41
66
  return (
42
67
  <TouchableOpacity
43
- style={[
44
- calendarStyles.dayCell,
45
- {
46
- backgroundColor: isSelected ? tokens.colors.primary : 'transparent',
47
- borderColor: isSelected
48
- ? tokens.colors.primary
49
- : day.isToday
50
- ? tokens.colors.primary
51
- : tokens.colors.border,
52
- borderWidth: isSelected ? 2 : day.isToday ? 2 : 1,
53
- opacity: day.isDisabled ? 0.4 : 1,
54
- },
55
- dayStyle,
56
- ]}
68
+ style={cellStyle}
57
69
  onPress={() => !day.isDisabled && onDateSelect(day.date)}
58
70
  disabled={day.isDisabled}
59
71
  testID={testID ? `${testID}-day-${index}` : undefined}
@@ -64,31 +76,35 @@ export const CalendarDayCell: React.FC<CalendarDayCellProps> = React.memo(({
64
76
  <AtomicText
65
77
  type="bodyMedium"
66
78
  color={isSelected ? 'inverse' : day.isCurrentMonth ? 'primary' : 'secondary'}
67
- style={[calendarStyles.dayText, day.isToday && !isSelected && { fontWeight: 'bold' }]}
79
+ style={dayTextStyle}
68
80
  >
69
81
  {day.date.getDate()}
70
82
  </AtomicText>
71
83
 
72
84
  <View style={calendarStyles.eventIndicators}>
73
85
  {day.isToday && eventCount === 0 && (
74
- <View style={[calendarStyles.eventDot, { backgroundColor: tokens.colors.success }]} />
86
+ <View style={todayDotStyle} />
75
87
  )}
76
88
 
77
- {visibleEvents.map((event) => (
78
- <View
79
- key={event.id}
80
- style={[
81
- calendarStyles.eventDot,
82
- {
83
- backgroundColor: event.color
84
- ? event.color
85
- : event.isCompleted
86
- ? tokens.colors.success
87
- : tokens.colors.primary,
88
- },
89
- ]}
90
- />
91
- ))}
89
+ {visibleEvents.map((event) => {
90
+ const eventDotStyle = useMemo(() => [
91
+ calendarStyles.eventDot,
92
+ {
93
+ backgroundColor: event.color
94
+ ? event.color
95
+ : event.isCompleted
96
+ ? tokens.colors.success
97
+ : tokens.colors.primary,
98
+ },
99
+ ], [event.color, event.isCompleted, tokens.colors.success, tokens.colors.primary]);
100
+
101
+ return (
102
+ <View
103
+ key={event.id}
104
+ style={eventDotStyle}
105
+ />
106
+ );
107
+ })}
92
108
 
93
109
  {showEventCount && hiddenEventCount > 0 && (
94
110
  <AtomicText type="bodySmall" color="secondary" style={calendarStyles.moreEventsText}>
@@ -8,7 +8,7 @@ import type { InfoGridProps } from './types';
8
8
  import { calculateResponsiveSize } from '../../responsive';
9
9
  import { INFO_GRID_ICONS } from '../../constants';
10
10
 
11
- export const InfoGrid: React.FC<InfoGridProps> = ({
11
+ export const InfoGrid: React.FC<InfoGridProps> = React.memo(({
12
12
  title,
13
13
  headerIcon,
14
14
  items,
@@ -73,6 +73,8 @@ export const InfoGrid: React.FC<InfoGridProps> = ({
73
73
  },
74
74
  }), [tokens, columns, spacingMultiplier]);
75
75
 
76
+ const memoizedItemStyle = useMemo(() => itemStyle, [itemStyle]);
77
+
76
78
  return (
77
79
  <View style={[styles.container, style]}>
78
80
  {(title || headerIcon) && (
@@ -88,7 +90,7 @@ export const InfoGrid: React.FC<InfoGridProps> = ({
88
90
 
89
91
  <View style={styles.grid}>
90
92
  {items.map((item) => (
91
- <View key={item.text} style={[styles.item, itemStyle]}>
93
+ <View key={item.text} style={[styles.item, memoizedItemStyle]}>
92
94
  {item.icon && (
93
95
  <View style={styles.iconContainer}>
94
96
  <AtomicIcon name={item.icon} size="xs" color="primary" />
@@ -102,4 +104,4 @@ export const InfoGrid: React.FC<InfoGridProps> = ({
102
104
  </View>
103
105
  </View>
104
106
  );
105
- };
107
+ });