@korsolutions/guidon 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1098 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var reactNative = require('react-native');
5
+ var zustand = require('zustand');
6
+ var jsxRuntime = require('react/jsx-runtime');
7
+ var Animated = require('react-native-reanimated');
8
+ var Svg = require('react-native-svg');
9
+ var reactNativeSafeAreaContext = require('react-native-safe-area-context');
10
+
11
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
12
+
13
+ var Animated__default = /*#__PURE__*/_interopDefault(Animated);
14
+ var Svg__default = /*#__PURE__*/_interopDefault(Svg);
15
+
16
+ // src/components/GuidonTarget.tsx
17
+ var initialState = {
18
+ config: null,
19
+ isActive: false,
20
+ currentStepIndex: 0,
21
+ isCompleted: false,
22
+ targetMeasurements: {},
23
+ isLoading: false,
24
+ error: null
25
+ };
26
+ var useGuidonStore = zustand.create((set, get) => ({
27
+ ...initialState,
28
+ configure: (config) => {
29
+ set({ config, currentStepIndex: 0, isCompleted: false });
30
+ },
31
+ start: () => {
32
+ const { config } = get();
33
+ if (!config || config.steps.length === 0) return;
34
+ set({ isActive: true, currentStepIndex: 0, isCompleted: false });
35
+ const firstStep = config.steps[0];
36
+ firstStep.onStepEnter?.();
37
+ config.onStepChange?.(0, firstStep);
38
+ },
39
+ next: () => {
40
+ const { config, currentStepIndex, isActive } = get();
41
+ if (!config || !isActive) return;
42
+ const currentStep = config.steps[currentStepIndex];
43
+ currentStep?.onStepExit?.();
44
+ if (currentStepIndex < config.steps.length - 1) {
45
+ const nextIndex = currentStepIndex + 1;
46
+ const nextStep = config.steps[nextIndex];
47
+ set({ currentStepIndex: nextIndex });
48
+ nextStep.onStepEnter?.();
49
+ config.onStepChange?.(nextIndex, nextStep);
50
+ } else {
51
+ get().complete();
52
+ }
53
+ },
54
+ previous: () => {
55
+ const { config, currentStepIndex, isActive } = get();
56
+ if (!config || !isActive || currentStepIndex === 0) return;
57
+ const currentStep = config.steps[currentStepIndex];
58
+ currentStep?.onStepExit?.();
59
+ const prevIndex = currentStepIndex - 1;
60
+ const prevStep = config.steps[prevIndex];
61
+ set({ currentStepIndex: prevIndex });
62
+ prevStep.onStepEnter?.();
63
+ config.onStepChange?.(prevIndex, prevStep);
64
+ },
65
+ goToStep: (index) => {
66
+ const { config, currentStepIndex, isActive } = get();
67
+ if (!config || !isActive) return;
68
+ if (index < 0 || index >= config.steps.length) return;
69
+ const currentStep = config.steps[currentStepIndex];
70
+ currentStep?.onStepExit?.();
71
+ const targetStep = config.steps[index];
72
+ set({ currentStepIndex: index });
73
+ targetStep.onStepEnter?.();
74
+ config.onStepChange?.(index, targetStep);
75
+ },
76
+ skip: () => {
77
+ const { config, currentStepIndex } = get();
78
+ if (!config) return;
79
+ const currentStep = config.steps[currentStepIndex];
80
+ currentStep?.onStepExit?.();
81
+ set({ isActive: false, isCompleted: false });
82
+ config.onSkip?.();
83
+ },
84
+ complete: () => {
85
+ const { config, currentStepIndex } = get();
86
+ if (!config) return;
87
+ const currentStep = config.steps[currentStepIndex];
88
+ currentStep?.onStepExit?.();
89
+ set({ isActive: false, isCompleted: true });
90
+ config.onComplete?.();
91
+ },
92
+ reset: () => {
93
+ set(initialState);
94
+ },
95
+ registerTarget: (targetId, measurements) => {
96
+ set((state) => ({
97
+ targetMeasurements: {
98
+ ...state.targetMeasurements,
99
+ [targetId]: measurements
100
+ }
101
+ }));
102
+ },
103
+ unregisterTarget: (targetId) => {
104
+ set((state) => {
105
+ const { [targetId]: _, ...rest } = state.targetMeasurements;
106
+ return { targetMeasurements: rest };
107
+ });
108
+ },
109
+ setLoading: (isLoading) => {
110
+ set({ isLoading });
111
+ },
112
+ setError: (error) => {
113
+ set({ error });
114
+ }
115
+ }));
116
+ var Guidon = {
117
+ /**
118
+ * Configure the walkthrough with steps and options
119
+ */
120
+ configure: (config) => {
121
+ useGuidonStore.getState().configure(config);
122
+ },
123
+ /**
124
+ * Start the walkthrough
125
+ */
126
+ start: () => {
127
+ useGuidonStore.getState().start();
128
+ },
129
+ /**
130
+ * Go to the next step
131
+ */
132
+ next: () => {
133
+ useGuidonStore.getState().next();
134
+ },
135
+ /**
136
+ * Go to the previous step
137
+ */
138
+ previous: () => {
139
+ useGuidonStore.getState().previous();
140
+ },
141
+ /**
142
+ * Go to a specific step by index
143
+ */
144
+ goToStep: (index) => {
145
+ useGuidonStore.getState().goToStep(index);
146
+ },
147
+ /**
148
+ * Skip the walkthrough
149
+ */
150
+ skip: () => {
151
+ useGuidonStore.getState().skip();
152
+ },
153
+ /**
154
+ * Complete the walkthrough
155
+ */
156
+ complete: () => {
157
+ useGuidonStore.getState().complete();
158
+ },
159
+ /**
160
+ * Reset the walkthrough to initial state
161
+ */
162
+ reset: () => {
163
+ useGuidonStore.getState().reset();
164
+ },
165
+ /**
166
+ * Check if the walkthrough is currently active
167
+ */
168
+ isActive: () => {
169
+ return useGuidonStore.getState().isActive;
170
+ },
171
+ /**
172
+ * Check if the walkthrough has been completed
173
+ */
174
+ isCompleted: () => {
175
+ return useGuidonStore.getState().isCompleted;
176
+ },
177
+ /**
178
+ * Get the current step index
179
+ */
180
+ getCurrentStepIndex: () => {
181
+ return useGuidonStore.getState().currentStepIndex;
182
+ },
183
+ /**
184
+ * Get the current step
185
+ */
186
+ getCurrentStep: () => {
187
+ const state = useGuidonStore.getState();
188
+ return state.config?.steps[state.currentStepIndex] ?? null;
189
+ },
190
+ /**
191
+ * Get all steps
192
+ */
193
+ getSteps: () => {
194
+ return useGuidonStore.getState().config?.steps ?? [];
195
+ },
196
+ /**
197
+ * Subscribe to store changes
198
+ */
199
+ subscribe: useGuidonStore.subscribe
200
+ };
201
+ var useGuidonActive = () => useGuidonStore((state) => state.isActive);
202
+ var useGuidonStep = () => useGuidonStore((state) => {
203
+ if (!state.config || !state.isActive) return null;
204
+ return state.config.steps[state.currentStepIndex];
205
+ });
206
+ var useGuidonProgress = () => useGuidonStore((state) => ({
207
+ currentStep: state.currentStepIndex + 1,
208
+ totalSteps: state.config?.steps.length ?? 0,
209
+ percentage: state.config ? (state.currentStepIndex + 1) / state.config.steps.length * 100 : 0
210
+ }));
211
+ var useTargetMeasurements = (targetId) => useGuidonStore(
212
+ (state) => state.targetMeasurements[targetId]
213
+ );
214
+ function GuidonTarget({
215
+ children,
216
+ targetId,
217
+ active = true
218
+ }) {
219
+ const viewRef = react.useRef(null);
220
+ const registerTarget = useGuidonStore((state) => state.registerTarget);
221
+ const unregisterTarget = useGuidonStore((state) => state.unregisterTarget);
222
+ const isActive = useGuidonStore((state) => state.isActive);
223
+ const config = useGuidonStore((state) => state.config);
224
+ const isTargetNeeded = isActive && config?.steps.some((step) => step.targetId === targetId);
225
+ const measureElement = react.useCallback(() => {
226
+ if (!viewRef.current || !active || !isTargetNeeded) return;
227
+ if (reactNative.Platform.OS === "web") {
228
+ const element = viewRef.current;
229
+ if (element && typeof element.getBoundingClientRect === "function") {
230
+ const rect = element.getBoundingClientRect();
231
+ const measurements = {
232
+ x: rect.left + window.scrollX,
233
+ y: rect.top + window.scrollY,
234
+ width: rect.width,
235
+ height: rect.height
236
+ };
237
+ registerTarget(targetId, measurements);
238
+ }
239
+ } else {
240
+ viewRef.current.measureInWindow((x, y, width, height) => {
241
+ if (width > 0 && height > 0) {
242
+ const measurements = { x, y, width, height };
243
+ registerTarget(targetId, measurements);
244
+ }
245
+ });
246
+ }
247
+ }, [targetId, active, isTargetNeeded, registerTarget]);
248
+ const handleLayout = react.useCallback(
249
+ (_event) => {
250
+ requestAnimationFrame(() => {
251
+ measureElement();
252
+ });
253
+ },
254
+ [measureElement]
255
+ );
256
+ react.useEffect(() => {
257
+ if (isTargetNeeded) {
258
+ const timer = setTimeout(() => {
259
+ measureElement();
260
+ }, 100);
261
+ return () => clearTimeout(timer);
262
+ }
263
+ }, [isTargetNeeded, measureElement]);
264
+ react.useEffect(() => {
265
+ if (reactNative.Platform.OS !== "web" || !isTargetNeeded) return;
266
+ const handleScroll = () => {
267
+ measureElement();
268
+ };
269
+ window.addEventListener("scroll", handleScroll, true);
270
+ window.addEventListener("resize", handleScroll);
271
+ return () => {
272
+ window.removeEventListener("scroll", handleScroll, true);
273
+ window.removeEventListener("resize", handleScroll);
274
+ };
275
+ }, [isTargetNeeded, measureElement]);
276
+ react.useEffect(() => {
277
+ return () => {
278
+ unregisterTarget(targetId);
279
+ };
280
+ }, [targetId, unregisterTarget]);
281
+ return /* @__PURE__ */ jsxRuntime.jsx(
282
+ reactNative.View,
283
+ {
284
+ ref: viewRef,
285
+ onLayout: handleLayout,
286
+ collapsable: false,
287
+ style: { alignSelf: "flex-start" },
288
+ children
289
+ }
290
+ );
291
+ }
292
+ var AnimatedSvg = Animated__default.default.createAnimatedComponent(Svg__default.default);
293
+ var DEFAULT_THEME = {
294
+ backdropColor: "#000000",
295
+ backdropOpacity: 0.75,
296
+ spotlightBorderRadius: 8,
297
+ spotlightPadding: 8
298
+ };
299
+ function GuidonOverlay({
300
+ theme = {},
301
+ animationDuration = 300,
302
+ onBackdropPress
303
+ }) {
304
+ const isActive = useGuidonStore((state) => state.isActive);
305
+ const config = useGuidonStore((state) => state.config);
306
+ const currentStepIndex = useGuidonStore((state) => state.currentStepIndex);
307
+ const targetMeasurements = useGuidonStore((state) => state.targetMeasurements);
308
+ const mergedTheme = { ...DEFAULT_THEME, ...theme };
309
+ const { width: screenWidth, height: screenHeight } = reactNative.Dimensions.get("window");
310
+ const currentStep = config?.steps[currentStepIndex];
311
+ const currentTargetId = currentStep?.targetId;
312
+ const measurements = currentTargetId ? targetMeasurements[currentTargetId] : void 0;
313
+ const spotlight = react.useMemo(() => {
314
+ if (!measurements) {
315
+ return { x: 0, y: 0, width: 0, height: 0 };
316
+ }
317
+ return {
318
+ x: measurements.x - mergedTheme.spotlightPadding,
319
+ y: measurements.y - mergedTheme.spotlightPadding,
320
+ width: measurements.width + mergedTheme.spotlightPadding * 2,
321
+ height: measurements.height + mergedTheme.spotlightPadding * 2
322
+ };
323
+ }, [measurements, mergedTheme.spotlightPadding]);
324
+ const animatedStyle = Animated.useAnimatedStyle(() => {
325
+ return {
326
+ opacity: Animated.withTiming(isActive && measurements ? 1 : 0, {
327
+ duration: animationDuration,
328
+ easing: Animated.Easing.inOut(Animated.Easing.ease)
329
+ })
330
+ };
331
+ }, [isActive, measurements, animationDuration]);
332
+ if (!isActive || !measurements) {
333
+ return null;
334
+ }
335
+ if (reactNative.Platform.OS === "web") {
336
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.TouchableWithoutFeedback, { onPress: onBackdropPress, children: /* @__PURE__ */ jsxRuntime.jsx(Animated__default.default.View, { style: [styles.container, animatedStyle], children: /* @__PURE__ */ jsxRuntime.jsx(
337
+ "div",
338
+ {
339
+ style: {
340
+ position: "absolute",
341
+ inset: 0,
342
+ backgroundColor: mergedTheme.backdropColor,
343
+ opacity: mergedTheme.backdropOpacity,
344
+ clipPath: `polygon(
345
+ 0% 0%,
346
+ 0% 100%,
347
+ ${spotlight.x}px 100%,
348
+ ${spotlight.x}px ${spotlight.y}px,
349
+ ${spotlight.x + spotlight.width}px ${spotlight.y}px,
350
+ ${spotlight.x + spotlight.width}px ${spotlight.y + spotlight.height}px,
351
+ ${spotlight.x}px ${spotlight.y + spotlight.height}px,
352
+ ${spotlight.x}px 100%,
353
+ 100% 100%,
354
+ 100% 0%
355
+ )`
356
+ }
357
+ }
358
+ ) }) });
359
+ }
360
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.TouchableWithoutFeedback, { onPress: onBackdropPress, children: /* @__PURE__ */ jsxRuntime.jsxs(
361
+ AnimatedSvg,
362
+ {
363
+ style: [styles.container, animatedStyle],
364
+ width: screenWidth,
365
+ height: screenHeight,
366
+ viewBox: `0 0 ${screenWidth} ${screenHeight}`,
367
+ children: [
368
+ /* @__PURE__ */ jsxRuntime.jsx(Svg.Defs, { children: /* @__PURE__ */ jsxRuntime.jsxs(Svg.Mask, { id: "spotlight-mask", children: [
369
+ /* @__PURE__ */ jsxRuntime.jsx(Svg.Rect, { x: "0", y: "0", width: "100%", height: "100%", fill: "white" }),
370
+ /* @__PURE__ */ jsxRuntime.jsx(
371
+ Svg.Rect,
372
+ {
373
+ x: spotlight.x,
374
+ y: spotlight.y,
375
+ width: spotlight.width,
376
+ height: spotlight.height,
377
+ rx: mergedTheme.spotlightBorderRadius,
378
+ ry: mergedTheme.spotlightBorderRadius,
379
+ fill: "black"
380
+ }
381
+ )
382
+ ] }) }),
383
+ /* @__PURE__ */ jsxRuntime.jsx(Svg.G, { mask: "url(#spotlight-mask)", children: /* @__PURE__ */ jsxRuntime.jsx(
384
+ Svg.Rect,
385
+ {
386
+ x: "0",
387
+ y: "0",
388
+ width: "100%",
389
+ height: "100%",
390
+ fill: mergedTheme.backdropColor,
391
+ fillOpacity: mergedTheme.backdropOpacity
392
+ }
393
+ ) })
394
+ ]
395
+ }
396
+ ) });
397
+ }
398
+ var styles = reactNative.StyleSheet.create({
399
+ container: {
400
+ ...reactNative.StyleSheet.absoluteFillObject,
401
+ zIndex: 999
402
+ }
403
+ });
404
+ var DEFAULT_THEME2 = {
405
+ tooltipBackgroundColor: "#ffffff",
406
+ tooltipBorderColor: "#e5e5e5",
407
+ tooltipBorderRadius: 12,
408
+ titleColor: "#1a1a1a",
409
+ descriptionColor: "#666666",
410
+ primaryColor: "#007AFF",
411
+ mutedColor: "#999999",
412
+ spotlightPadding: 8
413
+ };
414
+ var TOOLTIP_MARGIN = 16;
415
+ var TOOLTIP_WIDTH = 300;
416
+ function GuidonTooltip({
417
+ theme = {},
418
+ animationDuration = 300,
419
+ renderCustomTooltip,
420
+ labels = {}
421
+ }) {
422
+ const insets = reactNativeSafeAreaContext.useSafeAreaInsets();
423
+ const { width: screenWidth, height: screenHeight } = reactNative.Dimensions.get("window");
424
+ const isActive = useGuidonStore((state) => state.isActive);
425
+ const config = useGuidonStore((state) => state.config);
426
+ const currentStepIndex = useGuidonStore((state) => state.currentStepIndex);
427
+ const targetMeasurements = useGuidonStore((state) => state.targetMeasurements);
428
+ const next = useGuidonStore((state) => state.next);
429
+ const previous = useGuidonStore((state) => state.previous);
430
+ const skip = useGuidonStore((state) => state.skip);
431
+ const mergedTheme = { ...DEFAULT_THEME2, ...theme };
432
+ const mergedLabels = {
433
+ next: labels.next ?? "Next",
434
+ previous: labels.previous ?? "Back",
435
+ skip: labels.skip ?? "Skip",
436
+ finish: labels.finish ?? "Finish",
437
+ stepOf: labels.stepOf ?? ((c, t) => `${c} of ${t}`)
438
+ };
439
+ const currentStep = config?.steps[currentStepIndex];
440
+ const totalSteps = config?.steps.length ?? 0;
441
+ const isFirstStep = currentStepIndex === 0;
442
+ const isLastStep = currentStepIndex === totalSteps - 1;
443
+ const measurements = currentStep?.targetId ? targetMeasurements[currentStep.targetId] : void 0;
444
+ const tooltipPosition = react.useMemo(() => {
445
+ if (!measurements) {
446
+ return { top: screenHeight / 2, left: screenWidth / 2 - TOOLTIP_WIDTH / 2 };
447
+ }
448
+ measurements.y + measurements.height / 2;
449
+ const targetBottom = measurements.y + measurements.height + mergedTheme.spotlightPadding;
450
+ const targetTop = measurements.y - mergedTheme.spotlightPadding;
451
+ let position = currentStep?.tooltipPosition ?? "auto";
452
+ if (position === "auto") {
453
+ const spaceAbove = targetTop - insets.top;
454
+ const spaceBelow = screenHeight - targetBottom - insets.bottom;
455
+ position = spaceBelow >= 200 ? "bottom" : spaceAbove >= 200 ? "top" : "bottom";
456
+ }
457
+ let top;
458
+ let left = Math.max(
459
+ TOOLTIP_MARGIN,
460
+ Math.min(
461
+ measurements.x + measurements.width / 2 - TOOLTIP_WIDTH / 2,
462
+ screenWidth - TOOLTIP_WIDTH - TOOLTIP_MARGIN
463
+ )
464
+ );
465
+ if (position === "top") {
466
+ top = targetTop - TOOLTIP_MARGIN - 150;
467
+ } else {
468
+ top = targetBottom + TOOLTIP_MARGIN;
469
+ }
470
+ top = Math.max(insets.top + TOOLTIP_MARGIN, Math.min(top, screenHeight - 200 - insets.bottom));
471
+ return { top, left, position };
472
+ }, [measurements, screenWidth, screenHeight, insets, currentStep?.tooltipPosition, mergedTheme.spotlightPadding]);
473
+ const animatedStyle = Animated.useAnimatedStyle(() => {
474
+ return {
475
+ opacity: Animated.withTiming(isActive && measurements ? 1 : 0, {
476
+ duration: animationDuration,
477
+ easing: Animated.Easing.inOut(Animated.Easing.ease)
478
+ }),
479
+ transform: [
480
+ {
481
+ translateY: Animated.withSpring(isActive && measurements ? 0 : 20, {
482
+ damping: 15,
483
+ stiffness: 150
484
+ })
485
+ }
486
+ ]
487
+ };
488
+ }, [isActive, measurements, animationDuration]);
489
+ if (!isActive || !currentStep || !measurements) {
490
+ return null;
491
+ }
492
+ if (renderCustomTooltip) {
493
+ return /* @__PURE__ */ jsxRuntime.jsx(
494
+ Animated__default.default.View,
495
+ {
496
+ style: [
497
+ styles2.tooltipContainer,
498
+ {
499
+ top: tooltipPosition.top,
500
+ left: tooltipPosition.left,
501
+ width: TOOLTIP_WIDTH
502
+ },
503
+ animatedStyle
504
+ ],
505
+ children: renderCustomTooltip({
506
+ step: currentStep,
507
+ currentIndex: currentStepIndex,
508
+ totalSteps,
509
+ onNext: next,
510
+ onPrevious: previous,
511
+ onSkip: skip
512
+ })
513
+ }
514
+ );
515
+ }
516
+ return /* @__PURE__ */ jsxRuntime.jsxs(
517
+ Animated__default.default.View,
518
+ {
519
+ style: [
520
+ styles2.tooltipContainer,
521
+ {
522
+ top: tooltipPosition.top,
523
+ left: tooltipPosition.left,
524
+ width: TOOLTIP_WIDTH,
525
+ backgroundColor: mergedTheme.tooltipBackgroundColor,
526
+ borderColor: mergedTheme.tooltipBorderColor,
527
+ borderRadius: mergedTheme.tooltipBorderRadius
528
+ },
529
+ animatedStyle
530
+ ],
531
+ children: [
532
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles2.progressContainer, children: [
533
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles2.progressText, { color: mergedTheme.mutedColor }], children: mergedLabels.stepOf(currentStepIndex + 1, totalSteps) }),
534
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles2.progressDots, children: Array.from({ length: totalSteps }).map((_, i) => /* @__PURE__ */ jsxRuntime.jsx(
535
+ reactNative.View,
536
+ {
537
+ style: [
538
+ styles2.progressDot,
539
+ {
540
+ backgroundColor: i === currentStepIndex ? mergedTheme.primaryColor : mergedTheme.mutedColor,
541
+ opacity: i === currentStepIndex ? 1 : 0.3
542
+ }
543
+ ]
544
+ },
545
+ i
546
+ )) })
547
+ ] }),
548
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles2.content, children: [
549
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles2.title, { color: mergedTheme.titleColor }], children: currentStep.title }),
550
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles2.description, { color: mergedTheme.descriptionColor }], children: currentStep.description }),
551
+ currentStep.customContent
552
+ ] }),
553
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles2.buttonContainer, children: [
554
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.TouchableOpacity, { onPress: skip, style: styles2.skipButton, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles2.skipText, { color: mergedTheme.mutedColor }], children: mergedLabels.skip }) }),
555
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles2.navButtons, children: [
556
+ !isFirstStep && /* @__PURE__ */ jsxRuntime.jsx(
557
+ reactNative.TouchableOpacity,
558
+ {
559
+ onPress: previous,
560
+ style: [styles2.navButton, styles2.backButton],
561
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles2.backButtonText, { color: mergedTheme.primaryColor }], children: mergedLabels.previous })
562
+ }
563
+ ),
564
+ /* @__PURE__ */ jsxRuntime.jsx(
565
+ reactNative.TouchableOpacity,
566
+ {
567
+ onPress: next,
568
+ style: [
569
+ styles2.navButton,
570
+ styles2.nextButton,
571
+ { backgroundColor: mergedTheme.primaryColor }
572
+ ],
573
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles2.nextButtonText, children: isLastStep ? mergedLabels.finish : mergedLabels.next })
574
+ }
575
+ )
576
+ ] })
577
+ ] })
578
+ ]
579
+ }
580
+ );
581
+ }
582
+ var styles2 = reactNative.StyleSheet.create({
583
+ tooltipContainer: {
584
+ position: "absolute",
585
+ zIndex: 1e3,
586
+ borderWidth: 1,
587
+ padding: 16,
588
+ ...reactNative.Platform.select({
589
+ ios: {
590
+ shadowColor: "#000",
591
+ shadowOffset: { width: 0, height: 4 },
592
+ shadowOpacity: 0.15,
593
+ shadowRadius: 12
594
+ },
595
+ android: {
596
+ elevation: 8
597
+ },
598
+ web: {
599
+ boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)"
600
+ }
601
+ })
602
+ },
603
+ progressContainer: {
604
+ flexDirection: "row",
605
+ alignItems: "center",
606
+ justifyContent: "space-between",
607
+ marginBottom: 12
608
+ },
609
+ progressText: {
610
+ fontSize: 12,
611
+ fontWeight: "500"
612
+ },
613
+ progressDots: {
614
+ flexDirection: "row",
615
+ gap: 4
616
+ },
617
+ progressDot: {
618
+ width: 6,
619
+ height: 6,
620
+ borderRadius: 3
621
+ },
622
+ content: {
623
+ marginBottom: 16
624
+ },
625
+ title: {
626
+ fontSize: 18,
627
+ fontWeight: "600",
628
+ marginBottom: 8
629
+ },
630
+ description: {
631
+ fontSize: 14,
632
+ lineHeight: 20
633
+ },
634
+ buttonContainer: {
635
+ flexDirection: "row",
636
+ alignItems: "center",
637
+ justifyContent: "space-between"
638
+ },
639
+ skipButton: {
640
+ paddingVertical: 8,
641
+ paddingHorizontal: 4
642
+ },
643
+ skipText: {
644
+ fontSize: 14
645
+ },
646
+ navButtons: {
647
+ flexDirection: "row",
648
+ gap: 8
649
+ },
650
+ navButton: {
651
+ paddingVertical: 10,
652
+ paddingHorizontal: 16,
653
+ borderRadius: 8
654
+ },
655
+ backButton: {
656
+ backgroundColor: "transparent"
657
+ },
658
+ backButtonText: {
659
+ fontSize: 14,
660
+ fontWeight: "600"
661
+ },
662
+ nextButton: {},
663
+ nextButtonText: {
664
+ color: "#ffffff",
665
+ fontSize: 14,
666
+ fontWeight: "600"
667
+ }
668
+ });
669
+ function useGuidonPersistence(adapter, guidonId) {
670
+ const [progress, setProgress] = react.useState(null);
671
+ const [isLoading, setIsLoading] = react.useState(true);
672
+ const [error, setError] = react.useState(null);
673
+ react.useEffect(() => {
674
+ if (!adapter) {
675
+ setIsLoading(false);
676
+ return;
677
+ }
678
+ let mounted = true;
679
+ const loadProgress = async () => {
680
+ try {
681
+ setIsLoading(true);
682
+ setError(null);
683
+ const data = await adapter.loadProgress(guidonId);
684
+ if (mounted) {
685
+ setProgress(data);
686
+ }
687
+ } catch (err) {
688
+ if (mounted) {
689
+ setError(err instanceof Error ? err.message : "Failed to load progress");
690
+ }
691
+ } finally {
692
+ if (mounted) {
693
+ setIsLoading(false);
694
+ }
695
+ }
696
+ };
697
+ loadProgress();
698
+ return () => {
699
+ mounted = false;
700
+ };
701
+ }, [adapter, guidonId]);
702
+ const saveProgress = react.useCallback(
703
+ async (newProgress) => {
704
+ if (!adapter) return;
705
+ const fullProgress = {
706
+ ...newProgress,
707
+ guidonId
708
+ };
709
+ try {
710
+ setError(null);
711
+ await adapter.saveProgress(fullProgress);
712
+ setProgress(fullProgress);
713
+ } catch (err) {
714
+ setError(err instanceof Error ? err.message : "Failed to save progress");
715
+ throw err;
716
+ }
717
+ },
718
+ [adapter, guidonId]
719
+ );
720
+ const clearProgress = react.useCallback(async () => {
721
+ if (!adapter?.clearProgress) return;
722
+ try {
723
+ setError(null);
724
+ await adapter.clearProgress(guidonId);
725
+ setProgress(null);
726
+ } catch (err) {
727
+ setError(err instanceof Error ? err.message : "Failed to clear progress");
728
+ throw err;
729
+ }
730
+ }, [adapter, guidonId]);
731
+ const markCompleted = react.useCallback(async () => {
732
+ const currentCount = progress?.completionCount ?? 0;
733
+ await saveProgress({
734
+ completed: true,
735
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
736
+ completionCount: currentCount + 1
737
+ });
738
+ }, [saveProgress, progress?.completionCount]);
739
+ const markStepViewed = react.useCallback(
740
+ async (stepIndex) => {
741
+ await saveProgress({
742
+ completed: progress?.completed ?? false,
743
+ lastStepIndex: stepIndex,
744
+ completedAt: progress?.completedAt,
745
+ completionCount: progress?.completionCount
746
+ });
747
+ },
748
+ [saveProgress, progress]
749
+ );
750
+ return {
751
+ progress,
752
+ isLoading,
753
+ error,
754
+ isCompleted: progress?.completed ?? false,
755
+ hasStarted: progress !== null,
756
+ saveProgress,
757
+ clearProgress,
758
+ markCompleted,
759
+ markStepViewed
760
+ };
761
+ }
762
+ function useShouldShowGuidon(adapter, guidonId, options) {
763
+ const { progress, isLoading } = useGuidonPersistence(adapter, guidonId);
764
+ const [shouldShow, setShouldShow] = react.useState(false);
765
+ const [isChecking, setIsChecking] = react.useState(true);
766
+ react.useEffect(() => {
767
+ if (isLoading) return;
768
+ const checkCondition = async () => {
769
+ setIsChecking(true);
770
+ if (options?.forceShow) {
771
+ setShouldShow(true);
772
+ setIsChecking(false);
773
+ return;
774
+ }
775
+ if (progress?.completed) {
776
+ setShouldShow(false);
777
+ setIsChecking(false);
778
+ return;
779
+ }
780
+ if (options?.additionalCondition) {
781
+ try {
782
+ const result = await options.additionalCondition();
783
+ setShouldShow(result);
784
+ } catch {
785
+ setShouldShow(true);
786
+ }
787
+ } else {
788
+ setShouldShow(true);
789
+ }
790
+ setIsChecking(false);
791
+ };
792
+ checkCondition();
793
+ }, [isLoading, progress?.completed, options?.forceShow, options?.additionalCondition]);
794
+ return {
795
+ shouldShow,
796
+ isChecking: isLoading || isChecking,
797
+ isCompleted: progress?.completed ?? false
798
+ };
799
+ }
800
+ var GuidonContext = react.createContext(null);
801
+ function useGuidonContext() {
802
+ const context = react.useContext(GuidonContext);
803
+ if (!context) {
804
+ throw new Error("useGuidonContext must be used within a GuidonProvider");
805
+ }
806
+ return context;
807
+ }
808
+ function GuidonProvider({
809
+ children,
810
+ config,
811
+ autoStart = true,
812
+ shouldStart,
813
+ persistenceAdapter,
814
+ portalComponent: Portal,
815
+ renderTooltip,
816
+ tooltipLabels,
817
+ onBackdropPress
818
+ }) {
819
+ const hasInitialized = react.useRef(false);
820
+ const isActive = useGuidonStore((state) => state.isActive);
821
+ const storeIsCompleted = useGuidonStore((state) => state.isCompleted);
822
+ const currentStepIndex = useGuidonStore((state) => state.currentStepIndex);
823
+ const configure = useGuidonStore((state) => state.configure);
824
+ const start = useGuidonStore((state) => state.start);
825
+ const skip = useGuidonStore((state) => state.skip);
826
+ const reset = useGuidonStore((state) => state.reset);
827
+ const {
828
+ isLoading,
829
+ isCompleted: persistedCompleted,
830
+ markCompleted,
831
+ markStepViewed,
832
+ clearProgress
833
+ } = useGuidonPersistence(persistenceAdapter, config.id);
834
+ const isCompleted = storeIsCompleted || persistedCompleted;
835
+ react.useEffect(() => {
836
+ const enhancedConfig = {
837
+ ...config,
838
+ onComplete: async () => {
839
+ config.onComplete?.();
840
+ if (persistenceAdapter) {
841
+ await markCompleted();
842
+ }
843
+ },
844
+ onSkip: async () => {
845
+ config.onSkip?.();
846
+ if (persistenceAdapter) {
847
+ await markStepViewed(currentStepIndex);
848
+ }
849
+ },
850
+ onStepChange: async (stepIndex, step) => {
851
+ config.onStepChange?.(stepIndex, step);
852
+ if (persistenceAdapter) {
853
+ await markStepViewed(stepIndex);
854
+ }
855
+ }
856
+ };
857
+ configure(enhancedConfig);
858
+ }, [config, configure, persistenceAdapter, markCompleted, markStepViewed, currentStepIndex]);
859
+ react.useEffect(() => {
860
+ if (!autoStart || hasInitialized.current || isLoading) return;
861
+ const checkAndStart = async () => {
862
+ hasInitialized.current = true;
863
+ if (persistedCompleted) return;
864
+ if (shouldStart) {
865
+ const should = await shouldStart();
866
+ if (!should) return;
867
+ }
868
+ setTimeout(() => {
869
+ start();
870
+ }, 500);
871
+ };
872
+ checkAndStart();
873
+ }, [autoStart, isLoading, persistedCompleted, shouldStart, start]);
874
+ const replay = react.useCallback(async () => {
875
+ if (persistenceAdapter) {
876
+ await clearProgress();
877
+ }
878
+ reset();
879
+ hasInitialized.current = false;
880
+ setTimeout(() => {
881
+ start();
882
+ }, 100);
883
+ }, [persistenceAdapter, clearProgress, reset, start]);
884
+ const manualStart = react.useCallback(() => {
885
+ if (!isActive && !isLoading) {
886
+ start();
887
+ }
888
+ }, [isActive, isLoading, start]);
889
+ const contextValue = {
890
+ start: manualStart,
891
+ skip,
892
+ reset,
893
+ replay,
894
+ isActive,
895
+ isCompleted,
896
+ isLoading
897
+ };
898
+ const overlayContent = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
899
+ /* @__PURE__ */ jsxRuntime.jsx(
900
+ GuidonOverlay,
901
+ {
902
+ theme: config.theme,
903
+ animationDuration: config.animationDuration,
904
+ onBackdropPress
905
+ }
906
+ ),
907
+ /* @__PURE__ */ jsxRuntime.jsx(
908
+ GuidonTooltip,
909
+ {
910
+ theme: config.theme,
911
+ animationDuration: config.animationDuration,
912
+ renderCustomTooltip: renderTooltip,
913
+ labels: tooltipLabels
914
+ }
915
+ )
916
+ ] });
917
+ return /* @__PURE__ */ jsxRuntime.jsxs(GuidonContext.Provider, { value: contextValue, children: [
918
+ children,
919
+ Portal ? /* @__PURE__ */ jsxRuntime.jsx(Portal, { children: overlayContent }) : overlayContent
920
+ ] });
921
+ }
922
+
923
+ // src/persistence/adapters.ts
924
+ var STORAGE_KEY_PREFIX = "@guidon:";
925
+ var createNoopAdapter = () => ({
926
+ loadProgress: async () => null,
927
+ saveProgress: async () => {
928
+ },
929
+ loadAllProgress: async () => ({}),
930
+ clearProgress: async () => {
931
+ }
932
+ });
933
+ var createMemoryAdapter = () => {
934
+ const store = {};
935
+ return {
936
+ loadProgress: async (guidonId) => store[guidonId] ?? null,
937
+ saveProgress: async (progress) => {
938
+ store[progress.guidonId] = progress;
939
+ },
940
+ loadAllProgress: async () => ({ ...store }),
941
+ clearProgress: async (guidonId) => {
942
+ delete store[guidonId];
943
+ }
944
+ };
945
+ };
946
+ var createLocalStorageAdapter = (keyPrefix = STORAGE_KEY_PREFIX) => ({
947
+ loadProgress: async (guidonId) => {
948
+ if (typeof window === "undefined" || !window.localStorage) {
949
+ return null;
950
+ }
951
+ try {
952
+ const data = localStorage.getItem(`${keyPrefix}${guidonId}`);
953
+ return data ? JSON.parse(data) : null;
954
+ } catch {
955
+ return null;
956
+ }
957
+ },
958
+ saveProgress: async (progress) => {
959
+ if (typeof window === "undefined" || !window.localStorage) {
960
+ return;
961
+ }
962
+ try {
963
+ localStorage.setItem(
964
+ `${keyPrefix}${progress.guidonId}`,
965
+ JSON.stringify(progress)
966
+ );
967
+ } catch {
968
+ }
969
+ },
970
+ loadAllProgress: async () => {
971
+ if (typeof window === "undefined" || !window.localStorage) {
972
+ return {};
973
+ }
974
+ const result = {};
975
+ try {
976
+ for (let i = 0; i < localStorage.length; i++) {
977
+ const key = localStorage.key(i);
978
+ if (key?.startsWith(keyPrefix)) {
979
+ const data = localStorage.getItem(key);
980
+ if (data) {
981
+ const progress = JSON.parse(data);
982
+ result[progress.guidonId] = progress;
983
+ }
984
+ }
985
+ }
986
+ } catch {
987
+ }
988
+ return result;
989
+ },
990
+ clearProgress: async (guidonId) => {
991
+ if (typeof window === "undefined" || !window.localStorage) {
992
+ return;
993
+ }
994
+ try {
995
+ localStorage.removeItem(`${keyPrefix}${guidonId}`);
996
+ } catch {
997
+ }
998
+ }
999
+ });
1000
+ var createAsyncStorageAdapter = (asyncStorage, keyPrefix = STORAGE_KEY_PREFIX) => ({
1001
+ loadProgress: async (guidonId) => {
1002
+ try {
1003
+ const data = await asyncStorage.getItem(`${keyPrefix}${guidonId}`);
1004
+ return data ? JSON.parse(data) : null;
1005
+ } catch {
1006
+ return null;
1007
+ }
1008
+ },
1009
+ saveProgress: async (progress) => {
1010
+ try {
1011
+ await asyncStorage.setItem(
1012
+ `${keyPrefix}${progress.guidonId}`,
1013
+ JSON.stringify(progress)
1014
+ );
1015
+ } catch {
1016
+ }
1017
+ },
1018
+ loadAllProgress: async () => {
1019
+ const result = {};
1020
+ try {
1021
+ const allKeys = await asyncStorage.getAllKeys();
1022
+ const relevantKeys = allKeys.filter((key) => key.startsWith(keyPrefix));
1023
+ const pairs = await asyncStorage.multiGet(relevantKeys);
1024
+ for (const [, value] of pairs) {
1025
+ if (value) {
1026
+ const progress = JSON.parse(value);
1027
+ result[progress.guidonId] = progress;
1028
+ }
1029
+ }
1030
+ } catch {
1031
+ }
1032
+ return result;
1033
+ },
1034
+ clearProgress: async (guidonId) => {
1035
+ try {
1036
+ await asyncStorage.removeItem(`${keyPrefix}${guidonId}`);
1037
+ } catch {
1038
+ }
1039
+ }
1040
+ });
1041
+ var createApiAdapter = (handlers) => {
1042
+ const noopAdapter = createNoopAdapter();
1043
+ return {
1044
+ loadProgress: handlers.loadProgress ?? noopAdapter.loadProgress,
1045
+ saveProgress: handlers.saveProgress ?? noopAdapter.saveProgress,
1046
+ loadAllProgress: handlers.loadAllProgress ?? noopAdapter.loadAllProgress,
1047
+ clearProgress: handlers.clearProgress ?? noopAdapter.clearProgress
1048
+ };
1049
+ };
1050
+ var createCompositeAdapter = (adapters) => ({
1051
+ loadProgress: async (guidonId) => {
1052
+ for (const adapter of adapters) {
1053
+ const progress = await adapter.loadProgress(guidonId);
1054
+ if (progress) return progress;
1055
+ }
1056
+ return null;
1057
+ },
1058
+ saveProgress: async (progress) => {
1059
+ await Promise.all(adapters.map((adapter) => adapter.saveProgress(progress)));
1060
+ },
1061
+ loadAllProgress: async () => {
1062
+ const result = {};
1063
+ for (const adapter of adapters) {
1064
+ if (adapter.loadAllProgress) {
1065
+ const data = await adapter.loadAllProgress();
1066
+ Object.assign(result, data);
1067
+ }
1068
+ }
1069
+ return result;
1070
+ },
1071
+ clearProgress: async (guidonId) => {
1072
+ await Promise.all(
1073
+ adapters.map((adapter) => adapter.clearProgress?.(guidonId))
1074
+ );
1075
+ }
1076
+ });
1077
+
1078
+ exports.Guidon = Guidon;
1079
+ exports.GuidonOverlay = GuidonOverlay;
1080
+ exports.GuidonProvider = GuidonProvider;
1081
+ exports.GuidonTarget = GuidonTarget;
1082
+ exports.GuidonTooltip = GuidonTooltip;
1083
+ exports.createApiAdapter = createApiAdapter;
1084
+ exports.createAsyncStorageAdapter = createAsyncStorageAdapter;
1085
+ exports.createCompositeAdapter = createCompositeAdapter;
1086
+ exports.createLocalStorageAdapter = createLocalStorageAdapter;
1087
+ exports.createMemoryAdapter = createMemoryAdapter;
1088
+ exports.createNoopAdapter = createNoopAdapter;
1089
+ exports.useGuidonActive = useGuidonActive;
1090
+ exports.useGuidonContext = useGuidonContext;
1091
+ exports.useGuidonPersistence = useGuidonPersistence;
1092
+ exports.useGuidonProgress = useGuidonProgress;
1093
+ exports.useGuidonStep = useGuidonStep;
1094
+ exports.useGuidonStore = useGuidonStore;
1095
+ exports.useShouldShowGuidon = useShouldShowGuidon;
1096
+ exports.useTargetMeasurements = useTargetMeasurements;
1097
+ //# sourceMappingURL=index.js.map
1098
+ //# sourceMappingURL=index.js.map