@umituz/react-native-design-system 2.6.61 → 2.6.64
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 +1 -1
- package/src/atoms/AtomicButton.tsx +6 -257
- package/src/atoms/AtomicChip.tsx +4 -224
- package/src/atoms/AtomicIcon.tsx +2 -6
- package/src/atoms/AtomicIcon.types.ts +5 -0
- package/src/atoms/AtomicInput.tsx +34 -154
- package/src/atoms/AtomicPicker.tsx +31 -123
- package/src/atoms/button/AtomicButton.tsx +108 -0
- package/src/atoms/button/configs/buttonSizeConfig.ts +37 -0
- package/src/atoms/button/index.ts +6 -0
- package/src/atoms/button/styles/buttonStyles.ts +36 -0
- package/src/atoms/button/styles/buttonVariantStyles.ts +88 -0
- package/src/atoms/button/types/index.ts +40 -0
- package/src/atoms/chip/AtomicChip.tsx +112 -0
- package/src/atoms/chip/configs/chipColorConfig.ts +47 -0
- package/src/atoms/chip/configs/chipSizeConfig.ts +34 -0
- package/src/atoms/chip/index.ts +6 -0
- package/src/atoms/chip/styles/chipStyles.ts +28 -0
- package/src/atoms/chip/types/index.ts +42 -0
- package/src/atoms/index.ts +6 -4
- package/src/atoms/input/components/InputHelper.tsx +49 -0
- package/src/atoms/input/components/InputIcon.tsx +44 -0
- package/src/atoms/input/components/InputLabel.tsx +20 -0
- package/src/atoms/input/styles/inputStylesHelper.ts +1 -1
- package/src/atoms/input/types.ts +72 -0
- package/src/atoms/picker/hooks/usePickerState.ts +139 -0
- package/src/exports/atoms.ts +69 -0
- package/src/exports/device.ts +58 -0
- package/src/exports/layouts.ts +19 -0
- package/src/exports/molecules.ts +166 -0
- package/src/exports/organisms.ts +9 -0
- package/src/exports/responsive.ts +36 -0
- package/src/exports/safe-area.ts +6 -0
- package/src/exports/theme.ts +47 -0
- package/src/exports/typography.ts +22 -0
- package/src/exports/utilities.ts +6 -0
- package/src/exports/variants.ts +22 -0
- package/src/index.ts +11 -417
- package/src/layouts/ScreenLayout/ScreenLayout.tsx +17 -181
- package/src/layouts/ScreenLayout/components/ContentWrapper.tsx +31 -0
- package/src/layouts/ScreenLayout/components/index.ts +6 -0
- package/src/layouts/ScreenLayout/styles/screenLayoutStyles.ts +47 -0
- package/src/layouts/ScreenLayout/types/index.ts +27 -0
- package/src/molecules/avatar/Avatar.constants.ts +103 -0
- package/src/molecules/avatar/Avatar.types.ts +64 -0
- package/src/molecules/avatar/Avatar.utils.ts +8 -160
- package/src/molecules/calendar/index.ts +4 -9
- package/src/molecules/calendar/infrastructure/storage/CalendarStore.ts +103 -302
- package/src/molecules/calendar/infrastructure/storage/CalendarStore.ts.bak +116 -0
- package/src/molecules/calendar/infrastructure/storage/CalendarStore.types.ts +64 -0
- package/src/molecules/calendar/infrastructure/storage/CalendarStore.utils.ts +56 -0
- package/src/molecules/calendar/infrastructure/storage/EventActions.ts +140 -0
- package/src/molecules/calendar/infrastructure/storage/NavigationActions.ts +118 -0
- package/src/molecules/calendar/infrastructure/stores/storageAdapter.ts +34 -0
- package/src/molecules/calendar/infrastructure/stores/useCalendarEvents.ts +168 -0
- package/src/molecules/calendar/infrastructure/stores/useCalendarNavigation.ts +47 -0
- package/src/molecules/calendar/infrastructure/stores/useCalendarView.ts +24 -0
- package/src/molecules/calendar/presentation/hooks/useCalendar.ts +7 -11
- package/src/responsive/compute/computeDeviceInfo.ts +22 -0
- package/src/responsive/compute/computeResponsivePositioning.ts +42 -0
- package/src/responsive/compute/computeResponsiveSizes.ts +48 -0
- package/src/responsive/padding/paddingUtils.ts +65 -0
- package/src/responsive/positioning/positioningUtils.ts +61 -0
- package/src/responsive/responsiveLayout.ts +11 -264
- package/src/responsive/screen/screenLayoutConfig.ts +38 -0
- package/src/responsive/tabbar/tabBarConfig.ts +88 -0
- package/src/responsive/types/responsiveTypes.ts +69 -0
- package/src/responsive/useResponsive.ts +69 -158
|
@@ -1,78 +1,12 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { View, TextInput,
|
|
2
|
+
import { View, TextInput, StyleSheet, StyleProp, ViewStyle, TextStyle } from 'react-native';
|
|
3
3
|
import { useAppDesignTokens } from '../theme';
|
|
4
|
-
import { AtomicIcon } from './AtomicIcon';
|
|
5
|
-
import { AtomicText } from './AtomicText';
|
|
6
4
|
import { useInputState } from './input/hooks/useInputState';
|
|
7
5
|
import { getSizeConfig, getVariantStyle, getTextColor } from './input/styles/inputStylesHelper';
|
|
8
|
-
import type {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export type AtomicInputSize = 'sm' | 'md' | 'lg';
|
|
13
|
-
|
|
14
|
-
export interface AtomicInputProps {
|
|
15
|
-
/** Input label */
|
|
16
|
-
label?: string;
|
|
17
|
-
/** Current input value */
|
|
18
|
-
value?: string;
|
|
19
|
-
/** Value change callback */
|
|
20
|
-
onChangeText?: (text: string) => void;
|
|
21
|
-
/** Input variant (outlined, filled, flat) */
|
|
22
|
-
variant?: AtomicInputVariant;
|
|
23
|
-
/** Input state (default, error, success, disabled) */
|
|
24
|
-
state?: AtomicInputState;
|
|
25
|
-
/** Input size (sm, md, lg) */
|
|
26
|
-
size?: AtomicInputSize;
|
|
27
|
-
/** Placeholder text */
|
|
28
|
-
placeholder?: string;
|
|
29
|
-
/** Helper text below input */
|
|
30
|
-
helperText?: string;
|
|
31
|
-
/** Leading icon (Ionicons name) */
|
|
32
|
-
leadingIcon?: IconName;
|
|
33
|
-
/** Trailing icon (Ionicons name) */
|
|
34
|
-
trailingIcon?: IconName;
|
|
35
|
-
/** Callback when trailing icon is pressed */
|
|
36
|
-
onTrailingIconPress?: () => void;
|
|
37
|
-
/** Show password toggle for secure inputs */
|
|
38
|
-
showPasswordToggle?: boolean;
|
|
39
|
-
/** Secure text entry (password field) */
|
|
40
|
-
secureTextEntry?: boolean;
|
|
41
|
-
/** Maximum character length */
|
|
42
|
-
maxLength?: number;
|
|
43
|
-
/** Show character counter */
|
|
44
|
-
showCharacterCount?: boolean;
|
|
45
|
-
/** Keyboard type */
|
|
46
|
-
keyboardType?: 'default' | 'email-address' | 'numeric' | 'phone-pad' | 'url' | 'number-pad' | 'decimal-pad' | 'web-search' | 'twitter' | 'numeric' | 'visible-password';
|
|
47
|
-
/** Return key type */
|
|
48
|
-
returnKeyType?: 'done' | 'go' | 'next' | 'search' | 'send';
|
|
49
|
-
/** Callback when submit button is pressed */
|
|
50
|
-
onSubmitEditing?: () => void;
|
|
51
|
-
/** Blur on submit */
|
|
52
|
-
blurOnSubmit?: boolean;
|
|
53
|
-
/** Auto focus */
|
|
54
|
-
autoFocus?: boolean;
|
|
55
|
-
/** Auto-capitalize */
|
|
56
|
-
autoCapitalize?: 'none' | 'sentences' | 'words' | 'characters';
|
|
57
|
-
/** Auto-correct */
|
|
58
|
-
autoCorrect?: boolean;
|
|
59
|
-
/** Disabled state */
|
|
60
|
-
disabled?: boolean;
|
|
61
|
-
/** Container style */
|
|
62
|
-
style?: StyleProp<ViewStyle>;
|
|
63
|
-
/** Input text style */
|
|
64
|
-
inputStyle?: StyleProp<TextStyle>;
|
|
65
|
-
/** Test ID for E2E testing */
|
|
66
|
-
testID?: string;
|
|
67
|
-
/** Blur callback */
|
|
68
|
-
onBlur?: () => void;
|
|
69
|
-
/** Focus callback */
|
|
70
|
-
onFocus?: () => void;
|
|
71
|
-
/** Multiline input support */
|
|
72
|
-
multiline?: boolean;
|
|
73
|
-
/** Number of lines for multiline input */
|
|
74
|
-
numberOfLines?: number;
|
|
75
|
-
}
|
|
6
|
+
import type { AtomicInputProps } from './input/types';
|
|
7
|
+
import { InputLabel } from './input/components/InputLabel';
|
|
8
|
+
import { InputIcon } from './input/components/InputIcon';
|
|
9
|
+
import { InputHelper } from './input/components/InputHelper';
|
|
76
10
|
|
|
77
11
|
/**
|
|
78
12
|
* AtomicInput - Pure React Native Text Input
|
|
@@ -188,25 +122,16 @@ export const AtomicInput = React.forwardRef<TextInput, AtomicInputProps>(({
|
|
|
188
122
|
|
|
189
123
|
return (
|
|
190
124
|
<View testID={testID}>
|
|
191
|
-
{label
|
|
192
|
-
<AtomicText
|
|
193
|
-
type="labelMedium"
|
|
194
|
-
color={hasError ? 'error' : hasSuccess ? 'success' : 'secondary'}
|
|
195
|
-
style={styles.label}
|
|
196
|
-
>
|
|
197
|
-
{label}
|
|
198
|
-
</AtomicText>
|
|
199
|
-
)}
|
|
125
|
+
<InputLabel label={label} state={state} />
|
|
200
126
|
|
|
201
127
|
<View style={containerStyle}>
|
|
202
128
|
{leadingIcon && (
|
|
203
|
-
<
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
</View>
|
|
129
|
+
<InputIcon
|
|
130
|
+
name={leadingIcon}
|
|
131
|
+
size={sizeConfig.iconSize}
|
|
132
|
+
color={iconColor}
|
|
133
|
+
position="leading"
|
|
134
|
+
/>
|
|
210
135
|
)}
|
|
211
136
|
|
|
212
137
|
<TextInput
|
|
@@ -240,55 +165,36 @@ export const AtomicInput = React.forwardRef<TextInput, AtomicInputProps>(({
|
|
|
240
165
|
/>
|
|
241
166
|
|
|
242
167
|
{(showPasswordToggle && secureTextEntry) && (
|
|
243
|
-
<
|
|
168
|
+
<InputIcon
|
|
169
|
+
name={isPasswordVisible ? "eye-off-outline" : "eye-outline"}
|
|
170
|
+
size={sizeConfig.iconSize}
|
|
171
|
+
color={iconColor}
|
|
172
|
+
position="trailing"
|
|
244
173
|
onPress={() => togglePasswordVisibility()}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
<AtomicIcon
|
|
248
|
-
name={isPasswordVisible ? "eye-off-outline" : "eye-outline"}
|
|
249
|
-
customSize={sizeConfig.iconSize}
|
|
250
|
-
customColor={iconColor}
|
|
251
|
-
/>
|
|
252
|
-
</Pressable>
|
|
174
|
+
testID={testID ? `${testID}-toggle-password` : undefined}
|
|
175
|
+
/>
|
|
253
176
|
)}
|
|
254
177
|
|
|
255
178
|
{trailingIcon && !showPasswordToggle && (
|
|
256
|
-
<
|
|
179
|
+
<InputIcon
|
|
180
|
+
name={trailingIcon}
|
|
181
|
+
size={sizeConfig.iconSize}
|
|
182
|
+
color={iconColor}
|
|
183
|
+
position="trailing"
|
|
257
184
|
onPress={onTrailingIconPress}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
>
|
|
261
|
-
<AtomicIcon
|
|
262
|
-
name={trailingIcon}
|
|
263
|
-
customSize={sizeConfig.iconSize}
|
|
264
|
-
customColor={iconColor}
|
|
265
|
-
/>
|
|
266
|
-
</Pressable>
|
|
185
|
+
testID={testID ? `${testID}-trailing-icon` : undefined}
|
|
186
|
+
/>
|
|
267
187
|
)}
|
|
268
188
|
</View>
|
|
269
189
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
{helperText}
|
|
279
|
-
</AtomicText>
|
|
280
|
-
)}
|
|
281
|
-
{showCharacterCount && maxLength && (
|
|
282
|
-
<AtomicText
|
|
283
|
-
style={[styles.helperText, styles.characterCount]}
|
|
284
|
-
color="secondary"
|
|
285
|
-
testID={testID ? `${testID}-count` : undefined}
|
|
286
|
-
>
|
|
287
|
-
{characterCount}/{maxLength}
|
|
288
|
-
</AtomicText>
|
|
289
|
-
)}
|
|
290
|
-
</View>
|
|
291
|
-
)}
|
|
190
|
+
<InputHelper
|
|
191
|
+
helperText={helperText}
|
|
192
|
+
showCharacterCount={showCharacterCount}
|
|
193
|
+
characterCount={characterCount}
|
|
194
|
+
maxLength={maxLength}
|
|
195
|
+
state={state}
|
|
196
|
+
testID={testID}
|
|
197
|
+
/>
|
|
292
198
|
</View>
|
|
293
199
|
);
|
|
294
200
|
});
|
|
@@ -303,30 +209,4 @@ const styles = StyleSheet.create({
|
|
|
303
209
|
margin: 0,
|
|
304
210
|
padding: 0,
|
|
305
211
|
},
|
|
306
|
-
label: {
|
|
307
|
-
marginBottom: 4,
|
|
308
|
-
},
|
|
309
|
-
leadingIcon: {
|
|
310
|
-
position: 'absolute',
|
|
311
|
-
left: 12,
|
|
312
|
-
zIndex: 1,
|
|
313
|
-
},
|
|
314
|
-
trailingIcon: {
|
|
315
|
-
position: 'absolute',
|
|
316
|
-
right: 12,
|
|
317
|
-
zIndex: 1,
|
|
318
|
-
},
|
|
319
|
-
helperRow: {
|
|
320
|
-
flexDirection: 'row',
|
|
321
|
-
justifyContent: 'space-between',
|
|
322
|
-
marginTop: 4,
|
|
323
|
-
},
|
|
324
|
-
helperText: {
|
|
325
|
-
flex: 1,
|
|
326
|
-
},
|
|
327
|
-
characterCount: {
|
|
328
|
-
marginLeft: 8,
|
|
329
|
-
},
|
|
330
212
|
});
|
|
331
|
-
|
|
332
|
-
export type { AtomicInputProps as InputProps };
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
* @module AtomicPicker
|
|
41
41
|
*/
|
|
42
42
|
|
|
43
|
-
import React
|
|
43
|
+
import React from 'react';
|
|
44
44
|
import {
|
|
45
45
|
View,
|
|
46
46
|
TouchableOpacity,
|
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
getPickerValueStyles,
|
|
60
60
|
getPickerErrorStyles,
|
|
61
61
|
} from './picker/styles/pickerStyles';
|
|
62
|
+
import { usePickerState } from './picker/hooks/usePickerState';
|
|
62
63
|
|
|
63
64
|
export type { AtomicPickerProps, PickerOption, PickerSize } from './picker/types';
|
|
64
65
|
|
|
@@ -92,8 +93,14 @@ export const AtomicPicker: React.FC<AtomicPickerProps> = ({
|
|
|
92
93
|
}) => {
|
|
93
94
|
const tokens = useAppDesignTokens();
|
|
94
95
|
|
|
95
|
-
const
|
|
96
|
-
|
|
96
|
+
const pickerState = usePickerState({
|
|
97
|
+
value,
|
|
98
|
+
multiple,
|
|
99
|
+
options,
|
|
100
|
+
placeholder,
|
|
101
|
+
autoClose,
|
|
102
|
+
onChange,
|
|
103
|
+
});
|
|
97
104
|
|
|
98
105
|
// Get style helpers with design tokens
|
|
99
106
|
const containerStyles = getPickerContainerStyles(tokens);
|
|
@@ -102,110 +109,6 @@ export const AtomicPicker: React.FC<AtomicPickerProps> = ({
|
|
|
102
109
|
const valueStyles = getPickerValueStyles(tokens);
|
|
103
110
|
const errorStyles = getPickerErrorStyles(tokens);
|
|
104
111
|
|
|
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 || placeholder
|
|
147
|
-
: `${selectedOptions.length} selected`;
|
|
148
|
-
}
|
|
149
|
-
return selectedOptions[0]?.label || placeholder;
|
|
150
|
-
}, [selectedOptions, placeholder, multiple]);
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Handle modal open
|
|
154
|
-
*/
|
|
155
|
-
const openModal = () => {
|
|
156
|
-
if (disabled) return;
|
|
157
|
-
setModalVisible(true);
|
|
158
|
-
setSearchQuery('');
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Handle modal close
|
|
163
|
-
*/
|
|
164
|
-
const closeModal = () => {
|
|
165
|
-
setModalVisible(false);
|
|
166
|
-
setSearchQuery('');
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Handle option selection
|
|
171
|
-
*/
|
|
172
|
-
const handleSelect = (optionValue: string) => {
|
|
173
|
-
if (multiple) {
|
|
174
|
-
const newValues = selectedValues.includes(optionValue)
|
|
175
|
-
? selectedValues.filter((v) => v !== optionValue)
|
|
176
|
-
: [...selectedValues, optionValue];
|
|
177
|
-
onChange(newValues);
|
|
178
|
-
} else {
|
|
179
|
-
onChange(optionValue);
|
|
180
|
-
if (autoClose) {
|
|
181
|
-
closeModal();
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Handle clear selection
|
|
188
|
-
*/
|
|
189
|
-
const handleClear = () => {
|
|
190
|
-
onChange(multiple ? [] : '');
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Handle search query change
|
|
195
|
-
*/
|
|
196
|
-
const handleSearch = (query: string) => {
|
|
197
|
-
setSearchQuery(query);
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Handle chip removal
|
|
204
|
-
*/
|
|
205
|
-
const handleChipRemove = (value: string) => {
|
|
206
|
-
handleSelect(value);
|
|
207
|
-
};
|
|
208
|
-
|
|
209
112
|
const pickerContainerStyle = StyleSheet.flatten([
|
|
210
113
|
containerStyles.base,
|
|
211
114
|
containerStyles.size[size],
|
|
@@ -221,12 +124,17 @@ export const AtomicPicker: React.FC<AtomicPickerProps> = ({
|
|
|
221
124
|
]);
|
|
222
125
|
|
|
223
126
|
const pickerValueStyle = StyleSheet.flatten([
|
|
224
|
-
selectedOptions.length > 0 ? valueStyles.base : placeholderStyles.base,
|
|
225
|
-
selectedOptions.length > 0
|
|
127
|
+
pickerState.selectedOptions.length > 0 ? valueStyles.base : placeholderStyles.base,
|
|
128
|
+
pickerState.selectedOptions.length > 0
|
|
226
129
|
? valueStyles.size[size]
|
|
227
130
|
: placeholderStyles.size[size],
|
|
228
131
|
]);
|
|
229
132
|
|
|
133
|
+
const handleOpenModal = () => {
|
|
134
|
+
if (disabled) return;
|
|
135
|
+
pickerState.openModal();
|
|
136
|
+
};
|
|
137
|
+
|
|
230
138
|
return (
|
|
231
139
|
<View>
|
|
232
140
|
{/* Label */}
|
|
@@ -234,7 +142,7 @@ export const AtomicPicker: React.FC<AtomicPickerProps> = ({
|
|
|
234
142
|
|
|
235
143
|
{/* Picker Button */}
|
|
236
144
|
<TouchableOpacity
|
|
237
|
-
onPress={
|
|
145
|
+
onPress={handleOpenModal}
|
|
238
146
|
disabled={disabled}
|
|
239
147
|
accessibilityRole="button"
|
|
240
148
|
accessibilityLabel={label || placeholder}
|
|
@@ -244,15 +152,15 @@ export const AtomicPicker: React.FC<AtomicPickerProps> = ({
|
|
|
244
152
|
>
|
|
245
153
|
{/* Display Text */}
|
|
246
154
|
<AtomicText style={pickerValueStyle} numberOfLines={1}>
|
|
247
|
-
{displayText}
|
|
155
|
+
{pickerState.displayText}
|
|
248
156
|
</AtomicText>
|
|
249
157
|
|
|
250
158
|
{/* Icons */}
|
|
251
159
|
<View style={{ flexDirection: 'row', alignItems: 'center', gap: tokens.spacing.xs }}>
|
|
252
160
|
{/* Clear Button */}
|
|
253
|
-
{clearable && selectedOptions.length > 0 && !disabled && (
|
|
161
|
+
{clearable && pickerState.selectedOptions.length > 0 && !disabled && (
|
|
254
162
|
<TouchableOpacity
|
|
255
|
-
onPress={handleClear}
|
|
163
|
+
onPress={pickerState.handleClear}
|
|
256
164
|
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
|
257
165
|
accessibilityRole="button"
|
|
258
166
|
accessibilityLabel={clearAccessibilityLabel}
|
|
@@ -264,7 +172,7 @@ export const AtomicPicker: React.FC<AtomicPickerProps> = ({
|
|
|
264
172
|
|
|
265
173
|
{/* Dropdown Icon */}
|
|
266
174
|
<AtomicIcon
|
|
267
|
-
name={modalVisible ? 'ChevronUp' : 'ChevronDown'}
|
|
175
|
+
name={pickerState.modalVisible ? 'ChevronUp' : 'ChevronDown'}
|
|
268
176
|
size="sm"
|
|
269
177
|
color={disabled ? 'surfaceVariant' : 'secondary'}
|
|
270
178
|
/>
|
|
@@ -273,8 +181,8 @@ export const AtomicPicker: React.FC<AtomicPickerProps> = ({
|
|
|
273
181
|
|
|
274
182
|
{/* Selected Chips (Multi-select) */}
|
|
275
183
|
<PickerChips
|
|
276
|
-
selectedOptions={selectedOptions}
|
|
277
|
-
onRemoveChip={handleChipRemove}
|
|
184
|
+
selectedOptions={pickerState.selectedOptions}
|
|
185
|
+
onRemoveChip={pickerState.handleChipRemove}
|
|
278
186
|
testID={testID}
|
|
279
187
|
/>
|
|
280
188
|
|
|
@@ -283,16 +191,16 @@ export const AtomicPicker: React.FC<AtomicPickerProps> = ({
|
|
|
283
191
|
|
|
284
192
|
{/* Selection Modal */}
|
|
285
193
|
<PickerModal
|
|
286
|
-
visible={modalVisible}
|
|
287
|
-
onClose={closeModal}
|
|
194
|
+
visible={pickerState.modalVisible}
|
|
195
|
+
onClose={pickerState.closeModal}
|
|
288
196
|
options={options}
|
|
289
|
-
selectedValues={selectedValues}
|
|
290
|
-
onSelect={handleSelect}
|
|
197
|
+
selectedValues={pickerState.selectedValues}
|
|
198
|
+
onSelect={pickerState.handleSelect}
|
|
291
199
|
title={modalTitle || label}
|
|
292
200
|
searchable={searchable}
|
|
293
|
-
searchQuery={searchQuery}
|
|
294
|
-
onSearchChange={handleSearch}
|
|
295
|
-
filteredOptions={filteredOptions}
|
|
201
|
+
searchQuery={pickerState.searchQuery}
|
|
202
|
+
onSearchChange={pickerState.handleSearch}
|
|
203
|
+
filteredOptions={pickerState.filteredOptions}
|
|
296
204
|
multiple={multiple}
|
|
297
205
|
emptyMessage={emptyMessage}
|
|
298
206
|
searchPlaceholder={searchPlaceholder}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AtomicButton Component
|
|
3
|
+
* Refactored: Extracted configs, styles, and types
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from 'react';
|
|
7
|
+
import { StyleProp, ViewStyle, TextStyle, TouchableOpacity, View } from 'react-native';
|
|
8
|
+
import { AtomicText } from '../AtomicText';
|
|
9
|
+
import { AtomicIcon } from '../AtomicIcon';
|
|
10
|
+
import { AtomicSpinner } from '../AtomicSpinner';
|
|
11
|
+
import { useAppDesignTokens } from '../../theme';
|
|
12
|
+
import { getButtonSizeConfig } from './configs/buttonSizeConfig';
|
|
13
|
+
import { getVariantStyles } from './styles/buttonVariantStyles';
|
|
14
|
+
import { buttonStyles } from './styles/buttonStyles';
|
|
15
|
+
import type { AtomicButtonProps } from './types';
|
|
16
|
+
|
|
17
|
+
export const AtomicButton: React.FC<AtomicButtonProps> = React.memo(({
|
|
18
|
+
title,
|
|
19
|
+
children,
|
|
20
|
+
onPress,
|
|
21
|
+
variant = 'primary',
|
|
22
|
+
size = 'md',
|
|
23
|
+
disabled = false,
|
|
24
|
+
loading = false,
|
|
25
|
+
icon,
|
|
26
|
+
iconPosition = 'left',
|
|
27
|
+
fullWidth = false,
|
|
28
|
+
style,
|
|
29
|
+
textStyle,
|
|
30
|
+
activeOpacity = 0.8,
|
|
31
|
+
testID,
|
|
32
|
+
}) => {
|
|
33
|
+
const tokens = useAppDesignTokens();
|
|
34
|
+
|
|
35
|
+
const handlePress = () => {
|
|
36
|
+
if (!disabled && !loading) {
|
|
37
|
+
onPress();
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const isDisabled = disabled || loading;
|
|
42
|
+
const config = getButtonSizeConfig(size, tokens);
|
|
43
|
+
const variantStyles = getVariantStyles(variant, tokens);
|
|
44
|
+
const iconColor = variantStyles.text.color;
|
|
45
|
+
|
|
46
|
+
const containerStyle: StyleProp<ViewStyle> = [
|
|
47
|
+
buttonStyles.button,
|
|
48
|
+
{
|
|
49
|
+
paddingVertical: config.paddingVertical,
|
|
50
|
+
paddingHorizontal: config.paddingHorizontal,
|
|
51
|
+
minHeight: config.minHeight,
|
|
52
|
+
borderRadius: tokens.borders.radius.md,
|
|
53
|
+
},
|
|
54
|
+
variantStyles.container,
|
|
55
|
+
fullWidth ? buttonStyles.fullWidth : undefined,
|
|
56
|
+
isDisabled ? buttonStyles.disabled : undefined,
|
|
57
|
+
style,
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
const buttonTextStyle: StyleProp<TextStyle> = [
|
|
61
|
+
{
|
|
62
|
+
fontSize: config.fontSize,
|
|
63
|
+
fontWeight: '600',
|
|
64
|
+
},
|
|
65
|
+
variantStyles.text,
|
|
66
|
+
isDisabled ? buttonStyles.disabledText : undefined,
|
|
67
|
+
textStyle,
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
const buttonText = title || children;
|
|
71
|
+
const showIcon = icon;
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<TouchableOpacity
|
|
75
|
+
style={containerStyle}
|
|
76
|
+
onPress={handlePress}
|
|
77
|
+
activeOpacity={activeOpacity}
|
|
78
|
+
disabled={isDisabled}
|
|
79
|
+
testID={testID}
|
|
80
|
+
>
|
|
81
|
+
<View style={[buttonStyles.content, iconPosition === 'right' && buttonStyles.rowReverse]}>
|
|
82
|
+
{loading ? (
|
|
83
|
+
<AtomicSpinner
|
|
84
|
+
size="sm"
|
|
85
|
+
color={iconColor as string}
|
|
86
|
+
style={iconPosition === 'right' ? buttonStyles.iconRight : buttonStyles.iconLeft}
|
|
87
|
+
/>
|
|
88
|
+
) : showIcon ? (
|
|
89
|
+
<AtomicIcon
|
|
90
|
+
name={icon}
|
|
91
|
+
customSize={config.iconSize}
|
|
92
|
+
customColor={iconColor as string | undefined}
|
|
93
|
+
style={iconPosition === 'right' ? buttonStyles.iconRight : buttonStyles.iconLeft}
|
|
94
|
+
/>
|
|
95
|
+
) : null}
|
|
96
|
+
|
|
97
|
+
<AtomicText style={buttonTextStyle}>
|
|
98
|
+
{buttonText}
|
|
99
|
+
</AtomicText>
|
|
100
|
+
</View>
|
|
101
|
+
</TouchableOpacity>
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
AtomicButton.displayName = 'AtomicButton';
|
|
106
|
+
|
|
107
|
+
// Re-export types for convenience
|
|
108
|
+
export type { AtomicButtonProps, ButtonVariant, ButtonSize } from './types';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Button Size Configuration
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { DesignTokens } from '../../../theme';
|
|
6
|
+
import type { ButtonSize, ButtonSizeConfig } from '../types';
|
|
7
|
+
|
|
8
|
+
export const getButtonSizeConfig = (
|
|
9
|
+
size: ButtonSize,
|
|
10
|
+
tokens: DesignTokens,
|
|
11
|
+
): ButtonSizeConfig => {
|
|
12
|
+
const sizeConfigs: Record<ButtonSize, ButtonSizeConfig> = {
|
|
13
|
+
sm: {
|
|
14
|
+
paddingVertical: tokens.spacing.xs,
|
|
15
|
+
paddingHorizontal: tokens.spacing.sm,
|
|
16
|
+
fontSize: tokens.typography.bodySmall.responsiveFontSize,
|
|
17
|
+
iconSize: 16 * tokens.spacingMultiplier,
|
|
18
|
+
minHeight: 32 * tokens.spacingMultiplier,
|
|
19
|
+
},
|
|
20
|
+
md: {
|
|
21
|
+
paddingVertical: tokens.spacing.sm,
|
|
22
|
+
paddingHorizontal: tokens.spacing.md,
|
|
23
|
+
fontSize: tokens.typography.bodyMedium.responsiveFontSize,
|
|
24
|
+
iconSize: 20 * tokens.spacingMultiplier,
|
|
25
|
+
minHeight: 44 * tokens.spacingMultiplier,
|
|
26
|
+
},
|
|
27
|
+
lg: {
|
|
28
|
+
paddingVertical: tokens.spacing.md,
|
|
29
|
+
paddingHorizontal: tokens.spacing.lg,
|
|
30
|
+
fontSize: tokens.typography.bodyLarge.responsiveFontSize,
|
|
31
|
+
iconSize: 24 * tokens.spacingMultiplier,
|
|
32
|
+
minHeight: 52 * tokens.spacingMultiplier,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return sizeConfigs[size];
|
|
37
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Button Base Styles
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { StyleSheet } from 'react-native';
|
|
6
|
+
|
|
7
|
+
export const buttonStyles = StyleSheet.create({
|
|
8
|
+
button: {
|
|
9
|
+
alignItems: 'center',
|
|
10
|
+
justifyContent: 'center',
|
|
11
|
+
flexDirection: 'row',
|
|
12
|
+
},
|
|
13
|
+
content: {
|
|
14
|
+
flexDirection: 'row',
|
|
15
|
+
alignItems: 'center',
|
|
16
|
+
justifyContent: 'center',
|
|
17
|
+
},
|
|
18
|
+
rowReverse: {
|
|
19
|
+
flexDirection: 'row-reverse',
|
|
20
|
+
},
|
|
21
|
+
fullWidth: {
|
|
22
|
+
width: '100%',
|
|
23
|
+
},
|
|
24
|
+
disabled: {
|
|
25
|
+
opacity: 0.5,
|
|
26
|
+
},
|
|
27
|
+
disabledText: {
|
|
28
|
+
opacity: 0.7,
|
|
29
|
+
},
|
|
30
|
+
iconLeft: {
|
|
31
|
+
marginRight: 8,
|
|
32
|
+
},
|
|
33
|
+
iconRight: {
|
|
34
|
+
marginLeft: 8,
|
|
35
|
+
},
|
|
36
|
+
});
|