├── .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 |
Previous
76 |
Next
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 |
--------------------------------------------------------------------------------