radix-tour 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE.md ADDED
@@ -0,0 +1,11 @@
1
+ Licensed under the MIT License. Copyright © 2026-present Wesley Júnior.
2
+
3
+ MIT License
4
+
5
+ Copyright © 2026-present Wesley Júnior
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
8
+
9
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
10
+
11
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,232 @@
1
- # Temporary Holding Version
1
+ # radix-tour
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ An unstyled product tour library for React, built on [Radix](https://www.radix-ui.com/primitives) primitives. Use it to introduce a feature, guide someone through a page, or explain a workflow one step at a time.
4
+
5
+ You build the card from composable parts and style it with your own classes. The library finds each target, positions the card beside it, handles navigation, and can draw a spotlight around it. Your app decides when the tour opens and whether a user should see it again.
6
+
7
+ ![A four-step product tour on a bakery order dashboard: the spotlight glides from the sidebar to the search box, the status tabs and the New order button while a dark card explains each one.](https://raw.githubusercontent.com/bw3sley/radix-tour/main/radix-tour.gif)
8
+
9
+ The tour above is the `Default` story in [`src/components/tour.stories.tsx`](./src/components/tour.stories.tsx).
10
+
11
+ ## Features
12
+
13
+ - **Composable.** Every part accepts `asChild`, so you can render your own button, heading or card.
14
+ - **Unstyled.** The library ships no CSS. Style parts with `className` and `data-*` attributes.
15
+ - **Radix underneath.** Positioning, collision handling and portals come from `@radix-ui/react-popover`.
16
+ - **Waits for targets.** A step whose element is not on the page yet renders once it appears.
17
+ - **Per-step placement.** Each step chooses its own `side` and `align`.
18
+ - **Independent spotlight.** The overlay is a separate part. Leave it out to point at things without dimming the page.
19
+ - **Server Components ready.** The build keeps the `"use client"` directive.
20
+ - **Small.** The library build is about 2 kB gzipped, with React and Radix external to the bundle.
21
+
22
+ ## Installation
23
+
24
+ The package has not had its first npm release. After it is published, install it with:
25
+
26
+ ```bash
27
+ npm install radix-tour
28
+ ```
29
+
30
+ `react` and `react-dom` (18 or 19) are peer dependencies. Tailwind is optional; the examples use it to show how consumers can style the parts.
31
+
32
+ ## Quick start
33
+
34
+ Give each target an element ID, define the steps in order, and control the tour's `open` state. `Tour.Content` is the card; `Tour.Spotlight` is the optional dimmed overlay.
35
+
36
+ ```tsx
37
+ import * as React from "react";
38
+ import * as Tour from "radix-tour";
39
+
40
+ const steps: Tour.TourStep[] = [
41
+ {
42
+ id: "search",
43
+ target: "#search",
44
+ title: "Find an order fast",
45
+ description: "Search by customer, product or order number.",
46
+ },
47
+ {
48
+ id: "new-order",
49
+ target: "#new-order",
50
+ side: "bottom",
51
+ align: "end",
52
+ title: "Add an order",
53
+ description: "Standing orders repeat every week.",
54
+ },
55
+ ];
56
+
57
+ function TourCard() {
58
+ const { isFirst, isLast } = Tour.useTour();
59
+
60
+ return (
61
+ <Tour.Content className="z-50 w-80 rounded-2xl bg-slate-900 p-5 text-white">
62
+ <Tour.Arrow className="fill-slate-900" />
63
+ <Tour.Close aria-label="Close tour" className="float-right">Close</Tour.Close>
64
+ <Tour.Title className="font-semibold" />
65
+ <Tour.Description className="mt-1 text-sm text-slate-300" />
66
+ <div className="mt-4 flex items-center justify-between">
67
+ <Tour.Progress className="text-xs text-slate-400" />
68
+ <div className="flex gap-2">
69
+ {!isFirst && <Tour.Previous>Back</Tour.Previous>}
70
+ <Tour.Next>{isLast ? "Done" : "Next"}</Tour.Next>
71
+ </div>
72
+ </div>
73
+ </Tour.Content>
74
+ );
75
+ }
76
+
77
+ export function OrdersTour() {
78
+ const [open, setOpen] = React.useState(false);
79
+
80
+ return (
81
+ <>
82
+ <input id="search" aria-label="Search orders" />
83
+ <button id="new-order" type="button">New order</button>
84
+ <button type="button" onClick={() => setOpen(true)}>Take the tour</button>
85
+
86
+ <Tour.Root steps={steps} open={open} onOpenChange={setOpen}>
87
+ <Tour.Spotlight className="z-40 text-slate-900/55" />
88
+ <TourCard />
89
+ </Tour.Root>
90
+ </>
91
+ );
92
+ }
93
+ ```
94
+
95
+ The `target` of each step is a CSS selector for an element in your page. When the tour opens, it starts at the first step. `Next` advances; on the last step it calls `onOpenChange(false)`. `Previous` moves back, and `Close` or <kbd>Esc</kbd> closes the tour.
96
+
97
+ ## Anatomy
98
+
99
+ Import the namespace and compose only the parts you need:
100
+
101
+ ```tsx
102
+ <Tour.Root steps={steps} open={open} onOpenChange={setOpen}>
103
+ <Tour.Spotlight />
104
+
105
+ <Tour.Content>
106
+ <Tour.Arrow />
107
+ <Tour.Close aria-label="Close tour">Close</Tour.Close>
108
+ <Tour.Title />
109
+ <Tour.Description />
110
+ <Tour.Progress />
111
+ <Tour.Previous>Back</Tour.Previous>
112
+ <Tour.Next>Next</Tour.Next>
113
+ </Tour.Content>
114
+ </Tour.Root>
115
+ ```
116
+
117
+ `Root` owns the active step. `Content` positions the card beside that step's target. `Spotlight` is separate and can be omitted. You can also import the long names (`Tour`, `TourContent`, `TourSpotlight`, and so on) directly.
118
+
119
+ ## How steps and targets work
120
+
121
+ A step has a unique `id` and a `target` CSS selector, such as `#search` or `[data-tour="search"]`. Optional `title` and `description` values appear in the matching parts; passing children to those parts overrides the values. `side` and `align` place the card for that step.
122
+
123
+ If a target has not mounted, the card waits until a matching element appears. The tour scrolls a resolved target into view and tracks its position while the page scrolls or resizes. Behavior for targets that remain missing or become hidden is tracked in [issue #1](https://github.com/bw3sley/radix-tour/issues/1).
124
+
125
+ The tour is controlled: your component owns `open`, and the library calls `onOpenChange(false)` when it finishes or closes. Set `open` to `true` again to restart it.
126
+
127
+ ## Styling
128
+
129
+ Parts render plain elements, so style them with `className`. Use `asChild` to apply a part's behavior to your own element:
130
+
131
+ ```tsx
132
+ <Tour.Next asChild>
133
+ <button className="rounded-md bg-blue-600 px-3 py-2 text-white">
134
+ Continue
135
+ </button>
136
+ </Tour.Next>
137
+ ```
138
+
139
+ The card and spotlight are separate layers. Give `Tour.Spotlight` a lower `z-index` than `Tour.Content`. The spotlight uses `currentColor`, so a text color with opacity controls the dimmed area. It does not block pointer events. State is exposed through attributes:
140
+
141
+ | Part | Attributes |
142
+ | --- | --- |
143
+ | `Tour.Content` | `data-step` (zero-based index), `data-side`, `data-align`, `data-state` |
144
+ | `Tour.Spotlight` | `data-tour-spotlight` on the overlay, `data-tour-cutout` on the cut-out |
145
+
146
+ The spotlight is colored with `currentColor`, so a `text-*` class sets the overlay color and opacity.
147
+
148
+ ### Animate the spotlight
149
+
150
+ The cut-out is an SVG rect, so you can make it glide between targets with CSS:
151
+
152
+ ```css
153
+ @media (prefers-reduced-motion: no-preference) {
154
+ [data-tour-cutout] {
155
+ transition:
156
+ x 360ms cubic-bezier(0.2, 0.8, 0.2, 1),
157
+ y 360ms cubic-bezier(0.2, 0.8, 0.2, 1),
158
+ width 360ms cubic-bezier(0.2, 0.8, 0.2, 1),
159
+ height 360ms cubic-bezier(0.2, 0.8, 0.2, 1);
160
+ }
161
+ }
162
+ ```
163
+
164
+ ### Label the last step
165
+
166
+ `useTour` reads the tour's state from inside `Tour.Root`:
167
+
168
+ ```tsx
169
+ function NextButton() {
170
+ const { isLast } = Tour.useTour();
171
+
172
+ return <Tour.Next>{isLast ? "Done" : "Next"}</Tour.Next>;
173
+ }
174
+ ```
175
+
176
+ ## API reference
177
+
178
+ ### `Tour.Root`
179
+
180
+ | Prop | Type | Description |
181
+ | --- | --- | --- |
182
+ | `steps` | `TourStep[]` | The steps, in order. |
183
+ | `open` | `boolean` | Whether the tour is showing. Opening always starts at the first step. |
184
+ | `onOpenChange` | `(open: boolean) => void` | Called with `false` when the last step is finished or the tour is closed. |
185
+
186
+ ### `TourStep`
187
+
188
+ | Field | Type | Description |
189
+ | --- | --- | --- |
190
+ | `id` | `string` | Unique id for the step. |
191
+ | `target` | `string` | CSS selector of the element to point at. |
192
+ | `title` | `ReactNode` | Rendered by `Tour.Title` when it has no children. |
193
+ | `description` | `ReactNode` | Rendered by `Tour.Description` when it has no children. |
194
+ | `side` | `"top" \| "right" \| "bottom" \| "left"` | Side of the target the card sits on. Defaults to `"bottom"`. |
195
+ | `align` | `"start" \| "center" \| "end"` | Alignment along that side. Defaults to `"center"`. |
196
+
197
+ ### Parts
198
+
199
+ | Part | Renders | Notes |
200
+ | --- | --- | --- |
201
+ | `Tour.Content` | Radix Popover content | Anchored to the current target. Accepts the Popover content props (`side`, `sideOffset`, `collisionPadding`, ...), which override the step's values. Renders nothing until the target exists. |
202
+ | `Tour.Arrow` | Radix Popover arrow | Place it inside `Tour.Content`. Fill it with a `fill-*` class. |
203
+ | `Tour.Spotlight` | `svg` | Props: `padding` (px around the target, default 8) and `radius` (corner radius, default 8). |
204
+ | `Tour.Title` | `h2` | Shows the step's `title` unless given children. |
205
+ | `Tour.Description` | `p` | Shows the step's `description` unless given children. |
206
+ | `Tour.Progress` | `span` | Shows `1 / 4` unless given children. |
207
+ | `Tour.Next` | `button` | Goes to the next step. On the last step, closes the tour. |
208
+ | `Tour.Previous` | `button` | Goes to the previous step. |
209
+ | `Tour.Close` | `button` | Closes the tour. |
210
+
211
+ Every part forwards its ref and accepts `asChild`. Event handlers run before the built-in behavior, and calling `event.preventDefault()` in `onClick` cancels it.
212
+
213
+ ### `useTour()`
214
+
215
+ Returns `{ steps, step, index, isFirst, isLast, next, previous, close }`. It throws if called outside `Tour.Root`.
216
+
217
+ ## Behavior
218
+
219
+ - The page stays interactive while the tour is open. Clicking outside the card does not close it.
220
+ - <kbd>Esc</kbd> closes the tour.
221
+ - Each step scrolls its target into view.
222
+ - The card is a Radix Popover, so it is exposed to assistive technology as a dialog. The spotlight is hidden from it.
223
+
224
+ Keyboard step navigation and focus return are tracked in [issue #2](https://github.com/bw3sley/radix-tour/issues/2).
225
+
226
+ ## Server Components
227
+
228
+ The build preserves `"use client"`, so a Server Component can import the package. Put state and event handlers, such as the `open` state in the quick start, in a Client Component.
229
+
230
+ ## License
231
+
232
+ License under the [MIT License](./LICENSE.md). If radix-tour helps your project, leave a feedback or ⭐ the repo.
@@ -0,0 +1,93 @@
1
+ import { Scope } from '@radix-ui/react-context';
2
+ import { Primitive } from '@radix-ui/react-primitive';
3
+ import * as PopoverPrimitive from "@radix-ui/react-popover";
4
+ import * as React from "react";
5
+ type ScopedProps<P> = P & {
6
+ __scopeTour?: Scope;
7
+ };
8
+ interface TourStep {
9
+ id: string;
10
+ /** CSS selector of the element this step points at. */
11
+ target: string;
12
+ title?: React.ReactNode;
13
+ description?: React.ReactNode;
14
+ /** Which side of the target the card sits on. Defaults to "bottom". */
15
+ side?: "top" | "right" | "bottom" | "left";
16
+ /** How the card aligns along that side. Defaults to "center". */
17
+ align?: "start" | "center" | "end";
18
+ }
19
+ declare const createTourScope: import('@radix-ui/react-context').CreateScope;
20
+ interface TourProps {
21
+ steps: TourStep[];
22
+ open: boolean;
23
+ onOpenChange: (open: boolean) => void;
24
+ children?: React.ReactNode;
25
+ }
26
+ declare function Tour(props: ScopedProps<TourProps>): React.JSX.Element | null;
27
+ declare namespace Tour {
28
+ var displayName: string;
29
+ }
30
+ /** Read the tour's state from inside `Tour.Root`, e.g. to label the last step's button "Done". */
31
+ declare function useTour(scope?: Scope): {
32
+ steps: TourStep[];
33
+ step: TourStep;
34
+ index: number;
35
+ isFirst: boolean;
36
+ isLast: boolean;
37
+ next: () => void;
38
+ previous: () => void;
39
+ close: () => void;
40
+ };
41
+ type PopoverContentProps = React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>;
42
+ interface TourContentProps extends PopoverContentProps {
43
+ }
44
+ /** Popover anchored to the current step's target. Renders nothing until the target exists. */
45
+ declare const TourContent: React.ForwardRefExoticComponent<TourContentProps & React.RefAttributes<HTMLDivElement>>;
46
+ type PopoverArrowProps = React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Arrow>;
47
+ interface TourArrowProps extends PopoverArrowProps {
48
+ }
49
+ /** Pointer from the card to its target. Place it inside `Tour.Content`; fill it with `fill-*`. */
50
+ declare const TourArrow: React.ForwardRefExoticComponent<TourArrowProps & React.RefAttributes<SVGSVGElement>>;
51
+ type PrimitiveSvgProps = React.ComponentPropsWithoutRef<typeof Primitive.svg>;
52
+ interface TourSpotlightProps extends PrimitiveSvgProps {
53
+ /** Extra space around the target, in px. */
54
+ padding?: number;
55
+ /** Corner radius of the cut-out, in px. */
56
+ radius?: number;
57
+ }
58
+ /** Dimmed overlay with a cut-out around the current target. Color it with `text-*` classes. */
59
+ declare const TourSpotlight: React.ForwardRefExoticComponent<TourSpotlightProps & React.RefAttributes<SVGSVGElement>>;
60
+ type PrimitiveHeadingProps = React.ComponentPropsWithoutRef<typeof Primitive.h2>;
61
+ interface TourTitleProps extends PrimitiveHeadingProps {
62
+ }
63
+ declare const TourTitle: React.ForwardRefExoticComponent<TourTitleProps & React.RefAttributes<HTMLHeadingElement>>;
64
+ type PrimitiveParagraphProps = React.ComponentPropsWithoutRef<typeof Primitive.p>;
65
+ interface TourDescriptionProps extends PrimitiveParagraphProps {
66
+ }
67
+ declare const TourDescription: React.ForwardRefExoticComponent<TourDescriptionProps & React.RefAttributes<HTMLParagraphElement>>;
68
+ type PrimitiveSpanProps = React.ComponentPropsWithoutRef<typeof Primitive.span>;
69
+ interface TourProgressProps extends PrimitiveSpanProps {
70
+ }
71
+ declare const TourProgress: React.ForwardRefExoticComponent<TourProgressProps & React.RefAttributes<HTMLSpanElement>>;
72
+ type PrimitiveButtonProps = React.ComponentPropsWithoutRef<typeof Primitive.button>;
73
+ interface TourNextProps extends PrimitiveButtonProps {
74
+ }
75
+ declare const TourNext: React.ForwardRefExoticComponent<TourNextProps & React.RefAttributes<HTMLButtonElement>>;
76
+ interface TourPreviousProps extends PrimitiveButtonProps {
77
+ }
78
+ declare const TourPrevious: React.ForwardRefExoticComponent<TourPreviousProps & React.RefAttributes<HTMLButtonElement>>;
79
+ interface TourCloseProps extends PrimitiveButtonProps {
80
+ }
81
+ declare const TourClose: React.ForwardRefExoticComponent<TourCloseProps & React.RefAttributes<HTMLButtonElement>>;
82
+ declare const Root: typeof Tour;
83
+ declare const Content: React.ForwardRefExoticComponent<TourContentProps & React.RefAttributes<HTMLDivElement>>;
84
+ declare const Arrow: React.ForwardRefExoticComponent<TourArrowProps & React.RefAttributes<SVGSVGElement>>;
85
+ declare const Spotlight: React.ForwardRefExoticComponent<TourSpotlightProps & React.RefAttributes<SVGSVGElement>>;
86
+ declare const Title: React.ForwardRefExoticComponent<TourTitleProps & React.RefAttributes<HTMLHeadingElement>>;
87
+ declare const Description: React.ForwardRefExoticComponent<TourDescriptionProps & React.RefAttributes<HTMLParagraphElement>>;
88
+ declare const Progress: React.ForwardRefExoticComponent<TourProgressProps & React.RefAttributes<HTMLSpanElement>>;
89
+ declare const Next: React.ForwardRefExoticComponent<TourNextProps & React.RefAttributes<HTMLButtonElement>>;
90
+ declare const Previous: React.ForwardRefExoticComponent<TourPreviousProps & React.RefAttributes<HTMLButtonElement>>;
91
+ declare const Close: React.ForwardRefExoticComponent<TourCloseProps & React.RefAttributes<HTMLButtonElement>>;
92
+ export type { TourArrowProps, TourCloseProps, TourContentProps, TourDescriptionProps, TourNextProps, TourPreviousProps, TourProgressProps, TourProps, TourSpotlightProps, TourStep, TourTitleProps, };
93
+ export { Arrow, Close, Content, createTourScope, Description, Next, Previous, Progress, Root, Spotlight, Title, Tour, TourArrow, TourClose, TourContent, TourDescription, TourNext, TourPrevious, TourProgress, TourSpotlight, TourTitle, useTour, };
@@ -0,0 +1,2 @@
1
+ /** Tracks an element's viewport rect across scroll, resize and layout shifts. */
2
+ export declare function useRect(element: Element | null): DOMRect | null;
@@ -0,0 +1,2 @@
1
+ /** Resolves a CSS selector to an element, waiting for it to appear in the DOM. */
2
+ export declare function useTarget(selector: string | undefined): Element | null;
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ "use client";Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,s)=>(s=n==null?{}:e(i(n)),o(r||!n||!n.__esModule||!a.call(n,`default`)?t(s,`default`,{value:n,enumerable:!0}):s,n));let c=require("@radix-ui/primitive"),l=require("@radix-ui/react-context"),u=require("@radix-ui/react-popover");u=s(u,1);let d=require("@radix-ui/react-primitive"),f=require("react");f=s(f,1);let p=require("react/jsx-runtime");function m(e){let[t,n]=(0,f.useState)(null);return(0,f.useEffect)(()=>{if(!e){n(null);return}let t=e,r=0;function i(){cancelAnimationFrame(r),r=requestAnimationFrame(()=>n(t.getBoundingClientRect()))}i();let a=new ResizeObserver(i);return a.observe(t),window.addEventListener(`scroll`,i,!0),window.addEventListener(`resize`,i),()=>{cancelAnimationFrame(r),a.disconnect(),window.removeEventListener(`scroll`,i,!0),window.removeEventListener(`resize`,i)}},[e]),t}function h(e){let[t,n]=(0,f.useState)(null);return(0,f.useEffect)(()=>{if(!e){n(null);return}let t=document.querySelector(e);if(n(t),t)return;let r=new MutationObserver(()=>{let t=document.querySelector(e);t&&(n(t),r.disconnect())});return r.observe(document.body,{childList:!0,subtree:!0}),()=>r.disconnect()},[e]),t}var g=`Tour`,[_,v]=(0,l.createContextScope)(g),[y,b]=_(g);function x(e){let{__scopeTour:t,steps:n,open:r,onOpenChange:i,children:a}=e,[o,s]=f.useState(0),c=n[o],l=h(r?c?.target:void 0);if(f.useEffect(()=>{r&&s(0)},[r]),f.useEffect(()=>{l?.scrollIntoView({block:`center`,behavior:`smooth`})},[l]),!r||!c)return null;let u=o===n.length-1;function d(){if(u){i(!1);return}s(o+1)}return(0,p.jsx)(y,{scope:t,steps:n,index:o,step:c,target:l,onNext:d,onPrevious:()=>s(Math.max(0,o-1)),onClose:()=>i(!1),children:a})}x.displayName=g;function S(e){let t=b(`useTour`,e);return{steps:t.steps,step:t.step,index:t.index,isFirst:t.index===0,isLast:t.index===t.steps.length-1,next:t.onNext,previous:t.onPrevious,close:t.onClose}}var C=`TourContent`,w=f.forwardRef((e,t)=>{let{__scopeTour:n,...r}=e,i=b(C,n),a=f.useRef(null);return a.current=i.target,i.target?(0,p.jsxs)(u.Root,{open:!0,children:[(0,p.jsx)(u.Anchor,{virtualRef:a}),(0,p.jsx)(u.Portal,{children:(0,p.jsx)(u.Content,{side:i.step.side??`bottom`,align:i.step.align??`center`,sideOffset:14,collisionPadding:12,"data-step":i.index,...r,ref:t,onEscapeKeyDown:(0,c.composeEventHandlers)(r.onEscapeKeyDown,i.onClose),onInteractOutside:(0,c.composeEventHandlers)(r.onInteractOutside,e=>e.preventDefault())})})]}):null});w.displayName=C;var T=`TourArrow`,E=f.forwardRef((e,t)=>{let{__scopeTour:n,...r}=e;return b(T,n),(0,p.jsx)(u.Arrow,{...r,ref:t})});E.displayName=T;var D=`TourSpotlight`,O=f.forwardRef((e,t)=>{let{__scopeTour:n,padding:r=8,radius:i=8,style:a,...o}=e,s=m(b(D,n).target),c=`tour-mask-${f.useId().replace(/:/g,``)}`;return s?(0,p.jsxs)(d.Primitive.svg,{"aria-hidden":!0,"data-tour-spotlight":``,...o,ref:t,style:{position:`fixed`,inset:0,width:`100%`,height:`100%`,pointerEvents:`none`,...a},children:[(0,p.jsx)(`defs`,{children:(0,p.jsxs)(`mask`,{id:c,children:[(0,p.jsx)(`rect`,{width:`100%`,height:`100%`,fill:`white`}),(0,p.jsx)(`rect`,{"data-tour-cutout":``,x:s.x-r,y:s.y-r,width:s.width+r*2,height:s.height+r*2,rx:i,fill:`black`})]})}),(0,p.jsx)(`rect`,{width:`100%`,height:`100%`,fill:`currentColor`,mask:`url(#${c})`})]}):null});O.displayName=D;var k=`TourTitle`,A=f.forwardRef((e,t)=>{let{__scopeTour:n,children:r,...i}=e,a=b(k,n);return(0,p.jsx)(d.Primitive.h2,{...i,ref:t,children:r??a.step.title})});A.displayName=k;var j=`TourDescription`,M=f.forwardRef((e,t)=>{let{__scopeTour:n,children:r,...i}=e,a=b(j,n);return(0,p.jsx)(d.Primitive.p,{...i,ref:t,children:r??a.step.description})});M.displayName=j;var N=`TourProgress`,P=f.forwardRef((e,t)=>{let{__scopeTour:n,children:r,...i}=e,a=b(N,n);return(0,p.jsx)(d.Primitive.span,{...i,ref:t,children:r??`${a.index+1} / ${a.steps.length}`})});P.displayName=N;var F=`TourNext`,I=f.forwardRef((e,t)=>{let{__scopeTour:n,...r}=e,i=b(F,n);return(0,p.jsx)(d.Primitive.button,{type:`button`,...r,ref:t,onClick:(0,c.composeEventHandlers)(e.onClick,i.onNext)})});I.displayName=F;var L=`TourPrevious`,R=f.forwardRef((e,t)=>{let{__scopeTour:n,...r}=e,i=b(L,n);return(0,p.jsx)(d.Primitive.button,{type:`button`,...r,ref:t,onClick:(0,c.composeEventHandlers)(e.onClick,i.onPrevious)})});R.displayName=L;var z=`TourClose`,B=f.forwardRef((e,t)=>{let{__scopeTour:n,...r}=e,i=b(z,n);return(0,p.jsx)(d.Primitive.button,{type:`button`,...r,ref:t,onClick:(0,c.composeEventHandlers)(e.onClick,i.onClose)})});B.displayName=z;var V=x,H=w,U=E,W=O,G=A,K=M,q=P,J=I,Y=R,X=B;exports.Arrow=U,exports.Close=X,exports.Content=H,exports.Description=K,exports.Next=J,exports.Previous=Y,exports.Progress=q,exports.Root=V,exports.Spotlight=W,exports.Title=G,exports.Tour=x,exports.TourArrow=E,exports.TourClose=B,exports.TourContent=w,exports.TourDescription=M,exports.TourNext=I,exports.TourPrevious=R,exports.TourProgress=P,exports.TourSpotlight=O,exports.TourTitle=A,exports.createTourScope=v,exports.useTour=S;
@@ -0,0 +1,2 @@
1
+ export type { TourArrowProps, TourCloseProps, TourContentProps, TourDescriptionProps, TourNextProps, TourPreviousProps, TourProgressProps, TourProps, TourSpotlightProps, TourStep, TourTitleProps, } from './components/tour';
2
+ export { Arrow, Close, Content, createTourScope, Description, Next, Previous, Progress, Root, Spotlight, Title, Tour, TourArrow, TourClose, TourContent, TourDescription, TourNext, TourPrevious, TourProgress, TourSpotlight, TourTitle, useTour, } from './components/tour';
package/dist/index.js ADDED
@@ -0,0 +1,220 @@
1
+ "use client";
2
+ import { composeEventHandlers as e } from "@radix-ui/primitive";
3
+ import { createContextScope as t } from "@radix-ui/react-context";
4
+ import * as n from "@radix-ui/react-popover";
5
+ import { Primitive as r } from "@radix-ui/react-primitive";
6
+ import * as i from "react";
7
+ import { useEffect as a, useState as o } from "react";
8
+ import { jsx as s, jsxs as c } from "react/jsx-runtime";
9
+ //#region src/hooks/use-rect.ts
10
+ function l(e) {
11
+ let [t, n] = o(null);
12
+ return a(() => {
13
+ if (!e) {
14
+ n(null);
15
+ return;
16
+ }
17
+ let t = e, r = 0;
18
+ function i() {
19
+ cancelAnimationFrame(r), r = requestAnimationFrame(() => n(t.getBoundingClientRect()));
20
+ }
21
+ i();
22
+ let a = new ResizeObserver(i);
23
+ return a.observe(t), window.addEventListener("scroll", i, !0), window.addEventListener("resize", i), () => {
24
+ cancelAnimationFrame(r), a.disconnect(), window.removeEventListener("scroll", i, !0), window.removeEventListener("resize", i);
25
+ };
26
+ }, [e]), t;
27
+ }
28
+ //#endregion
29
+ //#region src/hooks/use-target.ts
30
+ function u(e) {
31
+ let [t, n] = o(null);
32
+ return a(() => {
33
+ if (!e) {
34
+ n(null);
35
+ return;
36
+ }
37
+ let t = document.querySelector(e);
38
+ if (n(t), t) return;
39
+ let r = new MutationObserver(() => {
40
+ let t = document.querySelector(e);
41
+ t && (n(t), r.disconnect());
42
+ });
43
+ return r.observe(document.body, {
44
+ childList: !0,
45
+ subtree: !0
46
+ }), () => r.disconnect();
47
+ }, [e]), t;
48
+ }
49
+ //#endregion
50
+ //#region src/components/tour.tsx
51
+ var d = "Tour", [f, p] = t(d), [m, h] = f(d);
52
+ function g(e) {
53
+ let { __scopeTour: t, steps: n, open: r, onOpenChange: a, children: o } = e, [c, l] = i.useState(0), d = n[c], f = u(r ? d?.target : void 0);
54
+ if (i.useEffect(() => {
55
+ r && l(0);
56
+ }, [r]), i.useEffect(() => {
57
+ f?.scrollIntoView({
58
+ block: "center",
59
+ behavior: "smooth"
60
+ });
61
+ }, [f]), !r || !d) return null;
62
+ let p = c === n.length - 1;
63
+ function h() {
64
+ if (p) {
65
+ a(!1);
66
+ return;
67
+ }
68
+ l(c + 1);
69
+ }
70
+ return /* @__PURE__ */ s(m, {
71
+ scope: t,
72
+ steps: n,
73
+ index: c,
74
+ step: d,
75
+ target: f,
76
+ onNext: h,
77
+ onPrevious: () => l(Math.max(0, c - 1)),
78
+ onClose: () => a(!1),
79
+ children: o
80
+ });
81
+ }
82
+ g.displayName = d;
83
+ function _(e) {
84
+ let t = h("useTour", e);
85
+ return {
86
+ steps: t.steps,
87
+ step: t.step,
88
+ index: t.index,
89
+ isFirst: t.index === 0,
90
+ isLast: t.index === t.steps.length - 1,
91
+ next: t.onNext,
92
+ previous: t.onPrevious,
93
+ close: t.onClose
94
+ };
95
+ }
96
+ var v = "TourContent", y = i.forwardRef((t, r) => {
97
+ let { __scopeTour: a, ...o } = t, l = h(v, a), u = i.useRef(null);
98
+ return u.current = l.target, l.target ? /* @__PURE__ */ c(n.Root, {
99
+ open: !0,
100
+ children: [/* @__PURE__ */ s(n.Anchor, { virtualRef: u }), /* @__PURE__ */ s(n.Portal, { children: /* @__PURE__ */ s(n.Content, {
101
+ side: l.step.side ?? "bottom",
102
+ align: l.step.align ?? "center",
103
+ sideOffset: 14,
104
+ collisionPadding: 12,
105
+ "data-step": l.index,
106
+ ...o,
107
+ ref: r,
108
+ onEscapeKeyDown: e(o.onEscapeKeyDown, l.onClose),
109
+ onInteractOutside: e(o.onInteractOutside, (e) => e.preventDefault())
110
+ }) })]
111
+ }) : null;
112
+ });
113
+ y.displayName = v;
114
+ var b = "TourArrow", x = i.forwardRef((e, t) => {
115
+ let { __scopeTour: r, ...i } = e;
116
+ return h(b, r), /* @__PURE__ */ s(n.Arrow, {
117
+ ...i,
118
+ ref: t
119
+ });
120
+ });
121
+ x.displayName = b;
122
+ var S = "TourSpotlight", C = i.forwardRef((e, t) => {
123
+ let { __scopeTour: n, padding: a = 8, radius: o = 8, style: u, ...d } = e, f = l(h(S, n).target), p = `tour-mask-${i.useId().replace(/:/g, "")}`;
124
+ return f ? /* @__PURE__ */ c(r.svg, {
125
+ "aria-hidden": !0,
126
+ "data-tour-spotlight": "",
127
+ ...d,
128
+ ref: t,
129
+ style: {
130
+ position: "fixed",
131
+ inset: 0,
132
+ width: "100%",
133
+ height: "100%",
134
+ pointerEvents: "none",
135
+ ...u
136
+ },
137
+ children: [/* @__PURE__ */ s("defs", { children: /* @__PURE__ */ c("mask", {
138
+ id: p,
139
+ children: [/* @__PURE__ */ s("rect", {
140
+ width: "100%",
141
+ height: "100%",
142
+ fill: "white"
143
+ }), /* @__PURE__ */ s("rect", {
144
+ "data-tour-cutout": "",
145
+ x: f.x - a,
146
+ y: f.y - a,
147
+ width: f.width + a * 2,
148
+ height: f.height + a * 2,
149
+ rx: o,
150
+ fill: "black"
151
+ })]
152
+ }) }), /* @__PURE__ */ s("rect", {
153
+ width: "100%",
154
+ height: "100%",
155
+ fill: "currentColor",
156
+ mask: `url(#${p})`
157
+ })]
158
+ }) : null;
159
+ });
160
+ C.displayName = S;
161
+ var w = "TourTitle", T = i.forwardRef((e, t) => {
162
+ let { __scopeTour: n, children: i, ...a } = e, o = h(w, n);
163
+ return /* @__PURE__ */ s(r.h2, {
164
+ ...a,
165
+ ref: t,
166
+ children: i ?? o.step.title
167
+ });
168
+ });
169
+ T.displayName = w;
170
+ var E = "TourDescription", D = i.forwardRef((e, t) => {
171
+ let { __scopeTour: n, children: i, ...a } = e, o = h(E, n);
172
+ return /* @__PURE__ */ s(r.p, {
173
+ ...a,
174
+ ref: t,
175
+ children: i ?? o.step.description
176
+ });
177
+ });
178
+ D.displayName = E;
179
+ var O = "TourProgress", k = i.forwardRef((e, t) => {
180
+ let { __scopeTour: n, children: i, ...a } = e, o = h(O, n);
181
+ return /* @__PURE__ */ s(r.span, {
182
+ ...a,
183
+ ref: t,
184
+ children: i ?? `${o.index + 1} / ${o.steps.length}`
185
+ });
186
+ });
187
+ k.displayName = O;
188
+ var A = "TourNext", j = i.forwardRef((t, n) => {
189
+ let { __scopeTour: i, ...a } = t, o = h(A, i);
190
+ return /* @__PURE__ */ s(r.button, {
191
+ type: "button",
192
+ ...a,
193
+ ref: n,
194
+ onClick: e(t.onClick, o.onNext)
195
+ });
196
+ });
197
+ j.displayName = A;
198
+ var M = "TourPrevious", N = i.forwardRef((t, n) => {
199
+ let { __scopeTour: i, ...a } = t, o = h(M, i);
200
+ return /* @__PURE__ */ s(r.button, {
201
+ type: "button",
202
+ ...a,
203
+ ref: n,
204
+ onClick: e(t.onClick, o.onPrevious)
205
+ });
206
+ });
207
+ N.displayName = M;
208
+ var P = "TourClose", F = i.forwardRef((t, n) => {
209
+ let { __scopeTour: i, ...a } = t, o = h(P, i);
210
+ return /* @__PURE__ */ s(r.button, {
211
+ type: "button",
212
+ ...a,
213
+ ref: n,
214
+ onClick: e(t.onClick, o.onClose)
215
+ });
216
+ });
217
+ F.displayName = P;
218
+ var I = g, L = y, R = x, z = C, B = T, V = D, H = k, U = j, W = N, G = F;
219
+ //#endregion
220
+ export { R as Arrow, G as Close, L as Content, V as Description, U as Next, W as Previous, H as Progress, I as Root, z as Spotlight, B as Title, g as Tour, x as TourArrow, F as TourClose, y as TourContent, D as TourDescription, j as TourNext, N as TourPrevious, k as TourProgress, C as TourSpotlight, T as TourTitle, p as createTourScope, _ as useTour };
package/package.json CHANGED
@@ -1,6 +1,95 @@
1
1
  {
2
2
  "name": "radix-tour",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Composable product tours built on Radix primitives, styled with Tailwind.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "main": "./dist/index.cjs",
12
+ "module": "./dist/index.js",
13
+ "types": "./dist/index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "import": "./dist/index.js",
18
+ "require": "./dist/index.cjs"
19
+ }
20
+ },
21
+ "scripts": {
22
+ "build": "vite build",
23
+ "format": "biome format --write .",
24
+ "check:fix": "biome check --write .",
25
+ "test": "vitest run",
26
+ "test:watch": "vitest",
27
+ "test:coverage": "vitest run --coverage",
28
+ "storybook": "storybook dev -p 6006",
29
+ "build-storybook": "storybook build",
30
+ "changeset": "changeset",
31
+ "version": "changeset version",
32
+ "release": "changeset publish",
33
+ "prepublishOnly": "tsc --noEmit && biome check . && vitest run && vite build"
34
+ },
35
+ "peerDependencies": {
36
+ "react": "^18.0.0 || ^19.0.0",
37
+ "react-dom": "^18.0.0 || ^19.0.0"
38
+ },
39
+ "dependencies": {
40
+ "@radix-ui/primitive": "^1.1.7",
41
+ "@radix-ui/react-context": "^1.2.2",
42
+ "@radix-ui/react-popover": "^1.1.23",
43
+ "@radix-ui/react-primitive": "^2.1.10"
44
+ },
45
+ "devDependencies": {
46
+ "@biomejs/biome": "2.5.15",
47
+ "@changesets/cli": "^3.0.3",
48
+ "@storybook/addon-a11y": "^10.6.1",
49
+ "@storybook/react-vite": "^10.6.1",
50
+ "@tailwindcss/vite": "^4.3.3",
51
+ "@testing-library/jest-dom": "^7.0.1",
52
+ "@testing-library/react": "^16.3.3",
53
+ "@testing-library/user-event": "^14.6.7",
54
+ "@types/node": "^26.6.4",
55
+ "@types/react": "^19.0.0",
56
+ "@types/react-dom": "^19.0.0",
57
+ "@vitejs/plugin-react": "^5.0.0",
58
+ "@vitest/coverage-v8": "^5.0.3",
59
+ "jsdom": "^29.1.1",
60
+ "lucide-react": "^1.52.0",
61
+ "react": "^19.3.0",
62
+ "react-dom": "^19.3.0",
63
+ "storybook": "^10.6.1",
64
+ "tailwindcss": "^4.3.3",
65
+ "typescript": "^5.6.0",
66
+ "vite": "^8.3.2",
67
+ "vite-plugin-dts": "^5.1.2",
68
+ "vitest": "^5.0.3"
69
+ },
70
+ "author": "Wesley Júnior",
71
+ "repository": {
72
+ "type": "git",
73
+ "url": "git+https://github.com/bw3sley/radix-tour.git"
74
+ },
75
+ "homepage": "https://github.com/bw3sley/radix-tour#readme",
76
+ "bugs": {
77
+ "url": "https://github.com/bw3sley/radix-tour/issues"
78
+ },
79
+ "publishConfig": {
80
+ "access": "public",
81
+ "provenance": true
82
+ },
83
+ "keywords": [
84
+ "react",
85
+ "tour",
86
+ "product-tour",
87
+ "onboarding",
88
+ "walkthrough",
89
+ "radix-ui",
90
+ "tailwindcss",
91
+ "headless",
92
+ "unstyled",
93
+ "accessibility"
94
+ ]
95
+ }