@umituz/react-native-design-system 1.5.17 → 1.5.18

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 (65) hide show
  1. package/package.json +9 -8
  2. package/src/index.js +100 -0
  3. package/src/presentation/atoms/AtomicAvatar.js +84 -0
  4. package/src/presentation/atoms/AtomicAvatarGroup.js +82 -0
  5. package/src/presentation/atoms/AtomicBadge.js +167 -0
  6. package/src/presentation/atoms/AtomicButton.js +171 -0
  7. package/src/presentation/atoms/AtomicCard.js +69 -0
  8. package/src/presentation/atoms/AtomicChip.js +130 -0
  9. package/src/presentation/atoms/AtomicDatePicker.js +245 -0
  10. package/src/presentation/atoms/AtomicDivider.js +57 -0
  11. package/src/presentation/atoms/AtomicFab.js +67 -0
  12. package/src/presentation/atoms/AtomicFilter.js +103 -0
  13. package/src/presentation/atoms/AtomicFormError.js +63 -0
  14. package/src/presentation/atoms/AtomicIcon.js +29 -0
  15. package/src/presentation/atoms/AtomicImage.js +91 -0
  16. package/src/presentation/atoms/AtomicInput.js +201 -0
  17. package/src/presentation/atoms/AtomicNumberInput.js +124 -0
  18. package/src/presentation/atoms/AtomicPicker.js +298 -0
  19. package/src/presentation/atoms/AtomicProgress.js +79 -0
  20. package/src/presentation/atoms/AtomicSearchBar.js +45 -0
  21. package/src/presentation/atoms/AtomicSort.js +76 -0
  22. package/src/presentation/atoms/AtomicSwitch.js +103 -0
  23. package/src/presentation/atoms/AtomicText.js +58 -0
  24. package/src/presentation/atoms/AtomicTextArea.js +195 -0
  25. package/src/presentation/atoms/AtomicTouchable.js +137 -0
  26. package/src/presentation/atoms/fab/styles/fabStyles.js +62 -0
  27. package/src/presentation/atoms/fab/types/index.js +1 -0
  28. package/src/presentation/atoms/filter/styles/filterStyles.js +28 -0
  29. package/src/presentation/atoms/filter/types/index.js +1 -0
  30. package/src/presentation/atoms/index.js +145 -0
  31. package/src/presentation/atoms/input/hooks/useInputState.js +12 -0
  32. package/src/presentation/atoms/input/styles/inputStyles.js +58 -0
  33. package/src/presentation/atoms/input/types/index.js +1 -0
  34. package/src/presentation/atoms/picker/styles/pickerStyles.js +176 -0
  35. package/src/presentation/atoms/picker/types/index.js +1 -0
  36. package/src/presentation/atoms/touchable/styles/touchableStyles.js +53 -0
  37. package/src/presentation/atoms/touchable/types/index.js +1 -0
  38. package/src/presentation/hooks/useResponsive.js +81 -0
  39. package/src/presentation/molecules/AtomicConfirmationModal.js +153 -0
  40. package/src/presentation/molecules/EmptyState.js +67 -0
  41. package/src/presentation/molecules/FormField.js +75 -0
  42. package/src/presentation/molecules/GridContainer.js +76 -0
  43. package/src/presentation/molecules/IconContainer.js +59 -0
  44. package/src/presentation/molecules/ListItem.js +23 -0
  45. package/src/presentation/molecules/ScreenHeader.js +93 -0
  46. package/src/presentation/molecules/SearchBar.js +46 -0
  47. package/src/presentation/molecules/SectionCard.js +46 -0
  48. package/src/presentation/molecules/SectionContainer.js +63 -0
  49. package/src/presentation/molecules/SectionHeader.js +72 -0
  50. package/src/presentation/molecules/confirmation-modal/styles/confirmationModalStyles.js +114 -0
  51. package/src/presentation/molecules/confirmation-modal/types/index.js +6 -0
  52. package/src/presentation/molecules/index.js +16 -0
  53. package/src/presentation/molecules/listitem/styles/listItemStyles.js +14 -0
  54. package/src/presentation/molecules/listitem/types/index.js +1 -0
  55. package/src/presentation/organisms/AppHeader.js +77 -0
  56. package/src/presentation/organisms/FormContainer.js +126 -0
  57. package/src/presentation/organisms/ScreenLayout.js +68 -0
  58. package/src/presentation/organisms/index.js +13 -0
  59. package/src/presentation/tokens/commonStyles.js +219 -0
  60. package/src/presentation/utils/platformConstants.js +113 -0
  61. package/src/presentation/utils/responsive.js +451 -0
  62. package/src/presentation/utils/variants/compound.js +15 -0
  63. package/src/presentation/utils/variants/core.js +22 -0
  64. package/src/presentation/utils/variants/helpers.js +9 -0
  65. package/src/presentation/utils/variants.js +3 -0
@@ -0,0 +1,69 @@
1
+ import React from 'react';
2
+ import { View, Pressable } from 'react-native';
3
+ import { useAppDesignTokens } from '@umituz/react-native-theme';
4
+ export const AtomicCard = ({ variant = 'elevated', padding = 'md', onPress, disabled = false, style, children, testID, }) => {
5
+ const tokens = useAppDesignTokens();
6
+ const handlePress = () => {
7
+ if (onPress && !disabled) {
8
+ onPress();
9
+ }
10
+ };
11
+ // Map padding to token values
12
+ const getPaddingValue = () => {
13
+ const paddingMap = {
14
+ none: 0,
15
+ sm: tokens.spacing.sm,
16
+ md: tokens.spacing.md,
17
+ lg: tokens.spacing.lg,
18
+ xl: tokens.spacing.xl,
19
+ };
20
+ return paddingMap[padding];
21
+ };
22
+ // Get variant styles
23
+ const getVariantStyle = () => {
24
+ const baseStyle = {
25
+ backgroundColor: tokens.colors.surface,
26
+ borderRadius: tokens.borders.radius.md,
27
+ };
28
+ switch (variant) {
29
+ case 'elevated':
30
+ return {
31
+ ...baseStyle,
32
+ borderWidth: 1,
33
+ borderColor: tokens.colors.border,
34
+ };
35
+ case 'outlined':
36
+ return {
37
+ ...baseStyle,
38
+ borderWidth: 1,
39
+ borderColor: tokens.colors.border,
40
+ };
41
+ case 'flat':
42
+ return {
43
+ ...baseStyle,
44
+ borderWidth: 0,
45
+ };
46
+ default:
47
+ return baseStyle;
48
+ }
49
+ };
50
+ const cardStyle = [
51
+ getVariantStyle(),
52
+ {
53
+ padding: getPaddingValue(),
54
+ opacity: disabled ? 0.5 : 1,
55
+ },
56
+ style,
57
+ ];
58
+ const cardContent = (<View style={cardStyle} testID={testID}>
59
+ {children}
60
+ </View>);
61
+ // If onPress provided, wrap with pressable
62
+ if (onPress && !disabled) {
63
+ return (<Pressable onPress={handlePress}>
64
+ {cardContent}
65
+ </Pressable>);
66
+ }
67
+ // Otherwise just return static card
68
+ return cardContent;
69
+ };
@@ -0,0 +1,130 @@
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
+ import React from 'react';
18
+ import { View, TouchableOpacity } from 'react-native';
19
+ import { AtomicText } from './AtomicText';
20
+ import { AtomicIcon } from './AtomicIcon';
21
+ import { useAppDesignTokens } from '@umituz/react-native-theme';
22
+ // =============================================================================
23
+ // COMPONENT IMPLEMENTATION
24
+ // =============================================================================
25
+ export const AtomicChip = ({ children, variant = 'filled', size = 'md', color = 'primary', backgroundColor, textColor, borderColor, leadingIcon, trailingIcon, clickable = false, onPress, selected = false, disabled = false, style, testID, }) => {
26
+ const tokens = useAppDesignTokens();
27
+ // Size mapping
28
+ const sizeMap = {
29
+ sm: {
30
+ paddingHorizontal: tokens.spacing.sm,
31
+ paddingVertical: tokens.spacing.xs,
32
+ fontSize: tokens.typography.bodySmall.fontSize,
33
+ iconSize: 'xs'
34
+ },
35
+ md: {
36
+ paddingHorizontal: tokens.spacing.md,
37
+ paddingVertical: tokens.spacing.sm,
38
+ fontSize: tokens.typography.bodyMedium.fontSize,
39
+ iconSize: 'sm'
40
+ },
41
+ lg: {
42
+ paddingHorizontal: tokens.spacing.md,
43
+ paddingVertical: tokens.spacing.sm,
44
+ fontSize: tokens.typography.bodyLarge.fontSize,
45
+ iconSize: 'sm'
46
+ },
47
+ };
48
+ const sizeConfig = sizeMap[size];
49
+ // Color mapping
50
+ const colorMap = {
51
+ primary: {
52
+ filled: { bg: tokens.colors.primary, text: tokens.colors.onPrimary, border: tokens.colors.primary },
53
+ outlined: { bg: 'transparent', text: tokens.colors.primary, border: tokens.colors.primary },
54
+ soft: { bg: tokens.colors.primaryContainer, text: tokens.colors.onPrimaryContainer, border: 'transparent' },
55
+ },
56
+ secondary: {
57
+ filled: { bg: tokens.colors.secondary, text: tokens.colors.onSecondary, border: tokens.colors.secondary },
58
+ outlined: { bg: 'transparent', text: tokens.colors.secondary, border: tokens.colors.secondary },
59
+ soft: { bg: tokens.colors.secondaryContainer, text: tokens.colors.onSecondaryContainer, border: 'transparent' },
60
+ },
61
+ success: {
62
+ filled: { bg: tokens.colors.success, text: tokens.colors.onSuccess, border: tokens.colors.success },
63
+ outlined: { bg: 'transparent', text: tokens.colors.success, border: tokens.colors.success },
64
+ soft: { bg: tokens.colors.successContainer, text: tokens.colors.onSuccessContainer, border: 'transparent' },
65
+ },
66
+ warning: {
67
+ filled: { bg: tokens.colors.warning, text: tokens.colors.onWarning, border: tokens.colors.warning },
68
+ outlined: { bg: 'transparent', text: tokens.colors.warning, border: tokens.colors.warning },
69
+ soft: { bg: tokens.colors.warningContainer, text: tokens.colors.onWarningContainer, border: 'transparent' },
70
+ },
71
+ error: {
72
+ filled: { bg: tokens.colors.error, text: tokens.colors.onError, border: tokens.colors.error },
73
+ outlined: { bg: 'transparent', text: tokens.colors.error, border: tokens.colors.error },
74
+ soft: { bg: tokens.colors.errorContainer, text: tokens.colors.onErrorContainer, border: 'transparent' },
75
+ },
76
+ info: {
77
+ filled: { bg: tokens.colors.info, text: tokens.colors.onInfo, border: tokens.colors.info },
78
+ outlined: { bg: 'transparent', text: tokens.colors.info, border: tokens.colors.info },
79
+ soft: { bg: tokens.colors.infoContainer, text: tokens.colors.onInfoContainer, border: 'transparent' },
80
+ },
81
+ };
82
+ const colorConfig = colorMap[color][variant];
83
+ // Apply custom colors if provided
84
+ const finalBackgroundColor = backgroundColor || colorConfig.bg;
85
+ const finalTextColor = textColor || colorConfig.text;
86
+ const finalBorderColor = borderColor || colorConfig.border;
87
+ // Handle disabled state
88
+ const isDisabled = disabled || (!clickable && !onPress);
89
+ const opacity = isDisabled ? 0.5 : 1;
90
+ // Handle selected state
91
+ const selectedStyle = selected ? {
92
+ borderWidth: tokens.borders.width.medium,
93
+ borderColor: tokens.colors.primary,
94
+ } : {};
95
+ const chipStyle = {
96
+ flexDirection: 'row',
97
+ alignItems: 'center',
98
+ justifyContent: 'center',
99
+ paddingHorizontal: sizeConfig.paddingHorizontal,
100
+ paddingVertical: sizeConfig.paddingVertical,
101
+ backgroundColor: finalBackgroundColor,
102
+ borderRadius: tokens.borders.radius.xl,
103
+ borderWidth: variant === 'outlined' ? 1 : 0,
104
+ borderColor: finalBorderColor,
105
+ opacity,
106
+ ...selectedStyle,
107
+ };
108
+ const textStyle = {
109
+ fontSize: sizeConfig.fontSize,
110
+ fontWeight: tokens.typography.medium,
111
+ };
112
+ const iconColor = finalTextColor;
113
+ const content = (<View style={[chipStyle, style]} testID={testID}>
114
+ {leadingIcon && (<AtomicIcon name={leadingIcon} size={sizeConfig.iconSize} customColor={iconColor} style={{ marginRight: tokens.spacing.xs }}/>)}
115
+ <AtomicText type="labelMedium" color={finalTextColor} style={textStyle}>
116
+ {children}
117
+ </AtomicText>
118
+ {trailingIcon && (<AtomicIcon name={trailingIcon} size={sizeConfig.iconSize} customColor={iconColor} style={{ marginLeft: tokens.spacing.xs }}/>)}
119
+ </View>);
120
+ if (clickable && onPress && !disabled) {
121
+ return (<TouchableOpacity onPress={onPress} activeOpacity={0.7}>
122
+ {content}
123
+ </TouchableOpacity>);
124
+ }
125
+ return content;
126
+ };
127
+ // =============================================================================
128
+ // EXPORTS
129
+ // =============================================================================
130
+ export default AtomicChip;
@@ -0,0 +1,245 @@
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
+ * - iOS: Opens modal with spinner wheel, requires "Done" button
34
+ * - Android: Opens native dialog, auto-closes on selection
35
+ *
36
+ * @module AtomicDatePicker
37
+ */
38
+ import React, { useState } from 'react';
39
+ import { View, Text, TouchableOpacity, StyleSheet, Modal, useWindowDimensions, } from 'react-native';
40
+ import DateTimePicker from '@react-native-community/datetimepicker';
41
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
42
+ import { useAppDesignTokens } from '@umituz/react-native-theme';
43
+ import { useResponsive } from '../hooks/useResponsive';
44
+ import { AtomicIcon } from './AtomicIcon';
45
+ /**
46
+ * AtomicDatePicker - Universal date/time picker component
47
+ *
48
+ * Wraps @react-native-community/datetimepicker with:
49
+ * - Theme integration
50
+ * - Platform-specific modal handling
51
+ * - Error states
52
+ * - Disabled states
53
+ * - Responsive sizing
54
+ */
55
+ export const AtomicDatePicker = ({ value, onChange, label, error, disabled = false, minimumDate, maximumDate, mode = 'date', placeholder = 'Select date', testID, }) => {
56
+ const tokens = useAppDesignTokens();
57
+ const { height } = useWindowDimensions();
58
+ const insets = useSafeAreaInsets();
59
+ const { isTabletDevice } = useResponsive();
60
+ const [show, setShow] = useState(false);
61
+ /**
62
+ * Handle date/time change
63
+ * Universal handler that works across all platforms
64
+ * Note: event.type can be 'set', 'dismissed', or 'neutralButtonPressed'
65
+ */
66
+ const handleChange = (event, selectedDate) => {
67
+ // Close picker when user confirms or dismisses
68
+ // iOS: Stays open until "Done" button (handled separately)
69
+ // Android/Web: Auto-closes on selection
70
+ if (event.type === 'set' || event.type === 'dismissed') {
71
+ setShow(false);
72
+ }
73
+ // Update value only if date was selected (not dismissed)
74
+ if (event.type === 'set' && selectedDate) {
75
+ onChange(selectedDate);
76
+ }
77
+ };
78
+ /**
79
+ * Format date based on mode
80
+ * Uses native Date formatting (locale-aware)
81
+ */
82
+ const formatDate = (date) => {
83
+ if (mode === 'time') {
84
+ // Format time only
85
+ return date.toLocaleTimeString([], {
86
+ hour: '2-digit',
87
+ minute: '2-digit'
88
+ });
89
+ }
90
+ if (mode === 'datetime') {
91
+ // Format date + time
92
+ const dateStr = date.toLocaleDateString([], {
93
+ year: 'numeric',
94
+ month: 'short',
95
+ day: 'numeric',
96
+ });
97
+ const timeStr = date.toLocaleTimeString([], {
98
+ hour: '2-digit',
99
+ minute: '2-digit'
100
+ });
101
+ return `${dateStr} ${timeStr}`;
102
+ }
103
+ // Format date only
104
+ return date.toLocaleDateString([], {
105
+ year: 'numeric',
106
+ month: 'long',
107
+ day: 'numeric',
108
+ });
109
+ };
110
+ /**
111
+ * Determine icon color based on state
112
+ */
113
+ const getIconColor = () => {
114
+ if (disabled)
115
+ return 'secondary';
116
+ if (error)
117
+ return 'error';
118
+ return 'primary';
119
+ };
120
+ const styles = getStyles(tokens, height, insets);
121
+ return (<View style={styles.container} testID={testID}>
122
+ {label && (<Text style={styles.label} testID={testID ? `${testID}-label` : undefined}>
123
+ {label}
124
+ </Text>)}
125
+
126
+ <TouchableOpacity style={[
127
+ styles.button,
128
+ error ? styles.buttonError : undefined,
129
+ disabled ? styles.buttonDisabled : undefined,
130
+ ]} onPress={() => !disabled && setShow(true)} disabled={disabled} testID={testID ? `${testID}-button` : undefined} accessibilityLabel={label || placeholder} accessibilityRole="button" accessibilityState={{ disabled }}>
131
+ <AtomicIcon name="calendar" color={getIconColor()} size="md"/>
132
+ <Text style={[
133
+ styles.text,
134
+ disabled ? styles.textDisabled : undefined,
135
+ error ? styles.textError : undefined,
136
+ ]}>
137
+ {value ? formatDate(value) : placeholder}
138
+ </Text>
139
+ </TouchableOpacity>
140
+
141
+ {error && (<Text style={styles.errorText} testID={testID ? `${testID}-error` : undefined}>
142
+ {error}
143
+ </Text>)}
144
+
145
+ {/* Universal DatePicker - Works across iOS, Android, Web */}
146
+ {show && (<Modal transparent animationType={isTabletDevice ? 'fade' : 'slide'} visible={show} onRequestClose={() => setShow(false)}>
147
+ <TouchableOpacity style={styles.modalOverlay} activeOpacity={1} onPress={() => setShow(false)} accessibilityLabel="Close date picker" accessibilityRole="button">
148
+ <View style={styles.pickerContainer} onStartShouldSetResponder={() => true}>
149
+ <DateTimePicker value={value || new Date()} mode={mode} display="spinner" onChange={handleChange} minimumDate={minimumDate} maximumDate={maximumDate} testID={testID ? `${testID}-picker` : undefined}/>
150
+ <View style={styles.buttonContainer}>
151
+ <TouchableOpacity style={styles.doneButton} onPress={() => setShow(false)} testID={testID ? `${testID}-done` : undefined} accessibilityLabel="Done" accessibilityRole="button">
152
+ <Text style={styles.doneText}>Done</Text>
153
+ </TouchableOpacity>
154
+ </View>
155
+ </View>
156
+ </TouchableOpacity>
157
+ </Modal>)}
158
+ </View>);
159
+ };
160
+ /**
161
+ * Get component styles based on design tokens
162
+ */
163
+ const getStyles = (tokens, height, insets) => {
164
+ // Responsive button sizing based on device height
165
+ const buttonMinWidth = height <= 667 ? Math.min(height * 0.25, 150) : 200;
166
+ return StyleSheet.create({
167
+ container: {
168
+ marginBottom: tokens.spacing.md,
169
+ },
170
+ label: {
171
+ fontSize: tokens.typography.bodyMedium.fontSize,
172
+ fontWeight: tokens.typography.semibold,
173
+ color: tokens.colors.textPrimary,
174
+ marginBottom: tokens.spacing.sm,
175
+ },
176
+ button: {
177
+ flexDirection: 'row',
178
+ alignItems: 'center',
179
+ backgroundColor: tokens.colors.surface,
180
+ borderWidth: 1,
181
+ borderColor: tokens.colors.border,
182
+ borderRadius: tokens.borders.radius.lg,
183
+ paddingHorizontal: tokens.spacing.md,
184
+ paddingVertical: tokens.spacing.md,
185
+ gap: tokens.spacing.sm,
186
+ minHeight: 48, // Apple HIG minimum touch target
187
+ },
188
+ buttonError: {
189
+ borderColor: tokens.colors.error,
190
+ borderWidth: tokens.borders.width.medium,
191
+ },
192
+ buttonDisabled: {
193
+ backgroundColor: tokens.colors.surfaceDisabled,
194
+ opacity: tokens.opacity.disabled,
195
+ },
196
+ text: {
197
+ flex: 1,
198
+ fontSize: tokens.typography.bodyLarge.fontSize,
199
+ color: tokens.colors.textPrimary,
200
+ },
201
+ textDisabled: {
202
+ color: tokens.colors.textDisabled,
203
+ },
204
+ textError: {
205
+ color: tokens.colors.error,
206
+ },
207
+ errorText: {
208
+ fontSize: tokens.typography.bodySmall.fontSize,
209
+ color: tokens.colors.error,
210
+ marginTop: tokens.spacing.xs,
211
+ marginLeft: tokens.spacing.xs,
212
+ },
213
+ modalOverlay: {
214
+ flex: 1,
215
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
216
+ justifyContent: 'flex-start',
217
+ },
218
+ pickerContainer: {
219
+ backgroundColor: tokens.colors.surface,
220
+ borderTopLeftRadius: tokens.borders.radius.xl,
221
+ borderTopRightRadius: tokens.borders.radius.xl,
222
+ paddingTop: tokens.spacing.lg,
223
+ paddingBottom: Math.max(insets.bottom + tokens.spacing.md, tokens.spacing.xl),
224
+ },
225
+ buttonContainer: {
226
+ alignItems: 'center',
227
+ marginTop: tokens.spacing.md,
228
+ paddingHorizontal: tokens.spacing.lg,
229
+ },
230
+ doneButton: {
231
+ backgroundColor: tokens.colors.primary,
232
+ paddingHorizontal: tokens.spacing.xl,
233
+ paddingVertical: tokens.spacing.sm,
234
+ borderRadius: tokens.borders.radius.lg,
235
+ minWidth: buttonMinWidth,
236
+ alignItems: 'center',
237
+ minHeight: 44, // Apple HIG minimum touch target
238
+ },
239
+ doneText: {
240
+ color: tokens.colors.onPrimary,
241
+ fontSize: tokens.typography.bodyLarge.fontSize,
242
+ fontWeight: tokens.typography.semibold,
243
+ },
244
+ });
245
+ };
@@ -0,0 +1,57 @@
1
+ /**
2
+ * AtomicDivider - Universal Divider Component
3
+ *
4
+ * Displays horizontal or vertical dividers for content separation
5
+ * Theme: {{THEME_NAME}} ({{CATEGORY}} category)
6
+ *
7
+ * Atomic Design Level: ATOM
8
+ * Purpose: Content separation and visual hierarchy
9
+ *
10
+ * Usage:
11
+ * - Section separators
12
+ * - List item dividers
13
+ * - Card separators
14
+ * - Menu dividers
15
+ * - Form field separators
16
+ */
17
+ import React from 'react';
18
+ import { View } from 'react-native';
19
+ import { useAppDesignTokens } from '@umituz/react-native-theme';
20
+ // =============================================================================
21
+ // COMPONENT IMPLEMENTATION
22
+ // =============================================================================
23
+ export const AtomicDivider = ({ orientation = 'horizontal', thickness = 'thin', color, length, margin, marginTop, marginBottom, marginLeft, marginRight, style, testID, }) => {
24
+ const tokens = useAppDesignTokens();
25
+ // Thickness mapping
26
+ const thicknessMap = {
27
+ thin: 1,
28
+ medium: 2,
29
+ thick: 4,
30
+ };
31
+ const dividerThickness = thicknessMap[thickness];
32
+ const dividerColor = color || tokens.colors.border;
33
+ // Compute final length values with proper type handling
34
+ const finalLength = length !== undefined ? length : (orientation === 'horizontal' ? '100%' : 20);
35
+ // Base styles for all dividers
36
+ const baseStyle = {
37
+ backgroundColor: dividerColor,
38
+ margin: margin,
39
+ marginTop: marginTop,
40
+ marginBottom: marginBottom,
41
+ marginLeft: marginLeft,
42
+ marginRight: marginRight,
43
+ };
44
+ // Orientation-specific styles with explicit type casting
45
+ const orientationStyle = (orientation === 'horizontal' ? {
46
+ width: finalLength,
47
+ height: dividerThickness,
48
+ } : {
49
+ width: dividerThickness,
50
+ height: finalLength,
51
+ });
52
+ return (<View style={[baseStyle, orientationStyle, style]} testID={testID}/>);
53
+ };
54
+ // =============================================================================
55
+ // EXPORTS
56
+ // =============================================================================
57
+ export default AtomicDivider;
@@ -0,0 +1,67 @@
1
+ import React from 'react';
2
+ import { TouchableOpacity, StyleSheet } from 'react-native';
3
+ import { useAppDesignTokens } from '@umituz/react-native-theme';
4
+ import { useResponsive } from '../hooks/useResponsive';
5
+ import { AtomicIcon } from './AtomicIcon';
6
+ import { FAB_SIZES, getFabVariants, getFabIconSize, getFabBorder, } from './fab/styles/fabStyles';
7
+ export { FAB_SIZES, getFabVariants, getFabIconSize, getFabBorder };
8
+ /**
9
+ * AtomicFab - Floating Action Button Component
10
+ *
11
+ * A Material Design 3 compliant FAB component for primary actions.
12
+ * Follows CLAUDE.md standards for responsive positioning.
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * // IMPORTANT: FAB must be used at screen level, NOT inside ScrollView
17
+ * <ScreenLayout>
18
+ * <ScrollView>
19
+ * {/* Your content *\/}
20
+ * </ScrollView>
21
+ * <AtomicFab
22
+ * icon="add"
23
+ * onPress={handleAddItem}
24
+ * variant="primary"
25
+ * size="md"
26
+ * />
27
+ * </ScreenLayout>
28
+ * ```
29
+ *
30
+ * Features:
31
+ * - Material Design 3 sizes (sm: 40px, md: 56px, lg: 72px)
32
+ * - Three variants: primary, secondary, surface
33
+ * - Responsive positioning (above tab bar, safe area aware)
34
+ * - Disabled state with opacity
35
+ * - Theme-aware colors from design tokens
36
+ * - Border for depth (no shadows per CLAUDE.md)
37
+ */
38
+ export const AtomicFab = ({ icon, onPress, variant = 'primary', size = 'md', disabled = false, style, testID, accessibilityLabel, }) => {
39
+ const tokens = useAppDesignTokens();
40
+ const responsive = useResponsive();
41
+ const isDisabled = disabled;
42
+ // Get configurations
43
+ const sizeConfig = FAB_SIZES[size];
44
+ const variants = getFabVariants(tokens);
45
+ const variantConfig = variants[variant];
46
+ const iconSize = getFabIconSize(size);
47
+ // Combine styles
48
+ const fabStyle = StyleSheet.flatten([
49
+ {
50
+ position: 'absolute',
51
+ bottom: responsive.fabPosition.bottom,
52
+ right: responsive.fabPosition.right,
53
+ width: sizeConfig.width,
54
+ height: sizeConfig.height,
55
+ borderRadius: sizeConfig.borderRadius,
56
+ backgroundColor: variantConfig.backgroundColor,
57
+ alignItems: 'center',
58
+ justifyContent: 'center',
59
+ },
60
+ getFabBorder(tokens),
61
+ isDisabled ? { opacity: tokens.opacity.disabled } : undefined,
62
+ style, // Custom style override
63
+ ]);
64
+ return (<TouchableOpacity style={fabStyle} onPress={onPress} disabled={isDisabled} activeOpacity={0.7} testID={testID} accessibilityLabel={accessibilityLabel || `${icon} button`} accessibilityRole="button">
65
+ <AtomicIcon name={icon} size={iconSize} customColor={variantConfig.iconColor}/>
66
+ </TouchableOpacity>);
67
+ };
@@ -0,0 +1,103 @@
1
+ import React from 'react';
2
+ import { ScrollView, View, TouchableOpacity } from 'react-native';
3
+ import { useAppDesignTokens } from '@umituz/react-native-theme';
4
+ import { AtomicChip } from './AtomicChip';
5
+ import { AtomicText } from './AtomicText';
6
+ import { AtomicIcon } from './AtomicIcon';
7
+ import { getFilterContainerStyle, getClearAllContainerStyle, getScrollContentContainerStyle, } from './filter/styles/filterStyles';
8
+ export { getFilterContainerStyle, getClearAllContainerStyle, getScrollContentContainerStyle, } from './filter/styles/filterStyles';
9
+ /**
10
+ * AtomicFilter - Horizontal Filter Chip Component
11
+ *
12
+ * A Material Design 3 compliant filter component using chip selection.
13
+ * Supports single and multi-select modes with "Clear All" functionality.
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * const [selectedFilters, setSelectedFilters] = useState<string[]>([]);
18
+ *
19
+ * <AtomicFilter
20
+ * options={[
21
+ * { id: 'active', label: 'Active', icon: 'check-circle' },
22
+ * { id: 'completed', label: 'Completed', icon: 'check' },
23
+ * { id: 'pending', label: 'Pending', icon: 'clock' },
24
+ * ]}
25
+ * selectedIds={selectedFilters}
26
+ * onSelectionChange={setSelectedFilters}
27
+ * multiSelect={true}
28
+ * showClearAll={true}
29
+ * />
30
+ * ```
31
+ *
32
+ * Features:
33
+ * - Horizontal scrollable filter chips
34
+ * - Single/Multi-select modes
35
+ * - Clear all button (when filters active)
36
+ * - Theme-aware colors from design tokens
37
+ * - Icon support per filter option
38
+ * - Fully controlled component
39
+ */
40
+ export const AtomicFilter = ({ options, selectedIds, onSelectionChange, multiSelect = true, showClearAll = true, variant = 'outlined', color = 'primary', size = 'md', style, testID, }) => {
41
+ const tokens = useAppDesignTokens();
42
+ /**
43
+ * Handle filter chip press
44
+ */
45
+ const handleFilterPress = (optionId) => {
46
+ if (multiSelect) {
47
+ // Multi-select mode: Toggle selection
48
+ if (selectedIds.includes(optionId)) {
49
+ // Deselect
50
+ onSelectionChange(selectedIds.filter(id => id !== optionId));
51
+ }
52
+ else {
53
+ // Select
54
+ onSelectionChange([...selectedIds, optionId]);
55
+ }
56
+ }
57
+ else {
58
+ // Single-select mode: Replace selection
59
+ if (selectedIds.includes(optionId)) {
60
+ // Deselect (clear selection)
61
+ onSelectionChange([]);
62
+ }
63
+ else {
64
+ // Select (only this one)
65
+ onSelectionChange([optionId]);
66
+ }
67
+ }
68
+ };
69
+ /**
70
+ * Handle clear all button press
71
+ */
72
+ const handleClearAll = () => {
73
+ onSelectionChange([]);
74
+ };
75
+ const hasActiveFilters = selectedIds.length > 0;
76
+ return (<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={getScrollContentContainerStyle()} style={[style]} testID={testID}>
77
+ <View style={getFilterContainerStyle()}>
78
+ {options.map((option) => {
79
+ const isSelected = selectedIds.includes(option.id);
80
+ return (<AtomicChip key={option.id} variant={isSelected ? 'filled' : variant} color={color} size={size} leadingIcon={option.icon} selected={isSelected} clickable={true} onPress={() => handleFilterPress(option.id)} testID={`filter-chip-${option.id}`}>
81
+ {option.label}
82
+ </AtomicChip>);
83
+ })}
84
+
85
+ {/* Clear All Button */}
86
+ {showClearAll && hasActiveFilters && (<TouchableOpacity onPress={handleClearAll} style={[
87
+ getClearAllContainerStyle(),
88
+ {
89
+ backgroundColor: tokens.colors.surfaceVariant,
90
+ borderWidth: 1,
91
+ borderColor: tokens.colors.outline,
92
+ }
93
+ ]} testID="clear-all-button">
94
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: tokens.spacing.xs }}>
95
+ <AtomicIcon name="X" size="xs" color="surfaceVariant"/>
96
+ <AtomicText type="labelSmall" style={{ color: tokens.colors.textSecondary }}>
97
+ Clear All
98
+ </AtomicText>
99
+ </View>
100
+ </TouchableOpacity>)}
101
+ </View>
102
+ </ScrollView>);
103
+ };