@umituz/react-native-design-system 1.5.36 → 1.5.38
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/README.md +2 -2
- package/package.json +7 -5
- package/src/index.ts +29 -221
- package/src/presentation/organisms/AppHeader.tsx +3 -5
- package/src/presentation/tokens/commonStyles.ts +1 -1
- package/src/presentation/atoms/AtomicAvatar.tsx +0 -157
- package/src/presentation/atoms/AtomicAvatarGroup.tsx +0 -169
- package/src/presentation/atoms/AtomicBadge.tsx +0 -232
- package/src/presentation/atoms/AtomicButton.tsx +0 -236
- package/src/presentation/atoms/AtomicCard.tsx +0 -107
- package/src/presentation/atoms/AtomicChip.tsx +0 -223
- package/src/presentation/atoms/AtomicDatePicker.tsx +0 -347
- package/src/presentation/atoms/AtomicDivider.tsx +0 -114
- package/src/presentation/atoms/AtomicFab.tsx +0 -98
- package/src/presentation/atoms/AtomicFilter.tsx +0 -154
- package/src/presentation/atoms/AtomicFormError.tsx +0 -105
- package/src/presentation/atoms/AtomicIcon.tsx +0 -40
- package/src/presentation/atoms/AtomicImage.tsx +0 -149
- package/src/presentation/atoms/AtomicInput.tsx +0 -363
- package/src/presentation/atoms/AtomicNumberInput.tsx +0 -182
- package/src/presentation/atoms/AtomicPicker.tsx +0 -458
- package/src/presentation/atoms/AtomicProgress.tsx +0 -139
- package/src/presentation/atoms/AtomicSearchBar.tsx +0 -114
- package/src/presentation/atoms/AtomicSort.tsx +0 -145
- package/src/presentation/atoms/AtomicSwitch.tsx +0 -166
- package/src/presentation/atoms/AtomicText.tsx +0 -55
- package/src/presentation/atoms/AtomicTextArea.tsx +0 -313
- package/src/presentation/atoms/AtomicTouchable.tsx +0 -209
- package/src/presentation/atoms/fab/styles/fabStyles.ts +0 -69
- package/src/presentation/atoms/fab/types/index.ts +0 -82
- package/src/presentation/atoms/filter/styles/filterStyles.ts +0 -32
- package/src/presentation/atoms/filter/types/index.ts +0 -89
- package/src/presentation/atoms/index.ts +0 -366
- package/src/presentation/atoms/input/hooks/useInputState.ts +0 -15
- package/src/presentation/atoms/input/styles/inputStyles.ts +0 -66
- package/src/presentation/atoms/input/types/index.ts +0 -25
- package/src/presentation/atoms/picker/styles/pickerStyles.ts +0 -207
- package/src/presentation/atoms/picker/types/index.ts +0 -40
- package/src/presentation/atoms/touchable/styles/touchableStyles.ts +0 -62
- package/src/presentation/atoms/touchable/types/index.ts +0 -155
- package/src/presentation/hooks/useResponsive.ts +0 -180
- package/src/presentation/molecules/AtomicConfirmationModal.tsx +0 -243
- package/src/presentation/molecules/EmptyState.tsx +0 -130
- package/src/presentation/molecules/FormField.tsx +0 -128
- package/src/presentation/molecules/GridContainer.tsx +0 -124
- package/src/presentation/molecules/IconContainer.tsx +0 -94
- package/src/presentation/molecules/ListItem.tsx +0 -36
- package/src/presentation/molecules/ScreenHeader.tsx +0 -140
- package/src/presentation/molecules/SearchBar.tsx +0 -85
- package/src/presentation/molecules/SectionCard.tsx +0 -74
- package/src/presentation/molecules/SectionContainer.tsx +0 -106
- package/src/presentation/molecules/SectionHeader.tsx +0 -125
- package/src/presentation/molecules/confirmation-modal/styles/confirmationModalStyles.ts +0 -133
- package/src/presentation/molecules/confirmation-modal/types/index.ts +0 -105
- package/src/presentation/molecules/index.ts +0 -41
- package/src/presentation/molecules/listitem/styles/listItemStyles.ts +0 -19
- package/src/presentation/molecules/listitem/types/index.ts +0 -17
- package/src/presentation/organisms/FormContainer.tsx +0 -180
- package/src/presentation/organisms/ScreenLayout.tsx +0 -171
- package/src/presentation/organisms/index.ts +0 -25
- package/src/presentation/utils/platformConstants.ts +0 -124
- package/src/presentation/utils/responsive.ts +0 -516
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { View, StyleProp, ViewStyle, Pressable } from 'react-native';
|
|
3
|
-
import { useAppDesignTokens } from '@umituz/react-native-theme';
|
|
4
|
-
|
|
5
|
-
export type AtomicCardVariant = 'flat' | 'elevated' | 'outlined';
|
|
6
|
-
export type AtomicCardPadding = 'none' | 'sm' | 'md' | 'lg' | 'xl';
|
|
7
|
-
|
|
8
|
-
export interface AtomicCardProps {
|
|
9
|
-
variant?: AtomicCardVariant;
|
|
10
|
-
padding?: AtomicCardPadding;
|
|
11
|
-
onPress?: () => void;
|
|
12
|
-
disabled?: boolean;
|
|
13
|
-
style?: StyleProp<ViewStyle>;
|
|
14
|
-
children?: React.ReactNode;
|
|
15
|
-
testID?: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
export const AtomicCard: React.FC<AtomicCardProps> = ({
|
|
20
|
-
variant = 'elevated',
|
|
21
|
-
padding = 'md',
|
|
22
|
-
onPress,
|
|
23
|
-
disabled = false,
|
|
24
|
-
style,
|
|
25
|
-
children,
|
|
26
|
-
testID,
|
|
27
|
-
}) => {
|
|
28
|
-
const tokens = useAppDesignTokens();
|
|
29
|
-
|
|
30
|
-
const handlePress = () => {
|
|
31
|
-
if (onPress && !disabled) {
|
|
32
|
-
onPress();
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
// Map padding to token values
|
|
37
|
-
const getPaddingValue = (): number => {
|
|
38
|
-
const paddingMap = {
|
|
39
|
-
none: 0,
|
|
40
|
-
sm: tokens.spacing.sm,
|
|
41
|
-
md: tokens.spacing.md,
|
|
42
|
-
lg: tokens.spacing.lg,
|
|
43
|
-
xl: tokens.spacing.xl,
|
|
44
|
-
};
|
|
45
|
-
return paddingMap[padding];
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
// Get variant styles
|
|
49
|
-
const getVariantStyle = (): ViewStyle => {
|
|
50
|
-
const baseStyle: ViewStyle = {
|
|
51
|
-
backgroundColor: tokens.colors.surface,
|
|
52
|
-
borderRadius: tokens.borders.radius.md,
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
switch (variant) {
|
|
56
|
-
case 'elevated':
|
|
57
|
-
return {
|
|
58
|
-
...baseStyle,
|
|
59
|
-
borderWidth: 1,
|
|
60
|
-
borderColor: tokens.colors.border,
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
case 'outlined':
|
|
64
|
-
return {
|
|
65
|
-
...baseStyle,
|
|
66
|
-
borderWidth: 1,
|
|
67
|
-
borderColor: tokens.colors.border,
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
case 'flat':
|
|
71
|
-
return {
|
|
72
|
-
...baseStyle,
|
|
73
|
-
borderWidth: 0,
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
default:
|
|
77
|
-
return baseStyle;
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
const cardStyle: StyleProp<ViewStyle> = [
|
|
82
|
-
getVariantStyle(),
|
|
83
|
-
{
|
|
84
|
-
padding: getPaddingValue(),
|
|
85
|
-
opacity: disabled ? 0.5 : 1,
|
|
86
|
-
},
|
|
87
|
-
style,
|
|
88
|
-
];
|
|
89
|
-
|
|
90
|
-
const cardContent = (
|
|
91
|
-
<View style={cardStyle} testID={testID}>
|
|
92
|
-
{children}
|
|
93
|
-
</View>
|
|
94
|
-
);
|
|
95
|
-
|
|
96
|
-
// If onPress provided, wrap with pressable
|
|
97
|
-
if (onPress && !disabled) {
|
|
98
|
-
return (
|
|
99
|
-
<Pressable onPress={handlePress}>
|
|
100
|
-
{cardContent}
|
|
101
|
-
</Pressable>
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Otherwise just return static card
|
|
106
|
-
return cardContent;
|
|
107
|
-
};
|
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AtomicChip - Universal Chip/Tag Component
|
|
3
|
-
*
|
|
4
|
-
* Displays small tags, labels, or status indicators
|
|
5
|
-
* Theme: {{THEME_NAME}} ({{CATEGORY}} category)
|
|
6
|
-
*
|
|
7
|
-
* Atomic Design Level: ATOM
|
|
8
|
-
* Purpose: Tag and label display
|
|
9
|
-
*
|
|
10
|
-
* Usage:
|
|
11
|
-
* - Category tags
|
|
12
|
-
* - Status indicators
|
|
13
|
-
* - Filter chips
|
|
14
|
-
* - Skill labels
|
|
15
|
-
* - Badge displays
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import React from 'react';
|
|
19
|
-
import { View, StyleSheet, ViewStyle, TouchableOpacity } from 'react-native';
|
|
20
|
-
import { AtomicText } from './AtomicText';
|
|
21
|
-
import { AtomicIcon } from './AtomicIcon';
|
|
22
|
-
import { useAppDesignTokens } from '@umituz/react-native-theme';
|
|
23
|
-
|
|
24
|
-
// =============================================================================
|
|
25
|
-
// TYPE DEFINITIONS
|
|
26
|
-
// =============================================================================
|
|
27
|
-
|
|
28
|
-
export interface AtomicChipProps {
|
|
29
|
-
/** Text content of the chip */
|
|
30
|
-
children: React.ReactNode;
|
|
31
|
-
/** Chip variant */
|
|
32
|
-
variant?: 'filled' | 'outlined' | 'soft';
|
|
33
|
-
/** Chip size */
|
|
34
|
-
size?: 'sm' | 'md' | 'lg';
|
|
35
|
-
/** Chip color theme */
|
|
36
|
-
color?: 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info';
|
|
37
|
-
/** Custom background color */
|
|
38
|
-
backgroundColor?: string;
|
|
39
|
-
/** Custom text color */
|
|
40
|
-
textColor?: string;
|
|
41
|
-
/** Custom border color */
|
|
42
|
-
borderColor?: string;
|
|
43
|
-
/** Leading icon */
|
|
44
|
-
leadingIcon?: string;
|
|
45
|
-
/** Trailing icon */
|
|
46
|
-
trailingIcon?: string;
|
|
47
|
-
/** Whether the chip is clickable */
|
|
48
|
-
clickable?: boolean;
|
|
49
|
-
/** Click handler */
|
|
50
|
-
onPress?: () => void;
|
|
51
|
-
/** Whether the chip is selected */
|
|
52
|
-
selected?: boolean;
|
|
53
|
-
/** Whether the chip is disabled */
|
|
54
|
-
disabled?: boolean;
|
|
55
|
-
/** Style overrides */
|
|
56
|
-
style?: ViewStyle;
|
|
57
|
-
/** Test ID for testing */
|
|
58
|
-
testID?: string;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// =============================================================================
|
|
62
|
-
// COMPONENT IMPLEMENTATION
|
|
63
|
-
// =============================================================================
|
|
64
|
-
|
|
65
|
-
export const AtomicChip: React.FC<AtomicChipProps> = ({
|
|
66
|
-
children,
|
|
67
|
-
variant = 'filled',
|
|
68
|
-
size = 'md',
|
|
69
|
-
color = 'primary',
|
|
70
|
-
backgroundColor,
|
|
71
|
-
textColor,
|
|
72
|
-
borderColor,
|
|
73
|
-
leadingIcon,
|
|
74
|
-
trailingIcon,
|
|
75
|
-
clickable = false,
|
|
76
|
-
onPress,
|
|
77
|
-
selected = false,
|
|
78
|
-
disabled = false,
|
|
79
|
-
style,
|
|
80
|
-
testID,
|
|
81
|
-
}) => {
|
|
82
|
-
const tokens = useAppDesignTokens();
|
|
83
|
-
|
|
84
|
-
// Size mapping
|
|
85
|
-
const sizeMap = {
|
|
86
|
-
sm: {
|
|
87
|
-
paddingHorizontal: tokens.spacing.sm,
|
|
88
|
-
paddingVertical: tokens.spacing.xs,
|
|
89
|
-
fontSize: tokens.typography.bodySmall.fontSize,
|
|
90
|
-
iconSize: 'xs' as const
|
|
91
|
-
},
|
|
92
|
-
md: {
|
|
93
|
-
paddingHorizontal: tokens.spacing.md,
|
|
94
|
-
paddingVertical: tokens.spacing.sm,
|
|
95
|
-
fontSize: tokens.typography.bodyMedium.fontSize,
|
|
96
|
-
iconSize: 'sm' as const
|
|
97
|
-
},
|
|
98
|
-
lg: {
|
|
99
|
-
paddingHorizontal: tokens.spacing.md,
|
|
100
|
-
paddingVertical: tokens.spacing.sm,
|
|
101
|
-
fontSize: tokens.typography.bodyLarge.fontSize,
|
|
102
|
-
iconSize: 'sm' as const
|
|
103
|
-
},
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
const sizeConfig = sizeMap[size];
|
|
107
|
-
|
|
108
|
-
// Color mapping
|
|
109
|
-
const colorMap = {
|
|
110
|
-
primary: {
|
|
111
|
-
filled: { bg: tokens.colors.primary, text: tokens.colors.onPrimary, border: tokens.colors.primary },
|
|
112
|
-
outlined: { bg: 'transparent', text: tokens.colors.primary, border: tokens.colors.primary },
|
|
113
|
-
soft: { bg: tokens.colors.primaryContainer, text: tokens.colors.onPrimaryContainer, border: 'transparent' },
|
|
114
|
-
},
|
|
115
|
-
secondary: {
|
|
116
|
-
filled: { bg: tokens.colors.secondary, text: tokens.colors.onSecondary, border: tokens.colors.secondary },
|
|
117
|
-
outlined: { bg: 'transparent', text: tokens.colors.secondary, border: tokens.colors.secondary },
|
|
118
|
-
soft: { bg: tokens.colors.secondaryContainer, text: tokens.colors.onSecondaryContainer, border: 'transparent' },
|
|
119
|
-
},
|
|
120
|
-
success: {
|
|
121
|
-
filled: { bg: tokens.colors.success, text: tokens.colors.onSuccess, border: tokens.colors.success },
|
|
122
|
-
outlined: { bg: 'transparent', text: tokens.colors.success, border: tokens.colors.success },
|
|
123
|
-
soft: { bg: tokens.colors.successContainer, text: tokens.colors.onSuccessContainer, border: 'transparent' },
|
|
124
|
-
},
|
|
125
|
-
warning: {
|
|
126
|
-
filled: { bg: tokens.colors.warning, text: tokens.colors.onWarning, border: tokens.colors.warning },
|
|
127
|
-
outlined: { bg: 'transparent', text: tokens.colors.warning, border: tokens.colors.warning },
|
|
128
|
-
soft: { bg: tokens.colors.warningContainer, text: tokens.colors.onWarningContainer, border: 'transparent' },
|
|
129
|
-
},
|
|
130
|
-
error: {
|
|
131
|
-
filled: { bg: tokens.colors.error, text: tokens.colors.onError, border: tokens.colors.error },
|
|
132
|
-
outlined: { bg: 'transparent', text: tokens.colors.error, border: tokens.colors.error },
|
|
133
|
-
soft: { bg: tokens.colors.errorContainer, text: tokens.colors.onErrorContainer, border: 'transparent' },
|
|
134
|
-
},
|
|
135
|
-
info: {
|
|
136
|
-
filled: { bg: tokens.colors.info, text: tokens.colors.onInfo, border: tokens.colors.info },
|
|
137
|
-
outlined: { bg: 'transparent', text: tokens.colors.info, border: tokens.colors.info },
|
|
138
|
-
soft: { bg: tokens.colors.infoContainer, text: tokens.colors.onInfoContainer, border: 'transparent' },
|
|
139
|
-
},
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
const colorConfig = colorMap[color][variant];
|
|
143
|
-
|
|
144
|
-
// Apply custom colors if provided
|
|
145
|
-
const finalBackgroundColor = backgroundColor || colorConfig.bg;
|
|
146
|
-
const finalTextColor = textColor || colorConfig.text;
|
|
147
|
-
const finalBorderColor = borderColor || colorConfig.border;
|
|
148
|
-
|
|
149
|
-
// Handle disabled state
|
|
150
|
-
const isDisabled = disabled || (!clickable && !onPress);
|
|
151
|
-
const opacity = isDisabled ? 0.5 : 1;
|
|
152
|
-
|
|
153
|
-
// Handle selected state
|
|
154
|
-
const selectedStyle = selected ? {
|
|
155
|
-
borderWidth: tokens.borders.width.medium,
|
|
156
|
-
borderColor: tokens.colors.primary,
|
|
157
|
-
} : {};
|
|
158
|
-
|
|
159
|
-
const chipStyle: ViewStyle = {
|
|
160
|
-
flexDirection: 'row',
|
|
161
|
-
alignItems: 'center',
|
|
162
|
-
justifyContent: 'center',
|
|
163
|
-
paddingHorizontal: sizeConfig.paddingHorizontal,
|
|
164
|
-
paddingVertical: sizeConfig.paddingVertical,
|
|
165
|
-
backgroundColor: finalBackgroundColor,
|
|
166
|
-
borderRadius: tokens.borders.radius.xl,
|
|
167
|
-
borderWidth: variant === 'outlined' ? 1 : 0,
|
|
168
|
-
borderColor: finalBorderColor,
|
|
169
|
-
opacity,
|
|
170
|
-
...selectedStyle,
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
const textStyle = {
|
|
174
|
-
fontSize: sizeConfig.fontSize,
|
|
175
|
-
fontWeight: tokens.typography.medium,
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
const iconColor = finalTextColor;
|
|
179
|
-
|
|
180
|
-
const content = (
|
|
181
|
-
<View style={[chipStyle, style]} testID={testID}>
|
|
182
|
-
{leadingIcon && (
|
|
183
|
-
<AtomicIcon
|
|
184
|
-
name={leadingIcon}
|
|
185
|
-
size={sizeConfig.iconSize}
|
|
186
|
-
customColor={iconColor}
|
|
187
|
-
style={{ marginRight: tokens.spacing.xs }}
|
|
188
|
-
/>
|
|
189
|
-
)}
|
|
190
|
-
<AtomicText
|
|
191
|
-
type="labelMedium"
|
|
192
|
-
color={finalTextColor}
|
|
193
|
-
style={textStyle}
|
|
194
|
-
>
|
|
195
|
-
{children}
|
|
196
|
-
</AtomicText>
|
|
197
|
-
{trailingIcon && (
|
|
198
|
-
<AtomicIcon
|
|
199
|
-
name={trailingIcon}
|
|
200
|
-
size={sizeConfig.iconSize}
|
|
201
|
-
customColor={iconColor}
|
|
202
|
-
style={{ marginLeft: tokens.spacing.xs }}
|
|
203
|
-
/>
|
|
204
|
-
)}
|
|
205
|
-
</View>
|
|
206
|
-
);
|
|
207
|
-
|
|
208
|
-
if (clickable && onPress && !disabled) {
|
|
209
|
-
return (
|
|
210
|
-
<TouchableOpacity onPress={onPress} activeOpacity={0.7}>
|
|
211
|
-
{content}
|
|
212
|
-
</TouchableOpacity>
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return content;
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
// =============================================================================
|
|
220
|
-
// EXPORTS
|
|
221
|
-
// =============================================================================
|
|
222
|
-
|
|
223
|
-
export default AtomicChip;
|
|
@@ -1,347 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AtomicDatePicker Component
|
|
3
|
-
*
|
|
4
|
-
* A reusable date picker component that wraps the native date picker
|
|
5
|
-
* with consistent styling and behavior across platforms.
|
|
6
|
-
*
|
|
7
|
-
* Features:
|
|
8
|
-
* - Platform-specific native pickers (iOS wheel, Android dialog)
|
|
9
|
-
* - Consistent styling with design tokens
|
|
10
|
-
* - Locale-aware date/time formatting (native Date methods)
|
|
11
|
-
* - Timezone-aware (respects device timezone)
|
|
12
|
-
* - Automatic language integration (native locale support)
|
|
13
|
-
* - Optional label and error states
|
|
14
|
-
* - Minimum and maximum date constraints
|
|
15
|
-
* - Disabled state support
|
|
16
|
-
* - Theme-aware styling
|
|
17
|
-
* - Proper keyboard avoidance on iOS
|
|
18
|
-
*
|
|
19
|
-
* Usage:
|
|
20
|
-
* ```tsx
|
|
21
|
-
* const [selectedDate, setSelectedDate] = useState(new Date());
|
|
22
|
-
*
|
|
23
|
-
* <AtomicDatePicker
|
|
24
|
-
* value={selectedDate}
|
|
25
|
-
* onChange={setSelectedDate}
|
|
26
|
-
* label="Birth Date"
|
|
27
|
-
* minimumDate={new Date(1900, 0, 1)}
|
|
28
|
-
* maximumDate={new Date()}
|
|
29
|
-
* />
|
|
30
|
-
* ```
|
|
31
|
-
*
|
|
32
|
-
* Platform Behavior:
|
|
33
|
-
* - Opens bottom sheet from bottom with spinner wheel
|
|
34
|
-
* - Requires "Done" button to confirm selection
|
|
35
|
-
* - Can be dismissed by swiping down or tapping backdrop
|
|
36
|
-
*
|
|
37
|
-
* @module AtomicDatePicker
|
|
38
|
-
*/
|
|
39
|
-
|
|
40
|
-
import React, { useState, useEffect } from 'react';
|
|
41
|
-
import {
|
|
42
|
-
View,
|
|
43
|
-
Text,
|
|
44
|
-
TouchableOpacity,
|
|
45
|
-
StyleSheet,
|
|
46
|
-
useWindowDimensions,
|
|
47
|
-
type StyleProp,
|
|
48
|
-
type ViewStyle,
|
|
49
|
-
} from 'react-native';
|
|
50
|
-
import DateTimePicker, { DateTimePickerEvent } from '@react-native-community/datetimepicker';
|
|
51
|
-
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
52
|
-
import { useAppDesignTokens } from '@umituz/react-native-theme';
|
|
53
|
-
import { AtomicIcon, type AtomicIconColor } from './AtomicIcon';
|
|
54
|
-
// @ts-ignore - Peer dependency, types may not be available during typecheck
|
|
55
|
-
import { BottomSheetModal, useBottomSheetModal } from '@umituz/react-native-bottom-sheet';
|
|
56
|
-
import { AtomicButton } from './AtomicButton';
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Props for AtomicDatePicker component
|
|
60
|
-
*/
|
|
61
|
-
export interface AtomicDatePickerProps {
|
|
62
|
-
/** Selected date value */
|
|
63
|
-
value: Date | null;
|
|
64
|
-
/** Callback when date changes */
|
|
65
|
-
onChange: (date: Date) => void;
|
|
66
|
-
/** Optional label displayed above picker */
|
|
67
|
-
label?: string;
|
|
68
|
-
/** Optional error message displayed below picker */
|
|
69
|
-
error?: string;
|
|
70
|
-
/** Disable picker interaction */
|
|
71
|
-
disabled?: boolean;
|
|
72
|
-
/** Minimum selectable date */
|
|
73
|
-
minimumDate?: Date;
|
|
74
|
-
/** Maximum selectable date */
|
|
75
|
-
maximumDate?: Date;
|
|
76
|
-
/** Picker mode - date, time, or datetime (iOS only) */
|
|
77
|
-
mode?: 'date' | 'time' | 'datetime';
|
|
78
|
-
/** Placeholder text when no value selected */
|
|
79
|
-
placeholder?: string;
|
|
80
|
-
/** Optional test ID for E2E testing */
|
|
81
|
-
testID?: string;
|
|
82
|
-
/** Optional container style */
|
|
83
|
-
style?: StyleProp<ViewStyle>;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* AtomicDatePicker - Universal date/time picker component
|
|
88
|
-
*
|
|
89
|
-
* Wraps @react-native-community/datetimepicker with:
|
|
90
|
-
* - Theme integration
|
|
91
|
-
* - Platform-specific modal handling
|
|
92
|
-
* - Error states
|
|
93
|
-
* - Disabled states
|
|
94
|
-
* - Responsive sizing
|
|
95
|
-
*/
|
|
96
|
-
export const AtomicDatePicker: React.FC<AtomicDatePickerProps> = ({
|
|
97
|
-
value,
|
|
98
|
-
onChange,
|
|
99
|
-
label,
|
|
100
|
-
error,
|
|
101
|
-
disabled = false,
|
|
102
|
-
minimumDate,
|
|
103
|
-
maximumDate,
|
|
104
|
-
mode = 'date',
|
|
105
|
-
placeholder = 'Select date',
|
|
106
|
-
testID,
|
|
107
|
-
style,
|
|
108
|
-
}) => {
|
|
109
|
-
const tokens = useAppDesignTokens();
|
|
110
|
-
const { height } = useWindowDimensions();
|
|
111
|
-
const insets = useSafeAreaInsets();
|
|
112
|
-
const { modalRef, present, dismiss } = useBottomSheetModal();
|
|
113
|
-
const [tempDate, setTempDate] = useState<Date>(value ?? new Date());
|
|
114
|
-
|
|
115
|
-
// Update tempDate when value prop changes
|
|
116
|
-
useEffect(() => {
|
|
117
|
-
if (value) {
|
|
118
|
-
setTempDate(value);
|
|
119
|
-
}
|
|
120
|
-
}, [value]);
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Handle date/time change in picker
|
|
124
|
-
* Updates temporary date state (not final until Done is pressed)
|
|
125
|
-
*/
|
|
126
|
-
const handleChange = (event: DateTimePickerEvent, selectedDate?: Date) => {
|
|
127
|
-
if (event.type === 'set' && selectedDate) {
|
|
128
|
-
setTempDate(selectedDate);
|
|
129
|
-
}
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Handle Done button - apply selected date and close sheet
|
|
134
|
-
*/
|
|
135
|
-
const handleDone = () => {
|
|
136
|
-
onChange(tempDate);
|
|
137
|
-
dismiss();
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Handle open - reset temp date to current value
|
|
142
|
-
*/
|
|
143
|
-
const handleOpen = () => {
|
|
144
|
-
setTempDate(value ?? new Date());
|
|
145
|
-
present();
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Format date based on mode
|
|
150
|
-
* Uses native Date formatting (locale-aware)
|
|
151
|
-
*/
|
|
152
|
-
const formatDate = (date: Date): string => {
|
|
153
|
-
if (mode === 'time') {
|
|
154
|
-
// Format time only
|
|
155
|
-
return date.toLocaleTimeString([], {
|
|
156
|
-
hour: '2-digit',
|
|
157
|
-
minute: '2-digit'
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
if (mode === 'datetime') {
|
|
161
|
-
// Format date + time
|
|
162
|
-
const dateStr = date.toLocaleDateString([], {
|
|
163
|
-
year: 'numeric',
|
|
164
|
-
month: 'short',
|
|
165
|
-
day: 'numeric',
|
|
166
|
-
});
|
|
167
|
-
const timeStr = date.toLocaleTimeString([], {
|
|
168
|
-
hour: '2-digit',
|
|
169
|
-
minute: '2-digit'
|
|
170
|
-
});
|
|
171
|
-
return `${dateStr} ${timeStr}`;
|
|
172
|
-
}
|
|
173
|
-
// Format date only
|
|
174
|
-
return date.toLocaleDateString([], {
|
|
175
|
-
year: 'numeric',
|
|
176
|
-
month: 'long',
|
|
177
|
-
day: 'numeric',
|
|
178
|
-
});
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Determine icon color based on state
|
|
183
|
-
*/
|
|
184
|
-
const getIconColor = (): AtomicIconColor => {
|
|
185
|
-
if (disabled) return 'secondary';
|
|
186
|
-
if (error) return 'error';
|
|
187
|
-
return 'primary';
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
const styles = getStyles(tokens, height, insets);
|
|
191
|
-
|
|
192
|
-
return (
|
|
193
|
-
<View style={[styles.container, style]} testID={testID}>
|
|
194
|
-
{label && (
|
|
195
|
-
<Text style={styles.label} testID={testID ? `${testID}-label` : undefined}>
|
|
196
|
-
{label}
|
|
197
|
-
</Text>
|
|
198
|
-
)}
|
|
199
|
-
|
|
200
|
-
<TouchableOpacity
|
|
201
|
-
style={[
|
|
202
|
-
styles.button,
|
|
203
|
-
error ? styles.buttonError : undefined,
|
|
204
|
-
disabled ? styles.buttonDisabled : undefined,
|
|
205
|
-
]}
|
|
206
|
-
onPress={handleOpen}
|
|
207
|
-
disabled={disabled}
|
|
208
|
-
testID={testID ? `${testID}-button` : undefined}
|
|
209
|
-
accessibilityLabel={label || placeholder}
|
|
210
|
-
accessibilityRole="button"
|
|
211
|
-
accessibilityState={{ disabled }}
|
|
212
|
-
>
|
|
213
|
-
<AtomicIcon
|
|
214
|
-
name="Calendar"
|
|
215
|
-
color={getIconColor()}
|
|
216
|
-
size="md"
|
|
217
|
-
/>
|
|
218
|
-
<Text
|
|
219
|
-
style={[
|
|
220
|
-
styles.text,
|
|
221
|
-
disabled ? styles.textDisabled : undefined,
|
|
222
|
-
error ? styles.textError : undefined,
|
|
223
|
-
]}
|
|
224
|
-
>
|
|
225
|
-
{value ? formatDate(value) : placeholder}
|
|
226
|
-
</Text>
|
|
227
|
-
</TouchableOpacity>
|
|
228
|
-
|
|
229
|
-
{error && (
|
|
230
|
-
<Text style={styles.errorText} testID={testID ? `${testID}-error` : undefined}>
|
|
231
|
-
{error}
|
|
232
|
-
</Text>
|
|
233
|
-
)}
|
|
234
|
-
|
|
235
|
-
{/* Bottom Sheet DatePicker */}
|
|
236
|
-
<BottomSheetModal
|
|
237
|
-
ref={modalRef}
|
|
238
|
-
preset="medium"
|
|
239
|
-
enableBackdrop
|
|
240
|
-
enablePanDownToClose
|
|
241
|
-
enableHandleIndicator
|
|
242
|
-
onDismiss={() => {
|
|
243
|
-
// Reset temp date when closed without saving
|
|
244
|
-
setTempDate(value ?? new Date());
|
|
245
|
-
}}
|
|
246
|
-
>
|
|
247
|
-
<View style={styles.bottomSheetContent}>
|
|
248
|
-
<DateTimePicker
|
|
249
|
-
value={tempDate}
|
|
250
|
-
mode={mode}
|
|
251
|
-
display="spinner"
|
|
252
|
-
onChange={handleChange}
|
|
253
|
-
minimumDate={minimumDate}
|
|
254
|
-
maximumDate={maximumDate}
|
|
255
|
-
testID={testID ? `${testID}-picker` : undefined}
|
|
256
|
-
/>
|
|
257
|
-
<View style={styles.buttonContainer}>
|
|
258
|
-
<AtomicButton
|
|
259
|
-
title="Done"
|
|
260
|
-
onPress={handleDone}
|
|
261
|
-
variant="primary"
|
|
262
|
-
style={styles.doneButton}
|
|
263
|
-
testID={testID ? `${testID}-done` : undefined}
|
|
264
|
-
/>
|
|
265
|
-
</View>
|
|
266
|
-
</View>
|
|
267
|
-
</BottomSheetModal>
|
|
268
|
-
</View>
|
|
269
|
-
);
|
|
270
|
-
};
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* Get component styles based on design tokens
|
|
274
|
-
*/
|
|
275
|
-
const getStyles = (
|
|
276
|
-
tokens: ReturnType<typeof useAppDesignTokens>,
|
|
277
|
-
height: number,
|
|
278
|
-
insets: { top: number; bottom: number; left: number; right: number },
|
|
279
|
-
) => {
|
|
280
|
-
// Responsive button sizing based on device height
|
|
281
|
-
const buttonMinWidth = height <= 667 ? Math.min(height * 0.25, 150) : 200;
|
|
282
|
-
|
|
283
|
-
return StyleSheet.create({
|
|
284
|
-
container: {
|
|
285
|
-
marginBottom: tokens.spacing.md,
|
|
286
|
-
},
|
|
287
|
-
label: {
|
|
288
|
-
fontSize: tokens.typography.bodyMedium.fontSize,
|
|
289
|
-
fontWeight: tokens.typography.semibold,
|
|
290
|
-
color: tokens.colors.textPrimary,
|
|
291
|
-
marginBottom: tokens.spacing.sm,
|
|
292
|
-
},
|
|
293
|
-
button: {
|
|
294
|
-
flexDirection: 'row',
|
|
295
|
-
alignItems: 'center',
|
|
296
|
-
backgroundColor: tokens.colors.surface,
|
|
297
|
-
borderWidth: 1,
|
|
298
|
-
borderColor: tokens.colors.border,
|
|
299
|
-
borderRadius: tokens.borders.radius.lg,
|
|
300
|
-
paddingHorizontal: tokens.spacing.md,
|
|
301
|
-
paddingVertical: tokens.spacing.md,
|
|
302
|
-
gap: tokens.spacing.sm,
|
|
303
|
-
minHeight: 48, // Apple HIG minimum touch target
|
|
304
|
-
},
|
|
305
|
-
buttonError: {
|
|
306
|
-
borderColor: tokens.colors.error,
|
|
307
|
-
borderWidth: tokens.borders.width.medium,
|
|
308
|
-
},
|
|
309
|
-
buttonDisabled: {
|
|
310
|
-
backgroundColor: tokens.colors.surfaceDisabled,
|
|
311
|
-
opacity: tokens.opacity.disabled,
|
|
312
|
-
},
|
|
313
|
-
text: {
|
|
314
|
-
flex: 1,
|
|
315
|
-
fontSize: tokens.typography.bodyLarge.fontSize,
|
|
316
|
-
color: tokens.colors.textPrimary,
|
|
317
|
-
},
|
|
318
|
-
textDisabled: {
|
|
319
|
-
color: tokens.colors.textDisabled,
|
|
320
|
-
},
|
|
321
|
-
textError: {
|
|
322
|
-
color: tokens.colors.error,
|
|
323
|
-
},
|
|
324
|
-
errorText: {
|
|
325
|
-
fontSize: tokens.typography.bodySmall.fontSize,
|
|
326
|
-
color: tokens.colors.error,
|
|
327
|
-
marginTop: tokens.spacing.xs,
|
|
328
|
-
marginLeft: tokens.spacing.xs,
|
|
329
|
-
},
|
|
330
|
-
bottomSheetContent: {
|
|
331
|
-
paddingHorizontal: tokens.spacing.lg,
|
|
332
|
-
paddingTop: tokens.spacing.md,
|
|
333
|
-
paddingBottom: Math.max(
|
|
334
|
-
insets.bottom + tokens.spacing.md,
|
|
335
|
-
tokens.spacing.xl,
|
|
336
|
-
),
|
|
337
|
-
},
|
|
338
|
-
buttonContainer: {
|
|
339
|
-
alignItems: 'center',
|
|
340
|
-
marginTop: tokens.spacing.lg,
|
|
341
|
-
paddingHorizontal: tokens.spacing.md,
|
|
342
|
-
},
|
|
343
|
-
doneButton: {
|
|
344
|
-
minWidth: buttonMinWidth,
|
|
345
|
-
},
|
|
346
|
-
});
|
|
347
|
-
};
|