Skip to content

Commit ad8da1d

Browse files
release: 0.3.0
1 parent ff74564 commit ad8da1d

10 files changed

Lines changed: 611 additions & 478 deletions

File tree

,

Whitespace-only changes.

AGENTS.md

Lines changed: 209 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -2,49 +2,70 @@
22

33
```yaml
44
name: next-easytour
5-
version: 0.3.0-alpha.3
6-
branch: point-three
5+
version: 0.3.0
76
kind: react-component-library
87
language: typescript
98
module_type: esm
10-
install: npm install next-easytour@next
9+
install: npm install next-easytour
1110
peer_deps:
1211
react: ">=18.0.0"
1312
react-dom: ">=18.0.0"
1413
import_path: next-easytour
1514
stylesheet_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
1833
requires_client_component: true
1934
requires_app_router: false
2035
framework_agnostic: true
2136
ssr_safe: true
22-
bundler: any
2337
css_prefix: eto-
2438
css_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";
3156
import { 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

3459
const 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

74140
Hook 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
79168
actions: [
@@ -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

Comments
 (0)