@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,1247 @@
1
+ // src/experiences/modalSequence.ts
2
+ // Multi-step modal sequence renderer with trigger support
3
+
4
+ import { sanitizeHtml } from "../utils/sanitize";
5
+ import { register, getRenderer } from "./registry";
6
+ import { resolveSelector } from "../utils/selectors";
7
+ import { modalCssText } from "../styles/modal.css";
8
+ import { createRuleEvaluationListener } from "../utils/ruleEvaluator";
9
+ import type {
10
+ ModalSequencePayload,
11
+ ModalSequenceStep,
12
+ ModalContent,
13
+ ModalSize,
14
+ TooltipPayload,
15
+ PopoverPayload,
16
+ CompletionTracker
17
+ } from "./types";
18
+
19
+ type ModalSeqFlow = { id: string; type: "modalSequence"; payload: ModalSequencePayload };
20
+
21
+ export function registerModalSequence() {
22
+ register("modalSequence", renderModalSequence);
23
+ }
24
+
25
+ export async function renderModalSequence(flow: ModalSeqFlow): Promise<void> {
26
+ console.debug("[DAP] renderModalSequence called with flow:", flow);
27
+ console.debug("[DAP] ModalSequence payload:", flow.payload);
28
+
29
+ const { payload, id } = flow;
30
+
31
+ if (!payload || !Array.isArray(payload.steps) || payload.steps.length === 0) {
32
+ console.warn("[DAP] Modal sequence has no steps");
33
+ return;
34
+ }
35
+
36
+ // Extract completion tracker
37
+ const completionTracker = payload._completionTracker;
38
+ const isMultiStep = payload.steps.length > 1;
39
+
40
+ // Ensure CSS is injected
41
+ ensureStyles();
42
+
43
+ // State management
44
+ let currentStepIndex = payload.startAt || 0;
45
+ let currentTriggerListeners: (() => void)[] = [];
46
+ let currentStepState: 'idle' | 'waiting-for-trigger' | 'rendered' | 'completed' = 'idle';
47
+ let activeStepTriggered = false;
48
+
49
+ // UI cleanup functions
50
+ let modalShell: ReturnType<typeof createModalShell> | null = null;
51
+ let tooltipCleanup: (() => void) | null = null;
52
+ let popoverCleanup: (() => void) | null = null;
53
+
54
+ // Focus management
55
+ const prevActive = document.activeElement as HTMLElement | null;
56
+
57
+ // Keyboard handler
58
+ const onKeyDown = (e: KeyboardEvent) => {
59
+ if (e.key === "Escape") {
60
+ closeAll();
61
+ }
62
+ };
63
+ document.addEventListener("keydown", onKeyDown, true);
64
+
65
+ // Start with first step
66
+ await evaluateAndRenderStep(currentStepIndex);
67
+
68
+ async function evaluateAndRenderStep(stepIndex: number) {
69
+ const step = payload.steps[stepIndex];
70
+ if (!step) return;
71
+
72
+ const stepId = step.stepId || `step-${stepIndex + 1}`;
73
+
74
+ console.debug(`[DAP] Evaluating step ${stepIndex}:`, {
75
+ stepId,
76
+ elementSelector: step.elementSelector,
77
+ elementTrigger: step.elementTrigger,
78
+ kind: step.kind
79
+ });
80
+
81
+ // Determine if this is a trigger-based step
82
+ const isTriggerBasedStep = Boolean(step.elementSelector && step.elementTrigger);
83
+
84
+ if (isTriggerBasedStep) {
85
+ // Setup trigger and wait
86
+ await setupStepTrigger(stepIndex, step, stepId);
87
+ currentStepState = 'waiting-for-trigger';
88
+ console.debug(`[DAP] Step ${stepIndex} waiting for trigger`);
89
+ } else {
90
+ // Render immediately
91
+ await renderStepExperience(stepIndex, step, stepId, true);
92
+ currentStepState = 'rendered';
93
+ console.debug(`[DAP] Step ${stepIndex} rendered immediately`);
94
+ }
95
+ }
96
+
97
+ async function setupStepTrigger(stepIndex: number, step: ModalSequenceStep, stepId: string) {
98
+ if (!step.elementSelector || !step.elementTrigger) return;
99
+
100
+ try {
101
+ // Wait for target element
102
+ const targetElement = await waitForElement(step.elementSelector);
103
+ if (!targetElement) {
104
+ console.warn(`[DAP] Target element not found: ${step.elementSelector}`);
105
+ return;
106
+ }
107
+
108
+ // Setup trigger listener
109
+ const triggerEvent = normalizeTrigger(step.elementTrigger);
110
+ const triggerHandler = () => {
111
+ if (activeStepTriggered) return; // Prevent duplicate triggers
112
+ activeStepTriggered = true;
113
+
114
+ console.debug(`[DAP] Step ${stepIndex} triggered by ${triggerEvent}`);
115
+ renderStepExperience(stepIndex, step, stepId, false); // false = no navigation for trigger steps
116
+ };
117
+
118
+ targetElement.addEventListener(triggerEvent, triggerHandler);
119
+
120
+ // Store cleanup function
121
+ const cleanup = () => {
122
+ targetElement.removeEventListener(triggerEvent, triggerHandler);
123
+ };
124
+ currentTriggerListeners.push(cleanup);
125
+
126
+ } catch (error) {
127
+ console.error(`[DAP] Error setting up trigger for step ${stepIndex}:`, error);
128
+ // Auto-advance on trigger setup failure
129
+ if (stepIndex < payload.steps.length - 1) {
130
+ setTimeout(() => transitionToStep(stepIndex + 1), 100);
131
+ } else {
132
+ closeAll();
133
+ }
134
+ }
135
+ }
136
+
137
+ async function renderStepExperience(stepIndex: number, step: ModalSequenceStep, stepId: string, showNavigation: boolean) {
138
+ // Clean up previous UI
139
+ cleanupCurrentStep();
140
+
141
+ console.debug(`[DAP] Rendering step ${stepIndex} (${step.kind})`);
142
+
143
+ switch (step.kind) {
144
+ case "modal":
145
+ await renderModalStep(step, showNavigation);
146
+ break;
147
+
148
+ case "tooltip":
149
+ if (step.tooltip) {
150
+ await renderTooltipStep(step.tooltip);
151
+ }
152
+ break;
153
+
154
+ case "popover":
155
+ if (step.popover) {
156
+ await renderPopoverStep(step.popover);
157
+ }
158
+ break;
159
+
160
+ case "survey":
161
+ // Survey implementation would go here
162
+ console.warn("[DAP] Survey step not implemented");
163
+ break;
164
+
165
+ case "rule":
166
+ // Rule processing - delegate to FlowEngine
167
+ if (step.rule) {
168
+ await renderRuleStep(step.rule, stepIndex);
169
+ } else {
170
+ console.warn("[DAP] Rule step has no rule data");
171
+ }
172
+ break;
173
+
174
+ default:
175
+ console.warn(`[DAP] Unknown step kind: ${(step as any).kind}`);
176
+ }
177
+
178
+ currentStepState = 'rendered';
179
+
180
+ // Auto-advance for trigger-based steps after brief delay
181
+ if (!showNavigation && stepIndex < payload.steps.length - 1) {
182
+ setTimeout(() => transitionToStep(stepIndex + 1), 2000);
183
+ }
184
+ }
185
+
186
+ async function renderModalStep(step: ModalSequenceStep, showNavigation: boolean) {
187
+ modalShell = createModalShell(step.size);
188
+
189
+ // Header - always populate the title area
190
+ const header = modalShell.modal.querySelector(".dap-header-bar") as HTMLElement;
191
+ if (step.title) {
192
+ const title = document.createElement("h2");
193
+ title.className = "dap-header-text";
194
+ title.textContent = step.title;
195
+ header.insertBefore(title, header.firstChild);
196
+ } else {
197
+ // Add empty space to maintain header layout
198
+ const title = document.createElement("h2");
199
+ title.className = "dap-header-text";
200
+ title.style.visibility = "hidden";
201
+ title.innerHTML = "&nbsp;";
202
+ header.insertBefore(title, header.firstChild);
203
+ }
204
+
205
+ // Body content
206
+ const body = modalShell.modal.querySelector(".dap-modal-body") as HTMLElement;
207
+ if (step.body && Array.isArray(step.body)) {
208
+ step.body.forEach(content => {
209
+ const contentEl = renderModalContent(content, step);
210
+ if (contentEl) body.appendChild(contentEl);
211
+ });
212
+ }
213
+
214
+ // Footer - always show footer for consistent layout
215
+ const footer = modalShell.modal.querySelector(".dap-modal-footer") as HTMLElement;
216
+
217
+ if (step.footerText) {
218
+ const footerText = document.createElement("p");
219
+ footerText.className = "dap-footer-text";
220
+ footerText.innerHTML = sanitizeHtml(step.footerText);
221
+ footer.appendChild(footerText);
222
+ }
223
+
224
+ if (showNavigation && isMultiStep) {
225
+ const nav = createNavigationButtons();
226
+ footer.appendChild(nav);
227
+ }
228
+
229
+ // Add to DOM
230
+ document.documentElement.appendChild(modalShell.overlay);
231
+
232
+ // Event handlers
233
+ modalShell.overlay.addEventListener("click", (e) => {
234
+ if (e.target === modalShell!.overlay) {
235
+ closeAll();
236
+ }
237
+ });
238
+
239
+ const closeBtn = modalShell.modal.querySelector(".dap-modal-close") as HTMLButtonElement;
240
+ if (closeBtn) {
241
+ closeBtn.addEventListener("click", closeAll);
242
+ }
243
+ }
244
+
245
+ async function renderTooltipStep(tooltipPayload: TooltipPayload) {
246
+ try {
247
+ const tooltipRenderer = getRenderer("tooltip");
248
+ if (tooltipRenderer) {
249
+ await tooltipRenderer({
250
+ id: `${id}-tooltip`,
251
+ type: "tooltip",
252
+ payload: tooltipPayload
253
+ });
254
+ }
255
+ } catch (error) {
256
+ console.error("[DAP] Error rendering tooltip step:", error);
257
+ }
258
+ }
259
+
260
+ async function renderPopoverStep(popoverPayload: PopoverPayload) {
261
+ try {
262
+ const popoverRenderer = getRenderer("popover");
263
+ if (popoverRenderer) {
264
+ await popoverRenderer({
265
+ id: `${id}-popover`,
266
+ type: "popover",
267
+ payload: popoverPayload
268
+ });
269
+ }
270
+ } catch (error) {
271
+ console.error("[DAP] Error rendering popover step:", error);
272
+ }
273
+ }
274
+
275
+ async function renderRuleStep(rulePayload: any, stepIndex: number) {
276
+ console.debug("[DAP] === RULE STEP PROCESSING START ===");
277
+ console.debug("[DAP] Step Index:", stepIndex);
278
+ console.debug("[DAP] Rule Payload:", rulePayload);
279
+
280
+ try {
281
+ const { inputSelector, rules } = rulePayload;
282
+
283
+ console.debug("[DAP] Extracted inputSelector:", inputSelector);
284
+ console.debug("[DAP] Extracted rules:", rules);
285
+
286
+ if (!inputSelector || !rules || rules.length === 0) {
287
+ console.warn("[DAP] Rule step missing inputSelector or rules");
288
+ console.warn("[DAP] inputSelector:", inputSelector);
289
+ console.warn("[DAP] rules:", rules);
290
+ // Auto-advance to next step
291
+ if (stepIndex < payload.steps.length - 1) {
292
+ setTimeout(() => transitionToStep(stepIndex + 1), 100);
293
+ }
294
+ return;
295
+ }
296
+
297
+ console.debug(`[DAP] Setting up rule evaluation listener for selector: ${inputSelector}`);
298
+ console.debug(`[DAP] Number of rules: ${rules.length}`);
299
+ rules.forEach((rule: any, index: number) => {
300
+ console.debug(`[DAP] Rule ${index + 1}:`, rule);
301
+ });
302
+
303
+ // Find the target element for input monitoring
304
+ console.debug(`[DAP] Resolving selector: ${inputSelector}`);
305
+ const targetElement = resolveSelector(inputSelector);
306
+ console.debug(`[DAP] Target element found:`, targetElement);
307
+
308
+ if (!targetElement) {
309
+ console.warn(`[DAP] Could not find target element for selector: ${inputSelector}`);
310
+ console.warn(`[DAP] Available elements with similar selectors:`);
311
+ // Try to find similar elements for debugging
312
+ try {
313
+ const allInputs = document.querySelectorAll('input, select, textarea');
314
+ console.warn(`[DAP] Found ${allInputs.length} input elements on page`);
315
+ allInputs.forEach((el, i) => {
316
+ console.warn(`[DAP] Input ${i + 1}: ${el.tagName}${el.id ? '#' + el.id : ''}${el.className ? '.' + el.className.replace(/\s+/g, '.') : ''}`);
317
+ });
318
+ } catch (e) {
319
+ console.warn(`[DAP] Error listing input elements:`, e);
320
+ }
321
+
322
+ // Auto-advance to next step
323
+ if (stepIndex < payload.steps.length - 1) {
324
+ setTimeout(() => transitionToStep(stepIndex + 1), 100);
325
+ }
326
+ return;
327
+ }
328
+
329
+ console.debug(`[DAP] Creating rule evaluation listener...`);
330
+
331
+ // Create rule evaluation listener
332
+ const eventHandler = createRuleEvaluationListener(
333
+ rules,
334
+ (nextFlowId: string) => {
335
+ console.debug(`[DAP] *** RULE EVALUATION TRIGGERED ***`);
336
+ console.debug(`[DAP] Rule evaluation triggered transition to flow: ${nextFlowId}`);
337
+
338
+ // Testing alert for rule evaluation
339
+ alert(`🎯 Rule Evaluated! Next Flow: ${nextFlowId}\n\nRule evaluation is working correctly.`);
340
+
341
+ // In modal sequence context, we'll just log this for now
342
+ // In a real implementation, this would trigger flow switching
343
+ console.info(`[DAP] Would transition to flow: ${nextFlowId}`);
344
+
345
+ // Auto-advance to next step after rule match
346
+ if (stepIndex < payload.steps.length - 1) {
347
+ console.debug(`[DAP] Auto-advancing to next step: ${stepIndex + 1}`);
348
+ setTimeout(() => transitionToStep(stepIndex + 1), 100);
349
+ } else {
350
+ console.debug(`[DAP] Rule step was the last step`);
351
+ }
352
+ }
353
+ );
354
+
355
+ console.debug(`[DAP] Attaching event listeners to target element...`);
356
+
357
+ // Attach event listener to the target element
358
+ targetElement.addEventListener('input', eventHandler);
359
+ targetElement.addEventListener('change', eventHandler);
360
+
361
+ console.debug(`[DAP] Event listeners attached for 'input' and 'change' events`);
362
+
363
+ // Create cleanup function
364
+ const cleanup = () => {
365
+ console.debug(`[DAP] Cleaning up rule step event listeners`);
366
+ targetElement.removeEventListener('input', eventHandler);
367
+ targetElement.removeEventListener('change', eventHandler);
368
+ };
369
+
370
+ // Store cleanup function for later
371
+ currentTriggerListeners.push(cleanup);
372
+
373
+ console.debug("[DAP] Rule step listener active - waiting for user input");
374
+ console.debug(`[DAP] Target element type: ${targetElement.tagName}, value: "${(targetElement as HTMLInputElement).value || ''}"`);
375
+ console.debug("[DAP] === RULE STEP PROCESSING COMPLETE ===");
376
+
377
+ // Testing alert to confirm rule step setup
378
+ alert(`🔍 Rule Step Setup Complete!\n\nMonitoring element: ${inputSelector}\nRules: ${rules.length}\n\nTry interacting with the target element.`);
379
+
380
+ } catch (error) {
381
+ console.error("[DAP] Error setting up rule step:", error);
382
+ if (error instanceof Error) {
383
+ console.error("[DAP] Stack trace:", error.stack);
384
+ }
385
+
386
+ // On error, try to continue to next step
387
+ if (stepIndex < payload.steps.length - 1) {
388
+ setTimeout(() => transitionToStep(stepIndex + 1), 100);
389
+ }
390
+ }
391
+ }
392
+
393
+ function createNavigationButtons(): HTMLElement {
394
+ const nav = document.createElement("div");
395
+ nav.className = "dap-modal-nav";
396
+
397
+ // Previous button
398
+ const prevBtn = document.createElement("button");
399
+ prevBtn.className = "dap-btn dap-btn-secondary";
400
+ prevBtn.textContent = "Previous";
401
+ prevBtn.disabled = currentStepIndex === 0;
402
+ prevBtn.addEventListener("click", goToPrevious);
403
+
404
+ // Step indicator
405
+ const indicator = document.createElement("span");
406
+ indicator.className = "dap-step-indicator";
407
+ indicator.textContent = `${currentStepIndex + 1} of ${payload.steps.length}`;
408
+
409
+ // Next button
410
+ const nextBtn = document.createElement("button");
411
+ nextBtn.className = "dap-btn dap-btn-primary";
412
+ nextBtn.textContent = currentStepIndex === payload.steps.length - 1 ? "Complete" : "Next";
413
+ nextBtn.addEventListener("click", goToNext);
414
+
415
+ nav.appendChild(prevBtn);
416
+ nav.appendChild(indicator);
417
+ nav.appendChild(nextBtn);
418
+
419
+ return nav;
420
+ }
421
+
422
+ function goToPrevious() {
423
+ if (currentStepIndex > 0) {
424
+ transitionToStep(currentStepIndex - 1);
425
+ }
426
+ }
427
+
428
+ function goToNext() {
429
+ if (currentStepIndex < payload.steps.length - 1) {
430
+ transitionToStep(currentStepIndex + 1);
431
+ } else {
432
+ closeAll();
433
+ }
434
+ }
435
+
436
+ function transitionToStep(newStepIndex: number) {
437
+ console.debug(`[DAP] Transitioning from step ${currentStepIndex} to step ${newStepIndex}`);
438
+
439
+ // Clean up current step
440
+ cleanupCurrentStep();
441
+
442
+ // Update state
443
+ const oldStepIndex = currentStepIndex;
444
+ currentStepIndex = newStepIndex;
445
+ activeStepTriggered = false;
446
+ currentStepState = 'idle';
447
+
448
+ // Notify step advancement
449
+ if (completionTracker?.onStepAdvance) {
450
+ const newStepId = payload.steps[newStepIndex]?.stepId || `step-${newStepIndex + 1}`;
451
+ completionTracker.onStepAdvance(newStepId);
452
+ }
453
+
454
+ // Render new step
455
+ evaluateAndRenderStep(currentStepIndex);
456
+ }
457
+
458
+ function cleanupCurrentStep() {
459
+ console.debug(`[DAP] Cleaning up step ${currentStepIndex}`);
460
+
461
+ // Remove trigger listeners
462
+ currentTriggerListeners.forEach(cleanup => cleanup());
463
+ currentTriggerListeners = [];
464
+
465
+ // Clean up UI
466
+ if (modalShell) {
467
+ modalShell.overlay.remove();
468
+ modalShell = null;
469
+ }
470
+
471
+ tooltipCleanup?.();
472
+ tooltipCleanup = null;
473
+
474
+ popoverCleanup?.();
475
+ popoverCleanup = null;
476
+
477
+ // Reset state
478
+ currentStepState = 'idle';
479
+ activeStepTriggered = false;
480
+ }
481
+
482
+ function closeAll() {
483
+ console.debug("[DAP] Closing modal sequence");
484
+
485
+ cleanupCurrentStep();
486
+ document.removeEventListener("keydown", onKeyDown, true);
487
+ prevActive?.focus();
488
+
489
+ // Signal completion
490
+ if (completionTracker?.onComplete) {
491
+ console.debug(`[DAP] Completing modal sequence flow: ${id}`);
492
+ completionTracker.onComplete();
493
+ }
494
+ }
495
+ }
496
+
497
+ function createModalShell(size?: string) {
498
+ const overlay = document.createElement("div");
499
+ overlay.className = "dap-modal-wrap";
500
+
501
+ const modal = document.createElement("div");
502
+ modal.className = "dap-modal";
503
+
504
+ // Apply size class if specified
505
+ if (size) {
506
+ modal.classList.add(`dap-modal-${size}`);
507
+ } else {
508
+ modal.classList.add("dap-modal-medium"); // Default to medium size
509
+ }
510
+
511
+ // Header
512
+ const header = document.createElement("div");
513
+ header.className = "dap-header-bar";
514
+
515
+ // Close button
516
+ const closeBtn = document.createElement("button");
517
+ closeBtn.className = "dap-modal-close";
518
+ closeBtn.setAttribute("aria-label", "Close modal");
519
+ closeBtn.innerHTML = "×";
520
+ header.appendChild(closeBtn);
521
+
522
+ // Body
523
+ const body = document.createElement("div");
524
+ body.className = "dap-modal-body";
525
+
526
+ // Footer
527
+ const footer = document.createElement("div");
528
+ footer.className = "dap-modal-footer";
529
+
530
+ // Assemble
531
+ modal.appendChild(header);
532
+ modal.appendChild(body);
533
+ modal.appendChild(footer);
534
+ overlay.appendChild(modal);
535
+
536
+ // Accessibility
537
+ modal.setAttribute("role", "dialog");
538
+ modal.setAttribute("aria-modal", "true");
539
+
540
+ return { overlay, modal, header, body, footer };
541
+ }
542
+
543
+ function renderModalContent(content: ModalContent, step?: any): HTMLElement | null {
544
+ switch (content.kind) {
545
+ case "text":
546
+ const textEl = document.createElement("div");
547
+ textEl.className = "dap-content-text";
548
+ textEl.innerHTML = sanitizeHtml(content.html);
549
+ return textEl;
550
+
551
+ case "link":
552
+ const linkEl = document.createElement("a");
553
+ linkEl.className = "dap-content-link";
554
+ linkEl.href = content.href;
555
+ linkEl.textContent = content.label || content.href;
556
+ linkEl.target = "_blank";
557
+ linkEl.rel = "noopener noreferrer";
558
+ return linkEl;
559
+
560
+ case "image":
561
+ const imgEl = document.createElement("img");
562
+ imgEl.className = "dap-content-image";
563
+ imgEl.src = content.url;
564
+ imgEl.alt = content.alt || "";
565
+ return imgEl;
566
+
567
+ case "video":
568
+ if (content.sources && content.sources.length > 0) {
569
+ const videoEl = document.createElement("video");
570
+ videoEl.className = "dap-content-video";
571
+ videoEl.controls = true;
572
+
573
+ content.sources.forEach(source => {
574
+ const sourceEl = document.createElement("source");
575
+ sourceEl.src = source.src;
576
+ if (source.type) sourceEl.type = source.type;
577
+ videoEl.appendChild(sourceEl);
578
+ });
579
+
580
+ return videoEl;
581
+ }
582
+ return null;
583
+
584
+ case "youtube":
585
+ const iframeEl = document.createElement("iframe");
586
+ iframeEl.className = "dap-content-youtube";
587
+ iframeEl.src = content.href;
588
+ iframeEl.setAttribute("frameborder", "0");
589
+ iframeEl.setAttribute("allowfullscreen", "true");
590
+ return iframeEl;
591
+
592
+ case "kb":
593
+ // Knowledge base rendering with in-modal viewing
594
+ console.debug("[DAP] Rendering KB content:", content);
595
+ // Store current step and KB data for navigation
596
+ currentSequenceStep = step;
597
+ currentSequenceKBData = content;
598
+ return renderKnowledgeBaseInSequence(content);
599
+
600
+ case "kb-item-viewer":
601
+ // Knowledge base item viewer for modal sequence
602
+ console.debug("[DAP] Rendering KB item viewer:", content);
603
+ return renderKBItemViewerInSequence(content);
604
+
605
+ case "article":
606
+ console.debug("[DAP] Rendering Article in sequence");
607
+ return createArticleViewerInSequence(content);
608
+
609
+ default:
610
+ return null;
611
+ }
612
+ }
613
+
614
+ async function waitForElement(selector: string, timeout = 5000): Promise<Element | null> {
615
+ // Check if element already exists
616
+ const existingElement = resolveSelector(selector);
617
+ if (existingElement) return existingElement;
618
+
619
+ // Wait for element to appear
620
+ return new Promise((resolve) => {
621
+ let timeoutId: number;
622
+ let observer: MutationObserver;
623
+
624
+ timeoutId = window.setTimeout(() => {
625
+ observer?.disconnect();
626
+ resolve(null);
627
+ }, timeout);
628
+
629
+ observer = new MutationObserver(() => {
630
+ const element = resolveSelector(selector);
631
+ if (element) {
632
+ clearTimeout(timeoutId);
633
+ observer.disconnect();
634
+ resolve(element);
635
+ }
636
+ });
637
+
638
+ observer.observe(document.body, {
639
+ childList: true,
640
+ subtree: true
641
+ });
642
+ });
643
+ }
644
+
645
+ function normalizeTrigger(trigger: string): string {
646
+ switch (trigger?.toLowerCase()) {
647
+ case "hover":
648
+ return "mouseenter";
649
+ case "focus":
650
+ return "focus";
651
+ case "click":
652
+ default:
653
+ return "click";
654
+ }
655
+ }
656
+
657
+ function ensureStyles() {
658
+ if (!document.getElementById("dap-modal-sequence-style")) {
659
+ const style = document.createElement("style");
660
+ style.id = "dap-modal-sequence-style";
661
+ style.textContent = modalCssText + `
662
+ .dap-modal-nav {
663
+ display: flex;
664
+ justify-content: space-between;
665
+ align-items: center;
666
+ gap: 12px;
667
+ margin-top: 16px;
668
+ }
669
+
670
+ .dap-step-indicator {
671
+ font-size: 14px;
672
+ color: var(--dap-text-muted);
673
+ }
674
+
675
+ .dap-btn {
676
+ padding: 8px 16px;
677
+ border: 1px solid var(--dap-border);
678
+ border-radius: 6px;
679
+ font-size: 14px;
680
+ cursor: pointer;
681
+ transition: all 0.15s ease;
682
+ }
683
+
684
+ .dap-btn-primary {
685
+ background: var(--dap-primary);
686
+ color: white;
687
+ border-color: var(--dap-primary);
688
+ }
689
+
690
+ .dap-btn-secondary {
691
+ background: transparent;
692
+ color: var(--dap-text);
693
+ }
694
+
695
+ .dap-btn:disabled {
696
+ opacity: 0.5;
697
+ cursor: not-allowed;
698
+ }
699
+
700
+ .dap-content-kb {
701
+ margin: 16px 0;
702
+ }
703
+
704
+ .dap-kb-item {
705
+ margin: 8px 0;
706
+ padding: 8px;
707
+ border: 1px solid var(--dap-border);
708
+ border-radius: 4px;
709
+ }
710
+
711
+ .dap-kb-item a {
712
+ font-weight: 500;
713
+ color: var(--dap-primary);
714
+ text-decoration: none;
715
+ }
716
+
717
+ .dap-kb-item p {
718
+ margin: 4px 0 0;
719
+ font-size: 14px;
720
+ color: var(--dap-text-muted);
721
+ }
722
+ `;
723
+ document.head.appendChild(style);
724
+ }
725
+ }
726
+
727
+ // Knowledge Base rendering functions for modal sequences
728
+ function renderKnowledgeBaseInSequence(content: any): HTMLElement {
729
+ const container = document.createElement('div');
730
+ container.className = 'dap-content-kb';
731
+
732
+ console.debug("[DAP] Rendering KB sequence with content:", content);
733
+ console.debug("[DAP] KB items count:", content?.items?.length || 0);
734
+
735
+ if (!content.items || !Array.isArray(content.items)) {
736
+ console.warn("[DAP] No KB items available for rendering");
737
+ container.innerHTML = '<p>No knowledge base items available.</p>';
738
+ return container;
739
+ }
740
+
741
+ // Add title if available
742
+ if (content.title) {
743
+ const title = document.createElement('h3');
744
+ title.textContent = content.title;
745
+ title.className = 'dap-kb-title';
746
+ container.appendChild(title);
747
+ }
748
+
749
+ // Add description if available
750
+ if (content.description) {
751
+ const desc = document.createElement('p');
752
+ desc.textContent = content.description;
753
+ desc.className = 'dap-kb-description';
754
+ container.appendChild(desc);
755
+ }
756
+
757
+ // Create items list
758
+ const itemsList = document.createElement('div');
759
+ itemsList.className = 'dap-kb-items-list';
760
+
761
+ content.items.forEach((item: any) => {
762
+ const itemElement = document.createElement('button');
763
+ itemElement.className = 'dap-kb-item-button';
764
+ itemElement.type = 'button';
765
+
766
+ // Add icon based on content type
767
+ const icon = document.createElement('span');
768
+ icon.className = 'dap-kb-item-icon';
769
+
770
+ const type = item.type?.toLowerCase() || '';
771
+ if (type === 'video' || item.url?.includes('youtube') || item.url?.includes('vimeo')) {
772
+ icon.textContent = '▶';
773
+ } else if (type === 'image' || /\.(jpg|jpeg|png|gif|webp)$/i.test(item.url || '')) {
774
+ icon.textContent = '🖼';
775
+ } else if (type === 'pdf' || /\.pdf$/i.test(item.url || '')) {
776
+ icon.textContent = '📄';
777
+ } else if (type === 'doc' || type === 'docx' || /\.(doc|docx)$/i.test(item.url || '')) {
778
+ icon.textContent = '📝';
779
+ } else {
780
+ icon.textContent = '📰';
781
+ }
782
+
783
+ const title = document.createElement('span');
784
+ title.className = 'dap-kb-item-title';
785
+ title.textContent = item.title || 'Untitled Item';
786
+
787
+ const description = document.createElement('span');
788
+ description.className = 'dap-kb-item-description';
789
+ description.textContent = item.description || '';
790
+
791
+ itemElement.appendChild(icon);
792
+ itemElement.appendChild(title);
793
+ if (item.description) {
794
+ itemElement.appendChild(description);
795
+ }
796
+
797
+ // Add click handler to open item in modal
798
+ itemElement.addEventListener('click', () => {
799
+ openKBItemInSequence(item);
800
+ });
801
+
802
+ itemsList.appendChild(itemElement);
803
+ });
804
+
805
+ container.appendChild(itemsList);
806
+ return container;
807
+ }
808
+
809
+ function renderKBItemViewerInSequence(content: any): HTMLElement {
810
+ const container = document.createElement('div');
811
+ container.className = 'dap-kb-item-viewer';
812
+
813
+ // Add back button
814
+ const backButton = document.createElement('button');
815
+ backButton.className = 'dap-kb-back-button';
816
+ backButton.innerHTML = '← Back to Knowledge Base';
817
+ backButton.addEventListener('click', goBackToKBListInSequence);
818
+ container.appendChild(backButton);
819
+
820
+ // Add title
821
+ const title = document.createElement('h3');
822
+ title.className = 'dap-kb-item-title';
823
+ title.textContent = content.title || 'Knowledge Base Item';
824
+ container.appendChild(title);
825
+
826
+ // Add content based on type
827
+ const type = content.type?.toLowerCase() || '';
828
+ const url = content.url || '';
829
+
830
+ if (type === 'video' || url.includes('youtube.com') || url.includes('youtu.be') || url.includes('vimeo.com')) {
831
+ const videoContainer = document.createElement('div');
832
+ videoContainer.className = 'dap-kb-video-container';
833
+
834
+ if (url.includes('youtube.com') || url.includes('youtu.be')) {
835
+ // Handle YouTube URLs
836
+ let videoId = '';
837
+ if (url.includes('youtube.com/watch?v=')) {
838
+ videoId = url.split('watch?v=')[1]?.split('&')[0];
839
+ } else if (url.includes('youtu.be/')) {
840
+ videoId = url.split('youtu.be/')[1]?.split('?')[0];
841
+ }
842
+
843
+ if (videoId) {
844
+ const iframe = document.createElement('iframe');
845
+ iframe.className = 'dap-kb-video';
846
+ iframe.src = `https://www.youtube.com/embed/${videoId}`;
847
+ iframe.setAttribute('frameborder', '0');
848
+ iframe.setAttribute('allowfullscreen', '');
849
+ videoContainer.appendChild(iframe);
850
+ }
851
+ } else if (url.includes('vimeo.com')) {
852
+ // Handle Vimeo URLs
853
+ const videoId = url.split('vimeo.com/')[1]?.split('?')[0];
854
+ if (videoId) {
855
+ const iframe = document.createElement('iframe');
856
+ iframe.className = 'dap-kb-video';
857
+ iframe.src = `https://player.vimeo.com/video/${videoId}`;
858
+ iframe.setAttribute('frameborder', '0');
859
+ iframe.setAttribute('allowfullscreen', '');
860
+ videoContainer.appendChild(iframe);
861
+ }
862
+ } else {
863
+ // Direct video file
864
+ const video = document.createElement('video');
865
+ video.className = 'dap-kb-video';
866
+ video.src = url;
867
+ video.controls = true;
868
+ videoContainer.appendChild(video);
869
+ }
870
+
871
+ container.appendChild(videoContainer);
872
+ } else if (type === 'image' || /\.(jpg|jpeg|png|gif|webp)$/i.test(url)) {
873
+ const img = document.createElement('img');
874
+ img.className = 'dap-kb-image';
875
+ img.src = url;
876
+ img.alt = content.title || 'Knowledge Base Image';
877
+ container.appendChild(img);
878
+ } else if (type === 'pdf' || /\.pdf$/i.test(url)) {
879
+ const pdfContainer = document.createElement('div');
880
+ pdfContainer.className = 'dap-kb-pdf-container';
881
+
882
+ const iframe = document.createElement('iframe');
883
+ iframe.className = 'dap-kb-pdf-iframe';
884
+ iframe.src = url;
885
+ iframe.setAttribute('frameborder', '0');
886
+
887
+ pdfContainer.appendChild(iframe);
888
+ container.appendChild(pdfContainer);
889
+ } else if (type === 'doc' || type === 'docx' || /\.(doc|docx)$/i.test(url)) {
890
+ const docContainer = document.createElement('div');
891
+ docContainer.className = 'dap-kb-document-container';
892
+
893
+ // For Office documents, we can try Office Online viewer or show download link
894
+ const iframe = document.createElement('iframe');
895
+ iframe.className = 'dap-kb-document-iframe';
896
+ iframe.src = `https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(url)}`;
897
+ iframe.setAttribute('frameborder', '0');
898
+
899
+ docContainer.appendChild(iframe);
900
+ container.appendChild(docContainer);
901
+ } else {
902
+ // Article or other content - use enhanced article viewer
903
+ const mimeType = content.mime || content.mimeType || null;
904
+
905
+ // Determine if this is a document-based article
906
+ const isDocumentArticle = mimeType && (
907
+ mimeType === 'application/pdf' ||
908
+ mimeType.includes('word') ||
909
+ mimeType.includes('msword') ||
910
+ mimeType.includes('presentation') ||
911
+ mimeType.includes('powerpoint') ||
912
+ /\.(pdf|doc|docx|ppt|pptx)$/i.test(url)
913
+ );
914
+
915
+ if (isDocumentArticle) {
916
+ console.debug("[DAP] Rendering document-based article with enhanced viewer");
917
+ // Use the enhanced article viewer for document content
918
+ const articleViewer = createArticleViewerInSequence(content);
919
+ container.appendChild(articleViewer);
920
+ } else {
921
+ // Traditional article content (HTML/text)
922
+ const articleContainer = document.createElement('div');
923
+ articleContainer.className = 'dap-kb-article-container';
924
+
925
+ if (content.content) {
926
+ articleContainer.innerHTML = content.content;
927
+ } else if (url) {
928
+ // Show enhanced fallback for external content
929
+ const fallback = createSequenceFallbackViewer(url, content.fileName || 'External Article',
930
+ 'This content is available at an external link.');
931
+ articleContainer.appendChild(fallback);
932
+ }
933
+
934
+ container.appendChild(articleContainer);
935
+ }
936
+ }
937
+
938
+ // Add description if available
939
+ if (content.description) {
940
+ const desc = document.createElement('p');
941
+ desc.className = 'dap-kb-item-description';
942
+ desc.textContent = content.description;
943
+ container.appendChild(desc);
944
+ }
945
+
946
+ return container;
947
+ }
948
+
949
+ // Global variables to track KB state in sequences
950
+ let currentSequenceKBData: any = null;
951
+ let currentSequenceStep: any = null;
952
+
953
+ function openKBItemInSequence(item: any) {
954
+ console.debug("[DAP] Opening KB item in sequence:", item);
955
+ console.debug("[DAP] KB view changed to item");
956
+
957
+ // Find the current modal and update its content
958
+ const modal = document.querySelector('.dap-modal-content');
959
+ if (modal && currentSequenceStep) {
960
+ console.debug("[DAP] KB items count:", currentSequenceKBData?.items?.length || 0);
961
+
962
+ // Update the step to show item viewer
963
+ const updatedStep = {
964
+ ...currentSequenceStep,
965
+ content: {
966
+ type: 'kb-item-viewer',
967
+ ...item
968
+ }
969
+ };
970
+
971
+ // Re-render the modal content
972
+ const contentElement = modal.querySelector('.dap-content');
973
+ if (contentElement) {
974
+ const newContent = renderKBItemViewerInSequence(item);
975
+ contentElement.innerHTML = '';
976
+ contentElement.appendChild(newContent);
977
+ }
978
+ }
979
+ }
980
+
981
+ function goBackToKBListInSequence() {
982
+ console.debug("[DAP] Going back to KB list in sequence");
983
+ console.debug("[DAP] KB view changed to list");
984
+
985
+ // Find the current modal and restore KB list view
986
+ const modal = document.querySelector('.dap-modal-content');
987
+ if (modal && currentSequenceKBData && currentSequenceStep) {
988
+ console.debug("[DAP] KB items count:", currentSequenceKBData?.items?.length || 0);
989
+
990
+ // Restore the original KB content
991
+ const updatedStep = {
992
+ ...currentSequenceStep,
993
+ content: {
994
+ type: 'kb',
995
+ ...currentSequenceKBData
996
+ }
997
+ };
998
+
999
+ // Re-render the modal content
1000
+ const contentElement = modal.querySelector('.dap-content');
1001
+ if (contentElement) {
1002
+ const newContent = renderKnowledgeBaseInSequence(currentSequenceKBData);
1003
+ contentElement.innerHTML = '';
1004
+ contentElement.appendChild(newContent);
1005
+ }
1006
+ } else {
1007
+ console.error("[DAP] Cannot go back: missing modal, KB data, or step");
1008
+ }
1009
+ }
1010
+
1011
+ // Article Viewer for Modal Sequences - matches the standard DAP Article behavior
1012
+ function createArticleViewerInSequence(content: any): HTMLElement {
1013
+ console.debug("[DAP] Creating Article viewer in sequence");
1014
+
1015
+ const container = document.createElement("div");
1016
+ container.className = "dap-content-article";
1017
+
1018
+ const url = content.url || content.presignedUrl || "";
1019
+ const fileName = content.fileName || "Document";
1020
+ const title = content.title || fileName;
1021
+ const mimeType = content.mime || content.mimeType || null;
1022
+
1023
+ console.debug("[DAP] Article URL:", url);
1024
+ console.debug("[DAP] Article MIME:", mimeType);
1025
+
1026
+ if (!url) {
1027
+ console.error("[DAP] No URL provided for Article content");
1028
+ container.innerHTML = `
1029
+ <div class="dap-fallback-viewer">
1030
+ <p><strong>No document URL provided</strong></p>
1031
+ <p>Unable to display article content.</p>
1032
+ </div>
1033
+ `;
1034
+ return container;
1035
+ }
1036
+
1037
+ // Determine viewer type based on MIME type and file extension
1038
+ const viewerType = resolveArticleViewerType(url, mimeType, fileName);
1039
+ console.debug("[DAP] Selected viewer type:", viewerType);
1040
+
1041
+ // Add title
1042
+ const titleEl = document.createElement("h4");
1043
+ titleEl.className = "dap-article-title";
1044
+ titleEl.textContent = title;
1045
+ container.appendChild(titleEl);
1046
+
1047
+ // Create appropriate viewer
1048
+ switch (viewerType) {
1049
+ case "pdf":
1050
+ const pdfViewer = createSequencePDFViewer(url, fileName);
1051
+ container.appendChild(pdfViewer);
1052
+ break;
1053
+
1054
+ case "document":
1055
+ const docViewer = createSequenceDocumentViewer(url, fileName, mimeType);
1056
+ container.appendChild(docViewer);
1057
+ break;
1058
+
1059
+ case "presentation":
1060
+ const pptViewer = createSequencePresentationViewer(url, fileName, mimeType);
1061
+ container.appendChild(pptViewer);
1062
+ break;
1063
+
1064
+ case "fallback":
1065
+ default:
1066
+ console.debug("[DAP] Fallback activated for sequence");
1067
+ const fallbackViewer = createSequenceFallbackViewer(url, fileName, "This document cannot be previewed inline.");
1068
+ container.appendChild(fallbackViewer);
1069
+ break;
1070
+ }
1071
+
1072
+ return container;
1073
+ }
1074
+
1075
+ // Article viewer type resolver for sequences
1076
+ function resolveArticleViewerType(url: string, mimeType: string | null, fileName: string): string {
1077
+ console.debug("[DAP] Resolving Article viewer type");
1078
+ console.debug("[DAP] MIME type:", mimeType || "none");
1079
+
1080
+ // Check MIME type first
1081
+ if (mimeType) {
1082
+ if (mimeType === "application/pdf") {
1083
+ return "pdf";
1084
+ }
1085
+ if (mimeType.includes("word") || mimeType.includes("msword") ||
1086
+ mimeType === "application/vnd.openxmlformats-officedocument.wordprocessingml.document") {
1087
+ return "document";
1088
+ }
1089
+ if (mimeType.includes("presentation") || mimeType.includes("powerpoint") ||
1090
+ mimeType === "application/vnd.openxmlformats-officedocument.presentationml.presentation") {
1091
+ return "presentation";
1092
+ }
1093
+ }
1094
+
1095
+ // Fallback to file extension
1096
+ const urlLower = url.toLowerCase();
1097
+ const fileNameLower = fileName.toLowerCase();
1098
+
1099
+ if (urlLower.includes(".pdf") || fileNameLower.endsWith(".pdf")) {
1100
+ return "pdf";
1101
+ }
1102
+ if (urlLower.match(/\.(doc|docx)/) || fileNameLower.match(/\.(doc|docx)$/)) {
1103
+ return "document";
1104
+ }
1105
+ if (urlLower.match(/\.(ppt|pptx)/) || fileNameLower.match(/\.(ppt|pptx)$/)) {
1106
+ return "presentation";
1107
+ }
1108
+
1109
+ return "fallback";
1110
+ }
1111
+
1112
+ // PDF viewer for sequences
1113
+ function createSequencePDFViewer(url: string, fileName: string): HTMLElement {
1114
+ const container = document.createElement("div");
1115
+ container.className = "dap-pdf-viewer-container";
1116
+
1117
+ const iframe = document.createElement("iframe");
1118
+ iframe.className = "dap-pdf-iframe";
1119
+ iframe.src = url;
1120
+ iframe.style.width = "100%";
1121
+ iframe.style.height = "400px";
1122
+ iframe.style.border = "1px solid #ddd";
1123
+ iframe.setAttribute("frameborder", "0");
1124
+
1125
+ container.appendChild(iframe);
1126
+
1127
+ // Add action buttons
1128
+ const actions = createSequenceDocumentActions(url, fileName);
1129
+ container.appendChild(actions);
1130
+
1131
+ return container;
1132
+ }
1133
+
1134
+ // Document viewer for sequences
1135
+ function createSequenceDocumentViewer(url: string, fileName: string, mimeType?: string | null): HTMLElement {
1136
+ const container = document.createElement("div");
1137
+ container.className = "dap-document-viewer-container";
1138
+
1139
+ // Try Office Online viewer for supported document types
1140
+ if (mimeType && (mimeType.includes("word") || mimeType.includes("msword"))) {
1141
+ const iframe = document.createElement("iframe");
1142
+ iframe.className = "dap-document-iframe";
1143
+ iframe.src = `https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(url)}`;
1144
+ iframe.style.width = "100%";
1145
+ iframe.style.height = "400px";
1146
+ iframe.style.border = "1px solid #ddd";
1147
+ iframe.setAttribute("frameborder", "0");
1148
+
1149
+ container.appendChild(iframe);
1150
+ } else {
1151
+ // Show fallback for unsupported document types
1152
+ const fallback = createSequenceFallbackViewer(url, fileName, "Document preview is not supported for this file type.");
1153
+ container.appendChild(fallback);
1154
+ }
1155
+
1156
+ // Add action buttons
1157
+ const actions = createSequenceDocumentActions(url, fileName);
1158
+ container.appendChild(actions);
1159
+
1160
+ return container;
1161
+ }
1162
+
1163
+ // Presentation viewer for sequences
1164
+ function createSequencePresentationViewer(url: string, fileName: string, mimeType?: string | null): HTMLElement {
1165
+ const container = document.createElement("div");
1166
+ container.className = "dap-presentation-viewer-container";
1167
+
1168
+ // Try Office Online viewer for presentations
1169
+ if (mimeType && mimeType.includes("presentation")) {
1170
+ const iframe = document.createElement("iframe");
1171
+ iframe.className = "dap-presentation-iframe";
1172
+ iframe.src = `https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(url)}`;
1173
+ iframe.style.width = "100%";
1174
+ iframe.style.height = "400px";
1175
+ iframe.style.border = "1px solid #ddd";
1176
+ iframe.setAttribute("frameborder", "0");
1177
+
1178
+ container.appendChild(iframe);
1179
+ } else {
1180
+ // Show fallback for unsupported presentation types
1181
+ const fallback = createSequenceFallbackViewer(url, fileName, "Presentation preview is not supported for this file type.");
1182
+ container.appendChild(fallback);
1183
+ }
1184
+
1185
+ // Add action buttons
1186
+ const actions = createSequenceDocumentActions(url, fileName);
1187
+ container.appendChild(actions);
1188
+
1189
+ return container;
1190
+ }
1191
+
1192
+ // Fallback viewer for sequences
1193
+ function createSequenceFallbackViewer(url: string, fileName: string, message: string): HTMLElement {
1194
+ const container = document.createElement("div");
1195
+ container.className = "dap-fallback-viewer";
1196
+
1197
+ container.innerHTML = `
1198
+ <div class="dap-fallback-message">
1199
+ <p><strong>${message}</strong></p>
1200
+ <p>File: ${fileName}</p>
1201
+ </div>
1202
+ `;
1203
+
1204
+ // Add action buttons
1205
+ const actions = createSequenceDocumentActions(url, fileName);
1206
+ container.appendChild(actions);
1207
+
1208
+ return container;
1209
+ }
1210
+
1211
+ // Document action buttons for sequences
1212
+ function createSequenceDocumentActions(url: string, fileName: string): HTMLElement {
1213
+ const actions = document.createElement("div");
1214
+ actions.className = "dap-document-actions";
1215
+
1216
+ // Download button
1217
+ const downloadBtn = document.createElement("button");
1218
+ downloadBtn.className = "dap-action-btn dap-download-btn";
1219
+ downloadBtn.textContent = "Download";
1220
+ downloadBtn.addEventListener("click", (e) => {
1221
+ e.preventDefault();
1222
+ e.stopPropagation();
1223
+
1224
+ const link = document.createElement("a");
1225
+ link.href = url;
1226
+ link.download = fileName || "download";
1227
+ link.target = "_blank";
1228
+ document.body.appendChild(link);
1229
+ link.click();
1230
+ document.body.removeChild(link);
1231
+ });
1232
+
1233
+ // Open in new tab button
1234
+ const openBtn = document.createElement("button");
1235
+ openBtn.className = "dap-action-btn dap-open-btn";
1236
+ openBtn.textContent = "Open in New Tab";
1237
+ openBtn.addEventListener("click", (e) => {
1238
+ e.preventDefault();
1239
+ e.stopPropagation();
1240
+ window.open(url, "_blank", "noopener,noreferrer");
1241
+ });
1242
+
1243
+ actions.appendChild(downloadBtn);
1244
+ actions.appendChild(openBtn);
1245
+
1246
+ return actions;
1247
+ }