@korsolutions/guidon 1.0.1 → 1.0.3
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/dist/commonjs/babel.config.js +15 -0
- package/dist/commonjs/babel.config.js.map +1 -0
- package/dist/commonjs/bob.config.js +11 -0
- package/dist/commonjs/bob.config.js.map +1 -0
- package/dist/commonjs/components/GuidonOverlay.js +206 -0
- package/dist/commonjs/components/GuidonOverlay.js.map +1 -0
- package/dist/commonjs/components/GuidonProvider.js +157 -0
- package/dist/commonjs/components/GuidonProvider.js.map +1 -0
- package/dist/commonjs/components/GuidonTarget.js +110 -0
- package/dist/commonjs/components/GuidonTarget.js.map +1 -0
- package/dist/commonjs/components/GuidonTooltip.js +422 -0
- package/dist/commonjs/components/GuidonTooltip.js.map +1 -0
- package/dist/commonjs/components/index.js +40 -0
- package/dist/commonjs/components/index.js.map +1 -0
- package/dist/commonjs/hooks/index.js +13 -0
- package/dist/commonjs/hooks/index.js.map +1 -0
- package/dist/commonjs/hooks/useGuidonRef.js +132 -0
- package/dist/commonjs/hooks/useGuidonRef.js.map +1 -0
- package/dist/commonjs/index.js +143 -0
- package/dist/commonjs/index.js.map +1 -0
- package/dist/commonjs/package.json +1 -0
- package/dist/commonjs/persistence/adapters.js +213 -0
- package/dist/commonjs/persistence/adapters.js.map +1 -0
- package/dist/commonjs/persistence/hooks.js +153 -0
- package/dist/commonjs/persistence/hooks.js.map +1 -0
- package/dist/commonjs/persistence/index.js +28 -0
- package/dist/commonjs/persistence/index.js.map +1 -0
- package/dist/commonjs/store.js +314 -0
- package/dist/commonjs/store.js.map +1 -0
- package/dist/commonjs/tsconfig.json +32 -0
- package/dist/commonjs/types.js +6 -0
- package/dist/commonjs/types.js.map +1 -0
- package/dist/module/babel.config.js +15 -0
- package/dist/module/babel.config.js.map +1 -0
- package/dist/module/bob.config.js +11 -0
- package/dist/module/bob.config.js.map +1 -0
- package/dist/module/components/GuidonOverlay.js +201 -0
- package/dist/module/components/GuidonOverlay.js.map +1 -0
- package/dist/module/components/GuidonProvider.js +152 -0
- package/dist/module/components/GuidonProvider.js.map +1 -0
- package/dist/module/components/GuidonTarget.js +106 -0
- package/dist/module/components/GuidonTarget.js.map +1 -0
- package/dist/module/components/GuidonTooltip.js +417 -0
- package/dist/module/components/GuidonTooltip.js.map +1 -0
- package/dist/module/components/index.js +7 -0
- package/dist/module/components/index.js.map +1 -0
- package/dist/module/hooks/index.js +4 -0
- package/dist/module/hooks/index.js.map +1 -0
- package/dist/module/hooks/useGuidonRef.js +129 -0
- package/dist/module/hooks/useGuidonRef.js.map +1 -0
- package/dist/module/index.js +17 -0
- package/dist/module/index.js.map +1 -0
- package/dist/module/package.json +1 -0
- package/dist/module/persistence/adapters.js +203 -0
- package/dist/module/persistence/adapters.js.map +1 -0
- package/dist/module/persistence/hooks.js +148 -0
- package/dist/module/persistence/hooks.js.map +1 -0
- package/dist/module/persistence/index.js +5 -0
- package/dist/module/persistence/index.js.map +1 -0
- package/dist/module/store.js +304 -0
- package/dist/module/store.js.map +1 -0
- package/dist/module/tsconfig.json +32 -0
- package/dist/module/types.js +4 -0
- package/dist/module/types.js.map +1 -0
- package/dist/typescript/commonjs/components/GuidonOverlay.d.ts +9 -0
- package/dist/typescript/commonjs/components/GuidonOverlay.d.ts.map +1 -0
- package/dist/typescript/commonjs/components/GuidonProvider.d.ts +14 -0
- package/dist/typescript/commonjs/components/GuidonProvider.d.ts.map +1 -0
- package/dist/typescript/commonjs/components/GuidonTarget.d.ts +7 -0
- package/dist/typescript/commonjs/components/GuidonTarget.d.ts.map +1 -0
- package/dist/typescript/commonjs/components/GuidonTooltip.d.ts +24 -0
- package/dist/typescript/commonjs/components/GuidonTooltip.d.ts.map +1 -0
- package/dist/typescript/commonjs/components/index.d.ts +5 -0
- package/dist/typescript/commonjs/components/index.d.ts.map +1 -0
- package/dist/typescript/commonjs/hooks/index.d.ts +2 -0
- package/dist/typescript/commonjs/hooks/index.d.ts.map +1 -0
- package/dist/typescript/commonjs/hooks/useGuidonRef.d.ts +35 -0
- package/dist/typescript/commonjs/hooks/useGuidonRef.d.ts.map +1 -0
- package/dist/typescript/commonjs/index.d.ts +7 -0
- package/dist/typescript/commonjs/index.d.ts.map +1 -0
- package/dist/typescript/commonjs/package.json +1 -0
- package/dist/typescript/commonjs/persistence/adapters.d.ts +57 -0
- package/dist/typescript/commonjs/persistence/adapters.d.ts.map +1 -0
- package/dist/typescript/commonjs/persistence/hooks.d.ts +29 -0
- package/dist/typescript/commonjs/persistence/hooks.d.ts.map +1 -0
- package/dist/typescript/commonjs/persistence/index.d.ts +3 -0
- package/dist/typescript/commonjs/persistence/index.d.ts.map +1 -0
- package/dist/typescript/commonjs/store.d.ts +89 -0
- package/dist/typescript/commonjs/store.d.ts.map +1 -0
- package/dist/{index-D_JFvCIg.d.mts → typescript/commonjs/types.d.ts} +40 -104
- package/dist/typescript/commonjs/types.d.ts.map +1 -0
- package/dist/typescript/module/components/GuidonOverlay.d.ts +9 -0
- package/dist/typescript/module/components/GuidonOverlay.d.ts.map +1 -0
- package/dist/typescript/module/components/GuidonProvider.d.ts +14 -0
- package/dist/typescript/module/components/GuidonProvider.d.ts.map +1 -0
- package/dist/typescript/module/components/GuidonTarget.d.ts +7 -0
- package/dist/typescript/module/components/GuidonTarget.d.ts.map +1 -0
- package/dist/typescript/module/components/GuidonTooltip.d.ts +24 -0
- package/dist/typescript/module/components/GuidonTooltip.d.ts.map +1 -0
- package/dist/typescript/module/components/index.d.ts +5 -0
- package/dist/typescript/module/components/index.d.ts.map +1 -0
- package/dist/typescript/module/hooks/index.d.ts +2 -0
- package/dist/typescript/module/hooks/index.d.ts.map +1 -0
- package/dist/typescript/module/hooks/useGuidonRef.d.ts +35 -0
- package/dist/typescript/module/hooks/useGuidonRef.d.ts.map +1 -0
- package/dist/typescript/module/index.d.ts +7 -0
- package/dist/typescript/module/index.d.ts.map +1 -0
- package/dist/typescript/module/package.json +1 -0
- package/dist/typescript/module/persistence/adapters.d.ts +57 -0
- package/dist/typescript/module/persistence/adapters.d.ts.map +1 -0
- package/dist/typescript/module/persistence/hooks.d.ts +29 -0
- package/dist/typescript/module/persistence/hooks.d.ts.map +1 -0
- package/dist/typescript/module/persistence/index.d.ts +3 -0
- package/dist/typescript/module/persistence/index.d.ts.map +1 -0
- package/dist/typescript/module/store.d.ts +89 -0
- package/dist/typescript/module/store.d.ts.map +1 -0
- package/dist/{index-D_JFvCIg.d.ts → typescript/module/types.d.ts} +40 -104
- package/dist/typescript/module/types.d.ts.map +1 -0
- package/package.json +25 -13
- package/src/babel.config.js +18 -0
- package/src/bob.config.js +14 -0
- package/src/components/GuidonOverlay.tsx +60 -4
- package/src/components/GuidonProvider.tsx +29 -1
- package/src/components/GuidonTarget.tsx +41 -25
- package/src/components/GuidonTooltip.tsx +143 -9
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useGuidonRef.ts +154 -0
- package/src/index.ts +6 -0
- package/src/store.ts +68 -15
- package/src/tsconfig.json +32 -0
- package/src/types.ts +32 -2
- package/dist/index.d.mts +0 -128
- package/dist/index.d.ts +0 -128
- package/dist/index.js +0 -1097
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -1072
- package/dist/index.mjs.map +0 -1
- package/dist/persistence/index.d.mts +0 -2
- package/dist/persistence/index.d.ts +0 -2
- package/dist/persistence/index.js +0 -300
- package/dist/persistence/index.js.map +0 -1
- package/dist/persistence/index.mjs +0 -291
- package/dist/persistence/index.mjs.map +0 -1
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import { useCallback, useEffect, useRef } from
|
|
2
|
-
import { View, Platform, type LayoutChangeEvent } from
|
|
3
|
-
import { useGuidonStore } from
|
|
4
|
-
import type {
|
|
1
|
+
import { useCallback, useEffect, useRef, type ComponentRef } from "react";
|
|
2
|
+
import { View, Platform, type LayoutChangeEvent } from "react-native";
|
|
3
|
+
import { useGuidonStore } from "../store";
|
|
4
|
+
import type {
|
|
5
|
+
GuidonTargetProps,
|
|
6
|
+
TargetMeasurements,
|
|
7
|
+
GuidonStore,
|
|
8
|
+
GuidonStep,
|
|
9
|
+
} from "../types";
|
|
5
10
|
|
|
6
11
|
/**
|
|
7
12
|
* Wrapper component that marks an element as a walkthrough target
|
|
@@ -12,23 +17,28 @@ export function GuidonTarget({
|
|
|
12
17
|
targetId,
|
|
13
18
|
active = true,
|
|
14
19
|
}: GuidonTargetProps) {
|
|
15
|
-
const viewRef = useRef<View
|
|
16
|
-
const registerTarget = useGuidonStore(
|
|
17
|
-
|
|
20
|
+
const viewRef = useRef<ComponentRef<typeof View>>(null);
|
|
21
|
+
const registerTarget = useGuidonStore(
|
|
22
|
+
(state: GuidonStore) => state.registerTarget,
|
|
23
|
+
);
|
|
24
|
+
const unregisterTarget = useGuidonStore(
|
|
25
|
+
(state: GuidonStore) => state.unregisterTarget,
|
|
26
|
+
);
|
|
18
27
|
const isActive = useGuidonStore((state: GuidonStore) => state.isActive);
|
|
19
28
|
const config = useGuidonStore((state: GuidonStore) => state.config);
|
|
20
29
|
|
|
21
30
|
// Check if this target is needed for the current walkthrough
|
|
22
31
|
const isTargetNeeded =
|
|
23
|
-
isActive &&
|
|
32
|
+
isActive &&
|
|
33
|
+
config?.steps.some((step: GuidonStep) => step.targetId === targetId);
|
|
24
34
|
|
|
25
35
|
const measureElement = useCallback(() => {
|
|
26
36
|
if (!viewRef.current || !active || !isTargetNeeded) return;
|
|
27
37
|
|
|
28
|
-
if (Platform.OS ===
|
|
38
|
+
if (Platform.OS === "web") {
|
|
29
39
|
// Web measurement using getBoundingClientRect
|
|
30
40
|
const element = viewRef.current as unknown as HTMLElement;
|
|
31
|
-
if (element && typeof element.getBoundingClientRect ===
|
|
41
|
+
if (element && typeof element.getBoundingClientRect === "function") {
|
|
32
42
|
const rect = element.getBoundingClientRect();
|
|
33
43
|
const measurements: TargetMeasurements = {
|
|
34
44
|
x: rect.left + window.scrollX,
|
|
@@ -40,7 +50,13 @@ export function GuidonTarget({
|
|
|
40
50
|
}
|
|
41
51
|
} else {
|
|
42
52
|
// Native measurement using measureInWindow
|
|
43
|
-
|
|
53
|
+
// Cast to access the native measureInWindow method
|
|
54
|
+
const nativeRef = viewRef.current as unknown as {
|
|
55
|
+
measureInWindow: (
|
|
56
|
+
callback: (x: number, y: number, width: number, height: number) => void
|
|
57
|
+
) => void;
|
|
58
|
+
};
|
|
59
|
+
nativeRef.measureInWindow((x, y, width, height) => {
|
|
44
60
|
if (width > 0 && height > 0) {
|
|
45
61
|
const measurements: TargetMeasurements = { x, y, width, height };
|
|
46
62
|
registerTarget(targetId, measurements);
|
|
@@ -57,34 +73,34 @@ export function GuidonTarget({
|
|
|
57
73
|
measureElement();
|
|
58
74
|
});
|
|
59
75
|
},
|
|
60
|
-
[measureElement]
|
|
76
|
+
[measureElement],
|
|
61
77
|
);
|
|
62
78
|
|
|
63
79
|
// Re-measure when walkthrough becomes active or when this target becomes relevant
|
|
64
80
|
useEffect(() => {
|
|
65
|
-
if (isTargetNeeded)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
81
|
+
if (!isTargetNeeded) return;
|
|
82
|
+
|
|
83
|
+
// Delay to ensure the element is rendered
|
|
84
|
+
const timer = setTimeout(() => {
|
|
85
|
+
measureElement();
|
|
86
|
+
}, 100);
|
|
87
|
+
return () => clearTimeout(timer);
|
|
72
88
|
}, [isTargetNeeded, measureElement]);
|
|
73
89
|
|
|
74
90
|
// Re-measure on scroll (web only)
|
|
75
91
|
useEffect(() => {
|
|
76
|
-
if (Platform.OS !==
|
|
92
|
+
if (Platform.OS !== "web" || !isTargetNeeded) return;
|
|
77
93
|
|
|
78
94
|
const handleScroll = () => {
|
|
79
95
|
measureElement();
|
|
80
96
|
};
|
|
81
97
|
|
|
82
|
-
window.addEventListener(
|
|
83
|
-
window.addEventListener(
|
|
98
|
+
window.addEventListener("scroll", handleScroll, true);
|
|
99
|
+
window.addEventListener("resize", handleScroll);
|
|
84
100
|
|
|
85
101
|
return () => {
|
|
86
|
-
window.removeEventListener(
|
|
87
|
-
window.removeEventListener(
|
|
102
|
+
window.removeEventListener("scroll", handleScroll, true);
|
|
103
|
+
window.removeEventListener("resize", handleScroll);
|
|
88
104
|
};
|
|
89
105
|
}, [isTargetNeeded, measureElement]);
|
|
90
106
|
|
|
@@ -100,7 +116,7 @@ export function GuidonTarget({
|
|
|
100
116
|
ref={viewRef}
|
|
101
117
|
onLayout={handleLayout}
|
|
102
118
|
collapsable={false}
|
|
103
|
-
|
|
119
|
+
style={{ alignSelf: "flex-start" }}
|
|
104
120
|
>
|
|
105
121
|
{children}
|
|
106
122
|
</View>
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
Dimensions,
|
|
7
7
|
TouchableOpacity,
|
|
8
8
|
Platform,
|
|
9
|
+
ActivityIndicator,
|
|
9
10
|
} from 'react-native';
|
|
10
11
|
import Animated, {
|
|
11
12
|
useAnimatedStyle,
|
|
@@ -14,8 +15,8 @@ import Animated, {
|
|
|
14
15
|
Easing,
|
|
15
16
|
} from 'react-native-reanimated';
|
|
16
17
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
17
|
-
import { useGuidonStore } from '../store';
|
|
18
|
-
import type { GuidonTheme, TooltipPosition, TargetMeasurements, GuidonStore, GuidonStep } from '../types';
|
|
18
|
+
import { useGuidonStore, useWaitingState, useIsFloatingStep } from '../store';
|
|
19
|
+
import type { GuidonTheme, TooltipPosition, TargetMeasurements, GuidonStore, GuidonStep, FloatingPosition } from '../types';
|
|
19
20
|
|
|
20
21
|
const DEFAULT_THEME: Required<
|
|
21
22
|
Pick<
|
|
@@ -42,6 +43,67 @@ const DEFAULT_THEME: Required<
|
|
|
42
43
|
|
|
43
44
|
const TOOLTIP_MARGIN = 16;
|
|
44
45
|
const TOOLTIP_WIDTH = 300;
|
|
46
|
+
const ESTIMATED_TOOLTIP_HEIGHT = 200;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Calculate position for floating tooltips (steps without a target element)
|
|
50
|
+
*/
|
|
51
|
+
function calculateFloatingPosition(
|
|
52
|
+
floatingPosition: FloatingPosition,
|
|
53
|
+
screenWidth: number,
|
|
54
|
+
screenHeight: number,
|
|
55
|
+
tooltipWidth: number,
|
|
56
|
+
insets: { top: number; bottom: number; left: number; right: number }
|
|
57
|
+
): { top: number; left: number } {
|
|
58
|
+
const MARGIN = 32;
|
|
59
|
+
|
|
60
|
+
const centerX = (screenWidth - tooltipWidth) / 2;
|
|
61
|
+
const centerY = (screenHeight - ESTIMATED_TOOLTIP_HEIGHT) / 2;
|
|
62
|
+
|
|
63
|
+
switch (floatingPosition) {
|
|
64
|
+
case 'center':
|
|
65
|
+
return { top: centerY, left: centerX };
|
|
66
|
+
|
|
67
|
+
case 'top':
|
|
68
|
+
return {
|
|
69
|
+
top: insets.top + MARGIN,
|
|
70
|
+
left: centerX,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
case 'bottom':
|
|
74
|
+
return {
|
|
75
|
+
top: screenHeight - ESTIMATED_TOOLTIP_HEIGHT - insets.bottom - MARGIN,
|
|
76
|
+
left: centerX,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
case 'top-left':
|
|
80
|
+
return {
|
|
81
|
+
top: insets.top + MARGIN,
|
|
82
|
+
left: MARGIN,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
case 'top-right':
|
|
86
|
+
return {
|
|
87
|
+
top: insets.top + MARGIN,
|
|
88
|
+
left: screenWidth - tooltipWidth - MARGIN,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
case 'bottom-left':
|
|
92
|
+
return {
|
|
93
|
+
top: screenHeight - ESTIMATED_TOOLTIP_HEIGHT - insets.bottom - MARGIN,
|
|
94
|
+
left: MARGIN,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
case 'bottom-right':
|
|
98
|
+
return {
|
|
99
|
+
top: screenHeight - ESTIMATED_TOOLTIP_HEIGHT - insets.bottom - MARGIN,
|
|
100
|
+
left: screenWidth - tooltipWidth - MARGIN,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
default:
|
|
104
|
+
return { top: centerY, left: centerX };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
45
107
|
|
|
46
108
|
interface GuidonTooltipProps {
|
|
47
109
|
theme?: GuidonTheme;
|
|
@@ -60,6 +122,7 @@ interface GuidonTooltipProps {
|
|
|
60
122
|
skip?: string;
|
|
61
123
|
finish?: string;
|
|
62
124
|
stepOf?: (current: number, total: number) => string;
|
|
125
|
+
waitingDefault?: string;
|
|
63
126
|
};
|
|
64
127
|
}
|
|
65
128
|
|
|
@@ -80,6 +143,11 @@ export function GuidonTooltip({
|
|
|
80
143
|
const previous = useGuidonStore((state: GuidonStore) => state.previous);
|
|
81
144
|
const skip = useGuidonStore((state: GuidonStore) => state.skip);
|
|
82
145
|
|
|
146
|
+
// Check for floating or waiting states
|
|
147
|
+
const isFloatingStep = useIsFloatingStep();
|
|
148
|
+
const waitingState = useWaitingState();
|
|
149
|
+
const isWaiting = waitingState?.isWaiting ?? false;
|
|
150
|
+
|
|
83
151
|
const mergedTheme = { ...DEFAULT_THEME, ...theme };
|
|
84
152
|
const mergedLabels = {
|
|
85
153
|
next: labels.next ?? 'Next',
|
|
@@ -87,6 +155,7 @@ export function GuidonTooltip({
|
|
|
87
155
|
skip: labels.skip ?? 'Skip',
|
|
88
156
|
finish: labels.finish ?? 'Finish',
|
|
89
157
|
stepOf: labels.stepOf ?? ((c: number, t: number) => `${c} of ${t}`),
|
|
158
|
+
waitingDefault: labels.waitingDefault ?? 'Navigate to continue...',
|
|
90
159
|
};
|
|
91
160
|
|
|
92
161
|
const currentStep = config?.steps[currentStepIndex];
|
|
@@ -100,11 +169,23 @@ export function GuidonTooltip({
|
|
|
100
169
|
|
|
101
170
|
// Calculate tooltip position
|
|
102
171
|
const tooltipPosition = useMemo(() => {
|
|
172
|
+
// Handle floating steps (no target element)
|
|
173
|
+
if (isFloatingStep) {
|
|
174
|
+
const floatingPos = currentStep?.floatingPosition ?? 'center';
|
|
175
|
+
return calculateFloatingPosition(
|
|
176
|
+
floatingPos,
|
|
177
|
+
screenWidth,
|
|
178
|
+
screenHeight,
|
|
179
|
+
TOOLTIP_WIDTH,
|
|
180
|
+
insets
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Handle waiting state or no measurements - center the tooltip
|
|
103
185
|
if (!measurements) {
|
|
104
|
-
return { top: screenHeight / 2, left: screenWidth / 2 - TOOLTIP_WIDTH / 2 };
|
|
186
|
+
return { top: screenHeight / 2 - ESTIMATED_TOOLTIP_HEIGHT / 2, left: screenWidth / 2 - TOOLTIP_WIDTH / 2 };
|
|
105
187
|
}
|
|
106
188
|
|
|
107
|
-
const targetCenterY = measurements.y + measurements.height / 2;
|
|
108
189
|
const targetBottom = measurements.y + measurements.height + mergedTheme.spotlightPadding;
|
|
109
190
|
const targetTop = measurements.y - mergedTheme.spotlightPadding;
|
|
110
191
|
|
|
@@ -138,27 +219,68 @@ export function GuidonTooltip({
|
|
|
138
219
|
top = Math.max(insets.top + TOOLTIP_MARGIN, Math.min(top, screenHeight - 200 - insets.bottom));
|
|
139
220
|
|
|
140
221
|
return { top, left, position };
|
|
141
|
-
}, [measurements, screenWidth, screenHeight, insets, currentStep?.tooltipPosition, mergedTheme.spotlightPadding]);
|
|
222
|
+
}, [measurements, screenWidth, screenHeight, insets, currentStep?.tooltipPosition, currentStep?.floatingPosition, mergedTheme.spotlightPadding, isFloatingStep]);
|
|
223
|
+
|
|
224
|
+
// Determine if tooltip should be visible
|
|
225
|
+
// Show for: normal steps with measurements, floating steps, or waiting states
|
|
226
|
+
const shouldShowTooltip = isActive && currentStep && (measurements || isFloatingStep || isWaiting);
|
|
142
227
|
|
|
143
228
|
// Animated styles
|
|
144
229
|
const animatedStyle = useAnimatedStyle(() => {
|
|
145
230
|
return {
|
|
146
|
-
opacity: withTiming(
|
|
231
|
+
opacity: withTiming(shouldShowTooltip ? 1 : 0, {
|
|
147
232
|
duration: animationDuration,
|
|
148
233
|
easing: Easing.inOut(Easing.ease),
|
|
149
234
|
}),
|
|
150
235
|
transform: [
|
|
151
236
|
{
|
|
152
|
-
translateY: withSpring(
|
|
237
|
+
translateY: withSpring(shouldShowTooltip ? 0 : 20, {
|
|
153
238
|
damping: 15,
|
|
154
239
|
stiffness: 150,
|
|
155
240
|
}),
|
|
156
241
|
},
|
|
157
242
|
],
|
|
158
243
|
};
|
|
159
|
-
}, [
|
|
244
|
+
}, [shouldShowTooltip, animationDuration]);
|
|
245
|
+
|
|
246
|
+
if (!isActive || !currentStep) {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Show waiting indicator when target element isn't mounted yet
|
|
251
|
+
if (isWaiting) {
|
|
252
|
+
return (
|
|
253
|
+
<Animated.View
|
|
254
|
+
style={[
|
|
255
|
+
styles.tooltipContainer,
|
|
256
|
+
{
|
|
257
|
+
top: tooltipPosition.top,
|
|
258
|
+
left: tooltipPosition.left,
|
|
259
|
+
width: TOOLTIP_WIDTH,
|
|
260
|
+
backgroundColor: mergedTheme.tooltipBackgroundColor,
|
|
261
|
+
borderColor: mergedTheme.tooltipBorderColor,
|
|
262
|
+
borderRadius: mergedTheme.tooltipBorderRadius,
|
|
263
|
+
},
|
|
264
|
+
animatedStyle,
|
|
265
|
+
]}
|
|
266
|
+
>
|
|
267
|
+
<View style={styles.waitingContainer}>
|
|
268
|
+
<ActivityIndicator color={mergedTheme.primaryColor} />
|
|
269
|
+
<Text style={[styles.waitingText, { color: mergedTheme.descriptionColor }]}>
|
|
270
|
+
{waitingState?.message || mergedLabels.waitingDefault}
|
|
271
|
+
</Text>
|
|
272
|
+
</View>
|
|
273
|
+
<TouchableOpacity onPress={skip} style={styles.skipButton}>
|
|
274
|
+
<Text style={[styles.skipText, { color: mergedTheme.mutedColor }]}>
|
|
275
|
+
{mergedLabels.skip}
|
|
276
|
+
</Text>
|
|
277
|
+
</TouchableOpacity>
|
|
278
|
+
</Animated.View>
|
|
279
|
+
);
|
|
280
|
+
}
|
|
160
281
|
|
|
161
|
-
|
|
282
|
+
// For non-floating steps, we need measurements
|
|
283
|
+
if (!isFloatingStep && !measurements) {
|
|
162
284
|
return null;
|
|
163
285
|
}
|
|
164
286
|
|
|
@@ -362,4 +484,16 @@ const styles = StyleSheet.create({
|
|
|
362
484
|
fontSize: 14,
|
|
363
485
|
fontWeight: '600',
|
|
364
486
|
},
|
|
487
|
+
waitingContainer: {
|
|
488
|
+
flexDirection: 'row',
|
|
489
|
+
alignItems: 'center',
|
|
490
|
+
justifyContent: 'center',
|
|
491
|
+
gap: 12,
|
|
492
|
+
paddingVertical: 8,
|
|
493
|
+
marginBottom: 12,
|
|
494
|
+
},
|
|
495
|
+
waitingText: {
|
|
496
|
+
fontSize: 14,
|
|
497
|
+
lineHeight: 20,
|
|
498
|
+
},
|
|
365
499
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useGuidonRef } from './useGuidonRef';
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, type RefObject } from 'react';
|
|
2
|
+
import { Platform } from 'react-native';
|
|
3
|
+
import { useGuidonStore } from '../store';
|
|
4
|
+
import type { TargetMeasurements, GuidonStore, GuidonStep } from '../types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Element type that can be measured
|
|
8
|
+
* Supports both web elements and React Native Views
|
|
9
|
+
*/
|
|
10
|
+
type MeasurableElement = {
|
|
11
|
+
getBoundingClientRect?: () => DOMRect;
|
|
12
|
+
measureInWindow?: (
|
|
13
|
+
callback: (x: number, y: number, width: number, height: number) => void
|
|
14
|
+
) => void;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Hook that returns a ref to attach to an element for guidon targeting.
|
|
19
|
+
* Alternative to the GuidonTarget wrapper component.
|
|
20
|
+
*
|
|
21
|
+
* @param targetId - The target ID that matches a step's targetId
|
|
22
|
+
* @returns A ref to attach to the target element
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```tsx
|
|
26
|
+
* function MyComponent() {
|
|
27
|
+
* const buttonRef = useGuidonRef<View>('my-button');
|
|
28
|
+
* return <Button ref={buttonRef}>Click me</Button>;
|
|
29
|
+
* }
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* @example Web usage
|
|
33
|
+
* ```tsx
|
|
34
|
+
* function MyComponent() {
|
|
35
|
+
* const divRef = useGuidonRef<HTMLDivElement>('my-div');
|
|
36
|
+
* return <div ref={divRef}>Highlight me</div>;
|
|
37
|
+
* }
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export function useGuidonRef<T extends MeasurableElement>(
|
|
41
|
+
targetId: string
|
|
42
|
+
): RefObject<T | null> {
|
|
43
|
+
const ref = useRef<T>(null);
|
|
44
|
+
const measurementFrameRef = useRef<number | null>(null);
|
|
45
|
+
|
|
46
|
+
const registerTarget = useGuidonStore(
|
|
47
|
+
(state: GuidonStore) => state.registerTarget
|
|
48
|
+
);
|
|
49
|
+
const unregisterTarget = useGuidonStore(
|
|
50
|
+
(state: GuidonStore) => state.unregisterTarget
|
|
51
|
+
);
|
|
52
|
+
const isActive = useGuidonStore((state: GuidonStore) => state.isActive);
|
|
53
|
+
const config = useGuidonStore((state: GuidonStore) => state.config);
|
|
54
|
+
const currentStepIndex = useGuidonStore(
|
|
55
|
+
(state: GuidonStore) => state.currentStepIndex
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// Check if this target is needed anywhere in the walkthrough
|
|
59
|
+
const isTargetNeeded =
|
|
60
|
+
isActive &&
|
|
61
|
+
config?.steps.some((step: GuidonStep) => step.targetId === targetId);
|
|
62
|
+
|
|
63
|
+
// Check if this specific target is needed for the current step
|
|
64
|
+
const isCurrentTarget =
|
|
65
|
+
config?.steps[currentStepIndex]?.targetId === targetId;
|
|
66
|
+
|
|
67
|
+
const measureElement = useCallback(() => {
|
|
68
|
+
if (!ref.current || !isTargetNeeded) return;
|
|
69
|
+
|
|
70
|
+
if (Platform.OS === 'web') {
|
|
71
|
+
// Web measurement using getBoundingClientRect
|
|
72
|
+
const element = ref.current as unknown as HTMLElement;
|
|
73
|
+
if (element && typeof element.getBoundingClientRect === 'function') {
|
|
74
|
+
const rect = element.getBoundingClientRect();
|
|
75
|
+
const measurements: TargetMeasurements = {
|
|
76
|
+
x: rect.left + window.scrollX,
|
|
77
|
+
y: rect.top + window.scrollY,
|
|
78
|
+
width: rect.width,
|
|
79
|
+
height: rect.height,
|
|
80
|
+
};
|
|
81
|
+
registerTarget(targetId, measurements);
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
// Native measurement using measureInWindow
|
|
85
|
+
const nativeRef = ref.current as unknown as {
|
|
86
|
+
measureInWindow: (
|
|
87
|
+
cb: (x: number, y: number, w: number, h: number) => void
|
|
88
|
+
) => void;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
if (nativeRef && typeof nativeRef.measureInWindow === 'function') {
|
|
92
|
+
nativeRef.measureInWindow((x, y, width, height) => {
|
|
93
|
+
if (width > 0 && height > 0) {
|
|
94
|
+
const measurements: TargetMeasurements = { x, y, width, height };
|
|
95
|
+
registerTarget(targetId, measurements);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}, [targetId, isTargetNeeded, registerTarget]);
|
|
101
|
+
|
|
102
|
+
// Measure when guidon becomes active or when this target becomes relevant
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
if (!isTargetNeeded) return;
|
|
105
|
+
|
|
106
|
+
// Use requestAnimationFrame to ensure layout is complete
|
|
107
|
+
measurementFrameRef.current = requestAnimationFrame(() => {
|
|
108
|
+
measureElement();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return () => {
|
|
112
|
+
if (measurementFrameRef.current) {
|
|
113
|
+
cancelAnimationFrame(measurementFrameRef.current);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}, [isTargetNeeded, measureElement]);
|
|
117
|
+
|
|
118
|
+
// Re-measure when this target becomes the current step
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
if (!isCurrentTarget) return;
|
|
121
|
+
|
|
122
|
+
const timer = setTimeout(() => {
|
|
123
|
+
measureElement();
|
|
124
|
+
}, 50);
|
|
125
|
+
|
|
126
|
+
return () => clearTimeout(timer);
|
|
127
|
+
}, [isCurrentTarget, measureElement]);
|
|
128
|
+
|
|
129
|
+
// Web: handle scroll and resize
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
if (Platform.OS !== 'web' || !isTargetNeeded) return;
|
|
132
|
+
|
|
133
|
+
const handleScrollOrResize = () => {
|
|
134
|
+
measureElement();
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
window.addEventListener('scroll', handleScrollOrResize, true);
|
|
138
|
+
window.addEventListener('resize', handleScrollOrResize);
|
|
139
|
+
|
|
140
|
+
return () => {
|
|
141
|
+
window.removeEventListener('scroll', handleScrollOrResize, true);
|
|
142
|
+
window.removeEventListener('resize', handleScrollOrResize);
|
|
143
|
+
};
|
|
144
|
+
}, [isTargetNeeded, measureElement]);
|
|
145
|
+
|
|
146
|
+
// Unregister on unmount
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
return () => {
|
|
149
|
+
unregisterTarget(targetId);
|
|
150
|
+
};
|
|
151
|
+
}, [targetId, unregisterTarget]);
|
|
152
|
+
|
|
153
|
+
return ref;
|
|
154
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,9 @@ export {
|
|
|
7
7
|
useGuidonContext,
|
|
8
8
|
} from "./components";
|
|
9
9
|
|
|
10
|
+
// Hooks
|
|
11
|
+
export { useGuidonRef } from "./hooks";
|
|
12
|
+
|
|
10
13
|
// Store and API
|
|
11
14
|
export {
|
|
12
15
|
useGuidonStore,
|
|
@@ -15,6 +18,8 @@ export {
|
|
|
15
18
|
useGuidonStep,
|
|
16
19
|
useGuidonProgress,
|
|
17
20
|
useTargetMeasurements,
|
|
21
|
+
useWaitingState,
|
|
22
|
+
useIsFloatingStep,
|
|
18
23
|
} from "./store";
|
|
19
24
|
|
|
20
25
|
// Types
|
|
@@ -29,6 +34,7 @@ export type {
|
|
|
29
34
|
GuidonTooltipLabels,
|
|
30
35
|
GuidonTooltipRenderProps,
|
|
31
36
|
TooltipPosition,
|
|
37
|
+
FloatingPosition,
|
|
32
38
|
TargetMeasurements,
|
|
33
39
|
GuidonState,
|
|
34
40
|
GuidonActions,
|
package/src/store.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { create } from "zustand";
|
|
2
|
+
import { useShallow } from "zustand/react/shallow";
|
|
2
3
|
import type {
|
|
3
4
|
GuidonConfig,
|
|
4
5
|
GuidonStore,
|
|
@@ -13,6 +14,8 @@ const initialState = {
|
|
|
13
14
|
targetMeasurements: {},
|
|
14
15
|
isLoading: false,
|
|
15
16
|
error: null,
|
|
17
|
+
waitingForTarget: false,
|
|
18
|
+
waitingTargetId: null,
|
|
16
19
|
};
|
|
17
20
|
|
|
18
21
|
export const useGuidonStore = create<GuidonStore>((set, get) => ({
|
|
@@ -30,8 +33,10 @@ export const useGuidonStore = create<GuidonStore>((set, get) => ({
|
|
|
30
33
|
|
|
31
34
|
// Call onStepEnter for the first step
|
|
32
35
|
const firstStep = config.steps[0];
|
|
33
|
-
firstStep
|
|
34
|
-
|
|
36
|
+
firstStep?.onStepEnter?.();
|
|
37
|
+
if (firstStep) {
|
|
38
|
+
config.onStepChange?.(0, firstStep);
|
|
39
|
+
}
|
|
35
40
|
},
|
|
36
41
|
|
|
37
42
|
next: () => {
|
|
@@ -47,8 +52,10 @@ export const useGuidonStore = create<GuidonStore>((set, get) => ({
|
|
|
47
52
|
|
|
48
53
|
set({ currentStepIndex: nextIndex });
|
|
49
54
|
|
|
50
|
-
nextStep
|
|
51
|
-
|
|
55
|
+
nextStep?.onStepEnter?.();
|
|
56
|
+
if (nextStep) {
|
|
57
|
+
config.onStepChange?.(nextIndex, nextStep);
|
|
58
|
+
}
|
|
52
59
|
} else {
|
|
53
60
|
// Last step completed
|
|
54
61
|
get().complete();
|
|
@@ -67,8 +74,10 @@ export const useGuidonStore = create<GuidonStore>((set, get) => ({
|
|
|
67
74
|
|
|
68
75
|
set({ currentStepIndex: prevIndex });
|
|
69
76
|
|
|
70
|
-
prevStep
|
|
71
|
-
|
|
77
|
+
prevStep?.onStepEnter?.();
|
|
78
|
+
if (prevStep) {
|
|
79
|
+
config.onStepChange?.(prevIndex, prevStep);
|
|
80
|
+
}
|
|
72
81
|
},
|
|
73
82
|
|
|
74
83
|
goToStep: (index: number) => {
|
|
@@ -83,8 +92,10 @@ export const useGuidonStore = create<GuidonStore>((set, get) => ({
|
|
|
83
92
|
|
|
84
93
|
set({ currentStepIndex: index });
|
|
85
94
|
|
|
86
|
-
targetStep
|
|
87
|
-
|
|
95
|
+
targetStep?.onStepEnter?.();
|
|
96
|
+
if (targetStep) {
|
|
97
|
+
config.onStepChange?.(index, targetStep);
|
|
98
|
+
}
|
|
88
99
|
},
|
|
89
100
|
|
|
90
101
|
skip: () => {
|
|
@@ -114,11 +125,17 @@ export const useGuidonStore = create<GuidonStore>((set, get) => ({
|
|
|
114
125
|
},
|
|
115
126
|
|
|
116
127
|
registerTarget: (targetId: string, measurements: TargetMeasurements) => {
|
|
128
|
+
const { waitingTargetId } = get();
|
|
117
129
|
set((state: GuidonStore) => ({
|
|
118
130
|
targetMeasurements: {
|
|
119
131
|
...state.targetMeasurements,
|
|
120
132
|
[targetId]: measurements,
|
|
121
133
|
},
|
|
134
|
+
// Clear waiting state if this was the target we were waiting for
|
|
135
|
+
waitingForTarget:
|
|
136
|
+
waitingTargetId === targetId ? false : state.waitingForTarget,
|
|
137
|
+
waitingTargetId:
|
|
138
|
+
waitingTargetId === targetId ? null : state.waitingTargetId,
|
|
122
139
|
}));
|
|
123
140
|
},
|
|
124
141
|
|
|
@@ -254,15 +271,51 @@ export const useGuidonStep = () =>
|
|
|
254
271
|
});
|
|
255
272
|
|
|
256
273
|
export const useGuidonProgress = () =>
|
|
257
|
-
useGuidonStore(
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
274
|
+
useGuidonStore(
|
|
275
|
+
useShallow((state: GuidonStore) => ({
|
|
276
|
+
currentStep: state.currentStepIndex + 1,
|
|
277
|
+
totalSteps: state.config?.steps.length ?? 0,
|
|
278
|
+
percentage: state.config
|
|
279
|
+
? ((state.currentStepIndex + 1) / state.config.steps.length) * 100
|
|
280
|
+
: 0,
|
|
281
|
+
}))
|
|
282
|
+
);
|
|
264
283
|
|
|
265
284
|
export const useTargetMeasurements = (targetId: string) =>
|
|
266
285
|
useGuidonStore(
|
|
267
286
|
(state: GuidonStore) => state.targetMeasurements[targetId],
|
|
268
287
|
);
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Hook to check if the guidon is waiting for a target element to mount
|
|
291
|
+
* Returns null if not active, not waiting, or if it's a floating step
|
|
292
|
+
*/
|
|
293
|
+
export const useWaitingState = () =>
|
|
294
|
+
useGuidonStore(
|
|
295
|
+
useShallow((state: GuidonStore) => {
|
|
296
|
+
if (!state.isActive || !state.config) return null;
|
|
297
|
+
|
|
298
|
+
const currentStep = state.config.steps[state.currentStepIndex];
|
|
299
|
+
const targetId = currentStep?.targetId;
|
|
300
|
+
|
|
301
|
+
// Floating step (no targetId) - not waiting
|
|
302
|
+
if (!targetId) return null;
|
|
303
|
+
|
|
304
|
+
const hasMeasurements = !!state.targetMeasurements[targetId];
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
isWaiting: !hasMeasurements,
|
|
308
|
+
targetId: hasMeasurements ? null : targetId,
|
|
309
|
+
message: currentStep?.waitingMessage ?? null,
|
|
310
|
+
};
|
|
311
|
+
})
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Hook to check if the current step is a floating step (no target element)
|
|
316
|
+
*/
|
|
317
|
+
export const useIsFloatingStep = () =>
|
|
318
|
+
useGuidonStore((state: GuidonStore) => {
|
|
319
|
+
if (!state.config || !state.isActive) return false;
|
|
320
|
+
return !state.config.steps[state.currentStepIndex]?.targetId;
|
|
321
|
+
});
|