@umituz/react-native-design-system 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +157 -0
  3. package/package.json +43 -0
  4. package/src/index.ts +345 -0
  5. package/src/presentation/atoms/AtomicAvatar.tsx +157 -0
  6. package/src/presentation/atoms/AtomicAvatarGroup.tsx +169 -0
  7. package/src/presentation/atoms/AtomicBadge.tsx +232 -0
  8. package/src/presentation/atoms/AtomicButton.tsx +124 -0
  9. package/src/presentation/atoms/AtomicCard.tsx +112 -0
  10. package/src/presentation/atoms/AtomicChip.tsx +223 -0
  11. package/src/presentation/atoms/AtomicDatePicker.tsx +347 -0
  12. package/src/presentation/atoms/AtomicDivider.tsx +114 -0
  13. package/src/presentation/atoms/AtomicFab.tsx +104 -0
  14. package/src/presentation/atoms/AtomicFilter.tsx +154 -0
  15. package/src/presentation/atoms/AtomicFormError.tsx +105 -0
  16. package/src/presentation/atoms/AtomicIcon.tsx +29 -0
  17. package/src/presentation/atoms/AtomicImage.tsx +149 -0
  18. package/src/presentation/atoms/AtomicInput.tsx +232 -0
  19. package/src/presentation/atoms/AtomicNumberInput.tsx +182 -0
  20. package/src/presentation/atoms/AtomicPicker.tsx +458 -0
  21. package/src/presentation/atoms/AtomicProgress.tsx +143 -0
  22. package/src/presentation/atoms/AtomicSearchBar.tsx +114 -0
  23. package/src/presentation/atoms/AtomicSkeleton.tsx +146 -0
  24. package/src/presentation/atoms/AtomicSort.tsx +145 -0
  25. package/src/presentation/atoms/AtomicSwitch.tsx +166 -0
  26. package/src/presentation/atoms/AtomicText.tsx +50 -0
  27. package/src/presentation/atoms/AtomicTextArea.tsx +198 -0
  28. package/src/presentation/atoms/AtomicTouchable.tsx +233 -0
  29. package/src/presentation/atoms/fab/styles/fabStyles.ts +69 -0
  30. package/src/presentation/atoms/fab/types/index.ts +88 -0
  31. package/src/presentation/atoms/filter/styles/filterStyles.ts +32 -0
  32. package/src/presentation/atoms/filter/types/index.ts +89 -0
  33. package/src/presentation/atoms/index.ts +378 -0
  34. package/src/presentation/atoms/input/hooks/useInputState.ts +15 -0
  35. package/src/presentation/atoms/input/styles/inputStyles.ts +66 -0
  36. package/src/presentation/atoms/input/types/index.ts +25 -0
  37. package/src/presentation/atoms/picker/styles/pickerStyles.ts +200 -0
  38. package/src/presentation/atoms/picker/types/index.ts +40 -0
  39. package/src/presentation/atoms/touchable/styles/touchableStyles.ts +71 -0
  40. package/src/presentation/atoms/touchable/types/index.ts +162 -0
  41. package/src/presentation/hooks/useAppDesignTokens.ts +78 -0
  42. package/src/presentation/hooks/useResponsive.ts +180 -0
  43. package/src/presentation/loading/index.ts +40 -0
  44. package/src/presentation/loading/presentation/components/LoadingSpinner.tsx +116 -0
  45. package/src/presentation/loading/presentation/components/LoadingState.tsx +200 -0
  46. package/src/presentation/loading/presentation/hooks/useLoading.ts +100 -0
  47. package/src/presentation/molecules/AtomicConfirmationModal.tsx +263 -0
  48. package/src/presentation/molecules/EmptyState.tsx +130 -0
  49. package/src/presentation/molecules/FormField.tsx +128 -0
  50. package/src/presentation/molecules/GridContainer.tsx +124 -0
  51. package/src/presentation/molecules/IconContainer.tsx +94 -0
  52. package/src/presentation/molecules/LanguageSwitcher.tsx +42 -0
  53. package/src/presentation/molecules/ListItem.tsx +36 -0
  54. package/src/presentation/molecules/ScreenHeader.tsx +140 -0
  55. package/src/presentation/molecules/SearchBar.tsx +85 -0
  56. package/src/presentation/molecules/SectionCard.tsx +74 -0
  57. package/src/presentation/molecules/SectionContainer.tsx +106 -0
  58. package/src/presentation/molecules/SectionHeader.tsx +125 -0
  59. package/src/presentation/molecules/confirmation-modal/styles/confirmationModalStyles.ts +133 -0
  60. package/src/presentation/molecules/confirmation-modal/types/index.ts +107 -0
  61. package/src/presentation/molecules/index.ts +42 -0
  62. package/src/presentation/molecules/languageswitcher/config/languageSwitcherConfig.ts +5 -0
  63. package/src/presentation/molecules/languageswitcher/hooks/useLanguageNavigation.ts +15 -0
  64. package/src/presentation/molecules/listitem/styles/listItemStyles.ts +19 -0
  65. package/src/presentation/molecules/listitem/types/index.ts +17 -0
  66. package/src/presentation/organisms/AppHeader.tsx +136 -0
  67. package/src/presentation/organisms/FormContainer.tsx +180 -0
  68. package/src/presentation/organisms/ScreenLayout.tsx +209 -0
  69. package/src/presentation/organisms/index.ts +25 -0
  70. package/src/presentation/tokens/AppDesignTokens.ts +57 -0
  71. package/src/presentation/tokens/commonStyles.ts +253 -0
  72. package/src/presentation/tokens/core/BaseTokens.ts +394 -0
  73. package/src/presentation/tokens/core/ColorPalette.ts +398 -0
  74. package/src/presentation/tokens/core/TokenFactory.ts +120 -0
  75. package/src/presentation/utils/platformConstants.ts +124 -0
  76. package/src/presentation/utils/responsive.ts +516 -0
  77. package/src/presentation/utils/variants/compound.ts +29 -0
  78. package/src/presentation/utils/variants/core.ts +39 -0
  79. package/src/presentation/utils/variants/helpers.ts +13 -0
  80. package/src/presentation/utils/variants.ts +3 -0
@@ -0,0 +1,458 @@
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
+ Modal,
48
+ FlatList,
49
+ TextInput,
50
+ useWindowDimensions,
51
+ StyleSheet,
52
+ } from 'react-native';
53
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
54
+ import { useAppDesignTokens } from '../hooks/useAppDesignTokens';
55
+ import { AtomicPickerProps, PickerOption } from './picker/types';
56
+ import { AtomicIcon } from './AtomicIcon';
57
+ import { AtomicText } from './AtomicText';
58
+ import {
59
+ getPickerContainerStyles,
60
+ getPickerLabelStyles,
61
+ getPickerPlaceholderStyles,
62
+ getPickerValueStyles,
63
+ getPickerErrorStyles,
64
+ getModalOverlayStyles,
65
+ getModalContainerStyles,
66
+ getModalHeaderStyles,
67
+ getModalTitleStyles,
68
+ getSearchContainerStyles,
69
+ getSearchInputStyles,
70
+ getOptionContainerStyles,
71
+ getOptionTextStyles,
72
+ getOptionDescriptionStyles,
73
+ getEmptyStateStyles,
74
+ getEmptyStateTextStyles,
75
+ getChipContainerStyles,
76
+ getChipStyles,
77
+ getChipTextStyles,
78
+ } from './picker/styles/pickerStyles';
79
+
80
+ export type { AtomicPickerProps, PickerOption, PickerSize } from './picker/types';
81
+
82
+ /**
83
+ * AtomicPicker - Universal option picker component
84
+ *
85
+ * Displays a button that opens a modal for selection.
86
+ * Supports single/multi-select, search, and custom rendering.
87
+ */
88
+ export const AtomicPicker: React.FC<AtomicPickerProps> = ({
89
+ value,
90
+ onChange,
91
+ options,
92
+ label,
93
+ placeholder = 'Select...',
94
+ error,
95
+ disabled = false,
96
+ multiple = false,
97
+ searchable = false,
98
+ clearable = false,
99
+ autoClose = true,
100
+ color = 'primary',
101
+ size = 'md',
102
+ modalTitle,
103
+ emptyMessage = 'No options available',
104
+ style,
105
+ labelStyle,
106
+ testID,
107
+ }) => {
108
+ const tokens = useAppDesignTokens();
109
+ const { height } = useWindowDimensions();
110
+ const insets = useSafeAreaInsets();
111
+
112
+ const [modalVisible, setModalVisible] = useState(false);
113
+ const [searchQuery, setSearchQuery] = useState('');
114
+
115
+ // Get style helpers with design tokens
116
+ const containerStyles = getPickerContainerStyles(tokens);
117
+ const labelStyles = getPickerLabelStyles(tokens);
118
+ const placeholderStyles = getPickerPlaceholderStyles(tokens);
119
+ const valueStyles = getPickerValueStyles(tokens);
120
+ const errorStyles = getPickerErrorStyles(tokens);
121
+ const modalOverlayStyles = getModalOverlayStyles(tokens);
122
+ const modalContainerStyles = getModalContainerStyles(tokens, height * 0.85);
123
+ const modalHeaderStyles = getModalHeaderStyles(tokens);
124
+ const modalTitleStyles = getModalTitleStyles(tokens);
125
+ const searchContainerStyles = getSearchContainerStyles(tokens);
126
+ const searchInputStyles = getSearchInputStyles(tokens);
127
+ const emptyStateStyles = getEmptyStateStyles(tokens);
128
+ const emptyStateTextStyles = getEmptyStateTextStyles(tokens);
129
+ const chipContainerStyles = getChipContainerStyles(tokens);
130
+ const chipStyles = getChipStyles(tokens);
131
+ const chipTextStyles = getChipTextStyles(tokens);
132
+
133
+ /**
134
+ * Normalize value to array for consistent handling
135
+ */
136
+ const selectedValues = useMemo(() => {
137
+ if (multiple) {
138
+ return Array.isArray(value) ? value : [];
139
+ }
140
+ return value ? [value as string] : [];
141
+ }, [value, multiple]);
142
+
143
+ /**
144
+ * Get selected option objects
145
+ */
146
+ const selectedOptions = useMemo(() => {
147
+ return options.filter((opt) => selectedValues.includes(opt.value));
148
+ }, [options, selectedValues]);
149
+
150
+ /**
151
+ * Filter options based on search query
152
+ */
153
+ const filteredOptions = useMemo(() => {
154
+ if (!searchQuery.trim()) return options;
155
+
156
+ const query = searchQuery.toLowerCase();
157
+ return options.filter(
158
+ (opt) =>
159
+ opt.label.toLowerCase().includes(query) ||
160
+ opt.description?.toLowerCase().includes(query)
161
+ );
162
+ }, [options, searchQuery]);
163
+
164
+ /**
165
+ * Format display text for selected value(s)
166
+ */
167
+ const displayText = useMemo(() => {
168
+ if (selectedOptions.length === 0) {
169
+ return placeholder;
170
+ }
171
+
172
+ if (multiple) {
173
+ return selectedOptions.length === 1
174
+ ? selectedOptions[0].label
175
+ : `${selectedOptions.length} selected`;
176
+ }
177
+
178
+ return selectedOptions[0]?.label || placeholder;
179
+ }, [selectedOptions, placeholder, multiple]);
180
+
181
+ /**
182
+ * Handle modal open
183
+ */
184
+ const openModal = () => {
185
+ if (disabled) return;
186
+ setModalVisible(true);
187
+ setSearchQuery('');
188
+ };
189
+
190
+ /**
191
+ * Handle modal close
192
+ */
193
+ const closeModal = () => {
194
+ setModalVisible(false);
195
+ setSearchQuery('');
196
+ };
197
+
198
+ /**
199
+ * Handle option selection
200
+ */
201
+ const handleSelect = (optionValue: string) => {
202
+ if (multiple) {
203
+ const newValues = selectedValues.includes(optionValue)
204
+ ? selectedValues.filter((v) => v !== optionValue)
205
+ : [...selectedValues, optionValue];
206
+ onChange(newValues);
207
+ } else {
208
+ onChange(optionValue);
209
+ if (autoClose) {
210
+ closeModal();
211
+ }
212
+ }
213
+ };
214
+
215
+ /**
216
+ * Handle clear selection
217
+ */
218
+ const handleClear = () => {
219
+ onChange(multiple ? [] : '');
220
+ };
221
+
222
+ /**
223
+ * Handle search query change
224
+ */
225
+ const handleSearch = (query: string) => {
226
+ setSearchQuery(query);
227
+ };
228
+
229
+ /**
230
+ * Check if option is selected
231
+ */
232
+ const isSelected = (optionValue: string): boolean => {
233
+ return selectedValues.includes(optionValue);
234
+ };
235
+
236
+ /**
237
+ * Render single option
238
+ */
239
+ const renderOption = ({ item }: { item: PickerOption }) => {
240
+ const selected = isSelected(item.value);
241
+ const itemDisabled = item.disabled || false;
242
+
243
+ const optionContainerStyle = getOptionContainerStyles(
244
+ tokens,
245
+ selected,
246
+ itemDisabled
247
+ );
248
+ const optionTextStyle = getOptionTextStyles(tokens, selected);
249
+ const optionDescriptionStyle = getOptionDescriptionStyles(tokens);
250
+
251
+ return (
252
+ <TouchableOpacity
253
+ onPress={() => !itemDisabled && handleSelect(item.value)}
254
+ disabled={itemDisabled}
255
+ testID={item.testID || `${testID}-option-${item.value}`}
256
+ style={optionContainerStyle}
257
+ >
258
+ {/* Option Icon */}
259
+ {item.icon && (
260
+ <AtomicIcon
261
+ name={item.icon}
262
+ size="md"
263
+ color={selected ? 'primary' : 'secondary'}
264
+ />
265
+ )}
266
+
267
+ {/* Option Content */}
268
+ <View style={{ flex: 1 }}>
269
+ <AtomicText style={optionTextStyle}>{item.label}</AtomicText>
270
+ {item.description && (
271
+ <AtomicText style={optionDescriptionStyle}>
272
+ {item.description}
273
+ </AtomicText>
274
+ )}
275
+ </View>
276
+
277
+ {/* Selected Indicator */}
278
+ {selected && (
279
+ <AtomicIcon name="CheckCircle" size="md" color="primary" />
280
+ )}
281
+ </TouchableOpacity>
282
+ );
283
+ };
284
+
285
+ /**
286
+ * Render selected chips for multi-select
287
+ */
288
+ const renderSelectedChips = () => {
289
+ if (!multiple || selectedOptions.length === 0) return null;
290
+
291
+ return (
292
+ <View style={chipContainerStyles}>
293
+ {selectedOptions.map((opt) => (
294
+ <View key={opt.value} style={chipStyles}>
295
+ <AtomicText style={chipTextStyles}>{opt.label}</AtomicText>
296
+ <TouchableOpacity
297
+ onPress={(e) => {
298
+ e.stopPropagation();
299
+ handleSelect(opt.value);
300
+ }}
301
+ hitSlop={{ top: 4, bottom: 4, left: 4, right: 4 }}
302
+ >
303
+ <AtomicIcon name="X" size="sm" color="primary" />
304
+ </TouchableOpacity>
305
+ </View>
306
+ ))}
307
+ </View>
308
+ );
309
+ };
310
+
311
+ const pickerContainerStyle = StyleSheet.flatten([
312
+ containerStyles.base,
313
+ containerStyles.size[size],
314
+ error ? containerStyles.state.error : undefined,
315
+ disabled ? containerStyles.state.disabled : undefined,
316
+ style,
317
+ ]);
318
+
319
+ const pickerLabelStyle = StyleSheet.flatten([
320
+ labelStyles.base,
321
+ labelStyles.size[size],
322
+ labelStyle,
323
+ ]);
324
+
325
+ const pickerValueStyle = StyleSheet.flatten([
326
+ selectedOptions.length > 0 ? valueStyles.base : placeholderStyles.base,
327
+ selectedOptions.length > 0
328
+ ? valueStyles.size[size]
329
+ : placeholderStyles.size[size],
330
+ ]);
331
+
332
+ return (
333
+ <View>
334
+ {/* Label */}
335
+ {label && <AtomicText style={pickerLabelStyle}>{label}</AtomicText>}
336
+
337
+ {/* Picker Button */}
338
+ <TouchableOpacity
339
+ onPress={openModal}
340
+ disabled={disabled}
341
+ accessibilityRole="button"
342
+ accessibilityLabel={label || placeholder}
343
+ accessibilityState={{ disabled }}
344
+ testID={testID}
345
+ style={pickerContainerStyle}
346
+ >
347
+ {/* Display Text */}
348
+ <AtomicText style={pickerValueStyle} numberOfLines={1}>
349
+ {displayText}
350
+ </AtomicText>
351
+
352
+ {/* Icons */}
353
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: tokens.spacing.xs }}>
354
+ {/* Clear Button */}
355
+ {clearable && selectedOptions.length > 0 && !disabled && (
356
+ <TouchableOpacity
357
+ onPress={handleClear}
358
+ hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
359
+ accessibilityRole="button"
360
+ accessibilityLabel="Clear selection"
361
+ testID={`${testID}-clear`}
362
+ >
363
+ <AtomicIcon name="cancel" size="sm" color="secondary" />
364
+ </TouchableOpacity>
365
+ )}
366
+
367
+ {/* Dropdown Icon */}
368
+ <AtomicIcon
369
+ name={modalVisible ? 'expand-less' : 'expand-more'}
370
+ size="sm"
371
+ color={disabled ? 'surfaceVariant' : 'secondary'}
372
+ />
373
+ </View>
374
+ </TouchableOpacity>
375
+
376
+ {/* Selected Chips (Multi-select) */}
377
+ {renderSelectedChips()}
378
+
379
+ {/* Error Message */}
380
+ {error && <AtomicText style={errorStyles}>{error}</AtomicText>}
381
+
382
+ {/* Selection Modal */}
383
+ <Modal
384
+ visible={modalVisible}
385
+ animationType="slide"
386
+ transparent
387
+ onRequestClose={closeModal}
388
+ testID={`${testID}-modal`}
389
+ >
390
+ <View style={modalOverlayStyles}>
391
+ <View
392
+ style={[
393
+ modalContainerStyles,
394
+ { paddingBottom: insets.bottom + tokens.spacing.md },
395
+ ]}
396
+ >
397
+ {/* Modal Header */}
398
+ <View style={modalHeaderStyles}>
399
+ {/* Title */}
400
+ <AtomicText style={modalTitleStyles}>
401
+ {modalTitle || label || 'Select'}
402
+ </AtomicText>
403
+
404
+ {/* Close Button */}
405
+ <TouchableOpacity
406
+ onPress={closeModal}
407
+ hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
408
+ accessibilityRole="button"
409
+ accessibilityLabel="Close picker"
410
+ testID={`${testID}-close`}
411
+ >
412
+ <AtomicIcon name="X" size="md" color="primary" />
413
+ </TouchableOpacity>
414
+ </View>
415
+
416
+ {/* Search Bar */}
417
+ {searchable && (
418
+ <View style={searchContainerStyles}>
419
+ <AtomicIcon name="Search" size="sm" color="secondary" />
420
+ <TextInput
421
+ value={searchQuery}
422
+ onChangeText={handleSearch}
423
+ placeholder="Search..."
424
+ placeholderTextColor={tokens.colors.textSecondary}
425
+ style={searchInputStyles}
426
+ testID={`${testID}-search`}
427
+ />
428
+ {searchQuery.length > 0 && (
429
+ <TouchableOpacity onPress={() => handleSearch('')}>
430
+ <AtomicIcon name="cancel" size="sm" color="secondary" />
431
+ </TouchableOpacity>
432
+ )}
433
+ </View>
434
+ )}
435
+
436
+ {/* Options List */}
437
+ {filteredOptions.length > 0 ? (
438
+ <FlatList
439
+ data={filteredOptions}
440
+ keyExtractor={(item) => item.value}
441
+ renderItem={renderOption}
442
+ showsVerticalScrollIndicator
443
+ testID={`${testID}-list`}
444
+ />
445
+ ) : (
446
+ <View style={emptyStateStyles}>
447
+ <AtomicIcon name="error-outline" size="xl" color="secondary" />
448
+ <AtomicText style={emptyStateTextStyles}>
449
+ {emptyMessage}
450
+ </AtomicText>
451
+ </View>
452
+ )}
453
+ </View>
454
+ </View>
455
+ </Modal>
456
+ </View>
457
+ );
458
+ };
@@ -0,0 +1,143 @@
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
+ * - Loading progress
14
+ * - Achievement progress
15
+ * - Form completion
16
+ */
17
+
18
+ import React from 'react';
19
+ import { View, StyleSheet, ViewStyle, DimensionValue, Text } from 'react-native';
20
+ import { useAppDesignTokens } from '../hooks/useAppDesignTokens';
21
+
22
+ // =============================================================================
23
+ // TYPE DEFINITIONS
24
+ // =============================================================================
25
+
26
+ export interface AtomicProgressProps {
27
+ /** Progress value (0-100) */
28
+ value: number;
29
+ /** Progress bar height */
30
+ height?: number;
31
+ /** Progress bar width */
32
+ width?: number | string;
33
+ /** Progress bar color */
34
+ color?: string;
35
+ /** Background color */
36
+ backgroundColor?: string;
37
+ /** Progress bar shape */
38
+ shape?: 'rounded' | 'square';
39
+ /** Whether to show percentage text */
40
+ showPercentage?: boolean;
41
+ /** Whether to show value text */
42
+ showValue?: boolean;
43
+ /** Custom text color */
44
+ textColor?: string;
45
+ /** Animation duration in milliseconds */
46
+ animationDuration?: number;
47
+ /** Style overrides */
48
+ style?: ViewStyle | ViewStyle[];
49
+ /** Test ID for testing */
50
+ testID?: string;
51
+ }
52
+
53
+ // =============================================================================
54
+ // COMPONENT IMPLEMENTATION
55
+ // =============================================================================
56
+
57
+ export const AtomicProgress: React.FC<AtomicProgressProps> = ({
58
+ value,
59
+ height = 8,
60
+ width = '100%',
61
+ color,
62
+ backgroundColor,
63
+ shape = 'rounded',
64
+ showPercentage = false,
65
+ showValue = false,
66
+ textColor,
67
+ animationDuration = 300,
68
+ style,
69
+ testID,
70
+ }) => {
71
+ const tokens = useAppDesignTokens();
72
+
73
+ // Clamp value between 0 and 100
74
+ const clampedValue = Math.max(0, Math.min(100, value));
75
+
76
+ // Default colors
77
+ const progressColor = color || tokens.colors.primary;
78
+ const progressBackground = backgroundColor || tokens.colors.surfaceVariant;
79
+ const progressTextColor = textColor || tokens.colors.textPrimary;
80
+
81
+ // Calculate progress width
82
+ const progressWidth = `${clampedValue}%`;
83
+
84
+ // Border radius based on shape
85
+ const borderRadius = shape === 'rounded' ? height / 2 : 0;
86
+
87
+ const containerStyle: ViewStyle = {
88
+ width: width as DimensionValue,
89
+ height,
90
+ backgroundColor: progressBackground,
91
+ borderRadius,
92
+ overflow: 'hidden',
93
+ };
94
+
95
+ const progressStyle: ViewStyle = {
96
+ width: progressWidth as DimensionValue,
97
+ height: '100%' as DimensionValue,
98
+ backgroundColor: progressColor,
99
+ borderRadius,
100
+ };
101
+
102
+ const textStyle = {
103
+ fontSize: tokens.typography.bodySmall.fontSize,
104
+ fontWeight: tokens.typography.labelMedium.fontWeight,
105
+ color: progressTextColor,
106
+ textAlign: 'center' as const,
107
+ };
108
+
109
+ return (
110
+ <View style={[containerStyle, style]} testID={testID}>
111
+ <View style={progressStyle} />
112
+ {(showPercentage || showValue) && (
113
+ <View style={styles.textContainer}>
114
+ <Text style={textStyle}>
115
+ {showPercentage ? `${Math.round(clampedValue)}%` : `${Math.round(clampedValue)}`}
116
+ </Text>
117
+ </View>
118
+ )}
119
+ </View>
120
+ );
121
+ };
122
+
123
+ // =============================================================================
124
+ // STYLES
125
+ // =============================================================================
126
+
127
+ const styles = StyleSheet.create({
128
+ textContainer: {
129
+ position: 'absolute',
130
+ top: 0,
131
+ left: 0,
132
+ right: 0,
133
+ bottom: 0,
134
+ justifyContent: 'center',
135
+ alignItems: 'center',
136
+ },
137
+ });
138
+
139
+ // =============================================================================
140
+ // EXPORTS
141
+ // =============================================================================
142
+
143
+ export default AtomicProgress;
@@ -0,0 +1,114 @@
1
+ import React from 'react';
2
+ import { View, TextInput, TouchableOpacity, StyleSheet } from 'react-native';
3
+ import type { StyleProp, ViewStyle, TextStyle } from 'react-native';
4
+ import { useAppDesignTokens } from '../hooks/useAppDesignTokens';
5
+ import { AtomicIcon } from './AtomicIcon';
6
+
7
+ export interface AtomicSearchBarProps {
8
+ value: string;
9
+ onChangeText: (text: string) => void;
10
+ placeholder?: string;
11
+ autoFocus?: boolean;
12
+ editable?: boolean;
13
+ onClear?: () => void;
14
+ onFocus?: () => void;
15
+ onBlur?: () => void;
16
+ onSubmitEditing?: () => void;
17
+ style?: StyleProp<ViewStyle>;
18
+ inputStyle?: StyleProp<TextStyle>;
19
+ accessibilityLabel?: string;
20
+ accessibilityHint?: string;
21
+ }
22
+
23
+ export const AtomicSearchBar: React.FC<AtomicSearchBarProps> = ({
24
+ value,
25
+ onChangeText,
26
+ placeholder = 'Search...',
27
+ autoFocus = false,
28
+ editable = true,
29
+ onClear,
30
+ onFocus,
31
+ onBlur,
32
+ onSubmitEditing,
33
+ style,
34
+ inputStyle,
35
+ accessibilityLabel = 'Search input',
36
+ accessibilityHint,
37
+ }) => {
38
+ const tokens = useAppDesignTokens();
39
+
40
+ const handleClear = () => {
41
+ onChangeText('');
42
+ onClear?.();
43
+ };
44
+
45
+ return (
46
+ <View
47
+ style={[
48
+ {
49
+ flexDirection: 'row',
50
+ alignItems: 'center',
51
+ backgroundColor: tokens.colors.surfaceSecondary,
52
+ borderRadius: tokens.borders.radius.lg,
53
+ borderWidth: 1,
54
+ borderColor: tokens.colors.border,
55
+ paddingHorizontal: tokens.spacing.md,
56
+ paddingVertical: tokens.spacing.sm,
57
+ gap: tokens.spacing.sm,
58
+ minHeight: 48,
59
+ },
60
+ style,
61
+ ]}
62
+ accessibilityRole="search"
63
+ >
64
+ <AtomicIcon
65
+ name="Search"
66
+ size="sm"
67
+ color="secondary"
68
+ />
69
+
70
+ <TextInput
71
+ value={value}
72
+ onChangeText={onChangeText}
73
+ placeholder={placeholder}
74
+ placeholderTextColor={tokens.colors.textSecondary}
75
+ autoFocus={autoFocus}
76
+ editable={editable}
77
+ onFocus={onFocus}
78
+ onBlur={onBlur}
79
+ onSubmitEditing={onSubmitEditing}
80
+ style={[
81
+ {
82
+ flex: 1,
83
+ ...tokens.typography.bodyMedium,
84
+ color: tokens.colors.textPrimary,
85
+ padding: 0,
86
+ margin: 0,
87
+ },
88
+ inputStyle,
89
+ ]}
90
+ accessibilityLabel={accessibilityLabel}
91
+ accessibilityHint={accessibilityHint}
92
+ returnKeyType="search"
93
+ underlineColorAndroid="transparent"
94
+ />
95
+
96
+ {value.length > 0 && (
97
+ <TouchableOpacity
98
+ onPress={handleClear}
99
+ style={{
100
+ padding: tokens.spacing.xs,
101
+ }}
102
+ accessibilityLabel="Clear search"
103
+ accessibilityRole="button"
104
+ >
105
+ <AtomicIcon
106
+ name="X"
107
+ size="sm"
108
+ color="secondary"
109
+ />
110
+ </TouchableOpacity>
111
+ )}
112
+ </View>
113
+ );
114
+ };