@seip/blue-bird 1.1.3 → 1.1.4

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.
@@ -4,63 +4,63 @@
4
4
  * @param {object} [options] - Component configuration options
5
5
  */
6
6
  function bluebird(component, options) {
7
- if (typeof component === 'object') {
8
- options = component;
9
- component = 'snackbar';
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);
10
20
  }
11
21
 
12
- // --- SNACKBAR COMPONENT ---
13
- if (component === 'snackbar') {
14
- let snackbarEl = document.getElementById('snackbar');
22
+ snackbarEl.className = 'show';
15
23
 
16
- if (!snackbarEl) {
17
- snackbarEl = document.createElement('div');
18
- snackbarEl.id = 'snackbar';
19
- document.body.appendChild(snackbarEl);
20
- }
24
+ if (options && options.type) {
25
+ snackbarEl.classList.add(options.type);
26
+ } else {
27
+ snackbarEl.classList.add('info');
28
+ }
21
29
 
22
- snackbarEl.className = 'show';
30
+ snackbarEl.textContent = (options && options.message) || '';
31
+ setTimeout(() => {
32
+ snackbarEl.classList.add('show');
33
+ }, 10);
23
34
 
24
- if (options && options.type) {
25
- snackbarEl.classList.add(options.type);
26
- } else {
27
- snackbarEl.classList.add('info');
28
- }
35
+ const duration = (options && options.duration) || 3000;
36
+ if (snackbarEl.timeoutId) {
37
+ clearTimeout(snackbarEl.timeoutId);
38
+ }
29
39
 
30
- snackbarEl.textContent = (options && options.message) || '';
31
- setTimeout(() => {
32
- snackbarEl.classList.add('show');
33
- }, 10);
40
+ snackbarEl.timeoutId = setTimeout(function () {
41
+ snackbarEl.className = '';
42
+ }, duration);
43
+ }
34
44
 
35
- const duration = (options && options.duration) || 3000;
36
- if (snackbarEl.timeoutId) {
37
- clearTimeout(snackbarEl.timeoutId);
38
- }
45
+ // --- MULTI-TOAST SYSTEM ---
46
+ if (component === 'toast') {
47
+ const position = (options && options.position) || 'bottom-right';
48
+ let container = document.querySelector(`.toast-container.${position}`);
39
49
 
40
- snackbarEl.timeoutId = setTimeout(function () {
41
- snackbarEl.className = '';
42
- }, duration);
50
+ if (!container) {
51
+ container = document.createElement('div');
52
+ container.className = `toast-container ${position}`;
53
+ document.body.appendChild(container);
43
54
  }
44
55
 
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}`;
56
+ const toastEl = document.createElement('div');
57
+ const typeClass = (options && options.type) ? `toast-${options.type}` : 'toast-info';
58
+ toastEl.className = `toast ${typeClass}`;
59
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>` : '';
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
62
 
63
- toastEl.innerHTML = `
63
+ toastEl.innerHTML = `
64
64
  <div class="toast-content">
65
65
  ${title}
66
66
  ${desc}
@@ -68,131 +68,137 @@ function bluebird(component, options) {
68
68
  <button class="toast-close" aria-label="Dismiss">&times;</button>
69
69
  `;
70
70
 
71
- const closeBtn = toastEl.querySelector('.toast-close');
72
- closeBtn.addEventListener('click', () => dismissToast(toastEl));
71
+ const closeBtn = toastEl.querySelector('.toast-close');
72
+ closeBtn.addEventListener('click', () => dismissToast(toastEl));
73
73
 
74
- container.appendChild(toastEl);
74
+ container.appendChild(toastEl);
75
75
 
76
- const duration = (options && options.duration) !== undefined ? options.duration : 4000;
77
- if (duration > 0) {
78
- setTimeout(() => dismissToast(toastEl), duration);
79
- }
76
+ const duration = (options && options.duration) !== undefined ? options.duration : 4000;
77
+ if (duration > 0) {
78
+ setTimeout(() => dismissToast(toastEl), duration);
80
79
  }
80
+ }
81
81
 
82
- // --- TABS COMPONENT ---
83
- if (component === 'tab') {
84
- const targetId = options && options.id;
85
- if (!targetId) return;
82
+ // --- TABS COMPONENT ---
83
+ if (component === 'tab') {
84
+ const targetId = options && options.id;
85
+ if (!targetId) return;
86
86
 
87
- const targetContent = document.getElementById(targetId);
88
- if (!targetContent) return;
87
+ const targetContent = document.getElementById(targetId);
88
+ if (!targetContent) return;
89
89
 
90
- const tabsContainer = targetContent.closest('.tabs');
91
- if (!tabsContainer) return;
90
+ const tabsContainer = targetContent.closest('.tabs');
91
+ if (!tabsContainer) return;
92
92
 
93
- const allTriggers = tabsContainer.querySelectorAll('.tab-trigger');
94
- const allContents = tabsContainer.querySelectorAll('.tab-content');
93
+ const allTriggers = tabsContainer.querySelectorAll('.tab-trigger');
94
+ const allContents = tabsContainer.querySelectorAll('.tab-content');
95
95
 
96
- allContents.forEach(c => c.classList.remove('active'));
97
- allTriggers.forEach(t => t.classList.remove('active'));
96
+ allContents.forEach(c => c.classList.remove('active'));
97
+ allTriggers.forEach(t => t.classList.remove('active'));
98
98
 
99
- targetContent.classList.add('active');
99
+ targetContent.classList.add('active');
100
100
 
101
- const matchingTrigger = Array.from(allTriggers).find(t =>
102
- t.getAttribute('data-tab-target') === targetId || t.getAttribute('href') === `#${targetId}`
103
- );
101
+ const matchingTrigger = Array.from(allTriggers).find(t =>
102
+ t.getAttribute('data-tab-target') === targetId || t.getAttribute('href') === `#${targetId}`
103
+ );
104
104
 
105
- if (matchingTrigger) {
106
- matchingTrigger.classList.add('active');
107
- }
105
+ if (matchingTrigger) {
106
+ matchingTrigger.classList.add('active');
108
107
  }
108
+ }
109
109
 
110
- // --- COMMAND PALETTE MODAL ---
111
- if (component === 'command') {
112
- const action = (options && options.action) || 'toggle';
113
- let backdrop = document.querySelector('.command-backdrop');
110
+ // --- COMMAND PALETTE MODAL ---
111
+ if (component === 'command') {
112
+ const action = (options && options.action) || 'toggle';
113
+ let backdrop = document.querySelector('.command-backdrop');
114
114
 
115
- if (!backdrop) {
116
- backdrop = createCommandPaletteModal();
117
- }
115
+ if (!backdrop) {
116
+ backdrop = createCommandPaletteModal();
117
+ }
118
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
- }
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');
131
130
  }
131
+ }
132
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;
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
138
 
139
- const popoverEl = document.getElementById(id) || document.querySelector(`[data-popover-id="${id}"]`);
140
- if (!popoverEl) return;
139
+ const popoverEl = document.getElementById(id) || document.querySelector(`[data-popover-id="${id}"]`);
140
+ if (!popoverEl) return;
141
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
- }
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');
148
147
  }
148
+ }
149
149
 
150
- // --- STANDALONE DRAWER COMPONENT ---
151
- if (component === 'drawer') {
152
- cleanupOrphanedBackdrops();
150
+ // --- STANDALONE DRAWER COMPONENT ---
151
+ if (component === 'drawer') {
152
+ cleanupOrphanedBackdrops();
153
153
 
154
- const id = options && options.id;
155
- const action = (options && options.action) || 'toggle';
156
- if (!id) return;
154
+ const id = options && options.id;
155
+ const action = (options && options.action) || 'toggle';
156
+ if (!id) return;
157
157
 
158
- const drawerEl = document.getElementById(id);
159
- if (!drawerEl) return;
158
+ const drawerEl = document.getElementById(id);
159
+ if (!drawerEl) return;
160
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
- }
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
+ });
188
170
  }
189
171
 
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));
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);
195
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
+ // --- RESPONSIVE DATATABLE COMPONENT ---
198
+ if (component === 'datatable' || component === 'table') {
199
+ const containerId = (options && (options.container || options.id)) || 'datatable';
200
+ return new ResponsiveDataTable(containerId, options);
201
+ }
196
202
  }
197
203
 
198
204
  /**
@@ -200,7 +206,7 @@ function bluebird(component, options) {
200
206
  * @param {object} options - Snackbar configuration options
201
207
  */
202
208
  function snackbar(options) {
203
- bluebird('snackbar', options);
209
+ bluebird('snackbar', options);
204
210
  }
205
211
 
206
212
  /**
@@ -208,28 +214,28 @@ function snackbar(options) {
208
214
  * @param {object} options - Toast configuration options
209
215
  */
210
216
  function toast(options) {
211
- bluebird('toast', options);
217
+ bluebird('toast', options);
212
218
  }
213
219
 
214
220
  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);
221
+ if (!toastEl || toastEl.isDismissing) return;
222
+ toastEl.isDismissing = true;
223
+ toastEl.style.opacity = '0';
224
+ toastEl.style.transform = 'translateY(-10px) scale(0.95)';
225
+ setTimeout(() => {
226
+ if (toastEl.parentNode) {
227
+ toastEl.remove();
228
+ }
229
+ }, 200);
224
230
  }
225
231
 
226
232
  /**
227
233
  * Create default command palette DOM modal
228
234
  */
229
235
  function createCommandPaletteModal() {
230
- const backdrop = document.createElement('div');
231
- backdrop.className = 'command-backdrop';
232
- backdrop.innerHTML = `
236
+ const backdrop = document.createElement('div');
237
+ backdrop.className = 'command-backdrop';
238
+ backdrop.innerHTML = `
233
239
  <div class="command-dialog">
234
240
  <div class="command-input-wrapper">
235
241
  <span>🔍</span>
@@ -254,644 +260,1043 @@ function createCommandPaletteModal() {
254
260
  </div>
255
261
  `;
256
262
 
257
- document.body.appendChild(backdrop);
263
+ document.body.appendChild(backdrop);
258
264
 
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
- });
265
+ backdrop.addEventListener('click', (e) => {
266
+ if (e.target === backdrop) {
267
+ bluebird('command', { action: 'close' });
268
+ }
269
+ });
270
+
271
+ const input = backdrop.querySelector('.command-input');
272
+ input.addEventListener('input', (e) => {
273
+ const query = e.target.value.toLowerCase().trim();
274
+ const items = backdrop.querySelectorAll('.command-item');
275
+ items.forEach(item => {
276
+ const text = item.textContent.toLowerCase();
277
+ item.style.display = text.includes(query) ? 'flex' : 'none';
273
278
  });
279
+ });
274
280
 
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
- });
281
+ backdrop.addEventListener('click', (e) => {
282
+ const item = e.target.closest('.command-item');
283
+ if (item && item.getAttribute('data-navigate')) {
284
+ window.location.hash = item.getAttribute('data-navigate');
285
+ bluebird('command', { action: 'close' });
286
+ }
287
+ });
282
288
 
283
- return backdrop;
289
+ return backdrop;
284
290
  }
285
291
 
286
292
  /**
287
293
  * Remove backdrops whose target drawer no longer exists in DOM
288
294
  */
289
295
  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
+ document.querySelectorAll('.drawer-backdrop[data-for]').forEach(backdrop => {
297
+ const targetId = backdrop.getAttribute('data-for');
298
+ if (!document.getElementById(targetId)) {
299
+ backdrop.remove();
300
+ }
301
+ });
296
302
  }
297
303
 
298
304
  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
- }
305
+ const mainEl = document.querySelector('main');
306
+ const aside = mainEl ? mainEl.querySelector(':scope > aside') : null;
307
+ if (!aside) return;
308
+
309
+ let overlay = document.querySelector('.bluebird-drawer-overlay');
310
+ if (!overlay) {
311
+ overlay = document.createElement('div');
312
+ overlay.className = 'bluebird-drawer-overlay';
313
+ document.body.appendChild(overlay);
314
+ }
315
+
316
+ let drawer = document.querySelector('.bluebird-drawer');
317
+ if (!drawer) {
318
+ drawer = document.createElement('div');
319
+ drawer.className = 'bluebird-drawer';
320
+ document.body.appendChild(drawer);
321
+ }
322
+
323
+ drawer.innerHTML = aside.innerHTML;
324
+
325
+ let toggle = document.querySelector('.bluebird-drawer-toggle');
326
+ if (!toggle) {
327
+ toggle = document.createElement('button');
328
+ toggle.className = 'bluebird-drawer-toggle';
329
+ toggle.innerHTML = '☰';
330
+ toggle.setAttribute('aria-label', 'Toggle navigation menu');
331
+
332
+ const header = document.querySelector('header');
333
+ if (header) {
334
+ const nav = header.querySelector('nav');
335
+ if (nav) {
336
+ nav.insertBefore(toggle, nav.firstChild);
337
+ } else {
338
+ header.prepend(toggle);
339
+ }
340
+ } else {
341
+ document.body.prepend(toggle);
337
342
  }
343
+ }
338
344
  }
339
345
 
340
346
  // Global keyboard listener for Ctrl+K / Cmd+K Command Palette shortcut & ESC key
341
347
  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
- }
348
+ if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
349
+ e.preventDefault();
350
+ bluebird('command', { action: 'toggle' });
351
+ }
352
+
353
+ if (e.key === 'Escape') {
354
+ const commandBackdrop = document.querySelector('.command-backdrop.open');
355
+ if (commandBackdrop) {
356
+ bluebird('command', { action: 'close' });
352
357
  }
358
+ }
353
359
  });
354
360
 
355
361
  // Global click listener for Material Ripples, Mobile Navigation Drawer, Tabs, Popovers & Declarative Data Attributes
356
362
  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' });
363
+ // 1. Declarative Tab Trigger Click
364
+ const tabTrigger = e.target.closest('[data-tab-target], .tab-trigger');
365
+ if (tabTrigger) {
366
+ const targetId = tabTrigger.getAttribute('data-tab-target') || (tabTrigger.getAttribute('href') || '').replace('#', '');
367
+ if (targetId) {
368
+ e.preventDefault();
369
+ bluebird('tab', { id: targetId });
372
370
  }
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');
371
+ }
372
+
373
+ // 2. Declarative Popover Trigger Click
374
+ const popoverTrigger = e.target.closest('[data-popover-target]');
375
+ if (popoverTrigger) {
376
+ const popoverId = popoverTrigger.getAttribute('data-popover-target');
377
+ bluebird('popover', { id: popoverId, action: 'toggle' });
378
+ }
379
+
380
+ // Close open popovers when clicking outside
381
+ if (!e.target.closest('.popover') && !e.target.closest('[data-popover-target]')) {
382
+ document.querySelectorAll('.popover.open').forEach(p => p.classList.remove('open'));
383
+ }
384
+
385
+ // 3. Mobile Navigation Toggle Button Click
386
+ const mobileToggle = e.target.closest('.bluebird-drawer-toggle');
387
+ if (mobileToggle) {
388
+ e.preventDefault();
389
+ e.stopPropagation();
390
+ initMobileDrawer();
391
+ const drawer = document.querySelector('.bluebird-drawer');
392
+ const overlay = document.querySelector('.bluebird-drawer-overlay');
393
+ if (drawer && overlay) {
394
+ const isOpen = drawer.classList.contains('open');
395
+ if (isOpen) {
396
+ drawer.classList.remove('open');
397
+ overlay.classList.remove('open');
408
398
  document.body.style.overflow = '';
409
- return;
399
+ } else {
400
+ drawer.classList.add('open');
401
+ overlay.classList.add('open');
402
+ document.body.style.overflow = 'hidden';
403
+ }
410
404
  }
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
- }
405
+ return;
406
+ }
407
+
408
+ // 4. Mobile Navigation Overlay Click
409
+ if (e.target.closest('.bluebird-drawer-overlay')) {
410
+ const drawer = document.querySelector('.bluebird-drawer');
411
+ const overlay = document.querySelector('.bluebird-drawer-overlay');
412
+ if (drawer) drawer.classList.remove('open');
413
+ if (overlay) overlay.classList.remove('open');
414
+ document.body.style.overflow = '';
415
+ return;
416
+ }
417
+
418
+ // 5. Mobile Navigation Drawer Link Click
419
+ if (e.target.closest('.bluebird-drawer a')) {
420
+ const drawer = document.querySelector('.bluebird-drawer');
421
+ const overlay = document.querySelector('.bluebird-drawer-overlay');
422
+ if (drawer) drawer.classList.remove('open');
423
+ if (overlay) overlay.classList.remove('open');
424
+ document.body.style.overflow = '';
425
+ }
426
+
427
+ // Material Ripple Effect
428
+ const btn = e.target.closest("button, a[role='button']");
429
+ if (btn && !btn.classList.contains('fab') && !btn.classList.contains('carousel-nav') && !btn.classList.contains('bluebird-drawer-toggle')) {
430
+ const rect = btn.getBoundingClientRect();
431
+ const size = Math.max(rect.width, rect.height);
432
+ const x = e.clientX - rect.left - size / 2;
433
+ const y = e.clientY - rect.top - size / 2;
434
+
435
+ const ripple = document.createElement('span');
436
+ ripple.className = 'ripple';
437
+ ripple.style.width = ripple.style.height = size + 'px';
438
+ ripple.style.left = x + 'px';
439
+ ripple.style.top = y + 'px';
440
+
441
+ btn.appendChild(ripple);
442
+ ripple.addEventListener('animationend', () => ripple.remove());
443
+ }
444
+
445
+ // Declarative Standalone Drawer Triggers
446
+ const drawerTrigger = e.target.closest('[data-drawer-target]');
447
+ if (drawerTrigger) {
448
+ const id = drawerTrigger.getAttribute('data-drawer-target');
449
+ bluebird('drawer', { id, action: 'toggle' });
450
+ }
451
+
452
+ const drawerClose = e.target.closest('[data-drawer-close]');
453
+ if (drawerClose) {
454
+ const drawerEl = drawerClose.closest('.drawer');
455
+ if (drawerEl && drawerEl.id) {
456
+ bluebird('drawer', { id: drawerEl.id, action: 'close' });
452
457
  }
458
+ }
453
459
  });
454
460
 
455
461
  /**
456
462
  * Single Carousel Initialization logic with Touch/Swipe, Drag & Arrow Controls
457
463
  */
458
464
  function initSingleCarousel(carousel, opts = {}) {
459
- if (carousel._bb_initialized) return;
460
- carousel._bb_initialized = true;
465
+ if (carousel._bb_initialized) return;
466
+ carousel._bb_initialized = true;
461
467
 
462
- const track = carousel.querySelector('.carousel-track');
463
- if (!track) return;
468
+ const track = carousel.querySelector('.carousel-track');
469
+ if (!track) return;
464
470
 
465
- const items = Array.from(track.querySelectorAll('.carousel-item, .carousel-card'));
466
- if (items.length === 0) return;
471
+ const items = Array.from(track.querySelectorAll('.carousel-item, .carousel-card'));
472
+ if (items.length === 0) return;
467
473
 
468
- const prevBtn = carousel.querySelector('.carousel-prev');
469
- const nextBtn = carousel.querySelector('.carousel-next');
470
- let indicatorsContainer = carousel.querySelector('.carousel-indicators');
474
+ const prevBtn = carousel.querySelector('.carousel-prev');
475
+ const nextBtn = carousel.querySelector('.carousel-next');
476
+ let indicatorsContainer = carousel.querySelector('.carousel-indicators');
471
477
 
472
- let currentSlideIndex = 0;
478
+ let currentSlideIndex = 0;
473
479
 
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
- });
480
+ if (indicatorsContainer && indicatorsContainer.children.length === 0) {
481
+ items.forEach((_, idx) => {
482
+ const dot = document.createElement('button');
483
+ dot.className = `carousel-dot ${idx === 0 ? 'active' : ''}`;
484
+ dot.setAttribute('aria-label', `Go to slide ${idx + 1}`);
485
+ dot.addEventListener('click', (e) => {
486
+ e.preventDefault();
487
+ scrollToSlide(idx);
488
+ });
489
+ indicatorsContainer.appendChild(dot);
490
+ });
491
+ }
492
+
493
+ function scrollToSlide(index) {
494
+ if (index < 0) index = 0;
495
+ if (index >= items.length) index = items.length - 1;
496
+ currentSlideIndex = index;
497
+ const targetItem = items[index];
498
+ if (targetItem) {
499
+ track.scrollTo({
500
+ left: targetItem.offsetLeft - track.offsetLeft,
501
+ behavior: 'smooth'
502
+ });
503
+ updateIndicators(index);
485
504
  }
505
+ }
486
506
 
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);
507
+ function updateIndicators(activeIndex) {
508
+ if (!indicatorsContainer) return;
509
+ const dots = Array.from(indicatorsContainer.children);
510
+ dots.forEach((dot, idx) => {
511
+ dot.classList.toggle('active', idx === activeIndex);
512
+ });
513
+ }
514
+
515
+ let scrollTimeout;
516
+ track.addEventListener('scroll', () => {
517
+ clearTimeout(scrollTimeout);
518
+ scrollTimeout = setTimeout(() => {
519
+ const trackLeft = track.scrollLeft;
520
+ let closestIndex = 0;
521
+ let minDistance = Infinity;
522
+
523
+ items.forEach((item, idx) => {
524
+ const distance = Math.abs(item.offsetLeft - track.offsetLeft - trackLeft);
525
+ if (distance < minDistance) {
526
+ minDistance = distance;
527
+ closestIndex = idx;
498
528
  }
499
- }
529
+ });
530
+
531
+ currentSlideIndex = closestIndex;
532
+ updateIndicators(closestIndex);
533
+ }, 40);
534
+ });
535
+
536
+ if (prevBtn) {
537
+ prevBtn.addEventListener('click', (e) => {
538
+ e.preventDefault();
539
+ e.stopPropagation();
540
+ scrollToSlide(currentSlideIndex - 1);
541
+ });
542
+ }
500
543
 
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
- });
544
+ if (nextBtn) {
545
+ nextBtn.addEventListener('click', (e) => {
546
+ e.preventDefault();
547
+ e.stopPropagation();
548
+ scrollToSlide(currentSlideIndex + 1);
549
+ });
550
+ }
551
+
552
+ // Mobile Touch Swipe & Desktop Mouse Drag
553
+ let startX = 0;
554
+ let isDragging = false;
555
+
556
+ track.addEventListener('touchstart', (e) => {
557
+ startX = e.touches[0].clientX;
558
+ isDragging = true;
559
+ }, { passive: true });
560
+
561
+ track.addEventListener('touchend', (e) => {
562
+ if (!isDragging) return;
563
+ isDragging = false;
564
+ const endX = e.changedTouches[0].clientX;
565
+ const diffX = startX - endX;
566
+
567
+ if (Math.abs(diffX) > 35) {
568
+ if (diffX > 0) {
569
+ scrollToSlide(currentSlideIndex + 1);
570
+ } else {
571
+ scrollToSlide(currentSlideIndex - 1);
572
+ }
573
+ }
574
+ });
575
+
576
+ track.addEventListener('mousedown', (e) => {
577
+ startX = e.clientX;
578
+ isDragging = true;
579
+ track.style.cursor = 'grabbing';
580
+ });
581
+
582
+ track.addEventListener('mouseleave', () => {
583
+ isDragging = false;
584
+ track.style.cursor = 'grab';
585
+ });
586
+
587
+ track.addEventListener('mouseup', (e) => {
588
+ if (!isDragging) return;
589
+ isDragging = false;
590
+ track.style.cursor = 'grab';
591
+ const endX = e.clientX;
592
+ const diffX = startX - endX;
593
+
594
+ if (Math.abs(diffX) > 35) {
595
+ if (diffX > 0) {
596
+ scrollToSlide(currentSlideIndex + 1);
597
+ } else {
598
+ scrollToSlide(currentSlideIndex - 1);
599
+ }
507
600
  }
601
+ });
602
+
603
+ const isAutoplay = (opts && opts.autoplay) || carousel.getAttribute('data-autoplay') === 'true';
604
+ const intervalTime = parseInt((opts && opts.interval) || carousel.getAttribute('data-interval') || 3500, 10);
605
+
606
+ if (isAutoplay) {
607
+ let autoInterval = setInterval(() => {
608
+ const nextIdx = (currentSlideIndex + 1) % items.length;
609
+ scrollToSlide(nextIdx);
610
+ }, intervalTime);
611
+
612
+ carousel.addEventListener('mouseenter', () => clearInterval(autoInterval));
613
+ carousel.addEventListener('mouseleave', () => {
614
+ autoInterval = setInterval(() => {
615
+ const nextIdx = (currentSlideIndex + 1) % items.length;
616
+ scrollToSlide(nextIdx);
617
+ }, intervalTime);
618
+ });
619
+ }
620
+ }
508
621
 
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
- }
622
+ // --- DECLARATIVE DATA ATTRIBUTES & GLOBAL EVENT DELEGATION ---
623
+ (function setupDeclarativeListeners() {
624
+ // Click Delegations
625
+ document.addEventListener('click', (e) => {
626
+ // 1. Data-Copy
627
+ const copyTrigger = e.target.closest('[data-copy]');
628
+ if (copyTrigger) {
629
+ e.preventDefault();
630
+ const targetAttr = copyTrigger.getAttribute('data-copy');
631
+ let textToCopy = targetAttr;
632
+
633
+ if (targetAttr && (targetAttr.startsWith('#') || targetAttr.startsWith('.'))) {
634
+ const targetEl = document.querySelector(targetAttr);
635
+ if (targetEl) {
636
+ textToCopy = targetEl.value !== undefined ? targetEl.value : (targetEl.innerText || targetEl.textContent);
637
+ }
638
+ }
639
+
640
+ if (textToCopy) {
641
+ navigator.clipboard.writeText(textToCopy.trim()).then(() => {
642
+ copyTrigger.classList.add('copied');
643
+ if (typeof bluebird === 'function') {
644
+ bluebird('toast', {
645
+ title: 'Copied to clipboard',
646
+ description: textToCopy.length > 50 ? textToCopy.substring(0, 50) + '...' : textToCopy,
647
+ type: 'success',
648
+ duration: 2500
523
649
  });
650
+ }
651
+ setTimeout(() => copyTrigger.classList.remove('copied'), 2000);
652
+ });
653
+ }
654
+ return;
655
+ }
524
656
 
525
- currentSlideIndex = closestIndex;
526
- updateIndicators(closestIndex);
527
- }, 40);
528
- });
657
+ // 2. Data-Confirm (Prompt verification before action)
658
+ const confirmTrigger = e.target.closest('[data-confirm]');
659
+ if (confirmTrigger) {
660
+ const msg = confirmTrigger.getAttribute('data-confirm') || 'Are you sure?';
661
+ if (!window.confirm(msg)) {
662
+ e.preventDefault();
663
+ e.stopImmediatePropagation();
664
+ return;
665
+ }
666
+ }
529
667
 
530
- if (prevBtn) {
531
- prevBtn.addEventListener('click', (e) => {
532
- e.preventDefault();
533
- e.stopPropagation();
534
- scrollToSlide(currentSlideIndex - 1);
535
- });
668
+ // 3. Data-Scroll-To (Smooth scroll with header compensation)
669
+ const scrollTrigger = e.target.closest('[data-scroll-to]');
670
+ if (scrollTrigger) {
671
+ e.preventDefault();
672
+ const targetId = scrollTrigger.getAttribute('data-scroll-to');
673
+ const targetEl = document.querySelector(targetId);
674
+ if (targetEl) {
675
+ targetEl.scrollIntoView({ behavior: 'smooth', block: 'start' });
676
+ }
677
+ return;
536
678
  }
537
679
 
538
- if (nextBtn) {
539
- nextBtn.addEventListener('click', (e) => {
540
- e.preventDefault();
541
- e.stopPropagation();
542
- scrollToSlide(currentSlideIndex + 1);
543
- });
680
+ // 4. Data-Password-Toggle (Toggle password unmask)
681
+ const passToggle = e.target.closest('[data-password-toggle]');
682
+ if (passToggle) {
683
+ e.preventDefault();
684
+ const targetSelector = passToggle.getAttribute('data-password-toggle');
685
+ const input = targetSelector
686
+ ? document.querySelector(targetSelector)
687
+ : (passToggle.closest('.input-group, .form-input-group, div')?.querySelector('input') || passToggle.previousElementSibling);
688
+
689
+ if (input && (input.type === 'password' || input.type === 'text')) {
690
+ const isPassword = input.type === 'password';
691
+ input.type = isPassword ? 'text' : 'password';
692
+ passToggle.classList.toggle('showing', isPassword);
693
+ passToggle.setAttribute('aria-pressed', isPassword ? 'true' : 'false');
694
+ }
695
+ return;
544
696
  }
545
697
 
546
- // Mobile Touch Swipe & Desktop Mouse Drag
547
- let startX = 0;
548
- let isDragging = false;
698
+ // 5. Data-Step-Up / Data-Step-Down (Number Steppers)
699
+ const stepUp = e.target.closest('[data-step-up]');
700
+ if (stepUp) {
701
+ e.preventDefault();
702
+ const targetInput = document.querySelector(stepUp.getAttribute('data-step-up')) ||
703
+ stepUp.closest('.stepper')?.querySelector('input[type="number"]');
704
+ if (targetInput && typeof targetInput.stepUp === 'function') {
705
+ targetInput.stepUp();
706
+ targetInput.dispatchEvent(new Event('input', { bubbles: true }));
707
+ targetInput.dispatchEvent(new Event('change', { bubbles: true }));
708
+ }
709
+ return;
710
+ }
549
711
 
550
- track.addEventListener('touchstart', (e) => {
551
- startX = e.touches[0].clientX;
552
- isDragging = true;
553
- }, { passive: true });
712
+ const stepDown = e.target.closest('[data-step-down]');
713
+ if (stepDown) {
714
+ e.preventDefault();
715
+ const targetInput = document.querySelector(stepDown.getAttribute('data-step-down')) ||
716
+ stepDown.closest('.stepper')?.querySelector('input[type="number"]');
717
+ if (targetInput && typeof targetInput.stepDown === 'function') {
718
+ targetInput.stepDown();
719
+ targetInput.dispatchEvent(new Event('input', { bubbles: true }));
720
+ targetInput.dispatchEvent(new Event('change', { bubbles: true }));
721
+ }
722
+ return;
723
+ }
554
724
 
555
- track.addEventListener('touchend', (e) => {
556
- if (!isDragging) return;
557
- isDragging = false;
558
- const endX = e.changedTouches[0].clientX;
559
- const diffX = startX - endX;
725
+ // 6. Data-Select-Value (ComboBox / Select2 item selection)
726
+ const selectItem = e.target.closest('[data-select-value]');
727
+ if (selectItem) {
728
+ const val = selectItem.getAttribute('data-select-value');
729
+ const targetSelector = selectItem.getAttribute('data-select-target') ||
730
+ selectItem.closest('[data-select-container]')?.getAttribute('data-select-target');
731
+ if (targetSelector) {
732
+ const targetEl = document.querySelector(targetSelector);
733
+ if (targetEl) {
734
+ if (targetEl.tagName === 'INPUT' || targetEl.tagName === 'SELECT') {
735
+ targetEl.value = val;
736
+ targetEl.dispatchEvent(new Event('input', { bubbles: true }));
737
+ targetEl.dispatchEvent(new Event('change', { bubbles: true }));
738
+ } else {
739
+ targetEl.textContent = selectItem.textContent.trim();
740
+ }
741
+ }
742
+ }
743
+ // Close dropdown if inside one
744
+ const parentDropdown = selectItem.closest('.dropdown-content, .popover-content');
745
+ if (parentDropdown) {
746
+ parentDropdown.classList.remove('open');
747
+ }
748
+ }
560
749
 
561
- if (Math.abs(diffX) > 35) {
562
- if (diffX > 0) {
563
- scrollToSlide(currentSlideIndex + 1);
564
- } else {
565
- scrollToSlide(currentSlideIndex - 1);
566
- }
750
+ // 7. Data-Toggle / Modal Trigger
751
+ const modalTrigger = e.target.closest('[data-toggle="modal"], [data-modal-target], [data-dialog-target]');
752
+ if (modalTrigger) {
753
+ e.preventDefault();
754
+ const targetSelector = modalTrigger.getAttribute('data-modal-target') ||
755
+ modalTrigger.getAttribute('data-dialog-target') ||
756
+ modalTrigger.getAttribute('data-target') ||
757
+ modalTrigger.getAttribute('href');
758
+ if (targetSelector) {
759
+ const dialog = document.querySelector(targetSelector);
760
+ if (dialog && typeof dialog.showModal === 'function') {
761
+ dialog.showModal();
567
762
  }
568
- });
763
+ }
764
+ return;
765
+ }
569
766
 
570
- track.addEventListener('mousedown', (e) => {
571
- startX = e.clientX;
572
- isDragging = true;
573
- track.style.cursor = 'grabbing';
574
- });
767
+ // 8. Data-Dismiss / Modal Close
768
+ const dismissTrigger = e.target.closest('[data-dismiss="modal"], [data-close-dialog], [data-close-modal]');
769
+ if (dismissTrigger) {
770
+ e.preventDefault();
771
+ const dialog = dismissTrigger.closest('dialog') ||
772
+ document.querySelector(dismissTrigger.getAttribute('data-target') || '');
773
+ if (dialog && typeof dialog.close === 'function') {
774
+ dialog.close();
775
+ }
776
+ return;
777
+ }
575
778
 
576
- track.addEventListener('mouseleave', () => {
577
- isDragging = false;
578
- track.style.cursor = 'grab';
579
- });
779
+ // 9. Data-Toggle Theme
780
+ const themeTrigger = e.target.closest('[data-toggle="theme"]');
781
+ if (themeTrigger) {
782
+ e.preventDefault();
783
+ const html = document.documentElement;
784
+ const current = html.getAttribute('data-theme') || 'light';
785
+ const next = current === 'dark' ? 'light' : 'dark';
786
+ html.setAttribute('data-theme', next);
787
+ try {
788
+ localStorage.setItem('bluebird-theme', next);
789
+ } catch (err) { }
790
+ return;
791
+ }
580
792
 
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
- });
793
+ // 10. Data-Toast Trigger
794
+ const toastTrigger = e.target.closest('[data-toast]');
795
+ if (toastTrigger) {
796
+ e.preventDefault();
797
+ const desc = toastTrigger.getAttribute('data-toast') || '';
798
+ const title = toastTrigger.getAttribute('data-toast-title') || '';
799
+ const type = toastTrigger.getAttribute('data-toast-type') || 'info';
800
+ if (typeof bluebird === 'function') {
801
+ bluebird('toast', { title, description: desc, type });
802
+ }
803
+ return;
804
+ }
596
805
 
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
- });
806
+ // 11. Data-Snackbar Trigger
807
+ const snackbarTrigger = e.target.closest('[data-snackbar]');
808
+ if (snackbarTrigger) {
809
+ e.preventDefault();
810
+ const message = snackbarTrigger.getAttribute('data-snackbar') || '';
811
+ const type = snackbarTrigger.getAttribute('data-snackbar-type') || 'info';
812
+ if (typeof bluebird === 'function') {
813
+ bluebird('snackbar', { message, type });
814
+ }
815
+ return;
613
816
  }
614
- }
615
817
 
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
- }
818
+ // 12. Click outside dropdown / popover auto-close
819
+ if (!e.target.closest('.dropdown') && !e.target.closest('.popover')) {
820
+ document.querySelectorAll('.dropdown-content.open, .popover-content.open').forEach(el => {
821
+ el.classList.remove('open');
822
+ });
823
+ }
824
+ });
825
+
826
+ // Live Input Event Delegations (Filter Target & Auto-Resize Textarea)
827
+ document.addEventListener('input', (e) => {
828
+ // A. Real-time List/Table/ComboBox Filtering (data-filter-target="#lista")
829
+ const filterInput = e.target.closest('[data-filter-target]');
830
+ if (filterInput) {
831
+ const targetSelector = filterInput.getAttribute('data-filter-target');
832
+ const targetContainer = document.querySelector(targetSelector);
833
+ if (targetContainer) {
834
+ const query = filterInput.value.toLowerCase().trim();
835
+ const items = targetContainer.querySelectorAll('[data-filter-item], li, tr, .card, .dropdown-item, .item');
836
+ let visibleCount = 0;
650
837
 
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
- }
838
+ items.forEach(item => {
839
+ const text = item.textContent.toLowerCase();
840
+ const matches = text.includes(query);
841
+ item.style.display = matches ? '' : 'none';
842
+ if (matches) visibleCount++;
843
+ });
661
844
 
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;
845
+ const emptyMsg = targetContainer.querySelector('.no-filter-results');
846
+ if (emptyMsg) {
847
+ emptyMsg.style.display = visibleCount === 0 ? 'block' : 'none';
672
848
  }
849
+ }
850
+ }
673
851
 
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
- }
852
+ // B. Auto-Resize Textarea (data-auto-resize)
853
+ if (e.target.matches('textarea[data-auto-resize]')) {
854
+ const textarea = e.target;
855
+ textarea.style.height = 'auto';
856
+ textarea.style.height = (textarea.scrollHeight + 2) + 'px';
857
+ }
858
+ });
691
859
 
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
- }
860
+ // Global Ctrl+K / Cmd+K listener
861
+ document.addEventListener('keydown', (e) => {
862
+ if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
863
+ e.preventDefault();
864
+ if (typeof bluebird === 'function') {
865
+ bluebird('command', { action: 'toggle' });
866
+ }
867
+ } else if (e.key === 'Escape') {
868
+ const openCommand = document.querySelector('.command-backdrop.open');
869
+ if (openCommand && typeof bluebird === 'function') {
870
+ bluebird('command', { action: 'close' });
871
+ }
872
+ }
873
+ });
874
+ })();
705
875
 
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
- }
876
+ // Auto Setup Helper Elements on DOMReady
877
+ (function () {
878
+ function init() {
879
+ cleanupOrphanedBackdrops();
880
+ initMobileDrawer();
881
+ document.querySelectorAll('.carousel').forEach(c => initSingleCarousel(c));
882
+
883
+ // Auto resize textareas on init
884
+ document.querySelectorAll('textarea[data-auto-resize]').forEach(t => {
885
+ t.style.height = 'auto';
886
+ t.style.height = (t.scrollHeight + 2) + 'px';
887
+ });
718
888
 
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
- }
889
+ // Restore saved theme if available
890
+ try {
891
+ const savedTheme = localStorage.getItem('bluebird-theme');
892
+ if (savedTheme) {
893
+ document.documentElement.setAttribute('data-theme', savedTheme);
894
+ }
895
+ } catch (e) { }
896
+ }
897
+
898
+ if (document.readyState === 'loading') {
899
+ document.addEventListener('DOMContentLoaded', () => setTimeout(init, 100));
900
+ } else {
901
+ setTimeout(init, 100);
902
+ }
903
+ })();
743
904
 
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
- }
905
+ /**
906
+ * Modern fetch wrapper with automatic CSRF token support and JSON error handling.
907
+ * @param {string} [url="/"] - Target endpoint URL.
908
+ * @param {string} [method="GET"] - HTTP method.
909
+ * @param {object|boolean} [body=false] - Request JSON body payload.
910
+ * @param {FormData|boolean} [bodyForm=false] - Request FormData body payload.
911
+ * @param {object} [headers={}] - Custom headers.
912
+ * @returns {Promise<any>} Parsed JSON response.
913
+ */
914
+ async function Http(
915
+ url = "/",
916
+ method = "GET",
917
+ body = false,
918
+ bodyForm = false,
919
+ headers = {},
920
+ ) {
921
+ const csrfEl = document.getElementById("csrf");
922
+ const csrfToken = csrfEl ? csrfEl.value : null;
923
+
924
+ const mergedHeaders = { ...headers };
925
+ if (csrfToken) {
926
+ mergedHeaders["X-CSRF-Token"] = csrfToken;
927
+ }
928
+
929
+ const options = { method: method, headers: mergedHeaders, credentials: "include" };
930
+
931
+ if (body) {
932
+ const payload = csrfToken ? { ...body, csrf: csrfToken } : body;
933
+ options["body"] = JSON.stringify(payload);
934
+ if (!mergedHeaders["Content-Type"]) {
935
+ mergedHeaders["Content-Type"] = "application/json";
936
+ }
937
+ }
760
938
 
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
- }
939
+ if (bodyForm) {
940
+ if (csrfToken && bodyForm instanceof FormData) {
941
+ bodyForm.append("csrf", csrfToken);
942
+ }
943
+ options["body"] = bodyForm;
944
+ }
945
+
946
+ const response = await fetch(url, options);
947
+ if (!response.ok) {
948
+ let errorData;
949
+ try {
950
+ errorData = await response.json();
951
+ } catch {
952
+ errorData = { message: `HTTP Error ${response.status}: ${response.statusText}` };
953
+ }
954
+ throw new Error(errorData.message || errorData.msg || errorData.mensaje || "Fetch request failed");
955
+ }
956
+ return await response.json();
957
+ }
772
958
 
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
- }
959
+ /**
960
+ * Extracts a query parameter from the current URL search string.
961
+ * @param {string} name - Query parameter key name.
962
+ * @returns {string|null} Parameter value or null.
963
+ */
964
+ function getUrlParameter(name) {
965
+ return new URLSearchParams(window.location.search).get(name);
966
+ }
786
967
 
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
- }
968
+ /**
969
+ * Checks if current document language starts with the specified code.
970
+ * @param {string} [l="es"] - Language prefix.
971
+ * @returns {boolean}
972
+ */
973
+ function lang(l = "es") {
974
+ const docLang = document.documentElement.lang || "es";
975
+ return docLang === l || docLang.startsWith(l);
976
+ }
799
977
 
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
- }
978
+ /**
979
+ * Responsive Data Table component with mobile card switching, live search, and pagination.
980
+ */
981
+ class ResponsiveDataTable {
982
+ constructor(containerId, options = {}) {
983
+ this.container = typeof containerId === "string" ? document.getElementById(containerId) : containerId;
984
+ if (!this.container) return;
985
+ this.defaults = {
986
+ data: [],
987
+ columns: [],
988
+ rowsPerPage: 10,
989
+ search: true,
990
+ pagination: true,
991
+ headerTitles: {},
992
+ summaryFields: ["id"],
993
+ edit: false,
994
+ delete: false,
995
+ breakpoint: 768,
996
+ };
997
+ this.options = { ...this.defaults, ...options };
998
+ this.currentPage = 1;
999
+ this.filteredData = [...this.options.data];
1000
+ this.isMobile = window.innerWidth < this.options.breakpoint;
1001
+ this.init();
1002
+ window.addEventListener("resize", () => this.handleResize());
1003
+ }
1004
+
1005
+ init() {
1006
+ this.renderContainer();
1007
+ this.updateTable();
1008
+ if (this.options.search) this.setupSearch();
1009
+ }
1010
+
1011
+ handleResize() {
1012
+ const wasMobile = this.isMobile;
1013
+ this.isMobile = window.innerWidth < this.options.breakpoint;
1014
+ if (wasMobile !== this.isMobile) this.updateTable();
1015
+ }
1016
+
1017
+ renderContainer() {
1018
+ this.container.innerHTML = `
1019
+ <section class="w-full">
1020
+ ${this.options.search
1021
+ ? `<div class="mb-4 flex items-center justify-between"><input type="search" class="datatable-search-input outline" placeholder="${lang() ? "Buscar..." : "Search..."}" aria-label="Search"/></div>`
1022
+ : ""
1023
+ }
1024
+
1025
+ <div class="overflow-x-auto">
1026
+ <table class="datatable-table hidden"></table>
1027
+ <div class="datatable-mobile"></div>
1028
+ </div>
811
1029
 
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
- }
1030
+ ${this.options.pagination ? `<nav class="datatable-pagination mt-4 flex items-center justify-center gap-1" aria-label="Pagination"></nav>` : ""}
1031
+ </section>`;
1032
+ }
1033
+
1034
+ renderTable() {
1035
+ const table = this.container.querySelector(".datatable-table");
1036
+ const mobileView = this.container.querySelector(".datatable-mobile");
1037
+ if (!table || !mobileView) return;
1038
+ if (this.isMobile) {
1039
+ table.classList.add("hidden");
1040
+ mobileView.classList.remove("hidden");
1041
+ this.renderMobileView();
1042
+ } else {
1043
+ table.classList.remove("hidden");
1044
+ mobileView.classList.add("hidden");
1045
+ this.renderDesktopTable();
1046
+ }
1047
+ }
1048
+
1049
+ renderDesktopTable() {
1050
+ const table = this.container.querySelector(".datatable-table");
1051
+ table.innerHTML = `
1052
+ <thead>
1053
+ <tr class="datatable-header"></tr>
1054
+ </thead>
1055
+ <tbody class="datatable-body"></tbody>`;
1056
+ const headerRow = table.querySelector("thead tr");
1057
+ this.options.columns.forEach((column) => {
1058
+ const th = document.createElement("th");
1059
+ th.scope = "col";
1060
+ th.textContent =
1061
+ this.options.headerTitles[column.key] || column.title || column.key;
1062
+ headerRow.appendChild(th);
818
1063
  });
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
- }
1064
+ if (this.options.edit || this.options.delete) {
1065
+ const th = document.createElement("th");
1066
+ th.scope = "col";
1067
+ th.textContent = lang() ? "Acciones" : "Actions";
1068
+ headerRow.appendChild(th);
1069
+ }
1070
+ const startIndex = (this.currentPage - 1) * this.options.rowsPerPage;
1071
+ const endIndex = startIndex + this.options.rowsPerPage;
1072
+ const paginatedData = this.filteredData.slice(startIndex, endIndex);
1073
+ const tbody = table.querySelector("tbody");
1074
+ paginatedData.forEach((item) => {
1075
+ const row = document.createElement("tr");
1076
+ this.options.columns.forEach((column) => {
1077
+ const td = document.createElement("td");
1078
+ const value = item[column.key];
1079
+ if (
1080
+ value &&
1081
+ typeof value === "string" &&
1082
+ /<[a-z][\s\S]*>/i.test(value)
1083
+ ) {
1084
+ td.innerHTML = value;
1085
+ } else {
1086
+ td.textContent = value !== undefined && value !== null ? value : "-";
844
1087
  }
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';
1088
+ row.appendChild(td);
1089
+ });
1090
+ if (this.options.edit || this.options.delete) {
1091
+ const td = document.createElement("td");
1092
+ const actionsDiv = document.createElement("div");
1093
+ actionsDiv.className = "flex items-center gap-2";
1094
+ if (this.options.edit) {
1095
+ const btn = document.createElement("button");
1096
+ btn.className = "outline";
1097
+ btn.textContent = lang() ? "Editar" : "Edit";
1098
+ btn.onclick = (e) => this.handleAction("edit", e, item);
1099
+ actionsDiv.appendChild(btn);
1100
+ }
1101
+ if (this.options.delete) {
1102
+ const btn = document.createElement("button");
1103
+ btn.className = "destructive";
1104
+ btn.textContent = lang() ? "Eliminar" : "Delete";
1105
+ btn.onclick = (e) => this.handleAction("delete", e, item);
1106
+ actionsDiv.appendChild(btn);
851
1107
  }
1108
+ td.appendChild(actionsDiv);
1109
+ row.appendChild(td);
1110
+ }
1111
+ tbody.appendChild(row);
852
1112
  });
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
- }
1113
+ }
1114
+
1115
+ renderMobileView() {
1116
+ const mobileView = this.container.querySelector(".datatable-mobile");
1117
+ mobileView.innerHTML = "";
1118
+ const startIndex = (this.currentPage - 1) * this.options.rowsPerPage;
1119
+ const endIndex = startIndex + this.options.rowsPerPage;
1120
+ const paginatedData = this.filteredData.slice(startIndex, endIndex);
1121
+ paginatedData.forEach((item) => {
1122
+ const card = document.createElement("article");
1123
+ card.className = "card mb-4";
1124
+ const summary = document.createElement("h3");
1125
+ summary.className =
1126
+ "flex items-center justify-between font-bold mb-2 pb-2";
1127
+ this.options.summaryFields.forEach((fieldKey) => {
1128
+ const value = item[fieldKey];
1129
+ summary.innerHTML += `<span>${value !== undefined && value !== null ? value : "-"}</span>`;
1130
+ });
1131
+ card.appendChild(summary);
1132
+ const details = document.createElement("dl");
1133
+ details.className =
1134
+ "grid cols-1 gap-2 mb-2 pb-2";
1135
+ this.options.columns.forEach((column) => {
1136
+ if (this.options.summaryFields.includes(column.key)) return;
1137
+ const dt = document.createElement("dt");
1138
+ dt.className =
1139
+ "font-bold text-muted";
1140
+ dt.textContent =
1141
+ this.options.headerTitles[column.key] || column.title || column.key;
1142
+ const dd = document.createElement("dd");
1143
+ dd.className =
1144
+ "text-left";
1145
+ const cellValue = item[column.key];
1146
+ if (
1147
+ cellValue &&
1148
+ typeof cellValue === "string" &&
1149
+ /<[a-z][\s\S]*>/i.test(cellValue)
1150
+ ) {
1151
+ dd.innerHTML = cellValue;
1152
+ } else {
1153
+ dd.textContent = cellValue !== undefined && cellValue !== null ? cellValue : "-";
1154
+ }
1155
+ details.appendChild(dt);
1156
+ details.appendChild(dd);
1157
+ });
1158
+ card.appendChild(details);
1159
+ if (this.options.edit || this.options.delete) {
1160
+ const actions = document.createElement("div");
1161
+ actions.className = "flex items-center gap-2 justify-end";
1162
+ if (this.options.edit) {
1163
+ const btn = document.createElement("button");
1164
+ btn.className = "outline";
1165
+ btn.textContent = lang() ? "Editar" : "Edit";
1166
+ btn.onclick = (e) => this.handleAction("edit", e, item);
1167
+ actions.appendChild(btn);
1168
+ }
1169
+ if (this.options.delete) {
1170
+ const btn = document.createElement("button");
1171
+ btn.className = "destructive";
1172
+ btn.textContent = lang() ? "Eliminar" : "Delete";
1173
+ btn.onclick = (e) => this.handleAction("delete", e, item);
1174
+ actions.appendChild(btn);
866
1175
  }
1176
+ card.appendChild(actions);
1177
+ }
1178
+ mobileView.appendChild(card);
867
1179
  });
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) { }
1180
+ }
1181
+
1182
+ renderPagination() {
1183
+ const pagination = this.container.querySelector(".datatable-pagination");
1184
+ if (!pagination || !this.options.pagination) return;
1185
+ pagination.innerHTML = "";
1186
+ const pageCount = Math.ceil(
1187
+ this.filteredData.length / this.options.rowsPerPage,
1188
+ );
1189
+ if (pageCount <= 1) return;
1190
+
1191
+ const baseClass = "px-3 py-2 outline";
1192
+ const activeClass = "px-3 py-2";
1193
+
1194
+ const prevButton = document.createElement("button");
1195
+ prevButton.textContent = "«";
1196
+ prevButton.className = baseClass;
1197
+ prevButton.disabled = this.currentPage === 1;
1198
+ prevButton.onclick = () => this.changePage(this.currentPage - 1);
1199
+ pagination.appendChild(prevButton);
1200
+
1201
+ const maxVisible = 5;
1202
+ let start = Math.max(1, this.currentPage - Math.floor(maxVisible / 2));
1203
+ let end = start + maxVisible - 1;
1204
+ if (end > pageCount) {
1205
+ end = pageCount;
1206
+ start = Math.max(1, end - maxVisible + 1);
890
1207
  }
891
-
892
- if (document.readyState === 'loading') {
893
- document.addEventListener('DOMContentLoaded', () => setTimeout(init, 100));
894
- } else {
895
- setTimeout(init, 100);
1208
+ if (start > 1) {
1209
+ const firstButton = document.createElement("button");
1210
+ firstButton.className = baseClass;
1211
+ firstButton.textContent = "1";
1212
+ firstButton.onclick = () => this.changePage(1);
1213
+ pagination.appendChild(firstButton);
1214
+ if (start > 2) pagination.appendChild(this.createEllipsis());
896
1215
  }
897
- })();
1216
+ for (let i = start; i <= end; i++) {
1217
+ const button = document.createElement("button");
1218
+ button.className =
1219
+ i === this.currentPage ? activeClass : baseClass;
1220
+ button.textContent = i;
1221
+ button.onclick = () => this.changePage(i);
1222
+ pagination.appendChild(button);
1223
+ }
1224
+ if (end < pageCount) {
1225
+ if (end < pageCount - 1) pagination.appendChild(this.createEllipsis());
1226
+ const lastButton = document.createElement("button");
1227
+ lastButton.className = baseClass;
1228
+ lastButton.textContent = pageCount;
1229
+ lastButton.onclick = () => this.changePage(pageCount);
1230
+ pagination.appendChild(lastButton);
1231
+ }
1232
+ const nextButton = document.createElement("button");
1233
+ nextButton.className = baseClass;
1234
+ nextButton.textContent = "»";
1235
+ nextButton.disabled = this.currentPage === pageCount;
1236
+ nextButton.onclick = () => this.changePage(this.currentPage + 1);
1237
+ pagination.appendChild(nextButton);
1238
+ }
1239
+
1240
+ createEllipsis() {
1241
+ const span = document.createElement("span");
1242
+ span.className = "px-2 text-muted";
1243
+ span.textContent = "...";
1244
+ return span;
1245
+ }
1246
+
1247
+ changePage(page) {
1248
+ this.currentPage = page;
1249
+ this.updateTable();
1250
+ }
1251
+
1252
+ handleAction(type, event, item) {
1253
+ if (!this.options[type]) return;
1254
+ const callback =
1255
+ typeof this.options[type] === "function"
1256
+ ? this.options[type]
1257
+ : window[this.options[type]];
1258
+ if (typeof callback === "function") callback(event, item.id || item);
1259
+ }
1260
+
1261
+ setupSearch() {
1262
+ const searchInput = this.container.querySelector(".datatable-search-input");
1263
+ if (!searchInput) return;
1264
+ searchInput.addEventListener("input", (e) => {
1265
+ const term = e.target.value.toLowerCase().trim();
1266
+ this.filteredData = this.options.data.filter((item) =>
1267
+ this.options.columns.some((column) =>
1268
+ String(item[column.key] || "")
1269
+ .toLowerCase()
1270
+ .includes(term),
1271
+ ),
1272
+ );
1273
+ this.currentPage = 1;
1274
+ this.updateTable();
1275
+ });
1276
+ }
1277
+
1278
+ updateTable() {
1279
+ this.renderTable();
1280
+ if (this.options.pagination) this.renderPagination();
1281
+ }
1282
+
1283
+ updateData(newData) {
1284
+ this.options.data = newData;
1285
+ this.filteredData = [...newData];
1286
+ this.currentPage = 1;
1287
+ this.updateTable();
1288
+ }
1289
+
1290
+ updateColumns(newColumns) {
1291
+ this.options.columns = newColumns;
1292
+ this.updateTable();
1293
+ }
1294
+ }
1295
+ if (typeof window !== "undefined") {
1296
+ window.ResponsiveDataTable = ResponsiveDataTable;
1297
+ window.Http = Http;
1298
+ window.getUrlParameter = getUrlParameter;
1299
+ window.snackbar = snackbar;
1300
+ window.toast = toast;
1301
+ window.bluebird = bluebird;
1302
+ }