@umituz/react-native-design-system 2.3.39 → 2.4.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/package.json +2 -1
- package/src/index.ts +8 -0
- package/src/molecules/PhotoUploadCard/PhotoUploadCard.tsx +241 -0
- package/src/molecules/PhotoUploadCard/index.ts +5 -0
- package/src/molecules/StepHeader/StepHeader.tsx +122 -0
- package/src/molecules/StepHeader/index.ts +2 -0
- package/src/molecules/index.ts +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-design-system",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.1",
|
|
4
4
|
"description": "Universal design system for React Native apps - Consolidated package with atoms, molecules, organisms, theme, typography, responsive and safe area utilities",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -123,6 +123,7 @@
|
|
|
123
123
|
"expo-device": "~7.0.2",
|
|
124
124
|
"expo-file-system": "^19.0.21",
|
|
125
125
|
"expo-haptics": "~14.0.0",
|
|
126
|
+
"expo-linear-gradient": "~15.0.7",
|
|
126
127
|
"expo-localization": "~16.0.1",
|
|
127
128
|
"expo-sharing": "~14.0.8",
|
|
128
129
|
"react": "19.1.0",
|
package/src/index.ts
CHANGED
|
@@ -405,6 +405,14 @@ export {
|
|
|
405
405
|
type CountdownTarget,
|
|
406
406
|
type CountdownFormatOptions,
|
|
407
407
|
type CountdownDisplayConfig,
|
|
408
|
+
// Photo Upload
|
|
409
|
+
PhotoUploadCard,
|
|
410
|
+
type PhotoUploadCardProps,
|
|
411
|
+
type PhotoUploadCardConfig,
|
|
412
|
+
// Step Header
|
|
413
|
+
StepHeader,
|
|
414
|
+
type StepHeaderProps,
|
|
415
|
+
type StepHeaderConfig,
|
|
408
416
|
// Animation
|
|
409
417
|
Animated,
|
|
410
418
|
useSharedValue,
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PhotoUploadCard Component
|
|
3
|
+
* Beautiful photo upload card with validation status and responsive design
|
|
4
|
+
*
|
|
5
|
+
* @package @umituz/react-native-design-system
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useMemo } from "react";
|
|
9
|
+
import {
|
|
10
|
+
View,
|
|
11
|
+
Image,
|
|
12
|
+
StyleSheet,
|
|
13
|
+
Pressable,
|
|
14
|
+
TouchableOpacity,
|
|
15
|
+
ActivityIndicator,
|
|
16
|
+
type ViewStyle,
|
|
17
|
+
type StyleProp,
|
|
18
|
+
} from "react-native";
|
|
19
|
+
import { LinearGradient } from "expo-linear-gradient";
|
|
20
|
+
import { AtomicText } from "../../atoms/AtomicText";
|
|
21
|
+
import { AtomicIcon } from "../../atoms/AtomicIcon";
|
|
22
|
+
import { useAppDesignTokens } from "../../theme/hooks/useAppDesignTokens";
|
|
23
|
+
|
|
24
|
+
export interface PhotoUploadCardConfig {
|
|
25
|
+
aspectRatio?: number;
|
|
26
|
+
borderRadius?: number;
|
|
27
|
+
iconSize?: number;
|
|
28
|
+
showValidationStatus?: boolean;
|
|
29
|
+
allowChange?: boolean;
|
|
30
|
+
borderStyle?: "solid" | "dashed";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface PhotoUploadCardProps {
|
|
34
|
+
imageUri: string | null;
|
|
35
|
+
onPress: () => void;
|
|
36
|
+
isValidating?: boolean;
|
|
37
|
+
isValid?: boolean | null;
|
|
38
|
+
disabled?: boolean;
|
|
39
|
+
config?: PhotoUploadCardConfig;
|
|
40
|
+
translations: {
|
|
41
|
+
tapToUpload: string;
|
|
42
|
+
selectPhoto: string;
|
|
43
|
+
change: string;
|
|
44
|
+
analyzing?: string;
|
|
45
|
+
};
|
|
46
|
+
style?: StyleProp<ViewStyle>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const DEFAULT_CONFIG: PhotoUploadCardConfig = {
|
|
50
|
+
aspectRatio: 1,
|
|
51
|
+
borderRadius: 28,
|
|
52
|
+
iconSize: 40,
|
|
53
|
+
showValidationStatus: true,
|
|
54
|
+
allowChange: true,
|
|
55
|
+
borderStyle: "dashed",
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const PhotoUploadCard: React.FC<PhotoUploadCardProps> = ({
|
|
59
|
+
imageUri,
|
|
60
|
+
onPress,
|
|
61
|
+
isValidating = false,
|
|
62
|
+
isValid = null,
|
|
63
|
+
disabled = false,
|
|
64
|
+
config = DEFAULT_CONFIG,
|
|
65
|
+
translations,
|
|
66
|
+
style,
|
|
67
|
+
}) => {
|
|
68
|
+
const tokens = useAppDesignTokens();
|
|
69
|
+
const cfg = { ...DEFAULT_CONFIG, ...config };
|
|
70
|
+
|
|
71
|
+
const borderColor = useMemo(() => {
|
|
72
|
+
if (!cfg.showValidationStatus) {
|
|
73
|
+
return `${tokens.colors.primary}40`;
|
|
74
|
+
}
|
|
75
|
+
if (isValidating) return tokens.colors.primary;
|
|
76
|
+
if (isValid === true) return tokens.colors.success;
|
|
77
|
+
if (isValid === false) return tokens.colors.error;
|
|
78
|
+
return `${tokens.colors.primary}40`;
|
|
79
|
+
}, [isValidating, isValid, tokens, cfg.showValidationStatus]);
|
|
80
|
+
|
|
81
|
+
const styles = useMemo(
|
|
82
|
+
() =>
|
|
83
|
+
StyleSheet.create({
|
|
84
|
+
container: {
|
|
85
|
+
marginHorizontal: 24,
|
|
86
|
+
marginBottom: 24,
|
|
87
|
+
},
|
|
88
|
+
card: {
|
|
89
|
+
aspectRatio: cfg.aspectRatio,
|
|
90
|
+
backgroundColor: tokens.colors.surfaceSecondary,
|
|
91
|
+
borderRadius: cfg.borderRadius,
|
|
92
|
+
justifyContent: "center",
|
|
93
|
+
alignItems: "center",
|
|
94
|
+
overflow: "hidden",
|
|
95
|
+
borderWidth: 2,
|
|
96
|
+
borderStyle: imageUri ? "solid" : cfg.borderStyle,
|
|
97
|
+
},
|
|
98
|
+
placeholder: {
|
|
99
|
+
alignItems: "center",
|
|
100
|
+
padding: 32,
|
|
101
|
+
},
|
|
102
|
+
iconCircle: {
|
|
103
|
+
width: 88,
|
|
104
|
+
height: 88,
|
|
105
|
+
borderRadius: 44,
|
|
106
|
+
justifyContent: "center",
|
|
107
|
+
alignItems: "center",
|
|
108
|
+
marginBottom: 20,
|
|
109
|
+
borderWidth: 2,
|
|
110
|
+
borderColor: `${tokens.colors.primary}30`,
|
|
111
|
+
},
|
|
112
|
+
iconGradient: {
|
|
113
|
+
width: 88,
|
|
114
|
+
height: 88,
|
|
115
|
+
borderRadius: 44,
|
|
116
|
+
justifyContent: "center",
|
|
117
|
+
alignItems: "center",
|
|
118
|
+
},
|
|
119
|
+
title: {
|
|
120
|
+
fontSize: 20,
|
|
121
|
+
fontWeight: "700",
|
|
122
|
+
color: tokens.colors.textPrimary,
|
|
123
|
+
marginBottom: 8,
|
|
124
|
+
letterSpacing: 0.3,
|
|
125
|
+
},
|
|
126
|
+
subtitle: {
|
|
127
|
+
fontSize: 14,
|
|
128
|
+
color: tokens.colors.textSecondary,
|
|
129
|
+
textAlign: "center",
|
|
130
|
+
lineHeight: 20,
|
|
131
|
+
maxWidth: 240,
|
|
132
|
+
},
|
|
133
|
+
image: {
|
|
134
|
+
width: "100%",
|
|
135
|
+
height: "100%",
|
|
136
|
+
resizeMode: "cover",
|
|
137
|
+
},
|
|
138
|
+
imageOverlay: {
|
|
139
|
+
...StyleSheet.absoluteFillObject,
|
|
140
|
+
backgroundColor: "rgba(0,0,0,0.15)",
|
|
141
|
+
},
|
|
142
|
+
changeButton: {
|
|
143
|
+
position: "absolute",
|
|
144
|
+
bottom: 20,
|
|
145
|
+
right: 20,
|
|
146
|
+
backgroundColor: tokens.colors.surface,
|
|
147
|
+
paddingHorizontal: 18,
|
|
148
|
+
paddingVertical: 12,
|
|
149
|
+
borderRadius: 28,
|
|
150
|
+
flexDirection: "row",
|
|
151
|
+
alignItems: "center",
|
|
152
|
+
gap: 8,
|
|
153
|
+
},
|
|
154
|
+
changeText: {
|
|
155
|
+
fontSize: 14,
|
|
156
|
+
fontWeight: "700",
|
|
157
|
+
color: tokens.colors.primary,
|
|
158
|
+
},
|
|
159
|
+
validatingContainer: {
|
|
160
|
+
alignItems: "center",
|
|
161
|
+
padding: 32,
|
|
162
|
+
},
|
|
163
|
+
validatingText: {
|
|
164
|
+
fontSize: 16,
|
|
165
|
+
fontWeight: "600",
|
|
166
|
+
color: tokens.colors.primary,
|
|
167
|
+
marginTop: 20,
|
|
168
|
+
},
|
|
169
|
+
pulseRing: {
|
|
170
|
+
position: "absolute",
|
|
171
|
+
width: 100,
|
|
172
|
+
height: 100,
|
|
173
|
+
borderRadius: 50,
|
|
174
|
+
borderWidth: 2,
|
|
175
|
+
borderColor: `${tokens.colors.primary}30`,
|
|
176
|
+
},
|
|
177
|
+
}),
|
|
178
|
+
[tokens, imageUri, cfg],
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<View style={[styles.container, style]}>
|
|
183
|
+
<Pressable
|
|
184
|
+
style={[styles.card, { borderColor }]}
|
|
185
|
+
onPress={onPress}
|
|
186
|
+
disabled={disabled || isValidating}
|
|
187
|
+
>
|
|
188
|
+
{isValidating ? (
|
|
189
|
+
<View style={styles.validatingContainer}>
|
|
190
|
+
<View style={styles.pulseRing} />
|
|
191
|
+
<ActivityIndicator size="large" color={tokens.colors.primary} />
|
|
192
|
+
<AtomicText style={styles.validatingText}>
|
|
193
|
+
{translations.analyzing || "Analyzing..."}
|
|
194
|
+
</AtomicText>
|
|
195
|
+
</View>
|
|
196
|
+
) : imageUri ? (
|
|
197
|
+
<>
|
|
198
|
+
<Image source={{ uri: imageUri }} style={styles.image} />
|
|
199
|
+
<View style={styles.imageOverlay} />
|
|
200
|
+
{cfg.allowChange && (
|
|
201
|
+
<TouchableOpacity style={styles.changeButton} onPress={onPress}>
|
|
202
|
+
<AtomicIcon
|
|
203
|
+
name="camera"
|
|
204
|
+
size={18}
|
|
205
|
+
customColor={tokens.colors.primary}
|
|
206
|
+
/>
|
|
207
|
+
<AtomicText style={styles.changeText}>
|
|
208
|
+
{translations.change}
|
|
209
|
+
</AtomicText>
|
|
210
|
+
</TouchableOpacity>
|
|
211
|
+
)}
|
|
212
|
+
</>
|
|
213
|
+
) : (
|
|
214
|
+
<View style={styles.placeholder}>
|
|
215
|
+
<View style={styles.iconCircle}>
|
|
216
|
+
<LinearGradient
|
|
217
|
+
colors={[
|
|
218
|
+
`${tokens.colors.primary}20`,
|
|
219
|
+
`${tokens.colors.primary}10`,
|
|
220
|
+
]}
|
|
221
|
+
style={styles.iconGradient}
|
|
222
|
+
>
|
|
223
|
+
<AtomicIcon
|
|
224
|
+
name="camera"
|
|
225
|
+
size={cfg.iconSize}
|
|
226
|
+
customColor={tokens.colors.primary}
|
|
227
|
+
/>
|
|
228
|
+
</LinearGradient>
|
|
229
|
+
</View>
|
|
230
|
+
<AtomicText style={styles.title}>
|
|
231
|
+
{translations.tapToUpload}
|
|
232
|
+
</AtomicText>
|
|
233
|
+
<AtomicText style={styles.subtitle}>
|
|
234
|
+
{translations.selectPhoto}
|
|
235
|
+
</AtomicText>
|
|
236
|
+
</View>
|
|
237
|
+
)}
|
|
238
|
+
</Pressable>
|
|
239
|
+
</View>
|
|
240
|
+
);
|
|
241
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StepHeader Component
|
|
3
|
+
* Header component for step-by-step flows with title and subtitle
|
|
4
|
+
*
|
|
5
|
+
* @package @umituz/react-native-design-system
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useMemo } from "react";
|
|
9
|
+
import { View, StyleSheet, type ViewStyle, type StyleProp } from "react-native";
|
|
10
|
+
import { AtomicText } from "../../atoms/AtomicText";
|
|
11
|
+
import { useAppDesignTokens } from "../../theme/hooks/useAppDesignTokens";
|
|
12
|
+
|
|
13
|
+
export interface StepHeaderConfig {
|
|
14
|
+
showStepIndicator?: boolean;
|
|
15
|
+
currentStep?: number;
|
|
16
|
+
totalSteps?: number;
|
|
17
|
+
titleAlignment?: "left" | "center" | "right";
|
|
18
|
+
titleFontSize?: number;
|
|
19
|
+
subtitleFontSize?: number;
|
|
20
|
+
spacing?: {
|
|
21
|
+
marginBottom?: number;
|
|
22
|
+
paddingHorizontal?: number;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface StepHeaderProps {
|
|
27
|
+
title: string;
|
|
28
|
+
subtitle?: string;
|
|
29
|
+
config?: StepHeaderConfig;
|
|
30
|
+
style?: StyleProp<ViewStyle>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const DEFAULT_CONFIG: StepHeaderConfig = {
|
|
34
|
+
showStepIndicator: false,
|
|
35
|
+
titleAlignment: "left",
|
|
36
|
+
titleFontSize: 28,
|
|
37
|
+
subtitleFontSize: 16,
|
|
38
|
+
spacing: {
|
|
39
|
+
marginBottom: 32,
|
|
40
|
+
paddingHorizontal: 24,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const StepHeader: React.FC<StepHeaderProps> = ({
|
|
45
|
+
title,
|
|
46
|
+
subtitle,
|
|
47
|
+
config = DEFAULT_CONFIG,
|
|
48
|
+
style,
|
|
49
|
+
}) => {
|
|
50
|
+
const tokens = useAppDesignTokens();
|
|
51
|
+
const cfg = { ...DEFAULT_CONFIG, ...config };
|
|
52
|
+
|
|
53
|
+
const styles = useMemo(
|
|
54
|
+
() =>
|
|
55
|
+
StyleSheet.create({
|
|
56
|
+
container: {
|
|
57
|
+
paddingHorizontal: cfg.spacing?.paddingHorizontal ?? 24,
|
|
58
|
+
marginBottom: cfg.spacing?.marginBottom ?? 32,
|
|
59
|
+
},
|
|
60
|
+
stepIndicator: {
|
|
61
|
+
flexDirection: "row",
|
|
62
|
+
alignItems: "center",
|
|
63
|
+
marginBottom: 12,
|
|
64
|
+
},
|
|
65
|
+
stepDot: {
|
|
66
|
+
width: 8,
|
|
67
|
+
height: 8,
|
|
68
|
+
borderRadius: 4,
|
|
69
|
+
marginHorizontal: 4,
|
|
70
|
+
},
|
|
71
|
+
activeDot: {
|
|
72
|
+
backgroundColor: tokens.colors.primary,
|
|
73
|
+
},
|
|
74
|
+
inactiveDot: {
|
|
75
|
+
backgroundColor: `${tokens.colors.primary}30`,
|
|
76
|
+
},
|
|
77
|
+
title: {
|
|
78
|
+
fontSize: cfg.titleFontSize,
|
|
79
|
+
fontWeight: "900",
|
|
80
|
+
color: tokens.colors.textPrimary,
|
|
81
|
+
textAlign: cfg.titleAlignment,
|
|
82
|
+
marginBottom: subtitle ? 12 : 0,
|
|
83
|
+
letterSpacing: 0.3,
|
|
84
|
+
},
|
|
85
|
+
subtitle: {
|
|
86
|
+
fontSize: cfg.subtitleFontSize,
|
|
87
|
+
fontWeight: "500",
|
|
88
|
+
color: tokens.colors.textSecondary,
|
|
89
|
+
textAlign: cfg.titleAlignment,
|
|
90
|
+
lineHeight: (cfg.subtitleFontSize ?? 16) * 1.5,
|
|
91
|
+
opacity: 0.9,
|
|
92
|
+
},
|
|
93
|
+
}),
|
|
94
|
+
[tokens, cfg, subtitle],
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<View style={[styles.container, style]}>
|
|
99
|
+
{cfg.showStepIndicator &&
|
|
100
|
+
cfg.currentStep !== undefined &&
|
|
101
|
+
cfg.totalSteps !== undefined && (
|
|
102
|
+
<View style={styles.stepIndicator}>
|
|
103
|
+
{Array.from({ length: cfg.totalSteps }, (_, i) => (
|
|
104
|
+
<View
|
|
105
|
+
key={i}
|
|
106
|
+
style={[
|
|
107
|
+
styles.stepDot,
|
|
108
|
+
i + 1 <= cfg.currentStep!
|
|
109
|
+
? styles.activeDot
|
|
110
|
+
: styles.inactiveDot,
|
|
111
|
+
]}
|
|
112
|
+
/>
|
|
113
|
+
))}
|
|
114
|
+
</View>
|
|
115
|
+
)}
|
|
116
|
+
|
|
117
|
+
<AtomicText style={styles.title}>{title}</AtomicText>
|
|
118
|
+
|
|
119
|
+
{subtitle && <AtomicText style={styles.subtitle}>{subtitle}</AtomicText>}
|
|
120
|
+
</View>
|
|
121
|
+
);
|
|
122
|
+
};
|
package/src/molecules/index.ts
CHANGED