@vanduo-oss/framework 1.2.5 → 1.2.7

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.
Files changed (53) hide show
  1. package/README.md +31 -5
  2. package/css/components/affix.css +53 -0
  3. package/css/components/bubble.css +165 -0
  4. package/css/components/datepicker.css +216 -0
  5. package/css/components/fab.css +225 -0
  6. package/css/components/flow.css +265 -0
  7. package/css/components/rating.css +112 -0
  8. package/css/components/ripple.css +63 -0
  9. package/css/components/sidenav.css +70 -0
  10. package/css/components/spotlight.css +119 -0
  11. package/css/components/stepper.css +176 -0
  12. package/css/components/suggest.css +119 -0
  13. package/css/components/timeline.css +201 -0
  14. package/css/components/timepicker.css +80 -0
  15. package/css/components/transfer.css +165 -0
  16. package/css/components/tree.css +173 -0
  17. package/css/components/waypoint.css +59 -0
  18. package/css/utilities/color-utilities.css +352 -0
  19. package/css/vanduo.css +20 -0
  20. package/dist/build-info.json +3 -3
  21. package/dist/vanduo.cjs.js +2152 -4
  22. package/dist/vanduo.cjs.js.map +4 -4
  23. package/dist/vanduo.cjs.min.js +5 -5
  24. package/dist/vanduo.cjs.min.js.map +4 -4
  25. package/dist/vanduo.css +3253 -271
  26. package/dist/vanduo.css.map +1 -1
  27. package/dist/vanduo.esm.js +2152 -4
  28. package/dist/vanduo.esm.js.map +4 -4
  29. package/dist/vanduo.esm.min.js +5 -5
  30. package/dist/vanduo.esm.min.js.map +4 -4
  31. package/dist/vanduo.js +2152 -4
  32. package/dist/vanduo.js.map +4 -4
  33. package/dist/vanduo.min.css +2 -2
  34. package/dist/vanduo.min.css.map +1 -1
  35. package/dist/vanduo.min.js +5 -5
  36. package/dist/vanduo.min.js.map +4 -4
  37. package/js/components/affix.js +129 -0
  38. package/js/components/bubble.js +203 -0
  39. package/js/components/datepicker.js +287 -0
  40. package/js/components/flow.js +264 -0
  41. package/js/components/rating.js +160 -0
  42. package/js/components/ripple.js +74 -0
  43. package/js/components/sidenav.js +9 -2
  44. package/js/components/spotlight.js +295 -0
  45. package/js/components/stepper.js +97 -0
  46. package/js/components/suggest.js +219 -0
  47. package/js/components/timepicker.js +142 -0
  48. package/js/components/transfer.js +206 -0
  49. package/js/components/tree.js +191 -0
  50. package/js/components/validate.js +185 -0
  51. package/js/components/waypoint.js +120 -0
  52. package/js/index.js +16 -0
  53. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- /*! Vanduo v1.2.5 | Built: 2026-03-08T10:52:48.224Z | git:f37c545 | development */
1
+ /*! Vanduo v1.2.7 | Built: 2026-03-12T16:17:38.253Z | git:2c1277a | development */
2
2
 
3
3
  // js/utils/lifecycle.js
4
4
  (function() {
@@ -107,7 +107,7 @@
107
107
  // js/vanduo.js
108
108
  (function() {
109
109
  "use strict";
110
- const VANDUO_VERSION = true ? "1.2.5" : "0.0.0-dev";
110
+ const VANDUO_VERSION = true ? "1.2.7" : "0.0.0-dev";
111
111
  const Vanduo2 = {
112
112
  version: VANDUO_VERSION,
113
113
  components: {},
@@ -3228,7 +3228,7 @@
3228
3228
  * Initialize sidenav components
3229
3229
  */
3230
3230
  init: function() {
3231
- const sidenavs = document.querySelectorAll(".vd-sidenav");
3231
+ const sidenavs = document.querySelectorAll(".vd-sidenav, .vd-offcanvas");
3232
3232
  sidenavs.forEach((sidenav) => {
3233
3233
  if (this.sidenavs.has(sidenav)) {
3234
3234
  return;
@@ -3262,8 +3262,13 @@
3262
3262
  * @param {HTMLElement} sidenav - Sidenav element
3263
3263
  */
3264
3264
  initSidenav: function(sidenav) {
3265
+ const position = sidenav.getAttribute("data-vd-position");
3266
+ if (position) {
3267
+ const prefix = sidenav.classList.contains("vd-offcanvas") ? "vd-offcanvas" : "vd-sidenav";
3268
+ sidenav.classList.add(prefix + "-" + position);
3269
+ }
3265
3270
  const overlay = this.createOverlay(sidenav);
3266
- const closeButton = sidenav.querySelector(".vd-sidenav-close");
3271
+ const closeButton = sidenav.querySelector(".vd-sidenav-close, .vd-offcanvas-close");
3267
3272
  const cleanupFunctions = [];
3268
3273
  sidenav.setAttribute("role", "navigation");
3269
3274
  sidenav.setAttribute("aria-hidden", "true");
@@ -6356,6 +6361,2149 @@
6356
6361
  window.VanduoLazyLoad = VanduoLazyLoad;
6357
6362
  })();
6358
6363
 
6364
+ // js/components/flow.js
6365
+ (function() {
6366
+ "use strict";
6367
+ const Flow = {
6368
+ instances: /* @__PURE__ */ new Map(),
6369
+ init: function() {
6370
+ const carousels = document.querySelectorAll(".vd-flow, .vd-carousel");
6371
+ carousels.forEach((el) => {
6372
+ if (this.instances.has(el)) return;
6373
+ this.initInstance(el);
6374
+ });
6375
+ },
6376
+ initInstance: function(el) {
6377
+ const track = el.querySelector(".vd-flow-track");
6378
+ if (!track) return;
6379
+ const slides = Array.from(track.querySelectorAll(".vd-flow-slide"));
6380
+ if (slides.length === 0) return;
6381
+ const isFade = el.classList.contains("vd-flow-fade");
6382
+ const autoplay = el.hasAttribute("data-vd-autoplay");
6383
+ const interval = parseInt(el.getAttribute("data-vd-interval"), 10) || 5e3;
6384
+ const loop = el.getAttribute("data-vd-loop") !== "false";
6385
+ const state = {
6386
+ current: 0,
6387
+ total: slides.length,
6388
+ autoplayTimer: null,
6389
+ isFade,
6390
+ loop,
6391
+ isDragging: false,
6392
+ startX: 0,
6393
+ currentX: 0,
6394
+ threshold: 50
6395
+ };
6396
+ const cleanup = [];
6397
+ slides.forEach((slide, i) => {
6398
+ slide.setAttribute("role", "group");
6399
+ slide.setAttribute("aria-roledescription", "slide");
6400
+ slide.setAttribute("aria-label", "Slide " + (i + 1) + " of " + slides.length);
6401
+ if (i === 0) slide.classList.add("is-active");
6402
+ });
6403
+ el.setAttribute("role", "region");
6404
+ el.setAttribute("aria-roledescription", "carousel");
6405
+ if (!el.getAttribute("aria-label")) {
6406
+ el.setAttribute("aria-label", "Carousel");
6407
+ }
6408
+ const liveRegion = document.createElement("div");
6409
+ liveRegion.setAttribute("aria-live", "polite");
6410
+ liveRegion.setAttribute("aria-atomic", "true");
6411
+ liveRegion.className = "sr-only";
6412
+ liveRegion.style.cssText = "position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);";
6413
+ el.appendChild(liveRegion);
6414
+ const goTo = (index, announce) => {
6415
+ if (announce === void 0) announce = true;
6416
+ let target = index;
6417
+ if (state.loop) {
6418
+ target = (index % state.total + state.total) % state.total;
6419
+ } else {
6420
+ target = Math.max(0, Math.min(index, state.total - 1));
6421
+ }
6422
+ const prev2 = state.current;
6423
+ state.current = target;
6424
+ if (state.isFade) {
6425
+ slides.forEach((s, i) => {
6426
+ s.classList.toggle("is-active", i === target);
6427
+ });
6428
+ } else {
6429
+ track.style.transform = "translateX(-" + target * 100 + "%)";
6430
+ }
6431
+ const indicators2 = el.querySelectorAll(".vd-flow-indicator");
6432
+ indicators2.forEach((ind, i) => {
6433
+ ind.classList.toggle("is-active", i === target);
6434
+ ind.setAttribute("aria-selected", i === target ? "true" : "false");
6435
+ });
6436
+ slides.forEach((s, i) => {
6437
+ s.setAttribute("aria-hidden", i !== target ? "true" : "false");
6438
+ });
6439
+ if (announce) {
6440
+ liveRegion.textContent = "Slide " + (target + 1) + " of " + state.total;
6441
+ }
6442
+ el.dispatchEvent(new CustomEvent("flow:change", {
6443
+ detail: { current: target, previous: prev2, total: state.total }
6444
+ }));
6445
+ };
6446
+ const next = () => goTo(state.current + 1);
6447
+ const prev = () => goTo(state.current - 1);
6448
+ const prevBtn = el.querySelector(".vd-flow-prev");
6449
+ const nextBtn = el.querySelector(".vd-flow-next");
6450
+ if (prevBtn) {
6451
+ const h = () => prev();
6452
+ prevBtn.addEventListener("click", h);
6453
+ cleanup.push(() => prevBtn.removeEventListener("click", h));
6454
+ }
6455
+ if (nextBtn) {
6456
+ const h = () => next();
6457
+ nextBtn.addEventListener("click", h);
6458
+ cleanup.push(() => nextBtn.removeEventListener("click", h));
6459
+ }
6460
+ const indicators = el.querySelectorAll(".vd-flow-indicator");
6461
+ indicators.forEach((ind, i) => {
6462
+ ind.setAttribute("role", "tab");
6463
+ ind.setAttribute("aria-selected", i === 0 ? "true" : "false");
6464
+ ind.setAttribute("aria-label", "Go to slide " + (i + 1));
6465
+ const h = () => goTo(i);
6466
+ ind.addEventListener("click", h);
6467
+ cleanup.push(() => ind.removeEventListener("click", h));
6468
+ });
6469
+ const keyHandler = (e) => {
6470
+ if (e.key === "ArrowLeft") {
6471
+ prev();
6472
+ e.preventDefault();
6473
+ }
6474
+ if (e.key === "ArrowRight") {
6475
+ next();
6476
+ e.preventDefault();
6477
+ }
6478
+ };
6479
+ el.setAttribute("tabindex", "0");
6480
+ el.addEventListener("keydown", keyHandler);
6481
+ cleanup.push(() => el.removeEventListener("keydown", keyHandler));
6482
+ const pointerDown = (e) => {
6483
+ state.isDragging = true;
6484
+ state.startX = e.clientX || e.touches && e.touches[0].clientX || 0;
6485
+ state.currentX = state.startX;
6486
+ el.classList.add("is-dragging");
6487
+ };
6488
+ const pointerMove = (e) => {
6489
+ if (!state.isDragging) return;
6490
+ state.currentX = e.clientX || e.touches && e.touches[0].clientX || 0;
6491
+ };
6492
+ const pointerUp = () => {
6493
+ if (!state.isDragging) return;
6494
+ state.isDragging = false;
6495
+ el.classList.remove("is-dragging");
6496
+ const diff = state.startX - state.currentX;
6497
+ if (Math.abs(diff) > state.threshold) {
6498
+ if (diff > 0) next();
6499
+ else prev();
6500
+ }
6501
+ };
6502
+ el.addEventListener("mousedown", pointerDown);
6503
+ el.addEventListener("mousemove", pointerMove);
6504
+ el.addEventListener("mouseup", pointerUp);
6505
+ el.addEventListener("mouseleave", pointerUp);
6506
+ el.addEventListener("touchstart", pointerDown, { passive: true });
6507
+ el.addEventListener("touchmove", pointerMove, { passive: true });
6508
+ el.addEventListener("touchend", pointerUp);
6509
+ cleanup.push(
6510
+ () => el.removeEventListener("mousedown", pointerDown),
6511
+ () => el.removeEventListener("mousemove", pointerMove),
6512
+ () => el.removeEventListener("mouseup", pointerUp),
6513
+ () => el.removeEventListener("mouseleave", pointerUp),
6514
+ () => el.removeEventListener("touchstart", pointerDown),
6515
+ () => el.removeEventListener("touchmove", pointerMove),
6516
+ () => el.removeEventListener("touchend", pointerUp)
6517
+ );
6518
+ const startAutoplay = () => {
6519
+ stopAutoplay();
6520
+ state.autoplayTimer = setInterval(next, interval);
6521
+ };
6522
+ const stopAutoplay = () => {
6523
+ if (state.autoplayTimer) {
6524
+ clearInterval(state.autoplayTimer);
6525
+ state.autoplayTimer = null;
6526
+ }
6527
+ };
6528
+ if (autoplay) {
6529
+ startAutoplay();
6530
+ const pauseHandler = () => stopAutoplay();
6531
+ const resumeHandler = () => startAutoplay();
6532
+ el.addEventListener("mouseenter", pauseHandler);
6533
+ el.addEventListener("mouseleave", resumeHandler);
6534
+ el.addEventListener("focusin", pauseHandler);
6535
+ el.addEventListener("focusout", resumeHandler);
6536
+ cleanup.push(
6537
+ () => el.removeEventListener("mouseenter", pauseHandler),
6538
+ () => el.removeEventListener("mouseleave", resumeHandler),
6539
+ () => el.removeEventListener("focusin", pauseHandler),
6540
+ () => el.removeEventListener("focusout", resumeHandler),
6541
+ () => stopAutoplay()
6542
+ );
6543
+ }
6544
+ goTo(0, false);
6545
+ this.instances.set(el, {
6546
+ cleanup,
6547
+ goTo,
6548
+ next,
6549
+ prev,
6550
+ getState: () => ({ ...state })
6551
+ });
6552
+ },
6553
+ goTo: function(el, index) {
6554
+ const instance = this.instances.get(el);
6555
+ if (instance) instance.goTo(index);
6556
+ },
6557
+ next: function(el) {
6558
+ const instance = this.instances.get(el);
6559
+ if (instance) instance.next();
6560
+ },
6561
+ prev: function(el) {
6562
+ const instance = this.instances.get(el);
6563
+ if (instance) instance.prev();
6564
+ },
6565
+ destroy: function(el) {
6566
+ const instance = this.instances.get(el);
6567
+ if (!instance) return;
6568
+ instance.cleanup.forEach((fn) => fn());
6569
+ this.instances.delete(el);
6570
+ },
6571
+ destroyAll: function() {
6572
+ this.instances.forEach((_, el) => this.destroy(el));
6573
+ }
6574
+ };
6575
+ if (typeof window.Vanduo !== "undefined") {
6576
+ window.Vanduo.register("flow", Flow);
6577
+ }
6578
+ window.VanduoFlow = Flow;
6579
+ })();
6580
+
6581
+ // js/components/bubble.js
6582
+ (function() {
6583
+ "use strict";
6584
+ const Bubble = {
6585
+ instances: /* @__PURE__ */ new Map(),
6586
+ _globalCleanups: [],
6587
+ init: function() {
6588
+ const triggers = document.querySelectorAll("[data-vd-bubble], [data-vd-popover]");
6589
+ triggers.forEach((el) => {
6590
+ if (this.instances.has(el)) return;
6591
+ this.initInstance(el);
6592
+ });
6593
+ if (this._globalCleanups.length === 0) {
6594
+ const outsideClick = (e) => {
6595
+ this.instances.forEach((inst, trigger) => {
6596
+ if (!inst.popover.contains(e.target) && !trigger.contains(e.target)) {
6597
+ this.hide(trigger);
6598
+ }
6599
+ });
6600
+ };
6601
+ const escHandler = (e) => {
6602
+ if (e.key === "Escape") {
6603
+ this.instances.forEach((_, trigger) => this.hide(trigger));
6604
+ }
6605
+ };
6606
+ document.addEventListener("click", outsideClick, true);
6607
+ document.addEventListener("keydown", escHandler);
6608
+ this._globalCleanups.push(
6609
+ () => document.removeEventListener("click", outsideClick, true),
6610
+ () => document.removeEventListener("keydown", escHandler)
6611
+ );
6612
+ }
6613
+ },
6614
+ initInstance: function(trigger) {
6615
+ const cleanup = [];
6616
+ const placement = trigger.getAttribute("data-vd-bubble-placement") || trigger.getAttribute("data-vd-popover-placement") || "bottom";
6617
+ const popover = document.createElement("div");
6618
+ popover.className = "vd-bubble-content";
6619
+ popover.setAttribute("role", "dialog");
6620
+ popover.setAttribute("aria-modal", "false");
6621
+ popover.setAttribute("data-placement", placement);
6622
+ const title = trigger.getAttribute("data-vd-bubble-title") || trigger.getAttribute("data-vd-popover-title");
6623
+ const content = trigger.getAttribute("data-vd-bubble") || trigger.getAttribute("data-vd-popover") || "";
6624
+ const htmlContent = trigger.getAttribute("data-vd-bubble-html") || trigger.getAttribute("data-vd-popover-html");
6625
+ if (title) {
6626
+ const header = document.createElement("div");
6627
+ header.className = "vd-bubble-header";
6628
+ const titleSpan = document.createElement("span");
6629
+ titleSpan.textContent = title;
6630
+ const closeBtn = document.createElement("button");
6631
+ closeBtn.className = "vd-bubble-close";
6632
+ closeBtn.setAttribute("aria-label", "Close");
6633
+ closeBtn.innerHTML = "×";
6634
+ header.appendChild(titleSpan);
6635
+ header.appendChild(closeBtn);
6636
+ popover.appendChild(header);
6637
+ const closeHandler = (e) => {
6638
+ e.stopPropagation();
6639
+ this.hide(trigger);
6640
+ };
6641
+ closeBtn.addEventListener("click", closeHandler);
6642
+ cleanup.push(() => closeBtn.removeEventListener("click", closeHandler));
6643
+ }
6644
+ const body = document.createElement("div");
6645
+ body.className = "vd-bubble-body";
6646
+ if (htmlContent) {
6647
+ if (typeof sanitizeHtml === "function") {
6648
+ body.innerHTML = sanitizeHtml(htmlContent);
6649
+ } else {
6650
+ body.textContent = htmlContent;
6651
+ }
6652
+ } else {
6653
+ body.textContent = content;
6654
+ }
6655
+ popover.appendChild(body);
6656
+ document.body.appendChild(popover);
6657
+ const popId = "vd-bubble-" + Math.random().toString(36).slice(2, 9);
6658
+ popover.id = popId;
6659
+ trigger.setAttribute("aria-haspopup", "dialog");
6660
+ trigger.setAttribute("aria-expanded", "false");
6661
+ trigger.setAttribute("aria-controls", popId);
6662
+ const toggleHandler = (e) => {
6663
+ e.stopPropagation();
6664
+ if (popover.classList.contains("is-visible")) {
6665
+ this.hide(trigger);
6666
+ } else {
6667
+ this.hideAll();
6668
+ this.show(trigger);
6669
+ }
6670
+ };
6671
+ trigger.addEventListener("click", toggleHandler);
6672
+ cleanup.push(() => trigger.removeEventListener("click", toggleHandler));
6673
+ this.instances.set(trigger, { popover, cleanup, placement });
6674
+ },
6675
+ position: function(trigger, popover, placement) {
6676
+ const rect = trigger.getBoundingClientRect();
6677
+ const popRect = popover.getBoundingClientRect();
6678
+ const gap = 10;
6679
+ let top, left;
6680
+ switch (placement) {
6681
+ case "top":
6682
+ top = rect.top - popRect.height - gap + window.scrollY;
6683
+ left = rect.left + (rect.width - popRect.width) / 2 + window.scrollX;
6684
+ break;
6685
+ case "left":
6686
+ top = rect.top + (rect.height - popRect.height) / 2 + window.scrollY;
6687
+ left = rect.left - popRect.width - gap + window.scrollX;
6688
+ break;
6689
+ case "right":
6690
+ top = rect.top + (rect.height - popRect.height) / 2 + window.scrollY;
6691
+ left = rect.right + gap + window.scrollX;
6692
+ break;
6693
+ default:
6694
+ top = rect.bottom + gap + window.scrollY;
6695
+ left = rect.left + (rect.width - popRect.width) / 2 + window.scrollX;
6696
+ }
6697
+ left = Math.max(8, Math.min(left, window.innerWidth - popRect.width - 8));
6698
+ top = Math.max(8, top);
6699
+ popover.style.top = top + "px";
6700
+ popover.style.left = left + "px";
6701
+ },
6702
+ show: function(trigger) {
6703
+ const instance = this.instances.get(trigger);
6704
+ if (!instance) return;
6705
+ const { popover, placement } = instance;
6706
+ popover.style.display = "block";
6707
+ popover.classList.add("is-visible");
6708
+ trigger.setAttribute("aria-expanded", "true");
6709
+ requestAnimationFrame(() => {
6710
+ this.position(trigger, popover, placement);
6711
+ });
6712
+ trigger.dispatchEvent(new CustomEvent("bubble:show", { bubbles: true }));
6713
+ },
6714
+ hide: function(trigger) {
6715
+ const instance = this.instances.get(trigger);
6716
+ if (!instance) return;
6717
+ instance.popover.classList.remove("is-visible");
6718
+ trigger.setAttribute("aria-expanded", "false");
6719
+ trigger.dispatchEvent(new CustomEvent("bubble:hide", { bubbles: true }));
6720
+ },
6721
+ hideAll: function() {
6722
+ this.instances.forEach((_, trigger) => this.hide(trigger));
6723
+ },
6724
+ destroy: function(trigger) {
6725
+ const instance = this.instances.get(trigger);
6726
+ if (!instance) return;
6727
+ instance.cleanup.forEach((fn) => fn());
6728
+ if (instance.popover.parentNode) {
6729
+ instance.popover.parentNode.removeChild(instance.popover);
6730
+ }
6731
+ trigger.removeAttribute("aria-haspopup");
6732
+ trigger.removeAttribute("aria-expanded");
6733
+ trigger.removeAttribute("aria-controls");
6734
+ this.instances.delete(trigger);
6735
+ },
6736
+ destroyAll: function() {
6737
+ this.instances.forEach((_, trigger) => this.destroy(trigger));
6738
+ this._globalCleanups.forEach((fn) => fn());
6739
+ this._globalCleanups = [];
6740
+ }
6741
+ };
6742
+ if (typeof window.Vanduo !== "undefined") {
6743
+ window.Vanduo.register("bubble", Bubble);
6744
+ }
6745
+ window.VanduoBubble = Bubble;
6746
+ })();
6747
+
6748
+ // js/components/waypoint.js
6749
+ (function() {
6750
+ "use strict";
6751
+ const Waypoint = {
6752
+ instances: /* @__PURE__ */ new Map(),
6753
+ init: function() {
6754
+ const navs = document.querySelectorAll("[data-vd-waypoint-nav], [data-vd-scrollspy-nav]");
6755
+ navs.forEach((nav) => {
6756
+ if (this.instances.has(nav)) return;
6757
+ this.initInstance(nav);
6758
+ });
6759
+ },
6760
+ initInstance: function(nav) {
6761
+ const links = Array.from(nav.querySelectorAll('a[href^="#"]'));
6762
+ if (links.length === 0) return;
6763
+ const cleanup = [];
6764
+ const offset = parseInt(nav.getAttribute("data-vd-waypoint-offset") || "80", 10);
6765
+ const sections = [];
6766
+ links.forEach((link) => {
6767
+ const id = link.getAttribute("href").slice(1);
6768
+ const section = document.getElementById(id);
6769
+ if (section) {
6770
+ section.setAttribute("data-vd-waypoint-section", "");
6771
+ sections.push({ id, link, section });
6772
+ }
6773
+ });
6774
+ if (sections.length === 0) return;
6775
+ const activeSections = /* @__PURE__ */ new Set();
6776
+ const setActive = (id) => {
6777
+ links.forEach((l) => l.classList.remove("is-active"));
6778
+ const target = links.find((l) => l.getAttribute("href") === "#" + id);
6779
+ if (target) {
6780
+ target.classList.add("is-active");
6781
+ nav.dispatchEvent(new CustomEvent("waypoint:change", {
6782
+ detail: { activeId: id, link: target }
6783
+ }));
6784
+ }
6785
+ };
6786
+ const rootMargin = "-" + offset + "px 0px -40% 0px";
6787
+ const observer = new IntersectionObserver((entries) => {
6788
+ entries.forEach((entry) => {
6789
+ if (entry.isIntersecting) {
6790
+ activeSections.add(entry.target.id);
6791
+ } else {
6792
+ activeSections.delete(entry.target.id);
6793
+ }
6794
+ });
6795
+ for (let i = 0; i < sections.length; i++) {
6796
+ if (activeSections.has(sections[i].id)) {
6797
+ setActive(sections[i].id);
6798
+ return;
6799
+ }
6800
+ }
6801
+ }, {
6802
+ rootMargin,
6803
+ threshold: 0
6804
+ });
6805
+ sections.forEach((s) => observer.observe(s.section));
6806
+ links.forEach((link) => {
6807
+ const clickHandler = (e) => {
6808
+ e.preventDefault();
6809
+ const id = link.getAttribute("href").slice(1);
6810
+ const section = document.getElementById(id);
6811
+ if (section) {
6812
+ section.scrollIntoView({ behavior: "smooth" });
6813
+ setActive(id);
6814
+ }
6815
+ };
6816
+ link.addEventListener("click", clickHandler);
6817
+ cleanup.push(() => link.removeEventListener("click", clickHandler));
6818
+ });
6819
+ cleanup.push(() => observer.disconnect());
6820
+ this.instances.set(nav, { observer, cleanup, sections, setActive });
6821
+ },
6822
+ refresh: function(nav) {
6823
+ this.destroy(nav);
6824
+ this.initInstance(nav);
6825
+ },
6826
+ destroy: function(nav) {
6827
+ const instance = this.instances.get(nav);
6828
+ if (!instance) return;
6829
+ instance.cleanup.forEach((fn) => fn());
6830
+ this.instances.delete(nav);
6831
+ },
6832
+ destroyAll: function() {
6833
+ this.instances.forEach((_, nav) => this.destroy(nav));
6834
+ }
6835
+ };
6836
+ if (typeof window.Vanduo !== "undefined") {
6837
+ window.Vanduo.register("waypoint", Waypoint);
6838
+ }
6839
+ window.VanduoWaypoint = Waypoint;
6840
+ })();
6841
+
6842
+ // js/components/ripple.js
6843
+ (function() {
6844
+ "use strict";
6845
+ const Ripple = {
6846
+ instances: /* @__PURE__ */ new Map(),
6847
+ init: function() {
6848
+ const elements = document.querySelectorAll(".vd-ripple, [data-vd-ripple]");
6849
+ elements.forEach((el) => {
6850
+ if (this.instances.has(el)) return;
6851
+ this.initInstance(el);
6852
+ });
6853
+ },
6854
+ initInstance: function(el) {
6855
+ const cleanup = [];
6856
+ const createWave = (e) => {
6857
+ const rect = el.getBoundingClientRect();
6858
+ const size = Math.max(rect.width, rect.height);
6859
+ const x = (e.clientX || e.touches && e.touches[0].clientX || rect.left + rect.width / 2) - rect.left - size / 2;
6860
+ const y = (e.clientY || e.touches && e.touches[0].clientY || rect.top + rect.height / 2) - rect.top - size / 2;
6861
+ const wave = document.createElement("span");
6862
+ wave.className = "vd-ripple-wave";
6863
+ wave.style.width = size + "px";
6864
+ wave.style.height = size + "px";
6865
+ wave.style.left = x + "px";
6866
+ wave.style.top = y + "px";
6867
+ el.appendChild(wave);
6868
+ wave.addEventListener("animationend", () => {
6869
+ if (wave.parentNode) wave.parentNode.removeChild(wave);
6870
+ });
6871
+ };
6872
+ el.addEventListener("mousedown", createWave);
6873
+ el.addEventListener("touchstart", createWave, { passive: true });
6874
+ cleanup.push(
6875
+ () => el.removeEventListener("mousedown", createWave),
6876
+ () => el.removeEventListener("touchstart", createWave)
6877
+ );
6878
+ this.instances.set(el, { cleanup });
6879
+ },
6880
+ destroy: function(el) {
6881
+ const instance = this.instances.get(el);
6882
+ if (!instance) return;
6883
+ instance.cleanup.forEach((fn) => fn());
6884
+ el.querySelectorAll(".vd-ripple-wave").forEach((w) => w.remove());
6885
+ this.instances.delete(el);
6886
+ },
6887
+ destroyAll: function() {
6888
+ this.instances.forEach((_, el) => this.destroy(el));
6889
+ }
6890
+ };
6891
+ if (typeof window.Vanduo !== "undefined") {
6892
+ window.Vanduo.register("ripple", Ripple);
6893
+ }
6894
+ window.VanduoRipple = Ripple;
6895
+ })();
6896
+
6897
+ // js/components/affix.js
6898
+ (function() {
6899
+ "use strict";
6900
+ function isScrollable(element) {
6901
+ if (!element || element === document.body) return false;
6902
+ const style = window.getComputedStyle(element);
6903
+ const overflowY = style.overflowY;
6904
+ const overflowX = style.overflowX;
6905
+ const canScrollY = /(auto|scroll|overlay)/.test(overflowY) && element.scrollHeight > element.clientHeight;
6906
+ const canScrollX = /(auto|scroll|overlay)/.test(overflowX) && element.scrollWidth > element.clientWidth;
6907
+ return canScrollY || canScrollX;
6908
+ }
6909
+ function getScrollParent(element) {
6910
+ let parent = element.parentElement;
6911
+ while (parent && parent !== document.body && parent !== document.documentElement) {
6912
+ if (isScrollable(parent)) return parent;
6913
+ parent = parent.parentElement;
6914
+ }
6915
+ return null;
6916
+ }
6917
+ const Affix = {
6918
+ instances: /* @__PURE__ */ new Map(),
6919
+ init: function() {
6920
+ const elements = document.querySelectorAll(".vd-affix, .vd-sticky, [data-vd-affix]");
6921
+ elements.forEach((el) => {
6922
+ if (this.instances.has(el)) return;
6923
+ this.initInstance(el);
6924
+ });
6925
+ },
6926
+ initInstance: function(el) {
6927
+ const cleanup = [];
6928
+ const parsedOffset = parseInt(el.getAttribute("data-vd-affix-offset") || "0", 10);
6929
+ const offset = Number.isNaN(parsedOffset) ? 0 : parsedOffset;
6930
+ const scrollParent = getScrollParent(el);
6931
+ let isStuck = false;
6932
+ const sentinel = document.createElement("div");
6933
+ sentinel.style.cssText = "display:block;height:1px;margin-bottom:-1px;visibility:hidden;pointer-events:none;";
6934
+ el.parentNode.insertBefore(sentinel, el);
6935
+ el.style.setProperty("--affix-top-offset", offset + "px");
6936
+ function stick() {
6937
+ if (isStuck) return;
6938
+ isStuck = true;
6939
+ el.classList.add("is-stuck");
6940
+ el.dispatchEvent(new CustomEvent("affix:stuck", {
6941
+ bubbles: true,
6942
+ detail: {
6943
+ offset,
6944
+ root: scrollParent || window
6945
+ }
6946
+ }));
6947
+ }
6948
+ function unstick() {
6949
+ if (!isStuck) return;
6950
+ isStuck = false;
6951
+ el.classList.remove("is-stuck");
6952
+ el.dispatchEvent(new CustomEvent("affix:unstuck", {
6953
+ bubbles: true,
6954
+ detail: {
6955
+ offset,
6956
+ root: scrollParent || window
6957
+ }
6958
+ }));
6959
+ }
6960
+ const observer = new IntersectionObserver(function(entries) {
6961
+ entries.forEach((entry) => {
6962
+ if (!entry.isIntersecting) {
6963
+ stick();
6964
+ } else {
6965
+ unstick();
6966
+ }
6967
+ });
6968
+ }, {
6969
+ root: scrollParent,
6970
+ rootMargin: "-" + offset + "px 0px 0px 0px",
6971
+ threshold: 0
6972
+ });
6973
+ observer.observe(sentinel);
6974
+ cleanup.push(
6975
+ () => observer.disconnect(),
6976
+ () => {
6977
+ if (sentinel.parentNode) sentinel.parentNode.removeChild(sentinel);
6978
+ },
6979
+ () => {
6980
+ el.classList.remove("is-stuck");
6981
+ el.style.removeProperty("--affix-top-offset");
6982
+ }
6983
+ );
6984
+ this.instances.set(el, { cleanup, observer, sentinel, scrollParent });
6985
+ },
6986
+ destroy: function(el) {
6987
+ const instance = this.instances.get(el);
6988
+ if (!instance) return;
6989
+ instance.cleanup.forEach((fn) => fn());
6990
+ el.classList.remove("is-stuck");
6991
+ this.instances.delete(el);
6992
+ },
6993
+ destroyAll: function() {
6994
+ this.instances.forEach((_, el) => this.destroy(el));
6995
+ }
6996
+ };
6997
+ if (typeof window.Vanduo !== "undefined") {
6998
+ window.Vanduo.register("affix", Affix);
6999
+ }
7000
+ window.VanduoAffix = Affix;
7001
+ })();
7002
+
7003
+ // js/components/suggest.js
7004
+ (function() {
7005
+ "use strict";
7006
+ const Suggest = {
7007
+ instances: /* @__PURE__ */ new Map(),
7008
+ init: function() {
7009
+ const inputs = document.querySelectorAll("[data-vd-suggest], [data-vd-autocomplete]");
7010
+ inputs.forEach((el) => {
7011
+ if (this.instances.has(el)) return;
7012
+ this.initInstance(el);
7013
+ });
7014
+ },
7015
+ initInstance: function(input) {
7016
+ const cleanup = [];
7017
+ const minChars = parseInt(input.getAttribute("data-vd-suggest-min-chars") || "1", 10);
7018
+ const url = input.getAttribute("data-vd-suggest-url") || "";
7019
+ const staticData = input.getAttribute("data-vd-suggest") || input.getAttribute("data-vd-autocomplete") || "";
7020
+ let items = [];
7021
+ try {
7022
+ items = JSON.parse(staticData);
7023
+ } catch (_e) {
7024
+ items = staticData.split(",").map((s) => s.trim()).filter(Boolean);
7025
+ }
7026
+ let wrapper = input.closest(".vd-suggest-wrapper, .vd-autocomplete-wrapper");
7027
+ if (!wrapper) {
7028
+ wrapper = document.createElement("div");
7029
+ wrapper.className = "vd-suggest-wrapper";
7030
+ input.parentNode.insertBefore(wrapper, input);
7031
+ wrapper.appendChild(input);
7032
+ }
7033
+ const list = document.createElement("ul");
7034
+ list.className = "vd-suggest-list";
7035
+ list.setAttribute("role", "listbox");
7036
+ const listId = "vd-suggest-" + Math.random().toString(36).slice(2, 9);
7037
+ list.id = listId;
7038
+ wrapper.appendChild(list);
7039
+ input.setAttribute("role", "combobox");
7040
+ input.setAttribute("aria-autocomplete", "list");
7041
+ input.setAttribute("aria-expanded", "false");
7042
+ input.setAttribute("aria-controls", listId);
7043
+ input.setAttribute("autocomplete", "off");
7044
+ let highlighted = -1;
7045
+ let currentItems = [];
7046
+ let debounceTimer = null;
7047
+ const renderItems = (filtered, query) => {
7048
+ list.innerHTML = "";
7049
+ currentItems = filtered;
7050
+ highlighted = -1;
7051
+ if (filtered.length === 0) {
7052
+ const empty = document.createElement("li");
7053
+ empty.className = "vd-suggest-empty";
7054
+ empty.textContent = "No results";
7055
+ list.appendChild(empty);
7056
+ return;
7057
+ }
7058
+ filtered.forEach((item, i) => {
7059
+ const li = document.createElement("li");
7060
+ li.className = "vd-suggest-item";
7061
+ li.setAttribute("role", "option");
7062
+ li.id = listId + "-item-" + i;
7063
+ const text = typeof item === "object" ? item.label || item.text || String(item) : String(item);
7064
+ if (query) {
7065
+ const re = new RegExp("(" + query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + ")", "gi");
7066
+ li.innerHTML = text.replace(re, '<span class="vd-suggest-match">$1</span>');
7067
+ } else {
7068
+ li.textContent = text;
7069
+ }
7070
+ li.addEventListener("click", () => selectItem(i));
7071
+ list.appendChild(li);
7072
+ });
7073
+ };
7074
+ const open = () => {
7075
+ list.classList.add("is-open");
7076
+ input.setAttribute("aria-expanded", "true");
7077
+ };
7078
+ const close = () => {
7079
+ list.classList.remove("is-open");
7080
+ input.setAttribute("aria-expanded", "false");
7081
+ highlighted = -1;
7082
+ input.removeAttribute("aria-activedescendant");
7083
+ };
7084
+ const selectItem = (index) => {
7085
+ const item = currentItems[index];
7086
+ const value = typeof item === "object" ? item.value || item.label || String(item) : String(item);
7087
+ input.value = value;
7088
+ close();
7089
+ input.dispatchEvent(new CustomEvent("suggest:select", {
7090
+ detail: { value, item, index },
7091
+ bubbles: true
7092
+ }));
7093
+ };
7094
+ const highlight = (index) => {
7095
+ const listItems = list.querySelectorAll(".vd-suggest-item");
7096
+ listItems.forEach((li) => li.classList.remove("is-highlighted"));
7097
+ if (index >= 0 && index < listItems.length) {
7098
+ highlighted = index;
7099
+ listItems[index].classList.add("is-highlighted");
7100
+ input.setAttribute("aria-activedescendant", listItems[index].id);
7101
+ listItems[index].scrollIntoView({ block: "nearest" });
7102
+ }
7103
+ };
7104
+ const doSearch = async (query) => {
7105
+ if (query.length < minChars) {
7106
+ close();
7107
+ return;
7108
+ }
7109
+ let filtered;
7110
+ if (url) {
7111
+ try {
7112
+ const separator = url.includes("?") ? "&" : "?";
7113
+ const res = await window.fetch(url + separator + "q=" + encodeURIComponent(query));
7114
+ filtered = await res.json();
7115
+ } catch (_e) {
7116
+ filtered = [];
7117
+ }
7118
+ } else {
7119
+ const lower = query.toLowerCase();
7120
+ filtered = items.filter((item) => {
7121
+ const text = typeof item === "object" ? item.label || item.text || String(item) : String(item);
7122
+ return text.toLowerCase().includes(lower);
7123
+ });
7124
+ }
7125
+ renderItems(filtered, query);
7126
+ if (filtered.length > 0) open();
7127
+ else open();
7128
+ };
7129
+ const inputHandler = () => {
7130
+ clearTimeout(debounceTimer);
7131
+ debounceTimer = setTimeout(() => doSearch(input.value), 200);
7132
+ };
7133
+ const keyHandler = (e) => {
7134
+ if (!list.classList.contains("is-open")) {
7135
+ if (e.key === "ArrowDown") {
7136
+ doSearch(input.value);
7137
+ e.preventDefault();
7138
+ }
7139
+ return;
7140
+ }
7141
+ const total = currentItems.length;
7142
+ switch (e.key) {
7143
+ case "ArrowDown":
7144
+ e.preventDefault();
7145
+ highlight(highlighted < total - 1 ? highlighted + 1 : 0);
7146
+ break;
7147
+ case "ArrowUp":
7148
+ e.preventDefault();
7149
+ highlight(highlighted > 0 ? highlighted - 1 : total - 1);
7150
+ break;
7151
+ case "Enter":
7152
+ e.preventDefault();
7153
+ if (highlighted >= 0) selectItem(highlighted);
7154
+ break;
7155
+ case "Escape":
7156
+ close();
7157
+ break;
7158
+ }
7159
+ };
7160
+ const blurHandler = () => {
7161
+ setTimeout(close, 200);
7162
+ };
7163
+ input.addEventListener("input", inputHandler);
7164
+ input.addEventListener("keydown", keyHandler);
7165
+ input.addEventListener("blur", blurHandler);
7166
+ input.addEventListener("focus", () => {
7167
+ if (input.value.length >= minChars) doSearch(input.value);
7168
+ });
7169
+ cleanup.push(
7170
+ () => input.removeEventListener("input", inputHandler),
7171
+ () => input.removeEventListener("keydown", keyHandler),
7172
+ () => input.removeEventListener("blur", blurHandler),
7173
+ () => clearTimeout(debounceTimer),
7174
+ () => {
7175
+ if (list.parentNode) list.parentNode.removeChild(list);
7176
+ }
7177
+ );
7178
+ this.instances.set(input, { cleanup, list, close });
7179
+ },
7180
+ destroy: function(el) {
7181
+ const instance = this.instances.get(el);
7182
+ if (!instance) return;
7183
+ instance.cleanup.forEach((fn) => fn());
7184
+ this.instances.delete(el);
7185
+ },
7186
+ destroyAll: function() {
7187
+ this.instances.forEach((_, el) => this.destroy(el));
7188
+ }
7189
+ };
7190
+ if (typeof window.Vanduo !== "undefined") {
7191
+ window.Vanduo.register("suggest", Suggest);
7192
+ }
7193
+ window.VanduoSuggest = Suggest;
7194
+ })();
7195
+
7196
+ // js/components/validate.js
7197
+ (function() {
7198
+ "use strict";
7199
+ const Validate = {
7200
+ instances: /* @__PURE__ */ new Map(),
7201
+ rules: {
7202
+ required: (value) => value.trim().length > 0,
7203
+ email: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
7204
+ url: (value) => {
7205
+ try {
7206
+ new URL(value);
7207
+ return true;
7208
+ } catch (_e) {
7209
+ return false;
7210
+ }
7211
+ },
7212
+ number: (value) => !isNaN(parseFloat(value)) && isFinite(value),
7213
+ min: (value, param) => value.length >= parseInt(param, 10),
7214
+ max: (value, param) => value.length <= parseInt(param, 10),
7215
+ minVal: (value, param) => parseFloat(value) >= parseFloat(param),
7216
+ maxVal: (value, param) => parseFloat(value) <= parseFloat(param),
7217
+ pattern: (value, param) => {
7218
+ try {
7219
+ return new RegExp(param).test(value);
7220
+ } catch (_e) {
7221
+ return false;
7222
+ }
7223
+ },
7224
+ match: (value, param) => {
7225
+ const other = document.querySelector('[name="' + param + '"]');
7226
+ return other ? value === other.value : false;
7227
+ }
7228
+ },
7229
+ messages: {
7230
+ required: "This field is required",
7231
+ email: "Please enter a valid email address",
7232
+ url: "Please enter a valid URL",
7233
+ number: "Please enter a valid number",
7234
+ min: "Minimum {0} characters required",
7235
+ max: "Maximum {0} characters allowed",
7236
+ minVal: "Value must be at least {0}",
7237
+ maxVal: "Value must be at most {0}",
7238
+ pattern: "Invalid format",
7239
+ match: "Fields do not match"
7240
+ },
7241
+ init: function() {
7242
+ const forms = document.querySelectorAll("[data-vd-validate], .vd-validate");
7243
+ forms.forEach((form) => {
7244
+ if (this.instances.has(form)) return;
7245
+ this.initInstance(form);
7246
+ });
7247
+ },
7248
+ initInstance: function(form) {
7249
+ const cleanup = [];
7250
+ const mode = form.getAttribute("data-vd-validate-mode") || "blur";
7251
+ const fields = form.querySelectorAll("[data-vd-rules]");
7252
+ const validateField = (field) => {
7253
+ const rulesStr = field.getAttribute("data-vd-rules") || "";
7254
+ const rules = rulesStr.split("|").map((r) => r.trim()).filter(Boolean);
7255
+ const value = field.value;
7256
+ const errors = [];
7257
+ for (const rule of rules) {
7258
+ const [name, ...params] = rule.split(":");
7259
+ const param = params.join(":");
7260
+ const validator = this.rules[name];
7261
+ if (validator && !validator(value, param)) {
7262
+ const customMsg = field.getAttribute("data-vd-msg-" + name);
7263
+ let msg = customMsg || this.messages[name] || "Invalid";
7264
+ if (param) msg = msg.replace("{0}", param);
7265
+ errors.push(msg);
7266
+ break;
7267
+ }
7268
+ }
7269
+ this.setFieldState(field, errors);
7270
+ return errors.length === 0;
7271
+ };
7272
+ const validateAll = () => {
7273
+ let valid = true;
7274
+ fields.forEach((field) => {
7275
+ if (!validateField(field)) valid = false;
7276
+ });
7277
+ return valid;
7278
+ };
7279
+ fields.forEach((field) => {
7280
+ if (mode === "input" || mode === "blur") {
7281
+ const eventType = mode === "input" ? "input" : "blur";
7282
+ const handler = () => validateField(field);
7283
+ field.addEventListener(eventType, handler);
7284
+ cleanup.push(() => field.removeEventListener(eventType, handler));
7285
+ if (mode === "blur") {
7286
+ const inputClear = () => {
7287
+ if (field.classList.contains("is-invalid") || field.classList.contains("is-valid")) {
7288
+ validateField(field);
7289
+ }
7290
+ };
7291
+ field.addEventListener("input", inputClear);
7292
+ cleanup.push(() => field.removeEventListener("input", inputClear));
7293
+ }
7294
+ }
7295
+ });
7296
+ const submitHandler = (e) => {
7297
+ const valid = validateAll();
7298
+ if (!valid) {
7299
+ e.preventDefault();
7300
+ e.stopPropagation();
7301
+ const firstInvalid = form.querySelector(".is-invalid");
7302
+ if (firstInvalid) firstInvalid.focus();
7303
+ }
7304
+ form.dispatchEvent(new CustomEvent("validate:submit", {
7305
+ detail: { valid },
7306
+ bubbles: true
7307
+ }));
7308
+ };
7309
+ form.addEventListener("submit", submitHandler);
7310
+ cleanup.push(() => form.removeEventListener("submit", submitHandler));
7311
+ this.instances.set(form, { cleanup, validateAll, validateField });
7312
+ },
7313
+ setFieldState: function(field, errors) {
7314
+ const wrapper = field.closest(".vd-form-group") || field.parentElement;
7315
+ let errorEl = wrapper.querySelector(".vd-validate-error");
7316
+ field.classList.remove("is-valid", "is-invalid");
7317
+ if (errors.length > 0) {
7318
+ field.classList.add("is-invalid");
7319
+ field.setAttribute("aria-invalid", "true");
7320
+ if (!errorEl) {
7321
+ errorEl = document.createElement("div");
7322
+ errorEl.className = "vd-validate-error";
7323
+ errorEl.id = "vd-err-" + Math.random().toString(36).slice(2, 9);
7324
+ errorEl.setAttribute("role", "alert");
7325
+ wrapper.appendChild(errorEl);
7326
+ }
7327
+ errorEl.textContent = errors[0];
7328
+ errorEl.style.display = "";
7329
+ field.setAttribute("aria-describedby", errorEl.id);
7330
+ } else if (field.value.trim()) {
7331
+ field.classList.add("is-valid");
7332
+ field.removeAttribute("aria-invalid");
7333
+ if (errorEl) errorEl.style.display = "none";
7334
+ } else {
7335
+ field.removeAttribute("aria-invalid");
7336
+ if (errorEl) errorEl.style.display = "none";
7337
+ }
7338
+ },
7339
+ validateForm: function(form) {
7340
+ const instance = this.instances.get(form);
7341
+ return instance ? instance.validateAll() : false;
7342
+ },
7343
+ addRule: function(name, validator, message) {
7344
+ this.rules[name] = validator;
7345
+ if (message) this.messages[name] = message;
7346
+ },
7347
+ destroy: function(form) {
7348
+ const instance = this.instances.get(form);
7349
+ if (!instance) return;
7350
+ instance.cleanup.forEach((fn) => fn());
7351
+ this.instances.delete(form);
7352
+ },
7353
+ destroyAll: function() {
7354
+ this.instances.forEach((_, form) => this.destroy(form));
7355
+ }
7356
+ };
7357
+ if (typeof window.Vanduo !== "undefined") {
7358
+ window.Vanduo.register("validate", Validate);
7359
+ }
7360
+ window.VanduoValidate = Validate;
7361
+ })();
7362
+
7363
+ // js/components/datepicker.js
7364
+ (function() {
7365
+ "use strict";
7366
+ const DAYS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
7367
+ const MONTHS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
7368
+ const Datepicker = {
7369
+ instances: /* @__PURE__ */ new Map(),
7370
+ init: function() {
7371
+ const inputs = document.querySelectorAll("[data-vd-datepicker]");
7372
+ inputs.forEach((el) => {
7373
+ if (this.instances.has(el)) return;
7374
+ this.initInstance(el);
7375
+ });
7376
+ },
7377
+ initInstance: function(input) {
7378
+ const cleanup = [];
7379
+ const format = input.getAttribute("data-vd-datepicker-format") || "yyyy-mm-dd";
7380
+ const minStr = input.getAttribute("data-vd-datepicker-min");
7381
+ const maxStr = input.getAttribute("data-vd-datepicker-max");
7382
+ const minDate = minStr ? new Date(minStr) : null;
7383
+ const maxDate = maxStr ? new Date(maxStr) : null;
7384
+ const today = /* @__PURE__ */ new Date();
7385
+ let viewYear = today.getFullYear();
7386
+ let viewMonth = today.getMonth();
7387
+ let selectedDate = null;
7388
+ let viewMode = "days";
7389
+ if (input.value) {
7390
+ const parsed = new Date(input.value);
7391
+ if (!isNaN(parsed.getTime())) {
7392
+ selectedDate = parsed;
7393
+ viewYear = parsed.getFullYear();
7394
+ viewMonth = parsed.getMonth();
7395
+ }
7396
+ }
7397
+ const popup = document.createElement("div");
7398
+ popup.className = "vd-datepicker-popup";
7399
+ popup.setAttribute("role", "dialog");
7400
+ popup.setAttribute("aria-label", "Choose date");
7401
+ const wrapper = document.createElement("div");
7402
+ wrapper.className = "vd-suggest-wrapper";
7403
+ wrapper.style.position = "relative";
7404
+ wrapper.style.display = "inline-block";
7405
+ input.parentNode.insertBefore(wrapper, input);
7406
+ wrapper.appendChild(input);
7407
+ wrapper.appendChild(popup);
7408
+ const formatDate = (d) => {
7409
+ const yyyy = d.getFullYear();
7410
+ const mm = String(d.getMonth() + 1).padStart(2, "0");
7411
+ const dd = String(d.getDate()).padStart(2, "0");
7412
+ return format.replace("yyyy", yyyy).replace("mm", mm).replace("dd", dd);
7413
+ };
7414
+ const isDisabled = (d) => {
7415
+ if (minDate && d < minDate) return true;
7416
+ if (maxDate && d > maxDate) return true;
7417
+ return false;
7418
+ };
7419
+ const isSameDay = (a, b) => a && b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
7420
+ const render = () => {
7421
+ popup.innerHTML = "";
7422
+ const header = document.createElement("div");
7423
+ header.className = "vd-datepicker-header";
7424
+ const prevBtn = document.createElement("button");
7425
+ prevBtn.type = "button";
7426
+ prevBtn.className = "vd-datepicker-prev";
7427
+ prevBtn.innerHTML = "&#8249;";
7428
+ prevBtn.setAttribute("aria-label", "Previous");
7429
+ const nextBtn = document.createElement("button");
7430
+ nextBtn.type = "button";
7431
+ nextBtn.className = "vd-datepicker-next";
7432
+ nextBtn.innerHTML = "&#8250;";
7433
+ nextBtn.setAttribute("aria-label", "Next");
7434
+ const title = document.createElement("span");
7435
+ title.className = "vd-datepicker-title";
7436
+ if (viewMode === "days") {
7437
+ title.textContent = MONTHS[viewMonth] + " " + viewYear;
7438
+ title.addEventListener("click", () => {
7439
+ viewMode = "months";
7440
+ render();
7441
+ });
7442
+ prevBtn.addEventListener("click", () => {
7443
+ viewMonth--;
7444
+ if (viewMonth < 0) {
7445
+ viewMonth = 11;
7446
+ viewYear--;
7447
+ }
7448
+ render();
7449
+ });
7450
+ nextBtn.addEventListener("click", () => {
7451
+ viewMonth++;
7452
+ if (viewMonth > 11) {
7453
+ viewMonth = 0;
7454
+ viewYear++;
7455
+ }
7456
+ render();
7457
+ });
7458
+ } else if (viewMode === "months") {
7459
+ title.textContent = String(viewYear);
7460
+ title.addEventListener("click", () => {
7461
+ viewMode = "years";
7462
+ render();
7463
+ });
7464
+ prevBtn.addEventListener("click", () => {
7465
+ viewYear--;
7466
+ render();
7467
+ });
7468
+ nextBtn.addEventListener("click", () => {
7469
+ viewYear++;
7470
+ render();
7471
+ });
7472
+ } else {
7473
+ const decadeStart = Math.floor(viewYear / 10) * 10;
7474
+ title.textContent = decadeStart + " - " + (decadeStart + 9);
7475
+ prevBtn.addEventListener("click", () => {
7476
+ viewYear -= 10;
7477
+ render();
7478
+ });
7479
+ nextBtn.addEventListener("click", () => {
7480
+ viewYear += 10;
7481
+ render();
7482
+ });
7483
+ }
7484
+ header.appendChild(prevBtn);
7485
+ header.appendChild(title);
7486
+ header.appendChild(nextBtn);
7487
+ popup.appendChild(header);
7488
+ if (viewMode === "days") {
7489
+ const weekdays = document.createElement("div");
7490
+ weekdays.className = "vd-datepicker-weekdays";
7491
+ DAYS.forEach((d) => {
7492
+ const span = document.createElement("span");
7493
+ span.textContent = d;
7494
+ weekdays.appendChild(span);
7495
+ });
7496
+ popup.appendChild(weekdays);
7497
+ const grid = document.createElement("div");
7498
+ grid.className = "vd-datepicker-days";
7499
+ const firstDay = new Date(viewYear, viewMonth, 1).getDay();
7500
+ const daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate();
7501
+ const daysInPrev = new Date(viewYear, viewMonth, 0).getDate();
7502
+ for (let i = firstDay - 1; i >= 0; i--) {
7503
+ const btn = createDayBtn(daysInPrev - i, true);
7504
+ grid.appendChild(btn);
7505
+ }
7506
+ for (let d = 1; d <= daysInMonth; d++) {
7507
+ const date = new Date(viewYear, viewMonth, d);
7508
+ const btn = createDayBtn(d, false, date);
7509
+ grid.appendChild(btn);
7510
+ }
7511
+ const totalCells = firstDay + daysInMonth;
7512
+ const remaining = totalCells % 7 === 0 ? 0 : 7 - totalCells % 7;
7513
+ for (let i = 1; i <= remaining; i++) {
7514
+ const btn = createDayBtn(i, true);
7515
+ grid.appendChild(btn);
7516
+ }
7517
+ popup.appendChild(grid);
7518
+ } else if (viewMode === "months") {
7519
+ const grid = document.createElement("div");
7520
+ grid.className = "vd-datepicker-months";
7521
+ MONTHS.forEach((name, i) => {
7522
+ const btn = document.createElement("button");
7523
+ btn.type = "button";
7524
+ btn.className = "vd-datepicker-month-btn";
7525
+ btn.textContent = name.slice(0, 3);
7526
+ if (selectedDate && selectedDate.getFullYear() === viewYear && selectedDate.getMonth() === i) {
7527
+ btn.classList.add("is-selected");
7528
+ }
7529
+ btn.addEventListener("click", () => {
7530
+ viewMonth = i;
7531
+ viewMode = "days";
7532
+ render();
7533
+ });
7534
+ grid.appendChild(btn);
7535
+ });
7536
+ popup.appendChild(grid);
7537
+ } else {
7538
+ const grid = document.createElement("div");
7539
+ grid.className = "vd-datepicker-years";
7540
+ const decadeStart = Math.floor(viewYear / 10) * 10;
7541
+ for (let y = decadeStart - 1; y <= decadeStart + 10; y++) {
7542
+ const btn = document.createElement("button");
7543
+ btn.type = "button";
7544
+ btn.className = "vd-datepicker-year-btn";
7545
+ btn.textContent = y;
7546
+ if (selectedDate && selectedDate.getFullYear() === y) btn.classList.add("is-selected");
7547
+ if (y < decadeStart || y > decadeStart + 9) btn.style.opacity = "0.4";
7548
+ btn.addEventListener("click", () => {
7549
+ viewYear = y;
7550
+ viewMode = "months";
7551
+ render();
7552
+ });
7553
+ grid.appendChild(btn);
7554
+ }
7555
+ popup.appendChild(grid);
7556
+ }
7557
+ };
7558
+ const createDayBtn = (day, outside, date) => {
7559
+ const btn = document.createElement("button");
7560
+ btn.type = "button";
7561
+ btn.className = "vd-datepicker-day";
7562
+ btn.textContent = day;
7563
+ if (outside) {
7564
+ btn.classList.add("is-outside");
7565
+ btn.tabIndex = -1;
7566
+ return btn;
7567
+ }
7568
+ if (date && isSameDay(date, today)) btn.classList.add("is-today");
7569
+ if (date && isSameDay(date, selectedDate)) btn.classList.add("is-selected");
7570
+ if (date && isDisabled(date)) {
7571
+ btn.classList.add("is-disabled");
7572
+ return btn;
7573
+ }
7574
+ if (date) {
7575
+ btn.addEventListener("click", () => {
7576
+ selectedDate = date;
7577
+ viewYear = date.getFullYear();
7578
+ viewMonth = date.getMonth();
7579
+ input.value = formatDate(date);
7580
+ close();
7581
+ input.dispatchEvent(new CustomEvent("datepicker:select", {
7582
+ detail: { date, formatted: input.value },
7583
+ bubbles: true
7584
+ }));
7585
+ input.dispatchEvent(new Event("change", { bubbles: true }));
7586
+ });
7587
+ }
7588
+ return btn;
7589
+ };
7590
+ const open = () => {
7591
+ render();
7592
+ popup.classList.add("is-open");
7593
+ input.setAttribute("aria-expanded", "true");
7594
+ };
7595
+ const close = () => {
7596
+ popup.classList.remove("is-open");
7597
+ input.setAttribute("aria-expanded", "false");
7598
+ viewMode = "days";
7599
+ };
7600
+ const focusHandler = () => open();
7601
+ const outsideHandler = (e) => {
7602
+ if (!wrapper.contains(e.target)) close();
7603
+ };
7604
+ const escHandler = (e) => {
7605
+ if (e.key === "Escape") close();
7606
+ };
7607
+ input.addEventListener("focus", focusHandler);
7608
+ document.addEventListener("click", outsideHandler, true);
7609
+ document.addEventListener("keydown", escHandler);
7610
+ input.setAttribute("aria-haspopup", "dialog");
7611
+ input.setAttribute("aria-expanded", "false");
7612
+ input.setAttribute("autocomplete", "off");
7613
+ cleanup.push(
7614
+ () => input.removeEventListener("focus", focusHandler),
7615
+ () => document.removeEventListener("click", outsideHandler, true),
7616
+ () => document.removeEventListener("keydown", escHandler)
7617
+ );
7618
+ this.instances.set(input, { cleanup, open, close, popup });
7619
+ },
7620
+ destroy: function(el) {
7621
+ const instance = this.instances.get(el);
7622
+ if (!instance) return;
7623
+ instance.cleanup.forEach((fn) => fn());
7624
+ this.instances.delete(el);
7625
+ },
7626
+ destroyAll: function() {
7627
+ this.instances.forEach((_, el) => this.destroy(el));
7628
+ }
7629
+ };
7630
+ if (typeof window.Vanduo !== "undefined") {
7631
+ window.Vanduo.register("datepicker", Datepicker);
7632
+ }
7633
+ window.VanduoDatepicker = Datepicker;
7634
+ })();
7635
+
7636
+ // js/components/timepicker.js
7637
+ (function() {
7638
+ "use strict";
7639
+ const Timepicker = {
7640
+ instances: /* @__PURE__ */ new Map(),
7641
+ init: function() {
7642
+ const inputs = document.querySelectorAll("[data-vd-timepicker]");
7643
+ inputs.forEach((el) => {
7644
+ if (this.instances.has(el)) return;
7645
+ this.initInstance(el);
7646
+ });
7647
+ },
7648
+ initInstance: function(input) {
7649
+ const cleanup = [];
7650
+ const is24h = input.getAttribute("data-vd-timepicker-format") === "24h";
7651
+ const step = parseInt(input.getAttribute("data-vd-timepicker-step") || "30", 10);
7652
+ let wrapper = input.closest(".vd-suggest-wrapper");
7653
+ if (!wrapper) {
7654
+ wrapper = document.createElement("div");
7655
+ wrapper.style.position = "relative";
7656
+ wrapper.style.display = "inline-block";
7657
+ input.parentNode.insertBefore(wrapper, input);
7658
+ wrapper.appendChild(input);
7659
+ }
7660
+ const popup = document.createElement("div");
7661
+ popup.className = "vd-timepicker-popup";
7662
+ popup.setAttribute("role", "listbox");
7663
+ wrapper.appendChild(popup);
7664
+ const times = [];
7665
+ for (let h = 0; h < 24; h++) {
7666
+ for (let m = 0; m < 60; m += step) {
7667
+ const hh24 = String(h).padStart(2, "0");
7668
+ const mm = String(m).padStart(2, "0");
7669
+ if (is24h) {
7670
+ times.push({ display: hh24 + ":" + mm, value: hh24 + ":" + mm });
7671
+ } else {
7672
+ const period = h < 12 ? "AM" : "PM";
7673
+ const h12 = h === 0 ? 12 : h > 12 ? h - 12 : h;
7674
+ const display = h12 + ":" + mm + " " + period;
7675
+ times.push({ display, value: hh24 + ":" + mm });
7676
+ }
7677
+ }
7678
+ }
7679
+ const render = () => {
7680
+ popup.innerHTML = "";
7681
+ times.forEach((t) => {
7682
+ const item = document.createElement("div");
7683
+ item.className = "vd-timepicker-item";
7684
+ item.setAttribute("role", "option");
7685
+ item.textContent = t.display;
7686
+ if (input.value === t.value || input.value === t.display) {
7687
+ item.classList.add("is-selected");
7688
+ }
7689
+ item.addEventListener("click", () => {
7690
+ input.value = t.display;
7691
+ popup.querySelectorAll(".vd-timepicker-item").forEach((i) => i.classList.remove("is-selected"));
7692
+ item.classList.add("is-selected");
7693
+ close();
7694
+ input.dispatchEvent(new CustomEvent("timepicker:select", {
7695
+ detail: { display: t.display, value: t.value },
7696
+ bubbles: true
7697
+ }));
7698
+ input.dispatchEvent(new Event("change", { bubbles: true }));
7699
+ });
7700
+ popup.appendChild(item);
7701
+ });
7702
+ };
7703
+ const open = () => {
7704
+ render();
7705
+ popup.classList.add("is-open");
7706
+ input.setAttribute("aria-expanded", "true");
7707
+ const selected = popup.querySelector(".is-selected");
7708
+ if (selected) selected.scrollIntoView({ block: "center" });
7709
+ };
7710
+ const close = () => {
7711
+ popup.classList.remove("is-open");
7712
+ input.setAttribute("aria-expanded", "false");
7713
+ };
7714
+ const focusHandler = () => open();
7715
+ const outsideHandler = (e) => {
7716
+ if (!wrapper.contains(e.target)) close();
7717
+ };
7718
+ const escHandler = (e) => {
7719
+ if (e.key === "Escape") close();
7720
+ };
7721
+ input.addEventListener("focus", focusHandler);
7722
+ document.addEventListener("click", outsideHandler, true);
7723
+ document.addEventListener("keydown", escHandler);
7724
+ input.setAttribute("aria-haspopup", "listbox");
7725
+ input.setAttribute("aria-expanded", "false");
7726
+ input.setAttribute("autocomplete", "off");
7727
+ input.readOnly = true;
7728
+ cleanup.push(
7729
+ () => input.removeEventListener("focus", focusHandler),
7730
+ () => document.removeEventListener("click", outsideHandler, true),
7731
+ () => document.removeEventListener("keydown", escHandler)
7732
+ );
7733
+ this.instances.set(input, { cleanup, open, close });
7734
+ },
7735
+ destroy: function(el) {
7736
+ const instance = this.instances.get(el);
7737
+ if (!instance) return;
7738
+ instance.cleanup.forEach((fn) => fn());
7739
+ this.instances.delete(el);
7740
+ },
7741
+ destroyAll: function() {
7742
+ this.instances.forEach((_, el) => this.destroy(el));
7743
+ }
7744
+ };
7745
+ if (typeof window.Vanduo !== "undefined") {
7746
+ window.Vanduo.register("timepicker", Timepicker);
7747
+ }
7748
+ window.VanduoTimepicker = Timepicker;
7749
+ })();
7750
+
7751
+ // js/components/stepper.js
7752
+ (function() {
7753
+ "use strict";
7754
+ const Stepper = {
7755
+ instances: /* @__PURE__ */ new Map(),
7756
+ init: function() {
7757
+ const steppers = document.querySelectorAll(".vd-stepper");
7758
+ steppers.forEach((el) => {
7759
+ if (this.instances.has(el)) return;
7760
+ this.initInstance(el);
7761
+ });
7762
+ },
7763
+ initInstance: function(el) {
7764
+ const cleanup = [];
7765
+ const items = Array.from(el.querySelectorAll(".vd-stepper-item"));
7766
+ const isClickable = el.classList.contains("vd-stepper-clickable");
7767
+ let currentIndex = items.findIndex((i) => i.classList.contains("is-active"));
7768
+ if (currentIndex === -1) currentIndex = 0;
7769
+ const setStep = (index) => {
7770
+ if (index < 0 || index >= items.length) return;
7771
+ const prev = currentIndex;
7772
+ currentIndex = index;
7773
+ items.forEach((item, i) => {
7774
+ item.classList.remove("is-active", "is-completed");
7775
+ if (i < index) item.classList.add("is-completed");
7776
+ else if (i === index) item.classList.add("is-active");
7777
+ });
7778
+ el.dispatchEvent(new CustomEvent("stepper:change", {
7779
+ detail: { current: index, previous: prev, total: items.length },
7780
+ bubbles: true
7781
+ }));
7782
+ };
7783
+ if (isClickable) {
7784
+ items.forEach((item, i) => {
7785
+ const handler = () => setStep(i);
7786
+ item.addEventListener("click", handler);
7787
+ cleanup.push(() => item.removeEventListener("click", handler));
7788
+ });
7789
+ }
7790
+ setStep(currentIndex);
7791
+ this.instances.set(el, {
7792
+ cleanup,
7793
+ setStep,
7794
+ next: () => setStep(currentIndex + 1),
7795
+ prev: () => setStep(currentIndex - 1),
7796
+ getCurrent: () => currentIndex
7797
+ });
7798
+ },
7799
+ setStep: function(el, index) {
7800
+ const inst = this.instances.get(el);
7801
+ if (inst) inst.setStep(index);
7802
+ },
7803
+ next: function(el) {
7804
+ const inst = this.instances.get(el);
7805
+ if (inst) inst.next();
7806
+ },
7807
+ prev: function(el) {
7808
+ const inst = this.instances.get(el);
7809
+ if (inst) inst.prev();
7810
+ },
7811
+ destroy: function(el) {
7812
+ const inst = this.instances.get(el);
7813
+ if (!inst) return;
7814
+ inst.cleanup.forEach((fn) => fn());
7815
+ this.instances.delete(el);
7816
+ },
7817
+ destroyAll: function() {
7818
+ this.instances.forEach((_, el) => this.destroy(el));
7819
+ }
7820
+ };
7821
+ if (typeof window.Vanduo !== "undefined") {
7822
+ window.Vanduo.register("stepper", Stepper);
7823
+ }
7824
+ window.VanduoStepper = Stepper;
7825
+ })();
7826
+
7827
+ // js/components/rating.js
7828
+ (function() {
7829
+ "use strict";
7830
+ const Rating = {
7831
+ instances: /* @__PURE__ */ new Map(),
7832
+ init: function() {
7833
+ const ratings = document.querySelectorAll("[data-vd-rating]");
7834
+ ratings.forEach((el) => {
7835
+ if (this.instances.has(el)) return;
7836
+ this.initInstance(el);
7837
+ });
7838
+ },
7839
+ initInstance: function(el) {
7840
+ const cleanup = [];
7841
+ const max = parseInt(el.getAttribute("data-vd-rating-max") || "5", 10);
7842
+ const initialValue = parseFloat(el.getAttribute("data-vd-rating-value") || "0");
7843
+ const readonly = el.classList.contains("vd-rating-readonly") || el.hasAttribute("data-vd-rating-readonly");
7844
+ let currentValue = initialValue;
7845
+ el.classList.add("vd-rating");
7846
+ el.setAttribute("role", "radiogroup");
7847
+ el.setAttribute("aria-label", el.getAttribute("aria-label") || "Rating");
7848
+ el.innerHTML = "";
7849
+ const stars = [];
7850
+ for (let i = 1; i <= max; i++) {
7851
+ const star = document.createElement("button");
7852
+ star.type = "button";
7853
+ star.className = "vd-rating-star";
7854
+ star.setAttribute("role", "radio");
7855
+ star.setAttribute("aria-label", i + " star" + (i > 1 ? "s" : ""));
7856
+ star.setAttribute("aria-checked", i <= currentValue ? "true" : "false");
7857
+ if (readonly) star.tabIndex = -1;
7858
+ stars.push(star);
7859
+ el.appendChild(star);
7860
+ }
7861
+ const valueDisplay = document.createElement("span");
7862
+ valueDisplay.className = "vd-rating-value";
7863
+ valueDisplay.textContent = currentValue > 0 ? currentValue.toString() : "";
7864
+ el.appendChild(valueDisplay);
7865
+ const updateStars = (value) => {
7866
+ stars.forEach((star, i) => {
7867
+ star.classList.remove("is-active", "is-half");
7868
+ const starNum = i + 1;
7869
+ if (starNum <= Math.floor(value)) {
7870
+ star.classList.add("is-active");
7871
+ } else if (starNum - 0.5 <= value) {
7872
+ star.classList.add("is-half");
7873
+ }
7874
+ star.setAttribute("aria-checked", starNum <= value ? "true" : "false");
7875
+ });
7876
+ valueDisplay.textContent = value > 0 ? value.toString() : "";
7877
+ };
7878
+ updateStars(currentValue);
7879
+ if (!readonly) {
7880
+ stars.forEach((star, i) => {
7881
+ const enterHandler = () => {
7882
+ stars.forEach((s, j) => {
7883
+ s.classList.toggle("is-hovered", j <= i);
7884
+ });
7885
+ };
7886
+ const leaveHandler = () => {
7887
+ stars.forEach((s) => s.classList.remove("is-hovered"));
7888
+ };
7889
+ const clickHandler = () => {
7890
+ currentValue = i + 1;
7891
+ el.setAttribute("data-vd-rating-value", currentValue);
7892
+ updateStars(currentValue);
7893
+ el.dispatchEvent(new CustomEvent("rating:change", {
7894
+ detail: { value: currentValue, max },
7895
+ bubbles: true
7896
+ }));
7897
+ };
7898
+ star.addEventListener("mouseenter", enterHandler);
7899
+ star.addEventListener("mouseleave", leaveHandler);
7900
+ star.addEventListener("click", clickHandler);
7901
+ cleanup.push(
7902
+ () => star.removeEventListener("mouseenter", enterHandler),
7903
+ () => star.removeEventListener("mouseleave", leaveHandler),
7904
+ () => star.removeEventListener("click", clickHandler)
7905
+ );
7906
+ });
7907
+ const keyHandler = (e) => {
7908
+ if (e.key === "ArrowRight" || e.key === "ArrowUp") {
7909
+ e.preventDefault();
7910
+ if (currentValue < max) {
7911
+ currentValue++;
7912
+ updateStars(currentValue);
7913
+ stars[currentValue - 1].focus();
7914
+ el.dispatchEvent(new CustomEvent("rating:change", { detail: { value: currentValue, max }, bubbles: true }));
7915
+ }
7916
+ } else if (e.key === "ArrowLeft" || e.key === "ArrowDown") {
7917
+ e.preventDefault();
7918
+ if (currentValue > 1) {
7919
+ currentValue--;
7920
+ updateStars(currentValue);
7921
+ stars[currentValue - 1].focus();
7922
+ el.dispatchEvent(new CustomEvent("rating:change", { detail: { value: currentValue, max }, bubbles: true }));
7923
+ }
7924
+ }
7925
+ };
7926
+ el.addEventListener("keydown", keyHandler);
7927
+ cleanup.push(() => el.removeEventListener("keydown", keyHandler));
7928
+ }
7929
+ this.instances.set(el, {
7930
+ cleanup,
7931
+ getValue: () => currentValue,
7932
+ setValue: (v) => {
7933
+ currentValue = v;
7934
+ updateStars(v);
7935
+ }
7936
+ });
7937
+ },
7938
+ getValue: function(el) {
7939
+ const inst = this.instances.get(el);
7940
+ return inst ? inst.getValue() : 0;
7941
+ },
7942
+ setValue: function(el, value) {
7943
+ const inst = this.instances.get(el);
7944
+ if (inst) inst.setValue(value);
7945
+ },
7946
+ destroy: function(el) {
7947
+ const inst = this.instances.get(el);
7948
+ if (!inst) return;
7949
+ inst.cleanup.forEach((fn) => fn());
7950
+ this.instances.delete(el);
7951
+ },
7952
+ destroyAll: function() {
7953
+ this.instances.forEach((_, el) => this.destroy(el));
7954
+ }
7955
+ };
7956
+ if (typeof window.Vanduo !== "undefined") {
7957
+ window.Vanduo.register("rating", Rating);
7958
+ }
7959
+ window.VanduoRating = Rating;
7960
+ })();
7961
+
7962
+ // js/components/transfer.js
7963
+ (function() {
7964
+ "use strict";
7965
+ const Transfer = {
7966
+ instances: /* @__PURE__ */ new Map(),
7967
+ init: function() {
7968
+ const transfers = document.querySelectorAll("[data-vd-transfer]");
7969
+ transfers.forEach((el) => {
7970
+ if (this.instances.has(el)) return;
7971
+ this.initInstance(el);
7972
+ });
7973
+ },
7974
+ initInstance: function(el) {
7975
+ const cleanup = [];
7976
+ el.classList.add("vd-transfer");
7977
+ let sourceData, targetData;
7978
+ try {
7979
+ const raw = JSON.parse(el.getAttribute("data-vd-transfer") || "[]");
7980
+ sourceData = raw.map((item, i) => ({
7981
+ id: item.id || "item-" + i,
7982
+ label: item.label || item.text || String(item),
7983
+ selected: false
7984
+ }));
7985
+ } catch (_e) {
7986
+ sourceData = [];
7987
+ }
7988
+ targetData = [];
7989
+ const sourceSelected = /* @__PURE__ */ new Set();
7990
+ const targetSelected = /* @__PURE__ */ new Set();
7991
+ const render = () => {
7992
+ el.innerHTML = "";
7993
+ const sourcePanel = createPanel("Source", sourceData, sourceSelected, "source");
7994
+ const actions = document.createElement("div");
7995
+ actions.className = "vd-transfer-actions";
7996
+ const moveRightBtn = document.createElement("button");
7997
+ moveRightBtn.type = "button";
7998
+ moveRightBtn.className = "vd-transfer-btn";
7999
+ moveRightBtn.innerHTML = "&#8250;";
8000
+ moveRightBtn.setAttribute("aria-label", "Move to target");
8001
+ moveRightBtn.disabled = sourceSelected.size === 0;
8002
+ moveRightBtn.addEventListener("click", () => moveRight());
8003
+ const moveLeftBtn = document.createElement("button");
8004
+ moveLeftBtn.type = "button";
8005
+ moveLeftBtn.className = "vd-transfer-btn";
8006
+ moveLeftBtn.innerHTML = "&#8249;";
8007
+ moveLeftBtn.setAttribute("aria-label", "Move to source");
8008
+ moveLeftBtn.disabled = targetSelected.size === 0;
8009
+ moveLeftBtn.addEventListener("click", () => moveLeft());
8010
+ actions.appendChild(moveRightBtn);
8011
+ actions.appendChild(moveLeftBtn);
8012
+ const targetPanel = createPanel("Target", targetData, targetSelected, "target");
8013
+ el.appendChild(sourcePanel);
8014
+ el.appendChild(actions);
8015
+ el.appendChild(targetPanel);
8016
+ };
8017
+ const createPanel = (title, data, selected, _side) => {
8018
+ const panel = document.createElement("div");
8019
+ panel.className = "vd-transfer-panel";
8020
+ const header = document.createElement("div");
8021
+ header.className = "vd-transfer-header";
8022
+ const titleSpan = document.createElement("span");
8023
+ titleSpan.textContent = title;
8024
+ const count = document.createElement("span");
8025
+ count.className = "vd-transfer-count";
8026
+ count.textContent = selected.size + "/" + data.length;
8027
+ header.appendChild(titleSpan);
8028
+ header.appendChild(count);
8029
+ panel.appendChild(header);
8030
+ const searchDiv = document.createElement("div");
8031
+ searchDiv.className = "vd-transfer-search";
8032
+ const searchInput = document.createElement("input");
8033
+ searchInput.type = "text";
8034
+ searchInput.placeholder = "Search...";
8035
+ searchInput.setAttribute("aria-label", "Search " + title.toLowerCase());
8036
+ searchDiv.appendChild(searchInput);
8037
+ panel.appendChild(searchDiv);
8038
+ const list = document.createElement("ul");
8039
+ list.className = "vd-transfer-list";
8040
+ list.setAttribute("role", "listbox");
8041
+ const renderList = (filter) => {
8042
+ list.innerHTML = "";
8043
+ const filtered = filter ? data.filter((d) => {
8044
+ const label = (d.label || d.text || String(d)).toLowerCase();
8045
+ return label.includes(filter.toLowerCase());
8046
+ }) : data;
8047
+ filtered.forEach((item) => {
8048
+ const li = document.createElement("li");
8049
+ li.className = "vd-transfer-item";
8050
+ li.setAttribute("role", "option");
8051
+ if (selected.has(item.id)) li.classList.add("is-selected");
8052
+ const checkbox = document.createElement("input");
8053
+ checkbox.type = "checkbox";
8054
+ checkbox.checked = selected.has(item.id);
8055
+ checkbox.setAttribute("aria-label", item.label);
8056
+ const label = document.createElement("span");
8057
+ label.textContent = item.label;
8058
+ li.addEventListener("click", () => {
8059
+ if (selected.has(item.id)) selected.delete(item.id);
8060
+ else selected.add(item.id);
8061
+ render();
8062
+ });
8063
+ li.appendChild(checkbox);
8064
+ li.appendChild(label);
8065
+ list.appendChild(li);
8066
+ });
8067
+ };
8068
+ searchInput.addEventListener("input", () => renderList(searchInput.value));
8069
+ renderList("");
8070
+ panel.appendChild(list);
8071
+ return panel;
8072
+ };
8073
+ const moveRight = () => {
8074
+ const toMove = sourceData.filter((d) => sourceSelected.has(d.id));
8075
+ sourceData = sourceData.filter((d) => !sourceSelected.has(d.id));
8076
+ targetData = targetData.concat(toMove);
8077
+ sourceSelected.clear();
8078
+ render();
8079
+ fireChange();
8080
+ };
8081
+ const moveLeft = () => {
8082
+ const toMove = targetData.filter((d) => targetSelected.has(d.id));
8083
+ targetData = targetData.filter((d) => !targetSelected.has(d.id));
8084
+ sourceData = sourceData.concat(toMove);
8085
+ targetSelected.clear();
8086
+ render();
8087
+ fireChange();
8088
+ };
8089
+ const fireChange = () => {
8090
+ el.dispatchEvent(new CustomEvent("transfer:change", {
8091
+ detail: {
8092
+ source: sourceData.map((d) => d.id),
8093
+ target: targetData.map((d) => d.id)
8094
+ },
8095
+ bubbles: true
8096
+ }));
8097
+ };
8098
+ render();
8099
+ this.instances.set(el, {
8100
+ cleanup,
8101
+ getTarget: () => targetData.map((d) => d.id),
8102
+ getSource: () => sourceData.map((d) => d.id)
8103
+ });
8104
+ },
8105
+ getSelected: function(el) {
8106
+ const inst = this.instances.get(el);
8107
+ return inst ? inst.getTarget() : [];
8108
+ },
8109
+ destroy: function(el) {
8110
+ const inst = this.instances.get(el);
8111
+ if (!inst) return;
8112
+ inst.cleanup.forEach((fn) => fn());
8113
+ el.innerHTML = "";
8114
+ this.instances.delete(el);
8115
+ },
8116
+ destroyAll: function() {
8117
+ this.instances.forEach((_, el) => this.destroy(el));
8118
+ }
8119
+ };
8120
+ if (typeof window.Vanduo !== "undefined") {
8121
+ window.Vanduo.register("transfer", Transfer);
8122
+ }
8123
+ window.VanduoTransfer = Transfer;
8124
+ })();
8125
+
8126
+ // js/components/tree.js
8127
+ (function() {
8128
+ "use strict";
8129
+ const Tree = {
8130
+ instances: /* @__PURE__ */ new Map(),
8131
+ init: function() {
8132
+ const trees = document.querySelectorAll("[data-vd-tree]");
8133
+ trees.forEach((el) => {
8134
+ if (this.instances.has(el)) return;
8135
+ this.initInstance(el);
8136
+ });
8137
+ },
8138
+ initInstance: function(el) {
8139
+ const cleanup = [];
8140
+ const cascade = el.getAttribute("data-vd-tree-cascade") !== "false";
8141
+ let data;
8142
+ try {
8143
+ data = JSON.parse(el.getAttribute("data-vd-tree") || "[]");
8144
+ } catch (_e) {
8145
+ data = [];
8146
+ }
8147
+ el.classList.add("vd-tree");
8148
+ el.setAttribute("role", "tree");
8149
+ const render = (items, parent) => {
8150
+ parent.innerHTML = "";
8151
+ items.forEach((item) => {
8152
+ const node = document.createElement("li");
8153
+ node.className = "vd-tree-node";
8154
+ node.setAttribute("role", "treeitem");
8155
+ node.setAttribute("aria-expanded", item.open ? "true" : "false");
8156
+ if (item.open) node.classList.add("is-open");
8157
+ const content = document.createElement("div");
8158
+ content.className = "vd-tree-node-content";
8159
+ if (item.children && item.children.length > 0) {
8160
+ const toggle = document.createElement("button");
8161
+ toggle.type = "button";
8162
+ toggle.className = "vd-tree-toggle";
8163
+ toggle.setAttribute("aria-label", "Toggle");
8164
+ toggle.addEventListener("click", (e) => {
8165
+ e.stopPropagation();
8166
+ item.open = !item.open;
8167
+ node.classList.toggle("is-open");
8168
+ node.setAttribute("aria-expanded", item.open ? "true" : "false");
8169
+ });
8170
+ content.appendChild(toggle);
8171
+ } else {
8172
+ const ph = document.createElement("span");
8173
+ ph.className = "vd-tree-toggle-placeholder";
8174
+ content.appendChild(ph);
8175
+ }
8176
+ if (el.hasAttribute("data-vd-tree-checkbox")) {
8177
+ const cb = document.createElement("input");
8178
+ cb.type = "checkbox";
8179
+ cb.className = "vd-tree-checkbox";
8180
+ cb.checked = !!item.checked;
8181
+ cb.setAttribute("aria-label", item.label);
8182
+ cb.addEventListener("change", (e) => {
8183
+ e.stopPropagation();
8184
+ item.checked = cb.checked;
8185
+ if (cascade && item.children) {
8186
+ setChildChecked(item.children, cb.checked);
8187
+ render(data, el);
8188
+ }
8189
+ el.dispatchEvent(new CustomEvent("tree:check", {
8190
+ detail: { id: item.id, checked: cb.checked, label: item.label },
8191
+ bubbles: true
8192
+ }));
8193
+ });
8194
+ content.appendChild(cb);
8195
+ }
8196
+ if (item.icon) {
8197
+ const icon = document.createElement("span");
8198
+ icon.className = "vd-tree-icon " + item.icon;
8199
+ content.appendChild(icon);
8200
+ }
8201
+ const label = document.createElement("span");
8202
+ label.className = "vd-tree-label";
8203
+ label.textContent = item.label || "";
8204
+ content.appendChild(label);
8205
+ node.appendChild(content);
8206
+ if (item.children && item.children.length > 0) {
8207
+ const childList = document.createElement("ul");
8208
+ childList.className = "vd-tree-children";
8209
+ childList.setAttribute("role", "group");
8210
+ render(item.children, childList);
8211
+ node.appendChild(childList);
8212
+ }
8213
+ parent.appendChild(node);
8214
+ });
8215
+ };
8216
+ const setChildChecked = (items, checked) => {
8217
+ items.forEach((item) => {
8218
+ item.checked = checked;
8219
+ if (item.children) setChildChecked(item.children, checked);
8220
+ });
8221
+ };
8222
+ const keyHandler = (e) => {
8223
+ const focused = document.activeElement;
8224
+ if (!el.contains(focused)) return;
8225
+ const nodes = Array.from(el.querySelectorAll(".vd-tree-node-content"));
8226
+ const idx = nodes.indexOf(focused.closest(".vd-tree-node-content"));
8227
+ if (idx === -1) return;
8228
+ switch (e.key) {
8229
+ case "ArrowDown":
8230
+ e.preventDefault();
8231
+ if (idx < nodes.length - 1) {
8232
+ const next = nodes[idx + 1].querySelector(".vd-tree-toggle, .vd-tree-label");
8233
+ if (next) next.focus();
8234
+ }
8235
+ break;
8236
+ case "ArrowUp":
8237
+ e.preventDefault();
8238
+ if (idx > 0) {
8239
+ const prev = nodes[idx - 1].querySelector(".vd-tree-toggle, .vd-tree-label");
8240
+ if (prev) prev.focus();
8241
+ }
8242
+ break;
8243
+ }
8244
+ };
8245
+ el.addEventListener("keydown", keyHandler);
8246
+ cleanup.push(() => el.removeEventListener("keydown", keyHandler));
8247
+ render(data, el);
8248
+ this.instances.set(el, {
8249
+ cleanup,
8250
+ getData: () => data,
8251
+ getChecked: () => {
8252
+ const checked = [];
8253
+ const collect = (items) => {
8254
+ items.forEach((i) => {
8255
+ if (i.checked) checked.push(i.id || i.label);
8256
+ if (i.children) collect(i.children);
8257
+ });
8258
+ };
8259
+ collect(data);
8260
+ return checked;
8261
+ }
8262
+ });
8263
+ },
8264
+ getChecked: function(el) {
8265
+ const inst = this.instances.get(el);
8266
+ return inst ? inst.getChecked() : [];
8267
+ },
8268
+ destroy: function(el) {
8269
+ const inst = this.instances.get(el);
8270
+ if (!inst) return;
8271
+ inst.cleanup.forEach((fn) => fn());
8272
+ el.innerHTML = "";
8273
+ this.instances.delete(el);
8274
+ },
8275
+ destroyAll: function() {
8276
+ this.instances.forEach((_, el) => this.destroy(el));
8277
+ }
8278
+ };
8279
+ if (typeof window.Vanduo !== "undefined") {
8280
+ window.Vanduo.register("tree", Tree);
8281
+ }
8282
+ window.VanduoTree = Tree;
8283
+ })();
8284
+
8285
+ // js/components/spotlight.js
8286
+ (function() {
8287
+ "use strict";
8288
+ const Spotlight = {
8289
+ _active: false,
8290
+ _steps: [],
8291
+ _currentStep: 0,
8292
+ _elements: {},
8293
+ _cleanup: [],
8294
+ _boundTriggers: /* @__PURE__ */ new WeakMap(),
8295
+ _triggerElement: null,
8296
+ init: function() {
8297
+ const triggers = document.querySelectorAll("[data-vd-spotlight]");
8298
+ triggers.forEach((trigger) => {
8299
+ if (this._boundTriggers.has(trigger)) return;
8300
+ const clickHandler = (event) => {
8301
+ event.preventDefault();
8302
+ const steps = this._parseSteps(trigger.getAttribute("data-vd-spotlight"));
8303
+ if (steps.length === 0) return;
8304
+ this.start(steps, { trigger });
8305
+ };
8306
+ trigger.addEventListener("click", clickHandler);
8307
+ this._boundTriggers.set(trigger, clickHandler);
8308
+ });
8309
+ },
8310
+ _parseSteps: function(raw) {
8311
+ if (typeof raw !== "string" || raw.trim() === "") return [];
8312
+ try {
8313
+ const parsed = JSON.parse(raw);
8314
+ return this._normalizeSteps(parsed);
8315
+ } catch (error) {
8316
+ console.error("VanduoSpotlight: invalid data-vd-spotlight payload.", error);
8317
+ return [];
8318
+ }
8319
+ },
8320
+ _normalizeStep: function(step) {
8321
+ if (!step || typeof step !== "object") return null;
8322
+ const target = step.target;
8323
+ const hasSelectorTarget = typeof target === "string" && target.trim() !== "";
8324
+ const hasElementTarget = typeof Element !== "undefined" && target instanceof Element;
8325
+ if (!hasSelectorTarget && !hasElementTarget) return null;
8326
+ const title = typeof step.title === "string" ? step.title : "";
8327
+ const description = typeof step.description === "string" ? step.description : typeof step.content === "string" ? step.content : "";
8328
+ return {
8329
+ target,
8330
+ title,
8331
+ description
8332
+ };
8333
+ },
8334
+ _normalizeSteps: function(steps) {
8335
+ if (!Array.isArray(steps)) return [];
8336
+ return steps.map((step) => this._normalizeStep(step)).filter(Boolean);
8337
+ },
8338
+ start: function(steps, options) {
8339
+ if (this._active) this.stop();
8340
+ const normalizedSteps = this._normalizeSteps(steps);
8341
+ if (normalizedSteps.length === 0) return;
8342
+ const startOptions = options || {};
8343
+ this._steps = normalizedSteps;
8344
+ this._currentStep = 0;
8345
+ this._active = true;
8346
+ this._triggerElement = startOptions.trigger || (document.activeElement instanceof HTMLElement ? document.activeElement : null);
8347
+ const overlay = document.createElement("div");
8348
+ overlay.className = "vd-spotlight-overlay";
8349
+ overlay.setAttribute("aria-hidden", "true");
8350
+ document.body.appendChild(overlay);
8351
+ const tooltip = document.createElement("div");
8352
+ tooltip.className = "vd-spotlight-tooltip";
8353
+ tooltip.setAttribute("role", "dialog");
8354
+ tooltip.setAttribute("aria-modal", "true");
8355
+ tooltip.tabIndex = -1;
8356
+ document.body.appendChild(tooltip);
8357
+ this._elements = { overlay, tooltip };
8358
+ const escHandler = (e) => {
8359
+ if (e.key === "Escape") this.stop();
8360
+ };
8361
+ document.addEventListener("keydown", escHandler);
8362
+ this._cleanup.push(() => document.removeEventListener("keydown", escHandler));
8363
+ overlay.addEventListener("click", () => this.stop());
8364
+ this._showStep(this._currentStep);
8365
+ },
8366
+ _showStep: function(index) {
8367
+ const step = this._steps[index];
8368
+ if (!step) return;
8369
+ const target = typeof step.target === "string" ? document.querySelector(step.target) : step.target;
8370
+ const { tooltip } = this._elements;
8371
+ document.querySelectorAll(".vd-spotlight-target").forEach((el) => {
8372
+ el.classList.remove("vd-spotlight-target");
8373
+ });
8374
+ if (target) {
8375
+ target.classList.add("vd-spotlight-target");
8376
+ target.scrollIntoView({ behavior: "smooth", block: "center" });
8377
+ }
8378
+ const total = this._steps.length;
8379
+ tooltip.innerHTML = "";
8380
+ tooltip.removeAttribute("aria-labelledby");
8381
+ tooltip.removeAttribute("aria-describedby");
8382
+ if (step.title) {
8383
+ const title = document.createElement("h4");
8384
+ title.className = "vd-spotlight-title";
8385
+ title.id = "vd-spotlight-title-" + index + "-" + Date.now();
8386
+ title.textContent = step.title;
8387
+ tooltip.appendChild(title);
8388
+ tooltip.setAttribute("aria-labelledby", title.id);
8389
+ }
8390
+ if (step.description) {
8391
+ const desc = document.createElement("p");
8392
+ desc.className = "vd-spotlight-description";
8393
+ desc.id = "vd-spotlight-description-" + index + "-" + Date.now();
8394
+ desc.textContent = step.description;
8395
+ tooltip.appendChild(desc);
8396
+ tooltip.setAttribute("aria-describedby", desc.id);
8397
+ }
8398
+ const footer = document.createElement("div");
8399
+ footer.className = "vd-spotlight-footer";
8400
+ footer.setAttribute("aria-label", "Step " + (index + 1) + " of " + total);
8401
+ const counter = document.createElement("span");
8402
+ counter.className = "vd-spotlight-counter";
8403
+ counter.textContent = index + 1 + " / " + total;
8404
+ const actions = document.createElement("div");
8405
+ actions.className = "vd-spotlight-actions";
8406
+ if (index > 0) {
8407
+ const prevBtn = document.createElement("button");
8408
+ prevBtn.type = "button";
8409
+ prevBtn.className = "vd-spotlight-btn";
8410
+ prevBtn.textContent = "Back";
8411
+ prevBtn.addEventListener("click", () => this.prev());
8412
+ actions.appendChild(prevBtn);
8413
+ }
8414
+ const skipBtn = document.createElement("button");
8415
+ skipBtn.type = "button";
8416
+ skipBtn.className = "vd-spotlight-btn";
8417
+ skipBtn.textContent = "Skip";
8418
+ skipBtn.addEventListener("click", () => this.stop());
8419
+ actions.appendChild(skipBtn);
8420
+ if (index < total - 1) {
8421
+ const nextBtn = document.createElement("button");
8422
+ nextBtn.type = "button";
8423
+ nextBtn.className = "vd-spotlight-btn vd-spotlight-btn-primary";
8424
+ nextBtn.textContent = "Next";
8425
+ nextBtn.addEventListener("click", () => this.next());
8426
+ actions.appendChild(nextBtn);
8427
+ } else {
8428
+ const doneBtn = document.createElement("button");
8429
+ doneBtn.type = "button";
8430
+ doneBtn.className = "vd-spotlight-btn vd-spotlight-btn-primary";
8431
+ doneBtn.textContent = "Done";
8432
+ doneBtn.addEventListener("click", () => this.stop());
8433
+ actions.appendChild(doneBtn);
8434
+ }
8435
+ footer.appendChild(counter);
8436
+ footer.appendChild(actions);
8437
+ tooltip.appendChild(footer);
8438
+ if (target) {
8439
+ requestAnimationFrame(() => {
8440
+ const rect = target.getBoundingClientRect();
8441
+ const tRect = tooltip.getBoundingClientRect();
8442
+ let top = rect.bottom + 12 + window.scrollY;
8443
+ let left = rect.left + (rect.width - tRect.width) / 2 + window.scrollX;
8444
+ left = Math.max(8, Math.min(left, window.innerWidth - tRect.width - 8));
8445
+ if (top + tRect.height > window.innerHeight + window.scrollY) {
8446
+ top = rect.top - tRect.height - 12 + window.scrollY;
8447
+ }
8448
+ tooltip.style.top = top + "px";
8449
+ tooltip.style.left = left + "px";
8450
+ });
8451
+ }
8452
+ document.dispatchEvent(new CustomEvent("spotlight:step", {
8453
+ detail: { index, step: index, total, data: step }
8454
+ }));
8455
+ },
8456
+ next: function() {
8457
+ if (this._currentStep < this._steps.length - 1) {
8458
+ this._currentStep++;
8459
+ this._showStep(this._currentStep);
8460
+ }
8461
+ },
8462
+ prev: function() {
8463
+ if (this._currentStep > 0) {
8464
+ this._currentStep--;
8465
+ this._showStep(this._currentStep);
8466
+ }
8467
+ },
8468
+ stop: function() {
8469
+ if (!this._active) return;
8470
+ const total = this._steps.length;
8471
+ const detail = {
8472
+ completedSteps: total === 0 ? 0 : Math.min(this._currentStep + 1, total),
8473
+ total,
8474
+ completed: total > 0 && this._currentStep >= total - 1
8475
+ };
8476
+ this._active = false;
8477
+ document.querySelectorAll(".vd-spotlight-target").forEach((el) => {
8478
+ el.classList.remove("vd-spotlight-target");
8479
+ });
8480
+ if (this._elements.overlay && this._elements.overlay.parentNode) {
8481
+ this._elements.overlay.parentNode.removeChild(this._elements.overlay);
8482
+ }
8483
+ if (this._elements.tooltip && this._elements.tooltip.parentNode) {
8484
+ this._elements.tooltip.parentNode.removeChild(this._elements.tooltip);
8485
+ }
8486
+ this._cleanup.forEach((fn) => fn());
8487
+ this._cleanup = [];
8488
+ this._elements = {};
8489
+ this._steps = [];
8490
+ this._currentStep = 0;
8491
+ if (this._triggerElement && this._triggerElement.isConnected && typeof this._triggerElement.focus === "function") {
8492
+ this._triggerElement.focus();
8493
+ }
8494
+ this._triggerElement = null;
8495
+ document.dispatchEvent(new CustomEvent("spotlight:end", { detail }));
8496
+ },
8497
+ destroyAll: function() {
8498
+ this.stop();
8499
+ }
8500
+ };
8501
+ if (typeof window.Vanduo !== "undefined") {
8502
+ window.Vanduo.register("spotlight", Spotlight);
8503
+ }
8504
+ window.VanduoSpotlight = Spotlight;
8505
+ })();
8506
+
6359
8507
  // js/index.js
6360
8508
  var Vanduo = window.Vanduo;
6361
8509
  var index_default = Vanduo;