22
33``` yaml
44name : next-easytour
5- version : 0.3.0-alpha.3
6- branch : point-three
5+ version : 0.3.0
76kind : react-component-library
87language : typescript
98module_type : esm
10- install : npm install next-easytour@next
9+ install : npm install next-easytour
1110peer_deps :
1211 react : " >=18.0.0"
1312 react-dom : " >=18.0.0"
1413import_path : next-easytour
1514stylesheet_path : next-easytour/styles.css
16- main_exports : [Tutorial, Card, Arrow, Spotlight, Circles, Tooltip, Editor, EditorHandles]
17- main_hooks : [useTutorial, useTutorialTarget, useEditorState, useCardRect]
15+ main_exports :
16+ - Tutorial # headless provider (required)
17+ - Card # step card (required)
18+ - Arrow # Bézier/straight arrow overlay
19+ - Spotlight # dimmer with target cutouts
20+ - Circles # annotated ellipses on target
21+ - Labels # text annotations with animation
22+ - Tooltip # lightweight popover (no nav controls)
23+ - TriggerButton # tutorial start button (annoying/default modes)
24+ - Editor # authoring wrapper
25+ - EditorHandles # drag handles for card/arrow
26+ - EditorPanel # Figma-style sidebar editor
27+ main_hooks :
28+ - useTutorial # navigation API inside <Tutorial>
29+ - useTutorialTarget # register DOM element as named target
30+ - useTutorialDone # cookie-based completion tracking
31+ - useEditorState # editor state (active, unsavedCount, save)
32+ - useCardRect # card bounding rect
1833requires_client_component : true
1934requires_app_router : false
2035framework_agnostic : true
2136ssr_safe : true
22- bundler : any
2337css_prefix : eto-
2438css_var_prefix : --eto-
2539` ` `
2640
27- ## Minimum viable setup (CSS-selector targeting — no code changes)
41+ ---
42+
43+ ## Quick start — Next.js App Router
44+
45+ ` ` ` tsx
46+ // app/layout.tsx
47+ import "next-easytour/styles.css";
48+ export default function RootLayout({ children } : { children: React.ReactNode }) {
49+ return <html lang="en"><body>{children}</body></html>;
50+ }
51+ ```
2852
2953``` tsx
54+ // app/page.tsx
3055" use client" ;
3156import { useState } from " react" ;
32- import { Tutorial, Card, Arrow, Spotlight, type Step } from "next-easytour";
57+ import { Tutorial , Card , Arrow , Spotlight , targetPoint , type Step } from " next-easytour" ;
3358
3459const steps: Step [] = [
35- { id: "welcome", title: "Hi ", body: "Welcome to the app.", autoAdvance: 3000 },
60+ { id: " welcome" , title: " Welcome! " , body: " Let me show you around. " },
3661 {
37- id : " save" ,
38- title : " Save" ,
39- body : " Click here to save." ,
40- selector : " #save-button" , // ← CSS selector, no hook needed
41- scrollIntoView : true,
62+ id: " search" ,
63+ title: " Search" ,
64+ body: " Type here to find anything." ,
65+ selector: " #search-input" ,
4266 highlight: true ,
43- annotations : {
44- spotlight : true,
45- arrow : { to: { space: "target", x: 50, y: 50 } },
46- },
47- waitFor : { type: "click" }, // wait for user to click the target
67+ annotations: { spotlight: true , arrow: { to: targetPoint (50 , 50 ) } },
68+ waitFor: { type: " input" , pattern: " \\ S+" },
4869 },
4970];
5071
@@ -53,27 +74,95 @@ export default function Page() {
5374 return (
5475 <>
5576 <button onClick = { () => setStepId (" welcome" )} >Start tour</button >
56- <button id="save-button">Save</button>
77+ <input id = " search-input" placeholder = " Search..." />
78+ <Tutorial steps = { steps } stepId = { stepId } onStepChange = { setStepId } >
79+ <Spotlight />
80+ <Arrow />
81+ <Card />
82+ </Tutorial >
83+ </>
84+ );
85+ }
86+ ```
87+
88+ ## Quick start — Vite + React
89+
90+ ``` tsx
91+ // src/main.tsx
92+ import " next-easytour/styles.css" ;
93+ import App from " ./App" ;
94+ ReactDOM .createRoot (document .getElementById (" root" )! ).render (<App />);
95+ ```
96+
97+ ``` tsx
98+ // src/App.tsx (no "use client" needed)
99+ import { useState } from " react" ;
100+ import { Tutorial , Card , Arrow , Spotlight , targetPoint , type Step } from " next-easytour" ;
101+
102+ const steps: Step [] = [
103+ { id: " welcome" , title: " Welcome!" , body: " Let me show you around." },
104+ {
105+ id: " btn" ,
106+ title: " Click me" ,
107+ body: " Click the button to continue." ,
108+ selector: " #my-button" ,
109+ highlight: true ,
110+ annotations: { spotlight: true , arrow: { to: targetPoint (50 , 50 ) } },
111+ waitFor: { type: " click" },
112+ },
113+ ];
114+
115+ export default function App() {
116+ const [stepId, setStepId] = useState <string | null >(null );
117+ return (
118+ <>
119+ <button onClick = { () => setStepId (" welcome" )} >Start tour</button >
120+ <button id = " my-button" >My button</button >
57121 <Tutorial steps = { steps } stepId = { stepId } onStepChange = { setStepId } >
58122 <Spotlight />
59123 <Arrow />
60- <Card variant="branded" logo="/logo.svg" />
124+ <Card />
61125 </Tutorial >
62126 </>
63127 );
64128}
65129```
66130
67- ## Two targeting modes
131+ ---
132+
133+ ## Targeting
68134
69135| Mode | When to use | Example |
70136| ---| ---| ---|
71- | ` selector: "#foo" ` | Target exists in host markup, no code change wanted | ` { selector: "#nav-search" } ` |
72- | ` useTutorialTarget("foo") ` | Target is dynamic / conditionally rendered | ` const ref = useTutorialTarget("row-1") ` |
137+ | ` selector: "#foo" ` | Target exists in host markup | ` { selector: "#nav-search" } ` |
138+ | ` useTutorialTarget("foo") ` | Target is dynamic / conditional | ` const ref = useTutorialTarget("row-1") ` |
73139
74140Hook targets take precedence when both are present.
75141
76- ## Step actions (fire on step enter)
142+ ## Step fields
143+
144+ | Field | Required | Description |
145+ | ---| ---| ---|
146+ | ` id ` | ** yes** | Unique string |
147+ | ` title ` | no | Card heading |
148+ | ` body ` | no | Card body text |
149+ | ` content ` | no | JSX body (overrides ` body ` ) |
150+ | ` selector ` | no | CSS selector for target |
151+ | ` targets ` | no | Hook-registered target IDs |
152+ | ` annotations.arrow ` | no | ` { to: targetPoint(x, y), style?, label? } ` |
153+ | ` annotations.spotlight ` | no | Boolean — dim everything except target |
154+ | ` annotations.circles ` | no | ` Circle[] ` — ellipses on target |
155+ | ` annotations.labels ` | no | ` TextLabel[] ` — text annotations |
156+ | ` scrollIntoView ` | no | ` true ` or ` ScrollIntoViewOptions ` |
157+ | ` actions ` | no | Ordered side-effects on enter |
158+ | ` waitFor ` | no | Block Next until condition met |
159+ | ` autoAdvance ` | no | Auto-advance after N ms |
160+ | ` highlight ` | no | ` true ` or ` { pulse, color, padding } ` |
161+ | ` transition ` | no | ` { enter: "fade" \| "fade-slide" \| "scale" \| "none" } ` |
162+ | ` cardAnchor ` | no | ` viewportAnchor(x, y) ` — card position |
163+ | ` meta ` | no | Host-owned typed metadata |
164+
165+ ## Actions (fire on step enter)
77166
78167``` tsx
79168actions : [
@@ -82,64 +171,118 @@ actions: [
82171 { type: " click" , selector: " .expand-btn" },
83172 { type: " highlight" , pulse: true , duration: 2000 },
84173 { type: " add-class" , selector: " .sidebar" , className: " ring-2" },
174+ { type: " remove-class" , selector: " .sidebar" , className: " hidden" },
85175 { type: " dispatch" , event: " tour:demo" , detail: { mode: " dark" } },
86176 { type: " focus" , selector: " #email-input" },
87177]
88178```
89179
90- ## WaitFor conditions (block Next until met)
180+ ## WaitFor conditions
91181
92182``` tsx
93- waitFor : { type : " click" } // click on step target
94- waitFor : { type : " click" , selector : " .submit" } // click on specific element
95- waitFor : { type : " input" , selector : " #name" , pattern : " \\ S+" } // non-empty input
96- waitFor : { type : " event" , name : " modal:closed" } // custom DOM event
97- waitFor : { type : " delay" , ms : 3000 } // time delay
98- waitFor : { type : " visible" , selector : " .result" } // element appears
99- waitFor : { type : " custom" , predicate : () => count > 5 } // custom function
183+ waitFor : { type : " click" } // click on target
184+ waitFor : { type : " click" , selector : " .submit" } // click on element
185+ waitFor : { type : " input" , selector : " #name" , pattern : " \\ S+" } // regex match
186+ waitFor : { type : " event" , name : " modal:closed" } // custom DOM event
187+ waitFor : { type : " delay" , ms : 3000 } // time delay
188+ waitFor : { type : " visible" , selector : " .result" } // element appears
189+ waitFor : { type : " custom" , predicate : () => count > 5 } // poll function
100190```
101191
102- ## Key fields reference
192+ ## Coordinates — DO NOT MIX
103193
104- | Field | Required? | Notes |
105- | ---| ---| ---|
106- | ` id ` | ** yes** | Unique string per step |
107- | ` title ` | no | Card heading |
108- | ` body ` | no | Card body text |
109- | ` content ` | no | JSX body (overrides ` body ` ) |
110- | ` selector ` | no | CSS selector for target |
111- | ` targets ` | no | Hook-registered target IDs |
112- | ` annotations.arrow ` | no | Arrow to target |
113- | ` annotations.spotlight ` | no | Dim everything except target |
114- | ` annotations.circles ` | no | Ellipses on target |
115- | ` scrollIntoView ` | no | Scroll target into view |
116- | ` actions ` | no | Side-effects on step enter |
117- | ` waitFor ` | no | Block Next until condition met |
118- | ` autoAdvance ` | no | Auto-advance after N ms |
119- | ` highlight ` | no | Pulsing ring on target |
120- | ` transition ` | no | Card animation config |
121- | ` cardAnchor ` | no | Card position override |
122- | ` meta ` | no | Host-owned typed metadata |
194+ ``` ts
195+ targetPoint (50 , 50 ) // { space: "target", x: 50, y: 50 } — % of target rect
196+ viewportAnchor (10 , 20 ) // { space: "viewport", x: 10, y: 20 } — % of viewport (fixed) or px (absolute)
197+ ```
123198
124- ## Coordinate systems — DO NOT MIX
199+ ## Custom card via render-prop
125200
126- ``` ts
127- type TargetPoint = { space: " target" ; x: number ; y: number }; // % of target rect
128- type ViewportAnchor = { space: " viewport" ; x: number ; y: number }; // % of viewport
201+ ``` tsx
202+ <Card >
203+ { ({ step , index , total , isFirst , isLast , canAdvance , isWaiting , stableMinHeight , next , prev , close }) => (
204+ <div className = " my-card" style = { { minHeight: stableMinHeight }} >
205+ <img src = " /logo.svg" alt = " Logo" />
206+ <h3 >{ step .title } </h3 >
207+ <p >{ step .body } </p >
208+ <footer >
209+ <button onClick = { prev } disabled = { isFirst } >Back</button >
210+ <span >{ index + 1 } / { total } </span >
211+ <button onClick = { next } disabled = { ! canAdvance } >
212+ { isLast ? " Done" : " Next" }
213+ </button >
214+ </footer >
215+ </div >
216+ )}
217+ </Card >
218+ ```
219+
220+ ` stableMinHeight ` is the max height across all steps — apply as ` minHeight ` for uniform card sizing with no jumping.
221+
222+ ## TriggerButton + completion tracking
223+
224+ ``` tsx
225+ import { TriggerButton , useTutorialDone } from " next-easytour" ;
226+
227+ // OUTSIDE <Tutorial>:
228+ const { done, markDone } = useTutorialDone (" my-tutorial" );
229+
230+ <TriggerButton
231+ onClick = { () => setStepId (steps [0 ].id )}
232+ text = " Take the tour"
233+ mode = " annoying" // "annoying" = flashy until done; "default" = always calm
234+ done = { done }
235+ />
236+
237+ // Mark complete on last step:
238+ <Tutorial
239+ onStepEnter = { (step ) => {
240+ if (step .id === steps [steps .length - 1 ].id ) markDone ();
241+ }}
242+ ...
243+ >
244+ ```
245+
246+ ## Theming
247+
248+ ```tsx
249+ <Tutorial theme = { { accent: " #7c3aed" , surface: " #fafafa" , cardRadius: " 16px" }} ...>
250+ ```
251+
252+ Or CSS:
253+ ```css
254+ :root { -- eto - accent : #262262 ; -- eto - surface : var (-- background ); }
255+ .dark { -- eto - accent : #60a5fa ; -- eto - surface : #18181b ; }
256+ ```
257+
258+ Properties: `accent`, `surface`, `fg`, `muted`, `mutedSoft`, `border`, `borderSoft`, `hoverBg`, `arrowColor`, `arrowOpacity`, `cardWidth`, `cardRadius`.
259+
260+ ## Editor
261+
262+ ```tsx
263+ <Editor steps = { baseSteps } canEdit = { isAdmin } triggerConfig = { { text: " Start" , mode: " annoying" }}
264+ onSave = { async (steps , opts ) => { await saveToServer (steps , opts ?.triggerConfig ); }} >
265+ { ({ steps }) => (
266+ <Tutorial steps = { steps } stepId = { stepId } onStepChange = { setStepId } >
267+ <Card /><Arrow /><Spotlight />
268+ { isAdmin && <EditorHandles />}
269+ { isAdmin && <EditorPanel />}
270+ </Tutorial >
271+ )}
272+ </Editor >
129273```
130274
131- ## Lifecycle callbacks
275+ ## Lifecycle
132276
133277```
134- goto("A") → onOpen, onStepEnter(A)
135- actions execute, scrollIntoView fires
136- waitFor starts watching
137- goto("B") → onStepLeave(A), onStepEnter(B)
138- close() → onStepLeave(B), onClose
278+ setStepId("a") → onOpen, onStepEnter(a), actions, scrollIntoView, waitFor
279+ setStepId("b") → onStepLeave(a), onStepEnter(b)
280+ setStepId(null) → onStepLeave(b), onClose
139281```
140282
141283## Does NOT do
142284
143- - Does not persist tour state.
144- - Does not ship analytics.
145- - Does not support nested tours.
285+ - Persist tour state (host owns `stepId`)
286+ - Ship analytics
287+ - Support nested tours
288+ - Require Next.js — works with any React 18+ setup
0 commit comments