react-native-lumen 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.
Files changed (50) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +231 -0
  3. package/lib/module/components/TourOverlay.js +134 -0
  4. package/lib/module/components/TourOverlay.js.map +1 -0
  5. package/lib/module/components/TourProvider.js +233 -0
  6. package/lib/module/components/TourProvider.js.map +1 -0
  7. package/lib/module/components/TourTooltip.js +233 -0
  8. package/lib/module/components/TourTooltip.js.map +1 -0
  9. package/lib/module/components/TourZone.js +246 -0
  10. package/lib/module/components/TourZone.js.map +1 -0
  11. package/lib/module/constants/animations.js +72 -0
  12. package/lib/module/constants/animations.js.map +1 -0
  13. package/lib/module/constants/defaults.js +14 -0
  14. package/lib/module/constants/defaults.js.map +1 -0
  15. package/lib/module/hooks/useTour.js +12 -0
  16. package/lib/module/hooks/useTour.js.map +1 -0
  17. package/lib/module/index.js +11 -0
  18. package/lib/module/index.js.map +1 -0
  19. package/lib/module/package.json +1 -0
  20. package/lib/module/types/index.js +4 -0
  21. package/lib/module/types/index.js.map +1 -0
  22. package/lib/typescript/package.json +1 -0
  23. package/lib/typescript/src/components/TourOverlay.d.ts +2 -0
  24. package/lib/typescript/src/components/TourOverlay.d.ts.map +1 -0
  25. package/lib/typescript/src/components/TourProvider.d.ts +21 -0
  26. package/lib/typescript/src/components/TourProvider.d.ts.map +1 -0
  27. package/lib/typescript/src/components/TourTooltip.d.ts +2 -0
  28. package/lib/typescript/src/components/TourTooltip.d.ts.map +1 -0
  29. package/lib/typescript/src/components/TourZone.d.ts +16 -0
  30. package/lib/typescript/src/components/TourZone.d.ts.map +1 -0
  31. package/lib/typescript/src/constants/animations.d.ts +34 -0
  32. package/lib/typescript/src/constants/animations.d.ts.map +1 -0
  33. package/lib/typescript/src/constants/defaults.d.ts +10 -0
  34. package/lib/typescript/src/constants/defaults.d.ts.map +1 -0
  35. package/lib/typescript/src/hooks/useTour.d.ts +2 -0
  36. package/lib/typescript/src/hooks/useTour.d.ts.map +1 -0
  37. package/lib/typescript/src/index.d.ts +9 -0
  38. package/lib/typescript/src/index.d.ts.map +1 -0
  39. package/lib/typescript/src/types/index.d.ts +135 -0
  40. package/lib/typescript/src/types/index.d.ts.map +1 -0
  41. package/package.json +171 -0
  42. package/src/components/TourOverlay.tsx +153 -0
  43. package/src/components/TourProvider.tsx +361 -0
  44. package/src/components/TourTooltip.tsx +252 -0
  45. package/src/components/TourZone.tsx +372 -0
  46. package/src/constants/animations.ts +71 -0
  47. package/src/constants/defaults.ts +15 -0
  48. package/src/hooks/useTour.ts +10 -0
  49. package/src/index.tsx +8 -0
  50. package/src/types/index.ts +142 -0
@@ -0,0 +1,372 @@
1
+ import React, {
2
+ useEffect,
3
+ useCallback,
4
+ useRef,
5
+ type ComponentType,
6
+ } from 'react';
7
+ import type { ViewStyle, StyleProp } from 'react-native';
8
+ import { useTour } from '../hooks/useTour';
9
+ import {
10
+ useAnimatedRef,
11
+ measure,
12
+ useFrameCallback,
13
+ withSpring,
14
+ default as Animated,
15
+ type AnimatedRef,
16
+ useSharedValue,
17
+ } from 'react-native-reanimated';
18
+ import { Dimensions } from 'react-native';
19
+ import type { InternalTourContextType } from '../types';
20
+
21
+ const { height: SCREEN_HEIGHT } = Dimensions.get('window');
22
+
23
+ const AnimatedView = Animated.View as unknown as ComponentType<any>;
24
+
25
+ interface TourZoneProps {
26
+ stepKey: string;
27
+ name?: string;
28
+ description: string;
29
+ order?: number;
30
+ shape?: 'rect' | 'circle';
31
+ borderRadius?: number;
32
+ children: React.ReactNode;
33
+ style?: StyleProp<ViewStyle>;
34
+ clickable?: boolean;
35
+ }
36
+
37
+ export const TourZone: React.FC<TourZoneProps> = ({
38
+ stepKey,
39
+ name,
40
+ description,
41
+ order,
42
+ shape = 'rect',
43
+ borderRadius = 10,
44
+ children,
45
+ style,
46
+ clickable,
47
+ }) => {
48
+ const {
49
+ registerStep,
50
+ unregisterStep,
51
+ updateStepLayout,
52
+ currentStep,
53
+ containerRef,
54
+ scrollViewRef,
55
+ targetX,
56
+ targetY,
57
+ targetWidth,
58
+ targetHeight,
59
+ targetRadius,
60
+ config,
61
+ } = useTour() as InternalTourContextType;
62
+ const viewRef = useAnimatedRef<any>();
63
+
64
+ const isActive = currentStep === stepKey;
65
+
66
+ // Track if we're currently scrolling to prevent position updates during scroll
67
+ const isScrolling = useSharedValue(false);
68
+ const hasScrolled = useRef(false);
69
+
70
+ // Signal when scroll completes (from JS thread)
71
+ const onScrollComplete = useCallback(() => {
72
+ isScrolling.value = false;
73
+ }, [isScrolling]);
74
+
75
+ /**
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.
79
+ */
80
+ const measureJS = useCallback(() => {
81
+ if (isScrolling.value || !isActive) {
82
+ return;
83
+ }
84
+
85
+ const view = viewRef.current as any;
86
+ const container = containerRef.current as any;
87
+
88
+ if (view && container) {
89
+ // 1. Measure the View in Screen Coordinates (PageX/PageY)
90
+ view.measure(
91
+ (
92
+ _x: number,
93
+ _y: number,
94
+ width: number,
95
+ height: number,
96
+ pageX: number,
97
+ pageY: number
98
+ ) => {
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
+ container.measure(
102
+ (
103
+ _cx: number,
104
+ _cy: number,
105
+ _cw: number,
106
+ _ch: number,
107
+ containerPageX: number,
108
+ containerPageY: number
109
+ ) => {
110
+ if (width > 0 && height > 0 && !isNaN(pageX) && !isNaN(pageY)) {
111
+ // Calculate final position relative to the Tour Overlay
112
+ const finalX = pageX - containerPageX;
113
+ const finalY = pageY - containerPageY;
114
+
115
+ updateStepLayout(stepKey, {
116
+ x: finalX,
117
+ y: finalY,
118
+ width,
119
+ height,
120
+ });
121
+ }
122
+ }
123
+ );
124
+ }
125
+ );
126
+ }
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);
182
+
183
+ // Auto-scroll Effect
184
+ useEffect(() => {
185
+ if (!isActive || !scrollViewRef?.current || !viewRef.current) {
186
+ return;
187
+ }
188
+
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
+
194
+ let attemptCount = 0;
195
+ const maxAttempts = 3;
196
+
197
+ const attemptMeasurement = (delay: number) => {
198
+ const timeoutId = setTimeout(() => {
199
+ if (hasScrolled.current) return;
200
+
201
+ attemptCount++;
202
+
203
+ // 1. Check current visibility on screen
204
+ view.measure(
205
+ (
206
+ _mx: number,
207
+ _my: number,
208
+ mw: number,
209
+ mh: number,
210
+ px: number,
211
+ py: number
212
+ ) => {
213
+ if (mw > 0 && mh > 0 && !isNaN(px) && !isNaN(py)) {
214
+ const viewportHeight = SCREEN_HEIGHT;
215
+ const topBuffer = 100;
216
+ const bottomBuffer = 150;
217
+
218
+ // Check if element is out of the "safe" visual zone
219
+ const needsScroll =
220
+ py < topBuffer || py + mh > viewportHeight - bottomBuffer;
221
+
222
+ if (needsScroll) {
223
+ hasScrolled.current = true;
224
+ isScrolling.value = true;
225
+
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
+ scroll.measure(
229
+ (
230
+ _sx: number,
231
+ _sy: number,
232
+ _sw: number,
233
+ _sh: number,
234
+ scrollPx: number,
235
+ scrollPy: number
236
+ ) => {
237
+ // 3. Measure Element relative to ScrollView Content
238
+ if (view.measureLayout) {
239
+ view.measureLayout(
240
+ scroll,
241
+ (contentX: number, contentY: number) => {
242
+ // Calculate target scroll position (center the element)
243
+ const centerY =
244
+ contentY - viewportHeight / 2 + mh / 2 + 50;
245
+ const scrollY = Math.max(0, centerY);
246
+
247
+ // 4. Measure Container to map coordinates to Overlay space
248
+ container.measure(
249
+ (
250
+ _cx: number,
251
+ _cy: number,
252
+ _cw: number,
253
+ _ch: number,
254
+ containerPx: number,
255
+ containerPy: number
256
+ ) => {
257
+ // THE FIX: Add scrollPy (ScrollView's screen Y)
258
+ // Visual Y = ScrollViewScreenY + (ElementContentY - ScrollAmount)
259
+ const targetScreenY =
260
+ scrollPy + contentY - scrollY - containerPy;
261
+
262
+ // X is simpler: ScrollViewScreenX + ElementContentX - ContainerScreenX
263
+ const targetScreenX =
264
+ scrollPx + contentX - containerPx;
265
+
266
+ updateStepLayout(stepKey, {
267
+ x: targetScreenX,
268
+ y: targetScreenY,
269
+ width: mw,
270
+ height: mh,
271
+ });
272
+
273
+ try {
274
+ scroll.scrollTo({ y: scrollY, animated: true });
275
+ // Wait for scroll animation
276
+ setTimeout(() => onScrollComplete(), 800);
277
+ } catch (e) {
278
+ console.error(e);
279
+ onScrollComplete();
280
+ }
281
+ }
282
+ );
283
+ }
284
+ );
285
+ }
286
+ }
287
+ );
288
+ } 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
+ );
310
+ }
311
+ } else if (attemptCount < maxAttempts) {
312
+ attemptMeasurement(150 * attemptCount);
313
+ }
314
+ }
315
+ );
316
+ }, delay);
317
+ return timeoutId;
318
+ };
319
+
320
+ const timeoutId = attemptMeasurement(150);
321
+ return () => clearTimeout(timeoutId);
322
+ }, [
323
+ isActive,
324
+ scrollViewRef,
325
+ viewRef,
326
+ stepKey,
327
+ isScrolling,
328
+ onScrollComplete,
329
+ containerRef,
330
+ updateStepLayout,
331
+ ]);
332
+
333
+ // Standard onLayout handler (uses the unified measureJS)
334
+ const onLayout = () => {
335
+ measureJS();
336
+ };
337
+
338
+ // Register step on mount
339
+ useEffect(() => {
340
+ registerStep({
341
+ key: stepKey,
342
+ name,
343
+ description,
344
+ order,
345
+ clickable,
346
+ meta: { shape, borderRadius },
347
+ });
348
+ return () => unregisterStep(stepKey);
349
+ }, [
350
+ stepKey,
351
+ name,
352
+ description,
353
+ order,
354
+ shape,
355
+ borderRadius,
356
+ registerStep,
357
+ registerStep,
358
+ unregisterStep,
359
+ clickable,
360
+ ]);
361
+
362
+ return (
363
+ <AnimatedView
364
+ ref={viewRef}
365
+ onLayout={onLayout}
366
+ style={style}
367
+ collapsable={false}
368
+ >
369
+ {children}
370
+ </AnimatedView>
371
+ );
372
+ };
@@ -0,0 +1,71 @@
1
+ import type { WithSpringConfig } from 'react-native-reanimated';
2
+
3
+ /**
4
+ * Default spring configuration matching Reanimated 3 defaults.
5
+ */
6
+ export const Reanimated3DefaultSpringConfig: WithSpringConfig = {
7
+ damping: 10,
8
+ mass: 1,
9
+ stiffness: 100,
10
+ };
11
+
12
+ /**
13
+ * Spring configuration with duration.
14
+ */
15
+ export const Reanimated3DefaultSpringConfigWithDuration: WithSpringConfig = {
16
+ duration: 1333,
17
+ dampingRatio: 0.5,
18
+ };
19
+
20
+ /**
21
+ * A bouncy and energetic spring configuration.
22
+ */
23
+ export const WigglySpringConfig: WithSpringConfig = {
24
+ damping: 90,
25
+ mass: 4,
26
+ stiffness: 900,
27
+ };
28
+
29
+ /**
30
+ * A bouncy spring configuration with fixed duration.
31
+ */
32
+ export const WigglySpringConfigWithDuration: WithSpringConfig = {
33
+ duration: 550,
34
+ dampingRatio: 0.75,
35
+ };
36
+
37
+ /**
38
+ * A gentle and smooth spring configuration.
39
+ */
40
+ export const GentleSpringConfig: WithSpringConfig = {
41
+ damping: 120,
42
+ mass: 4,
43
+ stiffness: 900,
44
+ };
45
+
46
+ /**
47
+ * A gentle spring configuration with fixed duration.
48
+ */
49
+ export const GentleSpringConfigWithDuration: WithSpringConfig = {
50
+ duration: 550,
51
+ dampingRatio: 1,
52
+ };
53
+
54
+ /**
55
+ * A snappy and responsive spring configuration.
56
+ */
57
+ export const SnappySpringConfig: WithSpringConfig = {
58
+ damping: 110,
59
+ mass: 4,
60
+ stiffness: 900,
61
+ overshootClamping: true,
62
+ };
63
+
64
+ /**
65
+ * A snappy spring configuration with fixed duration.
66
+ */
67
+ export const SnappySpringConfigWithDuration: WithSpringConfig = {
68
+ duration: 550,
69
+ dampingRatio: 0.92,
70
+ overshootClamping: true,
71
+ };
@@ -0,0 +1,15 @@
1
+ import type { WithSpringConfig } from 'react-native-reanimated';
2
+
3
+ export const DEFAULT_SPRING_CONFIG: WithSpringConfig = {
4
+ damping: 20,
5
+ stiffness: 90,
6
+ };
7
+
8
+ export const DEFAULT_BACKDROP_OPACITY = 0.5;
9
+
10
+ export const DEFAULT_LABELS = {
11
+ next: 'Next',
12
+ previous: 'Previous',
13
+ finish: 'Finish',
14
+ skip: 'Skip',
15
+ };
@@ -0,0 +1,10 @@
1
+ import { useContext } from 'react';
2
+ import { TourContext } from '../components/TourProvider';
3
+
4
+ export const useTour = () => {
5
+ const context = useContext(TourContext);
6
+ if (!context) {
7
+ throw new Error('useTour must be used within a TourProvider');
8
+ }
9
+ return context;
10
+ };
package/src/index.tsx ADDED
@@ -0,0 +1,8 @@
1
+ export * from './types';
2
+ export * from './components/TourProvider';
3
+ export * from './components/TourZone';
4
+ export * from './hooks/useTour';
5
+ export { TourOverlay } from './components/TourOverlay';
6
+ export { TourTooltip } from './components/TourTooltip';
7
+ export * from './constants/defaults';
8
+ export * from './constants/animations';
@@ -0,0 +1,142 @@
1
+ import type { WithSpringConfig, SharedValue } from 'react-native-reanimated';
2
+ import React from 'react';
3
+
4
+ export interface TourStep {
5
+ /**
6
+ * Unique key for this step.
7
+ */
8
+ key: string;
9
+ /**
10
+ * Optional display name/label for this step.
11
+ */
12
+ name?: string;
13
+ /**
14
+ * Description text to show in the tooltip.
15
+ */
16
+ description: string;
17
+ /**
18
+ * Optional order index. If not provided, registration order is used (or explicit ordering in context).
19
+ */
20
+ order?: number;
21
+ /**
22
+ * Optional data for custom tooltip rendering
23
+ */
24
+ meta?: any;
25
+ /**
26
+ * If true, allows user interaction with the target element.
27
+ * If false, interactions are blocked (default behavior depends on global config).
28
+ */
29
+ clickable?: boolean;
30
+ }
31
+
32
+ export interface MeasureResult {
33
+ x: number;
34
+ y: number;
35
+ width: number;
36
+ height: number;
37
+ }
38
+
39
+ export type StepMap = Record<string, TourStep>;
40
+
41
+ export interface TourLabels {
42
+ next?: string;
43
+ previous?: string;
44
+ finish?: string;
45
+ skip?: string;
46
+ }
47
+
48
+ export interface CardProps {
49
+ step: TourStep;
50
+ currentStepIndex: number;
51
+ totalSteps: number;
52
+ next: () => void;
53
+ prev: () => void;
54
+ stop: () => void;
55
+ isFirst: boolean;
56
+ isLast: boolean;
57
+ labels?: TourLabels;
58
+ }
59
+
60
+ export interface TourConfig {
61
+ /**
62
+ * Animation configuration for the spotlight movement.
63
+ */
64
+ springConfig?: WithSpringConfig;
65
+ /**
66
+ * If true, prevents interaction with the underlying app while tour is active.
67
+ * Default: false (interactions allowed outside the tooltip, but overlay might block them depending on implementation).
68
+ */
69
+ preventInteraction?: boolean;
70
+ /**
71
+ * Custom labels for buttons.
72
+ */
73
+ labels?: TourLabels;
74
+ /**
75
+ * Custom renderer for the card/tooltip.
76
+ */
77
+ renderCard?: (props: CardProps) => React.ReactNode;
78
+ /**
79
+ * Backdrop opacity. Default 0.5
80
+ */
81
+ backdropOpacity?: number;
82
+ }
83
+
84
+ export interface TourContextType {
85
+ /**
86
+ * Starts the tour at the first step or a specific step (by key).
87
+ */
88
+ start: (stepKey?: string) => void;
89
+ /**
90
+ * Stops the tour and hides the overlay.
91
+ */
92
+ stop: () => void;
93
+ /**
94
+ * Advances to the next step.
95
+ */
96
+ next: () => void;
97
+ /**
98
+ * Goes back to the previous step.
99
+ */
100
+ prev: () => void;
101
+ /**
102
+ * Registers a zone/step.
103
+ */
104
+ registerStep: (step: TourStep) => void;
105
+ /**
106
+ * Unregisters a zone/step.
107
+ */
108
+ unregisterStep: (key: string) => void;
109
+ /**
110
+ * Updates the layout of a specific step.
111
+ * This is called by TourZone on layout/mount.
112
+ */
113
+ updateStepLayout: (key: string, measure: MeasureResult) => void;
114
+ /**
115
+ * The current active step key, or null if tour is inactive.
116
+ */
117
+ currentStep: string | null;
118
+ /**
119
+ * Map of registered steps.
120
+ */
121
+ steps: StepMap;
122
+ /**
123
+ * Global tour configuration
124
+ */
125
+ config?: TourConfig;
126
+ /**
127
+ * Registers the main ScrollView ref for auto-scrolling
128
+ */
129
+ setScrollViewRef: (ref: any) => void;
130
+ }
131
+
132
+ export interface InternalTourContextType extends TourContextType {
133
+ targetX: SharedValue<number>;
134
+ targetY: SharedValue<number>;
135
+ targetWidth: SharedValue<number>;
136
+ targetHeight: SharedValue<number>;
137
+ targetRadius: SharedValue<number>;
138
+ opacity: SharedValue<number>;
139
+ containerRef: React.RefObject<any>;
140
+ scrollViewRef: React.RefObject<any>;
141
+ setScrollViewRef: (ref: any) => void;
142
+ }