@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,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AtomicPicker Component
|
|
3
|
+
*
|
|
4
|
+
* A reusable option picker/dropdown component for selecting from a list of options.
|
|
5
|
+
*
|
|
6
|
+
* Features:
|
|
7
|
+
* - Single and multi-select support
|
|
8
|
+
* - Modal display mode (full-screen on mobile)
|
|
9
|
+
* - Optional search/filter capability
|
|
10
|
+
* - Error and disabled states
|
|
11
|
+
* - Theme-aware styling
|
|
12
|
+
* - Icons for options
|
|
13
|
+
* - Clearable selection
|
|
14
|
+
* - react-hook-form integration ready
|
|
15
|
+
*
|
|
16
|
+
* Architecture:
|
|
17
|
+
* - Follows AtomicButton pattern with separated types and styles
|
|
18
|
+
* - Uses helper functions from picker/styles/pickerStyles.ts
|
|
19
|
+
* - Types defined in picker/types/index.ts
|
|
20
|
+
* - Zero inline StyleSheet.create()
|
|
21
|
+
*
|
|
22
|
+
* Usage:
|
|
23
|
+
* ```tsx
|
|
24
|
+
* const [partyType, setPartyType] = useState('birthday');
|
|
25
|
+
*
|
|
26
|
+
* <AtomicPicker
|
|
27
|
+
* value={partyType}
|
|
28
|
+
* onChange={setPartyType}
|
|
29
|
+
* options={[
|
|
30
|
+
* { label: 'Birthday Party', value: 'birthday', icon: 'cake' },
|
|
31
|
+
* { label: 'Wedding', value: 'wedding', icon: 'heart' },
|
|
32
|
+
* { label: 'Corporate Event', value: 'corporate', icon: 'briefcase' },
|
|
33
|
+
* ]}
|
|
34
|
+
* label="Party Type"
|
|
35
|
+
* placeholder="Select party type"
|
|
36
|
+
* searchable
|
|
37
|
+
* />
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* @module AtomicPicker
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import React, { useState, useMemo } from 'react';
|
|
44
|
+
import {
|
|
45
|
+
View,
|
|
46
|
+
TouchableOpacity,
|
|
47
|
+
StyleSheet,
|
|
48
|
+
} from 'react-native';
|
|
49
|
+
import { useAppDesignTokens } from '../theme';
|
|
50
|
+
import { AtomicPickerProps, PickerOption } from './picker/types';
|
|
51
|
+
import { AtomicIcon } from './AtomicIcon';
|
|
52
|
+
import { AtomicText } from './AtomicText';
|
|
53
|
+
import { PickerModal } from './picker/components/PickerModal';
|
|
54
|
+
import { PickerChips } from './picker/components/PickerChips';
|
|
55
|
+
import {
|
|
56
|
+
getPickerContainerStyles,
|
|
57
|
+
getPickerLabelStyles,
|
|
58
|
+
getPickerPlaceholderStyles,
|
|
59
|
+
getPickerValueStyles,
|
|
60
|
+
getPickerErrorStyles,
|
|
61
|
+
} from './picker/styles/pickerStyles';
|
|
62
|
+
|
|
63
|
+
export type { AtomicPickerProps, PickerOption, PickerSize } from './picker/types';
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* AtomicPicker - Universal option picker component
|
|
67
|
+
*
|
|
68
|
+
* Displays a button that opens a modal for selection.
|
|
69
|
+
* Supports single/multi-select, search, and custom rendering.
|
|
70
|
+
*/
|
|
71
|
+
export const AtomicPicker: React.FC<AtomicPickerProps> = ({
|
|
72
|
+
value,
|
|
73
|
+
onChange,
|
|
74
|
+
options,
|
|
75
|
+
label,
|
|
76
|
+
placeholder = 'Select...',
|
|
77
|
+
error,
|
|
78
|
+
disabled = false,
|
|
79
|
+
multiple = false,
|
|
80
|
+
searchable = false,
|
|
81
|
+
clearable = false,
|
|
82
|
+
autoClose = true,
|
|
83
|
+
color = 'primary',
|
|
84
|
+
size = 'md',
|
|
85
|
+
modalTitle,
|
|
86
|
+
emptyMessage = 'No options available',
|
|
87
|
+
clearAccessibilityLabel = 'Clear selection',
|
|
88
|
+
closeAccessibilityLabel = 'Close picker',
|
|
89
|
+
style,
|
|
90
|
+
labelStyle,
|
|
91
|
+
testID,
|
|
92
|
+
}) => {
|
|
93
|
+
const tokens = useAppDesignTokens();
|
|
94
|
+
|
|
95
|
+
const [modalVisible, setModalVisible] = useState(false);
|
|
96
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
97
|
+
|
|
98
|
+
// Get style helpers with design tokens
|
|
99
|
+
const containerStyles = getPickerContainerStyles(tokens);
|
|
100
|
+
const labelStyles = getPickerLabelStyles(tokens);
|
|
101
|
+
const placeholderStyles = getPickerPlaceholderStyles(tokens);
|
|
102
|
+
const valueStyles = getPickerValueStyles(tokens);
|
|
103
|
+
const errorStyles = getPickerErrorStyles(tokens);
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Normalize value to array for consistent handling
|
|
107
|
+
*/
|
|
108
|
+
const selectedValues = useMemo(() => {
|
|
109
|
+
if (multiple) {
|
|
110
|
+
return Array.isArray(value) ? value : [];
|
|
111
|
+
}
|
|
112
|
+
return value ? [value as string] : [];
|
|
113
|
+
}, [value, multiple]);
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get selected option objects
|
|
117
|
+
*/
|
|
118
|
+
const selectedOptions = useMemo(() => {
|
|
119
|
+
return options.filter((opt) => selectedValues.includes(opt.value));
|
|
120
|
+
}, [options, selectedValues]);
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Filter options based on search query
|
|
124
|
+
*/
|
|
125
|
+
const filteredOptions = useMemo(() => {
|
|
126
|
+
if (!searchQuery.trim()) return options;
|
|
127
|
+
|
|
128
|
+
const query = searchQuery.toLowerCase();
|
|
129
|
+
return options.filter(
|
|
130
|
+
(opt) =>
|
|
131
|
+
opt.label.toLowerCase().includes(query) ||
|
|
132
|
+
opt.description?.toLowerCase().includes(query)
|
|
133
|
+
);
|
|
134
|
+
}, [options, searchQuery]);
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Format display text for selected value(s)
|
|
138
|
+
*/
|
|
139
|
+
const displayText = useMemo(() => {
|
|
140
|
+
if (selectedOptions.length === 0) {
|
|
141
|
+
return placeholder;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (multiple) {
|
|
145
|
+
return selectedOptions.length === 1
|
|
146
|
+
? selectedOptions[0].label
|
|
147
|
+
: `${selectedOptions.length} selected`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return selectedOptions[0]?.label || placeholder;
|
|
151
|
+
}, [selectedOptions, placeholder, multiple]);
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Handle modal open
|
|
155
|
+
*/
|
|
156
|
+
const openModal = () => {
|
|
157
|
+
if (disabled) return;
|
|
158
|
+
setModalVisible(true);
|
|
159
|
+
setSearchQuery('');
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Handle modal close
|
|
164
|
+
*/
|
|
165
|
+
const closeModal = () => {
|
|
166
|
+
setModalVisible(false);
|
|
167
|
+
setSearchQuery('');
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Handle option selection
|
|
172
|
+
*/
|
|
173
|
+
const handleSelect = (optionValue: string) => {
|
|
174
|
+
if (multiple) {
|
|
175
|
+
const newValues = selectedValues.includes(optionValue)
|
|
176
|
+
? selectedValues.filter((v) => v !== optionValue)
|
|
177
|
+
: [...selectedValues, optionValue];
|
|
178
|
+
onChange(newValues);
|
|
179
|
+
} else {
|
|
180
|
+
onChange(optionValue);
|
|
181
|
+
if (autoClose) {
|
|
182
|
+
closeModal();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Handle clear selection
|
|
189
|
+
*/
|
|
190
|
+
const handleClear = () => {
|
|
191
|
+
onChange(multiple ? [] : '');
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Handle search query change
|
|
196
|
+
*/
|
|
197
|
+
const handleSearch = (query: string) => {
|
|
198
|
+
setSearchQuery(query);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Check if option is selected
|
|
203
|
+
*/
|
|
204
|
+
const isSelected = (optionValue: string): boolean => {
|
|
205
|
+
return selectedValues.includes(optionValue);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Handle chip removal
|
|
210
|
+
*/
|
|
211
|
+
const handleChipRemove = (value: string) => {
|
|
212
|
+
handleSelect(value);
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const pickerContainerStyle = StyleSheet.flatten([
|
|
216
|
+
containerStyles.base,
|
|
217
|
+
containerStyles.size[size],
|
|
218
|
+
error ? containerStyles.state.error : undefined,
|
|
219
|
+
disabled ? containerStyles.state.disabled : undefined,
|
|
220
|
+
style,
|
|
221
|
+
]);
|
|
222
|
+
|
|
223
|
+
const pickerLabelStyle = StyleSheet.flatten([
|
|
224
|
+
labelStyles.base,
|
|
225
|
+
labelStyles.size[size],
|
|
226
|
+
labelStyle,
|
|
227
|
+
]);
|
|
228
|
+
|
|
229
|
+
const pickerValueStyle = StyleSheet.flatten([
|
|
230
|
+
selectedOptions.length > 0 ? valueStyles.base : placeholderStyles.base,
|
|
231
|
+
selectedOptions.length > 0
|
|
232
|
+
? valueStyles.size[size]
|
|
233
|
+
: placeholderStyles.size[size],
|
|
234
|
+
]);
|
|
235
|
+
|
|
236
|
+
return (
|
|
237
|
+
<View>
|
|
238
|
+
{/* Label */}
|
|
239
|
+
{label && <AtomicText style={pickerLabelStyle}>{label}</AtomicText>}
|
|
240
|
+
|
|
241
|
+
{/* Picker Button */}
|
|
242
|
+
<TouchableOpacity
|
|
243
|
+
onPress={openModal}
|
|
244
|
+
disabled={disabled}
|
|
245
|
+
accessibilityRole="button"
|
|
246
|
+
accessibilityLabel={label || placeholder}
|
|
247
|
+
accessibilityState={{ disabled }}
|
|
248
|
+
testID={testID}
|
|
249
|
+
style={pickerContainerStyle}
|
|
250
|
+
>
|
|
251
|
+
{/* Display Text */}
|
|
252
|
+
<AtomicText style={pickerValueStyle} numberOfLines={1}>
|
|
253
|
+
{displayText}
|
|
254
|
+
</AtomicText>
|
|
255
|
+
|
|
256
|
+
{/* Icons */}
|
|
257
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', gap: tokens.spacing.xs }}>
|
|
258
|
+
{/* Clear Button */}
|
|
259
|
+
{clearable && selectedOptions.length > 0 && !disabled && (
|
|
260
|
+
<TouchableOpacity
|
|
261
|
+
onPress={handleClear}
|
|
262
|
+
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
|
263
|
+
accessibilityRole="button"
|
|
264
|
+
accessibilityLabel={clearAccessibilityLabel}
|
|
265
|
+
testID={`${testID}-clear`}
|
|
266
|
+
>
|
|
267
|
+
<AtomicIcon name="X" size="sm" color="secondary" />
|
|
268
|
+
</TouchableOpacity>
|
|
269
|
+
)}
|
|
270
|
+
|
|
271
|
+
{/* Dropdown Icon */}
|
|
272
|
+
<AtomicIcon
|
|
273
|
+
name={modalVisible ? 'ChevronUp' : 'ChevronDown'}
|
|
274
|
+
size="sm"
|
|
275
|
+
color={disabled ? 'surfaceVariant' : 'secondary'}
|
|
276
|
+
/>
|
|
277
|
+
</View>
|
|
278
|
+
</TouchableOpacity>
|
|
279
|
+
|
|
280
|
+
{/* Selected Chips (Multi-select) */}
|
|
281
|
+
<PickerChips
|
|
282
|
+
selectedOptions={selectedOptions}
|
|
283
|
+
onRemoveChip={handleChipRemove}
|
|
284
|
+
testID={testID}
|
|
285
|
+
/>
|
|
286
|
+
|
|
287
|
+
{/* Error Message */}
|
|
288
|
+
{error && <AtomicText style={errorStyles}>{error}</AtomicText>}
|
|
289
|
+
|
|
290
|
+
{/* Selection Modal */}
|
|
291
|
+
<PickerModal
|
|
292
|
+
visible={modalVisible}
|
|
293
|
+
onClose={closeModal}
|
|
294
|
+
options={options}
|
|
295
|
+
selectedValues={selectedValues}
|
|
296
|
+
onSelect={handleSelect}
|
|
297
|
+
title={modalTitle || label}
|
|
298
|
+
searchable={searchable}
|
|
299
|
+
searchQuery={searchQuery}
|
|
300
|
+
onSearchChange={handleSearch}
|
|
301
|
+
filteredOptions={filteredOptions}
|
|
302
|
+
multiple={multiple}
|
|
303
|
+
emptyMessage={emptyMessage}
|
|
304
|
+
searchPlaceholder="Search..."
|
|
305
|
+
closeAccessibilityLabel={closeAccessibilityLabel}
|
|
306
|
+
testID={testID}
|
|
307
|
+
/>
|
|
308
|
+
</View>
|
|
309
|
+
);
|
|
310
|
+
};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AtomicProgress - Universal Progress Bar Component
|
|
3
|
+
*
|
|
4
|
+
* Displays progress bars for completion tracking
|
|
5
|
+
* Theme: {{THEME_NAME}} ({{CATEGORY}} category)
|
|
6
|
+
*
|
|
7
|
+
* Atomic Design Level: ATOM
|
|
8
|
+
* Purpose: Progress indication and completion tracking
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* - File upload progress
|
|
12
|
+
* - Task completion progress
|
|
13
|
+
* - Achievement progress
|
|
14
|
+
* - Form completion
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import React from 'react';
|
|
18
|
+
import { View, StyleSheet, ViewStyle, DimensionValue, Text } from 'react-native';
|
|
19
|
+
import { useAppDesignTokens } from '../theme';
|
|
20
|
+
|
|
21
|
+
// =============================================================================
|
|
22
|
+
// TYPE DEFINITIONS
|
|
23
|
+
// =============================================================================
|
|
24
|
+
|
|
25
|
+
export interface AtomicProgressProps {
|
|
26
|
+
/** Progress value (0-100) */
|
|
27
|
+
value: number;
|
|
28
|
+
/** Progress bar height */
|
|
29
|
+
height?: number;
|
|
30
|
+
/** Progress bar width */
|
|
31
|
+
width?: number | string;
|
|
32
|
+
/** Progress bar color */
|
|
33
|
+
color?: string;
|
|
34
|
+
/** Background color */
|
|
35
|
+
backgroundColor?: string;
|
|
36
|
+
/** Progress bar shape */
|
|
37
|
+
shape?: 'rounded' | 'square';
|
|
38
|
+
/** Whether to show percentage text */
|
|
39
|
+
showPercentage?: boolean;
|
|
40
|
+
/** Whether to show value text */
|
|
41
|
+
showValue?: boolean;
|
|
42
|
+
/** Custom text color */
|
|
43
|
+
textColor?: string;
|
|
44
|
+
/** Style overrides */
|
|
45
|
+
style?: ViewStyle | ViewStyle[];
|
|
46
|
+
/** Test ID for testing */
|
|
47
|
+
testID?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// =============================================================================
|
|
51
|
+
// COMPONENT IMPLEMENTATION
|
|
52
|
+
// =============================================================================
|
|
53
|
+
|
|
54
|
+
export const AtomicProgress: React.FC<AtomicProgressProps> = ({
|
|
55
|
+
value,
|
|
56
|
+
height = 8,
|
|
57
|
+
width = '100%',
|
|
58
|
+
color,
|
|
59
|
+
backgroundColor,
|
|
60
|
+
shape = 'rounded',
|
|
61
|
+
showPercentage = false,
|
|
62
|
+
showValue = false,
|
|
63
|
+
textColor,
|
|
64
|
+
style,
|
|
65
|
+
testID,
|
|
66
|
+
}) => {
|
|
67
|
+
const tokens = useAppDesignTokens();
|
|
68
|
+
|
|
69
|
+
// Clamp value between 0 and 100
|
|
70
|
+
const clampedValue = Math.max(0, Math.min(100, value));
|
|
71
|
+
|
|
72
|
+
// Default colors
|
|
73
|
+
const progressColor = color || tokens.colors.primary;
|
|
74
|
+
const progressBackground = backgroundColor || tokens.colors.surfaceVariant;
|
|
75
|
+
const progressTextColor = textColor || tokens.colors.textPrimary;
|
|
76
|
+
|
|
77
|
+
// Calculate progress width
|
|
78
|
+
const progressWidth = `${clampedValue}%`;
|
|
79
|
+
|
|
80
|
+
// Border radius based on shape
|
|
81
|
+
const borderRadius = shape === 'rounded' ? height / 2 : 0;
|
|
82
|
+
|
|
83
|
+
const containerStyle: ViewStyle = {
|
|
84
|
+
width: width as DimensionValue,
|
|
85
|
+
height,
|
|
86
|
+
backgroundColor: progressBackground,
|
|
87
|
+
borderRadius,
|
|
88
|
+
overflow: 'hidden',
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const progressStyle: ViewStyle = {
|
|
92
|
+
width: progressWidth as DimensionValue,
|
|
93
|
+
height: '100%' as DimensionValue,
|
|
94
|
+
backgroundColor: progressColor,
|
|
95
|
+
borderRadius,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const textStyle = {
|
|
99
|
+
fontSize: tokens.typography.bodySmall.fontSize,
|
|
100
|
+
fontWeight: tokens.typography.labelMedium.fontWeight,
|
|
101
|
+
color: progressTextColor,
|
|
102
|
+
textAlign: 'center' as const,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<View
|
|
107
|
+
style={[containerStyle, style]}
|
|
108
|
+
testID={testID}
|
|
109
|
+
accessibilityRole="progressbar"
|
|
110
|
+
accessibilityValue={{ min: 0, max: 100, now: Math.round(clampedValue) }}
|
|
111
|
+
accessibilityLabel={`Progress: ${Math.round(clampedValue)}${showPercentage ? '%' : ''}`}
|
|
112
|
+
>
|
|
113
|
+
<View style={progressStyle} />
|
|
114
|
+
{(showPercentage || showValue) && (
|
|
115
|
+
<View style={styles.textContainer}>
|
|
116
|
+
<Text
|
|
117
|
+
style={textStyle}
|
|
118
|
+
accessibilityLiveRegion="polite"
|
|
119
|
+
accessibilityLabel={`Current progress: ${Math.round(clampedValue)}${showPercentage ? '%' : ''}`}
|
|
120
|
+
>
|
|
121
|
+
{showPercentage ? `${Math.round(clampedValue)}%` : `${Math.round(clampedValue)}`}
|
|
122
|
+
</Text>
|
|
123
|
+
</View>
|
|
124
|
+
)}
|
|
125
|
+
</View>
|
|
126
|
+
);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// =============================================================================
|
|
130
|
+
// STYLES
|
|
131
|
+
// =============================================================================
|
|
132
|
+
|
|
133
|
+
const styles = StyleSheet.create({
|
|
134
|
+
textContainer: {
|
|
135
|
+
position: 'absolute',
|
|
136
|
+
top: 0,
|
|
137
|
+
left: 0,
|
|
138
|
+
right: 0,
|
|
139
|
+
bottom: 0,
|
|
140
|
+
justifyContent: 'center',
|
|
141
|
+
alignItems: 'center',
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// =============================================================================
|
|
146
|
+
// EXPORTS
|
|
147
|
+
// =============================================================================
|
|
148
|
+
|
|
149
|
+
export default AtomicProgress;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Text, StyleProp, TextStyle } from 'react-native';
|
|
3
|
+
import { useAppDesignTokens } from '../theme';
|
|
4
|
+
import type { TextStyleVariant, ColorVariant } from '../typography';
|
|
5
|
+
import { getTextColor } from '../typography';
|
|
6
|
+
|
|
7
|
+
export interface AtomicTextProps {
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
type?: TextStyleVariant;
|
|
10
|
+
color?: ColorVariant | string;
|
|
11
|
+
numberOfLines?: number;
|
|
12
|
+
ellipsizeMode?: 'head' | 'middle' | 'tail' | 'clip';
|
|
13
|
+
textAlign?: 'auto' | 'left' | 'right' | 'center' | 'justify';
|
|
14
|
+
style?: StyleProp<TextStyle>;
|
|
15
|
+
testID?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const AtomicText: React.FC<AtomicTextProps> = ({
|
|
19
|
+
children,
|
|
20
|
+
type = 'bodyMedium',
|
|
21
|
+
color,
|
|
22
|
+
numberOfLines,
|
|
23
|
+
ellipsizeMode,
|
|
24
|
+
textAlign,
|
|
25
|
+
style,
|
|
26
|
+
testID,
|
|
27
|
+
}) => {
|
|
28
|
+
const tokens = useAppDesignTokens();
|
|
29
|
+
|
|
30
|
+
// Get typography style from tokens
|
|
31
|
+
const typographyStyle = (tokens.typography as Record<string, any>)[type];
|
|
32
|
+
|
|
33
|
+
// Get color from tokens or use custom color using utility function
|
|
34
|
+
const resolvedColor = getTextColor(color, tokens);
|
|
35
|
+
|
|
36
|
+
const textStyle: StyleProp<TextStyle> = [
|
|
37
|
+
typographyStyle,
|
|
38
|
+
{
|
|
39
|
+
color: resolvedColor,
|
|
40
|
+
...(textAlign && { textAlign }),
|
|
41
|
+
},
|
|
42
|
+
style,
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<Text
|
|
47
|
+
numberOfLines={numberOfLines}
|
|
48
|
+
ellipsizeMode={ellipsizeMode}
|
|
49
|
+
style={textStyle}
|
|
50
|
+
testID={testID}
|
|
51
|
+
>
|
|
52
|
+
{children}
|
|
53
|
+
</Text>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AtomicButton Tests
|
|
3
|
+
*
|
|
4
|
+
* Basic test cases for AtomicButton component
|
|
5
|
+
* These tests can be run by consuming applications
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import { render, fireEvent } from '@testing-library/react-native';
|
|
10
|
+
import { AtomicButton } from '../AtomicButton';
|
|
11
|
+
|
|
12
|
+
// Mock design tokens
|
|
13
|
+
jest.mock('@umituz/react-native-design-system-theme', () => ({
|
|
14
|
+
useAppDesignTokens: () => ({
|
|
15
|
+
colors: {
|
|
16
|
+
primary: '#007AFF',
|
|
17
|
+
onPrimary: '#FFFFFF',
|
|
18
|
+
secondary: '#8E8E93',
|
|
19
|
+
surface: '#F2F2F7',
|
|
20
|
+
border: '#C6C6C8',
|
|
21
|
+
},
|
|
22
|
+
spacing: {
|
|
23
|
+
xs: 4,
|
|
24
|
+
sm: 8,
|
|
25
|
+
md: 16,
|
|
26
|
+
lg: 24,
|
|
27
|
+
},
|
|
28
|
+
typography: {
|
|
29
|
+
bodyMedium: { fontSize: 16 },
|
|
30
|
+
labelLarge: { fontSize: 18 },
|
|
31
|
+
},
|
|
32
|
+
borders: {
|
|
33
|
+
radius: {
|
|
34
|
+
md: 8,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
}),
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
describe('AtomicButton', () => {
|
|
41
|
+
it('renders correctly with title', () => {
|
|
42
|
+
const { getByText } = render(
|
|
43
|
+
<AtomicButton title="Test Button" onPress={() => {}} />
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
expect(getByText('Test Button')).toBeTruthy();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('handles press events', () => {
|
|
50
|
+
const mockOnPress = jest.fn();
|
|
51
|
+
const { getByText } = render(
|
|
52
|
+
<AtomicButton title="Test Button" onPress={mockOnPress} />
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
fireEvent.press(getByText('Test Button'));
|
|
56
|
+
expect(mockOnPress).toHaveBeenCalledTimes(1);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('renders with different variants', () => {
|
|
60
|
+
const { getByText, rerender } = render(
|
|
61
|
+
<AtomicButton title="Primary" onPress={() => {}} variant="primary" />
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
expect(getByText('Primary')).toBeTruthy();
|
|
65
|
+
|
|
66
|
+
rerender(<AtomicButton title="Secondary" onPress={() => {}} variant="secondary" />);
|
|
67
|
+
expect(getByText('Secondary')).toBeTruthy();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('renders with different sizes', () => {
|
|
71
|
+
const { getByText, rerender } = render(
|
|
72
|
+
<AtomicButton title="Small" onPress={() => {}} size="sm" />
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
expect(getByText('Small')).toBeTruthy();
|
|
76
|
+
|
|
77
|
+
rerender(<AtomicButton title="Large" onPress={() => {}} size="lg" />);
|
|
78
|
+
expect(getByText('Large')).toBeTruthy();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('is disabled when disabled prop is true', () => {
|
|
82
|
+
const mockOnPress = jest.fn();
|
|
83
|
+
const { getByText } = render(
|
|
84
|
+
<AtomicButton title="Disabled" onPress={mockOnPress} disabled />
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const button = getByText('Disabled').parent;
|
|
88
|
+
expect(button).toBeDisabled();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('renders with icon', () => {
|
|
92
|
+
const { getByTestId } = render(
|
|
93
|
+
<AtomicButton title="With Icon" onPress={() => {}} icon="Settings" testID="test-button" />
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
expect(getByTestId('test-button')).toBeTruthy();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('applies custom styles', () => {
|
|
100
|
+
const customStyle = { backgroundColor: 'red' };
|
|
101
|
+
const { getByTestId } = render(
|
|
102
|
+
<AtomicButton title="Custom" onPress={() => {}} style={customStyle} testID="test-button" />
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
expect(getByTestId('test-button')).toBeTruthy();
|
|
106
|
+
});
|
|
107
|
+
});
|