@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.

Files changed (60) hide show
  1. package/.github/copilot-instructions.md +95 -0
  2. package/README.md +79 -0
  3. package/TRACKING.md +105 -0
  4. package/USER_CONTEXT_README.md +284 -0
  5. package/package.json +154 -0
  6. package/src/config.ts +25 -0
  7. package/src/core/flowEngine.ts +1833 -0
  8. package/src/core/triggerManager.ts +1011 -0
  9. package/src/experiences/banner.ts +366 -0
  10. package/src/experiences/beacon.ts +668 -0
  11. package/src/experiences/hotspotTour.ts +654 -0
  12. package/src/experiences/hotspots.ts +566 -0
  13. package/src/experiences/modal.ts +1337 -0
  14. package/src/experiences/modalSequence.ts +1247 -0
  15. package/src/experiences/popover.ts +652 -0
  16. package/src/experiences/registry.ts +21 -0
  17. package/src/experiences/survey.ts +1639 -0
  18. package/src/experiences/taskList.ts +625 -0
  19. package/src/experiences/tooltip.ts +740 -0
  20. package/src/experiences/types.ts +395 -0
  21. package/src/experiences/walkthrough.ts +670 -0
  22. package/src/flow-sequence.ts +177 -0
  23. package/src/flows.ts +512 -0
  24. package/src/http.ts +61 -0
  25. package/src/index.ts +355 -0
  26. package/src/services/flowManager.ts +905 -0
  27. package/src/services/flowNormalizer.ts +74 -0
  28. package/src/services/locationContextService.ts +189 -0
  29. package/src/services/pageContextService.ts +221 -0
  30. package/src/services/userContextService.ts +286 -0
  31. package/src/state/appState.ts +0 -0
  32. package/src/state/hooks.ts +0 -0
  33. package/src/state/index.ts +0 -0
  34. package/src/state/migration.ts +0 -0
  35. package/src/state/store.ts +0 -0
  36. package/src/styles/banner.css.ts +0 -0
  37. package/src/styles/hotspot.css.ts +0 -0
  38. package/src/styles/hotspotTour.css.ts +0 -0
  39. package/src/styles/modal.css.ts +564 -0
  40. package/src/styles/survey.css.ts +1013 -0
  41. package/src/styles/taskList.css.ts +0 -0
  42. package/src/styles/tooltip.css.ts +149 -0
  43. package/src/styles/walkthrough.css.ts +0 -0
  44. package/src/tourUtils.ts +0 -0
  45. package/src/tracking.ts +223 -0
  46. package/src/utils/debounce.ts +66 -0
  47. package/src/utils/eventSequenceValidator.ts +124 -0
  48. package/src/utils/flowTrackingSystem.ts +524 -0
  49. package/src/utils/idGenerator.ts +155 -0
  50. package/src/utils/immediateValidationPrevention.ts +184 -0
  51. package/src/utils/normalize.ts +50 -0
  52. package/src/utils/privacyManager.ts +166 -0
  53. package/src/utils/ruleEvaluator.ts +199 -0
  54. package/src/utils/sanitize.ts +79 -0
  55. package/src/utils/selectors.ts +107 -0
  56. package/src/utils/stepExecutor.ts +345 -0
  57. package/src/utils/triggerNormalizer.ts +149 -0
  58. package/src/utils/validationInterceptor.ts +650 -0
  59. package/tsconfig.json +13 -0
  60. 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
+ }