@eventop/sdk 1.2.2 → 1.2.11

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.
@@ -52,12 +52,16 @@ function useFeatureScope() {
52
52
  *
53
53
  * In practice most flows are flat (index 0, 1, 2, 3...) but the
54
54
  * registry supports nested depth for complex interactions.
55
+ *
56
+ * Route awareness:
57
+ * Features can declare the pathname they live on via `route`.
58
+ * The snapshot() includes this so the SDK core can navigate
59
+ * automatically when a tour step targets a feature on a different page.
55
60
  */
56
61
  function createFeatureRegistry() {
57
62
  // Map<featureId, featureData>
58
63
  const features = new Map();
59
64
  // Map<featureId, Map<stepKey, stepData>>
60
- // stepKey is either a number (flat) or "parentIndex.childIndex" (nested)
61
65
  const flowSteps = new Map();
62
66
  const listeners = new Set();
63
67
  function notify() {
@@ -74,21 +78,19 @@ function createFeatureRegistry() {
74
78
  notify();
75
79
  }
76
80
  function unregisterFeature(id) {
77
- features.delete(id);
81
+ features.set(id, {
82
+ ...existing,
83
+ selector: null,
84
+ advanceOn: null,
85
+ waitFor: null,
86
+ _ghost: true
87
+ });
78
88
  flowSteps.delete(id);
79
89
  notify();
80
90
  }
81
91
 
82
92
  // ── Step registration ────────────────────────────────────────────────────
83
93
 
84
- /**
85
- * Register a flow step.
86
- *
87
- * @param {string} featureId - Parent feature id
88
- * @param {number} index - Position in the flat flow (0, 1, 2…)
89
- * @param {number|null} parentStep - If set, this is a sub-step of parentStep
90
- * @param {object} stepData - { selector, waitFor, advanceOn, ... }
91
- */
92
94
  function registerStep(featureId, index, parentStep, stepData) {
93
95
  if (!flowSteps.has(featureId)) {
94
96
  flowSteps.set(featureId, new Map());
@@ -113,35 +115,14 @@ function createFeatureRegistry() {
113
115
 
114
116
  // ── Snapshot ─────────────────────────────────────────────────────────────
115
117
 
116
- /**
117
- * Build the flat flow array the SDK core expects.
118
- *
119
- * For nested steps we inline sub-steps after their parent step,
120
- * in index order. This means a flow like:
121
- *
122
- * Step 0 (flat)
123
- * Step 1 (flat)
124
- * Step 1.0 (sub-step of 1)
125
- * Step 1.1 (sub-step of 1)
126
- * Step 2 (flat)
127
- *
128
- * becomes the SDK flow: [step0, step1, step1.0, step1.1, step2]
129
- *
130
- * The AI writes titles/text for the parent feature.
131
- * Sub-steps inherit the parent step's text with a "(N/M)" suffix added
132
- * by the SDK's expandFlowSteps() function.
133
- */
134
118
  function buildFlow(featureId) {
135
119
  const map = flowSteps.get(featureId);
136
120
  if (!map || map.size === 0) return null;
137
121
  const allSteps = Array.from(map.values());
138
-
139
- // Separate top-level and nested steps
140
122
  const topLevel = allSteps.filter(s => s.parentStep == null).sort((a, b) => a.index - b.index);
141
123
  const result = [];
142
124
  topLevel.forEach(step => {
143
125
  result.push(step);
144
- // Inline any sub-steps after the parent
145
126
  const children = allSteps.filter(s => s.parentStep === step.index).sort((a, b) => a.index - b.index);
146
127
  result.push(...children);
147
128
  });
@@ -150,7 +131,9 @@ function createFeatureRegistry() {
150
131
 
151
132
  /**
152
133
  * Returns the live feature array in the format the SDK core expects.
153
- * screen.check() is automatic — mounted = available.
134
+ *
135
+ * `route` is now included in every feature entry so the core can detect
136
+ * when navigation is needed before showing a step.
154
137
  */
155
138
  function snapshot() {
156
139
  return Array.from(features.values()).map(feature => {
@@ -160,6 +143,7 @@ function createFeatureRegistry() {
160
143
  name: feature.name,
161
144
  description: feature.description,
162
145
  selector: feature.selector,
146
+ route: feature.route || null,
163
147
  advanceOn: feature.advanceOn || null,
164
148
  waitFor: feature.waitFor || null,
165
149
  ...(flow ? {
@@ -195,7 +179,8 @@ function EventopProvider({
195
179
  assistantName,
196
180
  suggestions,
197
181
  theme,
198
- position
182
+ position,
183
+ router
199
184
  }) {
200
185
  if (!provider) throw new Error('[Eventop] <EventopProvider> requires a provider prop.');
201
186
  if (!appName) throw new Error('[Eventop] <EventopProvider> requires an appName prop.');
@@ -209,10 +194,9 @@ function EventopProvider({
209
194
  });
210
195
  }, [registry]);
211
196
  react.useEffect(() => {
212
- // Dynamically import core.js only in the browser
213
197
  async function boot() {
214
- // Import the core SDK (this only runs on client)
215
- await Promise.resolve().then(function () { return require('./core.cjs'); }).then(function (n) { return n.core; });
198
+ await Promise.resolve().then(function () { return require('./core.cjs'); }); // Ensure the UMD bundle is loaded (for Shepherd and global Eventop)
199
+
216
200
  window.Eventop.init({
217
201
  provider,
218
202
  config: {
@@ -221,6 +205,7 @@ function EventopProvider({
221
205
  suggestions,
222
206
  theme,
223
207
  position,
208
+ router,
224
209
  features: registry.snapshot(),
225
210
  _providerName: 'custom'
226
211
  }
@@ -238,6 +223,22 @@ function EventopProvider({
238
223
  }
239
224
  };
240
225
  }, [provider, appName, assistantName, suggestions, theme, position, registry, syncToSDK]);
226
+ // Note: `router` is intentionally omitted from the deps array above.
227
+ // Router instances from useNavigate / useRouter are stable references —
228
+ // including them would re-boot the SDK on every render.
229
+ // Instead, we sync router updates via a separate effect below.
230
+
231
+ // ── Keep the router reference fresh without re-booting the SDK ─────────────
232
+ // When the router instance changes (rare, but can happen in Next.js during
233
+ // hydration), we push the new reference into the already-running SDK core.
234
+ react.useEffect(() => {
235
+ if (sdkReady.current && window.Eventop) {
236
+ var _window$Eventop$_upda2, _window$Eventop3;
237
+ (_window$Eventop$_upda2 = (_window$Eventop3 = window.Eventop)._updateConfig) === null || _window$Eventop$_upda2 === void 0 || _window$Eventop$_upda2.call(_window$Eventop3, {
238
+ router
239
+ });
240
+ }
241
+ }, [router]);
241
242
  const ctx = {
242
243
  registerFeature: registry.registerFeature,
243
244
  unregisterFeature: registry.unregisterFeature,
@@ -256,6 +257,7 @@ function EventopTarget({
256
257
  id,
257
258
  name,
258
259
  description,
260
+ route,
259
261
  navigate,
260
262
  navigateWaitFor,
261
263
  advanceOn,
@@ -275,6 +277,7 @@ function EventopTarget({
275
277
  id,
276
278
  name,
277
279
  description,
280
+ route,
278
281
  selector,
279
282
  navigate,
280
283
  navigateWaitFor,
@@ -285,7 +288,7 @@ function EventopTarget({
285
288
  } : null
286
289
  });
287
290
  return () => registry.unregisterFeature(id);
288
- }, [id, name, description]);
291
+ }, [id, name, description, route]);
289
292
  const child = react.Children.only(children);
290
293
  let wrapped;
291
294
  try {
@@ -15,6 +15,29 @@ export interface EventopAIProviderProps {
15
15
  suggestions?: string[];
16
16
  theme?: Theme;
17
17
  position?: Position;
18
+ /**
19
+ * Navigation function for cross-page tours.
20
+ *
21
+ * When a tour step lives on a different route, the SDK calls this function
22
+ * with the target pathname, shows the user a message explaining the navigation,
23
+ * then waits for the target element to appear before showing the step.
24
+ *
25
+ * If omitted, the SDK falls back to window.history.pushState + popstate
26
+ * (best-effort; works for simple SPAs but not React Router / Next.js).
27
+ *
28
+ * @example — React Router v6
29
+ * const navigate = useNavigate();
30
+ * <EventopAIProvider router={navigate} ...>
31
+ *
32
+ * @example — Next.js App Router
33
+ * const router = useRouter(); // from 'next/navigation'
34
+ * <EventopAIProvider router={(path) => router.push(path)} ...>
35
+ *
36
+ * @example — Next.js Pages Router
37
+ * const router = useRouter(); // from 'next/router'
38
+ * <EventopAIProvider router={(path) => router.push(path)} ...>
39
+ */
40
+ router?: (path: string) => void | Promise<void>;
18
41
  }
19
42
 
20
43
  /**
@@ -23,13 +46,36 @@ export interface EventopAIProviderProps {
23
46
  * register with this provider automatically.
24
47
  *
25
48
  * @example
26
- * <EventopAIProvider
27
- * provider={myServerFetcher}
28
- * appName="My App"
29
- * theme={{ mode: 'auto', tokens: { accent: '#6366f1' } }}
30
- * >
31
- * <App />
32
- * </EventopAIProvider>
49
+ * // React Router v6
50
+ * function Root() {
51
+ * const navigate = useNavigate();
52
+ * return (
53
+ * <EventopAIProvider
54
+ * router={navigate}
55
+ * provider={myServerFetcher}
56
+ * appName="My App"
57
+ * theme={{ mode: 'auto', tokens: { accent: '#6366f1' } }}
58
+ * >
59
+ * <App />
60
+ * </EventopAIProvider>
61
+ * );
62
+ * }
63
+ *
64
+ * @example
65
+ * // Next.js App Router (must be 'use client')
66
+ * 'use client';
67
+ * export function EventopProvider({ children }) {
68
+ * const router = useRouter();
69
+ * return (
70
+ * <EventopAIProvider
71
+ * router={(path) => router.push(path)}
72
+ * provider={myServerFetcher}
73
+ * appName="My App"
74
+ * >
75
+ * {children}
76
+ * </EventopAIProvider>
77
+ * );
78
+ * }
33
79
  */
34
80
  export const EventopAIProvider: FC<EventopAIProviderProps>;
35
81
 
@@ -44,9 +90,34 @@ export interface EventopTargetProps {
44
90
  name: string;
45
91
  /** What the feature does — AI uses this to match user intent */
46
92
  description?: string;
93
+ /**
94
+ * The pathname where this feature lives (e.g. "/settings/billing").
95
+ *
96
+ * When a tour step targets this feature and the user is on a different page,
97
+ * the SDK will automatically:
98
+ * 1. Tell the user which page it's navigating to and why
99
+ * 2. Call the `router` function passed to EventopAIProvider
100
+ * 3. Wait for this feature's element to appear before showing the step
101
+ *
102
+ * Only required for features that live on a different page than where
103
+ * the tour is typically started from. Features on the same page don't
104
+ * need this prop.
105
+ *
106
+ * @example
107
+ * <EventopTarget
108
+ * id="billing"
109
+ * name="Billing Settings"
110
+ * route="/settings/billing"
111
+ * >
112
+ * <BillingSection />
113
+ * </EventopTarget>
114
+ */
115
+ route?: string;
47
116
  /**
48
117
  * Navigate to this screen if the component is not currently mounted.
49
- * Called when the user asks about this feature from a different screen.
118
+ * @deprecated Prefer `route` + the `router` prop on EventopAIProvider
119
+ * for React Router and Next.js apps. `navigate` is still
120
+ * supported for custom or non-URL-based navigation logic.
50
121
  */
51
122
  navigate?: () => void | Promise<void>;
52
123
  /** CSS selector to wait for after navigating */
@@ -61,20 +132,27 @@ export interface EventopTargetProps {
61
132
  }
62
133
 
63
134
  /**
64
- * Wraps any component and registers it as a EventopAI feature.
135
+ * Wraps any component and registers it as an Eventop feature.
65
136
  * Registration happens at the CALL SITE — the wrapped component is unchanged.
66
137
  *
67
138
  * Works with any component: your own, shadcn, MUI, Radix, anything.
68
- * The wrapped component does not need to accept refs or know about EventopAI.
139
+ * The wrapped component does not need to accept refs or know about Eventop.
69
140
  *
70
141
  * @example
71
- * // Same Button, different features in different parts of the app
142
+ * // Feature on the same page no route needed
72
143
  * <EventopTarget id="export" name="Export Design" description="Download as PNG or SVG">
73
144
  * <Button onClick={handleExport}>Export</Button>
74
145
  * </EventopTarget>
75
146
  *
76
- * <EventopTarget id="share" name="Share Document" description="Share with teammates">
77
- * <Button onClick={handleShare}>Share</Button>
147
+ * @example
148
+ * // Feature on a different page — add route so the SDK can navigate there
149
+ * <EventopTarget
150
+ * id="billing"
151
+ * name="Billing Settings"
152
+ * description="Manage your subscription"
153
+ * route="/settings/billing"
154
+ * >
155
+ * <BillingSection />
78
156
  * </EventopTarget>
79
157
  */
80
158
  export const EventopTarget: FC<EventopTargetProps>;
@@ -50,12 +50,16 @@ function useFeatureScope() {
50
50
  *
51
51
  * In practice most flows are flat (index 0, 1, 2, 3...) but the
52
52
  * registry supports nested depth for complex interactions.
53
+ *
54
+ * Route awareness:
55
+ * Features can declare the pathname they live on via `route`.
56
+ * The snapshot() includes this so the SDK core can navigate
57
+ * automatically when a tour step targets a feature on a different page.
53
58
  */
54
59
  function createFeatureRegistry() {
55
60
  // Map<featureId, featureData>
56
61
  const features = new Map();
57
62
  // Map<featureId, Map<stepKey, stepData>>
58
- // stepKey is either a number (flat) or "parentIndex.childIndex" (nested)
59
63
  const flowSteps = new Map();
60
64
  const listeners = new Set();
61
65
  function notify() {
@@ -72,21 +76,19 @@ function createFeatureRegistry() {
72
76
  notify();
73
77
  }
74
78
  function unregisterFeature(id) {
75
- features.delete(id);
79
+ features.set(id, {
80
+ ...existing,
81
+ selector: null,
82
+ advanceOn: null,
83
+ waitFor: null,
84
+ _ghost: true
85
+ });
76
86
  flowSteps.delete(id);
77
87
  notify();
78
88
  }
79
89
 
80
90
  // ── Step registration ────────────────────────────────────────────────────
81
91
 
82
- /**
83
- * Register a flow step.
84
- *
85
- * @param {string} featureId - Parent feature id
86
- * @param {number} index - Position in the flat flow (0, 1, 2…)
87
- * @param {number|null} parentStep - If set, this is a sub-step of parentStep
88
- * @param {object} stepData - { selector, waitFor, advanceOn, ... }
89
- */
90
92
  function registerStep(featureId, index, parentStep, stepData) {
91
93
  if (!flowSteps.has(featureId)) {
92
94
  flowSteps.set(featureId, new Map());
@@ -111,35 +113,14 @@ function createFeatureRegistry() {
111
113
 
112
114
  // ── Snapshot ─────────────────────────────────────────────────────────────
113
115
 
114
- /**
115
- * Build the flat flow array the SDK core expects.
116
- *
117
- * For nested steps we inline sub-steps after their parent step,
118
- * in index order. This means a flow like:
119
- *
120
- * Step 0 (flat)
121
- * Step 1 (flat)
122
- * Step 1.0 (sub-step of 1)
123
- * Step 1.1 (sub-step of 1)
124
- * Step 2 (flat)
125
- *
126
- * becomes the SDK flow: [step0, step1, step1.0, step1.1, step2]
127
- *
128
- * The AI writes titles/text for the parent feature.
129
- * Sub-steps inherit the parent step's text with a "(N/M)" suffix added
130
- * by the SDK's expandFlowSteps() function.
131
- */
132
116
  function buildFlow(featureId) {
133
117
  const map = flowSteps.get(featureId);
134
118
  if (!map || map.size === 0) return null;
135
119
  const allSteps = Array.from(map.values());
136
-
137
- // Separate top-level and nested steps
138
120
  const topLevel = allSteps.filter(s => s.parentStep == null).sort((a, b) => a.index - b.index);
139
121
  const result = [];
140
122
  topLevel.forEach(step => {
141
123
  result.push(step);
142
- // Inline any sub-steps after the parent
143
124
  const children = allSteps.filter(s => s.parentStep === step.index).sort((a, b) => a.index - b.index);
144
125
  result.push(...children);
145
126
  });
@@ -148,7 +129,9 @@ function createFeatureRegistry() {
148
129
 
149
130
  /**
150
131
  * Returns the live feature array in the format the SDK core expects.
151
- * screen.check() is automatic — mounted = available.
132
+ *
133
+ * `route` is now included in every feature entry so the core can detect
134
+ * when navigation is needed before showing a step.
152
135
  */
153
136
  function snapshot() {
154
137
  return Array.from(features.values()).map(feature => {
@@ -158,6 +141,7 @@ function createFeatureRegistry() {
158
141
  name: feature.name,
159
142
  description: feature.description,
160
143
  selector: feature.selector,
144
+ route: feature.route || null,
161
145
  advanceOn: feature.advanceOn || null,
162
146
  waitFor: feature.waitFor || null,
163
147
  ...(flow ? {
@@ -193,7 +177,8 @@ function EventopProvider({
193
177
  assistantName,
194
178
  suggestions,
195
179
  theme,
196
- position
180
+ position,
181
+ router
197
182
  }) {
198
183
  if (!provider) throw new Error('[Eventop] <EventopProvider> requires a provider prop.');
199
184
  if (!appName) throw new Error('[Eventop] <EventopProvider> requires an appName prop.');
@@ -207,10 +192,9 @@ function EventopProvider({
207
192
  });
208
193
  }, [registry]);
209
194
  useEffect(() => {
210
- // Dynamically import core.js only in the browser
211
195
  async function boot() {
212
- // Import the core SDK (this only runs on client)
213
- await import('./core.js').then(function (n) { return n.c; });
196
+ await import('./core.js'); // Ensure the UMD bundle is loaded (for Shepherd and global Eventop)
197
+
214
198
  window.Eventop.init({
215
199
  provider,
216
200
  config: {
@@ -219,6 +203,7 @@ function EventopProvider({
219
203
  suggestions,
220
204
  theme,
221
205
  position,
206
+ router,
222
207
  features: registry.snapshot(),
223
208
  _providerName: 'custom'
224
209
  }
@@ -236,6 +221,22 @@ function EventopProvider({
236
221
  }
237
222
  };
238
223
  }, [provider, appName, assistantName, suggestions, theme, position, registry, syncToSDK]);
224
+ // Note: `router` is intentionally omitted from the deps array above.
225
+ // Router instances from useNavigate / useRouter are stable references —
226
+ // including them would re-boot the SDK on every render.
227
+ // Instead, we sync router updates via a separate effect below.
228
+
229
+ // ── Keep the router reference fresh without re-booting the SDK ─────────────
230
+ // When the router instance changes (rare, but can happen in Next.js during
231
+ // hydration), we push the new reference into the already-running SDK core.
232
+ useEffect(() => {
233
+ if (sdkReady.current && window.Eventop) {
234
+ var _window$Eventop$_upda2, _window$Eventop3;
235
+ (_window$Eventop$_upda2 = (_window$Eventop3 = window.Eventop)._updateConfig) === null || _window$Eventop$_upda2 === void 0 || _window$Eventop$_upda2.call(_window$Eventop3, {
236
+ router
237
+ });
238
+ }
239
+ }, [router]);
239
240
  const ctx = {
240
241
  registerFeature: registry.registerFeature,
241
242
  unregisterFeature: registry.unregisterFeature,
@@ -254,6 +255,7 @@ function EventopTarget({
254
255
  id,
255
256
  name,
256
257
  description,
258
+ route,
257
259
  navigate,
258
260
  navigateWaitFor,
259
261
  advanceOn,
@@ -273,6 +275,7 @@ function EventopTarget({
273
275
  id,
274
276
  name,
275
277
  description,
278
+ route,
276
279
  selector,
277
280
  navigate,
278
281
  navigateWaitFor,
@@ -283,7 +286,7 @@ function EventopTarget({
283
286
  } : null
284
287
  });
285
288
  return () => registry.unregisterFeature(id);
286
- }, [id, name, description]);
289
+ }, [id, name, description, route]);
287
290
  const child = Children.only(children);
288
291
  let wrapped;
289
292
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventop/sdk",
3
- "version": "1.2.2",
3
+ "version": "1.2.11",
4
4
  "description": "AI-powered guided tours for any web app. Drop-in, themeable, provider-agnostic.",
5
5
  "keywords": [
6
6
  "onboarding",