@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 +77 -23
- package/dist/index.js +77 -23
- package/dist/react/index.cjs +77 -23
- package/dist/react/index.js +77 -23
- package/package.json +1 -1
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,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
|
-
|
|
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
|
|
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
|
-
*
|
|
136
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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,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
|
-
|
|
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
|
|
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
|
-
*
|
|
134
|
-
*
|
|
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
|
|
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
|
|
219
|
+
isRegistered,
|
|
166
220
|
subscribe: fn => {
|
|
167
221
|
listeners.add(fn);
|
|
168
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,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
|
-
|
|
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
|
|
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
|
-
*
|
|
136
|
-
*
|
|
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
|
|
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
|
|
221
|
+
isRegistered,
|
|
168
222
|
subscribe: fn => {
|
|
169
223
|
listeners.add(fn);
|
|
170
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,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
|
-
|
|
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
|
|
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
|
-
*
|
|
134
|
-
*
|
|
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
|
|
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
|
|
219
|
+
isRegistered,
|
|
166
220
|
subscribe: fn => {
|
|
167
221
|
listeners.add(fn);
|
|
168
222
|
return () => listeners.delete(fn);
|