@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,740 @@
|
|
|
1
|
+
import { sanitizeHtml } from "../utils/sanitize";
|
|
2
|
+
import { register } from "./registry";
|
|
3
|
+
import type { TooltipPayload } from "./types";
|
|
4
|
+
import { resolveSelector } from "../utils/selectors";
|
|
5
|
+
|
|
6
|
+
type TooltipFlow = { id: string; type: "tooltip"; payload: TooltipPayload };
|
|
7
|
+
type Placement = "top" | "right" | "bottom" | "left";
|
|
8
|
+
type Trigger = "hover" | "click" | "focus" | "pageload";
|
|
9
|
+
|
|
10
|
+
export function registerTooltip() {
|
|
11
|
+
register("tooltip", renderTooltip);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Main tooltip rendering function
|
|
15
|
+
export async function renderTooltip(flow: TooltipFlow): Promise<void> {
|
|
16
|
+
const { payload, id } = flow;
|
|
17
|
+
|
|
18
|
+
console.debug("[DAP] Tooltip initialized", { id, selector: payload.targetSelector });
|
|
19
|
+
|
|
20
|
+
// Validate required fields from uxExperience
|
|
21
|
+
if (!payload.targetSelector || !payload.text) {
|
|
22
|
+
console.error("[DAP] Tooltip missing required fields", {
|
|
23
|
+
targetSelector: payload.targetSelector,
|
|
24
|
+
hasText: !!payload.text
|
|
25
|
+
});
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Wait for target element
|
|
30
|
+
const target = await waitForTarget(payload.targetSelector, 5000);
|
|
31
|
+
if (!target) {
|
|
32
|
+
console.warn("[DAP] Tooltip target not found", { selector: payload.targetSelector });
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.debug("[DAP] Tooltip target resolved", { selector: payload.targetSelector });
|
|
37
|
+
|
|
38
|
+
// Create and initialize tooltip
|
|
39
|
+
const tooltip = new DAPTooltip(id, target, payload);
|
|
40
|
+
tooltip.initialize();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Direct tooltip rendering for validation
|
|
44
|
+
export async function renderDirectTooltip(payload: TooltipPayload): Promise<void> {
|
|
45
|
+
const directFlow: TooltipFlow = {
|
|
46
|
+
id: `validation-${Date.now()}`,
|
|
47
|
+
type: "tooltip",
|
|
48
|
+
payload: payload
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
await renderTooltip(directFlow);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Clean tooltip implementation
|
|
55
|
+
class DAPTooltip {
|
|
56
|
+
private id: string;
|
|
57
|
+
private target: HTMLElement;
|
|
58
|
+
private payload: TooltipPayload;
|
|
59
|
+
private container: HTMLElement | null = null;
|
|
60
|
+
private overlay: HTMLElement | null = null;
|
|
61
|
+
private isVisible: boolean = false;
|
|
62
|
+
private listeners: Array<() => void> = [];
|
|
63
|
+
private targetObserver: MutationObserver | null = null;
|
|
64
|
+
private trigger: Trigger;
|
|
65
|
+
|
|
66
|
+
constructor(id: string, target: HTMLElement, payload: TooltipPayload) {
|
|
67
|
+
this.id = id;
|
|
68
|
+
this.target = target;
|
|
69
|
+
this.payload = payload;
|
|
70
|
+
this.trigger = this.normalizeTrigger(payload.trigger);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
initialize(): void {
|
|
74
|
+
this.setupTrigger();
|
|
75
|
+
this.setupGlobalListeners();
|
|
76
|
+
this.setupTargetObserver();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private normalizeTrigger(trigger: any): Trigger {
|
|
80
|
+
if (typeof trigger === "string" && ["hover", "click", "focus", "pageload"].includes(trigger)) {
|
|
81
|
+
return trigger as Trigger;
|
|
82
|
+
}
|
|
83
|
+
return "hover"; // default
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private setupTrigger(): void {
|
|
87
|
+
switch (this.trigger) {
|
|
88
|
+
case "hover":
|
|
89
|
+
this.setupHoverTrigger();
|
|
90
|
+
break;
|
|
91
|
+
case "click":
|
|
92
|
+
this.setupClickTrigger();
|
|
93
|
+
break;
|
|
94
|
+
case "focus":
|
|
95
|
+
this.setupFocusTrigger();
|
|
96
|
+
break;
|
|
97
|
+
case "pageload":
|
|
98
|
+
// Show immediately
|
|
99
|
+
this.show();
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private setupHoverTrigger(): void {
|
|
105
|
+
const onMouseEnter = () => this.show();
|
|
106
|
+
const onMouseLeave = (e: MouseEvent) => {
|
|
107
|
+
// Don't hide if hovering over tooltip
|
|
108
|
+
const related = e.relatedTarget as Element | null;
|
|
109
|
+
if (related && this.container?.contains(related)) return;
|
|
110
|
+
this.hide();
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
this.target.addEventListener("mouseenter", onMouseEnter);
|
|
114
|
+
this.target.addEventListener("mouseleave", onMouseLeave);
|
|
115
|
+
|
|
116
|
+
this.listeners.push(
|
|
117
|
+
() => this.target.removeEventListener("mouseenter", onMouseEnter),
|
|
118
|
+
() => this.target.removeEventListener("mouseleave", onMouseLeave)
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private setupClickTrigger(): void {
|
|
123
|
+
const onClick = (e: MouseEvent) => {
|
|
124
|
+
e.stopPropagation();
|
|
125
|
+
this.isVisible ? this.hide() : this.show();
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const onDocumentClick = (e: MouseEvent) => {
|
|
129
|
+
const target = e.target as Element;
|
|
130
|
+
if (!this.container?.contains(target) && !this.target.contains(target)) {
|
|
131
|
+
this.hide();
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
this.target.addEventListener("click", onClick);
|
|
136
|
+
document.addEventListener("click", onDocumentClick, true);
|
|
137
|
+
|
|
138
|
+
this.listeners.push(
|
|
139
|
+
() => this.target.removeEventListener("click", onClick),
|
|
140
|
+
() => document.removeEventListener("click", onDocumentClick, true)
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private setupFocusTrigger(): void {
|
|
145
|
+
const onFocus = () => this.show();
|
|
146
|
+
const onBlur = () => this.hide();
|
|
147
|
+
|
|
148
|
+
this.target.addEventListener("focus", onFocus);
|
|
149
|
+
this.target.addEventListener("blur", onBlur);
|
|
150
|
+
|
|
151
|
+
this.listeners.push(
|
|
152
|
+
() => this.target.removeEventListener("focus", onFocus),
|
|
153
|
+
() => this.target.removeEventListener("blur", onBlur)
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private setupGlobalListeners(): void {
|
|
158
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
159
|
+
if (e.key === "Escape" && this.isVisible) {
|
|
160
|
+
this.hide();
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const onScroll = () => {
|
|
165
|
+
if (this.isVisible) {
|
|
166
|
+
if (!this.isTargetInViewport()) {
|
|
167
|
+
this.hide();
|
|
168
|
+
} else {
|
|
169
|
+
this.position();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const onResize = () => {
|
|
175
|
+
if (this.isVisible) {
|
|
176
|
+
this.position();
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const onVisibilityChange = () => {
|
|
181
|
+
if (document.hidden && this.isVisible) {
|
|
182
|
+
this.hide();
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
document.addEventListener("keydown", onKeyDown);
|
|
187
|
+
window.addEventListener("scroll", onScroll, true);
|
|
188
|
+
window.addEventListener("resize", onResize);
|
|
189
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
190
|
+
|
|
191
|
+
this.listeners.push(
|
|
192
|
+
() => document.removeEventListener("keydown", onKeyDown),
|
|
193
|
+
() => window.removeEventListener("scroll", onScroll, true),
|
|
194
|
+
() => window.removeEventListener("resize", onResize),
|
|
195
|
+
() => document.removeEventListener("visibilitychange", onVisibilityChange)
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private setupTargetObserver(): void {
|
|
200
|
+
this.targetObserver = new MutationObserver(() => {
|
|
201
|
+
if (!document.contains(this.target)) {
|
|
202
|
+
this.destroy();
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
this.targetObserver.observe(document.documentElement, {
|
|
207
|
+
childList: true,
|
|
208
|
+
subtree: true
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private show(): void {
|
|
213
|
+
if (this.isVisible) return;
|
|
214
|
+
|
|
215
|
+
console.debug("[DAP] Tooltip shown", { id: this.id });
|
|
216
|
+
|
|
217
|
+
this.createTooltip();
|
|
218
|
+
this.position();
|
|
219
|
+
this.isVisible = true;
|
|
220
|
+
|
|
221
|
+
// Add visible class for animation
|
|
222
|
+
requestAnimationFrame(() => {
|
|
223
|
+
if (this.container) {
|
|
224
|
+
this.container.classList.add("dap-tooltip-visible");
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Complete the flow step
|
|
229
|
+
if (this.payload._completionTracker?.onComplete) {
|
|
230
|
+
console.debug("[DAP] Completing tooltip flow", { id: this.id });
|
|
231
|
+
this.payload._completionTracker.onComplete();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private hide(): void {
|
|
236
|
+
if (!this.isVisible) return;
|
|
237
|
+
|
|
238
|
+
console.debug("[DAP] Tooltip dismissed", { id: this.id });
|
|
239
|
+
|
|
240
|
+
// Animate out before removing
|
|
241
|
+
if (this.container) {
|
|
242
|
+
this.container.style.animation = "dap-tooltip-exit 0.2s cubic-bezier(0.4, 0.0, 0.2, 1) forwards";
|
|
243
|
+
|
|
244
|
+
setTimeout(() => {
|
|
245
|
+
this.removeTooltip();
|
|
246
|
+
}, 200);
|
|
247
|
+
} else {
|
|
248
|
+
this.removeTooltip();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
this.isVisible = false;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private createTooltip(): void {
|
|
255
|
+
// Create overlay if it doesn't exist
|
|
256
|
+
this.overlay = this.getOrCreateOverlay();
|
|
257
|
+
|
|
258
|
+
// Create tooltip container
|
|
259
|
+
this.container = document.createElement("div");
|
|
260
|
+
this.container.className = "dap-tooltip";
|
|
261
|
+
this.container.id = `dap-tooltip-${this.id}`;
|
|
262
|
+
this.container.setAttribute("role", "tooltip");
|
|
263
|
+
this.container.setAttribute("aria-live", "polite");
|
|
264
|
+
|
|
265
|
+
// Create content
|
|
266
|
+
const content = document.createElement("div");
|
|
267
|
+
content.className = "dap-tooltip-content";
|
|
268
|
+
content.textContent = this.payload.text || "";
|
|
269
|
+
|
|
270
|
+
// Create arrow
|
|
271
|
+
const arrow = document.createElement("div");
|
|
272
|
+
arrow.className = "dap-tooltip-arrow";
|
|
273
|
+
|
|
274
|
+
this.container.appendChild(content);
|
|
275
|
+
this.container.appendChild(arrow);
|
|
276
|
+
|
|
277
|
+
// Add hover behavior for hover trigger
|
|
278
|
+
if (this.trigger === "hover") {
|
|
279
|
+
const onTooltipMouseLeave = () => this.hide();
|
|
280
|
+
this.container.addEventListener("mouseleave", onTooltipMouseLeave);
|
|
281
|
+
this.listeners.push(
|
|
282
|
+
() => this.container?.removeEventListener("mouseleave", onTooltipMouseLeave)
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
this.overlay.appendChild(this.container);
|
|
287
|
+
|
|
288
|
+
// Set ARIA relationship
|
|
289
|
+
const tooltipId = this.container.id;
|
|
290
|
+
const prevDesc = this.target.getAttribute("aria-describedby") || "";
|
|
291
|
+
this.target.setAttribute("aria-describedby", [prevDesc, tooltipId].filter(Boolean).join(" ").trim());
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private removeTooltip(): void {
|
|
295
|
+
if (this.container) {
|
|
296
|
+
// Clean up ARIA relationship
|
|
297
|
+
const tooltipId = this.container.id;
|
|
298
|
+
const currentDesc = this.target.getAttribute("aria-describedby") || "";
|
|
299
|
+
const newDesc = currentDesc.split(/\s+/).filter(Boolean).filter(id => id !== tooltipId).join(" ");
|
|
300
|
+
|
|
301
|
+
if (newDesc) {
|
|
302
|
+
this.target.setAttribute("aria-describedby", newDesc);
|
|
303
|
+
} else {
|
|
304
|
+
this.target.removeAttribute("aria-describedby");
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
this.container.remove();
|
|
308
|
+
this.container = null;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private position(): void {
|
|
313
|
+
if (!this.container) return;
|
|
314
|
+
|
|
315
|
+
const targetRect = this.target.getBoundingClientRect();
|
|
316
|
+
const placement = this.normalizePlacement(this.payload.placement);
|
|
317
|
+
|
|
318
|
+
// Reset positioning for measurement
|
|
319
|
+
this.container.style.position = "fixed";
|
|
320
|
+
this.container.style.visibility = "hidden";
|
|
321
|
+
this.container.style.top = "0px";
|
|
322
|
+
this.container.style.left = "0px";
|
|
323
|
+
this.container.style.display = "block";
|
|
324
|
+
|
|
325
|
+
// Get tooltip dimensions
|
|
326
|
+
const tooltipRect = this.container.getBoundingClientRect();
|
|
327
|
+
const gap = 8;
|
|
328
|
+
const viewport = {
|
|
329
|
+
width: window.innerWidth,
|
|
330
|
+
height: window.innerHeight
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
// Calculate best position
|
|
334
|
+
const position = this.calculatePosition(targetRect, tooltipRect, placement, gap, viewport);
|
|
335
|
+
|
|
336
|
+
// Apply position
|
|
337
|
+
this.container.style.top = `${position.top}px`;
|
|
338
|
+
this.container.style.left = `${position.left}px`;
|
|
339
|
+
this.container.setAttribute("data-placement", position.placement);
|
|
340
|
+
this.container.style.visibility = "visible";
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private normalizePlacement(placement: any): Placement {
|
|
344
|
+
if (typeof placement === "string" && ["top", "right", "bottom", "left"].includes(placement)) {
|
|
345
|
+
return placement as Placement;
|
|
346
|
+
}
|
|
347
|
+
return "top"; // default
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
private calculatePosition(
|
|
351
|
+
targetRect: DOMRect,
|
|
352
|
+
tooltipRect: DOMRect,
|
|
353
|
+
preferredPlacement: Placement,
|
|
354
|
+
gap: number,
|
|
355
|
+
viewport: { width: number; height: number }
|
|
356
|
+
): { top: number; left: number; placement: Placement } {
|
|
357
|
+
|
|
358
|
+
const positions = {
|
|
359
|
+
top: {
|
|
360
|
+
top: targetRect.top - tooltipRect.height - gap,
|
|
361
|
+
left: targetRect.left + (targetRect.width - tooltipRect.width) / 2
|
|
362
|
+
},
|
|
363
|
+
right: {
|
|
364
|
+
top: targetRect.top + (targetRect.height - tooltipRect.height) / 2,
|
|
365
|
+
left: targetRect.right + gap
|
|
366
|
+
},
|
|
367
|
+
bottom: {
|
|
368
|
+
top: targetRect.bottom + gap,
|
|
369
|
+
left: targetRect.left + (targetRect.width - tooltipRect.width) / 2
|
|
370
|
+
},
|
|
371
|
+
left: {
|
|
372
|
+
top: targetRect.top + (targetRect.height - tooltipRect.height) / 2,
|
|
373
|
+
left: targetRect.left - tooltipRect.width - gap
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
// Check if position fits in viewport
|
|
378
|
+
const fits = (pos: { top: number; left: number }) => {
|
|
379
|
+
return pos.top >= 0 &&
|
|
380
|
+
pos.left >= 0 &&
|
|
381
|
+
pos.top + tooltipRect.height <= viewport.height &&
|
|
382
|
+
pos.left + tooltipRect.width <= viewport.width;
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
// Try preferred placement first
|
|
386
|
+
let finalPosition = positions[preferredPlacement];
|
|
387
|
+
let finalPlacement = preferredPlacement;
|
|
388
|
+
|
|
389
|
+
if (!fits(finalPosition)) {
|
|
390
|
+
// Try alternative placements
|
|
391
|
+
const alternatives: Placement[] = ["top", "right", "bottom", "left"];
|
|
392
|
+
for (const alt of alternatives) {
|
|
393
|
+
if (alt !== preferredPlacement) {
|
|
394
|
+
const altPos = positions[alt];
|
|
395
|
+
if (fits(altPos)) {
|
|
396
|
+
finalPosition = altPos;
|
|
397
|
+
finalPlacement = alt;
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Clamp to viewport if still overflowing
|
|
405
|
+
const margin = 4;
|
|
406
|
+
finalPosition.top = Math.max(margin, Math.min(finalPosition.top, viewport.height - tooltipRect.height - margin));
|
|
407
|
+
finalPosition.left = Math.max(margin, Math.min(finalPosition.left, viewport.width - tooltipRect.width - margin));
|
|
408
|
+
|
|
409
|
+
return { ...finalPosition, placement: finalPlacement };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
private isTargetInViewport(): boolean {
|
|
413
|
+
const rect = this.target.getBoundingClientRect();
|
|
414
|
+
return rect.top >= 0 &&
|
|
415
|
+
rect.left >= 0 &&
|
|
416
|
+
rect.bottom <= window.innerHeight &&
|
|
417
|
+
rect.right <= window.innerWidth;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
private getOrCreateOverlay(): HTMLElement {
|
|
421
|
+
let overlay = document.getElementById("dap-tooltip-overlay");
|
|
422
|
+
|
|
423
|
+
if (!overlay) {
|
|
424
|
+
overlay = document.createElement("div");
|
|
425
|
+
overlay.id = "dap-tooltip-overlay";
|
|
426
|
+
overlay.style.cssText = `
|
|
427
|
+
position: fixed;
|
|
428
|
+
top: 0;
|
|
429
|
+
left: 0;
|
|
430
|
+
right: 0;
|
|
431
|
+
bottom: 0;
|
|
432
|
+
pointer-events: none;
|
|
433
|
+
z-index: 2147483640;
|
|
434
|
+
`;
|
|
435
|
+
|
|
436
|
+
// Inject CSS
|
|
437
|
+
this.injectCSS();
|
|
438
|
+
document.body.appendChild(overlay);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return overlay;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
private injectCSS(): void {
|
|
445
|
+
if (document.getElementById("dap-tooltip-styles")) return;
|
|
446
|
+
|
|
447
|
+
const style = document.createElement("style");
|
|
448
|
+
style.id = "dap-tooltip-styles";
|
|
449
|
+
style.textContent = `
|
|
450
|
+
.dap-tooltip {
|
|
451
|
+
position: fixed;
|
|
452
|
+
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
|
|
453
|
+
color: #2c3e50;
|
|
454
|
+
padding: 12px 16px;
|
|
455
|
+
border-radius: 8px;
|
|
456
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
457
|
+
font-size: 14px;
|
|
458
|
+
font-weight: 400;
|
|
459
|
+
line-height: 1.5;
|
|
460
|
+
max-width: 320px;
|
|
461
|
+
min-width: 200px;
|
|
462
|
+
word-wrap: break-word;
|
|
463
|
+
z-index: 2147483641;
|
|
464
|
+
pointer-events: auto;
|
|
465
|
+
box-shadow:
|
|
466
|
+
0 8px 32px rgba(0, 0, 0, 0.15),
|
|
467
|
+
0 4px 16px rgba(0, 0, 0, 0.08),
|
|
468
|
+
0 0 0 1px rgba(0, 0, 0, 0.05);
|
|
469
|
+
border: 1px solid rgba(0, 0, 0, 0.08);
|
|
470
|
+
backdrop-filter: blur(8px);
|
|
471
|
+
opacity: 0;
|
|
472
|
+
transform: scale(0.95) translateY(-4px);
|
|
473
|
+
transition: all 0.2s cubic-bezier(0.4, 0.0, 0.2, 1);
|
|
474
|
+
animation: dap-tooltip-enter 0.25s cubic-bezier(0.4, 0.0, 0.2, 1) forwards;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
.dap-tooltip.dap-tooltip-visible {
|
|
478
|
+
opacity: 1;
|
|
479
|
+
transform: scale(1) translateY(0);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
.dap-tooltip-content {
|
|
483
|
+
margin: 0;
|
|
484
|
+
color: #2c3e50;
|
|
485
|
+
text-shadow: none;
|
|
486
|
+
letter-spacing: 0.02em;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
.dap-tooltip-content p {
|
|
490
|
+
margin: 0;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
.dap-tooltip-content strong {
|
|
494
|
+
font-weight: 600;
|
|
495
|
+
color: #1a202c;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
.dap-tooltip-arrow {
|
|
499
|
+
position: absolute;
|
|
500
|
+
width: 0;
|
|
501
|
+
height: 0;
|
|
502
|
+
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.08));
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/* Arrow positioning and styling */
|
|
506
|
+
.dap-tooltip[data-placement="top"] .dap-tooltip-arrow {
|
|
507
|
+
bottom: -8px;
|
|
508
|
+
left: 50%;
|
|
509
|
+
transform: translateX(-50%);
|
|
510
|
+
border-left: 8px solid transparent;
|
|
511
|
+
border-right: 8px solid transparent;
|
|
512
|
+
border-top: 8px solid #f8f9fa;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
.dap-tooltip[data-placement="right"] .dap-tooltip-arrow {
|
|
516
|
+
left: -8px;
|
|
517
|
+
top: 50%;
|
|
518
|
+
transform: translateY(-50%);
|
|
519
|
+
border-top: 8px solid transparent;
|
|
520
|
+
border-bottom: 8px solid transparent;
|
|
521
|
+
border-right: 8px solid #f8f9fa;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
.dap-tooltip[data-placement="bottom"] .dap-tooltip-arrow {
|
|
525
|
+
top: -8px;
|
|
526
|
+
left: 50%;
|
|
527
|
+
transform: translateX(-50%);
|
|
528
|
+
border-left: 8px solid transparent;
|
|
529
|
+
border-right: 8px solid transparent;
|
|
530
|
+
border-bottom: 8px solid #f8f9fa;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
.dap-tooltip[data-placement="left"] .dap-tooltip-arrow {
|
|
534
|
+
right: -8px;
|
|
535
|
+
top: 50%;
|
|
536
|
+
transform: translateY(-50%);
|
|
537
|
+
border-top: 8px solid transparent;
|
|
538
|
+
border-bottom: 8px solid transparent;
|
|
539
|
+
border-left: 8px solid #f8f9fa;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/* Animation keyframes */
|
|
543
|
+
@keyframes dap-tooltip-enter {
|
|
544
|
+
0% {
|
|
545
|
+
opacity: 0;
|
|
546
|
+
transform: scale(0.9) translateY(-8px);
|
|
547
|
+
}
|
|
548
|
+
50% {
|
|
549
|
+
opacity: 0.8;
|
|
550
|
+
}
|
|
551
|
+
100% {
|
|
552
|
+
opacity: 1;
|
|
553
|
+
transform: scale(1) translateY(0);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
@keyframes dap-tooltip-exit {
|
|
558
|
+
0% {
|
|
559
|
+
opacity: 1;
|
|
560
|
+
transform: scale(1) translateY(0);
|
|
561
|
+
}
|
|
562
|
+
100% {
|
|
563
|
+
opacity: 0;
|
|
564
|
+
transform: scale(0.95) translateY(-4px);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/* Hover states */
|
|
569
|
+
.dap-tooltip:hover {
|
|
570
|
+
background: linear-gradient(135deg, #ffffff 0%, #f1f3f5 100%);
|
|
571
|
+
box-shadow:
|
|
572
|
+
0 12px 40px rgba(0, 0, 0, 0.18),
|
|
573
|
+
0 6px 20px rgba(0, 0, 0, 0.1),
|
|
574
|
+
0 0 0 1px rgba(0, 0, 0, 0.08);
|
|
575
|
+
transform: translateY(-1px);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/* High contrast mode support */
|
|
579
|
+
@media (prefers-contrast: high) {
|
|
580
|
+
.dap-tooltip {
|
|
581
|
+
background: #ffffff;
|
|
582
|
+
border: 2px solid #000000;
|
|
583
|
+
color: #000000;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
.dap-tooltip-arrow {
|
|
587
|
+
filter: none;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
.dap-tooltip[data-placement="top"] .dap-tooltip-arrow {
|
|
591
|
+
border-top-color: #ffffff;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
.dap-tooltip[data-placement="right"] .dap-tooltip-arrow {
|
|
595
|
+
border-right-color: #ffffff;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
.dap-tooltip[data-placement="bottom"] .dap-tooltip-arrow {
|
|
599
|
+
border-bottom-color: #ffffff;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
.dap-tooltip[data-placement="left"] .dap-tooltip-arrow {
|
|
603
|
+
border-left-color: #ffffff;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/* Reduced motion support */
|
|
608
|
+
@media (prefers-reduced-motion: reduce) {
|
|
609
|
+
.dap-tooltip {
|
|
610
|
+
transition: opacity 0.15s ease;
|
|
611
|
+
animation: none;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
.dap-tooltip:hover {
|
|
615
|
+
transform: none;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/* Dark mode support */
|
|
620
|
+
@media (prefers-color-scheme: dark) {
|
|
621
|
+
.dap-tooltip {
|
|
622
|
+
background: linear-gradient(135deg, #2d3748 0%, #4a5568 100%);
|
|
623
|
+
color: #ffffff;
|
|
624
|
+
border-color: rgba(255, 255, 255, 0.1);
|
|
625
|
+
box-shadow:
|
|
626
|
+
0 8px 32px rgba(0, 0, 0, 0.4),
|
|
627
|
+
0 4px 16px rgba(0, 0, 0, 0.24),
|
|
628
|
+
0 0 0 1px rgba(255, 255, 255, 0.1);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
.dap-tooltip-content {
|
|
632
|
+
color: #ffffff;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
.dap-tooltip-content strong {
|
|
636
|
+
color: #e2e8f0;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
.dap-tooltip[data-placement="top"] .dap-tooltip-arrow {
|
|
640
|
+
border-top-color: #2d3748;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
.dap-tooltip[data-placement="right"] .dap-tooltip-arrow {
|
|
644
|
+
border-right-color: #2d3748;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
.dap-tooltip[data-placement="bottom"] .dap-tooltip-arrow {
|
|
648
|
+
border-bottom-color: #2d3748;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
.dap-tooltip[data-placement="left"] .dap-tooltip-arrow {
|
|
652
|
+
border-left-color: #2d3748;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/* Focus indicators for accessibility */
|
|
657
|
+
.dap-tooltip:focus-within {
|
|
658
|
+
outline: 2px solid #4a90e2;
|
|
659
|
+
outline-offset: 2px;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/* RTL support */
|
|
663
|
+
[dir="rtl"] .dap-tooltip {
|
|
664
|
+
text-align: right;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/* Mobile responsive adjustments */
|
|
668
|
+
@media (max-width: 768px) {
|
|
669
|
+
.dap-tooltip {
|
|
670
|
+
max-width: calc(100vw - 32px);
|
|
671
|
+
font-size: 16px;
|
|
672
|
+
padding: 16px 20px;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
@media (max-width: 480px) {
|
|
677
|
+
.dap-tooltip {
|
|
678
|
+
max-width: calc(100vw - 16px);
|
|
679
|
+
border-radius: 12px;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
`;
|
|
683
|
+
|
|
684
|
+
document.head.appendChild(style);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
destroy(): void {
|
|
688
|
+
console.debug("[DAP] Tooltip destroyed", { id: this.id });
|
|
689
|
+
|
|
690
|
+
this.hide();
|
|
691
|
+
|
|
692
|
+
// Clean up all listeners
|
|
693
|
+
this.listeners.forEach(cleanup => cleanup());
|
|
694
|
+
this.listeners = [];
|
|
695
|
+
|
|
696
|
+
// Clean up target observer
|
|
697
|
+
if (this.targetObserver) {
|
|
698
|
+
this.targetObserver.disconnect();
|
|
699
|
+
this.targetObserver = null;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// Target resolution utility
|
|
705
|
+
async function waitForTarget(selector: string, timeout: number): Promise<HTMLElement | null> {
|
|
706
|
+
const startTime = Date.now();
|
|
707
|
+
|
|
708
|
+
// Try immediate resolution
|
|
709
|
+
let element = resolveSelector<HTMLElement>(selector);
|
|
710
|
+
if (element) return element;
|
|
711
|
+
|
|
712
|
+
return new Promise((resolve) => {
|
|
713
|
+
const observer = new MutationObserver(() => {
|
|
714
|
+
element = resolveSelector<HTMLElement>(selector);
|
|
715
|
+
if (element) {
|
|
716
|
+
observer.disconnect();
|
|
717
|
+
resolve(element);
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Check timeout
|
|
722
|
+
if (Date.now() - startTime > timeout) {
|
|
723
|
+
observer.disconnect();
|
|
724
|
+
resolve(null);
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
observer.observe(document.documentElement, {
|
|
729
|
+
childList: true,
|
|
730
|
+
subtree: true,
|
|
731
|
+
attributes: true
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
// Fallback timeout
|
|
735
|
+
setTimeout(() => {
|
|
736
|
+
observer.disconnect();
|
|
737
|
+
resolve(resolveSelector<HTMLElement>(selector));
|
|
738
|
+
}, timeout);
|
|
739
|
+
});
|
|
740
|
+
}
|