diff --git a/.gitignore b/.gitignore index 4c3e209..e4eb1b7 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,5 @@ yarn-debug.log* yarn-error.log* # vercel -.vercel \ No newline at end of file +.vercel +/.idea diff --git a/README.md b/README.md index c5fe7ac..4df51b0 100644 --- a/README.md +++ b/README.md @@ -17,15 +17,15 @@ yarn add onborda ### Global `layout.tsx` ```tsx - - - {children} - + + + {children} + ``` ### Components & `page.tsx` -Target anything in your app using the elements `id` attribute. +Optionally target anything in your app using the elements `id` attribute to overlay a backdrop and highlight the target element. If no `selector` or `customQuerySelector` is provided, the overlay will cover the entire page and the step will display as a traditional modal. ```tsx
Onboard Step
``` @@ -43,124 +43,279 @@ const config: Config = { } ``` -### Custom Card -If you require greater control over the card design or simply wish to create a totally custom component then you can do so easily. +### Card Component +If you require greater control over the card design or simply wish to create a totally custom component then you can do so easily. The following props can be used to customise your card component. + +| Prop | Type | Description | +|----------------------|-------------------------------------|-----------------------------------------------------------------------------------------------| +| `step` | `Object` | The current `Step` object from your steps array, including content, title, etc. | +| `currentStep` | `number` | The index of the current step in the steps array. | +| `totalSteps` | `number` | The total number of steps in the onboarding process. | +| `setStep` | `(step: number \| string) => void;` | A function to set the current step in the onboarding process. | +| `nextStep` | `() => void` | A function to advance to the next step in the onboarding process. | +| `prevStep` | `() => void` | A function to go back to the previous step in the onboarding process. | +| `closeOnborda` | `() => void` | A function to close the onboarding process. | +| `arrow` | `JSX.Element` | An SVG object, the orientation is controlled by the steps side prop | +| `completedSteps` | `number[]` | An array of completed step indexes/ids. | +| `pendingRouteChange` | `boolean` | A boolean to determine if a route change is pending. Only set if the step has a selector set. | -| Prop | Type | Description | -|---------------|------------------|----------------------------------------------------------------------| -| `step` | `Object` | The current `Step` object from your steps array, including content, title, etc. | -| `currentStep` | `number` | The index of the current step in the steps array. | -| `totalSteps` | `number` | The total number of steps in the onboarding process. | -| `nextStep` | | A function to advance to the next step in the onboarding process. | -| `prevStep` | | A function to go back to the previous step in the onboarding process.| -| `arrow` | | Returns an SVG object, the orientation is controlled by the steps side prop | ```tsx "use client" import type { CardComponentProps } from "onborda"; -export const CustomCard = ({ +export const CustomCardComponent = ({ step, currentStep, - totalSteps, + totalSteps, + setStep, nextStep, prevStep, + closeOnborda, arrow, + completedSteps, + pendingRouteChange }: CardComponentProps) => { return (
-

{step.icon} {step.title}

+

{step.icon} {step.title} {completedSteps.contains(currentStep) ? <>✅ : <>}

{currentStep} of {totalSteps}

-

{step.content}

- - +
+ {step.content} +
+ + + + {arrow}
) } ``` -### Steps object -Steps have changed since Onborda v1.2.3 and now fully supports multiple "tours" so you have the option to create multple product tours should you need to! The original Step format remains but with some additional content as shown in the example below! +### Tour Component +If you require greater control over the tour design or simply wish to create a totally custom component then you can do so easily. + +| Prop | Type | Description | +|------------------|-------------------------------------|---------------------------------------------------------------| +| `tour` | `Tour` | The current `Tour` object from your tours array. | +| `currentTour` | `string` | The current tour name. | +| `currentStep` | `number` | The index of the current step in the steps array. | +| `completedSteps` | `number[]` | An array of completed step indexes/ids. | +| `setStep` | `(step: number \| string) => void;` | A function to set the current step in the onboarding process. | +| `closeOnborda` | `() => void` | A function to close the onboarding process. | ```tsx -{ - tour: "firstyour", - steps: [ - Step - ], - tour: "secondtour", - steps: [ - Step - ] +"use client" +import type { TourComponentProps } from "onborda"; + +export const CustomTourComponent = ({ + tour, + currentTour, + currentStep, + setStep, + completedSteps, + closeOnborda +}: TourComponentProps) => { + return ( +
{/* Tailwind CSS classes to position TourCard in bottom left of screen */} +

{currentTour}

+

{currentStep} of {tour.steps.length}

+

{completedSteps.length} of {tour.steps.length} completed

+ + +
+ ) +} +``` + +### Tour object +| Prop | Type | Description | +|------------------------------|----------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `tour` | `string` | The name of the tour. | +| `steps` | `Step[]` | An array of `Step` objects defining each step of the tour. | +| `title` | `string` | Optional. The title of the tour. | +| `description` | `string` | Optional. The description of the tour. | +| `dissmissable` | `boolean` | Optional. Determines whether the user can dismiss the tour. | +| `onComplete` | `() => void` | Optional. A function that is called when the tour is completed. | +| `initialCompletedStepsState` | `() => Promise` | Optional. A client or server function that returns a promise that resolves to an array of booleans for each step. If true, the step is marked as completed on tour init. | +| `[key: string]` | `any` | Optional. Any additional properties you wish to add. Will be available in the `useOnborda` hook as well as the TourCard component. | + + +```tsx +const tour : Tour = { + tour: "firstyour", + steps: [ + Step + ] } ``` ### Step object -| Prop | Type | Description | -|----------------|-------------------------------|---------------------------------------------------------------------------------------| -| `icon` | `React.ReactNode`, `string`, `null` | An icon or element to display alongside the step title. | -| `title` | `string` | The title of your step | -| `content` | `React.ReactNode` | The main content or body of the step. | -| `selector` | `string` | A string used to target an `id` that this step refers to. | -| `side` | `"top"`, `"bottom"`, `"left"`, `"right"` | Optional. Determines where the tooltip should appear relative to the selector. | -| `showControls` | `boolean` | Optional. Determines whether control buttons (next, prev) should be shown if using the default card. | -| `pointerPadding` | `number` | Optional. The padding around the pointer (keyhole) highlighting the target element. | -| `pointerRadius` | `number` | Optional. The border-radius of the pointer (keyhole) highlighting the target element. | -| `nextRoute` | `string` | Optional. The route to navigate to using `next/navigation` when moving to the next step. | -| `prevRoute` | `string` | Optional. The route to navigate to using `next/navigation` when moving to the previous step. | +| Prop | Type | Description | +|------------------------|------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `content` | `React.ReactNode` | The main content or body of the step. | +| `id` | `string` | Optional. A unique identifier for the step. If set, step can be acitavted using `setStep(step.id)`. Otherwise steps[] index is used. | +| `title` | `string` | Optional. The title of your step | +| `icon` | `React.ReactNode`, `string`, `null` | Optional. An icon or element to display alongside the step title. | +| `selector` | `string` | Optional. A string used to target an Element by `id` that this step refers to. Takes precedence over `customQuerySelector`. | +| `customQuerySelector` | `()=>Element \| null` | Optional. A client function that returns the element to target that this step refers to. Proceeded by `selector`.
Useful for targeting complex elements, like those from UI libraries. | +| `side` | `"top"`, `"bottom"`, `"left"`, `"right"` | Optional. Determines where the tooltip should appear relative to the selector. | +| `showControls` | `boolean` | Optional. Determines whether control buttons (next, prev) should be shown if using the default card. | +| `pointerPadding` | `number` | Optional. The padding around the pointer (keyhole) highlighting the target element. | +| `pointerRadius` | `number` | Optional. The border-radius of the pointer (keyhole) highlighting the target element. | +| `route` | `string` | Optional. The route to navigate to using `next/navigation` when this step is set. | +| `interactable` | `boolean` | Optional. Determines whether the user can interact with the target element. | +| `isCompleteConditions` | `(element: Element) => boolean` | Optional. A client function that returns a boolean. If true, the step is marked as completed. Called when the user interacts with the target element. | +| `onComplete` | `() => void` | Optional. A client function that is called when the step is marked as completed. Called if `isCompleteConditions` returns true. | +| `[key: string]` | `any` | Optional. Any additional properties you wish to add. Will be available in the `useOnborda` hook as well as the Card component. | +| `nextRoute` | `string` | **Deprecated**. Optional. The route to navigate to using `next/navigation` when moving to the next step. | +| `prevRoute` | `string` | **Deprecated**. Optional. The route to navigate to using `next/navigation` when moving to the previous step. | -> **Note** _Both `nextRoute` and `prevRoute` have a `500`ms delay before setting the next step, a function will be added soon to control the delay in case your application loads slower than this._ +> [!IMPORTANT] +> The `nextRoute` and `prevRoute` properties have been deprecated. Please use the `route` property instead. +```tsx +const step: Step = { + icon: <>👋, + title: "Tour 1, Step 1", + content: <>First tour, first step, + selector: "#tour1-step1", + side: "top", + showControls: true, + pointerPadding: 10, + pointerRadius: 10, + route: "/foo" +} +``` -### Example `steps` +### Example `tours` array ```tsx -{ - tour: "firsttour", - steps: [ +import { FirstTourInitialState } from "@app/lib/initialTourStates"; // Optional initial stare from server action +const tours : Tour[] = [ + { + tour: "firsttour", + title: "First Tour", + description: "This is the first tour", + dissmissable: false, + customProperty: "This is a custom property passed to the Tour card", + initialCompletedStepsState: FirstTourInitialState, //Optional server action called on tour init to get completed steps boolean array + steps: [ + { + icon: <>👋, + title: "Tour 1, Step 1", + content: <>First tour, first step, + selector: "#tour1-step1", + side: "top", + showControls: true, + pointerPadding: 10, + pointerRadius: 10, + route: "/foo", + customProperty: "This is a custom property passed to the Step card" + }, + { + icon: <>👋, + title: "Tour 1, Step 2", + content: <>First tour, second step. To proceed please provide a value, + selector: "#tour1-step2-input", // target the input + isCompleteConditions: (element) => ((element as HTMLInputElement)?.value?.trim() !== ''), // check if the step is completed when element is interacted with + interactable: true, // allow user to interact with the input + side: "right", + showControls: true, + pointerPadding: 10, + pointerRadius: 10, + route: "/bar" + }, + { + icon: <>👋, + title: "Tour 1, Step 3", + content: <>Thanks for settings the value, + selector: "#tour1-step3", + side: "top", + showControls: true, + pointerPadding: 10, + pointerRadius: 10, + route: "/bar" + } + ], + }, { - icon: <>👋, - title: "Tour 1, Step 1", - content: <>First tour, first step, - selector: "#tour1-step1", - side: "top", - showControls: true, - pointerPadding: 10, - pointerRadius: 10, - nextRoute: "/foo", - prevRoute: "/bar" + tour: "secondtour", + steps: [ + { + icon: <>👋👋, + title: "Second tour, Step 1", + content: <>Second tour, first step!, + //selector: "#onborda-step1", + customQuerySelector: () => document.getElementById("onborda-step1").closest("div"), // get the parent div + side: "top", + showControls: true, + pointerPadding: 10, + pointerRadius: 10, + route: "/foo", + interactable: true, + } + ] } - ... - ], - tour: "secondtour", - steps: [ - icon: <>👋👋, - title: "Second tour, Step 1", - content: <>Second tour, first step!, - selector: "#onborda-step1", - side: "top", - showControls: true, - pointerPadding: 10, - pointerRadius: 10, - nextRoute: "/foo", - prevRoute: "/bar" - ] +] +``` +```tsx +'use server' +import API from "@app/lib/api"; +export async function FirstTourInitialState() { + return Promise.all([ + (await API.get("/api/first-tour-step-1")).value === 'true', + (await API.get("/api/first-tour-step-2")).value === 'true', + (await API.get("/api/first-tour-step-3")).value === 'true', + ]) + // return [true, false, false] // Example of an initial state } ``` +### OnbordaProvider Props +| Property | Type | Description | +|--------------------|----------------------|-------------------------------------------------------------------------------------| +| `children` | `React.ReactNode` | Your website or application content. | +| `tours` | `Tour[]` | An array of `Tour` objects defining each tour of the onboarding process. | +| `activeTour` | `string` | Optional. The id of the active tour. If set, Onborda will start tour automatically. | + +```tsx + + {children} + +``` ### Onborda Props -| Property | Type | Description | -|-----------------|-----------------------|---------------------------------------------------------------------------------------| -| `children` | `React.ReactNode` | Your website or application content. | -| `steps` | `Array[]` | An array of `Step` objects defining each step of the onboarding process. | -| `showOnborda` | `boolean` | Optional. Controls the visibility of the onboarding overlay, eg. if the user is a first time visitor. Defaults to `false`. | -| `shadowRgb` | `string` | Optional. The RGB values for the shadow color surrounding the target area. Defaults to black `"0,0,0"`. | -| `shadowOpacity` | `string` | Optional. The opacity value for the shadow surrounding the target area. Defaults to `"0.2"` | -| `customCard` | `React.ReactNode` | Optional. A custom card (or tooltip) that can be used to replace the default TailwindCSS card. | -| `cardTransition`| `Transition` | Transitions between steps are of the type Transition from [framer-motion](https://www.framer.com/motion/transition/), see the [transition docs](https://www.framer.com/motion/transition/) for more info. Example: `{{ type: "spring" }}`. | +| Property | Type | Description | +|--------------------|----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `children` | `React.ReactNode` | Your website or application content. | +| `showOnborda` | `boolean` | Optional. Controls the visibility of the onboarding overlay, eg. if the user is a first time visitor. Defaults to `false`. | +| `shadowRgb` | `string` | Optional. The RGB values for the shadow color surrounding the target area. Defaults to black `"0,0,0"`. | +| `shadowOpacity` | `string` | Optional. The opacity value for the shadow surrounding the target area. Defaults to `"0.2"` | +| `cardTransition` | `Transition` | Transitions between steps are of the type Transition from [framer-motion](https://www.framer.com/motion/transition/), see the [transition docs](https://www.framer.com/motion/transition/) for more info. Example: `{{ type: "spring" }}`. | +| `cardComponent` | `React.ReactNode` | The React component to use as the card for each step. | +| `tourComponent` | `React.ReactNode` | The React component to use as a list of steps for the current tour. | +| `debug` | `boolean` | Optional. Console logs the current step and the target element. Defaults to `false`. | +| `observerTimeout` | `number` | Optional. The timeout in milliseconds for the observer to wait for the target element to be available. Defaults to `5000`. Observer is used to wait for the target element to be available before proceeding to the next step e.g. when the target element is on a different route. | +| `steps` | `Array[]` | **Deprecated** An array of `Step` objects defining each step of the onboarding process. | +> [!IMPORTANT] +> The `steps` property has been deprecated. Please use the `OnbordaProvider.tours` property instead. ```tsx {children} ``` + +### UseOnborda Context +The `useOnborda` hook provides a set of functions to control the onboarding process from any child within the `OnboardaProvider`. The hook returns an object with the following properties: + +| Property | Type | Description | +|--------------------|-------------------------------------------------------|--------------------------------------------------------------------------| +| `tours` | `Tour[]` | An array of `Tour` objects defining each tour of the onboarding process. | +| `startOnborda` | `() => void` | A function to start the onboarding process. | +| `closeOnborda` | `() => void` | A function to close the onboarding process. | +| `curreentTour` | `string` | The name of the current tour. | +| `currentStep` | `number` | The index of the current step in the steps array. | +| `currentTourSteps` | `Step[]` | The steps array for the current tour. | +| `setCurrentStep` | `(step: number \| string) => void;` | A function to set the current step in the onboarding process. | +| `completedSteps` | `Set` | An array of completed step indexes/ids. | +| `setCompletedSteps`| `React.Dispatch>>` | A function to set the completed steps array. | +| `isOnbordaVisible` | `boolean` | A boolean to determine if the onboarding overlay is visible. | + +```tsx +const { + startOnborda, + closeOnborda, + currentTour, + currentStep, + currentTourSteps, + setCurrentStep, + completedSteps, + setCompletedSteps, + isOnbordaVisible +} = useOnborda(); +``` + +### Contribution +To setup the project locally, clone the repository and run the following commands: +```bash +# Install dependencies +npm install +``` + +To test the local library in a local Next.js project, run the following command in this project to setup npm link +```bash +# Create a symlink to the package +npm link +``` + +Then in your Next.js project run the following command to link the package. This will create a symlink in your Next.js project to the local package. +```bash +# Link the package +npm link onborda +``` + +To unlink the package, run the following command in your Next.js project +```bash +# Unlink the package +npm unlink onborda +``` + +If you already have the published package installed in your project, the symlink will take precedence over the published package. Ensure to push changes to this package before pushing changes to your project. + +Now you can make changes to the package and see them reflected in your Next.js project. The package must be built after every change to see the changes in your Next.js project. +```bash +# Build the package +npm run build +``` + +To save rebuilding the package after every change, you can run the following command in this project to watch for changes and rebuild the package, which will in turn update the symlink in your Next.js project. +```bash +# Watch for changes and rebuild the package +npm run dev +# Cancel the watch process +^c +``` diff --git a/dist/Onborda.d.ts b/dist/Onborda.d.ts index 6abc7bc..83e3bce 100644 --- a/dist/Onborda.d.ts +++ b/dist/Onborda.d.ts @@ -1,4 +1,9 @@ import React from "react"; import { OnbordaProps } from "./types"; +/** + * Onborda Component + * @param {OnbordaProps} props + * @constructor + */ declare const Onborda: React.FC; export default Onborda; diff --git a/dist/Onborda.js b/dist/Onborda.js index 489d5e6..822fecf 100644 --- a/dist/Onborda.js +++ b/dist/Onborda.js @@ -1,43 +1,243 @@ "use client"; -import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; -import { useState, useEffect, useRef } from "react"; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { useEffect, useMemo, useRef, useState } from "react"; import { useOnborda } from "./OnbordaContext"; -import { motion, useInView } from "framer-motion"; -import { useRouter } from "next/navigation"; +import { motion } from "framer-motion"; +import { usePathname, useRouter } from "next/navigation"; import { Portal } from "@radix-ui/react-portal"; -const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2", cardTransition = { ease: "anticipate", duration: 0.6 }, cardComponent: CardComponent, }) => { - const { currentTour, currentStep, setCurrentStep, isOnbordaVisible } = useOnborda(); - const currentTourSteps = steps.find((tour) => tour.tour === currentTour)?.steps; +import { getCardStyle, getArrowStyle } from "./OnbordaStyles"; +/** + * Onborda Component + * @param {OnbordaProps} props + * @constructor + */ +const Onborda = ({ children, shadowRgb = "0, 0, 0", shadowOpacity = "0.2", cardTransition = { ease: "anticipate", duration: 0.6 }, cardComponent: CardComponent, tourComponent: TourComponent, debug = false, observerTimeout = 5000, }) => { + const { currentTour, currentStep, setCurrentStep, isOnbordaVisible, currentTourSteps, completedSteps, setCompletedSteps, tours, closeOnborda } = useOnborda(); const [elementToScroll, setElementToScroll] = useState(null); const [pointerPosition, setPointerPosition] = useState(null); const currentElementRef = useRef(null); - const observeRef = useRef(null); // Ref for the observer element - const isInView = useInView(observeRef); - const offset = 20; + const [currentRoute, setCurrentRoute] = useState(null); + const [pendingRouteChange, setPendingRouteChange] = useState(false); + const hasSelector = (step) => { + return !!step?.selector || !!step?.customQuerySelector; + }; + const getStepSelectorElement = (step) => { + return step?.selector ? document.querySelector(step.selector) : step?.customQuerySelector ? step.customQuerySelector() : null; + }; + // Get the current tour object + const currentTourObject = useMemo(() => { + return tours.find((tour) => tour.tour === currentTour); + }, [currentTour, isOnbordaVisible]); // - - // Route Changes const router = useRouter(); + const path = usePathname(); + // Update the current route on route changes + useEffect(() => { + setCurrentRoute(path); + }, [path]); // - - // Initialisze useEffect(() => { + let cleanup = []; if (isOnbordaVisible && currentTourSteps) { - console.log("Onborda: Current Step Changed"); + debug && console.log("Onborda: Current Step Changed", currentStep); const step = currentTourSteps[currentStep]; if (step) { - const element = document.querySelector(step.selector); - if (element) { - setPointerPosition(getElementPosition(element)); - currentElementRef.current = element; - setElementToScroll(element); - const rect = element.getBoundingClientRect(); - const isInViewportWithOffset = rect.top >= -offset && rect.bottom <= window.innerHeight + offset; - if (!isInView || !isInViewportWithOffset) { - element.scrollIntoView({ behavior: "smooth", block: "center" }); + let elementFound = false; + // Check if the step has a selector + if (hasSelector(step)) { + // This step has a selector. Lets find the element + const element = getStepSelectorElement(step); + // Check if the element is found + if (element) { + // Once the element is found, update the step and scroll to the element + setPointerPosition(getElementPosition(element)); + setElementToScroll(element); + currentElementRef.current = element; + // Function to mark the step as completed if the conditions are met + const handleInteraction = () => { + const isComplete = step?.isCompleteConditions?.(element) ?? true; + debug && console.log("Onborda: Step Interaction", step, isComplete); + // Check if the step is complete based on the conditions, and not already marked as completed + if (isComplete && !Array.from(completedSteps).includes(currentStep)) { + debug && console.log("Onborda: Step Completed", step); + setCompletedSteps((prev) => { + return prev.add(currentStep); + }); + // If callback is provided, call it + step?.onComplete && step.onComplete(); + } // Check if the step is incomplete based on the conditions, and already marked as completed + else if (!isComplete && Array.from(completedSteps).includes(currentStep)) { + debug && console.log("Onborda: Step Incomplete", step); + setCompletedSteps((prev) => { + prev.delete(currentStep); + return prev; + }); + } + }; + // Initial check + handleInteraction(); + // Enable pointer events on the element + if (step.interactable) { + const htmlElement = element; + htmlElement.style.pointerEvents = "auto"; + // Add event listeners if the step is interactable and has conditions + if (step?.isCompleteConditions) { + element.addEventListener("click", handleInteraction); + element.addEventListener("input", handleInteraction); + element.addEventListener("change", handleInteraction); + debug && console.log("Onborda: Added event listeners for element", element); + cleanup.push(() => { + // Cleanup the event listeners + element.removeEventListener("click", handleInteraction); + element.removeEventListener("input", handleInteraction); + element.removeEventListener("change", handleInteraction); + debug && console.log("Onborda: Removed event listeners for element", element); + }); + } + } + elementFound = true; + } + // Even if the element is already found, we still need to check if the route is different from the current route + // do we have a route to navigate to? + if (step.route) { + // Check if the route is set and different from the current route + if (currentRoute == null || !currentRoute?.endsWith(step.route)) { + debug && console.log("Onborda: Navigating to route", step.route); + // Trigger the next route + router.push(step.route); + // Use MutationObserver to detect when the target element is available in the DOM + const observer = new MutationObserver((mutations, observer) => { + const shouldSelect = hasSelector(currentTourSteps[currentStep]); + if (shouldSelect) { + const element = getStepSelectorElement(currentTourSteps[currentStep]); + if (element) { + // Once the element is found, update the step and scroll to the element + setPointerPosition(getElementPosition(element)); + setElementToScroll(element); + currentElementRef.current = element; + const handleInteraction = () => { + const isComplete = step?.isCompleteConditions?.(element) ?? true; + debug && console.log("Onborda: Step Interaction", step, isComplete); + // Check if the step is complete based on the conditions, and not already marked as completed + if (isComplete && !Array.from(completedSteps).includes(currentStep)) { + debug && console.log("Onborda: Step Completed", step); + setCompletedSteps((prev) => { + return prev.add(currentStep); + }); + // If callback is provided, call it + step?.onComplete && step.onComplete(); + } // Check if the step is incomplete based on the conditions, and already marked as completed + else if (!isComplete && Array.from(completedSteps).includes(currentStep)) { + debug && console.log("Onborda: Step Incomplete", step); + setCompletedSteps((prev) => { + prev.delete(currentStep); + return prev; + }); + } + }; + // Initial check + handleInteraction(); + // Enable pointer events on the element + if (step.interactable) { + const htmlElement = element; + htmlElement.style.pointerEvents = "auto"; + } + // Stop observing after the element is found + observer.disconnect(); + debug && console.log("Onborda: Observer disconnected after element found", element); + } + else { + debug && console.log("Onborda: Observing for element...", currentTourSteps[currentStep]); + } + } + else { + setCurrentStep(currentStep); + observer.disconnect(); + debug && console.log("Onborda: Observer disconnected after no selector set", currentTourSteps[currentStep]); + } + }); + // Start observing the document body for changes + observer.observe(document.body, { + childList: true, + subtree: true, + }); + setPendingRouteChange(true); + // Set a timeout to disconnect the observer if the element is not found within a certain period + const timeoutId = setTimeout(() => { + observer.disconnect(); + console.error("Onborda: Observer Timeout", currentTourSteps[currentStep]); + }, observerTimeout); // Adjust the timeout period as needed + // Clear the timeout if the observer disconnects successfully + const originalDisconnect = observer.disconnect.bind(observer); + observer.disconnect = () => { + setPendingRouteChange(false); + clearTimeout(timeoutId); + originalDisconnect(); + }; + } + } + } + else { + // no selector, but might still need to navigate to a route + if (step.route) { + // Check if the route is set and different from the current route + if (currentRoute == null || !currentRoute?.endsWith(step.route)) { + debug && console.log("Onborda: Navigating to route", step.route); + // Trigger the next route + router.push(step.route); + } + else { + // Mark the step as completed + step?.onComplete && step.onComplete(); + setCompletedSteps((prev) => { + return prev.add(currentStep); + }); + } + } + else { + // Mark the step as completed + step?.onComplete && step.onComplete(); + setCompletedSteps((prev) => { + return prev.add(currentStep); + }); } } + // No element set for this step? Place the pointer at the center of the screen + if (!elementFound) { + setPointerPosition({ + x: window.innerWidth / 2, + y: window.innerHeight / 2, + width: 0, + height: 0, + }); + setElementToScroll(null); + currentElementRef.current = null; + } + // Prefetch the next route + const nextStep = currentTourSteps[currentStep + 1]; + if (nextStep && nextStep?.route) { + debug && console.log("Onborda: Prefetching Next Route", nextStep.route); + router.prefetch(nextStep.route); + } } } - }, [currentStep, currentTourSteps, isInView, offset, isOnbordaVisible]); + return () => { + // Disable pointer events on the element on cleanup + if (currentElementRef.current) { + const htmlElement = currentElementRef.current; + htmlElement.style.pointerEvents = ""; + } + // Cleanup any event listeners we may have added + cleanup.forEach(fn => fn()); + }; + }, [ + currentStep, // Re-run the effect when the current step changes + currentTourSteps, // Re-run the effect when the current tour steps change + isOnbordaVisible, // Re-run the effect when the onborda visibility changes + currentRoute, // Re-run the effect when the current route changes + ]); // - - // Helper function to get element position const getElementPosition = (element) => { @@ -52,29 +252,10 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" }; }; // - - - // Update pointerPosition when currentStep changes - useEffect(() => { - if (isOnbordaVisible && currentTourSteps) { - console.log("Onborda: Current Step Changed"); - const step = currentTourSteps[currentStep]; - if (step) { - const element = document.querySelector(step.selector); - if (element) { - setPointerPosition(getElementPosition(element)); - currentElementRef.current = element; - setElementToScroll(element); - const rect = element.getBoundingClientRect(); - const isInViewportWithOffset = rect.top >= -offset && rect.bottom <= window.innerHeight + offset; - if (!isInView || !isInViewportWithOffset) { - element.scrollIntoView({ behavior: "smooth", block: "center" }); - } - } - } - } - }, [currentStep, currentTourSteps, isInView, offset, isOnbordaVisible]); + // Scroll to the element when the elementToScroll changes useEffect(() => { - if (elementToScroll && !isInView && isOnbordaVisible) { - console.log("Onborda: Element to Scroll Changed"); + if (elementToScroll && isOnbordaVisible) { + debug && console.log("Onborda: Element to Scroll Changed"); const rect = elementToScroll.getBoundingClientRect(); const isAbove = rect.top < 0; elementToScroll.scrollIntoView({ @@ -83,17 +264,28 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" inline: "center", }); } - }, [elementToScroll, isInView, isOnbordaVisible]); + }, [elementToScroll, isOnbordaVisible]); // - - // Update pointer position on window resize const updatePointerPosition = () => { if (currentTourSteps) { const step = currentTourSteps[currentStep]; if (step) { - const element = document.querySelector(step.selector); + const element = getStepSelectorElement(step); if (element) { setPointerPosition(getElementPosition(element)); } + else { + // if the element is not found, place the pointer at the center of the screen + setPointerPosition({ + x: window.innerWidth / 2, + y: window.innerHeight / 2, + width: 0, + height: 0, + }); + setElementToScroll(null); + currentElementRef.current = null; + } } } }; @@ -108,256 +300,23 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" // - - // Step Controls const nextStep = async () => { - if (currentTourSteps && currentStep < currentTourSteps.length - 1) { - try { - const nextStepIndex = currentStep + 1; - const route = currentTourSteps[currentStep].nextRoute; - if (route) { - await router.push(route); - const targetSelector = currentTourSteps[nextStepIndex].selector; - // Use MutationObserver to detect when the target element is available in the DOM - const observer = new MutationObserver((mutations, observer) => { - const element = document.querySelector(targetSelector); - if (element) { - // Once the element is found, update the step and scroll to the element - setCurrentStep(nextStepIndex); - scrollToElement(nextStepIndex); - // Stop observing after the element is found - observer.disconnect(); - } - }); - // Start observing the document body for changes - observer.observe(document.body, { - childList: true, - subtree: true, - }); - } - else { - setCurrentStep(nextStepIndex); - scrollToElement(nextStepIndex); - } - } - catch (error) { - console.error("Error navigating to next route", error); - } - } + const nextStepIndex = currentStep + 1; + await setStep(nextStepIndex); }; const prevStep = async () => { - if (currentTourSteps && currentStep > 0) { - try { - const prevStepIndex = currentStep - 1; - const route = currentTourSteps[currentStep].prevRoute; - if (route) { - await router.push(route); - const targetSelector = currentTourSteps[prevStepIndex].selector; - // Use MutationObserver to detect when the target element is available in the DOM - const observer = new MutationObserver((mutations, observer) => { - const element = document.querySelector(targetSelector); - if (element) { - // Once the element is found, update the step and scroll to the element - setCurrentStep(prevStepIndex); - scrollToElement(prevStepIndex); - // Stop observing after the element is found - observer.disconnect(); - } - }); - // Start observing the document body for changes - observer.observe(document.body, { - childList: true, - subtree: true, - }); - } - else { - setCurrentStep(prevStepIndex); - scrollToElement(prevStepIndex); - } - } - catch (error) { - console.error("Error navigating to previous route", error); - } - } - }; - // - - - // Scroll to the correct element when the step changes - const scrollToElement = (stepIndex) => { - if (currentTourSteps) { - const element = document.querySelector(currentTourSteps[stepIndex].selector); - if (element) { - const { top } = element.getBoundingClientRect(); - const isInViewport = top >= -offset && top <= window.innerHeight + offset; - if (!isInViewport) { - element.scrollIntoView({ behavior: "smooth", block: "center" }); - } - // Update pointer position after scrolling - setPointerPosition(getElementPosition(element)); - } - } + const prevStepIndex = currentStep - 1; + await setStep(prevStepIndex); }; - // - - - // Card Side - const getCardStyle = (side) => { - switch (side) { - case "top": - return { - transform: `translate(-50%, 0)`, - left: "50%", - bottom: "100%", - marginBottom: "25px", - }; - case "bottom": - return { - transform: `translate(-50%, 0)`, - left: "50%", - top: "100%", - marginTop: "25px", - }; - case "left": - return { - transform: `translate(0, -50%)`, - right: "100%", - top: "50%", - marginRight: "25px", - }; - case "right": - return { - transform: `translate(0, -50%)`, - left: "100%", - top: "50%", - marginLeft: "25px", - }; - case "top-left": - return { - bottom: "100%", - marginBottom: "25px", - }; - case "top-right": - return { - right: 0, - bottom: "100%", - marginBottom: "25px", - }; - case "bottom-left": - return { - top: "100%", - marginTop: "25px", - }; - case "bottom-right": - return { - right: 0, - top: "100%", - marginTop: "25px", - }; - case "right-bottom": - return { - left: "100%", - bottom: 0, - marginLeft: "25px", - }; - case "right-top": - return { - left: "100%", - top: 0, - marginLeft: "25px", - }; - case "left-bottom": - return { - right: "100%", - bottom: 0, - marginRight: "25px", - }; - case "left-top": - return { - right: "100%", - top: 0, - marginRight: "25px", - }; - default: - return {}; // Default case if no side is specified - } - }; - // - - - // Arrow position based on card side - const getArrowStyle = (side) => { - switch (side) { - case "bottom": - return { - transform: `translate(-50%, 0) rotate(270deg)`, - left: "50%", - top: "-23px", - }; - case "top": - return { - transform: `translate(-50%, 0) rotate(90deg)`, - left: "50%", - bottom: "-23px", - }; - case "right": - return { - transform: `translate(0, -50%) rotate(180deg)`, - top: "50%", - left: "-23px", - }; - case "left": - return { - transform: `translate(0, -50%) rotate(0deg)`, - top: "50%", - right: "-23px", - }; - case "top-left": - return { - transform: `rotate(90deg)`, - left: "10px", - bottom: "-23px", - }; - case "top-right": - return { - transform: `rotate(90deg)`, - right: "10px", - bottom: "-23px", - }; - case "bottom-left": - return { - transform: `rotate(270deg)`, - left: "10px", - top: "-23px", - }; - case "bottom-right": - return { - transform: `rotate(270deg)`, - right: "10px", - top: "-23px", - }; - case "right-bottom": - return { - transform: `rotate(180deg)`, - left: "-23px", - bottom: "10px", - }; - case "right-top": - return { - transform: `rotate(180deg)`, - left: "-23px", - top: "10px", - }; - case "left-bottom": - return { - transform: `rotate(0deg)`, - right: "-23px", - bottom: "10px", - }; - case "left-top": - return { - transform: `rotate(0deg)`, - right: "-23px", - top: "10px", - }; - default: - return {}; // Default case if no side is specified - } + const setStep = async (step) => { + const setStepIndex = typeof step === 'string' ? currentTourSteps.findIndex((s) => s?.id === step) : step; + setCurrentStep(setStepIndex); }; // - - // Card Arrow - const CardArrow = () => { + const CardArrow = ({ isVisible }) => { + if (!isVisible) { + return null; + } return (_jsx("svg", { viewBox: "0 0 54 54", "data-name": "onborda-arrow", className: "absolute w-6 h-6 origin-center", style: getArrowStyle(currentTourSteps?.[currentStep]?.side), children: _jsx("path", { id: "triangle", d: "M27 27L0 0V54L27 27Z", fill: "currentColor" }) })); }; // - - @@ -371,23 +330,24 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" const pointerPadding = currentTourSteps?.[currentStep]?.pointerPadding ?? 30; const pointerPadOffset = pointerPadding / 2; const pointerRadius = currentTourSteps?.[currentStep]?.pointerRadius ?? 28; - return (_jsxs("div", { "data-name": "onborda-wrapper", className: "relative w-full", "data-onborda": "dev", children: [_jsx("div", { "data-name": "onborda-site", className: "block w-full", children: children }), pointerPosition && isOnbordaVisible && CardComponent && (_jsx(Portal, { children: _jsx(motion.div, { "data-name": "onborda-overlay", className: "absolute inset-0 ", initial: "hidden", animate: isOnbordaVisible ? "visible" : "hidden", variants: variants, transition: { duration: 0.5 }, children: _jsx(motion.div, { "data-name": "onborda-pointer", className: "relative z-[999]", style: { - boxShadow: `0 0 200vw 200vh rgba(${shadowRgb}, ${shadowOpacity})`, - borderRadius: `${pointerRadius}px ${pointerRadius}px ${pointerRadius}px ${pointerRadius}px`, - }, initial: pointerPosition - ? { - x: pointerPosition.x - pointerPadOffset, - y: pointerPosition.y - pointerPadOffset, - width: pointerPosition.width + pointerPadding, - height: pointerPosition.height + pointerPadding, - } - : {}, animate: pointerPosition - ? { - x: pointerPosition.x - pointerPadOffset, - y: pointerPosition.y - pointerPadOffset, - width: pointerPosition.width + pointerPadding, - height: pointerPosition.height + pointerPadding, - } - : {}, transition: cardTransition, children: _jsx("div", { className: "absolute flex flex-col max-w-[100%] transition-all min-w-min pointer-events-auto z-[999]", "data-name": "onborda-card", style: getCardStyle(currentTourSteps?.[currentStep]?.side), children: _jsx(CardComponent, { step: currentTourSteps?.[currentStep], currentStep: currentStep, totalSteps: currentTourSteps?.length ?? 0, nextStep: nextStep, prevStep: prevStep, arrow: _jsx(CardArrow, {}) }) }) }) }) }))] })); + const pointerEvents = pointerPosition && isOnbordaVisible ? 'pointer-events-none' : ''; + return (_jsxs(_Fragment, { children: [_jsx("div", { "data-name": "onborda-site-wrapper", className: ` ${pointerEvents} `, children: children }), pointerPosition && isOnbordaVisible && CardComponent && (_jsxs(Portal, { children: [_jsx(motion.div, { "data-name": "onborda-overlay", className: "absolute inset-0 pointer-events-none z-[997]", initial: "hidden", animate: isOnbordaVisible ? "visible" : "hidden", variants: variants, transition: { duration: 0.5 }, children: _jsx(motion.div, { "data-name": "onborda-pointer", className: "relative z-[998]", style: { + boxShadow: `0 0 200vw 200vh rgba(${shadowRgb}, ${shadowOpacity})`, + borderRadius: `${pointerRadius}px ${pointerRadius}px ${pointerRadius}px ${pointerRadius}px`, + }, initial: pointerPosition + ? { + x: pointerPosition.x - pointerPadOffset, + y: pointerPosition.y - pointerPadOffset, + width: pointerPosition.width + pointerPadding, + height: pointerPosition.height + pointerPadding, + } + : {}, animate: pointerPosition + ? { + x: pointerPosition.x - pointerPadOffset, + y: pointerPosition.y - pointerPadOffset, + width: pointerPosition.width + pointerPadding, + height: pointerPosition.height + pointerPadding, + } + : {}, transition: cardTransition, children: _jsx("div", { className: "absolute flex flex-col max-w-[100%] transition-all min-w-min pointer-events-auto z-[999]", "data-name": "onborda-card", style: getCardStyle(currentTourSteps?.[currentStep]?.side), children: _jsx(CardComponent, { step: currentTourSteps?.[currentStep], currentStep: currentStep, totalSteps: currentTourSteps?.length ?? 0, nextStep: nextStep, prevStep: prevStep, setStep: setStep, closeOnborda: closeOnborda, arrow: _jsx(CardArrow, { isVisible: currentTourSteps?.[currentStep] ? hasSelector(currentTourSteps?.[currentStep]) : false }), completedSteps: Array.from(completedSteps), pendingRouteChange: pendingRouteChange }) }) }) }), TourComponent && currentTourObject !== undefined && (_jsx(motion.div, { "data-name": 'onborda-tour-wrapper', className: 'fixed top-0 left-0 z-[998] w-screen h-screen pointer-events-none', children: _jsx(motion.div, { "data-name": 'onborda-tour', className: 'pointer-events-auto', children: _jsx(TourComponent, { tour: currentTourObject, currentTour: currentTour, currentStep: currentStep, setStep: setStep, completedSteps: Array.from(completedSteps), closeOnborda: closeOnborda }) }) }))] }))] })); }; export default Onborda; diff --git a/dist/OnbordaContext.d.ts b/dist/OnbordaContext.d.ts index a7ef976..b2f432e 100644 --- a/dist/OnbordaContext.d.ts +++ b/dist/OnbordaContext.d.ts @@ -1,7 +1,5 @@ import React from "react"; -import { OnbordaContextType } from "./types"; +import { OnbordaContextType, OnbordaProviderProps } from "./types"; declare const useOnborda: () => OnbordaContextType; -declare const OnbordaProvider: React.FC<{ - children: React.ReactNode; -}>; +declare const OnbordaProvider: React.FC; export { OnbordaProvider, useOnborda }; diff --git a/dist/OnbordaContext.js b/dist/OnbordaContext.js index dce5940..3e25e72 100644 --- a/dist/OnbordaContext.js +++ b/dist/OnbordaContext.js @@ -1,6 +1,6 @@ "use client"; import { jsx as _jsx } from "react/jsx-runtime"; -import { createContext, useContext, useState, useCallback } from "react"; +import { createContext, useContext, useState, useCallback, useEffect } from "react"; // Example Hooks Usage: // const { setCurrentStep, closeOnborda, startOnborda } = useOnborda(); // // To trigger a specific step @@ -16,11 +16,27 @@ const useOnborda = () => { } return context; }; -const OnbordaProvider = ({ children, }) => { - const [currentTour, setCurrentTour] = useState(null); +const OnbordaProvider = ({ children, tours = [], activeTour = null, }) => { + const [currentTour, setCurrentTourState] = useState(null); const [currentStep, setCurrentStepState] = useState(0); const [isOnbordaVisible, setOnbordaVisible] = useState(false); + const [currentTourSteps, setCurrentTourStepsState] = useState([]); + const [completedSteps, setCompletedSteps] = useState(new Set()); + // Start the active tour on mount + useEffect(() => { + if (activeTour) { + startOnborda(activeTour); + } + }, [activeTour]); const setCurrentStep = useCallback((step, delay) => { + // If step is a string, find the index of the step with that id + if (typeof step === 'string') { + const index = currentTourSteps.findIndex((s) => s?.id === step); + if (index === -1) { + throw new Error(`Step with id ${step} not found`); + } + step = index; + } if (delay) { setTimeout(() => { setCurrentStepState(step); @@ -33,21 +49,57 @@ const OnbordaProvider = ({ children, }) => { } }, []); const closeOnborda = useCallback(() => { + // If all steps are completed, call the onComplete function + if (completedSteps.size === currentTourSteps.length) { + tours.find((tour) => (tour.tour) === currentTour)?.onComplete?.(); + } setOnbordaVisible(false); - setCurrentTour(null); - }, []); + setCurrentTourState(null); + setCurrentTourStepsState([]); + setCurrentStepState(0); + setCompletedSteps(new Set()); + }, [currentTour, currentTourSteps, completedSteps]); + const initializeCompletedSteps = useCallback(async (tour) => { + // Get the initial state of the completed steps + const completeSteps = tour?.initialCompletedStepsState && await tour.initialCompletedStepsState() || tour.steps.map(() => false); + const firstIncomplete = completeSteps.findIndex((result) => !result); + const completed = completeSteps.reduce((acc, result, index) => { + if (result) { + acc.push(index); + } + return acc; + }, []); + setCompletedSteps(new Set(completed)); + // If all steps are completed, return the last step + return firstIncomplete === -1 ? tour.steps.length - 1 : firstIncomplete; + }, [currentTour]); + const setCurrentTour = useCallback((tourName) => { + if (!tourName) { + closeOnborda(); + return; + } + setCurrentTourState(tourName); + const tour = tours.find((tour) => tour.tour === tourName); + setCurrentTourStepsState(tour?.steps || []); + tour && initializeCompletedSteps(tour).then(r => { + setCurrentStep(r); + setOnbordaVisible(true); + }); + }, [tours]); const startOnborda = useCallback((tourName) => { setCurrentTour(tourName); - setCurrentStepState(0); - setOnbordaVisible(true); - }, []); + }, [setCurrentTour]); return (_jsx(OnbordaContext.Provider, { value: { + tours, currentTour, currentStep, + currentTourSteps, setCurrentStep, closeOnborda, startOnborda, isOnbordaVisible, + completedSteps, + setCompletedSteps }, children: children })); }; export { OnbordaProvider, useOnborda }; diff --git a/dist/OnbordaStyles.d.ts b/dist/OnbordaStyles.d.ts new file mode 100644 index 0000000..b077379 --- /dev/null +++ b/dist/OnbordaStyles.d.ts @@ -0,0 +1,3 @@ +import React from "react"; +export declare const getCardStyle: (side: string) => React.CSSProperties; +export declare const getArrowStyle: (side: string) => React.CSSProperties; diff --git a/dist/OnbordaStyles.js b/dist/OnbordaStyles.js new file mode 100644 index 0000000..8a40e2b --- /dev/null +++ b/dist/OnbordaStyles.js @@ -0,0 +1,165 @@ +export const getCardStyle = (side) => { + switch (side) { + case "top": + return { + transform: `translate(-50%, 0)`, + left: "50%", + bottom: "100%", + marginBottom: "25px", + }; + case "bottom": + return { + transform: `translate(-50%, 0)`, + left: "50%", + top: "100%", + marginTop: "25px", + }; + case "left": + return { + transform: `translate(0, -50%)`, + right: "100%", + top: "50%", + marginRight: "25px", + }; + case "right": + return { + transform: `translate(0, -50%)`, + left: "100%", + top: "50%", + marginLeft: "25px", + }; + case "top-left": + return { + bottom: "100%", + marginBottom: "25px", + }; + case "top-right": + return { + right: 0, + bottom: "100%", + marginBottom: "25px", + }; + case "bottom-left": + return { + top: "100%", + marginTop: "25px", + }; + case "bottom-right": + return { + right: 0, + top: "100%", + marginTop: "25px", + }; + case "right-bottom": + return { + left: "100%", + bottom: 0, + marginLeft: "25px", + }; + case "right-top": + return { + left: "100%", + top: 0, + marginLeft: "25px", + }; + case "left-bottom": + return { + right: "100%", + bottom: 0, + marginRight: "25px", + }; + case "left-top": + return { + right: "100%", + top: 0, + marginRight: "25px", + }; + default: + // Default case if no side is specified. Center the card to the screen + return { + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', // Center the card + position: 'fixed', // Make sure it's positioned relative to the viewport + margin: '0', + }; + } +}; +export const getArrowStyle = (side) => { + switch (side) { + case "bottom": + return { + transform: `translate(-50%, 0) rotate(270deg)`, + left: "50%", + top: "-23px", + }; + case "top": + return { + transform: `translate(-50%, 0) rotate(90deg)`, + left: "50%", + bottom: "-23px", + }; + case "right": + return { + transform: `translate(0, -50%) rotate(180deg)`, + top: "50%", + left: "-23px", + }; + case "left": + return { + transform: `translate(0, -50%) rotate(0deg)`, + top: "50%", + right: "-23px", + }; + case "top-left": + return { + transform: `rotate(90deg)`, + left: "10px", + bottom: "-23px", + }; + case "top-right": + return { + transform: `rotate(90deg)`, + right: "10px", + bottom: "-23px", + }; + case "bottom-left": + return { + transform: `rotate(270deg)`, + left: "10px", + top: "-23px", + }; + case "bottom-right": + return { + transform: `rotate(270deg)`, + right: "10px", + top: "-23px", + }; + case "right-bottom": + return { + transform: `rotate(180deg)`, + left: "-23px", + bottom: "10px", + }; + case "right-top": + return { + transform: `rotate(180deg)`, + left: "-23px", + top: "10px", + }; + case "left-bottom": + return { + transform: `rotate(0deg)`, + right: "-23px", + bottom: "10px", + }; + case "left-top": + return { + transform: `rotate(0deg)`, + right: "-23px", + top: "10px", + }; + default: + return {}; // Default case if no side is specified + } +}; diff --git a/dist/types/index.d.ts b/dist/types/index.d.ts index 8857579..60f5bf5 100644 --- a/dist/types/index.d.ts +++ b/dist/types/index.d.ts @@ -1,43 +1,146 @@ -/// import { Transition } from "framer-motion"; +export interface OnbordaProviderProps { + /** The children elements to be rendered inside the OnbordaProvider component */ + children: React.ReactNode; + /** An array of tours, each containing multiple steps */ + tours: Tour[]; + /** Active Tour */ + activeTour?: string; +} export interface OnbordaContextType { + /** array of tours */ + tours: Tour[]; + /** current step index */ currentStep: number; + /** current tour name */ currentTour: string | null; - setCurrentStep: (step: number, delay?: number) => void; + /** current tour steps */ + currentTourSteps: Step[]; + /** function to set the current step */ + setCurrentStep: (step: number | string, delay?: number) => void; + /** function to close Onborda */ closeOnborda: () => void; + /** function to start Onborda */ startOnborda: (tourName: string) => void; + /** flag to check if Onborda is visible */ isOnbordaVisible: boolean; + /** default completed steps */ + completedSteps: Set; + /** setstate function to set the completed steps */ + setCompletedSteps: React.Dispatch>>; } export interface Step { - icon: React.ReactNode | string | null; - title: string; + /** The unique identifier for the step */ + id?: string; + /** The title of the step */ + title?: string; + /** The content to be displayed in the step */ content: React.ReactNode; - selector: string; + /** The icon to be displayed in the step */ + icon?: React.ReactNode | string | null; + /** The CSS selector for the element to highlight. Takes precedence over customQuerySelector if both are provided. */ + selector?: string; + /** A custom function to query the target element. Ignored if selector is provided. */ + customQuerySelector?: () => Element | null; + /** The side where the step should be displayed */ side?: "top" | "bottom" | "left" | "right" | "top-left" | "top-right" | "bottom-left" | "bottom-right" | "left-top" | "left-bottom" | "right-top" | "right-bottom"; + /** Flag to show or hide the controls */ showControls?: boolean; + /** Padding around the pointer */ pointerPadding?: number; + /** Radius of the pointer */ pointerRadius?: number; + /** Flag to make the step interactable */ + interactable?: boolean; + /** Conditions to be met before the next step can be triggered. Function is bound to event listeners on the target element on 'input', 'change' and 'click' events. */ + isCompleteConditions?: (element: Element | null) => boolean; + /** The route for this step */ + route?: string; + /** The route to navigate to for the next step */ + /** @deprecated Use `route` instead */ nextRoute?: string; + /** The route to navigate to for the previous step */ + /** @deprecated Use `route` instead */ prevRoute?: string; + /** Callback function to be called when the step is completed */ + onComplete?: () => Promise; + /** Any additional data for custom use */ + [key: string]: any; } export interface Tour { + /** The tour ID */ tour: string; + /** Tour Title */ + title?: string; + /** Tour Description */ + description?: string; + /** An array of steps in the tour */ steps: Step[]; + /** Complete Callback */ + onComplete?: () => void; + /** Tour can be dismissed. */ + dismissible?: boolean; + /** Any additional data for custom use */ + [key: string]: any; + /** Initial completed steps state of the tour. an async function called on tour started. Can be a Server Action e.g. Promise.all([]) on API calls. */ + initialCompletedStepsState?: () => Promise; } export interface OnbordaProps { + /** The children elements to be rendered inside the Onborda component */ children: React.ReactNode; - steps: Tour[]; + /** An array of tours, each containing multiple steps */ + /** @deprecated Use `OnbordaProvider.tours` instead */ + steps?: Tour[]; + /** Flag to show or hide the Onborda component */ showOnborda?: boolean; + /** RGB value for the shadow color */ shadowRgb?: string; + /** Opacity value for the shadow */ shadowOpacity?: string; + /** Transition settings for the card component */ cardTransition?: Transition; + /** Custom card component to be used in the Onborda */ cardComponent?: React.ComponentType; + /** Custom tour component to be used in the Onborda */ + tourComponent?: React.ComponentType; + /** Flag to enable or disable debug mode */ + debug?: boolean; + /** Timeout value for the observer when observing for the target element */ + observerTimeout?: number; } export interface CardComponentProps { + /** The current step object containing details of the step */ step: Step; + /** The index of the current step */ currentStep: number; + /** The total number of steps in the tour */ totalSteps: number; + /** Function to set the current step by step index or step.id */ + setStep: (step: number | string) => void; + /** Function to navigate to the next step */ nextStep: () => void; + /** Function to navigate to the previous step */ prevStep: () => void; + /** Function to close the Onborda */ + closeOnborda: () => void; + /** The arrow element to be displayed in the card */ arrow: JSX.Element; + /** Array of completed steps */ + completedSteps: (string | number)[]; + /** Is waiting for Route change */ + pendingRouteChange: boolean; +} +export interface TourComponentProps { + /** The current tour name */ + currentTour: string | null; + /** The index of the current step */ + currentStep: number; + /** The current Tour object containing details of the tour */ + tour: Tour; + /** Function to set the current step by step index or step.id */ + setStep: (step: number | string) => void; + /** Array of completed steps */ + completedSteps: (string | number)[]; + /** Function to close the Onborda */ + closeOnborda: () => void; } diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..26fa357 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1491 @@ +{ + "name": "onborda", + "version": "1.2.4", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "onborda", + "version": "1.2.4", + "license": "MIT", + "devDependencies": { + "@types/react": "^18.2.63", + "nodemon": "^3.1.7", + "typescript": "^5.3.3" + }, + "peerDependencies": { + "@radix-ui/react-portal": ">=1.1.1", + "framer-motion": ">=11", + "next": ">=13", + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", + "integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@next/env": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.0.1.tgz", + "integrity": "sha512-lc4HeDUKO9gxxlM5G2knTRifqhsY6yYpwuHspBZdboZe0Gp+rZHBNNSIjmQKDJIdRXiXGyVnSD6gafrbQPvILQ==", + "license": "MIT", + "peer": true + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.0.1.tgz", + "integrity": "sha512-C9k/Xv4sxkQRTA37Z6MzNq3Yb1BJMmSqjmwowoWEpbXTkAdfOwnoKOpAb71ItSzoA26yUTIo6ZhN8rKGu4ExQw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.0.1.tgz", + "integrity": "sha512-uHl13HXOuq1G7ovWFxCACDJHTSDVbn/sbLv8V1p+7KIvTrYQ5HNoSmKBdYeEKRRCbEmd+OohOgg9YOp8Ux3MBg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.0.1.tgz", + "integrity": "sha512-LvyhvxHOihFTEIbb35KxOc3q8w8G4xAAAH/AQnsYDEnOvwawjL2eawsB59AX02ki6LJdgDaHoTEnC54Gw+82xw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.0.1.tgz", + "integrity": "sha512-vFmCGUFNyk/A5/BYcQNhAQqPIw01RJaK6dRO+ZEhz0DncoW+hJW1kZ8aH2UvTX27zPq3m85zN5waMSbZEmANcQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.0.1.tgz", + "integrity": "sha512-5by7IYq0NCF8rouz6Qg9T97jYU68kaClHPfGpQG2lCZpSYHtSPQF1kjnqBTd34RIqPKMbCa4DqCufirgr8HM5w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.0.1.tgz", + "integrity": "sha512-lmYr6H3JyDNBJLzklGXLfbehU3ay78a+b6UmBGlHls4xhDXBNZfgb0aI67sflrX+cGBnv1LgmWzFlYrAYxS1Qw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.0.1.tgz", + "integrity": "sha512-DS8wQtl6diAj0eZTdH0sefykm4iXMbHT4MOvLwqZiIkeezKpkgPFcEdFlz3vKvXa2R/2UEgMh48z1nEpNhjeOQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.0.1.tgz", + "integrity": "sha512-4Ho2ggvDdMKlZ/0e9HNdZ9ngeaBwtc+2VS5oCeqrbXqOgutX6I4U2X/42VBw0o+M5evn4/7v3zKgGHo+9v/VjA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", + "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.2.tgz", + "integrity": "sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", + "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/react-slot": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", + "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", + "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/@swc/helpers": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.13.tgz", + "integrity": "sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.13", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", + "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.12", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz", + "integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "peer": true, + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001673", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001673.tgz", + "integrity": "sha512-WTrjUCSMp3LYX0nE12ECkV0a+e6LC85E0Auz75555/qr78Oc8YWhEPNfDd6SHdtlCMSzqtuXY0uyEMNRcsKpKw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0", + "peer": true + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT", + "peer": true + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/framer-motion": { + "version": "11.11.10", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.11.10.tgz", + "integrity": "sha512-061Bt1jL/vIm+diYIiA4dP/Yld7vD47ROextS7ESBW5hr4wQFhxB5D5T5zAc3c/5me3cOa+iO5LqhA38WDln/A==", + "license": "MIT", + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT", + "peer": true + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/next/-/next-15.0.1.tgz", + "integrity": "sha512-PSkFkr/w7UnFWm+EP8y/QpHrJXMqpZzAXpergB/EqLPOh4SGPJXv1wj4mslr2hUZBAS9pX7/9YLIdxTv6fwytw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@next/env": "15.0.1", + "@swc/counter": "0.1.3", + "@swc/helpers": "0.5.13", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=18.18.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.0.1", + "@next/swc-darwin-x64": "15.0.1", + "@next/swc-linux-arm64-gnu": "15.0.1", + "@next/swc-linux-arm64-musl": "15.0.1", + "@next/swc-linux-x64-gnu": "15.0.1", + "@next/swc-linux-x64-musl": "15.0.1", + "@next/swc-win32-arm64-msvc": "15.0.1", + "@next/swc-win32-x64-msvc": "15.0.1", + "sharp": "^0.33.5" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-69d4b800-20241021", + "react-dom": "^18.2.0 || 19.0.0-rc-69d4b800-20241021", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/nodemon": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz", + "integrity": "sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC", + "peer": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "devOptional": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "peer": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "peer": true, + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tslib": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", + "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==", + "license": "0BSD", + "peer": true + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/package.json b/package.json index 735ee31..0ea1b73 100644 --- a/package.json +++ b/package.json @@ -25,17 +25,19 @@ "types": "dist/index.d.ts", "scripts": { "build": "tsc --project tsconfig.json", - "start": "npm run build && node dist/index.js" + "start": "npm run build && node dist/index.js", + "dev": "nodemon --watch \"src/**/*\" --ext \"ts,tsx,js,json\" --exec \"npm run build\"" }, "peerDependencies": { + "@radix-ui/react-portal": ">=1.1.1", "framer-motion": ">=11", "next": ">=13", "react": ">=18", - "react-dom": ">=18", - "@radix-ui/react-portal": ">=1.1.1" + "react-dom": ">=18" }, "devDependencies": { "@types/react": "^18.2.63", + "nodemon": "^3.1.7", "typescript": "^5.3.3" } } diff --git a/src/Onborda.tsx b/src/Onborda.tsx index fa71fdb..61b9f62 100644 --- a/src/Onborda.tsx +++ b/src/Onborda.tsx @@ -1,509 +1,502 @@ "use client"; -import React, { useState, useEffect, useRef } from "react"; -import { useOnborda } from "./OnbordaContext"; -import { motion, useInView } from "framer-motion"; -import { useRouter } from "next/navigation"; -import { Portal } from "@radix-ui/react-portal"; +import React, {useEffect, useMemo, useRef, useState} from "react"; +import {useOnborda} from "./OnbordaContext"; +import {motion} from "framer-motion"; +import {usePathname, useRouter} from "next/navigation"; +import {Portal} from "@radix-ui/react-portal"; // Types -import { OnbordaProps } from "./types"; - +import {OnbordaProps, Step} from "./types"; +import {getCardStyle, getArrowStyle} from "./OnbordaStyles"; + +/** + * Onborda Component + * @param {OnbordaProps} props + * @constructor + */ const Onborda: React.FC = ({ - children, - steps, - shadowRgb = "0, 0, 0", - shadowOpacity = "0.2", - cardTransition = { ease: "anticipate", duration: 0.6 }, - cardComponent: CardComponent, -}) => { - const { currentTour, currentStep, setCurrentStep, isOnbordaVisible } = - useOnborda(); - const currentTourSteps = steps.find( - (tour) => tour.tour === currentTour - )?.steps; - - const [elementToScroll, setElementToScroll] = useState(null); - const [pointerPosition, setPointerPosition] = useState<{ - x: number; - y: number; - width: number; - height: number; - } | null>(null); - const currentElementRef = useRef(null); - const observeRef = useRef(null); // Ref for the observer element - const isInView = useInView(observeRef); - const offset = 20; - - // - - - // Route Changes - const router = useRouter(); - - // - - - // Initialisze - useEffect(() => { - if (isOnbordaVisible && currentTourSteps) { - console.log("Onborda: Current Step Changed"); - const step = currentTourSteps[currentStep]; - if (step) { - const element = document.querySelector(step.selector) as Element | null; - if (element) { - setPointerPosition(getElementPosition(element)); - currentElementRef.current = element; - setElementToScroll(element); - - const rect = element.getBoundingClientRect(); - const isInViewportWithOffset = - rect.top >= -offset && rect.bottom <= window.innerHeight + offset; - - if (!isInView || !isInViewportWithOffset) { - element.scrollIntoView({ behavior: "smooth", block: "center" }); - } - } - } - } - }, [currentStep, currentTourSteps, isInView, offset, isOnbordaVisible]); - - // - - - // Helper function to get element position - const getElementPosition = (element: Element) => { - const { top, left, width, height } = element.getBoundingClientRect(); - const scrollTop = window.scrollY || document.documentElement.scrollTop; - const scrollLeft = window.scrollX || document.documentElement.scrollLeft; - return { - x: left + scrollLeft, - y: top + scrollTop, - width, - height, - }; - }; - - // - - - // Update pointerPosition when currentStep changes - useEffect(() => { - if (isOnbordaVisible && currentTourSteps) { - console.log("Onborda: Current Step Changed"); - const step = currentTourSteps[currentStep]; - if (step) { - const element = document.querySelector(step.selector) as Element | null; - if (element) { - setPointerPosition(getElementPosition(element)); - currentElementRef.current = element; - setElementToScroll(element); - - const rect = element.getBoundingClientRect(); - const isInViewportWithOffset = - rect.top >= -offset && rect.bottom <= window.innerHeight + offset; - - if (!isInView || !isInViewportWithOffset) { - element.scrollIntoView({ behavior: "smooth", block: "center" }); - } - } - } - } - }, [currentStep, currentTourSteps, isInView, offset, isOnbordaVisible]); - - useEffect(() => { - if (elementToScroll && !isInView && isOnbordaVisible) { - console.log("Onborda: Element to Scroll Changed"); - const rect = elementToScroll.getBoundingClientRect(); - const isAbove = rect.top < 0; - elementToScroll.scrollIntoView({ - behavior: "smooth", - block: isAbove ? "center" : "center", - inline: "center", - }); + children, + shadowRgb = "0, 0, 0", + shadowOpacity = "0.2", + cardTransition = {ease: "anticipate", duration: 0.6}, + cardComponent: CardComponent, + tourComponent: TourComponent, + debug = false, + observerTimeout = 5000, +}: OnbordaProps) => { + const {currentTour, currentStep, setCurrentStep, isOnbordaVisible, currentTourSteps, completedSteps, setCompletedSteps, tours, closeOnborda} = useOnborda(); + + const [elementToScroll, setElementToScroll] = useState(null); + const [pointerPosition, setPointerPosition] = useState<{ + x: number; + y: number; + width: number; + height: number; + } | null>(null); + const currentElementRef = useRef(null); + const [currentRoute, setCurrentRoute] = useState(null); + const [pendingRouteChange, setPendingRouteChange] = useState(false); + + const hasSelector = (step: Step): boolean => { + return !!step?.selector || !!step?.customQuerySelector; } - }, [elementToScroll, isInView, isOnbordaVisible]); - - // - - - // Update pointer position on window resize - const updatePointerPosition = () => { - if (currentTourSteps) { - const step = currentTourSteps[currentStep]; - if (step) { - const element = document.querySelector(step.selector) as Element | null; - if (element) { - setPointerPosition(getElementPosition(element)); - } - } - } - }; - - // - - - // Update pointer position on window resize - useEffect(() => { - if (isOnbordaVisible) { - window.addEventListener("resize", updatePointerPosition); - return () => window.removeEventListener("resize", updatePointerPosition); + const getStepSelectorElement = (step: Step): Element | null => { + return step?.selector ? document.querySelector(step.selector) : step?.customQuerySelector ? step.customQuerySelector() : null; } - }, [currentStep, currentTourSteps, isOnbordaVisible]); - - // - - - // Step Controls - const nextStep = async () => { - if (currentTourSteps && currentStep < currentTourSteps.length - 1) { - try { - const nextStepIndex = currentStep + 1; - const route = currentTourSteps[currentStep].nextRoute; - - if (route) { - await router.push(route); - - const targetSelector = currentTourSteps[nextStepIndex].selector; - // Use MutationObserver to detect when the target element is available in the DOM - const observer = new MutationObserver((mutations, observer) => { - const element = document.querySelector(targetSelector); - if (element) { - // Once the element is found, update the step and scroll to the element - setCurrentStep(nextStepIndex); - scrollToElement(nextStepIndex); + // Get the current tour object + const currentTourObject = useMemo(() => { + return tours.find((tour) => tour.tour === currentTour); + }, [currentTour, isOnbordaVisible]); + + // - - + // Route Changes + const router = useRouter(); + const path = usePathname(); + + // Update the current route on route changes + useEffect(() => { + setCurrentRoute(path); + },[path]) + + // - - + // Initialisze + useEffect(() => { + let cleanup : any[] = []; + if (isOnbordaVisible && currentTourSteps) { + debug && console.log("Onborda: Current Step Changed", currentStep); + const step = currentTourSteps[currentStep]; + if (step) { + let elementFound = false; + // Check if the step has a selector + if (hasSelector(step)) { + // This step has a selector. Lets find the element + const element = getStepSelectorElement(step) + // Check if the element is found + if (element) { + // Once the element is found, update the step and scroll to the element + setPointerPosition(getElementPosition(element)); + setElementToScroll(element); + currentElementRef.current = element; + + // Function to mark the step as completed if the conditions are met + const handleInteraction = () => { + const isComplete = step?.isCompleteConditions?.(element) ?? true; + + debug && console.log("Onborda: Step Interaction", step, isComplete); + + // Check if the step is complete based on the conditions, and not already marked as completed + if (isComplete && !Array.from(completedSteps).includes(currentStep)) { + debug && console.log("Onborda: Step Completed", step); + setCompletedSteps((prev) => { + return prev.add(currentStep); + }); + // If callback is provided, call it + step?.onComplete && step.onComplete(); + + } // Check if the step is incomplete based on the conditions, and already marked as completed + else if (!isComplete && Array.from(completedSteps).includes(currentStep)) { + debug && console.log("Onborda: Step Incomplete", step); + setCompletedSteps((prev) => { + prev.delete(currentStep); + return prev; + }); + } + } + + // Initial check + handleInteraction(); + + + // Enable pointer events on the element + if (step.interactable) { + const htmlElement = element as HTMLElement; + htmlElement.style.pointerEvents = "auto"; + + // Add event listeners if the step is interactable and has conditions + if (step?.isCompleteConditions) { + element.addEventListener("click", handleInteraction); + element.addEventListener("input", handleInteraction); + element.addEventListener("change", handleInteraction); + debug && console.log("Onborda: Added event listeners for element", element); + + cleanup.push(() => { + // Cleanup the event listeners + element.removeEventListener("click", handleInteraction); + element.removeEventListener("input", handleInteraction); + element.removeEventListener("change", handleInteraction); + debug && console.log("Onborda: Removed event listeners for element", element); + }); + } + } + elementFound = true; + } + // Even if the element is already found, we still need to check if the route is different from the current route + // do we have a route to navigate to? + if (step.route) { + // Check if the route is set and different from the current route + if (currentRoute == null || !currentRoute?.endsWith(step.route)) { + debug && console.log("Onborda: Navigating to route", step.route); + // Trigger the next route + router.push(step.route); + + // Use MutationObserver to detect when the target element is available in the DOM + const observer = new MutationObserver((mutations, observer) => { + const shouldSelect = hasSelector(currentTourSteps[currentStep]); + if (shouldSelect) { + const element = getStepSelectorElement(currentTourSteps[currentStep]); + if (element) { + // Once the element is found, update the step and scroll to the element + setPointerPosition(getElementPosition(element)); + setElementToScroll(element); + currentElementRef.current = element; + + const handleInteraction = () => { + const isComplete = step?.isCompleteConditions?.(element) ?? true; + + debug && console.log("Onborda: Step Interaction", step, isComplete); + + // Check if the step is complete based on the conditions, and not already marked as completed + if (isComplete && !Array.from(completedSteps).includes(currentStep)) { + debug && console.log("Onborda: Step Completed", step); + setCompletedSteps((prev) => { + return prev.add(currentStep); + }); + // If callback is provided, call it + step?.onComplete && step.onComplete(); + + } // Check if the step is incomplete based on the conditions, and already marked as completed + else if (!isComplete && Array.from(completedSteps).includes(currentStep)) { + debug && console.log("Onborda: Step Incomplete", step); + setCompletedSteps((prev) => { + prev.delete(currentStep); + return prev; + }); + } + } + + // Initial check + handleInteraction(); + + + // Enable pointer events on the element + if (step.interactable) { + const htmlElement = element as HTMLElement; + htmlElement.style.pointerEvents = "auto"; + } + + // Stop observing after the element is found + observer.disconnect(); + debug && console.log("Onborda: Observer disconnected after element found", element); + } else { + debug && console.log("Onborda: Observing for element...", currentTourSteps[currentStep]); + } + } else { + setCurrentStep(currentStep); + observer.disconnect(); + debug && console.log("Onborda: Observer disconnected after no selector set", currentTourSteps[currentStep]); + } + }); + + // Start observing the document body for changes + observer.observe(document.body, { + childList: true, + subtree: true, + }); + + setPendingRouteChange(true); + + // Set a timeout to disconnect the observer if the element is not found within a certain period + const timeoutId = setTimeout(() => { + observer.disconnect(); + console.error("Onborda: Observer Timeout", currentTourSteps[currentStep]); + }, observerTimeout); // Adjust the timeout period as needed + + // Clear the timeout if the observer disconnects successfully + const originalDisconnect = observer.disconnect.bind(observer); + observer.disconnect = () => { + setPendingRouteChange(false); + clearTimeout(timeoutId); + originalDisconnect(); + }; + } + } + }else { + // no selector, but might still need to navigate to a route + if (step.route) { + // Check if the route is set and different from the current route + if (currentRoute == null || !currentRoute?.endsWith(step.route)) { + debug && console.log("Onborda: Navigating to route", step.route); + // Trigger the next route + router.push(step.route); + }else { + // Mark the step as completed + step?.onComplete && step.onComplete(); + setCompletedSteps((prev) => { + return prev.add(currentStep); + }); + } + }else { + // Mark the step as completed + step?.onComplete && step.onComplete(); + setCompletedSteps((prev) => { + return prev.add(currentStep); + }); + } - // Stop observing after the element is found - observer.disconnect(); + } + + // No element set for this step? Place the pointer at the center of the screen + if (!elementFound) { + setPointerPosition({ + x: window.innerWidth / 2, + y: window.innerHeight / 2, + width: 0, + height: 0, + }); + setElementToScroll(null); + currentElementRef.current = null; + } + + // Prefetch the next route + const nextStep = currentTourSteps[currentStep + 1]; + if (nextStep && nextStep?.route) { + debug && console.log("Onborda: Prefetching Next Route", nextStep.route); + router.prefetch(nextStep.route); + } } - }); - - // Start observing the document body for changes - observer.observe(document.body, { - childList: true, - subtree: true, - }); - } else { - setCurrentStep(nextStepIndex); - scrollToElement(nextStepIndex); } - } catch (error) { - console.error("Error navigating to next route", error); - } - } - }; + return () => { + // Disable pointer events on the element on cleanup + if (currentElementRef.current) { + const htmlElement = currentElementRef.current as HTMLElement; + htmlElement.style.pointerEvents = ""; + } + // Cleanup any event listeners we may have added + cleanup.forEach(fn => fn()); + } + }, [ + currentStep, // Re-run the effect when the current step changes + currentTourSteps, // Re-run the effect when the current tour steps change + isOnbordaVisible, // Re-run the effect when the onborda visibility changes + currentRoute, // Re-run the effect when the current route changes + ]); + + // - - + // Helper function to get element position + const getElementPosition = (element: Element) => { + const {top, left, width, height} = element.getBoundingClientRect(); + const scrollTop = window.scrollY || document.documentElement.scrollTop; + const scrollLeft = window.scrollX || document.documentElement.scrollLeft; + return { + x: left + scrollLeft, + y: top + scrollTop, + width, + height, + }; + }; - const prevStep = async () => { - if (currentTourSteps && currentStep > 0) { - try { - const prevStepIndex = currentStep - 1; - const route = currentTourSteps[currentStep].prevRoute; + // - - + // Scroll to the element when the elementToScroll changes + useEffect(() => { + if (elementToScroll && isOnbordaVisible) { + debug && console.log("Onborda: Element to Scroll Changed"); + const rect = elementToScroll.getBoundingClientRect(); + const isAbove = rect.top < 0; + elementToScroll.scrollIntoView({ + behavior: "smooth", + block: isAbove ? "center" : "center", + inline: "center", + }); + } + }, [elementToScroll, isOnbordaVisible]); + + // - - + // Update pointer position on window resize + const updatePointerPosition = () => { + if (currentTourSteps) { + const step = currentTourSteps[currentStep]; + if (step) { + const element = getStepSelectorElement(step); + if (element) { + setPointerPosition(getElementPosition(element)); + } else { + // if the element is not found, place the pointer at the center of the screen + setPointerPosition({ + x: window.innerWidth / 2, + y: window.innerHeight / 2, + width: 0, + height: 0, + }); + setElementToScroll(null); + currentElementRef.current = null; + } + } + } + }; - if (route) { - await router.push(route); + // - - + // Update pointer position on window resize + useEffect(() => { + if (isOnbordaVisible) { + window.addEventListener("resize", updatePointerPosition); + return () => window.removeEventListener("resize", updatePointerPosition); + } + }, [currentStep, currentTourSteps, isOnbordaVisible]); - const targetSelector = currentTourSteps[prevStepIndex].selector; + // - - + // Step Controls + const nextStep = async () => { + const nextStepIndex = currentStep + 1; + await setStep(nextStepIndex); + }; - // Use MutationObserver to detect when the target element is available in the DOM - const observer = new MutationObserver((mutations, observer) => { - const element = document.querySelector(targetSelector); - if (element) { - // Once the element is found, update the step and scroll to the element - setCurrentStep(prevStepIndex); - scrollToElement(prevStepIndex); + const prevStep = async () => { + const prevStepIndex = currentStep - 1; + await setStep(prevStepIndex); + }; - // Stop observing after the element is found - observer.disconnect(); - } - }); - - // Start observing the document body for changes - observer.observe(document.body, { - childList: true, - subtree: true, - }); - } else { - setCurrentStep(prevStepIndex); - scrollToElement(prevStepIndex); - } - } catch (error) { - console.error("Error navigating to previous route", error); - } - } - }; - - // - - - // Scroll to the correct element when the step changes - const scrollToElement = (stepIndex: number) => { - if (currentTourSteps) { - const element = document.querySelector( - currentTourSteps[stepIndex].selector - ) as Element | null; - if (element) { - const { top } = element.getBoundingClientRect(); - const isInViewport = - top >= -offset && top <= window.innerHeight + offset; - if (!isInViewport) { - element.scrollIntoView({ behavior: "smooth", block: "center" }); - } - // Update pointer position after scrolling - setPointerPosition(getElementPosition(element)); - } + const setStep = async (step: number | string) => { + const setStepIndex = typeof step === 'string' ? currentTourSteps.findIndex((s) => s?.id === step) : step; + setCurrentStep(setStepIndex); } - }; - // - - - // Card Side - const getCardStyle = (side: string) => { - switch (side) { - case "top": - return { - transform: `translate(-50%, 0)`, - left: "50%", - bottom: "100%", - marginBottom: "25px", - }; - case "bottom": - return { - transform: `translate(-50%, 0)`, - left: "50%", - top: "100%", - marginTop: "25px", - }; - case "left": - return { - transform: `translate(0, -50%)`, - right: "100%", - top: "50%", - marginRight: "25px", - }; - case "right": - return { - transform: `translate(0, -50%)`, - left: "100%", - top: "50%", - marginLeft: "25px", - }; - case "top-left": - return { - bottom: "100%", - marginBottom: "25px", - }; - case "top-right": - return { - right: 0, - bottom: "100%", - marginBottom: "25px", - }; - case "bottom-left": - return { - top: "100%", - marginTop: "25px", - }; - case "bottom-right": - return { - right: 0, - top: "100%", - marginTop: "25px", - }; - case "right-bottom": - return { - left: "100%", - bottom: 0, - marginLeft: "25px", - }; - case "right-top": - return { - left: "100%", - top: 0, - marginLeft: "25px", - }; - case "left-bottom": - return { - right: "100%", - bottom: 0, - marginRight: "25px", - }; - case "left-top": - return { - right: "100%", - top: 0, - marginRight: "25px", - }; - default: - return {}; // Default case if no side is specified - } - }; - // - - - // Arrow position based on card side - const getArrowStyle = (side: string) => { - switch (side) { - case "bottom": - return { - transform: `translate(-50%, 0) rotate(270deg)`, - left: "50%", - top: "-23px", - }; - case "top": - return { - transform: `translate(-50%, 0) rotate(90deg)`, - left: "50%", - bottom: "-23px", - }; - case "right": - return { - transform: `translate(0, -50%) rotate(180deg)`, - top: "50%", - left: "-23px", - }; - case "left": - return { - transform: `translate(0, -50%) rotate(0deg)`, - top: "50%", - right: "-23px", - }; - case "top-left": - return { - transform: `rotate(90deg)`, - left: "10px", - bottom: "-23px", - }; - case "top-right": - return { - transform: `rotate(90deg)`, - right: "10px", - bottom: "-23px", - }; - case "bottom-left": - return { - transform: `rotate(270deg)`, - left: "10px", - top: "-23px", - }; - case "bottom-right": - return { - transform: `rotate(270deg)`, - right: "10px", - top: "-23px", - }; - case "right-bottom": - return { - transform: `rotate(180deg)`, - left: "-23px", - bottom: "10px", - }; - case "right-top": - return { - transform: `rotate(180deg)`, - left: "-23px", - top: "10px", - }; - case "left-bottom": - return { - transform: `rotate(0deg)`, - right: "-23px", - bottom: "10px", - }; - case "left-top": - return { - transform: `rotate(0deg)`, - right: "-23px", - top: "10px", - }; - default: - return {}; // Default case if no side is specified - } - }; - - // - - - // Card Arrow - const CardArrow = () => { - return ( - - - - ); - }; - - // - - - // Overlay Variants - const variants = { - visible: { opacity: 1 }, - hidden: { opacity: 0 }, - }; - - // - - - // Pointer Options - const pointerPadding = currentTourSteps?.[currentStep]?.pointerPadding ?? 30; - const pointerPadOffset = pointerPadding / 2; - const pointerRadius = currentTourSteps?.[currentStep]?.pointerRadius ?? 28; - - return ( -
- {/* Container for the Website content */} -
- {children} -
- - {/* Onborda Overlay Step Content */} - {pointerPosition && isOnbordaVisible && CardComponent && ( - - - { + if (!isVisible) { + return null; + } + return ( + - {/* Card */} -
+ + ); + }; + + // - - + // Overlay Variants + const variants = { + visible: {opacity: 1}, + hidden: {opacity: 0}, + }; + + // - - + // Pointer Options + const pointerPadding = currentTourSteps?.[currentStep]?.pointerPadding ?? 30; + const pointerPadOffset = pointerPadding / 2; + const pointerRadius = currentTourSteps?.[currentStep]?.pointerRadius ?? 28; + const pointerEvents = pointerPosition && isOnbordaVisible ? 'pointer-events-none' : ''; + + return (<> + + {/* Container for the Website content */} +
+ {children} +
+ + {/* Onborda Tour */} + {/*
*/} + {/* {TourComponent && currentTourSteps && (*/} + {/*
*/} + {/* */} + {/*
*/} + {/* )}*/} + {/*
*/} + + {/* Onborda Overlay Step Content */} + {pointerPosition && isOnbordaVisible && CardComponent && ( + + + + {/* Card */} +
+ } + completedSteps={Array.from(completedSteps)} + pendingRouteChange={pendingRouteChange} + /> +
+
+
+ {TourComponent && currentTourObject !== undefined && ( + + + + + )} - > - } - /> -
-
-
-
- )} -
- ); + + )} + ); }; export default Onborda; diff --git a/src/OnbordaContext.tsx b/src/OnbordaContext.tsx index c3c48c7..ead1a83 100644 --- a/src/OnbordaContext.tsx +++ b/src/OnbordaContext.tsx @@ -1,8 +1,8 @@ "use client"; -import React, { createContext, useContext, useState, useCallback } from "react"; +import React, {createContext, useContext, useState, useCallback, useEffect} from "react"; // Types -import { OnbordaContextType } from "./types"; +import {OnbordaContextType, OnbordaProviderProps, Step, Tour} from "./types"; // Example Hooks Usage: // const { setCurrentStep, closeOnborda, startOnborda } = useOnborda(); @@ -24,14 +24,33 @@ const useOnborda = () => { return context; }; -const OnbordaProvider: React.FC<{ children: React.ReactNode }> = ({ +const OnbordaProvider: React.FC = ({ children, + tours = [], + activeTour = null, }) => { - const [currentTour, setCurrentTour] = useState(null); + const [currentTour, setCurrentTourState] = useState(null); const [currentStep, setCurrentStepState] = useState(0); const [isOnbordaVisible, setOnbordaVisible] = useState(false); + const [currentTourSteps, setCurrentTourStepsState] = useState([]); + const [completedSteps, setCompletedSteps] = useState>(new Set()); - const setCurrentStep = useCallback((step: number, delay?: number) => { + // Start the active tour on mount + useEffect(() => { + if (activeTour) { + startOnborda(activeTour); + } + }, [activeTour]); + + const setCurrentStep = useCallback((step: number | string, delay?: number) => { + // If step is a string, find the index of the step with that id + if (typeof step === 'string') { + const index = currentTourSteps.findIndex((s) => s?.id === step); + if (index === -1) { + throw new Error(`Step with id ${step} not found`); + } + step = index; + } if (delay) { setTimeout(() => { setCurrentStepState(step); @@ -44,25 +63,66 @@ const OnbordaProvider: React.FC<{ children: React.ReactNode }> = ({ }, []); const closeOnborda = useCallback(() => { + // If all steps are completed, call the onComplete function + if (completedSteps.size === currentTourSteps.length) { + tours.find((tour) => (tour.tour) === currentTour)?.onComplete?.(); + } setOnbordaVisible(false); - setCurrentTour(null); - }, []); - - const startOnborda = useCallback((tourName: string) => { - setCurrentTour(tourName); + setCurrentTourState(null); + setCurrentTourStepsState([]); setCurrentStepState(0); - setOnbordaVisible(true); - }, []); + setCompletedSteps(new Set()); + }, [currentTour, currentTourSteps, completedSteps]); + + + const initializeCompletedSteps = useCallback(async (tour: Tour) => { + // Get the initial state of the completed steps + const completeSteps = tour?.initialCompletedStepsState && await tour.initialCompletedStepsState() || tour.steps.map(() => false); + const firstIncomplete = completeSteps.findIndex((result) => !result); + const completed = completeSteps.reduce<(number)[]>((acc, result, index) => { + if (result) { + acc.push(index); + } + return acc; + }, []); + setCompletedSteps(new Set(completed)); + // If all steps are completed, return the last step + return firstIncomplete === -1 ? tour.steps.length - 1 : firstIncomplete; + + },[currentTour]); + + const setCurrentTour = useCallback((tourName: string | null) => { + if (!tourName) { + closeOnborda(); + return + } + setCurrentTourState(tourName); + const tour = tours.find((tour) => tour.tour === tourName) + setCurrentTourStepsState(tour?.steps || []); + tour && initializeCompletedSteps(tour).then(r => { + setCurrentStep(r); + setOnbordaVisible(true); + }); + }, [tours]); + + const startOnborda = useCallback((tourName: string) => { + setCurrentTour(tourName); + }, [setCurrentTour]); + return ( {children} diff --git a/src/OnbordaStyles.tsx b/src/OnbordaStyles.tsx new file mode 100644 index 0000000..757128c --- /dev/null +++ b/src/OnbordaStyles.tsx @@ -0,0 +1,168 @@ +import React from "react"; + +export const getCardStyle: (side: string) => React.CSSProperties = (side: string) => { + switch (side) { + case "top": + return { + transform: `translate(-50%, 0)`, + left: "50%", + bottom: "100%", + marginBottom: "25px", + }; + case "bottom": + return { + transform: `translate(-50%, 0)`, + left: "50%", + top: "100%", + marginTop: "25px", + }; + case "left": + return { + transform: `translate(0, -50%)`, + right: "100%", + top: "50%", + marginRight: "25px", + }; + case "right": + return { + transform: `translate(0, -50%)`, + left: "100%", + top: "50%", + marginLeft: "25px", + }; + case "top-left": + return { + bottom: "100%", + marginBottom: "25px", + }; + case "top-right": + return { + right: 0, + bottom: "100%", + marginBottom: "25px", + }; + case "bottom-left": + return { + top: "100%", + marginTop: "25px", + }; + case "bottom-right": + return { + right: 0, + top: "100%", + marginTop: "25px", + }; + case "right-bottom": + return { + left: "100%", + bottom: 0, + marginLeft: "25px", + }; + case "right-top": + return { + left: "100%", + top: 0, + marginLeft: "25px", + }; + case "left-bottom": + return { + right: "100%", + bottom: 0, + marginRight: "25px", + }; + case "left-top": + return { + right: "100%", + top: 0, + marginRight: "25px", + }; + default: + // Default case if no side is specified. Center the card to the screen + return { + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', // Center the card + position: 'fixed', // Make sure it's positioned relative to the viewport + margin: '0', + }; + } +} + +export const getArrowStyle: (side: string) => React.CSSProperties = (side: string) => { + switch (side) { + case "bottom": + return { + transform: `translate(-50%, 0) rotate(270deg)`, + left: "50%", + top: "-23px", + }; + case "top": + return { + transform: `translate(-50%, 0) rotate(90deg)`, + left: "50%", + bottom: "-23px", + }; + case "right": + return { + transform: `translate(0, -50%) rotate(180deg)`, + top: "50%", + left: "-23px", + }; + case "left": + return { + transform: `translate(0, -50%) rotate(0deg)`, + top: "50%", + right: "-23px", + }; + case "top-left": + return { + transform: `rotate(90deg)`, + left: "10px", + bottom: "-23px", + }; + case "top-right": + return { + transform: `rotate(90deg)`, + right: "10px", + bottom: "-23px", + }; + case "bottom-left": + return { + transform: `rotate(270deg)`, + left: "10px", + top: "-23px", + }; + case "bottom-right": + return { + transform: `rotate(270deg)`, + right: "10px", + top: "-23px", + }; + case "right-bottom": + return { + transform: `rotate(180deg)`, + left: "-23px", + bottom: "10px", + }; + case "right-top": + return { + transform: `rotate(180deg)`, + left: "-23px", + top: "10px", + }; + case "left-bottom": + return { + transform: `rotate(0deg)`, + right: "-23px", + bottom: "10px", + }; + case "left-top": + return { + transform: `rotate(0deg)`, + right: "-23px", + top: "10px", + }; + default: + return {}; // Default case if no side is specified + } +}; diff --git a/src/types/index.ts b/src/types/index.ts index 3d82486..2cf0489 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,56 +1,189 @@ import { Transition } from "framer-motion"; +// Provider +export interface OnbordaProviderProps { + /** The children elements to be rendered inside the OnbordaProvider component */ + children: React.ReactNode; + /** An array of tours, each containing multiple steps */ + tours: Tour[]; + /** Active Tour */ + activeTour?: string; +} + // Context export interface OnbordaContextType { - currentStep: number; - currentTour: string | null; - setCurrentStep: (step: number, delay?: number) => void; - closeOnborda: () => void; - startOnborda: (tourName: string) => void; - isOnbordaVisible: boolean; + /** array of tours */ + tours: Tour[]; + /** current step index */ + currentStep: number; + /** current tour name */ + currentTour: string | null; + /** current tour steps */ + currentTourSteps: Step[]; + /** function to set the current step */ + setCurrentStep: (step: number | string, delay?: number) => void; + /** function to close Onborda */ + closeOnborda: () => void; + /** function to start Onborda */ + startOnborda: (tourName: string) => void; + /** flag to check if Onborda is visible */ + isOnbordaVisible: boolean; + /** default completed steps */ + completedSteps: Set; + /** setstate function to set the completed steps */ + setCompletedSteps: React.Dispatch>>; } // Step export interface Step { - // Step Content - icon: React.ReactNode | string | null; - title: string; - content: React.ReactNode; - selector: string; - // Options - side?: "top" | "bottom" | "left" | "right" | "top-left" | "top-right" | "bottom-left" | "bottom-right" | "left-top" | "left-bottom" | "right-top" | "right-bottom"; - showControls?: boolean; - pointerPadding?: number; - pointerRadius?: number; - // Routing - nextRoute?: string; - prevRoute?: string; + // Step Content + /** The unique identifier for the step */ + id?: string; + /** The title of the step */ + title?: string; + /** The content to be displayed in the step */ + content: React.ReactNode; + /** The icon to be displayed in the step */ + icon?: React.ReactNode | string | null; + /** The CSS selector for the element to highlight. Takes precedence over customQuerySelector if both are provided. */ + selector?: string; + /** A custom function to query the target element. Ignored if selector is provided. */ + customQuerySelector?: () => Element | null; + + // Options + /** The side where the step should be displayed */ + side?: "top" | "bottom" | "left" | "right" | "top-left" | "top-right" | "bottom-left" | "bottom-right" | "left-top" | "left-bottom" | "right-top" | "right-bottom"; + /** Flag to show or hide the controls */ + showControls?: boolean; + /** Padding around the pointer */ + pointerPadding?: number; + /** Radius of the pointer */ + pointerRadius?: number; + /** Flag to make the step interactable */ + interactable?: boolean; + /** Conditions to be met before the next step can be triggered. Function is bound to event listeners on the target element on 'input', 'change' and 'click' events. */ + isCompleteConditions?: (element: Element | null) => boolean; + + // Routing + /** The route for this step */ + route?: string; + /** The route to navigate to for the next step */ + /** @deprecated Use `route` instead */ + nextRoute?: string; + /** The route to navigate to for the previous step */ + /** @deprecated Use `route` instead */ + prevRoute?: string; + + // Callbacks + /** Callback function to be called when the step is completed */ + onComplete?: () => Promise; + + /** Any additional data for custom use */ + [key: string]: any; + } // Tour // export interface Tour { - tour: string; - steps: Step[]; + /** The tour ID */ + tour: string; + /** Tour Title */ + title?: string; + /** Tour Description */ + description?: string; + /** An array of steps in the tour */ + steps: Step[]; + /** Complete Callback */ + onComplete?: () => void; + /** Tour can be dismissed. */ + dismissible?: boolean; + /** Any additional data for custom use */ + [key: string]: any; + /** Initial completed steps state of the tour. an async function called on tour started. Can be a Server Action e.g. Promise.all([]) on API calls. */ + initialCompletedStepsState?: () => Promise; } // Onborda +// export interface OnbordaProps { - children: React.ReactNode; - steps: Tour[]; - showOnborda?: boolean; - shadowRgb?: string; - shadowOpacity?: string; - cardTransition?: Transition; - cardComponent?: React.ComponentType; + /** The children elements to be rendered inside the Onborda component */ + children: React.ReactNode; + + /** An array of tours, each containing multiple steps */ + /** @deprecated Use `OnbordaProvider.tours` instead */ + steps?: Tour[]; + + /** Flag to show or hide the Onborda component */ + showOnborda?: boolean; + + /** RGB value for the shadow color */ + shadowRgb?: string; + + /** Opacity value for the shadow */ + shadowOpacity?: string; + + /** Transition settings for the card component */ + cardTransition?: Transition; + + /** Custom card component to be used in the Onborda */ + cardComponent?: React.ComponentType; + + /** Custom tour component to be used in the Onborda */ + tourComponent?: React.ComponentType; + + /** Flag to enable or disable debug mode */ + debug?: boolean; + + /** Timeout value for the observer when observing for the target element */ + observerTimeout?: number; } // Custom Card export interface CardComponentProps { - step: Step; - currentStep: number; - totalSteps: number; - nextStep: () => void; - prevStep: () => void; - arrow: JSX.Element; + /** The current step object containing details of the step */ + step: Step; + + /** The index of the current step */ + currentStep: number; + + /** The total number of steps in the tour */ + totalSteps: number; + + /** Function to set the current step by step index or step.id */ + setStep: (step: number | string) => void; + + /** Function to navigate to the next step */ + nextStep: () => void; + + /** Function to navigate to the previous step */ + prevStep: () => void; + + /** Function to close the Onborda */ + closeOnborda: () => void; + + /** The arrow element to be displayed in the card */ + arrow: JSX.Element; + + /** Array of completed steps */ + completedSteps: (string|number)[]; + + /** Is waiting for Route change */ + pendingRouteChange: boolean; +} + +// Tour Component +export interface TourComponentProps { + /** The current tour name */ + currentTour: string | null; + /** The index of the current step */ + currentStep: number; + /** The current Tour object containing details of the tour */ + tour: Tour; + /** Function to set the current step by step index or step.id */ + setStep: (step: number | string) => void; + /** Array of completed steps */ + completedSteps: (string|number)[]; + /** Function to close the Onborda */ + closeOnborda: () => void; }