@eventop/sdk 1.2.11 → 1.2.12

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/dist/index.cjs CHANGED
@@ -29,18 +29,41 @@ function useFeatureScope() {
29
29
  /**
30
30
  * FeatureRegistry
31
31
  *
32
- * The live map of everything currently mounted in the React tree.
32
+ * The live map of everything registered in the React tree — both mounted
33
+ * (full entries) and unmounted (ghost entries).
33
34
  *
34
- * Features — registered by EventopTarget
35
- * Flow steps registered by EventopStep, attached to a feature by id
35
+ * ┌─────────────────────────────────────────────────────────────────────┐
36
+ * │ Full entry component is currently mounted │
37
+ * │ { id, name, description, route, selector, ... } │
38
+ * │ │
39
+ * │ Ghost entry — component has unmounted (navigated away) │
40
+ * │ { id, name, description, route, selector: null, _ghost: true } │
41
+ * └─────────────────────────────────────────────────────────────────────┘
36
42
  *
37
- * Both features and steps can live anywhere in the component tree.
38
- * They don't need to be co-located. An EventopStep just needs to know
39
- * which feature id it belongs to.
43
+ * Why ghosts?
44
+ *
45
+ * The AI system prompt is built from the registry snapshot. Without ghosts,
46
+ * features on other pages don't exist in the snapshot when the user is
47
+ * elsewhere — the AI can't pick them, so cross-page tours never start.
48
+ *
49
+ * With ghosts, every feature the app has ever rendered stays in the
50
+ * snapshot permanently. The AI always has the full picture. The tour
51
+ * runner already resolves selectors lazily (after navigation), so a
52
+ * null selector on a ghost is fine — it gets filled in at show-time
53
+ * once the component mounts on the target page.
54
+ *
55
+ * Lifecycle:
56
+ *
57
+ * EventopTarget mounts → registerFeature() → full entry
58
+ * EventopTarget unmounts → unregisterFeature() → ghost entry (metadata kept)
59
+ * EventopTarget remounts → registerFeature() → full entry again (selector restored)
60
+ *
61
+ * Flow steps follow the same pattern — they're pruned on unmount but the
62
+ * parent feature ghost keeps the feature visible to the AI.
40
63
  *
41
64
  * Nested steps:
42
- * Steps can themselves have children steps (sub-steps) by passing
43
- * a parentStep prop to EventopStep. This lets you model flows like:
65
+ * Steps can have children steps (sub-steps) by passing a parentStep prop.
66
+ * This lets you model flows like:
44
67
  *
45
68
  * Feature: "Create styled text"
46
69
  * Step 0: Click Add Text
@@ -49,17 +72,10 @@ function useFeatureScope() {
49
72
  * Step 1.1: Open font picker ← parentStep: 1, index: 1
50
73
  * Step 1.2: Choose a font ← parentStep: 1, index: 2
51
74
  * Step 2: Click Done
52
- *
53
- * In practice most flows are flat (index 0, 1, 2, 3...) but the
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.
60
75
  */
61
76
  function createFeatureRegistry() {
62
77
  // Map<featureId, featureData>
78
+ // Values are either full entries or ghost entries (_ghost: true)
63
79
  const features = new Map();
64
80
  // Map<featureId, Map<stepKey, stepData>>
65
81
  const flowSteps = new Map();
@@ -71,20 +87,37 @@ function createFeatureRegistry() {
71
87
  // ── Feature registration ─────────────────────────────────────────────────
72
88
 
73
89
  function registerFeature(feature) {
90
+ // Restore a ghost or create a fresh full entry.
91
+ // Always overwrite so a remount gets the latest selector.
74
92
  features.set(feature.id, {
75
93
  ...feature,
94
+ _ghost: false,
76
95
  _registeredAt: Date.now()
77
96
  });
78
97
  notify();
79
98
  }
80
99
  function unregisterFeature(id) {
100
+ if (!features.has(id)) return;
101
+
102
+ // Downgrade to ghost — keep all metadata, null out the live selector.
103
+ // Flow steps are pruned separately (their DOM elements are gone too).
104
+ const feature = features.get(id);
81
105
  features.set(id, {
82
- ...existing,
106
+ id: feature.id,
107
+ name: feature.name,
108
+ description: feature.description,
109
+ route: feature.route || null,
110
+ navigate: feature.navigate || null,
111
+ navigateWaitFor: feature.navigateWaitFor || null,
112
+ _registeredAt: feature._registeredAt,
83
113
  selector: null,
84
114
  advanceOn: null,
85
115
  waitFor: null,
86
116
  _ghost: true
87
117
  });
118
+
119
+ // Prune flow steps — their selectors are dead too.
120
+ // They'll re-register when the component remounts on the target page.
88
121
  flowSteps.delete(id);
89
122
  notify();
90
123
  }
@@ -130,41 +163,62 @@ function createFeatureRegistry() {
130
163
  }
131
164
 
132
165
  /**
133
- * Returns the live feature array in the format the SDK core expects.
166
+ * Returns the full feature array both live and ghost entries.
167
+ *
168
+ * Ghost entries have selector: null. The SDK core handles this correctly:
169
+ * - The AI system prompt uses name/description/route (always present)
170
+ * - The tour runner resolves the selector lazily after navigation,
171
+ * at which point the component has remounted and re-registered
134
172
  *
135
- * `route` is now included in every feature entry so the core can detect
136
- * when navigation is needed before showing a step.
173
+ * screen.check() returns false for ghosts so the SDK knows navigation
174
+ * is needed before showing the step.
137
175
  */
138
176
  function snapshot() {
139
177
  return Array.from(features.values()).map(feature => {
140
178
  const flow = buildFlow(feature.id);
179
+ const isGhost = feature._ghost === true;
141
180
  return {
142
181
  id: feature.id,
143
182
  name: feature.name,
144
183
  description: feature.description,
145
- selector: feature.selector,
184
+ selector: feature.selector || null,
146
185
  route: feature.route || null,
147
186
  advanceOn: feature.advanceOn || null,
148
187
  waitFor: feature.waitFor || null,
188
+ _ghost: isGhost,
149
189
  ...(flow ? {
150
190
  flow
151
191
  } : {}),
152
192
  screen: {
153
193
  id: feature.id,
154
- check: () => features.has(feature.id),
194
+ // Ghost entries always fail the check SDK knows to navigate
195
+ check: () => {
196
+ var _features$get;
197
+ return ((_features$get = features.get(feature.id)) === null || _features$get === void 0 ? void 0 : _features$get._ghost) !== true;
198
+ },
155
199
  navigate: feature.navigate || null,
156
200
  waitFor: feature.navigateWaitFor || feature.selector || null
157
201
  }
158
202
  };
159
203
  });
160
204
  }
205
+
206
+ // ── Helpers ───────────────────────────────────────────────────────────────
207
+
208
+ /**
209
+ * Returns true only for fully mounted (non-ghost) features.
210
+ */
211
+ function isRegistered(id) {
212
+ const f = features.get(id);
213
+ return !!f && !f._ghost;
214
+ }
161
215
  return {
162
216
  registerFeature,
163
217
  unregisterFeature,
164
218
  registerStep,
165
219
  unregisterStep,
166
220
  snapshot,
167
- isRegistered: id => features.has(id),
221
+ isRegistered,
168
222
  subscribe: fn => {
169
223
  listeners.add(fn);
170
224
  return () => listeners.delete(fn);
package/dist/index.js CHANGED
@@ -27,18 +27,41 @@ function useFeatureScope() {
27
27
  /**
28
28
  * FeatureRegistry
29
29
  *
30
- * The live map of everything currently mounted in the React tree.
30
+ * The live map of everything registered in the React tree — both mounted
31
+ * (full entries) and unmounted (ghost entries).
31
32
  *
32
- * Features — registered by EventopTarget
33
- * Flow steps registered by EventopStep, attached to a feature by id
33
+ * ┌─────────────────────────────────────────────────────────────────────┐
34
+ * │ Full entry component is currently mounted │
35
+ * │ { id, name, description, route, selector, ... } │
36
+ * │ │
37
+ * │ Ghost entry — component has unmounted (navigated away) │
38
+ * │ { id, name, description, route, selector: null, _ghost: true } │
39
+ * └─────────────────────────────────────────────────────────────────────┘
34
40
  *
35
- * Both features and steps can live anywhere in the component tree.
36
- * They don't need to be co-located. An EventopStep just needs to know
37
- * which feature id it belongs to.
41
+ * Why ghosts?
42
+ *
43
+ * The AI system prompt is built from the registry snapshot. Without ghosts,
44
+ * features on other pages don't exist in the snapshot when the user is
45
+ * elsewhere — the AI can't pick them, so cross-page tours never start.
46
+ *
47
+ * With ghosts, every feature the app has ever rendered stays in the
48
+ * snapshot permanently. The AI always has the full picture. The tour
49
+ * runner already resolves selectors lazily (after navigation), so a
50
+ * null selector on a ghost is fine — it gets filled in at show-time
51
+ * once the component mounts on the target page.
52
+ *
53
+ * Lifecycle:
54
+ *
55
+ * EventopTarget mounts → registerFeature() → full entry
56
+ * EventopTarget unmounts → unregisterFeature() → ghost entry (metadata kept)
57
+ * EventopTarget remounts → registerFeature() → full entry again (selector restored)
58
+ *
59
+ * Flow steps follow the same pattern — they're pruned on unmount but the
60
+ * parent feature ghost keeps the feature visible to the AI.
38
61
  *
39
62
  * Nested steps:
40
- * Steps can themselves have children steps (sub-steps) by passing
41
- * a parentStep prop to EventopStep. This lets you model flows like:
63
+ * Steps can have children steps (sub-steps) by passing a parentStep prop.
64
+ * This lets you model flows like:
42
65
  *
43
66
  * Feature: "Create styled text"
44
67
  * Step 0: Click Add Text
@@ -47,17 +70,10 @@ function useFeatureScope() {
47
70
  * Step 1.1: Open font picker ← parentStep: 1, index: 1
48
71
  * Step 1.2: Choose a font ← parentStep: 1, index: 2
49
72
  * Step 2: Click Done
50
- *
51
- * In practice most flows are flat (index 0, 1, 2, 3...) but the
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.
58
73
  */
59
74
  function createFeatureRegistry() {
60
75
  // Map<featureId, featureData>
76
+ // Values are either full entries or ghost entries (_ghost: true)
61
77
  const features = new Map();
62
78
  // Map<featureId, Map<stepKey, stepData>>
63
79
  const flowSteps = new Map();
@@ -69,20 +85,37 @@ function createFeatureRegistry() {
69
85
  // ── Feature registration ─────────────────────────────────────────────────
70
86
 
71
87
  function registerFeature(feature) {
88
+ // Restore a ghost or create a fresh full entry.
89
+ // Always overwrite so a remount gets the latest selector.
72
90
  features.set(feature.id, {
73
91
  ...feature,
92
+ _ghost: false,
74
93
  _registeredAt: Date.now()
75
94
  });
76
95
  notify();
77
96
  }
78
97
  function unregisterFeature(id) {
98
+ if (!features.has(id)) return;
99
+
100
+ // Downgrade to ghost — keep all metadata, null out the live selector.
101
+ // Flow steps are pruned separately (their DOM elements are gone too).
102
+ const feature = features.get(id);
79
103
  features.set(id, {
80
- ...existing,
104
+ id: feature.id,
105
+ name: feature.name,
106
+ description: feature.description,
107
+ route: feature.route || null,
108
+ navigate: feature.navigate || null,
109
+ navigateWaitFor: feature.navigateWaitFor || null,
110
+ _registeredAt: feature._registeredAt,
81
111
  selector: null,
82
112
  advanceOn: null,
83
113
  waitFor: null,
84
114
  _ghost: true
85
115
  });
116
+
117
+ // Prune flow steps — their selectors are dead too.
118
+ // They'll re-register when the component remounts on the target page.
86
119
  flowSteps.delete(id);
87
120
  notify();
88
121
  }
@@ -128,41 +161,62 @@ function createFeatureRegistry() {
128
161
  }
129
162
 
130
163
  /**
131
- * Returns the live feature array in the format the SDK core expects.
164
+ * Returns the full feature array both live and ghost entries.
165
+ *
166
+ * Ghost entries have selector: null. The SDK core handles this correctly:
167
+ * - The AI system prompt uses name/description/route (always present)
168
+ * - The tour runner resolves the selector lazily after navigation,
169
+ * at which point the component has remounted and re-registered
132
170
  *
133
- * `route` is now included in every feature entry so the core can detect
134
- * when navigation is needed before showing a step.
171
+ * screen.check() returns false for ghosts so the SDK knows navigation
172
+ * is needed before showing the step.
135
173
  */
136
174
  function snapshot() {
137
175
  return Array.from(features.values()).map(feature => {
138
176
  const flow = buildFlow(feature.id);
177
+ const isGhost = feature._ghost === true;
139
178
  return {
140
179
  id: feature.id,
141
180
  name: feature.name,
142
181
  description: feature.description,
143
- selector: feature.selector,
182
+ selector: feature.selector || null,
144
183
  route: feature.route || null,
145
184
  advanceOn: feature.advanceOn || null,
146
185
  waitFor: feature.waitFor || null,
186
+ _ghost: isGhost,
147
187
  ...(flow ? {
148
188
  flow
149
189
  } : {}),
150
190
  screen: {
151
191
  id: feature.id,
152
- check: () => features.has(feature.id),
192
+ // Ghost entries always fail the check SDK knows to navigate
193
+ check: () => {
194
+ var _features$get;
195
+ return ((_features$get = features.get(feature.id)) === null || _features$get === void 0 ? void 0 : _features$get._ghost) !== true;
196
+ },
153
197
  navigate: feature.navigate || null,
154
198
  waitFor: feature.navigateWaitFor || feature.selector || null
155
199
  }
156
200
  };
157
201
  });
158
202
  }
203
+
204
+ // ── Helpers ───────────────────────────────────────────────────────────────
205
+
206
+ /**
207
+ * Returns true only for fully mounted (non-ghost) features.
208
+ */
209
+ function isRegistered(id) {
210
+ const f = features.get(id);
211
+ return !!f && !f._ghost;
212
+ }
159
213
  return {
160
214
  registerFeature,
161
215
  unregisterFeature,
162
216
  registerStep,
163
217
  unregisterStep,
164
218
  snapshot,
165
- isRegistered: id => features.has(id),
219
+ isRegistered,
166
220
  subscribe: fn => {
167
221
  listeners.add(fn);
168
222
  return () => listeners.delete(fn);
@@ -29,18 +29,41 @@ function useFeatureScope() {
29
29
  /**
30
30
  * FeatureRegistry
31
31
  *
32
- * The live map of everything currently mounted in the React tree.
32
+ * The live map of everything registered in the React tree — both mounted
33
+ * (full entries) and unmounted (ghost entries).
33
34
  *
34
- * Features — registered by EventopTarget
35
- * Flow steps registered by EventopStep, attached to a feature by id
35
+ * ┌─────────────────────────────────────────────────────────────────────┐
36
+ * │ Full entry component is currently mounted │
37
+ * │ { id, name, description, route, selector, ... } │
38
+ * │ │
39
+ * │ Ghost entry — component has unmounted (navigated away) │
40
+ * │ { id, name, description, route, selector: null, _ghost: true } │
41
+ * └─────────────────────────────────────────────────────────────────────┘
36
42
  *
37
- * Both features and steps can live anywhere in the component tree.
38
- * They don't need to be co-located. An EventopStep just needs to know
39
- * which feature id it belongs to.
43
+ * Why ghosts?
44
+ *
45
+ * The AI system prompt is built from the registry snapshot. Without ghosts,
46
+ * features on other pages don't exist in the snapshot when the user is
47
+ * elsewhere — the AI can't pick them, so cross-page tours never start.
48
+ *
49
+ * With ghosts, every feature the app has ever rendered stays in the
50
+ * snapshot permanently. The AI always has the full picture. The tour
51
+ * runner already resolves selectors lazily (after navigation), so a
52
+ * null selector on a ghost is fine — it gets filled in at show-time
53
+ * once the component mounts on the target page.
54
+ *
55
+ * Lifecycle:
56
+ *
57
+ * EventopTarget mounts → registerFeature() → full entry
58
+ * EventopTarget unmounts → unregisterFeature() → ghost entry (metadata kept)
59
+ * EventopTarget remounts → registerFeature() → full entry again (selector restored)
60
+ *
61
+ * Flow steps follow the same pattern — they're pruned on unmount but the
62
+ * parent feature ghost keeps the feature visible to the AI.
40
63
  *
41
64
  * Nested steps:
42
- * Steps can themselves have children steps (sub-steps) by passing
43
- * a parentStep prop to EventopStep. This lets you model flows like:
65
+ * Steps can have children steps (sub-steps) by passing a parentStep prop.
66
+ * This lets you model flows like:
44
67
  *
45
68
  * Feature: "Create styled text"
46
69
  * Step 0: Click Add Text
@@ -49,17 +72,10 @@ function useFeatureScope() {
49
72
  * Step 1.1: Open font picker ← parentStep: 1, index: 1
50
73
  * Step 1.2: Choose a font ← parentStep: 1, index: 2
51
74
  * Step 2: Click Done
52
- *
53
- * In practice most flows are flat (index 0, 1, 2, 3...) but the
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.
60
75
  */
61
76
  function createFeatureRegistry() {
62
77
  // Map<featureId, featureData>
78
+ // Values are either full entries or ghost entries (_ghost: true)
63
79
  const features = new Map();
64
80
  // Map<featureId, Map<stepKey, stepData>>
65
81
  const flowSteps = new Map();
@@ -71,20 +87,37 @@ function createFeatureRegistry() {
71
87
  // ── Feature registration ─────────────────────────────────────────────────
72
88
 
73
89
  function registerFeature(feature) {
90
+ // Restore a ghost or create a fresh full entry.
91
+ // Always overwrite so a remount gets the latest selector.
74
92
  features.set(feature.id, {
75
93
  ...feature,
94
+ _ghost: false,
76
95
  _registeredAt: Date.now()
77
96
  });
78
97
  notify();
79
98
  }
80
99
  function unregisterFeature(id) {
100
+ if (!features.has(id)) return;
101
+
102
+ // Downgrade to ghost — keep all metadata, null out the live selector.
103
+ // Flow steps are pruned separately (their DOM elements are gone too).
104
+ const feature = features.get(id);
81
105
  features.set(id, {
82
- ...existing,
106
+ id: feature.id,
107
+ name: feature.name,
108
+ description: feature.description,
109
+ route: feature.route || null,
110
+ navigate: feature.navigate || null,
111
+ navigateWaitFor: feature.navigateWaitFor || null,
112
+ _registeredAt: feature._registeredAt,
83
113
  selector: null,
84
114
  advanceOn: null,
85
115
  waitFor: null,
86
116
  _ghost: true
87
117
  });
118
+
119
+ // Prune flow steps — their selectors are dead too.
120
+ // They'll re-register when the component remounts on the target page.
88
121
  flowSteps.delete(id);
89
122
  notify();
90
123
  }
@@ -130,41 +163,62 @@ function createFeatureRegistry() {
130
163
  }
131
164
 
132
165
  /**
133
- * Returns the live feature array in the format the SDK core expects.
166
+ * Returns the full feature array both live and ghost entries.
167
+ *
168
+ * Ghost entries have selector: null. The SDK core handles this correctly:
169
+ * - The AI system prompt uses name/description/route (always present)
170
+ * - The tour runner resolves the selector lazily after navigation,
171
+ * at which point the component has remounted and re-registered
134
172
  *
135
- * `route` is now included in every feature entry so the core can detect
136
- * when navigation is needed before showing a step.
173
+ * screen.check() returns false for ghosts so the SDK knows navigation
174
+ * is needed before showing the step.
137
175
  */
138
176
  function snapshot() {
139
177
  return Array.from(features.values()).map(feature => {
140
178
  const flow = buildFlow(feature.id);
179
+ const isGhost = feature._ghost === true;
141
180
  return {
142
181
  id: feature.id,
143
182
  name: feature.name,
144
183
  description: feature.description,
145
- selector: feature.selector,
184
+ selector: feature.selector || null,
146
185
  route: feature.route || null,
147
186
  advanceOn: feature.advanceOn || null,
148
187
  waitFor: feature.waitFor || null,
188
+ _ghost: isGhost,
149
189
  ...(flow ? {
150
190
  flow
151
191
  } : {}),
152
192
  screen: {
153
193
  id: feature.id,
154
- check: () => features.has(feature.id),
194
+ // Ghost entries always fail the check SDK knows to navigate
195
+ check: () => {
196
+ var _features$get;
197
+ return ((_features$get = features.get(feature.id)) === null || _features$get === void 0 ? void 0 : _features$get._ghost) !== true;
198
+ },
155
199
  navigate: feature.navigate || null,
156
200
  waitFor: feature.navigateWaitFor || feature.selector || null
157
201
  }
158
202
  };
159
203
  });
160
204
  }
205
+
206
+ // ── Helpers ───────────────────────────────────────────────────────────────
207
+
208
+ /**
209
+ * Returns true only for fully mounted (non-ghost) features.
210
+ */
211
+ function isRegistered(id) {
212
+ const f = features.get(id);
213
+ return !!f && !f._ghost;
214
+ }
161
215
  return {
162
216
  registerFeature,
163
217
  unregisterFeature,
164
218
  registerStep,
165
219
  unregisterStep,
166
220
  snapshot,
167
- isRegistered: id => features.has(id),
221
+ isRegistered,
168
222
  subscribe: fn => {
169
223
  listeners.add(fn);
170
224
  return () => listeners.delete(fn);
@@ -27,18 +27,41 @@ function useFeatureScope() {
27
27
  /**
28
28
  * FeatureRegistry
29
29
  *
30
- * The live map of everything currently mounted in the React tree.
30
+ * The live map of everything registered in the React tree — both mounted
31
+ * (full entries) and unmounted (ghost entries).
31
32
  *
32
- * Features — registered by EventopTarget
33
- * Flow steps registered by EventopStep, attached to a feature by id
33
+ * ┌─────────────────────────────────────────────────────────────────────┐
34
+ * │ Full entry component is currently mounted │
35
+ * │ { id, name, description, route, selector, ... } │
36
+ * │ │
37
+ * │ Ghost entry — component has unmounted (navigated away) │
38
+ * │ { id, name, description, route, selector: null, _ghost: true } │
39
+ * └─────────────────────────────────────────────────────────────────────┘
34
40
  *
35
- * Both features and steps can live anywhere in the component tree.
36
- * They don't need to be co-located. An EventopStep just needs to know
37
- * which feature id it belongs to.
41
+ * Why ghosts?
42
+ *
43
+ * The AI system prompt is built from the registry snapshot. Without ghosts,
44
+ * features on other pages don't exist in the snapshot when the user is
45
+ * elsewhere — the AI can't pick them, so cross-page tours never start.
46
+ *
47
+ * With ghosts, every feature the app has ever rendered stays in the
48
+ * snapshot permanently. The AI always has the full picture. The tour
49
+ * runner already resolves selectors lazily (after navigation), so a
50
+ * null selector on a ghost is fine — it gets filled in at show-time
51
+ * once the component mounts on the target page.
52
+ *
53
+ * Lifecycle:
54
+ *
55
+ * EventopTarget mounts → registerFeature() → full entry
56
+ * EventopTarget unmounts → unregisterFeature() → ghost entry (metadata kept)
57
+ * EventopTarget remounts → registerFeature() → full entry again (selector restored)
58
+ *
59
+ * Flow steps follow the same pattern — they're pruned on unmount but the
60
+ * parent feature ghost keeps the feature visible to the AI.
38
61
  *
39
62
  * Nested steps:
40
- * Steps can themselves have children steps (sub-steps) by passing
41
- * a parentStep prop to EventopStep. This lets you model flows like:
63
+ * Steps can have children steps (sub-steps) by passing a parentStep prop.
64
+ * This lets you model flows like:
42
65
  *
43
66
  * Feature: "Create styled text"
44
67
  * Step 0: Click Add Text
@@ -47,17 +70,10 @@ function useFeatureScope() {
47
70
  * Step 1.1: Open font picker ← parentStep: 1, index: 1
48
71
  * Step 1.2: Choose a font ← parentStep: 1, index: 2
49
72
  * Step 2: Click Done
50
- *
51
- * In practice most flows are flat (index 0, 1, 2, 3...) but the
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.
58
73
  */
59
74
  function createFeatureRegistry() {
60
75
  // Map<featureId, featureData>
76
+ // Values are either full entries or ghost entries (_ghost: true)
61
77
  const features = new Map();
62
78
  // Map<featureId, Map<stepKey, stepData>>
63
79
  const flowSteps = new Map();
@@ -69,20 +85,37 @@ function createFeatureRegistry() {
69
85
  // ── Feature registration ─────────────────────────────────────────────────
70
86
 
71
87
  function registerFeature(feature) {
88
+ // Restore a ghost or create a fresh full entry.
89
+ // Always overwrite so a remount gets the latest selector.
72
90
  features.set(feature.id, {
73
91
  ...feature,
92
+ _ghost: false,
74
93
  _registeredAt: Date.now()
75
94
  });
76
95
  notify();
77
96
  }
78
97
  function unregisterFeature(id) {
98
+ if (!features.has(id)) return;
99
+
100
+ // Downgrade to ghost — keep all metadata, null out the live selector.
101
+ // Flow steps are pruned separately (their DOM elements are gone too).
102
+ const feature = features.get(id);
79
103
  features.set(id, {
80
- ...existing,
104
+ id: feature.id,
105
+ name: feature.name,
106
+ description: feature.description,
107
+ route: feature.route || null,
108
+ navigate: feature.navigate || null,
109
+ navigateWaitFor: feature.navigateWaitFor || null,
110
+ _registeredAt: feature._registeredAt,
81
111
  selector: null,
82
112
  advanceOn: null,
83
113
  waitFor: null,
84
114
  _ghost: true
85
115
  });
116
+
117
+ // Prune flow steps — their selectors are dead too.
118
+ // They'll re-register when the component remounts on the target page.
86
119
  flowSteps.delete(id);
87
120
  notify();
88
121
  }
@@ -128,41 +161,62 @@ function createFeatureRegistry() {
128
161
  }
129
162
 
130
163
  /**
131
- * Returns the live feature array in the format the SDK core expects.
164
+ * Returns the full feature array both live and ghost entries.
165
+ *
166
+ * Ghost entries have selector: null. The SDK core handles this correctly:
167
+ * - The AI system prompt uses name/description/route (always present)
168
+ * - The tour runner resolves the selector lazily after navigation,
169
+ * at which point the component has remounted and re-registered
132
170
  *
133
- * `route` is now included in every feature entry so the core can detect
134
- * when navigation is needed before showing a step.
171
+ * screen.check() returns false for ghosts so the SDK knows navigation
172
+ * is needed before showing the step.
135
173
  */
136
174
  function snapshot() {
137
175
  return Array.from(features.values()).map(feature => {
138
176
  const flow = buildFlow(feature.id);
177
+ const isGhost = feature._ghost === true;
139
178
  return {
140
179
  id: feature.id,
141
180
  name: feature.name,
142
181
  description: feature.description,
143
- selector: feature.selector,
182
+ selector: feature.selector || null,
144
183
  route: feature.route || null,
145
184
  advanceOn: feature.advanceOn || null,
146
185
  waitFor: feature.waitFor || null,
186
+ _ghost: isGhost,
147
187
  ...(flow ? {
148
188
  flow
149
189
  } : {}),
150
190
  screen: {
151
191
  id: feature.id,
152
- check: () => features.has(feature.id),
192
+ // Ghost entries always fail the check SDK knows to navigate
193
+ check: () => {
194
+ var _features$get;
195
+ return ((_features$get = features.get(feature.id)) === null || _features$get === void 0 ? void 0 : _features$get._ghost) !== true;
196
+ },
153
197
  navigate: feature.navigate || null,
154
198
  waitFor: feature.navigateWaitFor || feature.selector || null
155
199
  }
156
200
  };
157
201
  });
158
202
  }
203
+
204
+ // ── Helpers ───────────────────────────────────────────────────────────────
205
+
206
+ /**
207
+ * Returns true only for fully mounted (non-ghost) features.
208
+ */
209
+ function isRegistered(id) {
210
+ const f = features.get(id);
211
+ return !!f && !f._ghost;
212
+ }
159
213
  return {
160
214
  registerFeature,
161
215
  unregisterFeature,
162
216
  registerStep,
163
217
  unregisterStep,
164
218
  snapshot,
165
- isRegistered: id => features.has(id),
219
+ isRegistered,
166
220
  subscribe: fn => {
167
221
  listeners.add(fn);
168
222
  return () => listeners.delete(fn);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventop/sdk",
3
- "version": "1.2.11",
3
+ "version": "1.2.12",
4
4
  "description": "AI-powered guided tours for any web app. Drop-in, themeable, provider-agnostic.",
5
5
  "keywords": [
6
6
  "onboarding",