react-native-lumen 1.0.1 → 1.1.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 +763 -231
- package/lib/module/components/TourOverlay.js +43 -3
- package/lib/module/components/TourOverlay.js.map +1 -1
- package/lib/module/components/TourProvider.js +318 -61
- package/lib/module/components/TourProvider.js.map +1 -1
- package/lib/module/components/TourTooltip.js +113 -73
- package/lib/module/components/TourTooltip.js.map +1 -1
- package/lib/module/components/TourZone.js +186 -119
- package/lib/module/components/TourZone.js.map +1 -1
- package/lib/module/constants/defaults.js +43 -0
- package/lib/module/constants/defaults.js.map +1 -1
- package/lib/module/context/TourContext.js +5 -0
- package/lib/module/context/TourContext.js.map +1 -0
- package/lib/module/hooks/useTour.js +1 -1
- package/lib/module/hooks/useTour.js.map +1 -1
- package/lib/module/hooks/useTourScrollView.js +71 -0
- package/lib/module/hooks/useTourScrollView.js.map +1 -0
- package/lib/module/index.js +6 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/utils/storage.js +188 -0
- package/lib/module/utils/storage.js.map +1 -0
- package/lib/typescript/src/components/TourOverlay.d.ts.map +1 -1
- package/lib/typescript/src/components/TourProvider.d.ts +21 -4
- package/lib/typescript/src/components/TourProvider.d.ts.map +1 -1
- package/lib/typescript/src/components/TourTooltip.d.ts.map +1 -1
- package/lib/typescript/src/components/TourZone.d.ts +19 -1
- package/lib/typescript/src/components/TourZone.d.ts.map +1 -1
- package/lib/typescript/src/constants/defaults.d.ts +10 -0
- package/lib/typescript/src/constants/defaults.d.ts.map +1 -1
- package/lib/typescript/src/context/TourContext.d.ts +3 -0
- package/lib/typescript/src/context/TourContext.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useTourScrollView.d.ts +65 -0
- package/lib/typescript/src/hooks/useTourScrollView.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +4 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/types/index.d.ts +296 -1
- package/lib/typescript/src/types/index.d.ts.map +1 -1
- package/lib/typescript/src/utils/storage.d.ts +51 -0
- package/lib/typescript/src/utils/storage.d.ts.map +1 -0
- package/package.json +173 -171
- package/src/components/TourOverlay.tsx +45 -2
- package/src/components/TourProvider.tsx +408 -56
- package/src/components/TourTooltip.tsx +144 -71
- package/src/components/TourZone.tsx +238 -140
- package/src/constants/defaults.ts +51 -0
- package/src/context/TourContext.ts +4 -0
- package/src/hooks/useTour.ts +1 -1
- package/src/hooks/useTourScrollView.ts +111 -0
- package/src/index.tsx +27 -0
- package/src/types/index.ts +306 -1
- package/src/utils/storage.ts +226 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, {
|
|
2
2
|
useEffect,
|
|
3
3
|
useCallback,
|
|
4
|
-
|
|
4
|
+
useMemo,
|
|
5
5
|
type ComponentType,
|
|
6
6
|
} from 'react';
|
|
7
7
|
import type { ViewStyle, StyleProp } from 'react-native';
|
|
@@ -16,7 +16,12 @@ import {
|
|
|
16
16
|
useSharedValue,
|
|
17
17
|
} from 'react-native-reanimated';
|
|
18
18
|
import { Dimensions } from 'react-native';
|
|
19
|
-
import type {
|
|
19
|
+
import type {
|
|
20
|
+
InternalTourContextType,
|
|
21
|
+
ZoneStyle,
|
|
22
|
+
ZoneShape,
|
|
23
|
+
CardProps,
|
|
24
|
+
} from '../types';
|
|
20
25
|
|
|
21
26
|
const { height: SCREEN_HEIGHT } = Dimensions.get('window');
|
|
22
27
|
|
|
@@ -27,11 +32,28 @@ interface TourZoneProps {
|
|
|
27
32
|
name?: string;
|
|
28
33
|
description: string;
|
|
29
34
|
order?: number;
|
|
30
|
-
shape?:
|
|
35
|
+
shape?: ZoneShape;
|
|
31
36
|
borderRadius?: number;
|
|
32
37
|
children: React.ReactNode;
|
|
33
38
|
style?: StyleProp<ViewStyle>;
|
|
34
39
|
clickable?: boolean;
|
|
40
|
+
preventInteraction?: boolean;
|
|
41
|
+
required?: boolean;
|
|
42
|
+
completed?: boolean;
|
|
43
|
+
zonePadding?: number;
|
|
44
|
+
zonePaddingTop?: number;
|
|
45
|
+
zonePaddingRight?: number;
|
|
46
|
+
zonePaddingBottom?: number;
|
|
47
|
+
zonePaddingLeft?: number;
|
|
48
|
+
zoneBorderWidth?: number;
|
|
49
|
+
zoneBorderColor?: string;
|
|
50
|
+
zoneGlowColor?: string;
|
|
51
|
+
zoneGlowRadius?: number;
|
|
52
|
+
zoneGlowSpread?: number;
|
|
53
|
+
zoneGlowOffsetX?: number;
|
|
54
|
+
zoneGlowOffsetY?: number;
|
|
55
|
+
zoneStyle?: ZoneStyle;
|
|
56
|
+
renderCustomCard?: (props: CardProps) => React.ReactNode;
|
|
35
57
|
}
|
|
36
58
|
|
|
37
59
|
export const TourZone: React.FC<TourZoneProps> = ({
|
|
@@ -39,11 +61,28 @@ export const TourZone: React.FC<TourZoneProps> = ({
|
|
|
39
61
|
name,
|
|
40
62
|
description,
|
|
41
63
|
order,
|
|
42
|
-
shape = 'rect',
|
|
64
|
+
shape = 'rounded-rect',
|
|
43
65
|
borderRadius = 10,
|
|
44
66
|
children,
|
|
45
67
|
style,
|
|
46
68
|
clickable,
|
|
69
|
+
preventInteraction,
|
|
70
|
+
required,
|
|
71
|
+
completed,
|
|
72
|
+
zonePadding,
|
|
73
|
+
zonePaddingTop,
|
|
74
|
+
zonePaddingRight,
|
|
75
|
+
zonePaddingBottom,
|
|
76
|
+
zonePaddingLeft,
|
|
77
|
+
zoneBorderWidth,
|
|
78
|
+
zoneBorderColor,
|
|
79
|
+
zoneGlowColor,
|
|
80
|
+
zoneGlowRadius,
|
|
81
|
+
zoneGlowSpread,
|
|
82
|
+
zoneGlowOffsetX,
|
|
83
|
+
zoneGlowOffsetY,
|
|
84
|
+
zoneStyle,
|
|
85
|
+
renderCustomCard,
|
|
47
86
|
}) => {
|
|
48
87
|
const {
|
|
49
88
|
registerStep,
|
|
@@ -59,34 +98,65 @@ export const TourZone: React.FC<TourZoneProps> = ({
|
|
|
59
98
|
targetRadius,
|
|
60
99
|
config,
|
|
61
100
|
} = useTour() as InternalTourContextType;
|
|
62
|
-
const viewRef = useAnimatedRef<any>();
|
|
63
101
|
|
|
102
|
+
const viewRef = useAnimatedRef<any>();
|
|
64
103
|
const isActive = currentStep === stepKey;
|
|
65
104
|
|
|
66
|
-
//
|
|
105
|
+
// The critical lock for the UI thread
|
|
67
106
|
const isScrolling = useSharedValue(false);
|
|
68
|
-
const hasScrolled = useRef(false);
|
|
69
107
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
108
|
+
console.log(`zoneGlowRadius ${stepKey}`, zoneGlowRadius);
|
|
109
|
+
|
|
110
|
+
const resolvedZoneStyle: ZoneStyle = useMemo(
|
|
111
|
+
() => ({
|
|
112
|
+
...zoneStyle,
|
|
113
|
+
...(zonePadding !== undefined && { padding: zonePadding }),
|
|
114
|
+
...(zonePaddingTop !== undefined && { paddingTop: zonePaddingTop }),
|
|
115
|
+
...(zonePaddingRight !== undefined && { paddingRight: zonePaddingRight }),
|
|
116
|
+
...(zonePaddingBottom !== undefined && {
|
|
117
|
+
paddingBottom: zonePaddingBottom,
|
|
118
|
+
}),
|
|
119
|
+
...(zonePaddingLeft !== undefined && { paddingLeft: zonePaddingLeft }),
|
|
120
|
+
...(zoneBorderWidth !== undefined && { borderWidth: zoneBorderWidth }),
|
|
121
|
+
...(zoneBorderColor !== undefined && { borderColor: zoneBorderColor }),
|
|
122
|
+
...(zoneGlowColor !== undefined && { glowColor: zoneGlowColor }),
|
|
123
|
+
...(zoneGlowRadius !== undefined && { glowRadius: zoneGlowRadius }),
|
|
124
|
+
...(zoneGlowSpread !== undefined && { glowSpread: zoneGlowSpread }),
|
|
125
|
+
...(zoneGlowOffsetX !== undefined && { glowOffsetX: zoneGlowOffsetX }),
|
|
126
|
+
...(zoneGlowOffsetY !== undefined && { glowOffsetY: zoneGlowOffsetY }),
|
|
127
|
+
shape,
|
|
128
|
+
borderRadius,
|
|
129
|
+
}),
|
|
130
|
+
[
|
|
131
|
+
zoneStyle,
|
|
132
|
+
zonePadding,
|
|
133
|
+
zonePaddingTop,
|
|
134
|
+
zonePaddingRight,
|
|
135
|
+
zonePaddingBottom,
|
|
136
|
+
zonePaddingLeft,
|
|
137
|
+
zoneBorderWidth,
|
|
138
|
+
zoneBorderColor,
|
|
139
|
+
zoneGlowColor,
|
|
140
|
+
zoneGlowRadius,
|
|
141
|
+
zoneGlowSpread,
|
|
142
|
+
zoneGlowOffsetX,
|
|
143
|
+
zoneGlowOffsetY,
|
|
144
|
+
shape,
|
|
145
|
+
borderRadius,
|
|
146
|
+
]
|
|
147
|
+
);
|
|
74
148
|
|
|
75
149
|
/**
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* This fixes the bug where measureLayout returned content-relative Y.
|
|
150
|
+
* Captures the final, perfect coordinates and UNLOCKS the UI thread.
|
|
151
|
+
* This is explicitly the ONLY function allowed to set isScrolling.value = false.
|
|
79
152
|
*/
|
|
80
153
|
const measureJS = useCallback(() => {
|
|
81
|
-
if (
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
154
|
+
if (!isActive) return;
|
|
84
155
|
|
|
85
156
|
const view = viewRef.current as any;
|
|
86
157
|
const container = containerRef.current as any;
|
|
87
158
|
|
|
88
159
|
if (view && container) {
|
|
89
|
-
// 1. Measure the View in Screen Coordinates (PageX/PageY)
|
|
90
160
|
view.measure(
|
|
91
161
|
(
|
|
92
162
|
_x: number,
|
|
@@ -96,8 +166,6 @@ export const TourZone: React.FC<TourZoneProps> = ({
|
|
|
96
166
|
pageX: number,
|
|
97
167
|
pageY: number
|
|
98
168
|
) => {
|
|
99
|
-
// 2. Measure the Container (TourOverlay) in Screen Coordinates
|
|
100
|
-
// This handles cases where the Tour Overlay isn't exactly at 0,0 (e.g. inside a SafeAreaView)
|
|
101
169
|
container.measure(
|
|
102
170
|
(
|
|
103
171
|
_cx: number,
|
|
@@ -108,7 +176,6 @@ export const TourZone: React.FC<TourZoneProps> = ({
|
|
|
108
176
|
containerPageY: number
|
|
109
177
|
) => {
|
|
110
178
|
if (width > 0 && height > 0 && !isNaN(pageX) && !isNaN(pageY)) {
|
|
111
|
-
// Calculate final position relative to the Tour Overlay
|
|
112
179
|
const finalX = pageX - containerPageX;
|
|
113
180
|
const finalY = pageY - containerPageY;
|
|
114
181
|
|
|
@@ -118,89 +185,43 @@ export const TourZone: React.FC<TourZoneProps> = ({
|
|
|
118
185
|
width,
|
|
119
186
|
height,
|
|
120
187
|
});
|
|
188
|
+
|
|
189
|
+
// Unlock the UI thread to take over tracking
|
|
190
|
+
isScrolling.value = false;
|
|
121
191
|
}
|
|
122
192
|
}
|
|
123
193
|
);
|
|
124
194
|
}
|
|
125
195
|
);
|
|
126
196
|
}
|
|
127
|
-
}, [containerRef,
|
|
197
|
+
}, [containerRef, isActive, isScrolling, stepKey, updateStepLayout, viewRef]);
|
|
128
198
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
// Small delay to ensure layout is ready
|
|
134
|
-
const timeoutId = setTimeout(() => {
|
|
135
|
-
measureJS();
|
|
136
|
-
}, 50);
|
|
137
|
-
|
|
138
|
-
return () => clearTimeout(timeoutId);
|
|
139
|
-
}, [isActive, measureJS]);
|
|
140
|
-
|
|
141
|
-
// Reanimated Frame Callback (UI Thread Tracking)
|
|
142
|
-
// This keeps the highlight sticky during manual user scrolling
|
|
143
|
-
useFrameCallback(() => {
|
|
144
|
-
'worklet';
|
|
145
|
-
if (!isActive || isScrolling.value) {
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
try {
|
|
149
|
-
const measured = measure(viewRef);
|
|
150
|
-
const container = measure(containerRef as AnimatedRef<any>);
|
|
151
|
-
|
|
152
|
-
if (measured && container) {
|
|
153
|
-
const x = measured.pageX - container.pageX;
|
|
154
|
-
const y = measured.pageY - container.pageY;
|
|
155
|
-
const width = measured.width;
|
|
156
|
-
const height = measured.height;
|
|
157
|
-
|
|
158
|
-
if (
|
|
159
|
-
width > 0 &&
|
|
160
|
-
height > 0 &&
|
|
161
|
-
!isNaN(x) &&
|
|
162
|
-
!isNaN(y) &&
|
|
163
|
-
isFinite(x) &&
|
|
164
|
-
isFinite(y)
|
|
165
|
-
) {
|
|
166
|
-
const springConfig = config?.springConfig ?? {
|
|
167
|
-
damping: 100,
|
|
168
|
-
stiffness: 100,
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
targetX.value = withSpring(x, springConfig);
|
|
172
|
-
targetY.value = withSpring(y, springConfig);
|
|
173
|
-
targetWidth.value = withSpring(width, springConfig);
|
|
174
|
-
targetHeight.value = withSpring(height, springConfig);
|
|
175
|
-
targetRadius.value = withSpring(borderRadius, springConfig);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
} catch {
|
|
179
|
-
// Silently ignore measurement errors on UI thread
|
|
180
|
-
}
|
|
181
|
-
}, isActive);
|
|
182
|
-
|
|
183
|
-
// Auto-scroll Effect
|
|
199
|
+
/**
|
|
200
|
+
* Unified Pipeline: Measure -> Predict Scroll -> Scroll -> Measure Final -> Unlock
|
|
201
|
+
* Replaces all independent timers to prevent race conditions on consecutive steps.
|
|
202
|
+
*/
|
|
184
203
|
useEffect(() => {
|
|
185
204
|
if (!isActive || !scrollViewRef?.current || !viewRef.current) {
|
|
186
205
|
return;
|
|
187
206
|
}
|
|
188
207
|
|
|
189
|
-
|
|
190
|
-
const view = viewRef.current as any;
|
|
191
|
-
const scroll = scrollViewRef.current as any;
|
|
192
|
-
const container = containerRef.current as any;
|
|
193
|
-
|
|
208
|
+
let cancelled = false;
|
|
194
209
|
let attemptCount = 0;
|
|
195
|
-
const maxAttempts =
|
|
210
|
+
const maxAttempts = 5;
|
|
211
|
+
let hasInitiatedScroll = false;
|
|
196
212
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if (hasScrolled.current) return;
|
|
213
|
+
// 1. Lock immediately so the UI thread doesn't grab off-screen coordinates
|
|
214
|
+
isScrolling.value = true;
|
|
200
215
|
|
|
216
|
+
const checkAndScroll = (delay: number) => {
|
|
217
|
+
const timeoutId = setTimeout(() => {
|
|
218
|
+
if (cancelled || hasInitiatedScroll) return;
|
|
201
219
|
attemptCount++;
|
|
202
220
|
|
|
203
|
-
|
|
221
|
+
const view = viewRef.current as any;
|
|
222
|
+
const scroll = scrollViewRef.current as any;
|
|
223
|
+
const container = containerRef.current as any;
|
|
224
|
+
|
|
204
225
|
view.measure(
|
|
205
226
|
(
|
|
206
227
|
_mx: number,
|
|
@@ -210,21 +231,17 @@ export const TourZone: React.FC<TourZoneProps> = ({
|
|
|
210
231
|
px: number,
|
|
211
232
|
py: number
|
|
212
233
|
) => {
|
|
234
|
+
if (cancelled) return;
|
|
235
|
+
|
|
213
236
|
if (mw > 0 && mh > 0 && !isNaN(px) && !isNaN(py)) {
|
|
214
|
-
const viewportHeight = SCREEN_HEIGHT;
|
|
215
237
|
const topBuffer = 100;
|
|
216
238
|
const bottomBuffer = 150;
|
|
217
|
-
|
|
218
|
-
// Check if element is out of the "safe" visual zone
|
|
219
239
|
const needsScroll =
|
|
220
|
-
py < topBuffer || py + mh >
|
|
240
|
+
py < topBuffer || py + mh > SCREEN_HEIGHT - bottomBuffer;
|
|
221
241
|
|
|
222
242
|
if (needsScroll) {
|
|
223
|
-
|
|
224
|
-
isScrolling.value = true;
|
|
243
|
+
hasInitiatedScroll = true;
|
|
225
244
|
|
|
226
|
-
// 2. Measure ScrollView to get its Screen Position (Offset from top)
|
|
227
|
-
// This fixes the "upwards" bug by accounting for headers/safe-areas
|
|
228
245
|
scroll.measure(
|
|
229
246
|
(
|
|
230
247
|
_sx: number,
|
|
@@ -234,17 +251,18 @@ export const TourZone: React.FC<TourZoneProps> = ({
|
|
|
234
251
|
scrollPx: number,
|
|
235
252
|
scrollPy: number
|
|
236
253
|
) => {
|
|
237
|
-
|
|
254
|
+
if (cancelled) return;
|
|
255
|
+
|
|
238
256
|
if (view.measureLayout) {
|
|
239
257
|
view.measureLayout(
|
|
240
258
|
scroll,
|
|
241
259
|
(contentX: number, contentY: number) => {
|
|
242
|
-
|
|
260
|
+
if (cancelled) return;
|
|
261
|
+
|
|
243
262
|
const centerY =
|
|
244
|
-
contentY -
|
|
263
|
+
contentY - SCREEN_HEIGHT / 2 + mh / 2 + 50;
|
|
245
264
|
const scrollY = Math.max(0, centerY);
|
|
246
265
|
|
|
247
|
-
// 4. Measure Container to map coordinates to Overlay space
|
|
248
266
|
container.measure(
|
|
249
267
|
(
|
|
250
268
|
_cx: number,
|
|
@@ -254,12 +272,12 @@ export const TourZone: React.FC<TourZoneProps> = ({
|
|
|
254
272
|
containerPx: number,
|
|
255
273
|
containerPy: number
|
|
256
274
|
) => {
|
|
257
|
-
|
|
258
|
-
|
|
275
|
+
if (cancelled) return;
|
|
276
|
+
|
|
277
|
+
// Calculate predictive screen coordinates so the zone smoothly jumps
|
|
278
|
+
// to the destination *while* the screen is scrolling.
|
|
259
279
|
const targetScreenY =
|
|
260
280
|
scrollPy + contentY - scrollY - containerPy;
|
|
261
|
-
|
|
262
|
-
// X is simpler: ScrollViewScreenX + ElementContentX - ContainerScreenX
|
|
263
281
|
const targetScreenX =
|
|
264
282
|
scrollPx + contentX - containerPx;
|
|
265
283
|
|
|
@@ -272,70 +290,141 @@ export const TourZone: React.FC<TourZoneProps> = ({
|
|
|
272
290
|
|
|
273
291
|
try {
|
|
274
292
|
scroll.scrollTo({ y: scrollY, animated: true });
|
|
275
|
-
// Wait for scroll animation
|
|
276
|
-
setTimeout(() => onScrollComplete(), 800);
|
|
277
293
|
} catch (e) {
|
|
278
294
|
console.error(e);
|
|
279
|
-
onScrollComplete();
|
|
280
295
|
}
|
|
296
|
+
|
|
297
|
+
// Wait for the scroll animation to settle, then verify exact position
|
|
298
|
+
setTimeout(() => {
|
|
299
|
+
if (!cancelled) measureJS();
|
|
300
|
+
}, 800);
|
|
281
301
|
}
|
|
282
302
|
);
|
|
303
|
+
},
|
|
304
|
+
() => {
|
|
305
|
+
// Fallback if measureLayout is unavailable
|
|
306
|
+
setTimeout(() => {
|
|
307
|
+
if (!cancelled) measureJS();
|
|
308
|
+
}, 800);
|
|
283
309
|
}
|
|
284
310
|
);
|
|
285
311
|
}
|
|
286
312
|
}
|
|
287
313
|
);
|
|
288
314
|
} else {
|
|
289
|
-
//
|
|
290
|
-
|
|
291
|
-
(
|
|
292
|
-
_cx: number,
|
|
293
|
-
_cy: number,
|
|
294
|
-
_cw: number,
|
|
295
|
-
_ch: number,
|
|
296
|
-
cPx: number,
|
|
297
|
-
cPy: number
|
|
298
|
-
) => {
|
|
299
|
-
const finalX = px - cPx;
|
|
300
|
-
const finalY = py - cPy;
|
|
301
|
-
|
|
302
|
-
updateStepLayout(stepKey, {
|
|
303
|
-
x: finalX,
|
|
304
|
-
y: finalY,
|
|
305
|
-
width: mw,
|
|
306
|
-
height: mh,
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
);
|
|
315
|
+
// No scroll needed, just get the perfect coordinates and unlock
|
|
316
|
+
measureJS();
|
|
310
317
|
}
|
|
311
318
|
} else if (attemptCount < maxAttempts) {
|
|
312
|
-
|
|
319
|
+
checkAndScroll(150);
|
|
313
320
|
}
|
|
314
321
|
}
|
|
315
322
|
);
|
|
316
323
|
}, delay);
|
|
324
|
+
|
|
317
325
|
return timeoutId;
|
|
318
326
|
};
|
|
319
327
|
|
|
320
|
-
const
|
|
321
|
-
|
|
328
|
+
const initialTimeout = checkAndScroll(50);
|
|
329
|
+
|
|
330
|
+
return () => {
|
|
331
|
+
cancelled = true;
|
|
332
|
+
clearTimeout(initialTimeout);
|
|
333
|
+
};
|
|
322
334
|
}, [
|
|
323
335
|
isActive,
|
|
324
336
|
scrollViewRef,
|
|
325
337
|
viewRef,
|
|
326
|
-
stepKey,
|
|
327
|
-
isScrolling,
|
|
328
|
-
onScrollComplete,
|
|
329
338
|
containerRef,
|
|
339
|
+
stepKey,
|
|
330
340
|
updateStepLayout,
|
|
341
|
+
measureJS,
|
|
342
|
+
isScrolling,
|
|
331
343
|
]);
|
|
332
344
|
|
|
333
|
-
//
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
345
|
+
// UI Thread tracking
|
|
346
|
+
useFrameCallback(() => {
|
|
347
|
+
'worklet';
|
|
348
|
+
if (!isActive || isScrolling.value) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
try {
|
|
352
|
+
const measured = measure(viewRef);
|
|
353
|
+
const container = measure(containerRef as AnimatedRef<any>);
|
|
354
|
+
|
|
355
|
+
if (measured && container) {
|
|
356
|
+
const x = measured.pageX - container.pageX;
|
|
357
|
+
const y = measured.pageY - container.pageY;
|
|
358
|
+
const width = measured.width;
|
|
359
|
+
const height = measured.height;
|
|
360
|
+
|
|
361
|
+
if (
|
|
362
|
+
width > 0 &&
|
|
363
|
+
height > 0 &&
|
|
364
|
+
!isNaN(x) &&
|
|
365
|
+
!isNaN(y) &&
|
|
366
|
+
isFinite(x) &&
|
|
367
|
+
isFinite(y)
|
|
368
|
+
) {
|
|
369
|
+
const springConfig = config?.springConfig ?? {
|
|
370
|
+
damping: 100,
|
|
371
|
+
stiffness: 100,
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const zpt =
|
|
375
|
+
resolvedZoneStyle.paddingTop ?? resolvedZoneStyle.padding ?? 0;
|
|
376
|
+
const zpr =
|
|
377
|
+
resolvedZoneStyle.paddingRight ?? resolvedZoneStyle.padding ?? 0;
|
|
378
|
+
const zpb =
|
|
379
|
+
resolvedZoneStyle.paddingBottom ?? resolvedZoneStyle.padding ?? 0;
|
|
380
|
+
const zpl =
|
|
381
|
+
resolvedZoneStyle.paddingLeft ?? resolvedZoneStyle.padding ?? 0;
|
|
382
|
+
const zShape = resolvedZoneStyle.shape ?? 'rounded-rect';
|
|
383
|
+
|
|
384
|
+
let sx = x - zpl;
|
|
385
|
+
let sy = y - zpt;
|
|
386
|
+
let sw = width + zpl + zpr;
|
|
387
|
+
let sh = height + zpt + zpb;
|
|
388
|
+
let sr = borderRadius;
|
|
389
|
+
|
|
390
|
+
if (zShape === 'circle') {
|
|
391
|
+
const cx = x + width / 2;
|
|
392
|
+
const cy = y + height / 2;
|
|
393
|
+
const radius =
|
|
394
|
+
Math.max(width, height) / 2 + (resolvedZoneStyle.padding ?? 0);
|
|
395
|
+
sx = cx - radius;
|
|
396
|
+
sy = cy - radius;
|
|
397
|
+
sw = radius * 2;
|
|
398
|
+
sh = radius * 2;
|
|
399
|
+
sr = radius;
|
|
400
|
+
} else if (zShape === 'pill') {
|
|
401
|
+
sx = x - zpl;
|
|
402
|
+
sy = y - zpt;
|
|
403
|
+
sw = width + zpl + zpr;
|
|
404
|
+
sh = height + zpt + zpb;
|
|
405
|
+
sr = sh / 2;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
targetX.value = withSpring(sx, springConfig);
|
|
409
|
+
targetY.value = withSpring(sy, springConfig);
|
|
410
|
+
targetWidth.value = withSpring(sw, springConfig);
|
|
411
|
+
targetHeight.value = withSpring(sh, springConfig);
|
|
412
|
+
targetRadius.value = withSpring(sr, springConfig);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
} catch {
|
|
416
|
+
// Silently ignore measurement errors on UI thread
|
|
417
|
+
}
|
|
418
|
+
}, isActive);
|
|
419
|
+
|
|
420
|
+
// Sync position if the element physically resizes, but strictly avoid
|
|
421
|
+
// measuring if we are currently handling an orchestrated scroll.
|
|
422
|
+
const onLayout = useCallback(() => {
|
|
423
|
+
if (isActive && !isScrolling.value) {
|
|
424
|
+
measureJS();
|
|
425
|
+
}
|
|
426
|
+
}, [isActive, isScrolling.value, measureJS]);
|
|
337
427
|
|
|
338
|
-
// Register step on mount
|
|
339
428
|
useEffect(() => {
|
|
340
429
|
registerStep({
|
|
341
430
|
key: stepKey,
|
|
@@ -343,7 +432,12 @@ export const TourZone: React.FC<TourZoneProps> = ({
|
|
|
343
432
|
description,
|
|
344
433
|
order,
|
|
345
434
|
clickable,
|
|
346
|
-
|
|
435
|
+
preventInteraction,
|
|
436
|
+
required,
|
|
437
|
+
completed,
|
|
438
|
+
meta: { shape: resolvedZoneStyle.shape, borderRadius },
|
|
439
|
+
zoneStyle: resolvedZoneStyle,
|
|
440
|
+
renderCustomCard,
|
|
347
441
|
});
|
|
348
442
|
return () => unregisterStep(stepKey);
|
|
349
443
|
}, [
|
|
@@ -351,11 +445,15 @@ export const TourZone: React.FC<TourZoneProps> = ({
|
|
|
351
445
|
name,
|
|
352
446
|
description,
|
|
353
447
|
order,
|
|
354
|
-
shape,
|
|
355
448
|
borderRadius,
|
|
356
449
|
registerStep,
|
|
357
450
|
unregisterStep,
|
|
358
451
|
clickable,
|
|
452
|
+
preventInteraction,
|
|
453
|
+
required,
|
|
454
|
+
completed,
|
|
455
|
+
resolvedZoneStyle,
|
|
456
|
+
renderCustomCard,
|
|
359
457
|
]);
|
|
360
458
|
|
|
361
459
|
return (
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { WithSpringConfig } from 'react-native-reanimated';
|
|
2
|
+
import type { ZoneStyle } from '../types';
|
|
2
3
|
|
|
3
4
|
export const DEFAULT_SPRING_CONFIG: WithSpringConfig = {
|
|
4
5
|
damping: 20,
|
|
@@ -13,3 +14,53 @@ export const DEFAULT_LABELS = {
|
|
|
13
14
|
finish: 'Finish',
|
|
14
15
|
skip: 'Skip',
|
|
15
16
|
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Default zone style configuration.
|
|
20
|
+
* These values are used when no custom style is provided.
|
|
21
|
+
*/
|
|
22
|
+
export const DEFAULT_ZONE_STYLE: Required<ZoneStyle> = {
|
|
23
|
+
padding: 0,
|
|
24
|
+
paddingTop: 0,
|
|
25
|
+
paddingRight: 0,
|
|
26
|
+
paddingBottom: 0,
|
|
27
|
+
paddingLeft: 0,
|
|
28
|
+
borderRadius: 10,
|
|
29
|
+
shape: 'rounded-rect',
|
|
30
|
+
borderWidth: 0,
|
|
31
|
+
borderColor: 'transparent',
|
|
32
|
+
glowColor: '#FFFFFF',
|
|
33
|
+
glowRadius: 10,
|
|
34
|
+
glowSpread: 5,
|
|
35
|
+
glowOffsetX: 0,
|
|
36
|
+
glowOffsetY: 0,
|
|
37
|
+
springDamping: 20,
|
|
38
|
+
springStiffness: 90,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Merges global and per-step zone styles with defaults.
|
|
43
|
+
*/
|
|
44
|
+
export function resolveZoneStyle(
|
|
45
|
+
globalStyle?: ZoneStyle,
|
|
46
|
+
stepStyle?: ZoneStyle
|
|
47
|
+
): Required<ZoneStyle> {
|
|
48
|
+
const merged = {
|
|
49
|
+
...DEFAULT_ZONE_STYLE,
|
|
50
|
+
...globalStyle,
|
|
51
|
+
...stepStyle,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Handle individual padding overrides
|
|
55
|
+
return {
|
|
56
|
+
...merged,
|
|
57
|
+
paddingTop:
|
|
58
|
+
stepStyle?.paddingTop ?? globalStyle?.paddingTop ?? merged.padding,
|
|
59
|
+
paddingRight:
|
|
60
|
+
stepStyle?.paddingRight ?? globalStyle?.paddingRight ?? merged.padding,
|
|
61
|
+
paddingBottom:
|
|
62
|
+
stepStyle?.paddingBottom ?? globalStyle?.paddingBottom ?? merged.padding,
|
|
63
|
+
paddingLeft:
|
|
64
|
+
stepStyle?.paddingLeft ?? globalStyle?.paddingLeft ?? merged.padding,
|
|
65
|
+
};
|
|
66
|
+
}
|
package/src/hooks/useTour.ts
CHANGED