@blazejkustra/react-native-onboarding 0.1.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/LICENSE +20 -0
- package/README.md +360 -0
- package/lib/module/index.js +23 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/spill-onboarding/adapters/expo-image.js +13 -0
- package/lib/module/spill-onboarding/adapters/expo-image.js.map +1 -0
- package/lib/module/spill-onboarding/adapters/react-native-svg.js +16 -0
- package/lib/module/spill-onboarding/adapters/react-native-svg.js.map +1 -0
- package/lib/module/spill-onboarding/buttons/PrimaryButton.js +50 -0
- package/lib/module/spill-onboarding/buttons/PrimaryButton.js.map +1 -0
- package/lib/module/spill-onboarding/buttons/SecondaryButton.js +51 -0
- package/lib/module/spill-onboarding/buttons/SecondaryButton.js.map +1 -0
- package/lib/module/spill-onboarding/buttons/SkipButton.js +35 -0
- package/lib/module/spill-onboarding/buttons/SkipButton.js.map +1 -0
- package/lib/module/spill-onboarding/components/OnboardingImageContainer.js +117 -0
- package/lib/module/spill-onboarding/components/OnboardingImageContainer.js.map +1 -0
- package/lib/module/spill-onboarding/components/OnboardingIntroPanel.js +97 -0
- package/lib/module/spill-onboarding/components/OnboardingIntroPanel.js.map +1 -0
- package/lib/module/spill-onboarding/components/OnboardingModal.js +69 -0
- package/lib/module/spill-onboarding/components/OnboardingModal.js.map +1 -0
- package/lib/module/spill-onboarding/components/OnboardingStepContainer.js +60 -0
- package/lib/module/spill-onboarding/components/OnboardingStepContainer.js.map +1 -0
- package/lib/module/spill-onboarding/components/OnboardingStepPanel.js +122 -0
- package/lib/module/spill-onboarding/components/OnboardingStepPanel.js.map +1 -0
- package/lib/module/spill-onboarding/hooks/useMeasureHeight.js +18 -0
- package/lib/module/spill-onboarding/hooks/useMeasureHeight.js.map +1 -0
- package/lib/module/spill-onboarding/icons/ArrowLeftIcon.js +57 -0
- package/lib/module/spill-onboarding/icons/ArrowLeftIcon.js.map +1 -0
- package/lib/module/spill-onboarding/icons/CloseIcon.js +49 -0
- package/lib/module/spill-onboarding/icons/CloseIcon.js.map +1 -0
- package/lib/module/spill-onboarding/index.js +181 -0
- package/lib/module/spill-onboarding/index.js.map +1 -0
- package/lib/module/spill-onboarding/types.js +4 -0
- package/lib/module/spill-onboarding/types.js.map +1 -0
- package/lib/module/utils/ThemeContext.js +78 -0
- package/lib/module/utils/ThemeContext.js.map +1 -0
- package/lib/module/utils/fontStyles.js +21 -0
- package/lib/module/utils/fontStyles.js.map +1 -0
- package/lib/module/utils/theme.js +27 -0
- package/lib/module/utils/theme.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/index.d.ts +5 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/spill-onboarding/adapters/expo-image.d.ts +4 -0
- package/lib/typescript/src/spill-onboarding/adapters/expo-image.d.ts.map +1 -0
- package/lib/typescript/src/spill-onboarding/adapters/react-native-svg.d.ts +5 -0
- package/lib/typescript/src/spill-onboarding/adapters/react-native-svg.d.ts.map +1 -0
- package/lib/typescript/src/spill-onboarding/buttons/PrimaryButton.d.ts +13 -0
- package/lib/typescript/src/spill-onboarding/buttons/PrimaryButton.d.ts.map +1 -0
- package/lib/typescript/src/spill-onboarding/buttons/SecondaryButton.d.ts +13 -0
- package/lib/typescript/src/spill-onboarding/buttons/SecondaryButton.d.ts.map +1 -0
- package/lib/typescript/src/spill-onboarding/buttons/SkipButton.d.ts +6 -0
- package/lib/typescript/src/spill-onboarding/buttons/SkipButton.d.ts.map +1 -0
- package/lib/typescript/src/spill-onboarding/components/OnboardingImageContainer.d.ts +18 -0
- package/lib/typescript/src/spill-onboarding/components/OnboardingImageContainer.d.ts.map +1 -0
- package/lib/typescript/src/spill-onboarding/components/OnboardingIntroPanel.d.ts +4 -0
- package/lib/typescript/src/spill-onboarding/components/OnboardingIntroPanel.d.ts.map +1 -0
- package/lib/typescript/src/spill-onboarding/components/OnboardingModal.d.ts +8 -0
- package/lib/typescript/src/spill-onboarding/components/OnboardingModal.d.ts.map +1 -0
- package/lib/typescript/src/spill-onboarding/components/OnboardingStepContainer.d.ts +16 -0
- package/lib/typescript/src/spill-onboarding/components/OnboardingStepContainer.d.ts.map +1 -0
- package/lib/typescript/src/spill-onboarding/components/OnboardingStepPanel.d.ts +4 -0
- package/lib/typescript/src/spill-onboarding/components/OnboardingStepPanel.d.ts.map +1 -0
- package/lib/typescript/src/spill-onboarding/hooks/useMeasureHeight.d.ts +9 -0
- package/lib/typescript/src/spill-onboarding/hooks/useMeasureHeight.d.ts.map +1 -0
- package/lib/typescript/src/spill-onboarding/icons/ArrowLeftIcon.d.ts +7 -0
- package/lib/typescript/src/spill-onboarding/icons/ArrowLeftIcon.d.ts.map +1 -0
- package/lib/typescript/src/spill-onboarding/icons/CloseIcon.d.ts +7 -0
- package/lib/typescript/src/spill-onboarding/icons/CloseIcon.d.ts.map +1 -0
- package/lib/typescript/src/spill-onboarding/index.d.ts +4 -0
- package/lib/typescript/src/spill-onboarding/index.d.ts.map +1 -0
- package/lib/typescript/src/spill-onboarding/types.d.ts +187 -0
- package/lib/typescript/src/spill-onboarding/types.d.ts.map +1 -0
- package/lib/typescript/src/utils/ThemeContext.d.ts +14 -0
- package/lib/typescript/src/utils/ThemeContext.d.ts.map +1 -0
- package/lib/typescript/src/utils/fontStyles.d.ts +19 -0
- package/lib/typescript/src/utils/fontStyles.d.ts.map +1 -0
- package/lib/typescript/src/utils/theme.d.ts +30 -0
- package/lib/typescript/src/utils/theme.d.ts.map +1 -0
- package/package.json +171 -0
- package/src/index.tsx +29 -0
- package/src/spill-onboarding/adapters/expo-image.ts +12 -0
- package/src/spill-onboarding/adapters/react-native-svg.ts +17 -0
- package/src/spill-onboarding/buttons/PrimaryButton.tsx +70 -0
- package/src/spill-onboarding/buttons/SecondaryButton.tsx +71 -0
- package/src/spill-onboarding/buttons/SkipButton.tsx +34 -0
- package/src/spill-onboarding/components/OnboardingImageContainer.tsx +166 -0
- package/src/spill-onboarding/components/OnboardingIntroPanel.tsx +105 -0
- package/src/spill-onboarding/components/OnboardingModal.tsx +75 -0
- package/src/spill-onboarding/components/OnboardingStepContainer.tsx +85 -0
- package/src/spill-onboarding/components/OnboardingStepPanel.tsx +118 -0
- package/src/spill-onboarding/hooks/useMeasureHeight.ts +21 -0
- package/src/spill-onboarding/icons/ArrowLeftIcon.tsx +69 -0
- package/src/spill-onboarding/icons/CloseIcon.tsx +55 -0
- package/src/spill-onboarding/index.tsx +220 -0
- package/src/spill-onboarding/types.ts +237 -0
- package/src/utils/ThemeContext.tsx +87 -0
- package/src/utils/fontStyles.ts +19 -0
- package/src/utils/theme.ts +29 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { useMemo, type ReactNode } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Image,
|
|
4
|
+
type ImageSourcePropType,
|
|
5
|
+
Platform,
|
|
6
|
+
StyleSheet,
|
|
7
|
+
useWindowDimensions,
|
|
8
|
+
} from 'react-native';
|
|
9
|
+
import Animated, {
|
|
10
|
+
interpolate,
|
|
11
|
+
useDerivedValue,
|
|
12
|
+
useAnimatedStyle,
|
|
13
|
+
withTiming,
|
|
14
|
+
Easing,
|
|
15
|
+
type SharedValue,
|
|
16
|
+
useSharedValue,
|
|
17
|
+
} from 'react-native-reanimated';
|
|
18
|
+
import { useTheme } from '../../utils/ThemeContext';
|
|
19
|
+
import useMeasureHeight from '../hooks/useMeasureHeight';
|
|
20
|
+
import type { Theme } from '../../utils/theme';
|
|
21
|
+
import type { OnboardingStep } from '../types';
|
|
22
|
+
|
|
23
|
+
interface OnboardingImageContainerProps {
|
|
24
|
+
currentStep: OnboardingStep | undefined;
|
|
25
|
+
currentStepImage: ImageSourcePropType | undefined;
|
|
26
|
+
position: 'top' | 'bottom';
|
|
27
|
+
animationDuration: number;
|
|
28
|
+
backgroundSpillProgress: SharedValue<number>;
|
|
29
|
+
introPanel: any;
|
|
30
|
+
stepPanel: any;
|
|
31
|
+
screenHeight: number;
|
|
32
|
+
background?: () => ReactNode;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function OnboardingImageContainer({
|
|
36
|
+
currentStep,
|
|
37
|
+
currentStepImage,
|
|
38
|
+
position,
|
|
39
|
+
animationDuration,
|
|
40
|
+
backgroundSpillProgress,
|
|
41
|
+
introPanel,
|
|
42
|
+
stepPanel,
|
|
43
|
+
screenHeight,
|
|
44
|
+
background,
|
|
45
|
+
}: OnboardingImageContainerProps) {
|
|
46
|
+
const { theme } = useTheme();
|
|
47
|
+
const image = useMeasureHeight();
|
|
48
|
+
const styles = useMemo(() => createStyles(theme), [theme]);
|
|
49
|
+
const extraPadding = Platform.OS === 'web' ? 16 : 0;
|
|
50
|
+
const { width: screenWidth } = useWindowDimensions();
|
|
51
|
+
|
|
52
|
+
// bottom panel height (intro or step)
|
|
53
|
+
const bottomPanelHeight = (currentStep ? stepPanel : introPanel).height || 0;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* When 0 -> no spill; when 1 -> fully spilled by bottomPanelHeight
|
|
57
|
+
*/
|
|
58
|
+
const backgroundSpillDistance = useDerivedValue(() =>
|
|
59
|
+
interpolate(backgroundSpillProgress.value, [0, 1], [0, bottomPanelHeight])
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const imageWrapperHeight = useDerivedValue(
|
|
63
|
+
() => screenHeight - bottomPanelHeight + backgroundSpillDistance.value
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const imageWrapperAnimation = useAnimatedStyle(() => ({
|
|
67
|
+
height: imageWrapperHeight.value,
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
const imageTargetY = useDerivedValue(() => {
|
|
71
|
+
const topSafe = theme.insets.top + extraPadding + 16;
|
|
72
|
+
|
|
73
|
+
if (position === 'top') {
|
|
74
|
+
return topSafe;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const imageH = image.height || 0;
|
|
78
|
+
const modalTop = screenHeight - bottomPanelHeight - 16;
|
|
79
|
+
const yBottom = modalTop - imageH;
|
|
80
|
+
|
|
81
|
+
const shouldClampTop = !currentStep;
|
|
82
|
+
return shouldClampTop ? Math.max(yBottom, topSafe) : yBottom;
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const hasMounted = useSharedValue(false);
|
|
86
|
+
|
|
87
|
+
const imageAnimation = useAnimatedStyle(() => {
|
|
88
|
+
const translateY = withTiming(
|
|
89
|
+
imageTargetY.value,
|
|
90
|
+
{
|
|
91
|
+
duration: hasMounted.value ? animationDuration : 0,
|
|
92
|
+
easing: Easing.out(Easing.cubic),
|
|
93
|
+
},
|
|
94
|
+
() => (hasMounted.value = true)
|
|
95
|
+
);
|
|
96
|
+
const sideEdges = Math.max(32 + 24 - backgroundSpillDistance.value, 0);
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
transform: [{ translateY }],
|
|
100
|
+
maxWidth: screenWidth - sideEdges,
|
|
101
|
+
};
|
|
102
|
+
}, [animationDuration]);
|
|
103
|
+
|
|
104
|
+
const backgroundAnimation = useAnimatedStyle(() => {
|
|
105
|
+
const topEdge = Math.max(
|
|
106
|
+
theme.insets.top + extraPadding - backgroundSpillDistance.value,
|
|
107
|
+
0
|
|
108
|
+
);
|
|
109
|
+
const sideEdge = Math.max(16 - backgroundSpillDistance.value, 0);
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
position: 'absolute',
|
|
113
|
+
top: topEdge,
|
|
114
|
+
left: sideEdge,
|
|
115
|
+
right: sideEdge,
|
|
116
|
+
bottom: screenHeight - imageWrapperHeight.value,
|
|
117
|
+
borderRadius: Math.max(12 - backgroundSpillDistance.value, 0),
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<>
|
|
123
|
+
{background ? (
|
|
124
|
+
<Animated.View style={backgroundAnimation}>
|
|
125
|
+
{background()}
|
|
126
|
+
</Animated.View>
|
|
127
|
+
) : (
|
|
128
|
+
<Animated.View style={[styles.colorBg, backgroundAnimation]} />
|
|
129
|
+
)}
|
|
130
|
+
|
|
131
|
+
{currentStepImage && (
|
|
132
|
+
<Animated.View style={[styles.imageWrapper, imageWrapperAnimation]}>
|
|
133
|
+
<Animated.View style={[styles.image, imageAnimation]} ref={image.ref}>
|
|
134
|
+
<Image
|
|
135
|
+
source={currentStepImage}
|
|
136
|
+
resizeMode="contain"
|
|
137
|
+
fadeDuration={0}
|
|
138
|
+
/>
|
|
139
|
+
</Animated.View>
|
|
140
|
+
</Animated.View>
|
|
141
|
+
)}
|
|
142
|
+
</>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export default OnboardingImageContainer;
|
|
147
|
+
|
|
148
|
+
const createStyles = (theme: Theme) =>
|
|
149
|
+
StyleSheet.create({
|
|
150
|
+
colorBg: {
|
|
151
|
+
backgroundColor: theme.bg.primary,
|
|
152
|
+
overflow: 'hidden',
|
|
153
|
+
},
|
|
154
|
+
imageWrapper: {
|
|
155
|
+
position: 'absolute',
|
|
156
|
+
top: 0,
|
|
157
|
+
left: 0,
|
|
158
|
+
right: 0,
|
|
159
|
+
overflow: 'hidden',
|
|
160
|
+
},
|
|
161
|
+
image: {
|
|
162
|
+
alignSelf: 'center',
|
|
163
|
+
alignItems: 'center',
|
|
164
|
+
overflow: 'hidden',
|
|
165
|
+
},
|
|
166
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { Image, StyleSheet, Text, View } from 'react-native';
|
|
3
|
+
import { useTheme } from '../../utils/ThemeContext';
|
|
4
|
+
import type { Theme } from '../../utils/theme';
|
|
5
|
+
import PrimaryButton from '../buttons/PrimaryButton';
|
|
6
|
+
import { fontSizes, lineHeights } from '../../utils/fontStyles';
|
|
7
|
+
import type { OnboardingIntroPanelProps } from '../types';
|
|
8
|
+
|
|
9
|
+
function OnboardingIntroPanel({
|
|
10
|
+
onPressStart,
|
|
11
|
+
title,
|
|
12
|
+
subtitle,
|
|
13
|
+
button,
|
|
14
|
+
image,
|
|
15
|
+
}: OnboardingIntroPanelProps) {
|
|
16
|
+
const { theme } = useTheme();
|
|
17
|
+
const styles = useMemo(() => createStyles(theme), [theme]);
|
|
18
|
+
|
|
19
|
+
const renderTitle = () => {
|
|
20
|
+
if (!title) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (typeof title === 'string') {
|
|
25
|
+
return (
|
|
26
|
+
<Text style={[styles.text, styles.line1, styles.titleText]}>
|
|
27
|
+
{title}
|
|
28
|
+
</Text>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return title;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const renderSubtitle = () => {
|
|
36
|
+
if (!subtitle) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (typeof subtitle === 'string') {
|
|
41
|
+
return (
|
|
42
|
+
<Text style={[styles.text, styles.line2, styles.subtitleText]}>
|
|
43
|
+
{subtitle}
|
|
44
|
+
</Text>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return subtitle;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const renderButton = () => {
|
|
52
|
+
if (typeof button === 'string') {
|
|
53
|
+
return <PrimaryButton text={button} onPress={onPressStart} />;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return button({ onPressStart });
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<View style={styles.container}>
|
|
61
|
+
{typeof image === 'function'
|
|
62
|
+
? image()
|
|
63
|
+
: image && <Image source={image} style={styles.image} />}
|
|
64
|
+
<View style={styles.textContainer}>
|
|
65
|
+
{renderTitle()}
|
|
66
|
+
{renderSubtitle()}
|
|
67
|
+
</View>
|
|
68
|
+
{renderButton()}
|
|
69
|
+
</View>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export default OnboardingIntroPanel;
|
|
74
|
+
|
|
75
|
+
const createStyles = (theme: Theme) =>
|
|
76
|
+
StyleSheet.create({
|
|
77
|
+
container: {
|
|
78
|
+
marginTop: 16,
|
|
79
|
+
},
|
|
80
|
+
image: {
|
|
81
|
+
alignSelf: 'center',
|
|
82
|
+
},
|
|
83
|
+
textContainer: {
|
|
84
|
+
alignItems: 'center',
|
|
85
|
+
marginBottom: 48,
|
|
86
|
+
},
|
|
87
|
+
text: {
|
|
88
|
+
fontSize: fontSizes.xxl,
|
|
89
|
+
lineHeight: lineHeights.xxl,
|
|
90
|
+
textAlign: 'center',
|
|
91
|
+
},
|
|
92
|
+
line1: {
|
|
93
|
+
marginTop: 20,
|
|
94
|
+
color: theme.text.primary,
|
|
95
|
+
},
|
|
96
|
+
line2: {
|
|
97
|
+
color: theme.bg.primary,
|
|
98
|
+
},
|
|
99
|
+
titleText: {
|
|
100
|
+
fontFamily: theme.fonts.introTitle,
|
|
101
|
+
},
|
|
102
|
+
subtitleText: {
|
|
103
|
+
fontFamily: theme.fonts.introSubtitle,
|
|
104
|
+
},
|
|
105
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
StyleSheet,
|
|
5
|
+
Modal,
|
|
6
|
+
TouchableOpacity,
|
|
7
|
+
useWindowDimensions,
|
|
8
|
+
} from 'react-native';
|
|
9
|
+
import { useTheme } from '../../utils/ThemeContext';
|
|
10
|
+
import type { Theme } from '../../utils/theme';
|
|
11
|
+
|
|
12
|
+
interface OnboardingModalProps {
|
|
13
|
+
onSkip?: () => void;
|
|
14
|
+
children: React.ReactNode;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function OnboardingModal({
|
|
18
|
+
onSkip,
|
|
19
|
+
children,
|
|
20
|
+
}: OnboardingModalProps) {
|
|
21
|
+
const { theme } = useTheme();
|
|
22
|
+
const { height, width } = useWindowDimensions();
|
|
23
|
+
const styles = useMemo(
|
|
24
|
+
() => createStyles(theme, height, width),
|
|
25
|
+
[height, width, theme]
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<Modal visible transparent onRequestClose={onSkip}>
|
|
30
|
+
<View style={styles.webOverlay}>
|
|
31
|
+
<TouchableOpacity
|
|
32
|
+
style={styles.webBackdrop}
|
|
33
|
+
activeOpacity={1}
|
|
34
|
+
onPress={onSkip}
|
|
35
|
+
/>
|
|
36
|
+
<View style={styles.webModal}>{children}</View>
|
|
37
|
+
</View>
|
|
38
|
+
</Modal>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const createStyles = (theme: Theme, height: number, width: number) =>
|
|
43
|
+
StyleSheet.create({
|
|
44
|
+
webOverlay: {
|
|
45
|
+
flex: 1,
|
|
46
|
+
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
47
|
+
justifyContent: 'center',
|
|
48
|
+
alignItems: 'center',
|
|
49
|
+
},
|
|
50
|
+
webBackdrop: {
|
|
51
|
+
position: 'absolute',
|
|
52
|
+
top: 0,
|
|
53
|
+
left: 0,
|
|
54
|
+
right: 0,
|
|
55
|
+
bottom: 0,
|
|
56
|
+
},
|
|
57
|
+
webModal: {
|
|
58
|
+
width: Math.min(width - 32, 500),
|
|
59
|
+
height: Math.min(height - 32, 800),
|
|
60
|
+
borderRadius: 28,
|
|
61
|
+
shadowColor: '#000',
|
|
62
|
+
shadowOffset: {
|
|
63
|
+
width: 0,
|
|
64
|
+
height: 10,
|
|
65
|
+
},
|
|
66
|
+
shadowOpacity: 0.25,
|
|
67
|
+
shadowRadius: 20,
|
|
68
|
+
overflow: 'hidden',
|
|
69
|
+
backgroundColor: theme.bg.secondary,
|
|
70
|
+
},
|
|
71
|
+
webContent: {
|
|
72
|
+
flex: 1,
|
|
73
|
+
paddingTop: 16,
|
|
74
|
+
},
|
|
75
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import React, { useMemo, type ReactNode } from 'react';
|
|
2
|
+
import { StyleSheet } from 'react-native';
|
|
3
|
+
import Reanimated, {
|
|
4
|
+
FadeIn,
|
|
5
|
+
FadeInDown,
|
|
6
|
+
FadeOut,
|
|
7
|
+
FadeOutDown,
|
|
8
|
+
} from 'react-native-reanimated';
|
|
9
|
+
import SkipButton from '../buttons/SkipButton';
|
|
10
|
+
import { useTheme } from '../../utils/ThemeContext';
|
|
11
|
+
import type { Theme } from '../../utils/theme';
|
|
12
|
+
import type { OnboardingStep } from '../types';
|
|
13
|
+
|
|
14
|
+
interface OnboardingStepContainerProps {
|
|
15
|
+
currentStep: OnboardingStep | undefined;
|
|
16
|
+
showCloseButton?: boolean;
|
|
17
|
+
animationDuration: number;
|
|
18
|
+
onSkip?: () => void;
|
|
19
|
+
ref: React.RefObject<any>;
|
|
20
|
+
renderStepContent: () => React.ReactNode;
|
|
21
|
+
skipButton?: ({ onPress }: { onPress: () => void }) => ReactNode;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function OnboardingStepContainer({
|
|
25
|
+
currentStep,
|
|
26
|
+
showCloseButton,
|
|
27
|
+
animationDuration,
|
|
28
|
+
onSkip,
|
|
29
|
+
ref,
|
|
30
|
+
renderStepContent,
|
|
31
|
+
skipButton,
|
|
32
|
+
}: OnboardingStepContainerProps) {
|
|
33
|
+
const { theme } = useTheme();
|
|
34
|
+
const styles = useMemo(() => createStyles(theme), [theme]);
|
|
35
|
+
|
|
36
|
+
if (!currentStep) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<>
|
|
42
|
+
{showCloseButton && onSkip && (
|
|
43
|
+
<Reanimated.View
|
|
44
|
+
entering={FadeIn.duration(animationDuration)}
|
|
45
|
+
exiting={FadeOut.duration(animationDuration)}
|
|
46
|
+
style={styles.close}
|
|
47
|
+
>
|
|
48
|
+
{skipButton ? (
|
|
49
|
+
skipButton({ onPress: onSkip })
|
|
50
|
+
) : (
|
|
51
|
+
<SkipButton onPress={onSkip} />
|
|
52
|
+
)}
|
|
53
|
+
</Reanimated.View>
|
|
54
|
+
)}
|
|
55
|
+
|
|
56
|
+
<Reanimated.View
|
|
57
|
+
ref={ref}
|
|
58
|
+
entering={FadeInDown.duration(animationDuration)}
|
|
59
|
+
exiting={FadeOutDown.duration(animationDuration)}
|
|
60
|
+
style={styles.bottomPanel}
|
|
61
|
+
>
|
|
62
|
+
{renderStepContent()}
|
|
63
|
+
</Reanimated.View>
|
|
64
|
+
</>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export default OnboardingStepContainer;
|
|
69
|
+
|
|
70
|
+
const createStyles = (theme: Theme) =>
|
|
71
|
+
StyleSheet.create({
|
|
72
|
+
bottomPanel: {
|
|
73
|
+
paddingHorizontal: 16,
|
|
74
|
+
paddingBottom: 16 + theme.insets.bottom,
|
|
75
|
+
position: 'absolute',
|
|
76
|
+
bottom: 0,
|
|
77
|
+
left: 0,
|
|
78
|
+
right: 0,
|
|
79
|
+
},
|
|
80
|
+
close: {
|
|
81
|
+
position: 'absolute',
|
|
82
|
+
top: theme.insets.top + 16,
|
|
83
|
+
right: 16,
|
|
84
|
+
},
|
|
85
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { StyleSheet, Text, View } from 'react-native';
|
|
3
|
+
import { useTheme } from '../../utils/ThemeContext';
|
|
4
|
+
import { type Theme } from '../../utils/theme';
|
|
5
|
+
import { fontSizes, lineHeights } from '../../utils/fontStyles';
|
|
6
|
+
import PrimaryButton from '../buttons/PrimaryButton';
|
|
7
|
+
import SecondaryButton from '../buttons/SecondaryButton';
|
|
8
|
+
import ArrowLeftIcon from '../icons/ArrowLeftIcon';
|
|
9
|
+
import type { OnboardingStepPanelProps } from '../types';
|
|
10
|
+
|
|
11
|
+
function OnboardingStepPanel({
|
|
12
|
+
label,
|
|
13
|
+
title,
|
|
14
|
+
description,
|
|
15
|
+
buttonLabel,
|
|
16
|
+
onBackPress,
|
|
17
|
+
onNextPress,
|
|
18
|
+
buttonPrimary,
|
|
19
|
+
showBackButton = true,
|
|
20
|
+
}: OnboardingStepPanelProps) {
|
|
21
|
+
const { theme } = useTheme();
|
|
22
|
+
const styles = useMemo(() => createStyles(theme), [theme]);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<View style={styles.container}>
|
|
26
|
+
<View style={styles.textContainer}>
|
|
27
|
+
{label && (
|
|
28
|
+
<View style={styles.labelBadge}>
|
|
29
|
+
<Text style={styles.labelText}>{label}</Text>
|
|
30
|
+
</View>
|
|
31
|
+
)}
|
|
32
|
+
<Text style={styles.title}>{title}</Text>
|
|
33
|
+
<Text style={styles.description}>{description}</Text>
|
|
34
|
+
</View>
|
|
35
|
+
<View style={styles.buttonRow}>
|
|
36
|
+
{onBackPress && showBackButton && (
|
|
37
|
+
<View style={styles.backButton}>
|
|
38
|
+
<SecondaryButton
|
|
39
|
+
text=""
|
|
40
|
+
onPress={onBackPress}
|
|
41
|
+
icon={<ArrowLeftIcon color={theme.text.primary} />}
|
|
42
|
+
/>
|
|
43
|
+
</View>
|
|
44
|
+
)}
|
|
45
|
+
<View style={styles.nextButton}>
|
|
46
|
+
{buttonPrimary ? (
|
|
47
|
+
<PrimaryButton text={buttonLabel} onPress={onNextPress} />
|
|
48
|
+
) : (
|
|
49
|
+
<SecondaryButton
|
|
50
|
+
text={buttonLabel}
|
|
51
|
+
onPress={onNextPress}
|
|
52
|
+
textStyle={styles.nextButtonText}
|
|
53
|
+
/>
|
|
54
|
+
)}
|
|
55
|
+
</View>
|
|
56
|
+
</View>
|
|
57
|
+
</View>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export default OnboardingStepPanel;
|
|
62
|
+
|
|
63
|
+
const createStyles = (theme: Theme) =>
|
|
64
|
+
StyleSheet.create({
|
|
65
|
+
container: {
|
|
66
|
+
backgroundColor: theme.bg.secondary,
|
|
67
|
+
padding: 16,
|
|
68
|
+
borderRadius: 18,
|
|
69
|
+
gap: 24,
|
|
70
|
+
},
|
|
71
|
+
textContainer: {
|
|
72
|
+
alignItems: 'center',
|
|
73
|
+
gap: 16,
|
|
74
|
+
},
|
|
75
|
+
labelBadge: {
|
|
76
|
+
backgroundColor: theme.bg.label,
|
|
77
|
+
paddingHorizontal: 8,
|
|
78
|
+
paddingVertical: 4,
|
|
79
|
+
borderRadius: 999,
|
|
80
|
+
},
|
|
81
|
+
labelText: {
|
|
82
|
+
fontFamily: theme.fonts.stepLabel,
|
|
83
|
+
fontSize: fontSizes.xs,
|
|
84
|
+
lineHeight: lineHeights.xs,
|
|
85
|
+
textAlign: 'center',
|
|
86
|
+
color: theme.text.primary,
|
|
87
|
+
},
|
|
88
|
+
title: {
|
|
89
|
+
fontFamily: theme.fonts.stepTitle,
|
|
90
|
+
fontSize: fontSizes.lg,
|
|
91
|
+
lineHeight: lineHeights.lg,
|
|
92
|
+
textAlign: 'center',
|
|
93
|
+
color: theme.text.primary,
|
|
94
|
+
},
|
|
95
|
+
description: {
|
|
96
|
+
fontFamily: theme.fonts.stepDescription,
|
|
97
|
+
fontSize: fontSizes.md,
|
|
98
|
+
lineHeight: lineHeights.md,
|
|
99
|
+
textAlign: 'center',
|
|
100
|
+
color: theme.text.secondary,
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
buttonRow: {
|
|
104
|
+
flexDirection: 'row',
|
|
105
|
+
gap: 8,
|
|
106
|
+
},
|
|
107
|
+
backButton: {
|
|
108
|
+
width: 48,
|
|
109
|
+
},
|
|
110
|
+
nextButton: {
|
|
111
|
+
flex: 1,
|
|
112
|
+
},
|
|
113
|
+
nextButtonText: {
|
|
114
|
+
fontFamily: theme.fonts.stepButton,
|
|
115
|
+
fontSize: fontSizes.md,
|
|
116
|
+
lineHeight: lineHeights.md,
|
|
117
|
+
},
|
|
118
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useLayoutEffect, useRef, useState } from 'react';
|
|
2
|
+
import type { View } from 'react-native';
|
|
3
|
+
import type Reanimated from 'react-native-reanimated';
|
|
4
|
+
|
|
5
|
+
export type ViewRef = React.ComponentRef<typeof Reanimated.View> &
|
|
6
|
+
React.ComponentRef<typeof View>;
|
|
7
|
+
|
|
8
|
+
function useMeasureHeight() {
|
|
9
|
+
const ref = useRef<ViewRef>(null);
|
|
10
|
+
const [height, setHeight] = useState(0);
|
|
11
|
+
|
|
12
|
+
useLayoutEffect(() => {
|
|
13
|
+
ref.current?.measure?.((_, __, ___, viewHeight) => {
|
|
14
|
+
setHeight(viewHeight);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
return { ref, height };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default useMeasureHeight;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Text, StyleSheet } from 'react-native';
|
|
2
|
+
import { ExpoImage } from '../adapters/expo-image';
|
|
3
|
+
import {
|
|
4
|
+
ReactNativeSVG,
|
|
5
|
+
ReactNativeSVGPath,
|
|
6
|
+
} from '../adapters/react-native-svg';
|
|
7
|
+
|
|
8
|
+
interface ArrowLeftIconProps {
|
|
9
|
+
size?: number;
|
|
10
|
+
color?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const SVG_ARROW_LEFT_STRING =
|
|
14
|
+
'M10.7071 5.29289C11.0976 5.68342 11.0976 6.31658 10.7071 6.70711L6.41421 11L20 11C20.5523 11 21 11.4477 21 12C21 12.5523 20.5523 13 20 13L6.41421 13L10.7071 17.2929C11.0976 17.6834 11.0976 18.3166 10.7071 18.7071C10.3166 19.0976 9.68342 19.0976 9.29289 18.7071L3.29289 12.7071C3.10536 12.5196 3 12.2652 3 12C3 11.7348 3.10536 11.4804 3.29289 11.2929L9.29289 5.29289C9.68342 4.90237 10.3166 4.90237 10.7071 5.29289Z';
|
|
15
|
+
|
|
16
|
+
export default function ArrowLeftIcon({
|
|
17
|
+
size = 24,
|
|
18
|
+
color = '#000',
|
|
19
|
+
}: ArrowLeftIconProps) {
|
|
20
|
+
// Use expo-image with dynamic SVG string (includes color)
|
|
21
|
+
if (ExpoImage) {
|
|
22
|
+
const svgString = `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
23
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="${SVG_ARROW_LEFT_STRING}" fill="${color}"/>
|
|
24
|
+
</svg>`;
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<ExpoImage
|
|
28
|
+
source={{ uri: `data:image/svg+xml;base64,${btoa(svgString)}` }}
|
|
29
|
+
style={{ width: size, height: size }}
|
|
30
|
+
contentFit="contain"
|
|
31
|
+
/>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Use SVG if available
|
|
36
|
+
if (ReactNativeSVG && ReactNativeSVGPath) {
|
|
37
|
+
return (
|
|
38
|
+
<ReactNativeSVG
|
|
39
|
+
width={size}
|
|
40
|
+
height={size}
|
|
41
|
+
viewBox="0 0 24 24"
|
|
42
|
+
fill="none"
|
|
43
|
+
>
|
|
44
|
+
<ReactNativeSVGPath d={SVG_ARROW_LEFT_STRING} fill={color} />
|
|
45
|
+
</ReactNativeSVG>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Fallback to text arrow
|
|
50
|
+
return (
|
|
51
|
+
<Text
|
|
52
|
+
style={[
|
|
53
|
+
styles.arrow,
|
|
54
|
+
{
|
|
55
|
+
fontSize: size,
|
|
56
|
+
color,
|
|
57
|
+
},
|
|
58
|
+
]}
|
|
59
|
+
>
|
|
60
|
+
Go Back
|
|
61
|
+
</Text>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const styles = StyleSheet.create({
|
|
66
|
+
arrow: {
|
|
67
|
+
textAlign: 'center',
|
|
68
|
+
},
|
|
69
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { View, StyleSheet } from 'react-native';
|
|
2
|
+
|
|
3
|
+
interface CloseIconProps {
|
|
4
|
+
size?: number;
|
|
5
|
+
color?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default function CloseIcon({
|
|
9
|
+
size = 24,
|
|
10
|
+
color = '#000',
|
|
11
|
+
}: CloseIconProps) {
|
|
12
|
+
const lineWidth = Math.max(2, size * 0.1);
|
|
13
|
+
const lineLength = size * 0.7;
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<View style={[styles.container, { width: size, height: size }]}>
|
|
17
|
+
{/* First diagonal line */}
|
|
18
|
+
<View
|
|
19
|
+
style={[
|
|
20
|
+
styles.line,
|
|
21
|
+
{
|
|
22
|
+
width: lineLength,
|
|
23
|
+
height: lineWidth,
|
|
24
|
+
backgroundColor: color,
|
|
25
|
+
transform: [{ rotate: '45deg' }],
|
|
26
|
+
},
|
|
27
|
+
]}
|
|
28
|
+
/>
|
|
29
|
+
{/* Second diagonal line */}
|
|
30
|
+
<View
|
|
31
|
+
style={[
|
|
32
|
+
styles.line,
|
|
33
|
+
// eslint-disable-next-line react-native/no-inline-styles
|
|
34
|
+
{
|
|
35
|
+
width: lineLength,
|
|
36
|
+
height: lineWidth,
|
|
37
|
+
backgroundColor: color,
|
|
38
|
+
transform: [{ rotate: '-45deg' }],
|
|
39
|
+
position: 'absolute',
|
|
40
|
+
},
|
|
41
|
+
]}
|
|
42
|
+
/>
|
|
43
|
+
</View>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const styles = StyleSheet.create({
|
|
48
|
+
container: {
|
|
49
|
+
justifyContent: 'center',
|
|
50
|
+
alignItems: 'center',
|
|
51
|
+
},
|
|
52
|
+
line: {
|
|
53
|
+
position: 'absolute',
|
|
54
|
+
},
|
|
55
|
+
});
|