@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/LICENCE +0 -0
- package/README.md +308 -37
- package/dist/cjs/client.js +54 -52
- package/dist/cjs/errors.js +28 -33
- package/dist/index.cjs +473 -0
- package/dist/index.js +464 -29
- package/package.json +25 -18
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;
|