@korsolutions/guidon 1.0.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/README.md +334 -0
- package/dist/index-D_JFvCIg.d.mts +314 -0
- package/dist/index-D_JFvCIg.d.ts +314 -0
- package/dist/index.d.mts +128 -0
- package/dist/index.d.ts +128 -0
- package/dist/index.js +1098 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1073 -0
- package/dist/index.mjs.map +1 -0
- package/dist/persistence/index.d.mts +2 -0
- package/dist/persistence/index.d.ts +2 -0
- package/dist/persistence/index.js +300 -0
- package/dist/persistence/index.js.map +1 -0
- package/dist/persistence/index.mjs +291 -0
- package/dist/persistence/index.mjs.map +1 -0
- package/package.json +76 -0
- package/src/components/GuidonOverlay.tsx +159 -0
- package/src/components/GuidonProvider.tsx +158 -0
- package/src/components/GuidonTarget.tsx +108 -0
- package/src/components/GuidonTooltip.tsx +365 -0
- package/src/components/index.ts +4 -0
- package/src/index.ts +51 -0
- package/src/persistence/adapters.ts +224 -0
- package/src/persistence/hooks.ts +179 -0
- package/src/persistence/index.ts +2 -0
- package/src/store.ts +268 -0
- package/src/types.ts +242 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { useEffect, useCallback, useRef, createContext, useContext } from 'react';
|
|
2
|
+
import { useGuidonStore } from '../store';
|
|
3
|
+
import { useGuidonPersistence } from '../persistence/hooks';
|
|
4
|
+
import { GuidonOverlay } from './GuidonOverlay';
|
|
5
|
+
import { GuidonTooltip } from './GuidonTooltip';
|
|
6
|
+
import type {
|
|
7
|
+
GuidonProviderProps,
|
|
8
|
+
GuidonConfig,
|
|
9
|
+
GuidonStore,
|
|
10
|
+
} from '../types';
|
|
11
|
+
|
|
12
|
+
interface GuidonContextValue {
|
|
13
|
+
start: () => void;
|
|
14
|
+
skip: () => void;
|
|
15
|
+
reset: () => void;
|
|
16
|
+
replay: () => Promise<void>;
|
|
17
|
+
isActive: boolean;
|
|
18
|
+
isCompleted: boolean;
|
|
19
|
+
isLoading: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const GuidonContext = createContext<GuidonContextValue | null>(null);
|
|
23
|
+
|
|
24
|
+
export function useGuidonContext() {
|
|
25
|
+
const context = useContext(GuidonContext);
|
|
26
|
+
if (!context) {
|
|
27
|
+
throw new Error('useGuidonContext must be used within a GuidonProvider');
|
|
28
|
+
}
|
|
29
|
+
return context;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function GuidonProvider({
|
|
33
|
+
children,
|
|
34
|
+
config,
|
|
35
|
+
autoStart = true,
|
|
36
|
+
shouldStart,
|
|
37
|
+
persistenceAdapter,
|
|
38
|
+
portalComponent: Portal,
|
|
39
|
+
renderTooltip,
|
|
40
|
+
tooltipLabels,
|
|
41
|
+
onBackdropPress,
|
|
42
|
+
}: GuidonProviderProps) {
|
|
43
|
+
const hasInitialized = useRef(false);
|
|
44
|
+
const isActive = useGuidonStore((state: GuidonStore) => state.isActive);
|
|
45
|
+
const storeIsCompleted = useGuidonStore((state: GuidonStore) => state.isCompleted);
|
|
46
|
+
const currentStepIndex = useGuidonStore((state: GuidonStore) => state.currentStepIndex);
|
|
47
|
+
const configure = useGuidonStore((state: GuidonStore) => state.configure);
|
|
48
|
+
const start = useGuidonStore((state: GuidonStore) => state.start);
|
|
49
|
+
const skip = useGuidonStore((state: GuidonStore) => state.skip);
|
|
50
|
+
const reset = useGuidonStore((state: GuidonStore) => state.reset);
|
|
51
|
+
|
|
52
|
+
const {
|
|
53
|
+
isLoading,
|
|
54
|
+
isCompleted: persistedCompleted,
|
|
55
|
+
markCompleted,
|
|
56
|
+
markStepViewed,
|
|
57
|
+
clearProgress,
|
|
58
|
+
} = useGuidonPersistence(persistenceAdapter, config.id);
|
|
59
|
+
|
|
60
|
+
const isCompleted = storeIsCompleted || persistedCompleted;
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
const enhancedConfig: GuidonConfig = {
|
|
64
|
+
...config,
|
|
65
|
+
onComplete: async () => {
|
|
66
|
+
config.onComplete?.();
|
|
67
|
+
if (persistenceAdapter) {
|
|
68
|
+
await markCompleted();
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
onSkip: async () => {
|
|
72
|
+
config.onSkip?.();
|
|
73
|
+
if (persistenceAdapter) {
|
|
74
|
+
await markStepViewed(currentStepIndex);
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
onStepChange: async (stepIndex, step) => {
|
|
78
|
+
config.onStepChange?.(stepIndex, step);
|
|
79
|
+
if (persistenceAdapter) {
|
|
80
|
+
await markStepViewed(stepIndex);
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
configure(enhancedConfig);
|
|
86
|
+
}, [config, configure, persistenceAdapter, markCompleted, markStepViewed, currentStepIndex]);
|
|
87
|
+
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
if (!autoStart || hasInitialized.current || isLoading) return;
|
|
90
|
+
|
|
91
|
+
const checkAndStart = async () => {
|
|
92
|
+
hasInitialized.current = true;
|
|
93
|
+
|
|
94
|
+
if (persistedCompleted) return;
|
|
95
|
+
|
|
96
|
+
if (shouldStart) {
|
|
97
|
+
const should = await shouldStart();
|
|
98
|
+
if (!should) return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
setTimeout(() => {
|
|
102
|
+
start();
|
|
103
|
+
}, 500);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
checkAndStart();
|
|
107
|
+
}, [autoStart, isLoading, persistedCompleted, shouldStart, start]);
|
|
108
|
+
|
|
109
|
+
const replay = useCallback(async () => {
|
|
110
|
+
if (persistenceAdapter) {
|
|
111
|
+
await clearProgress();
|
|
112
|
+
}
|
|
113
|
+
reset();
|
|
114
|
+
hasInitialized.current = false;
|
|
115
|
+
setTimeout(() => {
|
|
116
|
+
start();
|
|
117
|
+
}, 100);
|
|
118
|
+
}, [persistenceAdapter, clearProgress, reset, start]);
|
|
119
|
+
|
|
120
|
+
const manualStart = useCallback(() => {
|
|
121
|
+
if (!isActive && !isLoading) {
|
|
122
|
+
start();
|
|
123
|
+
}
|
|
124
|
+
}, [isActive, isLoading, start]);
|
|
125
|
+
|
|
126
|
+
const contextValue: GuidonContextValue = {
|
|
127
|
+
start: manualStart,
|
|
128
|
+
skip,
|
|
129
|
+
reset,
|
|
130
|
+
replay,
|
|
131
|
+
isActive,
|
|
132
|
+
isCompleted,
|
|
133
|
+
isLoading,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const overlayContent = (
|
|
137
|
+
<>
|
|
138
|
+
<GuidonOverlay
|
|
139
|
+
theme={config.theme}
|
|
140
|
+
animationDuration={config.animationDuration}
|
|
141
|
+
onBackdropPress={onBackdropPress}
|
|
142
|
+
/>
|
|
143
|
+
<GuidonTooltip
|
|
144
|
+
theme={config.theme}
|
|
145
|
+
animationDuration={config.animationDuration}
|
|
146
|
+
renderCustomTooltip={renderTooltip}
|
|
147
|
+
labels={tooltipLabels}
|
|
148
|
+
/>
|
|
149
|
+
</>
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<GuidonContext.Provider value={contextValue}>
|
|
154
|
+
{children}
|
|
155
|
+
{Portal ? <Portal>{overlayContent}</Portal> : overlayContent}
|
|
156
|
+
</GuidonContext.Provider>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
import { View, Platform, type LayoutChangeEvent } from 'react-native';
|
|
3
|
+
import { useGuidonStore } from '../store';
|
|
4
|
+
import type { GuidonTargetProps, TargetMeasurements, GuidonStore, GuidonStep } from '../types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Wrapper component that marks an element as a walkthrough target
|
|
8
|
+
* Automatically measures and reports its position to the walkthrough store
|
|
9
|
+
*/
|
|
10
|
+
export function GuidonTarget({
|
|
11
|
+
children,
|
|
12
|
+
targetId,
|
|
13
|
+
active = true,
|
|
14
|
+
}: GuidonTargetProps) {
|
|
15
|
+
const viewRef = useRef<View>(null);
|
|
16
|
+
const registerTarget = useGuidonStore((state: GuidonStore) => state.registerTarget);
|
|
17
|
+
const unregisterTarget = useGuidonStore((state: GuidonStore) => state.unregisterTarget);
|
|
18
|
+
const isActive = useGuidonStore((state: GuidonStore) => state.isActive);
|
|
19
|
+
const config = useGuidonStore((state: GuidonStore) => state.config);
|
|
20
|
+
|
|
21
|
+
// Check if this target is needed for the current walkthrough
|
|
22
|
+
const isTargetNeeded =
|
|
23
|
+
isActive && config?.steps.some((step: GuidonStep) => step.targetId === targetId);
|
|
24
|
+
|
|
25
|
+
const measureElement = useCallback(() => {
|
|
26
|
+
if (!viewRef.current || !active || !isTargetNeeded) return;
|
|
27
|
+
|
|
28
|
+
if (Platform.OS === 'web') {
|
|
29
|
+
// Web measurement using getBoundingClientRect
|
|
30
|
+
const element = viewRef.current as unknown as HTMLElement;
|
|
31
|
+
if (element && typeof element.getBoundingClientRect === 'function') {
|
|
32
|
+
const rect = element.getBoundingClientRect();
|
|
33
|
+
const measurements: TargetMeasurements = {
|
|
34
|
+
x: rect.left + window.scrollX,
|
|
35
|
+
y: rect.top + window.scrollY,
|
|
36
|
+
width: rect.width,
|
|
37
|
+
height: rect.height,
|
|
38
|
+
};
|
|
39
|
+
registerTarget(targetId, measurements);
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
// Native measurement using measureInWindow
|
|
43
|
+
viewRef.current.measureInWindow((x, y, width, height) => {
|
|
44
|
+
if (width > 0 && height > 0) {
|
|
45
|
+
const measurements: TargetMeasurements = { x, y, width, height };
|
|
46
|
+
registerTarget(targetId, measurements);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}, [targetId, active, isTargetNeeded, registerTarget]);
|
|
51
|
+
|
|
52
|
+
// Measure on layout change
|
|
53
|
+
const handleLayout = useCallback(
|
|
54
|
+
(_event: LayoutChangeEvent) => {
|
|
55
|
+
// Small delay to ensure layout is complete
|
|
56
|
+
requestAnimationFrame(() => {
|
|
57
|
+
measureElement();
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
[measureElement]
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// Re-measure when walkthrough becomes active or when this target becomes relevant
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (isTargetNeeded) {
|
|
66
|
+
// Delay to ensure the element is rendered
|
|
67
|
+
const timer = setTimeout(() => {
|
|
68
|
+
measureElement();
|
|
69
|
+
}, 100);
|
|
70
|
+
return () => clearTimeout(timer);
|
|
71
|
+
}
|
|
72
|
+
}, [isTargetNeeded, measureElement]);
|
|
73
|
+
|
|
74
|
+
// Re-measure on scroll (web only)
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (Platform.OS !== 'web' || !isTargetNeeded) return;
|
|
77
|
+
|
|
78
|
+
const handleScroll = () => {
|
|
79
|
+
measureElement();
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
window.addEventListener('scroll', handleScroll, true);
|
|
83
|
+
window.addEventListener('resize', handleScroll);
|
|
84
|
+
|
|
85
|
+
return () => {
|
|
86
|
+
window.removeEventListener('scroll', handleScroll, true);
|
|
87
|
+
window.removeEventListener('resize', handleScroll);
|
|
88
|
+
};
|
|
89
|
+
}, [isTargetNeeded, measureElement]);
|
|
90
|
+
|
|
91
|
+
// Unregister on unmount
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
return () => {
|
|
94
|
+
unregisterTarget(targetId);
|
|
95
|
+
};
|
|
96
|
+
}, [targetId, unregisterTarget]);
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<View
|
|
100
|
+
ref={viewRef}
|
|
101
|
+
onLayout={handleLayout}
|
|
102
|
+
collapsable={false}
|
|
103
|
+
style={{ alignSelf: 'flex-start' }}
|
|
104
|
+
>
|
|
105
|
+
{children}
|
|
106
|
+
</View>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
StyleSheet,
|
|
6
|
+
Dimensions,
|
|
7
|
+
TouchableOpacity,
|
|
8
|
+
Platform,
|
|
9
|
+
} from 'react-native';
|
|
10
|
+
import Animated, {
|
|
11
|
+
useAnimatedStyle,
|
|
12
|
+
withTiming,
|
|
13
|
+
withSpring,
|
|
14
|
+
Easing,
|
|
15
|
+
} from 'react-native-reanimated';
|
|
16
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
17
|
+
import { useGuidonStore } from '../store';
|
|
18
|
+
import type { GuidonTheme, TooltipPosition, TargetMeasurements, GuidonStore, GuidonStep } from '../types';
|
|
19
|
+
|
|
20
|
+
const DEFAULT_THEME: Required<
|
|
21
|
+
Pick<
|
|
22
|
+
GuidonTheme,
|
|
23
|
+
| 'tooltipBackgroundColor'
|
|
24
|
+
| 'tooltipBorderColor'
|
|
25
|
+
| 'tooltipBorderRadius'
|
|
26
|
+
| 'titleColor'
|
|
27
|
+
| 'descriptionColor'
|
|
28
|
+
| 'primaryColor'
|
|
29
|
+
| 'mutedColor'
|
|
30
|
+
| 'spotlightPadding'
|
|
31
|
+
>
|
|
32
|
+
> = {
|
|
33
|
+
tooltipBackgroundColor: '#ffffff',
|
|
34
|
+
tooltipBorderColor: '#e5e5e5',
|
|
35
|
+
tooltipBorderRadius: 12,
|
|
36
|
+
titleColor: '#1a1a1a',
|
|
37
|
+
descriptionColor: '#666666',
|
|
38
|
+
primaryColor: '#007AFF',
|
|
39
|
+
mutedColor: '#999999',
|
|
40
|
+
spotlightPadding: 8,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const TOOLTIP_MARGIN = 16;
|
|
44
|
+
const TOOLTIP_WIDTH = 300;
|
|
45
|
+
|
|
46
|
+
interface GuidonTooltipProps {
|
|
47
|
+
theme?: GuidonTheme;
|
|
48
|
+
animationDuration?: number;
|
|
49
|
+
renderCustomTooltip?: (props: {
|
|
50
|
+
step: GuidonStep;
|
|
51
|
+
currentIndex: number;
|
|
52
|
+
totalSteps: number;
|
|
53
|
+
onNext: () => void;
|
|
54
|
+
onPrevious: () => void;
|
|
55
|
+
onSkip: () => void;
|
|
56
|
+
}) => React.ReactNode;
|
|
57
|
+
labels?: {
|
|
58
|
+
next?: string;
|
|
59
|
+
previous?: string;
|
|
60
|
+
skip?: string;
|
|
61
|
+
finish?: string;
|
|
62
|
+
stepOf?: (current: number, total: number) => string;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function GuidonTooltip({
|
|
67
|
+
theme = {},
|
|
68
|
+
animationDuration = 300,
|
|
69
|
+
renderCustomTooltip,
|
|
70
|
+
labels = {},
|
|
71
|
+
}: GuidonTooltipProps) {
|
|
72
|
+
const insets = useSafeAreaInsets();
|
|
73
|
+
const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
|
|
74
|
+
|
|
75
|
+
const isActive = useGuidonStore((state: GuidonStore) => state.isActive);
|
|
76
|
+
const config = useGuidonStore((state: GuidonStore) => state.config);
|
|
77
|
+
const currentStepIndex = useGuidonStore((state: GuidonStore) => state.currentStepIndex);
|
|
78
|
+
const targetMeasurements = useGuidonStore((state: GuidonStore) => state.targetMeasurements);
|
|
79
|
+
const next = useGuidonStore((state: GuidonStore) => state.next);
|
|
80
|
+
const previous = useGuidonStore((state: GuidonStore) => state.previous);
|
|
81
|
+
const skip = useGuidonStore((state: GuidonStore) => state.skip);
|
|
82
|
+
|
|
83
|
+
const mergedTheme = { ...DEFAULT_THEME, ...theme };
|
|
84
|
+
const mergedLabels = {
|
|
85
|
+
next: labels.next ?? 'Next',
|
|
86
|
+
previous: labels.previous ?? 'Back',
|
|
87
|
+
skip: labels.skip ?? 'Skip',
|
|
88
|
+
finish: labels.finish ?? 'Finish',
|
|
89
|
+
stepOf: labels.stepOf ?? ((c: number, t: number) => `${c} of ${t}`),
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const currentStep = config?.steps[currentStepIndex];
|
|
93
|
+
const totalSteps = config?.steps.length ?? 0;
|
|
94
|
+
const isFirstStep = currentStepIndex === 0;
|
|
95
|
+
const isLastStep = currentStepIndex === totalSteps - 1;
|
|
96
|
+
|
|
97
|
+
const measurements: TargetMeasurements | undefined = currentStep?.targetId
|
|
98
|
+
? targetMeasurements[currentStep.targetId]
|
|
99
|
+
: undefined;
|
|
100
|
+
|
|
101
|
+
// Calculate tooltip position
|
|
102
|
+
const tooltipPosition = useMemo(() => {
|
|
103
|
+
if (!measurements) {
|
|
104
|
+
return { top: screenHeight / 2, left: screenWidth / 2 - TOOLTIP_WIDTH / 2 };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const targetCenterY = measurements.y + measurements.height / 2;
|
|
108
|
+
const targetBottom = measurements.y + measurements.height + mergedTheme.spotlightPadding;
|
|
109
|
+
const targetTop = measurements.y - mergedTheme.spotlightPadding;
|
|
110
|
+
|
|
111
|
+
// Determine preferred position
|
|
112
|
+
let position: TooltipPosition = currentStep?.tooltipPosition ?? 'auto';
|
|
113
|
+
|
|
114
|
+
if (position === 'auto') {
|
|
115
|
+
// Auto-detect best position
|
|
116
|
+
const spaceAbove = targetTop - insets.top;
|
|
117
|
+
const spaceBelow = screenHeight - targetBottom - insets.bottom;
|
|
118
|
+
|
|
119
|
+
position = spaceBelow >= 200 ? 'bottom' : spaceAbove >= 200 ? 'top' : 'bottom';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let top: number;
|
|
123
|
+
let left: number = Math.max(
|
|
124
|
+
TOOLTIP_MARGIN,
|
|
125
|
+
Math.min(
|
|
126
|
+
measurements.x + measurements.width / 2 - TOOLTIP_WIDTH / 2,
|
|
127
|
+
screenWidth - TOOLTIP_WIDTH - TOOLTIP_MARGIN
|
|
128
|
+
)
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
if (position === 'top') {
|
|
132
|
+
top = targetTop - TOOLTIP_MARGIN - 150; // Approximate tooltip height
|
|
133
|
+
} else {
|
|
134
|
+
top = targetBottom + TOOLTIP_MARGIN;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Ensure tooltip is within screen bounds
|
|
138
|
+
top = Math.max(insets.top + TOOLTIP_MARGIN, Math.min(top, screenHeight - 200 - insets.bottom));
|
|
139
|
+
|
|
140
|
+
return { top, left, position };
|
|
141
|
+
}, [measurements, screenWidth, screenHeight, insets, currentStep?.tooltipPosition, mergedTheme.spotlightPadding]);
|
|
142
|
+
|
|
143
|
+
// Animated styles
|
|
144
|
+
const animatedStyle = useAnimatedStyle(() => {
|
|
145
|
+
return {
|
|
146
|
+
opacity: withTiming(isActive && measurements ? 1 : 0, {
|
|
147
|
+
duration: animationDuration,
|
|
148
|
+
easing: Easing.inOut(Easing.ease),
|
|
149
|
+
}),
|
|
150
|
+
transform: [
|
|
151
|
+
{
|
|
152
|
+
translateY: withSpring(isActive && measurements ? 0 : 20, {
|
|
153
|
+
damping: 15,
|
|
154
|
+
stiffness: 150,
|
|
155
|
+
}),
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
};
|
|
159
|
+
}, [isActive, measurements, animationDuration]);
|
|
160
|
+
|
|
161
|
+
if (!isActive || !currentStep || !measurements) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Render custom tooltip if provided
|
|
166
|
+
if (renderCustomTooltip) {
|
|
167
|
+
return (
|
|
168
|
+
<Animated.View
|
|
169
|
+
style={[
|
|
170
|
+
styles.tooltipContainer,
|
|
171
|
+
{
|
|
172
|
+
top: tooltipPosition.top,
|
|
173
|
+
left: tooltipPosition.left,
|
|
174
|
+
width: TOOLTIP_WIDTH,
|
|
175
|
+
},
|
|
176
|
+
animatedStyle,
|
|
177
|
+
]}
|
|
178
|
+
>
|
|
179
|
+
{renderCustomTooltip({
|
|
180
|
+
step: currentStep,
|
|
181
|
+
currentIndex: currentStepIndex,
|
|
182
|
+
totalSteps,
|
|
183
|
+
onNext: next,
|
|
184
|
+
onPrevious: previous,
|
|
185
|
+
onSkip: skip,
|
|
186
|
+
})}
|
|
187
|
+
</Animated.View>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Default tooltip UI
|
|
192
|
+
return (
|
|
193
|
+
<Animated.View
|
|
194
|
+
style={[
|
|
195
|
+
styles.tooltipContainer,
|
|
196
|
+
{
|
|
197
|
+
top: tooltipPosition.top,
|
|
198
|
+
left: tooltipPosition.left,
|
|
199
|
+
width: TOOLTIP_WIDTH,
|
|
200
|
+
backgroundColor: mergedTheme.tooltipBackgroundColor,
|
|
201
|
+
borderColor: mergedTheme.tooltipBorderColor,
|
|
202
|
+
borderRadius: mergedTheme.tooltipBorderRadius,
|
|
203
|
+
},
|
|
204
|
+
animatedStyle,
|
|
205
|
+
]}
|
|
206
|
+
>
|
|
207
|
+
{/* Progress indicator */}
|
|
208
|
+
<View style={styles.progressContainer}>
|
|
209
|
+
<Text style={[styles.progressText, { color: mergedTheme.mutedColor }]}>
|
|
210
|
+
{mergedLabels.stepOf(currentStepIndex + 1, totalSteps)}
|
|
211
|
+
</Text>
|
|
212
|
+
<View style={styles.progressDots}>
|
|
213
|
+
{Array.from({ length: totalSteps }).map((_, i) => (
|
|
214
|
+
<View
|
|
215
|
+
key={i}
|
|
216
|
+
style={[
|
|
217
|
+
styles.progressDot,
|
|
218
|
+
{
|
|
219
|
+
backgroundColor:
|
|
220
|
+
i === currentStepIndex
|
|
221
|
+
? mergedTheme.primaryColor
|
|
222
|
+
: mergedTheme.mutedColor,
|
|
223
|
+
opacity: i === currentStepIndex ? 1 : 0.3,
|
|
224
|
+
},
|
|
225
|
+
]}
|
|
226
|
+
/>
|
|
227
|
+
))}
|
|
228
|
+
</View>
|
|
229
|
+
</View>
|
|
230
|
+
|
|
231
|
+
{/* Content */}
|
|
232
|
+
<View style={styles.content}>
|
|
233
|
+
<Text style={[styles.title, { color: mergedTheme.titleColor }]}>
|
|
234
|
+
{currentStep.title}
|
|
235
|
+
</Text>
|
|
236
|
+
<Text style={[styles.description, { color: mergedTheme.descriptionColor }]}>
|
|
237
|
+
{currentStep.description}
|
|
238
|
+
</Text>
|
|
239
|
+
{currentStep.customContent}
|
|
240
|
+
</View>
|
|
241
|
+
|
|
242
|
+
{/* Navigation buttons */}
|
|
243
|
+
<View style={styles.buttonContainer}>
|
|
244
|
+
<TouchableOpacity onPress={skip} style={styles.skipButton}>
|
|
245
|
+
<Text style={[styles.skipText, { color: mergedTheme.mutedColor }]}>
|
|
246
|
+
{mergedLabels.skip}
|
|
247
|
+
</Text>
|
|
248
|
+
</TouchableOpacity>
|
|
249
|
+
|
|
250
|
+
<View style={styles.navButtons}>
|
|
251
|
+
{!isFirstStep && (
|
|
252
|
+
<TouchableOpacity
|
|
253
|
+
onPress={previous}
|
|
254
|
+
style={[styles.navButton, styles.backButton]}
|
|
255
|
+
>
|
|
256
|
+
<Text style={[styles.backButtonText, { color: mergedTheme.primaryColor }]}>
|
|
257
|
+
{mergedLabels.previous}
|
|
258
|
+
</Text>
|
|
259
|
+
</TouchableOpacity>
|
|
260
|
+
)}
|
|
261
|
+
<TouchableOpacity
|
|
262
|
+
onPress={next}
|
|
263
|
+
style={[
|
|
264
|
+
styles.navButton,
|
|
265
|
+
styles.nextButton,
|
|
266
|
+
{ backgroundColor: mergedTheme.primaryColor },
|
|
267
|
+
]}
|
|
268
|
+
>
|
|
269
|
+
<Text style={styles.nextButtonText}>
|
|
270
|
+
{isLastStep ? mergedLabels.finish : mergedLabels.next}
|
|
271
|
+
</Text>
|
|
272
|
+
</TouchableOpacity>
|
|
273
|
+
</View>
|
|
274
|
+
</View>
|
|
275
|
+
</Animated.View>
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const styles = StyleSheet.create({
|
|
280
|
+
tooltipContainer: {
|
|
281
|
+
position: 'absolute',
|
|
282
|
+
zIndex: 1000,
|
|
283
|
+
borderWidth: 1,
|
|
284
|
+
padding: 16,
|
|
285
|
+
...Platform.select({
|
|
286
|
+
ios: {
|
|
287
|
+
shadowColor: '#000',
|
|
288
|
+
shadowOffset: { width: 0, height: 4 },
|
|
289
|
+
shadowOpacity: 0.15,
|
|
290
|
+
shadowRadius: 12,
|
|
291
|
+
},
|
|
292
|
+
android: {
|
|
293
|
+
elevation: 8,
|
|
294
|
+
},
|
|
295
|
+
web: {
|
|
296
|
+
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
|
297
|
+
} as any,
|
|
298
|
+
}),
|
|
299
|
+
},
|
|
300
|
+
progressContainer: {
|
|
301
|
+
flexDirection: 'row',
|
|
302
|
+
alignItems: 'center',
|
|
303
|
+
justifyContent: 'space-between',
|
|
304
|
+
marginBottom: 12,
|
|
305
|
+
},
|
|
306
|
+
progressText: {
|
|
307
|
+
fontSize: 12,
|
|
308
|
+
fontWeight: '500',
|
|
309
|
+
},
|
|
310
|
+
progressDots: {
|
|
311
|
+
flexDirection: 'row',
|
|
312
|
+
gap: 4,
|
|
313
|
+
},
|
|
314
|
+
progressDot: {
|
|
315
|
+
width: 6,
|
|
316
|
+
height: 6,
|
|
317
|
+
borderRadius: 3,
|
|
318
|
+
},
|
|
319
|
+
content: {
|
|
320
|
+
marginBottom: 16,
|
|
321
|
+
},
|
|
322
|
+
title: {
|
|
323
|
+
fontSize: 18,
|
|
324
|
+
fontWeight: '600',
|
|
325
|
+
marginBottom: 8,
|
|
326
|
+
},
|
|
327
|
+
description: {
|
|
328
|
+
fontSize: 14,
|
|
329
|
+
lineHeight: 20,
|
|
330
|
+
},
|
|
331
|
+
buttonContainer: {
|
|
332
|
+
flexDirection: 'row',
|
|
333
|
+
alignItems: 'center',
|
|
334
|
+
justifyContent: 'space-between',
|
|
335
|
+
},
|
|
336
|
+
skipButton: {
|
|
337
|
+
paddingVertical: 8,
|
|
338
|
+
paddingHorizontal: 4,
|
|
339
|
+
},
|
|
340
|
+
skipText: {
|
|
341
|
+
fontSize: 14,
|
|
342
|
+
},
|
|
343
|
+
navButtons: {
|
|
344
|
+
flexDirection: 'row',
|
|
345
|
+
gap: 8,
|
|
346
|
+
},
|
|
347
|
+
navButton: {
|
|
348
|
+
paddingVertical: 10,
|
|
349
|
+
paddingHorizontal: 16,
|
|
350
|
+
borderRadius: 8,
|
|
351
|
+
},
|
|
352
|
+
backButton: {
|
|
353
|
+
backgroundColor: 'transparent',
|
|
354
|
+
},
|
|
355
|
+
backButtonText: {
|
|
356
|
+
fontSize: 14,
|
|
357
|
+
fontWeight: '600',
|
|
358
|
+
},
|
|
359
|
+
nextButton: {},
|
|
360
|
+
nextButtonText: {
|
|
361
|
+
color: '#ffffff',
|
|
362
|
+
fontSize: 14,
|
|
363
|
+
fontWeight: '600',
|
|
364
|
+
},
|
|
365
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Components
|
|
2
|
+
export {
|
|
3
|
+
GuidonTarget,
|
|
4
|
+
GuidonOverlay,
|
|
5
|
+
GuidonTooltip,
|
|
6
|
+
GuidonProvider,
|
|
7
|
+
useGuidonContext,
|
|
8
|
+
} from "./components";
|
|
9
|
+
|
|
10
|
+
// Store and API
|
|
11
|
+
export {
|
|
12
|
+
useGuidonStore,
|
|
13
|
+
Guidon,
|
|
14
|
+
useGuidonActive,
|
|
15
|
+
useGuidonStep,
|
|
16
|
+
useGuidonProgress,
|
|
17
|
+
useTargetMeasurements,
|
|
18
|
+
} from "./store";
|
|
19
|
+
|
|
20
|
+
// Types
|
|
21
|
+
export type {
|
|
22
|
+
GuidonStep,
|
|
23
|
+
GuidonConfig,
|
|
24
|
+
GuidonTheme,
|
|
25
|
+
GuidonProgress,
|
|
26
|
+
GuidonPersistenceAdapter,
|
|
27
|
+
GuidonProviderProps,
|
|
28
|
+
GuidonTargetProps,
|
|
29
|
+
GuidonTooltipLabels,
|
|
30
|
+
GuidonTooltipRenderProps,
|
|
31
|
+
TooltipPosition,
|
|
32
|
+
TargetMeasurements,
|
|
33
|
+
GuidonState,
|
|
34
|
+
GuidonActions,
|
|
35
|
+
GuidonStore,
|
|
36
|
+
} from "./types";
|
|
37
|
+
|
|
38
|
+
// Persistence adapters
|
|
39
|
+
export {
|
|
40
|
+
createNoopAdapter,
|
|
41
|
+
createMemoryAdapter,
|
|
42
|
+
createLocalStorageAdapter,
|
|
43
|
+
createAsyncStorageAdapter,
|
|
44
|
+
createApiAdapter,
|
|
45
|
+
createCompositeAdapter,
|
|
46
|
+
} from "./persistence/adapters";
|
|
47
|
+
|
|
48
|
+
export {
|
|
49
|
+
useGuidonPersistence,
|
|
50
|
+
useShouldShowGuidon,
|
|
51
|
+
} from "./persistence/hooks";
|