@umituz/react-native-ai-generation-content 1.13.1 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-ai-generation-content",
3
- "version": "1.13.1",
3
+ "version": "1.14.0",
4
4
  "description": "Provider-agnostic AI generation orchestration for React Native",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -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,7 +28,7 @@ export function CreationCard({
29
28
  locale = "en-US",
30
29
  }: CreationCardProps) {
31
30
  const tokens = useAppDesignTokens();
32
- const { translatedTypes, t } = useCreationsProvider();
31
+ const { translatedTypes } = useCreationsProvider();
33
32
 
34
33
  const typeConfig = translatedTypes.find((type) => type.id === creation.type);
35
34
  const icon = typeConfig?.icon;
@@ -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 {
@@ -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 { GalleryHeader, CreationsGrid, CreationImageViewer, GalleryEmptyStates } from "../components";
22
- import { getTranslatedTypes, getFilterCategoriesFromConfig } from "../utils/filterUtils";
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 translatedTypes = useMemo(() => getTranslatedTypes(config, t), [config, t]);
87
- const allCategories = useMemo(() => getFilterCategoriesFromConfig(config, t), [config, t]);
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(userId, creation.id, isFavorite);
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 { ScrollView, StyleSheet, Dimensions, type ViewStyle, type StyleProp } from "react-native";
9
- import { Animated, useAppDesignTokens } from "@umituz/react-native-design-system";
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<GenerationResultContentProps> = ({
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(() => StyleSheet.create({
54
- container: {
55
- width: width - 40,
56
- maxHeight: "90%",
57
- backgroundColor: tokens.colors.background,
58
- borderRadius: 28,
59
- overflow: "hidden",
60
- },
61
- scrollView: {
62
- flex: 1,
63
- },
64
- scrollContent: {
65
- paddingTop: 24,
66
- paddingBottom: 20,
67
- },
68
- }), [tokens]);
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 title={result.title} date={result.date} />
78
- <ResultImageCard imageUrl={result.imageUrl} badgeText={translations.aiGenerated} />
79
- {result.story && <ResultStoryCard story={result.story} />}
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(() => StyleSheet.create({
40
- container: {
41
- paddingHorizontal: 20,
42
- paddingBottom: 20,
43
- },
44
- retryButton: {
45
- flexDirection: "row",
46
- alignItems: "center",
47
- justifyContent: "center",
48
- gap: 6,
49
- paddingVertical: 12,
50
- marginBottom: 16,
51
- },
52
- retryText: {
53
- fontSize: 14,
54
- fontWeight: "600",
55
- color: tokens.colors.primary,
56
- },
57
- buttons: {
58
- flexDirection: "row",
59
- gap: 10,
60
- },
61
- button: {
62
- flex: 1,
63
- flexDirection: "row",
64
- alignItems: "center",
65
- justifyContent: "center",
66
- gap: 8,
67
- paddingVertical: 14,
68
- borderRadius: 14,
69
- },
70
- shareButton: {
71
- backgroundColor: tokens.colors.primary,
72
- },
73
- shareText: {
74
- fontSize: 15,
75
- fontWeight: "700",
76
- color: tokens.colors.onPrimary,
77
- },
78
- saveButton: {
79
- backgroundColor: tokens.colors.surface,
80
- borderWidth: 2,
81
- borderColor: tokens.colors.primary,
82
- },
83
- saveText: {
84
- fontSize: 15,
85
- fontWeight: "700",
86
- color: tokens.colors.primary,
87
- },
88
- }), [tokens]);
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
- {onRetry && (
170
+ {topActions && (
93
171
  <TouchableOpacity style={styles.retryButton} onPress={onRetry}>
94
- <AtomicIcon name="refresh" size="sm" color="primary" />
95
- <AtomicText style={styles.retryText}>{translations.retry}</AtomicText>
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
- {onShare && (
101
- <TouchableOpacity
102
- style={[styles.button, styles.shareButton]}
103
- onPress={onShare}
104
- disabled={isSharing}
105
- >
106
- <AtomicIcon
107
- name={isSharing ? "hourglass" : "share-social"}
108
- size="md"
109
- color="onPrimary"
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
- {onSave && (
118
- <TouchableOpacity
119
- style={[styles.button, styles.saveButton]}
120
- onPress={onSave}
121
- disabled={isSaving}
122
- >
123
- <AtomicIcon
124
- name={isSaving ? "hourglass" : "download"}
125
- size="md"
126
- color="primary"
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> = ({ title, date }) => {
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(() => StyleSheet.create({
24
- container: {
25
- alignItems: "center",
26
- paddingHorizontal: 24,
27
- marginBottom: 20,
28
- },
29
- title: {
30
- fontSize: 24,
31
- lineHeight: 32,
32
- fontWeight: "800",
33
- color: tokens.colors.textPrimary,
34
- textAlign: "center",
35
- marginBottom: 12,
36
- },
37
- badge: {
38
- flexDirection: "row",
39
- alignItems: "center",
40
- gap: 6,
41
- paddingHorizontal: 14,
42
- paddingVertical: 6,
43
- backgroundColor: tokens.colors.primaryContainer,
44
- borderRadius: 16,
45
- },
46
- dateText: {
47
- fontSize: 12,
48
- fontWeight: "600",
49
- color: tokens.colors.primary,
50
- },
51
- }), [tokens]);
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 && <AtomicText style={styles.title}>{title}</AtomicText>}
58
- {date && (
88
+ {cfg.showTitle && title && (
89
+ <AtomicText style={styles.title}>{title}</AtomicText>
90
+ )}
91
+ {cfg.showDate && date && (
59
92
  <View style={styles.badge}>
60
- <AtomicIcon
61
- name="calendar-outline"
62
- size="sm"
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 styles = useMemo(() => StyleSheet.create({
27
- container: {
28
- paddingHorizontal: 20,
29
- marginBottom: 20,
30
- },
31
- frame: {
32
- borderRadius: 20,
33
- overflow: "hidden",
34
- backgroundColor: tokens.colors.surface,
35
- },
36
- image: {
37
- width: "100%",
38
- aspectRatio: 1,
39
- },
40
- badge: {
41
- position: "absolute",
42
- top: 12,
43
- right: 12,
44
- flexDirection: "row",
45
- alignItems: "center",
46
- gap: 4,
47
- paddingHorizontal: 10,
48
- paddingVertical: 5,
49
- backgroundColor: "rgba(0, 0, 0, 0.6)",
50
- borderRadius: 12,
51
- },
52
- badgeText: {
53
- fontSize: 10,
54
- fontWeight: "700",
55
- color: "#FFFFFF",
56
- letterSpacing: 0.5,
57
- },
58
- }), [tokens]);
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 source={{ uri: imageUrl }} style={styles.image} resizeMode="cover" />
64
- <View style={styles.badge}>
65
- <AtomicIcon name="sparkles" size="xs" color="onPrimary" />
66
- <AtomicText style={styles.badgeText}>{badgeText}</AtomicText>
67
- </View>
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> = ({ story }) => {
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 styles = useMemo(() => StyleSheet.create({
23
- outer: {
24
- paddingHorizontal: 20,
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
- borderWidth: 1,
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
- return (
58
- <View style={styles.outer}>
59
- <LinearGradient
60
- colors={[tokens.colors.primaryContainer, tokens.colors.surface]}
61
- style={styles.container}
62
- >
63
- <AtomicText style={styles.quoteIcon}>&quot;</AtomicText>
64
- <AtomicText style={styles.text}>{story}</AtomicText>
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}>&quot;</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
  &quot;
68
99
  </AtomicText>
69
100
  </View>
70
- </LinearGradient>
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
+ };