@umituz/react-native-ai-generation-content 1.17.268 → 1.17.269
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/features/scenarios/domain/types.ts +75 -0
- package/src/features/scenarios/index.ts +45 -0
- package/src/features/scenarios/presentation/components/InspirationChips.tsx +82 -0
- package/src/features/scenarios/presentation/components/MagicPromptHeadline.tsx +79 -0
- package/src/features/scenarios/presentation/components/ScenarioGrid.tsx +225 -0
- package/src/features/scenarios/presentation/components/ScenarioHeader.tsx +56 -0
- package/src/features/scenarios/presentation/components/StyleSelector.tsx +119 -0
- package/src/features/scenarios/presentation/components/index.ts +18 -0
- package/src/features/scenarios/presentation/screens/MagicPromptScreen.tsx +242 -0
- package/src/features/scenarios/presentation/screens/ScenarioPreviewScreen.tsx +164 -0
- package/src/features/scenarios/presentation/screens/ScenarioSelectorScreen.tsx +65 -0
- package/src/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.17.
|
|
3
|
+
"version": "1.17.269",
|
|
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,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scenario Domain Types
|
|
3
|
+
* Generic types for scenario selection feature
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export enum ScenarioCategory {
|
|
7
|
+
TIME_TRAVEL = "time_travel",
|
|
8
|
+
FAMILY = "family",
|
|
9
|
+
LIFESTYLE = "lifestyle",
|
|
10
|
+
FANTASY = "fantasy",
|
|
11
|
+
CAREER = "career",
|
|
12
|
+
TRAVEL = "travel",
|
|
13
|
+
CULTURAL = "cultural",
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ScenarioData {
|
|
17
|
+
readonly id: string;
|
|
18
|
+
readonly category?: ScenarioCategory;
|
|
19
|
+
readonly title: string;
|
|
20
|
+
readonly description: string;
|
|
21
|
+
readonly icon: string;
|
|
22
|
+
readonly imageUrl?: string;
|
|
23
|
+
readonly previewImageUrl?: string;
|
|
24
|
+
readonly aiPrompt: string;
|
|
25
|
+
readonly storyTemplate: string;
|
|
26
|
+
readonly requiresPhoto?: boolean;
|
|
27
|
+
readonly hidden?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ScenarioSelectorConfig {
|
|
31
|
+
readonly titleKey: string;
|
|
32
|
+
readonly subtitleKey: string;
|
|
33
|
+
readonly showCategoryFilter?: boolean;
|
|
34
|
+
readonly enableSearch?: boolean;
|
|
35
|
+
readonly pageSize?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ScenarioPreviewConfig {
|
|
39
|
+
readonly showTips?: boolean;
|
|
40
|
+
readonly showDetails?: boolean;
|
|
41
|
+
readonly enableCustomization?: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface MagicPromptConfig {
|
|
45
|
+
readonly maxLength: number;
|
|
46
|
+
readonly minLength: number;
|
|
47
|
+
readonly headerKey: string;
|
|
48
|
+
readonly headlinePart1Key: string;
|
|
49
|
+
readonly headlinePart2Key: string;
|
|
50
|
+
readonly subtitleKey: string;
|
|
51
|
+
readonly inputLabelKey: string;
|
|
52
|
+
readonly surpriseButtonKey: string;
|
|
53
|
+
readonly placeholderKey: string;
|
|
54
|
+
readonly styleTitleKey: string;
|
|
55
|
+
readonly inspirationTitleKey: string;
|
|
56
|
+
readonly continueKey: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface VisualStyleOption {
|
|
60
|
+
readonly id: string;
|
|
61
|
+
readonly icon: string;
|
|
62
|
+
readonly labelKey: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface InspirationChipData {
|
|
66
|
+
readonly id: string;
|
|
67
|
+
readonly labelKey: string;
|
|
68
|
+
readonly promptKey: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const SCENARIO_DEFAULTS = {
|
|
72
|
+
pageSize: 10,
|
|
73
|
+
maxPromptLength: 500,
|
|
74
|
+
minPromptLength: 10,
|
|
75
|
+
} as const;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scenarios Feature
|
|
3
|
+
* Config-driven scenario selection and preview screens
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Domain types
|
|
7
|
+
export type {
|
|
8
|
+
ScenarioData,
|
|
9
|
+
ScenarioCategory,
|
|
10
|
+
ScenarioSelectorConfig,
|
|
11
|
+
ScenarioPreviewConfig,
|
|
12
|
+
MagicPromptConfig,
|
|
13
|
+
VisualStyleOption,
|
|
14
|
+
InspirationChipData,
|
|
15
|
+
} from "./domain/types";
|
|
16
|
+
export { ScenarioCategory as ScenarioCategoryEnum, SCENARIO_DEFAULTS } from "./domain/types";
|
|
17
|
+
|
|
18
|
+
// Components
|
|
19
|
+
export {
|
|
20
|
+
ScenarioHeader,
|
|
21
|
+
ScenarioGrid,
|
|
22
|
+
MagicPromptHeadline,
|
|
23
|
+
InspirationChips,
|
|
24
|
+
StyleSelector,
|
|
25
|
+
} from "./presentation/components";
|
|
26
|
+
export type {
|
|
27
|
+
ScenarioHeaderProps,
|
|
28
|
+
ScenarioGridProps,
|
|
29
|
+
MagicPromptHeadlineProps,
|
|
30
|
+
InspirationChipsProps,
|
|
31
|
+
StyleSelectorProps,
|
|
32
|
+
} from "./presentation/components";
|
|
33
|
+
|
|
34
|
+
// Screens
|
|
35
|
+
export { ScenarioSelectorScreen } from "./presentation/screens/ScenarioSelectorScreen";
|
|
36
|
+
export type { ScenarioSelectorScreenProps } from "./presentation/screens/ScenarioSelectorScreen";
|
|
37
|
+
|
|
38
|
+
export { ScenarioPreviewScreen } from "./presentation/screens/ScenarioPreviewScreen";
|
|
39
|
+
export type {
|
|
40
|
+
ScenarioPreviewScreenProps,
|
|
41
|
+
ScenarioPreviewTranslations,
|
|
42
|
+
} from "./presentation/screens/ScenarioPreviewScreen";
|
|
43
|
+
|
|
44
|
+
export { MagicPromptScreen } from "./presentation/screens/MagicPromptScreen";
|
|
45
|
+
export type { MagicPromptScreenProps } from "./presentation/screens/MagicPromptScreen";
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InspirationChips Component
|
|
3
|
+
* Horizontal scrollable suggestion chips for Magic Prompt
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useMemo } from "react";
|
|
7
|
+
import { View, StyleSheet, ScrollView, TouchableOpacity } from "react-native";
|
|
8
|
+
import {
|
|
9
|
+
AtomicText,
|
|
10
|
+
useAppDesignTokens,
|
|
11
|
+
} from "@umituz/react-native-design-system";
|
|
12
|
+
import type { InspirationChipData } from "../../domain/types";
|
|
13
|
+
|
|
14
|
+
export interface InspirationChipsProps {
|
|
15
|
+
readonly chips: readonly InspirationChipData[];
|
|
16
|
+
readonly title: string;
|
|
17
|
+
readonly onSelect: (promptKey: string) => void;
|
|
18
|
+
readonly t: (key: string) => string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const InspirationChips: React.FC<InspirationChipsProps> = ({
|
|
22
|
+
chips,
|
|
23
|
+
title,
|
|
24
|
+
onSelect,
|
|
25
|
+
t,
|
|
26
|
+
}) => {
|
|
27
|
+
const tokens = useAppDesignTokens();
|
|
28
|
+
|
|
29
|
+
const styles = useMemo(
|
|
30
|
+
() =>
|
|
31
|
+
StyleSheet.create({
|
|
32
|
+
container: {
|
|
33
|
+
marginBottom: tokens.spacing.lg,
|
|
34
|
+
},
|
|
35
|
+
sectionTitle: {
|
|
36
|
+
fontWeight: "700",
|
|
37
|
+
marginBottom: tokens.spacing.sm,
|
|
38
|
+
},
|
|
39
|
+
chipsContainer: {
|
|
40
|
+
gap: tokens.spacing.sm,
|
|
41
|
+
paddingBottom: 4,
|
|
42
|
+
},
|
|
43
|
+
chip: {
|
|
44
|
+
paddingHorizontal: tokens.spacing.md,
|
|
45
|
+
paddingVertical: 10,
|
|
46
|
+
borderRadius: 999,
|
|
47
|
+
borderWidth: 1,
|
|
48
|
+
borderColor: tokens.colors.border,
|
|
49
|
+
backgroundColor: tokens.colors.surface,
|
|
50
|
+
},
|
|
51
|
+
}),
|
|
52
|
+
[tokens],
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<View style={styles.container}>
|
|
57
|
+
<AtomicText type="labelLarge" style={styles.sectionTitle}>
|
|
58
|
+
{title}
|
|
59
|
+
</AtomicText>
|
|
60
|
+
<ScrollView
|
|
61
|
+
horizontal
|
|
62
|
+
showsHorizontalScrollIndicator={false}
|
|
63
|
+
contentContainerStyle={styles.chipsContainer}
|
|
64
|
+
>
|
|
65
|
+
{chips.map((chip) => (
|
|
66
|
+
<TouchableOpacity
|
|
67
|
+
key={chip.id}
|
|
68
|
+
style={styles.chip}
|
|
69
|
+
onPress={() => onSelect(chip.promptKey)}
|
|
70
|
+
>
|
|
71
|
+
<AtomicText
|
|
72
|
+
type="bodySmall"
|
|
73
|
+
style={{ color: tokens.colors.textPrimary }}
|
|
74
|
+
>
|
|
75
|
+
{t(chip.labelKey)}
|
|
76
|
+
</AtomicText>
|
|
77
|
+
</TouchableOpacity>
|
|
78
|
+
))}
|
|
79
|
+
</ScrollView>
|
|
80
|
+
</View>
|
|
81
|
+
);
|
|
82
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MagicPromptHeadline Component
|
|
3
|
+
* Headline section with highlighted text for Magic Prompt screen
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, StyleSheet } from "react-native";
|
|
8
|
+
import {
|
|
9
|
+
AtomicText,
|
|
10
|
+
useAppDesignTokens,
|
|
11
|
+
} from "@umituz/react-native-design-system";
|
|
12
|
+
|
|
13
|
+
export interface MagicPromptHeadlineProps {
|
|
14
|
+
readonly headlinePart1: string;
|
|
15
|
+
readonly headlinePart2: string;
|
|
16
|
+
readonly subtitle: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const MagicPromptHeadline: React.FC<MagicPromptHeadlineProps> = ({
|
|
20
|
+
headlinePart1,
|
|
21
|
+
headlinePart2,
|
|
22
|
+
subtitle,
|
|
23
|
+
}) => {
|
|
24
|
+
const tokens = useAppDesignTokens();
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<View style={styles.container}>
|
|
28
|
+
<View style={styles.titleRow}>
|
|
29
|
+
<AtomicText
|
|
30
|
+
type="headlineLarge"
|
|
31
|
+
style={[styles.title, { color: tokens.colors.textPrimary }]}
|
|
32
|
+
>
|
|
33
|
+
{headlinePart1}{" "}
|
|
34
|
+
<AtomicText
|
|
35
|
+
type="headlineLarge"
|
|
36
|
+
style={[
|
|
37
|
+
styles.titleHighlight,
|
|
38
|
+
{ color: tokens.colors.textPrimary },
|
|
39
|
+
]}
|
|
40
|
+
>
|
|
41
|
+
{headlinePart2}
|
|
42
|
+
</AtomicText>
|
|
43
|
+
</AtomicText>
|
|
44
|
+
</View>
|
|
45
|
+
<AtomicText
|
|
46
|
+
type="bodyLarge"
|
|
47
|
+
style={[styles.subtitle, { color: tokens.colors.textSecondary }]}
|
|
48
|
+
>
|
|
49
|
+
{subtitle}
|
|
50
|
+
</AtomicText>
|
|
51
|
+
</View>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const styles = StyleSheet.create({
|
|
56
|
+
container: {
|
|
57
|
+
marginVertical: 24,
|
|
58
|
+
},
|
|
59
|
+
titleRow: {
|
|
60
|
+
marginBottom: 12,
|
|
61
|
+
},
|
|
62
|
+
title: {
|
|
63
|
+
fontWeight: "800",
|
|
64
|
+
fontSize: 32,
|
|
65
|
+
lineHeight: 40,
|
|
66
|
+
},
|
|
67
|
+
titleHighlight: {
|
|
68
|
+
fontWeight: "800",
|
|
69
|
+
fontSize: 32,
|
|
70
|
+
lineHeight: 40,
|
|
71
|
+
textDecorationLine: "underline",
|
|
72
|
+
textDecorationColor: "rgba(255, 140, 90, 0.5)",
|
|
73
|
+
textDecorationStyle: "solid",
|
|
74
|
+
},
|
|
75
|
+
subtitle: {
|
|
76
|
+
fontSize: 16,
|
|
77
|
+
lineHeight: 24,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ScenarioGrid Component
|
|
3
|
+
* Grid display for scenario selection with category filtering
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useMemo, useCallback, useState } from "react";
|
|
7
|
+
import {
|
|
8
|
+
View as RNView,
|
|
9
|
+
FlatList as RNFlatList,
|
|
10
|
+
StyleSheet,
|
|
11
|
+
} from "react-native";
|
|
12
|
+
import {
|
|
13
|
+
useAppDesignTokens,
|
|
14
|
+
useResponsive,
|
|
15
|
+
AtomicCard,
|
|
16
|
+
AtomicText,
|
|
17
|
+
AtomicSkeleton,
|
|
18
|
+
FilterGroup,
|
|
19
|
+
type DesignTokens,
|
|
20
|
+
} from "@umituz/react-native-design-system";
|
|
21
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
22
|
+
import type { ScenarioData, ScenarioCategory } from "../../domain/types";
|
|
23
|
+
import { SCENARIO_DEFAULTS } from "../../domain/types";
|
|
24
|
+
|
|
25
|
+
const View = RNView as any;
|
|
26
|
+
const FlatList = RNFlatList as any;
|
|
27
|
+
|
|
28
|
+
export interface ScenarioGridProps {
|
|
29
|
+
readonly scenarios: readonly ScenarioData[];
|
|
30
|
+
readonly selectedScenarioId: string | null;
|
|
31
|
+
readonly onSelect: (id: string) => void;
|
|
32
|
+
readonly categories: readonly ScenarioCategory[];
|
|
33
|
+
readonly t: (key: string) => string;
|
|
34
|
+
readonly pageSize?: number;
|
|
35
|
+
readonly categoryAllLabel?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const ScenarioGrid: React.FC<ScenarioGridProps> = ({
|
|
39
|
+
scenarios,
|
|
40
|
+
selectedScenarioId,
|
|
41
|
+
onSelect,
|
|
42
|
+
categories,
|
|
43
|
+
t,
|
|
44
|
+
pageSize = SCENARIO_DEFAULTS.pageSize,
|
|
45
|
+
categoryAllLabel = "All",
|
|
46
|
+
}) => {
|
|
47
|
+
const [category, setCategory] = useState<ScenarioCategory | "all">("all");
|
|
48
|
+
const [displayedCount, setDisplayedCount] = useState(pageSize);
|
|
49
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
50
|
+
|
|
51
|
+
const tokens = useAppDesignTokens();
|
|
52
|
+
const insets = useSafeAreaInsets();
|
|
53
|
+
const { width } = useResponsive();
|
|
54
|
+
|
|
55
|
+
const numColumns = 2;
|
|
56
|
+
const horizontalPadding = tokens.spacing.md;
|
|
57
|
+
const cardSpacing = tokens.spacing.md;
|
|
58
|
+
const availableWidth = width - horizontalPadding * 2 - cardSpacing;
|
|
59
|
+
const cardWidth = availableWidth / numColumns;
|
|
60
|
+
|
|
61
|
+
const filteredScenarios = useMemo(() => {
|
|
62
|
+
if (category === "all") return scenarios;
|
|
63
|
+
return scenarios.filter((s) => s.category === category);
|
|
64
|
+
}, [scenarios, category]);
|
|
65
|
+
|
|
66
|
+
const displayedScenarios = useMemo(
|
|
67
|
+
() => filteredScenarios.slice(0, displayedCount),
|
|
68
|
+
[filteredScenarios, displayedCount],
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const hasMore = displayedCount < filteredScenarios.length;
|
|
72
|
+
const isLoadingMore = isLoading && displayedCount > pageSize;
|
|
73
|
+
|
|
74
|
+
const styles = useMemo(
|
|
75
|
+
() => createStyles(tokens, cardWidth, cardSpacing),
|
|
76
|
+
[tokens, cardWidth, cardSpacing],
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const loadMore = useCallback(() => {
|
|
80
|
+
if (hasMore && !isLoading) {
|
|
81
|
+
setIsLoading(true);
|
|
82
|
+
setTimeout(() => {
|
|
83
|
+
setDisplayedCount((prev) =>
|
|
84
|
+
Math.min(prev + pageSize, filteredScenarios.length),
|
|
85
|
+
);
|
|
86
|
+
setIsLoading(false);
|
|
87
|
+
}, 300);
|
|
88
|
+
}
|
|
89
|
+
}, [hasMore, isLoading, filteredScenarios.length, pageSize]);
|
|
90
|
+
|
|
91
|
+
const handleCategoryChange = useCallback(
|
|
92
|
+
(val: ScenarioCategory | "all") => {
|
|
93
|
+
setCategory(val);
|
|
94
|
+
setDisplayedCount(pageSize);
|
|
95
|
+
},
|
|
96
|
+
[pageSize],
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const ListEmptyComponent = useMemo(
|
|
100
|
+
() => (
|
|
101
|
+
<View style={styles.centerContainer}>
|
|
102
|
+
<AtomicText type="bodyMedium" color="textSecondary">
|
|
103
|
+
{t("scenario.empty")}
|
|
104
|
+
</AtomicText>
|
|
105
|
+
</View>
|
|
106
|
+
),
|
|
107
|
+
[t, styles.centerContainer],
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const ListFooterComponent = useMemo(
|
|
111
|
+
() =>
|
|
112
|
+
isLoadingMore ? (
|
|
113
|
+
<View style={styles.footerLoader}>
|
|
114
|
+
<AtomicSkeleton pattern="card" count={2} />
|
|
115
|
+
</View>
|
|
116
|
+
) : null,
|
|
117
|
+
[isLoadingMore, styles.footerLoader],
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const ListHeaderComponent = useMemo(
|
|
121
|
+
() => (
|
|
122
|
+
<View style={{ marginBottom: tokens.spacing.md }}>
|
|
123
|
+
<FilterGroup
|
|
124
|
+
items={[
|
|
125
|
+
{ label: categoryAllLabel, value: "all" },
|
|
126
|
+
...categories.map((cat) => ({
|
|
127
|
+
label: t(`category.${cat}`),
|
|
128
|
+
value: cat,
|
|
129
|
+
})),
|
|
130
|
+
]}
|
|
131
|
+
selectedValue={category}
|
|
132
|
+
onSelect={(val) =>
|
|
133
|
+
handleCategoryChange(val as ScenarioCategory | "all")
|
|
134
|
+
}
|
|
135
|
+
contentContainerStyle={{ paddingHorizontal: horizontalPadding }}
|
|
136
|
+
/>
|
|
137
|
+
</View>
|
|
138
|
+
),
|
|
139
|
+
[
|
|
140
|
+
category,
|
|
141
|
+
t,
|
|
142
|
+
horizontalPadding,
|
|
143
|
+
tokens.spacing.md,
|
|
144
|
+
categories,
|
|
145
|
+
categoryAllLabel,
|
|
146
|
+
handleCategoryChange,
|
|
147
|
+
],
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const renderItem = useCallback(
|
|
151
|
+
({ item }: { item: ScenarioData }) => {
|
|
152
|
+
const title = t(`scenario.${item.id}.title`);
|
|
153
|
+
const description = t(`scenario.${item.id}.description`);
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<AtomicCard
|
|
157
|
+
image={item.imageUrl || ""}
|
|
158
|
+
title={title}
|
|
159
|
+
subtitle={description}
|
|
160
|
+
selected={selectedScenarioId === item.id}
|
|
161
|
+
imageAspectRatio={1.25}
|
|
162
|
+
style={{ width: cardWidth }}
|
|
163
|
+
onPress={() => onSelect(item.id)}
|
|
164
|
+
testID={`scenario-card-${item.id}`}
|
|
165
|
+
/>
|
|
166
|
+
);
|
|
167
|
+
},
|
|
168
|
+
[cardWidth, selectedScenarioId, onSelect, t],
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<FlatList
|
|
173
|
+
data={displayedScenarios}
|
|
174
|
+
numColumns={numColumns}
|
|
175
|
+
key={`grid-${numColumns}`}
|
|
176
|
+
showsVerticalScrollIndicator={false}
|
|
177
|
+
columnWrapperStyle={styles.row}
|
|
178
|
+
renderItem={renderItem}
|
|
179
|
+
keyExtractor={(item: ScenarioData) => item.id}
|
|
180
|
+
ListHeaderComponent={ListHeaderComponent}
|
|
181
|
+
ListEmptyComponent={filteredScenarios.length === 0 ? ListEmptyComponent : null}
|
|
182
|
+
ListFooterComponent={ListFooterComponent}
|
|
183
|
+
onEndReached={loadMore}
|
|
184
|
+
onEndReachedThreshold={0.5}
|
|
185
|
+
contentContainerStyle={[
|
|
186
|
+
styles.listContent,
|
|
187
|
+
{
|
|
188
|
+
paddingBottom: insets.bottom + 100,
|
|
189
|
+
paddingHorizontal: horizontalPadding,
|
|
190
|
+
},
|
|
191
|
+
]}
|
|
192
|
+
initialNumToRender={pageSize}
|
|
193
|
+
maxToRenderPerBatch={pageSize}
|
|
194
|
+
windowSize={5}
|
|
195
|
+
/>
|
|
196
|
+
);
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const createStyles = (
|
|
200
|
+
tokens: DesignTokens,
|
|
201
|
+
cardWidth: number,
|
|
202
|
+
cardSpacing: number,
|
|
203
|
+
) =>
|
|
204
|
+
StyleSheet.create({
|
|
205
|
+
container: {
|
|
206
|
+
flex: 1,
|
|
207
|
+
},
|
|
208
|
+
listContent: {
|
|
209
|
+
paddingTop: tokens.spacing.sm,
|
|
210
|
+
flexGrow: 1,
|
|
211
|
+
},
|
|
212
|
+
row: {
|
|
213
|
+
gap: cardSpacing,
|
|
214
|
+
marginBottom: cardSpacing,
|
|
215
|
+
},
|
|
216
|
+
centerContainer: {
|
|
217
|
+
flex: 1,
|
|
218
|
+
justifyContent: "center",
|
|
219
|
+
alignItems: "center",
|
|
220
|
+
paddingVertical: tokens.spacing.xl,
|
|
221
|
+
},
|
|
222
|
+
footerLoader: {
|
|
223
|
+
paddingVertical: tokens.spacing.md,
|
|
224
|
+
},
|
|
225
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ScenarioHeader Component
|
|
3
|
+
* Header section for scenario selection screen
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, StyleSheet } from "react-native";
|
|
8
|
+
import {
|
|
9
|
+
AtomicText,
|
|
10
|
+
useAppDesignTokens,
|
|
11
|
+
type DesignTokens,
|
|
12
|
+
} from "@umituz/react-native-design-system";
|
|
13
|
+
|
|
14
|
+
export interface ScenarioHeaderProps {
|
|
15
|
+
readonly title: string;
|
|
16
|
+
readonly subtitle: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const ScenarioHeader: React.FC<ScenarioHeaderProps> = ({
|
|
20
|
+
title,
|
|
21
|
+
subtitle,
|
|
22
|
+
}) => {
|
|
23
|
+
const tokens = useAppDesignTokens();
|
|
24
|
+
const styles = React.useMemo(() => createStyles(tokens), [tokens]);
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<View style={styles.container}>
|
|
28
|
+
<AtomicText type="headlineLarge" style={styles.title}>
|
|
29
|
+
{title}
|
|
30
|
+
</AtomicText>
|
|
31
|
+
<AtomicText
|
|
32
|
+
type="bodyMedium"
|
|
33
|
+
color="textSecondary"
|
|
34
|
+
style={styles.subtitle}
|
|
35
|
+
>
|
|
36
|
+
{subtitle}
|
|
37
|
+
</AtomicText>
|
|
38
|
+
</View>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const createStyles = (tokens: DesignTokens) =>
|
|
43
|
+
StyleSheet.create({
|
|
44
|
+
container: {
|
|
45
|
+
paddingHorizontal: tokens.spacing.md,
|
|
46
|
+
paddingTop: tokens.spacing.lg,
|
|
47
|
+
paddingBottom: tokens.spacing.md,
|
|
48
|
+
gap: tokens.spacing.xs,
|
|
49
|
+
},
|
|
50
|
+
title: {
|
|
51
|
+
lineHeight: 34,
|
|
52
|
+
},
|
|
53
|
+
subtitle: {
|
|
54
|
+
lineHeight: 22,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StyleSelector Component
|
|
3
|
+
* Visual style selection grid for Magic Prompt
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useMemo } from "react";
|
|
7
|
+
import { View, StyleSheet, TouchableOpacity } from "react-native";
|
|
8
|
+
import {
|
|
9
|
+
AtomicText,
|
|
10
|
+
AtomicIcon,
|
|
11
|
+
useAppDesignTokens,
|
|
12
|
+
} from "@umituz/react-native-design-system";
|
|
13
|
+
import type { VisualStyleOption } from "../../domain/types";
|
|
14
|
+
|
|
15
|
+
export interface StyleSelectorProps {
|
|
16
|
+
readonly styles: readonly VisualStyleOption[];
|
|
17
|
+
readonly selectedStyle: string;
|
|
18
|
+
readonly title: string;
|
|
19
|
+
readonly onSelect: (styleId: string) => void;
|
|
20
|
+
readonly t: (key: string) => string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const StyleSelector: React.FC<StyleSelectorProps> = ({
|
|
24
|
+
styles: visualStyles,
|
|
25
|
+
selectedStyle,
|
|
26
|
+
title,
|
|
27
|
+
onSelect,
|
|
28
|
+
t,
|
|
29
|
+
}) => {
|
|
30
|
+
const tokens = useAppDesignTokens();
|
|
31
|
+
|
|
32
|
+
const componentStyles = useMemo(
|
|
33
|
+
() =>
|
|
34
|
+
StyleSheet.create({
|
|
35
|
+
container: {
|
|
36
|
+
marginBottom: tokens.spacing.lg,
|
|
37
|
+
},
|
|
38
|
+
sectionTitle: {
|
|
39
|
+
fontWeight: "700",
|
|
40
|
+
marginBottom: tokens.spacing.sm,
|
|
41
|
+
},
|
|
42
|
+
stylesGrid: {
|
|
43
|
+
flexDirection: "row",
|
|
44
|
+
gap: tokens.spacing.md,
|
|
45
|
+
},
|
|
46
|
+
styleItem: {
|
|
47
|
+
flex: 1,
|
|
48
|
+
alignItems: "center",
|
|
49
|
+
gap: tokens.spacing.xs,
|
|
50
|
+
},
|
|
51
|
+
styleIcon: {
|
|
52
|
+
width: 64,
|
|
53
|
+
height: 64,
|
|
54
|
+
borderRadius: tokens.borders.radius.md,
|
|
55
|
+
alignItems: "center",
|
|
56
|
+
justifyContent: "center",
|
|
57
|
+
},
|
|
58
|
+
styleLabel: {
|
|
59
|
+
textAlign: "center",
|
|
60
|
+
},
|
|
61
|
+
}),
|
|
62
|
+
[tokens],
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<View style={componentStyles.container}>
|
|
67
|
+
<AtomicText type="labelLarge" style={componentStyles.sectionTitle}>
|
|
68
|
+
{title}
|
|
69
|
+
</AtomicText>
|
|
70
|
+
<View style={componentStyles.stylesGrid}>
|
|
71
|
+
{visualStyles.map((style) => (
|
|
72
|
+
<TouchableOpacity
|
|
73
|
+
key={style.id}
|
|
74
|
+
style={componentStyles.styleItem}
|
|
75
|
+
onPress={() => onSelect(style.id)}
|
|
76
|
+
>
|
|
77
|
+
<View
|
|
78
|
+
style={[
|
|
79
|
+
componentStyles.styleIcon,
|
|
80
|
+
{
|
|
81
|
+
backgroundColor:
|
|
82
|
+
selectedStyle === style.id
|
|
83
|
+
? tokens.colors.primaryContainer
|
|
84
|
+
: tokens.colors.surface,
|
|
85
|
+
borderWidth: selectedStyle === style.id ? 2 : 1,
|
|
86
|
+
borderColor:
|
|
87
|
+
selectedStyle === style.id
|
|
88
|
+
? tokens.colors.primary
|
|
89
|
+
: tokens.colors.border,
|
|
90
|
+
},
|
|
91
|
+
]}
|
|
92
|
+
>
|
|
93
|
+
<AtomicIcon
|
|
94
|
+
name={style.icon as any}
|
|
95
|
+
size="md"
|
|
96
|
+
color={selectedStyle === style.id ? "primary" : "textSecondary"}
|
|
97
|
+
/>
|
|
98
|
+
</View>
|
|
99
|
+
<AtomicText
|
|
100
|
+
type="labelSmall"
|
|
101
|
+
style={[
|
|
102
|
+
componentStyles.styleLabel,
|
|
103
|
+
{
|
|
104
|
+
color:
|
|
105
|
+
selectedStyle === style.id
|
|
106
|
+
? tokens.colors.textPrimary
|
|
107
|
+
: tokens.colors.textSecondary,
|
|
108
|
+
fontWeight: selectedStyle === style.id ? "700" : "500",
|
|
109
|
+
},
|
|
110
|
+
]}
|
|
111
|
+
>
|
|
112
|
+
{t(style.labelKey)}
|
|
113
|
+
</AtomicText>
|
|
114
|
+
</TouchableOpacity>
|
|
115
|
+
))}
|
|
116
|
+
</View>
|
|
117
|
+
</View>
|
|
118
|
+
);
|
|
119
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scenario Components Index
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { ScenarioHeader } from "./ScenarioHeader";
|
|
6
|
+
export type { ScenarioHeaderProps } from "./ScenarioHeader";
|
|
7
|
+
|
|
8
|
+
export { ScenarioGrid } from "./ScenarioGrid";
|
|
9
|
+
export type { ScenarioGridProps } from "./ScenarioGrid";
|
|
10
|
+
|
|
11
|
+
export { MagicPromptHeadline } from "./MagicPromptHeadline";
|
|
12
|
+
export type { MagicPromptHeadlineProps } from "./MagicPromptHeadline";
|
|
13
|
+
|
|
14
|
+
export { InspirationChips } from "./InspirationChips";
|
|
15
|
+
export type { InspirationChipsProps } from "./InspirationChips";
|
|
16
|
+
|
|
17
|
+
export { StyleSelector } from "./StyleSelector";
|
|
18
|
+
export type { StyleSelectorProps } from "./StyleSelector";
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MagicPromptScreen
|
|
3
|
+
* Config-driven custom prompt screen
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useState, useCallback, useMemo } from "react";
|
|
7
|
+
import { View, StyleSheet, TouchableOpacity } from "react-native";
|
|
8
|
+
import {
|
|
9
|
+
AtomicText,
|
|
10
|
+
AtomicTextArea,
|
|
11
|
+
AtomicIcon,
|
|
12
|
+
useAppDesignTokens,
|
|
13
|
+
ScreenLayout,
|
|
14
|
+
NavigationHeader,
|
|
15
|
+
} from "@umituz/react-native-design-system";
|
|
16
|
+
import { MagicPromptHeadline } from "../components/MagicPromptHeadline";
|
|
17
|
+
import { InspirationChips } from "../components/InspirationChips";
|
|
18
|
+
import { StyleSelector } from "../components/StyleSelector";
|
|
19
|
+
import type {
|
|
20
|
+
MagicPromptConfig,
|
|
21
|
+
VisualStyleOption,
|
|
22
|
+
InspirationChipData,
|
|
23
|
+
} from "../../domain/types";
|
|
24
|
+
|
|
25
|
+
export interface MagicPromptScreenProps {
|
|
26
|
+
readonly config: MagicPromptConfig;
|
|
27
|
+
readonly visualStyles: readonly VisualStyleOption[];
|
|
28
|
+
readonly inspirationChips: readonly InspirationChipData[];
|
|
29
|
+
readonly surprisePrompts: readonly string[];
|
|
30
|
+
readonly initialPrompt: string;
|
|
31
|
+
readonly initialVisualStyle: string;
|
|
32
|
+
readonly onContinue: (prompt: string, visualStyle: string) => void;
|
|
33
|
+
readonly onBack: () => void;
|
|
34
|
+
readonly t: (key: string) => string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const MagicPromptScreen: React.FC<MagicPromptScreenProps> = ({
|
|
38
|
+
config,
|
|
39
|
+
visualStyles,
|
|
40
|
+
inspirationChips,
|
|
41
|
+
surprisePrompts,
|
|
42
|
+
initialPrompt,
|
|
43
|
+
initialVisualStyle,
|
|
44
|
+
onContinue,
|
|
45
|
+
onBack,
|
|
46
|
+
t,
|
|
47
|
+
}) => {
|
|
48
|
+
const tokens = useAppDesignTokens();
|
|
49
|
+
const [text, setText] = useState(initialPrompt);
|
|
50
|
+
const [selectedStyle, setSelectedStyle] = useState(initialVisualStyle);
|
|
51
|
+
|
|
52
|
+
const handleSurprise = useCallback(() => {
|
|
53
|
+
const randomIndex = Math.floor(Math.random() * surprisePrompts.length);
|
|
54
|
+
const surprisePrompt = t(surprisePrompts[randomIndex]);
|
|
55
|
+
setText(surprisePrompt);
|
|
56
|
+
|
|
57
|
+
const randomStyleIndex = Math.floor(Math.random() * visualStyles.length);
|
|
58
|
+
setSelectedStyle(visualStyles[randomStyleIndex].id);
|
|
59
|
+
}, [surprisePrompts, visualStyles, t]);
|
|
60
|
+
|
|
61
|
+
const handleInspirationSelect = useCallback(
|
|
62
|
+
(promptKey: string) => {
|
|
63
|
+
const prompt = t(promptKey);
|
|
64
|
+
setText(prompt);
|
|
65
|
+
},
|
|
66
|
+
[t],
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const handleStyleSelect = useCallback((styleId: string) => {
|
|
70
|
+
setSelectedStyle(styleId);
|
|
71
|
+
}, []);
|
|
72
|
+
|
|
73
|
+
const handleContinue = useCallback(() => {
|
|
74
|
+
onContinue(text, selectedStyle);
|
|
75
|
+
}, [onContinue, text, selectedStyle]);
|
|
76
|
+
|
|
77
|
+
const isValid = text.trim().length >= config.minLength;
|
|
78
|
+
|
|
79
|
+
const componentStyles = useMemo(
|
|
80
|
+
() =>
|
|
81
|
+
StyleSheet.create({
|
|
82
|
+
inputCard: {
|
|
83
|
+
borderWidth: 1,
|
|
84
|
+
borderRadius: 16,
|
|
85
|
+
padding: 16,
|
|
86
|
+
marginBottom: 24,
|
|
87
|
+
},
|
|
88
|
+
inputHeader: {
|
|
89
|
+
flexDirection: "row",
|
|
90
|
+
justifyContent: "space-between",
|
|
91
|
+
alignItems: "center",
|
|
92
|
+
marginBottom: 12,
|
|
93
|
+
},
|
|
94
|
+
label: {
|
|
95
|
+
textTransform: "uppercase",
|
|
96
|
+
fontWeight: "700",
|
|
97
|
+
letterSpacing: 1,
|
|
98
|
+
},
|
|
99
|
+
surpriseButton: {
|
|
100
|
+
flexDirection: "row",
|
|
101
|
+
alignItems: "center",
|
|
102
|
+
gap: 6,
|
|
103
|
+
paddingHorizontal: 12,
|
|
104
|
+
paddingVertical: 6,
|
|
105
|
+
borderRadius: 999,
|
|
106
|
+
},
|
|
107
|
+
surpriseText: { fontWeight: "600" },
|
|
108
|
+
textArea: { marginBottom: 12 },
|
|
109
|
+
charCounter: { alignItems: "flex-end", paddingTop: 8 },
|
|
110
|
+
}),
|
|
111
|
+
[],
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<View style={{ flex: 1, backgroundColor: tokens.colors.backgroundPrimary }}>
|
|
116
|
+
<NavigationHeader
|
|
117
|
+
title={t(config.headerKey)}
|
|
118
|
+
onBackPress={onBack}
|
|
119
|
+
rightElement={
|
|
120
|
+
<TouchableOpacity
|
|
121
|
+
onPress={handleContinue}
|
|
122
|
+
disabled={!isValid}
|
|
123
|
+
activeOpacity={0.7}
|
|
124
|
+
style={{
|
|
125
|
+
flexDirection: "row",
|
|
126
|
+
alignItems: "center",
|
|
127
|
+
backgroundColor: isValid
|
|
128
|
+
? tokens.colors.primary
|
|
129
|
+
: tokens.colors.surfaceVariant,
|
|
130
|
+
paddingHorizontal: tokens.spacing.md,
|
|
131
|
+
paddingVertical: tokens.spacing.xs,
|
|
132
|
+
borderRadius: tokens.borders.radius.full,
|
|
133
|
+
opacity: isValid ? 1 : 0.5,
|
|
134
|
+
}}
|
|
135
|
+
>
|
|
136
|
+
<AtomicText
|
|
137
|
+
type="bodyMedium"
|
|
138
|
+
style={{
|
|
139
|
+
fontWeight: "800",
|
|
140
|
+
color: isValid
|
|
141
|
+
? tokens.colors.onPrimary
|
|
142
|
+
: tokens.colors.textSecondary,
|
|
143
|
+
marginRight: 4,
|
|
144
|
+
}}
|
|
145
|
+
>
|
|
146
|
+
{t(config.continueKey)}
|
|
147
|
+
</AtomicText>
|
|
148
|
+
<AtomicIcon
|
|
149
|
+
name="arrow-forward"
|
|
150
|
+
size="sm"
|
|
151
|
+
color={isValid ? "onPrimary" : "textSecondary"}
|
|
152
|
+
/>
|
|
153
|
+
</TouchableOpacity>
|
|
154
|
+
}
|
|
155
|
+
/>
|
|
156
|
+
<ScreenLayout
|
|
157
|
+
scrollable={true}
|
|
158
|
+
edges={["left", "right"]}
|
|
159
|
+
keyboardAvoiding={true}
|
|
160
|
+
contentContainerStyle={{ paddingHorizontal: tokens.spacing.lg }}
|
|
161
|
+
>
|
|
162
|
+
<MagicPromptHeadline
|
|
163
|
+
headlinePart1={t(config.headlinePart1Key)}
|
|
164
|
+
headlinePart2={t(config.headlinePart2Key)}
|
|
165
|
+
subtitle={t(config.subtitleKey)}
|
|
166
|
+
/>
|
|
167
|
+
|
|
168
|
+
<View
|
|
169
|
+
style={[
|
|
170
|
+
componentStyles.inputCard,
|
|
171
|
+
{ borderColor: tokens.colors.border },
|
|
172
|
+
]}
|
|
173
|
+
>
|
|
174
|
+
<View style={componentStyles.inputHeader}>
|
|
175
|
+
<AtomicText
|
|
176
|
+
type="labelSmall"
|
|
177
|
+
style={[
|
|
178
|
+
componentStyles.label,
|
|
179
|
+
{ color: tokens.colors.textSecondary },
|
|
180
|
+
]}
|
|
181
|
+
>
|
|
182
|
+
{t(config.inputLabelKey)}
|
|
183
|
+
</AtomicText>
|
|
184
|
+
<TouchableOpacity
|
|
185
|
+
onPress={handleSurprise}
|
|
186
|
+
style={[
|
|
187
|
+
componentStyles.surpriseButton,
|
|
188
|
+
{ backgroundColor: tokens.colors.primaryContainer },
|
|
189
|
+
]}
|
|
190
|
+
>
|
|
191
|
+
<AtomicIcon name="sparkles" size="xs" color="primary" />
|
|
192
|
+
<AtomicText
|
|
193
|
+
type="labelSmall"
|
|
194
|
+
style={[
|
|
195
|
+
componentStyles.surpriseText,
|
|
196
|
+
{ color: tokens.colors.primary },
|
|
197
|
+
]}
|
|
198
|
+
>
|
|
199
|
+
{t(config.surpriseButtonKey)}
|
|
200
|
+
</AtomicText>
|
|
201
|
+
</TouchableOpacity>
|
|
202
|
+
</View>
|
|
203
|
+
|
|
204
|
+
<AtomicTextArea
|
|
205
|
+
placeholder={t(config.placeholderKey)}
|
|
206
|
+
value={text}
|
|
207
|
+
onChangeText={setText}
|
|
208
|
+
numberOfLines={8}
|
|
209
|
+
maxLength={config.maxLength}
|
|
210
|
+
autoFocus
|
|
211
|
+
minHeight={200}
|
|
212
|
+
style={componentStyles.textArea}
|
|
213
|
+
/>
|
|
214
|
+
|
|
215
|
+
<View style={componentStyles.charCounter}>
|
|
216
|
+
<AtomicText
|
|
217
|
+
type="labelSmall"
|
|
218
|
+
style={{ color: tokens.colors.textSecondary }}
|
|
219
|
+
>
|
|
220
|
+
{text.length}/{config.maxLength}
|
|
221
|
+
</AtomicText>
|
|
222
|
+
</View>
|
|
223
|
+
</View>
|
|
224
|
+
|
|
225
|
+
<InspirationChips
|
|
226
|
+
chips={inspirationChips}
|
|
227
|
+
title={t(config.inspirationTitleKey)}
|
|
228
|
+
onSelect={handleInspirationSelect}
|
|
229
|
+
t={t}
|
|
230
|
+
/>
|
|
231
|
+
|
|
232
|
+
<StyleSelector
|
|
233
|
+
styles={visualStyles}
|
|
234
|
+
selectedStyle={selectedStyle}
|
|
235
|
+
title={t(config.styleTitleKey)}
|
|
236
|
+
onSelect={handleStyleSelect}
|
|
237
|
+
t={t}
|
|
238
|
+
/>
|
|
239
|
+
</ScreenLayout>
|
|
240
|
+
</View>
|
|
241
|
+
);
|
|
242
|
+
};
|
|
@@ -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/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.previewImageUrl ?? scenario.imageUrl}
|
|
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
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ScenarioSelectorScreen
|
|
3
|
+
* Config-driven scenario selection screen
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { StyleSheet } from "react-native";
|
|
8
|
+
import {
|
|
9
|
+
useAppDesignTokens,
|
|
10
|
+
ScreenLayout,
|
|
11
|
+
type DesignTokens,
|
|
12
|
+
} from "@umituz/react-native-design-system";
|
|
13
|
+
import { ScenarioHeader } from "../components/ScenarioHeader";
|
|
14
|
+
import { ScenarioGrid } from "../components/ScenarioGrid";
|
|
15
|
+
import type { ScenarioData, ScenarioCategory, ScenarioSelectorConfig } from "../../domain/types";
|
|
16
|
+
|
|
17
|
+
export interface ScenarioSelectorScreenProps {
|
|
18
|
+
readonly config: ScenarioSelectorConfig;
|
|
19
|
+
readonly scenarios: readonly ScenarioData[];
|
|
20
|
+
readonly categories: readonly ScenarioCategory[];
|
|
21
|
+
readonly selectedScenarioId: string | null;
|
|
22
|
+
readonly onSelect: (id: string) => void;
|
|
23
|
+
readonly t: (key: string) => string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const ScenarioSelectorScreen: React.FC<ScenarioSelectorScreenProps> = ({
|
|
27
|
+
config,
|
|
28
|
+
scenarios,
|
|
29
|
+
categories,
|
|
30
|
+
selectedScenarioId,
|
|
31
|
+
onSelect,
|
|
32
|
+
t,
|
|
33
|
+
}) => {
|
|
34
|
+
const tokens = useAppDesignTokens();
|
|
35
|
+
const styles = React.useMemo(() => createStyles(tokens), [tokens]);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<ScreenLayout
|
|
39
|
+
scrollable={false}
|
|
40
|
+
edges={["top", "left", "right"]}
|
|
41
|
+
contentContainerStyle={styles.container}
|
|
42
|
+
backgroundColor={tokens.colors.backgroundPrimary}
|
|
43
|
+
>
|
|
44
|
+
<ScenarioHeader
|
|
45
|
+
title={t(config.titleKey)}
|
|
46
|
+
subtitle={t(config.subtitleKey)}
|
|
47
|
+
/>
|
|
48
|
+
<ScenarioGrid
|
|
49
|
+
scenarios={scenarios}
|
|
50
|
+
categories={categories}
|
|
51
|
+
selectedScenarioId={selectedScenarioId}
|
|
52
|
+
onSelect={onSelect}
|
|
53
|
+
t={t}
|
|
54
|
+
categoryAllLabel={t("category.all")}
|
|
55
|
+
/>
|
|
56
|
+
</ScreenLayout>
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const createStyles = (tokens: DesignTokens) =>
|
|
61
|
+
StyleSheet.create({
|
|
62
|
+
container: {
|
|
63
|
+
flex: 1,
|
|
64
|
+
},
|
|
65
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -153,6 +153,7 @@ export * from "./features/meme-generator";
|
|
|
153
153
|
export * from "./features/couple-future";
|
|
154
154
|
export * from "./features/love-message";
|
|
155
155
|
export * from "./features/partner-upload";
|
|
156
|
+
export * from "./features/scenarios";
|
|
156
157
|
export * from "./infrastructure/orchestration";
|
|
157
158
|
|
|
158
159
|
// Result Preview Domain
|