tour-guide-live 0.1.0
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 +55 -0
- package/dist/index.d.mts +67 -0
- package/dist/index.d.ts +67 -0
- package/dist/index.js +817 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +796 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +38 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,817 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
TourProvider: () => TourProvider,
|
|
24
|
+
TourTooltip: () => TourTooltip,
|
|
25
|
+
useTour: () => useTour
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
|
|
29
|
+
// src/context.tsx
|
|
30
|
+
var import_react = require("react");
|
|
31
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
32
|
+
function storageKey(tourId) {
|
|
33
|
+
return `tour_progress_${tourId}`;
|
|
34
|
+
}
|
|
35
|
+
function loadProgress(tourId) {
|
|
36
|
+
if (typeof window === "undefined") return null;
|
|
37
|
+
try {
|
|
38
|
+
const raw = localStorage.getItem(storageKey(tourId));
|
|
39
|
+
return raw ? JSON.parse(raw) : null;
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function saveProgress(tourId, progress) {
|
|
45
|
+
if (typeof window === "undefined") return;
|
|
46
|
+
try {
|
|
47
|
+
localStorage.setItem(storageKey(tourId), JSON.stringify(progress));
|
|
48
|
+
} catch {
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function reducer(state, action) {
|
|
52
|
+
switch (action.type) {
|
|
53
|
+
case "SET_TOURS":
|
|
54
|
+
return { ...state, tours: action.tours, resolved: true };
|
|
55
|
+
case "START_TOUR":
|
|
56
|
+
return {
|
|
57
|
+
...state,
|
|
58
|
+
activeTourId: action.tourId,
|
|
59
|
+
currentStepIndex: action.resumeIndex,
|
|
60
|
+
completedSteps: action.completedSteps
|
|
61
|
+
};
|
|
62
|
+
case "END_TOUR":
|
|
63
|
+
return {
|
|
64
|
+
...state,
|
|
65
|
+
activeTourId: null,
|
|
66
|
+
currentStepIndex: 0,
|
|
67
|
+
completedSteps: []
|
|
68
|
+
};
|
|
69
|
+
case "GO_TO_STEP":
|
|
70
|
+
return { ...state, currentStepIndex: action.index };
|
|
71
|
+
case "COMPLETE_STEP": {
|
|
72
|
+
const tour = state.tours.find((t) => t.id === state.activeTourId);
|
|
73
|
+
if (!tour) return state;
|
|
74
|
+
const newCompleted = state.completedSteps.includes(state.currentStepIndex) ? state.completedSteps : [...state.completedSteps, state.currentStepIndex];
|
|
75
|
+
const isLastStep = state.currentStepIndex >= tour.steps.length - 1;
|
|
76
|
+
if (isLastStep) {
|
|
77
|
+
return {
|
|
78
|
+
...state,
|
|
79
|
+
completedSteps: newCompleted
|
|
80
|
+
// Tour will be ended by the effect that detects completion
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
...state,
|
|
85
|
+
completedSteps: newCompleted,
|
|
86
|
+
currentStepIndex: state.currentStepIndex + 1
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
case "SKIP_TOUR":
|
|
90
|
+
return {
|
|
91
|
+
...state,
|
|
92
|
+
activeTourId: null,
|
|
93
|
+
currentStepIndex: 0,
|
|
94
|
+
completedSteps: []
|
|
95
|
+
};
|
|
96
|
+
default:
|
|
97
|
+
return state;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
var initialState = {
|
|
101
|
+
tours: [],
|
|
102
|
+
activeTourId: null,
|
|
103
|
+
currentStepIndex: 0,
|
|
104
|
+
completedSteps: [],
|
|
105
|
+
resolved: false
|
|
106
|
+
};
|
|
107
|
+
var TourContext = (0, import_react.createContext)(null);
|
|
108
|
+
function TourProvider({
|
|
109
|
+
apiKey,
|
|
110
|
+
userId,
|
|
111
|
+
userAttributes,
|
|
112
|
+
apiBaseUrl = "",
|
|
113
|
+
children
|
|
114
|
+
}) {
|
|
115
|
+
const [state, dispatch] = (0, import_react.useReducer)(reducer, initialState);
|
|
116
|
+
const baseUrl = apiBaseUrl.replace(/\/$/, "");
|
|
117
|
+
const autoStarted = (0, import_react.useRef)(false);
|
|
118
|
+
(0, import_react.useEffect)(() => {
|
|
119
|
+
if (typeof window === "undefined") return;
|
|
120
|
+
let cancelled = false;
|
|
121
|
+
async function resolve() {
|
|
122
|
+
try {
|
|
123
|
+
const currentUrl = window.location.pathname;
|
|
124
|
+
const res = await fetch(`${baseUrl}/api/tours/resolve`, {
|
|
125
|
+
method: "POST",
|
|
126
|
+
headers: { "Content-Type": "application/json" },
|
|
127
|
+
body: JSON.stringify({ apiKey, userId, userAttributes, currentUrl })
|
|
128
|
+
});
|
|
129
|
+
if (!res.ok) {
|
|
130
|
+
console.warn("[tour-sdk] Failed to resolve tours:", res.status);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const data = await res.json();
|
|
134
|
+
if (!cancelled) {
|
|
135
|
+
dispatch({ type: "SET_TOURS", tours: data.tours ?? [] });
|
|
136
|
+
}
|
|
137
|
+
} catch (err) {
|
|
138
|
+
console.warn("[tour-sdk] Error resolving tours:", err);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
resolve();
|
|
142
|
+
return () => {
|
|
143
|
+
cancelled = true;
|
|
144
|
+
};
|
|
145
|
+
}, [baseUrl, apiKey, userId]);
|
|
146
|
+
(0, import_react.useEffect)(() => {
|
|
147
|
+
if (!state.resolved || autoStarted.current || state.tours.length === 0) return;
|
|
148
|
+
for (const tour of state.tours) {
|
|
149
|
+
const progress = loadProgress(tour.id);
|
|
150
|
+
if (progress?.completedAt || progress?.skipped) continue;
|
|
151
|
+
autoStarted.current = true;
|
|
152
|
+
const completed = progress?.completedSteps ?? [];
|
|
153
|
+
let resumeIndex = 0;
|
|
154
|
+
for (let i = 0; i < tour.steps.length; i++) {
|
|
155
|
+
if (!completed.includes(i)) {
|
|
156
|
+
resumeIndex = i;
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
dispatch({
|
|
161
|
+
type: "START_TOUR",
|
|
162
|
+
tourId: tour.id,
|
|
163
|
+
resumeIndex,
|
|
164
|
+
completedSteps: completed
|
|
165
|
+
});
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
}, [state.resolved, state.tours]);
|
|
169
|
+
const activeTour = (0, import_react.useMemo)(
|
|
170
|
+
() => state.tours.find((t) => t.id === state.activeTourId) ?? null,
|
|
171
|
+
[state.tours, state.activeTourId]
|
|
172
|
+
);
|
|
173
|
+
(0, import_react.useEffect)(() => {
|
|
174
|
+
if (!state.activeTourId || !activeTour) return;
|
|
175
|
+
const isComplete = activeTour.steps.length > 0 && state.completedSteps.length >= activeTour.steps.length;
|
|
176
|
+
saveProgress(state.activeTourId, {
|
|
177
|
+
completedSteps: state.completedSteps,
|
|
178
|
+
completedAt: isComplete ? (/* @__PURE__ */ new Date()).toISOString() : null,
|
|
179
|
+
skipped: false
|
|
180
|
+
});
|
|
181
|
+
if (isComplete) {
|
|
182
|
+
syncProgress(state.activeTourId, state.completedSteps, false, (/* @__PURE__ */ new Date()).toISOString());
|
|
183
|
+
dispatch({ type: "END_TOUR" });
|
|
184
|
+
}
|
|
185
|
+
}, [state.activeTourId, state.completedSteps, activeTour]);
|
|
186
|
+
const syncProgress = (0, import_react.useCallback)(
|
|
187
|
+
async (tourId, completedSteps, skipped, completedAt) => {
|
|
188
|
+
try {
|
|
189
|
+
await fetch(`${baseUrl}/api/tours/progress`, {
|
|
190
|
+
method: "POST",
|
|
191
|
+
headers: { "Content-Type": "application/json" },
|
|
192
|
+
body: JSON.stringify({
|
|
193
|
+
apiKey,
|
|
194
|
+
userId,
|
|
195
|
+
tourId,
|
|
196
|
+
completedSteps,
|
|
197
|
+
skipped,
|
|
198
|
+
completedAt
|
|
199
|
+
})
|
|
200
|
+
});
|
|
201
|
+
} catch (err) {
|
|
202
|
+
console.warn("[tour-sdk] Failed to sync progress:", err);
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
[baseUrl, apiKey, userId]
|
|
206
|
+
);
|
|
207
|
+
const startTour = (0, import_react.useCallback)(
|
|
208
|
+
(tourId) => {
|
|
209
|
+
const tour = state.tours.find((t) => t.id === tourId);
|
|
210
|
+
if (!tour) {
|
|
211
|
+
console.warn(`[tour-sdk] Tour "${tourId}" not found`);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const progress = loadProgress(tourId);
|
|
215
|
+
const completed = progress?.completedSteps ?? [];
|
|
216
|
+
let resumeIndex = 0;
|
|
217
|
+
for (let i = 0; i < tour.steps.length; i++) {
|
|
218
|
+
if (!completed.includes(i)) {
|
|
219
|
+
resumeIndex = i;
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
dispatch({ type: "START_TOUR", tourId, resumeIndex, completedSteps: completed });
|
|
224
|
+
},
|
|
225
|
+
[state.tours]
|
|
226
|
+
);
|
|
227
|
+
const endTour = (0, import_react.useCallback)(() => {
|
|
228
|
+
dispatch({ type: "END_TOUR" });
|
|
229
|
+
}, []);
|
|
230
|
+
const goToStep = (0, import_react.useCallback)((index) => {
|
|
231
|
+
dispatch({ type: "GO_TO_STEP", index });
|
|
232
|
+
}, []);
|
|
233
|
+
const completeStep = (0, import_react.useCallback)(() => {
|
|
234
|
+
dispatch({ type: "COMPLETE_STEP" });
|
|
235
|
+
}, []);
|
|
236
|
+
const skipTour = (0, import_react.useCallback)(() => {
|
|
237
|
+
if (state.activeTourId) {
|
|
238
|
+
saveProgress(state.activeTourId, {
|
|
239
|
+
completedSteps: state.completedSteps,
|
|
240
|
+
completedAt: null,
|
|
241
|
+
skipped: true
|
|
242
|
+
});
|
|
243
|
+
syncProgress(state.activeTourId, state.completedSteps, true, null);
|
|
244
|
+
}
|
|
245
|
+
dispatch({ type: "SKIP_TOUR" });
|
|
246
|
+
}, [state.activeTourId, state.completedSteps, syncProgress]);
|
|
247
|
+
const currentStep = activeTour ? activeTour.steps[state.currentStepIndex] ?? null : null;
|
|
248
|
+
const contextValue = (0, import_react.useMemo)(
|
|
249
|
+
() => ({
|
|
250
|
+
startTour,
|
|
251
|
+
endTour,
|
|
252
|
+
goToStep,
|
|
253
|
+
completeStep,
|
|
254
|
+
skipTour,
|
|
255
|
+
currentStep,
|
|
256
|
+
currentStepIndex: state.currentStepIndex,
|
|
257
|
+
activeTourId: state.activeTourId,
|
|
258
|
+
activeTour,
|
|
259
|
+
isActive: state.activeTourId !== null && currentStep !== null
|
|
260
|
+
}),
|
|
261
|
+
[
|
|
262
|
+
startTour,
|
|
263
|
+
endTour,
|
|
264
|
+
goToStep,
|
|
265
|
+
completeStep,
|
|
266
|
+
skipTour,
|
|
267
|
+
currentStep,
|
|
268
|
+
state.currentStepIndex,
|
|
269
|
+
state.activeTourId,
|
|
270
|
+
activeTour
|
|
271
|
+
]
|
|
272
|
+
);
|
|
273
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TourContext.Provider, { value: contextValue, children });
|
|
274
|
+
}
|
|
275
|
+
function useTour() {
|
|
276
|
+
const ctx = (0, import_react.useContext)(TourContext);
|
|
277
|
+
if (!ctx) {
|
|
278
|
+
throw new Error("useTour must be used within a TourProvider");
|
|
279
|
+
}
|
|
280
|
+
return ctx;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// src/TourTooltip.tsx
|
|
284
|
+
var import_react5 = require("react");
|
|
285
|
+
var import_react_dom = require("react-dom");
|
|
286
|
+
|
|
287
|
+
// src/hooks/useElementDetection.ts
|
|
288
|
+
var import_react2 = require("react");
|
|
289
|
+
var TIMEOUT_MS = 5e3;
|
|
290
|
+
function useElementDetection(target) {
|
|
291
|
+
const [element, setElement] = (0, import_react2.useState)(null);
|
|
292
|
+
const prevTarget = (0, import_react2.useRef)(target);
|
|
293
|
+
if (prevTarget.current !== target) {
|
|
294
|
+
prevTarget.current = target;
|
|
295
|
+
}
|
|
296
|
+
(0, import_react2.useEffect)(() => {
|
|
297
|
+
if (typeof window === "undefined" || !target) {
|
|
298
|
+
setElement(null);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const selector = target.startsWith("[") || target.startsWith("#") || target.startsWith(".") ? target : `[data-tour="${target}"]`;
|
|
302
|
+
const found = document.querySelector(selector);
|
|
303
|
+
if (found) {
|
|
304
|
+
setElement(found);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
let timedOut = false;
|
|
308
|
+
const observer = new MutationObserver(() => {
|
|
309
|
+
const el = document.querySelector(selector);
|
|
310
|
+
if (el) {
|
|
311
|
+
observer.disconnect();
|
|
312
|
+
if (!timedOut) setElement(el);
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
316
|
+
const timer = setTimeout(() => {
|
|
317
|
+
timedOut = true;
|
|
318
|
+
observer.disconnect();
|
|
319
|
+
console.warn(
|
|
320
|
+
`[tour-sdk] Element "${target}" not found after ${TIMEOUT_MS}ms \u2014 skipping step`
|
|
321
|
+
);
|
|
322
|
+
setElement(null);
|
|
323
|
+
}, TIMEOUT_MS);
|
|
324
|
+
return () => {
|
|
325
|
+
timedOut = true;
|
|
326
|
+
observer.disconnect();
|
|
327
|
+
clearTimeout(timer);
|
|
328
|
+
};
|
|
329
|
+
}, [target]);
|
|
330
|
+
return element;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// src/hooks/useTooltipPosition.ts
|
|
334
|
+
var import_react3 = require("react");
|
|
335
|
+
var ARROW_SIZE = 8;
|
|
336
|
+
var TOOLTIP_GAP = 12;
|
|
337
|
+
var VIEWPORT_PADDING = 8;
|
|
338
|
+
function useTooltipPosition(targetEl, tooltipEl, preferredPlacement) {
|
|
339
|
+
const [position, setPosition] = (0, import_react3.useState)({
|
|
340
|
+
top: 0,
|
|
341
|
+
left: 0,
|
|
342
|
+
actualPlacement: preferredPlacement,
|
|
343
|
+
arrowTop: 0,
|
|
344
|
+
arrowLeft: 0,
|
|
345
|
+
targetRect: null
|
|
346
|
+
});
|
|
347
|
+
const calculate = (0, import_react3.useCallback)(() => {
|
|
348
|
+
if (typeof window === "undefined" || !targetEl || !tooltipEl) return;
|
|
349
|
+
const targetRect = targetEl.getBoundingClientRect();
|
|
350
|
+
const tooltipRect = tooltipEl.getBoundingClientRect();
|
|
351
|
+
const vw = window.innerWidth;
|
|
352
|
+
const vh = window.innerHeight;
|
|
353
|
+
const tooltipW = tooltipRect.width;
|
|
354
|
+
const tooltipH = tooltipRect.height;
|
|
355
|
+
function getPos(p) {
|
|
356
|
+
let top2 = 0;
|
|
357
|
+
let left2 = 0;
|
|
358
|
+
switch (p) {
|
|
359
|
+
case "bottom":
|
|
360
|
+
top2 = targetRect.bottom + TOOLTIP_GAP;
|
|
361
|
+
left2 = targetRect.left + targetRect.width / 2 - tooltipW / 2;
|
|
362
|
+
break;
|
|
363
|
+
case "top":
|
|
364
|
+
top2 = targetRect.top - tooltipH - TOOLTIP_GAP;
|
|
365
|
+
left2 = targetRect.left + targetRect.width / 2 - tooltipW / 2;
|
|
366
|
+
break;
|
|
367
|
+
case "right":
|
|
368
|
+
top2 = targetRect.top + targetRect.height / 2 - tooltipH / 2;
|
|
369
|
+
left2 = targetRect.right + TOOLTIP_GAP;
|
|
370
|
+
break;
|
|
371
|
+
case "left":
|
|
372
|
+
top2 = targetRect.top + targetRect.height / 2 - tooltipH / 2;
|
|
373
|
+
left2 = targetRect.left - tooltipW - TOOLTIP_GAP;
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
return { top: top2, left: left2 };
|
|
377
|
+
}
|
|
378
|
+
function fits(p) {
|
|
379
|
+
const { top: top2, left: left2 } = getPos(p);
|
|
380
|
+
return top2 >= VIEWPORT_PADDING && left2 >= VIEWPORT_PADDING && top2 + tooltipH <= vh - VIEWPORT_PADDING && left2 + tooltipW <= vw - VIEWPORT_PADDING;
|
|
381
|
+
}
|
|
382
|
+
const opposite = {
|
|
383
|
+
top: "bottom",
|
|
384
|
+
bottom: "top",
|
|
385
|
+
left: "right",
|
|
386
|
+
right: "left"
|
|
387
|
+
};
|
|
388
|
+
const fallbackOrder = ["bottom", "top", "right", "left"];
|
|
389
|
+
let actual = preferredPlacement;
|
|
390
|
+
if (!fits(preferredPlacement)) {
|
|
391
|
+
if (fits(opposite[preferredPlacement])) {
|
|
392
|
+
actual = opposite[preferredPlacement];
|
|
393
|
+
} else {
|
|
394
|
+
actual = fallbackOrder.find(fits) ?? preferredPlacement;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
let { top, left } = getPos(actual);
|
|
398
|
+
left = Math.max(VIEWPORT_PADDING, Math.min(left, vw - tooltipW - VIEWPORT_PADDING));
|
|
399
|
+
top = Math.max(VIEWPORT_PADDING, Math.min(top, vh - tooltipH - VIEWPORT_PADDING));
|
|
400
|
+
let arrowTop = 0;
|
|
401
|
+
let arrowLeft = 0;
|
|
402
|
+
const targetCenterX = targetRect.left + targetRect.width / 2;
|
|
403
|
+
const targetCenterY = targetRect.top + targetRect.height / 2;
|
|
404
|
+
switch (actual) {
|
|
405
|
+
case "bottom":
|
|
406
|
+
arrowTop = -ARROW_SIZE;
|
|
407
|
+
arrowLeft = Math.max(12, Math.min(targetCenterX - left, tooltipW - 12));
|
|
408
|
+
break;
|
|
409
|
+
case "top":
|
|
410
|
+
arrowTop = tooltipH;
|
|
411
|
+
arrowLeft = Math.max(12, Math.min(targetCenterX - left, tooltipW - 12));
|
|
412
|
+
break;
|
|
413
|
+
case "right":
|
|
414
|
+
arrowLeft = -ARROW_SIZE;
|
|
415
|
+
arrowTop = Math.max(12, Math.min(targetCenterY - top, tooltipH - 12));
|
|
416
|
+
break;
|
|
417
|
+
case "left":
|
|
418
|
+
arrowLeft = tooltipW;
|
|
419
|
+
arrowTop = Math.max(12, Math.min(targetCenterY - top, tooltipH - 12));
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
setPosition({
|
|
423
|
+
top,
|
|
424
|
+
left,
|
|
425
|
+
actualPlacement: actual,
|
|
426
|
+
arrowTop,
|
|
427
|
+
arrowLeft,
|
|
428
|
+
targetRect
|
|
429
|
+
});
|
|
430
|
+
}, [targetEl, tooltipEl, preferredPlacement]);
|
|
431
|
+
(0, import_react3.useEffect)(() => {
|
|
432
|
+
if (typeof window === "undefined" || !targetEl || !tooltipEl) return;
|
|
433
|
+
calculate();
|
|
434
|
+
const onScroll = () => calculate();
|
|
435
|
+
window.addEventListener("scroll", onScroll, true);
|
|
436
|
+
window.addEventListener("resize", onScroll);
|
|
437
|
+
let resizeObserver;
|
|
438
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
439
|
+
resizeObserver = new ResizeObserver(() => calculate());
|
|
440
|
+
resizeObserver.observe(targetEl);
|
|
441
|
+
resizeObserver.observe(tooltipEl);
|
|
442
|
+
}
|
|
443
|
+
return () => {
|
|
444
|
+
window.removeEventListener("scroll", onScroll, true);
|
|
445
|
+
window.removeEventListener("resize", onScroll);
|
|
446
|
+
resizeObserver?.disconnect();
|
|
447
|
+
};
|
|
448
|
+
}, [targetEl, tooltipEl, calculate]);
|
|
449
|
+
return position;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// src/hooks/useStepTrigger.ts
|
|
453
|
+
var import_react4 = require("react");
|
|
454
|
+
function useStepTrigger(step, targetEl, onComplete) {
|
|
455
|
+
(0, import_react4.useEffect)(() => {
|
|
456
|
+
if (!step || step.completionTrigger !== "element-click" || !targetEl) return;
|
|
457
|
+
const handler = () => onComplete();
|
|
458
|
+
targetEl.addEventListener("click", handler, { once: true });
|
|
459
|
+
return () => targetEl.removeEventListener("click", handler);
|
|
460
|
+
}, [step, targetEl, onComplete]);
|
|
461
|
+
(0, import_react4.useEffect)(() => {
|
|
462
|
+
if (typeof window === "undefined" || !step || step.completionTrigger !== "url-change" || !step.urlTrigger)
|
|
463
|
+
return;
|
|
464
|
+
const pattern = step.urlTrigger;
|
|
465
|
+
function matchesUrl() {
|
|
466
|
+
const path = window.location.pathname;
|
|
467
|
+
const regex = new RegExp(
|
|
468
|
+
"^" + pattern.replace(/\*/g, ".*").replace(/\?/g, ".") + "$"
|
|
469
|
+
);
|
|
470
|
+
return regex.test(path);
|
|
471
|
+
}
|
|
472
|
+
if (matchesUrl()) {
|
|
473
|
+
onComplete();
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
const onUrlChange = () => {
|
|
477
|
+
if (matchesUrl()) {
|
|
478
|
+
onComplete();
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
window.addEventListener("popstate", onUrlChange);
|
|
482
|
+
const originalPush = history.pushState.bind(history);
|
|
483
|
+
const originalReplace = history.replaceState.bind(history);
|
|
484
|
+
history.pushState = function(...args) {
|
|
485
|
+
originalPush(...args);
|
|
486
|
+
onUrlChange();
|
|
487
|
+
};
|
|
488
|
+
history.replaceState = function(...args) {
|
|
489
|
+
originalReplace(...args);
|
|
490
|
+
onUrlChange();
|
|
491
|
+
};
|
|
492
|
+
return () => {
|
|
493
|
+
window.removeEventListener("popstate", onUrlChange);
|
|
494
|
+
history.pushState = originalPush;
|
|
495
|
+
history.replaceState = originalReplace;
|
|
496
|
+
};
|
|
497
|
+
}, [step, onComplete]);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// src/Spotlight.tsx
|
|
501
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
502
|
+
var PADDING = 4;
|
|
503
|
+
function Spotlight({ targetRect }) {
|
|
504
|
+
if (!targetRect) return null;
|
|
505
|
+
const top = targetRect.top - PADDING;
|
|
506
|
+
const left = targetRect.left - PADDING;
|
|
507
|
+
const width = targetRect.width + PADDING * 2;
|
|
508
|
+
const height = targetRect.height + PADDING * 2;
|
|
509
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
510
|
+
"div",
|
|
511
|
+
{
|
|
512
|
+
className: "__tg_spotlight",
|
|
513
|
+
style: {
|
|
514
|
+
position: "fixed",
|
|
515
|
+
top: 0,
|
|
516
|
+
left: 0,
|
|
517
|
+
width: "100vw",
|
|
518
|
+
height: "100vh",
|
|
519
|
+
zIndex: 9999,
|
|
520
|
+
pointerEvents: "none"
|
|
521
|
+
},
|
|
522
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
523
|
+
"svg",
|
|
524
|
+
{
|
|
525
|
+
width: "100%",
|
|
526
|
+
height: "100%",
|
|
527
|
+
style: { position: "absolute", top: 0, left: 0 },
|
|
528
|
+
children: [
|
|
529
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("defs", { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("mask", { id: "__tg_spotlight_mask", children: [
|
|
530
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("rect", { x: "0", y: "0", width: "100%", height: "100%", fill: "white" }),
|
|
531
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
532
|
+
"rect",
|
|
533
|
+
{
|
|
534
|
+
x: left,
|
|
535
|
+
y: top,
|
|
536
|
+
width,
|
|
537
|
+
height,
|
|
538
|
+
rx: "4",
|
|
539
|
+
ry: "4",
|
|
540
|
+
fill: "black"
|
|
541
|
+
}
|
|
542
|
+
)
|
|
543
|
+
] }) }),
|
|
544
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
545
|
+
"rect",
|
|
546
|
+
{
|
|
547
|
+
x: "0",
|
|
548
|
+
y: "0",
|
|
549
|
+
width: "100%",
|
|
550
|
+
height: "100%",
|
|
551
|
+
fill: "rgba(0,0,0,0.55)",
|
|
552
|
+
mask: "url(#__tg_spotlight_mask)"
|
|
553
|
+
}
|
|
554
|
+
)
|
|
555
|
+
]
|
|
556
|
+
}
|
|
557
|
+
)
|
|
558
|
+
}
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// src/TourTooltip.tsx
|
|
563
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
564
|
+
var PREFIX = "__tg_";
|
|
565
|
+
var styles = {
|
|
566
|
+
tooltip: {
|
|
567
|
+
position: "fixed",
|
|
568
|
+
zIndex: 1e4,
|
|
569
|
+
background: "#fff",
|
|
570
|
+
borderRadius: "8px",
|
|
571
|
+
boxShadow: "0 4px 24px rgba(0,0,0,0.18)",
|
|
572
|
+
padding: "16px",
|
|
573
|
+
maxWidth: "340px",
|
|
574
|
+
minWidth: "240px",
|
|
575
|
+
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
|
576
|
+
fontSize: "14px",
|
|
577
|
+
lineHeight: "1.5",
|
|
578
|
+
color: "#1a1a1a"
|
|
579
|
+
},
|
|
580
|
+
arrow: {
|
|
581
|
+
position: "absolute",
|
|
582
|
+
width: "16px",
|
|
583
|
+
height: "16px",
|
|
584
|
+
background: "#fff",
|
|
585
|
+
transform: "rotate(45deg)",
|
|
586
|
+
zIndex: -1,
|
|
587
|
+
boxShadow: "2px 2px 4px rgba(0,0,0,0.06)"
|
|
588
|
+
},
|
|
589
|
+
header: {
|
|
590
|
+
display: "flex",
|
|
591
|
+
justifyContent: "space-between",
|
|
592
|
+
alignItems: "flex-start",
|
|
593
|
+
marginBottom: "8px"
|
|
594
|
+
},
|
|
595
|
+
stepCounter: {
|
|
596
|
+
fontSize: "12px",
|
|
597
|
+
color: "#888",
|
|
598
|
+
fontWeight: 500,
|
|
599
|
+
flexShrink: 0
|
|
600
|
+
},
|
|
601
|
+
dismissBtn: {
|
|
602
|
+
background: "none",
|
|
603
|
+
border: "none",
|
|
604
|
+
cursor: "pointer",
|
|
605
|
+
fontSize: "18px",
|
|
606
|
+
color: "#999",
|
|
607
|
+
padding: "0 0 0 8px",
|
|
608
|
+
lineHeight: "1"
|
|
609
|
+
},
|
|
610
|
+
title: {
|
|
611
|
+
fontSize: "15px",
|
|
612
|
+
fontWeight: 600,
|
|
613
|
+
margin: "0 0 6px 0",
|
|
614
|
+
color: "#111"
|
|
615
|
+
},
|
|
616
|
+
body: {
|
|
617
|
+
margin: "0 0 12px 0",
|
|
618
|
+
color: "#444",
|
|
619
|
+
fontSize: "13px",
|
|
620
|
+
lineHeight: "1.55"
|
|
621
|
+
},
|
|
622
|
+
image: {
|
|
623
|
+
width: "100%",
|
|
624
|
+
borderRadius: "4px",
|
|
625
|
+
marginBottom: "12px"
|
|
626
|
+
},
|
|
627
|
+
footer: {
|
|
628
|
+
display: "flex",
|
|
629
|
+
justifyContent: "space-between",
|
|
630
|
+
alignItems: "center",
|
|
631
|
+
gap: "8px"
|
|
632
|
+
},
|
|
633
|
+
btn: {
|
|
634
|
+
padding: "6px 14px",
|
|
635
|
+
borderRadius: "6px",
|
|
636
|
+
fontSize: "13px",
|
|
637
|
+
fontWeight: 500,
|
|
638
|
+
cursor: "pointer",
|
|
639
|
+
border: "none"
|
|
640
|
+
},
|
|
641
|
+
btnPrimary: {
|
|
642
|
+
background: "#2563eb",
|
|
643
|
+
color: "#fff"
|
|
644
|
+
},
|
|
645
|
+
btnSecondary: {
|
|
646
|
+
background: "#f1f5f9",
|
|
647
|
+
color: "#334155"
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
function renderSimpleMarkdown(text) {
|
|
651
|
+
const parts = [];
|
|
652
|
+
const regex = /(\*\*(.+?)\*\*|\*(.+?)\*|`(.+?)`|\[(.+?)\]\((.+?)\))/g;
|
|
653
|
+
let lastIndex = 0;
|
|
654
|
+
let match;
|
|
655
|
+
let key = 0;
|
|
656
|
+
while ((match = regex.exec(text)) !== null) {
|
|
657
|
+
if (match.index > lastIndex) {
|
|
658
|
+
parts.push(text.slice(lastIndex, match.index));
|
|
659
|
+
}
|
|
660
|
+
if (match[2]) {
|
|
661
|
+
parts.push(/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("strong", { children: match[2] }, key++));
|
|
662
|
+
} else if (match[3]) {
|
|
663
|
+
parts.push(/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("em", { children: match[3] }, key++));
|
|
664
|
+
} else if (match[4]) {
|
|
665
|
+
parts.push(
|
|
666
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
667
|
+
"code",
|
|
668
|
+
{
|
|
669
|
+
style: {
|
|
670
|
+
background: "#f1f5f9",
|
|
671
|
+
padding: "1px 4px",
|
|
672
|
+
borderRadius: "3px",
|
|
673
|
+
fontSize: "12px"
|
|
674
|
+
},
|
|
675
|
+
children: match[4]
|
|
676
|
+
},
|
|
677
|
+
key++
|
|
678
|
+
)
|
|
679
|
+
);
|
|
680
|
+
} else if (match[5] && match[6]) {
|
|
681
|
+
parts.push(
|
|
682
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
683
|
+
"a",
|
|
684
|
+
{
|
|
685
|
+
href: match[6],
|
|
686
|
+
target: "_blank",
|
|
687
|
+
rel: "noopener noreferrer",
|
|
688
|
+
style: { color: "#2563eb", textDecoration: "underline" },
|
|
689
|
+
children: match[5]
|
|
690
|
+
},
|
|
691
|
+
key++
|
|
692
|
+
)
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
lastIndex = match.index + match[0].length;
|
|
696
|
+
}
|
|
697
|
+
if (lastIndex < text.length) {
|
|
698
|
+
parts.push(text.slice(lastIndex));
|
|
699
|
+
}
|
|
700
|
+
return parts;
|
|
701
|
+
}
|
|
702
|
+
function TourTooltip() {
|
|
703
|
+
const {
|
|
704
|
+
isActive,
|
|
705
|
+
currentStep,
|
|
706
|
+
currentStepIndex,
|
|
707
|
+
activeTour,
|
|
708
|
+
completeStep,
|
|
709
|
+
goToStep,
|
|
710
|
+
skipTour
|
|
711
|
+
} = useTour();
|
|
712
|
+
const targetEl = useElementDetection(currentStep?.target ?? null);
|
|
713
|
+
const tooltipRef = (0, import_react5.useRef)(null);
|
|
714
|
+
const [portalContainer, setPortalContainer] = (0, import_react5.useState)(null);
|
|
715
|
+
(0, import_react5.useEffect)(() => {
|
|
716
|
+
if (typeof document === "undefined") return;
|
|
717
|
+
let container = document.getElementById(`${PREFIX}portal`);
|
|
718
|
+
if (!container) {
|
|
719
|
+
container = document.createElement("div");
|
|
720
|
+
container.id = `${PREFIX}portal`;
|
|
721
|
+
document.body.appendChild(container);
|
|
722
|
+
}
|
|
723
|
+
setPortalContainer(container);
|
|
724
|
+
}, []);
|
|
725
|
+
const position = useTooltipPosition(
|
|
726
|
+
targetEl,
|
|
727
|
+
tooltipRef.current,
|
|
728
|
+
currentStep?.placement ?? "bottom"
|
|
729
|
+
);
|
|
730
|
+
useStepTrigger(currentStep, targetEl, completeStep);
|
|
731
|
+
if (!isActive || !currentStep || !activeTour || !portalContainer) {
|
|
732
|
+
return null;
|
|
733
|
+
}
|
|
734
|
+
const totalSteps = activeTour.steps.length;
|
|
735
|
+
const isFirst = currentStepIndex === 0;
|
|
736
|
+
const isLast = currentStepIndex === totalSteps - 1;
|
|
737
|
+
const content = /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
738
|
+
currentStep.spotlight && targetEl && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Spotlight, { targetRect: position.targetRect }),
|
|
739
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
740
|
+
"div",
|
|
741
|
+
{
|
|
742
|
+
ref: tooltipRef,
|
|
743
|
+
className: `${PREFIX}tooltip`,
|
|
744
|
+
style: {
|
|
745
|
+
...styles.tooltip,
|
|
746
|
+
top: `${position.top}px`,
|
|
747
|
+
left: `${position.left}px`
|
|
748
|
+
},
|
|
749
|
+
children: [
|
|
750
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
751
|
+
"div",
|
|
752
|
+
{
|
|
753
|
+
style: {
|
|
754
|
+
...styles.arrow,
|
|
755
|
+
top: `${position.arrowTop}px`,
|
|
756
|
+
left: `${position.arrowLeft}px`
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
),
|
|
760
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: styles.header, children: [
|
|
761
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: styles.stepCounter, children: [
|
|
762
|
+
currentStepIndex + 1,
|
|
763
|
+
" of ",
|
|
764
|
+
totalSteps
|
|
765
|
+
] }),
|
|
766
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
767
|
+
"button",
|
|
768
|
+
{
|
|
769
|
+
style: styles.dismissBtn,
|
|
770
|
+
onClick: skipTour,
|
|
771
|
+
"aria-label": "Dismiss tour",
|
|
772
|
+
children: "\xD7"
|
|
773
|
+
}
|
|
774
|
+
)
|
|
775
|
+
] }),
|
|
776
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h4", { style: styles.title, children: currentStep.title }),
|
|
777
|
+
currentStep.imageUrl && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
778
|
+
"img",
|
|
779
|
+
{
|
|
780
|
+
src: currentStep.imageUrl,
|
|
781
|
+
alt: "",
|
|
782
|
+
style: styles.image
|
|
783
|
+
}
|
|
784
|
+
),
|
|
785
|
+
currentStep.body && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: styles.body, children: renderSimpleMarkdown(currentStep.body) }),
|
|
786
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: styles.footer, children: [
|
|
787
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { children: !isFirst && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
788
|
+
"button",
|
|
789
|
+
{
|
|
790
|
+
style: { ...styles.btn, ...styles.btnSecondary },
|
|
791
|
+
onClick: () => goToStep(currentStepIndex - 1),
|
|
792
|
+
children: "Back"
|
|
793
|
+
}
|
|
794
|
+
) }),
|
|
795
|
+
currentStep.completionTrigger === "next-button" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
796
|
+
"button",
|
|
797
|
+
{
|
|
798
|
+
style: { ...styles.btn, ...styles.btnPrimary },
|
|
799
|
+
onClick: completeStep,
|
|
800
|
+
children: isLast ? "Finish" : "Next"
|
|
801
|
+
}
|
|
802
|
+
),
|
|
803
|
+
currentStep.completionTrigger !== "next-button" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { fontSize: "12px", color: "#888" }, children: currentStep.completionTrigger === "element-click" ? "Click the highlighted element" : currentStep.completionTrigger === "url-change" ? "Navigate to continue" : "Waiting..." })
|
|
804
|
+
] })
|
|
805
|
+
]
|
|
806
|
+
}
|
|
807
|
+
)
|
|
808
|
+
] });
|
|
809
|
+
return (0, import_react_dom.createPortal)(content, portalContainer);
|
|
810
|
+
}
|
|
811
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
812
|
+
0 && (module.exports = {
|
|
813
|
+
TourProvider,
|
|
814
|
+
TourTooltip,
|
|
815
|
+
useTour
|
|
816
|
+
});
|
|
817
|
+
//# sourceMappingURL=index.js.map
|