@umituz/react-native-ai-generation-content 1.9.1 → 1.10.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 +5 -1
- package/src/infrastructure/utils/index.ts +1 -0
- package/src/infrastructure/utils/photo-generation/index.ts +15 -0
- package/src/infrastructure/utils/photo-generation/photo-preparation.util.ts +82 -0
- package/src/presentation/components/index.ts +2 -0
- package/src/presentation/components/result/GenerationResultContent.tsx +93 -0
- package/src/presentation/components/result/ResultActions.tsx +135 -0
- package/src/presentation/components/result/ResultHeader.tsx +71 -0
- package/src/presentation/components/result/ResultImageCard.tsx +72 -0
- package/src/presentation/components/result/ResultStoryCard.tsx +74 -0
- package/src/presentation/components/result/index.ts +21 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.1",
|
|
4
4
|
"description": "Provider-agnostic AI generation orchestration for React Native",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -31,19 +31,23 @@
|
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"@tanstack/react-query": ">=5.0.0",
|
|
34
|
+
"@umituz/react-native-animation": "latest",
|
|
34
35
|
"@umituz/react-native-design-system": "latest",
|
|
36
|
+
"expo-linear-gradient": ">=15.0.0",
|
|
35
37
|
"react": ">=18.0.0",
|
|
36
38
|
"react-native": ">=0.74.0"
|
|
37
39
|
},
|
|
38
40
|
"devDependencies": {
|
|
39
41
|
"@react-native-async-storage/async-storage": "2.2.0",
|
|
40
42
|
"@tanstack/react-query": "^5.0.0",
|
|
43
|
+
"@umituz/react-native-animation": "latest",
|
|
41
44
|
"@umituz/react-native-design-system": "latest",
|
|
42
45
|
"@types/react": "~19.1.10",
|
|
43
46
|
"@types/react-native": "^0.73.0",
|
|
44
47
|
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
|
45
48
|
"@typescript-eslint/parser": "^7.0.0",
|
|
46
49
|
"eslint": "^8.57.0",
|
|
50
|
+
"expo-linear-gradient": "~15.0.7",
|
|
47
51
|
"react": "19.1.0",
|
|
48
52
|
"react-native": "0.81.5",
|
|
49
53
|
"typescript": "^5.3.0",
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Photo Generation Utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
cleanBase64,
|
|
7
|
+
addBase64Prefix,
|
|
8
|
+
preparePhoto,
|
|
9
|
+
preparePhotos,
|
|
10
|
+
isValidBase64,
|
|
11
|
+
getBase64Size,
|
|
12
|
+
getBase64SizeMB,
|
|
13
|
+
} from "./photo-preparation.util";
|
|
14
|
+
|
|
15
|
+
export type { PhotoInput, PreparedImage } from "./photo-preparation.util";
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Photo Preparation Utilities
|
|
3
|
+
*
|
|
4
|
+
* Utilities for preparing photos for AI generation.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface PhotoInput {
|
|
8
|
+
base64: string;
|
|
9
|
+
mimeType?: string;
|
|
10
|
+
previewUrl?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface PreparedImage {
|
|
14
|
+
base64: string;
|
|
15
|
+
mimeType: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Clean base64 string by removing data URI prefix
|
|
20
|
+
*/
|
|
21
|
+
export const cleanBase64 = (base64: string): string => {
|
|
22
|
+
return base64.replace(/^data:image\/(png|jpeg|jpg|webp);base64,/, "");
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Add data URI prefix to base64 string
|
|
27
|
+
*/
|
|
28
|
+
export const addBase64Prefix = (
|
|
29
|
+
base64: string,
|
|
30
|
+
mimeType: string = "image/jpeg",
|
|
31
|
+
): string => {
|
|
32
|
+
const cleanedBase64 = cleanBase64(base64);
|
|
33
|
+
const imageType = mimeType.replace("image/", "");
|
|
34
|
+
return `data:image/${imageType};base64,${cleanedBase64}`;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Prepare single photo for API consumption
|
|
39
|
+
*/
|
|
40
|
+
export const preparePhoto = (photo: PhotoInput): PreparedImage => {
|
|
41
|
+
return {
|
|
42
|
+
base64: cleanBase64(photo.base64),
|
|
43
|
+
mimeType: photo.mimeType || "image/jpeg",
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Prepare multiple photos for API consumption
|
|
49
|
+
*/
|
|
50
|
+
export const preparePhotos = (photos: PhotoInput[]): PreparedImage[] => {
|
|
51
|
+
return photos.map(preparePhoto);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Validate photo base64 string
|
|
56
|
+
*/
|
|
57
|
+
export const isValidBase64 = (base64: string): boolean => {
|
|
58
|
+
if (!base64 || typeof base64 !== "string") return false;
|
|
59
|
+
|
|
60
|
+
const cleaned = cleanBase64(base64);
|
|
61
|
+
|
|
62
|
+
if (cleaned.length === 0) return false;
|
|
63
|
+
if (cleaned.length % 4 !== 0) return false;
|
|
64
|
+
|
|
65
|
+
const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
66
|
+
return base64Regex.test(cleaned);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get image size estimate in bytes from base64
|
|
71
|
+
*/
|
|
72
|
+
export const getBase64Size = (base64: string): number => {
|
|
73
|
+
const cleaned = cleanBase64(base64);
|
|
74
|
+
return (cleaned.length * 3) / 4;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get image size estimate in MB from base64
|
|
79
|
+
*/
|
|
80
|
+
export const getBase64SizeMB = (base64: string): number => {
|
|
81
|
+
return getBase64Size(base64) / (1024 * 1024);
|
|
82
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GenerationResultContent Component
|
|
3
|
+
* Composition of result components for CelebrationModal
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { ScrollView, StyleSheet, Dimensions } from "react-native";
|
|
8
|
+
import { Animated } from "@umituz/react-native-animation";
|
|
9
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
10
|
+
import { ResultHeader } from "./ResultHeader";
|
|
11
|
+
import { ResultImageCard } from "./ResultImageCard";
|
|
12
|
+
import { ResultStoryCard } from "./ResultStoryCard";
|
|
13
|
+
import { ResultActions } from "./ResultActions";
|
|
14
|
+
|
|
15
|
+
const { width } = Dimensions.get("window");
|
|
16
|
+
|
|
17
|
+
export interface GenerationResultData {
|
|
18
|
+
imageUrl: string;
|
|
19
|
+
story?: string;
|
|
20
|
+
title?: string;
|
|
21
|
+
date?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface GenerationResultContentProps {
|
|
25
|
+
result: GenerationResultData;
|
|
26
|
+
onShare?: () => void;
|
|
27
|
+
onSave?: () => void;
|
|
28
|
+
onRetry?: () => void;
|
|
29
|
+
isSharing?: boolean;
|
|
30
|
+
isSaving?: boolean;
|
|
31
|
+
translations: {
|
|
32
|
+
share: string;
|
|
33
|
+
sharing: string;
|
|
34
|
+
save: string;
|
|
35
|
+
retry: string;
|
|
36
|
+
aiGenerated: string;
|
|
37
|
+
};
|
|
38
|
+
modalStyle?: any;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const GenerationResultContent: React.FC<GenerationResultContentProps> = ({
|
|
42
|
+
result,
|
|
43
|
+
onShare,
|
|
44
|
+
onSave,
|
|
45
|
+
onRetry,
|
|
46
|
+
isSharing,
|
|
47
|
+
isSaving,
|
|
48
|
+
translations,
|
|
49
|
+
modalStyle,
|
|
50
|
+
}) => {
|
|
51
|
+
const tokens = useAppDesignTokens();
|
|
52
|
+
const styles = createStyles(tokens);
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<Animated.View style={[styles.container, modalStyle]}>
|
|
56
|
+
<ScrollView
|
|
57
|
+
style={styles.scrollView}
|
|
58
|
+
contentContainerStyle={styles.scrollContent}
|
|
59
|
+
showsVerticalScrollIndicator={false}
|
|
60
|
+
>
|
|
61
|
+
<ResultHeader title={result.title} date={result.date} />
|
|
62
|
+
<ResultImageCard imageUrl={result.imageUrl} badgeText={translations.aiGenerated} />
|
|
63
|
+
{result.story && <ResultStoryCard story={result.story} />}
|
|
64
|
+
<ResultActions
|
|
65
|
+
onShare={onShare}
|
|
66
|
+
onSave={onSave}
|
|
67
|
+
onRetry={onRetry}
|
|
68
|
+
isSharing={isSharing}
|
|
69
|
+
isSaving={isSaving}
|
|
70
|
+
translations={translations}
|
|
71
|
+
/>
|
|
72
|
+
</ScrollView>
|
|
73
|
+
</Animated.View>
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const createStyles = (tokens: any) =>
|
|
78
|
+
StyleSheet.create({
|
|
79
|
+
container: {
|
|
80
|
+
width: width - 40,
|
|
81
|
+
maxHeight: "90%",
|
|
82
|
+
backgroundColor: tokens.colors.backgroundPrimary,
|
|
83
|
+
borderRadius: 28,
|
|
84
|
+
overflow: "hidden",
|
|
85
|
+
},
|
|
86
|
+
scrollView: {
|
|
87
|
+
flex: 1,
|
|
88
|
+
},
|
|
89
|
+
scrollContent: {
|
|
90
|
+
paddingTop: 24,
|
|
91
|
+
paddingBottom: 20,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ResultActions Component
|
|
3
|
+
* Action buttons for generation results
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, TouchableOpacity, StyleSheet } from "react-native";
|
|
8
|
+
import {
|
|
9
|
+
AtomicText,
|
|
10
|
+
AtomicIcon,
|
|
11
|
+
useAppDesignTokens,
|
|
12
|
+
} from "@umituz/react-native-design-system";
|
|
13
|
+
|
|
14
|
+
export interface ResultActionsProps {
|
|
15
|
+
onShare?: () => void;
|
|
16
|
+
onSave?: () => void;
|
|
17
|
+
onRetry?: () => void;
|
|
18
|
+
isSharing?: boolean;
|
|
19
|
+
isSaving?: boolean;
|
|
20
|
+
translations: {
|
|
21
|
+
share: string;
|
|
22
|
+
sharing: string;
|
|
23
|
+
save: string;
|
|
24
|
+
retry: string;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const ResultActions: React.FC<ResultActionsProps> = ({
|
|
29
|
+
onShare,
|
|
30
|
+
onSave,
|
|
31
|
+
onRetry,
|
|
32
|
+
isSharing = false,
|
|
33
|
+
isSaving = false,
|
|
34
|
+
translations,
|
|
35
|
+
}) => {
|
|
36
|
+
const tokens = useAppDesignTokens();
|
|
37
|
+
const styles = createStyles(tokens);
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<View style={styles.container}>
|
|
41
|
+
{onRetry && (
|
|
42
|
+
<TouchableOpacity style={styles.retryButton} onPress={onRetry}>
|
|
43
|
+
<AtomicIcon name="refresh" size={18} customColor={tokens.colors.primary} />
|
|
44
|
+
<AtomicText style={styles.retryText}>{translations.retry}</AtomicText>
|
|
45
|
+
</TouchableOpacity>
|
|
46
|
+
)}
|
|
47
|
+
|
|
48
|
+
<View style={styles.buttons}>
|
|
49
|
+
{onShare && (
|
|
50
|
+
<TouchableOpacity
|
|
51
|
+
style={[styles.button, styles.shareButton]}
|
|
52
|
+
onPress={onShare}
|
|
53
|
+
disabled={isSharing}
|
|
54
|
+
>
|
|
55
|
+
<AtomicIcon
|
|
56
|
+
name={isSharing ? "hourglass" : "share-social"}
|
|
57
|
+
size={22}
|
|
58
|
+
customColor="#fff"
|
|
59
|
+
/>
|
|
60
|
+
<AtomicText style={styles.shareText}>
|
|
61
|
+
{isSharing ? translations.sharing : translations.share}
|
|
62
|
+
</AtomicText>
|
|
63
|
+
</TouchableOpacity>
|
|
64
|
+
)}
|
|
65
|
+
|
|
66
|
+
{onSave && (
|
|
67
|
+
<TouchableOpacity
|
|
68
|
+
style={[styles.button, styles.saveButton]}
|
|
69
|
+
onPress={onSave}
|
|
70
|
+
disabled={isSaving}
|
|
71
|
+
>
|
|
72
|
+
<AtomicIcon
|
|
73
|
+
name={isSaving ? "hourglass" : "download"}
|
|
74
|
+
size={22}
|
|
75
|
+
customColor={tokens.colors.primary}
|
|
76
|
+
/>
|
|
77
|
+
<AtomicText style={styles.saveText}>{translations.save}</AtomicText>
|
|
78
|
+
</TouchableOpacity>
|
|
79
|
+
)}
|
|
80
|
+
</View>
|
|
81
|
+
</View>
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const createStyles = (tokens: any) =>
|
|
86
|
+
StyleSheet.create({
|
|
87
|
+
container: {
|
|
88
|
+
paddingHorizontal: 20,
|
|
89
|
+
paddingBottom: 20,
|
|
90
|
+
},
|
|
91
|
+
retryButton: {
|
|
92
|
+
flexDirection: "row",
|
|
93
|
+
alignItems: "center",
|
|
94
|
+
justifyContent: "center",
|
|
95
|
+
gap: 6,
|
|
96
|
+
paddingVertical: 12,
|
|
97
|
+
marginBottom: 16,
|
|
98
|
+
},
|
|
99
|
+
retryText: {
|
|
100
|
+
fontSize: 14,
|
|
101
|
+
fontWeight: "600",
|
|
102
|
+
color: tokens.colors.primary,
|
|
103
|
+
},
|
|
104
|
+
buttons: {
|
|
105
|
+
flexDirection: "row",
|
|
106
|
+
gap: 10,
|
|
107
|
+
},
|
|
108
|
+
button: {
|
|
109
|
+
flex: 1,
|
|
110
|
+
flexDirection: "row",
|
|
111
|
+
alignItems: "center",
|
|
112
|
+
justifyContent: "center",
|
|
113
|
+
gap: 8,
|
|
114
|
+
paddingVertical: 14,
|
|
115
|
+
borderRadius: 14,
|
|
116
|
+
},
|
|
117
|
+
shareButton: {
|
|
118
|
+
backgroundColor: tokens.colors.primary,
|
|
119
|
+
},
|
|
120
|
+
shareText: {
|
|
121
|
+
fontSize: 15,
|
|
122
|
+
fontWeight: "700",
|
|
123
|
+
color: "#fff",
|
|
124
|
+
},
|
|
125
|
+
saveButton: {
|
|
126
|
+
backgroundColor: tokens.colors.surface,
|
|
127
|
+
borderWidth: 2,
|
|
128
|
+
borderColor: tokens.colors.primary,
|
|
129
|
+
},
|
|
130
|
+
saveText: {
|
|
131
|
+
fontSize: 15,
|
|
132
|
+
fontWeight: "700",
|
|
133
|
+
color: tokens.colors.primary,
|
|
134
|
+
},
|
|
135
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ResultHeader Component
|
|
3
|
+
* Header with title and date badge
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, StyleSheet } from "react-native";
|
|
8
|
+
import {
|
|
9
|
+
AtomicText,
|
|
10
|
+
AtomicIcon,
|
|
11
|
+
useAppDesignTokens,
|
|
12
|
+
} from "@umituz/react-native-design-system";
|
|
13
|
+
|
|
14
|
+
export interface ResultHeaderProps {
|
|
15
|
+
title?: string;
|
|
16
|
+
date?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const ResultHeader: React.FC<ResultHeaderProps> = ({ title, date }) => {
|
|
20
|
+
const tokens = useAppDesignTokens();
|
|
21
|
+
const styles = createStyles(tokens);
|
|
22
|
+
|
|
23
|
+
if (!title && !date) return null;
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<View style={styles.container}>
|
|
27
|
+
{title && <AtomicText style={styles.title}>{title}</AtomicText>}
|
|
28
|
+
{date && (
|
|
29
|
+
<View style={styles.badge}>
|
|
30
|
+
<AtomicIcon
|
|
31
|
+
name="calendar-outline"
|
|
32
|
+
size={14}
|
|
33
|
+
customColor={tokens.colors.primary}
|
|
34
|
+
/>
|
|
35
|
+
<AtomicText style={styles.dateText}>{date}</AtomicText>
|
|
36
|
+
</View>
|
|
37
|
+
)}
|
|
38
|
+
</View>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const createStyles = (tokens: any) =>
|
|
43
|
+
StyleSheet.create({
|
|
44
|
+
container: {
|
|
45
|
+
alignItems: "center",
|
|
46
|
+
paddingHorizontal: 24,
|
|
47
|
+
marginBottom: 20,
|
|
48
|
+
},
|
|
49
|
+
title: {
|
|
50
|
+
fontSize: 24,
|
|
51
|
+
lineHeight: 32,
|
|
52
|
+
fontWeight: "800",
|
|
53
|
+
color: tokens.colors.textPrimary,
|
|
54
|
+
textAlign: "center",
|
|
55
|
+
marginBottom: 12,
|
|
56
|
+
},
|
|
57
|
+
badge: {
|
|
58
|
+
flexDirection: "row",
|
|
59
|
+
alignItems: "center",
|
|
60
|
+
gap: 6,
|
|
61
|
+
paddingHorizontal: 14,
|
|
62
|
+
paddingVertical: 6,
|
|
63
|
+
backgroundColor: `${tokens.colors.primary}15`,
|
|
64
|
+
borderRadius: 16,
|
|
65
|
+
},
|
|
66
|
+
dateText: {
|
|
67
|
+
fontSize: 12,
|
|
68
|
+
fontWeight: "600",
|
|
69
|
+
color: tokens.colors.primary,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ResultImageCard Component
|
|
3
|
+
* Displays generated image with AI badge
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, Image, StyleSheet } from "react-native";
|
|
8
|
+
import {
|
|
9
|
+
AtomicText,
|
|
10
|
+
AtomicIcon,
|
|
11
|
+
useAppDesignTokens,
|
|
12
|
+
} from "@umituz/react-native-design-system";
|
|
13
|
+
|
|
14
|
+
export interface ResultImageCardProps {
|
|
15
|
+
imageUrl: string;
|
|
16
|
+
badgeText: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const ResultImageCard: React.FC<ResultImageCardProps> = ({
|
|
20
|
+
imageUrl,
|
|
21
|
+
badgeText,
|
|
22
|
+
}) => {
|
|
23
|
+
const tokens = useAppDesignTokens();
|
|
24
|
+
const styles = createStyles(tokens);
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<View style={styles.container}>
|
|
28
|
+
<View style={styles.frame}>
|
|
29
|
+
<Image source={{ uri: imageUrl }} style={styles.image} resizeMode="cover" />
|
|
30
|
+
<View style={styles.badge}>
|
|
31
|
+
<AtomicIcon name="sparkles" size={12} customColor="#fff" />
|
|
32
|
+
<AtomicText style={styles.badgeText}>{badgeText}</AtomicText>
|
|
33
|
+
</View>
|
|
34
|
+
</View>
|
|
35
|
+
</View>
|
|
36
|
+
);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const createStyles = (tokens: any) =>
|
|
40
|
+
StyleSheet.create({
|
|
41
|
+
container: {
|
|
42
|
+
paddingHorizontal: 20,
|
|
43
|
+
marginBottom: 20,
|
|
44
|
+
},
|
|
45
|
+
frame: {
|
|
46
|
+
borderRadius: 20,
|
|
47
|
+
overflow: "hidden",
|
|
48
|
+
backgroundColor: tokens.colors.surface,
|
|
49
|
+
},
|
|
50
|
+
image: {
|
|
51
|
+
width: "100%",
|
|
52
|
+
aspectRatio: 1,
|
|
53
|
+
},
|
|
54
|
+
badge: {
|
|
55
|
+
position: "absolute",
|
|
56
|
+
top: 12,
|
|
57
|
+
right: 12,
|
|
58
|
+
flexDirection: "row",
|
|
59
|
+
alignItems: "center",
|
|
60
|
+
gap: 4,
|
|
61
|
+
paddingHorizontal: 10,
|
|
62
|
+
paddingVertical: 5,
|
|
63
|
+
backgroundColor: "rgba(0,0,0,0.6)",
|
|
64
|
+
borderRadius: 12,
|
|
65
|
+
},
|
|
66
|
+
badgeText: {
|
|
67
|
+
fontSize: 10,
|
|
68
|
+
fontWeight: "700",
|
|
69
|
+
color: "#fff",
|
|
70
|
+
letterSpacing: 0.5,
|
|
71
|
+
},
|
|
72
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ResultStoryCard Component
|
|
3
|
+
* Displays story text with quote styling
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, StyleSheet } from "react-native";
|
|
8
|
+
import {
|
|
9
|
+
AtomicText,
|
|
10
|
+
useAppDesignTokens,
|
|
11
|
+
} from "@umituz/react-native-design-system";
|
|
12
|
+
import { LinearGradient } from "expo-linear-gradient";
|
|
13
|
+
|
|
14
|
+
export interface ResultStoryCardProps {
|
|
15
|
+
story: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const ResultStoryCard: React.FC<ResultStoryCardProps> = ({ story }) => {
|
|
19
|
+
const tokens = useAppDesignTokens();
|
|
20
|
+
const styles = createStyles(tokens);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<View style={styles.outer}>
|
|
24
|
+
<LinearGradient
|
|
25
|
+
colors={[`${tokens.colors.primary}15`, `${tokens.colors.primary}05`]}
|
|
26
|
+
style={styles.container}
|
|
27
|
+
>
|
|
28
|
+
<AtomicText style={styles.quoteIcon}>"</AtomicText>
|
|
29
|
+
<AtomicText style={styles.text}>{story}</AtomicText>
|
|
30
|
+
<View style={styles.quoteEnd}>
|
|
31
|
+
<AtomicText style={[styles.quoteIcon, styles.quoteIconEnd]}>
|
|
32
|
+
"
|
|
33
|
+
</AtomicText>
|
|
34
|
+
</View>
|
|
35
|
+
</LinearGradient>
|
|
36
|
+
</View>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const createStyles = (tokens: any) =>
|
|
41
|
+
StyleSheet.create({
|
|
42
|
+
outer: {
|
|
43
|
+
paddingHorizontal: 20,
|
|
44
|
+
marginBottom: 20,
|
|
45
|
+
},
|
|
46
|
+
container: {
|
|
47
|
+
padding: 20,
|
|
48
|
+
borderRadius: 16,
|
|
49
|
+
borderWidth: 1,
|
|
50
|
+
borderColor: `${tokens.colors.primary}20`,
|
|
51
|
+
},
|
|
52
|
+
quoteIcon: {
|
|
53
|
+
fontSize: 40,
|
|
54
|
+
lineHeight: 40,
|
|
55
|
+
color: tokens.colors.primary,
|
|
56
|
+
opacity: 0.4,
|
|
57
|
+
marginBottom: -12,
|
|
58
|
+
},
|
|
59
|
+
quoteEnd: {
|
|
60
|
+
alignItems: "flex-end",
|
|
61
|
+
marginTop: -12,
|
|
62
|
+
},
|
|
63
|
+
quoteIconEnd: {
|
|
64
|
+
marginBottom: 0,
|
|
65
|
+
},
|
|
66
|
+
text: {
|
|
67
|
+
fontSize: 14,
|
|
68
|
+
color: tokens.colors.textPrimary,
|
|
69
|
+
textAlign: "center",
|
|
70
|
+
lineHeight: 22,
|
|
71
|
+
fontStyle: "italic",
|
|
72
|
+
fontWeight: "500",
|
|
73
|
+
},
|
|
74
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Result Components
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { GenerationResultContent } from "./GenerationResultContent";
|
|
6
|
+
export type {
|
|
7
|
+
GenerationResultData,
|
|
8
|
+
GenerationResultContentProps,
|
|
9
|
+
} from "./GenerationResultContent";
|
|
10
|
+
|
|
11
|
+
export { ResultHeader } from "./ResultHeader";
|
|
12
|
+
export type { ResultHeaderProps } from "./ResultHeader";
|
|
13
|
+
|
|
14
|
+
export { ResultImageCard } from "./ResultImageCard";
|
|
15
|
+
export type { ResultImageCardProps } from "./ResultImageCard";
|
|
16
|
+
|
|
17
|
+
export { ResultStoryCard } from "./ResultStoryCard";
|
|
18
|
+
export type { ResultStoryCardProps } from "./ResultStoryCard";
|
|
19
|
+
|
|
20
|
+
export { ResultActions } from "./ResultActions";
|
|
21
|
+
export type { ResultActionsProps } from "./ResultActions";
|