@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,146 @@
1
+ /**
2
+ * AtomicSkeleton - Universal Skeleton Loading Component
3
+ *
4
+ * Displays animated skeleton placeholders for loading states
5
+ * Theme: {{THEME_NAME}} ({{CATEGORY}} category)
6
+ *
7
+ * Atomic Design Level: ATOM
8
+ * Purpose: Loading state placeholder
9
+ *
10
+ * Usage:
11
+ * - Content loading placeholders
12
+ * - List item skeletons
13
+ * - Card skeletons
14
+ * - Text line skeletons
15
+ * - Image placeholders
16
+ */
17
+
18
+ import React, { useEffect, useRef } from 'react';
19
+ import { View, StyleSheet, ViewStyle, Animated, DimensionValue } from 'react-native';
20
+ import { useAppDesignTokens } from '../hooks/useAppDesignTokens';
21
+
22
+ // =============================================================================
23
+ // TYPE DEFINITIONS
24
+ // =============================================================================
25
+
26
+ export interface AtomicSkeletonProps {
27
+ /** Skeleton width */
28
+ width?: number | string;
29
+ /** Skeleton height */
30
+ height?: number | string;
31
+ /** Skeleton shape */
32
+ shape?: 'rectangle' | 'circle' | 'rounded';
33
+ /** Border radius for rounded shapes */
34
+ borderRadius?: number;
35
+ /** Animation duration in milliseconds */
36
+ duration?: number;
37
+ /** Whether to show animation */
38
+ animated?: boolean;
39
+ /** Skeleton color */
40
+ color?: string;
41
+ /** Highlight color for animation */
42
+ highlightColor?: string;
43
+ /** Style overrides */
44
+ style?: ViewStyle;
45
+ /** Test ID for testing */
46
+ testID?: string;
47
+ }
48
+
49
+ // =============================================================================
50
+ // COMPONENT IMPLEMENTATION
51
+ // =============================================================================
52
+
53
+ export const AtomicSkeleton: React.FC<AtomicSkeletonProps> = ({
54
+ width = '100%',
55
+ height = 20,
56
+ shape = 'rectangle',
57
+ borderRadius,
58
+ duration,
59
+ animated = true,
60
+ color,
61
+ highlightColor,
62
+ style,
63
+ testID,
64
+ }) => {
65
+ const tokens = useAppDesignTokens();
66
+ const animatedValue = useRef(new Animated.Value(0)).current;
67
+
68
+ // Default values
69
+ const finalDuration = duration ?? tokens.animations.slowest;
70
+ const skeletonColor = color || tokens.colors.surfaceVariant;
71
+ const skeletonHighlight = highlightColor || tokens.colors.surface;
72
+
73
+ // Animation effect
74
+ useEffect(() => {
75
+ if (animated) {
76
+ const animation = Animated.loop(
77
+ Animated.sequence([
78
+ Animated.timing(animatedValue, {
79
+ toValue: 1,
80
+ duration: finalDuration,
81
+ useNativeDriver: false,
82
+ }),
83
+ Animated.timing(animatedValue, {
84
+ toValue: 0,
85
+ duration: finalDuration,
86
+ useNativeDriver: false,
87
+ }),
88
+ ])
89
+ );
90
+ animation.start();
91
+
92
+ return () => animation.stop();
93
+ }
94
+ }, [animated, finalDuration, animatedValue]);
95
+
96
+ // Calculate border radius based on shape
97
+ const getBorderRadius = (): number => {
98
+ if (borderRadius !== undefined) return borderRadius;
99
+
100
+ switch (shape) {
101
+ case 'circle':
102
+ return typeof height === 'number' ? height / 2 : 20;
103
+ case 'rounded':
104
+ return tokens.borders.radius.md;
105
+ case 'rectangle':
106
+ default:
107
+ return tokens.borders.radius.sm;
108
+ }
109
+ };
110
+
111
+ const skeletonStyle: ViewStyle = {
112
+ width: width as DimensionValue,
113
+ height: height as DimensionValue,
114
+ backgroundColor: skeletonColor,
115
+ borderRadius: getBorderRadius(),
116
+ };
117
+
118
+ if (animated) {
119
+ const animatedStyle = {
120
+ backgroundColor: animatedValue.interpolate({
121
+ inputRange: [0, 1],
122
+ outputRange: [skeletonColor, skeletonHighlight],
123
+ }),
124
+ };
125
+
126
+ return (
127
+ <Animated.View
128
+ style={[skeletonStyle, animatedStyle, style]}
129
+ testID={testID}
130
+ />
131
+ );
132
+ }
133
+
134
+ return (
135
+ <View
136
+ style={[skeletonStyle, style]}
137
+ testID={testID}
138
+ />
139
+ );
140
+ };
141
+
142
+ // =============================================================================
143
+ // EXPORTS
144
+ // =============================================================================
145
+
146
+ export default AtomicSkeleton;
@@ -0,0 +1,145 @@
1
+ import React from 'react';
2
+ import { ScrollView, View } from 'react-native';
3
+ import type { StyleProp, ViewStyle } from 'react-native';
4
+ import { useAppDesignTokens } from '../hooks/useAppDesignTokens';
5
+ import { AtomicChip } from './AtomicChip';
6
+
7
+ /**
8
+ * Sort option interface
9
+ */
10
+ export interface SortOption {
11
+ id: string;
12
+ label: string;
13
+ icon?: string;
14
+ }
15
+
16
+ /**
17
+ * Sort direction type
18
+ */
19
+ export type SortDirection = 'asc' | 'desc';
20
+
21
+ /**
22
+ * AtomicSort component props
23
+ */
24
+ export interface AtomicSortProps {
25
+ options: SortOption[];
26
+ selectedId: string | null;
27
+ sortDirection: SortDirection;
28
+ onSortChange: (optionId: string, direction: SortDirection) => void;
29
+ showDirectionToggle?: boolean;
30
+ variant?: 'outlined' | 'filled' | 'soft';
31
+ color?: 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info';
32
+ size?: 'sm' | 'md' | 'lg';
33
+ style?: StyleProp<ViewStyle>;
34
+ testID?: string;
35
+ }
36
+
37
+ /**
38
+ * AtomicSort - Horizontal Sort Chip Component
39
+ *
40
+ * A Material Design 3 compliant sort component using chip selection.
41
+ * Supports single selection with ascending/descending direction toggle.
42
+ *
43
+ * @example
44
+ * ```tsx
45
+ * const [sortBy, setSortBy] = useState<string | null>('name');
46
+ * const [sortDir, setSortDir] = useState<SortDirection>('asc');
47
+ *
48
+ * <AtomicSort
49
+ * options={[
50
+ * { id: 'name', label: 'Name', icon: 'sort-alpha' },
51
+ * { id: 'date', label: 'Date', icon: 'schedule' },
52
+ * { id: 'priority', label: 'Priority', icon: 'flag' },
53
+ * ]}
54
+ * selectedId={sortBy}
55
+ * sortDirection={sortDir}
56
+ * onSortChange={(id, dir) => {
57
+ * setSortBy(id);
58
+ * setSortDir(dir);
59
+ * }}
60
+ * showDirectionToggle={true}
61
+ * />
62
+ * ```
63
+ *
64
+ * Features:
65
+ * - Horizontal scrollable sort chips
66
+ * - Single selection (one active sort at a time)
67
+ * - Direction toggle (click active chip to switch asc/desc)
68
+ * - Visual arrow indicators (↑ asc, ↓ desc)
69
+ * - Theme-aware colors from design tokens
70
+ * - Icon support per sort option
71
+ * - Fully controlled component
72
+ *
73
+ * Behavior:
74
+ * - Click inactive chip → Selects it with ascending direction
75
+ * - Click active chip → Toggles direction (asc ↔ desc)
76
+ * - Visual feedback via filled variant for active sort
77
+ */
78
+ export const AtomicSort: React.FC<AtomicSortProps> = ({
79
+ options,
80
+ selectedId,
81
+ sortDirection,
82
+ onSortChange,
83
+ showDirectionToggle = true,
84
+ variant = 'outlined',
85
+ color = 'primary',
86
+ size = 'md',
87
+ style,
88
+ testID,
89
+ }) => {
90
+ const tokens = useAppDesignTokens();
91
+
92
+ /**
93
+ * Handle sort chip press
94
+ * - If clicking active chip: Toggle direction
95
+ * - If clicking inactive chip: Select it with 'asc' direction
96
+ */
97
+ const handleSortPress = (optionId: string) => {
98
+ if (selectedId === optionId) {
99
+ const newDirection = sortDirection === 'asc' ? 'desc' : 'asc';
100
+ onSortChange(optionId, newDirection);
101
+ } else {
102
+ onSortChange(optionId, 'asc');
103
+ }
104
+ };
105
+
106
+ const directionIcon = sortDirection === 'asc' ? 'arrow-upward' : 'arrow-downward';
107
+
108
+ return (
109
+ <ScrollView
110
+ horizontal
111
+ showsHorizontalScrollIndicator={false}
112
+ contentContainerStyle={{
113
+ paddingHorizontal: tokens.spacing.sm,
114
+ gap: tokens.spacing.sm,
115
+ }}
116
+ style={[style]}
117
+ testID={testID}
118
+ >
119
+ <View style={{ flexDirection: 'row', gap: tokens.spacing.sm }}>
120
+ {options.map((option) => {
121
+ const isSelected = selectedId === option.id;
122
+
123
+ return (
124
+ <AtomicChip
125
+ key={option.id}
126
+ variant={isSelected ? 'filled' : variant}
127
+ color={color}
128
+ size={size}
129
+ leadingIcon={option.icon}
130
+ trailingIcon={
131
+ isSelected && showDirectionToggle ? directionIcon : undefined
132
+ }
133
+ selected={isSelected}
134
+ clickable={true}
135
+ onPress={() => handleSortPress(option.id)}
136
+ testID={`sort-chip-${option.id}`}
137
+ >
138
+ {option.label}
139
+ </AtomicChip>
140
+ );
141
+ })}
142
+ </View>
143
+ </ScrollView>
144
+ );
145
+ };
@@ -0,0 +1,166 @@
1
+ /**
2
+ * AtomicSwitch - Universal Switch Component
3
+ *
4
+ * Provides consistent switch/toggle functionality with theme integration
5
+ * Theme: {{THEME_NAME}} ({{CATEGORY}} category)
6
+ *
7
+ * Atomic Design Level: ATOM
8
+ * Purpose: Basic switch/toggle input
9
+ *
10
+ * Usage:
11
+ * - Settings toggles
12
+ * - Feature enable/disable
13
+ * - Boolean preferences
14
+ * - Form inputs
15
+ */
16
+
17
+ import React from 'react';
18
+ import { Switch, SwitchProps, StyleSheet, ViewStyle } from 'react-native';
19
+ import { useAppDesignTokens } from '../hooks/useAppDesignTokens';
20
+
21
+ // =============================================================================
22
+ // TYPE DEFINITIONS
23
+ // =============================================================================
24
+
25
+ export interface AtomicSwitchProps extends Omit<SwitchProps, 'style'> {
26
+ /** Switch value */
27
+ value: boolean;
28
+ /** Value change handler */
29
+ onValueChange: (value: boolean) => void;
30
+ /** Size variant */
31
+ size?: 'sm' | 'md' | 'lg';
32
+ /** Color variant */
33
+ variant?: 'primary' | 'secondary' | 'success' | 'warning' | 'error';
34
+ /** Disabled state */
35
+ disabled?: boolean;
36
+ /** Container style override */
37
+ style?: ViewStyle;
38
+ /** Track color override */
39
+ trackColor?: { false: string; true: string };
40
+ /** Thumb color override */
41
+ thumbColor?: string;
42
+ /** iOS specific props */
43
+ ios_backgroundColor?: string;
44
+ }
45
+
46
+ // =============================================================================
47
+ // SIZE CONFIGURATION
48
+ // =============================================================================
49
+
50
+ const SIZE_CONFIG = {
51
+ sm: { scaleX: 0.8, scaleY: 0.8 },
52
+ md: { scaleX: 1, scaleY: 1 },
53
+ lg: { scaleX: 1.2, scaleY: 1.2 },
54
+ } as const;
55
+
56
+ // =============================================================================
57
+ // COMPONENT IMPLEMENTATION
58
+ // =============================================================================
59
+
60
+ export const AtomicSwitch: React.FC<AtomicSwitchProps> = ({
61
+ value,
62
+ onValueChange,
63
+ size = 'md',
64
+ variant = 'primary',
65
+ disabled = false,
66
+ style,
67
+ trackColor,
68
+ thumbColor,
69
+ ios_backgroundColor,
70
+ ...props
71
+ }) => {
72
+ const tokens = useAppDesignTokens();
73
+ const styles = getStyles(tokens);
74
+
75
+ const sizeConfig = SIZE_CONFIG[size];
76
+ const colors = getVariantColors(tokens, variant);
77
+
78
+ const defaultTrackColor = trackColor || {
79
+ false: colors.trackFalse,
80
+ true: colors.trackTrue,
81
+ };
82
+
83
+ const defaultThumbColor = thumbColor || colors.thumb;
84
+ const defaultIosBackgroundColor = ios_backgroundColor || colors.trackFalse;
85
+
86
+ return (
87
+ <Switch
88
+ value={value}
89
+ onValueChange={onValueChange}
90
+ disabled={disabled}
91
+ trackColor={defaultTrackColor}
92
+ thumbColor={defaultThumbColor}
93
+ ios_backgroundColor={defaultIosBackgroundColor}
94
+ style={[
95
+ styles.switch,
96
+ {
97
+ transform: [{ scaleX: sizeConfig.scaleX }, { scaleY: sizeConfig.scaleY }],
98
+ },
99
+ style,
100
+ ]}
101
+ {...props}
102
+ />
103
+ );
104
+ };
105
+
106
+ // =============================================================================
107
+ // HELPER FUNCTIONS
108
+ // =============================================================================
109
+
110
+ const getVariantColors = (tokens: ReturnType<typeof useAppDesignTokens>, variant: AtomicSwitchProps['variant']) => {
111
+ switch (variant) {
112
+ case 'primary':
113
+ return {
114
+ trackFalse: tokens.colors.surfaceSecondary,
115
+ trackTrue: tokens.colors.primary,
116
+ thumb: tokens.colors.surface,
117
+ };
118
+ case 'secondary':
119
+ return {
120
+ trackFalse: tokens.colors.surfaceSecondary,
121
+ trackTrue: tokens.colors.secondary,
122
+ thumb: tokens.colors.surface,
123
+ };
124
+ case 'success':
125
+ return {
126
+ trackFalse: tokens.colors.surfaceSecondary,
127
+ trackTrue: tokens.colors.success,
128
+ thumb: tokens.colors.surface,
129
+ };
130
+ case 'warning':
131
+ return {
132
+ trackFalse: tokens.colors.surfaceSecondary,
133
+ trackTrue: tokens.colors.warning,
134
+ thumb: tokens.colors.surface,
135
+ };
136
+ case 'error':
137
+ return {
138
+ trackFalse: tokens.colors.surfaceSecondary,
139
+ trackTrue: tokens.colors.error,
140
+ thumb: tokens.colors.surface,
141
+ };
142
+ default:
143
+ return {
144
+ trackFalse: tokens.colors.surfaceSecondary,
145
+ trackTrue: tokens.colors.primary,
146
+ thumb: tokens.colors.surface,
147
+ };
148
+ }
149
+ };
150
+
151
+ // =============================================================================
152
+ // STYLES
153
+ // =============================================================================
154
+
155
+ const getStyles = (tokens: ReturnType<typeof useAppDesignTokens>) =>
156
+ StyleSheet.create({
157
+ switch: {
158
+ // Default switch styling is handled by platform
159
+ },
160
+ });
161
+
162
+ // =============================================================================
163
+ // EXPORTS
164
+ // =============================================================================
165
+
166
+ export default AtomicSwitch;
@@ -0,0 +1,50 @@
1
+ import React from 'react';
2
+ import { Text as PaperText } from 'react-native-paper';
3
+ import { StyleProp, TextStyle } from 'react-native';
4
+
5
+ export type TextStyleVariant =
6
+ | 'displayLarge' | 'displayMedium' | 'displaySmall'
7
+ | 'headlineLarge' | 'headlineMedium' | 'headlineSmall'
8
+ | 'titleLarge' | 'titleMedium' | 'titleSmall'
9
+ | 'bodyLarge' | 'bodyMedium' | 'bodySmall'
10
+ | 'labelLarge' | 'labelMedium' | 'labelSmall';
11
+
12
+ export type ColorVariant =
13
+ | 'primary'
14
+ | 'secondary'
15
+ | 'tertiary'
16
+ | 'disabled'
17
+ | 'inverse'
18
+ | 'success'
19
+ | 'error'
20
+ | 'warning'
21
+ | 'info';
22
+
23
+ export interface AtomicTextProps {
24
+ children: React.ReactNode;
25
+ type?: TextStyleVariant;
26
+ color?: ColorVariant | string;
27
+ numberOfLines?: number;
28
+ style?: StyleProp<TextStyle>;
29
+ testID?: string;
30
+ }
31
+
32
+ export const AtomicText: React.FC<AtomicTextProps> = ({
33
+ children,
34
+ type = 'bodyMedium',
35
+ color,
36
+ numberOfLines,
37
+ style,
38
+ testID,
39
+ }) => {
40
+ return (
41
+ <PaperText
42
+ variant={type}
43
+ numberOfLines={numberOfLines}
44
+ style={style}
45
+ testID={testID}
46
+ >
47
+ {children}
48
+ </PaperText>
49
+ );
50
+ };
@@ -0,0 +1,198 @@
1
+ /**
2
+ * AtomicTextArea Component
3
+ *
4
+ * A multiline text input component with Material Design 3 integration
5
+ * for longer text entry with consistent styling.
6
+ *
7
+ * Features:
8
+ * - React Native Paper TextInput with multiline
9
+ * - Material Design 3 outlined/filled/flat variants
10
+ * - Error, success, disabled states
11
+ * - Character counter with max length
12
+ * - Helper text for guidance or errors
13
+ * - Configurable rows for height
14
+ * - Theme-aware styling
15
+ * - Full accessibility support
16
+ *
17
+ * Usage:
18
+ * ```tsx
19
+ * const [description, setDescription] = useState('');
20
+ *
21
+ * <AtomicTextArea
22
+ * value={description}
23
+ * onChangeText={setDescription}
24
+ * label="Description"
25
+ * placeholder="Enter description..."
26
+ * maxLength={500}
27
+ * showCharacterCount
28
+ * rows={6}
29
+ * helperText="Provide a detailed description"
30
+ * />
31
+ * ```
32
+ */
33
+
34
+ import React from 'react';
35
+ import { View, StyleProp, ViewStyle, TextStyle } from 'react-native';
36
+ import { TextInput, HelperText } from 'react-native-paper';
37
+ import { useAppDesignTokens } from '../hooks/useAppDesignTokens';
38
+
39
+ export type AtomicTextAreaVariant = 'outlined' | 'filled' | 'flat';
40
+ export type AtomicTextAreaState = 'default' | 'error' | 'success' | 'disabled';
41
+ export type AtomicTextAreaSize = 'sm' | 'md' | 'lg';
42
+
43
+ export interface AtomicTextAreaProps {
44
+ /** Textarea label */
45
+ label?: string;
46
+ /** Current textarea value */
47
+ value?: string;
48
+ /** Value change callback */
49
+ onChangeText?: (text: string) => void;
50
+ /** Textarea variant (outlined, filled, flat) */
51
+ variant?: AtomicTextAreaVariant;
52
+ /** Textarea state (default, error, success, disabled) */
53
+ state?: AtomicTextAreaState;
54
+ /** Textarea size (sm, md, lg) */
55
+ size?: AtomicTextAreaSize;
56
+ /** Placeholder text */
57
+ placeholder?: string;
58
+ /** Helper text below textarea */
59
+ helperText?: string;
60
+ /** Maximum character length */
61
+ maxLength?: number;
62
+ /** Show character counter */
63
+ showCharacterCount?: boolean;
64
+ /** Number of visible text rows */
65
+ rows?: number;
66
+ /** Minimum height in pixels */
67
+ minHeight?: number;
68
+ /** Auto-capitalize */
69
+ autoCapitalize?: 'none' | 'sentences' | 'words' | 'characters';
70
+ /** Auto-correct */
71
+ autoCorrect?: boolean;
72
+ /** Disabled state */
73
+ disabled?: boolean;
74
+ /** Container style */
75
+ style?: StyleProp<ViewStyle>;
76
+ /** Input text style */
77
+ inputStyle?: StyleProp<TextStyle>;
78
+ /** Test ID for E2E testing */
79
+ testID?: string;
80
+ /** Blur callback */
81
+ onBlur?: () => void;
82
+ /** Focus callback */
83
+ onFocus?: () => void;
84
+ }
85
+
86
+ /**
87
+ * AtomicTextArea - Material Design 3 Multiline Text Input
88
+ */
89
+ export const AtomicTextArea: React.FC<AtomicTextAreaProps> = ({
90
+ variant = 'outlined',
91
+ state = 'default',
92
+ size = 'md',
93
+ label,
94
+ value = '',
95
+ onChangeText,
96
+ placeholder,
97
+ helperText,
98
+ maxLength,
99
+ showCharacterCount = false,
100
+ rows = 4,
101
+ minHeight,
102
+ autoCapitalize = 'sentences',
103
+ autoCorrect = true,
104
+ disabled = false,
105
+ style,
106
+ inputStyle,
107
+ testID,
108
+ onBlur,
109
+ onFocus,
110
+ }) => {
111
+ const tokens = useAppDesignTokens();
112
+ const isDisabled = state === 'disabled' || disabled;
113
+ const characterCount = value?.toString().length || 0;
114
+
115
+ // Map variant to Paper mode
116
+ const getPaperMode = (): 'outlined' | 'flat' => {
117
+ if (variant === 'outlined') return 'outlined';
118
+ return 'flat'; // filled and flat both use 'flat' mode
119
+ };
120
+
121
+ // Map state to Paper error prop
122
+ const hasError = state === 'error';
123
+
124
+ // Calculate height based on rows
125
+ const getTextAreaHeight = () => {
126
+ if (minHeight) return minHeight;
127
+
128
+ // Base line height: 24px per row (approximate)
129
+ const lineHeight = 24;
130
+ const padding = 32; // Top and bottom padding
131
+ return (rows * lineHeight) + padding;
132
+ };
133
+
134
+ // Get text color based on state
135
+ const getTextColor = () => {
136
+ if (state === 'error') return tokens.colors.error;
137
+ if (state === 'success') return tokens.colors.success;
138
+ return tokens.colors.onSurface;
139
+ };
140
+
141
+ return (
142
+ <View style={style} testID={testID}>
143
+ <TextInput
144
+ mode={getPaperMode()}
145
+ label={label}
146
+ value={value}
147
+ onChangeText={onChangeText}
148
+ placeholder={placeholder}
149
+ error={hasError}
150
+ disabled={isDisabled}
151
+ maxLength={maxLength}
152
+ autoCapitalize={autoCapitalize}
153
+ autoCorrect={autoCorrect}
154
+ multiline={true}
155
+ numberOfLines={rows}
156
+ style={[
157
+ { height: getTextAreaHeight(), textAlignVertical: 'top' },
158
+ inputStyle,
159
+ ]}
160
+ textColor={getTextColor()}
161
+ onBlur={onBlur}
162
+ onFocus={onFocus}
163
+ testID={testID ? `${testID}-input` : undefined}
164
+ />
165
+
166
+ {(helperText || showCharacterCount) && (
167
+ <View style={{
168
+ flexDirection: 'row',
169
+ justifyContent: 'space-between',
170
+ marginTop: tokens.spacing.xs,
171
+ }}>
172
+ {helperText && (
173
+ <HelperText
174
+ type={hasError ? 'error' : 'info'}
175
+ visible={Boolean(helperText)}
176
+ style={{ flex: 1 }}
177
+ testID={testID ? `${testID}-helper` : undefined}
178
+ >
179
+ {helperText}
180
+ </HelperText>
181
+ )}
182
+ {showCharacterCount && maxLength && (
183
+ <HelperText
184
+ type="info"
185
+ visible={true}
186
+ style={{ marginLeft: tokens.spacing.xs }}
187
+ testID={testID ? `${testID}-count` : undefined}
188
+ >
189
+ {characterCount}/{maxLength}
190
+ </HelperText>
191
+ )}
192
+ </View>
193
+ )}
194
+ </View>
195
+ );
196
+ };
197
+
198
+ export type { AtomicTextAreaProps as TextAreaProps };