@umituz/react-native-design-system 1.15.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 +133 -56
- 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,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AtomicIcon Tests
|
|
3
|
+
*
|
|
4
|
+
* Basic test cases for AtomicIcon component
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import { render } from '@testing-library/react-native';
|
|
9
|
+
import { AtomicIcon } from '../AtomicIcon';
|
|
10
|
+
|
|
11
|
+
// Mock design tokens
|
|
12
|
+
jest.mock('@umituz/react-native-design-system-theme', () => ({
|
|
13
|
+
useAppDesignTokens: () => ({
|
|
14
|
+
colors: {
|
|
15
|
+
primary: '#007AFF',
|
|
16
|
+
secondary: '#8E8E93',
|
|
17
|
+
},
|
|
18
|
+
}),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
// Mock Lucide icons
|
|
22
|
+
jest.mock('lucide-react-native', () => ({
|
|
23
|
+
Settings: () => 'Settings-Icon',
|
|
24
|
+
Heart: () => 'Heart-Icon',
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
describe('AtomicIcon', () => {
|
|
28
|
+
it('renders with default props', () => {
|
|
29
|
+
const { getByTestId } = render(
|
|
30
|
+
<AtomicIcon name="Settings" testID="test-icon" />
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
expect(getByTestId('test-icon')).toBeTruthy();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('renders with different sizes', () => {
|
|
37
|
+
const { getByTestId, rerender } = render(
|
|
38
|
+
<AtomicIcon name="Settings" size="sm" testID="test-icon" />
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
expect(getByTestId('test-icon')).toBeTruthy();
|
|
42
|
+
|
|
43
|
+
rerender(<AtomicIcon name="Settings" size="lg" testID="test-icon" />);
|
|
44
|
+
expect(getByTestId('test-icon')).toBeTruthy();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('renders with different colors', () => {
|
|
48
|
+
const { getByTestId, rerender } = render(
|
|
49
|
+
<AtomicIcon name="Settings" color="primary" testID="test-icon" />
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
expect(getByTestId('test-icon')).toBeTruthy();
|
|
53
|
+
|
|
54
|
+
rerender(<AtomicIcon name="Settings" color="secondary" testID="test-icon" />);
|
|
55
|
+
expect(getByTestId('test-icon')).toBeTruthy();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('renders with custom size', () => {
|
|
59
|
+
const { getByTestId } = render(
|
|
60
|
+
<AtomicIcon name="Settings" customSize={32} testID="test-icon" />
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
expect(getByTestId('test-icon')).toBeTruthy();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('renders with custom color', () => {
|
|
67
|
+
const { getByTestId } = render(
|
|
68
|
+
<AtomicIcon name="Settings" customColor="#FF0000" testID="test-icon" />
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
expect(getByTestId('test-icon')).toBeTruthy();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('renders with background', () => {
|
|
75
|
+
const { getByTestId } = render(
|
|
76
|
+
<AtomicIcon
|
|
77
|
+
name="Settings"
|
|
78
|
+
withBackground
|
|
79
|
+
backgroundColor="#F0F0F0"
|
|
80
|
+
testID="test-icon"
|
|
81
|
+
/>
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
expect(getByTestId('test-icon')).toBeTruthy();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('has accessibility label when provided', () => {
|
|
88
|
+
const { getByTestId } = render(
|
|
89
|
+
<AtomicIcon
|
|
90
|
+
name="Settings"
|
|
91
|
+
accessibilityLabel="Settings icon"
|
|
92
|
+
testID="test-icon"
|
|
93
|
+
/>
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
expect(getByTestId('test-icon')).toBeTruthy();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('handles unknown icon names gracefully', () => {
|
|
100
|
+
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
|
|
101
|
+
|
|
102
|
+
render(<AtomicIcon name="UnknownIcon" testID="test-icon" />);
|
|
103
|
+
|
|
104
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
105
|
+
expect.stringContaining('Icon "UnknownIcon" not found')
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
consoleSpy.mockRestore();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AtomicInput Tests
|
|
3
|
+
*
|
|
4
|
+
* Basic test cases for AtomicInput component
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import { render, fireEvent } from '@testing-library/react-native';
|
|
9
|
+
import { AtomicInput } from '../AtomicInput';
|
|
10
|
+
|
|
11
|
+
// Mock design tokens
|
|
12
|
+
jest.mock('@umituz/react-native-design-system-theme', () => ({
|
|
13
|
+
useAppDesignTokens: () => ({
|
|
14
|
+
colors: {
|
|
15
|
+
primary: '#007AFF',
|
|
16
|
+
secondary: '#8E8E93',
|
|
17
|
+
error: '#FF3B30',
|
|
18
|
+
success: '#34C759',
|
|
19
|
+
surface: '#F2F2F7',
|
|
20
|
+
border: '#C6C6C8',
|
|
21
|
+
textPrimary: '#000000',
|
|
22
|
+
textSecondary: '#8E8E93',
|
|
23
|
+
textDisabled: '#C7C7CC',
|
|
24
|
+
},
|
|
25
|
+
spacing: {
|
|
26
|
+
xs: 4,
|
|
27
|
+
sm: 8,
|
|
28
|
+
md: 16,
|
|
29
|
+
lg: 24,
|
|
30
|
+
},
|
|
31
|
+
typography: {
|
|
32
|
+
bodyMedium: { fontSize: 16 },
|
|
33
|
+
bodySmall: { fontSize: 14 },
|
|
34
|
+
},
|
|
35
|
+
borders: {
|
|
36
|
+
radius: {
|
|
37
|
+
md: 8,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
}),
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
describe('AtomicInput', () => {
|
|
44
|
+
it('renders correctly with basic props', () => {
|
|
45
|
+
const { getByDisplayValue } = render(
|
|
46
|
+
<AtomicInput value="test" onChangeText={() => {}} />
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
expect(getByDisplayValue('test')).toBeTruthy();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('handles text changes', () => {
|
|
53
|
+
const mockOnChange = jest.fn();
|
|
54
|
+
const { getByDisplayValue } = render(
|
|
55
|
+
<AtomicInput value="" onChangeText={mockOnChange} />
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const input = getByDisplayValue('');
|
|
59
|
+
fireEvent.changeText(input, 'new text');
|
|
60
|
+
|
|
61
|
+
expect(mockOnChange).toHaveBeenCalledWith('new text');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('renders with label', () => {
|
|
65
|
+
const { getByText } = render(
|
|
66
|
+
<AtomicInput value="" onChangeText={() => {}} label="Test Label" />
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
expect(getByText('Test Label')).toBeTruthy();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('renders with placeholder', () => {
|
|
73
|
+
const { getByPlaceholderText } = render(
|
|
74
|
+
<AtomicInput value="" onChangeText={() => {}} placeholder="Enter text" />
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
expect(getByPlaceholderText('Enter text')).toBeTruthy();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('renders with error state', () => {
|
|
81
|
+
const { getByText } = render(
|
|
82
|
+
<AtomicInput
|
|
83
|
+
value=""
|
|
84
|
+
onChangeText={() => {}}
|
|
85
|
+
state="error"
|
|
86
|
+
helperText="Error message"
|
|
87
|
+
/>
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
expect(getByText('Error message')).toBeTruthy();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('renders with success state', () => {
|
|
94
|
+
const { getByText } = render(
|
|
95
|
+
<AtomicInput
|
|
96
|
+
value=""
|
|
97
|
+
onChangeText={() => {}}
|
|
98
|
+
state="success"
|
|
99
|
+
helperText="Success message"
|
|
100
|
+
/>
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
expect(getByText('Success message')).toBeTruthy();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('renders with leading icon', () => {
|
|
107
|
+
const { getByTestId } = render(
|
|
108
|
+
<AtomicInput
|
|
109
|
+
value=""
|
|
110
|
+
onChangeText={() => {}}
|
|
111
|
+
leadingIcon="Search"
|
|
112
|
+
testID="test-input"
|
|
113
|
+
/>
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
expect(getByTestId('test-input')).toBeTruthy();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('renders with trailing icon', () => {
|
|
120
|
+
const { getByTestId } = render(
|
|
121
|
+
<AtomicInput
|
|
122
|
+
value=""
|
|
123
|
+
onChangeText={() => {}}
|
|
124
|
+
trailingIcon="Eye"
|
|
125
|
+
testID="test-input"
|
|
126
|
+
/>
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
expect(getByTestId('test-input')).toBeTruthy();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('renders with password toggle', () => {
|
|
133
|
+
const { getByTestId } = render(
|
|
134
|
+
<AtomicInput
|
|
135
|
+
value=""
|
|
136
|
+
onChangeText={() => {}}
|
|
137
|
+
secureTextEntry
|
|
138
|
+
showPasswordToggle
|
|
139
|
+
testID="test-input"
|
|
140
|
+
/>
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
expect(getByTestId('test-input')).toBeTruthy();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('is disabled when disabled prop is true', () => {
|
|
147
|
+
const { getByDisplayValue } = render(
|
|
148
|
+
<AtomicInput value="test" onChangeText={() => {}} disabled />
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const input = getByDisplayValue('test');
|
|
152
|
+
expect(input).toBeDisabled();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('renders with different variants', () => {
|
|
156
|
+
const { getByDisplayValue, rerender } = render(
|
|
157
|
+
<AtomicInput value="test" onChangeText={() => {}} variant="outlined" />
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
expect(getByDisplayValue('test')).toBeTruthy();
|
|
161
|
+
|
|
162
|
+
rerender(<AtomicInput value="test" onChangeText={() => {}} variant="filled" />);
|
|
163
|
+
expect(getByDisplayValue('test')).toBeTruthy();
|
|
164
|
+
|
|
165
|
+
rerender(<AtomicInput value="test" onChangeText={() => {}} variant="flat" />);
|
|
166
|
+
expect(getByDisplayValue('test')).toBeTruthy();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('renders with different sizes', () => {
|
|
170
|
+
const { getByDisplayValue, rerender } = render(
|
|
171
|
+
<AtomicInput value="test" onChangeText={() => {}} size="sm" />
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
expect(getByDisplayValue('test')).toBeTruthy();
|
|
175
|
+
|
|
176
|
+
rerender(<AtomicInput value="test" onChangeText={() => {}} size="md" />);
|
|
177
|
+
expect(getByDisplayValue('test')).toBeTruthy();
|
|
178
|
+
|
|
179
|
+
rerender(<AtomicInput value="test" onChangeText={() => {}} size="lg" />);
|
|
180
|
+
expect(getByDisplayValue('test')).toBeTruthy();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('shows character count when enabled', () => {
|
|
184
|
+
const { getByText } = render(
|
|
185
|
+
<AtomicInput
|
|
186
|
+
value="test"
|
|
187
|
+
onChangeText={() => {}}
|
|
188
|
+
showCharacterCount
|
|
189
|
+
maxLength={10}
|
|
190
|
+
/>
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
expect(getByText('4/10')).toBeTruthy();
|
|
194
|
+
});
|
|
195
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DatePickerButton Component
|
|
3
|
+
*
|
|
4
|
+
* Button component that triggers the date picker modal.
|
|
5
|
+
* Extracted from AtomicDatePicker for better separation of concerns.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import {
|
|
10
|
+
View,
|
|
11
|
+
TouchableOpacity,
|
|
12
|
+
StyleSheet,
|
|
13
|
+
} from 'react-native';
|
|
14
|
+
import { useAppDesignTokens } from '../../../theme';
|
|
15
|
+
import { AtomicIcon } from '../../AtomicIcon';
|
|
16
|
+
import { AtomicText } from '../../AtomicText';
|
|
17
|
+
|
|
18
|
+
interface DatePickerButtonProps {
|
|
19
|
+
onPress: () => void;
|
|
20
|
+
disabled?: boolean;
|
|
21
|
+
displayText: string;
|
|
22
|
+
hasValue: boolean;
|
|
23
|
+
error?: boolean;
|
|
24
|
+
testID?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const DatePickerButton: React.FC<DatePickerButtonProps> = ({
|
|
28
|
+
onPress,
|
|
29
|
+
disabled = false,
|
|
30
|
+
displayText,
|
|
31
|
+
hasValue,
|
|
32
|
+
error,
|
|
33
|
+
testID,
|
|
34
|
+
}) => {
|
|
35
|
+
const tokens = useAppDesignTokens();
|
|
36
|
+
|
|
37
|
+
const buttonStyles = StyleSheet.create({
|
|
38
|
+
container: {
|
|
39
|
+
flexDirection: 'row',
|
|
40
|
+
alignItems: 'center',
|
|
41
|
+
justifyContent: 'space-between',
|
|
42
|
+
paddingHorizontal: tokens.spacing.md,
|
|
43
|
+
paddingVertical: tokens.spacing.sm,
|
|
44
|
+
borderRadius: tokens.borders.radius.md,
|
|
45
|
+
borderWidth: 1,
|
|
46
|
+
backgroundColor: tokens.colors.surface,
|
|
47
|
+
minHeight: 48,
|
|
48
|
+
},
|
|
49
|
+
containerError: {
|
|
50
|
+
borderColor: tokens.colors.error,
|
|
51
|
+
},
|
|
52
|
+
containerDisabled: {
|
|
53
|
+
backgroundColor: tokens.colors.surfaceVariant,
|
|
54
|
+
borderColor: tokens.colors.outline,
|
|
55
|
+
opacity: 0.6,
|
|
56
|
+
},
|
|
57
|
+
containerDefault: {
|
|
58
|
+
borderColor: tokens.colors.outline,
|
|
59
|
+
},
|
|
60
|
+
textContainer: {
|
|
61
|
+
flex: 1,
|
|
62
|
+
},
|
|
63
|
+
placeholderText: {
|
|
64
|
+
fontSize: tokens.typography.bodyMedium.fontSize,
|
|
65
|
+
color: tokens.colors.textSecondary,
|
|
66
|
+
},
|
|
67
|
+
valueText: {
|
|
68
|
+
fontSize: tokens.typography.bodyMedium.fontSize,
|
|
69
|
+
color: tokens.colors.onSurface,
|
|
70
|
+
fontWeight: '500',
|
|
71
|
+
},
|
|
72
|
+
iconContainer: {
|
|
73
|
+
flexDirection: 'row',
|
|
74
|
+
alignItems: 'center',
|
|
75
|
+
gap: tokens.spacing.xs,
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const containerStyle = [
|
|
80
|
+
buttonStyles.container,
|
|
81
|
+
error ? buttonStyles.containerError :
|
|
82
|
+
disabled ? buttonStyles.containerDisabled :
|
|
83
|
+
buttonStyles.containerDefault,
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
const textStyle = hasValue ? buttonStyles.valueText : buttonStyles.placeholderText;
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<TouchableOpacity
|
|
90
|
+
onPress={onPress}
|
|
91
|
+
disabled={disabled}
|
|
92
|
+
accessibilityRole="button"
|
|
93
|
+
accessibilityState={{ disabled }}
|
|
94
|
+
testID={testID}
|
|
95
|
+
style={containerStyle}
|
|
96
|
+
>
|
|
97
|
+
<View style={buttonStyles.textContainer}>
|
|
98
|
+
<AtomicText style={textStyle} numberOfLines={1}>
|
|
99
|
+
{displayText}
|
|
100
|
+
</AtomicText>
|
|
101
|
+
</View>
|
|
102
|
+
|
|
103
|
+
<View style={buttonStyles.iconContainer}>
|
|
104
|
+
<AtomicIcon
|
|
105
|
+
name="Calendar"
|
|
106
|
+
size="md"
|
|
107
|
+
color={disabled ? 'surfaceVariant' : 'secondary'}
|
|
108
|
+
/>
|
|
109
|
+
</View>
|
|
110
|
+
</TouchableOpacity>
|
|
111
|
+
);
|
|
112
|
+
};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DatePickerModal Component
|
|
3
|
+
*
|
|
4
|
+
* Modal component for iOS date picker with proper styling and behavior.
|
|
5
|
+
* Extracted from AtomicDatePicker for better separation of concerns.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import {
|
|
10
|
+
View,
|
|
11
|
+
Modal,
|
|
12
|
+
TouchableOpacity,
|
|
13
|
+
StyleSheet,
|
|
14
|
+
Platform,
|
|
15
|
+
} from 'react-native';
|
|
16
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
17
|
+
import DateTimePicker, { DateTimePickerEvent } from '@react-native-community/datetimepicker';
|
|
18
|
+
import { useAppDesignTokens } from '../../../theme';
|
|
19
|
+
import { AtomicIcon } from '../../AtomicIcon';
|
|
20
|
+
import { AtomicText } from '../../AtomicText';
|
|
21
|
+
|
|
22
|
+
interface DatePickerModalProps {
|
|
23
|
+
visible: boolean;
|
|
24
|
+
onClose: () => void;
|
|
25
|
+
onDateChange: (event: DateTimePickerEvent, date?: Date) => void;
|
|
26
|
+
currentDate: Date;
|
|
27
|
+
mode?: 'date' | 'time' | 'datetime';
|
|
28
|
+
minimumDate?: Date;
|
|
29
|
+
maximumDate?: Date;
|
|
30
|
+
overlayOpacity?: number;
|
|
31
|
+
titleText?: {
|
|
32
|
+
date?: string;
|
|
33
|
+
time?: string;
|
|
34
|
+
datetime?: string;
|
|
35
|
+
};
|
|
36
|
+
doneButtonText?: string;
|
|
37
|
+
testID?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const DatePickerModal: React.FC<DatePickerModalProps> = ({
|
|
41
|
+
visible,
|
|
42
|
+
onClose,
|
|
43
|
+
onDateChange,
|
|
44
|
+
currentDate,
|
|
45
|
+
mode = 'date',
|
|
46
|
+
minimumDate,
|
|
47
|
+
maximumDate,
|
|
48
|
+
overlayOpacity = 0.5,
|
|
49
|
+
titleText,
|
|
50
|
+
doneButtonText = 'Done',
|
|
51
|
+
testID,
|
|
52
|
+
}) => {
|
|
53
|
+
const tokens = useAppDesignTokens();
|
|
54
|
+
const insets = useSafeAreaInsets();
|
|
55
|
+
|
|
56
|
+
const modalStyles = StyleSheet.create({
|
|
57
|
+
overlay: {
|
|
58
|
+
flex: 1,
|
|
59
|
+
backgroundColor: `rgba(0, 0, 0, ${overlayOpacity})`,
|
|
60
|
+
justifyContent: 'flex-end',
|
|
61
|
+
},
|
|
62
|
+
container: {
|
|
63
|
+
backgroundColor: tokens.colors.surface,
|
|
64
|
+
borderTopLeftRadius: tokens.borders.radius.lg,
|
|
65
|
+
borderTopRightRadius: tokens.borders.radius.lg,
|
|
66
|
+
paddingBottom: insets.bottom,
|
|
67
|
+
},
|
|
68
|
+
header: {
|
|
69
|
+
flexDirection: 'row',
|
|
70
|
+
justifyContent: 'space-between',
|
|
71
|
+
alignItems: 'center',
|
|
72
|
+
paddingHorizontal: tokens.spacing.md,
|
|
73
|
+
paddingVertical: tokens.spacing.sm,
|
|
74
|
+
borderBottomWidth: 1,
|
|
75
|
+
borderBottomColor: tokens.colors.outline,
|
|
76
|
+
},
|
|
77
|
+
title: {
|
|
78
|
+
fontSize: tokens.typography.titleLarge.fontSize,
|
|
79
|
+
fontWeight: '600',
|
|
80
|
+
color: tokens.colors.onSurface,
|
|
81
|
+
},
|
|
82
|
+
doneButton: {
|
|
83
|
+
paddingHorizontal: tokens.spacing.md,
|
|
84
|
+
paddingVertical: tokens.spacing.xs,
|
|
85
|
+
borderRadius: tokens.borders.radius.md,
|
|
86
|
+
backgroundColor: tokens.colors.primary,
|
|
87
|
+
},
|
|
88
|
+
doneButtonText: {
|
|
89
|
+
fontSize: tokens.typography.labelMedium.fontSize,
|
|
90
|
+
fontWeight: '500',
|
|
91
|
+
color: tokens.colors.onPrimary,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (Platform.OS !== 'ios') {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<Modal
|
|
101
|
+
visible={visible}
|
|
102
|
+
transparent
|
|
103
|
+
animationType="slide"
|
|
104
|
+
onRequestClose={onClose}
|
|
105
|
+
testID={`${testID}-modal`}
|
|
106
|
+
>
|
|
107
|
+
<View style={modalStyles.overlay}>
|
|
108
|
+
<View style={modalStyles.container}>
|
|
109
|
+
{/* Header */}
|
|
110
|
+
<View style={modalStyles.header}>
|
|
111
|
+
<AtomicText style={modalStyles.title}>
|
|
112
|
+
{mode === 'date'
|
|
113
|
+
? (titleText?.date || 'Select Date')
|
|
114
|
+
: mode === 'time'
|
|
115
|
+
? (titleText?.time || 'Select Time')
|
|
116
|
+
: (titleText?.datetime || 'Select Date & Time')
|
|
117
|
+
}
|
|
118
|
+
</AtomicText>
|
|
119
|
+
<TouchableOpacity
|
|
120
|
+
onPress={onClose}
|
|
121
|
+
style={modalStyles.doneButton}
|
|
122
|
+
testID={`${testID}-done`}
|
|
123
|
+
>
|
|
124
|
+
<AtomicText style={modalStyles.doneButtonText}>{doneButtonText}</AtomicText>
|
|
125
|
+
</TouchableOpacity>
|
|
126
|
+
</View>
|
|
127
|
+
|
|
128
|
+
{/* Date Picker */}
|
|
129
|
+
<DateTimePicker
|
|
130
|
+
value={currentDate}
|
|
131
|
+
mode={mode}
|
|
132
|
+
onChange={onDateChange}
|
|
133
|
+
minimumDate={minimumDate}
|
|
134
|
+
maximumDate={maximumDate}
|
|
135
|
+
display="spinner"
|
|
136
|
+
style={{ alignSelf: 'center' }}
|
|
137
|
+
testID={`${testID}-picker`}
|
|
138
|
+
/>
|
|
139
|
+
</View>
|
|
140
|
+
</View>
|
|
141
|
+
</Modal>
|
|
142
|
+
);
|
|
143
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FAB (Floating Action Button) Styles
|
|
3
|
+
*
|
|
4
|
+
* Material Design 3 compliant FAB sizing and styling
|
|
5
|
+
* Used by AtomicFab component
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ViewStyle } from 'react-native';
|
|
9
|
+
import type { FabSizeConfig, FabVariantConfig } from '../types';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* FAB size configurations based on Material Design 3
|
|
13
|
+
* - sm: Small FAB (40x40)
|
|
14
|
+
* - md: Regular FAB (56x56) - Default
|
|
15
|
+
* - lg: Large FAB (72x72)
|
|
16
|
+
*/
|
|
17
|
+
export const FAB_SIZES: Record<'sm' | 'md' | 'lg', FabSizeConfig> = {
|
|
18
|
+
sm: {
|
|
19
|
+
width: 40,
|
|
20
|
+
height: 40,
|
|
21
|
+
borderRadius: 12,
|
|
22
|
+
},
|
|
23
|
+
md: {
|
|
24
|
+
width: 56,
|
|
25
|
+
height: 56,
|
|
26
|
+
borderRadius: 16,
|
|
27
|
+
},
|
|
28
|
+
lg: {
|
|
29
|
+
width: 72,
|
|
30
|
+
height: 72,
|
|
31
|
+
borderRadius: 20,
|
|
32
|
+
},
|
|
33
|
+
} as const;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get FAB variant configurations based on design tokens
|
|
37
|
+
* @param tokens - Design tokens from theme
|
|
38
|
+
* @returns Variant configurations for primary, secondary, and surface
|
|
39
|
+
*/
|
|
40
|
+
export function getFabVariants(tokens: {
|
|
41
|
+
colors: {
|
|
42
|
+
primary: string;
|
|
43
|
+
onPrimary: string;
|
|
44
|
+
secondary: string;
|
|
45
|
+
onSecondary: string;
|
|
46
|
+
surface: string;
|
|
47
|
+
onSurface: string;
|
|
48
|
+
};
|
|
49
|
+
}): Record<'primary' | 'secondary' | 'surface', FabVariantConfig> {
|
|
50
|
+
return {
|
|
51
|
+
primary: {
|
|
52
|
+
backgroundColor: tokens.colors.primary,
|
|
53
|
+
iconColor: tokens.colors.onPrimary,
|
|
54
|
+
},
|
|
55
|
+
secondary: {
|
|
56
|
+
backgroundColor: tokens.colors.secondary,
|
|
57
|
+
iconColor: tokens.colors.onSecondary,
|
|
58
|
+
},
|
|
59
|
+
surface: {
|
|
60
|
+
backgroundColor: tokens.colors.surface,
|
|
61
|
+
iconColor: tokens.colors.onSurface,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get icon size based on FAB size
|
|
68
|
+
* @param size - FAB size variant
|
|
69
|
+
* @returns Icon size in pixels
|
|
70
|
+
*/
|
|
71
|
+
export function getFabIconSize(size: 'sm' | 'md' | 'lg'): number {
|
|
72
|
+
switch (size) {
|
|
73
|
+
case 'sm':
|
|
74
|
+
return 20;
|
|
75
|
+
case 'md':
|
|
76
|
+
return 24;
|
|
77
|
+
case 'lg':
|
|
78
|
+
return 28;
|
|
79
|
+
default:
|
|
80
|
+
return 24;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get FAB border style for depth (no shadows per CLAUDE.md)
|
|
86
|
+
* @param tokens - Design tokens from theme
|
|
87
|
+
* @returns Border style object
|
|
88
|
+
*/
|
|
89
|
+
export function getFabBorder(tokens: {
|
|
90
|
+
colors: {
|
|
91
|
+
border: string;
|
|
92
|
+
};
|
|
93
|
+
}): ViewStyle {
|
|
94
|
+
return {
|
|
95
|
+
borderWidth: 1,
|
|
96
|
+
borderColor: tokens.colors.border,
|
|
97
|
+
};
|
|
98
|
+
}
|