@umituz/react-native-design-system 1.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.
Files changed (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +157 -0
  3. package/package.json +43 -0
  4. package/src/index.ts +345 -0
  5. package/src/presentation/atoms/AtomicAvatar.tsx +157 -0
  6. package/src/presentation/atoms/AtomicAvatarGroup.tsx +169 -0
  7. package/src/presentation/atoms/AtomicBadge.tsx +232 -0
  8. package/src/presentation/atoms/AtomicButton.tsx +124 -0
  9. package/src/presentation/atoms/AtomicCard.tsx +112 -0
  10. package/src/presentation/atoms/AtomicChip.tsx +223 -0
  11. package/src/presentation/atoms/AtomicDatePicker.tsx +347 -0
  12. package/src/presentation/atoms/AtomicDivider.tsx +114 -0
  13. package/src/presentation/atoms/AtomicFab.tsx +104 -0
  14. package/src/presentation/atoms/AtomicFilter.tsx +154 -0
  15. package/src/presentation/atoms/AtomicFormError.tsx +105 -0
  16. package/src/presentation/atoms/AtomicIcon.tsx +29 -0
  17. package/src/presentation/atoms/AtomicImage.tsx +149 -0
  18. package/src/presentation/atoms/AtomicInput.tsx +232 -0
  19. package/src/presentation/atoms/AtomicNumberInput.tsx +182 -0
  20. package/src/presentation/atoms/AtomicPicker.tsx +458 -0
  21. package/src/presentation/atoms/AtomicProgress.tsx +143 -0
  22. package/src/presentation/atoms/AtomicSearchBar.tsx +114 -0
  23. package/src/presentation/atoms/AtomicSkeleton.tsx +146 -0
  24. package/src/presentation/atoms/AtomicSort.tsx +145 -0
  25. package/src/presentation/atoms/AtomicSwitch.tsx +166 -0
  26. package/src/presentation/atoms/AtomicText.tsx +50 -0
  27. package/src/presentation/atoms/AtomicTextArea.tsx +198 -0
  28. package/src/presentation/atoms/AtomicTouchable.tsx +233 -0
  29. package/src/presentation/atoms/fab/styles/fabStyles.ts +69 -0
  30. package/src/presentation/atoms/fab/types/index.ts +88 -0
  31. package/src/presentation/atoms/filter/styles/filterStyles.ts +32 -0
  32. package/src/presentation/atoms/filter/types/index.ts +89 -0
  33. package/src/presentation/atoms/index.ts +378 -0
  34. package/src/presentation/atoms/input/hooks/useInputState.ts +15 -0
  35. package/src/presentation/atoms/input/styles/inputStyles.ts +66 -0
  36. package/src/presentation/atoms/input/types/index.ts +25 -0
  37. package/src/presentation/atoms/picker/styles/pickerStyles.ts +200 -0
  38. package/src/presentation/atoms/picker/types/index.ts +40 -0
  39. package/src/presentation/atoms/touchable/styles/touchableStyles.ts +71 -0
  40. package/src/presentation/atoms/touchable/types/index.ts +162 -0
  41. package/src/presentation/hooks/useAppDesignTokens.ts +78 -0
  42. package/src/presentation/hooks/useResponsive.ts +180 -0
  43. package/src/presentation/loading/index.ts +40 -0
  44. package/src/presentation/loading/presentation/components/LoadingSpinner.tsx +116 -0
  45. package/src/presentation/loading/presentation/components/LoadingState.tsx +200 -0
  46. package/src/presentation/loading/presentation/hooks/useLoading.ts +100 -0
  47. package/src/presentation/molecules/AtomicConfirmationModal.tsx +263 -0
  48. package/src/presentation/molecules/EmptyState.tsx +130 -0
  49. package/src/presentation/molecules/FormField.tsx +128 -0
  50. package/src/presentation/molecules/GridContainer.tsx +124 -0
  51. package/src/presentation/molecules/IconContainer.tsx +94 -0
  52. package/src/presentation/molecules/LanguageSwitcher.tsx +42 -0
  53. package/src/presentation/molecules/ListItem.tsx +36 -0
  54. package/src/presentation/molecules/ScreenHeader.tsx +140 -0
  55. package/src/presentation/molecules/SearchBar.tsx +85 -0
  56. package/src/presentation/molecules/SectionCard.tsx +74 -0
  57. package/src/presentation/molecules/SectionContainer.tsx +106 -0
  58. package/src/presentation/molecules/SectionHeader.tsx +125 -0
  59. package/src/presentation/molecules/confirmation-modal/styles/confirmationModalStyles.ts +133 -0
  60. package/src/presentation/molecules/confirmation-modal/types/index.ts +107 -0
  61. package/src/presentation/molecules/index.ts +42 -0
  62. package/src/presentation/molecules/languageswitcher/config/languageSwitcherConfig.ts +5 -0
  63. package/src/presentation/molecules/languageswitcher/hooks/useLanguageNavigation.ts +15 -0
  64. package/src/presentation/molecules/listitem/styles/listItemStyles.ts +19 -0
  65. package/src/presentation/molecules/listitem/types/index.ts +17 -0
  66. package/src/presentation/organisms/AppHeader.tsx +136 -0
  67. package/src/presentation/organisms/FormContainer.tsx +180 -0
  68. package/src/presentation/organisms/ScreenLayout.tsx +209 -0
  69. package/src/presentation/organisms/index.ts +25 -0
  70. package/src/presentation/tokens/AppDesignTokens.ts +57 -0
  71. package/src/presentation/tokens/commonStyles.ts +253 -0
  72. package/src/presentation/tokens/core/BaseTokens.ts +394 -0
  73. package/src/presentation/tokens/core/ColorPalette.ts +398 -0
  74. package/src/presentation/tokens/core/TokenFactory.ts +120 -0
  75. package/src/presentation/utils/platformConstants.ts +124 -0
  76. package/src/presentation/utils/responsive.ts +516 -0
  77. package/src/presentation/utils/variants/compound.ts +29 -0
  78. package/src/presentation/utils/variants/core.ts +39 -0
  79. package/src/presentation/utils/variants/helpers.ts +13 -0
  80. package/src/presentation/utils/variants.ts +3 -0
@@ -0,0 +1,112 @@
1
+ import React from 'react';
2
+ import { Card as PaperCard } from 'react-native-paper';
3
+ import { StyleProp, ViewStyle } from 'react-native';
4
+ import Animated, { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated';
5
+ import { Pressable } from 'react-native';
6
+
7
+ export type AtomicCardVariant = 'flat' | 'elevated' | 'outlined';
8
+ export type AtomicCardPadding = 'none' | 'sm' | 'md' | 'lg' | 'xl';
9
+
10
+ export interface AtomicCardProps {
11
+ variant?: AtomicCardVariant;
12
+ padding?: AtomicCardPadding;
13
+ onPress?: () => void;
14
+ disabled?: boolean;
15
+ style?: StyleProp<ViewStyle>;
16
+ children?: React.ReactNode;
17
+ testID?: string;
18
+ }
19
+
20
+ const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
21
+
22
+ export const AtomicCard: React.FC<AtomicCardProps> = ({
23
+ variant = 'elevated',
24
+ padding = 'md',
25
+ onPress,
26
+ disabled = false,
27
+ style,
28
+ children,
29
+ testID,
30
+ }) => {
31
+ // Animation for tap feedback
32
+ const scale = useSharedValue(1);
33
+
34
+ const animatedStyle = useAnimatedStyle(() => ({
35
+ transform: [{ scale: scale.value }],
36
+ }));
37
+
38
+ const handlePressIn = () => {
39
+ if (onPress && !disabled) {
40
+ scale.value = withSpring(0.98, {
41
+ damping: 15,
42
+ stiffness: 150,
43
+ });
44
+ }
45
+ };
46
+
47
+ const handlePressOut = () => {
48
+ scale.value = withSpring(1);
49
+ };
50
+
51
+ const handlePress = () => {
52
+ if (onPress && !disabled) {
53
+ onPress();
54
+ }
55
+ };
56
+
57
+ // Map variants to Paper modes
58
+ const getPaperMode = (): 'elevated' | 'outlined' | 'contained' => {
59
+ switch (variant) {
60
+ case 'elevated':
61
+ return 'elevated';
62
+ case 'outlined':
63
+ return 'outlined';
64
+ case 'flat':
65
+ return 'contained';
66
+ default:
67
+ return 'elevated';
68
+ }
69
+ };
70
+
71
+ // Map padding to actual values
72
+ const getContentStyle = () => {
73
+ const paddingMap = {
74
+ none: 0,
75
+ sm: 8,
76
+ md: 16,
77
+ lg: 24,
78
+ xl: 32,
79
+ };
80
+ const paddingValue = paddingMap[padding];
81
+ return { padding: paddingValue };
82
+ };
83
+
84
+ const cardContent = (
85
+ <PaperCard
86
+ mode={getPaperMode()}
87
+ style={style}
88
+ testID={testID}
89
+ >
90
+ <PaperCard.Content style={getContentStyle()}>
91
+ {children}
92
+ </PaperCard.Content>
93
+ </PaperCard>
94
+ );
95
+
96
+ // If onPress provided, wrap with animated pressable
97
+ if (onPress && !disabled) {
98
+ return (
99
+ <AnimatedPressable
100
+ style={animatedStyle}
101
+ onPressIn={handlePressIn}
102
+ onPressOut={handlePressOut}
103
+ onPress={handlePress}
104
+ >
105
+ {cardContent}
106
+ </AnimatedPressable>
107
+ );
108
+ }
109
+
110
+ // Otherwise just return static card
111
+ return cardContent;
112
+ };
@@ -0,0 +1,223 @@
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 '../hooks/useAppDesignTokens';
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;
@@ -0,0 +1,347 @@
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 (via timezone domain)
11
+ * - Timezone-aware (respects device timezone)
12
+ * - Automatic language integration (changes with app language)
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
+ * - iOS: Opens modal with spinner wheel, requires "Done" button
34
+ * - Android: Opens native dialog, auto-closes on selection
35
+ *
36
+ * @module AtomicDatePicker
37
+ */
38
+
39
+ import React, { useState } from 'react';
40
+ import {
41
+ View,
42
+ Text,
43
+ TouchableOpacity,
44
+ StyleSheet,
45
+ Modal,
46
+ useWindowDimensions,
47
+ } from 'react-native';
48
+ import DateTimePicker, { DateTimePickerEvent } from '@react-native-community/datetimepicker';
49
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
50
+ import { useTimezone } from '@domains/timezone';
51
+ import { useAppDesignTokens } from '../hooks/useAppDesignTokens';
52
+ import { useResponsive } from '../hooks/useResponsive';
53
+ import { AtomicIcon, type AtomicIconColor } from './AtomicIcon';
54
+
55
+ /**
56
+ * Props for AtomicDatePicker component
57
+ */
58
+ export interface AtomicDatePickerProps {
59
+ /** Selected date value */
60
+ value: Date;
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
+ }
80
+
81
+ /**
82
+ * AtomicDatePicker - Universal date/time picker component
83
+ *
84
+ * Wraps @react-native-community/datetimepicker with:
85
+ * - Theme integration
86
+ * - Platform-specific modal handling
87
+ * - Error states
88
+ * - Disabled states
89
+ * - Responsive sizing
90
+ */
91
+ export const AtomicDatePicker: React.FC<AtomicDatePickerProps> = ({
92
+ value,
93
+ onChange,
94
+ label,
95
+ error,
96
+ disabled = false,
97
+ minimumDate,
98
+ maximumDate,
99
+ mode = 'date',
100
+ placeholder = 'Select date',
101
+ testID,
102
+ }) => {
103
+ const tokens = useAppDesignTokens();
104
+ const { height } = useWindowDimensions();
105
+ const insets = useSafeAreaInsets();
106
+ const { isTabletDevice } = useResponsive();
107
+ const { formatDate: formatDateTz, formatTime } = useTimezone();
108
+ const [show, setShow] = useState(false);
109
+
110
+ /**
111
+ * Handle date/time change
112
+ * Universal handler that works across all platforms
113
+ * Note: event.type can be 'set', 'dismissed', or 'neutralButtonPressed'
114
+ */
115
+ const handleChange = (event: DateTimePickerEvent, selectedDate?: Date) => {
116
+ // Close picker when user confirms or dismisses
117
+ // iOS: Stays open until "Done" button (handled separately)
118
+ // Android/Web: Auto-closes on selection
119
+ if (event.type === 'set' || event.type === 'dismissed') {
120
+ setShow(false);
121
+ }
122
+
123
+ // Update value only if date was selected (not dismissed)
124
+ if (event.type === 'set' && selectedDate) {
125
+ onChange(selectedDate);
126
+ }
127
+ };
128
+
129
+ /**
130
+ * Format date based on mode
131
+ * Uses timezone domain for locale-aware and timezone-aware formatting
132
+ * Automatically respects user's selected language and device timezone
133
+ */
134
+ const formatDate = (date: Date): string => {
135
+ if (mode === 'time') {
136
+ // Format time only (e.g., "02:30 PM" in English, "14:30" in Turkish)
137
+ return formatTime(date);
138
+ }
139
+ if (mode === 'datetime') {
140
+ // Format date + time (e.g., "January 15, 2024 02:30 PM")
141
+ const dateStr = formatDateTz(date, {
142
+ year: 'numeric',
143
+ month: 'short',
144
+ day: 'numeric',
145
+ });
146
+ const timeStr = formatTime(date);
147
+ return `${dateStr} ${timeStr}`;
148
+ }
149
+ // Format date only (e.g., "January 15, 2024" in English, "15 Ocak 2024" in Turkish)
150
+ return formatDateTz(date);
151
+ };
152
+
153
+ /**
154
+ * Determine icon color based on state
155
+ */
156
+ const getIconColor = (): AtomicIconColor => {
157
+ if (disabled) return 'secondary';
158
+ if (error) return 'error';
159
+ return 'primary';
160
+ };
161
+
162
+ const styles = getStyles(tokens, height, insets);
163
+
164
+ return (
165
+ <View style={styles.container} testID={testID}>
166
+ {label && (
167
+ <Text style={styles.label} testID={testID ? `${testID}-label` : undefined}>
168
+ {label}
169
+ </Text>
170
+ )}
171
+
172
+ <TouchableOpacity
173
+ style={[
174
+ styles.button,
175
+ error ? styles.buttonError : undefined,
176
+ disabled ? styles.buttonDisabled : undefined,
177
+ ]}
178
+ onPress={() => !disabled && setShow(true)}
179
+ disabled={disabled}
180
+ testID={testID ? `${testID}-button` : undefined}
181
+ accessibilityLabel={label || placeholder}
182
+ accessibilityRole="button"
183
+ accessibilityState={{ disabled }}
184
+ >
185
+ <AtomicIcon
186
+ name="calendar"
187
+ color={getIconColor()}
188
+ size="md"
189
+ />
190
+ <Text
191
+ style={[
192
+ styles.text,
193
+ disabled ? styles.textDisabled : undefined,
194
+ error ? styles.textError : undefined,
195
+ ]}
196
+ >
197
+ {value ? formatDate(value) : placeholder}
198
+ </Text>
199
+ </TouchableOpacity>
200
+
201
+ {error && (
202
+ <Text style={styles.errorText} testID={testID ? `${testID}-error` : undefined}>
203
+ {error}
204
+ </Text>
205
+ )}
206
+
207
+ {/* Universal DatePicker - Works across iOS, Android, Web */}
208
+ {show && (
209
+ <Modal
210
+ transparent
211
+ animationType={isTabletDevice ? 'fade' : 'slide'}
212
+ visible={show}
213
+ onRequestClose={() => setShow(false)}
214
+ >
215
+ <TouchableOpacity
216
+ style={styles.modalOverlay}
217
+ activeOpacity={1}
218
+ onPress={() => setShow(false)}
219
+ accessibilityLabel="Close date picker"
220
+ accessibilityRole="button"
221
+ >
222
+ <View
223
+ style={styles.pickerContainer}
224
+ onStartShouldSetResponder={() => true}
225
+ >
226
+ <DateTimePicker
227
+ value={value || new Date()}
228
+ mode={mode}
229
+ display="spinner"
230
+ onChange={handleChange}
231
+ minimumDate={minimumDate}
232
+ maximumDate={maximumDate}
233
+ testID={testID ? `${testID}-picker` : undefined}
234
+ />
235
+ <View style={styles.buttonContainer}>
236
+ <TouchableOpacity
237
+ style={styles.doneButton}
238
+ onPress={() => setShow(false)}
239
+ testID={testID ? `${testID}-done` : undefined}
240
+ accessibilityLabel="Done"
241
+ accessibilityRole="button"
242
+ >
243
+ <Text style={styles.doneText}>Done</Text>
244
+ </TouchableOpacity>
245
+ </View>
246
+ </View>
247
+ </TouchableOpacity>
248
+ </Modal>
249
+ )}
250
+ </View>
251
+ );
252
+ };
253
+
254
+ /**
255
+ * Get component styles based on design tokens
256
+ */
257
+ const getStyles = (
258
+ tokens: ReturnType<typeof useAppDesignTokens>,
259
+ height: number,
260
+ insets: { top: number; bottom: number; left: number; right: number },
261
+ ) => {
262
+ // Responsive button sizing based on device height
263
+ const buttonMinWidth = height <= 667 ? Math.min(height * 0.25, 150) : 200;
264
+
265
+ return StyleSheet.create({
266
+ container: {
267
+ marginBottom: tokens.spacing.md,
268
+ },
269
+ label: {
270
+ fontSize: tokens.typography.bodyMedium.fontSize,
271
+ fontWeight: tokens.typography.semibold,
272
+ color: tokens.colors.textPrimary,
273
+ marginBottom: tokens.spacing.sm,
274
+ },
275
+ button: {
276
+ flexDirection: 'row',
277
+ alignItems: 'center',
278
+ backgroundColor: tokens.colors.surface,
279
+ borderWidth: 1,
280
+ borderColor: tokens.colors.border,
281
+ borderRadius: tokens.borders.radius.lg,
282
+ paddingHorizontal: tokens.spacing.md,
283
+ paddingVertical: tokens.spacing.md,
284
+ gap: tokens.spacing.sm,
285
+ minHeight: 48, // Apple HIG minimum touch target
286
+ },
287
+ buttonError: {
288
+ borderColor: tokens.colors.error,
289
+ borderWidth: tokens.borders.width.medium,
290
+ },
291
+ buttonDisabled: {
292
+ backgroundColor: tokens.colors.surfaceDisabled,
293
+ opacity: tokens.opacity.disabled,
294
+ },
295
+ text: {
296
+ flex: 1,
297
+ fontSize: tokens.typography.bodyLarge.fontSize,
298
+ color: tokens.colors.textPrimary,
299
+ },
300
+ textDisabled: {
301
+ color: tokens.colors.textDisabled,
302
+ },
303
+ textError: {
304
+ color: tokens.colors.error,
305
+ },
306
+ errorText: {
307
+ fontSize: tokens.typography.bodySmall.fontSize,
308
+ color: tokens.colors.error,
309
+ marginTop: tokens.spacing.xs,
310
+ marginLeft: tokens.spacing.xs,
311
+ },
312
+ modalOverlay: {
313
+ flex: 1,
314
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
315
+ justifyContent: 'flex-start',
316
+ },
317
+ pickerContainer: {
318
+ backgroundColor: tokens.colors.surface,
319
+ borderTopLeftRadius: tokens.borders.radius.xl,
320
+ borderTopRightRadius: tokens.borders.radius.xl,
321
+ paddingTop: tokens.spacing.lg,
322
+ paddingBottom: Math.max(
323
+ insets.bottom + tokens.spacing.md,
324
+ tokens.spacing.xl,
325
+ ),
326
+ },
327
+ buttonContainer: {
328
+ alignItems: 'center',
329
+ marginTop: tokens.spacing.md,
330
+ paddingHorizontal: tokens.spacing.lg,
331
+ },
332
+ doneButton: {
333
+ backgroundColor: tokens.colors.primary,
334
+ paddingHorizontal: tokens.spacing.xl,
335
+ paddingVertical: tokens.spacing.sm,
336
+ borderRadius: tokens.borders.radius.lg,
337
+ minWidth: buttonMinWidth,
338
+ alignItems: 'center',
339
+ minHeight: 44, // Apple HIG minimum touch target
340
+ },
341
+ doneText: {
342
+ color: tokens.colors.onPrimary,
343
+ fontSize: tokens.typography.bodyLarge.fontSize,
344
+ fontWeight: tokens.typography.semibold,
345
+ },
346
+ });
347
+ };