react-headless-tour 0.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/dist/index.js ADDED
@@ -0,0 +1,689 @@
1
+ "use client";
2
+
3
+ // src/TourContext.tsx
4
+ import {
5
+ createContext,
6
+ useCallback,
7
+ useContext,
8
+ useEffect as useEffect4,
9
+ useMemo as useMemo2,
10
+ useRef as useRef2,
11
+ useState as useState4
12
+ } from "react";
13
+ import { createPortal } from "react-dom";
14
+
15
+ // src/motion.ts
16
+ import { useEffect, useState } from "react";
17
+ var EASE_SPRING = "cubic-bezier(0.22, 1, 0.36, 1)";
18
+ function useEntered() {
19
+ const [entered, setEntered] = useState(false);
20
+ useEffect(() => {
21
+ let inner = 0;
22
+ const outer = requestAnimationFrame(() => {
23
+ inner = requestAnimationFrame(() => setEntered(true));
24
+ });
25
+ return () => {
26
+ cancelAnimationFrame(outer);
27
+ cancelAnimationFrame(inner);
28
+ };
29
+ }, []);
30
+ return entered;
31
+ }
32
+ function useIsScrolling(idleMs = 150) {
33
+ const [scrolling, setScrolling] = useState(false);
34
+ useEffect(() => {
35
+ let timer = null;
36
+ const onScroll = () => {
37
+ setScrolling(true);
38
+ if (timer) clearTimeout(timer);
39
+ timer = setTimeout(() => setScrolling(false), idleMs);
40
+ };
41
+ window.addEventListener("scroll", onScroll, { capture: true, passive: true });
42
+ return () => {
43
+ window.removeEventListener("scroll", onScroll, { capture: true });
44
+ if (timer) clearTimeout(timer);
45
+ };
46
+ }, [idleMs]);
47
+ return scrolling;
48
+ }
49
+ function useReducedMotion() {
50
+ const [reduced, setReduced] = useState(false);
51
+ useEffect(() => {
52
+ const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
53
+ setReduced(mq.matches);
54
+ const onChange = (e) => setReduced(e.matches);
55
+ mq.addEventListener("change", onChange);
56
+ return () => mq.removeEventListener("change", onChange);
57
+ }, []);
58
+ return reduced;
59
+ }
60
+
61
+ // src/useTargetRect.ts
62
+ import { useEffect as useEffect2, useState as useState2 } from "react";
63
+ function resolveTarget(step) {
64
+ if (!step?.target) return null;
65
+ if (typeof step.target === "function") return step.target();
66
+ if (typeof document === "undefined") return null;
67
+ return document.querySelector(step.target);
68
+ }
69
+ function readRect(el) {
70
+ const r = el.getBoundingClientRect();
71
+ return { x: r.left, y: r.top, width: r.width, height: r.height };
72
+ }
73
+ function rectsEqual(a, b) {
74
+ if (a === b) return true;
75
+ if (!a || !b) return false;
76
+ return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
77
+ }
78
+ function useTargetRect(step, active) {
79
+ const [rect, setRect] = useState2(null);
80
+ useEffect2(() => {
81
+ if (!active || !step) {
82
+ setRect(null);
83
+ return;
84
+ }
85
+ let el = resolveTarget(step);
86
+ let frame = 0;
87
+ let current = null;
88
+ const update = () => {
89
+ if (!el || !el.isConnected) el = resolveTarget(step);
90
+ const next = el ? readRect(el) : null;
91
+ if (!rectsEqual(current, next)) {
92
+ current = next;
93
+ setRect(next);
94
+ }
95
+ };
96
+ const tick = () => {
97
+ update();
98
+ frame = requestAnimationFrame(tick);
99
+ };
100
+ frame = requestAnimationFrame(tick);
101
+ update();
102
+ return () => cancelAnimationFrame(frame);
103
+ }, [step, active]);
104
+ return rect;
105
+ }
106
+
107
+ // src/TourOverlay.tsx
108
+ import { useId } from "react";
109
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
110
+ function TourOverlay({
111
+ rect,
112
+ step,
113
+ interactive,
114
+ padding,
115
+ radius,
116
+ color,
117
+ blur,
118
+ className,
119
+ onMaskClick
120
+ }) {
121
+ const maskId = useId();
122
+ const reducedMotion = useReducedMotion();
123
+ const scrolling = useIsScrolling();
124
+ const hole = rect ? {
125
+ x: rect.x - padding,
126
+ y: rect.y - padding,
127
+ width: rect.width + padding * 2,
128
+ height: rect.height + padding * 2
129
+ } : null;
130
+ const fill = color ?? "var(--tour-overlay-color, rgba(0, 0, 0, 0.55))";
131
+ const interactable = Boolean(step.interactable && hole);
132
+ const holeTransition = reducedMotion || scrolling ? void 0 : ["x", "y", "width", "height", "rx"].map((p) => `${p} 350ms ${EASE_SPRING}`).join(", ");
133
+ const blockerStyle = {
134
+ position: "absolute",
135
+ pointerEvents: interactive ? "auto" : "none",
136
+ cursor: onMaskClick ? "pointer" : "default"
137
+ };
138
+ return /* @__PURE__ */ jsxs(
139
+ "div",
140
+ {
141
+ "data-tour-overlay": "",
142
+ className,
143
+ style: { position: "absolute", inset: 0, pointerEvents: "none" },
144
+ children: [
145
+ /* @__PURE__ */ jsxs(
146
+ "svg",
147
+ {
148
+ width: "100%",
149
+ height: "100%",
150
+ style: {
151
+ position: "absolute",
152
+ inset: 0,
153
+ backdropFilter: blur ? `blur(${blur}px)` : void 0,
154
+ WebkitBackdropFilter: blur ? `blur(${blur}px)` : void 0
155
+ },
156
+ "aria-hidden": "true",
157
+ children: [
158
+ /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsxs("mask", { id: maskId, children: [
159
+ /* @__PURE__ */ jsx("rect", { x: "0", y: "0", width: "100%", height: "100%", fill: "#fff" }),
160
+ hole && /* @__PURE__ */ jsx(
161
+ "rect",
162
+ {
163
+ x: hole.x,
164
+ y: hole.y,
165
+ width: hole.width,
166
+ height: hole.height,
167
+ rx: radius,
168
+ fill: "#000",
169
+ style: { transition: holeTransition }
170
+ }
171
+ )
172
+ ] }) }),
173
+ /* @__PURE__ */ jsx("rect", { x: "0", y: "0", width: "100%", height: "100%", fill, mask: `url(#${maskId})` })
174
+ ]
175
+ }
176
+ ),
177
+ interactable && hole ? /* @__PURE__ */ jsxs(Fragment, { children: [
178
+ /* @__PURE__ */ jsx("div", { style: { ...blockerStyle, left: 0, top: 0, right: 0, height: Math.max(0, hole.y) }, onClick: onMaskClick }),
179
+ /* @__PURE__ */ jsx("div", { style: { ...blockerStyle, left: 0, top: hole.y + hole.height, right: 0, bottom: 0 }, onClick: onMaskClick }),
180
+ /* @__PURE__ */ jsx("div", { style: { ...blockerStyle, left: 0, top: hole.y, width: Math.max(0, hole.x), height: hole.height }, onClick: onMaskClick }),
181
+ /* @__PURE__ */ jsx("div", { style: { ...blockerStyle, left: hole.x + hole.width, top: hole.y, right: 0, height: hole.height }, onClick: onMaskClick })
182
+ ] }) : /* @__PURE__ */ jsx("div", { style: { ...blockerStyle, inset: 0 }, onClick: onMaskClick })
183
+ ]
184
+ }
185
+ );
186
+ }
187
+
188
+ // src/TourPopover.tsx
189
+ import { useEffect as useEffect3, useMemo, useRef, useState as useState3 } from "react";
190
+ import {
191
+ arrow,
192
+ autoUpdate,
193
+ flip,
194
+ offset,
195
+ shift,
196
+ size,
197
+ useFloating
198
+ } from "@floating-ui/react-dom";
199
+
200
+ // src/DefaultCard.tsx
201
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
202
+ function DefaultCard(props) {
203
+ const { step, stepIndex, totalSteps, isFirst, isLast, next, prev, stop, arrow: arrow2, labels, classNames } = props;
204
+ const buttonBase = {
205
+ font: "inherit",
206
+ fontSize: "0.875em",
207
+ fontWeight: 500,
208
+ border: "var(--tour-border, 1px solid rgba(0,0,0,0.12))",
209
+ borderRadius: "calc(var(--tour-radius, 12px) * 0.6)",
210
+ padding: "0.45em 0.9em",
211
+ cursor: "pointer",
212
+ background: "transparent",
213
+ color: "inherit"
214
+ };
215
+ return /* @__PURE__ */ jsxs2(
216
+ "div",
217
+ {
218
+ "data-tour-card": "",
219
+ className: classNames?.card,
220
+ style: {
221
+ position: "relative",
222
+ background: "var(--tour-bg, #ffffff)",
223
+ color: "var(--tour-fg, #1a1a1a)",
224
+ fontFamily: "var(--tour-font, inherit)",
225
+ borderRadius: "var(--tour-radius, 12px)",
226
+ boxShadow: "var(--tour-shadow, 0 10px 38px -10px rgba(0,0,0,0.35), 0 10px 20px -15px rgba(0,0,0,0.2))",
227
+ padding: "var(--tour-padding, 16px)",
228
+ width: "max-content",
229
+ maxWidth: "min(var(--tour-max-width, 320px), 100%)",
230
+ boxSizing: "border-box"
231
+ },
232
+ children: [
233
+ arrow2,
234
+ step.title != null && /* @__PURE__ */ jsx2(
235
+ "div",
236
+ {
237
+ "data-tour-title": "",
238
+ className: classNames?.title,
239
+ style: { fontWeight: 600, fontSize: "1em", marginBottom: 6 },
240
+ children: step.title
241
+ }
242
+ ),
243
+ step.content != null && /* @__PURE__ */ jsx2(
244
+ "div",
245
+ {
246
+ "data-tour-content": "",
247
+ className: classNames?.content,
248
+ style: { fontSize: "0.9em", lineHeight: 1.5, color: "var(--tour-muted, #555)" },
249
+ children: step.content
250
+ }
251
+ ),
252
+ /* @__PURE__ */ jsxs2(
253
+ "div",
254
+ {
255
+ "data-tour-footer": "",
256
+ className: classNames?.footer,
257
+ style: { display: "flex", alignItems: "center", gap: 8, marginTop: 14 },
258
+ children: [
259
+ /* @__PURE__ */ jsx2(
260
+ "span",
261
+ {
262
+ "data-tour-progress": "",
263
+ className: classNames?.progress,
264
+ style: { fontSize: "0.8em", color: "var(--tour-muted, #888)", marginRight: "auto" },
265
+ children: labels?.progress ? labels.progress(stepIndex, totalSteps) : `${stepIndex + 1} / ${totalSteps}`
266
+ }
267
+ ),
268
+ /* @__PURE__ */ jsx2(
269
+ "button",
270
+ {
271
+ type: "button",
272
+ "data-tour-skip": "",
273
+ className: classNames?.skipButton,
274
+ onClick: () => stop("skipped"),
275
+ style: { ...buttonBase, border: "none", color: "var(--tour-muted, #888)" },
276
+ children: labels?.skip ?? "Skip"
277
+ }
278
+ ),
279
+ !isFirst && /* @__PURE__ */ jsx2(
280
+ "button",
281
+ {
282
+ type: "button",
283
+ "data-tour-prev": "",
284
+ className: classNames?.navButton,
285
+ onClick: prev,
286
+ style: buttonBase,
287
+ children: labels?.prev ?? "Back"
288
+ }
289
+ ),
290
+ /* @__PURE__ */ jsx2(
291
+ "button",
292
+ {
293
+ type: "button",
294
+ "data-tour-next": "",
295
+ className: classNames?.primaryButton,
296
+ onClick: next,
297
+ style: {
298
+ ...buttonBase,
299
+ border: "none",
300
+ background: "var(--tour-accent, #1a1a1a)",
301
+ color: "var(--tour-accent-fg, #ffffff)"
302
+ },
303
+ children: isLast ? labels?.finish ?? "Finish" : labels?.next ?? "Next"
304
+ }
305
+ )
306
+ ]
307
+ }
308
+ )
309
+ ]
310
+ }
311
+ );
312
+ }
313
+
314
+ // src/TourPopover.tsx
315
+ import { jsx as jsx3 } from "react/jsx-runtime";
316
+ function TourPopover({
317
+ rect,
318
+ step,
319
+ controls,
320
+ interactive,
321
+ offset: offsetDistance,
322
+ padding,
323
+ components,
324
+ classNames,
325
+ labels
326
+ }) {
327
+ const arrowRef = useRef(null);
328
+ const popoverRef = useRef(null);
329
+ const hasTarget = Boolean(rect);
330
+ const entered = useEntered();
331
+ const reducedMotion = useReducedMotion();
332
+ useEffect3(() => {
333
+ if (interactive) popoverRef.current?.focus({ preventScroll: true });
334
+ }, [interactive]);
335
+ const [maxSize, setMaxSize] = useState3(null);
336
+ const maxSizeRef = useRef(maxSize);
337
+ maxSizeRef.current = maxSize;
338
+ const { refs, floatingStyles, middlewareData, placement, update, isPositioned } = useFloating({
339
+ placement: step.placement ?? "bottom",
340
+ strategy: "fixed",
341
+ whileElementsMounted: autoUpdate,
342
+ middleware: [
343
+ offset(offsetDistance + padding),
344
+ // If neither side of the preferred axis fits (e.g. a full-width target
345
+ // with placement "right"), try the other axis too.
346
+ flip({ padding: 12, fallbackAxisSideDirection: "start" }),
347
+ // crossAxis lets shift pull the popover back into view even when that
348
+ // means overlapping a large target.
349
+ shift({ padding: 12, crossAxis: true }),
350
+ size({
351
+ padding: 12,
352
+ apply({ availableWidth, availableHeight }) {
353
+ const next = {
354
+ width: Math.max(120, Math.floor(availableWidth)),
355
+ height: Math.max(80, Math.floor(availableHeight))
356
+ };
357
+ const current = maxSizeRef.current;
358
+ if (!current || Math.abs(current.width - next.width) > 1 || Math.abs(current.height - next.height) > 1) {
359
+ setMaxSize(next);
360
+ }
361
+ }
362
+ }),
363
+ arrow({ element: arrowRef, padding: 12 })
364
+ ]
365
+ });
366
+ useEffect3(() => {
367
+ if (!rect) return;
368
+ refs.setReference({
369
+ getBoundingClientRect: () => ({
370
+ x: rect.x,
371
+ y: rect.y,
372
+ width: rect.width,
373
+ height: rect.height,
374
+ top: rect.y,
375
+ left: rect.x,
376
+ right: rect.x + rect.width,
377
+ bottom: rect.y + rect.height
378
+ })
379
+ });
380
+ update();
381
+ }, [rect, refs, update]);
382
+ const side = placement.split("-")[0];
383
+ const arrowNode = useMemo(() => {
384
+ if (!hasTarget) return null;
385
+ const { x, y } = middlewareData.arrow ?? {};
386
+ const staticSide = { top: "bottom", bottom: "top", left: "right", right: "left" }[side];
387
+ const ArrowComponent = components?.Arrow;
388
+ return /* @__PURE__ */ jsx3(
389
+ "div",
390
+ {
391
+ ref: arrowRef,
392
+ "data-tour-arrow": "",
393
+ className: classNames?.arrow,
394
+ style: {
395
+ position: "absolute",
396
+ left: x != null ? x : void 0,
397
+ top: y != null ? y : void 0,
398
+ [staticSide]: "calc(var(--tour-arrow-size, 12px) / -2)",
399
+ pointerEvents: "none"
400
+ },
401
+ children: ArrowComponent ? /* @__PURE__ */ jsx3(ArrowComponent, { side }) : /* @__PURE__ */ jsx3(
402
+ "div",
403
+ {
404
+ style: {
405
+ width: "var(--tour-arrow-size, 12px)",
406
+ height: "var(--tour-arrow-size, 12px)",
407
+ transform: "rotate(45deg)",
408
+ background: "var(--tour-bg, #ffffff)",
409
+ borderRadius: 2
410
+ }
411
+ }
412
+ )
413
+ }
414
+ );
415
+ }, [hasTarget, middlewareData.arrow, side, components?.Arrow, classNames?.arrow]);
416
+ const CustomCard = components?.Card;
417
+ const cardProps = { ...controls, step, arrow: arrowNode };
418
+ const positionStyles = hasTarget ? floatingStyles : {
419
+ position: "fixed",
420
+ top: "50%",
421
+ left: "50%",
422
+ transform: "translate(-50%, -50%)"
423
+ };
424
+ return /* @__PURE__ */ jsx3(
425
+ "div",
426
+ {
427
+ ref: (node) => {
428
+ refs.setFloating(node);
429
+ popoverRef.current = node;
430
+ },
431
+ tabIndex: -1,
432
+ "data-tour-popover": "",
433
+ className: classNames?.popover,
434
+ style: {
435
+ ...positionStyles,
436
+ // Never paint before Floating UI has computed a real position — a
437
+ // freshly mounted popover would otherwise flash at a stale spot.
438
+ visibility: hasTarget && !isPositioned ? "hidden" : void 0,
439
+ pointerEvents: interactive ? "auto" : "none",
440
+ maxWidth: hasTarget && maxSize ? Math.min(maxSize.width, window.innerWidth - 24) : "calc(100vw - 24px)",
441
+ maxHeight: hasTarget && maxSize ? maxSize.height : "calc(100vh - 24px)",
442
+ overflowY: "auto"
443
+ },
444
+ role: "dialog",
445
+ "aria-modal": "false",
446
+ "aria-label": `Tour step ${controls.stepIndex + 1} of ${controls.totalSteps}${typeof step.title === "string" ? `: ${step.title}` : ""}`,
447
+ children: /* @__PURE__ */ jsx3(
448
+ "div",
449
+ {
450
+ style: {
451
+ opacity: entered ? 1 : 0,
452
+ transform: entered ? "none" : `scale(0.96) translateY(${side === "top" ? 6 : -6}px)`,
453
+ transition: reducedMotion ? void 0 : `opacity 200ms ease, transform 250ms ${EASE_SPRING}`
454
+ },
455
+ children: CustomCard ? /* @__PURE__ */ jsx3(CustomCard, { ...cardProps }) : /* @__PURE__ */ jsx3(DefaultCard, { ...cardProps, labels, classNames })
456
+ }
457
+ )
458
+ }
459
+ );
460
+ }
461
+
462
+ // src/TourContext.tsx
463
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
464
+ var TourContext = createContext(null);
465
+ var EXIT_DURATION_MS = 220;
466
+ function useTour() {
467
+ const ctx = useContext(TourContext);
468
+ if (!ctx) throw new Error("useTour must be used within a <TourProvider>");
469
+ return ctx;
470
+ }
471
+ function TourLayer({
472
+ closing,
473
+ zIndex,
474
+ className,
475
+ theme,
476
+ children
477
+ }) {
478
+ const entered = useEntered();
479
+ const reducedMotion = useReducedMotion();
480
+ return /* @__PURE__ */ jsx4(
481
+ "div",
482
+ {
483
+ "data-tour-root": "",
484
+ className,
485
+ style: {
486
+ position: "fixed",
487
+ inset: 0,
488
+ zIndex: `var(--tour-z-index, ${zIndex})`,
489
+ pointerEvents: "none",
490
+ opacity: entered && !closing ? 1 : 0,
491
+ transition: reducedMotion ? void 0 : `opacity ${EXIT_DURATION_MS}ms ease`,
492
+ ...theme
493
+ },
494
+ children
495
+ }
496
+ );
497
+ }
498
+ function TourProvider(props) {
499
+ const {
500
+ children,
501
+ steps,
502
+ autoStart = false,
503
+ stepIndex: controlledIndex,
504
+ onStepChange,
505
+ onStart,
506
+ onStop,
507
+ keyboard = true,
508
+ lockScroll = false,
509
+ scrollIntoViewOptions = { behavior: "smooth", block: "center", inline: "nearest" },
510
+ portalContainer
511
+ } = props;
512
+ const [mounted, setMounted] = useState4(false);
513
+ const [phase, setPhase] = useState4("idle");
514
+ const [internalIndex, setInternalIndex] = useState4(0);
515
+ const isControlled = controlledIndex !== void 0;
516
+ const isActive = phase === "active";
517
+ const stepIndex = isActive ? isControlled ? controlledIndex : internalIndex : -1;
518
+ const step = stepIndex >= 0 && stepIndex < steps.length ? steps[stepIndex] : null;
519
+ const closingSnapshot = useRef2(null);
520
+ const exitTimer = useRef2(null);
521
+ const stateRef = useRef2({ steps, stepIndex, step, phase, onStepChange, onStart, onStop });
522
+ stateRef.current = { steps, stepIndex, step, phase, onStepChange, onStart, onStop };
523
+ useEffect4(() => setMounted(true), []);
524
+ useEffect4(
525
+ () => () => {
526
+ if (exitTimer.current) clearTimeout(exitTimer.current);
527
+ },
528
+ []
529
+ );
530
+ const setIndex = useCallback(
531
+ (index) => {
532
+ const s = stateRef.current;
533
+ const clamped = Math.max(0, Math.min(index, s.steps.length - 1));
534
+ if (clamped === s.stepIndex) return;
535
+ s.step?.onExit?.(s.step, s.stepIndex);
536
+ if (!isControlled) setInternalIndex(clamped);
537
+ s.onStepChange?.(clamped, s.steps[clamped]);
538
+ },
539
+ [isControlled]
540
+ );
541
+ const start = useCallback(
542
+ (atIndex = 0) => {
543
+ const s = stateRef.current;
544
+ if (s.phase === "active" || s.steps.length === 0) return;
545
+ if (exitTimer.current) clearTimeout(exitTimer.current);
546
+ const clamped = Math.max(0, Math.min(atIndex, s.steps.length - 1));
547
+ if (!isControlled) setInternalIndex(clamped);
548
+ else s.onStepChange?.(clamped, s.steps[clamped]);
549
+ setPhase("active");
550
+ s.onStart?.();
551
+ },
552
+ [isControlled]
553
+ );
554
+ const stop = useCallback((reason = "programmatic") => {
555
+ const s = stateRef.current;
556
+ if (s.phase !== "active") return;
557
+ s.step?.onExit?.(s.step, s.stepIndex);
558
+ if (s.step) closingSnapshot.current = { step: s.step, stepIndex: s.stepIndex };
559
+ setPhase("closing");
560
+ if (exitTimer.current) clearTimeout(exitTimer.current);
561
+ exitTimer.current = setTimeout(() => {
562
+ closingSnapshot.current = null;
563
+ setPhase("idle");
564
+ }, EXIT_DURATION_MS);
565
+ s.onStop?.(reason, s.stepIndex);
566
+ }, []);
567
+ const next = useCallback(() => {
568
+ const s = stateRef.current;
569
+ if (s.phase !== "active") return;
570
+ if (s.stepIndex >= s.steps.length - 1) stop("finished");
571
+ else setIndex(s.stepIndex + 1);
572
+ }, [setIndex, stop]);
573
+ const prev = useCallback(() => {
574
+ const s = stateRef.current;
575
+ if (s.phase !== "active" || s.stepIndex <= 0) return;
576
+ setIndex(s.stepIndex - 1);
577
+ }, [setIndex]);
578
+ const goTo = useCallback((index) => setIndex(index), [setIndex]);
579
+ const autoStartedRef = useRef2(false);
580
+ useEffect4(() => {
581
+ if (autoStart && mounted && !autoStartedRef.current) {
582
+ autoStartedRef.current = true;
583
+ start(0);
584
+ }
585
+ }, [autoStart, mounted, start]);
586
+ useEffect4(() => {
587
+ if (!isActive || !step) return;
588
+ step.onEnter?.(step, stepIndex);
589
+ if (!step.disableScroll) {
590
+ const el = resolveTarget(step);
591
+ const reducedMotion = typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
592
+ el?.scrollIntoView(
593
+ reducedMotion ? { ...scrollIntoViewOptions, behavior: "auto" } : scrollIntoViewOptions
594
+ );
595
+ }
596
+ }, [isActive, stepIndex]);
597
+ useEffect4(() => {
598
+ if (!isActive || !keyboard) return;
599
+ const onKey = (e) => {
600
+ if (e.key === "Escape") stop("escape");
601
+ else if (e.key === "ArrowRight" || e.key === "Enter") next();
602
+ else if (e.key === "ArrowLeft") prev();
603
+ };
604
+ window.addEventListener("keydown", onKey);
605
+ return () => window.removeEventListener("keydown", onKey);
606
+ }, [isActive, keyboard, next, prev, stop]);
607
+ useEffect4(() => {
608
+ if (!isActive || !lockScroll) return;
609
+ const previous = document.body.style.overflow;
610
+ document.body.style.overflow = "hidden";
611
+ return () => {
612
+ document.body.style.overflow = previous;
613
+ };
614
+ }, [isActive, lockScroll]);
615
+ const renderStep = step ?? closingSnapshot.current?.step ?? null;
616
+ const renderIndex = step ? stepIndex : closingSnapshot.current?.stepIndex ?? -1;
617
+ const targetRect = useTargetRect(renderStep, phase !== "idle");
618
+ const controls = useMemo2(
619
+ () => ({
620
+ isActive,
621
+ stepIndex,
622
+ totalSteps: steps.length,
623
+ step,
624
+ isFirst: stepIndex <= 0,
625
+ isLast: stepIndex === steps.length - 1,
626
+ start,
627
+ stop,
628
+ next,
629
+ prev,
630
+ goTo
631
+ }),
632
+ [isActive, stepIndex, steps.length, step, start, stop, next, prev, goTo]
633
+ );
634
+ const container = portalContainer ?? (typeof document !== "undefined" ? document.body : null);
635
+ const zIndex = props.zIndex ?? 1e4;
636
+ const closing = phase === "closing";
637
+ return /* @__PURE__ */ jsxs3(TourContext.Provider, { value: controls, children: [
638
+ children,
639
+ mounted && container && phase !== "idle" && renderStep && createPortal(
640
+ /* @__PURE__ */ jsxs3(
641
+ TourLayer,
642
+ {
643
+ closing,
644
+ zIndex,
645
+ className: props.classNames?.root,
646
+ theme: props.theme,
647
+ children: [
648
+ (props.showOverlay ?? true) && /* @__PURE__ */ jsx4(
649
+ TourOverlay,
650
+ {
651
+ rect: targetRect,
652
+ step: renderStep,
653
+ interactive: !closing,
654
+ padding: renderStep.spotlightPadding ?? props.spotlightPadding ?? 8,
655
+ radius: renderStep.spotlightRadius ?? props.spotlightRadius ?? 8,
656
+ color: props.overlayColor,
657
+ blur: props.overlayBlur ?? 0,
658
+ className: props.classNames?.overlay,
659
+ onMaskClick: props.closeOnMaskClick ? () => stop("mask") : void 0
660
+ }
661
+ ),
662
+ /* @__PURE__ */ jsx4(
663
+ TourPopover,
664
+ {
665
+ rect: targetRect,
666
+ controls,
667
+ step: renderStep,
668
+ interactive: !closing,
669
+ offset: props.offset ?? 12,
670
+ padding: renderStep.spotlightPadding ?? props.spotlightPadding ?? 8,
671
+ components: props.components,
672
+ classNames: props.classNames,
673
+ labels: props.labels
674
+ },
675
+ renderIndex
676
+ )
677
+ ]
678
+ }
679
+ ),
680
+ container
681
+ )
682
+ ] });
683
+ }
684
+ export {
685
+ DefaultCard,
686
+ TourProvider,
687
+ useTour
688
+ };
689
+ //# sourceMappingURL=index.js.map