UNPKG

just-a-walkthrough

Version:

Framework-agnostic onboarding walkthrough / product tour library with optional React provider & Tailwind/shadcn support.

1,294 lines (1,293 loc) 44.5 kB
import wt, { useState as A, useEffect as H, useRef as bt, useCallback as K, createContext as vt, useContext as kt } from "react"; var P = { exports: {} }, C = {}; /** * @license React * react-jsx-runtime.production.js * * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ var X; function Et() { if (X) return C; X = 1; var s = Symbol.for("react.transitional.element"), t = Symbol.for("react.fragment"); function e(o, c, l) { var n = null; if (l !== void 0 && (n = "" + l), c.key !== void 0 && (n = "" + c.key), "key" in c) { l = {}; for (var i in c) i !== "key" && (l[i] = c[i]); } else l = c; return c = l.ref, { $$typeof: s, type: o, key: n, ref: c !== void 0 ? c : null, props: l }; } return C.Fragment = t, C.jsx = e, C.jsxs = e, C; } var I = {}; /** * @license React * react-jsx-runtime.development.js * * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ var Z; function _t() { return Z || (Z = 1, process.env.NODE_ENV !== "production" && (function() { function s(r) { if (r == null) return null; if (typeof r == "function") return r.$$typeof === gt ? null : r.displayName || r.name || null; if (typeof r == "string") return r; switch (r) { case x: return "Fragment"; case L: return "Profiler"; case _: return "StrictMode"; case pt: return "Suspense"; case ht: return "SuspenseList"; case mt: return "Activity"; } if (typeof r == "object") switch (typeof r.tag == "number" && console.error( "Received an unexpected object in getComponentNameFromType(). This is likely a bug in React. Please file an issue." ), r.$$typeof) { case N: return "Portal"; case ut: return (r.displayName || "Context") + ".Provider"; case ct: return (r._context.displayName || "Context") + ".Consumer"; case dt: var h = r.render; return r = r.displayName, r || (r = h.displayName || h.name || "", r = r !== "" ? "ForwardRef(" + r + ")" : "ForwardRef"), r; case ft: return h = r.displayName || null, h !== null ? h : s(r.type) || "Memo"; case B: h = r._payload, r = r._init; try { return s(r(h)); } catch { } } return null; } function t(r) { return "" + r; } function e(r) { try { t(r); var h = !1; } catch { h = !0; } if (h) { h = console; var m = h.error, w = typeof Symbol == "function" && Symbol.toStringTag && r[Symbol.toStringTag] || r.constructor.name || "Object"; return m.call( h, "The provided key is an unsupported type %s. This value must be coerced to a string before using it here.", w ), t(r); } } function o(r) { if (r === x) return "<>"; if (typeof r == "object" && r !== null && r.$$typeof === B) return "<...>"; try { var h = s(r); return h ? "<" + h + ">" : "<...>"; } catch { return "<...>"; } } function c() { var r = M.A; return r === null ? null : r.getOwner(); } function l() { return Error("react-stack-top-frame"); } function n(r) { if (U.call(r, "key")) { var h = Object.getOwnPropertyDescriptor(r, "key").get; if (h && h.isReactWarning) return !1; } return r.key !== void 0; } function i(r, h) { function m() { Y || (Y = !0, console.error( "%s: `key` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://react.dev/link/special-props)", h )); } m.isReactWarning = !0, Object.defineProperty(r, "key", { get: m, configurable: !0 }); } function a() { var r = s(this.type); return q[r] || (q[r] = !0, console.error( "Accessing element.ref was removed in React 19. ref is now a regular prop. It will be removed from the JSX Element type in a future release." )), r = this.props.ref, r !== void 0 ? r : null; } function u(r, h, m, w, T, k, D, F) { return m = k.ref, r = { $$typeof: y, type: r, key: h, props: k, _owner: T }, (m !== void 0 ? m : null) !== null ? Object.defineProperty(r, "ref", { enumerable: !1, get: a }) : Object.defineProperty(r, "ref", { enumerable: !1, value: null }), r._store = {}, Object.defineProperty(r._store, "validated", { configurable: !1, enumerable: !1, writable: !0, value: 0 }), Object.defineProperty(r, "_debugInfo", { configurable: !1, enumerable: !1, writable: !0, value: null }), Object.defineProperty(r, "_debugStack", { configurable: !1, enumerable: !1, writable: !0, value: D }), Object.defineProperty(r, "_debugTask", { configurable: !1, enumerable: !1, writable: !0, value: F }), Object.freeze && (Object.freeze(r.props), Object.freeze(r)), r; } function p(r, h, m, w, T, k, D, F) { var b = h.children; if (b !== void 0) if (w) if (yt(b)) { for (w = 0; w < b.length; w++) d(b[w]); Object.freeze && Object.freeze(b); } else console.error( "React.jsx: Static children should always be an array. You are likely explicitly calling React.jsxs or React.jsxDEV. Use the Babel transform instead." ); else d(b); if (U.call(h, "key")) { b = s(r); var S = Object.keys(h).filter(function(xt) { return xt !== "key"; }); w = 0 < S.length ? "{key: someKey, " + S.join(": ..., ") + ": ...}" : "{key: someKey}", G[b + w] || (S = 0 < S.length ? "{" + S.join(": ..., ") + ": ...}" : "{}", console.error( `A props object containing a "key" prop is being spread into JSX: let props = %s; <%s {...props} /> React keys must be passed directly to JSX without using spread: let props = %s; <%s key={someKey} {...props} />`, w, b, S, b ), G[b + w] = !0); } if (b = null, m !== void 0 && (e(m), b = "" + m), n(h) && (e(h.key), b = "" + h.key), "key" in h) { m = {}; for (var W in h) W !== "key" && (m[W] = h[W]); } else m = h; return b && i( m, typeof r == "function" ? r.displayName || r.name || "Unknown" : r ), u( r, b, k, T, c(), m, D, F ); } function d(r) { typeof r == "object" && r !== null && r.$$typeof === y && r._store && (r._store.validated = 1); } var f = wt, y = Symbol.for("react.transitional.element"), N = Symbol.for("react.portal"), x = Symbol.for("react.fragment"), _ = Symbol.for("react.strict_mode"), L = Symbol.for("react.profiler"), ct = Symbol.for("react.consumer"), ut = Symbol.for("react.context"), dt = Symbol.for("react.forward_ref"), pt = Symbol.for("react.suspense"), ht = Symbol.for("react.suspense_list"), ft = Symbol.for("react.memo"), B = Symbol.for("react.lazy"), mt = Symbol.for("react.activity"), gt = Symbol.for("react.client.reference"), M = f.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE, U = Object.prototype.hasOwnProperty, yt = Array.isArray, $ = console.createTask ? console.createTask : function() { return null; }; f = { react_stack_bottom_frame: function(r) { return r(); } }; var Y, q = {}, J = f.react_stack_bottom_frame.bind( f, l )(), V = $(o(l)), G = {}; I.Fragment = x, I.jsx = function(r, h, m, w, T) { var k = 1e4 > M.recentlyCreatedOwnerStacks++; return p( r, h, m, !1, w, T, k ? Error("react-stack-top-frame") : J, k ? $(o(r)) : V ); }, I.jsxs = function(r, h, m, w, T) { var k = 1e4 > M.recentlyCreatedOwnerStacks++; return p( r, h, m, !0, w, T, k ? Error("react-stack-top-frame") : J, k ? $(o(r)) : V ); }; })()), I; } var Q; function Tt() { return Q || (Q = 1, process.env.NODE_ENV === "production" ? P.exports = Et() : P.exports = _t()), P.exports; } var g = Tt(); let tt = !1; try { typeof process < "u" && process.env?.JUST_A_WALKTHROUGH_DEBUG === "true" && (tt = !0); } catch { } function et() { return typeof window < "u" && window.__JUST_A_WALKTHROUGH_DEBUG ? !0 : tt; } function v(s, t, e, o) { et(); } new Proxy({}, { get(s, t) { return (...e) => { if (et()) try { console[t]?.("[walkthrough]", ...e); } catch { } }; } }); const R = typeof window < "u" && typeof document < "u"; function St(s) { try { const t = document.implementation.createHTMLDocument("wt"), e = t.createElement("div"); e.innerHTML = s; const o = /* @__PURE__ */ new Set(["SCRIPT", "STYLE", "TEMPLATE", "IFRAME", "OBJECT", "EMBED"]), c = ["href", "src", "xlink:href"], l = /^(https?:|mailto:|tel:|data:image\/(?:png|gif|jpeg|jpg|webp|svg\+xml);)/i, n = t.createTreeWalker(e, NodeFilter.SHOW_ELEMENT), i = []; for (; n.nextNode(); ) { const a = n.currentNode; if (o.has(a.tagName)) { i.push(a); continue; } for (const u of Array.from(a.attributes)) { const p = u.name.toLowerCase(); if (p.startsWith("on") || p === "srcdoc") { a.removeAttribute(u.name); continue; } if (c.includes(p)) { const d = u.value.trim(); d && !l.test(d) && a.removeAttribute(u.name); } } } for (const a of i) a.remove(); return e.innerHTML; } catch { const t = document.createElement("div"); return t.textContent = s, t.innerHTML; } } class O { steps; opts; index = -1; active = !1; root; overlayParts; resizeHandler = () => this.reposition(); scrollHandler = () => this.reposition(); keyHandler = (t) => this.onKey(t); mutationObserver; focusTrapDisposer; /** * Create a new walkthrough. * @param steps Ordered list of steps. * @param options Optional configuration. */ constructor(t, e = {}) { this.steps = t.map((o) => ({ ...o })), this.opts = { backdropOpacity: e.backdropOpacity ?? 0.55, zIndex: e.zIndex ?? 9999, keyboard: e.keyboard ?? !0, allowBodyScroll: e.allowBodyScroll ?? !1, // Allow explicit 0 to mean "no waiting / single attempt" but clamp negatives stepWaitMs: Math.max(0, e.stepWaitMs ?? 5e3), // A poll interval of 0 can create a tight loop under some schedulers; treat 0 as 1ms minimum stepPollIntervalMs: Math.max(0, e.stepPollIntervalMs ?? 120), onFinish: e.onFinish ?? (() => { }), onSkip: e.onSkip ?? (() => { }), onStepChange: e.onStepChange ?? (() => { }), advanceOnTargetClick: e.advanceOnTargetClick ?? !1, advanceOnOverlayClick: e.advanceOnOverlayClick ?? !1, scrollIntoView: e.scrollIntoView ?? !0, scrollOptions: e.scrollOptions, persistProgress: e.persistProgress ?? !1, resume: e.resume ?? !!e.persistProgress, tourId: e.tourId, customTooltip: e.customTooltip, disableFocusTrap: e.disableFocusTrap ?? !1, theme: e.theme ?? "default", tooltipClass: e.tooltipClass, ringClass: e.ringClass, overlayClass: e.overlayClass, alwaysOnTop: e.alwaysOnTop ?? !0 }; } hasDOM() { return typeof window < "u" && typeof document < "u" && !!document.body; } /** * Begin the walkthrough. If persistence+resume is active and stored progress * exists, that index will be used instead of `startIndex`. * Safe to call multiple times (subsequent calls while active are ignored). */ async start(t = 0) { if (this.active) return; if (!this.hasDOM()) { v("walkthrough", "start-no-dom", this.opts.tourId || "<anon>"); return; } if (this.active = !0, this.buildDom(), v("walkthrough", "start", this.opts.tourId || "<anon>", { steps: this.steps.length, resume: this.opts.resume, persist: this.opts.persistProgress }), this.opts.allowBodyScroll || (document.body.style.overflow = "hidden"), R && (window.addEventListener("resize", this.resizeHandler, { passive: !0 }), window.addEventListener("scroll", this.scrollHandler, !0)), this.opts.keyboard && document.addEventListener("keydown", this.keyHandler), this.hasDOM() && typeof MutationObserver < "u") { this.mutationObserver = new MutationObserver(() => { this.hasDOM() && (this.reposition(), this.opts.alwaysOnTop && this.ensureRootOnTop()); }); try { this.mutationObserver.observe(document.body, { attributes: !0, childList: !0, subtree: !0 }); } catch { } } const e = this.loadProgress(), o = this.opts.persistProgress && this.opts.resume && e != null ? e : t; await this.go(o); } /** * Jump to an arbitrary 0‑based step index. If out of range, finishes the tour. * Intended for internal use & custom navigation UIs. */ async go(t) { if (!this.active) return; if (t < 0 || t >= this.steps.length) return this.finish(); if (this.index >= 0) { const o = this.steps[this.index]; o.afterStep && await o.afterStep(); } this.index = t, v("walkthrough", "step", this.opts.tourId || "<anon>", { index: this.index, selector: this.steps[this.index].selector }), this.opts.onStepChange(this.index); const e = this.steps[this.index]; if (e.beforeStep && await e.beforeStep(), e._el = await this.resolveElement(e), !e._el) if (v( "walkthrough", e.required ? "missing-required" : "missing-optional", this.opts.tourId || "<anon>", { selector: e.selector } ), e.required) { this.skip(`Required element not found for selector: ${e.selector}`); return; } else return this.go(t + 1); v("walkthrough", "resolved", this.opts.tourId || "<anon>", { index: this.index, selector: e.selector }), this.renderStep(e), this.saveProgress(); } /** Advance to next step. */ next() { this.go(this.index + 1); } /** Go back to previous step. */ prev() { this.go(this.index - 1); } /** * Complete the walkthrough (fires `onFinish`, marks persisted state as completed). * Further navigation is disabled until a new instance is started. */ finish() { this.active && (this.markCompleted(), this.cleanup(), v("walkthrough", "finish", this.opts.tourId || "<anon>", { finalIndex: this.index }), this.opts.onFinish()); } /** * Abort the walkthrough (fires `onSkip` with a reason token; does NOT mark completed). * Useful for user cancellations or required element failures. */ skip(t) { this.active && (this.cleanup(), v("walkthrough", "skip", this.opts.tourId || "<anon>", { index: this.index }), this.opts.onSkip(t)); } /** Hard teardown: remove DOM & listeners without firing callbacks or marking progress. */ destroy() { this.cleanup(), v("walkthrough", "destroy", this.opts.tourId || "<anon>"); } /** * Attempt to resolve a DOM element for a step, polling until found or timeout. * Fast path: when effective wait is 0 only a single synchronous query is performed. */ async resolveElement(t) { const e = Math.max( 0, t.waitMs == null ? this.opts.stepWaitMs : t.waitMs ); if (e === 0) return document.querySelector(t.selector) || null; const o = Date.now() + e, c = Math.max(1, this.opts.stepPollIntervalMs); for (; Date.now() <= o; ) { const l = document.querySelector(t.selector); if (l) return l; await new Promise((n) => setTimeout(n, c)); } return null; } /** Lazily build overlay DOM (idempotent). */ buildDom() { if (this.root) return; const t = document.createElement("div"); if (t.className = "wt-root", t.setAttribute("data-walkthrough", ""), t.style.position = "fixed", t.style.inset = "0", t.style.zIndex = String(this.opts.zIndex), t.style.pointerEvents = "none", this.opts.theme === "default" && !document.getElementById("__walkthrough_styles")) { const p = document.createElement("style"); p.id = "__walkthrough_styles", p.textContent = this.generateStyles(), document.head.appendChild(p); } const e = (p) => { const d = document.createElement("div"); return d.className = `wt-part wt-overlay ${p}`, d.style.position = "fixed", this.opts.theme === "default" ? d.style.background = `rgba(0,0,0,${this.opts.backdropOpacity})` : this.opts.theme === "tailwind" && d.classList.add("bg-black/60"), this.opts.overlayClass && d.classList.add(...this.opts.overlayClass.split(/\s+/).filter(Boolean)), d.style.pointerEvents = this.opts.advanceOnOverlayClick ? "auto" : "none", d; }, o = e("wt-top"), c = e("wt-left"), l = e("wt-right"), n = e("wt-bottom"), i = document.createElement("div"); i.className = "wt-ring", i.style.position = "fixed", i.style.pointerEvents = "none", this.opts.theme === "default" ? (i.style.border = "2px solid #6366f1", i.style.borderRadius = "8px", i.style.boxShadow = "0 0 0 4px rgba(99,102,241,0.35), 0 4px 18px rgba(0,0,0,0.4)", i.style.transition = "all 180ms cubic-bezier(.4,0,.2,1)") : this.opts.theme === "tailwind" && i.classList.add( "rounded-lg", "border-2", "border-primary", "shadow-[0_0_0_4px_rgba(99,102,241,0.35)]", "transition-all" ), this.opts.ringClass && i.classList.add(...this.opts.ringClass.split(/\s+/).filter(Boolean)); const a = document.createElement("div"); a.className = "wt-tooltip", a.style.position = "fixed", a.style.maxWidth = "340px", a.style.pointerEvents = "auto", a.style.display = "flex", a.style.flexDirection = "column", a.style.gap = "12px", this.opts.theme === "tailwind" ? a.classList.add( "rounded-lg", "border", "p-4", "bg-popover", "text-popover-foreground", "shadow-lg" ) : this.opts.theme === "unstyled" && (a.removeAttribute("style"), a.style.position = "fixed", a.style.pointerEvents = "auto"), this.opts.tooltipClass && a.classList.add( ...this.opts.tooltipClass.split(/\s+/).filter(Boolean) ); const u = document.createElement("div"); u.className = "wt-live", u.setAttribute("aria-live", "polite"), u.setAttribute("role", "status"), u.style.position = "absolute", u.style.width = "1px", u.style.height = "1px", u.style.overflow = "hidden", u.style.clip = "rect(1px,1px,1px,1px)", u.style.whiteSpace = "nowrap", u.style.pointerEvents = "none", t.appendChild(u), [o, c, l, n, i, a].forEach( (p) => t.appendChild(p) ), document.body.appendChild(t), this.root = t, this.overlayParts = { top: o, left: c, right: l, bottom: n, ring: i, tooltip: a, live: u }, this.opts.alwaysOnTop && this.ensureRootOnTop(), this.opts.advanceOnOverlayClick && [o, c, l, n].forEach( (p) => p.addEventListener("click", () => this.next()) ); } /** Ensure root remains the last <body> child (z-order correctness). */ ensureRootOnTop() { if (!this.root) return; const t = document.body; t.lastElementChild !== this.root && t.appendChild(this.root); } /** Generate injected stylesheet (default theme only). */ generateStyles() { return ` .wt-root { font-family: system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; } .wt-tooltip { background: #111827EE; color: #f9fafb; border: 1px solid #374151; border-radius: 10px; padding: 16px 18px; box-shadow: 0 8px 28px -6px rgba(0,0,0,.55); } .wt-tooltip h3 { margin: 0 0 4px; font-size: 16px; font-weight: 600; } .wt-tooltip .wt-content { font-size: 14px; line-height: 1.4; } .wt-tooltip .wt-nav { display:flex; gap:8px; justify-content: flex-end; } .wt-tooltip button { all:unset; font:inherit; background:#6366f1; color:#fff; padding:6px 14px; border-radius:6px; cursor:pointer; font-size:13px; font-weight:500; box-shadow:0 2px 4px rgba(0,0,0,.25); } .wt-tooltip button:hover { background:#4f46e5; } .wt-tooltip button.wt-secondary { background:#374151; } .wt-tooltip button.wt-secondary:hover { background:#4b5563; } @media(prefers-color-scheme:light){ .wt-tooltip { background:#ffffffF2; color:#111827; border-color:#e5e7eb; } .wt-tooltip button.wt-secondary { background:#e5e7eb; color:#111827; } .wt-tooltip button.wt-secondary:hover { background:#d1d5db; } } `; } /** Render highlight + tooltip for a resolved step element. */ renderStep(t) { if (!t._el) return; const e = t.padding ?? 8; if (this.positionHighlight(t._el, e), this.renderTooltip(t), t.focus) try { t._el.focus(); } catch { } if (this.opts.scrollIntoView && t._el) try { t._el.scrollIntoView( this.opts.scrollOptions || { behavior: "smooth", block: "center", inline: "center" } ); } catch { } if (this.opts.advanceOnTargetClick) { const o = () => { t._el?.removeEventListener("click", o), this.next(); }; t._el.addEventListener("click", o, { once: !0 }); } } /** Position the dark overlay panels + highlight ring around the target. */ positionHighlight(t, e) { if (!R || typeof window > "u") return; const o = t.getBoundingClientRect(), c = e, l = o.left - c, n = o.top - c, i = o.width + c * 2, a = o.height + c * 2, { top: u, left: p, right: d, bottom: f, ring: y } = this.overlayParts; u.style.top = "0px", u.style.left = "0px", u.style.width = "100%", u.style.height = `${Math.max(0, n)}px`, p.style.top = `${n}px`, p.style.left = "0px", p.style.width = `${Math.max(0, l)}px`, p.style.height = `${a}px`, d.style.top = `${n}px`, d.style.left = `${l + i}px`, R && (d.style.width = `${Math.max(0, window.innerWidth - (l + i))}px`), d.style.height = `${a}px`, f.style.top = `${n + a}px`, f.style.left = "0px", f.style.width = "100%", R && (f.style.height = `${Math.max(0, window.innerHeight - (n + a))}px`), y.style.top = `${n}px`, y.style.left = `${l}px`, y.style.width = `${i}px`, y.style.height = `${a}px`, typeof window > "u"; } /** Build & mount tooltip content for the current step. */ renderTooltip(t) { const { tooltip: e, live: o } = this.overlayParts; e.innerHTML = "", o.textContent = t.title || ""; let c = null; const l = () => { const i = document.createElement("div"); if (i.className = "wt-nav", this.index > 0) { const d = document.createElement("button"); d.className = "wt-secondary", d.textContent = "Back", d.addEventListener("click", () => this.prev()), i.appendChild(d); } const a = document.createElement("button"); a.className = "wt-secondary", a.textContent = "Skip", a.addEventListener("click", () => this.skip("user-skip")), i.appendChild(a); const u = document.createElement("button"), p = this.index === this.steps.length - 1; return u.textContent = p ? "Done" : "Next", u.addEventListener( "click", () => p ? this.finish() : this.next() ), i.appendChild(u), i; }; if (this.opts.customTooltip) { const i = this.opts.customTooltip({ step: t, index: this.index, total: this.steps.length, api: this, defaultNav: l }); e.appendChild(i); } else { if (t.title) { const i = document.createElement("h3"); i.textContent = t.title, e.appendChild(i); } if (t.content) { const i = document.createElement("div"); i.className = "wt-content", i.innerHTML = t.allowUnsafeHTML ? t.content : St(t.content), e.appendChild(i); } e.appendChild(l()); } t._el && this.positionTooltip(t._el, e), this.opts.disableFocusTrap || this.setupFocusTrap(e); const n = e.querySelectorAll( O.FOCUSABLE_SEL ); n.length ? (c = n[0], setTimeout(() => c?.focus(), 0)) : (e.tabIndex = -1, e.focus()); } static FOCUSABLE_SEL = 'a[href], button:not([disabled]), textarea, input[type="text"], input[type="radio"], input[type="checkbox"], select, [tabindex]:not([tabindex="-1"])'; /** Simple cyclical focus trap for interactive tooltip controls. */ setupFocusTrap(t) { this.teardownFocusTrap(); const e = (o) => { if (o.key !== "Tab") return; const c = Array.from( t.querySelectorAll(O.FOCUSABLE_SEL) ).filter((i) => !i.hasAttribute("disabled")); if (!c.length) { o.preventDefault(), t.focus(); return; } const l = c[0], n = c[c.length - 1]; o.shiftKey ? document.activeElement === l && (o.preventDefault(), n.focus()) : document.activeElement === n && (o.preventDefault(), l.focus()); }; document.addEventListener("keydown", e, !0), this.focusTrapDisposer = () => document.removeEventListener("keydown", e, !0); } /** Remove previously installed focus trap handler if present. */ teardownFocusTrap() { this.focusTrapDisposer?.(), this.focusTrapDisposer = void 0; } /** Compute and set tooltip coordinates (prefers bottom, top, right, left then clamps). */ positionTooltip(t, e) { if (!R || typeof window > "u") return; const o = t.getBoundingClientRect(), c = 14, l = e.offsetWidth || 320, n = e.offsetHeight || 140; let i = o.bottom + c, a = o.left + (o.width - l) / 2, u = !1; const p = (d, f) => d >= 4 && d + l <= window.innerWidth - 4 && f >= 4 && f + n <= window.innerHeight - 4; if (p(a, i) && (u = !0), !u) { const d = o.top - c - n; p(a, d) && (i = d, u = !0); } if (!u) { const d = o.right + c, f = o.top + (o.height - n) / 2; p(d, f) && (a = d, i = f, u = !0); } if (!u) { const d = o.left - c - l, f = o.top + (o.height - n) / 2; p(d, f) && (a = d, i = f, u = !0); } u || (i = Math.min( Math.max(4, o.bottom + c), window.innerHeight - n - 4 ), a = Math.min( Math.max(4, o.left + (o.width - l) / 2), window.innerWidth - l - 4 )), e.style.top = `${i}px`, e.style.left = `${a}px`; } /** Recompute highlight + tooltip position (on resize/scroll/mutation). */ reposition() { if (!this.active) return; const t = this.steps[this.index]; !t || !t._el || (this.positionHighlight(t._el, t.padding ?? 8), this.positionTooltip(t._el, this.overlayParts.tooltip), typeof window > "u"); } /** Keyboard handler for global navigation / dismissal keys. */ onKey(t) { if (this.active) switch (t.key) { case "Escape": this.skip("esc"); break; case "ArrowRight": case "Enter": this.next(); break; case "ArrowLeft": this.prev(); break; } } /** Internal teardown used by finish/skip/destroy (difference is which callbacks fire). */ cleanup() { this.active && (this.active = !1, this.opts.allowBodyScroll || (document.body.style.overflow = ""), R && (window.removeEventListener("resize", this.resizeHandler), window.removeEventListener("scroll", this.scrollHandler, !0)), document.removeEventListener("keydown", this.keyHandler), this.teardownFocusTrap(), this.mutationObserver?.disconnect(), this.root?.parentNode && this.root.parentNode.removeChild(this.root), v("walkthrough", "cleanup", this.opts.tourId || "<anon>", { index: this.index })); } // Persistence helpers /** Build the localStorage key (if persistence enabled). */ progressKey() { return this.opts.tourId ? `__walkthrough:${this.opts.tourId}` : void 0; } /** Persist current index (not yet completed). */ saveProgress() { if (!(!this.opts.persistProgress || !this.opts.tourId)) try { const t = this.progressKey(); t && localStorage.setItem( t, JSON.stringify({ index: this.index, completed: !1, ts: Date.now() }) ); } catch { } } /** Load prior progress index (returns null if completed or unavailable). */ loadProgress() { if (!this.opts.persistProgress || !this.opts.tourId) return null; try { const t = this.progressKey(); if (!t) return null; const e = localStorage.getItem(t); if (!e) return null; const o = JSON.parse(e); if (o.completed) return null; if (typeof o.index == "number") return o.index; } catch { } return null; } /** Persist completion + final index. */ markCompleted() { if (!(!this.opts.persistProgress || !this.opts.tourId)) try { const t = this.progressKey(); t && localStorage.setItem( t, JSON.stringify({ index: this.index, completed: !0, ts: Date.now() }) ); } catch { } } /** Remove any stored progress / completion state for this tour instance. */ clearProgress() { if (this.opts.persistProgress && this.opts.tourId) try { const t = this.progressKey(); t && localStorage.removeItem(t); } catch { } } } function j(s, t) { const e = new O(s, t); return e.start(), e; } class rt { tours; currentIndex = -1; currentInstance = null; constructor(t) { this.tours = t; } start() { this.advanceToNext(); } advanceToNext() { if (this.currentIndex++, this.currentInstance && (this.currentInstance.destroy(), this.currentInstance = null), this.currentIndex >= this.tours.length) return; const t = this.tours[this.currentIndex]; if (t.options?.persistProgress && t.options.tourId) try { const o = localStorage.getItem( `__walkthrough:${t.options.tourId}` ); if (o && JSON.parse(o)?.completed) { this.advanceToNext(); return; } } catch { } const e = new O(t.steps, { ...t.options, onFinish: () => { t.options?.onFinish?.(), this.advanceToNext(); }, onSkip: (o) => { t.options?.onSkip?.(o); } }); this.currentInstance = e, e.start(); } stop() { this.currentInstance?.skip("chain-stop"); } } const E = []; function Rt(s) { const t = E.findIndex((e) => e.id === s.id); t >= 0 ? E.splice(t, 1, s) : E.push(s), v("orchestrator", "register", s.id, { match: s.match }); } function Ct(s) { s.forEach(Rt); } function It() { return [...E]; } function Nt() { E.splice(0, E.length); } function ot(s) { const t = E.filter((e) => Ot(e.match, s)); return v("orchestrator", "match", s, { count: t.length }), t; } function Ot(s, t) { return typeof s == "string" ? s.endsWith("*") ? t.startsWith(s.slice(0, -1)) : t === s : s instanceof RegExp ? s.test(t) : s(t); } function st(s) { return s ? `__walkthrough:${s}` : void 0; } function z(s) { try { const t = st(s); if (!t) return !1; const e = localStorage.getItem(t); return e ? !!JSON.parse(e)?.completed : !1; } catch { return !1; } } function nt(s) { try { const t = st(s); t && localStorage.removeItem(t); } catch { } } async function it({ pathname: s, firstOnly: t }) { const e = ot(s).filter( (c) => (c.trigger ?? "auto") === "auto" ), o = []; for (const c of e) { const { id: l, condition: n, oncePerSession: i, options: a, steps: u, skipIfCompleted: p = !0 } = c; if (!(i && sessionStorage.getItem(`__wt_session_started:${l}`)) && !(p && a?.persistProgress && (a.tourId || l) && z(a.tourId || l)) && !(n && !await n()) && (j(u, { tourId: a?.tourId || l, ...a }), sessionStorage.setItem(`__wt_session_started:${l}`, "1"), o.push(l), t)) break; } return o; } async function at(s) { const t = ot(s).filter( (n) => (n.trigger ?? "auto") === "auto" ), e = []; for (const n of t) { const { id: i, condition: a, oncePerSession: u, options: p, skipIfCompleted: d = !0 } = n; u && sessionStorage.getItem(`__wt_session_started:${i}`) || d && p?.persistProgress && p.tourId && z(p.tourId) || a && !await a() || e.push(n); } if (!e.length) return { ids: [], chain: null }; const o = [...e].sort((n, i) => (n.order ?? 0) - (i.order ?? 0)), c = o.map((n) => n.id); o.forEach( (n) => sessionStorage.setItem(`__wt_session_started:${n.id}`, "1") ); const l = new rt( o.map((n) => ({ id: n.id, steps: n.steps, options: { tourId: n.id, ...n.options } })) ); return l.start(), { ids: c, chain: l }; } function Lt(s) { const t = E.find((e) => e.id === s); if (!t) throw new Error(`Tour '${s}' not registered`); return j(t.steps, { tourId: t.id, ...t.options }); } function Pt() { E.forEach((s) => { (s.options?.tourId || s.id) && nt(s.options?.tourId || s.id); }); } async function At(s) { const t = await import( /* @vite-ignore */ s ), e = t.tours ?? t.default ?? t.toursDefault; return Array.isArray(e) && Ct(e), e; } function Mt({ pathname: s, className: t = "", style: e, chainMatches: o }) { const [c, l] = A(0), [n, i] = A(() => { try { return localStorage.getItem("__wt_devpanel_collapsed") === "1"; } catch { return !1; } }); H(() => { try { localStorage.setItem("__wt_devpanel_collapsed", n ? "1" : "0"); } catch { } }, [n]); const a = It(), u = () => l((x) => x + 1), p = s || (typeof window < "u" ? window.location.pathname : "/"), d = async () => { o ? await at(p) : await it({ pathname: p }), u(); }, f = (x) => { const _ = a.find((L) => L.id === x); _ && j(_.steps, { tourId: _.id, ..._.options }); }, y = (x) => { nt(x), u(); }, N = () => { Pt(), u(); }; return n ? /* @__PURE__ */ g.jsxs( "button", { type: "button", onClick: () => i(!1), className: `walkthrough-dev-panel-collapsed ${t}`, style: { position: "fixed", bottom: 16, right: 16, zIndex: 999999, background: "#111", color: "#fff", border: "1px solid #333", borderRadius: 20, padding: "6px 12px", fontSize: 12, fontFamily: "ui-sans-serif, system-ui, sans-serif", cursor: "pointer", boxShadow: "0 4px 12px rgba(0,0,0,0.5)", ...e }, title: "Expand walkthrough dev panel", children: [ "Tours (", a.length, ") ▴" ] } ) : /* @__PURE__ */ g.jsxs( "div", { className: "walkthrough-dev-panel " + t, style: { position: "fixed", bottom: 16, right: 16, width: 320, maxHeight: "60vh", overflowY: "auto", background: "#111", color: "#fff", fontFamily: "ui-sans-serif, system-ui, sans-serif", fontSize: 12, border: "1px solid #333", borderRadius: 8, padding: 12, boxShadow: "0 4px 12px rgba(0,0,0,0.5)", zIndex: 999999, ...e }, "data-refresh": c, children: [ /* @__PURE__ */ g.jsxs( "div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 8 }, children: [ /* @__PURE__ */ g.jsx("strong", { style: { fontSize: 13 }, children: "Walkthrough Dev Panel" }), /* @__PURE__ */ g.jsxs("div", { style: { display: "flex", gap: 6 }, children: [ /* @__PURE__ */ g.jsx( "button", { type: "button", onClick: () => i(!0), style: { background: "#222", color: "#fff", border: "1px solid #444", borderRadius: 4, padding: "2px 6px", cursor: "pointer" }, title: "Minimize", children: "_" } ), /* @__PURE__ */ g.jsx( "button", { type: "button", onClick: N, style: { background: "#222", color: "#fff", border: "1px solid #444", borderRadius: 4, padding: "2px 6px", cursor: "pointer" }, children: "Reset" } ) ] }) ] } ), /* @__PURE__ */ g.jsxs("div", { style: { display: "flex", gap: 8, marginBottom: 8 }, children: [ /* @__PURE__ */ g.jsx( "button", { type: "button", onClick: d, style: { flex: 1, background: "#2563eb", color: "#fff", border: "none", borderRadius: 4, padding: "4px 6px", cursor: "pointer" }, children: o ? "Chain Matches" : "Run Matches" } ), /* @__PURE__ */ g.jsx( "button", { type: "button", onClick: u, style: { background: "#222", color: "#fff", border: "1px solid #444", borderRadius: 4, padding: "4px 6px", cursor: "pointer" }, children: "Refresh" } ) ] }), a.length === 0 && /* @__PURE__ */ g.jsx("div", { style: { opacity: 0.7 }, children: "No tours registered." }), /* @__PURE__ */ g.jsx( "ul", { style: { listStyle: "none", margin: 0, padding: 0, display: "flex", flexDirection: "column", gap: 8 }, children: a.map((x) => { const _ = z(x.id); return /* @__PURE__ */ g.jsxs( "li", { style: { border: "1px solid #333", borderRadius: 6, padding: 8, background: "#1c1c1c" }, children: [ /* @__PURE__ */ g.jsxs( "div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 4 }, children: [ /* @__PURE__ */ g.jsx("span", { style: { fontWeight: 600 }, children: x.id }), /* @__PURE__ */ g.jsx("span", { style: { fontSize: 10, opacity: 0.7 }, children: _ ? "completed" : "pending" }) ] } ), /* @__PURE__ */ g.jsxs("div", { style: { fontSize: 10, opacity: 0.7, marginBottom: 4 }, children: [ "match:", " ", typeof x.match == "string" ? x.match : x.match instanceof RegExp ? x.match.toString() : "fn" ] }), /* @__PURE__ */ g.jsxs("div", { style: { display: "flex", gap: 6 }, children: [ /* @__PURE__ */ g.jsx( "button", { type: "button", onClick: () => f(x.id), style: { flex: 1, background: "#10b981", color: "#fff", border: "none", borderRadius: 4, padding: "4px 6px", cursor: "pointer" }, children: "Start" } ), /* @__PURE__ */ g.jsx( "button", { type: "button", onClick: () => y(x.id), style: { background: "#dc2626", color: "#fff", border: "none", borderRadius: 4, padding: "4px 6px", cursor: "pointer" }, children: "Reset" } ) ] }) ] }, x.id ); }) } ) ] } ); } const lt = vt( void 0 ); function $t({ children: s, autoStart: t }) { const [e, o] = A(null), [c, l] = A(null), n = !!e, i = bt(null), a = K( (p, d) => { i.current?.destroy(); const f = j(p, { ...d || {}, onStepChange: (y) => { l(y), d?.onStepChange?.(y); }, onFinish: () => { d?.onFinish?.(), i.current = null, o(null), l(null); }, onSkip: (y) => { d?.onSkip?.(y), i.current = null, o(null), l(null); } }); return i.current = f, o(f), f; }, [] ), u = K((p) => { const d = new rt( p.map((f) => ({ ...f, options: { ...f.options || {}, onStepChange: (y) => { l(y), f.options?.onStepChange?.(y); }, onFinish: () => { f.options?.onFinish?.(); }, onSkip: (y) => { f.options?.onSkip?.(y); } } })) ); return d.start(), d; }, []); return H(() => { t && a(t.steps, t.options); }, []), /* @__PURE__ */ g.jsx( lt.Provider, { value: { start: a, chain: u, active: n, currentIndex: c, instance: i.current }, children: s } ); } function Dt() { const s = kt(lt); if (!s) throw new Error("useWalkthrough must be used within WalkthroughProvider"); return s; } function Ft({ pathname: s, firstOnly: t, chain: e, dynamicModule: o, onStartIds: c }) { return H(() => { let l = !1; return (async () => { if (o) try { await At(o); } catch (n) { v("react", "dyn-load-fail", o, { error: n.message }); } if (e) { const { ids: n } = await at(s); !l && n.length && c && c(n); } else { const n = await it({ pathname: s, firstOnly: t }); !l && n.length && c && c(n); } })(), () => { l = !0; }; }, [s, t, e, o, c]), null; } export { Ft as RouteOrchestrator, O as Walkthrough, rt as WalkthroughChain, Mt as WalkthroughDevPanel, $t as WalkthroughProvider, at as chainAutoMatches, nt as clearTourProgress, Nt as clearTours, ot as findMatchingTours, z as isTourCompleted, It as listTours, At as loadTours, Rt as registerTour, Ct as registerTours, Pt as resetAllTourProgress, it as startAutoMatches, Lt as startTourById, j as startWalkthrough, Dt as useWalkthrough }; //# sourceMappingURL=react-walkthrough.esm.js.map