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.
673 lines (670 loc) • 28.3 kB
JavaScript
import { useState, useRef, useCallback, useEffect, useMemo } from 'react';
import { createPortal } from 'react-dom';
import { AnimatePresence, motion } from 'framer-motion';
import { X } from 'lucide-react';
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
// src/Tour.tsx
function Tour({
tourId,
steps = [],
theme,
deviceMode = "desktop",
isActive = true,
onComplete,
onSkip,
onStart,
onStepChange,
welcomeScreen = { enabled: false },
buttonLabels = {},
showProgressDots = false
}) {
const [currentStep, setCurrentStep] = useState(welcomeScreen.enabled ? -1 : 0);
const [isVisible, setIsVisible] = useState(isActive);
const [tooltipPosition, setTooltipPosition] = useState({ top: 0, left: 0 });
const [tooltipTransform, setTooltipTransform] = useState("");
const [arrowStyle, setArrowStyle] = useState({});
const [navigationDirection, setNavigationDirection] = useState(null);
const [portalContainer, setPortalContainer] = useState(null);
const [welcomeStyle, setWelcomeStyle] = useState({});
const [zIndex, setZIndex] = useState(2147483646);
const [validSteps, setValidSteps] = useState(steps);
const [isDomReady, setIsDomReady] = useState(false);
const highlightedElements = useRef(/* @__PURE__ */ new Map());
const waitForElement = useCallback((selector, timeout = 5e3) => {
return new Promise((resolve) => {
const startTime = Date.now();
const check = () => {
const element = document.querySelector(selector);
if (element) return resolve(element);
if (Date.now() - startTime > timeout) return resolve(null);
setTimeout(check, 500);
};
if (document.readyState === "complete") check();
else window.addEventListener("load", check, { once: true });
});
}, []);
useEffect(() => {
const container = document.createElement("div");
container.id = `tour-portal-${tourId}`;
container.className = "fixed inset-0 pointer-events-none";
document.body.appendChild(container);
setPortalContainer(container);
const originalBodyOverflow = document.body.style.overflow;
const originalHtmlOverflow = document.documentElement.style.overflow;
document.body.style.overflow = "visible";
document.documentElement.style.overflow = "visible";
const updateZIndex = () => {
const elements = document.querySelectorAll("*");
let maxZIndex = 2147483646;
elements.forEach((el) => {
const z = parseInt(window.getComputedStyle(el).zIndex, 10);
if (!isNaN(z) && z > maxZIndex && z < 2147483647) maxZIndex = z;
});
setZIndex(maxZIndex + 1);
};
updateZIndex();
const observer = new MutationObserver(updateZIndex);
observer.observe(document.body, { childList: true, subtree: true });
return () => {
if (container && container.parentNode) {
container.parentNode.removeChild(container);
}
document.body.style.overflow = originalBodyOverflow;
document.documentElement.style.overflow = originalHtmlOverflow;
observer.disconnect();
};
}, [tourId]);
useEffect(() => {
if (!isVisible) return;
const validateSteps = async () => {
const valid = [];
for (const step of steps) {
const element = await waitForElement(step.target);
if (element) valid.push(step);
}
setValidSteps(valid);
setIsDomReady(true);
if (valid.length === 0 && steps.length > 0) {
console.warn("No valid tour steps found. Tour will not proceed.");
setIsVisible(false);
onSkip?.();
}
};
validateSteps();
const observer = new MutationObserver(() => validateSteps());
observer.observe(document.body, { childList: true, subtree: true });
return () => observer.disconnect();
}, [steps, isVisible, waitForElement, onSkip]);
useEffect(() => {
if (!isVisible || !isDomReady) return;
const styleId = `tour-styles-${tourId}`;
let existingStyle = document.getElementById(styleId);
if (!existingStyle) {
const style = document.createElement("style");
style.id = styleId;
style.textContent = `
.tour-highlight-${tourId} {
position: relative !important;
z-index: ${zIndex - 1} !important;
box-shadow: 0 0 8px 2px rgba(29, 78, 216, 0.5) !important;
border: 2px solid rgba(29, 78, 216, 0.7) !important;
border-radius: 6px !important;
transition: all 0.3s ease !important;
}
.tour-overlay-${tourId} {
pointer-events: auto !important;
z-index: ${zIndex} !important;
isolation: isolate;
}
.tour-content-${tourId} {
pointer-events: auto !important;
z-index: ${zIndex + 1} !important;
isolation: isolate;
}
.tour-arrow-${tourId} {
z-index: ${zIndex + 2} !important;
}
[data-theme="dark"] .tour-highlight-${tourId}, .dark .tour-highlight-${tourId} {
box-shadow: 0 0 8px 2px rgba(37, 99, 235, 0.5) !important;
border: 2px solid rgba(37, 99, 235, 0.7) !important;
}
.tour-overlay-${tourId}:not([style*="opacity: 1"]) .tour-highlight-${tourId} {
box-shadow: none !important;
border: none !important;
z-index: auto !important;
}
`;
document.head.appendChild(style);
}
return () => {
const style = document.getElementById(styleId);
if (style && style.parentNode) {
style.parentNode.removeChild(style);
}
};
}, [isVisible, isDomReady, tourId, zIndex]);
const isMobile = deviceMode === "mobile" || deviceMode === "tablet";
const filteredSteps = useMemo(() => {
return validSteps.filter((step) => {
if (!step.device || step.device === "both") return true;
return step.device === (isMobile ? "mobile" : "desktop");
});
}, [validSteps, isMobile]);
useEffect(() => {
setIsVisible(isActive && isDomReady);
if (isActive && welcomeScreen.enabled && currentStep === -1 && isDomReady) {
onStart?.();
}
}, [isActive, welcomeScreen.enabled, currentStep, onStart, isDomReady]);
useEffect(() => {
if (!isVisible || currentStep !== -1 || !welcomeScreen.enabled || !isDomReady) return;
const updateResponsiveStyles = () => {
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const config = welcomeScreen.content;
const hasCustomPosition = config?.position || isMobile && config?.mobilePosition;
let top = viewportHeight * 0.5;
let left = viewportWidth * 0.5;
let transform = "translate(-50%, -50%)";
if (hasCustomPosition) {
let userTop = isMobile && config.mobilePosition?.top ? config.mobilePosition.top : config.position?.top ?? 0;
let userLeft = isMobile && config.mobilePosition?.left ? config.mobilePosition.left : config.position?.left ?? 0;
let userTransform = isMobile && config.mobilePosition?.transform ? config.mobilePosition.transform : config.position?.transform;
if (typeof userTop === "string") {
userTop = parseFloat(userTop) * (userTop.endsWith("%") ? viewportHeight / 100 : userTop.endsWith("rem") ? 16 : 1);
}
if (typeof userLeft === "string") {
userLeft = parseFloat(userLeft) * (userLeft.endsWith("%") ? viewportWidth / 100 : userLeft.endsWith("rem") ? 16 : 1);
}
top += typeof userTop === "number" ? userTop : 0;
left += typeof userLeft === "number" ? userLeft : 0;
const welcomeWidth = Math.min(viewportWidth * 0.9, 448);
const welcomeHeight = 300;
top = Math.max(10, Math.min(top, viewportHeight - welcomeHeight - 10));
left = Math.max(10, Math.min(left, viewportWidth - welcomeWidth - 10));
setWelcomeStyle({
position: "absolute",
top,
left,
transform: userTransform ?? transform
});
} else {
setWelcomeStyle({ top: "50%", left: "50%", transform });
}
};
updateResponsiveStyles();
const debouncedUpdate = debounce(updateResponsiveStyles, 100);
window.addEventListener("resize", debouncedUpdate);
return () => window.removeEventListener("resize", debouncedUpdate);
}, [isVisible, currentStep, welcomeScreen, isMobile, isDomReady]);
const debounce = (func, wait) => {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
};
const getTargetElement = useCallback(async (step) => {
let targetElement = await waitForElement(step.target);
if (!targetElement && step.target.startsWith(".")) {
const elements = document.querySelectorAll(step.target);
targetElement = Array.from(elements).find((el) => {
const rect = el.getBoundingClientRect();
return rect.width > 0 && rect.height > 0 && window.getComputedStyle(el).display !== "none";
}) || null;
}
return targetElement;
}, [waitForElement]);
const getStepContent = (step) => {
if (isMobile && step.contentMobile) return step.contentMobile;
if (!isMobile && step.contentDesktop) return step.contentDesktop;
return step.content;
};
const createArrowStyle = useCallback(
(position, theme2) => {
const baseArrow = {
position: "absolute",
width: 0,
height: 0,
borderStyle: "solid",
zIndex: zIndex + 2
};
const triangleColor = theme2 === "dark" ? "#ffffff" : "#111827";
switch (position) {
case "top":
return {
...baseArrow,
bottom: "-7px",
left: "50%",
borderWidth: "7px 7px 0 7px",
borderColor: `${triangleColor} transparent transparent transparent`,
transform: "translateX(-50%)"
};
case "bottom":
return {
...baseArrow,
top: "-7px",
left: "50%",
borderWidth: "0 7px 7px 7px",
borderColor: `transparent transparent ${triangleColor} transparent`,
transform: "translateX(-50%)"
};
case "left":
return {
...baseArrow,
right: "-7px",
top: "50%",
borderWidth: "7px 7px 7px 0",
borderColor: `transparent ${triangleColor} transparent transparent`,
transform: "translateY(-50%)"
};
case "right":
return {
...baseArrow,
left: "-7px",
top: "50%",
borderWidth: "7px 0 7px 7px",
borderColor: `transparent transparent transparent ${triangleColor}`,
transform: "translateY(-50%)"
};
default:
return { display: "none" };
}
},
[zIndex]
);
const ensureVisible = useCallback((element) => {
element.scrollIntoView({ behavior: "smooth", block: "center", inline: "center" });
const rect = element.getBoundingClientRect();
const isVisible2 = rect.top >= 0 && rect.left >= 0 && rect.bottom <= window.innerHeight && rect.right <= window.innerWidth;
if (!isVisible2) {
window.scrollBy({ top: rect.top - 50, left: rect.left - 50, behavior: "smooth" });
}
let parent = element.parentElement;
while (parent && parent !== document.body) {
const parentStyle = window.getComputedStyle(parent);
if (parentStyle.overflow === "auto" || parentStyle.overflow === "scroll") {
parent.scrollIntoView({ behavior: "smooth", block: "center" });
}
parent = parent.parentElement;
}
}, []);
const clearAllHighlights = useCallback(() => {
const highlightClass = `tour-highlight-${tourId}`;
highlightedElements.current.forEach(({ originalOverflow }, element) => {
element.classList.remove(highlightClass);
const parent = element.parentElement;
if (parent && originalOverflow) {
parent.style.overflow = originalOverflow;
}
});
highlightedElements.current.clear();
}, [tourId]);
useEffect(() => {
if (!isVisible || currentStep < 0 || currentStep >= filteredSteps.length || !isDomReady) return;
const step = filteredSteps[currentStep];
let isMounted = true;
const updatePosition = async () => {
const targetElement = await getTargetElement(step);
if (!targetElement || !isMounted) {
console.warn(`Tour target not found: ${step.target}`);
if (navigationDirection !== "backward" && currentStep < filteredSteps.length - 1) {
setNavigationDirection("forward");
setCurrentStep(currentStep + 1);
} else {
setIsVisible(false);
onComplete?.();
}
return;
}
ensureVisible(targetElement);
const rect = targetElement.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const tooltipWidth = 320;
const tooltipHeight = 150;
let top = 0;
let left = 0;
let transform = "";
const position = step.position || "bottom";
const offsetX = step.offset?.x || 0;
const offsetY = step.offset?.y || 10;
if (step.customPosition) {
setTooltipPosition({
top: step.customPosition.top ?? "50%",
left: step.customPosition.left ?? "50%"
});
setTooltipTransform(step.customPosition.transform ?? "");
setArrowStyle({ display: "none" });
return;
}
if (position === "center") {
setTooltipPosition({ top: "50%", left: "50%" });
setTooltipTransform("translate(-50%, -50%)");
setArrowStyle({ display: "none" });
return;
}
switch (position) {
case "top":
top = rect.top - tooltipHeight - offsetY;
left = rect.left + rect.width / 2 + offsetX;
transform = "translateX(-50%)";
break;
case "bottom":
top = rect.bottom + offsetY;
left = rect.left + rect.width / 2 + offsetX;
transform = "translateX(-50%)";
break;
case "left":
top = rect.top + rect.height / 2 + offsetY;
left = rect.left - tooltipWidth - 10 + offsetX;
transform = "translateY(-50%)";
break;
case "right":
top = rect.top + rect.height / 2 + offsetY;
left = rect.right + 10 + offsetX;
transform = "translateY(-50%)";
break;
case "top-left":
top = rect.top - tooltipHeight - offsetY;
left = rect.left + offsetX;
transform = "translateX(-10%)";
break;
case "top-right":
top = rect.top - tooltipHeight - offsetY;
left = rect.right + offsetX;
transform = "translateX(-90%)";
break;
case "bottom-left":
top = rect.bottom + offsetY;
left = rect.left + offsetX;
transform = "translateX(-10%)";
break;
case "bottom-right":
top = rect.bottom + offsetY;
left = rect.right + offsetX;
transform = "translateX(-90%)";
break;
case "center-left":
top = rect.top + rect.height / 2 + offsetY;
left = rect.left - tooltipWidth - 10 + offsetX;
transform = "translateY(-50%)";
break;
case "center-right":
top = rect.top + rect.height / 2 + offsetY;
left = rect.right + 10 + offsetX;
transform = "translateY(-50%)";
break;
}
top = Math.max(10, Math.min(top, viewportHeight - tooltipHeight - 10));
left = Math.max(10, Math.min(left, viewportWidth - tooltipWidth - 10));
setTooltipPosition({ top, left });
setTooltipTransform(transform);
setArrowStyle(createArrowStyle(position, theme));
};
updatePosition();
const debouncedUpdate = debounce(updatePosition, 100);
window.addEventListener("resize", debouncedUpdate);
window.addEventListener("scroll", debouncedUpdate);
return () => {
isMounted = false;
window.removeEventListener("resize", debouncedUpdate);
window.removeEventListener("scroll", debouncedUpdate);
};
}, [isVisible, currentStep, filteredSteps, theme, navigationDirection, createArrowStyle, getTargetElement, ensureVisible, isDomReady]);
useEffect(() => {
if (!isVisible || currentStep < 0 || currentStep >= filteredSteps.length || !isDomReady) {
clearAllHighlights();
return;
}
const step = filteredSteps[currentStep];
let isMounted = true;
const highlightElement = async () => {
const targetElement = await getTargetElement(step);
if (!targetElement || !isMounted) return;
clearAllHighlights();
const highlightClass = `tour-highlight-${tourId}`;
targetElement.classList.add(highlightClass);
const parent = targetElement.parentElement;
let originalOverflow = "";
if (parent) {
originalOverflow = window.getComputedStyle(parent).overflow;
if (originalOverflow.includes("hidden")) {
parent.style.overflow = "visible";
}
}
highlightedElements.current.set(targetElement, { originalOverflow });
return () => {
if (isMounted) {
targetElement.classList.remove(highlightClass);
if (parent && originalOverflow) {
parent.style.overflow = originalOverflow;
}
highlightedElements.current.delete(targetElement);
}
};
};
highlightElement();
return () => {
isMounted = false;
};
}, [currentStep, filteredSteps, isVisible, tourId, getTargetElement, isDomReady, clearAllHighlights]);
useEffect(() => {
if (!isVisible || !isDomReady) return;
const handleKeyDown = (e) => {
if (e.key === "ArrowRight" || e.key === "Enter") {
e.preventDefault();
if (currentStep === -1) {
handleStart();
} else {
handleNext();
}
} else if (e.key === "ArrowLeft" && currentStep > 0) {
e.preventDefault();
handlePrevious();
} else if (e.key === "Escape") {
e.preventDefault();
handleSkip();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [currentStep, isVisible, isDomReady]);
useEffect(() => {
if (isVisible && currentStep >= 0 && isDomReady) {
onStepChange?.(currentStep);
}
}, [currentStep, isVisible, onStepChange, isDomReady]);
const completeTour = useCallback(() => {
clearAllHighlights();
setIsVisible(false);
setCurrentStep(welcomeScreen.enabled ? -1 : 0);
onComplete?.();
}, [onComplete, welcomeScreen.enabled, clearAllHighlights]);
const handleStart = useCallback(() => {
setCurrentStep(0);
setNavigationDirection("forward");
onStart?.();
}, [onStart]);
const handleNext = useCallback(() => {
if (currentStep < filteredSteps.length - 1) {
setCurrentStep(currentStep + 1);
setNavigationDirection("forward");
} else {
completeTour();
}
}, [currentStep, filteredSteps.length, completeTour]);
const handlePrevious = useCallback(() => {
if (currentStep > 0) {
setCurrentStep(currentStep - 1);
setNavigationDirection("backward");
}
}, [currentStep]);
const handleSkip = useCallback(() => {
completeTour();
onSkip?.();
}, [onSkip, completeTour]);
if (!isVisible || !portalContainer || !isDomReady || filteredSteps.length === 0) return null;
const tourContent = /* @__PURE__ */ jsx(AnimatePresence, { children: /* @__PURE__ */ jsx(
motion.div,
{
className: `tour-overlay-${tourId} fixed inset-0 flex items-center justify-center ${theme === "dark" ? "bg-gray-900/80" : "bg-gray-900/60"}`,
initial: { opacity: 0 },
animate: { opacity: 1 },
exit: { opacity: 0 },
style: { zIndex },
children: currentStep === -1 && welcomeScreen.enabled ? /* @__PURE__ */ jsx(
motion.div,
{
className: `tour-content-${tourId} p-6 rounded-xl w-[90%] max-w-md backdrop-blur-sm shadow-lg ring-1 ${theme === "dark" ? "bg-gray-800 text-white ring-gray-700/50" : "bg-white text-gray-900 ring-gray-200/50"}`,
style: welcomeStyle,
initial: { opacity: 0, scale: 0.95, y: "2rem" },
animate: { opacity: 1, scale: 1, y: 0 },
exit: { opacity: 0, scale: 0.95, y: "2rem" },
transition: { duration: 0.3, ease: "easeOut" },
role: "dialog",
"aria-labelledby": `tour-welcome-title-${tourId}`,
children: welcomeScreen.content && typeof welcomeScreen.content === "object" && "title" in welcomeScreen.content ? /* @__PURE__ */ jsxs(Fragment, { children: [
/* @__PURE__ */ jsxs("div", { className: "flex justify-between items-center mb-4", children: [
/* @__PURE__ */ jsx("h3", { id: `tour-welcome-title-${tourId}`, className: "text-lg font-semibold", children: welcomeScreen.content.title }),
/* @__PURE__ */ jsx(
"button",
{
onClick: handleSkip,
className: `p-1 rounded-full transition-colors ${theme === "dark" ? "text-gray-400 hover:bg-gray-700" : "text-gray-500 hover:bg-gray-100"}`,
"aria-label": "Skip tour",
children: /* @__PURE__ */ jsx(X, { size: 20 })
}
)
] }),
/* @__PURE__ */ jsx("p", { className: "text-sm mb-6", children: welcomeScreen.content.message }),
/* @__PURE__ */ jsxs("div", { className: "flex justify-between items-center", children: [
/* @__PURE__ */ jsx(
"button",
{
onClick: handleSkip,
className: `text-sm font-medium transition-colors ${theme === "dark" ? "text-gray-400 hover:text-gray-200" : "text-gray-500 hover:text-gray-700"}`,
children: buttonLabels.skip || "Skip Tour"
}
),
/* @__PURE__ */ jsx(
"button",
{
onClick: handleStart,
className: `px-4 py-2 rounded-md text-sm font-medium transition-all ${theme === "dark" ? "bg-blue-600 text-white hover:bg-blue-500 hover:text-gray-900" : "bg-blue-700 text-white hover:bg-blue-600"}`,
children: welcomeScreen.content.startButtonText || buttonLabels.start || "Start Tour"
}
)
] })
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
welcomeScreen.content,
/* @__PURE__ */ jsxs("div", { className: "flex justify-between items-center mt-6", children: [
/* @__PURE__ */ jsx(
"button",
{
onClick: handleSkip,
className: `text-sm font-medium transition-colors ${theme === "dark" ? "text-gray-400 hover:text-gray-200" : "text-gray-500 hover:text-gray-700"}`,
children: buttonLabels.skip || "Skip Tour"
}
),
/* @__PURE__ */ jsx(
"button",
{
onClick: handleStart,
className: `px-4 py-2 rounded-md text-sm font-medium transition-all ${theme === "dark" ? "bg-blue-600 text-white hover:bg-blue-500 hover:text-gray-900" : "bg-blue-700 text-white hover:bg-blue-600"}`,
children: buttonLabels.start || "Start Tour"
}
)
] })
] })
}
) : currentStep < filteredSteps.length && /* @__PURE__ */ jsxs(
motion.div,
{
className: `tour-content-${tourId} absolute p-5 rounded-xl max-w-sm w-full min-w-[200px] backdrop-blur-sm shadow-lg ring-1 ${theme === "dark" ? "bg-gray-800 text-white ring-gray-700/50" : "bg-white text-gray-900 ring-gray-200/50"}`,
style: {
top: `${tooltipPosition.top}px`,
left: `${tooltipPosition.left}px`,
transform: tooltipTransform,
margin: "0 10px",
zIndex: zIndex + 1
},
initial: {
opacity: 0,
y: filteredSteps[currentStep].position?.includes("top") ? "0.6rem" : "-0.6rem",
x: filteredSteps[currentStep].position?.includes("left") ? "0.6rem" : "-0.6rem"
},
animate: { opacity: 1, y: 0, x: 0 },
exit: {
opacity: 0,
y: filteredSteps[currentStep].position?.includes("top") ? "0.6rem" : "-0.6rem",
x: filteredSteps[currentStep].position?.includes("left") ? "0.6rem" : "-0.6rem"
},
transition: { duration: 0.3, ease: "easeOut" },
role: "dialog",
"aria-labelledby": `tour-step-title-${tourId}-${currentStep}`,
children: [
/* @__PURE__ */ jsx("div", { className: `arrow tour-arrow-${tourId}`, style: arrowStyle }),
/* @__PURE__ */ jsx("p", { id: `tour-step-title-${tourId}-${currentStep}`, className: "text-sm mb-4", children: getStepContent(filteredSteps[currentStep]) }),
/* @__PURE__ */ jsxs("div", { className: "mb-3", children: [
/* @__PURE__ */ jsx("div", { className: "relative h-1.5 rounded-full bg-gray-200 dark:bg-gray-700", children: /* @__PURE__ */ jsx(
"div",
{
className: "absolute h-1.5 bg-blue-500 rounded-full transition-all duration-300",
style: { width: `${(currentStep + 1) / filteredSteps.length * 100}%` }
}
) }),
showProgressDots && /* @__PURE__ */ jsx("div", { className: "flex justify-center gap-1.5 mt-2", children: filteredSteps.map((_, index) => /* @__PURE__ */ jsx(
"div",
{
className: `w-1.5 h-1.5 rounded-full transition-colors ${index === currentStep ? "bg-blue-500" : "bg-gray-400"}`
},
index
)) })
] }),
/* @__PURE__ */ jsxs("div", { className: "flex justify-between items-center", children: [
/* @__PURE__ */ jsx(
"button",
{
onClick: handleSkip,
className: `text-sm font-medium transition-colors ${theme === "dark" ? "text-gray-400 hover:text-gray-200" : "text-gray-500 hover:text-gray-700"}`,
children: buttonLabels.skip || "Skip"
}
),
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
/* @__PURE__ */ jsx(
"button",
{
onClick: handlePrevious,
disabled: currentStep === 0,
className: `px-3 py-1 rounded-md text-sm font-medium transition-all ${currentStep === 0 ? "bg-gray-400 text-gray-600 cursor-not-allowed" : theme === "dark" ? "bg-gray-600 text-white hover:bg-gray-500 hover:text-gray-100" : "bg-gray-700 text-white hover:bg-gray-600"}`,
children: buttonLabels.previous || "Previous"
}
),
/* @__PURE__ */ jsxs("span", { className: "text-xs text-gray-400", children: [
currentStep + 1,
" / ",
filteredSteps.length
] }),
/* @__PURE__ */ jsx(
"button",
{
onClick: handleNext,
className: `px-3 py-1 rounded-md text-sm font-medium transition-all ${theme === "dark" ? "bg-blue-600 text-white hover:bg-blue-500 hover:text-gray-900" : "bg-blue-700 text-white hover:bg-blue-600"}`,
children: currentStep < filteredSteps.length - 1 ? buttonLabels.next || "Next" : buttonLabels.finish || "Finish"
}
)
] })
] })
]
}
)
}
) });
return createPortal(tourContent, portalContainer);
}
// src/index.tsx
var index_default = Tour;
export { Tour, index_default as default };
//# sourceMappingURL=index.mjs.map
//# sourceMappingURL=index.mjs.map