├── .editorconfig ├── .gitattributes ├── .gitignore ├── .pnp.cjs ├── README.md ├── dist ├── Onborda.d.ts ├── Onborda.js ├── OnbordaContext.d.ts ├── OnbordaContext.js ├── index.d.ts ├── index.js └── types │ ├── index.d.ts │ └── index.js ├── package.json ├── pnpm-lock.yaml ├── src ├── Onborda.tsx ├── OnbordaContext.tsx ├── index.ts └── types │ └── index.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.{js,json,yml}] 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 2 11 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | /.yarn/** linguist-vendored 2 | /.yarn/releases/* binary 3 | /.yarn/plugins/**/* binary 4 | /.pnp.* binary linguist-generated 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # misc 10 | .DS_Store 11 | *.pem 12 | 13 | # debug 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | 18 | # vercel 19 | .vercel -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Onborda - Next.js onboarding flow 2 | Onborda is a lightweight onboarding flow that utilises [framer-motion](https://www.framer.com/motion/) for animations and [tailwindcss](https://tailwindcss.com/) for styling. Fully customisable pointers (tooltips) that can easily be used with [shadcn/ui](https://ui.shadcn.com/) for modern web applications. 3 | 4 | - **Demo - [onborda.vercel.app](https://onborda.vercel.app)** 5 | - **[Demo repository](https://github.com/uixmat/onborda-demo)** 6 | 7 | 8 | ## Getting started 9 | ```bash 10 | # npm 11 | npm i onborda 12 | # pnpm 13 | pnpm add onborda 14 | # yarn 15 | yarn add onborda 16 | ``` 17 | 18 | ### Global `layout.tsx` 19 | ```tsx 20 | 21 | 22 | {children} 23 | 24 | 25 | ``` 26 | 27 | ### Components & `page.tsx` 28 | Target anything in your app using the elements `id` attribute. 29 | ```tsx 30 |
Onboard Step
31 | ``` 32 | 33 | ### Tailwind config 34 | Tailwind CSS will need to scan the node module in order to include the classes used. See [configuring source paths](https://tailwindcss.com/docs/content-configuration#configuring-source-paths) for more information about this topic. 35 | 36 | > **Note** _You only require this if you're **not using** a custom component. 37 | 38 | ```ts 39 | const config: Config = { 40 | content: [ 41 | './node_modules/onborda/dist/**/*.{js,ts,jsx,tsx}' // Add this 42 | ] 43 | } 44 | ``` 45 | 46 | ### Custom Card 47 | If you require greater control over the card design or simply wish to create a totally custom component then you can do so easily. 48 | 49 | | Prop | Type | Description | 50 | |---------------|------------------|----------------------------------------------------------------------| 51 | | `step` | `Object` | The current `Step` object from your steps array, including content, title, etc. | 52 | | `currentStep` | `number` | The index of the current step in the steps array. | 53 | | `totalSteps` | `number` | The total number of steps in the onboarding process. | 54 | | `nextStep` | | A function to advance to the next step in the onboarding process. | 55 | | `prevStep` | | A function to go back to the previous step in the onboarding process.| 56 | | `arrow` | | Returns an SVG object, the orientation is controlled by the steps side prop | 57 | 58 | ```tsx 59 | "use client" 60 | import type { CardComponentProps } from "onborda"; 61 | 62 | export const CustomCard = ({ 63 | step, 64 | currentStep, 65 | totalSteps, 66 | nextStep, 67 | prevStep, 68 | arrow, 69 | }: CardComponentProps) => { 70 | return ( 71 |
72 |

{step.icon} {step.title}

73 |

{currentStep} of {totalSteps}

74 |

{step.content}

75 | 76 | 77 | {arrow} 78 |
79 | ) 80 | } 81 | ``` 82 | 83 | ### Steps object 84 | 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! 85 | 86 | ```tsx 87 | { 88 | tour: "firstyour", 89 | steps: [ 90 | Step 91 | ], 92 | tour: "secondtour", 93 | steps: [ 94 | Step 95 | ] 96 | } 97 | ``` 98 | 99 | ### Step object 100 | 101 | | Prop | Type | Description | 102 | |----------------|-------------------------------|---------------------------------------------------------------------------------------| 103 | | `icon` | `React.ReactNode`, `string`, `null` | Optional. An icon or element to display alongside the step title. | 104 | | `title` | `string` | The title of your step | 105 | | `content` | `React.ReactNode` | The main content or body of the step. | 106 | | `selector` | `string` | A string used to target an `id` that this step refers to. | 107 | | `side` | `"top"`, `"bottom"`, `"left"`, `"right"` | Optional. Determines where the tooltip should appear relative to the selector. | 108 | | `showControls` | `boolean` | Optional. Determines whether control buttons (next, prev) should be shown if using the default card. | 109 | | `pointerPadding` | `number` | Optional. The padding around the pointer (keyhole) highlighting the target element. | 110 | | `pointerRadius` | `number` | Optional. The border-radius of the pointer (keyhole) highlighting the target element. | 111 | | `nextRoute` | `string` | Optional. The route to navigate to using `next/navigation` when moving to the next step. | 112 | | `prevRoute` | `string` | Optional. The route to navigate to using `next/navigation` when moving to the previous step. | 113 | 114 | > **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._ 115 | 116 | ### Example `steps` 117 | 118 | ```tsx 119 | { 120 | tour: "firsttour", 121 | steps: [ 122 | { 123 | icon: <>👋, 124 | title: "Tour 1, Step 1", 125 | content: <>First tour, first step, 126 | selector: "#tour1-step1", 127 | side: "top", 128 | showControls: true, 129 | pointerPadding: 10, 130 | pointerRadius: 10, 131 | nextRoute: "/foo", 132 | prevRoute: "/bar" 133 | } 134 | ... 135 | ], 136 | tour: "secondtour", 137 | steps: [ 138 | icon: <>👋👋, 139 | title: "Second tour, Step 1", 140 | content: <>Second tour, first step!, 141 | selector: "#onborda-step1", 142 | side: "top", 143 | showControls: true, 144 | pointerPadding: 10, 145 | pointerRadius: 10, 146 | nextRoute: "/foo", 147 | prevRoute: "/bar" 148 | ] 149 | } 150 | ``` 151 | 152 | ### Onborda Props 153 | 154 | | Property | Type | Description | 155 | |-----------------|-----------------------|---------------------------------------------------------------------------------------| 156 | | `children` | `React.ReactNode` | Your website or application content. | 157 | | `interact` | `boolean` | Optional. Controls whether the onboarding overlay should be interactive. Defaults to `false`. | 158 | | `steps` | `Array[]` | An array of `Step` objects defining each step of the onboarding process. | 159 | | `showOnborda` | `boolean` | Optional. Controls the visibility of the onboarding overlay, eg. if the user is a first time visitor. Defaults to `false`. | 160 | | `shadowRgb` | `string` | Optional. The RGB values for the shadow color surrounding the target area. Defaults to black `"0,0,0"`. | 161 | | `shadowOpacity` | `string` | Optional. The opacity value for the shadow surrounding the target area. Defaults to `"0.2"` | 162 | | `customCard` | `React.ReactNode` | Optional. A custom card (or tooltip) that can be used to replace the default TailwindCSS card. | 163 | | `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" }}`. | 164 | 165 | 166 | ```tsx 167 | 175 | {children} 176 | 177 | ``` 178 | -------------------------------------------------------------------------------- /dist/Onborda.d.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { OnbordaProps } from "./types"; 3 | declare const Onborda: React.FC; 4 | export default Onborda; 5 | -------------------------------------------------------------------------------- /dist/Onborda.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; 3 | import { useState, useEffect, useRef } from "react"; 4 | import { useOnborda } from "./OnbordaContext"; 5 | import { motion, useInView } from "framer-motion"; 6 | import { useRouter } from "next/navigation"; 7 | import { Portal } from "@radix-ui/react-portal"; 8 | const Onborda = ({ children, interact = false, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2", cardTransition = { ease: "anticipate", duration: 0.6 }, cardComponent: CardComponent, }) => { 9 | const { currentTour, currentStep, setCurrentStep, isOnbordaVisible } = useOnborda(); 10 | const currentTourSteps = steps.find((tour) => tour.tour === currentTour)?.steps; 11 | const [elementToScroll, setElementToScroll] = useState(null); 12 | const [pointerPosition, setPointerPosition] = useState(null); 13 | const currentElementRef = useRef(null); 14 | const observeRef = useRef(null); // Ref for the observer element 15 | const isInView = useInView(observeRef); 16 | const offset = 20; 17 | // - - 18 | // Route Changes 19 | const router = useRouter(); 20 | // - - 21 | // Initialisze 22 | const previousElementRef = useRef(null); 23 | useEffect(() => { 24 | if (isOnbordaVisible && currentTourSteps) { 25 | // Clean up all elements that might have our styles 26 | currentTourSteps.forEach(tourStep => { 27 | const element = document.querySelector(tourStep.selector); 28 | if (element && tourStep !== currentTourSteps[currentStep]) { 29 | // Reset styles for non-active elements if interaction is enabled 30 | if (interact) { 31 | const style = element.style; 32 | style.position = ''; 33 | style.zIndex = ''; 34 | } 35 | } 36 | }); 37 | const step = currentTourSteps[currentStep]; 38 | if (step) { 39 | const element = document.querySelector(step.selector); 40 | if (element) { 41 | // Set styles for current element 42 | element.style.position = 'relative'; 43 | if (interact) { 44 | element.style.zIndex = '990'; 45 | } 46 | setPointerPosition(getElementPosition(element)); 47 | currentElementRef.current = element; 48 | setElementToScroll(element); 49 | const rect = element.getBoundingClientRect(); 50 | const isInViewportWithOffset = rect.top >= -offset && rect.bottom <= window.innerHeight + offset; 51 | if (!isInView || !isInViewportWithOffset) { 52 | element.scrollIntoView({ behavior: "smooth", block: "center" }); 53 | } 54 | } 55 | } 56 | } 57 | // Cleanup function for component unmount 58 | return () => { 59 | if (currentTourSteps) { 60 | currentTourSteps.forEach(step => { 61 | const element = document.querySelector(step.selector); 62 | if (element && interact) { 63 | element.style.position = ''; 64 | element.style.zIndex = ''; 65 | } 66 | }); 67 | } 68 | }; 69 | }, [currentStep, currentTourSteps, isInView, offset, isOnbordaVisible, interact]); 70 | // - - 71 | // Helper function to get element position 72 | const getElementPosition = (element) => { 73 | const { top, left, width, height } = element.getBoundingClientRect(); 74 | const scrollTop = window.scrollY || document.documentElement.scrollTop; 75 | const scrollLeft = window.scrollX || document.documentElement.scrollLeft; 76 | return { 77 | x: left + scrollLeft, 78 | y: top + scrollTop, 79 | width, 80 | height, 81 | }; 82 | }; 83 | // - - 84 | // Update pointerPosition when currentStep changes 85 | useEffect(() => { 86 | if (isOnbordaVisible && currentTourSteps) { 87 | console.log("Onborda: Current Step Changed"); 88 | const step = currentTourSteps[currentStep]; 89 | if (step) { 90 | const element = document.querySelector(step.selector); 91 | if (element) { 92 | setPointerPosition(getElementPosition(element)); 93 | currentElementRef.current = element; 94 | setElementToScroll(element); 95 | const rect = element.getBoundingClientRect(); 96 | const isInViewportWithOffset = rect.top >= -offset && rect.bottom <= window.innerHeight + offset; 97 | if (!isInView || !isInViewportWithOffset) { 98 | element.scrollIntoView({ behavior: "smooth", block: "center" }); 99 | } 100 | } 101 | } 102 | } 103 | }, [currentStep, currentTourSteps, isInView, offset, isOnbordaVisible]); 104 | useEffect(() => { 105 | if (elementToScroll && !isInView && isOnbordaVisible) { 106 | console.log("Onborda: Element to Scroll Changed"); 107 | const rect = elementToScroll.getBoundingClientRect(); 108 | const isAbove = rect.top < 0; 109 | elementToScroll.scrollIntoView({ 110 | behavior: "smooth", 111 | block: isAbove ? "center" : "center", 112 | inline: "center", 113 | }); 114 | } 115 | }, [elementToScroll, isInView, isOnbordaVisible]); 116 | // - - 117 | // Update pointer position on window resize 118 | const updatePointerPosition = () => { 119 | if (currentTourSteps) { 120 | const step = currentTourSteps[currentStep]; 121 | if (step) { 122 | const element = document.querySelector(step.selector); 123 | if (element) { 124 | setPointerPosition(getElementPosition(element)); 125 | } 126 | } 127 | } 128 | }; 129 | // - - 130 | // Update pointer position on window resize 131 | useEffect(() => { 132 | if (isOnbordaVisible) { 133 | window.addEventListener("resize", updatePointerPosition); 134 | return () => window.removeEventListener("resize", updatePointerPosition); 135 | } 136 | }, [currentStep, currentTourSteps, isOnbordaVisible]); 137 | // - - 138 | // Step Controls 139 | const nextStep = async () => { 140 | if (currentTourSteps && currentStep < currentTourSteps.length - 1) { 141 | try { 142 | const nextStepIndex = currentStep + 1; 143 | const route = currentTourSteps[currentStep].nextRoute; 144 | if (route) { 145 | await router.push(route); 146 | const targetSelector = currentTourSteps[nextStepIndex].selector; 147 | // Use MutationObserver to detect when the target element is available in the DOM 148 | const observer = new MutationObserver((mutations, observer) => { 149 | const element = document.querySelector(targetSelector); 150 | if (element) { 151 | // Once the element is found, update the step and scroll to the element 152 | setCurrentStep(nextStepIndex); 153 | scrollToElement(nextStepIndex); 154 | // Stop observing after the element is found 155 | observer.disconnect(); 156 | } 157 | }); 158 | // Start observing the document body for changes 159 | observer.observe(document.body, { 160 | childList: true, 161 | subtree: true, 162 | }); 163 | } 164 | else { 165 | setCurrentStep(nextStepIndex); 166 | scrollToElement(nextStepIndex); 167 | } 168 | } 169 | catch (error) { 170 | console.error("Error navigating to next route", error); 171 | } 172 | } 173 | }; 174 | const prevStep = async () => { 175 | if (currentTourSteps && currentStep > 0) { 176 | try { 177 | const prevStepIndex = currentStep - 1; 178 | const route = currentTourSteps[currentStep].prevRoute; 179 | if (route) { 180 | await router.push(route); 181 | const targetSelector = currentTourSteps[prevStepIndex].selector; 182 | // Use MutationObserver to detect when the target element is available in the DOM 183 | const observer = new MutationObserver((mutations, observer) => { 184 | const element = document.querySelector(targetSelector); 185 | if (element) { 186 | // Once the element is found, update the step and scroll to the element 187 | setCurrentStep(prevStepIndex); 188 | scrollToElement(prevStepIndex); 189 | // Stop observing after the element is found 190 | observer.disconnect(); 191 | } 192 | }); 193 | // Start observing the document body for changes 194 | observer.observe(document.body, { 195 | childList: true, 196 | subtree: true, 197 | }); 198 | } 199 | else { 200 | setCurrentStep(prevStepIndex); 201 | scrollToElement(prevStepIndex); 202 | } 203 | } 204 | catch (error) { 205 | console.error("Error navigating to previous route", error); 206 | } 207 | } 208 | }; 209 | // - - 210 | // Scroll to the correct element when the step changes 211 | const scrollToElement = (stepIndex) => { 212 | if (currentTourSteps) { 213 | const element = document.querySelector(currentTourSteps[stepIndex].selector); 214 | if (element) { 215 | const { top } = element.getBoundingClientRect(); 216 | const isInViewport = top >= -offset && top <= window.innerHeight + offset; 217 | if (!isInViewport) { 218 | element.scrollIntoView({ behavior: "smooth", block: "center" }); 219 | } 220 | // Update pointer position after scrolling 221 | setPointerPosition(getElementPosition(element)); 222 | } 223 | } 224 | }; 225 | // - - 226 | // Card Side 227 | const getCardStyle = (side) => { 228 | switch (side) { 229 | case "top": 230 | return { 231 | transform: `translate(-50%, 0)`, 232 | left: "50%", 233 | bottom: "100%", 234 | marginBottom: "25px", 235 | }; 236 | case "bottom": 237 | return { 238 | transform: `translate(-50%, 0)`, 239 | left: "50%", 240 | top: "100%", 241 | marginTop: "25px", 242 | }; 243 | case "left": 244 | return { 245 | transform: `translate(0, -50%)`, 246 | right: "100%", 247 | top: "50%", 248 | marginRight: "25px", 249 | }; 250 | case "right": 251 | return { 252 | transform: `translate(0, -50%)`, 253 | left: "100%", 254 | top: "50%", 255 | marginLeft: "25px", 256 | }; 257 | case "top-left": 258 | return { 259 | bottom: "100%", 260 | marginBottom: "25px", 261 | }; 262 | case "top-right": 263 | return { 264 | right: 0, 265 | bottom: "100%", 266 | marginBottom: "25px", 267 | }; 268 | case "bottom-left": 269 | return { 270 | top: "100%", 271 | marginTop: "25px", 272 | }; 273 | case "bottom-right": 274 | return { 275 | right: 0, 276 | top: "100%", 277 | marginTop: "25px", 278 | }; 279 | case "right-bottom": 280 | return { 281 | left: "100%", 282 | bottom: 0, 283 | marginLeft: "25px", 284 | }; 285 | case "right-top": 286 | return { 287 | left: "100%", 288 | top: 0, 289 | marginLeft: "25px", 290 | }; 291 | case "left-bottom": 292 | return { 293 | right: "100%", 294 | bottom: 0, 295 | marginRight: "25px", 296 | }; 297 | case "left-top": 298 | return { 299 | right: "100%", 300 | top: 0, 301 | marginRight: "25px", 302 | }; 303 | default: 304 | return {}; // Default case if no side is specified 305 | } 306 | }; 307 | // - - 308 | // Arrow position based on card side 309 | const getArrowStyle = (side) => { 310 | switch (side) { 311 | case "bottom": 312 | return { 313 | transform: `translate(-50%, 0) rotate(270deg)`, 314 | left: "50%", 315 | top: "-23px", 316 | }; 317 | case "top": 318 | return { 319 | transform: `translate(-50%, 0) rotate(90deg)`, 320 | left: "50%", 321 | bottom: "-23px", 322 | }; 323 | case "right": 324 | return { 325 | transform: `translate(0, -50%) rotate(180deg)`, 326 | top: "50%", 327 | left: "-23px", 328 | }; 329 | case "left": 330 | return { 331 | transform: `translate(0, -50%) rotate(0deg)`, 332 | top: "50%", 333 | right: "-23px", 334 | }; 335 | case "top-left": 336 | return { 337 | transform: `rotate(90deg)`, 338 | left: "10px", 339 | bottom: "-23px", 340 | }; 341 | case "top-right": 342 | return { 343 | transform: `rotate(90deg)`, 344 | right: "10px", 345 | bottom: "-23px", 346 | }; 347 | case "bottom-left": 348 | return { 349 | transform: `rotate(270deg)`, 350 | left: "10px", 351 | top: "-23px", 352 | }; 353 | case "bottom-right": 354 | return { 355 | transform: `rotate(270deg)`, 356 | right: "10px", 357 | top: "-23px", 358 | }; 359 | case "right-bottom": 360 | return { 361 | transform: `rotate(180deg)`, 362 | left: "-23px", 363 | bottom: "10px", 364 | }; 365 | case "right-top": 366 | return { 367 | transform: `rotate(180deg)`, 368 | left: "-23px", 369 | top: "10px", 370 | }; 371 | case "left-bottom": 372 | return { 373 | transform: `rotate(0deg)`, 374 | right: "-23px", 375 | bottom: "10px", 376 | }; 377 | case "left-top": 378 | return { 379 | transform: `rotate(0deg)`, 380 | right: "-23px", 381 | top: "10px", 382 | }; 383 | default: 384 | return {}; // Default case if no side is specified 385 | } 386 | }; 387 | // - - 388 | // Card Arrow 389 | const CardArrow = () => { 390 | 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" }) })); 391 | }; 392 | // - - 393 | // Overlay Variants 394 | const variants = { 395 | visible: { opacity: 1 }, 396 | hidden: { opacity: 0 }, 397 | }; 398 | // - - 399 | // Pointer Options 400 | const pointerPadding = currentTourSteps?.[currentStep]?.pointerPadding ?? 30; 401 | const pointerPadOffset = pointerPadding / 2; 402 | const pointerRadius = currentTourSteps?.[currentStep]?.pointerRadius ?? 28; 403 | 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 && (_jsxs(Portal, { children: [!interact && (_jsx("div", { className: "fixed inset-0 z-[900]" })), _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-[900]", style: { 404 | boxShadow: `0 0 200vw 200vh rgba(${shadowRgb}, ${shadowOpacity})`, 405 | borderRadius: `${pointerRadius}px ${pointerRadius}px ${pointerRadius}px ${pointerRadius}px`, 406 | }, initial: pointerPosition 407 | ? { 408 | x: pointerPosition.x - pointerPadOffset, 409 | y: pointerPosition.y - pointerPadOffset, 410 | width: pointerPosition.width + pointerPadding, 411 | height: pointerPosition.height + pointerPadding, 412 | } 413 | : {}, animate: pointerPosition 414 | ? { 415 | x: pointerPosition.x - pointerPadOffset, 416 | y: pointerPosition.y - pointerPadOffset, 417 | width: pointerPosition.width + pointerPadding, 418 | height: pointerPosition.height + pointerPadding, 419 | } 420 | : {}, transition: cardTransition, children: _jsx("div", { className: "absolute flex flex-col max-w-[100%] transition-all min-w-min pointer-events-auto z-[950]", "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, {}) }) }) }) })] }))] })); 421 | }; 422 | export default Onborda; 423 | -------------------------------------------------------------------------------- /dist/OnbordaContext.d.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { OnbordaContextType } from "./types"; 3 | declare const useOnborda: () => OnbordaContextType; 4 | declare const OnbordaProvider: React.FC<{ 5 | children: React.ReactNode; 6 | }>; 7 | export { OnbordaProvider, useOnborda }; 8 | -------------------------------------------------------------------------------- /dist/OnbordaContext.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { jsx as _jsx } from "react/jsx-runtime"; 3 | import { createContext, useContext, useState, useCallback } from "react"; 4 | // Example Hooks Usage: 5 | // const { setCurrentStep, closeOnborda, startOnborda } = useOnborda(); 6 | // // To trigger a specific step 7 | // setCurrentStep(2); // step 3 8 | // // To close/start onboarding 9 | // closeOnborda(); 10 | // startOnborda(); 11 | const OnbordaContext = createContext(undefined); 12 | const useOnborda = () => { 13 | const context = useContext(OnbordaContext); 14 | if (context === undefined) { 15 | throw new Error("useOnborda must be used within an OnbordaProvider"); 16 | } 17 | return context; 18 | }; 19 | const OnbordaProvider = ({ children, }) => { 20 | const [currentTour, setCurrentTour] = useState(null); 21 | const [currentStep, setCurrentStepState] = useState(0); 22 | const [isOnbordaVisible, setOnbordaVisible] = useState(false); 23 | const setCurrentStep = useCallback((step, delay) => { 24 | if (delay) { 25 | setTimeout(() => { 26 | setCurrentStepState(step); 27 | setOnbordaVisible(true); 28 | }, delay); 29 | } 30 | else { 31 | setCurrentStepState(step); 32 | setOnbordaVisible(true); 33 | } 34 | }, []); 35 | const closeOnborda = useCallback(() => { 36 | setOnbordaVisible(false); 37 | setCurrentTour(null); 38 | }, []); 39 | const startOnborda = useCallback((tourName) => { 40 | setCurrentTour(tourName); 41 | setCurrentStepState(0); 42 | setOnbordaVisible(true); 43 | }, []); 44 | return (_jsx(OnbordaContext.Provider, { value: { 45 | currentTour, 46 | currentStep, 47 | setCurrentStep, 48 | closeOnborda, 49 | startOnborda, 50 | isOnbordaVisible, 51 | }, children: children })); 52 | }; 53 | export { OnbordaProvider, useOnborda }; 54 | -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | export { OnbordaProvider, useOnborda } from "./OnbordaContext"; 2 | export { default as Onborda } from "./Onborda"; 3 | export type { OnbordaProps, Step, OnbordaContextType, CardComponentProps } from "./types"; 4 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | export { OnbordaProvider, useOnborda } from "./OnbordaContext"; 2 | export { default as Onborda } from "./Onborda"; 3 | -------------------------------------------------------------------------------- /dist/types/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { Transition } from "framer-motion"; 3 | export interface OnbordaContextType { 4 | currentStep: number; 5 | currentTour: string | null; 6 | setCurrentStep: (step: number, delay?: number) => void; 7 | closeOnborda: () => void; 8 | startOnborda: (tourName: string) => void; 9 | isOnbordaVisible: boolean; 10 | } 11 | export interface Step { 12 | icon: React.ReactNode | string | null; 13 | title: string; 14 | content: React.ReactNode; 15 | selector: string; 16 | side?: "top" | "bottom" | "left" | "right" | "top-left" | "top-right" | "bottom-left" | "bottom-right" | "left-top" | "left-bottom" | "right-top" | "right-bottom"; 17 | showControls?: boolean; 18 | pointerPadding?: number; 19 | pointerRadius?: number; 20 | nextRoute?: string; 21 | prevRoute?: string; 22 | } 23 | export interface Tour { 24 | tour: string; 25 | steps: Step[]; 26 | } 27 | export interface OnbordaProps { 28 | children: React.ReactNode; 29 | interact?: boolean; 30 | steps: Tour[]; 31 | showOnborda?: boolean; 32 | shadowRgb?: string; 33 | shadowOpacity?: string; 34 | cardTransition?: Transition; 35 | cardComponent?: React.ComponentType; 36 | } 37 | export interface CardComponentProps { 38 | step: Step; 39 | currentStep: number; 40 | totalSteps: number; 41 | nextStep: () => void; 42 | prevStep: () => void; 43 | arrow: JSX.Element; 44 | } 45 | -------------------------------------------------------------------------------- /dist/types/index.js: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "onborda", 3 | "version": "1.2.5", 4 | "description": "The ultimate product tour library for Next.js", 5 | "license": "MIT", 6 | "author": "Matt Litherland", 7 | "homepage": "https://github.com/uixmat/onborda#readme", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/uixmat/onborda.git" 11 | }, 12 | "keywords": [ 13 | "react-onboarding", 14 | "react-onborda", 15 | "onboarding", 16 | "onborda", 17 | "wizard", 18 | "react", 19 | "product-tour", 20 | "tour", 21 | "demo", 22 | "product-demo" 23 | ], 24 | "main": "dist/index.js", 25 | "types": "dist/index.d.ts", 26 | "scripts": { 27 | "build": "tsc --project tsconfig.json", 28 | "start": "npm run build && node dist/index.js" 29 | }, 30 | "peerDependencies": { 31 | "framer-motion": ">=11", 32 | "next": ">=13", 33 | "react": ">=18", 34 | "react-dom": ">=18", 35 | "@radix-ui/react-portal": ">=1.1.1" 36 | }, 37 | "devDependencies": { 38 | "@types/react": "^18.2.63", 39 | "typescript": "^5.3.3" 40 | }, 41 | "packageManager": "pnpm@9.15.0+sha512.76e2379760a4328ec4415815bcd6628dee727af3779aaa4c914e3944156c4299921a89f976381ee107d41f12cfa4b66681ca9c718f0668fa0831ed4c6d8ba56c" 42 | } 43 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | dependencies: 11 | '@radix-ui/react-portal': 12 | specifier: '>=1.1.1' 13 | version: 1.1.1(@types/react@18.2.63)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) 14 | framer-motion: 15 | specifier: '>=11' 16 | version: 11.0.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0) 17 | next: 18 | specifier: '>=13' 19 | version: 14.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) 20 | react: 21 | specifier: '>=18' 22 | version: 18.2.0 23 | react-dom: 24 | specifier: '>=18' 25 | version: 18.2.0(react@18.2.0) 26 | devDependencies: 27 | '@types/react': 28 | specifier: ^18.2.63 29 | version: 18.2.63 30 | typescript: 31 | specifier: ^5.3.3 32 | version: 5.3.3 33 | 34 | packages: 35 | 36 | '@emotion/is-prop-valid@0.8.8': 37 | resolution: {integrity: sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==} 38 | 39 | '@emotion/memoize@0.7.4': 40 | resolution: {integrity: sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==} 41 | 42 | '@next/env@14.1.2': 43 | resolution: {integrity: sha512-U0iEG+JF86j6qyu330sfPgsMmDVH8vWVmzZadl+an5EU3o5HqdNytOpM+HsFpl58PmhGBTKx3UmM9c+eoLK0mA==} 44 | 45 | '@next/swc-darwin-arm64@14.1.2': 46 | resolution: {integrity: sha512-E4/clgk0ZrYMo9eMRwP/4IO/cvXF1yEYSnGcdGfH+NYTR8bNFy76TSlc1Vb2rK3oaQY4BVHRpx8f/sMN/D5gNw==} 47 | engines: {node: '>= 10'} 48 | cpu: [arm64] 49 | os: [darwin] 50 | 51 | '@next/swc-darwin-x64@14.1.2': 52 | resolution: {integrity: sha512-j8mEOI+ZM0tU9B/L/OGa6F7d9FXYMkog5OWWuhTWzz3iZ91UKIGGpD/ojTNKuejainDMgbqOBTNnLg0jZywM/g==} 53 | engines: {node: '>= 10'} 54 | cpu: [x64] 55 | os: [darwin] 56 | 57 | '@next/swc-linux-arm64-gnu@14.1.2': 58 | resolution: {integrity: sha512-qpRrd5hl6BFTWiFLgHtJmqqQGRMs+ol0MN9pEp0SYoLs3j8OTErPiDMhbKWjMWHGdc2E3kg4RRBV3cSTZiePiQ==} 59 | engines: {node: '>= 10'} 60 | cpu: [arm64] 61 | os: [linux] 62 | 63 | '@next/swc-linux-arm64-musl@14.1.2': 64 | resolution: {integrity: sha512-HAhvVXAv+wnbj0wztT0YnpgJVoHtw1Mv4Y1R/JJcg5yXSU8FsP2uEGUwjQaqPoD76YSZjuKl32YbJlmPgQbLFw==} 65 | engines: {node: '>= 10'} 66 | cpu: [arm64] 67 | os: [linux] 68 | 69 | '@next/swc-linux-x64-gnu@14.1.2': 70 | resolution: {integrity: sha512-PCWC312woXLWOXiedi1E+fEw6B/ECP1fMiK1nSoGS2E43o56Z8kq4WeJLbJoufFQGVj5ZOKU3jIVyV//3CI4wQ==} 71 | engines: {node: '>= 10'} 72 | cpu: [x64] 73 | os: [linux] 74 | 75 | '@next/swc-linux-x64-musl@14.1.2': 76 | resolution: {integrity: sha512-KQSKzdWPNrYZjeTPCsepEpagOzU8Nf3Zzu53X1cLsSY6QlOIkYcSgEihRjsMKyeQW4aSvc+nN5pIpC2pLWNSMA==} 77 | engines: {node: '>= 10'} 78 | cpu: [x64] 79 | os: [linux] 80 | 81 | '@next/swc-win32-arm64-msvc@14.1.2': 82 | resolution: {integrity: sha512-3b0PouKd09Ulm2T1tjaRnwQj9+UwSsMO680d/sD4XAlm29KkNmVLAEIwWTfb3L+E11Qyw+jdcN3HtbDCg5+vYA==} 83 | engines: {node: '>= 10'} 84 | cpu: [arm64] 85 | os: [win32] 86 | 87 | '@next/swc-win32-ia32-msvc@14.1.2': 88 | resolution: {integrity: sha512-CC1gaJY4h+wg6d5r2biggGM6nCFXh/6WEim2VOQI0WrA6easCQi2P2hzWyrU6moQ0g1GOiWzesGc6nn0a92Kgg==} 89 | engines: {node: '>= 10'} 90 | cpu: [ia32] 91 | os: [win32] 92 | 93 | '@next/swc-win32-x64-msvc@14.1.2': 94 | resolution: {integrity: sha512-pfASwanOd+yP3D80O63DuQffrBySZPuB7wRN0IGSRq/0rDm9p/MvvnLzzgP2kSiLOUklOrFYVax7P6AEzjGykQ==} 95 | engines: {node: '>= 10'} 96 | cpu: [x64] 97 | os: [win32] 98 | 99 | '@radix-ui/react-compose-refs@1.1.0': 100 | resolution: {integrity: sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==} 101 | peerDependencies: 102 | '@types/react': '*' 103 | react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 104 | peerDependenciesMeta: 105 | '@types/react': 106 | optional: true 107 | 108 | '@radix-ui/react-portal@1.1.1': 109 | resolution: {integrity: sha512-A3UtLk85UtqhzFqtoC8Q0KvR2GbXF3mtPgACSazajqq6A41mEQgo53iPzY4i6BwDxlIFqWIhiQ2G729n+2aw/g==} 110 | peerDependencies: 111 | '@types/react': '*' 112 | '@types/react-dom': '*' 113 | react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 114 | react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 115 | peerDependenciesMeta: 116 | '@types/react': 117 | optional: true 118 | '@types/react-dom': 119 | optional: true 120 | 121 | '@radix-ui/react-primitive@2.0.0': 122 | resolution: {integrity: sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==} 123 | peerDependencies: 124 | '@types/react': '*' 125 | '@types/react-dom': '*' 126 | react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 127 | react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 128 | peerDependenciesMeta: 129 | '@types/react': 130 | optional: true 131 | '@types/react-dom': 132 | optional: true 133 | 134 | '@radix-ui/react-slot@1.1.0': 135 | resolution: {integrity: sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==} 136 | peerDependencies: 137 | '@types/react': '*' 138 | react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 139 | peerDependenciesMeta: 140 | '@types/react': 141 | optional: true 142 | 143 | '@radix-ui/react-use-layout-effect@1.1.0': 144 | resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==} 145 | peerDependencies: 146 | '@types/react': '*' 147 | react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 148 | peerDependenciesMeta: 149 | '@types/react': 150 | optional: true 151 | 152 | '@swc/helpers@0.5.2': 153 | resolution: {integrity: sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==} 154 | 155 | '@types/prop-types@15.7.11': 156 | resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==} 157 | 158 | '@types/react@18.2.63': 159 | resolution: {integrity: sha512-ppaqODhs15PYL2nGUOaOu2RSCCB4Difu4UFrP4I3NHLloXC/ESQzQMi9nvjfT1+rudd0d2L3fQPJxRSey+rGlQ==} 160 | 161 | '@types/scheduler@0.16.8': 162 | resolution: {integrity: sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==} 163 | 164 | busboy@1.6.0: 165 | resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} 166 | engines: {node: '>=10.16.0'} 167 | 168 | caniuse-lite@1.0.30001594: 169 | resolution: {integrity: sha512-VblSX6nYqyJVs8DKFMldE2IVCJjZ225LW00ydtUWwh5hk9IfkTOffO6r8gJNsH0qqqeAF8KrbMYA2VEwTlGW5g==} 170 | 171 | client-only@0.0.1: 172 | resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} 173 | 174 | csstype@3.1.3: 175 | resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} 176 | 177 | framer-motion@11.0.8: 178 | resolution: {integrity: sha512-1KSGNuqe1qZkS/SWQlDnqK2VCVzRVEoval379j0FiUBJAZoqgwyvqFkfvJbgW2IPFo4wX16K+M0k5jO23lCIjA==} 179 | peerDependencies: 180 | react: ^18.0.0 181 | react-dom: ^18.0.0 182 | peerDependenciesMeta: 183 | react: 184 | optional: true 185 | react-dom: 186 | optional: true 187 | 188 | graceful-fs@4.2.11: 189 | resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} 190 | 191 | js-tokens@4.0.0: 192 | resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} 193 | 194 | loose-envify@1.4.0: 195 | resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} 196 | hasBin: true 197 | 198 | nanoid@3.3.7: 199 | resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} 200 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 201 | hasBin: true 202 | 203 | next@14.1.2: 204 | resolution: {integrity: sha512-p4RfNmopqkzRP1uUyBJnHii+qMg71f2udWhTTZopBB8b3T5QXNzn7yO+LCYHPWZG2kAvEn4l4neyJHqkXvo2wg==} 205 | engines: {node: '>=18.17.0'} 206 | hasBin: true 207 | peerDependencies: 208 | '@opentelemetry/api': ^1.1.0 209 | react: ^18.2.0 210 | react-dom: ^18.2.0 211 | sass: ^1.3.0 212 | peerDependenciesMeta: 213 | '@opentelemetry/api': 214 | optional: true 215 | sass: 216 | optional: true 217 | 218 | picocolors@1.0.0: 219 | resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} 220 | 221 | postcss@8.4.31: 222 | resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} 223 | engines: {node: ^10 || ^12 || >=14} 224 | 225 | react-dom@18.2.0: 226 | resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} 227 | peerDependencies: 228 | react: ^18.2.0 229 | 230 | react@18.2.0: 231 | resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} 232 | engines: {node: '>=0.10.0'} 233 | 234 | scheduler@0.23.0: 235 | resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} 236 | 237 | source-map-js@1.0.2: 238 | resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} 239 | engines: {node: '>=0.10.0'} 240 | 241 | streamsearch@1.1.0: 242 | resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} 243 | engines: {node: '>=10.0.0'} 244 | 245 | styled-jsx@5.1.1: 246 | resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} 247 | engines: {node: '>= 12.0.0'} 248 | peerDependencies: 249 | '@babel/core': '*' 250 | babel-plugin-macros: '*' 251 | react: '>= 16.8.0 || 17.x.x || ^18.0.0-0' 252 | peerDependenciesMeta: 253 | '@babel/core': 254 | optional: true 255 | babel-plugin-macros: 256 | optional: true 257 | 258 | tslib@2.6.2: 259 | resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} 260 | 261 | typescript@5.3.3: 262 | resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} 263 | engines: {node: '>=14.17'} 264 | hasBin: true 265 | 266 | snapshots: 267 | 268 | '@emotion/is-prop-valid@0.8.8': 269 | dependencies: 270 | '@emotion/memoize': 0.7.4 271 | optional: true 272 | 273 | '@emotion/memoize@0.7.4': 274 | optional: true 275 | 276 | '@next/env@14.1.2': {} 277 | 278 | '@next/swc-darwin-arm64@14.1.2': 279 | optional: true 280 | 281 | '@next/swc-darwin-x64@14.1.2': 282 | optional: true 283 | 284 | '@next/swc-linux-arm64-gnu@14.1.2': 285 | optional: true 286 | 287 | '@next/swc-linux-arm64-musl@14.1.2': 288 | optional: true 289 | 290 | '@next/swc-linux-x64-gnu@14.1.2': 291 | optional: true 292 | 293 | '@next/swc-linux-x64-musl@14.1.2': 294 | optional: true 295 | 296 | '@next/swc-win32-arm64-msvc@14.1.2': 297 | optional: true 298 | 299 | '@next/swc-win32-ia32-msvc@14.1.2': 300 | optional: true 301 | 302 | '@next/swc-win32-x64-msvc@14.1.2': 303 | optional: true 304 | 305 | '@radix-ui/react-compose-refs@1.1.0(@types/react@18.2.63)(react@18.2.0)': 306 | dependencies: 307 | react: 18.2.0 308 | optionalDependencies: 309 | '@types/react': 18.2.63 310 | 311 | '@radix-ui/react-portal@1.1.1(@types/react@18.2.63)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': 312 | dependencies: 313 | '@radix-ui/react-primitive': 2.0.0(@types/react@18.2.63)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) 314 | '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.63)(react@18.2.0) 315 | react: 18.2.0 316 | react-dom: 18.2.0(react@18.2.0) 317 | optionalDependencies: 318 | '@types/react': 18.2.63 319 | 320 | '@radix-ui/react-primitive@2.0.0(@types/react@18.2.63)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': 321 | dependencies: 322 | '@radix-ui/react-slot': 1.1.0(@types/react@18.2.63)(react@18.2.0) 323 | react: 18.2.0 324 | react-dom: 18.2.0(react@18.2.0) 325 | optionalDependencies: 326 | '@types/react': 18.2.63 327 | 328 | '@radix-ui/react-slot@1.1.0(@types/react@18.2.63)(react@18.2.0)': 329 | dependencies: 330 | '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.63)(react@18.2.0) 331 | react: 18.2.0 332 | optionalDependencies: 333 | '@types/react': 18.2.63 334 | 335 | '@radix-ui/react-use-layout-effect@1.1.0(@types/react@18.2.63)(react@18.2.0)': 336 | dependencies: 337 | react: 18.2.0 338 | optionalDependencies: 339 | '@types/react': 18.2.63 340 | 341 | '@swc/helpers@0.5.2': 342 | dependencies: 343 | tslib: 2.6.2 344 | 345 | '@types/prop-types@15.7.11': {} 346 | 347 | '@types/react@18.2.63': 348 | dependencies: 349 | '@types/prop-types': 15.7.11 350 | '@types/scheduler': 0.16.8 351 | csstype: 3.1.3 352 | 353 | '@types/scheduler@0.16.8': {} 354 | 355 | busboy@1.6.0: 356 | dependencies: 357 | streamsearch: 1.1.0 358 | 359 | caniuse-lite@1.0.30001594: {} 360 | 361 | client-only@0.0.1: {} 362 | 363 | csstype@3.1.3: {} 364 | 365 | framer-motion@11.0.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0): 366 | dependencies: 367 | tslib: 2.6.2 368 | optionalDependencies: 369 | '@emotion/is-prop-valid': 0.8.8 370 | react: 18.2.0 371 | react-dom: 18.2.0(react@18.2.0) 372 | 373 | graceful-fs@4.2.11: {} 374 | 375 | js-tokens@4.0.0: {} 376 | 377 | loose-envify@1.4.0: 378 | dependencies: 379 | js-tokens: 4.0.0 380 | 381 | nanoid@3.3.7: {} 382 | 383 | next@14.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0): 384 | dependencies: 385 | '@next/env': 14.1.2 386 | '@swc/helpers': 0.5.2 387 | busboy: 1.6.0 388 | caniuse-lite: 1.0.30001594 389 | graceful-fs: 4.2.11 390 | postcss: 8.4.31 391 | react: 18.2.0 392 | react-dom: 18.2.0(react@18.2.0) 393 | styled-jsx: 5.1.1(react@18.2.0) 394 | optionalDependencies: 395 | '@next/swc-darwin-arm64': 14.1.2 396 | '@next/swc-darwin-x64': 14.1.2 397 | '@next/swc-linux-arm64-gnu': 14.1.2 398 | '@next/swc-linux-arm64-musl': 14.1.2 399 | '@next/swc-linux-x64-gnu': 14.1.2 400 | '@next/swc-linux-x64-musl': 14.1.2 401 | '@next/swc-win32-arm64-msvc': 14.1.2 402 | '@next/swc-win32-ia32-msvc': 14.1.2 403 | '@next/swc-win32-x64-msvc': 14.1.2 404 | transitivePeerDependencies: 405 | - '@babel/core' 406 | - babel-plugin-macros 407 | 408 | picocolors@1.0.0: {} 409 | 410 | postcss@8.4.31: 411 | dependencies: 412 | nanoid: 3.3.7 413 | picocolors: 1.0.0 414 | source-map-js: 1.0.2 415 | 416 | react-dom@18.2.0(react@18.2.0): 417 | dependencies: 418 | loose-envify: 1.4.0 419 | react: 18.2.0 420 | scheduler: 0.23.0 421 | 422 | react@18.2.0: 423 | dependencies: 424 | loose-envify: 1.4.0 425 | 426 | scheduler@0.23.0: 427 | dependencies: 428 | loose-envify: 1.4.0 429 | 430 | source-map-js@1.0.2: {} 431 | 432 | streamsearch@1.1.0: {} 433 | 434 | styled-jsx@5.1.1(react@18.2.0): 435 | dependencies: 436 | client-only: 0.0.1 437 | react: 18.2.0 438 | 439 | tslib@2.6.2: {} 440 | 441 | typescript@5.3.3: {} 442 | -------------------------------------------------------------------------------- /src/Onborda.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useState, useEffect, useRef } from "react"; 3 | import { useOnborda } from "./OnbordaContext"; 4 | import { motion, useInView } from "framer-motion"; 5 | import { useRouter } from "next/navigation"; 6 | import { Portal } from "@radix-ui/react-portal"; 7 | 8 | // Types 9 | import { OnbordaProps } from "./types"; 10 | 11 | const Onborda: React.FC = ({ 12 | children, 13 | interact = false, 14 | steps, 15 | shadowRgb = "0, 0, 0", 16 | shadowOpacity = "0.2", 17 | cardTransition = { ease: "anticipate", duration: 0.6 }, 18 | cardComponent: CardComponent, 19 | }) => { 20 | const { currentTour, currentStep, setCurrentStep, isOnbordaVisible } = 21 | useOnborda(); 22 | const currentTourSteps = steps.find( 23 | (tour) => tour.tour === currentTour 24 | )?.steps; 25 | 26 | const [elementToScroll, setElementToScroll] = useState(null); 27 | const [pointerPosition, setPointerPosition] = useState<{ 28 | x: number; 29 | y: number; 30 | width: number; 31 | height: number; 32 | } | null>(null); 33 | const currentElementRef = useRef(null); 34 | const observeRef = useRef(null); // Ref for the observer element 35 | const isInView = useInView(observeRef); 36 | const offset = 20; 37 | 38 | // - - 39 | // Route Changes 40 | const router = useRouter(); 41 | 42 | // - - 43 | // Initialisze 44 | const previousElementRef = useRef(null); 45 | 46 | useEffect(() => { 47 | if (isOnbordaVisible && currentTourSteps) { 48 | // Clean up all elements that might have our styles 49 | currentTourSteps.forEach(tourStep => { 50 | const element = document.querySelector(tourStep.selector) as HTMLElement | null; 51 | if (element && tourStep !== currentTourSteps[currentStep]) { 52 | // Reset styles for non-active elements if interaction is enabled 53 | if (interact) { 54 | const style = element.style; 55 | style.position = ''; 56 | style.zIndex = ''; 57 | } 58 | } 59 | }); 60 | 61 | const step = currentTourSteps[currentStep]; 62 | if (step) { 63 | const element = document.querySelector(step.selector) as Element | null; 64 | if (element) { 65 | // Set styles for current element 66 | (element as HTMLElement).style.position = 'relative'; 67 | if (interact) { 68 | (element as HTMLElement).style.zIndex = '990'; 69 | } 70 | 71 | setPointerPosition(getElementPosition(element)); 72 | currentElementRef.current = element; 73 | setElementToScroll(element); 74 | 75 | const rect = element.getBoundingClientRect(); 76 | const isInViewportWithOffset = 77 | rect.top >= -offset && rect.bottom <= window.innerHeight + offset; 78 | 79 | if (!isInView || !isInViewportWithOffset) { 80 | element.scrollIntoView({ behavior: "smooth", block: "center" }); 81 | } 82 | } 83 | } 84 | } 85 | 86 | // Cleanup function for component unmount 87 | return () => { 88 | if (currentTourSteps) { 89 | currentTourSteps.forEach(step => { 90 | const element = document.querySelector(step.selector) as HTMLElement | null; 91 | if (element && interact) { 92 | element.style.position = ''; 93 | element.style.zIndex = ''; 94 | } 95 | }); 96 | } 97 | }; 98 | }, [currentStep, currentTourSteps, isInView, offset, isOnbordaVisible, interact]); 99 | 100 | // - - 101 | // Helper function to get element position 102 | const getElementPosition = (element: Element) => { 103 | const { top, left, width, height } = element.getBoundingClientRect(); 104 | const scrollTop = window.scrollY || document.documentElement.scrollTop; 105 | const scrollLeft = window.scrollX || document.documentElement.scrollLeft; 106 | return { 107 | x: left + scrollLeft, 108 | y: top + scrollTop, 109 | width, 110 | height, 111 | }; 112 | }; 113 | 114 | // - - 115 | // Update pointerPosition when currentStep changes 116 | useEffect(() => { 117 | if (isOnbordaVisible && currentTourSteps) { 118 | console.log("Onborda: Current Step Changed"); 119 | const step = currentTourSteps[currentStep]; 120 | if (step) { 121 | const element = document.querySelector(step.selector) as Element | null; 122 | if (element) { 123 | setPointerPosition(getElementPosition(element)); 124 | currentElementRef.current = element; 125 | setElementToScroll(element); 126 | 127 | const rect = element.getBoundingClientRect(); 128 | const isInViewportWithOffset = 129 | rect.top >= -offset && rect.bottom <= window.innerHeight + offset; 130 | 131 | if (!isInView || !isInViewportWithOffset) { 132 | element.scrollIntoView({ behavior: "smooth", block: "center" }); 133 | } 134 | } 135 | } 136 | } 137 | }, [currentStep, currentTourSteps, isInView, offset, isOnbordaVisible]); 138 | 139 | useEffect(() => { 140 | if (elementToScroll && !isInView && isOnbordaVisible) { 141 | console.log("Onborda: Element to Scroll Changed"); 142 | const rect = elementToScroll.getBoundingClientRect(); 143 | const isAbove = rect.top < 0; 144 | elementToScroll.scrollIntoView({ 145 | behavior: "smooth", 146 | block: isAbove ? "center" : "center", 147 | inline: "center", 148 | }); 149 | } 150 | }, [elementToScroll, isInView, isOnbordaVisible]); 151 | 152 | // - - 153 | // Update pointer position on window resize 154 | const updatePointerPosition = () => { 155 | if (currentTourSteps) { 156 | const step = currentTourSteps[currentStep]; 157 | if (step) { 158 | const element = document.querySelector(step.selector) as Element | null; 159 | if (element) { 160 | setPointerPosition(getElementPosition(element)); 161 | } 162 | } 163 | } 164 | }; 165 | 166 | // - - 167 | // Update pointer position on window resize 168 | useEffect(() => { 169 | if (isOnbordaVisible) { 170 | window.addEventListener("resize", updatePointerPosition); 171 | return () => window.removeEventListener("resize", updatePointerPosition); 172 | } 173 | }, [currentStep, currentTourSteps, isOnbordaVisible]); 174 | 175 | // - - 176 | // Step Controls 177 | const nextStep = async () => { 178 | if (currentTourSteps && currentStep < currentTourSteps.length - 1) { 179 | try { 180 | const nextStepIndex = currentStep + 1; 181 | const route = currentTourSteps[currentStep].nextRoute; 182 | 183 | if (route) { 184 | await router.push(route); 185 | 186 | const targetSelector = currentTourSteps[nextStepIndex].selector; 187 | 188 | // Use MutationObserver to detect when the target element is available in the DOM 189 | const observer = new MutationObserver((mutations, observer) => { 190 | const element = document.querySelector(targetSelector); 191 | if (element) { 192 | // Once the element is found, update the step and scroll to the element 193 | setCurrentStep(nextStepIndex); 194 | scrollToElement(nextStepIndex); 195 | 196 | // Stop observing after the element is found 197 | observer.disconnect(); 198 | } 199 | }); 200 | 201 | // Start observing the document body for changes 202 | observer.observe(document.body, { 203 | childList: true, 204 | subtree: true, 205 | }); 206 | } else { 207 | setCurrentStep(nextStepIndex); 208 | scrollToElement(nextStepIndex); 209 | } 210 | } catch (error) { 211 | console.error("Error navigating to next route", error); 212 | } 213 | } 214 | }; 215 | 216 | const prevStep = async () => { 217 | if (currentTourSteps && currentStep > 0) { 218 | try { 219 | const prevStepIndex = currentStep - 1; 220 | const route = currentTourSteps[currentStep].prevRoute; 221 | 222 | if (route) { 223 | await router.push(route); 224 | 225 | const targetSelector = currentTourSteps[prevStepIndex].selector; 226 | 227 | // Use MutationObserver to detect when the target element is available in the DOM 228 | const observer = new MutationObserver((mutations, observer) => { 229 | const element = document.querySelector(targetSelector); 230 | if (element) { 231 | // Once the element is found, update the step and scroll to the element 232 | setCurrentStep(prevStepIndex); 233 | scrollToElement(prevStepIndex); 234 | 235 | // Stop observing after the element is found 236 | observer.disconnect(); 237 | } 238 | }); 239 | 240 | // Start observing the document body for changes 241 | observer.observe(document.body, { 242 | childList: true, 243 | subtree: true, 244 | }); 245 | } else { 246 | setCurrentStep(prevStepIndex); 247 | scrollToElement(prevStepIndex); 248 | } 249 | } catch (error) { 250 | console.error("Error navigating to previous route", error); 251 | } 252 | } 253 | }; 254 | 255 | // - - 256 | // Scroll to the correct element when the step changes 257 | const scrollToElement = (stepIndex: number) => { 258 | if (currentTourSteps) { 259 | const element = document.querySelector( 260 | currentTourSteps[stepIndex].selector 261 | ) as Element | null; 262 | if (element) { 263 | const { top } = element.getBoundingClientRect(); 264 | const isInViewport = 265 | top >= -offset && top <= window.innerHeight + offset; 266 | if (!isInViewport) { 267 | element.scrollIntoView({ behavior: "smooth", block: "center" }); 268 | } 269 | // Update pointer position after scrolling 270 | setPointerPosition(getElementPosition(element)); 271 | } 272 | } 273 | }; 274 | 275 | // - - 276 | // Card Side 277 | const getCardStyle = (side: string) => { 278 | switch (side) { 279 | case "top": 280 | return { 281 | transform: `translate(-50%, 0)`, 282 | left: "50%", 283 | bottom: "100%", 284 | marginBottom: "25px", 285 | }; 286 | case "bottom": 287 | return { 288 | transform: `translate(-50%, 0)`, 289 | left: "50%", 290 | top: "100%", 291 | marginTop: "25px", 292 | }; 293 | case "left": 294 | return { 295 | transform: `translate(0, -50%)`, 296 | right: "100%", 297 | top: "50%", 298 | marginRight: "25px", 299 | }; 300 | case "right": 301 | return { 302 | transform: `translate(0, -50%)`, 303 | left: "100%", 304 | top: "50%", 305 | marginLeft: "25px", 306 | }; 307 | case "top-left": 308 | return { 309 | bottom: "100%", 310 | marginBottom: "25px", 311 | }; 312 | case "top-right": 313 | return { 314 | right: 0, 315 | bottom: "100%", 316 | marginBottom: "25px", 317 | }; 318 | case "bottom-left": 319 | return { 320 | top: "100%", 321 | marginTop: "25px", 322 | }; 323 | case "bottom-right": 324 | return { 325 | right: 0, 326 | top: "100%", 327 | marginTop: "25px", 328 | }; 329 | case "right-bottom": 330 | return { 331 | left: "100%", 332 | bottom: 0, 333 | marginLeft: "25px", 334 | }; 335 | case "right-top": 336 | return { 337 | left: "100%", 338 | top: 0, 339 | marginLeft: "25px", 340 | }; 341 | case "left-bottom": 342 | return { 343 | right: "100%", 344 | bottom: 0, 345 | marginRight: "25px", 346 | }; 347 | case "left-top": 348 | return { 349 | right: "100%", 350 | top: 0, 351 | marginRight: "25px", 352 | }; 353 | default: 354 | return {}; // Default case if no side is specified 355 | } 356 | }; 357 | 358 | // - - 359 | // Arrow position based on card side 360 | const getArrowStyle = (side: string) => { 361 | switch (side) { 362 | case "bottom": 363 | return { 364 | transform: `translate(-50%, 0) rotate(270deg)`, 365 | left: "50%", 366 | top: "-23px", 367 | }; 368 | case "top": 369 | return { 370 | transform: `translate(-50%, 0) rotate(90deg)`, 371 | left: "50%", 372 | bottom: "-23px", 373 | }; 374 | case "right": 375 | return { 376 | transform: `translate(0, -50%) rotate(180deg)`, 377 | top: "50%", 378 | left: "-23px", 379 | }; 380 | case "left": 381 | return { 382 | transform: `translate(0, -50%) rotate(0deg)`, 383 | top: "50%", 384 | right: "-23px", 385 | }; 386 | case "top-left": 387 | return { 388 | transform: `rotate(90deg)`, 389 | left: "10px", 390 | bottom: "-23px", 391 | }; 392 | case "top-right": 393 | return { 394 | transform: `rotate(90deg)`, 395 | right: "10px", 396 | bottom: "-23px", 397 | }; 398 | case "bottom-left": 399 | return { 400 | transform: `rotate(270deg)`, 401 | left: "10px", 402 | top: "-23px", 403 | }; 404 | case "bottom-right": 405 | return { 406 | transform: `rotate(270deg)`, 407 | right: "10px", 408 | top: "-23px", 409 | }; 410 | case "right-bottom": 411 | return { 412 | transform: `rotate(180deg)`, 413 | left: "-23px", 414 | bottom: "10px", 415 | }; 416 | case "right-top": 417 | return { 418 | transform: `rotate(180deg)`, 419 | left: "-23px", 420 | top: "10px", 421 | }; 422 | case "left-bottom": 423 | return { 424 | transform: `rotate(0deg)`, 425 | right: "-23px", 426 | bottom: "10px", 427 | }; 428 | case "left-top": 429 | return { 430 | transform: `rotate(0deg)`, 431 | right: "-23px", 432 | top: "10px", 433 | }; 434 | default: 435 | return {}; // Default case if no side is specified 436 | } 437 | }; 438 | 439 | // - - 440 | // Card Arrow 441 | const CardArrow = () => { 442 | return ( 443 | 449 | 450 | 451 | ); 452 | }; 453 | 454 | // - - 455 | // Overlay Variants 456 | const variants = { 457 | visible: { opacity: 1 }, 458 | hidden: { opacity: 0 }, 459 | }; 460 | 461 | // - - 462 | // Pointer Options 463 | const pointerPadding = currentTourSteps?.[currentStep]?.pointerPadding ?? 30; 464 | const pointerPadOffset = pointerPadding / 2; 465 | const pointerRadius = currentTourSteps?.[currentStep]?.pointerRadius ?? 28; 466 | 467 | return ( 468 |
473 | {/* Container for the Website content */} 474 |
475 | {children} 476 |
477 | 478 | {/* Onborda Overlay Step Content */} 479 | {pointerPosition && isOnbordaVisible && CardComponent && ( 480 | 481 | {!interact && ( 482 |
483 | )} 484 | 492 | 521 | {/* Card */} 522 |
529 | } 536 | /> 537 |
538 |
539 |
540 | 541 | )} 542 |
543 | ); 544 | }; 545 | 546 | export default Onborda; 547 | -------------------------------------------------------------------------------- /src/OnbordaContext.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { createContext, useContext, useState, useCallback } from "react"; 3 | 4 | // Types 5 | import { OnbordaContextType } from "./types"; 6 | 7 | // Example Hooks Usage: 8 | // const { setCurrentStep, closeOnborda, startOnborda } = useOnborda(); 9 | 10 | // // To trigger a specific step 11 | // setCurrentStep(2); // step 3 12 | 13 | // // To close/start onboarding 14 | // closeOnborda(); 15 | // startOnborda(); 16 | 17 | const OnbordaContext = createContext(undefined); 18 | 19 | const useOnborda = () => { 20 | const context = useContext(OnbordaContext); 21 | if (context === undefined) { 22 | throw new Error("useOnborda must be used within an OnbordaProvider"); 23 | } 24 | return context; 25 | }; 26 | 27 | const OnbordaProvider: React.FC<{ children: React.ReactNode }> = ({ 28 | children, 29 | }) => { 30 | const [currentTour, setCurrentTour] = useState(null); 31 | const [currentStep, setCurrentStepState] = useState(0); 32 | const [isOnbordaVisible, setOnbordaVisible] = useState(false); 33 | 34 | const setCurrentStep = useCallback((step: number, delay?: number) => { 35 | if (delay) { 36 | setTimeout(() => { 37 | setCurrentStepState(step); 38 | setOnbordaVisible(true); 39 | }, delay); 40 | } else { 41 | setCurrentStepState(step); 42 | setOnbordaVisible(true); 43 | } 44 | }, []); 45 | 46 | const closeOnborda = useCallback(() => { 47 | setOnbordaVisible(false); 48 | setCurrentTour(null); 49 | }, []); 50 | 51 | const startOnborda = useCallback((tourName: string) => { 52 | setCurrentTour(tourName); 53 | setCurrentStepState(0); 54 | setOnbordaVisible(true); 55 | }, []); 56 | 57 | return ( 58 | 68 | {children} 69 | 70 | ); 71 | }; 72 | 73 | export { OnbordaProvider, useOnborda }; 74 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { OnbordaProvider, useOnborda } from "./OnbordaContext"; 2 | export { default as Onborda } from "./Onborda"; 3 | export type { OnbordaProps, Step, OnbordaContextType, CardComponentProps } from "./types"; 4 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | import { Transition } from "framer-motion"; 2 | 3 | // Context 4 | export interface OnbordaContextType { 5 | currentStep: number; 6 | currentTour: string | null; 7 | setCurrentStep: (step: number, delay?: number) => void; 8 | closeOnborda: () => void; 9 | startOnborda: (tourName: string) => void; 10 | isOnbordaVisible: boolean; 11 | } 12 | 13 | // Step 14 | export interface Step { 15 | // Step Content 16 | icon?: React.ReactNode | string | null; 17 | title: string; 18 | content: React.ReactNode; 19 | selector: string; 20 | // Options 21 | side?: "top" | "bottom" | "left" | "right" | "top-left" | "top-right" | "bottom-left" | "bottom-right" | "left-top" | "left-bottom" | "right-top" | "right-bottom"; 22 | showControls?: boolean; 23 | pointerPadding?: number; 24 | pointerRadius?: number; 25 | // Routing 26 | nextRoute?: string; 27 | prevRoute?: string; 28 | } 29 | 30 | // Tour 31 | // 32 | export interface Tour { 33 | tour: string; 34 | steps: Step[]; 35 | } 36 | 37 | // Onborda 38 | export interface OnbordaProps { 39 | children: React.ReactNode; 40 | interact?: boolean; 41 | steps: Tour[]; 42 | showOnborda?: boolean; 43 | shadowRgb?: string; 44 | shadowOpacity?: string; 45 | cardTransition?: Transition; 46 | cardComponent?: React.ComponentType; 47 | } 48 | 49 | // Custom Card 50 | export interface CardComponentProps { 51 | step: Step; 52 | currentStep: number; 53 | totalSteps: number; 54 | nextStep: () => void; 55 | prevStep: () => void; 56 | arrow: JSX.Element; 57 | } 58 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "lib": ["dom", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": false, 16 | "jsx": "react-jsx", 17 | "outDir": "dist", 18 | "declaration": true, 19 | "rootDir": "./src", 20 | }, 21 | "include": ["src/**/*"], 22 | "exclude": ["node_modules"] 23 | } 24 | --------------------------------------------------------------------------------