@umituz/react-native-design-system 1.2.0 â 1.3.1
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/README.md +16 -11
- package/package.json +1 -2
- package/src/domains/icons/domain/interfaces/IIconAdapter.ts +1 -0
- package/src/presentation/atoms/AtomicButton.tsx +188 -40
- package/src/presentation/atoms/AtomicCard.tsx +52 -29
- package/src/presentation/atoms/AtomicInput.tsx +217 -99
- package/src/presentation/atoms/AtomicText.tsx +38 -6
- package/src/presentation/atoms/AtomicTextArea.tsx +173 -58
- package/src/presentation/organisms/FormContainer.tsx +16 -16
- package/src/presentation/tokens/core/ColorPalette.ts +2 -0
package/README.md
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
# @umituz/react-native-design-system
|
|
2
2
|
|
|
3
|
-
Universal design system for React Native apps following Domain-Driven Design (DDD) architecture with Material Design 3
|
|
3
|
+
Universal design system for React Native apps following Domain-Driven Design (DDD) architecture with Material Design 3 principles.
|
|
4
4
|
|
|
5
5
|
## âĻ Features
|
|
6
6
|
|
|
7
7
|
- ðĻ **Material Design 3** - Modern, accessible UI components
|
|
8
|
-
- âïļ **
|
|
9
|
-
- ðïļ **
|
|
8
|
+
- âïļ **Pure React Native** - No external UI library dependencies (lightweight!)
|
|
9
|
+
- ðïļ **Atomic Design** - Organized component hierarchy (Atoms â Molecules â Organisms)
|
|
10
|
+
- ð§Ž **DDD Architecture** - Clean domain-driven structure
|
|
10
11
|
- ð **Theme Support** - Built-in light/dark mode
|
|
11
12
|
- ðą **Responsive** - Adaptive layouts for phones and tablets
|
|
12
13
|
- âŋ **Accessible** - WCAG AA compliant components
|
|
13
14
|
- ð **Animations** - Smooth React Native Reanimated animations
|
|
14
15
|
- ðĶ **Zero Config** - Works out of the box
|
|
16
|
+
- ðŠķ **Lightweight** - Smaller bundle size (no Paper dependency)
|
|
15
17
|
|
|
16
18
|
## ðĶ Installation
|
|
17
19
|
|
|
@@ -22,9 +24,11 @@ npm install @umituz/react-native-design-system
|
|
|
22
24
|
### Peer Dependencies
|
|
23
25
|
|
|
24
26
|
```bash
|
|
25
|
-
npm install react@18.3.1 react-native@0.76.3 react-native-
|
|
27
|
+
npm install react@18.3.1 react-native@0.76.3 react-native-reanimated@~3.10.1 lucide-react-native@^0.468.0
|
|
26
28
|
```
|
|
27
29
|
|
|
30
|
+
> **v1.3.0 Breaking Change**: React Native Paper dependency removed! All components now use pure React Native implementation for lighter bundle size and full control over styling.
|
|
31
|
+
|
|
28
32
|
## ð Usage
|
|
29
33
|
|
|
30
34
|
```typescript
|
|
@@ -60,11 +64,12 @@ const MyScreen = () => {
|
|
|
60
64
|
## ð§Đ Components
|
|
61
65
|
|
|
62
66
|
### Atoms (Primitive UI Components)
|
|
63
|
-
- `AtomicButton` -
|
|
64
|
-
- `AtomicText` - Typography with MD3 type scale
|
|
65
|
-
- `AtomicInput` - Text inputs with validation
|
|
66
|
-
- `
|
|
67
|
-
- `
|
|
67
|
+
- `AtomicButton` - Pure React Native buttons with variants (primary, secondary, outline, text, danger)
|
|
68
|
+
- `AtomicText` - Typography with MD3 type scale (pure RN Text)
|
|
69
|
+
- `AtomicInput` - Text inputs with validation states (pure RN TextInput)
|
|
70
|
+
- `AtomicTextArea` - Multiline inputs with character counter (pure RN TextInput)
|
|
71
|
+
- `AtomicCard` - Container cards with elevation (pure RN View)
|
|
72
|
+
- `AtomicIcon` - Lucide icons with 1,639 icons
|
|
68
73
|
- `AtomicSwitch` - Toggle switches
|
|
69
74
|
- `AtomicBadge` - Status badges
|
|
70
75
|
- `AtomicProgress` - Progress indicators
|
|
@@ -79,9 +84,9 @@ const MyScreen = () => {
|
|
|
79
84
|
- And more...
|
|
80
85
|
|
|
81
86
|
### Organisms (Complex Patterns)
|
|
82
|
-
- `ScreenLayout` - Screen wrapper with safe area
|
|
87
|
+
- `ScreenLayout` - Screen wrapper with safe area (pure RN View)
|
|
83
88
|
- `AppHeader` - Application header
|
|
84
|
-
- `FormContainer` - Form layout container
|
|
89
|
+
- `FormContainer` - Form layout container with keyboard handling (pure RN View + ScrollView)
|
|
85
90
|
|
|
86
91
|
## ðĻ Design Tokens
|
|
87
92
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-design-system",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "Universal design system for React Native apps - Domain-Driven Design architecture with Material Design 3 components",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -27,7 +27,6 @@
|
|
|
27
27
|
"peerDependencies": {
|
|
28
28
|
"react": ">=18.2.0",
|
|
29
29
|
"react-native": ">=0.74.0",
|
|
30
|
-
"react-native-paper": "^5.12.5",
|
|
31
30
|
"react-native-reanimated": "~3.10.1",
|
|
32
31
|
"@react-native-community/datetimepicker": "8.0.1",
|
|
33
32
|
"@expo/vector-icons": "^14.0.0",
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
3
|
-
import { StyleSheet, StyleProp, ViewStyle, TextStyle } from 'react-native';
|
|
2
|
+
import { StyleSheet, StyleProp, ViewStyle, TextStyle, TouchableOpacity, ActivityIndicator, View } from 'react-native';
|
|
4
3
|
import Animated, { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated';
|
|
5
|
-
import {
|
|
4
|
+
import { AtomicText } from './AtomicText';
|
|
5
|
+
import { Icon } from '../../domains/icons/presentation/components/Icon';
|
|
6
|
+
import { useAppDesignTokens } from '../hooks/useAppDesignTokens';
|
|
7
|
+
import type { IconName } from '../../domains/icons/domain/interfaces/IIconAdapter';
|
|
6
8
|
|
|
7
9
|
export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'text' | 'danger';
|
|
8
10
|
export type ButtonSize = 'sm' | 'md' | 'lg';
|
|
@@ -15,14 +17,14 @@ export interface AtomicButtonProps {
|
|
|
15
17
|
size?: ButtonSize;
|
|
16
18
|
disabled?: boolean;
|
|
17
19
|
loading?: boolean;
|
|
18
|
-
icon?:
|
|
20
|
+
icon?: IconName;
|
|
19
21
|
fullWidth?: boolean;
|
|
20
22
|
style?: StyleProp<ViewStyle>;
|
|
21
23
|
textStyle?: StyleProp<TextStyle>;
|
|
22
24
|
testID?: string;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
const
|
|
27
|
+
const AnimatedTouchable = Animated.createAnimatedComponent(TouchableOpacity);
|
|
26
28
|
|
|
27
29
|
export const AtomicButton: React.FC<AtomicButtonProps> = ({
|
|
28
30
|
title,
|
|
@@ -35,8 +37,11 @@ export const AtomicButton: React.FC<AtomicButtonProps> = ({
|
|
|
35
37
|
icon,
|
|
36
38
|
fullWidth = false,
|
|
37
39
|
style,
|
|
40
|
+
textStyle,
|
|
38
41
|
testID,
|
|
39
42
|
}) => {
|
|
43
|
+
const tokens = useAppDesignTokens();
|
|
44
|
+
|
|
40
45
|
// Animation
|
|
41
46
|
const scale = useSharedValue(1);
|
|
42
47
|
|
|
@@ -63,62 +68,205 @@ export const AtomicButton: React.FC<AtomicButtonProps> = ({
|
|
|
63
68
|
}
|
|
64
69
|
};
|
|
65
70
|
|
|
66
|
-
//
|
|
67
|
-
const
|
|
71
|
+
// Size configurations
|
|
72
|
+
const sizeConfig = {
|
|
73
|
+
sm: {
|
|
74
|
+
paddingVertical: tokens.spacing.xs,
|
|
75
|
+
paddingHorizontal: tokens.spacing.sm,
|
|
76
|
+
fontSize: tokens.typography.bodySmall.fontSize,
|
|
77
|
+
iconSize: 16,
|
|
78
|
+
minHeight: 32,
|
|
79
|
+
},
|
|
80
|
+
md: {
|
|
81
|
+
paddingVertical: tokens.spacing.sm,
|
|
82
|
+
paddingHorizontal: tokens.spacing.md,
|
|
83
|
+
fontSize: tokens.typography.bodyMedium.fontSize,
|
|
84
|
+
iconSize: 20,
|
|
85
|
+
minHeight: 44,
|
|
86
|
+
},
|
|
87
|
+
lg: {
|
|
88
|
+
paddingVertical: tokens.spacing.md,
|
|
89
|
+
paddingHorizontal: tokens.spacing.lg,
|
|
90
|
+
fontSize: tokens.typography.bodyLarge.fontSize,
|
|
91
|
+
iconSize: 24,
|
|
92
|
+
minHeight: 52,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const config = sizeConfig[size];
|
|
97
|
+
|
|
98
|
+
// Variant styles
|
|
99
|
+
const getVariantStyles = () => {
|
|
100
|
+
const baseStyle: ViewStyle = {
|
|
101
|
+
backgroundColor: tokens.colors.primary,
|
|
102
|
+
borderWidth: 0,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const baseTextStyle: TextStyle = {
|
|
106
|
+
color: tokens.colors.textInverse,
|
|
107
|
+
};
|
|
108
|
+
|
|
68
109
|
switch (variant) {
|
|
69
110
|
case 'primary':
|
|
70
|
-
return
|
|
111
|
+
return {
|
|
112
|
+
container: {
|
|
113
|
+
...baseStyle,
|
|
114
|
+
backgroundColor: tokens.colors.primary,
|
|
115
|
+
},
|
|
116
|
+
text: {
|
|
117
|
+
...baseTextStyle,
|
|
118
|
+
color: tokens.colors.textInverse,
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
|
|
71
122
|
case 'secondary':
|
|
72
|
-
return
|
|
123
|
+
return {
|
|
124
|
+
container: {
|
|
125
|
+
...baseStyle,
|
|
126
|
+
backgroundColor: tokens.colors.surfaceSecondary,
|
|
127
|
+
},
|
|
128
|
+
text: {
|
|
129
|
+
...baseTextStyle,
|
|
130
|
+
color: tokens.colors.textPrimary,
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
|
|
73
134
|
case 'outline':
|
|
74
|
-
return
|
|
135
|
+
return {
|
|
136
|
+
container: {
|
|
137
|
+
...baseStyle,
|
|
138
|
+
backgroundColor: 'transparent',
|
|
139
|
+
borderWidth: 1,
|
|
140
|
+
borderColor: tokens.colors.border,
|
|
141
|
+
},
|
|
142
|
+
text: {
|
|
143
|
+
...baseTextStyle,
|
|
144
|
+
color: tokens.colors.textPrimary,
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
|
|
75
148
|
case 'text':
|
|
76
|
-
return
|
|
149
|
+
return {
|
|
150
|
+
container: {
|
|
151
|
+
...baseStyle,
|
|
152
|
+
backgroundColor: 'transparent',
|
|
153
|
+
},
|
|
154
|
+
text: {
|
|
155
|
+
...baseTextStyle,
|
|
156
|
+
color: tokens.colors.primary,
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
|
|
77
160
|
case 'danger':
|
|
78
|
-
return
|
|
161
|
+
return {
|
|
162
|
+
container: {
|
|
163
|
+
...baseStyle,
|
|
164
|
+
backgroundColor: tokens.colors.error,
|
|
165
|
+
},
|
|
166
|
+
text: {
|
|
167
|
+
...baseTextStyle,
|
|
168
|
+
color: tokens.colors.textInverse,
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
|
|
79
172
|
default:
|
|
80
|
-
return
|
|
173
|
+
return {
|
|
174
|
+
container: baseStyle,
|
|
175
|
+
text: baseTextStyle,
|
|
176
|
+
};
|
|
81
177
|
}
|
|
82
178
|
};
|
|
83
179
|
|
|
84
|
-
|
|
85
|
-
const getContentStyle = () => {
|
|
86
|
-
const paddingMap = {
|
|
87
|
-
sm: { paddingVertical: 4, paddingHorizontal: 12 },
|
|
88
|
-
md: { paddingVertical: 8, paddingHorizontal: 16 },
|
|
89
|
-
lg: { paddingVertical: 12, paddingHorizontal: 20 },
|
|
90
|
-
};
|
|
91
|
-
return paddingMap[size];
|
|
92
|
-
};
|
|
180
|
+
const variantStyles = getVariantStyles();
|
|
93
181
|
|
|
94
|
-
const
|
|
95
|
-
|
|
182
|
+
const containerStyle: StyleProp<ViewStyle> = [
|
|
183
|
+
styles.button,
|
|
184
|
+
{
|
|
185
|
+
paddingVertical: config.paddingVertical,
|
|
186
|
+
paddingHorizontal: config.paddingHorizontal,
|
|
187
|
+
minHeight: config.minHeight,
|
|
188
|
+
borderRadius: tokens.borders.radius.md,
|
|
189
|
+
},
|
|
190
|
+
variantStyles.container,
|
|
191
|
+
fullWidth ? styles.fullWidth : undefined,
|
|
192
|
+
disabled ? styles.disabled : undefined,
|
|
96
193
|
style,
|
|
97
|
-
]
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
const buttonTextStyle: StyleProp<TextStyle> = [
|
|
197
|
+
{
|
|
198
|
+
fontSize: config.fontSize,
|
|
199
|
+
fontWeight: '600',
|
|
200
|
+
},
|
|
201
|
+
variantStyles.text,
|
|
202
|
+
disabled ? styles.disabledText : undefined,
|
|
203
|
+
textStyle,
|
|
204
|
+
];
|
|
98
205
|
|
|
99
206
|
const buttonText = title || children;
|
|
207
|
+
const showIcon = icon && !loading;
|
|
208
|
+
const iconColor = variantStyles.text.color;
|
|
100
209
|
|
|
101
210
|
return (
|
|
102
|
-
<
|
|
103
|
-
style={animatedStyle}
|
|
211
|
+
<AnimatedTouchable
|
|
212
|
+
style={[animatedStyle, containerStyle]}
|
|
104
213
|
onPressIn={handlePressIn}
|
|
105
214
|
onPressOut={handlePressOut}
|
|
106
215
|
onPress={handlePress}
|
|
216
|
+
activeOpacity={0.8}
|
|
217
|
+
disabled={disabled || loading}
|
|
218
|
+
testID={testID}
|
|
107
219
|
>
|
|
108
|
-
<
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
220
|
+
<View style={styles.content}>
|
|
221
|
+
{loading ? (
|
|
222
|
+
<ActivityIndicator
|
|
223
|
+
size="small"
|
|
224
|
+
color={variantStyles.text.color}
|
|
225
|
+
style={styles.loader}
|
|
226
|
+
/>
|
|
227
|
+
) : showIcon ? (
|
|
228
|
+
<Icon
|
|
229
|
+
name={icon}
|
|
230
|
+
customSize={config.iconSize}
|
|
231
|
+
customColor={typeof iconColor === 'string' ? iconColor : undefined}
|
|
232
|
+
style={styles.icon}
|
|
233
|
+
/>
|
|
234
|
+
) : null}
|
|
235
|
+
|
|
236
|
+
<AtomicText style={buttonTextStyle}>
|
|
237
|
+
{buttonText}
|
|
238
|
+
</AtomicText>
|
|
239
|
+
</View>
|
|
240
|
+
</AnimatedTouchable>
|
|
121
241
|
);
|
|
122
242
|
};
|
|
123
243
|
|
|
244
|
+
const styles = StyleSheet.create({
|
|
245
|
+
button: {
|
|
246
|
+
alignItems: 'center',
|
|
247
|
+
justifyContent: 'center',
|
|
248
|
+
flexDirection: 'row',
|
|
249
|
+
},
|
|
250
|
+
content: {
|
|
251
|
+
flexDirection: 'row',
|
|
252
|
+
alignItems: 'center',
|
|
253
|
+
justifyContent: 'center',
|
|
254
|
+
},
|
|
255
|
+
fullWidth: {
|
|
256
|
+
width: '100%',
|
|
257
|
+
},
|
|
258
|
+
disabled: {
|
|
259
|
+
opacity: 0.5,
|
|
260
|
+
},
|
|
261
|
+
disabledText: {
|
|
262
|
+
opacity: 0.7,
|
|
263
|
+
},
|
|
264
|
+
icon: {
|
|
265
|
+
marginRight: 8,
|
|
266
|
+
},
|
|
267
|
+
loader: {
|
|
268
|
+
marginRight: 8,
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
|
|
124
272
|
export type { AtomicButtonProps as ButtonProps };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
3
|
-
import { StyleProp, ViewStyle } from 'react-native';
|
|
2
|
+
import { View, StyleProp, ViewStyle } from 'react-native';
|
|
4
3
|
import Animated, { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated';
|
|
5
4
|
import { Pressable } from 'react-native';
|
|
5
|
+
import { useAppDesignTokens } from '../hooks/useAppDesignTokens';
|
|
6
6
|
|
|
7
7
|
export type AtomicCardVariant = 'flat' | 'elevated' | 'outlined';
|
|
8
8
|
export type AtomicCardPadding = 'none' | 'sm' | 'md' | 'lg' | 'xl';
|
|
@@ -28,6 +28,8 @@ export const AtomicCard: React.FC<AtomicCardProps> = ({
|
|
|
28
28
|
children,
|
|
29
29
|
testID,
|
|
30
30
|
}) => {
|
|
31
|
+
const tokens = useAppDesignTokens();
|
|
32
|
+
|
|
31
33
|
// Animation for tap feedback
|
|
32
34
|
const scale = useSharedValue(1);
|
|
33
35
|
|
|
@@ -54,43 +56,64 @@ export const AtomicCard: React.FC<AtomicCardProps> = ({
|
|
|
54
56
|
}
|
|
55
57
|
};
|
|
56
58
|
|
|
57
|
-
// Map
|
|
58
|
-
const
|
|
59
|
+
// Map padding to token values
|
|
60
|
+
const getPaddingValue = (): number => {
|
|
61
|
+
const paddingMap = {
|
|
62
|
+
none: 0,
|
|
63
|
+
sm: tokens.spacing.sm,
|
|
64
|
+
md: tokens.spacing.md,
|
|
65
|
+
lg: tokens.spacing.lg,
|
|
66
|
+
xl: tokens.spacing.xl,
|
|
67
|
+
};
|
|
68
|
+
return paddingMap[padding];
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Get variant styles
|
|
72
|
+
const getVariantStyle = (): ViewStyle => {
|
|
73
|
+
const baseStyle: ViewStyle = {
|
|
74
|
+
backgroundColor: tokens.colors.surface,
|
|
75
|
+
borderRadius: tokens.borders.radius.md,
|
|
76
|
+
};
|
|
77
|
+
|
|
59
78
|
switch (variant) {
|
|
60
79
|
case 'elevated':
|
|
61
|
-
return
|
|
80
|
+
return {
|
|
81
|
+
...baseStyle,
|
|
82
|
+
borderWidth: 1,
|
|
83
|
+
borderColor: tokens.colors.border,
|
|
84
|
+
};
|
|
85
|
+
|
|
62
86
|
case 'outlined':
|
|
63
|
-
return
|
|
87
|
+
return {
|
|
88
|
+
...baseStyle,
|
|
89
|
+
borderWidth: 1,
|
|
90
|
+
borderColor: tokens.colors.border,
|
|
91
|
+
};
|
|
92
|
+
|
|
64
93
|
case 'flat':
|
|
65
|
-
return
|
|
94
|
+
return {
|
|
95
|
+
...baseStyle,
|
|
96
|
+
borderWidth: 0,
|
|
97
|
+
};
|
|
98
|
+
|
|
66
99
|
default:
|
|
67
|
-
return
|
|
100
|
+
return baseStyle;
|
|
68
101
|
}
|
|
69
102
|
};
|
|
70
103
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
};
|
|
80
|
-
const paddingValue = paddingMap[padding];
|
|
81
|
-
return { padding: paddingValue };
|
|
82
|
-
};
|
|
104
|
+
const cardStyle: StyleProp<ViewStyle> = [
|
|
105
|
+
getVariantStyle(),
|
|
106
|
+
{
|
|
107
|
+
padding: getPaddingValue(),
|
|
108
|
+
opacity: disabled ? 0.5 : 1,
|
|
109
|
+
},
|
|
110
|
+
style,
|
|
111
|
+
];
|
|
83
112
|
|
|
84
113
|
const cardContent = (
|
|
85
|
-
<
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
testID={testID}
|
|
89
|
-
>
|
|
90
|
-
<PaperCard.Content style={getContentStyle()}>
|
|
91
|
-
{children}
|
|
92
|
-
</PaperCard.Content>
|
|
93
|
-
</PaperCard>
|
|
114
|
+
<View style={cardStyle} testID={testID}>
|
|
115
|
+
{children}
|
|
116
|
+
</View>
|
|
94
117
|
);
|
|
95
118
|
|
|
96
119
|
// If onPress provided, wrap with animated pressable
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import React, { useState } from 'react';
|
|
2
|
-
import { View, Pressable, StyleProp, ViewStyle, TextStyle } from 'react-native';
|
|
3
|
-
import { TextInput, HelperText } from 'react-native-paper';
|
|
2
|
+
import { View, TextInput, Pressable, StyleSheet, StyleProp, ViewStyle, TextStyle } from 'react-native';
|
|
4
3
|
import { useAppDesignTokens } from '../hooks/useAppDesignTokens';
|
|
5
4
|
import { AtomicIcon } from './AtomicIcon';
|
|
5
|
+
import { AtomicText } from './AtomicText';
|
|
6
6
|
import type { AtomicIconName, AtomicIconSize } from './AtomicIcon';
|
|
7
7
|
|
|
8
8
|
export type AtomicInputVariant = 'outlined' | 'filled' | 'flat';
|
|
@@ -61,12 +61,12 @@ export interface AtomicInputProps {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
|
-
* AtomicInput -
|
|
64
|
+
* AtomicInput - Pure React Native Text Input
|
|
65
65
|
*
|
|
66
66
|
* Features:
|
|
67
|
-
* - React Native Paper
|
|
67
|
+
* - Pure React Native implementation (no Paper dependency)
|
|
68
68
|
* - Lucide icons for password toggle and custom icons
|
|
69
|
-
* -
|
|
69
|
+
* - Outlined/filled/flat variants
|
|
70
70
|
* - Error, success, disabled states
|
|
71
71
|
* - Character counter
|
|
72
72
|
* - Responsive sizing
|
|
@@ -100,128 +100,212 @@ export const AtomicInput: React.FC<AtomicInputProps> = ({
|
|
|
100
100
|
}) => {
|
|
101
101
|
const tokens = useAppDesignTokens();
|
|
102
102
|
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
|
|
103
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
103
104
|
const isDisabled = state === 'disabled' || disabled;
|
|
104
105
|
const characterCount = value?.toString().length || 0;
|
|
106
|
+
const hasError = state === 'error';
|
|
107
|
+
const hasSuccess = state === 'success';
|
|
105
108
|
|
|
106
|
-
//
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
109
|
+
// Size configuration
|
|
110
|
+
const sizeConfig = {
|
|
111
|
+
sm: {
|
|
112
|
+
paddingVertical: tokens.spacing.xs,
|
|
113
|
+
paddingHorizontal: tokens.spacing.sm,
|
|
114
|
+
fontSize: tokens.typography.bodySmall.fontSize,
|
|
115
|
+
iconSize: 16,
|
|
116
|
+
minHeight: 40,
|
|
117
|
+
},
|
|
118
|
+
md: {
|
|
119
|
+
paddingVertical: tokens.spacing.sm,
|
|
120
|
+
paddingHorizontal: tokens.spacing.md,
|
|
121
|
+
fontSize: tokens.typography.bodyMedium.fontSize,
|
|
122
|
+
iconSize: 20,
|
|
123
|
+
minHeight: 48,
|
|
124
|
+
},
|
|
125
|
+
lg: {
|
|
126
|
+
paddingVertical: tokens.spacing.md,
|
|
127
|
+
paddingHorizontal: tokens.spacing.lg,
|
|
128
|
+
fontSize: tokens.typography.bodyLarge.fontSize,
|
|
129
|
+
iconSize: 24,
|
|
130
|
+
minHeight: 56,
|
|
131
|
+
},
|
|
110
132
|
};
|
|
111
133
|
|
|
112
|
-
|
|
113
|
-
const hasError = state === 'error';
|
|
134
|
+
const config = sizeConfig[size];
|
|
114
135
|
|
|
115
|
-
// Get
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
};
|
|
136
|
+
// Get variant styles
|
|
137
|
+
const getVariantStyle = (): ViewStyle => {
|
|
138
|
+
const baseStyle: ViewStyle = {
|
|
139
|
+
backgroundColor: tokens.colors.surface,
|
|
140
|
+
borderRadius: tokens.borders.radius.md,
|
|
141
|
+
};
|
|
123
142
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
// Render leading icon
|
|
130
|
-
const renderLeadingIcon = leadingIcon ? () => (
|
|
131
|
-
<AtomicIcon
|
|
132
|
-
name={leadingIcon}
|
|
133
|
-
size={iconSizeName}
|
|
134
|
-
customColor={iconColor}
|
|
135
|
-
/>
|
|
136
|
-
) : undefined;
|
|
137
|
-
|
|
138
|
-
// Render trailing icon or password toggle
|
|
139
|
-
const renderTrailingIcon = () => {
|
|
140
|
-
if (showPasswordToggle && secureTextEntry) {
|
|
141
|
-
return (
|
|
142
|
-
<Pressable onPress={() => setIsPasswordVisible(!isPasswordVisible)}>
|
|
143
|
-
<AtomicIcon
|
|
144
|
-
name={isPasswordVisible ? "EyeOff" : "Eye"}
|
|
145
|
-
size={iconSizeName}
|
|
146
|
-
customColor={iconColor}
|
|
147
|
-
/>
|
|
148
|
-
</Pressable>
|
|
149
|
-
);
|
|
150
|
-
}
|
|
143
|
+
let borderColor = tokens.colors.border;
|
|
144
|
+
if (isFocused) borderColor = tokens.colors.primary;
|
|
145
|
+
if (hasError) borderColor = tokens.colors.error;
|
|
146
|
+
if (hasSuccess) borderColor = tokens.colors.success;
|
|
147
|
+
if (isDisabled) borderColor = tokens.colors.borderDisabled;
|
|
151
148
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
);
|
|
149
|
+
switch (variant) {
|
|
150
|
+
case 'outlined':
|
|
151
|
+
return {
|
|
152
|
+
...baseStyle,
|
|
153
|
+
borderWidth: isFocused ? 2 : 1,
|
|
154
|
+
borderColor,
|
|
155
|
+
};
|
|
160
156
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
157
|
+
case 'filled':
|
|
158
|
+
return {
|
|
159
|
+
...baseStyle,
|
|
160
|
+
backgroundColor: tokens.colors.surfaceSecondary,
|
|
161
|
+
borderWidth: 0,
|
|
162
|
+
borderBottomWidth: isFocused ? 2 : 1,
|
|
163
|
+
borderBottomColor: borderColor,
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
case 'flat':
|
|
167
|
+
return {
|
|
168
|
+
...baseStyle,
|
|
169
|
+
backgroundColor: 'transparent',
|
|
170
|
+
borderWidth: 0,
|
|
171
|
+
borderBottomWidth: 1,
|
|
172
|
+
borderBottomColor: borderColor,
|
|
173
|
+
borderRadius: 0,
|
|
174
|
+
};
|
|
165
175
|
|
|
166
|
-
|
|
176
|
+
default:
|
|
177
|
+
return baseStyle;
|
|
178
|
+
}
|
|
167
179
|
};
|
|
168
180
|
|
|
169
181
|
// Get text color based on state
|
|
170
182
|
const getTextColor = () => {
|
|
171
|
-
if (
|
|
172
|
-
if (
|
|
173
|
-
return tokens.colors.
|
|
183
|
+
if (isDisabled) return tokens.colors.textDisabled;
|
|
184
|
+
if (hasError) return tokens.colors.error;
|
|
185
|
+
if (hasSuccess) return tokens.colors.success;
|
|
186
|
+
return tokens.colors.textPrimary;
|
|
174
187
|
};
|
|
175
188
|
|
|
189
|
+
const iconColor = isDisabled ? tokens.colors.textDisabled : tokens.colors.textSecondary;
|
|
190
|
+
|
|
191
|
+
const containerStyle: StyleProp<ViewStyle> = [
|
|
192
|
+
styles.container,
|
|
193
|
+
getVariantStyle(),
|
|
194
|
+
{
|
|
195
|
+
paddingVertical: config.paddingVertical,
|
|
196
|
+
paddingHorizontal: config.paddingHorizontal,
|
|
197
|
+
minHeight: config.minHeight,
|
|
198
|
+
opacity: isDisabled ? 0.5 : 1,
|
|
199
|
+
},
|
|
200
|
+
style,
|
|
201
|
+
];
|
|
202
|
+
|
|
203
|
+
const textInputStyle: StyleProp<TextStyle> = [
|
|
204
|
+
styles.input,
|
|
205
|
+
{
|
|
206
|
+
fontSize: config.fontSize,
|
|
207
|
+
color: getTextColor(),
|
|
208
|
+
},
|
|
209
|
+
leadingIcon ? { paddingLeft: config.iconSize + 8 } : undefined,
|
|
210
|
+
(trailingIcon || showPasswordToggle) ? { paddingRight: config.iconSize + 8 } : undefined,
|
|
211
|
+
inputStyle,
|
|
212
|
+
];
|
|
213
|
+
|
|
176
214
|
return (
|
|
177
|
-
<View
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
215
|
+
<View testID={testID}>
|
|
216
|
+
{label && (
|
|
217
|
+
<AtomicText
|
|
218
|
+
type="labelMedium"
|
|
219
|
+
color={hasError ? 'error' : hasSuccess ? 'success' : 'secondary'}
|
|
220
|
+
style={styles.label}
|
|
221
|
+
>
|
|
222
|
+
{label}
|
|
223
|
+
</AtomicText>
|
|
224
|
+
)}
|
|
225
|
+
|
|
226
|
+
<View style={containerStyle}>
|
|
227
|
+
{leadingIcon && (
|
|
228
|
+
<View style={styles.leadingIcon}>
|
|
229
|
+
<AtomicIcon
|
|
230
|
+
name={leadingIcon}
|
|
231
|
+
customSize={config.iconSize}
|
|
232
|
+
customColor={iconColor}
|
|
233
|
+
/>
|
|
234
|
+
</View>
|
|
235
|
+
)}
|
|
236
|
+
|
|
237
|
+
<TextInput
|
|
238
|
+
value={value}
|
|
239
|
+
onChangeText={onChangeText}
|
|
240
|
+
placeholder={placeholder}
|
|
241
|
+
placeholderTextColor={tokens.colors.textSecondary}
|
|
242
|
+
secureTextEntry={secureTextEntry && !isPasswordVisible}
|
|
243
|
+
maxLength={maxLength}
|
|
244
|
+
keyboardType={keyboardType}
|
|
245
|
+
autoCapitalize={autoCapitalize}
|
|
246
|
+
autoCorrect={autoCorrect}
|
|
247
|
+
editable={!isDisabled}
|
|
248
|
+
style={textInputStyle}
|
|
249
|
+
onBlur={() => {
|
|
250
|
+
setIsFocused(false);
|
|
251
|
+
onBlur?.();
|
|
252
|
+
}}
|
|
253
|
+
onFocus={() => {
|
|
254
|
+
setIsFocused(true);
|
|
255
|
+
onFocus?.();
|
|
256
|
+
}}
|
|
257
|
+
testID={testID ? `${testID}-input` : undefined}
|
|
258
|
+
/>
|
|
259
|
+
|
|
260
|
+
{(showPasswordToggle && secureTextEntry) && (
|
|
261
|
+
<Pressable
|
|
262
|
+
onPress={() => setIsPasswordVisible(!isPasswordVisible)}
|
|
263
|
+
style={styles.trailingIcon}
|
|
264
|
+
>
|
|
265
|
+
<AtomicIcon
|
|
266
|
+
name={isPasswordVisible ? "EyeOff" : "Eye"}
|
|
267
|
+
customSize={config.iconSize}
|
|
268
|
+
customColor={iconColor}
|
|
269
|
+
/>
|
|
270
|
+
</Pressable>
|
|
271
|
+
)}
|
|
272
|
+
|
|
273
|
+
{trailingIcon && !showPasswordToggle && (
|
|
274
|
+
<Pressable
|
|
275
|
+
onPress={onTrailingIconPress}
|
|
276
|
+
style={styles.trailingIcon}
|
|
277
|
+
disabled={!onTrailingIconPress}
|
|
278
|
+
>
|
|
279
|
+
<AtomicIcon
|
|
280
|
+
name={trailingIcon}
|
|
281
|
+
customSize={config.iconSize}
|
|
282
|
+
customColor={iconColor}
|
|
283
|
+
/>
|
|
284
|
+
</Pressable>
|
|
285
|
+
)}
|
|
286
|
+
</View>
|
|
199
287
|
|
|
200
288
|
{(helperText || showCharacterCount) && (
|
|
201
|
-
<View style={
|
|
202
|
-
flexDirection: 'row',
|
|
203
|
-
justifyContent: 'space-between',
|
|
204
|
-
marginTop: tokens.spacing.xs,
|
|
205
|
-
}}>
|
|
289
|
+
<View style={styles.helperRow}>
|
|
206
290
|
{helperText && (
|
|
207
|
-
<
|
|
208
|
-
type=
|
|
209
|
-
|
|
210
|
-
style={
|
|
291
|
+
<AtomicText
|
|
292
|
+
type="bodySmall"
|
|
293
|
+
color={hasError ? 'error' : 'secondary'}
|
|
294
|
+
style={styles.helperText}
|
|
211
295
|
testID={testID ? `${testID}-helper` : undefined}
|
|
212
296
|
>
|
|
213
297
|
{helperText}
|
|
214
|
-
</
|
|
298
|
+
</AtomicText>
|
|
215
299
|
)}
|
|
216
300
|
{showCharacterCount && maxLength && (
|
|
217
|
-
<
|
|
218
|
-
type="
|
|
219
|
-
|
|
220
|
-
style={
|
|
301
|
+
<AtomicText
|
|
302
|
+
type="bodySmall"
|
|
303
|
+
color="secondary"
|
|
304
|
+
style={styles.characterCount}
|
|
221
305
|
testID={testID ? `${testID}-count` : undefined}
|
|
222
306
|
>
|
|
223
307
|
{characterCount}/{maxLength}
|
|
224
|
-
</
|
|
308
|
+
</AtomicText>
|
|
225
309
|
)}
|
|
226
310
|
</View>
|
|
227
311
|
)}
|
|
@@ -229,4 +313,38 @@ export const AtomicInput: React.FC<AtomicInputProps> = ({
|
|
|
229
313
|
);
|
|
230
314
|
};
|
|
231
315
|
|
|
316
|
+
const styles = StyleSheet.create({
|
|
317
|
+
container: {
|
|
318
|
+
flexDirection: 'row',
|
|
319
|
+
alignItems: 'center',
|
|
320
|
+
},
|
|
321
|
+
input: {
|
|
322
|
+
flex: 1,
|
|
323
|
+
},
|
|
324
|
+
label: {
|
|
325
|
+
marginBottom: 4,
|
|
326
|
+
},
|
|
327
|
+
leadingIcon: {
|
|
328
|
+
position: 'absolute',
|
|
329
|
+
left: 12,
|
|
330
|
+
zIndex: 1,
|
|
331
|
+
},
|
|
332
|
+
trailingIcon: {
|
|
333
|
+
position: 'absolute',
|
|
334
|
+
right: 12,
|
|
335
|
+
zIndex: 1,
|
|
336
|
+
},
|
|
337
|
+
helperRow: {
|
|
338
|
+
flexDirection: 'row',
|
|
339
|
+
justifyContent: 'space-between',
|
|
340
|
+
marginTop: 4,
|
|
341
|
+
},
|
|
342
|
+
helperText: {
|
|
343
|
+
flex: 1,
|
|
344
|
+
},
|
|
345
|
+
characterCount: {
|
|
346
|
+
marginLeft: 8,
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
|
|
232
350
|
export type { AtomicInputProps as InputProps };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { Text
|
|
3
|
-
import {
|
|
2
|
+
import { Text, StyleProp, TextStyle } from 'react-native';
|
|
3
|
+
import { useAppDesignTokens } from '../hooks/useAppDesignTokens';
|
|
4
4
|
|
|
5
5
|
export type TextStyleVariant =
|
|
6
6
|
| 'displayLarge' | 'displayMedium' | 'displaySmall'
|
|
@@ -37,14 +37,46 @@ export const AtomicText: React.FC<AtomicTextProps> = ({
|
|
|
37
37
|
style,
|
|
38
38
|
testID,
|
|
39
39
|
}) => {
|
|
40
|
+
const tokens = useAppDesignTokens();
|
|
41
|
+
|
|
42
|
+
// Get typography style from tokens
|
|
43
|
+
const typographyStyle = tokens.typography[type];
|
|
44
|
+
|
|
45
|
+
// Get color from tokens or use custom color
|
|
46
|
+
const getTextColor = (): string => {
|
|
47
|
+
if (!color) {
|
|
48
|
+
return tokens.colors.textPrimary;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Check if it's a semantic color name
|
|
52
|
+
const colorMap: Record<ColorVariant, string> = {
|
|
53
|
+
primary: tokens.colors.textPrimary,
|
|
54
|
+
secondary: tokens.colors.textSecondary,
|
|
55
|
+
tertiary: tokens.colors.textTertiary,
|
|
56
|
+
disabled: tokens.colors.textDisabled,
|
|
57
|
+
inverse: tokens.colors.textInverse,
|
|
58
|
+
success: tokens.colors.success,
|
|
59
|
+
error: tokens.colors.error,
|
|
60
|
+
warning: tokens.colors.warning,
|
|
61
|
+
info: tokens.colors.info,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return colorMap[color as ColorVariant] || color;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const textStyle: StyleProp<TextStyle> = [
|
|
68
|
+
typographyStyle,
|
|
69
|
+
{ color: getTextColor() },
|
|
70
|
+
style,
|
|
71
|
+
];
|
|
72
|
+
|
|
40
73
|
return (
|
|
41
|
-
<
|
|
42
|
-
variant={type}
|
|
74
|
+
<Text
|
|
43
75
|
numberOfLines={numberOfLines}
|
|
44
|
-
style={
|
|
76
|
+
style={textStyle}
|
|
45
77
|
testID={testID}
|
|
46
78
|
>
|
|
47
79
|
{children}
|
|
48
|
-
</
|
|
80
|
+
</Text>
|
|
49
81
|
);
|
|
50
82
|
};
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AtomicTextArea Component
|
|
3
3
|
*
|
|
4
|
-
* A multiline text input component with
|
|
4
|
+
* A multiline text input component with pure React Native implementation
|
|
5
5
|
* for longer text entry with consistent styling.
|
|
6
6
|
*
|
|
7
7
|
* Features:
|
|
8
|
-
* - React Native
|
|
9
|
-
* -
|
|
8
|
+
* - Pure React Native TextInput with multiline
|
|
9
|
+
* - Outlined/filled/flat variants
|
|
10
10
|
* - Error, success, disabled states
|
|
11
11
|
* - Character counter with max length
|
|
12
12
|
* - Helper text for guidance or errors
|
|
@@ -31,10 +31,10 @@
|
|
|
31
31
|
* ```
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
|
-
import React from 'react';
|
|
35
|
-
import { View, StyleProp, ViewStyle, TextStyle } from 'react-native';
|
|
36
|
-
import { TextInput, HelperText } from 'react-native-paper';
|
|
34
|
+
import React, { useState } from 'react';
|
|
35
|
+
import { View, TextInput, StyleSheet, StyleProp, ViewStyle, TextStyle } from 'react-native';
|
|
37
36
|
import { useAppDesignTokens } from '../hooks/useAppDesignTokens';
|
|
37
|
+
import { AtomicText } from './AtomicText';
|
|
38
38
|
|
|
39
39
|
export type AtomicTextAreaVariant = 'outlined' | 'filled' | 'flat';
|
|
40
40
|
export type AtomicTextAreaState = 'default' | 'error' | 'success' | 'disabled';
|
|
@@ -84,7 +84,7 @@ export interface AtomicTextAreaProps {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
|
-
* AtomicTextArea -
|
|
87
|
+
* AtomicTextArea - Pure React Native Multiline Text Input
|
|
88
88
|
*/
|
|
89
89
|
export const AtomicTextArea: React.FC<AtomicTextAreaProps> = ({
|
|
90
90
|
variant = 'outlined',
|
|
@@ -109,85 +109,177 @@ export const AtomicTextArea: React.FC<AtomicTextAreaProps> = ({
|
|
|
109
109
|
onFocus,
|
|
110
110
|
}) => {
|
|
111
111
|
const tokens = useAppDesignTokens();
|
|
112
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
112
113
|
const isDisabled = state === 'disabled' || disabled;
|
|
113
114
|
const characterCount = value?.toString().length || 0;
|
|
115
|
+
const hasError = state === 'error';
|
|
116
|
+
const hasSuccess = state === 'success';
|
|
114
117
|
|
|
115
|
-
//
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
118
|
+
// Size configuration
|
|
119
|
+
const sizeConfig = {
|
|
120
|
+
sm: {
|
|
121
|
+
paddingVertical: tokens.spacing.xs,
|
|
122
|
+
paddingHorizontal: tokens.spacing.sm,
|
|
123
|
+
fontSize: tokens.typography.bodySmall.fontSize,
|
|
124
|
+
lineHeight: 20,
|
|
125
|
+
},
|
|
126
|
+
md: {
|
|
127
|
+
paddingVertical: tokens.spacing.sm,
|
|
128
|
+
paddingHorizontal: tokens.spacing.md,
|
|
129
|
+
fontSize: tokens.typography.bodyMedium.fontSize,
|
|
130
|
+
lineHeight: 24,
|
|
131
|
+
},
|
|
132
|
+
lg: {
|
|
133
|
+
paddingVertical: tokens.spacing.md,
|
|
134
|
+
paddingHorizontal: tokens.spacing.lg,
|
|
135
|
+
fontSize: tokens.typography.bodyLarge.fontSize,
|
|
136
|
+
lineHeight: 28,
|
|
137
|
+
},
|
|
119
138
|
};
|
|
120
139
|
|
|
121
|
-
|
|
122
|
-
const hasError = state === 'error';
|
|
140
|
+
const config = sizeConfig[size];
|
|
123
141
|
|
|
124
142
|
// Calculate height based on rows
|
|
125
143
|
const getTextAreaHeight = () => {
|
|
126
144
|
if (minHeight) return minHeight;
|
|
145
|
+
const paddingVertical = config.paddingVertical * 2;
|
|
146
|
+
return (rows * config.lineHeight) + paddingVertical;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// Get variant styles
|
|
150
|
+
const getVariantStyle = (): ViewStyle => {
|
|
151
|
+
const baseStyle: ViewStyle = {
|
|
152
|
+
backgroundColor: tokens.colors.surface,
|
|
153
|
+
borderRadius: tokens.borders.radius.md,
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
let borderColor = tokens.colors.border;
|
|
157
|
+
if (isFocused) borderColor = tokens.colors.primary;
|
|
158
|
+
if (hasError) borderColor = tokens.colors.error;
|
|
159
|
+
if (hasSuccess) borderColor = tokens.colors.success;
|
|
160
|
+
if (isDisabled) borderColor = tokens.colors.borderDisabled;
|
|
161
|
+
|
|
162
|
+
switch (variant) {
|
|
163
|
+
case 'outlined':
|
|
164
|
+
return {
|
|
165
|
+
...baseStyle,
|
|
166
|
+
borderWidth: isFocused ? 2 : 1,
|
|
167
|
+
borderColor,
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
case 'filled':
|
|
171
|
+
return {
|
|
172
|
+
...baseStyle,
|
|
173
|
+
backgroundColor: tokens.colors.surfaceSecondary,
|
|
174
|
+
borderWidth: 0,
|
|
175
|
+
borderBottomWidth: isFocused ? 2 : 1,
|
|
176
|
+
borderBottomColor: borderColor,
|
|
177
|
+
};
|
|
127
178
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
179
|
+
case 'flat':
|
|
180
|
+
return {
|
|
181
|
+
...baseStyle,
|
|
182
|
+
backgroundColor: 'transparent',
|
|
183
|
+
borderWidth: 0,
|
|
184
|
+
borderBottomWidth: 1,
|
|
185
|
+
borderBottomColor: borderColor,
|
|
186
|
+
borderRadius: 0,
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
default:
|
|
190
|
+
return baseStyle;
|
|
191
|
+
}
|
|
132
192
|
};
|
|
133
193
|
|
|
134
194
|
// Get text color based on state
|
|
135
195
|
const getTextColor = () => {
|
|
136
|
-
if (
|
|
137
|
-
if (
|
|
138
|
-
return tokens.colors.
|
|
196
|
+
if (isDisabled) return tokens.colors.textDisabled;
|
|
197
|
+
if (hasError) return tokens.colors.error;
|
|
198
|
+
if (hasSuccess) return tokens.colors.success;
|
|
199
|
+
return tokens.colors.textPrimary;
|
|
139
200
|
};
|
|
140
201
|
|
|
202
|
+
const containerStyle: StyleProp<ViewStyle> = [
|
|
203
|
+
styles.container,
|
|
204
|
+
getVariantStyle(),
|
|
205
|
+
{
|
|
206
|
+
paddingVertical: config.paddingVertical,
|
|
207
|
+
paddingHorizontal: config.paddingHorizontal,
|
|
208
|
+
height: getTextAreaHeight(),
|
|
209
|
+
opacity: isDisabled ? 0.5 : 1,
|
|
210
|
+
},
|
|
211
|
+
style,
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
const textInputStyle: StyleProp<TextStyle> = [
|
|
215
|
+
styles.input,
|
|
216
|
+
{
|
|
217
|
+
fontSize: config.fontSize,
|
|
218
|
+
lineHeight: config.lineHeight,
|
|
219
|
+
color: getTextColor(),
|
|
220
|
+
},
|
|
221
|
+
inputStyle,
|
|
222
|
+
];
|
|
223
|
+
|
|
141
224
|
return (
|
|
142
|
-
<View
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
{
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
225
|
+
<View testID={testID}>
|
|
226
|
+
{label && (
|
|
227
|
+
<AtomicText
|
|
228
|
+
type="labelMedium"
|
|
229
|
+
color={hasError ? 'error' : hasSuccess ? 'success' : 'secondary'}
|
|
230
|
+
style={styles.label}
|
|
231
|
+
>
|
|
232
|
+
{label}
|
|
233
|
+
</AtomicText>
|
|
234
|
+
)}
|
|
235
|
+
|
|
236
|
+
<View style={containerStyle}>
|
|
237
|
+
<TextInput
|
|
238
|
+
value={value}
|
|
239
|
+
onChangeText={onChangeText}
|
|
240
|
+
placeholder={placeholder}
|
|
241
|
+
placeholderTextColor={tokens.colors.textSecondary}
|
|
242
|
+
maxLength={maxLength}
|
|
243
|
+
autoCapitalize={autoCapitalize}
|
|
244
|
+
autoCorrect={autoCorrect}
|
|
245
|
+
editable={!isDisabled}
|
|
246
|
+
multiline={true}
|
|
247
|
+
numberOfLines={rows}
|
|
248
|
+
textAlignVertical="top"
|
|
249
|
+
style={textInputStyle}
|
|
250
|
+
onBlur={() => {
|
|
251
|
+
setIsFocused(false);
|
|
252
|
+
onBlur?.();
|
|
253
|
+
}}
|
|
254
|
+
onFocus={() => {
|
|
255
|
+
setIsFocused(true);
|
|
256
|
+
onFocus?.();
|
|
257
|
+
}}
|
|
258
|
+
testID={testID ? `${testID}-input` : undefined}
|
|
259
|
+
/>
|
|
260
|
+
</View>
|
|
165
261
|
|
|
166
262
|
{(helperText || showCharacterCount) && (
|
|
167
|
-
<View style={
|
|
168
|
-
flexDirection: 'row',
|
|
169
|
-
justifyContent: 'space-between',
|
|
170
|
-
marginTop: tokens.spacing.xs,
|
|
171
|
-
}}>
|
|
263
|
+
<View style={styles.helperRow}>
|
|
172
264
|
{helperText && (
|
|
173
|
-
<
|
|
174
|
-
type=
|
|
175
|
-
|
|
176
|
-
style={
|
|
265
|
+
<AtomicText
|
|
266
|
+
type="bodySmall"
|
|
267
|
+
color={hasError ? 'error' : 'secondary'}
|
|
268
|
+
style={styles.helperText}
|
|
177
269
|
testID={testID ? `${testID}-helper` : undefined}
|
|
178
270
|
>
|
|
179
271
|
{helperText}
|
|
180
|
-
</
|
|
272
|
+
</AtomicText>
|
|
181
273
|
)}
|
|
182
274
|
{showCharacterCount && maxLength && (
|
|
183
|
-
<
|
|
184
|
-
type="
|
|
185
|
-
|
|
186
|
-
style={
|
|
275
|
+
<AtomicText
|
|
276
|
+
type="bodySmall"
|
|
277
|
+
color="secondary"
|
|
278
|
+
style={styles.characterCount}
|
|
187
279
|
testID={testID ? `${testID}-count` : undefined}
|
|
188
280
|
>
|
|
189
281
|
{characterCount}/{maxLength}
|
|
190
|
-
</
|
|
282
|
+
</AtomicText>
|
|
191
283
|
)}
|
|
192
284
|
</View>
|
|
193
285
|
)}
|
|
@@ -195,4 +287,27 @@ export const AtomicTextArea: React.FC<AtomicTextAreaProps> = ({
|
|
|
195
287
|
);
|
|
196
288
|
};
|
|
197
289
|
|
|
290
|
+
const styles = StyleSheet.create({
|
|
291
|
+
container: {
|
|
292
|
+
justifyContent: 'flex-start',
|
|
293
|
+
},
|
|
294
|
+
input: {
|
|
295
|
+
flex: 1,
|
|
296
|
+
},
|
|
297
|
+
label: {
|
|
298
|
+
marginBottom: 4,
|
|
299
|
+
},
|
|
300
|
+
helperRow: {
|
|
301
|
+
flexDirection: 'row',
|
|
302
|
+
justifyContent: 'space-between',
|
|
303
|
+
marginTop: 4,
|
|
304
|
+
},
|
|
305
|
+
helperText: {
|
|
306
|
+
flex: 1,
|
|
307
|
+
},
|
|
308
|
+
characterCount: {
|
|
309
|
+
marginLeft: 8,
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
|
|
198
313
|
export type { AtomicTextAreaProps as TextAreaProps };
|
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* FormContainer Component
|
|
3
3
|
*
|
|
4
|
-
* A reusable container for forms with
|
|
5
|
-
* proper keyboard handling, and responsive layout.
|
|
4
|
+
* A reusable container for forms with proper keyboard handling and responsive layout.
|
|
6
5
|
*
|
|
7
6
|
* Features:
|
|
8
|
-
* -
|
|
7
|
+
* - Pure React Native implementation (no Paper dependency)
|
|
9
8
|
* - Universal keyboard handling (no platform-specific code)
|
|
10
9
|
* - ScrollView with automatic content padding
|
|
11
10
|
* - Safe area insets for bottom tab navigation overlap
|
|
12
11
|
* - Responsive max width for large screens (tablets)
|
|
13
12
|
* - Consistent vertical spacing between form elements
|
|
14
|
-
* - Theme-aware surface colors
|
|
13
|
+
* - Theme-aware surface colors
|
|
15
14
|
* - Optimized performance with memoized styles
|
|
16
15
|
*
|
|
17
16
|
* Usage:
|
|
@@ -31,13 +30,12 @@
|
|
|
31
30
|
* - Consistent form layout across all 100+ generated apps
|
|
32
31
|
* - Responsive design for tablets (max 700px) and phones (full width)
|
|
33
32
|
* - Automatic vertical spacing between form elements (no manual marginBottom)
|
|
34
|
-
* - Material Design 3 surface with proper elevation
|
|
35
33
|
* - Reduces boilerplate in form screens
|
|
36
34
|
* - Universal code - no platform checks, works on iOS, Android, Web
|
|
37
35
|
*
|
|
38
36
|
* Technical Details:
|
|
39
37
|
* - Uses ScrollView with contentContainerStyle for keyboard handling
|
|
40
|
-
* - React Native
|
|
38
|
+
* - Pure React Native View for surface (lightweight)
|
|
41
39
|
* - Vertical spacing via Children.map() wrapping (universal compatibility)
|
|
42
40
|
* - Safe area insets from react-native-safe-area-context
|
|
43
41
|
* - Responsive values from useResponsive hook
|
|
@@ -53,7 +51,6 @@ import {
|
|
|
53
51
|
StyleProp,
|
|
54
52
|
ViewStyle,
|
|
55
53
|
} from 'react-native';
|
|
56
|
-
import { Surface } from 'react-native-paper';
|
|
57
54
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
58
55
|
import { useAppDesignTokens } from '../hooks/useAppDesignTokens';
|
|
59
56
|
import { useResponsive } from '../hooks/useResponsive';
|
|
@@ -72,15 +69,15 @@ export interface FormContainerProps {
|
|
|
72
69
|
showsVerticalScrollIndicator?: boolean;
|
|
73
70
|
/** Optional test ID for E2E testing */
|
|
74
71
|
testID?: string;
|
|
75
|
-
/**
|
|
76
|
-
|
|
72
|
+
/** Show surface border (default: true) */
|
|
73
|
+
showBorder?: boolean;
|
|
77
74
|
}
|
|
78
75
|
|
|
79
76
|
/**
|
|
80
77
|
* FormContainer - Universal form wrapper component
|
|
81
78
|
*
|
|
82
79
|
* Wraps forms with:
|
|
83
|
-
* -
|
|
80
|
+
* - Pure React Native surface
|
|
84
81
|
* - Universal keyboard handling (no platform checks)
|
|
85
82
|
* - ScrollView for content overflow
|
|
86
83
|
* - Safe area insets (bottom tabs, notch)
|
|
@@ -93,7 +90,7 @@ export const FormContainer: React.FC<FormContainerProps> = ({
|
|
|
93
90
|
contentContainerStyle,
|
|
94
91
|
showsVerticalScrollIndicator = false,
|
|
95
92
|
testID,
|
|
96
|
-
|
|
93
|
+
showBorder = true,
|
|
97
94
|
}) => {
|
|
98
95
|
const tokens = useAppDesignTokens();
|
|
99
96
|
const insets = useSafeAreaInsets();
|
|
@@ -111,6 +108,9 @@ export const FormContainer: React.FC<FormContainerProps> = ({
|
|
|
111
108
|
surface: {
|
|
112
109
|
flex: 1,
|
|
113
110
|
backgroundColor: tokens.colors.surface,
|
|
111
|
+
borderWidth: showBorder ? 1 : 0,
|
|
112
|
+
borderColor: tokens.colors.border,
|
|
113
|
+
borderRadius: tokens.borders.radius.md,
|
|
114
114
|
},
|
|
115
115
|
scrollView: {
|
|
116
116
|
flex: 1,
|
|
@@ -135,12 +135,15 @@ export const FormContainer: React.FC<FormContainerProps> = ({
|
|
|
135
135
|
[
|
|
136
136
|
tokens.colors.backgroundPrimary,
|
|
137
137
|
tokens.colors.surface,
|
|
138
|
+
tokens.colors.border,
|
|
139
|
+
tokens.borders.radius.md,
|
|
138
140
|
tokens.spacing.lg,
|
|
139
141
|
tokens.spacing.xl,
|
|
140
142
|
formBottomPadding,
|
|
141
143
|
formContentWidth,
|
|
142
144
|
formElementSpacing,
|
|
143
145
|
insets.bottom,
|
|
146
|
+
showBorder,
|
|
144
147
|
]
|
|
145
148
|
);
|
|
146
149
|
|
|
@@ -160,10 +163,7 @@ export const FormContainer: React.FC<FormContainerProps> = ({
|
|
|
160
163
|
|
|
161
164
|
return (
|
|
162
165
|
<View style={[styles.container, containerStyle]} testID={testID}>
|
|
163
|
-
<
|
|
164
|
-
style={styles.surface}
|
|
165
|
-
elevation={disableElevation ? 0 : 1}
|
|
166
|
-
>
|
|
166
|
+
<View style={styles.surface}>
|
|
167
167
|
<ScrollView
|
|
168
168
|
style={styles.scrollView}
|
|
169
169
|
contentContainerStyle={[styles.contentContainer, contentContainerStyle]}
|
|
@@ -174,7 +174,7 @@ export const FormContainer: React.FC<FormContainerProps> = ({
|
|
|
174
174
|
>
|
|
175
175
|
{childrenWithSpacing}
|
|
176
176
|
</ScrollView>
|
|
177
|
-
</
|
|
177
|
+
</View>
|
|
178
178
|
</View>
|
|
179
179
|
);
|
|
180
180
|
};
|
|
@@ -155,6 +155,7 @@ export const lightColors = {
|
|
|
155
155
|
borderLight: '#F1F5F9',
|
|
156
156
|
borderMedium: '#CBD5E1',
|
|
157
157
|
borderFocus: '#3B82F6',
|
|
158
|
+
borderDisabled: '#F1F5F9',
|
|
158
159
|
|
|
159
160
|
// =============================================================================
|
|
160
161
|
// COMPONENT-SPECIFIC COLORS
|
|
@@ -327,6 +328,7 @@ export const darkColors = {
|
|
|
327
328
|
borderLight: '#475569', // Slate 600 - Light border
|
|
328
329
|
borderMedium: '#64748B', // Slate 500 - Medium border
|
|
329
330
|
borderFocus: '#60A5FA', // Blue 400 - Focus border (lighter)
|
|
331
|
+
borderDisabled: '#475569', // Slate 600 - Disabled border
|
|
330
332
|
|
|
331
333
|
// =============================================================================
|
|
332
334
|
// COMPONENT-SPECIFIC COLORS (dark mode specific)
|