@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,566 @@
|
|
|
1
|
+
// src/experiences/hotspots.ts
|
|
2
|
+
// Interactive hotspots experience renderer
|
|
3
|
+
|
|
4
|
+
import { sanitizeHtml } from "../utils/sanitize";
|
|
5
|
+
import { register } from "./registry";
|
|
6
|
+
import { resolveSelector } from "../utils/selectors";
|
|
7
|
+
import { waitForElement } from "../utils/triggerNormalizer";
|
|
8
|
+
import type { HotspotsPayload, HotspotItem, CornerPlacement } from "./types";
|
|
9
|
+
|
|
10
|
+
type HotspotsFlow = { id: string; type: "hotspots"; payload: HotspotsPayload };
|
|
11
|
+
|
|
12
|
+
const hotspotsCssText = `
|
|
13
|
+
:root {
|
|
14
|
+
--dap-z-hotspots: 2147483630;
|
|
15
|
+
--dap-hotspot-primary: #3b82f6;
|
|
16
|
+
--dap-hotspot-bg: #ffffff;
|
|
17
|
+
--dap-hotspot-border: #e2e8f0;
|
|
18
|
+
--dap-hotspot-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
|
|
19
|
+
--dap-hotspot-text: #1e293b;
|
|
20
|
+
--dap-hotspot-text-muted: #64748b;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.dap-hotspots-overlay {
|
|
24
|
+
position: fixed;
|
|
25
|
+
inset: 0;
|
|
26
|
+
background: rgba(15, 23, 42, 0.4);
|
|
27
|
+
z-index: var(--dap-z-hotspots);
|
|
28
|
+
pointer-events: none;
|
|
29
|
+
opacity: 0;
|
|
30
|
+
animation: hotspotsOverlayFadeIn 0.3s ease-out forwards;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@keyframes hotspotsOverlayFadeIn {
|
|
34
|
+
to { opacity: 1; }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.dap-hotspot-marker {
|
|
38
|
+
position: absolute;
|
|
39
|
+
width: 24px;
|
|
40
|
+
height: 24px;
|
|
41
|
+
background: var(--dap-hotspot-primary);
|
|
42
|
+
border: 3px solid white;
|
|
43
|
+
border-radius: 50%;
|
|
44
|
+
cursor: pointer;
|
|
45
|
+
z-index: calc(var(--dap-z-hotspots) + 1);
|
|
46
|
+
animation: hotspotPulse 2s infinite;
|
|
47
|
+
transition: all 0.2s ease;
|
|
48
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.dap-hotspot-marker:hover {
|
|
52
|
+
transform: scale(1.1);
|
|
53
|
+
animation-play-state: paused;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.dap-hotspot-marker.completed {
|
|
57
|
+
background: #10b981;
|
|
58
|
+
animation: none;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.dap-hotspot-marker.required {
|
|
62
|
+
background: #f59e0b;
|
|
63
|
+
animation-duration: 1.5s;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@keyframes hotspotPulse {
|
|
67
|
+
0% {
|
|
68
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2), 0 0 0 0 rgba(59, 130, 246, 0.7);
|
|
69
|
+
}
|
|
70
|
+
70% {
|
|
71
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2), 0 0 0 10px rgba(59, 130, 246, 0);
|
|
72
|
+
}
|
|
73
|
+
100% {
|
|
74
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2), 0 0 0 0 rgba(59, 130, 246, 0);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.dap-hotspot-tooltip {
|
|
79
|
+
position: absolute;
|
|
80
|
+
background: var(--dap-hotspot-bg);
|
|
81
|
+
border: 1px solid var(--dap-hotspot-border);
|
|
82
|
+
border-radius: 8px;
|
|
83
|
+
box-shadow: var(--dap-hotspot-shadow);
|
|
84
|
+
padding: 16px;
|
|
85
|
+
max-width: 320px;
|
|
86
|
+
z-index: calc(var(--dap-z-hotspots) + 2);
|
|
87
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
88
|
+
opacity: 0;
|
|
89
|
+
transform: scale(0.9);
|
|
90
|
+
animation: hotspotTooltipIn 0.2s ease-out forwards;
|
|
91
|
+
pointer-events: auto;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@keyframes hotspotTooltipIn {
|
|
95
|
+
to {
|
|
96
|
+
opacity: 1;
|
|
97
|
+
transform: scale(1);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.dap-hotspot-tooltip::before {
|
|
102
|
+
content: '';
|
|
103
|
+
position: absolute;
|
|
104
|
+
width: 0;
|
|
105
|
+
height: 0;
|
|
106
|
+
border: 8px solid transparent;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.dap-hotspot-tooltip.top::before {
|
|
110
|
+
bottom: -16px;
|
|
111
|
+
left: 50%;
|
|
112
|
+
transform: translateX(-50%);
|
|
113
|
+
border-top-color: var(--dap-hotspot-bg);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.dap-hotspot-tooltip.bottom::before {
|
|
117
|
+
top: -16px;
|
|
118
|
+
left: 50%;
|
|
119
|
+
transform: translateX(-50%);
|
|
120
|
+
border-bottom-color: var(--dap-hotspot-bg);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.dap-hotspot-tooltip.left::before {
|
|
124
|
+
right: -16px;
|
|
125
|
+
top: 50%;
|
|
126
|
+
transform: translateY(-50%);
|
|
127
|
+
border-left-color: var(--dap-hotspot-bg);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.dap-hotspot-tooltip.right::before {
|
|
131
|
+
left: -16px;
|
|
132
|
+
top: 50%;
|
|
133
|
+
transform: translateY(-50%);
|
|
134
|
+
border-right-color: var(--dap-hotspot-bg);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.dap-hotspot-title {
|
|
138
|
+
font-size: 16px;
|
|
139
|
+
font-weight: 600;
|
|
140
|
+
color: var(--dap-hotspot-text);
|
|
141
|
+
margin: 0 0 8px 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.dap-hotspot-description {
|
|
145
|
+
font-size: 14px;
|
|
146
|
+
color: var(--dap-hotspot-text-muted);
|
|
147
|
+
line-height: 1.4;
|
|
148
|
+
margin: 0 0 12px 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.dap-hotspot-actions {
|
|
152
|
+
display: flex;
|
|
153
|
+
gap: 8px;
|
|
154
|
+
justify-content: flex-end;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.dap-hotspot-btn {
|
|
158
|
+
padding: 6px 12px;
|
|
159
|
+
border: 1px solid var(--dap-hotspot-primary);
|
|
160
|
+
border-radius: 4px;
|
|
161
|
+
background: transparent;
|
|
162
|
+
color: var(--dap-hotspot-primary);
|
|
163
|
+
font-size: 12px;
|
|
164
|
+
font-weight: 500;
|
|
165
|
+
cursor: pointer;
|
|
166
|
+
transition: all 0.15s ease;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.dap-hotspot-btn:hover {
|
|
170
|
+
background: var(--dap-hotspot-primary);
|
|
171
|
+
color: white;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.dap-hotspot-btn.primary {
|
|
175
|
+
background: var(--dap-hotspot-primary);
|
|
176
|
+
color: white;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.dap-hotspot-btn.primary:hover {
|
|
180
|
+
background: #2563eb;
|
|
181
|
+
border-color: #2563eb;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.dap-hotspots-progress {
|
|
185
|
+
position: fixed;
|
|
186
|
+
top: 20px;
|
|
187
|
+
right: 20px;
|
|
188
|
+
background: var(--dap-hotspot-bg);
|
|
189
|
+
border: 1px solid var(--dap-hotspot-border);
|
|
190
|
+
border-radius: 8px;
|
|
191
|
+
padding: 12px 16px;
|
|
192
|
+
z-index: calc(var(--dap-z-hotspots) + 1);
|
|
193
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
194
|
+
box-shadow: var(--dap-hotspot-shadow);
|
|
195
|
+
animation: hotspotProgressIn 0.3s ease-out forwards;
|
|
196
|
+
opacity: 0;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
@keyframes hotspotProgressIn {
|
|
200
|
+
to { opacity: 1; }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.dap-hotspots-progress-text {
|
|
204
|
+
font-size: 14px;
|
|
205
|
+
color: var(--dap-hotspot-text);
|
|
206
|
+
margin-bottom: 4px;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.dap-hotspots-progress-bar {
|
|
210
|
+
width: 120px;
|
|
211
|
+
height: 4px;
|
|
212
|
+
background: var(--dap-hotspot-border);
|
|
213
|
+
border-radius: 2px;
|
|
214
|
+
overflow: hidden;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.dap-hotspots-progress-fill {
|
|
218
|
+
height: 100%;
|
|
219
|
+
background: var(--dap-hotspot-primary);
|
|
220
|
+
border-radius: 2px;
|
|
221
|
+
transition: width 0.3s ease;
|
|
222
|
+
width: 0%;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.dap-hotspots-controls {
|
|
226
|
+
position: fixed;
|
|
227
|
+
bottom: 20px;
|
|
228
|
+
right: 20px;
|
|
229
|
+
display: flex;
|
|
230
|
+
gap: 8px;
|
|
231
|
+
z-index: calc(var(--dap-z-hotspots) + 1);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.dap-hotspots-skip {
|
|
235
|
+
padding: 8px 16px;
|
|
236
|
+
background: var(--dap-hotspot-bg);
|
|
237
|
+
border: 1px solid var(--dap-hotspot-border);
|
|
238
|
+
border-radius: 6px;
|
|
239
|
+
color: var(--dap-hotspot-text);
|
|
240
|
+
font-size: 14px;
|
|
241
|
+
cursor: pointer;
|
|
242
|
+
transition: all 0.15s ease;
|
|
243
|
+
box-shadow: var(--dap-hotspot-shadow);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.dap-hotspots-skip:hover {
|
|
247
|
+
background: var(--dap-hotspot-border);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
@media (max-width: 640px) {
|
|
251
|
+
.dap-hotspots-progress {
|
|
252
|
+
top: 10px;
|
|
253
|
+
right: 10px;
|
|
254
|
+
padding: 8px 12px;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.dap-hotspots-controls {
|
|
258
|
+
bottom: 10px;
|
|
259
|
+
right: 10px;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.dap-hotspot-tooltip {
|
|
263
|
+
max-width: 280px;
|
|
264
|
+
padding: 12px;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
`;
|
|
268
|
+
|
|
269
|
+
export function registerHotspots() {
|
|
270
|
+
register("hotspots", renderHotspots);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export async function renderHotspots(flow: HotspotsFlow): Promise<void> {
|
|
274
|
+
const { payload, id } = flow;
|
|
275
|
+
|
|
276
|
+
// Extract completion tracker
|
|
277
|
+
const completionTracker = payload._completionTracker;
|
|
278
|
+
|
|
279
|
+
// Ensure CSS is injected
|
|
280
|
+
ensureStyles();
|
|
281
|
+
|
|
282
|
+
// Track completed hotspots
|
|
283
|
+
const completedHotspots = new Set<string>();
|
|
284
|
+
let currentTooltip: HTMLElement | null = null;
|
|
285
|
+
|
|
286
|
+
// Create overlay
|
|
287
|
+
const overlay = document.createElement("div");
|
|
288
|
+
overlay.className = "dap-hotspots-overlay";
|
|
289
|
+
document.documentElement.appendChild(overlay);
|
|
290
|
+
|
|
291
|
+
// Create progress indicator if enabled
|
|
292
|
+
let progressEl: HTMLElement | null = null;
|
|
293
|
+
if (payload.showProgress) {
|
|
294
|
+
progressEl = createProgressIndicator(payload);
|
|
295
|
+
document.documentElement.appendChild(progressEl);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Create skip controls if allowed
|
|
299
|
+
let skipEl: HTMLElement | null = null;
|
|
300
|
+
if (payload.allowSkip) {
|
|
301
|
+
skipEl = createSkipControls();
|
|
302
|
+
document.documentElement.appendChild(skipEl);
|
|
303
|
+
skipEl.addEventListener("click", completeHotspots);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Create hotspot markers
|
|
307
|
+
const markers: HTMLElement[] = [];
|
|
308
|
+
const waitForElements = payload.hotspots.map(async (hotspot) => {
|
|
309
|
+
try {
|
|
310
|
+
const element = await waitForElement(hotspot.selector, { timeout: 2000 });
|
|
311
|
+
if (element && element instanceof HTMLElement) {
|
|
312
|
+
createHotspotMarker(hotspot, element);
|
|
313
|
+
}
|
|
314
|
+
} catch (error) {
|
|
315
|
+
console.warn(`[DAP] Failed to find element for hotspot: ${hotspot.selector}`);
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
await Promise.allSettled(waitForElements);
|
|
320
|
+
|
|
321
|
+
function createHotspotMarker(hotspot: HotspotItem, element: HTMLElement) {
|
|
322
|
+
const rect = element.getBoundingClientRect();
|
|
323
|
+
const marker = document.createElement("div");
|
|
324
|
+
marker.className = "dap-hotspot-marker";
|
|
325
|
+
marker.dataset.hotspotId = hotspot.id;
|
|
326
|
+
|
|
327
|
+
if (hotspot.required) {
|
|
328
|
+
marker.classList.add("required");
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (hotspot.pulseColor) {
|
|
332
|
+
marker.style.background = hotspot.pulseColor;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Position marker at element center
|
|
336
|
+
marker.style.left = `${rect.left + window.scrollX + rect.width / 2 - 12}px`;
|
|
337
|
+
marker.style.top = `${rect.top + window.scrollY + rect.height / 2 - 12}px`;
|
|
338
|
+
|
|
339
|
+
marker.addEventListener("click", () => showTooltip(hotspot, marker, element));
|
|
340
|
+
|
|
341
|
+
document.documentElement.appendChild(marker);
|
|
342
|
+
markers.push(marker);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function showTooltip(hotspot: HotspotItem, marker: HTMLElement, element: HTMLElement) {
|
|
346
|
+
// Hide current tooltip if any
|
|
347
|
+
if (currentTooltip) {
|
|
348
|
+
currentTooltip.remove();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const tooltip = document.createElement("div");
|
|
352
|
+
tooltip.className = "dap-hotspot-tooltip";
|
|
353
|
+
|
|
354
|
+
const title = document.createElement("h3");
|
|
355
|
+
title.className = "dap-hotspot-title";
|
|
356
|
+
title.textContent = hotspot.title;
|
|
357
|
+
|
|
358
|
+
const description = document.createElement("div");
|
|
359
|
+
description.className = "dap-hotspot-description";
|
|
360
|
+
description.innerHTML = sanitizeHtml(hotspot.description);
|
|
361
|
+
|
|
362
|
+
const actions = document.createElement("div");
|
|
363
|
+
actions.className = "dap-hotspot-actions";
|
|
364
|
+
|
|
365
|
+
const gotItBtn = document.createElement("button");
|
|
366
|
+
gotItBtn.className = "dap-hotspot-btn primary";
|
|
367
|
+
gotItBtn.textContent = "Got it!";
|
|
368
|
+
gotItBtn.addEventListener("click", () => {
|
|
369
|
+
markHotspotCompleted(hotspot, marker);
|
|
370
|
+
tooltip.remove();
|
|
371
|
+
currentTooltip = null;
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
actions.appendChild(gotItBtn);
|
|
375
|
+
tooltip.appendChild(title);
|
|
376
|
+
tooltip.appendChild(description);
|
|
377
|
+
tooltip.appendChild(actions);
|
|
378
|
+
|
|
379
|
+
// Position tooltip
|
|
380
|
+
positionTooltip(tooltip, marker, element, hotspot.placement || "top");
|
|
381
|
+
|
|
382
|
+
document.documentElement.appendChild(tooltip);
|
|
383
|
+
currentTooltip = tooltip;
|
|
384
|
+
|
|
385
|
+
// Click outside to close
|
|
386
|
+
setTimeout(() => {
|
|
387
|
+
const closeOnOutside = (e: MouseEvent) => {
|
|
388
|
+
if (!tooltip.contains(e.target as Node) && !marker.contains(e.target as Node)) {
|
|
389
|
+
tooltip.remove();
|
|
390
|
+
currentTooltip = null;
|
|
391
|
+
document.removeEventListener("click", closeOnOutside);
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
document.addEventListener("click", closeOnOutside);
|
|
395
|
+
}, 100);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function positionTooltip(tooltip: HTMLElement, marker: HTMLElement, element: HTMLElement, placement: CornerPlacement) {
|
|
399
|
+
const rect = element.getBoundingClientRect();
|
|
400
|
+
const markerRect = marker.getBoundingClientRect();
|
|
401
|
+
|
|
402
|
+
// Apply placement class for arrow positioning
|
|
403
|
+
tooltip.classList.add(placement);
|
|
404
|
+
|
|
405
|
+
let left = 0;
|
|
406
|
+
let top = 0;
|
|
407
|
+
|
|
408
|
+
switch (placement) {
|
|
409
|
+
case "top":
|
|
410
|
+
left = markerRect.left - 160 + markerRect.width / 2;
|
|
411
|
+
top = markerRect.top - 16 - 120; // tooltip height estimate
|
|
412
|
+
break;
|
|
413
|
+
case "bottom":
|
|
414
|
+
left = markerRect.left - 160 + markerRect.width / 2;
|
|
415
|
+
top = markerRect.bottom + 16;
|
|
416
|
+
break;
|
|
417
|
+
case "left":
|
|
418
|
+
left = markerRect.left - 320 - 16;
|
|
419
|
+
top = markerRect.top - 60 + markerRect.height / 2;
|
|
420
|
+
break;
|
|
421
|
+
case "right":
|
|
422
|
+
left = markerRect.right + 16;
|
|
423
|
+
top = markerRect.top - 60 + markerRect.height / 2;
|
|
424
|
+
break;
|
|
425
|
+
default:
|
|
426
|
+
left = markerRect.left - 160 + markerRect.width / 2;
|
|
427
|
+
top = markerRect.top - 16 - 120;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Keep within viewport bounds
|
|
431
|
+
const viewportWidth = window.innerWidth;
|
|
432
|
+
const viewportHeight = window.innerHeight;
|
|
433
|
+
|
|
434
|
+
left = Math.max(10, Math.min(left, viewportWidth - 330));
|
|
435
|
+
top = Math.max(10, Math.min(top, viewportHeight - 150));
|
|
436
|
+
|
|
437
|
+
tooltip.style.left = `${left + window.scrollX}px`;
|
|
438
|
+
tooltip.style.top = `${top + window.scrollY}px`;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function markHotspotCompleted(hotspot: HotspotItem, marker: HTMLElement) {
|
|
442
|
+
completedHotspots.add(hotspot.id);
|
|
443
|
+
marker.classList.add("completed");
|
|
444
|
+
|
|
445
|
+
// Update progress
|
|
446
|
+
if (progressEl) {
|
|
447
|
+
updateProgress();
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Check if all required hotspots are completed
|
|
451
|
+
const requiredHotspots = payload.hotspots.filter(h => h.required);
|
|
452
|
+
const completedRequired = requiredHotspots.filter(h => completedHotspots.has(h.id));
|
|
453
|
+
|
|
454
|
+
if (completedRequired.length === requiredHotspots.length) {
|
|
455
|
+
// Allow completion after a short delay
|
|
456
|
+
setTimeout(() => {
|
|
457
|
+
if (canComplete()) {
|
|
458
|
+
completeHotspots();
|
|
459
|
+
}
|
|
460
|
+
}, 1000);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function updateProgress() {
|
|
465
|
+
if (!progressEl) return;
|
|
466
|
+
|
|
467
|
+
const total = payload.hotspots.length;
|
|
468
|
+
const completed = completedHotspots.size;
|
|
469
|
+
const percentage = (completed / total) * 100;
|
|
470
|
+
|
|
471
|
+
const progressText = progressEl.querySelector(".dap-hotspots-progress-text") as HTMLElement;
|
|
472
|
+
const progressFill = progressEl.querySelector(".dap-hotspots-progress-fill") as HTMLElement;
|
|
473
|
+
|
|
474
|
+
if (progressText) {
|
|
475
|
+
progressText.textContent = `${completed} / ${total} explored`;
|
|
476
|
+
}
|
|
477
|
+
if (progressFill) {
|
|
478
|
+
progressFill.style.width = `${percentage}%`;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function canComplete(): boolean {
|
|
483
|
+
const requiredHotspots = payload.hotspots.filter(h => h.required);
|
|
484
|
+
return requiredHotspots.every(h => completedHotspots.has(h.id));
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function completeHotspots() {
|
|
488
|
+
// Clean up
|
|
489
|
+
overlay.remove();
|
|
490
|
+
markers.forEach(marker => marker.remove());
|
|
491
|
+
if (currentTooltip) {
|
|
492
|
+
currentTooltip.remove();
|
|
493
|
+
}
|
|
494
|
+
if (progressEl) {
|
|
495
|
+
progressEl.remove();
|
|
496
|
+
}
|
|
497
|
+
if (skipEl) {
|
|
498
|
+
skipEl.remove();
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Signal completion
|
|
502
|
+
if (completionTracker?.onComplete) {
|
|
503
|
+
console.debug(`[DAP] Completing hotspots flow: ${id}`);
|
|
504
|
+
completionTracker.onComplete();
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Keyboard accessibility
|
|
509
|
+
document.addEventListener("keydown", (e) => {
|
|
510
|
+
if (e.key === "Escape") {
|
|
511
|
+
if (currentTooltip) {
|
|
512
|
+
currentTooltip.remove();
|
|
513
|
+
currentTooltip = null;
|
|
514
|
+
} else if (payload.allowSkip) {
|
|
515
|
+
completeHotspots();
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
// Update progress initially
|
|
521
|
+
if (progressEl) {
|
|
522
|
+
updateProgress();
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function createProgressIndicator(payload: HotspotsPayload): HTMLElement {
|
|
527
|
+
const progress = document.createElement("div");
|
|
528
|
+
progress.className = "dap-hotspots-progress";
|
|
529
|
+
|
|
530
|
+
const text = document.createElement("div");
|
|
531
|
+
text.className = "dap-hotspots-progress-text";
|
|
532
|
+
text.textContent = "0 / " + payload.hotspots.length + " explored";
|
|
533
|
+
|
|
534
|
+
const bar = document.createElement("div");
|
|
535
|
+
bar.className = "dap-hotspots-progress-bar";
|
|
536
|
+
|
|
537
|
+
const fill = document.createElement("div");
|
|
538
|
+
fill.className = "dap-hotspots-progress-fill";
|
|
539
|
+
|
|
540
|
+
bar.appendChild(fill);
|
|
541
|
+
progress.appendChild(text);
|
|
542
|
+
progress.appendChild(bar);
|
|
543
|
+
|
|
544
|
+
return progress;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function createSkipControls(): HTMLElement {
|
|
548
|
+
const controls = document.createElement("div");
|
|
549
|
+
controls.className = "dap-hotspots-controls";
|
|
550
|
+
|
|
551
|
+
const skipBtn = document.createElement("button");
|
|
552
|
+
skipBtn.className = "dap-hotspots-skip";
|
|
553
|
+
skipBtn.textContent = "Skip tour";
|
|
554
|
+
|
|
555
|
+
controls.appendChild(skipBtn);
|
|
556
|
+
return controls;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function ensureStyles() {
|
|
560
|
+
if (!document.getElementById("dap-hotspots-style")) {
|
|
561
|
+
const style = document.createElement("style");
|
|
562
|
+
style.id = "dap-hotspots-style";
|
|
563
|
+
style.textContent = hotspotsCssText;
|
|
564
|
+
document.head.appendChild(style);
|
|
565
|
+
}
|
|
566
|
+
}
|