@umituz/react-native-ai-generation-content 1.20.47 → 1.21.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-ai-generation-content",
3
- "version": "1.20.47",
3
+ "version": "1.21.1",
4
4
  "description": "Provider-agnostic AI generation orchestration for React Native with result preview components",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Scenario Domain Types
3
+ * Generic scenario system for AI generation apps
4
+ */
5
+
6
+ /**
7
+ * Scenario represents a pre-configured AI generation template
8
+ */
9
+ export interface Scenario {
10
+ readonly id: string;
11
+ readonly title: string;
12
+ readonly description: string;
13
+ readonly icon?: string;
14
+ readonly emoji?: string;
15
+ readonly imageUrl?: string;
16
+ readonly previewImageUrl?: string;
17
+ readonly category: string;
18
+ readonly tags?: readonly string[];
19
+ readonly aiPrompt: string;
20
+ readonly storyTemplate?: string;
21
+ readonly requiresPhoto?: boolean;
22
+ readonly metadata?: Record<string, unknown>;
23
+ }
24
+
25
+ /**
26
+ * Main Category (Top-level grouping)
27
+ */
28
+ export interface MainCategory {
29
+ readonly id: string;
30
+ readonly title: string;
31
+ readonly description?: string;
32
+ readonly icon?: string;
33
+ readonly emoji?: string;
34
+ readonly order: number;
35
+ readonly subCategories: readonly string[];
36
+ }
37
+
38
+ /**
39
+ * Sub Category (Second-level grouping)
40
+ */
41
+ export interface SubCategory {
42
+ readonly id: string;
43
+ readonly title: string;
44
+ readonly description?: string;
45
+ readonly icon?: string;
46
+ readonly emoji?: string;
47
+ readonly mainCategoryId: string;
48
+ readonly scenarioCategories: readonly string[];
49
+ readonly order: number;
50
+ }
51
+
52
+ /**
53
+ * Scenario Category (Third-level - actual scenario grouping)
54
+ */
55
+ export interface ScenarioCategory {
56
+ readonly id: string;
57
+ readonly name: string;
58
+ readonly description?: string;
59
+ }
60
+
61
+ /**
62
+ * Complete scenario system configuration
63
+ */
64
+ export interface ScenarioSystemConfig {
65
+ readonly mainCategories: readonly MainCategory[];
66
+ readonly subCategories: readonly SubCategory[];
67
+ readonly scenarioCategories: readonly ScenarioCategory[];
68
+ readonly scenarios: readonly Scenario[];
69
+ }
70
+
71
+ /**
72
+ * Scenario selection result
73
+ */
74
+ export interface ScenarioSelection {
75
+ readonly scenario: Scenario;
76
+ readonly mainCategory: MainCategory;
77
+ readonly subCategory: SubCategory;
78
+ }
@@ -88,11 +88,20 @@ export const CoupleFutureWizard: React.FC<CoupleFutureWizardProps> = ({
88
88
  return null; // Rendered by parent via CategoryNavigationContainer
89
89
 
90
90
  case StepType.SCENARIO_PREVIEW:
91
+ const scenario = flow.selectedCategory || data.selectedScenario;
92
+ const handlePreviewBack = () => {
93
+ // If we came from CategoryNavigationContainer, reset to SCENARIO step
94
+ if (data.selectedScenario && !flow.selectedCategory) {
95
+ callbacks?.onBackToScenarioSelection?.();
96
+ } else {
97
+ flow.previousStep();
98
+ }
99
+ };
91
100
  return (
92
101
  <ScenarioPreviewScreen
93
- scenario={flow.selectedCategory as never}
102
+ scenario={scenario as never}
94
103
  translations={translations.scenarioPreview as never}
95
- onBack={flow.previousStep}
104
+ onBack={handlePreviewBack}
96
105
  onContinue={handleScenarioPreviewContinue}
97
106
  t={t}
98
107
  />
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Scenario Domain Types
3
3
  * Generic types for scenario selection feature
4
+ * Supports both flat and hierarchical category systems
4
5
  */
5
6
 
6
7
  export enum ScenarioCategory {
@@ -15,7 +16,7 @@ export enum ScenarioCategory {
15
16
 
16
17
  export interface ScenarioData {
17
18
  readonly id: string;
18
- readonly category?: ScenarioCategory;
19
+ readonly category?: ScenarioCategory | string;
19
20
  readonly title: string;
20
21
  readonly description: string;
21
22
  readonly icon: string;
@@ -27,6 +28,41 @@ export interface ScenarioData {
27
28
  readonly hidden?: boolean;
28
29
  }
29
30
 
31
+ /**
32
+ * Scenario Main Category (Top-level grouping for hierarchical system)
33
+ */
34
+ export interface ScenarioMainCategory {
35
+ readonly id: string;
36
+ readonly titleKey: string;
37
+ readonly descriptionKey?: string;
38
+ readonly icon?: string;
39
+ readonly emoji?: string;
40
+ readonly order: number;
41
+ readonly subCategoryIds: readonly string[];
42
+ }
43
+
44
+ /**
45
+ * Scenario Sub Category (Second-level grouping for hierarchical system)
46
+ */
47
+ export interface ScenarioSubCategory {
48
+ readonly id: string;
49
+ readonly titleKey: string;
50
+ readonly descriptionKey?: string;
51
+ readonly icon?: string;
52
+ readonly emoji?: string;
53
+ readonly mainCategoryId: string;
54
+ readonly scenarioCategories: readonly string[];
55
+ readonly order: number;
56
+ }
57
+
58
+ /**
59
+ * Hierarchical scenario category configuration
60
+ */
61
+ export interface ScenarioHierarchyConfig {
62
+ readonly mainCategories: readonly ScenarioMainCategory[];
63
+ readonly subCategories: readonly ScenarioSubCategory[];
64
+ }
65
+
30
66
  export interface ScenarioSelectorConfig {
31
67
  readonly titleKey: string;
32
68
  readonly subtitleKey: string;
@@ -12,6 +12,9 @@ export type {
12
12
  MagicPromptConfig,
13
13
  VisualStyleOption,
14
14
  InspirationChipData,
15
+ ScenarioMainCategory,
16
+ ScenarioSubCategory,
17
+ ScenarioHierarchyConfig,
15
18
  } from "./domain/types";
16
19
  export { ScenarioCategory as ScenarioCategoryEnum, SCENARIO_DEFAULTS } from "./domain/types";
17
20
 
@@ -43,3 +46,17 @@ export type {
43
46
 
44
47
  export { MagicPromptScreen } from "./presentation/screens/MagicPromptScreen";
45
48
  export type { MagicPromptScreenProps } from "./presentation/screens/MagicPromptScreen";
49
+
50
+ // Hierarchical Screens
51
+ export { MainCategoryScreen } from "./presentation/screens/MainCategoryScreen";
52
+ export type { MainCategoryScreenProps } from "./presentation/screens/MainCategoryScreen";
53
+
54
+ export { SubCategoryScreen } from "./presentation/screens/SubCategoryScreen";
55
+ export type { SubCategoryScreenProps } from "./presentation/screens/SubCategoryScreen";
56
+
57
+ export { HierarchicalScenarioListScreen } from "./presentation/screens/HierarchicalScenarioListScreen";
58
+ export type { HierarchicalScenarioListScreenProps } from "./presentation/screens/HierarchicalScenarioListScreen";
59
+
60
+ // Containers
61
+ export { CategoryNavigationContainer } from "./presentation/containers/CategoryNavigationContainer";
62
+ export type { CategoryNavigationContainerProps } from "./presentation/containers/CategoryNavigationContainer";
@@ -43,7 +43,6 @@ const createStyles = (tokens: DesignTokens) =>
43
43
  StyleSheet.create({
44
44
  container: {
45
45
  paddingHorizontal: tokens.spacing.md,
46
- paddingTop: tokens.spacing.lg,
47
46
  paddingBottom: tokens.spacing.md,
48
47
  gap: tokens.spacing.xs,
49
48
  },
@@ -0,0 +1,114 @@
1
+ /**
2
+ * CategoryNavigationContainer
3
+ * Orchestrates 3-step hierarchical scenario selection flow:
4
+ * Main Category → Sub Category → Scenario List
5
+ */
6
+
7
+ import React, { useState, useCallback } from "react";
8
+ import type {
9
+ ScenarioData,
10
+ ScenarioMainCategory,
11
+ ScenarioSubCategory,
12
+ } from "../../domain/types";
13
+ import { MainCategoryScreen } from "../screens/MainCategoryScreen";
14
+ import { SubCategoryScreen } from "../screens/SubCategoryScreen";
15
+ import { HierarchicalScenarioListScreen } from "../screens/HierarchicalScenarioListScreen";
16
+
17
+ type NavigationStep = "main_category" | "sub_category" | "scenario_list";
18
+
19
+ export interface CategoryNavigationContainerProps {
20
+ readonly mainCategories: readonly ScenarioMainCategory[];
21
+ readonly subCategories: readonly ScenarioSubCategory[];
22
+ readonly scenarios: readonly ScenarioData[];
23
+ readonly onSelectScenario: (scenarioId: string) => void;
24
+ readonly onBack?: () => void;
25
+ readonly t: (key: string) => string;
26
+ readonly headerTitle?: string;
27
+ readonly headerDescription?: string;
28
+ readonly numColumns?: number;
29
+ }
30
+
31
+ export const CategoryNavigationContainer: React.FC<
32
+ CategoryNavigationContainerProps
33
+ > = ({
34
+ mainCategories,
35
+ subCategories,
36
+ scenarios,
37
+ onSelectScenario,
38
+ onBack,
39
+ t,
40
+ headerTitle,
41
+ headerDescription,
42
+ numColumns = 2,
43
+ }) => {
44
+ const [currentStep, setCurrentStep] = useState<NavigationStep>("main_category");
45
+ const [selectedMainCategoryId, setSelectedMainCategoryId] = useState<string | null>(null);
46
+ const [selectedSubCategoryId, setSelectedSubCategoryId] = useState<string | null>(null);
47
+
48
+ const handleSelectMainCategory = useCallback((categoryId: string) => {
49
+ setSelectedMainCategoryId(categoryId);
50
+ setCurrentStep("sub_category");
51
+ }, []);
52
+
53
+ const handleSelectSubCategory = useCallback((subCategoryId: string) => {
54
+ setSelectedSubCategoryId(subCategoryId);
55
+ setCurrentStep("scenario_list");
56
+ }, []);
57
+
58
+ const handleBackFromSubCategory = useCallback(() => {
59
+ setSelectedMainCategoryId(null);
60
+ setCurrentStep("main_category");
61
+ }, []);
62
+
63
+ const handleBackFromScenarioList = useCallback(() => {
64
+ setSelectedSubCategoryId(null);
65
+ setCurrentStep("sub_category");
66
+ }, []);
67
+
68
+ const handleBackFromMainCategory = useCallback(() => {
69
+ if (onBack) {
70
+ onBack();
71
+ }
72
+ }, [onBack]);
73
+
74
+ if (currentStep === "main_category") {
75
+ return (
76
+ <MainCategoryScreen
77
+ mainCategories={mainCategories}
78
+ onSelectCategory={handleSelectMainCategory}
79
+ onBack={onBack ? handleBackFromMainCategory : undefined}
80
+ t={t}
81
+ headerTitle={headerTitle}
82
+ headerDescription={headerDescription}
83
+ />
84
+ );
85
+ }
86
+
87
+ if (currentStep === "sub_category" && selectedMainCategoryId) {
88
+ return (
89
+ <SubCategoryScreen
90
+ mainCategoryId={selectedMainCategoryId}
91
+ subCategories={subCategories}
92
+ onSelectSubCategory={handleSelectSubCategory}
93
+ onBack={handleBackFromSubCategory}
94
+ t={t}
95
+ />
96
+ );
97
+ }
98
+
99
+ if (currentStep === "scenario_list" && selectedSubCategoryId) {
100
+ return (
101
+ <HierarchicalScenarioListScreen
102
+ subCategoryId={selectedSubCategoryId}
103
+ subCategories={subCategories}
104
+ scenarios={scenarios}
105
+ onSelectScenario={onSelectScenario}
106
+ onBack={handleBackFromScenarioList}
107
+ t={t}
108
+ numColumns={numColumns}
109
+ />
110
+ );
111
+ }
112
+
113
+ return null;
114
+ };
@@ -0,0 +1,237 @@
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 } 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
+ type DesignTokens,
24
+ } from "@umituz/react-native-design-system";
25
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
26
+ import type { ScenarioData, ScenarioSubCategory } from "../../domain/types";
27
+
28
+ export interface HierarchicalScenarioListScreenProps {
29
+ readonly subCategoryId: string;
30
+ readonly subCategories: readonly ScenarioSubCategory[];
31
+ readonly scenarios: readonly ScenarioData[];
32
+ readonly onSelectScenario: (scenarioId: string) => void;
33
+ readonly onBack: () => void;
34
+ readonly t: (key: string) => string;
35
+ readonly numColumns?: number;
36
+ }
37
+
38
+ export const HierarchicalScenarioListScreen: React.FC<HierarchicalScenarioListScreenProps> = ({
39
+ subCategoryId,
40
+ subCategories,
41
+ scenarios,
42
+ onSelectScenario,
43
+ onBack,
44
+ t,
45
+ numColumns = 2,
46
+ }) => {
47
+ const tokens = useAppDesignTokens();
48
+ const insets = useSafeAreaInsets();
49
+ const { width } = useResponsive();
50
+
51
+ const [selectedId, setSelectedId] = useState<string | null>(null);
52
+
53
+ const subCategory = useMemo(
54
+ () => subCategories.find((sub) => sub.id === subCategoryId),
55
+ [subCategories, subCategoryId]
56
+ );
57
+
58
+ const filteredScenarios = useMemo(() => {
59
+ if (!subCategory) return [];
60
+
61
+ return scenarios.filter((scenario) => {
62
+ if (!scenario.category) return false;
63
+ return subCategory.scenarioCategories.includes(scenario.category);
64
+ });
65
+ }, [scenarios, subCategory]);
66
+
67
+ const horizontalPadding = tokens.spacing.md;
68
+ const cardSpacing = tokens.spacing.md;
69
+
70
+ // Calculate card width once - memoized to prevent unnecessary recalculations
71
+ const cardWidth = useMemo(() => {
72
+ const availableWidth = width - horizontalPadding * 2 - cardSpacing;
73
+ return availableWidth / numColumns;
74
+ }, [width, horizontalPadding, cardSpacing, numColumns]);
75
+
76
+ const styles = useMemo(
77
+ () => createStyles(tokens, cardSpacing, horizontalPadding),
78
+ [tokens, cardSpacing, horizontalPadding]
79
+ );
80
+
81
+ const handleContinue = useCallback(() => {
82
+ if (selectedId) {
83
+ onSelectScenario(selectedId);
84
+ }
85
+ }, [selectedId, onSelectScenario]);
86
+
87
+ // Memoized callback for card selection - prevents inline arrow functions
88
+ const handleCardPress = useCallback((itemId: string) => {
89
+ setSelectedId(itemId);
90
+ }, []);
91
+
92
+ const renderItem = useCallback(
93
+ ({ item }: ListRenderItemInfo<ScenarioData>) => {
94
+ const title = t(`scenario.${item.id}.title`);
95
+ const description = t(`scenario.${item.id}.description`);
96
+ const isSelected = selectedId === item.id;
97
+
98
+ return (
99
+ <AtomicCard
100
+ image={item.previewImageUrl || item.imageUrl || ""}
101
+ title={title}
102
+ subtitle={description}
103
+ imageAspectRatio={1.25}
104
+ selected={isSelected}
105
+ style={{ width: cardWidth }}
106
+ onPress={() => handleCardPress(item.id)}
107
+ testID={`scenario-card-${item.id}`}
108
+ />
109
+ );
110
+ },
111
+ [cardWidth, selectedId, t, handleCardPress]
112
+ );
113
+
114
+ const ListEmptyComponent = useMemo(
115
+ () => (
116
+ <View style={styles.emptyState}>
117
+ <AtomicText type="bodyLarge" color="textSecondary">
118
+ {t("scenario.list.empty")}
119
+ </AtomicText>
120
+ </View>
121
+ ),
122
+ [t, styles.emptyState]
123
+ );
124
+
125
+ if (!subCategory) {
126
+ return null;
127
+ }
128
+
129
+ const canContinue = !!selectedId;
130
+
131
+ return (
132
+ <View style={styles.container}>
133
+ <NavigationHeader
134
+ title={t(subCategory.titleKey)}
135
+ onBackPress={onBack}
136
+ rightElement={
137
+ <TouchableOpacity
138
+ onPress={handleContinue}
139
+ disabled={!canContinue}
140
+ activeOpacity={0.7}
141
+ style={[
142
+ styles.continueButton,
143
+ {
144
+ backgroundColor: canContinue
145
+ ? tokens.colors.primary
146
+ : tokens.colors.surfaceVariant,
147
+ opacity: canContinue ? 1 : 0.5,
148
+ },
149
+ ]}
150
+ >
151
+ <AtomicText
152
+ type="bodyMedium"
153
+ style={[
154
+ styles.continueText,
155
+ {
156
+ color: canContinue
157
+ ? tokens.colors.onPrimary
158
+ : tokens.colors.textSecondary,
159
+ },
160
+ ]}
161
+ >
162
+ {t("common.continue")}
163
+ </AtomicText>
164
+ <AtomicIcon
165
+ name="arrow-forward"
166
+ size="sm"
167
+ color={canContinue ? "onPrimary" : "textSecondary"}
168
+ />
169
+ </TouchableOpacity>
170
+ }
171
+ />
172
+ <ScreenLayout
173
+ scrollable={false}
174
+ edges={["left", "right"]}
175
+ backgroundColor={tokens.colors.backgroundPrimary}
176
+ >
177
+ <FlatList
178
+ data={filteredScenarios}
179
+ numColumns={numColumns}
180
+ showsVerticalScrollIndicator={false}
181
+ columnWrapperStyle={styles.row}
182
+ renderItem={renderItem}
183
+ keyExtractor={(item) => item.id}
184
+ ListEmptyComponent={
185
+ filteredScenarios.length === 0 ? ListEmptyComponent : null
186
+ }
187
+ contentContainerStyle={[
188
+ styles.listContent,
189
+ { paddingBottom: insets.bottom + 100 },
190
+ ]}
191
+ removeClippedSubviews
192
+ maxToRenderPerBatch={10}
193
+ updateCellsBatchingPeriod={50}
194
+ initialNumToRender={10}
195
+ windowSize={21}
196
+ />
197
+ </ScreenLayout>
198
+ </View>
199
+ );
200
+ };
201
+
202
+ const createStyles = (
203
+ tokens: DesignTokens,
204
+ cardSpacing: number,
205
+ horizontalPadding: number
206
+ ) =>
207
+ StyleSheet.create({
208
+ container: {
209
+ flex: 1,
210
+ },
211
+ listContent: {
212
+ paddingTop: tokens.spacing.sm,
213
+ flexGrow: 1,
214
+ },
215
+ row: {
216
+ gap: cardSpacing,
217
+ marginBottom: cardSpacing,
218
+ paddingHorizontal: horizontalPadding,
219
+ },
220
+ emptyState: {
221
+ flex: 1,
222
+ justifyContent: "center",
223
+ alignItems: "center",
224
+ paddingVertical: tokens.spacing.xl,
225
+ },
226
+ continueButton: {
227
+ flexDirection: "row",
228
+ alignItems: "center",
229
+ paddingHorizontal: tokens.spacing.md,
230
+ paddingVertical: tokens.spacing.xs,
231
+ borderRadius: tokens.borders.radius.full,
232
+ },
233
+ continueText: {
234
+ fontWeight: "800",
235
+ marginRight: 4,
236
+ },
237
+ });
@@ -0,0 +1,186 @@
1
+ /**
2
+ * MainCategoryScreen
3
+ * Displays main categories for hierarchical scenario selection
4
+ */
5
+
6
+ import React, { useMemo, useCallback } 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/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
+ const styles = useMemo(() => createStyles(tokens), [tokens]);
46
+
47
+ const handleCategoryPress = useCallback(
48
+ (categoryId: string) => {
49
+ onSelectCategory(categoryId);
50
+ },
51
+ [onSelectCategory]
52
+ );
53
+
54
+ const renderItem = useCallback(
55
+ ({ item }: ListRenderItemInfo<ScenarioMainCategory>) => {
56
+ const title = t(item.titleKey);
57
+ const description = item.descriptionKey ? t(item.descriptionKey) : "";
58
+
59
+ return (
60
+ <TouchableOpacity
61
+ style={[
62
+ styles.card,
63
+ {
64
+ backgroundColor: tokens.colors.surface,
65
+ borderColor: tokens.colors.border,
66
+ },
67
+ ]}
68
+ onPress={() => handleCategoryPress(item.id)}
69
+ activeOpacity={0.7}
70
+ testID={`main-category-${item.id}`}
71
+ >
72
+ <View style={styles.cardContent}>
73
+ <View
74
+ style={[
75
+ styles.iconContainer,
76
+ { backgroundColor: tokens.colors.surfaceVariant },
77
+ ]}
78
+ >
79
+ {item.emoji ? (
80
+ <AtomicText style={styles.emoji}>{item.emoji}</AtomicText>
81
+ ) : (
82
+ <AtomicIcon name={item.icon as never} size="lg" color="primary" />
83
+ )}
84
+ </View>
85
+ <View style={styles.textContent}>
86
+ <AtomicText
87
+ style={[styles.title, { color: tokens.colors.textPrimary }]}
88
+ >
89
+ {title}
90
+ </AtomicText>
91
+ {description ? (
92
+ <AtomicText
93
+ style={[
94
+ styles.description,
95
+ { color: tokens.colors.textSecondary },
96
+ ]}
97
+ numberOfLines={2}
98
+ >
99
+ {description}
100
+ </AtomicText>
101
+ ) : null}
102
+ </View>
103
+ <AtomicIcon
104
+ name="chevron-forward"
105
+ size="md"
106
+ color="textSecondary"
107
+ />
108
+ </View>
109
+ </TouchableOpacity>
110
+ );
111
+ },
112
+ [t, tokens, styles, handleCategoryPress]
113
+ );
114
+
115
+ return (
116
+ <ScreenLayout
117
+ scrollable={false}
118
+ edges={["top", "left", "right"]}
119
+ backgroundColor={tokens.colors.backgroundPrimary}
120
+ >
121
+ <AIGenScreenHeader
122
+ title={headerTitle || t("scenario.main_category.title")}
123
+ description={headerDescription || t("scenario.main_category.subtitle")}
124
+ onNavigationPress={onBack}
125
+ />
126
+ <FlatList
127
+ data={mainCategories}
128
+ showsVerticalScrollIndicator={false}
129
+ renderItem={renderItem}
130
+ keyExtractor={(item) => item.id}
131
+ contentContainerStyle={[
132
+ styles.listContent,
133
+ { paddingBottom: insets.bottom + 100 },
134
+ ]}
135
+ removeClippedSubviews
136
+ maxToRenderPerBatch={10}
137
+ updateCellsBatchingPeriod={50}
138
+ initialNumToRender={7}
139
+ windowSize={11}
140
+ />
141
+ </ScreenLayout>
142
+ );
143
+ };
144
+
145
+ const createStyles = (tokens: DesignTokens) =>
146
+ StyleSheet.create({
147
+ listContent: {
148
+ paddingHorizontal: tokens.spacing.md,
149
+ paddingBottom: tokens.spacing.xl,
150
+ gap: tokens.spacing.sm,
151
+ },
152
+ card: {
153
+ borderRadius: tokens.borders.radius.lg,
154
+ borderWidth: 1,
155
+ overflow: "hidden",
156
+ },
157
+ cardContent: {
158
+ flexDirection: "row",
159
+ alignItems: "center",
160
+ padding: tokens.spacing.md,
161
+ },
162
+ iconContainer: {
163
+ width: 56,
164
+ height: 56,
165
+ borderRadius: 28,
166
+ justifyContent: "center",
167
+ alignItems: "center",
168
+ marginRight: tokens.spacing.md,
169
+ },
170
+ emoji: {
171
+ fontSize: 28,
172
+ },
173
+ textContent: {
174
+ flex: 1,
175
+ marginRight: tokens.spacing.sm,
176
+ },
177
+ title: {
178
+ fontSize: 17,
179
+ fontWeight: "700",
180
+ marginBottom: 2,
181
+ },
182
+ description: {
183
+ fontSize: 14,
184
+ lineHeight: 18,
185
+ },
186
+ });
@@ -37,7 +37,7 @@ export const ScenarioSelectorScreen: React.FC<ScenarioSelectorScreenProps> = ({
37
37
  return (
38
38
  <ScreenLayout
39
39
  scrollable={false}
40
- edges={["top", "left", "right"]}
40
+ edges={["left", "right"]}
41
41
  contentContainerStyle={styles.container}
42
42
  backgroundColor={tokens.colors.backgroundPrimary}
43
43
  >
@@ -61,5 +61,6 @@ const createStyles = (_tokens: DesignTokens) =>
61
61
  StyleSheet.create({
62
62
  container: {
63
63
  flex: 1,
64
+ paddingTop: 0,
64
65
  },
65
66
  });
@@ -0,0 +1,193 @@
1
+ /**
2
+ * SubCategoryScreen
3
+ * Displays sub-categories for a selected main category
4
+ */
5
+
6
+ import React, { useMemo, useCallback } 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 { ScenarioSubCategory } from "../../domain/types";
24
+
25
+ export interface SubCategoryScreenProps {
26
+ readonly mainCategoryId: string;
27
+ readonly subCategories: readonly ScenarioSubCategory[];
28
+ readonly onSelectSubCategory: (subCategoryId: string) => void;
29
+ readonly onBack: () => void;
30
+ readonly t: (key: string) => string;
31
+ readonly headerTitleKey?: string;
32
+ readonly headerDescriptionKey?: string;
33
+ }
34
+
35
+ export const SubCategoryScreen: React.FC<SubCategoryScreenProps> = ({
36
+ mainCategoryId,
37
+ subCategories,
38
+ onSelectSubCategory,
39
+ onBack,
40
+ t,
41
+ headerTitleKey,
42
+ headerDescriptionKey,
43
+ }) => {
44
+ const tokens = useAppDesignTokens();
45
+ const insets = useSafeAreaInsets();
46
+
47
+ const filteredSubCategories = useMemo(
48
+ () => subCategories.filter((sub) => sub.mainCategoryId === mainCategoryId),
49
+ [subCategories, mainCategoryId]
50
+ );
51
+
52
+ const styles = useMemo(() => createStyles(tokens), [tokens]);
53
+
54
+ const handleSubCategoryPress = useCallback(
55
+ (subCategoryId: string) => {
56
+ onSelectSubCategory(subCategoryId);
57
+ },
58
+ [onSelectSubCategory]
59
+ );
60
+
61
+ const renderItem = useCallback(
62
+ ({ item }: ListRenderItemInfo<ScenarioSubCategory>) => {
63
+ const title = t(item.titleKey);
64
+ const description = item.descriptionKey ? t(item.descriptionKey) : "";
65
+
66
+ return (
67
+ <TouchableOpacity
68
+ style={[
69
+ styles.card,
70
+ {
71
+ backgroundColor: tokens.colors.surface,
72
+ borderColor: tokens.colors.border,
73
+ },
74
+ ]}
75
+ onPress={() => handleSubCategoryPress(item.id)}
76
+ activeOpacity={0.7}
77
+ testID={`sub-category-${item.id}`}
78
+ >
79
+ <View style={styles.cardContent}>
80
+ <View
81
+ style={[
82
+ styles.iconContainer,
83
+ { backgroundColor: tokens.colors.surfaceVariant },
84
+ ]}
85
+ >
86
+ {item.emoji ? (
87
+ <AtomicText style={styles.emoji}>{item.emoji}</AtomicText>
88
+ ) : (
89
+ <AtomicIcon name={item.icon as never} size="lg" color="primary" />
90
+ )}
91
+ </View>
92
+ <View style={styles.textContent}>
93
+ <AtomicText
94
+ style={[styles.title, { color: tokens.colors.textPrimary }]}
95
+ >
96
+ {title}
97
+ </AtomicText>
98
+ {description ? (
99
+ <AtomicText
100
+ style={[
101
+ styles.description,
102
+ { color: tokens.colors.textSecondary },
103
+ ]}
104
+ numberOfLines={2}
105
+ >
106
+ {description}
107
+ </AtomicText>
108
+ ) : null}
109
+ </View>
110
+ <AtomicIcon
111
+ name="chevron-forward"
112
+ size="md"
113
+ color="textSecondary"
114
+ />
115
+ </View>
116
+ </TouchableOpacity>
117
+ );
118
+ },
119
+ [t, tokens, styles, handleSubCategoryPress]
120
+ );
121
+
122
+ return (
123
+ <ScreenLayout
124
+ scrollable={false}
125
+ edges={["left", "right"]}
126
+ backgroundColor={tokens.colors.backgroundPrimary}
127
+ >
128
+ <AIGenScreenHeader
129
+ title={headerTitleKey ? t(headerTitleKey) : t("scenario.sub_category.title")}
130
+ description={headerDescriptionKey ? t(headerDescriptionKey) : t("scenario.sub_category.subtitle")}
131
+ onNavigationPress={onBack}
132
+ />
133
+ <FlatList
134
+ data={filteredSubCategories}
135
+ showsVerticalScrollIndicator={false}
136
+ renderItem={renderItem}
137
+ keyExtractor={(item) => item.id}
138
+ contentContainerStyle={[
139
+ styles.listContent,
140
+ { paddingBottom: insets.bottom + 100 },
141
+ ]}
142
+ removeClippedSubviews
143
+ maxToRenderPerBatch={10}
144
+ updateCellsBatchingPeriod={50}
145
+ initialNumToRender={10}
146
+ windowSize={11}
147
+ />
148
+ </ScreenLayout>
149
+ );
150
+ };
151
+
152
+ const createStyles = (tokens: DesignTokens) =>
153
+ StyleSheet.create({
154
+ listContent: {
155
+ paddingHorizontal: tokens.spacing.md,
156
+ paddingBottom: tokens.spacing.xl,
157
+ gap: tokens.spacing.sm,
158
+ },
159
+ card: {
160
+ borderRadius: tokens.borders.radius.lg,
161
+ borderWidth: 1,
162
+ overflow: "hidden",
163
+ },
164
+ cardContent: {
165
+ flexDirection: "row",
166
+ alignItems: "center",
167
+ padding: tokens.spacing.md,
168
+ },
169
+ iconContainer: {
170
+ width: 56,
171
+ height: 56,
172
+ borderRadius: 28,
173
+ justifyContent: "center",
174
+ alignItems: "center",
175
+ marginRight: tokens.spacing.md,
176
+ },
177
+ emoji: {
178
+ fontSize: 28,
179
+ },
180
+ textContent: {
181
+ flex: 1,
182
+ marginRight: tokens.spacing.sm,
183
+ },
184
+ title: {
185
+ fontSize: 17,
186
+ fontWeight: "700",
187
+ marginBottom: 2,
188
+ },
189
+ description: {
190
+ fontSize: 14,
191
+ lineHeight: 18,
192
+ },
193
+ });