@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,652 @@
1
+ import { sanitizeHtml } from "../utils/sanitize";
2
+ import { register } from "./registry";
3
+ import type { PopoverPayload, PopoverButton } from "./types";
4
+ import { resolveSelector } from "../utils/selectors";
5
+
6
+ type PopoverFlow = { id: string; type: "popover"; payload: PopoverPayload };
7
+
8
+ interface PopoverState {
9
+ id: string;
10
+ element: HTMLElement;
11
+ targetElement: HTMLElement;
12
+ observer: MutationObserver | null;
13
+ cleanup: (() => void)[];
14
+ isActive: boolean;
15
+ }
16
+
17
+ const activePopovers = new Map<string, PopoverState>();
18
+
19
+ export function registerPopover() {
20
+ register("popover", renderPopover);
21
+ }
22
+
23
+ export async function renderPopover(flow: PopoverFlow): Promise<void> {
24
+ const { payload, id } = flow;
25
+
26
+ console.debug("[DAP] Popover initialized", { id, payload });
27
+ console.debug("[DAP] Popover targetSelector:", payload.targetSelector);
28
+ console.debug("[DAP] Popover trigger:", payload.trigger);
29
+ console.debug("[DAP] Popover body:", payload.body);
30
+ console.debug("[DAP] Popover bodyBlocks:", payload.bodyBlocks);
31
+
32
+ if (!payload.targetSelector) {
33
+ console.error("[DAP] Popover missing required elementSelector");
34
+ payload._completionTracker?.onComplete?.();
35
+ return;
36
+ }
37
+
38
+ if (!payload.body && !payload.bodyBlocks) {
39
+ console.error("[DAP] Popover missing required content");
40
+ payload._completionTracker?.onComplete?.();
41
+ return;
42
+ }
43
+
44
+ const targetSelector = payload.targetSelector;
45
+ const trigger = payload.trigger || "click";
46
+
47
+ console.debug("[DAP] Popover looking for target element:", targetSelector);
48
+
49
+ if (activePopovers.has(id)) {
50
+ cleanupPopover(id);
51
+ }
52
+
53
+ const targetElement = await waitForTargetElement(targetSelector);
54
+
55
+ if (!targetElement) {
56
+ console.warn("[DAP] Popover target not found:", targetSelector);
57
+ console.debug("[DAP] Available elements with IDs:", Array.from(document.querySelectorAll('[id]')).map(el => el.id));
58
+ console.debug("[DAP] Available elements with classes:", Array.from(document.querySelectorAll('[class]')).slice(0, 10).map(el => el.className));
59
+ payload._completionTracker?.onComplete?.();
60
+ return;
61
+ }
62
+
63
+ console.debug("[DAP] Popover anchor resolved successfully", { targetSelector, targetElement });
64
+
65
+ const popoverElement = createPopoverElement(payload, id);
66
+ console.debug("[DAP] Popover element created:", popoverElement);
67
+
68
+ const popoverState: PopoverState = {
69
+ id,
70
+ element: popoverElement,
71
+ targetElement,
72
+ observer: null,
73
+ cleanup: [],
74
+ isActive: false
75
+ };
76
+
77
+ activePopovers.set(id, popoverState);
78
+
79
+ setupTriggerHandling(popoverState, trigger, payload);
80
+ setupTargetObservation(popoverState);
81
+
82
+ console.debug("[DAP] Popover setup complete", { id, trigger });
83
+ }
84
+
85
+ async function waitForTargetElement(
86
+ selector: string,
87
+ timeout = 5000
88
+ ): Promise<HTMLElement | null> {
89
+
90
+ const existing = resolveSelector(selector);
91
+ if (existing) {
92
+ return existing as HTMLElement;
93
+ }
94
+
95
+ return new Promise((resolve) => {
96
+ let timeoutId: number;
97
+ let observer: MutationObserver;
98
+
99
+ const cleanup = () => {
100
+ if (timeoutId) clearTimeout(timeoutId);
101
+ if (observer) observer.disconnect();
102
+ };
103
+
104
+ timeoutId = setTimeout(() => {
105
+ cleanup();
106
+ resolve(null);
107
+ }, timeout);
108
+
109
+ observer = new MutationObserver(() => {
110
+ const element = resolveSelector(selector);
111
+ if (element) {
112
+ cleanup();
113
+ resolve(element as HTMLElement);
114
+ }
115
+ });
116
+
117
+ observer.observe(document.documentElement, {
118
+ childList: true,
119
+ subtree: true,
120
+ attributes: false
121
+ });
122
+ });
123
+ }
124
+
125
+ function createPopoverElement(payload: PopoverPayload, id: string): HTMLElement {
126
+ const popover = document.createElement('div');
127
+ popover.className = 'dap-popover';
128
+ popover.id = `dap-popover-${id}`;
129
+ popover.setAttribute('role', 'dialog');
130
+ popover.setAttribute('aria-live', 'polite');
131
+
132
+ Object.assign(popover.style, {
133
+ position: 'absolute',
134
+ zIndex: '9999',
135
+ background: '#f0f9ff',
136
+ border: '1px solid #bae6fd',
137
+ borderRadius: '12px',
138
+ boxShadow: '0 8px 32px rgba(59, 130, 246, 0.12), 0 4px 16px rgba(0, 0, 0, 0.08)',
139
+ padding: '18px',
140
+ maxWidth: '320px',
141
+ minWidth: '200px',
142
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
143
+ fontSize: '14px',
144
+ lineHeight: '1.5',
145
+ color: '#1e293b',
146
+ opacity: '0',
147
+ transform: 'scale(0.95)',
148
+ transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
149
+ pointerEvents: 'none'
150
+ });
151
+
152
+ if (payload.title) {
153
+ const title = document.createElement('h4');
154
+ title.style.cssText = `
155
+ margin: 0 0 10px 0;
156
+ font-size: 16px;
157
+ font-weight: 600;
158
+ color: #0f172a;
159
+ line-height: 1.25;
160
+ text-shadow: 0 1px 2px rgba(255, 255, 255, 0.5);
161
+ `;
162
+ title.textContent = payload.title;
163
+ popover.appendChild(title);
164
+ }
165
+
166
+ if (payload.body) {
167
+ const body = document.createElement('div');
168
+ body.style.cssText = `
169
+ color: #475569;
170
+ line-height: 1.6;
171
+ margin: 0;
172
+ text-shadow: 0 1px 1px rgba(255, 255, 255, 0.3);
173
+ `;
174
+ body.innerHTML = sanitizeHtml(payload.body);
175
+ popover.appendChild(body);
176
+ }
177
+
178
+ const ctaContainer = createCTAButtons(payload, id);
179
+ if (ctaContainer) {
180
+ popover.appendChild(ctaContainer);
181
+ }
182
+
183
+ if (payload.showArrow !== false) {
184
+ const arrow = createPopoverArrow();
185
+ popover.appendChild(arrow);
186
+ }
187
+
188
+ return popover;
189
+ }
190
+
191
+ function createCTAButtons(payload: PopoverPayload, id: string): HTMLElement | null {
192
+ const hasButtons = payload.bodyBlocks?.some(block => block.kind === 'button');
193
+
194
+ if (!hasButtons) return null;
195
+
196
+ const container = document.createElement('div');
197
+ container.style.cssText = `
198
+ margin-top: 12px;
199
+ display: flex;
200
+ gap: 8px;
201
+ justify-content: flex-end;
202
+ `;
203
+
204
+ payload.bodyBlocks?.forEach(block => {
205
+ if (block.kind === 'button') {
206
+ const buttonBlock = block as PopoverButton;
207
+ const button = document.createElement('button');
208
+ button.style.cssText = `
209
+ padding: 8px 16px;
210
+ border: 1px solid ${buttonBlock.variant === 'primary' ? '#3b82f6' : '#cbd5e1'};
211
+ border-radius: 8px;
212
+ background: ${buttonBlock.variant === 'primary' ? '#3b82f6' : '#ffffff'};
213
+ color: ${buttonBlock.variant === 'primary' ? '#ffffff' : '#1e293b'};
214
+ font-size: 13px;
215
+ font-weight: 500;
216
+ cursor: pointer;
217
+ transition: all 0.2s ease;
218
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
219
+ `;
220
+
221
+ button.textContent = buttonBlock.label;
222
+
223
+ button.addEventListener('click', () => {
224
+ console.debug("[DAP] Popover CTA clicked", { id, action: buttonBlock.action });
225
+
226
+ if (buttonBlock.action === 'advance') {
227
+ payload._completionTracker?.onStepAdvance?.(payload.stepId || id);
228
+ } else if (buttonBlock.action === 'dismiss') {
229
+ dismissPopover(id);
230
+ }
231
+
232
+ payload._completionTracker?.onComplete?.();
233
+ });
234
+
235
+ button.addEventListener('mouseenter', () => {
236
+ if (buttonBlock.variant === 'primary') {
237
+ button.style.background = '#2563eb';
238
+ button.style.transform = 'translateY(-1px)';
239
+ } else {
240
+ button.style.background = '#f1f5f9';
241
+ button.style.transform = 'translateY(-1px)';
242
+ }
243
+ });
244
+
245
+ button.addEventListener('mouseleave', () => {
246
+ button.style.background = buttonBlock.variant === 'primary' ? '#3b82f6' : '#ffffff';
247
+ button.style.transform = 'translateY(0)';
248
+ });
249
+
250
+ container.appendChild(button);
251
+ }
252
+ });
253
+
254
+ return container.children.length > 0 ? container : null;
255
+ }
256
+
257
+ function createPopoverArrow(): HTMLElement {
258
+ const arrow = document.createElement('div');
259
+ arrow.className = 'dap-popover-arrow';
260
+ arrow.style.cssText = `
261
+ position: absolute;
262
+ width: 10px;
263
+ height: 10px;
264
+ background: #f0f9ff;
265
+ border: 1px solid #bae6fd;
266
+ transform: rotate(45deg);
267
+ z-index: -1;
268
+ `;
269
+ return arrow;
270
+ }
271
+
272
+ function setupTriggerHandling(
273
+ state: PopoverState,
274
+ trigger: string,
275
+ payload: PopoverPayload
276
+ ): void {
277
+
278
+ const { targetElement, element } = state;
279
+
280
+ // Normalize trigger names
281
+ const normalizedTrigger = trigger === 'on click' ? 'click' :
282
+ trigger === 'on hover' ? 'hover' :
283
+ trigger === 'on focus' ? 'focus' :
284
+ trigger === 'on page load' ? 'on page load' :
285
+ trigger;
286
+
287
+ console.debug("[DAP] Popover setting up trigger:", { originalTrigger: trigger, normalizedTrigger, targetElement });
288
+
289
+ switch (normalizedTrigger) {
290
+ case 'click':
291
+ const clickHandler = (e: Event) => {
292
+ console.debug("[DAP] Popover click triggered", e);
293
+ e.preventDefault();
294
+ e.stopPropagation();
295
+ showPopover(state, payload);
296
+ };
297
+ targetElement.addEventListener('click', clickHandler);
298
+ state.cleanup.push(() => targetElement.removeEventListener('click', clickHandler));
299
+ console.debug("[DAP] Popover click listener attached");
300
+ break;
301
+
302
+ case 'hover':
303
+ const showHandler = () => {
304
+ console.debug("[DAP] Popover hover show triggered");
305
+ showPopover(state, payload);
306
+ };
307
+ const hideHandler = () => {
308
+ console.debug("[DAP] Popover hover hide triggered");
309
+ hidePopover(state, payload);
310
+ };
311
+
312
+ targetElement.addEventListener('mouseenter', showHandler);
313
+ targetElement.addEventListener('mouseleave', hideHandler);
314
+ element.addEventListener('mouseenter', showHandler);
315
+ element.addEventListener('mouseleave', hideHandler);
316
+
317
+ state.cleanup.push(() => {
318
+ targetElement.removeEventListener('mouseenter', showHandler);
319
+ targetElement.removeEventListener('mouseleave', hideHandler);
320
+ element.removeEventListener('mouseenter', showHandler);
321
+ element.removeEventListener('mouseleave', hideHandler);
322
+ });
323
+ console.debug("[DAP] Popover hover listeners attached");
324
+ break;
325
+
326
+ case 'focus':
327
+ const focusHandler = () => {
328
+ console.debug("[DAP] Popover focus triggered");
329
+ showPopover(state, payload);
330
+ };
331
+ const blurHandler = () => {
332
+ console.debug("[DAP] Popover blur triggered");
333
+ hidePopover(state, payload);
334
+ };
335
+
336
+ targetElement.addEventListener('focus', focusHandler);
337
+ targetElement.addEventListener('blur', blurHandler);
338
+
339
+ state.cleanup.push(() => {
340
+ targetElement.removeEventListener('focus', focusHandler);
341
+ targetElement.removeEventListener('blur', blurHandler);
342
+ });
343
+ console.debug("[DAP] Popover focus listeners attached");
344
+ break;
345
+
346
+ case 'on page load':
347
+ default:
348
+ setTimeout(() => showPopover(state, payload), 100);
349
+ break;
350
+ }
351
+ }
352
+
353
+ function setupTargetObservation(state: PopoverState): void {
354
+ const observer = new MutationObserver(() => {
355
+ const isStillConnected = state.targetElement.isConnected;
356
+
357
+ if (!isStillConnected) {
358
+ console.debug("[DAP] Popover target element disappeared", { id: state.id });
359
+ cleanupPopover(state.id);
360
+ }
361
+ });
362
+
363
+ observer.observe(document.documentElement, {
364
+ childList: true,
365
+ subtree: true
366
+ });
367
+
368
+ state.observer = observer;
369
+ state.cleanup.push(() => observer.disconnect());
370
+ }
371
+
372
+ function showPopover(state: PopoverState, payload: PopoverPayload): void {
373
+ console.debug("[DAP] showPopover called", { id: state.id, isActive: state.isActive });
374
+
375
+ if (state.isActive) {
376
+ console.debug("[DAP] Popover already active, skipping");
377
+ return;
378
+ }
379
+
380
+ state.isActive = true;
381
+
382
+ console.debug("[DAP] Popover shown", { id: state.id });
383
+
384
+ document.body.appendChild(state.element);
385
+ console.debug("[DAP] Popover element appended to body");
386
+
387
+ positionPopover(state, payload.placement || "bottom", payload.showArrow !== false);
388
+ console.debug("[DAP] Popover positioned");
389
+
390
+ setTimeout(() => {
391
+ state.element.style.pointerEvents = 'auto';
392
+ state.element.style.opacity = '1';
393
+ state.element.style.transform = 'scale(1)';
394
+ console.debug("[DAP] Popover animation started");
395
+ }, 10);
396
+
397
+ setupGlobalEventHandlers(state, payload);
398
+
399
+ if (hasButtons(payload)) {
400
+ state.element.setAttribute('tabindex', '-1');
401
+ state.element.focus();
402
+ trapFocus(state.element);
403
+ }
404
+
405
+ console.debug("[DAP] Popover show complete");
406
+ }
407
+
408
+ function hidePopover(state: PopoverState, payload: PopoverPayload): void {
409
+ if (!state.isActive) return;
410
+
411
+ state.isActive = false;
412
+
413
+ console.debug("[DAP] Popover dismissed", { id: state.id });
414
+
415
+ state.element.style.opacity = '0';
416
+ state.element.style.transform = 'scale(0.95)';
417
+ state.element.style.pointerEvents = 'none';
418
+
419
+ setTimeout(() => {
420
+ if (state.element.parentNode) {
421
+ state.element.parentNode.removeChild(state.element);
422
+ }
423
+ }, 150);
424
+
425
+ payload._completionTracker?.onComplete?.();
426
+ }
427
+
428
+ function dismissPopover(id: string): void {
429
+ const state = activePopovers.get(id);
430
+ if (state) {
431
+ hidePopover(state, {} as PopoverPayload);
432
+ }
433
+ }
434
+
435
+ function positionPopover(state: PopoverState, placement: string, showArrow: boolean): void {
436
+ const { element, targetElement } = state;
437
+ const targetRect = targetElement.getBoundingClientRect();
438
+ const popoverRect = element.getBoundingClientRect();
439
+ const viewportWidth = window.innerWidth;
440
+ const viewportHeight = window.innerHeight;
441
+ const scrollX = window.scrollX;
442
+ const scrollY = window.scrollY;
443
+
444
+ const spacing = 8;
445
+ const viewportPadding = 16;
446
+
447
+ let top: number;
448
+ let left: number;
449
+ let actualPlacement = placement;
450
+
451
+ const positions = {
452
+ top: {
453
+ top: targetRect.top + scrollY - popoverRect.height - spacing,
454
+ left: targetRect.left + scrollX + (targetRect.width - popoverRect.width) / 2
455
+ },
456
+ bottom: {
457
+ top: targetRect.bottom + scrollY + spacing,
458
+ left: targetRect.left + scrollX + (targetRect.width - popoverRect.width) / 2
459
+ },
460
+ left: {
461
+ top: targetRect.top + scrollY + (targetRect.height - popoverRect.height) / 2,
462
+ left: targetRect.left + scrollX - popoverRect.width - spacing
463
+ },
464
+ right: {
465
+ top: targetRect.top + scrollY + (targetRect.height - popoverRect.height) / 2,
466
+ left: targetRect.right + scrollX + spacing
467
+ }
468
+ };
469
+
470
+ const preferred = positions[placement as keyof typeof positions];
471
+ if (!preferred || !fitsInViewport(preferred, popoverRect, viewportPadding)) {
472
+ actualPlacement = findBestPlacement(positions, popoverRect, viewportPadding) || placement;
473
+ }
474
+
475
+ const finalPosition = positions[actualPlacement as keyof typeof positions] || positions.bottom;
476
+ top = finalPosition.top;
477
+ left = finalPosition.left;
478
+
479
+ left = Math.max(viewportPadding, Math.min(left, viewportWidth - popoverRect.width - viewportPadding));
480
+ top = Math.max(viewportPadding, Math.min(top, viewportHeight - popoverRect.height - viewportPadding));
481
+
482
+ element.style.top = `${top}px`;
483
+ element.style.left = `${left}px`;
484
+
485
+ if (showArrow) {
486
+ positionArrow(element, targetRect, actualPlacement, { top, left }, scrollX, scrollY);
487
+ }
488
+ }
489
+
490
+ function fitsInViewport(
491
+ position: { top: number; left: number },
492
+ rect: DOMRect,
493
+ padding: number
494
+ ): boolean {
495
+ return position.top >= padding &&
496
+ position.left >= padding &&
497
+ position.top + rect.height <= window.innerHeight - padding &&
498
+ position.left + rect.width <= window.innerWidth - padding;
499
+ }
500
+
501
+ function findBestPlacement(
502
+ positions: Record<string, { top: number; left: number }>,
503
+ rect: DOMRect,
504
+ padding: number
505
+ ): string | null {
506
+
507
+ const placements = ['bottom', 'top', 'right', 'left'];
508
+
509
+ for (const placement of placements) {
510
+ const pos = positions[placement];
511
+ if (pos && fitsInViewport(pos, rect, padding)) {
512
+ return placement;
513
+ }
514
+ }
515
+
516
+ return null;
517
+ }
518
+
519
+ function positionArrow(
520
+ popover: HTMLElement,
521
+ targetRect: DOMRect,
522
+ placement: string,
523
+ popoverPos: { top: number; left: number },
524
+ scrollX: number,
525
+ scrollY: number
526
+ ): void {
527
+
528
+ const arrow = popover.querySelector('.dap-popover-arrow') as HTMLElement;
529
+ if (!arrow) return;
530
+
531
+ const arrowSize = 8;
532
+ const targetCenterX = targetRect.left + scrollX + targetRect.width / 2;
533
+ const targetCenterY = targetRect.top + scrollY + targetRect.height / 2;
534
+
535
+ switch (placement) {
536
+ case 'top':
537
+ arrow.style.top = `calc(100% - 1px)`;
538
+ arrow.style.left = `${Math.max(12, Math.min(targetCenterX - popoverPos.left - arrowSize/2, popover.offsetWidth - 20))}px`;
539
+ arrow.style.borderBottomColor = 'transparent';
540
+ arrow.style.borderRightColor = 'transparent';
541
+ break;
542
+
543
+ case 'bottom':
544
+ arrow.style.top = `-${arrowSize/2}px`;
545
+ arrow.style.left = `${Math.max(12, Math.min(targetCenterX - popoverPos.left - arrowSize/2, popover.offsetWidth - 20))}px`;
546
+ arrow.style.borderTopColor = 'transparent';
547
+ arrow.style.borderLeftColor = 'transparent';
548
+ break;
549
+
550
+ case 'left':
551
+ arrow.style.left = `calc(100% - 1px)`;
552
+ arrow.style.top = `${Math.max(12, Math.min(targetCenterY - popoverPos.top - arrowSize/2, popover.offsetHeight - 20))}px`;
553
+ arrow.style.borderRightColor = 'transparent';
554
+ arrow.style.borderBottomColor = 'transparent';
555
+ break;
556
+
557
+ case 'right':
558
+ arrow.style.left = `-${arrowSize/2}px`;
559
+ arrow.style.top = `${Math.max(12, Math.min(targetCenterY - popoverPos.top - arrowSize/2, popover.offsetHeight - 20))}px`;
560
+ arrow.style.borderLeftColor = 'transparent';
561
+ arrow.style.borderTopColor = 'transparent';
562
+ break;
563
+ }
564
+ }
565
+
566
+ function setupGlobalEventHandlers(state: PopoverState, payload: PopoverPayload): void {
567
+ const outsideClickHandler = (e: Event) => {
568
+ const target = e.target as Node;
569
+ if (!state.element.contains(target) && !state.targetElement.contains(target)) {
570
+ hidePopover(state, payload);
571
+ }
572
+ };
573
+
574
+ const keyHandler = (e: KeyboardEvent) => {
575
+ if (e.key === 'Escape') {
576
+ e.preventDefault();
577
+ hidePopover(state, payload);
578
+ }
579
+ };
580
+
581
+ const navigationHandler = () => {
582
+ hidePopover(state, payload);
583
+ };
584
+
585
+ setTimeout(() => {
586
+ document.addEventListener('click', outsideClickHandler);
587
+ document.addEventListener('keydown', keyHandler);
588
+ window.addEventListener('beforeunload', navigationHandler);
589
+ window.addEventListener('popstate', navigationHandler);
590
+ }, 100);
591
+
592
+ state.cleanup.push(() => {
593
+ document.removeEventListener('click', outsideClickHandler);
594
+ document.removeEventListener('keydown', keyHandler);
595
+ window.removeEventListener('beforeunload', navigationHandler);
596
+ window.removeEventListener('popstate', navigationHandler);
597
+ });
598
+ }
599
+
600
+ function hasButtons(payload: PopoverPayload): boolean {
601
+ return payload.bodyBlocks?.some(block => block.kind === 'button') || false;
602
+ }
603
+
604
+ function trapFocus(element: HTMLElement): void {
605
+ const focusableElements = element.querySelectorAll(
606
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
607
+ );
608
+
609
+ if (focusableElements.length === 0) return;
610
+
611
+ const firstElement = focusableElements[0] as HTMLElement;
612
+ const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;
613
+
614
+ const handleTab = (e: KeyboardEvent) => {
615
+ if (e.key !== 'Tab') return;
616
+
617
+ if (e.shiftKey) {
618
+ if (document.activeElement === firstElement) {
619
+ e.preventDefault();
620
+ lastElement.focus();
621
+ }
622
+ } else {
623
+ if (document.activeElement === lastElement) {
624
+ e.preventDefault();
625
+ firstElement.focus();
626
+ }
627
+ }
628
+ };
629
+
630
+ element.addEventListener('keydown', handleTab);
631
+ }
632
+
633
+ function cleanupPopover(id: string): void {
634
+ console.debug("[DAP] Popover destroyed", { id });
635
+
636
+ const state = activePopovers.get(id);
637
+ if (!state) return;
638
+
639
+ state.cleanup.forEach(cleanup => {
640
+ try {
641
+ cleanup();
642
+ } catch (error) {
643
+ console.warn("[DAP] Error during popover cleanup:", error);
644
+ }
645
+ });
646
+
647
+ if (state.element && state.element.parentNode) {
648
+ state.element.parentNode.removeChild(state.element);
649
+ }
650
+
651
+ activePopovers.delete(id);
652
+ }
@@ -0,0 +1,21 @@
1
+ // src/experiences/registry.ts
2
+ // A minimal, generic renderer registry. No coupling to flow types.
3
+
4
+ export type Renderer<T = any> = (flow: T) => Promise<void> | void;
5
+
6
+ const REGISTRY = new Map<string, Renderer>();
7
+
8
+ /** Register a renderer for a normalized flow type (e.g., "modalSequence", "modal", "tooltip", "popover"). */
9
+ export function register(type: string, renderer: Renderer): void {
10
+ REGISTRY.set(type, renderer);
11
+ }
12
+
13
+ /** Look up the renderer for a normalized flow type. */
14
+ export function getRenderer<T = any>(type: string): Renderer<T> | undefined {
15
+ return REGISTRY.get(type) as Renderer<T> | undefined;
16
+ }
17
+
18
+ /** For tests/dev: clears all registered renderers. */
19
+ export function resetRegistry(): void {
20
+ REGISTRY.clear();
21
+ }