@umituz/react-native-ai-generation-content 1.17.108 → 1.17.110

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.108",
3
+ "version": "1.17.110",
4
4
  "description": "Provider-agnostic AI generation orchestration for React Native",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -0,0 +1,124 @@
1
+ /**
2
+ * CreationImagePreview Component
3
+ * Displays image preview with loading/placeholder states
4
+ */
5
+
6
+ import React, { useMemo, useState } from "react";
7
+ import { View, StyleSheet, Image, ImageErrorEventData, NativeSyntheticEvent } from "react-native";
8
+ import {
9
+ useAppDesignTokens,
10
+ AtomicIcon,
11
+ AtomicSpinner,
12
+ } from "@umituz/react-native-design-system";
13
+ import type { CreationStatus, CreationTypeId } from "../../domain/types";
14
+ import { isInProgress, getTypeIcon } from "../../domain/utils";
15
+
16
+ export interface CreationImagePreviewProps {
17
+ /** Preview image URL */
18
+ readonly uri?: string | null;
19
+ /** Creation status */
20
+ readonly status?: CreationStatus;
21
+ /** Creation type for placeholder icon */
22
+ readonly type?: CreationTypeId;
23
+ /** Aspect ratio (default: 16/9) */
24
+ readonly aspectRatio?: number;
25
+ /** Custom height (overrides aspectRatio) */
26
+ readonly height?: number;
27
+ /** Show loading indicator when in progress */
28
+ readonly showLoadingIndicator?: boolean;
29
+ }
30
+
31
+ export function CreationImagePreview({
32
+ uri,
33
+ status = "completed",
34
+ type = "text-to-image",
35
+ aspectRatio = 16 / 9,
36
+ height,
37
+ showLoadingIndicator = true,
38
+ }: CreationImagePreviewProps) {
39
+ const tokens = useAppDesignTokens();
40
+ const inProgress = isInProgress(status);
41
+ const typeIcon = getTypeIcon(type);
42
+ const [imageError, setImageError] = useState(false);
43
+
44
+ const hasPreview = !!uri && !inProgress && !imageError;
45
+
46
+ const handleImageError = (_error: NativeSyntheticEvent<ImageErrorEventData>) => {
47
+ setImageError(true);
48
+ };
49
+
50
+ const styles = useMemo(
51
+ () =>
52
+ StyleSheet.create({
53
+ container: {
54
+ width: "100%",
55
+ aspectRatio: height ? undefined : aspectRatio,
56
+ height: height,
57
+ backgroundColor: tokens.colors.backgroundSecondary,
58
+ position: "relative",
59
+ overflow: "hidden",
60
+ },
61
+ image: {
62
+ width: "100%",
63
+ height: "100%",
64
+ },
65
+ placeholder: {
66
+ width: "100%",
67
+ height: "100%",
68
+ justifyContent: "center",
69
+ alignItems: "center",
70
+ },
71
+ loadingContainer: {
72
+ width: "100%",
73
+ height: "100%",
74
+ justifyContent: "center",
75
+ alignItems: "center",
76
+ },
77
+ loadingIcon: {
78
+ width: 64,
79
+ height: 64,
80
+ borderRadius: 32,
81
+ backgroundColor: tokens.colors.primary + "20",
82
+ justifyContent: "center",
83
+ alignItems: "center",
84
+ },
85
+ }),
86
+ [tokens, aspectRatio, height]
87
+ );
88
+
89
+ // Show loading state
90
+ if (inProgress && showLoadingIndicator) {
91
+ return (
92
+ <View style={styles.container}>
93
+ <View style={styles.loadingContainer}>
94
+ <View style={styles.loadingIcon}>
95
+ <AtomicSpinner size="lg" color="primary" />
96
+ </View>
97
+ </View>
98
+ </View>
99
+ );
100
+ }
101
+
102
+ // Show image preview
103
+ if (hasPreview) {
104
+ return (
105
+ <View style={styles.container}>
106
+ <Image
107
+ source={{ uri }}
108
+ style={styles.image}
109
+ resizeMode="cover"
110
+ onError={handleImageError}
111
+ />
112
+ </View>
113
+ );
114
+ }
115
+
116
+ // Show placeholder
117
+ return (
118
+ <View style={styles.container}>
119
+ <View style={styles.placeholder}>
120
+ <AtomicIcon name={typeIcon} color="secondary" size="xl" />
121
+ </View>
122
+ </View>
123
+ );
124
+ }
@@ -1,26 +1,31 @@
1
1
  /**
2
2
  * CreationPreview Component
3
- * Displays creation preview image with loading/placeholder states
3
+ * Smart wrapper that delegates to CreationImagePreview or CreationVideoPreview
4
+ * based on creation type
4
5
  */
5
6
 
6
- import React, { useMemo } from "react";
7
- import { View, StyleSheet, Image } from "react-native";
8
- import {
9
- useAppDesignTokens,
10
- AtomicIcon,
11
- AtomicSpinner,
12
- } from "@umituz/react-native-design-system";
7
+ import React from "react";
13
8
  import type { CreationStatus, CreationTypeId } from "../../domain/types";
14
- import { isInProgress } from "../../domain/utils";
15
- import { getTypeIcon } from "../../domain/utils";
9
+ import { CreationImagePreview } from "./CreationImagePreview";
10
+ import { CreationVideoPreview } from "./CreationVideoPreview";
11
+
12
+ /** Video creation types */
13
+ const VIDEO_TYPES: CreationTypeId[] = ["text-to-video", "image-to-video"];
14
+
15
+ /** Check if creation type is a video type */
16
+ function isVideoType(type?: CreationTypeId | string): boolean {
17
+ return VIDEO_TYPES.includes(type as CreationTypeId);
18
+ }
16
19
 
17
20
  interface CreationPreviewProps {
18
- /** Preview image URL */
21
+ /** Preview image/thumbnail URL */
19
22
  readonly uri?: string | null;
23
+ /** Thumbnail URL for videos (optional, if different from uri) */
24
+ readonly thumbnailUrl?: string | null;
20
25
  /** Creation status */
21
26
  readonly status?: CreationStatus;
22
- /** Creation type for placeholder icon */
23
- readonly type?: CreationTypeId;
27
+ /** Creation type for determining preview type */
28
+ readonly type?: CreationTypeId | string;
24
29
  /** Aspect ratio (default: 16/9) */
25
30
  readonly aspectRatio?: number;
26
31
  /** Custom height (overrides aspectRatio) */
@@ -31,88 +36,37 @@ interface CreationPreviewProps {
31
36
 
32
37
  export function CreationPreview({
33
38
  uri,
39
+ thumbnailUrl,
34
40
  status = "completed",
35
41
  type = "text-to-image",
36
42
  aspectRatio = 16 / 9,
37
43
  height,
38
44
  showLoadingIndicator = true,
39
45
  }: CreationPreviewProps) {
40
- const tokens = useAppDesignTokens();
41
- const inProgress = isInProgress(status);
42
- const typeIcon = getTypeIcon(type);
43
- const hasPreview = !!uri && !inProgress;
44
-
45
- const styles = useMemo(
46
- () =>
47
- StyleSheet.create({
48
- container: {
49
- width: "100%",
50
- aspectRatio: height ? undefined : aspectRatio,
51
- height: height,
52
- backgroundColor: tokens.colors.backgroundSecondary,
53
- position: "relative",
54
- overflow: "hidden",
55
- },
56
- image: {
57
- width: "100%",
58
- height: "100%",
59
- },
60
- placeholder: {
61
- width: "100%",
62
- height: "100%",
63
- justifyContent: "center",
64
- alignItems: "center",
65
- },
66
- loadingContainer: {
67
- width: "100%",
68
- height: "100%",
69
- justifyContent: "center",
70
- alignItems: "center",
71
- },
72
- loadingIcon: {
73
- width: 64,
74
- height: 64,
75
- borderRadius: 32,
76
- backgroundColor: tokens.colors.primary + "20",
77
- justifyContent: "center",
78
- alignItems: "center",
79
- },
80
- }),
81
- [tokens, aspectRatio, height]
82
- );
83
-
84
- // Show loading state
85
- if (inProgress && showLoadingIndicator) {
86
- return (
87
- <View style={styles.container}>
88
- <View style={styles.loadingContainer}>
89
- <View style={styles.loadingIcon}>
90
- <AtomicSpinner size="lg" color="primary" />
91
- </View>
92
- </View>
93
- </View>
94
- );
95
- }
96
-
97
- // Show image preview
98
- if (hasPreview) {
46
+ // For video types, use CreationVideoPreview
47
+ if (isVideoType(type)) {
99
48
  return (
100
- <View style={styles.container}>
101
- <Image
102
- source={{ uri }}
103
- style={styles.image}
104
- resizeMode="cover"
105
- />
106
- </View>
49
+ <CreationVideoPreview
50
+ thumbnailUrl={thumbnailUrl || uri}
51
+ videoUrl={uri}
52
+ status={status}
53
+ type={type as CreationTypeId}
54
+ aspectRatio={aspectRatio}
55
+ height={height}
56
+ showLoadingIndicator={showLoadingIndicator}
57
+ />
107
58
  );
108
59
  }
109
60
 
110
- // Show placeholder
61
+ // For image types, use CreationImagePreview
111
62
  return (
112
- <View style={styles.container}>
113
- <View style={styles.placeholder}>
114
- <AtomicIcon name={typeIcon} color="secondary" size="xl" />
115
- </View>
116
- </View>
63
+ <CreationImagePreview
64
+ uri={uri}
65
+ status={status}
66
+ type={type as CreationTypeId}
67
+ aspectRatio={aspectRatio}
68
+ height={height}
69
+ showLoadingIndicator={showLoadingIndicator}
70
+ />
117
71
  );
118
72
  }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * CreationVideoPreview Component
3
+ * Displays video preview with thumbnail and play icon overlay
4
+ */
5
+
6
+ import React, { useMemo } from "react";
7
+ import { View, StyleSheet, Image } from "react-native";
8
+ import {
9
+ useAppDesignTokens,
10
+ AtomicIcon,
11
+ AtomicSpinner,
12
+ } from "@umituz/react-native-design-system";
13
+ import type { CreationStatus, CreationTypeId } from "../../domain/types";
14
+ import { isInProgress } from "../../domain/utils";
15
+
16
+ export interface CreationVideoPreviewProps {
17
+ /** Thumbnail image URL (optional) */
18
+ readonly thumbnailUrl?: string | null;
19
+ /** Video URL (for display purposes only) */
20
+ readonly videoUrl?: string | null;
21
+ /** Creation status */
22
+ readonly status?: CreationStatus;
23
+ /** Creation type for placeholder icon */
24
+ readonly type?: CreationTypeId;
25
+ /** Aspect ratio (default: 16/9) */
26
+ readonly aspectRatio?: number;
27
+ /** Custom height (overrides aspectRatio) */
28
+ readonly height?: number;
29
+ /** Show loading indicator when in progress */
30
+ readonly showLoadingIndicator?: boolean;
31
+ }
32
+
33
+ /** Check if URL is a video URL (mp4, mov, etc.) */
34
+ function isVideoUrl(url?: string | null): boolean {
35
+ if (!url) return false;
36
+ const videoExtensions = [".mp4", ".mov", ".avi", ".webm", ".mkv"];
37
+ const lowerUrl = url.toLowerCase();
38
+ return videoExtensions.some((ext) => lowerUrl.includes(ext));
39
+ }
40
+
41
+ export function CreationVideoPreview({
42
+ thumbnailUrl,
43
+ videoUrl,
44
+ status = "completed",
45
+ aspectRatio = 16 / 9,
46
+ height,
47
+ showLoadingIndicator = true,
48
+ }: CreationVideoPreviewProps) {
49
+ const tokens = useAppDesignTokens();
50
+ const inProgress = isInProgress(status);
51
+
52
+ // Only use thumbnail if it's a real image URL, not a video URL
53
+ const hasThumbnail = !!thumbnailUrl && !inProgress && !isVideoUrl(thumbnailUrl);
54
+
55
+ const styles = useMemo(
56
+ () =>
57
+ StyleSheet.create({
58
+ container: {
59
+ width: "100%",
60
+ aspectRatio: height ? undefined : aspectRatio,
61
+ height: height,
62
+ backgroundColor: tokens.colors.backgroundSecondary,
63
+ position: "relative",
64
+ overflow: "hidden",
65
+ },
66
+ thumbnail: {
67
+ width: "100%",
68
+ height: "100%",
69
+ },
70
+ placeholder: {
71
+ width: "100%",
72
+ height: "100%",
73
+ justifyContent: "center",
74
+ alignItems: "center",
75
+ },
76
+ loadingContainer: {
77
+ width: "100%",
78
+ height: "100%",
79
+ justifyContent: "center",
80
+ alignItems: "center",
81
+ },
82
+ loadingIcon: {
83
+ width: 64,
84
+ height: 64,
85
+ borderRadius: 32,
86
+ backgroundColor: tokens.colors.primary + "20",
87
+ justifyContent: "center",
88
+ alignItems: "center",
89
+ },
90
+ playIconOverlay: {
91
+ ...StyleSheet.absoluteFillObject,
92
+ justifyContent: "center",
93
+ alignItems: "center",
94
+ },
95
+ playIconContainer: {
96
+ width: 56,
97
+ height: 56,
98
+ borderRadius: 28,
99
+ backgroundColor: tokens.colors.primary,
100
+ justifyContent: "center",
101
+ alignItems: "center",
102
+ paddingLeft: 4,
103
+ },
104
+ }),
105
+ [tokens, aspectRatio, height]
106
+ );
107
+
108
+ // Show loading state
109
+ if (inProgress && showLoadingIndicator) {
110
+ return (
111
+ <View style={styles.container}>
112
+ <View style={styles.loadingContainer}>
113
+ <View style={styles.loadingIcon}>
114
+ <AtomicSpinner size="lg" color="primary" />
115
+ </View>
116
+ </View>
117
+ </View>
118
+ );
119
+ }
120
+
121
+ // Show thumbnail with play icon overlay
122
+ if (hasThumbnail) {
123
+ return (
124
+ <View style={styles.container}>
125
+ <Image
126
+ source={{ uri: thumbnailUrl }}
127
+ style={styles.thumbnail}
128
+ resizeMode="cover"
129
+ />
130
+ <View style={styles.playIconOverlay}>
131
+ <View style={styles.playIconContainer}>
132
+ <AtomicIcon name="play" customSize={24} color="onPrimary" />
133
+ </View>
134
+ </View>
135
+ </View>
136
+ );
137
+ }
138
+
139
+ // Show placeholder with play icon (no thumbnail available)
140
+ return (
141
+ <View style={styles.container}>
142
+ <View style={styles.placeholder}>
143
+ <View style={styles.playIconContainer}>
144
+ <AtomicIcon name="play" customSize={24} color="onPrimary" />
145
+ </View>
146
+ </View>
147
+ </View>
148
+ );
149
+ }
@@ -4,6 +4,8 @@
4
4
 
5
5
  // Core Components
6
6
  export { CreationPreview } from "./CreationPreview";
7
+ export { CreationImagePreview, type CreationImagePreviewProps } from "./CreationImagePreview";
8
+ export { CreationVideoPreview, type CreationVideoPreviewProps } from "./CreationVideoPreview";
7
9
  export { CreationBadges } from "./CreationBadges";
8
10
  export { CreationActions, type CreationAction } from "./CreationActions";
9
11
  export {
package/src/index.ts CHANGED
@@ -306,7 +306,6 @@ export {
306
306
  ImagePickerBox,
307
307
  DualImagePicker,
308
308
  // New Generic Sections
309
- AIGenerationProgressInline,
310
309
  PromptInput,
311
310
  AIGenerationHero,
312
311
  ExamplePrompts,
@@ -361,7 +360,6 @@ export type {
361
360
  // Image Picker
362
361
  ImagePickerBoxProps,
363
362
  DualImagePickerProps,
364
- AIGenerationProgressInlineProps,
365
363
  PromptInputProps,
366
364
  AIGenerationHeroProps,
367
365
  ExamplePromptsProps,
@@ -13,8 +13,8 @@ import { AspectRatioSelector } from "./selectors/AspectRatioSelector";
13
13
  import { PromptInput } from "./PromptInput";
14
14
  import { GenerateButton } from "./buttons/GenerateButton";
15
15
  import { ExamplePrompts } from "./prompts/ExamplePrompts";
16
- import { AIGenerationProgressInline } from "./AIGenerationProgressInline";
17
16
  import { StylePresetsGrid } from "./StylePresetsGrid";
17
+ import { GenerationProgressModal } from "./GenerationProgressModal";
18
18
  import type { AIGenerationFormProps } from "./AIGenerationForm.types";
19
19
 
20
20
  export const AIGenerationForm: React.FC<AIGenerationFormProps> = ({
@@ -36,6 +36,8 @@ export const AIGenerationForm: React.FC<AIGenerationFormProps> = ({
36
36
  isGenerating,
37
37
  hideGenerateButton,
38
38
  progress,
39
+ progressIcon,
40
+ onCloseProgressModal,
39
41
  generateButtonProps,
40
42
  showAdvanced,
41
43
  onAdvancedToggle,
@@ -155,13 +157,15 @@ export const AIGenerationForm: React.FC<AIGenerationFormProps> = ({
155
157
  </>
156
158
  )}
157
159
 
158
- {isGenerating && progress !== undefined && (
159
- <AIGenerationProgressInline
160
- progress={progress}
161
- title={translations.progressTitle || translations.generatingButton}
162
- hint={translations.progressHint}
163
- />
164
- )}
160
+ {/* MANDATORY: Progress Modal shows automatically when isGenerating */}
161
+ <GenerationProgressModal
162
+ visible={isGenerating}
163
+ progress={progress ?? 0}
164
+ icon={progressIcon || "sparkles-outline"}
165
+ title={translations.progressTitle || translations.generatingButton}
166
+ message={translations.progressMessage || translations.progressHint}
167
+ onClose={onCloseProgressModal}
168
+ />
165
169
  </>
166
170
  );
167
171
  };
@@ -14,7 +14,9 @@ export interface AIGenerationFormTranslations {
14
14
  examplePromptsTitle?: string;
15
15
  generateButton: string;
16
16
  generatingButton: string;
17
+ // Progress Modal (mandatory when isGenerating)
17
18
  progressTitle?: string;
19
+ progressMessage?: string;
18
20
  progressHint?: string;
19
21
  presetsTitle?: string;
20
22
  showAdvancedLabel?: string;
@@ -54,7 +56,10 @@ export interface AIGenerationFormProps extends PropsWithChildren {
54
56
 
55
57
  // Optional: Generation Progress
56
58
  progress?: number;
57
-
59
+ progressIcon?: string;
60
+ /** Callback when user closes the progress modal (for background generation) */
61
+ onCloseProgressModal?: () => void;
62
+
58
63
  // Custom Generate Button Props
59
64
  generateButtonProps?: {
60
65
  costLabel?: string;
@@ -21,6 +21,10 @@ export interface GenerationProgressContentProps {
21
21
  readonly hint?: string;
22
22
  readonly dismissLabel?: string;
23
23
  readonly onDismiss?: () => void;
24
+ /** Close button in top-right corner for background generation */
25
+ readonly onClose?: () => void;
26
+ /** Close button label (default: "Continue in background") */
27
+ readonly closeLabel?: string;
24
28
  readonly backgroundColor?: string;
25
29
  readonly textColor?: string;
26
30
  readonly hintColor?: string;
@@ -39,6 +43,8 @@ export const GenerationProgressContent: React.FC<
39
43
  hint,
40
44
  dismissLabel,
41
45
  onDismiss,
46
+ onClose,
47
+ closeLabel,
42
48
  backgroundColor,
43
49
  textColor,
44
50
  hintColor,
@@ -62,6 +68,17 @@ export const GenerationProgressContent: React.FC<
62
68
  },
63
69
  ]}
64
70
  >
71
+ {/* Close button in top-right corner */}
72
+ {onClose && (
73
+ <TouchableOpacity
74
+ style={styles.closeButton}
75
+ onPress={onClose}
76
+ hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
77
+ >
78
+ <AtomicIcon name="close" size="md" color="secondary" />
79
+ </TouchableOpacity>
80
+ )}
81
+
65
82
  {icon && (
66
83
  <View style={styles.iconContainer}>
67
84
  <AtomicIcon name={icon} size="xl" color="primary" />
@@ -130,6 +147,18 @@ const styles = StyleSheet.create({
130
147
  padding: 32,
131
148
  borderWidth: 1,
132
149
  alignItems: "center",
150
+ position: "relative",
151
+ },
152
+ closeButton: {
153
+ position: "absolute",
154
+ top: 16,
155
+ right: 16,
156
+ width: 32,
157
+ height: 32,
158
+ borderRadius: 16,
159
+ justifyContent: "center",
160
+ alignItems: "center",
161
+ zIndex: 1,
133
162
  },
134
163
  iconContainer: {
135
164
  marginBottom: 20,
@@ -22,6 +22,7 @@ export interface GenerationProgressRenderProps {
22
22
  readonly message?: string;
23
23
  readonly hint?: string;
24
24
  readonly onDismiss?: () => void;
25
+ readonly onClose?: () => void;
25
26
  }
26
27
 
27
28
  export interface GenerationProgressModalProps
@@ -44,6 +45,8 @@ export const GenerationProgressModal: React.FC<
44
45
  hint,
45
46
  dismissLabel,
46
47
  onDismiss,
48
+ onClose,
49
+ closeLabel,
47
50
  modalBackgroundColor,
48
51
  textColor,
49
52
  hintColor,
@@ -67,6 +70,7 @@ export const GenerationProgressModal: React.FC<
67
70
  message,
68
71
  hint,
69
72
  onDismiss,
73
+ onClose,
70
74
  })
71
75
  ) : (
72
76
  <GenerationProgressContent
@@ -77,6 +81,8 @@ export const GenerationProgressModal: React.FC<
77
81
  hint={hint}
78
82
  dismissLabel={dismissLabel}
79
83
  onDismiss={onDismiss}
84
+ onClose={onClose}
85
+ closeLabel={closeLabel}
80
86
  backgroundColor={modalBackgroundColor || tokens.colors.surface}
81
87
  textColor={textColor || tokens.colors.textPrimary}
82
88
  hintColor={hintColor || tokens.colors.textTertiary}
@@ -4,7 +4,6 @@ export { GenerationProgressBar } from "./GenerationProgressBar";
4
4
  export { PendingJobCard } from "./PendingJobCard";
5
5
  export { PendingJobProgressBar } from "./PendingJobProgressBar";
6
6
  export { PendingJobCardActions } from "./PendingJobCardActions";
7
- export { AIGenerationProgressInline } from "./AIGenerationProgressInline";
8
7
  export { PromptInput } from "./PromptInput";
9
8
  export { AIGenerationHero } from "./AIGenerationHero";
10
9
  export * from "./StylePresetsGrid";
@@ -26,7 +25,6 @@ export type {
26
25
 
27
26
  export type { PendingJobProgressBarProps } from "./PendingJobProgressBar";
28
27
  export type { PendingJobCardActionsProps } from "./PendingJobCardActions";
29
- export type { AIGenerationProgressInlineProps } from "./AIGenerationProgressInline";
30
28
  export type { PromptInputProps } from "./PromptInput";
31
29
  export type { AIGenerationHeroProps } from "./AIGenerationHero";
32
30
 
@@ -1,106 +0,0 @@
1
- /**
2
- * AIGenerationProgressInline Component
3
- * Generic inline generation progress display
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
- useAppDesignTokens,
12
- AtomicProgress,
13
- AtomicSpinner,
14
- } from "@umituz/react-native-design-system";
15
-
16
- export interface AIGenerationProgressInlineProps {
17
- readonly progress: number;
18
- readonly title?: string;
19
- readonly message?: string;
20
- readonly hint?: string;
21
- readonly backgroundColor?: string;
22
- readonly accentColor?: string;
23
- }
24
-
25
- export const AIGenerationProgressInline: React.FC<
26
- AIGenerationProgressInlineProps
27
- > = ({
28
- progress,
29
- title,
30
- message,
31
- hint,
32
- backgroundColor,
33
- accentColor,
34
- }) => {
35
- const tokens = useAppDesignTokens();
36
- const primaryColor = accentColor || tokens.colors.primary;
37
- const bgColor = backgroundColor || tokens.colors.surface;
38
-
39
- const clampedProgress = Math.max(0, Math.min(100, progress));
40
-
41
- return (
42
- <View style={[styles.container, { backgroundColor: bgColor }]}>
43
- <AtomicSpinner size="lg" color={primaryColor} />
44
-
45
- {title && (
46
- <AtomicText
47
- type="bodyMedium"
48
- style={[styles.title, { color: tokens.colors.textPrimary }]}
49
- >
50
- {title}
51
- </AtomicText>
52
- )}
53
-
54
- {message && (
55
- <AtomicText
56
- type="bodySmall"
57
- style={[styles.message, { color: tokens.colors.textSecondary }]}
58
- >
59
- {message}
60
- </AtomicText>
61
- )}
62
-
63
- <View style={styles.progressBarWrapper}>
64
- <AtomicProgress
65
- value={clampedProgress}
66
- height={8}
67
- color={primaryColor}
68
- backgroundColor={tokens.colors.surfaceVariant}
69
- shape="rounded"
70
- showPercentage={false}
71
- />
72
- </View>
73
-
74
- <AtomicText
75
- type="labelSmall"
76
- style={[styles.progressLabel, { color: tokens.colors.textSecondary }]}
77
- >
78
- {clampedProgress}%{hint ? ` • ${hint}` : ""}
79
- </AtomicText>
80
- </View>
81
- );
82
- };
83
-
84
- const styles = StyleSheet.create({
85
- container: {
86
- margin: 16,
87
- padding: 24,
88
- borderRadius: 16,
89
- alignItems: "center",
90
- },
91
- title: {
92
- marginTop: 16,
93
- fontWeight: "600",
94
- },
95
- message: {
96
- marginTop: 8,
97
- textAlign: "center",
98
- },
99
- progressBarWrapper: {
100
- width: "100%",
101
- marginTop: 16,
102
- },
103
- progressLabel: {
104
- marginTop: 8,
105
- },
106
- });