@umituz/react-native-ai-generation-content 1.36.2 → 1.37.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.
Files changed (22) hide show
  1. package/package.json +2 -2
  2. package/src/domains/creations/infrastructure/repositories/CreationsWriter.ts +124 -199
  3. package/src/domains/creations/presentation/components/GalleryResultPreview.tsx +88 -0
  4. package/src/domains/creations/presentation/hooks/useGalleryCallbacks.ts +127 -0
  5. package/src/domains/creations/presentation/screens/CreationsGalleryScreen.tsx +37 -123
  6. package/src/domains/generation/wizard/presentation/components/GenericWizardFlow.tsx +5 -231
  7. package/src/domains/generation/wizard/presentation/components/WizardContinueButton.tsx +73 -0
  8. package/src/domains/generation/wizard/presentation/components/WizardFlowContent.tsx +181 -0
  9. package/src/domains/generation/wizard/presentation/components/WizardStepRenderer.tsx +18 -134
  10. package/src/domains/generation/wizard/presentation/components/step-renderers/renderPhotoUploadStep.tsx +52 -0
  11. package/src/domains/generation/wizard/presentation/components/step-renderers/renderSelectionStep.tsx +59 -0
  12. package/src/domains/generation/wizard/presentation/components/step-renderers/renderTextInputStep.tsx +62 -0
  13. package/src/domains/generation/wizard/presentation/hooks/useWizardFlowHandlers.ts +133 -0
  14. package/src/domains/generation/wizard/presentation/screens/GenericPhotoUploadScreen.tsx +15 -83
  15. package/src/domains/generation/wizard/presentation/screens/SelectionScreen.tsx +55 -134
  16. package/src/domains/result-preview/presentation/components/GenerationErrorScreen.tsx +19 -122
  17. package/src/domains/result-preview/presentation/hooks/useResultActions.ts +16 -131
  18. package/src/domains/scenarios/presentation/components/ScenarioContinueButton.tsx +76 -0
  19. package/src/domains/scenarios/presentation/hooks/useHierarchicalScenarios.ts +70 -0
  20. package/src/domains/scenarios/presentation/screens/HierarchicalScenarioListScreen.tsx +37 -170
  21. package/src/presentation/hooks/generation/moderation-handler.ts +77 -0
  22. package/src/presentation/hooks/generation/orchestrator.ts +33 -125
@@ -1,17 +1,14 @@
1
1
  /**
2
2
  * Generic Photo Upload Screen
3
3
  * Used by wizard domain for ANY photo upload step
4
- * NO feature-specific concepts (no partner, couple, etc.)
5
- * Works for: couple features, face-swap, image-to-video, ANY photo upload need
6
4
  */
7
5
 
8
6
  import React, { useMemo } from "react";
9
- import { View, TouchableOpacity, StyleSheet } from "react-native";
7
+ import { View, StyleSheet } from "react-native";
10
8
  import {
11
9
  useAppDesignTokens,
12
10
  ScreenLayout,
13
11
  AtomicText,
14
- AtomicIcon,
15
12
  NavigationHeader,
16
13
  InfoGrid,
17
14
  type DesignTokens,
@@ -20,6 +17,7 @@ import {
20
17
  import { PhotoUploadCard } from "../../../../../presentation/components";
21
18
  import type { UploadedImage } from "../../../../../presentation/hooks/generation/useAIGenerateState";
22
19
  import { usePhotoUploadState } from "../hooks/usePhotoUploadState";
20
+ import { WizardContinueButton } from "../components/WizardContinueButton";
23
21
 
24
22
  export interface PhotoUploadScreenTranslations {
25
23
  readonly title: string;
@@ -51,10 +49,7 @@ export interface PhotoUploadScreenProps {
51
49
  readonly stepId?: string;
52
50
  }
53
51
 
54
- const DEFAULT_CONFIG: PhotoUploadScreenConfig = {
55
- showPhotoTips: true,
56
- maxFileSizeMB: 10,
57
- };
52
+ const DEFAULT_CONFIG: PhotoUploadScreenConfig = { showPhotoTips: true, maxFileSizeMB: 10 };
58
53
 
59
54
  export const GenericPhotoUploadScreen: React.FC<PhotoUploadScreenProps> = ({
60
55
  translations,
@@ -66,7 +61,6 @@ export const GenericPhotoUploadScreen: React.FC<PhotoUploadScreenProps> = ({
66
61
  stepId,
67
62
  }) => {
68
63
  const tokens = useAppDesignTokens();
69
-
70
64
  const { image, handlePickImage, canContinue } = usePhotoUploadState({
71
65
  config: { maxFileSizeMB: config.maxFileSizeMB },
72
66
  translations: {
@@ -87,7 +81,6 @@ export const GenericPhotoUploadScreen: React.FC<PhotoUploadScreenProps> = ({
87
81
  const styles = useMemo(() => createStyles(tokens), [tokens]);
88
82
  const showPhotoTips = config.showPhotoTips ?? true;
89
83
 
90
- // Build photo tips items from translations
91
84
  const photoTipsItems: InfoGridItem[] = useMemo(() => {
92
85
  const tipKeys = [
93
86
  { key: "photoUpload.tips.clearFace", icon: "happy-outline" },
@@ -95,10 +88,7 @@ export const GenericPhotoUploadScreen: React.FC<PhotoUploadScreenProps> = ({
95
88
  { key: "photoUpload.tips.recentPhoto", icon: "time-outline" },
96
89
  { key: "photoUpload.tips.noFilters", icon: "image-outline" },
97
90
  ];
98
- return tipKeys.map(({ key, icon }) => ({
99
- text: t(key),
100
- icon,
101
- }));
91
+ return tipKeys.map(({ key, icon }) => ({ text: t(key), icon }));
102
92
  }, [t]);
103
93
 
104
94
  return (
@@ -107,33 +97,11 @@ export const GenericPhotoUploadScreen: React.FC<PhotoUploadScreenProps> = ({
107
97
  title={translations.title}
108
98
  onBackPress={onBack}
109
99
  rightElement={
110
- <TouchableOpacity
100
+ <WizardContinueButton
101
+ canContinue={canContinue && !!image}
111
102
  onPress={handleContinuePress}
112
- activeOpacity={0.7}
113
- disabled={!canContinue || !image}
114
- style={[
115
- styles.continueButton,
116
- {
117
- backgroundColor: canContinue && image ? tokens.colors.primary : tokens.colors.surfaceVariant,
118
- opacity: canContinue && image ? 1 : 0.5,
119
- },
120
- ]}
121
- >
122
- <AtomicText
123
- type="bodyMedium"
124
- style={[
125
- styles.continueText,
126
- { color: canContinue && image ? tokens.colors.onPrimary : tokens.colors.textSecondary },
127
- ]}
128
- >
129
- {translations.continue}
130
- </AtomicText>
131
- <AtomicIcon
132
- name="chevron-forward-outline"
133
- size="sm"
134
- color={canContinue && image ? "onPrimary" : "textSecondary"}
135
- />
136
- </TouchableOpacity>
103
+ label={translations.continue}
104
+ />
137
105
  }
138
106
  />
139
107
  <ScreenLayout
@@ -148,15 +116,9 @@ export const GenericPhotoUploadScreen: React.FC<PhotoUploadScreenProps> = ({
148
116
  {translations.subtitle}
149
117
  </AtomicText>
150
118
 
151
- {/* Photo Tips - InfoGrid version */}
152
119
  {showPhotoTips && (
153
120
  <View style={styles.tipsContainer}>
154
- <InfoGrid
155
- items={photoTipsItems}
156
- columns={2}
157
- title={t("photoUpload.tips.title")}
158
- headerIcon="bulb-outline"
159
- />
121
+ <InfoGrid items={photoTipsItems} columns={2} title={t("photoUpload.tips.title")} headerIcon="bulb-outline" />
160
122
  </View>
161
123
  )}
162
124
 
@@ -175,10 +137,7 @@ export const GenericPhotoUploadScreen: React.FC<PhotoUploadScreenProps> = ({
175
137
 
176
138
  {translations.aiDisclosure && (
177
139
  <View style={styles.disclosureContainer}>
178
- <AtomicText
179
- type="labelSmall"
180
- style={[styles.disclosureText, { color: tokens.colors.textSecondary }]}
181
- >
140
+ <AtomicText type="labelSmall" style={[styles.disclosureText, { color: tokens.colors.textSecondary }]}>
182
141
  {translations.aiDisclosure}
183
142
  </AtomicText>
184
143
  </View>
@@ -190,33 +149,10 @@ export const GenericPhotoUploadScreen: React.FC<PhotoUploadScreenProps> = ({
190
149
 
191
150
  const createStyles = (tokens: DesignTokens) =>
192
151
  StyleSheet.create({
193
- container: {
194
- flex: 1,
195
- },
196
- scrollContent: {
197
- paddingBottom: 40,
198
- },
199
- subtitle: {
200
- fontSize: 16,
201
- textAlign: "center",
202
- marginHorizontal: 24,
203
- marginBottom: 24,
204
- },
205
- tipsContainer: {
206
- marginHorizontal: 24,
207
- marginBottom: 20,
208
- },
209
- continueButton: {
210
- flexDirection: "row",
211
- alignItems: "center",
212
- paddingHorizontal: tokens.spacing.md,
213
- paddingVertical: tokens.spacing.xs,
214
- borderRadius: tokens.borders.radius.full,
215
- },
216
- continueText: {
217
- fontWeight: "800",
218
- marginRight: 4,
219
- },
152
+ container: { flex: 1 },
153
+ scrollContent: { paddingBottom: 40 },
154
+ subtitle: { fontSize: 16, textAlign: "center", marginHorizontal: 24, marginBottom: 24 },
155
+ tipsContainer: { marginHorizontal: 24, marginBottom: 20 },
220
156
  disclosureContainer: {
221
157
  marginTop: 24,
222
158
  marginHorizontal: 24,
@@ -224,9 +160,5 @@ const createStyles = (tokens: DesignTokens) =>
224
160
  borderRadius: 12,
225
161
  backgroundColor: tokens.colors.surfaceVariant + "40",
226
162
  },
227
- disclosureText: {
228
- textAlign: "center",
229
- lineHeight: 18,
230
- opacity: 0.8,
231
- },
163
+ disclosureText: { textAlign: "center", lineHeight: 18, opacity: 0.8 },
232
164
  });
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * SelectionScreen
3
3
  * Generic selection step for wizard flows (duration, style, etc.)
4
- * Uses design system: NavigationHeader + ScreenLayout
5
4
  */
6
5
 
7
6
  import React, { useState, useCallback, useMemo } from "react";
@@ -14,7 +13,8 @@ import {
14
13
  NavigationHeader,
15
14
  type DesignTokens,
16
15
  } from "@umituz/react-native-design-system";
17
- import type { SelectionScreenProps } from "./SelectionScreen.types";
16
+ import type { SelectionScreenProps, SelectionOption } from "./SelectionScreen.types";
17
+ import { WizardContinueButton } from "../components/WizardContinueButton";
18
18
 
19
19
  export type {
20
20
  SelectionOption,
@@ -41,129 +41,74 @@ export const SelectionScreen: React.FC<SelectionScreenProps> = ({
41
41
 
42
42
  const isMultiSelect = config?.multiSelect ?? false;
43
43
  const isRequired = config?.required ?? true;
44
-
45
44
  const canContinue = isRequired
46
- ? isMultiSelect
47
- ? (selected as string[]).length > 0
48
- : selected !== ""
45
+ ? isMultiSelect ? (selected as string[]).length > 0 : selected !== ""
49
46
  : true;
50
47
 
51
- const handleSelect = useCallback(
52
- (optionId: string) => {
53
- if (isMultiSelect) {
54
- setSelected((prev) => {
55
- const arr = prev as string[];
56
- return arr.includes(optionId)
57
- ? arr.filter((id) => id !== optionId)
58
- : [...arr, optionId];
59
- });
60
- } else {
61
- setSelected(optionId);
62
- }
63
- },
64
- [isMultiSelect],
65
- );
48
+ const handleSelect = useCallback((optionId: string) => {
49
+ if (isMultiSelect) {
50
+ setSelected((prev) => {
51
+ const arr = prev as string[];
52
+ return arr.includes(optionId) ? arr.filter((id) => id !== optionId) : [...arr, optionId];
53
+ });
54
+ } else {
55
+ setSelected(optionId);
56
+ }
57
+ }, [isMultiSelect]);
66
58
 
67
59
  const handleContinue = useCallback(() => {
68
- if (canContinue) {
69
- onContinue(selected);
70
- }
60
+ if (canContinue) onContinue(selected);
71
61
  }, [canContinue, selected, onContinue]);
72
62
 
73
63
  const isOptionSelected = useCallback(
74
- (optionId: string): boolean => {
75
- if (isMultiSelect) {
76
- return (selected as string[]).includes(optionId);
77
- }
78
- return selected === optionId;
79
- },
80
- [isMultiSelect, selected],
64
+ (optionId: string): boolean =>
65
+ isMultiSelect ? (selected as string[]).includes(optionId) : selected === optionId,
66
+ [isMultiSelect, selected]
81
67
  );
82
68
 
83
69
  const styles = useMemo(() => createStyles(tokens), [tokens]);
84
70
 
71
+ const renderOption = (option: SelectionOption) => {
72
+ const isSelected = isOptionSelected(option.id);
73
+ return (
74
+ <TouchableOpacity
75
+ key={option.id}
76
+ style={[
77
+ styles.optionCard,
78
+ {
79
+ backgroundColor: isSelected ? tokens.colors.primaryContainer : tokens.colors.backgroundSecondary,
80
+ borderColor: isSelected ? tokens.colors.primary : tokens.colors.border,
81
+ },
82
+ ]}
83
+ onPress={() => handleSelect(option.id)}
84
+ activeOpacity={0.7}
85
+ >
86
+ {option.icon ? <AtomicIcon name={option.icon} size="lg" color={isSelected ? "primary" : "textSecondary"} /> : null}
87
+ <AtomicText type="labelLarge" color={isSelected ? "primary" : "textPrimary"} style={styles.optionLabel}>
88
+ {option.label}
89
+ </AtomicText>
90
+ {isSelected ? (
91
+ <View style={[styles.checkmark, { backgroundColor: tokens.colors.primary }]}>
92
+ <AtomicIcon name="checkmark" size="xs" color="onPrimary" />
93
+ </View>
94
+ ) : null}
95
+ </TouchableOpacity>
96
+ );
97
+ };
98
+
85
99
  return (
86
100
  <View style={{ flex: 1, backgroundColor: tokens.colors.backgroundPrimary }}>
87
101
  <NavigationHeader
88
102
  title=""
89
103
  onBackPress={onBack}
90
104
  rightElement={
91
- <TouchableOpacity
92
- onPress={handleContinue}
93
- activeOpacity={0.7}
94
- disabled={!canContinue}
95
- style={[
96
- styles.continueButton,
97
- {
98
- backgroundColor: canContinue ? tokens.colors.primary : tokens.colors.surfaceVariant,
99
- opacity: canContinue ? 1 : 0.5,
100
- },
101
- ]}
102
- >
103
- <AtomicText
104
- type="bodyMedium"
105
- style={[
106
- styles.continueText,
107
- { color: canContinue ? tokens.colors.onPrimary : tokens.colors.textSecondary },
108
- ]}
109
- >
110
- {translations.continueButton}
111
- </AtomicText>
112
- <AtomicIcon
113
- name="arrow-forward"
114
- size="sm"
115
- color={canContinue ? "onPrimary" : "textSecondary"}
116
- />
117
- </TouchableOpacity>
105
+ <WizardContinueButton canContinue={canContinue} onPress={handleContinue} label={translations.continueButton} icon="arrow-forward" />
118
106
  }
119
107
  />
120
- <ScreenLayout
121
- scrollable={true}
122
- edges={["left", "right"]}
123
- hideScrollIndicator={true}
124
- contentContainerStyle={styles.scrollContent}
125
- >
126
- <AtomicText type="headlineMedium" color="textPrimary" style={styles.title}>
127
- {translations.title}
128
- </AtomicText>
129
-
130
- {translations.subtitle ? (
131
- <AtomicText type="bodyMedium" color="textSecondary" style={styles.subtitle}>
132
- {translations.subtitle}
133
- </AtomicText>
134
- ) : null}
135
-
136
- <View style={styles.optionsGrid}>
137
- {options.map((option) => {
138
- const isSelected = isOptionSelected(option.id);
139
- return (
140
- <TouchableOpacity
141
- key={option.id}
142
- style={[
143
- styles.optionCard,
144
- {
145
- backgroundColor: isSelected ? tokens.colors.primaryContainer : tokens.colors.backgroundSecondary,
146
- borderColor: isSelected ? tokens.colors.primary : tokens.colors.border,
147
- },
148
- ]}
149
- onPress={() => handleSelect(option.id)}
150
- activeOpacity={0.7}
151
- >
152
- {option.icon ? (
153
- <AtomicIcon name={option.icon} size="lg" color={isSelected ? "primary" : "textSecondary"} />
154
- ) : null}
155
- <AtomicText type="labelLarge" color={isSelected ? "primary" : "textPrimary"} style={styles.optionLabel}>
156
- {option.label}
157
- </AtomicText>
158
- {isSelected ? (
159
- <View style={[styles.checkmark, { backgroundColor: tokens.colors.primary }]}>
160
- <AtomicIcon name="checkmark" size="xs" color="onPrimary" />
161
- </View>
162
- ) : null}
163
- </TouchableOpacity>
164
- );
165
- })}
166
- </View>
108
+ <ScreenLayout scrollable={true} edges={["left", "right"]} hideScrollIndicator={true} contentContainerStyle={styles.scrollContent}>
109
+ <AtomicText type="headlineMedium" color="textPrimary" style={styles.title}>{translations.title}</AtomicText>
110
+ {translations.subtitle ? <AtomicText type="bodyMedium" color="textSecondary" style={styles.subtitle}>{translations.subtitle}</AtomicText> : null}
111
+ <View style={styles.optionsGrid}>{options.map(renderOption)}</View>
167
112
  </ScreenLayout>
168
113
  </View>
169
114
  );
@@ -171,21 +116,10 @@ export const SelectionScreen: React.FC<SelectionScreenProps> = ({
171
116
 
172
117
  const createStyles = (tokens: DesignTokens) =>
173
118
  StyleSheet.create({
174
- scrollContent: {
175
- paddingHorizontal: tokens.spacing.lg,
176
- paddingBottom: 40,
177
- },
178
- title: {
179
- marginBottom: tokens.spacing.sm,
180
- },
181
- subtitle: {
182
- marginBottom: tokens.spacing.lg,
183
- },
184
- optionsGrid: {
185
- flexDirection: "row",
186
- flexWrap: "wrap",
187
- gap: tokens.spacing.sm,
188
- },
119
+ scrollContent: { paddingHorizontal: tokens.spacing.lg, paddingBottom: 40 },
120
+ title: { marginBottom: tokens.spacing.sm },
121
+ subtitle: { marginBottom: tokens.spacing.lg },
122
+ optionsGrid: { flexDirection: "row", flexWrap: "wrap", gap: tokens.spacing.sm },
189
123
  optionCard: {
190
124
  flex: 1,
191
125
  minWidth: "45%",
@@ -197,9 +131,7 @@ const createStyles = (tokens: DesignTokens) =>
197
131
  gap: tokens.spacing.xs,
198
132
  position: "relative",
199
133
  },
200
- optionLabel: {
201
- textAlign: "center",
202
- },
134
+ optionLabel: { textAlign: "center" },
203
135
  checkmark: {
204
136
  position: "absolute",
205
137
  top: tokens.spacing.xs,
@@ -210,15 +142,4 @@ const createStyles = (tokens: DesignTokens) =>
210
142
  alignItems: "center",
211
143
  justifyContent: "center",
212
144
  },
213
- continueButton: {
214
- flexDirection: "row",
215
- alignItems: "center",
216
- paddingHorizontal: tokens.spacing.md,
217
- paddingVertical: tokens.spacing.xs,
218
- borderRadius: tokens.borders.radius.full,
219
- },
220
- continueText: {
221
- fontWeight: "800",
222
- marginRight: 4,
223
- },
224
145
  });
@@ -14,59 +14,31 @@ import {
14
14
  type DesignTokens,
15
15
  } from "@umituz/react-native-design-system";
16
16
 
17
- /**
18
- * Error screen translations
19
- */
20
17
  export interface GenerationErrorTranslations {
21
- /** Error title */
22
18
  readonly title: string;
23
- /** Try again button */
24
19
  readonly tryAgain: string;
25
- /** Choose another button */
26
20
  readonly chooseAnother: string;
27
- /** Credit info message */
28
21
  readonly noCreditCharged: string;
29
22
  }
30
23
 
31
- /**
32
- * Error screen config
33
- */
34
24
  export interface GenerationErrorConfig {
35
- /** Show credit info message */
36
25
  readonly showCreditInfo?: boolean;
37
- /** Icon name */
38
26
  readonly iconName?: string;
39
- /** Icon size */
40
27
  readonly iconSize?: number;
41
28
  }
42
29
 
43
- /**
44
- * Error screen props
45
- */
46
30
  export interface GenerationErrorScreenProps {
47
- /** Error message to display */
48
31
  readonly errorMessage?: string;
49
- /** Translations */
50
32
  readonly translations: GenerationErrorTranslations;
51
- /** Configuration */
52
33
  readonly config?: GenerationErrorConfig;
53
- /** Try again callback */
54
34
  readonly onTryAgain: () => void;
55
- /** Choose another scenario callback */
56
35
  readonly onChooseAnother: () => void;
57
- /** Optional premium button text */
58
36
  readonly premiumButtonText?: string;
59
- /** Optional premium button callback */
60
37
  readonly onGoPremium?: () => void;
61
- /** Optional custom style */
62
38
  readonly style?: StyleProp<ViewStyle>;
63
39
  }
64
40
 
65
- const DEFAULT_CONFIG: GenerationErrorConfig = {
66
- showCreditInfo: true,
67
- iconName: "alert-circle",
68
- iconSize: 56,
69
- };
41
+ const DEFAULT_CONFIG: GenerationErrorConfig = { showCreditInfo: true, iconName: "alert-circle", iconSize: 56 };
70
42
 
71
43
  export const GenerationErrorScreen: React.FC<GenerationErrorScreenProps> = ({
72
44
  errorMessage,
@@ -83,43 +55,19 @@ export const GenerationErrorScreen: React.FC<GenerationErrorScreenProps> = ({
83
55
  const mergedConfig = { ...DEFAULT_CONFIG, ...config };
84
56
 
85
57
  return (
86
- <View
87
- style={[
88
- styles.container,
89
- { backgroundColor: tokens.colors.backgroundPrimary },
90
- style,
91
- ]}
92
- >
93
- <ScreenLayout
94
- scrollable={false}
95
- edges={["top", "bottom"]}
96
- backgroundColor="transparent"
97
- >
58
+ <View style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }, style]}>
59
+ <ScreenLayout scrollable={false} edges={["top", "bottom"]} backgroundColor="transparent">
98
60
  <View style={styles.content}>
99
61
  <View style={styles.iconContainer}>
100
- <AtomicIcon
101
- name={mergedConfig.iconName || "alert-circle"}
102
- size={mergedConfig.iconSize || 56}
103
- customColor={tokens.colors.error}
104
- />
62
+ <AtomicIcon name={mergedConfig.iconName || "alert-circle"} size={mergedConfig.iconSize || 56} customColor={tokens.colors.error} />
105
63
  </View>
106
-
107
64
  <AtomicText style={styles.title}>{translations.title}</AtomicText>
108
-
109
- <AtomicText style={styles.message}>
110
- {errorMessage || translations.title}
111
- </AtomicText>
65
+ <AtomicText style={styles.message}>{errorMessage || translations.title}</AtomicText>
112
66
 
113
67
  {mergedConfig.showCreditInfo && (
114
68
  <View style={styles.infoContainer}>
115
- <AtomicIcon
116
- name="information-circle-outline"
117
- size={16}
118
- customColor={tokens.colors.textSecondary}
119
- />
120
- <AtomicText style={styles.infoText}>
121
- {translations.noCreditCharged}
122
- </AtomicText>
69
+ <AtomicIcon name="information-circle-outline" size={16} customColor={tokens.colors.textSecondary} />
70
+ <AtomicText style={styles.infoText}>{translations.noCreditCharged}</AtomicText>
123
71
  </View>
124
72
  )}
125
73
 
@@ -127,15 +75,8 @@ export const GenerationErrorScreen: React.FC<GenerationErrorScreenProps> = ({
127
75
 
128
76
  <View style={styles.actionsContainer}>
129
77
  {onGoPremium && premiumButtonText && (
130
- <AtomicButton
131
- title={premiumButtonText}
132
- onPress={onGoPremium}
133
- variant="primary"
134
- size="lg"
135
- fullWidth
136
- />
78
+ <AtomicButton title={premiumButtonText} onPress={onGoPremium} variant="primary" size="lg" fullWidth />
137
79
  )}
138
-
139
80
  <AtomicButton
140
81
  title={translations.tryAgain}
141
82
  onPress={onTryAgain}
@@ -143,15 +84,8 @@ export const GenerationErrorScreen: React.FC<GenerationErrorScreenProps> = ({
143
84
  size="lg"
144
85
  fullWidth
145
86
  />
146
-
147
87
  <View style={styles.secondaryButtonContainer}>
148
- <AtomicButton
149
- title={translations.chooseAnother}
150
- onPress={onChooseAnother}
151
- variant="text"
152
- size="md"
153
- fullWidth
154
- />
88
+ <AtomicButton title={translations.chooseAnother} onPress={onChooseAnother} variant="text" size="md" fullWidth />
155
89
  </View>
156
90
  </View>
157
91
  </View>
@@ -162,51 +96,14 @@ export const GenerationErrorScreen: React.FC<GenerationErrorScreenProps> = ({
162
96
 
163
97
  const createStyles = (tokens: DesignTokens) =>
164
98
  StyleSheet.create({
165
- container: {
166
- flex: 1,
167
- },
168
- content: {
169
- flex: 1,
170
- alignItems: "center",
171
- paddingHorizontal: 32,
172
- paddingTop: 80,
173
- paddingBottom: 40,
174
- },
175
- iconContainer: {
176
- marginBottom: 24,
177
- },
178
- title: {
179
- ...tokens.typography.headingMedium,
180
- color: tokens.colors.textPrimary,
181
- fontWeight: "700",
182
- marginBottom: 12,
183
- textAlign: "center",
184
- },
185
- message: {
186
- ...tokens.typography.bodyMedium,
187
- color: tokens.colors.textSecondary,
188
- textAlign: "center",
189
- lineHeight: 22,
190
- marginBottom: 24,
191
- },
192
- infoContainer: {
193
- flexDirection: "row",
194
- alignItems: "center",
195
- gap: 6,
196
- opacity: 0.7,
197
- },
198
- infoText: {
199
- ...tokens.typography.bodySmall,
200
- color: tokens.colors.textSecondary,
201
- },
202
- spacer: {
203
- flex: 1,
204
- },
205
- actionsContainer: {
206
- width: "100%",
207
- gap: 16,
208
- },
209
- secondaryButtonContainer: {
210
- marginTop: 4,
211
- },
99
+ container: { flex: 1 },
100
+ content: { flex: 1, alignItems: "center", paddingHorizontal: 32, paddingTop: 80, paddingBottom: 40 },
101
+ iconContainer: { marginBottom: 24 },
102
+ title: { ...tokens.typography.headingMedium, color: tokens.colors.textPrimary, fontWeight: "700", marginBottom: 12, textAlign: "center" },
103
+ message: { ...tokens.typography.bodyMedium, color: tokens.colors.textSecondary, textAlign: "center", lineHeight: 22, marginBottom: 24 },
104
+ infoContainer: { flexDirection: "row", alignItems: "center", gap: 6, opacity: 0.7 },
105
+ infoText: { ...tokens.typography.bodySmall, color: tokens.colors.textSecondary },
106
+ spacer: { flex: 1 },
107
+ actionsContainer: { width: "100%", gap: 16 },
108
+ secondaryButtonContainer: { marginTop: 4 },
212
109
  });