@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,625 @@
1
+ // src/experiences/taskList.ts
2
+ // Task list/checklist experience renderer
3
+
4
+ import { sanitizeHtml } from "../utils/sanitize";
5
+ import { register } from "./registry";
6
+ import type { TaskListPayload, TaskItem } from "./types";
7
+
8
+ type TaskListFlow = { id: string; type: "taskList"; payload: TaskListPayload };
9
+
10
+ const taskListCssText = `
11
+ :root {
12
+ --dap-z-tasklist: 2147483625;
13
+ --dap-tasklist-bg: #ffffff;
14
+ --dap-tasklist-border: #e2e8f0;
15
+ --dap-tasklist-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
16
+ --dap-tasklist-text: #1e293b;
17
+ --dap-tasklist-text-muted: #64748b;
18
+ --dap-tasklist-primary: #3b82f6;
19
+ --dap-tasklist-success: #10b981;
20
+ --dap-tasklist-overlay: rgba(15, 23, 42, 0.3);
21
+ }
22
+
23
+ .dap-tasklist-overlay {
24
+ position: fixed;
25
+ inset: 0;
26
+ background: var(--dap-tasklist-overlay);
27
+ z-index: var(--dap-z-tasklist);
28
+ display: flex;
29
+ align-items: center;
30
+ justify-content: center;
31
+ padding: 20px;
32
+ opacity: 0;
33
+ animation: tasklistOverlayFadeIn 0.3s ease-out forwards;
34
+ }
35
+
36
+ @keyframes tasklistOverlayFadeIn {
37
+ to { opacity: 1; }
38
+ }
39
+
40
+ .dap-tasklist-modal {
41
+ background: var(--dap-tasklist-bg);
42
+ border: 1px solid var(--dap-tasklist-border);
43
+ border-radius: 12px;
44
+ box-shadow: var(--dap-tasklist-shadow);
45
+ width: 100%;
46
+ max-width: 480px;
47
+ max-height: 80vh;
48
+ display: flex;
49
+ flex-direction: column;
50
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
51
+ opacity: 0;
52
+ transform: scale(0.95);
53
+ animation: tasklistModalIn 0.3s ease-out 0.1s forwards;
54
+ }
55
+
56
+ @keyframes tasklistModalIn {
57
+ to {
58
+ opacity: 1;
59
+ transform: scale(1);
60
+ }
61
+ }
62
+
63
+ .dap-tasklist-header {
64
+ padding: 20px 20px 16px;
65
+ border-bottom: 1px solid var(--dap-tasklist-border);
66
+ }
67
+
68
+ .dap-tasklist-title {
69
+ font-size: 20px;
70
+ font-weight: 600;
71
+ color: var(--dap-tasklist-text);
72
+ margin: 0 0 8px 0;
73
+ }
74
+
75
+ .dap-tasklist-description {
76
+ font-size: 14px;
77
+ color: var(--dap-tasklist-text-muted);
78
+ line-height: 1.5;
79
+ margin: 0;
80
+ }
81
+
82
+ .dap-tasklist-progress {
83
+ padding: 16px 20px;
84
+ border-bottom: 1px solid var(--dap-tasklist-border);
85
+ background: #f8fafc;
86
+ }
87
+
88
+ .dap-tasklist-progress-text {
89
+ font-size: 14px;
90
+ color: var(--dap-tasklist-text);
91
+ margin-bottom: 8px;
92
+ font-weight: 500;
93
+ }
94
+
95
+ .dap-tasklist-progress-bar {
96
+ width: 100%;
97
+ height: 6px;
98
+ background: var(--dap-tasklist-border);
99
+ border-radius: 3px;
100
+ overflow: hidden;
101
+ }
102
+
103
+ .dap-tasklist-progress-fill {
104
+ height: 100%;
105
+ background: var(--dap-tasklist-success);
106
+ border-radius: 3px;
107
+ transition: width 0.4s ease;
108
+ width: 0%;
109
+ }
110
+
111
+ .dap-tasklist-body {
112
+ flex: 1;
113
+ overflow-y: auto;
114
+ padding: 16px 20px;
115
+ }
116
+
117
+ .dap-tasklist-item {
118
+ display: flex;
119
+ align-items: flex-start;
120
+ gap: 12px;
121
+ padding: 12px 0;
122
+ border-bottom: 1px solid #f1f5f9;
123
+ transition: background-color 0.15s ease;
124
+ }
125
+
126
+ .dap-tasklist-item:last-child {
127
+ border-bottom: none;
128
+ }
129
+
130
+ .dap-tasklist-item:hover {
131
+ background: #f8fafc;
132
+ margin: 0 -20px;
133
+ padding: 12px 20px;
134
+ border-radius: 6px;
135
+ border-bottom: none;
136
+ }
137
+
138
+ .dap-tasklist-item.completed {
139
+ opacity: 0.7;
140
+ }
141
+
142
+ .dap-tasklist-checkbox {
143
+ width: 20px;
144
+ height: 20px;
145
+ border: 2px solid var(--dap-tasklist-border);
146
+ border-radius: 4px;
147
+ display: flex;
148
+ align-items: center;
149
+ justify-content: center;
150
+ cursor: pointer;
151
+ transition: all 0.15s ease;
152
+ flex-shrink: 0;
153
+ margin-top: 2px;
154
+ }
155
+
156
+ .dap-tasklist-checkbox:hover {
157
+ border-color: var(--dap-tasklist-primary);
158
+ }
159
+
160
+ .dap-tasklist-checkbox.checked {
161
+ background: var(--dap-tasklist-success);
162
+ border-color: var(--dap-tasklist-success);
163
+ color: white;
164
+ }
165
+
166
+ .dap-tasklist-checkbox.required {
167
+ border-color: #f59e0b;
168
+ }
169
+
170
+ .dap-tasklist-checkbox.required.checked {
171
+ background: var(--dap-tasklist-success);
172
+ border-color: var(--dap-tasklist-success);
173
+ }
174
+
175
+ .dap-tasklist-content {
176
+ flex: 1;
177
+ min-width: 0;
178
+ }
179
+
180
+ .dap-tasklist-item-title {
181
+ font-size: 15px;
182
+ font-weight: 500;
183
+ color: var(--dap-tasklist-text);
184
+ margin: 0 0 4px 0;
185
+ line-height: 1.4;
186
+ }
187
+
188
+ .dap-tasklist-item.completed .dap-tasklist-item-title {
189
+ text-decoration: line-through;
190
+ color: var(--dap-tasklist-text-muted);
191
+ }
192
+
193
+ .dap-tasklist-item-description {
194
+ font-size: 13px;
195
+ color: var(--dap-tasklist-text-muted);
196
+ line-height: 1.4;
197
+ margin: 0;
198
+ }
199
+
200
+ .dap-tasklist-required-badge {
201
+ display: inline-block;
202
+ background: #fef3c7;
203
+ color: #92400e;
204
+ font-size: 10px;
205
+ font-weight: 600;
206
+ padding: 2px 6px;
207
+ border-radius: 10px;
208
+ margin-left: 8px;
209
+ text-transform: uppercase;
210
+ letter-spacing: 0.5px;
211
+ }
212
+
213
+ .dap-tasklist-footer {
214
+ padding: 16px 20px;
215
+ border-top: 1px solid var(--dap-tasklist-border);
216
+ display: flex;
217
+ justify-content: space-between;
218
+ align-items: center;
219
+ gap: 12px;
220
+ }
221
+
222
+ .dap-tasklist-actions {
223
+ display: flex;
224
+ gap: 8px;
225
+ }
226
+
227
+ .dap-tasklist-btn {
228
+ padding: 8px 16px;
229
+ border: 1px solid var(--dap-tasklist-border);
230
+ border-radius: 6px;
231
+ background: transparent;
232
+ color: var(--dap-tasklist-text);
233
+ font-size: 14px;
234
+ font-weight: 500;
235
+ cursor: pointer;
236
+ transition: all 0.15s ease;
237
+ }
238
+
239
+ .dap-tasklist-btn:hover {
240
+ background: var(--dap-tasklist-border);
241
+ }
242
+
243
+ .dap-tasklist-btn.primary {
244
+ background: var(--dap-tasklist-primary);
245
+ border-color: var(--dap-tasklist-primary);
246
+ color: white;
247
+ }
248
+
249
+ .dap-tasklist-btn.primary:hover {
250
+ background: #2563eb;
251
+ border-color: #2563eb;
252
+ }
253
+
254
+ .dap-tasklist-btn.success {
255
+ background: var(--dap-tasklist-success);
256
+ border-color: var(--dap-tasklist-success);
257
+ color: white;
258
+ }
259
+
260
+ .dap-tasklist-btn.success:hover {
261
+ background: #059669;
262
+ border-color: #059669;
263
+ }
264
+
265
+ .dap-tasklist-btn:disabled {
266
+ opacity: 0.5;
267
+ cursor: not-allowed;
268
+ }
269
+
270
+ .dap-tasklist-completion-text {
271
+ font-size: 12px;
272
+ color: var(--dap-tasklist-text-muted);
273
+ }
274
+
275
+ @media (max-width: 640px) {
276
+ .dap-tasklist-overlay {
277
+ padding: 12px;
278
+ }
279
+
280
+ .dap-tasklist-modal {
281
+ max-width: 100%;
282
+ }
283
+
284
+ .dap-tasklist-header {
285
+ padding: 16px;
286
+ }
287
+
288
+ .dap-tasklist-body {
289
+ padding: 12px 16px;
290
+ }
291
+
292
+ .dap-tasklist-footer {
293
+ padding: 12px 16px;
294
+ flex-direction: column;
295
+ gap: 8px;
296
+ }
297
+
298
+ .dap-tasklist-actions {
299
+ width: 100%;
300
+ justify-content: space-between;
301
+ }
302
+ }
303
+ `;
304
+
305
+ export function registerTaskList() {
306
+ register("taskList", renderTaskList);
307
+ }
308
+
309
+ export async function renderTaskList(flow: TaskListFlow): Promise<void> {
310
+ const { payload, id } = flow;
311
+
312
+ // Extract completion tracker
313
+ const completionTracker = payload._completionTracker;
314
+
315
+ // Ensure CSS is injected
316
+ ensureStyles();
317
+
318
+ // Track completed tasks
319
+ const completedTasks = new Set<string>();
320
+
321
+ // Initialize with pre-completed tasks
322
+ payload.tasks.forEach(task => {
323
+ if (task.completed) {
324
+ completedTasks.add(task.id);
325
+ }
326
+ });
327
+
328
+ // Create modal
329
+ const { overlay, modal, progressEl, bodyEl, footerEl } = createTaskListModal(payload);
330
+
331
+ // Populate tasks
332
+ renderTasks();
333
+
334
+ // Update progress initially
335
+ updateProgress();
336
+
337
+ // Add to DOM
338
+ document.documentElement.appendChild(overlay);
339
+
340
+ // Focus management
341
+ modal.setAttribute("role", "dialog");
342
+ modal.setAttribute("aria-modal", "true");
343
+ modal.setAttribute("aria-labelledby", "tasklist-title");
344
+
345
+ function renderTasks() {
346
+ bodyEl.innerHTML = "";
347
+
348
+ payload.tasks.forEach(task => {
349
+ const taskEl = createTaskElement(task);
350
+ bodyEl.appendChild(taskEl);
351
+ });
352
+ }
353
+
354
+ function createTaskElement(task: TaskItem): HTMLElement {
355
+ const taskEl = document.createElement("div");
356
+ taskEl.className = `dap-tasklist-item${completedTasks.has(task.id) ? " completed" : ""}`;
357
+
358
+ // Checkbox
359
+ const checkbox = document.createElement("div");
360
+ checkbox.className = `dap-tasklist-checkbox${completedTasks.has(task.id) ? " checked" : ""}${task.required ? " required" : ""}`;
361
+ checkbox.innerHTML = completedTasks.has(task.id) ? "✓" : "";
362
+ checkbox.setAttribute("role", "checkbox");
363
+ checkbox.setAttribute("aria-checked", completedTasks.has(task.id) ? "true" : "false");
364
+
365
+ checkbox.addEventListener("click", () => {
366
+ toggleTask(task.id);
367
+ });
368
+
369
+ // Content
370
+ const content = document.createElement("div");
371
+ content.className = "dap-tasklist-content";
372
+
373
+ const title = document.createElement("h4");
374
+ title.className = "dap-tasklist-item-title";
375
+ title.textContent = task.title;
376
+
377
+ if (task.required) {
378
+ const badge = document.createElement("span");
379
+ badge.className = "dap-tasklist-required-badge";
380
+ badge.textContent = "Required";
381
+ title.appendChild(badge);
382
+ }
383
+
384
+ content.appendChild(title);
385
+
386
+ if (task.description) {
387
+ const description = document.createElement("p");
388
+ description.className = "dap-tasklist-item-description";
389
+ description.innerHTML = sanitizeHtml(task.description);
390
+ content.appendChild(description);
391
+ }
392
+
393
+ taskEl.appendChild(checkbox);
394
+ taskEl.appendChild(content);
395
+
396
+ return taskEl;
397
+ }
398
+
399
+ function toggleTask(taskId: string) {
400
+ if (completedTasks.has(taskId)) {
401
+ completedTasks.delete(taskId);
402
+ } else {
403
+ completedTasks.add(taskId);
404
+
405
+ // Trigger custom action if defined
406
+ const task = payload.tasks.find(t => t.id === taskId);
407
+ if (task?.action) {
408
+ window.dispatchEvent(new CustomEvent("dap-task-completed", {
409
+ detail: { action: task.action, taskId, taskListId: id }
410
+ }));
411
+ }
412
+ }
413
+
414
+ renderTasks();
415
+ updateProgress();
416
+ updateFooter();
417
+ }
418
+
419
+ function updateProgress() {
420
+ if (!progressEl) return;
421
+
422
+ const total = payload.tasks.length;
423
+ const completed = completedTasks.size;
424
+ const percentage = total > 0 ? (completed / total) * 100 : 0;
425
+
426
+ const progressText = progressEl.querySelector(".dap-tasklist-progress-text") as HTMLElement;
427
+ const progressFill = progressEl.querySelector(".dap-tasklist-progress-fill") as HTMLElement;
428
+
429
+ if (progressText) {
430
+ progressText.textContent = `${completed} of ${total} tasks completed`;
431
+ }
432
+ if (progressFill) {
433
+ progressFill.style.width = `${percentage}%`;
434
+ }
435
+ }
436
+
437
+ function updateFooter() {
438
+ const requiredTasks = payload.tasks.filter(t => t.required);
439
+ const completedRequired = requiredTasks.filter(t => completedTasks.has(t.id));
440
+ const allRequiredCompleted = completedRequired.length === requiredTasks.length;
441
+ const allTasksCompleted = completedTasks.size === payload.tasks.length;
442
+
443
+ const completeBtn = footerEl.querySelector(".dap-tasklist-complete") as HTMLButtonElement;
444
+ const skipBtn = footerEl.querySelector(".dap-tasklist-skip") as HTMLButtonElement;
445
+ const completionText = footerEl.querySelector(".dap-tasklist-completion-text") as HTMLElement;
446
+
447
+ // Update completion button
448
+ if (allTasksCompleted) {
449
+ completeBtn.textContent = "All Done!";
450
+ completeBtn.className = "dap-tasklist-btn success";
451
+ completeBtn.disabled = false;
452
+ } else if (allRequiredCompleted || payload.allowPartialCompletion) {
453
+ completeBtn.textContent = "Complete";
454
+ completeBtn.className = "dap-tasklist-btn primary";
455
+ completeBtn.disabled = false;
456
+ } else {
457
+ completeBtn.textContent = "Complete";
458
+ completeBtn.className = "dap-tasklist-btn primary";
459
+ completeBtn.disabled = true;
460
+ }
461
+
462
+ // Update completion text
463
+ if (requiredTasks.length > 0 && !allRequiredCompleted) {
464
+ const remaining = requiredTasks.length - completedRequired.length;
465
+ completionText.textContent = `${remaining} required task${remaining === 1 ? "" : "s"} remaining`;
466
+ } else {
467
+ completionText.textContent = "";
468
+ }
469
+ }
470
+
471
+ function completeTaskList() {
472
+ overlay.style.animation = "tasklistOverlayFadeOut 0.2s ease-in";
473
+ modal.style.animation = "tasklistModalOut 0.2s ease-in";
474
+
475
+ setTimeout(() => {
476
+ overlay.remove();
477
+
478
+ // Signal completion
479
+ if (completionTracker?.onComplete) {
480
+ console.debug(`[DAP] Completing task list flow: ${id}`);
481
+ completionTracker.onComplete();
482
+ }
483
+ }, 200);
484
+ }
485
+
486
+ // Footer button events
487
+ const completeBtn = footerEl.querySelector(".dap-tasklist-complete") as HTMLButtonElement;
488
+ const skipBtn = footerEl.querySelector(".dap-tasklist-skip") as HTMLButtonElement;
489
+
490
+ completeBtn.addEventListener("click", completeTaskList);
491
+ if (skipBtn) {
492
+ skipBtn.addEventListener("click", completeTaskList);
493
+ }
494
+
495
+ // Close on overlay click
496
+ overlay.addEventListener("click", (e) => {
497
+ if (e.target === overlay) {
498
+ completeTaskList();
499
+ }
500
+ });
501
+
502
+ // Keyboard support
503
+ document.addEventListener("keydown", handleKeyboard);
504
+
505
+ function handleKeyboard(e: KeyboardEvent) {
506
+ if (e.key === "Escape") {
507
+ completeTaskList();
508
+ document.removeEventListener("keydown", handleKeyboard);
509
+ }
510
+ }
511
+
512
+ // Add exit animations
513
+ if (!document.getElementById("dap-tasklist-exit-styles")) {
514
+ const style = document.createElement("style");
515
+ style.id = "dap-tasklist-exit-styles";
516
+ style.textContent = `
517
+ @keyframes tasklistOverlayFadeOut {
518
+ from { opacity: 1; }
519
+ to { opacity: 0; }
520
+ }
521
+ @keyframes tasklistModalOut {
522
+ from { opacity: 1; transform: scale(1); }
523
+ to { opacity: 0; transform: scale(0.95); }
524
+ }
525
+ `;
526
+ document.head.appendChild(style);
527
+ }
528
+
529
+ // Initial footer update
530
+ updateFooter();
531
+ }
532
+
533
+ function createTaskListModal(payload: TaskListPayload) {
534
+ const overlay = document.createElement("div");
535
+ overlay.className = "dap-tasklist-overlay";
536
+
537
+ const modal = document.createElement("div");
538
+ modal.className = "dap-tasklist-modal";
539
+
540
+ // Header
541
+ const header = document.createElement("div");
542
+ header.className = "dap-tasklist-header";
543
+
544
+ const title = document.createElement("h2");
545
+ title.className = "dap-tasklist-title";
546
+ title.id = "tasklist-title";
547
+ title.textContent = payload.title || "Tasks";
548
+
549
+ header.appendChild(title);
550
+
551
+ if (payload.description) {
552
+ const description = document.createElement("p");
553
+ description.className = "dap-tasklist-description";
554
+ description.innerHTML = sanitizeHtml(payload.description);
555
+ header.appendChild(description);
556
+ }
557
+
558
+ // Progress section
559
+ let progressEl: HTMLElement | null = null;
560
+ if (payload.showProgress) {
561
+ progressEl = document.createElement("div");
562
+ progressEl.className = "dap-tasklist-progress";
563
+
564
+ const progressText = document.createElement("div");
565
+ progressText.className = "dap-tasklist-progress-text";
566
+ progressText.textContent = "0 of 0 tasks completed";
567
+
568
+ const progressBar = document.createElement("div");
569
+ progressBar.className = "dap-tasklist-progress-bar";
570
+
571
+ const progressFill = document.createElement("div");
572
+ progressFill.className = "dap-tasklist-progress-fill";
573
+
574
+ progressBar.appendChild(progressFill);
575
+ progressEl.appendChild(progressText);
576
+ progressEl.appendChild(progressBar);
577
+ }
578
+
579
+ // Body
580
+ const bodyEl = document.createElement("div");
581
+ bodyEl.className = "dap-tasklist-body";
582
+
583
+ // Footer
584
+ const footerEl = document.createElement("div");
585
+ footerEl.className = "dap-tasklist-footer";
586
+
587
+ const completionText = document.createElement("div");
588
+ completionText.className = "dap-tasklist-completion-text";
589
+
590
+ const actions = document.createElement("div");
591
+ actions.className = "dap-tasklist-actions";
592
+
593
+ if (payload.allowPartialCompletion) {
594
+ const skipBtn = document.createElement("button");
595
+ skipBtn.className = "dap-tasklist-btn dap-tasklist-skip";
596
+ skipBtn.textContent = "Skip";
597
+ actions.appendChild(skipBtn);
598
+ }
599
+
600
+ const completeBtn = document.createElement("button");
601
+ completeBtn.className = "dap-tasklist-btn primary dap-tasklist-complete";
602
+ completeBtn.textContent = "Complete";
603
+ actions.appendChild(completeBtn);
604
+
605
+ footerEl.appendChild(completionText);
606
+ footerEl.appendChild(actions);
607
+
608
+ // Assemble modal
609
+ modal.appendChild(header);
610
+ if (progressEl) modal.appendChild(progressEl);
611
+ modal.appendChild(bodyEl);
612
+ modal.appendChild(footerEl);
613
+ overlay.appendChild(modal);
614
+
615
+ return { overlay, modal, progressEl, bodyEl, footerEl };
616
+ }
617
+
618
+ function ensureStyles() {
619
+ if (!document.getElementById("dap-tasklist-style")) {
620
+ const style = document.createElement("style");
621
+ style.id = "dap-tasklist-style";
622
+ style.textContent = taskListCssText;
623
+ document.head.appendChild(style);
624
+ }
625
+ }