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