@umituz/react-native-ai-generation-content 1.17.16 → 1.17.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/domains/creations/presentation/screens/CreationsGalleryScreen.tsx +98 -69
- package/src/features/text-to-image/index.ts +92 -4
- package/src/features/text-to-image/presentation/components/AspectRatioSelector.tsx +98 -0
- package/src/features/text-to-image/presentation/components/ExamplePrompts.tsx +88 -0
- package/src/features/text-to-image/presentation/components/ImageSizeSelector.tsx +98 -0
- package/src/features/text-to-image/presentation/components/NumImagesSelector.tsx +93 -0
- package/src/features/text-to-image/presentation/components/OutputFormatSelector.tsx +98 -0
- package/src/features/text-to-image/presentation/components/PromptInput.tsx +90 -0
- package/src/features/text-to-image/presentation/components/SettingsSheet.tsx +139 -0
- package/src/features/text-to-image/presentation/components/StyleSelector.tsx +110 -0
- package/src/features/text-to-image/presentation/components/TextToImageGenerateButton.tsx +84 -0
- package/src/features/text-to-image/presentation/components/index.ts +41 -0
- package/src/features/text-to-image/presentation/hooks/index.ts +25 -0
- package/src/features/text-to-image/presentation/hooks/useFormState.ts +103 -0
- package/src/features/text-to-image/presentation/hooks/useGeneration.ts +99 -0
- package/src/features/text-to-image/presentation/hooks/useTextToImageForm.ts +58 -0
- package/src/features/text-to-image/presentation/index.ts +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
declare const __DEV__: boolean;
|
|
2
2
|
|
|
3
3
|
import React, { useMemo, useCallback, useState } from "react";
|
|
4
|
+
import { View, FlatList, RefreshControl, StyleSheet } from "react-native";
|
|
5
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
4
6
|
import {
|
|
5
7
|
useAppDesignTokens,
|
|
6
8
|
useAlert,
|
|
@@ -9,7 +11,7 @@ import {
|
|
|
9
11
|
useSharing,
|
|
10
12
|
FilterBottomSheet,
|
|
11
13
|
type BottomSheetModalRef,
|
|
12
|
-
|
|
14
|
+
type DesignTokens,
|
|
13
15
|
} from "@umituz/react-native-design-system";
|
|
14
16
|
import { useFocusEffect } from "@react-navigation/native";
|
|
15
17
|
import { useCreations } from "../hooks/useCreations";
|
|
@@ -17,7 +19,7 @@ import { useDeleteCreation } from "../hooks/useDeleteCreation";
|
|
|
17
19
|
import { useCreationsFilter } from "../hooks/useCreationsFilter";
|
|
18
20
|
import {
|
|
19
21
|
GalleryHeader,
|
|
20
|
-
|
|
22
|
+
CreationCard,
|
|
21
23
|
CreationImageViewer,
|
|
22
24
|
GalleryEmptyStates,
|
|
23
25
|
} from "../components";
|
|
@@ -26,6 +28,7 @@ import type { Creation } from "../../domain/entities/Creation";
|
|
|
26
28
|
import type { CreationsConfig } from "../../domain/value-objects/CreationsConfig";
|
|
27
29
|
import type { ICreationsRepository } from "../../domain/repositories/ICreationsRepository";
|
|
28
30
|
import { CreationDetailScreen } from "./CreationDetailScreen";
|
|
31
|
+
import { CreationsProvider } from "../components/CreationsProvider";
|
|
29
32
|
|
|
30
33
|
interface CreationsGalleryScreenProps {
|
|
31
34
|
readonly userId: string | null;
|
|
@@ -40,8 +43,6 @@ interface CreationsGalleryScreenProps {
|
|
|
40
43
|
readonly showFilter?: boolean;
|
|
41
44
|
}
|
|
42
45
|
|
|
43
|
-
import { CreationsProvider } from "../components/CreationsProvider";
|
|
44
|
-
|
|
45
46
|
export function CreationsGalleryScreen(props: CreationsGalleryScreenProps) {
|
|
46
47
|
return (
|
|
47
48
|
<CreationsProvider config={props.config} t={props.t}>
|
|
@@ -62,6 +63,7 @@ function CreationsGalleryScreenContent({
|
|
|
62
63
|
emptyActionLabel,
|
|
63
64
|
showFilter = config.showFilter ?? true,
|
|
64
65
|
}: CreationsGalleryScreenProps) {
|
|
66
|
+
const insets = useSafeAreaInsets();
|
|
65
67
|
const tokens = useAppDesignTokens();
|
|
66
68
|
const { share } = useSharing();
|
|
67
69
|
const alert = useAlert();
|
|
@@ -76,14 +78,12 @@ function CreationsGalleryScreenContent({
|
|
|
76
78
|
const deleteMutation = useDeleteCreation({ userId, repository });
|
|
77
79
|
const { filtered, selectedIds, toggleFilter, clearFilters, isFiltered } = useCreationsFilter({ creations });
|
|
78
80
|
|
|
79
|
-
// Refetch creations when screen comes into focus
|
|
80
81
|
useFocusEffect(
|
|
81
82
|
useCallback(() => {
|
|
82
83
|
void refetch();
|
|
83
84
|
}, [refetch])
|
|
84
85
|
);
|
|
85
86
|
|
|
86
|
-
// Prepare data for UI using utils
|
|
87
87
|
const allCategories = useMemo(
|
|
88
88
|
() => getFilterCategoriesFromConfig(config, t),
|
|
89
89
|
[config, t],
|
|
@@ -118,27 +118,62 @@ function CreationsGalleryScreenContent({
|
|
|
118
118
|
);
|
|
119
119
|
}, [alert, config, deleteMutation, t]);
|
|
120
120
|
|
|
121
|
-
// Handle viewing a creation - shows detail screen
|
|
122
121
|
const handleView = useCallback((creation: Creation) => {
|
|
123
122
|
setSelectedCreation(creation);
|
|
124
123
|
}, []);
|
|
125
124
|
|
|
126
|
-
// Handle favorite toggle
|
|
127
125
|
const handleFavorite = useCallback((creation: Creation, isFavorite: boolean) => {
|
|
128
126
|
void (async () => {
|
|
129
127
|
if (!userId) return;
|
|
130
|
-
const success = await repository.updateFavorite(
|
|
131
|
-
userId,
|
|
132
|
-
creation.id,
|
|
133
|
-
isFavorite,
|
|
134
|
-
);
|
|
128
|
+
const success = await repository.updateFavorite(userId, creation.id, isFavorite);
|
|
135
129
|
if (success) {
|
|
136
130
|
void refetch();
|
|
137
131
|
}
|
|
138
132
|
})();
|
|
139
133
|
}, [userId, repository, refetch]);
|
|
140
134
|
|
|
141
|
-
const
|
|
135
|
+
const styles = useStyles(tokens, insets);
|
|
136
|
+
|
|
137
|
+
const renderItem = useCallback(
|
|
138
|
+
({ item }: { item: Creation }) => (
|
|
139
|
+
<CreationCard
|
|
140
|
+
creation={item}
|
|
141
|
+
callbacks={{
|
|
142
|
+
onPress: () => handleView(item),
|
|
143
|
+
onShare: async () => handleShare(item),
|
|
144
|
+
onDelete: () => handleDelete(item),
|
|
145
|
+
onFavorite: () => handleFavorite(item, !item.isFavorite),
|
|
146
|
+
}}
|
|
147
|
+
/>
|
|
148
|
+
),
|
|
149
|
+
[handleView, handleShare, handleDelete, handleFavorite]
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const renderHeader = useMemo(() => {
|
|
153
|
+
if ((!creations || creations.length === 0) && !isLoading) return null;
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<View style={styles.header}>
|
|
157
|
+
<GalleryHeader
|
|
158
|
+
title={t(config.translations.title)}
|
|
159
|
+
count={filtered.length}
|
|
160
|
+
countLabel={t(config.translations.photoCount)}
|
|
161
|
+
isFiltered={isFiltered}
|
|
162
|
+
showFilter={showFilter}
|
|
163
|
+
filterLabel={t(config.translations.filterLabel)}
|
|
164
|
+
onFilterPress={() => {
|
|
165
|
+
if (__DEV__) {
|
|
166
|
+
// eslint-disable-next-line no-console
|
|
167
|
+
console.log('[CreationsGallery] Filter button pressed');
|
|
168
|
+
}
|
|
169
|
+
filterSheetRef.current?.present();
|
|
170
|
+
}}
|
|
171
|
+
/>
|
|
172
|
+
</View>
|
|
173
|
+
);
|
|
174
|
+
}, [creations, isLoading, filtered.length, isFiltered, showFilter, t, config, styles.header]);
|
|
175
|
+
|
|
176
|
+
const renderEmpty = useMemo(() => (
|
|
142
177
|
<GalleryEmptyStates
|
|
143
178
|
isLoading={isLoading}
|
|
144
179
|
creations={creations}
|
|
@@ -166,63 +201,37 @@ function CreationsGalleryScreenContent({
|
|
|
166
201
|
}
|
|
167
202
|
|
|
168
203
|
return (
|
|
169
|
-
|
|
170
|
-
<
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
// eslint-disable-next-line no-console
|
|
188
|
-
console.log('[CreationsGallery] filterSheetRef.current:', filterSheetRef.current);
|
|
189
|
-
// eslint-disable-next-line no-console
|
|
190
|
-
console.log('[CreationsGallery] allCategories:', allCategories);
|
|
191
|
-
}
|
|
192
|
-
filterSheetRef.current?.present();
|
|
193
|
-
}}
|
|
194
|
-
/>
|
|
195
|
-
)
|
|
204
|
+
<View style={[styles.container, { backgroundColor: tokens.colors.background }]}>
|
|
205
|
+
<FlatList
|
|
206
|
+
data={filtered}
|
|
207
|
+
renderItem={renderItem}
|
|
208
|
+
keyExtractor={(item) => item.id}
|
|
209
|
+
ListHeaderComponent={renderHeader}
|
|
210
|
+
ListEmptyComponent={renderEmpty}
|
|
211
|
+
contentContainerStyle={[
|
|
212
|
+
styles.listContent,
|
|
213
|
+
(!filtered || filtered.length === 0) && styles.emptyContent,
|
|
214
|
+
]}
|
|
215
|
+
showsVerticalScrollIndicator={false}
|
|
216
|
+
refreshControl={
|
|
217
|
+
<RefreshControl
|
|
218
|
+
refreshing={isLoading}
|
|
219
|
+
onRefresh={() => void refetch()}
|
|
220
|
+
tintColor={tokens.colors.primary}
|
|
221
|
+
/>
|
|
196
222
|
}
|
|
197
|
-
|
|
198
|
-
{/* Main Content Grid - handles empty/loading via ListEmptyComponent */}
|
|
199
|
-
<CreationsGrid
|
|
200
|
-
creations={filtered}
|
|
201
|
-
isLoading={isLoading}
|
|
202
|
-
onRefresh={() => void refetch()}
|
|
203
|
-
onPress={(creation) => handleView(creation as Creation)}
|
|
204
|
-
onShare={async (creation) => handleShare(creation as Creation)}
|
|
205
|
-
onDelete={(creation) => handleDelete(creation as Creation)}
|
|
206
|
-
onFavorite={(creation) => {
|
|
207
|
-
const c = creation as Creation;
|
|
208
|
-
handleFavorite(c, !c.isFavorite);
|
|
209
|
-
}}
|
|
210
|
-
contentContainerStyle={{ paddingBottom: tokens.spacing.xl }}
|
|
211
|
-
ListEmptyComponent={renderEmptyComponent}
|
|
212
|
-
/>
|
|
223
|
+
/>
|
|
213
224
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
</ScreenLayout>
|
|
225
|
+
<CreationImageViewer
|
|
226
|
+
creations={filtered}
|
|
227
|
+
visible={viewerVisible}
|
|
228
|
+
index={viewerIndex}
|
|
229
|
+
onDismiss={() => setViewerVisible(false)}
|
|
230
|
+
onIndexChange={setViewerIndex}
|
|
231
|
+
enableEditing={enableEditing}
|
|
232
|
+
onImageEdit={onImageEdit}
|
|
233
|
+
/>
|
|
224
234
|
|
|
225
|
-
{/* FilterBottomSheet must be outside ScreenLayout for proper portal rendering */}
|
|
226
235
|
<FilterBottomSheet
|
|
227
236
|
ref={filterSheetRef}
|
|
228
237
|
categories={allCategories}
|
|
@@ -234,7 +243,27 @@ function CreationsGalleryScreenContent({
|
|
|
234
243
|
onClearFilters={clearFilters}
|
|
235
244
|
title={t(config.translations.filterTitle)}
|
|
236
245
|
/>
|
|
237
|
-
|
|
246
|
+
</View>
|
|
238
247
|
);
|
|
239
248
|
}
|
|
240
249
|
|
|
250
|
+
const useStyles = (tokens: DesignTokens, insets: { top: number; bottom: number }) =>
|
|
251
|
+
StyleSheet.create({
|
|
252
|
+
container: {
|
|
253
|
+
flex: 1,
|
|
254
|
+
},
|
|
255
|
+
header: {
|
|
256
|
+
paddingTop: insets.top + tokens.spacing.md,
|
|
257
|
+
backgroundColor: tokens.colors.surface,
|
|
258
|
+
borderBottomWidth: 1,
|
|
259
|
+
borderBottomColor: tokens.colors.border,
|
|
260
|
+
},
|
|
261
|
+
listContent: {
|
|
262
|
+
paddingHorizontal: tokens.spacing.md,
|
|
263
|
+
paddingTop: tokens.spacing.md,
|
|
264
|
+
paddingBottom: insets.bottom + 100,
|
|
265
|
+
},
|
|
266
|
+
emptyContent: {
|
|
267
|
+
flexGrow: 1,
|
|
268
|
+
},
|
|
269
|
+
});
|
|
@@ -3,25 +3,113 @@
|
|
|
3
3
|
* Provider-agnostic text-to-image generation feature
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
//
|
|
6
|
+
// =============================================================================
|
|
7
|
+
// DOMAIN LAYER - Types
|
|
8
|
+
// =============================================================================
|
|
9
|
+
|
|
10
|
+
// Form Types
|
|
11
|
+
export type {
|
|
12
|
+
AspectRatio,
|
|
13
|
+
ImageSize,
|
|
14
|
+
OutputFormat,
|
|
15
|
+
NumImages,
|
|
16
|
+
StyleOption,
|
|
17
|
+
TextToImageFormState,
|
|
18
|
+
TextToImageFormActions,
|
|
19
|
+
TextToImageFormDefaults,
|
|
20
|
+
} from "./domain";
|
|
21
|
+
|
|
22
|
+
// Config Types
|
|
23
|
+
export type {
|
|
24
|
+
GenerationRequest,
|
|
25
|
+
GenerationResult,
|
|
26
|
+
GenerationResultSuccess,
|
|
27
|
+
GenerationResultError,
|
|
28
|
+
TextToImageCallbacks,
|
|
29
|
+
TextToImageFormConfig,
|
|
30
|
+
TextToImageTranslations,
|
|
31
|
+
} from "./domain";
|
|
32
|
+
|
|
33
|
+
// Provider Types
|
|
7
34
|
export type {
|
|
8
35
|
TextToImageOptions,
|
|
9
36
|
TextToImageRequest,
|
|
10
37
|
TextToImageResult,
|
|
11
38
|
TextToImageFeatureState,
|
|
12
|
-
TextToImageTranslations,
|
|
13
39
|
TextToImageInputBuilder,
|
|
14
40
|
TextToImageResultExtractor,
|
|
15
41
|
TextToImageFeatureConfig,
|
|
16
42
|
} from "./domain";
|
|
17
43
|
|
|
18
|
-
//
|
|
44
|
+
// =============================================================================
|
|
45
|
+
// DOMAIN LAYER - Constants
|
|
46
|
+
// =============================================================================
|
|
47
|
+
|
|
48
|
+
export {
|
|
49
|
+
DEFAULT_IMAGE_STYLES,
|
|
50
|
+
DEFAULT_NUM_IMAGES_OPTIONS,
|
|
51
|
+
DEFAULT_ASPECT_RATIO_OPTIONS,
|
|
52
|
+
DEFAULT_SIZE_OPTIONS,
|
|
53
|
+
DEFAULT_OUTPUT_FORMAT_OPTIONS,
|
|
54
|
+
DEFAULT_FORM_VALUES,
|
|
55
|
+
} from "./domain";
|
|
56
|
+
|
|
57
|
+
// =============================================================================
|
|
58
|
+
// INFRASTRUCTURE LAYER
|
|
59
|
+
// =============================================================================
|
|
60
|
+
|
|
19
61
|
export { executeTextToImage, hasTextToImageSupport } from "./infrastructure";
|
|
20
62
|
export type { ExecuteTextToImageOptions } from "./infrastructure";
|
|
21
63
|
|
|
22
|
-
//
|
|
64
|
+
// =============================================================================
|
|
65
|
+
// PRESENTATION LAYER - Hooks
|
|
66
|
+
// =============================================================================
|
|
67
|
+
|
|
68
|
+
export { useFormState, useGeneration, useTextToImageForm } from "./presentation";
|
|
69
|
+
export type {
|
|
70
|
+
UseFormStateOptions,
|
|
71
|
+
UseFormStateReturn,
|
|
72
|
+
GenerationState,
|
|
73
|
+
UseGenerationOptions,
|
|
74
|
+
UseGenerationReturn,
|
|
75
|
+
UseTextToImageFormOptions,
|
|
76
|
+
UseTextToImageFormReturn,
|
|
77
|
+
} from "./presentation";
|
|
78
|
+
|
|
79
|
+
// Provider-based Feature Hook
|
|
23
80
|
export { useTextToImageFeature } from "./presentation";
|
|
24
81
|
export type {
|
|
25
82
|
UseTextToImageFeatureProps,
|
|
26
83
|
UseTextToImageFeatureReturn,
|
|
27
84
|
} from "./presentation";
|
|
85
|
+
|
|
86
|
+
// =============================================================================
|
|
87
|
+
// PRESENTATION LAYER - Components
|
|
88
|
+
// =============================================================================
|
|
89
|
+
|
|
90
|
+
export {
|
|
91
|
+
PromptInput,
|
|
92
|
+
ExamplePrompts,
|
|
93
|
+
NumImagesSelector,
|
|
94
|
+
StyleSelector,
|
|
95
|
+
AspectRatioSelector,
|
|
96
|
+
ImageSizeSelector,
|
|
97
|
+
OutputFormatSelector,
|
|
98
|
+
TextToImageGenerateButton,
|
|
99
|
+
SettingsSheet,
|
|
100
|
+
} from "./presentation";
|
|
101
|
+
|
|
102
|
+
export type {
|
|
103
|
+
PromptInputProps,
|
|
104
|
+
ExamplePromptsProps,
|
|
105
|
+
NumImagesSelectorProps,
|
|
106
|
+
StyleSelectorProps,
|
|
107
|
+
AspectRatioSelectorProps,
|
|
108
|
+
AspectRatioOption,
|
|
109
|
+
ImageSizeSelectorProps,
|
|
110
|
+
ImageSizeOption,
|
|
111
|
+
OutputFormatSelectorProps,
|
|
112
|
+
OutputFormatOption,
|
|
113
|
+
TextToImageGenerateButtonProps,
|
|
114
|
+
SettingsSheetProps,
|
|
115
|
+
} from "./presentation";
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aspect Ratio Selector Component
|
|
3
|
+
* Button group for selecting image aspect ratio
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, TouchableOpacity, StyleSheet } from "react-native";
|
|
8
|
+
import {
|
|
9
|
+
AtomicText,
|
|
10
|
+
useAppDesignTokens,
|
|
11
|
+
} from "@umituz/react-native-design-system";
|
|
12
|
+
import type { AspectRatio } from "../../domain/types/form.types";
|
|
13
|
+
|
|
14
|
+
export interface AspectRatioOption {
|
|
15
|
+
value: AspectRatio;
|
|
16
|
+
label: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface AspectRatioSelectorProps {
|
|
20
|
+
value: AspectRatio;
|
|
21
|
+
onChange: (ratio: AspectRatio) => void;
|
|
22
|
+
options: AspectRatioOption[];
|
|
23
|
+
label: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const AspectRatioSelector: React.FC<AspectRatioSelectorProps> = ({
|
|
27
|
+
value,
|
|
28
|
+
onChange,
|
|
29
|
+
options,
|
|
30
|
+
label,
|
|
31
|
+
}) => {
|
|
32
|
+
const tokens = useAppDesignTokens();
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<View style={styles.container}>
|
|
36
|
+
<AtomicText
|
|
37
|
+
type="bodyMedium"
|
|
38
|
+
style={[styles.label, { color: tokens.colors.textPrimary }]}
|
|
39
|
+
>
|
|
40
|
+
{label}
|
|
41
|
+
</AtomicText>
|
|
42
|
+
<View style={styles.optionsRow}>
|
|
43
|
+
{options.map((option) => {
|
|
44
|
+
const isSelected = value === option.value;
|
|
45
|
+
return (
|
|
46
|
+
<TouchableOpacity
|
|
47
|
+
key={option.value}
|
|
48
|
+
style={[
|
|
49
|
+
styles.option,
|
|
50
|
+
{
|
|
51
|
+
backgroundColor: isSelected
|
|
52
|
+
? tokens.colors.primary
|
|
53
|
+
: tokens.colors.surface,
|
|
54
|
+
borderColor: isSelected
|
|
55
|
+
? tokens.colors.primary
|
|
56
|
+
: tokens.colors.borderLight,
|
|
57
|
+
},
|
|
58
|
+
]}
|
|
59
|
+
onPress={() => onChange(option.value)}
|
|
60
|
+
activeOpacity={0.7}
|
|
61
|
+
>
|
|
62
|
+
<AtomicText
|
|
63
|
+
type="bodySmall"
|
|
64
|
+
style={{
|
|
65
|
+
color: isSelected ? "#FFFFFF" : tokens.colors.textPrimary,
|
|
66
|
+
fontWeight: isSelected ? "600" : "400",
|
|
67
|
+
}}
|
|
68
|
+
>
|
|
69
|
+
{option.label}
|
|
70
|
+
</AtomicText>
|
|
71
|
+
</TouchableOpacity>
|
|
72
|
+
);
|
|
73
|
+
})}
|
|
74
|
+
</View>
|
|
75
|
+
</View>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const styles = StyleSheet.create({
|
|
80
|
+
container: {
|
|
81
|
+
marginBottom: 20,
|
|
82
|
+
},
|
|
83
|
+
label: {
|
|
84
|
+
fontWeight: "600",
|
|
85
|
+
marginBottom: 12,
|
|
86
|
+
},
|
|
87
|
+
optionsRow: {
|
|
88
|
+
flexDirection: "row",
|
|
89
|
+
gap: 8,
|
|
90
|
+
},
|
|
91
|
+
option: {
|
|
92
|
+
flex: 1,
|
|
93
|
+
padding: 12,
|
|
94
|
+
borderRadius: 8,
|
|
95
|
+
borderWidth: 1,
|
|
96
|
+
alignItems: "center",
|
|
97
|
+
},
|
|
98
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example Prompts Component
|
|
3
|
+
* Horizontal scrollable list of example prompts
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, ScrollView, TouchableOpacity, StyleSheet } from "react-native";
|
|
8
|
+
import {
|
|
9
|
+
AtomicText,
|
|
10
|
+
useAppDesignTokens,
|
|
11
|
+
} from "@umituz/react-native-design-system";
|
|
12
|
+
|
|
13
|
+
export interface ExamplePromptsProps {
|
|
14
|
+
prompts: string[];
|
|
15
|
+
onSelectPrompt: (prompt: string) => void;
|
|
16
|
+
label: string;
|
|
17
|
+
cardWidth?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const ExamplePrompts: React.FC<ExamplePromptsProps> = ({
|
|
21
|
+
prompts,
|
|
22
|
+
onSelectPrompt,
|
|
23
|
+
label,
|
|
24
|
+
cardWidth = 180,
|
|
25
|
+
}) => {
|
|
26
|
+
const tokens = useAppDesignTokens();
|
|
27
|
+
|
|
28
|
+
if (prompts.length === 0) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<View style={styles.container}>
|
|
34
|
+
<AtomicText
|
|
35
|
+
type="bodyMedium"
|
|
36
|
+
style={[styles.label, { color: tokens.colors.textPrimary }]}
|
|
37
|
+
>
|
|
38
|
+
{label}
|
|
39
|
+
</AtomicText>
|
|
40
|
+
<ScrollView
|
|
41
|
+
horizontal
|
|
42
|
+
showsHorizontalScrollIndicator={false}
|
|
43
|
+
contentContainerStyle={styles.scrollContent}
|
|
44
|
+
>
|
|
45
|
+
{prompts.map((prompt, index) => (
|
|
46
|
+
<TouchableOpacity
|
|
47
|
+
key={`prompt-${index}`}
|
|
48
|
+
style={[
|
|
49
|
+
styles.card,
|
|
50
|
+
{
|
|
51
|
+
backgroundColor: tokens.colors.surface,
|
|
52
|
+
width: cardWidth,
|
|
53
|
+
},
|
|
54
|
+
]}
|
|
55
|
+
onPress={() => onSelectPrompt(prompt)}
|
|
56
|
+
activeOpacity={0.7}
|
|
57
|
+
>
|
|
58
|
+
<AtomicText
|
|
59
|
+
type="bodySmall"
|
|
60
|
+
style={{ color: tokens.colors.textPrimary }}
|
|
61
|
+
numberOfLines={2}
|
|
62
|
+
>
|
|
63
|
+
{prompt}
|
|
64
|
+
</AtomicText>
|
|
65
|
+
</TouchableOpacity>
|
|
66
|
+
))}
|
|
67
|
+
</ScrollView>
|
|
68
|
+
</View>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const styles = StyleSheet.create({
|
|
73
|
+
container: {
|
|
74
|
+
marginBottom: 24,
|
|
75
|
+
},
|
|
76
|
+
label: {
|
|
77
|
+
fontWeight: "600",
|
|
78
|
+
marginBottom: 12,
|
|
79
|
+
},
|
|
80
|
+
scrollContent: {
|
|
81
|
+
paddingRight: 16,
|
|
82
|
+
},
|
|
83
|
+
card: {
|
|
84
|
+
padding: 12,
|
|
85
|
+
borderRadius: 8,
|
|
86
|
+
marginRight: 12,
|
|
87
|
+
},
|
|
88
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Size Selector Component
|
|
3
|
+
* Selection for image output size
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, TouchableOpacity, StyleSheet } from "react-native";
|
|
8
|
+
import {
|
|
9
|
+
AtomicText,
|
|
10
|
+
useAppDesignTokens,
|
|
11
|
+
} from "@umituz/react-native-design-system";
|
|
12
|
+
import type { ImageSize } from "../../domain/types/form.types";
|
|
13
|
+
|
|
14
|
+
export interface ImageSizeOption {
|
|
15
|
+
value: ImageSize;
|
|
16
|
+
label: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ImageSizeSelectorProps {
|
|
20
|
+
value: ImageSize;
|
|
21
|
+
onChange: (size: ImageSize) => void;
|
|
22
|
+
options: ImageSizeOption[];
|
|
23
|
+
label: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const ImageSizeSelector: React.FC<ImageSizeSelectorProps> = ({
|
|
27
|
+
value,
|
|
28
|
+
onChange,
|
|
29
|
+
options,
|
|
30
|
+
label,
|
|
31
|
+
}) => {
|
|
32
|
+
const tokens = useAppDesignTokens();
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<View style={styles.container}>
|
|
36
|
+
<AtomicText
|
|
37
|
+
type="bodyMedium"
|
|
38
|
+
style={[styles.label, { color: tokens.colors.textPrimary }]}
|
|
39
|
+
>
|
|
40
|
+
{label}
|
|
41
|
+
</AtomicText>
|
|
42
|
+
<View style={styles.optionsGrid}>
|
|
43
|
+
{options.map((option) => {
|
|
44
|
+
const isSelected = value === option.value;
|
|
45
|
+
return (
|
|
46
|
+
<TouchableOpacity
|
|
47
|
+
key={option.value}
|
|
48
|
+
style={[
|
|
49
|
+
styles.option,
|
|
50
|
+
{
|
|
51
|
+
backgroundColor: isSelected
|
|
52
|
+
? tokens.colors.primary
|
|
53
|
+
: tokens.colors.surface,
|
|
54
|
+
borderColor: isSelected
|
|
55
|
+
? tokens.colors.primary
|
|
56
|
+
: tokens.colors.borderLight,
|
|
57
|
+
},
|
|
58
|
+
]}
|
|
59
|
+
onPress={() => onChange(option.value)}
|
|
60
|
+
activeOpacity={0.7}
|
|
61
|
+
>
|
|
62
|
+
<AtomicText
|
|
63
|
+
type="bodySmall"
|
|
64
|
+
style={{
|
|
65
|
+
color: isSelected ? "#FFFFFF" : tokens.colors.textPrimary,
|
|
66
|
+
fontWeight: isSelected ? "600" : "400",
|
|
67
|
+
}}
|
|
68
|
+
>
|
|
69
|
+
{option.label}
|
|
70
|
+
</AtomicText>
|
|
71
|
+
</TouchableOpacity>
|
|
72
|
+
);
|
|
73
|
+
})}
|
|
74
|
+
</View>
|
|
75
|
+
</View>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const styles = StyleSheet.create({
|
|
80
|
+
container: {
|
|
81
|
+
marginBottom: 20,
|
|
82
|
+
},
|
|
83
|
+
label: {
|
|
84
|
+
fontWeight: "600",
|
|
85
|
+
marginBottom: 12,
|
|
86
|
+
},
|
|
87
|
+
optionsGrid: {
|
|
88
|
+
flexDirection: "row",
|
|
89
|
+
flexWrap: "wrap",
|
|
90
|
+
gap: 8,
|
|
91
|
+
},
|
|
92
|
+
option: {
|
|
93
|
+
paddingHorizontal: 16,
|
|
94
|
+
paddingVertical: 10,
|
|
95
|
+
borderRadius: 8,
|
|
96
|
+
borderWidth: 1,
|
|
97
|
+
},
|
|
98
|
+
});
|