@cognior/iap-sdk 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.
Potentially problematic release.
This version of @cognior/iap-sdk might be problematic. Click here for more details.
- package/.github/copilot-instructions.md +95 -0
- package/README.md +79 -0
- package/TRACKING.md +105 -0
- package/USER_CONTEXT_README.md +284 -0
- package/package.json +154 -0
- package/src/config.ts +25 -0
- package/src/core/flowEngine.ts +1833 -0
- package/src/core/triggerManager.ts +1011 -0
- package/src/experiences/banner.ts +366 -0
- package/src/experiences/beacon.ts +668 -0
- package/src/experiences/hotspotTour.ts +654 -0
- package/src/experiences/hotspots.ts +566 -0
- package/src/experiences/modal.ts +1337 -0
- package/src/experiences/modalSequence.ts +1247 -0
- package/src/experiences/popover.ts +652 -0
- package/src/experiences/registry.ts +21 -0
- package/src/experiences/survey.ts +1639 -0
- package/src/experiences/taskList.ts +625 -0
- package/src/experiences/tooltip.ts +740 -0
- package/src/experiences/types.ts +395 -0
- package/src/experiences/walkthrough.ts +670 -0
- package/src/flow-sequence.ts +177 -0
- package/src/flows.ts +512 -0
- package/src/http.ts +61 -0
- package/src/index.ts +355 -0
- package/src/services/flowManager.ts +905 -0
- package/src/services/flowNormalizer.ts +74 -0
- package/src/services/locationContextService.ts +189 -0
- package/src/services/pageContextService.ts +221 -0
- package/src/services/userContextService.ts +286 -0
- package/src/state/appState.ts +0 -0
- package/src/state/hooks.ts +0 -0
- package/src/state/index.ts +0 -0
- package/src/state/migration.ts +0 -0
- package/src/state/store.ts +0 -0
- package/src/styles/banner.css.ts +0 -0
- package/src/styles/hotspot.css.ts +0 -0
- package/src/styles/hotspotTour.css.ts +0 -0
- package/src/styles/modal.css.ts +564 -0
- package/src/styles/survey.css.ts +1013 -0
- package/src/styles/taskList.css.ts +0 -0
- package/src/styles/tooltip.css.ts +149 -0
- package/src/styles/walkthrough.css.ts +0 -0
- package/src/tourUtils.ts +0 -0
- package/src/tracking.ts +223 -0
- package/src/utils/debounce.ts +66 -0
- package/src/utils/eventSequenceValidator.ts +124 -0
- package/src/utils/flowTrackingSystem.ts +524 -0
- package/src/utils/idGenerator.ts +155 -0
- package/src/utils/immediateValidationPrevention.ts +184 -0
- package/src/utils/normalize.ts +50 -0
- package/src/utils/privacyManager.ts +166 -0
- package/src/utils/ruleEvaluator.ts +199 -0
- package/src/utils/sanitize.ts +79 -0
- package/src/utils/selectors.ts +107 -0
- package/src/utils/stepExecutor.ts +345 -0
- package/src/utils/triggerNormalizer.ts +149 -0
- package/src/utils/validationInterceptor.ts +650 -0
- package/tsconfig.json +13 -0
- package/tsup.config.ts +13 -0
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
// src/experiences/hotspotTour.ts
|
|
2
|
+
// Sequential guided tour experience with hotspots
|
|
3
|
+
|
|
4
|
+
import { sanitizeHtml } from "../utils/sanitize";
|
|
5
|
+
import { register } from "./registry";
|
|
6
|
+
import { waitForElement } from "../utils/triggerNormalizer";
|
|
7
|
+
import type { HotspotTourPayload, HotspotTourStep, CornerPlacement } from "./types";
|
|
8
|
+
|
|
9
|
+
type HotspotTourFlow = { id: string; type: "hotspotTour"; payload: HotspotTourPayload };
|
|
10
|
+
|
|
11
|
+
const hotspotTourCssText = `
|
|
12
|
+
:root {
|
|
13
|
+
--dap-z-tour: 2147483635;
|
|
14
|
+
--dap-tour-primary: #3b82f6;
|
|
15
|
+
--dap-tour-bg: #ffffff;
|
|
16
|
+
--dap-tour-border: #e2e8f0;
|
|
17
|
+
--dap-tour-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
|
|
18
|
+
--dap-tour-text: #1e293b;
|
|
19
|
+
--dap-tour-text-muted: #64748b;
|
|
20
|
+
--dap-tour-overlay: rgba(15, 23, 42, 0.5);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.dap-tour-overlay {
|
|
24
|
+
position: fixed;
|
|
25
|
+
inset: 0;
|
|
26
|
+
background: var(--dap-tour-overlay);
|
|
27
|
+
z-index: var(--dap-z-tour);
|
|
28
|
+
pointer-events: none;
|
|
29
|
+
opacity: 0;
|
|
30
|
+
animation: tourOverlayFadeIn 0.3s ease-out forwards;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@keyframes tourOverlayFadeIn {
|
|
34
|
+
to { opacity: 1; }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.dap-tour-spotlight {
|
|
38
|
+
position: absolute;
|
|
39
|
+
border: 3px solid var(--dap-tour-primary);
|
|
40
|
+
border-radius: 8px;
|
|
41
|
+
pointer-events: none;
|
|
42
|
+
z-index: calc(var(--dap-z-tour) + 1);
|
|
43
|
+
box-shadow: 0 0 0 9999px rgba(15, 23, 42, 0.5);
|
|
44
|
+
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
|
45
|
+
animation: tourSpotlightPulse 2s ease-in-out infinite;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@keyframes tourSpotlightPulse {
|
|
49
|
+
0%, 100% {
|
|
50
|
+
border-color: var(--dap-tour-primary);
|
|
51
|
+
box-shadow: 0 0 0 9999px rgba(15, 23, 42, 0.5), 0 0 20px rgba(59, 130, 246, 0.3);
|
|
52
|
+
}
|
|
53
|
+
50% {
|
|
54
|
+
border-color: #60a5fa;
|
|
55
|
+
box-shadow: 0 0 0 9999px rgba(15, 23, 42, 0.4), 0 0 30px rgba(59, 130, 246, 0.5);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.dap-tour-tooltip {
|
|
60
|
+
position: absolute;
|
|
61
|
+
background: var(--dap-tour-bg);
|
|
62
|
+
border: 1px solid var(--dap-tour-border);
|
|
63
|
+
border-radius: 12px;
|
|
64
|
+
box-shadow: var(--dap-tour-shadow);
|
|
65
|
+
padding: 20px;
|
|
66
|
+
max-width: 360px;
|
|
67
|
+
z-index: calc(var(--dap-z-tour) + 2);
|
|
68
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
69
|
+
opacity: 0;
|
|
70
|
+
transform: scale(0.9);
|
|
71
|
+
animation: tourTooltipIn 0.3s ease-out 0.2s forwards;
|
|
72
|
+
pointer-events: auto;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@keyframes tourTooltipIn {
|
|
76
|
+
to {
|
|
77
|
+
opacity: 1;
|
|
78
|
+
transform: scale(1);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.dap-tour-tooltip::before {
|
|
83
|
+
content: '';
|
|
84
|
+
position: absolute;
|
|
85
|
+
width: 0;
|
|
86
|
+
height: 0;
|
|
87
|
+
border: 12px solid transparent;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.dap-tour-tooltip.top::before {
|
|
91
|
+
bottom: -24px;
|
|
92
|
+
left: 50%;
|
|
93
|
+
transform: translateX(-50%);
|
|
94
|
+
border-top-color: var(--dap-tour-bg);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.dap-tour-tooltip.bottom::before {
|
|
98
|
+
top: -24px;
|
|
99
|
+
left: 50%;
|
|
100
|
+
transform: translateX(-50%);
|
|
101
|
+
border-bottom-color: var(--dap-tour-bg);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.dap-tour-tooltip.left::before {
|
|
105
|
+
right: -24px;
|
|
106
|
+
top: 50%;
|
|
107
|
+
transform: translateY(-50%);
|
|
108
|
+
border-left-color: var(--dap-tour-bg);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.dap-tour-tooltip.right::before {
|
|
112
|
+
left: -24px;
|
|
113
|
+
top: 50%;
|
|
114
|
+
transform: translateY(-50%);
|
|
115
|
+
border-right-color: var(--dap-tour-bg);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.dap-tour-header {
|
|
119
|
+
display: flex;
|
|
120
|
+
align-items: center;
|
|
121
|
+
justify-content: space-between;
|
|
122
|
+
margin-bottom: 12px;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.dap-tour-title {
|
|
126
|
+
font-size: 18px;
|
|
127
|
+
font-weight: 600;
|
|
128
|
+
color: var(--dap-tour-text);
|
|
129
|
+
margin: 0;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.dap-tour-step-indicator {
|
|
133
|
+
font-size: 12px;
|
|
134
|
+
color: var(--dap-tour-text-muted);
|
|
135
|
+
background: var(--dap-tour-border);
|
|
136
|
+
padding: 2px 8px;
|
|
137
|
+
border-radius: 12px;
|
|
138
|
+
font-weight: 500;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.dap-tour-description {
|
|
142
|
+
font-size: 14px;
|
|
143
|
+
color: var(--dap-tour-text-muted);
|
|
144
|
+
line-height: 1.5;
|
|
145
|
+
margin: 0 0 16px 0;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.dap-tour-actions {
|
|
149
|
+
display: flex;
|
|
150
|
+
gap: 8px;
|
|
151
|
+
justify-content: space-between;
|
|
152
|
+
align-items: center;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.dap-tour-nav {
|
|
156
|
+
display: flex;
|
|
157
|
+
gap: 8px;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.dap-tour-btn {
|
|
161
|
+
padding: 8px 16px;
|
|
162
|
+
border: 1px solid var(--dap-tour-border);
|
|
163
|
+
border-radius: 6px;
|
|
164
|
+
background: transparent;
|
|
165
|
+
color: var(--dap-tour-text);
|
|
166
|
+
font-size: 14px;
|
|
167
|
+
font-weight: 500;
|
|
168
|
+
cursor: pointer;
|
|
169
|
+
transition: all 0.15s ease;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.dap-tour-btn:hover {
|
|
173
|
+
background: var(--dap-tour-border);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.dap-tour-btn.primary {
|
|
177
|
+
background: var(--dap-tour-primary);
|
|
178
|
+
border-color: var(--dap-tour-primary);
|
|
179
|
+
color: white;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.dap-tour-btn.primary:hover {
|
|
183
|
+
background: #2563eb;
|
|
184
|
+
border-color: #2563eb;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.dap-tour-skip {
|
|
188
|
+
font-size: 12px;
|
|
189
|
+
color: var(--dap-tour-text-muted);
|
|
190
|
+
text-decoration: underline;
|
|
191
|
+
cursor: pointer;
|
|
192
|
+
padding: 4px;
|
|
193
|
+
border: none;
|
|
194
|
+
background: none;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.dap-tour-skip:hover {
|
|
198
|
+
color: var(--dap-tour-text);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.dap-tour-progress {
|
|
202
|
+
position: fixed;
|
|
203
|
+
top: 20px;
|
|
204
|
+
left: 50%;
|
|
205
|
+
transform: translateX(-50%);
|
|
206
|
+
background: var(--dap-tour-bg);
|
|
207
|
+
border: 1px solid var(--dap-tour-border);
|
|
208
|
+
border-radius: 20px;
|
|
209
|
+
padding: 8px 16px;
|
|
210
|
+
z-index: calc(var(--dap-z-tour) + 1);
|
|
211
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
212
|
+
box-shadow: var(--dap-tour-shadow);
|
|
213
|
+
animation: tourProgressIn 0.3s ease-out forwards;
|
|
214
|
+
opacity: 0;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
@keyframes tourProgressIn {
|
|
218
|
+
to { opacity: 1; }
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.dap-tour-progress-text {
|
|
222
|
+
font-size: 12px;
|
|
223
|
+
color: var(--dap-tour-text-muted);
|
|
224
|
+
margin-bottom: 4px;
|
|
225
|
+
text-align: center;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.dap-tour-progress-bar {
|
|
229
|
+
width: 200px;
|
|
230
|
+
height: 3px;
|
|
231
|
+
background: var(--dap-tour-border);
|
|
232
|
+
border-radius: 2px;
|
|
233
|
+
overflow: hidden;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.dap-tour-progress-fill {
|
|
237
|
+
height: 100%;
|
|
238
|
+
background: var(--dap-tour-primary);
|
|
239
|
+
border-radius: 2px;
|
|
240
|
+
transition: width 0.4s ease;
|
|
241
|
+
width: 0%;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.dap-tour-close {
|
|
245
|
+
position: fixed;
|
|
246
|
+
top: 20px;
|
|
247
|
+
right: 20px;
|
|
248
|
+
background: var(--dap-tour-bg);
|
|
249
|
+
border: 1px solid var(--dap-tour-border);
|
|
250
|
+
border-radius: 50%;
|
|
251
|
+
width: 40px;
|
|
252
|
+
height: 40px;
|
|
253
|
+
display: flex;
|
|
254
|
+
align-items: center;
|
|
255
|
+
justify-content: center;
|
|
256
|
+
cursor: pointer;
|
|
257
|
+
z-index: calc(var(--dap-z-tour) + 1);
|
|
258
|
+
box-shadow: var(--dap-tour-shadow);
|
|
259
|
+
color: var(--dap-tour-text-muted);
|
|
260
|
+
font-size: 18px;
|
|
261
|
+
transition: all 0.15s ease;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.dap-tour-close:hover {
|
|
265
|
+
background: var(--dap-tour-border);
|
|
266
|
+
color: var(--dap-tour-text);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
@media (max-width: 640px) {
|
|
270
|
+
.dap-tour-tooltip {
|
|
271
|
+
max-width: 320px;
|
|
272
|
+
padding: 16px;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.dap-tour-progress {
|
|
276
|
+
top: 10px;
|
|
277
|
+
padding: 6px 12px;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.dap-tour-progress-bar {
|
|
281
|
+
width: 160px;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.dap-tour-close {
|
|
285
|
+
top: 10px;
|
|
286
|
+
right: 10px;
|
|
287
|
+
width: 36px;
|
|
288
|
+
height: 36px;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.dap-tour-actions {
|
|
292
|
+
flex-direction: column;
|
|
293
|
+
gap: 8px;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.dap-tour-nav {
|
|
297
|
+
width: 100%;
|
|
298
|
+
justify-content: space-between;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
`;
|
|
302
|
+
|
|
303
|
+
export function registerHotspotTour() {
|
|
304
|
+
register("hotspotTour", renderHotspotTour);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export async function renderHotspotTour(flow: HotspotTourFlow): Promise<void> {
|
|
308
|
+
const { payload, id } = flow;
|
|
309
|
+
|
|
310
|
+
// Extract completion tracker
|
|
311
|
+
const completionTracker = payload._completionTracker;
|
|
312
|
+
|
|
313
|
+
// Ensure CSS is injected
|
|
314
|
+
ensureStyles();
|
|
315
|
+
|
|
316
|
+
// Tour state
|
|
317
|
+
let currentStepIndex = 0;
|
|
318
|
+
let currentSpotlight: HTMLElement | null = null;
|
|
319
|
+
let currentTooltip: HTMLElement | null = null;
|
|
320
|
+
let autoAdvanceTimer: number | undefined;
|
|
321
|
+
|
|
322
|
+
// Create overlay and UI elements
|
|
323
|
+
const overlay = createTourOverlay();
|
|
324
|
+
const progressEl = payload.showProgress ? createProgressIndicator(payload) : null;
|
|
325
|
+
const closeEl = createCloseButton();
|
|
326
|
+
|
|
327
|
+
document.documentElement.appendChild(overlay);
|
|
328
|
+
if (progressEl) document.documentElement.appendChild(progressEl);
|
|
329
|
+
document.documentElement.appendChild(closeEl);
|
|
330
|
+
|
|
331
|
+
// Event listeners
|
|
332
|
+
closeEl.addEventListener("click", completeTour);
|
|
333
|
+
|
|
334
|
+
// Keyboard navigation
|
|
335
|
+
document.addEventListener("keydown", handleKeyboard);
|
|
336
|
+
|
|
337
|
+
// Start the tour
|
|
338
|
+
await showStep(currentStepIndex);
|
|
339
|
+
|
|
340
|
+
async function showStep(stepIndex: number) {
|
|
341
|
+
if (stepIndex >= payload.steps.length) {
|
|
342
|
+
completeTour();
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const step = payload.steps[stepIndex];
|
|
347
|
+
currentStepIndex = stepIndex;
|
|
348
|
+
|
|
349
|
+
// Clear auto-advance timer
|
|
350
|
+
if (autoAdvanceTimer) {
|
|
351
|
+
clearTimeout(autoAdvanceTimer);
|
|
352
|
+
autoAdvanceTimer = undefined;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
// Wait for target element
|
|
357
|
+
const element = await waitForElement(step.selector, { timeout: 3000 });
|
|
358
|
+
if (!(element instanceof HTMLElement)) {
|
|
359
|
+
console.warn(`[DAP] Element not found for step: ${step.selector}`);
|
|
360
|
+
nextStep();
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Create spotlight
|
|
365
|
+
createSpotlight(element);
|
|
366
|
+
|
|
367
|
+
// Create tooltip
|
|
368
|
+
createTooltip(step, element);
|
|
369
|
+
|
|
370
|
+
// Update progress
|
|
371
|
+
if (progressEl) {
|
|
372
|
+
updateProgress();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Auto-advance if configured
|
|
376
|
+
if (payload.autoAdvance && payload.autoAdvance > 0) {
|
|
377
|
+
autoAdvanceTimer = window.setTimeout(() => {
|
|
378
|
+
nextStep();
|
|
379
|
+
}, payload.autoAdvance * 1000);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
} catch (error) {
|
|
383
|
+
console.warn(`[DAP] Failed to show step ${stepIndex}:`, error);
|
|
384
|
+
nextStep();
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function createSpotlight(element: HTMLElement) {
|
|
389
|
+
// Remove previous spotlight
|
|
390
|
+
if (currentSpotlight) {
|
|
391
|
+
currentSpotlight.remove();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const rect = element.getBoundingClientRect();
|
|
395
|
+
const spotlight = document.createElement("div");
|
|
396
|
+
spotlight.className = "dap-tour-spotlight";
|
|
397
|
+
|
|
398
|
+
// Position spotlight around element with padding
|
|
399
|
+
const padding = 8;
|
|
400
|
+
spotlight.style.left = `${rect.left + window.scrollX - padding}px`;
|
|
401
|
+
spotlight.style.top = `${rect.top + window.scrollY - padding}px`;
|
|
402
|
+
spotlight.style.width = `${rect.width + padding * 2}px`;
|
|
403
|
+
spotlight.style.height = `${rect.height + padding * 2}px`;
|
|
404
|
+
|
|
405
|
+
document.documentElement.appendChild(spotlight);
|
|
406
|
+
currentSpotlight = spotlight;
|
|
407
|
+
|
|
408
|
+
// Scroll element into view if needed
|
|
409
|
+
element.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function createTooltip(step: HotspotTourStep, element: HTMLElement) {
|
|
413
|
+
// Remove previous tooltip
|
|
414
|
+
if (currentTooltip) {
|
|
415
|
+
currentTooltip.remove();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const tooltip = document.createElement("div");
|
|
419
|
+
tooltip.className = "dap-tour-tooltip";
|
|
420
|
+
|
|
421
|
+
// Header
|
|
422
|
+
const header = document.createElement("div");
|
|
423
|
+
header.className = "dap-tour-header";
|
|
424
|
+
|
|
425
|
+
const title = document.createElement("h3");
|
|
426
|
+
title.className = "dap-tour-title";
|
|
427
|
+
title.textContent = step.title;
|
|
428
|
+
|
|
429
|
+
const indicator = document.createElement("span");
|
|
430
|
+
indicator.className = "dap-tour-step-indicator";
|
|
431
|
+
indicator.textContent = `${currentStepIndex + 1} / ${payload.steps.length}`;
|
|
432
|
+
|
|
433
|
+
header.appendChild(title);
|
|
434
|
+
header.appendChild(indicator);
|
|
435
|
+
|
|
436
|
+
// Description
|
|
437
|
+
const description = document.createElement("div");
|
|
438
|
+
description.className = "dap-tour-description";
|
|
439
|
+
description.innerHTML = sanitizeHtml(step.description);
|
|
440
|
+
|
|
441
|
+
// Actions
|
|
442
|
+
const actions = document.createElement("div");
|
|
443
|
+
actions.className = "dap-tour-actions";
|
|
444
|
+
|
|
445
|
+
// Skip button (if allowed)
|
|
446
|
+
if (payload.allowSkip) {
|
|
447
|
+
const skipBtn = document.createElement("button");
|
|
448
|
+
skipBtn.className = "dap-tour-skip";
|
|
449
|
+
skipBtn.textContent = "Skip tour";
|
|
450
|
+
skipBtn.addEventListener("click", completeTour);
|
|
451
|
+
actions.appendChild(skipBtn);
|
|
452
|
+
} else {
|
|
453
|
+
actions.appendChild(document.createElement("div")); // spacer
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Navigation
|
|
457
|
+
const nav = document.createElement("div");
|
|
458
|
+
nav.className = "dap-tour-nav";
|
|
459
|
+
|
|
460
|
+
// Previous button
|
|
461
|
+
if (currentStepIndex > 0) {
|
|
462
|
+
const prevBtn = document.createElement("button");
|
|
463
|
+
prevBtn.className = "dap-tour-btn";
|
|
464
|
+
prevBtn.textContent = "Previous";
|
|
465
|
+
prevBtn.addEventListener("click", previousStep);
|
|
466
|
+
nav.appendChild(prevBtn);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Next/Close button
|
|
470
|
+
const nextBtn = document.createElement("button");
|
|
471
|
+
nextBtn.className = "dap-tour-btn primary";
|
|
472
|
+
|
|
473
|
+
if (currentStepIndex === payload.steps.length - 1) {
|
|
474
|
+
nextBtn.textContent = step.action === "close" ? "Close" : "Finish";
|
|
475
|
+
nextBtn.addEventListener("click", () => {
|
|
476
|
+
if (step.action === "custom" && step.customAction) {
|
|
477
|
+
window.dispatchEvent(new CustomEvent("dap-tour-action", {
|
|
478
|
+
detail: { action: step.customAction, tourId: id, stepId: step.id }
|
|
479
|
+
}));
|
|
480
|
+
}
|
|
481
|
+
completeTour();
|
|
482
|
+
});
|
|
483
|
+
} else {
|
|
484
|
+
nextBtn.textContent = "Next";
|
|
485
|
+
nextBtn.addEventListener("click", nextStep);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
nav.appendChild(nextBtn);
|
|
489
|
+
actions.appendChild(nav);
|
|
490
|
+
|
|
491
|
+
// Assemble tooltip
|
|
492
|
+
tooltip.appendChild(header);
|
|
493
|
+
tooltip.appendChild(description);
|
|
494
|
+
tooltip.appendChild(actions);
|
|
495
|
+
|
|
496
|
+
// Position tooltip
|
|
497
|
+
positionTooltip(tooltip, element, step.placement || "bottom");
|
|
498
|
+
|
|
499
|
+
document.documentElement.appendChild(tooltip);
|
|
500
|
+
currentTooltip = tooltip;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function positionTooltip(tooltip: HTMLElement, element: HTMLElement, placement: CornerPlacement) {
|
|
504
|
+
const rect = element.getBoundingClientRect();
|
|
505
|
+
|
|
506
|
+
// Apply placement class for arrow
|
|
507
|
+
tooltip.classList.add(placement);
|
|
508
|
+
|
|
509
|
+
let left = 0;
|
|
510
|
+
let top = 0;
|
|
511
|
+
|
|
512
|
+
switch (placement) {
|
|
513
|
+
case "top":
|
|
514
|
+
left = rect.left + window.scrollX + rect.width / 2 - 180; // tooltip width / 2
|
|
515
|
+
top = rect.top + window.scrollY - 16 - 140; // tooltip height estimate
|
|
516
|
+
break;
|
|
517
|
+
case "bottom":
|
|
518
|
+
left = rect.left + window.scrollX + rect.width / 2 - 180;
|
|
519
|
+
top = rect.bottom + window.scrollY + 16;
|
|
520
|
+
break;
|
|
521
|
+
case "left":
|
|
522
|
+
left = rect.left + window.scrollX - 360 - 16;
|
|
523
|
+
top = rect.top + window.scrollY + rect.height / 2 - 70;
|
|
524
|
+
break;
|
|
525
|
+
case "right":
|
|
526
|
+
left = rect.right + window.scrollX + 16;
|
|
527
|
+
top = rect.top + window.scrollY + rect.height / 2 - 70;
|
|
528
|
+
break;
|
|
529
|
+
default:
|
|
530
|
+
left = rect.left + window.scrollX + rect.width / 2 - 180;
|
|
531
|
+
top = rect.bottom + window.scrollY + 16;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Keep within viewport bounds
|
|
535
|
+
const viewportWidth = window.innerWidth;
|
|
536
|
+
const viewportHeight = window.innerHeight;
|
|
537
|
+
|
|
538
|
+
left = Math.max(10, Math.min(left, viewportWidth - 370));
|
|
539
|
+
top = Math.max(10, Math.min(top, viewportHeight - 160));
|
|
540
|
+
|
|
541
|
+
tooltip.style.left = `${left}px`;
|
|
542
|
+
tooltip.style.top = `${top}px`;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function nextStep() {
|
|
546
|
+
showStep(currentStepIndex + 1);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function previousStep() {
|
|
550
|
+
if (currentStepIndex > 0) {
|
|
551
|
+
showStep(currentStepIndex - 1);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function updateProgress() {
|
|
556
|
+
if (!progressEl) return;
|
|
557
|
+
|
|
558
|
+
const progress = ((currentStepIndex + 1) / payload.steps.length) * 100;
|
|
559
|
+
const progressText = progressEl.querySelector(".dap-tour-progress-text") as HTMLElement;
|
|
560
|
+
const progressFill = progressEl.querySelector(".dap-tour-progress-fill") as HTMLElement;
|
|
561
|
+
|
|
562
|
+
if (progressText) {
|
|
563
|
+
progressText.textContent = `Step ${currentStepIndex + 1} of ${payload.steps.length}`;
|
|
564
|
+
}
|
|
565
|
+
if (progressFill) {
|
|
566
|
+
progressFill.style.width = `${progress}%`;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function handleKeyboard(e: KeyboardEvent) {
|
|
571
|
+
switch (e.key) {
|
|
572
|
+
case "Escape":
|
|
573
|
+
if (payload.allowSkip) {
|
|
574
|
+
completeTour();
|
|
575
|
+
}
|
|
576
|
+
break;
|
|
577
|
+
case "ArrowRight":
|
|
578
|
+
e.preventDefault();
|
|
579
|
+
nextStep();
|
|
580
|
+
break;
|
|
581
|
+
case "ArrowLeft":
|
|
582
|
+
e.preventDefault();
|
|
583
|
+
previousStep();
|
|
584
|
+
break;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function completeTour() {
|
|
589
|
+
// Clear timer
|
|
590
|
+
if (autoAdvanceTimer) {
|
|
591
|
+
clearTimeout(autoAdvanceTimer);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Remove event listener
|
|
595
|
+
document.removeEventListener("keydown", handleKeyboard);
|
|
596
|
+
|
|
597
|
+
// Clean up elements
|
|
598
|
+
overlay.remove();
|
|
599
|
+
if (currentSpotlight) currentSpotlight.remove();
|
|
600
|
+
if (currentTooltip) currentTooltip.remove();
|
|
601
|
+
if (progressEl) progressEl.remove();
|
|
602
|
+
closeEl.remove();
|
|
603
|
+
|
|
604
|
+
// Signal completion
|
|
605
|
+
if (completionTracker?.onComplete) {
|
|
606
|
+
console.debug(`[DAP] Completing hotspot tour flow: ${id}`);
|
|
607
|
+
completionTracker.onComplete();
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function createTourOverlay(): HTMLElement {
|
|
613
|
+
const overlay = document.createElement("div");
|
|
614
|
+
overlay.className = "dap-tour-overlay";
|
|
615
|
+
return overlay;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function createProgressIndicator(payload: HotspotTourPayload): HTMLElement {
|
|
619
|
+
const progress = document.createElement("div");
|
|
620
|
+
progress.className = "dap-tour-progress";
|
|
621
|
+
|
|
622
|
+
const text = document.createElement("div");
|
|
623
|
+
text.className = "dap-tour-progress-text";
|
|
624
|
+
text.textContent = `Step 1 of ${payload.steps.length}`;
|
|
625
|
+
|
|
626
|
+
const bar = document.createElement("div");
|
|
627
|
+
bar.className = "dap-tour-progress-bar";
|
|
628
|
+
|
|
629
|
+
const fill = document.createElement("div");
|
|
630
|
+
fill.className = "dap-tour-progress-fill";
|
|
631
|
+
|
|
632
|
+
bar.appendChild(fill);
|
|
633
|
+
progress.appendChild(text);
|
|
634
|
+
progress.appendChild(bar);
|
|
635
|
+
|
|
636
|
+
return progress;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function createCloseButton(): HTMLElement {
|
|
640
|
+
const closeBtn = document.createElement("button");
|
|
641
|
+
closeBtn.className = "dap-tour-close";
|
|
642
|
+
closeBtn.innerHTML = "×";
|
|
643
|
+
closeBtn.setAttribute("aria-label", "Close tour");
|
|
644
|
+
return closeBtn;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function ensureStyles() {
|
|
648
|
+
if (!document.getElementById("dap-tour-style")) {
|
|
649
|
+
const style = document.createElement("style");
|
|
650
|
+
style.id = "dap-tour-style";
|
|
651
|
+
style.textContent = hotspotTourCssText;
|
|
652
|
+
document.head.appendChild(style);
|
|
653
|
+
}
|
|
654
|
+
}
|