@eventop/sdk 1.2.10 → 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/README.md +27 -0
- package/dist/index.cjs +83 -23
- package/dist/index.js +83 -23
- package/dist/react/index.cjs +83 -23
- package/dist/react/index.js +83 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -304,6 +304,33 @@ that lives on a different page.
|
|
|
304
304
|
Features that share the page where tours are typically started don't need `route`.
|
|
305
305
|
Only add it to features on other pages.
|
|
306
306
|
|
|
307
|
+
### How the registry stays aware of every page
|
|
308
|
+
|
|
309
|
+
When you navigate away from a page, its `EventopTarget` components unmount. Rather
|
|
310
|
+
than removing them from the registry entirely, the SDK downgrades them to **ghost
|
|
311
|
+
entries** — the metadata (`id`, `name`, `description`, `route`) is kept, only the
|
|
312
|
+
live DOM selector is nulled out.
|
|
313
|
+
|
|
314
|
+
```
|
|
315
|
+
EventopTarget mounts → full entry { id, name, description, route, selector }
|
|
316
|
+
EventopTarget unmounts → ghost entry { id, name, description, route, selector: null }
|
|
317
|
+
EventopTarget remounts → full entry (selector restored)
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
This means the AI system prompt always contains every feature the app has ever
|
|
321
|
+
rendered, regardless of which page you're currently on. The AI can plan cross-page
|
|
322
|
+
tours from any starting point without you doing anything extra.
|
|
323
|
+
|
|
324
|
+
The selector is resolved lazily — after navigation completes and the target page's
|
|
325
|
+
components remount, the ghost upgrades back to a full entry and the tour picks up
|
|
326
|
+
the correct selector just before showing the step.
|
|
327
|
+
|
|
328
|
+
> **Note on `id` stability** — ghost entries accumulate for the lifetime of the
|
|
329
|
+
> session, so `id` should identify a **UI capability**, not a data record.
|
|
330
|
+
> Wrapping dynamic list items with unique ids (e.g. `` `project-${p.id}` ``)
|
|
331
|
+
> will create an unbounded number of ghosts. Use a single `EventopTarget` for
|
|
332
|
+
> the repeating pattern instead.
|
|
333
|
+
|
|
307
334
|
---
|
|
308
335
|
|
|
309
336
|
## Multi-step flows
|
package/dist/index.cjs
CHANGED
|
@@ -29,18 +29,41 @@ function useFeatureScope() {
|
|
|
29
29
|
/**
|
|
30
30
|
* FeatureRegistry
|
|
31
31
|
*
|
|
32
|
-
* The live map of everything
|
|
32
|
+
* The live map of everything registered in the React tree — both mounted
|
|
33
|
+
* (full entries) and unmounted (ghost entries).
|
|
33
34
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
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
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
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
|
|
43
|
-
*
|
|
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,14 +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) {
|
|
81
|
-
features.
|
|
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);
|
|
105
|
+
features.set(id, {
|
|
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,
|
|
113
|
+
selector: null,
|
|
114
|
+
advanceOn: null,
|
|
115
|
+
waitFor: null,
|
|
116
|
+
_ghost: true
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Prune flow steps — their selectors are dead too.
|
|
120
|
+
// They'll re-register when the component remounts on the target page.
|
|
82
121
|
flowSteps.delete(id);
|
|
83
122
|
notify();
|
|
84
123
|
}
|
|
@@ -124,41 +163,62 @@ function createFeatureRegistry() {
|
|
|
124
163
|
}
|
|
125
164
|
|
|
126
165
|
/**
|
|
127
|
-
* Returns the
|
|
166
|
+
* Returns the full feature array — both live and ghost entries.
|
|
128
167
|
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
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
|
|
172
|
+
*
|
|
173
|
+
* screen.check() returns false for ghosts so the SDK knows navigation
|
|
174
|
+
* is needed before showing the step.
|
|
131
175
|
*/
|
|
132
176
|
function snapshot() {
|
|
133
177
|
return Array.from(features.values()).map(feature => {
|
|
134
178
|
const flow = buildFlow(feature.id);
|
|
179
|
+
const isGhost = feature._ghost === true;
|
|
135
180
|
return {
|
|
136
181
|
id: feature.id,
|
|
137
182
|
name: feature.name,
|
|
138
183
|
description: feature.description,
|
|
139
|
-
selector: feature.selector,
|
|
184
|
+
selector: feature.selector || null,
|
|
140
185
|
route: feature.route || null,
|
|
141
186
|
advanceOn: feature.advanceOn || null,
|
|
142
187
|
waitFor: feature.waitFor || null,
|
|
188
|
+
_ghost: isGhost,
|
|
143
189
|
...(flow ? {
|
|
144
190
|
flow
|
|
145
191
|
} : {}),
|
|
146
192
|
screen: {
|
|
147
193
|
id: feature.id,
|
|
148
|
-
check
|
|
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
|
+
},
|
|
149
199
|
navigate: feature.navigate || null,
|
|
150
200
|
waitFor: feature.navigateWaitFor || feature.selector || null
|
|
151
201
|
}
|
|
152
202
|
};
|
|
153
203
|
});
|
|
154
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
|
+
}
|
|
155
215
|
return {
|
|
156
216
|
registerFeature,
|
|
157
217
|
unregisterFeature,
|
|
158
218
|
registerStep,
|
|
159
219
|
unregisterStep,
|
|
160
220
|
snapshot,
|
|
161
|
-
isRegistered
|
|
221
|
+
isRegistered,
|
|
162
222
|
subscribe: fn => {
|
|
163
223
|
listeners.add(fn);
|
|
164
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
|
|
30
|
+
* The live map of everything registered in the React tree — both mounted
|
|
31
|
+
* (full entries) and unmounted (ghost entries).
|
|
31
32
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
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
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
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
|
|
41
|
-
*
|
|
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,14 +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) {
|
|
79
|
-
features.
|
|
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);
|
|
103
|
+
features.set(id, {
|
|
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,
|
|
111
|
+
selector: null,
|
|
112
|
+
advanceOn: null,
|
|
113
|
+
waitFor: null,
|
|
114
|
+
_ghost: true
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Prune flow steps — their selectors are dead too.
|
|
118
|
+
// They'll re-register when the component remounts on the target page.
|
|
80
119
|
flowSteps.delete(id);
|
|
81
120
|
notify();
|
|
82
121
|
}
|
|
@@ -122,41 +161,62 @@ function createFeatureRegistry() {
|
|
|
122
161
|
}
|
|
123
162
|
|
|
124
163
|
/**
|
|
125
|
-
* Returns the
|
|
164
|
+
* Returns the full feature array — both live and ghost entries.
|
|
126
165
|
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
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
|
|
170
|
+
*
|
|
171
|
+
* screen.check() returns false for ghosts so the SDK knows navigation
|
|
172
|
+
* is needed before showing the step.
|
|
129
173
|
*/
|
|
130
174
|
function snapshot() {
|
|
131
175
|
return Array.from(features.values()).map(feature => {
|
|
132
176
|
const flow = buildFlow(feature.id);
|
|
177
|
+
const isGhost = feature._ghost === true;
|
|
133
178
|
return {
|
|
134
179
|
id: feature.id,
|
|
135
180
|
name: feature.name,
|
|
136
181
|
description: feature.description,
|
|
137
|
-
selector: feature.selector,
|
|
182
|
+
selector: feature.selector || null,
|
|
138
183
|
route: feature.route || null,
|
|
139
184
|
advanceOn: feature.advanceOn || null,
|
|
140
185
|
waitFor: feature.waitFor || null,
|
|
186
|
+
_ghost: isGhost,
|
|
141
187
|
...(flow ? {
|
|
142
188
|
flow
|
|
143
189
|
} : {}),
|
|
144
190
|
screen: {
|
|
145
191
|
id: feature.id,
|
|
146
|
-
check
|
|
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
|
+
},
|
|
147
197
|
navigate: feature.navigate || null,
|
|
148
198
|
waitFor: feature.navigateWaitFor || feature.selector || null
|
|
149
199
|
}
|
|
150
200
|
};
|
|
151
201
|
});
|
|
152
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
|
+
}
|
|
153
213
|
return {
|
|
154
214
|
registerFeature,
|
|
155
215
|
unregisterFeature,
|
|
156
216
|
registerStep,
|
|
157
217
|
unregisterStep,
|
|
158
218
|
snapshot,
|
|
159
|
-
isRegistered
|
|
219
|
+
isRegistered,
|
|
160
220
|
subscribe: fn => {
|
|
161
221
|
listeners.add(fn);
|
|
162
222
|
return () => listeners.delete(fn);
|
package/dist/react/index.cjs
CHANGED
|
@@ -29,18 +29,41 @@ function useFeatureScope() {
|
|
|
29
29
|
/**
|
|
30
30
|
* FeatureRegistry
|
|
31
31
|
*
|
|
32
|
-
* The live map of everything
|
|
32
|
+
* The live map of everything registered in the React tree — both mounted
|
|
33
|
+
* (full entries) and unmounted (ghost entries).
|
|
33
34
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
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
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
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
|
|
43
|
-
*
|
|
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,14 +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) {
|
|
81
|
-
features.
|
|
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);
|
|
105
|
+
features.set(id, {
|
|
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,
|
|
113
|
+
selector: null,
|
|
114
|
+
advanceOn: null,
|
|
115
|
+
waitFor: null,
|
|
116
|
+
_ghost: true
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Prune flow steps — their selectors are dead too.
|
|
120
|
+
// They'll re-register when the component remounts on the target page.
|
|
82
121
|
flowSteps.delete(id);
|
|
83
122
|
notify();
|
|
84
123
|
}
|
|
@@ -124,41 +163,62 @@ function createFeatureRegistry() {
|
|
|
124
163
|
}
|
|
125
164
|
|
|
126
165
|
/**
|
|
127
|
-
* Returns the
|
|
166
|
+
* Returns the full feature array — both live and ghost entries.
|
|
128
167
|
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
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
|
|
172
|
+
*
|
|
173
|
+
* screen.check() returns false for ghosts so the SDK knows navigation
|
|
174
|
+
* is needed before showing the step.
|
|
131
175
|
*/
|
|
132
176
|
function snapshot() {
|
|
133
177
|
return Array.from(features.values()).map(feature => {
|
|
134
178
|
const flow = buildFlow(feature.id);
|
|
179
|
+
const isGhost = feature._ghost === true;
|
|
135
180
|
return {
|
|
136
181
|
id: feature.id,
|
|
137
182
|
name: feature.name,
|
|
138
183
|
description: feature.description,
|
|
139
|
-
selector: feature.selector,
|
|
184
|
+
selector: feature.selector || null,
|
|
140
185
|
route: feature.route || null,
|
|
141
186
|
advanceOn: feature.advanceOn || null,
|
|
142
187
|
waitFor: feature.waitFor || null,
|
|
188
|
+
_ghost: isGhost,
|
|
143
189
|
...(flow ? {
|
|
144
190
|
flow
|
|
145
191
|
} : {}),
|
|
146
192
|
screen: {
|
|
147
193
|
id: feature.id,
|
|
148
|
-
check
|
|
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
|
+
},
|
|
149
199
|
navigate: feature.navigate || null,
|
|
150
200
|
waitFor: feature.navigateWaitFor || feature.selector || null
|
|
151
201
|
}
|
|
152
202
|
};
|
|
153
203
|
});
|
|
154
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
|
+
}
|
|
155
215
|
return {
|
|
156
216
|
registerFeature,
|
|
157
217
|
unregisterFeature,
|
|
158
218
|
registerStep,
|
|
159
219
|
unregisterStep,
|
|
160
220
|
snapshot,
|
|
161
|
-
isRegistered
|
|
221
|
+
isRegistered,
|
|
162
222
|
subscribe: fn => {
|
|
163
223
|
listeners.add(fn);
|
|
164
224
|
return () => listeners.delete(fn);
|
package/dist/react/index.js
CHANGED
|
@@ -27,18 +27,41 @@ function useFeatureScope() {
|
|
|
27
27
|
/**
|
|
28
28
|
* FeatureRegistry
|
|
29
29
|
*
|
|
30
|
-
* The live map of everything
|
|
30
|
+
* The live map of everything registered in the React tree — both mounted
|
|
31
|
+
* (full entries) and unmounted (ghost entries).
|
|
31
32
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
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
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
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
|
|
41
|
-
*
|
|
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,14 +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) {
|
|
79
|
-
features.
|
|
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);
|
|
103
|
+
features.set(id, {
|
|
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,
|
|
111
|
+
selector: null,
|
|
112
|
+
advanceOn: null,
|
|
113
|
+
waitFor: null,
|
|
114
|
+
_ghost: true
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Prune flow steps — their selectors are dead too.
|
|
118
|
+
// They'll re-register when the component remounts on the target page.
|
|
80
119
|
flowSteps.delete(id);
|
|
81
120
|
notify();
|
|
82
121
|
}
|
|
@@ -122,41 +161,62 @@ function createFeatureRegistry() {
|
|
|
122
161
|
}
|
|
123
162
|
|
|
124
163
|
/**
|
|
125
|
-
* Returns the
|
|
164
|
+
* Returns the full feature array — both live and ghost entries.
|
|
126
165
|
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
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
|
|
170
|
+
*
|
|
171
|
+
* screen.check() returns false for ghosts so the SDK knows navigation
|
|
172
|
+
* is needed before showing the step.
|
|
129
173
|
*/
|
|
130
174
|
function snapshot() {
|
|
131
175
|
return Array.from(features.values()).map(feature => {
|
|
132
176
|
const flow = buildFlow(feature.id);
|
|
177
|
+
const isGhost = feature._ghost === true;
|
|
133
178
|
return {
|
|
134
179
|
id: feature.id,
|
|
135
180
|
name: feature.name,
|
|
136
181
|
description: feature.description,
|
|
137
|
-
selector: feature.selector,
|
|
182
|
+
selector: feature.selector || null,
|
|
138
183
|
route: feature.route || null,
|
|
139
184
|
advanceOn: feature.advanceOn || null,
|
|
140
185
|
waitFor: feature.waitFor || null,
|
|
186
|
+
_ghost: isGhost,
|
|
141
187
|
...(flow ? {
|
|
142
188
|
flow
|
|
143
189
|
} : {}),
|
|
144
190
|
screen: {
|
|
145
191
|
id: feature.id,
|
|
146
|
-
check
|
|
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
|
+
},
|
|
147
197
|
navigate: feature.navigate || null,
|
|
148
198
|
waitFor: feature.navigateWaitFor || feature.selector || null
|
|
149
199
|
}
|
|
150
200
|
};
|
|
151
201
|
});
|
|
152
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
|
+
}
|
|
153
213
|
return {
|
|
154
214
|
registerFeature,
|
|
155
215
|
unregisterFeature,
|
|
156
216
|
registerStep,
|
|
157
217
|
unregisterStep,
|
|
158
218
|
snapshot,
|
|
159
|
-
isRegistered
|
|
219
|
+
isRegistered,
|
|
160
220
|
subscribe: fn => {
|
|
161
221
|
listeners.add(fn);
|
|
162
222
|
return () => listeners.delete(fn);
|