@umituz/react-native-ai-generation-content 1.17.1 → 1.17.3
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/components/CreationsHomeCard.tsx +1 -1
- package/src/domains/creations/presentation/hooks/useAdvancedFilter.ts +0 -1
- package/src/domains/creations/presentation/screens/CreationsGalleryScreen.tsx +9 -6
- package/src/domains/face-detection/infrastructure/validators/faceValidator.ts +1 -1
- package/src/domains/prompts/infrastructure/services/PromptGenerationService.ts +1 -1
- package/src/domains/prompts/presentation/hooks/useFaceSwap.ts +2 -2
- package/src/domains/prompts/presentation/hooks/usePromptGeneration.ts +4 -4
- package/src/features/image-to-video/domain/index.ts +1 -0
- package/src/features/image-to-video/domain/types/image-to-video.types.ts +71 -0
- package/src/features/image-to-video/domain/types/index.ts +10 -0
- package/src/features/image-to-video/index.ts +27 -0
- package/src/features/image-to-video/infrastructure/index.ts +1 -0
- package/src/features/image-to-video/infrastructure/services/image-to-video-executor.ts +112 -0
- package/src/features/image-to-video/infrastructure/services/index.ts +5 -0
- package/src/features/image-to-video/presentation/hooks/index.ts +5 -0
- package/src/features/image-to-video/presentation/hooks/useImageToVideoFeature.ts +121 -0
- package/src/features/image-to-video/presentation/index.ts +1 -0
- package/src/features/text-to-image/domain/index.ts +1 -0
- package/src/features/text-to-image/domain/types/index.ts +10 -0
- package/src/features/text-to-image/domain/types/text-to-image.types.ts +66 -0
- package/src/features/text-to-image/index.ts +27 -1
- package/src/features/text-to-image/infrastructure/index.ts +1 -0
- package/src/features/text-to-image/infrastructure/services/index.ts +5 -0
- package/src/features/text-to-image/infrastructure/services/text-to-image-executor.ts +113 -0
- package/src/features/text-to-image/presentation/hooks/index.ts +5 -0
- package/src/features/text-to-image/presentation/hooks/useTextToImageFeature.ts +111 -0
- package/src/features/text-to-image/presentation/index.ts +1 -0
- package/src/features/text-to-video/domain/index.ts +1 -0
- package/src/features/text-to-video/domain/types/index.ts +10 -0
- package/src/features/text-to-video/domain/types/text-to-video.types.ts +65 -0
- package/src/features/text-to-video/index.ts +27 -1
- package/src/features/text-to-video/infrastructure/index.ts +1 -0
- package/src/features/text-to-video/infrastructure/services/index.ts +5 -0
- package/src/features/text-to-video/infrastructure/services/text-to-video-executor.ts +108 -0
- package/src/features/text-to-video/presentation/hooks/index.ts +5 -0
- package/src/features/text-to-video/presentation/hooks/useTextToVideoFeature.ts +111 -0
- package/src/features/text-to-video/presentation/index.ts +1 -0
- package/src/features/text-to-voice/domain/index.ts +1 -0
- package/src/features/text-to-voice/domain/types/index.ts +10 -0
- package/src/features/text-to-voice/domain/types/text-to-voice.types.ts +65 -0
- package/src/features/text-to-voice/index.ts +27 -0
- package/src/features/text-to-voice/infrastructure/index.ts +1 -0
- package/src/features/text-to-voice/infrastructure/services/index.ts +5 -0
- package/src/features/text-to-voice/infrastructure/services/text-to-voice-executor.ts +111 -0
- package/src/features/text-to-voice/presentation/hooks/index.ts +5 -0
- package/src/features/text-to-voice/presentation/hooks/useTextToVoiceFeature.ts +105 -0
- package/src/features/text-to-voice/presentation/index.ts +1 -0
- package/src/index.ts +24 -0
- package/src/presentation/components/buttons/GenerateButton.tsx +141 -0
- package/src/presentation/components/buttons/index.ts +1 -0
- package/src/presentation/components/display/ErrorDisplay.tsx +111 -0
- package/src/presentation/components/display/ResultDisplay.tsx +122 -0
- package/src/presentation/components/display/index.ts +6 -0
- package/src/presentation/components/headers/FeatureHeader.tsx +85 -0
- package/src/presentation/components/headers/index.ts +1 -0
- package/src/presentation/components/image-picker/DualImagePicker.tsx +95 -0
- package/src/presentation/components/image-picker/ImagePickerBox.tsx +165 -0
- package/src/presentation/components/image-picker/index.ts +2 -0
- package/src/presentation/components/index.ts +4 -0
- package/src/features/text-to-image/domain/entities.ts +0 -58
- package/src/features/text-to-video/domain/entities.ts +0 -52
- package/src/types/jsx.d.ts +0 -19
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text-to-Voice Executor
|
|
3
|
+
* Provider-agnostic text-to-voice execution using active AI provider
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { providerRegistry } from "../../../../infrastructure/services";
|
|
7
|
+
import type {
|
|
8
|
+
TextToVoiceRequest,
|
|
9
|
+
TextToVoiceResult,
|
|
10
|
+
TextToVoiceInputBuilder,
|
|
11
|
+
TextToVoiceResultExtractor,
|
|
12
|
+
} from "../../domain/types";
|
|
13
|
+
|
|
14
|
+
declare const __DEV__: boolean;
|
|
15
|
+
|
|
16
|
+
export interface ExecuteTextToVoiceOptions {
|
|
17
|
+
model: string;
|
|
18
|
+
buildInput: TextToVoiceInputBuilder;
|
|
19
|
+
extractResult?: TextToVoiceResultExtractor;
|
|
20
|
+
onProgress?: (progress: number) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function defaultExtractResult(
|
|
24
|
+
result: unknown,
|
|
25
|
+
): { audioUrl?: string; duration?: number } | undefined {
|
|
26
|
+
if (typeof result !== "object" || result === null) return undefined;
|
|
27
|
+
|
|
28
|
+
const r = result as Record<string, unknown>;
|
|
29
|
+
|
|
30
|
+
if (typeof r.audio === "string") {
|
|
31
|
+
return { audioUrl: r.audio };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (typeof r.audio_url === "string") {
|
|
35
|
+
return { audioUrl: r.audio_url };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (r.audio && typeof r.audio === "object") {
|
|
39
|
+
const audio = r.audio as Record<string, unknown>;
|
|
40
|
+
if (typeof audio.url === "string") {
|
|
41
|
+
return {
|
|
42
|
+
audioUrl: audio.url,
|
|
43
|
+
duration: typeof audio.duration === "number" ? audio.duration : undefined,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function executeTextToVoice(
|
|
52
|
+
request: TextToVoiceRequest,
|
|
53
|
+
options: ExecuteTextToVoiceOptions,
|
|
54
|
+
): Promise<TextToVoiceResult> {
|
|
55
|
+
const provider = providerRegistry.getActiveProvider();
|
|
56
|
+
|
|
57
|
+
if (!provider) {
|
|
58
|
+
return { success: false, error: "No AI provider configured" };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!provider.isInitialized()) {
|
|
62
|
+
return { success: false, error: "AI provider not initialized" };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!request.text) {
|
|
66
|
+
return { success: false, error: "Text is required" };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const { model, buildInput, extractResult, onProgress } = options;
|
|
70
|
+
|
|
71
|
+
if (__DEV__) {
|
|
72
|
+
// eslint-disable-next-line no-console
|
|
73
|
+
console.log(`[TextToVoice] Provider: ${provider.providerId}, Model: ${model}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
onProgress?.(10);
|
|
78
|
+
|
|
79
|
+
const input = buildInput(request.text, request.options);
|
|
80
|
+
onProgress?.(20);
|
|
81
|
+
|
|
82
|
+
const result = await provider.run(model, input);
|
|
83
|
+
onProgress?.(90);
|
|
84
|
+
|
|
85
|
+
const extractor = extractResult || defaultExtractResult;
|
|
86
|
+
const extracted = extractor(result);
|
|
87
|
+
onProgress?.(100);
|
|
88
|
+
|
|
89
|
+
if (!extracted?.audioUrl) {
|
|
90
|
+
return { success: false, error: "No audio in response" };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
success: true,
|
|
95
|
+
audioUrl: extracted.audioUrl,
|
|
96
|
+
duration: extracted.duration,
|
|
97
|
+
};
|
|
98
|
+
} catch (error) {
|
|
99
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
100
|
+
if (__DEV__) {
|
|
101
|
+
// eslint-disable-next-line no-console
|
|
102
|
+
console.error("[TextToVoice] Error:", message);
|
|
103
|
+
}
|
|
104
|
+
return { success: false, error: message };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function hasTextToVoiceSupport(): boolean {
|
|
109
|
+
const provider = providerRegistry.getActiveProvider();
|
|
110
|
+
return provider !== null && provider.isInitialized();
|
|
111
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text-to-Voice Feature Hook
|
|
3
|
+
* Provider-agnostic hook for text-to-voice generation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useCallback } from "react";
|
|
7
|
+
import { executeTextToVoice } from "../../infrastructure/services";
|
|
8
|
+
import type {
|
|
9
|
+
TextToVoiceFeatureState,
|
|
10
|
+
TextToVoiceFeatureConfig,
|
|
11
|
+
TextToVoiceResult,
|
|
12
|
+
TextToVoiceOptions,
|
|
13
|
+
} from "../../domain/types";
|
|
14
|
+
|
|
15
|
+
export interface UseTextToVoiceFeatureProps {
|
|
16
|
+
config: TextToVoiceFeatureConfig;
|
|
17
|
+
userId: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface UseTextToVoiceFeatureReturn {
|
|
21
|
+
state: TextToVoiceFeatureState;
|
|
22
|
+
setText: (text: string) => void;
|
|
23
|
+
generate: (options?: TextToVoiceOptions) => Promise<TextToVoiceResult>;
|
|
24
|
+
reset: () => void;
|
|
25
|
+
isReady: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const initialState: TextToVoiceFeatureState = {
|
|
29
|
+
text: "",
|
|
30
|
+
audioUrl: null,
|
|
31
|
+
isProcessing: false,
|
|
32
|
+
progress: 0,
|
|
33
|
+
error: null,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export function useTextToVoiceFeature(
|
|
37
|
+
props: UseTextToVoiceFeatureProps,
|
|
38
|
+
): UseTextToVoiceFeatureReturn {
|
|
39
|
+
const { config, userId } = props;
|
|
40
|
+
const [state, setState] = useState<TextToVoiceFeatureState>(initialState);
|
|
41
|
+
|
|
42
|
+
const setText = useCallback(
|
|
43
|
+
(text: string) => {
|
|
44
|
+
setState((prev) => ({ ...prev, text, error: null }));
|
|
45
|
+
config.onTextChange?.(text);
|
|
46
|
+
},
|
|
47
|
+
[config],
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const generate = useCallback(
|
|
51
|
+
async (options?: TextToVoiceOptions): Promise<TextToVoiceResult> => {
|
|
52
|
+
if (!state.text) {
|
|
53
|
+
const error = "Text is required";
|
|
54
|
+
setState((prev) => ({ ...prev, error }));
|
|
55
|
+
return { success: false, error };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
setState((prev) => ({
|
|
59
|
+
...prev,
|
|
60
|
+
isProcessing: true,
|
|
61
|
+
progress: 0,
|
|
62
|
+
error: null,
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
config.onProcessingStart?.();
|
|
66
|
+
|
|
67
|
+
const result = await executeTextToVoice(
|
|
68
|
+
{ text: state.text, userId, options },
|
|
69
|
+
{
|
|
70
|
+
model: config.model,
|
|
71
|
+
buildInput: config.buildInput,
|
|
72
|
+
extractResult: config.extractResult,
|
|
73
|
+
onProgress: (progress) => {
|
|
74
|
+
setState((prev) => ({ ...prev, progress }));
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
if (result.success && result.audioUrl) {
|
|
80
|
+
setState((prev) => ({
|
|
81
|
+
...prev,
|
|
82
|
+
audioUrl: result.audioUrl ?? null,
|
|
83
|
+
isProcessing: false,
|
|
84
|
+
progress: 100,
|
|
85
|
+
}));
|
|
86
|
+
} else {
|
|
87
|
+
const error = result.error || "Generation failed";
|
|
88
|
+
setState((prev) => ({ ...prev, isProcessing: false, error }));
|
|
89
|
+
config.onError?.(error);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
config.onProcessingComplete?.(result);
|
|
93
|
+
return result;
|
|
94
|
+
},
|
|
95
|
+
[state.text, userId, config],
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const reset = useCallback(() => {
|
|
99
|
+
setState(initialState);
|
|
100
|
+
}, []);
|
|
101
|
+
|
|
102
|
+
const isReady = state.text.length > 0 && !state.isProcessing;
|
|
103
|
+
|
|
104
|
+
return { state, setText, generate, reset, isReady };
|
|
105
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./hooks";
|
package/src/index.ts
CHANGED
|
@@ -353,3 +353,27 @@ export * from "./features/remove-background";
|
|
|
353
353
|
|
|
354
354
|
export * from "./features/remove-object";
|
|
355
355
|
|
|
356
|
+
// =============================================================================
|
|
357
|
+
// FEATURES - Text-to-Video
|
|
358
|
+
// =============================================================================
|
|
359
|
+
|
|
360
|
+
export * from "./features/text-to-video";
|
|
361
|
+
|
|
362
|
+
// =============================================================================
|
|
363
|
+
// FEATURES - Text-to-Image
|
|
364
|
+
// =============================================================================
|
|
365
|
+
|
|
366
|
+
export * from "./features/text-to-image";
|
|
367
|
+
|
|
368
|
+
// =============================================================================
|
|
369
|
+
// FEATURES - Image-to-Video
|
|
370
|
+
// =============================================================================
|
|
371
|
+
|
|
372
|
+
export * from "./features/image-to-video";
|
|
373
|
+
|
|
374
|
+
// =============================================================================
|
|
375
|
+
// FEATURES - Text-to-Voice
|
|
376
|
+
// =============================================================================
|
|
377
|
+
|
|
378
|
+
export * from "./features/text-to-voice";
|
|
379
|
+
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GenerateButton Component
|
|
3
|
+
* Generic AI generation button with gradient/solid variants
|
|
4
|
+
* Props-driven for 100+ apps compatibility
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { View, StyleSheet, TouchableOpacity } from "react-native";
|
|
9
|
+
import {
|
|
10
|
+
AtomicText,
|
|
11
|
+
useAppDesignTokens,
|
|
12
|
+
AtomicIcon,
|
|
13
|
+
} from "@umituz/react-native-design-system";
|
|
14
|
+
import { LinearGradient } from "expo-linear-gradient";
|
|
15
|
+
|
|
16
|
+
export interface GenerateButtonProps {
|
|
17
|
+
readonly isDisabled?: boolean;
|
|
18
|
+
readonly isProcessing?: boolean;
|
|
19
|
+
readonly onPress: () => void;
|
|
20
|
+
readonly text: string;
|
|
21
|
+
readonly processingText?: string;
|
|
22
|
+
readonly variant?: "gradient" | "solid";
|
|
23
|
+
readonly gradientColors?: readonly [string, string, ...string[]];
|
|
24
|
+
readonly icon?: string;
|
|
25
|
+
readonly iconSize?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const GenerateButton: React.FC<GenerateButtonProps> = ({
|
|
29
|
+
isDisabled = false,
|
|
30
|
+
isProcessing = false,
|
|
31
|
+
onPress,
|
|
32
|
+
text,
|
|
33
|
+
processingText,
|
|
34
|
+
variant = "gradient",
|
|
35
|
+
gradientColors = ["#FF6B9D", "#C74375", "#FF6B9D"],
|
|
36
|
+
icon = "sparkles",
|
|
37
|
+
iconSize = 24,
|
|
38
|
+
}) => {
|
|
39
|
+
const tokens = useAppDesignTokens();
|
|
40
|
+
const disabled = isDisabled || isProcessing;
|
|
41
|
+
const displayText = isProcessing && processingText ? processingText : text;
|
|
42
|
+
|
|
43
|
+
if (variant === "solid") {
|
|
44
|
+
return (
|
|
45
|
+
<View style={[styles.solidContainer, { marginTop: tokens.spacing.xl }]}>
|
|
46
|
+
<TouchableOpacity
|
|
47
|
+
onPress={onPress}
|
|
48
|
+
disabled={disabled}
|
|
49
|
+
activeOpacity={0.8}
|
|
50
|
+
style={[
|
|
51
|
+
styles.solidButton,
|
|
52
|
+
{
|
|
53
|
+
backgroundColor: disabled
|
|
54
|
+
? tokens.colors.surfaceSecondary
|
|
55
|
+
: tokens.colors.primary,
|
|
56
|
+
},
|
|
57
|
+
]}
|
|
58
|
+
>
|
|
59
|
+
<View style={styles.buttonContent}>
|
|
60
|
+
<AtomicIcon name={icon} customSize={20} customColor="#FFFFFF" />
|
|
61
|
+
<AtomicText type="bodyLarge" style={styles.solidButtonText}>
|
|
62
|
+
{displayText}
|
|
63
|
+
</AtomicText>
|
|
64
|
+
</View>
|
|
65
|
+
</TouchableOpacity>
|
|
66
|
+
</View>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<View style={[styles.gradientContainer, { marginTop: tokens.spacing.xl }]}>
|
|
72
|
+
<TouchableOpacity
|
|
73
|
+
onPress={onPress}
|
|
74
|
+
disabled={disabled}
|
|
75
|
+
activeOpacity={0.85}
|
|
76
|
+
style={styles.buttonWrapper}
|
|
77
|
+
>
|
|
78
|
+
<LinearGradient
|
|
79
|
+
colors={disabled ? ["#9CA3AF", "#6B7280"] : gradientColors}
|
|
80
|
+
start={[0, 0]}
|
|
81
|
+
end={[1, 0]}
|
|
82
|
+
style={[styles.gradientButton, disabled && styles.disabledButton]}
|
|
83
|
+
>
|
|
84
|
+
<View style={styles.buttonContent}>
|
|
85
|
+
<AtomicIcon name={icon} customSize={iconSize} customColor="#FFF" />
|
|
86
|
+
<AtomicText type="bodyLarge" style={styles.gradientButtonText}>
|
|
87
|
+
{displayText}
|
|
88
|
+
</AtomicText>
|
|
89
|
+
</View>
|
|
90
|
+
</LinearGradient>
|
|
91
|
+
</TouchableOpacity>
|
|
92
|
+
</View>
|
|
93
|
+
);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const styles = StyleSheet.create({
|
|
97
|
+
gradientContainer: {
|
|
98
|
+
alignItems: "center",
|
|
99
|
+
paddingHorizontal: 16,
|
|
100
|
+
},
|
|
101
|
+
solidContainer: {
|
|
102
|
+
paddingHorizontal: 16,
|
|
103
|
+
},
|
|
104
|
+
buttonWrapper: {
|
|
105
|
+
width: "100%",
|
|
106
|
+
maxWidth: 320,
|
|
107
|
+
borderRadius: 30,
|
|
108
|
+
},
|
|
109
|
+
gradientButton: {
|
|
110
|
+
paddingVertical: 18,
|
|
111
|
+
paddingHorizontal: 32,
|
|
112
|
+
borderRadius: 30,
|
|
113
|
+
alignItems: "center",
|
|
114
|
+
justifyContent: "center",
|
|
115
|
+
},
|
|
116
|
+
solidButton: {
|
|
117
|
+
borderRadius: 16,
|
|
118
|
+
paddingVertical: 16,
|
|
119
|
+
paddingHorizontal: 24,
|
|
120
|
+
alignItems: "center",
|
|
121
|
+
justifyContent: "center",
|
|
122
|
+
},
|
|
123
|
+
disabledButton: {
|
|
124
|
+
opacity: 0.5,
|
|
125
|
+
},
|
|
126
|
+
buttonContent: {
|
|
127
|
+
flexDirection: "row",
|
|
128
|
+
alignItems: "center",
|
|
129
|
+
justifyContent: "center",
|
|
130
|
+
gap: 12,
|
|
131
|
+
},
|
|
132
|
+
gradientButtonText: {
|
|
133
|
+
color: "#FFFFFF",
|
|
134
|
+
fontWeight: "700",
|
|
135
|
+
fontSize: 18,
|
|
136
|
+
},
|
|
137
|
+
solidButtonText: {
|
|
138
|
+
color: "#FFFFFF",
|
|
139
|
+
fontWeight: "600",
|
|
140
|
+
},
|
|
141
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { GenerateButton, type GenerateButtonProps } from "./GenerateButton";
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ErrorDisplay Component
|
|
3
|
+
* Generic error display with retry action
|
|
4
|
+
* Props-driven for 100+ apps compatibility
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { View, StyleSheet, TouchableOpacity } from "react-native";
|
|
9
|
+
import {
|
|
10
|
+
AtomicText,
|
|
11
|
+
useAppDesignTokens,
|
|
12
|
+
AtomicIcon,
|
|
13
|
+
} from "@umituz/react-native-design-system";
|
|
14
|
+
|
|
15
|
+
export interface ErrorDisplayProps {
|
|
16
|
+
readonly error: string | null;
|
|
17
|
+
readonly onRetry?: () => void;
|
|
18
|
+
readonly retryText?: string;
|
|
19
|
+
readonly icon?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const ErrorDisplay: React.FC<ErrorDisplayProps> = ({
|
|
23
|
+
error,
|
|
24
|
+
onRetry,
|
|
25
|
+
retryText,
|
|
26
|
+
icon = "alert-circle",
|
|
27
|
+
}) => {
|
|
28
|
+
const tokens = useAppDesignTokens();
|
|
29
|
+
|
|
30
|
+
if (!error) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<View
|
|
36
|
+
style={[
|
|
37
|
+
styles.container,
|
|
38
|
+
{
|
|
39
|
+
backgroundColor: `${tokens.colors.error}15`,
|
|
40
|
+
borderColor: `${tokens.colors.error}30`,
|
|
41
|
+
padding: tokens.spacing.md,
|
|
42
|
+
marginTop: tokens.spacing.md,
|
|
43
|
+
},
|
|
44
|
+
]}
|
|
45
|
+
>
|
|
46
|
+
<View style={styles.header}>
|
|
47
|
+
<AtomicIcon
|
|
48
|
+
name={icon}
|
|
49
|
+
customSize={20}
|
|
50
|
+
customColor={tokens.colors.error}
|
|
51
|
+
/>
|
|
52
|
+
<AtomicText
|
|
53
|
+
type="bodyMedium"
|
|
54
|
+
style={[styles.errorText, { color: tokens.colors.error }]}
|
|
55
|
+
>
|
|
56
|
+
{error}
|
|
57
|
+
</AtomicText>
|
|
58
|
+
</View>
|
|
59
|
+
{onRetry && retryText && (
|
|
60
|
+
<TouchableOpacity
|
|
61
|
+
onPress={onRetry}
|
|
62
|
+
style={[
|
|
63
|
+
styles.retryButton,
|
|
64
|
+
{
|
|
65
|
+
backgroundColor: tokens.colors.error,
|
|
66
|
+
marginTop: tokens.spacing.sm,
|
|
67
|
+
},
|
|
68
|
+
]}
|
|
69
|
+
>
|
|
70
|
+
<AtomicIcon
|
|
71
|
+
name="refresh"
|
|
72
|
+
customSize={16}
|
|
73
|
+
customColor={tokens.colors.onError}
|
|
74
|
+
/>
|
|
75
|
+
<AtomicText
|
|
76
|
+
type="bodySmall"
|
|
77
|
+
style={{ color: tokens.colors.onError, fontWeight: "600" }}
|
|
78
|
+
>
|
|
79
|
+
{retryText}
|
|
80
|
+
</AtomicText>
|
|
81
|
+
</TouchableOpacity>
|
|
82
|
+
)}
|
|
83
|
+
</View>
|
|
84
|
+
);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const styles = StyleSheet.create({
|
|
88
|
+
container: {
|
|
89
|
+
borderRadius: 12,
|
|
90
|
+
borderWidth: 1,
|
|
91
|
+
},
|
|
92
|
+
header: {
|
|
93
|
+
flexDirection: "row",
|
|
94
|
+
alignItems: "flex-start",
|
|
95
|
+
gap: 10,
|
|
96
|
+
},
|
|
97
|
+
errorText: {
|
|
98
|
+
flex: 1,
|
|
99
|
+
lineHeight: 20,
|
|
100
|
+
},
|
|
101
|
+
retryButton: {
|
|
102
|
+
flexDirection: "row",
|
|
103
|
+
alignItems: "center",
|
|
104
|
+
justifyContent: "center",
|
|
105
|
+
gap: 6,
|
|
106
|
+
paddingVertical: 10,
|
|
107
|
+
paddingHorizontal: 16,
|
|
108
|
+
borderRadius: 8,
|
|
109
|
+
alignSelf: "flex-start",
|
|
110
|
+
},
|
|
111
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ResultDisplay Component
|
|
3
|
+
* Generic result display with save/reset actions
|
|
4
|
+
* Props-driven for 100+ apps compatibility
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { View, StyleSheet } from "react-native";
|
|
9
|
+
import {
|
|
10
|
+
AtomicText,
|
|
11
|
+
AtomicButton,
|
|
12
|
+
useAppDesignTokens,
|
|
13
|
+
AtomicIcon,
|
|
14
|
+
} from "@umituz/react-native-design-system";
|
|
15
|
+
|
|
16
|
+
export interface ResultDisplayAction {
|
|
17
|
+
readonly id: string;
|
|
18
|
+
readonly label: string;
|
|
19
|
+
readonly onPress: () => void;
|
|
20
|
+
readonly variant?: "primary" | "outline";
|
|
21
|
+
readonly icon?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ResultDisplayProps {
|
|
25
|
+
readonly visible?: boolean;
|
|
26
|
+
readonly successText: string;
|
|
27
|
+
readonly actions: ResultDisplayAction[];
|
|
28
|
+
readonly successIcon?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const ResultDisplay: React.FC<ResultDisplayProps> = ({
|
|
32
|
+
visible = true,
|
|
33
|
+
successText,
|
|
34
|
+
actions,
|
|
35
|
+
successIcon = "checkmark-circle",
|
|
36
|
+
}) => {
|
|
37
|
+
const tokens = useAppDesignTokens();
|
|
38
|
+
|
|
39
|
+
if (!visible) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<View style={[styles.container, { marginTop: tokens.spacing.lg }]}>
|
|
45
|
+
<View style={styles.successHeader}>
|
|
46
|
+
<AtomicIcon
|
|
47
|
+
name={successIcon}
|
|
48
|
+
customSize={24}
|
|
49
|
+
customColor={tokens.colors.success}
|
|
50
|
+
/>
|
|
51
|
+
<AtomicText
|
|
52
|
+
type="bodyMedium"
|
|
53
|
+
style={[styles.successText, { color: tokens.colors.success }]}
|
|
54
|
+
>
|
|
55
|
+
{successText}
|
|
56
|
+
</AtomicText>
|
|
57
|
+
</View>
|
|
58
|
+
<View style={[styles.actionButtons, { gap: tokens.spacing.sm }]}>
|
|
59
|
+
{actions.map((action) => (
|
|
60
|
+
<AtomicButton
|
|
61
|
+
key={action.id}
|
|
62
|
+
onPress={action.onPress}
|
|
63
|
+
variant={action.variant === "outline" ? "outline" : undefined}
|
|
64
|
+
style={styles.actionButton}
|
|
65
|
+
>
|
|
66
|
+
<View style={styles.buttonContent}>
|
|
67
|
+
{action.icon && (
|
|
68
|
+
<AtomicIcon
|
|
69
|
+
name={action.icon}
|
|
70
|
+
customSize={18}
|
|
71
|
+
customColor={
|
|
72
|
+
action.variant === "outline"
|
|
73
|
+
? tokens.colors.primary
|
|
74
|
+
: tokens.colors.onPrimary
|
|
75
|
+
}
|
|
76
|
+
/>
|
|
77
|
+
)}
|
|
78
|
+
<AtomicText
|
|
79
|
+
type="bodyMedium"
|
|
80
|
+
style={{
|
|
81
|
+
color:
|
|
82
|
+
action.variant === "outline"
|
|
83
|
+
? tokens.colors.primary
|
|
84
|
+
: tokens.colors.onPrimary,
|
|
85
|
+
}}
|
|
86
|
+
>
|
|
87
|
+
{action.label}
|
|
88
|
+
</AtomicText>
|
|
89
|
+
</View>
|
|
90
|
+
</AtomicButton>
|
|
91
|
+
))}
|
|
92
|
+
</View>
|
|
93
|
+
</View>
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const styles = StyleSheet.create({
|
|
98
|
+
container: {
|
|
99
|
+
alignItems: "center",
|
|
100
|
+
},
|
|
101
|
+
successHeader: {
|
|
102
|
+
flexDirection: "row",
|
|
103
|
+
alignItems: "center",
|
|
104
|
+
gap: 8,
|
|
105
|
+
marginBottom: 16,
|
|
106
|
+
},
|
|
107
|
+
successText: {
|
|
108
|
+
textAlign: "center",
|
|
109
|
+
},
|
|
110
|
+
actionButtons: {
|
|
111
|
+
width: "100%",
|
|
112
|
+
maxWidth: 280,
|
|
113
|
+
},
|
|
114
|
+
actionButton: {
|
|
115
|
+
flex: 1,
|
|
116
|
+
},
|
|
117
|
+
buttonContent: {
|
|
118
|
+
flexDirection: "row",
|
|
119
|
+
alignItems: "center",
|
|
120
|
+
gap: 8,
|
|
121
|
+
},
|
|
122
|
+
});
|