@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-ai-generation-content",
3
- "version": "1.17.16",
3
+ "version": "1.17.17",
4
4
  "description": "Provider-agnostic AI generation orchestration for React Native",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -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
- ScreenLayout,
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
- CreationsGrid,
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 renderEmptyComponent = useMemo(() => (
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
- <ScreenLayout
171
- scrollable={false}
172
- edges={["top"]}
173
- backgroundColor={tokens.colors.background}
174
- header={
175
- (!creations || creations?.length === 0) && !isLoading ? null : (
176
- <GalleryHeader
177
- title={t(config.translations.title)}
178
- count={filtered.length}
179
- countLabel={t(config.translations.photoCount)}
180
- isFiltered={isFiltered}
181
- showFilter={showFilter}
182
- filterLabel={t(config.translations.filterLabel)}
183
- onFilterPress={() => {
184
- if (__DEV__) {
185
- // eslint-disable-next-line no-console
186
- console.log('[CreationsGallery] Filter button pressed');
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
- <CreationImageViewer
215
- creations={filtered}
216
- visible={viewerVisible}
217
- index={viewerIndex}
218
- onDismiss={() => setViewerVisible(false)}
219
- onIndexChange={setViewerIndex}
220
- enableEditing={enableEditing}
221
- onImageEdit={onImageEdit}
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
- // Domain Types
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
- // Infrastructure Services
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
- // Presentation Hooks
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
+ });