softui-css 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/softui.js ADDED
@@ -0,0 +1,2844 @@
1
+ /*! SoftUI v1.1.0 — Interactive Behaviors */
2
+
3
+ const SoftUI = (() => {
4
+ function pad(n) { return n < 10 ? '0' + n : '' + n; }
5
+
6
+ // =========================================
7
+ // Modal
8
+ // =========================================
9
+ function modal(selector) {
10
+ const backdrop = document.querySelector(selector);
11
+ if (!backdrop) return null;
12
+
13
+ let previouslyFocused = null;
14
+
15
+ function open() {
16
+ previouslyFocused = document.activeElement;
17
+ backdrop.classList.add('sui-modal-open');
18
+ document.body.style.overflow = 'hidden';
19
+
20
+ // Focus the first focusable element inside the modal
21
+ const modal = backdrop.querySelector('.sui-modal');
22
+ if (modal) {
23
+ const first = getFocusable(modal)[0];
24
+ if (first) first.focus();
25
+ }
26
+ }
27
+
28
+ function close() {
29
+ backdrop.classList.remove('sui-modal-open');
30
+ document.body.style.overflow = '';
31
+
32
+ // Restore focus to the element that opened the modal
33
+ if (previouslyFocused) {
34
+ previouslyFocused.focus();
35
+ previouslyFocused = null;
36
+ }
37
+ }
38
+
39
+ function isOpen() {
40
+ return backdrop.classList.contains('sui-modal-open');
41
+ }
42
+
43
+ return { open, close, isOpen };
44
+ }
45
+
46
+ // =========================================
47
+ // Sheet / Drawer
48
+ // =========================================
49
+ function sheet(selector) {
50
+ const backdrop = document.querySelector(selector);
51
+ if (!backdrop) return null;
52
+
53
+ let previouslyFocused = null;
54
+
55
+ function open() {
56
+ previouslyFocused = document.activeElement;
57
+ backdrop.classList.add('sui-sheet-open');
58
+ document.body.style.overflow = 'hidden';
59
+
60
+ const panel = backdrop.querySelector('.sui-sheet');
61
+ if (panel) {
62
+ const first = getFocusable(panel)[0];
63
+ if (first) first.focus();
64
+ }
65
+ }
66
+
67
+ function close() {
68
+ backdrop.classList.remove('sui-sheet-open');
69
+ document.body.style.overflow = '';
70
+
71
+ if (previouslyFocused) {
72
+ previouslyFocused.focus();
73
+ previouslyFocused = null;
74
+ }
75
+ }
76
+
77
+ function isOpen() {
78
+ return backdrop.classList.contains('sui-sheet-open');
79
+ }
80
+
81
+ return { open, close, isOpen };
82
+ }
83
+
84
+ // =========================================
85
+ // Focus trap helper
86
+ // =========================================
87
+ function getFocusable(container) {
88
+ return Array.from(container.querySelectorAll(
89
+ 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
90
+ ));
91
+ }
92
+
93
+ // =========================================
94
+ // Global listeners (auto-initialized)
95
+ // =========================================
96
+ function init() {
97
+ // Escape key closes any open modal or sheet
98
+ document.addEventListener('keydown', (e) => {
99
+ if (e.key === 'Escape') {
100
+ const openModal = document.querySelector('.sui-modal-backdrop.sui-modal-open');
101
+ if (openModal) {
102
+ openModal.classList.remove('sui-modal-open');
103
+ document.body.style.overflow = '';
104
+ }
105
+ const openSheet = document.querySelector('.sui-sheet-backdrop.sui-sheet-open');
106
+ if (openSheet && !openSheet.classList.contains('sui-sheet-static')) {
107
+ openSheet.classList.remove('sui-sheet-open');
108
+ document.body.style.overflow = '';
109
+ }
110
+ }
111
+ });
112
+
113
+ // Click backdrop to close (or shake if static)
114
+ document.addEventListener('click', (e) => {
115
+ if (e.target.classList.contains('sui-modal-backdrop') && e.target.classList.contains('sui-modal-open')) {
116
+ if (e.target.classList.contains('sui-modal-static')) {
117
+ e.target.classList.add('sui-modal-shake');
118
+ setTimeout(() => e.target.classList.remove('sui-modal-shake'), 200);
119
+ } else {
120
+ e.target.classList.remove('sui-modal-open');
121
+ document.body.style.overflow = '';
122
+ }
123
+ }
124
+ });
125
+
126
+ // Focus trap inside open modals
127
+ document.addEventListener('keydown', (e) => {
128
+ if (e.key !== 'Tab') return;
129
+
130
+ const backdrop = document.querySelector('.sui-modal-backdrop.sui-modal-open');
131
+ if (!backdrop) return;
132
+
133
+ const modal = backdrop.querySelector('.sui-modal');
134
+ if (!modal) return;
135
+
136
+ const focusable = getFocusable(modal);
137
+ if (focusable.length === 0) return;
138
+
139
+ const first = focusable[0];
140
+ const last = focusable[focusable.length - 1];
141
+
142
+ if (e.shiftKey) {
143
+ if (document.activeElement === first) {
144
+ e.preventDefault();
145
+ last.focus();
146
+ }
147
+ } else {
148
+ if (document.activeElement === last) {
149
+ e.preventDefault();
150
+ first.focus();
151
+ }
152
+ }
153
+ });
154
+
155
+ // Close button handler (any .sui-modal-close inside a backdrop)
156
+ document.addEventListener('click', (e) => {
157
+ const closeBtn = e.target.closest('.sui-modal-close');
158
+ if (!closeBtn) return;
159
+
160
+ const backdrop = closeBtn.closest('.sui-modal-backdrop');
161
+ if (backdrop) {
162
+ backdrop.classList.remove('sui-modal-open');
163
+ document.body.style.overflow = '';
164
+ }
165
+ });
166
+
167
+ // Sheet backdrop click to close (or shake if static)
168
+ document.addEventListener('click', (e) => {
169
+ if (e.target.classList.contains('sui-sheet-backdrop') && e.target.classList.contains('sui-sheet-open')) {
170
+ if (e.target.classList.contains('sui-sheet-static')) {
171
+ e.target.classList.add('sui-sheet-shake');
172
+ setTimeout(() => e.target.classList.remove('sui-sheet-shake'), 200);
173
+ } else {
174
+ e.target.classList.remove('sui-sheet-open');
175
+ document.body.style.overflow = '';
176
+ }
177
+ }
178
+ });
179
+
180
+ // Sheet close button handler
181
+ document.addEventListener('click', (e) => {
182
+ const closeBtn = e.target.closest('.sui-sheet-close');
183
+ if (!closeBtn) return;
184
+
185
+ const backdrop = closeBtn.closest('.sui-sheet-backdrop');
186
+ if (backdrop) {
187
+ backdrop.classList.remove('sui-sheet-open');
188
+ document.body.style.overflow = '';
189
+ }
190
+ });
191
+
192
+ // Focus trap inside open sheets
193
+ document.addEventListener('keydown', (e) => {
194
+ if (e.key !== 'Tab') return;
195
+
196
+ const sheetBackdrop = document.querySelector('.sui-sheet-backdrop.sui-sheet-open');
197
+ if (!sheetBackdrop) return;
198
+
199
+ const panel = sheetBackdrop.querySelector('.sui-sheet');
200
+ if (!panel) return;
201
+
202
+ const focusable = getFocusable(panel);
203
+ if (focusable.length === 0) return;
204
+
205
+ const first = focusable[0];
206
+ const last = focusable[focusable.length - 1];
207
+
208
+ if (e.shiftKey) {
209
+ if (document.activeElement === first) {
210
+ e.preventDefault();
211
+ last.focus();
212
+ }
213
+ } else {
214
+ if (document.activeElement === last) {
215
+ e.preventDefault();
216
+ first.focus();
217
+ }
218
+ }
219
+ });
220
+
221
+ // Dismissible alerts
222
+ document.addEventListener('click', (e) => {
223
+ const closeBtn = e.target.closest('.sui-alert-close');
224
+ if (!closeBtn) return;
225
+
226
+ const alert = closeBtn.closest('.sui-alert');
227
+ if (alert) {
228
+ alert.style.opacity = '0';
229
+ alert.style.transform = 'translateX(20px)';
230
+ alert.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
231
+ setTimeout(() => alert.remove(), 300);
232
+ }
233
+ });
234
+
235
+ // Dismissible chips
236
+ document.addEventListener('click', (e) => {
237
+ const closeBtn = e.target.closest('.sui-chip-close');
238
+ if (!closeBtn) return;
239
+
240
+ const chip = closeBtn.closest('.sui-chip');
241
+ if (chip) {
242
+ chip.classList.add('sui-chip-removing');
243
+ setTimeout(() => chip.remove(), 250);
244
+ }
245
+ });
246
+
247
+ // Tabs
248
+ initTabs();
249
+
250
+ // Accordion
251
+ initAccordion();
252
+
253
+ // Collapsible
254
+ initCollapsible();
255
+
256
+ // Dropdown
257
+ initDropdown();
258
+
259
+ // Context Menu
260
+ initContextMenu();
261
+
262
+ // Command Palette
263
+ initCommand();
264
+
265
+ // Calendar
266
+ initCalendar();
267
+ initTimePicker();
268
+
269
+ // Menubar
270
+ initMenubar();
271
+
272
+ // Combobox
273
+ initCombobox();
274
+
275
+ // Resizable
276
+ initResizable();
277
+
278
+ // Popover
279
+ initPopover();
280
+
281
+ // Carousels
282
+ initCarousels();
283
+
284
+ // Sliders
285
+ initSliders();
286
+
287
+ // Toggle Groups
288
+ initToggleGroups();
289
+
290
+ // Input OTP
291
+ initOtp();
292
+
293
+ // Charts
294
+ initCharts();
295
+
296
+ // Data Tables
297
+ initDataTables();
298
+
299
+ // Drag & Drop
300
+ initDragDrop();
301
+
302
+ // Tooltip auto-positioning
303
+ document.addEventListener('mouseenter', (e) => {
304
+ if (!e.target.closest) return;
305
+ const tip = e.target.closest('.sui-tooltip');
306
+ if (!tip) return;
307
+
308
+ const rect = tip.getBoundingClientRect();
309
+ const margin = 80; // space needed for tooltip
310
+
311
+ // Store original direction so we can restore on leave
312
+ const original = tip.dataset.suiTooltipDir;
313
+ if (original === undefined) {
314
+ if (tip.classList.contains('sui-tooltip-bottom')) tip.dataset.suiTooltipDir = 'bottom';
315
+ else if (tip.classList.contains('sui-tooltip-left')) tip.dataset.suiTooltipDir = 'left';
316
+ else if (tip.classList.contains('sui-tooltip-right')) tip.dataset.suiTooltipDir = 'right';
317
+ else tip.dataset.suiTooltipDir = 'top';
318
+ }
319
+
320
+ // Remove all direction classes
321
+ tip.classList.remove('sui-tooltip-bottom', 'sui-tooltip-left', 'sui-tooltip-right');
322
+
323
+ // Determine best direction
324
+ const dir = tip.dataset.suiTooltipDir;
325
+ let best = dir;
326
+
327
+ if (dir === 'top' && rect.top < margin) best = 'bottom';
328
+ else if (dir === 'bottom' && rect.bottom + margin > window.innerHeight) best = 'top';
329
+ else if (dir === 'left' && rect.left < margin) best = 'right';
330
+ else if (dir === 'right' && rect.right + margin > window.innerWidth) best = 'left';
331
+
332
+ // Apply direction (top is default, no class needed)
333
+ if (best !== 'top') {
334
+ tip.classList.add('sui-tooltip-' + best);
335
+ }
336
+ }, true);
337
+
338
+ document.addEventListener('mouseleave', (e) => {
339
+ if (!e.target.closest) return;
340
+ const tip = e.target.closest('.sui-tooltip');
341
+ if (!tip || !tip.dataset.suiTooltipDir) return;
342
+
343
+ // Delay restore until after fade-out transition completes
344
+ setTimeout(() => {
345
+ if (tip.matches(':hover')) return;
346
+ tip.classList.remove('sui-tooltip-bottom', 'sui-tooltip-left', 'sui-tooltip-right');
347
+ const dir = tip.dataset.suiTooltipDir;
348
+ if (dir !== 'top') {
349
+ tip.classList.add('sui-tooltip-' + dir);
350
+ }
351
+ }, 200);
352
+ }, true);
353
+
354
+ // Hover Card auto-repositioning
355
+ document.addEventListener('mouseenter', (e) => {
356
+ if (!e.target.closest) return;
357
+ const hc = e.target.closest('.sui-hover-card');
358
+ if (!hc) return;
359
+ autoReposition(hc, 'sui-hover-card');
360
+ }, true);
361
+
362
+ document.addEventListener('mouseleave', (e) => {
363
+ if (!e.target.closest) return;
364
+ const hc = e.target.closest('.sui-hover-card');
365
+ if (!hc || !hc.dataset.suiOrigDir) return;
366
+ // Delay restore until after fade-out transition completes
367
+ setTimeout(() => {
368
+ if (!hc.matches(':hover')) restorePosition(hc, 'sui-hover-card');
369
+ }, 200);
370
+ }, true);
371
+ }
372
+
373
+ // Shared auto-reposition logic for popovers and hover cards
374
+ function autoReposition(el, prefix) {
375
+ const dirClasses = [prefix + '-top', prefix + '-left', prefix + '-right'];
376
+
377
+ // Store original direction on first interaction
378
+ if (el.dataset.suiOrigDir === undefined) {
379
+ if (el.classList.contains(prefix + '-top')) el.dataset.suiOrigDir = 'top';
380
+ else if (el.classList.contains(prefix + '-left')) el.dataset.suiOrigDir = 'left';
381
+ else if (el.classList.contains(prefix + '-right')) el.dataset.suiOrigDir = 'right';
382
+ else el.dataset.suiOrigDir = 'bottom';
383
+ }
384
+
385
+ const rect = el.getBoundingClientRect();
386
+ const content = el.querySelector('.' + prefix + '-content');
387
+ const cw = content ? content.offsetWidth : 280;
388
+ const ch = content ? content.offsetHeight : 120;
389
+ const pad = 16;
390
+
391
+ const dir = el.dataset.suiOrigDir;
392
+ let best = dir;
393
+
394
+ if (dir === 'bottom' && rect.bottom + ch + pad > window.innerHeight) best = 'top';
395
+ else if (dir === 'top' && rect.top - ch - pad < 0) best = 'bottom';
396
+ else if (dir === 'left' && rect.left - cw - pad < 0) best = 'right';
397
+ else if (dir === 'right' && rect.right + cw + pad > window.innerWidth) best = 'left';
398
+
399
+ // Also check horizontal overflow for top/bottom placements
400
+ if (best === 'bottom' || best === 'top') {
401
+ const centerX = rect.left + rect.width / 2;
402
+ if (centerX - cw / 2 < pad) {
403
+ el.classList.add(prefix + '-start');
404
+ } else if (centerX + cw / 2 > window.innerWidth - pad) {
405
+ el.classList.add(prefix + '-end');
406
+ }
407
+ }
408
+
409
+ dirClasses.forEach(c => el.classList.remove(c));
410
+ if (best !== 'bottom') {
411
+ el.classList.add(prefix + '-' + best);
412
+ }
413
+ }
414
+
415
+ function restorePosition(el, prefix) {
416
+ const dirClasses = [prefix + '-top', prefix + '-left', prefix + '-right', prefix + '-start', prefix + '-end'];
417
+ dirClasses.forEach(c => el.classList.remove(c));
418
+ const dir = el.dataset.suiOrigDir;
419
+ if (dir !== 'bottom') {
420
+ el.classList.add(prefix + '-' + dir);
421
+ }
422
+ }
423
+
424
+ function delayedRestore(el, prefix) {
425
+ setTimeout(() => restorePosition(el, prefix), 200);
426
+ }
427
+
428
+ // Auto-init when DOM is ready
429
+ if (document.readyState === 'loading') {
430
+ document.addEventListener('DOMContentLoaded', init);
431
+ } else {
432
+ init();
433
+ }
434
+
435
+ // =========================================
436
+ // Tabs
437
+ // =========================================
438
+ function initTabs() {
439
+ document.addEventListener('click', (e) => {
440
+ const tab = e.target.closest('.sui-tab');
441
+ if (!tab) return;
442
+
443
+ const tabList = tab.closest('.sui-tab-list, .sui-tab-list-pill, .sui-tab-list-underlined, .sui-tab-list-boxed');
444
+ if (!tabList) return;
445
+
446
+ const container = tabList.closest('.sui-tabs');
447
+ if (!container) return;
448
+
449
+ // Deactivate all tabs
450
+ tabList.querySelectorAll('.sui-tab').forEach(t => {
451
+ t.classList.remove('active');
452
+ t.setAttribute('aria-selected', 'false');
453
+ });
454
+ tab.classList.add('active');
455
+ tab.setAttribute('aria-selected', 'true');
456
+
457
+ // Switch panels
458
+ const target = tab.getAttribute('data-sui-tab');
459
+ if (target) {
460
+ container.querySelectorAll('.sui-tab-panel').forEach(p => p.classList.remove('active'));
461
+ const panel = container.querySelector('#' + target);
462
+ if (panel) panel.classList.add('active');
463
+ }
464
+ });
465
+ }
466
+
467
+ // =========================================
468
+ // Accordion
469
+ // =========================================
470
+ function initAccordion() {
471
+ // Set accurate max-height on initially active items
472
+ document.querySelectorAll('.sui-accordion-item.active .sui-accordion-body').forEach(body => {
473
+ body.style.setProperty('--sui-accordion-height', body.scrollHeight + 'px');
474
+ });
475
+
476
+ document.addEventListener('click', (e) => {
477
+ const header = e.target.closest('.sui-accordion-header');
478
+ if (!header) return;
479
+
480
+ const item = header.closest('.sui-accordion-item');
481
+ if (!item) return;
482
+
483
+ const accordion = item.closest('.sui-accordion');
484
+ const isActive = item.classList.contains('active');
485
+
486
+ // Close siblings (single-open mode, unless data-sui-multi is set)
487
+ if (accordion && !accordion.hasAttribute('data-sui-multi')) {
488
+ accordion.querySelectorAll('.sui-accordion-item.active').forEach(i => {
489
+ if (i !== item) {
490
+ i.classList.remove('active');
491
+ const h = i.querySelector('.sui-accordion-header');
492
+ if (h) h.setAttribute('aria-expanded', 'false');
493
+ const b = i.querySelector('.sui-accordion-body');
494
+ if (b) b.style.removeProperty('--sui-accordion-height');
495
+ }
496
+ });
497
+ }
498
+
499
+ if (isActive) {
500
+ item.classList.remove('active');
501
+ header.setAttribute('aria-expanded', 'false');
502
+ const body = item.querySelector('.sui-accordion-body');
503
+ if (body) body.style.removeProperty('--sui-accordion-height');
504
+ } else {
505
+ const body = item.querySelector('.sui-accordion-body');
506
+ if (body) body.style.setProperty('--sui-accordion-height', body.scrollHeight + 'px');
507
+ item.classList.add('active');
508
+ header.setAttribute('aria-expanded', 'true');
509
+ }
510
+ });
511
+ }
512
+
513
+ // =========================================
514
+ // Collapsible
515
+ // =========================================
516
+ function initCollapsible() {
517
+ // Set height on initially open collapsibles
518
+ document.querySelectorAll('.sui-collapsible.open .sui-collapsible-content').forEach(function(content) {
519
+ content.style.setProperty('--sui-collapsible-height', content.scrollHeight + 'px');
520
+ });
521
+
522
+ document.addEventListener('click', function(e) {
523
+ var trigger = e.target.closest('.sui-collapsible-trigger');
524
+ if (!trigger) return;
525
+
526
+ var collapsible = trigger.closest('.sui-collapsible');
527
+ if (!collapsible) return;
528
+
529
+ var content = collapsible.querySelector('.sui-collapsible-content');
530
+ if (!content) return;
531
+
532
+ var isOpen = collapsible.classList.contains('open');
533
+
534
+ if (isOpen) {
535
+ collapsible.classList.remove('open');
536
+ trigger.setAttribute('aria-expanded', 'false');
537
+ content.style.removeProperty('--sui-collapsible-height');
538
+ } else {
539
+ content.style.setProperty('--sui-collapsible-height', content.scrollHeight + 'px');
540
+ collapsible.classList.add('open');
541
+ trigger.setAttribute('aria-expanded', 'true');
542
+ }
543
+ });
544
+ }
545
+
546
+ // =========================================
547
+ // Toast
548
+ // =========================================
549
+ function getToastContainer(position) {
550
+ const pos = position || 'tr';
551
+ const cls = 'sui-toast-container sui-toast-' + pos;
552
+ let container = document.querySelector('.sui-toast-container.sui-toast-' + pos);
553
+ if (!container) {
554
+ container = document.createElement('div');
555
+ container.className = cls;
556
+ document.body.appendChild(container);
557
+ }
558
+ return container;
559
+ }
560
+
561
+ function toast(options) {
562
+ const opts = Object.assign({
563
+ title: '',
564
+ message: '',
565
+ variant: '',
566
+ duration: 4000,
567
+ position: 'tr',
568
+ closable: true
569
+ }, options);
570
+
571
+ const container = getToastContainer(opts.position);
572
+
573
+ const el = document.createElement('div');
574
+ let cls = 'sui-toast';
575
+ if (opts.variant) cls += ' sui-toast-' + opts.variant;
576
+ el.className = cls;
577
+ el.setAttribute('role', 'alert');
578
+ el.setAttribute('aria-live', 'polite');
579
+
580
+ let html = '<div class="sui-toast-body">';
581
+ if (opts.title) html += '<div class="sui-toast-title">' + opts.title + '</div>';
582
+ if (opts.message) html += '<div class="sui-toast-message">' + opts.message + '</div>';
583
+ html += '</div>';
584
+ if (opts.closable) html += '<button class="sui-toast-close"></button>';
585
+ if (opts.duration > 0) html += '<div class="sui-toast-progress"></div>';
586
+ el.innerHTML = html;
587
+
588
+ container.appendChild(el);
589
+
590
+ // Trigger slide-in
591
+ requestAnimationFrame(() => {
592
+ requestAnimationFrame(() => {
593
+ el.classList.add('sui-toast-show');
594
+ });
595
+ });
596
+
597
+ // Progress bar + auto-dismiss
598
+ let timer = null;
599
+ if (opts.duration > 0) {
600
+ const bar = el.querySelector('.sui-toast-progress');
601
+ if (bar) {
602
+ bar.style.width = '100%';
603
+ requestAnimationFrame(() => {
604
+ bar.style.transitionDuration = opts.duration + 'ms';
605
+ bar.style.width = '0%';
606
+ });
607
+ }
608
+ timer = setTimeout(() => dismiss(el), opts.duration);
609
+ }
610
+
611
+ // Close button
612
+ const closeBtn = el.querySelector('.sui-toast-close');
613
+ if (closeBtn) {
614
+ closeBtn.addEventListener('click', () => {
615
+ if (timer) clearTimeout(timer);
616
+ dismiss(el);
617
+ });
618
+ }
619
+
620
+ return el;
621
+ }
622
+
623
+ function dismiss(el) {
624
+ el.classList.remove('sui-toast-show');
625
+ el.addEventListener('transitionend', () => {
626
+ el.remove();
627
+ }, { once: true });
628
+ // Fallback in case transitionend doesn't fire
629
+ setTimeout(() => { if (el.parentNode) el.remove(); }, 400);
630
+ }
631
+
632
+ // =========================================
633
+ // Dropdown
634
+ // =========================================
635
+ function initDropdown() {
636
+ document.addEventListener('click', (e) => {
637
+ const toggle = e.target.closest('[data-sui-dropdown], .sui-dropdown-toggle');
638
+ if (toggle) {
639
+ const dropdown = toggle.closest('.sui-dropdown, .sui-dropdown-split');
640
+ if (!dropdown) return;
641
+
642
+ // Close all other open dropdowns
643
+ document.querySelectorAll('.sui-dropdown.open, .sui-dropdown-split.open').forEach(d => {
644
+ if (d !== dropdown) {
645
+ d.classList.remove('open');
646
+ const t = d.querySelector('[data-sui-dropdown], .sui-dropdown-toggle');
647
+ if (t) t.setAttribute('aria-expanded', 'false');
648
+ }
649
+ });
650
+
651
+ dropdown.classList.toggle('open');
652
+ const isNowOpen = dropdown.classList.contains('open');
653
+ toggle.setAttribute('aria-expanded', isNowOpen ? 'true' : 'false');
654
+ e.stopPropagation();
655
+ return;
656
+ }
657
+
658
+ // Click on a dropdown item closes the menu
659
+ const item = e.target.closest('.sui-dropdown-item');
660
+ if (item) {
661
+ const dropdown = item.closest('.sui-dropdown, .sui-dropdown-split');
662
+ if (dropdown) {
663
+ dropdown.classList.remove('open');
664
+ const t = dropdown.querySelector('[data-sui-dropdown], .sui-dropdown-toggle');
665
+ if (t) t.setAttribute('aria-expanded', 'false');
666
+ }
667
+ return;
668
+ }
669
+
670
+ // Click outside closes all dropdowns
671
+ document.querySelectorAll('.sui-dropdown.open, .sui-dropdown-split.open').forEach(d => {
672
+ d.classList.remove('open');
673
+ const t = d.querySelector('[data-sui-dropdown], .sui-dropdown-toggle');
674
+ if (t) t.setAttribute('aria-expanded', 'false');
675
+ });
676
+ });
677
+
678
+ // Escape closes dropdowns
679
+ document.addEventListener('keydown', (e) => {
680
+ if (e.key === 'Escape') {
681
+ document.querySelectorAll('.sui-dropdown.open, .sui-dropdown-split.open').forEach(d => {
682
+ d.classList.remove('open');
683
+ const t = d.querySelector('[data-sui-dropdown], .sui-dropdown-toggle');
684
+ if (t) t.setAttribute('aria-expanded', 'false');
685
+ });
686
+ }
687
+ });
688
+ }
689
+
690
+ // =========================================
691
+ // Context Menu
692
+ // =========================================
693
+ function initContextMenu() {
694
+ var openMenu = null;
695
+
696
+ function closeAll() {
697
+ if (openMenu) {
698
+ openMenu.classList.remove('open');
699
+ openMenu.querySelectorAll('.sui-context-sub.open').forEach(function(s) {
700
+ s.classList.remove('open');
701
+ });
702
+ openMenu = null;
703
+ }
704
+ }
705
+
706
+ function positionMenu(menu, x, y) {
707
+ menu.style.left = '0px';
708
+ menu.style.top = '0px';
709
+ menu.classList.add('open');
710
+
711
+ var rect = menu.getBoundingClientRect();
712
+ var vw = window.innerWidth;
713
+ var vh = window.innerHeight;
714
+
715
+ if (x + rect.width > vw) x = vw - rect.width - 4;
716
+ if (y + rect.height > vh) y = vh - rect.height - 4;
717
+ if (x < 0) x = 4;
718
+ if (y < 0) y = 4;
719
+
720
+ menu.style.left = x + 'px';
721
+ menu.style.top = y + 'px';
722
+ }
723
+
724
+ // Right-click triggers
725
+ document.addEventListener('contextmenu', function(e) {
726
+ var trigger = e.target.closest('[data-sui-context]');
727
+ if (!trigger) return;
728
+
729
+ e.preventDefault();
730
+ closeAll();
731
+
732
+ var menuId = trigger.getAttribute('data-sui-context');
733
+ var menu = document.getElementById(menuId);
734
+ if (!menu) return;
735
+
736
+ positionMenu(menu, e.clientX, e.clientY);
737
+ openMenu = menu;
738
+
739
+ // Focus first item for keyboard nav
740
+ var firstItem = menu.querySelector('.sui-context-item:not(.disabled), .sui-context-sub-trigger');
741
+ if (firstItem) firstItem.focus();
742
+ });
743
+
744
+ // Click outside closes
745
+ document.addEventListener('click', function(e) {
746
+ if (openMenu && !e.target.closest('.sui-context-menu')) {
747
+ closeAll();
748
+ }
749
+ });
750
+
751
+ // Click on item closes (unless checkbox/radio)
752
+ document.addEventListener('click', function(e) {
753
+ var item = e.target.closest('.sui-context-item');
754
+ if (!item || !openMenu) return;
755
+ if (!item.closest('.sui-context-menu')) return;
756
+
757
+ // Checkbox toggle
758
+ if (item.hasAttribute('data-sui-context-check')) {
759
+ var check = item.querySelector('.sui-context-check');
760
+ if (check) {
761
+ var isChecked = check.textContent.trim() !== '';
762
+ check.textContent = isChecked ? '' : '\u2713';
763
+ }
764
+ return; // Don't close on checkbox click
765
+ }
766
+
767
+ // Radio toggle
768
+ if (item.hasAttribute('data-sui-context-radio')) {
769
+ var group = item.getAttribute('data-sui-context-radio');
770
+ openMenu.querySelectorAll('[data-sui-context-radio="' + group + '"] .sui-context-check').forEach(function(c) {
771
+ c.textContent = '';
772
+ });
773
+ var radio = item.querySelector('.sui-context-check');
774
+ if (radio) radio.textContent = '\u2022';
775
+ return; // Don't close on radio click
776
+ }
777
+
778
+ // Normal item — close
779
+ if (!item.classList.contains('disabled')) {
780
+ closeAll();
781
+ }
782
+ });
783
+
784
+ // Submenu hover
785
+ document.addEventListener('mouseenter', function(e) {
786
+ var subTrigger = e.target.closest && e.target.closest('.sui-context-sub-trigger');
787
+ if (!subTrigger) return;
788
+ var sub = subTrigger.closest('.sui-context-sub');
789
+ if (!sub) return;
790
+
791
+ // Close sibling subs
792
+ var parent = sub.parentElement;
793
+ if (parent) {
794
+ parent.querySelectorAll(':scope > .sui-context-sub.open').forEach(function(s) {
795
+ if (s !== sub) s.classList.remove('open');
796
+ });
797
+ }
798
+ sub.classList.add('open');
799
+ }, true);
800
+
801
+ document.addEventListener('mouseleave', function(e) {
802
+ var sub = e.target.closest && e.target.closest('.sui-context-sub');
803
+ if (!sub) return;
804
+ // Only close if not moving into the sub-content
805
+ setTimeout(function() {
806
+ if (!sub.matches(':hover')) {
807
+ sub.classList.remove('open');
808
+ }
809
+ }, 100);
810
+ }, true);
811
+
812
+ // Escape closes
813
+ document.addEventListener('keydown', function(e) {
814
+ if (e.key === 'Escape' && openMenu) {
815
+ // If a sub is open, close it first
816
+ var openSub = openMenu.querySelector('.sui-context-sub.open');
817
+ if (openSub) {
818
+ openSub.classList.remove('open');
819
+ openSub.querySelector('.sui-context-sub-trigger').focus();
820
+ } else {
821
+ closeAll();
822
+ }
823
+ }
824
+
825
+ if (!openMenu) return;
826
+
827
+ // Arrow key navigation
828
+ if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
829
+ e.preventDefault();
830
+ var activeContainer = openMenu.querySelector('.sui-context-sub.open > .sui-context-sub-content') || openMenu;
831
+ var items = Array.from(activeContainer.querySelectorAll(':scope > .sui-context-item:not(.disabled), :scope > .sui-context-sub > .sui-context-sub-trigger'));
832
+ if (items.length === 0) return;
833
+
834
+ var current = items.indexOf(document.activeElement);
835
+ if (e.key === 'ArrowDown') {
836
+ current = current < items.length - 1 ? current + 1 : 0;
837
+ } else {
838
+ current = current > 0 ? current - 1 : items.length - 1;
839
+ }
840
+ items[current].focus();
841
+ }
842
+
843
+ // ArrowRight opens submenu
844
+ if (e.key === 'ArrowRight') {
845
+ var focused = document.activeElement;
846
+ if (focused && focused.classList.contains('sui-context-sub-trigger')) {
847
+ var sub = focused.closest('.sui-context-sub');
848
+ if (sub) {
849
+ sub.classList.add('open');
850
+ var first = sub.querySelector('.sui-context-sub-content .sui-context-item:not(.disabled), .sui-context-sub-content .sui-context-sub-trigger');
851
+ if (first) first.focus();
852
+ }
853
+ }
854
+ }
855
+
856
+ // ArrowLeft closes submenu
857
+ if (e.key === 'ArrowLeft') {
858
+ var openSub = document.activeElement && document.activeElement.closest('.sui-context-sub.open');
859
+ if (openSub && openSub.closest('.sui-context-menu') === openMenu) {
860
+ openSub.classList.remove('open');
861
+ openSub.querySelector('.sui-context-sub-trigger').focus();
862
+ }
863
+ }
864
+
865
+ // Enter activates
866
+ if (e.key === 'Enter') {
867
+ var focused = document.activeElement;
868
+ if (focused && (focused.classList.contains('sui-context-item') || focused.classList.contains('sui-context-sub-trigger'))) {
869
+ focused.click();
870
+ }
871
+ }
872
+ });
873
+
874
+ // Scroll / resize closes
875
+ window.addEventListener('scroll', closeAll, true);
876
+ window.addEventListener('resize', closeAll);
877
+ }
878
+
879
+ // =========================================
880
+ // Command Palette
881
+ // =========================================
882
+ function initCommand() {
883
+ document.querySelectorAll('.sui-command[data-sui-command]').forEach(function(cmd) {
884
+ var input = cmd.querySelector('.sui-command-input');
885
+ var list = cmd.querySelector('.sui-command-list');
886
+ var empty = cmd.querySelector('.sui-command-empty');
887
+ if (!input || !list) return;
888
+
889
+ var items = list.querySelectorAll('.sui-command-item');
890
+ var groups = list.querySelectorAll('.sui-command-group');
891
+ var separators = list.querySelectorAll('.sui-command-separator');
892
+ var focusedIndex = -1;
893
+
894
+ function getVisibleItems() {
895
+ return Array.from(list.querySelectorAll('.sui-command-item:not([hidden])'));
896
+ }
897
+
898
+ function updateFocus(visibleItems) {
899
+ items.forEach(function(it) { it.classList.remove('focused'); });
900
+ if (focusedIndex >= 0 && focusedIndex < visibleItems.length) {
901
+ visibleItems[focusedIndex].classList.add('focused');
902
+ visibleItems[focusedIndex].scrollIntoView({ block: 'nearest' });
903
+ }
904
+ }
905
+
906
+ function filter() {
907
+ var query = input.value.toLowerCase().trim();
908
+ var anyVisible = false;
909
+
910
+ items.forEach(function(item) {
911
+ var text = item.textContent.toLowerCase();
912
+ var match = !query || text.indexOf(query) !== -1;
913
+ item.hidden = !match;
914
+ if (match) anyVisible = true;
915
+ });
916
+
917
+ // Hide groups with no visible items
918
+ groups.forEach(function(group) {
919
+ var hasVisible = group.querySelector('.sui-command-item:not([hidden])');
920
+ group.hidden = !hasVisible;
921
+ });
922
+
923
+ // Hide separators between hidden groups
924
+ separators.forEach(function(sep) {
925
+ var next = sep.nextElementSibling;
926
+ var prev = sep.previousElementSibling;
927
+ var nextHidden = next && next.hidden;
928
+ var prevHidden = prev && prev.hidden;
929
+ sep.hidden = nextHidden || prevHidden;
930
+ });
931
+
932
+ if (empty) {
933
+ empty.classList.toggle('visible', !anyVisible);
934
+ }
935
+
936
+ focusedIndex = anyVisible ? 0 : -1;
937
+ updateFocus(getVisibleItems());
938
+ }
939
+
940
+ input.addEventListener('input', filter);
941
+
942
+ // Keyboard nav
943
+ cmd.addEventListener('keydown', function(e) {
944
+ var visibleItems = getVisibleItems();
945
+
946
+ if (e.key === 'ArrowDown') {
947
+ e.preventDefault();
948
+ if (visibleItems.length > 0) {
949
+ focusedIndex = focusedIndex < visibleItems.length - 1 ? focusedIndex + 1 : 0;
950
+ updateFocus(visibleItems);
951
+ }
952
+ } else if (e.key === 'ArrowUp') {
953
+ e.preventDefault();
954
+ if (visibleItems.length > 0) {
955
+ focusedIndex = focusedIndex > 0 ? focusedIndex - 1 : visibleItems.length - 1;
956
+ updateFocus(visibleItems);
957
+ }
958
+ } else if (e.key === 'Enter') {
959
+ e.preventDefault();
960
+ if (focusedIndex >= 0 && focusedIndex < visibleItems.length) {
961
+ visibleItems[focusedIndex].click();
962
+ }
963
+ }
964
+ });
965
+
966
+ // Mouse hover updates focus
967
+ items.forEach(function(item) {
968
+ item.addEventListener('mouseenter', function() {
969
+ var visibleItems = getVisibleItems();
970
+ focusedIndex = visibleItems.indexOf(item);
971
+ updateFocus(visibleItems);
972
+ });
973
+ });
974
+
975
+ // Initial focus on first item
976
+ var initial = getVisibleItems();
977
+ if (initial.length > 0) {
978
+ focusedIndex = 0;
979
+ updateFocus(initial);
980
+ }
981
+ });
982
+
983
+ // Dialog mode — Cmd+K / Ctrl+K
984
+ document.querySelectorAll('.sui-command-dialog').forEach(function(dialog) {
985
+ var cmd = dialog.querySelector('.sui-command');
986
+ var input = cmd ? cmd.querySelector('.sui-command-input') : null;
987
+
988
+ function openDialog() {
989
+ dialog.classList.add('open');
990
+ document.body.style.overflow = 'hidden';
991
+ if (input) {
992
+ input.value = '';
993
+ input.dispatchEvent(new Event('input'));
994
+ setTimeout(function() { input.focus(); }, 50);
995
+ }
996
+ }
997
+
998
+ function closeDialog() {
999
+ dialog.classList.remove('open');
1000
+ document.body.style.overflow = '';
1001
+ }
1002
+
1003
+ // Cmd+K / Ctrl+K to open
1004
+ var shortcut = dialog.dataset.suiCommandKey || 'k';
1005
+ document.addEventListener('keydown', function(e) {
1006
+ if ((e.metaKey || e.ctrlKey) && e.key === shortcut) {
1007
+ e.preventDefault();
1008
+ if (dialog.classList.contains('open')) {
1009
+ closeDialog();
1010
+ } else {
1011
+ openDialog();
1012
+ }
1013
+ }
1014
+ });
1015
+
1016
+ // Escape to close
1017
+ dialog.addEventListener('keydown', function(e) {
1018
+ if (e.key === 'Escape') {
1019
+ e.preventDefault();
1020
+ closeDialog();
1021
+ }
1022
+ });
1023
+
1024
+ // Click backdrop to close
1025
+ dialog.addEventListener('click', function(e) {
1026
+ if (e.target === dialog) {
1027
+ closeDialog();
1028
+ }
1029
+ });
1030
+
1031
+ // Trigger buttons
1032
+ document.querySelectorAll('[data-sui-command-open="' + dialog.id + '"]').forEach(function(btn) {
1033
+ btn.addEventListener('click', function() {
1034
+ openDialog();
1035
+ });
1036
+ });
1037
+ });
1038
+ }
1039
+
1040
+ // =========================================
1041
+ // Calendar
1042
+ // =========================================
1043
+ function initCalendar() {
1044
+ var MONTHS = ['January','February','March','April','May','June','July','August','September','October','November','December'];
1045
+ var MONTHS_SHORT = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
1046
+ var DAYS = ['Su','Mo','Tu','We','Th','Fr','Sa'];
1047
+
1048
+ function daysInMonth(year, month) {
1049
+ return new Date(year, month + 1, 0).getDate();
1050
+ }
1051
+
1052
+ function sameDay(a, b) {
1053
+ return a && b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
1054
+ }
1055
+
1056
+ function between(d, start, end) {
1057
+ if (!start || !end) return false;
1058
+ var t = d.getTime(), s = Math.min(start.getTime(), end.getTime()), e = Math.max(start.getTime(), end.getTime());
1059
+ return t > s && t < e;
1060
+ }
1061
+
1062
+ function parseDate(str) {
1063
+ if (!str) return null;
1064
+ if (str === 'today') return new Date(new Date().setHours(0,0,0,0));
1065
+ var parts = str.split('-');
1066
+ if (parts.length === 3) return new Date(+parts[0], +parts[1] - 1, +parts[2]);
1067
+ return null;
1068
+ }
1069
+
1070
+ function formatDate(d, includeTime, hour, minute, period) {
1071
+ var str = MONTHS[d.getMonth()].substring(0, 3) + ' ' + d.getDate() + ', ' + d.getFullYear();
1072
+ if (includeTime) {
1073
+ var hh = (hour !== undefined && hour !== null) ? hour : 12;
1074
+ var mm = (minute !== undefined && minute !== null) ? minute : 0;
1075
+ str += ' ' + pad(hh) + ':' + pad(mm);
1076
+ if (period) str += ' ' + period;
1077
+ }
1078
+ return str;
1079
+ }
1080
+
1081
+ document.querySelectorAll('.sui-calendar[data-sui-calendar]').forEach(function(cal) {
1082
+ var mode = cal.dataset.suiCalendar || 'single';
1083
+ var today = new Date();
1084
+ today.setHours(0,0,0,0);
1085
+
1086
+ var minDate = parseDate(cal.dataset.suiMin);
1087
+ var maxDate = parseDate(cal.dataset.suiMax);
1088
+
1089
+ var disabledDays = [];
1090
+ if (cal.dataset.suiDisabled) {
1091
+ cal.dataset.suiDisabled.split(',').forEach(function(s) {
1092
+ var d = parseDate(s.trim());
1093
+ if (d) disabledDays.push(d);
1094
+ });
1095
+ }
1096
+
1097
+ function isDisabled(d) {
1098
+ for (var i = 0; i < disabledDays.length; i++) {
1099
+ if (sameDay(d, disabledDays[i])) return true;
1100
+ }
1101
+ if (minDate && d < minDate) return true;
1102
+ if (maxDate && d > maxDate) return true;
1103
+ return false;
1104
+ }
1105
+
1106
+ var selected = null;
1107
+ var rangeStart = null;
1108
+ var rangeEnd = null;
1109
+ var defaultPlaceholder = '';
1110
+ var viewMode = 'days'; // 'days', 'months', 'years'
1111
+ var yearPageStart = 0;
1112
+
1113
+ // Time picker
1114
+ var hasTime = cal.hasAttribute('data-sui-calendar-time');
1115
+ var is24h = cal.getAttribute('data-sui-calendar-time') === '24h';
1116
+ var timeHour = is24h ? 0 : 12, timeMinute = 0, timePeriod = 'AM';
1117
+ var timeRow = null, hourInput = null, minuteInput = null, periodBtn = null;
1118
+
1119
+ if (hasTime) {
1120
+ timeRow = cal.querySelector('.sui-calendar-time');
1121
+ if (!timeRow) {
1122
+ timeRow = document.createElement('div');
1123
+ timeRow.className = 'sui-calendar-time';
1124
+
1125
+ var label = document.createElement('span');
1126
+ label.className = 'sui-calendar-time-label';
1127
+ label.textContent = 'Time';
1128
+ timeRow.appendChild(label);
1129
+
1130
+ hourInput = document.createElement('input');
1131
+ hourInput.type = 'text';
1132
+ hourInput.className = 'sui-calendar-time-input';
1133
+ hourInput.value = pad(timeHour);
1134
+ hourInput.maxLength = 2;
1135
+ hourInput.setAttribute('aria-label', 'Hour');
1136
+ timeRow.appendChild(hourInput);
1137
+
1138
+ var sep = document.createElement('span');
1139
+ sep.className = 'sui-calendar-time-sep';
1140
+ sep.textContent = ':';
1141
+ timeRow.appendChild(sep);
1142
+
1143
+ minuteInput = document.createElement('input');
1144
+ minuteInput.type = 'text';
1145
+ minuteInput.className = 'sui-calendar-time-input';
1146
+ minuteInput.value = pad(timeMinute);
1147
+ minuteInput.maxLength = 2;
1148
+ minuteInput.setAttribute('aria-label', 'Minute');
1149
+ timeRow.appendChild(minuteInput);
1150
+
1151
+ if (!is24h) {
1152
+ periodBtn = document.createElement('button');
1153
+ periodBtn.type = 'button';
1154
+ periodBtn.className = 'sui-calendar-time-period';
1155
+ periodBtn.textContent = timePeriod;
1156
+ timeRow.appendChild(periodBtn);
1157
+ }
1158
+
1159
+ // Insert before clear button or append
1160
+ var clearBtn = cal.querySelector('[data-sui-calendar-clear]');
1161
+ if (clearBtn) {
1162
+ cal.insertBefore(timeRow, clearBtn);
1163
+ } else {
1164
+ cal.appendChild(timeRow);
1165
+ }
1166
+ } else {
1167
+ hourInput = timeRow.querySelectorAll('.sui-calendar-time-input')[0];
1168
+ minuteInput = timeRow.querySelectorAll('.sui-calendar-time-input')[1];
1169
+ periodBtn = timeRow.querySelector('.sui-calendar-time-period');
1170
+ }
1171
+
1172
+ var hourMax = is24h ? 23 : 12;
1173
+ var hourMin = is24h ? 0 : 1;
1174
+
1175
+ function parseHour(v) {
1176
+ var n = parseInt(v, 10);
1177
+ if (isNaN(n) || n < 0) n = 0;
1178
+ if (n > 23) n = 23;
1179
+ if (is24h) return { hour: n };
1180
+ // Convert 24h input to 12h + period
1181
+ if (n === 0) return { hour: 12, period: 'AM' };
1182
+ if (n < 12) return { hour: n, period: 'AM' };
1183
+ if (n === 12) return { hour: 12, period: 'PM' };
1184
+ return { hour: n - 12, period: 'PM' };
1185
+ }
1186
+ function clampMinute(v) { var n = parseInt(v, 10); if (isNaN(n) || n < 0) return 0; if (n > 59) return 59; return n; }
1187
+
1188
+ hourInput.addEventListener('blur', function() {
1189
+ var result = parseHour(this.value);
1190
+ timeHour = result.hour;
1191
+ if (!is24h && result.period) {
1192
+ timePeriod = result.period;
1193
+ if (periodBtn) periodBtn.textContent = timePeriod;
1194
+ }
1195
+ this.value = pad(timeHour);
1196
+ fireTimeUpdate();
1197
+ });
1198
+ hourInput.addEventListener('keydown', function(e) {
1199
+ if (e.key === 'ArrowUp') { e.preventDefault(); timeHour = timeHour >= hourMax ? hourMin : timeHour + 1; this.value = pad(timeHour); fireTimeUpdate(); }
1200
+ if (e.key === 'ArrowDown') { e.preventDefault(); timeHour = timeHour <= hourMin ? hourMax : timeHour - 1; this.value = pad(timeHour); fireTimeUpdate(); }
1201
+ if (e.key === 'Enter') { this.blur(); }
1202
+ });
1203
+
1204
+ minuteInput.addEventListener('blur', function() {
1205
+ timeMinute = clampMinute(this.value);
1206
+ this.value = pad(timeMinute);
1207
+ fireTimeUpdate();
1208
+ });
1209
+ minuteInput.addEventListener('keydown', function(e) {
1210
+ if (e.key === 'ArrowUp') { e.preventDefault(); timeMinute = timeMinute >= 59 ? 0 : timeMinute + 1; this.value = pad(timeMinute); fireTimeUpdate(); }
1211
+ if (e.key === 'ArrowDown') { e.preventDefault(); timeMinute = timeMinute <= 0 ? 59 : timeMinute - 1; this.value = pad(timeMinute); fireTimeUpdate(); }
1212
+ if (e.key === 'Enter') { this.blur(); }
1213
+ });
1214
+
1215
+ if (periodBtn) {
1216
+ periodBtn.addEventListener('click', function(e) {
1217
+ e.stopPropagation();
1218
+ timePeriod = timePeriod === 'AM' ? 'PM' : 'AM';
1219
+ this.textContent = timePeriod;
1220
+ fireTimeUpdate();
1221
+ });
1222
+ }
1223
+ }
1224
+
1225
+ function fireTimeUpdate() {
1226
+ if (!hasTime) return;
1227
+ if (mode === 'single' && selected) {
1228
+ cal.dispatchEvent(new CustomEvent('sui-date-select', { detail: { date: selected, hour: timeHour, minute: timeMinute, period: is24h ? null : timePeriod, is24h: is24h } }));
1229
+ }
1230
+ }
1231
+
1232
+ var monthContainers = cal.querySelectorAll('.sui-calendar-month');
1233
+ var isMultiMonth = monthContainers.length > 0;
1234
+ if (!isMultiMonth) monthContainers = [cal];
1235
+
1236
+ var viewOffsets = [];
1237
+ monthContainers.forEach(function(mc, i) { viewOffsets.push(i); });
1238
+
1239
+ var viewYear = today.getFullYear();
1240
+ var viewMonth = today.getMonth();
1241
+
1242
+ var prevBtn = cal.querySelector('[data-sui-calendar-prev]');
1243
+ var nextBtn = cal.querySelector('[data-sui-calendar-next]');
1244
+ var titleEl = cal.querySelector('.sui-calendar-header .sui-calendar-title');
1245
+
1246
+ function renderDays() {
1247
+ viewMode = 'days';
1248
+ if (timeRow) timeRow.style.display = '';
1249
+ monthContainers.forEach(function(mc, idx) {
1250
+ var m = viewMonth + viewOffsets[idx];
1251
+ var y = viewYear;
1252
+ while (m > 11) { m -= 12; y++; }
1253
+ while (m < 0) { m += 12; y--; }
1254
+
1255
+ // Title
1256
+ var t = mc.querySelector('.sui-calendar-title');
1257
+ if (t) {
1258
+ if (idx === 0 && !isMultiMonth && t === titleEl) {
1259
+ t.textContent = MONTHS[m] + ' ' + y;
1260
+ t.style.cursor = 'pointer';
1261
+ } else {
1262
+ t.textContent = MONTHS[m] + ' ' + y;
1263
+ }
1264
+ }
1265
+
1266
+ var grid = mc.querySelector('.sui-calendar-grid');
1267
+ if (!grid) return;
1268
+ grid.innerHTML = '';
1269
+ grid.style.gridTemplateColumns = 'repeat(7, 1fr)';
1270
+
1271
+ DAYS.forEach(function(d) {
1272
+ var lbl = document.createElement('div');
1273
+ lbl.className = 'sui-calendar-day-label';
1274
+ lbl.textContent = d;
1275
+ grid.appendChild(lbl);
1276
+ });
1277
+
1278
+ var firstDay = new Date(y, m, 1).getDay();
1279
+ var total = daysInMonth(y, m);
1280
+
1281
+ var prevTotal = daysInMonth(y, m - 1);
1282
+ for (var p = firstDay - 1; p >= 0; p--) {
1283
+ var btn = document.createElement('button');
1284
+ btn.className = 'sui-calendar-day outside';
1285
+ btn.textContent = prevTotal - p;
1286
+ btn.type = 'button';
1287
+ btn.disabled = true;
1288
+ grid.appendChild(btn);
1289
+ }
1290
+
1291
+ for (var d = 1; d <= total; d++) {
1292
+ var date = new Date(y, m, d);
1293
+ var btn = document.createElement('button');
1294
+ btn.className = 'sui-calendar-day';
1295
+ btn.textContent = d;
1296
+ btn.type = 'button';
1297
+
1298
+ if (sameDay(date, today)) btn.classList.add('today');
1299
+ if (isDisabled(date)) btn.classList.add('disabled');
1300
+
1301
+ if (mode === 'single' && sameDay(date, selected)) btn.classList.add('selected');
1302
+
1303
+ if (mode === 'range') {
1304
+ if (sameDay(date, rangeStart)) btn.classList.add('range-start');
1305
+ if (sameDay(date, rangeEnd)) btn.classList.add('range-end');
1306
+ if (rangeStart && rangeEnd && between(date, rangeStart, rangeEnd)) btn.classList.add('in-range');
1307
+ }
1308
+
1309
+ (function(dt) {
1310
+ btn.addEventListener('click', function(e) {
1311
+ e.stopPropagation();
1312
+ if (mode === 'single') {
1313
+ selected = dt;
1314
+ var detail = { date: dt };
1315
+ if (hasTime) { detail.hour = timeHour; detail.minute = timeMinute; detail.period = timePeriod; }
1316
+ cal.dispatchEvent(new CustomEvent('sui-date-select', { detail: detail }));
1317
+ } else if (mode === 'range') {
1318
+ if (!rangeStart || rangeEnd) {
1319
+ rangeStart = dt;
1320
+ rangeEnd = null;
1321
+ } else {
1322
+ if (dt < rangeStart) { rangeEnd = rangeStart; rangeStart = dt; }
1323
+ else { rangeEnd = dt; }
1324
+ cal.dispatchEvent(new CustomEvent('sui-date-select', { detail: { start: rangeStart, end: rangeEnd } }));
1325
+ }
1326
+ }
1327
+ renderDays();
1328
+ });
1329
+ })(date);
1330
+
1331
+ grid.appendChild(btn);
1332
+ }
1333
+
1334
+ var totalCells = firstDay + total;
1335
+ var remaining = totalCells % 7 === 0 ? 0 : 7 - (totalCells % 7);
1336
+ for (var n = 1; n <= remaining; n++) {
1337
+ var btn = document.createElement('button');
1338
+ btn.className = 'sui-calendar-day outside';
1339
+ btn.textContent = n;
1340
+ btn.type = 'button';
1341
+ btn.disabled = true;
1342
+ grid.appendChild(btn);
1343
+ }
1344
+ });
1345
+
1346
+ updateClear();
1347
+ }
1348
+
1349
+ function renderMonths() {
1350
+ viewMode = 'months';
1351
+ if (timeRow) timeRow.style.display = 'none';
1352
+ if (titleEl) {
1353
+ titleEl.textContent = viewYear;
1354
+ titleEl.style.cursor = 'pointer';
1355
+ }
1356
+
1357
+ // Only render in first (or only) grid
1358
+ var grid = monthContainers[0].querySelector('.sui-calendar-grid');
1359
+ if (!grid) return;
1360
+ grid.innerHTML = '';
1361
+ grid.style.gridTemplateColumns = 'repeat(4, 1fr)';
1362
+
1363
+ for (var m = 0; m < 12; m++) {
1364
+ var btn = document.createElement('button');
1365
+ btn.className = 'sui-calendar-day';
1366
+ btn.textContent = MONTHS_SHORT[m];
1367
+ btn.type = 'button';
1368
+
1369
+ if (m === viewMonth && viewYear === today.getFullYear()) btn.classList.add('today');
1370
+ if (m === viewMonth) btn.classList.add('selected');
1371
+
1372
+ (function(month) {
1373
+ btn.addEventListener('click', function(e) {
1374
+ e.stopPropagation();
1375
+ viewMonth = month;
1376
+ renderDays();
1377
+ });
1378
+ })(m);
1379
+
1380
+ grid.appendChild(btn);
1381
+ }
1382
+ }
1383
+
1384
+ function renderYears() {
1385
+ viewMode = 'years';
1386
+ if (timeRow) timeRow.style.display = 'none';
1387
+ var start = yearPageStart;
1388
+ var end = start + 11;
1389
+ if (titleEl) {
1390
+ titleEl.textContent = start + ' – ' + end;
1391
+ titleEl.style.cursor = 'default';
1392
+ }
1393
+
1394
+ var grid = monthContainers[0].querySelector('.sui-calendar-grid');
1395
+ if (!grid) return;
1396
+ grid.innerHTML = '';
1397
+ grid.style.gridTemplateColumns = 'repeat(4, 1fr)';
1398
+
1399
+ for (var yr = start; yr <= end; yr++) {
1400
+ var btn = document.createElement('button');
1401
+ btn.className = 'sui-calendar-day';
1402
+ btn.textContent = yr;
1403
+ btn.type = 'button';
1404
+
1405
+ if (yr === today.getFullYear()) btn.classList.add('today');
1406
+ if (yr === viewYear) btn.classList.add('selected');
1407
+
1408
+ (function(year) {
1409
+ btn.addEventListener('click', function(e) {
1410
+ e.stopPropagation();
1411
+ viewYear = year;
1412
+ renderMonths();
1413
+ });
1414
+ })(yr);
1415
+
1416
+ grid.appendChild(btn);
1417
+ }
1418
+ }
1419
+
1420
+ function updateClear() {
1421
+ var clearBtn = cal.querySelector('[data-sui-calendar-clear]');
1422
+ if (clearBtn) {
1423
+ var hasSelection = mode === 'single' ? !!selected : !!(rangeStart || rangeEnd);
1424
+ clearBtn.style.display = hasSelection ? '' : 'none';
1425
+ }
1426
+ }
1427
+
1428
+ // Nav buttons
1429
+ if (prevBtn) {
1430
+ prevBtn.addEventListener('click', function() {
1431
+ if (viewMode === 'days') {
1432
+ viewMonth--;
1433
+ if (viewMonth < 0) { viewMonth = 11; viewYear--; }
1434
+ renderDays();
1435
+ } else if (viewMode === 'months') {
1436
+ viewYear--;
1437
+ renderMonths();
1438
+ } else if (viewMode === 'years') {
1439
+ yearPageStart -= 12;
1440
+ renderYears();
1441
+ }
1442
+ });
1443
+ }
1444
+
1445
+ if (nextBtn) {
1446
+ nextBtn.addEventListener('click', function() {
1447
+ if (viewMode === 'days') {
1448
+ viewMonth++;
1449
+ if (viewMonth > 11) { viewMonth = 0; viewYear++; }
1450
+ renderDays();
1451
+ } else if (viewMode === 'months') {
1452
+ viewYear++;
1453
+ renderMonths();
1454
+ } else if (viewMode === 'years') {
1455
+ yearPageStart += 12;
1456
+ renderYears();
1457
+ }
1458
+ });
1459
+ }
1460
+
1461
+ // Title click: days → months → years
1462
+ if (titleEl && !isMultiMonth) {
1463
+ titleEl.addEventListener('click', function() {
1464
+ if (viewMode === 'days') {
1465
+ renderMonths();
1466
+ } else if (viewMode === 'months') {
1467
+ yearPageStart = viewYear - (viewYear % 12);
1468
+ renderYears();
1469
+ }
1470
+ });
1471
+ }
1472
+
1473
+ // Clear button
1474
+ cal.querySelectorAll('[data-sui-calendar-clear]').forEach(function(btn) {
1475
+ btn.addEventListener('click', function(e) {
1476
+ e.stopPropagation();
1477
+ selected = null;
1478
+ rangeStart = null;
1479
+ rangeEnd = null;
1480
+ if (hasTime) {
1481
+ timeHour = is24h ? 0 : 12; timeMinute = 0; timePeriod = 'AM';
1482
+ if (hourInput) hourInput.value = pad(timeHour);
1483
+ if (minuteInput) minuteInput.value = pad(timeMinute);
1484
+ if (periodBtn) periodBtn.textContent = timePeriod;
1485
+ }
1486
+ cal.dispatchEvent(new CustomEvent('sui-date-clear'));
1487
+ renderDays();
1488
+ });
1489
+ });
1490
+
1491
+ renderDays();
1492
+
1493
+ // Date Picker integration
1494
+ var picker = cal.closest('.sui-datepicker');
1495
+ if (picker) {
1496
+ var trigger = picker.querySelector('.sui-datepicker-trigger');
1497
+ var popover = picker.querySelector('.sui-datepicker-popover');
1498
+ var placeholderEl = trigger ? trigger.querySelector('.sui-datepicker-placeholder') : null;
1499
+ if (placeholderEl) defaultPlaceholder = placeholderEl.textContent;
1500
+
1501
+ if (trigger && popover) {
1502
+ trigger.addEventListener('click', function(e) {
1503
+ e.stopPropagation();
1504
+ popover.classList.toggle('open');
1505
+ });
1506
+
1507
+ document.addEventListener('mousedown', function(e) {
1508
+ if (!picker.contains(e.target)) {
1509
+ popover.classList.remove('open');
1510
+ }
1511
+ });
1512
+ }
1513
+
1514
+ cal.addEventListener('sui-date-select', function(e) {
1515
+ if (!trigger) return;
1516
+ var span = trigger.querySelector('.sui-datepicker-value') || trigger.querySelector('.sui-datepicker-placeholder');
1517
+ if (mode === 'single' && e.detail.date) {
1518
+ var text = formatDate(e.detail.date, hasTime, e.detail.hour, e.detail.minute, e.detail.is24h ? null : e.detail.period);
1519
+ if (span) { span.textContent = text; span.className = 'sui-datepicker-value'; }
1520
+ if (!hasTime && popover) popover.classList.remove('open');
1521
+ } else if (mode === 'range' && e.detail.start && e.detail.end) {
1522
+ var text = formatDate(e.detail.start) + ' – ' + formatDate(e.detail.end);
1523
+ if (span) { span.textContent = text; span.className = 'sui-datepicker-value'; }
1524
+ if (popover) popover.classList.remove('open');
1525
+ }
1526
+ updateClear();
1527
+ });
1528
+
1529
+ cal.addEventListener('sui-date-clear', function() {
1530
+ var span = trigger.querySelector('.sui-datepicker-value') || trigger.querySelector('.sui-datepicker-placeholder');
1531
+ if (span) { span.textContent = defaultPlaceholder; span.className = 'sui-datepicker-placeholder'; }
1532
+ updateClear();
1533
+ });
1534
+ }
1535
+ });
1536
+ }
1537
+
1538
+ // =========================================
1539
+ // Standalone Time Picker
1540
+ // =========================================
1541
+ function initTimePicker() {
1542
+ document.querySelectorAll('.sui-timepicker[data-sui-timepicker]').forEach(function(tp) {
1543
+ var is24h = tp.getAttribute('data-sui-timepicker') === '24h';
1544
+ var hourMax = is24h ? 23 : 12;
1545
+ var hourMin = is24h ? 0 : 1;
1546
+ var tHour = is24h ? 0 : 12, tMinute = 0, tPeriod = 'AM';
1547
+
1548
+ var hInput = tp.querySelectorAll('.sui-calendar-time-input')[0];
1549
+ var mInput = tp.querySelectorAll('.sui-calendar-time-input')[1];
1550
+ var pBtn = tp.querySelector('.sui-calendar-time-period');
1551
+
1552
+ if (!hInput || !mInput) {
1553
+ // Auto-build the UI
1554
+ var label = document.createElement('span');
1555
+ label.className = 'sui-calendar-time-label';
1556
+ label.textContent = 'Time';
1557
+ tp.appendChild(label);
1558
+
1559
+ hInput = document.createElement('input');
1560
+ hInput.type = 'text';
1561
+ hInput.className = 'sui-calendar-time-input';
1562
+ hInput.value = pad(tHour);
1563
+ hInput.maxLength = 2;
1564
+ hInput.setAttribute('aria-label', 'Hour');
1565
+ tp.appendChild(hInput);
1566
+
1567
+ var sep = document.createElement('span');
1568
+ sep.className = 'sui-calendar-time-sep';
1569
+ sep.textContent = ':';
1570
+ tp.appendChild(sep);
1571
+
1572
+ mInput = document.createElement('input');
1573
+ mInput.type = 'text';
1574
+ mInput.className = 'sui-calendar-time-input';
1575
+ mInput.value = pad(tMinute);
1576
+ mInput.maxLength = 2;
1577
+ mInput.setAttribute('aria-label', 'Minute');
1578
+ tp.appendChild(mInput);
1579
+
1580
+ if (!is24h) {
1581
+ pBtn = document.createElement('button');
1582
+ pBtn.type = 'button';
1583
+ pBtn.className = 'sui-calendar-time-period';
1584
+ pBtn.textContent = tPeriod;
1585
+ tp.appendChild(pBtn);
1586
+ }
1587
+ }
1588
+
1589
+ function parseH(v) {
1590
+ var n = parseInt(v, 10);
1591
+ if (isNaN(n) || n < 0) n = 0;
1592
+ if (n > 23) n = 23;
1593
+ if (is24h) return { hour: n };
1594
+ if (n === 0) return { hour: 12, period: 'AM' };
1595
+ if (n < 12) return { hour: n, period: 'AM' };
1596
+ if (n === 12) return { hour: 12, period: 'PM' };
1597
+ return { hour: n - 12, period: 'PM' };
1598
+ }
1599
+ function clampM(v) { var n = parseInt(v, 10); if (isNaN(n) || n < 0) return 0; if (n > 59) return 59; return n; }
1600
+
1601
+ function fireChange() {
1602
+ tp.dispatchEvent(new CustomEvent('sui-time-change', { detail: { hour: tHour, minute: tMinute, period: is24h ? null : tPeriod, is24h: is24h } }));
1603
+ }
1604
+
1605
+ hInput.addEventListener('blur', function() {
1606
+ var result = parseH(this.value);
1607
+ tHour = result.hour;
1608
+ if (!is24h && result.period) { tPeriod = result.period; if (pBtn) pBtn.textContent = tPeriod; }
1609
+ this.value = pad(tHour);
1610
+ fireChange();
1611
+ });
1612
+ hInput.addEventListener('keydown', function(e) {
1613
+ if (e.key === 'ArrowUp') { e.preventDefault(); tHour = tHour >= hourMax ? hourMin : tHour + 1; this.value = pad(tHour); fireChange(); }
1614
+ if (e.key === 'ArrowDown') { e.preventDefault(); tHour = tHour <= hourMin ? hourMax : tHour - 1; this.value = pad(tHour); fireChange(); }
1615
+ if (e.key === 'Enter') this.blur();
1616
+ });
1617
+
1618
+ mInput.addEventListener('blur', function() { tMinute = clampM(this.value); this.value = pad(tMinute); fireChange(); });
1619
+ mInput.addEventListener('keydown', function(e) {
1620
+ if (e.key === 'ArrowUp') { e.preventDefault(); tMinute = tMinute >= 59 ? 0 : tMinute + 1; this.value = pad(tMinute); fireChange(); }
1621
+ if (e.key === 'ArrowDown') { e.preventDefault(); tMinute = tMinute <= 0 ? 59 : tMinute - 1; this.value = pad(tMinute); fireChange(); }
1622
+ if (e.key === 'Enter') this.blur();
1623
+ });
1624
+
1625
+ if (pBtn) {
1626
+ pBtn.addEventListener('click', function(e) {
1627
+ e.stopPropagation();
1628
+ tPeriod = tPeriod === 'AM' ? 'PM' : 'AM';
1629
+ this.textContent = tPeriod;
1630
+ fireChange();
1631
+ });
1632
+ }
1633
+ });
1634
+ }
1635
+
1636
+ // =========================================
1637
+ // Menubar
1638
+ // =========================================
1639
+ function initMenubar() {
1640
+ var menubarOpen = false;
1641
+
1642
+ function closeAllMenus(bar) {
1643
+ bar.querySelectorAll('.sui-menubar-menu.open').forEach(function(m) {
1644
+ m.classList.remove('open');
1645
+ m.querySelector('.sui-menubar-trigger').classList.remove('active');
1646
+ });
1647
+ bar.querySelectorAll('.sui-menubar-sub.open').forEach(function(s) {
1648
+ s.classList.remove('open');
1649
+ });
1650
+ menubarOpen = false;
1651
+ }
1652
+
1653
+ function openMenu(menu) {
1654
+ // Close all menubars on the page, not just the current one
1655
+ document.querySelectorAll('.sui-menubar').forEach(function(bar) {
1656
+ closeAllMenus(bar);
1657
+ });
1658
+ menu.classList.add('open');
1659
+ menu.querySelector('.sui-menubar-trigger').classList.add('active');
1660
+ menubarOpen = true;
1661
+ }
1662
+
1663
+ document.addEventListener('click', function(e) {
1664
+ var trigger = e.target.closest('.sui-menubar-trigger');
1665
+ if (trigger) {
1666
+ var menu = trigger.closest('.sui-menubar-menu');
1667
+ if (menu.classList.contains('open')) {
1668
+ closeAllMenus(menu.closest('.sui-menubar'));
1669
+ } else {
1670
+ openMenu(menu);
1671
+ }
1672
+ e.stopPropagation();
1673
+ return;
1674
+ }
1675
+
1676
+ // Submenu triggers
1677
+ var subTrigger = e.target.closest('.sui-menubar-sub-trigger');
1678
+ if (subTrigger) {
1679
+ var sub = subTrigger.closest('.sui-menubar-sub');
1680
+ var isOpen = sub.classList.contains('open');
1681
+ // Close sibling subs
1682
+ sub.parentElement.querySelectorAll('.sui-menubar-sub.open').forEach(function(s) {
1683
+ s.classList.remove('open');
1684
+ });
1685
+ if (!isOpen) sub.classList.add('open');
1686
+ e.stopPropagation();
1687
+ return;
1688
+ }
1689
+
1690
+ // Clicking a menubar item closes the menu
1691
+ var item = e.target.closest('.sui-menubar-item');
1692
+ if (item && item.closest('.sui-menubar')) {
1693
+ var bar = item.closest('.sui-menubar');
1694
+ closeAllMenus(bar);
1695
+ return;
1696
+ }
1697
+
1698
+ // Click outside
1699
+ document.querySelectorAll('.sui-menubar').forEach(function(bar) {
1700
+ closeAllMenus(bar);
1701
+ });
1702
+ });
1703
+
1704
+ // Hover to switch menus when one is already open
1705
+ document.addEventListener('mouseenter', function(e) {
1706
+ if (!menubarOpen) return;
1707
+ var trigger = e.target.closest ? e.target.closest('.sui-menubar-trigger') : null;
1708
+ if (!trigger) return;
1709
+ var menu = trigger.closest('.sui-menubar-menu');
1710
+ if (menu && !menu.classList.contains('open')) {
1711
+ openMenu(menu);
1712
+ }
1713
+ }, true);
1714
+
1715
+ // Hover to open submenus
1716
+ document.querySelectorAll('.sui-menubar-sub').forEach(function(sub) {
1717
+ sub.addEventListener('mouseenter', function() {
1718
+ sub.parentElement.querySelectorAll('.sui-menubar-sub.open').forEach(function(s) {
1719
+ if (s !== sub) s.classList.remove('open');
1720
+ });
1721
+ sub.classList.add('open');
1722
+ });
1723
+ sub.addEventListener('mouseleave', function() {
1724
+ sub.classList.remove('open');
1725
+ });
1726
+ });
1727
+
1728
+ // Escape closes menubar
1729
+ document.addEventListener('keydown', function(e) {
1730
+ if (e.key === 'Escape') {
1731
+ document.querySelectorAll('.sui-menubar').forEach(function(bar) {
1732
+ closeAllMenus(bar);
1733
+ });
1734
+ }
1735
+ });
1736
+ }
1737
+
1738
+ // =========================================
1739
+ // Combobox
1740
+ // =========================================
1741
+ function initCombobox() {
1742
+ document.querySelectorAll('.sui-combobox').forEach(function(combo) {
1743
+ var trigger = combo.querySelector('.sui-combobox-trigger');
1744
+ var content = combo.querySelector('.sui-combobox-content');
1745
+ var input = combo.querySelector('.sui-combobox-input');
1746
+ var items = combo.querySelectorAll('.sui-combobox-item');
1747
+ var valueEl = combo.querySelector('.sui-combobox-value');
1748
+ var chipsEl = combo.querySelector('.sui-combobox-chips');
1749
+ var emptyEl = combo.querySelector('.sui-combobox-empty');
1750
+ var clearBtn = combo.querySelector('.sui-combobox-clear');
1751
+ var isMultiple = combo.classList.contains('sui-combobox-multiple');
1752
+ var placeholder = valueEl ? valueEl.textContent : '';
1753
+ if (chipsEl) placeholder = chipsEl.textContent.trim();
1754
+
1755
+ if (!trigger || !content) return;
1756
+ if (combo.classList.contains('sui-combobox-disabled')) return;
1757
+
1758
+ function open() {
1759
+ // Close other comboboxes
1760
+ document.querySelectorAll('.sui-combobox.open').forEach(function(c) {
1761
+ if (c !== combo) c.classList.remove('open');
1762
+ });
1763
+ combo.classList.add('open');
1764
+ if (input) {
1765
+ input.value = '';
1766
+ input.focus();
1767
+ filterItems('');
1768
+ }
1769
+ }
1770
+
1771
+ function close() {
1772
+ combo.classList.remove('open');
1773
+ }
1774
+
1775
+ function filterItems(query) {
1776
+ var q = query.toLowerCase();
1777
+ var visibleCount = 0;
1778
+ items.forEach(function(item) {
1779
+ var text = item.textContent.toLowerCase();
1780
+ var match = !q || text.indexOf(q) !== -1;
1781
+ item.style.display = match ? '' : 'none';
1782
+ if (match) visibleCount++;
1783
+ });
1784
+ // Show/hide groups based on visible children
1785
+ combo.querySelectorAll('.sui-combobox-label').forEach(function(label) {
1786
+ var next = label.nextElementSibling;
1787
+ var hasVisible = false;
1788
+ while (next && !next.classList.contains('sui-combobox-label') && !next.classList.contains('sui-combobox-separator')) {
1789
+ if (next.classList.contains('sui-combobox-item') && next.style.display !== 'none') hasVisible = true;
1790
+ next = next.nextElementSibling;
1791
+ }
1792
+ label.style.display = hasVisible ? '' : 'none';
1793
+ });
1794
+ combo.querySelectorAll('.sui-combobox-separator').forEach(function(sep) {
1795
+ sep.style.display = q ? 'none' : '';
1796
+ });
1797
+ if (emptyEl) {
1798
+ emptyEl.classList.toggle('visible', visibleCount === 0);
1799
+ }
1800
+ }
1801
+
1802
+ function updateClear() {
1803
+ if (!clearBtn) return;
1804
+ var hasSelection = combo.querySelectorAll('.sui-combobox-item.selected').length > 0;
1805
+ clearBtn.classList.toggle('visible', hasSelection);
1806
+ }
1807
+
1808
+ function clearAll() {
1809
+ items.forEach(function(i) { i.classList.remove('selected'); });
1810
+ if (valueEl) {
1811
+ valueEl.textContent = placeholder;
1812
+ valueEl.classList.add('placeholder');
1813
+ }
1814
+ if (chipsEl) updateChips();
1815
+ updateClear();
1816
+ }
1817
+
1818
+ function updateChips() {
1819
+ if (!chipsEl) return;
1820
+ chipsEl.innerHTML = '';
1821
+ var selectedItems = combo.querySelectorAll('.sui-combobox-item.selected');
1822
+ if (selectedItems.length === 0) {
1823
+ var ph = document.createElement('span');
1824
+ ph.className = 'placeholder';
1825
+ ph.textContent = placeholder;
1826
+ chipsEl.appendChild(ph);
1827
+ updateClear();
1828
+ return;
1829
+ }
1830
+ selectedItems.forEach(function(item) {
1831
+ var chip = document.createElement('span');
1832
+ chip.className = 'sui-combobox-chip';
1833
+ chip.textContent = item.getAttribute('data-value') || item.textContent.trim();
1834
+ var remove = document.createElement('span');
1835
+ remove.className = 'sui-combobox-chip-remove';
1836
+ remove.innerHTML = '&#10005;';
1837
+ remove.addEventListener('click', function(e) {
1838
+ e.stopPropagation();
1839
+ item.classList.remove('selected');
1840
+ updateChips();
1841
+ });
1842
+ chip.appendChild(remove);
1843
+ chipsEl.appendChild(chip);
1844
+ });
1845
+ updateClear();
1846
+ }
1847
+
1848
+ if (clearBtn) {
1849
+ clearBtn.addEventListener('click', function(e) {
1850
+ e.stopPropagation();
1851
+ clearAll();
1852
+ });
1853
+ }
1854
+
1855
+ trigger.addEventListener('click', function(e) {
1856
+ e.stopPropagation();
1857
+ if (combo.classList.contains('open')) {
1858
+ close();
1859
+ } else {
1860
+ open();
1861
+ }
1862
+ });
1863
+
1864
+ if (input) {
1865
+ input.addEventListener('input', function() {
1866
+ filterItems(input.value);
1867
+ });
1868
+ input.addEventListener('click', function(e) {
1869
+ e.stopPropagation();
1870
+ });
1871
+ }
1872
+
1873
+ items.forEach(function(item) {
1874
+ item.addEventListener('click', function(e) {
1875
+ e.stopPropagation();
1876
+ if (isMultiple) {
1877
+ item.classList.toggle('selected');
1878
+ updateChips();
1879
+ } else {
1880
+ items.forEach(function(i) { i.classList.remove('selected'); });
1881
+ item.classList.add('selected');
1882
+ if (valueEl) {
1883
+ valueEl.textContent = item.getAttribute('data-value') || item.textContent.trim();
1884
+ valueEl.classList.remove('placeholder');
1885
+ }
1886
+ updateClear();
1887
+ close();
1888
+ }
1889
+ });
1890
+ });
1891
+
1892
+ // Click outside closes
1893
+ document.addEventListener('click', function(e) {
1894
+ if (!combo.contains(e.target)) {
1895
+ close();
1896
+ }
1897
+ });
1898
+
1899
+ // Escape closes
1900
+ combo.addEventListener('keydown', function(e) {
1901
+ if (e.key === 'Escape') close();
1902
+ });
1903
+
1904
+ // Initialize clear button visibility for pre-selected items
1905
+ updateClear();
1906
+ });
1907
+ }
1908
+
1909
+ // =========================================
1910
+ // Resizable
1911
+ // =========================================
1912
+ function initResizable() {
1913
+ document.querySelectorAll('.sui-resizable').forEach(function(container) {
1914
+ var isVertical = container.classList.contains('sui-resizable-vertical');
1915
+ var handles = container.querySelectorAll(':scope > .sui-resizable-handle');
1916
+
1917
+ // Initialize panels with flex-grow from data-size or equal split
1918
+ var panels = Array.from(container.querySelectorAll(':scope > .sui-resizable-panel'));
1919
+ panels.forEach(function(p) {
1920
+ var size = parseFloat(p.getAttribute('data-size')) || (100 / panels.length);
1921
+ p.style.flexGrow = size;
1922
+ });
1923
+
1924
+ handles.forEach(function(handle) {
1925
+ var prevPanel = handle.previousElementSibling;
1926
+ var nextPanel = handle.nextElementSibling;
1927
+ if (!prevPanel || !nextPanel) return;
1928
+
1929
+ handle.setAttribute('tabindex', '0');
1930
+ handle.setAttribute('role', 'separator');
1931
+ handle.setAttribute('aria-orientation', isVertical ? 'horizontal' : 'vertical');
1932
+
1933
+ function getGrow(panel) {
1934
+ return parseFloat(panel.style.flexGrow) || 0;
1935
+ }
1936
+
1937
+ function getMin(panel) {
1938
+ return parseFloat(panel.getAttribute('data-min-size')) || 0;
1939
+ }
1940
+
1941
+ function resize(delta) {
1942
+ var prevG = getGrow(prevPanel);
1943
+ var nextG = getGrow(nextPanel);
1944
+ var total = prevG + nextG;
1945
+ var prevMin = getMin(prevPanel);
1946
+ var nextMin = getMin(nextPanel);
1947
+ var newPrev = Math.max(prevMin, Math.min(total - nextMin, prevG + delta));
1948
+ var newNext = total - newPrev;
1949
+ prevPanel.style.flexGrow = newPrev;
1950
+ nextPanel.style.flexGrow = newNext;
1951
+ }
1952
+
1953
+ function onPointerDown(e) {
1954
+ e.preventDefault();
1955
+ handle.focus();
1956
+ handle.classList.add('dragging');
1957
+
1958
+ var prevG = getGrow(prevPanel);
1959
+ var nextG = getGrow(nextPanel);
1960
+ var totalG = prevG + nextG;
1961
+
1962
+ // Measure actual pixel sizes of the two panels
1963
+ var prevPx = isVertical ? prevPanel.offsetHeight : prevPanel.offsetWidth;
1964
+ var nextPx = isVertical ? nextPanel.offsetHeight : nextPanel.offsetWidth;
1965
+ var pairPx = prevPx + nextPx;
1966
+ var startPos = isVertical ? e.clientY : e.clientX;
1967
+
1968
+ var prevMin = getMin(prevPanel);
1969
+ var nextMin = getMin(nextPanel);
1970
+
1971
+ function onPointerMove(ev) {
1972
+ var pos = isVertical ? ev.clientY : ev.clientX;
1973
+ var delta = pos - startPos;
1974
+ // Clamp delta so panels stay within 0..pairPx range
1975
+ delta = Math.max(-prevPx, Math.min(nextPx, delta));
1976
+ var ratio = pairPx > 0 ? delta / pairPx : 0;
1977
+ var newPrev = Math.max(prevMin, Math.min(totalG - nextMin, prevG + ratio * totalG));
1978
+ var newNext = totalG - newPrev;
1979
+ prevPanel.style.flexGrow = newPrev;
1980
+ nextPanel.style.flexGrow = newNext;
1981
+ }
1982
+
1983
+ function onPointerUp() {
1984
+ handle.classList.remove('dragging');
1985
+ document.removeEventListener('pointermove', onPointerMove);
1986
+ document.removeEventListener('pointerup', onPointerUp);
1987
+ }
1988
+
1989
+ document.addEventListener('pointermove', onPointerMove);
1990
+ document.addEventListener('pointerup', onPointerUp);
1991
+ }
1992
+
1993
+ handle.addEventListener('pointerdown', onPointerDown);
1994
+
1995
+ // Keyboard: Arrow keys resize, Home/End for extremes
1996
+ handle.addEventListener('keydown', function(e) {
1997
+ var step = e.shiftKey ? 10 : 2;
1998
+ var growKey = isVertical ? 'ArrowDown' : 'ArrowRight';
1999
+ var shrinkKey = isVertical ? 'ArrowUp' : 'ArrowLeft';
2000
+
2001
+ if (e.key === growKey) {
2002
+ e.preventDefault();
2003
+ resize(step);
2004
+ } else if (e.key === shrinkKey) {
2005
+ e.preventDefault();
2006
+ resize(-step);
2007
+ } else if (e.key === 'Home') {
2008
+ e.preventDefault();
2009
+ resize(-getGrow(prevPanel));
2010
+ } else if (e.key === 'End') {
2011
+ e.preventDefault();
2012
+ resize(getGrow(nextPanel));
2013
+ }
2014
+ });
2015
+ });
2016
+ });
2017
+ }
2018
+
2019
+ // =========================================
2020
+ // Popover
2021
+ // =========================================
2022
+ function initPopover() {
2023
+ document.addEventListener('click', (e) => {
2024
+ const trigger = e.target.closest('[data-sui-popover]');
2025
+ if (trigger) {
2026
+ const popover = trigger.closest('.sui-popover');
2027
+ if (!popover) return;
2028
+
2029
+ // Close all other open popovers
2030
+ document.querySelectorAll('.sui-popover.open').forEach(p => {
2031
+ if (p !== popover) {
2032
+ p.classList.remove('open');
2033
+ delayedRestore(p, 'sui-popover');
2034
+ const t = p.querySelector('[data-sui-popover]');
2035
+ if (t) t.setAttribute('aria-expanded', 'false');
2036
+ }
2037
+ });
2038
+
2039
+ const wasOpen = popover.classList.contains('open');
2040
+ if (!wasOpen) {
2041
+ autoReposition(popover, 'sui-popover');
2042
+ }
2043
+ popover.classList.toggle('open');
2044
+ const isNowOpen = popover.classList.contains('open');
2045
+ if (!isNowOpen) {
2046
+ delayedRestore(popover, 'sui-popover');
2047
+ }
2048
+ trigger.setAttribute('aria-expanded', isNowOpen ? 'true' : 'false');
2049
+ e.stopPropagation();
2050
+ return;
2051
+ }
2052
+
2053
+ // Close button inside popover
2054
+ const closeBtn = e.target.closest('.sui-popover-close');
2055
+ if (closeBtn) {
2056
+ const popover = closeBtn.closest('.sui-popover');
2057
+ if (popover) {
2058
+ popover.classList.remove('open');
2059
+ delayedRestore(popover, 'sui-popover');
2060
+ const t = popover.querySelector('[data-sui-popover]');
2061
+ if (t) {
2062
+ t.setAttribute('aria-expanded', 'false');
2063
+ t.focus();
2064
+ }
2065
+ }
2066
+ return;
2067
+ }
2068
+
2069
+ // Click inside popover content — do nothing
2070
+ if (e.target.closest('.sui-popover-content')) return;
2071
+
2072
+ // Click outside closes all popovers
2073
+ document.querySelectorAll('.sui-popover.open').forEach(p => {
2074
+ p.classList.remove('open');
2075
+ delayedRestore(p, 'sui-popover');
2076
+ const t = p.querySelector('[data-sui-popover]');
2077
+ if (t) t.setAttribute('aria-expanded', 'false');
2078
+ });
2079
+ });
2080
+
2081
+ // Escape closes popovers
2082
+ document.addEventListener('keydown', (e) => {
2083
+ if (e.key === 'Escape') {
2084
+ document.querySelectorAll('.sui-popover.open').forEach(p => {
2085
+ p.classList.remove('open');
2086
+ delayedRestore(p, 'sui-popover');
2087
+ const t = p.querySelector('[data-sui-popover]');
2088
+ if (t) {
2089
+ t.setAttribute('aria-expanded', 'false');
2090
+ t.focus();
2091
+ }
2092
+ });
2093
+ }
2094
+ });
2095
+ }
2096
+
2097
+ // =========================================
2098
+ // Slider
2099
+ // =========================================
2100
+ function initSliders() {
2101
+ document.querySelectorAll('.sui-slider').forEach(slider => {
2102
+ const input = slider.querySelector('input[type="range"]');
2103
+ const display = slider.querySelector('.sui-slider-value');
2104
+ if (!input || !display) return;
2105
+
2106
+ // Set initial value
2107
+ display.textContent = input.value;
2108
+
2109
+ // Update on input
2110
+ input.addEventListener('input', () => {
2111
+ display.textContent = input.value;
2112
+ });
2113
+ });
2114
+ }
2115
+
2116
+ // =========================================
2117
+ // Input OTP
2118
+ // =========================================
2119
+ function initOtp() {
2120
+ document.querySelectorAll('.sui-otp[data-sui-otp]').forEach(function(otp) {
2121
+ if (otp.dataset.suiOtpDisabled !== undefined) return;
2122
+
2123
+ var slots = otp.querySelectorAll('.sui-otp-slot');
2124
+ var len = slots.length;
2125
+ var pattern = otp.dataset.suiOtpPattern || 'digits'; // "digits" or "alphanumeric"
2126
+ var regex = pattern === 'alphanumeric' ? /^[a-zA-Z0-9]$/ : /^[0-9]$/;
2127
+
2128
+ // Create hidden input
2129
+ var input = document.createElement('input');
2130
+ input.className = 'sui-otp-input';
2131
+ input.setAttribute('inputmode', pattern === 'alphanumeric' ? 'text' : 'numeric');
2132
+ input.setAttribute('autocomplete', 'one-time-code');
2133
+ input.setAttribute('maxlength', len);
2134
+ input.setAttribute('aria-label', 'One-time code');
2135
+ otp.appendChild(input);
2136
+
2137
+ function updateSlots() {
2138
+ var val = input.value;
2139
+ slots.forEach(function(slot, i) {
2140
+ slot.textContent = val[i] || '';
2141
+ slot.classList.toggle('sui-otp-filled', !!val[i]);
2142
+ slot.classList.remove('sui-otp-active');
2143
+ });
2144
+ // Show cursor on current slot
2145
+ var pos = Math.min(val.length, len - 1);
2146
+ if (document.activeElement === input && val.length < len) {
2147
+ slots[pos].classList.add('sui-otp-active');
2148
+ } else if (document.activeElement === input && val.length === len) {
2149
+ slots[len - 1].classList.add('sui-otp-active');
2150
+ }
2151
+ }
2152
+
2153
+ input.addEventListener('input', function() {
2154
+ // Filter to allowed characters
2155
+ var filtered = '';
2156
+ for (var i = 0; i < input.value.length && filtered.length < len; i++) {
2157
+ if (regex.test(input.value[i])) {
2158
+ filtered += pattern === 'alphanumeric' ? input.value[i].toUpperCase() : input.value[i];
2159
+ }
2160
+ }
2161
+ input.value = filtered;
2162
+ updateSlots();
2163
+
2164
+ // Dispatch event when complete
2165
+ if (filtered.length === len) {
2166
+ otp.dispatchEvent(new CustomEvent('sui-otp-complete', { detail: { value: filtered } }));
2167
+ }
2168
+ });
2169
+
2170
+ input.addEventListener('focus', updateSlots);
2171
+ input.addEventListener('blur', function() {
2172
+ slots.forEach(function(s) { s.classList.remove('sui-otp-active'); });
2173
+ });
2174
+
2175
+ // Click on OTP area or individual slot focuses input
2176
+ otp.addEventListener('click', function(e) {
2177
+ input.focus();
2178
+ });
2179
+
2180
+ // Click on a specific slot positions cursor there
2181
+ slots.forEach(function(slot, i) {
2182
+ slot.addEventListener('click', function(e) {
2183
+ e.stopPropagation();
2184
+ input.focus();
2185
+ // Set cursor position
2186
+ var pos = Math.min(i, input.value.length);
2187
+ input.setSelectionRange(pos, pos);
2188
+ updateSlots();
2189
+ });
2190
+ });
2191
+
2192
+ updateSlots();
2193
+ });
2194
+ }
2195
+
2196
+ // =========================================
2197
+ // Toggle Group
2198
+ // =========================================
2199
+ function initToggleGroups() {
2200
+ document.querySelectorAll('.sui-toggle-group[data-sui-toggle]').forEach(function(group) {
2201
+ var mode = group.dataset.suiToggle; // "single" or "multi"
2202
+ var items = group.querySelectorAll('.sui-toggle-group-item:not([disabled])');
2203
+
2204
+ items.forEach(function(item) {
2205
+ item.addEventListener('click', function() {
2206
+ if (mode === 'single') {
2207
+ var wasActive = item.classList.contains('active');
2208
+ items.forEach(function(it) {
2209
+ it.classList.remove('active');
2210
+ it.setAttribute('aria-pressed', 'false');
2211
+ });
2212
+ if (!wasActive) {
2213
+ item.classList.add('active');
2214
+ item.setAttribute('aria-pressed', 'true');
2215
+ }
2216
+ } else {
2217
+ item.classList.toggle('active');
2218
+ item.setAttribute('aria-pressed', item.classList.contains('active') ? 'true' : 'false');
2219
+ }
2220
+ });
2221
+ });
2222
+ });
2223
+ }
2224
+
2225
+ // =========================================
2226
+ // Carousel
2227
+ // =========================================
2228
+ function carousel(selector) {
2229
+ const el = document.querySelector(selector);
2230
+ if (!el) return null;
2231
+
2232
+ const track = el.querySelector('.sui-carousel-track');
2233
+ if (!track) return null;
2234
+
2235
+ // Auto-wrap track in viewport
2236
+ if (!track.parentElement.classList.contains('sui-carousel-viewport')) {
2237
+ const viewport = document.createElement('div');
2238
+ viewport.className = 'sui-carousel-viewport';
2239
+ track.parentElement.insertBefore(viewport, track);
2240
+ viewport.appendChild(track);
2241
+ }
2242
+
2243
+ const realItems = Array.from(track.children);
2244
+ const isVertical = el.classList.contains('sui-carousel-vertical');
2245
+ const loopMode = el.hasAttribute('data-sui-loop') ? (el.dataset.suiLoop || 'seamless') : false;
2246
+ const isLoop = !!loopMode;
2247
+ const isSeamless = loopMode === 'seamless';
2248
+ const autoplayMs = parseInt(el.dataset.suiAutoplay) || 0;
2249
+
2250
+ let visible = 1;
2251
+ if (el.classList.contains('sui-carousel-4')) visible = 4;
2252
+ else if (el.classList.contains('sui-carousel-3')) visible = 3;
2253
+ else if (el.classList.contains('sui-carousel-2')) visible = 2;
2254
+
2255
+ const totalReal = realItems.length;
2256
+ const maxIndex = Math.max(0, totalReal - visible);
2257
+ let current = 0;
2258
+ let autoplayTimer = null;
2259
+ let jumping = false;
2260
+
2261
+ const prevBtn = el.querySelector('.sui-carousel-prev');
2262
+ const nextBtn = el.querySelector('.sui-carousel-next');
2263
+ const dots = el.querySelectorAll('.sui-carousel-dot');
2264
+
2265
+ // Seamless loop: clone slides at both ends
2266
+ let cloneCount = 0;
2267
+ if (isSeamless && totalReal > visible) {
2268
+ for (var i = totalReal - 1; i >= totalReal - visible; i--) {
2269
+ var clone = realItems[i].cloneNode(true);
2270
+ clone.setAttribute('aria-hidden', 'true');
2271
+ track.insertBefore(clone, track.firstChild);
2272
+ }
2273
+ for (var i = 0; i < visible; i++) {
2274
+ var clone = realItems[i].cloneNode(true);
2275
+ clone.setAttribute('aria-hidden', 'true');
2276
+ track.appendChild(clone);
2277
+ }
2278
+ cloneCount = visible;
2279
+ }
2280
+
2281
+ var allItems = Array.from(track.children);
2282
+
2283
+ function moveTo(displayIndex, animate) {
2284
+ if (animate === false) track.style.transition = 'none';
2285
+
2286
+ if (isVertical) {
2287
+ var vh = track.parentElement.offsetHeight;
2288
+ var itemH = vh / visible;
2289
+ allItems.forEach(function(it) { it.style.height = itemH + 'px'; });
2290
+ track.style.transform = 'translateY(-' + (displayIndex * itemH) + 'px)';
2291
+ } else {
2292
+ var item = allItems[displayIndex];
2293
+ var base = allItems[0];
2294
+ var px = (item && base) ? item.offsetLeft - base.offsetLeft : 0;
2295
+ track.style.transform = 'translateX(-' + px + 'px)';
2296
+ }
2297
+
2298
+ if (animate === false) {
2299
+ track.offsetHeight; // force reflow
2300
+ track.style.transition = '';
2301
+ }
2302
+ }
2303
+
2304
+ function update(animate) {
2305
+ var displayIndex = current + cloneCount;
2306
+ moveTo(displayIndex, animate);
2307
+
2308
+ var dotIndex = ((current % totalReal) + totalReal) % totalReal;
2309
+ dots.forEach(function(d, i) { d.classList.toggle('active', i === dotIndex); });
2310
+
2311
+ if (!isLoop) {
2312
+ if (prevBtn) prevBtn.disabled = current <= 0;
2313
+ if (nextBtn) nextBtn.disabled = current >= maxIndex;
2314
+ }
2315
+
2316
+ // Seamless jump after transition ends
2317
+ if (isSeamless && !jumping) {
2318
+ if (current >= totalReal) {
2319
+ jumping = true;
2320
+ setTimeout(function() {
2321
+ current -= totalReal;
2322
+ moveTo(current + cloneCount, false);
2323
+ jumping = false;
2324
+ }, 420);
2325
+ } else if (current <= -visible) {
2326
+ jumping = true;
2327
+ setTimeout(function() {
2328
+ current += totalReal;
2329
+ moveTo(current + cloneCount, false);
2330
+ jumping = false;
2331
+ }, 420);
2332
+ }
2333
+ }
2334
+ }
2335
+
2336
+ function goTo(index) {
2337
+ if (jumping) return;
2338
+ if (isLoop) {
2339
+ current = ((index % totalReal) + totalReal) % totalReal;
2340
+ } else {
2341
+ current = Math.max(0, Math.min(index, maxIndex));
2342
+ }
2343
+ update(true);
2344
+ }
2345
+
2346
+ function next() {
2347
+ if (jumping) return;
2348
+ if (isSeamless) {
2349
+ current++;
2350
+ } else if (isLoop) {
2351
+ current = (current + 1) > maxIndex ? 0 : current + 1;
2352
+ } else {
2353
+ if (current >= maxIndex) return;
2354
+ current++;
2355
+ }
2356
+ update(true);
2357
+ }
2358
+
2359
+ function prev() {
2360
+ if (jumping) return;
2361
+ if (isSeamless) {
2362
+ current--;
2363
+ } else if (isLoop) {
2364
+ current = (current - 1) < 0 ? maxIndex : current - 1;
2365
+ } else {
2366
+ if (current <= 0) return;
2367
+ current--;
2368
+ }
2369
+ update(true);
2370
+ }
2371
+
2372
+ if (prevBtn) prevBtn.addEventListener('click', function() { prev(); resetAutoplay(); });
2373
+ if (nextBtn) nextBtn.addEventListener('click', function() { next(); resetAutoplay(); });
2374
+ dots.forEach(function(d, i) { d.addEventListener('click', function() { goTo(i); resetAutoplay(); }); });
2375
+
2376
+ // Autoplay
2377
+ function startAutoplay() {
2378
+ if (autoplayMs > 0) {
2379
+ autoplayTimer = setInterval(next, autoplayMs);
2380
+ }
2381
+ }
2382
+
2383
+ function resetAutoplay() {
2384
+ if (autoplayTimer) clearInterval(autoplayTimer);
2385
+ startAutoplay();
2386
+ }
2387
+
2388
+ if (autoplayMs > 0) {
2389
+ el.addEventListener('mouseenter', function() { if (autoplayTimer) clearInterval(autoplayTimer); });
2390
+ el.addEventListener('mouseleave', function() { startAutoplay(); });
2391
+ }
2392
+
2393
+ update(false);
2394
+ startAutoplay();
2395
+
2396
+ return { next: next, prev: prev, goTo: goTo, current: function() { return current; } };
2397
+ }
2398
+
2399
+ function initCarousels() {
2400
+ document.querySelectorAll('.sui-carousel').forEach(el => {
2401
+ if (!el.id) return;
2402
+ carousel('#' + el.id);
2403
+ });
2404
+ }
2405
+
2406
+ // =========================================
2407
+ // Charts
2408
+ // =========================================
2409
+ function initCharts() {
2410
+ // Bar charts — set heights from data-value
2411
+ document.querySelectorAll('.sui-chart-bar-col').forEach(function(col) {
2412
+ // Skip grouped bars (handled separately)
2413
+ if (col.querySelector('.sui-chart-bar-group')) return;
2414
+ var fill = col.querySelector('.sui-chart-bar-fill');
2415
+ if (!fill) return;
2416
+ var val = parseFloat(fill.getAttribute('data-value'));
2417
+ if (isNaN(val)) return;
2418
+ var max = parseFloat(fill.getAttribute('data-max')) || 100;
2419
+ var pct = Math.min(100, Math.max(0, (val / max) * 100));
2420
+ fill.style.height = pct + '%';
2421
+ });
2422
+
2423
+ // Grouped bars — set heights for each fill in a group
2424
+ document.querySelectorAll('.sui-chart-bar-group').forEach(function(group) {
2425
+ group.querySelectorAll('.sui-chart-bar-fill').forEach(function(fill) {
2426
+ var val = parseFloat(fill.getAttribute('data-value'));
2427
+ if (isNaN(val)) return;
2428
+ var max = parseFloat(fill.getAttribute('data-max')) || 100;
2429
+ var pct = Math.min(100, Math.max(0, (val / max) * 100));
2430
+ fill.style.height = pct + '%';
2431
+ });
2432
+ });
2433
+
2434
+ // Horizontal bars
2435
+ document.querySelectorAll('.sui-chart-bar-row').forEach(function(row) {
2436
+ var fill = row.querySelector('.sui-chart-bar-fill');
2437
+ if (!fill) return;
2438
+ var val = parseFloat(fill.getAttribute('data-value'));
2439
+ if (isNaN(val)) return;
2440
+ var max = parseFloat(fill.getAttribute('data-max')) || 100;
2441
+ var pct = Math.min(100, Math.max(0, (val / max) * 100));
2442
+ fill.style.width = pct + '%';
2443
+ });
2444
+
2445
+ // Stacked bars
2446
+ document.querySelectorAll('.sui-chart-bar-track-stacked').forEach(function(track) {
2447
+ var fills = track.querySelectorAll('.sui-chart-bar-fill');
2448
+ var total = 0;
2449
+ fills.forEach(function(f) { total += parseFloat(f.getAttribute('data-value')) || 0; });
2450
+ var max = parseFloat(track.getAttribute('data-max')) || total || 100;
2451
+ fills.forEach(function(f) {
2452
+ var v = parseFloat(f.getAttribute('data-value')) || 0;
2453
+ f.style.height = ((v / max) * 100) + '%';
2454
+ });
2455
+ });
2456
+
2457
+ // Donut / Pie charts — build conic-gradient from data-segments
2458
+ document.querySelectorAll('.sui-chart-donut[data-segments]').forEach(function(donut) {
2459
+ try {
2460
+ var segments = JSON.parse(donut.getAttribute('data-segments'));
2461
+ var total = 0;
2462
+ segments.forEach(function(s) { total += s.value; });
2463
+ var stops = [];
2464
+ var cumulative = 0;
2465
+ segments.forEach(function(s) {
2466
+ var start = (cumulative / total) * 100;
2467
+ cumulative += s.value;
2468
+ var end = (cumulative / total) * 100;
2469
+ stops.push(s.color + ' ' + start + '% ' + end + '%');
2470
+ });
2471
+ donut.style.background = 'conic-gradient(' + stops.join(', ') + ')';
2472
+ } catch(e) {}
2473
+ });
2474
+
2475
+ // Line / Area charts — measure path length for animation
2476
+ document.querySelectorAll('.sui-chart-line-wrap .chart-line').forEach(function(path) {
2477
+ if (path.getTotalLength) {
2478
+ var len = path.getTotalLength();
2479
+ path.style.setProperty('--line-length', len);
2480
+ path.style.strokeDasharray = len;
2481
+ path.style.strokeDashoffset = len;
2482
+ }
2483
+ });
2484
+
2485
+ // SVG dot tooltips
2486
+ document.querySelectorAll('.sui-chart-line-wrap').forEach(function(wrap) {
2487
+ var dots = wrap.querySelectorAll('.chart-dot[data-value]');
2488
+ if (!dots.length) return;
2489
+
2490
+ var tip = document.createElement('div');
2491
+ tip.className = 'sui-chart-tooltip';
2492
+ wrap.appendChild(tip);
2493
+
2494
+ dots.forEach(function(dot) {
2495
+ dot.addEventListener('mouseenter', function() {
2496
+ var val = dot.getAttribute('data-value');
2497
+ tip.textContent = val;
2498
+ var svg = wrap.querySelector('svg');
2499
+ var svgRect = svg.getBoundingClientRect();
2500
+ var wrapRect = wrap.getBoundingClientRect();
2501
+ var cx = parseFloat(dot.getAttribute('cx'));
2502
+ var cy = parseFloat(dot.getAttribute('cy'));
2503
+ var viewBox = svg.viewBox.baseVal;
2504
+ var scaleX = svgRect.width / viewBox.width;
2505
+ var scaleY = svgRect.height / viewBox.height;
2506
+ var px = (cx * scaleX) + (svgRect.left - wrapRect.left);
2507
+ var py = (cy * scaleY) + (svgRect.top - wrapRect.top);
2508
+ tip.style.left = px + 'px';
2509
+ tip.style.top = (py - 8) + 'px';
2510
+ tip.classList.add('visible');
2511
+ });
2512
+ dot.addEventListener('mouseleave', function() {
2513
+ tip.classList.remove('visible');
2514
+ });
2515
+ });
2516
+ });
2517
+ }
2518
+
2519
+ function initDataTables() {
2520
+ document.querySelectorAll('.sui-datatable').forEach(function(dt) {
2521
+ var table = dt.querySelector('.sui-table');
2522
+ if (!table) return;
2523
+
2524
+ var tbody = table.querySelector('tbody');
2525
+ if (!tbody) return;
2526
+
2527
+ var allRows = Array.prototype.slice.call(tbody.querySelectorAll('tr'));
2528
+ var filteredRows = allRows.slice();
2529
+ var currentPage = 1;
2530
+
2531
+ // Per-page selector
2532
+ var perpageSelect = dt.querySelector('.sui-datatable-perpage select');
2533
+ var perPage = perpageSelect ? parseInt(perpageSelect.value, 10) : allRows.length;
2534
+
2535
+ // Info & pagination elements
2536
+ var infoEl = dt.querySelector('.sui-datatable-info');
2537
+ var paginationEl = dt.querySelector('.sui-datatable-pagination');
2538
+
2539
+ // Search input
2540
+ var searchInput = dt.querySelector('.sui-datatable-search input');
2541
+
2542
+ function render() {
2543
+ var total = filteredRows.length;
2544
+ var totalPages = perPage > 0 ? Math.ceil(total / perPage) : 1;
2545
+ if (currentPage > totalPages) currentPage = totalPages;
2546
+ if (currentPage < 1) currentPage = 1;
2547
+
2548
+ var start = (currentPage - 1) * perPage;
2549
+ var end = Math.min(start + perPage, total);
2550
+
2551
+ // Hide all rows, then show only current page
2552
+ allRows.forEach(function(row) { row.style.display = 'none'; });
2553
+ for (var i = start; i < end; i++) {
2554
+ filteredRows[i].style.display = '';
2555
+ }
2556
+
2557
+ // Show empty message if no results
2558
+ var emptyRow = tbody.querySelector('.sui-datatable-empty-row');
2559
+ if (total === 0) {
2560
+ if (!emptyRow) {
2561
+ emptyRow = document.createElement('tr');
2562
+ emptyRow.className = 'sui-datatable-empty-row';
2563
+ var td = document.createElement('td');
2564
+ td.className = 'sui-datatable-empty';
2565
+ td.colSpan = table.querySelectorAll('thead th').length;
2566
+ td.textContent = 'No matching records found.';
2567
+ emptyRow.appendChild(td);
2568
+ tbody.appendChild(emptyRow);
2569
+ }
2570
+ emptyRow.style.display = '';
2571
+ } else if (emptyRow) {
2572
+ emptyRow.style.display = 'none';
2573
+ }
2574
+
2575
+ // Update info text
2576
+ if (infoEl) {
2577
+ if (total === 0) {
2578
+ infoEl.textContent = 'No entries';
2579
+ } else {
2580
+ infoEl.textContent = 'Showing ' + (start + 1) + '–' + end + ' of ' + total;
2581
+ }
2582
+ }
2583
+
2584
+ // Build pagination buttons
2585
+ if (paginationEl) {
2586
+ paginationEl.innerHTML = '';
2587
+
2588
+ var prevBtn = document.createElement('button');
2589
+ prevBtn.textContent = '\u2039';
2590
+ prevBtn.disabled = currentPage <= 1;
2591
+ prevBtn.addEventListener('click', function() {
2592
+ if (currentPage > 1) { currentPage--; render(); }
2593
+ });
2594
+ paginationEl.appendChild(prevBtn);
2595
+
2596
+ for (var p = 1; p <= totalPages; p++) {
2597
+ (function(page) {
2598
+ var btn = document.createElement('button');
2599
+ btn.textContent = page;
2600
+ if (page === currentPage) btn.className = 'active';
2601
+ btn.addEventListener('click', function() {
2602
+ currentPage = page;
2603
+ render();
2604
+ });
2605
+ paginationEl.appendChild(btn);
2606
+ })(p);
2607
+ }
2608
+
2609
+ var nextBtn = document.createElement('button');
2610
+ nextBtn.textContent = '\u203A';
2611
+ nextBtn.disabled = currentPage >= totalPages;
2612
+ nextBtn.addEventListener('click', function() {
2613
+ if (currentPage < totalPages) { currentPage++; render(); }
2614
+ });
2615
+ paginationEl.appendChild(nextBtn);
2616
+ }
2617
+ }
2618
+
2619
+ // Search / filter
2620
+ if (searchInput) {
2621
+ searchInput.addEventListener('input', function() {
2622
+ var query = searchInput.value.toLowerCase().trim();
2623
+ filteredRows = allRows.filter(function(row) {
2624
+ return row.textContent.toLowerCase().indexOf(query) !== -1;
2625
+ });
2626
+ currentPage = 1;
2627
+ render();
2628
+ });
2629
+ }
2630
+
2631
+ // Per-page change
2632
+ if (perpageSelect) {
2633
+ perpageSelect.addEventListener('change', function() {
2634
+ perPage = parseInt(perpageSelect.value, 10);
2635
+ currentPage = 1;
2636
+ render();
2637
+ });
2638
+ }
2639
+
2640
+ // Sortable headers
2641
+ var ths = table.querySelectorAll('th[data-sort]');
2642
+ ths.forEach(function(th) {
2643
+ th.addEventListener('click', function() {
2644
+ var col = th.getAttribute('data-sort');
2645
+ var colIndex = Array.prototype.indexOf.call(th.parentElement.children, th);
2646
+ var type = col; // 'string' or 'number'
2647
+ var dir = 'asc';
2648
+
2649
+ if (th.classList.contains('sort-asc')) {
2650
+ dir = 'desc';
2651
+ }
2652
+
2653
+ // Reset all headers
2654
+ ths.forEach(function(h) { h.classList.remove('sort-asc', 'sort-desc'); });
2655
+ th.classList.add(dir === 'asc' ? 'sort-asc' : 'sort-desc');
2656
+
2657
+ filteredRows.sort(function(a, b) {
2658
+ var aText = a.children[colIndex] ? a.children[colIndex].textContent.trim() : '';
2659
+ var bText = b.children[colIndex] ? b.children[colIndex].textContent.trim() : '';
2660
+
2661
+ if (type === 'number') {
2662
+ var aNum = parseFloat(aText.replace(/[^0-9.\-]/g, '')) || 0;
2663
+ var bNum = parseFloat(bText.replace(/[^0-9.\-]/g, '')) || 0;
2664
+ return dir === 'asc' ? aNum - bNum : bNum - aNum;
2665
+ }
2666
+
2667
+ return dir === 'asc' ? aText.localeCompare(bText) : bText.localeCompare(aText);
2668
+ });
2669
+
2670
+ // Re-append sorted rows to DOM
2671
+ filteredRows.forEach(function(row) { tbody.appendChild(row); });
2672
+ currentPage = 1;
2673
+ render();
2674
+ });
2675
+ });
2676
+
2677
+ // Initial render
2678
+ render();
2679
+ });
2680
+ }
2681
+
2682
+ function initDragDrop() {
2683
+ // ── Sortable Lists ──
2684
+ document.querySelectorAll('.sui-sortable').forEach(function(list) {
2685
+ var dragItem = null;
2686
+
2687
+ list.querySelectorAll('.sui-sortable-item').forEach(function(item) {
2688
+ var handle = item.querySelector('.sui-sortable-handle');
2689
+ var dragTarget = handle || item;
2690
+
2691
+ dragTarget.setAttribute('draggable', 'true');
2692
+ if (handle) item.classList.add('has-handle');
2693
+
2694
+ dragTarget.addEventListener('dragstart', function(e) {
2695
+ dragItem = item;
2696
+ item.classList.add('dragging');
2697
+ e.dataTransfer.effectAllowed = 'move';
2698
+ });
2699
+
2700
+ item.addEventListener('dragover', function(e) {
2701
+ e.preventDefault();
2702
+ e.dataTransfer.dropEffect = 'move';
2703
+ if (item !== dragItem) {
2704
+ item.classList.add('drag-over');
2705
+ }
2706
+ });
2707
+
2708
+ item.addEventListener('dragleave', function() {
2709
+ item.classList.remove('drag-over');
2710
+ });
2711
+
2712
+ item.addEventListener('drop', function(e) {
2713
+ e.preventDefault();
2714
+ item.classList.remove('drag-over');
2715
+ if (!dragItem || dragItem === item) return;
2716
+ var items = Array.prototype.slice.call(list.children);
2717
+ var fromIndex = items.indexOf(dragItem);
2718
+ var toIndex = items.indexOf(item);
2719
+ if (fromIndex < toIndex) {
2720
+ list.insertBefore(dragItem, item.nextSibling);
2721
+ } else {
2722
+ list.insertBefore(dragItem, item);
2723
+ }
2724
+ });
2725
+
2726
+ item.addEventListener('dragend', function() {
2727
+ item.classList.remove('dragging');
2728
+ list.querySelectorAll('.drag-over').forEach(function(el) {
2729
+ el.classList.remove('drag-over');
2730
+ });
2731
+ dragItem = null;
2732
+ });
2733
+ });
2734
+ });
2735
+
2736
+ // ── Kanban ──
2737
+ document.querySelectorAll('.sui-kanban').forEach(function(kanban) {
2738
+ var dragCard = null;
2739
+
2740
+ kanban.querySelectorAll('.sui-kanban-card').forEach(function(card) {
2741
+ card.setAttribute('draggable', 'true');
2742
+
2743
+ card.addEventListener('dragstart', function(e) {
2744
+ dragCard = card;
2745
+ card.classList.add('dragging');
2746
+ e.dataTransfer.effectAllowed = 'move';
2747
+ });
2748
+
2749
+ card.addEventListener('dragend', function() {
2750
+ card.classList.remove('dragging');
2751
+ kanban.querySelectorAll('.drag-over').forEach(function(el) {
2752
+ el.classList.remove('drag-over');
2753
+ });
2754
+ dragCard = null;
2755
+ });
2756
+ });
2757
+
2758
+ kanban.querySelectorAll('.sui-kanban-col-body').forEach(function(col) {
2759
+ col.addEventListener('dragover', function(e) {
2760
+ e.preventDefault();
2761
+ e.dataTransfer.dropEffect = 'move';
2762
+ col.classList.add('drag-over');
2763
+
2764
+ // Position among siblings
2765
+ if (!dragCard) return;
2766
+ var afterEl = getDragAfterElement(col, e.clientY);
2767
+ if (afterEl) {
2768
+ col.insertBefore(dragCard, afterEl);
2769
+ } else {
2770
+ col.appendChild(dragCard);
2771
+ }
2772
+ });
2773
+
2774
+ col.addEventListener('dragleave', function(e) {
2775
+ if (!col.contains(e.relatedTarget)) {
2776
+ col.classList.remove('drag-over');
2777
+ }
2778
+ });
2779
+
2780
+ col.addEventListener('drop', function(e) {
2781
+ e.preventDefault();
2782
+ col.classList.remove('drag-over');
2783
+ });
2784
+ });
2785
+ });
2786
+
2787
+ function getDragAfterElement(container, y) {
2788
+ var els = Array.prototype.slice.call(
2789
+ container.querySelectorAll('.sui-kanban-card:not(.dragging)')
2790
+ );
2791
+ var closest = null;
2792
+ var closestOffset = Number.NEGATIVE_INFINITY;
2793
+ els.forEach(function(el) {
2794
+ var box = el.getBoundingClientRect();
2795
+ var offset = y - box.top - box.height / 2;
2796
+ if (offset < 0 && offset > closestOffset) {
2797
+ closestOffset = offset;
2798
+ closest = el;
2799
+ }
2800
+ });
2801
+ return closest;
2802
+ }
2803
+
2804
+ // ── Drop Zone ──
2805
+ document.querySelectorAll('.sui-dropzone').forEach(function(zone) {
2806
+ zone.addEventListener('dragover', function(e) {
2807
+ e.preventDefault();
2808
+ e.dataTransfer.dropEffect = 'copy';
2809
+ zone.classList.add('drag-over');
2810
+ });
2811
+
2812
+ zone.addEventListener('dragleave', function(e) {
2813
+ if (!zone.contains(e.relatedTarget)) {
2814
+ zone.classList.remove('drag-over');
2815
+ }
2816
+ });
2817
+
2818
+ zone.addEventListener('drop', function(e) {
2819
+ e.preventDefault();
2820
+ zone.classList.remove('drag-over');
2821
+ var files = e.dataTransfer.files;
2822
+ if (!files.length) return;
2823
+ var fileList = zone.querySelector('.sui-dropzone-files');
2824
+ if (!fileList) {
2825
+ fileList = document.createElement('div');
2826
+ fileList.className = 'sui-dropzone-files';
2827
+ zone.appendChild(fileList);
2828
+ }
2829
+ Array.prototype.slice.call(files).forEach(function(file) {
2830
+ var item = document.createElement('div');
2831
+ item.className = 'sui-dropzone-file';
2832
+ item.innerHTML = '<span>' + file.name + '</span><button class="sui-dropzone-file-remove" type="button">&times;</button>';
2833
+ item.querySelector('.sui-dropzone-file-remove').addEventListener('click', function(ev) {
2834
+ ev.stopPropagation();
2835
+ item.remove();
2836
+ });
2837
+ fileList.appendChild(item);
2838
+ });
2839
+ });
2840
+ });
2841
+ }
2842
+
2843
+ return { modal, sheet, toast, carousel };
2844
+ })();