@eventop/sdk 1.0.2 → 1.0.4

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 ADDED
@@ -0,0 +1,473 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+
5
+ /**
6
+ * Root context — holds the global feature registry.
7
+ * Set by EventopAIProvider at the root of the app.
8
+ */
9
+ const EventopRegistryContext = /*#__PURE__*/react.createContext(null);
10
+
11
+ /**
12
+ * Feature scope context — set by EventopTarget.
13
+ * Tells any EventopStep inside the tree which feature it belongs to,
14
+ * so you can nest EventopStep inside a EventopTarget without repeating
15
+ * the feature id. Also supports explicit feature="id" on EventopStep
16
+ * for steps that live outside their parent EventopTarget in the tree.
17
+ */
18
+ const EventopFeatureScopeContext = /*#__PURE__*/react.createContext(null);
19
+ function useRegistry() {
20
+ const ctx = react.useContext(EventopRegistryContext);
21
+ if (!ctx) throw new Error('[EventopAI] Must be used inside <EventopAIProvider>.');
22
+ return ctx;
23
+ }
24
+ function useFeatureScope() {
25
+ return react.useContext(EventopFeatureScopeContext); // null is fine — steps can declare feature explicitly
26
+ }
27
+
28
+ /**
29
+ * FeatureRegistry
30
+ *
31
+ * The live map of everything currently mounted in the React tree.
32
+ *
33
+ * Features — registered by ShepherdTarget
34
+ * Flow steps — registered by ShepherdStep, attached to a feature by id
35
+ *
36
+ * Both features and steps can live anywhere in the component tree.
37
+ * They don't need to be co-located. A ShepherdStep just needs to know
38
+ * which feature id it belongs to.
39
+ *
40
+ * Nested steps:
41
+ * Steps can themselves have children steps (sub-steps) by passing
42
+ * a parentStep prop to ShepherdStep. This lets you model flows like:
43
+ *
44
+ * Feature: "Create styled text"
45
+ * Step 0: Click Add Text
46
+ * Step 1: Type your content ← parentStep: 0
47
+ * Step 1.0: Select the text ← parentStep: 1, index: 0
48
+ * Step 1.1: Open font picker ← parentStep: 1, index: 1
49
+ * Step 1.2: Choose a font ← parentStep: 1, index: 2
50
+ * Step 2: Click Done
51
+ *
52
+ * In practice most flows are flat (index 0, 1, 2, 3...) but the
53
+ * registry supports nested depth for complex interactions.
54
+ */
55
+ function createFeatureRegistry() {
56
+ // Map<featureId, featureData>
57
+ const features = new Map();
58
+ // Map<featureId, Map<stepKey, stepData>>
59
+ // stepKey is either a number (flat) or "parentIndex.childIndex" (nested)
60
+ const flowSteps = new Map();
61
+ const listeners = new Set();
62
+ function notify() {
63
+ listeners.forEach(fn => fn());
64
+ }
65
+
66
+ // ── Feature registration ─────────────────────────────────────────────────
67
+
68
+ function registerFeature(feature) {
69
+ features.set(feature.id, {
70
+ ...feature,
71
+ _registeredAt: Date.now()
72
+ });
73
+ notify();
74
+ }
75
+ function unregisterFeature(id) {
76
+ features.delete(id);
77
+ flowSteps.delete(id);
78
+ notify();
79
+ }
80
+
81
+ // ── Step registration ────────────────────────────────────────────────────
82
+
83
+ /**
84
+ * Register a flow step.
85
+ *
86
+ * @param {string} featureId - Parent feature id
87
+ * @param {number} index - Position in the flat flow (0, 1, 2…)
88
+ * @param {number|null} parentStep - If set, this is a sub-step of parentStep
89
+ * @param {object} stepData - { selector, waitFor, advanceOn, ... }
90
+ */
91
+ function registerStep(featureId, index, parentStep, stepData) {
92
+ if (!flowSteps.has(featureId)) {
93
+ flowSteps.set(featureId, new Map());
94
+ }
95
+ const key = parentStep != null ? `${parentStep}.${index}` : String(index);
96
+ flowSteps.get(featureId).set(key, {
97
+ ...stepData,
98
+ index,
99
+ parentStep: parentStep ?? null,
100
+ key
101
+ });
102
+ notify();
103
+ }
104
+ function unregisterStep(featureId, index, parentStep) {
105
+ const map = flowSteps.get(featureId);
106
+ if (!map) return;
107
+ const key = parentStep != null ? `${parentStep}.${index}` : String(index);
108
+ map.delete(key);
109
+ if (map.size === 0) flowSteps.delete(featureId);
110
+ notify();
111
+ }
112
+
113
+ // ── Snapshot ─────────────────────────────────────────────────────────────
114
+
115
+ /**
116
+ * Build the flat flow array the SDK core expects.
117
+ *
118
+ * For nested steps we inline sub-steps after their parent step,
119
+ * in index order. This means a flow like:
120
+ *
121
+ * Step 0 (flat)
122
+ * Step 1 (flat)
123
+ * Step 1.0 (sub-step of 1)
124
+ * Step 1.1 (sub-step of 1)
125
+ * Step 2 (flat)
126
+ *
127
+ * becomes the SDK flow: [step0, step1, step1.0, step1.1, step2]
128
+ *
129
+ * The AI writes titles/text for the parent feature.
130
+ * Sub-steps inherit the parent step's text with a "(N/M)" suffix added
131
+ * by the SDK's expandFlowSteps() function.
132
+ */
133
+ function buildFlow(featureId) {
134
+ const map = flowSteps.get(featureId);
135
+ if (!map || map.size === 0) return null;
136
+ const allSteps = Array.from(map.values());
137
+
138
+ // Separate top-level and nested steps
139
+ const topLevel = allSteps.filter(s => s.parentStep == null).sort((a, b) => a.index - b.index);
140
+ const result = [];
141
+ topLevel.forEach(step => {
142
+ result.push(step);
143
+ // Inline any sub-steps after the parent
144
+ const children = allSteps.filter(s => s.parentStep === step.index).sort((a, b) => a.index - b.index);
145
+ result.push(...children);
146
+ });
147
+ return result.length > 0 ? result : null;
148
+ }
149
+
150
+ /**
151
+ * Returns the live feature array in the format the SDK core expects.
152
+ * screen.check() is automatic — mounted = available.
153
+ */
154
+ function snapshot() {
155
+ return Array.from(features.values()).map(feature => {
156
+ const flow = buildFlow(feature.id);
157
+ return {
158
+ id: feature.id,
159
+ name: feature.name,
160
+ description: feature.description,
161
+ selector: feature.selector,
162
+ advanceOn: feature.advanceOn || null,
163
+ waitFor: feature.waitFor || null,
164
+ ...(flow ? {
165
+ flow
166
+ } : {}),
167
+ screen: {
168
+ id: feature.id,
169
+ check: () => features.has(feature.id),
170
+ navigate: feature.navigate || null,
171
+ waitFor: feature.navigateWaitFor || feature.selector || null
172
+ }
173
+ };
174
+ });
175
+ }
176
+ return {
177
+ registerFeature,
178
+ unregisterFeature,
179
+ registerStep,
180
+ unregisterStep,
181
+ snapshot,
182
+ isRegistered: id => features.has(id),
183
+ subscribe: fn => {
184
+ listeners.add(fn);
185
+ return () => listeners.delete(fn);
186
+ }
187
+ };
188
+ }
189
+
190
+ /**
191
+ * EventopProvider
192
+ *
193
+ * Drop this once at the root of your app.
194
+ * Every EventopTarget and EventopStep anywhere in the tree will
195
+ * register with this provider automatically.
196
+ *
197
+ * @example
198
+ * <EventopProvider
199
+ * provider={myServerFetcher}
200
+ * appName="PixelCraft"
201
+ * assistantName="Pixel AI"
202
+ * suggestions={['Add a shadow', 'Export design']}
203
+ * theme={{ mode: 'dark', tokens: { accent: '#6366f1' } }}
204
+ * position={{ corner: 'bottom-right' }}
205
+ * >
206
+ * <App />
207
+ * </EventopProvider>
208
+ */
209
+ function EventopProvider({
210
+ children,
211
+ provider,
212
+ appName,
213
+ assistantName,
214
+ suggestions,
215
+ theme,
216
+ position
217
+ }) {
218
+ if (!provider) throw new Error('[Eventop] <EventopProvider> requires a provider prop.');
219
+ if (!appName) throw new Error('[Eventop] <EventopProvider> requires an appName prop.');
220
+ const registry = react.useRef(createFeatureRegistry()).current;
221
+ const sdkReady = react.useRef(false);
222
+ const syncToSDK = react.useCallback(() => {
223
+ if (!sdkReady.current || !window.Eventop) return;
224
+ window.Eventop._updateConfig?.({
225
+ features: registry.snapshot()
226
+ });
227
+ }, [registry]);
228
+ react.useEffect(() => {
229
+ function boot() {
230
+ window.Eventop.init({
231
+ provider,
232
+ config: {
233
+ appName,
234
+ assistantName,
235
+ suggestions,
236
+ theme,
237
+ position,
238
+ features: registry.snapshot(),
239
+ _providerName: 'custom'
240
+ }
241
+ });
242
+ sdkReady.current = true;
243
+ syncToSDK();
244
+ }
245
+ boot();
246
+ const unsub = registry.subscribe(syncToSDK);
247
+ return () => {
248
+ unsub();
249
+ window.Eventop?.cancelTour();
250
+ };
251
+ }, []);
252
+ const ctx = {
253
+ registerFeature: registry.registerFeature,
254
+ unregisterFeature: registry.unregisterFeature,
255
+ registerStep: registry.registerStep,
256
+ unregisterStep: registry.unregisterStep,
257
+ isRegistered: registry.isRegistered
258
+ };
259
+ return /*#__PURE__*/React.createElement(EventopRegistryContext.Provider, {
260
+ value: ctx
261
+ }, children);
262
+ }
263
+
264
+ /**
265
+ * EventopTarget
266
+ *
267
+ * Wraps any component and registers it as an Eventop feature at the call site.
268
+ * The wrapped component does not need to know about Eventop.
269
+ */
270
+ function EventopTarget({
271
+ children,
272
+ id,
273
+ name,
274
+ description,
275
+ navigate,
276
+ navigateWaitFor,
277
+ advanceOn,
278
+ waitFor,
279
+ ...rest
280
+ }) {
281
+ const registry = useRegistry();
282
+ const ref = react.useRef(null);
283
+ const dataAttr = `data-evtp-${id}`;
284
+ const selector = `[${dataAttr}]`;
285
+ react.useEffect(() => {
286
+ if (!id || !name) {
287
+ console.warn('[Eventop] <EventopTarget> requires id and name props.');
288
+ return;
289
+ }
290
+ registry.registerFeature({
291
+ id,
292
+ name,
293
+ description,
294
+ selector,
295
+ navigate,
296
+ navigateWaitFor,
297
+ waitFor,
298
+ advanceOn: advanceOn ? {
299
+ selector,
300
+ ...advanceOn
301
+ } : null
302
+ });
303
+ return () => registry.unregisterFeature(id);
304
+ }, [id, name, description]);
305
+ const child = react.Children.only(children);
306
+ let wrapped;
307
+ try {
308
+ wrapped = /*#__PURE__*/react.cloneElement(child, {
309
+ [dataAttr]: '',
310
+ ref: node => {
311
+ ref.current = node;
312
+ const originalRef = child.ref;
313
+ if (typeof originalRef === 'function') originalRef(node);else if (originalRef && 'current' in originalRef) originalRef.current = node;
314
+ }
315
+ });
316
+ } catch {
317
+ wrapped = /*#__PURE__*/React.createElement("span", {
318
+ [dataAttr]: '',
319
+ ref: ref,
320
+ style: {
321
+ display: 'contents'
322
+ }
323
+ }, child);
324
+ }
325
+ return /*#__PURE__*/React.createElement(EventopFeatureScopeContext.Provider, {
326
+ value: id
327
+ }, wrapped);
328
+ }
329
+
330
+ /**
331
+ * EventopStep
332
+ *
333
+ * Registers one step in a multi-step flow. Can live anywhere in the tree.
334
+ * Steps self-assemble into order via the `index` prop.
335
+ */
336
+ function EventopStep({
337
+ children,
338
+ feature,
339
+ index,
340
+ parentStep,
341
+ waitFor,
342
+ advanceOn
343
+ }) {
344
+ const registry = useRegistry();
345
+ const featureScope = useFeatureScope();
346
+ const featureId = feature || featureScope;
347
+ const ref = react.useRef(null);
348
+ if (!featureId) {
349
+ console.warn('[Eventop] <EventopStep> needs either a feature prop or an <EventopTarget> ancestor.');
350
+ }
351
+ if (index == null) {
352
+ console.warn('[Eventop] <EventopStep> requires an index prop.');
353
+ }
354
+ const dataAttr = `data-evtp-step-${featureId}-${parentStep != null ? `${parentStep}-` : ''}${index}`;
355
+ const selector = `[${dataAttr}]`;
356
+ react.useEffect(() => {
357
+ if (!featureId || index == null) return;
358
+ registry.registerStep(featureId, index, parentStep ?? null, {
359
+ selector,
360
+ waitFor: waitFor || null,
361
+ advanceOn: advanceOn ? {
362
+ selector,
363
+ ...advanceOn
364
+ } : null
365
+ });
366
+ return () => registry.unregisterStep(featureId, index, parentStep ?? null);
367
+ }, [featureId, index, parentStep]);
368
+ const child = react.Children.only(children);
369
+ let wrapped;
370
+ try {
371
+ wrapped = /*#__PURE__*/react.cloneElement(child, {
372
+ [dataAttr]: '',
373
+ ref: node => {
374
+ ref.current = node;
375
+ const originalRef = child.ref;
376
+ if (typeof originalRef === 'function') originalRef(node);else if (originalRef && 'current' in originalRef) originalRef.current = node;
377
+ }
378
+ });
379
+ } catch {
380
+ wrapped = /*#__PURE__*/React.createElement("span", {
381
+ [dataAttr]: '',
382
+ ref: ref,
383
+ style: {
384
+ display: 'contents'
385
+ }
386
+ }, child);
387
+ }
388
+ return wrapped;
389
+ }
390
+
391
+ // ═══════════════════════════════════════════════════════════════════════════
392
+ // useEventopAI
393
+ //
394
+ // Access the SDK programmatic API from inside any component.
395
+ // Use for stepComplete(), stepFail(), open(), close() etc.
396
+ //
397
+ // @example
398
+ // function CheckoutForm() {
399
+ // const { stepComplete, stepFail } = useEventopAI();
400
+ //
401
+ // async function handleNext() {
402
+ // const ok = await validateEmail(email);
403
+ // if (ok) stepComplete();
404
+ // else stepFail('Please enter a valid email address.');
405
+ // }
406
+ // }
407
+ // ═══════════════════════════════════════════════════════════════════════════
408
+
409
+ function useEventop() {
410
+ const sdk = () => window.Eventop;
411
+ return {
412
+ open: () => sdk()?.open(),
413
+ close: () => sdk()?.close(),
414
+ cancelTour: () => sdk()?.cancelTour(),
415
+ resumeTour: () => sdk()?.resumeTour(),
416
+ isActive: () => sdk()?.isActive() ?? false,
417
+ isPaused: () => sdk()?.isPaused() ?? false,
418
+ stepComplete: () => sdk()?.stepComplete(),
419
+ stepFail: msg => sdk()?.stepFail(msg),
420
+ runTour: steps => sdk()?.runTour(steps)
421
+ };
422
+ }
423
+
424
+ // ═══════════════════════════════════════════════════════════════════════════
425
+ // useEventopTour
426
+ //
427
+ // Reactively tracks tour state so you can render your own UI.
428
+ // Polls at 300ms — lightweight enough for a status indicator.
429
+ //
430
+ // @example
431
+ // function TourBar() {
432
+ // const { isActive, isPaused, resume, cancel } = useEventopTour();
433
+ // if (!isActive && !isPaused) return null;
434
+ // return (
435
+ // <div>
436
+ // {isPaused && <button onClick={resume}>Resume tour</button>}
437
+ // <button onClick={cancel}>End</button>
438
+ // </div>
439
+ // );
440
+ // }
441
+ // ═══════════════════════════════════════════════════════════════════════════
442
+
443
+ function useEventopTour() {
444
+ const [state, setState] = react.useState({
445
+ isActive: false,
446
+ isPaused: false
447
+ });
448
+ react.useEffect(() => {
449
+ const id = setInterval(() => {
450
+ const sdk = window.Eventop;
451
+ if (!sdk) return;
452
+ const next = {
453
+ isActive: sdk.isActive(),
454
+ isPaused: sdk.isPaused()
455
+ };
456
+ setState(prev => prev.isActive !== next.isActive || prev.isPaused !== next.isPaused ? next : prev);
457
+ }, 300);
458
+ return () => clearInterval(id);
459
+ }, []);
460
+ return {
461
+ ...state,
462
+ resume: react.useCallback(() => window.Eventop?.resumeTour(), []),
463
+ cancel: react.useCallback(() => window.Eventop?.cancelTour(), []),
464
+ open: react.useCallback(() => window.Eventop?.open(), []),
465
+ close: react.useCallback(() => window.Eventop?.close(), [])
466
+ };
467
+ }
468
+
469
+ exports.EventopProvider = EventopProvider;
470
+ exports.EventopStep = EventopStep;
471
+ exports.EventopTarget = EventopTarget;
472
+ exports.useEventop = useEventop;
473
+ exports.useEventopTour = useEventopTour;