@umituz/react-native-ai-generation-content 1.13.0 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/domains/creations/presentation/components/CreationCard.tsx +2 -3
- package/src/domains/creations/presentation/components/CreationsGrid.tsx +0 -1
- package/src/domains/creations/presentation/components/CreationsProvider.tsx +56 -0
- package/src/domains/creations/presentation/components/index.ts +1 -0
- package/src/domains/creations/presentation/screens/CreationsGalleryScreen.tsx +17 -22
- package/src/index.ts +8 -0
- package/src/presentation/components/result/GenerationResultContent.tsx +60 -23
- package/src/presentation/components/result/ResultActions.tsx +171 -84
- package/src/presentation/components/result/ResultHeader.tsx +69 -38
- package/src/presentation/components/result/ResultImageCard.tsx +118 -39
- package/src/presentation/components/result/ResultStoryCard.tsx +94 -44
- package/src/presentation/components/result/index.ts +14 -0
- package/src/presentation/types/result-config.types.ts +194 -0
package/package.json
CHANGED
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
} from "@umituz/react-native-design-system";
|
|
8
8
|
import { timezoneService } from "@umituz/react-native-timezone";
|
|
9
9
|
import type { Creation } from "../../domain/entities/Creation";
|
|
10
|
-
import type { CreationType } from "../../domain/value-objects/CreationsConfig";
|
|
11
10
|
|
|
12
11
|
import { useCreationsProvider } from "./CreationsProvider";
|
|
13
12
|
|
|
@@ -29,9 +28,9 @@ export function CreationCard({
|
|
|
29
28
|
locale = "en-US",
|
|
30
29
|
}: CreationCardProps) {
|
|
31
30
|
const tokens = useAppDesignTokens();
|
|
32
|
-
const { translatedTypes
|
|
31
|
+
const { translatedTypes } = useCreationsProvider();
|
|
33
32
|
|
|
34
|
-
const typeConfig = translatedTypes.find((
|
|
33
|
+
const typeConfig = translatedTypes.find((type) => type.id === creation.type);
|
|
35
34
|
const icon = typeConfig?.icon;
|
|
36
35
|
// Use manual name if available, otherwise use translated label from config
|
|
37
36
|
const label = (creation.metadata?.names as string) || typeConfig?.labelKey || creation.type;
|
|
@@ -2,7 +2,6 @@ import React from 'react';
|
|
|
2
2
|
import { FlatList, RefreshControl, StyleSheet, type ViewStyle } from 'react-native';
|
|
3
3
|
import { useAppDesignTokens, type DesignTokens } from "@umituz/react-native-design-system";
|
|
4
4
|
import type { Creation } from "../../domain/entities/Creation";
|
|
5
|
-
import type { CreationType } from "../../domain/value-objects/CreationsConfig";
|
|
6
5
|
import { CreationCard } from "./CreationCard";
|
|
7
6
|
|
|
8
7
|
interface CreationsGridProps {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creations Provider
|
|
3
|
+
* Context provider for creations configuration and localization
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { createContext, useContext, useMemo, type ReactNode } from "react";
|
|
7
|
+
import type { CreationsConfig, CreationType } from "../../domain/value-objects/CreationsConfig";
|
|
8
|
+
import { getTranslatedTypes } from "../utils/filterUtils";
|
|
9
|
+
|
|
10
|
+
interface CreationsContextValue {
|
|
11
|
+
config: CreationsConfig;
|
|
12
|
+
t: (key: string) => string;
|
|
13
|
+
translatedTypes: readonly CreationType[];
|
|
14
|
+
getLocalizedTitle: (typeId: string) => string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const CreationsContext = createContext<CreationsContextValue | null>(null);
|
|
18
|
+
|
|
19
|
+
interface CreationsProviderProps {
|
|
20
|
+
config: CreationsConfig;
|
|
21
|
+
t: (key: string) => string;
|
|
22
|
+
children: ReactNode;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function CreationsProvider({ config, t, children }: CreationsProviderProps) {
|
|
26
|
+
const translatedTypes = useMemo(() => getTranslatedTypes(config, t), [config, t]);
|
|
27
|
+
|
|
28
|
+
const getLocalizedTitle = (typeId: string): string => {
|
|
29
|
+
const typeConfig = translatedTypes.find((type) => type.id === typeId);
|
|
30
|
+
return typeConfig?.labelKey || typeId;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const value = useMemo<CreationsContextValue>(
|
|
34
|
+
() => ({
|
|
35
|
+
config,
|
|
36
|
+
t,
|
|
37
|
+
translatedTypes,
|
|
38
|
+
getLocalizedTitle,
|
|
39
|
+
}),
|
|
40
|
+
[config, t, translatedTypes]
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<CreationsContext.Provider value={value}>
|
|
45
|
+
{children}
|
|
46
|
+
</CreationsContext.Provider>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function useCreationsProvider(): CreationsContextValue {
|
|
51
|
+
const context = useContext(CreationsContext);
|
|
52
|
+
if (!context) {
|
|
53
|
+
throw new Error("useCreationsProvider must be used within a CreationsProvider");
|
|
54
|
+
}
|
|
55
|
+
return context;
|
|
56
|
+
}
|
|
@@ -11,6 +11,7 @@ export { CreationCard } from "./CreationCard";
|
|
|
11
11
|
export { CreationThumbnail } from "./CreationThumbnail";
|
|
12
12
|
export { CreationImageViewer } from "./CreationImageViewer";
|
|
13
13
|
export { CreationsGrid } from "./CreationsGrid";
|
|
14
|
+
export { CreationsProvider, useCreationsProvider } from "./CreationsProvider";
|
|
14
15
|
|
|
15
16
|
// Detail Components
|
|
16
17
|
export { DetailHeader } from "./CreationDetail/DetailHeader";
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
declare const __DEV__: boolean;
|
|
2
2
|
|
|
3
3
|
import React, { useMemo, useCallback, useState } from "react";
|
|
4
|
-
import { View, StyleSheet, type LayoutChangeEvent } from "react-native";
|
|
5
4
|
import {
|
|
6
5
|
useAppDesignTokens,
|
|
7
6
|
useAlert,
|
|
@@ -9,17 +8,20 @@ import {
|
|
|
9
8
|
AlertMode,
|
|
10
9
|
useSharing,
|
|
11
10
|
FilterBottomSheet,
|
|
12
|
-
type DesignTokens,
|
|
13
11
|
type BottomSheetModalRef,
|
|
14
|
-
ScreenLayout
|
|
12
|
+
ScreenLayout,
|
|
15
13
|
} from "@umituz/react-native-design-system";
|
|
16
|
-
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
17
14
|
import { useFocusEffect } from "@react-navigation/native";
|
|
18
15
|
import { useCreations } from "../hooks/useCreations";
|
|
19
16
|
import { useDeleteCreation } from "../hooks/useDeleteCreation";
|
|
20
17
|
import { useCreationsFilter } from "../hooks/useCreationsFilter";
|
|
21
|
-
import {
|
|
22
|
-
|
|
18
|
+
import {
|
|
19
|
+
GalleryHeader,
|
|
20
|
+
CreationsGrid,
|
|
21
|
+
CreationImageViewer,
|
|
22
|
+
GalleryEmptyStates,
|
|
23
|
+
} from "../components";
|
|
24
|
+
import { getFilterCategoriesFromConfig } from "../utils/filterUtils";
|
|
23
25
|
import type { Creation } from "../../domain/entities/Creation";
|
|
24
26
|
import type { CreationsConfig } from "../../domain/value-objects/CreationsConfig";
|
|
25
27
|
import type { ICreationsRepository } from "../../domain/repositories/ICreationsRepository";
|
|
@@ -61,7 +63,6 @@ function CreationsGalleryScreenContent({
|
|
|
61
63
|
showFilter = config.showFilter ?? true,
|
|
62
64
|
}: CreationsGalleryScreenProps) {
|
|
63
65
|
const tokens = useAppDesignTokens();
|
|
64
|
-
const insets = useSafeAreaInsets();
|
|
65
66
|
const { share } = useSharing();
|
|
66
67
|
const alert = useAlert();
|
|
67
68
|
|
|
@@ -83,8 +84,10 @@ function CreationsGalleryScreenContent({
|
|
|
83
84
|
);
|
|
84
85
|
|
|
85
86
|
// Prepare data for UI using utils
|
|
86
|
-
const
|
|
87
|
-
|
|
87
|
+
const allCategories = useMemo(
|
|
88
|
+
() => getFilterCategoriesFromConfig(config, t),
|
|
89
|
+
[config, t],
|
|
90
|
+
);
|
|
88
91
|
|
|
89
92
|
const handleShare = useCallback((creation: Creation) => {
|
|
90
93
|
void share(creation.uri, { dialogTitle: t("common.share") });
|
|
@@ -124,15 +127,17 @@ function CreationsGalleryScreenContent({
|
|
|
124
127
|
const handleFavorite = useCallback((creation: Creation, isFavorite: boolean) => {
|
|
125
128
|
void (async () => {
|
|
126
129
|
if (!userId) return;
|
|
127
|
-
const success = await repository.updateFavorite(
|
|
130
|
+
const success = await repository.updateFavorite(
|
|
131
|
+
userId,
|
|
132
|
+
creation.id,
|
|
133
|
+
isFavorite,
|
|
134
|
+
);
|
|
128
135
|
if (success) {
|
|
129
136
|
void refetch();
|
|
130
137
|
}
|
|
131
138
|
})();
|
|
132
139
|
}, [userId, repository, refetch]);
|
|
133
140
|
|
|
134
|
-
const styles = useStyles(tokens);
|
|
135
|
-
|
|
136
141
|
const renderEmptyComponent = useMemo(() => (
|
|
137
142
|
<GalleryEmptyStates
|
|
138
143
|
isLoading={isLoading}
|
|
@@ -228,13 +233,3 @@ function CreationsGalleryScreenContent({
|
|
|
228
233
|
);
|
|
229
234
|
}
|
|
230
235
|
|
|
231
|
-
const useStyles = (tokens: DesignTokens) => StyleSheet.create({
|
|
232
|
-
container: { flex: 1, backgroundColor: tokens.colors.background },
|
|
233
|
-
centerContainer: {
|
|
234
|
-
flex: 1,
|
|
235
|
-
justifyContent: 'center',
|
|
236
|
-
alignItems: 'center',
|
|
237
|
-
minHeight: 400,
|
|
238
|
-
paddingHorizontal: tokens.spacing.xl
|
|
239
|
-
},
|
|
240
|
-
});
|
package/src/index.ts
CHANGED
|
@@ -221,6 +221,7 @@ export {
|
|
|
221
221
|
ResultImageCard,
|
|
222
222
|
ResultStoryCard,
|
|
223
223
|
ResultActions,
|
|
224
|
+
DEFAULT_RESULT_CONFIG,
|
|
224
225
|
} from "./presentation/components";
|
|
225
226
|
|
|
226
227
|
export type {
|
|
@@ -238,6 +239,13 @@ export type {
|
|
|
238
239
|
ResultImageCardProps,
|
|
239
240
|
ResultStoryCardProps,
|
|
240
241
|
ResultActionsProps,
|
|
242
|
+
ResultConfig,
|
|
243
|
+
ResultHeaderConfig,
|
|
244
|
+
ResultImageConfig,
|
|
245
|
+
ResultStoryConfig,
|
|
246
|
+
ResultActionsConfig,
|
|
247
|
+
ResultLayoutConfig,
|
|
248
|
+
ResultActionButton,
|
|
241
249
|
} from "./presentation/components";
|
|
242
250
|
|
|
243
251
|
// =============================================================================
|
|
@@ -1,16 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* GenerationResultContent Component
|
|
3
|
-
* Composition of result components for CelebrationModal
|
|
3
|
+
* Composition of result components for CelebrationModal - fully configurable
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import * as React from "react";
|
|
7
7
|
import { useMemo } from "react";
|
|
8
|
-
import {
|
|
9
|
-
|
|
8
|
+
import {
|
|
9
|
+
ScrollView,
|
|
10
|
+
StyleSheet,
|
|
11
|
+
Dimensions,
|
|
12
|
+
type ViewStyle,
|
|
13
|
+
type StyleProp,
|
|
14
|
+
type DimensionValue,
|
|
15
|
+
} from "react-native";
|
|
16
|
+
import {
|
|
17
|
+
Animated,
|
|
18
|
+
useAppDesignTokens,
|
|
19
|
+
} from "@umituz/react-native-design-system";
|
|
10
20
|
import { ResultHeader } from "./ResultHeader";
|
|
11
21
|
import { ResultImageCard } from "./ResultImageCard";
|
|
12
22
|
import { ResultStoryCard } from "./ResultStoryCard";
|
|
13
23
|
import { ResultActions } from "./ResultActions";
|
|
24
|
+
import type { ResultConfig } from "../../types/result-config.types";
|
|
25
|
+
import { DEFAULT_RESULT_CONFIG } from "../../types/result-config.types";
|
|
14
26
|
|
|
15
27
|
const { width } = Dimensions.get("window");
|
|
16
28
|
|
|
@@ -36,9 +48,12 @@ export interface GenerationResultContentProps {
|
|
|
36
48
|
aiGenerated: string;
|
|
37
49
|
};
|
|
38
50
|
modalStyle?: StyleProp<ViewStyle>;
|
|
51
|
+
config?: ResultConfig;
|
|
39
52
|
}
|
|
40
53
|
|
|
41
|
-
export const GenerationResultContent: React.FC<
|
|
54
|
+
export const GenerationResultContent: React.FC<
|
|
55
|
+
GenerationResultContentProps
|
|
56
|
+
> = ({
|
|
42
57
|
result,
|
|
43
58
|
onShare,
|
|
44
59
|
onSave,
|
|
@@ -47,25 +62,35 @@ export const GenerationResultContent: React.FC<GenerationResultContentProps> = (
|
|
|
47
62
|
isSaving,
|
|
48
63
|
translations,
|
|
49
64
|
modalStyle,
|
|
65
|
+
config = DEFAULT_RESULT_CONFIG,
|
|
50
66
|
}) => {
|
|
51
67
|
const tokens = useAppDesignTokens();
|
|
68
|
+
const cfg = { ...DEFAULT_RESULT_CONFIG, ...config };
|
|
52
69
|
|
|
53
|
-
const styles = useMemo(() =>
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
70
|
+
const styles = useMemo(() => {
|
|
71
|
+
const containerWidth = cfg.layout?.maxWidth ?? width - 40;
|
|
72
|
+
const maxHeight: DimensionValue = (cfg.layout?.maxHeight ??
|
|
73
|
+
"90%") as DimensionValue;
|
|
74
|
+
|
|
75
|
+
return StyleSheet.create({
|
|
76
|
+
container: {
|
|
77
|
+
width: containerWidth,
|
|
78
|
+
maxHeight,
|
|
79
|
+
backgroundColor:
|
|
80
|
+
cfg.layout?.backgroundColor ?? tokens.colors.background,
|
|
81
|
+
borderRadius: cfg.layout?.borderRadius ?? 28,
|
|
82
|
+
overflow: "hidden",
|
|
83
|
+
},
|
|
84
|
+
scrollView: {
|
|
85
|
+
flex: 1,
|
|
86
|
+
},
|
|
87
|
+
scrollContent: {
|
|
88
|
+
paddingTop: cfg.layout?.contentPadding?.top ?? 24,
|
|
89
|
+
paddingBottom: cfg.layout?.contentPadding?.bottom ?? 20,
|
|
90
|
+
paddingHorizontal: cfg.layout?.contentPadding?.horizontal ?? 0,
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
}, [tokens, cfg, width]);
|
|
69
94
|
|
|
70
95
|
return (
|
|
71
96
|
<Animated.View style={[styles.container, modalStyle]}>
|
|
@@ -73,10 +98,21 @@ export const GenerationResultContent: React.FC<GenerationResultContentProps> = (
|
|
|
73
98
|
style={styles.scrollView}
|
|
74
99
|
contentContainerStyle={styles.scrollContent}
|
|
75
100
|
showsVerticalScrollIndicator={false}
|
|
101
|
+
scrollEnabled={cfg.layout?.scrollEnabled ?? true}
|
|
76
102
|
>
|
|
77
|
-
<ResultHeader
|
|
78
|
-
|
|
79
|
-
|
|
103
|
+
<ResultHeader
|
|
104
|
+
title={result.title}
|
|
105
|
+
date={result.date}
|
|
106
|
+
config={cfg.header}
|
|
107
|
+
/>
|
|
108
|
+
<ResultImageCard
|
|
109
|
+
imageUrl={result.imageUrl}
|
|
110
|
+
badgeText={translations.aiGenerated}
|
|
111
|
+
config={cfg.image}
|
|
112
|
+
/>
|
|
113
|
+
{result.story && (
|
|
114
|
+
<ResultStoryCard story={result.story} config={cfg.story} />
|
|
115
|
+
)}
|
|
80
116
|
<ResultActions
|
|
81
117
|
onShare={onShare}
|
|
82
118
|
onSave={onSave}
|
|
@@ -84,6 +120,7 @@ export const GenerationResultContent: React.FC<GenerationResultContentProps> = (
|
|
|
84
120
|
isSharing={isSharing}
|
|
85
121
|
isSaving={isSaving}
|
|
86
122
|
translations={translations}
|
|
123
|
+
config={cfg.actions}
|
|
87
124
|
/>
|
|
88
125
|
</ScrollView>
|
|
89
126
|
</Animated.View>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ResultActions Component
|
|
3
|
-
* Action buttons for generation results
|
|
3
|
+
* Action buttons for generation results - fully configurable
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import * as React from "react";
|
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
AtomicIcon,
|
|
12
12
|
useAppDesignTokens,
|
|
13
13
|
} from "@umituz/react-native-design-system";
|
|
14
|
+
import type { ResultActionsConfig } from "../../types/result-config.types";
|
|
15
|
+
import { DEFAULT_RESULT_CONFIG } from "../../types/result-config.types";
|
|
14
16
|
|
|
15
17
|
export interface ResultActionsProps {
|
|
16
18
|
onShare?: () => void;
|
|
@@ -24,6 +26,7 @@ export interface ResultActionsProps {
|
|
|
24
26
|
save: string;
|
|
25
27
|
retry: string;
|
|
26
28
|
};
|
|
29
|
+
config?: ResultActionsConfig;
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
export const ResultActions: React.FC<ResultActionsProps> = ({
|
|
@@ -33,102 +36,186 @@ export const ResultActions: React.FC<ResultActionsProps> = ({
|
|
|
33
36
|
isSharing = false,
|
|
34
37
|
isSaving = false,
|
|
35
38
|
translations,
|
|
39
|
+
config = DEFAULT_RESULT_CONFIG.actions,
|
|
36
40
|
}) => {
|
|
37
41
|
const tokens = useAppDesignTokens();
|
|
42
|
+
const cfg = { ...DEFAULT_RESULT_CONFIG.actions, ...config };
|
|
38
43
|
|
|
39
|
-
const styles = useMemo(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
44
|
+
const styles = useMemo(
|
|
45
|
+
() =>
|
|
46
|
+
StyleSheet.create({
|
|
47
|
+
container: {
|
|
48
|
+
paddingHorizontal: cfg.spacing?.paddingHorizontal ?? 20,
|
|
49
|
+
paddingBottom: cfg.spacing?.paddingBottom ?? 20,
|
|
50
|
+
},
|
|
51
|
+
retryButton: {
|
|
52
|
+
flexDirection: "row",
|
|
53
|
+
alignItems: "center",
|
|
54
|
+
justifyContent: "center",
|
|
55
|
+
gap: 6,
|
|
56
|
+
paddingVertical: 12,
|
|
57
|
+
marginBottom: cfg.retry?.position === "top" ? 16 : 0,
|
|
58
|
+
marginTop: cfg.retry?.position === "bottom" ? 16 : 0,
|
|
59
|
+
},
|
|
60
|
+
retryText: {
|
|
61
|
+
fontSize: 14,
|
|
62
|
+
fontWeight: "600",
|
|
63
|
+
color: tokens.colors.primary,
|
|
64
|
+
},
|
|
65
|
+
buttons: {
|
|
66
|
+
flexDirection:
|
|
67
|
+
cfg.layout === "vertical" ? "column" : "row",
|
|
68
|
+
gap: cfg.buttonSpacing ?? 10,
|
|
69
|
+
},
|
|
70
|
+
button: {
|
|
71
|
+
flex: cfg.layout === "horizontal" ? 1 : undefined,
|
|
72
|
+
flexDirection: "row",
|
|
73
|
+
alignItems: "center",
|
|
74
|
+
justifyContent: "center",
|
|
75
|
+
gap: 8,
|
|
76
|
+
paddingVertical: 14,
|
|
77
|
+
borderRadius: 14,
|
|
78
|
+
},
|
|
79
|
+
}),
|
|
80
|
+
[tokens, cfg],
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const getButtonStyle = (variant?: string) => {
|
|
84
|
+
switch (variant) {
|
|
85
|
+
case "primary":
|
|
86
|
+
return {
|
|
87
|
+
backgroundColor: tokens.colors.primary,
|
|
88
|
+
color: tokens.colors.onPrimary,
|
|
89
|
+
textColor: tokens.colors.onPrimary,
|
|
90
|
+
};
|
|
91
|
+
case "secondary":
|
|
92
|
+
return {
|
|
93
|
+
backgroundColor: tokens.colors.surface,
|
|
94
|
+
borderWidth: 2,
|
|
95
|
+
borderColor: tokens.colors.primary,
|
|
96
|
+
color: tokens.colors.primary,
|
|
97
|
+
textColor: tokens.colors.primary,
|
|
98
|
+
};
|
|
99
|
+
case "outline":
|
|
100
|
+
return {
|
|
101
|
+
backgroundColor: "transparent",
|
|
102
|
+
borderWidth: 1,
|
|
103
|
+
borderColor: tokens.colors.borderLight,
|
|
104
|
+
color: tokens.colors.textPrimary,
|
|
105
|
+
textColor: tokens.colors.textPrimary,
|
|
106
|
+
};
|
|
107
|
+
case "text":
|
|
108
|
+
return {
|
|
109
|
+
backgroundColor: "transparent",
|
|
110
|
+
color: tokens.colors.primary,
|
|
111
|
+
textColor: tokens.colors.primary,
|
|
112
|
+
};
|
|
113
|
+
default:
|
|
114
|
+
return {
|
|
115
|
+
backgroundColor: tokens.colors.primary,
|
|
116
|
+
color: tokens.colors.onPrimary,
|
|
117
|
+
textColor: tokens.colors.onPrimary,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const renderButton = (
|
|
123
|
+
key: string,
|
|
124
|
+
onPress?: () => void,
|
|
125
|
+
isProcessing?: boolean,
|
|
126
|
+
label?: string,
|
|
127
|
+
processingLabel?: string,
|
|
128
|
+
icon?: string,
|
|
129
|
+
variant?: string,
|
|
130
|
+
) => {
|
|
131
|
+
if (!onPress) return null;
|
|
132
|
+
|
|
133
|
+
const buttonStyle = getButtonStyle(variant);
|
|
134
|
+
const displayLabel = isProcessing ? processingLabel : label;
|
|
135
|
+
const displayIcon = isProcessing ? "hourglass" : icon;
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<TouchableOpacity
|
|
139
|
+
key={key}
|
|
140
|
+
style={[styles.button, buttonStyle]}
|
|
141
|
+
onPress={onPress}
|
|
142
|
+
disabled={isProcessing}
|
|
143
|
+
>
|
|
144
|
+
{displayIcon && (
|
|
145
|
+
<AtomicIcon
|
|
146
|
+
name={displayIcon}
|
|
147
|
+
size="md"
|
|
148
|
+
customColor={buttonStyle.textColor}
|
|
149
|
+
/>
|
|
150
|
+
)}
|
|
151
|
+
<AtomicText
|
|
152
|
+
style={{
|
|
153
|
+
fontSize: 15,
|
|
154
|
+
fontWeight: "700",
|
|
155
|
+
color: buttonStyle.textColor,
|
|
156
|
+
}}
|
|
157
|
+
>
|
|
158
|
+
{displayLabel}
|
|
159
|
+
</AtomicText>
|
|
160
|
+
</TouchableOpacity>
|
|
161
|
+
);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const topActions = cfg.retry?.enabled && cfg.retry?.position === "top" && onRetry;
|
|
165
|
+
const bottomActions =
|
|
166
|
+
cfg.retry?.enabled && cfg.retry?.position === "bottom" && onRetry;
|
|
89
167
|
|
|
90
168
|
return (
|
|
91
169
|
<View style={styles.container}>
|
|
92
|
-
{
|
|
170
|
+
{topActions && (
|
|
93
171
|
<TouchableOpacity style={styles.retryButton} onPress={onRetry}>
|
|
94
|
-
<AtomicIcon
|
|
95
|
-
|
|
172
|
+
<AtomicIcon
|
|
173
|
+
name={cfg.retry?.icon ?? "refresh"}
|
|
174
|
+
size="sm"
|
|
175
|
+
color="primary"
|
|
176
|
+
/>
|
|
177
|
+
<AtomicText style={styles.retryText}>
|
|
178
|
+
{cfg.retry?.label ?? translations.retry}
|
|
179
|
+
</AtomicText>
|
|
96
180
|
</TouchableOpacity>
|
|
97
181
|
)}
|
|
98
182
|
|
|
99
183
|
<View style={styles.buttons}>
|
|
100
|
-
{
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
/>
|
|
111
|
-
<AtomicText style={styles.shareText}>
|
|
112
|
-
{isSharing ? translations.sharing : translations.share}
|
|
113
|
-
</AtomicText>
|
|
114
|
-
</TouchableOpacity>
|
|
115
|
-
)}
|
|
184
|
+
{cfg.share?.enabled &&
|
|
185
|
+
renderButton(
|
|
186
|
+
"share",
|
|
187
|
+
onShare,
|
|
188
|
+
isSharing,
|
|
189
|
+
cfg.share?.label ?? translations.share,
|
|
190
|
+
translations.sharing,
|
|
191
|
+
cfg.share?.icon ?? "share-social",
|
|
192
|
+
cfg.share?.variant,
|
|
193
|
+
)}
|
|
116
194
|
|
|
117
|
-
{
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
/>
|
|
128
|
-
<AtomicText style={styles.saveText}>{translations.save}</AtomicText>
|
|
129
|
-
</TouchableOpacity>
|
|
130
|
-
)}
|
|
195
|
+
{cfg.save?.enabled &&
|
|
196
|
+
renderButton(
|
|
197
|
+
"save",
|
|
198
|
+
onSave,
|
|
199
|
+
isSaving,
|
|
200
|
+
cfg.save?.label ?? translations.save,
|
|
201
|
+
translations.save,
|
|
202
|
+
cfg.save?.icon ?? "download",
|
|
203
|
+
cfg.save?.variant,
|
|
204
|
+
)}
|
|
131
205
|
</View>
|
|
206
|
+
|
|
207
|
+
{bottomActions && (
|
|
208
|
+
<TouchableOpacity style={styles.retryButton} onPress={onRetry}>
|
|
209
|
+
<AtomicIcon
|
|
210
|
+
name={cfg.retry?.icon ?? "refresh"}
|
|
211
|
+
size="sm"
|
|
212
|
+
color="primary"
|
|
213
|
+
/>
|
|
214
|
+
<AtomicText style={styles.retryText}>
|
|
215
|
+
{cfg.retry?.label ?? translations.retry}
|
|
216
|
+
</AtomicText>
|
|
217
|
+
</TouchableOpacity>
|
|
218
|
+
)}
|
|
132
219
|
</View>
|
|
133
220
|
);
|
|
134
221
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ResultHeader Component
|
|
3
|
-
* Header with title and date badge
|
|
3
|
+
* Header with title and date badge - fully configurable
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import * as React from "react";
|
|
@@ -11,57 +11,88 @@ import {
|
|
|
11
11
|
AtomicIcon,
|
|
12
12
|
useAppDesignTokens,
|
|
13
13
|
} from "@umituz/react-native-design-system";
|
|
14
|
+
import type { ResultHeaderConfig } from "../../types/result-config.types";
|
|
15
|
+
import { DEFAULT_RESULT_CONFIG } from "../../types/result-config.types";
|
|
14
16
|
|
|
15
17
|
export interface ResultHeaderProps {
|
|
16
18
|
title?: string;
|
|
17
19
|
date?: string;
|
|
20
|
+
config?: ResultHeaderConfig;
|
|
18
21
|
}
|
|
19
22
|
|
|
20
|
-
export const ResultHeader: React.FC<ResultHeaderProps> = ({
|
|
23
|
+
export const ResultHeader: React.FC<ResultHeaderProps> = ({
|
|
24
|
+
title,
|
|
25
|
+
date,
|
|
26
|
+
config = DEFAULT_RESULT_CONFIG.header,
|
|
27
|
+
}) => {
|
|
21
28
|
const tokens = useAppDesignTokens();
|
|
29
|
+
const cfg = { ...DEFAULT_RESULT_CONFIG.header, ...config };
|
|
22
30
|
|
|
23
|
-
const styles = useMemo(() =>
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
31
|
+
const styles = useMemo(() => {
|
|
32
|
+
const badgeStyles =
|
|
33
|
+
cfg.dateBadgeStyle === "outline"
|
|
34
|
+
? {
|
|
35
|
+
backgroundColor: "transparent",
|
|
36
|
+
borderWidth: 1,
|
|
37
|
+
borderColor: tokens.colors.primary,
|
|
38
|
+
}
|
|
39
|
+
: cfg.dateBadgeStyle === "minimal"
|
|
40
|
+
? {
|
|
41
|
+
backgroundColor: "transparent",
|
|
42
|
+
}
|
|
43
|
+
: {
|
|
44
|
+
backgroundColor: tokens.colors.primaryContainer,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return StyleSheet.create({
|
|
48
|
+
container: {
|
|
49
|
+
alignItems:
|
|
50
|
+
cfg.titleAlignment === "left"
|
|
51
|
+
? "flex-start"
|
|
52
|
+
: cfg.titleAlignment === "right"
|
|
53
|
+
? "flex-end"
|
|
54
|
+
: "center",
|
|
55
|
+
paddingHorizontal: cfg.spacing?.paddingHorizontal ?? 24,
|
|
56
|
+
marginBottom: cfg.spacing?.marginBottom ?? 20,
|
|
57
|
+
},
|
|
58
|
+
title: {
|
|
59
|
+
fontSize: cfg.titleFontSize ?? 24,
|
|
60
|
+
lineHeight: (cfg.titleFontSize ?? 24) * 1.33,
|
|
61
|
+
fontWeight: cfg.titleFontWeight ?? "800",
|
|
62
|
+
color: tokens.colors.textPrimary,
|
|
63
|
+
textAlign: cfg.titleAlignment ?? "center",
|
|
64
|
+
marginBottom: cfg.spacing?.titleMarginBottom ?? 12,
|
|
65
|
+
},
|
|
66
|
+
badge: {
|
|
67
|
+
flexDirection: "row",
|
|
68
|
+
alignItems: "center",
|
|
69
|
+
gap: 6,
|
|
70
|
+
paddingHorizontal: 14,
|
|
71
|
+
paddingVertical: 6,
|
|
72
|
+
borderRadius: 16,
|
|
73
|
+
...badgeStyles,
|
|
74
|
+
},
|
|
75
|
+
dateText: {
|
|
76
|
+
fontSize: 12,
|
|
77
|
+
fontWeight: "600",
|
|
78
|
+
color: tokens.colors.primary,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}, [tokens, cfg]);
|
|
52
82
|
|
|
53
83
|
if (!title && !date) return null;
|
|
84
|
+
if (!cfg.showTitle && !cfg.showDate) return null;
|
|
54
85
|
|
|
55
86
|
return (
|
|
56
87
|
<View style={styles.container}>
|
|
57
|
-
{title &&
|
|
58
|
-
|
|
88
|
+
{cfg.showTitle && title && (
|
|
89
|
+
<AtomicText style={styles.title}>{title}</AtomicText>
|
|
90
|
+
)}
|
|
91
|
+
{cfg.showDate && date && (
|
|
59
92
|
<View style={styles.badge}>
|
|
60
|
-
|
|
61
|
-
name="calendar-outline"
|
|
62
|
-
|
|
63
|
-
color="primary"
|
|
64
|
-
/>
|
|
93
|
+
{cfg.showDateIcon && (
|
|
94
|
+
<AtomicIcon name="calendar-outline" size="sm" color="primary" />
|
|
95
|
+
)}
|
|
65
96
|
<AtomicText style={styles.dateText}>{date}</AtomicText>
|
|
66
97
|
</View>
|
|
67
98
|
)}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ResultImageCard Component
|
|
3
|
-
* Displays generated image with AI badge
|
|
3
|
+
* Displays generated image with AI badge - fully configurable
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import * as React from "react";
|
|
@@ -11,60 +11,139 @@ import {
|
|
|
11
11
|
AtomicIcon,
|
|
12
12
|
useAppDesignTokens,
|
|
13
13
|
} from "@umituz/react-native-design-system";
|
|
14
|
+
import { LinearGradient } from "expo-linear-gradient";
|
|
15
|
+
import type { ResultImageConfig } from "../../types/result-config.types";
|
|
16
|
+
import { DEFAULT_RESULT_CONFIG } from "../../types/result-config.types";
|
|
14
17
|
|
|
15
18
|
export interface ResultImageCardProps {
|
|
16
19
|
imageUrl: string;
|
|
17
20
|
badgeText: string;
|
|
21
|
+
config?: ResultImageConfig;
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
export const ResultImageCard: React.FC<ResultImageCardProps> = ({
|
|
21
25
|
imageUrl,
|
|
22
26
|
badgeText,
|
|
27
|
+
config = DEFAULT_RESULT_CONFIG.image,
|
|
23
28
|
}) => {
|
|
24
29
|
const tokens = useAppDesignTokens();
|
|
30
|
+
const cfg = { ...DEFAULT_RESULT_CONFIG.image, ...config };
|
|
25
31
|
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
32
|
+
const badgePosition = useMemo(() => {
|
|
33
|
+
switch (cfg.badgePosition) {
|
|
34
|
+
case "top-left":
|
|
35
|
+
return { top: 12, left: 12 };
|
|
36
|
+
case "bottom-left":
|
|
37
|
+
return { bottom: 12, left: 12 };
|
|
38
|
+
case "bottom-right":
|
|
39
|
+
return { bottom: 12, right: 12 };
|
|
40
|
+
case "top-right":
|
|
41
|
+
default:
|
|
42
|
+
return { top: 12, right: 12 };
|
|
43
|
+
}
|
|
44
|
+
}, [cfg.badgePosition]);
|
|
45
|
+
|
|
46
|
+
const badgeBackground = useMemo(() => {
|
|
47
|
+
if (cfg.badgeStyle === "light") {
|
|
48
|
+
return "rgba(255, 255, 255, 0.9)";
|
|
49
|
+
} else if (cfg.badgeStyle === "dark") {
|
|
50
|
+
return "rgba(0, 0, 0, 0.6)";
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}, [cfg.badgeStyle]);
|
|
54
|
+
|
|
55
|
+
const styles = useMemo(
|
|
56
|
+
() =>
|
|
57
|
+
StyleSheet.create({
|
|
58
|
+
container: {
|
|
59
|
+
paddingHorizontal: cfg.spacing?.paddingHorizontal ?? 20,
|
|
60
|
+
marginBottom: cfg.spacing?.marginBottom ?? 20,
|
|
61
|
+
},
|
|
62
|
+
frame: {
|
|
63
|
+
borderRadius: cfg.borderRadius ?? 20,
|
|
64
|
+
overflow: "hidden",
|
|
65
|
+
backgroundColor: tokens.colors.surface,
|
|
66
|
+
},
|
|
67
|
+
image: {
|
|
68
|
+
width: "100%",
|
|
69
|
+
aspectRatio: cfg.aspectRatio ?? 1,
|
|
70
|
+
},
|
|
71
|
+
badge: {
|
|
72
|
+
position: "absolute",
|
|
73
|
+
...badgePosition,
|
|
74
|
+
flexDirection: "row",
|
|
75
|
+
alignItems: "center",
|
|
76
|
+
gap: 4,
|
|
77
|
+
paddingHorizontal: 10,
|
|
78
|
+
paddingVertical: 5,
|
|
79
|
+
backgroundColor: badgeBackground ?? undefined,
|
|
80
|
+
borderRadius: 12,
|
|
81
|
+
overflow: "hidden",
|
|
82
|
+
},
|
|
83
|
+
gradientBadge: {
|
|
84
|
+
flexDirection: "row",
|
|
85
|
+
alignItems: "center",
|
|
86
|
+
gap: 4,
|
|
87
|
+
paddingHorizontal: 10,
|
|
88
|
+
paddingVertical: 5,
|
|
89
|
+
},
|
|
90
|
+
badgeText: {
|
|
91
|
+
fontSize: 10,
|
|
92
|
+
fontWeight: "700",
|
|
93
|
+
color:
|
|
94
|
+
cfg.badgeStyle === "light"
|
|
95
|
+
? tokens.colors.textPrimary
|
|
96
|
+
: "#FFFFFF",
|
|
97
|
+
letterSpacing: 0.5,
|
|
98
|
+
},
|
|
99
|
+
}),
|
|
100
|
+
[tokens, cfg, badgePosition, badgeBackground],
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const renderBadge = () => {
|
|
104
|
+
if (!cfg.showBadge) return null;
|
|
105
|
+
|
|
106
|
+
const iconColor =
|
|
107
|
+
cfg.badgeStyle === "light" ? tokens.colors.primary : "#FFFFFF";
|
|
108
|
+
|
|
109
|
+
const badgeContent = (
|
|
110
|
+
<>
|
|
111
|
+
<AtomicIcon
|
|
112
|
+
name={cfg.badgeIcon ?? "sparkles"}
|
|
113
|
+
size="xs"
|
|
114
|
+
customColor={iconColor}
|
|
115
|
+
/>
|
|
116
|
+
<AtomicText style={styles.badgeText}>{badgeText}</AtomicText>
|
|
117
|
+
</>
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
if (cfg.badgeStyle === "gradient") {
|
|
121
|
+
return (
|
|
122
|
+
<View style={styles.badge}>
|
|
123
|
+
<LinearGradient
|
|
124
|
+
colors={[tokens.colors.primary, tokens.colors.secondary]}
|
|
125
|
+
start={{ x: 0, y: 0 }}
|
|
126
|
+
end={{ x: 1, y: 0 }}
|
|
127
|
+
style={styles.gradientBadge}
|
|
128
|
+
>
|
|
129
|
+
{badgeContent}
|
|
130
|
+
</LinearGradient>
|
|
131
|
+
</View>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return <View style={styles.badge}>{badgeContent}</View>;
|
|
136
|
+
};
|
|
59
137
|
|
|
60
138
|
return (
|
|
61
139
|
<View style={styles.container}>
|
|
62
140
|
<View style={styles.frame}>
|
|
63
|
-
<Image
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
141
|
+
<Image
|
|
142
|
+
source={{ uri: imageUrl }}
|
|
143
|
+
style={styles.image}
|
|
144
|
+
resizeMode="cover"
|
|
145
|
+
/>
|
|
146
|
+
{renderBadge()}
|
|
68
147
|
</View>
|
|
69
148
|
</View>
|
|
70
149
|
);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ResultStoryCard Component
|
|
3
|
-
* Displays story text with quote styling
|
|
3
|
+
* Displays story text with quote styling - fully configurable
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import * as React from "react";
|
|
@@ -11,63 +11,113 @@ import {
|
|
|
11
11
|
useAppDesignTokens,
|
|
12
12
|
} from "@umituz/react-native-design-system";
|
|
13
13
|
import { LinearGradient } from "expo-linear-gradient";
|
|
14
|
+
import type { ResultStoryConfig } from "../../types/result-config.types";
|
|
15
|
+
import { DEFAULT_RESULT_CONFIG } from "../../types/result-config.types";
|
|
14
16
|
|
|
15
17
|
export interface ResultStoryCardProps {
|
|
16
18
|
story: string;
|
|
19
|
+
config?: ResultStoryConfig;
|
|
17
20
|
}
|
|
18
21
|
|
|
19
|
-
export const ResultStoryCard: React.FC<ResultStoryCardProps> = ({
|
|
22
|
+
export const ResultStoryCard: React.FC<ResultStoryCardProps> = ({
|
|
23
|
+
story,
|
|
24
|
+
config = DEFAULT_RESULT_CONFIG.story,
|
|
25
|
+
}) => {
|
|
20
26
|
const tokens = useAppDesignTokens();
|
|
27
|
+
const cfg = { ...DEFAULT_RESULT_CONFIG.story, ...config };
|
|
21
28
|
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
marginBottom: 20,
|
|
26
|
-
},
|
|
27
|
-
container: {
|
|
28
|
-
padding: 20,
|
|
29
|
+
const containerStyle = useMemo(() => {
|
|
30
|
+
const base = {
|
|
31
|
+
padding: cfg.spacing?.padding ?? 20,
|
|
29
32
|
borderRadius: 16,
|
|
30
|
-
|
|
31
|
-
borderColor: tokens.colors.primaryContainer,
|
|
32
|
-
},
|
|
33
|
-
quoteIcon: {
|
|
34
|
-
fontSize: 40,
|
|
35
|
-
lineHeight: 40,
|
|
36
|
-
color: tokens.colors.primary,
|
|
37
|
-
opacity: 0.4,
|
|
38
|
-
marginBottom: -12,
|
|
39
|
-
},
|
|
40
|
-
quoteEnd: {
|
|
41
|
-
alignItems: "flex-end",
|
|
42
|
-
marginTop: -12,
|
|
43
|
-
},
|
|
44
|
-
quoteIconEnd: {
|
|
45
|
-
marginBottom: 0,
|
|
46
|
-
},
|
|
47
|
-
text: {
|
|
48
|
-
fontSize: 14,
|
|
49
|
-
color: tokens.colors.textPrimary,
|
|
50
|
-
textAlign: "center",
|
|
51
|
-
lineHeight: 22,
|
|
52
|
-
fontStyle: "italic",
|
|
53
|
-
fontWeight: "500",
|
|
54
|
-
},
|
|
55
|
-
}), [tokens]);
|
|
33
|
+
};
|
|
56
34
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
35
|
+
if (cfg.borderStyle === "outline") {
|
|
36
|
+
return {
|
|
37
|
+
...base,
|
|
38
|
+
borderWidth: 1,
|
|
39
|
+
borderColor: tokens.colors.primaryContainer,
|
|
40
|
+
backgroundColor: "transparent",
|
|
41
|
+
};
|
|
42
|
+
} else if (cfg.borderStyle === "filled") {
|
|
43
|
+
return {
|
|
44
|
+
...base,
|
|
45
|
+
backgroundColor: tokens.colors.primaryContainer,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return base;
|
|
50
|
+
}, [cfg.borderStyle, cfg.spacing, tokens]);
|
|
51
|
+
|
|
52
|
+
const styles = useMemo(
|
|
53
|
+
() =>
|
|
54
|
+
StyleSheet.create({
|
|
55
|
+
outer: {
|
|
56
|
+
paddingHorizontal: cfg.spacing?.paddingHorizontal ?? 20,
|
|
57
|
+
marginBottom: cfg.spacing?.marginBottom ?? 20,
|
|
58
|
+
},
|
|
59
|
+
container: containerStyle,
|
|
60
|
+
quoteIcon: {
|
|
61
|
+
fontSize: 40,
|
|
62
|
+
lineHeight: 40,
|
|
63
|
+
color: tokens.colors.primary,
|
|
64
|
+
opacity: 0.4,
|
|
65
|
+
marginBottom: -12,
|
|
66
|
+
},
|
|
67
|
+
quoteEnd: {
|
|
68
|
+
alignItems:
|
|
69
|
+
cfg.textAlignment === "left"
|
|
70
|
+
? "flex-start"
|
|
71
|
+
: cfg.textAlignment === "right"
|
|
72
|
+
? "flex-end"
|
|
73
|
+
: "flex-end",
|
|
74
|
+
marginTop: -12,
|
|
75
|
+
},
|
|
76
|
+
quoteIconEnd: {
|
|
77
|
+
marginBottom: 0,
|
|
78
|
+
},
|
|
79
|
+
text: {
|
|
80
|
+
fontSize: cfg.fontSize ?? 14,
|
|
81
|
+
color: tokens.colors.textPrimary,
|
|
82
|
+
textAlign: cfg.textAlignment ?? "center",
|
|
83
|
+
lineHeight: (cfg.fontSize ?? 14) * 1.57,
|
|
84
|
+
fontStyle: cfg.fontStyle ?? "italic",
|
|
85
|
+
fontWeight: cfg.fontWeight ?? "500",
|
|
86
|
+
},
|
|
87
|
+
}),
|
|
88
|
+
[tokens, cfg, containerStyle],
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const renderContent = () => (
|
|
92
|
+
<>
|
|
93
|
+
{cfg.showQuotes && <AtomicText style={styles.quoteIcon}>"</AtomicText>}
|
|
94
|
+
<AtomicText style={styles.text}>{story}</AtomicText>
|
|
95
|
+
{cfg.showQuotes && (
|
|
65
96
|
<View style={styles.quoteEnd}>
|
|
66
97
|
<AtomicText style={[styles.quoteIcon, styles.quoteIconEnd]}>
|
|
67
98
|
"
|
|
68
99
|
</AtomicText>
|
|
69
100
|
</View>
|
|
70
|
-
|
|
101
|
+
)}
|
|
102
|
+
</>
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
if (cfg.borderStyle === "gradient") {
|
|
106
|
+
return (
|
|
107
|
+
<View style={styles.outer}>
|
|
108
|
+
<LinearGradient
|
|
109
|
+
colors={[tokens.colors.primaryContainer, tokens.colors.surface]}
|
|
110
|
+
style={styles.container}
|
|
111
|
+
>
|
|
112
|
+
{renderContent()}
|
|
113
|
+
</LinearGradient>
|
|
114
|
+
</View>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<View style={styles.outer}>
|
|
120
|
+
<View style={styles.container}>{renderContent()}</View>
|
|
71
121
|
</View>
|
|
72
122
|
);
|
|
73
123
|
};
|
|
@@ -19,3 +19,17 @@ export type { ResultStoryCardProps } from "./ResultStoryCard";
|
|
|
19
19
|
|
|
20
20
|
export { ResultActions } from "./ResultActions";
|
|
21
21
|
export type { ResultActionsProps } from "./ResultActions";
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
DEFAULT_RESULT_CONFIG,
|
|
25
|
+
} from "../../types/result-config.types";
|
|
26
|
+
|
|
27
|
+
export type {
|
|
28
|
+
ResultConfig,
|
|
29
|
+
ResultHeaderConfig,
|
|
30
|
+
ResultImageConfig,
|
|
31
|
+
ResultStoryConfig,
|
|
32
|
+
ResultActionsConfig,
|
|
33
|
+
ResultLayoutConfig,
|
|
34
|
+
ResultActionButton,
|
|
35
|
+
} from "../../types/result-config.types";
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Result Preview Configuration Types
|
|
3
|
+
* Allows main apps to customize result preview appearance and behavior
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface ResultHeaderConfig {
|
|
7
|
+
showTitle?: boolean;
|
|
8
|
+
showDate?: boolean;
|
|
9
|
+
showDateIcon?: boolean;
|
|
10
|
+
titleAlignment?: "left" | "center" | "right";
|
|
11
|
+
titleFontSize?: number;
|
|
12
|
+
titleFontWeight?:
|
|
13
|
+
| "normal"
|
|
14
|
+
| "bold"
|
|
15
|
+
| "100"
|
|
16
|
+
| "200"
|
|
17
|
+
| "300"
|
|
18
|
+
| "400"
|
|
19
|
+
| "500"
|
|
20
|
+
| "600"
|
|
21
|
+
| "700"
|
|
22
|
+
| "800"
|
|
23
|
+
| "900";
|
|
24
|
+
dateBadgeStyle?: "outline" | "filled" | "minimal";
|
|
25
|
+
spacing?: {
|
|
26
|
+
marginBottom?: number;
|
|
27
|
+
titleMarginBottom?: number;
|
|
28
|
+
paddingHorizontal?: number;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ResultImageConfig {
|
|
33
|
+
aspectRatio?: number;
|
|
34
|
+
borderRadius?: number;
|
|
35
|
+
showBadge?: boolean;
|
|
36
|
+
badgePosition?: "top-left" | "top-right" | "bottom-left" | "bottom-right";
|
|
37
|
+
badgeStyle?: "dark" | "light" | "gradient";
|
|
38
|
+
badgeIcon?: string;
|
|
39
|
+
spacing?: {
|
|
40
|
+
marginBottom?: number;
|
|
41
|
+
paddingHorizontal?: number;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ResultStoryConfig {
|
|
46
|
+
showQuotes?: boolean;
|
|
47
|
+
textAlignment?: "left" | "center" | "right";
|
|
48
|
+
fontSize?: number;
|
|
49
|
+
fontStyle?: "normal" | "italic";
|
|
50
|
+
fontWeight?:
|
|
51
|
+
| "normal"
|
|
52
|
+
| "bold"
|
|
53
|
+
| "100"
|
|
54
|
+
| "200"
|
|
55
|
+
| "300"
|
|
56
|
+
| "400"
|
|
57
|
+
| "500"
|
|
58
|
+
| "600"
|
|
59
|
+
| "700"
|
|
60
|
+
| "800"
|
|
61
|
+
| "900";
|
|
62
|
+
borderStyle?: "outline" | "filled" | "gradient";
|
|
63
|
+
spacing?: {
|
|
64
|
+
marginBottom?: number;
|
|
65
|
+
paddingHorizontal?: number;
|
|
66
|
+
padding?: number;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface ResultActionButton {
|
|
71
|
+
enabled?: boolean;
|
|
72
|
+
label?: string;
|
|
73
|
+
icon?: string;
|
|
74
|
+
variant?: "primary" | "secondary" | "outline" | "text";
|
|
75
|
+
position?: "top" | "bottom";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface ResultActionsConfig {
|
|
79
|
+
share?: ResultActionButton;
|
|
80
|
+
save?: ResultActionButton;
|
|
81
|
+
retry?: ResultActionButton;
|
|
82
|
+
layout?: "horizontal" | "vertical" | "grid";
|
|
83
|
+
buttonSpacing?: number;
|
|
84
|
+
spacing?: {
|
|
85
|
+
paddingHorizontal?: number;
|
|
86
|
+
paddingBottom?: number;
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface ResultLayoutConfig {
|
|
91
|
+
maxWidth?: number;
|
|
92
|
+
maxHeight?: string | number;
|
|
93
|
+
borderRadius?: number;
|
|
94
|
+
backgroundColor?: string;
|
|
95
|
+
scrollEnabled?: boolean;
|
|
96
|
+
contentPadding?: {
|
|
97
|
+
top?: number;
|
|
98
|
+
bottom?: number;
|
|
99
|
+
horizontal?: number;
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Complete Result Preview Configuration
|
|
105
|
+
* Pass this from main app to customize all aspects of result preview
|
|
106
|
+
*/
|
|
107
|
+
export interface ResultConfig {
|
|
108
|
+
header?: ResultHeaderConfig;
|
|
109
|
+
image?: ResultImageConfig;
|
|
110
|
+
story?: ResultStoryConfig;
|
|
111
|
+
actions?: ResultActionsConfig;
|
|
112
|
+
layout?: ResultLayoutConfig;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Default configuration for result preview
|
|
117
|
+
* Used when no config is provided by main app
|
|
118
|
+
*/
|
|
119
|
+
export const DEFAULT_RESULT_CONFIG: ResultConfig = {
|
|
120
|
+
header: {
|
|
121
|
+
showTitle: true,
|
|
122
|
+
showDate: true,
|
|
123
|
+
showDateIcon: true,
|
|
124
|
+
titleAlignment: "center",
|
|
125
|
+
titleFontSize: 24,
|
|
126
|
+
titleFontWeight: "800",
|
|
127
|
+
dateBadgeStyle: "filled",
|
|
128
|
+
spacing: {
|
|
129
|
+
marginBottom: 20,
|
|
130
|
+
titleMarginBottom: 12,
|
|
131
|
+
paddingHorizontal: 24,
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
image: {
|
|
135
|
+
aspectRatio: 1,
|
|
136
|
+
borderRadius: 20,
|
|
137
|
+
showBadge: true,
|
|
138
|
+
badgePosition: "top-right",
|
|
139
|
+
badgeStyle: "dark",
|
|
140
|
+
badgeIcon: "sparkles",
|
|
141
|
+
spacing: {
|
|
142
|
+
marginBottom: 20,
|
|
143
|
+
paddingHorizontal: 20,
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
story: {
|
|
147
|
+
showQuotes: true,
|
|
148
|
+
textAlignment: "center",
|
|
149
|
+
fontSize: 14,
|
|
150
|
+
fontStyle: "italic",
|
|
151
|
+
fontWeight: "500",
|
|
152
|
+
borderStyle: "gradient",
|
|
153
|
+
spacing: {
|
|
154
|
+
marginBottom: 20,
|
|
155
|
+
paddingHorizontal: 20,
|
|
156
|
+
padding: 20,
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
actions: {
|
|
160
|
+
share: {
|
|
161
|
+
enabled: true,
|
|
162
|
+
icon: "share-social",
|
|
163
|
+
variant: "primary",
|
|
164
|
+
position: "bottom",
|
|
165
|
+
},
|
|
166
|
+
save: {
|
|
167
|
+
enabled: true,
|
|
168
|
+
icon: "download",
|
|
169
|
+
variant: "secondary",
|
|
170
|
+
position: "bottom",
|
|
171
|
+
},
|
|
172
|
+
retry: {
|
|
173
|
+
enabled: true,
|
|
174
|
+
icon: "refresh",
|
|
175
|
+
variant: "text",
|
|
176
|
+
position: "top",
|
|
177
|
+
},
|
|
178
|
+
layout: "horizontal",
|
|
179
|
+
buttonSpacing: 10,
|
|
180
|
+
spacing: {
|
|
181
|
+
paddingHorizontal: 20,
|
|
182
|
+
paddingBottom: 20,
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
layout: {
|
|
186
|
+
borderRadius: 28,
|
|
187
|
+
scrollEnabled: true,
|
|
188
|
+
contentPadding: {
|
|
189
|
+
top: 24,
|
|
190
|
+
bottom: 20,
|
|
191
|
+
horizontal: 0,
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
};
|