UNPKG

nfsfu234-tour-guide

Version:

A plug-and-play React component for creating customizable onboarding tours with tooltips, modals, and smooth user guidance. Built with Tailwind CSS and supports dark mode, Framer Motion animations, and full control over step logic.

620 lines (617 loc) 21.7 kB
'use strict'; var react = require('react'); var reactDom = require('react-dom'); var jsxRuntime = require('react/jsx-runtime'); // src/Tour.tsx function Branding({ color = "#ffffff" }) { return /* @__PURE__ */ jsxRuntime.jsx( "a", { href: "https://tour-guide.nforshifu234dev.com", target: "_blank", rel: "noopener noreferrer", style: { display: "block", textAlign: "center", marginTop: "16px", fontSize: "11px", color, opacity: 0.35, textDecoration: "none", letterSpacing: "0.02em", transition: "opacity 0.2s" }, onMouseEnter: (e) => e.currentTarget.style.opacity = "0.65", onMouseLeave: (e) => e.currentTarget.style.opacity = "0.35", children: "Built with NFSFU234TourGuide" } ); } var THEME_PRESETS = { dark: { backdrop: "rgba(0, 0, 0, 0.75)", tooltipBg: "#18181b", tooltipText: "#ffffff", tooltipBorder: "#3f3f46", buttonBg: "#27272a", buttonText: "#ffffff", progressBar: "#3f3f46", highlightRing: "rgba(16, 185, 129, 0.5)" }, light: { backdrop: "rgba(0, 0, 0, 0.5)", tooltipBg: "#ffffff", tooltipText: "#18181b", tooltipBorder: "#e4e4e7", buttonBg: "#f4f4f5", buttonText: "#18181b", progressBar: "#e4e4e7", highlightRing: "rgba(59, 130, 246, 0.5)" } }; var DEFAULT_BUTTON_LABELS = { next: "Next", previous: "Back", skip: "Skip", finish: "Finish", start: "Start Tour" }; var DEFAULT_WELCOME_SCREEN = { enabled: false, title: "Welcome", message: "Let's guide you through the key features.", startButtonText: "Start Tour" }; function isMobile() { return typeof window !== "undefined" && window.innerWidth < 768; } function shouldShowStep(step, currentDevice) { if (!step.device || step.device === "both") return true; return currentDevice === "mobile" ? step.device === "mobile" : step.device === "desktop"; } function getArrowStyle(position, color) { const borderColor = color || "#18181b"; switch (position) { case "top": return { content: "", position: "absolute", bottom: "-8px", left: "50%", transform: "translateX(-50%)", width: 0, height: 0, borderLeft: "8px solid transparent", borderRight: "8px solid transparent", borderTop: `8px solid ${borderColor}` }; case "bottom": return { content: "", position: "absolute", top: "-8px", left: "50%", transform: "translateX(-50%)", width: 0, height: 0, borderLeft: "8px solid transparent", borderRight: "8px solid transparent", borderBottom: `8px solid ${borderColor}` }; case "left": return { content: "", position: "absolute", right: "-8px", top: "50%", transform: "translateY(-50%)", width: 0, height: 0, borderTop: "8px solid transparent", borderBottom: "8px solid transparent", borderLeft: `8px solid ${borderColor}` }; case "right": return { content: "", position: "absolute", left: "-8px", top: "50%", transform: "translateY(-50%)", width: 0, height: 0, borderTop: "8px solid transparent", borderBottom: "8px solid transparent", borderRight: `8px solid ${borderColor}` }; default: return { display: "none" }; } } function Tooltip({ step, stepIndex, totalSteps, themeConfig, accentColor, showProgress, buttonLabels, tooltipClassName, onNext, onPrevious, onSkip }) { const [targetElement, setTargetElement] = react.useState(null); const [tooltipStyle, setTooltipStyle] = react.useState({}); const tooltipRef = react.useRef(null); const content = isMobile() && step.contentMobile ? step.contentMobile : step.content; react.useEffect(() => { const target = document.querySelector(step.target); if (!target) { console.warn(`[NFSFU234TourGuide] Target "${step.target}" not found`); return; } setTargetElement(target); target.scrollIntoView({ behavior: "smooth", block: "center" }); target.style.position = "relative"; target.style.zIndex = "9999"; target.classList.add("nfsfu234-tour-active-target"); const observer = new IntersectionObserver( (entries) => { if (!entries[0].isIntersecting) { target.scrollIntoView({ behavior: "smooth", block: "center" }); } }, { threshold: 0.3 } ); observer.observe(target); return () => { observer.disconnect(); target.classList.remove("nfsfu234-tour-active-target"); target.style.zIndex = ""; }; }, [step.target]); react.useEffect(() => { if (!targetElement || !tooltipRef.current) return; let rafId; const updatePosition = () => { const targetRect = targetElement.getBoundingClientRect(); const tooltipRect = tooltipRef.current.getBoundingClientRect(); let position = step.position || "bottom"; const space = { top: targetRect.top, bottom: window.innerHeight - targetRect.bottom, left: targetRect.left, right: window.innerWidth - targetRect.right }; const MIN_SPACE = 40; if (position === "top" && tooltipRect.height + MIN_SPACE > space.top) position = "bottom"; else if (position === "bottom" && tooltipRect.height + MIN_SPACE > space.bottom) position = "top"; else if (position === "left" && tooltipRect.width + MIN_SPACE > space.left) position = "right"; else if (position === "right" && tooltipRect.width + MIN_SPACE > space.right) position = "left"; if (position === "top" && space.top < MIN_SPACE || position === "left" && space.left < MIN_SPACE) { position = position === "top" ? "bottom" : "right"; } let top = 0; let left = 0; const offsetX = step.offset?.x ?? 0; const offsetY = step.offset?.y ?? (isMobile() ? 12 : 16); switch (position) { case "top": top = targetRect.top - tooltipRect.height - offsetY; left = targetRect.left + targetRect.width / 2 - tooltipRect.width / 2 + offsetX; break; case "bottom": top = targetRect.bottom + offsetY; left = targetRect.left + targetRect.width / 2 - tooltipRect.width / 2 + offsetX; break; case "left": top = targetRect.top + targetRect.height / 2 - tooltipRect.height / 2 + offsetY; left = targetRect.left - tooltipRect.width - offsetY; break; case "right": top = targetRect.top + targetRect.height / 2 - tooltipRect.height / 2 + offsetY; left = targetRect.right + offsetY; break; } const vw = window.innerWidth; const vh = window.innerHeight; top = Math.max(16, Math.min(top, vh - tooltipRect.height - 16)); left = Math.max(16, Math.min(left, vw - tooltipRect.width - 16)); setTooltipStyle({ position: "fixed", top: `${top}px`, left: `${left}px`, zIndex: 1e4 }); }; const debouncedUpdate = () => { cancelAnimationFrame(rafId); rafId = requestAnimationFrame(updatePosition); }; debouncedUpdate(); window.addEventListener("scroll", debouncedUpdate, true); window.addEventListener("resize", debouncedUpdate); return () => { cancelAnimationFrame(rafId); window.removeEventListener("scroll", debouncedUpdate, true); window.removeEventListener("resize", debouncedUpdate); }; }, [targetElement, step.position, step.offset, step.target]); if (!targetElement) return null; return reactDom.createPortal( /* @__PURE__ */ jsxRuntime.jsxs( "div", { ref: tooltipRef, className: tooltipClassName, style: { ...tooltipStyle, backgroundColor: themeConfig.tooltipBg, color: themeConfig.tooltipText, border: `1px solid ${themeConfig.tooltipBorder}`, borderRadius: "12px", padding: "20px 24px", maxWidth: "400px", width: isMobile() ? "90%" : "auto", boxShadow: "0 20px 60px rgba(0, 0, 0, 0.3)" }, children: [ /* @__PURE__ */ jsxRuntime.jsx("p", { style: { marginBottom: "16px", fontSize: "15px", lineHeight: "1.6" }, children: content }), showProgress && /* @__PURE__ */ jsxRuntime.jsx( "div", { style: { height: "4px", backgroundColor: themeConfig.progressBar, borderRadius: "4px", overflow: "hidden", marginBottom: "20px" }, children: /* @__PURE__ */ jsxRuntime.jsx( "div", { style: { height: "100%", width: `${(stepIndex + 1) / totalSteps * 100}%`, backgroundColor: accentColor, transition: "width 0.3s ease" } } ) } ), /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center" }, children: [ /* @__PURE__ */ jsxRuntime.jsx( "button", { onClick: onSkip, style: { background: "none", border: "none", color: themeConfig.tooltipText, opacity: 0.7, cursor: "pointer", fontSize: "14px", padding: "8px 0" }, onMouseEnter: (e) => e.currentTarget.style.opacity = "1", onMouseLeave: (e) => e.currentTarget.style.opacity = "0.7", children: buttonLabels.skip } ), /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: "12px" }, children: [ /* @__PURE__ */ jsxRuntime.jsx( "button", { onClick: onPrevious, disabled: stepIndex === 0, style: { padding: "8px 16px", borderRadius: "8px", border: "none", backgroundColor: themeConfig.buttonBg, color: themeConfig.buttonText, cursor: stepIndex === 0 ? "not-allowed" : "pointer", opacity: stepIndex === 0 ? 0.5 : 1, fontSize: "14px" }, children: buttonLabels.previous } ), /* @__PURE__ */ jsxRuntime.jsx( "button", { onClick: onNext, style: { padding: "8px 20px", borderRadius: "8px", border: "none", backgroundColor: accentColor, color: "#ffffff", cursor: "pointer", fontSize: "14px", fontWeight: "500" }, children: stepIndex < totalSteps - 1 ? buttonLabels.next : buttonLabels.finish } ) ] }) ] }), /* @__PURE__ */ jsxRuntime.jsx("div", { style: getArrowStyle(step.position, themeConfig.tooltipBg) }) ] } ), document.body ); } function Tour({ tourId = "nfsfu234-tour-guide", steps, isActive = true, theme = "dark", customTheme, accentColor = "#10b981", onComplete, onSkip, onStart, onStepChange, welcomeScreen, buttonLabels, showProgress = true, showBranding = true, className = "", overlayClassName = "", tooltipClassName = "", highlightClassName = "nfsfu234-tour-highlight" }) { const welcomeConfig = react.useMemo(() => ({ ...DEFAULT_WELCOME_SCREEN, ...welcomeScreen }), [welcomeScreen]); const labels = { ...DEFAULT_BUTTON_LABELS, ...buttonLabels }; const themeConfig = customTheme || (theme !== "custom" ? THEME_PRESETS[theme] : THEME_PRESETS.dark); const [currentDevice, setCurrentDevice] = react.useState(isMobile() ? "mobile" : "desktop"); react.useEffect(() => { const checkDevice = () => { setCurrentDevice(isMobile() ? "mobile" : "desktop"); }; window.addEventListener("resize", checkDevice); checkDevice(); return () => window.removeEventListener("resize", checkDevice); }, []); const filteredSteps = react.useMemo( () => steps.filter((step) => shouldShowStep(step, currentDevice)), [steps, currentDevice] ); const [phase, setPhase] = react.useState( welcomeConfig.enabled ? "welcome" : filteredSteps.length > 0 ? "active" : "done" ); const [currentStep, setCurrentStep] = react.useState(0); const [mounted, setMounted] = react.useState(false); react.useRef(null); react.useEffect(() => { setMounted(true); }, []); react.useEffect(() => { if (isActive) { setPhase(welcomeConfig.enabled ? "welcome" : filteredSteps.length > 0 ? "active" : "done"); setCurrentStep(0); } }, [isActive, welcomeConfig.enabled, filteredSteps.length]); react.useEffect(() => { if (phase !== "active") return; const current = filteredSteps[currentStep]; if (!current || !shouldShowStep(current, currentDevice)) { const nextValidIndex = filteredSteps.findIndex( (s, i) => i > currentStep && shouldShowStep(s, currentDevice) ); if (nextValidIndex !== -1) { setCurrentStep(nextValidIndex); onStepChange?.(nextValidIndex); } else { setPhase("done"); onComplete?.(); } } }, [currentDevice, filteredSteps, currentStep, phase, onStepChange, onComplete]); react.useEffect(() => { if (!mounted || !isActive || phase !== "welcome") return; const scrollY = window.scrollY; document.body.style.position = "fixed"; document.body.style.top = `-${scrollY}px`; document.body.style.width = "100%"; document.body.style.overflow = "hidden"; return () => { document.body.style.position = ""; document.body.style.top = ""; document.body.style.width = ""; document.body.style.overflow = ""; window.scrollTo({ top: scrollY, behavior: "instant" }); }; }, [mounted, isActive, phase]); const handleStart = () => { setPhase("active"); setCurrentStep(0); onStart?.(); onStepChange?.(0); }; const handleNext = () => { if (currentStep < filteredSteps.length - 1) { const next = currentStep + 1; setCurrentStep(next); onStepChange?.(next); } else { setPhase("done"); onComplete?.(); } }; const handlePrevious = () => { if (currentStep > 0) { const prev = currentStep - 1; setCurrentStep(prev); onStepChange?.(prev); } }; const handleSkip = () => { setPhase("done"); onSkip?.(); }; if (!mounted) return null; return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [ isActive && phase === "active" && /* @__PURE__ */ jsxRuntime.jsx( "div", { className: overlayClassName, onClick: handleSkip, style: { position: "fixed", inset: 0, width: "100vw", height: "100vh", backgroundColor: themeConfig.backdrop, zIndex: 9998 }, "aria-hidden": "true" } ), isActive && phase === "welcome" && welcomeConfig.enabled && /* @__PURE__ */ jsxRuntime.jsx( "div", { style: { position: "fixed", inset: 0, width: "100vw", height: "100vh", backgroundColor: themeConfig.backdrop, display: "flex", alignItems: "center", justifyContent: "center", zIndex: 9999 }, children: /* @__PURE__ */ jsxRuntime.jsxs( "div", { className: tooltipClassName, style: { backgroundColor: themeConfig.tooltipBg, color: themeConfig.tooltipText, border: `1px solid ${themeConfig.tooltipBorder}`, borderRadius: "16px", padding: isMobile() ? "24px" : "32px", maxWidth: "500px", width: "90%", boxShadow: "0 20px 60px rgba(0, 0, 0, 0.3)" }, children: [ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "flex-start", marginBottom: "20px" }, children: [ /* @__PURE__ */ jsxRuntime.jsx("h2", { style: { fontSize: "24px", fontWeight: "bold", margin: 0 }, children: welcomeConfig.title }), /* @__PURE__ */ jsxRuntime.jsx( "button", { onClick: handleSkip, "aria-label": "Close welcome screen", style: { background: "none", border: "none", fontSize: "28px", lineHeight: 1, cursor: "pointer", color: themeConfig.tooltipText, opacity: 0.7, padding: "4px 8px" }, onMouseEnter: (e) => e.currentTarget.style.opacity = "1", onMouseLeave: (e) => e.currentTarget.style.opacity = "0.7", children: "\xD7" } ) ] }), /* @__PURE__ */ jsxRuntime.jsx("p", { style: { marginBottom: "24px", fontSize: "15px", lineHeight: "1.6", whiteSpace: "pre-line" }, children: welcomeConfig.message }), /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", justifyContent: "flex-end", gap: "16px" }, children: [ /* @__PURE__ */ jsxRuntime.jsx( "button", { onClick: handleSkip, style: { background: "none", border: "none", color: themeConfig.tooltipText, opacity: 0.8, cursor: "pointer", fontSize: "14px", padding: "12px 0" }, onMouseEnter: (e) => e.currentTarget.style.opacity = "1", onMouseLeave: (e) => e.currentTarget.style.opacity = "0.8", children: labels.skip } ), /* @__PURE__ */ jsxRuntime.jsx( "button", { onClick: handleStart, style: { padding: "12px 24px", borderRadius: "12px", border: "none", backgroundColor: accentColor, color: "#ffffff", cursor: "pointer", fontSize: "14px", fontWeight: "500", boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)" }, children: welcomeConfig.startButtonText || labels.start } ) ] }), showBranding && /* @__PURE__ */ jsxRuntime.jsx(Branding, { color: themeConfig.tooltipText }) ] } ) } ), isActive && phase === "active" && filteredSteps[currentStep] && /* @__PURE__ */ jsxRuntime.jsx( Tooltip, { step: filteredSteps[currentStep], stepIndex: currentStep, totalSteps: filteredSteps.length, themeConfig, accentColor, showProgress, buttonLabels: labels, tooltipClassName, onNext: handleNext, onPrevious: handlePrevious, onSkip: handleSkip }, currentStep ), isActive && phase !== "done" && /* @__PURE__ */ jsxRuntime.jsx("style", { children: ` .nfsfu234-tour-active-target { position: relative !important; z-index: 9999 !important; box-shadow: 0 0 0 4px ${themeConfig.highlightRing || "rgba(16, 185, 129, 0.5)"}, 0 0 0 8px ${themeConfig.highlightRing ? themeConfig.highlightRing.replace("0.5", "0.2") : "rgba(16, 185, 129, 0.2)"}, 0 20px 40px rgba(0, 0, 0, 0.4) !important; border-radius: 12px; transition: box-shadow 0.3s ease; animation: nfsfu234-tour-pulse 2s ease-in-out infinite; } @keyframes nfsfu234-tour-pulse { 0%, 100% { box-shadow: 0 0 0 4px ${themeConfig.highlightRing || "rgba(16, 185, 129, 0.5)"}, 0 0 0 8px ${themeConfig.highlightRing ? themeConfig.highlightRing.replace("0.5", "0.2") : "rgba(16, 185, 129, 0.2)"}, 0 20px 40px rgba(0, 0, 0, 0.4); } 50% { box-shadow: 0 0 0 4px ${themeConfig.highlightRing || "rgba(16, 185, 129, 0.7)"}, 0 0 0 12px ${themeConfig.highlightRing ? themeConfig.highlightRing.replace("0.5", "0.3") : "rgba(16, 185, 129, 0.3)"}, 0 20px 40px rgba(0, 0, 0, 0.4); } } ` }) ] }); } exports.Tour = Tour; //# sourceMappingURL=index.cjs.map //# sourceMappingURL=index.cjs.map