@umituz/react-native-ai-generation-content 1.17.246 → 1.17.248

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.17.246",
3
+ "version": "1.17.248",
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",
@@ -43,3 +43,20 @@ export const COUPLE_FUTURE_DEFAULTS = {
43
43
  outputFormat: "jpeg" as NanoBananaOutputFormat,
44
44
  timeoutMs: 300000,
45
45
  };
46
+
47
+ export type CoupleFeatureId =
48
+ | "romantic-mood"
49
+ | "art-style"
50
+ | "artist-style"
51
+ | "wardrobe";
52
+
53
+ export interface CoupleFeatureSelection {
54
+ romanticMoods?: string[];
55
+ romanticIntensity?: number;
56
+ artStyle?: string | null;
57
+ artStyleIntensity?: number;
58
+ artist?: string | null;
59
+ artistIntensity?: number;
60
+ wardrobeStyle?: string | null;
61
+ wardrobeIntensity?: number;
62
+ }
@@ -10,7 +10,34 @@ export type {
10
10
  CoupleFutureResult,
11
11
  NanoBananaAspectRatio,
12
12
  NanoBananaOutputFormat,
13
+ CoupleFeatureId,
14
+ CoupleFeatureSelection,
13
15
  } from "./domain/types";
14
16
  export { COUPLE_FUTURE_DEFAULTS } from "./domain/types";
15
17
  export { useCoupleFutureGeneration } from "./presentation/hooks/useCoupleFutureGeneration";
16
18
  export type { UseCoupleFutureGenerationConfig } from "./presentation/hooks/useCoupleFutureGeneration";
19
+ export {
20
+ RomanticMoodSelector,
21
+ ArtStyleSelector,
22
+ ArtistStyleSelector,
23
+ WardrobeSelector,
24
+ } from "./presentation/components";
25
+ export type {
26
+ RomanticMoodSelectorProps,
27
+ ArtStyleSelectorProps,
28
+ ArtistStyleSelectorProps,
29
+ WardrobeSelectorProps,
30
+ } from "./presentation/components";
31
+ export { CoupleFeatureScreen } from "./presentation/screens/CoupleFeatureScreen";
32
+ export type { CoupleFeatureScreenProps } from "./presentation/screens/CoupleFeatureScreen";
33
+ export {
34
+ COUPLE_FEATURE_CONFIGS,
35
+ ROMANTIC_MOOD_OPTIONS,
36
+ ART_STYLE_OPTIONS,
37
+ ARTIST_STYLE_OPTIONS,
38
+ } from "./infrastructure/coupleFeatureRegistry";
39
+ export type {
40
+ CoupleFeatureConfig,
41
+ CoupleFeatureOption,
42
+ } from "./infrastructure/coupleFeatureRegistry";
43
+ export { enhanceCouplePrompt } from "./infrastructure/couplePromptEnhancer";
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Couple Feature Registry
3
+ * Configuration for couple generation feature selectors
4
+ */
5
+
6
+ export interface CoupleFeatureOption {
7
+ id: string;
8
+ iconKey: string;
9
+ labelKey: string;
10
+ }
11
+
12
+ export interface CoupleFeatureConfig {
13
+ id: string;
14
+ translationPrefix: string;
15
+ hasIntensitySlider: boolean;
16
+ selectionType: "single" | "multi";
17
+ options: CoupleFeatureOption[];
18
+ }
19
+
20
+ export const ROMANTIC_MOOD_OPTIONS: CoupleFeatureOption[] = [
21
+ { id: "romantic", iconKey: "heart", labelKey: "romantic" },
22
+ { id: "mysterious", iconKey: "moon", labelKey: "mysterious" },
23
+ { id: "magical", iconKey: "sparkles", labelKey: "magical" },
24
+ { id: "energetic", iconKey: "bolt", labelKey: "energetic" },
25
+ { id: "melancholic", iconKey: "cloud", labelKey: "melancholic" },
26
+ { id: "passionate", iconKey: "flame", labelKey: "passionate" },
27
+ { id: "nostalgic", iconKey: "camera", labelKey: "nostalgic" },
28
+ { id: "futuristic", iconKey: "rocket", labelKey: "futuristic" },
29
+ ];
30
+
31
+ export const ART_STYLE_OPTIONS: CoupleFeatureOption[] = [
32
+ { id: "original", iconKey: "image", labelKey: "original" },
33
+ { id: "cubism", iconKey: "shape", labelKey: "cubism" },
34
+ { id: "popArt", iconKey: "color", labelKey: "popArt" },
35
+ { id: "impressionism", iconKey: "brush", labelKey: "impressionism" },
36
+ { id: "surrealism", iconKey: "eye", labelKey: "surrealism" },
37
+ { id: "renaissance", iconKey: "palette", labelKey: "renaissance" },
38
+ ];
39
+
40
+ export const ARTIST_STYLE_OPTIONS: CoupleFeatureOption[] = [
41
+ { id: "vanGogh", iconKey: "brush", labelKey: "vanGogh" },
42
+ { id: "picasso", iconKey: "shape", labelKey: "picasso" },
43
+ { id: "fridaKahlo", iconKey: "flower", labelKey: "fridaKahlo" },
44
+ { id: "daVinci", iconKey: "palette", labelKey: "daVinci" },
45
+ ];
46
+
47
+ export const COUPLE_FEATURE_CONFIGS: Record<string, CoupleFeatureConfig> = {
48
+ "romantic-mood": {
49
+ id: "romantic-mood",
50
+ translationPrefix: "romanticPhoto",
51
+ hasIntensitySlider: true,
52
+ selectionType: "multi",
53
+ options: ROMANTIC_MOOD_OPTIONS,
54
+ },
55
+ "art-style": {
56
+ id: "art-style",
57
+ translationPrefix: "artStyles",
58
+ hasIntensitySlider: true,
59
+ selectionType: "single",
60
+ options: ART_STYLE_OPTIONS,
61
+ },
62
+ "artist-style": {
63
+ id: "artist-style",
64
+ translationPrefix: "artistStyles",
65
+ hasIntensitySlider: true,
66
+ selectionType: "single",
67
+ options: ARTIST_STYLE_OPTIONS,
68
+ },
69
+ wardrobe: {
70
+ id: "wardrobe",
71
+ translationPrefix: "wardrobe",
72
+ hasIntensitySlider: false,
73
+ selectionType: "single",
74
+ options: [],
75
+ },
76
+ };
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Couple Prompt Enhancer
3
+ * Enhances base prompts with feature-specific selections
4
+ */
5
+
6
+ import type { CoupleFeatureSelection } from "../domain/types";
7
+
8
+ const MOOD_DESCRIPTIONS: Record<string, string> = {
9
+ romantic: "warm romantic tones, soft candlelight, intimate atmosphere",
10
+ mysterious: "moody low-key lighting, deep shadows, enigmatic ambiance",
11
+ magical: "sparkly magical effects, ethereal glow, fairy-tale atmosphere",
12
+ energetic: "vibrant dynamic energy, bright lighting, lively atmosphere",
13
+ melancholic: "soft melancholic tones, gentle shadows, contemplative mood",
14
+ passionate: "intense passionate colors, dramatic lighting, emotional depth",
15
+ nostalgic: "vintage nostalgic feel, warm sepia tones, timeless quality",
16
+ futuristic: "modern futuristic aesthetic, neon accents, sci-fi atmosphere",
17
+ };
18
+
19
+ const ART_STYLE_DESCRIPTIONS: Record<string, string> = {
20
+ cubism: "geometric cubist style with angular shapes and fragmented forms",
21
+ popArt: "bold pop art style with bright vibrant colors and graphic elements",
22
+ impressionism: "impressionist style with visible brushstrokes and light effects",
23
+ surrealism: "surrealist style with dreamlike imagery and unexpected juxtapositions",
24
+ renaissance: "classical renaissance style with rich details and balanced composition",
25
+ };
26
+
27
+ const ARTIST_STYLE_DESCRIPTIONS: Record<string, string> = {
28
+ vanGogh: "Van Gogh's post-impressionist style with thick expressive brushstrokes and swirling patterns",
29
+ picasso: "Picasso's cubist style with geometric fragmentation and multiple perspectives",
30
+ fridaKahlo: "Frida Kahlo's vibrant symbolic style with rich colors and personal imagery",
31
+ daVinci: "Leonardo da Vinci's renaissance mastery with subtle sfumato and perfect proportion",
32
+ };
33
+
34
+ const WARDROBE_STYLE_DESCRIPTIONS: Record<string, string> = {
35
+ casual: "casual comfortable outfit with relaxed fit",
36
+ formal: "elegant formal attire with sophisticated style",
37
+ sporty: "athletic sporty wear with dynamic energy",
38
+ elegant: "refined elegant clothing with graceful details",
39
+ };
40
+
41
+ const getIntensityLabel = (intensity: number): string => {
42
+ if (intensity >= 75) return "strong";
43
+ if (intensity >= 50) return "moderate";
44
+ return "subtle";
45
+ };
46
+
47
+ const getArtistIntensityLabel = (intensity: number): string => {
48
+ if (intensity >= 75) return "strong homage to";
49
+ if (intensity >= 50) return "inspired by";
50
+ return "subtle influence of";
51
+ };
52
+
53
+ export const enhanceCouplePrompt = (
54
+ basePrompt: string,
55
+ selections: CoupleFeatureSelection
56
+ ): string => {
57
+ let enhanced = basePrompt;
58
+
59
+ if (selections.romanticMoods && selections.romanticMoods.length > 0) {
60
+ const moodDescriptions = selections.romanticMoods
61
+ .map((mood) => MOOD_DESCRIPTIONS[mood])
62
+ .filter(Boolean)
63
+ .join(", ");
64
+
65
+ if (moodDescriptions) {
66
+ const intensity = selections.romanticIntensity || 70;
67
+ const label = getIntensityLabel(intensity);
68
+ enhanced += `. Apply ${label} ${moodDescriptions}`;
69
+ }
70
+ }
71
+
72
+ if (selections.artStyle && selections.artStyle !== "original") {
73
+ const styleDescription = ART_STYLE_DESCRIPTIONS[selections.artStyle];
74
+ if (styleDescription) {
75
+ const intensity = selections.artStyleIntensity || 80;
76
+ const label = getIntensityLabel(intensity);
77
+ enhanced += `. Render in ${label} applied ${styleDescription}`;
78
+ }
79
+ }
80
+
81
+ if (selections.artist) {
82
+ const artistDescription = ARTIST_STYLE_DESCRIPTIONS[selections.artist];
83
+ if (artistDescription) {
84
+ const intensity = selections.artistIntensity || 70;
85
+ const label = getArtistIntensityLabel(intensity);
86
+ enhanced += `. Create in ${label} ${artistDescription}`;
87
+ }
88
+ }
89
+
90
+ if (selections.wardrobeStyle) {
91
+ const wardrobeDescription = WARDROBE_STYLE_DESCRIPTIONS[selections.wardrobeStyle];
92
+ if (wardrobeDescription) {
93
+ const intensity = selections.wardrobeIntensity || 70;
94
+ const label = getIntensityLabel(intensity);
95
+ enhanced += `. Dress them in ${label} ${wardrobeDescription}`;
96
+ }
97
+ }
98
+
99
+ return enhanced;
100
+ };
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Art Style Selector Component
3
+ * Single-select art style selector with intensity control
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, TouchableOpacity, StyleSheet, ScrollView } from "react-native";
8
+ import {
9
+ AtomicText,
10
+ AtomicIcon,
11
+ useAppDesignTokens,
12
+ } from "@umituz/react-native-design-system";
13
+ import type { CoupleFeatureSelection } from "../../domain/types";
14
+ import { ART_STYLE_OPTIONS } from "../../infrastructure/coupleFeatureRegistry";
15
+
16
+ export interface ArtStyleSelectorProps {
17
+ selection: CoupleFeatureSelection;
18
+ onSelectionChange: (selection: CoupleFeatureSelection) => void;
19
+ translationPrefix: string;
20
+ t: (key: string) => string;
21
+ }
22
+
23
+ export const ArtStyleSelector: React.FC<ArtStyleSelectorProps> = ({
24
+ selection,
25
+ onSelectionChange,
26
+ translationPrefix,
27
+ t,
28
+ }) => {
29
+ const tokens = useAppDesignTokens();
30
+ const selectedStyle = selection.artStyle;
31
+ const intensity = selection.artStyleIntensity || 80;
32
+
33
+ const handleStyleSelect = (styleId: string) => {
34
+ onSelectionChange({ ...selection, artStyle: styleId });
35
+ };
36
+
37
+ const handleIntensityChange = (value: number) => {
38
+ onSelectionChange({ ...selection, artStyleIntensity: value });
39
+ };
40
+
41
+ return (
42
+ <ScrollView style={s.container}>
43
+ <View style={s.section}>
44
+ <AtomicText
45
+ type="bodyMedium"
46
+ style={[s.title, { color: tokens.colors.textPrimary }]}
47
+ >
48
+ {t(`${translationPrefix}.selectStyle`)}
49
+ </AtomicText>
50
+
51
+ <View style={s.grid}>
52
+ {ART_STYLE_OPTIONS.map((style) => {
53
+ const isSelected = selectedStyle === style.id;
54
+ return (
55
+ <TouchableOpacity
56
+ key={style.id}
57
+ style={[
58
+ s.card,
59
+ {
60
+ backgroundColor: isSelected
61
+ ? `${tokens.colors.primary}15`
62
+ : tokens.colors.surface,
63
+ borderColor: isSelected
64
+ ? tokens.colors.primary
65
+ : tokens.colors.borderLight,
66
+ },
67
+ ]}
68
+ onPress={() => handleStyleSelect(style.id)}
69
+ >
70
+ <AtomicIcon
71
+ name={style.iconKey as never}
72
+ size="lg"
73
+ color={isSelected ? "primary" : "textSecondary"}
74
+ />
75
+ <AtomicText
76
+ type="labelMedium"
77
+ style={[
78
+ s.label,
79
+ {
80
+ color: isSelected
81
+ ? tokens.colors.primary
82
+ : tokens.colors.textSecondary,
83
+ fontWeight: isSelected ? "600" : "400",
84
+ },
85
+ ]}
86
+ >
87
+ {t(`${translationPrefix}.styles.${style.labelKey}`)}
88
+ </AtomicText>
89
+ </TouchableOpacity>
90
+ );
91
+ })}
92
+ </View>
93
+ </View>
94
+
95
+ {selectedStyle && selectedStyle !== "original" && (
96
+ <View style={s.section}>
97
+ <View style={s.header}>
98
+ <AtomicText type="bodyMedium" style={[s.title, { color: tokens.colors.textPrimary }]}>
99
+ {t(`${translationPrefix}.intensity`)}
100
+ </AtomicText>
101
+ <AtomicText type="bodyMedium" style={{ color: tokens.colors.primary, fontWeight: "600" }}>
102
+ {intensity}%
103
+ </AtomicText>
104
+ </View>
105
+
106
+ <View style={s.slider}>
107
+ {[25, 50, 75, 100].map((value) => (
108
+ <TouchableOpacity
109
+ key={value}
110
+ style={[
111
+ s.button,
112
+ {
113
+ backgroundColor: intensity === value ? tokens.colors.primary : tokens.colors.surface,
114
+ borderColor: intensity === value ? tokens.colors.primary : tokens.colors.borderLight,
115
+ },
116
+ ]}
117
+ onPress={() => handleIntensityChange(value)}
118
+ >
119
+ <AtomicText
120
+ type="labelSmall"
121
+ style={{
122
+ color: intensity === value ? tokens.colors.onPrimary : tokens.colors.textSecondary,
123
+ }}
124
+ >
125
+ {value}%
126
+ </AtomicText>
127
+ </TouchableOpacity>
128
+ ))}
129
+ </View>
130
+ </View>
131
+ )}
132
+ </ScrollView>
133
+ );
134
+ };
135
+
136
+ const s = StyleSheet.create({
137
+ container: { flex: 1 },
138
+ section: { padding: 16, marginBottom: 8 },
139
+ title: { fontWeight: "600", marginBottom: 12 },
140
+ grid: { flexDirection: "row", flexWrap: "wrap", gap: 12 },
141
+ card: { width: "30%", aspectRatio: 0.85, padding: 12, borderRadius: 12, borderWidth: 2, alignItems: "center", justifyContent: "center" },
142
+ label: { marginTop: 8, textAlign: "center" },
143
+ header: { flexDirection: "row", justifyContent: "space-between", alignItems: "center" },
144
+ slider: { flexDirection: "row", gap: 8, marginTop: 8 },
145
+ button: { flex: 1, padding: 12, borderRadius: 8, borderWidth: 2, alignItems: "center" },
146
+ });
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Artist Style Selector Component
3
+ * Single-select artist style selector with intensity control
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, TouchableOpacity, StyleSheet, ScrollView } from "react-native";
8
+ import {
9
+ AtomicText,
10
+ AtomicIcon,
11
+ useAppDesignTokens,
12
+ } from "@umituz/react-native-design-system";
13
+ import type { CoupleFeatureSelection } from "../../domain/types";
14
+ import { ARTIST_STYLE_OPTIONS } from "../../infrastructure/coupleFeatureRegistry";
15
+
16
+ export interface ArtistStyleSelectorProps {
17
+ selection: CoupleFeatureSelection;
18
+ onSelectionChange: (selection: CoupleFeatureSelection) => void;
19
+ translationPrefix: string;
20
+ t: (key: string) => string;
21
+ }
22
+
23
+ export const ArtistStyleSelector: React.FC<ArtistStyleSelectorProps> = ({
24
+ selection,
25
+ onSelectionChange,
26
+ translationPrefix,
27
+ t,
28
+ }) => {
29
+ const tokens = useAppDesignTokens();
30
+ const selectedArtist = selection.artist;
31
+ const intensity = selection.artistIntensity || 70;
32
+
33
+ const handleArtistSelect = (artistId: string) => {
34
+ onSelectionChange({ ...selection, artist: artistId });
35
+ };
36
+
37
+ const handleIntensityChange = (value: number) => {
38
+ onSelectionChange({ ...selection, artistIntensity: value });
39
+ };
40
+
41
+ return (
42
+ <ScrollView style={s.container}>
43
+ <View style={s.section}>
44
+ <AtomicText
45
+ type="bodyMedium"
46
+ style={[s.title, { color: tokens.colors.textPrimary }]}
47
+ >
48
+ {t(`${translationPrefix}.selectArtist`)}
49
+ </AtomicText>
50
+
51
+ <View style={s.grid}>
52
+ {ARTIST_STYLE_OPTIONS.map((artist) => {
53
+ const isSelected = selectedArtist === artist.id;
54
+ return (
55
+ <TouchableOpacity
56
+ key={artist.id}
57
+ style={[
58
+ s.card,
59
+ {
60
+ backgroundColor: isSelected ? `${tokens.colors.primary}15` : tokens.colors.surface,
61
+ borderColor: isSelected ? tokens.colors.primary : tokens.colors.borderLight,
62
+ },
63
+ ]}
64
+ onPress={() => handleArtistSelect(artist.id)}
65
+ >
66
+ <View style={[s.iconContainer, { backgroundColor: isSelected ? tokens.colors.primary : tokens.colors.surfaceVariant }]}>
67
+ <AtomicIcon name={artist.iconKey as never} size="lg" color={isSelected ? "onPrimary" : "textSecondary"} />
68
+ </View>
69
+ <AtomicText
70
+ type="labelMedium"
71
+ style={[s.label, { color: isSelected ? tokens.colors.primary : tokens.colors.textPrimary, fontWeight: isSelected ? "600" : "500" }]}
72
+ >
73
+ {t(`${translationPrefix}.artists.${artist.labelKey}`)}
74
+ </AtomicText>
75
+ </TouchableOpacity>
76
+ );
77
+ })}
78
+ </View>
79
+ </View>
80
+
81
+ {selectedArtist && (
82
+ <View style={s.section}>
83
+ <View style={s.header}>
84
+ <AtomicText type="bodyMedium" style={[s.title, { color: tokens.colors.textPrimary }]}>
85
+ {t(`${translationPrefix}.intensity`)}
86
+ </AtomicText>
87
+ <AtomicText type="bodyMedium" style={{ color: tokens.colors.primary, fontWeight: "600" }}>
88
+ {intensity}%
89
+ </AtomicText>
90
+ </View>
91
+
92
+ <View style={s.slider}>
93
+ {[25, 50, 75, 100].map((value) => (
94
+ <TouchableOpacity
95
+ key={value}
96
+ style={[s.button, { backgroundColor: intensity === value ? tokens.colors.primary : tokens.colors.surface, borderColor: intensity === value ? tokens.colors.primary : tokens.colors.borderLight }]}
97
+ onPress={() => handleIntensityChange(value)}
98
+ >
99
+ <AtomicText type="labelSmall" style={{ color: intensity === value ? tokens.colors.onPrimary : tokens.colors.textSecondary }}>
100
+ {value}%
101
+ </AtomicText>
102
+ </TouchableOpacity>
103
+ ))}
104
+ </View>
105
+ </View>
106
+ )}
107
+ </ScrollView>
108
+ );
109
+ };
110
+
111
+ const s = StyleSheet.create({
112
+ container: { flex: 1 },
113
+ section: { padding: 16, marginBottom: 8 },
114
+ title: { fontWeight: "600", marginBottom: 12 },
115
+ grid: { flexDirection: "row", flexWrap: "wrap", gap: 12 },
116
+ card: { width: "47%", padding: 12, borderRadius: 12, borderWidth: 2, alignItems: "center", justifyContent: "center" },
117
+ iconContainer: { width: 64, height: 64, borderRadius: 32, alignItems: "center", justifyContent: "center", marginBottom: 8 },
118
+ label: { textAlign: "center" },
119
+ header: { flexDirection: "row", justifyContent: "space-between", alignItems: "center" },
120
+ slider: { flexDirection: "row", gap: 8, marginTop: 8 },
121
+ button: { flex: 1, padding: 12, borderRadius: 8, borderWidth: 2, alignItems: "center" },
122
+ });
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Romantic Mood Selector Component
3
+ * Multi-select mood selector with intensity control
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, TouchableOpacity, StyleSheet, ScrollView } from "react-native";
8
+ import {
9
+ AtomicText,
10
+ AtomicIcon,
11
+ useAppDesignTokens,
12
+ } from "@umituz/react-native-design-system";
13
+ import type { CoupleFeatureSelection } from "../../domain/types";
14
+ import { ROMANTIC_MOOD_OPTIONS } from "../../infrastructure/coupleFeatureRegistry";
15
+
16
+ export interface RomanticMoodSelectorProps {
17
+ selection: CoupleFeatureSelection;
18
+ onSelectionChange: (selection: CoupleFeatureSelection) => void;
19
+ translationPrefix: string;
20
+ t: (key: string) => string;
21
+ }
22
+
23
+ export const RomanticMoodSelector: React.FC<RomanticMoodSelectorProps> = ({
24
+ selection,
25
+ onSelectionChange,
26
+ translationPrefix,
27
+ t,
28
+ }) => {
29
+ const tokens = useAppDesignTokens();
30
+ const selectedMoods = selection.romanticMoods || [];
31
+ const intensity = selection.romanticIntensity || 70;
32
+
33
+ const handleMoodToggle = (moodId: string) => {
34
+ const newMoods = selectedMoods.includes(moodId)
35
+ ? selectedMoods.filter((id) => id !== moodId)
36
+ : [...selectedMoods, moodId];
37
+ onSelectionChange({ ...selection, romanticMoods: newMoods });
38
+ };
39
+
40
+ const handleIntensityChange = (value: number) => {
41
+ onSelectionChange({ ...selection, romanticIntensity: value });
42
+ };
43
+
44
+ return (
45
+ <ScrollView style={s.container}>
46
+ <View style={s.section}>
47
+ <AtomicText
48
+ type="bodyMedium"
49
+ style={[s.title, { color: tokens.colors.textPrimary }]}
50
+ >
51
+ {t(`${translationPrefix}.selectMoods`)}
52
+ </AtomicText>
53
+
54
+ <View style={s.grid}>
55
+ {ROMANTIC_MOOD_OPTIONS.map((mood) => {
56
+ const isSelected = selectedMoods.includes(mood.id);
57
+ return (
58
+ <TouchableOpacity
59
+ key={mood.id}
60
+ style={[
61
+ s.card,
62
+ {
63
+ backgroundColor: isSelected
64
+ ? `${tokens.colors.primary}15`
65
+ : tokens.colors.surface,
66
+ borderColor: isSelected
67
+ ? tokens.colors.primary
68
+ : tokens.colors.borderLight,
69
+ },
70
+ ]}
71
+ onPress={() => handleMoodToggle(mood.id)}
72
+ >
73
+ <AtomicIcon
74
+ name={mood.iconKey as never}
75
+ size="md"
76
+ color={isSelected ? "primary" : "textSecondary"}
77
+ />
78
+ <AtomicText
79
+ type="labelSmall"
80
+ style={[
81
+ s.label,
82
+ {
83
+ color: isSelected
84
+ ? tokens.colors.primary
85
+ : tokens.colors.textSecondary,
86
+ fontWeight: isSelected ? "600" : "400",
87
+ },
88
+ ]}
89
+ >
90
+ {t(`${translationPrefix}.moods.${mood.labelKey}`)}
91
+ </AtomicText>
92
+ </TouchableOpacity>
93
+ );
94
+ })}
95
+ </View>
96
+ </View>
97
+
98
+ <View style={s.section}>
99
+ <View style={s.header}>
100
+ <AtomicText type="bodyMedium" style={[s.title, { color: tokens.colors.textPrimary }]}>
101
+ {t(`${translationPrefix}.intensity`)}
102
+ </AtomicText>
103
+ <AtomicText type="bodyMedium" style={{ color: tokens.colors.primary, fontWeight: "600" }}>
104
+ {intensity}%
105
+ </AtomicText>
106
+ </View>
107
+
108
+ <View style={s.slider}>
109
+ {[25, 50, 75, 100].map((value) => (
110
+ <TouchableOpacity
111
+ key={value}
112
+ style={[
113
+ s.button,
114
+ {
115
+ backgroundColor: intensity === value ? tokens.colors.primary : tokens.colors.surface,
116
+ borderColor: intensity === value ? tokens.colors.primary : tokens.colors.borderLight,
117
+ },
118
+ ]}
119
+ onPress={() => handleIntensityChange(value)}
120
+ >
121
+ <AtomicText
122
+ type="labelSmall"
123
+ style={{
124
+ color: intensity === value ? tokens.colors.onPrimary : tokens.colors.textSecondary,
125
+ }}
126
+ >
127
+ {value}%
128
+ </AtomicText>
129
+ </TouchableOpacity>
130
+ ))}
131
+ </View>
132
+ </View>
133
+ </ScrollView>
134
+ );
135
+ };
136
+
137
+ const s = StyleSheet.create({
138
+ container: { flex: 1 },
139
+ section: { padding: 16, marginBottom: 8 },
140
+ title: { fontWeight: "600", marginBottom: 12 },
141
+ grid: { flexDirection: "row", flexWrap: "wrap", gap: 12 },
142
+ card: { width: "22%", aspectRatio: 1, padding: 8, borderRadius: 12, borderWidth: 2, alignItems: "center", justifyContent: "center" },
143
+ label: { marginTop: 4, textAlign: "center", fontSize: 10 },
144
+ header: { flexDirection: "row", justifyContent: "space-between", alignItems: "center" },
145
+ slider: { flexDirection: "row", gap: 8, marginTop: 8 },
146
+ button: { flex: 1, padding: 12, borderRadius: 8, borderWidth: 2, alignItems: "center" },
147
+ });
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Wardrobe Selector Component
3
+ * Wardrobe style selector with intensity control
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, TouchableOpacity, StyleSheet, ScrollView } from "react-native";
8
+ import {
9
+ AtomicText,
10
+ AtomicIcon,
11
+ useAppDesignTokens,
12
+ } from "@umituz/react-native-design-system";
13
+ import type { CoupleFeatureSelection } from "../../domain/types";
14
+
15
+ const WARDROBE_STYLES = [
16
+ { id: "casual", iconKey: "shirt-outline" },
17
+ { id: "formal", iconKey: "briefcase-outline" },
18
+ { id: "sporty", iconKey: "fitness-outline" },
19
+ { id: "elegant", iconKey: "star-outline" },
20
+ ];
21
+
22
+ export interface WardrobeSelectorProps {
23
+ selection: CoupleFeatureSelection;
24
+ onSelectionChange: (selection: CoupleFeatureSelection) => void;
25
+ translationPrefix: string;
26
+ t: (key: string) => string;
27
+ }
28
+
29
+ export const WardrobeSelector: React.FC<WardrobeSelectorProps> = ({
30
+ selection,
31
+ onSelectionChange,
32
+ translationPrefix,
33
+ t,
34
+ }) => {
35
+ const tokens = useAppDesignTokens();
36
+ const selectedStyle = selection.wardrobeStyle;
37
+ const intensity = selection.wardrobeIntensity || 70;
38
+
39
+ const handleStyleSelect = (styleId: string) => {
40
+ onSelectionChange({ ...selection, wardrobeStyle: styleId });
41
+ };
42
+
43
+ const handleIntensityChange = (value: number) => {
44
+ onSelectionChange({ ...selection, wardrobeIntensity: value });
45
+ };
46
+
47
+ return (
48
+ <ScrollView style={s.container}>
49
+ <View style={s.section}>
50
+ <AtomicText
51
+ type="bodyMedium"
52
+ style={[s.title, { color: tokens.colors.textPrimary }]}
53
+ >
54
+ {t(`${translationPrefix}.selectStyle`)}
55
+ </AtomicText>
56
+
57
+ <View style={s.grid}>
58
+ {WARDROBE_STYLES.map((style) => {
59
+ const isSelected = selectedStyle === style.id;
60
+ return (
61
+ <TouchableOpacity
62
+ key={style.id}
63
+ style={[
64
+ s.card,
65
+ {
66
+ backgroundColor: isSelected
67
+ ? `${tokens.colors.primary}15`
68
+ : tokens.colors.surface,
69
+ borderColor: isSelected
70
+ ? tokens.colors.primary
71
+ : tokens.colors.borderLight,
72
+ },
73
+ ]}
74
+ onPress={() => handleStyleSelect(style.id)}
75
+ >
76
+ <AtomicIcon
77
+ name={style.iconKey as never}
78
+ size="lg"
79
+ color={isSelected ? "primary" : "textSecondary"}
80
+ />
81
+ <AtomicText
82
+ type="labelMedium"
83
+ style={[
84
+ s.label,
85
+ {
86
+ color: isSelected
87
+ ? tokens.colors.primary
88
+ : tokens.colors.textSecondary,
89
+ fontWeight: isSelected ? "600" : "400",
90
+ },
91
+ ]}
92
+ >
93
+ {t(`${translationPrefix}.styles.${style.id}`)}
94
+ </AtomicText>
95
+ </TouchableOpacity>
96
+ );
97
+ })}
98
+ </View>
99
+ </View>
100
+
101
+ {selectedStyle && (
102
+ <View style={s.section}>
103
+ <View style={s.header}>
104
+ <AtomicText type="bodyMedium" style={[s.title, { color: tokens.colors.textPrimary }]}>
105
+ {t(`${translationPrefix}.intensity`)}
106
+ </AtomicText>
107
+ <AtomicText type="bodyMedium" style={{ color: tokens.colors.primary, fontWeight: "600" }}>
108
+ {intensity}%
109
+ </AtomicText>
110
+ </View>
111
+
112
+ <View style={s.slider}>
113
+ {[25, 50, 75, 100].map((value) => (
114
+ <TouchableOpacity
115
+ key={value}
116
+ style={[s.button, { backgroundColor: intensity === value ? tokens.colors.primary : tokens.colors.surface, borderColor: intensity === value ? tokens.colors.primary : tokens.colors.borderLight }]}
117
+ onPress={() => handleIntensityChange(value)}
118
+ >
119
+ <AtomicText type="labelSmall" style={{ color: intensity === value ? tokens.colors.onPrimary : tokens.colors.textSecondary }}>
120
+ {value}%
121
+ </AtomicText>
122
+ </TouchableOpacity>
123
+ ))}
124
+ </View>
125
+ </View>
126
+ )}
127
+ </ScrollView>
128
+ );
129
+ };
130
+
131
+ const s = StyleSheet.create({
132
+ container: { flex: 1 },
133
+ section: { padding: 16, marginBottom: 8 },
134
+ title: { fontWeight: "600", marginBottom: 12 },
135
+ grid: { flexDirection: "row", flexWrap: "wrap", gap: 12 },
136
+ card: { width: "47%", padding: 16, borderRadius: 12, borderWidth: 2, alignItems: "center", justifyContent: "center" },
137
+ label: { marginTop: 8, textAlign: "center" },
138
+ header: { flexDirection: "row", justifyContent: "space-between", alignItems: "center" },
139
+ slider: { flexDirection: "row", gap: 8, marginTop: 8 },
140
+ button: { flex: 1, padding: 12, borderRadius: 8, borderWidth: 2, alignItems: "center" },
141
+ });
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Couple Future Components
3
+ */
4
+
5
+ export { RomanticMoodSelector } from "./RomanticMoodSelector";
6
+ export type { RomanticMoodSelectorProps } from "./RomanticMoodSelector";
7
+ export { ArtStyleSelector } from "./ArtStyleSelector";
8
+ export type { ArtStyleSelectorProps } from "./ArtStyleSelector";
9
+ export { ArtistStyleSelector } from "./ArtistStyleSelector";
10
+ export type { ArtistStyleSelectorProps } from "./ArtistStyleSelector";
11
+ export { WardrobeSelector } from "./WardrobeSelector";
12
+ export type { WardrobeSelectorProps } from "./WardrobeSelector";
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Couple Feature Screen
3
+ * Generic screen for couple feature selection
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, StyleSheet, ScrollView } from "react-native";
8
+ import {
9
+ AtomicButton,
10
+ AtomicText,
11
+ useAppDesignTokens,
12
+ } from "@umituz/react-native-design-system";
13
+ import type { CoupleFeatureId, CoupleFeatureSelection } from "../../domain/types";
14
+ import { COUPLE_FEATURE_CONFIGS } from "../../infrastructure/coupleFeatureRegistry";
15
+ import { RomanticMoodSelector } from "../components/RomanticMoodSelector";
16
+ import { ArtStyleSelector } from "../components/ArtStyleSelector";
17
+ import { ArtistStyleSelector } from "../components/ArtistStyleSelector";
18
+ import { WardrobeSelector } from "../components/WardrobeSelector";
19
+
20
+ interface CoupleFeatureScreenProps {
21
+ featureId: CoupleFeatureId;
22
+ selection: CoupleFeatureSelection;
23
+ onSelectionChange: (selection: CoupleFeatureSelection) => void;
24
+ onContinue: () => void;
25
+ onBack: () => void;
26
+ t: (key: string) => string;
27
+ }
28
+
29
+ const FeatureComponentMap = {
30
+ "romantic-mood": RomanticMoodSelector,
31
+ "art-style": ArtStyleSelector,
32
+ "artist-style": ArtistStyleSelector,
33
+ "wardrobe": WardrobeSelector,
34
+ };
35
+
36
+ export const CoupleFeatureScreen: React.FC<CoupleFeatureScreenProps> = ({
37
+ featureId,
38
+ selection,
39
+ onSelectionChange,
40
+ onContinue,
41
+ onBack,
42
+ t,
43
+ }) => {
44
+ const tokens = useAppDesignTokens();
45
+ const config = COUPLE_FEATURE_CONFIGS[featureId];
46
+
47
+ if (!config) return null;
48
+
49
+ const FeatureComponent = FeatureComponentMap[featureId];
50
+ if (!FeatureComponent) return null;
51
+
52
+ const selectorProps = {
53
+ selection,
54
+ onSelectionChange,
55
+ translationPrefix: config.translationPrefix,
56
+ t,
57
+ };
58
+
59
+ return (
60
+ <View
61
+ style={[
62
+ styles.container,
63
+ { backgroundColor: tokens.colors.backgroundPrimary },
64
+ ]}
65
+ >
66
+ <View style={styles.header}>
67
+ <AtomicText
68
+ type="headlineMedium"
69
+ style={{ color: tokens.colors.textPrimary }}
70
+ >
71
+ {t(`${config.translationPrefix}.title`)}
72
+ </AtomicText>
73
+ </View>
74
+
75
+ <ScrollView style={styles.content} showsVerticalScrollIndicator={false}>
76
+ <FeatureComponent {...selectorProps} />
77
+ </ScrollView>
78
+
79
+ <View
80
+ style={[
81
+ styles.footer,
82
+ { borderTopColor: tokens.colors.borderLight },
83
+ ]}
84
+ >
85
+ <AtomicButton
86
+ title={t("common.back")}
87
+ onPress={onBack}
88
+ variant="secondary"
89
+ style={styles.backButton}
90
+ />
91
+ <AtomicButton
92
+ title={t("common.continue")}
93
+ onPress={onContinue}
94
+ variant="primary"
95
+ style={styles.continueButton}
96
+ />
97
+ </View>
98
+ </View>
99
+ );
100
+ };
101
+
102
+ const styles = StyleSheet.create({
103
+ container: {
104
+ flex: 1,
105
+ },
106
+ header: {
107
+ padding: 16,
108
+ paddingTop: 20,
109
+ },
110
+ content: {
111
+ flex: 1,
112
+ },
113
+ footer: {
114
+ flexDirection: "row",
115
+ padding: 16,
116
+ borderTopWidth: 1,
117
+ gap: 12,
118
+ },
119
+ backButton: {
120
+ flex: 1,
121
+ },
122
+ continueButton: {
123
+ flex: 2,
124
+ },
125
+ });