@umituz/react-native-ai-generation-content 1.83.89 → 1.83.91

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.83.89",
3
+ "version": "1.83.91",
4
4
  "description": "Provider-agnostic AI generation orchestration for React Native with result preview components",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -0,0 +1,72 @@
1
+ /**
2
+ * CreationCardCompact Component
3
+ * Compact 2-column grid card — image fills the card, badge overlay only
4
+ */
5
+
6
+ import React, { useMemo, useCallback } from "react";
7
+ import { View, StyleSheet, TouchableOpacity } from "react-native";
8
+ import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
9
+ import { CreationPreview } from "./CreationPreview";
10
+ import { CreationBadges } from "./CreationBadges";
11
+ import { getPreviewUrl } from "../../domain/utils";
12
+ import type { CreationCardData, CreationCardCallbacks } from "./CreationCard.types";
13
+ import type { CreationTypeId } from "../../domain/types";
14
+
15
+ interface CreationCardCompactProps {
16
+ readonly creation: CreationCardData;
17
+ readonly callbacks?: CreationCardCallbacks;
18
+ }
19
+
20
+ export function CreationCardCompact({ creation, callbacks }: CreationCardCompactProps) {
21
+ const tokens = useAppDesignTokens();
22
+ const previewUrl = getPreviewUrl(creation.output) || creation.uri;
23
+
24
+ const handlePress = useCallback(() => {
25
+ callbacks?.onPress?.(creation);
26
+ }, [callbacks, creation]);
27
+
28
+ const styles = useMemo(
29
+ () =>
30
+ StyleSheet.create({
31
+ card: {
32
+ flex: 1,
33
+ borderRadius: 12,
34
+ overflow: "hidden",
35
+ aspectRatio: 3 / 4,
36
+ backgroundColor: tokens.colors.surface,
37
+ },
38
+ previewWrapper: {
39
+ width: "100%",
40
+ height: "100%",
41
+ },
42
+ }),
43
+ [tokens],
44
+ );
45
+
46
+ return (
47
+ <TouchableOpacity
48
+ style={styles.card}
49
+ onPress={handlePress}
50
+ activeOpacity={callbacks?.onPress ? 0.85 : 1}
51
+ disabled={!callbacks?.onPress}
52
+ >
53
+ <View style={styles.previewWrapper}>
54
+ <CreationPreview
55
+ uri={previewUrl}
56
+ thumbnailUrl={creation.output?.thumbnailUrl}
57
+ status={creation.status}
58
+ type={creation.type as CreationTypeId}
59
+ aspectRatio={3 / 4}
60
+ showLoadingIndicator
61
+ />
62
+ </View>
63
+ {creation.status && (
64
+ <CreationBadges
65
+ status={creation.status}
66
+ type={creation.type as CreationTypeId}
67
+ showType={false}
68
+ />
69
+ )}
70
+ </TouchableOpacity>
71
+ );
72
+ }
@@ -4,8 +4,8 @@
4
4
  * Uses expo-image for caching and performance
5
5
  */
6
6
 
7
- import React, { useMemo, useState, useCallback } from "react";
8
- import { View, StyleSheet } from "react-native";
7
+ import React, { useMemo, useState, useCallback, useRef, useEffect } from "react";
8
+ import { View, StyleSheet, Animated } from "react-native";
9
9
  import { AtomicIcon, AtomicSpinner } from "@umituz/react-native-design-system/atoms";
10
10
  import { AtomicImage } from "@umituz/react-native-design-system/image";
11
11
  import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
@@ -40,6 +40,19 @@ export function CreationImagePreview({
40
40
  const typeIcon = getTypeIcon(type);
41
41
  const [imageError, setImageError] = useState(false);
42
42
  const [isLoading, setIsLoading] = useState(true);
43
+ const pulseAnim = useRef(new Animated.Value(0.4)).current;
44
+
45
+ useEffect(() => {
46
+ if (!inProgress) return;
47
+ const animation = Animated.loop(
48
+ Animated.sequence([
49
+ Animated.timing(pulseAnim, { toValue: 1, duration: 1100, useNativeDriver: true }),
50
+ Animated.timing(pulseAnim, { toValue: 0.4, duration: 1100, useNativeDriver: true }),
51
+ ]),
52
+ );
53
+ animation.start();
54
+ return () => animation.stop();
55
+ }, [inProgress, pulseAnim]);
43
56
 
44
57
  const hasPreview = !!uri && !inProgress && !imageError;
45
58
 
@@ -91,15 +104,15 @@ export function CreationImagePreview({
91
104
  [tokens, aspectRatio, height]
92
105
  );
93
106
 
94
- // Show loading state during generation
107
+ // Show pulsing shimmer during generation
95
108
  if (inProgress && showLoadingIndicator) {
96
109
  return (
97
110
  <View style={styles.container}>
98
- <View style={styles.loadingOverlay}>
111
+ <Animated.View style={[styles.loadingOverlay, { opacity: pulseAnim, backgroundColor: tokens.colors.backgroundSecondary }]}>
99
112
  <View style={styles.loadingIcon}>
100
113
  <AtomicSpinner size="lg" color="primary" />
101
114
  </View>
102
- </View>
115
+ </Animated.View>
103
116
  </View>
104
117
  );
105
118
  }
@@ -19,6 +19,8 @@ interface GalleryHeaderProps {
19
19
  readonly filterButtons?: FilterButtonConfig[];
20
20
  readonly showFilter?: boolean;
21
21
  readonly style?: ViewStyle;
22
+ readonly viewMode?: "list" | "grid";
23
+ readonly onViewModeChange?: (mode: "list" | "grid") => void;
22
24
  }
23
25
 
24
26
  const EMPTY_FILTER_BUTTONS: FilterButtonConfig[] = [];
@@ -29,13 +31,15 @@ export const GalleryHeader: React.FC<GalleryHeaderProps> = ({
29
31
  filterButtons = EMPTY_FILTER_BUTTONS,
30
32
  showFilter = true,
31
33
  style,
34
+ viewMode = "list",
35
+ onViewModeChange,
32
36
  }: GalleryHeaderProps) => {
33
37
  const tokens = useAppDesignTokens();
34
38
  const styles = useStyles(tokens);
35
39
 
36
40
  return (
37
41
  <View style={[styles.headerArea, style]}>
38
- <View>
42
+ <View style={styles.leftSection}>
39
43
  {title ? (
40
44
  <View style={styles.titleRow}>
41
45
  <AtomicText style={styles.title}>{title}</AtomicText>
@@ -45,35 +49,64 @@ export const GalleryHeader: React.FC<GalleryHeaderProps> = ({
45
49
  {countLabel}
46
50
  </AtomicText>
47
51
  </View>
48
- {showFilter && filterButtons.length > 0 && (
49
- <View style={styles.filterRow}>
50
- {filterButtons.map((btn: FilterButtonConfig) => (
52
+
53
+ <View style={styles.rightSection}>
54
+ {showFilter && filterButtons.length > 0 && (
55
+ <View style={styles.filterRow}>
56
+ {filterButtons.map((btn: FilterButtonConfig) => (
57
+ <TouchableOpacity
58
+ key={btn.id}
59
+ onPress={() => {
60
+ if (__DEV__) {
61
+ console.log(`[GalleryHeader] ${btn.id} filter pressed`);
62
+ }
63
+ btn.onPress();
64
+ }}
65
+ style={[styles.filterButton, btn.isActive && styles.filterButtonActive]}
66
+ activeOpacity={0.7}
67
+ >
68
+ <AtomicIcon
69
+ name={btn.icon}
70
+ size="sm"
71
+ color={btn.isActive ? "primary" : "secondary"}
72
+ />
73
+ <AtomicText
74
+ style={[styles.filterText, { color: btn.isActive ? tokens.colors.primary : tokens.colors.textSecondary }]}
75
+ >
76
+ {btn.label}
77
+ </AtomicText>
78
+ </TouchableOpacity>
79
+ ))}
80
+ </View>
81
+ )}
82
+
83
+ {onViewModeChange && (
84
+ <View style={styles.viewToggle}>
51
85
  <TouchableOpacity
52
- key={btn.id}
53
- onPress={() => {
54
- if (__DEV__) {
55
-
56
- console.log(`[GalleryHeader] ${btn.id} filter pressed`);
57
- }
58
- btn.onPress();
59
- }}
60
- style={[styles.filterButton, btn.isActive && styles.filterButtonActive]}
86
+ onPress={() => onViewModeChange("list")}
87
+ style={[styles.toggleBtn, viewMode === "list" && styles.toggleBtnActive]}
61
88
  activeOpacity={0.7}
62
89
  >
63
90
  <AtomicIcon
64
- name={btn.icon}
91
+ name="list-outline"
65
92
  size="sm"
66
- color={btn.isActive ? "primary" : "secondary"}
93
+ color={viewMode === "list" ? "primary" : "secondary"}
67
94
  />
68
- <AtomicText
69
- style={[styles.filterText, { color: btn.isActive ? tokens.colors.primary : tokens.colors.textSecondary }]}
70
- >
71
- {btn.label}
72
- </AtomicText>
73
95
  </TouchableOpacity>
74
- ))}
75
- </View>
76
- )}
96
+ <TouchableOpacity
97
+ onPress={() => onViewModeChange("grid")}
98
+ style={[styles.toggleBtn, viewMode === "grid" && styles.toggleBtnActive]}
99
+ activeOpacity={0.7}
100
+ >
101
+ <AtomicIcon
102
+ name="grid-outline"
103
+ size="sm"
104
+ color={viewMode === "grid" ? "primary" : "secondary"}
105
+ />
106
+ </TouchableOpacity>
107
+ </View>
108
+ )}
109
+ </View>
77
110
  </View>
78
111
  );
79
112
  };
@@ -88,6 +121,14 @@ const useStyles = (tokens: DesignTokens) =>
88
121
  paddingVertical: tokens.spacing.sm,
89
122
  marginBottom: tokens.spacing.sm,
90
123
  },
124
+ leftSection: {
125
+ flex: 1,
126
+ },
127
+ rightSection: {
128
+ flexDirection: "row",
129
+ alignItems: "center",
130
+ gap: tokens.spacing.sm,
131
+ },
91
132
  titleRow: {
92
133
  flexDirection: "row",
93
134
  alignItems: "center",
@@ -127,4 +168,21 @@ const useStyles = (tokens: DesignTokens) =>
127
168
  fontSize: 13,
128
169
  fontWeight: "500",
129
170
  },
171
+ viewToggle: {
172
+ flexDirection: "row",
173
+ gap: 2,
174
+ backgroundColor: tokens.colors.surfaceVariant,
175
+ borderRadius: 8,
176
+ padding: 2,
177
+ },
178
+ toggleBtn: {
179
+ width: 32,
180
+ height: 32,
181
+ borderRadius: 6,
182
+ justifyContent: "center",
183
+ alignItems: "center",
184
+ },
185
+ toggleBtnActive: {
186
+ backgroundColor: tokens.colors.surface,
187
+ },
130
188
  });
@@ -7,6 +7,8 @@ export {
7
7
  CreationCard,
8
8
  } from "./CreationCard";
9
9
 
10
+ export { CreationCardCompact } from "./CreationCardCompact";
11
+
10
12
  // Gallery Components
11
13
  export { GalleryHeader } from "./GalleryHeader";
12
14
  export { GalleryEmptyStates } from "./GalleryEmptyStates";
@@ -1,4 +1,4 @@
1
- import React, { useMemo, useCallback } from "react";
1
+ import React, { useMemo, useCallback, useState } from "react";
2
2
  import { View } from "react-native";
3
3
  import { ScreenLayout } from "@umituz/react-native-design-system/layouts";
4
4
  import { FilterSheet, useAppFocusEffect } from "@umituz/react-native-design-system/molecules";
@@ -9,7 +9,7 @@ import { useProcessingJobsPoller } from "../hooks/useProcessingJobsPoller";
9
9
  import { useGalleryFilters } from "../hooks/useGalleryFilters";
10
10
  import { useGalleryCallbacks } from "../hooks/useGalleryCallbacks";
11
11
  import { useGalleryState } from "../hooks/useGalleryState";
12
- import { GalleryHeader, CreationCard, GalleryEmptyStates } from "../components";
12
+ import { GalleryHeader, CreationCard, CreationCardCompact, GalleryEmptyStates } from "../components";
13
13
  import { GalleryResultPreview } from "../components/GalleryResultPreview";
14
14
  import { GalleryScreenHeader } from "../components/GalleryScreenHeader";
15
15
  import { MEDIA_FILTER_OPTIONS, STATUS_FILTER_OPTIONS } from "../../domain/types/creation-filter";
@@ -34,6 +34,7 @@ export function CreationsGalleryScreen({
34
34
  onCreationPress,
35
35
  }: CreationsGalleryScreenProps) {
36
36
  const tokens = useAppDesignTokens();
37
+ const [viewMode, setViewMode] = useState<"list" | "grid">("list");
37
38
 
38
39
  const { data: creations, isLoading, refetch } = useCreations({ userId, repository });
39
40
  const deleteMutation = useDeleteCreation({ userId, repository });
@@ -89,18 +90,43 @@ export function CreationsGalleryScreen({
89
90
  [config.types, t, getCreationTitle]
90
91
  );
91
92
 
93
+ const getItemCallbacks = useCallback((item: Creation) => ({
94
+ onPress: () => onCreationPress ? onCreationPress(item) : callbacks.handleCardPress(item),
95
+ onShare: async () => callbacks.handleShareCard(item),
96
+ onDelete: () => callbacks.handleDelete(item),
97
+ onFavorite: () => callbacks.handleFavorite(item),
98
+ }), [callbacks, onCreationPress]);
99
+
92
100
  const renderItem = useCallback(({ item }: { item: Creation }) => (
93
101
  <CreationCard
94
102
  creation={item}
95
103
  titleText={getItemTitle(item)}
96
- callbacks={{
97
- onPress: () => onCreationPress ? onCreationPress(item) : callbacks.handleCardPress(item),
98
- onShare: async () => callbacks.handleShareCard(item),
99
- onDelete: () => callbacks.handleDelete(item),
100
- onFavorite: () => callbacks.handleFavorite(item),
101
- }}
104
+ callbacks={getItemCallbacks(item)}
102
105
  />
103
- ), [callbacks, getItemTitle, onCreationPress]);
106
+ ), [getItemTitle, getItemCallbacks]);
107
+
108
+ const renderGridItems = useCallback((items: Creation[]) => {
109
+ const rows: Array<{ left: Creation; right: Creation | null }> = [];
110
+ for (let i = 0; i < items.length; i += 2) {
111
+ rows.push({ left: items[i], right: items[i + 1] ?? null });
112
+ }
113
+ return rows.map((row, index) => (
114
+ <View key={`grid-row-${index}`} style={styles.gridRow}>
115
+ <CreationCardCompact
116
+ creation={row.left}
117
+ callbacks={{ onPress: getItemCallbacks(row.left).onPress }}
118
+ />
119
+ {row.right ? (
120
+ <CreationCardCompact
121
+ creation={row.right}
122
+ callbacks={{ onPress: getItemCallbacks(row.right).onPress }}
123
+ />
124
+ ) : (
125
+ <View style={styles.gridPlaceholder} />
126
+ )}
127
+ </View>
128
+ ));
129
+ }, [getItemCallbacks]);
104
130
 
105
131
  const hasScreenHeader = Boolean(onBack);
106
132
 
@@ -115,10 +141,12 @@ export function CreationsGalleryScreen({
115
141
  countLabel={`${filters.filtered.length} ${t(config.translations.photoCount)}`}
116
142
  showFilter={showFilter}
117
143
  filterButtons={filterButtons}
144
+ viewMode={viewMode}
145
+ onViewModeChange={setViewMode}
118
146
  />
119
147
  </View>
120
148
  );
121
- }, [creations, isLoading, filters.filtered.length, showFilter, filterButtons, t, config, tokens, hasScreenHeader]);
149
+ }, [creations, isLoading, filters.filtered.length, showFilter, filterButtons, t, config, tokens, hasScreenHeader, viewMode]);
122
150
 
123
151
  const renderEmpty = useMemo(() => (
124
152
  <GalleryEmptyStates
@@ -165,6 +193,10 @@ export function CreationsGalleryScreen({
165
193
  <View style={[styles.listContent, styles.emptyContent]}>
166
194
  {renderEmpty}
167
195
  </View>
196
+ ) : viewMode === "grid" ? (
197
+ <View style={styles.gridContent}>
198
+ {renderGridItems(filters.filtered)}
199
+ </View>
168
200
  ) : (
169
201
  <View style={styles.listContent}>
170
202
  {filters.filtered.map((item) => (
@@ -7,7 +7,16 @@ import { StyleSheet } from "react-native";
7
7
  export const creationsGalleryStyles = StyleSheet.create({
8
8
  header: { borderBottomWidth: 1 },
9
9
  listContent: { paddingHorizontal: 16, paddingTop: 16 },
10
+ gridContent: { paddingHorizontal: 12, paddingTop: 12 },
10
11
  emptyContent: { flexGrow: 1 },
12
+ gridRow: {
13
+ flexDirection: "row",
14
+ gap: 8,
15
+ marginBottom: 8,
16
+ },
17
+ gridPlaceholder: {
18
+ flex: 1,
19
+ },
11
20
  screenHeader: {
12
21
  flexDirection: "row",
13
22
  alignItems: "center",
@@ -48,7 +48,6 @@ export function usePhotoBlockingGeneration(
48
48
  } = props;
49
49
 
50
50
  const creationIdRef = useRef<string | null>(null);
51
- const logSessionIdRef = useRef<string | undefined>(undefined);
52
51
 
53
52
  const handleSuccess = useCallback(
54
53
  async (result: unknown) => {
@@ -5,8 +5,8 @@
5
5
  * Supports background generation - user can dismiss and generation continues
6
6
  */
7
7
 
8
- import React, { useMemo } from "react";
9
- import { View, StyleSheet, ActivityIndicator, TouchableOpacity } from "react-native";
8
+ import React, { useMemo, useRef, useEffect } from "react";
9
+ import { View, StyleSheet, ActivityIndicator, TouchableOpacity, Animated } from "react-native";
10
10
  import { AtomicText } from "@umituz/react-native-design-system/atoms";
11
11
  import { ScreenLayout } from "@umituz/react-native-design-system/layouts";
12
12
  import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
@@ -30,6 +30,18 @@ interface GeneratingScreenProps {
30
30
  readonly onDismiss?: () => void;
31
31
  }
32
32
 
33
+ const PHASES = [
34
+ { id: "queued", index: 0 },
35
+ { id: "processing", index: 1 },
36
+ { id: "finalizing", index: 2 },
37
+ ] as const;
38
+
39
+ const PHASE_LABELS = {
40
+ queued: "Analyzing",
41
+ processing: "Creating",
42
+ finalizing: "Refining",
43
+ } as const;
44
+
33
45
  export const GeneratingScreen: React.FC<GeneratingScreenProps> = ({
34
46
  progress: _progress,
35
47
  scenario,
@@ -38,6 +50,18 @@ export const GeneratingScreen: React.FC<GeneratingScreenProps> = ({
38
50
  }) => {
39
51
  const tokens = useAppDesignTokens();
40
52
  const phase = useGenerationPhase();
53
+ const pulseAnim = useRef(new Animated.Value(0.6)).current;
54
+
55
+ useEffect(() => {
56
+ const animation = Animated.loop(
57
+ Animated.sequence([
58
+ Animated.timing(pulseAnim, { toValue: 1, duration: 900, useNativeDriver: true }),
59
+ Animated.timing(pulseAnim, { toValue: 0.6, duration: 900, useNativeDriver: true }),
60
+ ]),
61
+ );
62
+ animation.start();
63
+ return () => animation.stop();
64
+ }, [pulseAnim]);
41
65
 
42
66
  const messages = useMemo(() => {
43
67
  const custom = scenario?.generatingMessages;
@@ -52,26 +76,84 @@ export const GeneratingScreen: React.FC<GeneratingScreenProps> = ({
52
76
  const statusMessage = useMemo(() => {
53
77
  switch (phase) {
54
78
  case "queued":
55
- return t("generator.status.queued") || "Waiting in queue...";
79
+ return t("generator.status.queued") || "Analyzing your photos...";
56
80
  case "processing":
57
- return t("generator.status.processing") || "Generating your content...";
81
+ return t("generator.status.processing") || "Creating your scene...";
58
82
  case "finalizing":
59
- return t("generator.status.finalizing") || "Almost done...";
83
+ return t("generator.status.finalizing") || "Adding finishing touches...";
60
84
  default:
61
85
  return messages.waitMessage;
62
86
  }
63
87
  }, [phase, t, messages.waitMessage]);
64
88
 
89
+ const activePhaseIndex = PHASES.find((p) => p.id === phase)?.index ?? 0;
90
+
65
91
  return (
66
92
  <ScreenLayout backgroundColor={tokens.colors.backgroundPrimary}>
67
93
  <View style={styles.container}>
68
94
  <View style={styles.content}>
69
- <ActivityIndicator size="large" color={tokens.colors.primary} />
95
+ {/* Pulsing spinner */}
96
+ <Animated.View style={[styles.spinnerContainer, { opacity: pulseAnim, backgroundColor: tokens.colors.primary + "15" }]}>
97
+ <ActivityIndicator size="large" color={tokens.colors.primary} />
98
+ </Animated.View>
70
99
 
71
100
  <AtomicText type="headlineMedium" style={styles.title}>
72
101
  {messages.title}
73
102
  </AtomicText>
74
103
 
104
+ {/* Phase step bubbles */}
105
+ <View style={styles.stepsRow}>
106
+ {PHASES.map((p, idx) => {
107
+ const isActive = idx === activePhaseIndex;
108
+ const isDone = idx < activePhaseIndex;
109
+ return (
110
+ <React.Fragment key={p.id}>
111
+ <View style={styles.stepItem}>
112
+ <View
113
+ style={[
114
+ styles.stepBubble,
115
+ {
116
+ backgroundColor: isDone || isActive
117
+ ? tokens.colors.primary
118
+ : tokens.colors.surfaceVariant,
119
+ borderColor: isActive ? tokens.colors.primary : "transparent",
120
+ },
121
+ ]}
122
+ >
123
+ <AtomicText
124
+ style={[
125
+ styles.stepNumber,
126
+ { color: isDone || isActive ? "#FFFFFF" : tokens.colors.textSecondary },
127
+ ]}
128
+ >
129
+ {isDone ? "✓" : String(idx + 1)}
130
+ </AtomicText>
131
+ </View>
132
+ <AtomicText
133
+ style={[
134
+ styles.stepLabel,
135
+ {
136
+ color: isActive ? tokens.colors.primary : tokens.colors.textSecondary,
137
+ fontWeight: isActive ? "700" : "400",
138
+ },
139
+ ]}
140
+ >
141
+ {PHASE_LABELS[p.id]}
142
+ </AtomicText>
143
+ </View>
144
+ {idx < PHASES.length - 1 && (
145
+ <View
146
+ style={[
147
+ styles.stepConnector,
148
+ { backgroundColor: idx < activePhaseIndex ? tokens.colors.primary : tokens.colors.surfaceVariant },
149
+ ]}
150
+ />
151
+ )}
152
+ </React.Fragment>
153
+ );
154
+ })}
155
+ </View>
156
+
75
157
  <AtomicText type="bodyMedium" style={[styles.message, { color: tokens.colors.textSecondary }]}>
76
158
  {statusMessage}
77
159
  </AtomicText>
@@ -83,12 +165,6 @@ export const GeneratingScreen: React.FC<GeneratingScreenProps> = ({
83
165
  />
84
166
  </View>
85
167
 
86
- {scenario && (
87
- <AtomicText type="bodySmall" style={[styles.hint, { color: tokens.colors.textSecondary }]}>
88
- {scenario.title || scenario.id}
89
- </AtomicText>
90
- )}
91
-
92
168
  <AtomicText type="bodySmall" style={[styles.hint, { color: tokens.colors.textSecondary }]}>
93
169
  {messages.hint}
94
170
  </AtomicText>
@@ -116,25 +192,64 @@ const styles = StyleSheet.create({
116
192
  alignItems: "center",
117
193
  },
118
194
  content: {
119
- width: "80%",
195
+ width: "85%",
120
196
  maxWidth: 400,
121
197
  alignItems: "center",
122
198
  gap: 16,
123
199
  },
200
+ spinnerContainer: {
201
+ width: 80,
202
+ height: 80,
203
+ borderRadius: 40,
204
+ justifyContent: "center",
205
+ alignItems: "center",
206
+ marginBottom: 8,
207
+ },
124
208
  title: {
125
209
  textAlign: "center",
126
- marginTop: 24,
210
+ },
211
+ stepsRow: {
212
+ flexDirection: "row",
213
+ alignItems: "center",
214
+ justifyContent: "center",
215
+ marginVertical: 8,
216
+ },
217
+ stepItem: {
218
+ alignItems: "center",
219
+ gap: 6,
220
+ },
221
+ stepBubble: {
222
+ width: 32,
223
+ height: 32,
224
+ borderRadius: 16,
225
+ justifyContent: "center",
226
+ alignItems: "center",
227
+ borderWidth: 2,
228
+ },
229
+ stepNumber: {
230
+ fontSize: 12,
231
+ fontWeight: "700",
232
+ },
233
+ stepLabel: {
234
+ fontSize: 11,
235
+ },
236
+ stepConnector: {
237
+ width: 40,
238
+ height: 2,
239
+ borderRadius: 1,
240
+ marginBottom: 18,
241
+ marginHorizontal: 4,
127
242
  },
128
243
  message: {
129
244
  textAlign: "center",
130
245
  },
131
246
  progressContainer: {
132
247
  width: "100%",
133
- marginTop: 24,
248
+ marginTop: 8,
134
249
  },
135
250
  hint: {
136
251
  textAlign: "center",
137
- marginTop: 8,
252
+ marginTop: 4,
138
253
  },
139
254
  backgroundHintButton: {
140
255
  marginTop: 32,
@@ -6,7 +6,6 @@ let useVideoPlayer: (...args: any[]) => any = () => null;
6
6
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
7
  let VideoView: React.ComponentType<any> = () => null;
8
8
  try {
9
- // eslint-disable-next-line @typescript-eslint/no-require-imports
10
9
  const expoVideo = require("expo-video");
11
10
  useVideoPlayer = expoVideo.useVideoPlayer;
12
11
  VideoView = expoVideo.VideoView;