@umituz/react-native-onboarding 3.2.0 → 3.3.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.
- package/package.json +6 -2
- package/src/domain/entities/OnboardingOptions.ts +9 -0
- package/src/domain/entities/OnboardingSlide.ts +19 -1
- package/src/domain/entities/OnboardingTheme.ts +6 -0
- package/src/presentation/components/OnboardingScreenContent.tsx +76 -9
- package/src/presentation/components/OnboardingSlide.tsx +45 -108
- package/src/presentation/components/QuestionSlide.tsx +13 -48
- package/src/presentation/screens/OnboardingScreen.tsx +2 -0
- package/src/presentation/styles/OnboardingSlideStyles.ts +144 -0
- package/src/presentation/styles/QuestionSlideStyles.ts +78 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-onboarding",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"description": "Advanced onboarding flow for React Native apps with personalization questions, theme-aware colors, animations, and customizable slides. SOLID, DRY, KISS principles applied.",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -38,6 +38,8 @@
|
|
|
38
38
|
"@umituz/react-native-design-system": "latest",
|
|
39
39
|
"@umituz/react-native-design-system-atoms": "latest",
|
|
40
40
|
"@expo/vector-icons": ">=14.0.0",
|
|
41
|
+
"expo-av": "~15.0.0",
|
|
42
|
+
"expo-image": "~2.0.0",
|
|
41
43
|
"expo-linear-gradient": "^15.0.0",
|
|
42
44
|
"react": ">=18.2.0",
|
|
43
45
|
"react-native": ">=0.74.0",
|
|
@@ -53,6 +55,8 @@
|
|
|
53
55
|
"@umituz/react-native-design-system-theme": "latest",
|
|
54
56
|
"@umituz/react-native-design-system-atoms": "latest",
|
|
55
57
|
"@expo/vector-icons": ">=14.0.0",
|
|
58
|
+
"expo-av": "~15.0.0",
|
|
59
|
+
"expo-image": "~2.0.0",
|
|
56
60
|
"expo-linear-gradient": "^15.0.7",
|
|
57
61
|
"react": "^18.2.0",
|
|
58
62
|
"react-native": "^0.74.0",
|
|
@@ -68,4 +72,4 @@
|
|
|
68
72
|
"README.md",
|
|
69
73
|
"LICENSE"
|
|
70
74
|
]
|
|
71
|
-
}
|
|
75
|
+
}
|
|
@@ -91,5 +91,14 @@ export interface OnboardingOptions {
|
|
|
91
91
|
* When true, all slides will use gradient backgrounds if available
|
|
92
92
|
*/
|
|
93
93
|
useGradient?: boolean;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Visual theme variant (default: "default")
|
|
97
|
+
* - default: Standard layout
|
|
98
|
+
* - card: Content in a card with shadow
|
|
99
|
+
* - minimal: Clean, text-focused layout
|
|
100
|
+
* - fullscreen: Immersive fullscreen layout
|
|
101
|
+
*/
|
|
102
|
+
themeVariant?: "default" | "card" | "minimal" | "fullscreen";
|
|
94
103
|
}
|
|
95
104
|
|
|
@@ -58,7 +58,25 @@ export interface OnboardingSlide {
|
|
|
58
58
|
/**
|
|
59
59
|
* Optional image URL (alternative to icon)
|
|
60
60
|
*/
|
|
61
|
-
image?:
|
|
61
|
+
image?: any;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Optional background image (URL or require path)
|
|
65
|
+
* Stretches to fill the screen behind content
|
|
66
|
+
*/
|
|
67
|
+
backgroundImage?: any;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Optional background video (URL or require path)
|
|
71
|
+
* Plays in loop behind content
|
|
72
|
+
*/
|
|
73
|
+
backgroundVideo?: any;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Opacity of the overlay gradient/color on top of background media
|
|
77
|
+
* Range: 0.0 to 1.0 (Default: 0.5)
|
|
78
|
+
*/
|
|
79
|
+
overlayOpacity?: number;
|
|
62
80
|
|
|
63
81
|
/**
|
|
64
82
|
* Optional features list to display
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
import React from "react";
|
|
7
7
|
import { View, StyleSheet, StatusBar } from "react-native";
|
|
8
8
|
import { LinearGradient } from "expo-linear-gradient";
|
|
9
|
+
import { Image } from "expo-image";
|
|
10
|
+
import { Video, ResizeMode } from "expo-av";
|
|
9
11
|
import { useTheme } from "@umituz/react-native-design-system-theme";
|
|
10
12
|
import type { OnboardingSlide } from "../../domain/entities/OnboardingSlide";
|
|
11
13
|
import { OnboardingHeader } from "./OnboardingHeader";
|
|
@@ -51,6 +53,7 @@ export interface OnboardingScreenContentProps {
|
|
|
51
53
|
renderSlide?: (slide: OnboardingSlide) => React.ReactNode;
|
|
52
54
|
onUpgrade?: () => void;
|
|
53
55
|
showPaywallOnComplete?: boolean;
|
|
56
|
+
variant?: "default" | "card" | "minimal" | "fullscreen";
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
export const OnboardingScreenContent: React.FC<OnboardingScreenContentProps> = ({
|
|
@@ -80,20 +83,78 @@ export const OnboardingScreenContent: React.FC<OnboardingScreenContentProps> = (
|
|
|
80
83
|
renderSlide,
|
|
81
84
|
onUpgrade,
|
|
82
85
|
showPaywallOnComplete,
|
|
86
|
+
variant = "default",
|
|
83
87
|
}) => {
|
|
84
88
|
const { themeMode } = useTheme();
|
|
85
89
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
+
const hasMedia = !!currentSlide?.backgroundImage || !!currentSlide?.backgroundVideo;
|
|
91
|
+
const overlayOpacity = currentSlide?.overlayOpacity ?? 0.5;
|
|
92
|
+
// If media is present, valid gradient mode is enforced (white text)
|
|
93
|
+
const effectivelyUseGradient = useGradient || hasMedia;
|
|
94
|
+
|
|
95
|
+
const renderBackground = () => {
|
|
96
|
+
if (!currentSlide) return null;
|
|
97
|
+
|
|
98
|
+
if (currentSlide.backgroundVideo) {
|
|
99
|
+
return (
|
|
100
|
+
<View style={StyleSheet.absoluteFill}>
|
|
101
|
+
<Video
|
|
102
|
+
source={currentSlide.backgroundVideo}
|
|
103
|
+
style={StyleSheet.absoluteFill}
|
|
104
|
+
resizeMode={ResizeMode.COVER}
|
|
105
|
+
isLooping
|
|
106
|
+
shouldPlay
|
|
107
|
+
isMuted
|
|
108
|
+
/>
|
|
109
|
+
<View
|
|
110
|
+
style={[
|
|
111
|
+
StyleSheet.absoluteFill,
|
|
112
|
+
{ backgroundColor: `rgba(0,0,0,${overlayOpacity})` }
|
|
113
|
+
]}
|
|
114
|
+
/>
|
|
115
|
+
</View>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (currentSlide.backgroundImage) {
|
|
120
|
+
return (
|
|
121
|
+
<View style={StyleSheet.absoluteFill}>
|
|
122
|
+
<Image
|
|
123
|
+
source={currentSlide.backgroundImage}
|
|
124
|
+
style={StyleSheet.absoluteFill}
|
|
125
|
+
contentFit="cover"
|
|
126
|
+
transition={500}
|
|
127
|
+
/>
|
|
128
|
+
<View
|
|
129
|
+
style={[
|
|
130
|
+
StyleSheet.absoluteFill,
|
|
131
|
+
{ backgroundColor: `rgba(0,0,0,${overlayOpacity})` }
|
|
132
|
+
]}
|
|
133
|
+
/>
|
|
134
|
+
</View>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (useGradient && currentSlide.gradient) {
|
|
139
|
+
return (
|
|
90
140
|
<LinearGradient
|
|
91
141
|
colors={currentSlide.gradient as [string, string, ...string[]]}
|
|
92
142
|
start={{ x: 0, y: 0 }}
|
|
93
143
|
end={{ x: 1, y: 1 }}
|
|
94
144
|
style={StyleSheet.absoluteFill}
|
|
95
145
|
/>
|
|
96
|
-
)
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return null;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<View style={[styles.container, containerStyle]}>
|
|
154
|
+
<StatusBar barStyle={themeMode === "dark" || effectivelyUseGradient ? "light-content" : "dark-content"} />
|
|
155
|
+
|
|
156
|
+
{renderBackground()}
|
|
157
|
+
|
|
97
158
|
{renderHeader ? (
|
|
98
159
|
renderHeader({
|
|
99
160
|
isFirstSlide,
|
|
@@ -108,7 +169,7 @@ export const OnboardingScreenContent: React.FC<OnboardingScreenContentProps> = (
|
|
|
108
169
|
showBackButton={showBackButton}
|
|
109
170
|
showSkipButton={showSkipButton}
|
|
110
171
|
skipButtonText={skipButtonText}
|
|
111
|
-
useGradient={
|
|
172
|
+
useGradient={effectivelyUseGradient}
|
|
112
173
|
/>
|
|
113
174
|
)}
|
|
114
175
|
{currentSlide &&
|
|
@@ -119,10 +180,15 @@ export const OnboardingScreenContent: React.FC<OnboardingScreenContentProps> = (
|
|
|
119
180
|
slide={currentSlide}
|
|
120
181
|
value={currentAnswer}
|
|
121
182
|
onChange={onAnswerChange}
|
|
122
|
-
useGradient={
|
|
183
|
+
useGradient={effectivelyUseGradient}
|
|
184
|
+
variant={variant}
|
|
123
185
|
/>
|
|
124
186
|
) : (
|
|
125
|
-
<OnboardingSlideComponent
|
|
187
|
+
<OnboardingSlideComponent
|
|
188
|
+
slide={currentSlide}
|
|
189
|
+
useGradient={effectivelyUseGradient}
|
|
190
|
+
variant={variant}
|
|
191
|
+
/>
|
|
126
192
|
))}
|
|
127
193
|
{renderFooter ? (
|
|
128
194
|
renderFooter({
|
|
@@ -145,7 +211,7 @@ export const OnboardingScreenContent: React.FC<OnboardingScreenContentProps> = (
|
|
|
145
211
|
nextButtonText={nextButtonText}
|
|
146
212
|
getStartedButtonText={getStartedButtonText}
|
|
147
213
|
disabled={!isAnswerValid}
|
|
148
|
-
useGradient={
|
|
214
|
+
useGradient={effectivelyUseGradient}
|
|
149
215
|
/>
|
|
150
216
|
)}
|
|
151
217
|
</View>
|
|
@@ -158,3 +224,4 @@ const styles = StyleSheet.create({
|
|
|
158
224
|
},
|
|
159
225
|
});
|
|
160
226
|
|
|
227
|
+
|
|
@@ -2,37 +2,56 @@
|
|
|
2
2
|
* Onboarding Slide Component
|
|
3
3
|
*
|
|
4
4
|
* Displays a single onboarding slide with icon, title, and description
|
|
5
|
-
*
|
|
5
|
+
* Supports multiple variants (default, card, minimal) and dark mode
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import React, { useMemo } from "react";
|
|
9
|
-
import { View, Text,
|
|
9
|
+
import { View, Text, ScrollView } from "react-native";
|
|
10
10
|
import { AtomicIcon } from "@umituz/react-native-design-system-atoms";
|
|
11
|
-
import { useAppDesignTokens
|
|
11
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
|
|
12
12
|
import type { OnboardingSlide as OnboardingSlideType } from "../../domain/entities/OnboardingSlide";
|
|
13
|
+
import type { OnboardingThemeVariant } from "../../domain/entities/OnboardingTheme";
|
|
14
|
+
import { createOnboardingStyles } from "../styles/OnboardingSlideStyles";
|
|
13
15
|
|
|
14
16
|
export interface OnboardingSlideProps {
|
|
15
17
|
slide: OnboardingSlideType;
|
|
16
18
|
useGradient?: boolean;
|
|
19
|
+
variant?: OnboardingThemeVariant;
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
const EMOJI_REGEX = /[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/u;
|
|
20
23
|
|
|
21
|
-
export const OnboardingSlide: React.FC<OnboardingSlideProps> = ({
|
|
24
|
+
export const OnboardingSlide: React.FC<OnboardingSlideProps> = ({
|
|
25
|
+
slide,
|
|
26
|
+
useGradient = false,
|
|
27
|
+
variant = "default"
|
|
28
|
+
}) => {
|
|
22
29
|
const tokens = useAppDesignTokens();
|
|
23
|
-
const styles = useMemo(
|
|
30
|
+
const styles = useMemo(
|
|
31
|
+
() => createOnboardingStyles(tokens, useGradient, variant),
|
|
32
|
+
[tokens, useGradient, variant]
|
|
33
|
+
);
|
|
24
34
|
|
|
25
35
|
const isEmoji = EMOJI_REGEX.test(slide.icon);
|
|
26
|
-
|
|
36
|
+
// Simple check for valid icon name - assuming string and not emoji
|
|
37
|
+
const isValidIconName = !isEmoji && typeof slide.icon === "string" && slide.icon.length > 0;
|
|
38
|
+
|
|
39
|
+
const iconColor = useGradient ? "#FFFFFF" : tokens.colors.primary;
|
|
40
|
+
const iconSize = variant === "minimal" ? 80 : 64;
|
|
27
41
|
|
|
28
|
-
const renderIcon = (
|
|
42
|
+
const renderIcon = () => {
|
|
29
43
|
if (isEmoji) {
|
|
30
|
-
return <Text style={
|
|
44
|
+
return <Text style={{ fontSize: iconSize }}>{slide.icon}</Text>;
|
|
31
45
|
}
|
|
32
46
|
if (isValidIconName) {
|
|
33
|
-
return <AtomicIcon name={slide.icon} customSize={
|
|
47
|
+
return <AtomicIcon name={slide.icon} customSize={iconSize} customColor={iconColor} />;
|
|
34
48
|
}
|
|
35
|
-
|
|
49
|
+
// Fallback if no icon
|
|
50
|
+
if (slide.image) {
|
|
51
|
+
// TODO: Implement image support if needed, for now just placeholder
|
|
52
|
+
return <AtomicIcon name="image" customSize={iconSize} customColor={iconColor} />;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
36
55
|
};
|
|
37
56
|
|
|
38
57
|
const renderFeatures = () => {
|
|
@@ -49,111 +68,29 @@ export const OnboardingSlide: React.FC<OnboardingSlideProps> = ({ slide, useGrad
|
|
|
49
68
|
);
|
|
50
69
|
};
|
|
51
70
|
|
|
52
|
-
const iconColor = useGradient ? "#FFFFFF" : tokens.colors.textPrimary;
|
|
53
|
-
|
|
54
71
|
return (
|
|
55
72
|
<ScrollView
|
|
56
73
|
contentContainerStyle={styles.content}
|
|
57
74
|
showsVerticalScrollIndicator={false}
|
|
75
|
+
bounces={false}
|
|
58
76
|
>
|
|
59
|
-
{
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
<
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
77
|
+
<View style={styles.slideContainer}>
|
|
78
|
+
{/* Icon Section */}
|
|
79
|
+
{(isEmoji || isValidIconName) && (
|
|
80
|
+
<View style={styles.iconBox}>
|
|
81
|
+
{renderIcon()}
|
|
82
|
+
</View>
|
|
83
|
+
)}
|
|
84
|
+
|
|
85
|
+
{/* Text Section */}
|
|
86
|
+
<Text style={styles.title}>{slide.title}</Text>
|
|
87
|
+
<Text style={styles.description}>{slide.description}</Text>
|
|
88
|
+
|
|
89
|
+
{/* Features Section */}
|
|
90
|
+
{renderFeatures()}
|
|
91
|
+
</View>
|
|
74
92
|
</ScrollView>
|
|
75
93
|
);
|
|
76
94
|
};
|
|
77
95
|
|
|
78
|
-
const getStyles = (tokens: ReturnType<typeof useAppDesignTokens>, useGradient: boolean) =>
|
|
79
|
-
StyleSheet.create({
|
|
80
|
-
content: {
|
|
81
|
-
flexGrow: 1,
|
|
82
|
-
justifyContent: "center",
|
|
83
|
-
alignItems: "center",
|
|
84
|
-
paddingHorizontal: 30,
|
|
85
|
-
paddingVertical: 40,
|
|
86
|
-
},
|
|
87
|
-
slideContent: {
|
|
88
|
-
alignItems: "center",
|
|
89
|
-
maxWidth: 400,
|
|
90
|
-
width: "100%",
|
|
91
|
-
backgroundColor: tokens.colors.surface,
|
|
92
|
-
padding: 30,
|
|
93
|
-
borderRadius: 24,
|
|
94
|
-
borderWidth: 1,
|
|
95
|
-
borderColor: tokens.colors.borderLight,
|
|
96
|
-
shadowColor: tokens.colors.textPrimary,
|
|
97
|
-
shadowOffset: {
|
|
98
|
-
width: 0,
|
|
99
|
-
height: 4,
|
|
100
|
-
},
|
|
101
|
-
shadowOpacity: 0.1,
|
|
102
|
-
shadowRadius: 8,
|
|
103
|
-
elevation: 4,
|
|
104
|
-
},
|
|
105
|
-
iconContainer: {
|
|
106
|
-
width: 120,
|
|
107
|
-
height: 120,
|
|
108
|
-
borderRadius: 60,
|
|
109
|
-
backgroundColor: useGradient
|
|
110
|
-
? "rgba(255, 255, 255, 0.25)"
|
|
111
|
-
: withAlpha(tokens.colors.primary, 0.2),
|
|
112
|
-
alignItems: "center",
|
|
113
|
-
justifyContent: "center",
|
|
114
|
-
marginBottom: 40,
|
|
115
|
-
borderWidth: 2,
|
|
116
|
-
borderColor: useGradient
|
|
117
|
-
? "rgba(255, 255, 255, 0.4)"
|
|
118
|
-
: withAlpha(tokens.colors.primary, 0.4),
|
|
119
|
-
},
|
|
120
|
-
icon: {
|
|
121
|
-
fontSize: 60,
|
|
122
|
-
},
|
|
123
|
-
title: {
|
|
124
|
-
fontSize: 28,
|
|
125
|
-
fontWeight: "bold",
|
|
126
|
-
color: useGradient ? "#FFFFFF" : tokens.colors.textPrimary,
|
|
127
|
-
textAlign: "center",
|
|
128
|
-
marginBottom: 16,
|
|
129
|
-
},
|
|
130
|
-
description: {
|
|
131
|
-
fontSize: 16,
|
|
132
|
-
color: useGradient ? "rgba(255, 255, 255, 0.9)" : tokens.colors.textSecondary,
|
|
133
|
-
textAlign: "center",
|
|
134
|
-
lineHeight: 24,
|
|
135
|
-
marginBottom: 20,
|
|
136
|
-
},
|
|
137
|
-
featuresContainer: {
|
|
138
|
-
width: "100%",
|
|
139
|
-
marginTop: 10,
|
|
140
|
-
},
|
|
141
|
-
featureItem: {
|
|
142
|
-
flexDirection: "row",
|
|
143
|
-
alignItems: "flex-start",
|
|
144
|
-
marginBottom: 12,
|
|
145
|
-
},
|
|
146
|
-
featureBullet: {
|
|
147
|
-
color: useGradient ? "#FFFFFF" : tokens.colors.primary,
|
|
148
|
-
fontSize: 20,
|
|
149
|
-
marginRight: 12,
|
|
150
|
-
marginTop: 2,
|
|
151
|
-
},
|
|
152
|
-
featureText: {
|
|
153
|
-
flex: 1,
|
|
154
|
-
fontSize: 15,
|
|
155
|
-
color: useGradient ? "rgba(255, 255, 255, 0.9)" : tokens.colors.textSecondary,
|
|
156
|
-
lineHeight: 22,
|
|
157
|
-
},
|
|
158
|
-
});
|
|
159
96
|
|
|
@@ -4,17 +4,20 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import React, { useMemo } from "react";
|
|
7
|
-
import { View, Text,
|
|
7
|
+
import { View, Text, ScrollView } from "react-native";
|
|
8
8
|
import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
|
|
9
9
|
import type { OnboardingSlide } from "../../domain/entities/OnboardingSlide";
|
|
10
|
+
import type { OnboardingThemeVariant } from "../../domain/entities/OnboardingTheme";
|
|
10
11
|
import { QuestionSlideHeader } from "./QuestionSlideHeader";
|
|
11
12
|
import { QuestionRenderer } from "./QuestionRenderer";
|
|
13
|
+
import { createQuestionStyles } from "../styles/QuestionSlideStyles";
|
|
12
14
|
|
|
13
15
|
export interface QuestionSlideProps {
|
|
14
16
|
slide: OnboardingSlide;
|
|
15
17
|
value: any;
|
|
16
18
|
onChange: (value: any) => void;
|
|
17
19
|
useGradient?: boolean;
|
|
20
|
+
variant?: OnboardingThemeVariant;
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
export const QuestionSlide: React.FC<QuestionSlideProps> = ({
|
|
@@ -22,9 +25,13 @@ export const QuestionSlide: React.FC<QuestionSlideProps> = ({
|
|
|
22
25
|
value,
|
|
23
26
|
onChange,
|
|
24
27
|
useGradient = false,
|
|
28
|
+
variant = "default",
|
|
25
29
|
}) => {
|
|
26
30
|
const tokens = useAppDesignTokens();
|
|
27
|
-
const styles = useMemo(
|
|
31
|
+
const styles = useMemo(
|
|
32
|
+
() => createQuestionStyles(tokens, useGradient, variant),
|
|
33
|
+
[tokens, useGradient, variant]
|
|
34
|
+
);
|
|
28
35
|
const { question } = slide;
|
|
29
36
|
|
|
30
37
|
if (!question) {
|
|
@@ -53,54 +60,12 @@ export const QuestionSlide: React.FC<QuestionSlideProps> = ({
|
|
|
53
60
|
<ScrollView
|
|
54
61
|
contentContainerStyle={styles.content}
|
|
55
62
|
showsVerticalScrollIndicator={false}
|
|
63
|
+
bounces={false}
|
|
56
64
|
>
|
|
57
|
-
{
|
|
58
|
-
content
|
|
59
|
-
|
|
60
|
-
<View style={styles.slideContent}>{content}</View>
|
|
61
|
-
)}
|
|
65
|
+
<View style={styles.slideContainer}>
|
|
66
|
+
{content}
|
|
67
|
+
</View>
|
|
62
68
|
</ScrollView>
|
|
63
69
|
);
|
|
64
70
|
};
|
|
65
71
|
|
|
66
|
-
const getStyles = (
|
|
67
|
-
tokens: ReturnType<typeof useAppDesignTokens>,
|
|
68
|
-
useGradient: boolean,
|
|
69
|
-
) =>
|
|
70
|
-
StyleSheet.create({
|
|
71
|
-
content: {
|
|
72
|
-
flexGrow: 1,
|
|
73
|
-
justifyContent: "center",
|
|
74
|
-
alignItems: "center",
|
|
75
|
-
paddingHorizontal: 30,
|
|
76
|
-
paddingVertical: 20,
|
|
77
|
-
},
|
|
78
|
-
slideContent: {
|
|
79
|
-
alignItems: "center",
|
|
80
|
-
maxWidth: 500,
|
|
81
|
-
width: "100%",
|
|
82
|
-
backgroundColor: tokens.colors.surface,
|
|
83
|
-
padding: 30,
|
|
84
|
-
borderRadius: 24,
|
|
85
|
-
borderWidth: 1,
|
|
86
|
-
borderColor: tokens.colors.borderLight,
|
|
87
|
-
shadowColor: tokens.colors.textPrimary,
|
|
88
|
-
shadowOffset: {
|
|
89
|
-
width: 0,
|
|
90
|
-
height: 4,
|
|
91
|
-
},
|
|
92
|
-
shadowOpacity: 0.1,
|
|
93
|
-
shadowRadius: 8,
|
|
94
|
-
elevation: 4,
|
|
95
|
-
},
|
|
96
|
-
questionContainer: {
|
|
97
|
-
width: "100%",
|
|
98
|
-
marginTop: 8,
|
|
99
|
-
},
|
|
100
|
-
requiredHint: {
|
|
101
|
-
fontSize: 13,
|
|
102
|
-
color: useGradient ? "rgba(255, 255, 255, 0.8)" : tokens.colors.textSecondary,
|
|
103
|
-
fontStyle: "italic",
|
|
104
|
-
marginTop: 12,
|
|
105
|
-
},
|
|
106
|
-
});
|
|
@@ -73,6 +73,7 @@ export const OnboardingScreen: React.FC<OnboardingScreenProps> = ({
|
|
|
73
73
|
onUpgrade,
|
|
74
74
|
showPaywallOnComplete = false,
|
|
75
75
|
useGradient: globalUseGradient = false,
|
|
76
|
+
themeVariant = "default",
|
|
76
77
|
}) => {
|
|
77
78
|
if (__DEV__) {
|
|
78
79
|
console.log("[OnboardingScreen] Rendering with slides:", slides?.length);
|
|
@@ -140,6 +141,7 @@ export const OnboardingScreen: React.FC<OnboardingScreenProps> = ({
|
|
|
140
141
|
renderSlide={renderSlide}
|
|
141
142
|
onUpgrade={onUpgrade}
|
|
142
143
|
showPaywallOnComplete={showPaywallOnComplete}
|
|
144
|
+
variant={themeVariant}
|
|
143
145
|
/>
|
|
144
146
|
);
|
|
145
147
|
};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { StyleSheet } from "react-native";
|
|
2
|
+
import { withAlpha } from "@umituz/react-native-design-system-theme";
|
|
3
|
+
|
|
4
|
+
export const createOnboardingStyles = (
|
|
5
|
+
tokens: any,
|
|
6
|
+
useGradient: boolean,
|
|
7
|
+
variant: "default" | "card" | "minimal" | "fullscreen" = "default"
|
|
8
|
+
) => {
|
|
9
|
+
const isDark = tokens.isDark;
|
|
10
|
+
|
|
11
|
+
// Base styles
|
|
12
|
+
const baseStyles = StyleSheet.create({
|
|
13
|
+
content: {
|
|
14
|
+
flexGrow: 1,
|
|
15
|
+
justifyContent: "center",
|
|
16
|
+
alignItems: "center",
|
|
17
|
+
paddingHorizontal: 24,
|
|
18
|
+
paddingVertical: 40,
|
|
19
|
+
},
|
|
20
|
+
title: {
|
|
21
|
+
...tokens.typography.headingLarge,
|
|
22
|
+
textAlign: "center",
|
|
23
|
+
marginBottom: 16,
|
|
24
|
+
color: useGradient ? "#FFFFFF" : tokens.colors.textPrimary,
|
|
25
|
+
},
|
|
26
|
+
description: {
|
|
27
|
+
...tokens.typography.bodyLarge,
|
|
28
|
+
textAlign: "center",
|
|
29
|
+
color: useGradient
|
|
30
|
+
? "rgba(255, 255, 255, 0.9)"
|
|
31
|
+
: tokens.colors.textSecondary,
|
|
32
|
+
lineHeight: 24,
|
|
33
|
+
marginBottom: 24,
|
|
34
|
+
maxWidth: 320,
|
|
35
|
+
},
|
|
36
|
+
iconContainer: {
|
|
37
|
+
marginBottom: 40,
|
|
38
|
+
alignItems: "center",
|
|
39
|
+
justifyContent: "center",
|
|
40
|
+
},
|
|
41
|
+
featuresContainer: {
|
|
42
|
+
width: "100%",
|
|
43
|
+
marginTop: 24,
|
|
44
|
+
paddingHorizontal: 16,
|
|
45
|
+
},
|
|
46
|
+
featureItem: {
|
|
47
|
+
flexDirection: "row",
|
|
48
|
+
alignItems: "flex-start",
|
|
49
|
+
marginBottom: 16,
|
|
50
|
+
backgroundColor: useGradient
|
|
51
|
+
? "rgba(255, 255, 255, 0.1)"
|
|
52
|
+
: tokens.colors.surfaceSecondary,
|
|
53
|
+
padding: 12,
|
|
54
|
+
borderRadius: 12,
|
|
55
|
+
},
|
|
56
|
+
featureBullet: {
|
|
57
|
+
color: useGradient ? "#FFFFFF" : tokens.colors.primary,
|
|
58
|
+
fontSize: 18,
|
|
59
|
+
marginRight: 12,
|
|
60
|
+
marginTop: 2,
|
|
61
|
+
},
|
|
62
|
+
featureText: {
|
|
63
|
+
flex: 1,
|
|
64
|
+
...tokens.typography.bodyMedium,
|
|
65
|
+
color: useGradient ? "#FFFFFF" : tokens.colors.textPrimary,
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Variant specific styles
|
|
70
|
+
if (variant === "card") {
|
|
71
|
+
return StyleSheet.create({
|
|
72
|
+
...baseStyles,
|
|
73
|
+
slideContainer: {
|
|
74
|
+
width: "100%",
|
|
75
|
+
maxWidth: 400,
|
|
76
|
+
backgroundColor: useGradient ? "transparent" : tokens.colors.surface,
|
|
77
|
+
borderRadius: 24,
|
|
78
|
+
padding: 32,
|
|
79
|
+
alignItems: "center",
|
|
80
|
+
// Shadow only for non-gradient or light mode
|
|
81
|
+
...(!useGradient && {
|
|
82
|
+
shadowColor: tokens.colors.shadow,
|
|
83
|
+
shadowOffset: { width: 0, height: 4 },
|
|
84
|
+
shadowOpacity: 0.1,
|
|
85
|
+
shadowRadius: 12,
|
|
86
|
+
elevation: 5,
|
|
87
|
+
borderWidth: 1,
|
|
88
|
+
borderColor: tokens.colors.borderLight,
|
|
89
|
+
}),
|
|
90
|
+
},
|
|
91
|
+
iconBox: {
|
|
92
|
+
width: 100,
|
|
93
|
+
height: 100,
|
|
94
|
+
borderRadius: 50,
|
|
95
|
+
backgroundColor: useGradient
|
|
96
|
+
? "rgba(255, 255, 255, 0.2)"
|
|
97
|
+
: withAlpha(tokens.colors.primary, 0.1),
|
|
98
|
+
alignItems: "center",
|
|
99
|
+
justifyContent: "center",
|
|
100
|
+
marginBottom: 24,
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (variant === "minimal") {
|
|
106
|
+
return StyleSheet.create({
|
|
107
|
+
...baseStyles,
|
|
108
|
+
slideContainer: {
|
|
109
|
+
width: "100%",
|
|
110
|
+
alignItems: "center",
|
|
111
|
+
padding: 0,
|
|
112
|
+
},
|
|
113
|
+
title: {
|
|
114
|
+
...baseStyles.title,
|
|
115
|
+
fontSize: 32,
|
|
116
|
+
marginBottom: 24,
|
|
117
|
+
},
|
|
118
|
+
iconBox: {
|
|
119
|
+
marginBottom: 48,
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Default / Fullscreen
|
|
125
|
+
return StyleSheet.create({
|
|
126
|
+
...baseStyles,
|
|
127
|
+
slideContainer: {
|
|
128
|
+
width: "100%",
|
|
129
|
+
alignItems: "center",
|
|
130
|
+
padding: 16,
|
|
131
|
+
},
|
|
132
|
+
iconBox: {
|
|
133
|
+
width: 120,
|
|
134
|
+
height: 120,
|
|
135
|
+
borderRadius: 60,
|
|
136
|
+
backgroundColor: useGradient
|
|
137
|
+
? "rgba(255, 255, 255, 0.15)"
|
|
138
|
+
: tokens.colors.surfaceSecondary,
|
|
139
|
+
alignItems: "center",
|
|
140
|
+
justifyContent: "center",
|
|
141
|
+
marginBottom: 32,
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { StyleSheet } from "react-native";
|
|
2
|
+
|
|
3
|
+
export const createQuestionStyles = (
|
|
4
|
+
tokens: any,
|
|
5
|
+
useGradient: boolean,
|
|
6
|
+
variant: "default" | "card" | "minimal" | "fullscreen" = "default"
|
|
7
|
+
) => {
|
|
8
|
+
// Base styles
|
|
9
|
+
const baseStyles = StyleSheet.create({
|
|
10
|
+
content: {
|
|
11
|
+
flexGrow: 1,
|
|
12
|
+
justifyContent: "center",
|
|
13
|
+
alignItems: "center",
|
|
14
|
+
paddingHorizontal: 24,
|
|
15
|
+
paddingVertical: 20,
|
|
16
|
+
},
|
|
17
|
+
questionContainer: {
|
|
18
|
+
width: "100%",
|
|
19
|
+
marginTop: 24,
|
|
20
|
+
},
|
|
21
|
+
requiredHint: {
|
|
22
|
+
fontSize: 13,
|
|
23
|
+
color: useGradient ? "rgba(255, 255, 255, 0.8)" : tokens.colors.textSecondary,
|
|
24
|
+
fontStyle: "italic",
|
|
25
|
+
marginTop: 16,
|
|
26
|
+
textAlign: "left",
|
|
27
|
+
alignSelf: "flex-start",
|
|
28
|
+
marginLeft: 4,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
if (variant === "card") {
|
|
33
|
+
return StyleSheet.create({
|
|
34
|
+
...baseStyles,
|
|
35
|
+
slideContainer: {
|
|
36
|
+
width: "100%",
|
|
37
|
+
maxWidth: 500,
|
|
38
|
+
backgroundColor: useGradient ? "transparent" : tokens.colors.surface,
|
|
39
|
+
borderRadius: 24,
|
|
40
|
+
padding: 32,
|
|
41
|
+
alignItems: "center",
|
|
42
|
+
...(!useGradient && {
|
|
43
|
+
shadowColor: tokens.colors.shadow,
|
|
44
|
+
shadowOffset: { width: 0, height: 4 },
|
|
45
|
+
shadowOpacity: 0.1,
|
|
46
|
+
shadowRadius: 12,
|
|
47
|
+
elevation: 5,
|
|
48
|
+
borderWidth: 1,
|
|
49
|
+
borderColor: tokens.colors.borderLight,
|
|
50
|
+
}),
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (variant === "minimal") {
|
|
56
|
+
return StyleSheet.create({
|
|
57
|
+
...baseStyles,
|
|
58
|
+
slideContainer: {
|
|
59
|
+
width: "100%",
|
|
60
|
+
alignItems: "stretch",
|
|
61
|
+
padding: 0,
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Default
|
|
67
|
+
return StyleSheet.create({
|
|
68
|
+
...baseStyles,
|
|
69
|
+
slideContainer: {
|
|
70
|
+
width: "100%",
|
|
71
|
+
maxWidth: 500,
|
|
72
|
+
alignItems: "center",
|
|
73
|
+
padding: 16,
|
|
74
|
+
backgroundColor: useGradient ? "rgba(255, 255, 255, 0.1)" : tokens.colors.surfaceSecondary,
|
|
75
|
+
borderRadius: 16,
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
};
|