@umituz/react-native-ai-generation-content 1.17.2 → 1.17.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/presentation/components/GenerationProgressContent.tsx +137 -99
- package/src/presentation/components/GenerationProgressModal.tsx +62 -52
- package/src/presentation/components/buttons/GenerateButton.tsx +141 -0
- package/src/presentation/components/buttons/index.ts +1 -0
- package/src/presentation/components/display/ErrorDisplay.tsx +111 -0
- package/src/presentation/components/display/ResultDisplay.tsx +122 -0
- package/src/presentation/components/display/index.ts +6 -0
- package/src/presentation/components/headers/FeatureHeader.tsx +85 -0
- package/src/presentation/components/headers/index.ts +1 -0
- package/src/presentation/components/image-picker/DualImagePicker.tsx +95 -0
- package/src/presentation/components/image-picker/ImagePickerBox.tsx +165 -0
- package/src/presentation/components/image-picker/index.ts +2 -0
- package/src/presentation/components/index.ts +4 -0
package/package.json
CHANGED
|
@@ -1,123 +1,161 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* GenerationProgressContent
|
|
3
3
|
* Content for the AI generation progress modal
|
|
4
|
+
* Props-driven for 100+ apps compatibility
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
import React from "react";
|
|
7
8
|
import { View, TouchableOpacity, StyleSheet } from "react-native";
|
|
8
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
AtomicText,
|
|
11
|
+
AtomicIcon,
|
|
12
|
+
useAppDesignTokens,
|
|
13
|
+
} from "@umituz/react-native-design-system";
|
|
9
14
|
import { GenerationProgressBar } from "./GenerationProgressBar";
|
|
10
15
|
|
|
11
16
|
export interface GenerationProgressContentProps {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
readonly progress: number;
|
|
18
|
+
readonly icon?: string;
|
|
19
|
+
readonly title?: string;
|
|
20
|
+
readonly message?: string;
|
|
21
|
+
readonly hint?: string;
|
|
22
|
+
readonly dismissLabel?: string;
|
|
23
|
+
readonly onDismiss?: () => void;
|
|
24
|
+
readonly backgroundColor?: string;
|
|
25
|
+
readonly textColor?: string;
|
|
26
|
+
readonly hintColor?: string;
|
|
27
|
+
readonly progressColor?: string;
|
|
28
|
+
readonly progressBackgroundColor?: string;
|
|
29
|
+
readonly dismissButtonColor?: string;
|
|
23
30
|
}
|
|
24
31
|
|
|
25
32
|
export const GenerationProgressContent: React.FC<
|
|
26
|
-
|
|
33
|
+
GenerationProgressContentProps
|
|
27
34
|
> = ({
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
progress,
|
|
36
|
+
icon,
|
|
37
|
+
title,
|
|
38
|
+
message,
|
|
39
|
+
hint,
|
|
40
|
+
dismissLabel,
|
|
41
|
+
onDismiss,
|
|
42
|
+
backgroundColor,
|
|
43
|
+
textColor,
|
|
44
|
+
hintColor,
|
|
45
|
+
progressColor,
|
|
46
|
+
progressBackgroundColor,
|
|
47
|
+
dismissButtonColor,
|
|
39
48
|
}) => {
|
|
40
|
-
|
|
49
|
+
const tokens = useAppDesignTokens();
|
|
41
50
|
|
|
42
|
-
|
|
43
|
-
|
|
51
|
+
const activeTextColor = textColor || tokens.colors.textPrimary;
|
|
52
|
+
const activeBgColor = backgroundColor || tokens.colors.surface;
|
|
53
|
+
const activeHintColor = hintColor || tokens.colors.textTertiary;
|
|
44
54
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
55
|
+
return (
|
|
56
|
+
<View
|
|
57
|
+
style={[
|
|
58
|
+
styles.modal,
|
|
59
|
+
{
|
|
60
|
+
backgroundColor: activeBgColor,
|
|
61
|
+
borderColor: tokens.colors.borderLight,
|
|
62
|
+
},
|
|
63
|
+
]}
|
|
64
|
+
>
|
|
65
|
+
{icon && (
|
|
66
|
+
<View style={styles.iconContainer}>
|
|
67
|
+
<AtomicIcon name={icon} size="xl" color="primary" />
|
|
68
|
+
</View>
|
|
69
|
+
)}
|
|
50
70
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
71
|
+
{title && (
|
|
72
|
+
<AtomicText
|
|
73
|
+
type="headlineSmall"
|
|
74
|
+
style={[styles.title, { color: activeTextColor }]}
|
|
75
|
+
>
|
|
76
|
+
{title}
|
|
77
|
+
</AtomicText>
|
|
78
|
+
)}
|
|
56
79
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
80
|
+
{message && (
|
|
81
|
+
<AtomicText
|
|
82
|
+
type="bodyMedium"
|
|
83
|
+
style={[styles.message, { color: tokens.colors.textSecondary }]}
|
|
84
|
+
>
|
|
85
|
+
{message}
|
|
86
|
+
</AtomicText>
|
|
87
|
+
)}
|
|
63
88
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
89
|
+
<GenerationProgressBar
|
|
90
|
+
progress={progress}
|
|
91
|
+
textColor={tokens.colors.primary}
|
|
92
|
+
progressColor={progressColor}
|
|
93
|
+
backgroundColor={progressBackgroundColor}
|
|
94
|
+
/>
|
|
67
95
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
96
|
+
{hint && (
|
|
97
|
+
<AtomicText
|
|
98
|
+
type="bodySmall"
|
|
99
|
+
style={[styles.hint, { color: activeHintColor }]}
|
|
100
|
+
>
|
|
101
|
+
{hint}
|
|
102
|
+
</AtomicText>
|
|
103
|
+
)}
|
|
104
|
+
|
|
105
|
+
{onDismiss && (
|
|
106
|
+
<TouchableOpacity
|
|
107
|
+
style={[
|
|
108
|
+
styles.dismissButton,
|
|
109
|
+
{ backgroundColor: dismissButtonColor || tokens.colors.primary },
|
|
110
|
+
]}
|
|
111
|
+
onPress={onDismiss}
|
|
112
|
+
>
|
|
113
|
+
<AtomicText type="bodyMedium" style={styles.dismissText}>
|
|
114
|
+
{dismissLabel || "OK"}
|
|
115
|
+
</AtomicText>
|
|
116
|
+
</TouchableOpacity>
|
|
117
|
+
)}
|
|
118
|
+
</View>
|
|
119
|
+
);
|
|
120
|
+
};
|
|
82
121
|
|
|
83
122
|
const styles = StyleSheet.create({
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
},
|
|
123
|
+
modal: {
|
|
124
|
+
width: "100%",
|
|
125
|
+
maxWidth: 380,
|
|
126
|
+
borderRadius: 24,
|
|
127
|
+
padding: 32,
|
|
128
|
+
borderWidth: 1,
|
|
129
|
+
alignItems: "center",
|
|
130
|
+
},
|
|
131
|
+
iconContainer: {
|
|
132
|
+
marginBottom: 20,
|
|
133
|
+
},
|
|
134
|
+
title: {
|
|
135
|
+
fontWeight: "700",
|
|
136
|
+
marginBottom: 8,
|
|
137
|
+
textAlign: "center",
|
|
138
|
+
},
|
|
139
|
+
message: {
|
|
140
|
+
marginBottom: 28,
|
|
141
|
+
textAlign: "center",
|
|
142
|
+
lineHeight: 20,
|
|
143
|
+
},
|
|
144
|
+
hint: {
|
|
145
|
+
textAlign: "center",
|
|
146
|
+
lineHeight: 18,
|
|
147
|
+
paddingHorizontal: 8,
|
|
148
|
+
},
|
|
149
|
+
dismissButton: {
|
|
150
|
+
marginTop: 16,
|
|
151
|
+
paddingVertical: 14,
|
|
152
|
+
paddingHorizontal: 32,
|
|
153
|
+
borderRadius: 12,
|
|
154
|
+
minWidth: 140,
|
|
155
|
+
alignItems: "center",
|
|
156
|
+
},
|
|
157
|
+
dismissText: {
|
|
158
|
+
color: "#FFFFFF",
|
|
159
|
+
fontWeight: "600",
|
|
160
|
+
},
|
|
123
161
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* GenerationProgressModal
|
|
3
3
|
* Generic AI generation progress modal with customizable rendering
|
|
4
|
+
* Props-driven for 100+ apps compatibility
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
import React from "react";
|
|
@@ -8,23 +9,26 @@ import { Modal, View, StyleSheet } from "react-native";
|
|
|
8
9
|
import { useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
9
10
|
import {
|
|
10
11
|
GenerationProgressContent,
|
|
11
|
-
GenerationProgressContentProps,
|
|
12
|
+
type GenerationProgressContentProps,
|
|
12
13
|
} from "./GenerationProgressContent";
|
|
13
14
|
|
|
14
15
|
export interface GenerationProgressRenderProps {
|
|
15
|
-
progress: number;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
readonly progress: number;
|
|
17
|
+
readonly icon?: string;
|
|
18
|
+
readonly title?: string;
|
|
19
|
+
readonly message?: string;
|
|
20
|
+
readonly hint?: string;
|
|
21
|
+
readonly onDismiss?: () => void;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
export interface GenerationProgressModalProps
|
|
23
25
|
extends Omit<GenerationProgressContentProps, "backgroundColor"> {
|
|
24
|
-
visible: boolean;
|
|
25
|
-
overlayColor?: string;
|
|
26
|
-
modalBackgroundColor?: string;
|
|
27
|
-
renderContent?: (
|
|
26
|
+
readonly visible: boolean;
|
|
27
|
+
readonly overlayColor?: string;
|
|
28
|
+
readonly modalBackgroundColor?: string;
|
|
29
|
+
readonly renderContent?: (
|
|
30
|
+
props: GenerationProgressRenderProps
|
|
31
|
+
) => React.ReactNode;
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
export const GenerationProgressModal: React.FC<
|
|
@@ -32,67 +36,73 @@ export const GenerationProgressModal: React.FC<
|
|
|
32
36
|
> = ({
|
|
33
37
|
visible,
|
|
34
38
|
progress,
|
|
39
|
+
icon,
|
|
35
40
|
title,
|
|
36
41
|
message,
|
|
37
42
|
hint,
|
|
38
43
|
dismissLabel,
|
|
39
44
|
onDismiss,
|
|
40
|
-
overlayColor = "rgba(0, 0, 0, 0.
|
|
45
|
+
overlayColor = "rgba(0, 0, 0, 0.75)",
|
|
41
46
|
modalBackgroundColor,
|
|
42
47
|
textColor,
|
|
48
|
+
hintColor,
|
|
43
49
|
progressColor,
|
|
44
50
|
progressBackgroundColor,
|
|
45
51
|
dismissButtonColor,
|
|
46
52
|
renderContent,
|
|
47
53
|
}) => {
|
|
48
|
-
|
|
49
|
-
|
|
54
|
+
const tokens = useAppDesignTokens();
|
|
55
|
+
const clampedProgress = Math.max(0, Math.min(100, progress));
|
|
50
56
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
57
|
+
const content = renderContent ? (
|
|
58
|
+
renderContent({
|
|
59
|
+
progress: clampedProgress,
|
|
60
|
+
icon,
|
|
61
|
+
title,
|
|
62
|
+
message,
|
|
63
|
+
hint,
|
|
64
|
+
onDismiss,
|
|
65
|
+
})
|
|
66
|
+
) : (
|
|
67
|
+
<GenerationProgressContent
|
|
68
|
+
progress={clampedProgress}
|
|
69
|
+
icon={icon}
|
|
70
|
+
title={title}
|
|
71
|
+
message={message}
|
|
72
|
+
hint={hint}
|
|
73
|
+
dismissLabel={dismissLabel}
|
|
74
|
+
onDismiss={onDismiss}
|
|
75
|
+
backgroundColor={modalBackgroundColor || tokens.colors.surface}
|
|
76
|
+
textColor={textColor || tokens.colors.textPrimary}
|
|
77
|
+
hintColor={hintColor || tokens.colors.textTertiary}
|
|
78
|
+
progressColor={progressColor || tokens.colors.primary}
|
|
79
|
+
progressBackgroundColor={
|
|
80
|
+
progressBackgroundColor || tokens.colors.surfaceVariant
|
|
81
|
+
}
|
|
82
|
+
dismissButtonColor={dismissButtonColor || tokens.colors.primary}
|
|
83
|
+
/>
|
|
84
|
+
);
|
|
76
85
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
</
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
return (
|
|
87
|
+
<Modal
|
|
88
|
+
visible={visible}
|
|
89
|
+
transparent
|
|
90
|
+
animationType="fade"
|
|
91
|
+
statusBarTranslucent
|
|
92
|
+
onRequestClose={onDismiss}
|
|
93
|
+
>
|
|
94
|
+
<View style={[styles.overlay, { backgroundColor: overlayColor }]}>
|
|
95
|
+
{content}
|
|
96
|
+
</View>
|
|
97
|
+
</Modal>
|
|
98
|
+
);
|
|
99
|
+
};
|
|
90
100
|
|
|
91
101
|
const styles = StyleSheet.create({
|
|
92
102
|
overlay: {
|
|
93
103
|
flex: 1,
|
|
94
104
|
justifyContent: "center",
|
|
95
105
|
alignItems: "center",
|
|
96
|
-
padding:
|
|
106
|
+
padding: 24,
|
|
97
107
|
},
|
|
98
108
|
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GenerateButton Component
|
|
3
|
+
* Generic AI generation button with gradient/solid variants
|
|
4
|
+
* Props-driven for 100+ apps compatibility
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { View, StyleSheet, TouchableOpacity } from "react-native";
|
|
9
|
+
import {
|
|
10
|
+
AtomicText,
|
|
11
|
+
useAppDesignTokens,
|
|
12
|
+
AtomicIcon,
|
|
13
|
+
} from "@umituz/react-native-design-system";
|
|
14
|
+
import { LinearGradient } from "expo-linear-gradient";
|
|
15
|
+
|
|
16
|
+
export interface GenerateButtonProps {
|
|
17
|
+
readonly isDisabled?: boolean;
|
|
18
|
+
readonly isProcessing?: boolean;
|
|
19
|
+
readonly onPress: () => void;
|
|
20
|
+
readonly text: string;
|
|
21
|
+
readonly processingText?: string;
|
|
22
|
+
readonly variant?: "gradient" | "solid";
|
|
23
|
+
readonly gradientColors?: readonly [string, string, ...string[]];
|
|
24
|
+
readonly icon?: string;
|
|
25
|
+
readonly iconSize?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const GenerateButton: React.FC<GenerateButtonProps> = ({
|
|
29
|
+
isDisabled = false,
|
|
30
|
+
isProcessing = false,
|
|
31
|
+
onPress,
|
|
32
|
+
text,
|
|
33
|
+
processingText,
|
|
34
|
+
variant = "gradient",
|
|
35
|
+
gradientColors = ["#FF6B9D", "#C74375", "#FF6B9D"],
|
|
36
|
+
icon = "sparkles",
|
|
37
|
+
iconSize = 24,
|
|
38
|
+
}) => {
|
|
39
|
+
const tokens = useAppDesignTokens();
|
|
40
|
+
const disabled = isDisabled || isProcessing;
|
|
41
|
+
const displayText = isProcessing && processingText ? processingText : text;
|
|
42
|
+
|
|
43
|
+
if (variant === "solid") {
|
|
44
|
+
return (
|
|
45
|
+
<View style={[styles.solidContainer, { marginTop: tokens.spacing.xl }]}>
|
|
46
|
+
<TouchableOpacity
|
|
47
|
+
onPress={onPress}
|
|
48
|
+
disabled={disabled}
|
|
49
|
+
activeOpacity={0.8}
|
|
50
|
+
style={[
|
|
51
|
+
styles.solidButton,
|
|
52
|
+
{
|
|
53
|
+
backgroundColor: disabled
|
|
54
|
+
? tokens.colors.surfaceSecondary
|
|
55
|
+
: tokens.colors.primary,
|
|
56
|
+
},
|
|
57
|
+
]}
|
|
58
|
+
>
|
|
59
|
+
<View style={styles.buttonContent}>
|
|
60
|
+
<AtomicIcon name={icon} customSize={20} customColor="#FFFFFF" />
|
|
61
|
+
<AtomicText type="bodyLarge" style={styles.solidButtonText}>
|
|
62
|
+
{displayText}
|
|
63
|
+
</AtomicText>
|
|
64
|
+
</View>
|
|
65
|
+
</TouchableOpacity>
|
|
66
|
+
</View>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<View style={[styles.gradientContainer, { marginTop: tokens.spacing.xl }]}>
|
|
72
|
+
<TouchableOpacity
|
|
73
|
+
onPress={onPress}
|
|
74
|
+
disabled={disabled}
|
|
75
|
+
activeOpacity={0.85}
|
|
76
|
+
style={styles.buttonWrapper}
|
|
77
|
+
>
|
|
78
|
+
<LinearGradient
|
|
79
|
+
colors={disabled ? ["#9CA3AF", "#6B7280"] : gradientColors}
|
|
80
|
+
start={[0, 0]}
|
|
81
|
+
end={[1, 0]}
|
|
82
|
+
style={[styles.gradientButton, disabled && styles.disabledButton]}
|
|
83
|
+
>
|
|
84
|
+
<View style={styles.buttonContent}>
|
|
85
|
+
<AtomicIcon name={icon} customSize={iconSize} customColor="#FFF" />
|
|
86
|
+
<AtomicText type="bodyLarge" style={styles.gradientButtonText}>
|
|
87
|
+
{displayText}
|
|
88
|
+
</AtomicText>
|
|
89
|
+
</View>
|
|
90
|
+
</LinearGradient>
|
|
91
|
+
</TouchableOpacity>
|
|
92
|
+
</View>
|
|
93
|
+
);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const styles = StyleSheet.create({
|
|
97
|
+
gradientContainer: {
|
|
98
|
+
alignItems: "center",
|
|
99
|
+
paddingHorizontal: 16,
|
|
100
|
+
},
|
|
101
|
+
solidContainer: {
|
|
102
|
+
paddingHorizontal: 16,
|
|
103
|
+
},
|
|
104
|
+
buttonWrapper: {
|
|
105
|
+
width: "100%",
|
|
106
|
+
maxWidth: 320,
|
|
107
|
+
borderRadius: 30,
|
|
108
|
+
},
|
|
109
|
+
gradientButton: {
|
|
110
|
+
paddingVertical: 18,
|
|
111
|
+
paddingHorizontal: 32,
|
|
112
|
+
borderRadius: 30,
|
|
113
|
+
alignItems: "center",
|
|
114
|
+
justifyContent: "center",
|
|
115
|
+
},
|
|
116
|
+
solidButton: {
|
|
117
|
+
borderRadius: 16,
|
|
118
|
+
paddingVertical: 16,
|
|
119
|
+
paddingHorizontal: 24,
|
|
120
|
+
alignItems: "center",
|
|
121
|
+
justifyContent: "center",
|
|
122
|
+
},
|
|
123
|
+
disabledButton: {
|
|
124
|
+
opacity: 0.5,
|
|
125
|
+
},
|
|
126
|
+
buttonContent: {
|
|
127
|
+
flexDirection: "row",
|
|
128
|
+
alignItems: "center",
|
|
129
|
+
justifyContent: "center",
|
|
130
|
+
gap: 12,
|
|
131
|
+
},
|
|
132
|
+
gradientButtonText: {
|
|
133
|
+
color: "#FFFFFF",
|
|
134
|
+
fontWeight: "700",
|
|
135
|
+
fontSize: 18,
|
|
136
|
+
},
|
|
137
|
+
solidButtonText: {
|
|
138
|
+
color: "#FFFFFF",
|
|
139
|
+
fontWeight: "600",
|
|
140
|
+
},
|
|
141
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { GenerateButton, type GenerateButtonProps } from "./GenerateButton";
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ErrorDisplay Component
|
|
3
|
+
* Generic error display with retry action
|
|
4
|
+
* Props-driven for 100+ apps compatibility
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { View, StyleSheet, TouchableOpacity } from "react-native";
|
|
9
|
+
import {
|
|
10
|
+
AtomicText,
|
|
11
|
+
useAppDesignTokens,
|
|
12
|
+
AtomicIcon,
|
|
13
|
+
} from "@umituz/react-native-design-system";
|
|
14
|
+
|
|
15
|
+
export interface ErrorDisplayProps {
|
|
16
|
+
readonly error: string | null;
|
|
17
|
+
readonly onRetry?: () => void;
|
|
18
|
+
readonly retryText?: string;
|
|
19
|
+
readonly icon?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const ErrorDisplay: React.FC<ErrorDisplayProps> = ({
|
|
23
|
+
error,
|
|
24
|
+
onRetry,
|
|
25
|
+
retryText,
|
|
26
|
+
icon = "alert-circle",
|
|
27
|
+
}) => {
|
|
28
|
+
const tokens = useAppDesignTokens();
|
|
29
|
+
|
|
30
|
+
if (!error) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<View
|
|
36
|
+
style={[
|
|
37
|
+
styles.container,
|
|
38
|
+
{
|
|
39
|
+
backgroundColor: `${tokens.colors.error}15`,
|
|
40
|
+
borderColor: `${tokens.colors.error}30`,
|
|
41
|
+
padding: tokens.spacing.md,
|
|
42
|
+
marginTop: tokens.spacing.md,
|
|
43
|
+
},
|
|
44
|
+
]}
|
|
45
|
+
>
|
|
46
|
+
<View style={styles.header}>
|
|
47
|
+
<AtomicIcon
|
|
48
|
+
name={icon}
|
|
49
|
+
customSize={20}
|
|
50
|
+
customColor={tokens.colors.error}
|
|
51
|
+
/>
|
|
52
|
+
<AtomicText
|
|
53
|
+
type="bodyMedium"
|
|
54
|
+
style={[styles.errorText, { color: tokens.colors.error }]}
|
|
55
|
+
>
|
|
56
|
+
{error}
|
|
57
|
+
</AtomicText>
|
|
58
|
+
</View>
|
|
59
|
+
{onRetry && retryText && (
|
|
60
|
+
<TouchableOpacity
|
|
61
|
+
onPress={onRetry}
|
|
62
|
+
style={[
|
|
63
|
+
styles.retryButton,
|
|
64
|
+
{
|
|
65
|
+
backgroundColor: tokens.colors.error,
|
|
66
|
+
marginTop: tokens.spacing.sm,
|
|
67
|
+
},
|
|
68
|
+
]}
|
|
69
|
+
>
|
|
70
|
+
<AtomicIcon
|
|
71
|
+
name="refresh"
|
|
72
|
+
customSize={16}
|
|
73
|
+
customColor={tokens.colors.onError}
|
|
74
|
+
/>
|
|
75
|
+
<AtomicText
|
|
76
|
+
type="bodySmall"
|
|
77
|
+
style={{ color: tokens.colors.onError, fontWeight: "600" }}
|
|
78
|
+
>
|
|
79
|
+
{retryText}
|
|
80
|
+
</AtomicText>
|
|
81
|
+
</TouchableOpacity>
|
|
82
|
+
)}
|
|
83
|
+
</View>
|
|
84
|
+
);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const styles = StyleSheet.create({
|
|
88
|
+
container: {
|
|
89
|
+
borderRadius: 12,
|
|
90
|
+
borderWidth: 1,
|
|
91
|
+
},
|
|
92
|
+
header: {
|
|
93
|
+
flexDirection: "row",
|
|
94
|
+
alignItems: "flex-start",
|
|
95
|
+
gap: 10,
|
|
96
|
+
},
|
|
97
|
+
errorText: {
|
|
98
|
+
flex: 1,
|
|
99
|
+
lineHeight: 20,
|
|
100
|
+
},
|
|
101
|
+
retryButton: {
|
|
102
|
+
flexDirection: "row",
|
|
103
|
+
alignItems: "center",
|
|
104
|
+
justifyContent: "center",
|
|
105
|
+
gap: 6,
|
|
106
|
+
paddingVertical: 10,
|
|
107
|
+
paddingHorizontal: 16,
|
|
108
|
+
borderRadius: 8,
|
|
109
|
+
alignSelf: "flex-start",
|
|
110
|
+
},
|
|
111
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ResultDisplay Component
|
|
3
|
+
* Generic result display with save/reset actions
|
|
4
|
+
* Props-driven for 100+ apps compatibility
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { View, StyleSheet } from "react-native";
|
|
9
|
+
import {
|
|
10
|
+
AtomicText,
|
|
11
|
+
AtomicButton,
|
|
12
|
+
useAppDesignTokens,
|
|
13
|
+
AtomicIcon,
|
|
14
|
+
} from "@umituz/react-native-design-system";
|
|
15
|
+
|
|
16
|
+
export interface ResultDisplayAction {
|
|
17
|
+
readonly id: string;
|
|
18
|
+
readonly label: string;
|
|
19
|
+
readonly onPress: () => void;
|
|
20
|
+
readonly variant?: "primary" | "outline";
|
|
21
|
+
readonly icon?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ResultDisplayProps {
|
|
25
|
+
readonly visible?: boolean;
|
|
26
|
+
readonly successText: string;
|
|
27
|
+
readonly actions: ResultDisplayAction[];
|
|
28
|
+
readonly successIcon?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const ResultDisplay: React.FC<ResultDisplayProps> = ({
|
|
32
|
+
visible = true,
|
|
33
|
+
successText,
|
|
34
|
+
actions,
|
|
35
|
+
successIcon = "checkmark-circle",
|
|
36
|
+
}) => {
|
|
37
|
+
const tokens = useAppDesignTokens();
|
|
38
|
+
|
|
39
|
+
if (!visible) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<View style={[styles.container, { marginTop: tokens.spacing.lg }]}>
|
|
45
|
+
<View style={styles.successHeader}>
|
|
46
|
+
<AtomicIcon
|
|
47
|
+
name={successIcon}
|
|
48
|
+
customSize={24}
|
|
49
|
+
customColor={tokens.colors.success}
|
|
50
|
+
/>
|
|
51
|
+
<AtomicText
|
|
52
|
+
type="bodyMedium"
|
|
53
|
+
style={[styles.successText, { color: tokens.colors.success }]}
|
|
54
|
+
>
|
|
55
|
+
{successText}
|
|
56
|
+
</AtomicText>
|
|
57
|
+
</View>
|
|
58
|
+
<View style={[styles.actionButtons, { gap: tokens.spacing.sm }]}>
|
|
59
|
+
{actions.map((action) => (
|
|
60
|
+
<AtomicButton
|
|
61
|
+
key={action.id}
|
|
62
|
+
onPress={action.onPress}
|
|
63
|
+
variant={action.variant === "outline" ? "outline" : undefined}
|
|
64
|
+
style={styles.actionButton}
|
|
65
|
+
>
|
|
66
|
+
<View style={styles.buttonContent}>
|
|
67
|
+
{action.icon && (
|
|
68
|
+
<AtomicIcon
|
|
69
|
+
name={action.icon}
|
|
70
|
+
customSize={18}
|
|
71
|
+
customColor={
|
|
72
|
+
action.variant === "outline"
|
|
73
|
+
? tokens.colors.primary
|
|
74
|
+
: tokens.colors.onPrimary
|
|
75
|
+
}
|
|
76
|
+
/>
|
|
77
|
+
)}
|
|
78
|
+
<AtomicText
|
|
79
|
+
type="bodyMedium"
|
|
80
|
+
style={{
|
|
81
|
+
color:
|
|
82
|
+
action.variant === "outline"
|
|
83
|
+
? tokens.colors.primary
|
|
84
|
+
: tokens.colors.onPrimary,
|
|
85
|
+
}}
|
|
86
|
+
>
|
|
87
|
+
{action.label}
|
|
88
|
+
</AtomicText>
|
|
89
|
+
</View>
|
|
90
|
+
</AtomicButton>
|
|
91
|
+
))}
|
|
92
|
+
</View>
|
|
93
|
+
</View>
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const styles = StyleSheet.create({
|
|
98
|
+
container: {
|
|
99
|
+
alignItems: "center",
|
|
100
|
+
},
|
|
101
|
+
successHeader: {
|
|
102
|
+
flexDirection: "row",
|
|
103
|
+
alignItems: "center",
|
|
104
|
+
gap: 8,
|
|
105
|
+
marginBottom: 16,
|
|
106
|
+
},
|
|
107
|
+
successText: {
|
|
108
|
+
textAlign: "center",
|
|
109
|
+
},
|
|
110
|
+
actionButtons: {
|
|
111
|
+
width: "100%",
|
|
112
|
+
maxWidth: 280,
|
|
113
|
+
},
|
|
114
|
+
actionButton: {
|
|
115
|
+
flex: 1,
|
|
116
|
+
},
|
|
117
|
+
buttonContent: {
|
|
118
|
+
flexDirection: "row",
|
|
119
|
+
alignItems: "center",
|
|
120
|
+
gap: 8,
|
|
121
|
+
},
|
|
122
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FeatureHeader Component
|
|
3
|
+
* Generic feature header with hero image and description
|
|
4
|
+
* Props-driven for 100+ apps compatibility
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { View, StyleSheet, ImageBackground } from "react-native";
|
|
9
|
+
import type { ImageSourcePropType } from "react-native";
|
|
10
|
+
import {
|
|
11
|
+
AtomicText,
|
|
12
|
+
useAppDesignTokens,
|
|
13
|
+
} from "@umituz/react-native-design-system";
|
|
14
|
+
import { LinearGradient } from "expo-linear-gradient";
|
|
15
|
+
|
|
16
|
+
export interface FeatureHeaderProps {
|
|
17
|
+
readonly imageSource: ImageSourcePropType;
|
|
18
|
+
readonly description: string;
|
|
19
|
+
readonly gradientColors?: readonly [string, string, ...string[]];
|
|
20
|
+
readonly minHeight?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const FeatureHeader: React.FC<FeatureHeaderProps> = ({
|
|
24
|
+
imageSource,
|
|
25
|
+
description,
|
|
26
|
+
gradientColors = ["rgba(0,0,0,0.3)", "rgba(0,0,0,0.1)", "rgba(0,0,0,0.4)"],
|
|
27
|
+
minHeight = 200,
|
|
28
|
+
}) => {
|
|
29
|
+
const tokens = useAppDesignTokens();
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<>
|
|
33
|
+
<View style={styles.headerContainer}>
|
|
34
|
+
<ImageBackground
|
|
35
|
+
source={imageSource}
|
|
36
|
+
style={[styles.heroImage, { minHeight }]}
|
|
37
|
+
imageStyle={styles.heroImageStyle}
|
|
38
|
+
>
|
|
39
|
+
<LinearGradient
|
|
40
|
+
colors={gradientColors}
|
|
41
|
+
style={[styles.gradient, { minHeight }]}
|
|
42
|
+
/>
|
|
43
|
+
</ImageBackground>
|
|
44
|
+
</View>
|
|
45
|
+
<AtomicText
|
|
46
|
+
type="bodyLarge"
|
|
47
|
+
style={[
|
|
48
|
+
styles.description,
|
|
49
|
+
{
|
|
50
|
+
color: tokens.colors.textSecondary,
|
|
51
|
+
marginTop: tokens.spacing.md,
|
|
52
|
+
marginBottom: tokens.spacing.md,
|
|
53
|
+
},
|
|
54
|
+
]}
|
|
55
|
+
>
|
|
56
|
+
{description}
|
|
57
|
+
</AtomicText>
|
|
58
|
+
</>
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const styles = StyleSheet.create({
|
|
63
|
+
headerContainer: {
|
|
64
|
+
marginBottom: 8,
|
|
65
|
+
borderRadius: 20,
|
|
66
|
+
overflow: "hidden",
|
|
67
|
+
borderWidth: 1,
|
|
68
|
+
borderColor: "rgba(255, 255, 255, 0.1)",
|
|
69
|
+
},
|
|
70
|
+
heroImage: {
|
|
71
|
+
width: "100%",
|
|
72
|
+
},
|
|
73
|
+
heroImageStyle: {
|
|
74
|
+
borderRadius: 20,
|
|
75
|
+
},
|
|
76
|
+
gradient: {
|
|
77
|
+
flex: 1,
|
|
78
|
+
},
|
|
79
|
+
description: {
|
|
80
|
+
textAlign: "center",
|
|
81
|
+
lineHeight: 22,
|
|
82
|
+
paddingHorizontal: 16,
|
|
83
|
+
fontWeight: "500",
|
|
84
|
+
},
|
|
85
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { FeatureHeader, type FeatureHeaderProps } from "./FeatureHeader";
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DualImagePicker Component
|
|
3
|
+
* Two-image picker for face swap, AI hug/kiss features
|
|
4
|
+
* Props-driven for 100+ apps compatibility
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { View, StyleSheet } from "react-native";
|
|
9
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
10
|
+
import { ImagePickerBox, type ImagePickerBoxProps } from "./ImagePickerBox";
|
|
11
|
+
|
|
12
|
+
export interface DualImagePickerProps {
|
|
13
|
+
readonly sourceImageUri: string | null;
|
|
14
|
+
readonly targetImageUri: string | null;
|
|
15
|
+
readonly isDisabled?: boolean;
|
|
16
|
+
readonly onSelectSource: () => void;
|
|
17
|
+
readonly onSelectTarget: () => void;
|
|
18
|
+
readonly sourcePlaceholder: string;
|
|
19
|
+
readonly targetPlaceholder: string;
|
|
20
|
+
readonly sourceGradient?: ImagePickerBoxProps["gradientColors"];
|
|
21
|
+
readonly targetGradient?: ImagePickerBoxProps["gradientColors"];
|
|
22
|
+
readonly variant?: ImagePickerBoxProps["variant"];
|
|
23
|
+
readonly layout?: "horizontal" | "vertical";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const DualImagePicker: React.FC<DualImagePickerProps> = ({
|
|
27
|
+
sourceImageUri,
|
|
28
|
+
targetImageUri,
|
|
29
|
+
isDisabled = false,
|
|
30
|
+
onSelectSource,
|
|
31
|
+
onSelectTarget,
|
|
32
|
+
sourcePlaceholder,
|
|
33
|
+
targetPlaceholder,
|
|
34
|
+
sourceGradient = ["#667eea", "#764ba2"],
|
|
35
|
+
targetGradient = ["#f093fb", "#f5576c"],
|
|
36
|
+
variant = "portrait",
|
|
37
|
+
layout = "horizontal",
|
|
38
|
+
}) => {
|
|
39
|
+
const tokens = useAppDesignTokens();
|
|
40
|
+
const isHorizontal = layout === "horizontal";
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<View
|
|
44
|
+
style={[
|
|
45
|
+
styles.container,
|
|
46
|
+
isHorizontal ? styles.horizontal : styles.vertical,
|
|
47
|
+
{ gap: tokens.spacing.md },
|
|
48
|
+
]}
|
|
49
|
+
>
|
|
50
|
+
<View style={isHorizontal ? styles.pickerHalf : styles.pickerFull}>
|
|
51
|
+
<ImagePickerBox
|
|
52
|
+
imageUri={sourceImageUri}
|
|
53
|
+
isDisabled={isDisabled}
|
|
54
|
+
onPress={onSelectSource}
|
|
55
|
+
placeholderText={sourcePlaceholder}
|
|
56
|
+
gradientColors={sourceGradient}
|
|
57
|
+
variant={variant}
|
|
58
|
+
/>
|
|
59
|
+
</View>
|
|
60
|
+
|
|
61
|
+
<View style={isHorizontal ? styles.pickerHalf : styles.pickerFull}>
|
|
62
|
+
<ImagePickerBox
|
|
63
|
+
imageUri={targetImageUri}
|
|
64
|
+
isDisabled={isDisabled}
|
|
65
|
+
onPress={onSelectTarget}
|
|
66
|
+
placeholderText={targetPlaceholder}
|
|
67
|
+
gradientColors={targetGradient}
|
|
68
|
+
variant={variant}
|
|
69
|
+
/>
|
|
70
|
+
</View>
|
|
71
|
+
</View>
|
|
72
|
+
);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const styles = StyleSheet.create({
|
|
76
|
+
container: {
|
|
77
|
+
width: "100%",
|
|
78
|
+
},
|
|
79
|
+
horizontal: {
|
|
80
|
+
flexDirection: "row",
|
|
81
|
+
justifyContent: "center",
|
|
82
|
+
},
|
|
83
|
+
vertical: {
|
|
84
|
+
flexDirection: "column",
|
|
85
|
+
alignItems: "center",
|
|
86
|
+
},
|
|
87
|
+
pickerHalf: {
|
|
88
|
+
flex: 1,
|
|
89
|
+
alignItems: "center",
|
|
90
|
+
},
|
|
91
|
+
pickerFull: {
|
|
92
|
+
width: "100%",
|
|
93
|
+
alignItems: "center",
|
|
94
|
+
},
|
|
95
|
+
});
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ImagePickerBox Component
|
|
3
|
+
* Generic image picker box with gradient design
|
|
4
|
+
* Props-driven for 100+ apps compatibility
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from "react";
|
|
8
|
+
import {
|
|
9
|
+
View,
|
|
10
|
+
StyleSheet,
|
|
11
|
+
TouchableOpacity,
|
|
12
|
+
Image,
|
|
13
|
+
type ViewStyle,
|
|
14
|
+
} from "react-native";
|
|
15
|
+
import {
|
|
16
|
+
AtomicText,
|
|
17
|
+
useAppDesignTokens,
|
|
18
|
+
AtomicIcon,
|
|
19
|
+
} from "@umituz/react-native-design-system";
|
|
20
|
+
import { LinearGradient } from "expo-linear-gradient";
|
|
21
|
+
|
|
22
|
+
export interface ImagePickerBoxProps {
|
|
23
|
+
readonly imageUri: string | null;
|
|
24
|
+
readonly isDisabled?: boolean;
|
|
25
|
+
readonly onPress: () => void;
|
|
26
|
+
readonly placeholderText: string;
|
|
27
|
+
readonly gradientColors?: readonly [string, string, ...string[]];
|
|
28
|
+
readonly variant?: "portrait" | "square" | "landscape";
|
|
29
|
+
readonly size?: "sm" | "md" | "lg";
|
|
30
|
+
readonly uploadIcon?: string;
|
|
31
|
+
readonly editIcon?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const VARIANT_STYLES: Record<string, ViewStyle> = {
|
|
35
|
+
portrait: { width: 200, height: 280, borderRadius: 20 },
|
|
36
|
+
square: { width: "100%", aspectRatio: 1, borderRadius: 24 },
|
|
37
|
+
landscape: { width: "100%", aspectRatio: 16 / 9, borderRadius: 16 },
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const SIZE_MULTIPLIERS = { sm: 0.7, md: 1, lg: 1.3 };
|
|
41
|
+
|
|
42
|
+
export const ImagePickerBox: React.FC<ImagePickerBoxProps> = ({
|
|
43
|
+
imageUri,
|
|
44
|
+
isDisabled = false,
|
|
45
|
+
onPress,
|
|
46
|
+
placeholderText,
|
|
47
|
+
gradientColors = ["#667eea", "#764ba2"],
|
|
48
|
+
variant = "portrait",
|
|
49
|
+
size = "md",
|
|
50
|
+
uploadIcon = "cloud-upload-outline",
|
|
51
|
+
editIcon = "image-outline",
|
|
52
|
+
}) => {
|
|
53
|
+
const tokens = useAppDesignTokens();
|
|
54
|
+
const multiplier = SIZE_MULTIPLIERS[size];
|
|
55
|
+
const baseStyle = VARIANT_STYLES[variant];
|
|
56
|
+
const iconSize = Math.round(32 * multiplier);
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<View style={styles.container}>
|
|
60
|
+
<TouchableOpacity
|
|
61
|
+
style={[
|
|
62
|
+
styles.box,
|
|
63
|
+
baseStyle,
|
|
64
|
+
{ backgroundColor: tokens.colors.backgroundSecondary },
|
|
65
|
+
]}
|
|
66
|
+
onPress={onPress}
|
|
67
|
+
disabled={isDisabled}
|
|
68
|
+
activeOpacity={0.8}
|
|
69
|
+
>
|
|
70
|
+
{imageUri ? (
|
|
71
|
+
<View style={styles.imageContainer}>
|
|
72
|
+
<Image source={{ uri: imageUri }} style={styles.image} />
|
|
73
|
+
<LinearGradient
|
|
74
|
+
colors={["transparent", "rgba(0,0,0,0.3)"]}
|
|
75
|
+
style={styles.imageOverlay}
|
|
76
|
+
>
|
|
77
|
+
<View
|
|
78
|
+
style={[
|
|
79
|
+
styles.editBadge,
|
|
80
|
+
{ backgroundColor: `${gradientColors[1]}E6` },
|
|
81
|
+
]}
|
|
82
|
+
>
|
|
83
|
+
<AtomicIcon
|
|
84
|
+
name={editIcon}
|
|
85
|
+
customSize={Math.round(16 * multiplier)}
|
|
86
|
+
customColor="#FFFFFF"
|
|
87
|
+
/>
|
|
88
|
+
</View>
|
|
89
|
+
</LinearGradient>
|
|
90
|
+
</View>
|
|
91
|
+
) : (
|
|
92
|
+
<LinearGradient colors={gradientColors} style={styles.placeholder}>
|
|
93
|
+
<View style={styles.placeholderContent}>
|
|
94
|
+
<View style={styles.uploadIconContainer}>
|
|
95
|
+
<AtomicIcon
|
|
96
|
+
name={uploadIcon}
|
|
97
|
+
customSize={iconSize}
|
|
98
|
+
customColor="#FFFFFF"
|
|
99
|
+
/>
|
|
100
|
+
</View>
|
|
101
|
+
<AtomicText
|
|
102
|
+
type="bodyMedium"
|
|
103
|
+
style={[styles.placeholderText, { color: "#FFFFFF" }]}
|
|
104
|
+
>
|
|
105
|
+
{placeholderText}
|
|
106
|
+
</AtomicText>
|
|
107
|
+
</View>
|
|
108
|
+
</LinearGradient>
|
|
109
|
+
)}
|
|
110
|
+
</TouchableOpacity>
|
|
111
|
+
</View>
|
|
112
|
+
);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const styles = StyleSheet.create({
|
|
116
|
+
container: {
|
|
117
|
+
marginVertical: 16,
|
|
118
|
+
alignItems: "center",
|
|
119
|
+
},
|
|
120
|
+
box: {
|
|
121
|
+
overflow: "hidden",
|
|
122
|
+
},
|
|
123
|
+
imageContainer: {
|
|
124
|
+
flex: 1,
|
|
125
|
+
position: "relative",
|
|
126
|
+
},
|
|
127
|
+
image: {
|
|
128
|
+
width: "100%",
|
|
129
|
+
height: "100%",
|
|
130
|
+
},
|
|
131
|
+
imageOverlay: {
|
|
132
|
+
position: "absolute",
|
|
133
|
+
bottom: 0,
|
|
134
|
+
left: 0,
|
|
135
|
+
right: 0,
|
|
136
|
+
height: "30%",
|
|
137
|
+
justifyContent: "flex-end",
|
|
138
|
+
alignItems: "flex-end",
|
|
139
|
+
padding: 12,
|
|
140
|
+
},
|
|
141
|
+
editBadge: {
|
|
142
|
+
borderRadius: 20,
|
|
143
|
+
padding: 8,
|
|
144
|
+
},
|
|
145
|
+
placeholder: {
|
|
146
|
+
flex: 1,
|
|
147
|
+
justifyContent: "center",
|
|
148
|
+
alignItems: "center",
|
|
149
|
+
},
|
|
150
|
+
placeholderContent: {
|
|
151
|
+
alignItems: "center",
|
|
152
|
+
justifyContent: "center",
|
|
153
|
+
paddingHorizontal: 16,
|
|
154
|
+
},
|
|
155
|
+
uploadIconContainer: {
|
|
156
|
+
backgroundColor: "rgba(255,255,255,0.2)",
|
|
157
|
+
borderRadius: 40,
|
|
158
|
+
padding: 16,
|
|
159
|
+
marginBottom: 12,
|
|
160
|
+
},
|
|
161
|
+
placeholderText: {
|
|
162
|
+
textAlign: "center",
|
|
163
|
+
fontWeight: "600",
|
|
164
|
+
},
|
|
165
|
+
});
|