@umituz/react-native-ai-generation-content 1.26.10 → 1.26.12
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/generation/wizard/index.ts +7 -0
- package/src/domains/generation/wizard/presentation/components/GenericWizardFlow.tsx +293 -0
- package/src/domains/generation/wizard/presentation/components/index.ts +2 -0
- package/src/domains/generation/wizard/presentation/screens/GeneratingScreen.tsx +123 -0
- package/src/domains/generation/wizard/presentation/screens/index.ts +1 -0
- package/src/domains/scenarios/domain/scenario.types.ts +39 -0
- package/src/domains/scenarios/index.ts +23 -0
- package/src/domains/scenarios/presentation/containers/CategoryNavigationContainer.tsx +191 -0
- package/src/domains/scenarios/presentation/containers/index.ts +2 -0
- package/src/domains/scenarios/presentation/screens/HierarchicalScenarioListScreen.tsx +291 -0
- package/src/domains/scenarios/presentation/screens/MainCategoryScreen.tsx +198 -0
- package/src/domains/scenarios/presentation/screens/ScenarioPreviewScreen.tsx +164 -0
- package/src/domains/scenarios/presentation/screens/SubCategoryScreen.tsx +216 -0
- package/src/domains/scenarios/presentation/screens/index.ts +6 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HierarchicalScenarioListScreen
|
|
3
|
+
* Displays scenarios filtered by sub-category with optimized performance
|
|
4
|
+
* PERFORMANCE OPTIMIZED: No FlatList key remounting, memoized calculations
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React, { useMemo, useCallback, useState, useEffect } from "react";
|
|
8
|
+
import {
|
|
9
|
+
View,
|
|
10
|
+
FlatList,
|
|
11
|
+
StyleSheet,
|
|
12
|
+
TouchableOpacity,
|
|
13
|
+
type ListRenderItemInfo,
|
|
14
|
+
} from "react-native";
|
|
15
|
+
import {
|
|
16
|
+
AtomicText,
|
|
17
|
+
AtomicCard,
|
|
18
|
+
useAppDesignTokens,
|
|
19
|
+
useResponsive,
|
|
20
|
+
ScreenLayout,
|
|
21
|
+
NavigationHeader,
|
|
22
|
+
AtomicIcon,
|
|
23
|
+
AtomicSpinner,
|
|
24
|
+
type DesignTokens,
|
|
25
|
+
} from "@umituz/react-native-design-system";
|
|
26
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
27
|
+
import type { ScenarioData, ScenarioSubCategory } from "../../domain/scenario.types";
|
|
28
|
+
|
|
29
|
+
export interface HierarchicalScenarioListScreenProps {
|
|
30
|
+
readonly subCategoryId: string;
|
|
31
|
+
readonly subCategories: readonly ScenarioSubCategory[];
|
|
32
|
+
readonly scenarios: readonly ScenarioData[];
|
|
33
|
+
readonly onSelectScenario: (scenarioId: string) => void;
|
|
34
|
+
readonly onBack: () => void;
|
|
35
|
+
readonly t: (key: string) => string;
|
|
36
|
+
readonly numColumns?: number;
|
|
37
|
+
readonly isLoading?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const HierarchicalScenarioListScreen: React.FC<HierarchicalScenarioListScreenProps> = ({
|
|
41
|
+
subCategoryId,
|
|
42
|
+
subCategories,
|
|
43
|
+
scenarios,
|
|
44
|
+
onSelectScenario,
|
|
45
|
+
onBack,
|
|
46
|
+
t,
|
|
47
|
+
numColumns = 2,
|
|
48
|
+
isLoading = false,
|
|
49
|
+
}) => {
|
|
50
|
+
const tokens = useAppDesignTokens();
|
|
51
|
+
const insets = useSafeAreaInsets();
|
|
52
|
+
const { width } = useResponsive();
|
|
53
|
+
|
|
54
|
+
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
55
|
+
|
|
56
|
+
const subCategory = useMemo(
|
|
57
|
+
() => subCategories.find((sub) => sub.id === subCategoryId),
|
|
58
|
+
[subCategories, subCategoryId]
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const filteredScenarios = useMemo(() => {
|
|
62
|
+
if (!subCategory) {
|
|
63
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
64
|
+
console.log("[HierarchicalScenarioListScreen] No subCategory found", {
|
|
65
|
+
subCategoryId,
|
|
66
|
+
subCategoriesCount: subCategories.length,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const filtered = scenarios.filter((scenario) => {
|
|
73
|
+
if (!scenario.category) return false;
|
|
74
|
+
return subCategory.scenarioCategories?.includes(scenario.category) ?? false;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
78
|
+
console.log("[HierarchicalScenarioListScreen] Filtered scenarios", {
|
|
79
|
+
subCategoryId: subCategory.id,
|
|
80
|
+
scenarioCategories: subCategory.scenarioCategories,
|
|
81
|
+
totalScenarios: scenarios.length,
|
|
82
|
+
filteredCount: filtered.length,
|
|
83
|
+
sampleScenarioCategories: scenarios.slice(0, 5).map(s => s.category),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return filtered;
|
|
88
|
+
}, [scenarios, subCategory, subCategoryId, subCategories]);
|
|
89
|
+
|
|
90
|
+
// Debug: Monitor component state
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
93
|
+
console.log("[HierarchicalScenarioListScreen] Component state", {
|
|
94
|
+
subCategoryId,
|
|
95
|
+
hasSubCategory: !!subCategory,
|
|
96
|
+
filteredScenariosCount: filteredScenarios.length,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}, [subCategoryId, subCategory, filteredScenarios]);
|
|
100
|
+
|
|
101
|
+
const horizontalPadding = tokens.spacing.md;
|
|
102
|
+
const cardSpacing = tokens.spacing.md;
|
|
103
|
+
|
|
104
|
+
// Calculate card width once - memoized to prevent unnecessary recalculations
|
|
105
|
+
const cardWidth = useMemo(() => {
|
|
106
|
+
const availableWidth = width - horizontalPadding * 2 - cardSpacing;
|
|
107
|
+
return availableWidth / numColumns;
|
|
108
|
+
}, [width, horizontalPadding, cardSpacing, numColumns]);
|
|
109
|
+
|
|
110
|
+
const styles = useMemo(
|
|
111
|
+
() => createStyles(tokens, cardSpacing, horizontalPadding),
|
|
112
|
+
[tokens, cardSpacing, horizontalPadding]
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const handleContinue = useCallback(() => {
|
|
116
|
+
if (selectedId) {
|
|
117
|
+
onSelectScenario(selectedId);
|
|
118
|
+
}
|
|
119
|
+
}, [selectedId, onSelectScenario]);
|
|
120
|
+
|
|
121
|
+
// Memoized callback for card selection - prevents inline arrow functions
|
|
122
|
+
const handleCardPress = useCallback((itemId: string) => {
|
|
123
|
+
setSelectedId(itemId);
|
|
124
|
+
}, []);
|
|
125
|
+
|
|
126
|
+
const renderItem = useCallback(
|
|
127
|
+
({ item }: ListRenderItemInfo<ScenarioData>) => {
|
|
128
|
+
const title = t(`scenario.${item.id}.title`);
|
|
129
|
+
const description = t(`scenario.${item.id}.description`);
|
|
130
|
+
const isSelected = selectedId === item.id;
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<AtomicCard
|
|
134
|
+
image={item.previewImageUrl || item.imageUrl || ""}
|
|
135
|
+
title={title}
|
|
136
|
+
subtitle={description}
|
|
137
|
+
imageAspectRatio={1.25}
|
|
138
|
+
selected={isSelected}
|
|
139
|
+
style={{ width: cardWidth }}
|
|
140
|
+
onPress={() => handleCardPress(item.id)}
|
|
141
|
+
testID={`scenario-card-${item.id}`}
|
|
142
|
+
/>
|
|
143
|
+
);
|
|
144
|
+
},
|
|
145
|
+
[cardWidth, selectedId, t, handleCardPress]
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const ListEmptyComponent = useMemo(
|
|
149
|
+
() => (
|
|
150
|
+
<View style={styles.emptyState}>
|
|
151
|
+
<AtomicText type="bodyLarge" color="textSecondary">
|
|
152
|
+
{t("scenario.list.empty")}
|
|
153
|
+
</AtomicText>
|
|
154
|
+
</View>
|
|
155
|
+
),
|
|
156
|
+
[t, styles.emptyState]
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const LoadingComponent = useMemo(
|
|
160
|
+
() => (
|
|
161
|
+
<View style={styles.loadingContainer}>
|
|
162
|
+
<AtomicSpinner size="lg" color="primary" />
|
|
163
|
+
<AtomicText type="bodyMedium" style={{ marginTop: tokens.spacing.md }}>
|
|
164
|
+
{t("common.loading")}
|
|
165
|
+
</AtomicText>
|
|
166
|
+
</View>
|
|
167
|
+
),
|
|
168
|
+
[tokens, t, styles.loadingContainer]
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
if (!subCategory) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const canContinue = !!selectedId;
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<View style={styles.container}>
|
|
179
|
+
<NavigationHeader
|
|
180
|
+
title={t(subCategory.titleKey)}
|
|
181
|
+
onBackPress={onBack}
|
|
182
|
+
rightElement={
|
|
183
|
+
<TouchableOpacity
|
|
184
|
+
onPress={handleContinue}
|
|
185
|
+
disabled={!canContinue}
|
|
186
|
+
activeOpacity={0.7}
|
|
187
|
+
style={[
|
|
188
|
+
styles.continueButton,
|
|
189
|
+
{
|
|
190
|
+
backgroundColor: canContinue
|
|
191
|
+
? tokens.colors.primary
|
|
192
|
+
: tokens.colors.surfaceVariant,
|
|
193
|
+
opacity: canContinue ? 1 : 0.5,
|
|
194
|
+
},
|
|
195
|
+
]}
|
|
196
|
+
>
|
|
197
|
+
<AtomicText
|
|
198
|
+
type="bodyMedium"
|
|
199
|
+
style={[
|
|
200
|
+
styles.continueText,
|
|
201
|
+
{
|
|
202
|
+
color: canContinue
|
|
203
|
+
? tokens.colors.onPrimary
|
|
204
|
+
: tokens.colors.textSecondary,
|
|
205
|
+
},
|
|
206
|
+
]}
|
|
207
|
+
>
|
|
208
|
+
{t("common.continue")}
|
|
209
|
+
</AtomicText>
|
|
210
|
+
<AtomicIcon
|
|
211
|
+
name="arrow-forward"
|
|
212
|
+
size="sm"
|
|
213
|
+
color={canContinue ? "onPrimary" : "textSecondary"}
|
|
214
|
+
/>
|
|
215
|
+
</TouchableOpacity>
|
|
216
|
+
}
|
|
217
|
+
/>
|
|
218
|
+
<ScreenLayout
|
|
219
|
+
scrollable={false}
|
|
220
|
+
edges={["left", "right"]}
|
|
221
|
+
backgroundColor={tokens.colors.backgroundPrimary}
|
|
222
|
+
>
|
|
223
|
+
<FlatList
|
|
224
|
+
data={filteredScenarios}
|
|
225
|
+
numColumns={numColumns}
|
|
226
|
+
showsVerticalScrollIndicator={false}
|
|
227
|
+
columnWrapperStyle={styles.row}
|
|
228
|
+
renderItem={renderItem}
|
|
229
|
+
keyExtractor={(item) => item.id}
|
|
230
|
+
ListEmptyComponent={
|
|
231
|
+
isLoading
|
|
232
|
+
? LoadingComponent
|
|
233
|
+
: (filteredScenarios.length === 0 ? ListEmptyComponent : null)
|
|
234
|
+
}
|
|
235
|
+
contentContainerStyle={[
|
|
236
|
+
styles.listContent,
|
|
237
|
+
{ paddingBottom: insets.bottom + 100 },
|
|
238
|
+
]}
|
|
239
|
+
removeClippedSubviews
|
|
240
|
+
maxToRenderPerBatch={10}
|
|
241
|
+
updateCellsBatchingPeriod={50}
|
|
242
|
+
initialNumToRender={10}
|
|
243
|
+
windowSize={21}
|
|
244
|
+
/>
|
|
245
|
+
</ScreenLayout>
|
|
246
|
+
</View>
|
|
247
|
+
);
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const createStyles = (
|
|
251
|
+
tokens: DesignTokens,
|
|
252
|
+
cardSpacing: number,
|
|
253
|
+
horizontalPadding: number
|
|
254
|
+
) =>
|
|
255
|
+
StyleSheet.create({
|
|
256
|
+
container: {
|
|
257
|
+
flex: 1,
|
|
258
|
+
},
|
|
259
|
+
listContent: {
|
|
260
|
+
paddingTop: tokens.spacing.sm,
|
|
261
|
+
flexGrow: 1,
|
|
262
|
+
},
|
|
263
|
+
row: {
|
|
264
|
+
gap: cardSpacing,
|
|
265
|
+
marginBottom: cardSpacing,
|
|
266
|
+
paddingHorizontal: horizontalPadding,
|
|
267
|
+
},
|
|
268
|
+
emptyState: {
|
|
269
|
+
flex: 1,
|
|
270
|
+
justifyContent: "center",
|
|
271
|
+
alignItems: "center",
|
|
272
|
+
paddingVertical: tokens.spacing.xl,
|
|
273
|
+
},
|
|
274
|
+
loadingContainer: {
|
|
275
|
+
flex: 1,
|
|
276
|
+
justifyContent: "center",
|
|
277
|
+
alignItems: "center",
|
|
278
|
+
paddingVertical: tokens.spacing.xl,
|
|
279
|
+
},
|
|
280
|
+
continueButton: {
|
|
281
|
+
flexDirection: "row",
|
|
282
|
+
alignItems: "center",
|
|
283
|
+
paddingHorizontal: tokens.spacing.md,
|
|
284
|
+
paddingVertical: tokens.spacing.xs,
|
|
285
|
+
borderRadius: tokens.borders.radius.full,
|
|
286
|
+
},
|
|
287
|
+
continueText: {
|
|
288
|
+
fontWeight: "800",
|
|
289
|
+
marginRight: 4,
|
|
290
|
+
},
|
|
291
|
+
});
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MainCategoryScreen
|
|
3
|
+
* Displays main categories for hierarchical scenario selection
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useMemo, useCallback, useEffect } from "react";
|
|
7
|
+
import {
|
|
8
|
+
View,
|
|
9
|
+
FlatList,
|
|
10
|
+
StyleSheet,
|
|
11
|
+
TouchableOpacity,
|
|
12
|
+
type ListRenderItemInfo,
|
|
13
|
+
} from "react-native";
|
|
14
|
+
import {
|
|
15
|
+
AtomicText,
|
|
16
|
+
AtomicIcon,
|
|
17
|
+
useAppDesignTokens,
|
|
18
|
+
ScreenLayout,
|
|
19
|
+
type DesignTokens,
|
|
20
|
+
} from "@umituz/react-native-design-system";
|
|
21
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
22
|
+
import { AIGenScreenHeader } from "../../../../presentation/components";
|
|
23
|
+
import type { ScenarioMainCategory } from "../../domain/scenario.types";
|
|
24
|
+
|
|
25
|
+
export interface MainCategoryScreenProps {
|
|
26
|
+
readonly mainCategories: readonly ScenarioMainCategory[];
|
|
27
|
+
readonly onSelectCategory: (categoryId: string) => void;
|
|
28
|
+
readonly onBack?: () => void;
|
|
29
|
+
readonly t: (key: string) => string;
|
|
30
|
+
readonly headerTitle?: string;
|
|
31
|
+
readonly headerDescription?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const MainCategoryScreen: React.FC<MainCategoryScreenProps> = ({
|
|
35
|
+
mainCategories,
|
|
36
|
+
onSelectCategory,
|
|
37
|
+
onBack,
|
|
38
|
+
t,
|
|
39
|
+
headerTitle,
|
|
40
|
+
headerDescription,
|
|
41
|
+
}) => {
|
|
42
|
+
const tokens = useAppDesignTokens();
|
|
43
|
+
const insets = useSafeAreaInsets();
|
|
44
|
+
|
|
45
|
+
// Debug: Monitor component state
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
48
|
+
console.log("[MainCategoryScreen] Component mounted/updated", {
|
|
49
|
+
mainCategoriesCount: mainCategories.length,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}, [mainCategories]);
|
|
53
|
+
|
|
54
|
+
const styles = useMemo(() => createStyles(tokens), [tokens]);
|
|
55
|
+
|
|
56
|
+
const handleCategoryPress = useCallback(
|
|
57
|
+
(categoryId: string) => {
|
|
58
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
59
|
+
console.log("[MainCategoryScreen] Category pressed", { categoryId });
|
|
60
|
+
}
|
|
61
|
+
onSelectCategory(categoryId);
|
|
62
|
+
},
|
|
63
|
+
[onSelectCategory]
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const renderItem = useCallback(
|
|
67
|
+
({ item }: ListRenderItemInfo<ScenarioMainCategory>) => {
|
|
68
|
+
const title = t(item.titleKey);
|
|
69
|
+
const description = item.descriptionKey ? t(item.descriptionKey) : "";
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<TouchableOpacity
|
|
73
|
+
style={[
|
|
74
|
+
styles.card,
|
|
75
|
+
{
|
|
76
|
+
backgroundColor: tokens.colors.surface,
|
|
77
|
+
borderColor: tokens.colors.border,
|
|
78
|
+
},
|
|
79
|
+
]}
|
|
80
|
+
onPress={() => handleCategoryPress(item.id)}
|
|
81
|
+
activeOpacity={0.7}
|
|
82
|
+
testID={`main-category-${item.id}`}
|
|
83
|
+
>
|
|
84
|
+
<View style={styles.cardContent}>
|
|
85
|
+
<View
|
|
86
|
+
style={[
|
|
87
|
+
styles.iconContainer,
|
|
88
|
+
{ backgroundColor: tokens.colors.surfaceVariant },
|
|
89
|
+
]}
|
|
90
|
+
>
|
|
91
|
+
{item.emoji ? (
|
|
92
|
+
<AtomicText style={styles.emoji}>{item.emoji}</AtomicText>
|
|
93
|
+
) : (
|
|
94
|
+
<AtomicIcon name={item.icon as never} size="lg" color="primary" />
|
|
95
|
+
)}
|
|
96
|
+
</View>
|
|
97
|
+
<View style={styles.textContent}>
|
|
98
|
+
<AtomicText
|
|
99
|
+
style={[styles.title, { color: tokens.colors.textPrimary }]}
|
|
100
|
+
>
|
|
101
|
+
{title}
|
|
102
|
+
</AtomicText>
|
|
103
|
+
{description ? (
|
|
104
|
+
<AtomicText
|
|
105
|
+
style={[
|
|
106
|
+
styles.description,
|
|
107
|
+
{ color: tokens.colors.textSecondary },
|
|
108
|
+
]}
|
|
109
|
+
numberOfLines={2}
|
|
110
|
+
>
|
|
111
|
+
{description}
|
|
112
|
+
</AtomicText>
|
|
113
|
+
) : null}
|
|
114
|
+
</View>
|
|
115
|
+
<AtomicIcon
|
|
116
|
+
name="chevron-forward"
|
|
117
|
+
size="md"
|
|
118
|
+
color="textSecondary"
|
|
119
|
+
/>
|
|
120
|
+
</View>
|
|
121
|
+
</TouchableOpacity>
|
|
122
|
+
);
|
|
123
|
+
},
|
|
124
|
+
[t, tokens, styles, handleCategoryPress]
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<ScreenLayout
|
|
129
|
+
scrollable={false}
|
|
130
|
+
edges={["top", "left", "right"]}
|
|
131
|
+
backgroundColor={tokens.colors.backgroundPrimary}
|
|
132
|
+
>
|
|
133
|
+
<AIGenScreenHeader
|
|
134
|
+
title={headerTitle || t("scenario.main_category.title")}
|
|
135
|
+
description={headerDescription || t("scenario.main_category.subtitle")}
|
|
136
|
+
onNavigationPress={onBack}
|
|
137
|
+
/>
|
|
138
|
+
<FlatList
|
|
139
|
+
data={mainCategories}
|
|
140
|
+
showsVerticalScrollIndicator={false}
|
|
141
|
+
renderItem={renderItem}
|
|
142
|
+
keyExtractor={(item) => item.id}
|
|
143
|
+
contentContainerStyle={[
|
|
144
|
+
styles.listContent,
|
|
145
|
+
{ paddingBottom: insets.bottom + 100 },
|
|
146
|
+
]}
|
|
147
|
+
removeClippedSubviews
|
|
148
|
+
maxToRenderPerBatch={10}
|
|
149
|
+
updateCellsBatchingPeriod={50}
|
|
150
|
+
initialNumToRender={7}
|
|
151
|
+
windowSize={11}
|
|
152
|
+
/>
|
|
153
|
+
</ScreenLayout>
|
|
154
|
+
);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const createStyles = (tokens: DesignTokens) =>
|
|
158
|
+
StyleSheet.create({
|
|
159
|
+
listContent: {
|
|
160
|
+
paddingHorizontal: tokens.spacing.md,
|
|
161
|
+
paddingBottom: tokens.spacing.xl,
|
|
162
|
+
gap: tokens.spacing.sm,
|
|
163
|
+
},
|
|
164
|
+
card: {
|
|
165
|
+
borderRadius: tokens.borders.radius.lg,
|
|
166
|
+
borderWidth: 1,
|
|
167
|
+
overflow: "hidden",
|
|
168
|
+
},
|
|
169
|
+
cardContent: {
|
|
170
|
+
flexDirection: "row",
|
|
171
|
+
alignItems: "center",
|
|
172
|
+
padding: tokens.spacing.md,
|
|
173
|
+
},
|
|
174
|
+
iconContainer: {
|
|
175
|
+
width: 56,
|
|
176
|
+
height: 56,
|
|
177
|
+
borderRadius: 28,
|
|
178
|
+
justifyContent: "center",
|
|
179
|
+
alignItems: "center",
|
|
180
|
+
marginRight: tokens.spacing.md,
|
|
181
|
+
},
|
|
182
|
+
emoji: {
|
|
183
|
+
fontSize: 28,
|
|
184
|
+
},
|
|
185
|
+
textContent: {
|
|
186
|
+
flex: 1,
|
|
187
|
+
marginRight: tokens.spacing.sm,
|
|
188
|
+
},
|
|
189
|
+
title: {
|
|
190
|
+
fontSize: 17,
|
|
191
|
+
fontWeight: "700",
|
|
192
|
+
marginBottom: 2,
|
|
193
|
+
},
|
|
194
|
+
description: {
|
|
195
|
+
fontSize: 14,
|
|
196
|
+
lineHeight: 18,
|
|
197
|
+
},
|
|
198
|
+
});
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ScenarioPreviewScreen
|
|
3
|
+
* Config-driven scenario preview screen
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useMemo } from "react";
|
|
7
|
+
import { View, StyleSheet, TouchableOpacity } from "react-native";
|
|
8
|
+
import {
|
|
9
|
+
AtomicText,
|
|
10
|
+
useAppDesignTokens,
|
|
11
|
+
ScreenLayout,
|
|
12
|
+
type DesignTokens,
|
|
13
|
+
HeroSection,
|
|
14
|
+
AtomicIcon,
|
|
15
|
+
NavigationHeader,
|
|
16
|
+
} from "@umituz/react-native-design-system";
|
|
17
|
+
import type { ScenarioData } from "../../domain/scenario.types";
|
|
18
|
+
|
|
19
|
+
export interface ScenarioPreviewTranslations {
|
|
20
|
+
readonly continueButton: string;
|
|
21
|
+
readonly whatToExpect: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ScenarioPreviewScreenProps {
|
|
25
|
+
readonly scenario: ScenarioData;
|
|
26
|
+
readonly translations: ScenarioPreviewTranslations;
|
|
27
|
+
readonly onBack: () => void;
|
|
28
|
+
readonly onContinue: () => void;
|
|
29
|
+
readonly t: (key: string) => string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const ScenarioPreviewScreen: React.FC<ScenarioPreviewScreenProps> = ({
|
|
33
|
+
scenario,
|
|
34
|
+
translations,
|
|
35
|
+
onBack,
|
|
36
|
+
onContinue,
|
|
37
|
+
t,
|
|
38
|
+
}) => {
|
|
39
|
+
const tokens = useAppDesignTokens();
|
|
40
|
+
const styles = useMemo(() => createStyles(tokens), [tokens]);
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<View style={{ flex: 1, backgroundColor: tokens.colors.backgroundPrimary }}>
|
|
44
|
+
<NavigationHeader
|
|
45
|
+
title=""
|
|
46
|
+
onBackPress={onBack}
|
|
47
|
+
rightElement={
|
|
48
|
+
<TouchableOpacity
|
|
49
|
+
onPress={onContinue}
|
|
50
|
+
activeOpacity={0.7}
|
|
51
|
+
style={{
|
|
52
|
+
flexDirection: "row",
|
|
53
|
+
alignItems: "center",
|
|
54
|
+
backgroundColor: tokens.colors.primary,
|
|
55
|
+
paddingHorizontal: tokens.spacing.md,
|
|
56
|
+
paddingVertical: tokens.spacing.xs,
|
|
57
|
+
borderRadius: tokens.borders.radius.full,
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
<AtomicText
|
|
61
|
+
type="bodyMedium"
|
|
62
|
+
style={{
|
|
63
|
+
fontWeight: "800",
|
|
64
|
+
color: tokens.colors.onPrimary,
|
|
65
|
+
marginRight: 4,
|
|
66
|
+
}}
|
|
67
|
+
>
|
|
68
|
+
{translations.continueButton}
|
|
69
|
+
</AtomicText>
|
|
70
|
+
<AtomicIcon name="arrow-forward" size="sm" color="onPrimary" />
|
|
71
|
+
</TouchableOpacity>
|
|
72
|
+
}
|
|
73
|
+
/>
|
|
74
|
+
<ScreenLayout
|
|
75
|
+
scrollable={true}
|
|
76
|
+
edges={["left", "right"]}
|
|
77
|
+
hideScrollIndicator={true}
|
|
78
|
+
contentContainerStyle={styles.scrollContent}
|
|
79
|
+
>
|
|
80
|
+
<HeroSection
|
|
81
|
+
icon={scenario.icon}
|
|
82
|
+
imageUrl={scenario.imageUrl ?? scenario.previewImageUrl}
|
|
83
|
+
/>
|
|
84
|
+
|
|
85
|
+
<View style={styles.contentSection}>
|
|
86
|
+
<AtomicText style={styles.scenarioTitle}>
|
|
87
|
+
{t(`scenario.${scenario.id}.title`)}
|
|
88
|
+
</AtomicText>
|
|
89
|
+
|
|
90
|
+
<AtomicText style={styles.scenarioDescription}>
|
|
91
|
+
{t(`scenario.${scenario.id}.description`)}
|
|
92
|
+
</AtomicText>
|
|
93
|
+
|
|
94
|
+
<View style={styles.infoCard}>
|
|
95
|
+
<View style={styles.infoHeader}>
|
|
96
|
+
<AtomicIcon name="information-circle" size="sm" color="primary" />
|
|
97
|
+
<AtomicText style={styles.infoTitle}>
|
|
98
|
+
{translations.whatToExpect}
|
|
99
|
+
</AtomicText>
|
|
100
|
+
</View>
|
|
101
|
+
<AtomicText style={styles.infoDescription}>
|
|
102
|
+
{t(`scenario.${scenario.id}.details`)}
|
|
103
|
+
</AtomicText>
|
|
104
|
+
</View>
|
|
105
|
+
</View>
|
|
106
|
+
</ScreenLayout>
|
|
107
|
+
</View>
|
|
108
|
+
);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const createStyles = (tokens: DesignTokens) =>
|
|
112
|
+
StyleSheet.create({
|
|
113
|
+
container: {
|
|
114
|
+
flex: 1,
|
|
115
|
+
backgroundColor: tokens.colors.backgroundPrimary,
|
|
116
|
+
},
|
|
117
|
+
scrollContent: {
|
|
118
|
+
paddingBottom: 120,
|
|
119
|
+
},
|
|
120
|
+
contentSection: {
|
|
121
|
+
paddingHorizontal: tokens.spacing.lg,
|
|
122
|
+
marginTop: -40,
|
|
123
|
+
},
|
|
124
|
+
scenarioTitle: {
|
|
125
|
+
...tokens.typography.headingLarge,
|
|
126
|
+
color: tokens.colors.textPrimary,
|
|
127
|
+
fontWeight: "900",
|
|
128
|
+
marginBottom: 12,
|
|
129
|
+
textAlign: "left",
|
|
130
|
+
},
|
|
131
|
+
scenarioDescription: {
|
|
132
|
+
...tokens.typography.bodyLarge,
|
|
133
|
+
color: tokens.colors.textSecondary,
|
|
134
|
+
lineHeight: 28,
|
|
135
|
+
marginBottom: 24,
|
|
136
|
+
opacity: 0.9,
|
|
137
|
+
textAlign: "left",
|
|
138
|
+
},
|
|
139
|
+
infoCard: {
|
|
140
|
+
backgroundColor: tokens.colors.surface,
|
|
141
|
+
borderRadius: tokens.borders.radius.lg,
|
|
142
|
+
padding: tokens.spacing.lg,
|
|
143
|
+
borderWidth: 1,
|
|
144
|
+
borderColor: tokens.colors.outlineVariant,
|
|
145
|
+
},
|
|
146
|
+
infoHeader: {
|
|
147
|
+
flexDirection: "row",
|
|
148
|
+
alignItems: "center",
|
|
149
|
+
marginBottom: tokens.spacing.sm,
|
|
150
|
+
gap: tokens.spacing.xs,
|
|
151
|
+
},
|
|
152
|
+
infoTitle: {
|
|
153
|
+
...tokens.typography.labelLarge,
|
|
154
|
+
color: tokens.colors.textPrimary,
|
|
155
|
+
fontWeight: "700",
|
|
156
|
+
textTransform: "uppercase",
|
|
157
|
+
letterSpacing: 0.5,
|
|
158
|
+
},
|
|
159
|
+
infoDescription: {
|
|
160
|
+
...tokens.typography.bodyMedium,
|
|
161
|
+
color: tokens.colors.textSecondary,
|
|
162
|
+
lineHeight: 22,
|
|
163
|
+
},
|
|
164
|
+
});
|