@umituz/react-native-onboarding 3.2.0 → 3.2.1

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-onboarding",
3
- "version": "3.2.0",
3
+ "version": "3.2.1",
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",
@@ -68,4 +68,4 @@
68
68
  "README.md",
69
69
  "LICENSE"
70
70
  ]
71
- }
71
+ }
@@ -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
 
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Onboarding Theme Variant
3
+ *
4
+ * Defines the visual style of the onboarding flow
5
+ */
6
+ export type OnboardingThemeVariant = "default" | "card" | "minimal" | "fullscreen";
@@ -51,6 +51,7 @@ export interface OnboardingScreenContentProps {
51
51
  renderSlide?: (slide: OnboardingSlide) => React.ReactNode;
52
52
  onUpgrade?: () => void;
53
53
  showPaywallOnComplete?: boolean;
54
+ variant?: "default" | "card" | "minimal" | "fullscreen";
54
55
  }
55
56
 
56
57
  export const OnboardingScreenContent: React.FC<OnboardingScreenContentProps> = ({
@@ -80,6 +81,7 @@ export const OnboardingScreenContent: React.FC<OnboardingScreenContentProps> = (
80
81
  renderSlide,
81
82
  onUpgrade,
82
83
  showPaywallOnComplete,
84
+ variant = "default",
83
85
  }) => {
84
86
  const { themeMode } = useTheme();
85
87
 
@@ -120,9 +122,14 @@ export const OnboardingScreenContent: React.FC<OnboardingScreenContentProps> = (
120
122
  value={currentAnswer}
121
123
  onChange={onAnswerChange}
122
124
  useGradient={useGradient}
125
+ variant={variant}
123
126
  />
124
127
  ) : (
125
- <OnboardingSlideComponent slide={currentSlide} useGradient={useGradient} />
128
+ <OnboardingSlideComponent
129
+ slide={currentSlide}
130
+ useGradient={useGradient}
131
+ variant={variant}
132
+ />
126
133
  ))}
127
134
  {renderFooter ? (
128
135
  renderFooter({
@@ -2,37 +2,56 @@
2
2
  * Onboarding Slide Component
3
3
  *
4
4
  * Displays a single onboarding slide with icon, title, and description
5
- * Uses AtomicIcon from design system for consistent icon rendering
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, StyleSheet, ScrollView } from "react-native";
9
+ import { View, Text, ScrollView } from "react-native";
10
10
  import { AtomicIcon } from "@umituz/react-native-design-system-atoms";
11
- import { useAppDesignTokens, withAlpha } from "@umituz/react-native-design-system-theme";
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> = ({ slide, useGradient = false }) => {
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(() => getStyles(tokens, useGradient), [tokens, useGradient]);
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
- const isValidIconName = !isEmoji && typeof slide.icon === "string";
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 = (color: string) => {
42
+ const renderIcon = () => {
29
43
  if (isEmoji) {
30
- return <Text style={styles.icon}>{slide.icon}</Text>;
44
+ return <Text style={{ fontSize: iconSize }}>{slide.icon}</Text>;
31
45
  }
32
46
  if (isValidIconName) {
33
- return <AtomicIcon name={slide.icon} customSize={60} customColor={color} />;
47
+ return <AtomicIcon name={slide.icon} customSize={iconSize} customColor={iconColor} />;
34
48
  }
35
- return <Text style={styles.icon}>📱</Text>;
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
- {useGradient ? (
60
- <>
61
- <View style={styles.iconContainer}>{renderIcon(iconColor)}</View>
62
- <Text style={styles.title}>{slide.title}</Text>
63
- <Text style={styles.description}>{slide.description}</Text>
64
- {renderFeatures()}
65
- </>
66
- ) : (
67
- <View style={styles.slideContent}>
68
- <View style={styles.iconContainer}>{renderIcon(iconColor)}</View>
69
- <Text style={styles.title}>{slide.title}</Text>
70
- <Text style={styles.description}>{slide.description}</Text>
71
- {renderFeatures()}
72
- </View>
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, StyleSheet, ScrollView } from "react-native";
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(() => getStyles(tokens, useGradient), [tokens, useGradient]);
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
- {useGradient ? (
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
+ };