@umituz/react-native-ai-generation-content 1.27.7 → 1.27.9

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.
Files changed (85) hide show
  1. package/package.json +1 -1
  2. package/src/domains/generation/wizard/infrastructure/strategies/image-generation.strategy.ts +3 -30
  3. package/src/domains/generation/wizard/infrastructure/strategies/video-generation.strategy.ts +3 -30
  4. package/src/domains/generation/wizard/infrastructure/strategies/wizard-strategy.constants.ts +0 -2
  5. package/src/domains/scenarios/configs/wizard-configs.ts +28 -28
  6. package/src/domains/scenarios/index.ts +3 -3
  7. package/src/features/image-to-video/README.md +414 -0
  8. package/src/features/image-to-video/domain/constants/animation.constants.ts +47 -0
  9. package/src/features/image-to-video/domain/constants/duration.constants.ts +13 -0
  10. package/src/features/image-to-video/domain/constants/form.constants.ts +22 -0
  11. package/src/features/image-to-video/domain/constants/index.ts +23 -0
  12. package/src/features/image-to-video/domain/constants/music.constants.ts +53 -0
  13. package/src/features/image-to-video/domain/index.ts +5 -0
  14. package/src/features/image-to-video/domain/types/animation.types.ts +20 -0
  15. package/src/features/image-to-video/domain/types/config.types.ts +56 -0
  16. package/src/features/image-to-video/domain/types/duration.types.ts +11 -0
  17. package/src/features/image-to-video/domain/types/form.types.ts +35 -0
  18. package/src/features/image-to-video/domain/types/image-to-video.types.ts +122 -0
  19. package/src/features/image-to-video/domain/types/index.ts +39 -0
  20. package/src/features/image-to-video/domain/types/music.types.ts +21 -0
  21. package/src/features/image-to-video/index.ts +116 -0
  22. package/src/features/image-to-video/infrastructure/index.ts +1 -0
  23. package/src/features/image-to-video/infrastructure/services/image-to-video-executor.ts +165 -0
  24. package/src/features/image-to-video/infrastructure/services/index.ts +5 -0
  25. package/src/features/image-to-video/presentation/components/AddMoreCard.tsx +52 -0
  26. package/src/features/image-to-video/presentation/components/AnimationStyleSelector.tsx +135 -0
  27. package/src/features/image-to-video/presentation/components/DurationSelector.tsx +110 -0
  28. package/src/features/image-to-video/presentation/components/EmptyGridState.tsx +69 -0
  29. package/src/features/image-to-video/presentation/components/GridImageItem.tsx +64 -0
  30. package/src/features/image-to-video/presentation/components/ImageSelectionGrid.styles.ts +84 -0
  31. package/src/features/image-to-video/presentation/components/ImageSelectionGrid.tsx +77 -0
  32. package/src/features/image-to-video/presentation/components/ImageSelectionGrid.types.ts +18 -0
  33. package/src/features/image-to-video/presentation/components/MusicMoodSelector.tsx +181 -0
  34. package/src/features/image-to-video/presentation/components/index.ts +30 -0
  35. package/src/features/image-to-video/presentation/hooks/index.ts +27 -0
  36. package/src/features/image-to-video/presentation/hooks/useFormState.ts +116 -0
  37. package/src/features/image-to-video/presentation/hooks/useGeneration.ts +85 -0
  38. package/src/features/image-to-video/presentation/hooks/useGenerationExecution.ts +143 -0
  39. package/src/features/image-to-video/presentation/hooks/useImageToVideoFeature.ts +107 -0
  40. package/src/features/image-to-video/presentation/hooks/useImageToVideoForm.ts +119 -0
  41. package/src/features/image-to-video/presentation/hooks/useImageToVideoValidation.ts +46 -0
  42. package/src/features/image-to-video/presentation/index.ts +5 -0
  43. package/src/features/text-to-image/README.md +394 -0
  44. package/src/features/text-to-image/domain/constants/index.ts +25 -0
  45. package/src/features/text-to-image/domain/constants/options.constants.ts +39 -0
  46. package/src/features/text-to-image/domain/constants/styles.constants.ts +34 -0
  47. package/src/features/text-to-image/domain/index.ts +7 -0
  48. package/src/features/text-to-image/domain/types/config.types.ts +75 -0
  49. package/src/features/text-to-image/domain/types/form.types.ts +58 -0
  50. package/src/features/text-to-image/domain/types/index.ts +38 -0
  51. package/src/features/text-to-image/domain/types/text-to-image.types.ts +58 -0
  52. package/src/features/text-to-image/index.ts +116 -0
  53. package/src/features/text-to-image/infrastructure/index.ts +1 -0
  54. package/src/features/text-to-image/infrastructure/services/index.ts +5 -0
  55. package/src/features/text-to-image/infrastructure/services/text-to-image-executor.ts +147 -0
  56. package/src/features/text-to-image/presentation/components/index.ts +30 -0
  57. package/src/features/text-to-image/presentation/hooks/index.ts +30 -0
  58. package/src/features/text-to-image/presentation/hooks/useFormState.ts +103 -0
  59. package/src/features/text-to-image/presentation/hooks/useGeneration.ts +134 -0
  60. package/src/features/text-to-image/presentation/hooks/useTextToImageFeature.ts +111 -0
  61. package/src/features/text-to-image/presentation/hooks/useTextToImageForm.ts +58 -0
  62. package/src/features/text-to-image/presentation/index.ts +7 -0
  63. package/src/features/text-to-video/README.md +412 -0
  64. package/src/features/text-to-video/domain/index.ts +1 -0
  65. package/src/features/text-to-video/domain/types/callback.types.ts +69 -0
  66. package/src/features/text-to-video/domain/types/component.types.ts +106 -0
  67. package/src/features/text-to-video/domain/types/config.types.ts +61 -0
  68. package/src/features/text-to-video/domain/types/index.ts +56 -0
  69. package/src/features/text-to-video/domain/types/request.types.ts +36 -0
  70. package/src/features/text-to-video/domain/types/state.types.ts +53 -0
  71. package/src/features/text-to-video/index.ts +68 -0
  72. package/src/features/text-to-video/infrastructure/index.ts +1 -0
  73. package/src/features/text-to-video/infrastructure/services/index.ts +5 -0
  74. package/src/features/text-to-video/infrastructure/services/text-to-video-executor.ts +141 -0
  75. package/src/features/text-to-video/presentation/components/FrameSelector.tsx +153 -0
  76. package/src/features/text-to-video/presentation/components/GenerationTabs.tsx +73 -0
  77. package/src/features/text-to-video/presentation/components/HeroSection.tsx +61 -0
  78. package/src/features/text-to-video/presentation/components/HintCarousel.tsx +96 -0
  79. package/src/features/text-to-video/presentation/components/OptionsPanel.tsx +121 -0
  80. package/src/features/text-to-video/presentation/components/index.ts +10 -0
  81. package/src/features/text-to-video/presentation/hooks/index.ts +17 -0
  82. package/src/features/text-to-video/presentation/hooks/useTextToVideoFeature.ts +187 -0
  83. package/src/features/text-to-video/presentation/hooks/useTextToVideoForm.ts +134 -0
  84. package/src/features/text-to-video/presentation/index.ts +7 -0
  85. package/src/index.ts +5 -0
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Image-to-Video Executor
3
+ * Provider-agnostic image-to-video execution using active AI provider
4
+ * Uses progress mapper for consistent progress reporting
5
+ */
6
+
7
+ import { providerRegistry } from "../../../../infrastructure/services";
8
+
9
+ /** Map job status to progress percentage */
10
+ const getProgressFromJobStatus = (status: string): number => {
11
+ switch (status.toLowerCase()) {
12
+ case "queued": return 10;
13
+ case "in_queue": return 15;
14
+ case "processing": return 50;
15
+ case "in_progress": return 60;
16
+ case "completed": return 100;
17
+ default: return 30;
18
+ }
19
+ };
20
+ import type {
21
+ ImageToVideoRequest,
22
+ ImageToVideoResult,
23
+ ImageToVideoInputBuilder,
24
+ ImageToVideoResultExtractor,
25
+ } from "../../domain/types";
26
+
27
+ declare const __DEV__: boolean;
28
+
29
+ export interface ExecuteImageToVideoOptions {
30
+ model: string;
31
+ buildInput: ImageToVideoInputBuilder;
32
+ extractResult?: ImageToVideoResultExtractor;
33
+ onProgress?: (progress: number) => void;
34
+ }
35
+
36
+ function defaultExtractResult(
37
+ result: unknown,
38
+ ): { videoUrl?: string; thumbnailUrl?: string } | undefined {
39
+ if (typeof result !== "object" || result === null) return undefined;
40
+
41
+ const r = result as Record<string, unknown>;
42
+
43
+ if (typeof r.video === "string") {
44
+ return { videoUrl: r.video };
45
+ }
46
+
47
+ if (r.video && typeof r.video === "object") {
48
+ const video = r.video as Record<string, unknown>;
49
+ if (typeof video.url === "string") {
50
+ return {
51
+ videoUrl: video.url,
52
+ thumbnailUrl:
53
+ typeof r.thumbnail === "string" ? r.thumbnail : undefined,
54
+ };
55
+ }
56
+ }
57
+
58
+ return undefined;
59
+ }
60
+
61
+ export async function executeImageToVideo(
62
+ request: ImageToVideoRequest,
63
+ options: ExecuteImageToVideoOptions,
64
+ ): Promise<ImageToVideoResult> {
65
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
66
+
67
+ console.log("[ImageToVideoExecutor] executeImageToVideo() called");
68
+ }
69
+
70
+ const provider = providerRegistry.getActiveProvider();
71
+
72
+ if (!provider) {
73
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
74
+
75
+ console.error("[ImageToVideoExecutor] No AI provider configured");
76
+ }
77
+ return { success: false, error: "No AI provider configured" };
78
+ }
79
+
80
+ if (!provider.isInitialized()) {
81
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
82
+
83
+ console.error("[ImageToVideoExecutor] AI provider not initialized");
84
+ }
85
+ return { success: false, error: "AI provider not initialized" };
86
+ }
87
+
88
+ if (!request.imageBase64) {
89
+ return { success: false, error: "Image base64 is required" };
90
+ }
91
+
92
+ const { model, buildInput, extractResult, onProgress } = options;
93
+
94
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
95
+
96
+ console.log(`[ImageToVideoExecutor] Provider: ${provider.providerId}, Model: ${model}`);
97
+ }
98
+
99
+ try {
100
+ onProgress?.(5);
101
+
102
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
103
+
104
+ console.log("[ImageToVideoExecutor] Starting provider.subscribe()...");
105
+ }
106
+
107
+ // Build input directly - let buildInput handle base64 format
108
+ const input = buildInput(request.imageBase64, request.motionPrompt, request.options);
109
+
110
+ // Use subscribe for video generation (long-running operation with queue)
111
+ // subscribe provides progress updates unlike run()
112
+ const result = await provider.subscribe(model, input, {
113
+ onQueueUpdate: (status) => {
114
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
115
+
116
+ console.log("[ImageToVideoExecutor] Queue status:", status.status, "position:", status.queuePosition);
117
+ }
118
+ // Map provider status to progress using centralized mapper
119
+ const progress = getProgressFromJobStatus(status.status);
120
+ onProgress?.(progress);
121
+ },
122
+ timeoutMs: 300000, // 5 minutes timeout for video generation
123
+ });
124
+
125
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
126
+
127
+ console.log("[ImageToVideoExecutor] Subscribe resolved, result keys:", result ? Object.keys(result as object) : "null");
128
+ }
129
+
130
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
131
+
132
+ console.log("[ImageToVideoExecutor] provider.subscribe() completed");
133
+ }
134
+
135
+ const extractor = extractResult || defaultExtractResult;
136
+ const extracted = extractor(result);
137
+ onProgress?.(100);
138
+
139
+ if (!extracted?.videoUrl) {
140
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
141
+
142
+ console.error("[ImageToVideoExecutor] No video URL in response");
143
+ }
144
+ return { success: false, error: "No video in response" };
145
+ }
146
+
147
+ return {
148
+ success: true,
149
+ videoUrl: extracted.videoUrl,
150
+ thumbnailUrl: extracted.thumbnailUrl,
151
+ };
152
+ } catch (error) {
153
+ const message = error instanceof Error ? error.message : String(error);
154
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
155
+
156
+ console.error("[ImageToVideoExecutor] Error:", message);
157
+ }
158
+ return { success: false, error: message };
159
+ }
160
+ }
161
+
162
+ export function hasImageToVideoSupport(): boolean {
163
+ const provider = providerRegistry.getActiveProvider();
164
+ return provider !== null && provider.isInitialized();
165
+ }
@@ -0,0 +1,5 @@
1
+ export {
2
+ executeImageToVideo,
3
+ hasImageToVideoSupport,
4
+ } from "./image-to-video-executor";
5
+ export type { ExecuteImageToVideoOptions } from "./image-to-video-executor";
@@ -0,0 +1,52 @@
1
+ /**
2
+ * AddMoreCard Component
3
+ * Displays "Add More" button in the image grid
4
+ */
5
+
6
+ import React from "react";
7
+ import { TouchableOpacity } from "react-native";
8
+ import {
9
+ AtomicIcon,
10
+ AtomicText,
11
+ useAppDesignTokens,
12
+ } from "@umituz/react-native-design-system";
13
+ import type { ImageSelectionGridStyles } from "./ImageSelectionGrid.styles";
14
+
15
+ interface AddMoreCardProps {
16
+ styles: ImageSelectionGridStyles;
17
+ addMoreText: string;
18
+ onPress: () => void;
19
+ }
20
+
21
+ export function AddMoreCard({
22
+ styles,
23
+ addMoreText,
24
+ onPress,
25
+ }: AddMoreCardProps) {
26
+ const tokens = useAppDesignTokens();
27
+
28
+ return (
29
+ <TouchableOpacity
30
+ style={[
31
+ styles.addMoreCard,
32
+ {
33
+ backgroundColor: tokens.colors.surface,
34
+ borderColor: tokens.colors.borderLight,
35
+ },
36
+ ]}
37
+ onPress={onPress}
38
+ activeOpacity={0.7}
39
+ >
40
+ <AtomicIcon name="add" size="lg" color="primary" />
41
+ <AtomicText
42
+ type="labelSmall"
43
+ style={[
44
+ styles.addMoreText,
45
+ { color: tokens.colors.primary },
46
+ ]}
47
+ >
48
+ {addMoreText}
49
+ </AtomicText>
50
+ </TouchableOpacity>
51
+ );
52
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Animation Style Selector Component
3
+ * Generic component for animation style selection
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, TouchableOpacity, StyleSheet } from "react-native";
8
+ import {
9
+ AtomicText,
10
+ AtomicIcon,
11
+ useAppDesignTokens,
12
+ } from "@umituz/react-native-design-system";
13
+ import type { AnimationStyle, AnimationStyleId } from "../../domain/types";
14
+
15
+ export interface AnimationStyleSelectorProps {
16
+ styles: AnimationStyle[];
17
+ selectedStyle: AnimationStyleId;
18
+ onStyleSelect: (styleId: AnimationStyleId) => void;
19
+ label: string;
20
+ }
21
+
22
+ export const AnimationStyleSelector: React.FC<AnimationStyleSelectorProps> = ({
23
+ styles,
24
+ selectedStyle,
25
+ onStyleSelect,
26
+ label,
27
+ }) => {
28
+ const tokens = useAppDesignTokens();
29
+
30
+ return (
31
+ <View style={componentStyles.section}>
32
+ <AtomicText
33
+ type="bodyMedium"
34
+ style={[componentStyles.label, { color: tokens.colors.textPrimary }]}
35
+ >
36
+ {label}
37
+ </AtomicText>
38
+ {styles.map((style) => {
39
+ const isSelected = selectedStyle === style.id;
40
+ return (
41
+ <TouchableOpacity
42
+ key={style.id}
43
+ style={[
44
+ componentStyles.card,
45
+ {
46
+ backgroundColor: isSelected
47
+ ? tokens.colors.primary + "20"
48
+ : tokens.colors.surface,
49
+ borderColor: isSelected
50
+ ? tokens.colors.primary
51
+ : tokens.colors.borderLight,
52
+ },
53
+ ]}
54
+ onPress={() => onStyleSelect(style.id)}
55
+ activeOpacity={0.7}
56
+ >
57
+ <View style={componentStyles.cardContent}>
58
+ <View
59
+ style={[
60
+ componentStyles.iconContainer,
61
+ {
62
+ backgroundColor: isSelected
63
+ ? tokens.colors.primary
64
+ : tokens.colors.primary + "20",
65
+ },
66
+ ]}
67
+ >
68
+ <AtomicIcon
69
+ name={style.icon as never}
70
+ size="md"
71
+ color={isSelected ? "onSurface" : "primary"}
72
+ />
73
+ </View>
74
+ <View style={componentStyles.textContainer}>
75
+ <AtomicText
76
+ type="bodyMedium"
77
+ style={[
78
+ componentStyles.styleName,
79
+ { color: tokens.colors.textPrimary },
80
+ ]}
81
+ >
82
+ {style.name}
83
+ </AtomicText>
84
+ <AtomicText
85
+ type="labelSmall"
86
+ style={{ color: tokens.colors.textSecondary }}
87
+ >
88
+ {style.description}
89
+ </AtomicText>
90
+ </View>
91
+ {isSelected && (
92
+ <AtomicIcon name="checkmark-outline" size="md" color="primary" />
93
+ )}
94
+ </View>
95
+ </TouchableOpacity>
96
+ );
97
+ })}
98
+ </View>
99
+ );
100
+ };
101
+
102
+ const componentStyles = StyleSheet.create({
103
+ section: {
104
+ padding: 16,
105
+ marginBottom: 8,
106
+ },
107
+ label: {
108
+ fontWeight: "600",
109
+ marginBottom: 12,
110
+ },
111
+ card: {
112
+ padding: 16,
113
+ borderRadius: 12,
114
+ borderWidth: 2,
115
+ marginBottom: 12,
116
+ },
117
+ cardContent: {
118
+ flexDirection: "row",
119
+ alignItems: "center",
120
+ },
121
+ iconContainer: {
122
+ width: 48,
123
+ height: 48,
124
+ borderRadius: 24,
125
+ alignItems: "center",
126
+ justifyContent: "center",
127
+ },
128
+ textContainer: {
129
+ flex: 1,
130
+ marginLeft: 12,
131
+ },
132
+ styleName: {
133
+ fontWeight: "600",
134
+ },
135
+ });
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Duration Selector Component
3
+ * Generic component for video duration selection
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 { VideoDuration, DurationOption } from "../../domain/types";
13
+
14
+ export interface DurationSelectorProps {
15
+ options: DurationOption[];
16
+ selectedDuration: VideoDuration;
17
+ onDurationSelect: (duration: VideoDuration) => void;
18
+ label: string;
19
+ imageCount: number;
20
+ totalVideoLabel: string;
21
+ }
22
+
23
+ export const DurationSelector: React.FC<DurationSelectorProps> = ({
24
+ options,
25
+ selectedDuration,
26
+ onDurationSelect,
27
+ label,
28
+ imageCount,
29
+ totalVideoLabel,
30
+ }) => {
31
+ const tokens = useAppDesignTokens();
32
+
33
+ return (
34
+ <View style={componentStyles.section}>
35
+ <AtomicText
36
+ type="bodyMedium"
37
+ style={[componentStyles.label, { color: tokens.colors.textPrimary }]}
38
+ >
39
+ {label}
40
+ </AtomicText>
41
+ <View style={componentStyles.grid}>
42
+ {options.map((option) => {
43
+ const isSelected = selectedDuration === option.value;
44
+ return (
45
+ <TouchableOpacity
46
+ key={option.value}
47
+ style={[
48
+ componentStyles.button,
49
+ {
50
+ backgroundColor: isSelected
51
+ ? tokens.colors.primary
52
+ : tokens.colors.surface,
53
+ borderColor: isSelected
54
+ ? tokens.colors.primary
55
+ : tokens.colors.borderLight,
56
+ },
57
+ ]}
58
+ onPress={() => onDurationSelect(option.value)}
59
+ activeOpacity={0.7}
60
+ >
61
+ <AtomicText
62
+ type="bodyMedium"
63
+ style={{
64
+ color: isSelected
65
+ ? tokens.colors.textInverse
66
+ : tokens.colors.textPrimary,
67
+ fontWeight: isSelected ? "600" : "400",
68
+ }}
69
+ >
70
+ {option.label ?? `${option.value}s`}
71
+ </AtomicText>
72
+ </TouchableOpacity>
73
+ );
74
+ })}
75
+ </View>
76
+ <AtomicText
77
+ type="labelSmall"
78
+ style={[componentStyles.hint, { color: tokens.colors.textSecondary }]}
79
+ >
80
+ {totalVideoLabel.replace("{duration}", String(imageCount * selectedDuration))}
81
+ </AtomicText>
82
+ </View>
83
+ );
84
+ };
85
+
86
+ const componentStyles = StyleSheet.create({
87
+ section: {
88
+ padding: 16,
89
+ marginBottom: 8,
90
+ },
91
+ label: {
92
+ fontWeight: "600",
93
+ marginBottom: 12,
94
+ },
95
+ grid: {
96
+ flexDirection: "row",
97
+ gap: 12,
98
+ },
99
+ button: {
100
+ flex: 1,
101
+ paddingVertical: 16,
102
+ borderRadius: 12,
103
+ borderWidth: 1,
104
+ alignItems: "center",
105
+ justifyContent: "center",
106
+ },
107
+ hint: {
108
+ marginTop: 8,
109
+ },
110
+ });
@@ -0,0 +1,69 @@
1
+ /**
2
+ * EmptyGridState Component
3
+ * Displays empty state when no images are selected
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, TouchableOpacity } from "react-native";
8
+ import {
9
+ AtomicText,
10
+ AtomicIcon,
11
+ useAppDesignTokens,
12
+ } from "@umituz/react-native-design-system";
13
+ import type { ImageSelectionGridStyles } from "./ImageSelectionGrid.styles";
14
+ import type { ImageSelectionGridTranslations } from "./ImageSelectionGrid.types";
15
+
16
+ interface EmptyGridStateProps {
17
+ styles: ImageSelectionGridStyles;
18
+ maxImages: number;
19
+ translations: ImageSelectionGridTranslations;
20
+ onSelectImages: () => void;
21
+ }
22
+
23
+ export function EmptyGridState({
24
+ styles,
25
+ maxImages,
26
+ translations,
27
+ onSelectImages,
28
+ }: EmptyGridStateProps) {
29
+ const tokens = useAppDesignTokens();
30
+
31
+ return (
32
+ <View style={styles.section}>
33
+ <AtomicText
34
+ type="bodyMedium"
35
+ style={[styles.label, { color: tokens.colors.textPrimary }]}
36
+ >
37
+ {translations.selectedImages} (0/{maxImages})
38
+ </AtomicText>
39
+ <TouchableOpacity
40
+ style={[
41
+ styles.uploadBox,
42
+ {
43
+ backgroundColor: tokens.colors.surface,
44
+ borderColor: tokens.colors.borderLight,
45
+ },
46
+ ]}
47
+ onPress={onSelectImages}
48
+ activeOpacity={0.7}
49
+ >
50
+ <AtomicIcon name="cloud-upload-outline" size="xl" color="primary" />
51
+ <AtomicText
52
+ type="bodyMedium"
53
+ style={[
54
+ styles.uploadText,
55
+ { color: tokens.colors.primary },
56
+ ]}
57
+ >
58
+ {translations.selectImages}
59
+ </AtomicText>
60
+ <AtomicText
61
+ type="labelSmall"
62
+ style={{ color: tokens.colors.textSecondary }}
63
+ >
64
+ {translations.chooseUpTo.replace("{max}", String(maxImages))}
65
+ </AtomicText>
66
+ </TouchableOpacity>
67
+ </View>
68
+ );
69
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * GridImageItem Component
3
+ * Displays a single image in the grid with remove button and badge
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, TouchableOpacity, Image } from "react-native";
8
+ import {
9
+ AtomicIcon,
10
+ AtomicText,
11
+ useAppDesignTokens,
12
+ } from "@umituz/react-native-design-system";
13
+ import type { ImageSelectionGridStyles } from "./ImageSelectionGrid.styles";
14
+
15
+ interface GridImageItemProps {
16
+ styles: ImageSelectionGridStyles;
17
+ uri: string;
18
+ index: number;
19
+ onRemove: () => void;
20
+ }
21
+
22
+ export function GridImageItem({
23
+ styles,
24
+ uri,
25
+ index,
26
+ onRemove,
27
+ }: GridImageItemProps) {
28
+ const tokens = useAppDesignTokens();
29
+
30
+ return (
31
+ <View style={styles.imageCard}>
32
+ <Image
33
+ source={{ uri }}
34
+ style={styles.imagePreview}
35
+ resizeMode="cover"
36
+ />
37
+ <TouchableOpacity
38
+ style={[
39
+ styles.removeButton,
40
+ { backgroundColor: tokens.colors.error },
41
+ ]}
42
+ onPress={onRemove}
43
+ >
44
+ <AtomicIcon name="close-circle" size="sm" color="onSurface" />
45
+ </TouchableOpacity>
46
+ <View
47
+ style={[
48
+ styles.imageBadge,
49
+ { backgroundColor: tokens.colors.primary },
50
+ ]}
51
+ >
52
+ <AtomicText
53
+ type="labelSmall"
54
+ style={[
55
+ styles.badgeText,
56
+ { color: tokens.colors.textInverse },
57
+ ]}
58
+ >
59
+ {index + 1}
60
+ </AtomicText>
61
+ </View>
62
+ </View>
63
+ );
64
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * ImageSelectionGrid Styles
3
+ */
4
+
5
+ import { StyleSheet } from "react-native";
6
+ import type { DesignTokens } from "@umituz/react-native-design-system";
7
+
8
+ export function createImageSelectionGridStyles(_tokens: DesignTokens) {
9
+ return StyleSheet.create({
10
+ section: {
11
+ padding: 16,
12
+ marginBottom: 8,
13
+ },
14
+ label: {
15
+ fontWeight: "600",
16
+ marginBottom: 12,
17
+ },
18
+ uploadBox: {
19
+ padding: 48,
20
+ borderRadius: 16,
21
+ alignItems: "center",
22
+ justifyContent: "center",
23
+ borderWidth: 2,
24
+ borderStyle: "dashed",
25
+ },
26
+ uploadText: {
27
+ fontWeight: "600",
28
+ marginTop: 12,
29
+ },
30
+ scroll: {
31
+ marginHorizontal: -16,
32
+ paddingHorizontal: 16,
33
+ },
34
+ imageCard: {
35
+ width: 120,
36
+ height: 120,
37
+ borderRadius: 12,
38
+ marginRight: 12,
39
+ position: "relative",
40
+ },
41
+ imagePreview: {
42
+ width: "100%",
43
+ height: "100%",
44
+ borderRadius: 12,
45
+ },
46
+ removeButton: {
47
+ position: "absolute",
48
+ top: 6,
49
+ right: 6,
50
+ width: 24,
51
+ height: 24,
52
+ borderRadius: 12,
53
+ alignItems: "center",
54
+ justifyContent: "center",
55
+ },
56
+ imageBadge: {
57
+ position: "absolute",
58
+ bottom: 6,
59
+ left: 6,
60
+ width: 24,
61
+ height: 24,
62
+ borderRadius: 12,
63
+ alignItems: "center",
64
+ justifyContent: "center",
65
+ },
66
+ badgeText: {
67
+ fontSize: 10,
68
+ },
69
+ addMoreCard: {
70
+ width: 120,
71
+ height: 120,
72
+ borderRadius: 12,
73
+ alignItems: "center",
74
+ justifyContent: "center",
75
+ borderWidth: 2,
76
+ borderStyle: "dashed",
77
+ },
78
+ addMoreText: {
79
+ marginTop: 4,
80
+ },
81
+ });
82
+ }
83
+
84
+ export type ImageSelectionGridStyles = ReturnType<typeof createImageSelectionGridStyles>;