@umituz/react-native-ai-generation-content 1.13.1 → 1.15.0

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.
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * ResultStoryCard Component
3
- * Displays story text with quote styling
3
+ * Displays story text with quote styling - fully configurable
4
4
  */
5
5
 
6
6
  import * as React from "react";
@@ -11,63 +11,113 @@ import {
11
11
  useAppDesignTokens,
12
12
  } from "@umituz/react-native-design-system";
13
13
  import { LinearGradient } from "expo-linear-gradient";
14
+ import type { ResultStoryConfig } from "../../types/result-config.types";
15
+ import { DEFAULT_RESULT_CONFIG } from "../../types/result-config.types";
14
16
 
15
17
  export interface ResultStoryCardProps {
16
18
  story: string;
19
+ config?: ResultStoryConfig;
17
20
  }
18
21
 
19
- export const ResultStoryCard: React.FC<ResultStoryCardProps> = ({ story }) => {
22
+ export const ResultStoryCard: React.FC<ResultStoryCardProps> = ({
23
+ story,
24
+ config = DEFAULT_RESULT_CONFIG.story,
25
+ }) => {
20
26
  const tokens = useAppDesignTokens();
27
+ const cfg = { ...DEFAULT_RESULT_CONFIG.story, ...config };
21
28
 
22
- const styles = useMemo(() => StyleSheet.create({
23
- outer: {
24
- paddingHorizontal: 20,
25
- marginBottom: 20,
26
- },
27
- container: {
28
- padding: 20,
29
+ const containerStyle = useMemo(() => {
30
+ const base = {
31
+ padding: cfg.spacing?.padding ?? 20,
29
32
  borderRadius: 16,
30
- borderWidth: 1,
31
- borderColor: tokens.colors.primaryContainer,
32
- },
33
- quoteIcon: {
34
- fontSize: 40,
35
- lineHeight: 40,
36
- color: tokens.colors.primary,
37
- opacity: 0.4,
38
- marginBottom: -12,
39
- },
40
- quoteEnd: {
41
- alignItems: "flex-end",
42
- marginTop: -12,
43
- },
44
- quoteIconEnd: {
45
- marginBottom: 0,
46
- },
47
- text: {
48
- fontSize: 14,
49
- color: tokens.colors.textPrimary,
50
- textAlign: "center",
51
- lineHeight: 22,
52
- fontStyle: "italic",
53
- fontWeight: "500",
54
- },
55
- }), [tokens]);
33
+ };
56
34
 
57
- return (
58
- <View style={styles.outer}>
59
- <LinearGradient
60
- colors={[tokens.colors.primaryContainer, tokens.colors.surface]}
61
- style={styles.container}
62
- >
63
- <AtomicText style={styles.quoteIcon}>&quot;</AtomicText>
64
- <AtomicText style={styles.text}>{story}</AtomicText>
35
+ if (cfg.borderStyle === "outline") {
36
+ return {
37
+ ...base,
38
+ borderWidth: 1,
39
+ borderColor: tokens.colors.primaryContainer,
40
+ backgroundColor: "transparent",
41
+ };
42
+ } else if (cfg.borderStyle === "filled") {
43
+ return {
44
+ ...base,
45
+ backgroundColor: tokens.colors.primaryContainer,
46
+ };
47
+ }
48
+
49
+ return base;
50
+ }, [cfg.borderStyle, cfg.spacing, tokens]);
51
+
52
+ const styles = useMemo(
53
+ () =>
54
+ StyleSheet.create({
55
+ outer: {
56
+ paddingHorizontal: cfg.spacing?.paddingHorizontal ?? 20,
57
+ marginBottom: cfg.spacing?.marginBottom ?? 20,
58
+ },
59
+ container: containerStyle,
60
+ quoteIcon: {
61
+ fontSize: 40,
62
+ lineHeight: 40,
63
+ color: tokens.colors.primary,
64
+ opacity: 0.4,
65
+ marginBottom: -12,
66
+ },
67
+ quoteEnd: {
68
+ alignItems:
69
+ cfg.textAlignment === "left"
70
+ ? "flex-start"
71
+ : cfg.textAlignment === "right"
72
+ ? "flex-end"
73
+ : "flex-end",
74
+ marginTop: -12,
75
+ },
76
+ quoteIconEnd: {
77
+ marginBottom: 0,
78
+ },
79
+ text: {
80
+ fontSize: cfg.fontSize ?? 14,
81
+ color: tokens.colors.textPrimary,
82
+ textAlign: cfg.textAlignment ?? "center",
83
+ lineHeight: (cfg.fontSize ?? 14) * 1.57,
84
+ fontStyle: cfg.fontStyle ?? "italic",
85
+ fontWeight: cfg.fontWeight ?? "500",
86
+ },
87
+ }),
88
+ [tokens, cfg, containerStyle],
89
+ );
90
+
91
+ const renderContent = () => (
92
+ <>
93
+ {cfg.showQuotes && <AtomicText style={styles.quoteIcon}>&quot;</AtomicText>}
94
+ <AtomicText style={styles.text}>{story}</AtomicText>
95
+ {cfg.showQuotes && (
65
96
  <View style={styles.quoteEnd}>
66
97
  <AtomicText style={[styles.quoteIcon, styles.quoteIconEnd]}>
67
98
  &quot;
68
99
  </AtomicText>
69
100
  </View>
70
- </LinearGradient>
101
+ )}
102
+ </>
103
+ );
104
+
105
+ if (cfg.borderStyle === "gradient") {
106
+ return (
107
+ <View style={styles.outer}>
108
+ <LinearGradient
109
+ colors={[tokens.colors.primaryContainer, tokens.colors.surface]}
110
+ style={styles.container}
111
+ >
112
+ {renderContent()}
113
+ </LinearGradient>
114
+ </View>
115
+ );
116
+ }
117
+
118
+ return (
119
+ <View style={styles.outer}>
120
+ <View style={styles.container}>{renderContent()}</View>
71
121
  </View>
72
122
  );
73
123
  };
@@ -19,3 +19,17 @@ export type { ResultStoryCardProps } from "./ResultStoryCard";
19
19
 
20
20
  export { ResultActions } from "./ResultActions";
21
21
  export type { ResultActionsProps } from "./ResultActions";
22
+
23
+ export {
24
+ DEFAULT_RESULT_CONFIG,
25
+ } from "../../types/result-config.types";
26
+
27
+ export type {
28
+ ResultConfig,
29
+ ResultHeaderConfig,
30
+ ResultImageConfig,
31
+ ResultStoryConfig,
32
+ ResultActionsConfig,
33
+ ResultLayoutConfig,
34
+ ResultActionButton,
35
+ } from "../../types/result-config.types";
@@ -34,3 +34,9 @@ export type {
34
34
  PhotoGenerationState,
35
35
  PhotoGenerationStatus,
36
36
  } from "./photo-generation.types";
37
+
38
+ export { useGenerationFlow } from "./useGenerationFlow";
39
+ export type {
40
+ UseGenerationFlowOptions,
41
+ UseGenerationFlowReturn,
42
+ } from "./useGenerationFlow";
@@ -0,0 +1,315 @@
1
+ /**
2
+ * useGenerationFlow Hook
3
+ * Manages step-by-step generation flow state and navigation
4
+ *
5
+ * @package @umituz/react-native-ai-generation-content
6
+ */
7
+
8
+ import { useState, useCallback, useMemo } from "react";
9
+ import type {
10
+ GenerationFlowConfig,
11
+ GenerationFlowState,
12
+ PhotoStepData,
13
+ PhotoStepConfig,
14
+ } from "../types/flow-config.types";
15
+
16
+ export interface UseGenerationFlowOptions {
17
+ /** Flow configuration */
18
+ config: GenerationFlowConfig;
19
+ /** Callback when flow is complete */
20
+ onComplete?: (state: GenerationFlowState) => void;
21
+ /** Callback when step changes */
22
+ onStepChange?: (stepIndex: number, stepConfig: PhotoStepConfig) => void;
23
+ }
24
+
25
+ export interface UseGenerationFlowReturn {
26
+ /** Current flow state */
27
+ state: GenerationFlowState;
28
+ /** Current step configuration */
29
+ currentStepConfig: PhotoStepConfig | null;
30
+ /** Current step data */
31
+ currentStepData: PhotoStepData | null;
32
+ /** Whether can go to next step */
33
+ canGoNext: boolean;
34
+ /** Whether can go to previous step */
35
+ canGoBack: boolean;
36
+ /** Go to next step */
37
+ goNext: () => void;
38
+ /** Go to previous step */
39
+ goBack: () => void;
40
+ /** Update current step photo */
41
+ updatePhoto: (imageUri: string, previewUrl?: string) => void;
42
+ /** Update current step name */
43
+ updateName: (name: string) => void;
44
+ /** Update current step validation */
45
+ updateValidation: (isValid: boolean) => void;
46
+ /** Update text input */
47
+ updateTextInput: (text: string) => void;
48
+ /** Reset flow */
49
+ reset: () => void;
50
+ /** Complete flow */
51
+ complete: () => void;
52
+ }
53
+
54
+ /**
55
+ * Hook to manage generation flow state
56
+ */
57
+ export const useGenerationFlow = ({
58
+ config,
59
+ onComplete,
60
+ onStepChange,
61
+ }: UseGenerationFlowOptions): UseGenerationFlowReturn => {
62
+ // Initialize state
63
+ const [state, setState] = useState<GenerationFlowState>(() => ({
64
+ currentStepIndex: 0,
65
+ photoSteps: config.photoSteps.map((step) => ({
66
+ id: step.id,
67
+ imageUri: null,
68
+ previewUrl: undefined,
69
+ name: undefined,
70
+ isValid: undefined,
71
+ validationStatus: "pending",
72
+ })),
73
+ textInput: config.textInputStep
74
+ ? {
75
+ id: config.textInputStep.id,
76
+ text: "",
77
+ isValid: false,
78
+ }
79
+ : undefined,
80
+ isComplete: false,
81
+ isProcessing: false,
82
+ }));
83
+
84
+ // Get current step configuration
85
+ const currentStepConfig = useMemo(() => {
86
+ if (state.currentStepIndex >= config.photoSteps.length) {
87
+ return null;
88
+ }
89
+ return config.photoSteps[state.currentStepIndex];
90
+ }, [state.currentStepIndex, config.photoSteps]);
91
+
92
+ // Get current step data
93
+ const currentStepData = useMemo(() => {
94
+ if (state.currentStepIndex >= state.photoSteps.length) {
95
+ return null;
96
+ }
97
+ return state.photoSteps[state.currentStepIndex];
98
+ }, [state.currentStepIndex, state.photoSteps]);
99
+
100
+ // Check if current step is valid
101
+ const isCurrentStepValid = useMemo(() => {
102
+ if (!currentStepData || !currentStepConfig) {
103
+ return false;
104
+ }
105
+
106
+ // Check photo
107
+ if (!currentStepData.imageUri) {
108
+ return false;
109
+ }
110
+
111
+ // Check name if required
112
+ if (currentStepConfig.requireNameInput && !currentStepData.name) {
113
+ return false;
114
+ }
115
+
116
+ // Check validation if enabled
117
+ if (currentStepConfig.enableValidation && !currentStepData.isValid) {
118
+ return false;
119
+ }
120
+
121
+ return true;
122
+ }, [currentStepData, currentStepConfig]);
123
+
124
+ // Can go to next step
125
+ const canGoNext = useMemo(() => {
126
+ return isCurrentStepValid && !state.isProcessing;
127
+ }, [isCurrentStepValid, state.isProcessing]);
128
+
129
+ // Can go back
130
+ const canGoBack = useMemo(() => {
131
+ return (
132
+ state.currentStepIndex > 0 &&
133
+ config.behavior?.allowBack !== false &&
134
+ !state.isProcessing
135
+ );
136
+ }, [state.currentStepIndex, config.behavior?.allowBack, state.isProcessing]);
137
+
138
+ // Go to next step
139
+ const goNext = useCallback(() => {
140
+ if (!canGoNext) return;
141
+
142
+ setState((prev) => {
143
+ const nextIndex = prev.currentStepIndex + 1;
144
+
145
+ // If we've completed all photo steps, mark as complete
146
+ if (nextIndex >= config.photoSteps.length) {
147
+ const newState = {
148
+ ...prev,
149
+ isComplete: true,
150
+ };
151
+ onComplete?.(newState);
152
+ return newState;
153
+ }
154
+
155
+ // Move to next step
156
+ const newState = {
157
+ ...prev,
158
+ currentStepIndex: nextIndex,
159
+ };
160
+
161
+ // Notify step change
162
+ onStepChange?.(nextIndex, config.photoSteps[nextIndex]);
163
+
164
+ return newState;
165
+ });
166
+ }, [canGoNext, config.photoSteps, onComplete, onStepChange]);
167
+
168
+ // Go to previous step
169
+ const goBack = useCallback(() => {
170
+ if (!canGoBack) return;
171
+
172
+ setState((prev) => {
173
+ const prevIndex = prev.currentStepIndex - 1;
174
+ const newState = {
175
+ ...prev,
176
+ currentStepIndex: prevIndex,
177
+ isComplete: false,
178
+ };
179
+
180
+ // Notify step change
181
+ onStepChange?.(prevIndex, config.photoSteps[prevIndex]);
182
+
183
+ return newState;
184
+ });
185
+ }, [canGoBack, config.photoSteps, onStepChange]);
186
+
187
+ // Update photo
188
+ const updatePhoto = useCallback(
189
+ (imageUri: string, previewUrl?: string) => {
190
+ setState((prev) => {
191
+ const newPhotoSteps = [...prev.photoSteps];
192
+ newPhotoSteps[prev.currentStepIndex] = {
193
+ ...newPhotoSteps[prev.currentStepIndex],
194
+ imageUri,
195
+ previewUrl,
196
+ validationStatus: "pending",
197
+ };
198
+
199
+ return {
200
+ ...prev,
201
+ photoSteps: newPhotoSteps,
202
+ };
203
+ });
204
+ },
205
+ [],
206
+ );
207
+
208
+ // Update name
209
+ const updateName = useCallback((name: string) => {
210
+ setState((prev) => {
211
+ const newPhotoSteps = [...prev.photoSteps];
212
+ newPhotoSteps[prev.currentStepIndex] = {
213
+ ...newPhotoSteps[prev.currentStepIndex],
214
+ name,
215
+ };
216
+
217
+ return {
218
+ ...prev,
219
+ photoSteps: newPhotoSteps,
220
+ };
221
+ });
222
+ }, []);
223
+
224
+ // Update validation
225
+ const updateValidation = useCallback((isValid: boolean) => {
226
+ setState((prev) => {
227
+ const newPhotoSteps = [...prev.photoSteps];
228
+ newPhotoSteps[prev.currentStepIndex] = {
229
+ ...newPhotoSteps[prev.currentStepIndex],
230
+ isValid,
231
+ validationStatus: isValid ? "valid" : "invalid",
232
+ };
233
+
234
+ return {
235
+ ...prev,
236
+ photoSteps: newPhotoSteps,
237
+ };
238
+ });
239
+ }, []);
240
+
241
+ // Update text input
242
+ const updateTextInput = useCallback(
243
+ (text: string) => {
244
+ setState((prev) => {
245
+ if (!prev.textInput) return prev;
246
+
247
+ const minLength = config.textInputStep?.minLength ?? 0;
248
+ const maxLength = config.textInputStep?.maxLength ?? Infinity;
249
+ const isValid = text.length >= minLength && text.length <= maxLength;
250
+
251
+ return {
252
+ ...prev,
253
+ textInput: {
254
+ ...prev.textInput,
255
+ text,
256
+ isValid,
257
+ },
258
+ };
259
+ });
260
+ },
261
+ [config.textInputStep],
262
+ );
263
+
264
+ // Reset flow
265
+ const reset = useCallback(() => {
266
+ setState({
267
+ currentStepIndex: 0,
268
+ photoSteps: config.photoSteps.map((step) => ({
269
+ id: step.id,
270
+ imageUri: null,
271
+ previewUrl: undefined,
272
+ name: undefined,
273
+ isValid: undefined,
274
+ validationStatus: "pending",
275
+ })),
276
+ textInput: config.textInputStep
277
+ ? {
278
+ id: config.textInputStep.id,
279
+ text: "",
280
+ isValid: false,
281
+ }
282
+ : undefined,
283
+ isComplete: false,
284
+ isProcessing: false,
285
+ });
286
+ }, [config.photoSteps, config.textInputStep]);
287
+
288
+ // Complete flow
289
+ const complete = useCallback(() => {
290
+ setState((prev) => {
291
+ const newState = {
292
+ ...prev,
293
+ isComplete: true,
294
+ };
295
+ onComplete?.(newState);
296
+ return newState;
297
+ });
298
+ }, [onComplete]);
299
+
300
+ return {
301
+ state,
302
+ currentStepConfig,
303
+ currentStepData,
304
+ canGoNext,
305
+ canGoBack,
306
+ goNext,
307
+ goBack,
308
+ updatePhoto,
309
+ updateName,
310
+ updateValidation,
311
+ updateTextInput,
312
+ reset,
313
+ complete,
314
+ };
315
+ };