@umituz/react-native-ai-generation-content 1.17.246 → 1.17.247
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/couple-future/domain/types.ts +17 -0
- package/src/features/couple-future/index.ts +25 -0
- package/src/features/couple-future/infrastructure/coupleFeatureRegistry.ts +76 -0
- package/src/features/couple-future/infrastructure/couplePromptEnhancer.ts +84 -0
- package/src/features/couple-future/presentation/components/ArtStyleSelector.tsx +146 -0
- package/src/features/couple-future/presentation/components/ArtistStyleSelector.tsx +122 -0
- package/src/features/couple-future/presentation/components/RomanticMoodSelector.tsx +147 -0
- package/src/features/couple-future/presentation/components/WardrobeSelector.tsx +87 -0
- package/src/features/couple-future/presentation/components/index.ts +12 -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.247",
|
|
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,32 @@ 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 {
|
|
32
|
+
COUPLE_FEATURE_CONFIGS,
|
|
33
|
+
ROMANTIC_MOOD_OPTIONS,
|
|
34
|
+
ART_STYLE_OPTIONS,
|
|
35
|
+
ARTIST_STYLE_OPTIONS,
|
|
36
|
+
} from "./infrastructure/coupleFeatureRegistry";
|
|
37
|
+
export type {
|
|
38
|
+
CoupleFeatureConfig,
|
|
39
|
+
CoupleFeatureOption,
|
|
40
|
+
} from "./infrastructure/coupleFeatureRegistry";
|
|
41
|
+
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,84 @@
|
|
|
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 getIntensityLabel = (intensity: number): string => {
|
|
35
|
+
if (intensity >= 75) return "strong";
|
|
36
|
+
if (intensity >= 50) return "moderate";
|
|
37
|
+
return "subtle";
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const getArtistIntensityLabel = (intensity: number): string => {
|
|
41
|
+
if (intensity >= 75) return "strong homage to";
|
|
42
|
+
if (intensity >= 50) return "inspired by";
|
|
43
|
+
return "subtle influence of";
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const enhanceCouplePrompt = (
|
|
47
|
+
basePrompt: string,
|
|
48
|
+
selections: CoupleFeatureSelection
|
|
49
|
+
): string => {
|
|
50
|
+
let enhanced = basePrompt;
|
|
51
|
+
|
|
52
|
+
if (selections.romanticMoods && selections.romanticMoods.length > 0) {
|
|
53
|
+
const moodDescriptions = selections.romanticMoods
|
|
54
|
+
.map((mood) => MOOD_DESCRIPTIONS[mood])
|
|
55
|
+
.filter(Boolean)
|
|
56
|
+
.join(", ");
|
|
57
|
+
|
|
58
|
+
if (moodDescriptions) {
|
|
59
|
+
const intensity = selections.romanticIntensity || 70;
|
|
60
|
+
const label = getIntensityLabel(intensity);
|
|
61
|
+
enhanced += `. Apply ${label} ${moodDescriptions}`;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (selections.artStyle && selections.artStyle !== "original") {
|
|
66
|
+
const styleDescription = ART_STYLE_DESCRIPTIONS[selections.artStyle];
|
|
67
|
+
if (styleDescription) {
|
|
68
|
+
const intensity = selections.artStyleIntensity || 80;
|
|
69
|
+
const label = getIntensityLabel(intensity);
|
|
70
|
+
enhanced += `. Render in ${label} applied ${styleDescription}`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (selections.artist) {
|
|
75
|
+
const artistDescription = ARTIST_STYLE_DESCRIPTIONS[selections.artist];
|
|
76
|
+
if (artistDescription) {
|
|
77
|
+
const intensity = selections.artistIntensity || 70;
|
|
78
|
+
const label = getArtistIntensityLabel(intensity);
|
|
79
|
+
enhanced += `. Create in ${label} ${artistDescription}`;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return enhanced;
|
|
84
|
+
};
|
|
@@ -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,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wardrobe Selector Component
|
|
3
|
+
* Simplified placeholder for Phase 1
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, StyleSheet } 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
|
+
export interface WardrobeSelectorProps {
|
|
16
|
+
selection: CoupleFeatureSelection;
|
|
17
|
+
onSelectionChange: (selection: CoupleFeatureSelection) => void;
|
|
18
|
+
translationPrefix: string;
|
|
19
|
+
t: (key: string) => string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const WardrobeSelector: React.FC<WardrobeSelectorProps> = ({
|
|
23
|
+
translationPrefix,
|
|
24
|
+
t,
|
|
25
|
+
}) => {
|
|
26
|
+
const tokens = useAppDesignTokens();
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<View style={s.container}>
|
|
30
|
+
<View
|
|
31
|
+
style={[
|
|
32
|
+
s.card,
|
|
33
|
+
{
|
|
34
|
+
backgroundColor: tokens.colors.surface,
|
|
35
|
+
borderColor: tokens.colors.borderLight,
|
|
36
|
+
},
|
|
37
|
+
]}
|
|
38
|
+
>
|
|
39
|
+
<View
|
|
40
|
+
style={[
|
|
41
|
+
s.iconContainer,
|
|
42
|
+
{ backgroundColor: tokens.colors.surfaceVariant },
|
|
43
|
+
]}
|
|
44
|
+
>
|
|
45
|
+
<AtomicIcon name="checkroom" size="xl" color="textSecondary" />
|
|
46
|
+
</View>
|
|
47
|
+
|
|
48
|
+
<AtomicText
|
|
49
|
+
type="headlineSmall"
|
|
50
|
+
style={[s.title, { color: tokens.colors.textPrimary }]}
|
|
51
|
+
>
|
|
52
|
+
{t(`${translationPrefix}.title`)}
|
|
53
|
+
</AtomicText>
|
|
54
|
+
|
|
55
|
+
<AtomicText
|
|
56
|
+
type="bodyMedium"
|
|
57
|
+
style={[s.subtitle, { color: tokens.colors.textSecondary }]}
|
|
58
|
+
>
|
|
59
|
+
{t(`${translationPrefix}.comingSoon`)}
|
|
60
|
+
</AtomicText>
|
|
61
|
+
|
|
62
|
+
<View
|
|
63
|
+
style={[
|
|
64
|
+
s.badge,
|
|
65
|
+
{ backgroundColor: `${tokens.colors.primary}15` },
|
|
66
|
+
]}
|
|
67
|
+
>
|
|
68
|
+
<AtomicText
|
|
69
|
+
type="labelSmall"
|
|
70
|
+
style={{ color: tokens.colors.primary, fontWeight: "600" }}
|
|
71
|
+
>
|
|
72
|
+
{t("common.comingSoon")}
|
|
73
|
+
</AtomicText>
|
|
74
|
+
</View>
|
|
75
|
+
</View>
|
|
76
|
+
</View>
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const s = StyleSheet.create({
|
|
81
|
+
container: { flex: 1, padding: 16, justifyContent: "center" },
|
|
82
|
+
card: { padding: 24, borderRadius: 16, borderWidth: 2, alignItems: "center" },
|
|
83
|
+
iconContainer: { width: 80, height: 80, borderRadius: 40, alignItems: "center", justifyContent: "center", marginBottom: 16 },
|
|
84
|
+
title: { fontWeight: "600", marginBottom: 8, textAlign: "center" },
|
|
85
|
+
subtitle: { marginBottom: 16, textAlign: "center" },
|
|
86
|
+
badge: { paddingHorizontal: 16, paddingVertical: 8, borderRadius: 8 },
|
|
87
|
+
});
|
|
@@ -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";
|