basecoat-css 0.1.2 → 0.2.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.
package/dist/js/all.js ADDED
@@ -0,0 +1,796 @@
1
+ (() => {
2
+ const initDropdownMenu = (dropdownMenuComponent) => {
3
+ const trigger = dropdownMenuComponent.querySelector(':scope > button');
4
+ const popover = dropdownMenuComponent.querySelector(':scope > [data-popover]');
5
+ const menu = popover.querySelector('[role="menu"]');
6
+
7
+ if (!trigger || !menu || !popover) {
8
+ console.error('Dropdown menu component is missing a trigger, a menu, or a popover content element.', dropdownMenuComponent);
9
+ return;
10
+ }
11
+
12
+ let menuItems = [];
13
+ let activeIndex = -1;
14
+
15
+ const closeMenu = () => {
16
+ if (trigger.getAttribute('aria-expanded') === 'false') return;
17
+ trigger.setAttribute('aria-expanded', 'false');
18
+ trigger.removeAttribute('aria-activedescendant');
19
+ popover.setAttribute('aria-hidden', 'true');
20
+ trigger.focus();
21
+ activeIndex = -1;
22
+ };
23
+
24
+ const openMenu = () => {
25
+ trigger.setAttribute('aria-expanded', 'true');
26
+ popover.setAttribute('aria-hidden', 'false');
27
+ menuItems = Array.from(menu.querySelectorAll('[role^="menuitem"]:not([disabled])'));
28
+ if (menuItems.length > 0) {
29
+ setActiveItem(0);
30
+ }
31
+ };
32
+
33
+ const setActiveItem = (index) => {
34
+ if (activeIndex > -1 && menuItems[activeIndex]) {
35
+ menuItems[activeIndex].classList.remove('active');
36
+ }
37
+ activeIndex = index;
38
+ if (activeIndex > -1 && menuItems[activeIndex]) {
39
+ const activeItem = menuItems[activeIndex];
40
+ activeItem.classList.add('active');
41
+ trigger.setAttribute('aria-activedescendant', activeItem.id);
42
+ } else {
43
+ trigger.removeAttribute('aria-activedescendant');
44
+ }
45
+ };
46
+
47
+ trigger.addEventListener('click', () => {
48
+ const isExpanded = trigger.getAttribute('aria-expanded') === 'true';
49
+ if (isExpanded) {
50
+ closeMenu();
51
+ } else {
52
+ openMenu();
53
+ }
54
+ });
55
+
56
+ dropdownMenuComponent.addEventListener('keydown', (e) => {
57
+ const isExpanded = trigger.getAttribute('aria-expanded') === 'true';
58
+
59
+ if (e.key === 'Escape') {
60
+ if (isExpanded) closeMenu();
61
+ return;
62
+ }
63
+
64
+ if (!isExpanded) {
65
+ if (['ArrowDown', 'ArrowUp', 'Enter', ' '].includes(e.key)) {
66
+ e.preventDefault();
67
+ openMenu();
68
+ }
69
+ return;
70
+ }
71
+
72
+ if (menuItems.length === 0) return;
73
+
74
+ let nextIndex = activeIndex;
75
+
76
+ switch (e.key) {
77
+ case 'ArrowDown':
78
+ e.preventDefault();
79
+ nextIndex = activeIndex < menuItems.length - 1 ? activeIndex + 1 : 0;
80
+ break;
81
+ case 'ArrowUp':
82
+ e.preventDefault();
83
+ nextIndex = activeIndex > 0 ? activeIndex - 1 : menuItems.length - 1;
84
+ break;
85
+ case 'Home':
86
+ e.preventDefault();
87
+ nextIndex = 0;
88
+ break;
89
+ case 'End':
90
+ e.preventDefault();
91
+ nextIndex = menuItems.length - 1;
92
+ break;
93
+ case 'Enter':
94
+ case ' ':
95
+ e.preventDefault();
96
+ menuItems[activeIndex]?.click();
97
+ closeMenu();
98
+ return;
99
+ }
100
+
101
+ if (nextIndex !== activeIndex) {
102
+ setActiveItem(nextIndex);
103
+ }
104
+ });
105
+
106
+ menu.addEventListener('click', (e) => {
107
+ if (e.target.closest('[role^="menuitem"]')) {
108
+ closeMenu();
109
+ }
110
+ });
111
+
112
+ document.addEventListener('click', (e) => {
113
+ if (!dropdownMenuComponent.contains(e.target)) {
114
+ closeMenu();
115
+ }
116
+ });
117
+
118
+ dropdownMenuComponent.dataset.dropdownMenuInitialized = true;
119
+ };
120
+
121
+ document.querySelectorAll('.dropdown-menu:not([data-dropdown-menu-initialized])').forEach(initDropdownMenu);
122
+
123
+ const observer = new MutationObserver((mutations) => {
124
+ mutations.forEach((mutation) => {
125
+ mutation.addedNodes.forEach((node) => {
126
+ if (node.nodeType !== Node.ELEMENT_NODE) return;
127
+ if (node.matches('.dropdown-menu:not([data-dropdown-menu-initialized])')) {
128
+ initDropdownMenu(node);
129
+ }
130
+ node.querySelectorAll('.dropdown-menu:not([data-dropdown-menu-initialized])').forEach(initDropdownMenu);
131
+ });
132
+ });
133
+ });
134
+
135
+ observer.observe(document.body, { childList: true, subtree: true });
136
+ })();
137
+ (() => {
138
+ const initPopover = (popoverComponent) => {
139
+ const trigger = popoverComponent.querySelector(':scope > button');
140
+ const content = popoverComponent.querySelector(':scope > [data-popover]');
141
+
142
+ if (!trigger || !content) {
143
+ console.error('Popover component is missing a trigger button or a content element.', popoverComponent);
144
+ return;
145
+ }
146
+
147
+ const closePopover = () => {
148
+ if (trigger.getAttribute('aria-expanded') === 'false') return;
149
+ trigger.setAttribute('aria-expanded', 'false');
150
+ content.setAttribute('aria-hidden', 'true');
151
+ trigger.focus();
152
+ };
153
+
154
+ const openPopover = () => {
155
+ const elementToFocus = content.querySelector('[autofocus]');
156
+ if (elementToFocus) {
157
+ content.addEventListener('transitionend', () => {
158
+ elementToFocus.focus();
159
+ }, { once: true });
160
+ }
161
+
162
+ trigger.setAttribute('aria-expanded', 'true');
163
+ content.setAttribute('aria-hidden', 'false');
164
+ };
165
+
166
+ trigger.addEventListener('click', () => {
167
+ const isExpanded = trigger.getAttribute('aria-expanded') === 'true';
168
+ if (isExpanded) {
169
+ closePopover();
170
+ } else {
171
+ openPopover();
172
+ }
173
+ });
174
+
175
+ popoverComponent.addEventListener('keydown', (e) => {
176
+ if (e.key === 'Escape') {
177
+ closePopover();
178
+ }
179
+ });
180
+
181
+ document.addEventListener('click', (e) => {
182
+ if (!popoverComponent.contains(e.target)) {
183
+ closePopover();
184
+ }
185
+ });
186
+
187
+ popoverComponent.dataset.popoverInitialized = true;
188
+ };
189
+
190
+ document.querySelectorAll('.popover:not([data-popover-initialized])').forEach(initPopover);
191
+
192
+ const observer = new MutationObserver((mutations) => {
193
+ mutations.forEach((mutation) => {
194
+ mutation.addedNodes.forEach((node) => {
195
+ if (node.nodeType !== Node.ELEMENT_NODE) return;
196
+ if (node.matches('.popover:not([data-popover-initialized])')) {
197
+ initPopover(node);
198
+ }
199
+ node.querySelectorAll('.popover:not([data-popover-initialized])').forEach(initPopover);
200
+ });
201
+ });
202
+ });
203
+
204
+ observer.observe(document.body, { childList: true, subtree: true });
205
+ })();
206
+
207
+ (() => {
208
+ const initSelect = (selectComponent) => {
209
+ const trigger = selectComponent.querySelector(':scope > button');
210
+ const selectedValue = trigger.querySelector(':scope > span');
211
+ const popover = selectComponent.querySelector(':scope > [data-popover]');
212
+ const listbox = popover.querySelector('[role="listbox"]');
213
+ const input = selectComponent.querySelector(':scope > input[type="hidden"]');
214
+ const filter = selectComponent.querySelector('header input[type="text"]');
215
+ if (!trigger || !popover || !listbox || !input) return;
216
+
217
+ const options = Array.from(listbox.querySelectorAll('[role="option"]'));
218
+ let visibleOptions = [...options];
219
+ let activeIndex = -1;
220
+
221
+ const updateValue = (option) => {
222
+ if (option) {
223
+ selectedValue.innerHTML = option.dataset.label || option.innerHTML;
224
+ input.value = option.dataset.value;
225
+ listbox.querySelector('[role="option"][aria-selected="true"]')?.removeAttribute('aria-selected');
226
+ option.setAttribute('aria-selected', 'true');
227
+ }
228
+ };
229
+
230
+ const closePopover = () => {
231
+ popover.setAttribute('aria-hidden', 'true');
232
+ trigger.setAttribute('aria-expanded', 'false');
233
+ if (filter) {
234
+ filter.value = '';
235
+ visibleOptions = [...options];
236
+ options.forEach(opt => opt.setAttribute('aria-hidden', 'false'));
237
+ }
238
+ trigger.removeAttribute('aria-activedescendant');
239
+ if (activeIndex > -1) options[activeIndex]?.classList.remove('active');
240
+ activeIndex = -1;
241
+ }
242
+
243
+ const selectOption = (option) => {
244
+ if (!option) return;
245
+
246
+ if (option.dataset.value) {
247
+ updateValue(option);
248
+ }
249
+ closePopover();
250
+ };
251
+
252
+ if (filter) {
253
+ const filterOptions = () => {
254
+ const searchTerm = filter.value.trim().toLowerCase();
255
+
256
+ if (activeIndex > -1) {
257
+ options[activeIndex].classList.remove('active');
258
+ trigger.removeAttribute('aria-activedescendant');
259
+ activeIndex = -1;
260
+ }
261
+
262
+ visibleOptions = [];
263
+ options.forEach(option => {
264
+ const optionText = (option.dataset.label || option.textContent).trim().toLowerCase();
265
+ const matches = optionText.includes(searchTerm);
266
+ option.setAttribute('aria-hidden', String(!matches));
267
+ if (matches) {
268
+ visibleOptions.push(option);
269
+ }
270
+ });
271
+ };
272
+
273
+ filter.addEventListener('input', filterOptions);
274
+ }
275
+
276
+ let initialOption = options.find(opt => input.value && opt.dataset.value === input.value);
277
+ if (!initialOption && options.length > 0) initialOption = options[0];
278
+
279
+ updateValue(initialOption);
280
+
281
+ const handleKeyNavigation = (e) => {
282
+ const isPopoverOpen = popover.getAttribute('aria-hidden') === 'false';
283
+
284
+ if (!['ArrowDown', 'ArrowUp', 'Enter', 'Home', 'End', 'Escape'].includes(e.key)) {
285
+ return;
286
+ }
287
+
288
+ if (!isPopoverOpen) {
289
+ if (e.key !== 'Enter' && e.key !== 'Escape') {
290
+ e.preventDefault();
291
+ trigger.click();
292
+ }
293
+ return;
294
+ }
295
+
296
+ e.preventDefault();
297
+
298
+ if (e.key === 'Escape') {
299
+ closePopover();
300
+ return;
301
+ }
302
+
303
+ if (e.key === 'Enter') {
304
+ if (activeIndex > -1) {
305
+ selectOption(visibleOptions[activeIndex]);
306
+ }
307
+ return;
308
+ }
309
+
310
+ if (visibleOptions.length === 0) return;
311
+
312
+ const currentVisibleIndex = activeIndex > -1 ? visibleOptions.indexOf(options[activeIndex]) : -1;
313
+ let nextVisibleIndex = currentVisibleIndex;
314
+
315
+ switch (e.key) {
316
+ case 'ArrowDown':
317
+ if (currentVisibleIndex < visibleOptions.length - 1) {
318
+ nextVisibleIndex = currentVisibleIndex + 1;
319
+ }
320
+ break;
321
+ case 'ArrowUp':
322
+ if (currentVisibleIndex > 0) {
323
+ nextVisibleIndex = currentVisibleIndex - 1;
324
+ } else if (currentVisibleIndex === -1) {
325
+ nextVisibleIndex = 0; // Start from top if nothing is active
326
+ }
327
+ break;
328
+ case 'Home':
329
+ nextVisibleIndex = 0;
330
+ break;
331
+ case 'End':
332
+ nextVisibleIndex = visibleOptions.length - 1;
333
+ break;
334
+ }
335
+
336
+ if (nextVisibleIndex !== currentVisibleIndex) {
337
+ if (currentVisibleIndex > -1) {
338
+ visibleOptions[currentVisibleIndex].classList.remove('active');
339
+ }
340
+
341
+ const newActiveOption = visibleOptions[nextVisibleIndex];
342
+ newActiveOption.classList.add('active');
343
+ activeIndex = options.indexOf(newActiveOption);
344
+
345
+ if (newActiveOption.id) {
346
+ trigger.setAttribute('aria-activedescendant', newActiveOption.id);
347
+ }
348
+ newActiveOption.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
349
+ }
350
+ };
351
+
352
+ trigger.addEventListener('keydown', handleKeyNavigation);
353
+ if (filter) {
354
+ filter.addEventListener('keydown', handleKeyNavigation);
355
+ }
356
+
357
+ trigger.addEventListener('click', () => {
358
+ const isExpanded = trigger.getAttribute('aria-expanded') === 'true';
359
+
360
+ if (isExpanded) {
361
+ closePopover();
362
+ } else {
363
+ popover.setAttribute('aria-hidden', 'false');
364
+ trigger.setAttribute('aria-expanded', 'true');
365
+ if (filter) filter.focus();
366
+
367
+ const selectedOption = listbox.querySelector('[role="option"][aria-selected="true"]');
368
+ if (selectedOption) {
369
+ if (activeIndex > -1) {
370
+ options[activeIndex]?.classList.remove('active');
371
+ }
372
+ activeIndex = options.indexOf(selectedOption);
373
+ selectedOption.classList.add('active');
374
+ if (selectedOption.id) {
375
+ trigger.setAttribute('aria-activedescendant', selectedOption.id);
376
+ }
377
+ selectedOption.scrollIntoView({ block: 'nearest' });
378
+ }
379
+ }
380
+ });
381
+
382
+ listbox.addEventListener('click', (e) => {
383
+ const clickedOption = e.target.closest('[role="option"]');
384
+ if (clickedOption) {
385
+ selectOption(clickedOption);
386
+ }
387
+ });
388
+
389
+ document.addEventListener('click', (e) => {
390
+ if (!selectComponent.contains(e.target)) {
391
+ closePopover();
392
+ }
393
+ });
394
+
395
+ popover.setAttribute('aria-hidden', 'true');
396
+ selectComponent.dataset.selectInitialized = true;
397
+ };
398
+
399
+ document.querySelectorAll('div.select:not([data-select-initialized])').forEach(initSelect);
400
+
401
+ const observer = new MutationObserver((mutations) => {
402
+ mutations.forEach((mutation) => {
403
+ mutation.addedNodes.forEach((node) => {
404
+ if (node.nodeType === Node.ELEMENT_NODE) {
405
+ if (node.matches('div.select:not([data-select-initialized])')) {
406
+ initSelect(node);
407
+ }
408
+ node.querySelectorAll('div.select:not([data-select-initialized])').forEach(initSelect);
409
+ }
410
+ });
411
+ });
412
+ });
413
+
414
+ observer.observe(document.body, { childList: true, subtree: true });
415
+ })();
416
+ (() => {
417
+ // Monkey patching the history API to detect client-side navigation
418
+ if (!window.history.__basecoatPatched) {
419
+ const originalPushState = window.history.pushState;
420
+ window.history.pushState = function(...args) {
421
+ originalPushState.apply(this, args);
422
+ window.dispatchEvent(new Event('basecoat:locationchange'));
423
+ };
424
+
425
+ const originalReplaceState = window.history.replaceState;
426
+ window.history.replaceState = function(...args) {
427
+ originalReplaceState.apply(this, args);
428
+ window.dispatchEvent(new Event('basecoat:locationchange'));
429
+ };
430
+
431
+ window.history.__basecoatPatched = true;
432
+ }
433
+
434
+ const initSidebar = (sidebarComponent) => {
435
+ const initialOpen = sidebarComponent.dataset.initialOpen !== 'false';
436
+ const initialMobileOpen = sidebarComponent.dataset.initialMobileOpen === 'true';
437
+ const breakpoint = parseInt(sidebarComponent.dataset.breakpoint) || 768;
438
+
439
+ let open = breakpoint > 0
440
+ ? (window.innerWidth >= breakpoint ? initialOpen : initialMobileOpen)
441
+ : initialOpen;
442
+
443
+ const updateCurrentPageLinks = () => {
444
+ const currentPath = window.location.pathname.replace(/\/$/, '');
445
+ sidebarComponent.querySelectorAll('a').forEach(link => {
446
+ if (link.hasAttribute('data-ignore-current')) return;
447
+
448
+ const linkPath = new URL(link.href).pathname.replace(/\/$/, '');
449
+ if (linkPath === currentPath) {
450
+ link.setAttribute('aria-current', 'page');
451
+ } else {
452
+ link.removeAttribute('aria-current');
453
+ }
454
+ });
455
+ };
456
+
457
+ const updateState = () => {
458
+ sidebarComponent.setAttribute('aria-hidden', !open);
459
+ if (open) {
460
+ sidebarComponent.removeAttribute('inert');
461
+ } else {
462
+ sidebarComponent.setAttribute('inert', '');
463
+ }
464
+ };
465
+
466
+ const setState = (state) => {
467
+ open = state;
468
+ updateState();
469
+ };
470
+
471
+ const sidebarId = sidebarComponent.id;
472
+
473
+ window.addEventListener('sidebar:open', (e) => {
474
+ if (!e.detail?.id || e.detail.id === sidebarId) setState(true);
475
+ });
476
+ window.addEventListener('sidebar:close', (e) => {
477
+ if (!e.detail?.id || e.detail.id === sidebarId) setState(false);
478
+ });
479
+ window.addEventListener('sidebar:toggle', (e) => {
480
+ if (!e.detail?.id || e.detail.id === sidebarId) setState(!open);
481
+ });
482
+
483
+ sidebarComponent.addEventListener('click', (e) => {
484
+ const target = e.target;
485
+ const nav = sidebarComponent.querySelector('nav');
486
+
487
+ const isMobile = window.innerWidth < breakpoint;
488
+
489
+ if (isMobile && (target.closest('a, button') && !target.closest('[data-keep-mobile-sidebar-open]'))) {
490
+ if (document.activeElement) document.activeElement.blur();
491
+ setState(false);
492
+ return;
493
+ }
494
+
495
+ if (target === sidebarComponent || (nav && !nav.contains(target))) {
496
+ if (document.activeElement) document.activeElement.blur();
497
+ setState(false);
498
+ }
499
+ });
500
+
501
+ window.addEventListener('popstate', updateCurrentPageLinks);
502
+ window.addEventListener('basecoat:locationchange', updateCurrentPageLinks);
503
+
504
+ updateState();
505
+ updateCurrentPageLinks();
506
+ sidebarComponent.dataset.sidebarInitialized = true;
507
+ };
508
+
509
+ document.querySelectorAll('.sidebar:not([data-sidebar-initialized])').forEach(initSidebar);
510
+
511
+ const observer = new MutationObserver((mutations) => {
512
+ mutations.forEach((mutation) => {
513
+ mutation.addedNodes.forEach((node) => {
514
+ if (node.nodeType === Node.ELEMENT_NODE) {
515
+ if (node.matches('.sidebar:not([data-sidebar-initialized])')) {
516
+ initSidebar(node);
517
+ }
518
+ node.querySelectorAll('.sidebar:not([data-sidebar-initialized])').forEach(initSidebar);
519
+ }
520
+ });
521
+ });
522
+ });
523
+
524
+ observer.observe(document.body, { childList: true, subtree: true });
525
+ })();
526
+ (() => {
527
+ const initTabs = (tabsComponent) => {
528
+ const tablist = tabsComponent.querySelector('[role="tablist"]');
529
+ if (!tablist) return;
530
+
531
+ const tabs = Array.from(tablist.querySelectorAll('[role="tab"]'));
532
+ const panels = tabs.map(tab => document.getElementById(tab.getAttribute('aria-controls'))).filter(Boolean);
533
+
534
+ const selectTab = (tabToSelect) => {
535
+ tabs.forEach((tab, index) => {
536
+ tab.setAttribute('aria-selected', 'false');
537
+ tab.setAttribute('tabindex', '-1');
538
+ if (panels[index]) panels[index].hidden = true;
539
+ });
540
+
541
+ tabToSelect.setAttribute('aria-selected', 'true');
542
+ tabToSelect.setAttribute('tabindex', '0');
543
+ const activePanel = document.getElementById(tabToSelect.getAttribute('aria-controls'));
544
+ if (activePanel) activePanel.hidden = false;
545
+ };
546
+
547
+ tablist.addEventListener('click', (e) => {
548
+ const clickedTab = e.target.closest('[role="tab"]');
549
+ if (clickedTab) selectTab(clickedTab);
550
+ });
551
+
552
+ tablist.addEventListener('keydown', (e) => {
553
+ const currentTab = e.target;
554
+ if (!tabs.includes(currentTab)) return;
555
+
556
+ let nextTab;
557
+ const currentIndex = tabs.indexOf(currentTab);
558
+
559
+ switch (e.key) {
560
+ case 'ArrowRight':
561
+ nextTab = tabs[(currentIndex + 1) % tabs.length];
562
+ break;
563
+ case 'ArrowLeft':
564
+ nextTab = tabs[(currentIndex - 1 + tabs.length) % tabs.length];
565
+ break;
566
+ case 'Home':
567
+ nextTab = tabs[0];
568
+ break;
569
+ case 'End':
570
+ nextTab = tabs[tabs.length - 1];
571
+ break;
572
+ default:
573
+ return;
574
+ }
575
+
576
+ e.preventDefault();
577
+ selectTab(nextTab);
578
+ nextTab.focus();
579
+ });
580
+
581
+ tabsComponent.dataset.tabsInitialized = true;
582
+ };
583
+
584
+ document.querySelectorAll('.tabs:not([data-tabs-initialized])').forEach(initTabs);
585
+
586
+ const observer = new MutationObserver((mutations) => {
587
+ mutations.forEach((mutation) => {
588
+ mutation.addedNodes.forEach((node) => {
589
+ if (node.nodeType === Node.ELEMENT_NODE) {
590
+ if (node.matches('.tabs:not([data-tabs-initialized])')) {
591
+ initTabs(node);
592
+ }
593
+ node.querySelectorAll('.tabs:not([data-tabs-initialized])').forEach(initTabs);
594
+ }
595
+ });
596
+ });
597
+ });
598
+
599
+ observer.observe(document.body, { childList: true, subtree: true });
600
+ })();
601
+ (() => {
602
+ let toaster;
603
+ const toasts = new WeakMap();
604
+ let isPaused = false;
605
+ const ICONS = {
606
+ success: '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>',
607
+ error: '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>',
608
+ info: '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>',
609
+ warning: '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>'
610
+ };
611
+
612
+ function initToaster(toasterElement) {
613
+ if (toasterElement.dataset.toasterInitialized) return;
614
+ toaster = toasterElement;
615
+
616
+ toaster.addEventListener('mouseenter', pauseAllTimeouts);
617
+ toaster.addEventListener('mouseleave', resumeAllTimeouts);
618
+ toaster.addEventListener('click', (e) => {
619
+ const actionLink = e.target.closest('.toast footer a');
620
+ const actionButton = e.target.closest('.toast footer button');
621
+ if (actionLink || actionButton) {
622
+ closeToast(e.target.closest('.toast'));
623
+ }
624
+ });
625
+
626
+ toaster.querySelectorAll('.toast:not([data-toast-initialized])').forEach(initToast);
627
+ toaster.dataset.toasterInitialized = 'true';
628
+ }
629
+
630
+ function initToast(element) {
631
+ if (element.dataset.toastInitialized) return;
632
+
633
+ const duration = parseInt(element.dataset.duration);
634
+ const timeoutDuration = duration !== -1
635
+ ? duration || (element.dataset.category === 'error' ? 5000 : 3000)
636
+ : -1;
637
+
638
+ const state = {
639
+ remainingTime: timeoutDuration,
640
+ timeoutId: null,
641
+ startTime: null,
642
+ };
643
+
644
+ if (timeoutDuration !== -1) {
645
+ if (isPaused) {
646
+ state.timeoutId = null;
647
+ } else {
648
+ state.startTime = Date.now();
649
+ state.timeoutId = setTimeout(() => closeToast(element), timeoutDuration);
650
+ }
651
+ }
652
+ toasts.set(element, state);
653
+
654
+ element.dataset.toastInitialized = 'true';
655
+ }
656
+
657
+ function pauseAllTimeouts() {
658
+ if (isPaused) return;
659
+
660
+ isPaused = true;
661
+
662
+ toaster.querySelectorAll('.toast:not([aria-hidden="true"])').forEach(element => {
663
+ if (!toasts.has(element)) return;
664
+
665
+ const state = toasts.get(element);
666
+ if (state.timeoutId) {
667
+ clearTimeout(state.timeoutId);
668
+ state.timeoutId = null;
669
+ state.remainingTime -= Date.now() - state.startTime;
670
+ }
671
+ });
672
+ }
673
+
674
+ function resumeAllTimeouts() {
675
+ if (!isPaused) return;
676
+
677
+ isPaused = false;
678
+
679
+ toaster.querySelectorAll('.toast:not([aria-hidden="true"])').forEach(element => {
680
+ if (!toasts.has(element)) return;
681
+
682
+ const state = toasts.get(element);
683
+ if (state.remainingTime !== -1 && !state.timeoutId) {
684
+ if (state.remainingTime > 0) {
685
+ state.startTime = Date.now();
686
+ state.timeoutId = setTimeout(() => closeToast(element), state.remainingTime);
687
+ } else {
688
+ closeToast(element);
689
+ }
690
+ }
691
+ });
692
+ }
693
+
694
+ function closeToast(element) {
695
+ if (!toasts.has(element)) return;
696
+
697
+ const state = toasts.get(element);
698
+ clearTimeout(state.timeoutId);
699
+ toasts.delete(element);
700
+
701
+ if (document.activeElement) document.activeElement.blur();
702
+ element.setAttribute('aria-hidden', 'true');
703
+ element.addEventListener('transitionend', () => element.remove(), { once: true });
704
+ }
705
+
706
+ function executeAction(button, toast) {
707
+ const actionString = button.dataset.toastAction;
708
+ if (!actionString) return;
709
+ try {
710
+ const func = new Function('close', actionString);
711
+ func(() => closeToast(toast));
712
+ } catch (e) {
713
+ console.error('Error executing toast action:', e);
714
+ }
715
+ }
716
+
717
+ function createToast(config) {
718
+ const {
719
+ category = 'info',
720
+ title,
721
+ description,
722
+ action,
723
+ cancel,
724
+ duration,
725
+ icon,
726
+ } = config;
727
+
728
+ const iconHtml = icon || (category && ICONS[category]) || '';
729
+ const titleHtml = title ? `<h2>${title}</h2>` : '';
730
+ const descriptionHtml = description ? `<p>${description}</p>` : '';
731
+ const actionHtml = action?.href
732
+ ? `<a href="${action.href}" class="btn" data-toast-action>${action.label}</a>`
733
+ : action?.onclick
734
+ ? `<button type="button" class="btn" data-toast-action onclick="${action.onclick}">${action.label}</button>`
735
+ : '';
736
+ const cancelHtml = cancel
737
+ ? `<button type="button" class="btn-outline h-6 text-xs px-2.5 rounded-sm" data-toast-cancel onclick="${cancel?.onclick}">${cancel.label}</button>`
738
+ : '';
739
+
740
+ const footerHtml = actionHtml || cancelHtml ? `<footer>${actionHtml}${cancelHtml}</footer>` : '';
741
+
742
+ const html = `
743
+ <div
744
+ class="toast"
745
+ role="${category === 'error' ? 'alert' : 'status'}"
746
+ aria-atomic="true"
747
+ ${category ? `data-category="${category}"` : ''}
748
+ ${duration !== undefined ? `data-duration="${duration}"` : ''}
749
+ >
750
+ <div class="toast-content">
751
+ ${iconHtml}
752
+ <section>
753
+ ${titleHtml}
754
+ ${descriptionHtml}
755
+ </section>
756
+ ${footerHtml}
757
+ </div>
758
+ </div>
759
+ </div>
760
+ `;
761
+ const template = document.createElement('template');
762
+ template.innerHTML = html.trim();
763
+ return template.content.firstChild;
764
+ }
765
+
766
+ const initialToaster = document.getElementById('toaster');
767
+ if (initialToaster) initToaster(initialToaster);
768
+
769
+ window.addEventListener('basecoat:toast', (e) => {
770
+ if (!toaster) {
771
+ console.error('Cannot create toast: toaster container not found on page.');
772
+ return;
773
+ }
774
+ const config = e.detail?.config || {};
775
+ const toastElement = createToast(config);
776
+ toaster.appendChild(toastElement);
777
+ });
778
+
779
+ const observer = new MutationObserver((mutations) => {
780
+ mutations.forEach((mutation) => {
781
+ mutation.addedNodes.forEach((node) => {
782
+ if (node.nodeType !== Node.ELEMENT_NODE) return;
783
+
784
+ if (node.matches('#toaster')) {
785
+ initToaster(node);
786
+ }
787
+
788
+ if (toaster && node.matches('.toast:not([data-toast-initialized])')) {
789
+ initToast(node);
790
+ }
791
+ });
792
+ });
793
+ });
794
+
795
+ observer.observe(document.body, { childList: true, subtree: true });
796
+ })();