react-native-lumen 1.0.0 → 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.
Files changed (51) hide show
  1. package/README.md +763 -231
  2. package/lib/module/components/TourOverlay.js +43 -3
  3. package/lib/module/components/TourOverlay.js.map +1 -1
  4. package/lib/module/components/TourProvider.js +318 -63
  5. package/lib/module/components/TourProvider.js.map +1 -1
  6. package/lib/module/components/TourTooltip.js +121 -79
  7. package/lib/module/components/TourTooltip.js.map +1 -1
  8. package/lib/module/components/TourZone.js +186 -119
  9. package/lib/module/components/TourZone.js.map +1 -1
  10. package/lib/module/constants/defaults.js +43 -0
  11. package/lib/module/constants/defaults.js.map +1 -1
  12. package/lib/module/context/TourContext.js +5 -0
  13. package/lib/module/context/TourContext.js.map +1 -0
  14. package/lib/module/hooks/useTour.js +1 -1
  15. package/lib/module/hooks/useTour.js.map +1 -1
  16. package/lib/module/hooks/useTourScrollView.js +71 -0
  17. package/lib/module/hooks/useTourScrollView.js.map +1 -0
  18. package/lib/module/index.js +6 -0
  19. package/lib/module/index.js.map +1 -1
  20. package/lib/module/utils/storage.js +188 -0
  21. package/lib/module/utils/storage.js.map +1 -0
  22. package/lib/typescript/src/components/TourOverlay.d.ts.map +1 -1
  23. package/lib/typescript/src/components/TourProvider.d.ts +21 -4
  24. package/lib/typescript/src/components/TourProvider.d.ts.map +1 -1
  25. package/lib/typescript/src/components/TourTooltip.d.ts.map +1 -1
  26. package/lib/typescript/src/components/TourZone.d.ts +19 -1
  27. package/lib/typescript/src/components/TourZone.d.ts.map +1 -1
  28. package/lib/typescript/src/constants/defaults.d.ts +10 -0
  29. package/lib/typescript/src/constants/defaults.d.ts.map +1 -1
  30. package/lib/typescript/src/context/TourContext.d.ts +3 -0
  31. package/lib/typescript/src/context/TourContext.d.ts.map +1 -0
  32. package/lib/typescript/src/hooks/useTourScrollView.d.ts +65 -0
  33. package/lib/typescript/src/hooks/useTourScrollView.d.ts.map +1 -0
  34. package/lib/typescript/src/index.d.ts +4 -0
  35. package/lib/typescript/src/index.d.ts.map +1 -1
  36. package/lib/typescript/src/types/index.d.ts +296 -1
  37. package/lib/typescript/src/types/index.d.ts.map +1 -1
  38. package/lib/typescript/src/utils/storage.d.ts +51 -0
  39. package/lib/typescript/src/utils/storage.d.ts.map +1 -0
  40. package/package.json +173 -171
  41. package/src/components/TourOverlay.tsx +45 -2
  42. package/src/components/TourProvider.tsx +409 -57
  43. package/src/components/TourTooltip.tsx +151 -74
  44. package/src/components/TourZone.tsx +238 -141
  45. package/src/constants/defaults.ts +51 -0
  46. package/src/context/TourContext.ts +4 -0
  47. package/src/hooks/useTour.ts +1 -1
  48. package/src/hooks/useTourScrollView.ts +111 -0
  49. package/src/index.tsx +27 -0
  50. package/src/types/index.ts +306 -1
  51. package/src/utils/storage.ts +226 -0
@@ -1,7 +1,7 @@
1
1
  import React, {
2
2
  useEffect,
3
3
  useCallback,
4
- useRef,
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 { InternalTourContextType } from '../types';
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?: 'rect' | 'circle';
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
- // Track if we're currently scrolling to prevent position updates during scroll
105
+ // The critical lock for the UI thread
67
106
  const isScrolling = useSharedValue(false);
68
- const hasScrolled = useRef(false);
69
107
 
70
- // Signal when scroll completes (from JS thread)
71
- const onScrollComplete = useCallback(() => {
72
- isScrolling.value = false;
73
- }, [isScrolling]);
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
- * UNIFIED MEASUREMENT FUNCTION (JS THREAD)
77
- * Always measures relative to SCREEN (Viewport), not Content.
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 (isScrolling.value || !isActive) {
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, stepKey, updateStepLayout, viewRef, isScrolling, isActive]);
128
-
129
- // Initial measurement when step becomes active
130
- useEffect(() => {
131
- if (!isActive) return;
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 (e) {
179
- // Silently ignore measurement errors on UI thread
180
- }
181
- }, isActive);
197
+ }, [containerRef, isActive, isScrolling, stepKey, updateStepLayout, viewRef]);
182
198
 
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
- hasScrolled.current = false;
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 = 3;
210
+ const maxAttempts = 5;
211
+ let hasInitiatedScroll = false;
196
212
 
197
- const attemptMeasurement = (delay: number) => {
198
- const timeoutId = setTimeout(() => {
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
- // 1. Check current visibility on screen
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 > viewportHeight - bottomBuffer;
240
+ py < topBuffer || py + mh > SCREEN_HEIGHT - bottomBuffer;
221
241
 
222
242
  if (needsScroll) {
223
- hasScrolled.current = true;
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
- // 3. Measure Element relative to ScrollView Content
254
+ if (cancelled) return;
255
+
238
256
  if (view.measureLayout) {
239
257
  view.measureLayout(
240
258
  scroll,
241
259
  (contentX: number, contentY: number) => {
242
- // Calculate target scroll position (center the element)
260
+ if (cancelled) return;
261
+
243
262
  const centerY =
244
- contentY - viewportHeight / 2 + mh / 2 + 50;
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
- // THE FIX: Add scrollPy (ScrollView's screen Y)
258
- // Visual Y = ScrollViewScreenY + (ElementContentY - ScrollAmount)
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
- // Element is already visible - just sync position
290
- container.measure(
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
- attemptMeasurement(150 * attemptCount);
319
+ checkAndScroll(150);
313
320
  }
314
321
  }
315
322
  );
316
323
  }, delay);
324
+
317
325
  return timeoutId;
318
326
  };
319
327
 
320
- const timeoutId = attemptMeasurement(150);
321
- return () => clearTimeout(timeoutId);
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
- // Standard onLayout handler (uses the unified measureJS)
334
- const onLayout = () => {
335
- measureJS();
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
- meta: { shape, borderRadius },
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,12 +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
- registerStep,
358
450
  unregisterStep,
359
451
  clickable,
452
+ preventInteraction,
453
+ required,
454
+ completed,
455
+ resolvedZoneStyle,
456
+ renderCustomCard,
360
457
  ]);
361
458
 
362
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
+ }
@@ -0,0 +1,4 @@
1
+ import { createContext } from 'react';
2
+ import type { InternalTourContextType } from '../types';
3
+
4
+ export const TourContext = createContext<InternalTourContextType | null>(null);
@@ -1,5 +1,5 @@
1
1
  import { useContext } from 'react';
2
- import { TourContext } from '../components/TourProvider';
2
+ import { TourContext } from '../context/TourContext';
3
3
 
4
4
  export const useTour = () => {
5
5
  const context = useContext(TourContext);