@seip/blue-bird 1.1.2 → 1.1.3

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.
@@ -0,0 +1,897 @@
1
+ /**
2
+ * Blue Bird CSS Framework JS Helper
3
+ * @param {string|object} component - Component name ('snackbar', 'drawer', 'carousel', 'toast', 'tab', 'command', 'popover') or options
4
+ * @param {object} [options] - Component configuration options
5
+ */
6
+ function bluebird(component, options) {
7
+ if (typeof component === 'object') {
8
+ options = component;
9
+ component = 'snackbar';
10
+ }
11
+
12
+ // --- SNACKBAR COMPONENT ---
13
+ if (component === 'snackbar') {
14
+ let snackbarEl = document.getElementById('snackbar');
15
+
16
+ if (!snackbarEl) {
17
+ snackbarEl = document.createElement('div');
18
+ snackbarEl.id = 'snackbar';
19
+ document.body.appendChild(snackbarEl);
20
+ }
21
+
22
+ snackbarEl.className = 'show';
23
+
24
+ if (options && options.type) {
25
+ snackbarEl.classList.add(options.type);
26
+ } else {
27
+ snackbarEl.classList.add('info');
28
+ }
29
+
30
+ snackbarEl.textContent = (options && options.message) || '';
31
+ setTimeout(() => {
32
+ snackbarEl.classList.add('show');
33
+ }, 10);
34
+
35
+ const duration = (options && options.duration) || 3000;
36
+ if (snackbarEl.timeoutId) {
37
+ clearTimeout(snackbarEl.timeoutId);
38
+ }
39
+
40
+ snackbarEl.timeoutId = setTimeout(function () {
41
+ snackbarEl.className = '';
42
+ }, duration);
43
+ }
44
+
45
+ // --- MULTI-TOAST SYSTEM ---
46
+ if (component === 'toast') {
47
+ const position = (options && options.position) || 'bottom-right';
48
+ let container = document.querySelector(`.toast-container.${position}`);
49
+
50
+ if (!container) {
51
+ container = document.createElement('div');
52
+ container.className = `toast-container ${position}`;
53
+ document.body.appendChild(container);
54
+ }
55
+
56
+ const toastEl = document.createElement('div');
57
+ const typeClass = (options && options.type) ? `toast-${options.type}` : 'toast-info';
58
+ toastEl.className = `toast ${typeClass}`;
59
+
60
+ const title = (options && options.title) ? `<div class="toast-title">${options.title}</div>` : '';
61
+ const desc = (options && options.description) ? `<div class="toast-description">${options.description}</div>` : '';
62
+
63
+ toastEl.innerHTML = `
64
+ <div class="toast-content">
65
+ ${title}
66
+ ${desc}
67
+ </div>
68
+ <button class="toast-close" aria-label="Dismiss">&times;</button>
69
+ `;
70
+
71
+ const closeBtn = toastEl.querySelector('.toast-close');
72
+ closeBtn.addEventListener('click', () => dismissToast(toastEl));
73
+
74
+ container.appendChild(toastEl);
75
+
76
+ const duration = (options && options.duration) !== undefined ? options.duration : 4000;
77
+ if (duration > 0) {
78
+ setTimeout(() => dismissToast(toastEl), duration);
79
+ }
80
+ }
81
+
82
+ // --- TABS COMPONENT ---
83
+ if (component === 'tab') {
84
+ const targetId = options && options.id;
85
+ if (!targetId) return;
86
+
87
+ const targetContent = document.getElementById(targetId);
88
+ if (!targetContent) return;
89
+
90
+ const tabsContainer = targetContent.closest('.tabs');
91
+ if (!tabsContainer) return;
92
+
93
+ const allTriggers = tabsContainer.querySelectorAll('.tab-trigger');
94
+ const allContents = tabsContainer.querySelectorAll('.tab-content');
95
+
96
+ allContents.forEach(c => c.classList.remove('active'));
97
+ allTriggers.forEach(t => t.classList.remove('active'));
98
+
99
+ targetContent.classList.add('active');
100
+
101
+ const matchingTrigger = Array.from(allTriggers).find(t =>
102
+ t.getAttribute('data-tab-target') === targetId || t.getAttribute('href') === `#${targetId}`
103
+ );
104
+
105
+ if (matchingTrigger) {
106
+ matchingTrigger.classList.add('active');
107
+ }
108
+ }
109
+
110
+ // --- COMMAND PALETTE MODAL ---
111
+ if (component === 'command') {
112
+ const action = (options && options.action) || 'toggle';
113
+ let backdrop = document.querySelector('.command-backdrop');
114
+
115
+ if (!backdrop) {
116
+ backdrop = createCommandPaletteModal();
117
+ }
118
+
119
+ const isOpen = backdrop.classList.contains('open');
120
+
121
+ if (action === 'open' || (action === 'toggle' && !isOpen)) {
122
+ backdrop.classList.add('open');
123
+ const input = backdrop.querySelector('.command-input');
124
+ if (input) {
125
+ input.value = '';
126
+ setTimeout(() => input.focus(), 50);
127
+ }
128
+ } else if (action === 'close' || (action === 'toggle' && isOpen)) {
129
+ backdrop.classList.remove('open');
130
+ }
131
+ }
132
+
133
+ // --- POPOVER COMPONENT ---
134
+ if (component === 'popover') {
135
+ const id = options && options.id;
136
+ const action = (options && options.action) || 'toggle';
137
+ if (!id) return;
138
+
139
+ const popoverEl = document.getElementById(id) || document.querySelector(`[data-popover-id="${id}"]`);
140
+ if (!popoverEl) return;
141
+
142
+ const isOpen = popoverEl.classList.contains('open');
143
+ if (action === 'open' || (action === 'toggle' && !isOpen)) {
144
+ popoverEl.classList.add('open');
145
+ } else {
146
+ popoverEl.classList.remove('open');
147
+ }
148
+ }
149
+
150
+ // --- STANDALONE DRAWER COMPONENT ---
151
+ if (component === 'drawer') {
152
+ cleanupOrphanedBackdrops();
153
+
154
+ const id = options && options.id;
155
+ const action = (options && options.action) || 'toggle';
156
+ if (!id) return;
157
+
158
+ const drawerEl = document.getElementById(id);
159
+ if (!drawerEl) return;
160
+
161
+ let overlay = document.querySelector(`.drawer-backdrop[data-for="${id}"]`);
162
+ if (!overlay) {
163
+ overlay = document.createElement('div');
164
+ overlay.className = 'drawer-backdrop';
165
+ overlay.setAttribute('data-for', id);
166
+ document.body.appendChild(overlay);
167
+ overlay.addEventListener('click', () => {
168
+ bluebird('drawer', { id, action: 'close' });
169
+ });
170
+ }
171
+
172
+ const isOpen = drawerEl.classList.contains('open') || drawerEl.classList.contains('active');
173
+
174
+ if (action === 'open' || (action === 'toggle' && !isOpen)) {
175
+ drawerEl.classList.add('open');
176
+ overlay.classList.add('open');
177
+ document.body.style.overflow = 'hidden';
178
+ } else if (action === 'close' || (action === 'toggle' && isOpen)) {
179
+ drawerEl.classList.remove('open');
180
+ overlay.classList.remove('open');
181
+ document.body.style.overflow = '';
182
+ setTimeout(() => {
183
+ if (overlay && overlay.parentNode && !drawerEl.classList.contains('open')) {
184
+ overlay.remove();
185
+ }
186
+ }, 300);
187
+ }
188
+ }
189
+
190
+ // --- CAROUSEL COMPONENT ---
191
+ if (component === 'carousel') {
192
+ const selector = (options && options.selector) || '.carousel';
193
+ const carousels = document.querySelectorAll(selector);
194
+ carousels.forEach(carousel => initSingleCarousel(carousel, options));
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Global alias for snackbar
200
+ * @param {object} options - Snackbar configuration options
201
+ */
202
+ function snackbar(options) {
203
+ bluebird('snackbar', options);
204
+ }
205
+
206
+ /**
207
+ * Global alias for toast notification
208
+ * @param {object} options - Toast configuration options
209
+ */
210
+ function toast(options) {
211
+ bluebird('toast', options);
212
+ }
213
+
214
+ function dismissToast(toastEl) {
215
+ if (!toastEl || toastEl.isDismissing) return;
216
+ toastEl.isDismissing = true;
217
+ toastEl.style.opacity = '0';
218
+ toastEl.style.transform = 'translateY(-10px) scale(0.95)';
219
+ setTimeout(() => {
220
+ if (toastEl.parentNode) {
221
+ toastEl.remove();
222
+ }
223
+ }, 200);
224
+ }
225
+
226
+ /**
227
+ * Create default command palette DOM modal
228
+ */
229
+ function createCommandPaletteModal() {
230
+ const backdrop = document.createElement('div');
231
+ backdrop.className = 'command-backdrop';
232
+ backdrop.innerHTML = `
233
+ <div class="command-dialog">
234
+ <div class="command-input-wrapper">
235
+ <span>🔍</span>
236
+ <input type="text" class="command-input" placeholder="Type a command or search documentation..." />
237
+ <kbd>ESC</kbd>
238
+ </div>
239
+ <div class="command-list">
240
+ <div class="command-group">
241
+ <div class="command-group-title">Navigation</div>
242
+ <div class="command-item" data-navigate="#/"><span>Documentation Home</span><kbd>↵</kbd></div>
243
+ <div class="command-item" data-navigate="#/nextjs"><span>Next.js Integration Guide</span><kbd>↵</kbd></div>
244
+ <div class="command-item" data-navigate="#/buttons"><span>Buttons & Badges</span><kbd>↵</kbd></div>
245
+ <div class="command-item" data-navigate="#/forms"><span>Forms & Inputs</span><kbd>↵</kbd></div>
246
+ </div>
247
+ <div class="command-group">
248
+ <div class="command-group-title">Components</div>
249
+ <div class="command-item" data-navigate="#/carousel"><span>Touch Carousel</span><kbd>↵</kbd></div>
250
+ <div class="command-item" data-navigate="#/aside-drawer"><span>Aside & Drawers</span><kbd>↵</kbd></div>
251
+ <div class="command-item" data-navigate="#/animations"><span>CSS Animations</span><kbd>↵</kbd></div>
252
+ </div>
253
+ </div>
254
+ </div>
255
+ `;
256
+
257
+ document.body.appendChild(backdrop);
258
+
259
+ backdrop.addEventListener('click', (e) => {
260
+ if (e.target === backdrop) {
261
+ bluebird('command', { action: 'close' });
262
+ }
263
+ });
264
+
265
+ const input = backdrop.querySelector('.command-input');
266
+ input.addEventListener('input', (e) => {
267
+ const query = e.target.value.toLowerCase().trim();
268
+ const items = backdrop.querySelectorAll('.command-item');
269
+ items.forEach(item => {
270
+ const text = item.textContent.toLowerCase();
271
+ item.style.display = text.includes(query) ? 'flex' : 'none';
272
+ });
273
+ });
274
+
275
+ backdrop.addEventListener('click', (e) => {
276
+ const item = e.target.closest('.command-item');
277
+ if (item && item.getAttribute('data-navigate')) {
278
+ window.location.hash = item.getAttribute('data-navigate');
279
+ bluebird('command', { action: 'close' });
280
+ }
281
+ });
282
+
283
+ return backdrop;
284
+ }
285
+
286
+ /**
287
+ * Remove backdrops whose target drawer no longer exists in DOM
288
+ */
289
+ function cleanupOrphanedBackdrops() {
290
+ document.querySelectorAll('.drawer-backdrop[data-for]').forEach(backdrop => {
291
+ const targetId = backdrop.getAttribute('data-for');
292
+ if (!document.getElementById(targetId)) {
293
+ backdrop.remove();
294
+ }
295
+ });
296
+ }
297
+
298
+ function initMobileDrawer() {
299
+ const mainEl = document.querySelector('main');
300
+ const aside = mainEl ? mainEl.querySelector(':scope > aside') : null;
301
+ if (!aside) return;
302
+
303
+ let overlay = document.querySelector('.bluebird-drawer-overlay');
304
+ if (!overlay) {
305
+ overlay = document.createElement('div');
306
+ overlay.className = 'bluebird-drawer-overlay';
307
+ document.body.appendChild(overlay);
308
+ }
309
+
310
+ let drawer = document.querySelector('.bluebird-drawer');
311
+ if (!drawer) {
312
+ drawer = document.createElement('div');
313
+ drawer.className = 'bluebird-drawer';
314
+ document.body.appendChild(drawer);
315
+ }
316
+
317
+ drawer.innerHTML = aside.innerHTML;
318
+
319
+ let toggle = document.querySelector('.bluebird-drawer-toggle');
320
+ if (!toggle) {
321
+ toggle = document.createElement('button');
322
+ toggle.className = 'bluebird-drawer-toggle';
323
+ toggle.innerHTML = '☰';
324
+ toggle.setAttribute('aria-label', 'Toggle navigation menu');
325
+
326
+ const header = document.querySelector('header');
327
+ if (header) {
328
+ const nav = header.querySelector('nav');
329
+ if (nav) {
330
+ nav.insertBefore(toggle, nav.firstChild);
331
+ } else {
332
+ header.prepend(toggle);
333
+ }
334
+ } else {
335
+ document.body.prepend(toggle);
336
+ }
337
+ }
338
+ }
339
+
340
+ // Global keyboard listener for Ctrl+K / Cmd+K Command Palette shortcut & ESC key
341
+ document.addEventListener('keydown', function (e) {
342
+ if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
343
+ e.preventDefault();
344
+ bluebird('command', { action: 'toggle' });
345
+ }
346
+
347
+ if (e.key === 'Escape') {
348
+ const commandBackdrop = document.querySelector('.command-backdrop.open');
349
+ if (commandBackdrop) {
350
+ bluebird('command', { action: 'close' });
351
+ }
352
+ }
353
+ });
354
+
355
+ // Global click listener for Material Ripples, Mobile Navigation Drawer, Tabs, Popovers & Declarative Data Attributes
356
+ document.addEventListener('click', function (e) {
357
+ // 1. Declarative Tab Trigger Click
358
+ const tabTrigger = e.target.closest('[data-tab-target], .tab-trigger');
359
+ if (tabTrigger) {
360
+ const targetId = tabTrigger.getAttribute('data-tab-target') || (tabTrigger.getAttribute('href') || '').replace('#', '');
361
+ if (targetId) {
362
+ e.preventDefault();
363
+ bluebird('tab', { id: targetId });
364
+ }
365
+ }
366
+
367
+ // 2. Declarative Popover Trigger Click
368
+ const popoverTrigger = e.target.closest('[data-popover-target]');
369
+ if (popoverTrigger) {
370
+ const popoverId = popoverTrigger.getAttribute('data-popover-target');
371
+ bluebird('popover', { id: popoverId, action: 'toggle' });
372
+ }
373
+
374
+ // Close open popovers when clicking outside
375
+ if (!e.target.closest('.popover') && !e.target.closest('[data-popover-target]')) {
376
+ document.querySelectorAll('.popover.open').forEach(p => p.classList.remove('open'));
377
+ }
378
+
379
+ // 3. Mobile Navigation Toggle Button Click
380
+ const mobileToggle = e.target.closest('.bluebird-drawer-toggle');
381
+ if (mobileToggle) {
382
+ e.preventDefault();
383
+ e.stopPropagation();
384
+ initMobileDrawer();
385
+ const drawer = document.querySelector('.bluebird-drawer');
386
+ const overlay = document.querySelector('.bluebird-drawer-overlay');
387
+ if (drawer && overlay) {
388
+ const isOpen = drawer.classList.contains('open');
389
+ if (isOpen) {
390
+ drawer.classList.remove('open');
391
+ overlay.classList.remove('open');
392
+ document.body.style.overflow = '';
393
+ } else {
394
+ drawer.classList.add('open');
395
+ overlay.classList.add('open');
396
+ document.body.style.overflow = 'hidden';
397
+ }
398
+ }
399
+ return;
400
+ }
401
+
402
+ // 4. Mobile Navigation Overlay Click
403
+ if (e.target.closest('.bluebird-drawer-overlay')) {
404
+ const drawer = document.querySelector('.bluebird-drawer');
405
+ const overlay = document.querySelector('.bluebird-drawer-overlay');
406
+ if (drawer) drawer.classList.remove('open');
407
+ if (overlay) overlay.classList.remove('open');
408
+ document.body.style.overflow = '';
409
+ return;
410
+ }
411
+
412
+ // 5. Mobile Navigation Drawer Link Click
413
+ if (e.target.closest('.bluebird-drawer a')) {
414
+ const drawer = document.querySelector('.bluebird-drawer');
415
+ const overlay = document.querySelector('.bluebird-drawer-overlay');
416
+ if (drawer) drawer.classList.remove('open');
417
+ if (overlay) overlay.classList.remove('open');
418
+ document.body.style.overflow = '';
419
+ }
420
+
421
+ // Material Ripple Effect
422
+ const btn = e.target.closest("button, a[role='button']");
423
+ if (btn && !btn.classList.contains('fab') && !btn.classList.contains('carousel-nav') && !btn.classList.contains('bluebird-drawer-toggle')) {
424
+ const rect = btn.getBoundingClientRect();
425
+ const size = Math.max(rect.width, rect.height);
426
+ const x = e.clientX - rect.left - size / 2;
427
+ const y = e.clientY - rect.top - size / 2;
428
+
429
+ const ripple = document.createElement('span');
430
+ ripple.className = 'ripple';
431
+ ripple.style.width = ripple.style.height = size + 'px';
432
+ ripple.style.left = x + 'px';
433
+ ripple.style.top = y + 'px';
434
+
435
+ btn.appendChild(ripple);
436
+ ripple.addEventListener('animationend', () => ripple.remove());
437
+ }
438
+
439
+ // Declarative Standalone Drawer Triggers
440
+ const drawerTrigger = e.target.closest('[data-drawer-target]');
441
+ if (drawerTrigger) {
442
+ const id = drawerTrigger.getAttribute('data-drawer-target');
443
+ bluebird('drawer', { id, action: 'toggle' });
444
+ }
445
+
446
+ const drawerClose = e.target.closest('[data-drawer-close]');
447
+ if (drawerClose) {
448
+ const drawerEl = drawerClose.closest('.drawer');
449
+ if (drawerEl && drawerEl.id) {
450
+ bluebird('drawer', { id: drawerEl.id, action: 'close' });
451
+ }
452
+ }
453
+ });
454
+
455
+ /**
456
+ * Single Carousel Initialization logic with Touch/Swipe, Drag & Arrow Controls
457
+ */
458
+ function initSingleCarousel(carousel, opts = {}) {
459
+ if (carousel._bb_initialized) return;
460
+ carousel._bb_initialized = true;
461
+
462
+ const track = carousel.querySelector('.carousel-track');
463
+ if (!track) return;
464
+
465
+ const items = Array.from(track.querySelectorAll('.carousel-item, .carousel-card'));
466
+ if (items.length === 0) return;
467
+
468
+ const prevBtn = carousel.querySelector('.carousel-prev');
469
+ const nextBtn = carousel.querySelector('.carousel-next');
470
+ let indicatorsContainer = carousel.querySelector('.carousel-indicators');
471
+
472
+ let currentSlideIndex = 0;
473
+
474
+ if (indicatorsContainer && indicatorsContainer.children.length === 0) {
475
+ items.forEach((_, idx) => {
476
+ const dot = document.createElement('button');
477
+ dot.className = `carousel-dot ${idx === 0 ? 'active' : ''}`;
478
+ dot.setAttribute('aria-label', `Go to slide ${idx + 1}`);
479
+ dot.addEventListener('click', (e) => {
480
+ e.preventDefault();
481
+ scrollToSlide(idx);
482
+ });
483
+ indicatorsContainer.appendChild(dot);
484
+ });
485
+ }
486
+
487
+ function scrollToSlide(index) {
488
+ if (index < 0) index = 0;
489
+ if (index >= items.length) index = items.length - 1;
490
+ currentSlideIndex = index;
491
+ const targetItem = items[index];
492
+ if (targetItem) {
493
+ track.scrollTo({
494
+ left: targetItem.offsetLeft - track.offsetLeft,
495
+ behavior: 'smooth'
496
+ });
497
+ updateIndicators(index);
498
+ }
499
+ }
500
+
501
+ function updateIndicators(activeIndex) {
502
+ if (!indicatorsContainer) return;
503
+ const dots = Array.from(indicatorsContainer.children);
504
+ dots.forEach((dot, idx) => {
505
+ dot.classList.toggle('active', idx === activeIndex);
506
+ });
507
+ }
508
+
509
+ let scrollTimeout;
510
+ track.addEventListener('scroll', () => {
511
+ clearTimeout(scrollTimeout);
512
+ scrollTimeout = setTimeout(() => {
513
+ const trackLeft = track.scrollLeft;
514
+ let closestIndex = 0;
515
+ let minDistance = Infinity;
516
+
517
+ items.forEach((item, idx) => {
518
+ const distance = Math.abs(item.offsetLeft - track.offsetLeft - trackLeft);
519
+ if (distance < minDistance) {
520
+ minDistance = distance;
521
+ closestIndex = idx;
522
+ }
523
+ });
524
+
525
+ currentSlideIndex = closestIndex;
526
+ updateIndicators(closestIndex);
527
+ }, 40);
528
+ });
529
+
530
+ if (prevBtn) {
531
+ prevBtn.addEventListener('click', (e) => {
532
+ e.preventDefault();
533
+ e.stopPropagation();
534
+ scrollToSlide(currentSlideIndex - 1);
535
+ });
536
+ }
537
+
538
+ if (nextBtn) {
539
+ nextBtn.addEventListener('click', (e) => {
540
+ e.preventDefault();
541
+ e.stopPropagation();
542
+ scrollToSlide(currentSlideIndex + 1);
543
+ });
544
+ }
545
+
546
+ // Mobile Touch Swipe & Desktop Mouse Drag
547
+ let startX = 0;
548
+ let isDragging = false;
549
+
550
+ track.addEventListener('touchstart', (e) => {
551
+ startX = e.touches[0].clientX;
552
+ isDragging = true;
553
+ }, { passive: true });
554
+
555
+ track.addEventListener('touchend', (e) => {
556
+ if (!isDragging) return;
557
+ isDragging = false;
558
+ const endX = e.changedTouches[0].clientX;
559
+ const diffX = startX - endX;
560
+
561
+ if (Math.abs(diffX) > 35) {
562
+ if (diffX > 0) {
563
+ scrollToSlide(currentSlideIndex + 1);
564
+ } else {
565
+ scrollToSlide(currentSlideIndex - 1);
566
+ }
567
+ }
568
+ });
569
+
570
+ track.addEventListener('mousedown', (e) => {
571
+ startX = e.clientX;
572
+ isDragging = true;
573
+ track.style.cursor = 'grabbing';
574
+ });
575
+
576
+ track.addEventListener('mouseleave', () => {
577
+ isDragging = false;
578
+ track.style.cursor = 'grab';
579
+ });
580
+
581
+ track.addEventListener('mouseup', (e) => {
582
+ if (!isDragging) return;
583
+ isDragging = false;
584
+ track.style.cursor = 'grab';
585
+ const endX = e.clientX;
586
+ const diffX = startX - endX;
587
+
588
+ if (Math.abs(diffX) > 35) {
589
+ if (diffX > 0) {
590
+ scrollToSlide(currentSlideIndex + 1);
591
+ } else {
592
+ scrollToSlide(currentSlideIndex - 1);
593
+ }
594
+ }
595
+ });
596
+
597
+ const isAutoplay = (opts && opts.autoplay) || carousel.getAttribute('data-autoplay') === 'true';
598
+ const intervalTime = parseInt((opts && opts.interval) || carousel.getAttribute('data-interval') || 3500, 10);
599
+
600
+ if (isAutoplay) {
601
+ let autoInterval = setInterval(() => {
602
+ const nextIdx = (currentSlideIndex + 1) % items.length;
603
+ scrollToSlide(nextIdx);
604
+ }, intervalTime);
605
+
606
+ carousel.addEventListener('mouseenter', () => clearInterval(autoInterval));
607
+ carousel.addEventListener('mouseleave', () => {
608
+ autoInterval = setInterval(() => {
609
+ const nextIdx = (currentSlideIndex + 1) % items.length;
610
+ scrollToSlide(nextIdx);
611
+ }, intervalTime);
612
+ });
613
+ }
614
+ }
615
+
616
+ // --- DECLARATIVE DATA ATTRIBUTES & GLOBAL EVENT DELEGATION ---
617
+ (function setupDeclarativeListeners() {
618
+ // Click Delegations
619
+ document.addEventListener('click', (e) => {
620
+ // 1. Data-Copy
621
+ const copyTrigger = e.target.closest('[data-copy]');
622
+ if (copyTrigger) {
623
+ e.preventDefault();
624
+ const targetAttr = copyTrigger.getAttribute('data-copy');
625
+ let textToCopy = targetAttr;
626
+
627
+ if (targetAttr && (targetAttr.startsWith('#') || targetAttr.startsWith('.'))) {
628
+ const targetEl = document.querySelector(targetAttr);
629
+ if (targetEl) {
630
+ textToCopy = targetEl.value !== undefined ? targetEl.value : (targetEl.innerText || targetEl.textContent);
631
+ }
632
+ }
633
+
634
+ if (textToCopy) {
635
+ navigator.clipboard.writeText(textToCopy.trim()).then(() => {
636
+ copyTrigger.classList.add('copied');
637
+ if (typeof bluebird === 'function') {
638
+ bluebird('toast', {
639
+ title: 'Copied to clipboard',
640
+ description: textToCopy.length > 50 ? textToCopy.substring(0, 50) + '...' : textToCopy,
641
+ type: 'success',
642
+ duration: 2500
643
+ });
644
+ }
645
+ setTimeout(() => copyTrigger.classList.remove('copied'), 2000);
646
+ });
647
+ }
648
+ return;
649
+ }
650
+
651
+ // 2. Data-Confirm (Prompt verification before action)
652
+ const confirmTrigger = e.target.closest('[data-confirm]');
653
+ if (confirmTrigger) {
654
+ const msg = confirmTrigger.getAttribute('data-confirm') || 'Are you sure?';
655
+ if (!window.confirm(msg)) {
656
+ e.preventDefault();
657
+ e.stopImmediatePropagation();
658
+ return;
659
+ }
660
+ }
661
+
662
+ // 3. Data-Scroll-To (Smooth scroll with header compensation)
663
+ const scrollTrigger = e.target.closest('[data-scroll-to]');
664
+ if (scrollTrigger) {
665
+ e.preventDefault();
666
+ const targetId = scrollTrigger.getAttribute('data-scroll-to');
667
+ const targetEl = document.querySelector(targetId);
668
+ if (targetEl) {
669
+ targetEl.scrollIntoView({ behavior: 'smooth', block: 'start' });
670
+ }
671
+ return;
672
+ }
673
+
674
+ // 4. Data-Password-Toggle (Toggle password unmask)
675
+ const passToggle = e.target.closest('[data-password-toggle]');
676
+ if (passToggle) {
677
+ e.preventDefault();
678
+ const targetSelector = passToggle.getAttribute('data-password-toggle');
679
+ const input = targetSelector
680
+ ? document.querySelector(targetSelector)
681
+ : (passToggle.closest('.input-group, .form-input-group, div')?.querySelector('input') || passToggle.previousElementSibling);
682
+
683
+ if (input && (input.type === 'password' || input.type === 'text')) {
684
+ const isPassword = input.type === 'password';
685
+ input.type = isPassword ? 'text' : 'password';
686
+ passToggle.classList.toggle('showing', isPassword);
687
+ passToggle.setAttribute('aria-pressed', isPassword ? 'true' : 'false');
688
+ }
689
+ return;
690
+ }
691
+
692
+ // 5. Data-Step-Up / Data-Step-Down (Number Steppers)
693
+ const stepUp = e.target.closest('[data-step-up]');
694
+ if (stepUp) {
695
+ e.preventDefault();
696
+ const targetInput = document.querySelector(stepUp.getAttribute('data-step-up')) ||
697
+ stepUp.closest('.stepper')?.querySelector('input[type="number"]');
698
+ if (targetInput && typeof targetInput.stepUp === 'function') {
699
+ targetInput.stepUp();
700
+ targetInput.dispatchEvent(new Event('input', { bubbles: true }));
701
+ targetInput.dispatchEvent(new Event('change', { bubbles: true }));
702
+ }
703
+ return;
704
+ }
705
+
706
+ const stepDown = e.target.closest('[data-step-down]');
707
+ if (stepDown) {
708
+ e.preventDefault();
709
+ const targetInput = document.querySelector(stepDown.getAttribute('data-step-down')) ||
710
+ stepDown.closest('.stepper')?.querySelector('input[type="number"]');
711
+ if (targetInput && typeof targetInput.stepDown === 'function') {
712
+ targetInput.stepDown();
713
+ targetInput.dispatchEvent(new Event('input', { bubbles: true }));
714
+ targetInput.dispatchEvent(new Event('change', { bubbles: true }));
715
+ }
716
+ return;
717
+ }
718
+
719
+ // 6. Data-Select-Value (ComboBox / Select2 item selection)
720
+ const selectItem = e.target.closest('[data-select-value]');
721
+ if (selectItem) {
722
+ const val = selectItem.getAttribute('data-select-value');
723
+ const targetSelector = selectItem.getAttribute('data-select-target') ||
724
+ selectItem.closest('[data-select-container]')?.getAttribute('data-select-target');
725
+ if (targetSelector) {
726
+ const targetEl = document.querySelector(targetSelector);
727
+ if (targetEl) {
728
+ if (targetEl.tagName === 'INPUT' || targetEl.tagName === 'SELECT') {
729
+ targetEl.value = val;
730
+ targetEl.dispatchEvent(new Event('input', { bubbles: true }));
731
+ targetEl.dispatchEvent(new Event('change', { bubbles: true }));
732
+ } else {
733
+ targetEl.textContent = selectItem.textContent.trim();
734
+ }
735
+ }
736
+ }
737
+ // Close dropdown if inside one
738
+ const parentDropdown = selectItem.closest('.dropdown-content, .popover-content');
739
+ if (parentDropdown) {
740
+ parentDropdown.classList.remove('open');
741
+ }
742
+ }
743
+
744
+ // 7. Data-Toggle / Modal Trigger
745
+ const modalTrigger = e.target.closest('[data-toggle="modal"], [data-modal-target], [data-dialog-target]');
746
+ if (modalTrigger) {
747
+ e.preventDefault();
748
+ const targetSelector = modalTrigger.getAttribute('data-modal-target') ||
749
+ modalTrigger.getAttribute('data-dialog-target') ||
750
+ modalTrigger.getAttribute('data-target') ||
751
+ modalTrigger.getAttribute('href');
752
+ if (targetSelector) {
753
+ const dialog = document.querySelector(targetSelector);
754
+ if (dialog && typeof dialog.showModal === 'function') {
755
+ dialog.showModal();
756
+ }
757
+ }
758
+ return;
759
+ }
760
+
761
+ // 8. Data-Dismiss / Modal Close
762
+ const dismissTrigger = e.target.closest('[data-dismiss="modal"], [data-close-dialog], [data-close-modal]');
763
+ if (dismissTrigger) {
764
+ e.preventDefault();
765
+ const dialog = dismissTrigger.closest('dialog') ||
766
+ document.querySelector(dismissTrigger.getAttribute('data-target') || '');
767
+ if (dialog && typeof dialog.close === 'function') {
768
+ dialog.close();
769
+ }
770
+ return;
771
+ }
772
+
773
+ // 9. Data-Toggle Theme
774
+ const themeTrigger = e.target.closest('[data-toggle="theme"]');
775
+ if (themeTrigger) {
776
+ e.preventDefault();
777
+ const html = document.documentElement;
778
+ const current = html.getAttribute('data-theme') || 'light';
779
+ const next = current === 'dark' ? 'light' : 'dark';
780
+ html.setAttribute('data-theme', next);
781
+ try {
782
+ localStorage.setItem('bluebird-theme', next);
783
+ } catch (err) { }
784
+ return;
785
+ }
786
+
787
+ // 10. Data-Toast Trigger
788
+ const toastTrigger = e.target.closest('[data-toast]');
789
+ if (toastTrigger) {
790
+ e.preventDefault();
791
+ const desc = toastTrigger.getAttribute('data-toast') || '';
792
+ const title = toastTrigger.getAttribute('data-toast-title') || '';
793
+ const type = toastTrigger.getAttribute('data-toast-type') || 'info';
794
+ if (typeof bluebird === 'function') {
795
+ bluebird('toast', { title, description: desc, type });
796
+ }
797
+ return;
798
+ }
799
+
800
+ // 11. Data-Snackbar Trigger
801
+ const snackbarTrigger = e.target.closest('[data-snackbar]');
802
+ if (snackbarTrigger) {
803
+ e.preventDefault();
804
+ const message = snackbarTrigger.getAttribute('data-snackbar') || '';
805
+ const type = snackbarTrigger.getAttribute('data-snackbar-type') || 'info';
806
+ if (typeof bluebird === 'function') {
807
+ bluebird('snackbar', { message, type });
808
+ }
809
+ return;
810
+ }
811
+
812
+ // 12. Click outside dropdown / popover auto-close
813
+ if (!e.target.closest('.dropdown') && !e.target.closest('.popover')) {
814
+ document.querySelectorAll('.dropdown-content.open, .popover-content.open').forEach(el => {
815
+ el.classList.remove('open');
816
+ });
817
+ }
818
+ });
819
+
820
+ // Live Input Event Delegations (Filter Target & Auto-Resize Textarea)
821
+ document.addEventListener('input', (e) => {
822
+ // A. Real-time List/Table/ComboBox Filtering (data-filter-target="#lista")
823
+ const filterInput = e.target.closest('[data-filter-target]');
824
+ if (filterInput) {
825
+ const targetSelector = filterInput.getAttribute('data-filter-target');
826
+ const targetContainer = document.querySelector(targetSelector);
827
+ if (targetContainer) {
828
+ const query = filterInput.value.toLowerCase().trim();
829
+ const items = targetContainer.querySelectorAll('[data-filter-item], li, tr, .card, .dropdown-item, .item');
830
+ let visibleCount = 0;
831
+
832
+ items.forEach(item => {
833
+ const text = item.textContent.toLowerCase();
834
+ const matches = text.includes(query);
835
+ item.style.display = matches ? '' : 'none';
836
+ if (matches) visibleCount++;
837
+ });
838
+
839
+ const emptyMsg = targetContainer.querySelector('.no-filter-results');
840
+ if (emptyMsg) {
841
+ emptyMsg.style.display = visibleCount === 0 ? 'block' : 'none';
842
+ }
843
+ }
844
+ }
845
+
846
+ // B. Auto-Resize Textarea (data-auto-resize)
847
+ if (e.target.matches('textarea[data-auto-resize]')) {
848
+ const textarea = e.target;
849
+ textarea.style.height = 'auto';
850
+ textarea.style.height = (textarea.scrollHeight + 2) + 'px';
851
+ }
852
+ });
853
+
854
+ // Global Ctrl+K / Cmd+K listener
855
+ document.addEventListener('keydown', (e) => {
856
+ if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
857
+ e.preventDefault();
858
+ if (typeof bluebird === 'function') {
859
+ bluebird('command', { action: 'toggle' });
860
+ }
861
+ } else if (e.key === 'Escape') {
862
+ const openCommand = document.querySelector('.command-backdrop.open');
863
+ if (openCommand && typeof bluebird === 'function') {
864
+ bluebird('command', { action: 'close' });
865
+ }
866
+ }
867
+ });
868
+ })();
869
+
870
+ // Auto Setup Helper Elements on DOMReady
871
+ (function () {
872
+ function init() {
873
+ cleanupOrphanedBackdrops();
874
+ initMobileDrawer();
875
+ document.querySelectorAll('.carousel').forEach(c => initSingleCarousel(c));
876
+
877
+ // Auto resize textareas on init
878
+ document.querySelectorAll('textarea[data-auto-resize]').forEach(t => {
879
+ t.style.height = 'auto';
880
+ t.style.height = (t.scrollHeight + 2) + 'px';
881
+ });
882
+
883
+ // Restore saved theme if available
884
+ try {
885
+ const savedTheme = localStorage.getItem('bluebird-theme');
886
+ if (savedTheme) {
887
+ document.documentElement.setAttribute('data-theme', savedTheme);
888
+ }
889
+ } catch (e) { }
890
+ }
891
+
892
+ if (document.readyState === 'loading') {
893
+ document.addEventListener('DOMContentLoaded', () => setTimeout(init, 100));
894
+ } else {
895
+ setTimeout(init, 100);
896
+ }
897
+ })();