@vanduo-oss/framework 1.2.6 → 1.2.8

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