@umituz/react-native-design-system 1.14.0 → 2.0.0
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 +26 -19
- package/src/atoms/AtomicAvatar.tsx +161 -0
- package/src/atoms/AtomicButton.tsx +241 -0
- package/src/atoms/AtomicChip.tsx +226 -0
- package/src/atoms/AtomicDatePicker.tsx +255 -0
- package/src/atoms/AtomicFab.tsx +99 -0
- package/src/atoms/AtomicIcon.tsx +149 -0
- package/src/atoms/AtomicInput.tsx +308 -0
- package/src/atoms/AtomicPicker.tsx +310 -0
- package/src/atoms/AtomicProgress.tsx +149 -0
- package/src/atoms/AtomicText.tsx +55 -0
- package/src/atoms/__tests__/AtomicButton.test.tsx +107 -0
- package/src/atoms/__tests__/AtomicIcon.test.tsx +110 -0
- package/src/atoms/__tests__/AtomicInput.test.tsx +195 -0
- package/src/atoms/datepicker/components/DatePickerButton.tsx +112 -0
- package/src/atoms/datepicker/components/DatePickerModal.tsx +143 -0
- package/src/atoms/fab/styles/fabStyles.ts +98 -0
- package/src/atoms/fab/types/index.ts +88 -0
- package/src/atoms/index.ts +70 -0
- package/src/atoms/input/hooks/useInputState.ts +63 -0
- package/src/atoms/input/styles/inputStylesHelper.ts +120 -0
- package/src/atoms/picker/components/PickerChips.tsx +57 -0
- package/src/atoms/picker/components/PickerModal.tsx +214 -0
- package/src/atoms/picker/styles/pickerStyles.ts +223 -0
- package/src/atoms/picker/types/index.ts +42 -0
- package/src/index.ts +148 -79
- package/src/molecules/ConfirmationModal.tsx +42 -0
- package/src/molecules/ConfirmationModalContent.tsx +87 -0
- package/src/molecules/ConfirmationModalMain.tsx +91 -0
- package/src/molecules/FormField.tsx +155 -0
- package/src/molecules/IconContainer.tsx +79 -0
- package/src/molecules/ListItem.tsx +35 -0
- package/src/molecules/ScreenHeader.tsx +171 -0
- package/src/molecules/SearchBar.tsx +198 -0
- package/src/molecules/confirmation-modal/components.tsx +94 -0
- package/src/molecules/confirmation-modal/index.ts +7 -0
- package/src/molecules/confirmation-modal/styles/confirmationModalStyles.ts +133 -0
- package/src/molecules/confirmation-modal/types/index.ts +41 -0
- package/src/molecules/confirmation-modal/useConfirmationModal.ts +50 -0
- package/src/molecules/index.ts +19 -0
- package/src/molecules/listitem/index.ts +6 -0
- package/src/molecules/listitem/styles/listItemStyles.ts +37 -0
- package/src/molecules/listitem/types/index.ts +21 -0
- package/src/organisms/AppHeader.tsx +136 -0
- package/src/organisms/FormContainer.tsx +169 -0
- package/src/organisms/ScreenLayout.tsx +183 -0
- package/src/organisms/index.ts +31 -0
- package/src/responsive/config.ts +139 -0
- package/src/responsive/deviceDetection.ts +155 -0
- package/src/responsive/gridUtils.ts +79 -0
- package/src/responsive/index.ts +52 -0
- package/src/responsive/platformConstants.ts +98 -0
- package/src/responsive/responsive.ts +61 -0
- package/src/responsive/responsiveLayout.ts +137 -0
- package/src/responsive/responsiveSizing.ts +134 -0
- package/src/responsive/useResponsive.ts +140 -0
- package/src/responsive/validation.ts +158 -0
- package/src/theme/core/BaseTokens.ts +42 -0
- package/src/theme/core/ColorPalette.ts +29 -0
- package/src/theme/core/CustomColors.ts +122 -0
- package/src/theme/core/NavigationTheme.ts +72 -0
- package/src/theme/core/TokenFactory.ts +103 -0
- package/src/theme/core/colors/ColorUtils.ts +53 -0
- package/src/theme/core/colors/DarkColors.ts +146 -0
- package/src/theme/core/colors/LightColors.ts +146 -0
- package/src/theme/core/constants/DesignConstants.ts +31 -0
- package/src/theme/core/themes.ts +118 -0
- package/src/theme/core/tokens/BaseTokens.ts +144 -0
- package/src/theme/core/tokens/Borders.ts +43 -0
- package/src/theme/core/tokens/Sizes.ts +51 -0
- package/src/theme/core/tokens/Spacing.ts +38 -0
- package/src/theme/core/tokens/Typography.ts +143 -0
- package/src/theme/hooks/useAppDesignTokens.ts +45 -0
- package/src/theme/hooks/useCommonStyles.ts +248 -0
- package/src/theme/hooks/useThemedStyles.ts +68 -0
- package/src/theme/index.ts +94 -0
- package/src/theme/infrastructure/globalThemeStore.ts +69 -0
- package/src/theme/infrastructure/storage/ThemeStorage.ts +93 -0
- package/src/theme/infrastructure/stores/themeStore.ts +109 -0
- package/src/typography/__tests__/colorValidationUtils.test.ts +180 -0
- package/src/typography/__tests__/textColorUtils.test.ts +185 -0
- package/src/typography/__tests__/textStyleUtils.test.ts +168 -0
- package/src/typography/domain/entities/TypographyTypes.ts +88 -0
- package/src/typography/index.ts +53 -0
- package/src/typography/presentation/utils/colorValidationUtils.ts +133 -0
- package/src/typography/presentation/utils/textColorUtils.ts +205 -0
- package/src/typography/presentation/utils/textStyleUtils.ts +159 -0
|
@@ -0,0 +1,226 @@
|
|
|
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 '../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
|
+
/** Active opacity for touch feedback */
|
|
60
|
+
activeOpacity?: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// =============================================================================
|
|
64
|
+
// COMPONENT IMPLEMENTATION
|
|
65
|
+
// =============================================================================
|
|
66
|
+
|
|
67
|
+
export const AtomicChip: React.FC<AtomicChipProps> = React.memo(({
|
|
68
|
+
children,
|
|
69
|
+
variant = 'filled',
|
|
70
|
+
size = 'md',
|
|
71
|
+
color = 'primary',
|
|
72
|
+
backgroundColor,
|
|
73
|
+
textColor,
|
|
74
|
+
borderColor,
|
|
75
|
+
leadingIcon,
|
|
76
|
+
trailingIcon,
|
|
77
|
+
clickable = false,
|
|
78
|
+
onPress,
|
|
79
|
+
selected = false,
|
|
80
|
+
disabled = false,
|
|
81
|
+
style,
|
|
82
|
+
testID,
|
|
83
|
+
activeOpacity = 0.7,
|
|
84
|
+
}) => {
|
|
85
|
+
const tokens = useAppDesignTokens();
|
|
86
|
+
|
|
87
|
+
// Size mapping
|
|
88
|
+
const sizeMap = {
|
|
89
|
+
sm: {
|
|
90
|
+
paddingHorizontal: tokens.spacing.sm,
|
|
91
|
+
paddingVertical: tokens.spacing.xs,
|
|
92
|
+
fontSize: tokens.typography.bodySmall.fontSize,
|
|
93
|
+
iconSize: 'xs' as const
|
|
94
|
+
},
|
|
95
|
+
md: {
|
|
96
|
+
paddingHorizontal: tokens.spacing.md,
|
|
97
|
+
paddingVertical: tokens.spacing.sm,
|
|
98
|
+
fontSize: tokens.typography.bodyMedium.fontSize,
|
|
99
|
+
iconSize: 'sm' as const
|
|
100
|
+
},
|
|
101
|
+
lg: {
|
|
102
|
+
paddingHorizontal: tokens.spacing.md,
|
|
103
|
+
paddingVertical: tokens.spacing.sm,
|
|
104
|
+
fontSize: tokens.typography.bodyLarge.fontSize,
|
|
105
|
+
iconSize: 'sm' as const
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const sizeConfig = sizeMap[size];
|
|
110
|
+
|
|
111
|
+
// Color mapping
|
|
112
|
+
const colorMap = {
|
|
113
|
+
primary: {
|
|
114
|
+
filled: { bg: tokens.colors.primary, text: tokens.colors.onPrimary, border: tokens.colors.primary },
|
|
115
|
+
outlined: { bg: 'transparent', text: tokens.colors.primary, border: tokens.colors.primary },
|
|
116
|
+
soft: { bg: tokens.colors.primaryContainer, text: tokens.colors.onPrimaryContainer, border: 'transparent' },
|
|
117
|
+
},
|
|
118
|
+
secondary: {
|
|
119
|
+
filled: { bg: tokens.colors.secondary, text: tokens.colors.onSecondary, border: tokens.colors.secondary },
|
|
120
|
+
outlined: { bg: 'transparent', text: tokens.colors.secondary, border: tokens.colors.secondary },
|
|
121
|
+
soft: { bg: tokens.colors.secondaryContainer, text: tokens.colors.onSecondaryContainer, border: 'transparent' },
|
|
122
|
+
},
|
|
123
|
+
success: {
|
|
124
|
+
filled: { bg: tokens.colors.success, text: tokens.colors.onSuccess, border: tokens.colors.success },
|
|
125
|
+
outlined: { bg: 'transparent', text: tokens.colors.success, border: tokens.colors.success },
|
|
126
|
+
soft: { bg: tokens.colors.successContainer, text: tokens.colors.onSuccessContainer, border: 'transparent' },
|
|
127
|
+
},
|
|
128
|
+
warning: {
|
|
129
|
+
filled: { bg: tokens.colors.warning, text: tokens.colors.onWarning, border: tokens.colors.warning },
|
|
130
|
+
outlined: { bg: 'transparent', text: tokens.colors.warning, border: tokens.colors.warning },
|
|
131
|
+
soft: { bg: tokens.colors.warningContainer, text: tokens.colors.onWarningContainer, border: 'transparent' },
|
|
132
|
+
},
|
|
133
|
+
error: {
|
|
134
|
+
filled: { bg: tokens.colors.error, text: tokens.colors.onError, border: tokens.colors.error },
|
|
135
|
+
outlined: { bg: 'transparent', text: tokens.colors.error, border: tokens.colors.error },
|
|
136
|
+
soft: { bg: tokens.colors.errorContainer, text: tokens.colors.onErrorContainer, border: 'transparent' },
|
|
137
|
+
},
|
|
138
|
+
info: {
|
|
139
|
+
filled: { bg: tokens.colors.info, text: tokens.colors.onInfo, border: tokens.colors.info },
|
|
140
|
+
outlined: { bg: 'transparent', text: tokens.colors.info, border: tokens.colors.info },
|
|
141
|
+
soft: { bg: tokens.colors.infoContainer, text: tokens.colors.onInfoContainer, border: 'transparent' },
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const colorConfig = colorMap[color][variant];
|
|
146
|
+
|
|
147
|
+
// Apply custom colors if provided
|
|
148
|
+
const finalBackgroundColor = backgroundColor || colorConfig.bg;
|
|
149
|
+
const finalTextColor = textColor || colorConfig.text;
|
|
150
|
+
const finalBorderColor = borderColor || colorConfig.border;
|
|
151
|
+
|
|
152
|
+
// Handle disabled state
|
|
153
|
+
const isDisabled = disabled || (!clickable && !onPress);
|
|
154
|
+
const opacity = isDisabled ? 0.5 : 1;
|
|
155
|
+
|
|
156
|
+
// Handle selected state
|
|
157
|
+
const selectedStyle = selected ? {
|
|
158
|
+
borderWidth: tokens.borders.width.medium,
|
|
159
|
+
borderColor: tokens.colors.primary,
|
|
160
|
+
} : {};
|
|
161
|
+
|
|
162
|
+
const chipStyle: ViewStyle = {
|
|
163
|
+
flexDirection: 'row',
|
|
164
|
+
alignItems: 'center',
|
|
165
|
+
justifyContent: 'center',
|
|
166
|
+
paddingHorizontal: sizeConfig.paddingHorizontal,
|
|
167
|
+
paddingVertical: sizeConfig.paddingVertical,
|
|
168
|
+
backgroundColor: finalBackgroundColor,
|
|
169
|
+
borderRadius: tokens.borders.radius.xl,
|
|
170
|
+
borderWidth: variant === 'outlined' ? 1 : 0,
|
|
171
|
+
borderColor: finalBorderColor,
|
|
172
|
+
opacity,
|
|
173
|
+
...selectedStyle,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const textStyle = {
|
|
177
|
+
fontSize: sizeConfig.fontSize,
|
|
178
|
+
fontWeight: tokens.typography.medium,
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const iconColor = finalTextColor;
|
|
182
|
+
|
|
183
|
+
const content = (
|
|
184
|
+
<View style={[chipStyle, style]} testID={testID}>
|
|
185
|
+
{leadingIcon && (
|
|
186
|
+
<AtomicIcon
|
|
187
|
+
name={leadingIcon}
|
|
188
|
+
size={sizeConfig.iconSize}
|
|
189
|
+
customColor={iconColor}
|
|
190
|
+
style={{ marginRight: tokens.spacing.xs }}
|
|
191
|
+
/>
|
|
192
|
+
)}
|
|
193
|
+
<AtomicText
|
|
194
|
+
type="labelMedium"
|
|
195
|
+
color={finalTextColor}
|
|
196
|
+
style={textStyle}
|
|
197
|
+
>
|
|
198
|
+
{children}
|
|
199
|
+
</AtomicText>
|
|
200
|
+
{trailingIcon && (
|
|
201
|
+
<AtomicIcon
|
|
202
|
+
name={trailingIcon}
|
|
203
|
+
size={sizeConfig.iconSize}
|
|
204
|
+
customColor={iconColor}
|
|
205
|
+
style={{ marginLeft: tokens.spacing.xs }}
|
|
206
|
+
/>
|
|
207
|
+
)}
|
|
208
|
+
</View>
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
if (clickable && onPress && !disabled) {
|
|
212
|
+
return (
|
|
213
|
+
<TouchableOpacity onPress={onPress} activeOpacity={activeOpacity}>
|
|
214
|
+
{content}
|
|
215
|
+
</TouchableOpacity>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return content;
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// =============================================================================
|
|
223
|
+
// EXPORTS
|
|
224
|
+
// =============================================================================
|
|
225
|
+
|
|
226
|
+
export default AtomicChip;
|
|
@@ -0,0 +1,255 @@
|
|
|
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, useMemo } from 'react';
|
|
41
|
+
import {
|
|
42
|
+
View,
|
|
43
|
+
Text,
|
|
44
|
+
StyleSheet,
|
|
45
|
+
Platform,
|
|
46
|
+
type StyleProp,
|
|
47
|
+
type ViewStyle,
|
|
48
|
+
} from 'react-native';
|
|
49
|
+
import DateTimePicker, { DateTimePickerEvent } from '@react-native-community/datetimepicker';
|
|
50
|
+
import { useAppDesignTokens } from '../theme';
|
|
51
|
+
import { AtomicText } from './AtomicText';
|
|
52
|
+
import { DatePickerModal } from './datepicker/components/DatePickerModal';
|
|
53
|
+
import { DatePickerButton } from './datepicker/components/DatePickerButton';
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Props for AtomicDatePicker component
|
|
57
|
+
*/
|
|
58
|
+
export interface AtomicDatePickerProps {
|
|
59
|
+
/** Selected date value */
|
|
60
|
+
value: Date | null;
|
|
61
|
+
/** Callback when date changes */
|
|
62
|
+
onChange: (date: Date) => void;
|
|
63
|
+
/** Optional label displayed above picker */
|
|
64
|
+
label?: string;
|
|
65
|
+
/** Optional error message displayed below picker */
|
|
66
|
+
error?: string;
|
|
67
|
+
/** Disable picker interaction */
|
|
68
|
+
disabled?: boolean;
|
|
69
|
+
/** Minimum selectable date */
|
|
70
|
+
minimumDate?: Date;
|
|
71
|
+
/** Maximum selectable date */
|
|
72
|
+
maximumDate?: Date;
|
|
73
|
+
/** Picker mode - date, time, or datetime (iOS only) */
|
|
74
|
+
mode?: 'date' | 'time' | 'datetime';
|
|
75
|
+
/** Placeholder text when no value selected */
|
|
76
|
+
placeholder?: string;
|
|
77
|
+
/** Optional test ID for E2E testing */
|
|
78
|
+
testID?: string;
|
|
79
|
+
/** Optional container style */
|
|
80
|
+
style?: StyleProp<ViewStyle>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* AtomicDatePicker - Universal date/time picker component
|
|
85
|
+
*
|
|
86
|
+
* Wraps @react-native-community/datetimepicker with:
|
|
87
|
+
* - Theme integration
|
|
88
|
+
* - Platform-specific modal handling
|
|
89
|
+
* - Error states
|
|
90
|
+
* - Disabled states
|
|
91
|
+
* - Responsive sizing
|
|
92
|
+
*/
|
|
93
|
+
export const AtomicDatePicker: React.FC<AtomicDatePickerProps> = ({
|
|
94
|
+
value,
|
|
95
|
+
onChange,
|
|
96
|
+
label,
|
|
97
|
+
error,
|
|
98
|
+
disabled = false,
|
|
99
|
+
minimumDate,
|
|
100
|
+
maximumDate,
|
|
101
|
+
mode = 'date',
|
|
102
|
+
placeholder = 'Select date',
|
|
103
|
+
testID,
|
|
104
|
+
style,
|
|
105
|
+
}) => {
|
|
106
|
+
const tokens = useAppDesignTokens();
|
|
107
|
+
const [showPicker, setShowPicker] = useState(false);
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Handle date/time change in picker
|
|
111
|
+
* On Android, directly apply the change. On iOS, show picker in modal and apply on confirm.
|
|
112
|
+
*/
|
|
113
|
+
const handleChange = (event: DateTimePickerEvent, selectedDate?: Date) => {
|
|
114
|
+
if (Platform.OS === 'android') {
|
|
115
|
+
setShowPicker(false);
|
|
116
|
+
if (event.type === 'set' && selectedDate) {
|
|
117
|
+
onChange(selectedDate);
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
// iOS: Update value while picker is open
|
|
121
|
+
if (event.type === 'set' && selectedDate) {
|
|
122
|
+
onChange(selectedDate);
|
|
123
|
+
}
|
|
124
|
+
// iOS: Close on dismiss (swipe down)
|
|
125
|
+
if (event.type === 'dismissed') {
|
|
126
|
+
setShowPicker(false);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Handle open - show native picker
|
|
133
|
+
*/
|
|
134
|
+
const handleOpen = () => {
|
|
135
|
+
if (Platform.OS === 'android') {
|
|
136
|
+
setShowPicker(true);
|
|
137
|
+
} else {
|
|
138
|
+
// iOS: Show picker inline
|
|
139
|
+
setShowPicker(true);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Format date based on mode
|
|
145
|
+
* Uses native Date formatting (locale-aware)
|
|
146
|
+
*/
|
|
147
|
+
const formatDate = useMemo(() => (date: Date): string => {
|
|
148
|
+
if (mode === 'time') {
|
|
149
|
+
return date.toLocaleTimeString([], {
|
|
150
|
+
hour: '2-digit',
|
|
151
|
+
minute: '2-digit'
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
if (mode === 'datetime') {
|
|
155
|
+
const dateStr = date.toLocaleDateString([], {
|
|
156
|
+
year: 'numeric',
|
|
157
|
+
month: 'short',
|
|
158
|
+
day: 'numeric',
|
|
159
|
+
});
|
|
160
|
+
const timeStr = date.toLocaleTimeString([], {
|
|
161
|
+
hour: '2-digit',
|
|
162
|
+
minute: '2-digit'
|
|
163
|
+
});
|
|
164
|
+
return `${dateStr} ${timeStr}`;
|
|
165
|
+
}
|
|
166
|
+
return date.toLocaleDateString([], {
|
|
167
|
+
year: 'numeric',
|
|
168
|
+
month: 'long',
|
|
169
|
+
day: 'numeric',
|
|
170
|
+
});
|
|
171
|
+
}, [mode]);
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Get display text for the button
|
|
175
|
+
*/
|
|
176
|
+
const displayText = useMemo(() => {
|
|
177
|
+
if (!value) return placeholder;
|
|
178
|
+
return formatDate(value);
|
|
179
|
+
}, [value, placeholder, formatDate]);
|
|
180
|
+
|
|
181
|
+
const styles = getStyles(tokens);
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<View style={[styles.container, style]} testID={testID}>
|
|
185
|
+
{label && (
|
|
186
|
+
<AtomicText style={styles.label} testID={testID ? `${testID}-label` : undefined}>
|
|
187
|
+
{label}
|
|
188
|
+
</AtomicText>
|
|
189
|
+
)}
|
|
190
|
+
|
|
191
|
+
<DatePickerButton
|
|
192
|
+
onPress={handleOpen}
|
|
193
|
+
disabled={disabled}
|
|
194
|
+
displayText={displayText}
|
|
195
|
+
hasValue={!!value}
|
|
196
|
+
error={!!error}
|
|
197
|
+
testID={testID ? `${testID}-button` : undefined}
|
|
198
|
+
/>
|
|
199
|
+
|
|
200
|
+
{error && (
|
|
201
|
+
<AtomicText style={styles.errorText} testID={testID ? `${testID}-error` : undefined}>
|
|
202
|
+
{error}
|
|
203
|
+
</AtomicText>
|
|
204
|
+
)}
|
|
205
|
+
|
|
206
|
+
{/* iOS Modal */}
|
|
207
|
+
<DatePickerModal
|
|
208
|
+
visible={Platform.OS === 'ios' && showPicker}
|
|
209
|
+
onClose={() => setShowPicker(false)}
|
|
210
|
+
onDateChange={handleChange}
|
|
211
|
+
currentDate={value ?? new Date()}
|
|
212
|
+
mode={mode}
|
|
213
|
+
minimumDate={minimumDate}
|
|
214
|
+
maximumDate={maximumDate}
|
|
215
|
+
testID={testID}
|
|
216
|
+
/>
|
|
217
|
+
|
|
218
|
+
{/* Android Picker */}
|
|
219
|
+
{Platform.OS === 'android' && showPicker && (
|
|
220
|
+
<DateTimePicker
|
|
221
|
+
value={value ?? new Date()}
|
|
222
|
+
mode={mode}
|
|
223
|
+
display="default"
|
|
224
|
+
onChange={handleChange}
|
|
225
|
+
minimumDate={minimumDate}
|
|
226
|
+
maximumDate={maximumDate}
|
|
227
|
+
testID={testID ? `${testID}-picker` : undefined}
|
|
228
|
+
/>
|
|
229
|
+
)}
|
|
230
|
+
</View>
|
|
231
|
+
);
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Get component styles based on design tokens
|
|
236
|
+
*/
|
|
237
|
+
const getStyles = (tokens: ReturnType<typeof useAppDesignTokens>) => {
|
|
238
|
+
return StyleSheet.create({
|
|
239
|
+
container: {
|
|
240
|
+
marginBottom: tokens.spacing.md,
|
|
241
|
+
},
|
|
242
|
+
label: {
|
|
243
|
+
fontSize: tokens.typography.bodyMedium.fontSize,
|
|
244
|
+
fontWeight: tokens.typography.semibold,
|
|
245
|
+
color: tokens.colors.onSurface,
|
|
246
|
+
marginBottom: tokens.spacing.sm,
|
|
247
|
+
},
|
|
248
|
+
errorText: {
|
|
249
|
+
fontSize: tokens.typography.bodySmall.fontSize,
|
|
250
|
+
color: tokens.colors.error,
|
|
251
|
+
marginTop: tokens.spacing.xs,
|
|
252
|
+
marginLeft: tokens.spacing.xs,
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { TouchableOpacity, StyleSheet } from 'react-native';
|
|
3
|
+
import { useAppDesignTokens } from '../theme';
|
|
4
|
+
import { useResponsive } from '../responsive';
|
|
5
|
+
import { AtomicIcon } from './AtomicIcon';
|
|
6
|
+
import { AtomicFabProps } from './fab/types';
|
|
7
|
+
import {
|
|
8
|
+
FAB_SIZES,
|
|
9
|
+
getFabVariants,
|
|
10
|
+
getFabIconSize,
|
|
11
|
+
getFabBorder,
|
|
12
|
+
} from './fab/styles/fabStyles';
|
|
13
|
+
|
|
14
|
+
export type { FabSize, FabVariant, FabVariantConfig, FabSizeConfig, AtomicFabProps } from './fab/types';
|
|
15
|
+
export { FAB_SIZES, getFabVariants, getFabIconSize, getFabBorder };
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* AtomicFab - Floating Action Button Component
|
|
19
|
+
*
|
|
20
|
+
* A Material Design 3 compliant FAB component for primary actions.
|
|
21
|
+
* Follows CLAUDE.md standards for responsive positioning.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```tsx
|
|
25
|
+
* // IMPORTANT: FAB must be used at screen level, NOT inside ScrollView
|
|
26
|
+
* <ScreenLayout>
|
|
27
|
+
* <ScrollView>
|
|
28
|
+
* {/* Your content *\/}
|
|
29
|
+
* </ScrollView>
|
|
30
|
+
* <AtomicFab
|
|
31
|
+
* icon="add"
|
|
32
|
+
* onPress={handleAddItem}
|
|
33
|
+
* variant="primary"
|
|
34
|
+
* size="md"
|
|
35
|
+
* />
|
|
36
|
+
* </ScreenLayout>
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* Features:
|
|
40
|
+
* - Material Design 3 sizes (sm: 40px, md: 56px, lg: 72px)
|
|
41
|
+
* - Three variants: primary, secondary, surface
|
|
42
|
+
* - Responsive positioning (above tab bar, safe area aware)
|
|
43
|
+
* - Disabled state with opacity
|
|
44
|
+
* - Theme-aware colors from design tokens
|
|
45
|
+
* - Border for depth (no shadows per CLAUDE.md)
|
|
46
|
+
*/
|
|
47
|
+
export const AtomicFab: React.FC<AtomicFabProps> = ({
|
|
48
|
+
icon,
|
|
49
|
+
onPress,
|
|
50
|
+
variant = 'primary',
|
|
51
|
+
size = 'md',
|
|
52
|
+
disabled = false,
|
|
53
|
+
style,
|
|
54
|
+
testID,
|
|
55
|
+
accessibilityLabel,
|
|
56
|
+
activeOpacity = 0.7,
|
|
57
|
+
}) => {
|
|
58
|
+
const tokens = useAppDesignTokens();
|
|
59
|
+
const responsive = useResponsive();
|
|
60
|
+
const isDisabled = disabled;
|
|
61
|
+
|
|
62
|
+
// Get configurations
|
|
63
|
+
const sizeConfig = FAB_SIZES[size as 'sm' | 'md' | 'lg'];
|
|
64
|
+
const variants = getFabVariants(tokens);
|
|
65
|
+
const variantConfig = variants[variant as 'primary' | 'secondary' | 'surface'];
|
|
66
|
+
const iconSize = getFabIconSize(size as 'sm' | 'md' | 'lg');
|
|
67
|
+
|
|
68
|
+
// Combine styles
|
|
69
|
+
const fabStyle = StyleSheet.flatten([
|
|
70
|
+
{
|
|
71
|
+
position: 'absolute' as const,
|
|
72
|
+
bottom: responsive.fabPosition.bottom,
|
|
73
|
+
right: responsive.fabPosition.right,
|
|
74
|
+
width: sizeConfig.width,
|
|
75
|
+
height: sizeConfig.height,
|
|
76
|
+
borderRadius: sizeConfig.borderRadius,
|
|
77
|
+
backgroundColor: variantConfig.backgroundColor,
|
|
78
|
+
alignItems: 'center' as const,
|
|
79
|
+
justifyContent: 'center' as const,
|
|
80
|
+
},
|
|
81
|
+
getFabBorder(tokens),
|
|
82
|
+
isDisabled ? { opacity: tokens.opacity.disabled } : undefined,
|
|
83
|
+
style, // Custom style override
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<TouchableOpacity
|
|
88
|
+
style={fabStyle}
|
|
89
|
+
onPress={onPress}
|
|
90
|
+
disabled={isDisabled}
|
|
91
|
+
activeOpacity={activeOpacity}
|
|
92
|
+
testID={testID}
|
|
93
|
+
accessibilityLabel={accessibilityLabel || `${icon} floating action button`}
|
|
94
|
+
accessibilityRole="button"
|
|
95
|
+
>
|
|
96
|
+
<AtomicIcon name={icon} size={iconSize} customColor={variantConfig.iconColor} />
|
|
97
|
+
</TouchableOpacity>
|
|
98
|
+
);
|
|
99
|
+
};
|