@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.
@@ -0,0 +1,2 @@
1
+ export { CategoryNavigationContainer } from "./CategoryNavigationContainer";
2
+ export type { CategoryNavigationContainerProps } from "./CategoryNavigationContainer";
@@ -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
+ });