@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.
- package/package.json +31 -8
- package/src/atoms/AtomicAvatar.tsx +69 -40
- package/src/atoms/AtomicDatePicker.tsx +6 -6
- package/src/atoms/AtomicSpinner.tsx +24 -22
- package/src/atoms/AtomicText.tsx +32 -27
- package/src/atoms/AtomicTextArea.tsx +17 -15
- package/src/atoms/EmptyState.tsx +44 -41
- package/src/atoms/button/AtomicButton.tsx +8 -9
- package/src/atoms/card/AtomicCard.tsx +26 -8
- package/src/atoms/datepicker/components/DatePickerButton.tsx +8 -8
- package/src/atoms/datepicker/components/DatePickerModal.tsx +7 -7
- package/src/atoms/fab/styles/fabStyles.ts +0 -21
- package/src/atoms/icon/index.ts +6 -20
- package/src/atoms/picker/components/PickerModal.tsx +24 -4
- package/src/atoms/skeleton/AtomicSkeleton.tsx +9 -11
- package/src/carousel/Carousel.tsx +43 -20
- package/src/carousel/carouselCalculations.ts +12 -9
- package/src/carousel/index.ts +0 -1
- package/src/device/detection/iPadDetection.ts +5 -14
- package/src/device/infrastructure/services/DeviceFeatureService.ts +89 -9
- package/src/device/infrastructure/services/DeviceInfoService.ts +33 -0
- package/src/device/infrastructure/services/UserFriendlyIdService.ts +8 -6
- package/src/device/infrastructure/utils/__tests__/stringUtils.test.ts +56 -20
- package/src/device/infrastructure/utils/nativeModuleUtils.ts +16 -2
- package/src/device/infrastructure/utils/stringUtils.ts +51 -5
- package/src/filesystem/domain/utils/FileUtils.ts +5 -1
- package/src/image/domain/utils/ImageUtils.ts +6 -0
- package/src/layouts/AppHeader/AppHeader.tsx +13 -3
- package/src/layouts/Container/Container.tsx +19 -1
- package/src/layouts/FormLayout/FormLayout.tsx +20 -1
- package/src/layouts/Grid/Grid.tsx +34 -4
- package/src/layouts/ScreenHeader/ScreenHeader.tsx +4 -0
- package/src/layouts/ScreenLayout/ScreenLayout.tsx +42 -3
- package/src/molecules/SearchBar/SearchBar.tsx +27 -23
- package/src/molecules/action-footer/ActionFooter.tsx +32 -31
- package/src/molecules/alerts/AlertService.ts +60 -15
- package/src/molecules/avatar/Avatar.tsx +3 -3
- package/src/molecules/avatar/AvatarGroup.tsx +7 -7
- package/src/molecules/bottom-sheet/components/BottomSheet.tsx +3 -3
- package/src/molecules/calendar/infrastructure/utils/DateUtilities.ts +12 -1
- package/src/molecules/calendar/presentation/components/CalendarDayCell.tsx +48 -32
- package/src/molecules/info-grid/InfoGrid.tsx +5 -3
- package/src/organisms/FormContainer.tsx +11 -1
- package/src/tanstack/domain/utils/ErrorHelpers.ts +2 -2
- package/src/tanstack/domain/utils/MetricsCalculator.ts +6 -1
- package/src/theme/core/colors/ColorUtils.ts +7 -4
- package/src/utils/formatters/stringFormatter.ts +18 -3
- package/src/utils/index.ts +6 -4
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
63
|
-
], [config.size, borderRadius,
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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(() =>
|
|
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
|
-
|
|
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={
|
|
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={
|
|
86
|
+
<View style={todayDotStyle} />
|
|
75
87
|
)}
|
|
76
88
|
|
|
77
|
-
{visibleEvents.map((event) =>
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
?
|
|
85
|
-
:
|
|
86
|
-
|
|
87
|
-
|
|
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,
|
|
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
|
+
});
|