@umituz/react-native-ai-generation-content 1.20.46 → 1.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/domains/scenarios/domain/scenario.types.ts +78 -0
- package/src/features/couple-future/presentation/components/CoupleFutureWizard.tsx +11 -2
- package/src/features/scenarios/domain/types.ts +37 -1
- package/src/features/scenarios/index.ts +17 -0
- package/src/features/scenarios/presentation/components/ScenarioHeader.tsx +0 -1
- package/src/features/scenarios/presentation/containers/CategoryNavigationContainer.tsx +114 -0
- package/src/features/scenarios/presentation/screens/HierarchicalScenarioListScreen.tsx +237 -0
- package/src/features/scenarios/presentation/screens/MainCategoryScreen.tsx +186 -0
- package/src/features/scenarios/presentation/screens/ScenarioSelectorScreen.tsx +2 -1
- package/src/features/scenarios/presentation/screens/SubCategoryScreen.tsx +193 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.21.0",
|
|
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={
|
|
102
|
+
scenario={scenario as never}
|
|
94
103
|
translations={translations.scenarioPreview as never}
|
|
95
|
-
onBack={
|
|
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";
|
|
@@ -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
|
+
onBack={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={["
|
|
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
|
+
onBack={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
|
+
});
|